diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 98efade0ba..0185aa8077 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -50,6 +50,7 @@ jobs:
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely
pip install src/bcf --no-deps
pip install git+https://github.com/zdhoward/aud
+ pip install pytest-xdist==3.8.0
- name: Install C++ dependencies
run: |
@@ -120,20 +121,23 @@ jobs:
sudo make -j $(nproc)
sudo make install
- - name: Run IfcConvert on Sample files
- run: |
- (find test/input src/bonsai/test/files -name '*.ifc' | while read i; do \
- echo $i | tee -a log; \
- timeout 1m "$(which IfcConvert)" -yv "$i" "$i.obj" --validate >> log 2>&1; \
- echo $i $? >> statuses; \
- done) || true
- echo Failed
- grep -v 0$ statuses
- grep -v 0$ statuses | wc -l
- echo Succeeded
- grep 0$ statuses
- grep 0$ statuses | wc -l
+ # - name: Run IfcConvert on Sample files
+ # run: |
+ # (find test/input src/bonsai/test/files -name '*.ifc' | while read i; do \
+ # echo $i | tee -a log; \
+ # timeout 1m "$(which IfcConvert)" -yv "$i" "$i.obj" --validate >> log 2>&1; \
+ # echo $i $? >> statuses; \
+ # done) || true
+ # echo Failed
+ # grep -v 0$ statuses
+ # grep -v 0$ statuses | wc -l
+ # echo Succeeded
+ # grep 0$ statuses
+ # grep 0$ statuses | wc -l
+ - name: Run IfcConvert on Sample file
+ run: |
+ IfcConvert test/input/acad2010_walls.ifc test/input/acad2010_walls.obj
- name: Test ifcopenshell-python
run: |
@@ -143,7 +147,7 @@ jobs:
mv ifcopenshell ifcopenshell-local # Force testing on installed module
pip install -e ../ifcpatch --no-deps # Needed for sql.py tests.
ERROR=0
- make test || ERROR=1
+ make test-parallel || ERROR=1
cd ../bcf && make test || ERROR=1
pip install requests
cd ../bsdd && make test || ERROR=1
diff --git a/.gitignore b/.gitignore
index 608fdd3e74..37f7ea9c98 100644
--- a/.gitignore
+++ b/.gitignore
@@ -109,4 +109,7 @@ src/bonsai/bonsai/bim/schema/Brick.ttl
bonsaiDecoratorForLoads.code-workspace
dev_environment.bat
-.pixi/
\ No newline at end of file
+.pixi/
+
+src/ifcopenshell-python/ifcopenshell/express/*.exp
+src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat
diff --git a/README.md b/README.md
index 9d5160b4fa..4e7285f53f 100644
--- a/README.md
+++ b/README.md
@@ -37,25 +37,25 @@ Contents
| Name | Description | License | Service |
| ------------------------- | --------------------------------------------------------------------- | ------------------- | ------- |
-| bcf | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [](https://pypi.org/project/bcf-client/) [](https://anaconda.org/conda-forge/bcf-client) |
-| bonsai | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [](https://bonsaibim.org/download.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true) [](https://community.chocolatey.org/packages/blenderbim-nightly/) |
-| bsdd | Library to query the bSDD API | LGPL-3.0-or-later | [](https://pypi.org/project/bsdd/) |
-| ifc2ca | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later |
-| ifc4d | Convert to and from IFC and project management software | LGPL-3.0-or-later | [](https://pypi.org/project/ifc4d/) |
-| ifc5d | Report and optimise cost information from IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifc5d/) |
-| ifcbimtester | Wrapper for Gherkin based unit testing for IFC models | LGPL-3.0-or-later |
+| [bcf](https://docs.ifcopenshell.org/bcf.html) | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [](https://pypi.org/project/bcf-client/) [](https://anaconda.org/conda-forge/bcf-client) |
+| [bonsai](https://docs.ifcopenshell.org/bonsai.html) | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [](https://bonsaibim.org/download.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true) [](https://community.chocolatey.org/packages/blenderbim-nightly/) |
+| [bsdd](https://docs.ifcopenshell.org/bsdd.html) | Library to query the bSDD API | LGPL-3.0-or-later | [](https://pypi.org/project/bsdd/) |
+| [ifc2ca](https://docs.ifcopenshell.org/ifc2ca.html) | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later |
+| [ifc4d](https://docs.ifcopenshell.org/ifc4d.html) | Convert to and from IFC and project management software | LGPL-3.0-or-later | [](https://pypi.org/project/ifc4d/) |
+| [ifc5d](https://docs.ifcopenshell.org/ifc5d.html) | Report and optimise cost information from IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifc5d/) |
+| [ifcbimtester](https://docs.ifcopenshell.org/bimtester.html) | Wrapper for Gherkin based unit testing for IFC models | LGPL-3.0-or-later |
| ifcblender | Historic Blender IFC import add-on | LGPL-3.0-or-later\* |
-| ifccityjson | Convert CityJSON to IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifccityjson/) |
-| ifcclash | Clash detection library and CLI app | LGPL-3.0-or-later | [](https://pypi.org/project/ifcclash/) |
-| ifcconvert | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcconvert/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true)
-| ifccsv | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifccsv/) |
-| ifcdiff | Compare changes between IFC models | LGPL-3.0-or-later | [](https://pypi.org/project/ifcdiff/) |
-| ifcfm | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [](https://pypi.org/project/ifcfm/) |
-| ifcmax | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcmax.html)
-| ifcopenshell-python | Python library for IFC manipulation | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [](https://pypi.org/project/ifcopenshell/) [](https://anaconda.org/conda-forge/ifcopenshell) [](https://anaconda.org/ifcopenshell/ifcopenshell) [](https://hub.docker.com/r/aecgeeks/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell-git) |
-| ifcpatch | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [](https://pypi.org/project/ifcpatch/) |
-| ifcsverchok | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
-| ifctester | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [](https://pypi.org/project/ifctester/) |
+| [ifccityjson](https://docs.ifcopenshell.org/ifccityjson.html) | Convert CityJSON to IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifccityjson/) |
+| [ifcclash](https://docs.ifcopenshell.org/ifcclash.html) | Clash detection library and CLI app | LGPL-3.0-or-later | [](https://pypi.org/project/ifcclash/) |
+| [ifcconvert](https://docs.ifcopenshell.org/ifcconvert.html) | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcconvert/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true)
+| [ifccsv](https://docs.ifcopenshell.org/ifccsv.html) | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifccsv/) |
+| [ifcdiff](https://docs.ifcopenshell.org/ifcdiff.html) | Compare changes between IFC models | LGPL-3.0-or-later | [](https://pypi.org/project/ifcdiff/) |
+| [ifcfm](https://docs.ifcopenshell.org/ifcfm.html) | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [](https://pypi.org/project/ifcfm/) |
+| [ifcmax](https://docs.ifcopenshell.org/ifcmax.html) | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcmax.html)
+| [ifcopenshell-python](https://docs.ifcopenshell.org/ifcopenshell-python.html) | Python library for IFC manipulation | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [](https://pypi.org/project/ifcopenshell/) [](https://anaconda.org/conda-forge/ifcopenshell) [](https://anaconda.org/ifcopenshell/ifcopenshell) [](https://hub.docker.com/r/aecgeeks/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell-git) |
+| [ifcpatch](https://docs.ifcopenshell.org/ifcpatch.html) | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [](https://pypi.org/project/ifcpatch/) |
+| [ifcsverchok](https://docs.ifcopenshell.org/ifcsverchok.html) | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
+| [ifctester](https://docs.ifcopenshell.org/ifctester.html) | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [](https://pypi.org/project/ifctester/) |
The IfcOpenShell C++ codebase is split into multiple interal libraries:
diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt
index 6ccf38cd16..560c3b02b8 100644
--- a/cmake/CMakeLists.txt
+++ b/cmake/CMakeLists.txt
@@ -426,7 +426,7 @@ if(WASM_BUILD)
else()
# @todo review this, shouldn't this be all possible header-only now?
# ... or rewritten using C++17 features?
- set(BOOST_COMPONENTS system program_options regex thread date_time)
+ set(BOOST_COMPONENTS system program_options regex thread date_time iostreams)
endif()
if(USE_MMAP)
@@ -1044,11 +1044,7 @@ if(BUILD_IFCGEOM)
foreach(schema ${SCHEMA_VERSIONS})
set(IFCGEOM_SCHEMA_LIBRARIES ${IFCGEOM_SCHEMA_LIBRARIES} geometry_mapping_ifc${schema})
endforeach()
- if(WASM_BUILD)
- set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
- else()
- set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
- endif()
+ set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
endif()
if(BUILD_CONVERT OR BUILD_IFCPYTHON)
@@ -1177,7 +1173,10 @@ if(BUILD_IFCGEOM)
add_library(geometry_mapping_ifc${schema} STATIC ${IFCGEOM_FILES})
set_target_properties(geometry_mapping_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
- target_link_libraries(geometry_mapping_ifc${schema} IfcParse IfcGeom)
+ target_link_libraries(geometry_mapping_ifc${schema} IfcParse)
+ if (NOT BUILD_SHARED_LIBS)
+ target_link_libraries(geometry_mapping_ifc${schema} IfcGeom)
+ endif()
list(APPEND mapping_libraries geometry_mapping_ifc${schema})
endforeach()
diff --git a/nix/build-all.py b/nix/build-all.py
index 701f1e4858..5226562557 100644
--- a/nix/build-all.py
+++ b/nix/build-all.py
@@ -154,7 +154,7 @@ SWIG_VERSION = "4.1.0"
OPENCOLLADA_VERSION = "v1.6.68"
HDF5_VERSION = "1.13.1"
-GMP_VERSION = "6.2.1"
+GMP_VERSION = "6.3.0"
MPFR_VERSION = "3.1.6" # latest is 4.1.0
CGAL_VERSION = "v5.6.3"
USD_VERSION = "23.05"
diff --git a/src/bonsai/bonsai/bim/data/assets/patterns.svg b/src/bonsai/bonsai/bim/data/assets/patterns.svg
index 11be0ee021..1f62673bda 100644
--- a/src/bonsai/bonsai/bim/data/assets/patterns.svg
+++ b/src/bonsai/bonsai/bim/data/assets/patterns.svg
@@ -1,85 +1,108 @@
diff --git a/src/bonsai/bonsai/bim/data/icons/dm_trim.png b/src/bonsai/bonsai/bim/data/icons/dm_trim.png
new file mode 100644
index 0000000000..235f85935d
Binary files /dev/null and b/src/bonsai/bonsai/bim/data/icons/dm_trim.png differ
diff --git a/src/bonsai/bonsai/bim/data/icons/lm_trim.png b/src/bonsai/bonsai/bim/data/icons/lm_trim.png
new file mode 100644
index 0000000000..ff116439df
Binary files /dev/null and b/src/bonsai/bonsai/bim/data/icons/lm_trim.png differ
diff --git a/src/bonsai/bonsai/bim/data/pset/EPset_Drawing.ifc b/src/bonsai/bonsai/bim/data/pset/EPset_Drawing.ifc
index a516b9765d..70d6049ad7 100644
--- a/src/bonsai/bonsai/bim/data/pset/EPset_Drawing.ifc
+++ b/src/bonsai/bonsai/bim/data/pset/EPset_Drawing.ifc
@@ -13,10 +13,10 @@ DATA;
#6=IFCSIMPLEPROPERTYTEMPLATE('0AK5C2UpL4$eaac2LszAx$',$,'HasUnderlay','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#7=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#8=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
-#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
+#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','Whether or not this drawing can be referenced in other drawings.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','Comma separated list of selector expressions to evaluate for each drawing elementand add results to their ''class'' attribute.\X2\000A\X0\E.g. ''Name, id'' would add to ''class'' value similar to ''Name-Wall id-1220''.\X2\000A\X0\Then it can be used to applied css styles based on the resulting class.\X2\000A\X0\If attribute is not present on the element, then it won''t be added to it''s ''class''.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
-#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
-#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
+#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
+#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#13=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#14=IFCSIMPLEPROPERTYTEMPLATE('3mRF52q81FQB$h4oTh7M45',$,'Markers','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#15=IFCSIMPLEPROPERTYTEMPLATE('1rhr_0N3LDtuORcEJP0KXM',$,'Symbols','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
diff --git a/src/bonsai/bonsai/bim/export_ifc.py b/src/bonsai/bonsai/bim/export_ifc.py
index 5fe5a1df97..d507481141 100644
--- a/src/bonsai/bonsai/bim/export_ifc.py
+++ b/src/bonsai/bonsai/bim/export_ifc.py
@@ -36,6 +36,7 @@ from bonsai.bim.ifc import IfcStore
from mathutils import Vector
from typing import Union
from logging import Logger
+from math import radians
class IfcExporter:
@@ -104,7 +105,16 @@ class IfcExporter:
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
element = self.file.by_id(tool.Blender.get_object_bim_props(obj).ifc_definition_id)
- if tool.Geometry.is_scaled(obj):
+ # Handle camera scales specially
+ if obj.type == "CAMERA":
+ # Check if this is a reflected ceiling plan camera
+ camera = tool.Ifc.get_entity(obj)
+ if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW":
+ # Ensure reflected ceiling cameras have the correct scale
+ if obj.scale != (-1, -1, -1):
+ obj.scale = (-1, -1, -1)
+ # Skip all other scale handling for cameras
+ elif tool.Geometry.is_scaled(obj):
bpy.ops.bim.update_representation(obj=obj.name)
# update_representation might not apply scale if the object has openings
# reset it, so let user know that the scale wasn't saved.
diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py
index 7b10f8c3fd..c7cb10cdde 100644
--- a/src/bonsai/bonsai/bim/handler.py
+++ b/src/bonsai/bonsai/bim/handler.py
@@ -222,8 +222,6 @@ def refresh_ui_data():
if isinstance(ifc_file := tool.Ifc.get(), ifcopenshell.sqlite):
ifc_file.clear_cache()
- props = tool.Drawing.get_document_props()
- props.should_draw_decorations = props.should_draw_decorations
if tool.Web.get_web_props().is_connected:
tool.Web.send_webui_data()
diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py
index 9252ecb94b..1765b1ca24 100644
--- a/src/bonsai/bonsai/bim/import_ifc.py
+++ b/src/bonsai/bonsai/bim/import_ifc.py
@@ -952,6 +952,9 @@ class IfcImporter:
if not props.ifc_file:
props.ifc_file = self.ifc_import_settings.input_file
self.file = tool.Ifc.get()
+ # IFC4 Reference View shall have no booleans https://github.com/BuildingSMART/IFC4-CV/issues/14
+ if self.file.schema == "IFC4" and "ReferenceView" in str(self.file.header.file_description.description):
+ self.ifc_import_settings.void_limit = 0
def calculate_unit_scale(self):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
diff --git a/src/bonsai/bonsai/bim/module/aggregate/operator.py b/src/bonsai/bonsai/bim/module/aggregate/operator.py
index 6e902e76f5..54147c9e34 100644
--- a/src/bonsai/bonsai/bim/module/aggregate/operator.py
+++ b/src/bonsai/bonsai/bim/module/aggregate/operator.py
@@ -94,6 +94,9 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
+ aggregates_to_check = set()
+
+ # First pass: unassign all parts and track their aggregates
for obj in tool.Blender.get_selected_objects():
element = tool.Ifc.get_entity(obj)
if not element:
@@ -101,6 +104,10 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
aggregate = ifcopenshell.util.element.get_aggregate(element)
if not aggregate:
continue
+
+ # Track this aggregate for later checking
+ aggregates_to_check.add(aggregate)
+
core.unassign_object(
tool.Ifc,
tool.Aggregate,
@@ -116,6 +123,29 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
+ # Second pass: delete aggregates that now have no parts
+ deleted_aggregates = []
+ for aggregate in aggregates_to_check:
+ related_objects = ifcopenshell.util.element.get_parts(aggregate)
+ if len(related_objects) == 0:
+ aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
+ deleted_aggregates.append(aggregate_name)
+
+ aggregate_obj = tool.Ifc.get_object(aggregate)
+ if aggregate_obj:
+ ifcopenshell.api.root.remove_product(tool.Ifc.get(), product=aggregate)
+ bpy.data.objects.remove(aggregate_obj, do_unlink=True)
+
+ # Show info message if aggregates were deleted
+ if deleted_aggregates:
+ if len(deleted_aggregates) == 1:
+ self.report(
+ {"INFO"}, f"Aggregate '{deleted_aggregates[0]}' was deleted because it had no remaining parts"
+ )
+ else:
+ aggregate_list = ", ".join(f"'{name}'" for name in deleted_aggregates)
+ self.report({"INFO"}, f"Aggregates {aggregate_list} were deleted because they had no remaining parts")
+
class BIM_OT_enable_editing_aggregate(bpy.types.Operator):
"""Enable editing aggregation relationship"""
diff --git a/src/bonsai/bonsai/bim/module/classification/ui.py b/src/bonsai/bonsai/bim/module/classification/ui.py
index 7cbfb0ed93..344c247af2 100644
--- a/src/bonsai/bonsai/bim/module/classification/ui.py
+++ b/src/bonsai/bonsai/bim/module/classification/ui.py
@@ -282,7 +282,11 @@ class BIM_PT_classification_references(Panel, ReferenceUI):
@classmethod
def poll(cls, context):
- return bool((obj := context.active_object) and tool.Ifc.get_entity(obj))
+ return (
+ (obj := tool.Blender.get_active_object())
+ and (element := tool.Ifc.get_entity(obj))
+ and element.is_a("IfcObjectDefinition")
+ )
def get_object_name(self, context: bpy.types.Context) -> str:
assert (obj := context.active_object)
diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py
index 3b527b198b..0820d5e3d6 100644
--- a/src/bonsai/bonsai/bim/module/debug/operator.py
+++ b/src/bonsai/bonsai/bim/module/debug/operator.py
@@ -818,7 +818,12 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.merge_identical_objects"
bl_label = "Merge Identical Objects"
- bl_description = "For materials currently only IfcMaterials are supported"
+ bl_description = (
+ "Merge identical IFC objects (that match all attributes).\n"
+ "\n"
+ "SHIFT + CLICK to merge by name/identification attribute only.\n"
+ "Merges names with number suffix, as well (ex: foo, foo.001, foo.002)\n"
+ )
bl_options = {"REGISTER", "UNDO"}
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
@@ -826,18 +831,36 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)),
)
+ by_name_or_identification_only: bpy.props.BoolProperty(
+ name="By Name/Identification Only",
+ description="Merge based only on Name or Identification attribute, ignoring other properties",
+ default=False,
+ )
+
if TYPE_CHECKING:
object_type: tool.Debug.PurgeMergeObjectType
+ def invoke(self, context, event):
+ # Check if shift key is pressed
+ if event.shift:
+ self.by_name_or_identification_only = True
+ else:
+ self.by_name_or_identification_only = False
+
+ return self.execute(context)
+
def _execute(self, context):
object_type: str = self.object_type
if object_type in ("PROFILE", "TYPE"):
self.report({"ERROR"}, f"Unsupported object type {object_type}.")
return {"CANCELLED"}
- merged_data = tool.Debug.merge_identical_objects(object_type)
+ merged_data = tool.Debug.merge_identical_objects(
+ object_type, by_name_or_identification_only=self.by_name_or_identification_only
+ )
plural_object_type = f"{object_type.lower().replace('_', ' ')}s"
if merged_data:
+ merge_mode = " by name/identification" if self.by_name_or_identification_only else ""
for element_type, element_names in merged_data.items():
print(f"- {element_type}:")
for name in element_names:
@@ -846,7 +869,8 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
merged = sum(len(v) for v in merged_data.values())
msg = " See system console for details." if merged else ""
- self.report({"INFO"}, f"{merged} identical {plural_object_type} were merged.{msg}")
+ merge_mode = " (by name/identification)" if self.by_name_or_identification_only else ""
+ self.report({"INFO"}, f"{merged} identical {plural_object_type} were merged{merge_mode}.{msg}")
if merged == 0:
return
diff --git a/src/bonsai/bonsai/bim/module/drawing/__init__.py b/src/bonsai/bonsai/bim/module/drawing/__init__.py
index 7ca0e4bd93..187020fc77 100644
--- a/src/bonsai/bonsai/bim/module/drawing/__init__.py
+++ b/src/bonsai/bonsai/bim/module/drawing/__init__.py
@@ -61,6 +61,7 @@ classes = (
operator.EnableEditingAssignedProduct,
operator.EnableEditingElementFilter,
operator.EnableEditingText,
+ operator.ExcludeAnnotation,
operator.ExpandSheet,
operator.LoadDrawings,
operator.LoadReferences,
diff --git a/src/bonsai/bonsai/bim/module/drawing/data.py b/src/bonsai/bonsai/bim/module/drawing/data.py
index b31b1371f4..cd24989981 100644
--- a/src/bonsai/bonsai/bim/module/drawing/data.py
+++ b/src/bonsai/bonsai/bim/module/drawing/data.py
@@ -235,13 +235,18 @@ class DecoratorData:
cut_cache = {}
slice_cache = {}
fill_cache = {}
+ camera_location_checksum = None
+ camera_rotation_checksum = None
+
+ @classmethod
+ def clear_cache(cls):
+ cls.cut_cache = {}
+ cls.layerset_cache = {}
+ cls.fill_cache = {}
@classmethod
def load(cls, handler):
cls.is_loaded = True
- cls.cut_cache = {}
- cls.layerset_cache = {}
- cls.fill_cache = {}
text = {}
dimension = {}
diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py
index 26bb251035..2e5a75c9be 100644
--- a/src/bonsai/bonsai/bim/module/drawing/decoration.py
+++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py
@@ -22,6 +22,7 @@ import blf
import math
import bmesh
import shapely
+import numpy as np
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.representation
@@ -1625,6 +1626,7 @@ class CutDecorator:
if cls.installed:
cls.uninstall()
handler = cls()
+ handler.cache_camera_matrix()
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
@classmethod
@@ -1727,6 +1729,30 @@ class CutDecorator:
shader.uniform_float("color", color)
batch.draw(shader)
+ def cache_camera_matrix(self):
+ obj = bpy.context.scene.camera
+ DecoratorData.camera_location_checksum = repr(np.array(obj.matrix_world.translation).tobytes())
+ DecoratorData.camera_rotation_checksum = repr(np.array(obj.matrix_world.to_3x3()).tobytes())
+
+ def is_camera_moved(self):
+ if not DecoratorData.camera_location_checksum:
+ self.cache_camera_matrix()
+ return True # Let's be conservative
+ obj = bpy.context.scene.camera
+ loc_check = np.frombuffer(eval(DecoratorData.camera_location_checksum))
+ loc_real = np.array(obj.matrix_world.translation).flatten()
+ if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm
+ self.cache_camera_matrix()
+ return True
+ rot_check = np.frombuffer(eval(DecoratorData.camera_rotation_checksum)).reshape(3, 3)
+ rot_real = np.array(obj.matrix_world.to_3x3())
+ rot_dot = np.dot(rot_check, rot_real.T)
+ angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1))
+ if angle_rad > 0.0017453292519943296: # 0.1 degrees
+ self.cache_camera_matrix()
+ return True
+ return False
+
def decorate(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
has_cut_cache = element.id() in DecoratorData.cut_cache
has_fill_cache = element.id() in DecoratorData.fill_cache
@@ -1734,9 +1760,9 @@ class CutDecorator:
# Currently selected objects must be recalculated as they may be being moved / edited.
# If the camera is selected, we also recalculate as the user may be moving the camera.
- if not has_cut_cache or obj.select_get() or context.scene.camera.select_get():
+ if not has_cut_cache or obj.select_get() or self.is_camera_moved():
self.recalculate_cut(context, obj, element)
- if not has_fill_cache or obj.select_get() or context.scene.camera.select_get():
+ if not has_fill_cache or obj.select_get() or self.is_camera_moved():
self.recalculate_fill(context, obj, element)
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
@@ -1933,8 +1959,7 @@ class DecorationsHandler:
# NOTE: we USE POST_PIXEL here so that we can use both POLYLINE_UNIFORM_COLOR
# and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL")
- if not DecoratorData.is_loaded:
- DecoratorData.load(handler)
+ DecoratorData.clear_cache()
@classmethod
def uninstall(cls):
@@ -1961,5 +1986,8 @@ class DecorationsHandler:
if not DrawingsData.is_loaded:
DrawingsData.load()
+ if not DecoratorData.is_loaded:
+ DecoratorData.load(self)
+
for obj, decorator in DecoratorData.data["object_decorators"]:
decorator.decorate(context, obj)
diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py
index 7a04e9f9ad..1ca45f155a 100644
--- a/src/bonsai/bonsai/bim/module/drawing/operator.py
+++ b/src/bonsai/bonsai/bim/module/drawing/operator.py
@@ -210,12 +210,6 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
should_duplicate_annotations=self.should_duplicate_annotations,
)
- # TODO: Why need to resync active drawing, if it wasn't changed.
- drawing = props.get_active_drawing()
- if drawing is None:
- return
- core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
-
class CreateDrawing(bpy.types.Operator):
"""Creates/refreshes a .svg drawing
@@ -442,6 +436,8 @@ class CreateDrawing(bpy.types.Operator):
context.scene.render.filepath = str(Path(svg_path).with_suffix(".png"))
assert (drawing_style := self.cprops.get_active_drawing_style())
+ tool.Blender.sync_render_visibility()
+
if drawing_style.render_type == "DEFAULT":
bpy.ops.render.render(write_still=True)
else:
@@ -625,7 +621,7 @@ class CreateDrawing(bpy.types.Operator):
path.attrib["d"] = d
group.append(g)
- def generate_wall_layers(self, context: bpy.types.Context, root) -> None:
+ def generate_material_layers(self, context: bpy.types.Context, root) -> None:
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
if "projection" in el.get("class", "").split():
continue
@@ -794,8 +790,9 @@ class CreateDrawing(bpy.types.Operator):
edge_bm.to_mesh(edge_mesh)
edge_bm.free()
- actual_path = svg_path[0:-4] + "0001.svg"
+ freestyle_svg_exporter = tool.Blender.get_addon("freestyle_svg_exporter")
context.scene.render.filepath = svg_path[0:-4]
+ actual_path = freestyle_svg_exporter.create_path(bpy.context.scene)
bpy.ops.render.render(write_still=False)
os.replace(actual_path, svg_path)
@@ -839,6 +836,8 @@ class CreateDrawing(bpy.types.Operator):
if tool.Drawing.is_camera_orthographic():
self.generate_bisect_linework(context, root)
+ if self.cprops.generate_material_layers:
+ self.generate_material_layers(context, root)
self.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root)
@@ -936,12 +935,14 @@ class CreateDrawing(bpy.types.Operator):
if self.cprops.cut_mode == "BISECT":
self.remove_cut_linework(root)
self.generate_bisect_linework(context, root)
- self.generate_wall_layers(context, root)
+ if self.cprops.generate_material_layers:
+ self.generate_material_layers(context, root)
self.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root)
elif self.cprops.cut_mode == "OPENCASCADE":
self.move_projection_to_bottom(root)
- self.generate_wall_layers(context, root)
+ if self.cprops.generate_material_layers:
+ self.generate_material_layers(context, root)
self.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root)
@@ -1251,6 +1252,8 @@ class CreateDrawing(bpy.types.Operator):
def get_svg_classes(self, element, layer=None):
classes = [element.is_a()]
+
+ # ─── Material ──────────────────────────────────────────────
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
material_name = ""
if material:
@@ -1263,6 +1266,7 @@ class CreateDrawing(bpy.types.Operator):
else:
classes.append("material-null")
+ # ─── Layer ─────────────────────────────────────────────────
if layer:
classes.append(layer.is_a())
layer_material = layer.Material
@@ -1275,12 +1279,19 @@ class CreateDrawing(bpy.types.Operator):
layer_material_category = tool.Drawing.canonicalise_class_name(layer_material.Category)
classes.append(f"layer-material-category-{layer_material_category}")
+ # ─── Metadata ──────────────────────────────────────────────
for key in self.metadata:
value = ifcopenshell.util.selector.get_element_value(element, key)
if value:
classes.append(
tool.Drawing.canonicalise_class_name(key) + "-" + tool.Drawing.canonicalise_class_name(str(value))
)
+
+ # ─── Target View ───────────────────────────────────────────
+ if getattr(self.cprops, "target_view", None):
+ target_view_class = tool.Drawing.canonicalise_class_name(str(self.cprops.target_view))
+ classes.append(f"target-view-{target_view_class}")
+
return classes
def is_manifold(self, obj) -> bool:
@@ -1347,7 +1358,13 @@ class CreateDrawing(bpy.types.Operator):
join_criteria = join_criteria.split(",")
else:
# Drawing convention states that same objects classes with the same material are merged when cut.
- join_criteria = ["class", "material.Name", "/Pset_.*Common/.Status", "EPset_Status.Status", "Material.Name"]
+ join_criteria = [
+ "class",
+ "material.Name",
+ "/Pset_.*Common/.Status",
+ "EPset_Status.Status",
+ "EPset_Status.UserDefinedStatus",
+ ]
group = root.find("{http://www.w3.org/2000/svg}g")
joined_paths = {}
@@ -1476,11 +1493,10 @@ class CreateDrawing(bpy.types.Operator):
joined_paths.setdefault(hash_keys, []).append(el)
for key, els in joined_paths.items():
- polygons = []
- classes = set()
+ queue = []
for el in els:
- classes.update(el.attrib["class"].split())
+ classes = set(el.attrib["class"].split())
classes.add(el.attrib["{http://www.ifcopenshell.org/ns}guid"])
is_closed_polygon = False
for path in el.findall("{http://www.w3.org/2000/svg}path"):
@@ -1495,31 +1511,30 @@ class CreateDrawing(bpy.types.Operator):
coords.append(coords[0])
if len(coords) > 2 and coords[0] == coords[-1]:
is_closed_polygon = True
- polygons.append(shapely.Polygon(coords))
+ queue.append((shapely.Polygon(coords), classes))
if is_closed_polygon:
el.getparent().remove(el)
- try:
- merged_polygons = shapely.ops.unary_union(polygons)
- except:
- print("Warning. Portions of the merge failed. Please report a bug!", polygons)
- merged_polygons = polygons
+ while queue:
+ polygon, polygon_classes = queue.pop()
+ for polygon2, polygon2_classes in queue[:]:
+ try:
+ merged_polygon = shapely.union(polygon, polygon2)
+ except:
+ print("Warning. Portions of the merge failed. Please report a bug!", polygon, polygon2)
+ continue
+ if type(merged_polygon) == shapely.Polygon:
+ polygon = merged_polygon
+ polygon_classes.update(polygon2_classes)
+ queue.remove((polygon2, polygon2_classes))
- if type(merged_polygons) == shapely.MultiPolygon:
- merged_polygons = merged_polygons.geoms
- elif type(merged_polygons) == shapely.Polygon:
- merged_polygons = [merged_polygons]
- else:
- merged_polygons = []
-
- for polygon in merged_polygons:
g = etree.Element("g")
path = etree.SubElement(g, "path")
d = "M" + " L".join([",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]]) + " Z"
for interior in polygon.interiors:
d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z"
path.attrib["d"] = d
- g.set("class", " ".join(list(classes)))
+ g.set("class", " ".join(list(polygon_classes)))
group.append(g)
def drawing_to_model_co(self, x: float, y: float) -> Vector:
@@ -1861,13 +1876,15 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = tool.Drawing.get_document_props()
- # Won't be visible in UI anyway.
- prefs = tool.Blender.get_addon_preferences()
- if not props.sheets or not prefs.data_dir:
- return False
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
+ if not props.sheets:
+ cls.poll_message_set("No sheets available.")
+ return False
+ if not tool.Blender.get_user_data_dir():
+ cls.poll_message_set("BIM data directory not set.")
+ return False
return True
def _execute(self, context):
@@ -2221,6 +2238,7 @@ class ActivateModel(bpy.types.Operator):
)
tool.Blender.reset_object_visibility()
+ tool.Drawing.hide_all_drawing_collections()
tool.Blender.update_viewport()
bonsai.bim.handler.refresh_ui_data()
@@ -2317,9 +2335,8 @@ class ActivateDrawingBase(tool.Ifc.Operator):
dprops.active_drawing_id = self.drawing
dprops.drawing_styles.clear()
- if ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "HasUnderlay"):
- bpy.ops.bim.reload_drawing_styles()
- bpy.ops.bim.activate_drawing_style()
+ bpy.ops.bim.reload_drawing_styles()
+ bpy.ops.bim.activate_drawing_style()
if tool.Drawing.is_camera_orthographic():
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
@@ -2332,10 +2349,22 @@ class ActivateDrawingBase(tool.Ifc.Operator):
camera = context.scene.camera
assert camera
camera_props = tool.Drawing.get_camera_props(camera)
+ # Check if this is a reflected ceiling camera and preserve its scale
+ camera_element = tool.Ifc.get_entity(camera)
+ is_reflected = False
+ if camera_element:
+ is_reflected = (
+ ifcopenshell.util.element.get_pset(camera_element, "EPset_Drawing", "TargetView")
+ == "REFLECTED_PLAN_VIEW"
+ )
+ if is_reflected and camera.scale != (-1, -1, -1):
+ camera.scale = (-1, -1, -1)
+
if camera_props.update_representation(camera.matrix_world):
bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="")
- # See 6452 and 6478.
- # bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
+ # Restore the scale after update if needed
+ if is_reflected:
+ camera.scale = (-1, -1, -1)
return {"FINISHED"}
@@ -2811,8 +2840,13 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
if not props.schedules:
cls.poll_message_set("No schedule selected.")
return False
- prefs = tool.Blender.get_addon_preferences()
- return props.schedules and props.sheets and prefs.data_dir
+ if not props.sheets:
+ cls.poll_message_set("No sheets available.")
+ return False
+ if not tool.Blender.get_user_data_dir():
+ cls.poll_message_set("BIM data directory not set.")
+ return False
+ return True
def _execute(self, context):
props = tool.Drawing.get_document_props()
@@ -2879,8 +2913,13 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
if not props.references:
cls.poll_message_set("No reference selected.")
return False
- bim_props = tool.Blender.get_bim_props()
- return props.references and props.sheets and bim_props.data_dir
+ if not props.sheets:
+ cls.poll_message_set("No sheets available.")
+ return False
+ if not tool.Blender.get_user_data_dir():
+ cls.poll_message_set("BIM data directory not set.")
+ return False
+ return True
def _execute(self, context):
props = tool.Drawing.get_document_props()
@@ -3729,3 +3768,19 @@ class OpenDocumentationWebUi(bpy.types.Operator):
else:
bpy.ops.bim.open_web_browser(page="documentation")
return {"FINISHED"}
+
+
+class ExcludeAnnotation(bpy.types.Operator, tool.Ifc.Operator):
+ bl_idname = "bim.exclude_annotation"
+ bl_label = "Exclude Annotation"
+ bl_description = "Excludes the automatic annotation reference from the drawing"
+ bl_options = {"REGISTER", "UNDO"}
+
+ def _execute(self, context):
+ if not (obj := bpy.context.scene.camera) or not (drawing := tool.Ifc.get_entity(obj)):
+ return
+ for obj in tool.Blender.get_selected_objects(include_active=False):
+ if (element := tool.Ifc.get_entity(obj)) and tool.Drawing.is_auto_annotation(element):
+ if referenced_element := tool.Drawing.get_annotation_element(element):
+ tool.Drawing.exclude_annotation_from_drawing(referenced_element, drawing)
+ core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py
index fb5c05679a..4dd025afc0 100644
--- a/src/bonsai/bonsai/bim/module/drawing/prop.py
+++ b/src/bonsai/bonsai/bim/module/drawing/prop.py
@@ -498,6 +498,9 @@ class BIMCameraProperties(PropertyGroup):
name="Linework Mode",
update=get_update_layer_callback("linework_mode", "LineworkMode"),
)
+ generate_material_layers: bpy.props.BoolProperty(
+ name="Generate Material Layers", description="Generate material layer linework in drawings", default=True
+ )
fill_mode: EnumProperty(
items=[
("NONE", "None", "Disable filling areas seen in projection"),
@@ -859,6 +862,19 @@ class BIMAnnotationProperties(PropertyGroup):
)
is_adding_type: bpy.props.BoolProperty(default=False)
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
+ tag_rotation_mode: bpy.props.EnumProperty(
+ name="Tag Rotation Mode",
+ description="How to orient the tag relative to the tagged object",
+ items=[
+ ("NONE", "No Rotation", "Keep tag in default orientation"),
+ ("LOCAL_X", "Local X Axis", "Align tag with object's local X axis"),
+ ("LOCAL_Y", "Local Y Axis", "Align tag with object's local Y axis"),
+ ("LOCAL_Z", "Local Z Axis", "Align tag with object's local Z axis"),
+ ("CAMERA_Horizontal", "Camera Horizontal", "Align tag with camera X axis"),
+ ("CAMERA_Vertical", "Camera Vertical", "Align tag with camera Y axis"),
+ ],
+ default="NONE",
+ )
if TYPE_CHECKING:
object_type: str
diff --git a/src/bonsai/bonsai/bim/module/drawing/ui.py b/src/bonsai/bonsai/bim/module/drawing/ui.py
index 905bc8e287..82ddfef55a 100644
--- a/src/bonsai/bonsai/bim/module/drawing/ui.py
+++ b/src/bonsai/bonsai/bim/module/drawing/ui.py
@@ -99,12 +99,13 @@ class BIM_PT_camera(Panel):
row = self.layout.row()
row.prop(props, "linework_mode")
+ row = self.layout.row()
+ row.prop(props, "generate_material_layers")
if props.linework_mode == "OPENCASCADE":
row = self.layout.row()
row.prop(props, "fill_mode")
row = self.layout.row()
row.prop(props, "cut_mode")
-
row = self.layout.row()
row.prop(props, "width")
row = self.layout.row()
@@ -193,6 +194,7 @@ class BIM_PT_element_filters(Panel):
text = "Exclude Filter" if ElementFiltersData.data["has_exclude_filter"] else "No Exclude Filter Found"
icon = "GREASEPENCIL" if ElementFiltersData.data["has_exclude_filter"] else "ADD"
row.label(text=text, icon="FILTER")
+ row.operator("bim.exclude_annotation", icon="REMOVE", text="")
row.operator("bim.enable_editing_element_filter", icon=icon, text="").filter_mode = "EXCLUDE"
diff --git a/src/bonsai/bonsai/bim/module/drawing/workspace.py b/src/bonsai/bonsai/bim/module/drawing/workspace.py
index bf2f026358..b938514c70 100644
--- a/src/bonsai/bonsai/bim/module/drawing/workspace.py
+++ b/src/bonsai/bonsai/bim/module/drawing/workspace.py
@@ -228,7 +228,6 @@ class AnnotationToolUI:
def draw_type_selection_interface(cls):
# shared by both sidebar and header
object_type = cls.props.object_type
-
row = cls.layout.row(align=True)
row.label(text="", icon="FILE_VOLUME")
prop_with_search(row, cls.props, "object_type", text="")
@@ -248,6 +247,9 @@ class AnnotationToolUI:
add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation")
if object_type in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP:
+ row = cls.layout.row(align=True)
+ row.label(text="", icon="DRIVER_ROTATIONAL_DIFFERENCE")
+ row.prop(cls.props, "tag_rotation_mode", text="")
add_layout_hotkey_operator(
cls.layout,
"Bulk Tag",
@@ -299,6 +301,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
return
related_objects = bpy.context.selected_objects
+ created_objects = []
+
for related_object in related_objects:
obj = core.add_annotation(
tool.Ifc,
@@ -311,7 +315,15 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
),
enable_editing=False,
)
- tool.Drawing.setup_annotation_object(obj, object_type, related_object)
+ tool.Drawing.setup_annotation_object(obj, object_type, related_object, props.tag_rotation_mode)
+ created_objects.append(obj)
+
+ # Select the created annotation objects
+ bpy.ops.object.select_all(action="DESELECT")
+ for obj in created_objects:
+ obj.select_set(True)
+ if created_objects:
+ bpy.context.view_layer.objects.active = created_objects[-1]
def hotkey_S_A(self):
if bpy.ops.bim.add_annotation.poll():
@@ -341,4 +353,5 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
continue
related_object = tool.Ifc.get_object(related_product)
- tool.Drawing.setup_annotation_object(obj, annotation_type, related_object)
+ rotation_mode = tool.Drawing.get_annotation_props().tag_rotation_mode
+ tool.Drawing.setup_annotation_object(obj, annotation_type, related_object, rotation_mode)
diff --git a/src/bonsai/bonsai/bim/module/geometry/__init__.py b/src/bonsai/bonsai/bim/module/geometry/__init__.py
index bba979af30..77a2c8b63b 100644
--- a/src/bonsai/bonsai/bim/module/geometry/__init__.py
+++ b/src/bonsai/bonsai/bim/module/geometry/__init__.py
@@ -109,8 +109,9 @@ def block_scale(scene: bpy.types.Scene) -> None:
if obj.type == "CAMERA":
camera = tool.Ifc.get_entity(obj)
if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW":
- obj.scale = (-1, -1, -1)
- obj.rotation_euler = (0.0, 0.0, math.radians(180))
+ # Only update if scale isn't already (-1, -1, -1)
+ if obj.scale != (-1, -1, -1):
+ obj.scale = (-1, -1, -1)
else:
if obj.scale != (1, 1, 1):
obj.scale = (1, 1, 1)
diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py
index 401428cddb..248fc9c948 100644
--- a/src/bonsai/bonsai/bim/module/geometry/operator.py
+++ b/src/bonsai/bonsai/bim/module/geometry/operator.py
@@ -858,6 +858,10 @@ class OverrideDelete(bpy.types.Operator):
objects_to_remove = context.selected_objects
self.process_arrays(context)
+
+ # Track aggregates before deleting their parts
+ aggregates_to_check = self.track_aggregates(objects_to_remove)
+
clear_active_object = True
for i, obj in enumerate(objects_to_remove, 1):
@@ -882,13 +886,28 @@ class OverrideDelete(bpy.types.Operator):
continue
if ifcopenshell.util.element.get_pset(element, "BBIM_Array"):
self.report({"INFO"}, "Elements that are part of an array cannot be deleted.")
- return {"FINISHED"}
+ continue
+ if element.is_a("IfcGridAxis"):
+ # Deleting the last W axis is OK
+ if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or (
+ (grid := element.PartOfV) and len(grid[0].VAxes) == 1
+ ):
+ self.report(
+ {"INFO"}, "The last grid axis of a grid cannot be deleted. Delete the grid instead."
+ )
+ continue
+ if tool.Drawing.is_auto_annotation(element):
+ self.report({"INFO"}, "References cannot be deleted. Exclude the referenced element instead.")
+ continue
tool.Geometry.delete_ifc_object(obj)
elif tool.Geometry.is_representation_item(obj):
tool.Geometry.delete_ifc_item(obj)
else:
bpy.data.objects.remove(obj)
+ # Delete empty aggregates after deleting their parts
+ self.delete_empty_aggregates(aggregates_to_check)
+
for opening in tool.Model.get_model_props().openings:
if opening.obj is not None and not tool.Ifc.get_entity(opening.obj):
bpy.data.objects.remove(opening.obj)
@@ -921,6 +940,49 @@ class OverrideDelete(bpy.types.Operator):
data["old_file"].redo()
tool.Ifc.set(data["new_file"])
+ def track_aggregates(self, objects_to_remove):
+ """Track aggregates that contain objects being deleted"""
+ aggregates_to_check = set()
+ for obj in objects_to_remove:
+ if not tool.Blender.is_valid_data_block(obj):
+ continue
+ element = tool.Ifc.get_entity(obj)
+ if not element:
+ continue
+ aggregate = ifcopenshell.util.element.get_aggregate(element)
+ if aggregate:
+ aggregates_to_check.add(aggregate)
+ return aggregates_to_check
+
+ def delete_empty_aggregates(self, aggregates_to_check):
+ """Delete aggregates that now have no parts"""
+ deleted_aggregates = []
+ for aggregate in aggregates_to_check:
+ # Check if aggregate still exists (might have been deleted already)
+ try:
+ aggregate.id()
+ except:
+ continue
+
+ related_objects = ifcopenshell.util.element.get_parts(aggregate)
+ if len(related_objects) == 0:
+ aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
+ deleted_aggregates.append(aggregate_name)
+
+ aggregate_obj = tool.Ifc.get_object(aggregate)
+ if aggregate_obj and tool.Blender.is_valid_data_block(aggregate_obj):
+ tool.Geometry.delete_ifc_object(aggregate_obj)
+
+ # Show info message if aggregates were deleted
+ if deleted_aggregates:
+ if len(deleted_aggregates) == 1:
+ self.report(
+ {"INFO"}, f"Aggregate '{deleted_aggregates[0]}' was deleted because it had no remaining parts"
+ )
+ else:
+ aggregate_list = ", ".join(f"'{name}'" for name in deleted_aggregates)
+ self.report({"INFO"}, f"Aggregates {aggregate_list} were deleted because they had no remaining parts")
+
def process_arrays(self, context: bpy.types.Context) -> None:
ifc_file = tool.Ifc.get()
selected_objects = set(context.selected_objects)
@@ -2973,6 +3035,7 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
item_mesh = bpy.data.meshes.new("tmp")
tool.Ifc.link(item, item_mesh)
item_obj = bpy.data.objects.new("tmp", item_mesh)
+ tool.Geometry.lock_scale(item_obj)
tool.Geometry.name_item_object(item_obj, item)
item_obj.matrix_world = obj.matrix_world
bpy.context.collection.objects.link(item_obj)
diff --git a/src/bonsai/bonsai/bim/module/light/data.py b/src/bonsai/bonsai/bim/module/light/data.py
index 55bea350d2..abe9d9e87d 100644
--- a/src/bonsai/bonsai/bim/module/light/data.py
+++ b/src/bonsai/bonsai/bim/module/light/data.py
@@ -41,7 +41,7 @@ class SolarData:
@classmethod
def sun_position(cls):
- return tool.Blender.get_sun_position_addon()
+ return tool.Blender.get_addon("sun_position")
@classmethod
def sites(cls):
diff --git a/src/bonsai/bonsai/bim/module/light/operator.py b/src/bonsai/bonsai/bim/module/light/operator.py
index f57ec3a4e0..4a8d993e0e 100644
--- a/src/bonsai/bonsai/bim/module/light/operator.py
+++ b/src/bonsai/bonsai/bim/module/light/operator.py
@@ -202,7 +202,7 @@ class RadianceRender(bpy.types.Operator):
print(f"Camera position: {camera_position}")
print(f"Camera direction: {camera_direction}")
- # sun_position = tool.Blender.get_sun_position_addon()
+ # sun_position = tool.Blender.get_addon("sun_position")
# azimuth, elevation = sun_position.sun_calc.get_sun_coordinates(
# sun_pos_props.time,
# sun_pos_props.latitude,
diff --git a/src/bonsai/bonsai/bim/module/light/prop.py b/src/bonsai/bonsai/bim/module/light/prop.py
index 54e5f6f73c..1cb36f82eb 100644
--- a/src/bonsai/bonsai/bim/module/light/prop.py
+++ b/src/bonsai/bonsai/bim/module/light/prop.py
@@ -40,7 +40,7 @@ from bpy.types import PropertyGroup
from bonsai.bim.module.light.data import SolarData
from bonsai.bim.module.light.decorator import SolarDecorator
-sun_position = tool.Blender.get_sun_position_addon()
+sun_position = tool.Blender.get_addon("sun_position")
now = datetime.datetime.now()
with open(os.path.join(os.path.dirname(__file__), "spectraldb.json"), "r") as f:
diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py
index 7245dfb443..a286e3daee 100644
--- a/src/bonsai/bonsai/bim/module/model/prop.py
+++ b/src/bonsai/bonsai/bim/module/model/prop.py
@@ -373,7 +373,12 @@ class BIMStairProperties(PropertyGroup):
if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0:
self["nosing_length"] = 0
- non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type")
+ def update_custom_tread_lock(self, context: bpy.types.Context) -> None:
+ """When lock is enabled, sync custom treads with tread_run"""
+ if self.custom_tread_lock:
+ self["custom_first_last_tread_run"] = (self.tread_run, self.tread_run)
+
+ non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type", "custom_tread_lock")
is_editing: bpy.props.BoolProperty(default=False)
width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01, subtype="DISTANCE")
@@ -407,6 +412,12 @@ class BIMStairProperties(PropertyGroup):
default="CONCRETE",
update=validate_nosing_value,
)
+ custom_tread_lock: bpy.props.BoolProperty(
+ name="Lock First/Last Treads to Tread Run",
+ description="When enabled, first and last treads automatically use the Tread Run value",
+ default=True,
+ update=update_custom_tread_lock,
+ )
custom_first_last_tread_run: bpy.props.FloatVectorProperty(
name="Custom First / Last Treads Widths",
description='Specify custom first / last treads widths, different from the general "Tread Run". Leave 0 to disable.',
@@ -442,6 +453,7 @@ class BIMStairProperties(PropertyGroup):
top_slab_depth: float
has_top_nib: bool
stair_type: str
+ custom_tread_lock: bool
custom_first_last_tread_run: tuple[float, float]
nosing_length: float
nosing_depth: float
@@ -480,8 +492,11 @@ class BIMStairProperties(PropertyGroup):
}
stair_kwargs.update(generic_props)
- # defined here to appear last in UI
- stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
+ # If locked, use tread_run for both first and last treads
+ if self.custom_tread_lock:
+ stair_kwargs["custom_first_last_tread_run"] = (self.tread_run, self.tread_run)
+ else:
+ stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
if not convert_to_project_units:
return stair_kwargs
@@ -489,8 +504,24 @@ class BIMStairProperties(PropertyGroup):
stair_kwargs = tool.Model.convert_data_to_project_units(stair_kwargs, self.non_si_units_props)
return stair_kwargs
+ def get_props_kwargs_for_ifc_export(self, convert_to_project_units=False, stair_type=None):
+ """Get props including custom_tread_lock for saving to IFC"""
+ stair_kwargs = self.get_props_kwargs(convert_to_project_units, stair_type)
+ # Add the lock state for IFC storage (after getting base kwargs to avoid passing to generate function)
+ stair_kwargs["custom_tread_lock"] = self.custom_tread_lock
+ return stair_kwargs
+
def set_props_kwargs_from_ifc_data(self, kwargs):
kwargs = tool.Model.convert_data_to_si_units(kwargs, self.non_si_units_props)
+
+ # Determine lock state based on whether custom treads match tread_run
+ # If custom_tread_lock wasn't saved (old files), infer it from the data
+ if "custom_tread_lock" not in kwargs:
+ custom_treads = kwargs.get("custom_first_last_tread_run", (0.0, 0.0))
+ tread_run = kwargs.get("tread_run", 0.3)
+ # Lock is off if either custom tread differs from tread_run and is not 0
+ kwargs["custom_tread_lock"] = not any(ct != 0.0 and ct != tread_run for ct in custom_treads)
+
for prop_name in kwargs:
setattr(self, prop_name, kwargs[prop_name])
diff --git a/src/bonsai/bonsai/bim/module/model/stair.py b/src/bonsai/bonsai/bim/module/model/stair.py
index d2b5184c76..71bfa97a9a 100644
--- a/src/bonsai/bonsai/bim/module/model/stair.py
+++ b/src/bonsai/bonsai/bim/module/model/stair.py
@@ -188,7 +188,8 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Model.get_stair_props(obj)
ifc_file = tool.Ifc.get()
- stair_data = props.get_props_kwargs(convert_to_project_units=True)
+ # Use the special method that includes custom_tread_lock for IFC storage
+ stair_data = props.get_props_kwargs_for_ifc_export(convert_to_project_units=True)
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
if not pset:
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="BBIM_Stair")
@@ -241,7 +242,8 @@ class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator):
assert element
props = tool.Model.get_stair_props(obj)
- data = props.get_props_kwargs(convert_to_project_units=True)
+ # Use the special method that includes custom_tread_lock for IFC storage
+ data = props.get_props_kwargs_for_ifc_export(convert_to_project_units=True)
props.is_editing = False
regenerate_stair_mesh(obj)
tool.Model.add_body_representation(obj)
diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py
index 0b17f6d9fb..1e1ff2cc2f 100644
--- a/src/bonsai/bonsai/bim/module/model/ui.py
+++ b/src/bonsai/bonsai/bim/module/model/ui.py
@@ -302,13 +302,38 @@ class BIM_PT_stair(bpy.types.Panel):
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
row = self.layout.row(align=True)
for prop_name in props.get_props_kwargs():
+ # Skip custom_tread_lock as it's handled with custom_first_last_tread_run
+ if prop_name == "custom_tread_lock":
+ continue
+
prop_value = getattr(props, prop_name)
- if isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
+
+ # Special handling for custom_first_last_tread_run
+ if prop_name == "custom_first_last_tread_run":
+ # Draw the lock toggle
+ row_lock = self.layout.row(align=True)
+ lock_text = (
+ "Lock First/Last Treads" if not props.custom_tread_lock else "Unlock First/Last Treads"
+ )
+ row_lock.prop(
+ props,
+ "custom_tread_lock",
+ text=lock_text,
+ icon="LOCKED" if props.custom_tread_lock else "UNLOCKED",
+ )
+
+ # Only show the custom values input if unlocked
+ if not props.custom_tread_lock:
+ prop_readable_name = props.bl_rna.properties[prop_name].name
+ self.layout.label(text=f"{prop_readable_name}:")
+ self.layout.prop(props, prop_name, text="")
+ elif isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
prop_readable_name = props.bl_rna.properties[prop_name].name
self.layout.label(text=f"{prop_readable_name}:")
self.layout.prop(props, prop_name, text="")
else:
self.layout.prop(props, prop_name)
+
if prop_name == "height": # Weak but we just want to insert this inside props drawing
row_length = self.layout.row(align=True)
row_length.prop(props, "total_length_target")
diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py
index 148937e577..2f5b9c906e 100644
--- a/src/bonsai/bonsai/bim/module/project/operator.py
+++ b/src/bonsai/bonsai/bim/module/project/operator.py
@@ -2522,6 +2522,7 @@ class FlipClippingPlane(bpy.types.Operator):
obj = context.active_object
if obj in tool.Project.get_project_props().clipping_planes_objs:
obj.rotation_euler[0] += radians(180)
+ obj.rotation_euler[0] %= radians(360)
context.view_layer.update()
return {"FINISHED"}
diff --git a/src/bonsai/bonsai/bim/module/project/prop.py b/src/bonsai/bonsai/bim/module/project/prop.py
index 5682c00c81..2cbdeb6e7b 100644
--- a/src/bonsai/bonsai/bim/module/project/prop.py
+++ b/src/bonsai/bonsai/bim/module/project/prop.py
@@ -450,9 +450,9 @@ class BIMProjectProperties(PropertyGroup):
self.mvd = header_data.mvd
self.author_name = header_data.author_name
self.author_email = header_data.author_email
- self.organisation_name = header_data.organization_name
- self.organisation_email = header_data.organization_email
- self.authorisation = header_data.authorization
+ self.organisation_name = header_data.organisation_name
+ self.organisation_email = header_data.organisation_email
+ self.authorisation = header_data.authorisation
if TYPE_CHECKING:
is_editing: bool
diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py
index 75d114e204..03b4222008 100644
--- a/src/bonsai/bonsai/bim/module/project/ui.py
+++ b/src/bonsai/bonsai/bim/module/project/ui.py
@@ -653,4 +653,5 @@ class BIM_PT_purge(Panel):
row = layout.row(align=True)
row.label(text=f"{object_type.replace('_', ' ').capitalize()}:")
row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = object_type
- row.operator("bim.merge_identical_objects", text="Merge Identical").object_type = object_type
+ merge_op = row.operator("bim.merge_identical_objects", text="Merge Identical")
+ merge_op.object_type = object_type
diff --git a/src/bonsai/bonsai/bim/module/pset/ui.py b/src/bonsai/bonsai/bim/module/pset/ui.py
index ab11c5f1ce..abd3a8b90f 100644
--- a/src/bonsai/bonsai/bim/module/pset/ui.py
+++ b/src/bonsai/bonsai/bim/module/pset/ui.py
@@ -242,14 +242,11 @@ class BIM_PT_object_psets(Panel):
@classmethod
def poll(cls, context):
- if not (obj := context.active_object):
- return False
- ifc_id = tool.Blender.get_ifc_definition_id(obj)
- if not ifc_id:
- return False
- if not tool.Ifc.get_object_by_identifier(ifc_id):
- return False
- return True
+ return (
+ (obj := tool.Blender.get_active_object())
+ and (element := tool.Ifc.get_entity(obj))
+ and element.is_a("IfcObjectDefinition")
+ )
def draw(self, context):
if not ObjectPsetsData.is_loaded:
@@ -388,14 +385,11 @@ class BIM_PT_object_qtos(Panel):
@classmethod
def poll(cls, context):
- if not (obj := context.active_object):
- return False
- ifc_id = tool.Blender.get_ifc_definition_id(obj)
- if not ifc_id:
- return False
- if not tool.Ifc.get_object_by_identifier(ifc_id):
- return False
- return True
+ return (
+ (obj := tool.Blender.get_active_object())
+ and (element := tool.Ifc.get_entity(obj))
+ and element.is_a("IfcObjectDefinition")
+ )
def draw(self, context):
if not ObjectQtosData.is_loaded:
diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py
index 55ffa56a79..a5bf766ae2 100644
--- a/src/bonsai/bonsai/bim/module/root/operator.py
+++ b/src/bonsai/bonsai/bim/module/root/operator.py
@@ -317,6 +317,7 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
f"Mesh '{obj.data.name}' has loose geometry, loose geometry will be ignored to save mesh to IFC as a tessellation.",
)
+ representation = tool.Geometry.export_mesh_to_tessellation(obj, ifc_context)
element = core.assign_class(
tool.Ifc,
tool.Collector,
@@ -326,13 +327,9 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
predefined_type=predefined_type,
should_add_representation=False,
)
- representation = tool.Geometry.export_mesh_to_tessellation(obj, ifc_context)
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
bonsai.core.geometry.switch_representation(
- tool.Ifc,
- tool.Geometry,
- obj=obj,
- representation=representation,
+ tool.Ifc, tool.Geometry, obj=obj, representation=representation
)
else:
diff --git a/src/bonsai/bonsai/bim/module/search/data.py b/src/bonsai/bonsai/bim/module/search/data.py
index 34f032c2b5..4bde0aa359 100644
--- a/src/bonsai/bonsai/bim/module/search/data.py
+++ b/src/bonsai/bonsai/bim/module/search/data.py
@@ -101,7 +101,7 @@ class ColourByPropertyData:
elif pset.endswith("BaseQuantities"):
keys.extend([f'/.*BaseQuantities/."{name}"' for name in properties.keys() if name != "id"])
else:
- keys.extend([f"{pset}.{name}" for name in properties.keys() if name != "id"])
+ keys.extend([f'"{pset}"."{name}"' for name in properties.keys() if name != "id"])
results = [(k, k, "") for k in keys]
return default + results
diff --git a/src/bonsai/bonsai/bim/module/sequence/__init__.py b/src/bonsai/bonsai/bim/module/sequence/__init__.py
index 40a7e1fb3a..3262b64527 100644
--- a/src/bonsai/bonsai/bim/module/sequence/__init__.py
+++ b/src/bonsai/bonsai/bim/module/sequence/__init__.py
@@ -105,6 +105,7 @@ classes = (
operator.RemoveWorkTime,
operator.ReorderTask,
operator.SaveAnimationColorScheme,
+ operator.SelectTaskElements,
operator.SelectTaskRelatedInputs,
operator.SelectTaskRelatedProducts,
operator.SelectUnassignedWorkScheduleProducts,
diff --git a/src/bonsai/bonsai/bim/module/sequence/data.py b/src/bonsai/bonsai/bim/module/sequence/data.py
index b011d48e15..94df71eacd 100644
--- a/src/bonsai/bonsai/bim/module/sequence/data.py
+++ b/src/bonsai/bonsai/bim/module/sequence/data.py
@@ -243,6 +243,9 @@ class SequenceData:
data["NestingIndex"] = None
for rel in task.Nests or []:
data["NestingIndex"] = rel.RelatedObjects.index(task)
+ data["TotalInputs"] = len(tool.Sequence.get_task_inputs(task))
+ data["TotalOutputs"] = len(tool.Sequence.get_task_outputs(task))
+ data["TotalElements"] = data["TotalInputs"] + data["TotalOutputs"]
cls.data["tasks"][task.id()] = data
@classmethod
diff --git a/src/bonsai/bonsai/bim/module/sequence/operator.py b/src/bonsai/bonsai/bim/module/sequence/operator.py
index 1dacf6efb7..c07b9a0496 100644
--- a/src/bonsai/bonsai/bim/module/sequence/operator.py
+++ b/src/bonsai/bonsai/bim/module/sequence/operator.py
@@ -1281,6 +1281,17 @@ class DisableEditingSequence(bpy.types.Operator):
return {"FINISHED"}
+class SelectTaskElements(bpy.types.Operator, tool.Ifc.Operator):
+ bl_idname = "bim.select_task_elements"
+ bl_label = "Select All Task Elements"
+ bl_options = {"REGISTER", "UNDO"}
+ task: bpy.props.IntProperty()
+
+ def _execute(self, context):
+ core.select_task_inputs(tool.Sequence, tool.Spatial, task=tool.Ifc.get().by_id(self.task))
+ core.select_task_outputs(tool.Sequence, tool.Spatial, task=tool.Ifc.get().by_id(self.task))
+
+
class SelectTaskRelatedProducts(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.select_task_related_products"
bl_label = "Select All Output Products"
diff --git a/src/bonsai/bonsai/bim/module/sequence/prop.py b/src/bonsai/bonsai/bim/module/sequence/prop.py
index 86b5a1966d..1c35910e6c 100644
--- a/src/bonsai/bonsai/bim/module/sequence/prop.py
+++ b/src/bonsai/bonsai/bim/module/sequence/prop.py
@@ -75,6 +75,8 @@ def update_active_task_index(self, context):
bonsai.bim.module.pset.data.refresh()
if self.editing_task_type == "SEQUENCE":
tool.Sequence.load_task_properties()
+ if self.should_auto_select:
+ tool.Sequence.select_active_task_elements(task)
def update_active_task_outputs(self, context):
@@ -485,6 +487,9 @@ class BIMWorkScheduleProperties(PropertyGroup):
other_columns: EnumProperty(
items=[
("Controls.Calendar", "Calendar", ""),
+ ("Controls.TotalElements", "Total Elements", "The total number of assigned elements to this task"),
+ ("Controls.TotalInputs", "Total Inputs", "The total number of input elements to this task"),
+ ("Controls.TotalOutputs", "Total Outputs", "The total number of input elements to this task"),
],
name="Special Columns",
)
@@ -518,9 +523,13 @@ class BIMWorkScheduleProperties(PropertyGroup):
active_task_input_index: IntProperty(name="Active Task Input Index")
task_outputs: CollectionProperty(name="Task Outputs", type=TaskProduct)
active_task_output_index: IntProperty(name="Active Task Output Index")
- show_nested_outputs: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_outputs)
- show_nested_resources: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_resources)
- show_nested_inputs: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_inputs)
+ show_nested_outputs: BoolProperty(
+ name="Show Nested Task Elements", default=False, update=update_active_task_outputs
+ )
+ show_nested_resources: BoolProperty(
+ name="Show Nested Task Elements", default=False, update=update_active_task_resources
+ )
+ show_nested_inputs: BoolProperty(name="Show Nested Task Elements", default=False, update=update_active_task_inputs)
product_input_tasks: CollectionProperty(name="Product Task Inputs", type=TaskProduct)
product_output_tasks: CollectionProperty(name="Product Task Outputs", type=TaskProduct)
active_product_output_task_index: IntProperty(name="Active Product Output Task Index")
@@ -531,6 +540,9 @@ class BIMWorkScheduleProperties(PropertyGroup):
filter_by_active_schedule: BoolProperty(
name="Filter By Active Schedule", default=False, update=update_filter_by_active_schedule
)
+ should_auto_select: BoolProperty(
+ name="Auto Select", description="Auto select elements when task is selected", default=False
+ )
if TYPE_CHECKING:
work_schedule_predefined_types: str
diff --git a/src/bonsai/bonsai/bim/module/sequence/ui.py b/src/bonsai/bonsai/bim/module/sequence/ui.py
index dc31471af3..9178268c3a 100644
--- a/src/bonsai/bonsai/bim/module/sequence/ui.py
+++ b/src/bonsai/bonsai/bim/module/sequence/ui.py
@@ -290,13 +290,21 @@ class BIM_PT_work_schedules(Panel):
row2 = self.layout.row(align=True)
row2.alignment = "RIGHT"
- row2.prop(self.props, "enable_reorder", text="", icon="SORTALPHA")
- row2.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING")
- row2.operator("bim.enable_editing_task_time", text="", icon="TIME").task = ifc_definition_id
- row2.operator("bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO").task = (
+ col = row2.column()
+ row_ = col.row(align=True)
+ op = row_.operator("bim.select_task_elements", icon="RESTRICT_SELECT_OFF", text="Select Elements")
+ op.task = task.ifc_definition_id
+ row_.prop(self.props, "should_auto_select", icon="OBJECT_HIDDEN", text="")
+
+ col = row2.column()
+ row_ = col.row(align=True)
+ row_.prop(self.props, "enable_reorder", text="", icon="SORTALPHA")
+ row_.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING")
+ row_.operator("bim.enable_editing_task_time", text="", icon="TIME").task = ifc_definition_id
+ row_.operator("bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO").task = (
ifc_definition_id
)
- row2.operator("bim.enable_editing_task_attributes", text="", icon="GREASEPENCIL").task = (
+ row_.operator("bim.enable_editing_task_attributes", text="", icon="GREASEPENCIL").task = (
ifc_definition_id
)
row.operator("bim.add_task", text="Add", icon="ADD").task = ifc_definition_id
@@ -716,11 +724,10 @@ class BIM_PT_task_icom(Panel):
input_id = self.props.task_inputs[self.props.active_task_input_index].ifc_definition_id
op.related_object = input_id
- op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="Select")
+ op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="")
op.task = task.ifc_definition_id
+ row2.prop(self.props, "show_nested_inputs", icon="OUTLINER", text="")
- row2 = col.row()
- row2.prop(self.props, "show_nested_inputs", text="Show Nested")
row2 = col.row()
row2.template_list("BIM_UL_task_inputs", "", self.props, "task_inputs", self.props, "active_task_input_index")
@@ -744,8 +751,7 @@ class BIM_PT_task_icom(Panel):
op.related_object_type = "RESOURCE"
op.resource = self.props.task_resources[self.props.active_task_resource_index].ifc_definition_id
- row2 = col.row()
- row2.prop(self.props, "show_nested_resources", text="Show Nested")
+ row2.prop(self.props, "show_nested_resources", icon="OUTLINER", text="")
row2 = col.row()
row2.template_list(
@@ -773,10 +779,10 @@ class BIM_PT_task_icom(Panel):
output_id = self.props.task_outputs[self.props.active_task_output_index].ifc_definition_id
op.relating_product = output_id
- op = row2.operator("bim.select_task_related_products", icon="RESTRICT_SELECT_OFF", text="Select")
+ op = row2.operator("bim.select_task_related_products", icon="RESTRICT_SELECT_OFF", text="")
op.task = task.ifc_definition_id
- row2 = col.row()
- row2.prop(self.props, "show_nested_outputs", text="Show Nested")
+ row2.prop(self.props, "show_nested_outputs", icon="OUTLINER", text="")
+
row2 = col.row()
row2.template_list(
"BIM_UL_task_outputs", "", self.props, "task_outputs", self.props, "active_task_output_index"
@@ -893,7 +899,7 @@ class BIM_UL_tasks(UIList):
split2 = split1.split(factor=0.9 - min(0.5, 0.15 * len(self.props.columns)))
split2.prop(item, "name", emboss=False, text="")
- BIM_UL_tasks.draw_custom_columns(self.props, split2, item, task)
+ BIM_UL_tasks.draw_custom_columns(self.props, split2, item, task, item.ifc_definition_id)
if self.props.active_task_id and self.props.editing_task_type == "ATTRIBUTES":
row.prop(
@@ -960,6 +966,7 @@ class BIM_UL_tasks(UIList):
row: bpy.types.UILayout,
item: Optional[bpy.types.PropertyGroup] = None,
task: Optional[dict[str, Any]] = None,
+ ifc_definition_id: int = 0,
*,
header: bool = False,
) -> None:
@@ -999,6 +1006,21 @@ class BIM_UL_tasks(UIList):
row.label(text=item.derived_calendar + "*")
else:
row.label(text=item.calendar or "-")
+ elif column.name == "Controls.TotalInputs":
+ if header:
+ row.label(text="# Inputs")
+ else:
+ row.label(text=str(SequenceData.data["tasks"][ifc_definition_id]["TotalInputs"]))
+ elif column.name == "Controls.TotalOutputs":
+ if header:
+ row.label(text="# Outputs")
+ else:
+ row.label(text=str(SequenceData.data["tasks"][ifc_definition_id]["TotalOutputs"]))
+ elif column.name == "Controls.TotalElements":
+ if header:
+ row.label(text="# Elements")
+ else:
+ row.label(text=str(SequenceData.data["tasks"][ifc_definition_id]["TotalElements"]))
else:
ifc_class, name = column.name.split(".")
if header:
diff --git a/src/bonsai/bonsai/bim/module/spatial/decorator.py b/src/bonsai/bonsai/bim/module/spatial/decorator.py
index b8781133b0..824fafec1f 100644
--- a/src/bonsai/bonsai/bim/module/spatial/decorator.py
+++ b/src/bonsai/bonsai/bim/module/spatial/decorator.py
@@ -70,7 +70,7 @@ class GridDecorator:
grid_props = tool.Spatial.get_grid_props()
for axis in grid_props.grid_axes:
- if not (obj := axis.obj) or obj.hide_get() == True:
+ if not (obj := axis.obj) or obj.visible_get() is False:
continue
if obj.select_get() and context.mode != "OBJECT":
continue
@@ -125,21 +125,22 @@ class GridDecorator:
unselected_edges = []
grid_props = tool.Spatial.get_grid_props()
for axis in grid_props.grid_axes:
- if (obj := axis.obj) and obj.hide_get() == False:
- if obj.select_get():
- if context.mode != "OBJECT":
- continue
- edges = selected_edges
- verts = selected_verts
- else:
- edges = unselected_edges
- verts = unselected_verts
- i = len(verts)
- edges.append([i, i + 1])
- matrix_world = obj.matrix_world
- v1 = matrix_world @ obj.data.vertices[0].co
- v2 = matrix_world @ obj.data.vertices[1].co
- verts.extend([v1, v2])
+ if not (obj := axis.obj) or obj.visible_get() is False:
+ continue
+ if obj.select_get():
+ if context.mode != "OBJECT":
+ continue
+ edges = selected_edges
+ verts = selected_verts
+ else:
+ edges = unselected_edges
+ verts = unselected_verts
+ i = len(verts)
+ edges.append([i, i + 1])
+ matrix_world = obj.matrix_world
+ v1 = matrix_world @ obj.data.vertices[0].co
+ v2 = matrix_world @ obj.data.vertices[1].co
+ verts.extend([v1, v2])
if unselected_verts:
self.draw_batch("LINES", unselected_verts, decorator_color_background, unselected_edges)
diff --git a/src/bonsai/bonsai/bim/module/type/ui.py b/src/bonsai/bonsai/bim/module/type/ui.py
index dd08278bc9..f749ceb07e 100644
--- a/src/bonsai/bonsai/bim/module/type/ui.py
+++ b/src/bonsai/bonsai/bim/module/type/ui.py
@@ -34,14 +34,11 @@ class BIM_PT_type(Panel):
@classmethod
def poll(cls, context):
- if not context.active_object:
- return False
- element = tool.Ifc.get_entity(context.active_object)
- if not element:
- return False
- if not element.is_a("IfcProduct") and not element.is_a("IfcTypeProduct"):
- return True
- return True
+ return (
+ (obj := tool.Blender.get_active_object())
+ and (element := tool.Ifc.get_entity(obj))
+ and (element.is_a("IfcObject") or element.is_a("IfcTypeObject"))
+ )
def draw(self, context):
if not TypeData.is_loaded:
diff --git a/src/bonsai/bonsai/core/drawing.py b/src/bonsai/bonsai/core/drawing.py
index 0345f4019a..2e2d07c96c 100644
--- a/src/bonsai/bonsai/core/drawing.py
+++ b/src/bonsai/bonsai/core/drawing.py
@@ -108,10 +108,6 @@ def add_sheet(ifc: type[tool.Ifc], drawing: type[tool.Drawing], titleblock: ifco
def regenerate_sheet(
drawing: type[tool.Drawing], sheet: ifcopenshell.entity_instance
) -> Union[list[tool.Drawing.SheetWarningType], None]:
- warnings = drawing.validate_sheet_files(sheet)
- if warnings:
- return warnings
-
titleblock_uri = drawing.get_document_uri(sheet, "TITLEBLOCK")
assert titleblock_uri
@@ -320,6 +316,7 @@ def duplicate_drawing(
) -> ifcopenshell.entity_instance:
drawing_name = drawing_tool.ensure_unique_drawing_name(drawing_tool.get_name(drawing))
new_drawing = ifc.run("root.copy_class", product=drawing)
+ drawing_tool.clear_annotation_relationships(new_drawing)
drawing_tool.copy_representation(drawing, new_drawing)
drawing_tool.set_name(new_drawing, drawing_name)
group = drawing_tool.get_drawing_group(new_drawing)
@@ -475,46 +472,40 @@ def sync_references(
if not drawing_tool.has_annotation(drawing):
return
- context = drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing))
- if not context:
+ if not (context := drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing))):
return
group = drawing_tool.get_drawing_group(drawing)
- for reference_element in drawing_tool.get_potential_reference_elements(drawing):
+ potential_reference_elements = drawing_tool.get_potential_reference_elements(drawing)
+
+ for element in potential_reference_elements:
+ if (obj := ifc.get_object(element)) and ifc.is_moved(obj):
+ drawing_tool.sync_object_placement(obj)
+
+ for element in drawing_tool.get_group_elements(group):
+ if not drawing_tool.is_auto_annotation(element):
+ continue
+ if (obj := ifc.get_object(element)) and ifc.is_moved(obj):
+ drawing_tool.sync_object_placement(obj)
+ reference_element = drawing_tool.get_assigned_product(element)
reference_obj = ifc.get_object(reference_element)
- annotation = drawing_tool.get_drawing_reference_annotation(drawing, reference_element)
+ if reference_element not in potential_reference_elements:
+ # It was auto created, so it makes sense to auto delete
+ if obj := ifc.get_object(element):
+ drawing_tool.delete_object(obj)
+ ifc.run("root.remove_product", product=element)
+ elif not drawing_tool.regenerate_reference_annotation(drawing, element, reference_element, context):
+ if obj := ifc.get_object(element):
+ drawing_tool.delete_object(obj)
+ ifc.run("root.remove_product", product=element)
- should_delete_existing_annotation = False
- should_create_annotation = False
-
- # remove annotation only if the reference object was changed
- # otherwise we rely on the existing annotation
- if annotation:
- if reference_obj and (ifc.is_moved(reference_obj) or ifc.is_edited(reference_obj)):
- should_delete_existing_annotation = True
-
- if should_delete_existing_annotation or not annotation:
- should_create_annotation = True
-
- if should_delete_existing_annotation:
- annotation_obj = ifc.get_object(annotation)
- if annotation_obj:
- drawing_tool.delete_object(annotation_obj)
- ifc.run("root.remove_product", product=annotation)
-
- if should_create_annotation:
- annotation = drawing_tool.generate_reference_annotation(drawing, reference_element, context)
- if annotation:
+ for reference_element in potential_reference_elements:
+ if not drawing_tool.get_drawing_reference_annotation(drawing, reference_element):
+ if annotation := drawing_tool.generate_reference_annotation(drawing, reference_element, context):
ifc.run("drawing.assign_product", relating_product=reference_element, related_object=annotation)
ifc.run("group.assign_group", group=group, products=[annotation])
collector.assign(ifc.get_object(annotation))
- if reference_obj and ifc.is_moved(reference_obj):
- drawing_tool.sync_object_placement(reference_obj)
-
- if reference_obj and ifc.is_edited(reference_obj):
- drawing_tool.sync_object_representation(reference_obj)
-
def select_assigned_product(drawing: type[tool.Drawing], context: bpy.types.Context) -> None:
drawing.select_assigned_product(context)
diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py
index b9b3ae0d49..ee54a2a7fc 100644
--- a/src/bonsai/bonsai/tool/blender.py
+++ b/src/bonsai/bonsai/tool/blender.py
@@ -1506,28 +1506,21 @@ class Blender(bonsai.core.tool.Blender):
return bpy.context.preferences.addons[blender_package_name].preferences
@classmethod
- def get_sun_position_addon(cls) -> Union[types.ModuleType, None]:
- # Check if it's installed as legacy Blender addon.
+ def get_addon(cls, name: str) -> Union[types.ModuleType, None]:
import importlib
try:
- sun_position = importlib.import_module("sun_position")
+ return importlib.import_module(name) # Legacy Blender addon
except ImportError:
- sun_position = None
-
- if sun_position:
- return sun_position
+ pass
for package_name in bpy.context.preferences.addons.keys():
- if package_name.endswith(".sun_position"):
+ if package_name.endswith(f".{name}"):
try:
- sun_position = importlib.import_module(package_name)
- return sun_position
+ return importlib.import_module(package_name)
except ModuleNotFoundError:
pass
- return sun_position
-
@classmethod
def get_sun_props(cls) -> Union[SunPosProperties, None]:
assert (scene := bpy.context.scene)
@@ -1889,6 +1882,24 @@ class Blender(bonsai.core.tool.Blender):
obj.select_set(True)
bpy.context.view_layer.objects.active = previously_active
+ @classmethod
+ def sync_render_visibility(cls):
+ # Doing bpy.ops.object.hide_render_clear_all() or
+ # bpy.ops.object.isolate_type_render() is extremely slow.
+ # Hopefully this doesn't crash on Windows, it doesn't crash on Linux.
+ should_hides = [0 if obj.visible_get() else 1 for obj in bpy.data.objects]
+ should_hides = np.fromiter(should_hides, dtype=np.uint8, count=len(should_hides))
+ bpy.data.objects.foreach_set("hide_render", should_hides)
+ return # Otherwise...
+ # for obj in bpy.data.objects:
+ # if not obj.data:
+ # continue
+ # # For speed, check equality prior to change to prevent needless updates
+ # if (is_visible := obj.visible_get()) and obj.hide_render is True:
+ # obj.hide_render = False
+ # elif not is_visible and obj.hide_render is False:
+ # obj.hide_render = True
+
@classmethod
def hide_objects(cls, objs):
previously_selected = {o.name for o in bpy.context.selected_objects}
diff --git a/src/bonsai/bonsai/tool/collector.py b/src/bonsai/bonsai/tool/collector.py
index 34f471ada4..65d5ae49bd 100644
--- a/src/bonsai/bonsai/tool/collector.py
+++ b/src/bonsai/bonsai/tool/collector.py
@@ -51,9 +51,13 @@ class Collector(bonsai.core.tool.Collector):
if tool.Geometry.is_locked(element):
tool.Geometry.lock_object(obj)
element = (element.PartOfU or element.PartOfV or element.PartOfW)[0]
+ if not tool.Spatial.get_grid_props().is_visible:
+ obj.hide_viewport = True
elif element.is_a("IfcGrid"):
if tool.Geometry.is_locked(element):
tool.Geometry.lock_object(obj)
+ if not tool.Spatial.get_grid_props().is_visible:
+ obj.hide_viewport = True
if element.is_a("IfcProject"):
if tool.Geometry.is_locked(element):
@@ -69,7 +73,8 @@ class Collector(bonsai.core.tool.Collector):
tool.Geometry.lock_object(obj)
collection = cls._create_project_child_collection("IfcSpace")
cls.link_collection_object_safe(collection, obj)
- obj.hide_viewport = True
+ if not tool.Spatial.get_spatial_props().is_visible:
+ obj.hide_viewport = True
elif element.is_a("IfcStructuralItem"):
collection = cls._create_project_child_collection("IfcStructuralItem")
cls.link_collection_object_safe(collection, obj)
@@ -93,7 +98,8 @@ class Collector(bonsai.core.tool.Collector):
cls.link_collection_object_safe(collection, obj)
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection)
- obj.hide_viewport = True
+ if not tool.Spatial.get_spatial_props().is_visible:
+ obj.hide_viewport = True
elif (
tool.Ifc.get_schema() != "IFC2X3"
and element.is_a("IfcSpatialElement")
@@ -105,7 +111,8 @@ class Collector(bonsai.core.tool.Collector):
cls.link_collection_object_safe(collection, obj)
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection)
- obj.hide_viewport = True
+ if not tool.Spatial.get_spatial_props().is_visible:
+ obj.hide_viewport = True
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
if collection := cls._create_own_collection(obj):
cls.link_collection_object_safe(collection, obj)
diff --git a/src/bonsai/bonsai/tool/cost.py b/src/bonsai/bonsai/tool/cost.py
index c65b2a29e2..d1ee2a841c 100644
--- a/src/bonsai/bonsai/tool/cost.py
+++ b/src/bonsai/bonsai/tool/cost.py
@@ -273,18 +273,16 @@ class Cost(bonsai.core.tool.Cost):
return
props = cls.get_cost_props()
props.cost_item_type_products.clear()
- # TODO implement process and resource types
- # props.cost_item_processes.clear()
- # props.cost_item_resources.clear()
+ props.cost_item_processes.clear()
+ props.cost_item_resources.clear()
for rel in cost_item.Controls or []:
for related_object in rel.RelatedObjects:
if related_object.is_a("IfcTypeProduct"):
new = props.cost_item_type_products.add()
- # TODO implement process and resource types
- # elif related_object.is_a("IfcProcess"):
- # new = props.cost_item_processes.add()
- # elif related_object.is_a("IfcResource"):
- # new = props.cost_item_resources.add()
+ elif related_object.is_a("IfcProcess"):
+ new = props.cost_item_processes.add()
+ elif related_object.is_a("IfcResource"):
+ new = props.cost_item_resources.add()
new.ifc_definition_id = related_object.id()
new.name = related_object.Name or "Unnamed"
diff --git a/src/bonsai/bonsai/tool/debug.py b/src/bonsai/bonsai/tool/debug.py
index 079892a9b5..35e877e734 100644
--- a/src/bonsai/bonsai/tool/debug.py
+++ b/src/bonsai/bonsai/tool/debug.py
@@ -18,6 +18,7 @@
from __future__ import annotations
import os
+import re
import json
import bmesh
import bpy
@@ -33,7 +34,7 @@ import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from mathutils import Vector
from collections import defaultdict
-from typing import Literal, TYPE_CHECKING, assert_never
+from typing import Literal, TYPE_CHECKING, assert_never, Union
from collections.abc import Iterable
if TYPE_CHECKING:
@@ -136,12 +137,30 @@ class Debug(bonsai.core.tool.Debug):
"PERSON",
"PERSON_AND_ORGANIZATION",
],
+ by_name_or_identification_only: bool = False,
) -> dict[str, list[str]]:
"""Merge identical objects.
Note that Styles UI (or other UI) should be updated manually after using this method.
+
+ Args:
+ object_type: The type of object to merge
+ by_name_or_identification_only: If True, merge based only on Name attribute (or equivalent identifier).
+ Strips .XXX suffix patterns (e.g., 'foo.001' matches 'foo', 'foo.002').
+ For PERSON, uses Identification. For APPLICATION, uses ApplicationFullName.
+ For PERSON_AND_ORGANIZATION, uses combination of person and organization identifiers.
"""
+ def normalize_name(name: str) -> str:
+ """Remove .XXX suffix pattern from names (e.g., 'foo.001' -> 'foo')"""
+ if not name:
+ return ""
+ # Match pattern: name ending with .digits
+ match = re.match(r"^(.+)\.\d+$", name)
+ if match:
+ return match.group(1)
+ return name
+
def get_hash(element: ifcopenshell.entity_instance) -> int:
data = element.get_info_2(include_identifier=False, recursive=True)
if object_type == "APPLICATION":
@@ -152,6 +171,49 @@ class Debug(bonsai.core.tool.Debug):
data["TheOrganization"] = element.TheOrganization.id()
return hash(json.dumps(data, sort_keys=True))
+ def get_name_key(element: ifcopenshell.entity_instance) -> str:
+ """Get key based on name/identifier attribute for the given object type"""
+ if object_type == "STYLE":
+ name = element.Name if element.Name else ""
+ return normalize_name(name)
+ elif object_type == "MATERIAL":
+ name = element.Name if element.Name else ""
+ return normalize_name(name)
+ elif object_type == "ORGANIZATION":
+ name = element.Name if element.Name else ""
+ return normalize_name(name)
+ elif object_type == "APPLICATION":
+ name = element.ApplicationFullName if element.ApplicationFullName else ""
+ return normalize_name(name)
+ elif object_type == "PERSON":
+ ident = element.Identification if element.Identification else ""
+ return normalize_name(ident)
+ elif object_type == "PERSON_AND_ORGANIZATION":
+ person_id = element.ThePerson.Identification if element.ThePerson.Identification else ""
+ org_name = element.TheOrganization.Name if element.TheOrganization.Name else ""
+ return f"{normalize_name(person_id)}|{normalize_name(org_name)}"
+ else:
+ assert_never(object_type)
+
+ def get_element_name(element: ifcopenshell.entity_instance) -> str:
+ """Get the actual name/identifier from element for sorting purposes"""
+ if object_type == "STYLE":
+ return element.Name if element.Name else ""
+ elif object_type == "MATERIAL":
+ return element.Name if element.Name else ""
+ elif object_type == "ORGANIZATION":
+ return element.Name if element.Name else ""
+ elif object_type == "APPLICATION":
+ return element.ApplicationFullName if element.ApplicationFullName else ""
+ elif object_type == "PERSON":
+ return element.Identification if element.Identification else ""
+ elif object_type == "PERSON_AND_ORGANIZATION":
+ person_id = element.ThePerson.Identification if element.ThePerson.Identification else ""
+ org_name = element.TheOrganization.Name if element.TheOrganization.Name else ""
+ return f"{person_id}|{org_name}"
+ else:
+ assert_never(object_type)
+
ifc_file = tool.Ifc.get()
merged_element_types: dict[str, list[str]] = {}
@@ -179,22 +241,47 @@ class Debug(bonsai.core.tool.Debug):
for element_type in element_types:
elements = ifc_file.by_type(element_type, include_subtypes=False)
- # Calculate hashes.
- hash_to_elements: defaultdict[int, list[ifcopenshell.entity_instance]] = defaultdict(list)
- for element in elements:
- # Except for styles, ignore unnamed elements as they may be not safe to merge
- merge_optional_names = ("STYLE", "PERSON")
- not_optional_name = ("APPLICATION", "ORGANIZATION")
- has_no_name = ("PERSON_AND_ORGANIZATION",)
- if (
- object_type not in merge_optional_names
- and object_type not in not_optional_name
- and object_type not in has_no_name
- and not element.Name
- ):
- continue
- element_hash = get_hash(element)
- hash_to_elements[element_hash].append(element)
+ # Calculate hashes or name keys.
+ hash_to_elements: defaultdict[Union[int, str], list[ifcopenshell.entity_instance]]
+
+ if by_name_or_identification_only:
+ # Group by name/identifier only (with .XXX suffix normalization)
+ hash_to_elements = defaultdict(list)
+ for element in elements:
+ name_key = get_name_key(element)
+ # Skip elements without a valid identifier
+ if not name_key:
+ continue
+ hash_to_elements[name_key].append(element)
+
+ # Sort elements within each group to keep the one without suffix (or lowest suffix)
+ for name_key in hash_to_elements:
+ # Sort by: 1) prefer names without .XXX suffix, 2) then by original name
+ def sort_key(el):
+ name = get_element_name(el)
+ # Check if name has .XXX suffix
+ has_suffix = bool(re.match(r"^.+\.\d+$", name))
+ # Return tuple: (has_suffix, name) - sort by no suffix first, then alphabetically
+ return (has_suffix, name)
+
+ hash_to_elements[name_key].sort(key=sort_key)
+ else:
+ # Group by full hash
+ hash_to_elements = defaultdict(list)
+ for element in elements:
+ # Except for styles, ignore unnamed elements as they may be not safe to merge
+ merge_optional_names = ("STYLE", "PERSON")
+ not_optional_name = ("APPLICATION", "ORGANIZATION")
+ has_no_name = ("PERSON_AND_ORGANIZATION",)
+ if (
+ object_type not in merge_optional_names
+ and object_type not in not_optional_name
+ and object_type not in has_no_name
+ and not element.Name
+ ):
+ continue
+ element_hash = get_hash(element)
+ hash_to_elements[element_hash].append(element)
merged_elements_names: list[str] = []
# Merge elements.
diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py
index 336b3fd0dd..a53272b408 100644
--- a/src/bonsai/bonsai/tool/drawing.py
+++ b/src/bonsai/bonsai/tool/drawing.py
@@ -208,6 +208,27 @@ class Drawing(bonsai.core.tool.Drawing):
return obj
+ @classmethod
+ def get_annotation_drawing(cls, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance | None:
+ for rel in element.HasAssignments:
+ if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING":
+ for e in rel.RelatedObjects:
+ if e.ObjectType == "DRAWING":
+ return e
+
+ @classmethod
+ def exclude_annotation_from_drawing(
+ cls, element: ifcopenshell.entity_instance, drawing: ifcopenshell.entity_instance
+ ) -> None:
+ pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
+ if not pset:
+ pset = ifcopenshell.api.pset.add_pset(ifc_file, product=drawing, name="EPset_Drawing")
+ exclude = ifcopenshell.util.element.get_property_definition(pset, prop="Exclude") or ""
+ if exclude:
+ exclude += "+"
+ exclude += element.GlobalId
+ ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Exclude": exclude})
+
@classmethod
def ensure_annotation_in_drawing_plane(
cls, obj: bpy.types.Object, camera: Optional[bpy.types.Object] = None
@@ -218,11 +239,7 @@ class Drawing(bonsai.core.tool.Drawing):
entity = tool.Ifc.get_entity(obj)
if not entity:
return
- for rel in entity.HasAssignments:
- if rel.is_a("IfcRelAssignsToGroup"):
- for e in rel.RelatedObjects:
- if e.ObjectType == "DRAWING":
- return tool.Ifc.get_object(e)
+ return tool.Ifc.get_object(cls.get_annotation_drawing(entity))
if not camera:
camera = get_camera_from_annotation_object(obj) or bpy.context.scene.camera
@@ -236,7 +253,11 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def setup_annotation_object(
- cls, obj: bpy.types.Object, object_type: str, related_object: Optional[bpy.types.Object] = None
+ cls,
+ obj: bpy.types.Object,
+ object_type: str,
+ related_object: Optional[bpy.types.Object] = None,
+ rotation_mode: str = "NONE",
) -> None:
"""Finish object's adjustments after both object and entity are created"""
@@ -308,6 +329,55 @@ class Drawing(bonsai.core.tool.Drawing):
ifc_file, relating_product=related_entity, related_object=obj_entity
)
+ if rotation_mode != "NONE" and related_object:
+ cls.apply_annotation_rotation(obj, related_object, rotation_mode)
+
+ @classmethod
+ def apply_annotation_rotation(
+ cls, tag_obj: bpy.types.Object, related_object: bpy.types.Object, rotation_mode: str
+ ) -> None:
+ """Apply rotation to annotation based on the selected rotation mode"""
+ camera = bpy.context.scene.camera
+ camera_right = camera.matrix_world.to_3x3() @ mathutils.Vector((1, 0, 0))
+
+ if rotation_mode == "CAMERA_Horizontal":
+ location = tag_obj.location.copy()
+ camera_matrix = camera.matrix_world.copy()
+ camera_matrix.translation = location
+ tag_obj.matrix_world = camera_matrix
+
+ elif rotation_mode == "CAMERA_Vertical":
+ location = tag_obj.location.copy()
+ camera_matrix = camera.matrix_world.copy()
+ camera_matrix.translation = location
+ rotation_90z = mathutils.Matrix.Rotation(math.pi / 2, 4, "Z")
+ camera_matrix = camera_matrix @ rotation_90z
+ tag_obj.matrix_world = camera_matrix
+
+ elif rotation_mode == "LOCAL_X":
+ local_x = related_object.matrix_world.to_3x3() @ mathutils.Vector((1, 0, 0))
+ local_x = local_x.normalized()
+ if local_x.dot(camera_right) < 0:
+ local_x = -local_x
+
+ tag_obj.rotation_euler = local_x.to_track_quat("X", "Z").to_euler()
+
+ elif rotation_mode == "LOCAL_Y":
+ local_y = related_object.matrix_world.to_3x3() @ mathutils.Vector((0, 1, 0))
+ local_y = local_y.normalized()
+ if local_y.dot(camera_right) < 0:
+ local_y = -local_y
+
+ tag_obj.rotation_euler = local_y.to_track_quat("X", "Z").to_euler()
+
+ elif rotation_mode == "LOCAL_Z":
+ local_z = related_object.matrix_world.to_3x3() @ mathutils.Vector((0, 0, 1))
+ local_z = local_z.normalized()
+ if local_z.dot(camera_right) < 0:
+ local_z = -local_z
+
+ tag_obj.rotation_euler = local_z.to_track_quat("X", "Z").to_euler()
+
@classmethod
def is_annotation_object_type(
cls, element: ifcopenshell.entity_instance, object_types: Union[str, Sequence[str]]
@@ -815,7 +885,13 @@ class Drawing(bonsai.core.tool.Drawing):
def get_assigned_product(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
for rel in element.HasAssignments:
if rel.is_a("IfcRelAssignsToProduct"):
- return rel.RelatingProduct
+ product = rel.RelatingProduct
+ if product.is_a("IfcGrid") and rel.Name:
+ for attribute in ("UAxes", "VAxes", "WAxes"):
+ for axis in getattr(product, attribute) or []:
+ if axis.AxisTag == rel.Name:
+ return axis
+ return product
@classmethod
def import_annotations_in_group(cls, group: ifcopenshell.entity_instance) -> None:
@@ -1401,7 +1477,10 @@ class Drawing(bonsai.core.tool.Drawing):
cls, drawing: ifcopenshell.entity_instance
) -> list[ifcopenshell.entity_instance]:
elements = []
- existing_references = cls.get_group_elements(cls.get_drawing_group(drawing))
+ existing_references = set(cls.get_group_elements(cls.get_drawing_group(drawing)))
+ if exclude := ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "Exclude"):
+ existing_references.update(ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), exclude))
+
for element in tool.Ifc.get().by_type("IfcAnnotation"):
if element in existing_references or element == drawing:
continue
@@ -1411,14 +1490,25 @@ class Drawing(bonsai.core.tool.Drawing):
"GlobalReferencing", False
):
elements.append(element)
- for element in tool.Ifc.get().by_type("IfcGridAxis"):
- elements.append(element)
+ for element in tool.Ifc.get().by_type("IfcGrid"):
+ if element in existing_references:
+ continue
+ for axis in element.UAxes + element.VAxes + (element.WAxes or tuple()):
+ if axis in existing_references:
+ continue
+ elements.append(axis)
target_view = tool.Drawing.get_drawing_target_view(drawing)
if target_view in ("SECTION_VIEW", "ELEVATION_VIEW"):
for element in tool.Ifc.get().by_type("IfcBuildingStorey"):
+ if element in existing_references:
+ continue
elements.append(element)
return elements
+ @classmethod
+ def is_auto_annotation(cls, element: ifcopenshell.entity_instance):
+ return element.is_a("IfcAnnotation") and element.ObjectType in ("GRID", "SECTION", "ELEVATION", "SECTION_LEVEL")
+
@classmethod
def get_drawing_reference_annotation(
cls, drawing: ifcopenshell.entity_instance, reference_element: ifcopenshell.entity_instance
@@ -1430,7 +1520,7 @@ class Drawing(bonsai.core.tool.Drawing):
# IfcRelAssignsToProduct.RelatingProduct = IfcGrid
# IfcRelAssignsToProduct.Name = IfcGridAxis.AxisTag
grid = None
- for attribute in ("PartOfW", "PartOfV", "PartOfU"):
+ for attribute in ("PartOfU", "PartOfV", "PartOfW"):
if getattr(reference_element, attribute, None):
grid = getattr(reference_element, attribute)[0]
break
@@ -1452,6 +1542,26 @@ class Drawing(bonsai.core.tool.Drawing):
if rel.is_a("IfcRelAssignsToProduct") and rel.RelatingProduct == reference_element:
return element
+ @classmethod
+ def regenerate_reference_annotation(
+ cls,
+ drawing: ifcopenshell.entity_instance,
+ annotation: ifcopenshell.entity_instance,
+ reference_element: ifcopenshell.entity_instance,
+ context: ifcopenshell.entity_instance,
+ ) -> ifcopenshell.entity_instance:
+ if reference_element.is_a("IfcGridAxis"):
+ return cls.regenerate_grid_axis_reference_annotation(drawing, annotation, reference_element, context)
+ elif reference_element.is_a("IfcAnnotation") and reference_element.ObjectType == "DRAWING":
+ target_view = ifcopenshell.util.element.get_pset(reference_element, "EPset_Drawing", "TargetView")
+ if target_view == "ELEVATION_VIEW":
+ return cls.regenerate_elevation_reference_annotation(drawing, annotation, reference_element, context)
+ elif target_view == "SECTION_VIEW":
+ return cls.regenerate_section_reference_annotation(drawing, annotation, reference_element, context)
+ elif reference_element.is_a("IfcBuildingStorey"):
+ return cls.regenerate_storey_annotation(drawing, annotation, reference_element, context)
+ return annotation
+
@classmethod
def generate_reference_annotation(
cls,
@@ -1461,333 +1571,410 @@ class Drawing(bonsai.core.tool.Drawing):
) -> ifcopenshell.entity_instance:
if reference_element.is_a("IfcGridAxis"):
return cls.generate_grid_axis_reference_annotation(drawing, reference_element, context)
-
elif reference_element.is_a("IfcAnnotation") and reference_element.ObjectType == "DRAWING":
-
- def ensure_referenced_drawing_obj_exists(drawing: ifcopenshell.entity_instance):
- obj = tool.Ifc.get_object(drawing)
- if obj is None:
- # Annotations in that drawing are lazy loaded as needed
- obj = cls.import_drawing(drawing)
- tool.Blender.get_layer_collection(obj.users_collection[0]).hide_viewport = True
-
target_view = ifcopenshell.util.element.get_pset(reference_element, "EPset_Drawing", "TargetView")
if target_view == "ELEVATION_VIEW":
- ensure_referenced_drawing_obj_exists(reference_element)
return cls.generate_elevation_reference_annotation(drawing, reference_element, context)
elif target_view == "SECTION_VIEW":
- ensure_referenced_drawing_obj_exists(reference_element)
return cls.generate_section_reference_annotation(drawing, reference_element, context)
-
elif reference_element.is_a("IfcBuildingStorey"):
return cls.generate_storey_annotation(drawing, reference_element, context)
+ @classmethod
+ def generate_storey_points(
+ cls, drawing: ifcopenshell.entity_instance, storey: ifcopenshell.entity_instance
+ ) -> list | None:
+ import bonsai.bim.module.drawing.helper as helper
+
+ camera = tool.Ifc.get_object(drawing)
+ if camera.data.type != "ORTHO":
+ return
+ if not cls.is_matrix_perpendicular(camera.matrix_world, Matrix()):
+ return
+
+ xmin, xmax, ymin, ymax = helper.ortho_view_frame(camera.data)[:4]
+ rl = ifcopenshell.util.placement.get_local_placement(storey.ObjectPlacement)[2][3]
+ y = (camera.matrix_world.inverted() @ Vector((0.0, 0.0, rl))).y
+ if y < ymin or y > ymax:
+ return
+
+ return (Vector((xmax, y, 0.0)), Vector((xmin, y, 0.0)))
+
@classmethod
def generate_storey_annotation(
cls,
drawing: ifcopenshell.entity_instance,
- reference_element: ifcopenshell.entity_instance,
+ storey: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
+ if not (points := cls.generate_storey_points(drawing, storey)):
+ return
+
+ camera = tool.Ifc.get_object(drawing)
+ mesh = bpy.data.meshes.new("Mesh")
+ obj = bpy.data.objects.new(storey.Name or "Unnamed", mesh)
+ obj.matrix_world = cls.get_default_annotation_matrix(camera)
+ element = cls.run_root_assign_class(
+ obj=obj, ifc_class="IfcAnnotation", predefined_type="SECTION_LEVEL", should_add_representation=False
+ )
+ tool.Geometry.run_edit_object_placement(obj)
+ element.Name = storey.Name or "Unnamed"
+ builder = ShapeBuilder(tool.Ifc.get())
+ unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
+ points = [p / unit_scale for p in points]
+ representation = builder.get_representation(context, [builder.polyline(points)])
+ ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
+ bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation)
+ return element
+
+ @classmethod
+ def regenerate_storey_annotation(
+ cls,
+ drawing: ifcopenshell.entity_instance,
+ annotation: ifcopenshell.entity_instance,
+ storey: ifcopenshell.entity_instance,
+ context: ifcopenshell.entity_instance,
+ ) -> ifcopenshell.entity_instance:
+ if not (points := cls.generate_storey_points(drawing, storey)):
+ return
+
+ camera = tool.Ifc.get_object(drawing)
+ settings = ifcopenshell.geom.settings()
+ settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
+ shape = ifcopenshell.geom.create_shape(settings, annotation)
+ m = ifcopenshell.util.shape.get_shape_matrix(shape)
+ mw = cls.get_default_annotation_matrix(camera)
+ existing_verts = [Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
+
+ new_points = None
+ if not np.allclose(m, np.array(mw), atol=1e-4):
+ new_points = points
+ elif len(existing_verts) != 2:
+ new_points = points
+ else:
+ existing_verts = sorted(existing_verts, key=lambda v: v.x)
+ xmin, xmax = [v.x for v in existing_verts]
+ y = points[0].y
+ if not tool.Cad.is_x(y, existing_verts[0].y) or not tool.Cad.is_x(y, existing_verts[1].y):
+ new_points = (Vector((xmax, y, 0.0)), Vector((xmin, y, 0.0)))
+
+ if new_points:
+ if representation := ifcopenshell.util.representation.get_representation(annotation, context):
+ ifcopenshell.api.geometry.unassign_representation(
+ tool.Ifc.get(), product=annotation, representation=representation
+ )
+ builder = ShapeBuilder(tool.Ifc.get())
+ unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
+ new_points = [p / unit_scale for p in new_points]
+ representation = builder.get_representation(context, [builder.polyline(new_points)])
+ ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), annotation, representation)
+
+ if obj := tool.Ifc.get_object(annotation):
+ obj.matrix_world = mw
+ bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
+ bonsai.core.geometry.switch_representation(
+ tool.Ifc, tool.Geometry, obj=obj, representation=representation
+ )
+ else:
+ ifcopenshell.api.geometry.edit_object_placement(tool.Ifc.get(), product=annotation, matrix=np.array(mw))
+
+ annotation.Name = storey.Name or "Unnamed"
+ return annotation
+
+ @classmethod
+ def generate_section_reference_points(
+ cls, drawing: ifcopenshell.entity_instance, section: ifcopenshell.entity_instance
+ ) -> list | None:
import bonsai.bim.module.drawing.helper as helper
camera = tool.Ifc.get_object(drawing)
- assert isinstance(camera, bpy.types.Object)
- assert isinstance((camera_data := camera.data), bpy.types.Camera)
- props = tool.Drawing.get_camera_props(camera_data)
+ if camera.data.type != "ORTHO":
+ return
+ settings = ifcopenshell.geom.settings()
+ shape = ifcopenshell.geom.create_shape(settings, section)
+ m = ifcopenshell.util.shape.get_shape_matrix(shape)
+ if not cls.is_matrix_perpendicular(camera.matrix_world, Matrix(m)):
+ return
+ if not cls.does_shape_intersect_camera(shape, camera):
+ return
- bounds = helper.ortho_view_frame(camera_data) if camera_data.type == "ORTHO" else None
- reference_obj = tool.Ifc.get_object(reference_element)
- assert isinstance(reference_obj, bpy.types.Object)
+ # Get cutting plane as a line
+ verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
+ cutting_plane_verts = sorted(verts, key=lambda v: v[2])[-4:]
+ v1, *_, v2 = sorted(cutting_plane_verts, key=lambda v: v[0]) # Cut is in +X direction
+ im = camera.matrix_world.inverted()
+ v1, v2 = [im @ Vector((m @ np.append(v, 1.0))[:3]) for v in [v1, v2]]
- def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
- mat = reference_obj.matrix_world.copy()
- xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
- xyz[2] = 0
- xyz = camera.matrix_world @ xyz
- mat.translation = xyz
- annotation_offset = mathutils.Vector((0, 0, -camera_data.clip_start - 0.05))
- annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
- mat.translation += annotation_offset
- return mat
-
- def project_point_onto_camera(point: Vector, camera: bpy.types.Object) -> Vector:
- projection = camera.matrix_world.to_quaternion() @ mathutils.Vector((0, 0, -1))
- return camera.matrix_world.inverted() @ mathutils.geometry.intersect_line_plane(
- point.xyz, point.xyz - projection, camera.location, projection
- )
-
- obj_matrix = to_camera_coords(camera, reference_obj)
-
- if props.raster_x > props.raster_y:
- width = camera_data.ortho_scale
- height = width / props.raster_x * props.raster_y
- else:
- height = camera_data.ortho_scale
- width = height / props.raster_y * props.raster_x
-
- projection = project_point_onto_camera(reference_obj.location, camera)
- co1 = camera.matrix_world @ mathutils.Vector((width / 2, projection[1], -1))
- co2 = camera.matrix_world @ mathutils.Vector((-(width / 2), projection[1], -1))
- co1 = obj_matrix.inverted() @ co1
- co2 = obj_matrix.inverted() @ co2
-
- data = bpy.data.curves.new("Annotation", type="CURVE")
- data.dimensions = "3D"
- data.resolution_u = 2
-
- polyline = data.splines.new("POLY")
- polyline.points.add(1)
- polyline.points[-2].co = list(co1) + [1]
- polyline.points[-1].co = list(co2) + [1]
-
- obj = bpy.data.objects.new(reference_obj.name, data)
- obj.matrix_world = obj_matrix
-
- element = cls.run_root_assign_class(
- obj=obj,
- ifc_class="IfcAnnotation",
- predefined_type="SECTION_LEVEL",
- should_add_representation=True,
- context=context,
- ifc_representation_class=None,
- )
- if representation := ifcopenshell.util.representation.get_representation(element, context):
- cls.reload_representation(obj=obj, representation=representation)
- bpy.data.curves.remove(data)
-
- return element
+ bounds = helper.ortho_view_frame(camera.data)
+ if not (points := helper.clip_segment(bounds, [v1, v2])):
+ return
+ for v in points:
+ v.z = 0
+ return points
@classmethod
def generate_section_reference_annotation(
cls,
drawing: ifcopenshell.entity_instance,
- reference_element: ifcopenshell.entity_instance,
+ section: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
- import bonsai.bim.module.drawing.helper as helper
+ if not (points := cls.generate_section_reference_points(drawing, section)):
+ return
- reference_obj = tool.Ifc.get_object(reference_element)
- reference_obj.matrix_world
camera = tool.Ifc.get_object(drawing)
- bounds = helper.ortho_view_frame(camera.data) if camera.data.type == "ORTHO" else None
+ mesh = bpy.data.meshes.new("Mesh")
+ obj = bpy.data.objects.new(section.Name or "Unnamed", mesh)
+ obj.matrix_world = cls.get_default_annotation_matrix(camera)
+ element = cls.run_root_assign_class(
+ obj=obj, ifc_class="IfcAnnotation", predefined_type="SECTION", should_add_representation=False
+ )
+ element.Name = section.Name or "Unnamed"
+ builder = ShapeBuilder(tool.Ifc.get())
+ unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
+ points = [p / unit_scale for p in points]
+ representation = builder.get_representation(context, [builder.polyline(points)])
+ ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
+ bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation)
+ return element
- def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
- mat = reference_obj.matrix_world.copy()
- xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
- xyz[2] = 0
- xyz = camera.matrix_world @ xyz
- mat.translation = xyz
- annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05))
- annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
- mat.translation += annotation_offset
- return mat
+ @classmethod
+ def regenerate_section_reference_annotation(
+ cls,
+ drawing: ifcopenshell.entity_instance,
+ annotation: ifcopenshell.entity_instance,
+ section: ifcopenshell.entity_instance,
+ context: ifcopenshell.entity_instance,
+ ) -> ifcopenshell.entity_instance:
+ if not (points := cls.generate_section_reference_points(drawing, section)):
+ return
- def clip_to_camera_boundary(
- mesh: bpy.types.Mesh, bounds: tuple[float, float, float, float, float, float]
- ) -> Union[bpy.types.Mesh, None]:
- mesh.verts.ensure_lookup_table()
- points = [v.co for v in mesh.verts[0:2]]
- points = helper.clip_segment(bounds, points)
- if points is None:
- return None
- mesh.verts[0].co = points[0]
- mesh.verts[1].co = points[1]
- return mesh
+ camera = tool.Ifc.get_object(drawing)
+ settings = ifcopenshell.geom.settings()
+ settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
+ shape = ifcopenshell.geom.create_shape(settings, annotation)
+ m = ifcopenshell.util.shape.get_shape_matrix(shape)
+ mw = cls.get_default_annotation_matrix(camera)
+ existing_verts = [Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
- if cls.is_perpendicular(camera, reference_obj) and cls.is_intersecting(camera, reference_obj):
- reference_mesh = cls.get_camera_block(reference_obj)
- obj_matrix = to_camera_coords(camera, reference_obj)
+ new_points = None
+ if not np.allclose(m, np.array(mw), atol=1e-4):
+ new_points = points
+ elif len(existing_verts) != 2:
+ new_points = points
+ else:
+ if not tool.Cad.are_edges_collinear(existing_verts, points):
+ # Attempt to update the section line by projecting existing verts onto the new line
+ v1 = tool.Cad.point_on_edge(existing_verts[0], points)
+ v2 = tool.Cad.point_on_edge(existing_verts[1], points)
+ existing_length = (existing_verts[0] - existing_verts[1]).length
+ new_length = (v2 - v1).length
+ if abs((existing_length - new_length) / existing_length) <= 0.10:
+ # If the projected line is within 10% of the previous length ...
+ new_points = (v1, v2)
+ else:
+ new_points = points
- # The reference mesh vertices represent a view cube. To convert
- # this into a section line we:
- # 1. Select the 4 +Z vertices local to the reference element. This
- # is the cutting plane.
- verts_local_to_reference = [reference_obj.matrix_world.inverted() @ v for v in reference_mesh["verts"]]
- cutting_plane_verts = sorted(verts_local_to_reference, key=lambda x: x.z)[-4:]
- global_cutting_plane_verts = [reference_obj.matrix_world @ v for v in cutting_plane_verts]
- # 2. Project the cutting plane onto our viewing camera.
- verts_local_to_camera = [camera.matrix_world.inverted() @ v for v in global_cutting_plane_verts]
- # 3. Collapse verts with the same XY coords, and set Z to be just
- # below the clip_start so it's visible
- collapsed_verts = []
- for vert in verts_local_to_camera:
- if not [True for v in collapsed_verts if (vert.xy - v.xy).length < 1e-2]:
- collapsed_verts.append(mathutils.Vector((vert.x, vert.y, -camera.data.clip_start - 0.05)))
- # 4. The first two vertices is the section line
- section_line = collapsed_verts[0:2]
- # 5. Sort the vertices in the +X direction so that the vertices are
- # ordered to "point" in the direction of the section cut.
- section_line = sorted(
- section_line, key=lambda co: (reference_obj.matrix_world.inverted() @ camera.matrix_world @ co).x
- )
- global_section_line = [camera.matrix_world @ v for v in section_line]
- local_section_line = [obj_matrix.inverted() @ v for v in global_section_line]
-
- mesh = bpy.data.meshes.new(name="Annotation")
- mesh.from_pydata(local_section_line, [(0, 1)], [])
- bm = bmesh.new()
- bm.from_mesh(mesh)
- bm = clip_to_camera_boundary(bm, bounds)
- bm.to_mesh(mesh)
- bm.free()
-
- obj = bpy.data.objects.new(reference_obj.name, mesh)
- obj.matrix_world = obj_matrix
-
- element = cls.run_root_assign_class(
- obj=obj,
- ifc_class="IfcAnnotation",
- predefined_type="SECTION",
- should_add_representation=True,
- context=context,
- ifc_representation_class=None,
- )
- if representation := ifcopenshell.util.representation.get_representation(element, context):
- cls.reload_representation(obj=obj, representation=representation)
- return element
+ if new_points:
+ if representation := ifcopenshell.util.representation.get_representation(annotation, context):
+ ifcopenshell.api.geometry.unassign_representation(
+ tool.Ifc.get(), product=annotation, representation=representation
+ )
+ builder = ShapeBuilder(tool.Ifc.get())
+ unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
+ new_points = [p / unit_scale for p in new_points]
+ representation = builder.get_representation(context, [builder.polyline(new_points)])
+ ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), annotation, representation)
+ if obj := tool.Ifc.get_object(annotation):
+ obj.matrix_world = mw
+ bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
+ bonsai.core.geometry.switch_representation(
+ tool.Ifc, tool.Geometry, obj=obj, representation=representation
+ )
+ else:
+ ifcopenshell.api.geometry.edit_object_placement(tool.Ifc.get(), product=annotation, matrix=np.array(mw))
+ annotation.Name = section.Name or "Unnamed"
+ return annotation
@classmethod
def generate_elevation_reference_annotation(
cls,
drawing: ifcopenshell.entity_instance,
- reference_element: ifcopenshell.entity_instance,
+ elevation: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
- reference_obj = tool.Ifc.get_object(reference_element)
- reference_obj.matrix_world
camera = tool.Ifc.get_object(drawing)
-
- def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
- mat = reference_obj.matrix_world.copy()
- xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
- xyz[2] = 0
- xyz = camera.matrix_world @ xyz
- mat.translation = xyz
- annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05))
- annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
- mat.translation += annotation_offset
- return mat
-
- if cls.is_perpendicular(camera, reference_obj) and cls.is_intersecting(camera, reference_obj):
- obj = bpy.data.objects.new(reference_obj.name, None)
+ if camera.data.type != "ORTHO":
+ return
+ settings = ifcopenshell.geom.settings()
+ shape = ifcopenshell.geom.create_shape(settings, elevation)
+ m = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
+ if cls.is_matrix_perpendicular(camera.matrix_world, m) and cls.does_shape_intersect_camera(shape, camera):
+ obj = bpy.data.objects.new(elevation.Name or "Unnamed", None)
obj.empty_display_size = 0.1
- obj.matrix_world = to_camera_coords(camera, reference_obj)
-
+ obj.matrix_world = cls.get_default_annotation_matrix(camera, matrix_world=m)
element = cls.run_root_assign_class(
- obj=obj,
- ifc_class="IfcAnnotation",
- predefined_type="ELEVATION",
- should_add_representation=False,
- context=context,
- ifc_representation_class=None,
+ obj=obj, ifc_class="IfcAnnotation", predefined_type="ELEVATION", should_add_representation=False
)
- if representation := ifcopenshell.util.representation.get_representation(element, context):
- cls.reload_representation(obj=obj, representation=representation)
+ element.Name = elevation.Name or "Unnamed"
return element
+ @classmethod
+ def regenerate_elevation_reference_annotation(
+ cls,
+ drawing: ifcopenshell.entity_instance,
+ annotation: ifcopenshell.entity_instance,
+ elevation: ifcopenshell.entity_instance,
+ context: ifcopenshell.entity_instance,
+ ) -> ifcopenshell.entity_instance:
+ camera = tool.Ifc.get_object(drawing)
+ if camera.data.type != "ORTHO":
+ return
+ settings = ifcopenshell.geom.settings()
+ shape = ifcopenshell.geom.create_shape(settings, elevation)
+ m = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
+ if cls.is_matrix_perpendicular(camera.matrix_world, m) and cls.does_shape_intersect_camera(shape, camera):
+ existing_matrix = Matrix(ifcopenshell.util.placement.get_local_placement(annotation.ObjectPlacement))
+ # The user is allowed to shift the elevation, but not rotate it
+ if not np.allclose(np.array(m.to_3x3()), np.array(existing_matrix.to_3x3()), atol=1e-4):
+ mw = cls.get_default_annotation_matrix(camera, matrix_world=m)
+ if obj := tool.Ifc.get_object(annotation):
+ obj.matrix_world = mw
+ bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
+ else:
+ ifcopenshell.api.geometry.edit_object_placement(
+ tool.Ifc.get(), product=annotation, matrix=np.array(mw)
+ )
+ annotation.Name = elevation.Name or "Unnamed"
+ return annotation
+
+ @classmethod
+ def generate_grid_axis_reference_points(
+ cls, drawing: ifcopenshell.entity_instance, axis: ifcopenshell.entity_instance
+ ) -> list | None:
+ import bonsai.bim.module.drawing.helper as helper
+
+ camera = tool.Ifc.get_object(drawing)
+ if camera.data.type != "ORTHO":
+ return
+
+ settings = ifcopenshell.geom.settings()
+ settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
+ geometry = ifcopenshell.geom.create_shape(settings, axis.AxisCurve)
+ verts = ifcopenshell.util.shape.get_vertices(geometry)
+ grid = (axis.PartOfU or axis.PartOfV or axis.PartOfW)[0]
+ m = ifcopenshell.util.placement.get_local_placement(grid.ObjectPlacement)
+ im = camera.matrix_world.inverted()
+ v1, v2 = [im @ Vector((m @ np.append(v, 1.0))[:3]) for v in verts[:2]]
+
+ target_view = tool.Drawing.get_drawing_target_view(drawing)
+ if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW"):
+ bounds = helper.ortho_view_frame(camera.data)
+ if not (points := helper.clip_segment(bounds, [v1, v2])):
+ return
+ elif target_view in ("ELEVATION_VIEW", "SECTION_VIEW"):
+ bounds = helper.ortho_view_frame(camera.data)
+ if not (points := helper.elevate_segment(bounds, [v1, v2])):
+ return
+ else:
+ return
+ for v in points:
+ v.z = 0
+ return points
+
@classmethod
def generate_grid_axis_reference_annotation(
cls,
drawing: ifcopenshell.entity_instance,
- reference_element: ifcopenshell.entity_instance,
+ axis: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> Union[ifcopenshell.entity_instance, None]:
- import bonsai.bim.module.drawing.helper as helper
-
- target_view = tool.Drawing.get_drawing_target_view(drawing)
+ if not (points := cls.generate_grid_axis_reference_points(drawing, axis)):
+ return
camera = tool.Ifc.get_object(drawing)
- assert isinstance(camera, bpy.types.Object)
- assert isinstance(camera_data := camera.data, bpy.types.Camera)
-
- is_ortho = camera_data.type == "ORTHO"
- bounds = helper.ortho_view_frame(camera_data) if is_ortho else None
- clipping = is_ortho and target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW")
- elevating = is_ortho and target_view in ("ELEVATION_VIEW", "SECTION_VIEW")
-
- def clone(src: bpy.types.Object) -> bpy.types.Object:
- dst = src.copy()
- assert isinstance(dst.data, bpy.types.Mesh)
- dst.data = dst.data.copy()
- dst.name = dst.name.replace("IfcGridAxis/", "")
- tool.Blender.get_object_bim_props(dst).ifc_definition_id = 0
- tool.Geometry.get_mesh_props(dst.data).ifc_definition_id = 0
- return dst
-
- def disassemble(obj: bpy.types.Object) -> tuple[bpy.types.Object, bmesh.types.BMesh]:
- assert isinstance(obj.data, bpy.types.Mesh)
- mesh = bmesh.new()
- mesh.verts.ensure_lookup_table()
- mesh.from_mesh(obj.data)
- return obj, mesh
-
- def assemble(obj: bpy.types.Object, mesh: bmesh.types.BMesh) -> bpy.types.Object:
- assert isinstance(obj.data, bpy.types.Mesh)
- mesh.to_mesh(obj.data)
- return obj
-
- def to_camera_coords(
- obj: bpy.types.Object, mesh: bmesh.types.BMesh
- ) -> tuple[bpy.types.Object, bmesh.types.BMesh]:
- mesh.transform(camera.matrix_world.inverted() @ obj.matrix_world)
- obj.matrix_world = camera.matrix_world
- annotation_offset = mathutils.Vector((0, 0, -camera_data.clip_start - 0.05))
- annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
- obj.matrix_world.translation += annotation_offset
- return obj, mesh
-
- def clip_to_camera_boundary(mesh: bmesh.types.BMesh) -> bmesh.types.BMesh:
- mesh.verts.ensure_lookup_table()
- points = [v.co for v in mesh.verts[0:2]]
- points = helper.clip_segment(bounds, points)
- if points is None:
- return None
- mesh.verts[0].co = points[0]
- mesh.verts[1].co = points[1]
- return mesh
-
- def draw_grids_vertically(mesh: bmesh.types.BMesh) -> bmesh.types.BMesh:
- mesh.verts.ensure_lookup_table()
- points = [v.co for v in mesh.verts[0:2]]
- points = helper.elevate_segment(bounds, points)
- if points is None:
- return None
- points = helper.clip_segment(bounds, points)
- if points is None:
- return None
- mesh.verts[0].co = points[0]
- mesh.verts[1].co = points[1]
- return mesh
-
- obj = tool.Ifc.get_object(reference_element)
- if not obj:
- return
- assert isinstance(obj, bpy.types.Object)
- obj, mesh = to_camera_coords(*disassemble(clone(obj)))
-
- if clipping:
- mesh = clip_to_camera_boundary(mesh)
- elif elevating:
- mesh = draw_grids_vertically(mesh)
-
- if mesh is None:
- return
-
- assemble(obj, mesh)
-
+ mesh = bpy.data.meshes.new("Mesh")
+ obj = bpy.data.objects.new(axis.AxisTag or "-", mesh)
+ obj.matrix_world = cls.get_default_annotation_matrix(camera)
element = cls.run_root_assign_class(
- obj=obj,
- ifc_class="IfcAnnotation",
- predefined_type="GRID",
- should_add_representation=True,
- context=context,
- ifc_representation_class=None,
+ obj=obj, ifc_class="IfcAnnotation", predefined_type="GRID", should_add_representation=False
)
- if representation := ifcopenshell.util.representation.get_representation(element, context):
- cls.reload_representation(obj=obj, representation=representation)
+ element.Name = axis.AxisTag or "-"
+ builder = ShapeBuilder(tool.Ifc.get())
+ unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
+ points = [p / unit_scale for p in points]
+ representation = builder.get_representation(context, [builder.polyline(points)])
+ ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
+ bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation)
return element
+ @classmethod
+ def regenerate_grid_axis_reference_annotation(
+ cls,
+ drawing: ifcopenshell.entity_instance,
+ annotation: ifcopenshell.entity_instance,
+ axis: ifcopenshell.entity_instance,
+ context: ifcopenshell.entity_instance,
+ ) -> Union[ifcopenshell.entity_instance, None]:
+ if not (points := cls.generate_grid_axis_reference_points(drawing, axis)):
+ return
+
+ camera = tool.Ifc.get_object(drawing)
+ settings = ifcopenshell.geom.settings()
+ settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
+ shape = ifcopenshell.geom.create_shape(settings, annotation)
+ m = ifcopenshell.util.shape.get_shape_matrix(shape)
+ mw = cls.get_default_annotation_matrix(camera)
+ existing_verts = [Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
+
+ new_points = None
+ if not np.allclose(m, np.array(mw), atol=1e-4):
+ new_points = points
+ elif len(existing_verts) != 2:
+ new_points = points
+ else:
+ if not tool.Cad.are_edges_collinear(existing_verts, points):
+ # Attempt to update the section line by projecting existing verts onto the new line
+ v1 = tool.Cad.point_on_edge(existing_verts[0], points)
+ v2 = tool.Cad.point_on_edge(existing_verts[1], points)
+ existing_length = (existing_verts[0] - existing_verts[1]).length
+ new_length = (v2 - v1).length
+ if abs((existing_length - new_length) / existing_length) <= 0.10:
+ # If the projected line is within 10% of the previous length ...
+ new_points = (v1, v2)
+ else:
+ new_points = points
+
+ if new_points:
+ if representation := ifcopenshell.util.representation.get_representation(annotation, context):
+ ifcopenshell.api.geometry.unassign_representation(
+ tool.Ifc.get(), product=annotation, representation=representation
+ )
+ builder = ShapeBuilder(tool.Ifc.get())
+ unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
+ new_points = [p / unit_scale for p in new_points]
+ representation = builder.get_representation(context, [builder.polyline(new_points)])
+ ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), annotation, representation)
+ if obj := tool.Ifc.get_object(annotation):
+ obj.matrix_world = mw
+ bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
+ bonsai.core.geometry.switch_representation(
+ tool.Ifc, tool.Geometry, obj=obj, representation=representation
+ )
+ else:
+ ifcopenshell.api.geometry.edit_object_placement(tool.Ifc.get(), product=annotation, matrix=np.array(mw))
+ annotation.Name = axis.AxisTag or "-"
+ return annotation
+
+ @classmethod
+ def get_default_annotation_matrix(cls, camera, matrix_world=None):
+ if matrix_world is None:
+ matrix_world = camera.matrix_world.copy()
+ annotation_offset = Vector((0, 0, -camera.data.clip_start - 0.05))
+ annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
+ matrix_world.translation += annotation_offset
+ return matrix_world
+
@classmethod
def is_perpendicular(cls, a: bpy.types.Object, b: bpy.types.Object) -> bool:
axes = [mathutils.Vector((1, 0, 0)), mathutils.Vector((0, 1, 0)), mathutils.Vector((0, 0, 1))]
@@ -1798,6 +1985,16 @@ class Drawing(bonsai.core.tool.Drawing):
return True
return False
+ @classmethod
+ def is_matrix_perpendicular(cls, a: Matrix, b: Matrix) -> bool:
+ axes = [mathutils.Vector((1, 0, 0)), mathutils.Vector((0, 1, 0)), mathutils.Vector((0, 0, 1))]
+ a_quaternion = a.to_quaternion()
+ b_quaternion = b.to_quaternion()
+ for axis in axes:
+ if abs((a_quaternion @ axis).angle(b_quaternion @ axis) - (math.pi / 2)) < 1e-5:
+ return True
+ return False
+
@classmethod
def get_camera_block(cls, obj: bpy.types.Object) -> dict:
assert isinstance(camera := obj.data, bpy.types.Camera)
@@ -1841,6 +2038,16 @@ class Drawing(bonsai.core.tool.Drawing):
b_tree = mathutils.bvhtree.BVHTree.FromPolygons(b_block["verts"], b_block["faces"])
return bool(a_tree.overlap(b_tree))
+ @classmethod
+ def does_shape_intersect_camera(cls, shape, camera) -> bool:
+ a_block = cls.get_camera_block(camera)
+ a_tree = mathutils.bvhtree.BVHTree.FromPolygons(a_block["verts"], a_block["faces"])
+ m = ifcopenshell.util.shape.get_shape_matrix(shape)
+ verts = [(m @ np.append(v, 1.0))[:3] for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
+ faces = ifcopenshell.util.shape.get_faces(shape.geometry)
+ b_tree = mathutils.bvhtree.BVHTree.FromPolygons(verts, faces)
+ return bool(a_tree.overlap(b_tree))
+
@classmethod
def replace_text_literal_variables(
cls,
@@ -2568,3 +2775,14 @@ class Drawing(bonsai.core.tool.Drawing):
ifcopenshell.api.document.remove_reference(tool.Ifc.get(), reference=reference)
tool.Drawing.import_sheets()
+
+ @classmethod
+ def hide_all_drawing_collections(cls) -> None:
+ for element in tool.Ifc.get().by_type("IfcAnnotation"):
+ if element.ObjectType == "DRAWING" and (obj := tool.Ifc.get_object(element)):
+ tool.Blender.get_layer_collection(obj.users_collection[0]).hide_viewport = True
+
+ @classmethod
+ def clear_annotation_relationships(cls, drawing: ifcopenshell.entity_instance) -> None:
+ for rel in drawing.ReferencedBy:
+ tool.Ifc.get().remove(rel)
diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py
index 8db4264783..6df540ef13 100644
--- a/src/bonsai/bonsai/tool/geometry.py
+++ b/src/bonsai/bonsai/tool/geometry.py
@@ -233,20 +233,20 @@ class Geometry(bonsai.core.tool.Geometry):
element = tool.Ifc.get_entity(obj)
if not element:
return
- elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
- return bonsai.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element)
+ elif element.is_a("IfcAnnotation"):
+ if element.ObjectType == "DRAWING":
+ return bonsai.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element)
+ elif tool.Drawing.is_auto_annotation(element):
+ return # For now, these are special referenced objects and cannot be deleted. Exclude instead.
elif element.is_a("IfcRelSpaceBoundary"):
ifcopenshell.api.boundary.remove_boundary(ifc_file, boundary=element)
tool.Boundary.undecorate_boundary(obj)
return bpy.data.objects.remove(obj)
elif element.is_a("IfcGridAxis"):
- is_last_axis = False
# Deleting the last W axis is OK
if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or (
(grid := element.PartOfV) and len(grid[0].VAxes) == 1
):
- is_last_axis = True
- if is_last_axis:
return
ifcopenshell.api.grid.remove_grid_axis(ifc_file, axis=element)
return bpy.data.objects.remove(obj)
diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py
index 10181c5d93..543106f937 100644
--- a/src/bonsai/bonsai/tool/model.py
+++ b/src/bonsai/bonsai/tool/model.py
@@ -1399,7 +1399,6 @@ class Model(bonsai.core.tool.Model):
number_of_risers = number_of_treads + 1
tread_rise = height / number_of_risers
- custom_tread_run = any(run != 0 for run in custom_first_last_tread_run)
nosing_overlap = max(nosing_length, 0)
nosing_tread_gap = -min(nosing_length, 0)
nosing_overlap_offset = -V_(nosing_overlap, 0)
@@ -1430,19 +1429,26 @@ class Model(bonsai.core.tool.Model):
default_tread_offset = Vector([tread_run, tread_rise])
def get_tread_data(i):
- if custom_tread_run:
- current_tread_run = None
- if i == 0:
- current_tread_run = custom_first_last_tread_run[0]
- elif i == number_of_risers - 1:
- current_tread_run = custom_first_last_tread_run[1]
+ # Check if this is first or last tread with custom run
+ current_tread_run = None
+ if i == 0 and custom_first_last_tread_run[0] is not None:
+ current_tread_run = custom_first_last_tread_run[0]
+ elif i == number_of_risers - 1 and custom_first_last_tread_run[1] is not None:
+ current_tread_run = custom_first_last_tread_run[1]
+
+ if current_tread_run is not None:
+ tread_offset = default_tread_offset.copy()
+ tread_offset.x = current_tread_run
+
+ # Handle zero-width treads
+ if current_tread_run == 0:
+ # For zero width, just return vertical offset with no horizontal tread
+ return tread_offset, ()
+
+ tread_verts = deepcopy(default_tread_verts)
+ tread_verts[-1].x = current_tread_run
+ return tread_offset, tread_verts
- if current_tread_run:
- tread_offset = default_tread_offset.copy()
- tread_offset.x = current_tread_run
- tread_verts = deepcopy(default_tread_verts)
- tread_verts[-1].x = current_tread_run
- return tread_offset, tread_verts
return default_tread_offset, default_tread_verts
# treads
@@ -1450,9 +1456,13 @@ class Model(bonsai.core.tool.Model):
for i in range(number_of_risers):
last_vert_i = len(vertices) - 1
tread_offset, tread_verts = get_tread_data(i)
- current_tread_verts = [v + current_offset for v in tread_verts]
- edges.extend(default_tread_edges + last_vert_i)
- vertices.extend(current_tread_verts)
+
+ # Skip adding vertices/edges for zero-width treads
+ if tread_verts:
+ current_tread_verts = [v + current_offset for v in tread_verts]
+ edges.extend(default_tread_edges + last_vert_i)
+ vertices.extend(current_tread_verts)
+
current_offset += tread_offset
if stair_type == "WOOD/STEEL":
@@ -1467,35 +1477,47 @@ class Model(bonsai.core.tool.Model):
default_tread_offset = V_(tread_run + nosing_tread_gap, tread_rise)
def get_tread_data(i):
- if custom_tread_run:
- current_tread_run = None
- if i == 0 and custom_first_last_tread_run[0] != 0:
- current_tread_run = custom_first_last_tread_run[0]
- elif i == number_of_risers - 1 and custom_first_last_tread_run[1] != 0:
- current_tread_run = custom_first_last_tread_run[1]
+ # Check if this is first or last tread with custom run
+ current_tread_run = None
+ if i == 0 and custom_first_last_tread_run[0] is not None:
+ current_tread_run = custom_first_last_tread_run[0]
+ elif i == number_of_risers - 1 and custom_first_last_tread_run[1] is not None:
+ current_tread_run = custom_first_last_tread_run[1]
+
+ if current_tread_run is not None:
+ tread_offset = default_tread_offset.copy()
+ tread_offset.x = current_tread_run + nosing_tread_gap
+
+ # Handle zero-width treads
+ if current_tread_run == 0:
+ return tread_offset, ()
+
+ tread_verts = get_tread_verts(size=V_(current_tread_run + nosing_overlap, tread_depth))
+ return tread_offset, tread_verts
- if current_tread_run:
- tread_offset = default_tread_offset.copy()
- tread_offset.x = current_tread_run + nosing_tread_gap
- tread_verts = get_tread_verts(size=V_(current_tread_run + nosing_overlap, tread_depth))
- return tread_offset, tread_verts
return default_tread_offset, default_tread_verts
# each tread is a separate shape
cur_offset = V_(0, 0)
+ tread_index = 0
for i in range(number_of_risers):
tread_offset, tread_verts = get_tread_data(i)
- cur_trade_shape = [v + cur_offset + nosing_overlap_offset for v in tread_verts]
- vertices.extend(cur_trade_shape)
- cur_vertex = i * 4
- verts_to_add = (
- (cur_vertex, cur_vertex + 1),
- (cur_vertex + 1, cur_vertex + 2),
- (cur_vertex + 2, cur_vertex + 3),
- (cur_vertex + 3, cur_vertex),
- )
- edges.extend(verts_to_add)
+ # Skip adding vertices/edges for zero-width treads
+ if tread_verts:
+ cur_trade_shape = [v + cur_offset + nosing_overlap_offset for v in tread_verts]
+ vertices.extend(cur_trade_shape)
+
+ cur_vertex = tread_index * 4
+ verts_to_add = (
+ (cur_vertex, cur_vertex + 1),
+ (cur_vertex + 1, cur_vertex + 2),
+ (cur_vertex + 2, cur_vertex + 3),
+ (cur_vertex + 3, cur_vertex),
+ )
+ edges.extend(verts_to_add)
+ tread_index += 1
+
cur_offset += tread_offset
elif stair_type == "GENERIC":
diff --git a/src/bonsai/bonsai/tool/sequence.py b/src/bonsai/bonsai/tool/sequence.py
index 96fbb487a5..14c88dd251 100644
--- a/src/bonsai/bonsai/tool/sequence.py
+++ b/src/bonsai/bonsai/tool/sequence.py
@@ -512,14 +512,12 @@ class Sequence(bonsai.core.tool.Sequence):
@classmethod
def get_task_inputs(cls, task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
props = cls.get_work_schedule_props()
- is_deep = props.show_nested_inputs
- return ifcopenshell.util.sequence.get_task_inputs(task, is_deep)
+ return ifcopenshell.util.sequence.get_task_inputs(task, is_recursive=props.show_nested_inputs)
@classmethod
def get_task_outputs(cls, task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
props = cls.get_work_schedule_props()
- is_deep = props.show_nested_outputs
- return ifcopenshell.util.sequence.get_task_outputs(task, is_deep)
+ return ifcopenshell.util.sequence.get_task_outputs(task, is_recursive=props.show_nested_outputs)
@classmethod
def are_entities_same_class(cls, entities: list[ifcopenshell.entity_instance]) -> bool:
@@ -540,8 +538,7 @@ class Sequence(bonsai.core.tool.Sequence):
if not task:
return
props = cls.get_work_schedule_props()
- is_deep = props.show_nested_resources
- return ifcopenshell.util.sequence.get_task_resources(task, is_deep)
+ return ifcopenshell.util.sequence.get_task_resources(task, props.show_nested_resources)
@classmethod
def load_task_inputs(cls, inputs: list[ifcopenshell.entity_instance]) -> None:
@@ -1582,7 +1579,7 @@ class Sequence(bonsai.core.tool.Sequence):
@classmethod
def create_new_task_json(cls, task, json, type_map=None, baseline_schedule=None):
task_time = task.TaskTime
- resources = ifcopenshell.util.sequence.get_task_resources(task, is_deep=False)
+ resources = ifcopenshell.util.sequence.get_task_resources(task, is_recursive=False)
string_resources = ""
resources_usage = ""
@@ -1689,8 +1686,8 @@ class Sequence(bonsai.core.tool.Sequence):
) -> list[ifcopenshell.entity_instance]:
products = []
for task in ifcopenshell.util.sequence.get_root_tasks(work_schedule):
- products.extend(ifcopenshell.util.sequence.get_task_inputs(task, is_deep=True))
- products.extend(ifcopenshell.util.sequence.get_task_outputs(task, is_deep=True))
+ products.extend(ifcopenshell.util.sequence.get_task_inputs(task, is_recursive=True))
+ products.extend(ifcopenshell.util.sequence.get_task_outputs(task, is_recursive=True))
return products
@classmethod
@@ -1789,6 +1786,16 @@ class Sequence(bonsai.core.tool.Sequence):
cls.load_task_outputs(outputs)
cls.load_task_resources(task)
+ @classmethod
+ def select_active_task_elements(cls, task):
+ if not task:
+ return
+ bpy.ops.object.select_all(action="DESELECT")
+ props = cls.get_work_schedule_props()
+ for element in cls.get_task_inputs(task) | cls.get_task_outputs(task):
+ if obj := tool.Ifc.get_object(element):
+ obj.select_set(True)
+
@classmethod
def refresh_task_resources(cls):
task = cls.get_highlighted_task()
diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py
index a9d7c170ef..ec0835b0a9 100644
--- a/src/bonsai/bonsai/tool/spatial.py
+++ b/src/bonsai/bonsai/tool/spatial.py
@@ -175,7 +175,6 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def select_products(cls, products: Iterable[ifcopenshell.entity_instance], unhide: bool = False) -> None:
- bpy.ops.object.select_all(action="DESELECT")
for product in products:
obj = tool.Ifc.get_object(product)
if obj and bpy.context.view_layer.objects.get(obj.name):
diff --git a/src/bonsai/test/tool/test_model.py b/src/bonsai/test/tool/test_model.py
index 35d5c6bc57..04fdd881a6 100644
--- a/src/bonsai/test/tool/test_model.py
+++ b/src/bonsai/test/tool/test_model.py
@@ -176,6 +176,27 @@ class TestStairCalculatedParams(NewFile):
calculated_data["Length"] += -0.2 + 0.1
self.compare_data(pset_data, calculated_data)
+ # zero-width first tread
+ pset_data = pset_data_base.copy()
+ calculated_data = calculated_data_base.copy()
+ pset_data["custom_first_last_tread_run"] = (0.0, 0.0)
+ calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each
+ self.compare_data(pset_data, calculated_data)
+
+ # zero-width last tread
+ pset_data = pset_data_base.copy()
+ calculated_data = calculated_data_base.copy()
+ pset_data["custom_first_last_tread_run"] = (0.3, 0.0)
+ calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each
+ self.compare_data(pset_data, calculated_data)
+
+ # both first and last treads zero-width
+ pset_data = pset_data_base.copy()
+ calculated_data = calculated_data_base.copy()
+ pset_data["custom_first_last_tread_run"] = (0.0, 0.0)
+ calculated_data["Length"] = 0.6 # Only 2 middle treads at 0.3 each
+ self.compare_data(pset_data, calculated_data)
+
# overlap affects stair length only by first tread
pset_data = pset_data_base.copy()
calculated_data = calculated_data_base.copy()
@@ -296,6 +317,94 @@ class TestGenerateStair2DProfile(NewFile):
generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile)
+ def test_create_concrete_stair_zero_width_first_tread(self):
+ """Test concrete stair with zero-width first tread"""
+ kwargs = {
+ "base_slab_depth": 0.25,
+ "has_top_nib": False,
+ "height": 1.0,
+ "number_of_treads": 3,
+ "stair_type": "CONCRETE",
+ "top_slab_depth": 0.25,
+ "tread_depth": 0.25,
+ "tread_run": 0.3,
+ "width": 1.2,
+ "custom_first_last_tread_run": (0.0, 0.0),
+ }
+ verts_data = (
+ V(0.0, 0, 0.0),
+ # First tread skipped - goes straight to second tread
+ V(0.0, 0, 0.5),
+ V(0.3, 0, 0.5),
+ V(0.3, 0, 0.75),
+ V(0.6, 0, 0.75),
+ V(0.6, 0, 1.0),
+ V(0.9, 0, 1.0),
+ V(0.9, 0, 0.67457),
+ V(0.0, 0, -0.25),
+ )
+ edges_data = (
+ (0, 1),
+ (1, 2),
+ (2, 3),
+ (3, 4),
+ (4, 5),
+ (5, 6),
+ (6, 7),
+ (8, 0),
+ (7, 8),
+ )
+ edges_data = [e[::-1] for e in edges_data]
+ faces_data = ()
+ expected_profile = (verts_data, edges_data, faces_data)
+ generated_profile = subject.generate_stair_2d_profile(**kwargs)
+ self.compare_data(generated_profile, expected_profile)
+
+ def test_create_concrete_stair_zero_width_last_tread(self):
+ """Test concrete stair with zero-width last tread"""
+ kwargs = {
+ "base_slab_depth": 0.25,
+ "has_top_nib": False,
+ "height": 1.0,
+ "number_of_treads": 3,
+ "stair_type": "CONCRETE",
+ "top_slab_depth": 0.25,
+ "tread_depth": 0.25,
+ "tread_run": 0.3,
+ "width": 1.2,
+ "custom_first_last_tread_run": (0.0, 0.0),
+ }
+ verts_data = (
+ V(0.0, 0, 0.0),
+ V(0.0, 0, 0.25),
+ V(0.3, 0, 0.25),
+ V(0.3, 0, 0.5),
+ V(0.6, 0, 0.5),
+ V(0.6, 0, 0.75),
+ V(0.9, 0, 0.75),
+ # Last tread skipped
+ V(0.9, 0, 0.67457),
+ V(0.1, 0, -0.25),
+ V(0.0, 0, -0.25),
+ )
+ edges_data = (
+ (0, 1),
+ (1, 2),
+ (2, 3),
+ (3, 4),
+ (4, 5),
+ (5, 6),
+ (6, 7),
+ (9, 0),
+ (8, 9),
+ (7, 8),
+ )
+ edges_data = [e[::-1] for e in edges_data]
+ faces_data = ()
+ expected_profile = (verts_data, edges_data, faces_data)
+ generated_profile = subject.generate_stair_2d_profile(**kwargs)
+ self.compare_data(generated_profile, expected_profile)
+
def test_create_wood_steel_stair(self):
kwargs = {
"height": 1.0,
@@ -348,6 +457,100 @@ class TestGenerateStair2DProfile(NewFile):
generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile)
+ def test_create_wood_steel_stair_zero_width_first_tread(self):
+ """Test wood/steel stair with zero-width first tread"""
+ kwargs = {
+ "height": 1.0,
+ "number_of_treads": 3,
+ "stair_type": "WOOD/STEEL",
+ "tread_depth": 0.25,
+ "tread_run": 0.3,
+ "width": 1.2,
+ "custom_first_last_tread_run": (0.0, 0.0),
+ }
+ verts_data = (
+ # First tread skipped - start at second tread
+ V(0.0, 0, 0.25),
+ V(0.3, 0, 0.25),
+ V(0.3, 0, 0.5),
+ V(0.0, 0, 0.5),
+ V(0.3, 0, 0.5),
+ V(0.6, 0, 0.5),
+ V(0.6, 0, 0.75),
+ V(0.3, 0, 0.75),
+ V(0.6, 0, 0.75),
+ V(0.9, 0, 0.75),
+ V(0.9, 0, 1.0),
+ V(0.6, 0, 1.0),
+ )
+ edges_data = (
+ (0, 1),
+ (1, 2),
+ (2, 3),
+ (3, 0),
+ (4, 5),
+ (5, 6),
+ (6, 7),
+ (7, 4),
+ (8, 9),
+ (9, 10),
+ (10, 11),
+ (11, 8),
+ )
+
+ faces_data = ()
+
+ expected_profile = (verts_data, edges_data, faces_data)
+ generated_profile = subject.generate_stair_2d_profile(**kwargs)
+ self.compare_data(generated_profile, expected_profile)
+
+ def test_create_wood_steel_stair_zero_width_last_tread(self):
+ """Test wood/steel stair with zero-width last tread"""
+ kwargs = {
+ "height": 1.0,
+ "number_of_treads": 3,
+ "stair_type": "WOOD/STEEL",
+ "tread_depth": 0.25,
+ "tread_run": 0.3,
+ "width": 1.2,
+ "custom_first_last_tread_run": (0.0, 0.0),
+ }
+ verts_data = (
+ V(0.0, 0, 0.0),
+ V(0.3, 0, 0.0),
+ V(0.3, 0, 0.25),
+ V(0.0, 0, 0.25),
+ V(0.3, 0, 0.25),
+ V(0.6, 0, 0.25),
+ V(0.6, 0, 0.5),
+ V(0.3, 0, 0.5),
+ V(0.6, 0, 0.5),
+ V(0.9, 0, 0.5),
+ V(0.9, 0, 0.75),
+ V(0.6, 0, 0.75),
+ # Last tread skipped
+ )
+ edges_data = (
+ (0, 1),
+ (1, 2),
+ (2, 3),
+ (3, 0),
+ (4, 5),
+ (5, 6),
+ (6, 7),
+ (7, 4),
+ (8, 9),
+ (9, 10),
+ (10, 11),
+ (11, 8),
+ )
+
+ faces_data = ()
+
+ expected_profile = (verts_data, edges_data, faces_data)
+ generated_profile = subject.generate_stair_2d_profile(**kwargs)
+ self.compare_data(generated_profile, expected_profile)
+
def test_create_generic_stair(self):
kwargs = {"height": 1.0, "number_of_treads": 3, "stair_type": "GENERIC", "tread_run": 0.3, "width": 1.2}
verts_data = (
@@ -381,6 +584,41 @@ class TestGenerateStair2DProfile(NewFile):
generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile)
+ def test_create_generic_stair_zero_width_treads(self):
+ """Test generic stair with zero-width first and last treads"""
+ kwargs = {
+ "height": 1.0,
+ "number_of_treads": 3,
+ "stair_type": "GENERIC",
+ "tread_run": 0.3,
+ "width": 1.2,
+ "custom_first_last_tread_run": (0.0, 0.0),
+ }
+ verts_data = (
+ V(0.0, 0, 0.0),
+ # First tread skipped
+ V(0.0, 0, 0.5),
+ V(0.3, 0, 0.5),
+ V(0.3, 0, 0.75),
+ V(0.6, 0, 0.75),
+ # Last tread skipped
+ V(0.6, 0, 0.0),
+ )
+ edges_data = (
+ (0, 1),
+ (1, 2),
+ (2, 3),
+ (3, 4),
+ (4, 5),
+ (5, 0),
+ )
+ edges_data = [e[::-1] for e in edges_data]
+
+ faces_data = ()
+ expected_profile = (verts_data, edges_data, faces_data)
+ generated_profile = subject.generate_stair_2d_profile(**kwargs)
+ self.compare_data(generated_profile, expected_profile)
+
class TestUsingArrays(NewFile):
def setup_array(self, add_second_layer=False, sync_children=False):
diff --git a/src/ifc5d/README.md b/src/ifc5d/README.md
index bcf72eefbc..63e133463e 100644
--- a/src/ifc5d/README.md
+++ b/src/ifc5d/README.md
@@ -96,11 +96,11 @@ writer.write()
### CLI app for converting IFC files to CSV, ODS or XLSX format.
Usage:
- python ifc5Dspreadsheet.py input_file output_file [-l log_file] [-f format_type]
+ python ifc5Dspreadsheet.py input_file output_dir [-l log_file] [-f format_type]
Arguments:
input_file (str): The path to the input IFC file to process.
- output_file (str): The output directory for CSV or filename for other formats.
+ output_dir (str): The output directory.
Options:
-l, --log log_file (str): The path to the file where errors should be logged. Default is process.log.
diff --git a/src/ifc5d/ifc5d/ifc5Dspreadsheet.py b/src/ifc5d/ifc5d/ifc5Dspreadsheet.py
index d5f3bba22c..369232c401 100644
--- a/src/ifc5d/ifc5d/ifc5Dspreadsheet.py
+++ b/src/ifc5d/ifc5d/ifc5Dspreadsheet.py
@@ -319,8 +319,8 @@ class Ifc5Dwriter:
cost_schedule: Optional[ifcopenshell.entity_instance] = None,
):
"""
- :param file: IFC file to exprot cost schedules from.
- :param output: Output directory for csv files.
+ :param file: IFC file to export cost schedules from.
+ :param output: Output directory.
:param cost_schedule: exported cost schedule. If not provided, will export all available schedules.
"""
self.output = output
@@ -404,6 +404,7 @@ class Ifc5DCsvWriter(Ifc5Dwriter):
import csv
super().write()
+ os.makedirs(self.output, exist_ok=True)
for sheet, data in self.sheet_data.items():
with open(
os.path.join(self.output, "{}.csv".format(data["Name"])), "w", newline="", encoding="utf-8"
@@ -436,6 +437,7 @@ class Ifc5DOdsWriter(Ifc5Dwriter):
ns1.addElement(Number(decimalplaces="2", minintegerdigits="1", grouping="true"))
self.doc.styles.addElement(ns1)
+ os.makedirs(self.output, exist_ok=True)
file_name = ""
for cost_schedule in self.cost_schedules:
if file_name:
@@ -537,6 +539,7 @@ class Ifc5DXlsxWriter(Ifc5Dwriter):
import xlsxwriter
super().write()
+ os.makedirs(self.output, exist_ok=True)
file_name = ""
for cost_schedule in self.cost_schedules:
if file_name:
@@ -683,7 +686,7 @@ class Ifc5DPdfWriter(Ifc5Dwriter):
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("input", type=str, help="Specify an IFC file to process")
- parser.add_argument("output", help="The output directory for CSV or filename for other formats")
+ parser.add_argument("output", help="The output directory")
parser.add_argument("-l", "--log", type=str, help="Specify where errors should be logged", default="process.log")
parser.add_argument(
"-f", "--format", type=str, help="Choose which format to export in (CSV/ODS/XLSX)", default="CSV"
diff --git a/src/ifcconvert/IfcConvert.cpp b/src/ifcconvert/IfcConvert.cpp
index 376373006f..c41c1eab80 100644
--- a/src/ifcconvert/IfcConvert.cpp
+++ b/src/ifcconvert/IfcConvert.cpp
@@ -207,7 +207,7 @@ size_t read_filters_from_file(const std::string&, inclusion_filter&, inclusion_t
void parse_filter(geom_filter &, const std::vector&);
std::vector setup_filters(const std::vector&, const std::string&);
-bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file, bool no_progress, bool mmap);
+bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file, bool no_progress, bool mmap, bool bypass_properties=false);
// from https://stackoverflow.com/questions/31696328/boost-program-options-using-zero-parameter-options-multiple-times
struct verbosity_counter {
@@ -965,7 +965,10 @@ int main(int argc, char** argv) {
time_t start,end;
time(&start);
- if (!init_input_file(IfcUtil::path::to_utf8(input_filename), ifc_file, no_progress || quiet, mmap)) {
+ // @nb last argument true -> bypass_properties which are not read by any of the geometry serializers
+ // XML, RocksDB, IFC are already special-cased above
+ // SVG requires properties for IfcAnnotation/DRAWING properties
+ if (!init_input_file(IfcUtil::path::to_utf8(input_filename), ifc_file, no_progress || quiet, mmap, output_extension != SVG)) {
write_log(!quiet);
serializer.reset();
IfcUtil::path::delete_file(IfcUtil::path::to_utf8(output_temp_filename)); /**< @todo Windows Unicode support */
@@ -1336,7 +1339,7 @@ void write_log(bool header) {
#include
-bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file, bool no_progress, bool mmap) {
+bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file, bool no_progress, bool mmap, bool bypass_properties) {
time_t start, end;
// Prevent IfcFile::Init() prints by setting output to null temporarily
@@ -1344,20 +1347,36 @@ bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file,
time(&start);
+ bool requires_init = false;
+
#ifdef WITH_IFCXML
if (boost::ends_with(boost::to_lower_copy(filename), ".ifcxml")) {
ifc_file = IfcParse::parse_ifcxml(filename);
- } else
+ } else
#endif
+ {
+ ifc_file = new IfcParse::IfcFile(IfcParse::uninitialized_tag{});
+ requires_init = true;
+ }
- {
+ ifc_file->bypass_type("IfcRelDefinesByProperties");
+ ifc_file->bypass_type("IfcPropertySetDefinition");
+ ifc_file->bypass_type("IfcProperty");
+ ifc_file->bypass_type("IfcMaterialProperties");
+ ifc_file->bypass_type("IfcProfileProperties");
+ ifc_file->bypass_type("IfcPhysicalQuantity");
+
#ifdef USE_MMAP
- ifc_file = new IfcParse::IfcFile(filename, mmap);
+ if (mmap) {
+ ifc_file->initialize(filename, mmap);
+ requires_init = false;
+ }
#else
- (void)mmap;
- ifc_file = new IfcParse::IfcFile(filename);
+ (void)mmap;
#endif
- }
+ if (requires_init) {
+ ifc_file->initialize(filename);
+ }
if (!ifc_file || !ifc_file->good()) {
Logger::Error("Unable to parse input file '" + filename + "'");
diff --git a/src/ifcgeom/infra_sweep_helper.cpp b/src/ifcgeom/infra_sweep_helper.cpp
index f3f79d4c18..f9d50f5b00 100644
--- a/src/ifcgeom/infra_sweep_helper.cpp
+++ b/src/ifcgeom/infra_sweep_helper.cpp
@@ -172,6 +172,7 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_,
}
auto interpolated_loop = polygon_from_points(points);
+ interpolated_loop->external = w1->external;
if (interpolated->kind() == taxonomy::FACE) {
std::static_pointer_cast(interpolated)->children.push_back(interpolated_loop);
} else {
diff --git a/src/ifcgeom/kernels/opencascade/loft.cpp b/src/ifcgeom/kernels/opencascade/loft.cpp
index 0900717bc8..188150b614 100644
--- a/src/ifcgeom/kernels/opencascade/loft.cpp
+++ b/src/ifcgeom/kernels/opencascade/loft.cpp
@@ -118,7 +118,8 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re
auto jt = it + 1;
std::array fa = { *it, *jt };
std::array shps;
- std::array ws;
+ std::vector> ws;
+ ws.emplace_back();
for (int i = 0; i < 2; ++i) {
if (fa[i]->kind() == taxonomy::FACE) {
if (!convert(std::static_pointer_cast(fa[i]), shps[i])) {
@@ -135,11 +136,20 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re
if (shps[i].ShapeType() != TopAbs_FACE && shps[i].ShapeType() != TopAbs_WIRE) {
return false;
}
- // @todo this is only outer wire
+
if (shps[i].ShapeType() == TopAbs_FACE) {
- ws[i] = BRepTools::OuterWire(TopoDS::Face(shps[i]));
+ ws[0][i] = BRepTools::OuterWire(TopoDS::Face(shps[i]));
+ size_t j = 1;
+ for (TopExp_Explorer exp(shps[i], TopAbs_WIRE); exp.More(); exp.Next()) {
+ if (exp.Current() != ws[0][i]) {
+ while (ws.size() <= j) {
+ ws.emplace_back();
+ }
+ ws[j++][i] = TopoDS::Wire(exp.Current());
+ }
+ }
} else {
- ws[i] = TopoDS::Wire(shps[i]);
+ ws[0][i] = TopoDS::Wire(shps[i]);
}
}
if (shps[0].ShapeType() == TopAbs_FACE) {
@@ -154,35 +164,38 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re
BB.Add(comp, shps[1]);
}
}
- BRepTools_WireExplorer a(ws[0]);
- BRepTools_WireExplorer b(ws[1]);
- for (; a.More() && b.More(); a.Next(), b.Next()) {
- auto& e1 = a.Current();
- // auto e3 = TopoDS::Edge(b.Current().Reversed());
- auto& e3 = b.Current();
- // Documentation says unconnected edges are automatically connected, but this is not the case
- TopoDS_Vertex e1a, e1b, e3a, e3b;
- TopExp::Vertices(e1, e1a, e1b, true);
- TopExp::Vertices(e3, e3a, e3b, true);
- auto e2 = BRepBuilderAPI_MakeEdge(e1b, e3a).Edge();
- auto e4 = BRepBuilderAPI_MakeEdge(e3b, e1a).Edge();
-
- /*
- BRepFill_Filling fill;
- fill.Add(e1, GeomAbs_C0);
- fill.Add(e2, GeomAbs_C0);
- fill.Add(e3, GeomAbs_C0);
- fill.Add(e4, GeomAbs_C0);
- fill.Build();
- // faces.Append(fill.Face());
- BB.Add(comp, fill.Face());
- */
+ for (auto& wp : ws) {
+ BRepTools_WireExplorer a(wp[0]);
+ BRepTools_WireExplorer b(wp[1]);
+ for (; a.More() && b.More(); a.Next(), b.Next()) {
+ auto& e1 = a.Current();
+ // auto e3 = TopoDS::Edge(b.Current().Reversed());
+ auto& e3 = b.Current();
- auto f = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e1a, e1b, e3b, true).Wire()).Face();
- BB.Add(comp, f);
- auto g = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e3b, e3a, e1a, true).Wire()).Face();
- BB.Add(comp, g);
+ // Documentation says unconnected edges are automatically connected, but this is not the case
+ TopoDS_Vertex e1a, e1b, e3a, e3b;
+ TopExp::Vertices(e1, e1a, e1b, true);
+ TopExp::Vertices(e3, e3a, e3b, true);
+ auto e2 = BRepBuilderAPI_MakeEdge(e1b, e3a).Edge();
+ auto e4 = BRepBuilderAPI_MakeEdge(e3b, e1a).Edge();
+
+ /*
+ BRepFill_Filling fill;
+ fill.Add(e1, GeomAbs_C0);
+ fill.Add(e2, GeomAbs_C0);
+ fill.Add(e3, GeomAbs_C0);
+ fill.Add(e4, GeomAbs_C0);
+ fill.Build();
+ // faces.Append(fill.Face());
+ BB.Add(comp, fill.Face());
+ */
+
+ auto f = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e1a, e1b, e3b, true).Wire()).Face();
+ BB.Add(comp, f);
+ auto g = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e3b, e3a, e1a, true).Wire()).Face();
+ BB.Add(comp, g);
+ }
}
}
diff --git a/src/ifcgeom/mapping/IfcAxis2PlacementLinear.cpp b/src/ifcgeom/mapping/IfcAxis2PlacementLinear.cpp
index 8e50f24c5c..b11b61b46b 100644
--- a/src/ifcgeom/mapping/IfcAxis2PlacementLinear.cpp
+++ b/src/ifcgeom/mapping/IfcAxis2PlacementLinear.cpp
@@ -25,62 +25,58 @@ using namespace ifcopenshell::geometry;
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcAxis2PlacementLinear* inst) {
- if (!inst->Location()->as())
- Logger::Error(std::runtime_error("Location must be IfcPointByDistanceExpression for IfcAxis2PlacementLinear"));
+ if (!inst->Location()->as()) {
+ Logger::Error(std::runtime_error("Location must be IfcPointByDistanceExpression for IfcAxis2PlacementLinear"));
+ }
- Eigen::Vector3d o, axis(0, 0, 1), refDirection;
+ Eigen::Vector3d o, axis(0, 0, 1), refDirection;
- taxonomy::matrix4::ptr m = taxonomy::cast(map(inst->Location()));
- o = m->components().col(3).head<3>();
+ taxonomy::matrix4::ptr m = taxonomy::cast(map(inst->Location()));
+ o = m->components().col(3).head<3>();
- // From 8.9.3.4 IfcAxis2PlacementLinear there are 4 cases that need to be considered
- // 1) Axis is given but not RefDirection
- // 2) RefDirection is given but not Axis
- // 3) Neither Axis or RefDirection are provided
- // 4) Both Axis and RefDirection are provided
+ // From 8.9.3.4 IfcAxis2PlacementLinear there are 4 cases that need to be considered
+ // 1) Axis is given but not RefDirection
+ // 2) RefDirection is given but not Axis
+ // 3) Neither Axis or RefDirection are provided
+ // 4) Both Axis and RefDirection are provided
- const bool hasAxis = inst->Axis() != nullptr;
- const bool hasRef = inst->RefDirection() != nullptr;
+ const bool hasAxis = inst->Axis() != nullptr;
+ const bool hasRef = inst->RefDirection() != nullptr;
- if (hasAxis != hasRef) {
+ /*
+ if (hasAxis != hasRef) {
Logger::Warning("Axis and RefDirection should be specified together", inst);
}
+ */
- if (hasAxis && !hasRef)
- {
- taxonomy::direction3::ptr a = taxonomy::cast(map(inst->Axis()));
- axis = *a->components_;
+ if (hasAxis && !hasRef) {
+ taxonomy::direction3::ptr a = taxonomy::cast(map(inst->Axis()));
+ axis = *a->components_;
- refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
- // refDirection is not necessarily orthogonal to axis.
- // axis.cross(refDirection) gives y. y.cross(axis) gives x=refDirection
- refDirection = axis.cross(refDirection).cross(axis);
- }
- else if (!hasAxis && hasRef)
- {
- taxonomy::direction3::ptr r = taxonomy::cast(map(inst->RefDirection()));
- refDirection = *r->components_;
- Eigen::Vector3d up(0, 0, 1);
- axis = refDirection.cross(up.cross(refDirection));
- }
- else if (!hasAxis && !hasRef)
- {
- refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
- Eigen::Vector3d up(0, 0, 1);
- axis = refDirection.cross(up.cross(refDirection));
- }
- else
- {
- taxonomy::direction3::ptr a = taxonomy::cast(map(inst->Axis()));
- axis = *a->components_;
+ refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
+ // refDirection is not necessarily orthogonal to axis.
+ // axis.cross(refDirection) gives y. y.cross(axis) gives x=refDirection
+ refDirection = axis.cross(refDirection).cross(axis);
+ } else if (!hasAxis && hasRef) {
+ taxonomy::direction3::ptr r = taxonomy::cast(map(inst->RefDirection()));
+ refDirection = *r->components_;
+ Eigen::Vector3d up(0, 0, 1);
+ axis = refDirection.cross(up.cross(refDirection));
+ } else if (!hasAxis && !hasRef) {
+ refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
+ Eigen::Vector3d up(0, 0, 1);
+ axis = refDirection.cross(up.cross(refDirection));
+ } else {
+ taxonomy::direction3::ptr a = taxonomy::cast(map(inst->Axis()));
+ axis = *a->components_;
- taxonomy::direction3::ptr r = taxonomy::cast(map(inst->RefDirection()));
- refDirection = *r->components_;
- refDirection = axis.cross(refDirection).cross(axis); // refDirection needs to be orthogonal to axis
- }
+ taxonomy::direction3::ptr r = taxonomy::cast(map(inst->RefDirection()));
+ refDirection = *r->components_;
+ refDirection = axis.cross(refDirection).cross(axis); // refDirection needs to be orthogonal to axis
+ }
- // axis and refDirection need to be orthogonal
- return taxonomy::make(o, axis, refDirection);
+ // axis and refDirection need to be orthogonal
+ return taxonomy::make(o, axis, refDirection);
}
#endif
diff --git a/src/ifcgeom/mapping/IfcBoundingBox.cpp b/src/ifcgeom/mapping/IfcBoundingBox.cpp
index 21085fb2bb..0de182a648 100644
--- a/src/ifcgeom/mapping/IfcBoundingBox.cpp
+++ b/src/ifcgeom/mapping/IfcBoundingBox.cpp
@@ -24,7 +24,7 @@ using namespace ifcopenshell::geometry;
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcBoundingBox* inst) {
if (!settings_.get().get()) {
- // @todo make sure it doesn't log.
+ failed_on_purpose_.insert(inst);
return nullptr;
}
diff --git a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp
index 82eea17648..a2bc648f08 100644
--- a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp
+++ b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp
@@ -81,7 +81,13 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in
rot = taxonomy::matrix4(
Eigen::Vector3d(0, 0, 0),
taxonomy::cast(map(csp->Axis()))->ccomponents()).ccomponents().block<3, 3>(0, 0);
- }
+ } else if (csp->RefDirection()) {
+ rot = taxonomy::matrix4(
+ Eigen::Vector3d(0, 0, 0),
+ Eigen::Vector3d(0, 0, 1),
+ taxonomy::cast(map(csp->RefDirection()))->ccomponents()
+ ).ccomponents().block<3, 3>(0, 0);
+ }
profile_rotations.push_back(rot);
}
if (faces.size() != profile_offsets.size()) {
diff --git a/src/ifcgeom/mapping/IfcSectionedSurface.cpp b/src/ifcgeom/mapping/IfcSectionedSurface.cpp
index 1111d4dac5..c6b700f5e5 100644
--- a/src/ifcgeom/mapping/IfcSectionedSurface.cpp
+++ b/src/ifcgeom/mapping/IfcSectionedSurface.cpp
@@ -82,7 +82,14 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSurface* inst) {
rot = taxonomy::matrix4(
Eigen::Vector3d(0, 0, 0),
taxonomy::cast(map(csp->Axis()))->ccomponents()).ccomponents().block<3, 3>(0, 0);
- }
+ } else if (csp->RefDirection()) {
+ rot = taxonomy::matrix4(
+ Eigen::Vector3d(0, 0, 0),
+ Eigen::Vector3d(0, 0, 1),
+ taxonomy::cast(map(csp->RefDirection()))->ccomponents())
+ .ccomponents()
+ .block<3, 3>(0, 0);
+ }
profile_rotations.push_back(rot);
}
#else
diff --git a/src/ifcgeom/mapping/mapping.cpp b/src/ifcgeom/mapping/mapping.cpp
index b4685a7a86..004a41821c 100644
--- a/src/ifcgeom/mapping/mapping.cpp
+++ b/src/ifcgeom/mapping/mapping.cpp
@@ -372,6 +372,18 @@ const IfcUtil::IfcBaseEntity* mapping::get_single_material_association(const Ifc
}
}
#endif
+
+#ifdef SCHEMA_HAS_IfcMaterialConstituentSet
+ if (associated_material->as()) {
+ IfcSchema::IfcMaterialConstituentSet* constituentset = associated_material->as();
+ if (settings_.get().value ? constituentset->MaterialConstituents()->get()->size() >= 1 : constituentset->MaterialConstituents()->get()->size() == 1) {
+ IfcSchema::IfcMaterialConstituent* constituent = (*constituentset->MaterialConstituents()->get()->begin());
+ if (auto* m_ = constituent->Material()) {
+ single_material = m_;
+ }
+ }
+ }
+#endif
}
}
}
diff --git a/src/ifcopenshell-python/Makefile b/src/ifcopenshell-python/Makefile
index aa660d9a9e..c0ecb4ff1f 100644
--- a/src/ifcopenshell-python/Makefile
+++ b/src/ifcopenshell-python/Makefile
@@ -79,6 +79,11 @@ build-urls:
test:
pytest -p no:pytest-blender test --ignore=test/util/test_shape_builder.py
+.PHONY: test-parallel
+test-parallel:
+ @NPROCS=$$(nproc 2>/dev/null || sysctl -n hw.ncpu); \
+ pytest -p no:pytest-blender -n $$NPROCS test --ignore=test/util/test_shape_builder.py
+
.PHONY: test-mathutils
test-mathutils:
pytest -p no:pytest-blender test/util/test_shape_builder.py
diff --git a/src/ifcopenshell-python/ifcopenshell/__init__.py b/src/ifcopenshell-python/ifcopenshell/__init__.py
index 74cee2d701..c12d619b58 100644
--- a/src/ifcopenshell-python/ifcopenshell/__init__.py
+++ b/src/ifcopenshell-python/ifcopenshell/__init__.py
@@ -60,6 +60,7 @@ import zipfile
import tempfile
from pathlib import Path
from typing import Optional, Union, TYPE_CHECKING, Any, overload, Literal
+from collections.abc import Sequence
if TYPE_CHECKING:
import ifcopenshell.express.schema_class
@@ -138,7 +139,12 @@ def open(
path: Union[os.PathLike, str], format: Optional[str] = None, *, should_stream: bool = False, readonly: bool = False
) -> Union[_file, sqlite, _stream]: ...
def open(
- path: Union[os.PathLike, str], format: Optional[str] = None, should_stream: bool = False, readonly: bool = False
+ path: Union[os.PathLike, str],
+ format: Optional[str] = None,
+ should_stream: bool = False,
+ readonly: bool = False,
+ mmap: bool = False,
+ bypass_types: Optional[Sequence[str]] = None,
) -> Union[_file, sqlite, _stream]:
"""Loads an IFC dataset from a filepath
@@ -186,7 +192,17 @@ def open(
if should_stream:
return stream(path)
if readonly: # Temporary conditional see #7131. Remove once newer builds don't segfault on Linux.
- f = ifcopenshell_wrapper.open(str(path.absolute()), readonly)
+ f = ifcopenshell_wrapper.open(str(path.absolute()), readonly=readonly)
+ elif bypass_types:
+ f = ifcopenshell_wrapper.file(ifcopenshell_wrapper.uninitialized_tag())
+ for ty in bypass_types:
+ f.bypass_type(ty)
+ if mmap:
+ f.initialize(str(path.absolute()), mmap=mmap)
+ else:
+ f.initialize(str(path.absolute()))
+ elif mmap:
+ f = ifcopenshell_wrapper.open(str(path.absolute()), mmap=mmap)
else:
f = ifcopenshell_wrapper.open(str(path.absolute()))
return file(f)
@@ -299,20 +315,44 @@ def guess_format(path: Path) -> Literal[".ifc", ".ifcZIP", ".ifcXML", ".ifcJSON"
return None
-def stream2(path: Union[Path, str]):
+def stream2(path: Union[Path, str], mmap: bool = False, page_size: int = 0):
"""Streams the content of a file path from disk, yielding each instance
as a dictionary.
Args:
path (Union[Path, str]): input file path
+ mmap (bool): open the file contents using memory mapping
+ page_size (int): open file in python and feed chunks to the parser
Yields:
dict: entity instance dictionaries
"""
- streamer = ifcopenshell_wrapper.InstanceStreamer(str(path))
- while streamer:
- if inst := streamer.read_instance_py():
- yield inst
+ if page_size:
+ import builtins
+
+ f = builtins.open(path, encoding="ascii")
+ strm = ifcopenshell_wrapper.InstanceStreamer()
+ strm.pushPage(f.read(page_size))
+ finished = False
+ while True:
+ while strm.hasSemicolon():
+ if inst := strm.readInstancePy():
+ yield inst
+ else:
+ finished = True
+ break
+ if finished:
+ break
+ else:
+ if data := f.read(page_size):
+ strm.pushPage(data)
+ else:
+ break
+ else:
+ streamer = ifcopenshell_wrapper.InstanceStreamer(str(path), mmap)
+ while streamer:
+ if inst := streamer.readInstancePy():
+ yield inst
def stream2_from_string(data: str):
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py
index 119ab82a14..ffde70580f 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py
@@ -143,7 +143,7 @@ class Usecase:
return next(e for e in self.file.get_inverse(self.task_time) if e.is_a("IfcTask"))
def handle_resource_calculation(self):
- resources = ifcopenshell.util.sequence.get_task_resources(self.task, is_deep=False)
+ resources = ifcopenshell.util.sequence.get_task_resources(self.task, is_recursive=False)
for resource in resources:
if ifcopenshell.util.constraint.is_attribute_locked(resource, "Usage.ScheduleWork"):
ifcopenshell.api.resource.calculate_resource_usage(self.file, resource=resource)
diff --git a/src/ifcopenshell-python/ifcopenshell/express/cat.py b/src/ifcopenshell-python/ifcopenshell/express/cat.py
new file mode 100644
index 0000000000..d6e7b2c880
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/express/cat.py
@@ -0,0 +1,15 @@
+import sys, fileinput
+
+if sys.platform == "win32" and not hasattr(sys.stdout, 'buffer'):
+ import os, msvcrt
+ msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
+
+files = sys.argv[1:]
+if files[0] == '-o':
+ b = open(files[1], 'wb')
+ files = files[2:]
+else:
+ b = getattr(sys.stdout, 'buffer', sys.stdout)
+
+for line in fileinput.input(files=files, mode='rb'):
+ b.write(line)
diff --git a/src/ifcopenshell-python/ifcopenshell/express/run.bat b/src/ifcopenshell-python/ifcopenshell/express/run.bat
new file mode 100644
index 0000000000..96fd58973d
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/express/run.bat
@@ -0,0 +1,185 @@
+@ECHO OFF
+
+:: python bootstrap.py express.bnf > express_parser.py
+
+IF EXIST IFC2X3_TC1.exp (
+ python express_parser.py IFC2X3_TC1.exp header implementation schema_class definitions
+
+ IF EXIST Ifc2x3-schema.cpp (
+ :: v0.6.0
+ python cat.py -o ..\..\..\ifcparse\Ifc2x3.cpp txt/header_ifc2x3.txt Ifc2x3.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc2x3.h txt/header_ifc2x3.txt Ifc2x3.h
+ python cat.py -o ..\..\..\ifcparse\Ifc2x3-schema.cpp txt/header_ifc2x3.txt Ifc2x3-schema.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc2x3-definitions.h txt/header_ifc2x3.txt Ifc2x3-definitions.h
+ ) ELSE (
+ :: v0.5.0
+ python cat.py -o ..\..\..\ifcparse\Ifc2x3.cpp txt/header_ifc2x3.txt txt/ifndef_ifc4.txt Ifc2x3.cpp txt/endif.txt
+ python cat.py -o ..\..\..\ifcparse\Ifc2x3.h txt/header_ifc2x3.txt Ifc2x3.h
+ python cat.py -o ..\..\..\ifcparse\Ifc2x3enum.h txt/header_ifc2x3.txt Ifc2x3enum.h
+ python cat.py -o ..\..\..\ifcparse\Ifc2x3-latebound.cpp txt/header_ifc2x3.txt txt/ifndef_ifc4.txt Ifc2x3-latebound.cpp txt/endif.txt
+ python cat.py -o ..\..\..\ifcparse\Ifc2x3-latebound.h txt/header_ifc2x3.txt Ifc2x3-latebound.h
+ )
+
+ del *.cpp *.h
+)
+
+IF EXIST IFC4_ADD2TC1.exp (
+ python express_parser.py IFC4_ADD2TC1.exp header implementation schema_class definitions
+
+ IF EXIST Ifc4-schema.cpp (
+ :: v0.6.0
+ python cat.py -o ..\..\..\ifcparse\Ifc4.cpp txt/header_ifc4.txt Ifc4.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4.h txt/header_ifc4.txt Ifc4.h
+ python cat.py -o ..\..\..\ifcparse\Ifc4-schema.cpp txt/header_ifc4.txt Ifc4-schema.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4-definitions.h txt/header_ifc4.txt Ifc4-definitions.h
+ ) ELSE (
+ :: v0.5.0
+ python cat.py -o ..\..\..\ifcparse\Ifc4.cpp txt/header_ifc4.txt txt/ifdef_ifc4.txt Ifc4.cpp txt/endif.txt
+ python cat.py -o ..\..\..\ifcparse\Ifc4.h txt/header_ifc4.txt Ifc4.h
+ python cat.py -o ..\..\..\ifcparse\Ifc4enum.h txt/header_ifc4.txt Ifc4enum.h
+ python cat.py -o ..\..\..\ifcparse\Ifc4-latebound.cpp txt/header_ifc4.txt txt/ifdef_ifc4.txt Ifc4-latebound.cpp txt/endif.txt
+ python cat.py -o ..\..\..\ifcparse\Ifc4-latebound.h txt/header_ifc4.txt Ifc4-latebound.h
+ )
+
+ del *.cpp *.h
+)
+
+IF EXIST IFC4x1.exp (
+ python express_parser.py IFC4x1.exp header implementation schema_class definitions
+
+ IF EXIST Ifc4x1-schema.cpp (
+ :: v0.6.0
+ python cat.py -o ..\..\..\ifcparse\Ifc4x1.cpp txt/header_ifc4x1.txt Ifc4x1.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4x1.h txt/header_ifc4x1.txt Ifc4x1.h
+ python cat.py -o ..\..\..\ifcparse\Ifc4x1-schema.cpp txt/header_ifc4x1.txt Ifc4x1-schema.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4x1-definitions.h txt/header_ifc4x1.txt Ifc4x1-definitions.h
+ )
+
+ del *.cpp *.h
+)
+
+IF EXIST IFC4x2.exp (
+ python express_parser.py IFC4x2.exp header implementation schema_class definitions
+
+ IF EXIST Ifc4x2-schema.cpp (
+ :: v0.6.0
+ python cat.py -o ..\..\..\ifcparse\Ifc4x2.cpp txt/header_ifc4x2.txt Ifc4x2.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4x2.h txt/header_ifc4x2.txt Ifc4x2.h
+ python cat.py -o ..\..\..\ifcparse\Ifc4x2-schema.cpp txt/header_ifc4x2.txt Ifc4x2-schema.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4x2-definitions.h txt/header_ifc4x2.txt Ifc4x2-definitions.h
+ )
+
+ del *.cpp *.h
+)
+
+IF EXIST IFC4x3_RC1.exp (
+ python express_parser.py IFC4x3_RC1.exp header implementation schema_class definitions
+
+ IF EXIST Ifc4x3_rc1-schema.cpp (
+ :: v0.6.0
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc1.cpp txt/header_ifc4x3_rc1.txt Ifc4x3_rc1.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc1.h txt/header_ifc4x3_rc1.txt Ifc4x3_rc1.h
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc1-schema.cpp txt/header_ifc4x3_rc1.txt Ifc4x3_rc1-schema.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc1-definitions.h txt/header_ifc4x3_rc1.txt Ifc4x3_rc1-definitions.h
+ )
+
+ del *.cpp *.h
+)
+
+IF EXIST IFC4x3_RC2.exp (
+ python express_parser.py IFC4x3_RC2.exp header implementation schema_class definitions
+
+ IF EXIST Ifc4x3_rc2-schema.cpp (
+ :: v0.6.0
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc2.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc2.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc2.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc2.h
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc2-schema.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc2-schema.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc2-definitions.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc2-definitions.h
+ )
+
+ del *.cpp *.h
+)
+
+IF EXIST IFC4x3_RC3.exp (
+ python express_parser.py IFC4x3_RC3.exp header implementation schema_class definitions
+
+ IF EXIST Ifc4x3_rc3-schema.cpp (
+ :: v0.6.0
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc3.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc3.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc3.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc3.h
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc3-schema.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc3-schema.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc3-definitions.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc3-definitions.h
+ )
+
+ del *.cpp *.h
+)
+
+IF EXIST IFC4x3_RC4.exp (
+ python express_parser.py IFC4x3_RC4.exp header implementation schema_class definitions
+
+ IF EXIST Ifc4x3_rc4-schema.cpp (
+ :: v0.6.0
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc4.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc4.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc4.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc4.h
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc4-schema.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc4-schema.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc4-definitions.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc4-definitions.h
+ )
+
+ del *.cpp *.h
+)
+
+IF EXIST IFC4X3.exp (
+ python express_parser.py IFC4X3.exp header implementation schema_class definitions
+
+ IF EXIST Ifc4x3-schema.cpp (
+ :: v0.6.0
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3.cpp txt/header_ifc4x3_rc2.txt Ifc4x3.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3.h txt/header_ifc4x3_rc2.txt Ifc4x3.h
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3-schema.cpp txt/header_ifc4x3_rc2.txt Ifc4x3-schema.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3-definitions.h txt/header_ifc4x3_rc2.txt Ifc4x3-definitions.h
+ )
+
+ del *.cpp *.h
+)
+
+IF EXIST IFC4X3_TC1.exp (
+ python express_parser.py IFC4X3_TC1.exp header implementation schema_class definitions
+
+ IF EXIST Ifc4x3_tc1-schema.cpp (
+ :: v0.6.0
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_tc1.cpp txt/header_ifc4x3_tc1.txt Ifc4x3_tc1.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_tc1.h txt/header_ifc4x3_tc1.txt Ifc4x3_tc1.h
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_tc1-schema.cpp txt/header_ifc4x3_tc1.txt Ifc4x3_tc1-schema.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_tc1-definitions.h txt/header_ifc4x3_tc1.txt Ifc4x3_tc1-definitions.h
+ )
+
+ del *.cpp *.h
+)
+
+IF EXIST IFC4X3_ADD1.exp (
+ python express_parser.py IFC4X3_ADD1.exp header implementation schema_class definitions
+
+ IF EXIST Ifc4x3_add1-schema.cpp (
+ :: v0.6.0
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_add1.cpp txt/header_ifc4x3_add1.txt Ifc4x3_add1.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_add1.h txt/header_ifc4x3_add1.txt Ifc4x3_add1.h
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_add1-schema.cpp txt/header_ifc4x3_add1.txt Ifc4x3_add1-schema.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_add1-definitions.h txt/header_ifc4x3_add1.txt Ifc4x3_add1-definitions.h
+ )
+
+ del *.cpp *.h
+)
+
+IF EXIST IFC4X3_ADD2.exp (
+ python express_parser.py IFC4X3_ADD2.exp header implementation schema_class definitions
+
+ IF EXIST Ifc4x3_add2-schema.cpp (
+ :: v0.6.0
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_add2.cpp txt/header_ifc4x3_add2.txt Ifc4x3_add2.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_add2.h txt/header_ifc4x3_add2.txt Ifc4x3_add2.h
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_add2-schema.cpp txt/header_ifc4x3_add2.txt Ifc4x3_add2-schema.cpp
+ python cat.py -o ..\..\..\ifcparse\Ifc4x3_add2-definitions.h txt/header_ifc4x3_add2.txt Ifc4x3_add2-definitions.h
+ )
+
+ del *.cpp *.h
+)
diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/endif.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/endif.txt
new file mode 100644
index 0000000000..69331c3ca9
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/express/txt/endif.txt
@@ -0,0 +1 @@
+#endif
diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc2x3.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc2x3.txt
new file mode 100644
index 0000000000..2487658eb8
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc2x3.txt
@@ -0,0 +1,25 @@
+/********************************************************************************
+ * *
+ * 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 has been generated from IFC2X3_TC1.exp. Do not make modifications *
+ * but instead modify the python script that has been used to generate this. *
+ * *
+ ********************************************************************************/
diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4.txt
new file mode 100644
index 0000000000..52da1accf8
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4.txt
@@ -0,0 +1,25 @@
+/********************************************************************************
+ * *
+ * 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 has been generated from IFC4.exp. Do not make modifications *
+ * but instead modify the python script that has been used to generate this. *
+ * *
+ ********************************************************************************/
diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x1.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x1.txt
new file mode 100644
index 0000000000..dfacb0e04b
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x1.txt
@@ -0,0 +1,25 @@
+/********************************************************************************
+ * *
+ * 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 has been generated from IFC4x1.exp. Do not make modifications *
+ * but instead modify the python script that has been used to generate this. *
+ * *
+ ********************************************************************************/
diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x2.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x2.txt
new file mode 100644
index 0000000000..5f6f1f75a7
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x2.txt
@@ -0,0 +1,25 @@
+/********************************************************************************
+ * *
+ * 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 has been generated from IFC4x2.exp. Do not make modifications *
+ * but instead modify the python script that has been used to generate this. *
+ * *
+ ********************************************************************************/
diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_add1.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_add1.txt
new file mode 100644
index 0000000000..85aac44883
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_add1.txt
@@ -0,0 +1,25 @@
+/********************************************************************************
+ * *
+ * 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 has been generated from IFC4X3_ADD1.exp. Do not make modifications *
+ * but instead modify the python script that has been used to generate this. *
+ * *
+ ********************************************************************************/
diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_add2.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_add2.txt
new file mode 100644
index 0000000000..6616f73b69
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_add2.txt
@@ -0,0 +1,25 @@
+/********************************************************************************
+ * *
+ * 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 has been generated from IFC4X3_ADD2.exp. Do not make modifications *
+ * but instead modify the python script that has been used to generate this. *
+ * *
+ ********************************************************************************/
diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_rc1.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_rc1.txt
new file mode 100644
index 0000000000..208e687c24
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_rc1.txt
@@ -0,0 +1,25 @@
+/********************************************************************************
+ * *
+ * 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 has been generated from IFC4x3_RC1.exp. Do not make modifications *
+ * but instead modify the python script that has been used to generate this. *
+ * *
+ ********************************************************************************/
diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_rc2.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_rc2.txt
new file mode 100644
index 0000000000..5d228771fd
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_rc2.txt
@@ -0,0 +1,25 @@
+/********************************************************************************
+ * *
+ * 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 has been generated from IFC4x3_RC2.exp. Do not make modifications *
+ * but instead modify the python script that has been used to generate this. *
+ * *
+ ********************************************************************************/
diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_tc1.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_tc1.txt
new file mode 100644
index 0000000000..cb46c3274f
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_tc1.txt
@@ -0,0 +1,25 @@
+/********************************************************************************
+ * *
+ * 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 has been generated from IFC4X3_TC1.exp. Do not make modifications *
+ * but instead modify the python script that has been used to generate this. *
+ * *
+ ********************************************************************************/
diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/ifdef_ifc4.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/ifdef_ifc4.txt
new file mode 100644
index 0000000000..cd56d7026e
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/express/txt/ifdef_ifc4.txt
@@ -0,0 +1,2 @@
+
+#ifdef USE_IFC4
diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/ifdef_ifc4x1.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/ifdef_ifc4x1.txt
new file mode 100644
index 0000000000..1f3194a48d
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/express/txt/ifdef_ifc4x1.txt
@@ -0,0 +1,2 @@
+
+#ifdef USE_IFC4X1
diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/ifndef_ifc4.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/ifndef_ifc4.txt
new file mode 100644
index 0000000000..f662cbf4cf
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/express/txt/ifndef_ifc4.txt
@@ -0,0 +1,2 @@
+
+#ifndef USE_IFC4
diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/ifndef_ifc4x1 - Copy.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/ifndef_ifc4x1 - Copy.txt
new file mode 100644
index 0000000000..a5826d1e34
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/express/txt/ifndef_ifc4x1 - Copy.txt
@@ -0,0 +1,2 @@
+
+#ifndef USE_IFC4X1
diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/ifndef_ifc4x1.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/ifndef_ifc4x1.txt
new file mode 100644
index 0000000000..a5826d1e34
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/express/txt/ifndef_ifc4x1.txt
@@ -0,0 +1,2 @@
+
+#ifndef USE_IFC4X1
diff --git a/src/ifcopenshell-python/ifcopenshell/file.py b/src/ifcopenshell-python/ifcopenshell/file.py
index a93c9b5dea..c7350a13da 100644
--- a/src/ifcopenshell-python/ifcopenshell/file.py
+++ b/src/ifcopenshell-python/ifcopenshell/file.py
@@ -248,6 +248,10 @@ READ_ERROR = ifcopenshell_wrapper.file_open_status.READ_ERROR
NO_HEADER = ifcopenshell_wrapper.file_open_status.NO_HEADER
UNSUPPORTED_SCHEMA = ifcopenshell_wrapper.file_open_status.UNSUPPORTED_SCHEMA
INVALID_SYNTAX = ifcopenshell_wrapper.file_open_status.INVALID_SYNTAX
+try:
+ UNKNOWN = ifcopenshell_wrapper.file_open_status.UNKNOWN
+except:
+ UNKNOWN = 5 # Workaround
import struct
@@ -496,6 +500,24 @@ class rocksdb_file_storage:
return "".join("".join(map(str, t)) if t[1] else "" for t in zip(prefixes, version_tuple[0:2]))
+class file_header:
+ def __init__(self, file, header_data):
+ self.file = file
+ self.header_data = header_data
+
+ @property
+ def file_description(self) -> entity_instance:
+ return entity_instance.wrap_value(self.header_data.file_description_py(), file=self.file)
+
+ @property
+ def file_name(self) -> entity_instance:
+ return entity_instance.wrap_value(self.header_data.file_name_py(), file=self.file)
+
+ @property
+ def file_schema(self) -> entity_instance:
+ return entity_instance.wrap_value(self.header_data.file_schema_py(), file=self.file)
+
+
class file:
"""Base class for containing IFC files.
@@ -513,7 +535,7 @@ class file:
"""
wrapped_data: ifcopenshell_wrapper.file
- header: ifcopenshell_wrapper.IfcSpfHeader
+ header: file_header
units: dict[str, entity_instance] = {}
history_size: int = 64
history: list[Transaction]
@@ -586,8 +608,11 @@ class file:
"Unsupported schema: %s" % ",".join(self.header.file_schema.schema_identifiers),
),
INVALID_SYNTAX: lambda: (Error, "Syntax error during parse, check logs"),
+ # This is the case when passing uninitialized_tag
+ UNKNOWN: lambda: (None, None),
}[f.good().value()]()
- raise exc(msg)
+ if exc is not None:
+ raise exc(msg)
else:
args = filter(None, [schema])
args = map(ifcopenshell_wrapper.schema_by_name, args)
@@ -1025,12 +1050,10 @@ class file:
@property
def header(self):
- try: # Temporary workaround until new builds are ready. See #7131.
- h = self.wrapped_data.header()
+ try:
+ return file_header(self, self.wrapped_data.header())
except:
return self.wrapped_data.header
- object.__setattr__(h, "file_ref", lambda inst: entity_instance.wrap_value(inst, file=self))
- return h
@property
def storage(self) -> Optional[rocksdb_file_storage]:
diff --git a/src/ifcopenshell-python/ifcopenshell/geom/stats.py b/src/ifcopenshell-python/ifcopenshell/geom/stats.py
new file mode 100644
index 0000000000..364083acd7
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/geom/stats.py
@@ -0,0 +1,246 @@
+import sys
+from typing import Optional, IO
+from itertools import accumulate
+
+from . import ifcopenshell_wrapper
+
+
+class TransformDefaultDict:
+ def __init__(self, keyfunc=lambda x: x, default_factory=None):
+ """
+ Similar to collections.defaultdict, but allows for storing
+ non-hashable keys by means of a transform function
+ """
+ self.keyfunc = keyfunc
+ self.default_factory = default_factory
+ self._data = {}
+ self._original_keys = {}
+
+ def __setitem__(self, key, value):
+ k = self.keyfunc(key)
+ self._data[k] = value
+ self._original_keys[k] = key
+
+ def __getitem__(self, key):
+ k = self.keyfunc(key)
+ if k in self._data:
+ return self._data[k]
+ if self.default_factory is not None:
+ value = self.default_factory()
+ self._data[k] = value
+ self._original_keys[k] = key
+ return value
+ raise KeyError(key)
+
+ def __contains__(self, key):
+ return self.keyfunc(key) in self._data
+
+ def __delitem__(self, key):
+ k = self.keyfunc(key)
+ del self._data[k]
+ del self._original_keys[k]
+
+ def __iter__(self):
+ return iter(self._original_keys.values())
+
+ def __len__(self):
+ return len(self._data)
+
+ def items(self):
+ return ((self._original_keys[k], v) for k, v in self._data.items())
+
+ def keys(self):
+ return self._original_keys.values()
+
+ def values(self):
+ return self._data.values()
+
+ def get(self, key, default=None):
+ return self._data.get(self.keyfunc(key), default)
+
+ def setdefault(self, key, default=None):
+ k = self.keyfunc(key)
+ if k not in self._data:
+ self._data[k] = default
+ self._original_keys[k] = key
+ return self._data[k]
+
+ def update(self, other):
+ for k, v in other.items():
+ self[k] = v
+
+ def clear(self):
+ self._data.clear()
+ self._original_keys.clear()
+
+ def __repr__(self):
+ items = ", ".join(f"{k!r}: {v!r}" for k, v in self.items())
+ return f"{self.__class__.__name__}({{{items}}})"
+
+
+class ProductCounter:
+ def __init__(self, only_with_representation=True):
+ self.total = 0
+ transform_func = lambda decl: decl.name() if hasattr(decl, "name") else decl
+ self.counts = TransformDefaultDict(keyfunc=transform_func, default_factory=int)
+ self.counts_excl = TransformDefaultDict(keyfunc=transform_func, default_factory=int)
+ self.only_with_representation = only_with_representation
+
+ def count(self, decl, values):
+ if decl._is("IfcProduct"):
+ if self.only_with_representation and values.get("Representation") is None:
+ return
+ self.counts_excl[decl] += 1
+ while decl.name() != "IfcProduct":
+ self.counts[decl] += 1
+ decl = decl.supertype()
+ self.total += 1
+
+ def print(self, out=sys.stdout):
+ if self.counts:
+ results = []
+
+ def process(ent, level=-1):
+ if ent in self.counts:
+ results.append((level, ent.name(), self.counts[ent]))
+ for subtype in ent.subtypes():
+ process(subtype, level=level + 1)
+
+ prod = next(iter(self.counts.keys())).schema().declaration_by_name("ifcproduct")
+ process(prod)
+ max_width = max(level * 2 + len(name) for level, name, _ in results)
+ for level, name, count in results:
+ print(" " * (level * 2), f"{name:<{max_width - level * 2}}", ": ", count, file=out, sep="")
+
+
+class BooleanResultCounter:
+ def __init__(self, exclude_union=True):
+ self.total = 0
+ self.exclude_union = exclude_union
+
+ def count(self, decl, values):
+ if decl._is("IfcBooleanResult"):
+ if self.exclude_union and values.get("Operator") == "UNION":
+ return
+ self.total += 1
+
+ def print(self, out=sys.stdout):
+ print("IfcBooleanResult:", self.total, file=out)
+
+
+class StatsCollector:
+ streamer: ifcopenshell_wrapper.InstanceStreamer
+ page_size: Optional[int]
+ file_stream: Optional[IO[str]]
+
+ finalized: bool = False
+ needs_data: bool = False
+
+ counters: list
+
+ num_semis: int = 0
+
+ def __init__(self):
+ self.streamer = ifcopenshell_wrapper.InstanceStreamer()
+ self.needs_data = True
+ self.counters = [ProductCounter(), BooleanResultCounter()]
+
+ def feedFromFile(self, f: Optional[IO[str]] = None):
+ if f:
+ self.file_stream = f
+ self.feed(self.file_stream.read(self.page_size))
+
+ def feed(self, data: str):
+ self.streamer.pushPage(data)
+ self.needs_data = False
+ self.num_semis = self.streamer.semicolonCount()
+
+ @staticmethod
+ def fromFilePath(fn, page_size: int = 102400):
+ collector = StatsCollector()
+ collector.page_size = page_size
+ collector.feedFromFile(open(str(fn), encoding="ascii"))
+ return collector
+
+ def next(self):
+ if self.num_semis > 0:
+ if inst := self.streamer.readInstancePy(True):
+ self.num_semis -= 1
+ return inst["type"], dict(list(inst.items())[2:])
+ else:
+ self.finalized = True
+ return None
+ elif self.file_stream:
+ self.feedFromFile()
+ return self.next()
+ else:
+ self.needs_data = True
+ return None
+
+ def process(self):
+ if n := self.next():
+ decl, values = n
+ else:
+ return
+ if decl.schema().name() == "HEADER_SECTION_SCHEMA":
+ return
+ for cnt in self.counters:
+ cnt.count(decl, values)
+
+ def print(self, out=sys.stdout):
+ for cnt in self.counters:
+ print(type(cnt).__name__, file=out)
+ print("=" * len(type(cnt).__name__), file=out)
+ cnt.print(out)
+
+ def includeElementTypesBasedOnBudget(self, priorities: dict, budget):
+ def specificity(decl, target):
+ if decl._is(target):
+ i = 0
+ while decl.name() != target:
+ decl = decl.supertype()
+ i += 1
+ return i
+ return None
+
+ def calc_prio(ty):
+ # find most specific priority directive for the given type in ty
+ return min(((specificity(ty, k), v) for k, v in priorities.items() if ty._is(k)), default=(0, 0))[1]
+
+ sorted_types = sorted(self.counters[0].counts_excl.keys(), key=calc_prio, reverse=True)
+ sorted_types = [s for s in sorted_types if not s._is("IfcFeatureElement")]
+ counts = [self.counters[0].counts_excl[v] for v in sorted_types]
+
+ ccounts = list(accumulate(counts))
+
+ if ccounts[0] >= budget:
+ # At the very least include one type
+ return sorted_types[0:1]
+
+ for i, s in enumerate(ccounts):
+ if s >= budget:
+ over = s - budget
+ under = budget - ccounts[i - 1]
+ k = i if over <= under else i - 1
+ return sorted_types[0 : k + 1]
+
+ # We're lucky we can render everything in budget
+ return sorted_types[:]
+
+
+if __name__ == "__main__":
+ collector = StatsCollector.fromFilePath(sys.argv[1])
+ while not collector.finalized:
+ collector.process()
+ collector.print()
+ print(
+ *collector.includeElementTypesBasedOnBudget(
+ {
+ "IfcWall": 3,
+ "IfcSlab": 3,
+ "IfcWindow": 2,
+ "IfcDoor": 2,
+ "IfcBuildingElement": 1,
+ }
+ )
+ )
diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py
index 48d65ae7ae..bf870cb1ed 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/element.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/element.py
@@ -117,6 +117,7 @@ def get_pset(
if (
psets_only
and not pset.is_a("IfcPropertySet")
+ and not pset.is_a("IfcPreDefinedPropertySet")
and not (is_ifc2x3 and pset.is_a("IfcExtendedMaterialProperties"))
):
pset = None
@@ -126,7 +127,11 @@ def get_pset(
if type_pset is not None and not prop:
if psets_only or qtos_only:
type_pset_element = element.file.by_id(type_pset["id"])
- if psets_only and not type_pset_element.is_a("IfcPropertySet"):
+ if (
+ psets_only
+ and not type_pset_element.is_a("IfcPropertySet")
+ and not type_pset_element.is_a("IfcPreDefinedPropertySet")
+ ):
type_pset = None
elif qtos_only and not type_pset_element.is_a("IfcElementQuantity"):
type_pset = None
@@ -177,7 +182,7 @@ def get_psets(
psets = {}
if element.is_a("IfcTypeObject"):
for definition in element.HasPropertySets or []:
- if psets_only and not definition.is_a("IfcPropertySet"):
+ if psets_only and not definition.is_a("IfcPropertySet") and not definition.is_a("IfcPreDefinedPropertySet"):
continue
if qtos_only and not definition.is_a("IfcElementQuantity"):
continue
@@ -213,7 +218,11 @@ def get_psets(
for relationship in is_defined_by:
if relationship.is_a("IfcRelDefinesByProperties"):
definition = relationship.RelatingPropertyDefinition
- if psets_only and not definition.is_a("IfcPropertySet"):
+ if (
+ psets_only
+ and not definition.is_a("IfcPropertySet")
+ and not definition.is_a("IfcPreDefinedPropertySet")
+ ):
continue
if qtos_only and not definition.is_a("IfcElementQuantity"):
continue
@@ -565,6 +574,40 @@ def get_predefined_type(element: ifcopenshell.entity_instance) -> Union[str, Non
return predefined_type
+def is_userdefined_type(element: ifcopenshell.entity_instance) -> bool:
+ """Checks if the predefined type is userdefined
+
+ :param element: The IFC Element entity
+ :return: True if userdefined
+
+ Example:
+
+ .. code:: python
+
+ element = ifcopenshell.by_type("IfcWall")[0]
+ is_userdefined_type = ifcopenshell.util.element.is_userdefined_type(element)
+ """
+ if element_type := get_type(element):
+ predefined_type = getattr(element_type, "PredefinedType", None)
+ if predefined_type == "USERDEFINED":
+ return True
+ elif not predefined_type:
+ predefined_type = getattr(element_type, "ElementType", ...)
+ if predefined_type == ...:
+ predefined_type = getattr(element_type, "ProcessType", None)
+ if predefined_type:
+ return True
+ if predefined_type and predefined_type != "NOTDEFINED":
+ return False
+
+ predefined_type = getattr(element, "PredefinedType", None)
+ if predefined_type == "USERDEFINED":
+ return True
+ elif not predefined_type:
+ return bool(getattr(element, "ObjectType", None))
+ return False
+
+
def get_type(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
"""Retrieves the construction type element of an element occurrence.
diff --git a/src/ifcopenshell-python/ifcopenshell/util/sequence.py b/src/ifcopenshell-python/ifcopenshell/util/sequence.py
index 930184ecdb..1452aa011c 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/sequence.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/sequence.py
@@ -329,61 +329,28 @@ def guess_date_range(work_schedule: ifcopenshell.entity_instance):
return earliest, latest
-def get_direct_task_outputs(task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
- return [rel.RelatingProduct for rel in task.HasAssignments if rel.is_a("IfcRelAssignsToProduct")]
+def get_task_outputs(
+ task: ifcopenshell.entity_instance, is_recursive: bool = False
+) -> set[ifcopenshell.entity_instance]:
+ if is_recursive:
+ return {o for subtask in [task] + list(get_all_nested_tasks(task)) for o in get_task_outputs(subtask)}
+ return {rel.RelatingProduct for rel in task.HasAssignments if rel.is_a("IfcRelAssignsToProduct")}
-def get_task_outputs(task: ifcopenshell.entity_instance, is_deep: bool = False) -> list[ifcopenshell.entity_instance]:
- if not is_deep:
- return get_direct_task_outputs(task)
- else:
- return [output for nested_task in get_all_nested_tasks(task) for output in get_direct_task_outputs(nested_task)]
+def get_task_inputs(
+ task: ifcopenshell.entity_instance, is_recursive: bool = False
+) -> set[ifcopenshell.entity_instance]:
+ if is_recursive:
+ return {o for subtask in [task] + list(get_all_nested_tasks(task)) for o in get_task_inputs(subtask)}
+ return {o for rel in task.OperatesOn for o in rel.RelatedObjects if o.is_a("IfcProduct")}
-def get_task_inputs(task: ifcopenshell.entity_instance, is_deep: bool = False) -> list[ifcopenshell.entity_instance]:
- if not is_deep:
- return [
- object
- for rel in task.OperatesOn
- if rel.is_a("IfcRelAssignsToProcess")
- for object in rel.RelatedObjects
- if object.is_a("IfcProduct")
- ]
- else:
- return [
- output
- for nested_task in get_all_nested_tasks(task)
- for output in [
- object
- for rel in nested_task.OperatesOn
- if rel.is_a("IfcRelAssignsToProcess")
- for object in rel.RelatedObjects
- if object.is_a("IfcProduct")
- ]
- ]
-
-
-def get_task_resources(task: ifcopenshell.entity_instance, is_deep: bool = False) -> list[ifcopenshell.entity_instance]:
- if not is_deep:
- return [
- object
- for rel in task.OperatesOn
- if rel.is_a("IfcRelAssignsToProcess")
- for object in rel.RelatedObjects
- if object.is_a("IfcResource")
- ]
- else:
- return [
- resource
- for nested_task in get_all_nested_tasks(task)
- for resource in [
- object
- for rel in nested_task.OperatesOn
- if rel.is_a("IfcRelAssignsToProcess")
- for object in rel.RelatedObjects
- if object.is_a("IfcResource")
- ]
- ]
+def get_task_resources(
+ task: ifcopenshell.entity_instance, is_recursive: bool = False
+) -> set[ifcopenshell.entity_instance]:
+ if is_recursive:
+ return {r for subtask in [task] + list(get_all_nested_tasks(task)) for r in get_task_resources(subtask)}
+ return {o for rel in task.OperatesOn for o in rel.RelatedObjects if o.is_a("IfcResource")}
def has_task_outputs(task: ifcopenshell.entity_instance) -> bool:
diff --git a/src/ifcopenshell-python/ifcopenshell/validate.py b/src/ifcopenshell-python/ifcopenshell/validate.py
index 6bb00992c0..25f9b3a685 100644
--- a/src/ifcopenshell-python/ifcopenshell/validate.py
+++ b/src/ifcopenshell-python/ifcopenshell/validate.py
@@ -320,14 +320,16 @@ def log_internal_cpp_errors(
chr_offset_re = re.compile(r"at offset (\d+)\s*")
for_instance_re = re.compile(r"\s*for instance #(\d+)\s*")
+ for_header_ent_re = re.compile(r"\s*for header entity (\w+)")
if log_content is None:
log_content = ifcopenshell.get_log()
msgs = list(map(json.loads, filter(None, log_content.split("\n"))))
chr_offsets = [chr_offset_re.findall(m["message"]) for m in msgs]
instance_messages = [for_instance_re.findall(m["message"]) for m in msgs]
+ header_messages = [for_header_ent_re.findall(m["message"]) for m in msgs]
- if chr_offsets or (instance_messages and f is None):
+ if any(chr_offsets) or (any(instance_messages) and f is None):
# The file is opened in binary mode, in order
# to correspond with the offsets reported by
# IfcOpenShell C++
@@ -348,7 +350,7 @@ def log_internal_cpp_errors(
else:
logger.error("For instance:\n %s\n%s", line, m)
- if instance_messages:
+ if any(instance_messages):
for instid, msg in zip(instance_messages, msgs):
if instid:
m = for_instance_re.sub("", msg["message"])
@@ -376,6 +378,12 @@ def log_internal_cpp_errors(
else:
logger.error(m)
+ if any(header_messages):
+ for hent, msg in zip(header_messages, msgs):
+ if hent:
+ m = msg["message"]
+ logger.error(m)
+
entity_attribute_map: dict[tuple[str, str], tuple[entity_type, tuple[attribute, ...]]] = {}
diff --git a/src/ifcopenshell-python/test/fixtures/rules/pass-rigid-op-IfcPlaneAngleMeasure-IfcPlaneAngleMeasure-ifc4x3_add2.ifc b/src/ifcopenshell-python/test/fixtures/rules/pass-rigid-op-IfcPlaneAngleMeasure-IfcPlaneAngleMeasure-ifc4x3_add2.ifc
index 9ac949de12..8946080689 100644
--- a/src/ifcopenshell-python/test/fixtures/rules/pass-rigid-op-IfcPlaneAngleMeasure-IfcPlaneAngleMeasure-ifc4x3_add2.ifc
+++ b/src/ifcopenshell-python/test/fixtures/rules/pass-rigid-op-IfcPlaneAngleMeasure-IfcPlaneAngleMeasure-ifc4x3_add2.ifc
@@ -1,12 +1,12 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
-FILE_NAME('','2023-01-11T09:47:45',(),(),'IfcOpenShell v0.7.0-8a108f37','IfcOpenShell v0.7.0-8a108f37','');
+FILE_NAME('','2023-01-11T09:47:45',(''),(''),'IfcOpenShell v0.7.0-8a108f37','IfcOpenShell v0.7.0-8a108f37','');
FILE_SCHEMA(('IFC4X3_ADD2'));
ENDSEC;
DATA;
-#1=IFCGEOGRAPHICCRS('EPSG:4326',$,$,$,$);
-#2=IFCGEOGRAPHICCRS('EPSG:3857',$,$,$,$);
+#1=IFCGEOGRAPHICCRS('EPSG:4326',$,$,$,$,$);
+#2=IFCGEOGRAPHICCRS('EPSG:3857',$,$,$,$,$);
#3=IFCRIGIDOPERATION(#1,#2,IFCPLANEANGLEMEASURE(0.),IFCPLANEANGLEMEASURE(0.),0.);
ENDSEC;
END-ISO-10303-21;
diff --git a/src/ifcopenshell-python/test/geom/stats.py b/src/ifcopenshell-python/test/geom/stats.py
new file mode 100644
index 0000000000..fd0cd8e337
--- /dev/null
+++ b/src/ifcopenshell-python/test/geom/stats.py
@@ -0,0 +1,26 @@
+import os
+import ifcopenshell
+from ifcopenshell.geom.stats import StatsCollector
+import pytest
+
+
+@pytest.mark.parametrize(
+ "file",
+ [os.path.join(os.path.dirname(__file__), "../../../../test/input/Allplan 2017 Test Bauteil-Oberflächen.ifc")],
+)
+def test_stats(file):
+ collector = StatsCollector.fromFilePath(file)
+ while not collector.finalized:
+ collector.process()
+ collector.print()
+ prio = {
+ "IfcWall": 2,
+ "IfcBuildingElement": 1,
+ "IfcBuildingElementProxy": -1,
+ }
+ ents = [a.name() for a in collector.includeElementTypesBasedOnBudget(prio, 1)]
+ assert ents == ["IfcWall"] or ents == ["IfcWallStandardCase"]
+ ents = [a.name() for a in collector.includeElementTypesBasedOnBudget(prio, 100)]
+ assert "IfcBuildingElementProxy" in ents
+ ents = [a.name() for a in collector.includeElementTypesBasedOnBudget(prio, 50)]
+ assert "IfcBuildingElementProxy" not in ents
diff --git a/src/ifcopenshell-python/test/test_streaming_rocksdb_and_simpletyperefs.py b/src/ifcopenshell-python/test/test_streaming_rocksdb_and_simpletyperefs.py
index 5cb3fccf85..b0bfb2925a 100644
--- a/src/ifcopenshell-python/test/test_streaming_rocksdb_and_simpletyperefs.py
+++ b/src/ifcopenshell-python/test/test_streaming_rocksdb_and_simpletyperefs.py
@@ -23,6 +23,11 @@ import pytest
import ifcopenshell
import tempfile
+try:
+ import psutil
+except ImportError:
+ psutil = None
+
fn = os.path.join(os.path.dirname(__file__), "fixtures/ColumnPSetsOfSets.ifc")
@@ -33,12 +38,50 @@ def test_stream():
}
+def test_chunked_stream():
+ assert list(ifcopenshell.stream2(fn)) == list(ifcopenshell.stream2(fn, page_size=1024))
+
+
+def test_mmaped_stream():
+ assert list(ifcopenshell.stream2(fn)) == list(ifcopenshell.stream2(fn, mmap=True))
+
+
def test_file():
f = ifcopenshell.open(fn)
assert f[139].RelatingPropertyDefinition.is_a("IfcPropertySetDefinitionSet")
assert {x.id() for x in f[139].RelatingPropertyDefinition[0]} == {136, 138}
+def test_partial_open():
+ f = ifcopenshell.open(fn)
+ assert len(f.by_type("ifccartesianpoint"))
+ f = ifcopenshell.open(fn, bypass_types=("IfcRepresentationItem",))
+ assert len(f.by_type("ifccartesianpoint")) == 0
+
+
+def test_opening_unicode():
+ import ifcopenshell.template
+
+ with tempfile.TemporaryDirectory() as d:
+ fn = os.path.join(d, "ხყჯ𐰢ᨕதకᎣᚱᾗ.ifc")
+ f = ifcopenshell.template.create()
+ f.write(fn)
+ g = ifcopenshell.open(fn)
+ assert g.by_type("ifcproject")
+
+
+@pytest.mark.skipif(psutil is None, reason="psutil not installed")
+def test_memusage_partial_open():
+ m0 = psutil.Process().memory_info().rss
+ f = ifcopenshell.open(fn)
+ m1 = psutil.Process().memory_info().rss
+ g = ifcopenshell.open(fn, bypass_types=("IfcRepresentationItem",))
+ m2 = psutil.Process().memory_info().rss
+ # arbitrary...
+ expected_ratio = 0.75
+ assert (m2 - m1) < (m1 - m0) * expected_ratio
+
+
def test_rocks():
with tempfile.TemporaryDirectory() as d:
rfn = os.path.join(d, os.path.basename(fn))
diff --git a/src/ifcopenshell-python/test/test_write.py b/src/ifcopenshell-python/test/test_write.py
index 352aea2538..e7a1f76b7c 100644
--- a/src/ifcopenshell-python/test/test_write.py
+++ b/src/ifcopenshell-python/test/test_write.py
@@ -40,9 +40,11 @@ class TestWrite:
def test_write_ifcspf(self):
self.assert_model_is_written("model.ifc")
+ @pytest.mark.skip(reason="IFCXML writing not supported")
def test_write_ifcxml(self):
self.assert_model_is_written("model.ifcXML")
+ @pytest.mark.skip(reason="IFCXML writing not supported")
def test_write_ifc_zip_ifcxml_format(self):
self.assert_model_is_written("model.ifcZIP", format=".ifcXML", zipped=True)
@@ -58,9 +60,11 @@ class TestWrite:
def test_write_anyextension_ifczip_ifcspf_format(self):
self.assert_model_is_written("model.anyextension", format=".ifc", zipped=True)
+ @pytest.mark.skip(reason="IFCXML writing not supported")
def test_write_anyextension_ifcxml_format(self):
self.assert_model_is_written("model.anyextension", format=".ifcXML")
+ @pytest.mark.skip(reason="IFCXML writing not supported")
def test_write_anyextension_ifczip_ifcxml_format(self):
self.assert_model_is_written("model.anyextension", format=".ifcXML", zipped=True)
diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py
index 1a1e28d58b..2c62c6d7c5 100644
--- a/src/ifcopenshell-python/test/util/test_element.py
+++ b/src/ifcopenshell-python/test/util/test_element.py
@@ -43,7 +43,7 @@ import ifcopenshell.util.element as subject
from ifcopenshell.util.shape_builder import ShapeBuilder
-class TestIFC2X3MaterialProfilePsts(test.bootstrap.IFC2X3):
+class TestIFC2X3MaterialProfilePsets(test.bootstrap.IFC2X3):
def test_get_profile_pset(self):
profile = ifcopenshell.api.profile.add_parameterized_profile(self.file, "IfcRectangleProfileDef")
pset = ifcopenshell.api.pset.add_pset(self.file, profile, "")
@@ -159,6 +159,15 @@ class TestGetPsetIFC4(test.bootstrap.IFC4):
assert subject.get_pset(self.file.create_entity("IfcPerson"), "name") is None
assert subject.get_pset(self.file.create_entity("IfcPerson"), "name", "a") is None
+ def test_getting_predefined_psets(self):
+ element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcDoorType")
+ pset = self.file.create_entity("IfcDoorLiningProperties", ifcopenshell.guid.new())
+ pset.Name = "My Lining"
+ pset.LiningDepth = 42
+ element.HasPropertySets = [pset]
+ assert subject.get_pset(element, "My Lining") == {"LiningDepth": 42, "id": pset.id()}
+ assert subject.get_pset(element, "My Lining", psets_only=True) == {"LiningDepth": 42, "id": pset.id()}
+
class TestGetPsetsIFC4(test.bootstrap.IFC4):
def test_getting_the_psets_of_a_product_as_a_dictionary(self):
@@ -222,6 +231,15 @@ class TestGetPsetsIFC4(test.bootstrap.IFC4):
ifcopenshell.api.pset.edit_qto(self.file, qto=qto, properties={"x": 42})
assert subject.get_psets(element, qtos_only=True) == {"qto": {"x": 42, "id": qto.id()}}
+ def test_getting_predefined_psets(self):
+ element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcDoorType")
+ pset = self.file.create_entity("IfcDoorLiningProperties", ifcopenshell.guid.new())
+ pset.Name = "My Lining"
+ pset.LiningDepth = 42
+ element.HasPropertySets = [pset]
+ assert subject.get_psets(element) == {"My Lining": {"LiningDepth": 42, "id": pset.id()}}
+ assert subject.get_psets(element, psets_only=True) == {"My Lining": {"LiningDepth": 42, "id": pset.id()}}
+
class TestGetPropertyDefinitionIFC4(test.bootstrap.IFC4):
def test_getting_the_properties_of_a_pset(self):
@@ -369,6 +387,65 @@ class TestGetPredefinedTypeIFC4(test.bootstrap.IFC4):
assert subject.get_predefined_type(element_type) == "NOTDEFINED"
+class TestIsUserdefinedTypeIFC4(test.bootstrap.IFC4):
+ def test_getting_a_predefined_element(self):
+ element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
+ element.PredefinedType = "PARTITIONING"
+ assert not subject.is_userdefined_type(element)
+
+ def test_getting_an_element_userdefined_type(self):
+ element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
+ element.PredefinedType = "USERDEFINED"
+ element.ObjectType = "FOOBAR"
+ assert subject.is_userdefined_type(element)
+
+ def test_getting_an_element_type_without_a_predefined_type_attribute(self):
+ element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcAnnotation")
+ element.ObjectType = "FOOBAR"
+ assert subject.is_userdefined_type(element)
+
+ def test_getting_an_inherited_predefined_type(self):
+ element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
+ element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType")
+ ifcopenshell.api.type.assign_type(self.file, related_objects=[element], relating_type=element_type)
+ element_type.PredefinedType = "PARTITIONING"
+ assert not subject.is_userdefined_type(element)
+
+ def test_getting_an_inherited_userdefined_type_for_an_element_type(self):
+ element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
+ element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType")
+ ifcopenshell.api.type.assign_type(self.file, related_objects=[element], relating_type=element_type)
+ element_type.PredefinedType = "USERDEFINED"
+ element_type.ElementType = "FOOBAR"
+ assert subject.is_userdefined_type(element)
+
+ def test_getting_an_overriden_predefined_type(self):
+ element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
+ element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType")
+ ifcopenshell.api.type.assign_type(self.file, related_objects=[element], relating_type=element_type)
+ element_type.PredefinedType = "NOTDEFINED"
+ element.PredefinedType = "PARTITIONING"
+ assert not subject.is_userdefined_type(element)
+
+ def test_getting_an_inherited_userdefined_type_for_a_process_type(self):
+ element = ifcopenshell.api.sequence.add_task(self.file)
+ element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcTaskType")
+ ifcopenshell.api.type.assign_type(self.file, related_objects=[element], relating_type=element_type)
+ element_type.PredefinedType = "USERDEFINED"
+ element_type.ProcessType = "FOOBAR"
+ assert subject.is_userdefined_type(element)
+
+ def test_getting_an_element_type_predefined_type(self):
+ element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType")
+ element_type.PredefinedType = "PARTITIONING"
+ assert not subject.is_userdefined_type(element_type)
+
+ def test_getting_an_element_type_null_predefined_type(self):
+ element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType")
+ element_type.PredefinedType = "NOTDEFINED"
+ assert not subject.is_userdefined_type(element_type)
+
+
class TestGetTypeIFC4(test.bootstrap.IFC4):
def test_getting_the_type_of_a_product(self):
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
diff --git a/src/ifcparse/FileReader.cpp b/src/ifcparse/FileReader.cpp
new file mode 100644
index 0000000000..e16d26e641
--- /dev/null
+++ b/src/ifcparse/FileReader.cpp
@@ -0,0 +1,326 @@
+#include "FileReader.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifdef USE_MMAP
+#include
+#include
+#endif
+
+#include "utils.h"
+
+namespace {
+
+#if defined(_WIN32)
+ inline void file_seek_abs(FILE* f, std::uint64_t off) {
+ if (_fseeki64(f, static_cast(off), SEEK_SET) != 0)
+ throw std::runtime_error("fseek failed");
+ }
+#else
+ inline void file_seek_abs(FILE* f, std::uint64_t off) {
+ if (fseeko(f, static_cast(off), SEEK_SET) != 0)
+ throw std::runtime_error("fseeko failed");
+ }
+#endif
+
+} // namespace
+
+using namespace IfcParse;
+
+struct FullBufferImpl final : FileReader::Impl {
+ std::vector buf_;
+ explicit FullBufferImpl(const std::string& fn) {
+#ifdef _MSC_VER
+ std::wstring fn_ws = IfcUtil::path::from_utf8(fn);
+ const wchar_t* fn_wide = fn_ws.c_str();
+ auto stream = _wfopen(fn_wide, L"rb");
+#else
+ auto stream = fopen(fn.c_str(), "rb");
+#endif
+ fseek(stream, 0, SEEK_END);
+ buf_.resize((size_t)ftell(stream));
+ rewind(stream);
+ buf_.resize((size_t) fread(buf_.data(), 1, buf_.capacity(), stream));
+ fclose(stream);
+ }
+ size_t size() const override { return buf_.size(); }
+ char get(size_t pos) const override {
+ if (pos >= buf_.size()) throw std::out_of_range("get out of range");
+ return buf_[pos];
+ }
+};
+
+struct PagedFileImpl final : FileReader::Impl {
+ std::string fn_;
+ FILE* fp_ = nullptr;
+ size_t file_size_ = 0;
+ size_t page_size_ = 4096;
+
+ // LRU cache
+ size_t capacity_ = 8;
+ mutable std::list lru_;
+ struct Entry {
+ FileReader::Page page;
+ std::list::iterator it;
+ };
+ mutable std::unordered_map map_;
+
+ PagedFileImpl(const std::string& fn, size_t page_size, size_t cap)
+ : fn_(fn), page_size_(std::max(512, page_size)), capacity_(std::max(2, cap))
+ {
+#ifdef _MSC_VER
+ std::wstring fn_ws = IfcUtil::path::from_utf8(fn);
+ const wchar_t* fn_wide = fn_ws.c_str();
+ fp_ = _wfopen(fn_wide, L"rb");
+#else
+ fp_ = fopen(fn.c_str(), "rb");
+#endif
+ fseek(fp_, 0, SEEK_END);
+ file_size_ = (size_t)ftell(fp_);
+ rewind(fp_);
+ }
+
+ ~PagedFileImpl() override {
+ if (fp_) std::fclose(fp_);
+ fp_ = nullptr;
+ }
+
+ size_t size() const override { return file_size_; }
+
+ char get(size_t pos) const override {
+ if (pos >= file_size_) throw std::out_of_range("get out of range");
+ const size_t pidx = pos / page_size_;
+ const FileReader::Page& p = fetchPage_(pidx);
+ const size_t off = pos % page_size_;
+ if (off >= p.data.size()) throw std::out_of_range("offset beyond valid page bytes");
+ return p.data[off];
+ }
+
+private:
+ const FileReader::Page& fetchPage_(size_t idx) const {
+ auto it = map_.find(idx);
+ if (it != map_.end()) {
+ touch_(it);
+ return it->second.page;
+ }
+ // Load page from disk using persistent FILE*
+ FileReader::Page pg;
+ pg.data.resize(page_size_);
+ const size_t begin = idx * page_size_;
+ const size_t avail = std::min(page_size_, file_size_ - begin);
+
+ file_seek_abs(fp_, begin);
+ if (avail > 0) {
+ const size_t nread = std::fread(pg.data.data(), 1, avail, fp_);
+ if (nread != avail) throw std::runtime_error("Short fread on page");
+ }
+ // trim to actual size
+ pg.data.resize(avail);
+
+ // Insert into LRU
+ if (map_.size() >= capacity_) evict_();
+ lru_.push_front(idx);
+ auto lit = lru_.begin();
+ auto [emplaced_it, ok] = map_.emplace(idx, Entry{ std::move(pg), lit });
+ (void)ok;
+ return emplaced_it->second.page;
+ }
+
+ void touch_(typename std::unordered_map::iterator it) const {
+ lru_.erase(it->second.it);
+ lru_.push_front(it->first);
+ it->second.it = lru_.begin();
+ }
+
+ void evict_() const {
+ if (lru_.empty()) return;
+ const size_t victim = lru_.back();
+ lru_.pop_back();
+ map_.erase(victim);
+ }
+};
+
+#ifdef USE_MMAP
+struct MMapImpl final : FileReader::Impl {
+ boost::iostreams::mapped_file_source map_;
+ size_t size_ = 0;
+
+ explicit MMapImpl(const std::string& fn) {
+ map_.open(boost::filesystem::path(IfcUtil::path::from_utf8(fn)));
+ if (!map_.is_open()) throw std::runtime_error("Failed to open mapped_file_source");
+ size_ = static_cast(map_.size());
+ }
+
+ size_t size() const override { return size_; }
+
+ char get(size_t pos) const override {
+ if (pos >= size_) throw std::out_of_range("get out of range");
+ return map_.data()[pos];
+ }
+};
+#endif
+
+/// User-pushed sequential backend with an arbitrary-length queue of future pages.
+/// We keep a deque of pages; when reads move forward, we drop fully-consumed
+/// pages from the front to release memory.
+struct PushedSequentialImpl final : std::enable_shared_from_this, FileReader::Impl {
+ // Deque of pages, front is earliest in file.
+ std::deque pages_;
+ // total bytes in dropped pages
+ size_t discarded_page_bytes_ = 0;
+
+ size_t size() const override {
+ size_t n = discarded_page_bytes_;
+ for (auto& pg : pages_) n += pg.data.size();
+ return n;
+ }
+
+ // Drop fully-consumed pages so pos is guaranteed to be within the first page
+ void dropPages(size_t pos) override {
+ while (!pages_.empty()) {
+ if (pos - discarded_page_bytes_ >= pages_.front().data.size()) {
+ discarded_page_bytes_ += pages_.front().data.size();
+ pages_.pop_front();
+ } else {
+ break;
+ }
+ }
+ }
+
+ char get(size_t pos) const override {
+ /*
+ auto self = const_cast(this);
+ // We do not do this automatically because all variable width tokens:
+ // ENUM/STRING/BINARY/KEYWORD are stored as file offsets until a full
+ // entity instance is finalized.
+ if (this->shared_from_this().use_count() == 2) {
+ // only drop pages when there is only one active client.
+ // NB this->shared_from_this() increases count by 1
+ self->drop_consumed_up_to(pos);
+ }
+ */
+
+ const size_t avail_end = size();
+ if (pos >= avail_end) throw std::out_of_range("pushed backend: position not committed yet");
+
+ pos -= discarded_page_bytes_;
+
+ size_t page_start = 0;
+ for (const auto& pg : pages_) {
+ if (pos < page_start + pg.data.size()) {
+ const size_t off = pos - page_start;
+ return pg.data[off];
+ } else {
+ page_start += pg.data.size();
+ }
+ }
+
+ throw std::out_of_range("pushed backend: internal inconsistency");
+ }
+
+ void pushNextPage(const std::string& data) override {
+ FileReader::Page p; p.data.assign(data.data(), data.data() + data.size());
+ pages_.push_back(std::move(p));
+ }
+};
+
+IfcParse::FileReader::FileReader(const std::string& fn)
+ : cursor_(0)
+{
+ impl_ = std::make_shared(fn);
+}
+
+IfcParse::FileReader::FileReader(const std::string& fn, const mmap_tag&)
+ : cursor_(0)
+{
+#ifdef USE_MMAP
+ impl_ = std::make_shared(fn);
+#else
+ (void)fn;
+ throw std::runtime_error("IfcParse::FileReader: mmap_tag specified but library not compiled with USE_MMAP");
+#endif
+}
+
+IfcParse::FileReader::FileReader(const caller_fed_tag&)
+ : cursor_(0)
+{
+ impl_ = std::make_shared();
+}
+
+IfcParse::FileReader::FileReader(const std::string& content, const caller_fed_tag&)
+{
+ impl_ = std::make_shared();
+ impl_->pushNextPage(content);
+}
+
+IfcParse::FileReader::FileReader(const std::string& fn, size_t page_size, size_t page_capacity)
+ : cursor_(0)
+{
+ impl_ = std::make_shared(fn, page_size, page_capacity);
+}
+
+FileReader FileReader::clone() const {
+ FileReader c(*this);
+ c.cursor_ = this->cursor_;
+ return c;
+}
+
+void FileReader::seek(size_t pos) {
+ if (pos > impl_->size()) throw std::out_of_range("seek out of range");
+ cursor_ = pos;
+}
+
+size_t FileReader::tell() const { return cursor_; }
+
+size_t FileReader::size() const { return impl_->size(); }
+
+char FileReader::peek() const {
+ if (cursor_ >= impl_->size()) throw std::out_of_range("peek at EOF");
+ return impl_->get(cursor_);
+}
+
+void FileReader::increment(size_t n) {
+ if (cursor_ + n > impl_->size()) throw std::out_of_range("increment past EOF");
+ cursor_ += n;
+}
+
+void IfcParse::FileReader::pushNextPage(const std::string& data)
+{
+ impl_->pushNextPage(data);
+}
+
+void IfcParse::FileReader::dropPages()
+{
+ impl_->dropPages(0);
+}
+
+void IfcParse::FileReader::dropPages(size_t up_to_pos)
+{
+ impl_->dropPages(up_to_pos);
+}
+
+bool IfcParse::FileReader::eof() const
+{
+ return cursor_ >= impl_->size();
+}
+
+char IfcParse::FileReader::read()
+{
+ auto c = peek();
+ increment(1);
+ return c;
+}
+
+char IfcParse::FileReader::get(size_t offset) const
+{
+ return impl_->get(offset);
+}
diff --git a/src/ifcparse/FileReader.h b/src/ifcparse/FileReader.h
new file mode 100644
index 0000000000..164c5826a6
--- /dev/null
+++ b/src/ifcparse/FileReader.h
@@ -0,0 +1,133 @@
+/********************************************************************************
+ * *
+ * 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 . *
+ * *
+ ********************************************************************************/
+
+/*********************************************************************************
+ * *
+ * Reads a file and provides functions to access its *
+ * contents randomly and character by character *
+ * *
+ ********************************************************************************/
+
+#ifndef IFCSPFSTREAM_H
+#define IFCSPFSTREAM_H
+
+#include "ifc_parse_api.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace IfcParse {
+
+/// \brief Read-only file accessor that supports four backends:
+/// - full-buffer in RAM
+/// - paged (LRU-cached with capacity)
+/// - memory-mapped via boost::iostreams
+/// - user-pushed sequential pages (caller feeds pages intended for streaming reads in WASM)
+class IFC_PARSE_API FileReader {
+public:
+ struct Page { std::vector data; };
+
+ /// \brief Tag to choose memory-mapped backend via boost::iostreams::mapped_file_source.
+ struct mmap_tag {};
+ /// \brief Tag to choose user-pushed sequential pages.
+ struct caller_fed_tag {};
+
+ /// \brief Construct a FileReader.
+ /// \param fn File path.
+ /// \param maybe_mmaped_or_chunked
+ /// - std::nullopt: full-buffer mode (entire file loaded into memory)
+ /// - size_t: paged mode with this page size in bytes (e.g., 4096)
+ /// - mmap_tag: memory-mapped mode using boost::iostreams::mapped_file_source
+ /// \param page_cache_capacity Number of pages to keep in the LRU when in paged mode.
+ FileReader(const std::string& fn);
+
+ FileReader(const std::string& fn, const mmap_tag&);
+
+ FileReader(const caller_fed_tag&);
+ FileReader(const std::string& content, const caller_fed_tag&);
+
+ FileReader(const std::string& fn, size_t page_size, size_t page_capacity);
+
+ /// \brief Copy-construct a new reader sharing the underlying storage but with its own cursor.
+ FileReader clone() const;
+
+ /// \brief Seek to an absolute byte position.
+ /// \throws std::out_of_range if pos > size().
+ void seek(size_t pos);
+
+ /// \brief Return the current cursor position.
+ size_t tell() const;
+
+ /// \brief Total file size in bytes.
+ size_t size() const;
+
+ /// \brief Peek the byte at the current cursor.
+ /// \throws std::out_of_range at EOF.
+ char peek() const;
+
+ /// \brief Advance the cursor by n bytes (default 1).
+ /// \throws std::out_of_range if advancing crosses EOF.
+ void increment(size_t n = 1);
+
+ /// \brief Push the next sequential page (pushed backend only).
+ /// \param data Contents of the page.
+ /// \throws std::logic_error if the current backend is not in pushed mode
+ void pushNextPage(const std::string& data);
+
+ /// \brief Drops pages up to cursor position or provided offset. Does nothing when current backend is not in pushed mode
+ /// \param up_to_pos Pages with an end offset before up_to_pos are dropped from memory
+ void dropPages();
+ void dropPages(size_t up_to_pos);
+
+ /// \brief Returns true if the cursor is at or beyond the end of available data.
+ /// For the pushed backend, EOF means all pushed bytes have been consumed.
+ bool eof() const;
+
+ /// \brief Equivalent of peek() followed by increment(1)
+ char read();
+
+ /// \brief Equivalent of peek() followed by increment(1)
+ char get(size_t offset) const;
+
+ struct Impl {
+ virtual ~Impl() = default;
+ virtual size_t size() const = 0;
+ virtual char get(size_t pos) const = 0;
+ /// \brief Backend may support pushing pages; default throws.
+ virtual void pushNextPage(const std::string&) {
+ throw std::logic_error("push_next_page: backend does not support pushed mode");
+ }
+ virtual void dropPages(size_t) {
+ // empty on purpose
+ }
+ };
+
+private:
+ std::shared_ptr impl_;
+ size_t cursor_ = 0;
+};
+
+} // namespace IfcParse
+
+#endif
diff --git a/src/ifcparse/IfcCharacterDecoder.cpp b/src/ifcparse/IfcCharacterDecoder.cpp
index 7e62910808..71685dcedc 100644
--- a/src/ifcparse/IfcCharacterDecoder.cpp
+++ b/src/ifcparse/IfcCharacterDecoder.cpp
@@ -29,7 +29,7 @@
#include "IfcCharacterDecoder.h"
#include "IfcException.h"
-#include "IfcSpfStream.h"
+#include "FileReader.h"
#include "IfcLogger.h"
#include
@@ -77,7 +77,7 @@
using namespace IfcParse;
-IfcCharacterDecoder::IfcCharacterDecoder(IfcParse::IfcSpfStream* stream) {
+IfcCharacterDecoder::IfcCharacterDecoder(IfcParse::FileReader* stream) {
stream_ = stream;
codepage_ = 0;
}
@@ -86,49 +86,9 @@ IfcCharacterDecoder::~IfcCharacterDecoder() {
}
namespace {
-unsigned int reference_helper = 0;
+ std::string read_string(IfcParse::FileReader& stream_, IfcParse::IfcCharacterDecoder::ConversionMode mode, char substitution_character) {
+ std::u32string builder_;
-class pure_impure_helper {
- private:
- bool pure_;
- IfcParse::IfcSpfStream* stream_;
- unsigned int& pointer_;
- std::u32string builder_;
-
- char peek() {
- if (pure_) {
- return stream_->peek_at(pointer_);
- }
- return stream_->Peek();
- }
-
- unsigned int tell() {
- if (pure_) {
- return pointer_;
- }
- return stream_->Tell();
- }
-
- void increment() {
- if (pure_) {
- stream_->increment_at(pointer_);
- } else {
- stream_->Inc();
- }
- }
-
- public:
- pure_impure_helper(IfcParse::IfcSpfStream* stream)
- : pure_(false),
- stream_(stream),
- pointer_(reference_helper) {}
-
- pure_impure_helper(IfcParse::IfcSpfStream* stream, unsigned int& pointer)
- : pure_(true),
- stream_(stream),
- pointer_(pointer) {}
-
- std::string get(IfcParse::IfcCharacterDecoder::ConversionMode mode, char substitution_character) {
unsigned int parse_state = 0;
builder_.clear();
builder_.push_back('\'');
@@ -137,7 +97,7 @@ class pure_impure_helper {
unsigned int hex = 0;
unsigned int hex_count = 0;
- while ((current_char = peek()) != 0) {
+ while ((current_char = stream_.peek()) != 0) {
if (EXPECTS_CHARACTER(parse_state)) {
builder_.push_back(IfcUtil::convert_codepage(codepage, current_char + 0x80));
parse_state = 0;
@@ -178,7 +138,7 @@ class pure_impure_helper {
} else if (IS_HEXADECIMAL(current_char) && EXPECTS_HEX(parse_state)) {
if (IS_LOWERCASE_HEX(current_char)) {
Logger::Warning("Lowercase hexadecimal character '" + std::string(1, current_char) +
- "' found at offset " + std::to_string(pointer_) +
+ "' found at offset " + std::to_string(stream_.tell()) +
". It is recommended to use uppercase for hexadecimal.");
}
hex <<= 4;
@@ -202,12 +162,12 @@ class pure_impure_helper {
if (parse_state == APOSTROPHE && current_char != '\'') {
break;
}
- throw IfcInvalidTokenException(tell(), current_char);
+ throw IfcInvalidTokenException(stream_.tell(), current_char);
} else {
parse_state = hex = hex_count = 0;
builder_.push_back(current_char);
}
- increment();
+ stream_.increment();
}
builder_.push_back('\'');
@@ -248,22 +208,25 @@ class pure_impure_helper {
}
throw IfcParse::IfcException("Invalid conversion mode");
}
-};
} // namespace
IfcCharacterDecoder::operator std::string() {
- return pure_impure_helper(stream_).get(mode, substitution_character);
+ return read_string(*stream_, mode, substitution_character);
}
-std::string IfcCharacterDecoder::get(unsigned int& ptr) {
- return pure_impure_helper(stream_, ptr).get(mode, substitution_character);
+std::string IfcCharacterDecoder::get(size_t& ptr) {
+ auto local_stream = *stream_;
+ local_stream.seek(ptr);
+ auto s = read_string(local_stream, mode, substitution_character);
+ ptr = local_stream.tell();
+ return s;
}
void IfcCharacterDecoder::skip() {
unsigned int parse_state = 0;
char current_char;
unsigned int hex_count = 0;
- while ((current_char = stream_->Peek()) != 0) {
+ while ((current_char = stream_->peek()) != 0) {
if (EXPECTS_CHARACTER(parse_state)) {
parse_state = 0;
} else if (current_char == '\'' && (parse_state == 0U)) {
@@ -318,11 +281,11 @@ void IfcCharacterDecoder::skip() {
if (parse_state == APOSTROPHE && current_char != '\'') {
break;
}
- throw IfcInvalidTokenException(stream_->Tell(), current_char);
+ throw IfcInvalidTokenException(stream_->tell(), current_char);
} else {
parse_state = hex_count = 0;
}
- stream_->Inc();
+ stream_->increment();
}
}
diff --git a/src/ifcparse/IfcCharacterDecoder.h b/src/ifcparse/IfcCharacterDecoder.h
index 6a6d318aa1..e978a5b4c2 100644
--- a/src/ifcparse/IfcCharacterDecoder.h
+++ b/src/ifcparse/IfcCharacterDecoder.h
@@ -27,7 +27,7 @@
#ifndef IFCCHARACTERDECODER_H
#define IFCCHARACTERDECODER_H
-#include "IfcSpfStream.h"
+#include "FileReader.h"
#include
@@ -41,7 +41,7 @@ namespace IfcParse {
class IFC_PARSE_API IfcCharacterDecoder {
private:
- IfcParse::IfcSpfStream* stream_;
+ IfcParse::FileReader* stream_;
int codepage_;
public:
@@ -52,7 +52,7 @@ class IFC_PARSE_API IfcCharacterDecoder {
};
static ConversionMode mode;
static char substitution_character;
- IfcCharacterDecoder(IfcParse::IfcSpfStream* stream);
+ IfcCharacterDecoder(IfcParse::FileReader* stream);
~IfcCharacterDecoder();
// Only advances the underlying token stream read pointer
// to the next token.
@@ -62,7 +62,7 @@ class IFC_PARSE_API IfcCharacterDecoder {
operator std::string();
// Gets a decoded string representation at the offset provided,
// does not mutate the underlying token stream read pointer.
- std::string get(unsigned int&);
+ std::string get(size_t&);
};
} // namespace IfcParse
diff --git a/src/ifcparse/IfcEntityInstanceData.cpp b/src/ifcparse/IfcEntityInstanceData.cpp
index ca2f73b4ef..e87c429545 100644
--- a/src/ifcparse/IfcEntityInstanceData.cpp
+++ b/src/ifcparse/IfcEntityInstanceData.cpp
@@ -38,7 +38,7 @@ namespace {
}
#ifdef IFOPSH_WITH_ROCKSDB
else {
- T val;
+ T val = T{};
const bool is_header = entity_or_type->schema() == &Header_section_schema::get_schema();
if constexpr (
// the following types cannot be directly deserialized from rocksdb, but need to be constructed
@@ -51,6 +51,12 @@ namespace {
(is_header ? entity_or_type->name() : std::to_string(instance_name_)) + "|" +
std::to_string(index_), &str);
impl::deserialize(array_.db_ptr, str, val);
+ } else {
+ static_assert(
+ std::is_same_v ||
+ std::is_same_v>, IfcUtil::IfcBaseClass>,
+ "RocksDB deserialization must be specialized for this EnumerationReference and IfcBaseClass*"
+ );
}
return val;
}
@@ -194,13 +200,15 @@ AttributeValue::operator IfcUtil::IfcBaseClass* () const
std::to_string(index_), &str);
size_t v;
memcpy(&v, str.data() + 2, sizeof(size_t));
- if (str[1] == 'i') {
+ if (str.size() > 1 && str[1] == 'i') {
// entity reference, by #Name
return array_.db_ptr->assert_existance(v, IfcParse::impl::rocks_db_file_storage::entityinstance_ref);
- } else if (str[1] == 't') {
+ } else if (str.size() > 1 && str[1] == 't') {
// type reference by Identity
return array_.db_ptr->assert_existance(v, IfcParse::impl::rocks_db_file_storage::typedecl_ref);
- }
+ } else {
+ throw std::runtime_error("Invalid data encountered");
+ }
}
#endif
}
@@ -339,28 +347,28 @@ bool impl::serialize(std::string& val, const aggregate_of_aggregate_of_instance:
return true;
}
-bool impl::serialize(std::string& val, const Blank& t)
+bool impl::serialize(std::string& val, const Blank&)
{
val.resize(1);
val[0] = TypeEncoder::encode_type();
return true;
}
-bool impl::serialize(std::string& val, const Derived& t)
+bool impl::serialize(std::string& val, const Derived&)
{
val.resize(1);
val[0] = TypeEncoder::encode_type();
return true;
}
-bool impl::serialize(std::string& val, const empty_aggregate_t& t)
+bool impl::serialize(std::string& val, const empty_aggregate_t&)
{
val.resize(1);
val[0] = TypeEncoder::encode_type();
return true;
}
-bool impl::serialize(std::string& val, const empty_aggregate_of_aggregate_t& t)
+bool impl::serialize(std::string& val, const empty_aggregate_of_aggregate_t&)
{
val.resize(1);
val[0] = TypeEncoder::encode_type();
@@ -397,6 +405,7 @@ bool impl::deserialize(IfcParse::impl::rocks_db_file_storage*, const std::string
} else {
return false;
}
+ return true;
}
bool impl::deserialize(IfcParse::impl::rocks_db_file_storage*, const std::string& val, boost::dynamic_bitset<>& t) {
diff --git a/src/ifcparse/IfcEntityInstanceData.h b/src/ifcparse/IfcEntityInstanceData.h
index df2edddb94..04e7dcb0ad 100644
--- a/src/ifcparse/IfcEntityInstanceData.h
+++ b/src/ifcparse/IfcEntityInstanceData.h
@@ -155,7 +155,7 @@ struct TypeEncoder_t> {
using TypeEncoder = TypeEncoder_t;
struct IFC_PARSE_API MutableAttributeValue {
- int name_;
+ uint32_t name_;
uint8_t index_;
};
@@ -165,7 +165,7 @@ namespace IfcParse {
}
}
-#if IFOPSH_WITH_ROCKSDB
+#ifdef IFOPSH_WITH_ROCKSDB
namespace impl {
@@ -266,7 +266,6 @@ namespace impl {
if (val[0] != TypeEncoder::encode_type()) {
return false;
}
- auto s = (val.size() - 1) / sizeof(T);
memcpy(&t, val.data() + 1, sizeof(T));
return true;
}
@@ -404,7 +403,7 @@ public:
template
auto apply_visitor(void* storage, const IfcParse::declaration* decl, std::size_t identity, std::size_t index, Visitor&& visitor) const {
// @todo do we need visitation on all data/storage/attribute levels?
- AttributeValue((IfcParse::impl::rocks_db_file_storage*)storage, identity, decl, index).apply_visitor(std::forward(visitor));
+ AttributeValue((IfcParse::impl::rocks_db_file_storage*)storage, identity, decl, (uint8_t) index).apply_visitor(std::forward(visitor));
}
#endif
};
diff --git a/src/ifcparse/IfcException.h b/src/ifcparse/IfcException.h
index 6689ebb7d4..f30a5e9f3f 100644
--- a/src/ifcparse/IfcException.h
+++ b/src/ifcparse/IfcException.h
@@ -57,7 +57,7 @@ class IFC_PARSE_API IfcAttributeOutOfRangeException : public IfcException {
class IFC_PARSE_API IfcInvalidTokenException : public IfcException {
public:
IfcInvalidTokenException(
- int token_start,
+ size_t token_start,
const std::string& token_string,
const std::string& expected_type)
: IfcException(
@@ -65,7 +65,7 @@ class IFC_PARSE_API IfcInvalidTokenException : public IfcException {
boost::lexical_cast(token_start) +
" invalid " + expected_type) {}
IfcInvalidTokenException(
- int token_start,
+ size_t token_start,
char character)
: IfcException(
std::string("Unexpected '") + std::string(1, character) + "' at offset " +
diff --git a/src/ifcparse/IfcFile.cpp b/src/ifcparse/IfcFile.cpp
index 25cd414c4c..d0b2dd030e 100644
--- a/src/ifcparse/IfcFile.cpp
+++ b/src/ifcparse/IfcFile.cpp
@@ -56,7 +56,7 @@ namespace {
constexpr bool is_type_in_variant_v = is_type_in_variant::value;
template
- void dispatch_token(int instance_id, int attribute_id, IfcParse::Token t, IfcParse::declaration* decl, Fn fn) {
+ void dispatch_token(boost::optional instance_id, int attribute_id, IfcParse::Token t, IfcParse::declaration* decl, Fn fn) {
if (t.type == IfcParse::Token_BINARY) {
fn(IfcParse::TokenFunc::asBinary(t));
} else if (IfcParse::TokenFunc::isBool(t)) {
@@ -89,7 +89,7 @@ namespace {
}
template
- void construct_(int instance_id, int attribute_id, IfcParse::parse_context& p, const IfcParse::aggregation_type* aggr, Fn fn) {
+ void construct_(boost::optional instance_id, int attribute_id, IfcParse::parse_context& p, const IfcParse::aggregation_type* aggr, Fn fn) {
if (p.tokens_.empty()) {
// @todo instead of ugly if-else we could also default initialize the respective
// variant types below.
@@ -234,7 +234,7 @@ namespace {
}
}
-IfcEntityInstanceData IfcParse::parse_context::construct(int name, unresolved_references& references_to_resolve, const IfcParse::declaration* decl, boost::optional expected_size, int resolve_reference_index, bool coerce_attribute_count) {
+IfcEntityInstanceData IfcParse::parse_context::construct(boost::optional name, unresolved_references& references_to_resolve, const IfcParse::declaration* decl, boost::optional expected_size, int resolve_reference_index, bool coerce_attribute_count) {
std::vector parameter_types;
std::unique_ptr transient_named_type;
@@ -259,7 +259,11 @@ IfcEntityInstanceData IfcParse::parse_context::construct(int name, unresolved_re
expected_size && *expected_size != tokens_.size())
{
size_t expected = expected_size ? *expected_size : parameter_types.size();
- Logger::Warning("Expected " + std::to_string(expected) + " attribute values, found " + std::to_string(tokens_.size()) + " for instance #" + std::to_string(name > 0 ? name : 0));
+ if (decl != nullptr && decl->schema() == &Header_section_schema::get_schema()) {
+ Logger::Warning("Expected " + std::to_string(expected) + " attribute values, found " + std::to_string(tokens_.size()) + " for header entity " + decl->name());
+ } else {
+ Logger::Warning("Expected " + std::to_string(expected) + " attribute values, found " + std::to_string(tokens_.size()) + (name ? std::string(" for instance #" + std::to_string(*name)) : std::string("")));
+ }
}
if (tokens_.empty()) {
@@ -289,12 +293,12 @@ IfcEntityInstanceData IfcParse::parse_context::construct(int name, unresolved_re
if constexpr (std::is_same_v, IfcParse::Token>) {
dispatch_token(name, index, v, param_type && param_type->as_named_type() ? param_type->as_named_type()->declared_type() : nullptr, [this, &storage, name, &references_to_resolve, index, resolve_reference_index](auto v) {
if constexpr (std::is_same_v, IfcParse::reference_or_simple_type>) {
- if (name > 0) {
+ if (name) {
references_to_resolve.push_back(std::make_pair(
// @todo previously this was storage but apparently the
// pointer is not constant with the moving and temporary nature
// maybe it ought to be and in that case a pointer is more direct
- MutableAttributeValue{ name, resolve_reference_index == -1 ? index : (uint8_t) resolve_reference_index },
+ MutableAttributeValue{ (uint32_t) *name, resolve_reference_index == -1 ? index : (uint8_t) resolve_reference_index },
v
));
}
@@ -311,12 +315,12 @@ IfcEntityInstanceData IfcParse::parse_context::construct(int name, unresolved_re
}
construct_<0>(name, index, *v, pt ? pt->as_aggregation_type() : nullptr, [this, &storage, name, &references_to_resolve, index, resolve_reference_index](const auto& v) {
if constexpr (std::is_same_v, std::vector>) {
- if (name > 0) {
- references_to_resolve.push_back({ {name, resolve_reference_index == -1 ? index : (uint8_t)resolve_reference_index }, v });
+ if (name) {
+ references_to_resolve.push_back({ { (uint32_t) *name, resolve_reference_index == -1 ? index : (uint8_t)resolve_reference_index }, v });
}
} else if constexpr (std::is_same_v, std::vector>>) {
- if (name > 0) {
- references_to_resolve.push_back({ {name, resolve_reference_index == -1 ? index : (uint8_t)resolve_reference_index }, v });
+ if (name) {
+ references_to_resolve.push_back({ { (uint32_t) *name, resolve_reference_index == -1 ? index : (uint8_t)resolve_reference_index }, v });
}
} else {
storage.set(index, v);
@@ -651,7 +655,18 @@ IfcParse::filetype IfcParse::guess_file_type(const std::string& fn) {
}
}
-std::optional> IfcParse::InstanceStreamer::read_instance() {
+void IfcParse::InstanceStreamer::bypassTypes(const std::set& type_names) {
+ for (auto& name : type_names) {
+ try {
+ types_to_bypass_.push_back(schema_->declaration_by_name(name));
+ } catch (const IfcException&) {
+ continue;
+ }
+ }
+ }
+
+
+std::optional> IfcParse::InstanceStreamer::readInstance() {
std::optional> return_value;
if (header_ && yielded_header_instances_ < 3) {
@@ -679,7 +694,7 @@ std::optionalstream->eof && !current_id) {
+ while (good_ && !lexer_->stream->eof() && !current_id) {
if (token_stream_[0].type == IfcParse::Token_IDENTIFIER &&
token_stream_[1].type == IfcParse::Token_OPERATOR &&
token_stream_[1].value_char == '=' &&
@@ -698,6 +713,15 @@ std::optionalis(*ty)) {
+ bypassed_instances_.push_back(current_id);
+ // Why is this a conditional clause in the loop?
+ current_id = 0;
+ goto advance;
+ }
+ }
+
parse_context ps;
lexer_->Next();
try {
@@ -734,7 +758,7 @@ std::optionalstream->eof && next_token.type == Token_NONE) {
+ if (!lexer_->stream->eof() && next_token.type == Token_NONE) {
good_ = file_open_status::INVALID_SYNTAX;
break;
}
@@ -742,13 +766,17 @@ std::optionalstream)->dropPages();
+
return return_value;
}
IfcUtil::IfcBaseClass* IfcParse::impl::rocks_db_file_storage::create(const IfcParse::declaration* decl) {
if (decl->as_entity() || decl->as_type_declaration()) {
auto* inst = file->schema()->instantiate(decl, rocks_db_attribute_storage{});
- inst->file_ = file;
+ // @todo maybe this needs to be set to file? In order to have a context (ie. rocksdb::db*) to write to?
+ inst->file_ = nullptr;
return file->addEntity(inst);
} else {
throw std::runtime_error("Requires and entity or type declaration");
@@ -764,6 +792,7 @@ IfcUtil::IfcBaseClass* IfcParse::impl::in_memory_file_storage::create(const IfcP
} else {
throw std::runtime_error("Requires and entity or type declaration");
}
- inst->file_ = file;
+ // file_ should be nullptr in order not to bypass addEntity() behaviour of registration in maps
+ inst->file_ = nullptr;
return file->addEntity(inst);
}
diff --git a/src/ifcparse/IfcFile.h b/src/ifcparse/IfcFile.h
index 030321d9ee..96685a88ee 100644
--- a/src/ifcparse/IfcFile.h
+++ b/src/ifcparse/IfcFile.h
@@ -88,23 +88,24 @@ IFC_PARSE_API filetype guess_file_type(const std::string& fn);
class IFC_PARSE_API InstanceStreamer {
private:
- IfcSpfStream* stream_;
+ FileReader* stream_;
IfcSpfLexer* lexer_;
IfcSpfHeader* header_;
boost::circular_buffer token_stream_;
const IfcParse::schema_definition* schema_;
- const IfcParse::declaration* ifcroot_type_;
IfcParse::impl::in_memory_file_storage storage_;
IfcParse::file_open_status good_ = IfcParse::file_open_status::SUCCESS;
int progress_;
IfcParse::unresolved_references references_to_resolve_;
int yielded_header_instances_ = 0;
+ std::vector types_to_bypass_;
+ std::vector bypassed_instances_;
-public:
+ public:
bool coerce_attribute_count = true;
operator bool() const {
- return good_ && !lexer_->stream->eof;
+ return good_ && !lexer_->stream->eof();
}
IfcParse::file_open_status status() const {
@@ -119,6 +120,11 @@ public:
return references_to_resolve_;
}
+ const std::vector& bypassed_instances() {
+ std::sort(bypassed_instances_.begin(), bypassed_instances_.end());
+ return bypassed_instances_;
+ }
+
const IfcParse::impl::in_memory_file_storage::entities_by_ref_t& inverses() const {
return storage_.byref_excl_;
}
@@ -127,12 +133,26 @@ public:
return storage_.byref_excl_;
}
- InstanceStreamer(const std::string& fn);
+ std::vector> stealInstances() {
+ return storage_.steal_instances();
+ }
+
+ bool hasSemicolon() const;
+
+ size_t semicolonCount() const;
+
+ void pushPage(const std::string& page);
+
+ InstanceStreamer();
+
+ InstanceStreamer(const std::string& fn, bool mmap=false);
InstanceStreamer(void* data, int length);
InstanceStreamer(const IfcParse::schema_definition* schema, IfcParse::IfcSpfLexer* lexer);
+ void bypassTypes(const std::set& type_names);
+
~InstanceStreamer() {
delete stream_;
if (stream_) {
@@ -141,9 +161,12 @@ public:
delete header_;
}
- std::optional> read_instance();
+ std::optional> readInstance();
};
+class uninitialized_tag {};
+
+
/// This class provides access to the entity instances in an IFC file
/// The file takes ownership of instances added to this file and deletes them when the file is deleted.
class IFC_PARSE_API IfcFile {
@@ -170,7 +193,10 @@ public:
// @todo temporarily public for header
storage_t storage_;
-private:
+
+ std::set types_to_bypass_loading_;
+
+ private:
file_open_status good_ = file_open_status::SUCCESS;
const IfcParse::schema_definition* schema_;
@@ -197,16 +223,60 @@ private:
public:
#ifdef USE_MMAP
- IfcFile(const std::string& path, bool mmap = false);
-#else
- IfcFile(const std::string& path, filetype ty=FT_AUTODETECT, bool readonly=false);
+ ///
+ /// Constructs an IfcFile object from a file path, optionally using memory-mapped I/O, only supports IFC-SPF files.
+ ///
+ /// UTF-8 file path to an IFC-SPF file
+ /// Whether to use memory-mapped I/O
+ IfcFile(const std::string& path, bool mmap);
#endif
+ ///
+ /// Constructs an IfcFile object from a file path, supports IFC-SPF and the IfcOpenShell-specific RocksDB format.
+ ///
+ /// UTF-8 file path to an IFC-SPF file or RocksDB database directory
+ /// File type of the path
+ /// Whether to open in read-only mode, only supported on RocksDB databases
+ IfcFile(const std::string& path, filetype ty=FT_AUTODETECT, bool readonly=false);
+
+ ///
+ /// Constructs an IfcFile object from a stream containing IFC-SPF data.
+ ///
IfcFile(std::istream& stream, int length);
+
+ ///
+ /// Constructs an IfcFile object from a memory buffer containing IFC-SPF data.
+ ///
IfcFile(void* data, int length);
- IfcFile(IfcParse::IfcSpfStream* stream);
- // @nb path is only used in rocksdb mode, for spf file is in-memory only until write() is called
+
+ ///
+ /// Constructs an IfcFile object from a given IFC SPF stream.
+ ///
+ /// A pointer to an IfcParse::FileReader object representing the input IFC SPF data stream.
+ IfcFile(IfcParse::FileReader* stream);
+
+ ///
+ /// Constructs an IfcFile object with the specified schema, file type, and file path.
+ /// @nb path is only used in rocksdb mode, for spf file is in-memory only until write() is called
+ ///
+ /// Pointer to the schema definition to use. Defaults to the IFC4 schema if not specified.
+ /// The file type to use for the file. Defaults to FT_AUTODETECT.
+ /// The file system path to the IFC file. Defaults to an empty string.
IfcFile(const IfcParse::schema_definition* schema = IfcParse::schema_by_name("IFC4"), filetype ty = FT_AUTODETECT, const std::string& path = "");
+ ///
+ /// Constructs an unitialized IfcFile object. Call initialize() later on. Allows to specify which types to bypass during load.
+ ///
+ IfcFile(const uninitialized_tag&);
+
+ bool initialize(const std::string& path, filetype ty = FT_AUTODETECT, bool readonly = false);
+#ifdef USE_MMAP
+ bool initialize(const std::string& path, bool mmap);
+#endif
+
+ /// @brief Bypass loading of all instances of the specified type name. Only applies to parsed IFC-SPF files.
+ /// @param type_name case insensitive name of the type to bypass
+ void bypass_type(const std::string& type_name);
+
~IfcFile();
IfcParse::file_open_status good() const { return good_; }
diff --git a/src/ifcparse/IfcParse.cpp b/src/ifcparse/IfcParse.cpp
index 183047b6fd..3a7c373a7d 100644
--- a/src/ifcparse/IfcParse.cpp
+++ b/src/ifcparse/IfcParse.cpp
@@ -26,7 +26,7 @@
#include "IfcLogger.h"
#include "IfcSchema.h"
#include "IfcSIPrefix.h"
-#include "IfcSpfStream.h"
+#include "FileReader.h"
#include "utils.h"
#include
@@ -102,167 +102,7 @@ void init_locale() {
#endif
-//
-// Opens the file and gets the filesize
-//
-#ifdef USE_MMAP
-IfcSpfStream::IfcSpfStream(const std::string& path, bool mmap)
-#else
-IfcSpfStream::IfcSpfStream(const std::string& path)
-#endif
- : stream_(0),
- buffer_(0),
- valid(false),
- eof(false) {
-#ifdef _MSC_VER
- std::wstring fn_ws = IfcUtil::path::from_utf8(path);
- const wchar_t* fn_wide = fn_ws.c_str();
-
-#ifdef USE_MMAP
- if (mmap) {
- mfs = boost::iostreams::mapped_file_source(boost::filesystem::wpath(fn_wide));
- } else {
-#endif
- stream_ = _wfopen(fn_wide, L"rb");
-#ifdef USE_MMAP
- }
-#endif
-
-#else
-
-#ifdef USE_MMAP
- if (mmap) {
- mfs = boost::iostreams::mapped_file_source(path);
- } else {
-#endif
- stream_ = fopen(path.c_str(), "rb");
-#ifdef USE_MMAP
- }
-#endif
-
-#endif
-
-#ifdef USE_MMAP
- if (mmap) {
- if (!mfs.is_open()) {
- return;
- }
-
- valid = true;
- buffer_ = mfs.data();
- ptr_ = 0;
- len_ = mfs.size();
- } else {
-#endif
- if (stream_ == NULL) {
- return;
- }
-
- valid = true;
- fseek(stream_, 0, SEEK_END);
- size = (unsigned int)ftell(stream_);
- rewind(stream_);
- char* buffer_rw = new char[size];
- len_ = (unsigned int)fread(buffer_rw, 1, size, stream_);
- buffer_ = buffer_rw;
- eof = len_ == 0;
- ptr_ = 0;
- fclose(stream_);
- stream_ = nullptr;
-#ifdef USE_MMAP
- }
-#endif
-}
-
-IfcSpfStream::IfcSpfStream(std::istream& stream, int length)
- : stream_(0),
- buffer_(0) {
- eof = false;
- size = length;
- char* buffer_rw = new char[size];
- stream.read(buffer_rw, size);
- buffer_ = buffer_rw;
- valid = stream.gcount() == size;
- ptr_ = 0;
- len_ = length;
-}
-
-IfcSpfStream::IfcSpfStream(void* data, int length)
- : stream_(0),
- buffer_(0) {
- eof = false;
- size = length;
- buffer_ = (char*)data;
- valid = true;
- ptr_ = 0;
- len_ = length;
-}
-
-IfcSpfStream::~IfcSpfStream() {
- Close();
-}
-
-void IfcSpfStream::Close() {
-#ifdef USE_MMAP
- if (mfs.is_open()) {
- mfs.close();
- return;
- }
-#endif
- delete[] buffer_;
- if (stream_ != nullptr) {
- fclose(stream_);
- }
-}
-
-//
-// Seeks an arbitrary position in the file
-//
-void IfcSpfStream::Seek(unsigned int offset) {
- ptr_ = offset;
- if (ptr_ >= len_) {
- throw IfcException("Reading outside of file limits");
- }
- eof = false;
-}
-
-//
-// Returns the character at the cursor
-//
-char IfcSpfStream::Peek() {
- return buffer_[ptr_];
-}
-
-//
-// Returns the character at specified offset
-//
-char IfcSpfStream::Read(unsigned int offset) {
- return buffer_[offset];
-}
-
-//
-// Returns the cursor position
-//
-unsigned int IfcSpfStream::Tell() const {
- return ptr_;
-}
-
-//
-// Increments cursor and reads new chunk if necessary
-//
-void IfcSpfStream::Inc() {
- if (++ptr_ == len_) {
- eof = true;
- return;
- }
- const char current = IfcSpfStream::Peek();
- if (current == '\n' || current == '\r') {
- // NB this is recursive. It might as well be a loop.
- IfcSpfStream::Inc();
- }
-}
-
-IfcSpfLexer::IfcSpfLexer(IfcParse::IfcSpfStream* stream_) {
+IfcSpfLexer::IfcSpfLexer(IfcParse::FileReader* stream_) {
stream = stream_;
decoder_ = new IfcCharacterDecoder(stream_);
}
@@ -271,12 +111,12 @@ IfcSpfLexer::~IfcSpfLexer() {
delete decoder_;
}
-unsigned int IfcSpfLexer::skipWhitespace() const {
- unsigned int index = 0;
- while (!stream->eof) {
- char character = stream->Peek();
+size_t IfcSpfLexer::skipWhitespace() const {
+ size_t index = 0;
+ while (!stream->eof()) {
+ char character = stream->peek();
if ((character == ' ' || character == '\r' || character == '\n' || character == '\t')) {
- stream->Inc();
+ stream->increment();
++index;
} else {
break;
@@ -285,22 +125,25 @@ unsigned int IfcSpfLexer::skipWhitespace() const {
return index;
}
-unsigned int IfcSpfLexer::skipComment() const {
- char character = stream->Peek();
+size_t IfcSpfLexer::skipComment() const {
+ if (stream->eof()) {
+ return 0;
+ }
+ char character = stream->peek();
if (character != '/') {
return 0;
}
- stream->Inc();
- character = stream->Peek();
+ stream->increment();
+ character = stream->peek();
if (character != '*') {
- stream->Seek(stream->Tell() - 1);
+ stream->seek(stream->tell() - 1);
return 0;
}
- unsigned int index = 2;
+ size_t index = 2;
char intermediate = 0;
- while (!stream->eof) {
- character = stream->Peek();
- stream->Inc();
+ while (!stream->eof()) {
+ character = stream->peek();
+ stream->increment();
++index;
if (character == '/' && intermediate == '*') {
break;
@@ -315,19 +158,20 @@ unsigned int IfcSpfLexer::skipComment() const {
//
Token IfcSpfLexer::Next() {
- if (stream->eof) {
+ if (stream->eof()) {
return Token{};
}
while ((skipWhitespace() != 0U) || (skipComment() != 0U)) {
}
- if (stream->eof) {
+ if (stream->eof()) {
return Token{};
}
- unsigned int pos = stream->Tell();
- char character = stream->Peek();
+ auto& str = GetTempString();
+ auto pos = stream->tell();
+ char character = stream->read();
// If the cursor is at [()=,;$*] we know token consists of single char
if (character == '(' ||
@@ -336,69 +180,47 @@ Token IfcSpfLexer::Next() {
character == ',' ||
character == ';' ||
character == '$' ||
- character == '*') {
- stream->Inc();
- return OperatorTokenPtr(this, pos, pos + 1);
+ character == '*')
+ {
+ return OperatorTokenPtr(this, pos, character);
}
- int len = 0;
-
- while (!stream->eof) {
-
- // Read character and increment pointer if not starting a new token
- character = stream->Peek();
- if ((len != 0) && (character == '(' ||
- character == ')' ||
- character == '=' ||
- character == ',' ||
- character == ';' ||
- character == '/')) {
- break;
- }
- stream->Inc();
- len++;
-
+ if (character == '\'') {
// If a string is encountered defer processing to the IfcCharacterDecoder
- if (character == '\'') {
- decoder_->skip();
+ str = *decoder_;
+ } else {
+ str.assign(&character, 1);
+
+ while (!stream->eof()) {
+ // Read character and increment pointer if not starting a new token
+ character = stream->peek();
+ if (character == '(' ||
+ character == ')' ||
+ character == '=' ||
+ character == ',' ||
+ character == ';' ||
+ character == '/') {
+ break;
+ }
+ if (!(character == ' ' || character == '\r' || character == '\n' || character == '\t')) {
+ str.push_back(character);
+ }
+ stream->increment();
}
}
- Token t;
- if (len != 0) {
- t = GeneralTokenPtr(this, pos, stream->Tell());
- } else {
- t = Token{};
- }
- // std::wcout << "token: " << pos << " " << TokenFunc::asStringRef(t).c_str() << std::endl;
- return t;
-}
-
-bool IfcSpfStream::is_eof_at(unsigned int local_ptr) const {
- return local_ptr >= len_;
-}
-
-void IfcSpfStream::increment_at(unsigned int& local_ptr) {
- if (++local_ptr == len_) {
- return;
- }
- const char current = IfcSpfStream::peek_at(local_ptr);
- if (current == '\n' || current == '\r') {
- IfcSpfStream::increment_at(local_ptr);
- }
-}
-
-char IfcSpfStream::peek_at(unsigned int local_ptr) {
- return buffer_[local_ptr];
+ return GeneralTokenPtr(this, pos, str);
}
//
// Reads a std::string from the file at specified offset
// Omits whitespace and comments
//
-void IfcSpfLexer::TokenString(unsigned int offset, std::string& buffer) {
+void IfcSpfLexer::TokenString(size_t offset, std::string& buffer) {
buffer.clear();
- while (!stream->is_eof_at(offset)) {
- char character = stream->peek_at(offset);
+ auto local_stream = *this->stream;
+ local_stream.seek(offset);
+ while (!local_stream.eof()) {
+ char character = local_stream.peek();
if (!buffer.empty() && (character == '(' ||
character == ')' ||
character == '=' ||
@@ -407,7 +229,7 @@ void IfcSpfLexer::TokenString(unsigned int offset, std::string& buffer) {
character == '/')) {
break;
}
- stream->increment_at(offset);
+ local_stream.increment();
if (character == ' ' ||
character == '\r' ||
character == '\n' ||
@@ -416,6 +238,7 @@ void IfcSpfLexer::TokenString(unsigned int offset, std::string& buffer) {
}
if (character == '\'') {
// todo, make decoder use local offset ptr
+ auto offset = local_stream.tell();
buffer = decoder_->get(offset);
break;
}
@@ -424,10 +247,11 @@ void IfcSpfLexer::TokenString(unsigned int offset, std::string& buffer) {
}
//Note: according to STEP standard, there may be newlines in tokens
-inline void RemoveTokenSeparators(IfcSpfStream* stream, unsigned start, unsigned end, std::string& oDestination) {
+/*
+inline void RemoveTokenSeparators(FileReader* stream, size_t start, size_t end, std::string& oDestination) {
oDestination.clear();
for (unsigned i = start; i < end; i++) {
- char character = stream->Read(i);
+ char character = stream->get(i);
if (character == ' ' ||
character == '\r' ||
character == '\n' ||
@@ -437,6 +261,7 @@ inline void RemoveTokenSeparators(IfcSpfStream* stream, unsigned start, unsigned
oDestination += character;
}
}
+*/
bool ParseInt(const char* pStart, int& val) {
char* pEnd;
@@ -481,22 +306,17 @@ bool ParseBool(const char* pStart, int& val) {
return true;
}
-Token IfcParse::OperatorTokenPtr(IfcSpfLexer* lexer, unsigned start, unsigned end) {
- char first = lexer->stream->Read(start);
- Token token(lexer, start, end, Token_OPERATOR);
- token.value_char = first;
+Token IfcParse::OperatorTokenPtr(IfcSpfLexer* lexer, size_t start, char data) {
+ Token token(lexer, start, Token_OPERATOR);
+ token.value_char = data;
return token;
}
-Token IfcParse::GeneralTokenPtr(IfcSpfLexer* lexer, unsigned start, unsigned end) {
- Token token(lexer, start, end, Token_NONE);
-
- //extract token into temp buffer (remove eol-s, no encoding changes)
- std::string& tokenStr = lexer->GetTempString();
- RemoveTokenSeparators(lexer->stream, start, end, tokenStr);
+Token IfcParse::GeneralTokenPtr(IfcSpfLexer* lexer, size_t start, const std::string& tokenStr) {
+ Token token(lexer, start, Token_NONE);
//determine type of the token
- char first = lexer->stream->Read(start);
+ const char& first = tokenStr.front();
if (first == '#') {
token.type = Token_IDENTIFIER;
if (!ParseInt(tokenStr.c_str() + 1, token.value_int)) {
@@ -676,7 +496,25 @@ boost::dynamic_bitset<> TokenFunc::asBinary(const Token& token) {
std::string TokenFunc::toString(const Token& token) {
std::string result;
- token.lexer->TokenString(token.startPos, result);
+ if (token.type == Token_OPERATOR) {
+ result.push_back(token.value_char);
+ } else if (token.type == Token_INT) {
+ result = std::to_string(token.value_int);
+ } else if (token.type == Token_BOOL) {
+ if (token.value_int == 1) {
+ result = ".T.";
+ } else if (token.value_int == 0) {
+ result = ".F.";
+ } else {
+ result = ".U.";
+ }
+ } else if (token.type == Token_FLOAT) {
+ std::ostringstream oss;
+ oss << std::setprecision(15) << token.value_double;
+ result = oss.str();
+ } else {
+ token.lexer->TokenString(token.startPos, result);
+ }
return result;
}
@@ -684,7 +522,7 @@ std::string TokenFunc::toString(const Token& token) {
// Reads the arguments from a list of token
// Aditionally, registers the ids (i.e. #[\d]+) in the inverse map
//
-void IfcParse::impl::in_memory_file_storage::load(unsigned entity_instance_name, const IfcParse::entity* entity, parse_context& context, int attribute_index) {
+void IfcParse::impl::in_memory_file_storage::load(boost::optional entity_instance_name, const IfcParse::entity* entity, parse_context& context, int attribute_index) {
Token next = tokens->Next();
/*
@@ -708,8 +546,8 @@ void IfcParse::impl::in_memory_file_storage::load(unsigned entity_instance_name,
load(entity_instance_name, entity, context.push(), attribute_index == -1 ? (int) attribute_index_within_data : attribute_index);
} else {
return_value++;
- if (TokenFunc::isIdentifier(next) && entity) {
- register_inverse(entity_instance_name, entity, next.value_int, attribute_index == -1 ? attribute_index_within_data : attribute_index);
+ if (TokenFunc::isIdentifier(next) && entity && entity_instance_name) {
+ register_inverse(*entity_instance_name, entity, next.value_int, attribute_index == -1 ? (int) attribute_index_within_data : attribute_index);
}
if (TokenFunc::isKeyword(next)) {
@@ -725,7 +563,7 @@ void IfcParse::impl::in_memory_file_storage::load(unsigned entity_instance_name,
// the 2nd pass.
load(entity_instance_name, entity, ps, attribute_index == -1 ? (int)attribute_index_within_data : attribute_index);
auto* simple_type_instance = (schema ? schema : file->schema())->instantiate(decl, ps.construct(entity_instance_name, *references_to_resolve, decl, boost::none, attribute_index == -1 ? (int)attribute_index_within_data : attribute_index));
- read_simple_type_instances.push_back(simple_type_instance);
+ read_simple_type_instances.emplace_back(simple_type_instance);
//@todo decide addEntity(((IfcUtil::IfcBaseClass*)*entity));
context.push(simple_type_instance);
simple_type_instance->file_ = file;
@@ -758,10 +596,10 @@ IfcEntityInstanceData IfcParse::impl::in_memory_file_storage::read(unsigned int
}
void IfcParse::impl::in_memory_file_storage::try_read_semicolon() const {
- unsigned int old_offset = tokens->stream->Tell();
+ auto old_offset = tokens->stream->tell();
Token semilocon = tokens->Next();
if (!TokenFunc::isOperator(semilocon, ';')) {
- tokens->stream->Seek(old_offset);
+ tokens->stream->seek(old_offset);
}
}
@@ -783,7 +621,7 @@ void IfcParse::impl::in_memory_file_storage::unregister_inverse(unsigned id_from
namespace {
template
- std::string to_string_fixed_width(const T& t, size_t w) {
+ std::string to_string_fixed_width(const T& t, size_t) {
// @todo currently inactive
std::ostringstream oss;
oss << /*std::setfill('0') << std::setw(w) <<*/ t;
@@ -1345,30 +1183,49 @@ IfcUtil::IfcBaseClass::set_attribute_value(const std::string& s, const T& t) {
//
#ifdef USE_MMAP
IfcFile::IfcFile(const std::string& fn, bool mmap) {
- IfcSpfStream s(fn, mmap);
- storage_ = impl::in_memory_file_storage{};
- std::get(storage_).read_from_stream(&s);
+ initialize(fn, mmap);
}
-#else
-IfcFile::IfcFile(const std::string& path, filetype ty, bool readonly)
- : schema_(nullptr)
- , max_id_(0)
- , _header(this)
-{
- // @todo allow for rocksdb from path
+
+bool IfcParse::IfcFile::initialize(const std::string& fn, bool mmap) {
+ std::unique_ptr s;
+ if (mmap) {
+ s = std::make_unique(fn, FileReader::mmap_tag{});
+ } else {
+ s = std::make_unique(fn);
+ }
+
+ storage_.emplace<1>(this);
+ std::get(storage_).read_from_stream(&*s, schema_, max_id_, types_to_bypass_loading_);
+
+ if ((good_ = std::get(storage_).good_)) {
+ // @todo unify these names, it's already confusing enough as it stands
+ byid_ = decltype(byid_)(&std::get(storage_).byid_);
+ byref_excl_ = decltype(byref_excl_)(&std::get(storage_).byref_excl_);
+ byguid_ = decltype(byguid_)(&std::get(storage_).byguid_);
+ }
+
+ ifcroot_type_ = schema_ ? schema_->declaration_by_name("IfcRoot") : nullptr;
+ return good_ == file_open_status::SUCCESS;
+}
+#endif
+
+IfcFile::IfcFile(const uninitialized_tag&)
+ : schema_(nullptr), max_id_(0), _header(this), good_(file_open_status::UNKNOWN), ifcroot_type_(nullptr) {}
+
+bool IfcParse::IfcFile::initialize(const std::string& path, filetype ty, bool readonly) {
if (ty == FT_AUTODETECT) {
ty = guess_file_type(path);
}
if (ty == FT_IFCSPF) {
- IfcSpfStream s(path);
+ FileReader s(path);
storage_.emplace<1>(this);
- std::get(storage_).read_from_stream(&s, schema_, max_id_);
+ std::get(storage_).read_from_stream(&s, schema_, max_id_, types_to_bypass_loading_);
- if (good_ = std::get(storage_).good_) {
+ if ((good_ = std::get(storage_).good_)) {
// @todo unify these names, it's already confusing enough as it stands
byid_ = decltype(byid_)(&std::get(storage_).byid_);
byref_excl_ = decltype(byref_excl_)(&std::get(storage_).byref_excl_);
- byguid_ = decltype(byguid_)(&std::get(storage_).byguid_);
+ byguid_ = decltype(byguid_)(&std::get(storage_).byguid_);
}
// byidentity_ = decltype(byidentity_)(&std::get(storage_).byidentity_);
} else if (ty == FT_ROCKSDB) {
@@ -1397,16 +1254,34 @@ IfcFile::IfcFile(const std::string& path, filetype ty, bool readonly)
// throw std::runtime_error("Unsupported file format");
}
ifcroot_type_ = schema_ ? schema_->declaration_by_name("IfcRoot") : nullptr;
+ return good_ == file_open_status::SUCCESS;
+}
+
+void IfcParse::IfcFile::bypass_type(const std::string& type_name) {
+ types_to_bypass_loading_.insert(type_name);
+}
+
+IfcFile::IfcFile(const std::string& path, filetype ty, bool readonly)
+ : schema_(nullptr)
+ , max_id_(0)
+ , _header(this)
+{
+ initialize(path, ty, readonly);
}
-#endif
IfcFile::IfcFile(std::istream& stream, int length)
: schema_(nullptr)
, max_id_(0)
{
- IfcSpfStream s(stream, length);
+ FileReader s(FileReader::caller_fed_tag{});
+
+ std::string string_data;
+ string_data.resize(length);
+ stream.read(string_data.data(), length);
+ s.pushNextPage(string_data);
+
storage_.emplace<1>(this);
- std::get(storage_).read_from_stream(&s, schema_, max_id_);
+ std::get(storage_).read_from_stream(&s, schema_, max_id_, types_to_bypass_loading_);
good_ = std::get(storage_).good_;
ifcroot_type_ = schema_ ? schema_->declaration_by_name("IfcRoot") : nullptr;
@@ -1419,9 +1294,10 @@ IfcFile::IfcFile(void* data, int length)
: schema_(nullptr)
, max_id_(0)
{
- IfcSpfStream s(data, length);
+ FileReader s(std::string((char*)data, length), FileReader::caller_fed_tag{});
+
storage_.emplace<1>(this);
- std::get(storage_).read_from_stream(&s, schema_, max_id_);
+ std::get(storage_).read_from_stream(&s, schema_, max_id_, types_to_bypass_loading_);
good_ = std::get(storage_).good_;
ifcroot_type_ = schema_ ? schema_->declaration_by_name("IfcRoot") : nullptr;
@@ -1430,12 +1306,12 @@ IfcFile::IfcFile(void* data, int length)
byguid_ = decltype(byguid_)(&std::get(storage_).byguid_);
}
-IfcFile::IfcFile(IfcParse::IfcSpfStream* s)
+IfcFile::IfcFile(IfcParse::FileReader* s)
: schema_(nullptr)
, max_id_(0)
{
storage_.emplace<1>(this);
- std::get(storage_).read_from_stream(s, schema_, max_id_);
+ std::get(storage_).read_from_stream(s, schema_, max_id_, types_to_bypass_loading_);
good_ = std::get(storage_).good_;
ifcroot_type_ = schema_ ? schema_->declaration_by_name("IfcRoot") : nullptr;
@@ -1475,18 +1351,94 @@ IfcFile::IfcFile(const IfcParse::schema_definition* schema, filetype ty, const s
setDefaultHeaderValues();
}
-IfcParse::InstanceStreamer::InstanceStreamer(const std::string& fn)
- : stream_(new IfcSpfStream(fn))
+bool IfcParse::InstanceStreamer::hasSemicolon() const {
+ auto local_stream = stream_->clone();
+ auto local_lexer = IfcSpfLexer(&local_stream);
+ Token t;
+ try {
+ t = local_lexer.Next();
+ } catch (const std::out_of_range&) {
+ return false;
+ }
+ while (t.type != Token_NONE) {
+ if (TokenFunc::isOperator(t, ';')) {
+ return true;
+ }
+ try {
+ t = local_lexer.Next();
+ } catch (const std::out_of_range&) {
+ // This most likely happens when a page boundary is contained within a string
+ break;
+ }
+ }
+ return false;
+}
+
+size_t IfcParse::InstanceStreamer::semicolonCount() const {
+ auto local_stream = stream_->clone();
+ auto local_lexer = IfcSpfLexer(&local_stream);
+ Token t;
+ size_t count = 0;
+ try {
+ t = local_lexer.Next();
+ } catch (const std::out_of_range&) {
+ return false;
+ }
+ while (t.type != Token_NONE) {
+ if (TokenFunc::isOperator(t, ';')) {
+ count++;
+ }
+ try {
+ t = local_lexer.Next();
+ } catch (const std::out_of_range&) {
+ // This most likely happens when a page boundary is contained within a string
+ break;
+ }
+ }
+ return count;
+}
+
+void IfcParse::InstanceStreamer::pushPage(const std::string& page)
+{
+ stream_->pushNextPage(page);
+ if (good_ == file_open_status::NO_HEADER) {
+ header_ = new IfcParse::IfcSpfHeader(lexer_);
+ if (header_->tryRead() && header_->file_schema()->schema_identifiers().size() == 1) {
+ try {
+ schema_ = IfcParse::schema_by_name(header_->file_schema()->schema_identifiers().front());
+ good_ = file_open_status::SUCCESS;
+ } catch (const IfcParse::IfcException&) {
+ }
+ }
+ storage_.file = nullptr;
+ storage_.schema = schema_;
+ storage_.tokens = lexer_;
+ storage_.references_to_resolve = &references_to_resolve_;
+ }
+}
+
+IfcParse::InstanceStreamer::InstanceStreamer()
+ : stream_(new FileReader(FileReader::caller_fed_tag{}))
+ , lexer_(new IfcSpfLexer(stream_))
+ , token_stream_(3, Token{})
+ , schema_(nullptr)
+ , progress_(0)
+{
+ init_locale();
+ good_ = file_open_status::NO_HEADER;
+}
+
+IfcParse::InstanceStreamer::InstanceStreamer(const std::string& fn, bool mmap)
+ : stream_(mmap ? new FileReader(fn, FileReader::mmap_tag{}) : new FileReader(fn))
, lexer_(new IfcSpfLexer(stream_))
, token_stream_(3, Token{})
, schema_(nullptr)
- , ifcroot_type_(nullptr)
, progress_(0)
{
init_locale();
good_ = file_open_status::NO_HEADER;
- if (*stream_) {
+ if (stream_->size() && !stream_->eof()) {
header_ = new IfcParse::IfcSpfHeader(lexer_);
if (header_->tryRead() && header_->file_schema()->schema_identifiers().size() == 1) {
try {
@@ -1503,17 +1455,16 @@ IfcParse::InstanceStreamer::InstanceStreamer(const std::string& fn)
}
IfcParse::InstanceStreamer::InstanceStreamer(void* data, int length)
- : stream_(new IfcSpfStream(data, length))
+ : stream_(new FileReader(std::string((char*) data, length), FileReader::caller_fed_tag{}))
, lexer_(new IfcSpfLexer(stream_))
, token_stream_(3, Token{})
, schema_(nullptr)
- , ifcroot_type_(nullptr)
, progress_(0)
{
init_locale();
good_ = file_open_status::NO_HEADER;
- if (*stream_) {
+ if (stream_->size() && !stream_->eof()) {
header_ = new IfcParse::IfcSpfHeader(lexer_);
if (header_->tryRead() && header_->file_schema()->schema_identifiers().size() == 1) {
try {
@@ -1535,7 +1486,6 @@ IfcParse::InstanceStreamer::InstanceStreamer(const IfcParse::schema_definition*
, header_(nullptr)
, token_stream_(3, Token{})
, schema_(schema)
- , ifcroot_type_(schema->declaration_by_name("IfcRoot"))
, progress_(0)
{
init_locale();
@@ -1546,14 +1496,14 @@ IfcParse::InstanceStreamer::InstanceStreamer(const IfcParse::schema_definition*
storage_.references_to_resolve = &references_to_resolve_;
}
-void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::IfcSpfStream* s, const IfcParse::schema_definition*& schema, unsigned int& max_id) {
+void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::FileReader* s, const IfcParse::schema_definition*& schema, unsigned int& max_id, const std::set& typed_to_bypass) {
// Initialize a "C" locale for locale-independent
// number parsing. See comment above on line 41.
init_locale();
tokens = nullptr;
- if (!s->valid) {
+ if (!s->size() || s->eof()) {
// @todo set good on parent file
good_ = file_open_status::READ_ERROR;
return;
@@ -1593,22 +1543,24 @@ void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::IfcSpfSt
auto ifcroot_type_ = schema->declaration_by_name("IfcRoot");
InstanceStreamer streamer(schema, tokens);
+ streamer.bypassTypes(typed_to_bypass);
Logger::Status("Scanning file...");
while (streamer) {
- auto inst = streamer.read_instance();
+ auto inst = streamer.readInstance();
if (!inst) {
// No more instances to read
break;
}
+
auto current_id = std::get<0>(*inst);
auto instance = schema->instantiate(std::get<1>(*inst), std::move(std::get<2>(*inst)));
instance->file_ = file;
- instance->id_ = current_id;
+ instance->id_ = (uint32_t) current_id;
if (instance->declaration().is(*ifcroot_type_)) {
try {
@@ -1641,7 +1593,7 @@ void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::IfcSpfSt
}
// byidentity_[instance->identity()] = instance;
- byid_.insert({ current_id, instance });
+ byid_.insert({(uint32_t) current_id, instance });
// @nb cannot assign to byid_;
// byid_[current_id] = instance;
@@ -1651,6 +1603,9 @@ void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::IfcSpfSt
good_ = streamer.status();
byref_excl_ = streamer.inverses();
+
+ // Move the storage of simple type instances so that they are retained during the lifetime of the file
+ read_simple_type_instances = streamer.stealInstances();
Logger::Status("\rDone scanning file ");
@@ -1660,11 +1615,16 @@ void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::IfcSpfSt
return;
}
+ const auto& bypassed = streamer.bypassed_instances();
+
for (const auto& p : streamer.references()) {
const auto& ref = p.first.name_;
const auto& refattr = p.first.index_;
if (auto* v = std::get_if(&p.second)) {
if (auto* name = std::get_if(v)) {
+ if (std::binary_search(bypassed.begin(), bypassed.end(), *name)) {
+ continue;
+ }
auto it = byid_.find(*name);
if (it == byid_.end()) {
Logger::Error("Instance reference #" + std::to_string(*name) + " used by instance #" + std::to_string(ref) + " at attribute index " + std::to_string(refattr) + " not found at offset " + std::to_string(name->file_offset));
@@ -1690,11 +1650,14 @@ void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::IfcSpfSt
} else if (auto* inst = std::get_if(v)) {
byid_[p.first.name_]->data().set_attribute_value(nullptr, nullptr, 0, p.first.index_, *inst);
}
- } else if (auto* v = std::get_if>(&p.second)) {
+ } else if (auto* vv = std::get_if>(&p.second)) {
aggregate_of_instance::ptr instances(new aggregate_of_instance);
- instances->reserve(v->size());
- for (const auto& vi : *v) {
+ instances->reserve(vv->size());
+ for (const auto& vi : *vv) {
if (auto* name = std::get_if(&vi)) {
+ if (std::binary_search(bypassed.begin(), bypassed.end(), *name)) {
+ continue;
+ }
auto it = byid_.find(*name);
if (it == byid_.end()) {
Logger::Error("Instance reference #" + std::to_string(*name) + " used by instance #" + std::to_string(ref) + " at attribute index " + std::to_string(refattr) + " not found at offset " + std::to_string(name->file_offset));
@@ -1723,12 +1686,15 @@ void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::IfcSpfSt
} else {
Logger::Error("Duplicate definition for instance reference");
}
- } else if (auto* v = std::get_if>>(&p.second)) {
+ } else if (auto* vvv = std::get_if>>(&p.second)) {
aggregate_of_aggregate_of_instance::ptr instances(new aggregate_of_aggregate_of_instance);
- for (const auto& vi : *v) {
+ for (const auto& vi : *vvv) {
std::vector inner;
for (const auto& vii : vi) {
if (auto* name = std::get_if(&vii)) {
+ if (std::binary_search(bypassed.begin(), bypassed.end(), *name)) {
+ continue;
+ }
auto it = byid_.find(*name);
if (it == byid_.end()) {
Logger::Error("Instance reference #" + std::to_string(*name) + " used by instance #" + std::to_string(ref) + " at attribute index " + std::to_string(refattr) + " not found at offset " + std::to_string(name->file_offset));
@@ -2000,15 +1966,15 @@ IfcUtil::IfcBaseClass* IfcFile::addEntity(IfcUtil::IfcBaseClass* entity, int id)
auto attr = entity->get_attribute_value(i);
IfcUtil::ArgumentType attr_type = attr.type();
- IfcParse::declaration* decl = 0;
+ IfcParse::declaration* potentially_length_measure_decl = 0;
if (entity->declaration().as_entity() != nullptr) {
- decl = 0;
+ potentially_length_measure_decl = 0;
const parameter_type* pt = entity->declaration().as_entity()->attribute_by_index(i)->type_of_attribute();
while (pt->as_aggregation_type() != nullptr) {
pt = pt->as_aggregation_type()->type_of_element();
}
if (pt->as_named_type() != nullptr) {
- decl = pt->as_named_type()->declared_type();
+ potentially_length_measure_decl = pt->as_named_type()->declared_type();
}
}
@@ -2047,7 +2013,7 @@ IfcUtil::IfcBaseClass* IfcFile::addEntity(IfcUtil::IfcBaseClass* entity, int id)
}
new_entity->set_attribute_value(i, new_instances);
- } else if ((decl != nullptr) && decl->is(*schema()->declaration_by_name("IfcLengthMeasure"))) {
+ } else if ((potentially_length_measure_decl != nullptr) && potentially_length_measure_decl->is(*schema()->declaration_by_name("IfcLengthMeasure"))) {
if (boost::math::isnan(conversion_factor)) {
std::pair this_file_unit = {nullptr, 1.0};
std::pair other_file_unit = {nullptr, 1.0};
@@ -2280,8 +2246,6 @@ void IfcFile::process_deletion_(IfcUtil::IfcBaseClass* entity) {
byid_.erase(entity->id());
- const IfcParse::declaration* ty = &entity->declaration();
-
remove_type_ref(entity);
// entity_file_map is in place to prevent duplicate definitions with usage of add().
@@ -2888,7 +2852,7 @@ AttributeValue IfcEntityInstanceData::get_attribute_value(void* storage, const I
if (storage_) {
return AttributeValue(storage_, (uint8_t)index);
} else {
- return AttributeValue((IfcParse::impl::rocks_db_file_storage*)storage, identity, decl, index);
+ return AttributeValue((IfcParse::impl::rocks_db_file_storage*)storage, identity, decl, (uint8_t) index);
}
}
diff --git a/src/ifcparse/IfcParse.h b/src/ifcparse/IfcParse.h
index 9430866ae7..a034fde218 100644
--- a/src/ifcparse/IfcParse.h
+++ b/src/ifcparse/IfcParse.h
@@ -32,7 +32,7 @@
#include "ifc_parse_api.h"
#include "IfcBaseClass.h"
#include "IfcCharacterDecoder.h"
-#include "IfcSpfStream.h"
+#include "FileReader.h"
#include "macros.h"
#include "storage.h"
@@ -103,27 +103,27 @@ class IFC_PARSE_API TokenFunc {
// Functions for creating Tokens from an arbitary file offset
// The first 4 bits are reserved for Tokens of type ()=,;$*
//
-Token OperatorTokenPtr(IfcSpfLexer* tokens, unsigned start, unsigned end);
-Token GeneralTokenPtr(IfcSpfLexer* tokens, unsigned start, unsigned end);
+Token OperatorTokenPtr(IfcSpfLexer* tokens, size_t start, char data);
+Token GeneralTokenPtr(IfcSpfLexer* tokens, size_t start, const std::string& data);
-/// A stream of tokens to be read from a IfcSpfStream.
+/// A stream of tokens to be read from a FileReader.
class IFC_PARSE_API IfcSpfLexer {
private:
IfcCharacterDecoder* decoder_;
- unsigned int skipWhitespace() const;
- unsigned int skipComment() const;
+ size_t skipWhitespace() const;
+ size_t skipComment() const;
public:
std::string& GetTempString() const {
static my_thread_local std::string string;
return string;
}
- IfcSpfStream* stream;
+ FileReader* stream;
// IfcFile* file;
- IfcSpfLexer(IfcSpfStream* stream);
+ IfcSpfLexer(FileReader* stream);
Token Next();
~IfcSpfLexer();
- void TokenString(unsigned int offset, std::string& result);
+ void TokenString(size_t offset, std::string& result);
};
IFC_PARSE_API aggregate_of_instance::ptr traverse(IfcUtil::IfcBaseClass* instance, int max_level = -1);
diff --git a/src/ifcparse/IfcSpfHeader.cpp b/src/ifcparse/IfcSpfHeader.cpp
index 7e264631d6..2b98adc4df 100644
--- a/src/ifcparse/IfcSpfHeader.cpp
+++ b/src/ifcparse/IfcSpfHeader.cpp
@@ -30,12 +30,12 @@ static const char* const DATA = "DATA";
using namespace IfcParse;
namespace {
- IfcEntityInstanceData read_from_spf_file(IfcParse::impl::in_memory_file_storage* storage, size_t s) {
+ IfcEntityInstanceData read_from_spf_file(IfcParse::impl::in_memory_file_storage* storage, const IfcParse::entity* decl) {
if (storage != nullptr) {
parse_context pc;
storage->tokens->Next();
storage->load(-1, nullptr, pc, -1);
- return pc.construct(-1, *storage->references_to_resolve, nullptr, s, -1);
+ return pc.construct(boost::none, *storage->references_to_resolve, decl, decl->as_entity()->attribute_count(), -1);
} else {
// std::unreachable();
return IfcEntityInstanceData(in_memory_attribute_storage(10));
@@ -153,19 +153,19 @@ void IfcSpfHeader::read() {
readTerminal(Header_section_schema::file_description::Class().name_uc(), NONE);
delete file_description_;
- file_description_ = new Header_section_schema::file_description(read_from_spf_file(storage_, Header_section_schema::file_description::Class().attribute_count()));
+ file_description_ = new Header_section_schema::file_description(read_from_spf_file(storage_, &Header_section_schema::file_description::Class()));
file_description_->file_ = file_;
readSemicolon();
readTerminal(Header_section_schema::file_name::Class().name_uc(), NONE);
delete file_name_;
- file_name_ = new Header_section_schema::file_name(read_from_spf_file(storage_, Header_section_schema::file_name::Class().attribute_count()));
+ file_name_ = new Header_section_schema::file_name(read_from_spf_file(storage_, &Header_section_schema::file_name::Class()));
file_name_->file_ = file_;
readSemicolon();
readTerminal(Header_section_schema::file_schema::Class().name_uc(), NONE);
delete file_schema_;
- file_schema_ = new Header_section_schema::file_schema(read_from_spf_file(storage_, Header_section_schema::file_schema::Class().attribute_count()));
+ file_schema_ = new Header_section_schema::file_schema(read_from_spf_file(storage_, &Header_section_schema::file_schema::Class()));
file_schema_->file_ = file_;
readSemicolon();
}
diff --git a/src/ifcparse/IfcSpfStream.h b/src/ifcparse/IfcSpfStream.h
deleted file mode 100644
index 7d42a9cd52..0000000000
--- a/src/ifcparse/IfcSpfStream.h
+++ /dev/null
@@ -1,84 +0,0 @@
-/********************************************************************************
- * *
- * 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 . *
- * *
- ********************************************************************************/
-
-/*********************************************************************************
- * *
- * Reads a file and provides functions to access its *
- * contents randomly and character by character *
- * *
- ********************************************************************************/
-
-#ifndef IFCSPFSTREAM_H
-#define IFCSPFSTREAM_H
-
-#include "ifc_parse_api.h"
-
-#include
-
-#ifdef USE_MMAP
-#include
-#endif
-
-namespace IfcParse {
-/// The IfcSpfStream class represents a ISO 10303-21 IFC-SPF file in memory.
-/// The file is interpreted as a sequence of tokens which are lazily
-/// interpreted only when requested.
-class IFC_PARSE_API IfcSpfStream {
- private:
-#ifdef USE_MMAP
- boost::iostreams::mapped_file_source mfs;
-#endif
- FILE* stream_;
- const char* buffer_;
- unsigned int ptr_;
- unsigned int len_;
-
- public:
- bool valid;
- bool eof;
- unsigned int size;
-#ifdef USE_MMAP
- IfcSpfStream(const std::string& path, bool mmap = false);
-#else
- IfcSpfStream(const std::string& path);
-#endif
- IfcSpfStream(std::istream& stream, int length);
- IfcSpfStream(void* data, int length);
- ~IfcSpfStream();
- /// Returns the character at the cursor
- char Peek();
- /// Returns the character at specified offset
- char Read(unsigned int offset);
- /// Increment the file cursor and reads new page if necessary
- void Inc();
- void Close();
- /// Moves the file cursor to an arbitrary offset in the file
- void Seek(unsigned int offset);
- /// Returns the cursor position
- unsigned int Tell() const;
-
- bool is_eof_at(unsigned int) const;
- void increment_at(unsigned int&);
- char peek_at(unsigned int);
-
- operator bool() const { return valid && !eof; }
-};
-} // namespace IfcParse
-
-#endif
diff --git a/src/ifcparse/IfcUtil.cpp b/src/ifcparse/IfcUtil.cpp
index 7aab44d391..a47083262f 100644
--- a/src/ifcparse/IfcUtil.cpp
+++ b/src/ifcparse/IfcUtil.cpp
@@ -76,8 +76,8 @@ void aggregate_of_instance::push(const aggregate_of_instance::ptr& instance) {
}
}
}
-unsigned int aggregate_of_instance::size() const { return (unsigned int)list_.size(); }
-void aggregate_of_instance::reserve(unsigned capacity) { list_.reserve((size_t)capacity); }
+size_t aggregate_of_instance::size() const { return list_.size(); }
+void aggregate_of_instance::reserve(size_t capacity) { list_.reserve(capacity); }
aggregate_of_instance::it aggregate_of_instance::begin() { return list_.begin(); }
aggregate_of_instance::it aggregate_of_instance::end() { return list_.end(); }
IfcUtil::IfcBaseClass* aggregate_of_instance::operator[](int i) {
diff --git a/src/ifcparse/aggregate_of_instance.h b/src/ifcparse/aggregate_of_instance.h
index aff12f426a..bcce94ce6e 100644
--- a/src/ifcparse/aggregate_of_instance.h
+++ b/src/ifcparse/aggregate_of_instance.h
@@ -48,8 +48,8 @@ class IFC_PARSE_API aggregate_of_instance {
it begin();
it end();
IfcUtil::IfcBaseClass* operator[](int index);
- unsigned int size() const;
- void reserve(unsigned capacity);
+ size_t size() const;
+ void reserve(size_t capacity);
bool contains(IfcUtil::IfcBaseClass*) const;
template
@@ -81,7 +81,7 @@ class aggregate_of {
}
it begin() { return list_.begin(); }
it end() { return list_.end(); }
- unsigned int size() const { return (unsigned int)list_.size(); }
+ size_t size() const { return list_.size(); }
aggregate_of_instance::ptr generalize() {
aggregate_of_instance::ptr result(new aggregate_of_instance());
for (it i = begin(); i != end(); ++i) {
diff --git a/src/ifcparse/rocksdb_map_adapter.h b/src/ifcparse/rocksdb_map_adapter.h
index a6254356c6..da2346789f 100644
--- a/src/ifcparse/rocksdb_map_adapter.h
+++ b/src/ifcparse/rocksdb_map_adapter.h
@@ -274,7 +274,7 @@ public:
}
iterator& operator++() {
-#if IFOPSH_WITH_ROCKSDB
+#ifdef IFOPSH_WITH_ROCKSDB
if (it_) {
it_->Next();
check_valid();
diff --git a/src/ifcparse/set_to_map_transformer.h b/src/ifcparse/set_to_map_transformer.h
index 21bf597f4f..eccbdfa766 100644
--- a/src/ifcparse/set_to_map_transformer.h
+++ b/src/ifcparse/set_to_map_transformer.h
@@ -106,7 +106,7 @@ public:
return iterator(base_map_->find(k), &transform_);
}
- size_t erase(const key_type& k) {
+ size_t erase(const key_type&) {
// @todo
return 0;
}
diff --git a/src/ifcparse/storage.h b/src/ifcparse/storage.h
index d0ebf4320c..60378c982e 100644
--- a/src/ifcparse/storage.h
+++ b/src/ifcparse/storage.h
@@ -125,7 +125,7 @@ namespace IfcParse {
class IfcFile;
class IfcSpfLexer;
- class IfcSpfStream;
+ class FileReader;
enum TokenType {
Token_NONE,
@@ -142,7 +142,7 @@ namespace IfcParse {
struct Token {
IfcSpfLexer* lexer; //TODO: remove it from here
- unsigned startPos;
+ size_t startPos;
TokenType type;
union {
char value_char; //types: OPERATOR
@@ -154,7 +154,7 @@ namespace IfcParse {
startPos(0),
type(Token_NONE) {
}
- Token(IfcSpfLexer* _lexer, unsigned _startPos, unsigned /*_endPos*/, TokenType _type)
+ Token(IfcSpfLexer* _lexer, size_t _startPos, TokenType _type)
: lexer(_lexer),
startPos(_startPos),
type(_type) {
@@ -184,15 +184,18 @@ namespace IfcParse {
void push(IfcUtil::IfcBaseClass* inst);
- IfcEntityInstanceData construct(int name, unresolved_references& references_to_resolve, const IfcParse::declaration* decl, boost::optional expected_size, int resolve_reference_index, bool coerce_attribute_count=true);
+ IfcEntityInstanceData construct(boost::optional name, unresolved_references& references_to_resolve, const IfcParse::declaration* decl, boost::optional expected_size, int resolve_reference_index, bool coerce_attribute_count=true);
};
namespace impl {
struct IFC_PARSE_API in_memory_file_storage {
- std::vector read_simple_type_instances;
+ std::vector> read_simple_type_instances;
+ std::vector> steal_instances() {
+ return std::move(read_simple_type_instances);
+ }
IfcParse::IfcSpfLexer* tokens;
- // IfcParse::IfcSpfStream* stream;
+ // IfcParse::FileReader* stream;
// Either one of these needs to be set
IfcParse::IfcFile* file;
@@ -214,14 +217,10 @@ namespace IfcParse {
typedef entity_instance_by_name_t::iterator iterator;
in_memory_file_storage(IfcParse::IfcFile* f = nullptr) : tokens(nullptr), file(f), schema(nullptr) {}
- ~in_memory_file_storage() {
- for (auto* inst : read_simple_type_instances) {
- delete inst;
- }
- }
in_memory_file_storage(const in_memory_file_storage&) = delete;
in_memory_file_storage(const in_memory_file_storage&&) = delete;
+
class type_iterator : public entities_by_type_t::const_iterator {
public:
using iterator_category = std::forward_iterator_tag;
@@ -261,7 +260,7 @@ namespace IfcParse {
entities_by_ref_t byref_excl_;
entity_instance_by_guid_t byguid_;
- void load(unsigned entity_instance_name, const IfcParse::entity* entity, parse_context&, int attribute_index = -1);
+ void load(boost::optional entity_instance_name, const IfcParse::entity* entity, parse_context&, int attribute_index = -1);
void try_read_semicolon() const;
void register_inverse(unsigned, const IfcParse::entity* from_entity, int inst_id, int attribute_index);
@@ -269,7 +268,7 @@ namespace IfcParse {
// @todo is this still used
IfcEntityInstanceData read(unsigned int index);
- void read_from_stream(IfcParse::IfcSpfStream* stream, const IfcParse::schema_definition*& schema, unsigned int& max_id);
+ void read_from_stream(IfcParse::FileReader* stream, const IfcParse::schema_definition*& schema, unsigned int& max_id, const std::set& typed_to_bypass);
file_open_status good_ = file_open_status::SUCCESS;
diff --git a/src/ifcpatch/ifcpatch/recipes/MergeStyles.py b/src/ifcpatch/ifcpatch/recipes/MergeStyles.py
index cb2780a360..5c1fa2f4fa 100644
--- a/src/ifcpatch/ifcpatch/recipes/MergeStyles.py
+++ b/src/ifcpatch/ifcpatch/recipes/MergeStyles.py
@@ -44,11 +44,12 @@ class Patcher(ifcpatch.BasePatcher):
uniques = {}
i = 0
for element in self.file.by_type(ifc_class):
- data = "-".join([str(a) for a in element])
- if unique := uniques.get(data, None):
+ ifc_class = element.is_a()
+ key = "-".join([str(a) for a in element])
+ if unique := uniques.get(ifc_class, {}).get(key, None):
ifcopenshell.util.element.replace_element(element, unique)
self.file.remove(element)
i += 1
else:
- uniques[data] = element
+ uniques.setdefault(ifc_class, {})[key] = element
print(f"Replaced {i} {ifc_class}")
diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py
index 97c24c59a1..6b5536299d 100644
--- a/src/ifctester/ifctester/facet.py
+++ b/src/ifctester/ifctester/facet.py
@@ -206,6 +206,12 @@ class Entity(Facet):
except:
# If the user has specified a class that doesn't exist in the version
results = []
+ if not self.name.endswith("TYPE"):
+ try:
+ for element_type in ifc_file.by_type(f"{self.name}Type"):
+ results.extend(ifcopenshell.util.element.get_types(element_type))
+ except:
+ pass
else:
results = []
ifc_classes = [t for t in ifc_file.wrapped_data.types() if t.upper() == self.name]
@@ -223,12 +229,25 @@ class Entity(Facet):
is_pass = inst.is_a().upper() == self.name
reason = None
- if not is_pass:
+ if (
+ not is_pass
+ and inst.file.schema == "IFC2X3"
+ and not self.name.endswith("TYPE")
+ and (element_type := ifcopenshell.util.element.get_type(inst))
+ ):
+ is_pass = element_type.is_a().upper() == f"{self.name}TYPE"
+ reason = {"type": "NAME", "actual": element_type.is_a().upper()[:-4]}
+ elif not is_pass:
reason = {"type": "NAME", "actual": inst.is_a().upper()}
if is_pass and self.predefinedType:
- predefined_type = ifcopenshell.util.element.get_predefined_type(inst)
- is_pass = predefined_type == self.predefinedType
+ if self.predefinedType == "USERDEFINED":
+ is_pass = ifcopenshell.util.element.is_userdefined_type(inst)
+ if not is_pass:
+ predefined_type = ifcopenshell.util.element.get_predefined_type(inst)
+ else:
+ predefined_type = ifcopenshell.util.element.get_predefined_type(inst)
+ is_pass = predefined_type == self.predefinedType
if not is_pass:
reason = {"type": "PREDEFINEDTYPE", "actual": predefined_type}
diff --git a/src/ifctester/test/test_facet.py b/src/ifctester/test/test_facet.py
index 10d94b2f03..167201979b 100644
--- a/src/ifctester/test/test_facet.py
+++ b/src/ifctester/test/test_facet.py
@@ -172,10 +172,10 @@ class TestEntity:
facet = Entity(name="IFCWALL", predefinedType="USERDEFINED")
ifc = ifcopenshell.file()
run(
- "A predefined type must always specify a meaningful type, not USERDEFINED itself",
+ "A predefined type may specify USERDEFINED itself",
facet=facet,
inst=ifc.createIfcWall(PredefinedType="USERDEFINED", ObjectType="WALDO"),
- expected=False,
+ expected=True,
)
ifc = ifcopenshell.file()
@@ -230,6 +230,32 @@ class TestEntity:
wall3 = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWall", predefined_type="BAZFOO")
run("Restrictions an be specified for the predefined type 3/3", facet=facet, inst=wall3, expected=False)
+ def test_ifc2x3_occurrence_type_mapping(self):
+ set_facet("entity")
+
+ ifc = ifcopenshell.file(schema="IFC2X3")
+ application = ifcopenshell.api.owner.add_application(ifc)
+ person = ifcopenshell.api.owner.add_person(
+ ifc, identification="LPARTEE", family_name="Partee", given_name="Leeable"
+ )
+ organisation = ifcopenshell.api.owner.add_organisation(
+ ifc, identification="AWB", name="Architects Without Ballpens"
+ )
+ user = ifcopenshell.api.owner.add_person_and_organisation(ifc, person=person, organisation=organisation)
+ ifcopenshell.api.owner.settings.get_user = lambda x: user
+ ifcopenshell.api.owner.settings.get_application = lambda x: application
+
+ element = ifcopenshell.api.root.create_entity(ifc, "IfcFlowTerminal")
+ element_type = ifcopenshell.api.root.create_entity(ifc, "IfcAirTerminalType")
+ ifcopenshell.api.type.assign_type(ifc, related_objects=[element], relating_type=element_type)
+ facet = Entity(name="IFCAIRTERMINAL")
+ assert facet.filter(ifc) == [element]
+ run("In IFC2X3 the type class is checked instead 1/2", facet=facet, inst=element, expected=True)
+
+ facet = Entity(name="IFCELECTRICAPPLIANCE")
+ assert facet.filter(ifc) == []
+ run("In IFC2X3 the type class is checked instead 2/2", facet=facet, inst=element, expected=False)
+
def test_to_string_required_applicability(self):
spec = ifctester.ids.Specification(name="Foo", minOccurs=1, maxOccurs="unbounded")
facet = Entity(name="IFCWALL")
diff --git a/src/ifcwrap/IfcParseWrapper.i b/src/ifcwrap/IfcParseWrapper.i
index 45a617916e..858705ebf0 100644
--- a/src/ifcwrap/IfcParseWrapper.i
+++ b/src/ifcwrap/IfcParseWrapper.i
@@ -36,7 +36,8 @@ private:
%ignore IfcParse::InstanceStreamer::InstanceStreamer(const IfcParse::schema_definition* schema, IfcParse::IfcSpfLexer* lexer);
-%ignore IfcParse::InstanceStreamer::read_instance;
+%ignore IfcParse::InstanceStreamer::readInstance;
+%ignore IfcParse::InstanceStreamer::stealInstances;
%ignore in_memory_file_storage;
%ignore rocks_db_file_storage;
@@ -650,6 +651,8 @@ private:
%extend IfcParse::IfcSpfHeader {
// Cast to base class pointers for SWIG, because
// it has no idea about the schema definitions.
+ // The code to access these methods as attributes
+ // is in file.py
IfcUtil::IfcBaseClass* file_description_py() {
return $self->file_description();
}
@@ -659,14 +662,6 @@ private:
IfcUtil::IfcBaseClass* file_schema_py() {
return $self->file_schema();
}
-
- %pythoncode %{
- # Hide the getters with read-only property implementations
- # self.file_ref is set in ifcopenshell.file.header()
- file_description = property(lambda self: self.file_ref(self.file_description_py()))
- file_name = property(lambda self: self.file_ref(self.file_name_py()))
- file_schema = property(lambda self: self.file_ref(self.file_schema_py()))
- %}
};
%extend IfcParse::FileDescription {
@@ -1002,7 +997,7 @@ private:
%}
%extend IfcParse::InstanceStreamer {
- PyObject* read_instance_py() {
+ PyObject* readInstancePy(bool type_as_declaration_instance=false) {
auto simply_type_to_dictionary = [&](IfcUtil::IfcBaseClass* t) -> PyObject* {
const auto& nm = t->declaration().name();
auto ifc_val = t->get_attribute_value(0);
@@ -1070,7 +1065,7 @@ private:
Py_INCREF(Py_None);
return Py_None;
}
- auto inst = self->read_instance();
+ auto inst = self->readInstance();
if (!inst) {
Py_INCREF(Py_None);
return Py_None;
@@ -1088,7 +1083,13 @@ private:
{
const std::string& key_cpp = "type";
auto name_py = pythonize(key_cpp);
- auto value_py = pythonize(std::get<1>(*inst)->name());
+ PyObject* value_py;
+ if (type_as_declaration_instance) {
+ // @todo should this just be the default behavior?
+ value_py = pythonize(std::get<1>(*inst));
+ } else {
+ value_py = pythonize(std::get<1>(*inst)->name());
+ }
PyDict_SetItem(d, name_py, value_py);
Py_DECREF(name_py);
Py_DECREF(value_py);
diff --git a/src/serializers/RocksDbSerializer.cpp b/src/serializers/RocksDbSerializer.cpp
index 014b519807..f5e95e7a3b 100644
--- a/src/serializers/RocksDbSerializer.cpp
+++ b/src/serializers/RocksDbSerializer.cpp
@@ -135,7 +135,7 @@ void RocksDbSerializer::write_streaming_() {
streamer.coerce_attribute_count = false;
while (streamer) {
- auto inst = streamer.read_instance();
+ auto inst = streamer.readInstance();
if (inst) {
// name can be zero in case of header instances
auto name = std::get<0>(*inst);
@@ -259,7 +259,8 @@ void RocksDbSerializer::write_streaming_() {
}
// @nb we also need to delete them
- delete inst;
+ // not anymore, as they are now registered as unique_ptr in the in_memory_file_storage
+ // delete inst;
}
// Entity type as numeric ref to index_in_schema
diff --git a/win/build-deps.cmd b/win/build-deps.cmd
index 037e090d47..3c3a174a5f 100644
--- a/win/build-deps.cmd
+++ b/win/build-deps.cmd
@@ -35,7 +35,7 @@ if NOT "%CD%" == "%batpath%" (
set PROJECT_NAME=IfcOpenShell
-call utils\cecho.cmd 15 0 "This script fetches and builds all %PROJECT_NAME% dependencies"
+call utils\cecho.cmd 0 15 "This script fetches and builds all %PROJECT_NAME% dependencies"
echo.
:: Enable the delayed environment variable expansion needed in vs-cfg.cmd.
diff --git a/win/utils/cecho.cmd b/win/utils/cecho.cmd
index 2923fcb986..f0eb1b3749 100644
--- a/win/utils/cecho.cmd
+++ b/win/utils/cecho.cmd
@@ -1,4 +1,42 @@
-:: Usage: call cecho.cmd background foreground "message here"
-:: NOTES/TODOS 1) This is super slow 2) leading spaces are omitted 3) printing string with quotes doesn't work (quotes must be escaped awkwardly)
-@powershell -command write-host -background %~1 -foreground "%~2" "%3"
-@exit /b
+@echo off
+setlocal ENABLEDELAYEDEXPANSION
+
+rem ==========================================================
+rem cecho.cmd - Fast ANSI color echo for Windows 10+
+rem Emulates PowerShell escape codes like `t and `n
+rem Usage: call cecho.cmd "Message"
+rem ==========================================================
+
+for /F %%A in ('echo prompt $E ^| cmd') do set "ESC=%%A"
+
+set "BG=%~1"
+set "FG=%~2"
+set "TEXT=%~3"
+
+rem Replace PowerShell-style escapes with real characters
+set "TEXT=%TEXT:`t= %"
+set "TEXT=%TEXT:`"="%"
+
+rem Foreground color table (0–15)
+set "FGCOLOR[0]=30" & set "FGCOLOR[1]=34" & set "FGCOLOR[2]=32" & set "FGCOLOR[3]=36"
+set "FGCOLOR[4]=31" & set "FGCOLOR[5]=35" & set "FGCOLOR[6]=33" & set "FGCOLOR[7]=37"
+set "FGCOLOR[8]=90" & set "FGCOLOR[9]=94" & set "FGCOLOR[10]=92" & set "FGCOLOR[11]=96"
+set "FGCOLOR[12]=91" & set "FGCOLOR[13]=95" & set "FGCOLOR[14]=93" & set "FGCOLOR[15]=97"
+
+rem Background color table (0–7)
+set "BGCOLOR[0]=40" & set "BGCOLOR[1]=44" & set "BGCOLOR[2]=42" & set "BGCOLOR[3]=46"
+set "BGCOLOR[4]=41" & set "BGCOLOR[5]=45" & set "BGCOLOR[6]=43" & set "BGCOLOR[7]=47"
+
+set "FGC=!FGCOLOR[%FG%]!"
+set "BGC=!BGCOLOR[%BG%]!"
+
+if defined BGC (
+ set "STYLE=!ESC![!BGC!;!FGC!m"
+) else (
+ set "STYLE=!ESC![!FGC!m"
+)
+
+echo|set /p="!STYLE!!TEXT!!ESC![0m"
+echo.
+endlocal
+exit /b 0