diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 6211ac53ae..823780ccf2 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -16,7 +16,7 @@ jobs: - run: echo ok go build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest needs: activate steps: - uses: actions/checkout@v2 @@ -80,7 +80,7 @@ jobs: path: build/assets/Ifc* deliver: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest needs: build name: Docker Build, Tag, Push diff --git a/.github/workflows/ci-bsdd-pypi.yaml b/.github/workflows/ci-bsdd-pypi.yaml new file mode 100644 index 0000000000..e1ccf133b0 --- /dev/null +++ b/.github/workflows/ci-bsdd-pypi.yaml @@ -0,0 +1,48 @@ +name: ci-bsdd-pypi + +on: + schedule: + # ┌───────────── minute (0 - 59) + # │ ┌───────────── hour (0 - 23) + # │ │ ┌───────────── day of the month (1 - 31) + # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) + # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) + # * * * * * + - cron: "0 0 18 * *" + workflow_dispatch: + +env: + major: 0 + minor: 0 + name: ifcopenshell + +jobs: + activate: + runs-on: ubuntu-latest + if: | + github.repository == 'IfcOpenShell/IfcOpenShell' + steps: + - name: Set env + run: echo ok go + + build: + needs: activate + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 # https://github.com/actions/setup-python + with: + python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax + - name: Compile + run: | + pip install build + cd src/bsdd && + make dist + - name: Publish a Python distribution to PyPI + uses: ortega2247/pypi-upload-action@master + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + packages_dir: src/bsdd/dist diff --git a/.github/workflows/ci-ifcclash-pypi.yaml b/.github/workflows/ci-ifcclash-pypi.yaml new file mode 100644 index 0000000000..9aa0349d0b --- /dev/null +++ b/.github/workflows/ci-ifcclash-pypi.yaml @@ -0,0 +1,48 @@ +name: ci-ifcclash-pypi + +on: + schedule: + # ┌───────────── minute (0 - 59) + # │ ┌───────────── hour (0 - 23) + # │ │ ┌───────────── day of the month (1 - 31) + # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) + # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) + # * * * * * + - cron: "0 0 18 * *" + workflow_dispatch: + +env: + major: 0 + minor: 0 + name: ifcopenshell + +jobs: + activate: + runs-on: ubuntu-latest + if: | + github.repository == 'IfcOpenShell/IfcOpenShell' + steps: + - name: Set env + run: echo ok go + + build: + needs: activate + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 # https://github.com/actions/setup-python + with: + python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax + - name: Compile + run: | + pip install build + cd src/ifcclash && + make dist + - name: Publish a Python distribution to PyPI + uses: ortega2247/pypi-upload-action@master + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + packages_dir: src/ifcclash/dist diff --git a/.github/workflows/ci-ifccsv-pypi.yaml b/.github/workflows/ci-ifccsv-pypi.yaml new file mode 100644 index 0000000000..c60fa790fe --- /dev/null +++ b/.github/workflows/ci-ifccsv-pypi.yaml @@ -0,0 +1,48 @@ +name: ci-ifccsv-pypi + +on: + schedule: + # ┌───────────── minute (0 - 59) + # │ ┌───────────── hour (0 - 23) + # │ │ ┌───────────── day of the month (1 - 31) + # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) + # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) + # * * * * * + - cron: "0 0 18 * *" + workflow_dispatch: + +env: + major: 0 + minor: 0 + name: ifcopenshell + +jobs: + activate: + runs-on: ubuntu-latest + if: | + github.repository == 'IfcOpenShell/IfcOpenShell' + steps: + - name: Set env + run: echo ok go + + build: + needs: activate + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 # https://github.com/actions/setup-python + with: + python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax + - name: Compile + run: | + pip install build + cd src/ifccsv && + make dist + - name: Publish a Python distribution to PyPI + uses: ortega2247/pypi-upload-action@master + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + packages_dir: src/ifccsv/dist diff --git a/.github/workflows/ci-ifcopenshell-pypi.yml b/.github/workflows/ci-ifcopenshell-pypi.yml index 0d0bf10fcd..5ee327e15c 100644 --- a/.github/workflows/ci-ifcopenshell-pypi.yml +++ b/.github/workflows/ci-ifcopenshell-pypi.yml @@ -1,6 +1,7 @@ name: ci-ifcopenshell-pypi on: + workflow_dispatch: schedule: # ┌───────────── minute (0 - 59) # │ ┌───────────── hour (0 - 23) @@ -35,7 +36,7 @@ jobs: strategy: fail-fast: false matrix: - pyver: [py36, py37, py38, py39, py310, py311] + pyver: [py39, py310, py311, py312] config: - { name: "Windows Build", @@ -57,7 +58,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 # https://github.com/actions/setup-python with: - python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax + python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified - run: echo ${{ env.DATE }} - name: Get current date diff --git a/.github/workflows/ci-ifcpatch-pypi.yaml b/.github/workflows/ci-ifcpatch-pypi.yaml new file mode 100644 index 0000000000..710c3fba74 --- /dev/null +++ b/.github/workflows/ci-ifcpatch-pypi.yaml @@ -0,0 +1,48 @@ +name: ci-ifcpatch-pypi + +on: + schedule: + # ┌───────────── minute (0 - 59) + # │ ┌───────────── hour (0 - 23) + # │ │ ┌───────────── day of the month (1 - 31) + # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) + # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) + # * * * * * + - cron: "0 0 18 * *" + workflow_dispatch: + +env: + major: 0 + minor: 0 + name: ifcopenshell + +jobs: + activate: + runs-on: ubuntu-latest + if: | + github.repository == 'IfcOpenShell/IfcOpenShell' + steps: + - name: Set env + run: echo ok go + + build: + needs: activate + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 # https://github.com/actions/setup-python + with: + python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax + - name: Compile + run: | + pip install build + cd src/ifcpatch && + make dist + - name: Publish a Python distribution to PyPI + uses: ortega2247/pypi-upload-action@master + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + packages_dir: src/ifcpatch/dist diff --git a/.github/workflows/ci-ifcsverchok-build.yml b/.github/workflows/ci-ifcsverchok-build.yml new file mode 100644 index 0000000000..637adc4441 --- /dev/null +++ b/.github/workflows/ci-ifcsverchok-build.yml @@ -0,0 +1,53 @@ +name: Publish-ifcsverchok + +on: + push: + paths: + - '.github/workflows/ci-ifcsverchok-build.yml' + - 'src/ifcsverchok/*' + branches: + - v0.7.0 + +env: + major: 0 + minor: 0 + name: ifcsverchok + +jobs: + activate: + runs-on: ubuntu-latest + if: | + github.repository == 'IfcOpenShell/IfcOpenShell' + steps: + - name: Set env + run: echo ok go + + build: + needs: activate + name: ifcsverchok + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 # https://github.com/actions/setup-python + with: + architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified + python-version: '3.11' + - run: echo ${{ env.DATE }} + - name: Get current date + id: date + run: echo "::set-output name=date::$(date +'%y%m%d')" + - name: Compile + run: | + cd src/ifcsverchok + make dist + - name: Upload Zip file to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: src/ifcsverchok/dist/ifcsverchok-${{steps.date.outputs.date}}.zip + asset_name: ifcsverchok-${{steps.date.outputs.date}}.zip + tag: "ifcsverchok-${{steps.date.outputs.date}}" + overwrite: true + body: "ifcsverchok build for ${{steps.date.outputs.date}}" diff --git a/.github/workflows/ci-ifctester-pypi.yml b/.github/workflows/ci-ifctester-pypi.yml index d1302e38f3..24226acd86 100644 --- a/.github/workflows/ci-ifctester-pypi.yml +++ b/.github/workflows/ci-ifctester-pypi.yml @@ -9,9 +9,6 @@ on: # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) # * * * * * - cron: "0 0 18 * *" - push: - paths: - - '.github/workflows/ci-ifctester-pypi.yml' workflow_dispatch: env: @@ -30,7 +27,6 @@ jobs: build: needs: activate - name: ${{ matrix.config.name }}-${{ matrix.pyver }} runs-on: ubuntu-latest strategy: fail-fast: false @@ -38,12 +34,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 # https://github.com/actions/setup-python with: - python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax - architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified - - run: echo ${{ env.DATE }} - - name: Get current date - id: date - run: echo "::set-output name=date::$(date +'%y%m%d')" + python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax - name: Compile run: | pip install build diff --git a/.gitignore b/.gitignore index 498b2e3f46..61037d93bf 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,9 @@ src/blenderbim/blenderbim/translations.py # blenderbim test temp files src/blenderbim/test/files/temp +src/blenderbim/test/files/basic.ifc.cache.blend +src/blenderbim/test/files/basic.ifc.cache.sqlite + src/blenderbim/drawings src/blenderbim/layouts diff --git a/README.md b/README.md index f353a28940..a563f01470 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,14 @@ and many other libraries, CLI apps, and more. Support is also provided for auxil For more information, see: * [IfcOpenShell Website](http://ifcopenshell.org) -* [IfcOpenShell Documentation](http://blenderbim.org/docs-python) - * [IfcOpenShell C++ Installation](https://blenderbim.org/docs-python/ifcopenshell/installation.html) - * [IfcOpenShell Python Installation](https://blenderbim.org/docs-python/ifcopenshell-python/installation.html) - * [IfcOpenShell Python Hello World Tutorial](https://blenderbim.org/docs-python/ifcopenshell-python/hello_world.html) +* [IfcOpenShell Documentation](https://docs.ifcopenshell.org) + * [IfcOpenShell C++ Installation](https://docs.ifcopenshell.org/ifcopenshell/installation.html) + * [IfcOpenShell Python Installation](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) + * [IfcOpenShell Python Hello World Tutorial](https://docs.ifcopenshell.org/ifcopenshell-python/hello_world.html) * [BlenderBIM Add-on Website](https://blenderbim.org) -* [BlenderBIM Add-on Documentation](http://blenderbim.org/docs) - * [Add-on Installation](https://blenderbim.org/docs/users/installation.html) - * [Exploring an IFC model](https://blenderbim.org/docs/users/exploring_an_ifc_model.html) +* [BlenderBIM Add-on Documentation](https://docs.blenderbim.org/index.html) + * [Add-on Installation](https://docs.blenderbim.org/users/installation.html) + * [Exploring an IFC model](https://docs.blenderbim.org/users/exploring_an_ifc_model.html) | Service | Status | | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/aws/lambda/Dockerfile b/aws/lambda/Dockerfile index fcffa1e87b..8076692e81 100644 --- a/aws/lambda/Dockerfile +++ b/aws/lambda/Dockerfile @@ -12,7 +12,7 @@ RUN apt-get -y update && apt-get -y install unzip curl # Install AWS Lambda runtime interface client RUN pip install --target ${FUNCTION_DIR} awslambdaric -# Set the IfcOpenShell build version (check available builds at: https://blenderbim.org/docs-python/ifcopenshell-python/installation.html) +# Set the IfcOpenShell build version (check available builds at: https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) ARG IFC_OPENSHELL_BUILD="39-v0.7.0-476ab50" # Download and extract IfcOpenShell diff --git a/choco/blenderbim/blenderbim.nuspec b/choco/blenderbim/blenderbim.nuspec index d76e86a8ce..bafb86e381 100644 --- a/choco/blenderbim/blenderbim.nuspec +++ b/choco/blenderbim/blenderbim.nuspec @@ -15,7 +15,7 @@ https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.7.0/COPYING true https://github.com/IfcOpenShell/IfcOpenShell - https://blenderbim.org/docs/ + https://docs.blenderbim.org/ https://github.com/IfcOpenShell/IfcOpenShell/issues blender bim blenderbim ifc python opensource foss diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 85721778f0..8f7451afa2 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -1172,7 +1172,7 @@ endforeach(COMPONENT) set(CPACK_DEBIAN_PACKAGE_NAME "${PROJECT_NAME}") set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${CPACK_PACKAGE_CONTACT}") -set(CPACK_DEBIAN_PACKAGE_DEPENDS "python3, libxml2, libocct-foundation-dev, libocct-modeling-algorithms-dev, libocct-modeling-data-dev, libocct-ocaf-dev, libocct-visualization-dev, libocct-data-exchange-dev, libhdf5-serial-dev, libpython3.8, python3-pytest ${BOOST_DEPS}") +set(CPACK_DEBIAN_PACKAGE_DEPENDS "python3, libxml2, libocct-foundation-dev, libocct-modeling-algorithms-dev, libocct-modeling-data-dev, libocct-ocaf-dev, libocct-visualization-dev, libocct-data-exchange-dev, libhdf5-serial-dev, libpython3-dev, python3-pytest ${BOOST_DEPS}") set(CPACK_DEBIAN_PACKAGE_DESCRIPTION_SUMMARY "${CPACK_PACKAGE_DESCRIPTION_SUMMARY}") set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "${CPACK_PACKAGE_DESCRIPTION}") set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional") diff --git a/nix/build-all.py b/nix/build-all.py index b9b017776e..7de9040c80 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -76,7 +76,7 @@ PROJECT_NAME = "IfcOpenShell" USE_CURRENT_PYTHON_VERSION = os.getenv("USE_CURRENT_PYTHON_VERSION") ADD_COMMIT_SHA = os.getenv("ADD_COMMIT_SHA") -PYTHON_VERSIONS = ["3.7.13", "3.8.13", "3.9.11", "3.10.3", "3.11.7", "3.12.1"] +PYTHON_VERSIONS = ["3.9.11", "3.10.3", "3.11.8", "3.12.1"] JSON_VERSION = "v3.6.1" OCE_VERSION = "0.18.3" OCCT_VERSION = "7.5.3" @@ -254,6 +254,11 @@ if not os.path.exists(LOG_FILE): open(LOG_FILE, "w").close() logger.info(f"using command log file '{LOG_FILE}'") +# Causing havoc in python 3.11 build +try: + del os.environ['__PYVENV_LAUNCHER__'] +except: pass + def run(cmds, cwd=None, can_fail=False): """ @@ -284,8 +289,9 @@ if platform.system() == "Darwin": # Apparently not supported PYTHON_VERSIONS = [pv for pv in PYTHON_VERSIONS if tuple(map(int, pv.split("."))) >= (3, 7)] if run(["sw_vers", "-productVersion"]) < "10.16": - # Apparently not supported - PYTHON_VERSIONS = [pv for pv in PYTHON_VERSIONS if tuple(map(int, pv.split("."))) < (3, 11)] + # This is now solved with the '__PYVENV_LAUNCHER__' hack + # PYTHON_VERSIONS = [pv for pv in PYTHON_VERSIONS if tuple(map(int, pv.split("."))) < (3, 11)] + pass BOOST_VERSION_UNDERSCORE = BOOST_VERSION.replace(".", "_") diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index 54f317465e..2bdf4b27a6 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -137,7 +137,7 @@ endif cp -r blenderbim/* dist/blenderbim/ # Provides IfcOpenShell Python functionality - cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-eaa2aa0-$(PLATFORM)64.zip + cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-f7c03db-$(PLATFORM)64.zip cd dist/working && unzip ifcopenshell-python* cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/ @@ -362,7 +362,9 @@ endif # Provides express rule validation for ifcopenshell.validate cd dist/working && . env/bin/activate && $(PIP) install pytest --target=./site-packages # Provides Brickschema functionality - cd dist/working && . env/bin/activate && $(PIP) install brickschema[persistence] --target=./site-packages + cd dist/working && . env/bin/activate && $(PIP) install "brickschema[persistence]==0.7.6a2" --target=./site-packages + # Required for SVG to DXF conversion + cd dist/working && . env/bin/activate && $(PIP) install "ezdxf" --target=./site-packages cp -r dist/working/site-packages/* dist/blenderbim/libs/site/packages/ rm -rf dist/working @@ -430,7 +432,7 @@ endif # Required by brickschema # This is a bit of a dodgy one, it should be provided by setuptools which Blender doesn't ship. mkdir dist/working - cd dist/working && wget https://blenderbim.org/builds/pkg_resources.tar.gz + cd dist/working && wget https://github.com/IfcOpenShell/IfcOpenShell/raw/v0.7.0/src/blenderbim/scripts/pkg_resources.tar.gz cd dist/working && tar -xzvf pkg_resources* cd dist/working/ && cp -r pkg_resources ../blenderbim/libs/site/packages/ rm -rf dist/working diff --git a/src/blenderbim/blenderbim/__init__.py b/src/blenderbim/blenderbim/__init__.py index 2f6e5481ba..67b03e5702 100644 --- a/src/blenderbim/blenderbim/__init__.py +++ b/src/blenderbim/blenderbim/__init__.py @@ -27,7 +27,7 @@ bl_info = { "blender": (3, 1, 0), "version": (0, 0, 999999), "location": "File Menu, Scene Properties Tab. See documentation for more.", - "doc_url": "https://blenderbim.org/docs", + "doc_url": "https://docs.blenderbim.org/", "tracker_url": "https://github.com/IfcOpenShell/IfcOpenShell/issues", "category": "System", } diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index f8cc59abbb..1476a30bff 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -23,6 +23,7 @@ import bpy.utils.previews import blenderbim import importlib from . import handler, ui, prop, operator, helper +from typing import Callable, Union try: from blenderbim.translations import translations_dict @@ -96,8 +97,10 @@ for name in modules.keys(): classes = [ operator.AddIfcFile, operator.BIM_OT_add_section_plane, + operator.BIM_OT_delete_object, operator.BIM_OT_open_webbrowser, operator.BIM_OT_remove_section_plane, + operator.BIM_OT_select_object, operator.BIM_OT_show_description, operator.ClippingPlaneCutWithCappings, operator.EditBlenderCollection, @@ -130,6 +133,7 @@ classes = [ prop.BIMMeshProperties, prop.BIMFacet, prop.BIMFilterGroup, + ui.BIM_UL_clipping_plane, ui.BIM_UL_generic, ui.BIM_UL_topics, ui.BIM_ADDON_preferences, @@ -187,7 +191,7 @@ addon_keymaps = [] icons = None is_registering = False last_commit_hash = "8888888" -overridden_scene_panels = dict() +original_scene_panels_polls: dict[bpy.types.Panel, Union[Callable, None]] = dict() def on_register(scene): @@ -295,10 +299,10 @@ def unregister(): km.keymap_items.remove(kmi) addon_keymaps.clear() - for panel in tuple(overridden_scene_panels.keys()): - original_panel, override_panel = overridden_scene_panels[panel] - bpy.utils.unregister_class(override_panel) - bpy.utils.register_class(original_panel) - del overridden_scene_panels[panel] + import blenderbim.tool as tool + + # use tuple() as method will be removing keys from dict + for panel in tuple(original_scene_panels_polls.keys()): + tool.Blender.remove_scene_panel_override(panel) bpy.app.translations.unregister("blenderbim") diff --git a/src/blenderbim/blenderbim/bim/data/assets/default.css b/src/blenderbim/blenderbim/bim/data/assets/default.css index 2b4d93bc51..861b8ba3ba 100644 --- a/src/blenderbim/blenderbim/bim/data/assets/default.css +++ b/src/blenderbim/blenderbim/bim/data/assets/default.css @@ -10,7 +10,7 @@ * (at your option) any later version. * * BlenderBIM Add-on is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of + * but WITHOUT ANY WARRANTY, without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * diff --git a/src/blenderbim/blenderbim/bim/data/assets/sample.css b/src/blenderbim/blenderbim/bim/data/assets/sample.css index c0a7b0ec74..6b280509a4 100644 --- a/src/blenderbim/blenderbim/bim/data/assets/sample.css +++ b/src/blenderbim/blenderbim/bim/data/assets/sample.css @@ -10,7 +10,7 @@ * (at your option) any later version. * * BlenderBIM Add-on is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of + * but WITHOUT ANY WARRANTY, without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * diff --git a/src/blenderbim/blenderbim/bim/data/libraries/IFC4 Entourage Library.ifc b/src/blenderbim/blenderbim/bim/data/libraries/IFC4 Entourage Library.ifc new file mode 100644 index 0000000000..cd9cab0678 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/data/libraries/IFC4 Entourage Library.ifc @@ -0,0 +1,1653 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1'); +FILE_NAME('/dev/null','2024-03-29T15:32:22+11:00',(),(),'IfcOpenShell v0.7.0-98386984f','IfcOpenShell v0.7.0-98386984f','Nobody'); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#1=IFCPROJECT('3nrYk6M6z1humWj5w6BMD9',$,'Entourage Assets Library',$,$,$,$,(#10,#14),#5); +#2=IFCPROJECTLIBRARY('3wn$cg$5LCi88GExc$we5s',$,'Entourage Assets Library',$,$,$,$,$,$); +#3=IFCRELDECLARES('1bz83_LQjFQANpzUPaDfwx',$,$,$,#1,(#2)); +#4=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.); +#5=IFCUNITASSIGNMENT((#4)); +#6=IFCCARTESIANPOINT((0.,0.,0.)); +#7=IFCDIRECTION((0.,0.,1.)); +#8=IFCDIRECTION((1.,0.,0.)); +#9=IFCAXIS2PLACEMENT3D(#6,#7,#8); +#10=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,$); +#11=IFCCARTESIANPOINT((0.,0.)); +#12=IFCDIRECTION((1.,0.)); +#13=IFCAXIS2PLACEMENT2D(#11,#12); +#14=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Plan',2,1.E-05,#13,$); +#15=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#10,$,.MODEL_VIEW.,$); +#16=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Plan',*,*,*,*,#14,$,.PLAN_VIEW.,$); +#17=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#10,$,.PLAN_VIEW.,$); +#18=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#10,$,.SECTION_VIEW.,$); +#19=IFCBUILDINGELEMENTPROXYTYPE('1rfzKQLFf6QeGzovoh2SJc',$,'Man Standing with Turban',$,$,$,(#545),$,'ENTOURAGE',.USERDEFINED.); +#20=IFCINDEXEDPOLYGONALFACE((1,3,2)); +#21=IFCINDEXEDPOLYGONALFACE((2,3,4)); +#22=IFCINDEXEDPOLYGONALFACE((5,6,4)); +#23=IFCINDEXEDPOLYGONALFACE((7,9,8)); +#24=IFCINDEXEDPOLYGONALFACE((9,11,10)); +#25=IFCINDEXEDPOLYGONALFACE((9,7,12)); +#26=IFCINDEXEDPOLYGONALFACE((13,15,14)); +#27=IFCINDEXEDPOLYGONALFACE((16,18,17)); +#28=IFCINDEXEDPOLYGONALFACE((19,20,17)); +#29=IFCINDEXEDPOLYGONALFACE((20,21,17)); +#30=IFCINDEXEDPOLYGONALFACE((21,20,22)); +#31=IFCINDEXEDPOLYGONALFACE((21,22,23)); +#32=IFCINDEXEDPOLYGONALFACE((24,26,25)); +#33=IFCINDEXEDPOLYGONALFACE((24,25,27)); +#34=IFCINDEXEDPOLYGONALFACE((27,25,26)); +#35=IFCINDEXEDPOLYGONALFACE((28,29,27)); +#36=IFCINDEXEDPOLYGONALFACE((29,31,30)); +#37=IFCINDEXEDPOLYGONALFACE((32,34,33)); +#38=IFCINDEXEDPOLYGONALFACE((35,34,36)); +#39=IFCINDEXEDPOLYGONALFACE((36,34,32)); +#40=IFCINDEXEDPOLYGONALFACE((33,38,37)); +#41=IFCINDEXEDPOLYGONALFACE((39,41,40)); +#42=IFCINDEXEDPOLYGONALFACE((40,41,38)); +#43=IFCINDEXEDPOLYGONALFACE((42,41,39)); +#44=IFCINDEXEDPOLYGONALFACE((43,39,40)); +#45=IFCINDEXEDPOLYGONALFACE((43,44,39)); +#46=IFCINDEXEDPOLYGONALFACE((44,46,45)); +#47=IFCINDEXEDPOLYGONALFACE((47,49,48)); +#48=IFCINDEXEDPOLYGONALFACE((50,51,48)); +#49=IFCINDEXEDPOLYGONALFACE((51,50,52)); +#50=IFCINDEXEDPOLYGONALFACE((50,53,52)); +#51=IFCINDEXEDPOLYGONALFACE((54,56,55)); +#52=IFCINDEXEDPOLYGONALFACE((55,58,57)); +#53=IFCINDEXEDPOLYGONALFACE((59,57,60)); +#54=IFCINDEXEDPOLYGONALFACE((61,63,62)); +#55=IFCINDEXEDPOLYGONALFACE((18,64,61)); +#56=IFCINDEXEDPOLYGONALFACE((64,66,65)); +#57=IFCINDEXEDPOLYGONALFACE((18,16,64)); +#58=IFCINDEXEDPOLYGONALFACE((19,17,18)); +#59=IFCINDEXEDPOLYGONALFACE((67,29,28)); +#60=IFCINDEXEDPOLYGONALFACE((43,40,38)); +#61=IFCINDEXEDPOLYGONALFACE((61,68,18)); +#62=IFCINDEXEDPOLYGONALFACE((38,44,43)); +#63=IFCINDEXEDPOLYGONALFACE((69,23,22)); +#64=IFCINDEXEDPOLYGONALFACE((26,67,27)); +#65=IFCINDEXEDPOLYGONALFACE((32,70,36)); +#66=IFCINDEXEDPOLYGONALFACE((71,47,72)); +#67=IFCINDEXEDPOLYGONALFACE((62,60,73)); +#68=IFCINDEXEDPOLYGONALFACE((55,74,58)); +#69=IFCINDEXEDPOLYGONALFACE((74,55,56)); +#70=IFCINDEXEDPOLYGONALFACE((23,26,24)); +#71=IFCINDEXEDPOLYGONALFACE((29,35,31)); +#72=IFCINDEXEDPOLYGONALFACE((33,46,44)); +#73=IFCINDEXEDPOLYGONALFACE((46,33,75)); +#74=IFCINDEXEDPOLYGONALFACE((27,67,28)); +#75=IFCINDEXEDPOLYGONALFACE((38,33,44)); +#76=IFCINDEXEDPOLYGONALFACE((72,76,71)); +#77=IFCINDEXEDPOLYGONALFACE((18,68,19)); +#78=IFCINDEXEDPOLYGONALFACE((77,46,78)); +#79=IFCINDEXEDPOLYGONALFACE((70,32,79)); +#80=IFCINDEXEDPOLYGONALFACE((80,70,79)); +#81=IFCINDEXEDPOLYGONALFACE((80,79,30)); +#82=IFCINDEXEDPOLYGONALFACE((46,75,81)); +#83=IFCINDEXEDPOLYGONALFACE((81,75,82)); +#84=IFCINDEXEDPOLYGONALFACE((49,47,71)); +#85=IFCINDEXEDPOLYGONALFACE((57,58,12)); +#86=IFCINDEXEDPOLYGONALFACE((75,33,34)); +#87=IFCINDEXEDPOLYGONALFACE((11,73,60)); +#88=IFCINDEXEDPOLYGONALFACE((60,57,11)); +#89=IFCINDEXEDPOLYGONALFACE((11,57,83)); +#90=IFCINDEXEDPOLYGONALFACE((62,73,84)); +#91=IFCINDEXEDPOLYGONALFACE((85,74,56)); +#92=IFCINDEXEDPOLYGONALFACE((56,53,85)); +#93=IFCINDEXEDPOLYGONALFACE((22,20,86)); +#94=IFCINDEXEDPOLYGONALFACE((75,34,87)); +#95=IFCINDEXEDPOLYGONALFACE((61,84,68)); +#96=IFCINDEXEDPOLYGONALFACE((61,62,84)); +#97=IFCINDEXEDPOLYGONALFACE((30,88,80)); +#98=IFCINDEXEDPOLYGONALFACE((88,31,35)); +#99=IFCINDEXEDPOLYGONALFACE((88,30,31)); +#100=IFCINDEXEDPOLYGONALFACE((46,81,78)); +#101=IFCINDEXEDPOLYGONALFACE((78,81,76)); +#102=IFCINDEXEDPOLYGONALFACE((86,68,69)); +#103=IFCINDEXEDPOLYGONALFACE((20,19,86)); +#104=IFCINDEXEDPOLYGONALFACE((13,89,48)); +#105=IFCINDEXEDPOLYGONALFACE((77,78,76)); +#106=IFCINDEXEDPOLYGONALFACE((86,69,22)); +#107=IFCINDEXEDPOLYGONALFACE((67,35,29)); +#108=IFCINDEXEDPOLYGONALFACE((76,72,77)); +#109=IFCINDEXEDPOLYGONALFACE((48,89,50)); +#110=IFCINDEXEDPOLYGONALFACE((11,9,73)); +#111=IFCINDEXEDPOLYGONALFACE((57,12,83)); +#112=IFCINDEXEDPOLYGONALFACE((84,73,7)); +#113=IFCINDEXEDPOLYGONALFACE((19,68,86)); +#114=IFCINDEXEDPOLYGONALFACE((90,69,91)); +#115=IFCINDEXEDPOLYGONALFACE((92,34,35)); +#116=IFCINDEXEDPOLYGONALFACE((23,69,26)); +#117=IFCINDEXEDPOLYGONALFACE((7,73,83)); +#118=IFCINDEXEDPOLYGONALFACE((83,9,93)); +#119=IFCINDEXEDPOLYGONALFACE((73,9,83)); +#120=IFCINDEXEDPOLYGONALFACE((94,14,95)); +#121=IFCINDEXEDPOLYGONALFACE((14,15,95)); +#122=IFCINDEXEDPOLYGONALFACE((96,14,94)); +#123=IFCINDEXEDPOLYGONALFACE((87,92,96)); +#124=IFCINDEXEDPOLYGONALFACE((96,94,82)); +#125=IFCINDEXEDPOLYGONALFACE((82,87,96)); +#126=IFCINDEXEDPOLYGONALFACE((97,92,90)); +#127=IFCINDEXEDPOLYGONALFACE((90,92,35)); +#128=IFCINDEXEDPOLYGONALFACE((90,67,26)); +#129=IFCINDEXEDPOLYGONALFACE((35,67,90)); +#130=IFCINDEXEDPOLYGONALFACE((82,94,97)); +#131=IFCINDEXEDPOLYGONALFACE((98,94,95)); +#132=IFCINDEXEDPOLYGONALFACE((97,94,98)); +#133=IFCINDEXEDPOLYGONALFACE((75,87,82)); +#134=IFCINDEXEDPOLYGONALFACE((49,99,15)); +#135=IFCINDEXEDPOLYGONALFACE((49,81,99)); +#136=IFCINDEXEDPOLYGONALFACE((13,48,49)); +#137=IFCINDEXEDPOLYGONALFACE((74,97,12)); +#138=IFCINDEXEDPOLYGONALFACE((58,74,12)); +#139=IFCINDEXEDPOLYGONALFACE((97,74,85)); +#140=IFCINDEXEDPOLYGONALFACE((98,92,97)); +#141=IFCINDEXEDPOLYGONALFACE((90,91,97)); +#142=IFCINDEXEDPOLYGONALFACE((92,98,96)); +#143=IFCINDEXEDPOLYGONALFACE((99,14,96)); +#144=IFCINDEXEDPOLYGONALFACE((99,96,98)); +#145=IFCINDEXEDPOLYGONALFACE((81,50,89)); +#146=IFCINDEXEDPOLYGONALFACE((81,89,14)); +#147=IFCINDEXEDPOLYGONALFACE((81,14,99)); +#148=IFCINDEXEDPOLYGONALFACE((85,81,82)); +#149=IFCINDEXEDPOLYGONALFACE((81,49,76)); +#150=IFCINDEXEDPOLYGONALFACE((50,81,85)); +#151=IFCINDEXEDPOLYGONALFACE((49,71,76)); +#152=IFCINDEXEDPOLYGONALFACE((50,85,53)); +#153=IFCINDEXEDPOLYGONALFACE((89,13,14)); +#154=IFCINDEXEDPOLYGONALFACE((88,35,36)); +#155=IFCINDEXEDPOLYGONALFACE((80,36,70)); +#156=IFCINDEXEDPOLYGONALFACE((88,36,80)); +#157=IFCINDEXEDPOLYGONALFACE((16,21,3)); +#158=IFCINDEXEDPOLYGONALFACE((16,3,100)); +#159=IFCINDEXEDPOLYGONALFACE((66,63,61)); +#160=IFCINDEXEDPOLYGONALFACE((101,103,102)); +#161=IFCINDEXEDPOLYGONALFACE((102,63,66)); +#162=IFCINDEXEDPOLYGONALFACE((103,63,102)); +#163=IFCINDEXEDPOLYGONALFACE((30,32,104)); +#164=IFCINDEXEDPOLYGONALFACE((105,97,8)); +#165=IFCINDEXEDPOLYGONALFACE((8,97,91)); +#166=IFCINDEXEDPOLYGONALFACE((61,64,65)); +#167=IFCINDEXEDPOLYGONALFACE((5,4,3)); +#168=IFCINDEXEDPOLYGONALFACE((106,5,3)); +#169=IFCINDEXEDPOLYGONALFACE((3,107,106)); +#170=IFCINDEXEDPOLYGONALFACE((106,107,108)); +#171=IFCINDEXEDPOLYGONALFACE((108,109,106)); +#172=IFCINDEXEDPOLYGONALFACE((83,12,7)); +#173=IFCINDEXEDPOLYGONALFACE((103,101,63)); +#174=IFCINDEXEDPOLYGONALFACE((9,10,8)); +#175=IFCINDEXEDPOLYGONALFACE((69,90,26)); +#176=IFCINDEXEDPOLYGONALFACE((108,104,37)); +#177=IFCINDEXEDPOLYGONALFACE((104,32,37)); +#178=IFCINDEXEDPOLYGONALFACE((100,110,64)); +#179=IFCINDEXEDPOLYGONALFACE((3,110,100)); +#180=IFCINDEXEDPOLYGONALFACE((42,111,41)); +#181=IFCINDEXEDPOLYGONALFACE((37,38,111)); +#182=IFCINDEXEDPOLYGONALFACE((84,69,68)); +#183=IFCINDEXEDPOLYGONALFACE((84,91,69)); +#184=IFCINDEXEDPOLYGONALFACE((93,83,105)); +#185=IFCINDEXEDPOLYGONALFACE((12,105,83)); +#186=IFCINDEXEDPOLYGONALFACE((100,64,16)); +#187=IFCINDEXEDPOLYGONALFACE((66,64,112)); +#188=IFCINDEXEDPOLYGONALFACE((61,65,66)); +#189=IFCINDEXEDPOLYGONALFACE((108,113,42)); +#190=IFCINDEXEDPOLYGONALFACE((49,15,13)); +#191=IFCINDEXEDPOLYGONALFACE((12,97,105)); +#192=IFCINDEXEDPOLYGONALFACE((8,91,84)); +#193=IFCINDEXEDPOLYGONALFACE((34,92,87)); +#194=IFCINDEXEDPOLYGONALFACE((93,11,83)); +#195=IFCINDEXEDPOLYGONALFACE((114,60,63)); +#196=IFCINDEXEDPOLYGONALFACE((114,59,60)); +#197=IFCINDEXEDPOLYGONALFACE((72,47,115)); +#198=IFCINDEXEDPOLYGONALFACE((111,38,41)); +#199=IFCINDEXEDPOLYGONALFACE((63,60,62)); +#200=IFCINDEXEDPOLYGONALFACE((52,53,56)); +#201=IFCINDEXEDPOLYGONALFACE((54,52,56)); +#202=IFCINDEXEDPOLYGONALFACE((48,116,47)); +#203=IFCINDEXEDPOLYGONALFACE((57,59,55)); +#204=IFCINDEXEDPOLYGONALFACE((52,54,51)); +#205=IFCINDEXEDPOLYGONALFACE((116,48,51)); +#206=IFCINDEXEDPOLYGONALFACE((27,107,24)); +#207=IFCINDEXEDPOLYGONALFACE((110,112,64)); +#208=IFCINDEXEDPOLYGONALFACE((110,3,117)); +#209=IFCINDEXEDPOLYGONALFACE((117,112,110)); +#210=IFCINDEXEDPOLYGONALFACE((30,79,32)); +#211=IFCINDEXEDPOLYGONALFACE((117,102,66)); +#212=IFCINDEXEDPOLYGONALFACE((1,102,117)); +#213=IFCINDEXEDPOLYGONALFACE((102,1,101)); +#214=IFCINDEXEDPOLYGONALFACE((114,1,2)); +#215=IFCINDEXEDPOLYGONALFACE((44,45,39)); +#216=IFCINDEXEDPOLYGONALFACE((66,112,117)); +#217=IFCINDEXEDPOLYGONALFACE((55,59,54)); +#218=IFCINDEXEDPOLYGONALFACE((33,37,32)); +#219=IFCINDEXEDPOLYGONALFACE((63,101,114)); +#220=IFCINDEXEDPOLYGONALFACE((101,1,114)); +#221=IFCINDEXEDPOLYGONALFACE((118,77,72)); +#222=IFCINDEXEDPOLYGONALFACE((51,54,116)); +#223=IFCINDEXEDPOLYGONALFACE((27,29,107)); +#224=IFCINDEXEDPOLYGONALFACE((42,113,111)); +#225=IFCINDEXEDPOLYGONALFACE((111,113,37)); +#226=IFCINDEXEDPOLYGONALFACE((17,21,16)); +#227=IFCINDEXEDPOLYGONALFACE((4,59,114)); +#228=IFCINDEXEDPOLYGONALFACE((45,46,77)); +#229=IFCINDEXEDPOLYGONALFACE((45,77,118)); +#230=IFCINDEXEDPOLYGONALFACE((104,29,30)); +#231=IFCINDEXEDPOLYGONALFACE((72,115,118)); +#232=IFCINDEXEDPOLYGONALFACE((107,29,104)); +#233=IFCINDEXEDPOLYGONALFACE((59,4,54)); +#234=IFCINDEXEDPOLYGONALFACE((39,45,42)); +#235=IFCINDEXEDPOLYGONALFACE((115,47,116)); +#236=IFCINDEXEDPOLYGONALFACE((108,37,113)); +#237=IFCINDEXEDPOLYGONALFACE((109,108,42)); +#238=IFCINDEXEDPOLYGONALFACE((107,104,108)); +#239=IFCINDEXEDPOLYGONALFACE((114,2,4)); +#240=IFCINDEXEDPOLYGONALFACE((115,119,45)); +#241=IFCINDEXEDPOLYGONALFACE((42,45,119)); +#242=IFCINDEXEDPOLYGONALFACE((109,42,119)); +#243=IFCINDEXEDPOLYGONALFACE((54,6,116)); +#244=IFCINDEXEDPOLYGONALFACE((23,24,107)); +#245=IFCINDEXEDPOLYGONALFACE((23,107,21)); +#246=IFCINDEXEDPOLYGONALFACE((3,21,107)); +#247=IFCINDEXEDPOLYGONALFACE((119,121,120)); +#248=IFCINDEXEDPOLYGONALFACE((119,115,121)); +#249=IFCINDEXEDPOLYGONALFACE((118,115,45)); +#250=IFCINDEXEDPOLYGONALFACE((6,123,122)); +#251=IFCINDEXEDPOLYGONALFACE((54,4,6)); +#252=IFCINDEXEDPOLYGONALFACE((3,1,117)); +#253=IFCINDEXEDPOLYGONALFACE((124,123,6)); +#254=IFCINDEXEDPOLYGONALFACE((124,120,123)); +#255=IFCINDEXEDPOLYGONALFACE((7,8,84)); +#256=IFCINDEXEDPOLYGONALFACE((82,97,85)); +#257=IFCINDEXEDPOLYGONALFACE((125,127,126)); +#258=IFCINDEXEDPOLYGONALFACE((128,130,129)); +#259=IFCINDEXEDPOLYGONALFACE((128,125,131)); +#260=IFCINDEXEDPOLYGONALFACE((132,6,133)); +#261=IFCINDEXEDPOLYGONALFACE((133,134,132)); +#262=IFCINDEXEDPOLYGONALFACE((135,137,136)); +#263=IFCINDEXEDPOLYGONALFACE((138,137,135)); +#264=IFCINDEXEDPOLYGONALFACE((139,141,140)); +#265=IFCINDEXEDPOLYGONALFACE((134,127,139)); +#266=IFCINDEXEDPOLYGONALFACE((134,139,142)); +#267=IFCINDEXEDPOLYGONALFACE((127,125,143)); +#268=IFCINDEXEDPOLYGONALFACE((143,128,144)); +#269=IFCINDEXEDPOLYGONALFACE((128,145,144)); +#270=IFCINDEXEDPOLYGONALFACE((129,145,128)); +#271=IFCINDEXEDPOLYGONALFACE((139,140,146)); +#272=IFCINDEXEDPOLYGONALFACE((147,137,138)); +#273=IFCINDEXEDPOLYGONALFACE((148,149,137)); +#274=IFCINDEXEDPOLYGONALFACE((147,148,137)); +#275=IFCINDEXEDPOLYGONALFACE((150,149,148)); +#276=IFCINDEXEDPOLYGONALFACE((131,125,126)); +#277=IFCINDEXEDPOLYGONALFACE((133,6,5)); +#278=IFCINDEXEDPOLYGONALFACE((151,133,5)); +#279=IFCINDEXEDPOLYGONALFACE((5,106,151)); +#280=IFCINDEXEDPOLYGONALFACE((126,133,151)); +#281=IFCINDEXEDPOLYGONALFACE((147,130,131)); +#282=IFCINDEXEDPOLYGONALFACE((148,147,131)); +#283=IFCINDEXEDPOLYGONALFACE((152,149,150)); +#284=IFCINDEXEDPOLYGONALFACE((149,152,153)); +#285=IFCINDEXEDPOLYGONALFACE((153,152,154)); +#286=IFCINDEXEDPOLYGONALFACE((155,156,149)); +#287=IFCINDEXEDPOLYGONALFACE((155,149,153)); +#288=IFCINDEXEDPOLYGONALFACE((157,159,158)); +#289=IFCINDEXEDPOLYGONALFACE((159,157,160)); +#290=IFCINDEXEDPOLYGONALFACE((161,159,160)); +#291=IFCINDEXEDPOLYGONALFACE((162,161,155)); +#292=IFCINDEXEDPOLYGONALFACE((161,162,159)); +#293=IFCINDEXEDPOLYGONALFACE((163,164,158)); +#294=IFCINDEXEDPOLYGONALFACE((164,165,158)); +#295=IFCINDEXEDPOLYGONALFACE((166,157,158)); +#296=IFCINDEXEDPOLYGONALFACE((166,158,165)); +#297=IFCINDEXEDPOLYGONALFACE((162,158,159)); +#298=IFCINDEXEDPOLYGONALFACE((167,168,162)); +#299=IFCINDEXEDPOLYGONALFACE((153,154,167)); +#300=IFCINDEXEDPOLYGONALFACE((169,163,154)); +#301=IFCINDEXEDPOLYGONALFACE((154,152,169)); +#302=IFCINDEXEDPOLYGONALFACE((170,119,171)); +#303=IFCINDEXEDPOLYGONALFACE((170,173,172)); +#304=IFCINDEXEDPOLYGONALFACE((174,173,170)); +#305=IFCINDEXEDPOLYGONALFACE((173,175,172)); +#306=IFCINDEXEDPOLYGONALFACE((176,169,175)); +#307=IFCINDEXEDPOLYGONALFACE((163,176,173)); +#308=IFCINDEXEDPOLYGONALFACE((168,163,162)); +#309=IFCINDEXEDPOLYGONALFACE((163,158,162)); +#310=IFCINDEXEDPOLYGONALFACE((167,154,168)); +#311=IFCINDEXEDPOLYGONALFACE((163,168,154)); +#312=IFCINDEXEDPOLYGONALFACE((177,179,178)); +#313=IFCINDEXEDPOLYGONALFACE((177,178,180)); +#314=IFCINDEXEDPOLYGONALFACE((181,177,180)); +#315=IFCINDEXEDPOLYGONALFACE((182,179,177)); +#316=IFCINDEXEDPOLYGONALFACE((181,182,177)); +#317=IFCINDEXEDPOLYGONALFACE((183,182,184)); +#318=IFCINDEXEDPOLYGONALFACE((185,187,186)); +#319=IFCINDEXEDPOLYGONALFACE((185,188,187)); +#320=IFCINDEXEDPOLYGONALFACE((161,160,187)); +#321=IFCINDEXEDPOLYGONALFACE((188,161,187)); +#322=IFCINDEXEDPOLYGONALFACE((189,161,188)); +#323=IFCINDEXEDPOLYGONALFACE((189,155,161)); +#324=IFCINDEXEDPOLYGONALFACE((155,189,190)); +#325=IFCINDEXEDPOLYGONALFACE((156,155,190)); +#326=IFCINDEXEDPOLYGONALFACE((156,136,149)); +#327=IFCINDEXEDPOLYGONALFACE((149,136,137)); +#328=IFCINDEXEDPOLYGONALFACE((191,156,192)); +#329=IFCINDEXEDPOLYGONALFACE((192,194,193)); +#330=IFCINDEXEDPOLYGONALFACE((193,191,192)); +#331=IFCINDEXEDPOLYGONALFACE((160,157,186)); +#332=IFCINDEXEDPOLYGONALFACE((157,166,186)); +#333=IFCINDEXEDPOLYGONALFACE((195,186,166)); +#334=IFCINDEXEDPOLYGONALFACE((196,166,165)); +#335=IFCINDEXEDPOLYGONALFACE((197,198,196)); +#336=IFCINDEXEDPOLYGONALFACE((196,198,166)); +#337=IFCINDEXEDPOLYGONALFACE((199,198,197)); +#338=IFCINDEXEDPOLYGONALFACE((199,197,200)); +#339=IFCINDEXEDPOLYGONALFACE((201,199,200)); +#340=IFCINDEXEDPOLYGONALFACE((199,201,202)); +#341=IFCINDEXEDPOLYGONALFACE((203,148,131)); +#342=IFCINDEXEDPOLYGONALFACE((203,131,106)); +#343=IFCINDEXEDPOLYGONALFACE((203,150,148)); +#344=IFCINDEXEDPOLYGONALFACE((152,150,203)); +#345=IFCINDEXEDPOLYGONALFACE((188,204,192)); +#346=IFCINDEXEDPOLYGONALFACE((194,192,204)); +#347=IFCINDEXEDPOLYGONALFACE((109,119,172)); +#348=IFCINDEXEDPOLYGONALFACE((109,172,169)); +#349=IFCINDEXEDPOLYGONALFACE((152,109,169)); +#350=IFCINDEXEDPOLYGONALFACE((203,109,152)); +#351=IFCINDEXEDPOLYGONALFACE((203,106,109)); +#352=IFCINDEXEDPOLYGONALFACE((163,169,176)); +#353=IFCINDEXEDPOLYGONALFACE((140,141,205)); +#354=IFCINDEXEDPOLYGONALFACE((206,207,146)); +#355=IFCINDEXEDPOLYGONALFACE((207,208,142)); +#356=IFCINDEXEDPOLYGONALFACE((208,207,209)); +#357=IFCINDEXEDPOLYGONALFACE((134,142,208)); +#358=IFCINDEXEDPOLYGONALFACE((208,132,134)); +#359=IFCINDEXEDPOLYGONALFACE((6,132,208)); +#360=IFCINDEXEDPOLYGONALFACE((146,207,139)); +#361=IFCINDEXEDPOLYGONALFACE((139,207,142)); +#362=IFCINDEXEDPOLYGONALFACE((210,205,211)); +#363=IFCINDEXEDPOLYGONALFACE((210,212,205)); +#364=IFCINDEXEDPOLYGONALFACE((213,215,214)); +#365=IFCINDEXEDPOLYGONALFACE((216,217,213)); +#366=IFCINDEXEDPOLYGONALFACE((214,215,218)); +#367=IFCINDEXEDPOLYGONALFACE((215,217,219)); +#368=IFCINDEXEDPOLYGONALFACE((217,215,213)); +#369=IFCINDEXEDPOLYGONALFACE((138,130,147)); +#370=IFCINDEXEDPOLYGONALFACE((124,6,209)); +#371=IFCINDEXEDPOLYGONALFACE((220,222,221)); +#372=IFCINDEXEDPOLYGONALFACE((223,194,224)); +#373=IFCINDEXEDPOLYGONALFACE((193,194,223)); +#374=IFCINDEXEDPOLYGONALFACE((193,223,225)); +#375=IFCINDEXEDPOLYGONALFACE((224,226,223)); +#376=IFCINDEXEDPOLYGONALFACE((224,227,226)); +#377=IFCINDEXEDPOLYGONALFACE((228,229,145)); +#378=IFCINDEXEDPOLYGONALFACE((230,228,231)); +#379=IFCINDEXEDPOLYGONALFACE((231,232,230)); +#380=IFCINDEXEDPOLYGONALFACE((228,230,233)); +#381=IFCINDEXEDPOLYGONALFACE((144,234,143)); +#382=IFCINDEXEDPOLYGONALFACE((234,235,143)); +#383=IFCINDEXEDPOLYGONALFACE((236,219,217)); +#384=IFCINDEXEDPOLYGONALFACE((227,237,226)); +#385=IFCINDEXEDPOLYGONALFACE((226,237,238)); +#386=IFCINDEXEDPOLYGONALFACE((227,199,237)); +#387=IFCINDEXEDPOLYGONALFACE((227,239,199)); +#388=IFCINDEXEDPOLYGONALFACE((239,240,199)); +#389=IFCINDEXEDPOLYGONALFACE((195,166,240)); +#390=IFCINDEXEDPOLYGONALFACE((119,109,241)); +#391=IFCINDEXEDPOLYGONALFACE((160,186,187)); +#392=IFCINDEXEDPOLYGONALFACE((198,199,240)); +#393=IFCINDEXEDPOLYGONALFACE((198,240,166)); +#394=IFCINDEXEDPOLYGONALFACE((242,237,202)); +#395=IFCINDEXEDPOLYGONALFACE((242,238,237)); +#396=IFCINDEXEDPOLYGONALFACE((237,199,202)); +#397=IFCINDEXEDPOLYGONALFACE((200,244,243)); +#398=IFCINDEXEDPOLYGONALFACE((244,183,222)); +#399=IFCINDEXEDPOLYGONALFACE((244,197,183)); +#400=IFCINDEXEDPOLYGONALFACE((200,197,244)); +#401=IFCINDEXEDPOLYGONALFACE((201,200,243)); +#402=IFCINDEXEDPOLYGONALFACE((201,243,232)); +#403=IFCINDEXEDPOLYGONALFACE((245,246,243)); +#404=IFCINDEXEDPOLYGONALFACE((244,245,243)); +#405=IFCINDEXEDPOLYGONALFACE((247,246,245)); +#406=IFCINDEXEDPOLYGONALFACE((246,233,230)); +#407=IFCINDEXEDPOLYGONALFACE((230,232,243)); +#408=IFCINDEXEDPOLYGONALFACE((230,243,246)); +#409=IFCINDEXEDPOLYGONALFACE((245,220,247)); +#410=IFCINDEXEDPOLYGONALFACE((221,247,220)); +#411=IFCINDEXEDPOLYGONALFACE((221,218,247)); +#412=IFCINDEXEDPOLYGONALFACE((218,229,233)); +#413=IFCINDEXEDPOLYGONALFACE((218,233,248)); +#414=IFCINDEXEDPOLYGONALFACE((218,248,247)); +#415=IFCINDEXEDPOLYGONALFACE((221,214,218)); +#416=IFCINDEXEDPOLYGONALFACE((236,218,219)); +#417=IFCINDEXEDPOLYGONALFACE((219,218,215)); +#418=IFCINDEXEDPOLYGONALFACE((234,216,235)); +#419=IFCINDEXEDPOLYGONALFACE((234,217,216)); +#420=IFCINDEXEDPOLYGONALFACE((235,216,211)); +#421=IFCINDEXEDPOLYGONALFACE((205,235,211)); +#422=IFCINDEXEDPOLYGONALFACE((205,141,235)); +#423=IFCINDEXEDPOLYGONALFACE((155,167,162)); +#424=IFCINDEXEDPOLYGONALFACE((155,153,167)); +#425=IFCINDEXEDPOLYGONALFACE((249,242,232)); +#426=IFCINDEXEDPOLYGONALFACE((202,201,242)); +#427=IFCINDEXEDPOLYGONALFACE((247,248,246)); +#428=IFCINDEXEDPOLYGONALFACE((246,248,233)); +#429=IFCINDEXEDPOLYGONALFACE((218,236,229)); +#430=IFCINDEXEDPOLYGONALFACE((143,235,141)); +#431=IFCINDEXEDPOLYGONALFACE((141,127,143)); +#432=IFCINDEXEDPOLYGONALFACE((125,128,143)); +#433=IFCINDEXEDPOLYGONALFACE((139,127,141)); +#434=IFCINDEXEDPOLYGONALFACE((144,236,234)); +#435=IFCINDEXEDPOLYGONALFACE((236,217,234)); +#436=IFCINDEXEDPOLYGONALFACE((228,233,229)); +#437=IFCINDEXEDPOLYGONALFACE((250,129,138)); +#438=IFCINDEXEDPOLYGONALFACE((250,138,251)); +#439=IFCINDEXEDPOLYGONALFACE((252,251,253)); +#440=IFCINDEXEDPOLYGONALFACE((252,250,251)); +#441=IFCINDEXEDPOLYGONALFACE((252,231,250)); +#442=IFCINDEXEDPOLYGONALFACE((249,253,254)); +#443=IFCINDEXEDPOLYGONALFACE((253,249,252)); +#444=IFCINDEXEDPOLYGONALFACE((252,232,231)); +#445=IFCINDEXEDPOLYGONALFACE((129,130,138)); +#446=IFCINDEXEDPOLYGONALFACE((130,128,131)); +#447=IFCINDEXEDPOLYGONALFACE((127,134,126)); +#448=IFCINDEXEDPOLYGONALFACE((134,133,126)); +#449=IFCINDEXEDPOLYGONALFACE((255,135,191)); +#450=IFCINDEXEDPOLYGONALFACE((191,135,136)); +#451=IFCINDEXEDPOLYGONALFACE((255,138,135)); +#452=IFCINDEXEDPOLYGONALFACE((193,255,191)); +#453=IFCINDEXEDPOLYGONALFACE((225,255,193)); +#454=IFCINDEXEDPOLYGONALFACE((255,251,138)); +#455=IFCINDEXEDPOLYGONALFACE((255,253,251)); +#456=IFCINDEXEDPOLYGONALFACE((255,254,253)); +#457=IFCINDEXEDPOLYGONALFACE((225,254,255)); +#458=IFCINDEXEDPOLYGONALFACE((254,238,249)); +#459=IFCINDEXEDPOLYGONALFACE((223,254,225)); +#460=IFCINDEXEDPOLYGONALFACE((254,223,226)); +#461=IFCINDEXEDPOLYGONALFACE((232,242,201)); +#462=IFCINDEXEDPOLYGONALFACE((195,240,239)); +#463=IFCINDEXEDPOLYGONALFACE((195,227,224)); +#464=IFCINDEXEDPOLYGONALFACE((185,195,224)); +#465=IFCINDEXEDPOLYGONALFACE((186,195,185)); +#466=IFCINDEXEDPOLYGONALFACE((224,194,204)); +#467=IFCINDEXEDPOLYGONALFACE((185,224,204)); +#468=IFCINDEXEDPOLYGONALFACE((188,185,204)); +#469=IFCINDEXEDPOLYGONALFACE((242,249,238)); +#470=IFCINDEXEDPOLYGONALFACE((232,252,249)); +#471=IFCINDEXEDPOLYGONALFACE((228,129,231)); +#472=IFCINDEXEDPOLYGONALFACE((145,129,228)); +#473=IFCINDEXEDPOLYGONALFACE((250,231,129)); +#474=IFCINDEXEDPOLYGONALFACE((145,229,144)); +#475=IFCINDEXEDPOLYGONALFACE((229,236,144)); +#476=IFCINDEXEDPOLYGONALFACE((238,254,226)); +#477=IFCINDEXEDPOLYGONALFACE((195,239,227)); +#478=IFCINDEXEDPOLYGONALFACE((156,189,188)); +#479=IFCINDEXEDPOLYGONALFACE((188,192,156)); +#480=IFCINDEXEDPOLYGONALFACE((191,136,156)); +#481=IFCINDEXEDPOLYGONALFACE((190,189,156)); +#482=IFCINDEXEDPOLYGONALFACE((245,244,220)); +#483=IFCINDEXEDPOLYGONALFACE((220,244,222)); +#484=IFCINDEXEDPOLYGONALFACE((106,131,151)); +#485=IFCINDEXEDPOLYGONALFACE((131,126,151)); +#486=IFCINDEXEDPOLYGONALFACE((169,172,175)); +#487=IFCINDEXEDPOLYGONALFACE((170,172,119)); +#488=IFCINDEXEDPOLYGONALFACE((256,257,119)); +#489=IFCINDEXEDPOLYGONALFACE((256,181,180)); +#490=IFCINDEXEDPOLYGONALFACE((164,173,174)); +#491=IFCINDEXEDPOLYGONALFACE((164,174,178)); +#492=IFCINDEXEDPOLYGONALFACE((257,170,171)); +#493=IFCINDEXEDPOLYGONALFACE((257,174,170)); +#494=IFCINDEXEDPOLYGONALFACE((196,165,258)); +#495=IFCINDEXEDPOLYGONALFACE((258,183,196)); +#496=IFCINDEXEDPOLYGONALFACE((173,164,163)); +#497=IFCINDEXEDPOLYGONALFACE((178,165,164)); +#498=IFCINDEXEDPOLYGONALFACE((258,165,178)); +#499=IFCINDEXEDPOLYGONALFACE((258,178,179)); +#500=IFCINDEXEDPOLYGONALFACE((179,183,258)); +#501=IFCINDEXEDPOLYGONALFACE((259,222,183)); +#502=IFCINDEXEDPOLYGONALFACE((184,260,259)); +#503=IFCINDEXEDPOLYGONALFACE((184,210,260)); +#504=IFCINDEXEDPOLYGONALFACE((211,260,210)); +#505=IFCINDEXEDPOLYGONALFACE((211,213,260)); +#506=IFCINDEXEDPOLYGONALFACE((182,183,179)); +#507=IFCINDEXEDPOLYGONALFACE((214,259,213)); +#508=IFCINDEXEDPOLYGONALFACE((221,222,259)); +#509=IFCINDEXEDPOLYGONALFACE((214,221,259)); +#510=IFCINDEXEDPOLYGONALFACE((213,259,260)); +#511=IFCINDEXEDPOLYGONALFACE((212,146,140)); +#512=IFCINDEXEDPOLYGONALFACE((140,205,212)); +#513=IFCINDEXEDPOLYGONALFACE((6,208,209)); +#514=IFCINDEXEDPOLYGONALFACE((256,180,257)); +#515=IFCINDEXEDPOLYGONALFACE((257,171,119)); +#516=IFCINDEXEDPOLYGONALFACE((178,174,257)); +#517=IFCINDEXEDPOLYGONALFACE((178,257,180)); +#518=IFCINDEXEDPOLYGONALFACE((206,256,124)); +#519=IFCINDEXEDPOLYGONALFACE((256,120,124)); +#520=IFCINDEXEDPOLYGONALFACE((256,119,120)); +#521=IFCINDEXEDPOLYGONALFACE((212,182,181)); +#522=IFCINDEXEDPOLYGONALFACE((210,184,212)); +#523=IFCINDEXEDPOLYGONALFACE((212,184,182)); +#524=IFCINDEXEDPOLYGONALFACE((146,212,206)); +#525=IFCINDEXEDPOLYGONALFACE((124,209,206)); +#526=IFCINDEXEDPOLYGONALFACE((206,212,181)); +#527=IFCINDEXEDPOLYGONALFACE((256,206,181)); +#528=IFCINDEXEDPOLYGONALFACE((206,209,207)); +#529=IFCINDEXEDPOLYGONALFACE((184,259,183)); +#530=IFCINDEXEDPOLYGONALFACE((176,175,173)); +#531=IFCINDEXEDPOLYGONALFACE((196,183,197)); +#532=IFCINDEXEDPOLYGONALFACE((213,211,216)); +#533=IFCINDEXEDPOLYGONALFACE((122,121,115)); +#534=IFCINDEXEDPOLYGONALFACE((122,123,121)); +#535=IFCINDEXEDPOLYGONALFACE((123,120,121)); +#536=IFCINDEXEDPOLYGONALFACE((116,122,115)); +#537=IFCINDEXEDPOLYGONALFACE((116,6,122)); +#538=IFCCARTESIANPOINTLIST3D(((250.027374267578,-23.882905960083,1415.95446777344),(189.34294128418,-35.7263298034668,1403.52282714844),(237.454940795898,54.2838821411133,1518.08740234375),(126.709022521973,-52.5286827087402,1465.04675292969),(72.5036392211914,31.4245052337646,1561.49487304688),(28.7249774932861,-44.164794921875,1507.29235839844),(167.752944946289,-41.1005210876465,272.277130126953),(88.4636154174805,133.811492919922,-1.86264543344805E-07),(146.995422363281,-42.8920211791992,64.3589630126953),(146.09033203125,32.7610664367676,-1.86264543344805E-07),(175.912811279297,-101.924354553223,-1.86264543344805E-07),(87.4011001586914,-35.3959732055664,182.559417724609),(-146.337890625,-247.331100463867,84.7031478881836),(-128.706588745117,-105.762428283691,72.7251815795898),(-191.414672851562,-195.42936706543,-1.86264543344805E-07),(198.540817260742,138.32893371582,1088.42199707031),(213.262069702148,208.597732543945,814.737426757812),(223.903457641602,158.067337036133,603.092712402344),(222.216049194336,195.401000976562,419.532836914062),(168.335983276367,241.589233398438,363.422088623047),(141.263061523438,122.122032165527,1471.9970703125),(188.906753540039,155.980361938477,514.717407226562),(81.5025100708008,168.2490234375,1202.76501464844),(41.5524711608887,166.412170410156,1207.63073730469),(7.80561876296997,155.191467285156,982.802978515625),(57.9982490539551,245.809005737305,483.971405029297),(-17.6877479553223,200.386428833008,1060.72265625),(17.9774417877197,176.138122558594,732.623291015625),(-74.0164031982422,149.849548339844,1060.47814941406),(-100.488059997559,211.286590576172,989.019409179688),(-106.23828125,189.579803466797,681.878845214844),(-227.530517578125,142.242568969727,1068.16943359375),(-212.905776977539,63.2130088806152,757.55517578125),(-123.063583374023,122.40258026123,633.907653808594),(-75.0333709716797,155.949584960938,495.433868408203),(-183.794021606445,156.028854370117,501.240814208984),(-284.961975097656,171.7763671875,1249.94677734375),(-320.667572021484,49.364631652832,847.766845703125),(-317.137542724609,-116.618293762207,934.140808105469),(-315.947265625,-64.7200698852539,889.551635742188),(-396.098724365234,-5.17033386230469,1006.86987304688),(-303.506683349609,28.2569217681885,1491.77648925781),(-378.405944824219,-37.4422225952148,647.299133300781),(-291.714935302734,-62.3378295898438,757.3779296875),(-247.474243164062,-14.4445514678955,1490.72448730469),(-223.26545715332,-72.8882598876953,737.782165527344),(-163.737350463867,-117.198844909668,889.643127441406),(-128.974685668945,-195.850173950195,690.143127441406),(-181.846130371094,-171.127288818359,347.562286376953),(-100.279792785645,-137.020523071289,269.947326660156),(-84.9218444824219,-145.87825012207,946.980041503906),(-27.9072284698486,-181.795471191406,794.125915527344),(17.5650749206543,-231.546752929688,258.120178222656),(42.0146293640137,-106.533447265625,1293.30859375),(73.8282623291016,-162.069351196289,865.566528320312),(36.9884071350098,-112.385345458984,658.78515625),(137.766799926758,-117.065483093262,521.771118164062),(93.2620849609375,-193.72575378418,210.056549072266),(131.815292358398,-135.018936157227,1027.45837402344),(171.427124023438,-159.859420776367,491.391387939453),(175.360702514648,79.7155838012695,726.210083007812),(223.453506469727,-38.7604484558105,480.577392578125),(205.460113525391,-77.460334777832,677.622863769531),(286.508911132812,162.409469604492,787.532775878906),(248.989151000977,54.3829689025879,720.736633300781),(236.968444824219,38.6795692443848,842.181823730469),(-14.3888101577759,210.571716308594,546.630065917969),(187.714797973633,101.758567810059,504.419006347656),(87.2769622802734,130.830810546875,393.722137451172),(-226.19856262207,249.275756835938,401.809020996094),(-202.331390380859,-100.888618469238,487.280517578125),(-202.734512329102,-104.948028564453,862.516357421875),(254.341201782227,-75.7364654541016,170.172393798828),(47.9848747253418,-83.7818984985352,240.561096191406),(-187.932357788086,-4.74206352233887,690.693664550781),(-254.408248901367,-181.340118408203,366.982391357422),(-238.162551879883,-141.793212890625,786.39453125),(-234.73176574707,-78.0891036987305,503.983001708984),(-171.837875366211,194.545074462891,921.239379882812),(-137.170669555664,276.274780273438,319.8212890625),(-182.028137207031,-42.0894432067871,201.879730224609),(-127.360870361328,-53.1248779296875,337.412628173828),(99.0055999755859,-74.0925827026367,124.908515930176),(176.656387329102,72.9649810791016,338.603942871094),(1.96485579013824,-78.6706390380859,278.222473144531),(194.152099609375,160.744506835938,259.376800537109),(-197.659713745117,19.7222137451172,393.040374755859),(-99.9229507446289,216.252670288086,307.358856201172),(-80.8954620361328,-260.127990722656,177.856781005859),(-1.46076452732086,154.994689941406,499.547607421875),(20.0321407318115,109.741317749023,341.221801757812),(-161.034683227539,128.413375854492,225.929534912109),(82.0537338256836,-183.277267456055,-1.86264543344805E-07),(-84.9847946166992,-9.26582050323486,96.9040603637695),(-80.3021469116211,-67.6039657592773,-1.86264543344805E-07),(-170.064987182617,46.298656463623,112.053344726562),(-14.7262620925903,74.1896057128906,165.622222900391),(-96.8446731567383,118.10758972168,-1.86264543344805E-07),(-171.509857177734,-116.833053588867,-2.81568168247759E-06),(257.419982910156,134.479064941406,1180.33276367188),(216.522766113281,-95.3502807617188,929.773376464844),(263.888305664062,-61.2300872802734,796.839294433594),(270.641235351562,-120.480857849121,590.52490234375),(-50.033016204834,214.258926391602,1267.05114746094),(77.1798324584961,-30.2338027954102,-1.86264543344805E-07),(27.9293670654297,72.1410369873047,1592.03479003906),(27.5987339019775,155.599700927734,1496.07653808594),(-100.322135925293,159.092620849609,1426.15380859375),(-62.4779853820801,106.087181091309,1569.58715820312),(295.056488037109,66.8833618164062,960.128540039062),(-335.873657226562,88.4998626708984,1153.00036621094),(365.221954345703,32.4079666137695,855.487915039062),(-281.184967041016,130.421447753906,1272.55029296875),(163.060455322266,-56.9987716674805,1030.525390625),(-87.3238830566406,-71.4930953979492,1434.99816894531),(-84.5702743530273,-135.309982299805,1187.45886230469),(287.197998046875,49.4243621826172,1330.24853515625),(-184.120391845703,-50.421516418457,1373.43566894531),(-142.448364257812,35.334041595459,1565.02941894531),(-99.3179550170898,-19.288537979126,1523.37231445312),(-93.320915222168,-45.390811920166,1479.18518066406),(-29.2994594573975,-57.8289451599121,1471.14526367188),(-35.2964897155762,-31.7266635894775,1515.33227539062),(-65.5984573364258,-60.2618713378906,1526.04479980469),(58.8699798583984,-22.6804714202881,1643.52099609375),(58.794059753418,-3.24364733695984,1596.69812011719),(39.4151382446289,-44.7899703979492,1589.05676269531),(56.7482948303223,-4.49460697174072,1678.35876464844),(65.5708694458008,7.61722707748413,1683.13317871094),(47.4631729125977,22.193675994873,1655.82824707031),(56.3341026306152,13.9416971206665,1623.55346679688),(48.0009269714355,-53.2649879455566,1538.89514160156),(75.7735595703125,-2.46892857551575,1575.37060546875),(46.9823722839355,-52.3717002868652,1573.06872558594),(66.5905303955078,61.1993713378906,1696.62231445312),(46.397575378418,86.9236450195312,1689.51574707031),(35.3512573242188,71.18359375,1659.33557128906),(53.5354232788086,43.4169006347656,1686.71154785156),(21.1407566070557,-54.3571548461914,1584.15661621094),(-21.7695751190186,-61.950065612793,1578.35888671875),(21.5765476226807,-47.9868316650391,1602.47143554688),(34.5973091125488,-66.6909027099609,1576.93969726562),(29.2881603240967,-30.0027637481689,1650.26586914062),(34.3188362121582,-13.5777320861816,1671.78601074219),(35.2478332519531,-37.7936706542969,1709.56225585938),(-13.5368356704712,-62.4897956848145,1568.09484863281),(29.7789516448975,46.6104774475098,1671.64904785156),(1.82311594486237,71.6545486450195,1641.24072265625),(17.1210784912109,106.867897033691,1675.75793457031),(-16.8500270843506,97.2289428710938,1641.71923828125),(66.2260589599609,37.6655502319336,1596.92211914062),(-8.87947082519531,112.180603027344,1624.4013671875),(15.606294631958,121.90754699707,1684.44287109375),(-14.4493827819824,139.713424682617,1654.13891601562),(-3.74729537963867,105.154037475586,1724.6572265625),(36.853157043457,56.968692779541,1733.27893066406),(-107.787452697754,50.2590713500977,1734.28967285156),(-111.847526550293,72.4723052978516,1707.8095703125),(-89.0276412963867,95.1375961303711,1709.05334472656),(-73.5994644165039,79.1380844116211,1727.65319824219),(-46.1447639465332,84.1150283813477,1724.58190917969),(-56.0835151672363,113.140914916992,1691.41149902344),(-64.6567001342773,101.772010803223,1659.74243164062),(-113.400001525879,76.9580764770508,1654.50439453125),(-140.313064575195,57.271053314209,1717.62268066406),(-130.794647216797,25.171085357666,1779.181640625),(-11.6539697647095,126.992546081543,1690.99108886719),(-26.2866077423096,113.777976989746,1671.01440429688),(-58.3992042541504,128.119964599609,1603.22546386719),(-116.66121673584,72.7393646240234,1621.02319335938),(-124.835304260254,54.6162528991699,1598.45190429688),(-89.5713653564453,99.8517456054688,1586.52966308594),(-99.4943008422852,101.570877075195,1638.84228515625),(-126.053352355957,60.064826965332,1631.58569335938),(-83.4318313598633,113.620147705078,1606.25549316406),(-57.5030403137207,112.11254119873,1631.6826171875),(-109.388824462891,-7.19644451141357,1626.74584960938),(-114.661773681641,58.6825637817383,1641.99792480469),(-121.194046020508,16.4241180419922,1666.09533691406),(-118.340675354004,15.6668634414673,1623.85632324219),(-100.413032531738,-27.8646450042725,1610.89392089844),(-119.065963745117,-40.2054672241211,1641.12915039062),(-127.211250305176,-19.4964771270752,1713.14807128906),(-99.4045257568359,-70.9100494384766,1629.31860351562),(-41.655460357666,50.295581817627,1825.77868652344),(-101.226608276367,52.7529525756836,1786.65112304688),(-55.055736541748,73.1881866455078,1792.86657714844),(-5.87042760848999,80.1052017211914,1779.80688476562),(-9.98227596282959,80.9901962280273,1751.09362792969),(24.2499217987061,80.5161209106445,1732.94213867188),(72.5959777832031,53.4743347167969,1747.63439941406),(36.0746688842773,41.2442512512207,1763.26123046875),(50.6687545776367,26.4588584899902,1781.81750488281),(31.1245708465576,6.53590202331543,1796.54870605469),(-82.0447769165039,26.7043991088867,1822.99499511719),(-156.420776367188,-0.598646640777588,1722.66259765625),(-126.260612487793,-59.5931396484375,1747.97924804688),(-146.866394042969,-25.8437519073486,1763.64904785156),(-116.805503845215,-66.2452087402344,1803.30102539062),(-109.480865478516,-92.2581024169922,1763.716796875),(-68.0312042236328,-122.729965209961,1794.67236328125),(-84.1829452514648,-88.0408477783203,1805.11474609375),(23.2931175231934,93.5863723754883,1618.4228515625),(11.1880254745483,34.1745147705078,1817.01477050781),(-29.5253868103027,-78.2835006713867,1599.21813964844),(-84.3417434692383,-63.7740097045898,1571.93908691406),(-13.3443574905396,-80.5143814086914,1554.70178222656),(17.0866050720215,-70.6183166503906,1538.67492675781),(-23.4297542572021,-75.5428924560547,1530.46716308594),(-68.2336807250977,-87.9513778686523,1605.31945800781),(-20.097225189209,-99.0588150024414,1612.77429199219),(-73.4214859008789,-59.3197593688965,1591.12976074219),(-52.7204971313477,-113.585334777832,1644.90148925781),(-86.80126953125,-101.023811340332,1691.12878417969),(-43.94482421875,-130.208251953125,1692.28747558594),(-17.4796237945557,-100.564514160156,1638.8994140625),(-13.3766117095947,-113.837280273438,1658.80065917969),(-42.8742446899414,-114.699562072754,1708.47570800781),(-14.4483852386475,-119.351066589355,1695.07263183594),(-98.4373474121094,-75.3005523681641,1733.36560058594),(-96.7265319824219,-77.0361938476562,1700.3935546875),(-108.931015014648,-35.1993446350098,1718.70092773438),(22.9082126617432,-19.9589500427246,1838.35314941406),(-5.36053991317749,-11.386736869812,1835.03649902344),(52.3087768554688,-9.16755485534668,1807.44372558594),(-11.7170696258545,-48.2666549682617,1845.03369140625),(-54.2986450195312,-63.0719985961914,1842.57153320312),(31.1523685455322,-54.402099609375,1728.83129882812),(15.5857229232788,-70.3590698242188,1706.11267089844),(5.26045608520508,-92.7595977783203,1758.42651367188),(50.0484619140625,-57.3395805358887,1733.59814453125),(9.64294719696045,-107.044288635254,1782.62023925781),(19.1671657562256,-71.3778381347656,1730.2509765625),(14.7019052505493,-71.9714126586914,1648.45837402344),(13.7142124176025,-60.3307304382324,1634.32580566406),(23.7327308654785,-70.3778839111328,1694.55334472656),(-43.2096328735352,-93.7546005249023,1834.45617675781),(-11.7863674163818,-83.3447113037109,1823.212890625),(-87.4517669677734,-44.174259185791,1828.69201660156),(-115.331466674805,-24.3140258789062,1822.38879394531),(-43.3605690002441,96.572639465332,1567.35949707031),(-24.9877071380615,-91.3389358520508,1814.35375976562),(-47.7474403381348,-107.26180267334,1781.99169921875),(-106.339996337891,-82.1798400878906,1755.80249023438),(-89.2768936157227,-97.65234375,1750.74169921875),(-33.6130409240723,-113.925422668457,1759.22412109375),(-41.1620254516602,-102.617835998535,1736.90686035156),(-19.0609798431396,-86.9065551757812,1738.84204101562),(12.0251731872559,-70.5674285888672,1797.03173828125),(61.6594314575195,-32.8811149597168,1730.0009765625),(60.6472129821777,16.3021373748779,1722.08154296875),(43.3303642272949,-63.0390586853027,1766.38647460938),(43.6823692321777,-28.4500370025635,1760.86938476562),(46.8201713562012,-36.1587028503418,1789.75598144531),(69.1895446777344,25.5214042663574,1733.66735839844),(-116.517105102539,-32.7507019042969,1585.50732421875),(-131.696182250977,26.6582431793213,1614.7607421875),(-145.591796875,17.3771591186523,1684.21557617188),(-101.790901184082,-53.6442909240723,1662.22338867188),(-68.0224609375,-93.8657455444336,1625.93310546875))); +#539=IFCPOLYGONALFACESET(#538,.F.,(#20,#21,#22,#23,#24,#25,#26,#27,#28,#29,#30,#31,#32,#33,#34,#35,#36,#37,#38,#39,#40,#41,#42,#43,#44,#45,#46,#47,#48,#49,#50,#51,#52,#53,#54,#55,#56,#57,#58,#59,#60,#61,#62,#63,#64,#65,#66,#67,#68,#69,#70,#71,#72,#73,#74,#75,#76,#77,#78,#79,#80,#81,#82,#83,#84,#85,#86,#87,#88,#89,#90,#91,#92,#93,#94,#95,#96,#97,#98,#99,#100,#101,#102,#103,#104,#105,#106,#107,#108,#109,#110,#111,#112,#113,#114,#115,#116,#117,#118,#119,#120,#121,#122,#123,#124,#125,#126,#127,#128,#129,#130,#131,#132,#133,#134,#135,#136,#137,#138,#139,#140,#141,#142,#143,#144,#145,#146,#147,#148,#149,#150,#151,#152,#153,#154,#155,#156,#157,#158,#159,#160,#161,#162,#163,#164,#165,#166,#167,#168,#169,#170,#171,#172,#173,#174,#175,#176,#177,#178,#179,#180,#181,#182,#183,#184,#185,#186,#187,#188,#189,#190,#191,#192,#193,#194,#195,#196,#197,#198,#199,#200,#201,#202,#203,#204,#205,#206,#207,#208,#209,#210,#211,#212,#213,#214,#215,#216,#217,#218,#219,#220,#221,#222,#223,#224,#225,#226,#227,#228,#229,#230,#231,#232,#233,#234,#235,#236,#237,#238,#239,#240,#241,#242,#243,#244,#245,#246,#247,#248,#249,#250,#251,#252,#253,#254,#255,#256,#257,#258,#259,#260,#261,#262,#263,#264,#265,#266,#267,#268,#269,#270,#271,#272,#273,#274,#275,#276,#277,#278,#279,#280,#281,#282,#283,#284,#285,#286,#287,#288,#289,#290,#291,#292,#293,#294,#295,#296,#297,#298,#299,#300,#301,#302,#303,#304,#305,#306,#307,#308,#309,#310,#311,#312,#313,#314,#315,#316,#317,#318,#319,#320,#321,#322,#323,#324,#325,#326,#327,#328,#329,#330,#331,#332,#333,#334,#335,#336,#337,#338,#339,#340,#341,#342,#343,#344,#345,#346,#347,#348,#349,#350,#351,#352,#353,#354,#355,#356,#357,#358,#359,#360,#361,#362,#363,#364,#365,#366,#367,#368,#369,#370,#371,#372,#373,#374,#375,#376,#377,#378,#379,#380,#381,#382,#383,#384,#385,#386,#387,#388,#389,#390,#391,#392,#393,#394,#395,#396,#397,#398,#399,#400,#401,#402,#403,#404,#405,#406,#407,#408,#409,#410,#411,#412,#413,#414,#415,#416,#417,#418,#419,#420,#421,#422,#423,#424,#425,#426,#427,#428,#429,#430,#431,#432,#433,#434,#435,#436,#437,#438,#439,#440,#441,#442,#443,#444,#445,#446,#447,#448,#449,#450,#451,#452,#453,#454,#455,#456,#457,#458,#459,#460,#461,#462,#463,#464,#465,#466,#467,#468,#469,#470,#471,#472,#473,#474,#475,#476,#477,#478,#479,#480,#481,#482,#483,#484,#485,#486,#487,#488,#489,#490,#491,#492,#493,#494,#495,#496,#497,#498,#499,#500,#501,#502,#503,#504,#505,#506,#507,#508,#509,#510,#511,#512,#513,#514,#515,#516,#517,#518,#519,#520,#521,#522,#523,#524,#525,#526,#527,#528,#529,#530,#531,#532,#533,#534,#535,#536,#537),$); +#540=IFCSHAPEREPRESENTATION(#15,'Body','Tessellation',(#539)); +#541=IFCCARTESIANPOINT((0.,0.,0.)); +#542=IFCDIRECTION((1.,0.,0.)); +#543=IFCDIRECTION((0.,0.,1.)); +#544=IFCAXIS2PLACEMENT3D(#541,#543,#542); +#545=IFCREPRESENTATIONMAP(#544,#540); +#546=IFCRELDECLARES('1LAs99F5HFPxJ8dv8mbtsX',$,$,$,#2,(#547,#19)); +#547=IFCBUILDINGELEMENTPROXYTYPE('04rXMTw3j5M8LqX77nU2uH',$,'Woman Dancing',$,$,$,(#1644),$,'ENTOURAGE',.USERDEFINED.); +#548=IFCINDEXEDPOLYGONALFACE((2,3,1)); +#549=IFCINDEXEDPOLYGONALFACE((4,1,3)); +#550=IFCINDEXEDPOLYGONALFACE((5,4,3)); +#551=IFCINDEXEDPOLYGONALFACE((1,7,6)); +#552=IFCINDEXEDPOLYGONALFACE((7,9,8)); +#553=IFCINDEXEDPOLYGONALFACE((7,10,9)); +#554=IFCINDEXEDPOLYGONALFACE((10,11,9)); +#555=IFCINDEXEDPOLYGONALFACE((9,12,8)); +#556=IFCINDEXEDPOLYGONALFACE((9,11,12)); +#557=IFCINDEXEDPOLYGONALFACE((13,15,14)); +#558=IFCINDEXEDPOLYGONALFACE((16,18,17)); +#559=IFCINDEXEDPOLYGONALFACE((18,16,19)); +#560=IFCINDEXEDPOLYGONALFACE((20,6,17)); +#561=IFCINDEXEDPOLYGONALFACE((17,6,16)); +#562=IFCINDEXEDPOLYGONALFACE((16,21,19)); +#563=IFCINDEXEDPOLYGONALFACE((7,8,6)); +#564=IFCINDEXEDPOLYGONALFACE((12,22,8)); +#565=IFCINDEXEDPOLYGONALFACE((12,23,22)); +#566=IFCINDEXEDPOLYGONALFACE((24,26,25)); +#567=IFCINDEXEDPOLYGONALFACE((28,29,27)); +#568=IFCINDEXEDPOLYGONALFACE((29,28,24)); +#569=IFCINDEXEDPOLYGONALFACE((30,31,27)); +#570=IFCINDEXEDPOLYGONALFACE((27,31,32)); +#571=IFCINDEXEDPOLYGONALFACE((28,34,33)); +#572=IFCINDEXEDPOLYGONALFACE((33,24,28)); +#573=IFCINDEXEDPOLYGONALFACE((13,34,35)); +#574=IFCINDEXEDPOLYGONALFACE((34,13,33)); +#575=IFCINDEXEDPOLYGONALFACE((26,33,13)); +#576=IFCINDEXEDPOLYGONALFACE((30,36,31)); +#577=IFCINDEXEDPOLYGONALFACE((28,27,32)); +#578=IFCINDEXEDPOLYGONALFACE((37,38,13)); +#579=IFCINDEXEDPOLYGONALFACE((23,39,22)); +#580=IFCINDEXEDPOLYGONALFACE((41,42,40)); +#581=IFCINDEXEDPOLYGONALFACE((44,45,43)); +#582=IFCINDEXEDPOLYGONALFACE((47,48,46)); +#583=IFCINDEXEDPOLYGONALFACE((49,39,23)); +#584=IFCINDEXEDPOLYGONALFACE((50,23,12)); +#585=IFCINDEXEDPOLYGONALFACE((52,53,51)); +#586=IFCINDEXEDPOLYGONALFACE((55,56,54)); +#587=IFCINDEXEDPOLYGONALFACE((57,59,58)); +#588=IFCINDEXEDPOLYGONALFACE((60,62,61)); +#589=IFCINDEXEDPOLYGONALFACE((63,65,64)); +#590=IFCINDEXEDPOLYGONALFACE((66,68,67)); +#591=IFCINDEXEDPOLYGONALFACE((67,68,63)); +#592=IFCINDEXEDPOLYGONALFACE((63,68,65)); +#593=IFCINDEXEDPOLYGONALFACE((68,69,65)); +#594=IFCINDEXEDPOLYGONALFACE((71,72,70)); +#595=IFCINDEXEDPOLYGONALFACE((71,73,72)); +#596=IFCINDEXEDPOLYGONALFACE((74,76,75)); +#597=IFCINDEXEDPOLYGONALFACE((71,77,73)); +#598=IFCINDEXEDPOLYGONALFACE((77,74,73)); +#599=IFCINDEXEDPOLYGONALFACE((77,78,74)); +#600=IFCINDEXEDPOLYGONALFACE((80,81,79)); +#601=IFCINDEXEDPOLYGONALFACE((83,84,82)); +#602=IFCINDEXEDPOLYGONALFACE((85,86,82)); +#603=IFCINDEXEDPOLYGONALFACE((84,88,87)); +#604=IFCINDEXEDPOLYGONALFACE((58,89,57)); +#605=IFCINDEXEDPOLYGONALFACE((90,58,91)); +#606=IFCINDEXEDPOLYGONALFACE((92,91,58)); +#607=IFCINDEXEDPOLYGONALFACE((92,93,91)); +#608=IFCINDEXEDPOLYGONALFACE((92,58,94)); +#609=IFCINDEXEDPOLYGONALFACE((94,58,59)); +#610=IFCINDEXEDPOLYGONALFACE((62,60,95)); +#611=IFCINDEXEDPOLYGONALFACE((97,98,96)); +#612=IFCINDEXEDPOLYGONALFACE((94,93,92)); +#613=IFCINDEXEDPOLYGONALFACE((99,93,94)); +#614=IFCINDEXEDPOLYGONALFACE((94,100,99)); +#615=IFCINDEXEDPOLYGONALFACE((94,59,101)); +#616=IFCINDEXEDPOLYGONALFACE((101,100,94)); +#617=IFCINDEXEDPOLYGONALFACE((102,104,103)); +#618=IFCINDEXEDPOLYGONALFACE((86,105,88)); +#619=IFCINDEXEDPOLYGONALFACE((83,86,88)); +#620=IFCINDEXEDPOLYGONALFACE((82,80,75)); +#621=IFCINDEXEDPOLYGONALFACE((75,80,72)); +#622=IFCINDEXEDPOLYGONALFACE((73,75,72)); +#623=IFCINDEXEDPOLYGONALFACE((82,75,85)); +#624=IFCINDEXEDPOLYGONALFACE((75,76,85)); +#625=IFCINDEXEDPOLYGONALFACE((106,108,107)); +#626=IFCINDEXEDPOLYGONALFACE((88,84,83)); +#627=IFCINDEXEDPOLYGONALFACE((109,111,110)); +#628=IFCINDEXEDPOLYGONALFACE((113,114,112)); +#629=IFCINDEXEDPOLYGONALFACE((114,116,115)); +#630=IFCINDEXEDPOLYGONALFACE((117,111,109)); +#631=IFCINDEXEDPOLYGONALFACE((101,118,100)); +#632=IFCINDEXEDPOLYGONALFACE((118,101,119)); +#633=IFCINDEXEDPOLYGONALFACE((120,122,121)); +#634=IFCINDEXEDPOLYGONALFACE((122,123,121)); +#635=IFCINDEXEDPOLYGONALFACE((122,124,123)); +#636=IFCINDEXEDPOLYGONALFACE((109,125,117)); +#637=IFCINDEXEDPOLYGONALFACE((127,125,126)); +#638=IFCINDEXEDPOLYGONALFACE((128,127,129)); +#639=IFCINDEXEDPOLYGONALFACE((130,128,129)); +#640=IFCINDEXEDPOLYGONALFACE((128,130,57)); +#641=IFCINDEXEDPOLYGONALFACE((128,131,127)); +#642=IFCINDEXEDPOLYGONALFACE((57,131,128)); +#643=IFCINDEXEDPOLYGONALFACE((88,66,60)); +#644=IFCINDEXEDPOLYGONALFACE((88,68,66)); +#645=IFCINDEXEDPOLYGONALFACE((105,69,88)); +#646=IFCINDEXEDPOLYGONALFACE((64,69,132)); +#647=IFCINDEXEDPOLYGONALFACE((133,105,134)); +#648=IFCINDEXEDPOLYGONALFACE((133,132,69)); +#649=IFCINDEXEDPOLYGONALFACE((131,57,89)); +#650=IFCINDEXEDPOLYGONALFACE((127,136,135)); +#651=IFCINDEXEDPOLYGONALFACE((127,135,125)); +#652=IFCINDEXEDPOLYGONALFACE((64,65,69)); +#653=IFCINDEXEDPOLYGONALFACE((100,137,99)); +#654=IFCINDEXEDPOLYGONALFACE((137,100,138)); +#655=IFCINDEXEDPOLYGONALFACE((139,140,138)); +#656=IFCINDEXEDPOLYGONALFACE((82,86,83)); +#657=IFCINDEXEDPOLYGONALFACE((109,126,125)); +#658=IFCINDEXEDPOLYGONALFACE((141,91,93)); +#659=IFCINDEXEDPOLYGONALFACE((142,143,141)); +#660=IFCINDEXEDPOLYGONALFACE((132,138,100)); +#661=IFCINDEXEDPOLYGONALFACE((100,118,64)); +#662=IFCINDEXEDPOLYGONALFACE((64,132,100)); +#663=IFCINDEXEDPOLYGONALFACE((69,68,88)); +#664=IFCINDEXEDPOLYGONALFACE((144,116,145)); +#665=IFCINDEXEDPOLYGONALFACE((146,144,145)); +#666=IFCINDEXEDPOLYGONALFACE((146,145,147)); +#667=IFCINDEXEDPOLYGONALFACE((74,78,148)); +#668=IFCINDEXEDPOLYGONALFACE((149,74,148)); +#669=IFCINDEXEDPOLYGONALFACE((76,74,149)); +#670=IFCINDEXEDPOLYGONALFACE((150,76,149)); +#671=IFCINDEXEDPOLYGONALFACE((85,76,150)); +#672=IFCINDEXEDPOLYGONALFACE((151,85,150)); +#673=IFCINDEXEDPOLYGONALFACE((86,85,151)); +#674=IFCINDEXEDPOLYGONALFACE((149,153,152)); +#675=IFCINDEXEDPOLYGONALFACE((149,152,150)); +#676=IFCINDEXEDPOLYGONALFACE((153,154,152)); +#677=IFCINDEXEDPOLYGONALFACE((154,155,152)); +#678=IFCINDEXEDPOLYGONALFACE((136,157,156)); +#679=IFCINDEXEDPOLYGONALFACE((148,153,149)); +#680=IFCINDEXEDPOLYGONALFACE((148,158,153)); +#681=IFCINDEXEDPOLYGONALFACE((159,153,158)); +#682=IFCINDEXEDPOLYGONALFACE((73,74,75)); +#683=IFCINDEXEDPOLYGONALFACE((136,156,135)); +#684=IFCINDEXEDPOLYGONALFACE((160,127,131)); +#685=IFCINDEXEDPOLYGONALFACE((129,127,126)); +#686=IFCINDEXEDPOLYGONALFACE((90,161,89)); +#687=IFCINDEXEDPOLYGONALFACE((161,131,89)); +#688=IFCINDEXEDPOLYGONALFACE((154,153,162)); +#689=IFCINDEXEDPOLYGONALFACE((154,162,163)); +#690=IFCINDEXEDPOLYGONALFACE((164,155,154)); +#691=IFCINDEXEDPOLYGONALFACE((165,159,166)); +#692=IFCINDEXEDPOLYGONALFACE((162,159,165)); +#693=IFCINDEXEDPOLYGONALFACE((165,163,162)); +#694=IFCINDEXEDPOLYGONALFACE((167,169,168)); +#695=IFCINDEXEDPOLYGONALFACE((167,170,169)); +#696=IFCINDEXEDPOLYGONALFACE((133,69,105)); +#697=IFCINDEXEDPOLYGONALFACE((159,162,153)); +#698=IFCINDEXEDPOLYGONALFACE((137,140,99)); +#699=IFCINDEXEDPOLYGONALFACE((171,157,136)); +#700=IFCINDEXEDPOLYGONALFACE((172,144,146)); +#701=IFCINDEXEDPOLYGONALFACE((166,173,165)); +#702=IFCINDEXEDPOLYGONALFACE((173,163,165)); +#703=IFCINDEXEDPOLYGONALFACE((174,133,175)); +#704=IFCINDEXEDPOLYGONALFACE((116,172,115)); +#705=IFCINDEXEDPOLYGONALFACE((116,144,172)); +#706=IFCINDEXEDPOLYGONALFACE((177,178,176)); +#707=IFCINDEXEDPOLYGONALFACE((179,173,177)); +#708=IFCINDEXEDPOLYGONALFACE((180,163,179)); +#709=IFCINDEXEDPOLYGONALFACE((181,163,180)); +#710=IFCINDEXEDPOLYGONALFACE((164,154,163)); +#711=IFCINDEXEDPOLYGONALFACE((181,182,163)); +#712=IFCINDEXEDPOLYGONALFACE((183,184,134)); +#713=IFCINDEXEDPOLYGONALFACE((138,132,174)); +#714=IFCINDEXEDPOLYGONALFACE((132,133,174)); +#715=IFCINDEXEDPOLYGONALFACE((138,174,175)); +#716=IFCINDEXEDPOLYGONALFACE((163,173,179)); +#717=IFCINDEXEDPOLYGONALFACE((177,185,180)); +#718=IFCINDEXEDPOLYGONALFACE((181,186,182)); +#719=IFCINDEXEDPOLYGONALFACE((187,152,155)); +#720=IFCINDEXEDPOLYGONALFACE((188,187,189)); +#721=IFCINDEXEDPOLYGONALFACE((189,187,190)); +#722=IFCINDEXEDPOLYGONALFACE((191,188,189)); +#723=IFCINDEXEDPOLYGONALFACE((192,188,191)); +#724=IFCINDEXEDPOLYGONALFACE((138,140,137)); +#725=IFCINDEXEDPOLYGONALFACE((193,178,194)); +#726=IFCINDEXEDPOLYGONALFACE((175,139,138)); +#727=IFCINDEXEDPOLYGONALFACE((187,155,190)); +#728=IFCINDEXEDPOLYGONALFACE((175,133,184)); +#729=IFCINDEXEDPOLYGONALFACE((196,197,195)); +#730=IFCINDEXEDPOLYGONALFACE((196,199,198)); +#731=IFCINDEXEDPOLYGONALFACE((196,200,199)); +#732=IFCINDEXEDPOLYGONALFACE((196,195,200)); +#733=IFCINDEXEDPOLYGONALFACE((195,197,201)); +#734=IFCINDEXEDPOLYGONALFACE((200,195,202)); +#735=IFCINDEXEDPOLYGONALFACE((203,205,204)); +#736=IFCINDEXEDPOLYGONALFACE((205,202,204)); +#737=IFCINDEXEDPOLYGONALFACE((206,205,203)); +#738=IFCINDEXEDPOLYGONALFACE((205,206,207)); +#739=IFCINDEXEDPOLYGONALFACE((208,210,209)); +#740=IFCINDEXEDPOLYGONALFACE((211,210,208)); +#741=IFCINDEXEDPOLYGONALFACE((209,210,212)); +#742=IFCINDEXEDPOLYGONALFACE((170,214,213)); +#743=IFCINDEXEDPOLYGONALFACE((215,216,211)); +#744=IFCINDEXEDPOLYGONALFACE((217,219,218)); +#745=IFCINDEXEDPOLYGONALFACE((220,219,217)); +#746=IFCINDEXEDPOLYGONALFACE((219,221,218)); +#747=IFCINDEXEDPOLYGONALFACE((221,219,222)); +#748=IFCINDEXEDPOLYGONALFACE((216,221,223)); +#749=IFCINDEXEDPOLYGONALFACE((221,222,223)); +#750=IFCINDEXEDPOLYGONALFACE((216,223,224)); +#751=IFCINDEXEDPOLYGONALFACE((211,216,224)); +#752=IFCINDEXEDPOLYGONALFACE((195,225,202)); +#753=IFCINDEXEDPOLYGONALFACE((142,226,143)); +#754=IFCINDEXEDPOLYGONALFACE((226,142,227)); +#755=IFCINDEXEDPOLYGONALFACE((225,228,227)); +#756=IFCINDEXEDPOLYGONALFACE((181,221,186)); +#757=IFCINDEXEDPOLYGONALFACE((216,215,186)); +#758=IFCINDEXEDPOLYGONALFACE((208,215,211)); +#759=IFCINDEXEDPOLYGONALFACE((201,197,229)); +#760=IFCINDEXEDPOLYGONALFACE((227,228,201)); +#761=IFCINDEXEDPOLYGONALFACE((195,201,228)); +#762=IFCINDEXEDPOLYGONALFACE((230,227,229)); +#763=IFCINDEXEDPOLYGONALFACE((229,227,201)); +#764=IFCINDEXEDPOLYGONALFACE((224,223,231)); +#765=IFCINDEXEDPOLYGONALFACE((213,222,219)); +#766=IFCINDEXEDPOLYGONALFACE((232,200,207)); +#767=IFCINDEXEDPOLYGONALFACE((200,205,207)); +#768=IFCINDEXEDPOLYGONALFACE((205,200,202)); +#769=IFCINDEXEDPOLYGONALFACE((232,199,200)); +#770=IFCINDEXEDPOLYGONALFACE((196,198,233)); +#771=IFCINDEXEDPOLYGONALFACE((198,199,234)); +#772=IFCINDEXEDPOLYGONALFACE((225,204,202)); +#773=IFCINDEXEDPOLYGONALFACE((227,204,225)); +#774=IFCINDEXEDPOLYGONALFACE((230,197,235)); +#775=IFCINDEXEDPOLYGONALFACE((236,235,237)); +#776=IFCINDEXEDPOLYGONALFACE((235,239,238)); +#777=IFCINDEXEDPOLYGONALFACE((197,196,239)); +#778=IFCINDEXEDPOLYGONALFACE((236,230,235)); +#779=IFCINDEXEDPOLYGONALFACE((230,229,197)); +#780=IFCINDEXEDPOLYGONALFACE((211,224,231)); +#781=IFCINDEXEDPOLYGONALFACE((223,214,231)); +#782=IFCINDEXEDPOLYGONALFACE((214,211,231)); +#783=IFCINDEXEDPOLYGONALFACE((221,216,186)); +#784=IFCINDEXEDPOLYGONALFACE((186,215,240)); +#785=IFCINDEXEDPOLYGONALFACE((215,241,240)); +#786=IFCINDEXEDPOLYGONALFACE((209,212,242)); +#787=IFCINDEXEDPOLYGONALFACE((242,241,209)); +#788=IFCINDEXEDPOLYGONALFACE((209,241,208)); +#789=IFCINDEXEDPOLYGONALFACE((208,241,215)); +#790=IFCINDEXEDPOLYGONALFACE((196,233,243)); +#791=IFCINDEXEDPOLYGONALFACE((239,196,243)); +#792=IFCINDEXEDPOLYGONALFACE((243,238,239)); +#793=IFCINDEXEDPOLYGONALFACE((238,243,244)); +#794=IFCINDEXEDPOLYGONALFACE((195,228,225)); +#795=IFCINDEXEDPOLYGONALFACE((245,247,246)); +#796=IFCINDEXEDPOLYGONALFACE((248,247,245)); +#797=IFCINDEXEDPOLYGONALFACE((199,232,234)); +#798=IFCINDEXEDPOLYGONALFACE((197,239,235)); +#799=IFCINDEXEDPOLYGONALFACE((230,249,226)); +#800=IFCINDEXEDPOLYGONALFACE((230,236,249)); +#801=IFCINDEXEDPOLYGONALFACE((250,187,188)); +#802=IFCINDEXEDPOLYGONALFACE((152,187,151)); +#803=IFCINDEXEDPOLYGONALFACE((190,247,189)); +#804=IFCINDEXEDPOLYGONALFACE((188,192,250)); +#805=IFCINDEXEDPOLYGONALFACE((86,251,105)); +#806=IFCINDEXEDPOLYGONALFACE((250,251,86)); +#807=IFCINDEXEDPOLYGONALFACE((150,152,151)); +#808=IFCINDEXEDPOLYGONALFACE((151,250,86)); +#809=IFCINDEXEDPOLYGONALFACE((183,134,105)); +#810=IFCINDEXEDPOLYGONALFACE((251,183,105)); +#811=IFCINDEXEDPOLYGONALFACE((250,192,251)); +#812=IFCINDEXEDPOLYGONALFACE((187,250,151)); +#813=IFCINDEXEDPOLYGONALFACE((190,155,247)); +#814=IFCINDEXEDPOLYGONALFACE((155,164,182)); +#815=IFCINDEXEDPOLYGONALFACE((247,155,246)); +#816=IFCINDEXEDPOLYGONALFACE((163,182,164)); +#817=IFCINDEXEDPOLYGONALFACE((252,198,234)); +#818=IFCINDEXEDPOLYGONALFACE((244,243,233)); +#819=IFCINDEXEDPOLYGONALFACE((252,254,253)); +#820=IFCINDEXEDPOLYGONALFACE((252,253,244)); +#821=IFCINDEXEDPOLYGONALFACE((252,244,233)); +#822=IFCINDEXEDPOLYGONALFACE((198,252,233)); +#823=IFCINDEXEDPOLYGONALFACE((230,226,227)); +#824=IFCINDEXEDPOLYGONALFACE((256,257,255)); +#825=IFCINDEXEDPOLYGONALFACE((237,235,238)); +#826=IFCINDEXEDPOLYGONALFACE((210,211,214)); +#827=IFCINDEXEDPOLYGONALFACE((214,223,213)); +#828=IFCINDEXEDPOLYGONALFACE((258,210,214)); +#829=IFCINDEXEDPOLYGONALFACE((192,191,259)); +#830=IFCINDEXEDPOLYGONALFACE((192,183,251)); +#831=IFCINDEXEDPOLYGONALFACE((183,260,184)); +#832=IFCINDEXEDPOLYGONALFACE((261,183,192)); +#833=IFCINDEXEDPOLYGONALFACE((261,262,183)); +#834=IFCINDEXEDPOLYGONALFACE((259,261,192)); +#835=IFCINDEXEDPOLYGONALFACE((248,259,191)); +#836=IFCINDEXEDPOLYGONALFACE((189,247,191)); +#837=IFCINDEXEDPOLYGONALFACE((191,247,248)); +#838=IFCINDEXEDPOLYGONALFACE((238,257,237)); +#839=IFCINDEXEDPOLYGONALFACE((255,257,238)); +#840=IFCINDEXEDPOLYGONALFACE((244,255,238)); +#841=IFCINDEXEDPOLYGONALFACE((143,226,263)); +#842=IFCINDEXEDPOLYGONALFACE((263,90,143)); +#843=IFCINDEXEDPOLYGONALFACE((226,249,160)); +#844=IFCINDEXEDPOLYGONALFACE((160,263,226)); +#845=IFCINDEXEDPOLYGONALFACE((240,246,186)); +#846=IFCINDEXEDPOLYGONALFACE((245,259,248)); +#847=IFCINDEXEDPOLYGONALFACE((186,246,182)); +#848=IFCINDEXEDPOLYGONALFACE((234,175,184)); +#849=IFCINDEXEDPOLYGONALFACE((234,184,252)); +#850=IFCINDEXEDPOLYGONALFACE((204,141,203)); +#851=IFCINDEXEDPOLYGONALFACE((90,263,161)); +#852=IFCINDEXEDPOLYGONALFACE((256,237,257)); +#853=IFCINDEXEDPOLYGONALFACE((237,256,160)); +#854=IFCINDEXEDPOLYGONALFACE((249,236,237)); +#855=IFCINDEXEDPOLYGONALFACE((241,245,240)); +#856=IFCINDEXEDPOLYGONALFACE((242,245,241)); +#857=IFCINDEXEDPOLYGONALFACE((242,261,245)); +#858=IFCINDEXEDPOLYGONALFACE((242,212,262)); +#859=IFCINDEXEDPOLYGONALFACE((262,261,242)); +#860=IFCINDEXEDPOLYGONALFACE((262,212,258)); +#861=IFCINDEXEDPOLYGONALFACE((170,213,169)); +#862=IFCINDEXEDPOLYGONALFACE((245,261,259)); +#863=IFCINDEXEDPOLYGONALFACE((240,245,246)); +#864=IFCINDEXEDPOLYGONALFACE((182,246,155)); +#865=IFCINDEXEDPOLYGONALFACE((256,255,264)); +#866=IFCINDEXEDPOLYGONALFACE((249,237,160)); +#867=IFCINDEXEDPOLYGONALFACE((264,255,253)); +#868=IFCINDEXEDPOLYGONALFACE((255,244,253)); +#869=IFCINDEXEDPOLYGONALFACE((258,212,210)); +#870=IFCINDEXEDPOLYGONALFACE((206,139,207)); +#871=IFCINDEXEDPOLYGONALFACE((139,175,207)); +#872=IFCINDEXEDPOLYGONALFACE((234,232,207)); +#873=IFCINDEXEDPOLYGONALFACE((175,234,207)); +#874=IFCINDEXEDPOLYGONALFACE((58,90,89)); +#875=IFCINDEXEDPOLYGONALFACE((90,91,143)); +#876=IFCINDEXEDPOLYGONALFACE((142,141,204)); +#877=IFCINDEXEDPOLYGONALFACE((204,227,142)); +#878=IFCINDEXEDPOLYGONALFACE((133,134,184)); +#879=IFCINDEXEDPOLYGONALFACE((265,252,184)); +#880=IFCINDEXEDPOLYGONALFACE((254,252,265)); +#881=IFCINDEXEDPOLYGONALFACE((262,253,265)); +#882=IFCINDEXEDPOLYGONALFACE((265,184,260)); +#883=IFCINDEXEDPOLYGONALFACE((260,262,265)); +#884=IFCINDEXEDPOLYGONALFACE((183,262,260)); +#885=IFCINDEXEDPOLYGONALFACE((213,223,222)); +#886=IFCINDEXEDPOLYGONALFACE((93,203,141)); +#887=IFCINDEXEDPOLYGONALFACE((203,93,206)); +#888=IFCINDEXEDPOLYGONALFACE((93,140,206)); +#889=IFCINDEXEDPOLYGONALFACE((206,140,139)); +#890=IFCINDEXEDPOLYGONALFACE((99,140,93)); +#891=IFCINDEXEDPOLYGONALFACE((143,91,141)); +#892=IFCINDEXEDPOLYGONALFACE((253,254,265)); +#893=IFCINDEXEDPOLYGONALFACE((253,258,264)); +#894=IFCINDEXEDPOLYGONALFACE((258,253,262)); +#895=IFCINDEXEDPOLYGONALFACE((161,263,160)); +#896=IFCINDEXEDPOLYGONALFACE((160,131,161)); +#897=IFCINDEXEDPOLYGONALFACE((171,167,157)); +#898=IFCINDEXEDPOLYGONALFACE((127,171,136)); +#899=IFCINDEXEDPOLYGONALFACE((264,258,170)); +#900=IFCINDEXEDPOLYGONALFACE((258,214,170)); +#901=IFCINDEXEDPOLYGONALFACE((266,219,220)); +#902=IFCINDEXEDPOLYGONALFACE((169,219,266)); +#903=IFCINDEXEDPOLYGONALFACE((169,213,219)); +#904=IFCINDEXEDPOLYGONALFACE((168,266,103)); +#905=IFCINDEXEDPOLYGONALFACE((168,169,266)); +#906=IFCINDEXEDPOLYGONALFACE((179,177,180)); +#907=IFCINDEXEDPOLYGONALFACE((185,181,180)); +#908=IFCINDEXEDPOLYGONALFACE((185,221,181)); +#909=IFCINDEXEDPOLYGONALFACE((177,176,185)); +#910=IFCINDEXEDPOLYGONALFACE((193,221,185)); +#911=IFCINDEXEDPOLYGONALFACE((193,176,178)); +#912=IFCINDEXEDPOLYGONALFACE((193,194,267)); +#913=IFCINDEXEDPOLYGONALFACE((267,268,193)); +#914=IFCINDEXEDPOLYGONALFACE((185,176,193)); +#915=IFCINDEXEDPOLYGONALFACE((193,268,221)); +#916=IFCINDEXEDPOLYGONALFACE((218,221,268)); +#917=IFCINDEXEDPOLYGONALFACE((171,160,256)); +#918=IFCINDEXEDPOLYGONALFACE((160,171,127)); +#919=IFCINDEXEDPOLYGONALFACE((256,264,171)); +#920=IFCINDEXEDPOLYGONALFACE((171,170,167)); +#921=IFCINDEXEDPOLYGONALFACE((264,170,171)); +#922=IFCINDEXEDPOLYGONALFACE((108,269,107)); +#923=IFCINDEXEDPOLYGONALFACE((270,107,271)); +#924=IFCINDEXEDPOLYGONALFACE((25,271,272)); +#925=IFCINDEXEDPOLYGONALFACE((26,24,33)); +#926=IFCINDEXEDPOLYGONALFACE((273,108,106)); +#927=IFCINDEXEDPOLYGONALFACE((274,108,273)); +#928=IFCINDEXEDPOLYGONALFACE((273,275,274)); +#929=IFCINDEXEDPOLYGONALFACE((275,273,276)); +#930=IFCINDEXEDPOLYGONALFACE((278,279,277)); +#931=IFCINDEXEDPOLYGONALFACE((281,282,280)); +#932=IFCINDEXEDPOLYGONALFACE((280,282,87)); +#933=IFCINDEXEDPOLYGONALFACE((84,87,279)); +#934=IFCINDEXEDPOLYGONALFACE((283,279,284)); +#935=IFCINDEXEDPOLYGONALFACE((279,283,84)); +#936=IFCINDEXEDPOLYGONALFACE((84,80,82)); +#937=IFCINDEXEDPOLYGONALFACE((284,285,283)); +#938=IFCINDEXEDPOLYGONALFACE((283,285,286)); +#939=IFCINDEXEDPOLYGONALFACE((13,14,26)); +#940=IFCINDEXEDPOLYGONALFACE((37,13,35)); +#941=IFCINDEXEDPOLYGONALFACE((37,287,38)); +#942=IFCINDEXEDPOLYGONALFACE((15,288,106)); +#943=IFCINDEXEDPOLYGONALFACE((107,270,106)); +#944=IFCINDEXEDPOLYGONALFACE((288,273,106)); +#945=IFCINDEXEDPOLYGONALFACE((15,270,14)); +#946=IFCINDEXEDPOLYGONALFACE((14,270,271)); +#947=IFCINDEXEDPOLYGONALFACE((25,14,271)); +#948=IFCINDEXEDPOLYGONALFACE((272,271,289)); +#949=IFCINDEXEDPOLYGONALFACE((25,26,14)); +#950=IFCINDEXEDPOLYGONALFACE((290,291,102)); +#951=IFCINDEXEDPOLYGONALFACE((272,102,291)); +#952=IFCINDEXEDPOLYGONALFACE((104,102,272)); +#953=IFCINDEXEDPOLYGONALFACE((104,168,103)); +#954=IFCINDEXEDPOLYGONALFACE((289,104,272)); +#955=IFCINDEXEDPOLYGONALFACE((168,104,289)); +#956=IFCINDEXEDPOLYGONALFACE((289,271,157)); +#957=IFCINDEXEDPOLYGONALFACE((168,289,157)); +#958=IFCINDEXEDPOLYGONALFACE((157,167,168)); +#959=IFCINDEXEDPOLYGONALFACE((157,271,107)); +#960=IFCINDEXEDPOLYGONALFACE((290,292,291)); +#961=IFCINDEXEDPOLYGONALFACE((293,295,294)); +#962=IFCINDEXEDPOLYGONALFACE((290,295,293)); +#963=IFCINDEXEDPOLYGONALFACE((293,292,290)); +#964=IFCINDEXEDPOLYGONALFACE((102,296,290)); +#965=IFCINDEXEDPOLYGONALFACE((283,286,81)); +#966=IFCINDEXEDPOLYGONALFACE((79,81,286)); +#967=IFCINDEXEDPOLYGONALFACE((70,80,79)); +#968=IFCINDEXEDPOLYGONALFACE((72,80,70)); +#969=IFCINDEXEDPOLYGONALFACE((81,84,283)); +#970=IFCINDEXEDPOLYGONALFACE((81,80,84)); +#971=IFCINDEXEDPOLYGONALFACE((124,122,297)); +#972=IFCINDEXEDPOLYGONALFACE((297,122,298)); +#973=IFCINDEXEDPOLYGONALFACE((267,194,120)); +#974=IFCINDEXEDPOLYGONALFACE((120,121,267)); +#975=IFCINDEXEDPOLYGONALFACE((267,123,268)); +#976=IFCINDEXEDPOLYGONALFACE((121,123,267)); +#977=IFCINDEXEDPOLYGONALFACE((299,124,297)); +#978=IFCINDEXEDPOLYGONALFACE((268,123,218)); +#979=IFCINDEXEDPOLYGONALFACE((299,123,124)); +#980=IFCINDEXEDPOLYGONALFACE((300,297,298)); +#981=IFCINDEXEDPOLYGONALFACE((217,299,297)); +#982=IFCINDEXEDPOLYGONALFACE((218,123,299)); +#983=IFCINDEXEDPOLYGONALFACE((217,297,300)); +#984=IFCINDEXEDPOLYGONALFACE((220,217,300)); +#985=IFCINDEXEDPOLYGONALFACE((217,218,299)); +#986=IFCINDEXEDPOLYGONALFACE((266,220,295)); +#987=IFCINDEXEDPOLYGONALFACE((103,266,295)); +#988=IFCINDEXEDPOLYGONALFACE((102,103,296)); +#989=IFCINDEXEDPOLYGONALFACE((103,295,296)); +#990=IFCINDEXEDPOLYGONALFACE((295,220,301)); +#991=IFCINDEXEDPOLYGONALFACE((295,290,296)); +#992=IFCINDEXEDPOLYGONALFACE((294,302,293)); +#993=IFCINDEXEDPOLYGONALFACE((302,304,303)); +#994=IFCINDEXEDPOLYGONALFACE((301,294,295)); +#995=IFCINDEXEDPOLYGONALFACE((301,305,294)); +#996=IFCINDEXEDPOLYGONALFACE((294,304,302)); +#997=IFCINDEXEDPOLYGONALFACE((305,304,294)); +#998=IFCINDEXEDPOLYGONALFACE((306,304,305)); +#999=IFCINDEXEDPOLYGONALFACE((301,306,305)); +#1000=IFCINDEXEDPOLYGONALFACE((301,220,306)); +#1001=IFCINDEXEDPOLYGONALFACE((304,306,298)); +#1002=IFCINDEXEDPOLYGONALFACE((300,298,306)); +#1003=IFCINDEXEDPOLYGONALFACE((300,306,220)); +#1004=IFCINDEXEDPOLYGONALFACE((70,79,307)); +#1005=IFCINDEXEDPOLYGONALFACE((28,32,31)); +#1006=IFCINDEXEDPOLYGONALFACE((36,308,31)); +#1007=IFCINDEXEDPOLYGONALFACE((309,56,18)); +#1008=IFCINDEXEDPOLYGONALFACE((19,309,18)); +#1009=IFCINDEXEDPOLYGONALFACE((56,55,18)); +#1010=IFCINDEXEDPOLYGONALFACE((145,54,56)); +#1011=IFCINDEXEDPOLYGONALFACE((56,113,145)); +#1012=IFCINDEXEDPOLYGONALFACE((147,145,112)); +#1013=IFCINDEXEDPOLYGONALFACE((112,145,113)); +#1014=IFCINDEXEDPOLYGONALFACE((310,113,56)); +#1015=IFCINDEXEDPOLYGONALFACE((311,312,54)); +#1016=IFCINDEXEDPOLYGONALFACE((116,311,54)); +#1017=IFCINDEXEDPOLYGONALFACE((145,116,54)); +#1018=IFCINDEXEDPOLYGONALFACE((310,116,113)); +#1019=IFCINDEXEDPOLYGONALFACE((116,114,113)); +#1020=IFCINDEXEDPOLYGONALFACE((311,116,310)); +#1021=IFCINDEXEDPOLYGONALFACE((313,311,310)); +#1022=IFCINDEXEDPOLYGONALFACE((314,311,313)); +#1023=IFCINDEXEDPOLYGONALFACE((98,313,315)); +#1024=IFCINDEXEDPOLYGONALFACE((98,314,313)); +#1025=IFCINDEXEDPOLYGONALFACE((311,314,97)); +#1026=IFCINDEXEDPOLYGONALFACE((97,316,311)); +#1027=IFCINDEXEDPOLYGONALFACE((311,316,312)); +#1028=IFCINDEXEDPOLYGONALFACE((60,66,95)); +#1029=IFCINDEXEDPOLYGONALFACE((316,62,317)); +#1030=IFCINDEXEDPOLYGONALFACE((62,95,317)); +#1031=IFCINDEXEDPOLYGONALFACE((317,95,66)); +#1032=IFCINDEXEDPOLYGONALFACE((318,66,67)); +#1033=IFCINDEXEDPOLYGONALFACE((319,321,320)); +#1034=IFCINDEXEDPOLYGONALFACE((320,321,66)); +#1035=IFCINDEXEDPOLYGONALFACE((319,322,321)); +#1036=IFCINDEXEDPOLYGONALFACE((321,322,323)); +#1037=IFCINDEXEDPOLYGONALFACE((321,323,317)); +#1038=IFCINDEXEDPOLYGONALFACE((323,322,324)); +#1039=IFCINDEXEDPOLYGONALFACE((317,323,312)); +#1040=IFCINDEXEDPOLYGONALFACE((323,324,55)); +#1041=IFCINDEXEDPOLYGONALFACE((320,325,319)); +#1042=IFCINDEXEDPOLYGONALFACE((318,320,66)); +#1043=IFCINDEXEDPOLYGONALFACE((97,96,326)); +#1044=IFCINDEXEDPOLYGONALFACE((314,98,97)); +#1045=IFCINDEXEDPOLYGONALFACE((316,326,327)); +#1046=IFCINDEXEDPOLYGONALFACE((326,316,97)); +#1047=IFCINDEXEDPOLYGONALFACE((328,98,315)); +#1048=IFCINDEXEDPOLYGONALFACE((98,328,329)); +#1049=IFCINDEXEDPOLYGONALFACE((330,328,49)); +#1050=IFCINDEXEDPOLYGONALFACE((329,328,330)); +#1051=IFCINDEXEDPOLYGONALFACE((50,331,49)); +#1052=IFCINDEXEDPOLYGONALFACE((331,330,49)); +#1053=IFCINDEXEDPOLYGONALFACE((50,12,332)); +#1054=IFCINDEXEDPOLYGONALFACE((333,334,61)); +#1055=IFCINDEXEDPOLYGONALFACE((333,10,334)); +#1056=IFCINDEXEDPOLYGONALFACE((333,61,327)); +#1057=IFCINDEXEDPOLYGONALFACE((60,61,334)); +#1058=IFCINDEXEDPOLYGONALFACE((61,62,327)); +#1059=IFCINDEXEDPOLYGONALFACE((334,10,335)); +#1060=IFCINDEXEDPOLYGONALFACE((10,280,335)); +#1061=IFCINDEXEDPOLYGONALFACE((10,4,280)); +#1062=IFCINDEXEDPOLYGONALFACE((4,336,280)); +#1063=IFCINDEXEDPOLYGONALFACE((337,335,280)); +#1064=IFCINDEXEDPOLYGONALFACE((335,60,334)); +#1065=IFCINDEXEDPOLYGONALFACE((60,335,337)); +#1066=IFCINDEXEDPOLYGONALFACE((62,316,327)); +#1067=IFCINDEXEDPOLYGONALFACE((329,96,98)); +#1068=IFCINDEXEDPOLYGONALFACE((96,329,326)); +#1069=IFCINDEXEDPOLYGONALFACE((57,338,5)); +#1070=IFCINDEXEDPOLYGONALFACE((59,5,339)); +#1071=IFCINDEXEDPOLYGONALFACE((339,5,340)); +#1072=IFCINDEXEDPOLYGONALFACE((326,329,330)); +#1073=IFCINDEXEDPOLYGONALFACE((337,280,87)); +#1074=IFCINDEXEDPOLYGONALFACE((88,60,337)); +#1075=IFCINDEXEDPOLYGONALFACE((88,337,87)); +#1076=IFCINDEXEDPOLYGONALFACE((330,331,326)); +#1077=IFCINDEXEDPOLYGONALFACE((12,11,332)); +#1078=IFCINDEXEDPOLYGONALFACE((332,331,50)); +#1079=IFCINDEXEDPOLYGONALFACE((333,332,11)); +#1080=IFCINDEXEDPOLYGONALFACE((326,331,333)); +#1081=IFCINDEXEDPOLYGONALFACE((341,110,342)); +#1082=IFCINDEXEDPOLYGONALFACE((57,130,338)); +#1083=IFCINDEXEDPOLYGONALFACE((343,110,338)); +#1084=IFCINDEXEDPOLYGONALFACE((5,338,281)); +#1085=IFCINDEXEDPOLYGONALFACE((280,5,281)); +#1086=IFCINDEXEDPOLYGONALFACE((345,346,344)); +#1087=IFCINDEXEDPOLYGONALFACE((281,346,345)); +#1088=IFCINDEXEDPOLYGONALFACE((345,282,281)); +#1089=IFCINDEXEDPOLYGONALFACE((269,348,347)); +#1090=IFCINDEXEDPOLYGONALFACE((107,269,156)); +#1091=IFCINDEXEDPOLYGONALFACE((157,107,156)); +#1092=IFCINDEXEDPOLYGONALFACE((156,269,347)); +#1093=IFCINDEXEDPOLYGONALFACE((349,156,347)); +#1094=IFCINDEXEDPOLYGONALFACE((156,349,135)); +#1095=IFCINDEXEDPOLYGONALFACE((349,117,135)); +#1096=IFCINDEXEDPOLYGONALFACE((125,135,117)); +#1097=IFCINDEXEDPOLYGONALFACE((347,350,349)); +#1098=IFCINDEXEDPOLYGONALFACE((349,350,117)); +#1099=IFCINDEXEDPOLYGONALFACE((109,110,341)); +#1100=IFCINDEXEDPOLYGONALFACE((126,341,129)); +#1101=IFCINDEXEDPOLYGONALFACE((341,126,109)); +#1102=IFCINDEXEDPOLYGONALFACE((129,341,342)); +#1103=IFCINDEXEDPOLYGONALFACE((117,350,111)); +#1104=IFCINDEXEDPOLYGONALFACE((347,348,350)); +#1105=IFCINDEXEDPOLYGONALFACE((111,338,110)); +#1106=IFCINDEXEDPOLYGONALFACE((338,111,350)); +#1107=IFCINDEXEDPOLYGONALFACE((108,274,269)); +#1108=IFCINDEXEDPOLYGONALFACE((269,274,348)); +#1109=IFCINDEXEDPOLYGONALFACE((348,274,275)); +#1110=IFCINDEXEDPOLYGONALFACE((350,348,281)); +#1111=IFCINDEXEDPOLYGONALFACE((348,275,276)); +#1112=IFCINDEXEDPOLYGONALFACE((276,346,348)); +#1113=IFCINDEXEDPOLYGONALFACE((345,279,282)); +#1114=IFCINDEXEDPOLYGONALFACE((281,348,346)); +#1115=IFCINDEXEDPOLYGONALFACE((350,281,338)); +#1116=IFCINDEXEDPOLYGONALFACE((87,282,279)); +#1117=IFCINDEXEDPOLYGONALFACE((270,15,106)); +#1118=IFCINDEXEDPOLYGONALFACE((15,13,38)); +#1119=IFCINDEXEDPOLYGONALFACE((287,351,38)); +#1120=IFCINDEXEDPOLYGONALFACE((352,79,286)); +#1121=IFCINDEXEDPOLYGONALFACE((79,352,307)); +#1122=IFCINDEXEDPOLYGONALFACE((17,324,353)); +#1123=IFCINDEXEDPOLYGONALFACE((55,17,18)); +#1124=IFCINDEXEDPOLYGONALFACE((324,17,55)); +#1125=IFCINDEXEDPOLYGONALFACE((316,317,312)); +#1126=IFCINDEXEDPOLYGONALFACE((66,321,317)); +#1127=IFCINDEXEDPOLYGONALFACE((59,57,5)); +#1128=IFCINDEXEDPOLYGONALFACE((354,279,345)); +#1129=IFCINDEXEDPOLYGONALFACE((345,355,354)); +#1130=IFCINDEXEDPOLYGONALFACE((355,345,344)); +#1131=IFCINDEXEDPOLYGONALFACE((354,277,279)); +#1132=IFCINDEXEDPOLYGONALFACE((354,355,356)); +#1133=IFCINDEXEDPOLYGONALFACE((357,359,358)); +#1134=IFCINDEXEDPOLYGONALFACE((357,358,355)); +#1135=IFCINDEXEDPOLYGONALFACE((357,355,344)); +#1136=IFCINDEXEDPOLYGONALFACE((285,360,286)); +#1137=IFCINDEXEDPOLYGONALFACE((361,351,287)); +#1138=IFCINDEXEDPOLYGONALFACE((38,288,15)); +#1139=IFCINDEXEDPOLYGONALFACE((38,351,53)); +#1140=IFCINDEXEDPOLYGONALFACE((361,51,351)); +#1141=IFCINDEXEDPOLYGONALFACE((358,53,52)); +#1142=IFCINDEXEDPOLYGONALFACE((53,351,51)); +#1143=IFCINDEXEDPOLYGONALFACE((352,286,360)); +#1144=IFCINDEXEDPOLYGONALFACE((360,44,352)); +#1145=IFCINDEXEDPOLYGONALFACE((352,44,362)); +#1146=IFCINDEXEDPOLYGONALFACE((313,48,315)); +#1147=IFCINDEXEDPOLYGONALFACE((315,48,47)); +#1148=IFCINDEXEDPOLYGONALFACE((315,47,328)); +#1149=IFCINDEXEDPOLYGONALFACE((331,332,333)); +#1150=IFCINDEXEDPOLYGONALFACE((288,276,273)); +#1151=IFCINDEXEDPOLYGONALFACE((276,288,359)); +#1152=IFCINDEXEDPOLYGONALFACE((363,359,288)); +#1153=IFCINDEXEDPOLYGONALFACE((276,359,357)); +#1154=IFCINDEXEDPOLYGONALFACE((356,277,354)); +#1155=IFCINDEXEDPOLYGONALFACE((55,312,323)); +#1156=IFCINDEXEDPOLYGONALFACE((55,54,312)); +#1157=IFCINDEXEDPOLYGONALFACE((310,48,313)); +#1158=IFCINDEXEDPOLYGONALFACE((364,48,310)); +#1159=IFCINDEXEDPOLYGONALFACE((310,56,364)); +#1160=IFCINDEXEDPOLYGONALFACE((5,318,340)); +#1161=IFCINDEXEDPOLYGONALFACE((2,5,3)); +#1162=IFCINDEXEDPOLYGONALFACE((318,5,320)); +#1163=IFCINDEXEDPOLYGONALFACE((320,5,2)); +#1164=IFCINDEXEDPOLYGONALFACE((320,2,365)); +#1165=IFCINDEXEDPOLYGONALFACE((327,326,333)); +#1166=IFCINDEXEDPOLYGONALFACE((346,276,344)); +#1167=IFCINDEXEDPOLYGONALFACE((344,276,357)); +#1168=IFCINDEXEDPOLYGONALFACE((366,277,356)); +#1169=IFCINDEXEDPOLYGONALFACE((366,278,277)); +#1170=IFCINDEXEDPOLYGONALFACE((278,45,44)); +#1171=IFCINDEXEDPOLYGONALFACE((51,367,43)); +#1172=IFCINDEXEDPOLYGONALFACE((368,43,367)); +#1173=IFCINDEXEDPOLYGONALFACE((43,368,44)); +#1174=IFCINDEXEDPOLYGONALFACE((368,362,44)); +#1175=IFCINDEXEDPOLYGONALFACE((278,44,285)); +#1176=IFCINDEXEDPOLYGONALFACE((285,44,360)); +#1177=IFCINDEXEDPOLYGONALFACE((358,359,53)); +#1178=IFCINDEXEDPOLYGONALFACE((363,38,53)); +#1179=IFCINDEXEDPOLYGONALFACE((359,363,53)); +#1180=IFCINDEXEDPOLYGONALFACE((51,361,367)); +#1181=IFCINDEXEDPOLYGONALFACE((280,336,5)); +#1182=IFCINDEXEDPOLYGONALFACE((278,284,279)); +#1183=IFCINDEXEDPOLYGONALFACE((355,358,356)); +#1184=IFCINDEXEDPOLYGONALFACE((356,358,52)); +#1185=IFCINDEXEDPOLYGONALFACE((356,52,366)); +#1186=IFCINDEXEDPOLYGONALFACE((40,302,303)); +#1187=IFCINDEXEDPOLYGONALFACE((36,30,369)); +#1188=IFCINDEXEDPOLYGONALFACE((42,308,36)); +#1189=IFCINDEXEDPOLYGONALFACE((42,41,308)); +#1190=IFCINDEXEDPOLYGONALFACE((36,369,42)); +#1191=IFCINDEXEDPOLYGONALFACE((369,292,42)); +#1192=IFCINDEXEDPOLYGONALFACE((42,292,40)); +#1193=IFCINDEXEDPOLYGONALFACE((302,40,292)); +#1194=IFCINDEXEDPOLYGONALFACE((352,70,307)); +#1195=IFCINDEXEDPOLYGONALFACE((328,47,39)); +#1196=IFCINDEXEDPOLYGONALFACE((328,39,49)); +#1197=IFCINDEXEDPOLYGONALFACE((49,23,50)); +#1198=IFCINDEXEDPOLYGONALFACE((5,336,4)); +#1199=IFCINDEXEDPOLYGONALFACE((284,278,285)); +#1200=IFCINDEXEDPOLYGONALFACE((45,278,366)); +#1201=IFCINDEXEDPOLYGONALFACE((291,292,369)); +#1202=IFCINDEXEDPOLYGONALFACE((30,291,369)); +#1203=IFCINDEXEDPOLYGONALFACE((291,30,27)); +#1204=IFCINDEXEDPOLYGONALFACE((291,25,272)); +#1205=IFCINDEXEDPOLYGONALFACE((25,291,27)); +#1206=IFCINDEXEDPOLYGONALFACE((45,51,43)); +#1207=IFCINDEXEDPOLYGONALFACE((51,45,52)); +#1208=IFCINDEXEDPOLYGONALFACE((45,366,52)); +#1209=IFCINDEXEDPOLYGONALFACE((38,363,288)); +#1210=IFCINDEXEDPOLYGONALFACE((22,39,46)); +#1211=IFCINDEXEDPOLYGONALFACE((22,371,370)); +#1212=IFCINDEXEDPOLYGONALFACE((22,46,371)); +#1213=IFCINDEXEDPOLYGONALFACE((371,364,309)); +#1214=IFCINDEXEDPOLYGONALFACE((371,46,364)); +#1215=IFCINDEXEDPOLYGONALFACE((309,364,56)); +#1216=IFCINDEXEDPOLYGONALFACE((46,48,364)); +#1217=IFCINDEXEDPOLYGONALFACE((39,47,46)); +#1218=IFCINDEXEDPOLYGONALFACE((333,11,10)); +#1219=IFCINDEXEDPOLYGONALFACE((29,24,25)); +#1220=IFCINDEXEDPOLYGONALFACE((29,25,27)); +#1221=IFCINDEXEDPOLYGONALFACE((302,292,293)); +#1222=IFCINDEXEDPOLYGONALFACE((309,19,371)); +#1223=IFCINDEXEDPOLYGONALFACE((19,21,370)); +#1224=IFCINDEXEDPOLYGONALFACE((371,19,370)); +#1225=IFCINDEXEDPOLYGONALFACE((8,370,21)); +#1226=IFCINDEXEDPOLYGONALFACE((365,325,320)); +#1227=IFCINDEXEDPOLYGONALFACE((353,325,365)); +#1228=IFCINDEXEDPOLYGONALFACE((325,322,319)); +#1229=IFCINDEXEDPOLYGONALFACE((322,325,324)); +#1230=IFCINDEXEDPOLYGONALFACE((353,324,325)); +#1231=IFCINDEXEDPOLYGONALFACE((2,1,20)); +#1232=IFCINDEXEDPOLYGONALFACE((20,365,2)); +#1233=IFCINDEXEDPOLYGONALFACE((353,365,20)); +#1234=IFCINDEXEDPOLYGONALFACE((20,17,353)); +#1235=IFCINDEXEDPOLYGONALFACE((1,6,20)); +#1236=IFCINDEXEDPOLYGONALFACE((16,8,21)); +#1237=IFCINDEXEDPOLYGONALFACE((6,8,16)); +#1238=IFCINDEXEDPOLYGONALFACE((8,22,370)); +#1239=IFCINDEXEDPOLYGONALFACE((4,7,1)); +#1240=IFCINDEXEDPOLYGONALFACE((4,10,7)); +#1241=IFCINDEXEDPOLYGONALFACE((146,115,172)); +#1242=IFCINDEXEDPOLYGONALFACE((146,147,115)); +#1243=IFCINDEXEDPOLYGONALFACE((115,112,114)); +#1244=IFCINDEXEDPOLYGONALFACE((373,374,372)); +#1245=IFCINDEXEDPOLYGONALFACE((373,375,374)); +#1246=IFCINDEXEDPOLYGONALFACE((376,374,375)); +#1247=IFCINDEXEDPOLYGONALFACE((374,376,377)); +#1248=IFCINDEXEDPOLYGONALFACE((378,380,379)); +#1249=IFCINDEXEDPOLYGONALFACE((382,378,381)); +#1250=IFCINDEXEDPOLYGONALFACE((382,383,378)); +#1251=IFCINDEXEDPOLYGONALFACE((383,382,384)); +#1252=IFCINDEXEDPOLYGONALFACE((382,381,385)); +#1253=IFCINDEXEDPOLYGONALFACE((378,383,380)); +#1254=IFCINDEXEDPOLYGONALFACE((384,382,386)); +#1255=IFCINDEXEDPOLYGONALFACE((382,387,386)); +#1256=IFCINDEXEDPOLYGONALFACE((383,388,380)); +#1257=IFCINDEXEDPOLYGONALFACE((318,67,389)); +#1258=IFCINDEXEDPOLYGONALFACE((390,392,391)); +#1259=IFCINDEXEDPOLYGONALFACE((392,390,393)); +#1260=IFCINDEXEDPOLYGONALFACE((390,394,393)); +#1261=IFCINDEXEDPOLYGONALFACE((389,395,390)); +#1262=IFCINDEXEDPOLYGONALFACE((394,397,396)); +#1263=IFCINDEXEDPOLYGONALFACE((395,397,394)); +#1264=IFCINDEXEDPOLYGONALFACE((397,398,396)); +#1265=IFCINDEXEDPOLYGONALFACE((395,399,397)); +#1266=IFCINDEXEDPOLYGONALFACE((395,118,399)); +#1267=IFCINDEXEDPOLYGONALFACE((395,64,118)); +#1268=IFCINDEXEDPOLYGONALFACE((395,394,390)); +#1269=IFCINDEXEDPOLYGONALFACE((63,64,395)); +#1270=IFCINDEXEDPOLYGONALFACE((63,389,67)); +#1271=IFCINDEXEDPOLYGONALFACE((389,63,395)); +#1272=IFCINDEXEDPOLYGONALFACE((400,402,401)); +#1273=IFCINDEXEDPOLYGONALFACE((402,400,403)); +#1274=IFCINDEXEDPOLYGONALFACE((400,401,404)); +#1275=IFCINDEXEDPOLYGONALFACE((405,400,406)); +#1276=IFCINDEXEDPOLYGONALFACE((407,400,408)); +#1277=IFCINDEXEDPOLYGONALFACE((404,408,400)); +#1278=IFCINDEXEDPOLYGONALFACE((407,406,400)); +#1279=IFCINDEXEDPOLYGONALFACE((410,411,409)); +#1280=IFCINDEXEDPOLYGONALFACE((412,414,413)); +#1281=IFCINDEXEDPOLYGONALFACE((415,417,416)); +#1282=IFCINDEXEDPOLYGONALFACE((419,411,418)); +#1283=IFCINDEXEDPOLYGONALFACE((420,398,397)); +#1284=IFCINDEXEDPOLYGONALFACE((412,421,414)); +#1285=IFCINDEXEDPOLYGONALFACE((422,423,421)); +#1286=IFCINDEXEDPOLYGONALFACE((412,424,421)); +#1287=IFCINDEXEDPOLYGONALFACE((421,424,422)); +#1288=IFCINDEXEDPOLYGONALFACE((425,427,426)); +#1289=IFCINDEXEDPOLYGONALFACE((429,430,428)); +#1290=IFCINDEXEDPOLYGONALFACE((431,428,430)); +#1291=IFCINDEXEDPOLYGONALFACE((432,428,433)); +#1292=IFCINDEXEDPOLYGONALFACE((433,428,431)); +#1293=IFCINDEXEDPOLYGONALFACE((433,431,434)); +#1294=IFCINDEXEDPOLYGONALFACE((425,432,435)); +#1295=IFCINDEXEDPOLYGONALFACE((435,432,433)); +#1296=IFCINDEXEDPOLYGONALFACE((436,435,433)); +#1297=IFCINDEXEDPOLYGONALFACE((437,439,438)); +#1298=IFCINDEXEDPOLYGONALFACE((438,439,440)); +#1299=IFCINDEXEDPOLYGONALFACE((440,436,438)); +#1300=IFCINDEXEDPOLYGONALFACE((441,436,440)); +#1301=IFCINDEXEDPOLYGONALFACE((435,436,441)); +#1302=IFCINDEXEDPOLYGONALFACE((435,427,425)); +#1303=IFCINDEXEDPOLYGONALFACE((427,442,426)); +#1304=IFCINDEXEDPOLYGONALFACE((443,422,424)); +#1305=IFCINDEXEDPOLYGONALFACE((442,423,422)); +#1306=IFCINDEXEDPOLYGONALFACE((443,442,422)); +#1307=IFCINDEXEDPOLYGONALFACE((444,445,443)); +#1308=IFCINDEXEDPOLYGONALFACE((445,429,428)); +#1309=IFCINDEXEDPOLYGONALFACE((445,426,443)); +#1310=IFCINDEXEDPOLYGONALFACE((428,425,445)); +#1311=IFCINDEXEDPOLYGONALFACE((425,428,432)); +#1312=IFCINDEXEDPOLYGONALFACE((445,425,426)); +#1313=IFCINDEXEDPOLYGONALFACE((447,448,446)); +#1314=IFCINDEXEDPOLYGONALFACE((449,450,447)); +#1315=IFCINDEXEDPOLYGONALFACE((451,450,449)); +#1316=IFCINDEXEDPOLYGONALFACE((447,446,449)); +#1317=IFCINDEXEDPOLYGONALFACE((449,446,452)); +#1318=IFCINDEXEDPOLYGONALFACE((449,452,453)); +#1319=IFCINDEXEDPOLYGONALFACE((452,454,453)); +#1320=IFCINDEXEDPOLYGONALFACE((449,453,451)); +#1321=IFCINDEXEDPOLYGONALFACE((451,453,455)); +#1322=IFCINDEXEDPOLYGONALFACE((455,424,412)); +#1323=IFCINDEXEDPOLYGONALFACE((453,456,455)); +#1324=IFCINDEXEDPOLYGONALFACE((454,456,453)); +#1325=IFCINDEXEDPOLYGONALFACE((455,456,424)); +#1326=IFCINDEXEDPOLYGONALFACE((412,418,455)); +#1327=IFCINDEXEDPOLYGONALFACE((411,457,409)); +#1328=IFCINDEXEDPOLYGONALFACE((419,458,457)); +#1329=IFCINDEXEDPOLYGONALFACE((419,457,411)); +#1330=IFCINDEXEDPOLYGONALFACE((418,414,419)); +#1331=IFCINDEXEDPOLYGONALFACE((414,418,412)); +#1332=IFCINDEXEDPOLYGONALFACE((455,418,459)); +#1333=IFCINDEXEDPOLYGONALFACE((455,459,451)); +#1334=IFCINDEXEDPOLYGONALFACE((418,411,459)); +#1335=IFCINDEXEDPOLYGONALFACE((459,410,460)); +#1336=IFCINDEXEDPOLYGONALFACE((459,460,450)); +#1337=IFCINDEXEDPOLYGONALFACE((451,459,450)); +#1338=IFCINDEXEDPOLYGONALFACE((461,462,450)); +#1339=IFCINDEXEDPOLYGONALFACE((463,462,461)); +#1340=IFCINDEXEDPOLYGONALFACE((464,463,461)); +#1341=IFCINDEXEDPOLYGONALFACE((465,466,463)); +#1342=IFCINDEXEDPOLYGONALFACE((467,463,466)); +#1343=IFCINDEXEDPOLYGONALFACE((467,388,463)); +#1344=IFCINDEXEDPOLYGONALFACE((465,464,466)); +#1345=IFCINDEXEDPOLYGONALFACE((461,468,464)); +#1346=IFCINDEXEDPOLYGONALFACE((469,470,461)); +#1347=IFCINDEXEDPOLYGONALFACE((463,464,465)); +#1348=IFCINDEXEDPOLYGONALFACE((461,450,469)); +#1349=IFCINDEXEDPOLYGONALFACE((435,471,427)); +#1350=IFCINDEXEDPOLYGONALFACE((393,471,435)); +#1351=IFCINDEXEDPOLYGONALFACE((393,435,441)); +#1352=IFCINDEXEDPOLYGONALFACE((472,427,471)); +#1353=IFCINDEXEDPOLYGONALFACE((398,471,396)); +#1354=IFCINDEXEDPOLYGONALFACE((471,398,472)); +#1355=IFCINDEXEDPOLYGONALFACE((472,423,427)); +#1356=IFCINDEXEDPOLYGONALFACE((423,442,427)); +#1357=IFCINDEXEDPOLYGONALFACE((473,419,414)); +#1358=IFCINDEXEDPOLYGONALFACE((420,474,473)); +#1359=IFCINDEXEDPOLYGONALFACE((475,474,420)); +#1360=IFCINDEXEDPOLYGONALFACE((472,414,423)); +#1361=IFCINDEXEDPOLYGONALFACE((421,423,414)); +#1362=IFCINDEXEDPOLYGONALFACE((472,473,414)); +#1363=IFCINDEXEDPOLYGONALFACE((420,473,472)); +#1364=IFCINDEXEDPOLYGONALFACE((472,398,420)); +#1365=IFCINDEXEDPOLYGONALFACE((476,441,440)); +#1366=IFCINDEXEDPOLYGONALFACE((476,440,439)); +#1367=IFCINDEXEDPOLYGONALFACE((411,410,459)); +#1368=IFCINDEXEDPOLYGONALFACE((460,410,477)); +#1369=IFCINDEXEDPOLYGONALFACE((469,478,470)); +#1370=IFCINDEXEDPOLYGONALFACE((480,481,479)); +#1371=IFCINDEXEDPOLYGONALFACE((480,482,481)); +#1372=IFCINDEXEDPOLYGONALFACE((483,485,484)); +#1373=IFCINDEXEDPOLYGONALFACE((483,486,485)); +#1374=IFCINDEXEDPOLYGONALFACE((485,486,487)); +#1375=IFCINDEXEDPOLYGONALFACE((401,488,404)); +#1376=IFCINDEXEDPOLYGONALFACE((489,415,416)); +#1377=IFCINDEXEDPOLYGONALFACE((490,119,405)); +#1378=IFCINDEXEDPOLYGONALFACE((119,399,118)); +#1379=IFCINDEXEDPOLYGONALFACE((490,399,119)); +#1380=IFCINDEXEDPOLYGONALFACE((397,399,490)); +#1381=IFCINDEXEDPOLYGONALFACE((489,405,415)); +#1382=IFCINDEXEDPOLYGONALFACE((400,489,403)); +#1383=IFCINDEXEDPOLYGONALFACE((400,405,489)); +#1384=IFCINDEXEDPOLYGONALFACE((406,490,405)); +#1385=IFCINDEXEDPOLYGONALFACE((490,406,407)); +#1386=IFCINDEXEDPOLYGONALFACE((490,407,491)); +#1387=IFCINDEXEDPOLYGONALFACE((490,491,492)); +#1388=IFCINDEXEDPOLYGONALFACE((475,490,492)); +#1389=IFCINDEXEDPOLYGONALFACE((475,397,490)); +#1390=IFCINDEXEDPOLYGONALFACE((420,397,475)); +#1391=IFCINDEXEDPOLYGONALFACE((474,475,492)); +#1392=IFCINDEXEDPOLYGONALFACE((486,493,404)); +#1393=IFCINDEXEDPOLYGONALFACE((493,494,404)); +#1394=IFCINDEXEDPOLYGONALFACE((494,408,404)); +#1395=IFCINDEXEDPOLYGONALFACE((491,407,408)); +#1396=IFCINDEXEDPOLYGONALFACE((491,474,492)); +#1397=IFCINDEXEDPOLYGONALFACE((458,474,408)); +#1398=IFCINDEXEDPOLYGONALFACE((408,474,491)); +#1399=IFCINDEXEDPOLYGONALFACE((408,494,458)); +#1400=IFCINDEXEDPOLYGONALFACE((458,473,474)); +#1401=IFCINDEXEDPOLYGONALFACE((457,458,494)); +#1402=IFCINDEXEDPOLYGONALFACE((494,493,409)); +#1403=IFCINDEXEDPOLYGONALFACE((493,483,477)); +#1404=IFCINDEXEDPOLYGONALFACE((483,493,486)); +#1405=IFCINDEXEDPOLYGONALFACE((495,483,484)); +#1406=IFCINDEXEDPOLYGONALFACE((495,484,496)); +#1407=IFCINDEXEDPOLYGONALFACE((409,493,477)); +#1408=IFCINDEXEDPOLYGONALFACE((410,409,477)); +#1409=IFCINDEXEDPOLYGONALFACE((409,457,494)); +#1410=IFCINDEXEDPOLYGONALFACE((419,473,458)); +#1411=IFCINDEXEDPOLYGONALFACE((393,441,392)); +#1412=IFCINDEXEDPOLYGONALFACE((441,497,392)); +#1413=IFCINDEXEDPOLYGONALFACE((393,396,471)); +#1414=IFCINDEXEDPOLYGONALFACE((393,394,396)); +#1415=IFCINDEXEDPOLYGONALFACE((476,497,441)); +#1416=IFCINDEXEDPOLYGONALFACE((391,392,497)); +#1417=IFCINDEXEDPOLYGONALFACE((498,499,391)); +#1418=IFCINDEXEDPOLYGONALFACE((498,376,375)); +#1419=IFCINDEXEDPOLYGONALFACE((500,502,501)); +#1420=IFCINDEXEDPOLYGONALFACE((374,501,503)); +#1421=IFCINDEXEDPOLYGONALFACE((374,504,501)); +#1422=IFCINDEXEDPOLYGONALFACE((504,500,501)); +#1423=IFCINDEXEDPOLYGONALFACE((372,374,503)); +#1424=IFCINDEXEDPOLYGONALFACE((377,504,374)); +#1425=IFCINDEXEDPOLYGONALFACE((505,500,504)); +#1426=IFCINDEXEDPOLYGONALFACE((505,506,500)); +#1427=IFCINDEXEDPOLYGONALFACE((506,507,500)); +#1428=IFCINDEXEDPOLYGONALFACE((506,508,507)); +#1429=IFCINDEXEDPOLYGONALFACE((431,509,508)); +#1430=IFCINDEXEDPOLYGONALFACE((434,508,506)); +#1431=IFCINDEXEDPOLYGONALFACE((434,431,508)); +#1432=IFCINDEXEDPOLYGONALFACE((431,430,509)); +#1433=IFCINDEXEDPOLYGONALFACE((434,506,505)); +#1434=IFCINDEXEDPOLYGONALFACE((377,505,504)); +#1435=IFCINDEXEDPOLYGONALFACE((434,505,377)); +#1436=IFCINDEXEDPOLYGONALFACE((434,377,437)); +#1437=IFCINDEXEDPOLYGONALFACE((434,438,433)); +#1438=IFCINDEXEDPOLYGONALFACE((433,438,436)); +#1439=IFCINDEXEDPOLYGONALFACE((377,510,437)); +#1440=IFCINDEXEDPOLYGONALFACE((510,377,376)); +#1441=IFCINDEXEDPOLYGONALFACE((510,376,498)); +#1442=IFCINDEXEDPOLYGONALFACE((511,437,510)); +#1443=IFCINDEXEDPOLYGONALFACE((511,510,498)); +#1444=IFCINDEXEDPOLYGONALFACE((439,437,511)); +#1445=IFCINDEXEDPOLYGONALFACE((498,476,511)); +#1446=IFCINDEXEDPOLYGONALFACE((511,476,439)); +#1447=IFCINDEXEDPOLYGONALFACE((391,476,498)); +#1448=IFCINDEXEDPOLYGONALFACE((476,391,497)); +#1449=IFCINDEXEDPOLYGONALFACE((375,499,498)); +#1450=IFCINDEXEDPOLYGONALFACE((373,372,512)); +#1451=IFCINDEXEDPOLYGONALFACE((512,513,373)); +#1452=IFCINDEXEDPOLYGONALFACE((373,514,375)); +#1453=IFCINDEXEDPOLYGONALFACE((513,514,373)); +#1454=IFCINDEXEDPOLYGONALFACE((391,515,390)); +#1455=IFCINDEXEDPOLYGONALFACE((390,515,389)); +#1456=IFCINDEXEDPOLYGONALFACE((389,515,318)); +#1457=IFCINDEXEDPOLYGONALFACE((499,515,391)); +#1458=IFCINDEXEDPOLYGONALFACE((517,518,516)); +#1459=IFCINDEXEDPOLYGONALFACE((519,520,516)); +#1460=IFCINDEXEDPOLYGONALFACE((512,517,513)); +#1461=IFCINDEXEDPOLYGONALFACE((513,517,521)); +#1462=IFCINDEXEDPOLYGONALFACE((513,521,522)); +#1463=IFCINDEXEDPOLYGONALFACE((375,514,499)); +#1464=IFCINDEXEDPOLYGONALFACE((523,514,522)); +#1465=IFCINDEXEDPOLYGONALFACE((514,523,499)); +#1466=IFCINDEXEDPOLYGONALFACE((499,523,515)); +#1467=IFCINDEXEDPOLYGONALFACE((524,525,522)); +#1468=IFCINDEXEDPOLYGONALFACE((522,521,524)); +#1469=IFCINDEXEDPOLYGONALFACE((520,526,521)); +#1470=IFCINDEXEDPOLYGONALFACE((521,527,524)); +#1471=IFCINDEXEDPOLYGONALFACE((526,520,519)); +#1472=IFCINDEXEDPOLYGONALFACE((519,488,526)); +#1473=IFCINDEXEDPOLYGONALFACE((521,526,527)); +#1474=IFCINDEXEDPOLYGONALFACE((524,527,416)); +#1475=IFCINDEXEDPOLYGONALFACE((416,417,524)); +#1476=IFCINDEXEDPOLYGONALFACE((526,401,527)); +#1477=IFCINDEXEDPOLYGONALFACE((401,526,488)); +#1478=IFCINDEXEDPOLYGONALFACE((527,403,416)); +#1479=IFCINDEXEDPOLYGONALFACE((527,402,403)); +#1480=IFCINDEXEDPOLYGONALFACE((401,402,527)); +#1481=IFCINDEXEDPOLYGONALFACE((518,528,516)); +#1482=IFCINDEXEDPOLYGONALFACE((516,528,519)); +#1483=IFCINDEXEDPOLYGONALFACE((529,519,528)); +#1484=IFCINDEXEDPOLYGONALFACE((487,519,529)); +#1485=IFCINDEXEDPOLYGONALFACE((519,487,488)); +#1486=IFCINDEXEDPOLYGONALFACE((487,404,488)); +#1487=IFCINDEXEDPOLYGONALFACE((523,522,525)); +#1488=IFCINDEXEDPOLYGONALFACE((417,59,339)); +#1489=IFCINDEXEDPOLYGONALFACE((417,339,525)); +#1490=IFCINDEXEDPOLYGONALFACE((339,340,525)); +#1491=IFCINDEXEDPOLYGONALFACE((524,417,525)); +#1492=IFCINDEXEDPOLYGONALFACE((415,59,417)); +#1493=IFCINDEXEDPOLYGONALFACE((101,59,415)); +#1494=IFCINDEXEDPOLYGONALFACE((101,405,119)); +#1495=IFCINDEXEDPOLYGONALFACE((101,415,405)); +#1496=IFCINDEXEDPOLYGONALFACE((443,426,442)); +#1497=IFCINDEXEDPOLYGONALFACE((480,479,530)); +#1498=IFCINDEXEDPOLYGONALFACE((496,481,495)); +#1499=IFCINDEXEDPOLYGONALFACE((481,482,495)); +#1500=IFCINDEXEDPOLYGONALFACE((483,482,480)); +#1501=IFCINDEXEDPOLYGONALFACE((480,477,483)); +#1502=IFCINDEXEDPOLYGONALFACE((477,480,460)); +#1503=IFCINDEXEDPOLYGONALFACE((460,480,531)); +#1504=IFCINDEXEDPOLYGONALFACE((531,478,460)); +#1505=IFCINDEXEDPOLYGONALFACE((460,478,450)); +#1506=IFCINDEXEDPOLYGONALFACE((450,478,469)); +#1507=IFCINDEXEDPOLYGONALFACE((495,482,483)); +#1508=IFCINDEXEDPOLYGONALFACE((532,533,485)); +#1509=IFCINDEXEDPOLYGONALFACE((534,532,485)); +#1510=IFCINDEXEDPOLYGONALFACE((485,533,484)); +#1511=IFCINDEXEDPOLYGONALFACE((533,532,535)); +#1512=IFCINDEXEDPOLYGONALFACE((535,532,536)); +#1513=IFCINDEXEDPOLYGONALFACE((487,486,404)); +#1514=IFCINDEXEDPOLYGONALFACE((515,523,318)); +#1515=IFCINDEXEDPOLYGONALFACE((525,340,318)); +#1516=IFCINDEXEDPOLYGONALFACE((318,523,525)); +#1517=IFCINDEXEDPOLYGONALFACE((537,470,478)); +#1518=IFCINDEXEDPOLYGONALFACE((537,468,470)); +#1519=IFCINDEXEDPOLYGONALFACE((538,468,537)); +#1520=IFCINDEXEDPOLYGONALFACE((538,385,539)); +#1521=IFCINDEXEDPOLYGONALFACE((386,387,385)); +#1522=IFCINDEXEDPOLYGONALFACE((385,387,382)); +#1523=IFCINDEXEDPOLYGONALFACE((538,386,385)); +#1524=IFCINDEXEDPOLYGONALFACE((536,386,535)); +#1525=IFCINDEXEDPOLYGONALFACE((540,535,386)); +#1526=IFCINDEXEDPOLYGONALFACE((541,535,540)); +#1527=IFCINDEXEDPOLYGONALFACE((541,533,535)); +#1528=IFCINDEXEDPOLYGONALFACE((533,496,484)); +#1529=IFCINDEXEDPOLYGONALFACE((496,533,541)); +#1530=IFCINDEXEDPOLYGONALFACE((496,541,479)); +#1531=IFCINDEXEDPOLYGONALFACE((481,496,479)); +#1532=IFCINDEXEDPOLYGONALFACE((530,542,531)); +#1533=IFCINDEXEDPOLYGONALFACE((537,478,531)); +#1534=IFCINDEXEDPOLYGONALFACE((542,541,537)); +#1535=IFCINDEXEDPOLYGONALFACE((540,386,538)); +#1536=IFCINDEXEDPOLYGONALFACE((537,541,540)); +#1537=IFCINDEXEDPOLYGONALFACE((530,479,541)); +#1538=IFCINDEXEDPOLYGONALFACE((530,541,542)); +#1539=IFCINDEXEDPOLYGONALFACE((542,537,531)); +#1540=IFCINDEXEDPOLYGONALFACE((537,540,538)); +#1541=IFCINDEXEDPOLYGONALFACE((531,480,530)); +#1542=IFCINDEXEDPOLYGONALFACE((529,528,543)); +#1543=IFCINDEXEDPOLYGONALFACE((544,534,543)); +#1544=IFCINDEXEDPOLYGONALFACE((543,534,529)); +#1545=IFCINDEXEDPOLYGONALFACE((534,485,487)); +#1546=IFCINDEXEDPOLYGONALFACE((529,534,487)); +#1547=IFCINDEXEDPOLYGONALFACE((536,532,545)); +#1548=IFCINDEXEDPOLYGONALFACE((544,543,546)); +#1549=IFCINDEXEDPOLYGONALFACE((534,544,545)); +#1550=IFCINDEXEDPOLYGONALFACE((386,536,545)); +#1551=IFCINDEXEDPOLYGONALFACE((545,532,534)); +#1552=IFCINDEXEDPOLYGONALFACE((384,545,544)); +#1553=IFCINDEXEDPOLYGONALFACE((545,384,386)); +#1554=IFCINDEXEDPOLYGONALFACE((388,547,380)); +#1555=IFCINDEXEDPOLYGONALFACE((544,548,384)); +#1556=IFCINDEXEDPOLYGONALFACE((544,448,548)); +#1557=IFCINDEXEDPOLYGONALFACE((548,388,383)); +#1558=IFCINDEXEDPOLYGONALFACE((548,463,388)); +#1559=IFCINDEXEDPOLYGONALFACE((548,447,463)); +#1560=IFCINDEXEDPOLYGONALFACE((447,548,448)); +#1561=IFCINDEXEDPOLYGONALFACE((383,384,548)); +#1562=IFCINDEXEDPOLYGONALFACE((463,450,462)); +#1563=IFCINDEXEDPOLYGONALFACE((463,447,450)); +#1564=IFCINDEXEDPOLYGONALFACE((461,470,468)); +#1565=IFCINDEXEDPOLYGONALFACE((468,549,466)); +#1566=IFCINDEXEDPOLYGONALFACE((379,539,378)); +#1567=IFCINDEXEDPOLYGONALFACE((385,381,378)); +#1568=IFCINDEXEDPOLYGONALFACE((539,385,378)); +#1569=IFCINDEXEDPOLYGONALFACE((379,549,539)); +#1570=IFCINDEXEDPOLYGONALFACE((538,549,468)); +#1571=IFCINDEXEDPOLYGONALFACE((549,538,539)); +#1572=IFCINDEXEDPOLYGONALFACE((466,464,468)); +#1573=IFCINDEXEDPOLYGONALFACE((517,520,521)); +#1574=IFCINDEXEDPOLYGONALFACE((550,456,454)); +#1575=IFCINDEXEDPOLYGONALFACE((454,452,551)); +#1576=IFCINDEXEDPOLYGONALFACE((551,552,454)); +#1577=IFCINDEXEDPOLYGONALFACE((553,552,551)); +#1578=IFCINDEXEDPOLYGONALFACE((554,550,454)); +#1579=IFCINDEXEDPOLYGONALFACE((454,552,554)); +#1580=IFCINDEXEDPOLYGONALFACE((552,553,555)); +#1581=IFCINDEXEDPOLYGONALFACE((502,553,501)); +#1582=IFCINDEXEDPOLYGONALFACE((502,555,553)); +#1583=IFCINDEXEDPOLYGONALFACE((556,552,555)); +#1584=IFCINDEXEDPOLYGONALFACE((556,554,552)); +#1585=IFCINDEXEDPOLYGONALFACE((456,550,443)); +#1586=IFCINDEXEDPOLYGONALFACE((550,556,557)); +#1587=IFCINDEXEDPOLYGONALFACE((554,556,550)); +#1588=IFCINDEXEDPOLYGONALFACE((555,502,508)); +#1589=IFCINDEXEDPOLYGONALFACE((507,502,500)); +#1590=IFCINDEXEDPOLYGONALFACE((502,507,508)); +#1591=IFCINDEXEDPOLYGONALFACE((556,444,557)); +#1592=IFCINDEXEDPOLYGONALFACE((557,444,443)); +#1593=IFCINDEXEDPOLYGONALFACE((429,444,556)); +#1594=IFCINDEXEDPOLYGONALFACE((509,429,556)); +#1595=IFCINDEXEDPOLYGONALFACE((509,556,555)); +#1596=IFCINDEXEDPOLYGONALFACE((509,555,508)); +#1597=IFCINDEXEDPOLYGONALFACE((430,429,509)); +#1598=IFCINDEXEDPOLYGONALFACE((429,445,444)); +#1599=IFCINDEXEDPOLYGONALFACE((456,443,424)); +#1600=IFCINDEXEDPOLYGONALFACE((437,438,434)); +#1601=IFCINDEXEDPOLYGONALFACE((467,466,549)); +#1602=IFCINDEXEDPOLYGONALFACE((467,547,388)); +#1603=IFCINDEXEDPOLYGONALFACE((380,549,379)); +#1604=IFCINDEXEDPOLYGONALFACE((467,549,380)); +#1605=IFCINDEXEDPOLYGONALFACE((467,380,547)); +#1606=IFCINDEXEDPOLYGONALFACE((522,514,513)); +#1607=IFCINDEXEDPOLYGONALFACE((550,557,443)); +#1608=IFCINDEXEDPOLYGONALFACE((516,520,517)); +#1609=IFCINDEXEDPOLYGONALFACE((543,528,518)); +#1610=IFCINDEXEDPOLYGONALFACE((543,518,558)); +#1611=IFCINDEXEDPOLYGONALFACE((546,543,558)); +#1612=IFCINDEXEDPOLYGONALFACE((559,544,546)); +#1613=IFCINDEXEDPOLYGONALFACE((448,544,559)); +#1614=IFCINDEXEDPOLYGONALFACE((448,559,446)); +#1615=IFCINDEXEDPOLYGONALFACE((446,559,452)); +#1616=IFCINDEXEDPOLYGONALFACE((452,559,551)); +#1617=IFCINDEXEDPOLYGONALFACE((546,560,559)); +#1618=IFCINDEXEDPOLYGONALFACE((559,561,551)); +#1619=IFCINDEXEDPOLYGONALFACE((559,560,561)); +#1620=IFCINDEXEDPOLYGONALFACE((561,553,551)); +#1621=IFCINDEXEDPOLYGONALFACE((560,503,501)); +#1622=IFCINDEXEDPOLYGONALFACE((561,501,553)); +#1623=IFCINDEXEDPOLYGONALFACE((560,501,561)); +#1624=IFCINDEXEDPOLYGONALFACE((546,558,560)); +#1625=IFCINDEXEDPOLYGONALFACE((503,512,372)); +#1626=IFCINDEXEDPOLYGONALFACE((512,503,562)); +#1627=IFCINDEXEDPOLYGONALFACE((518,512,562)); +#1628=IFCINDEXEDPOLYGONALFACE((518,562,558)); +#1629=IFCINDEXEDPOLYGONALFACE((512,518,517)); +#1630=IFCINDEXEDPOLYGONALFACE((558,503,560)); +#1631=IFCINDEXEDPOLYGONALFACE((558,562,503)); +#1632=IFCINDEXEDPOLYGONALFACE((112,115,147)); +#1633=IFCINDEXEDPOLYGONALFACE((129,342,130)); +#1634=IFCINDEXEDPOLYGONALFACE((130,343,338)); +#1635=IFCINDEXEDPOLYGONALFACE((342,343,130)); +#1636=IFCINDEXEDPOLYGONALFACE((342,110,343)); +#1637=IFCCARTESIANPOINTLIST3D(((-234.259872436523,26.0206451416016,1268.17810058594),(-100.622589111328,89.1703338623047,1358.38659667969),(-174.998931884766,84.4214172363281,1289.44897460938),(-196.189651489258,26.9298496246338,1213.30908203125),(-72.8485260009766,73.3184814453125,1323.95959472656),(-345.774108886719,-49.3664436340332,1298.96325683594),(-326.359466552734,-37.9339141845703,1239.11169433594),(-455.8876953125,-59.9907608032227,1240.53430175781),(-361.961212158203,-40.2571678161621,1210.66223144531),(-294.290893554688,-52.8562927246094,1164.40112304688),(-360.114013671875,-77.8896331787109,1158.00598144531),(-420.540679931641,-112.721755981445,1106.28112792969),(-174.020874023438,182.901596069336,125.607543945312),(-30.1420288085938,192.876953125,334.829833984375),(-32.1650161743164,144.114334106445,287.031280517578),(-376.397827148438,-63.5164642333984,1395.50573730469),(-343.002655029297,-18.6739673614502,1456.07995605469),(-384.862976074219,-40.3055877685547,1492.75451660156),(-429.320373535156,-51.7892723083496,1412.16223144531),(-249.589797973633,23.7823734283447,1320.48657226562),(-436.293060302734,-28.2356185913086,1331.52783203125),(-389.614166259766,-99.2635650634766,1263.74230957031),(-352.909545898438,-88.9738235473633,1197.65832519531),(-127.19367980957,187.262359619141,91.7172393798828),(0.550186991691589,184.988464355469,271.917114257812),(-109.129829406738,208.982406616211,214.271453857422),(-53.3709259033203,292.429107666016,102.230285644531),(-109.36164855957,279.544677734375,10.0952625274658),(-58.5215110778809,218.701721191406,111.573677062988),(-1.16115438938141,306.455474853516,97.5913391113281),(-42.5377960205078,333.894561767578,13.5360069274902),(-77.3127822875977,341.250793457031,32.6841278076172),(-182.928314208984,244.205703735352,47.796760559082),(-196.406845092773,277.48388671875,11.2913303375244),(-260.438049316406,231.842819213867,8.3146333694458),(48.1291275024414,339.953186035156,44.5714645385742),(-281.729095458984,187.189514160156,8.92827892303467),(-130.312393188477,136.459121704102,138.386489868164),(-323.713745117188,-86.0534439086914,1288.24206542969),(172.426818847656,279.070587158203,6.77609443664551),(130.639266967773,244.967727661133,9.28795719146729),(57.8884468078613,316.890777587891,88.9064865112305),(-205.648010253906,56.7607955932617,148.7763671875),(-244.74821472168,-38.6621055603027,54.3230133056641),(-172.18278503418,29.0755653381348,256.801666259766),(-354.347503662109,-86.4489898681641,1363.07922363281),(-289.320922851562,-91.2859344482422,1351.06079101562),(-262.40087890625,-60.1678237915039,1390.63830566406),(-359.798614501953,-168.317199707031,1270.29931640625),(-371.118041992188,-201.981323242188,1146.25),(-162.621124267578,58.8279685974121,127.04443359375),(-105.6953125,35.7283096313477,349.046142578125),(-100.901763916016,88.7468490600586,283.423614501953),(-259.459259033203,-151.988891601562,1597.00500488281),(-325.426116943359,-79.227409362793,1552.19775390625),(-354.647064208984,-109.775039672852,1577.22155761719),(105.394004821777,47.0450134277344,1218.7939453125),(143.695266723633,95.4449157714844,1310.88317871094),(53.1794776916504,88.2259674072266,1411.14013671875),(-93.6701202392578,-102.186378479004,1350.81909179688),(-206.495086669922,-108.140785217285,1309.5009765625),(-166.171630859375,-129.207290649414,1375.42114257812),(8.2303991317749,-45.5022201538086,1443.43676757812),(67.4000854492188,-52.1921157836914,1453.99145507812),(32.3846015930176,-81.7621688842773,1425.6611328125),(-65.1914978027344,-40.7295989990234,1440.2666015625),(-27.6138095855713,-22.5396099090576,1451.76342773438),(-22.4503383636475,-90.6965103149414,1425.47521972656),(45.200626373291,-108.750053405762,1411.40100097656),(-253.74870300293,-173.990127563477,2.80772709846497),(-188.811676025391,-197.434387207031,1.62604331970215),(-141.394714355469,-154.38639831543,84.325080871582),(-144.390380859375,-206.764877319336,46.6824226379395),(-67.0683288574219,-231.445053100586,75.1596755981445),(-106.494743347168,-205.124084472656,227.876815795898),(-69.1273651123047,-227.695098876953,210.034652709961),(-132.834976196289,-221.483734130859,2.70664954185486),(-59.9483108520508,-251.556686401367,0.433635205030441),(-238.044021606445,-158.393890380859,82.6289749145508),(-145.278305053711,-122.224197387695,236.698623657227),(-168.42936706543,-143.536834716797,230.725936889648),(-111.595741271973,-190.565505981445,621.587341308594),(-111.56485748291,-168.792770385742,1014.46014404297),(-161.771865844727,-131.279891967773,781.861999511719),(-82.9896545410156,-235.451568603516,508.337219238281),(-59.3738021850586,-218.05924987793,984.255981445312),(-109.838088989258,-75.3973236083984,1174.73388671875),(-58.3983879089355,-137.72998046875,1348.484375),(169.098037719727,2.49960660934448,1199.70971679688),(268.213012695312,28.7850322723389,1155.13781738281),(212.121215820312,62.7963714599609,1281.23571777344),(211.473937988281,102.82096862793,1365.90966796875),(221.213317871094,67.7997436523438,1390.20971679688),(149.427856445312,75.5331573486328,1434.66577148438),(-133.805435180664,-96.4271621704102,1434.79077148438),(-296.830108642578,-273.018890380859,1347.32861328125),(-314.549163818359,-224.977249145508,1438.04370117188),(-338.938873291016,-283.220733642578,1426.24877929688),(234.905609130859,18.5421848297119,1443.41723632812),(134.689010620117,-7.08544301986694,1468.84875488281),(86.4454803466797,55.6466102600098,1456.04138183594),(170.499710083008,196.93928527832,367.807922363281),(223.427978515625,47.2690734863281,505.998291015625),(144.529571533203,132.24153137207,480.139312744141),(-4.70727396011353,-171.973083496094,1254.0078125),(57.016429901123,77.0934143066406,557.01171875),(114.453987121582,47.4405632019043,725.84326171875),(61.3384208679199,42.6156234741211,595.980163574219),(130.760726928711,59.0489387512207,1004.70300292969),(56.1174049377441,61.3588256835938,1078.35754394531),(26.7942752838135,29.3527793884277,1021.92547607422),(-172.117828369141,-293.532836914062,1774.51904296875),(-304.544769287109,-234.783432006836,1692.11389160156),(-185.501373291016,-328.179321289062,1730.30700683594),(-87.196403503418,-337.01123046875,1739.81225585938),(-198.697662353516,-296.360534667969,1701.09802246094),(105.671966552734,29.1725769042969,917.033935546875),(87.0264358520508,-22.3563709259033,1459.66491699219),(76.4012222290039,29.8333110809326,1467.81311035156),(282.479675292969,39.4475975036621,5.19572496414185),(266.129943847656,60.9157752990723,31.7507934570312),(222.847778320312,98.9792251586914,9.44344806671143),(209.856155395508,-55.9840049743652,111.861015319824),(194.558685302734,-8.1816291809082,83.5947036743164),(192.744766235352,-30.3785724639893,1010.16217041016),(190.087020874023,-58.6640357971191,1051.10400390625),(214.105239868164,-109.607299804688,1119.47082519531),(179.220672607422,-46.2212982177734,1139.26123046875),(169.943771362305,-14.1302680969238,1103.96520996094),(100.925979614258,33.8412933349609,1185.6650390625),(209.844131469727,-79.9096145629883,1174.33752441406),(147.470733642578,-146.801666259766,1353.03466796875),(148.167739868164,-159.253921508789,1260.08312988281),(71.6631164550781,-188.811111450195,1225.95178222656),(189.147521972656,26.4641056060791,939.224365234375),(221.66455078125,-35.0480766296387,969.51953125),(236.265380859375,-77.077262878418,1407.22290039062),(182.103866577148,-79.6638793945312,1352.71252441406),(262.556671142578,-77.5801849365234,1323.5341796875),(240.757186889648,-52.3165016174316,1378.26892089844),(341.992980957031,-4.5083475112915,1283.97143554688),(368.147918701172,-18.4431190490723,1164.26440429688),(280.039947509766,23.3627166748047,1210.63989257812),(-64.0344543457031,-289.496734619141,1717.87084960938),(-195.189376831055,-270.101776123047,1744.28186035156),(-79.1865615844727,-270.032684326172,1757.63195800781),(-97.2441253662109,-273.117431640625,1768.56274414062),(-44.3893508911133,-212.602279663086,1.46154367923737),(-40.7168769836426,-180.231002807617,97.5091171264648),(-53.893611907959,-179.648147583008,295.200317382812),(-54.0461082458496,-212.207458496094,581.433898925781),(14.6313772201538,-208.832733154297,245.684219360352),(-6.76602649688721,-189.713928222656,124.969528198242),(49.1640930175781,-242.114410400391,187.770812988281),(61.3618621826172,-249.659164428711,318.480438232422),(148.024612426758,21.3876552581787,832.409240722656),(200.182968139648,9.91830348968506,866.363342285156),(0.506551861763,-227.3544921875,14.836483001709),(50.2909622192383,-209.27067565918,20.0473861694336),(279.526977539062,-96.9463882446289,1058.3662109375),(266.403381347656,-7.66716909408569,1100.16198730469),(36.8900566101074,-197.712738037109,199.115936279297),(78.588737487793,-183.978469848633,216.088439941406),(78.0460357666016,-208.343688964844,276.298858642578),(52.7577018737793,-166.980392456055,154.516387939453),(56.6364250183105,-176.391799926758,2.56371259689331),(250.200347900391,-60.670539855957,885.808044433594),(241.896118164062,17.7724590301514,594.098205566406),(228.815887451172,-78.8302917480469,654.730285644531),(219.107696533203,-152.897079467773,822.713989257812),(230.230560302734,-104.30167388916,1017.83447265625),(-69.2549896240234,-322.792938232422,1743.02453613281),(113.45849609375,-221.281433105469,3.2618362903595),(185.49674987793,-61.1086311340332,1258.38232421875),(212.140441894531,-112.815856933594,1266.68200683594),(212.231842041016,-150.177886962891,52.884090423584),(193.43310546875,-144.418380737305,6.11850070953369),(241.940994262695,-107.766044616699,2.80753087997437),(135.199066162109,-151.250183105469,48.6296844482422),(107.207931518555,-147.557800292969,237.379272460938),(96.7631912231445,-181.811370849609,330.823852539062),(125.37393951416,-234.321640014648,272.505432128906),(58.7994117736816,-215.066452026367,1160.4033203125),(127.874649047852,-206.078918457031,1266.04150390625),(154.300064086914,-157.079208374023,260.234039306641),(127.502777099609,-215.131774902344,330.523193359375),(34.1889533996582,-217.497528076172,378.504669189453),(34.7865982055664,-243.546188354492,594.079650878906),(12.0976400375366,-284.996856689453,461.390014648438),(-15.8745355606079,-292.565551757812,294.191802978516),(27.2890815734863,-280.032379150391,679.055358886719),(67.6122741699219,-245.154312133789,1034.18212890625),(192.588134765625,-109.331016540527,139.856369018555),(214.282150268555,-65.5875244140625,4.3946967124939),(485.333129882812,-101.890800476074,1143.18615722656),(364.089996337891,-164.960968017578,1112.13928222656),(436.542053222656,-113.766792297363,1073.89306640625),(363.7236328125,-175.23046875,1209.75769042969),(347.211181640625,-154.081100463867,1253.51989746094),(403.778869628906,-139.319763183594,1255.37756347656),(481.695831298828,-56.3661956787109,1070.92749023438),(445.47021484375,-92.0810852050781,1223.81237792969),(358.431976318359,-60.4159889221191,1287.80786132812),(408.411254882812,-56.1850814819336,1259.23962402344),(397.688201904297,-116.629638671875,1303.21032714844),(315.505462646484,-78.9872436523438,1315.50268554688),(276.939117431641,-119.453987121582,1321.89636230469),(188.465042114258,-304.243865966797,397.150054931641),(160.809936523438,-262.796142578125,805.354858398438),(203.332580566406,-246.257904052734,773.599060058594),(259.857757568359,-298.334381103516,426.012054443359),(147.664993286133,-238.491973876953,1008.68743896484),(192.343307495117,-157.946182250977,659.137756347656),(185.160919189453,-201.867858886719,720.268798828125),(186.643859863281,-264.314544677734,377.798126220703),(205.294372558594,-237.602523803711,407.926666259766),(151.504852294922,-39.1747512817383,280.009460449219),(177.406814575195,-102.663719177246,326.255584716797),(202.127624511719,-69.6266632080078,326.591583251953),(193.145324707031,33.1915512084961,263.857574462891),(164.464447021484,-163.047897338867,357.746185302734),(247.165740966797,-133.986694335938,363.527313232422),(227.365463256836,-193.647537231445,376.384826660156),(283.534729003906,-242.950988769531,419.483917236328),(414.337524414062,-74.2443008422852,1167.27038574219),(369.537170410156,-85.9891204833984,1051.84948730469),(404.827026367188,-72.8606414794922,1112.72814941406),(407.421478271484,-96.4484329223633,1130.64721679688),(473.46533203125,-86.1440963745117,1012.70166015625),(437.606140136719,-65.8767929077148,965.449462890625),(236.806304931641,-225.523635864258,481.408416748047),(297.629730224609,-120.587661743164,1290.78527832031),(323.314422607422,-176.81721496582,1192.16748046875),(260.157592773438,-135.264190673828,1259.5029296875),(336.322357177734,-122.433921813965,1037.03234863281),(364.126342773438,-130.807113647461,935.928405761719),(309.030181884766,-135.372375488281,950.143920898438),(278.101501464844,-122.49227142334,1091.39782714844),(330.940185546875,-156.066329956055,1084.29479980469),(129.381759643555,-236.528305053711,518.649047851562),(132.750106811523,-234.21305847168,653.487670898438),(120.984184265137,-254.061706542969,931.172180175781),(332.416015625,-88.7421646118164,1147.03820800781),(245.219161987305,-122.538154602051,1185.7099609375),(131.642654418945,-291.287719726562,427.089477539062),(116.164497375488,-269.177581787109,333.953704833984),(100.750999450684,-365.325897216797,355.390106201172),(92.9562530517578,-334.828826904297,541.520385742188),(345.203125,-105.10066986084,1027.06286621094),(-0.282331973314285,-245.40217590332,850.967407226562),(-9.91721343994141,-234.005096435547,1051.09155273438),(192.275253295898,-196.170043945312,1228.09899902344),(203.226577758789,-139.625366210938,1195.75073242188),(181.33903503418,-172.821807861328,1214.49816894531),(255.358200073242,-158.387359619141,1142.03308105469),(245.205429077148,-141.107284545898,1008.36901855469),(288.378143310547,-177.676177978516,1005.04571533203),(168.612915039062,-191.296936035156,1035.94677734375),(80.7072219848633,-301.066925048828,828.076171875),(96.1312789916992,-239.572052001953,1189.82104492188),(111.940330505371,-278.002319335938,928.733032226562),(122.164833068848,-209.147583007812,1169.44458007812),(336.85986328125,-42.5956192016602,1048.13122558594),(200.684204101562,-148.02458190918,1075.6298828125),(137.877807617188,-244.42985534668,1186.93579101562),(199.932220458984,-8.53413581848145,421.528869628906),(194.687835693359,-48.5160675048828,45.7340278625488),(159.225692749023,-96.2861099243164,169.432495117188),(37.2209625244141,72.1331558227539,712.920104980469),(91.0833969116211,80.5275115966797,569.639038085938),(129.217086791992,84.2431335449219,639.683471679688),(94.2891540527344,144.876831054688,446.311126708984),(-5.78799533843994,86.9931869506836,642.360778808594),(-7.31255722045898,50.6945991516113,702.014709472656),(-38.825927734375,25.3310928344727,846.389221191406),(-45.0165061950684,67.8793563842773,767.40966796875),(-145.277099609375,-8.27912998199463,748.223449707031),(-192.577621459961,-0.633410274982452,375.178894042969),(-129.685485839844,-33.8545417785645,970.439880371094),(-107.642707824707,-20.1257991790771,1246.58081054688),(-44.2391204833984,14.8375968933105,1181.673828125),(-91.1340255737305,-29.4557914733887,1151.98876953125),(-191.03337097168,-127.531745910645,506.393707275391),(-162.196136474609,-66.7209243774414,633.170349121094),(-176.999206542969,-50.8013381958008,248.601715087891),(-230.035675048828,-133.382827758789,112.767387390137),(-246.184555053711,152.172241210938,7.54552841186523),(-14.1170129776001,86.548942565918,372.274261474609),(167.21533203125,72.3524017333984,589.4775390625),(136.8125,269.382659912109,191.416610717773),(71.5297470092773,287.880767822266,216.711898803711),(74.540397644043,370.947418212891,119.111824035645),(133.028625488281,327.585021972656,100.417167663574),(148.794662475586,269.060699462891,104.780456542969),(169.967681884766,109.48486328125,250.85823059082),(189.553146362305,168.804489135742,228.852828979492),(185.619110107422,59.0821151733398,88.5192642211914),(221.865356445312,162.241439819336,24.1779232025146),(187.571685791016,-50.6560325622559,204.6435546875),(211.225204467773,82.2998962402344,122.356285095215),(155.21842956543,86.5802383422852,236.424011230469),(189.487121582031,309.881408691406,45.5172882080078),(210.763137817383,239.834243774414,9.35835647583008),(169.113067626953,204.443557739258,8.62976837158203),(149.133163452148,215.826812744141,83.5866546630859),(170.582443237305,148.817901611328,53.1442337036133),(-300.863861083984,-157.656265258789,35.9106750488281),(86.1821212768555,300.579681396484,8.44492149353027),(-404.658203125,-120.59839630127,1496.64660644531),(-373.734344482422,-185.118179321289,1552.60070800781),(-277.911987304688,-219.467437744141,1570.08642578125),(-270.670257568359,-140.886962890625,1558.55395507812),(-307.949645996094,-179.936721801758,1524.38720703125),(-312.341827392578,-265.590270996094,1502.47021484375),(-343.112060546875,-190.961761474609,1396.91052246094),(-257.206237792969,-165.078460693359,1398.27990722656),(-169.326187133789,-83.5106811523438,1468.94580078125),(-45.2263603210449,12.5155572891235,1453.42370605469),(-163.127563476562,50.7695579528809,1443.33801269531),(-97.1828384399414,42.8138046264648,1425.12939453125),(-176.406143188477,-33.1263465881348,1480.37634277344),(-224.613388061523,-1.62007975578308,1429.32214355469),(-261.904083251953,-65.6184997558594,1519.5126953125),(-302.266204833984,-20.0826473236084,1409.99060058594),(-194.050537109375,44.9293060302734,1370.322265625),(-330.362335205078,-185.16618347168,1320.09375),(-302.137329101562,-143.216705322266,1326.15930175781),(-380.224884033203,-178.806503295898,1319.92529296875),(-320.069366455078,-280.262023925781,1310.17724609375),(-325.509002685547,-264.641235351562,1251.47583007812),(-306.571380615234,-236.216003417969,1208.86840820312),(-348.646392822266,-193.627899169922,1128.93627929688),(-322.764282226562,-122.944648742676,1206.00463867188),(-227.334060668945,-100.000473022461,1237.63952636719),(-150.030471801758,-71.1294479370117,1249.9541015625),(-128.373641967773,21.5559558868408,1239.54614257812),(-109.857025146484,-62.3633460998535,1306.44323730469),(-5.45178365707397,34.3603973388672,1208.82348632812),(15.6776657104492,86.8136444091797,1391.94812011719),(-37.118968963623,61.9902839660645,1429.99987792969),(104.297897338867,26.6234397888184,1074.28002929688),(103.164924621582,26.3919353485107,1127.47253417969),(57.7263488769531,36.3444366455078,1140.66833496094),(-71.4559936523438,14.3564548492432,840.276000976562),(-76.6244735717773,-18.6751823425293,1019.06353759766),(-65.8723754882812,23.820764541626,1032.32604980469),(75.5481338500977,88.368896484375,777.67236328125),(-32.2922782897949,18.0561809539795,942.27783203125),(95.6580810546875,81.6739349365234,886.651245117188),(15.7327737808228,57.837272644043,974.274597167969),(-201.080551147461,101.168640136719,98.570686340332),(-289.260803222656,-104.399887084961,11.3370065689087),(-297.5048828125,38.9086532592773,1373.87524414062),(-111.359497070312,-34.5642509460449,783.250732421875),(-98.4062347412109,10.7842063903809,698.300109863281),(-109.949310302734,-14.509256362915,551.925598144531),(-51.639347076416,18.9487743377686,658.59619140625),(-78.1960754394531,46.6424751281738,499.489807128906),(-43.3707008361816,57.8422889709473,425.090484619141),(-195.434448242188,-88.1886901855469,88.7719879150391),(-256.575958251953,106.748611450195,6.2224268913269),(-282.607940673828,-5.43050765991211,4.25207567214966),(-53.7902679443359,84.2270812988281,278.401153564453),(-353.248901367188,-101.966873168945,1493.28869628906),(-196.578109741211,88.9360427856445,1343.17919921875),(-151.250717163086,20.8457908630371,417.927612304688),(-304.972930908203,72.6408538818359,11.6079778671265),(-329.454376220703,15.0221586227417,5.15277338027954),(10.3479175567627,356.6474609375,97.2546234130859),(-424.256958007812,-105.741661071777,1317.17004394531),(-414.061920166016,-125.973098754883,1359.53125),(-113.575721740723,125.182006835938,1524.3681640625),(-86.1567230224609,115.965057373047,1492.20886230469),(-117.240768432617,113.329612731934,1541.01147460938),(-75.0297775268555,103.098014831543,1496.85998535156),(-100.981216430664,68.8193511962891,1516.89892578125),(-116.712928771973,81.3248825073242,1534.5673828125),(40.1087112426758,224.180389404297,1554.21228027344),(55.2424850463867,228.096069335938,1573.20739746094),(52.0294380187988,220.650115966797,1583.29736328125),(27.6150588989258,224.404037475586,1541.00561523438),(20.6359710693359,209.799728393555,1528.2841796875),(27.3676090240479,215.940902709961,1560.34216308594),(5.76483488082886,192.338027954102,1526.03015136719),(37.1759567260742,212.91032409668,1535.86608886719),(29.1523342132568,191.362365722656,1525.49096679688),(28.6984882354736,204.555282592773,1523.72692871094),(56.4874649047852,209.349868774414,1597.42041015625),(-14.4212265014648,-23.0675487518311,1458.56921386719),(-29.7597770690918,-17.0394020080566,1496.51965332031),(-70.9299850463867,-17.2349987030029,1486.20959472656),(-73.7109985351562,-27.5204887390137,1502.36779785156),(-30.5149631500244,-23.5454502105713,1548.98669433594),(1.83288025856018,-13.3799915313721,1521.00183105469),(37.8389167785645,-32.7231025695801,1461.29956054688),(10.6368379592896,5.48521661758423,1557.26794433594),(30.5814113616943,1.57328248023987,1526.83947753906),(33.9724655151367,7.76734781265259,1555.83056640625),(70.7004013061523,-2.17202520370483,1467.79528808594),(78.6741256713867,69.1588897705078,1492.65014648438),(78.4659042358398,92.3239822387695,1484.82751464844),(75.4610977172852,86.7758636474609,1461.76843261719),(61.4135665893555,72.1802597045898,1473.77014160156),(70.9747772216797,96.4617004394531,1495.83312988281),(53.6593399047852,51.6954727172852,1480.53002929688),(69.4760818481445,41.8450355529785,1492.98400878906),(76.0567169189453,42.6811103820801,1508.2802734375),(73.8561325073242,69.4737091064453,1515.13977050781),(84.6694717407227,101.128875732422,1540.47277832031),(85.3650665283203,107.239677429199,1568.13330078125),(77.8931427001953,75.1578598022461,1591.20690917969),(33.0704002380371,70.1776885986328,1646.32592773438),(33.8830299377441,51.5049438476562,1627.7587890625),(40.7202606201172,29.0004825592041,1626.66320800781),(53.6601524353027,61.0881690979004,1464.50427246094),(59.0355911254883,71.6298294067383,1472.91589355469),(39.7420387268066,70.1308975219727,1451.48754882812),(59.6833724975586,66.7699966430664,1611.15856933594),(69.2930221557617,37.436466217041,1603.04833984375),(38.6539077758789,14.9436511993408,1550.74841308594),(25.1376686096191,40.7397270202637,1641.21105957031),(-5.45751905441284,36.7394676208496,1653.55419921875),(18.1719779968262,22.5732383728027,1634.83386230469),(-8.5576057434082,63.4988441467285,1662.55908203125),(-65.9412689208984,23.3862171173096,1625.67163085938),(-47.3745193481445,18.7052669525146,1641.37646484375),(-10.1549577713013,4.91736841201782,1615.02917480469),(-103.409973144531,55.2447814941406,1615.44970703125),(-119.151214599609,74.6872634887695,1623.66650390625),(-126.155563354492,64.5326309204102,1618.49731445312),(-111.926651000977,55.8391647338867,1601.84106445312),(-74.5309600830078,35.6530990600586,1620.134765625),(-95.9652404785156,34.5729103088379,1599.60876464844),(-116.207382202148,51.1680450439453,1580.06506347656),(-49.3067893981934,16.6121482849121,1607.46398925781),(-88.136116027832,22.3731842041016,1579.61120605469),(-113.943626403809,46.2731628417969,1562.29650878906),(-92.5713272094727,36.8668937683105,1567.96252441406),(-115.383071899414,7.92506122589111,1538.07019042969),(-91.192138671875,9.4000883102417,1552.15454101562),(-67.2494049072266,0.555085420608521,1579.39318847656),(-25.9960899353027,20.0776863098145,1644.13879394531),(-62.8531837463379,52.8868827819824,1656.671875),(-95.7298736572266,72.4900131225586,1653.73791503906),(-102.556533813477,59.7004547119141,1639.59118652344),(36.7050819396973,181.189712524414,1586.69104003906),(56.0937271118164,168.654113769531,1588.27905273438),(28.3847389221191,187.388137817383,1559.20568847656),(48.278564453125,163.58805847168,1613.85107421875),(70.4153900146484,131.734756469727,1594.84875488281),(50.0555686950684,125.047988891602,1636.34692382812),(-22.0689868927002,182.913635253906,1629.01123046875),(11.0097780227661,157.756408691406,1647.26171875),(-36.2250595092773,148.731796264648,1655.99987792969),(39.4887962341309,107.31908416748,1646.9609375),(-24.6603813171387,103.131538391113,1667.595703125),(91.6101226806641,70.2689590454102,1563.38977050781),(81.4488372802734,48.0304908752441,1569.1357421875),(70.3326721191406,102.810134887695,1601.3212890625),(72.9910049438477,128.569854736328,1557.46740722656),(89.2945251464844,156.26123046875,1597.82055664062),(78.739372253418,151.29411315918,1603.04052734375),(69.0121688842773,186.269470214844,1603.41333007812),(89.9434204101562,177.107223510742,1597.65087890625),(85.4082565307617,193.738403320312,1601.94750976562),(81.8152465820312,198.763748168945,1589.65185546875),(67.6442947387695,220.045913696289,1593.25),(88.9900131225586,174.113494873047,1585.31018066406),(94.1403045654297,144.667526245117,1597.46496582031),(95.1149444580078,154.339080810547,1587.76696777344),(-15.900899887085,-0.691527128219604,1578.92895507812),(20.5557975769043,4.02943420410156,1598.52685546875),(58.7821578979492,21.0654945373535,1575.85229492188),(63.8384246826172,31.5590400695801,1548.03344726562),(43.1311569213867,21.1730651855469,1526.13916015625),(-99.3362121582031,5.88167762756348,1530.93798828125),(78.2768402099609,122.414169311523,1541.7421875),(87.981071472168,142.011825561523,1578.30712890625),(92.2791748046875,173.672668457031,1552.27185058594),(96.0646057128906,139.523681640625,1556.39721679688),(96.1841583251953,167.597518920898,1539.00537109375),(99.31201171875,143.991088867188,1539.31384277344),(86.7889022827148,133.999969482422,1529.29833984375),(79.0168380737305,165.672454833984,1513.74719238281),(63.1840705871582,148.564270019531,1501.51110839844),(84.8034362792969,127.631164550781,1510.87744140625),(63.073371887207,127.283325195312,1490.67297363281),(53.7515182495117,104.988243103027,1485.16870117188),(62.3159370422363,68.2941513061523,1476.35620117188),(53.5460205078125,31.7911033630371,1500.64770507812),(63.9988594055176,43.2823829650879,1523.74719238281),(48.0260696411133,36.8978881835938,1523.22729492188),(78.4457092285156,101.313835144043,1529.45031738281),(87.712028503418,72.1794509887695,1531.53845214844),(82.9435348510742,150.257598876953,1530.06225585938),(84.0949325561523,172.855438232422,1532.66528320312),(-88.2408981323242,-5.67919301986694,1518.27795410156),(-93.551872253418,61.0295181274414,1499.48852539062),(-64.3972473144531,41.2935981750488,1474.70520019531),(-129.675582885742,115.201606750488,1574.70422363281),(-129.302963256836,130.147277832031,1579.95959472656),(-124.743827819824,111.590309143066,1599.02111816406),(-118.581703186035,137.487686157227,1544.07019042969),(-121.445251464844,109.26685333252,1557.3046875),(-130.845718383789,82.4912719726562,1559.37475585938),(-128.739303588867,86.3893280029297,1568.41345214844),(-137.022109985352,98.8915100097656,1586.59057617188),(-120.534721374512,94.0420150756836,1597.99853515625),(-128.516677856445,80.6572647094727,1611.14758300781),(-100.700874328613,52.9861488342285,1543.31884765625),(-100.370506286621,28.7165565490723,1523.56140136719),(-71.6600799560547,134.653198242188,1483.3564453125),(-47.6845359802246,107.31665802002,1461.91711425781),(-50.5442695617676,95.0276947021484,1479.14929199219),(-44.3628234863281,12.8352375030518,1470.51416015625),(-9.59514045715332,150.318466186523,1477.25354003906),(-31.1618804931641,141.470016479492,1466.62060546875),(-37.2183074951172,165.292907714844,1489.1923828125),(23.2937202453613,131.654922485352,1474.15405273438),(-0.149351730942726,135.890197753906,1455.57189941406),(10.8297147750854,106.050392150879,1457.66564941406),(-4.73065519332886,82.7443008422852,1473.79431152344),(-39.2331466674805,45.2719841003418,1467.78759765625),(43.6294136047363,77.7739639282227,1473.76306152344),(-5.54138803482056,70.0845108032227,1448.72180175781),(40.9911003112793,107.879447937012,1464.13415527344),(62.083683013916,89.6187515258789,1473.49768066406),(9.83149433135986,153.214431762695,1481.05261230469),(32.5141372680664,134.653503417969,1486.85607910156),(95.1986236572266,164.832229614258,1567.01672363281),(92.5120849609375,138.931411743164,1571.63415527344),(43.814323425293,170.36799621582,1503.23315429688),(69.8880233764648,188.286117553711,1526.00830078125),(33.6955795288086,149.046524047852,1500.26477050781),(55.507999420166,189.1328125,1528.93835449219),(45.8849182128906,190.43017578125,1514.07092285156),(69.0374221801758,171.354080200195,1572.42602539062),(47.8584289550781,202.034088134766,1557.28100585938),(56.2005386352539,216.99951171875,1559.67297363281),(49.6362800598145,186.180221557617,1547.59497070312),(67.570671081543,186.152053833008,1546.78051757812),(81.5277633666992,175.332061767578,1567.64343261719),(-1.24849903583527,161.438919067383,1498.05908203125),(5.40095329284668,184.023300170898,1526.90966796875),(24.1932201385498,166.245971679688,1516.11413574219),(-28.9154834747314,191.175491333008,1539.17370605469),(48.3305816650391,228.22917175293,1597.3486328125),(32.2272415161133,201.510314941406,1555.77709960938),(66.5850601196289,209.041641235352,1577.99890136719),(-81.1425170898438,113.942237854004,1655.3447265625),(-54.0028457641602,178.061798095703,1616.86779785156),(-79.9074401855469,141.797698974609,1627.0205078125),(-100.54029083252,151.466613769531,1614.01025390625),(-85.1366271972656,132.896179199219,1641.85217285156),(-107.957695007324,118.715560913086,1615.83630371094),(-112.850852966309,100.448387145996,1632.87072753906),(-88.2589721679688,88.7780227661133,1649.50280761719),(-57.0232620239258,166.791427612305,1515.27026367188),(-16.370569229126,197.421020507812,1589.68139648438),(-71.4700393676758,183.09260559082,1557.46264648438),(-76.7884292602539,179.287841796875,1576.80920410156),(-81.445671081543,152.388381958008,1510.34204101562))); +#1638=IFCPOLYGONALFACESET(#1637,.F.,(#548,#549,#550,#551,#552,#553,#554,#555,#556,#557,#558,#559,#560,#561,#562,#563,#564,#565,#566,#567,#568,#569,#570,#571,#572,#573,#574,#575,#576,#577,#578,#579,#580,#581,#582,#583,#584,#585,#586,#587,#588,#589,#590,#591,#592,#593,#594,#595,#596,#597,#598,#599,#600,#601,#602,#603,#604,#605,#606,#607,#608,#609,#610,#611,#612,#613,#614,#615,#616,#617,#618,#619,#620,#621,#622,#623,#624,#625,#626,#627,#628,#629,#630,#631,#632,#633,#634,#635,#636,#637,#638,#639,#640,#641,#642,#643,#644,#645,#646,#647,#648,#649,#650,#651,#652,#653,#654,#655,#656,#657,#658,#659,#660,#661,#662,#663,#664,#665,#666,#667,#668,#669,#670,#671,#672,#673,#674,#675,#676,#677,#678,#679,#680,#681,#682,#683,#684,#685,#686,#687,#688,#689,#690,#691,#692,#693,#694,#695,#696,#697,#698,#699,#700,#701,#702,#703,#704,#705,#706,#707,#708,#709,#710,#711,#712,#713,#714,#715,#716,#717,#718,#719,#720,#721,#722,#723,#724,#725,#726,#727,#728,#729,#730,#731,#732,#733,#734,#735,#736,#737,#738,#739,#740,#741,#742,#743,#744,#745,#746,#747,#748,#749,#750,#751,#752,#753,#754,#755,#756,#757,#758,#759,#760,#761,#762,#763,#764,#765,#766,#767,#768,#769,#770,#771,#772,#773,#774,#775,#776,#777,#778,#779,#780,#781,#782,#783,#784,#785,#786,#787,#788,#789,#790,#791,#792,#793,#794,#795,#796,#797,#798,#799,#800,#801,#802,#803,#804,#805,#806,#807,#808,#809,#810,#811,#812,#813,#814,#815,#816,#817,#818,#819,#820,#821,#822,#823,#824,#825,#826,#827,#828,#829,#830,#831,#832,#833,#834,#835,#836,#837,#838,#839,#840,#841,#842,#843,#844,#845,#846,#847,#848,#849,#850,#851,#852,#853,#854,#855,#856,#857,#858,#859,#860,#861,#862,#863,#864,#865,#866,#867,#868,#869,#870,#871,#872,#873,#874,#875,#876,#877,#878,#879,#880,#881,#882,#883,#884,#885,#886,#887,#888,#889,#890,#891,#892,#893,#894,#895,#896,#897,#898,#899,#900,#901,#902,#903,#904,#905,#906,#907,#908,#909,#910,#911,#912,#913,#914,#915,#916,#917,#918,#919,#920,#921,#922,#923,#924,#925,#926,#927,#928,#929,#930,#931,#932,#933,#934,#935,#936,#937,#938,#939,#940,#941,#942,#943,#944,#945,#946,#947,#948,#949,#950,#951,#952,#953,#954,#955,#956,#957,#958,#959,#960,#961,#962,#963,#964,#965,#966,#967,#968,#969,#970,#971,#972,#973,#974,#975,#976,#977,#978,#979,#980,#981,#982,#983,#984,#985,#986,#987,#988,#989,#990,#991,#992,#993,#994,#995,#996,#997,#998,#999,#1000,#1001,#1002,#1003,#1004,#1005,#1006,#1007,#1008,#1009,#1010,#1011,#1012,#1013,#1014,#1015,#1016,#1017,#1018,#1019,#1020,#1021,#1022,#1023,#1024,#1025,#1026,#1027,#1028,#1029,#1030,#1031,#1032,#1033,#1034,#1035,#1036,#1037,#1038,#1039,#1040,#1041,#1042,#1043,#1044,#1045,#1046,#1047,#1048,#1049,#1050,#1051,#1052,#1053,#1054,#1055,#1056,#1057,#1058,#1059,#1060,#1061,#1062,#1063,#1064,#1065,#1066,#1067,#1068,#1069,#1070,#1071,#1072,#1073,#1074,#1075,#1076,#1077,#1078,#1079,#1080,#1081,#1082,#1083,#1084,#1085,#1086,#1087,#1088,#1089,#1090,#1091,#1092,#1093,#1094,#1095,#1096,#1097,#1098,#1099,#1100,#1101,#1102,#1103,#1104,#1105,#1106,#1107,#1108,#1109,#1110,#1111,#1112,#1113,#1114,#1115,#1116,#1117,#1118,#1119,#1120,#1121,#1122,#1123,#1124,#1125,#1126,#1127,#1128,#1129,#1130,#1131,#1132,#1133,#1134,#1135,#1136,#1137,#1138,#1139,#1140,#1141,#1142,#1143,#1144,#1145,#1146,#1147,#1148,#1149,#1150,#1151,#1152,#1153,#1154,#1155,#1156,#1157,#1158,#1159,#1160,#1161,#1162,#1163,#1164,#1165,#1166,#1167,#1168,#1169,#1170,#1171,#1172,#1173,#1174,#1175,#1176,#1177,#1178,#1179,#1180,#1181,#1182,#1183,#1184,#1185,#1186,#1187,#1188,#1189,#1190,#1191,#1192,#1193,#1194,#1195,#1196,#1197,#1198,#1199,#1200,#1201,#1202,#1203,#1204,#1205,#1206,#1207,#1208,#1209,#1210,#1211,#1212,#1213,#1214,#1215,#1216,#1217,#1218,#1219,#1220,#1221,#1222,#1223,#1224,#1225,#1226,#1227,#1228,#1229,#1230,#1231,#1232,#1233,#1234,#1235,#1236,#1237,#1238,#1239,#1240,#1241,#1242,#1243,#1244,#1245,#1246,#1247,#1248,#1249,#1250,#1251,#1252,#1253,#1254,#1255,#1256,#1257,#1258,#1259,#1260,#1261,#1262,#1263,#1264,#1265,#1266,#1267,#1268,#1269,#1270,#1271,#1272,#1273,#1274,#1275,#1276,#1277,#1278,#1279,#1280,#1281,#1282,#1283,#1284,#1285,#1286,#1287,#1288,#1289,#1290,#1291,#1292,#1293,#1294,#1295,#1296,#1297,#1298,#1299,#1300,#1301,#1302,#1303,#1304,#1305,#1306,#1307,#1308,#1309,#1310,#1311,#1312,#1313,#1314,#1315,#1316,#1317,#1318,#1319,#1320,#1321,#1322,#1323,#1324,#1325,#1326,#1327,#1328,#1329,#1330,#1331,#1332,#1333,#1334,#1335,#1336,#1337,#1338,#1339,#1340,#1341,#1342,#1343,#1344,#1345,#1346,#1347,#1348,#1349,#1350,#1351,#1352,#1353,#1354,#1355,#1356,#1357,#1358,#1359,#1360,#1361,#1362,#1363,#1364,#1365,#1366,#1367,#1368,#1369,#1370,#1371,#1372,#1373,#1374,#1375,#1376,#1377,#1378,#1379,#1380,#1381,#1382,#1383,#1384,#1385,#1386,#1387,#1388,#1389,#1390,#1391,#1392,#1393,#1394,#1395,#1396,#1397,#1398,#1399,#1400,#1401,#1402,#1403,#1404,#1405,#1406,#1407,#1408,#1409,#1410,#1411,#1412,#1413,#1414,#1415,#1416,#1417,#1418,#1419,#1420,#1421,#1422,#1423,#1424,#1425,#1426,#1427,#1428,#1429,#1430,#1431,#1432,#1433,#1434,#1435,#1436,#1437,#1438,#1439,#1440,#1441,#1442,#1443,#1444,#1445,#1446,#1447,#1448,#1449,#1450,#1451,#1452,#1453,#1454,#1455,#1456,#1457,#1458,#1459,#1460,#1461,#1462,#1463,#1464,#1465,#1466,#1467,#1468,#1469,#1470,#1471,#1472,#1473,#1474,#1475,#1476,#1477,#1478,#1479,#1480,#1481,#1482,#1483,#1484,#1485,#1486,#1487,#1488,#1489,#1490,#1491,#1492,#1493,#1494,#1495,#1496,#1497,#1498,#1499,#1500,#1501,#1502,#1503,#1504,#1505,#1506,#1507,#1508,#1509,#1510,#1511,#1512,#1513,#1514,#1515,#1516,#1517,#1518,#1519,#1520,#1521,#1522,#1523,#1524,#1525,#1526,#1527,#1528,#1529,#1530,#1531,#1532,#1533,#1534,#1535,#1536,#1537,#1538,#1539,#1540,#1541,#1542,#1543,#1544,#1545,#1546,#1547,#1548,#1549,#1550,#1551,#1552,#1553,#1554,#1555,#1556,#1557,#1558,#1559,#1560,#1561,#1562,#1563,#1564,#1565,#1566,#1567,#1568,#1569,#1570,#1571,#1572,#1573,#1574,#1575,#1576,#1577,#1578,#1579,#1580,#1581,#1582,#1583,#1584,#1585,#1586,#1587,#1588,#1589,#1590,#1591,#1592,#1593,#1594,#1595,#1596,#1597,#1598,#1599,#1600,#1601,#1602,#1603,#1604,#1605,#1606,#1607,#1608,#1609,#1610,#1611,#1612,#1613,#1614,#1615,#1616,#1617,#1618,#1619,#1620,#1621,#1622,#1623,#1624,#1625,#1626,#1627,#1628,#1629,#1630,#1631,#1632,#1633,#1634,#1635,#1636),$); +#1639=IFCSHAPEREPRESENTATION(#15,'Body','Tessellation',(#1638)); +#1640=IFCCARTESIANPOINT((0.,0.,0.)); +#1641=IFCDIRECTION((1.,0.,0.)); +#1642=IFCDIRECTION((0.,0.,1.)); +#1643=IFCAXIS2PLACEMENT3D(#1640,#1642,#1641); +#1644=IFCREPRESENTATIONMAP(#1643,#1639); +ENDSEC; +END-ISO-10303-21; diff --git a/src/blenderbim/blenderbim/bim/data/pset/EPset_Drawing.ifc b/src/blenderbim/blenderbim/bim/data/pset/EPset_Drawing.ifc index 5a2227b90e..13e2c4e629 100644 --- a/src/blenderbim/blenderbim/bim/data/pset/EPset_Drawing.ifc +++ b/src/blenderbim/blenderbim/bim/data/pset/EPset_Drawing.ifc @@ -5,7 +5,7 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',(),(),'EPset_Drawing','EPset FILE_SCHEMA(('IFC4')); ENDSEC; DATA; -#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22)); +#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22,#23)); #2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); #3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); #4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); @@ -27,5 +27,6 @@ DATA; #20=IFCSIMPLEPROPERTYTEMPLATE('0joEq0Rd10cxweEh0NeHT6',$,'JoinCriteria','Comma separated selection keys which determine what cut objects are to be joined.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); #21=IFCSIMPLEPROPERTYTEMPLATE('0nYMT3OSj5gArVniCWZRtv',$,'ShadingStyles','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); #22=IFCSIMPLEPROPERTYTEMPLATE('3VWG22eZXBdQwdKlzMeVQH',$,'CurrentShadingStyle','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); +#23=IFCSIMPLEPROPERTYTEMPLATE('28mJ$GDxb7kBbKFH_rACMQ',$,'AngleDecimalPlaces','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.); ENDSEC; END-ISO-10303-21; diff --git a/src/blenderbim/blenderbim/bim/export_ifc.py b/src/blenderbim/blenderbim/bim/export_ifc.py index 77ed7eaa50..5efaefa4b1 100644 --- a/src/blenderbim/blenderbim/bim/export_ifc.py +++ b/src/blenderbim/blenderbim/bim/export_ifc.py @@ -27,6 +27,7 @@ import addon_utils import ifcopenshell import ifcopenshell.api import ifcopenshell.util.placement +import ifcopenshell.util.unit import blenderbim.tool as tool import blenderbim.core.geometry import blenderbim.core.aggregate @@ -34,6 +35,7 @@ import blenderbim.core.spatial import blenderbim.core.style from blenderbim.bim.ifc import IfcStore from mathutils import Vector +from typing import Union class IfcExporter: @@ -85,8 +87,8 @@ class IfcExporter: self.get_application_name(), tool.Blender.get_blenderbim_version() ) - def sync_all_objects(self): - results = [] + def sync_all_objects(self, skip_unlinking=False) -> list[ifcopenshell.entity_instance]: + results: list[ifcopenshell.entity_instance] = [] self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) for ifc_definition_id in list(IfcStore.id_map.keys()): obj = IfcStore.id_map[ifc_definition_id] @@ -95,19 +97,21 @@ class IfcExporter: continue if obj.library: continue - tool.Collector.sync(obj) + tool.Collector.sync(obj, skip_unlinking) result = self.sync_object_placement(obj) if result: results.append(result) result = self.sync_object_material(obj) + # TODO: sync_object_material always returns None + # so it's never really appended if result: results.append(result) except ReferenceError: pass # The object is likely deleted return results - def sync_edited_objects(self): - results = [] + def sync_edited_objects(self) -> list[ifcopenshell.entity_instance]: + results: list[ifcopenshell.entity_instance] = [] for obj in IfcStore.edited_objs.copy(): if not obj: continue @@ -125,7 +129,7 @@ class IfcExporter: IfcStore.edited_objs.clear() return results - def sync_object_material(self, obj): + def sync_object_material(self, obj: bpy.types.Object) -> None: if not obj.data or not isinstance(obj.data, bpy.types.Mesh): return if not self.has_changed_materials(obj): @@ -136,11 +140,10 @@ class IfcExporter: checksum = obj.data.BIMMeshProperties.material_checksum return checksum != str([s.id() for s in tool.Geometry.get_styles(obj) if s]) - def sync_object_placement(self, obj): + def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]: element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) if not tool.Ifc.is_moved(obj): return - blender_matrix = np.array(obj.matrix_world) if (obj.scale - Vector((1.0, 1.0, 1.0))).length > 1e-4: bpy.ops.bim.update_representation(obj=obj.name) return element @@ -151,7 +154,7 @@ class IfcExporter: blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) return element - def sync_grid_axis_object_placement(self, obj, element): + def sync_grid_axis_object_placement(self, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None: grid = (element.PartOfU or element.PartOfV or element.PartOfW)[0] grid_obj = tool.Ifc.get_object(grid) if grid_obj: diff --git a/src/blenderbim/blenderbim/bim/handler.py b/src/blenderbim/blenderbim/bim/handler.py index bf07cc9fc6..28306671de 100644 --- a/src/blenderbim/blenderbim/bim/handler.py +++ b/src/blenderbim/blenderbim/bim/handler.py @@ -31,6 +31,7 @@ from blenderbim.bim.module.model.data import AuthoringData from blenderbim.bim.module.model.workspace import LIST_OF_TOOLS, TOOLS_TO_CLASSES_MAP from mathutils import Vector from math import cos, degrees +from typing import Union cwd = os.path.dirname(os.path.realpath(__file__)) @@ -191,6 +192,8 @@ def refresh_ui_data(): if isinstance(tool.Ifc.get(), ifcopenshell.sqlite): tool.Ifc.get().clear_cache() + bpy.context.scene.DocProperties.should_draw_decorations = bpy.context.scene.DocProperties.should_draw_decorations + @persistent def loadIfcStore(scene): @@ -254,60 +257,6 @@ def viewport_shading_changed_callback(area): bpy.context.scene.BIMStylesProperties.active_style_type = "External" -@classmethod -def poll_check_blender_tab(cls, context): - return tool.Blender.is_tab(context, "BLENDER") - - -def get_override_scene_panel(panel_name): - original_panel = getattr(bpy.types, panel_name) - - override_panel = type(f"Override_{panel_name}", (original_panel,), {}) - override_panel.bl_idname = f"{panel_name}_override" - - # some blender panel depend on other and register_class will throw an error - # if we won't override their `bl_parent_id` - bl_parent_id = getattr(override_panel, "bl_parent_id", None) - if bl_parent_id is not None: - override_panel.bl_parent_id = f"{bl_parent_id}_override" - - # override poll method - poll = getattr(override_panel, "poll", None) - if poll is None: - override_panel.poll = poll_check_blender_tab - else: - - @classmethod - def wrapped_poll(cls, context): - return super(override_panel, cls).poll(context) and poll_check_blender_tab.__func__(cls, context) - - override_panel.poll = wrapped_poll - - return override_panel - - -# TODO: possibly override scene panels from other addons too? -# list can be updated from -# https://projects.blender.org/blender/blender/src/branch/main/scripts/startup/bl_ui/properties_scene.py#L421 -OVERRIDE_SCENE_PANELS = ( - "SCENE_PT_scene", - "SCENE_PT_unit", - "SCENE_PT_physics", - "SCENE_PT_rigid_body_world", - "SCENE_PT_rigid_body_world_settings", - "SCENE_PT_rigid_body_cache", - "SCENE_PT_rigid_body_field_weights", - "SCENE_PT_audio", - "SCENE_PT_keying_sets", - "SCENE_PT_custom_props", - # after SCENE_PT_keying_sets - "SCENE_PT_keying_set_paths", - "SCENE_PT_keyframing_settings", -) -if bpy.app.version >= (4, 0): - OVERRIDE_SCENE_PANELS += ("SCENE_PT_simulation",) - - @persistent def load_post(scene): global global_subscription_owner @@ -343,15 +292,11 @@ def load_post(scene): else: bpy.ops.workspace.append_activate(idname="BIM", filepath=os.path.join(cwd, "data", "workspace.blend")) - # To improve usability for new users, we hijack the scene properties - # tab. We override default scene properties panels with our own poll - # to hide them unless the user has chosen to view Blender properties. - for panel in OVERRIDE_SCENE_PANELS: - if panel in blenderbim.bim.overridden_scene_panels: - continue - override_panel = get_override_scene_panel(panel) - original_panel = getattr(bpy.types, panel) - bpy.utils.register_class(override_panel) - bpy.utils.unregister_class(original_panel) - blenderbim.bim.overridden_scene_panels[panel] = (original_panel, override_panel) + # To improve usability for new users, we hijack the scene properties + # tab. We override default scene properties panels with our own poll + # to hide them unless the user has chosen to view Blender properties. + for panel in tool.Blender.get_scene_panels_list(): + if panel in blenderbim.bim.original_scene_panels_polls: + continue + tool.Blender.override_scene_panel(panel) tool.Blender.setup_tabs() diff --git a/src/blenderbim/blenderbim/bim/helper.py b/src/blenderbim/blenderbim/bim/helper.py index 48bc67a294..a1bb904f74 100644 --- a/src/blenderbim/blenderbim/bim/helper.py +++ b/src/blenderbim/blenderbim/bim/helper.py @@ -29,6 +29,7 @@ from mathutils import geometry from mathutils import Vector import blenderbim.tool as tool from blenderbim.bim.ifc import IfcStore +from typing import Optional, Callable, Any def draw_attributes(props, layout, copy_operator=None, popup_active_attribute=None): @@ -75,6 +76,10 @@ def draw_attribute(attribute, layout, copy_operator=None): op.data_path = attribute.path_from_id("string_value") if attribute.is_optional: layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + + if attribute.name == "GlobalId": + layout.operator("bim.generate_global_id", icon="FILE_REFRESH", text="") + if copy_operator: op = layout.operator(f"{copy_operator}", text="", icon="COPYDOWN") op.name = attribute.name @@ -116,7 +121,7 @@ def import_attribute(attribute, props, data, callback=None): elif is_handled_by_callback is False: props.remove(len(props) - 1) elif data_type == "string": - new.string_value = "" if new.is_null else str(data[attribute.name()]) + new.string_value = "" if new.is_null else str(data[attribute.name()]).replace("\n", "\\n") if attribute.type_of_attribute().declared_type().name() == "IfcURIReference": new.is_uri = True elif data_type == "boolean": @@ -178,7 +183,7 @@ def add_attribute_description(attribute_blender, attribute_ifc=None): attribute_blender.description = description -def export_attributes(props, callback=None): +def export_attributes(props, callback: Optional[Callable] = None) -> dict[str, Any]: attributes = {} for prop in props: is_handled_by_callback = callback(attributes, prop) if callback else False @@ -216,39 +221,6 @@ def get_enum_items(data, prop_name, context=None): return items -def get_obj_ifc_definition_id(context, obj, obj_type): - if obj_type == "Object": - return bpy.data.objects.get(obj).BIMObjectProperties.ifc_definition_id - elif obj_type == "Material": - return bpy.data.materials.get(obj).BIMObjectProperties.ifc_definition_id - elif obj_type == "MaterialSet": - return ifcopenshell.util.element.get_material( - tool.Ifc.get_entity(bpy.data.objects.get(obj)), should_skip_usage=True - ).id() - elif obj_type == "MaterialSetItem": - return bpy.data.objects.get(obj).BIMObjectMaterialProperties.active_material_set_item_id - elif obj_type == "Task": - return context.scene.BIMTaskTreeProperties.tasks[ - context.scene.BIMWorkScheduleProperties.active_task_index - ].ifc_definition_id - elif obj_type == "Cost": - return context.scene.BIMCostProperties.cost_items[ - context.scene.BIMCostProperties.active_cost_item_index - ].ifc_definition_id - elif obj_type == "Resource": - return context.scene.BIMResourceTreeProperties.resources[ - context.scene.BIMResourceProperties.active_resource_index - ].ifc_definition_id - elif obj_type == "Profile": - return context.scene.BIMProfileProperties.profiles[ - context.scene.BIMProfileProperties.active_profile_index - ].ifc_definition_id - elif obj_type == "WorkSchedule": - return context.scene.BIMWorkScheduleProperties.active_work_schedule_id - elif obj_type == "Group": - prop = context.scene.BIMGroupProperties - return prop.groups[prop.active_group_index].ifc_definition_id - # hack to close popup # https://blender.stackexchange.com/a/202576/130742 def close_operator_panel(event): @@ -304,6 +276,9 @@ def draw_filter(layout, filter_groups, data, module): op.type = sprops.facet op.index = i op.module = module + op = row.operator("bim.remove_filter_group", text="", icon="X") + op.index = i + op.module = module for j, ifc_filter in enumerate(filter_group.filters): if ifc_filter.type == "entity": @@ -350,11 +325,6 @@ def draw_filter(layout, filter_groups, data, module): op.index = j op.module = module - row = box.row() - op = row.operator("bim.remove_filter_group", icon="X") - op.index = i - op.module = module - # TODO this should move into ifcopenshell.util diff --git a/src/blenderbim/blenderbim/bim/ifc.py b/src/blenderbim/blenderbim/bim/ifc.py index f58cd080bd..93014e6cac 100644 --- a/src/blenderbim/blenderbim/bim/ifc.py +++ b/src/blenderbim/blenderbim/bim/ifc.py @@ -23,33 +23,39 @@ import hashlib import zipfile import tempfile import ifcopenshell +import ifcopenshell.geom +import ifcopenshell.ifcopenshell_wrapper import blenderbim.bim.handler import blenderbim.tool as tool from pathlib import Path from blenderbim.tool.brick import BrickStore +from typing import Set, Union, Optional + + +IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object] class IfcStore: - path = "" - file = None - schema = None - cache = None - cache_path = None - id_map = {} - guid_map = {} - edited_objs = set() - pset_template_path = "" - pset_template_file = None - classification_path = "" - classification_file = None - library_path = "" - library_file = None + path: str = "" + file: ifcopenshell.file = None + schema: ifcopenshell.ifcopenshell_wrapper.schema_definition = None + cache: ifcopenshell.ifcopenshell_wrapper.HdfSerializer = None + cache_path: str = None + id_map: dict[int, IFC_CONNECTED_TYPE] = {} + guid_map: dict[str, IFC_CONNECTED_TYPE] = {} + edited_objs: Set[bpy.types.Object] = set() + pset_template_path: str = "" + pset_template_file: ifcopenshell.file = None + classification_path: str = "" + classification_file: ifcopenshell.file = None + library_path: str = "" + library_file: ifcopenshell.file = None current_transaction = "" last_transaction = "" history = [] future = [] schema_identifiers = ["IFC4", "IFC2X3", "IFC4X3"] - session_files = {} + session_files: dict[str, ifcopenshell.file] = {} @staticmethod def purge(): @@ -117,7 +123,7 @@ class IfcStore: IfcStore.get_cache() @staticmethod - def load_file(path): + def load_file(path) -> None: if not os.path.isfile(path): return extension = path.split(".")[-1] @@ -146,7 +152,7 @@ class IfcStore: return IfcStore.schema @staticmethod - def get_element(id_or_guid): + def get_element(id_or_guid: Union[int, str]) -> IFC_CONNECTED_TYPE: if isinstance(id_or_guid, int): map_object = IfcStore.id_map else: @@ -159,7 +165,7 @@ class IfcStore: return obj @staticmethod - def relink_all_objects(): + def relink_all_objects() -> None: if not IfcStore.get_file(): return for obj in bpy.data.objects: @@ -172,7 +178,7 @@ class IfcStore: IfcStore.relink_object(obj) @staticmethod - def relink_object(obj): + def relink_object(obj: IFC_CONNECTED_TYPE) -> None: if not obj: return if obj.BIMObjectProperties.ifc_definition_id: @@ -193,7 +199,7 @@ class IfcStore: IfcStore.commit_link_element(data) @staticmethod - def link_element(element, obj): + def link_element(element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None: # Please use tool.Ifc.link() instead of this method. We want to # refactor this class and deprecate usage of IfcStore in favour of # tools. @@ -259,7 +265,7 @@ class IfcStore: # TODO We're handling id_map and guid_map, but what about edited_objs? This might cause big problems. @staticmethod - def rollback_unlink_element(data): + def rollback_unlink_element(data) -> None: if "id" not in data or "obj" not in data: return obj = bpy.data.objects.get(data["obj"]) @@ -268,13 +274,15 @@ class IfcStore: IfcStore.guid_map[data["guid"]] = obj @staticmethod - def commit_unlink_element(data): + def commit_unlink_element(data) -> None: del IfcStore.id_map[data["id"]] if data["guid"]: del IfcStore.guid_map[data["guid"]] @staticmethod - def unlink_element(element=None, obj=None): + def unlink_element( + element: Optional[ifcopenshell.entity_instance] = None, obj: Optional[IFC_CONNECTED_TYPE] = None + ) -> None: if element is None: try: element = tool.Ifc.get_entity(obj) @@ -320,7 +328,7 @@ class IfcStore: ) @staticmethod - def execute_ifc_operator(operator, context, is_invoke=False): + def execute_ifc_operator(operator: bpy.types.Operator, context: bpy.types.Context, is_invoke=False): bpy.context.scene.BIMProperties.is_dirty = True is_top_level_operator = not bool(IfcStore.current_transaction) @@ -354,17 +362,17 @@ class IfcStore: return result @staticmethod - def begin_transaction(operator): + def begin_transaction(operator: bpy.types.Operator) -> None: IfcStore.current_transaction = str(uuid.uuid4()) operator.transaction_key = IfcStore.current_transaction @staticmethod - def end_transaction(operator): + def end_transaction(operator: bpy.types.Operator) -> None: IfcStore.current_transaction = "" operator.transaction_key = "" @staticmethod - def add_transaction_operation(operator, rollback=None, commit=None): + def add_transaction_operation(operator: bpy.types.Operator, rollback=None, commit=None) -> None: key = getattr(operator, "transaction_key", None) data = getattr(operator, "transaction_data", None) bpy.context.scene.BIMProperties.last_transaction = key @@ -380,7 +388,7 @@ class IfcStore: IfcStore.future = [] @staticmethod - def undo(until_key=None): + def undo(until_key=None) -> None: BrickStore.undo() if not IfcStore.history: return @@ -395,7 +403,7 @@ class IfcStore: IfcStore.future.append(event) @staticmethod - def redo(until_key=None): + def redo(until_key=None) -> None: BrickStore.redo() if not IfcStore.future: diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index 970c4b9cf2..02173e2681 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -32,24 +32,28 @@ import ifcopenshell.geom import ifcopenshell.util.unit import ifcopenshell.util.element import ifcopenshell.util.geolocation +import ifcopenshell.util.placement +import ifcopenshell.util.representation import blenderbim.tool as tool +import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper from itertools import chain, accumulate -from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.ifc import IfcStore, IFC_CONNECTED_TYPE +from blenderbim.tool.loader import OBJECT_DATA_TYPE from blenderbim.bim.module.drawing.prop import ANNOTATION_TYPES_DATA -from typing import Dict +from typing import Dict, Union, Optional, Any class MaterialCreator: def __init__(self, ifc_import_settings: IfcImportSettings, ifc_importer: IfcImporter): - self.mesh = None - self.obj = None + self.mesh: bpy.types.Mesh = None + self.obj: bpy.types.Object = None self.materials: Dict[int, bpy.types.Material] = {} self.styles: Dict[int, bpy.types.Material] = {} - self.parsed_meshes = set() + self.parsed_meshes: set[str] = set() self.ifc_import_settings = ifc_import_settings self.ifc_importer = ifc_importer - def create(self, element, obj, mesh): + def create(self, element: ifcopenshell.entity_instance, obj: bpy.types.Object, mesh: bpy.types.Mesh) -> None: self.mesh = mesh # as ifcopenshell triangulates the mesh, we need to merge it to quads again self.obj = obj @@ -65,7 +69,7 @@ class MaterialCreator: self.assign_material_slots_to_faces() tool.Geometry.record_object_materials(obj) - def add_default_material(self, element): + def add_default_material(self, element: ifcopenshell.entity_instance) -> None: element_material = ifcopenshell.util.element.get_material(element) if not element_material: return @@ -83,14 +87,17 @@ class MaterialCreator: self.mesh.materials.append(self.materials[material.id()]) return - def load_existing_materials(self): + def load_existing_materials(self) -> None: for material in bpy.data.materials: if material.BIMObjectProperties.ifc_definition_id: self.materials[material.BIMObjectProperties.ifc_definition_id] = material if material.BIMMaterialProperties.ifc_style_id: self.styles[material.BIMMaterialProperties.ifc_style_id] = material - def parse_representations(self, element): + def parse_representations(self, element: ifcopenshell.entity_instance) -> bool: + """Search for styles in in all `element`'s representation items + and adds them to `self.mesh.materials`.\n + Returns `True` if any styles were found and added, returns `False` otherwise.""" has_parsed = False if hasattr(element, "Representation"): for representation in element.Representation.Representations: @@ -104,7 +111,7 @@ class MaterialCreator: has_parsed = True return has_parsed - def parse_representation(self, representation): + def parse_representation(self, representation: ifcopenshell.entity_instance) -> bool: has_parsed = False representation_items = self.resolve_all_stylable_representation_items(representation) for item in representation_items: @@ -112,9 +119,9 @@ class MaterialCreator: has_parsed = True return has_parsed - def parse_representation_item(self, item): + def parse_representation_item(self, item: ifcopenshell.entity_instance) -> bool: if not item.StyledByItem: - return + return False style_ids = [] styles = list(item.StyledByItem[0].Styles) while styles: @@ -124,11 +131,12 @@ class MaterialCreator: elif style.is_a("IfcPresentationStyleAssignment"): styles.extend(style.Styles) if not style_ids: - return + return False for style_id in style_ids: material = self.styles[style_id] - def get_ifc_coordinate(material): + def get_ifc_coordinate(material: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: + """returns IfcTextureCoordinate""" texture_style = tool.Style.get_texture_style(material) if not texture_style: return @@ -149,7 +157,7 @@ class MaterialCreator: self.mesh.materials.append(material) return True - def assign_material_slots_to_faces(self): + def assign_material_slots_to_faces(self) -> None: if "ios_materials" not in self.mesh or not self.mesh["ios_materials"]: return if len(self.mesh.materials) == 1: @@ -157,8 +165,12 @@ class MaterialCreator: material_to_slot = {} if len(self.mesh.polygons) == len(self.mesh["ios_material_ids"]): - for i, style_id in enumerate(self.mesh["ios_materials"]): - slot_index = self.mesh.materials.find(self.styles[style_id].name) + for i, style_or_material_id in enumerate(self.mesh["ios_materials"]): + if style_or_material_id in self.styles: + blender_material = self.styles[style_or_material_id] + else: + blender_material = self.materials[style_or_material_id] + slot_index = self.mesh.materials.find(blender_material.name) material_to_slot[i] = slot_index material_index = [ @@ -166,7 +178,10 @@ class MaterialCreator: ] self.mesh.polygons.foreach_set("material_index", material_index) - def resolve_all_stylable_representation_items(self, representation): + def resolve_all_stylable_representation_items( + self, representation: ifcopenshell.entity_instance + ) -> list[ifcopenshell.entity_instance]: + """returns list of resolved IfcRepresentationItems""" items = [] for item in representation.Items: if item.is_a("IfcMappedItem"): @@ -187,21 +202,27 @@ class IfcImporter: def __init__(self, ifc_import_settings): self.ifc_import_settings = ifc_import_settings self.diff = None - self.file = None - self.context_settings = [] - self.gross_context_settings = [] + self.file: ifcopenshell.file = None + self.context_settings: list[ifcopenshell.geom.main.settings] = [] + self.gross_context_settings: list[ifcopenshell.geom.main.settings] = [] self.contexts = [] self.project = None self.has_existing_project = False - self.collections = {} - self.elements = set() - self.type_collection = None + # element guids to blender collections mapping + self.collections: dict[str, bpy.types.Collection] = {} + self.elements: set[ifcopenshell.entity_instance] = set() + self.annotations: set[ifcopenshell.entity_instance] = set() + self.gross_elements: set[ifcopenshell.entity_instance] = set() + self.element_types: set[ifcopenshell.entity_instance] = set() + self.spatial_elements: set[ifcopenshell.entity_instance] = set() + self.type_collection: bpy.types.Collection = None self.type_products = {} self.meshes = {} self.mesh_shapes = {} self.time = 0 self.unit_scale = 1 - self.added_data = {} + # ifc definition ids to blender elements mapping + self.added_data: dict[int, IFC_CONNECTED_TYPE] = {} self.native_elements = set() self.native_data = {} self.progress = 0 @@ -440,7 +461,9 @@ class IfcImporter: for element in self.gross_elements: print(element) - def get_spatial_elements_filtered_by_elements(self, elements): + def get_spatial_elements_filtered_by_elements( + self, elements: set[ifcopenshell.entity_instance] + ) -> set[ifcopenshell.entity_instance]: leaf_spatial_elements = set([ifcopenshell.util.element.get_container(e) for e in elements]) results = set() for spatial_element in leaf_spatial_elements: @@ -776,7 +799,7 @@ class IfcImporter: continue self.create_element_type(element_type) - def create_element_type(self, element): + def create_element_type(self, element: ifcopenshell.entity_instance) -> None: self.ifc_import_settings.logger.info("Creating object %s", element) mesh = None if self.ifc_import_settings.should_load_geometry: @@ -833,17 +856,23 @@ class IfcImporter: self.create_product(element, mesh=mesh) print("Done creating geometry") - def create_spatial_elements(self): - self.create_generic_elements(self.spatial_elements) + def create_spatial_elements(self) -> None: + if bpy.context.preferences.addons["blenderbim"].preferences.spatial_elements_unselectable: + self.create_generic_elements(self.spatial_elements, unselectable=True) + else: + self.create_generic_elements(self.spatial_elements, unselectable=False) - def create_elements(self): + + def create_elements(self) -> None: self.create_generic_elements(self.elements) tmp = self.context_settings self.context_settings = self.gross_context_settings self.create_generic_elements(self.gross_elements) self.context_settings = tmp - def create_generic_shape(self, element): + def create_generic_shape( + self, element: ifcopenshell.entity_instance + ) -> Union[ifcopenshell_wrapper.TriangulationElement, None]: for settings in self.context_settings: try: result = ifcopenshell.geom.create_shape(settings, element) @@ -852,7 +881,7 @@ class IfcImporter: except: pass - def create_generic_elements(self, elements): + def create_generic_elements(self, elements: set[ifcopenshell.entity_instance], unselectable=False) -> None: if isinstance(self.file, ifcopenshell.sqlite): return self.create_generic_sqlite_elements(elements) @@ -866,12 +895,17 @@ class IfcImporter: elements -= products total = len(elements) + objects = set() for i, element in enumerate(elements): if i % 250 == 0: print("{} / {} elements processed ...".format(i, total)) - self.create_product(element) + objects.add(self.create_product(element)) - def create_generic_sqlite_elements(self, elements): + if unselectable: + for obj in objects: + obj.hide_select = True + + def create_generic_sqlite_elements(self, elements: set[ifcopenshell.entity_instance]) -> None: self.geometry_cache = self.file.get_geometry([e.id() for e in elements]) for geometry_id, geometry in self.geometry_cache["geometry"].items(): mesh_name = tool.Loader.get_mesh_name(type("Geometry", (), {"id": geometry_id})) @@ -918,7 +952,9 @@ class IfcImporter: mesh = self.meshes.get(mesh_name) self.create_product(element, mesh=mesh) - def create_products(self, products, settings=None): + def create_products( + self, products, settings: Optional[ifcopenshell.geom.main.settings] = None + ) -> set[ifcopenshell.entity_instance]: results = set() if not products: return results @@ -1041,7 +1077,7 @@ class IfcImporter: return mapped_representation return None - def create_pointclouds(self, products): + def create_pointclouds(self, products: set[ifcopenshell.entity_instance]) -> set[ifcopenshell.entity_instance]: result = set() for product in products: representation = self.get_pointcloud_representation(product) @@ -1052,7 +1088,9 @@ class IfcImporter: return result - def create_pointcloud(self, product, representation): + def create_pointcloud( + self, product: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance + ) -> Union[ifcopenshell.entity_instance, None]: placement_matrix = self.get_element_matrix(product) vertex_list = [] for item in representation.Items: @@ -1080,7 +1118,12 @@ class IfcImporter: self.link_element(product, obj) return product - def create_product(self, element, shape=None, mesh=None): + def create_product( + self, + element: ifcopenshell.entity_instance, + shape: Optional[Any] = None, + mesh: Optional[OBJECT_DATA_TYPE] = None, + ) -> Union[bpy.types.Object, None]: if element is None: return @@ -1125,6 +1168,9 @@ class IfcImporter: return obj + def load_existing_meshes(self) -> None: + self.meshes.update({m.name: m for m in bpy.data.meshes}) + def get_representation_item_material_name(self, item): if not item.StyledByItem: return @@ -1531,18 +1577,19 @@ class IfcImporter: "{}/{}".format(self.project["ifc"].is_a(), self.project["ifc"].Name) ) obj = self.create_product(self.project["ifc"]) + obj.hide_select = True self.project["blender"].objects.link(obj) self.project["blender"].BIMCollectionProperties.obj = obj obj.BIMObjectProperties.collection = self.collections[project.GlobalId] = self.project["blender"] - def create_collections(self): + def create_collections(self) -> None: self.create_spatial_decomposition_collections() if self.ifc_import_settings.collection_mode == "DECOMPOSITION": self.create_aggregate_and_nest_collections() elif self.ifc_import_settings.collection_mode == "SPATIAL_DECOMPOSITION": pass - def create_spatial_decomposition_collections(self): + def create_spatial_decomposition_collections(self) -> None: for rel_aggregate in self.project["ifc"].IsDecomposedBy or []: self.create_spatial_decomposition_collection(self.project["blender"], rel_aggregate.RelatedObjects) @@ -1555,7 +1602,9 @@ class IfcImporter: tool.Loader.create_project_collection("Views") self.type_collection = tool.Loader.create_project_collection("Types") - def create_spatial_decomposition_collection(self, parent, related_objects): + def create_spatial_decomposition_collection( + self, parent: Union[bpy.types.Collection, bpy.types.Object], related_objects: list[ifcopenshell.entity_instance] + ) -> None: for element in related_objects: if element not in self.spatial_elements: continue @@ -1595,7 +1644,11 @@ class IfcImporter: ] + [ r for r in self.file.by_type("IfcRelNests") - if (r.RelatingObject.is_a("IfcElement") or r.RelatingObject.is_a("IfcElementType")) + if ( + r.RelatingObject.is_a("IfcElement") + or r.RelatingObject.is_a("IfcElementType") + or (r.RelatingObject.is_a("IfcPositioningElement") and not r.RelatingObject.is_a("IfcGrid")) + ) and [e for e in r.RelatedObjects if not e.is_a("IfcPort")] ] @@ -1605,9 +1658,9 @@ class IfcImporter: self.ifc_import_settings.collection_mode = "SPATIAL_DECOMPOSITION" return - aggregates = {} + aggregates: dict[str, dict] = {} for rel_aggregate in rel_aggregates: - element = rel_aggregate.RelatingObject + element: ifcopenshell.entity_instance = rel_aggregate.RelatingObject collection = bpy.data.collections.new(tool.Loader.get_name(element)) aggregates[element.GlobalId] = {"element": element, "collection": collection} self.collections[element.GlobalId] = collection @@ -1659,7 +1712,9 @@ class IfcImporter: continue self.create_style(style) - def create_style(self, style, blender_material=None): + def create_style( + self, style: ifcopenshell.entity_instance, blender_material: Optional[bpy.types.Material] = None + ) -> None: if not blender_material: name = style.Name or str(style.id()) blender_material = bpy.data.materials.new(name) @@ -1676,18 +1731,20 @@ class IfcImporter: else: blender_material.BIMStyleProperties.active_style_type = "Shading" - def place_objects_in_collections(self): + def place_objects_in_collections(self) -> None: for ifc_definition_id, obj in self.added_data.items(): if isinstance(obj, bpy.types.Object): self.place_object_in_collection(self.file.by_id(ifc_definition_id), obj) - def place_object_in_collection(self, element, obj): + def place_object_in_collection(self, element: ifcopenshell.entity_instance, obj: bpy.types.Object) -> None: if self.ifc_import_settings.collection_mode == "DECOMPOSITION": self.place_object_in_decomposition_collection(element, obj) elif self.ifc_import_settings.collection_mode == "SPATIAL_DECOMPOSITION": self.place_object_in_spatial_decomposition_collection(element, obj) - def place_object_in_decomposition_collection(self, element, obj): + def place_object_in_decomposition_collection( + self, element: ifcopenshell.entity_instance, obj: bpy.types.Object + ) -> None: if element.is_a("IfcProject"): return elif element.is_a("IfcGridAxis"): @@ -1704,10 +1761,12 @@ class IfcImporter: elif getattr(element, "Nests", None) and not element.is_a("IfcPort"): nest = ifcopenshell.util.element.get_nest(element) return self.collections[nest.GlobalId].objects.link(obj) - else: - return self.place_object_in_spatial_decomposition_collection(element, obj) - def place_object_in_spatial_decomposition_collection(self, element, obj): + return self.place_object_in_spatial_decomposition_collection(element, obj) + + def place_object_in_spatial_decomposition_collection( + self, element: ifcopenshell.entity_instance, obj: bpy.types.Object + ) -> None: if element.is_a("IfcProject"): return elif element.is_a("IfcGridAxis"): @@ -1751,7 +1810,7 @@ class IfcImporter: if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING": return rel.RelatingGroup - def get_element_matrix(self, element): + def get_element_matrix(self, element: ifcopenshell.entity_instance) -> np.array: if isinstance(element, ifcopenshell.sqlite_entity): result = self.geometry_cache["shapes"][element.id()]["matrix"] else: @@ -1761,7 +1820,7 @@ class IfcImporter: result[2][3] *= self.unit_scale return result - def scale_matrix(self, matrix): + def scale_matrix(self, matrix: np.array) -> np.array: matrix[0][3] *= self.unit_scale matrix[1][3] *= self.unit_scale matrix[2][3] *= self.unit_scale @@ -1793,7 +1852,7 @@ class IfcImporter: ): return representation.Items[0].MappingTarget - def create_curve(self, element, shape): + def create_curve(self, element: ifcopenshell.entity_instance, shape) -> bpy.types.Curve: if hasattr(shape, "geometry"): geometry = shape.geometry else: @@ -1818,12 +1877,13 @@ class IfcImporter: polyline.points[-1].co = mathutils.Vector(v2) return curve - def create_mesh(self, element, shape): + def create_mesh(self, element: ifcopenshell.entity_instance, shape) -> bpy.types.Mesh: try: if hasattr(shape, "geometry"): - geometry = shape.geometry + # shape is ifcopenshell_wrapper.TriangulationElement + geometry: ifcopenshell_wrapper.Triangulation = shape.geometry else: - geometry = shape + geometry: ifcopenshell_wrapper.Triangulation = shape mesh = bpy.data.meshes.new(tool.Loader.get_mesh_name(geometry)) @@ -1989,7 +2049,7 @@ class IfcImportSettings: self.has_filter = None self.should_filter_spatial_elements = True self.should_setup_viewport_camera = True - self.elements = set() + self.elements: set[ifcopenshell.entity_instance] = set() self.collection_mode = "DECOMPOSITION" @staticmethod diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py index 9e26e78db0..89185b9021 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py @@ -44,7 +44,12 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, Operator): def _execute(self, context): relating_obj = None if self.relating_object: - relating_obj = tool.Ifc.get_object(tool.Ifc.get().by_id(self.relating_object)) + element = tool.Ifc.get().by_id(self.relating_object) + if element.IsDecomposedBy: + relating_obj = tool.Ifc.get_object(element) + else: + assembly = element.Decomposes[0].RelatingObject + relating_obj = tool.Ifc.get_object(assembly) elif context.active_object: relating_obj = context.active_object if not relating_obj: @@ -89,7 +94,7 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, Operator): relating_obj=tool.Ifc.get_object(aggregate), related_obj=tool.Ifc.get_object(element), ) - + # Removes Pset related to Linked Aggregates if not element.is_a('IfcElementAssembly'): pset = ifcopenshell.util.element.get_pset(element, 'BBIM_Linked_Aggregate') @@ -97,7 +102,7 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, Operator): pset = tool.Ifc.get().by_id(pset["id"]) ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset) - + class BIM_OT_enable_editing_aggregate(bpy.types.Operator, Operator): """Enable editing aggregation relationship""" @@ -202,15 +207,29 @@ class BIM_OT_select_aggregate(bpy.types.Operator): bl_label = "Select Aggregate" bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() + select_parts: bpy.props.BoolProperty(default=False) + + @classmethod + def description(cls, context, properties): + if properties.select_parts: + return "Select Aggregate and Parts" + else: + return "Select Aggregate" def execute(self, context): self.file = IfcStore.get_file() obj = bpy.data.objects.get(self.obj) or context.active_object aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(obj)) aggregate_obj = tool.Ifc.get_object(aggregate) + if self.select_parts: + bpy.ops.bim.select_parts(obj=aggregate_obj.name) if aggregate_obj in context.selectable_objects: - aggregate_obj.select_set(True) - bpy.context.view_layer.objects.active = aggregate_obj + tool.Blender.set_objects_selection( + context, + aggregate_obj, + [aggregate_obj], + clear_previous_selection=not self.select_parts, + ) return {"FINISHED"} @@ -238,6 +257,7 @@ class BIM_OT_add_part_to_object(bpy.types.Operator, Operator): part_name=self.part_name, ) + class BIM_OT_break_link_to_other_aggregates(bpy.types.Operator, Operator): bl_idname = "bim.break_link_to_other_aggregates" bl_label = "Break link to other aggregates" @@ -252,22 +272,23 @@ class BIM_OT_break_link_to_other_aggregates(bpy.types.Operator, Operator): return [] parts = ifcopenshell.util.element.get_parts(aggregate) - + for part in parts: - pset = ifcopenshell.util.element.get_pset(part, 'BBIM_Linked_Aggregate') + pset = ifcopenshell.util.element.get_pset(part, "BBIM_Linked_Aggregate") pset = tool.Ifc.get().by_id(pset["id"]) ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset) - + linked_aggregate_group = [ r.RelatingGroup for r in getattr(aggregate, "HasAssignments", []) or [] if r.is_a("IfcRelAssignsToGroup") if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name ] - tool.Ifc.run("group.unassign_group", group=linked_aggregate_group[0], product=aggregate) - + tool.Ifc.run("group.unassign_group", group=linked_aggregate_group[0], products=[aggregate]) + return {"FINISHED"} + class BIM_OT_select_linked_aggregates(bpy.types.Operator, Operator): bl_idname = "bim.select_linked_aggregates" bl_label = "Select linked aggregates" @@ -280,17 +301,17 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator, Operator): return [] if not element: return [] - + linked_aggregate_group = [ r.RelatingGroup for r in getattr(aggregate, "HasAssignments", []) or [] if r.is_a("IfcRelAssignsToGroup") if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name ] - + for obj in context.selected_objects: obj.select_set(False) - + for rel in linked_aggregate_group[0].IsGroupedBy or []: for element in rel.RelatedObjects: obj = tool.Ifc.get_object(element) diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py index dcef2a9435..41f4b5b98d 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py @@ -62,8 +62,12 @@ class BIM_PT_aggregate(Panel): row = layout.row(align=True) if AggregateData.data["has_relating_object"]: row.label(text=AggregateData.data["relating_object_label"], icon="TRIA_UP") + op = row.operator("bim.select_aggregate", icon="OBJECT_DATA", text="") + op.obj = context.active_object.name + op.select_parts = False op = row.operator("bim.select_aggregate", icon="RESTRICT_SELECT_OFF", text="") op.obj = context.active_object.name + op.select_parts = True row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="") row.operator("bim.add_aggregate", icon="ADD", text="") op = row.operator("bim.aggregate_unassign_object", icon="X", text="") diff --git a/src/blenderbim/blenderbim/bim/module/attribute/operator.py b/src/blenderbim/blenderbim/bim/module/attribute/operator.py index 89b2d06df2..a86ff15e21 100644 --- a/src/blenderbim/blenderbim/bim/module/attribute/operator.py +++ b/src/blenderbim/blenderbim/bim/module/attribute/operator.py @@ -47,10 +47,18 @@ class EnableEditingAttributes(bpy.types.Operator): obj = bpy.data.objects.get(self.obj) elif self.obj_type == "Material": obj = bpy.data.materials.get(self.obj) - oprops = obj.BIMObjectProperties props = obj.BIMAttributeProperties props.attributes.clear() + element = tool.Ifc.get_entity(obj) + has_inherited_predefined_type = False + if not element.is_a("IfcTypeObject") and (element_type := ifcopenshell.util.element.get_type(element)): + # Allow for None due to https://github.com/buildingSMART/IFC4.3.x-development/issues/818 + has_inherited_predefined_type = ifcopenshell.util.element.get_predefined_type(element_type) not in ( + "NOTDEFINED", + None, + ) + def callback(name, prop, data): if name in ("RefLatitude", "RefLongitude"): new = props.attributes.add() @@ -62,10 +70,11 @@ class EnableEditingAttributes(bpy.types.Operator): new.string_value = "" if new.is_null else json.dumps(data[name]) blenderbim.bim.helper.add_attribute_description(new) new.description += " The degrees, minutes and seconds should follow this format : [12,34,56]" + if name in ("PredefinedType", "ObjectType") and has_inherited_predefined_type: + props.attributes.remove(len(props.attributes) - 1) + return True - blenderbim.bim.helper.import_attributes2( - tool.Ifc.get().by_id(oprops.ifc_definition_id), props.attributes, callback=callback - ) + blenderbim.bim.helper.import_attributes2(element, props.attributes, callback=callback) props.is_editing_attributes = True return {"FINISHED"} @@ -120,20 +129,46 @@ class EditAttributes(bpy.types.Operator, Operator): return {"FINISHED"} -class GenerateGlobalId(bpy.types.Operator): +class GenerateGlobalId(bpy.types.Operator, Operator): bl_idname = "bim.generate_global_id" bl_label = "Regenerate GlobalId" + bl_description = "Regenerate GlobalId\n\nSHIFT+CLICK to regenerate GlobalIds for all selected objects" bl_options = {"REGISTER", "UNDO"} - def execute(self, context): - index = context.active_object.BIMAttributeProperties.attributes.find("GlobalId") - if index >= 0: - global_id = context.active_object.BIMAttributeProperties.attributes[index] + use_selected: bpy.props.BoolProperty(name="Use All Selected Objects", default=False, options={"SKIP_SAVE"}) + + def invoke(self, context, event): + # using all selected objects on shift+click + # make sure to use SKIP_SAVE on property, otherwise it might get stuck + if event.type == "LEFTMOUSE" and event.shift: + self.use_selected = True + return self.execute(context) + + def _execute(self, context): + if self.use_selected: + for obj in context.selected_objects: + element = tool.Ifc.get_entity(obj) + if not element or not element.is_a("IfcRoot"): + continue + element.GlobalId = ifcopenshell.guid.new() + + obj = context.active_object + if not obj or not obj.BIMAttributeProperties.is_editing_attributes: + return {"FINISHED"} + + props = obj.BIMAttributeProperties + element = tool.Ifc.get_entity(obj) + + if not element.is_a("IfcRoot"): + return {"FINISHED"} + + if self.use_selected and obj in context.selected_objects: + # guid value was already regenerated, just update the ui prop + guid_value = element.GlobalId else: - global_id = context.active_object.BIMAttributeProperties.attributes.add() - global_id.name = "GlobalId" - global_id.data_type = "string" - global_id.string_value = ifcopenshell.guid.new() + guid_value = ifcopenshell.guid.new() + + props.attributes["GlobalId"].string_value = guid_value return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/boundary/__init__.py b/src/blenderbim/blenderbim/bim/module/boundary/__init__.py index 389b1e64c9..02537d043d 100644 --- a/src/blenderbim/blenderbim/bim/module/boundary/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/boundary/__init__.py @@ -22,6 +22,7 @@ from . import ui, operator, prop classes = ( operator.AddBoundary, operator.ColourByRelatedBuildingElement, + operator.DecorateBoundaries, operator.DisableEditingBoundary, operator.DisableEditingBoundaryGeometry, operator.EditBoundaryAttributes, diff --git a/src/blenderbim/blenderbim/bim/module/boundary/operator.py b/src/blenderbim/blenderbim/bim/module/boundary/operator.py index fda54ceb63..6a20852f06 100644 --- a/src/blenderbim/blenderbim/bim/module/boundary/operator.py +++ b/src/blenderbim/blenderbim/bim/module/boundary/operator.py @@ -24,6 +24,7 @@ import mathutils import numpy as np import multiprocessing import ifcopenshell.api +import ifcopenshell.geom import ifcopenshell.util.unit import ifcopenshell.util.shape import ifcopenshell.util.element @@ -37,6 +38,7 @@ from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.model.decorator import ProfileDecorator from blenderbim.bim.module.boundary.decorator import BoundaryDecorator import blenderbim.core +import blenderbim.core.geometry def get_boundaries_collection(blender_space): @@ -147,7 +149,9 @@ class Loader: self.ifc_importer = import_ifc.IfcImporter(ifc_import_settings) self.ifc_importer.file = self.ifc_file - def load_boundary(self, boundary, blender_space=None): + def load_boundary( + self, boundary: ifcopenshell.entity_instance, blender_space: bpy.types.Object = None + ) -> bpy.types.Object: if not blender_space: if not boundary.RelatingSpace: self.operator.report( @@ -509,10 +513,10 @@ class DisableEditingBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator): class ShowBoundaries(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.show_boundaries" bl_label = "Show Boundaries" + bl_description = "Load selected objects boundaries if they are not loaded" bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - props = bpy.context.scene.BIMBoundaryProperties loader = Loader(self) for obj in context.selected_objects: element = tool.Ifc.get_entity(obj) @@ -524,7 +528,7 @@ class ShowBoundaries(bpy.types.Operator, tool.Ifc.Operator): for rel in element.BoundedBy or []: boundary_obj = loader.load_boundary(rel, obj) tool.Boundary.decorate_boundary(boundary_obj) - BoundaryDecorator.install(bpy.context) + BoundaryDecorator.install(context) return {"FINISHED"} @@ -557,6 +561,36 @@ class HideBoundaries(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} +class DecorateBoundaries(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.decorate_boundaries" + bl_label = "Toggle Boundaries Decorations" + bl_description = "Add special decorations for all boundaries in the scene or hide them if they are already added" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + props = context.scene.BIMBoundaryProperties + # filter not decorated boundaries and add decorations for them + decorated_boundaries = set([i.obj for i in props.boundaries]) + active_boundaries = set() + for element in tool.Ifc.get().by_type("IfcRelSpaceBoundary"): + obj = tool.Ifc.get_object(element) + if obj is not None: + active_boundaries.add(obj) + + boundaries_to_decorate = active_boundaries - decorated_boundaries + if boundaries_to_decorate: + for obj in boundaries_to_decorate: + tool.Boundary.decorate_boundary(obj) + BoundaryDecorator.install(context) + else: + for obj in decorated_boundaries: + tool.Boundary.undecorate_boundary(obj) + props.boundaries.clear() + BoundaryDecorator.uninstall() + tool.Blender.update_viewport() + return {"FINISHED"} + + class AddBoundary(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_boundary" bl_label = "Add Boundary" diff --git a/src/blenderbim/blenderbim/bim/module/cad/prop.py b/src/blenderbim/blenderbim/bim/module/cad/prop.py index daf4346716..ed795a74aa 100644 --- a/src/blenderbim/blenderbim/bim/module/cad/prop.py +++ b/src/blenderbim/blenderbim/bim/module/cad/prop.py @@ -24,10 +24,10 @@ from math import pi class BIMCadProperties(PropertyGroup): resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1) - radius: bpy.props.FloatProperty(name="Radius", default=0.1) - distance: bpy.props.FloatProperty(name="Distance", default=0.1) - x: bpy.props.FloatProperty(name="X", default=0.2) - y: bpy.props.FloatProperty(name="Y", default=0.1) + radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE") + distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE") + x: bpy.props.FloatProperty(name="X", default=0.2, subtype="DISTANCE") + y: bpy.props.FloatProperty(name="Y", default=0.1, subtype="DISTANCE") gable_roof_edge_angle: bpy.props.FloatProperty( name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE" ) diff --git a/src/blenderbim/blenderbim/bim/module/cad/workspace.py b/src/blenderbim/blenderbim/bim/module/cad/workspace.py index 60b04dfc9a..e5b8e33d0a 100644 --- a/src/blenderbim/blenderbim/bim/module/cad/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/cad/workspace.py @@ -20,6 +20,7 @@ import os import bpy import blenderbim.tool as tool import blenderbim.bim.module.type.prop as type_prop +import ifcopenshell.util.unit from bpy.types import WorkSpaceTool from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData @@ -239,8 +240,9 @@ class CadHotkey(bpy.types.Operator): row.prop(props, "resolution") def hotkey_S_C(self): + si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) if self.is_profile(): - bpy.ops.bim.add_ifccircle(radius=self.props.radius) + bpy.ops.bim.add_ifccircle(radius=self.props.radius / si_conversion) else: bpy.ops.bim.cad_arc_from_2_points() @@ -248,13 +250,15 @@ class CadHotkey(bpy.types.Operator): bpy.ops.bim.cad_trim_extend() def hotkey_S_F(self): + si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) if self.is_profile(): - bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius) + bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius / si_conversion) else: - bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius) + bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius / si_conversion) def hotkey_S_O(self): - bpy.ops.bim.cad_offset(distance=self.props.distance) + si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + bpy.ops.bim.cad_offset(distance=self.props.distance / si_conversion) def hotkey_S_Q(self): element = tool.Ifc.get_entity(bpy.context.active_object) @@ -270,7 +274,8 @@ class CadHotkey(bpy.types.Operator): def hotkey_S_R(self): if self.is_profile(): - bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y) + si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + bpy.ops.bim.add_rectangle(x=self.props.x / si_conversion, y=self.props.y / si_conversion) elif ( (RoofData.is_loaded or not RoofData.load()) and RoofData.data["pset_data"] diff --git a/src/blenderbim/blenderbim/bim/module/clash/__init__.py b/src/blenderbim/blenderbim/bim/module/clash/__init__.py index 5cb662fa02..b87fb8c3ff 100644 --- a/src/blenderbim/blenderbim/bim/module/clash/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/clash/__init__.py @@ -25,15 +25,12 @@ classes = ( operator.ExecuteIfcClash, operator.ExportClashSets, operator.ImportClashSets, - operator.LoadIfcClashes, - operator.SaveIfcClashes, operator.LoadSmartGroupsForActiveClashSet, operator.RemoveClashSet, operator.RemoveClashSource, operator.SelectClash, operator.SelectClashResults, operator.SelectClashSource, - operator.SelectIfcClashResults, operator.SelectSmartGroup, operator.SelectSmartGroupedClashesPath, operator.SmartClashGroup, @@ -44,7 +41,6 @@ classes = ( prop.BIMClashProperties, ui.BIM_PT_ifcclash, ui.BIM_PT_clash_manager, - ui.BIM_PT_dumb_clash_manager, ui.BIM_PT_smart_clash_manager, ui.BIM_UL_clashes, ui.BIM_UL_clash_sets, diff --git a/src/blenderbim/blenderbim/bim/module/clash/data.py b/src/blenderbim/blenderbim/bim/module/clash/data.py new file mode 100644 index 0000000000..7f61222f7b --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/clash/data.py @@ -0,0 +1,52 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2024 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +import json +import ifcopenshell.util.element +import blenderbim.tool as tool + + +def refresh(): + ClashData.is_loaded = False + + +class ClashData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.is_loaded = True + cls.data = {} + cls.data["saved_searches"] = cls.saved_searches() + + @classmethod + def saved_searches(cls): + if not tool.Ifc.get(): + return [] + groups = tool.Ifc.get().by_type("IfcGroup") + results = [] + for group in groups: + try: + data = json.loads(group.Description) + if isinstance(data, dict) and data.get("type", None) == "BBIM_Search" and data.get("query", None): + results.append(group) + except: + pass + return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")] diff --git a/src/blenderbim/blenderbim/bim/module/clash/decorator.py b/src/blenderbim/blenderbim/bim/module/clash/decorator.py new file mode 100644 index 0000000000..af7946ddc1 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/clash/decorator.py @@ -0,0 +1,102 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2024 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import blf +import gpu +import bmesh +import blenderbim.tool as tool +from bpy.types import SpaceView3D +from mathutils import Vector +from gpu_extras.batch import batch_for_shader +from bpy_extras.view3d_utils import location_3d_to_region_2d + + +class ClashDecorator: + is_installed = False + handlers = [] + + @classmethod + def install(cls, context): + if cls.is_installed: + cls.uninstall() + handler = cls() + cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL")) + cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_geometry, (context,), "WINDOW", "POST_VIEW")) + cls.is_installed = True + + @classmethod + def uninstall(cls): + for handler in cls.handlers: + try: + SpaceView3D.draw_handler_remove(handler, "WINDOW") + except ValueError: + pass + cls.is_installed = False + + def draw_batch(self, shader_type, content_pos, color, indices=None): + shader = self.line_shader if shader_type == "LINES" else self.shader + batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices) + shader.uniform_float("color", color) + batch.draw(shader) + + def draw_text(self, context): + self.addon_prefs = context.preferences.addons["blenderbim"].preferences + selected_elements_color = self.addon_prefs.decorator_color_selected + unselected_elements_color = self.addon_prefs.decorator_color_unselected + special_elements_color = self.addon_prefs.decorator_color_special + + text = context.scene.BIMClashProperties.active_clash_text + p = context.scene.BIMClashProperties.p1.lerp(context.scene.BIMClashProperties.p2, 0.5) + + font_id = 0 + blf.size(font_id, 12) + coords_2d = location_3d_to_region_2d(context.region, context.region_data, p) + color = self.addon_prefs.decorations_colour + blf.color(font_id, *color) + if coords_2d: + w, h = blf.dimensions(font_id, text) + coords_2d -= Vector((w * .5, 0)) + blf.position(font_id, coords_2d[0], coords_2d[1], 0) + blf.draw(font_id, text) # Set your text here + + def draw_geometry(self, context): + self.addon_prefs = context.preferences.addons["blenderbim"].preferences + selected_elements_color = self.addon_prefs.decorator_color_selected + unselected_elements_color = self.addon_prefs.decorator_color_unselected + special_elements_color = self.addon_prefs.decorator_color_special + + gpu.state.point_size_set(6) + gpu.state.blend_set("ALPHA") + + self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR") + self.line_shader.bind() # required to be able to change uniforms of the shader + # POLYLINE_UNIFORM_COLOR specific uniforms + self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height)) + self.line_shader.uniform_float("lineWidth", 2.0) + + # general shader + self.shader = gpu.shader.from_builtin("UNIFORM_COLOR") + + selected_vertices = [context.scene.BIMClashProperties.p1, context.scene.BIMClashProperties.p2] + selected_edges = [] + if selected_vertices[0] != selected_vertices[1]: + selected_edges = [[0, 1]] + + self.draw_batch("POINTS", selected_vertices, special_elements_color) + if selected_edges: + self.draw_batch("LINES", selected_vertices, special_elements_color, selected_edges) diff --git a/src/blenderbim/blenderbim/bim/module/clash/operator.py b/src/blenderbim/blenderbim/bim/module/clash/operator.py index a56a45f560..f32e7bfad3 100644 --- a/src/blenderbim/blenderbim/bim/module/clash/operator.py +++ b/src/blenderbim/blenderbim/bim/module/clash/operator.py @@ -23,10 +23,11 @@ import bmesh import logging import numpy as np import ifcopenshell -from mathutils import Matrix, Vector -from math import radians -from blenderbim.bim.ifc import IfcStore import blenderbim.tool as tool +from math import radians +from mathutils import Matrix, Vector +from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.module.clash.decorator import ClashDecorator class ExportClashSets(bpy.types.Operator): @@ -45,18 +46,7 @@ class ExportClashSets(bpy.types.Operator): def execute(self, context): self.filepath = bpy.path.ensure_ext(self.filepath, ".json") - clash_sets = [] - for clash_set in context.scene.BIMClashProperties.clash_sets: - self.a = [] - self.b = [] - for ab in ["a", "b"]: - for data in getattr(clash_set, ab): - clash_source = {"file": data.name} - if data.selector: - clash_source["selector"] = data.selector - clash_source["mode"] = data.mode - getattr(self, ab).append(clash_source) - clash_sets.append({"name": clash_set.name, "tolerance": clash_set.tolerance, "a": self.a, "b": self.b}) + clash_sets = tool.Clash.export_clash_sets() with open(self.filepath, "w") as destination: destination.write(json.dumps(clash_sets, indent=4)) return {"FINISHED"} @@ -78,25 +68,34 @@ class ImportClashSets(bpy.types.Operator): return {"RUNNING_MODAL"} def execute(self, context): - with open(self.filepath) as f: - clash_sets = json.load(f) - for clash_set in clash_sets: + tool.Clash.load_clash_sets(self.filepath) + context.scene.BIMClashProperties.clash_sets.clear() + for clash_set in tool.Clash.get_clash_sets(): new = context.scene.BIMClashProperties.clash_sets.add() new.name = clash_set["name"] - new.tolerance = clash_set["tolerance"] + new.mode = clash_set["mode"] + if new.mode == "intersection": + new.tolerance = clash_set["tolerance"] + new.check_all = clash_set["check_all"] + elif new.mode == "collision": + new.allow_touching = clash_set["allow_touching"] + elif new.mode == "clearance": + new.clearance = clash_set["clearance"] + new.check_all = clash_set["check_all"] for clash_source in clash_set["a"]: new_source = new.a.add() new_source.name = clash_source["file"] if "selector" in clash_source: - new_source.selector = clash_source["selector"] + tool.Search.import_filter_query(clash_source["selector"], new_source.filter_groups) new_source.mode = clash_source["mode"] - if clash_set["b"]: + if "b" in clash_set and clash_set["b"]: for clash_source in clash_set["b"]: new_source = new.b.add() new_source.name = clash_source["file"] if "selector" in clash_source: - new_source.selector = clash_source["selector"] + tool.Search.import_filter_query(clash_source["selector"], new_source.filter_groups) new_source.mode = clash_source["mode"] + tool.Clash.import_active_clashes() return {"FINISHED"} @@ -109,7 +108,6 @@ class AddClashSet(bpy.types.Operator): def execute(self, context): new = context.scene.BIMClashProperties.clash_sets.add() new.name = "New Clash Set" - new.tolerance = 0.01 return {"FINISHED"} @@ -211,8 +209,11 @@ class ExecuteIfcClash(bpy.types.Operator): def invoke(self, context, event): _, extension = os.path.splitext(self.filepath) - if extension != ".json": - self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".bcf") + if extension != ".bcf": + self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") + # TODO Temporarily until BCF support comes back + # if extension != ".json": + # self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".bcf") WindowManager = context.window_manager WindowManager.fileselect_add(self) return {"RUNNING_MODAL"} @@ -220,7 +221,12 @@ class ExecuteIfcClash(bpy.types.Operator): def execute(self, context): from ifcclash import ifcclash + self.props = context.scene.BIMClashProperties + _, extension = os.path.splitext(self.filepath) + if extension != ".bcf": + self.filepath = bpy.path.ensure_ext(self.filepath, ".json") + # TODO Temporarily until BCF support comes back if extension != ".json": self.filepath = bpy.path.ensure_ext(self.filepath, ".bcf") @@ -230,7 +236,7 @@ class ExecuteIfcClash(bpy.types.Operator): settings.logger.setLevel(logging.DEBUG) clasher = ifcclash.Clasher(settings) - if context.scene.BIMClashProperties.should_create_clash_snapshots: + if self.props.should_create_clash_snapshots: def get_viewpoint_snapshot(viewpoint): camera = bpy.data.objects.get("IFC Clash Camera") @@ -271,23 +277,13 @@ class ExecuteIfcClash(bpy.types.Operator): clasher.get_viewpoint_snapshot = get_viewpoint_snapshot - clasher.clash_sets = [] - for clash_set in context.scene.BIMClashProperties.clash_sets: - self.a = [] - self.b = [] - for ab in ["a", "b"]: - for data in getattr(clash_set, ab): - clash_source = {"file": data.name} - if data.selector: - clash_source["selector"] = data.selector - clash_source["mode"] = data.mode - getattr(self, ab).append(clash_source) - clash_set_data = {"name": clash_set.name, "tolerance": clash_set.tolerance, "a": self.a} - if self.b: - clash_set_data["b"] = self.b - clasher.clash_sets.append(clash_set_data) + clasher.clash_sets = tool.Clash.export_clash_sets() clasher.clash() clasher.export() + + if extension == ".json": + tool.Clash.load_clash_sets(self.filepath) + tool.Clash.import_active_clashes() return {"FINISHED"} @@ -306,6 +302,7 @@ class SelectIfcClashResults(bpy.types.Operator): return {"RUNNING_MODAL"} def execute(self, context): + # TODO refactor into new clash results system self.file = IfcStore.get_file() self.filepath = bpy.path.ensure_ext(self.filepath, ".json") with open(self.filepath) as f: @@ -352,83 +349,6 @@ class SelectIfcClashResults(bpy.types.Operator): return {"FINISHED"} -class LoadIfcClashes(bpy.types.Operator): - bl_idname = "bim.load_ifc_clashes" - bl_label = "Load IFC Clashes" - bl_options = {"REGISTER", "UNDO"} - bl_description = "Load the clashing IFC geometry stored in a file" - filename_ext = ".json" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def invoke(self, context, event): - self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} - - def execute(self, context): - self.file = IfcStore.get_file() - self.filepath = bpy.path.ensure_ext(self.filepath, ".json") - with open(self.filepath) as f: - clash_sets_json = json.load(f) - active_clash_set = context.scene.BIMClashProperties.active_clash_set - - for clash_set in clash_sets_json: - if not "clashes" in clash_set.keys(): - self.report({"WARNING"}, "No clashes found for the selected Clash Set.") - return {"CANCELLED"} - active_clash_set.clashes.clear() - for clash in clash_set["clashes"].values(): - blender_clash = active_clash_set.clashes.add() - blender_clash.a_global_id = clash["a_global_id"] - blender_clash.b_global_id = clash["b_global_id"] - blender_clash.a_name = "{}/{}".format(clash["a_ifc_class"], clash["a_name"]) - blender_clash.b_name = "{}/{}".format(clash["b_ifc_class"], clash["b_name"]) - blender_clash.status = False if not "status" in clash.keys() else clash["status"] - return {"FINISHED"} - - -class SaveIfcClashes(bpy.types.Operator): - bl_idname = "bim.save_ifc_clashes" - bl_label = "Save IFC Clashes" - bl_options = {"REGISTER", "UNDO"} - bl_description = "Save the clashing IFC geometry stored in a file" - filename_ext = ".json" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def invoke(self, context, event): - self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} - - def execute(self, context): - self.file = IfcStore.get_file() - self.filepath = bpy.path.ensure_ext(self.filepath, ".json") - clash_sets_json = self.load_json() - active_clash_set = context.scene.BIMClashProperties.active_clash_set - self.update_clash_sets(clash_sets_json, active_clash_set) - self.save_json(clash_sets_json) - return {"FINISHED"} - - def load_json(self): - with open(self.filepath) as f: - return json.load(f) - - def update_clash_sets(self, clash_sets_json, active_clash_set): - for clash_set in clash_sets_json: - if clash_set["name"] != active_clash_set.name or "clashes" not in clash_set.keys(): - continue - for clash in active_clash_set.clashes: - clash_key = clash.a_global_id + "-" + clash.b_global_id - if clash_set.get("clashes", {}).get(clash_key, None) is not None: - clash_set["clashes"][clash_key]["status"] = clash.status - - def save_json(self, clash_sets_json): - with open(self.filepath, "w") as destination: - json.dump(clash_sets_json, destination, indent=4) - - class SelectClash(bpy.types.Operator): bl_idname = "bim.select_clash" bl_label = "Select Clash" @@ -437,31 +357,29 @@ class SelectClash(bpy.types.Operator): index: bpy.props.IntProperty() def execute(self, context): - clash = context.scene.BIMClashProperties.active_clash + self.props = context.scene.BIMClashProperties + clash_set = tool.Clash.get_clash_set(self.props.active_clash_set.name) + active_clash = self.props.active_clash + clash = tool.Clash.get_clash(clash_set, active_clash.a_global_id, active_clash.b_global_id) + + if not clash: + return {"FINISHED"} products = [] - linked_objects = [] - try: - products.append(tool.Ifc.get().by_guid(clash.a_global_id)) - except: - linked_objects.append(clash.a_name) - try: - products.append(tool.Ifc.get().by_guid(clash.b_global_id)) - except: - linked_objects.append(clash.b_name) + + for global_id in (clash["a_global_id"], clash["b_global_id"]): + try: + products.append(tool.Ifc.get().by_guid(global_id)) + except: + pass tool.Spatial.select_products(products, unhide=True) - print(linked_objects) - for obj_name in linked_objects: - obj = bpy.data.objects.get(obj_name) - if obj: - obj.select_set(True) - else: - print("Object not found:", obj_name) - if context.scene.BIMClashProperties.sould_focus_on_clash: - context_override = tool.Blender.get_viewport_context() - with bpy.context.temp_override(**context_override): - bpy.ops.view3d.view_selected() + ClashDecorator.install(bpy.context) + target = Vector(clash["p1"]) + tool.Clash.look_at(target, target + Vector((5, 5, 5))) + self.props.p1 = clash["p1"] + self.props.p2 = clash["p2"] + self.props.active_clash_text = clash["type"].title() + " " + str(round(clash["distance"] * 1000)) + "mm" return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/clash/prop.py b/src/blenderbim/blenderbim/bim/module/clash/prop.py index 6bd8120fed..0f7e7c2275 100644 --- a/src/blenderbim/blenderbim/bim/module/clash/prop.py +++ b/src/blenderbim/blenderbim/bim/module/clash/prop.py @@ -17,7 +17,7 @@ # along with BlenderBIM Add-on. If not, see . import bpy -from blenderbim.bim.prop import StrProperty, Attribute +from blenderbim.bim.prop import StrProperty, Attribute, BIMFilterGroup from bpy.types import PropertyGroup from bpy.props import ( PointerProperty, @@ -33,11 +33,12 @@ from bpy.props import ( class ClashSource(PropertyGroup): name: StringProperty(name="File") - selector: StringProperty(name="Selector") + filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups") mode: EnumProperty( items=[ - ("i", "Include", "Only the selected objects are included for clashing"), - ("e", "Exclude", "All objects except the selected objects are included for clashing"), + ("a", "All Elements", "All elements will be used for clashing"), + ("i", "Include", "Only the selected elements are included for clashing"), + ("e", "Exclude", "All elements except the selected elements are included for clashing"), ], name="Mode", ) @@ -53,7 +54,24 @@ class Clash(PropertyGroup): class ClashSet(PropertyGroup): name: StringProperty(name="Name") - tolerance: FloatProperty(name="Tolerance") + mode: EnumProperty( + items=[ + ( + "intersection", + "Intersection", + "Detect objects that protrude or pierce another object", + "PIVOT_MEDIAN", + 1, + ), + ("collision", "Collision", "Detect touching objects with any surface collision", "PIVOT_INDIVIDUAL", 2), + ("clearance", "Clearance", "Detect objects within a proximity threshold", "PIVOT_ACTIVE", 3), + ], + name="Mode", + ) + tolerance: FloatProperty(name="Tolerance", default=0.002) + clearance: FloatProperty(name="Clearance", default=0.01) + allow_touching: BoolProperty(name="Allow Touching", default=False) + check_all: BoolProperty(name="Check All", default=False) a: CollectionProperty(name="Group A", type=ClashSource) b: CollectionProperty(name="Group B", type=ClashSource) clashes: CollectionProperty(name="Clashes", type=Clash) @@ -78,22 +96,21 @@ class BIMClashProperties(PropertyGroup): smart_clash_grouping_max_distance: IntProperty( name="Smart Clash Grouping Max Distance", default=3, soft_min=1, soft_max=10 ) - sould_focus_on_clash: BoolProperty(name="Show Focus Clash", default=False) + p1: FloatVectorProperty(name="P1", default=(0.0, 0.0, 0.0), subtype="XYZ") + p2: FloatVectorProperty(name="P2", default=(0.0, 0.0, 0.0), subtype="XYZ") + active_clash_text: StringProperty(name="Active Clash Text") @property def active_clash_set(self): - if not self.clash_sets: - return None - return self.clash_sets[self.active_clash_set_index] + if self.active_clash_set_index < len(self.clash_sets): + return self.clash_sets[self.active_clash_set_index] @property def active_smart_group(self): - if not self.smart_clash_groups: - return None - return self.smart_clash_groups[self.active_smart_group_index] + if self.active_smart_group_index < len(self.smart_clash_groups): + return self.smart_clash_groups[self.active_smart_group_index] @property def active_clash(self): - if not self.active_clash_set: - return None - return self.active_clash_set.clashes[self.active_clash_index] + if self.active_clash_index < len(self.active_clash_set.clashes): + return self.active_clash_set.clashes[self.active_clash_index] diff --git a/src/blenderbim/blenderbim/bim/module/clash/ui.py b/src/blenderbim/blenderbim/bim/module/clash/ui.py index 8b7ba0110f..d5322a568b 100644 --- a/src/blenderbim/blenderbim/bim/module/clash/ui.py +++ b/src/blenderbim/blenderbim/bim/module/clash/ui.py @@ -17,7 +17,9 @@ # along with BlenderBIM Add-on. If not, see . import bpy +import blenderbim.bim.helper from bpy.types import Panel +from blenderbim.bim.module.clash.data import ClashData class BIM_PT_ifcclash(Panel): @@ -30,6 +32,9 @@ class BIM_PT_ifcclash(Panel): bl_parent_id = "BIM_PT_tab_clash_detection" def draw(self, context): + if not ClashData.is_loaded: + ClashData.load() + layout = self.layout scene = context.scene @@ -45,57 +50,83 @@ class BIM_PT_ifcclash(Panel): layout.template_list("BIM_UL_clash_sets", "", props, "clash_sets", props, "active_clash_set_index") - if props.active_clash_set_index < len(props.clash_sets): - clash_set = props.active_clash_set + if not props.active_clash_set: + return - row = layout.row(align=True) - row.prop(clash_set, "name") - row.operator("bim.remove_clash_set", icon="X", text="").index = props.active_clash_set_index + clash_set = props.active_clash_set - row = layout.row(align=True) + row = layout.row(align=True) + row.prop(clash_set, "name") + row.operator("bim.remove_clash_set", icon="X", text="").index = props.active_clash_set_index + + row = layout.row() + row.prop(clash_set, "mode") + + if clash_set.mode == "intersection": + row = layout.row() row.prop(clash_set, "tolerance") - - layout.label(text="Group A:") row = layout.row() - row.operator("bim.add_clash_source").group = "a" - - for index, source in enumerate(clash_set.a): - row = layout.row(align=True) - row.prop(source, "name", text="") - op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="") - op.index = index - op.group = "a" - op = row.operator("bim.remove_clash_source", icon="X", text="") - op.index = index - op.group = "a" - - row = layout.row(align=True) - row.prop(source, "mode", text="") - row.prop(source, "selector", text="") - - layout.label(text="Group B:") + row.prop(clash_set, "check_all") + elif clash_set.mode == "collision": row = layout.row() - row.operator("bim.add_clash_source").group = "b" - - for index, source in enumerate(clash_set.b): - row = layout.row(align=True) - row.prop(source, "name", text="") - op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="") - op.index = index - op.group = "b" - op = row.operator("bim.remove_clash_source", icon="X", text="") - op.index = index - op.group = "b" - - row = layout.row(align=True) - row.prop(source, "mode", text="") - row.prop(source, "selector", text="") - + row.prop(clash_set, "allow_touching") + elif clash_set.mode == "clearance": row = layout.row() - row.prop(props, "should_create_clash_snapshots") + row.prop(clash_set, "clearance") + row = layout.row() + row.prop(clash_set, "check_all") + + row = layout.row(align=True) + row.label(text="Group A:", icon="OUTLINER_OB_POINTCLOUD") + row.operator("bim.add_clash_source", icon="ADD", text="").group = "a" + + for index, source in enumerate(clash_set.a): row = layout.row(align=True) - row.operator("bim.execute_ifc_clash") - row.operator("bim.select_ifc_clash_results") + row.prop(source, "name", text="") + row.prop(source, "mode", text="") + op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="") + op.index = index + op.group = "a" + op = row.operator("bim.remove_clash_source", icon="X", text="") + op.index = index + op.group = "a" + + if source.mode != "a": + blenderbim.bim.helper.draw_filter( + layout, source.filter_groups, ClashData, f"clash_{props.active_clash_set_index}_a_{index}" + ) + + row = layout.row(align=True) + row.label(text="Group B:", icon="OUTLINER_OB_POINTCLOUD") + row.operator("bim.add_clash_source", icon="ADD", text="").group = "b" + + for index, source in enumerate(clash_set.b): + row = layout.row(align=True) + row.prop(source, "name", text="") + row.prop(source, "mode", text="") + op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="") + op.index = index + op.group = "b" + op = row.operator("bim.remove_clash_source", icon="X", text="") + op.index = index + op.group = "b" + + if source.mode != "a": + blenderbim.bim.helper.draw_filter( + layout, source.filter_groups, ClashData, f"clash_{props.active_clash_set_index}_b_{index}" + ) + + row = layout.row() + row.prop(props, "should_create_clash_snapshots") + row = layout.row() + row.operator("bim.execute_ifc_clash") + + row = layout.row() + row.label(text=f"{len(clash_set.clashes)} Clashes Found", icon="PIVOT_CURSOR") + + layout.template_list("BIM_UL_clashes", "", props.active_clash_set, "clashes", props, "active_clash_index") + row = layout.row() + row.operator("bim.select_clash") class BIM_PT_clash_manager(Panel): @@ -111,29 +142,6 @@ class BIM_PT_clash_manager(Panel): pass -class BIM_PT_dumb_clash_manager(Panel): - bl_idname = "BIM_PT_dumb_clash_manager" - bl_label = "Dumb Manager" - bl_options = {"DEFAULT_CLOSED"} - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "scene" - bl_parent_id = "BIM_PT_clash_manager" - - def draw(self, context): - layout = self.layout - props = context.scene.BIMClashProperties - - row = layout.row(align=True) - row.operator("bim.load_ifc_clashes", text="LOAD IFC CLASHES", icon="IMPORT") - row.operator("bim.save_ifc_clashes", text="SAVE CLASH RESULTS", icon="EXPORT") - layout.template_list("BIM_UL_clashes", "", props.active_clash_set, "clashes", props, "active_clash_index") - # bim.select_clash - row = layout.row(align=True) - row.operator("bim.select_clash", text="SELECT CLASH", icon="IMPORT") - row.prop(props, "sould_focus_on_clash", icon="RESTRICT_VIEW_OFF") - - class BIM_PT_smart_clash_manager(Panel): bl_idname = "BIM_PT_smart_clash_manager" bl_label = "Smart Clash Manager" diff --git a/src/blenderbim/blenderbim/bim/module/classification/operator.py b/src/blenderbim/blenderbim/bim/module/classification/operator.py index 607ba967f5..623aa59770 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/operator.py +++ b/src/blenderbim/blenderbim/bim/module/classification/operator.py @@ -20,6 +20,8 @@ import bpy import json import ifcopenshell import ifcopenshell.api +import ifcopenshell.util.classification +import ifcopenshell.util.element import blenderbim.tool as tool import blenderbim.bim.helper from blenderbim.bim.ifc import IfcStore @@ -87,7 +89,7 @@ class AddManualClassificationReference(bpy.types.Operator, tool.Ifc.Operator): reference = ifcopenshell.api.run( "classification.add_reference", tool.Ifc.get(), - product=product, + products=[product], classification=classification, identification="X", name="Unnamed", @@ -292,20 +294,25 @@ class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator): active_reference = tool.Ifc.get().by_id(self.reference) identification = active_reference[1] + elements_by_references: dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]] = {} for obj in objects: - ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type) + ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context) element = tool.Ifc.get().by_id(ifc_definition_id) references = ifcopenshell.util.classification.get_references(element, should_inherit=False) for reference in references: if (identification and reference[1] == identification) or ( not identification and reference == active_reference ): - ifcopenshell.api.run( - "classification.remove_reference", - tool.Ifc.get(), - reference=reference, - product=element, - ) + elements_by_references.setdefault(reference, []).append(element) + + if elements_by_references: + for reference, products in elements_by_references.items(): + ifcopenshell.api.run( + "classification.remove_reference", + tool.Ifc.get(), + reference=reference, + products=products, + ) class EditClassificationReference(bpy.types.Operator, tool.Ifc.Operator): @@ -357,15 +364,18 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator): classification = element break - for obj in objects: - ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type) - if not ifc_definition_id: - continue + ifc_file = tool.Ifc.get() + products = [ + ifc_file.by_id(ifc_definition_id) + for obj in objects + if (ifc_definition_id := tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)) + ] + if products: ifcopenshell.api.run( "classification.add_reference", tool.Ifc.get(), reference=IfcStore.classification_file.by_id(self.reference), - product=tool.Ifc.get().by_id(ifc_definition_id), + products=products, classification=classification, ) @@ -406,14 +416,14 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator): classification.Location = bsdd_classification.domain_namespace_uri for obj in objects: - ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type) + ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context) if not ifc_definition_id: continue element = tool.Ifc.get().by_id(ifc_definition_id) reference = ifcopenshell.api.run( "classification.add_reference", tool.Ifc.get(), - product=element, + products=[element], classification=classification, identification=bsdd_classification.reference_code, name=bsdd_classification.name, diff --git a/src/blenderbim/blenderbim/bim/module/constraint/operator.py b/src/blenderbim/blenderbim/bim/module/constraint/operator.py index 6ab81a4695..f461554c49 100644 --- a/src/blenderbim/blenderbim/bim/module/constraint/operator.py +++ b/src/blenderbim/blenderbim/bim/module/constraint/operator.py @@ -114,7 +114,7 @@ class EditObjective(bpy.types.Operator): ifcopenshell.api.run( "constraint.edit_objective", self.file, - **{"objective": self.file.by_id(props.active_constraint_id), "attributes": attributes} + **{"objective": self.file.by_id(props.active_constraint_id), "attributes": attributes}, ) bpy.ops.bim.load_objectives() return {"FINISHED"} @@ -179,19 +179,17 @@ class AssignConstraint(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects - for obj in objs: - obj_id = obj.BIMObjectProperties.ifc_definition_id - if not obj_id: - continue + products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)] + if products: ifcopenshell.api.run( "constraint.assign_constraint", self.file, **{ - "product": self.file.by_id(obj_id), + "products": products, "constraint": self.file.by_id(self.constraint), - } + }, ) return {"FINISHED"} @@ -207,18 +205,16 @@ class UnassignConstraint(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects - for obj in objs: - obj_id = obj.BIMObjectProperties.ifc_definition_id - if not obj_id: - continue + products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)] + if products: ifcopenshell.api.run( "constraint.unassign_constraint", self.file, **{ - "product": self.file.by_id(obj_id), + "products": products, "constraint": self.file.by_id(self.constraint), - } + }, ) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/covering/workspace.py b/src/blenderbim/blenderbim/bim/module/covering/workspace.py index abae481529..cf4218a40e 100644 --- a/src/blenderbim/blenderbim/bim/module/covering/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/covering/workspace.py @@ -191,11 +191,11 @@ class CoveringToolUI: row = cls.layout.row(align=True) row.label(text="", icon="FILE_3D") prop_with_search(row, cls.props, "relating_type_id", text="") - row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="") + row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="") else: row.label(text=f"No {AuthoringData.data['ifc_element_type']} Found", icon="ERROR") row = cls.layout.row() - row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="Launch Type Manager") + row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="Launch Type Manager") @classmethod def draw_basic_bim_tool_interface(cls): diff --git a/src/blenderbim/blenderbim/bim/module/debug/__init__.py b/src/blenderbim/blenderbim/bim/module/debug/__init__.py index 93f3713205..271950ea61 100644 --- a/src/blenderbim/blenderbim/bim/module/debug/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/debug/__init__.py @@ -24,6 +24,7 @@ classes = ( operator.CopyDebugInformation, operator.CreateAllShapes, operator.CreateShapeFromStepId, + operator.DebugActiveDrawing, operator.InspectFromObject, operator.InspectFromStepId, operator.OverrideDisplayType, diff --git a/src/blenderbim/blenderbim/bim/module/debug/operator.py b/src/blenderbim/blenderbim/bim/module/debug/operator.py index 1a1f80f123..c97791b6ca 100644 --- a/src/blenderbim/blenderbim/bim/module/debug/operator.py +++ b/src/blenderbim/blenderbim/bim/module/debug/operator.py @@ -20,11 +20,14 @@ import os import sys import bpy import time +import random import logging import platform import subprocess import addon_utils import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.element import ifcopenshell.util.placement import ifcopenshell.util.representation import blenderbim.tool as tool @@ -660,3 +663,104 @@ class PipInstall(bpy.types.Operator): ] ) return {"FINISHED"} + + +class DebugActiveDrawing(bpy.types.Operator): + bl_idname = "bim.debug_active_drawing" + bl_label = "Search Active Drawing For Failing Elements" + bl_description = ( + "Will iterate over all visible drawing's objects, trying to narrow down the list of possible failing objects" + ) + + def execute(self, context: bpy.types.Context): + props = context.scene.DocProperties + drawing_item = props.drawings[props.active_drawing_index] + drawing = tool.Ifc.get().by_id(drawing_item.ifc_definition_id) + + GREEN = "\033[92m" + CYAN = "\033[96m" + END = "\033[0m" + ATTEMPS = 10 + + # run create drawing with sync for once + # to make sure everything is actually in sync + try: + bpy.ops.bim.create_drawing(sync=True) + except: + pass + else: + self.report({"INFO"}, "No errors creating drawing, nothing to investigate.") + return {"FINISHED"} + + all_elements = [e for obj in context.visible_objects if (e := tool.Ifc.get_entity(obj))] + all_elements = set(all_elements) + + original_exclude = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "Exclude") + pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing") + + def drawing_fails_to_load(chunk_to_include: set) -> bool: + current_elements = all_elements - chunk_to_include + excluded_guids = ", ".join([e.GlobalId for e in current_elements if hasattr(e, "GlobalId")]) + tool.Ifc.run("pset.edit_pset", pset=pset, properties={"Exclude": f"{original_exclude}, {excluded_guids}"}) + + try: + bpy.ops.bim.create_drawing(sync=False) + result = False + except Exception as e: + # print(e) + result = True + + tool.Ifc.run("pset.edit_pset", pset=pset, properties={"Exclude": original_exclude}) + return result + + def test_elements(elements: list[ifcopenshell.entity_instance], attempts: int = ATTEMPS) -> None: + print(f"{CYAN}processing {len(elements)} elements{END}") + if not elements: + print(f"Empty list of elements, will stop...") + return + + n_elements = len(elements) + middle = int(n_elements / 2) + chunk1, chunk2 = elements[:middle], elements[middle:] + test_chunk_1 = drawing_fails_to_load(set(chunk1)) + test_chunk_2 = drawing_fails_to_load(set(chunk2)) + + if ( + # both chunks do not fail anymore + (not test_chunk_1 and not test_chunk_2) + # or we have 1 element chunk that is still failing + or (test_chunk_1 and not chunk2) + or (test_chunk_2 and not chunk1) + ): + if attempts == 0 or n_elements in (1, 2): + print(f"{GREEN}Couldn't narrow it down any further.{END}") + print(f"It's some of the {n_elements} elements:") + print(elements) + + print("Let's test excluding them...") + for element in elements: + test = drawing_fails_to_load(all_elements - {element}) + if test: + print(f"{CYAN}Excluding element didn't fixed the drawing: {END}") + print(element) + else: + print(f"{GREEN}Excluding element fixed the drawing: {END}") + print(element) + else: + print(f"{CYAN}Will try to reshuffle elements and try again, attempt {ATTEMPS-attempts+1}/{ATTEMPS}") + attempts -= 1 + random.shuffle(elements) + test_elements(elements, attempts) + return + + # if chunk fails we need to investigate it further + if test_chunk_1: + test_elements(chunk1) + + if test_chunk_2: + test_elements(chunk2) + + test_elements(list(all_elements)) + + self.report({"INFO"}, "See system console for the results") + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/debug/ui.py b/src/blenderbim/blenderbim/bim/module/debug/ui.py index 21a6e11f80..2fd503cf6f 100644 --- a/src/blenderbim/blenderbim/bim/module/debug/ui.py +++ b/src/blenderbim/blenderbim/bim/module/debug/ui.py @@ -75,6 +75,9 @@ class BIM_PT_debug(Panel): row = layout.row() row.operator("bim.profile_import_ifc") + row = layout.row() + row.operator("bim.debug_active_drawing") + row = layout.split(factor=0.5, align=True) row.operator("bim.create_shape_from_step_id").should_include_curves = False row.operator("bim.create_shape_from_step_id", text="", icon="IPO_ELASTIC").should_include_curves = True diff --git a/src/blenderbim/blenderbim/bim/module/diff/data.py b/src/blenderbim/blenderbim/bim/module/diff/data.py index 8dfc47e615..d84b9c19a2 100644 --- a/src/blenderbim/blenderbim/bim/module/diff/data.py +++ b/src/blenderbim/blenderbim/bim/module/diff/data.py @@ -38,6 +38,7 @@ class DiffData: cls.data["total_deleted"] = cls.total_deleted() cls.data["total_changed"] = cls.total_changed() cls.data["changes"] = cls.changes() + cls.data["saved_searches"] = cls.saved_searches() cls.is_loaded = True @classmethod @@ -84,3 +85,18 @@ class DiffData: elif element.GlobalId in diff["deleted"]: changes["Deleted"] = True return changes + + @classmethod + def saved_searches(cls): + if not tool.Ifc.get(): + return [] + groups = tool.Ifc.get().by_type("IfcGroup") + results = [] + for group in groups: + try: + data = json.loads(group.Description) + if isinstance(data, dict) and data.get("type", None) == "BBIM_Search" and data.get("query", None): + results.append(group) + except: + pass + return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")] diff --git a/src/blenderbim/blenderbim/bim/module/diff/operator.py b/src/blenderbim/blenderbim/bim/module/diff/operator.py index a0bb7d0c2c..9f1e85f66c 100644 --- a/src/blenderbim/blenderbim/bim/module/diff/operator.py +++ b/src/blenderbim/blenderbim/bim/module/diff/operator.py @@ -135,6 +135,7 @@ class ExecuteIfcDiff(bpy.types.Operator): bl_label = "Execute IFC Diff" filename_ext = ".json" filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) def invoke(self, context, event): self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") @@ -162,7 +163,7 @@ class ExecuteIfcDiff(bpy.types.Operator): new = ifcopenshell.open(self.props.new_file) relationships = [r.relationship for r in self.props.diff_relationships] - query = self.props.diff_filter_elements + query = tool.Search.export_filter_query(self.props.filter_groups) or None ifc_diff = ifcdiff.IfcDiff(old, new, relationships=relationships, filter_elements=query) ifc_diff.diff() diff --git a/src/blenderbim/blenderbim/bim/module/diff/prop.py b/src/blenderbim/blenderbim/bim/module/diff/prop.py index 8a1522e771..c6b0a06c91 100644 --- a/src/blenderbim/blenderbim/bim/module/diff/prop.py +++ b/src/blenderbim/blenderbim/bim/module/diff/prop.py @@ -17,7 +17,7 @@ # along with BlenderBIM Add-on. If not, see . import bpy -from blenderbim.bim.prop import StrProperty +from blenderbim.bim.prop import StrProperty, BIMFilterGroup from blenderbim.bim.module.diff.data import DiffData from bpy.types import PropertyGroup from bpy.props import ( @@ -48,7 +48,7 @@ class DiffProperties(PropertyGroup): old_file: StringProperty(default="", name="Old IFC File") new_file: StringProperty(default="", name="New IFC File") diff_relationships: CollectionProperty(type=Relationships, name="Relationships") - diff_filter_elements: StringProperty(default="", name="Filter") + filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups") should_load_changed_elements: BoolProperty(name="Load Changed Elements", default=True) active_file: EnumProperty( items=[ diff --git a/src/blenderbim/blenderbim/bim/module/diff/ui.py b/src/blenderbim/blenderbim/bim/module/diff/ui.py index d91f0eb9a0..6593e7cd6f 100644 --- a/src/blenderbim/blenderbim/bim/module/diff/ui.py +++ b/src/blenderbim/blenderbim/bim/module/diff/ui.py @@ -19,8 +19,8 @@ from bpy.types import Panel from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.diff.data import DiffData +import blenderbim.bim.helper import blenderbim.tool as tool -import json class BIM_PT_diff(Panel): @@ -93,9 +93,7 @@ class BIM_PT_diff(Panel): remove.collection = "diff_relationships" remove.index = index - row = layout.row(align=True) - row.prop(props, "diff_filter_elements") - row.operator("bim.ifc_selector", icon="FILTER", text="") + blenderbim.bim.helper.draw_filter(self.layout, props.filter_groups, DiffData, "diff") row = layout.row() row.operator("bim.execute_ifc_diff") diff --git a/src/blenderbim/blenderbim/bim/module/drawing/__init__.py b/src/blenderbim/blenderbim/bim/module/drawing/__init__.py index 61af897a73..18227bed86 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/__init__.py @@ -41,6 +41,7 @@ classes = ( operator.CleanWireframes, operator.ContractSheet, operator.ContractTargetView, + operator.ConvertSVGToDXF, operator.CreateDrawing, operator.CreateSheets, operator.DisableAddAnnotationType, @@ -126,7 +127,7 @@ def register(): bpy.types.Object.BIMAssignedProductProperties = bpy.props.PointerProperty(type=prop.BIMAssignedProductProperties) bpy.types.Object.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties) bpy.types.TextCurve.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties) - bpy.app.handlers.load_post.append(handler.toggleDecorationsOnLoad) + bpy.app.handlers.load_post.append(handler.load_post) bpy.app.handlers.depsgraph_update_pre.append(handler.depsgraph_update_pre_handler) bpy.types.VIEW3D_MT_image_add.append(ui.add_object_button) @@ -140,6 +141,6 @@ def unregister(): del bpy.types.Object.BIMAssignedProductProperties del bpy.types.Object.BIMTextProperties del bpy.types.TextCurve.BIMTextProperties - bpy.app.handlers.load_post.remove(handler.toggleDecorationsOnLoad) + bpy.app.handlers.load_post.remove(handler.load_post) bpy.app.handlers.depsgraph_update_pre.remove(handler.depsgraph_update_pre_handler) bpy.types.VIEW3D_MT_image_add.remove(ui.add_object_button) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/data.py b/src/blenderbim/blenderbim/bim/module/drawing/data.py index d9b493cae0..61652baa8e 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/data.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/data.py @@ -23,6 +23,7 @@ import ifcopenshell.util.element import ifcopenshell.util.representation import blenderbim.tool as tool from pathlib import Path +from typing import Any, Union def refresh(): @@ -234,9 +235,9 @@ class DecoratorData: cut_cache = {} layerset_cache = {} - # used by Ifc Annotations with ObjectType = "BATTING" @classmethod def get_batting_thickness(cls, obj): + """used by IfcAnnotations with ObjectType = "BATTING" """ result = cls.data.get(obj.name, None) if result is not None: return result @@ -248,9 +249,9 @@ class DecoratorData: cls.data[obj.name] = thickness return thickness - # used by Ifc Annotations with ObjectType = "SECTION" @classmethod def get_section_markers_display_data(cls, obj): + """used by IfcAnnotations with ObjectType = "SECTION" """ result = cls.data.get(obj.name, None) if result is not None: return result @@ -290,10 +291,10 @@ class DecoratorData: cls.data[obj.name] = display_data return display_data - # used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER" @classmethod - def get_ifc_text_data(cls, obj): - """returns font size in mm for current ifc text object""" + def get_ifc_text_data(cls, obj: bpy.types.Object) -> dict[str, Any]: + """used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER"\n + returns font size in mm for current ifc text object""" result = cls.data.get(obj.name, None) if result is not None: return result @@ -317,7 +318,7 @@ class DecoratorData: font_size = FONT_SIZES[font_size_type] # get symbol - symbol = pset_data.get("Symbol", None) + symbol = tool.Drawing.get_annotation_symbol(element) # other attributes props_literals = props.literals @@ -340,6 +341,11 @@ class DecoratorData: cls.data[obj.name] = text_data return text_data + @classmethod + def get_symbol(cls, obj: bpy.types.Object) -> Union[str, None]: + """used by IfcAnnotations with ObjectType MULTI_SYMBOL""" + return tool.Drawing.get_annotation_symbol(tool.Ifc.get_entity(obj)) + @classmethod def get_dimension_data(cls, obj): """used by Ifc Annotations with ObjectType: @@ -412,10 +418,12 @@ class AnnotationData: relating_types = [] for relating_type in tool.Ifc.get().by_type("IfcTypeProduct"): if tool.Drawing.is_annotation_object_type(relating_type, object_type): - relating_types.append({ - "id": relating_type.id(), - "name": relating_type.Name or "Unnamed", - "description": relating_type.Description or "No Description", - }) + relating_types.append( + { + "id": relating_type.id(), + "name": relating_type.Name or "Unnamed", + "description": relating_type.Description or "No Description", + } + ) return sorted(relating_types, key=lambda x: x["name"]) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py index bee1b3c318..663b060db9 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py @@ -37,6 +37,7 @@ from blenderbim.bim.module.drawing.shaders import add_verts_sequence, add_offset from blenderbim.bim.module.drawing.helper import format_distance from timeit import default_timer as timer from functools import lru_cache +from typing import Optional, Iterator, Type, Union UNSPECIAL_ELEMENT_COLOR = (0.2, 0.2, 0.2, 1) # GREY @@ -69,7 +70,7 @@ class profile_consequential: cls.lines = [] -def worldspace_to_winspace(verts, context): +def worldspace_to_winspace(verts: list[Vector], context: bpy.types.Context) -> list[Vector]: """Convert world space verts to window space""" region = context.region region3d = context.region_data @@ -80,7 +81,7 @@ def worldspace_to_winspace(verts, context): return winspace_verts -def winspace_to_worldspace(verts, context): +def winspace_to_worldspace(verts: list[Vector], context: bpy.types.Context) -> list[Vector]: """Convert winspace verts to world space""" region = context.region region3d = context.region_data @@ -121,17 +122,18 @@ def get_callout_head(edge_dir, edge_side, callout_size, callout_gap): return head -def get_circle_head(size, segments=20): +def get_circle_head(size: float, segments: int = 20, rotation: float = 0.0) -> list[Vector]: + """`rotation` value in radians""" angle_d = 2 * pi / segments head = [] for i in range(segments): - angle = angle_d * i + angle = angle_d * i + rotation head.append(Vector([cos(angle), sin(angle), 0]) * size) return head -def get_circle_head_asterisk(size, segments=6): - circle_head = get_circle_head(size, segments) +def get_circle_head_asterisk(size: float, segments: int = 6, rotation: float = 0.0) -> Iterator[tuple[Vector, Vector]]: + circle_head = get_circle_head(size, segments, rotation) middle = segments // 2 return zip(circle_head[:middle], circle_head[middle:]) @@ -264,7 +266,7 @@ class BaseDecorator: vertices = [obj.matrix_world @ v.co for v in bm.verts] return vertices, indices - def decorate(self, context, obj): + def decorate(self, context: bpy.types.Context, obj: bpy.types.Object): """perform actual drawing stuff""" raise NotImplementedError() @@ -310,13 +312,28 @@ class BaseDecorator: self.draw_lines(context, obj, output_verts, output_edges) - def draw_batch(self, shader_type, content_pos, color, indices=None): + def draw_batch( + self, + shader_type: str, + content_pos: list[Vector], + color: tuple[float, float, float, float], + indices: Optional[list[tuple[int, int]]] = None, + ) -> None: shader = self.line_shader if shader_type == "LINES" else self.base_shader batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices) shader.uniform_float("color", color) batch.draw(shader) - def draw_lines(self, context, obj, vertices, indices, color=None, line_width=1.0): + def draw_lines( + self, + context: bpy.types.Context, + obj: Optional[bpy.types.Object], + vertices: list[Vector], + indices: list[tuple[int, int]], + color: Optional[tuple[float, float, float, float]] = None, + line_width: float = 1.0, + ) -> None: + # TODO: `obj` is actually never used, need to remove it """`verts` should be in winspace with `(0,0,0)` in the screen left bottom corner, not in the center""" region = context.region if not color: @@ -329,7 +346,7 @@ class BaseDecorator: gpu.state.blend_set("ALPHA") self.draw_batch("LINES", vertices, color, indices) - def get_viewport_drawing_scale(self, context): + def get_viewport_drawing_scale(self, context: bpy.types.Context) -> float: # Horrific prototype code factor = self.camera_zoom_to_factor(context.space_data.region_3d.view_camera_zoom) camera_width_px = factor * context.region.width @@ -485,14 +502,16 @@ class BaseDecorator: decimal_places = drawing_pset_data.get("DecimalPlaces", None) return format_distance(value, precision=precision, decimal_places=decimal_places) - def draw_asterisk(self, context, obj): + def draw_asterisk(self, context: bpy.types.Context, pos: Vector, rotation: float = 0.0, scale: float = 1.0) -> None: + """`pos` is a world space position\n + `rotation` value in radians""" # gather geometry data and convert to winspace - verts = [obj.location] + verts = [pos] viewportDrawingScale = self.get_viewport_drawing_scale(context) winspace_verts = worldspace_to_winspace(verts, context) # setup geometry parameters - circle_size = viewportDrawingScale * 4 + circle_size = viewportDrawingScale * 4 * scale output_verts = [] output_edges = [] @@ -502,20 +521,13 @@ class BaseDecorator: } v0 = winspace_verts[0] - asterisk_head = get_circle_head_asterisk(circle_size) + asterisk_head = get_circle_head_asterisk(circle_size, rotation=rotation) start_i = 0 for segment in asterisk_head: start_i = add_verts_sequence(add_offsets(v0, segment), start_i, **out_kwargs) - self.draw_lines(context, obj, output_verts, output_edges) + self.draw_lines(context, None, output_verts, output_edges) - def draw_text(self, context, obj, text_world_position=None, reverse_lines_order=False): - """if `text_world_position` is not provided, the object's location will be used""" - - if not text_world_position: - text_world_position = obj.location - - region = context.region - region3d = context.region_data + def get_annotation_direction(self, context: bpy.types.Context, obj: bpy.types.Object) -> Vector: camera = context.scene.camera def get_basis_vector(matrix, i=0): @@ -523,10 +535,57 @@ class BaseDecorator: return matrix.inverted()[i].to_3d().normalized() text_dir_world_x_axis = get_basis_vector(obj.matrix_world) - camera_matrix = tool.Drawing.get_camera_matrix(camera) - text_dir = (camera_matrix.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized() + return text_dir + + def draw_symbol(self, context: bpy.types.Context, obj: bpy.types.Object, annotation_dir: Vector) -> None: + if obj.type not in ("MESH", "EMPTY"): + return + + if obj.type == "MESH": + mesh: bpy.types.Mesh = obj.data + + if len(mesh.vertices) == 0: + return + + if mesh.edges: + MiscDecorator.decorate(self, context, obj) + return + + symbol = DecoratorData.get_symbol(obj) + if not symbol: + return + + for vert in mesh.vertices: + self.draw_asterisk(context, obj.matrix_world @ vert.co) + + return + + # EMPTY objects + symbol = DecoratorData.get_symbol(obj) + if not symbol: + return + + rotation = -Vector((1, 0)).angle_signed(annotation_dir) + # NOTE: for now we assume that scale is uniform + self.draw_asterisk(context, obj.location, rotation, obj.scale.x) + + def draw_text( + self, + context: bpy.types.Context, + obj: bpy.types.Object, + text_world_position: Optional[Vector] = None, + reverse_lines_order=False, + ) -> None: + """if `text_world_position` is not provided, the object's location will be used""" + + if not text_world_position: + text_world_position = obj.location + + region = context.region + region3d = context.region_data + text_dir = self.get_annotation_direction(context, obj) pos = location_3d_to_region_2d(region, region3d, text_world_position) props = obj.BIMTextProperties @@ -539,8 +598,7 @@ class BaseDecorator: # draw asterisk symbol to indicate that there is some symbol that's not shown in viewport if symbol: - self.draw_asterisk(context, obj) - # NOTE: for now we assume that scale is uniform + self.draw_symbol(context, obj, text_dir) text_scale = obj.scale.x line_i = 0 @@ -548,7 +606,7 @@ class BaseDecorator: for literal_data in literals_data: box_alignment = literal_data["BoxAlignment"] - for line in literal_data["CurrentValue"].split("\\n"): + for line in literal_data["CurrentValue"].split("\n"): self.draw_label( context, line, @@ -812,8 +870,9 @@ class AngleDecorator(BaseDecorator): base_edge = edge0 if tool.Cad.is_counter_clockwise_order(p0, p1, p2) else edge1 text_offset = (Matrix.Rotation(-angle_rad / 2, 2) @ base_edge).normalized() * (radius + ANGLE_LABEL_OFFSET) label_position = p1 + text_offset - - text = f"{int(angle)}deg" + drawing_pset_data = DrawingsData.data["active_drawing_pset_data"] + decimal_places = drawing_pset_data.get("AngleDecimalPlaces", None) + text = f"{round(angle, decimal_places)}°" label_dir = Vector((1, 0)) self.draw_label(context, text, label_position, label_dir, box_alignment="center") @@ -1003,7 +1062,7 @@ class StairDecorator(BaseDecorator): class MiscDecorator(BaseDecorator): objecttype = "MISC" - def decorate(self, context, obj): + def decorate(self, context: bpy.types.Context, obj: bpy.types.Object): if obj.data.is_editmode: verts, idxs = self.get_editmesh_geom(obj) else: @@ -1505,6 +1564,14 @@ class TextDecorator(BaseDecorator): self.draw_text(context, obj) +class SymbolDecorator(BaseDecorator): + objecttype = "SYMBOL" + + def decorate(self, context: bpy.types.Context, obj: bpy.types.Object) -> None: + annotation_dir = self.get_annotation_direction(context, obj) + self.draw_symbol(context, obj, annotation_dir) + + class CutDecorator: installed = None cache = {} @@ -1901,7 +1968,7 @@ class CutDecorator: class DecorationsHandler: - decorators_classes = [ + decorators_classes: list[Type[BaseDecorator]] = [ DimensionDecorator, AngleDecorator, GridDecorator, @@ -1918,6 +1985,7 @@ class DecorationsHandler: SectionDecorator, ElevationDecorator, TextDecorator, + SymbolDecorator, BattingDecorator, FallDecorator, ] @@ -1945,6 +2013,7 @@ class DecorationsHandler: self.decorators = {cls.objecttype: cls() for cls in self.decorators_classes} for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"): self.decorators[object_type] = self.decorators["FALL"] + self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"] def get_objects_and_decorators(self, collection): # TODO: do it in data instead of the handler for performance? @@ -1962,7 +2031,7 @@ class DecorationsHandler: if not element.is_a("IfcAnnotation"): continue - object_type = element.ObjectType + object_type: Union[str, None] = element.ObjectType if object_type == "DRAWING": continue diff --git a/src/blenderbim/blenderbim/bim/module/drawing/handler.py b/src/blenderbim/blenderbim/bim/module/drawing/handler.py index d553fcb113..9e45415d8f 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/handler.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/handler.py @@ -23,7 +23,7 @@ from bpy.app.handlers import persistent @persistent -def toggleDecorationsOnLoad(*args): +def load_post(*args): if bpy.context.scene.DocProperties.should_draw_decorations: decoration.DecorationsHandler.install(bpy.context) else: diff --git a/src/blenderbim/blenderbim/bim/module/drawing/helper.py b/src/blenderbim/blenderbim/bim/module/drawing/helper.py index 043c5d7bbd..70a066c2f7 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/helper.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/helper.py @@ -188,7 +188,6 @@ def format_distance( feet += 1 inches = 0 - if not isArea: add_inches = bool(inches) or not suppress_zero_inches tx_dist = "" @@ -299,7 +298,9 @@ def parse_diagram_scale(camera): return float(numerator) / float(denominator) -def ortho_view_frame(camera, margin=0.015): +def ortho_view_frame( + camera: bpy.types.Camera, margin: float = 0.015 +) -> tuple[float, float, float, float, float, float]: """Calculates 2d bounding box of camera view area. Similar to `bpy.types.Camera.view_frame` diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index 9702b9188a..1d5e751062 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -44,7 +44,7 @@ import blenderbim.bim.module.drawing.helper as helper import blenderbim.bim.export_ifc from blenderbim.bim.module.drawing.decoration import CutDecorator from blenderbim.bim.module.drawing.data import DecoratorData, DrawingsData -from typing import NamedTuple, List +from typing import NamedTuple, List, Union, Optional from lxml import etree from mathutils import Vector, Color, Matrix from timeit import default_timer as timer @@ -199,6 +199,11 @@ class CreateDrawing(bpy.types.Operator): + "SHIFT+CLICK to print all selected drawings" ) print_all: bpy.props.BoolProperty(name="Print All", default=False, options={"SKIP_SAVE"}) + sync: bpy.props.BoolProperty( + name="Sync Before Creating Drawing", + description="Could save some time if you're sure IFC and current Blender session are already in sync", + default=True, + ) @classmethod def poll(cls, context): @@ -220,7 +225,8 @@ class CreateDrawing(bpy.types.Operator): else: drawings_to_print = [self.props.active_drawing_id] - for drawing_id in drawings_to_print: + for drawing_i, drawing_id in enumerate(drawings_to_print): + self.drawing_index = drawing_i if self.print_all: bpy.ops.bim.activate_drawing(drawing=drawing_id, camera_view_point=False) @@ -335,6 +341,13 @@ class CreateDrawing(bpy.types.Operator): if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasUnderlay"): return svg_path = self.get_svg_path(cache_type="underlay") + if os.path.isfile(svg_path) and self.props.should_use_underlay_cache: + return svg_path + + visible_object_names = {obj.name for obj in bpy.context.visible_objects} + for obj in bpy.context.view_layer.objects: + obj.hide_render = obj.name not in visible_object_names + context.scene.render.filepath = svg_path[0:-4] + ".png" drawing_style = context.scene.DocProperties.drawing_styles[self.cprops.active_drawing_style_index] @@ -445,8 +458,14 @@ class CreateDrawing(bpy.types.Operator): return LineworkContexts(body_contexts, annotation_contexts) def serialize_contexts_elements( - self, ifc, tree: ifcopenshell.geom.tree, contexts: LineworkContexts, context_type, drawing_elements, target_view - ): + self, + ifc: ifcopenshell.file, + tree: ifcopenshell.geom.tree, + contexts: LineworkContexts, + context_type: str, + drawing_elements: set[ifcopenshell.entity_instance], + target_view: str, + ) -> None: drawing_elements = drawing_elements.copy() contexts = getattr(contexts, context_type) for context in contexts: @@ -472,20 +491,24 @@ class CreateDrawing(bpy.types.Operator): tree.add_element(elem) drawing_elements -= processed - def generate_linework(self, context): + def generate_linework(self, context: bpy.types.Context) -> Union[str, None]: if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"): return svg_path = self.get_svg_path(cache_type="linework") if os.path.isfile(svg_path) and self.props.should_use_linework_cache: return svg_path - with profile("sync"): - # All very hackish whilst prototyping - exporter = blenderbim.bim.export_ifc.IfcExporter(None) - exporter.file = tool.Ifc.get() - invalidated_elements = exporter.sync_all_objects() - invalidated_elements += exporter.sync_edited_objects() - invalidated_guids = [e.GlobalId for e in invalidated_elements if hasattr(e, "GlobalId")] + # in case of printing multiple drawings we need to sync just once + if self.sync and self.drawing_index == 0: + with profile("sync"): + # All very hackish whilst prototyping + exporter = blenderbim.bim.export_ifc.IfcExporter(None) + exporter.file = tool.Ifc.get() + invalidated_elements = exporter.sync_all_objects(skip_unlinking=True) + invalidated_elements += exporter.sync_edited_objects() + invalidated_guids = [e.GlobalId for e in invalidated_elements if hasattr(e, "GlobalId")] + cache = IfcStore.get_cache() + [cache.remove(guid) for guid in invalidated_guids] # If we have already calculated it in the SVG in the past, don't recalculate edited_guids = set() @@ -506,6 +529,7 @@ class CreateDrawing(bpy.types.Operator): for ifc_path, ifc in files.items(): # Don't use draw.main() just whilst we're prototyping and experimenting + # TODO: hash paths are never used ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest() ifc_cache_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5") @@ -518,8 +542,6 @@ class CreateDrawing(bpy.types.Operator): drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element) self.setup_serialiser(ifc, target_view) - cache = IfcStore.get_cache() - [cache.remove(guid) for guid in invalidated_guids] tree = ifcopenshell.geom.tree() tree.enable_face_styles(True) @@ -1115,14 +1137,14 @@ class CreateDrawing(bpy.types.Operator): continue return space - def get_material_name(self, element): + def get_material_name(self, element: ifcopenshell.entity_instance) -> str: if hasattr(element, "Name") and element.Name: return element.Name elif hasattr(element, "LayerSetName") and element.LayerSetName: return element.LayerSetName return "mat-" + str(element.id()) - def get_svg_path(self, cache_type=None): + def get_svg_path(self, cache_type: Optional[str] = None) -> str: drawing_path = tool.Drawing.get_document_uri(self.camera_document) drawings_dir = os.path.dirname(drawing_path) @@ -1345,7 +1367,7 @@ class SelectAllDrawings(bpy.types.Operator): bl_label = "Select All Drawings" view: bpy.props.StringProperty() bl_description = "Select all drawings in the drawing list.\n\n" + "SHIFT+CLICK to deselect all drawings" - select_all: bpy.props.BoolProperty(name="Open All", default=False, options={"SKIP_SAVE"}) + select_all: bpy.props.BoolProperty(name="Open All", default=True, options={"SKIP_SAVE"}) def invoke(self, context, event): # deselect all drawings on shift+click @@ -1451,10 +1473,6 @@ class ActivateModel(bpy.types.Operator): class ActivateDrawing(bpy.types.Operator): - """ - Activates the selected drawing view - """ - bl_idname = "bim.activate_drawing" bl_label = "Activate Drawing" bl_options = {"REGISTER", "UNDO"} @@ -1600,8 +1618,14 @@ class ReloadDrawingStyles(bpy.types.Operator): json_path = Path(tool.Ifc.resolve_uri(rel_path)) if not json_path.exists(): - self.report({"ERROR"}, "Shading styles file not found: {}".format(json_path)) - return {"CANCELLED"} + ootb_resource = Path(context.scene.BIMProperties.data_dir) / "assets" / "shading_styles.json" + print( + f"WARNING. Couldn't find shading_styles for the drawing by the path: {json_path}. " + f"Default BBIM resource will be copied from {ootb_resource}" + ) + if ootb_resource.exists(): + os.makedirs(json_path.parent, exist_ok=True) + shutil.copy(ootb_resource, json_path) with open(json_path, "r") as fi: shading_styles_json = json.load(fi) @@ -2546,8 +2570,16 @@ class EnableEditingElementFilter(bpy.types.Operator, Operator): def _execute(self, context): obj = bpy.context.scene.camera - if obj: - obj.data.BIMCameraProperties.filter_mode = self.filter_mode + if not obj: + return + obj.data.BIMCameraProperties.filter_mode = self.filter_mode + element = tool.Ifc.get_entity(obj) + if query := ifcopenshell.util.element.get_pset(element, "EPset_Drawing", self.filter_mode.title()): + filter_groups = tool.Search.get_filter_groups(f"drawing_{self.filter_mode.lower()}") + try: + tool.Search.import_filter_query(query, filter_groups) + except: + pass class EditElementFilter(bpy.types.Operator, Operator): @@ -2689,3 +2721,50 @@ class AddReferenceImage(bpy.types.Operator, Operator): ) tool.Style.reload_material_from_ifc(material) tool.Geometry.record_object_materials(obj) + + +class ConvertSVGToDXF(bpy.types.Operator): + bl_idname = "bim.convert_svg_to_dxf" + bl_label = "Convert SVG to DXF" + bl_options = {"REGISTER", "UNDO"} + view: bpy.props.StringProperty() + bl_description = "Convert current drawing's .svg to .dxf.\n\nSHIFT+CLICK to convert all selected drawings" + convert_all: bpy.props.BoolProperty(name="Convert All", default=False, options={"SKIP_SAVE"}) + + def invoke(self, context, event): + # convert all drawings on shift+click + # make sure to use SKIP_SAVE on property, otherwise it might get stuck + if event.type == "LEFTMOUSE" and event.shift: + self.open_all = True + return self.execute(context) + + def execute(self, context): + if self.convert_all: + drawings = [ + tool.Ifc.get().by_id(d.ifc_definition_id) for d in context.scene.DocProperties.drawings if d.is_selected + ] + else: + drawings = [tool.Ifc.get().by_id(context.scene.DocProperties.drawings.get(self.view).ifc_definition_id)] + + drawing_uris: list[Path] = [] + drawings_not_found: list[str] = [] + + for drawing in drawings: + drawing_uri = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_document(drawing)) + if drawing_uri is None or not os.path.exists(drawing_uri): + drawings_not_found.append(drawing.Name) + else: + drawing_uris.append(Path(drawing_uri)) + + if drawings_not_found: + msg = "Some drawings .svg files were not found, need to print them first: \n{}.".format( + "\n".join(drawings_not_found) + ) + self.report({"ERROR"}, msg) + return {"CANCELLED"} + + for drawing_uri in drawing_uris: + tool.Drawing.convert_svg_to_dxf(drawing_uri, drawing_uri.with_suffix(".dxf")) + + self.report({"INFO"}, f"{len(drawing_uris)} drawings were converted to .dxf.") + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/drawing/prop.py b/src/blenderbim/blenderbim/bim/module/drawing/prop.py index 3e4d3e1d03..7773d41930 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/prop.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/prop.py @@ -66,6 +66,11 @@ def get_location_hint(self, context): def update_diagram_scale(self, context): + if not context.scene.camera or context.scene.camera.data != self.id_data: + return + element = tool.Ifc.get_entity(context.scene.camera) + if not element: + return try: element = ( tool.Ifc.get() @@ -87,6 +92,11 @@ def update_diagram_scale(self, context): def update_is_nts(self, context): + if not context.scene.camera or context.scene.camera.data != self.id_data: + return + element = tool.Ifc.get_entity(context.scene.camera) + if not element: + return try: element = ( tool.Ifc.get() @@ -194,7 +204,7 @@ def update_document_name(self, context): def update_has_underlay(self, context): update_layer(self, context, "HasUnderlay", self.has_underlay) # making sure that camera is active - if self.has_underlay and (context.active_object and context.active_object.data == self.id_data): + if self.has_underlay and (context.scene.camera and context.scene.camera.data == self.id_data): bpy.ops.bim.reload_drawing_styles() bpy.ops.bim.activate_drawing_style() @@ -208,10 +218,12 @@ def update_has_annotation(self, context): def update_layer(self, context, name, value): - element = tool.Ifc.get_entity(context.active_object) + if not context.scene.camera or context.scene.camera.data != self.id_data: + return + element = tool.Ifc.get_entity(context.scene.camera) if not element: return - pset = ifcopenshell.util.element.get_psets(element).get("EPset_Drawing") + pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing") if pset: pset = tool.Ifc.get().by_id(pset["id"]) else: @@ -229,9 +241,8 @@ def update_titleblocks(self, context): SheetsData.data["titleblocks"] = SheetsData.titleblocks() -def toggleDecorations(self, context): - toggle = self.should_draw_decorations - if toggle: +def update_should_draw_decorations(self, context): + if self.should_draw_decorations: # TODO: design a proper text variable templating renderer collection = context.scene.camera.BIMObjectProperties.collection for obj in collection.objects: @@ -343,7 +354,7 @@ class DocProperties(PropertyGroup): active_sheet_index: IntProperty(name="Active Sheet Index") ifc_files: CollectionProperty(name="IFCs", type=StrProperty) drawing_styles: CollectionProperty(name="Drawing Styles", type=DrawingStyle) - should_draw_decorations: BoolProperty(name="Should Draw Decorations", update=toggleDecorations) + should_draw_decorations: BoolProperty(name="Should Draw Decorations", update=update_should_draw_decorations) sheets_dir: StringProperty(default=os.path.join("sheets") + os.path.sep, name="Default Sheets Directory") layouts_dir: StringProperty(default=os.path.join("layouts") + os.path.sep, name="Default Layouts Directory") titleblocks_dir: StringProperty( diff --git a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py index b56d5a795e..736d8efbe5 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py @@ -29,9 +29,11 @@ import svgwrite import ifcopenshell import ifcopenshell.util.element import ifcopenshell.util.representation +import ifcopenshell.util.selector import blenderbim.tool as tool import blenderbim.bim.module.drawing.helper as helper import blenderbim.bim.module.drawing.annotation as annotation +from blenderbim.bim.module.drawing.data import DrawingsData from blenderbim.bim.module.drawing.data import DecoratorData from blenderbim.bim.ifc import IfcStore @@ -39,6 +41,7 @@ from blenderbim.bim.ifc import IfcStore from math import pi, ceil, atan, degrees, acos from mathutils import geometry, Vector from bpy_extras import view3d_utils +from typing import Optional class External(svgwrite.container.Group): @@ -322,7 +325,7 @@ class SvgWriter: ) ) - def draw_misc_annotation(self, obj): + def draw_misc_annotation(self, obj: bpy.types.Object) -> None: # We have to decide whether this should come from Blender or from IFC. # For the moment, for convenience of experimenting with ideas, it comes # from Blender. In the future, it should probably come from IFC. @@ -356,14 +359,14 @@ class SvgWriter: d = "M{}".format(d[1:]) path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes))) - def get_attribute_classes(self, obj): + def get_attribute_classes(self, obj: bpy.types.Object) -> list[str]: element = tool.Ifc.get_entity(obj) global_id = "GlobalId-{}".format(element.GlobalId) predefined_type = "PredefinedType-" + tool.Drawing.canonicalise_class_name( str(ifcopenshell.util.element.get_predefined_type(element)) ) classes = [global_id, element.is_a(), predefined_type] - custom_classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes") + custom_classes: str = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes") if custom_classes: classes.extend(custom_classes.split()) for key in self.metadata: @@ -787,7 +790,7 @@ class SvgWriter: attrib["filter"] = "url(#fill-background)" return element - def draw_text_annotation(self, text_obj, position): + def draw_text_annotation(self, text_obj: bpy.types.Object, position: Vector) -> None: x_offset = self.raw_width / 2 y_offset = self.raw_height / 2 element = tool.Ifc.get_entity(text_obj) @@ -799,17 +802,7 @@ class SvgWriter: text_position_svg = text_position * self.svg_scale text_position_svg_str = ", ".join(map(str, text_position_svg)) - def get_basis_vector(matrix, i=0): - """returns basis vector for i in world space, unaffected by object scale""" - return matrix.inverted()[i].to_3d().normalized() - - text_dir_world_x_axis = get_basis_vector(text_obj.matrix_world) - - # RCP cameras may be scaled, so reset scales. - camera_matrix = tool.Drawing.get_camera_matrix(self.camera) - text_dir = (camera_matrix.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized() - angle = math.degrees(-text_dir.angle_signed(Vector((1, 0)))) - + angle = self.get_empty_object_angle(text_obj) classes = self.get_attribute_classes(text_obj) classes_str = " ".join(classes) fill_bg = "fill-bg" in classes @@ -817,8 +810,7 @@ class SvgWriter: symbol = tool.Drawing.get_annotation_symbol(element) template_text_fields = [] if symbol: - # NOTE: for now we assume that scale is uniform - symbol_transform = f"translate({text_position_svg_str}) rotate({angle}) scale({text_obj.scale.x})" + symbol_transform = self.get_symbol_transform(text_position_svg_str, angle, text_obj) symbol_svg = self.find_xml_symbol_by_id(symbol) if symbol_svg: @@ -847,7 +839,7 @@ class SvgWriter: return None if not symbol_svg or not template_text_fields: - self.svg.add(self.svg.use(f"#{symbol}", transform=symbol_transform)) + self.draw_symbol(symbol, symbol_transform) line_number = 0 for text_literal in text_literals: @@ -865,17 +857,14 @@ class SvgWriter: self.svg.add(tag) line_number += len(tag.elements) - def draw_empty_annotation(self, obj, classes): + def draw_empty_annotation(self, obj: bpy.types.Object, classes: list[str]) -> None: x_offset = self.raw_width / 2 y_offset = self.raw_height / 2 point = self.project_point_onto_camera(obj.matrix_world.translation) element = tool.Ifc.get_entity(obj) - svg_id = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Symbol") - if not svg_id: - # EPset_AnnotationSurveyArea is not standard! See bSI-4.3 proposal #660. - svg_id = ifcopenshell.util.element.get_pset(element, "EPset_AnnotationSurveyArea", "PointType") + svg_id = tool.Drawing.get_annotation_symbol(element) if not svg_id: svg_id = str(ifcopenshell.util.element.get_predefined_type(element)) if not svg_id: @@ -883,9 +872,37 @@ class SvgWriter: point = Vector(((x_offset + point.x), (y_offset - point.y))) symbol_position_svg = point * self.svg_scale - self.svg.add(self.svg.use(f"#{svg_id}", insert=symbol_position_svg)) + svg_position_str = ", ".join(map(str, symbol_position_svg)) - def draw_point_annotation(self, obj, classes): + angle = self.get_empty_object_angle(obj) + symbol_transform = self.get_symbol_transform(svg_position_str, angle, obj) + self.draw_symbol(svg_id, symbol_transform) + + def get_empty_object_angle(self, obj: bpy.types.Object) -> float: + def get_basis_vector(matrix: bpy.types.Object, i: int = 0) -> Vector: + """returns basis vector for i in world space, unaffected by object scale""" + return matrix.inverted()[i].to_3d().normalized() + + text_dir_world_x_axis = get_basis_vector(obj.matrix_world) + + # RCP cameras may be scaled, so reset scales. + camera_matrix = tool.Drawing.get_camera_matrix(self.camera) + text_dir = (camera_matrix.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized() + return math.degrees(-text_dir.angle_signed(Vector((1, 0)))) + + def get_symbol_transform( + self, svg_position_str: str, angle: float = 0.0, obj: Optional[bpy.types.Object] = None + ) -> str: + """`obj` will be used to identify symbol scale, + ignore it if object scale doesn't match the symbol scale""" + # NOTE: for now we assume that scale is uniform + scale = 1.0 if not obj else obj.scale.x + return f"translate({svg_position_str}) rotate({angle}) scale({scale})" + + def draw_symbol(self, symbol_id: str, symbol_transform: str) -> None: + self.svg.add(self.svg.use(f"#{symbol_id}", transform=symbol_transform)) + + def draw_point_annotation(self, obj: bpy.types.Object, classes: list[str]) -> None: x_offset = self.raw_width / 2 y_offset = self.raw_height / 2 @@ -893,10 +910,7 @@ class SvgWriter: projected_points = [self.project_point_onto_camera(matrix_world @ v.co) for v in obj.data.vertices] element = tool.Ifc.get_entity(obj) - svg_id = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Symbol") - if not svg_id: - # EPset_AnnotationSurveyArea is not standard! See bSI-4.3 proposal #660. - svg_id = ifcopenshell.util.element.get_pset(element, "EPset_AnnotationSurveyArea", "PointType") + svg_id = tool.Drawing.get_annotation_symbol(element) if not svg_id: svg_id = str(ifcopenshell.util.element.get_predefined_type(element)) if not svg_id: @@ -905,7 +919,10 @@ class SvgWriter: for symbol_position in projected_points: symbol_position = Vector(((x_offset + symbol_position.x), (y_offset - symbol_position.y))) symbol_position_svg = symbol_position * self.svg_scale - self.svg.add(self.svg.use(f"#{svg_id}", insert=symbol_position_svg)) + svg_position_str = ", ".join(map(str, symbol_position_svg)) + + symbol_transform = self.get_symbol_transform(svg_position_str) + self.draw_symbol(svg_id, symbol_transform) def draw_break_annotations(self, obj): x_offset = self.raw_width / 2 @@ -1066,7 +1083,9 @@ class SvgWriter: # calculating text parameters and adding text text_position = arc_mid_point + arc_mid_dir * 5 text_style = SvgWriter.get_box_alignment_parameters("center") - angle_text = f"{int(angle)}deg" + drawing_pset_data = DrawingsData.data["active_drawing_pset_data"] + decimal_places = drawing_pset_data.get("AngleDecimalPlaces", None) + angle_text = f"{round(angle, decimal_places)}°" self.svg.add(self.svg.text(angle_text, insert=text_position, class_="ANGLE", **text_style)) # Draw SVG arc, see for details: http://xahlee.info/js/svg_circle_arc.html @@ -1402,7 +1421,7 @@ class SvgWriter: return text_tags - def project_point_onto_camera(self, point): + def project_point_onto_camera(self, point: Vector) -> Vector: # TODO is this needlessly complex? return self.camera.matrix_world.inverted() @ geometry.intersect_line_plane( point.xyz, diff --git a/src/blenderbim/blenderbim/bim/module/drawing/ui.py b/src/blenderbim/blenderbim/bim/module/drawing/ui.py index 2ccdb9ae8c..33a40450fd 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/ui.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/ui.py @@ -268,6 +268,11 @@ class BIM_PT_drawings(Panel): col = row.column() col.alignment = "RIGHT" + convert_to_dxf = row.row(align=True) + op = convert_to_dxf.operator("bim.convert_svg_to_dxf", text="", icon="IMAGE_DATA") + op.view = active_drawing.name + convert_to_dxf.enabled = active_drawing.ifc_definition_id > 0 + op = row.operator("bim.select_all_drawings", icon="SELECT_SUBTRACT", text="") open_drawing_button = row.row(align=True) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/workspace.py b/src/blenderbim/blenderbim/bim/module/drawing/workspace.py index 4e4b7e4dc6..0a7ae62ee6 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/workspace.py @@ -250,7 +250,7 @@ class AnnotationToolUI: row = cls.layout.row(align=True) row.label(text="", icon="FILE_3D") prop_with_search(row, cls.props, "relating_type_id", text="") - row.operator("bim.launch_annotation_type_manager", icon="LIGHTPROBE_GRID", text="") + row.operator("bim.launch_annotation_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="") add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation") diff --git a/src/blenderbim/blenderbim/bim/module/geometry/helper.py b/src/blenderbim/blenderbim/bim/module/geometry/helper.py index 383bdb6a4b..3b7281a1a8 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/helper.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/helper.py @@ -21,12 +21,14 @@ import bmesh import mathutils import ifcopenshell import ifcopenshell.util.unit +import blenderbim.tool as tool from math import pi, pow from mathutils import Vector, Matrix, geometry +from typing import Union class Helper: - def __init__(self, file): + def __init__(self, file: ifcopenshell.file): self.file = file self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) @@ -34,7 +36,7 @@ class Helper: # edge that shares a single vertex only with that face to find the extrusion # edge. A face with the normal facing down is prioritised. A limited # dissolve ensure that faces are quads and not tris. - def auto_detect_rectangle_profile_extruded_area_solid(self, mesh): + def auto_detect_rectangle_profile_extruded_area_solid(self, mesh: bpy.types.Mesh) -> dict: bm = bmesh.new() bm.from_mesh(mesh) bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges) @@ -115,7 +117,9 @@ class Helper: return {"profile": profile, "extrusion": extrusion} - def auto_detect_arbitrary_profile_with_voids(self, obj, mesh): + def auto_detect_arbitrary_profile_with_voids( + self, obj: bpy.types.Object, mesh: bpy.types.Mesh + ) -> Union[tuple, dict]: groups = {"IFCARCINDEX": [], "IFCCIRCLE": []} for i, group in enumerate(obj.vertex_groups): if "IFCARCINDEX" in group.name: @@ -138,28 +142,31 @@ class Helper: total_groups = 0 is_circle = False for group_type, group_indices in groups.items(): - for group_index in group_indices: - if group_index in vert[deform_layer]: - if group_type == "IFCCIRCLE": - is_circle = True - group_verts[group_type].setdefault(group_index, 0) - group_verts[group_type][group_index] += 1 - total_groups += 0 + if not group_indices: + continue + is_special, group_index = tool.Blender.bmesh_check_vertex_in_groups(vert, deform_layer, group_indices) + if not is_special: + continue + if group_type == "IFCCIRCLE": + is_circle = True + group_verts[group_type].setdefault(group_index, 0) + group_verts[group_type][group_index] += 1 + total_groups += 0 if total_groups > 1: # A vert can only belong to one group return (False, "AMBIGUOUS_SPECIAL_VERTEX") elif is_circle: - pass # Circles are allowed to be unclosed + pass # Circles are allowed to be unclosed elif total_groups == 0 and len(vert.link_edges) != 2: # Unclosed loop or forked loop return (False, "UNCLOSED_LOOP") for group_type, group_counts in group_verts.items(): if group_type == "IFCARCINDEX": for group_count in group_counts.values(): - if group_count != 3: # Each arc needs 3 verts + if group_count != 3: # Each arc needs 3 verts return (False, "3POINT_ARC") elif group_type == "IFCCIRCLE": for group_count in group_counts.values(): - if group_count != 2: # Each circle needs 2 verts + if group_count != 2: # Each circle needs 2 verts return (False, "CIRCLE") loop_edges = set(bm.edges) @@ -269,7 +276,7 @@ class Helper: inner_loops.remove(outer_loop) # Copy vectors to prevent random data mangling after bmesh is freed. - points = [Vector(list(v.co)) for v in bm.verts] + points = [v.co.copy() for v in bm.verts] bm.to_mesh(mesh) mesh.update() diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index 50ad245ac0..fb37c3cf7e 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -324,8 +324,8 @@ class UpdateRepresentation(bpy.types.Operator, Operator): # We are explicitly casting to a tessellation, so remove all parametric materials. element_type = ifcopenshell.util.element.get_type(product) if element_type: # Some invalid IFCs use material sets without a type. - ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), product=element_type) - ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), product=product) + ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), products=[element_type]) + ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), products=[product]) else: # These objects are parametrically based on an axis and should not be modified as a mesh return @@ -972,7 +972,7 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator): if parts: index = get_max_index(parts) add_linked_aggregate_pset(element, index) - index +=1 + index += 1 for part in parts: if part.is_a("IfcElementAssembly"): select_objects_and_add_data(part) @@ -982,16 +982,13 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator): obj = tool.Ifc.get_object(part) obj.select_set(True) - - + def add_linked_aggregate_pset(part, index): pset = ifcopenshell.util.element.get_pset(part, self.pset_name) - + if not pset: - pset = ifcopenshell.api.run( - "pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name - ) - + pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name) + ifcopenshell.api.run( "pset.edit_pset", tool.Ifc.get(), @@ -1016,7 +1013,9 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator): return linked_aggregate_group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.group_name) - ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), products=[element], group=linked_aggregate_group) + ifcopenshell.api.run( + "group.assign_group", tool.Ifc.get(), products=[element], group=linked_aggregate_group + ) def custom_incremental_naming_for_element_assembly(old_to_new): for new in old_to_new.values(): @@ -1027,16 +1026,16 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator): if r.is_a("IfcRelAssignsToGroup") if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name ][0] - + number = len(group_elements) - 1 number = f"{number:02d}" new_obj = tool.Ifc.get_object(new[0]) - pattern1 = r'_\d' + pattern1 = r"_\d" if re.findall(pattern1, new_obj.name): split_name = new_obj.name.split("_") new_obj.name = split_name[0] + "_" + number continue - pattern2 = r'\.\d{3}' + pattern2 = r"\.\d{3}" if re.findall(pattern2, new_obj.name): split_name = new_obj.name.split(".") new_obj.name = split_name[0] + "_" + number @@ -1074,6 +1073,7 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator): ] tool.Ifc.run("group.assign_group", group=linked_aggregate_group[0], products=new) + if len(context.selected_objects) != 1: return {"FINISHED"} @@ -1099,14 +1099,11 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator): copy_linked_aggregate_data(old_to_new) custom_incremental_naming_for_element_assembly(old_to_new) - + blenderbim.bim.handler.refresh_ui_data() return old_to_new - - - class RefreshLinkedAggregate(bpy.types.Operator): bl_idname = "bim.refresh_linked_aggregate" @@ -1154,9 +1151,9 @@ class RefreshLinkedAggregate(bpy.types.Operator): original_names[group] = {} pset = ifcopenshell.util.element.get_pset(element, self.pset_name) - index = pset['Index'] + index = pset["Index"] original_names[group][index] = tool.Ifc.get_object(element).name - + parts = ifcopenshell.util.element.get_parts(element) if parts: for part in parts: @@ -1164,20 +1161,22 @@ class RefreshLinkedAggregate(bpy.types.Operator): original_names | get_original_names(part) else: try: - pset = ifcopenshell.util.element.get_pset(part, self.pset_name) + pset = ifcopenshell.util.element.get_pset(part, self.pset_name) except: - continue - index = pset['Index'] + continue + index = pset["Index"] original_names[group][index] = tool.Ifc.get_object(part).name - + return original_names def set_original_name(obj, original_names): element = tool.Ifc.get_entity(obj) aggregate = ifcopenshell.util.element.get_aggregate(element) - if ifcopenshell.util.element.get_parts(element): # if element has parts it means it is the base of and aggregate or sub-aggregate + if ifcopenshell.util.element.get_parts( + element + ): # if element has parts it means it is the base of and aggregate or sub-aggregate aggregate = element - + group = [ r.RelatingGroup for r in getattr(aggregate, "HasAssignments", []) or [] @@ -1186,12 +1185,12 @@ class RefreshLinkedAggregate(bpy.types.Operator): ] if not group: return - + group = group[0].id() - + pset = ifcopenshell.util.element.get_pset(element, self.pset_name) - index = pset['Index'] - + index = pset["Index"] + try: obj.name = original_names[group][index] except: @@ -1295,9 +1294,9 @@ class RefreshLinkedAggregate(bpy.types.Operator): element_aggregate = ifcopenshell.util.element.get_aggregate(element) selected_matrix, duplicate_matrix = get_original_matrix(element, base_instance) - + original_names = get_original_names(element) - + delete_objects(element) for obj in context.selected_objects: @@ -1308,24 +1307,24 @@ class RefreshLinkedAggregate(bpy.types.Operator): old_to_new = DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(self, context) set_new_matrix(selected_matrix, duplicate_matrix, old_to_new) - + for old, new in old_to_new.items(): if element_aggregate and new[0].is_a("IfcElementAssembly"): new_aggregate = ifcopenshell.util.element.get_aggregate(new[0]) if not new_aggregate: blenderbim.core.aggregate.assign_object( - tool.Ifc, - tool.Aggregate, - tool.Collector, - relating_obj=tool.Ifc.get_object(element_aggregate), - related_obj=tool.Ifc.get_object(new[0]), - ) - + tool.Ifc, + tool.Aggregate, + tool.Collector, + relating_obj=tool.Ifc.get_object(element_aggregate), + related_obj=tool.Ifc.get_object(new[0]), + ) + for old, new in old_to_new.items(): new_obj = tool.Ifc.get_object(new[0]) set_original_name(new_obj, original_names) - + blenderbim.bim.handler.refresh_ui_data() operator_time = time() - refresh_start_time @@ -1473,7 +1472,9 @@ class OverrideModeSetEdit(bpy.types.Operator): for obj in selected_objs: if not obj: continue - if not obj.data: + obj_supports_edit_mode, message = tool.Blender.object_supports_edit_mode(obj) + if not obj_supports_edit_mode: + self.report({"INFO"}, message) obj.select_set(False) continue element = tool.Ifc.get_entity(obj) diff --git a/src/blenderbim/blenderbim/bim/module/geometry/ui.py b/src/blenderbim/blenderbim/bim/module/geometry/ui.py index ea596cad28..b2a52315ec 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/ui.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/ui.py @@ -212,7 +212,7 @@ class BIM_PT_representation_items(Panel): if props.is_editing_item_style: # NOTE: we currently support 1 item having just 1 style # when IfcStyledItem can actually have multiple styles - row.prop(props, "representation_item_style", icon="SHADING_RENDERED", text="") + prop_with_search(row, props, "representation_item_style", icon="SHADING_RENDERED", text="") row.operator("bim.edit_representation_item_style", icon="CHECKMARK", text="") row.operator("bim.disable_editing_representation_item_style", icon="CANCEL", text="") else: diff --git a/src/blenderbim/blenderbim/bim/module/group/operator.py b/src/blenderbim/blenderbim/bim/module/group/operator.py index c7dbdd6c8b..ddb5bb8845 100644 --- a/src/blenderbim/blenderbim/bim/module/group/operator.py +++ b/src/blenderbim/blenderbim/bim/module/group/operator.py @@ -212,7 +212,7 @@ class UnassignGroup(bpy.types.Operator, tool.Ifc.Operator): "group.unassign_group", self.file, **{ - "product": self.file.by_id(product.BIMObjectProperties.ifc_definition_id), + "products": [self.file.by_id(product.BIMObjectProperties.ifc_definition_id)], "group": self.file.by_id(self.group), } ) diff --git a/src/blenderbim/blenderbim/bim/module/layer/operator.py b/src/blenderbim/blenderbim/bim/module/layer/operator.py index c3c918e907..70d1213586 100644 --- a/src/blenderbim/blenderbim/bim/module/layer/operator.py +++ b/src/blenderbim/blenderbim/bim/module/layer/operator.py @@ -151,7 +151,10 @@ class AssignPresentationLayer(bpy.types.Operator): ifcopenshell.api.run( "layer.assign_layer", self.file, - **{"item": self.file.by_id(item.BIMMeshProperties.ifc_definition_id), "layer": self.file.by_id(self.layer)} + **{ + "items": [self.file.by_id(item.BIMMeshProperties.ifc_definition_id)], + "layer": self.file.by_id(self.layer), + } ) return {"FINISHED"} @@ -173,7 +176,10 @@ class UnassignPresentationLayer(bpy.types.Operator): ifcopenshell.api.run( "layer.unassign_layer", self.file, - **{"item": self.file.by_id(item.BIMMeshProperties.ifc_definition_id), "layer": self.file.by_id(self.layer)} + **{ + "items": [self.file.by_id(item.BIMMeshProperties.ifc_definition_id)], + "layer": self.file.by_id(self.layer), + } ) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/material/data.py b/src/blenderbim/blenderbim/bim/module/material/data.py index d2a07d480d..cfdf3a8058 100644 --- a/src/blenderbim/blenderbim/bim/module/material/data.py +++ b/src/blenderbim/blenderbim/bim/module/material/data.py @@ -19,6 +19,7 @@ import os import bpy import ifcopenshell +import ifcopenshell.util.doc import ifcopenshell.util.schema import blenderbim.tool as tool @@ -109,29 +110,41 @@ class MaterialsData: def active_styles(cls): props = bpy.context.scene.BIMMaterialProperties results = [] - if props.materials and props.active_material_index < len(props.materials): - material = props.materials[props.active_material_index].ifc_definition_id - if not material: - return results - material = tool.Ifc.get().by_id(material) - for definition in material.HasRepresentation: - for representation in definition.Representations: - if not representation.is_a("IfcStyledRepresentation"): + if not props.materials or props.active_material_index >= len(props.materials): + return results + + material = props.materials[props.active_material_index].ifc_definition_id + if not material: + return results + + material = tool.Ifc.get_entity_by_id(material) + + # NOTE: it's possible that data will be refreshed during material removal + # (when it will try to fetch active material type and will reach for material_types) + # and props.materials[i].ifc_definition_id will contain already removed id + if not material or not material.is_a("IfcMaterial"): + return results + + for definition in material.HasRepresentation: + for representation in definition.Representations: + if not representation.is_a("IfcStyledRepresentation"): + continue + context = representation.ContextOfItems + for item in representation.Items: + if not item.is_a("IfcStyledItem"): continue - context = representation.ContextOfItems - for item in representation.Items: - if not item.is_a("IfcStyledItem"): - continue - for style in item.Styles: - if style.is_a("IfcSurfaceStyle"): - results.append({ + for style in item.Styles: + if style.is_a("IfcSurfaceStyle"): + results.append( + { "context_type": context.ContextType, "context_identifier": getattr(context, "ContextIdentifier", ""), "target_view": getattr(context, "TargetView", ""), "name": style.Name or "Unnamed", "id": style.id(), "context_id": context.id(), - }) + } + ) return results diff --git a/src/blenderbim/blenderbim/bim/module/material/operator.py b/src/blenderbim/blenderbim/bim/module/material/operator.py index a682a66e20..77241afef5 100644 --- a/src/blenderbim/blenderbim/bim/module/material/operator.py +++ b/src/blenderbim/blenderbim/bim/module/material/operator.py @@ -34,6 +34,7 @@ from blenderbim.bim.ifc import IfcStore class LoadMaterials(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.load_materials" bl_label = "Load Materials" + bl_description = "Display list of named materials" bl_options = {"REGISTER", "UNDO"} def _execute(self, context): @@ -117,7 +118,7 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_material" bl_label = "Add Material" bl_options = {"REGISTER", "UNDO"} - obj: bpy.props.StringProperty() + obj: bpy.props.StringProperty(name="Material Name") name: bpy.props.StringProperty(default="Default") def invoke(self, context, event): @@ -762,6 +763,11 @@ class EditMaterialStyle(bpy.types.Operator, tool.Ifc.Operator): ifcopenshell.api.run("style.assign_material_style", ifc_file, material=material, style=style, context=context) tool.Material.disable_editing_material() core.load_materials(tool.Material, props.material_type) + # NOTE: Update all elements that has this material and + # not just the ones that are using it in IfcMaterialConstituent, + # to handle cases where Style is connected to geometry implicitly: + # e.g. RepItem->IfcElement->IfcMaterial->Style + # instead of RepItem->ShapeAspect->Constituent->Style. tool.Material.update_elements_using_material(material) diff --git a/src/blenderbim/blenderbim/bim/module/material/ui.py b/src/blenderbim/blenderbim/bim/module/material/ui.py index 06a0fc04c3..1f24392af2 100644 --- a/src/blenderbim/blenderbim/bim/module/material/ui.py +++ b/src/blenderbim/blenderbim/bim/module/material/ui.py @@ -175,7 +175,7 @@ class BIM_PT_object_material(Panel): if ObjectMaterialData.data["type_material"]: row = self.layout.row(align=True) - row.label(text="Inherited Material: " + ObjectMaterialData.data["type_material"], icon="FILE_PARENT") + row.label(text="Inherited Material: " + ObjectMaterialData.data["type_material"], icon="CON_CHILDOF") if ObjectMaterialData.data["material_class"]: return self.draw_material_ui() diff --git a/src/blenderbim/blenderbim/bim/module/model/decorator.py b/src/blenderbim/blenderbim/bim/module/model/decorator.py index 72d47d3c42..080966c22d 100644 --- a/src/blenderbim/blenderbim/bim/module/model/decorator.py +++ b/src/blenderbim/blenderbim/bim/module/model/decorator.py @@ -23,6 +23,7 @@ from math import sin, cos, radians from bpy.types import SpaceView3D from mathutils import Vector, Matrix from gpu_extras.batch import batch_for_shader +from typing import Union ERROR_ELEMENTS_COLOR = (1, 0.2, 0.322, 1) # RED @@ -35,15 +36,6 @@ def transparent_color(color, alpha=0.1): return color -def bm_check_vertex_in_groups(vertex, deform_layer, groups): - """returns tuple boolean (whether vertex is in any of the groups) - and related group index""" - for group_index in vertex[deform_layer].keys(): - if group_index in groups: - return True, group_index - return False, None - - class ProfileDecorator: installed = None @@ -148,12 +140,12 @@ class ProfileDecorator: # deform_layer is None if there are no verts assigned to vertex groups # even if there are vertex groups in the obj.vertex_groups if deform_layer: - is_arc, group_index = bm_check_vertex_in_groups(vertex, deform_layer, arc_groups) + is_arc, group_index = tool.Blender.bmesh_check_vertex_in_groups(vertex, deform_layer, arc_groups) if is_arc: arcs.setdefault(group_index, []).append(vertex) special_vertex_indices[vertex.index] = group_index - is_circle, group_index = bm_check_vertex_in_groups(vertex, deform_layer, circle_groups) + is_circle, group_index = tool.Blender.bmesh_check_vertex_in_groups(vertex, deform_layer, circle_groups) if is_circle: circles.setdefault(group_index, []).append(vertex) special_vertex_indices[vertex.index] = group_index diff --git a/src/blenderbim/blenderbim/bim/module/model/mep.py b/src/blenderbim/blenderbim/bim/module/model/mep.py index cc3daa610d..c3a19c5727 100644 --- a/src/blenderbim/blenderbim/bim/module/model/mep.py +++ b/src/blenderbim/blenderbim/bim/module/model/mep.py @@ -542,7 +542,7 @@ class MEPGenerator: ifc_representation_class=None, ) - rel = ifcopenshell.api.run("material.assign_material", ifc_file, product=element, type="IfcMaterialProfileSet") + rel = ifcopenshell.api.run("material.assign_material", ifc_file, products=[element], type="IfcMaterialProfileSet") profile_set = rel.RelatingMaterial material_profile = ifcopenshell.api.run( "material.add_profile", ifc_file, profile_set=profile_set, material=material diff --git a/src/blenderbim/blenderbim/bim/module/model/opening.py b/src/blenderbim/blenderbim/bim/module/model/opening.py index 2c21ba8352..1d55fb305d 100644 --- a/src/blenderbim/blenderbim/bim/module/model/opening.py +++ b/src/blenderbim/blenderbim/bim/module/model/opening.py @@ -24,10 +24,14 @@ import shapely import logging import numpy as np import ifcopenshell +import ifcopenshell.api +import ifcopenshell.geom import ifcopenshell.util.shape import ifcopenshell.util.element import ifcopenshell.util.shape_builder +import ifcopenshell.util.placement import ifcopenshell.util.representation +import ifcopenshell.util.unit import blenderbim.tool as tool import blenderbim.core.geometry import blenderbim.bim.import_ifc as import_ifc @@ -107,7 +111,7 @@ class FilledOpeningGenerator: # Equivalent to "side Z" for a wall axis, so that stuff like skylights appear on the top. local_position_on_voided_obj.z = layers["offset"] + layers["thickness"] new_matrix.translation.xyz = voided_obj.matrix_world @ local_position_on_voided_obj - rotation_matrix = Matrix.Rotation(radians(-90), 4, 'X') + rotation_matrix = Matrix.Rotation(radians(-90), 4, "X") new_matrix @= rotation_matrix filling_obj.matrix_world = new_matrix @@ -184,7 +188,13 @@ class FilledOpeningGenerator: ) def regenerate_from_type(self, usecase_path, ifc_file, settings): - filling = settings["related_object"] + relating_type = settings["relating_type"] + + for related_object in settings["related_objects"]: + self._regenerate_from_type(related_object) + + def _regenerate_from_type(self, related_object: ifcopenshell.entity_instance) -> None: + filling = related_object if not getattr(filling, "FillsVoids", None): return diff --git a/src/blenderbim/blenderbim/bim/module/model/product.py b/src/blenderbim/blenderbim/bim/module/model/product.py index 8d255a38a0..8c66e838d9 100644 --- a/src/blenderbim/blenderbim/bim/module/model/product.py +++ b/src/blenderbim/blenderbim/bim/module/model/product.py @@ -25,9 +25,13 @@ import ifcopenshell.util.system import ifcopenshell.util.element import ifcopenshell.util.placement import ifcopenshell.util.representation +import ifcopenshell.util.type +import ifcopenshell.util.unit import blenderbim.tool as tool +import blenderbim.core.aggregate import blenderbim.core.type import blenderbim.core.geometry +import blenderbim.core.spatial from . import wall, slab, profile, mep from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.model.data import AuthoringData @@ -273,7 +277,7 @@ class AddConstrTypeInstance(bpy.types.Operator): obj.location[2] = collection_obj.location[2] + bpy.context.scene.BIMModelProperties.rl2 @staticmethod - def generate_layered_element(ifc_class, relating_type): + def generate_layered_element(ifc_class: str, relating_type: ifcopenshell.entity_instance) -> bool: layer_set_direction = None parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric") @@ -297,6 +301,7 @@ class AddConstrTypeInstance(bpy.types.Operator): material = ifcopenshell.util.element.get_material(tool.Ifc.get_entity(obj)) material.LayerSetDirection = layer_set_direction return True + return False class ChangeTypePage(bpy.types.Operator, tool.Ifc.Operator): @@ -510,7 +515,7 @@ def ensure_material_assigned(usecase_path, ifc_file, settings): if usecase_path == "material.assign_material": if not settings.get("material", None): return - elements = [settings["product"]] + elements = settings["products"] else: elements = [] for rel in ifc_file.by_type("IfcRelAssociatesMaterial"): @@ -546,7 +551,7 @@ def ensure_material_assigned(usecase_path, ifc_file, settings): def ensure_material_unassigned(usecase_path, ifc_file, settings): - elements = [settings["product"]] + elements = settings["products"] if elements[0].is_a("IfcElementType"): elements.extend(ifcopenshell.util.element.get_types(elements[0])) for element in elements: diff --git a/src/blenderbim/blenderbim/bim/module/model/profile.py b/src/blenderbim/blenderbim/bim/module/model/profile.py index 0b889ba035..c7b0af7b78 100644 --- a/src/blenderbim/blenderbim/bim/module/model/profile.py +++ b/src/blenderbim/blenderbim/bim/module/model/profile.py @@ -21,14 +21,18 @@ import copy import bmesh import mathutils.geometry import ifcopenshell +import ifcopenshell.api import ifcopenshell.util.type import ifcopenshell.util.unit import ifcopenshell.util.element +import ifcopenshell.util.placement +import ifcopenshell.util.representation import blenderbim.bim.handler import blenderbim.tool as tool import blenderbim.core.type import blenderbim.core.geometry import blenderbim.core.material +import blenderbim.core.root from math import pi, degrees, inf from mathutils import Vector, Matrix, Quaternion from blenderbim.bim.module.geometry.helper import Helper @@ -92,7 +96,7 @@ class DumbProfileGenerator: should_add_representation=False, context=self.body_context, ) - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=self.relating_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=self.relating_type) material = ifcopenshell.util.element.get_material(element) material.CardinalPoint = self.cardinal_point @@ -207,12 +211,19 @@ class DumbProfileRegenerator: return results def regenerate_from_type(self, usecase_path, ifc_file, settings): - obj = tool.Ifc.get_object(settings["related_object"]) - if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id: - return - new_material = ifcopenshell.util.element.get_material(settings["relating_type"]) + relating_type = settings["relating_type"] + + new_material = ifcopenshell.util.element.get_material(relating_type) if not new_material or not new_material.is_a("IfcMaterialProfileSet"): return + + for related_object in settings["related_objects"]: + self._regenerate_from_type(related_object) + + def _regenerate_from_type(self, related_object: ifcopenshell.entity_instance) -> None: + obj = tool.Ifc.get_object(related_object) + if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id: + return DumbProfileRecalculator().recalculate([obj]) diff --git a/src/blenderbim/blenderbim/bim/module/model/slab.py b/src/blenderbim/blenderbim/bim/module/model/slab.py index 624d41e2bd..ec23ca101f 100644 --- a/src/blenderbim/blenderbim/bim/module/model/slab.py +++ b/src/blenderbim/blenderbim/bim/module/model/slab.py @@ -20,17 +20,22 @@ import bpy import json import bmesh import ifcopenshell -import ifcopenshell.util.type -import ifcopenshell.util.unit +import ifcopenshell.api import ifcopenshell.util.element +import ifcopenshell.util.placement +import ifcopenshell.util.representation +import ifcopenshell.util.unit +import ifcopenshell.util.type import blenderbim.bim.handler import blenderbim.core.type import blenderbim.core.geometry +import blenderbim.core.root import blenderbim.tool as tool from math import radians from mathutils import Vector, Matrix from blenderbim.bim.module.geometry.helper import Helper from blenderbim.bim.module.model.decorator import ProfileDecorator +from typing import Optional def calculate_quantities(usecase_path, ifc_file, settings): @@ -110,7 +115,7 @@ def calculate_quantities(usecase_path, ifc_file, settings): class DumbSlabGenerator: - def __init__(self, relating_type): + def __init__(self, relating_type: ifcopenshell.entity_instance): self.relating_type = relating_type def generate(self): @@ -173,7 +178,7 @@ class DumbSlabGenerator: should_add_representation=False, context=self.body_context, ) - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=self.relating_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=self.relating_type) blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) representation = ifcopenshell.api.run( @@ -246,27 +251,38 @@ class DumbSlabPlaner: self.change_thickness(element, total_thickness) def regenerate_from_type(self, usecase_path, ifc_file, settings): - obj = tool.Ifc.get_object(settings["related_object"]) - if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id: - return - self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) - new_material = ifcopenshell.util.element.get_material(settings["relating_type"]) + relating_type = settings["relating_type"] + + new_material = ifcopenshell.util.element.get_material(relating_type) if not new_material or not new_material.is_a("IfcMaterialLayerSet"): return + parametric = ifcopenshell.util.element.get_psets(settings["relating_type"]).get("EPset_Parametric") layer_set_direction = None if parametric: layer_set_direction = parametric.get("LayerSetDirection", layer_set_direction) new_thickness = sum([l.LayerThickness for l in new_material.MaterialLayers]) - material = ifcopenshell.util.element.get_material(settings["related_object"]) + + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) + for related_object in settings["related_objects"]: + self._regenerate_from_type(related_object, layer_set_direction, new_thickness) + + def _regenerate_from_type( + self, related_object: ifcopenshell.entity_instance, layer_set_direction: Optional[str], new_thickness: float + ) -> None: + obj = tool.Ifc.get_object(related_object) + if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id: + return + + material = ifcopenshell.util.element.get_material(related_object) if not material or not material.is_a("IfcMaterialLayerSetUsage"): return if layer_set_direction: material.LayerSetDirection = layer_set_direction if material.LayerSetDirection == "AXIS3": - self.change_thickness(settings["related_object"], new_thickness) + self.change_thickness(related_object, new_thickness) - def change_thickness(self, element, thickness): + def change_thickness(self, element: ifcopenshell.entity_instance, thickness: float) -> None: body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") obj = tool.Ifc.get_object(element) if not obj: @@ -698,9 +714,7 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator): new_footprint = ifcopenshell.api.run( "geometry.add_footprint_representation", tool.Ifc.get(), context=footprint_context, curves=curves ) - old_footprint = ifcopenshell.util.representation.get_representation( - element, "Plan", "FootPrint", "SKETCH_VIEW" - ) + old_footprint = ifcopenshell.util.representation.get_representation(element, "Plan", "FootPrint", "SKETCH_VIEW") if old_footprint: for inverse in tool.Ifc.get().get_inverse(old_footprint): ifcopenshell.util.element.replace_attribute(inverse, old_footprint, new_footprint) diff --git a/src/blenderbim/blenderbim/bim/module/model/space.py b/src/blenderbim/blenderbim/bim/module/model/space.py index 2e14db2097..90ada213a6 100644 --- a/src/blenderbim/blenderbim/bim/module/model/space.py +++ b/src/blenderbim/blenderbim/bim/module/model/space.py @@ -29,13 +29,17 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.generate_space" bl_label = "Generate Space" bl_options = {"REGISTER"} - bl_description = "Create a space from the cursor position. Move the cursor position into the desired position, select the right space collection and run the operator" + bl_description = ( + "Create a space from the cursor position. " + "Move the cursor position into the desired position, " + "select the right space collection and run the operator" + ) @classmethod def poll(cls, context): collection = context.view_layer.active_layer_collection.collection collection_obj = collection.BIMCollectionProperties.obj - active_obj = bpy.context.active_object + active_obj = context.active_object element = tool.Ifc.get_entity(active_obj) return tool.Ifc.get_entity(collection_obj) and not element.is_a("IfcWall") @@ -47,16 +51,14 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator): def msg(self, context): self.layout.label(text="NO ACTIVE STOREY") -# props = context.scene.BIMModelProperties - collection = context.view_layer.active_layer_collection.collection collection_obj = collection.BIMCollectionProperties.obj if not collection_obj: - bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR") + context.window_manager.popup_menu(msg, title="Error", icon="ERROR") return spatial_element = tool.Ifc.get_entity(collection_obj) if not spatial_element: - bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR") + context.window_manager.popup_menu(msg, title="Error", icon="ERROR") return core.generate_space(tool.Ifc, tool.Spatial, tool.Model, tool.Type) @@ -70,7 +72,7 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - active_obj = bpy.context.active_object + active_obj = context.active_object element = tool.Ifc.get_entity(active_obj) if element: return context.selected_objects and element.is_a("IfcWall") @@ -81,7 +83,7 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator): # In order to run, the active object must be a wall and # there must be selected walls - active_obj = bpy.context.active_object + active_obj = context.active_object element = tool.Ifc.get_entity(active_obj) container = tool.Spatial.get_container(element) @@ -96,28 +98,30 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator): if not container: self.report({"ERROR"}, "The wall is not contained.") - if not bpy.context.selected_objects: + if not context.selected_objects: self.report({"ERROR"}, "No selected objects found. Please select walls.") return core.generate_spaces_from_walls(tool.Ifc, tool.Spatial, tool.Collector) + class ToggleSpaceVisibility(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.toggle_space_visibility" bl_label = "Toggle Space Visibility" bl_options = {"REGISTER"} bl_description = "Change the space visibility" - def execute(cls, context): + def execute(self, context): core.toggle_space_visibility(tool.Ifc, tool.Spatial) return {"FINISHED"} + class ToggleHideSpaces(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.toggle_hide_spaces" bl_label = "Toggle Hide Spaces" bl_options = {"REGISTER"} bl_description = "Hide or Unhide all spaces" - def execute(cls, context): + def execute(self, context): core.toggle_hide_spaces(tool.Ifc, tool.Spatial) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/model/ui.py b/src/blenderbim/blenderbim/bim/module/model/ui.py index f3ecff8691..367917a3f0 100644 --- a/src/blenderbim/blenderbim/bim/module/model/ui.py +++ b/src/blenderbim/blenderbim/bim/module/model/ui.py @@ -54,8 +54,10 @@ class LaunchTypeManager(bpy.types.Operator): props.type_page = 1 if get_ifc_class(None, context): ifc_class = props.ifc_class or AuthoringData.data["ifc_element_type"] - props.type_class = ifc_class - bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class, offset=0, limit=9) + else: + ifc_class = AuthoringData.data["ifc_element_type"] + props.type_class = ifc_class + bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class, offset=0, limit=9) return context.window_manager.invoke_popup(self, width=550) def draw(self, context): diff --git a/src/blenderbim/blenderbim/bim/module/model/wall.py b/src/blenderbim/blenderbim/bim/module/model/wall.py index dc1be54f0c..78e8b4c579 100644 --- a/src/blenderbim/blenderbim/bim/module/model/wall.py +++ b/src/blenderbim/blenderbim/bim/module/model/wall.py @@ -24,7 +24,9 @@ import ifcopenshell import ifcopenshell.api import ifcopenshell.util.unit import ifcopenshell.util.element +import ifcopenshell.util.placement import ifcopenshell.util.representation +import ifcopenshell.util.type import mathutils.geometry import blenderbim.bim.handler import blenderbim.core.type @@ -35,6 +37,7 @@ from blenderbim.bim.ifc import IfcStore from math import pi, sin, cos, degrees, radians from mathutils import Vector, Matrix from blenderbim.bim.module.model.opening import FilledOpeningGenerator +from typing import Optional class JoinWall(bpy.types.Operator, tool.Ifc.Operator): @@ -555,7 +558,10 @@ class DumbWallGenerator: for face in sibling_obj.data.polygons: normal = (sibling_obj.matrix_world.to_quaternion() @ face.normal).normalized() face_center = sibling_obj.matrix_world @ face.center - if normal.z != 0 or abs(mathutils.geometry.distance_point_to_plane(self.location, face_center, normal)) > 0.01: + if ( + normal.z != 0 + or abs(mathutils.geometry.distance_point_to_plane(self.location, face_center, normal)) > 0.01 + ): continue rotation = math.atan2(normal[1], normal[0]) @@ -603,7 +609,7 @@ class DumbWallGenerator: should_add_representation=False, context=self.body_context, ) - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=self.relating_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=self.relating_type) if self.axis_context: representation = ifcopenshell.api.run( "geometry.add_axis_representation", @@ -724,18 +730,29 @@ class DumbWallPlaner: DumbWallRecalculator().recalculate([w for w in set(walls) if w]) def regenerate_from_type(self, usecase_path, ifc_file, settings): - obj = tool.Ifc.get_object(settings["related_object"]) - if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id: - return - self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) - new_material = ifcopenshell.util.element.get_material(settings["relating_type"]) + relating_type = settings["relating_type"] + + new_material = ifcopenshell.util.element.get_material(relating_type) if not new_material or not new_material.is_a("IfcMaterialLayerSet"): return - parametric = ifcopenshell.util.element.get_psets(settings["relating_type"]).get("EPset_Parametric") + + parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric") layer_set_direction = None if parametric: layer_set_direction = parametric.get("LayerSetDirection", layer_set_direction) - material = ifcopenshell.util.element.get_material(settings["related_object"]) + + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) + for related_object in settings["related_objects"]: + self._regenerate_from_type(related_object, layer_set_direction) + + def _regenerate_from_type( + self, related_object: ifcopenshell.entity_instance, layer_set_direction: Optional[str] + ) -> None: + obj = tool.Ifc.get_object(related_object) + if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id: + return + + material = ifcopenshell.util.element.get_material(related_object) if not material or not material.is_a("IfcMaterialLayerSetUsage"): return if layer_set_direction: @@ -854,11 +871,11 @@ class DumbWallJoiner: if (location_on_base - location).length < (location_on_side - location).length: axis_offset = location_on_side - location_on_base offset_from_axis = location_on_base - location - opening_matrix.translation = (location_on_base - axis_offset - offset_from_axis) + opening_matrix.translation = location_on_base - axis_offset - offset_from_axis else: axis_offset = location_on_side - location_on_base offset_from_axis = location_on_side - location - opening_matrix.translation = (location_on_side - axis_offset - offset_from_axis) + opening_matrix.translation = location_on_side - axis_offset - offset_from_axis opening_matrixes[opening] = opening_matrix for filling in [r.RelatedBuildingElement for r in opening.HasFillings]: @@ -871,11 +888,11 @@ class DumbWallJoiner: if (location_on_base - location).length < (location_on_side - location).length: axis_offset = location_on_side - location_on_base offset_from_axis = location_on_base - location - filling_matrix.translation = (location_on_base - axis_offset - offset_from_axis) + filling_matrix.translation = location_on_base - axis_offset - offset_from_axis else: axis_offset = location_on_side - location_on_base offset_from_axis = location_on_side - location - filling_matrix.translation = (location_on_side - axis_offset - offset_from_axis) + filling_matrix.translation = location_on_side - axis_offset - offset_from_axis filling_matrixes[filling] = filling_matrix self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"]) @@ -1120,13 +1137,8 @@ class DumbWallJoiner: other = tool.Ifc.get_object(rel.RelatedElement) if connection not in ["ATPATH", "NOTDEFINED"]: self.join( - obj, - other, - connection, - rel.RelatedConnectionType, - is_relating=True, - description=rel.Description - ) + obj, other, connection, rel.RelatedConnectionType, is_relating=True, description=rel.Description + ) for rel in element.ConnectedFrom: if rel.is_a("IfcRelConnectsPathElements"): connection = rel.RelatedConnectionType diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py index 100d59cb5d..c1c62f95ce 100644 --- a/src/blenderbim/blenderbim/bim/module/model/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py @@ -516,20 +516,21 @@ class BimToolUI: prop_with_search(row, cls.props, "relating_type_id", text="") else: row.label(text="No Construction Type", icon="FILE_3D") - row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="") + row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="") else: if AuthoringData.data["ifc_element_type"]: row.label(text=f"No {AuthoringData.data['ifc_element_type']} Found", icon="ERROR") row = cls.layout.row() row.prop(cls.props, "type_name") - row = cls.layout.row() + row = cls.layout.row(align=True) op = row.operator( "bim.add_default_type", icon="ADD", text=f"Add {AuthoringData.data['ifc_element_type']}" ) op.ifc_element_type = AuthoringData.data["ifc_element_type"] + row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="") else: row.label(text="No Element Types Found", icon="ERROR") - row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="Launch Type Manager") + row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="Launch Type Manager") @classmethod def draw_basic_bim_tool_interface(cls): diff --git a/src/blenderbim/blenderbim/bim/module/patch/prop.py b/src/blenderbim/blenderbim/bim/module/patch/prop.py index 1fd91dc973..e965e63e15 100644 --- a/src/blenderbim/blenderbim/bim/module/patch/prop.py +++ b/src/blenderbim/blenderbim/bim/module/patch/prop.py @@ -18,6 +18,7 @@ import bpy import importlib +import importlib.util from pathlib import Path import ifcpatch from blenderbim.bim.prop import StrProperty, Attribute diff --git a/src/blenderbim/blenderbim/bim/module/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py index 7dc03dceaa..ec1008cd33 100644 --- a/src/blenderbim/blenderbim/bim/module/project/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py @@ -21,9 +21,12 @@ from . import ui, prop, operator, workspace, gizmo, decorator classes = ( operator.AppendEntireLibrary, + operator.AppendInspectedLinkedElement, operator.AppendLibraryElement, operator.AppendLibraryElementByQuery, operator.AssignLibraryDeclaration, + operator.BIM_OT_load_clipping_planes, + operator.BIM_OT_save_clipping_planes, operator.ChangeLibraryElement, operator.CreateClippingPlane, operator.CreateProject, diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index 938ba91598..7449f8bcad 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -28,6 +28,7 @@ import ifcopenshell.api import ifcopenshell.util.selector import ifcopenshell.util.geolocation import ifcopenshell.util.representation +import ifcopenshell.util.element import blenderbim.bim.handler import blenderbim.bim.schema import blenderbim.tool as tool @@ -38,6 +39,8 @@ from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ui import IFCFileSelector from blenderbim.bim import import_ifc from blenderbim.bim import export_ifc +from collections import defaultdict +import json from math import radians from pathlib import Path from mathutils import Vector, Matrix @@ -112,6 +115,10 @@ class CreateProject(bpy.types.Operator): if tool.Blender.is_default_scene(): for obj in bpy.data.objects: bpy.data.objects.remove(obj) + for mesh in bpy.data.meshes: + bpy.data.meshes.remove(mesh) + for mat in bpy.data.materials: + bpy.data.materials.remove(mat) core.create_project(tool.Ifc, tool.Project, schema=props.export_schema, template=template) tool.Blender.register_toolbar() @@ -185,7 +192,9 @@ class RefreshLibrary(bpy.types.Operator): self.props.active_library_element = "" types = IfcStore.library_file.wrapped_data.types_with_super() - for importable_type in sorted(["IfcTypeProduct", "IfcMaterial", "IfcCostSchedule", "IfcProfileDef"]): + + element_classes = ["IfcTypeProduct", "IfcMaterial", "IfcCostSchedule", "IfcProfileDef"] + for importable_type in sorted(element_classes): if importable_type in types: new = self.props.library_elements.add() new.name = importable_type @@ -204,11 +213,15 @@ class ChangeLibraryElement(bpy.types.Operator): self.library_file = IfcStore.library_file ifc_classes = set() self.props.active_library_element = self.element_name + crumb = self.props.library_breadcrumb.add() crumb.name = self.element_name + elements = self.library_file.by_type(self.element_name) [ifc_classes.add(e.is_a()) for e in elements] + self.props.library_elements.clear() + if len(ifc_classes) == 1 and list(ifc_classes)[0] == self.element_name: for name, ifc_definition_id in sorted([(self.get_name(e), e.id()) for e in elements]): self.add_library_asset(name, ifc_definition_id) @@ -429,7 +442,13 @@ class AppendLibraryElement(bpy.types.Operator): if element.is_a("IfcTypeProduct"): self.import_type_from_ifc(element, context) elif element.is_a("IfcProduct"): + # NOTE: not used as UI doesn't allow appending non-types self.import_product_from_ifc(element, context) + element_type = ifcopenshell.util.element.get_type(element) + obj = tool.Ifc.get_object(element_type) + if obj is None: + self.import_type_from_ifc(element_type, context) + elif element.is_a("IfcMaterial"): self.import_material_from_ifc(element, context) try: @@ -440,7 +459,7 @@ class AppendLibraryElement(bpy.types.Operator): blenderbim.bim.handler.refresh_ui_data() return {"FINISHED"} - def import_material_from_ifc(self, element, context): + def import_material_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None: self.file = IfcStore.get_file() logger = logging.getLogger("ImportIFC") ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger) @@ -449,7 +468,7 @@ class AppendLibraryElement(bpy.types.Operator): blender_material = ifc_importer.create_material(element) self.import_material_styles(blender_material, element, ifc_importer) - def import_product_from_ifc(self, element, context): + def import_product_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None: self.file = IfcStore.get_file() logger = logging.getLogger("ImportIFC") ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger) @@ -462,7 +481,7 @@ class AppendLibraryElement(bpy.types.Operator): ifc_importer.create_generic_elements({element}) ifc_importer.place_objects_in_collections() - def import_type_from_ifc(self, element, context): + def import_type_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None: self.file = IfcStore.get_file() logger = logging.getLogger("ImportIFC") ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger) @@ -487,14 +506,14 @@ class AppendLibraryElement(bpy.types.Operator): ifc_importer.create_element_type(element) ifc_importer.place_objects_in_collections() - def import_materials(self, element, ifc_importer): + def import_materials(self, element: ifcopenshell.entity_instance, ifc_importer: import_ifc.IfcImporter) -> None: for material in ifcopenshell.util.element.get_materials(element): if IfcStore.get_element(material.id()): continue blender_material = ifc_importer.create_material(material) self.import_material_styles(blender_material, material, ifc_importer) - def import_styles(self, element, ifc_importer): + def import_styles(self, element: ifcopenshell.entity_instance, ifc_importer: import_ifc.IfcImporter) -> None: if element.is_a("IfcTypeProduct"): representations = element.RepresentationMaps or [] elif element.is_a("IfcProduct"): @@ -507,7 +526,12 @@ class AppendLibraryElement(bpy.types.Operator): if element2.is_a("IfcSurfaceStyle") and not IfcStore.get_element(element2.id()): ifc_importer.create_style(element2) - def import_material_styles(self, blender_material, material, ifc_importer): + def import_material_styles( + self, + blender_material: bpy.types.Material, + material: ifcopenshell.entity_instance, + ifc_importer: import_ifc.IfcImporter, + ) -> None: if not material.HasRepresentation: return for element in self.file.traverse(material.HasRepresentation[0]): @@ -838,7 +862,10 @@ class LinkIfc(bpy.types.Operator): continue new = context.scene.BIMProjectProperties.links.add() if self.use_relative_path: - filepath = os.path.relpath(filepath, bpy.path.abspath("//")).replace("\\", "/") + try: + filepath = os.path.relpath(filepath, bpy.path.abspath("//")).replace("\\", "/") + except: + pass # Perhaps on another drive or something new.name = filepath bpy.ops.bim.load_link(filepath=filepath, false_origin=self.false_origin) print(f"Finished linking {len(files)} IFCs", time.time() - start) @@ -876,15 +903,30 @@ class UnloadLink(bpy.types.Operator): self.filepath = self.filepath.replace("\\", "/") filepath = self.filepath if not os.path.isabs(filepath): - filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), filepath)).replace("\\", "/") - for collection in context.scene.collection.children: - if collection.library and collection.library.filepath.replace("\\", "/") == filepath: - context.scene.collection.children.unlink(collection) - for scene in bpy.data.scenes: - if scene.library and scene.library.filepath.replace("\\", "/") == filepath: + filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), filepath)) + filepath = Path(filepath) + if filepath.suffix.lower() == ".ifc": + filepath = filepath.with_suffix(".ifc.cache.blend") + + for collection in context.scene.collection.children[:]: + if collection.library and Path(collection.library.filepath) == filepath: + bpy.data.collections.remove(collection) + + for scene in bpy.data.scenes[:]: + if scene.library and Path(scene.library.filepath) == filepath: bpy.data.scenes.remove(scene) - link = context.scene.BIMProjectProperties.links.get(self.filepath) - link.is_loaded = False + + links = context.scene.BIMProjectProperties.links + links.get(self.filepath).is_loaded = False + + if not any([l.is_loaded for l in links]): + ProjectDecorator.uninstall() + # we make sure we don't draw queried object from the file that was just unlinked + elif queried_obj := context.scene.BIMProjectProperties.queried_obj: + queried_filepath = Path(queried_obj["ifc_filepath"]) + if queried_filepath == filepath: + ProjectDecorator.uninstall() + return {"FINISHED"} @@ -975,24 +1017,24 @@ class ToggleLinkSelectability(bpy.types.Operator): bl_label = "Toggle Link Selectability" bl_options = {"REGISTER", "UNDO"} bl_description = "Toggle selectability" - link: bpy.props.StringProperty() + link: bpy.props.StringProperty(name="Linked IFC Filepath") def execute(self, context): props = context.scene.BIMProjectProperties link = props.links.get(self.link) - self.filepath = self.link - if not os.path.isabs(self.filepath): - self.filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.filepath)).replace("\\", "/") + if not os.path.isabs(self.link): + self.link = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.link)) + self.library_filepath = Path(self.link).with_suffix(".ifc.cache.blend") for collection in self.get_linked_collections(): collection.hide_select = not collection.hide_select link.is_selectable = not collection.hide_select return {"FINISHED"} - def get_linked_collections(self): + def get_linked_collections(self) -> list[bpy.types.Collection]: return [ c for c in bpy.data.collections - if "IfcProject" in c.name and c.library and c.library.filepath.replace("\\", "/") == self.filepath + if "IfcProject" in c.name and c.library and Path(c.library.filepath) == self.library_filepath ] @@ -1001,15 +1043,15 @@ class ToggleLinkVisibility(bpy.types.Operator): bl_label = "Toggle Link Visibility" bl_options = {"REGISTER", "UNDO"} bl_description = "Toggle visibility between SOLID and WIREFRAME" - link: bpy.props.StringProperty() - mode: bpy.props.StringProperty() + link: bpy.props.StringProperty(name="Linked IFC Filepath") + mode: bpy.props.EnumProperty(name="Visibility Mode", items=((i, i, "") for i in ("WIREFRAME", "VISIBLE"))) def execute(self, context): props = context.scene.BIMProjectProperties link = props.links.get(self.link) - self.filepath = self.link - if not os.path.isabs(self.filepath): - self.filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.filepath)).replace("\\", "/") + if not os.path.isabs(self.link): + self.link = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.link)) + self.library_filepath = Path(self.link).with_suffix(".ifc.cache.blend") if self.mode == "WIREFRAME": self.toggle_wireframe(link) elif self.mode == "VISIBLE": @@ -1030,25 +1072,17 @@ class ToggleLinkVisibility(bpy.types.Operator): def toggle_visibility(self, link): linked_collections = self.get_linked_collections() - queue = [bpy.context.view_layer.layer_collection] - layer_collection = None - while queue: - layer = queue.pop() - if layer.collection in linked_collections: - layer_collection = layer - break - queue.extend(list(layer.children)) - - if layer_collection: + layer_collections = tool.Blender.get_layer_collections_mapping(linked_collections) + for layer_collection in layer_collections.values(): layer_collection.exclude = not layer_collection.exclude link.is_hidden = layer_collection.exclude - def get_linked_collections(self): + def get_linked_collections(self) -> list[bpy.types.Collection]: return [ c for c in bpy.data.collections - if "IfcProject" in c.name and c.library and c.library.filepath.replace("\\", "/") == self.filepath + if "IfcProject" in c.name and c.library and Path(c.library.filepath) == self.library_filepath ] @@ -1262,6 +1296,8 @@ class LoadLinkedProject(bpy.types.Operator): if len(shape.geometry.faces) > 1000 and self.is_local(shape): # 333 tris self.process_occurrence(shape) if not iterator.next(): + if not chunked_verts: + break mats = np.concatenate(chunked_materials) midx = np.concatenate(chunked_material_ids) mats, mapping = np.unique(mats, axis=0, return_inverse=True) @@ -1297,6 +1333,9 @@ class LoadLinkedProject(bpy.types.Operator): ms = np.vstack([default_mat, np.frombuffer(shape.geometry.colors_buffer).reshape((-1, 4))]) mi = np.frombuffer(shape.geometry.material_ids_buffer, dtype=np.int32) + for geom_material_idx, geom_material in enumerate(shape.geometry.materials): + if not geom_material.has_diffuse: + ms[geom_material_idx + 1] = (0.8, 0.8, 0.8, 1) chunked_materials.append(ms) chunked_material_ids.append(mi + material_offset + 1) material_offset += len(ms) @@ -1417,7 +1456,10 @@ class LoadLinkedProject(bpy.types.Operator): alpha = 1.0 if material.has_transparency and material.transparency > 0: alpha = 1.0 - material.transparency - diffuse = material.diffuse + (alpha,) + if material.has_diffuse: + diffuse = material.diffuse + (alpha,) + else: + diffuse = (0.8, 0.8, 0.8, 1) # Blender's default material material_name = f"{diffuse[0]}-{diffuse[1]}-{diffuse[2]}-{diffuse[3]}" blender_mat = self.blender_mats.get(material_name, None) if not blender_mat: @@ -1495,6 +1537,7 @@ class LoadLinkedProject(bpy.types.Operator): obj["guids"] = list(guids) obj["guid_ids"] = list(guid_ids) obj["db"] = self.db_filepath + obj["ifc_filepath"] = self.filepath self.collection.objects.link(obj) @@ -1626,6 +1669,42 @@ class QueryLinkedElement(bpy.types.Operator): return self.execute(context) +class AppendInspectedLinkedElement(AppendLibraryElement): + bl_idname = "bim.append_inspected_linked_element" + bl_label = "Append Inspected Linked Element" + bl_description = "Append inspected linked element" + bl_options = {"REGISTER"} + + def _execute(self, context): + from blenderbim.bim.module.project.data import LinksData + + if not LinksData.linked_data: + self.report({"INFO"}, "No linked element found.") + return {"CANCELLED"} + + guid = LinksData.linked_data["attributes"].get("GlobalId") + if guid is None: + self.report({"INFO"}, "Cannot find Global Id for element.") + return {"CANCELLED"} + + queried_obj = context.scene.BIMProjectProperties.queried_obj + ifc_file = ifcopenshell.open(queried_obj["ifc_filepath"]) + element_to_append = ifc_file.by_guid(guid) + element = ifcopenshell.api.run( + "project.append_asset", + tool.Ifc.get(), + library=ifc_file, + element=element_to_append, + ) + self.import_product_from_ifc(element, context) + element_type = ifcopenshell.util.element.get_type(element) + obj = tool.Ifc.get_object(element_type) + if obj is None: + self.import_type_from_ifc(element_type, context) + + return {"FINISHED"} + + class EnableCulling(bpy.types.Operator): bl_idname = "bim.enable_culling" bl_label = "Enable Culling" @@ -1843,32 +1922,32 @@ class CreateClippingPlane(bpy.types.Operator): bl_label = "Create Clipping Plane" bl_options = {"REGISTER", "UNDO"} - @classmethod - def poll(cls, context): - return context.area.type == "VIEW_3D" - def execute(self, context): from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d # Clean up deleted planes - if len(bpy.context.scene.BIMProjectProperties.clipping_planes) > 5: + if len(context.scene.BIMProjectProperties.clipping_planes) > 5: self.report({"INFO"}, "Maximum of six clipping planes allowed.") return {"FINISHED"} - for area in bpy.context.screen.areas: + for area in context.screen.areas: if area.type == "VIEW_3D": area.tag_redraw() region = context.region rv3d = context.region_data - coord = (self.mouse_x, self.mouse_y) - origin = region_2d_to_origin_3d(region, rv3d, coord) - direction = region_2d_to_vector_3d(region, rv3d, coord) - hit, location, normal, face_index, obj, matrix = self.ray_cast(context, origin, direction) - if not hit: - self.report({"INFO"}, "No object found.") - return {"FINISHED"} + if rv3d: # Called from a 3D viewport + coord = (self.mouse_x, self.mouse_y) + origin = region_2d_to_origin_3d(region, rv3d, coord) + direction = region_2d_to_vector_3d(region, rv3d, coord) + hit, location, normal, face_index, obj, matrix = self.ray_cast(context, origin, direction) + if not hit: + self.report({"INFO"}, "No object found.") + return {"FINISHED"} + else: # Not Called from a 3D viewport + location = (0, 0, 1) + normal = (0, 0, 1) vertices = [(-0.5, -0.5, 0), (0.5, -0.5, 0), (0.5, 0.5, 0), (-0.5, 0.5, 0)] @@ -1879,20 +1958,20 @@ class CreateClippingPlane(bpy.types.Operator): mesh.update() plane_obj = bpy.data.objects.new("ClippingPlane", mesh) - bpy.context.collection.objects.link(plane_obj) + context.collection.objects.link(plane_obj) z_axis = Vector((0, 0, 1)) rotation_matrix = z_axis.rotation_difference(normal).to_matrix().to_4x4() plane_obj.matrix_world = rotation_matrix plane_obj.matrix_world.translation = location - bpy.context.scene.cursor.location = location + context.scene.cursor.location = location - new = bpy.context.scene.BIMProjectProperties.clipping_planes.add() + new = context.scene.BIMProjectProperties.clipping_planes.add() new.obj = plane_obj tool.Blender.set_active_object(plane_obj) - ClippingPlaneDecorator.install(bpy.context) + ClippingPlaneDecorator.install(context) bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT") return {"FINISHED"} @@ -1914,11 +1993,69 @@ class FlipClippingPlane(bpy.types.Operator): @classmethod def poll(cls, context): - return context.area.type == "VIEW_3D" + return context.active_object def execute(self, context): - obj = bpy.context.active_object - if obj in [cp.obj for cp in bpy.context.scene.BIMProjectProperties.clipping_planes]: + obj = context.active_object + if obj in context.scene.BIMProjectProperties.clipping_planes_objs: obj.rotation_euler[0] += radians(180) - bpy.context.view_layer.update() + context.view_layer.update() + return {"FINISHED"} + + +CLIPPING_PLANES_FILE_NAME = "ClippingPlanes.json" # TODO un-hardcode := + + +class BIM_OT_save_clipping_planes(bpy.types.Operator): + bl_idname = "bim.save_clipping_planes" + bl_label = "Save Clipping Planes" + bl_description = "Save Clipping Planes to Disk" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + if IfcStore.path: + return context.scene.BIMProjectProperties.clipping_planes + cls.poll_message_set("Please Save The IFC File") + + def execute(self, context): + clipping_planes_to_serialize = defaultdict(dict) + clipping_planes = context.scene.BIMProjectProperties.clipping_planes + for clipping_plane in clipping_planes: + obj = clipping_plane.obj + name = obj.name + clipping_planes_to_serialize[name]["location"] = obj.location[0:3] + clipping_planes_to_serialize[name]["rotation"] = obj.rotation_euler[0:3] + with open(Path(IfcStore.path).with_name(CLIPPING_PLANES_FILE_NAME), "w") as file: + json.dump(clipping_planes_to_serialize, file, indent=4) + return {"FINISHED"} + + +class BIM_OT_load_clipping_planes(bpy.types.Operator): + bl_idname = "bim.load_clipping_planes" + bl_label = "Load Clipping Planes" + bl_description = "Load Clipping Planes from Disk" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + if filepath := IfcStore.path: + if Path(filepath).with_name(CLIPPING_PLANES_FILE_NAME).exists(): + return True + else: + cls.poll_message_set(f"No Clipping Planes File in Folder {filepath}") + else: + cls.poll_message_set("Please Save The IFC File") + + def execute(self, context): + bpy.data.batch_remove(context.scene.BIMProjectProperties.clipping_planes_objs) + context.scene.BIMProjectProperties.clipping_planes.clear() + with open(Path(IfcStore.path).with_name(CLIPPING_PLANES_FILE_NAME), "r") as file: + clipping_planes_dict = json.load(file) + for name, values in clipping_planes_dict.items(): + bpy.ops.bim.create_clipping_plane() + obj = context.scene.BIMProjectProperties.clipping_planes_objs[-1] + obj.name = name + obj.location = values["location"] + obj.rotation_euler = values["rotation"] return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/project/prop.py b/src/blenderbim/blenderbim/bim/module/project/prop.py index e988a89f5b..6e34461be4 100644 --- a/src/blenderbim/blenderbim/bim/module/project/prop.py +++ b/src/blenderbim/blenderbim/bim/module/project/prop.py @@ -187,6 +187,11 @@ class BIMProjectProperties(PropertyGroup): use_relative_project_path: BoolProperty(name="Use Relative Project Path", default=False) queried_obj: bpy.props.PointerProperty(type=bpy.types.Object) clipping_planes: bpy.props.CollectionProperty(type=ObjProperty) + clipping_planes_active: bpy.props.IntProperty(min=0, default=0, max=5) + + @property + def clipping_planes_objs(self): + return list({cp.obj for cp in self.clipping_planes if cp.obj}) def get_library_element_index(self, lib_element): return next((i for i in range(len(self.library_elements)) if self.library_elements[i] == lib_element)) diff --git a/src/blenderbim/blenderbim/bim/module/project/ui.py b/src/blenderbim/blenderbim/bim/module/project/ui.py index 1a73ef061b..e961bf7499 100644 --- a/src/blenderbim/blenderbim/bim/module/project/ui.py +++ b/src/blenderbim/blenderbim/bim/module/project/ui.py @@ -267,10 +267,9 @@ class BIM_PT_project_library(Panel): ) row.operator("bim.append_library_element_by_query", text="", icon="APPEND_BLEND") row.operator("bim.save_library_file", text="", icon="EXPORT") + self.draw_library_ul() else: row.label(text="No Library Loaded", icon="ASSET_MANAGER") - if IfcStore.library_file: - self.draw_library_ul() def draw_library_ul(self): if not self.props.library_elements: @@ -319,11 +318,16 @@ class BIM_PT_links(Panel): row = self.layout.row(align=True) row.label(text="Object Culling Enabled", icon="MOD_TRIANGULATE") - if not LinksData.linked_data: - row = self.layout.row(align=True) - row.label(text="No Object Selected", icon="QUESTION") + if not LinksData.linked_data or not self.props.links: + if self.props.links: + row = self.layout.row(align=True) + row.label(text="No Object Selected", icon="QUESTION") return + row = self.layout.row(align=True) + row.label(text="") + row.operator("bim.append_inspected_linked_element", icon="APPEND_BLEND", text="") + for name, value in LinksData.linked_data["attributes"].items(): row = self.layout.row(align=True) row.label(text=name) diff --git a/src/blenderbim/blenderbim/bim/module/pset/data.py b/src/blenderbim/blenderbim/bim/module/pset/data.py index 739b1d7c97..0c3093f3a5 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/data.py +++ b/src/blenderbim/blenderbim/bim/module/pset/data.py @@ -69,6 +69,7 @@ class ObjectPsetsData(Data): @classmethod def load(cls): cls.data = { + "is_occurrence": cls.is_occurrence(), "psets": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object), psets_only=True), "inherited_psets": cls.inherited_psets(), "pset_name": cls.pset_name(), @@ -76,14 +77,18 @@ class ObjectPsetsData(Data): } cls.is_loaded = True + @classmethod + def is_occurrence(cls): + return not tool.Ifc.get_entity(bpy.context.active_object).is_a("IfcTypeObject") + @classmethod def inherited_psets(cls): element = tool.Ifc.get_entity(bpy.context.active_object) if element.is_a("IfcTypeObject"): - return + return [] element_type = ifcopenshell.util.element.get_type(element) if element_type: - return cls.psetqtos(element_type) + return cls.psetqtos(element_type, psets_only=True) @classmethod def pset_name(cls): @@ -125,9 +130,26 @@ class ObjectQtosData(Data): @classmethod def load(cls): - cls.data = {"qtos": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object), qtos_only=True)} + cls.data = { + "is_occurrence": cls.is_occurrence(), + "qtos": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object), qtos_only=True), + "inherited_qsets": cls.inherited_qsets(), + } cls.is_loaded = True + @classmethod + def is_occurrence(cls): + return not tool.Ifc.get_entity(bpy.context.active_object).is_a("IfcTypeObject") + + @classmethod + def inherited_qsets(cls): + element = tool.Ifc.get_entity(bpy.context.active_object) + if element.is_a("IfcTypeObject"): + return [] + element_type = ifcopenshell.util.element.get_type(element) + if element_type: + return cls.psetqtos(element_type, qtos_only=True) + class MaterialPsetsData(Data): data = {} diff --git a/src/blenderbim/blenderbim/bim/module/pset/operator.py b/src/blenderbim/blenderbim/bim/module/pset/operator.py index 66d0701697..c699a63d52 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/operator.py +++ b/src/blenderbim/blenderbim/bim/module/pset/operator.py @@ -85,7 +85,6 @@ class EnablePsetEditing(bpy.types.Operator): def execute(self, context): self.props = get_pset_props(context, self.obj, self.obj_type) self.props.properties.clear() - ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type) if self.pset_id: pset = tool.Ifc.get().by_id(self.pset_id) @@ -250,7 +249,7 @@ class DisablePsetEditing(bpy.types.Operator, Operator): props = get_pset_props(context, self.obj, self.obj_type) if props.active_pset_id: pset = tool.Ifc.get().by_id(props.active_pset_id) - ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type) + ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context) if tool.Pset.is_pset_empty(pset): ifcopenshell.api.run( "pset.remove_pset", tool.Ifc.get(), product=tool.Ifc.get().by_id(ifc_definition_id), pset=pset @@ -272,7 +271,7 @@ class EditPset(bpy.types.Operator, Operator): def _execute(self, context): self.file = IfcStore.get_file() props = get_pset_props(context, self.obj, self.obj_type) - ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type) + ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context) element = tool.Ifc.get().by_id(ifc_definition_id) properties = {} @@ -343,7 +342,7 @@ class RemovePset(bpy.types.Operator, Operator): pset_name = tool.Ifc.get().by_id(self.pset_id).Name for obj in objects: props = get_pset_props(context, obj, self.obj_type) - ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type) + ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context) element = tool.Ifc.get().by_id(ifc_definition_id) pset = ifcopenshell.util.element.get_psets(element, should_inherit=False).get(pset_name, None) if pset: @@ -374,7 +373,7 @@ class AddQto(bpy.types.Operator, Operator): def _execute(self, context): self.file = IfcStore.get_file() props = get_pset_props(context, self.obj, self.obj_type) - ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type) + ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context) element = tool.Ifc.get().by_id(ifc_definition_id) bpy.ops.bim.enable_pset_editing( pset_id=0, pset_name=props.qto_name, pset_type="QTO", obj=self.obj, obj_type=self.obj_type diff --git a/src/blenderbim/blenderbim/bim/module/pset/qto_calculator.py b/src/blenderbim/blenderbim/bim/module/pset/qto_calculator.py index 2452708fbe..713f549d81 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/qto_calculator.py +++ b/src/blenderbim/blenderbim/bim/module/pset/qto_calculator.py @@ -25,8 +25,16 @@ from shapely.geometry import Polygon from shapely.ops import unary_union import blenderbim.tool as tool import ifcopenshell +import ifcopenshell.geom +import ifcopenshell.util.element from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper import blenderbim.bim +from typing import Literal, Union, Optional + + +AxisType = Literal["x", "y", "z"] +VectorTuple = tuple[float, float, float] +QuanityTypes = Literal["Q_LENGTH", "Q_AREA", "Q_VOLUME"] class QtoCalculator: @@ -45,7 +53,7 @@ class QtoCalculator: else: self.mapping_dict[key][item] = None - def calculate_quantity(self, qto_name, quantity_name, obj): + def calculate_quantity(self, qto_name: str, quantity_name: str, obj: bpy.types.Object) -> float: """calculates the value of the quantity in the project units""" string = "self.mapping_dict[qto_name][quantity_name](obj" if isinstance(mapper[qto_name][quantity_name], dict): @@ -54,11 +62,13 @@ class QtoCalculator: args = "" string += args string += ")" - value = eval(string) + value: float = eval(string) return tool.Qto.convert_to_project_units(value, qto_name, quantity_name) or value - def guess_quantity(self, prop_name, alternative_prop_names, obj): + def guess_quantity( + self, prop_name: str, alternative_prop_names: list[str], obj: bpy.types.Object + ) -> Union[float, None]: """guess the value of the quantity by name, returns the value in the project units""" prop_name = prop_name.lower() alternative_prop_names = [p.lower() for p in alternative_prop_names] @@ -89,7 +99,7 @@ class QtoCalculator: if value is None: return - unit_type_keywords = { + unit_type_keywords: dict[str, QuanityTypes] = { "length": "Q_LENGTH", "width": "Q_LENGTH", "height": "Q_LENGTH", @@ -102,10 +112,10 @@ class QtoCalculator: unit_type = next(unit_type_keywords[k] for k in unit_type_keywords if k in prop_name) return tool.Qto.convert_to_project_units(value, quantity_type=unit_type) or value - def get_units(self, o, vg_index): + def get_units(self, o: bpy.types.Object, vg_index: int) -> int: return len([v for v in o.data.vertices if vg_index in [g.group for g in v.groups]]) - def get_linear_length(self, o): + def get_linear_length(self, o: bpy.types.Object) -> float: """_summary_: Returns the length of the longest edge of the object bounding box :param blender-object o: Blender Object @@ -116,7 +126,7 @@ class QtoCalculator: z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length return max(x, y, z) - def get_length(self, o, vg_index=None, main_axis: str = ""): + def get_length(self, o: bpy.types.Object, vg_index: Optional[int] = None, main_axis: str = "") -> float: if vg_index is None: x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length @@ -141,23 +151,23 @@ class QtoCalculator: length += self.get_edge_distance(o, e) return length - def get_stair_length(self, obj): + def get_stair_length(self, obj: bpy.types.Object) -> float: length = self.get_length(obj) height = self.get_height(obj) stair_length = math.sqrt(pow(length, 2) + pow(height, 2)) return stair_length - def get_net_stair_area(self, obj): + def get_net_stair_area(self, obj: bpy.types.Object) -> float: OBB_obj = self.get_OBB_object(obj) OBB_net_footprint_area = self.get_net_footprint_area(OBB_obj) return OBB_net_footprint_area - def get_gross_stair_area(self, obj): + def get_gross_stair_area(self, obj: bpy.types.Object) -> float: OBB_obj = self.get_OBB_object(obj) OBB_gross_footprint_area = self.get_gross_footprint_area(OBB_obj) return OBB_gross_footprint_area - def get_parametric_axis(self, obj): + def get_parametric_axis(self, obj: bpy.types.Object) -> Literal["AXIS2", "AXIS3", None]: relating_type = ifcopenshell.util.element.get_type(tool.Ifc.get_entity(obj)) if relating_type: parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric") @@ -172,7 +182,7 @@ class QtoCalculator: return None return None - def get_covering_gross_area(self, obj): + def get_covering_gross_area(self, obj: bpy.types.Object) -> float: get_parametric_axis = self.get_parametric_axis(obj) if not get_parametric_axis: return self.get_gross_footprint_area(obj) @@ -181,7 +191,7 @@ class QtoCalculator: elif get_parametric_axis == "AXIS3": return self.get_gross_footprint_area(obj) - def get_covering_net_area(self, obj): + def get_covering_net_area(self, obj: bpy.types.Object) -> float: get_parametric_axis = self.get_parametric_axis(obj) if not get_parametric_axis: return self.get_net_footprint_area(obj) @@ -190,7 +200,7 @@ class QtoCalculator: elif get_parametric_axis == "AXIS3": return self.get_net_footprint_area(obj) - def get_covering_width(self, obj): + def get_covering_width(self, obj: bpy.types.Object) -> float: get_parametric_axis = self.get_parametric_axis(obj) if not get_parametric_axis: return self.get_height(obj) @@ -199,7 +209,7 @@ class QtoCalculator: elif get_parametric_axis == "AXIS3": return self.get_height(obj) - def get_width(self, o): + def get_width(self, o: bpy.types.Object) -> float: """_summary_: Returns the width of the object bounding box :param blender-object o: blender object @@ -209,7 +219,7 @@ class QtoCalculator: y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length return min(x, y) - def get_height(self, o): + def get_height(self, o: bpy.types.Object) -> float: """_summary_: Returns the height of the object bounding box :param blender-object o: blender object @@ -217,64 +227,77 @@ class QtoCalculator: """ return (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length - def get_opening_height(self, obj): + def get_opening_height(self, obj: bpy.types.Object) -> float: if self.is_opening_horizontal(obj): return self.get_width(obj) else: return self.get_height(obj) - def get_opening_depth(self, obj): + def get_opening_depth(self, obj: bpy.types.Object) -> float: if self.is_opening_horizontal(obj): return self.get_height(obj) else: return self.get_width(obj) - def get_opening_mapping_area(self, obj): + def get_opening_mapping_area(self, obj: bpy.types.Object) -> float: if self.is_opening_horizontal(obj): return self.get_net_footprint_area(obj) else: return self.get_net_side_area(obj) - def get_finish_ceiling_height(self, obj): - space_height = self.get_height(obj) + def get_finish_ceiling_height(self, obj: bpy.types.Object) -> float: floor_height = self.get_finish_floor_height(obj) ceiling_height = self.get_ceiling_height(obj) - finish_ceiling_height = space_height - floor_height - ceiling_height + finish_ceiling_height = ceiling_height - floor_height return finish_ceiling_height - def get_finish_floor_height(self, obj): + def get_max_global_z(self, obj: bpy.types.Object) -> float: + z_values = [(obj.matrix_world @ Vector(co))[2] for co in obj.bound_box] + return max(z_values) + + def get_min_global_z(self, obj: bpy.types.Object) -> float: + z_values = [(obj.matrix_world @ Vector(co))[2] for co in obj.bound_box] + return min(z_values) + + def get_finish_floor_height(self, obj: bpy.types.Object) -> float: + space_min_z_value = self.get_min_global_z(obj) + space_max_z_value = self.get_max_global_z(obj) + element = tool.Ifc.get_entity(obj) decompositions = ifcopenshell.util.element.get_decomposition(element) - finish_floor_height = 0 + flooring_max_z_value = space_min_z_value for decomposition in decompositions: if ( - decomposition.get_info()["PredefinedType"] == "FLOORING" - and decomposition.get_info()["type"] == "IfcCovering" + decomposition.is_a() == "IfcCovering" + and ifcopenshell.util.element.get_predefined_type(decomposition) == "FLOORING" ): - floor_obj = tool.Ifc.get_object(decomposition) - new_finish_floor_height = self.get_height(floor_obj) - if new_finish_floor_height > finish_floor_height: - finish_floor_height = new_finish_floor_height + flooring_obj = tool.Ifc.get_object(decomposition) + flooring_z_value = self.get_max_global_z(flooring_obj) + if flooring_z_value > space_min_z_value: + flooring_max_z_value = flooring_z_value - return finish_floor_height + return flooring_max_z_value - space_min_z_value + + def get_ceiling_height(self, obj: bpy.types.Object) -> float: + space_min_z_value = self.get_min_global_z(obj) + space_max_z_value = self.get_max_global_z(obj) - def get_ceiling_height(self, obj): element = tool.Ifc.get_entity(obj) decompositions = ifcopenshell.util.element.get_decomposition(element) - finish_ceiling_height = 0 + ceiling_min_z_value = space_max_z_value for decomposition in decompositions: if ( - decomposition.get_info()["PredefinedType"] == "CEILING" - and decomposition.get_info()["type"] == "IfcCovering" + decomposition.is_a() == "IfcCovering" + and ifcopenshell.util.element.get_predefined_type(decomposition) == "CEILING" ): ceiling_obj = tool.Ifc.get_object(decomposition) - new_finish_ceiling_height = self.get_height(ceiling_obj) - if new_finish_ceiling_height > finish_ceiling_height: - finish_ceiling_height = new_finish_ceiling_height + ceiling_z_value = self.get_min_global_z(ceiling_obj) + if ceiling_z_value < space_max_z_value: + ceiling_min_z_value = ceiling_z_value - return finish_ceiling_height + return ceiling_min_z_value - space_min_z_value - def get_net_perimeter(self, o): + def get_net_perimeter(self, o: bpy.types.Object) -> float: parsed_edges = [] shared_edges = [] perimeter = 0 @@ -289,7 +312,7 @@ class QtoCalculator: perimeter -= self.get_edge_key_distance(o, edge_key) return perimeter - def get_gross_perimeter(self, o): + def get_gross_perimeter(self, o: bpy.types.Object) -> float: element = tool.Ifc.get_entity(o) mesh = self.get_gross_element_mesh(element) gross_obj = bpy.data.objects.new("GrossObj", mesh) @@ -297,15 +320,15 @@ class QtoCalculator: self.delete_obj(gross_obj) return gross_perimeter - def get_space_net_perimeter(self, obj): + def get_space_net_perimeter(self, obj: bpy.types.Object) -> float: pass - def get_rectangular_perimeter(self, obj): + def get_rectangular_perimeter(self, obj: bpy.types.Object) -> float: length = self.get_length(obj, main_axis="x") height = self.get_height(obj) return (length + height) * 2 - def get_lowest_polygons(self, o): + def get_lowest_polygons(self, o: bpy.types.Object) -> list[bpy.types.MeshPolygon]: lowest_polygons = [] lowest_z = None for polygon in o.data.polygons: @@ -321,7 +344,7 @@ class QtoCalculator: lowest_z = z return lowest_polygons - def get_highest_polygons(self, o): + def get_highest_polygons(self, o: bpy.types.Object) -> list[bpy.types.MeshPolygon]: highest_polygons = [] highest_z = None for polygon in o.data.polygons: @@ -337,13 +360,13 @@ class QtoCalculator: highest_z = z return highest_polygons - def get_edge_key_distance(self, obj, edge_key): + def get_edge_key_distance(self, obj: bpy.types.Object, edge_key: tuple[int, int]) -> float: return (obj.data.vertices[edge_key[1]].co - obj.data.vertices[edge_key[0]].co).length - def get_edge_distance(self, obj, edge): + def get_edge_distance(self, obj: bpy.types.Object, edge: bpy.types.MeshEdge) -> float: return (obj.data.vertices[edge.vertices[1]].co - obj.data.vertices[edge.vertices[0]].co).length - def get_net_floor_area(self, obj): + def get_net_floor_area(self, obj: bpy.types.Object) -> float: decompositions = self.get_obj_decompositions(obj) if not decompositions: return self.get_gross_footprint_area(obj) @@ -359,7 +382,7 @@ class QtoCalculator: return total_net_floor_area - def get_gross_ceiling_area(self, obj): + def get_gross_ceiling_area(self, obj: bpy.types.Object) -> float: decompositions = self.get_obj_decompositions(obj) if not decompositions: return self.get_gross_top_area(obj) @@ -367,15 +390,15 @@ class QtoCalculator: total_gross_ceiling_area = 0 for decomposition in decompositions: - decomposition_type = decomposition.get_info()["type"] - decomposition_predefined_type = decomposition.get_info()["PredefinedType"] - if decomposition_type == "IfcCovering" and decomposition_predefined_type == "CEILING": + decomposition_class = decomposition.is_a() + decomposition_predefined_type = ifcopenshell.util.element.get_predefined_type(decomposition) + if decomposition_class == "IfcCovering" and decomposition_predefined_type == "CEILING": decomposition_obj = tool.Ifc.get_object(decomposition) total_gross_ceiling_area += self.get_gross_footprint_area(decomposition_obj) return total_gross_ceiling_area - def get_net_ceiling_area(self, obj): + def get_net_ceiling_area(self, obj: bpy.types.Object) -> float: decompositions = self.get_obj_decompositions(obj) if not decompositions: return self.get_net_top_area(obj) @@ -383,19 +406,19 @@ class QtoCalculator: total_net_ceiling_area = 0 for decomposition in decompositions: - decomposition_type = decomposition.get_info()["type"] - decomposition_predefined_type = decomposition.get_info()["PredefinedType"] - if decomposition_type == "IfcCovering" and decomposition_predefined_type == "CEILING": + decomposition_class = decomposition.is_a() + decomposition_predefined_type = ifcopenshell.util.element.get_predefined_type(decomposition) + if decomposition_class == "IfcCovering" and decomposition_predefined_type == "CEILING": decomposition_obj = tool.Ifc.get_object(decomposition) total_net_ceiling_area += self.get_net_footprint_area(decomposition_obj) - if decomposition_type == "IfcWall" or decomposition_type == "IfcColumn": + if decomposition_class == "IfcWall" or decomposition_class == "IfcColumn": decomposition_obj = tool.Ifc.get_object(decomposition) total_net_ceiling_area -= self.get_net_roofprint_area(decomposition_obj) return total_net_ceiling_area - def get_space_net_volume(self, obj): + def get_space_net_volume(self, obj: bpy.types.Object) -> float: decompositions = self.get_obj_decompositions(obj) if not decompositions: return self.get_gross_volume(obj) @@ -410,7 +433,7 @@ class QtoCalculator: return total_space_net_volume - def get_net_footprint_area(self, o): + def get_net_footprint_area(self, o: bpy.types.Object) -> float: """_summary_: Returns the area of the footprint of the object, excluding any holes :param blender-object o: blender object @@ -421,7 +444,7 @@ class QtoCalculator: area += polygon.area return area - def get_gross_footprint_area(self, o): + def get_gross_footprint_area(self, o: bpy.types.Object) -> float: """_summary_: Returns the area of the footprint of the object, without related opening and excluding any holes :param blender-object o: blender object @@ -437,7 +460,7 @@ class QtoCalculator: self.delete_mesh(mesh) return gross_footprint_area - def get_net_roofprint_area(self, o): + def get_net_roofprint_area(self, o: bpy.types.Object) -> float: # Is roofprint the right word? Couldn't think of anything better - vulevukusej """_summary_: Returns the area of the net roofprint of the object, excluding any holes @@ -449,7 +472,7 @@ class QtoCalculator: area += polygon.area return area - def get_side_area(self, o): + def get_side_area(self, o: bpy.types.Object) -> float: # There are a few dumb options for this, but this seems the dumbest # until I get more practical experience on what works best. x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length @@ -457,8 +480,7 @@ class QtoCalculator: z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length return max(x * z, y * z) - def get_cross_section_area(self, obj): - element = tool.Ifc.get_entity(obj) + def get_cross_section_area(self, obj: bpy.types.Object) -> float: representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) item = representation.Items[0] while True: @@ -474,7 +496,7 @@ class QtoCalculator: return area # TODO handle other types of sections, and then fall back to mesh parsing - def get_gross_surface_area(self, o, vg_index=None): + def get_gross_surface_area(self, o: bpy.types.Object, vg_index: Optional[int] = None) -> float: if vg_index is None: if not self.has_openings(o): return self.get_net_surface_area(o) @@ -492,29 +514,29 @@ class QtoCalculator: area += polygon.area return area - def get_net_surface_area(self, obj): + def get_net_surface_area(self, obj: bpy.types.Object) -> float: return self.get_mesh_area(obj.data) - def get_mesh_area(self, mesh): + def get_mesh_area(self, mesh: bpy.types.Mesh) -> float: area = 0 for polygon in mesh.polygons: area += polygon.area return area - def is_polygon_in_vg(self, polygon, vertices_in_vg): + def is_polygon_in_vg(self, polygon: bpy.types.MeshPolygon, vertices_in_vg: list[bpy.types.MeshVertex]) -> bool: for v in polygon.vertices: if v not in vertices_in_vg: return False return True - def get_net_volume(self, o): + def get_net_volume(self, o: bpy.types.Object) -> float: o_mesh = bmesh.new() o_mesh.from_mesh(o.data) volume = o_mesh.calc_volume() o_mesh.free() return volume - def get_gross_volume(self, o): + def get_gross_volume(self, o: bpy.types.Object) -> float: if not self.has_openings(o): return self.get_net_volume(o) @@ -529,16 +551,18 @@ class QtoCalculator: return gross_volume - def has_openings(self, obj): + def has_openings( + self, obj: bpy.types.Object + ) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]: element = tool.Ifc.get_entity(obj) return element and getattr(element, "HasOpenings", []) - def get_obj_decompositions(self, obj): + def get_obj_decompositions(self, obj: bpy.types.Object) -> list[ifcopenshell.entity_instance]: element = tool.Ifc.get_entity(obj) decompositions = ifcopenshell.util.element.get_decomposition(element) return decompositions - def get_gross_weight(self, obj): + def get_gross_weight(self, obj: bpy.types.Object) -> Union[float, None]: obj_mass_density = self.get_obj_mass_density(obj) if not obj_mass_density: return @@ -546,7 +570,7 @@ class QtoCalculator: gross_weight = obj_mass_density * gross_volume return gross_weight - def get_net_weight(self, obj): + def get_net_weight(self, obj: bpy.types.Object) -> Union[float, None]: obj_mass_density = self.get_obj_mass_density(obj) if not obj_mass_density: return @@ -554,7 +578,7 @@ class QtoCalculator: net_weight = obj_mass_density * net_volume return net_weight - def get_obj_mass_density(self, obj): + def get_obj_mass_density(self, obj: bpy.types.Object) -> Union[float, None]: entity = tool.Ifc.get_entity(obj) material = ifcopenshell.util.element.get_material(entity) if material is None: @@ -629,7 +653,7 @@ class QtoCalculator: # volume += v1.dot(v2.cross(v3)) / 6.0 # return volume - def get_opening_type(self, opening, obj): + def get_opening_type(self, opening: bpy.types.Object, obj: bpy.types.Object) -> Literal["OPENING", "RECESS"]: """_summary_: Returns the opening type - OPENING / RECESS :param blender-object opening: blender opening object @@ -649,14 +673,22 @@ class QtoCalculator: return "OPENING" if ray_intersections % 2 == 0 else "RECESS" def get_opening_area( - self, obj, angle_z1: int = 45, angle_z2: int = 135, min_area: int = 0, ignore_recesses: bool = False - ): + self, + obj: bpy.types.Object, + angle_z1: int = 45, + angle_z2: int = 135, + min_area: int = 0, + ignore_recesses: bool = False, + ) -> float: """_summary_: Returns the lateral area of the openings in the object. :param obj: blender object - :param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the opening area. Openings with a normal_vector lower than this value will be ignored, defaults to 45 - :param int angle_z2: Angle measured from the positive z-axis to the normal-vector of the opening area. Openings with a normal_vector greater than this value will be ignored,defaults to 135 - :param float min_area: Minimum opening area to consider. Values lower than this will be ignored, defaults to 0 + :param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the opening area. + Openings with a normal_vector lower than this value will be ignored, defaults to 45 + :param int angle_z2: Angle measured from the positive z-axis to the normal-vector of the opening area. + Openings with a normal_vector greater than this value will be ignored,defaults to 135 + :param float min_area: Minimum opening area to consider. Values lower than this will be ignored, + defaults to 0 :param bool ignore_recesses: Toggle whether recess areas should be considered, defaults to False :return float: Opening Area """ @@ -702,14 +734,14 @@ class QtoCalculator: def get_lateral_area( self, - obj, + obj: bpy.types.Object, subtract_openings: bool = True, exclude_end_areas: bool = False, exclude_side_areas: bool = False, angle_z1: int = 45, angle_z2: int = 135, main_axis: str = "", - ): + ) -> float: """_summary_ :param blender-object obj: blender object, bpy.types.Object @@ -760,7 +792,7 @@ class QtoCalculator: area += polygon.area return area + total_opening_area - def get_gross_side_area(self, obj): + def get_gross_side_area(self, obj: bpy.types.Object) -> float: if not self.has_openings(obj): return self.get_net_side_area(obj) @@ -768,15 +800,15 @@ class QtoCalculator: return gross_side_area - def get_net_side_area(self, obj): + def get_net_side_area(self, obj: bpy.types.Object) -> float: net_side_area = self.get_lateral_area(obj, exclude_end_areas=True, main_axis="x") / 2 return net_side_area - def get_outer_surface_area(self, obj): + def get_outer_surface_area(self, obj: bpy.types.Object) -> float: outer_surface_area = self.get_lateral_area(obj, exclude_end_areas=True, angle_z1=0, angle_z2=360) return outer_surface_area - def get_end_area(self, obj): + def get_end_area(self, obj: bpy.types.Object) -> float: element = tool.Ifc.get_entity(obj) gross_mesh = self.get_gross_element_mesh(element) gross_obj = bpy.data.objects.new("MyObject", gross_mesh) @@ -790,7 +822,7 @@ class QtoCalculator: return end_area - def get_gross_top_area(self, obj, angle: int = 45): + def get_gross_top_area(self, obj: bpy.types.Object, angle: int = 45) -> float: """_summary_: Returns the gross top area of the object. :param blender-object obj: blender object @@ -827,12 +859,14 @@ class QtoCalculator: return area + opening_area # curently net top area is larger then projected area, because its taking into account internal polygons, or window sills - def get_net_top_area(self, obj, angle: int = 45, ignore_internal: bool = True): + def get_net_top_area(self, obj: bpy.types.Object, angle: int = 45, ignore_internal: bool = True) -> float: """_summary_: Returns the net top area of the object. :param blender-object obj: blender object - :param int angle: Angle measured from the positive z-axis to the normal-vector of the area. Values lower than this will be ignored, defaults to 45 - :param bool ignore_internal: Toggle whether internal areas should be subtracted (Like window sills), defaults to True + :param int angle: Angle measured from the positive z-axis to the normal-vector of the area. + Values lower than this will be ignored, defaults to 45 + :param bool ignore_internal: Toggle whether internal areas should be subtracted (Like window sills), + defaults to True :return float: Net Top Area """ z_axis = (0, 0, 1) @@ -852,11 +886,11 @@ class QtoCalculator: return area - def get_projected_area(self, obj, projection_axis: str = "z", is_gross: bool = True): + def get_projected_area(self, obj, projection_axis: AxisType = "z", is_gross: bool = True) -> float: """_summary_: Returns the projected area of the object. :param blender-object obj: blender object - :param str projection_axis: Axis to project the area onto. Can be "X", "Y" or "Z" + :param str projection_axis: Axis to project the area onto. Can be "x", "y" or "z" :param bool is_gross: if True, the projected area will include openings, if False, the projected area will exclude openings :return float: Projected Area """ @@ -891,7 +925,7 @@ class QtoCalculator: return projected_polygon.area + void_area return projected_polygon.area - def get_OBB_object(self, obj): + def get_OBB_object(self, obj: bpy.types.Object) -> bpy.types.Object: """_summary_: Returns the Oriented-Bounding-Box (OBB) of the object. :param blender-object obj: Blender Object @@ -932,7 +966,7 @@ class QtoCalculator: return new_OBB_object - def get_AABB_object(self, obj): + def get_AABB_object(self, obj: bpy.types.Object) -> bpy.types.Object: """_summary_: Returns the Axis-Aligned-Bounding-Box (AABB) of the object. :param blender-object obj: Blender Object @@ -988,12 +1022,12 @@ class QtoCalculator: def get_bisected_obj( self, - obj, - plane_co_pos, - plane_no_pos, - plane_co_neg, - plane_no_neg, - ): + obj: bpy.types.Object, + plane_co_pos: VectorTuple, + plane_no_pos: VectorTuple, + plane_co_neg: VectorTuple, + plane_no_neg: VectorTuple, + ) -> bpy.types.Object: """_summary_: Returns the object bisected by two planes. :param blender-object obj: Blender Object @@ -1031,11 +1065,12 @@ class QtoCalculator: return bis_obj - def get_total_contact_area(self, obj, class_filter: str = ["IfcElement"]): + def get_total_contact_area(self, obj: bpy.types.Object, class_filter: list[str] = ["IfcElement"]) -> float: """_summary_: Returns the total contact area of the object with other objects. :param blender-object obj: Blender Object - :param list [] class_filter: A list of classes used to filter the objects to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"] + :param list [] class_filter: A list of classes used to filter the objects + to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"] :return float: Total contact area of the object with other objects. """ total_contact_area = 0 @@ -1046,11 +1081,12 @@ class QtoCalculator: return total_contact_area - def get_touching_objects(self, obj, class_filter): + def get_touching_objects(self, obj: bpy.types.Object, class_filter: list[str]) -> list[bpy.types.Object]: """_summary_: Returns a list of objects that are touching the object. :param blender-object obj: Blender Object - :param list [] class_filter: A list of classes used to filter the objects to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"] + :param list [] class_filter: A list of classes used to filter the objects + to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"] :return list: List of touching objects """ # rotate the object ever so slightly, otherwise bvhtree.overlap won't work properly. https://blender.stackexchange.com/a/275244/130742 @@ -1094,7 +1130,7 @@ class QtoCalculator: return touching_objects - def get_contact_area(self, object1, object2): + def get_contact_area(self, object1: bpy.types.Object, object2: bpy.types.Object) -> float: """_summary_: Returns the contact area between two objects. :param blender-object obj: Blender Object @@ -1109,7 +1145,13 @@ class QtoCalculator: total_area += self.get_intersection_between_polygons(object1, poly1, object2, poly2) return total_area - def get_intersection_between_polygons(self, object1, poly1, object2, poly2): + def get_intersection_between_polygons( + self, + object1: bpy.types.Object, + poly1: bpy.types.MeshPolygon, + object2: bpy.types.Object, + poly2: bpy.types.MeshPolygon, + ) -> float: """_summary_: Returns the intersection between two polygons. :param blender-object object1: Blender Object @@ -1152,7 +1194,9 @@ class QtoCalculator: # TopologicalError - Generated Geometry might be invalid return 0 - def create_shapely_polygon(self, obj, polygon, trans_matrix): + def create_shapely_polygon( + self, obj: bpy.types.Object, polygon: bpy.types.MeshPolygon, trans_matrix: Matrix + ) -> Polygon: """_summary_: Create a shapely polygon :param blender-object obj: Blender Object @@ -1171,12 +1215,14 @@ class QtoCalculator: polygon_tuples.append((x, y)) return Polygon(polygon_tuples) - def get_gross_element_mesh(self, element): + def get_gross_element_mesh(self, element: ifcopenshell.entity_instance) -> bpy.types.Mesh: settings = ifcopenshell.geom.settings() settings.set(settings.DISABLE_OPENING_SUBTRACTIONS, True) return self.create_mesh_from_shape(element, settings) - def create_mesh_from_shape(self, element, settings=None): + def create_mesh_from_shape( + self, element: ifcopenshell.entity_instance, settings: Optional[ifcopenshell.geom.settings] = None + ) -> bpy.types.Mesh: if settings is None: settings = ifcopenshell.geom.settings() shape = ifcopenshell.geom.create_shape(settings, element) @@ -1203,12 +1249,12 @@ class QtoCalculator: mesh.update() return mesh - def get_bmesh_from_mesh(self, mesh): + def get_bmesh_from_mesh(self, mesh: bpy.types.Mesh) -> bmesh.types.BMesh: bm = bmesh.new() bm.from_mesh(mesh) return bm - def get_object_main_axis(self, o): + def get_object_main_axis(self, o: bpy.types.Object) -> AxisType: """_summary_: Returns the main object axis. Useful for profile-defined objects. :param blender-object o: Blender Object @@ -1227,18 +1273,18 @@ class QtoCalculator: else: return "x" - def is_opening_horizontal(self, o): + def is_opening_horizontal(self, o: bpy.types.Object) -> bool: x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length return z < x and z < y - def delete_mesh(self, mesh): + def delete_mesh(self, mesh: bpy.types.Mesh) -> None: mesh.user_clear() bpy.data.meshes.remove(mesh) - def delete_obj(self, obj): + def delete_obj(self, obj: bpy.types.Object) -> None: bpy.data.objects.remove(obj, do_unlink=True) diff --git a/src/blenderbim/blenderbim/bim/module/pset/ui.py b/src/blenderbim/blenderbim/bim/module/pset/ui.py index 549a9fbe18..8a715e4718 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/ui.py +++ b/src/blenderbim/blenderbim/bim/module/pset/ui.py @@ -33,7 +33,6 @@ from blenderbim.bim.module.pset.data import ( GroupPsetData, ProfilePsetsData, WorkSchedulePsetsData, - ) @@ -91,7 +90,7 @@ def get_active_pset_obj_name(context, obj_type): return "" -def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type): +def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_removing=True): box = layout.box() row = box.row(align=True) if "is_expanded" not in pset: @@ -115,16 +114,20 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type): op.pset_id = pset_id op.obj = obj_name op.obj_type = obj_type - op = row.operator("bim.remove_pset", icon="X", text="") + remove_pset_row = row.row(align=True) + op = remove_pset_row.operator("bim.remove_pset", icon="X", text="") op.pset_id = pset_id op.obj = obj_name op.obj_type = obj_type + remove_pset_row.enabled = allow_removing elif props.active_pset_id != pset_id: row.label(text=pset["Name"], icon="COPY_ID") - op = row.operator("bim.remove_pset", icon="X", text="") + remove_pset_row = row.row(align=True) + op = remove_pset_row.operator("bim.remove_pset", icon="X", text="") op.pset_id = pset_id op.obj = obj_name op.obj_type = obj_type + remove_pset_row.enabled = allow_removing if pset["is_expanded"]: if props.active_pset_id == pset_id: for prop in props.properties: @@ -172,7 +175,7 @@ def draw_psetqto_editable_ui(box, props, prop): class BIM_PT_object_psets(Panel): - bl_label = "Object Property Sets" + bl_label = "Property Sets" bl_idname = "BIM_PT_object_psets" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" @@ -204,17 +207,22 @@ class BIM_PT_object_psets(Panel): if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "PSET": draw_psetqto_ui(context, 0, {}, props, self.layout, "Object") - for pset in ObjectPsetsData.data["psets"]: - draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object") + if ObjectPsetsData.data["psets"]: + if ObjectPsetsData.data["is_occurrence"]: + self.layout.label(text="Occurrence Properties:") + else: + self.layout.label(text="Type Properties:") + for pset in ObjectPsetsData.data["psets"]: + draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object") if ObjectPsetsData.data["inherited_psets"]: - self.layout.label(text="Inherited Psets:", icon="FILE_PARENT") + self.layout.label(text="Inherited Type Properties:", icon="CON_CHILDOF") for pset in ObjectPsetsData.data["inherited_psets"]: - draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object") + draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object", allow_removing=False) class BIM_PT_object_qtos(Panel): - bl_label = "Object Quantity Sets" + bl_label = "Quantity Sets" bl_idname = "BIM_PT_object_qtos" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" @@ -246,8 +254,18 @@ class BIM_PT_object_qtos(Panel): if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "QTO": draw_psetqto_ui(context, 0, {}, props, self.layout, "Object") - for qto in ObjectQtosData.data["qtos"]: - draw_psetqto_ui(context, qto["id"], qto, props, self.layout, "Object") + if ObjectQtosData.data["qtos"]: + if ObjectQtosData.data["is_occurrence"]: + self.layout.label(text="Occurrence Quantities:") + else: + self.layout.label(text="Type Quantities:") + for qto in ObjectQtosData.data["qtos"]: + draw_psetqto_ui(context, qto["id"], qto, props, self.layout, "Object") + + if ObjectQtosData.data["inherited_qsets"]: + self.layout.label(text="Inherited Type Quantities:", icon="CON_CHILDOF") + for qset in ObjectQtosData.data["inherited_qsets"]: + draw_psetqto_ui(context, qset["id"], qset, props, self.layout, "Object", allow_removing=False) class BIM_PT_material_psets(Panel): @@ -541,8 +559,6 @@ class BIM_PT_group_psets(Panel): draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Group") - - class BIM_PT_profile_psets(Panel): bl_label = "Profile Property Sets" bl_idname = "BIM_PT_profile_psets" diff --git a/src/blenderbim/blenderbim/bim/module/pset_template/__init__.py b/src/blenderbim/blenderbim/bim/module/pset_template/__init__.py index 766045f8af..e121c9a24f 100644 --- a/src/blenderbim/blenderbim/bim/module/pset_template/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/pset_template/__init__.py @@ -34,6 +34,7 @@ classes = ( operator.RemovePropTemplate, operator.RemovePsetTemplate, operator.RemovePsetTemplateFile, + operator.SavePsetTemplateFile, prop.PsetTemplate, prop.EnumerationValues, prop.PropTemplate, diff --git a/src/blenderbim/blenderbim/bim/module/root/data.py b/src/blenderbim/blenderbim/bim/module/root/data.py index 6b91e18ab8..18f7fbffcd 100644 --- a/src/blenderbim/blenderbim/bim/module/root/data.py +++ b/src/blenderbim/blenderbim/bim/module/root/data.py @@ -42,6 +42,7 @@ class IfcClassData: cls.data["contexts"] = cls.contexts() cls.data["has_entity"] = cls.has_entity() cls.data["name"] = cls.name() + cls.data["has_inherited_predefined_type"] = cls.has_inherited_predefined_type() cls.data["ifc_class"] = cls.ifc_class() cls.data["ifc_predefined_types"] = cls.ifc_predefined_types() cls.data["can_reassign_class"] = cls.can_reassign_class() @@ -162,6 +163,16 @@ class IfcClassData: name += f"[{predefined_type}]" return name + @classmethod + def has_inherited_predefined_type(cls): + element = tool.Ifc.get_entity(bpy.context.view_layer.objects.active) + if not element: + return + if element_type := ifcopenshell.util.element.get_type(element): + # Allow for None due to https://github.com/buildingSMART/IFC4.3.x-development/issues/818 + return ifcopenshell.util.element.get_predefined_type(element_type) not in ("NOTDEFINED", None) + return False + @classmethod def ifc_class(cls): element = tool.Ifc.get_entity(bpy.context.view_layer.objects.active) diff --git a/src/blenderbim/blenderbim/bim/module/root/operator.py b/src/blenderbim/blenderbim/bim/module/root/operator.py index fdeb0d9c34..ed1985c5bd 100644 --- a/src/blenderbim/blenderbim/bim/module/root/operator.py +++ b/src/blenderbim/blenderbim/bim/module/root/operator.py @@ -215,7 +215,7 @@ class UnlinkObject(bpy.types.Operator): bl_idname = "bim.unlink_object" bl_label = "Unlink Object" bl_options = {"REGISTER", "UNDO"} - obj: bpy.props.StringProperty() + obj: bpy.props.StringProperty(name="Object Name") should_delete: bpy.props.BoolProperty(name="Delete IFC Element", default=True) def execute(self, context): @@ -227,6 +227,7 @@ class UnlinkObject(bpy.types.Operator): else: objects = context.selected_objects + objects: list[bpy.types.Object] for obj in objects: was_active_object = obj == context.active_object @@ -242,6 +243,27 @@ class UnlinkObject(bpy.types.Operator): if obj.data: obj_copy.data = obj.data.copy() + # prevent unlinking materials that might be used elsewhere + replacements: dict[bpy.types.Material, bpy.types.Material] = dict() + for material_slot in obj_copy.material_slots: + material = material_slot.material + if material is None: + continue + + if material in replacements: + material_replacement = replacements[material] + + # no need to copy non-ifc materials as unlinking won't do anything to them + elif tool.Ifc.get_entity(material) is None and tool.Style.get_style(material) is None: + replacements[material] = material + continue + + else: + material_replacement = material.copy() + replacements[material] = material_replacement + + material_slot.material = material_replacement + tool.Geometry.delete_ifc_object(obj) obj = obj_copy diff --git a/src/blenderbim/blenderbim/bim/module/root/ui.py b/src/blenderbim/blenderbim/bim/module/root/ui.py index b80c733ee5..a57bec2e40 100644 --- a/src/blenderbim/blenderbim/bim/module/root/ui.py +++ b/src/blenderbim/blenderbim/bim/module/root/ui.py @@ -61,7 +61,10 @@ class BIM_PT_class(Panel): self.layout.prop(context.scene.BIMRootProperties, "relating_class_object", icon="COPYDOWN") else: row = self.layout.row(align=True) - row.label(text=IfcClassData.data["name"]) + row.label( + text=IfcClassData.data["name"], + icon="CON_CHILDOF" if IfcClassData.data["has_inherited_predefined_type"] else "NONE", + ) row.operator("bim.select_ifc_class", text="", icon="RESTRICT_SELECT_OFF") row.operator("bim.unlink_object", icon="UNLINKED", text="") if IfcClassData.data["can_reassign_class"]: diff --git a/src/blenderbim/blenderbim/bim/module/search/__init__.py b/src/blenderbim/blenderbim/bim/module/search/__init__.py index 606ed092c8..2abf6b132c 100644 --- a/src/blenderbim/blenderbim/bim/module/search/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/search/__init__.py @@ -24,21 +24,15 @@ classes = ( operator.ActivateIfcClassFilter, operator.AddFilter, operator.AddFilterGroup, - operator.AddToIfcGroup, operator.ColourByProperty, operator.EditFilterQuery, - operator.FilterModelElements, - operator.IfcSelector, operator.LoadColourscheme, - operator.LoadQuery, operator.LoadSearch, - operator.OpenQueryLibrary, operator.RemoveFilter, operator.RemoveFilterGroup, operator.ResetObjectColours, operator.SaveColourscheme, operator.SaveSearch, - operator.SaveSelectorQuery, operator.Search, operator.SelectByProperty, operator.SelectFilterElements, @@ -51,11 +45,6 @@ classes = ( prop.BIMFilterClasses, prop.BIMFilterBuildingStoreys, prop.BIMSearchProperties, - prop.SearchCollection, - prop.SearchQueryFilter, - prop.SearchQuery, - prop.SearchQueryGroup, - prop.IfcSelectorProperties, ui.BIM_PT_search, ui.BIM_PT_filter, ui.BIM_PT_colour_by_property, @@ -63,15 +52,12 @@ classes = ( ui.BIM_UL_colourscheme, ui.BIM_UL_ifc_class_filter, ui.BIM_UL_ifc_building_storey_filter, - ui.BIM_PT_IFCSelector, ) def register(): bpy.types.Scene.BIMSearchProperties = bpy.props.PointerProperty(type=prop.BIMSearchProperties) - bpy.types.Scene.IfcSelectorProperties = bpy.props.PointerProperty(type=prop.IfcSelectorProperties) def unregister(): del bpy.types.Scene.BIMSearchProperties - del bpy.types.Scene.IfcSelectorProperties diff --git a/src/blenderbim/blenderbim/bim/module/search/operator.py b/src/blenderbim/blenderbim/bim/module/search/operator.py index 2a3f710471..6ffe447470 100644 --- a/src/blenderbim/blenderbim/bim/module/search/operator.py +++ b/src/blenderbim/blenderbim/bim/module/search/operator.py @@ -131,6 +131,7 @@ class SelectFilterElements(bpy.types.Operator): text_data = bpy.data.texts.new(name) text_data.from_string(",".join(global_ids)) filter_groups[self.group_index].filters[self.index].value = f"bpy.data.texts['{name}']" + self.report({"INFO"}, f'List of Global Ids was saved to the text file "{name}" in the current .blend file') else: filter_groups[self.group_index].filters[self.index].value = ",".join(global_ids) return {"FINISHED"} @@ -234,7 +235,7 @@ class LoadSearch(Operator, tool.Ifc.Operator): def _execute(self, context): filter_groups = tool.Search.get_filter_groups(self.module) group = tool.Ifc.get().by_id(int(context.scene.BIMSearchProperties.saved_searches)) - query = tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups) + tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups) def draw(self, context): props = context.scene.BIMSearchProperties @@ -270,25 +271,29 @@ class ColourByProperty(Operator): colourscheme = {} if len(props.colourscheme): - colourscheme = {cs.name: cs.colour[0:3] for cs in props.colourscheme} + colourscheme = {cs.name: {"colour": cs.colour[0:3], "total": 0} for cs in props.colourscheme} for obj in context.visible_objects: element = tool.Ifc.get_entity(obj) if not element: continue value = str(ifcopenshell.util.selector.get_element_value(element, query)) - if value not in colourscheme: - colourscheme[value] = next(colours)[0:3] - obj.color = (*colourscheme[value], 1) + if value in colourscheme: + colourscheme[value]["total"] += 1 + else: + colourscheme[value] = {"colour": next(colours)[0:3], "total": 1} + obj.color = (*colourscheme[value]["colour"], 1) areas = [a for a in context.screen.areas if a.type == "VIEW_3D"] if areas: areas[0].spaces[0].shading.color_type = "OBJECT" props.colourscheme.clear() - for value, colour in colourscheme.items(): + for value in sorted(colourscheme.keys()): + data = colourscheme[value] new = props.colourscheme.add() new.name = str(value) - new.colour = colour[0:3] + new.total = data["total"] + new.colour = data["colour"][0:3] return {"FINISHED"} def store_state(self, context): @@ -350,16 +355,16 @@ class SaveColourscheme(Operator, tool.Ifc.Operator): query = props.colourscheme_query group = [g for g in tool.Ifc.get().by_type("IfcGroup") if g.Name == self.name] - coulour_scheme = {cs.name: cs.colour[0:3] for cs in props.colourscheme} + colourscheme = {cs.name: {"colour": cs.colour[0:3], "total": cs.total} for cs in props.colourscheme} if group: group = group[0] description = json.loads(group.Description) - description["colourscheme"] = coulour_scheme + description["colourscheme"] = colourscheme description["colourscheme_query"] = query group.Description = json.dumps(description) else: description = json.dumps( - {"type": "BBIM_Search", "colourscheme": coulour_scheme, "colourscheme_query": query} + {"type": "BBIM_Search", "colourscheme": colourscheme, "colourscheme_query": query} ) group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.name, Description=description) @@ -379,10 +384,11 @@ class LoadColourscheme(Operator, tool.Ifc.Operator): description = json.loads(group.Description) props.colourscheme_query = description.get("colourscheme_query") props.colourscheme.clear() - for name, colour in description.get("colourscheme", {}).items(): + for name, data in description.get("colourscheme", {}).items(): new = props.colourscheme.add() new.name = name - new.colour = colour + new.total = data["total"] + new.colour = data["colour"] def draw(self, context): props = context.scene.BIMSearchProperties @@ -538,9 +544,11 @@ class ActivateIfcClassFilter(Operator): "filter_classes", context.scene.BIMSearchProperties, "filter_classes_index", - rows=20 - if len(bpy.context.scene.BIMSearchProperties.filter_classes) > 20 - else len(bpy.context.scene.BIMSearchProperties.filter_classes), + rows=( + 20 + if len(bpy.context.scene.BIMSearchProperties.filter_classes) > 20 + else len(bpy.context.scene.BIMSearchProperties.filter_classes) + ), ) row = self.layout.row(align=True) row.operator("bim.toggle_filter_selection", text="Select All").action = "SELECT" @@ -595,9 +603,11 @@ class ActivateContainerFilter(Operator): "filter_container", context.scene.BIMSearchProperties, "filter_container_index", - rows=20 - if len(bpy.context.scene.BIMSearchProperties.filter_container) > 20 - else len(bpy.context.scene.BIMSearchProperties.filter_container), + rows=( + 20 + if len(bpy.context.scene.BIMSearchProperties.filter_container) > 20 + else len(bpy.context.scene.BIMSearchProperties.filter_container) + ), ) row = self.layout.row(align=True) row.operator("bim.toggle_filter_selection", text="Select All").action = "SELECT" @@ -618,223 +628,6 @@ class ShowAllElements(Operator): return {"FINISHED"} -class FilterModelElements(Operator): - """Filter model elements based on selection""" - - bl_idname = "bim.filter_model_elements" - bl_label = "Filter Model Elements" - option: StringProperty("select|isolate|hide") - - def execute(self, context): - selection_props = context.scene.IfcSelectorProperties - selection = ( - selection_props.selector_query_syntax - if selection_props.manual_override - else self.add_groups(selection_props) - ) - selection_props.selector_query_syntax = selection - r = core.search(tool.Search, tool.Spatial, query=selection, action=self.option) - if isinstance(r, str): - self.report({"WARNING"}, r) - return {"FINISHED"} - - def add_groups(self, selector: str) -> str: - selection = "" - for group_index, group in enumerate(selector.groups): - if group_index != 0: - selection += " | " - selection += "(" if len(selector.groups) > 1 else "" - selection = self.add_queries(selection, group) - - selection += ")" if len(selector.groups) > 1 else "" - return selection - - def add_queries(self, selection: str, group: str) -> str: - for query_index, query in enumerate(group.queries): - if query_index != 0: - selection += " & " if query.and_or == "and" else " | " - - if query.selector == "IFC Class": - active_option = query.active_option.split(": ")[1] - selection += f".{active_option}" - selection = self.add_filters(selection, query) - - elif query.selector == "GlobalId": - selection += f"#{query.value}" - - elif query.selector == "IfcElementType": - index = int(query.active_sub_option.split(":")[0]) - selection += f"* #{query.sub_options[index].global_id}" - - elif query.selector == "IfcSpatialElement": - index = int(query.active_sub_option.split(":")[0]) - selection += f"@ #{query.sub_options[index].global_id}" - return selection - - def add_filters(self, selection: str, query: str) -> str: - for f_index, f in enumerate(query.filters): - if f_index != 0: - selection += " & " if f.and_or == "and" else " | " - selection += f".{query.active_option}" - - if f.value in ("True", "False"): - value = f.value - elif f.value.isnumeric() and str(float(f.value))[0] == f.value[0]: - value = f.value - else: - value = f'"{f.value}"' - - selection += "[" - - if f.selector == "IfcPropertySet": - selection += f'{f.active_option.split(": ")[1]}.{f.active_sub_option.split(": ")[1]} {"!" if f.negation else ""}{f.comparison} {value}' - elif f.selector == "Attribute": - # we're using the prop_search functionality in blender which returns the index of the option. Sometimes the user can override this and enter a value that doesn't exist in the list. In this case there is no index and we need to handle it. @vulevukusej - pattern = re.compile(r"^[0-9]+:") - match = pattern.search(f.active_option) - selection += f'{f.active_option.split(": ")[1] if match else f.active_option} {"!" if f.negation else ""}{f.comparison} {value}' - selection += "]" - return selection - - -# This needs to be moved into ui code, I know ;) - vulevukusej -class IfcSelector(Operator): - """Select elements in model with IFC Selector""" - - bl_idname = "bim.ifc_selector" - bl_label = "Select elements with IFC Selector" - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self, width=800) - - @classmethod - def poll(cls, context): - return IfcStore.get_file() - - def execute(self, context): - return {"FINISHED"} - - def draw(self, context): - from . import ui - - ui.IfcSelectorUI.draw(context, self.layout) - - -class SaveSelectorQuery(Operator): - bl_idname = "bim.save_selector_query" - bl_label = "Save Selector Query" - save_name: StringProperty() - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self, width=400) - - def draw(self, context): - layout = self.layout - layout.prop(self, "save_name") - - def execute(self, context): - ifc_selector = context.scene.IfcSelectorProperties - new = ifc_selector.query_library.add() - new.name = self.save_name - new.query = ifc_selector.selector_query_syntax - return {"FINISHED"} - - -class OpenQueryLibrary(Operator): - """Open Query Library""" - - bl_idname = "bim.open_query_library" - bl_label = "Open Query Library" - - def invoke(self, context, event): - return context.window_manager.invoke_popup(self, width=400) - - def draw(self, context): - layout = self.layout - ifc_selector = context.scene.IfcSelectorProperties - - for index, query in enumerate(ifc_selector.query_library): - row = layout.row(align=True) - row.prop(query, "query", text=query.name) - op = row.operator("bim.load_query", icon="SORT_ASC", text="") - op.index = index - - row.context_pointer_set(name="bim_prop_group", data=ifc_selector) - op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="") - op.option = "remove" - op.collection = "query_library" - op.index = index - - def execute(self, context): - return {"FINISHED"} - - -class LoadQuery(Operator): - bl_idname = "bim.load_query" - bl_label = "Load Query" - index: IntProperty() - - def invoke(self, context, event): - close_operator_panel(event) - return self.execute(context) - - def execute(self, context): - ifc_selector = context.scene.IfcSelectorProperties - ifc_selector.selector_query_syntax = ifc_selector.query_library[self.index].query - return {"FINISHED"} - - -class AddToIfcGroup(Operator): - bl_idname = "bim.add_to_ifc_group" - bl_label = "Add to IFC Group" - group_name: StringProperty(name="Group Name") - - def invoke(self, context, event): - bpy.ops.bim.load_groups() - return context.window_manager.invoke_props_dialog(self, width=400) - - def draw(self, context): - self.props = context.scene.BIMGroupProperties - row = self.layout.row() - row.operator("bim.add_group") - - self.layout.template_list( - "BIM_UL_groups", - "", - self.props, - "groups", - self.props, - "active_group_index", - ) - - if self.props.active_group_id: - for attribute in self.props.group_attributes: - if attribute.name in ["Name", "Description"]: - row = self.layout.row(align=True) - row.prop(attribute, "string_value", text=attribute.name) - - def execute(self, context): - active_group_index = self.props.active_group_index - ifc_definition_id = self.props.groups[active_group_index].ifc_definition_id - - bpy.ops.bim.enable_editing_group(group=ifc_definition_id) - - selector_query_syntax = context.scene.IfcSelectorProperties.selector_query_syntax - - for attribute in self.props.group_attributes: - if attribute.name == "Description": - if "*selector*" not in attribute.string_value: - attribute.string_value += f" *selector*{selector_query_syntax}*selector*" - else: - new_description = attribute.string_value.split("*selector*") - new_description[1] = selector_query_syntax - attribute.string_value = "*selector*".join(new_description) - - bpy.ops.bim.edit_group() - bpy.ops.bim.disable_group_editing_ui() - return {"FINISHED"} - - class SelectSimilar(Operator, tool.Ifc.Operator): bl_idname = "bim.select_similar" bl_label = "Select Similar" diff --git a/src/blenderbim/blenderbim/bim/module/search/prop.py b/src/blenderbim/blenderbim/bim/module/search/prop.py index 22d0e80173..c8c5580e6e 100644 --- a/src/blenderbim/blenderbim/bim/module/search/prop.py +++ b/src/blenderbim/blenderbim/bim/module/search/prop.py @@ -78,7 +78,7 @@ def update_is_container_selected(self, context): else: for obj in context.selected_objects: container = tool.Spatial.get_container(tool.Ifc.get_entity(obj)) - if (container and container.Name == self.name) or (not container and self.name== "None"): + if (container and container.Name == self.name) or (not container and self.name == "None"): obj.select_set(False) new = self.unselected_objects.add() new.obj = obj @@ -100,6 +100,7 @@ class BIMFilterBuildingStoreys(PropertyGroup): class BIMColour(PropertyGroup): name: StringProperty(name="Name") + total: IntProperty(name="Total") colour: FloatVectorProperty(name="Colour", subtype="COLOR", default=(1, 0, 0), min=0.0, max=1.0) @@ -138,139 +139,3 @@ def get_classes(self, ifc_product): declarations = util.schema.get_subtypes(declaration) names = [d.name() for d in declarations] return [(c, c, "") for c in sorted(names)] - - -def load_sub_options(self, context): - if self.selector not in ["IfcClass"]: - self.load_option = "sub_options" - load_selection_options(self, context) - - -def load_selection_options(self, context): - ifc = IfcStore.file - load_option = self.load_option - op = getattr(self, load_option) - op.clear() - options = [] - - if load_option == "options": - self.sub_options.clear() - if self.selector == "IFC Class": - options = get_classes(self, "IfcElement") - options.append(("IfcSpace", "IfcSpace", "")) - elif self.selector == "IfcSpatialElement": - options = get_classes(self, "IfcSpatialElement") - elif self.selector == "IfcElementType": - options = get_classes(self, "IfcElementType") - elif self.selector == "GlobalId": - return - elif self.selector == "IfcPropertySet": - psets = Selector.parse(ifc, ".IfcPropertySet") - options = set([o.Name for o in psets]) - elif self.selector == "Attribute": - attributes = ["GlobalId", "Name", "Description", "ObjectType", "Tag", "PredefinedType"] - for a in attributes: - options.append((a, a, "")) - - elif load_option == "sub_options": - if self.selector in ["IfcSpatialElement", "IfcElementType"]: - active_option = self.active_option.split(": ")[1] - options = Selector.parse(ifc, f".{active_option}") - - elif self.selector == "IfcPropertySet": - options = set() - active_pset = self.active_option.split(": ")[1] - psets_in_file = Selector.parse(ifc, f'.IfcPropertySet[Name="{active_pset}"]') - for pset in psets_in_file: - for prop in pset.HasProperties: - options.add(prop.Name) - - for index, option in enumerate(options): - new = op.add() - if self.selector in ["IfcSpatialElement", "IfcElementType"]: - if self.load_option == "sub_options": - new.name = f"{index}: {option.Name}" - new.global_id = option.GlobalId - else: - new.name = f"{index}: {option[0]}" - elif self.selector == "IfcPropertySet": - new.name = f"{index}: {option}" - else: - new.name = f"{index}: {option[0]}" - self.load_option = "options" - - -def load_spatial_elements(self, context): - ifc = IfcStore.file - col_items = Selector.parse(ifc, f".{self.selected_spatial_element}") - collection = getattr(self, "sub_spatial_elements", None) - collection.clear() - for index, c in enumerate(col_items): - new = collection.add() - new.name = f"{index}-{c.Name}" - - -class SearchCollection(PropertyGroup): - name: StringProperty() - long_name: StringProperty() - global_id: StringProperty() - query: StringProperty() - - -class IfcSelector: - and_or: EnumProperty( - items=[(i, i, i) for i in ["and", "or"]], - ) - negation: BoolProperty(name="not") - comparison: EnumProperty( - items=[ - ("=", "equal to", ""), - ("*=", "contains", ""), - (">=", "greater than or equal to", ""), - ("<=", "lesser than or equal to", ""), - (">", "greater than", ""), - ("<", "less than", ""), - ], - ) - load_option: StringProperty( - default="options", description="controls whether or not options or sub_options are loaded" - ) - options: CollectionProperty(type=SearchCollection) - active_option: StringProperty(update=load_sub_options) - sub_options: CollectionProperty(type=SearchCollection) - active_sub_option: StringProperty() - value: StringProperty(description="generic 'value' that can be used in multiple scenarios") - - -class SearchQueryFilter(PropertyGroup, IfcSelector): - selector: EnumProperty( - items=[(i, i, i) for i in ["-", "IfcPropertySet", "Attribute"]], - name="Filter selection by", - update=load_selection_options, - default="-", - ) - - -class SearchQuery(PropertyGroup, IfcSelector): - filters: CollectionProperty(type=SearchQueryFilter) - selector: EnumProperty( - items=[(i, i, i) for i in ["-", "IFC Class", "IfcSpatialElement", "IfcElementType", "GlobalId"]], - name="Selector type", - update=load_selection_options, - default="-", - ) - - -class SearchQueryGroup(PropertyGroup, IfcSelector): - queries: CollectionProperty(type=SearchQuery) - - -class IfcSelectorProperties(PropertyGroup, IfcSelector): - groups: CollectionProperty(type=SearchQueryGroup) - selector_query_syntax: StringProperty() - - query_library: CollectionProperty(type=SearchCollection) - active_query: StringProperty() - - active_query: StringProperty() - manual_override: BoolProperty(default=False, description="Toggle to allow manual typing of query-syntax") diff --git a/src/blenderbim/blenderbim/bim/module/search/ui.py b/src/blenderbim/blenderbim/bim/module/search/ui.py index b8c733c848..8117bc9f52 100644 --- a/src/blenderbim/blenderbim/bim/module/search/ui.py +++ b/src/blenderbim/blenderbim/bim/module/search/ui.py @@ -125,7 +125,7 @@ class BIM_UL_colourscheme(bpy.types.UIList): if not item: return row = layout.row(align=True) - row.label(text=item.name) + row.label(text=f"{item.name} ({item.total})") row.prop(item, "colour", text="") @@ -159,153 +159,3 @@ class BIM_UL_ifc_building_storey_filter(bpy.types.UIList): split = split.column() split.scale_x = 0.5 split.label(text=str(item.total)) - - -class BIM_PT_IFCSelector(Panel): - bl_label = "Selector" - bl_idname = "BIM_PT_ifc_selector" - bl_options = {"DEFAULT_CLOSED"} - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "scene" - bl_parent_id = "BIM_PT_tab_sandbox" - - def draw(self, context): - layout = self.layout - layout.operator("bim.ifc_selector") - - -# this doesn't inherit from Panel since it's just a class to abstract away UI code from the IfcSelector Operator -class IfcSelectorUI: - @classmethod - def draw(self, context, layout): - ifc_selector = context.scene.IfcSelectorProperties - row = layout.row() - - row.context_pointer_set(name="bim_prop_group", data=ifc_selector) - op = row.operator("bim.edit_blender_collection", text="Add selection group") - op.option = "add" - op.collection = "groups" - layout.separator() - - self.draw_query_group_ui(self, ifc_selector, layout) - - if len(ifc_selector.groups) != 0: - row = layout.row(align=True) - row.prop(ifc_selector, "selector_query_syntax", text="Query Syntax") - row.prop(ifc_selector, "manual_override", text="") - - select = row.operator("bim.filter_model_elements", text="", icon="RESTRICT_SELECT_OFF") - select.option = "select" - isolate = row.operator("bim.filter_model_elements", text="", icon="ZOOM_SELECTED") - isolate.option = "isolate" - hide = row.operator("bim.filter_model_elements", text="", icon="HIDE_ON") - hide.option = "hide" - unhide = row.operator("bim.filter_model_elements", text="", icon="HIDE_OFF") - unhide.option = "unhide" - - row = layout.row(align=True) - row.alignment = "CENTER" - row.operator("bim.save_selector_query", text="Save Query") - op = row.operator("bim.open_query_library", text="Load Query") - row.operator("bim.add_to_ifc_group", text="Add to IFC Group") - - row = layout.row(align=True) - row.operator("bim.show_scene_elements", text="Show All Elements", icon="HIDE_OFF") - - def draw_query_group_ui(self, ifc_selector, layout): - for index, group in enumerate(ifc_selector.groups): - row = layout.row() - row.alignment = "CENTER" - row.label(text="or") if index != 0 else None - - box = layout.box() - row = box.row(align=True) - row.alignment = "CENTER" - row.label(text=f"Group #{str(index+1)}") - row.context_pointer_set(name="bim_prop_group", data=group) - op = row.operator("bim.edit_blender_collection", text="Add query", icon="PLUS") - op.option = "add" - op.collection = "queries" - - row.context_pointer_set(name="bim_prop_group", data=ifc_selector) - op = row.operator("bim.edit_blender_collection", text="Remove selection group") - op.option = "remove" - op.collection = "groups" - op.index = index - - self.draw_query_ui(self, group, box) - - def draw_query_ui(self, group, box): - for index, query in enumerate(group.queries): - row = box.row() - row.alignment = "LEFT" - - row.prop(query, "and_or", text="") if index != 0 else None - if query.and_or == "or": - row = box.row() - row.alignment = "LEFT" - row.prop(query, "selector", text="") - - if query.selector in ["IFC Class", "IfcSpatialElement", "IfcElementType"]: - row.label(text="Equals") - row.prop_search(query, "active_option", query, "options", text="") - row.prop_search(query, "active_sub_option", query, "sub_options", text="") if len( - query.sub_options - ) != 0 else None - self.draw_filter_ui(self, box, query) - - elif query.selector in ["GlobalId", "Attribute"]: - row.prop(query, "negation", text="") - row.prop(query, "comparison", text="") - row.prop(query, "value", text="") - - row.context_pointer_set(name="bim_prop_group", data=group) - op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="") - op.option = "remove" - op.collection = "queries" - op.index = index - - row.context_pointer_set(name="bim_prop_group", data=query) - op = ( - row.operator("bim.edit_blender_collection", text="Add filter") - if query.selector == "IFC Class" - else None - ) - if op: - op.option = "add" - op.collection = "filters" - - def draw_filter_ui(self, box, query): - for filter_index, f in enumerate(query.filters): - row = box.row() - row.alignment = "LEFT" - row.label(text=" ↪") - row.prop(f, "and_or", text="") if filter_index != 0 else None - if f.and_or == "or": - row = box.row() - row.alignment = "LEFT" - row.label(text=" ↪") - row.prop(f, "selector", text="") - - if f.selector == "Attribute": - row.prop_search(f, "active_option", f, "options", text="", results_are_suggestions=True) - row.prop( - f, - "negation", - ) - row.prop(f, "comparison", text="") - row.prop(f, "value", text="") - - elif f.selector == "IfcPropertySet": - row.prop_search(f, "active_option", f, "options", text="") - row.prop_search(f, "active_sub_option", f, "sub_options", text="") - row.prop(f, "negation") - row.prop(f, "comparison", text="") - row.prop(f, "value", text="") - - row.context_pointer_set(name="bim_prop_group", data=query) - op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="") - op.option = "remove" - op.collection = "filters" - op.index = filter_index diff --git a/src/blenderbim/blenderbim/bim/module/spatial/workspace.py b/src/blenderbim/blenderbim/bim/module/spatial/workspace.py index 8e91081ffb..c2e08bd1d0 100644 --- a/src/blenderbim/blenderbim/bim/module/spatial/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/spatial/workspace.py @@ -48,6 +48,7 @@ class SpatialTool(WorkSpaceTool): ("bim.spatial_hotkey", {"type": "B", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_B")]}), ("bim.spatial_hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}), ("bim.spatial_hotkey", {"type": "B", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_B")]}), + ("bim.spatial_hotkey", {"type": "V", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_V")]}), ("bim.spatial_hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}), ("bim.spatial_hotkey", {"type": "G", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_G")]}), ("bim.spatial_hotkey", {"type": "H", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_H")]}), @@ -92,36 +93,34 @@ class SpatialToolUI: if context.active_object and context.selected_objects: cls.draw_selected_object_interface(context) + add_layout_hotkey(cls.layout, "Decorate Boundaries", "A_V", bpy.ops.bim.decorate_boundaries.__doc__) + @classmethod def draw_default_interface(cls, context): row = cls.layout.row(align=True) row.prop(data=cls.model_props, property="rl3", text="RL") row = cls.layout.row(align=True) - row.label(text="", icon="EVENT_SHIFT") - row.label(text="", icon="EVENT_A") - row.operator("bim.generate_spaces_from_walls") - row = cls.layout.row(align=True) - row.label(text="", icon="EVENT_SHIFT") - row.label(text="", icon="EVENT_A") - row.operator("bim.generate_space") - row = cls.layout.row(align=True) - row.label(text="", icon="EVENT_SHIFT") - row.label(text="", icon="EVENT_T") - row.operator("bim.toggle_space_visibility") - row = cls.layout.row(align=True) - row.label(text="", icon="EVENT_SHIFT") - row.label(text="", icon="EVENT_H") - row.operator("bim.toggle_hide_spaces") + op_name = lambda op: op.get_rna_type().name + add_layout_hotkey( + cls.layout, + op_name(bpy.ops.bim.generate_spaces_from_walls), + "S_A", + bpy.ops.bim.generate_spaces_from_walls.__doc__, + ) + add_layout_hotkey(cls.layout, op_name(bpy.ops.bim.generate_space), "S_A", bpy.ops.bim.generate_space.__doc__) + add_layout_hotkey( + cls.layout, op_name(bpy.ops.bim.toggle_space_visibility), "S_T", bpy.ops.bim.toggle_space_visibility.__doc__ + ) + add_layout_hotkey( + cls.layout, op_name(bpy.ops.bim.toggle_hide_spaces), "S_H", bpy.ops.bim.toggle_hide_spaces.__doc__ + ) @classmethod def draw_selected_object_interface(cls, context): active_obj = bpy.context.active_object element = tool.Ifc.get_entity(active_obj) if element and bpy.context.selected_objects and element.is_a("IfcSpace"): - row = cls.layout.row(align=True) - row.label(text="", icon="EVENT_SHIFT") - row.label(text="", icon="EVENT_G") - row.operator("bim.generate_space", text="Regen") + add_layout_hotkey(cls.layout, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__) row = cls.layout.row(align=True) row.label(text="", icon="EVENT_SHIFT") @@ -129,7 +128,7 @@ class SpatialToolUI: row.prop(cls.model_props, "boundary_class", text="") row.operator("bim.add_boundary", text="Add Boundary") - add_layout_hotkey(cls.layout, "Boundaries", "A_B", "Toggle boundaries") + add_layout_hotkey(cls.layout, "Boundaries", "A_B", "Load/unload boundaries on selected objects") @classmethod def draw_type_selection_interface(cls, context): @@ -175,6 +174,8 @@ class Hotkey(bpy.types.Operator, Operator): bpy.ops.bim.add_boundary() def hotkey_A_B(self): + if not AuthoringData.is_loaded: + AuthoringData.load() if not bpy.context.selected_objects: return if AuthoringData.data["has_visible_boundaries"]: @@ -182,6 +183,9 @@ class Hotkey(bpy.types.Operator, Operator): else: bpy.ops.bim.show_boundaries() + def hotkey_A_V(self): + bpy.ops.bim.decorate_boundaries() + def hotkey_S_T(self): bpy.ops.bim.toggle_space_visibility() diff --git a/src/blenderbim/blenderbim/bim/module/structural/operator.py b/src/blenderbim/blenderbim/bim/module/structural/operator.py index 3662397056..8415771d2b 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/operator.py +++ b/src/blenderbim/blenderbim/bim/module/structural/operator.py @@ -473,7 +473,7 @@ class AssignStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator): self.file, **{ "relating_object": self.file.by_id(self.work_plan), - "product": self.file.by_id(self.load_case), + "products": [self.file.by_id(self.load_case)], }, ) return {"FINISHED"} @@ -492,7 +492,7 @@ class UnassignStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator): self.file, **{ "relating_object": self.file.by_id(self.work_plan), - "product": self.file.by_id(self.load_case), + "products": [self.file.by_id(self.load_case)], }, ) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/style/operator.py b/src/blenderbim/blenderbim/bim/module/style/operator.py index 7dc2b64d58..db679a6cf8 100644 --- a/src/blenderbim/blenderbim/bim/module/style/operator.py +++ b/src/blenderbim/blenderbim/bim/module/style/operator.py @@ -330,16 +330,21 @@ class ActivateExternalStyle(bpy.types.Operator, tool.Ifc.Operator): style_path = Path(tool.Ifc.resolve_uri(external_style.Location)) if style_path.suffix != ".blend": - self.report({"ERROR"}, f"Error loading external style - only Blender external styles are supported") + self.report( + {"ERROR"}, + f"Error loading external style for \"{material.name}\" - only Blender external styles are supported", + ) return {"CANCELLED"} if not style_path.exists(): - self.report({"ERROR"}, f"Error loading external style - cannot find file: '{style_path}'") + self.report( + {"ERROR"}, f"Error loading external style for \"{material.name}\" - cannot find file: '{style_path}'" + ) return {"CANCELLED"} db = tool.Blender.append_data_block(str(style_path), data_block_type, data_block) if not db["data_block"]: - self.report({"ERROR"}, f"Error loading external style - {db['msg']}") + self.report({"ERROR"}, f"Error loading external style for \"{material.name}\" - {db['msg']}") return {"CANCELLED"} self.copy_material_attributes(db["data_block"], material) diff --git a/src/blenderbim/blenderbim/bim/module/style/prop.py b/src/blenderbim/blenderbim/bim/module/style/prop.py index cf17b2ace2..e195c1472a 100644 --- a/src/blenderbim/blenderbim/bim/module/style/prop.py +++ b/src/blenderbim/blenderbim/bim/module/style/prop.py @@ -259,7 +259,7 @@ def update_shading_style(self, context): try: bpy.ops.bim.activate_external_style(material_name=blender_material.name) except RuntimeError as error: - if str(error).startswith("Error: Error loading external style - "): + if str(error).startswith("Error: Error loading external style for "): return raise error diff --git a/src/blenderbim/blenderbim/bim/module/tester/operator.py b/src/blenderbim/blenderbim/bim/module/tester/operator.py index 4819c05d50..8560462eb2 100644 --- a/src/blenderbim/blenderbim/bim/module/tester/operator.py +++ b/src/blenderbim/blenderbim/bim/module/tester/operator.py @@ -27,7 +27,7 @@ import ifctester.reporter import ifcopenshell import blenderbim.tool as tool import blenderbim.bim.handler -from blenderbim.bim.module.tester.data import TesterData +from pathlib import Path class ExecuteIfcTester(bpy.types.Operator, tool.Ifc.Operator): @@ -44,7 +44,7 @@ class ExecuteIfcTester(bpy.types.Operator, tool.Ifc.Operator): with tempfile.TemporaryDirectory() as dirpath: start = time.time() - output = os.path.join(dirpath, "{}.html".format(props.specs)) + output = Path(os.path.join(dirpath, "{}.html".format(props.specs))) filepath = None if props.should_load_from_memory and tool.Ifc.get(): @@ -65,8 +65,15 @@ class ExecuteIfcTester(bpy.types.Operator, tool.Ifc.Operator): if props.generate_html_report: engine = ifctester.reporter.Html(specs) engine.report() - engine.to_file(output) - webbrowser.open("file://" + output) + output_path = output.as_posix() + engine.to_file(output_path) + webbrowser.open(f"file://{output_path}") + + if props.generate_ods_report: + engine = ifctester.reporter.Ods(specs) + engine.report() + output_path = output.with_suffix(".ods").as_posix() + engine.to_file(output_path) report = None report = ifctester.reporter.Json(specs).report()["specifications"] diff --git a/src/blenderbim/blenderbim/bim/module/tester/prop.py b/src/blenderbim/blenderbim/bim/module/tester/prop.py index 05bb2054ad..a82fb233ce 100644 --- a/src/blenderbim/blenderbim/bim/module/tester/prop.py +++ b/src/blenderbim/blenderbim/bim/module/tester/prop.py @@ -52,6 +52,7 @@ class IfcTesterProperties(PropertyGroup): ifc_file: StringProperty(default="", name="IFC File") should_load_from_memory: BoolProperty(default=False, name="Load from Memory") generate_html_report: BoolProperty(default=False, name="Generate HTML report") + generate_ods_report: BoolProperty(default=False, name="Generate ODS report") flag: BoolProperty(default=False, name="Flag Failed Entities") active_specification_index: IntProperty(name="Active Specification Index", update=update_active_specification_index) old_index: IntProperty(name="", default=0) diff --git a/src/blenderbim/blenderbim/bim/module/tester/ui.py b/src/blenderbim/blenderbim/bim/module/tester/ui.py index 899d52415d..b4ae0bcdad 100644 --- a/src/blenderbim/blenderbim/bim/module/tester/ui.py +++ b/src/blenderbim/blenderbim/bim/module/tester/ui.py @@ -44,6 +44,8 @@ class BIM_PT_tester(Panel): row = self.layout.row() row.prop(props, "generate_html_report") row = self.layout.row() + row.prop(props, "generate_ods_report") + row = self.layout.row() row.prop(props, "flag") if not tool.Ifc.get() or not props.should_load_from_memory: diff --git a/src/blenderbim/blenderbim/bim/module/type/operator.py b/src/blenderbim/blenderbim/bim/module/type/operator.py index 43abc6e47b..899dfe4ec1 100644 --- a/src/blenderbim/blenderbim/bim/module/type/operator.py +++ b/src/blenderbim/blenderbim/bim/module/type/operator.py @@ -18,7 +18,9 @@ import bpy import bmesh +import ifcopenshell.util.element import ifcopenshell.util.schema +import ifcopenshell.util.representation import ifcopenshell.util.type import ifcopenshell.api import blenderbim.tool as tool @@ -76,7 +78,7 @@ class UnassignType(bpy.types.Operator): element = tool.Ifc.get_entity(obj) if not element or element.is_a("IfcElementType"): continue - ifcopenshell.api.run("type.unassign_type", self.file, related_object=element) + ifcopenshell.api.run("type.unassign_type", self.file, related_objects=[element]) active_representation = tool.Geometry.get_active_representation(obj) active_context = active_representation.ContextOfItems @@ -171,15 +173,22 @@ class SelectSimilarType(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() objects = bpy.context.selected_objects + + # store relating types to avoid selecting same elements multiple times + relating_types = set() + for related_object in objects: relating_type = ifcopenshell.util.element.get_type(tool.Ifc.get_entity(related_object)) if not relating_type: related_object.select_set(False) continue + relating_types.add(relating_type) + + for relating_type in relating_types: related_objects = ifcopenshell.util.element.get_types(relating_type) - for obj in context.visible_objects: - element = tool.Ifc.get_entity(obj) - if element and element in related_objects: + for element in related_objects: + obj = tool.Ifc.get_object(element) + if obj and obj in context.visible_objects: obj.select_set(True) return {"FINISHED"} @@ -274,7 +283,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator): else: material = self.add_default_material() rel = ifcopenshell.api.run( - "material.assign_material", ifc_file, product=element, type="IfcMaterialLayerSet" + "material.assign_material", ifc_file, products=[element], type="IfcMaterialLayerSet" ) layer_set = rel.RelatingMaterial layer = ifcopenshell.api.run("material.add_layer", ifc_file, layer_set=layer_set, material=material) @@ -352,7 +361,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator): ) rel = ifcopenshell.api.run( - "material.assign_material", ifc_file, product=element, type="IfcMaterialProfileSet" + "material.assign_material", ifc_file, products=[element], type="IfcMaterialProfileSet" ) profile_set = rel.RelatingMaterial material_profile = ifcopenshell.api.run( diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py index ca3b1d4aa3..55032392f7 100644 --- a/src/blenderbim/blenderbim/bim/operator.py +++ b/src/blenderbim/blenderbim/bim/operator.py @@ -357,7 +357,7 @@ class OpenUpstream(bpy.types.Operator): if self.page == "home": webbrowser.open("https://blenderbim.org/") elif self.page == "docs": - webbrowser.open("https://blenderbim.org/docs/") + webbrowser.open("https://docs.blenderbim.org/") elif self.page == "wiki": webbrowser.open("https://wiki.osarch.org/index.php?title=Category:BlenderBIM_Add-on") elif self.page == "community": @@ -936,6 +936,31 @@ class BIM_OT_enum_property_search(bpy.types.Operator): ) +class BIM_OT_select_object(bpy.types.Operator): + bl_idname = "bim.select_object" + bl_label = "Select Object" + bl_options = {"REGISTER", "UNDO"} + obj_name: bpy.props.StringProperty(description="Object Name To Select") + + def execute(self, context): + obj = bpy.data.objects[self.obj_name] + tool.Blender.set_objects_selection(context, obj, [obj], clear_previous_selection=True) + return {"FINISHED"} + + +class BIM_OT_delete_object(bpy.types.Operator): + bl_idname = "bim.delete_object" + bl_label = "Delete Object" + bl_options = {"REGISTER", "UNDO"} + obj_name: bpy.props.StringProperty(description="Object Name To Delete") + + def execute(self, context): + obj = bpy.data.objects[self.obj_name] + with context.temp_override(selected_objects=[obj], active_object=obj): + bpy.ops.bim.override_object_delete(is_batch=False) + return {"FINISHED"} + + class EditBlenderCollection(bpy.types.Operator): bl_idname = "bim.edit_blender_collection" bl_label = "Add or Remove Blender Collection Item" diff --git a/src/blenderbim/blenderbim/bim/prop.py b/src/blenderbim/blenderbim/bim/prop.py index d2309ac97b..5bb6591ff1 100644 --- a/src/blenderbim/blenderbim/bim/prop.py +++ b/src/blenderbim/blenderbim/bim/prop.py @@ -273,6 +273,8 @@ class Attribute(PropertyGroup): def get_value(self): if self.is_optional and self.is_null: return None + if self.data_type == "string": + return self.string_value.replace("\\n", "\n") return getattr(self, str(self.get_value_name()), None) def get_value_default(self): diff --git a/src/blenderbim/blenderbim/bim/schema/classifications/CCI(EN).ifc b/src/blenderbim/blenderbim/bim/schema/classifications/CCI(EN).ifc new file mode 100644 index 0000000000..5049175e34 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/schema/classifications/CCI(EN).ifc @@ -0,0 +1,1288 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); +FILE_NAME('','2024-04-02T18:00:00',(),(),'IfcOpenShell v0.7.0-f7c03db75','IfcOpenShell v0.7.0-f7c03db75',''); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#1=IFCPROJECTLIBRARY($,$,'CCI',$,$,$,$,$,$); +#2=IFCCLASSIFICATION('CCIC','2022-11-09','2022-11-09','CCI','The CCI classification tables','https://cci-collaboration.org/the-standard',$); +#3=IFCRELASSOCIATESCLASSIFICATION('30b4e$ts223eGUqY6ZfKw5',$,$,$,(#1),#2); +#100=IFCCLASSIFICATIONREFERENCE($,'CC - Construction complex',' ',#2,' ',$); +#101=IFCCLASSIFICATIONREFERENCE($,'[CC]A','Residential complex',#100,'construction complex for dwelling',$); +#102=IFCCLASSIFICATIONREFERENCE($,'[CC]B','Health and care complex',#100,'construction complex for promotion of wellness and/or treatment of illness',$); +#103=IFCCLASSIFICATIONREFERENCE($,'[CC]C','Administration, trading, or service complex',#100,'construction complex for immaterial production',$); +#104=IFCCLASSIFICATIONREFERENCE($,'[CC]D','Education or science complex',#100,'construction complex for knowledge production and/or transfer',$); +#105=IFCCLASSIFICATIONREFERENCE($,'[CC]E','Protection or defence complex',#100,'construction complex for protection and defence',$); +#106=IFCCLASSIFICATIONREFERENCE($,'[CC]F','Culture complex',#100,'construction complex for cultural activities',$); +#107=IFCCLASSIFICATIONREFERENCE($,'[CC]G','Recreational and sports complex',#100,'construction complex for social and physical activities and/or mental stimulation',$); +#108=IFCCLASSIFICATIONREFERENCE($,'[CC]L','Energy supply complex',#100,'construction complex for conversion of energy',$); +#109=IFCCLASSIFICATIONREFERENCE($,'[CC]M','Distribution complex',#100,'construction complex for continuous transport of matter, electricity or signals',$); +#110=IFCCLASSIFICATIONREFERENCE($,'[CC]N','Production complex',#100,'construction complex for raw materials extraction or manufacturing',$); +#111=IFCCLASSIFICATIONREFERENCE($,'[CC]P','Processing complex',#100,'construction complex for treating matter or electricity',$); +#112=IFCCLASSIFICATIONREFERENCE($,'[CC]Q','Storage complex',#100,'construction complex for accumulations of matter or electricity',$); +#113=IFCCLASSIFICATIONREFERENCE($,'[CC]R','Traffic complex',#100,'construction complex for discrete transport of people and/or matter',$); +#114=IFCCLASSIFICATIONREFERENCE($,'[CC]T','Land use complex',#100,'construction complex for land-based production or management',$); +#115=IFCCLASSIFICATIONREFERENCE($,'[CC]U','Water use complex',#100,'construction complex for water-based production or management',$); +#116=IFCCLASSIFICATIONREFERENCE($,'[CC]X','Ruins complex',#100,'construction complex being remains',$); +#200=IFCCLASSIFICATIONREFERENCE($,'CE - Construction entity',' ',#2,' ',$); +#201=IFCCLASSIFICATIONREFERENCE($,'[CE]A?','Residential building',#200,'construction entity for human living',$); +#202=IFCCLASSIFICATIONREFERENCE($,'[CE]AA','Single-dwelling house',#201,'residential building for one household',$); +#203=IFCCLASSIFICATIONREFERENCE($,'[CE]AB','Dual-dwelling building',#201,'residential building for two households',$); +#204=IFCCLASSIFICATIONREFERENCE($,'[CE]AC','Multi-dwelling building',#201,'residential building for three or more households',$); +#205=IFCCLASSIFICATIONREFERENCE($,'[CE]AD','Lodging building',#201,'residential building for temporary living',$); +#206=IFCCLASSIFICATIONREFERENCE($,'[CE]B?','Physical needs building',#200,'construction entity for human or animal wellbeing',$); +#207=IFCCLASSIFICATIONREFERENCE($,'[CE]BA','Care building',#206,'physical needs building for treatment of persons',$); +#208=IFCCLASSIFICATIONREFERENCE($,'[CE]BB','Care home building',#206,'physical needs building for long-term care',$); +#209=IFCCLASSIFICATIONREFERENCE($,'[CE]BD','Animal hospital',#206,'physical needs building for treatment of animals',$); +#210=IFCCLASSIFICATIONREFERENCE($,'[CE]BE','Toilet building',#206,'physical needs building for persons to relieve themselves',$); +#211=IFCCLASSIFICATIONREFERENCE($,'[CE]BF','Baths building',#206,'physical needs building for persons to wash themselves',$); +#212=IFCCLASSIFICATIONREFERENCE($,'[CE]C?','Administration, trading, or service building',#200,'construction entity for immaterial production',$); +#213=IFCCLASSIFICATIONREFERENCE($,'[CE]CA','Office building',#212,'administration building for production or interpretation of information',$); +#214=IFCCLASSIFICATIONREFERENCE($,'[CE]CB','Service building',#212,'service building for providing non-acute assistance or advice',$); +#215=IFCCLASSIFICATIONREFERENCE($,'[CE]CC','Emergency service building',#212,'service building for providing acute assistance or advice',$); +#216=IFCCLASSIFICATIONREFERENCE($,'[CE]CD','Shop building',#212,'service building for sale of goods',$); +#217=IFCCLASSIFICATIONREFERENCE($,'[CE]CE','Exhibition hall',#212,'trading building for display of goods and for assemblies',$); +#218=IFCCLASSIFICATIONREFERENCE($,'[CE]CF','Restaurant building',#212,'service building for serving food and/or beverages',$); +#219=IFCCLASSIFICATIONREFERENCE($,'[CE]CG','Operations building',#212,'service building for monitoring and controlling of processes',$); +#220=IFCCLASSIFICATIONREFERENCE($,'[CE]D?','Education or research building',#200,'construction entity for knowledge production and transfer',$); +#221=IFCCLASSIFICATIONREFERENCE($,'[CE]DA','Preschool building',#220,'education building for small children',$); +#222=IFCCLASSIFICATIONREFERENCE($,'[CE]DB','School building',#220,'education building for basic education',$); +#223=IFCCLASSIFICATIONREFERENCE($,'[CE]DC','Course centre building',#220,'education building for series of lectures or lessons in a particular subject',$); +#224=IFCCLASSIFICATIONREFERENCE($,'[CE]DD','University building',#220,'education building for further education',$); +#225=IFCCLASSIFICATIONREFERENCE($,'[CE]DE','Laboratory building',#220,'research building for experiments',$); +#226=IFCCLASSIFICATIONREFERENCE($,'[CE]F?','Cultural building',#200,'construction entity for artistic activities',$); +#227=IFCCLASSIFICATIONREFERENCE($,'[CE]FA','Theatre building',#226,'cultural building for oral performances',$); +#228=IFCCLASSIFICATIONREFERENCE($,'[CE]FB','Concert building',#226,'cultural building for musical performances',$); +#229=IFCCLASSIFICATIONREFERENCE($,'[CE]FC','Exhibition building',#226,'cultural building for display of art',$); +#230=IFCCLASSIFICATIONREFERENCE($,'[CE]FD','Planetarium',#226,'cultural building for displaying the night sky',$); +#231=IFCCLASSIFICATIONREFERENCE($,'[CE]FE','Cinema building',#226,'cultural building for displaying films',$); +#232=IFCCLASSIFICATIONREFERENCE($,'[CE]FF','Library building',#226,'cultural building providing books and other media',$); +#233=IFCCLASSIFICATIONREFERENCE($,'[CE]FG','Cultural center',#226,'cultural building for multiple use',$); +#234=IFCCLASSIFICATIONREFERENCE($,'[CE]FH','Casino',#226,'cultural building for gambling',$); +#235=IFCCLASSIFICATIONREFERENCE($,'[CE]FJ','Studio',#226,'cultural building for production of art and entertainment',$); +#236=IFCCLASSIFICATIONREFERENCE($,'[CE]G?','Recreational facility',#200,'construction entity for social activities and/or mental stimulation',$); +#237=IFCCLASSIFICATIONREFERENCE($,'[CE]GA','Allotment building',#236,'recreational facility for gardening tools',$); +#238=IFCCLASSIFICATIONREFERENCE($,'[CE]GB','Gazebo',#236,'recreational facility for garden retreat',$); +#239=IFCCLASSIFICATIONREFERENCE($,'[CE]GC','Cantilever roof',#236,'recreational facility for protection from sun and precipitation',$); +#240=IFCCLASSIFICATIONREFERENCE($,'[CE]GD','Playhouse',#236,'recreational facility for child play',$); +#241=IFCCLASSIFICATIONREFERENCE($,'[CE]GE','Wind shelter',#236,'recreational facility for protection from wind',$); +#242=IFCCLASSIFICATIONREFERENCE($,'[CE]GF','Observation tower',#236,'recreational facility for raised viewing position',$); +#243=IFCCLASSIFICATIONREFERENCE($,'[CE]GG','Hunting tower',#236,'recreational facility for raised shooting position',$); +#244=IFCCLASSIFICATIONREFERENCE($,'[CE]GH','Amusement facility',#236,'recreational facility for physical exaltation',$); +#245=IFCCLASSIFICATIONREFERENCE($,'[CE]GJ','Garden',#236,'recreational facility for private outdoor activities',$); +#246=IFCCLASSIFICATIONREFERENCE($,'[CE]GK','Park',#236,'recreational facility for public outdoor activities',$); +#247=IFCCLASSIFICATIONREFERENCE($,'[CE]GL','Playground',#236,'recreational facility for children''s activities for enjoyment and recreation',$); +#248=IFCCLASSIFICATIONREFERENCE($,'[CE]GM','Campsite',#236,'recreational facility for tents and recreational vehicles',$); +#249=IFCCLASSIFICATIONREFERENCE($,'[CE]GN','Bathing site',#236,'recreational facility for outdoor water activities',$); +#250=IFCCLASSIFICATIONREFERENCE($,'[CE]GP','Changing building',#236,'recreational facility for persons to change clothing',$); +#251=IFCCLASSIFICATIONREFERENCE($,'[CE]H?','Sports facility',#200,'construction entity for physical activities',$); +#252=IFCCLASSIFICATIONREFERENCE($,'[CE]HA','Multi-purpose sports building',#251,'sports facility for multiple activities',$); +#253=IFCCLASSIFICATIONREFERENCE($,'[CE]HB','Swimming hall',#251,'sports facility for indoor water activities',$); +#254=IFCCLASSIFICATIONREFERENCE($,'[CE]HC','Riding school',#251,'sports facility for equestrian activities',$); +#255=IFCCLASSIFICATIONREFERENCE($,'[CE]HD','Diving tower',#251,'sports facility for diving',$); +#256=IFCCLASSIFICATIONREFERENCE($,'[CE]HE','Bowling hall',#251,'sports facility for bowling',$); +#257=IFCCLASSIFICATIONREFERENCE($,'[CE]HF','Skating rink',#251,'sports facility for ice skating',$); +#258=IFCCLASSIFICATIONREFERENCE($,'[CE]HG','Ball sport building',#251,'sports facility for boll sports',$); +#259=IFCCLASSIFICATIONREFERENCE($,'[CE]HH','Stadium',#251,'sports facility with spectator facilities',$); +#260=IFCCLASSIFICATIONREFERENCE($,'[CE]HJ','Playing field',#251,'sports facility for ball games',$); +#261=IFCCLASSIFICATIONREFERENCE($,'[CE]HK','Racetrack',#251,'sports facility for racing competitions',$); +#262=IFCCLASSIFICATIONREFERENCE($,'[CE]HL','Track and field area',#251,'sports facility for track and field',$); +#263=IFCCLASSIFICATIONREFERENCE($,'[CE]HM','Activity park',#251,'sports facility for multiple or specific outdoor activity(ies)',$); +#264=IFCCLASSIFICATIONREFERENCE($,'[CE]HN','Downhill skiing slope',#251,'sports facility for downhill skiing',$); +#265=IFCCLASSIFICATIONREFERENCE($,'[CE]HP','Exercise track',#251,'sports facility for running',$); +#266=IFCCLASSIFICATIONREFERENCE($,'[CE]HQ','Foot path',#251,'sports facility for walking',$); +#267=IFCCLASSIFICATIONREFERENCE($,'[CE]HR','Golf course',#251,'sports facility for golf',$); +#268=IFCCLASSIFICATIONREFERENCE($,'[CE]J?','Assembly facility',#200,'construction entity for groups of people',$); +#269=IFCCLASSIFICATIONREFERENCE($,'[CE]JA','Activity centre',#268,'assembly facility for common activities',$); +#270=IFCCLASSIFICATIONREFERENCE($,'[CE]JB','Community hall',#268,'assembly facility for an association',$); +#271=IFCCLASSIFICATIONREFERENCE($,'[CE]JC','Ceremonial building',#268,'assembly facility for a religious community',$); +#272=IFCCLASSIFICATIONREFERENCE($,'[CE]JD','Stand',#268,'assembly facility for an audience',$); +#273=IFCCLASSIFICATIONREFERENCE($,'[CE]JE','Personnel building',#268,'assembly facility for staff',$); +#274=IFCCLASSIFICATIONREFERENCE($,'[CE]JF','Square',#268,'assembly facility for outdoors urban activities',$); +#275=IFCCLASSIFICATIONREFERENCE($,'[CE]JG','Training site',#268,'assembly facility for practicing competencies',$); +#276=IFCCLASSIFICATIONREFERENCE($,'[CE]JH','School yard',#268,'assembly facility for school persons',$); +#277=IFCCLASSIFICATIONREFERENCE($,'[CE]K?','Access or barrier facility',#200,'construction entity for allowing or preventing movement',$); +#278=IFCCLASSIFICATIONREFERENCE($,'[CE]KA','Passageway',#277,'access facility for horizontal movement between two or more buildings',$); +#279=IFCCLASSIFICATIONREFERENCE($,'[CE]KB','Staircase tower',#277,'access facility for non-automated vertical movement between two or more storeys',$); +#280=IFCCLASSIFICATIONREFERENCE($,'[CE]KC','Lift tower',#277,'access facility for automated vertical movement between two or more levels',$); +#281=IFCCLASSIFICATIONREFERENCE($,'[CE]KD','Gatehouse',#277,'access facility to an open area',$); +#282=IFCCLASSIFICATIONREFERENCE($,'[CE]KE','Tollgate',#277,'access facility to a road',$); +#283=IFCCLASSIFICATIONREFERENCE($,'[CE]KF','Barrier',#277,'barrier facility',$); +#284=IFCCLASSIFICATIONREFERENCE($,'[CE]L?','Energy supply facility',#200,'construction entity for conversion of energy',$); +#285=IFCCLASSIFICATIONREFERENCE($,'[CE]LA','Power station',#284,'energy supply facility for electrical power',$); +#286=IFCCLASSIFICATIONREFERENCE($,'[CE]LB','Heating plant',#284,'energy supply facility for hot liquid',$); +#287=IFCCLASSIFICATIONREFERENCE($,'[CE]LC','Combined heat and power station',#284,'energy supply facility for hot liquid and electrical power',$); +#288=IFCCLASSIFICATIONREFERENCE($,'[CE]LD','Cooling plant',#284,'energy supply facility for cold liquid',$); +#289=IFCCLASSIFICATIONREFERENCE($,'[CE]M?','Distribution facility',#200,'construction entity for distribution and communication between persons or systems',$); +#290=IFCCLASSIFICATIONREFERENCE($,'[CE]MA','Control tower',#289,'distribution facility for control, regulation, and monitoring',$); +#291=IFCCLASSIFICATIONREFERENCE($,'[CE]MB','Aerial mast',#289,'distribution facility for electromagnetic waves',$); +#292=IFCCLASSIFICATIONREFERENCE($,'[CE]MC','Lighthouse',#289,'distribution facility for optical signals',$); +#293=IFCCLASSIFICATIONREFERENCE($,'[CE]MD','Bell tower',#289,'distribution facility for audio signals',$); +#294=IFCCLASSIFICATIONREFERENCE($,'[CE]ME','Overhead power line',#289,'distribution facility for electrical power above ground',$); +#295=IFCCLASSIFICATIONREFERENCE($,'[CE]MF','Power cabling',#289,'distribution facility for electrical power in ground',$); +#296=IFCCLASSIFICATIONREFERENCE($,'[CE]MG','Overhead signal line',#289,'distribution facility for signals above ground',$); +#297=IFCCLASSIFICATIONREFERENCE($,'[CE]MH','Signal cabling',#289,'distribution facility for signals in ground',$); +#298=IFCCLASSIFICATIONREFERENCE($,'[CE]MJ','Piping',#289,'distribution facility for fluids in a closed environment',$); +#299=IFCCLASSIFICATIONREFERENCE($,'[CE]MK','Canal',#289,'distribution facility for fluids in an open environment',$); +#300=IFCCLASSIFICATIONREFERENCE($,'[CE]ML','Heating piping',#289,'distribution facility for heating fluid',$); +#301=IFCCLASSIFICATIONREFERENCE($,'[CE]MM','Cooling piping',#289,'distribution facility for cooling fluid',$); +#302=IFCCLASSIFICATIONREFERENCE($,'[CE]MN','Distribution node facility',#289,'distribution facility for connecting parts of the network',$); +#303=IFCCLASSIFICATIONREFERENCE($,'[CE]MP','Conveyor',#289,'distribution facility for moving matter over larger distances',$); +#304=IFCCLASSIFICATIONREFERENCE($,'[CE]MQ','Cable car',#289,'distribution facility for moving people over larger distances',$); +#305=IFCCLASSIFICATIONREFERENCE($,'[CE]MR','Garbage suction facility',#289,'distribution facility for solid waste',$); +#306=IFCCLASSIFICATIONREFERENCE($,'[CE]N?','Production facility',#200,'construction entity for raw materials extraction or for manufacturing',$); +#307=IFCCLASSIFICATIONREFERENCE($,'[CE]NA','Mine',#306,'production facility for solid matter used for further refinement',$); +#308=IFCCLASSIFICATIONREFERENCE($,'[CE]NB','Gas or liquid extraction facility',#306,'production facility for fluid matter used for further refinement',$); +#309=IFCCLASSIFICATIONREFERENCE($,'[CE]NC','Refinement facility',#306,'production facility for matter refinement',$); +#310=IFCCLASSIFICATIONREFERENCE($,'[CE]ND','Workshop',#306,'production facility for fabrication or repair of products',$); +#311=IFCCLASSIFICATIONREFERENCE($,'[CE]NE','Greenhouse',#306,'production facility for indoor plant production',$); +#312=IFCCLASSIFICATIONREFERENCE($,'[CE]P?','Processing facility',#200,'construction entity for treating matter or electricity',$); +#313=IFCCLASSIFICATIONREFERENCE($,'[CE]PA','Pumping station',#312,'processing facility to increase pressure',$); +#314=IFCCLASSIFICATIONREFERENCE($,'[CE]PB','Treatment facility',#312,'processing facility for treating matter',$); +#315=IFCCLASSIFICATIONREFERENCE($,'[CE]PC','Incinerator facility',#312,'processing facility for destruction by burning',$); +#316=IFCCLASSIFICATIONREFERENCE($,'[CE]PD','Washing facility',#312,'processing facility for cleaning objects',$); +#317=IFCCLASSIFICATIONREFERENCE($,'[CE]PE','Print shop',#312,'processing facility for printing',$); +#318=IFCCLASSIFICATIONREFERENCE($,'[CE]PF','Mail sorting terminal',#312,'processing facility for mail sorting',$); +#319=IFCCLASSIFICATIONREFERENCE($,'[CE]PG','Measuring station',#312,'processing facility for surveillance and/or data capture',$); +#320=IFCCLASSIFICATIONREFERENCE($,'[CE]PH','Electrical power treatment facility',#312,'processing facility for treating electrical power',$); +#321=IFCCLASSIFICATIONREFERENCE($,'[CE]Q?','Storage facility',#200,'construction entity for accumulations of matter or energy',$); +#322=IFCCLASSIFICATIONREFERENCE($,'[CE]QA','Raw materials depot',#321,'storage facility for matter used for further refinement',$); +#323=IFCCLASSIFICATIONREFERENCE($,'[CE]QB','Products storage',#321,'storage facility for products',$); +#324=IFCCLASSIFICATIONREFERENCE($,'[CE]QC','Equipment building',#321,'storage facility for moveable technical equipment',$); +#325=IFCCLASSIFICATIONREFERENCE($,'[CE]QD','Garage',#321,'storage facility for four-wheeled or larger road vehicles',$); +#326=IFCCLASSIFICATIONREFERENCE($,'[CE]QE','Bicycle parking',#321,'storage facility for two-wheeled road vehicles',$); +#327=IFCCLASSIFICATIONREFERENCE($,'[CE]QF','Train garage',#321,'storage facility for railway vehicles',$); +#328=IFCCLASSIFICATIONREFERENCE($,'[CE]QG','Hangar',#321,'storage facility for aircraft',$); +#329=IFCCLASSIFICATIONREFERENCE($,'[CE]QH','Boathouse',#321,'storage facility for watercraft',$); +#330=IFCCLASSIFICATIONREFERENCE($,'[CE]QJ','Container space',#321,'storage facility for containers',$); +#331=IFCCLASSIFICATIONREFERENCE($,'[CE]QK','Tank',#321,'storage facility for liquid, gas or other masses in sealed units',$); +#332=IFCCLASSIFICATIONREFERENCE($,'[CE]QL','Landfill site',#321,'storage facility for loosened earth material',$); +#333=IFCCLASSIFICATIONREFERENCE($,'[CE]QM','Geological repository',#321,'storage facility for hazardous material underground',$); +#334=IFCCLASSIFICATIONREFERENCE($,'[CE]QN','Animal house',#321,'storage facility for animals',$); +#335=IFCCLASSIFICATIONREFERENCE($,'[CE]QP','Data storage',#321,'storage facility for data',$); +#336=IFCCLASSIFICATIONREFERENCE($,'[CE]QQ','Energy storage',#321,'storage facility for energy',$); +#337=IFCCLASSIFICATIONREFERENCE($,'[CE]QZ','Burial site',#321,'storage facility for the deceased',$); +#338=IFCCLASSIFICATIONREFERENCE($,'[CE]R?','Traffic facility',#200,'construction entity for discrete transport of people and/or matter',$); +#339=IFCCLASSIFICATIONREFERENCE($,'[CE]RA','Road',#338,'traffic facility on land for non-track bound vehicles',$); +#340=IFCCLASSIFICATIONREFERENCE($,'[CE]RB','Railroad',#338,'traffic facility for long distance transport with rail bound vehicles, with level crossings with other traffic',$); +#341=IFCCLASSIFICATIONREFERENCE($,'[CE]RC','Light rail transit (LRT)',#338,'traffic facility for urban transport with rail bound vehicles, with level crossings with other traffic',$); +#342=IFCCLASSIFICATIONREFERENCE($,'[CE]RD','Rapid transit',#338,'traffic facility for urban transport with rail bound vehicles, without level crossings with other traffic',$); +#343=IFCCLASSIFICATIONREFERENCE($,'[CE]RE','Funicular',#338,'traffic facility for hillside railway traffic',$); +#344=IFCCLASSIFICATIONREFERENCE($,'[CE]RF','Footpath',#338,'traffic facility for pedestrian traffic',$); +#345=IFCCLASSIFICATIONREFERENCE($,'[CE]RG','Foot and bicycle path',#338,'traffic facility for bicycle and pedestrian traffic',$); +#346=IFCCLASSIFICATIONREFERENCE($,'[CE]RH','Bicycle path',#338,'traffic facility for bicycle traffic',$); +#347=IFCCLASSIFICATIONREFERENCE($,'[CE]RJ','Airport way',#338,'traffic facility for air traffic starting, landing and ground movements',$); +#348=IFCCLASSIFICATIONREFERENCE($,'[CE]RK','Bridge',#338,'traffic facility to cross over an obstacle',$); +#349=IFCCLASSIFICATIONREFERENCE($,'[CE]RL','Transport tunnel',#338,'traffic facility to cross under or through an obstacle',$); +#350=IFCCLASSIFICATIONREFERENCE($,'[CE]RM','Transport canal',#338,'traffic facility for water-borne traffic in ground',$); +#351=IFCCLASSIFICATIONREFERENCE($,'[CE]RN','Aqueduct',#338,'traffic facility for water-borne traffic above ground',$); +#352=IFCCLASSIFICATIONREFERENCE($,'[CE]RP','Bridleway',#338,'traffic facility for equestrian traffic',$); +#353=IFCCLASSIFICATIONREFERENCE($,'[CE]RQ','Wildlife crossing',#338,'traffic facility for wildlife bypassing man-made entities',$); +#354=IFCCLASSIFICATIONREFERENCE($,'[CE]S?','Traffic transfer facility',#200,'construction entity for connecting traffic facilities',$); +#355=IFCCLASSIFICATIONREFERENCE($,'[CE]SA','Train station',#354,'traffic transfer facility between rail transport and other traffic',$); +#356=IFCCLASSIFICATIONREFERENCE($,'[CE]SB','Railway platform',#354,'traffic transfer facility for embarking a rail bound vehicle',$); +#357=IFCCLASSIFICATIONREFERENCE($,'[CE]SC','Level crossing',#354,'traffic transfer facility between railroads and roads or paths',$); +#358=IFCCLASSIFICATIONREFERENCE($,'[CE]SD','Travel centre',#354,'traffic transfer facility between multiple means of transport',$); +#359=IFCCLASSIFICATIONREFERENCE($,'[CE]SE','Airport terminal',#354,'traffic transfer facility between air transport and other traffic',$); +#360=IFCCLASSIFICATIONREFERENCE($,'[CE]SF','Bus station',#354,'traffic transfer facility between bus transport and other traffic',$); +#361=IFCCLASSIFICATIONREFERENCE($,'[CE]SG','Truck terminal',#354,'traffic transfer facility between truck transport and other traffic',$); +#362=IFCCLASSIFICATIONREFERENCE($,'[CE]SH','Taxi stand',#354,'traffic transfer facility between taxi transport and other traffic',$); +#363=IFCCLASSIFICATIONREFERENCE($,'[CE]SJ','Sea terminal',#354,'traffic transfer facility between water-borne and other traffic',$); +#364=IFCCLASSIFICATIONREFERENCE($,'[CE]T?','Land use facility',#200,'construction entity for land-based production',$); +#365=IFCCLASSIFICATIONREFERENCE($,'[CE]TA','Field',#364,'land use facility for annual yield',$); +#366=IFCCLASSIFICATIONREFERENCE($,'[CE]TB','Forest',#364,'land use facility for long-term yield',$); +#367=IFCCLASSIFICATIONREFERENCE($,'[CE]U?','Water control facility',#200,'construction entity for water management',$); +#368=IFCCLASSIFICATIONREFERENCE($,'[CE]UA','Dam',#367,'water control facility to retain a water level',$); +#369=IFCCLASSIFICATIONREFERENCE($,'[CE]UB','Barrage',#367,'water control facility to retain a water level with an overflow',$); +#370=IFCCLASSIFICATIONREFERENCE($,'[CE]UC','Sluice',#367,'water control facility to adjust water level',$); +#371=IFCCLASSIFICATIONREFERENCE($,'[CE]UD','Mole',#367,'water control facility to dampen wave movements',$); +#372=IFCCLASSIFICATIONREFERENCE($,'[CE]UF','Dry dock',#367,'water control facility to lower the water level to zero',$); +#373=IFCCLASSIFICATIONREFERENCE($,'[CE]UG','Dyke',#367,'water control facility to prevent overflow',$); +#374=IFCCLASSIFICATIONREFERENCE($,'[CE]UH','Gate',#367,'water control facility to control a water flow',$); +#375=IFCCLASSIFICATIONREFERENCE($,'[CE]X?','Ruins',#200,'construction entity permanently lacking original use',$); +#376=IFCCLASSIFICATIONREFERENCE($,'[CE]XA','Contemporary building ruins',#375,'ruins lacking cultural and historic value',$); +#377=IFCCLASSIFICATIONREFERENCE($,'[CE]XB','Cultural heritage building ruins',#375,'ruins of cultural and historic value of a building',$); +#378=IFCCLASSIFICATIONREFERENCE($,'[CE]XC','Cultural heritage site ruins',#375,'ruins of cultural and historic value of a site',$); +#379=IFCCLASSIFICATIONREFERENCE($,'[CE]XD','Cultural heritage grave ruins',#375,'ruins of cultural and historic value of a grave or grave site',$); +#380=IFCCLASSIFICATIONREFERENCE($,'[CE]Y?','Ornamental entity',#200,'construction entity with symbolic or artistic meaning',$); +#381=IFCCLASSIFICATIONREFERENCE($,'[CE]YA','Monument',#380,'ornamental entity for remembrance of person or event',$); +#382=IFCCLASSIFICATIONREFERENCE($,'[CE]YB','Fountain',#380,'ornamental entity with water',$); +#383=IFCCLASSIFICATIONREFERENCE($,'[CE]YC','Statue',#380,'ornamental entity in freestanding sculptural form',$); +#384=IFCCLASSIFICATIONREFERENCE($,'[CE]Z?','Unknown construction entity',#200,'construction entity with unknown use',$); +#385=IFCCLASSIFICATIONREFERENCE($,'[CE]ZZ','Unknown construction entity',#384,'unknown construction entity',$); +#400=IFCCLASSIFICATIONREFERENCE($,'CS - Built space',' ',#2,' ',$); +#401=IFCCLASSIFICATIONREFERENCE($,'[CS]A??','Space for human dwelling',#400,'space designed for human dwelling and related activities',$); +#402=IFCCLASSIFICATIONREFERENCE($,'[CS]AA?','Occupiable space',#401,'space for human dwelling for rest, relaxation, recreation or waiting',$); +#403=IFCCLASSIFICATIONREFERENCE($,'[CS]AAA','Room',#402,'occupiable space for personal use',$); +#404=IFCCLASSIFICATIONREFERENCE($,'[CS]AAB','Living space',#402,'occupiable space for shared use',$); +#405=IFCCLASSIFICATIONREFERENCE($,'[CS]AAC','Waiting space',#402,'occupiable space for waiting',$); +#406=IFCCLASSIFICATIONREFERENCE($,'[CS]AAD','Courtyard',#402,'occupiable space outdoors entirely or partly surrounded by buildings',$); +#407=IFCCLASSIFICATIONREFERENCE($,'[CS]AB?','Personal hygiene space',#401,'space for human dwelling for personal hygiene use',$); +#408=IFCCLASSIFICATIONREFERENCE($,'[CS]ABA','Bathroom',#407,'personal hygiene space enabling persons to wash and possibly relieve themselves',$); +#409=IFCCLASSIFICATIONREFERENCE($,'[CS]ABB','Toilet',#407,'personal hygiene space enabling persons to relieve themselves',$); +#410=IFCCLASSIFICATIONREFERENCE($,'[CS]ABC','Baby changing room',#407,'personal hygiene space for young children',$); +#411=IFCCLASSIFICATIONREFERENCE($,'[CS]ABD','Changing room',#407,'personal hygiene space for changing clothes',$); +#412=IFCCLASSIFICATIONREFERENCE($,'[CS]ABE','Shower',#407,'personal hygiene space enabling persons to wash themselves',$); +#413=IFCCLASSIFICATIONREFERENCE($,'[CS]ABF','Sauna',#407,'personal hygiene space enabling persons to take a sweat-inducing bath',$); +#414=IFCCLASSIFICATIONREFERENCE($,'[CS]AC?','Isolation space',#401,'space for human dwelling for isolating persons',$); +#415=IFCCLASSIFICATIONREFERENCE($,'[CS]ACA','Shelter',#414,'isolation space to protect persons from hazardous of dangerous situations',$); +#416=IFCCLASSIFICATIONREFERENCE($,'[CS]ACB','Cell',#414,'isolation space to separate persons from other persons',$); +#417=IFCCLASSIFICATIONREFERENCE($,'[CS]B??','Space for human activity',#400,'space designed for humans’ leisure and business activities',$); +#418=IFCCLASSIFICATIONREFERENCE($,'[CS]BA?','Workspace',#417,'space for human activity for producing an immaterial result',$); +#419=IFCCLASSIFICATIONREFERENCE($,'[CS]BAA','Office',#418,'workspace for the production, processing and interpretation of information',$); +#420=IFCCLASSIFICATIONREFERENCE($,'[CS]BAB','Meeting space',#418,'workspace for meetings',$); +#421=IFCCLASSIFICATIONREFERENCE($,'[CS]BAC','Reception',#418,'workspace for receiving guests and goods',$); +#422=IFCCLASSIFICATIONREFERENCE($,'[CS]BAD','Sales area',#418,'workspace for selling goods and services',$); +#423=IFCCLASSIFICATIONREFERENCE($,'[CS]BAE','Teaching space',#418,'workspace conveying knowledge and competencies',$); +#424=IFCCLASSIFICATIONREFERENCE($,'[CS]BAF','Study',#418,'workspace for reading',$); +#425=IFCCLASSIFICATIONREFERENCE($,'[CS]BAG','Laundry',#418,'workspace for laundering clothes and linen',$); +#426=IFCCLASSIFICATIONREFERENCE($,'[CS]BAH','Treatment space',#418,'workspace for health-related and other treatment and care of individuals',$); +#427=IFCCLASSIFICATIONREFERENCE($,'[CS]BAJ','Examining space',#418,'workspace for diagnosis',$); +#428=IFCCLASSIFICATIONREFERENCE($,'[CS]BAK','Observation space',#418,'workspace for monitoring individuals',$); +#429=IFCCLASSIFICATIONREFERENCE($,'[CS]BAL','Viewing space',#418,'workspace for observing processes and persons in adjacent space',$); +#430=IFCCLASSIFICATIONREFERENCE($,'[CS]BAM','Media workshop',#418,'workspace for recording and editing media',$); +#431=IFCCLASSIFICATIONREFERENCE($,'[CS]BB?','Production space',#417,'space for human activity for producing a material result',$); +#432=IFCCLASSIFICATIONREFERENCE($,'[CS]BBA','Kitchen',#431,'production space for working with foodstuffs',$); +#433=IFCCLASSIFICATIONREFERENCE($,'[CS]BBB','Materials workshop',#431,'production space for working with materials',$); +#434=IFCCLASSIFICATIONREFERENCE($,'[CS]BBC','Machine shop',#431,'production space for working with machinery and equipment',$); +#435=IFCCLASSIFICATIONREFERENCE($,'[CS]BBD','Studio',#431,'production space for working on art and crafts',$); +#436=IFCCLASSIFICATIONREFERENCE($,'[CS]BBE','Packing space',#431,'production space for sorting and packing objects',$); +#437=IFCCLASSIFICATIONREFERENCE($,'[CS]BC?','Laboratory',#417,'space for human activity for performing analyses and tests',$); +#438=IFCCLASSIFICATIONREFERENCE($,'[CS]BCA','Technical laboratory',#437,'laboratory for technical analyses',$); +#439=IFCCLASSIFICATIONREFERENCE($,'[CS]BCB','Materials laboratory',#437,'laboratory for studying materials',$); +#440=IFCCLASSIFICATIONREFERENCE($,'[CS]BCC','Biology laboratory',#437,'laboratory for biological analysis',$); +#441=IFCCLASSIFICATIONREFERENCE($,'[CS]BD?','Practice space',#417,'space for human activity of sport or culture',$); +#442=IFCCLASSIFICATIONREFERENCE($,'[CS]BDA','Sports hall',#441,'practice space for sports',$); +#443=IFCCLASSIFICATIONREFERENCE($,'[CS]BDB','Training room',#441,'practice space for training strength, condition and balance',$); +#444=IFCCLASSIFICATIONREFERENCE($,'[CS]BDC','Rehearsal room',#441,'practice space for practicing artistic competencies',$); +#445=IFCCLASSIFICATIONREFERENCE($,'[CS]BDD','Stage',#441,'practice space for performance artistic works',$); +#446=IFCCLASSIFICATIONREFERENCE($,'[CS]BDE','Playroom',#441,'practice space for children',$); +#447=IFCCLASSIFICATIONREFERENCE($,'[CS]BE?','Assembly space',#417,'space for human activity for mass assemblies of people/persons',$); +#448=IFCCLASSIFICATIONREFERENCE($,'[CS]BEA','Dining space',#447,'assembly space for consumption of food',$); +#449=IFCCLASSIFICATIONREFERENCE($,'[CS]BEB','Hall',#447,'assembly space for social events',$); +#450=IFCCLASSIFICATIONREFERENCE($,'[CS]BEC','Gallery',#447,'assembly space for displaying objects',$); +#451=IFCCLASSIFICATIONREFERENCE($,'[CS]BED','Religious faith space',#447,'assembly space for religious activities',$); +#452=IFCCLASSIFICATIONREFERENCE($,'[CS]C??','Storage space',#400,'space designed for storage of materials, equipment and organisms',$); +#453=IFCCLASSIFICATIONREFERENCE($,'[CS]CA?','Material storage space',#452,'storage space for resources',$); +#454=IFCCLASSIFICATIONREFERENCE($,'[CS]CAA','Refuse storage space',#453,'material storage space for refused and rejected materials or objects',$); +#455=IFCCLASSIFICATIONREFERENCE($,'[CS]CAB','Chemicals storage space',#453,'material storage space for chemicals',$); +#456=IFCCLASSIFICATIONREFERENCE($,'[CS]CAC','Medicine storage space',#453,'material storage space for medicines',$); +#457=IFCCLASSIFICATIONREFERENCE($,'[CS]CAD','Archive storage space',#453,'material storage space for physical documents or media',$); +#458=IFCCLASSIFICATIONREFERENCE($,'[CS]CAE','Fluid storage space',#453,'material storage space for fluid',$); +#459=IFCCLASSIFICATIONREFERENCE($,'[CS]CAF','Raw materials storage space',#453,'material storage space for substances or materials for use in production processes',$); +#460=IFCCLASSIFICATIONREFERENCE($,'[CS]CAG','Food storage space',#453,'material storage space for food and drinks',$); +#461=IFCCLASSIFICATIONREFERENCE($,'[CS]CAH','Secure storage space',#453,'material storage space for valuable objects',$); +#462=IFCCLASSIFICATIONREFERENCE($,'[CS]CAJ','Packaging storage space',#453,'material storage space of materials for packaging',$); +#463=IFCCLASSIFICATIONREFERENCE($,'[CS]CAK','Clothes storage space',#453,'material storage space for clothing, shoes and linen',$); +#464=IFCCLASSIFICATIONREFERENCE($,'[CS]CAL','Building materials storage space',#453,'material storage space for building materials',$); +#465=IFCCLASSIFICATIONREFERENCE($,'[CS]CAM','Explosives space',#453,'material storage space for explosives',$); +#466=IFCCLASSIFICATIONREFERENCE($,'[CS]CAN','Stockroom',#453,'material storage space for products, goods and cargo',$); +#467=IFCCLASSIFICATIONREFERENCE($,'[CS]CB?','Equipment storage space',#452,'storage space for moveable technical equipment',$); +#468=IFCCLASSIFICATIONREFERENCE($,'[CS]CBA','Furniture storage space',#467,'equipment storage space of objects for fitting out (of spaces)',$); +#469=IFCCLASSIFICATIONREFERENCE($,'[CS]CBB','Tool storage space',#467,'equipment storage space for tools',$); +#470=IFCCLASSIFICATIONREFERENCE($,'[CS]CBC','Instrument storage space',#467,'equipment storage space for technical instruments',$); +#471=IFCCLASSIFICATIONREFERENCE($,'[CS]CBD','Vehicle storage space',#467,'equipment storage space for modes of transport',$); +#472=IFCCLASSIFICATIONREFERENCE($,'[CS]CC?','Animal enclosure',#452,'storage space for enclosing animals',$); +#473=IFCCLASSIFICATIONREFERENCE($,'[CS]CCA','Cage',#472,'animal enclosure for keeping animals locked up',$); +#474=IFCCLASSIFICATIONREFERENCE($,'[CS]CCB','Stall',#472,'animal enclosure for keeping animals fenced in',$); +#475=IFCCLASSIFICATIONREFERENCE($,'[CS]CCC','Stable',#472,'animal enclosure for providing shelter',$); +#476=IFCCLASSIFICATIONREFERENCE($,'[CS]CCD','Water enclosure',#472,'animal enclosure for keeping animals in water',$); +#477=IFCCLASSIFICATIONREFERENCE($,'[CS]CD?','Planting space',#452,'storage space for plants',$); +#478=IFCCLASSIFICATIONREFERENCE($,'[CS]CDA','Indoor planting space',#477,'planting space for growing plants indoor',$); +#479=IFCCLASSIFICATIONREFERENCE($,'[CS]CDB','Outdoor planting space',#477,'planting space for growing plants outdoor',$); +#480=IFCCLASSIFICATIONREFERENCE($,'[CS]CE?','Dead body storage',#452,'storage space for dead bodies',$); +#481=IFCCLASSIFICATIONREFERENCE($,'[CS]CEA','Chapel of rest',#480,'dead body storage for interim storage',$); +#482=IFCCLASSIFICATIONREFERENCE($,'[CS]CEB','Crypt',#480,'dead body storage for permanent storage',$); +#483=IFCCLASSIFICATIONREFERENCE($,'[CS]D??','Space for technical systems',#400,'space designed for active technical equipment',$); +#484=IFCCLASSIFICATIONREFERENCE($,'[CS]DA?','Installation space',#483,'space for technical systems which support the function of construction entities',$); +#485=IFCCLASSIFICATIONREFERENCE($,'[CS]DAA','Electric installation space',#484,'installation space for stationary electric equipment',$); +#486=IFCCLASSIFICATIONREFERENCE($,'[CS]DAB','Information technology (IT) equipment space',#484,'installation space for stationary electronic equipment',$); +#487=IFCCLASSIFICATIONREFERENCE($,'[CS]DAC','Mechanical installation space',#484,'installation space for stationary mechanical equipment',$); +#488=IFCCLASSIFICATIONREFERENCE($,'[CS]DAD','Shared installation space',#484,'installation space for stationary technical equipment',$); +#489=IFCCLASSIFICATIONREFERENCE($,'[CS]DB?','Equipment space',#483,'space for technical systems which support human activities',$); +#490=IFCCLASSIFICATIONREFERENCE($,'[CS]DBA','Control equipment space',#489,'equipment space of controlling, regulating and monitoring',$); +#491=IFCCLASSIFICATIONREFERENCE($,'[CS]DBB','Production plant space',#489,'equipment space of fabricating and preparing products',$); +#492=IFCCLASSIFICATIONREFERENCE($,'[CS]DBC','Service equipment space',#489,'equipment space of service functions',$); +#493=IFCCLASSIFICATIONREFERENCE($,'[CS]DBD','Analysis equipment space',#489,'equipment space of analyses',$); +#494=IFCCLASSIFICATIONREFERENCE($,'[CS]DC?','Equipment operation space',#483,'space for technical systems which provides space for equipment operation',$); +#495=IFCCLASSIFICATIONREFERENCE($,'[CS]DCA','Work envelope',#494,'equipment operation space providing equipment movement space',$); +#496=IFCCLASSIFICATIONREFERENCE($,'[CS]DCB','Clearance space',#494,'equipment operation space providing clearance around equipment',$); +#497=IFCCLASSIFICATIONREFERENCE($,'[CS]E??','Space for infrastructure',#400,'space designed to create links between activity spaces',$); +#498=IFCCLASSIFICATIONREFERENCE($,'[CS]EA?','Circulation space',#497,'space for infrastructure providing access',$); +#499=IFCCLASSIFICATIONREFERENCE($,'[CS]EAA','Vestibule',#498,'circulation space between outdoor and indoor spaces',$); +#500=IFCCLASSIFICATIONREFERENCE($,'[CS]EAB','Air lock',#498,'circulation space for regulating passage between spaces',$); +#501=IFCCLASSIFICATIONREFERENCE($,'[CS]EAC','Corridor',#498,'circulation space horizontally between two or more spaces',$); +#502=IFCCLASSIFICATIONREFERENCE($,'[CS]EAD','Stairwell',#498,'circulation space vertically and non-automated between two or more storeys or levels',$); +#503=IFCCLASSIFICATIONREFERENCE($,'[CS]EAE','Lift space',#498,'circulation space vertically and automated between two or more levels',$); +#504=IFCCLASSIFICATIONREFERENCE($,'[CS]EAF','Portico',#498,'circulation space through a building',$); +#505=IFCCLASSIFICATIONREFERENCE($,'[CS]EB?','Services space',#497,'space for infrastructure for distributing technical services or media',$); +#506=IFCCLASSIFICATIONREFERENCE($,'[CS]EBA','Services tunnel',#505,'services space with horizontal access for persons',$); +#507=IFCCLASSIFICATIONREFERENCE($,'[CS]EBB','Shaft',#505,'services space with vertical access for persons',$); +#508=IFCCLASSIFICATIONREFERENCE($,'[CS]EBC','Duct',#505,'services space without access for persons',$); +#509=IFCCLASSIFICATIONREFERENCE($,'[CS]F??','Traffic space',#400,'space for traffic',$); +#510=IFCCLASSIFICATIONREFERENCE($,'[CS]FA?','Vehicle traffic space',#509,'traffic space for vehicles',$); +#511=IFCCLASSIFICATIONREFERENCE($,'[CS]FAA','Carriageway',#510,'vehicle traffic space intended for vehicles',$); +#512=IFCCLASSIFICATIONREFERENCE($,'[CS]FAB','Traffic lane',#510,'vehicle traffic space for one line of traffic',$); +#513=IFCCLASSIFICATIONREFERENCE($,'[CS]FAC','Shoulder',#510,'vehicle traffic space along a carriageway for emergency and intermediate use',$); +#514=IFCCLASSIFICATIONREFERENCE($,'[CS]FAD','Lay-by out lane',#510,'vehicle traffic space for stopping of vehicles',$); +#515=IFCCLASSIFICATIONREFERENCE($,'[CS]FAE','Passing place',#510,'vehicle traffic space for meeting of vehicles',$); +#516=IFCCLASSIFICATIONREFERENCE($,'[CS]FAF','Turning place',#510,'vehicle traffic space for turning of vehicles',$); +#517=IFCCLASSIFICATIONREFERENCE($,'[CS]FB?','Active transportation space',#509,'traffic space for persons, animals or bicycles',$); +#518=IFCCLASSIFICATIONREFERENCE($,'[CS]FBA','Walkway',#517,'active transportation spaces for pedestrians',$); +#519=IFCCLASSIFICATIONREFERENCE($,'[CS]FBB','Cycle path',#517,'active transportation space for bicycles',$); +#520=IFCCLASSIFICATIONREFERENCE($,'[CS]FBC','Cycle- and footpath',#517,'active transportation space for bicycles and pedestrians',$); +#521=IFCCLASSIFICATIONREFERENCE($,'[CS]FBD','Animal path',#517,'active transportation space for animals',$); +#522=IFCCLASSIFICATIONREFERENCE($,'[CS]FC','Aircraft traffic space',#509,'traffic space for aircrafts',$); +#523=IFCCLASSIFICATIONREFERENCE($,'[CS]FCA','Runway',#522,'aircraft traffic space for take-off and landing',$); +#524=IFCCLASSIFICATIONREFERENCE($,'[CS]FCB','Taxiway',#522,'aircraft traffic space for taxiing',$); +#525=IFCCLASSIFICATIONREFERENCE($,'[CS]FD?','Track space',#509,'traffic space for track bound objects',$); +#526=IFCCLASSIFICATIONREFERENCE($,'[CS]FDA','Rail track',#525,'track space not intended for crossing',$); +#527=IFCCLASSIFICATIONREFERENCE($,'[CS]FDB','Railway crossing',#525,'track space intended for crossing',$); +#528=IFCCLASSIFICATIONREFERENCE($,'[CS]FE?','Vessel traffic space',#509,'traffic space for vessels',$); +#529=IFCCLASSIFICATIONREFERENCE($,'[CS]FEA','Fairway',#528,'vessel track space for horizontal traffic',$); +#530=IFCCLASSIFICATIONREFERENCE($,'[CS]FEB','Sluice',#528,'vessel track space for vertical traffic',$); +#531=IFCCLASSIFICATIONREFERENCE($,'[CS]FF?','Traffic management space',#509,'traffic space for separation and management',$); +#532=IFCCLASSIFICATIONREFERENCE($,'[CS]FFA','Dividing strip',#531,'traffic management space separating traffic spaces',$); +#533=IFCCLASSIFICATIONREFERENCE($,'[CS]FFB','Platform space',#531,'traffic management space for change of transport mode',$); +#534=IFCCLASSIFICATIONREFERENCE($,'[CS]FFC','Traffic information space',#531,'traffic management space providing information',$); +#535=IFCCLASSIFICATIONREFERENCE($,'[CS]FFD','Clearance profile',#531,'traffic management space providing clearance to other objects',$); +#536=IFCCLASSIFICATIONREFERENCE($,'[CS]FFE','Prohibited area',#531,'traffic management space being part of read or street not meant for vehicle traffic',$); +#537=IFCCLASSIFICATIONREFERENCE($,'[CS]P??','Physical phenomenon space',#400,'space designed for the occurrence of physical phenomena',$); +#538=IFCCLASSIFICATIONREFERENCE($,'[CS]PA?','Matter flow space',#537,'physical phenomenon space for flow or occurrence of matter',$); +#539=IFCCLASSIFICATIONREFERENCE($,'[CS]PAA','Debris space',#538,'matter flow space for objects',$); +#540=IFCCLASSIFICATIONREFERENCE($,'[CS]PAB','Solid material flow space',#538,'matter flow space for solid materials',$); +#541=IFCCLASSIFICATIONREFERENCE($,'[CS]PAC','Gas flow spaces',#538,'matter flow space for gas',$); +#542=IFCCLASSIFICATIONREFERENCE($,'[CS]PAD','Liquid flow spaces',#538,'matter flow space for liquid',$); +#543=IFCCLASSIFICATIONREFERENCE($,'[CS]PAE','Plasma flow space',#538,'matter flow space for plasma',$); +#544=IFCCLASSIFICATIONREFERENCE($,'[CS]PAF','Smoke occurrence space',#538,'matter flow space for multiple matter kinds',$); +#545=IFCCLASSIFICATIONREFERENCE($,'[CS]PB?','Energy flow space',#537,'physical phenomenon space for flow of energy',$); +#546=IFCCLASSIFICATIONREFERENCE($,'[CS]PBA','Electrical energy flow space',#545,'energy flow spaces for electrical energy',$); +#547=IFCCLASSIFICATIONREFERENCE($,'[CS]PBB','Thermal energy flow space',#545,'energy flow spaces for thermal energy',$); +#548=IFCCLASSIFICATIONREFERENCE($,'[CS]PBC','Mechanical energy flow space',#545,'energy flow spaces for mechanical energy',$); +#549=IFCCLASSIFICATIONREFERENCE($,'[CS]PBD','Explosion space',#545,'energy flow spaces for explosion',$); +#550=IFCCLASSIFICATIONREFERENCE($,'[CS]PBE','Fire zone',#545,'energy flow spaces for combustion',$); + +#600=IFCCLASSIFICATIONREFERENCE($,'CF - Functional system',' ',#2,' ',$); +#601=IFCCLASSIFICATIONREFERENCE($,'[CF]A','Ground system',#600,'space system which terminates a construction entity downwards',$); +#602=IFCCLASSIFICATIONREFERENCE($,'[CF]B','Wall system',#600,'space system which forms and separates space vertically',$); +#603=IFCCLASSIFICATIONREFERENCE($,'[CF]C','Slab system',#600,'space system which forms and separates space horizontally',$); +#604=IFCCLASSIFICATIONREFERENCE($,'[CF]D','Roof system',#600,'space system which terminates a construction entity upwards',$); +#605=IFCCLASSIFICATIONREFERENCE($,'[CF]E','Gas and air system',#600,'installation system which supplies technical gases or technical air',$); +#606=IFCCLASSIFICATIONREFERENCE($,'[CF]F','Water and fluid system',#600,'installation system which supplies domestic water, technical water or other liquid',$); +#607=IFCCLASSIFICATIONREFERENCE($,'[CF]G','Drainage and waste system',#600,'installation system which discharges liquid or disposes of waste',$); +#608=IFCCLASSIFICATIONREFERENCE($,'[CF]H','Cooling and/or heating system',#600,'installation system which supplies cooling and/or heating',$); +#609=IFCCLASSIFICATIONREFERENCE($,'[CF]J','Ventilation system',#600,'installation system which supplies air exchange',$); +#610=IFCCLASSIFICATIONREFERENCE($,'[CF]K','Electrical system',#600,'installation system which supplies electrical energy',$); +#611=IFCCLASSIFICATIONREFERENCE($,'[CF]L','Automation system',#600,'installation system which controls, adjusts and monitors technical systems in a context',$); +#612=IFCCLASSIFICATIONREFERENCE($,'[CF]M','Information and communication system',#600,'installation system which provides communications between persons or technical systems',$); +#613=IFCCLASSIFICATIONREFERENCE($,'[CF]N','Transportation system',#600,'installation system which transports goods or persons',$); +#614=IFCCLASSIFICATIONREFERENCE($,'[CF]P','Security and safety system',#600,'installation system which protects any object from danger and damage',$); +#615=IFCCLASSIFICATIONREFERENCE($,'[CF]Q','Lighting system',#600,'installation system for light management',$); +#616=IFCCLASSIFICATIONREFERENCE($,'[CF]R','Railroad system',#600,'installation system guiding and directing track-bound vehicles',$); +#617=IFCCLASSIFICATIONREFERENCE($,'[CF]S','Arrangement system',#600,'fit-out system which equips a construction entity and its spaces with fittings and equipment',$); +#700=IFCCLASSIFICATIONREFERENCE($,'CT - Technical system',' ',#2,' ',$); +#701=IFCCLASSIFICATIONREFERENCE($,'[CT]A?','Assembly system',#700,'technical system which constitutes a layered construction',$); +#702=IFCCLASSIFICATIONREFERENCE($,'[CT]AA','Pavement construction',#701,'assembly system forming areas for transport',$); +#703=IFCCLASSIFICATIONREFERENCE($,'[CT]AB','Foundation construction',#701,'assembly system forming separation towards underground',$); +#704=IFCCLASSIFICATIONREFERENCE($,'[CT]AC','Slab construction',#701,'assembly system forming horizontal separation',$); +#705=IFCCLASSIFICATIONREFERENCE($,'[CT]AD','Wall construction',#701,'assembly system forming vertical separation',$); +#706=IFCCLASSIFICATIONREFERENCE($,'[CT]AE','Roof construction',#701,'assembly system terminating a construction entity upwards',$); +#707=IFCCLASSIFICATIONREFERENCE($,'[CT]AF','Stairway construction',#701,'assembly system establishing a step-wise link between two or more levels',$); +#708=IFCCLASSIFICATIONREFERENCE($,'[CT]AG','Ramp construction',#701,'assembly system establishing a step-free link between two or more levels',$); +#709=IFCCLASSIFICATIONREFERENCE($,'[CT]AH','Balcony',#701,'assembly system that creates an adjoining outer user space built into wall system or roof system',$); +#710=IFCCLASSIFICATIONREFERENCE($,'[CT]AJ','Bay window',#701,'assembly system that creates an extended inner user space built into wall system',$); +#711=IFCCLASSIFICATIONREFERENCE($,'[CT]AK','Dormer',#701,'assembly system that creates an extended inner user space built into roof system',$); +#712=IFCCLASSIFICATIONREFERENCE($,'[CT]AL','Roof tower',#701,'assembly system that creates an extended inner user space built on top of roof system or construction entity',$); +#713=IFCCLASSIFICATIONREFERENCE($,'[CT]AM','Light well',#701,'assembly system that creates an extended space for light transmission',$); +#714=IFCCLASSIFICATIONREFERENCE($,'[CT]B?','Structural system',#700,'technical system which forms structural construction',$); +#715=IFCCLASSIFICATIONREFERENCE($,'[CT]BA','Groundworks structure',#714,'structural system forming a regulated volume of underground',$); +#716=IFCCLASSIFICATIONREFERENCE($,'[CT]BB','Foundation structure',#714,'structural system connecting a construction entity with the underground',$); +#717=IFCCLASSIFICATIONREFERENCE($,'[CT]BC','Slab structure',#714,'structural system in a horizontal separation',$); +#718=IFCCLASSIFICATIONREFERENCE($,'[CT]BD','Wall structure',#714,'structural system in a vertical separation',$); +#719=IFCCLASSIFICATIONREFERENCE($,'[CT]BE','Roof structure',#714,'structural system terminating a construction entity upwards',$); +#720=IFCCLASSIFICATIONREFERENCE($,'[CT]BF','Floor structure',#714,'structural system terminating a space downwards',$); +#721=IFCCLASSIFICATIONREFERENCE($,'[CT]BG','Ceiling structure',#714,'structural system terminating a space upwards',$); +#722=IFCCLASSIFICATIONREFERENCE($,'[CT]BH','Routing structure',#714,'structural system supporting supply or distribution system',$); +#723=IFCCLASSIFICATIONREFERENCE($,'[CT]C?','Ground surface construction system',#700,'technical construction system forming ground-based areas',$); +#724=IFCCLASSIFICATIONREFERENCE($,'[CT]CA','Subgrade construction',#723,'ground surface construction system which gives support to other constructions',$); +#725=IFCCLASSIFICATIONREFERENCE($,'[CT]CB','Ground superstructure construction',#723,'ground surface construction system for hard covered outdoor space',$); +#726=IFCCLASSIFICATIONREFERENCE($,'[CT]CC','Traffic island construction',#723,'ground surface construction system for traffic separation area',$); +#727=IFCCLASSIFICATIONREFERENCE($,'[CT]CD','Side area construction',#723,'ground surface construction system for safety zone for traffic',$); +#728=IFCCLASSIFICATIONREFERENCE($,'[CT]CE','Railway embankment construction',#723,'ground surface construction system for railway track support',$); +#729=IFCCLASSIFICATIONREFERENCE($,'[CT]CF','Vegetation area construction',#723,'ground surface construction system for vegetation areas',$); +#730=IFCCLASSIFICATIONREFERENCE($,'[CT]CG','Water management construction',#723,'ground surface construction system for transport, storage or infiltration of water, or for space along water or over water',$); +#731=IFCCLASSIFICATIONREFERENCE($,'[CT]CH','Pipe ditch system',#723,'ground surface construction system for refilled media excavation',$); +#732=IFCCLASSIFICATIONREFERENCE($,'[CT]CJ','Foundation and ground support construction',#723,'ground surface construction system which carries vertical or horizontal load',$); +#733=IFCCLASSIFICATIONREFERENCE($,'[CT]CK','Terrain stairway / terrain ramp construction',#723,'ground surface construction system which connects spaces at different levels',$); +#734=IFCCLASSIFICATIONREFERENCE($,'[CT]CL','Terrain wall construction',#723,'ground surface construction system which separates spaces',$); +#735=IFCCLASSIFICATIONREFERENCE($,'[CT]D?','Railway track construction system',#700,'technical system which guides rail bound vehicles',$); +#736=IFCCLASSIFICATIONREFERENCE($,'[CT]DA','Ballasted railway track construction',#735,'railway track construction system with two rails, on ballast',$); +#737=IFCCLASSIFICATIONREFERENCE($,'[CT]DB','Non-ballasted railway track construction',#735,'railway track construction system with two rails, without ballast',$); +#738=IFCCLASSIFICATIONREFERENCE($,'[CT]DC','Monorail construction',#735,'railway track construction system with one rail',$); +#739=IFCCLASSIFICATIONREFERENCE($,'[CT]H?','Supply system',#700,'technical system supplying consumption',$); +#740=IFCCLASSIFICATIONREFERENCE($,'[CT]HA','Gas and air supply system',#739,'supply system for process gas',$); +#741=IFCCLASSIFICATIONREFERENCE($,'[CT]HB','Liquid supply system',#739,'supply system for liquid',$); +#742=IFCCLASSIFICATIONREFERENCE($,'[CT]HC','Cooling supply system',#739,'supply system for cooling',$); +#743=IFCCLASSIFICATIONREFERENCE($,'[CT]HD','Heating supply system',#739,'supply system for heating',$); +#744=IFCCLASSIFICATIONREFERENCE($,'[CT]HE','Combined heating and cooling supply system',#739,'supply system for cooling and heating',$); +#745=IFCCLASSIFICATIONREFERENCE($,'[CT]HF','Ventilation supply system',#739,'supply system for ventilation',$); +#746=IFCCLASSIFICATIONREFERENCE($,'[CT]HG','Power supply system',#739,'supply system for electrical energy',$); +#747=IFCCLASSIFICATIONREFERENCE($,'[CT]HH','Lighting system',#739,'supply system for lighting',$); +#748=IFCCLASSIFICATIONREFERENCE($,'[CT]HJ','Signal supply system',#739,'supply system for signals',$); +#749=IFCCLASSIFICATIONREFERENCE($,'[CT]J?','Transporting system',#700,'technical system which brings something from one place to another',$); +#750=IFCCLASSIFICATIONREFERENCE($,'[CT]JA','Gas distribution system',#749,'transport system for gas',$); +#751=IFCCLASSIFICATIONREFERENCE($,'[CT]JB','Water distribution system',#749,'transport system for water',$); +#752=IFCCLASSIFICATIONREFERENCE($,'[CT]JC','Chemical distribution system',#749,'transport system for flammable or other chemical liquids',$); +#753=IFCCLASSIFICATIONREFERENCE($,'[CT]JD','Liquids outflow system',#749,'transport system for liquid waste',$); +#754=IFCCLASSIFICATIONREFERENCE($,'[CT]JE','Solids outflow system',#749,'transport system for solid waste',$); +#755=IFCCLASSIFICATIONREFERENCE($,'[CT]JF','Cooling distribution system',#749,'transport system for cooling',$); +#756=IFCCLASSIFICATIONREFERENCE($,'[CT]JG','Heating distribution system',#749,'transport system for heating',$); +#757=IFCCLASSIFICATIONREFERENCE($,'[CT]JH','Combined heat and cooling distribution system',#749,'transport system for cooling and heating',$); +#758=IFCCLASSIFICATIONREFERENCE($,'[CT]JJ','Air distribution system',#749,'transport system for air',$); +#759=IFCCLASSIFICATIONREFERENCE($,'[CT]JK','Power distribution system',#749,'transport for electrical energy',$); +#760=IFCCLASSIFICATIONREFERENCE($,'[CT]JL','Signal distribution system',#749,'transport system for signals',$); +#761=IFCCLASSIFICATIONREFERENCE($,'[CT]JM','Passenger transportation system',#749,'transport system for persons',$); +#762=IFCCLASSIFICATIONREFERENCE($,'[CT]JN','Goods transportation system',#749,'transport system for solid goods or packets',$); +#763=IFCCLASSIFICATIONREFERENCE($,'[CT]K?','Treatment system',#700,'technical system which provides treatment',$); +#764=IFCCLASSIFICATIONREFERENCE($,'[CT]KA','Solar screening system',#763,'treatment system limiting or excluding solar heat gain',$); +#765=IFCCLASSIFICATIONREFERENCE($,'[CT]KB','Opening control system',#763,'treatment system for automatic control of openings',$); +#766=IFCCLASSIFICATIONREFERENCE($,'[CT]KC','Filter system',#763,'treatment system for separating solid particles from liquid or air flow',$); +#767=IFCCLASSIFICATIONREFERENCE($,'[CT]KD','Separator system',#763,'treatment system for separating substances',$); +#768=IFCCLASSIFICATIONREFERENCE($,'[CT]KE','Mixing system',#763,'treatment system for adding substances',$); +#769=IFCCLASSIFICATIONREFERENCE($,'[CT]KF','Pump system',#763,'treatment system for increasing pressure in a flow of liquid',$); +#770=IFCCLASSIFICATIONREFERENCE($,'[CT]KG','Pressure and expansion system',#763,'treatment system for stabilizing pressure and volume in liquid or in gas flow',$); +#771=IFCCLASSIFICATIONREFERENCE($,'[CT]KH','Transformer system',#763,'treatment system for transforming electrical energy',$); +#772=IFCCLASSIFICATIONREFERENCE($,'[CT]KJ','Antenna system',#763,'treatment system for bidirectional transforming between radio waves and electrical signals',$); +#773=IFCCLASSIFICATIONREFERENCE($,'[CT]KK','Weather station system',#763,'treatment system for transforming weather-related information into electrical signals',$); +#774=IFCCLASSIFICATIONREFERENCE($,'[CT]KL','Access control system',#763,'treatment system for physical separation of a flow of persons or items',$); +#775=IFCCLASSIFICATIONREFERENCE($,'[CT]L?','Monitoring and control system',#700,'technical system which monitors and/or controls events and processes',$); +#776=IFCCLASSIFICATIONREFERENCE($,'[CT]LA','Gas alarm system',#775,'monitoring system which raises an alarm on the occurrence of hazardous gases',$); +#777=IFCCLASSIFICATIONREFERENCE($,'[CT]LB','Fire alarm system',#775,'monitoring system which raises an alarm in the presence of smoke or fire',$); +#778=IFCCLASSIFICATIONREFERENCE($,'[CT]LC','Automation system',#775,'monitoring system which automates processes in buildings and process plants in a central unit',$); +#779=IFCCLASSIFICATIONREFERENCE($,'[CT]LD','Access monitor system',#775,'monitoring system which ensures authorized access for persons to an area or a construction entity',$); +#780=IFCCLASSIFICATIONREFERENCE($,'[CT]LE','Alarm system',#775,'monitoring system which raises an alarm in the presence of dangerous or undesirable conditions',$); +#781=IFCCLASSIFICATIONREFERENCE($,'[CT]LF','Video surveillance system',#775,'monitoring system which provides remote visual monitoring',$); +#782=IFCCLASSIFICATIONREFERENCE($,'[CT]M?','Information presenting system',#700,'technical system which presents information',$); +#783=IFCCLASSIFICATIONREFERENCE($,'[CT]MA','Warning system',#782,'information presenting system which produces a fixed sound or voice messages to warn of hazardous conditions',$); +#784=IFCCLASSIFICATIONREFERENCE($,'[CT]MB','Audio-video system',#782,'information presenting system which produces images and/or sounds',$); +#785=IFCCLASSIFICATIONREFERENCE($,'[CT]MC','Traffic control system',#782,'information presenting system which controls traffic',$); +#786=IFCCLASSIFICATIONREFERENCE($,'[CT]MD','Time system',#782,'information presenting system for time',$); +#787=IFCCLASSIFICATIONREFERENCE($,'[CT]ME','Sign system',#782,'information presenting system which provides instruction in a written or symbolic form',$); +#788=IFCCLASSIFICATIONREFERENCE($,'[CT]P?','Protection system',#700,'self-acting technical system which protects against danger or unwanted conditions',$); +#789=IFCCLASSIFICATIONREFERENCE($,'[CT]PA','Fire protection system',#788,'protection system by opening or closing doors and windows in the event of a fire',$); +#790=IFCCLASSIFICATIONREFERENCE($,'[CT]PB','Firefighting system',#788,'protection system against fire by extinguishing',$); +#791=IFCCLASSIFICATIONREFERENCE($,'[CT]PC','Earthing system',#788,'protection system against dangerous electrical currents by earthing of electrical systems',$); +#792=IFCCLASSIFICATIONREFERENCE($,'[CT]PD','Lightning protection system',#788,'protection system against lightning by earthing of electrical systems',$); +#793=IFCCLASSIFICATIONREFERENCE($,'[CT]PE','Cathodic protection system',#788,'protection system for metal constructions against corrosion by corrosion protection',$); +#794=IFCCLASSIFICATIONREFERENCE($,'[CT]Q?','Storage system',#700,'technical system which stores information, energy or substances',$); +#795=IFCCLASSIFICATIONREFERENCE($,'[CT]QA','Meter system',#794,'storage system for metered consumption information',$); +#796=IFCCLASSIFICATIONREFERENCE($,'[CT]QB','Gas and air storage system',#794,'storage system for air and gases',$); +#797=IFCCLASSIFICATIONREFERENCE($,'[CT]QC','Liquid storage system',#794,'storage system for liquids',$); +#798=IFCCLASSIFICATIONREFERENCE($,'[CT]QD','Energy storage system',#794,'storage system for energy',$); +#799=IFCCLASSIFICATIONREFERENCE($,'[CT]R?','Furnishing system',#700,'technical system which fits out space or construction elements in a construction entity',$); +#800=IFCCLASSIFICATIONREFERENCE($,'[CT]RA','Planting system',#799,'furnishing system in the form of vegetation',$); +#801=IFCCLASSIFICATIONREFERENCE($,'[CT]RB','Furniture system',#799,'furnishing system in the form of fixed or moveable construction elements',$); +#802=IFCCLASSIFICATIONREFERENCE($,'[CT]RC','Equipment system',#799,'furnishing system in the form of tools used in a particular situation',$); +#900=IFCCLASSIFICATIONREFERENCE($,'CO - Construction component',' ',#2,' ',$); +#901=IFCCLASSIFICATIONREFERENCE($,'[CO]B??','Sensing object',#900,'object for picking up information and providing a representation',$); +#902=IFCCLASSIFICATIONREFERENCE($,'[CO]BA?','Electric potential sensing object',#901,'sensing object for electric potential',$); +#903=IFCCLASSIFICATIONREFERENCE($,'[CO]BAA','Voltage transformer',#902,'electric potential sensing object, with scalar output',$); +#904=IFCCLASSIFICATIONREFERENCE($,'[CO]BAB','Voltage relay',#902,'electric potential sensing object, with Boolean output',$); +#905=IFCCLASSIFICATIONREFERENCE($,'[CO]BB?','Resistivity sensing object',#901,'sensing object for resistivity or conductivity',$); +#906=IFCCLASSIFICATIONREFERENCE($,'[CO]BBA','Electric resistivity sensor',#905,'resistivity sensing object for electric current flow, with scalar output',$); +#907=IFCCLASSIFICATIONREFERENCE($,'[CO]BBB','Electric resistivity detector',#905,'resistivity sensing object for electric current flow, with Boolean output',$); +#908=IFCCLASSIFICATIONREFERENCE($,'[CO]BBC','Thermal resistivity sensor',#905,'resistivity sensing object for thermal flow, with scalar output',$); +#909=IFCCLASSIFICATIONREFERENCE($,'[CO]BBD','Thermal resistivity detector',#905,'resistivity sensing object for thermal flow, with Boolean output',$); +#910=IFCCLASSIFICATIONREFERENCE($,'[CO]BC?','Electric current sensing object',#901,'sensing object for electric current',$); +#911=IFCCLASSIFICATIONREFERENCE($,'[CO]BCA','Current transformer',#910,'electric current sensing object, with scalar output',$); +#912=IFCCLASSIFICATIONREFERENCE($,'[CO]BCB','Current relay',#910,'electric current sensing object, with Boolean output',$); +#913=IFCCLASSIFICATIONREFERENCE($,'[CO]BD?','Density sensing object',#901,'sensing object for density',$); +#914=IFCCLASSIFICATIONREFERENCE($,'[CO]BDA','Density transmitter',#913,'density sensing object, with scalar output',$); +#915=IFCCLASSIFICATIONREFERENCE($,'[CO]BDB','Density switch',#913,'density sensing object, with Boolean output',$); +#916=IFCCLASSIFICATIONREFERENCE($,'[CO]BE?','Field sensing object',#901,'sensing object for field',$); +#917=IFCCLASSIFICATIONREFERENCE($,'[CO]BEA','Electric field sensor',#916,'field sensing object for electric field, with scalar output',$); +#918=IFCCLASSIFICATIONREFERENCE($,'[CO]BEB','Electric field detector',#916,'field sensing object for electric field, with Boolean output',$); +#919=IFCCLASSIFICATIONREFERENCE($,'[CO]BEC','Magnetic field sensor',#916,'field sensing object for magnetic field, with scalar output',$); +#920=IFCCLASSIFICATIONREFERENCE($,'[CO]BED','Magnetic field detector',#916,'field sensing object for magnetic field, with Boolean output',$); +#921=IFCCLASSIFICATIONREFERENCE($,'[CO]BF?','Flow sensing object',#901,'sensing object for flow',$); +#922=IFCCLASSIFICATIONREFERENCE($,'[CO]BFA','Flow transmitter',#921,'flow sensing object, with scalar output',$); +#923=IFCCLASSIFICATIONREFERENCE($,'[CO]BFB','Flow switch',#921,'flow sensing object, with Boolean output',$); +#924=IFCCLASSIFICATIONREFERENCE($,'[CO]BG?','Physical dimension sensing object',#901,'sensing object of spatial dimension and/or position',$); +#925=IFCCLASSIFICATIONREFERENCE($,'[CO]BGA','Position transmitter',#924,'physical dimension sensing object of position, with scalar output',$); +#926=IFCCLASSIFICATIONREFERENCE($,'[CO]BGB','Position switch',#924,'physical dimension sensing object of position, with Boolean output',$); +#927=IFCCLASSIFICATIONREFERENCE($,'[CO]BGC','Distance transmitter',#924,'physical dimension sensing object of length, with scalar output',$); +#928=IFCCLASSIFICATIONREFERENCE($,'[CO]BGD','Distance switch',#924,'physical dimension sensing object of length, with Boolean output',$); +#929=IFCCLASSIFICATIONREFERENCE($,'[CO]BGE','Angle transmitter',#924,'physical dimension sensing object of angle or direction, with scalar output',$); +#930=IFCCLASSIFICATIONREFERENCE($,'[CO]BGF','Angle switch',#924,'physical dimension sensing object of angle or direction, with Boolean output',$); +#931=IFCCLASSIFICATIONREFERENCE($,'[CO]BGG','Object scanner',#924,'physical dimension sensing object of physical object',$); +#932=IFCCLASSIFICATIONREFERENCE($,'[CO]BH?','Energy sensing object',#901,'sensing object for energy',$); +#933=IFCCLASSIFICATIONREFERENCE($,'[CO]BHA','Flow energy meter',#932,'energy sensing object of flow with defined energy density',$); +#934=IFCCLASSIFICATIONREFERENCE($,'[CO]BHB','Thermal energy meter',#932,'energy sensing object of thermal flow with defined heat capacity',$); +#935=IFCCLASSIFICATIONREFERENCE($,'[CO]BHC','Electric energy meter',#932,'energy sensing object of electricity flow',$); +#936=IFCCLASSIFICATIONREFERENCE($,'[CO]BJ?','Power sensing object',#901,'sensing object for power',$); +#937=IFCCLASSIFICATIONREFERENCE($,'[CO]BJA','Power meter',#936,'power sensing object, with scalar output',$); +#938=IFCCLASSIFICATIONREFERENCE($,'[CO]BJB','Power limit switch',#936,'power sensing object, with Boolean output',$); +#939=IFCCLASSIFICATIONREFERENCE($,'[CO]BK?','Time sensing object',#901,'sensing object for time',$); +#940=IFCCLASSIFICATIONREFERENCE($,'[CO]BKA','Time sensor',#939,'time sensing object, with scalar output',$); +#941=IFCCLASSIFICATIONREFERENCE($,'[CO]BKB','Timer switch',#939,'time sensing object, with Boolean output',$); +#942=IFCCLASSIFICATIONREFERENCE($,'[CO]BL?','Level sensing object',#901,'sensing object for level',$); +#943=IFCCLASSIFICATIONREFERENCE($,'[CO]BLA','Level transmitter',#942,'level sensing object, with scalar output',$); +#944=IFCCLASSIFICATIONREFERENCE($,'[CO]BLB','Level switch',#942,'level sensing object, with Boolean output',$); +#945=IFCCLASSIFICATIONREFERENCE($,'[CO]BM?','Humidity sensing object',#901,'sensing object for humidity',$); +#946=IFCCLASSIFICATIONREFERENCE($,'[CO]BMA','Humidity transmitter',#945,'humidity sensing object, with scalar output',$); +#947=IFCCLASSIFICATIONREFERENCE($,'[CO]BMB','Humidity switch',#945,'humidity sensing object, with Boolean output',$); +#948=IFCCLASSIFICATIONREFERENCE($,'[CO]BP?','Pressure sensing object',#901,'sensing object for pressure',$); +#949=IFCCLASSIFICATIONREFERENCE($,'[CO]BPA','Absolute pressure transmitter',#948,'pressure sensing object utilizing one port for absolute pressure sensing, with scalar output',$); +#950=IFCCLASSIFICATIONREFERENCE($,'[CO]BPB','Absolute Pressure Switch',#948,'pressure sensing object utilizing one port for absolute pressure sensing, with Boolean output',$); +#951=IFCCLASSIFICATIONREFERENCE($,'[CO]BPC','Differential pressure transmitter',#948,'pressure sensing object utilizing two ports for differential pressure sensing, with scalar output',$); +#952=IFCCLASSIFICATIONREFERENCE($,'[CO]BPD','Differential pressure switch',#948,'pressure sensing object utilizing two ports for differential pressure sensing, with Boolean output',$); +#953=IFCCLASSIFICATIONREFERENCE($,'[CO]BQ?','Concentration sensing object',#901,'sensing object for substance concentration',$); +#954=IFCCLASSIFICATIONREFERENCE($,'[CO]BQA','Gas sensor',#953,'concentration sensing object of a gas, with scalar output',$); +#955=IFCCLASSIFICATIONREFERENCE($,'[CO]BQB','Gas detector',#953,'concentration sensing object of a gas, with Boolean output',$); +#956=IFCCLASSIFICATIONREFERENCE($,'[CO]BQC','Liquid sensor',#953,'concentration sensing object of liquid, with scalar output',$); +#957=IFCCLASSIFICATIONREFERENCE($,'[CO]BQD','Liquid detector',#953,'concentration sensing object of liquid, with Boolean output',$); +#958=IFCCLASSIFICATIONREFERENCE($,'[CO]BQE','Solid matter sensor',#953,'concentration sensing object of solid matter, with scalar output',$); +#959=IFCCLASSIFICATIONREFERENCE($,'[CO]BQF','Solid matter detector',#953,'concentration sensing object of solid matter, with Boolean output',$); +#960=IFCCLASSIFICATIONREFERENCE($,'[CO]BR?','Radiation sensing object',#901,'sensing object for radiation',$); +#961=IFCCLASSIFICATIONREFERENCE($,'[CO]BRA','Light sensor',#960,'radiation sensing object of visible electromagnetic waves, with scalar output',$); +#962=IFCCLASSIFICATIONREFERENCE($,'[CO]BRB','Light detector',#960,'radiation sensing object of visible electromagnetic waves, with Boolean output',$); +#963=IFCCLASSIFICATIONREFERENCE($,'[CO]BRC','Electromagnetic wave sensor',#960,'radiation sensing object for non-visible electromagnetic waves, with scalar output',$); +#964=IFCCLASSIFICATIONREFERENCE($,'[CO]BRD','Electromagnetic wave detector',#960,'radiation sensing object for non-visible electromagnetic waves, with Boolean output',$); +#965=IFCCLASSIFICATIONREFERENCE($,'[CO]BRE','Radiation counter',#960,'radiation sensing object of nuclear particles, with scalar output',$); +#966=IFCCLASSIFICATIONREFERENCE($,'[CO]BRF','Radiation detector',#960,'radiation sensing object of nuclear particles, with Boolean output',$); +#967=IFCCLASSIFICATIONREFERENCE($,'[CO]BS?','Time-rating object',#901,'sensing object for time-rate',$); +#968=IFCCLASSIFICATIONREFERENCE($,'[CO]BSA','Cycle rate transmitter',#967,'time-rating object of non-rotational cycles, with scalar output',$); +#969=IFCCLASSIFICATIONREFERENCE($,'[CO]BSB','Cycle rate detecting switch',#967,'time-rating object of non-rotational cycles, with Boolean output',$); +#970=IFCCLASSIFICATIONREFERENCE($,'[CO]BSC','Speed sensing transmitter',#967,'time-rating object of distance, with scalar output',$); +#971=IFCCLASSIFICATIONREFERENCE($,'[CO]BSD','Speed detecting switch',#967,'time-rating object of distance, with Boolean output',$); +#972=IFCCLASSIFICATIONREFERENCE($,'[CO]BSE','Tachometer',#967,'time-rating object of rotation, with scalar output',$); +#973=IFCCLASSIFICATIONREFERENCE($,'[CO]BSF','Rotation speed relay',#967,'time-rating object of rotation, with Boolean output',$); +#974=IFCCLASSIFICATIONREFERENCE($,'[CO]BSG','Accelerometer transmitter',#967,'time-rating object of speed, with scalar output',$); +#975=IFCCLASSIFICATIONREFERENCE($,'[CO]BSH','Accelerometer relay',#967,'time-rating object of speed, with Boolean output',$); +#976=IFCCLASSIFICATIONREFERENCE($,'[CO]BT?','Temperature sensing object',#901,'sensing object for temperature',$); +#977=IFCCLASSIFICATIONREFERENCE($,'[CO]BTA','Temperature transmitter',#976,'temperature sensing object, with scalar output',$); +#978=IFCCLASSIFICATIONREFERENCE($,'[CO]BTB','Temperature switch',#976,'temperature sensing object, with Boolean output',$); +#979=IFCCLASSIFICATIONREFERENCE($,'[CO]BU?','Multi-quantity sensing object',#901,'sensing object for multiple quantities',$); +#980=IFCCLASSIFICATIONREFERENCE($,'[CO]BUA','Multi-sensor',#979,'multi-quantity sensing object, with scalar output(s)',$); +#981=IFCCLASSIFICATIONREFERENCE($,'[CO]BUB','Multi-detector',#979,'multi-quantity sensing object, with Boolean output(s)',$); +#982=IFCCLASSIFICATIONREFERENCE($,'[CO]BW?','Force sensing object',#901,'sensing object for weight, force or torque',$); +#983=IFCCLASSIFICATIONREFERENCE($,'[CO]BWA','Weight transmitter',#982,'force sensing object of weight, with scalar output',$); +#984=IFCCLASSIFICATIONREFERENCE($,'[CO]BWB','Weight detecting switch',#982,'force sensing object of weight, with Boolean output',$); +#985=IFCCLASSIFICATIONREFERENCE($,'[CO]BWC','Force value transmitter',#982,'force sensing object of force, with scalar output',$); +#986=IFCCLASSIFICATIONREFERENCE($,'[CO]BWD','Force detecting switch',#982,'force sensing object of force, with Boolean output',$); +#987=IFCCLASSIFICATIONREFERENCE($,'[CO]BWE','Torque value transmitter',#982,'force sensing object of torque, with scalar output',$); +#988=IFCCLASSIFICATIONREFERENCE($,'[CO]BWF','Torque switch',#982,'force sensing object of torque, with Boolean output',$); +#989=IFCCLASSIFICATIONREFERENCE($,'[CO]BX?','Audio-visual sensing object',#901,'sensing object of sound and/or visual appearance',$); +#990=IFCCLASSIFICATIONREFERENCE($,'[CO]BXA','Acoustic sensor',#989,'audio-visual sensing object of sound, with scalar output',$); +#991=IFCCLASSIFICATIONREFERENCE($,'[CO]BXB','Acoustic detector',#989,'audio-visual sensing object of sound, with Boolean output',$); +#992=IFCCLASSIFICATIONREFERENCE($,'[CO]BXC','Image sensor',#989,'audio-visual sensing object of visual appearance, with scalar output',$); +#993=IFCCLASSIFICATIONREFERENCE($,'[CO]BXD','Image detector',#989,'audio-visual sensing object of visual appearance, with Boolean output',$); +#994=IFCCLASSIFICATIONREFERENCE($,'[CO]BY?','Information sensing object',#901,'sensing object of stored information',$); +#995=IFCCLASSIFICATIONREFERENCE($,'[CO]BYA','Chip reader',#994,'information sensing object by electrical connection',$); +#996=IFCCLASSIFICATIONREFERENCE($,'[CO]BYB','Electromagnetic reader',#994,'information sensing object by electromagnetic field',$); +#997=IFCCLASSIFICATIONREFERENCE($,'[CO]BYC','Optical reader',#994,'information sensing object by light',$); +#998=IFCCLASSIFICATIONREFERENCE($,'[CO]BZ?','Incident sensing object',#901,'sensing object for incidents or amounts',$); +#999=IFCCLASSIFICATIONREFERENCE($,'[CO]BZA','Counter',#998,'incident sensing object detecting the number of incidents, with scalar output',$); +#1000=IFCCLASSIFICATIONREFERENCE($,'[CO]BZB','Threshold detector',#998,'incident sensing object detecting an incident with Boolean output',$); +#1001=IFCCLASSIFICATIONREFERENCE($,'[CO]BZC','Person detector',#998,'incident sensing object detecting the presence of humans, with Boolean output',$); +#1002=IFCCLASSIFICATIONREFERENCE($,'[CO]BZD','Matter detector',#998,'incident sensing object detecting the presence of matters, with Boolean output',$); +#1003=IFCCLASSIFICATIONREFERENCE($,'[CO]C??','Storing object',#900,'object for storing for subsequent retrieval',$); +#1004=IFCCLASSIFICATIONREFERENCE($,'[CO]CA?','Capacitive storing object',#1003,'storing object for electric energy within an electrostatic field',$); +#1005=IFCCLASSIFICATIONREFERENCE($,'[CO]CAA','Capacitor',#1004,'capacitive storing object for electric charge',$); +#1006=IFCCLASSIFICATIONREFERENCE($,'[CO]CB?','Inductive storing object',#1003,'storing object for electric energy within an electromagnetic field',$); +#1007=IFCCLASSIFICATIONREFERENCE($,'[CO]CBA','Inductor',#1006,'inductive storing object for inductive charge',$); +#1008=IFCCLASSIFICATIONREFERENCE($,'[CO]CC?','Electrochemical storing object',#1003,'storing object for electric energy within an electrochemical substance',$); +#1009=IFCCLASSIFICATIONREFERENCE($,'[CO]CCA','Rechargeable battery',#1008,'electrochemical storing object by reversible reactions',$); +#1010=IFCCLASSIFICATIONREFERENCE($,'[CO]CF?','Information storing object',#1003,'storing object for information',$); +#1011=IFCCLASSIFICATIONREFERENCE($,'[CO]CFA','Information storing media',#1010,'information storing object on a carrier medium',$); +#1012=IFCCLASSIFICATIONREFERENCE($,'[CO]CL?','Open stationary storing object',#1003,'storing object for matters or persons within a stationary open enclosure',$); +#1013=IFCCLASSIFICATIONREFERENCE($,'[CO]CLA','Pool',#1012,'open stationary storing object for fluids',$); +#1014=IFCCLASSIFICATIONREFERENCE($,'[CO]CLB','Shelf',#1012,'open stationary storing object for solid objects',$); +#1015=IFCCLASSIFICATIONREFERENCE($,'[CO]CLC','Seat',#1012,'open stationary storing object for persons',$); +#1016=IFCCLASSIFICATIONREFERENCE($,'[CO]CM?','Enclosed stationary storing object',#1003,'storing object for matters within a stationary closed enclosure',$); +#1017=IFCCLASSIFICATIONREFERENCE($,'[CO]CMA','Tank',#1016,'enclosed stationary storing object for fluids',$); +#1018=IFCCLASSIFICATIONREFERENCE($,'[CO]CMB','Box',#1016,'enclosed stationary storing object for solid matters',$); +#1019=IFCCLASSIFICATIONREFERENCE($,'[CO]CN?','Moveable storing object',#1003,'storing object for matters or persons within a moveable closed enclosure',$); +#1020=IFCCLASSIFICATIONREFERENCE($,'[CO]CNA','Container',#1019,'moveable storing object for solid matters or persons',$); +#1021=IFCCLASSIFICATIONREFERENCE($,'[CO]CNB','Gas cylinder',#1019,'moveable storing object for gas',$); +#1022=IFCCLASSIFICATIONREFERENCE($,'[CO]CNC','Drum',#1019,'moveable storing object for liquid',$); +#1023=IFCCLASSIFICATIONREFERENCE($,'[CO]CP?','Thermal energy storing object',#1003,'storing object of thermal energy',$); +#1024=IFCCLASSIFICATIONREFERENCE($,'[CO]CPA','Liquid Tank',#1023,'thermal energy storing object of liquid',$); +#1025=IFCCLASSIFICATIONREFERENCE($,'[CO]CPB','Gas tank',#1023,'thermal energy storing object of gas',$); +#1026=IFCCLASSIFICATIONREFERENCE($,'[CO]CPC','Matter tank',#1023,'thermal energy storing object of solid matter',$); +#1027=IFCCLASSIFICATIONREFERENCE($,'[CO]CPD','Crystalline tank',#1023,'thermal energy storing object of state changing material',$); +#1028=IFCCLASSIFICATIONREFERENCE($,'[CO]CQ?','Mechanical energy storing object',#1003,'storing object of mechanical energy',$); +#1029=IFCCLASSIFICATIONREFERENCE($,'[CO]CQA','Flywheel',#1028,'mechanical energy storing object in the form of kinetic energy',$); +#1030=IFCCLASSIFICATIONREFERENCE($,'[CO]CQB','Spring',#1028,'mechanical energy storing object in the form of elastic energy',$); +#1031=IFCCLASSIFICATIONREFERENCE($,'[CO]CQC','Counterweight',#1028,'mechanical energy storing object in the form of gravitational energy',$); +#1032=IFCCLASSIFICATIONREFERENCE($,'[CO]E??','Emitting object',#900,'object for emitting',$); +#1033=IFCCLASSIFICATIONREFERENCE($,'[CO]EA?','Light object',#1032,'emitting object of light',$); +#1034=IFCCLASSIFICATIONREFERENCE($,'[CO]EAA','Electric lamp',#1033,'light object by electricity',$); +#1035=IFCCLASSIFICATIONREFERENCE($,'[CO]EAB','Gas lamp',#1033,'light object by gas combustion',$); +#1036=IFCCLASSIFICATIONREFERENCE($,'[CO]EAC','Liqiud lamp',#1033,'light object by liquid combustion',$); +#1037=IFCCLASSIFICATIONREFERENCE($,'[CO]EB?','Electric heating object',#1032,'emitting object of heat provided by electric energy',$); +#1038=IFCCLASSIFICATIONREFERENCE($,'[CO]EBA','Electric boiler',#1037,'electric heating object delivered by fluid',$); +#1039=IFCCLASSIFICATIONREFERENCE($,'[CO]EBB','Electric heating surface',#1037,'electric heating object delivered by a surface',$); +#1040=IFCCLASSIFICATIONREFERENCE($,'[CO]EBC','Heating cable',#1037,'electric heating object delivered by a cable',$); +#1041=IFCCLASSIFICATIONREFERENCE($,'[CO]EBD','Electric hot air blower',#1037,'electric heating object delivered by forced air',$); +#1042=IFCCLASSIFICATIONREFERENCE($,'[CO]EBE','Arc heating object',#1037,'electric heating object delivered by an electric arc',$); +#1043=IFCCLASSIFICATIONREFERENCE($,'[CO]EBF','Induction heater',#1037,'electric heating object delivered by a current flow',$); +#1044=IFCCLASSIFICATIONREFERENCE($,'[CO]EBG','Infrared heater',#1037,'electric heating object delivered by infrared radiation',$); +#1045=IFCCLASSIFICATIONREFERENCE($,'[CO]EBH','Electric oven',#1037,'electric heating object delivered by surrounding surfaces',$); +#1046=IFCCLASSIFICATIONREFERENCE($,'[CO]EC?','Electric cooling object',#1032,'emitting object of coldness provided by electric energy',$); +#1047=IFCCLASSIFICATIONREFERENCE($,'[CO]ECA','Electric cooling surface',#1046,'electric cooling object applying a cooling surface',$); +#1048=IFCCLASSIFICATIONREFERENCE($,'[CO]ECB','Electric cold air blower',#1046,'electric cooling object applying forced air',$); +#1049=IFCCLASSIFICATIONREFERENCE($,'[CO]ECC','Compression chiller',#1046,'electric cooling object utilizing phase changing',$); +#1050=IFCCLASSIFICATIONREFERENCE($,'[CO]EE?','Wireless power object',#1032,'emitting object of wireless power',$); +#1051=IFCCLASSIFICATIONREFERENCE($,'[CO]EEA','Inductive power antenna',#1050,'wireless power object utilizing inductive coupling',$); +#1052=IFCCLASSIFICATIONREFERENCE($,'[CO]EEB','Magnetron',#1050,'wireless power object utilizing microwaves',$); +#1053=IFCCLASSIFICATIONREFERENCE($,'[CO]EEC','X-ray source',#1050,'wireless power object utilizing x-rays',$); +#1054=IFCCLASSIFICATIONREFERENCE($,'[CO]EED','Gamma ray source',#1050,'wireless power object utilizing gamma rays',$); +#1055=IFCCLASSIFICATIONREFERENCE($,'[CO]EEE','Capacitive coupler',#1050,'wireless power object utilizing capacitive coupling',$); +#1056=IFCCLASSIFICATIONREFERENCE($,'[CO]EG?','Thermal energy transfer object',#1032,'emitting object of heat and coldness provided by transfer of thermal energy',$); +#1057=IFCCLASSIFICATIONREFERENCE($,'[CO]EGA','Heat pump',#1056,'thermal energy transfer object utilizing phase changing',$); +#1058=IFCCLASSIFICATIONREFERENCE($,'[CO]EGB','Peltier element',#1056,'thermal energy transfer object utilizing Peltier effect',$); +#1059=IFCCLASSIFICATIONREFERENCE($,'[CO]EGC','Heat exchanger',#1056,'thermal energy transfer object between flows by means of an intermediate material',$); +#1060=IFCCLASSIFICATIONREFERENCE($,'[CO]EM?','Combustion heating object',#1032,'emitting object of heat provided by combustion',$); +#1061=IFCCLASSIFICATIONREFERENCE($,'[CO]EMA','Stove',#1060,'combustion heating object for heating of space',$); +#1062=IFCCLASSIFICATIONREFERENCE($,'[CO]EMB','Combustion boiler',#1060,'combustion heating object for heating of fluid',$); +#1063=IFCCLASSIFICATIONREFERENCE($,'[CO]EMC','Burner',#1060,'combustion heating object for heating of solid matter',$); +#1064=IFCCLASSIFICATIONREFERENCE($,'[CO]EP?','Thermal heating object',#1032,'emitting object of heat provided by thermal energy',$); +#1065=IFCCLASSIFICATIONREFERENCE($,'[CO]EPA','Heating surface',#1064,'thermal heating object delivered by a permeable heated surface',$); +#1066=IFCCLASSIFICATIONREFERENCE($,'[CO]EPB','Heating tube',#1064,'thermal heating object delivered by heated fluid',$); +#1067=IFCCLASSIFICATIONREFERENCE($,'[CO]EPC','Heating panel',#1064,'thermal heating object delivered by a non-permeable heated surface',$); +#1068=IFCCLASSIFICATIONREFERENCE($,'[CO]EPD','Hot air blower',#1064,'thermal heating object delivered by forced air',$); +#1069=IFCCLASSIFICATIONREFERENCE($,'[CO]EQ?','Thermal cooling object',#1032,'emitting object of coldness provided by thermal energy',$); +#1070=IFCCLASSIFICATIONREFERENCE($,'[CO]EQA','Cooling surface',#1069,'thermal cooling object applying a permeable cold surface',$); +#1071=IFCCLASSIFICATIONREFERENCE($,'[CO]EQB','Cooling panel',#1069,'thermal cooling object applying a non-permeable cold surface',$); +#1072=IFCCLASSIFICATIONREFERENCE($,'[CO]EQC','Chiller',#1069,'thermal cooling object applying forced air',$); +#1073=IFCCLASSIFICATIONREFERENCE($,'[CO]EQD','Cooler',#1069,'thermal cooling object providing cold fluid',$); +#1074=IFCCLASSIFICATIONREFERENCE($,'[CO]EQE','Cooling tube',#1069,'thermal cooling object applying chilled liquid in tubes',$); +#1075=IFCCLASSIFICATIONREFERENCE($,'[CO]ET?','Nuclear powered heating object',#1032,'emitting object of heat provided by nuclear fission',$); +#1076=IFCCLASSIFICATIONREFERENCE($,'[CO]ETA','Boiling water reactor',#1075,'nuclear powered heating object providing hot water',$); +#1077=IFCCLASSIFICATIONREFERENCE($,'[CO]ETB','Pressurized water reactor',#1075,'nuclear powered heating object providing steam fluid',$); +#1078=IFCCLASSIFICATIONREFERENCE($,'[CO]ETC','High-temperature gas-cooled reactor',#1075,'nuclear powered heating object providing hot gas',$); +#1079=IFCCLASSIFICATIONREFERENCE($,'[CO]EU?','Particle emitting object',#1032,'emitting object of sub-atomic particles',$); +#1080=IFCCLASSIFICATIONREFERENCE($,'[CO]EUA','Plasma generator',#1079,'particle emitting object being ionized',$); +#1081=IFCCLASSIFICATIONREFERENCE($,'[CO]EUB','Particle generator',#1079,'particle emitting object being non-ionized',$); +#1082=IFCCLASSIFICATIONREFERENCE($,'[CO]EV?','Acoustic wave emitting object',#1032,'emitting object of acoustic waves',$); +#1083=IFCCLASSIFICATIONREFERENCE($,'[CO]EVA','Sonar',#1082,'acoustic wave emitting object utilizing ultrasonic frequencies',$); +#1084=IFCCLASSIFICATIONREFERENCE($,'[CO]EVB','Anti-noise loudspeaker',#1082,'acoustic wave emitting object utilizing audible frequencies for noise cancellation',$); +#1085=IFCCLASSIFICATIONREFERENCE($,'[CO]F??','Protecting object',#900,'object for protecting against the effects of dangerous or undesirable conditions',$); +#1086=IFCCLASSIFICATIONREFERENCE($,'[CO]FA?','Overvoltage protecting object',#1085,'protecting object related to overvoltages',$); +#1087=IFCCLASSIFICATIONREFERENCE($,'[CO]FAA','Spark gap surge arrester',#1086,'overvoltage protecting object by a spark gap',$); +#1088=IFCCLASSIFICATIONREFERENCE($,'[CO]FAB','Varistor surge arrester',#1086,'overvoltage protecting object by a varistor',$); +#1089=IFCCLASSIFICATIONREFERENCE($,'[CO]FAC','Zener diode',#1086,'overvoltage protecting object by a zener diode',$); +#1090=IFCCLASSIFICATIONREFERENCE($,'[CO]FAD','Surge absorber',#1086,'overvoltage protecting object by surge absorption',$); +#1091=IFCCLASSIFICATIONREFERENCE($,'[CO]FB?','Earth fault current protecting object',#1085,'protecting object related to earth fault currents',$); +#1092=IFCCLASSIFICATIONREFERENCE($,'[CO]FBA','Residual current device',#1091,'earth fault current protecting object that monitors an electric network and switches off in the event of earth fault currents',$); +#1093=IFCCLASSIFICATIONREFERENCE($,'[CO]FBB','Neutral earthing limiter',#1091,'earth fault current protecting object that limits the value of an earth fault current',$); +#1094=IFCCLASSIFICATIONREFERENCE($,'[CO]FC?','Overcurrent protecting object',#1085,'protecting object related to overcurrents',$); +#1095=IFCCLASSIFICATIONREFERENCE($,'[CO]FCA','Fuse',#1094,'overcurrent protecting object that by fusing opens the circuit in which it is inserted by breaking the current when this exceeds a given value for a sufficient time',$); +#1096=IFCCLASSIFICATIONREFERENCE($,'[CO]FCB','Miniature circuit-breaker',#1094,'overcurrent protecting object capable of making, carrying and breaking currents under normal circuit conditions and also making, carrying for a specified time, and automatically breaking currents under specified abnormal circuit conditions (this class is intended for circuit-breakers such as those defined in accordance with IEC 60898 series)',$); +#1097=IFCCLASSIFICATIONREFERENCE($,'[CO]FCC','Bimetal',#1094,'overcurrent protecting object that opens the circuit in which it is inserted by interrupting the current when this exceeds a given value for a sufficient time',$); +#1098=IFCCLASSIFICATIONREFERENCE($,'[CO]FE?','Field protecting object',#1085,'protecting object related to electric and/or magnetic fields',$); +#1099=IFCCLASSIFICATIONREFERENCE($,'[CO]FEA','Electric field shield',#1098,'field protecting object of electric fields',$); +#1100=IFCCLASSIFICATIONREFERENCE($,'[CO]FEB','Magnetic field shield',#1098,'field protecting object of magnetic fields',$); +#1101=IFCCLASSIFICATIONREFERENCE($,'[CO]FEC','Electromagnetic field shield',#1098,'field protecting object of electromagnetic fields',$); +#1102=IFCCLASSIFICATIONREFERENCE($,'[CO]FL?','Pressure protecting object',#1085,'protecting object related to pressure',$); +#1103=IFCCLASSIFICATIONREFERENCE($,'[CO]FLA','Safety valve',#1102,'pressure protecting object releasing steam or liquid when the pressure exceeds a set limit value',$); +#1104=IFCCLASSIFICATIONREFERENCE($,'[CO]FLB','Safety damper',#1102,'pressure protecting object opening or closing an air flow when the pressure is above or below a set limit value',$); +#1105=IFCCLASSIFICATIONREFERENCE($,'[CO]FLC','Vacuum breaker',#1102,'pressure protecting object preventing vacuum',$); +#1106=IFCCLASSIFICATIONREFERENCE($,'[CO]FLD','Rupture disc',#1102,'pressure protecting object that breaks open irreversibly in the event of excessive pressure difference',$); +#1107=IFCCLASSIFICATIONREFERENCE($,'[CO]FLE','Expansion tank',#1102,'pressure protecting object provided by a volume expansion',$); +#1108=IFCCLASSIFICATIONREFERENCE($,'[CO]FM?','Fire protecting object',#1085,'protecting object related to fire',$); +#1109=IFCCLASSIFICATIONREFERENCE($,'[CO]FMA','Fire damper',#1108,'fire protecting object closing an air flow on detection of fire',$); +#1110=IFCCLASSIFICATIONREFERENCE($,'[CO]FMB','Flue damper',#1108,'fire protecting object opening or closing an air flow on detection of smoke',$); +#1111=IFCCLASSIFICATIONREFERENCE($,'[CO]FMC','Fire-and-flue damper',#1108,'fire protecting object closing an air flow on detection of smoke and/or fire',$); +#1112=IFCCLASSIFICATIONREFERENCE($,'[CO]FMD','Fire extinguisher',#1108,'fire protecting object for manual operation',$); +#1113=IFCCLASSIFICATIONREFERENCE($,'[CO]FME','Fire blind',#1108,'fire protecting object closing a passage for preventing spread of smoke and/or fire',$); +#1114=IFCCLASSIFICATIONREFERENCE($,'[CO]FMF','Smoke curtain',#1108,'fire protecting object dividing an area into smoke zones',$); +#1115=IFCCLASSIFICATIONREFERENCE($,'[CO]FMG','Smoke closure',#1108,'fire protecting object closing a hole for preventing spread of fire',$); +#1116=IFCCLASSIFICATIONREFERENCE($,'[CO]FMH','Fire insulation',#1108,'fire protecting object by a surface cover',$); +#1117=IFCCLASSIFICATIONREFERENCE($,'[CO]FMJ','Fire retardant coating',#1108,'fire protecting object by a surface coating',$); +#1118=IFCCLASSIFICATIONREFERENCE($,'[CO]FMK','Fire impregnation',#1108,'fire protecting object by impregnation',$); +#1119=IFCCLASSIFICATIONREFERENCE($,'[CO]FN?','Mechanical force protecting object',#1085,'protecting object related to mechanical force',$); +#1120=IFCCLASSIFICATIONREFERENCE($,'[CO]FNA','Safety clutch',#1119,'mechanical force protecting object providing a disconnection when a torque becomes too high',$); +#1121=IFCCLASSIFICATIONREFERENCE($,'[CO]FNB','Impact protection',#1119,'mechanical force protecting object absorbing impact',$); +#1122=IFCCLASSIFICATIONREFERENCE($,'[CO]FQ?','Preventive protecting object',#1085,'protecting object by barrier or obstacle',$); +#1123=IFCCLASSIFICATIONREFERENCE($,'[CO]FQA','Protective mesh',#1122,'preventive protecting object against objects passing',$); +#1124=IFCCLASSIFICATIONREFERENCE($,'[CO]FQB','Glide protection',#1122,'preventive protecting object against objects or matter sliding or drifting',$); +#1125=IFCCLASSIFICATIONREFERENCE($,'[CO]FQC','Protective rod',#1122,'preventive protecting object against falling',$); +#1126=IFCCLASSIFICATIONREFERENCE($,'[CO]FQD','Protective rail',#1122,'preventive protecting object against falling out or colliding',$); +#1127=IFCCLASSIFICATIONREFERENCE($,'[CO]FQE','Protective fabric',#1122,'preventive protecting object against physical penetration',$); +#1128=IFCCLASSIFICATIONREFERENCE($,'[CO]FQF','Glare protection',#1122,'preventive protecting object against glare',$); +#1129=IFCCLASSIFICATIONREFERENCE($,'[CO]FQG','Splatter protection',#1122,'preventive protecting object against splatter',$); +#1130=IFCCLASSIFICATIONREFERENCE($,'[CO]FQH','Break protection',#1122,'preventive protecting object against manual breakage',$); +#1131=IFCCLASSIFICATIONREFERENCE($,'[CO]FQJ','Lock hatch cover',#1122,'preventive protecting object against manual unlocking',$); +#1132=IFCCLASSIFICATIONREFERENCE($,'[CO]FR?','Wear protection object',#1085,'protecting object related to material erosion',$); +#1133=IFCCLASSIFICATIONREFERENCE($,'[CO]FRA','Lubrication object',#1132,'wear protection object providing liquid material between objects',$); +#1134=IFCCLASSIFICATIONREFERENCE($,'[CO]FRB','Wear pad',#1132,'wear protection object providing a solid separating material between objects',$); +#1135=IFCCLASSIFICATIONREFERENCE($,'[CO]FS?','Environment protecting object',#1085,'protecting object related to the local environment',$); +#1136=IFCCLASSIFICATIONREFERENCE($,'[CO]FSA','Plaster',#1135,'environment protecting object in the form of a solidified paste layer covering another object',$); +#1137=IFCCLASSIFICATIONREFERENCE($,'[CO]FSB','Paint',#1135,'environment protecting object in the form of a hardened liquid film on the surface of another object',$); +#1138=IFCCLASSIFICATIONREFERENCE($,'[CO]FSC','Impregnation',#1135,'environment protecting object in the form of an applied or induced liquid making the object resistant to externally induced decay or destruction',$); +#1139=IFCCLASSIFICATIONREFERENCE($,'[CO]FSD','Shelter',#1135,'environment protecting object providing shelter against climatic effects',$); +#1140=IFCCLASSIFICATIONREFERENCE($,'[CO]FSE','Seismic vibration control device',#1135,'environment protecting object in the form of vibration control',$); +#1141=IFCCLASSIFICATIONREFERENCE($,'[CO]FSF','Corrosion protection',#1135,'environment protection object in the form of oxidation of metal',$); +#1142=IFCCLASSIFICATIONREFERENCE($,'[CO]FSG','Protective seal',#1135,'environment protecting object in the form of a seal on the surface of another object',$); +#1143=IFCCLASSIFICATIONREFERENCE($,'[CO]FSH','Material separating layer',#1135,'environment protecting object in the form of a capillary action breaking layer',$); +#1144=IFCCLASSIFICATIONREFERENCE($,'[CO]G??','Generating object',#900,'object for providing a controllable flow',$); +#1145=IFCCLASSIFICATIONREFERENCE($,'[CO]GA?','Mechanical to electrical energy generating object',#1144,'generating object of electric energy powered by mechanical energy',$); +#1146=IFCCLASSIFICATIONREFERENCE($,'[CO]GAA','AC generator',#1145,'mechanical to electrical energy generating object of alternating current',$); +#1147=IFCCLASSIFICATIONREFERENCE($,'[CO]GAB','DC generator',#1145,'mechanical to electrical energy generating object of direct current',$); +#1148=IFCCLASSIFICATIONREFERENCE($,'[CO]GB?','Chemical to electrical energy generating object',#1144,'generating object of electric energy powered by chemical reaction',$); +#1149=IFCCLASSIFICATIONREFERENCE($,'[CO]GBA','Electric battery',#1148,'Chemical to electrical energy generating object from a non-renewable source',$); +#1150=IFCCLASSIFICATIONREFERENCE($,'[CO]GBB','Fuel cell',#1148,'Chemical to electrical energy generating object continuously fed by fuel and oxidant',$); +#1151=IFCCLASSIFICATIONREFERENCE($,'[CO]GC?','Solar to electrical energy generating object',#1144,'generating object of electric energy from solar energy',$); +#1152=IFCCLASSIFICATIONREFERENCE($,'[CO]GCA','Crystalline photovoltaic module',#1151,'Solar to electrical energy generating object utilizing wafer based semiconducting material',$); +#1153=IFCCLASSIFICATIONREFERENCE($,'[CO]GCB','Thin film photovoltaic module',#1151,'Solar to electrical energy generating object utilizing thin film material',$); +#1154=IFCCLASSIFICATIONREFERENCE($,'[CO]GF?','Signal generating object',#1144,'generating object of signals for carrier of information',$); +#1155=IFCCLASSIFICATIONREFERENCE($,'[CO]GFA','Electromagnetic wave transmitter',#1154,'signal generating object by electromagnetic waves',$); +#1156=IFCCLASSIFICATIONREFERENCE($,'[CO]GFB','Signal generator',#1154,'signal generating object by wires',$); +#1157=IFCCLASSIFICATIONREFERENCE($,'[CO]GL?','Continuous transfer object',#1144,'generating object continuously of items',$); +#1158=IFCCLASSIFICATIONREFERENCE($,'[CO]GLA','Belt conveyer',#1157,'continuous transfer object on a belt',$); +#1159=IFCCLASSIFICATIONREFERENCE($,'[CO]GLB','Chain conveyer',#1157,'continuous transfer object on a chain',$); +#1160=IFCCLASSIFICATIONREFERENCE($,'[CO]GLC','Roller conveyer',#1157,'continuous transfer object on rollers',$); +#1161=IFCCLASSIFICATIONREFERENCE($,'[CO]GLD','Screw conveyer',#1157,'continuous transfer object propelled by a screw',$); +#1162=IFCCLASSIFICATIONREFERENCE($,'[CO]GLE','Escalator',#1157,'continuous transfer object consisting of a chain of steps',$); +#1163=IFCCLASSIFICATIONREFERENCE($,'[CO]GLF','Paternoster',#1157,'continuous transfer object consisting of chain of containers',$); +#1164=IFCCLASSIFICATIONREFERENCE($,'[CO]GLG','Vibrating conveyor',#1157,'continuous transfer object on a vibrating surface',$); +#1165=IFCCLASSIFICATIONREFERENCE($,'[CO]GM?','Discontinuous transfer object',#1144,'generating object discontinuously of items',$); +#1166=IFCCLASSIFICATIONREFERENCE($,'[CO]GMA','Wagon',#1165,'discontinuous transfer object horizontally',$); +#1167=IFCCLASSIFICATIONREFERENCE($,'[CO]GMB','Elevator',#1165,'discontinuous transfer object vertically',$); +#1168=IFCCLASSIFICATIONREFERENCE($,'[CO]GMC','Crane',#1165,'discontinuous transfer object lifting and moving objects vertically and/or horizontally',$); +#1169=IFCCLASSIFICATIONREFERENCE($,'[CO]GMD','Palletizer',#1165,'discontinuous transfer object placing objects together',$); +#1170=IFCCLASSIFICATIONREFERENCE($,'[CO]GME','Turntable',#1165,'discontinuous transfer object turning a conveyed flow',$); +#1171=IFCCLASSIFICATIONREFERENCE($,'[CO]GMF','Vibrating feeder',#1165,'discontinuous transfer object moving objects in a linear flow by vibrating',$); +#1172=IFCCLASSIFICATIONREFERENCE($,'[CO]GP?','Liquid flow generating object',#1144,'generating object of liquid',$); +#1173=IFCCLASSIFICATIONREFERENCE($,'[CO]GPA','Positive displacement pump',#1172,'liquid flow generating object by forcing trapped liquid to the discharge side',$); +#1174=IFCCLASSIFICATIONREFERENCE($,'[CO]GPB','Liquid velocity pump',#1172,'liquid flow generating object by increasing the flow velocity through mechanical motion',$); +#1175=IFCCLASSIFICATIONREFERENCE($,'[CO]GPC','Hydraulic ram pump',#1172,'liquid flow generating object by transferring the kinetic energy of an input flow to an output flow',$); +#1176=IFCCLASSIFICATIONREFERENCE($,'[CO]GPD','Lubricator',#1172,'liquid flow generating object by converting potential energy to kinetic energy by gravity',$); +#1177=IFCCLASSIFICATIONREFERENCE($,'[CO]GQ?','Gaseous flow generating object',#1144,'generating object of gas',$); +#1178=IFCCLASSIFICATIONREFERENCE($,'[CO]GQA','Gas compressor',#1177,'gaseous flow generating object by forcing trapped gas to the discharge side',$); +#1179=IFCCLASSIFICATIONREFERENCE($,'[CO]GQB','Mechanical fan',#1177,'gaseous flow generating object by increasing the flow velocity through mechanical motion',$); +#1180=IFCCLASSIFICATIONREFERENCE($,'[CO]GQC','Gas ejector',#1177,'gaseous flow generating object by transferring the kinetic energy of an input flow to an output flow',$); +#1181=IFCCLASSIFICATIONREFERENCE($,'[CO]GR?','Solar to thermal energy generating object',#1144,'generating object of thermal energy from solar energy',$); +#1182=IFCCLASSIFICATIONREFERENCE($,'[CO]GRA','Solar panel',#1181,'solar to thermal energy generating object into a fluid thermal flow',$); +#1183=IFCCLASSIFICATIONREFERENCE($,'[CO]GRB','Solar tower',#1181,'solar to thermal energy generating object generating a fluid flow',$); +#1184=IFCCLASSIFICATIONREFERENCE($,'[CO]H??','Matter processing object',#900,'object for treating matter',$); +#1185=IFCCLASSIFICATIONREFERENCE($,'[CO]HJ?','Primary forming object',#1184,'matter processing object by primary forming',$); +#1186=IFCCLASSIFICATIONREFERENCE($,'[CO]HJA','Casting tool',#1185,'primary forming object from liquid initial material state',$); +#1187=IFCCLASSIFICATIONREFERENCE($,'[CO]HJB','Press',#1185,'primary forming object from solid matter initial material state',$); +#1188=IFCCLASSIFICATIONREFERENCE($,'[CO]HK?','Surface treatment object',#1184,'matter processing object for surface treatment',$); +#1189=IFCCLASSIFICATIONREFERENCE($,'[CO]HKA','Surface preparation machine',#1188,'surface treatment object by preparation',$); +#1190=IFCCLASSIFICATIONREFERENCE($,'[CO]HKB','Surface modification machine',#1188,'surface treatment object by modification',$); +#1191=IFCCLASSIFICATIONREFERENCE($,'[CO]HKC','Surface coating machine',#1188,'surface treatment object by coating',$); +#1192=IFCCLASSIFICATIONREFERENCE($,'[CO]HL?','Assembling object',#1184,'matter processing object by joining or assembling',$); +#1193=IFCCLASSIFICATIONREFERENCE($,'[CO]HLA','Assembly robot',#1192,'assembling object by physical assembly',$); +#1194=IFCCLASSIFICATIONREFERENCE($,'[CO]HLB','Mechanical joining machine',#1192,'assembling object by mechanical joining',$); +#1195=IFCCLASSIFICATIONREFERENCE($,'[CO]HLC','Thermal welding machine',#1192,'assembling object by thermal joining',$); +#1196=IFCCLASSIFICATIONREFERENCE($,'[CO]HLD','Chemical joining machine',#1192,'assembling object by chemical joining',$); +#1197=IFCCLASSIFICATIONREFERENCE($,'[CO]HM?','Force separating object',#1184,'matter processing object by separating mixed substances by force',$); +#1198=IFCCLASSIFICATIONREFERENCE($,'[CO]HMA','Settling tank',#1197,'force separating object of particles by gravity',$); +#1199=IFCCLASSIFICATIONREFERENCE($,'[CO]HMB','Centrifuge',#1197,'force separating object of substances by rotation in a rotating chamber',$); +#1200=IFCCLASSIFICATIONREFERENCE($,'[CO]HMC','Cyclone',#1197,'force separating object of substances by rotation in a fixed chamber',$); +#1201=IFCCLASSIFICATIONREFERENCE($,'[CO]HP?','Thermal separating object',#1184,'matter processing object by thermally separating mixed substances',$); +#1202=IFCCLASSIFICATIONREFERENCE($,'[CO]HPA','Dryer',#1201,'thermal separating object removing moisture',$); +#1203=IFCCLASSIFICATIONREFERENCE($,'[CO]HPB','Distillation column',#1201,'thermal separating object generating the vaporized aggregation of a liquid',$); +#1204=IFCCLASSIFICATIONREFERENCE($,'[CO]HQ?','Mechanical separating object',#1184,'matter processing object by mechanically separating mixed substances',$); +#1205=IFCCLASSIFICATIONREFERENCE($,'[CO]HQA','Skimmer',#1204,'mechanical separating object by removing substance from the surface of a liquid',$); +#1206=IFCCLASSIFICATIONREFERENCE($,'[CO]HQB','Separation grate',#1204,'mechanical separating object of particles based on their size and/or shape',$); +#1207=IFCCLASSIFICATIONREFERENCE($,'[CO]HR?','Electric or magnetic separating object',#1184,'matter processing object by electromagnetically separating mixed substances',$); +#1208=IFCCLASSIFICATIONREFERENCE($,'[CO]HRA','Electrostatic separator',#1207,'electric or magnetic separating object utilising an electric field',$); +#1209=IFCCLASSIFICATIONREFERENCE($,'[CO]HRB','Magnetic separator',#1207,'electric or magnetic separating object separating object utilising a magnetic field',$); +#1210=IFCCLASSIFICATIONREFERENCE($,'[CO]HS?','Chemical separating object',#1184,'matter processing object by chemically separating mixed substances',$); +#1211=IFCCLASSIFICATIONREFERENCE($,'[CO]HSA','Ion exchanger',#1210,'chemical separating object utilising exchange of ions',$); +#1212=IFCCLASSIFICATIONREFERENCE($,'[CO]HSB','Absorber',#1210,'chemical separating object utilising absorption',$); +#1213=IFCCLASSIFICATIONREFERENCE($,'[CO]HSC','Adsorber',#1210,'chemical separating object utilising adsorption',$); +#1214=IFCCLASSIFICATIONREFERENCE($,'[CO]HU?','Grinding and crushing object',#1184,'matter processing object providing smaller forms of solid material',$); +#1215=IFCCLASSIFICATIONREFERENCE($,'[CO]HUA','Cutter',#1214,'grinding and crushing object for soft, medium-hard, fibrous and tough matter',$); +#1216=IFCCLASSIFICATIONREFERENCE($,'[CO]HUB','Mill',#1214,'grinding and crushing object for machining parts into precise sizes and shapes',$); +#1217=IFCCLASSIFICATIONREFERENCE($,'[CO]HUC','Crusher',#1214,'grinding and crushing object of rock kind',$); +#1218=IFCCLASSIFICATIONREFERENCE($,'[CO]HV?','Agglomerating object',#1184,'matter processing object providing larger forms of substances',$); +#1219=IFCCLASSIFICATIONREFERENCE($,'[CO]HVA','Pelletizer',#1218,'agglomerating object compressing or moulding a material into the shape of a pellet',$); +#1220=IFCCLASSIFICATIONREFERENCE($,'[CO]HVB','Flocculator',#1218,'agglomerating object enabling (a process wherein) colloids to come out of suspension in the form of floc or flake',$); +#1221=IFCCLASSIFICATIONREFERENCE($,'[CO]HW?','Mixing object',#1184,'matter processing object mixing different substances',$); +#1222=IFCCLASSIFICATIONREFERENCE($,'[CO]HWA','Mixer',#1221,'mixing object that provides a new property substance from different substances',$); +#1223=IFCCLASSIFICATIONREFERENCE($,'[CO]HWB','Humidifier',#1221,'mixing object providing humid gaseous substance',$); +#1224=IFCCLASSIFICATIONREFERENCE($,'[CO]HX?','Reacting object',#1184,'matter processing object providing new material by chemical or biological reaction',$); +#1225=IFCCLASSIFICATIONREFERENCE($,'[CO]HXA','Chemical reactor',#1224,'reacting object enabling and controlling a chemical reaction',$); +#1226=IFCCLASSIFICATIONREFERENCE($,'[CO]HXB','Fermenter',#1224,'reacting object enabling and controlling a biological reaction',$); +#1227=IFCCLASSIFICATIONREFERENCE($,'[CO]K??','Information processing object',#900,'object for treating input signals and providing an appropriate output',$); +#1228=IFCCLASSIFICATIONREFERENCE($,'[CO]KE?','Electric signal processing object',#1227,'information processing object for processing electric signals',$); +#1229=IFCCLASSIFICATIONREFERENCE($,'[CO]KEA','Computer',#1228,'electric signal processing object with human intervention',$); +#1230=IFCCLASSIFICATIONREFERENCE($,'[CO]KEB','Control unit',#1228,'electric signal processing object without human intervention for control of devices',$); +#1231=IFCCLASSIFICATIONREFERENCE($,'[CO]KEC','Communication device',#1228,'electric signal processing object without human intervention providing input/output to devices',$); +#1232=IFCCLASSIFICATIONREFERENCE($,'[CO]KED','Router',#1228,'electric signal processing object without human intervention forwarding signals between data networks',$); +#1233=IFCCLASSIFICATIONREFERENCE($,'[CO]KEE','Card writer',#1228,'electric signal processing object without human intervention saving the information provided by the input on a storage device',$); +#1234=IFCCLASSIFICATIONREFERENCE($,'[CO]KF?','Electric signal relaying object',#1227,'information processing object for relaying electric signals',$); +#1235=IFCCLASSIFICATIONREFERENCE($,'[CO]KFA','Relay',#1234,'electric signal relaying object by forwarding without delay',$); +#1236=IFCCLASSIFICATIONREFERENCE($,'[CO]KFB','Time relay',#1234,'electric signal relaying object by forwarding with delay',$); +#1237=IFCCLASSIFICATIONREFERENCE($,'[CO]KFC','Repeater',#1234,'electric signal relaying object by repeating the input signal as an improved output signal',$); +#1238=IFCCLASSIFICATIONREFERENCE($,'[CO]KFD','Electric network bridge',#1234,'electric signal relaying object by bridging multiple data network segments',$); +#1239=IFCCLASSIFICATIONREFERENCE($,'[CO]KFE','Electric network switch',#1234,'electric signal relaying object by forwarding the input to specific devices within a data network',$); +#1240=IFCCLASSIFICATIONREFERENCE($,'[CO]KG?','Optical signalling object',#1227,'information processing object of optical signals',$); +#1241=IFCCLASSIFICATIONREFERENCE($,'[CO]KGA','Optical network switch',#1240,'optical signalling object actively by forwarding the input to specific devices within an optical network',$); +#1242=IFCCLASSIFICATIONREFERENCE($,'[CO]KGB','Optical router',#1240,'optical signalling object forwarding data packages between computer networks',$); +#1243=IFCCLASSIFICATIONREFERENCE($,'[CO]KGC','Optical repeater',#1240,'optical signalling object by repeating the input signal as an improved output signal',$); +#1244=IFCCLASSIFICATIONREFERENCE($,'[CO]KH?','Fluid signalling object',#1227,'information processing object of fluid signals',$); +#1245=IFCCLASSIFICATIONREFERENCE($,'[CO]KHA','Fluid controller',#1244,'fluid signalling object for feedback control',$); +#1246=IFCCLASSIFICATIONREFERENCE($,'[CO]KHB','Pilot valve',#1244,'fluid signalling object relaying signals',$); +#1247=IFCCLASSIFICATIONREFERENCE($,'[CO]KJ?','Mechanical signalling object',#1227,'information processing object of mechanical signals',$); +#1248=IFCCLASSIFICATIONREFERENCE($,'[CO]KJA','Mechanical regulator',#1247,'mechanical signalling object adjusting an air or fluid flow',$); +#1249=IFCCLASSIFICATIONREFERENCE($,'[CO]KJB','Locking cylinder',#1247,'mechanical signalling object for locking/unlocking of a locking mechanism',$); +#1250=IFCCLASSIFICATIONREFERENCE($,'[CO]KJC','Key coded device',#1247,'mechanical signalling object providing a binary state of a mechanical device',$); +#1251=IFCCLASSIFICATIONREFERENCE($,'[CO]KZ?','Multiple kind signalling object',#1227,'information processing object of signals of multiple kinds',$); +#1252=IFCCLASSIFICATIONREFERENCE($,'[CO]KZA','Multiple kind switch',#1251,'multiple kind signalling object forwarding the input to specific devices',$); +#1253=IFCCLASSIFICATIONREFERENCE($,'[CO]M??','Driving object',#900,'object for providing mechanical movement or force',$); +#1254=IFCCLASSIFICATIONREFERENCE($,'[CO]MA?','Electromagnetic rotational driving object',#1253,'driving object in the form of rotational torque, powered by electromagnetic force',$); +#1255=IFCCLASSIFICATIONREFERENCE($,'[CO]MAA','Electric motor',#1254,'electromagnetic rotational driving object providing a continuous movement',$); +#1256=IFCCLASSIFICATIONREFERENCE($,'[CO]MAB','Stepper motor',#1254,'electromagnetic rotational driving object providing discrete steps',$); +#1257=IFCCLASSIFICATIONREFERENCE($,'[CO]MB?','Electromagnetic linear driving object',#1253,'driving object in the form of linear force, powered by electromagnetic force',$); +#1258=IFCCLASSIFICATIONREFERENCE($,'[CO]MBA','Linear motor',#1257,'electromagnetic linear driving object providing a continuous movement',$); +#1259=IFCCLASSIFICATIONREFERENCE($,'[CO]MBB','Electromagnet',#1257,'electromagnetic linear driving object providing discrete steps',$); +#1260=IFCCLASSIFICATIONREFERENCE($,'[CO]MC?','Magnetic force driving object',#1253,'driving object powered by magnetic force',$); +#1261=IFCCLASSIFICATIONREFERENCE($,'[CO]MCA','Permanent magnet',#1260,'magnetic force driving object by permanent magnet',$); +#1262=IFCCLASSIFICATIONREFERENCE($,'[CO]MD?','Piezoelectric driving object',#1253,'driving object powered by piezoelectric force',$); +#1263=IFCCLASSIFICATIONREFERENCE($,'[CO]MDA','Piezoelectric motor',#1262,'piezoelectric driving object by piezoelectric crystal',$); +#1264=IFCCLASSIFICATIONREFERENCE($,'[CO]ML?','Mechanical energy driving object',#1253,'driving object powered by mechanical energy',$); +#1265=IFCCLASSIFICATIONREFERENCE($,'[CO]MLA','Gravity energy drive',#1264,'mechanical energy driving object releasing gravitational energy',$); +#1266=IFCCLASSIFICATIONREFERENCE($,'[CO]MLB','Elastic energy drive',#1264,'mechanical energy driving object releasing elastic energy',$); +#1267=IFCCLASSIFICATIONREFERENCE($,'[CO]MLC','Wind turbine',#1264,'mechanical energy driving object in the form of kinetic energy in blowing air',$); +#1268=IFCCLASSIFICATIONREFERENCE($,'[CO]MLD','Water turbine',#1264,'mechanical energy driving object in the form of kinetic energy in streaming water',$); +#1269=IFCCLASSIFICATIONREFERENCE($,'[CO]MLE','Gas turbine',#1264,'mechanical energy driving object in the form of kinetic energy and reaction force in streaming and/or expanding gases',$); +#1270=IFCCLASSIFICATIONREFERENCE($,'[CO]MM?','Fluid powered driving object',#1253,'driving object powered by fluid displacement or pressure',$); +#1271=IFCCLASSIFICATIONREFERENCE($,'[CO]MMA','Hydraulic cylinder',#1270,'fluid powered driving object providing movement corresponding to a liquid volume',$); +#1272=IFCCLASSIFICATIONREFERENCE($,'[CO]MMB','Pneumatic cylinder',#1270,'fluid powered driving object providing movement to end positions depending on the gaseous pressure',$); +#1273=IFCCLASSIFICATIONREFERENCE($,'[CO]MMC','Hydraulic motor',#1270,'fluid powered driving object providing rotation by a flowing liquid',$); +#1274=IFCCLASSIFICATIONREFERENCE($,'[CO]MMD','Pneumatic motor',#1270,'fluid powered driving object providing rotation by a pressurized gas',$); +#1275=IFCCLASSIFICATIONREFERENCE($,'[CO]MS?','Combustion engine',#1253,'driving object powered by combustion of fuels',$); +#1276=IFCCLASSIFICATIONREFERENCE($,'[CO]MSA','Otto cycle engine',#1275,'combustion engine in cylinders with pistons powered by spark ignited combustion',$); +#1277=IFCCLASSIFICATIONREFERENCE($,'[CO]MSB','Diesel cycle engine',#1275,'combustion engine in cylinders with pistons powered by compressed ignited combustion',$); +#1278=IFCCLASSIFICATIONREFERENCE($,'[CO]MSC','Wankel engine',#1275,'combustion engine in circular compartment with eccentric rotary piston powered by spark ignited combustion',$); +#1279=IFCCLASSIFICATIONREFERENCE($,'[CO]MT?','Heat engine',#1253,'driving object powered by an external heat source',$); +#1280=IFCCLASSIFICATIONREFERENCE($,'[CO]MTA','Steam engine',#1279,'heat engine utilizing expanding steam acting on one piston',$); +#1281=IFCCLASSIFICATIONREFERENCE($,'[CO]MTB','Stirling Engine',#1279,'heat engine utilizing two pistons within one heated and one cooled cylinder or cylinder part',$); +#1282=IFCCLASSIFICATIONREFERENCE($,'[CO]N??','Covering object',#900,'object for enclosing partly or fully another object',$); +#1283=IFCCLASSIFICATIONREFERENCE($,'[CO]NA?','Infilling object',#1282,'covering object by filling an opening',$); +#1284=IFCCLASSIFICATIONREFERENCE($,'[CO]NAA','Pane',#1283,'infilling object permitting light entry',$); +#1285=IFCCLASSIFICATIONREFERENCE($,'[CO]NAB','Panel',#1283,'infilling object preventing entry of light and solid objects',$); +#1286=IFCCLASSIFICATIONREFERENCE($,'[CO]NAC','Grating',#1283,'infilling object permitting fluid entry',$); +#1287=IFCCLASSIFICATIONREFERENCE($,'[CO]NAD','Sealing joint',#1283,'infilling object preventing fluid entry',$); +#1288=IFCCLASSIFICATIONREFERENCE($,'[CO]NB?','Closure object',#1282,'covering object by closing an opening',$); +#1289=IFCCLASSIFICATIONREFERENCE($,'[CO]NBA','Door leaf',#1288,'closure object in a door',$); +#1290=IFCCLASSIFICATIONREFERENCE($,'[CO]NBB','Leaf of large door',#1288,'closure object in a large door',$); +#1291=IFCCLASSIFICATIONREFERENCE($,'[CO]NBC','Cabinet door',#1288,'closure object in an enclosure',$); +#1292=IFCCLASSIFICATIONREFERENCE($,'[CO]NBD','Gate leaf',#1288,'closure object in a gate',$); +#1293=IFCCLASSIFICATIONREFERENCE($,'[CO]NBE','Hatch cover',#1288,'closure object in a hatch',$); +#1294=IFCCLASSIFICATIONREFERENCE($,'[CO]NC?','Finishing object',#1282,'covering object by finishing structures',$); +#1295=IFCCLASSIFICATIONREFERENCE($,'[CO]NCA','Paving',#1294,'finishing object of a pavement',$); +#1296=IFCCLASSIFICATIONREFERENCE($,'[CO]NCB','Wall covering',#1294,'finishing object of a wall',$); +#1297=IFCCLASSIFICATIONREFERENCE($,'[CO]NCC','Flooring',#1294,'finishing object of a floor',$); +#1298=IFCCLASSIFICATIONREFERENCE($,'[CO]NCD','Ceiling finish',#1294,'finishing object of a ceiling',$); +#1299=IFCCLASSIFICATIONREFERENCE($,'[CO]NCE','Roofing',#1294,'finishing object of a roof',$); +#1300=IFCCLASSIFICATIONREFERENCE($,'[CO]NCF','Window sill',#1294,'finishing object of an opening',$); +#1301=IFCCLASSIFICATIONREFERENCE($,'[CO]NCG','Plant mat',#1294,'finishing object of a vegetation area',$); +#1302=IFCCLASSIFICATIONREFERENCE($,'[CO]NCH','Surface forming object',#1294,'finishing object by forming the outer shape of an object',$); +#1303=IFCCLASSIFICATIONREFERENCE($,'[CO]ND?','Terminating object',#1282,'covering object by terminating another object',$); +#1304=IFCCLASSIFICATIONREFERENCE($,'[CO]NDA','Curb',#1303,'terminating object delimiting an edge of pavement or vegetation area',$); +#1305=IFCCLASSIFICATIONREFERENCE($,'[CO]NDB','Crown',#1303,'terminating object delimiting an edge of a wall',$); +#1306=IFCCLASSIFICATIONREFERENCE($,'[CO]NDC','Raised floor edge',#1303,'terminating object delimiting an edge of a floor',$); +#1307=IFCCLASSIFICATIONREFERENCE($,'[CO]NDD','Ceiling edge',#1303,'terminating object delimiting an edge of a ceiling',$); +#1308=IFCCLASSIFICATIONREFERENCE($,'[CO]NDE','Fascia',#1303,'terminating object delimiting an edge of a roof',$); +#1309=IFCCLASSIFICATIONREFERENCE($,'[CO]NE?','Hiding object',#1282,'covering object by hiding joints and transitions',$); +#1310=IFCCLASSIFICATIONREFERENCE($,'[CO]NEA','Ceiling lining',#1309,'hiding object between a wall and a ceiling',$); +#1311=IFCCLASSIFICATIONREFERENCE($,'[CO]NEB','Skirting board',#1309,'hiding object between a wall and a floor',$); +#1312=IFCCLASSIFICATIONREFERENCE($,'[CO]NEC','Wall corner lining',#1309,'hiding object between two walls',$); +#1313=IFCCLASSIFICATIONREFERENCE($,'[CO]NED','Flashing',#1309,'hiding object on a roof',$); +#1314=IFCCLASSIFICATIONREFERENCE($,'[CO]NEE','Reveal liner',#1309,'hiding object around an opening',$); +#1315=IFCCLASSIFICATIONREFERENCE($,'[CO]P??','Presenting object',#900,'object for providing perceptible information',$); +#1316=IFCCLASSIFICATIONREFERENCE($,'[CO]PF?','Visible state indicator',#1315,'presenting object in visible form of discrete states',$); +#1317=IFCCLASSIFICATIONREFERENCE($,'[CO]PFA','Signal lamp',#1316,'visible state indicator by light',$); +#1318=IFCCLASSIFICATIONREFERENCE($,'[CO]PFB','Semaphore',#1316,'visible state indicator by mechanical positions',$); +#1319=IFCCLASSIFICATIONREFERENCE($,'[CO]PG?','Scalar display',#1315,'presenting object in a visible form of a single scalar variable',$); +#1320=IFCCLASSIFICATIONREFERENCE($,'[CO]PGA','Voltmeter',#1319,'scalar display representing electric voltage',$); +#1321=IFCCLASSIFICATIONREFERENCE($,'[CO]PGB','Resistivity indicator',#1319,'scalar display representing resistivity or conductivity',$); +#1322=IFCCLASSIFICATIONREFERENCE($,'[CO]PGC','Ampere meter',#1319,'scalar display representing electric current',$); +#1323=IFCCLASSIFICATIONREFERENCE($,'[CO]PGD','Density indicator',#1319,'scalar display representing density',$); +#1324=IFCCLASSIFICATIONREFERENCE($,'[CO]PGE','Field indicator',#1319,'scalar display representing field',$); +#1325=IFCCLASSIFICATIONREFERENCE($,'[CO]PGF','Flow meter',#1319,'scalar display representing flow',$); +#1326=IFCCLASSIFICATIONREFERENCE($,'[CO]PGG','Dimension indicator',#1319,'scalar display representing spatial dimension and/or position',$); +#1327=IFCCLASSIFICATIONREFERENCE($,'[CO]PGH','Energy meter',#1319,'scalar display representing energy',$); +#1328=IFCCLASSIFICATIONREFERENCE($,'[CO]PGJ','Wattmeter',#1319,'scalar display representing electric power',$); +#1329=IFCCLASSIFICATIONREFERENCE($,'[CO]PGK','Clock',#1319,'scalar display representing time',$); +#1330=IFCCLASSIFICATIONREFERENCE($,'[CO]PGL','Level indicator',#1319,'scalar display representing level',$); +#1331=IFCCLASSIFICATIONREFERENCE($,'[CO]PGM','Humidity indicator',#1319,'scalar display representing humidity',$); +#1332=IFCCLASSIFICATIONREFERENCE($,'[CO]PGP','Pressure indicator',#1319,'scalar display representing pressure',$); +#1333=IFCCLASSIFICATIONREFERENCE($,'[CO]PGQ','Concentration indicator',#1319,'scalar display representing substance concentration',$); +#1334=IFCCLASSIFICATIONREFERENCE($,'[CO]PGR','Electromagnetic wave indicator',#1319,'scalar display representing radiation',$); +#1335=IFCCLASSIFICATIONREFERENCE($,'[CO]PGS','Frequency meter',#1319,'scalar display representing time-rate',$); +#1336=IFCCLASSIFICATIONREFERENCE($,'[CO]PGT','Temperature indicator',#1319,'scalar display representing temperature',$); +#1337=IFCCLASSIFICATIONREFERENCE($,'[CO]PGV','Cos-phi meter',#1319,'scalar display representing the offset between active power and reactive power',$); +#1338=IFCCLASSIFICATIONREFERENCE($,'[CO]PGW','Force indicator',#1319,'scalar display representing weight, force to torque',$); +#1339=IFCCLASSIFICATIONREFERENCE($,'[CO]PGX','Audio spectrometer',#1319,'scalar display representing sound',$); +#1340=IFCCLASSIFICATIONREFERENCE($,'[CO]PGZ','Quantity meter',#1319,'scalar display representing number of incidents',$); +#1341=IFCCLASSIFICATIONREFERENCE($,'[CO]PH?','Graphical display',#1315,'presenting object in visible form by pictures, symbols, text or physical characteristics',$); +#1342=IFCCLASSIFICATIONREFERENCE($,'[CO]PHA','Display',#1341,'graphical display dynamically on its surface',$); +#1343=IFCCLASSIFICATIONREFERENCE($,'[CO]PHB','Projector',#1341,'graphical display dynamically on another surface',$); +#1344=IFCCLASSIFICATIONREFERENCE($,'[CO]PHC','Printer',#1341,'graphical display on a printed media',$); +#1345=IFCCLASSIFICATIONREFERENCE($,'[CO]PHD','Sign',#1341,'graphical display statically on its surface',$); +#1346=IFCCLASSIFICATIONREFERENCE($,'[CO]PHE','Marking',#1341,'graphical display statically on the surface of another object',$); +#1347=IFCCLASSIFICATIONREFERENCE($,'[CO]PHF','Marking post',#1341,'graphical display statically by its physical characteristics',$); +#1348=IFCCLASSIFICATIONREFERENCE($,'[CO]PJ?','Acoustic device',#1315,'presenting object in audible form',$); +#1349=IFCCLASSIFICATIONREFERENCE($,'[CO]PJA','Loudspeaker',#1348,'acoustic device provided by the movement of a membrane',$); +#1350=IFCCLASSIFICATIONREFERENCE($,'[CO]PJB','Horn',#1348,'acoustic device provided by the stream of air',$); +#1351=IFCCLASSIFICATIONREFERENCE($,'[CO]PJC','Bell',#1348,'acoustic device provided by mechanical excitation of the object',$); +#1352=IFCCLASSIFICATIONREFERENCE($,'[CO]PK?','Tactile device',#1315,'presenting object in tactile form',$); +#1353=IFCCLASSIFICATIONREFERENCE($,'[CO]PKA','Vibrator',#1352,'tactile device of discrete states by vibration',$); +#1354=IFCCLASSIFICATIONREFERENCE($,'[CO]PKB','Braille text sign',#1352,'tactile device of static information',$); +#1355=IFCCLASSIFICATIONREFERENCE($,'[CO]PKC','Free height warning',#1352,'tactile device for suprajacent obstacles',$); +#1356=IFCCLASSIFICATIONREFERENCE($,'[CO]PL?','Ornamental object',#1315,'presenting object in an ornamental form',$); +#1357=IFCCLASSIFICATIONREFERENCE($,'[CO]PLA','Cornice',#1356,'ornamental object as a separation between two areas',$); +#1358=IFCCLASSIFICATIONREFERENCE($,'[CO]PLB','Figure',#1356,'ornamental object as a spatial representation of something',$); +#1359=IFCCLASSIFICATIONREFERENCE($,'[CO]PLC','Frieze',#1356,'ornamental object as protruding figures and/or decorative elements on a wall',$); +#1360=IFCCLASSIFICATIONREFERENCE($,'[CO]PLD','Picture',#1356,'ornamental object representing, illustrating or reproducing something visibly on a surface',$); +#1361=IFCCLASSIFICATIONREFERENCE($,'[CO]PLE','Carpet',#1356,'ornamental object by a fabric covering part of another object',$); +#1362=IFCCLASSIFICATIONREFERENCE($,'[CO]PZ?','Multiple form presenting object',#1315,'presenting object in multiple forms',$); +#1363=IFCCLASSIFICATIONREFERENCE($,'[CO]PZA','Audio-visual alarm',#1362,'multiple form presenting object for discrete states',$); +#1364=IFCCLASSIFICATIONREFERENCE($,'[CO]PZB','Television set',#1362,'multiple form presenting object for multiple information',$); +#1365=IFCCLASSIFICATIONREFERENCE($,'[CO]Q??','Controlling object',#900,'object for controlling access or flow',$); +#1366=IFCCLASSIFICATIONREFERENCE($,'[CO]QA?','Electric controlling object',#1365,'controlling object of electric current in an electric circuit',$); +#1367=IFCCLASSIFICATIONREFERENCE($,'[CO]QAA','Contactor',#1366,'electric controlling object mechanically under normal service conditions only',$); +#1368=IFCCLASSIFICATIONREFERENCE($,'[CO]QAB','Circuit breaker',#1366,'electric controlling object mechanically under normal and abnormal service conditions',$); +#1369=IFCCLASSIFICATIONREFERENCE($,'[CO]QAC','Electronic power switch',#1366,'electric controlling object electronically',$); +#1370=IFCCLASSIFICATIONREFERENCE($,'[CO]QB?','Electric separating object',#1365,'controlling object by separating electric circuits',$); +#1371=IFCCLASSIFICATIONREFERENCE($,'[CO]QBA','Fuseless disconnector',#1370,'electric separating object by mechanically isolating the downstream circuit from the upstream circuit only',$); +#1372=IFCCLASSIFICATIONREFERENCE($,'[CO]QBB','Fused disconnector',#1370,'electric separating object by mechanically isolating the downstream circuit from the upstream circuit and with a built-in fuse',$); +#1373=IFCCLASSIFICATIONREFERENCE($,'[CO]QC?','Electric earthing object',#1365,'controlling object by earthing electric circuits',$); +#1374=IFCCLASSIFICATIONREFERENCE($,'[CO]QCA','Earthing switch',#1373,'electric earthing object connecting electric circuits to earth',$); +#1375=IFCCLASSIFICATIONREFERENCE($,'[CO]QM?','Sealed fluid switching object',#1365,'controlling object of fluids on/off in sealed enclosures',$); +#1376=IFCCLASSIFICATIONREFERENCE($,'[CO]QMA','Liquid shutoff valve',#1375,'sealed fluid switching object of a liquid flow',$); +#1377=IFCCLASSIFICATIONREFERENCE($,'[CO]QMB','Gas shutoff valve',#1375,'sealed fluid switching object of a gas flow',$); +#1378=IFCCLASSIFICATIONREFERENCE($,'[CO]QN?','Sealed fluid varying object',#1365,'controlling object for varying the flow of fluids in sealed enclosures',$); +#1379=IFCCLASSIFICATIONREFERENCE($,'[CO]QNA','Liquid control valve',#1378,'sealed fluid varying object of a liquid flow',$); +#1380=IFCCLASSIFICATIONREFERENCE($,'[CO]QNB','Gas control valve',#1378,'sealed fluid varying object of a gas flow',$); +#1381=IFCCLASSIFICATIONREFERENCE($,'[CO]QP?','Open flow controlling object',#1365,'controlling object of fluids in an open enclosures',$); +#1382=IFCCLASSIFICATIONREFERENCE($,'[CO]QPA','Lock gate',#1381,'open fluid controlling object for varying or switching on/off',$); +#1383=IFCCLASSIFICATIONREFERENCE($,'[CO]QPB','Vortex generator',#1381,'open fluid controlling object for directing a flow by a fixed means',$); +#1384=IFCCLASSIFICATIONREFERENCE($,'[CO]QPC','Rudder',#1381,'open fluid controlling object for directing a flow by a variable means',$); +#1385=IFCCLASSIFICATIONREFERENCE($,'[CO]QQ?','Space access object',#1365,'controlling object to a space',$); +#1386=IFCCLASSIFICATIONREFERENCE($,'[CO]QQA','Window',#1385,'space access object for light entry only',$); +#1387=IFCCLASSIFICATIONREFERENCE($,'[CO]QQB','Window unit',#1385,'space access object for entry of light and persons',$); +#1388=IFCCLASSIFICATIONREFERENCE($,'[CO]QQC','Door',#1385,'space access object for upright passage by persons',$); +#1389=IFCCLASSIFICATIONREFERENCE($,'[CO]QQD','Hatch',#1385,'space access object of restricted size for passage of persons and goods',$); +#1390=IFCCLASSIFICATIONREFERENCE($,'[CO]QQE','Large door',#1385,'space access object of extended size for transfer of objects',$); +#1391=IFCCLASSIFICATIONREFERENCE($,'[CO]QQF','Gate',#1385,'space access object partially',$); +#1392=IFCCLASSIFICATIONREFERENCE($,'[CO]QQG','Rail scotch block',#1385,'space access object by blocking entrance to a track',$); +#1393=IFCCLASSIFICATIONREFERENCE($,'[CO]QR?','Solid substance flow varying object',#1365,'controlling object of solid substance',$); +#1394=IFCCLASSIFICATIONREFERENCE($,'[CO]QRA','Rotary air lock valve',#1393,'solid substance flow varying object with rotating chambers',$); +#1395=IFCCLASSIFICATIONREFERENCE($,'[CO]QRB','Iris valve',#1393,'solid substance flow varying object with an iris opening',$); +#1396=IFCCLASSIFICATIONREFERENCE($,'[CO]QRC','Flap gate valve',#1393,'solid substance flow varying object with a hinged flap or disc',$); +#1397=IFCCLASSIFICATIONREFERENCE($,'[CO]QS?','Mechanical movement controlling object',#1365,'controlling object for mechanical movement',$); +#1398=IFCCLASSIFICATIONREFERENCE($,'[CO]QSA','Lock',#1397,'mechanical movement controlling object by blocking',$); +#1399=IFCCLASSIFICATIONREFERENCE($,'[CO]QSB','Track switch',#1397,'mechanical movement controlling object by switching between different tracks',$); +#1400=IFCCLASSIFICATIONREFERENCE($,'[CO]QZ?','Multiple measure controlling object',#1365,'controlling object in electrical circuits utilising different measures',$); +#1401=IFCCLASSIFICATIONREFERENCE($,'[CO]QZA','Earthing and disconnecting switch',#1400,'multiple measure controlling object for earthing or separating electrical circuit',$); +#1402=IFCCLASSIFICATIONREFERENCE($,'[CO]R??','Restricting object',#900,'object for restricting or stabilising',$); +#1403=IFCCLASSIFICATIONREFERENCE($,'[CO]RA?','Electricity restricting object',#1402,'restricting object by restricting a flow of electric energy',$); +#1404=IFCCLASSIFICATIONREFERENCE($,'[CO]RAA','Diode',#1403,'electricity restricting object by blocking current flow in one direction',$); +#1405=IFCCLASSIFICATIONREFERENCE($,'[CO]RAB','Reactor',#1403,'electricity restricting object by providing an inductance in a circuit',$); +#1406=IFCCLASSIFICATIONREFERENCE($,'[CO]RAC','Resistor',#1403,'electricity restricting object by providing a resistance in a circuit',$); +#1407=IFCCLASSIFICATIONREFERENCE($,'[CO]RB?','Electricity stabilising object',#1402,'restricting object by stabilising a flow of electric energy',$); +#1408=IFCCLASSIFICATIONREFERENCE($,'[CO]RBA','Uninterrupted power supply',#1407,'electricity stabilising object ensuring a continuous power supply',$); +#1409=IFCCLASSIFICATIONREFERENCE($,'[CO]RBB','Power filter',#1407,'electricity stabilising object suppressing a defined frequency range within the flow',$); +#1410=IFCCLASSIFICATIONREFERENCE($,'[CO]RBC','Phase compensator',#1407,'electricity stabilising object providing reactive power',$); +#1411=IFCCLASSIFICATIONREFERENCE($,'[CO]RBD','Power equaliser',#1407,'electricity stabilising object suppressing or amplifying multiple frequency ranges within the flow',$); +#1412=IFCCLASSIFICATIONREFERENCE($,'[CO]RF?','Signal stabilising object',#1402,'restricting object by stabilising a signal',$); +#1413=IFCCLASSIFICATIONREFERENCE($,'[CO]RFA','Signal equaliser',#1412,'signal stabilising object suppressing or amplifying multiple frequency ranges of the signal',$); +#1414=IFCCLASSIFICATIONREFERENCE($,'[CO]RFB','Signal filter',#1412,'signal stabilising object suppressing a defined frequency range of the signal',$); +#1415=IFCCLASSIFICATIONREFERENCE($,'[CO]RL?','Movement restricting object',#1402,'restricting object by restricting movements',$); +#1416=IFCCLASSIFICATIONREFERENCE($,'[CO]RLA','Security chain',#1415,'movement restricting object by limiting movement of an object',$); +#1417=IFCCLASSIFICATIONREFERENCE($,'[CO]RLB','Shock absorber',#1415,'movement restricting object by settling and damping movements of an object',$); +#1418=IFCCLASSIFICATIONREFERENCE($,'[CO]RLC','Brake',#1415,'movement restricting object by reducing the speed of or stopping another object',$); +#1419=IFCCLASSIFICATIONREFERENCE($,'[CO]RLD','Speed bump',#1415,'movement restricting object by preventing high speed of another object',$); +#1420=IFCCLASSIFICATIONREFERENCE($,'[CO]RM?','Return flow restricting object',#1402,'restricting object by restricting a return flow of matter',$); +#1421=IFCCLASSIFICATIONREFERENCE($,'[CO]RMA','Non-return valve',#1420,'return flow restricting object of liquids',$); +#1422=IFCCLASSIFICATIONREFERENCE($,'[CO]RMB','Check damper',#1420,'return flow restricting object of gas',$); +#1423=IFCCLASSIFICATIONREFERENCE($,'[CO]RMC','Flap',#1420,'return flow restricting object of solid substances',$); +#1424=IFCCLASSIFICATIONREFERENCE($,'[CO]RMD','Stench trap',#1420,'return flow restricting object of odour',$); +#1425=IFCCLASSIFICATIONREFERENCE($,'[CO]RN?','Flow restrictor',#1402,'restricting object by restricting a guided flow of matter',$); +#1426=IFCCLASSIFICATIONREFERENCE($,'[CO]RNA','Regulating valve',#1425,'flow restrictor of liquids by an adjustable means',$); +#1427=IFCCLASSIFICATIONREFERENCE($,'[CO]RNB','Regulating damper',#1425,'flow restrictor of gas by an adjustable means',$); +#1428=IFCCLASSIFICATIONREFERENCE($,'[CO]RNC','Solids regulating valve',#1425,'flow restrictor of solid substances by an adjustable means',$); +#1429=IFCCLASSIFICATIONREFERENCE($,'[CO]RND','Diffuser',#1425,'flow restrictor by a fixed means',$); +#1430=IFCCLASSIFICATIONREFERENCE($,'[CO]RQ?','Local climate stabilising object',#1402,'restricting object of the local environment',$); +#1431=IFCCLASSIFICATIONREFERENCE($,'[CO]RQA','Insulation',#1430,'local climate stabilising object by restricting a thermal or sound transmission',$); +#1432=IFCCLASSIFICATIONREFERENCE($,'[CO]RQB','Membrane',#1430,'local climate stabilising object by stabilising a flow of liquids, moisture, wind or material',$); +#1433=IFCCLASSIFICATIONREFERENCE($,'[CO]RQC','Noise barrier',#1430,'local climate stabilising object by stabilising the propagation of sound',$); +#1434=IFCCLASSIFICATIONREFERENCE($,'[CO]RQD','Screen',#1430,'local climate stabilising object by restricting wind, precipitation or radiation',$); +#1435=IFCCLASSIFICATIONREFERENCE($,'[CO]RQE','Curtain',#1430,'local climate stabilising object by restricting transmission of light using textiles',$); +#1436=IFCCLASSIFICATIONREFERENCE($,'[CO]RQF','Louvered blind',#1430,'local climate stabilising object by restricting transmission of light using narrow adjustable louvers',$); +#1437=IFCCLASSIFICATIONREFERENCE($,'[CO]RQG','Shutter',#1430,'local climate stabilising object by restricting transmission of light using an external panel',$); +#1438=IFCCLASSIFICATIONREFERENCE($,'[CO]RQH','Weed control fabric',#1430,'local climate stabilising object preventing weed growth',$); +#1439=IFCCLASSIFICATIONREFERENCE($,'[CO]RQJ','Screen wall',#1430,'local climate stabilising object by restricting vision and sound',$); +#1440=IFCCLASSIFICATIONREFERENCE($,'[CO]RU?','Access restricting object',#1402,'restricting object by restricting access to an area',$); +#1441=IFCCLASSIFICATIONREFERENCE($,'[CO]RUA','Fence',#1440,'access restricting object by a horizontal elongated barrier with a vertical extent',$); +#1442=IFCCLASSIFICATIONREFERENCE($,'[CO]RUB','Movable barrier',#1440,'access restriction object by a movable horizontal barrier',$); +#1443=IFCCLASSIFICATIONREFERENCE($,'[CO]RUC','Turnstile',#1440,'access restricting object by a rotational barrier',$); +#1444=IFCCLASSIFICATIONREFERENCE($,'[CO]RUD','Bollard',#1440,'access restricting object by a barrier with limited horizontal and vertical extent',$); +#1445=IFCCLASSIFICATIONREFERENCE($,'[CO]RUE','Rodent barrier',#1440,'access restricting object by shaping a path',$); +#1446=IFCCLASSIFICATIONREFERENCE($,'[CO]RUF','Bird mesh',#1440,'access restricting object by a mesh',$); +#1447=IFCCLASSIFICATIONREFERENCE($,'[CO]S??','Human interaction object',#900,'object for detecting a human action and providing an appropriate response',$); +#1448=IFCCLASSIFICATIONREFERENCE($,'[CO]SF?','Face interaction device',#1447,'human interaction object by facial interaction',$); +#1449=IFCCLASSIFICATIONREFERENCE($,'[CO]SFA','Eye focus reader',#1448,'face interaction device by sensing of facial activity',$); +#1450=IFCCLASSIFICATIONREFERENCE($,'[CO]SFB','Face recognition device',#1448,'face interaction device by face recognition',$); +#1451=IFCCLASSIFICATIONREFERENCE($,'[CO]SG?','Hand interaction device',#1447,'human interaction object to a hand action',$); +#1452=IFCCLASSIFICATIONREFERENCE($,'[CO]SGA','Turn handle',#1451,'hand interaction device by turning',$); +#1453=IFCCLASSIFICATIONREFERENCE($,'[CO]SGB','Grip handle',#1451,'hand interaction device by gripping',$); +#1454=IFCCLASSIFICATIONREFERENCE($,'[CO]SGC','Push handle',#1451,'hand interaction device by pushing',$); +#1455=IFCCLASSIFICATIONREFERENCE($,'[CO]SGD','Lever handle',#1451,'hand interaction device by lever',$); +#1456=IFCCLASSIFICATIONREFERENCE($,'[CO]SGE','Pull handle',#1451,'hand interaction device by pulling',$); +#1457=IFCCLASSIFICATIONREFERENCE($,'[CO]SGF','Key',#1451,'hand interaction device to turn a matching locking cylinder or locking mechanism',$); +#1458=IFCCLASSIFICATIONREFERENCE($,'[CO]SH?','Foot interaction device',#1447,'human interaction object to a foot position',$); +#1459=IFCCLASSIFICATIONREFERENCE($,'[CO]SHA','Pedal switch',#1458,'foot interaction device by pressing by the foot',$); +#1460=IFCCLASSIFICATIONREFERENCE($,'[CO]SJ?','Finger interaction device',#1447,'human interaction object by finger operation',$); +#1461=IFCCLASSIFICATIONREFERENCE($,'[CO]SJA','Flip switch',#1460,'finger interaction device by lever operation',$); +#1462=IFCCLASSIFICATIONREFERENCE($,'[CO]SJB','Pushbutton',#1460,'finger interaction device by pressing',$); +#1463=IFCCLASSIFICATIONREFERENCE($,'[CO]SJC','Touch actuator',#1460,'finger interaction device by a touching',$); +#1464=IFCCLASSIFICATIONREFERENCE($,'[CO]SJD','Turning wheel',#1460,'finger interaction device by advancing/reversing a wheel',$); +#1465=IFCCLASSIFICATIONREFERENCE($,'[CO]SK?','Movement interaction device',#1447,'manual interaction object by its movement or positioning',$); +#1466=IFCCLASSIFICATIONREFERENCE($,'[CO]SKA','Joystick',#1465,'movement interaction device by restricted positioning and selecting operations',$); +#1467=IFCCLASSIFICATIONREFERENCE($,'[CO]SKB','Mouse',#1465,'movement interaction device by unrestricted positioning and selecting operations',$); +#1468=IFCCLASSIFICATIONREFERENCE($,'[CO]SZ?','Multi-interaction device',#1447,'human interaction object with multiple kinds of interacting means',$); +#1469=IFCCLASSIFICATIONREFERENCE($,'[CO]SZA','Operating panel',#1468,'multi-interaction device involving manual means',$); +#1470=IFCCLASSIFICATIONREFERENCE($,'[CO]T??','Transforming object',#900,'object for transforming',$); +#1471=IFCCLASSIFICATIONREFERENCE($,'[CO]TA?','Electric energy transforming object',#1470,'transforming object of electric energy while retaining AC or DC',$); +#1472=IFCCLASSIFICATIONREFERENCE($,'[CO]TAA','Transformer',#1471,'electric energy transforming object from AC to AC without change of the frequency',$); +#1473=IFCCLASSIFICATIONREFERENCE($,'[CO]TAB','DC/DC converter',#1471,'electric energy transforming object from DC to DC',$); +#1474=IFCCLASSIFICATIONREFERENCE($,'[CO]TAC','Frequency converter',#1471,'electric energy transforming object from AC to AC while changing the frequency',$); +#1475=IFCCLASSIFICATIONREFERENCE($,'[CO]TAD','Phase shifter',#1471,'electric energy transforming object from AC to AC while shifting the angle between voltages and currents',$); +#1476=IFCCLASSIFICATIONREFERENCE($,'[CO]TB?','Electric energy converting object',#1470,'transforming object of electric energy changing between AC and DC',$); +#1477=IFCCLASSIFICATIONREFERENCE($,'[CO]TBA','Rectifier',#1476,'electric energy converting object converting from AC to DC',$); +#1478=IFCCLASSIFICATIONREFERENCE($,'[CO]TBB','Inverter',#1476,'electric energy converting object converting from DC to AC',$); +#1479=IFCCLASSIFICATIONREFERENCE($,'[CO]TBC','Bidirectional converter',#1476,'electric energy converting object converting from AC to DC or DC to AC',$); +#1480=IFCCLASSIFICATIONREFERENCE($,'[CO]TC?','Universal power supply',#1470,'transforming object of electric energy from AC and DC to AC or DC',$); +#1481=IFCCLASSIFICATIONREFERENCE($,'[CO]TCA','Universal AC power supply',#1480,'universal power supply providing AC',$); +#1482=IFCCLASSIFICATIONREFERENCE($,'[CO]TCB','Universal DC power supply',#1480,'universal power supply providing DC',$); +#1483=IFCCLASSIFICATIONREFERENCE($,'[CO]TF?','Signal converting object',#1470,'transforming object of signal',$); +#1484=IFCCLASSIFICATIONREFERENCE($,'[CO]TFA','Amplifier',#1483,'signal converting object maintaining the signal form',$); +#1485=IFCCLASSIFICATIONREFERENCE($,'[CO]TFB','Signal antenna',#1483,'signal converting object between a wired and wireless form',$); +#1486=IFCCLASSIFICATIONREFERENCE($,'[CO]TFC','Signal converter',#1483,'signal converting object between wired forms',$); +#1487=IFCCLASSIFICATIONREFERENCE($,'[CO]TFD','Optical receiver/transmitter',#1483,'signal converting object between electrical and optical form',$); +#1488=IFCCLASSIFICATIONREFERENCE($,'[CO]TFE','Telephone',#1483,'signal converting object between acoustic waves and electrical signals',$); +#1489=IFCCLASSIFICATIONREFERENCE($,'[CO]TL?','Mechanical energy transforming object',#1470,'transforming object of mechanical energy',$); +#1490=IFCCLASSIFICATIONREFERENCE($,'[CO]TLA','Gear',#1489,'mechanical energy transforming object modifying velocity',$); +#1491=IFCCLASSIFICATIONREFERENCE($,'[CO]TLB','Torque converter',#1489,'mechanical energy transforming object modifying torque',$); +#1492=IFCCLASSIFICATIONREFERENCE($,'[CO]TLC','Lever',#1489,'mechanical energy transforming object modifying force',$); +#1493=IFCCLASSIFICATIONREFERENCE($,'[CO]TM?','Mass reduction object',#1470,'transforming object of matter by mass reduction',$); +#1494=IFCCLASSIFICATIONREFERENCE($,'[CO]TMA','Drilling machine',#1493,'mass reduction object by a mechanical process',$); +#1495=IFCCLASSIFICATIONREFERENCE($,'[CO]TMB','Thermal cutter',#1493,'mass reduction object by a thermal process',$); +#1496=IFCCLASSIFICATIONREFERENCE($,'[CO]TMC','Etching machine',#1493,'mass reduction object by a chemical process',$); +#1497=IFCCLASSIFICATIONREFERENCE($,'[CO]TP?','Matter reshaping object',#1470,'transforming object of matter by shape reforming',$); +#1498=IFCCLASSIFICATIONREFERENCE($,'[CO]TPA','Forging machine',#1497,'matter reshaping object by forging',$); +#1499=IFCCLASSIFICATIONREFERENCE($,'[CO]TPB','Extruder',#1497,'matter reshaping object by extruding',$); +#1500=IFCCLASSIFICATIONREFERENCE($,'[CO]TPC','Wire drawing machine',#1497,'matter reshaping object by drawing',$); +#1501=IFCCLASSIFICATIONREFERENCE($,'[CO]TPD','Rolling machine',#1497,'matter reshaping object by rolling or spinning',$); +#1502=IFCCLASSIFICATIONREFERENCE($,'[CO]TPE','Bending machine',#1497,'matter reshaping object by bending',$); +#1503=IFCCLASSIFICATIONREFERENCE($,'[CO]TPF','Electromagnetic forming machine',#1497,'matter reshaping object by high energy rate forming',$); +#1504=IFCCLASSIFICATIONREFERENCE($,'[CO]TR?','Organic plant',#1470,'transforming object of radiation by photosynthesis',$); +#1505=IFCCLASSIFICATIONREFERENCE($,'[CO]TRA','Tree',#1504,'organic plant with a single woody stem',$); +#1506=IFCCLASSIFICATIONREFERENCE($,'[CO]TRB','Shrub',#1504,'organic plant with several woody stems',$); +#1507=IFCCLASSIFICATIONREFERENCE($,'[CO]TRC','Climber',#1504,'organic plant with long shoots with climbing organs',$); +#1508=IFCCLASSIFICATIONREFERENCE($,'[CO]TRD','Herb',#1504,'organic plant that is not woody or is insignificantly woody',$); +#1509=IFCCLASSIFICATIONREFERENCE($,'[CO]U??','Holding object',#900,'object for localising other objects',$); +#1510=IFCCLASSIFICATIONREFERENCE($,'[CO]UA?','Positioning object',#1509,'holding object by fixing at a defined position',$); +#1511=IFCCLASSIFICATIONREFERENCE($,'[CO]UAA','Insulator',#1510,'positioning object for conductor',$); +#1512=IFCCLASSIFICATIONREFERENCE($,'[CO]UAB','Cable gland',#1510,'positioning object for cable',$); +#1513=IFCCLASSIFICATIONREFERENCE($,'[CO]UAC','Light fixture',#1510,'positioning object for light source',$); +#1514=IFCCLASSIFICATIONREFERENCE($,'[CO]UAD','Stair stringer',#1510,'positioning object for tread',$); +#1515=IFCCLASSIFICATIONREFERENCE($,'[CO]UAE','Batten',#1510,'positioning object for cover',$); +#1516=IFCCLASSIFICATIONREFERENCE($,'[CO]UAF','Stake',#1510,'positioning object for plant',$); +#1517=IFCCLASSIFICATIONREFERENCE($,'[CO]UAG','Rail gauge',#1510,'positioning object for rail spacing',$); +#1518=IFCCLASSIFICATIONREFERENCE($,'[CO]UAH ','Clamp',#1517,'positioning object for tool or work piece',$); +#1519=IFCCLASSIFICATIONREFERENCE($,'[CO]UAJ','Vehicle stand',#1510,'positioning object for vehicle',$); +#1520=IFCCLASSIFICATIONREFERENCE($,'[CO]UB?','Carrying object',#1509,'holding object by carrying',$); +#1521=IFCCLASSIFICATIONREFERENCE($,'[CO]UBA','Cable tray',#1520,'carrying object between discrete positions',$); +#1522=IFCCLASSIFICATIONREFERENCE($,'[CO]UBB','Hanger',#1520,'carrying object at a discrete position by vertical pulling',$); +#1523=IFCCLASSIFICATIONREFERENCE($,'[CO]UBC','Tread',#1520,'carrying object at a discrete position by steps',$); +#1524=IFCCLASSIFICATIONREFERENCE($,'[CO]UBD','Mast',#1520,'carrying object at a discrete position by vertical pushing',$); +#1525=IFCCLASSIFICATIONREFERENCE($,'[CO]UBE','Table',#1520,'carrying object at a discrete plane by pushing',$); +#1526=IFCCLASSIFICATIONREFERENCE($,'[CO]UBF','Portal',#1520,'carrying object between discrete positions at a defined level',$); +#1527=IFCCLASSIFICATIONREFERENCE($,'[CO]UC?','Enclosing object',#1509,'holding object by enclosing',$); +#1528=IFCCLASSIFICATIONREFERENCE($,'[CO]UCA','Cubicle',#1527,'enclosing object for devices',$); +#1529=IFCCLASSIFICATIONREFERENCE($,'[CO]UCB','Plant soil',#1527,'enclosing object for plant roots',$); +#1530=IFCCLASSIFICATIONREFERENCE($,'[CO]UCC','Cooker hood',#1527,'enclosing object for spot ventilation',$); +#1531=IFCCLASSIFICATIONREFERENCE($,'[CO]UL?','Structural supporting object',#1509,'holding object by forming a structural support',$); +#1532=IFCCLASSIFICATIONREFERENCE($,'[CO]ULA','Base course',#1531,'structural supporting object in form of an aggregate material layer',$); +#1533=IFCCLASSIFICATIONREFERENCE($,'[CO]ULB','Console',#1531,'structural supporting object in form of a projection supporting an overlying structure',$); +#1534=IFCCLASSIFICATIONREFERENCE($,'[CO]ULC','Pile',#1531,'structural supporting object in a curved or straight linear form transferring compressing forces to surrounding media',$); +#1535=IFCCLASSIFICATIONREFERENCE($,'[CO]ULD','Column',#1531,'structural supporting object in a curved or straight linear form transferring compressing forces to other structural objects',$); +#1536=IFCCLASSIFICATIONREFERENCE($,'[CO]ULE','Beam',#1531,'structural supporting object in a straight linear form withstanding bending moments',$); +#1537=IFCCLASSIFICATIONREFERENCE($,'[CO]ULF','Tension brace',#1531,'structural supporting object in a straight linear form withstanding tensile forces',$); +#1538=IFCCLASSIFICATIONREFERENCE($,'[CO]ULG','Block',#1531,'structural supporting object in form of a block',$); +#1539=IFCCLASSIFICATIONREFERENCE($,'[CO]ULH','Arch',#1531,'structural supporting object in a curved linear form withstanding bending moments',$); +#1540=IFCCLASSIFICATIONREFERENCE($,'[CO]ULJ','Abutment',#1531,'structural supporting object in a plane or curved surface form withstanding compression forces and bending moment',$); +#1541=IFCCLASSIFICATIONREFERENCE($,'[CO]ULK','Slab plate',#1531,'structural supporting object in laying a plane surface form withstanding bending moments',$); +#1542=IFCCLASSIFICATIONREFERENCE($,'[CO]ULL','Retaining wall plate',#1531,'structural supporting object in a standing plane or curved surface form withstanding bending moments',$); +#1543=IFCCLASSIFICATIONREFERENCE($,'[CO]ULM','Wall plate',#1531,'structural supporting object in a plane or curved surface form withstanding compression forces',$); +#1544=IFCCLASSIFICATIONREFERENCE($,'[CO]ULN','Shell',#1531,'structural supporting object a plane or curved surface form withstanding tensile forces',$); +#1545=IFCCLASSIFICATIONREFERENCE($,'[CO]ULP','Continuous arch',#1531,'structural supporting object in a curved surface form withstanding bending moments',$); +#1546=IFCCLASSIFICATIONREFERENCE($,'[CO]ULQ','Bearing plate',#1531,'structural supporting object a flat surface form distributing loads in joints',$); +#1547=IFCCLASSIFICATIONREFERENCE($,'[CO]ULR','Catenary',#1531,'structural supporting object in a curved linear form withstanding tensile forces',$); +#1548=IFCCLASSIFICATIONREFERENCE($,'[CO]UM?','Reinforcing object',#1509,'holding object by reinforcement',$); +#1549=IFCCLASSIFICATIONREFERENCE($,'[CO]UMA','Reinforcing rod',#1548,'reinforcing object as an embedded rod',$); +#1550=IFCCLASSIFICATIONREFERENCE($,'[CO]UMB','Reinforcing mesh',#1548,'reinforcing object as an embedded mesh or fabric',$); +#1551=IFCCLASSIFICATIONREFERENCE($,'[CO]UMC','Reinforcing mass layer',#1548,'reinforcing object as a mass layer',$); +#1552=IFCCLASSIFICATIONREFERENCE($,'[CO]UMD','Reinforcing matter',#1548,'reinforcing object as an embedded matter',$); +#1553=IFCCLASSIFICATIONREFERENCE($,'[CO]UME','Security strike plate',#1548,'reinforcing object as a lock latch',$); +#1554=IFCCLASSIFICATIONREFERENCE($,'[CO]UMF','Splice',#1548,'reinforcing object as an intermediate filling',$); +#1555=IFCCLASSIFICATIONREFERENCE($,'[CO]UMG','Soil vertical drain',#1548,'reinforcing object by vertical draining',$); +#1556=IFCCLASSIFICATIONREFERENCE($,'[CO]UMH','Concrete spurting',#1548,'reinforcing object by concrete spurting',$); +#1557=IFCCLASSIFICATIONREFERENCE($,'[CO]UMJ','Concrete lining',#1548,'reinforcing object by concrete lining',$); +#1558=IFCCLASSIFICATIONREFERENCE($,'[CO]UMK','Stiffener',#1548,'reinforcing object by stiffener',$); +#1559=IFCCLASSIFICATIONREFERENCE($,'[CO]UML','Diagonal bonding',#1548,'reinforcing object by diagonal bonding',$); +#1560=IFCCLASSIFICATIONREFERENCE($,'[CO]UMM','Stabilizing cable',#1548,'reinforcing object as cables',$); +#1561=IFCCLASSIFICATIONREFERENCE($,'[CO]UMN','Horizontal rail',#1548,'reinforcing object as a horizontal rail',$); +#1562=IFCCLASSIFICATIONREFERENCE($,'[CO]UMP','Earth pressure embankment',#1548,'reinforcing object which by its counterweight stabilizes soil',$); +#1563=IFCCLASSIFICATIONREFERENCE($,'[CO]UN?','Framing object',#1509,'holding object by framing other objects',$); +#1564=IFCCLASSIFICATIONREFERENCE($,'[CO]UNA','Fixed frame',#1563,'framing object with a fixed frame',$); +#1565=IFCCLASSIFICATIONREFERENCE($,'[CO]UNB','Threshold',#1563,'framing object with the lowest part of gate abutting',$); +#1566=IFCCLASSIFICATIONREFERENCE($,'[CO]UNC','Movable frame',#1563,'framing object with a moveable frame',$); +#1567=IFCCLASSIFICATIONREFERENCE($,'[CO]UND','Glazing bar frame',#1563,'framing object dividing a sash or door',$); +#1568=IFCCLASSIFICATIONREFERENCE($,'[CO]UP?','Jointing object',#1509,'holding object by non-static jointing',$); +#1569=IFCCLASSIFICATIONREFERENCE($,'[CO]UPA','Bearing',#1568,'jointing object by bearing',$); +#1570=IFCCLASSIFICATIONREFERENCE($,'[CO]UPB','Hinge',#1568,'jointing object by mechanical joint',$); +#1571=IFCCLASSIFICATIONREFERENCE($,'[CO]UPC','Chemical joint',#1568,'jointing object by chemical bonding',$); +#1572=IFCCLASSIFICATIONREFERENCE($,'[CO]UQ?','Fastening object',#1509,'holding object by static jointing',$); +#1573=IFCCLASSIFICATIONREFERENCE($,'[CO]UQA','Anchor plate',#1572,'fastening object by rigid and permanent mechanical means',$); +#1574=IFCCLASSIFICATIONREFERENCE($,'[CO]UQB','Bolt',#1572,'fastening object by rigid and reversible mechanically means',$); +#1575=IFCCLASSIFICATIONREFERENCE($,'[CO]UQC','Chemical bond',#1572,'fastening object by chemical bonding',$); +#1576=IFCCLASSIFICATIONREFERENCE($,'[CO]UT?','Levelling object',#1509,'holding object for levelling',$); +#1577=IFCCLASSIFICATIONREFERENCE($,'[CO]UTA','Filling',#1576,'levelling object providing a defined profiled surface of plane by adding material',$); +#1578=IFCCLASSIFICATIONREFERENCE($,'[CO]UTB','Excavation',#1576,'levelling object providing a defined profiled surface of plane by removing material',$); +#1579=IFCCLASSIFICATIONREFERENCE($,'[CO]UU?','Existing ground',#1509,'holding object being existing ground',$); +#1580=IFCCLASSIFICATIONREFERENCE($,'[CO]UUA','Rock',#1579,'existing ground of rock',$); +#1581=IFCCLASSIFICATIONREFERENCE($,'[CO]UUB','Friction soil',#1579,'existing ground of friction soil',$); +#1582=IFCCLASSIFICATIONREFERENCE($,'[CO]UUC','Cohesive soil',#1579,'existing ground of cohesive soil',$); +#1583=IFCCLASSIFICATIONREFERENCE($,'[CO]UUD','Organic soil',#1579,'existing ground of organic soil',$); +#1584=IFCCLASSIFICATIONREFERENCE($,'[CO]UUE','Filled material',#1579,'existing ground of filled material',$); +#1585=IFCCLASSIFICATIONREFERENCE($,'[CO]W??','Guiding object',#900,'object for leading from one place to another',$); +#1586=IFCCLASSIFICATIONREFERENCE($,'[CO]WB?','High voltage electric energy guiding object',#1585,'guiding object of high voltage electric energy',$); +#1587=IFCCLASSIFICATIONREFERENCE($,'[CO]WBA','High voltage busbar',#1586,'high voltage electric energy guiding object by a busbar',$); +#1588=IFCCLASSIFICATIONREFERENCE($,'[CO]WBB','High voltage cable',#1586,'high voltage electric energy guiding object by a cable',$); +#1589=IFCCLASSIFICATIONREFERENCE($,'[CO]WBC','High voltage wire',#1586,'high voltage electric energy guiding object by a wire',$); +#1590=IFCCLASSIFICATIONREFERENCE($,'[CO]WBD','High voltage bushing',#1586,'high voltage electric energy guiding object through an enclosure, wall or barrier',$); +#1591=IFCCLASSIFICATIONREFERENCE($,'[CO]WD?','Low voltage electric energy guiding object',#1585,'guiding object of low voltage electric energy',$); +#1592=IFCCLASSIFICATIONREFERENCE($,'[CO]WDA','Low voltage busbar',#1591,'low voltage electric energy guiding object by a busbar',$); +#1593=IFCCLASSIFICATIONREFERENCE($,'[CO]WDB','Low voltage cable',#1591,'low voltage electric energy guiding object by a cable',$); +#1594=IFCCLASSIFICATIONREFERENCE($,'[CO]WDC','Low voltage wire',#1591,'low voltage electric energy guiding object by a wire',$); +#1595=IFCCLASSIFICATIONREFERENCE($,'[CO]WDD','Low voltage bushing',#1591,'low voltage electric energy guiding object through an enclosure, wall or barrier',$); +#1596=IFCCLASSIFICATIONREFERENCE($,'[CO]WE?','Reference potential guiding object',#1585,'guiding object of a reference potential',$); +#1597=IFCCLASSIFICATIONREFERENCE($,'[CO]WEA','Earthing rail',#1596,'reference potential guiding object of earth by a busbar',$); +#1598=IFCCLASSIFICATIONREFERENCE($,'[CO]WEB','Earthing cable',#1596,'reference potential guiding object of earth by a cable',$); +#1599=IFCCLASSIFICATIONREFERENCE($,'[CO]WEC','Equipotential bonding rail',#1596,'reference potential guiding object by a busbar',$); +#1600=IFCCLASSIFICATIONREFERENCE($,'[CO]WED','Equipotential bonding cable',#1596,'reference potential guiding object by a cable',$); +#1601=IFCCLASSIFICATIONREFERENCE($,'[CO]WG?','Electric signal guiding object',#1585,'guiding object for electric signals',$); +#1602=IFCCLASSIFICATIONREFERENCE($,'[CO]WGA','Control cable',#1601,'electric signal guiding object for controlling and measuring',$); +#1603=IFCCLASSIFICATIONREFERENCE($,'[CO]WGB','Data cable',#1601,'electric signal guiding object for data transmission',$); +#1604=IFCCLASSIFICATIONREFERENCE($,'[CO]WGC','Electric signal bushing',#1601,'electric signal guiding object through an enclosure or wall',$); +#1605=IFCCLASSIFICATIONREFERENCE($,'[CO]WH?','Light guiding object',#1585,'guiding object for light',$); +#1606=IFCCLASSIFICATIONREFERENCE($,'[CO]WHA','Fibre optical cable',#1605,'light guiding object for signal transmission',$); +#1607=IFCCLASSIFICATIONREFERENCE($,'[CO]WHB','Optical light fibre',#1605,'light guiding object for light transmission',$); +#1608=IFCCLASSIFICATIONREFERENCE($,'[CO]WHC','Lens',#1605,'light guiding object for defined light refraction',$); +#1609=IFCCLASSIFICATIONREFERENCE($,'[CO]WHD','Mirror',#1605,'light guiding object for defined light reflection',$); +#1610=IFCCLASSIFICATIONREFERENCE($,'[CO]WJ?','Sound guiding object',#1585,'guiding object for sound',$); +#1611=IFCCLASSIFICATIONREFERENCE($,'[CO]WJA','Sound reflector',#1610,'sound guiding object by reflection',$); +#1612=IFCCLASSIFICATIONREFERENCE($,'[CO]WL?','Solid matter guiding object',#1585,'guiding object of solid matter in open enclosure',$); +#1613=IFCCLASSIFICATIONREFERENCE($,'[CO]WLA','Roller table',#1612,'solid matter guiding object by rolling',$); +#1614=IFCCLASSIFICATIONREFERENCE($,'[CO]WLB','Chute',#1612,'solid matter guiding object by sliding',$); +#1615=IFCCLASSIFICATIONREFERENCE($,'[CO]WM?','Open enclosure guiding object',#1585,'guiding object of fluid flow in open enclosure',$); +#1616=IFCCLASSIFICATIONREFERENCE($,'[CO]WMA','Drainage layer',#1615,'open enclosure guiding object in a layer',$); +#1617=IFCCLASSIFICATIONREFERENCE($,'[CO]WMB','Gutter',#1615,'open enclosure guiding object for liquid in open conduit',$); +#1618=IFCCLASSIFICATIONREFERENCE($,'[CO]WMC','Drain liquid flashing',#1615,'open enclosure guiding object for liquid with an open surface',$); +#1619=IFCCLASSIFICATIONREFERENCE($,'[CO]WMD','Drip nose',#1615,'open enclosure guiding object for liquid by projection',$); +#1620=IFCCLASSIFICATIONREFERENCE($,'[CO]WME','Gutter drip',#1615,'open enclosure guiding object for liquid by liquid-draining profile',$); +#1621=IFCCLASSIFICATIONREFERENCE($,'[CO]WMF','Drainpipe',#1615,'open enclosure guiding object by perforated pipe',$); +#1622=IFCCLASSIFICATIONREFERENCE($,'[CO]WMG','Infiltration chamber',#1615,'open enclosure guiding object in a chamber',$); +#1623=IFCCLASSIFICATIONREFERENCE($,'[CO]WP?','Closed enclosure guiding object',#1585,'guiding object of matter flow in closed enclosure',$); +#1624=IFCCLASSIFICATIONREFERENCE($,'[CO]WPA','Pipe',#1623,'closed enclosure guiding object in a circular rigid form',$); +#1625=IFCCLASSIFICATIONREFERENCE($,'[CO]WPB','Duct',#1623,'closed enclosure guiding object in a non-circular rigid form',$); +#1626=IFCCLASSIFICATIONREFERENCE($,'[CO]WPC','Hose',#1623,'closed enclosure guiding object in a flexible form',$); +#1627=IFCCLASSIFICATIONREFERENCE($,'[CO]WQ?','Mechanical energy guiding object',#1585,'guiding object of mechanical energy',$); +#1628=IFCCLASSIFICATIONREFERENCE($,'[CO]WQA','Drive axle',#1627,'mechanical energy guiding object by a shaft',$); +#1629=IFCCLASSIFICATIONREFERENCE($,'[CO]WQB','Drive belt',#1627,'mechanical energy guiding object by a belt',$); +#1630=IFCCLASSIFICATIONREFERENCE($,'[CO]WQC','Drive chain',#1627,'mechanical energy guiding object by a chain',$); +#1631=IFCCLASSIFICATIONREFERENCE($,'[CO]WQD','Drive link',#1627,'mechanical energy guiding object by a linkage',$); +#1632=IFCCLASSIFICATIONREFERENCE($,'[CO]WQE','Wheel',#1627,'mechanical energy guiding object by a wheel',$); +#1633=IFCCLASSIFICATIONREFERENCE($,'[CO]WQF','Toothed bar',#1627,'mechanical energy guiding object by a toothed bar',$); +#1634=IFCCLASSIFICATIONREFERENCE($,'[CO]WQG','Hydraulic hose',#1627,'mechanical energy guiding object by a fluid link',$); +#1635=IFCCLASSIFICATIONREFERENCE($,'[CO]WR?','Rail object',#1585,'guiding object for track bound objects',$); +#1636=IFCCLASSIFICATIONREFERENCE($,'[CO]WRA','Track',#1635,'rail object providing a directed path',$); +#1637=IFCCLASSIFICATIONREFERENCE($,'[CO]WRB','Track crossing',#1635,'rail object providing a crossing',$); +#1638=IFCCLASSIFICATIONREFERENCE($,'[CO]WV?','Thermal energy guiding object',#1585,'guiding object for thermal energy',$); +#1639=IFCCLASSIFICATIONREFERENCE($,'[CO]WVA','Thermal conductor',#1638,'thermal energy guiding object by a solid mass',$); +#1640=IFCCLASSIFICATIONREFERENCE($,'[CO]WVB','Thermal paste',#1638,'thermal energy guiding object by a static fluid mass',$); +#1641=IFCCLASSIFICATIONREFERENCE($,'[CO]WZ?','Multiple flow guiding object',#1585,'guiding object with multiple kinds of flows',$); +#1642=IFCCLASSIFICATIONREFERENCE($,'[CO]WZA','Umbilical cable',#1641,'multiple flow guiding object of electrical energy, mechanical energy, electrical signal, optical signals and/or fluid',$); +#1643=IFCCLASSIFICATIONREFERENCE($,'[CO]X??','Interfacing object',#900,'object for interfacing an object',$); +#1644=IFCCLASSIFICATIONREFERENCE($,'[CO]XB?','High voltage connecting object',#1643,'interfacing object for high voltage electric power',$); +#1645=IFCCLASSIFICATIONREFERENCE($,'[CO]XBA','High voltage terminal',#1644,'high voltage connecting object for a single connection',$); +#1646=IFCCLASSIFICATIONREFERENCE($,'[CO]XBB','High voltage socket',#1644,'high voltage connecting object for pluggable connections',$); +#1647=IFCCLASSIFICATIONREFERENCE($,'[CO]XBC','High voltage terminal box',#1644,'high voltage connecting object for multiple fixed connections',$); +#1648=IFCCLASSIFICATIONREFERENCE($,'[CO]XD?','Low voltage connecting object',#1643,'interfacing object for low voltage electric power',$); +#1649=IFCCLASSIFICATIONREFERENCE($,'[CO]XDA','Electric terminal',#1648,'low voltage connecting object for a single connection',$); +#1650=IFCCLASSIFICATIONREFERENCE($,'[CO]XDB','Low voltage power socket',#1648,'low voltage connecting object for pluggable connections',$); +#1651=IFCCLASSIFICATIONREFERENCE($,'[CO]XDC','Terminal box',#1648,'low voltage connecting object for multiple fixed connections',$); +#1652=IFCCLASSIFICATIONREFERENCE($,'[CO]XDD','Power outlet',#1648,'low voltage connecting object for fixed connection of a flexible cable for a current-using equipment',$); +#1653=IFCCLASSIFICATIONREFERENCE($,'[CO]XDE','Power distributer',#1648,'low voltage connecting object of multiple pluggable current-using equipment',$); +#1654=IFCCLASSIFICATIONREFERENCE($,'[CO]XE?','Potential connecting object',#1643,'interfacing object for earth or reference potential',$); +#1655=IFCCLASSIFICATIONREFERENCE($,'[CO]XEA','PE terminal',#1654,'potential connecting object for connecting to PE-conductors',$); +#1656=IFCCLASSIFICATIONREFERENCE($,'[CO]XEB','PB terminal',#1654,'potential connecting object for connecting to PB conductors',$); +#1657=IFCCLASSIFICATIONREFERENCE($,'[CO]XEC','FE terminal',#1654,'potential connecting object for connecting to FE-conductors',$); +#1658=IFCCLASSIFICATIONREFERENCE($,'[CO]XED','FB terminal',#1654,'potential connecting object for connecting to FB conductors',$); +#1659=IFCCLASSIFICATIONREFERENCE($,'[CO]XEE','Earth electrode',#1654,'potential connecting object providing electrical contact with earth',$); +#1660=IFCCLASSIFICATIONREFERENCE($,'[CO]XEF','Collector',#1654,'potential connecting object connecting lightning to an electrical circuit',$); +#1661=IFCCLASSIFICATIONREFERENCE($,'[CO]XG?','Electric signal connecting object',#1643,'interfacing object for electric signals',$); +#1662=IFCCLASSIFICATIONREFERENCE($,'[CO]XGA','Signal socket',#1661,'electric signal connecting object for single pluggable interconnections',$); +#1663=IFCCLASSIFICATIONREFERENCE($,'[CO]XGB','Terminal',#1661,'electric signal connecting object for a single interconnection',$); +#1664=IFCCLASSIFICATIONREFERENCE($,'[CO]XGC','Patch panel',#1661,'electric signal connecting object among several lines',$); +#1665=IFCCLASSIFICATIONREFERENCE($,'[CO]XGD','Signal distributor',#1661,'electric signal connecting object for multiple pluggable interconnections',$); +#1666=IFCCLASSIFICATIONREFERENCE($,'[CO]XH?','Light connecting object',#1643,'interfacing object of optical fibres',$); +#1667=IFCCLASSIFICATIONREFERENCE($,'[CO]XHA','Fixed light fibre socket',#1666,'light connecting object for pluggable connections',$); +#1668=IFCCLASSIFICATIONREFERENCE($,'[CO]XK?','Collecting interfacing object',#1643,'interfacing object collecting for feeding into a flow',$); +#1669=IFCCLASSIFICATIONREFERENCE($,'[CO]XKA','Sink',#1668,'collecting interfacing object of wastewater',$); +#1670=IFCCLASSIFICATIONREFERENCE($,'[CO]XKB','Toilet',#1668,'collecting interfacing object of faeces and urine',$); +#1671=IFCCLASSIFICATIONREFERENCE($,'[CO]XKC','Urinal',#1668,'collecting interfacing object of urine only',$); +#1672=IFCCLASSIFICATIONREFERENCE($,'[CO]XKD','Drip cup',#1668,'collecting interfacing object of surplus liquid from technical system',$); +#1673=IFCCLASSIFICATIONREFERENCE($,'[CO]XKE','Floor drain',#1668,'collecting interfacing object of surplus',$); +#1674=IFCCLASSIFICATIONREFERENCE($,'[CO]XM?','Sealed flow connecting object',#1643,'Interfacing object for sealed material flow',$); +#1675=IFCCLASSIFICATIONREFERENCE($,'[CO]XMA','Tube flange',#1674,'sealed flow connecting object by a reversible form',$); +#1676=IFCCLASSIFICATIONREFERENCE($,'[CO]XMB','Tube fitting',#1674,'sealed flow connecting object by an irreversible form',$); +#1677=IFCCLASSIFICATIONREFERENCE($,'[CO]XN?','Non-detachable coupling',#1643,'interfacing object for continuous transfer of mechanical forces',$); +#1678=IFCCLASSIFICATIONREFERENCE($,'[CO]XNA','Flange coupler',#1677,'non-detachable coupling of shafts by a pair of flanges',$); +#1679=IFCCLASSIFICATIONREFERENCE($,'[CO]XNB','Split-muff coupler',#1677,'non-detachable coupling of shafts by a splitted sleeve',$); +#1680=IFCCLASSIFICATIONREFERENCE($,'[CO]XNC','Hirth coupler',#1677,'non-detachable coupling between parallel shafts by teeth and grooves meshing together',$); +#1681=IFCCLASSIFICATIONREFERENCE($,'[CO]XND','Cardan joint',#1677,'non-detachable coupling for transmitting power of misaligned shafts',$); +#1682=IFCCLASSIFICATIONREFERENCE($,'[CO]XNE','Buffer-and-chain coupler',#1677,'non-detachable coupling to catch and connect two moving objects',$); +#1683=IFCCLASSIFICATIONREFERENCE($,'[CO]XNF','Link-and pin coupler',#1677,'non-detachable coupling to fix two moving objects',$); +#1684=IFCCLASSIFICATIONREFERENCE($,'[CO]XP?','Detachable coupling',#1643,'interfacing object for discontinuous transfer of mechanical forces',$); +#1685=IFCCLASSIFICATIONREFERENCE($,'[CO]XPA','Friction clutch',#1684,'detachable coupling for power transmission by means of frictional forces',$); +#1686=IFCCLASSIFICATIONREFERENCE($,'[CO]XPB','Fluid coupling',#1684,'detachable coupling for power transmission by means of hydrodynamic effect',$); +#1687=IFCCLASSIFICATIONREFERENCE($,'[CO]XPC','Overrunning clutch',#1684,'detachable coupling for power transmission by interlocking means',$); +#1688=IFCCLASSIFICATIONREFERENCE($,'[CO]XPD','Magnetic hoist',#1684,'detachable coupling for load by means of magnetic force',$); +#1689=IFCCLASSIFICATIONREFERENCE($,'[CO]XS?','Level connecting component',#1643,'interfacing object connecting levels',$); +#1690=IFCCLASSIFICATIONREFERENCE($,'[CO]XSA','Landing',#1689,'level connecting object in the form of a horizontal plane connected to a flight of stairs',$); +#1691=IFCCLASSIFICATIONREFERENCE($,'[CO]XSB','Flight of stairs',#1689,'level connecting object in the form of sequential steps',$); +#1692=IFCCLASSIFICATIONREFERENCE($,'[CO]XSC','Ladder',#1689,'level connecting object in the form of sequential rungs or bars',$); +#1693=IFCCLASSIFICATIONREFERENCE($,'[CO]XSD','Ramp',#1689,'level connecting object in the form of an inclined plane',$); +#1694=IFCCLASSIFICATIONREFERENCE($,'[CO]XSE','Fireman’s pole',#1689,'level connecting object in the form of a vertical pole',$); +#1695=IFCCLASSIFICATIONREFERENCE($,'[CO]XT?','Space linking object',#1643,'interfacing object linking space',$); +#1696=IFCCLASSIFICATIONREFERENCE($,'[CO]XTA','Hole',#1695,'space linking object in the form of penetration',$); +#1697=IFCCLASSIFICATIONREFERENCE($,'[CO]XTB','Box-out',#1695,'space linking object in the form of cavity',$); +#1698=IFCCLASSIFICATIONREFERENCE($,'[CO]XZ?','Multiple flow connector object',#1643,'interfacing object with multiple kinds of flows',$); +#1699=IFCCLASSIFICATIONREFERENCE($,'[CO]XZA','Multi connector',#1698,'multiple flow connector object of electric energy, electrical signal, optical signals and/or fluid',$); +ENDSEC; +END-ISO-10303-21; diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py index bdc74c516a..7654062959 100644 --- a/src/blenderbim/blenderbim/bim/ui.py +++ b/src/blenderbim/blenderbim/bim/ui.py @@ -127,6 +127,42 @@ class BIM_PT_section_with_cappings(Panel): row.operator("bim.clipping_plane_cut_with_cappings", icon="XRAY", text="Cut") row.operator("bim.revert_clipping_plane_cut", icon="FILE_REFRESH", text="Revert Cut") + props = context.scene.BIMProjectProperties + box = layout.box() + header = box.row(align=True) + header.label(text="Clipping Planes") + header.operator("bim.save_clipping_planes", text="", icon="EXPORT") + header.operator("bim.load_clipping_planes", text="", icon="IMPORT") + header.operator("bim.create_clipping_plane", text="", icon="ADD") + + box.template_list( + "BIM_UL_clipping_plane", + "", + props, + "clipping_planes", + props, + "clipping_planes_active", + ) + + if props.clipping_planes_active < len(props.clipping_planes): + active_clipping_plane_obj = props.clipping_planes[props.clipping_planes_active].obj + box.prop(active_clipping_plane_obj, "location") + box.prop(active_clipping_plane_obj, "rotation_euler") + + +class BIM_UL_clipping_plane(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item and item.obj: + obj = item.obj + row = layout.row(align=True) + row.prop(obj, "name", text="", emboss=False) + row.operator("bim.select_object", text="", icon="RESTRICT_SELECT_OFF").obj_name = obj.name + row.context_pointer_set("active_object", obj) + row.operator("bim.flip_clipping_plane", text="", icon="PASTEFLIPDOWN") + row.operator("bim.delete_object", text="", icon="TRASH").obj_name = obj.name + else: + layout.label(text="", translate=False) + class BIM_UL_generic(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): @@ -168,6 +204,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): name="Should Make A Cha-Ching Sound When Project Costs Updates", default=False ) lock_grids_on_import: BoolProperty(name="Should Lock Grids By Default", default=True) + spatial_elements_unselectable: BoolProperty(name="Should Make Spatial Elements Unselectable By Default", default=True) decorations_colour: bpy.props.FloatVectorProperty( name="Decorations Colour", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4 ) @@ -249,6 +286,10 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): row.prop(self, "should_play_chaching_sound") row = layout.row() row.prop(self, "lock_grids_on_import") + row = layout.row() + row.prop(self, "spatial_elements_unselectable") + + row = layout.row() row.prop(context.scene.BIMProjectProperties, "should_disable_undo_on_save") @@ -432,7 +473,7 @@ class BIM_PT_tab_grouping_and_filtering(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_options = {"DEFAULT_CLOSED"} + bl_options = {"DEFAULT_CLOSED","HEADER_LAYOUT_EXPAND"} @classmethod def poll(cls, context): @@ -441,6 +482,13 @@ class BIM_PT_tab_grouping_and_filtering(Panel): def draw(self, context): pass + def draw_header(self, context): + # Draws help button on the right + row = self.layout.row(align=True) + row.label(text="") # empty text occupies the left of the row + row.operator("bim.open_uri", text="", icon="HELP").uri = \ + "https://docs.ifcopenshell.org/ifcopenshell-python/selector_syntax.html" + class BIM_PT_tab_geometry(Panel): bl_label = "Geometry" diff --git a/src/blenderbim/blenderbim/core/aggregate.py b/src/blenderbim/blenderbim/core/aggregate.py index 5445a5647c..58d10814a7 100644 --- a/src/blenderbim/blenderbim/core/aggregate.py +++ b/src/blenderbim/blenderbim/core/aggregate.py @@ -29,7 +29,7 @@ def assign_object(ifc, aggregator, collector, relating_obj=None, related_obj=Non if not aggregator.can_aggregate(relating_obj, related_obj): return rel = ifc.run( - "aggregate.assign_object", product=ifc.get_entity(related_obj), relating_object=ifc.get_entity(relating_obj) + "aggregate.assign_object", products=[ifc.get_entity(related_obj)], relating_object=ifc.get_entity(relating_obj) ) collector.assign(relating_obj) collector.assign(related_obj) @@ -45,9 +45,9 @@ def unassign_object(ifc, aggregate, collector, relating_obj=None, related_obj=No if related_element: relating_obj = ifc.get_object(relating_element) if relating_obj: - ifc.run("aggregate.unassign_object", product=related_element) + ifc.run("aggregate.unassign_object", products=[related_element]) if container: - ifc.run("spatial.assign_container", product=related_element, relating_structure=container) + ifc.run("spatial.assign_container", products=[related_element], relating_structure=container) collector.assign(relating_obj) collector.assign(related_obj) diff --git a/src/blenderbim/blenderbim/core/brick.py b/src/blenderbim/blenderbim/core/brick.py index c16b341b1c..58e7afed18 100644 --- a/src/blenderbim/blenderbim/core/brick.py +++ b/src/blenderbim/blenderbim/core/brick.py @@ -74,7 +74,7 @@ def assign_brick_reference(ifc, brick, element=None, library=None, brick_uri=Non if not reference: reference = ifc.run("library.add_reference", library=library) ifc.run("library.edit_reference", reference=reference, attributes=brick.export_brick_attributes(brick_uri)) - ifc.run("library.assign_reference", product=element, reference=reference) + ifc.run("library.assign_reference", products=[element], reference=reference) project = brick.get_brickifc_project() if not project: project = brick.add_brickifc_project(brick.get_namespace(brick_uri)) diff --git a/src/blenderbim/blenderbim/core/covering.py b/src/blenderbim/blenderbim/core/covering.py index 49cf4722b4..f98cbe267a 100644 --- a/src/blenderbim/blenderbim/core/covering.py +++ b/src/blenderbim/blenderbim/core/covering.py @@ -47,7 +47,6 @@ def add_instance_flooring_covering_from_cursor(ifc, spatial, model, Type, geomet obj = spatial.get_named_obj_from_mesh(name, mesh) spatial.set_obj_origin_to_cursor_position(obj) -# spatial.traslate_obj_to_z_location(obj, z) spatial.link_obj_to_active_collection(obj) points = spatial.get_2d_vertices_from_obj(obj) points = spatial.get_scaled_2d_vertices(points) @@ -153,7 +152,7 @@ def add_instance_flooring_coverings_from_walls(ifc, spatial, collector, geometry spatial.regen_obj_representation(ifc, geometry, obj, body) def add_instance_ceiling_coverings_from_walls(ifc, spatial, collector, geometry, covering): - z = covering.get_z_from_ceiling_height() + z = covering.get_z_from_ceiling_height() + spatial.get_active_obj_z() union = spatial.get_union_shape_from_selected_objects() for i, linear_ring in enumerate(union.interiors): poly = spatial.get_buffered_poly_from_linear_ring(linear_ring) diff --git a/src/blenderbim/blenderbim/core/document.py b/src/blenderbim/blenderbim/core/document.py index ce6dc374c6..ce58e1edf4 100644 --- a/src/blenderbim/blenderbim/core/document.py +++ b/src/blenderbim/blenderbim/core/document.py @@ -109,8 +109,8 @@ def remove_document(ifc, document_tool, document=None): def assign_document(ifc, product=None, document=None): - ifc.run("document.assign_document", product=product, document=document) + ifc.run("document.assign_document", products=[product], document=document) def unassign_document(ifc, product=None, document=None): - ifc.run("document.unassign_document", product=product, document=document) + ifc.run("document.unassign_document", products=[product], document=document) diff --git a/src/blenderbim/blenderbim/core/drawing.py b/src/blenderbim/blenderbim/core/drawing.py index 32333dde0c..cccdc16ad1 100644 --- a/src/blenderbim/blenderbim/core/drawing.py +++ b/src/blenderbim/blenderbim/core/drawing.py @@ -241,7 +241,7 @@ def add_drawing(ifc, collector, drawing, target_view=None, location_hint=None): attributes = {"Identification": "X", "Name": drawing_name, "Scope": "DRAWING"} ifc.run("document.edit_information", information=information, attributes=attributes) ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri}) - ifc.run("document.assign_document", product=element, document=reference) + ifc.run("document.assign_document", products=[element], document=reference) drawing.import_drawings() @@ -251,7 +251,7 @@ def duplicate_drawing(ifc, drawing_tool, drawing=None, should_duplicate_annotati drawing_tool.copy_representation(drawing, new_drawing) drawing_tool.set_name(new_drawing, drawing_name) group = drawing_tool.get_drawing_group(new_drawing) - ifc.run("group.unassign_group", group=group, product=new_drawing) + ifc.run("group.unassign_group", group=group, products=[new_drawing]) new_group = ifc.run("group.add_group") ifc.run("group.edit_group", group=new_group, attributes={"Name": drawing_name, "ObjectType": "DRAWING"}) ifc.run("group.assign_group", group=new_group, products=[new_drawing]) @@ -261,11 +261,11 @@ def duplicate_drawing(ifc, drawing_tool, drawing=None, should_duplicate_annotati continue new_annotation = ifc.run("root.copy_class", product=annotation) drawing_tool.copy_representation(annotation, new_annotation) - ifc.run("group.unassign_group", group=group, product=new_annotation) + ifc.run("group.unassign_group", group=group, products=[new_annotation]) ifc.run("group.assign_group", group=new_group, products=[new_annotation]) old_reference = drawing_tool.get_drawing_document(new_drawing) - ifc.run("document.unassign_document", product=new_drawing, document=old_reference) + ifc.run("document.unassign_document", products=[new_drawing], document=old_reference) information = ifc.run("document.add_information") uri = drawing_tool.get_default_drawing_path(drawing_name) @@ -276,7 +276,7 @@ def duplicate_drawing(ifc, drawing_tool, drawing=None, should_duplicate_annotati attributes = {"Identification": "X", "Name": drawing_name, "Scope": "DRAWING"} ifc.run("document.edit_information", information=information, attributes=attributes) ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri}) - ifc.run("document.assign_document", product=new_drawing, document=reference) + ifc.run("document.assign_document", products=[new_drawing], document=reference) drawing_tool.import_drawings() return new_drawing @@ -388,13 +388,13 @@ def sync_references(ifc, collector, drawing_tool, drawing=None): 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 - elif not reference_obj: - should_delete_existing_annotation = True - if reference_obj and (should_delete_existing_annotation or not annotation): + if should_delete_existing_annotation or not annotation: should_create_annotation = True if should_delete_existing_annotation: diff --git a/src/blenderbim/blenderbim/core/geometry.py b/src/blenderbim/blenderbim/core/geometry.py index 8677152284..6226059ebb 100644 --- a/src/blenderbim/blenderbim/core/geometry.py +++ b/src/blenderbim/blenderbim/core/geometry.py @@ -16,10 +16,17 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . -import blenderbim.core.style +from __future__ import annotations +from typing import TYPE_CHECKING, Optional +import blenderbim.core.tool as tool + +if TYPE_CHECKING: + import bpy -def edit_object_placement(ifc, geometry, surveyor, obj=None): +def edit_object_placement( + ifc: tool.Ifc, geometry: tool.Geometry, surveyor: tool.Surveyor, obj: Optional[bpy.types.Object] = None +) -> None: element = ifc.get_entity(obj) if not element: return @@ -192,23 +199,25 @@ def select_connection(geometry, connection=None): def remove_connection(geometry, connection=None): geometry.remove_connection(connection) + def get_similar_openings(ifc, opening): model = ifc.get() all_openings = model.by_type("IfcOpeningElement") similar_openings = [o for o in all_openings if o.ObjectPlacement == opening.ObjectPlacement and o != opening] return similar_openings + def get_similar_openings_building_objs(ifc, similar_openings): building_objs = [] for similar_opening in similar_openings: building_objs.append(ifc.get_object(similar_opening.VoidsElements[0].RelatingBuildingElement)) return building_objs -def edit_similar_opening_placement(geometry, opening = None, similar_openings = None): + +def edit_similar_opening_placement(geometry, opening=None, similar_openings=None): if not opening or not similar_openings: return for similar_opening in similar_openings: old_placement = similar_opening.ObjectPlacement similar_opening.ObjectPlacement = opening.ObjectPlacement geometry.delete_opening_object_placement(old_placement) - diff --git a/src/blenderbim/blenderbim/core/library.py b/src/blenderbim/blenderbim/core/library.py index eba4fc770a..6b5ce7e767 100644 --- a/src/blenderbim/blenderbim/core/library.py +++ b/src/blenderbim/blenderbim/core/library.py @@ -83,8 +83,8 @@ def edit_library_reference(ifc, library): def assign_library_reference(ifc, obj=None, reference=None): - ifc.run("library.assign_reference", product=ifc.get_entity(obj), reference=reference) + ifc.run("library.assign_reference", products=[ifc.get_entity(obj)], reference=reference) def unassign_library_reference(ifc, obj=None, reference=None): - ifc.run("library.unassign_reference", product=ifc.get_entity(obj), reference=reference) + ifc.run("library.unassign_reference", products=[ifc.get_entity(obj)], reference=reference) diff --git a/src/blenderbim/blenderbim/core/material.py b/src/blenderbim/blenderbim/core/material.py index ba1a19a1b0..da265b2a7c 100644 --- a/src/blenderbim/blenderbim/core/material.py +++ b/src/blenderbim/blenderbim/core/material.py @@ -102,7 +102,7 @@ def assign_material(ifc, material_tool, material_type, objects): element = ifc.get_entity(obj) if not element: continue - ifc.run("material.assign_material", product=element, type=material_type, material=material) + ifc.run("material.assign_material", products=[element], type=material_type, material=material) assigned_material = material_tool.get_material(element) if material_tool.is_a_material_set(assigned_material): material_tool.add_material_to_set(material_set=assigned_material, material=material) @@ -116,12 +116,12 @@ def unassign_material(ifc, material_tool, objects): inherited_material = material_tool.get_material(element, should_inherit=True) if material and "Usage" in material.is_a(): element_type = material_tool.get_type(element) - ifc.run("material.unassign_material", product=element_type) + ifc.run("material.unassign_material", products=[element_type]) elif not material and inherited_material: element_type = material_tool.get_type(element) - ifc.run("material.unassign_material", product=element_type) + ifc.run("material.unassign_material", products=[element_type]) elif material: - ifc.run("material.unassign_material", product=element) + ifc.run("material.unassign_material", products=[element]) def patch_non_parametric_mep_segment(ifc, material_tool, profile_tool, obj): diff --git a/src/blenderbim/blenderbim/core/nest.py b/src/blenderbim/blenderbim/core/nest.py index 087a1aa5f6..2dec6b2280 100644 --- a/src/blenderbim/blenderbim/core/nest.py +++ b/src/blenderbim/blenderbim/core/nest.py @@ -29,7 +29,9 @@ def assign_object(ifc, nest, collector, relating_obj=None, related_obj=None): if not nest.can_nest(relating_obj, related_obj): return rel = ifc.run( - "nest.assign_object", related_object=ifc.get_entity(related_obj), relating_object=ifc.get_entity(relating_obj) + "nest.assign_object", + related_objects=[ifc.get_entity(related_obj)], + relating_object=ifc.get_entity(relating_obj), ) collector.assign(relating_obj) collector.assign(related_obj) @@ -45,9 +47,9 @@ def unassign_object(ifc, nest, collector, relating_obj=None, related_obj=None): if related_element: relating_obj = ifc.get_object(relating_element) if relating_obj: - ifc.run("nest.unassign_object", related_object=related_element) + ifc.run("nest.unassign_object", related_objects=[related_element]) if container: - ifc.run("spatial.assign_container", product=related_element, relating_structure=container) + ifc.run("spatial.assign_container", products=[related_element], relating_structure=container) collector.assign(relating_obj) collector.assign(related_obj) diff --git a/src/blenderbim/blenderbim/core/resource.py b/src/blenderbim/blenderbim/core/resource.py index dd3875a04f..63524cd03c 100644 --- a/src/blenderbim/blenderbim/core/resource.py +++ b/src/blenderbim/blenderbim/core/resource.py @@ -195,7 +195,7 @@ def add_usage_constraint(ifc, resource_tool, resource=None, reference_path=None) }, ) ifc.run("constraint.add_metric_reference", metric=metric, reference_path=reference_path) - ifc.run("constraint.assign_constraint", product=resource, constraint=objective) + ifc.run("constraint.assign_constraint", products=[resource], constraint=objective) def remove_usage_constraint(ifc, resource_tool, resource, reference_path): @@ -206,7 +206,7 @@ def remove_usage_constraint(ifc, resource_tool, resource, reference_path): reference = resource_tool.get_metric_reference(metric, is_deep=True) if reference == reference_path: ifc.run("constraint.remove_metric", metric=metric) - ifc.run("constraint.unassign_constraint", product=resource, constraint=constraint) + ifc.run("constraint.unassign_constraint", products=[resource], constraint=constraint) ifc.run("constraint.remove_constraint", constraint=constraint) diff --git a/src/blenderbim/blenderbim/core/sequence.py b/src/blenderbim/blenderbim/core/sequence.py index 2fa2debf5d..9f0b5ab903 100644 --- a/src/blenderbim/blenderbim/core/sequence.py +++ b/src/blenderbim/blenderbim/core/sequence.py @@ -61,11 +61,11 @@ def remove_work_schedule(ifc, work_schedule=None): def assign_work_schedule(ifc, work_plan=None, work_schedule=None): if work_schedule: - return ifc.run("aggregate.assign_object", relating_object=work_plan, product=work_schedule) + return ifc.run("aggregate.assign_object", relating_object=work_plan, products=[work_schedule]) def unassign_work_schedule(ifc, work_schedule=None): - ifc.run("aggregate.unassign_object", product=work_schedule) + ifc.run("aggregate.unassign_object", products=[work_schedule]) def enable_editing_work_schedule(sequence, work_schedule=None): diff --git a/src/blenderbim/blenderbim/core/spatial.py b/src/blenderbim/blenderbim/core/spatial.py index 6a7f0806d0..171642900a 100644 --- a/src/blenderbim/blenderbim/core/spatial.py +++ b/src/blenderbim/blenderbim/core/spatial.py @@ -16,23 +16,47 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . +from __future__ import annotations +from typing import TYPE_CHECKING, Optional, Union +import blenderbim.core.tool as tool -def reference_structure(ifc, spatial, structure=None, element=None): +if TYPE_CHECKING: + import bpy + import ifcopenshell + + +def reference_structure( + ifc: tool.Ifc, + spatial: tool.Spatial, + structure: Optional[ifcopenshell.entity_instance] = None, + element: Optional[ifcopenshell.entity_instance] = None, +) -> Union[ifcopenshell.entity_instance, None]: if spatial.can_reference(structure, element): - return ifc.run("spatial.reference_structure", product=element, relating_structure=structure) + return ifc.run("spatial.reference_structure", products=[element], relating_structure=structure) -def dereference_structure(ifc, spatial, structure=None, element=None): +def dereference_structure( + ifc: tool.Ifc, + spatial: tool.Spatial, + structure: Optional[ifcopenshell.entity_instance] = None, + element: Optional[ifcopenshell.entity_instance] = None, +) -> None: if spatial.can_reference(structure, element): - return ifc.run("spatial.dereference_structure", product=element, relating_structure=structure) + return ifc.run("spatial.dereference_structure", products=[element], relating_structure=structure) -def assign_container(ifc, collector, spatial, structure_obj=None, element_obj=None): +def assign_container( + ifc: tool.Ifc, + collector: tool.Collector, + spatial: tool.Spatial, + structure_obj: Optional[bpy.types.Object] = None, + element_obj: Optional[bpy.types.Object] = None, +) -> Union[ifcopenshell.entity_instance, None]: if not spatial.can_contain(structure_obj, element_obj): return rel = ifc.run( "spatial.assign_container", - product=ifc.get_entity(element_obj), + products=[ifc.get_entity(element_obj)], relating_structure=ifc.get_entity(structure_obj), ) spatial.disable_editing(element_obj) @@ -54,7 +78,7 @@ def change_spatial_level(spatial, parent=None): def remove_container(ifc, collector, obj=None): - ifc.run("spatial.remove_container", product=ifc.get_entity(obj)) + ifc.run("spatial.unassign_container", products=[ifc.get_entity(obj)]) collector.assign(obj) @@ -126,7 +150,7 @@ def select_decomposed_elements(spatial): spatial.select_products(spatial.get_decomposed_elements(container)) -#HERE STARTS SPATIAL TOOL +# HERE STARTS SPATIAL TOOL def generate_space(ifc, spatial, model, Type): active_obj = spatial.get_active_obj() selected_objects = spatial.get_selected_objects() @@ -140,21 +164,20 @@ def generate_space(ifc, spatial, model, Type): relating_type = None if selected_objects and active_obj: - x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_active_obj(active_obj) ##mat + x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_active_obj(active_obj) ##mat element = ifc.get_entity(active_obj) else: - x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor() ##mat - + x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor() ##mat space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y) if not space_polygon: return - bm = spatial.get_bmesh_from_polygon(space_polygon, h=h) ##mat + bm = spatial.get_bmesh_from_polygon(space_polygon, h=h) ##mat - mesh = spatial.get_named_mesh_from_bmesh(name = "Space", bmesh = bm) + mesh = spatial.get_named_mesh_from_bmesh(name="Space", bmesh=bm) if element and element.is_a("IfcSpace"): mesh = spatial.get_transformed_mesh_from_local_to_global(mesh) @@ -175,6 +198,7 @@ def generate_space(ifc, spatial, model, Type): if relating_type: spatial.assign_relating_type_to_element(ifc, Type, element, relating_type) + def generate_spaces_from_walls(ifc, spatial, collector): z = spatial.get_active_obj_z() h = spatial.get_active_obj_height() @@ -188,7 +212,7 @@ def generate_spaces_from_walls(ifc, spatial, collector): name = "Space" + str(i) - obj = spatial.get_named_obj_from_bmesh(name, bmesh = bm) + obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm) spatial.set_obj_origin_to_bboxcenter(obj) spatial.traslate_obj_to_z_location(obj, z) @@ -206,6 +230,7 @@ def toggle_space_visibility(ifc, spatial): return spatial.toggle_spaces_visibility_wired_and_textured(spaces) + def toggle_hide_spaces(ifc, spatial): model = ifc.get() spaces = model.by_type("IfcSpace") diff --git a/src/blenderbim/blenderbim/core/system.py b/src/blenderbim/blenderbim/core/system.py index 3c6ce42e5a..8015825f1b 100644 --- a/src/blenderbim/blenderbim/core/system.py +++ b/src/blenderbim/blenderbim/core/system.py @@ -55,11 +55,11 @@ def disable_editing_system(system): def assign_system(ifc, system=None, product=None): - ifc.run("system.assign_system", product=product, system=system) + ifc.run("system.assign_system", products=[product], system=system) def unassign_system(ifc, system=None, product=None): - ifc.run("system.unassign_system", product=product, system=system) + ifc.run("system.unassign_system", products=[product], system=system) def select_system_products(system_tool, system=None): diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index b82a9159ce..c3688ab283 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -76,7 +76,7 @@ class Blender: def get_name(cls, ifc_class, name): pass def get_selected_objects(cls): pass def create_ifc_object(cls, ifc_class: str, name: str = None, data=None): pass - def get_obj_ifc_definition_id(cls, obj=None, obj_type=None): pass + def get_obj_ifc_definition_id(cls, obj=None, obj_type=None, context=None): pass def is_ifc_object(cls, obj): pass def is_ifc_class_active(cls, ifc_class): pass def get_viewport_context(cls): pass @@ -153,6 +153,17 @@ class Bsdd: def should_load_preview_domains(cls): pass +@interface +class Clash: + def export_clash_sets(cls): pass + def get_clash(cls, clash_set, a_global_id, b_global_id): pass + def get_clash_set(cls, name): pass + def get_clash_sets(cls): pass + def import_active_clashes(cls): pass + def load_clash_sets(cls, fn): pass + def look_at(cls, target, location): pass + + @interface class Collector: def assign(cls, obj): pass @@ -508,7 +519,7 @@ class Misc: class Model: def convert_si_to_unit(cls, value): pass def convert_unit_to_si(cls, value): pass - def export_curve(cls, position, edge_indices, points=None): pass + def export_curve(cls, position, edge_indices): pass def export_points(cls, position, indices): pass def export_profile(cls, obj, position=None): pass def generate_occurrence_name(cls, element_type, ifc_class): pass diff --git a/src/blenderbim/blenderbim/core/type.py b/src/blenderbim/blenderbim/core/type.py index ec36ad5ffe..969dc3c125 100644 --- a/src/blenderbim/blenderbim/core/type.py +++ b/src/blenderbim/blenderbim/core/type.py @@ -20,7 +20,7 @@ import blenderbim.core.geometry def assign_type(ifc, type_tool, element=None, type=None): - ifc.run("type.assign_type", related_object=element, relating_type=type) + ifc.run("type.assign_type", related_objects=[element], relating_type=type) obj = ifc.get_object(element) if type_tool.has_material_usage(element): pass # for now, representation regeneration handled by API listeners diff --git a/src/blenderbim/blenderbim/tool/__init__.py b/src/blenderbim/blenderbim/tool/__init__.py index 4f98a7b5e3..80ea55419f 100644 --- a/src/blenderbim/blenderbim/tool/__init__.py +++ b/src/blenderbim/blenderbim/tool/__init__.py @@ -22,6 +22,7 @@ from blenderbim.tool.boundary import Boundary from blenderbim.tool.brick import Brick from blenderbim.tool.bsdd import Bsdd from blenderbim.tool.cad import Cad +from blenderbim.tool.clash import Clash from blenderbim.tool.collector import Collector from blenderbim.tool.context import Context from blenderbim.tool.debug import Debug diff --git a/src/blenderbim/blenderbim/tool/aggregate.py b/src/blenderbim/blenderbim/tool/aggregate.py index 0731c7ffe5..66e549f67d 100644 --- a/src/blenderbim/blenderbim/tool/aggregate.py +++ b/src/blenderbim/blenderbim/tool/aggregate.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . +import bpy import blenderbim.core.tool import blenderbim.tool as tool import ifcopenshell.util.element @@ -23,7 +24,7 @@ import ifcopenshell.util.element class Aggregate(blenderbim.core.tool.Aggregate): @classmethod - def can_aggregate(cls, relating_obj, related_obj): + def can_aggregate(cls, relating_obj: bpy.types.Object, related_obj: bpy.types.Object) -> bool: relating_object = tool.Ifc.get_entity(relating_obj) related_object = tool.Ifc.get_entity(related_obj) if not relating_object or not related_object: @@ -60,4 +61,4 @@ class Aggregate(blenderbim.core.tool.Aggregate): def get_relating_object(cls, related_element): for rel in related_element.Decomposes: if rel.is_a("IfcRelAggregates"): - return rel.RelatingObject \ No newline at end of file + return rel.RelatingObject diff --git a/src/blenderbim/blenderbim/tool/blender.py b/src/blenderbim/blenderbim/tool/blender.py index 8fe8d30f30..63bec8569f 100644 --- a/src/blenderbim/blenderbim/tool/blender.py +++ b/src/blenderbim/blenderbim/tool/blender.py @@ -17,13 +17,17 @@ # along with BlenderBIM Add-on. If not, see . import bpy +import bmesh import json import ifcopenshell.api +import ifcopenshell.util.element import blenderbim.tool as tool import blenderbim.bim +import addon_utils from mathutils import Vector from pathlib import Path -import addon_utils +from blenderbim.bim.ifc import IFC_CONNECTED_TYPE +from typing import Any, Optional, Union, Literal, Iterable, Callable VIEWPORT_ATTRIBUTES = [ @@ -37,13 +41,16 @@ VIEWPORT_ATTRIBUTES = [ "clip_planes", ] +OBJECT_DATA_TYPE = Union[bpy.types.Mesh, bpy.types.Curve, bpy.types.Camera] + class Blender(blenderbim.core.tool.Blender): OBJECT_TYPES_THAT_SUPPORT_EDIT_MODE = ("MESH", "CURVE", "SURFACE", "META", "FONT", "LATTICE", "ARMATURE") OBJECT_TYPES_THAT_SUPPORT_EDIT_GPENCIL_MODE = ("GPENCIL",) + TYPE_MANAGER_ICON = "LIGHTPROBE_VOLUME" if bpy.app.version >= (4, 1, 0) else "LIGHTPROBE_GRID" @classmethod - def get_area_props(cls, context): + def get_area_props(cls, context: bpy.types.Context) -> Any: try: if context.screen.name.endswith("-nonnormal"): # Ctrl-space temporary fullscreen screen = bpy.data.screens[context.screen.name[0 : -len("-nonnormal")]] @@ -55,12 +62,12 @@ class Blender(blenderbim.core.tool.Blender): return @classmethod - def set_active_object(cls, obj): + def set_active_object(cls, obj: bpy.types.Object) -> None: bpy.context.view_layer.objects.active = obj obj.select_set(True) @classmethod - def setup_tabs(cls): + def setup_tabs(cls) -> None: # https://blender.stackexchange.com/questions/140644/how-can-make-the-state-of-a-boolean-property-relative-to-the-3d-view-area for screen in bpy.data.screens: if len(screen.BIMAreaProperties) == 20: @@ -70,7 +77,7 @@ class Blender(blenderbim.core.tool.Blender): screen.BIMAreaProperties.add() @classmethod - def is_tab(cls, context, tab): + def is_tab(cls, context: bpy.types.Context, tab: str) -> bool: aprops = cls.get_area_props(context) if not aprops: return context.screen.BIMTabProperties.tab == tab @@ -79,7 +86,7 @@ class Blender(blenderbim.core.tool.Blender): return aprops.tab == tab @classmethod - def is_default_scene(cls): + def is_default_scene(cls) -> bool: if len(bpy.context.scene.objects) != 3: return False if {obj.type for obj in bpy.context.scene.objects} == {"MESH", "LIGHT", "CAMERA"}: @@ -87,7 +94,7 @@ class Blender(blenderbim.core.tool.Blender): return False @classmethod - def get_name(cls, ifc_class, name): + def get_name(cls, ifc_class: str, name: str) -> str: if not bpy.data.objects.get(f"{ifc_class}/{name}"): return name i = 2 @@ -96,13 +103,15 @@ class Blender(blenderbim.core.tool.Blender): return f"{name} {i}" @classmethod - def get_selected_objects(cls): + def get_selected_objects(cls) -> set[bpy.types.Object]: if bpy.context.selected_objects: return set(bpy.context.selected_objects + [bpy.context.active_object]) return set([bpy.context.active_object]) @classmethod - def create_ifc_object(cls, ifc_class: str, name: str = None, data=None) -> bpy.types.Object: + def create_ifc_object( + cls, ifc_class: str, name: Optional[str] = None, data: Optional[OBJECT_DATA_TYPE] = None + ) -> bpy.types.Object: name = name or "My " + ifc_class name = cls.get_name(ifc_class, name) obj = bpy.data.objects.new(name, data) @@ -110,7 +119,11 @@ class Blender(blenderbim.core.tool.Blender): return obj @classmethod - def get_obj_ifc_definition_id(cls, obj=None, obj_type=None): + def get_obj_ifc_definition_id( + cls, obj: Optional[str] = None, obj_type: Optional[str] = None, context: Optional[bpy.types.Context] = None + ) -> Union[int, None]: + if context is None: + context = bpy.context if obj_type == "Object": return bpy.data.objects.get(obj).BIMObjectProperties.ifc_definition_id elif obj_type == "Material": @@ -122,41 +135,42 @@ class Blender(blenderbim.core.tool.Blender): elif obj_type == "MaterialSetItem": return bpy.data.objects.get(obj).BIMObjectMaterialProperties.active_material_set_item_id elif obj_type == "Task": - return bpy.context.scene.BIMTaskTreeProperties.tasks[ - bpy.context.scene.BIMWorkScheduleProperties.active_task_index + return context.scene.BIMTaskTreeProperties.tasks[ + context.scene.BIMWorkScheduleProperties.active_task_index ].ifc_definition_id elif obj_type == "Cost": - return bpy.context.scene.BIMCostProperties.cost_items[ - bpy.context.scene.BIMCostProperties.active_cost_item_index + return context.scene.BIMCostProperties.cost_items[ + context.scene.BIMCostProperties.active_cost_item_index ].ifc_definition_id elif obj_type == "Resource": - return bpy.context.scene.BIMResourceTreeProperties.resources[ - bpy.context.scene.BIMResourceProperties.active_resource_index + return context.scene.BIMResourceTreeProperties.resources[ + context.scene.BIMResourceProperties.active_resource_index ].ifc_definition_id elif obj_type == "Profile": - return bpy.context.scene.BIMProfileProperties.profiles[ - bpy.context.scene.BIMProfileProperties.active_profile_index + return context.scene.BIMProfileProperties.profiles[ + context.scene.BIMProfileProperties.active_profile_index ].ifc_definition_id elif obj_type == "WorkSchedule": - return bpy.context.scene.BIMWorkScheduleProperties.active_work_schedule_id + return context.scene.BIMWorkScheduleProperties.active_work_schedule_id elif obj_type == "Group": - prop = bpy.context.scene.BIMGroupProperties + prop = context.scene.BIMGroupProperties return prop.groups[prop.active_group_index].ifc_definition_id @classmethod - def is_ifc_object(cls, obj): + def is_ifc_object(cls, obj: bpy.types.Object) -> bool: return bool(obj.BIMObjectProperties.ifc_definition_id) @classmethod - def is_ifc_class_active(cls, ifc_class): - if bpy.context.active_object: - if cls.is_ifc_object(bpy.context.active_object): - return tool.Ifc.get_entity(bpy.context.active_object).is_a(ifc_class) + def is_ifc_class_active(cls, ifc_class: str) -> bool: + obj = bpy.context.active_object + if obj: + if cls.is_ifc_object(obj): + return tool.Ifc.get_entity(obj).is_a(ifc_class) return False return False @classmethod - def show_info_message(cls, text, message_type="INFO"): + def show_info_message(cls, text: str, message_type: Literal["INFO", "ERROR"] = "INFO") -> None: """useful for showing error messages outside blender operators Possible `message_type`: `INFO` / `ERROR`""" @@ -167,14 +181,14 @@ class Blender(blenderbim.core.tool.Blender): bpy.context.window_manager.popup_menu(message_ui, title=message_type.capitalize(), icon=message_type) @classmethod - def get_view3d_area(cls): + def get_view3d_area(cls) -> Union[bpy.types.Area, None]: for window in bpy.context.window_manager.windows: for area in window.screen.areas: if area.type == "VIEW_3D": return area @classmethod - def get_blender_prop_default_value(cls, props, prop_name): + def get_blender_prop_default_value(cls, props, prop_name: str) -> Any: prop_bl_rna = props.bl_rna.properties[prop_name] if getattr(prop_bl_rna, "array_length", 0) > 0: prop_value = prop_bl_rna.default_array @@ -183,7 +197,7 @@ class Blender(blenderbim.core.tool.Blender): return prop_value @classmethod - def get_viewport_context(cls): + def get_viewport_context(cls) -> dict: """Get viewport area context for context overriding. Useful for calling operators outside viewport context. @@ -198,25 +212,25 @@ class Blender(blenderbim.core.tool.Blender): return context_override @classmethod - def get_viewport_position(cls): + def get_viewport_position(cls) -> dict: region_3d = cls.get_viewport_context()["area"].spaces[0].region_3d copy_if_possible = lambda x: x.copy() if hasattr(x, "copy") else x viewport_data = {attr: copy_if_possible(getattr(region_3d, attr)) for attr in VIEWPORT_ATTRIBUTES} return viewport_data @classmethod - def set_viewport_position(cls, data): + def set_viewport_position(cls, data: dict) -> None: region_3d = cls.get_viewport_context()["area"].spaces[0].region_3d for attr in VIEWPORT_ATTRIBUTES: setattr(region_3d, attr, data[attr]) @classmethod - def set_viewport_tool(cls, tool_name): + def set_viewport_tool(cls, tool_name: str) -> None: with bpy.context.temp_override(**tool.Blender.get_viewport_context()): bpy.ops.wm.tool_set_by_id(name=tool_name) @classmethod - def get_shader_editor_context(cls): + def get_shader_editor_context(cls) -> Union[dict, None]: for screen in bpy.data.screens: for area in screen.areas: if area.type == "NODE_EDITOR": @@ -226,7 +240,7 @@ class Blender(blenderbim.core.tool.Blender): return context_override @classmethod - def copy_node_graph(cls, material_to, material_from): + def copy_node_graph(cls, material_to: bpy.types.Material, material_from: bpy.types.Material) -> None: # https://projects.blender.org/blender/blender/issues/108763 if bpy.app.version[:2] == (4, 0): print( @@ -270,7 +284,9 @@ class Blender(blenderbim.core.tool.Blender): shader_editor.pin = previous_pin_setting @classmethod - def get_material_node(cls, blender_material, node_type, kwargs={}): + def get_material_node( + cls, blender_material: bpy.types.Material, node_type: str, kwargs: Optional[dict] = {} + ) -> Union[bpy.types.ShaderNode, None]: """returns first node from the `blender_material` shader graph with type `node_type`""" if not blender_material.use_nodes: return @@ -280,15 +296,15 @@ class Blender(blenderbim.core.tool.Blender): return node @classmethod - def update_screen(cls): + def update_screen(cls) -> None: bpy.ops.wm.redraw_timer(type="DRAW_WIN_SWAP", iterations=1) @classmethod - def update_viewport(cls): + def update_viewport(cls) -> None: tool.Blender.get_viewport_context()["area"].tag_redraw() @classmethod - def force_depsgraph_update(cls): + def force_depsgraph_update(cls) -> None: """useful if you need to trigger callbacks like `depsgraph_update_pre`""" # blender is requiring some ID to be changed # to trigger depsgraph update @@ -297,9 +313,11 @@ class Blender(blenderbim.core.tool.Blender): bpy.context.view_layer.update() @classmethod - def ensure_unique_name(cls, name, objects, iteration=0): + def ensure_unique_name(cls, name: str, objects: Iterable[str], iteration=0) -> str: """returns a unique name for the given name and dictionary of objects - blender style name with .001, .002, etc. suffix + blender style name with .001, .002, etc. suffix. + + objects can be `bpy.data.objects`. """ current_iteration = name if not iteration else f"{name}.{iteration:03d}" if current_iteration not in objects: @@ -307,7 +325,7 @@ class Blender(blenderbim.core.tool.Blender): return cls.ensure_unique_name(name, objects, iteration + 1) @classmethod - def blender_path_to_posix(cls, blender_path): + def blender_path_to_posix(cls, blender_path: str) -> str: """Process blender path to be saved as posix. If path is relative the method will keep it relative to .ifc file @@ -322,7 +340,7 @@ class Blender(blenderbim.core.tool.Blender): return path.as_posix() @classmethod - def get_default_selection_keypmap(cls): + def get_default_selection_keypmap(cls) -> tuple: """keymap to replicate default blender selection behaviour with click and box selection""" # code below comes from blender_default.py which is part of default blender scripts licensed under GPL v2 # https://github.com/blender/blender/blob/master/release/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -369,7 +387,9 @@ class Blender(blenderbim.core.tool.Blender): return keymap @classmethod - def add_layout_hotkey_operator(cls, tool_name, layout, text, hotkey, description): + def add_layout_hotkey_operator( + cls, tool_name: str, layout: bpy.types.UILayout, text: str, hotkey: str, description: str + ) -> tuple[bpy.types.OperatorProperties, bpy.types.UILayout]: modifiers = { "A": "EVENT_ALT", "S": "EVENT_SHIFT", @@ -386,7 +406,7 @@ class Blender(blenderbim.core.tool.Blender): return op, row @classmethod - def get_object_bounding_box(cls, obj): + def get_object_bounding_box(cls, obj: bpy.types.Object) -> dict: """Returns dict with local min and max x, y, z values for the object. Careful with using this method for objects in EDIT mode because @@ -407,14 +427,20 @@ class Blender(blenderbim.core.tool.Blender): return bbox_dict @classmethod - def select_and_activate_single_object(cls, context, active_object): + def select_and_activate_single_object(cls, context: bpy.types.Context, active_object: bpy.types.Object) -> None: for obj in context.selected_objects: obj.select_set(False) context.view_layer.objects.active = active_object active_object.select_set(True) @classmethod - def set_objects_selection(cls, context, active_object, selected_objects, clear_previous_selection=True): + def set_objects_selection( + cls, + context: bpy.types.Context, + active_object: Optional[bpy.types.Object] = None, + selected_objects: list[bpy.types.Object] = list(), + clear_previous_selection=True, + ) -> None: if clear_previous_selection: for obj in context.selected_objects: obj.select_set(False) @@ -425,7 +451,7 @@ class Blender(blenderbim.core.tool.Blender): active_object.select_set(True) @classmethod - def enum_property_has_valid_index(cls, props, prop_name, enum_items): + def enum_property_has_valid_index(cls, props, prop_name: str, enum_items: tuple) -> bool: """method created for readibility and to avoid console warnings like `pyrna_enum_to_py: current value '17' matches no enum in 'BIMModelProperties', '', 'relating_type_id'` """ @@ -436,7 +462,7 @@ class Blender(blenderbim.core.tool.Blender): return current_value_index < len(enum_items) @classmethod - def append_data_block(cls, filepath, data_block_type, name, link=False, relative=False): + def append_data_block(cls, filepath: str, data_block_type: str, name: str, link=False, relative=False) -> dict: if Path(filepath) == Path(bpy.data.filepath): data_block = getattr(bpy.data, data_block_type).get(name, None) if not data_block: @@ -450,7 +476,7 @@ class Blender(blenderbim.core.tool.Blender): return {"data_block": getattr(data_to, data_block_type)[0], "msg": ""} @classmethod - def remove_data_block(cls, data_block, do_unlink=True): + def remove_data_block(cls, data_block: bpy.types.ID, do_unlink=True) -> None: """Removes a datablock (such as a mesh) See https://projects.blender.org/blender/blender/issues/118787 for more @@ -472,7 +498,7 @@ class Blender(blenderbim.core.tool.Blender): ## BMESH UTILS ## @classmethod - def apply_bmesh(cls, mesh, bm, obj=None): + def apply_bmesh(cls, mesh: bpy.types.Mesh, bm: bmesh.types.BMesh, obj: Optional[bpy.types.Object] = None) -> None: """`obj` argument is not optional if you plan to update mesh in EDIT mode and it's possible that that mesh object won't be currenly active.""" import bmesh @@ -506,7 +532,7 @@ class Blender(blenderbim.core.tool.Blender): mesh.update() @classmethod - def get_bmesh_for_mesh(cls, mesh, clean=False): + def get_bmesh_for_mesh(cls, mesh: bpy.types.Mesh, clean=False) -> bmesh.types.BMesh: import bmesh if mesh.is_editmode: @@ -520,7 +546,16 @@ class Blender(blenderbim.core.tool.Blender): return bm @classmethod - def bmesh_join(cls, bm_a, bm_b, callback=None): + def bmesh_join( + cls, + bm_a: bmesh.types.BMesh, + bm_b: bmesh.types.BMesh, + callback: Optional[ + Callable[ + [bmesh.types.BMesh, list[bmesh.types.BMVert], list[bmesh.types.BMEdge], list[bmesh.types.BMFace]], None + ] + ] = None, + ): """Join two meshes into single one, store it in `bm_a`""" import bmesh @@ -535,7 +570,19 @@ class Blender(blenderbim.core.tool.Blender): return bm_a @classmethod - def toggle_edit_mode(cls, context): + def bmesh_check_vertex_in_groups( + cls, vertex: bmesh.types.BMVert, deform_layer: bmesh.types.BMLayerItem, groups: list[int] + ) -> Union[tuple[Literal[True], int], tuple[Literal[False], None]]: + """returns tuple boolean (whether vertex is in any of the groups) and related group index""" + for group_index in vertex[deform_layer].keys(): + # ignore vertex groups assignments produced by edge subdivision near arcs + # they usually have weight = 0.5 + if group_index in groups and vertex[deform_layer][group_index] == 1.0: + return True, group_index + return False, None + + @classmethod + def toggle_edit_mode(cls, context: bpy.types.Context) -> set: ao = context.active_object if not ao: return {"CANCELLED"} @@ -547,21 +594,21 @@ class Blender(blenderbim.core.tool.Blender): return {"CANCELLED"} @classmethod - def is_object_an_ifc_class(cls, obj, classes): + def is_object_an_ifc_class(cls, obj: bpy.types.Object, classes: Iterable[str]) -> bool: if not tool.Ifc.get(): return False element = tool.Ifc.get_entity(obj) return element and element.is_a() in classes @classmethod - def get_object_from_guid(cls, guid): + def get_object_from_guid(cls, guid: str) -> Union[IFC_CONNECTED_TYPE, None]: element = tool.Ifc.get().by_guid(guid) obj = tool.Ifc.get_object(element) if obj: return obj @classmethod - def lock_transform(cls, obj, lock_state=True): + def lock_transform(cls, obj: bpy.types.ObjectBase, lock_state=True) -> None: for prop in ("lock_location", "lock_rotation", "lock_scale"): attr = getattr(obj, prop) for axis_idx in range(3): @@ -570,7 +617,9 @@ class Blender(blenderbim.core.tool.Blender): operator_invoke_filepath_hotkeys_description = "Hold Shift to open the file, Alt to browse containing directory" @classmethod - def operator_invoke_filepath_hotkeys(cls, operator, context, event, filepath: Path): + def operator_invoke_filepath_hotkeys( + cls, operator: bpy.types.Operator, context: bpy.types.Context, event: bpy.types.Event, filepath: Path + ) -> Union[set, None]: if not event.alt and not event.shift: return @@ -611,6 +660,43 @@ class Blender(blenderbim.core.tool.Blender): open_file_or_folder(filepath.as_posix()) return {"PASS_THROUGH"} + @classmethod + def get_layer_collection( + cls, collection: bpy.types.Collection, view_layer: Optional[bpy.types.ViewLayer] = None + ) -> Union[bpy.types.LayerCollection, None]: + return cls.get_layer_collections_mapping([collection], view_layer).get(collection) + + @classmethod + def get_layer_collections_mapping( + cls, collections: list[bpy.types.Collection], view_layer: Optional[bpy.types.ViewLayer] = None + ) -> dict[bpy.types.Collection, bpy.types.LayerCollection]: + if view_layer is None: + view_layer = bpy.context.view_layer + + collections = list(collections) # copy to prevent mutation + collections_mapping = dict() + queue = [view_layer.layer_collection] + + while queue: + layer = queue.pop() + collection = layer.collection + if collection in collections: + collections_mapping[collection] = layer + collections.remove(collection) + if not collections: + break + queue.extend(list(layer.children)) + + return collections_mapping + + @classmethod + def object_supports_edit_mode(cls, obj): + if not obj.data: + return False, "Can't Edit Empty Object" + if obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs: + return False, "Can't Edit Clipping Plane Geometry" + return True, "" + class Modifier: @classmethod def is_eligible_for_railing_modifier(cls, obj): @@ -712,8 +798,7 @@ class Blender(blenderbim.core.tool.Blender): @classmethod def get_modifiers_data(cls, parent_element): array_pset = ifcopenshell.util.element.get_pset(parent_element, "BBIM_Array") - for modifier_data in json.loads(array_pset["Data"]): - yield modifier_data + yield from json.loads(array_pset["Data"]) @classmethod def get_children_objects(cls, modifier_data): @@ -760,7 +845,9 @@ class Blender(blenderbim.core.tool.Blender): bpy.utils.register_tool(ws_model.PipeTool, after={"bim.duct_tool"}, separator=False, group=False) bpy.utils.register_tool(ws_model.BimTool, after={"bim.pipe_tool"}, separator=False, group=False) bpy.utils.register_tool(ws_drawing.AnnotationTool, after={"bim.bim_tool"}, separator=True, group=False) - bpy.utils.register_tool(ws_spatial.SpatialTool, after={"bim.annotation_tool"}, separator=False, group=False) + bpy.utils.register_tool( + ws_spatial.SpatialTool, after={"bim.annotation_tool"}, separator=False, group=False + ) bpy.utils.register_tool( ws_structural.StructuralTool, after={"bim.spatial_tool"}, separator=False, group=False ) @@ -797,3 +884,105 @@ class Blender(blenderbim.core.tool.Blender): bpy.utils.unregister_tool(ws_covering.CoveringTool) except: pass + + @classmethod + def get_scene_panels_list(cls) -> tuple[bpy.types.Panel, ...]: + # example default blender scene panels can be found in + # https://projects.blender.org/blender/blender/src/branch/main/scripts/startup/bl_ui/properties_scene.py#L421 + scene_panels: list[str] = [] + panels_to_parents: dict[str, str] = dict() + for item_name in dir(bpy.types): + item = getattr(bpy.types, item_name) + # filter only panels + if not hasattr(item, "bl_rna") or not isinstance(item.bl_rna, bpy.types.Panel): + continue + # ignore bbim panels + if item.__module__.startswith("blenderbim"): + continue + # filter scene panels + if getattr(item, "bl_context", None) != "scene": + continue + scene_panels.append(item_name) + parent_panel = getattr(item, "bl_parent_id", None) + if parent_panel is not None: + panels_to_parents[item_name] = parent_panel + + scene_panels = cls.sort_panels_for_register(scene_panels, panels_to_parents) + final_panels = [getattr(bpy.types, p) for p in scene_panels] + return tuple(final_panels) + + @classmethod + def sort_panels_for_register(cls, items: list[str], items_to_parents: dict[str, str]) -> list[str]: + """sort panels ensuring parents panels will be registered first + as otherwise we'll get errors unregistering them all and registering child panel""" + final_items = [] + unsorted = items.copy() + + # first, add items without parents + for item in unsorted[:]: + if item not in items_to_parents: + final_items.append(item) + unsorted.remove(item) + + # store children for each parent + children: dict[str, list[str]] = dict() + for item in items_to_parents: + children.setdefault(items_to_parents[item], []).append(item) + + # add children recursively, ensuring parents are added first + keep_looking = True + while keep_looking: + keep_looking = False + for item in list(children.keys()): + # check if parent panel was already added + if item not in final_items: + continue + final_items.extend(children[item]) + del children[item] + keep_looking = True + + assert set(items) == set(final_items), "Sorted list doesn't match original" + return final_items + + @classmethod + def override_scene_panel(cls, original_panel: bpy.types.Panel) -> None: + @classmethod + def poll_check_blender_tab(cls, context): + return tool.Blender.is_tab(context, "BLENDER") + + polls = blenderbim.bim.original_scene_panels_polls + + # override poll method + if not hasattr(original_panel, "poll"): + polls[original_panel] = None + original_panel.poll = poll_check_blender_tab + else: + polls[original_panel] = original_panel.poll + + @classmethod + def wrapped_poll(cls, context): + return polls[cls](context) and poll_check_blender_tab.__func__(cls, context) + + original_panel.poll = wrapped_poll + + # reregister to activate new poll + bpy.utils.unregister_class(original_panel) + bpy.utils.register_class(original_panel) + + @classmethod + def remove_scene_panel_override(cls, panel: bpy.types.Panel) -> None: + polls = blenderbim.bim.original_scene_panels_polls + + poll = polls[panel] + if poll is None: + del panel.poll + else: + panel.poll = poll + + # panel might be already unregistered during blender exit + # or if it's addon was disabled + if panel.is_registered: + # reregister to activate new poll + bpy.utils.unregister_class(panel) + bpy.utils.register_class(panel) + del polls[panel] diff --git a/src/blenderbim/blenderbim/tool/boundary.py b/src/blenderbim/blenderbim/tool/boundary.py index c922a23462..1f47e3eb82 100644 --- a/src/blenderbim/blenderbim/tool/boundary.py +++ b/src/blenderbim/blenderbim/tool/boundary.py @@ -72,7 +72,11 @@ class Boundary(blenderbim.core.tool.Boundary): obj.matrix_world = space.matrix_world @classmethod - def decorate_boundary(cls, obj): + def decorate_boundary(cls, obj: bpy.types.Object) -> None: new = bpy.context.scene.BIMBoundaryProperties.boundaries.add() new.obj = obj obj.show_in_front = True + + @classmethod + def undecorate_boundary(cls, obj: bpy.types.Object) -> None: + obj.show_in_front = False diff --git a/src/blenderbim/blenderbim/tool/clash.py b/src/blenderbim/blenderbim/tool/clash.py new file mode 100644 index 0000000000..c83c3f9deb --- /dev/null +++ b/src/blenderbim/blenderbim/tool/clash.py @@ -0,0 +1,117 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2024 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import os +import bpy +import json +import ifcopenshell +import blenderbim.core.tool +import blenderbim.tool as tool +from contextlib import contextmanager +from mathutils import Vector + + +class Clash(blenderbim.core.tool.Clash): + + @classmethod + def export_clash_sets(cls): + clash_sets = [] + for clash_set in bpy.context.scene.BIMClashProperties.clash_sets: + a = [] + b = [] + for ab in ["a", "b"]: + for data in getattr(clash_set, ab): + clash_source = {"file": data.name} + query = tool.Search.export_filter_query(data.filter_groups) + if query and data.mode != "a": + clash_source["selector"] = query + clash_source["mode"] = data.mode + if ab == "a": + a.append(clash_source) + elif ab == "b": + b.append(clash_source) + clash_set_data = {"name": clash_set.name, "mode": clash_set.mode, "a": a, "b": b} + if clash_set.mode == "intersection": + clash_set_data["tolerance"] = clash_set.tolerance + clash_set_data["check_all"] = clash_set.check_all + elif clash_set.mode == "collision": + clash_set_data["allow_touching"] = clash_set.allow_touching + elif clash_set.mode == "clearance": + clash_set_data["clearance"] = clash_set.clearance + clash_set_data["check_all"] = clash_set.check_all + clash_sets.append(clash_set_data) + return clash_sets + + @classmethod + def get_clash(cls, clash_set, a_global_id, b_global_id): + clashes = clash_set.get("clashes", None) + if not clashes: + return + return clashes.get(f"{a_global_id}-{b_global_id}", None) + + @classmethod + def get_clash_set(cls, name): + for clash_set in ClashStore.clash_sets: + if clash_set["name"] == name: + return clash_set + + @classmethod + def get_clash_sets(cls): + return ClashStore.clash_sets + + @classmethod + def import_active_clashes(cls): + clash_set = bpy.context.scene.BIMClashProperties.active_clash_set + if not clash_set: + return + clash_set.clashes.clear() + result = tool.Clash.get_clash_set(clash_set.name) + for clash in sorted(result.get("clashes", {}).values(), key=lambda x: x["distance"]): + blender_clash = clash_set.clashes.add() + blender_clash.a_global_id = clash["a_global_id"] + blender_clash.b_global_id = clash["b_global_id"] + blender_clash.a_name = "{}/{}".format(clash["a_ifc_class"], clash["a_name"]) + blender_clash.b_name = "{}/{}".format(clash["b_ifc_class"], clash["b_name"]) + blender_clash.status = False if not "status" in clash.keys() else clash["status"] + + @classmethod + def load_clash_sets(cls, fn): + with open(fn) as f: + ClashStore.clash_sets = json.load(f) + + @classmethod + def look_at(cls, target, location): + camera_location = location + area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") + region = next(region for region in area.regions if region.type == "WINDOW") + space = next(space for space in area.spaces if space.type == "VIEW_3D") + override = {"area": area, "region": region, "space_data": space} + space.region_3d.view_location = target + space.region_3d.view_rotation = Vector((camera_location - target)).to_track_quat("Z", "Y") + space.region_3d.view_distance = (camera_location - target).length + space.shading.show_xray = True + + +class ClashStore: + clash_sets = None + path = None + + @staticmethod + def purge(): + ClashStore.clash_sets = None + ClashStore.path = None diff --git a/src/blenderbim/blenderbim/tool/collector.py b/src/blenderbim/blenderbim/tool/collector.py index 841a515cc7..095d56ea1d 100644 --- a/src/blenderbim/blenderbim/tool/collector.py +++ b/src/blenderbim/blenderbim/tool/collector.py @@ -20,12 +20,24 @@ import bpy import ifcopenshell.util.element import blenderbim.core.tool +import blenderbim.core.spatial +import blenderbim.core.aggregate import blenderbim.tool as tool +from typing import Union class Collector(blenderbim.core.tool.Collector): @classmethod - def sync(cls, obj): + def sync(cls, obj: bpy.types.Object, skip_unlinking=False) -> None: + """Sync object IFC state (assigned containter / aggregate) with the collection it's currently in. + + Then subsequently run `Collector.assign` (if state has changed) + to link them to collections / unlink from anything unrelated collections. + + If `skip_unlinking` is `True` then method won't try to assign parent object + if it's already assigned in IFC saving some time. + But it has a downside not unlinking object from unrelated collections. + """ # This is the reverse of assign. It reads the Blender collection and figures out its IFC hierarchy element = tool.Ifc.get_entity(obj) @@ -40,48 +52,57 @@ class Collector(blenderbim.core.tool.Collector): if not obj.users_collection: return - object_collection = None - collection_collection = None - - object_collection = cls._get_own_collection(element, obj) - if object_collection: - collection_collection = cls._get_collection(element, obj) - else: - object_collection = cls._get_collection(element, obj) + # create related collections + cls._get_own_collection(element, obj) + cls._get_collection(element, obj) parent_collection = None for collection in obj.users_collection: if parent_collection: break + # skip Types and non-BIM collections if not collection.BIMCollectionProperties.obj: continue + # for objects that own collections we search for the first parent collection if collection.BIMCollectionProperties.obj == obj: + collection_name = collection.name for bpy_collection in bpy.data.collections: - if bpy_collection.children.find(collection.name) != -1: + if bpy_collection.children.get(collection_name) and bpy_collection.BIMCollectionProperties.obj: parent_collection = bpy_collection + parent_obj = bpy_collection.BIMCollectionProperties.obj + break else: parent_collection = collection + parent_obj = collection.BIMCollectionProperties.obj if not parent_collection: return - parent_obj = parent_collection.BIMCollectionProperties.obj - if not parent_obj: - return - parent = tool.Ifc.get_entity(parent_obj) + if skip_unlinking: + previous_parent = ifcopenshell.util.element.get_container( + element, should_get_direct=True + ) or ifcopenshell.util.element.get_aggregate(element) + if parent == previous_parent: + return + if parent: # This is lazy, but works. One of these will succeed, the other will fail silently. blenderbim.core.spatial.assign_container( tool.Ifc, tool.Collector, tool.Spatial, structure_obj=parent_obj, element_obj=obj ) + # NOTE: won't allow assigning IfcElements to the IfcProject directly + # and some elements might get missing in other viewers if they're don't support displaying + # elements without hierarchy blenderbim.core.aggregate.assign_object( tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=parent_obj, related_obj=obj ) @classmethod - def assign(cls, obj): + def assign(cls, obj: bpy.types.Object) -> None: + """link object and it's owned collection to the proper collection + and unlink them from any other""" element = tool.Ifc.get_entity(obj) object_collection = None @@ -93,12 +114,17 @@ class Collector(blenderbim.core.tool.Collector): else: object_collection = cls._get_collection(element, obj) + # NOTE: calling `obj.users_collection` is expensive in large projects + # since it's iterating over all collections under the hood + + # ensure object is linked only to object_collection if obj.users_collection != (object_collection,): for collection in obj.users_collection: collection.objects.unlink(obj) if object_collection is not None: object_collection.objects.link(obj) + # ensure object_collection is linked only to collection_collection if collection_collection and collection_collection.children.find(object_collection.name) == -1: if bpy.context.scene.collection.children.find(object_collection.name) != -1: bpy.context.scene.collection.children.unlink(object_collection) @@ -112,7 +138,10 @@ class Collector(blenderbim.core.tool.Collector): bpy.data.collections.remove(obj.BIMObjectProperties.collection) @classmethod - def _get_own_collection(cls, element, obj): + def _get_own_collection( + cls, element: ifcopenshell.entity_instance, obj: bpy.types.Object + ) -> Union[bpy.types.Collection, None]: + """get or create own collection for the element if it's neccessary for it's type""" if element.is_a("IfcProject"): return cls._create_own_collection(obj) @@ -139,10 +168,8 @@ class Collector(blenderbim.core.tool.Collector): grid_obj = tool.Ifc.get_object(grid) if grid_obj: grid_col = cls._get_own_collection(grid, grid_obj) - axes_col = [c for c in grid_col.children if axes in c.name] - if axes_col: - return axes_col[0] - return bpy.data.collections.new(axes) + axes_col = next((c for c in grid_col.children if axes in c.name), None) + return axes_col or bpy.data.collections.new(axes) if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING": return cls._create_own_collection(obj) @@ -157,11 +184,12 @@ class Collector(blenderbim.core.tool.Collector): return cls._create_own_collection(obj) if getattr(element, "IsNestedBy", None): - if [e for e in element.IsNestedBy[0].RelatedObjects if not e.is_a("IfcPort")]: + if any(e for e in element.IsNestedBy[0].RelatedObjects if not e.is_a("IfcPort")): return cls._create_own_collection(obj) @classmethod - def _get_collection(cls, element, obj): + def _get_collection(cls, element: ifcopenshell.entity_instance, obj: bpy.types.Object) -> bpy.types.Collection: + """get or create collection for the element based on it's type""" if element.is_a("IfcTypeObject"): return cls._create_project_child_collection("Types") @@ -224,12 +252,11 @@ class Collector(blenderbim.core.tool.Collector): project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) if project_obj: - collection = bpy.data.collections.get(project_obj.name) - if collection: - return collection + return project_obj.BIMObjectProperties.collection @classmethod - def _create_project_child_collection(cls, name): + def _create_project_child_collection(cls, name: str) -> bpy.types.Collection: + """get or create new collection inside project""" collection = bpy.data.collections.get(name) if not collection: collection = bpy.data.collections.new(name) @@ -239,7 +266,8 @@ class Collector(blenderbim.core.tool.Collector): return collection @classmethod - def _create_own_collection(cls, obj): + def _create_own_collection(cls, obj: bpy.types.Object) -> bpy.types.Collection: + """get or create own collection for the element""" if obj.BIMObjectProperties.collection: return obj.BIMObjectProperties.collection collection = bpy.data.collections.new(obj.name) diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index e59a40868f..d1d7e9649a 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -18,6 +18,7 @@ import os import re +import collections import bpy import math import json @@ -27,29 +28,36 @@ import shutil import logging import shapely import platform -from shapely.ops import unary_union import mathutils import subprocess import numpy as np import blenderbim.core.tool import blenderbim.core.geometry import blenderbim.tool as tool +import ifcopenshell.api +import ifcopenshell.geom import ifcopenshell.util.representation +import ifcopenshell.util.element +import ifcopenshell.util.selector +import blenderbim.bim.import_ifc import blenderbim.bim.module.drawing.sheeter as sheeter import blenderbim.bim.module.drawing.scheduler as scheduler import blenderbim.bim.module.drawing.annotation as annotation import blenderbim.bim.module.drawing.helper as helper +import blenderbim.core.root +from shapely.ops import unary_union from blenderbim.bim.module.drawing.data import FONT_SIZES, DecoratorData from blenderbim.bim.module.drawing.prop import get_diagram_scales, BOX_ALIGNMENT_POSITIONS, ANNOTATION_TYPES_DATA from lxml import etree -from mathutils import Vector +from mathutils import Vector, Matrix from fractions import Fraction -import collections +from typing import Optional, Union, Iterable, Any +from pathlib import Path class Drawing(blenderbim.core.tool.Drawing): @classmethod - def canonicalise_class_name(self, name): + def canonicalise_class_name(cls, name): return re.sub("[^0-9a-zA-Z]+", "", name) @classmethod @@ -244,7 +252,7 @@ class Drawing(blenderbim.core.tool.Drawing): bpy.data.collections.remove(collection, do_unlink=True) @classmethod - def delete_drawing_elements(cls, elements): + def delete_drawing_elements(cls, elements: Iterable[ifcopenshell.entity_instance]) -> None: for element in elements: obj = tool.Ifc.get_object(element) ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element) @@ -255,7 +263,7 @@ class Drawing(blenderbim.core.tool.Drawing): tool.Blender.remove_data_block(obj_data) @classmethod - def delete_object(cls, obj): + def delete_object(cls, obj: bpy.types.Object) -> None: bpy.data.objects.remove(obj) @classmethod @@ -334,7 +342,7 @@ class Drawing(blenderbim.core.tool.Drawing): return identification @classmethod - def export_text_literal_attributes(cls, obj): + def export_text_literal_attributes(cls, obj: bpy.types.Object) -> list[dict[str, Any]]: literals = [] for literal_props in obj.BIMTextProperties.literals: literal_data = blenderbim.bim.helper.export_attributes(literal_props.attributes) @@ -342,7 +350,9 @@ class Drawing(blenderbim.core.tool.Drawing): return literals @classmethod - def create_annotation_context(cls, target_view, object_type=None): + def create_annotation_context( + cls, target_view: str, object_type: Optional[str] = None + ) -> ifcopenshell.entity_instance: # checking PLAN target view and annotation type that doesn't require 3d if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW") and object_type not in ( "FALL", @@ -363,7 +373,9 @@ class Drawing(blenderbim.core.tool.Drawing): ) @classmethod - def get_annotation_context(cls, target_view, object_type=None): + def get_annotation_context( + cls, target_view: str, object_type: Optional[str] = None + ) -> Union[ifcopenshell.entity_instance, None]: # checking PLAN target view and annotation type that doesn't require 3d if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW") and object_type not in ( "FALL", @@ -378,7 +390,9 @@ class Drawing(blenderbim.core.tool.Drawing): return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") @classmethod - def get_document_uri(cls, document, description=None): + def get_document_uri( + cls, document: ifcopenshell.entity_instance, description: Optional[str] = None + ) -> Union[str, None]: if getattr(document, "Location", None): if os.path.isabs(document.Location): return document.Location @@ -417,13 +431,13 @@ class Drawing(blenderbim.core.tool.Drawing): return obj.BIMObjectProperties.collection @classmethod - def get_drawing_group(cls, drawing): + def get_drawing_group(cls, drawing: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: for rel in drawing.HasAssignments or []: if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING": return rel.RelatingGroup @classmethod - def get_drawing_document(cls, drawing): + def get_drawing_document(cls, drawing: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: for rel in drawing.HasAssociations: if rel.is_a("IfcRelAssociatesDocument"): return rel.RelatingDocument @@ -437,11 +451,11 @@ class Drawing(blenderbim.core.tool.Drawing): return results @classmethod - def get_drawing_target_view(cls, drawing): + def get_drawing_target_view(cls, drawing: ifcopenshell.entity_instance) -> str: return ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {}).get("TargetView", "MODEL_VIEW") @classmethod - def get_group_elements(cls, group): + def get_group_elements(cls, group: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: for rel in group.IsGroupedBy or []: return rel.RelatedObjects @@ -584,13 +598,13 @@ class Drawing(blenderbim.core.tool.Drawing): return ifc_literal @classmethod - def get_assigned_product(cls, element): + 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 @classmethod - def import_annotations_in_group(cls, group): + def import_annotations_in_group(cls, group: ifcopenshell.entity_instance) -> None: elements = set( [e for e in cls.get_group_elements(group) if e.is_a("IfcAnnotation") and e.ObjectType != "DRAWING"] ) @@ -600,6 +614,7 @@ class Drawing(blenderbim.core.tool.Drawing): ifc_importer.file = tool.Ifc.get() ifc_importer.calculate_unit_scale() ifc_importer.process_context_filter() + ifc_importer.load_existing_meshes() ifc_importer.material_creator.load_existing_materials() ifc_importer.create_generic_elements(elements) for obj in ifc_importer.added_data.values(): @@ -609,7 +624,7 @@ class Drawing(blenderbim.core.tool.Drawing): # but BIMCameraProperties are only synced with EPsetDrawing at drawing import # therefore camera props can differ from pset if the user changed them from pset. @classmethod - def import_drawing(cls, drawing): + def import_drawing(cls, drawing: ifcopenshell.entity_instance) -> bpy.types.Object: settings = ifcopenshell.geom.settings() settings.set(settings.STRICT_TOLERANCE, True) @@ -656,8 +671,40 @@ class Drawing(blenderbim.core.tool.Drawing): camera.angle = fov - psets = ifcopenshell.util.element.get_psets(drawing) - pset = psets.get("EPset_Drawing") + tool.Loader.link_mesh(shape, camera) + + obj = bpy.data.objects.new(tool.Loader.get_name(drawing), camera) + cls.import_camera_props(drawing, obj) + tool.Ifc.link(drawing, obj) + + m = shape.transformation.matrix.data + mat = mathutils.Matrix( + ([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]) + ) + obj.matrix_world = mat + + if cls.get_drawing_target_view(drawing) == "REFLECTED_PLAN_VIEW": + obj.matrix_world[1][1] *= -1 + + tool.Geometry.record_object_position(obj) + tool.Collector.assign(obj) + + return obj + + @classmethod + def import_camera_props(cls, drawing, obj): + # Temporarily clear the definition id to prevent prop update callbacks to IFC. + ifc_definition_id = obj.BIMObjectProperties.ifc_definition_id + obj.BIMObjectProperties.ifc_definition_id = 0 + + camera = obj.data + camera.BIMCameraProperties.has_underlay = False + camera.BIMCameraProperties.has_linework = True + camera.BIMCameraProperties.has_annotation = True + camera.BIMCameraProperties.target_view = "PLAN_VIEW" + camera.BIMCameraProperties.is_nts = False + + pset = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing") if pset: if "TargetView" in pset: camera.BIMCameraProperties.target_view = pset["TargetView"] @@ -684,24 +731,7 @@ class Drawing(blenderbim.core.tool.Drawing): if "IsNTS" in pset: camera.BIMCameraProperties.is_nts = bool(pset["IsNTS"]) - tool.Loader.link_mesh(shape, camera) - - obj = bpy.data.objects.new(tool.Loader.get_name(drawing), camera) - tool.Ifc.link(drawing, obj) - - m = shape.transformation.matrix.data - mat = mathutils.Matrix( - ([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]) - ) - obj.matrix_world = mat - - if cls.get_drawing_target_view(drawing) == "REFLECTED_PLAN_VIEW": - obj.matrix_world[1][1] *= -1 - - tool.Geometry.record_object_position(obj) - tool.Collector.assign(obj) - - return obj + obj.BIMObjectProperties.ifc_definition_id = ifc_definition_id @classmethod def import_drawings(cls): @@ -733,10 +763,10 @@ class Drawing(blenderbim.core.tool.Drawing): for drawing in sorted(drawings, key=lambda x: x.Name or "Unnamed"): new = props.drawings.add() - new.ifc_definition_id = drawing.id() new.name = drawing.Name or "Unnamed" new.is_selected = current_drawings_selection.get(drawing.id(), True) new.is_drawing = True + new.ifc_definition_id = drawing.id() # Last, to prevent unnecessary prop callbacks @classmethod def import_documents(cls, document_type): @@ -862,7 +892,7 @@ class Drawing(blenderbim.core.tool.Drawing): should_add_representation=True, context=None, ifc_representation_class=None, - ): + ) -> Union[ifcopenshell.entity_instance, None]: return blenderbim.core.root.assign_class( tool.Ifc, tool.Collector, @@ -1048,7 +1078,9 @@ class Drawing(blenderbim.core.tool.Drawing): shutil.copy(ootb_resource, resource_path) @classmethod - def get_potential_reference_elements(cls, drawing): + def get_potential_reference_elements( + cls, drawing: ifcopenshell.entity_instance + ) -> list[ifcopenshell.entity_instance]: elements = [] existing_references = cls.get_group_elements(cls.get_drawing_group(drawing)) for element in tool.Ifc.get().by_type("IfcAnnotation"): @@ -1069,7 +1101,9 @@ class Drawing(blenderbim.core.tool.Drawing): return elements @classmethod - def get_drawing_reference_annotation(cls, drawing, reference_element): + def get_drawing_reference_annotation( + cls, drawing: ifcopenshell.entity_instance, reference_element: ifcopenshell.entity_instance + ) -> Union[ifcopenshell.entity_instance, bool, None]: if drawing == reference_element: return True for element in cls.get_group_elements(cls.get_drawing_group(drawing)): @@ -1085,26 +1119,49 @@ class Drawing(blenderbim.core.tool.Drawing): return element @classmethod - def generate_reference_annotation(cls, drawing, reference_element, context): + def generate_reference_annotation( + cls, + drawing: ifcopenshell.entity_instance, + reference_element: ifcopenshell.entity_instance, + context: ifcopenshell.entity_instance, + ) -> 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": - psets = ifcopenshell.util.element.get_psets(reference_element) - target_view = psets.get("EPset_Drawing", {}).get("TargetView", None) + + def ensure_referenced_drawing_obj_exists(drawing: ifcopenshell.entity_instance): + obj = tool.Ifc.get_object(drawing) + if obj is None: + obj = cls.import_drawing(drawing) + # there is no need for other drawing annotations in that case + # but we import them anyway so we won't break that drawing activation + cls.import_annotations_in_group(cls.get_drawing_group(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_annotation(cls, drawing, reference_element, context): + def generate_storey_annotation( + cls, + drawing: ifcopenshell.entity_instance, + reference_element: ifcopenshell.entity_instance, + context: ifcopenshell.entity_instance, + ) -> ifcopenshell.entity_instance: camera = tool.Ifc.get_object(drawing) bounds = helper.ortho_view_frame(camera.data) if camera.data.type == "ORTHO" else None reference_obj = tool.Ifc.get_object(reference_element) - def to_camera_coords(camera, reference_obj): + 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 @@ -1159,13 +1216,18 @@ class Drawing(blenderbim.core.tool.Drawing): return element @classmethod - def generate_section_reference_annotation(cls, drawing, reference_element, context): + def generate_section_reference_annotation( + cls, + drawing: ifcopenshell.entity_instance, + reference_element: 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) bounds = helper.ortho_view_frame(camera.data) if camera.data.type == "ORTHO" else None - def to_camera_coords(camera, reference_obj): + 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 @@ -1176,7 +1238,9 @@ class Drawing(blenderbim.core.tool.Drawing): mat.translation += annotation_offset return mat - def clip_to_camera_boundary(mesh, bounds): + 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) @@ -1237,12 +1301,17 @@ class Drawing(blenderbim.core.tool.Drawing): return element @classmethod - def generate_elevation_reference_annotation(cls, drawing, reference_element, context): + def generate_elevation_reference_annotation( + cls, + drawing: ifcopenshell.entity_instance, + reference_element: 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, reference_obj): + 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 @@ -1269,7 +1338,12 @@ class Drawing(blenderbim.core.tool.Drawing): return element @classmethod - def generate_grid_axis_reference_annotation(cls, drawing, reference_element, context): + def generate_grid_axis_reference_annotation( + cls, + drawing: ifcopenshell.entity_instance, + reference_element: ifcopenshell.entity_instance, + context: ifcopenshell.entity_instance, + ) -> ifcopenshell.entity_instance: target_view = tool.Drawing.get_drawing_target_view(drawing) camera = tool.Ifc.get_object(drawing) @@ -1297,7 +1371,7 @@ class Drawing(blenderbim.core.tool.Drawing): mesh.to_mesh(obj.data) return obj - def to_camera_coords(obj, mesh): + def to_camera_coords(obj: bpy.types.Object, mesh: bpy.types.Mesh) -> tuple[bpy.types.Object, bpy.types.Mesh]: 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)) @@ -1354,7 +1428,7 @@ class Drawing(blenderbim.core.tool.Drawing): return element @classmethod - def is_perpendicular(cls, a, b): + 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))] a_quaternion = a.matrix_world.to_quaternion() b_quaternion = b.matrix_world.to_quaternion() @@ -1364,7 +1438,7 @@ class Drawing(blenderbim.core.tool.Drawing): return False @classmethod - def get_camera_block(cls, obj): + def get_camera_block(cls, obj: bpy.types.Object) -> dict: raster_x = obj.data.BIMCameraProperties.raster_x raster_y = obj.data.BIMCameraProperties.raster_y @@ -1397,7 +1471,7 @@ class Drawing(blenderbim.core.tool.Drawing): return {"verts": verts, "faces": faces} @classmethod - def is_intersecting(cls, a, b): + def is_intersecting(cls, a: bpy.types.Object, b: bpy.types.Object) -> bool: a_block = cls.get_camera_block(a) a_tree = mathutils.bvhtree.BVHTree.FromPolygons(a_block["verts"], a_block["faces"]) b_block = cls.get_camera_block(b) @@ -1405,7 +1479,7 @@ class Drawing(blenderbim.core.tool.Drawing): return bool(a_tree.overlap(b_tree)) @classmethod - def replace_text_literal_variables(cls, text, product): + def replace_text_literal_variables(cls, text: str, product: Optional[ifcopenshell.entity_instance] = None) -> str: if not product: return text @@ -1502,19 +1576,27 @@ class Drawing(blenderbim.core.tool.Drawing): @classmethod def get_drawing_metadata(cls, drawing): + # fmt: off return [ v.strip() - for v in (ifcopenshell.util.element.get_psets(drawing)["EPset_Drawing"].get("Metadata", "") or "").split(",") + for v in ( + ifcopenshell.util.element.get_psets(drawing)["EPset_Drawing"].get("Metadata", "") or "" + ).split(",") if v ] + # fmt: on @classmethod def get_annotation_z_index(cls, drawing): return ifcopenshell.util.element.get_pset(drawing, "EPset_Annotation", "ZIndex") or 0 @classmethod - def get_annotation_symbol(cls, drawing): - return ifcopenshell.util.element.get_pset(drawing, "EPset_Annotation", "Symbol") + def get_annotation_symbol(cls, element: ifcopenshell.entity_instance) -> Union[str, None]: + symbol = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Symbol") + if not symbol: + # EPset_AnnotationSurveyArea is not standard! See bSI-4.3 proposal #660. + symbol = ifcopenshell.util.element.get_pset(element, "EPset_AnnotationSurveyArea", "PointType") + return symbol @classmethod def has_linework(cls, drawing): @@ -1525,7 +1607,7 @@ class Drawing(blenderbim.core.tool.Drawing): return ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {}).get("HasAnnotation", False) @classmethod - def get_drawing_elements(cls, drawing): + def get_drawing_elements(cls, drawing: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]: """returns a set of elements that are included in the drawing""" ifc_file = tool.Ifc.get() pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {}) @@ -1539,6 +1621,10 @@ class Drawing(blenderbim.core.tool.Drawing): else: base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement")) elements = {e for e in (elements & base_elements) if e.is_a() != "IfcSpace"} + + # exclude annotations to avoid including annotations from other drawings + elements = {i for i in elements if not i.is_a("IfcAnnotation")} + # add annotations from the current drawing annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing)) elements.update(annotations) @@ -1549,15 +1635,13 @@ class Drawing(blenderbim.core.tool.Drawing): return elements @classmethod - def get_drawing_spaces(cls, drawing): + def get_drawing_spaces(cls, drawing: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]: ifc_file = tool.Ifc.get() pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {}) - include = pset.get("Include", None) elements = cls.get_elements_in_camera_view( tool.Ifc.get_object(drawing), [tool.Ifc.get_object(e) for e in ifc_file.by_type("IfcSpace")] ) - if include: - elements = ifcopenshell.util.selector.filter_elements(ifc_file, include, elements=elements) + # NOTE: EPset_Drawing.Include is not used to avoid adding other elements besides spaces exclude = pset.get("Exclude", None) if exclude: elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude) @@ -1609,15 +1693,15 @@ class Drawing(blenderbim.core.tool.Drawing): bpy.ops.bim.activate_model() @classmethod - def activate_drawing(cls, camera): + def activate_drawing(cls, camera: bpy.types.Object) -> None: area = tool.Blender.get_view3d_area() is_local_view = area.spaces[0].local_view is not None if is_local_view: - #turn off local view before activating drawing, and then turn it on again. + # turn off local view before activating drawing, and then turn it on again. for a in bpy.context.screen.areas: - if a.type == 'VIEW_3D': + if a.type == "VIEW_3D": override = bpy.context.copy() - override['area'] = a + override["area"] = a bpy.ops.view3d.localview(override) bpy.context.scene.camera = camera bpy.ops.view3d.localview(override) @@ -1648,15 +1732,8 @@ class Drawing(blenderbim.core.tool.Drawing): filtered_elements = cls.get_drawing_elements(drawing) | cls.get_drawing_spaces(drawing) filtered_elements.add(drawing) - for view_layer_object in bpy.context.view_layer.objects: - element = tool.Ifc.get_entity(view_layer_object) - if not element or element.is_a("IfcTypeProduct"): - continue - hide = element not in filtered_elements - view_layer_object.hide_set(hide) - view_layer_object.hide_render = hide - subcontexts = [] + subcontexts: list[ifcopenshell.entity_instance] = [] target_view = cls.get_drawing_target_view(drawing) if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW"): @@ -1689,6 +1766,13 @@ class Drawing(blenderbim.core.tool.Drawing): if subcontext: subcontexts.append(context_filter) + # Hide everything first, then selectively show. This is significantly faster. + with bpy.context.temp_override(area=next(a for a in bpy.context.screen.areas if a.type == "VIEW_3D")): + bpy.ops.object.hide_view_set(unselected=False) + bpy.ops.object.hide_view_set(unselected=True) + + # switch representations and hide elements without representations + element_obj_names = set() for element in filtered_elements: obj = tool.Ifc.get_object(element) current_representation = tool.Geometry.get_active_representation(obj) @@ -1717,12 +1801,23 @@ class Drawing(blenderbim.core.tool.Drawing): has_context = True break - if not has_context: - obj.hide_set(True) - obj.hide_render = True + # Don't hide IfcAnnotations as some of them might exist without representations + if has_context or element.is_a("IfcAnnotation"): + element_obj_names.add(obj.name) + + # Note that render visibility is only set on drawing generation time for speed. + [ + obj.hide_set(False) # Show the object + for obj in bpy.context.view_layer.objects + if obj.name in element_obj_names or not tool.Ifc.get_entity(obj) + ] + + cls.import_camera_props(drawing, camera) @classmethod - def get_elements_in_camera_view(cls, camera, objs): + def get_elements_in_camera_view( + cls, camera: bpy.types.Object, objs: list[ifcopenshell.entity_instance] + ) -> set[ifcopenshell.entity_instance]: props = camera.data.BIMCameraProperties x = props.width y = props.height @@ -1922,3 +2017,60 @@ class Drawing(blenderbim.core.tool.Drawing): rotate180z = mathutils.Matrix.Rotation(math.radians(180.0), 4, "Z") return mathutils.Matrix.Translation(location) @ rotation.to_matrix().to_4x4() @ rotate180z return mathutils.Matrix.Translation(location) @ rotation.to_matrix().to_4x4() + + @classmethod + def convert_svg_to_dxf(cls, svg_filepath: Path, dxf_filepath: Path) -> None: + import ezdxf + import xml.etree.ElementTree as ET + + SVG = "{http://www.w3.org/2000/svg}" + IFC = "{http://www.ifcopenshell.org/ns}" + + doc = ezdxf.new("R2010") + msp = doc.modelspace() + svg = ET.parse(svg_filepath).getroot() + + def finalize_dxf(): + doc.saveas(dxf_filepath) + + drawing = svg.findall(f"{SVG}g[@{IFC}name]") + if not drawing: + finalize_dxf() + return + drawing = drawing[0] + + NUMBER = r"-?\d+\.?\d+" + COORD = rf"{NUMBER},{NUMBER}" + POLYLINE_PATTERN = rf"M{COORD} (?:L{COORD} ?)+Z? ?" + MULTI_POLYLINE_PATTERN = rf"^({POLYLINE_PATTERN})+$" + + for element_g in drawing.findall(f"{SVG}g"): + paths = element_g.findall(f"{SVG}path") + + for path in paths: + path = path.attrib["d"] + + if not re.match(MULTI_POLYLINE_PATTERN, path): + # print(f'Path "{path}" doesn\'t match expected pattern {MULTI_POLYLINE_PATTERN}') + continue + + for polyline_path in re.findall(POLYLINE_PATTERN, path): + points = re.findall(rf"{NUMBER}", polyline_path) + points = [float(p) for p in points] + POINT_SIZE = 2 + + grouped_points = [] + for i in range(0, len(points), POINT_SIZE): + point = points[i : i + POINT_SIZE] + point[1] *= -1 + grouped_points.append(point) + points = grouped_points + + # Z marks closed polylines + is_closed_polyline = polyline_path.rstrip().endswith("Z") + if is_closed_polyline or len(points) > 2: + msp.add_lwpolyline(points, close=is_closed_polyline) + else: # LINE + msp.add_line(*points) + + finalize_dxf() diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py index 77af31d01d..0a3ff34fdf 100644 --- a/src/blenderbim/blenderbim/tool/geometry.py +++ b/src/blenderbim/blenderbim/tool/geometry.py @@ -26,12 +26,13 @@ import ifcopenshell import blenderbim.core.tool import blenderbim.core.style import blenderbim.core.spatial +import blenderbim.core.geometry import blenderbim.tool as tool import blenderbim.bim.import_ifc from math import radians, pi from mathutils import Vector, Matrix from blenderbim.bim.ifc import IfcStore -from typing import List +from typing import Union class Geometry(blenderbim.core.tool.Geometry): @@ -90,6 +91,17 @@ class Geometry(blenderbim.core.tool.Geometry): if element.is_a("IfcRelSpaceBoundary"): ifcopenshell.api.run("boundary.remove_boundary", tool.Ifc.get(), boundary=element) return bpy.data.objects.remove(obj) + if 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.run("grid.remove_grid_axis", tool.Ifc.get(), axis=element) + return bpy.data.objects.remove(obj) collection = obj.BIMObjectProperties.collection if collection: @@ -314,7 +326,9 @@ class Geometry(blenderbim.core.tool.Geometry): return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") @classmethod - def get_subcontext_parameters(cls, subcontext): + def get_subcontext_parameters( + cls, subcontext: ifcopenshell.entity_instance + ) -> tuple[Union[str, None], Union[str, None], Union[str, None]]: return ( subcontext.ContextType, subcontext.ContextIdentifier, @@ -723,6 +737,9 @@ class Geometry(blenderbim.core.tool.Geometry): @classmethod def reload_representation(cls, obj): + """reload `obj` active representation""" + if not obj.data: + return representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) blenderbim.core.geometry.switch_representation( tool.Ifc, @@ -883,7 +900,7 @@ class Geometry(blenderbim.core.tool.Geometry): return inverse @classmethod - def get_shape_aspect_styles(cls, element, shape_aspect, representation_item) -> List[ifcopenshell.entity_instance]: + def get_shape_aspect_styles(cls, element, shape_aspect, representation_item) -> list[ifcopenshell.entity_instance]: """update `representation_item` style based on styles connected to the `shape_aspect` through material constituents with the same name """ diff --git a/src/blenderbim/blenderbim/tool/ifc.py b/src/blenderbim/blenderbim/tool/ifc.py index d17c819894..f961d03a02 100644 --- a/src/blenderbim/blenderbim/tool/ifc.py +++ b/src/blenderbim/blenderbim/tool/ifc.py @@ -20,45 +20,47 @@ import os import bpy import numpy as np import ifcopenshell.api +import ifcopenshell.util.element import blenderbim.core.tool import blenderbim.bim.handler import blenderbim.tool as tool -from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.ifc import IfcStore, IFC_CONNECTED_TYPE +from typing import Optional, Union, Any class Ifc(blenderbim.core.tool.Ifc): @classmethod - def run(cls, command, **kwargs): + def run(cls, command: str, **kwargs) -> Any: return ifcopenshell.api.run(command, IfcStore.get_file(), **kwargs) @classmethod - def set(cls, ifc): + def set(cls, ifc: ifcopenshell.file) -> None: IfcStore.file = ifc @classmethod - def get(cls): + def get(cls) -> ifcopenshell.file: return IfcStore.get_file() @classmethod - def get_path(cls): + def get_path(cls) -> str: return IfcStore.path @classmethod - def get_schema(cls): + def get_schema(cls) -> str: if IfcStore.get_file(): return IfcStore.get_file().schema @classmethod - def has_changed_shading(cls, obj): + def has_changed_shading(cls, obj: bpy.types.Material) -> bool: checksum = obj.BIMMaterialProperties.shading_checksum return checksum != repr(np.array(obj.diffuse_color).tobytes()) @classmethod - def is_edited(cls, obj): + def is_edited(cls, obj: bpy.types.Object) -> bool: return list(obj.scale) != [1.0, 1.0, 1.0] or obj in IfcStore.edited_objs @classmethod - def is_moved(cls, obj): + def is_moved(cls, obj: bpy.types.Object) -> bool: element = cls.get_entity(obj) if not element or element.is_a("IfcTypeProduct") or element.is_a("IfcProject"): return False @@ -77,7 +79,7 @@ class Ifc(blenderbim.core.tool.Ifc): return IfcStore.get_schema() @classmethod - def get_entity(cls, obj): + def get_entity(cls, obj: IFC_CONNECTED_TYPE) -> ifcopenshell.entity_instance: ifc = IfcStore.get_file() props = getattr(obj, "BIMObjectProperties", None) if ifc and props and props.ifc_definition_id: @@ -87,7 +89,7 @@ class Ifc(blenderbim.core.tool.Ifc): pass @classmethod - def get_entity_by_id(cls, entity_id): + def get_entity_by_id(cls, entity_id) -> Union[ifcopenshell.entity_instance, None]: """useful to check whether entity_id is still exists in IFC""" ifc_file = tool.Ifc.get() try: @@ -96,11 +98,11 @@ class Ifc(blenderbim.core.tool.Ifc): return None @classmethod - def get_object(cls, element): + def get_object(cls, element: ifcopenshell.entity_instance) -> IFC_CONNECTED_TYPE: return IfcStore.get_element(element.id()) @classmethod - def rebuild_element_maps(cls): + def rebuild_element_maps(cls) -> None: """Rebuilds the id_map and guid_map When any Blender object is stored outside a Blender PointerProperty, @@ -153,15 +155,15 @@ class Ifc(blenderbim.core.tool.Ifc): blenderbim.bim.handler.subscribe_to(obj, "diffuse_color", blenderbim.bim.handler.color_callback) @classmethod - def link(cls, element, obj): + def link(cls, element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None: IfcStore.link_element(element, obj) @classmethod - def edit(cls, obj): + def edit(cls, obj: bpy.types.Object) -> None: IfcStore.edited_objs.add(obj) @classmethod - def finish_edit(cls, obj): + def finish_edit(cls, obj: bpy.types.Object) -> None: try: IfcStore.edited_objs.remove(obj) except: @@ -186,11 +188,13 @@ class Ifc(blenderbim.core.tool.Ifc): return os.path.relpath(uri, ifc_path).replace("\\", "/") @classmethod - def unlink(cls, element=None, obj=None): + def unlink( + cls, element: Optional[ifcopenshell.entity_instance] = None, obj: Optional[IFC_CONNECTED_TYPE] = None + ) -> None: IfcStore.unlink_element(element, obj) @classmethod - def get_all_element_occurrences(cls, element): + def get_all_element_occurrences(cls, element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: if element.is_a("IfcElementType"): element_type = element occurrences = ifcopenshell.util.element.get_types(element_type) diff --git a/src/blenderbim/blenderbim/tool/loader.py b/src/blenderbim/blenderbim/tool/loader.py index 32952b461c..aac640d4da 100644 --- a/src/blenderbim/blenderbim/tool/loader.py +++ b/src/blenderbim/blenderbim/tool/loader.py @@ -26,6 +26,7 @@ import os import numpy as np from mathutils import Vector from pathlib import Path +from typing import Union # Progressively we'll refactor loading elements into Blender objects into this @@ -34,9 +35,12 @@ from pathlib import Path # supplementary objects (e.g. drawings, structural analysis models, etc). +OBJECT_DATA_TYPE = Union[bpy.types.Mesh, bpy.types.Curve] + + class Loader(blenderbim.core.tool.Loader): @classmethod - def create_project_collection(self, name): + def create_project_collection(cls, name: str) -> bpy.types.Collection: project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) project_collection = project_obj.BIMObjectProperties.collection for collection in project_collection.children: @@ -51,7 +55,7 @@ class Loader(blenderbim.core.tool.Loader): return collection @classmethod - def get_mesh_name(cls, geometry): + def get_mesh_name(cls, geometry) -> str: representation_id = geometry.id if "-" in representation_id: representation_id = int(re.sub(r"\D", "", representation_id.split("-")[0])) @@ -62,11 +66,11 @@ class Loader(blenderbim.core.tool.Loader): return "{}/{}".format(context_id, representation_id) @classmethod - def get_name(cls, element): + def get_name(cls, element: ifcopenshell.entity_instance) -> str: return "{}/{}".format(element.is_a(), getattr(element, "Name", "None")) @classmethod - def link_mesh(cls, shape, mesh): + def link_mesh(cls, shape, mesh: OBJECT_DATA_TYPE) -> None: geometry = shape.geometry if hasattr(shape, "geometry") else shape if "-" in geometry.id: mesh.BIMMeshProperties.ifc_definition_id = int(geometry.id.split("-")[0]) diff --git a/src/blenderbim/blenderbim/tool/material.py b/src/blenderbim/blenderbim/tool/material.py index 70c64eeb83..8ebb745fa0 100644 --- a/src/blenderbim/blenderbim/tool/material.py +++ b/src/blenderbim/blenderbim/tool/material.py @@ -219,7 +219,7 @@ class Material(blenderbim.core.tool.Material): material = tool.Ifc.get().by_type("IfcMaterial")[0] else: blenderbim.core.material.unassign_material(tool.Ifc, tool.Material, objects=[tool.Ifc.get_object(element)]) - tool.Ifc.run("material.assign_material", product=element, type="IfcMaterialProfileSet", material=material) + tool.Ifc.run("material.assign_material", products=[element], type="IfcMaterialProfileSet", material=material) assinged_material = cls.get_material(element) material_profile = tool.Ifc.run("material.add_profile", profile_set=assinged_material, material=material) return material_profile diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index 95fe41da0c..32c2856389 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -20,6 +20,7 @@ import bpy import json import bmesh import collections +import collections.abc import numpy as np import ifcopenshell import ifcopenshell.util.unit @@ -36,23 +37,26 @@ from blenderbim.bim import import_ifc from blenderbim.bim.module.geometry.helper import Helper from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData, WindowData, DoorData from ifcopenshell.util.shape_builder import V, ShapeBuilder +from typing import Optional, Union, TypeVar, Any + +T = TypeVar("T") class Model(blenderbim.core.tool.Model): @classmethod - def convert_si_to_unit(cls, value): + def convert_si_to_unit(cls, value: T) -> T: if isinstance(value, (tuple, list)): return [v / cls.unit_scale for v in value] return value / cls.unit_scale @classmethod - def convert_unit_to_si(cls, value): + def convert_unit_to_si(cls, value: T) -> T: if isinstance(value, (tuple, list)): return [v * cls.unit_scale for v in value] return value * cls.unit_scale @classmethod - def convert_data_to_project_units(cls, data, non_si_props=[]): + def convert_data_to_project_units(cls, data: dict[str, Any], non_si_props: list[str] = []) -> dict[str, Any]: si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) for prop_name in data: if prop_name in non_si_props: @@ -65,7 +69,7 @@ class Model(blenderbim.core.tool.Model): return data @classmethod - def convert_data_to_si_units(cls, data, non_si_props=[]): + def convert_data_to_si_units(cls, data: dict[str, Any], non_si_props: list[str] = []) -> dict[str, Any]: si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) for prop_name in data: if prop_name in non_si_props: @@ -78,34 +82,35 @@ class Model(blenderbim.core.tool.Model): return data @classmethod - def export_curve(cls, position, edge_indices, points=None): + def export_curve(cls, position: Matrix, edge_indices: list[tuple[int, int]]) -> ifcopenshell.entity_instance: position_i = position.inverted() + ifc_file = tool.Ifc.get() if len(edge_indices) == 2: diameter = edge_indices[0] p1 = cls.bm.verts[diameter[0]].co p2 = cls.bm.verts[diameter[1]].co center = cls.convert_si_to_unit(list(position_i @ p1.lerp(p2, 0.5))) radius = cls.convert_si_to_unit((p1 - p2).length / 2) - return tool.Ifc.get().createIfcCircle( - tool.Ifc.get().createIfcAxis2Placement2D(tool.Ifc.get().createIfcCartesianPoint(center[0:2])), radius + return ifc_file.createIfcCircle( + ifc_file.createIfcAxis2Placement2D(ifc_file.createIfcCartesianPoint(center[0:2])), radius ) - if tool.Ifc.get().schema == "IFC2X3": + if ifc_file.schema == "IFC2X3": points = [] for edge in edge_indices: local_point = (position_i @ Vector(cls.bm.verts[edge[0]].co)).to_2d() - points.append(tool.Ifc.get().createIfcCartesianPoint(cls.convert_si_to_unit(local_point))) + points.append(ifc_file.createIfcCartesianPoint(cls.convert_si_to_unit(local_point))) points.append(points[0]) - return tool.Ifc.get().createIfcPolyline(points) + return ifc_file.createIfcPolyline(points) segments = [] for segment in edge_indices: if len(segment) == 2: - segments.append(tool.Ifc.get().createIfcLineIndex([i + 1 for i in segment])) + segments.append(ifc_file.createIfcLineIndex([i + 1 for i in segment])) elif len(segment) == 3: - segments.append(tool.Ifc.get().createIfcArcIndex([i + 1 for i in segment])) - return tool.Ifc.get().createIfcIndexedPolyCurve(cls.points, segments, False) + segments.append(ifc_file.createIfcArcIndex([i + 1 for i in segment])) + return ifc_file.createIfcIndexedPolyCurve(cls.points, segments, False) @classmethod - def export_points(cls, position, indices): + def export_points(cls, position: Matrix, indices: list[Vector]) -> ifcopenshell.entity_instance: position_i = position.inverted() points = [] for point in indices: @@ -114,7 +119,10 @@ class Model(blenderbim.core.tool.Model): return tool.Ifc.get().createIfcCartesianPointList2D(points) @classmethod - def export_profile(cls, obj, position=None): + def export_profile( + cls, obj: bpy.types.Object, position: Optional[Matrix] = None + ) -> Union[ifcopenshell.entity_instance, None]: + """Returns `None` in case if profile was invalid.""" if position is None: position = Matrix() @@ -150,7 +158,7 @@ class Model(blenderbim.core.tool.Model): return profile @classmethod - def export_surface(cls, obj): + def export_surface(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]: p1, p2, p3 = [v.co.copy() for v in obj.data.vertices[0:3]] edge1 = p2 - p1 @@ -202,7 +210,7 @@ class Model(blenderbim.core.tool.Model): return surface @classmethod - def generate_occurrence_name(cls, element_type: ifcopenshell.entity_instance, ifc_class: str): + def generate_occurrence_name(cls, element_type: ifcopenshell.entity_instance, ifc_class: str) -> str: props = bpy.context.scene.BIMModelProperties if props.occurrence_name_style == "CLASS": return ifc_class[3:] @@ -216,7 +224,8 @@ class Model(blenderbim.core.tool.Model): return "Instance" @classmethod - def get_extrusion(cls, representation): + def get_extrusion(cls, representation: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: + """return first found IfcExtrudedAreaSolid""" item = representation.Items[0] while True: if item.is_a("IfcExtrudedAreaSolid"): diff --git a/src/blenderbim/blenderbim/tool/pset.py b/src/blenderbim/blenderbim/tool/pset.py index ed7d383bad..0b4db9e9cb 100644 --- a/src/blenderbim/blenderbim/tool/pset.py +++ b/src/blenderbim/blenderbim/tool/pset.py @@ -18,14 +18,18 @@ import bpy import ifcopenshell +import ifcopenshell.util.element import blenderbim.core.tool import blenderbim.tool as tool import blenderbim.bim.schema +from typing import Union class Pset(blenderbim.core.tool.Pset): @classmethod - def get_element_pset(cls, element, pset_name): + def get_element_pset( + cls, element: ifcopenshell.entity_instance, pset_name: str + ) -> Union[ifcopenshell.entity_instance, None]: pset = ifcopenshell.util.element.get_pset(element, pset_name) if pset: return tool.Ifc.get().by_id(pset["id"]) @@ -50,13 +54,14 @@ class Pset(blenderbim.core.tool.Pset): return bpy.context.scene.WorkSchedulePsetProperties elif obj_type == "Group": return bpy.context.scene.GroupPsetProperties + @classmethod def get_pset_name(cls, obj, obj_type): pset = cls.get_pset_props(obj, obj_type) return pset.pset_name @classmethod - def is_pset_applicable(cls, element, pset_name): + def is_pset_applicable(cls, element: ifcopenshell.entity_instance, pset_name: str) -> bool: return bool( pset_name in blenderbim.bim.schema.ifc.psetqto.get_applicable_names( diff --git a/src/blenderbim/blenderbim/tool/qto.py b/src/blenderbim/blenderbim/tool/qto.py index c0fa802212..b96b638907 100644 --- a/src/blenderbim/blenderbim/tool/qto.py +++ b/src/blenderbim/blenderbim/tool/qto.py @@ -23,14 +23,17 @@ import blenderbim.tool as tool import ifcopenshell from mathutils import Vector from ifcopenshell import util -from blenderbim.bim.module.pset.qto_calculator import QtoCalculator +import ifcopenshell.util.unit +import ifcopenshell.util.element +from blenderbim.bim.module.pset.qto_calculator import QtoCalculator, QuanityTypes from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper import blenderbim.bim.schema +from typing import Optional, Union, Literal class Qto(blenderbim.core.tool.Qto): @classmethod - def get_radius_of_selected_vertices(cls, obj): + def get_radius_of_selected_vertices(cls, obj: bpy.types.Object) -> float: selected_verts = [v.co for v in obj.data.vertices if v.select] total = Vector() for v in selected_verts: @@ -39,16 +42,16 @@ class Qto(blenderbim.core.tool.Qto): return max([(v - circle_center).length for v in selected_verts]) @classmethod - def set_qto_result(cls, result): + def set_qto_result(cls, result: float) -> None: bpy.context.scene.BIMQtoProperties.qto_result = str(round(result, 3)) @classmethod - def add_object_base_qto(cls, obj): + def add_object_base_qto(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]: product = tool.Ifc.get_entity(obj) return cls.add_product_base_qto(product) @classmethod - def add_product_base_qto(cls, product): + def add_product_base_qto(cls, product: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: base_quantity_name = cls.get_applicable_base_quantity_name(product) if base_quantity_name: return tool.Ifc.run( @@ -58,7 +61,7 @@ class Qto(blenderbim.core.tool.Qto): ) @classmethod - def get_applicable_quantity_names(cls, qto_name): + def get_applicable_quantity_names(cls, qto_name: str) -> list[str]: pset_template = blenderbim.bim.schema.ifc.psetqto.get_by_name(qto_name) return ( [property.Name for property in pset_template.HasPropertyTemplates] @@ -67,7 +70,9 @@ class Qto(blenderbim.core.tool.Qto): ) @classmethod - def get_applicable_base_quantity_name(cls, product=None): + def get_applicable_base_quantity_name( + cls, product: Optional[ifcopenshell.entity_instance] = None + ) -> Union[str, None]: if not product: return applicable_qto_names = blenderbim.bim.schema.ifc.psetqto.get_applicable_names( @@ -76,36 +81,44 @@ class Qto(blenderbim.core.tool.Qto): return next((qto_name for qto_name in applicable_qto_names if "Qto_" in qto_name and "Base" in qto_name), None) @classmethod - def get_new_calculated_quantity(cls, qto_name, quantity_name, obj): + def get_new_calculated_quantity(cls, qto_name: str, quantity_name: str, obj: bpy.types.Object) -> float: return QtoCalculator().calculate_quantity(qto_name, quantity_name, obj) @classmethod - def get_new_guessed_quantity(cls, obj, quantity_name, alternative_prop_names): + def get_new_guessed_quantity( + cls, obj: bpy.types.Object, quantity_name: str, alternative_prop_names: list[str] + ) -> Union[float, None]: return QtoCalculator().guess_quantity(quantity_name, alternative_prop_names, obj) @classmethod - def get_rounded_value(cls, new_quantity): + def get_rounded_value(cls, new_quantity: float) -> float: return round(new_quantity, 3) @classmethod - def get_calculated_object_quantities(cls, calculator, qto_name, obj): + def get_calculated_object_quantities( + cls, calculator: QtoCalculator, qto_name: str, obj: bpy.types.Object + ) -> dict[str, float]: return { - quantity_name: cls.get_rounded_value(calculator.calculate_quantity(qto_name, quantity_name, obj)) + quantity_name: cls.get_rounded_value(value) for quantity_name in cls.get_applicable_quantity_names(qto_name) or [] if cls.has_calculator(qto_name, quantity_name) - and calculator.calculate_quantity(qto_name, quantity_name, obj) is not None + and (value := calculator.calculate_quantity(qto_name, quantity_name, obj)) is not None } @classmethod - def has_calculator(cls, qto_name, quantity_name): + def has_calculator(cls, qto_name: str, quantity_name: str) -> bool: return bool(mapper.get(qto_name, {}).get(quantity_name, None)) @classmethod - def convert_to_project_units(cls, value, qto_name=None, quantity_name=None, quantity_type=None): + def convert_to_project_units( + cls, + value: float, + qto_name: Optional[str] = None, + quantity_name: Optional[str] = None, + quantity_type: Optional[QuanityTypes] = None, + ) -> Union[float, None]: """You can either specify `quantity_type` or provide `qto_name/quantity_name` to let method figure the `quantity_type` from the templates - - `quantity_type` values are `Q_LENGTH`, `Q_AREA`, `Q_VOLUME` """ ifc_file = tool.Ifc.get() quantity_to_unit_types = { @@ -135,7 +148,9 @@ class Qto(blenderbim.core.tool.Qto): return value @classmethod - def get_guessed_quantities(cls, obj, pset_qto_properties): + def get_guessed_quantities( + cls, obj: bpy.types.Object, pset_qto_properties: list[ifcopenshell.entity_instance] + ) -> dict[str, float]: calculated_quantities = {} for pset_qto_property in pset_qto_properties: quantity_name = pset_qto_property.get_info()["Name"] @@ -153,7 +168,7 @@ class Qto(blenderbim.core.tool.Qto): return calculated_quantities @classmethod - def get_base_qto(cls, product): + def get_base_qto(cls, product: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: if not hasattr(product, "IsDefinedBy"): return for rel in product.IsDefinedBy or []: @@ -166,7 +181,7 @@ class Qto(blenderbim.core.tool.Qto): return rel.RelatingPropertyDefinition @classmethod - def get_related_cost_item_quantities(cls, product): + def get_related_cost_item_quantities(cls, product: ifcopenshell.entity_instance) -> list[dict]: """_summary_: Returns the related cost item and related quantities of the product :param ifc-instance product: ifc instance diff --git a/src/blenderbim/blenderbim/tool/root.py b/src/blenderbim/blenderbim/tool/root.py index 6f5b0d95c1..8a9273eb10 100644 --- a/src/blenderbim/blenderbim/tool/root.py +++ b/src/blenderbim/blenderbim/tool/root.py @@ -18,9 +18,14 @@ import bpy import ifcopenshell +import ifcopenshell.api import ifcopenshell.util.representation +import ifcopenshell.util.element import blenderbim.core.tool +import blenderbim.core.aggregate import blenderbim.core.geometry +import blenderbim.core.material +import blenderbim.core.style import blenderbim.tool as tool from mathutils import Vector from blenderbim.bim.module.model.opening import FilledOpeningGenerator @@ -97,15 +102,18 @@ class Root(blenderbim.core.tool.Root): if not element: continue if hasattr(element, "ConnectedTo") and element.ConnectedTo: - paths = [connection for connection in element.ConnectedTo if connection.is_a("IfcRelConnectsPathElements")] + paths = [ + connection for connection in element.ConnectedTo if connection.is_a("IfcRelConnectsPathElements") + ] for path in paths: - relationships[element] = {"type": "path", - "related_connection_type": path.RelatedConnectionType, - "related_element": path.RelatedElement, - "related_priorities": path.RelatedPriorities, - "relating_connection_type": path.RelatingConnectionType, - "relating_element": path.RelatingElement, - "relating_priorities": path.RelatingPriorities, + relationships[element] = { + "type": "path", + "related_connection_type": path.RelatedConnectionType, + "related_element": path.RelatedElement, + "related_priorities": path.RelatedPriorities, + "relating_connection_type": path.RelatingConnectionType, + "relating_element": path.RelatingElement, + "relating_priorities": path.RelatingPriorities, } return relationships @@ -234,7 +242,6 @@ class Root(blenderbim.core.tool.Root): related_connection=data["related_connection_type"], ) - @classmethod def recreate_aggregate(cls, old_to_new): for old, new in old_to_new.items(): @@ -244,35 +251,34 @@ class Root(blenderbim.core.tool.Root): new_aggregate = old_to_new[old_aggregate] except: blenderbim.core.aggregate.unassign_object( - tool.Ifc, - tool.Aggregate, - tool.Collector, - relating_obj=tool.Ifc.get_object(old_aggregate), - related_obj=tool.Ifc.get_object(new[0]), - ) + tool.Ifc, + tool.Aggregate, + tool.Collector, + relating_obj=tool.Ifc.get_object(old_aggregate), + related_obj=tool.Ifc.get_object(new[0]), + ) continue - + blenderbim.core.aggregate.assign_object( - tool.Ifc, - tool.Aggregate, - tool.Collector, - relating_obj=tool.Ifc.get_object(new_aggregate[0]), - related_obj=tool.Ifc.get_object(new[0]), - ) - + tool.Ifc, + tool.Aggregate, + tool.Collector, + relating_obj=tool.Ifc.get_object(new_aggregate[0]), + related_obj=tool.Ifc.get_object(new[0]), + ) + # Make sure that the array children also get reassigned to the correct aggregate pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Array") if pset: array_children = tool.Blender.Modifier.Array.get_all_children_objects(new[0]) for obj in array_children: blenderbim.core.aggregate.assign_object( - tool.Ifc, - tool.Aggregate, - tool.Collector, - relating_obj=tool.Ifc.get_object(new_aggregate[0]), - related_obj=tool.Ifc.get_object(tool.Ifc.get_entity(obj)), - ) - + tool.Ifc, + tool.Aggregate, + tool.Collector, + relating_obj=tool.Ifc.get_object(new_aggregate[0]), + related_obj=tool.Ifc.get_object(tool.Ifc.get_entity(obj)), + ) @classmethod def run_geometry_add_representation( diff --git a/src/blenderbim/blenderbim/tool/search.py b/src/blenderbim/blenderbim/tool/search.py index 834b99304c..cbad2ac02e 100644 --- a/src/blenderbim/blenderbim/tool/search.py +++ b/src/blenderbim/blenderbim/tool/search.py @@ -5,32 +5,41 @@ import blenderbim.core.tool import blenderbim.tool as tool import ifcopenshell.util.selector from ifcopenshell.util.selector import Selector +from blenderbim.bim.prop import BIMFacet +from typing import Union, Literal class Search(blenderbim.core.tool.Search): @classmethod - def get_group_query(cls, group): + def get_group_query(cls, group: ifcopenshell.entity_instance) -> str: return json.loads(group.Description)["query"] @classmethod - def get_filter_groups(cls, module): + def get_filter_groups(cls, module: str) -> bpy.types.bpy_prop_collection: if module == "search": return bpy.context.scene.BIMSearchProperties.filter_groups elif module == "csv": return bpy.context.scene.CsvProperties.filter_groups + elif module == "diff": + return bpy.context.scene.DiffProperties.filter_groups elif module == "drawing_include": return bpy.context.active_object.data.BIMCameraProperties.include_filter_groups elif module == "drawing_exclude": return bpy.context.active_object.data.BIMCameraProperties.exclude_filter_groups + elif module.startswith("clash"): + _, clash_set_index, ab, clash_source_index = module.split("_") + return getattr(bpy.context.scene.BIMClashProperties.clash_sets[int(clash_set_index)], ab)[ + int(clash_source_index) + ].filter_groups @classmethod - def import_filter_query(cls, query, filter_groups): + def import_filter_query(cls, query: str, filter_groups: bpy.types.bpy_prop_collection) -> None: filter_groups.clear() transformer = ImportFilterQueryTransformer(filter_groups) transformer.transform(ifcopenshell.util.selector.filter_elements_grammar.parse(query)) @classmethod - def export_filter_query(cls, filter_groups): + def export_filter_query(cls, filter_groups: bpy.types.bpy_prop_collection) -> str: query = [] for filter_group in filter_groups: filter_group_query = [] @@ -87,13 +96,15 @@ class Search(blenderbim.core.tool.Search): return " + ".join(query) @classmethod - def get_comparison_and_value(cls, ifc_filter): + def get_comparison_and_value( + cls, ifc_filter: BIMFacet + ) -> Union[tuple[Literal["!="], str], tuple[Literal["="], str]]: if ifc_filter.value.startswith("!="): return ("!=", cls.wrap_value(ifc_filter, ifc_filter.value[2:].strip())) return ("=", cls.wrap_value(ifc_filter, ifc_filter.value.strip())) @classmethod - def wrap_value(cls, ifc_filter, value): + def wrap_value(cls, ifc_filter: BIMFacet, value: str) -> str: if value.startswith("/") and value.endswith("/"): return value elif value in ("NULL", "TRUE", "FALSE"): @@ -101,7 +112,7 @@ class Search(blenderbim.core.tool.Search): return '"' + value.replace('"', '\\"') + '"' @classmethod - def from_selector_query(cls, query): + def from_selector_query(cls, query: str) -> list[ifcopenshell.entity_instance]: """Returns a list of products from a selector query""" return Selector().parse(tool.Ifc.get(), query) @@ -143,7 +154,7 @@ class ImportFilterQueryTransformer(lark.Transformer): if "pset" in arg: new2.pset = arg["pset"] if "comparison" in arg: - new2.comparison = arg["comparison"] + new2.comparison = arg["comparison"] or "=" def facet(self, args): return args[0] @@ -198,7 +209,7 @@ class ImportFilterQueryTransformer(lark.Transformer): return ( is_not + { - "equals": "=", + "equals": "=" if is_not else "", # Blank because it's the default situation "morethanequalto": ">=", "lessthanequalto": "<=", "morethan": ">", diff --git a/src/blenderbim/blenderbim/tool/spatial.py b/src/blenderbim/blenderbim/tool/spatial.py index 033b712384..48d2b54274 100644 --- a/src/blenderbim/blenderbim/tool/spatial.py +++ b/src/blenderbim/blenderbim/tool/spatial.py @@ -20,6 +20,7 @@ import bpy import bmesh import shapely import ifcopenshell +import ifcopenshell.util.element import blenderbim.core.type import blenderbim.core.tool import blenderbim.core.root @@ -34,7 +35,7 @@ from shapely import Polygon, MultiPolygon class Spatial(blenderbim.core.tool.Spatial): @classmethod - def can_contain(cls, structure_obj, element_obj): + def can_contain(cls, structure_obj: bpy.types.Object, element_obj: bpy.types.Object) -> bool: structure = tool.Ifc.get_entity(structure_obj) element = tool.Ifc.get_entity(element_obj) if not structure or not element: @@ -52,7 +53,7 @@ class Spatial(blenderbim.core.tool.Spatial): return True @classmethod - def can_reference(cls, structure, element): + def can_reference(cls, structure: ifcopenshell.entity_instance, element: ifcopenshell.entity_instance) -> bool: if not structure or not element: return False if tool.Ifc.get_schema() == "IFC2X3": @@ -150,9 +151,9 @@ class Spatial(blenderbim.core.tool.Spatial): for product in products: obj = tool.Ifc.get_object(product) if obj and bpy.context.view_layer.objects.get(obj.name): - obj.select_set(True) if unhide: obj.hide_set(False) + obj.select_set(True) @classmethod def filter_products(cls, products, action): @@ -689,7 +690,7 @@ class Spatial(blenderbim.core.tool.Spatial): instance_class = ifcopenshell.util.type.get_applicable_entities(ifc_class, tool.Ifc.get().schema)[0] bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class) element = tool.Ifc.get_entity(obj) - tool.Ifc.run("type.assign_type", related_object=element, relating_type=relating_type) + tool.Ifc.run("type.assign_type", related_objects=[element], relating_type=relating_type) @classmethod def assign_relating_type_to_element(cls, ifc, Type, element, relating_type): diff --git a/src/blenderbim/blenderbim/tool/style.py b/src/blenderbim/blenderbim/tool/style.py index ee7f0b021b..30628cb1ef 100644 --- a/src/blenderbim/blenderbim/tool/style.py +++ b/src/blenderbim/blenderbim/tool/style.py @@ -23,6 +23,7 @@ import blenderbim.core.tool import blenderbim.tool as tool import blenderbim.bim.helper from mathutils import Color +from typing import Union # fmt: off TEXTURE_MAPS_BY_METHODS = { @@ -95,7 +96,7 @@ class Style(blenderbim.core.tool.Style): return obj.name @classmethod - def get_style(cls, obj): + def get_style(cls, obj: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]: if obj.BIMMaterialProperties.ifc_style_id: try: return tool.Ifc.get().by_id(obj.BIMMaterialProperties.ifc_style_id) @@ -103,7 +104,9 @@ class Style(blenderbim.core.tool.Style): return @classmethod - def get_style_elements(cls, blender_material_or_style): + def get_style_elements( + cls, blender_material_or_style: Union[bpy.types.Material, ifcopenshell.entity_instance] + ) -> dict[str, ifcopenshell.entity_instance]: if isinstance(blender_material_or_style, bpy.types.Material): if not blender_material_or_style.BIMMaterialProperties.ifc_style_id: return {} @@ -454,7 +457,7 @@ class Style(blenderbim.core.tool.Style): return results @classmethod - def get_style_ui_props_attributes(self, style_type): + def get_style_ui_props_attributes(cls, style_type): props = bpy.context.scene.BIMStylesProperties if style_type == "IfcExternallyDefinedSurfaceStyle": return props.external_style_attributes @@ -499,7 +502,7 @@ class Style(blenderbim.core.tool.Style): blenderbim.bim.helper.import_attributes2(style, attributes) @classmethod - def has_blender_external_style(cls, style_elements): + def has_blender_external_style(cls, style_elements: dict[str, ifcopenshell.entity_instance]) -> bool: external_style = style_elements.get("IfcExternallyDefinedSurfaceStyle", None) return bool(external_style and external_style.Location and external_style.Location.endswith(".blend")) diff --git a/src/blenderbim/docs/_static/custom.css b/src/blenderbim/docs/_static/custom.css index ecd7ac3d44..03f5b9016f 100644 --- a/src/blenderbim/docs/_static/custom.css +++ b/src/blenderbim/docs/_static/custom.css @@ -1,12 +1,44 @@ +@import url("https://fonts.googleapis.com/css2?family=Nunito:wght@200;300;400;500;600;700;800&display=swap"); +@import url('https://fonts.googleapis.com/css2?family=Inconsolata:wght@200;300;400;500;600;700&display=swap'); +h1, h2, h3, h4 { + font-weight: normal; + color: #7cbf33; + background: -webkit-linear-gradient(0deg, #7cbf33, #049344); + background-clip: border-box; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} +a { + text-decoration: none; +} +.toc-tree .reference:hover { + color: #d98014; +} +.sidebar-brand-text { + font-size: 1rem; +} .blockbutton { - border: 1px solid var(--color-admonition-title--seealso); - background-color: var(--color-admonition-title-background--seealso); - padding: 5px; - text-align: center; - border-radius: 5px; max-width: 500px; margin-left: auto; margin-right: auto; + text-align: center; +} +.blockbutton a { + display: block; + padding-top: 15px; + padding-bottom: 15px; + border-radius: 5px; + background: #40b74c; + width: 100%; + height: 100%; + color: #fff; + text-decoration: none; +} +.blockbutton a:hover { + background: #70ba35; +} +.highlight .hll { + background-color: #ffc2; } section img { display: block; diff --git a/src/blenderbim/docs/conf.py b/src/blenderbim/docs/conf.py index a589be30ca..69afbca790 100644 --- a/src/blenderbim/docs/conf.py +++ b/src/blenderbim/docs/conf.py @@ -36,11 +36,11 @@ # -- Project information ----------------------------------------------------- project = "BlenderBIM Add-on" -copyright = "2020-2023 IfcOpenShell Contributors" +copyright = "2020-2024 IfcOpenShell Contributors" author = "IfcOpenShell Contributors" # The full version, including alpha/beta/rc tags -release = "0.0.231104" +release = "0.0.240402" # -- General configuration --------------------------------------------------- @@ -48,7 +48,7 @@ release = "0.0.231104" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ["sphinx.ext.autodoc", "sphinx.ext.autosectionlabel"] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.autosectionlabel", "sphinx_copybutton"] # Auto add document prefixes to help guarantee uniqueness of automatic section references. autosectionlabel_prefix_document = True @@ -75,3 +75,59 @@ html_theme = "furo" html_static_path = ["_static"] html_css_files = ["custom.css"] + +# Code block styles. Dark styling helps important code examples "pop" on the +# page even on light themes. +pygments_style = "one-dark" +pygments_dark_style = "one-dark" + +html_logo = "https://blenderbim.org/assets/images/blender/blender-logo.png" +html_theme_options = { + "source_repository": "https://github.com/IfcOpenShell/IfcOpenShell/", + "source_branch": "v0.7.0", + "source_directory": "src/blenderbim/docs/", + "light_css_variables": { + "color-brand-primary": "#39b54a", + "color-brand-content": "#39b54a", + "color-brand-visited": "#d9e021", + "color-background-primary": "#f7f7f6", + "color-background-secondary": "#eeeeec", + "color-background-border": "#cfd0cb", + "color-foreground-primary": "#2e3436", + "color-sidebar-item-background--hover": "#f7f7f6", + "color-link--hover": "#d98014", + "font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji" + }, + "dark_css_variables": { + "color-brand-primary": "#39b54a", + "color-brand-content": "#39b54a", + "color-brand-visited": "#d9e021", + "color-background-primary": "#2e3436", + "color-background-border": "#2e3436", + "color-foreground-primary": "#eeeeec", + "color-sidebar-item-background--hover": "#2e3436", + "color-link--hover": "#d98014", + "font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji" + }, + + "footer_icons": [ + { + "name": "IfcOpenShell", + "url": "https://ifcopenshell.org", + "html": """ + + """, + "class": "", + }, + { + "name": "GitHub", + "url": "https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.7.0/src/blenderbim/docs", + "html": """ + + + + """, + "class": "", + }, + ], +} diff --git a/src/blenderbim/docs/devs/code_style.rst b/src/blenderbim/docs/devs/code_style.rst new file mode 100644 index 0000000000..2ee2b5f204 --- /dev/null +++ b/src/blenderbim/docs/devs/code_style.rst @@ -0,0 +1,20 @@ +Code style +============ + + +Black code formatter +------------------------------- +For Python code formatting, we use `Black code formatter `__ with ``--line-length 120``. + +``black`` can be installed using ``pip install black`` and files can be formatted with the following example command: + +.. code-block:: bash + + black --line-length 120 src/blenderbim/blenderbim/bim/module/qto/operator.py + +Using PowerShell, you can run the Black formatter on the last commit in the repository. +You can change ``~1`` to ``~n`` to affect ``n`` commits. + +.. code-block:: powershell + + git diff HEAD HEAD~1 --name-only | where {$_ -like "*.py"} | foreach-object { start $_ && black --line-length 120 $_ } diff --git a/src/blenderbim/docs/devs/installation.rst b/src/blenderbim/docs/devs/installation.rst index a7b47335ab..a9b5135e66 100644 --- a/src/blenderbim/docs/devs/installation.rst +++ b/src/blenderbim/docs/devs/installation.rst @@ -22,8 +22,8 @@ You will need to choose which build to download. - If you are on Blender >=4.1, choose py311 - If you are on Blender >=3.1 and <=4.0, choose py10 - If you are on Blender >=2.93 and <3.1, choose py39 -- Choose ``linux``, ``macos``, ``macosm1`` (for Apple M1 devices), or ``win`` - depending on your operating system +- Choose ``linux``, ``macos`` (Apple Intel), ``macosm1`` (Apple Silicon), or + ``win`` depending on your operating system Sometimes, a build may be delayed, or contain broken code. We try to avoid this, but it happens. @@ -43,11 +43,11 @@ compile IfcOpenShell is out of scope of this document. You can create your own package by using the Makefile as shown below. You can choose between a ``PLATFORM`` of ``linux``, ``macos``, ``macosm1``, and ``win``. -You can choose between a ``PYVERSION`` of ``py39``, or ``py310``. +You can choose between a ``PYVERSION`` of ``py311``, ``py310``, or ``py39``. :: $ cd src/blenderbim - $ make dist PLATFORM=linux PYVERSION=py310 + $ make dist PLATFORM=linux PYVERSION=py311 $ ls dist/ This will give you a fully packaged Blender add-on zip that you can distribute @@ -258,10 +258,12 @@ Required Python modules to be stored in ``libs/site/packages/`` are: ifcclash bimtester ifccobie - ifcdiff ifccsv + ifcdiff + ifc4d + ifc5d ifcpatch - ifcp6 + ifctester pystache svgwrite dateutil @@ -276,17 +278,13 @@ Required Python modules to be stored in ``libs/site/packages/`` are: elementpath six lark-parser - hppfcl behave parse parse_type xlsxwriter odfpy defusedxml - boto3 - botocore jmespath - s3transfer ifcjson Notes: diff --git a/src/blenderbim/docs/devs/system_support.rst b/src/blenderbim/docs/devs/system_support.rst index 3bd7bcb807..9d8519e7b3 100644 --- a/src/blenderbim/docs/devs/system_support.rst +++ b/src/blenderbim/docs/devs/system_support.rst @@ -6,9 +6,13 @@ the Python version shipped by the Blender Foundation for the most recent three major Blender versions: - 64-bit Linux on Python 3.10 -- 64-bit MacOS on Python 3.10 -- 64-bit MacOS M1 on Python 3.10 +- 64-bit Linux on Python 3.11 +- 64-bit MacOS Intel on Python 3.10 +- 64-bit MacOS Intel on Python 3.11 +- 64-bit MacOS Silicon on Python 3.10 +- 64-bit MacOS Silicon on Python 3.11 - 64-bit Windows on Python 3.10 +- 64-bit Windows on Python 3.11 Note that developer builds may exist for older versions of Python but there will be no guarantee of the uptime or stability of these builds. diff --git a/src/blenderbim/docs/devs/writing_docs.rst b/src/blenderbim/docs/devs/writing_docs.rst index b40bccf4a3..f888b398da 100644 --- a/src/blenderbim/docs/devs/writing_docs.rst +++ b/src/blenderbim/docs/devs/writing_docs.rst @@ -12,6 +12,8 @@ Writing technical documentation All documentation is written in ReStructured Text and is available in the `BlenderBIM Add-on docs directory `_. +You can press the edit button on the top right on any documentation page to +quickly edit their content. You can link to `external websites `_. @@ -86,3 +88,27 @@ Tables can be very annoying to format. You can use a CSV table instead. "ABC", "01", "02" "DEF", "03", "04" + +Building documentation +---------------------- + +If you want to build the documentation locally, the documentation system uses +`Sphinx `_. First, install the theme and +theme dependencies: + +.. code-block:: console + + $ pip install furo + $ pip install sphinx-autoapi + $ pip install sphinx-copybutton + +Now you can generate the documentation: + +.. code-block:: console + + $ cd /path/to/ifcopenshell/src/blenderbim/docs/ + $ make html + $ cd _build/html + $ python -m http.server + +You will now have a local webserver running hosting the documentation. diff --git a/src/blenderbim/docs/index.rst b/src/blenderbim/docs/index.rst index 662b914e17..b8f21e5a6b 100644 --- a/src/blenderbim/docs/index.rst +++ b/src/blenderbim/docs/index.rst @@ -40,6 +40,7 @@ Learn how to model a small building and create simple architectural plans, secti devs/installation devs/hello_world + devs/code_style devs/running_tests devs/writing_docs devs/translations diff --git a/src/blenderbim/docs/users/creating_an_ifc_model.rst b/src/blenderbim/docs/users/creating_an_ifc_model.rst index 623956d9b6..f9cf3c6031 100644 --- a/src/blenderbim/docs/users/creating_an_ifc_model.rst +++ b/src/blenderbim/docs/users/creating_an_ifc_model.rst @@ -70,8 +70,8 @@ In the top left **File** menu, Save your new IFC model on your computer. Congratulations! You have now created your first OpenBIM model from Blender! You can open the IFC file in any other program, and you will see something similar to the image below. Three simple open source online viewers you can test with -are `IfcPipeline `__, `IFC.JS -`__, and `3DViewer +are `IfcPipeline `__, `ThatOpenEditor +`__, and `3DViewer `__. .. image:: images/ifc-pipeline.png diff --git a/src/blenderbim/docs/users/installation.rst b/src/blenderbim/docs/users/installation.rst index fd23a05832..c429c89012 100644 --- a/src/blenderbim/docs/users/installation.rst +++ b/src/blenderbim/docs/users/installation.rst @@ -25,7 +25,7 @@ Installation .. warning:: - If you are not using Blender version >=3.1, please follow the **Unstable installation** instructions. :doc:`Read more <../devs/installation>` + If you are not using Blender version >=4.1, please follow the **Unstable installation** instructions. :doc:`Read more <../devs/installation>` 3. **Install the BlenderBIM Add-on** @@ -130,29 +130,7 @@ directory in your Blender add-ons directory. FAQ --- -1. **I get an error similar to "ImportError: IfcOpenShell not built for 'linux/64bit/python3.7'"** - - Check which BlenderBIM Add-on build you are using. The zip will have either - ``py39`` or ``py310`` in the name. If you are using a Mac, also make sure - you are using the M1 version if you have a newer Mac. See the instructions - in the :ref:`devs/installation:unstable installation` section to check that - you have installed the correct version. - -2. **I am on Ubuntu and get an error similar to "ImportError: - /lib/x86_64-linux-gnu/libm.so.6: version GLIBC_2.29 not found"** - - Our latest package which uses IfcOpenShell v0.7.0 is built using Ubuntu 20 LTS. - If you have an older Ubuntu version, you can either upgrade to 19.10 or above, - or you'll need to compile IfcOpenShell yourself. - -3. **I get an error saying "ModuleNotFoundError: No module named 'numpy'"**" - - If you have installed Blender from another source instead of from - `Blender.org `__, such as from your - distro's package repositories, then you may be missing some modules like - ``numpy``. Try installing it manually like ``apt install python-numpy``. - -4. **Some other error prevents me from installing or doing basic functions with +1. **Some other error prevents me from installing or doing basic functions with the add-on. Is it specific to my environment?** Sometimes it is helpful to try installing and using the BlenderBIM Add-on on @@ -167,6 +145,31 @@ FAQ If this fixes your issue, consider disabling other add-ons one by one until you find a conflict as a next step to isolating the issue. +2. **I get an error similar to "ImportError: IfcOpenShell not built for 'linux/64bit/python3.10'"** + + If you are using a Mac, be sure to use the Mac Silicon version if you have a + newer Mac. The only exception is if you have installed Blender using Steam + on a Mac, in which case you need to use the Mac Intel download. + + For all other scenarios, check the BlenderBIM Add-on zip file which you + downloaded. The zip will have either ``py39``, ``py310``, or ``py311`` in + the name. See the instructions in the :ref:`devs/installation:unstable + installation` section to check that you have installed the correct version. + +3. **I am on Ubuntu and get an error similar to "ImportError: + /lib/x86_64-linux-gnu/libm.so.6: version GLIBC_2.29 not found"** + + Our latest package which uses IfcOpenShell v0.7.0 is built using Ubuntu 20 LTS. + If you have an older Ubuntu version, you can either upgrade to 19.10 or above, + or you'll need to compile IfcOpenShell yourself. + +4. **I get an error saying "ModuleNotFoundError: No module named 'numpy'"**" + + If you have installed Blender from another source instead of from + `Blender.org `__, such as from your + distro's package repositories, then you may be missing some modules like + ``numpy``. Try installing it manually like ``apt install python-numpy``. + 5. **I get an error similar to RuntimeError: Instance #1234 not found** Blender saves and loads projects to a ``.blend`` file. However. the diff --git a/src/blenderbim/docs/users/other_addons.rst b/src/blenderbim/docs/users/other_addons.rst index 88d4c4ff7a..7ae799a6fb 100644 --- a/src/blenderbim/docs/users/other_addons.rst +++ b/src/blenderbim/docs/users/other_addons.rst @@ -29,7 +29,9 @@ Some of these add-ons are not shipped with Blender: - `Sverchok `__ - Sverchok is a visual programming add-on for Blender that allows you to generate parametric geometry, create scripts for non-programmers, model solids from FreeCAD, and - much more. + much more. There is also + `IfcSverchok `__ + that adds IFC features to Sverchok. - `BlenderGIS `__ - BlenderGIS lets you import GIS data, grab elevation data from the web, and generate TINs from survey points and contours. diff --git a/src/blenderbim/scripts/entourage-library.blend b/src/blenderbim/scripts/entourage-library.blend new file mode 100644 index 0000000000..f23774dea8 Binary files /dev/null and b/src/blenderbim/scripts/entourage-library.blend differ diff --git a/src/blenderbim/scripts/generate_au_library.py b/src/blenderbim/scripts/generate_au_library.py index e1895fb44b..e75cf739b0 100644 --- a/src/blenderbim/scripts/generate_au_library.py +++ b/src/blenderbim/scripts/generate_au_library.py @@ -188,7 +188,7 @@ class LibraryGenerator: def create_layer_set_type(self, name, data): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=data["type"], name=name) element.Description = data["Description"] - rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialLayerSet") + rel = ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialLayerSet") layer_set = rel.RelatingMaterial for layer_data in data["Layers"]: layer = ifcopenshell.api.run( @@ -201,7 +201,7 @@ class LibraryGenerator: def create_layer_type(self, ifc_class, name, thickness): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name) - rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialLayerSet") + rel = ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialLayerSet") layer_set = rel.RelatingMaterial layer = ifcopenshell.api.run("material.add_layer", self.file, layer_set=layer_set, material=self.materials["TBD"]["ifc"]) layer.LayerThickness = thickness @@ -210,7 +210,7 @@ class LibraryGenerator: def create_profile_type(self, ifc_class, name, profile): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name) - rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialProfileSet") + rel = ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialProfileSet") profile_set = rel.RelatingMaterial material_profile = ifcopenshell.api.run( "material.add_profile", self.file, profile_set=profile_set, material=self.materials["TBD"]["ifc"] diff --git a/src/blenderbim/scripts/generate_demo_library.py b/src/blenderbim/scripts/generate_demo_library.py index 26ab3ad278..14f97bc52d 100644 --- a/src/blenderbim/scripts/generate_demo_library.py +++ b/src/blenderbim/scripts/generate_demo_library.py @@ -205,7 +205,7 @@ class LibraryGenerator: def create_layer_type(self, ifc_class, name, thickness): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name) - rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialLayerSet") + rel = ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialLayerSet") layer_set = rel.RelatingMaterial layer = ifcopenshell.api.run("material.add_layer", self.file, layer_set=layer_set, material=self.material) layer.LayerThickness = thickness @@ -214,7 +214,7 @@ class LibraryGenerator: def create_profile_type(self, ifc_class, name, profile): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name) - rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialProfileSet") + rel = ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialProfileSet") profile_set = rel.RelatingMaterial material_profile = ifcopenshell.api.run( "material.add_profile", self.file, profile_set=profile_set, material=self.material diff --git a/src/blenderbim/scripts/generate_entourage_library.py b/src/blenderbim/scripts/generate_entourage_library.py new file mode 100644 index 0000000000..a84c73e4ef --- /dev/null +++ b/src/blenderbim/scripts/generate_entourage_library.py @@ -0,0 +1,138 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2024 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import os +import bpy +import ifcopenshell +import ifcopenshell.api +import blenderbim.tool as tool +from pathlib import Path + + +# When run from Blender +BLEND_DIR = os.path.dirname(bpy.data.filepath) +OUT_PATH = os.path.join(BLEND_DIR, "..", "blenderbim", "bim", "data", "libraries", "IFC4 Entourage Library.ifc") + + +class LibraryGenerator: + def generate(self, library_name, output_filename): + ifcopenshell.api.pre_listeners = {} + ifcopenshell.api.post_listeners = {} + + self.materials = {} + + self.file = ifcopenshell.api.run("project.create_file") + self.project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject", name=library_name) + self.library = ifcopenshell.api.run( + "root.create_entity", self.file, ifc_class="IfcProjectLibrary", name=library_name + ) + ifcopenshell.api.run( + "project.assign_declaration", self.file, definition=self.library, relating_context=self.project + ) + unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", prefix="MILLI") + ifcopenshell.api.run("unit.assign_unit", self.file, units=[unit]) + + model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model") + plan = ifcopenshell.api.run("context.add_context", self.file, context_type="Plan") + self.representations = { + "Model/Body/MODEL_VIEW": ifcopenshell.api.run( + "context.add_context", + self.file, + context_type="Model", + context_identifier="Body", + target_view="MODEL_VIEW", + parent=model, + ), + "Plan/Body/PLAN_VIEW": ifcopenshell.api.run( + "context.add_context", + self.file, + context_type="Plan", + context_identifier="Body", + target_view="PLAN_VIEW", + parent=plan, + ), + "Model/Body/PLAN_VIEW": ifcopenshell.api.run( + "context.add_context", + self.file, + context_type="Model", + context_identifier="Body", + target_view="PLAN_VIEW", + parent=model, + ), + "Model/Body/SECTION_VIEW": ifcopenshell.api.run( + "context.add_context", + self.file, + context_type="Model", + context_identifier="Body", + target_view="SECTION_VIEW", + parent=model, + ), + } + + # Manually modeled trees + for obj in bpy.data.objects: + if not obj.type == "MESH": + continue + if "Plan/" in obj.name or "Model/" in obj.name: + continue + representations = {"Model/Body/MODEL_VIEW": obj.name} + for rep_key in self.representations.keys(): + rep_obj = bpy.data.objects.get(obj.name + " " + rep_key) + if rep_obj: + representations[rep_key] = rep_obj.name + self.create_type("IfcBuildingElementProxyType", obj.name, representations) + + self.file.write(output_filename) + + def create_type(self, ifc_class, name, representations): + element = ifcopenshell.api.run( + "root.create_entity", self.file, ifc_class=ifc_class, predefined_type="ENTOURAGE", name=name + ) + for rep_name, obj_name in representations.items(): + obj = bpy.data.objects.get(obj_name) + representation = ifcopenshell.api.run( + "geometry.add_representation", + self.file, + context=self.representations[rep_name], + blender_object=obj, + geometry=obj.data, + total_items=max(1, len(obj.material_slots)), + ) + styles = [] + for slot in obj.material_slots: + style = ifcopenshell.api.run("style.add_style", self.file, name=slot.material.name) + ifcopenshell.api.run( + "style.add_surface_style", + self.file, + style=style, + ifc_class="IfcSurfaceStyleShading", + attributes=tool.Style.get_surface_shading_attributes(slot.material), + ) + styles.append(style) + if styles: + ifcopenshell.api.run( + "style.assign_representation_styles", self.file, shape_representation=representation, styles=styles + ) + ifcopenshell.api.run( + "geometry.assign_representation", self.file, product=element, representation=representation + ) + ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library) + + +if __name__ == "__main__": + LibraryGenerator().generate("Entourage Assets Library", output_filename=OUT_PATH) diff --git a/src/blenderbim/scripts/generate_furniture_library.py b/src/blenderbim/scripts/generate_furniture_library.py index 2837096df7..6d38170ed0 100644 --- a/src/blenderbim/scripts/generate_furniture_library.py +++ b/src/blenderbim/scripts/generate_furniture_library.py @@ -1803,7 +1803,7 @@ class LibraryGenerator: def create_layer_set_type(self, name, data): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=data["type"], name=name) element.Description = data["Description"] - rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialLayerSet") + rel = ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialLayerSet") layer_set = rel.RelatingMaterial for layer_data in data["Layers"]: layer = ifcopenshell.api.run( @@ -1816,7 +1816,7 @@ class LibraryGenerator: def create_layer_type(self, ifc_class, name, thickness): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name) - rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialLayerSet") + rel = ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialLayerSet") layer_set = rel.RelatingMaterial layer = ifcopenshell.api.run( "material.add_layer", self.file, layer_set=layer_set, material=self.materials["TBD"]["ifc"] @@ -1827,7 +1827,7 @@ class LibraryGenerator: def create_profile_type(self, ifc_class, name, profile): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name) - rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialProfileSet") + rel = ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialProfileSet") profile_set = rel.RelatingMaterial material_profile = ifcopenshell.api.run( "material.add_profile", diff --git a/src/blenderbim/scripts/generate_steel_profiles_library.py b/src/blenderbim/scripts/generate_steel_profiles_library.py index 8f7765eb5d..685435660f 100644 --- a/src/blenderbim/scripts/generate_steel_profiles_library.py +++ b/src/blenderbim/scripts/generate_steel_profiles_library.py @@ -175,7 +175,7 @@ class LibraryGenerator: def create_profile_type(self, ifc_class, name, profile): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name) - rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialProfileSet") + rel = ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialProfileSet") profile_set = rel.RelatingMaterial material_profile = ifcopenshell.api.run( "material.add_profile", self.file, profile_set=profile_set, material=self.material diff --git a/src/blenderbim/scripts/obj2ifc-meshlab.py b/src/blenderbim/scripts/obj2ifc-meshlab.py index c0130fb68a..2ecb5684fb 100644 --- a/src/blenderbim/scripts/obj2ifc-meshlab.py +++ b/src/blenderbim/scripts/obj2ifc-meshlab.py @@ -85,7 +85,9 @@ class Obj2Ifc: Name=mesh.label() or self.basename, Representation=representation, ) - ifcopenshell.api.run("spatial.assign_container", self.file, product=product, relating_structure=self.storey) + ifcopenshell.api.run( + "spatial.assign_container", self.file, products=[product], relating_structure=self.storey + ) product.ObjectPlacement = self.placement self.file.write(self.outfile) @@ -125,9 +127,9 @@ class Obj2Ifc: self.storey = ifcopenshell.api.run( "root.create_entity", self.file, ifc_class="IfcBuildingStorey", name="My Storey" ) - ifcopenshell.api.run("aggregate.assign_object", self.file, product=site, relating_object=project) - ifcopenshell.api.run("aggregate.assign_object", self.file, product=building, relating_object=site) - ifcopenshell.api.run("aggregate.assign_object", self.file, product=self.storey, relating_object=building) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[site], relating_object=project) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[building], relating_object=site) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[self.storey], relating_object=building) ifcopenshell.api.run("geometry.edit_object_placement", self.file, product=site) ifcopenshell.api.run("geometry.edit_object_placement", self.file, product=building) diff --git a/src/blenderbim/scripts/obj2ifc.py b/src/blenderbim/scripts/obj2ifc.py index e26a83898a..2999fbb6b6 100644 --- a/src/blenderbim/scripts/obj2ifc.py +++ b/src/blenderbim/scripts/obj2ifc.py @@ -79,7 +79,9 @@ class Obj2Ifc: Name=mesh.name or self.basename, Representation=representation, ) - ifcopenshell.api.run("spatial.assign_container", self.file, product=product, relating_structure=self.storey) + ifcopenshell.api.run( + "spatial.assign_container", self.file, products=[product], relating_structure=self.storey + ) product.ObjectPlacement = self.placement self.file.write(self.outfile) @@ -118,9 +120,9 @@ class Obj2Ifc: self.storey = ifcopenshell.api.run( "root.create_entity", self.file, ifc_class="IfcBuildingStorey", name="My Storey" ) - ifcopenshell.api.run("aggregate.assign_object", self.file, product=site, relating_object=project) - ifcopenshell.api.run("aggregate.assign_object", self.file, product=building, relating_object=site) - ifcopenshell.api.run("aggregate.assign_object", self.file, product=self.storey, relating_object=building) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[site], relating_object=project) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[building], relating_object=site) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[self.storey], relating_object=building) ifcopenshell.api.run("geometry.edit_object_placement", self.file, product=site) ifcopenshell.api.run("geometry.edit_object_placement", self.file, product=building) diff --git a/src/blenderbim/scripts/pkg_resources.tar.gz b/src/blenderbim/scripts/pkg_resources.tar.gz new file mode 100644 index 0000000000..e2c00cda0d Binary files /dev/null and b/src/blenderbim/scripts/pkg_resources.tar.gz differ diff --git a/src/blenderbim/test/bim/feature/geometry.feature b/src/blenderbim/test/bim/feature/geometry.feature index 465469537f..8e5a8c7214 100644 --- a/src/blenderbim/test/bim/feature/geometry.feature +++ b/src/blenderbim/test/bim/feature/geometry.feature @@ -438,7 +438,7 @@ Scenario: Override duplicate move - copying an aggregate And the object "IfcElementAssembly/Assembly.001" exists And the object "IfcElementAssembly/Assembly.001" is in the collection "IfcElementAssembly/Assembly.001" And the collection "IfcElementAssembly/Assembly.001" is in the collection "IfcBuildingStorey/My Storey" - + Scenario: Override duplicate move - copying objects with connection Given an empty IFC project And I load the demo construction library @@ -467,7 +467,7 @@ Scenario: Override duplicate move - copying objects with connection Then the object "IfcSlab/Slab.001" exists And the variable "slab_name" is "[o.name for o in bpy.context.selected_objects if o.name == 'IfcSlab/Slab.001'][0]" Then the object "{wall_name}" has a connection with "{slab_name}" - + Scenario: Override duplicate move - copying walls with mitre joint Given an empty IFC project And I load the demo construction library @@ -477,24 +477,24 @@ Scenario: Override duplicate move - copying walls with mitre joint And I press "bim.hotkey(hotkey='S_A')" And the cursor is at "0.5,0,0" And I press "bim.hotkey(hotkey='S_A')" - And the object "IfcWall/Wall" is selected - And additionally the object "IfcWall/Wall.001" is selected + And the object "IfcWall/Wall.001" is selected + And additionally the object "IfcWall/Wall" is selected When I press "bim.hotkey(hotkey='S_Y')" - Then the object "IfcWall/Wall.001" dimensions are "0.5,0.1,3" - And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0" - And the object "IfcWall/Wall" dimensions are "1.1,0.1,3" - And the object "IfcWall/Wall" bottom left corner is at "0.5,0.1,0" - And the object "IfcWall/Wall" top right corner is at "0.6,-1,3" + Then the object "IfcWall/Wall" dimensions are "0.5,0.1,3" + And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0" + And the object "IfcWall/Wall.001" dimensions are "1.1,0.1,3" + And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0.1,0" + And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3" When I deselect all objects - And the object "IfcWall/Wall" is selected - And additionally the object "IfcWall/Wall.001" is selected + And the object "IfcWall/Wall.001" is selected + And additionally the object "IfcWall/Wall" is selected When I duplicate the selected objects Then the object "IfcWall/Wall.002" exists And the variable "wall_name1" is "[o.name for o in bpy.context.selected_objects if o.name == 'IfcWall/Wall.002'][0]" Then the object "IfcWall/Wall.003" exists And the variable "wall_name2" is "[o.name for o in bpy.context.selected_objects if o.name == 'IfcWall/Wall.003'][0]" Then the object "{wall_name1}" has a connection with "{wall_name2}" - + Scenario: Override duplicate move linked - without active IFC data Given an empty Blender session And I add a cube @@ -544,14 +544,14 @@ Scenario: Override paste buffer - with active IFC data And the object "IfcWall/Cube" is selected And additionally the object "IfcBuildingStorey/My Storey" is selected When I press "view3d.copybuffer" + # IFC elements unlinked on paste for safety And I press "bim.override_paste_buffer" Then the object "IfcWall/Cube" exists And the object "IfcWall/Cube" is an "IfcWall" - And the object "IfcWall/Cube.001" exists - And the object "IfcWall/Cube.001" is an "IfcWall" - And the object "IfcWall/Cube.001" has a "Tessellation" representation of "Model/Body/MODEL_VIEW" - And the object "IfcBuildingStorey/My Storey.001" exists - And the object "IfcBuildingStorey/My Storey.001" is an "IfcBuildingStorey" + And the object "Cube.001" exists + And the object "Cube.001" is not an IFC element + And the object "My Storey.001" exists + And the object "My Storey.001" is not an IFC element Scenario: Duplicate linked aggregate Given I load the IFC test file "/test/files/linked-aggregates.ifc" @@ -559,8 +559,8 @@ Scenario: Duplicate linked aggregate When I duplicate linked aggregate the selected objects Then the object "IfcWall/Wall_01.001" exists And the object "IfcWall/Wall_02.001" exists - And the object "IfcElementAssembly/Assembly_02" exists - Then the object "IfcElementAssembly/Assembly" and "IfcElementAssembly/Assembly_02" belong to the same Linked Aggregate Group + And the object "IfcElementAssembly/Assembly_01" exists + Then the object "IfcElementAssembly/Assembly" and "IfcElementAssembly/Assembly_01" belong to the same Linked Aggregate Group Scenario: Refresh linked aggregate Given I load the IFC test file "/test/files/linked-aggregates.ifc" @@ -588,7 +588,7 @@ Scenario: Refresh linked aggregate - after deleting an object When I refresh linked aggregate the selected object Then the object "IfcWall/Wall_01" does not exist And the object "IfcWall/Wall_02" exists - + Scenario: Refresh linked aggregate - after duplicating an object Given I load the IFC test file "/test/files/linked-aggregates.ifc" And the object "IfcWall/Wall_01" is selected @@ -606,4 +606,4 @@ Scenario: Refresh linked aggregate - after duplicating an object Then the object "IfcWall/Wall_01.001" exists And the object "IfcWall/Wall_02.001" exists And the object "IfcWall/Wall_03.001" exists - + diff --git a/src/blenderbim/test/bim/feature/model.feature b/src/blenderbim/test/bim/feature/model.feature index bdaa4a45f0..bf9208b012 100644 --- a/src/blenderbim/test/bim/feature/model.feature +++ b/src/blenderbim/test/bim/feature/model.feature @@ -107,11 +107,11 @@ Scenario: Add a wall perpendicular to an existing wall And I press "bim.hotkey(hotkey='S_A')" And the cursor is at "0.5,0,0" When I press "bim.hotkey(hotkey='S_A')" - Then the object "IfcWall/Wall.001" dimensions are "1,0.1,3" - And the object "IfcWall/Wall.001" bottom left corner is at "0,0,0" - And the object "IfcWall/Wall" dimensions are "1,0.1,3" - And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0" - And the object "IfcWall/Wall" top right corner is at "0.6,-1,3" + Then the object "IfcWall/Wall" dimensions are "1,0.1,3" + And the object "IfcWall/Wall" bottom left corner is at "0,0,0" + And the object "IfcWall/Wall.001" dimensions are "1,0.1,3" + And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0" + And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3" Scenario: Extend one wall to another Given an empty IFC project @@ -122,35 +122,17 @@ Scenario: Extend one wall to another And I press "bim.hotkey(hotkey='S_A')" And the cursor is at "0.5,0,0" And I press "bim.hotkey(hotkey='S_A')" - And the object "IfcWall/Wall" is moved to "0.5,-1,0" - And the object "IfcWall/Wall" is selected - And additionally the object "IfcWall/Wall.001" is selected + And the object "IfcWall/Wall.001" is moved to "0.5,-1,0" + And the object "IfcWall/Wall.001" is selected + And additionally the object "IfcWall/Wall" is selected When I press "bim.hotkey(hotkey='S_E')" - Then the object "IfcWall/Wall.001" dimensions are "1,0.1,3" - And the object "IfcWall/Wall.001" bottom left corner is at "0,0,0" - And the object "IfcWall/Wall" dimensions are "2,0.1,3" - And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0" - And the object "IfcWall/Wall" top right corner is at "0.6,-2,3" + Then the object "IfcWall/Wall" dimensions are "1,0.1,3" + And the object "IfcWall/Wall" bottom left corner is at "0,0,0" + And the object "IfcWall/Wall.001" dimensions are "2,0.1,3" + And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0" + And the object "IfcWall/Wall.001" top right corner is at "0.6,-2,3" Scenario: Join two walls with a butt joint - first wall has priority - Given an empty IFC project - And I load the demo construction library - And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" - And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" - And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" - And the cursor is at "0.5,0,0" - And I press "bim.hotkey(hotkey='S_A')" - And the object "IfcWall/Wall" is selected - And additionally the object "IfcWall/Wall.001" is selected - When I press "bim.hotkey(hotkey='S_T')" - Then the object "IfcWall/Wall.001" dimensions are "0.4,0.1,3" - And the object "IfcWall/Wall.001" bottom left corner is at "0.6,0,0" - And the object "IfcWall/Wall" dimensions are "1.1,0.1,3" - And the object "IfcWall/Wall" bottom left corner is at "0.5,0.1,0" - And the object "IfcWall/Wall" top right corner is at "0.6,-1,3" - -Scenario: Join two walls with a butt joint - second wall has priority Given an empty IFC project And I load the demo construction library And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" @@ -162,13 +144,13 @@ Scenario: Join two walls with a butt joint - second wall has priority And the object "IfcWall/Wall.001" is selected And additionally the object "IfcWall/Wall" is selected When I press "bim.hotkey(hotkey='S_T')" - Then the object "IfcWall/Wall.001" dimensions are "0.5,0.1,3" - And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0" - And the object "IfcWall/Wall" dimensions are "1,0.1,3" - And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0" - And the object "IfcWall/Wall" top right corner is at "0.6,-1,3" + Then the object "IfcWall/Wall" dimensions are "0.4,0.1,3" + And the object "IfcWall/Wall" bottom left corner is at "0.6,0,0" + And the object "IfcWall/Wall.001" dimensions are "1.1,0.1,3" + And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0.1,0" + And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3" -Scenario: Join two walls with a mitre joint +Scenario: Join two walls with a butt joint - second wall has priority Given an empty IFC project And I load the demo construction library And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" @@ -179,12 +161,30 @@ Scenario: Join two walls with a mitre joint And I press "bim.hotkey(hotkey='S_A')" And the object "IfcWall/Wall" is selected And additionally the object "IfcWall/Wall.001" is selected - When I press "bim.hotkey(hotkey='S_Y')" - Then the object "IfcWall/Wall.001" dimensions are "0.5,0.1,3" + When I press "bim.hotkey(hotkey='S_T')" + Then the object "IfcWall/Wall" dimensions are "0.5,0.1,3" + And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0" + And the object "IfcWall/Wall.001" dimensions are "1,0.1,3" And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0" - And the object "IfcWall/Wall" dimensions are "1.1,0.1,3" - And the object "IfcWall/Wall" bottom left corner is at "0.5,0.1,0" - And the object "IfcWall/Wall" top right corner is at "0.6,-1,3" + And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3" + +Scenario: Join two walls with a mitre joint + Given an empty IFC project + And I load the demo construction library + And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" + And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" + And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" + And I press "bim.hotkey(hotkey='S_A')" + And the cursor is at "0.5,0,0" + And I press "bim.hotkey(hotkey='S_A')" + And the object "IfcWall/Wall.001" is selected + And additionally the object "IfcWall/Wall" is selected + When I press "bim.hotkey(hotkey='S_Y')" + Then the object "IfcWall/Wall" dimensions are "0.5,0.1,3" + And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0" + And the object "IfcWall/Wall.001" dimensions are "1.1,0.1,3" + And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0.1,0" + And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3" Scenario: Change the height of a wall Given an empty IFC project diff --git a/src/blenderbim/test/bim/feature/project.feature b/src/blenderbim/test/bim/feature/project.feature index 14a8a952c3..7433bcfd30 100644 --- a/src/blenderbim/test/bim/feature/project.feature +++ b/src/blenderbim/test/bim/feature/project.feature @@ -313,49 +313,65 @@ Scenario: Unload project Scenario: Link IFC Given an empty IFC project - When I press "bim.link_ifc(filepath='{cwd}/test/files/basic.blend')" - Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.blend'].is_loaded" is "True" - And the object "IfcWall/Wall" exists - And the object "IfcSlab/Slab" exists - And the object "IfcElementAssembly/Empty" exists - And the object "IfcBeam/Beam" exists - And the object "IfcBuildingStorey/Ground Floor" exists - And the object "IfcBuildingStorey/Level 1" exists + When I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" + Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "True" + And the collection "IfcProject/basic.ifc" exists + And the object "Chunk" exists + And the object "Chunk" is placed in the collection "IfcProject/basic.ifc" Scenario: Toggle link visibility - wireframe mode Given an empty IFC project - And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.blend')" - When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.blend', mode='WIREFRAME')" - Then nothing happens + And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" + When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='WIREFRAME')" + Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_wireframe" is "True" + And the object "Chunk" should display as "WIRE" + When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='WIREFRAME')" + Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_wireframe" is "False" + And the object "Chunk" should display as "TEXTURED" + +Scenario: Toggle link selectability + Given an empty IFC project + And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" + When I press "bim.toggle_link_selectability(link='{cwd}/test/files/basic.ifc')" + Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_selectable" is "False" + And the collection "IfcProject/basic.ifc" is unselectable + When I press "bim.toggle_link_selectability(link='{cwd}/test/files/basic.ifc')" + Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_selectable" is "True" + And the collection "IfcProject/basic.ifc" is selectable Scenario: Toggle link visibility - visible mode Given an empty IFC project - And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.blend')" - When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.blend', mode='VISIBLE')" - Then nothing happens + And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" + When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='VISIBLE')" + Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_hidden" is "True" + And the collection "IfcProject/basic.ifc" exclude status is "True" + When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='VISIBLE')" + Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_hidden" is "False" + And the collection "IfcProject/basic.ifc" exclude status is "False" Scenario: Unload link Given an empty Blender session - And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.blend')" - When I press "bim.unload_link(filepath='{cwd}/test/files/basic.blend')" - Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.blend'].is_loaded" is "False" - And "scene.collection.children.get('IfcProject/My Project')" is "None" + And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" + When I press "bim.unload_link(filepath='{cwd}/test/files/basic.ifc')" + Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "False" + And the collection "IfcProject/basic.ifc" does not exist Scenario: Load link Given an empty Blender session - And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.blend')" - And I press "bim.unload_link(filepath='{cwd}/test/files/basic.blend')" - When I press "bim.load_link(filepath='{cwd}/test/files/basic.blend')" - Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.blend'].is_loaded" is "True" - And "scene.collection.children['IfcProject/My Project'].users" is "2" + And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" + And I press "bim.unload_link(filepath='{cwd}/test/files/basic.ifc')" + When I press "bim.load_link(filepath='{cwd}/test/files/basic.ifc')" + Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "True" + And the collection "IfcProject/basic.ifc" exists in viewlayer Scenario: Unlink IFC Given an empty Blender session - And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.blend')" - And I press "bim.unload_link(filepath='{cwd}/test/files/basic.blend')" - When I press "bim.unlink_ifc(filepath='{cwd}/test/files/basic.blend')" - Then "scene.BIMProjectProperties.links.get('{cwd}/test/files/basic.blend')" is "None" - And "scene.collection.children.get('IfcProject/My Project')" is "None" + And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" + And I press "bim.unload_link(filepath='{cwd}/test/files/basic.ifc')" + When I press "bim.unlink_ifc(filepath='{cwd}/test/files/basic.ifc')" + Then "scene.BIMProjectProperties.links.get('{cwd}/test/files/basic.ifc')" is "None" + And "scene.collection.children.get('IfcProject/basic.ifc')" is "None" + And the object "Chunk" does not exist Scenario: Export IFC - blank project Given an empty IFC project diff --git a/src/blenderbim/test/bim/feature/pset.feature b/src/blenderbim/test/bim/feature/pset.feature index 033c64627b..c769559e1f 100644 --- a/src/blenderbim/test/bim/feature/pset.feature +++ b/src/blenderbim/test/bim/feature/pset.feature @@ -387,10 +387,10 @@ Scenario: Edit pset length property When I set "active_object.PsetProperties.properties['NosingLength'].metadata.length_value" to "0.45" Then "active_object.PsetProperties.properties['NosingLength'].metadata.float_value" is roughly "450" - + When I press "bim.edit_pset(obj='IfcStairFlight/StairFlight', obj_type='Object')" Then nothing happens - + Scenario: Edit qset length property Given an empty IFC project And I press "mesh.add_clever_stair" @@ -401,15 +401,14 @@ Scenario: Edit qset length property # Testing Q_LENGTH type of prop Then "active_object.PsetProperties.properties['Length'].metadata.special_type" is "LENGTH" - And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2492.57495" + And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2492.57397" And "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "2.49257" When I set "active_object.PsetProperties.properties['Length'].metadata.float_value" to "350" - Then "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "350" + Then "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "0.35" When I set "active_object.PsetProperties.properties['Length'].metadata.length_value" to "0.45" Then "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "450" - + When I press "bim.edit_pset(obj='IfcStairFlight/StairFlight', obj_type='Object')" Then nothing happens - \ No newline at end of file diff --git a/src/blenderbim/test/bim/feature/root.feature b/src/blenderbim/test/bim/feature/root.feature index ea2a032a27..14a47a6666 100644 --- a/src/blenderbim/test/bim/feature/root.feature +++ b/src/blenderbim/test/bim/feature/root.feature @@ -24,7 +24,7 @@ Scenario: Unlink object And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" - And I press "bim.add_material" + And I press "bim.add_style()" When I press "bim.unlink_object(obj='IfcWall/Cube')" Then the object "Cube" is not an IFC element And the material "Material" is an IFC style diff --git a/src/blenderbim/test/bim/feature/search.feature b/src/blenderbim/test/bim/feature/search.feature index 80ec5a2c10..b82d780569 100644 --- a/src/blenderbim/test/bim/feature/search.feature +++ b/src/blenderbim/test/bim/feature/search.feature @@ -8,9 +8,9 @@ Scenario: Select all walls And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" - And I add a new item to "scene.IfcSelectorProperties.groups" - And I add a new item to "scene.IfcSelectorProperties.groups[0].queries" - And I set "scene.IfcSelectorProperties.groups[0].queries[0].selector" to "IFC Class" - And I set "scene.IfcSelectorProperties.groups[0].queries[0].active_option" to "124: IfcWall" - When I press "bim.filter_model_elements(option='select')" + And I press "bim.add_filter_group(module='search')" + And I set "scene.BIMSearchProperties.facet" to "entity" + And I press "bim.add_filter(index=0, type='entity', module='search')" + And I set "scene.BIMSearchProperties.filter_groups[0].filters[0].value" to "IfcWall" + When I press "bim.search" Then the object "IfcWall/Cube" is selected diff --git a/src/blenderbim/test/bim/feature/style.feature b/src/blenderbim/test/bim/feature/style.feature index 478b65624f..7fc685144a 100644 --- a/src/blenderbim/test/bim/feature/style.feature +++ b/src/blenderbim/test/bim/feature/style.feature @@ -18,7 +18,7 @@ Scenario: Remove style When I press "bim.remove_style(style={style})" Then nothing happens -Scenario: Add style +Scenario: Add style to current blender material Given an empty IFC project And I add a cube And I add a material @@ -33,32 +33,39 @@ Scenario: Unlink style When I press "bim.unlink_style" Then the material "Material" is not an IFC style -Scenario: Enable editing style +Scenario: Add a new style Given an empty IFC project - And I add a cube - And I add a material - And I press "bim.add_style" - When I press "bim.enable_editing_style" + And I press "bim.load_styles(style_type='IfcSurfaceStyle')" + And I press "bim.enable_adding_presentation_style" + When I press "bim.add_presentation_style" Then nothing happens -Scenario: Disable editing style +Scenario: Disable adding a new style Given an empty IFC project - And I add a cube - And I add a material - And I press "bim.add_style" - And I press "bim.enable_editing_style" - When I press "bim.disable_editing_style" + And I press "bim.load_styles(style_type='IfcSurfaceStyle')" + When I press "bim.disable_adding_presentation_style" Then nothing happens Scenario: Edit style Given an empty IFC project - And I add a cube - And I add a material - And I press "bim.add_style" - And I press "bim.enable_editing_style" + And I press "bim.load_styles(style_type='IfcSurfaceStyle')" + And I press "bim.enable_adding_presentation_style" + And I press "bim.add_presentation_style" + And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()" + And I press "bim.enable_editing_style(style={style})" When I press "bim.edit_style" Then nothing happens +Scenario: Disable editing style + Given an empty IFC project + And I press "bim.load_styles(style_type='IfcSurfaceStyle')" + And I press "bim.enable_adding_presentation_style" + And I press "bim.add_presentation_style" + And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()" + And I press "bim.enable_editing_style(style={style})" + When I press "bim.disable_editing_style" + Then nothing happens + Scenario: Load styles Given an empty IFC project And I add a cube diff --git a/src/blenderbim/test/bim/test_feature.py b/src/blenderbim/test/bim/test_feature.py index a8c6397057..07b178ddc0 100644 --- a/src/blenderbim/test/bim/test_feature.py +++ b/src/blenderbim/test/bim/test_feature.py @@ -29,11 +29,12 @@ from blenderbim.bim.module.model.data import AuthoringData from pytest_bdd import scenarios, given, when, then, parsers from mathutils import Vector from math import radians +from pathlib import Path scenarios("feature") variables = { - "cwd": os.getcwd(), + "cwd": Path.cwd().as_posix(), "ifc": "IfcStore.get_file()", "pset_ifc": "IfcStore.pset_template_file", "classification_ifc": "IfcStore.classification_file", @@ -44,15 +45,13 @@ webbrowser.open = lambda x: True def replace_variables(value): - if "{cwd}" in value and os.name == "nt": - value = value.replace("/", "\\").replace("{cwd}", os.getcwd()).replace("\\", "\\\\") for key, new_value in variables.items(): value = value.replace("{" + key + "}", str(new_value)) return value def is_x(number, x): - return abs(number - x) < 1e-6 + return abs(number - x) < 1e-5 def vectors_are_equal(v1, v2): @@ -177,7 +176,7 @@ def the_material_name_colour_is_set_to_colour(name, colour): @given("I add an array modifier") -def i_add_a_cube(): +def i_add_an_array_modifier(): bpy.ops.object.modifier_add(type="ARRAY") @@ -202,11 +201,13 @@ def i_press_operator_and_expect_error(operator, error_msg): else: exec(f"bpy.ops.{operator}()") assert False, f"Operator bpy.ops.{operator} ran without exception '{error_msg}'" - except Exception as e: + except Exception as e: actual_error_msg = str(e).strip() if str(e).strip() != error_msg: traceback.print_exc() - assert False, f"Got different exception running bpy.ops.{operator} - '{actual_error_msg}' instead of '{error_msg}'" + msg = f"Got different exception running bpy.ops.{operator} - '{actual_error_msg}' instead of '{error_msg}'" + assert False, msg + @given(parsers.parse('I press "{operator}"')) @when(parsers.parse('I press "{operator}"')) @@ -233,12 +234,14 @@ def i_evaluate_expression(expression): def i_duplicate_the_selected_objects(): bpy.ops.bim.override_object_duplicate_move() blenderbim.bim.handler.active_object_callback() - + + @when("I duplicate linked aggregate the selected objects") -def i_duplicate_the_selected_objects(): +def i_duplicate_linked_aggregate_the_selected_objects(): bpy.ops.bim.object_duplicate_move_linked_aggregate() blenderbim.bim.handler.active_object_callback() + @when("I refresh linked aggregate the selected object") def i_refresh_the_selected_objects(): bpy.ops.bim.refresh_linked_aggregate() @@ -291,12 +294,18 @@ def the_object_name_is_scaled_to_scale(name, scale): @given(parsers.parse('the object "{name}" is placed in the collection "{collection}"')) @when(parsers.parse('the object "{name}" is placed in the collection "{collection}"')) -def the_object_name_is_placed_in_the_collection_collection(name, collection): +def the_object_name_is_placed_in_the_collection_collection(name: str, collection: str) -> None: obj = the_object_name_exists(name) [c.objects.unlink(obj) for c in obj.users_collection] bpy.data.collections.get(collection).objects.link(obj) +@then(parsers.parse('the object "{name}" is placed in the collection "{collection}"')) +def then_the_object_name_is_placed_in_the_collection_collection(name: str, collection: str) -> None: + obj = the_object_name_exists(name) + assert obj in bpy.data.collections.get(collection).objects[:] + + @given(parsers.parse('additionally the object "{name}" is selected')) @when(parsers.parse('additionally the object "{name}" is selected')) def additionally_the_object_name_is_selected(name): @@ -329,7 +338,7 @@ def i_set_prop_to_value(prop, value): @given(parsers.parse('I set "{prop}" to ""')) @when(parsers.parse('I set "{prop}" to ""')) -def i_set_prop_to_value(prop): +def i_set_prop_to_empty_string(prop): try: eval(f"bpy.context.{prop}") except: @@ -363,7 +372,7 @@ def nothing_happens(): @then(parsers.parse('the object "{name}" exists')) -def the_object_name_exists(name) -> bpy.types.Object: +def the_object_name_exists(name: str) -> bpy.types.Object: obj = bpy.data.objects.get(name) if not obj: assert False, f'The object "{name}" does not exist' @@ -377,6 +386,7 @@ def the_object_name_does_not_exist(name) -> bpy.types.Object: assert True, f'The object "{name}" exists' return obj + @given(parsers.parse('the collection "{name}" exists')) @when(parsers.parse('the collection "{name}" exists')) @then(parsers.parse('the collection "{name}" exists')) @@ -387,6 +397,33 @@ def the_collection_name_exists(name) -> bpy.types.Collection: return obj +@then(parsers.parse('the collection "{name}" is selectable')) +def the_collection_name_is_selectable(name: str) -> None: + col = the_collection_name_exists(name) + assert col.hide_select == False + + +@then(parsers.parse('the collection "{name}" is unselectable')) +def the_collection_name_is_unselectable(name: str) -> None: + col = the_collection_name_exists(name) + assert col.hide_select == True + + +@then(parsers.parse('the collection "{name}" exists in viewlayer')) +def the_collection_exists_in_viewlayer(name: str) -> bpy.types.LayerCollection: + col = the_collection_name_exists(name) + layer = tool.Blender.get_layer_collection(col) + if not layer: + assert False, f'The collection "{name}" is not present in the current viewlayer' + return layer + + +@then(parsers.parse('the collection "{name}" exclude status is "{exclude}"')) +def the_collection_exclude_status_is(name: str, exclude: str) -> None: + layer = the_collection_exists_in_viewlayer(name) + assert layer.exclude == (exclude == "True") + + @then(parsers.parse('the object "{name1}" and "{name2}" are different elements')) def the_object_name1_and_name2_are_different_elements(name1, name2): ifc = an_ifc_file_exists() @@ -413,7 +450,7 @@ def the_object_name_has_a_representation_type_of_context(name, type, context): @given(parsers.parse('the object "{name}" data is a "{type}" representation of "{context}"')) @then(parsers.parse('the object "{name}" data is a "{type}" representation of "{context}"')) -def the_object_name_has_a_representation_type_of_context(name, type, context): +def the_object_name_data_is_a_type_representation_of_context(name, type, context): ifc = an_ifc_file_exists() context, subcontext, target_view = context.split("/") rep = ifc.by_id(the_object_name_exists(name).data.BIMMeshProperties.ifc_definition_id) @@ -518,7 +555,7 @@ def the_object_name_has_no_data(name): @then(parsers.parse('the object "{name}" has data which is an IFC representation')) -def the_object_name_is_not_an_ifc_element(name): +def the_object_name_has_ifc_representation_data(name): id = the_object_name_exists(name).data.BIMMeshProperties.ifc_definition_id assert id != 0, f"The ID is {id}" @@ -552,7 +589,7 @@ def the_material_name_is_not_an_ifc_style(name): @then(parsers.parse('the material "{name}" colour is "{colour}"')) -def the_material_name_colour_is_set_to_colour(name, colour): +def then_the_material_name_colour_is_set_to_colour(name, colour): diffuse_color = list(the_material_name_exists(name).diffuse_color) assert diffuse_color == [float(c) for c in colour.split(",")], f"The colour is {diffuse_color}" @@ -616,7 +653,7 @@ def prop_is_value(prop, value): @then(parsers.parse('"{prop}" is roughly "{value}"')) -def prop_is_value(prop, value): +def prop_is_roughly_value(prop, value): prop = replace_variables(prop) value = replace_variables(value) is_value = False @@ -661,6 +698,7 @@ def the_object_name_has_a_thickness_thick_layered_material_containing_the_materi assert is_x(total_thickness, float(thickness)) assert material_name in material_names + @then(parsers.parse('the object "{name}" has no IFC materials')) def the_object_has_no_ifc_materials(name): element = tool.Ifc.get_entity(the_object_name_exists(name)) @@ -705,12 +743,6 @@ def the_collection_name1_is_in_the_collection_name2(name1, name2): assert bpy.data.collections.get(name2).children.get(name1) -@then(parsers.parse('the object "{name}" does not exist')) -def the_object_name_does_not_exist(name): - obj = bpy.data.objects.get(name) - assert obj is None or len(obj.users_collection) == 0, f"Object {name} exists" - - @then(parsers.parse('the collection "{name}" does not exist')) def the_collection_name_does_not_exist(name): obj = bpy.data.collections.get(name) @@ -753,7 +785,7 @@ def the_object_name_dimensions_are_dimensions(name, dimensions): @then(parsers.parse('the object "{name}" top right corner is at "{location}"')) -def the_object_name_is_at_location(name, location): +def the_object_name_top_right_corner_is_at_location(name, location): obj = the_object_name_exists(name) obj_corner = obj.matrix_world @ Vector(obj.bound_box[6]) assert ( @@ -762,7 +794,7 @@ def the_object_name_is_at_location(name, location): @then(parsers.parse('the object "{name}" bottom left corner is at "{location}"')) -def the_object_name_is_at_location(name, location): +def the_object_name_bottom_left_corner_is_at_location(name, location): obj = the_object_name_exists(name) obj_corner = obj.matrix_world @ Vector(obj.bound_box[0]) assert ( @@ -791,11 +823,13 @@ def the_file_name_should_contain_value(name, value): def the_object_name_has_no_modifiers(name): assert len(the_object_name_exists(name).modifiers) == 0 + @given(parsers.parse('I load the IFC test file "{filepath}"')) def i_load_the_ifc_test_file(filepath): filepath = f"{variables['cwd']}{filepath}" bpy.ops.bim.load_project(filepath=filepath, use_relative_path=True) + @given("I load the demo construction library") @when("I load the demo construction library") def i_add_a_construction_library(): @@ -836,7 +870,7 @@ def move_cursor_bottom_left(): @given(parsers.parse("I prepare to undo")) @when(parsers.parse("I prepare to undo")) @then(parsers.parse("I prepare to undo")) -def hit_undo(): +def prepare_undo(): bpy.ops.ed.undo_push(message="UNDO STEP") @@ -847,6 +881,7 @@ def hit_undo(): bpy.ops.ed.undo_push(message="UNDO STEP") bpy.ops.ed.undo() + @then(parsers.parse('the object "{obj_name1}" has a connection with "{obj_name2}"')) def the_obj1_has_a_connection_with_obj2(obj_name1, obj_name2): element1 = replace_variables(obj_name1) @@ -866,9 +901,11 @@ def the_obj1_has_a_connection_with_obj2(obj_name1, obj_name2): relationships[conn.RelatedElement] = conn.RelatingElement for key, value in relationships.items(): - assert (key.id() == element1.id() and value.id() == element2.id()) or (key.id() == element2.id() and value.id() == element1.id()), f"The object {obj_name1} is connected to {obj_name2}" + assert (key.id() == element1.id() and value.id() == element2.id()) or ( + key.id() == element2.id() and value.id() == element1.id() + ), f"The object {obj_name1} is connected to {obj_name2}" + - @then(parsers.parse('the object "{obj_name1}" and "{obj_name2}" belong to the same Linked Aggregate Group')) def the_obj1_and_obj2_belong_the_same_linked_aggregate_group(obj_name1, obj_name2): element1 = replace_variables(obj_name1) @@ -892,6 +929,7 @@ def the_obj1_and_obj2_belong_the_same_linked_aggregate_group(obj_name1, obj_name assert groups[0] == groups[1], "Objects do not belong to the same Linked Aggregate group" + @when(parsers.parse('the object layer length is set to "{value}"')) def the_obj_layer_lenght_is_set_to(value): value = float(value) @@ -900,11 +938,11 @@ def the_obj_layer_lenght_is_set_to(value): except: assert False, f"Property BIMModelProperties.length does not exist when trying to set to value {value}" - print(50*"@", bpy.context.selected_objects) - + print(50 * "@", bpy.context.selected_objects) + bpy.context.scene.BIMModelProperties.length = value bpy.ops.bim.change_layer_length(length=value) - + # These definitions are not to be used in tests but simply in debugging failing tests @@ -953,5 +991,3 @@ def run_pdb(): import pdb pdb.set_trace() - - diff --git a/src/blenderbim/test/core/test_aggregate.py b/src/blenderbim/test/core/test_aggregate.py index ca8cadcbdc..e4f3efe58d 100644 --- a/src/blenderbim/test/core/test_aggregate.py +++ b/src/blenderbim/test/core/test_aggregate.py @@ -38,7 +38,7 @@ class TestAssignObject: ifc.get_entity("relating_obj").should_be_called().will_return("relating_object") ifc.get_entity("related_obj").should_be_called().will_return("related_object") ifc.run( - "aggregate.assign_object", product="related_object", relating_object="relating_object" + "aggregate.assign_object", products=["related_object"], relating_object="relating_object" ).should_be_called().will_return("rel") aggregate.disable_editing("related_obj").should_be_called() collector.assign("relating_obj").should_be_called() @@ -53,8 +53,8 @@ class TestUnassignObject: def test_run(self, ifc, aggregate, collector): ifc.get_entity("related_obj").should_be_called().will_return("element") aggregate.get_container("element").should_be_called().will_return("container") - ifc.run("spatial.assign_container", product="element", relating_structure="container").should_be_called() - ifc.run("aggregate.unassign_object", product="element").should_be_called().will_return("rel") + ifc.run("spatial.assign_container", products=["element"], relating_structure="container").should_be_called() + ifc.run("aggregate.unassign_object", products=["element"]).should_be_called() collector.assign("relating_obj").should_be_called() collector.assign("related_obj").should_be_called() subject.unassign_object(ifc, aggregate, collector, relating_obj="relating_obj", related_obj="related_obj") diff --git a/src/blenderbim/test/core/test_brick.py b/src/blenderbim/test/core/test_brick.py index 157a363682..e410fbe9b9 100644 --- a/src/blenderbim/test/core/test_brick.py +++ b/src/blenderbim/test/core/test_brick.py @@ -124,21 +124,21 @@ class TestAssignBrickReference: ifc.run("library.add_reference", library="library").should_be_called().will_return("reference") brick.export_brick_attributes("brick_uri").should_be_called().will_return("attributes") ifc.run("library.edit_reference", reference="reference", attributes="attributes").should_be_called() - ifc.run("library.assign_reference", product="element", reference="reference").should_be_called() + ifc.run("library.assign_reference", products=["element"], reference="reference").should_be_called() brick.get_brickifc_project().should_be_called().will_return("project") brick.add_brickifc_reference("brick_uri", "element", "project").should_be_called() subject.assign_brick_reference(ifc, brick, element="element", library="library", brick_uri="brick_uri") def test_assigning_to_an_existing_reference(self, ifc, brick): brick.get_library_brick_reference("library", "brick_uri").should_be_called().will_return("reference") - ifc.run("library.assign_reference", product="element", reference="reference").should_be_called() + ifc.run("library.assign_reference", products=["element"], reference="reference").should_be_called() brick.get_brickifc_project().should_be_called().will_return("project") brick.add_brickifc_reference("brick_uri", "element", "project").should_be_called() subject.assign_brick_reference(ifc, brick, element="element", library="library", brick_uri="brick_uri") def test_adding_a_brickifc_project_if_it_doesnt_exist(self, ifc, brick): brick.get_library_brick_reference("library", "brick_uri").should_be_called().will_return("reference") - ifc.run("library.assign_reference", product="element", reference="reference").should_be_called() + ifc.run("library.assign_reference", products=["element"], reference="reference").should_be_called() brick.get_brickifc_project().should_be_called().will_return(None) brick.get_namespace("brick_uri").should_be_called().will_return("namespace") brick.add_brickifc_project("namespace").should_be_called().will_return("project") @@ -277,4 +277,4 @@ class TestSetBrickListRoot: class TestRemoveBrickRelation: def test_run(self, brick): brick.remove_relation("brick_uri", "predicate", "object").should_be_called() - subject.remove_brick_relation(brick, brick_uri="brick_uri", predicate="predicate", object="object") \ No newline at end of file + subject.remove_brick_relation(brick, brick_uri="brick_uri", predicate="predicate", object="object") diff --git a/src/blenderbim/test/core/test_document.py b/src/blenderbim/test/core/test_document.py index 708c899e48..9811c887a8 100644 --- a/src/blenderbim/test/core/test_document.py +++ b/src/blenderbim/test/core/test_document.py @@ -133,11 +133,11 @@ class TestRemoveDocument: class TestAssignDocument: def test_run(self, ifc): - ifc.run("document.assign_document", product="product", document="document").should_be_called() + ifc.run("document.assign_document", products=["product"], document="document").should_be_called() subject.assign_document(ifc, product="product", document="document") class TestUnassignDocument: def test_run(self, ifc): - ifc.run("document.unassign_document", product="product", document="document").should_be_called() + ifc.run("document.unassign_document", products=["product"], document="document").should_be_called() subject.unassign_document(ifc, product="product", document="document") diff --git a/src/blenderbim/test/core/test_drawing.py b/src/blenderbim/test/core/test_drawing.py index 55c6dfc3f8..a2b6608f5d 100644 --- a/src/blenderbim/test/core/test_drawing.py +++ b/src/blenderbim/test/core/test_drawing.py @@ -365,7 +365,7 @@ class TestAddDrawing: attributes={"Identification": "X", "Name": "name", "Scope": "DRAWING"}, ).should_be_called() ifc.run("document.edit_reference", reference="reference", attributes={"Location": "uri"}).should_be_called() - ifc.run("document.assign_document", product="element", document="reference").should_be_called() + ifc.run("document.assign_document", products=["element"], document="reference").should_be_called() drawing.import_drawings().should_be_called() subject.add_drawing(ifc, collector, drawing, target_view="target_view", location_hint="location_hint") @@ -378,7 +378,7 @@ class TestDuplicateDrawing: drawing.copy_representation("drawing", "new_drawing").should_be_called() drawing.set_name("new_drawing", "unique_name").should_be_called() drawing.get_drawing_group("new_drawing").should_be_called().will_return("group") - ifc.run("group.unassign_group", group="group", product="new_drawing").should_be_called() + ifc.run("group.unassign_group", group="group", products=["new_drawing"]).should_be_called() ifc.run("group.add_group").should_be_called().will_return("new_group") ifc.run( "group.edit_group", group="new_group", attributes={"Name": "unique_name", "ObjectType": "DRAWING"} @@ -387,11 +387,11 @@ class TestDuplicateDrawing: drawing.get_group_elements("group").should_be_called().will_return(["drawing", "annotation"]) ifc.run("root.copy_class", product="annotation").should_be_called().will_return("new_annotation") drawing.copy_representation("annotation", "new_annotation").should_be_called() - ifc.run("group.unassign_group", group="group", product="new_annotation").should_be_called() + ifc.run("group.unassign_group", group="group", products=["new_annotation"]).should_be_called() ifc.run("group.assign_group", group="new_group", products=["new_annotation"]).should_be_called() drawing.get_drawing_document("new_drawing").should_be_called().will_return("old_reference") - ifc.run("document.unassign_document", product="new_drawing", document="old_reference").should_be_called() + ifc.run("document.unassign_document", products=["new_drawing"], document="old_reference").should_be_called() ifc.run("document.add_information").should_be_called().will_return("information") ifc.run("document.add_reference", information="information").should_be_called().will_return("reference") @@ -405,7 +405,7 @@ class TestDuplicateDrawing: ifc.run( "document.edit_reference", reference="reference", attributes={"Location": "drawing_path"} ).should_be_called() - ifc.run("document.assign_document", product="new_drawing", document="reference").should_be_called() + ifc.run("document.assign_document", products=["new_drawing"], document="reference").should_be_called() drawing.import_drawings().should_be_called() subject.duplicate_drawing(ifc, drawing, drawing="drawing", should_duplicate_annotations=True) diff --git a/src/blenderbim/test/core/test_library.py b/src/blenderbim/test/core/test_library.py index 2809722e81..4722cde5c0 100644 --- a/src/blenderbim/test/core/test_library.py +++ b/src/blenderbim/test/core/test_library.py @@ -114,12 +114,12 @@ class TestEditLibraryReference: class TestAssignLibraryReference: def test_run(self, ifc): ifc.get_entity("obj").should_be_called().will_return("product") - ifc.run("library.assign_reference", product="product", reference="reference").should_be_called() + ifc.run("library.assign_reference", products=["product"], reference="reference").should_be_called() subject.assign_library_reference(ifc, obj="obj", reference="reference") class TestUnassignLibraryReference: def test_run(self, ifc): ifc.get_entity("obj").should_be_called().will_return("product") - ifc.run("library.unassign_reference", product="product", reference="reference").should_be_called() + ifc.run("library.unassign_reference", products=["product"], reference="reference").should_be_called() subject.unassign_library_reference(ifc, obj="obj", reference="reference") diff --git a/src/blenderbim/test/core/test_nest.py b/src/blenderbim/test/core/test_nest.py index 56f73cacbb..2eb0ffbc33 100644 --- a/src/blenderbim/test/core/test_nest.py +++ b/src/blenderbim/test/core/test_nest.py @@ -38,7 +38,7 @@ class TestAssignObject: ifc.get_entity("relating_obj").should_be_called().will_return("relating_object") ifc.get_entity("related_obj").should_be_called().will_return("related_object") ifc.run( - "nest.assign_object", related_object="related_object", relating_object="relating_object" + "nest.assign_object", related_objects=["related_object"], relating_object="relating_object" ).should_be_called().will_return("rel") nest.disable_editing("related_obj").should_be_called() collector.assign("relating_obj").should_be_called() @@ -52,8 +52,8 @@ class TestUnassignObject: def test_run(self, ifc, nest, collector): ifc.get_entity("related_obj").should_be_called().will_return("element") nest.get_container("element").should_be_called().will_return("container") - ifc.run("spatial.assign_container", product="element", relating_structure="container").should_be_called() - ifc.run("nest.unassign_object", related_object="element").should_be_called().will_return("rel") + ifc.run("spatial.assign_container", products=["element"], relating_structure="container").should_be_called() + ifc.run("nest.unassign_object", related_objects=["element"]).should_be_called().will_return("rel") collector.assign("relating_obj").should_be_called() collector.assign("related_obj").should_be_called() subject.unassign_object(ifc, nest, collector, relating_obj="relating_obj", related_obj="related_obj") diff --git a/src/blenderbim/test/core/test_spatial.py b/src/blenderbim/test/core/test_spatial.py index 88210a9682..9e01aefde4 100644 --- a/src/blenderbim/test/core/test_spatial.py +++ b/src/blenderbim/test/core/test_spatial.py @@ -23,14 +23,14 @@ from test.core.bootstrap import ifc, collector, spatial class TestReferenceStructure: def test_run(self, ifc, spatial): spatial.can_reference("structure", "element").should_be_called().will_return(True) - ifc.run("spatial.reference_structure", product="element", relating_structure="structure").should_be_called() + ifc.run("spatial.reference_structure", products=["element"], relating_structure="structure").should_be_called() subject.reference_structure(ifc, spatial, structure="structure", element="element") class TestDereferenceStructure: def test_run(self, ifc, spatial): spatial.can_reference("structure", "element").should_be_called().will_return(True) - ifc.run("spatial.dereference_structure", product="element", relating_structure="structure").should_be_called() + ifc.run("spatial.dereference_structure", products=["element"], relating_structure="structure").should_be_called() subject.dereference_structure(ifc, spatial, structure="structure", element="element") @@ -40,7 +40,7 @@ class TestAssignContainer: ifc.get_entity("structure_obj").should_be_called().will_return("structure") ifc.get_entity("element_obj").should_be_called().will_return("element") ifc.run( - "spatial.assign_container", product="element", relating_structure="structure" + "spatial.assign_container", products=["element"], relating_structure="structure" ).should_be_called().will_return("rel") spatial.disable_editing("element_obj").should_be_called() collector.assign("element_obj").should_be_called() @@ -72,7 +72,7 @@ class TestChangeSpatialLevel: class TestRemoveContainer: def test_run(self, ifc, collector): ifc.get_entity("obj").should_be_called().will_return("element") - ifc.run("spatial.remove_container", product="element").should_be_called() + ifc.run("spatial.unassign_container", products=["element"]).should_be_called() collector.assign("obj").should_be_called() subject.remove_container(ifc, collector, obj="obj") diff --git a/src/blenderbim/test/core/test_system.py b/src/blenderbim/test/core/test_system.py index f77cae4cd1..51c4dc9bae 100644 --- a/src/blenderbim/test/core/test_system.py +++ b/src/blenderbim/test/core/test_system.py @@ -74,13 +74,13 @@ class TestDisableEditingSystem: class TestAssignSystem: def test_run(self, ifc): - ifc.run("system.assign_system", product="product", system="system").should_be_called() + ifc.run("system.assign_system", products=["product"], system="system").should_be_called() subject.assign_system(ifc, system="system", product="product") class TestUnassignSystem: def test_run(self, ifc): - ifc.run("system.unassign_system", product="product", system="system").should_be_called() + ifc.run("system.unassign_system", products=["product"], system="system").should_be_called() subject.unassign_system(ifc, system="system", product="product") diff --git a/src/blenderbim/test/core/test_type.py b/src/blenderbim/test/core/test_type.py index 0a084f8438..5800f04ea4 100644 --- a/src/blenderbim/test/core/test_type.py +++ b/src/blenderbim/test/core/test_type.py @@ -22,7 +22,7 @@ from test.core.bootstrap import ifc, type class TestAssignType: def test_assigning_and_switching_to_an_existing_type_data(self, ifc, type): - ifc.run("type.assign_type", related_object="element", relating_type="type").should_be_called() + ifc.run("type.assign_type", related_objects=["element"], relating_type="type").should_be_called() type.has_material_usage("element").should_be_called().will_return(False) ifc.get_object("type").should_be_called().will_return("type_obj") type.get_object_data("type_obj").should_be_called().will_return("type_obj_data") @@ -32,7 +32,7 @@ class TestAssignType: subject.assign_type(ifc, type, element="element", type="type") def test_assigning_and_not_changing_data_if_the_type_has_no_data(self, ifc, type): - ifc.run("type.assign_type", related_object="element", relating_type="type").should_be_called() + ifc.run("type.assign_type", related_objects=["element"], relating_type="type").should_be_called() type.has_material_usage("element").should_be_called().will_return(False) ifc.get_object("type").should_be_called().will_return("type_obj") type.get_object_data("type_obj").should_be_called().will_return(None) diff --git a/src/blenderbim/test/tool/test_aggregate.py b/src/blenderbim/test/tool/test_aggregate.py index 21a1b7002c..6540d2fcb7 100644 --- a/src/blenderbim/test/tool/test_aggregate.py +++ b/src/blenderbim/test/tool/test_aggregate.py @@ -125,5 +125,5 @@ class TestGetContainer(NewFile): tool.Ifc.set(ifc) element = ifc.createIfcWall() container = ifc.createIfcBuildingStorey() - ifcopenshell.api.run("spatial.assign_container", ifc, product=element, relating_structure=container) + ifcopenshell.api.run("spatial.assign_container", ifc, products=[element], relating_structure=container) assert subject.get_container(element) == container diff --git a/src/blenderbim/test/tool/test_blender.py b/src/blenderbim/test/tool/test_blender.py index e05e98f98a..328363bafb 100644 --- a/src/blenderbim/test/tool/test_blender.py +++ b/src/blenderbim/test/tool/test_blender.py @@ -20,6 +20,7 @@ import bpy import ifcopenshell import blenderbim.core.tool import blenderbim.tool as tool +import pytest from test.bim.bootstrap import NewFile from blenderbim.tool.blender import Blender as subject @@ -44,3 +45,17 @@ class TestCopyNodeGraph(NewFile): subject.copy_node_graph(material_to, material_from) assert len(material_to_nodes) == 2 + + +class TestSortPanelsForRegister(NewFile): + def test_run(self): + items = ["A", "B", "C", "D"] + items_to_parents = {"A": "D", "D": "C", "C": "B"} + sorted_items = subject.sort_panels_for_register(items, items_to_parents) + assert tuple(sorted_items) == ("B", "C", "D", "A") + + with pytest.raises(AssertionError): + subject.sort_panels_for_register(items, {"A": "K"}) + + with pytest.raises(AssertionError): + subject.sort_panels_for_register(items, {"J": "A"}) diff --git a/src/blenderbim/test/tool/test_brick.py b/src/blenderbim/test/tool/test_brick.py index b70b9b2240..b5d5a1f973 100644 --- a/src/blenderbim/test/tool/test_brick.py +++ b/src/blenderbim/test/tool/test_brick.py @@ -320,8 +320,8 @@ class TestGetParentSpace(NewFile): element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBuildingStorey") subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSpace") project = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") - ifcopenshell.api.run("aggregate.assign_object", ifc, product=subelement, relating_object=element) - ifcopenshell.api.run("aggregate.assign_object", ifc, product=element, relating_object=project) + ifcopenshell.api.run("aggregate.assign_object", ifc, products=[subelement], relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", ifc, products=[element], relating_object=project) assert subject.get_parent_space(subelement) == element assert subject.get_parent_space(element) is None diff --git a/src/blenderbim/test/tool/test_collector.py b/src/blenderbim/test/tool/test_collector.py index 94e9bee0cc..f9bf18d0f1 100644 --- a/src/blenderbim/test/tool/test_collector.py +++ b/src/blenderbim/test/tool/test_collector.py @@ -18,6 +18,8 @@ import bpy import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.element import blenderbim.core.tool import blenderbim.tool as tool from blenderbim.tool.collector import Collector as subject @@ -39,7 +41,7 @@ class TestAssign(NewFile): ifcopenshell.api.run( "spatial.assign_container", tool.Ifc.get(), - product=wall_element, + products=[wall_element], relating_structure=tool.Ifc.get().by_type("IfcSite")[0], ) subject.assign(wall_obj) @@ -66,7 +68,7 @@ class TestAssign(NewFile): "aggregate.assign_object", tool.Ifc.get(), relating_object=tool.Ifc.get().by_type("IfcSite")[0], - product=space_element, + products=[space_element], ) subject.assign(space_obj) assert len(space_obj.users_collection) == 1 @@ -82,7 +84,7 @@ class TestAssign(NewFile): "aggregate.assign_object", tool.Ifc.get(), relating_object=tool.Ifc.get().by_type("IfcSite")[0], - product=space_element, + products=[space_element], ) subject.assign(space_obj) subject.assign(space_obj) @@ -101,7 +103,7 @@ class TestAssign(NewFile): "aggregate.assign_object", tool.Ifc.get(), relating_object=tool.Ifc.get().by_type("IfcSite")[0], - product=space_element, + products=[space_element], ) subject.assign(space_obj) assert len(space_obj.users_collection) == 1 @@ -119,7 +121,7 @@ class TestAssign(NewFile): "aggregate.assign_object", tool.Ifc.get(), relating_object=element, - product=subelement, + products=[subelement], ) subject.assign(element_obj) assert len(element_obj.users_collection) == 1 @@ -159,7 +161,7 @@ class TestAssign(NewFile): "aggregate.assign_object", tool.Ifc.get(), relating_object=tool.Ifc.get().by_type("IfcSite")[0], - product=space_element, + products=[space_element], ) subject.assign(space_obj) assert bpy.context.scene.collection.children.find(space_collection.name) == -1 @@ -178,7 +180,7 @@ class TestAssign(NewFile): "aggregate.assign_object", tool.Ifc.get(), relating_object=element, - product=subelement, + products=[subelement], ) subject.assign(element_obj) subject.assign(subelement_obj) @@ -194,14 +196,14 @@ class TestAssign(NewFile): ifcopenshell.api.run( "spatial.assign_container", tool.Ifc.get(), - product=element, + products=[element], relating_structure=tool.Ifc.get().by_type("IfcSite")[0], ) ifcopenshell.api.run( "aggregate.assign_object", tool.Ifc.get(), relating_object=element, - product=subelement, + products=[subelement], ) subject.assign(subelement_obj) assert subelement_obj.users_collection[0].name == "IfcSite/My Site" @@ -234,7 +236,7 @@ class TestAssign(NewFile): ifcopenshell.api.run( "spatial.assign_container", tool.Ifc.get(), - product=element, + products=[element], relating_structure=tool.Ifc.get().by_type("IfcSite")[0], ) bpy.context.scene.collection.objects.link(element_obj) @@ -253,7 +255,7 @@ class TestAssign(NewFile): ifcopenshell.api.run( "spatial.assign_container", tool.Ifc.get(), - product=element, + products=[element], relating_structure=tool.Ifc.get().by_type("IfcSite")[0], ) bpy.context.scene.collection.objects.link(element_obj) diff --git a/src/blenderbim/test/tool/test_document.py b/src/blenderbim/test/tool/test_document.py index 3ef56744e0..8b22ce19d0 100644 --- a/src/blenderbim/test/tool/test_document.py +++ b/src/blenderbim/test/tool/test_document.py @@ -18,6 +18,7 @@ import bpy import ifcopenshell +import ifcopenshell.api import blenderbim.core.tool import blenderbim.tool as tool from test.bim.bootstrap import NewFile @@ -188,7 +189,7 @@ class TestImportReferences(NewFile): assert len(props.documents) == 1 assert props.documents[0].ifc_definition_id == reference.id() assert props.documents[0].name == "Unnamed" - assert props.documents[0].identification == "*" + assert props.documents[0].identification == "X" assert props.documents[0].is_information is False diff --git a/src/blenderbim/test/tool/test_geometry.py b/src/blenderbim/test/tool/test_geometry.py index 48f443ad19..0837fe50a3 100644 --- a/src/blenderbim/test/tool/test_geometry.py +++ b/src/blenderbim/test/tool/test_geometry.py @@ -199,7 +199,7 @@ class TestGetElementType(NewFile): ifc = tool.Ifc.get() element = ifc.createIfcWall() type = ifc.createIfcWallType() - ifcopenshell.api.run("type.assign_type", ifc, related_object=element, relating_type=type) + ifcopenshell.api.run("type.assign_type", ifc, related_objects=[element], relating_type=type) assert subject.get_element_type(element) == type @@ -209,7 +209,7 @@ class TestGetElementsOfType(NewFile): ifc = tool.Ifc.get() element = ifc.createIfcWall() type = ifc.createIfcWallType() - ifcopenshell.api.run("type.assign_type", ifc, related_object=element, relating_type=type) + ifcopenshell.api.run("type.assign_type", ifc, related_objects=[element], relating_type=type) assert subject.get_elements_of_type(type) == (element,) diff --git a/src/blenderbim/test/tool/test_material.py b/src/blenderbim/test/tool/test_material.py index 467eb34afe..6fd0a8ac68 100644 --- a/src/blenderbim/test/tool/test_material.py +++ b/src/blenderbim/test/tool/test_material.py @@ -79,7 +79,7 @@ class TestGetElementsByMaterial(NewFile): tool.Ifc.set(ifc) element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") material = ifcopenshell.api.run("material.add_material", ifc) - ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material) + ifcopenshell.api.run("material.assign_material", ifc, products=[element], material=material) assert subject.get_elements_by_material(material) == {element} diff --git a/src/blenderbim/test/tool/test_misc.py b/src/blenderbim/test/tool/test_misc.py index 02ed0ca22d..bf7e0f5ce7 100644 --- a/src/blenderbim/test/tool/test_misc.py +++ b/src/blenderbim/test/tool/test_misc.py @@ -40,7 +40,7 @@ class TestGetObjectStorey(test.bim.bootstrap.NewFile): wall = ifc.createIfcWall() tool.Ifc.link(wall, obj) storey = ifc.createIfcBuildingStorey() - ifcopenshell.api.run("spatial.assign_container", ifc, product=wall, relating_structure=storey) + ifcopenshell.api.run("spatial.assign_container", ifc, products=[wall], relating_structure=storey) assert subject.get_object_storey(obj) == storey def test_only_returning_a_building_storey(self): @@ -50,7 +50,7 @@ class TestGetObjectStorey(test.bim.bootstrap.NewFile): wall = ifc.createIfcWall() tool.Ifc.link(wall, obj) building = ifc.createIfcBuilding() - ifcopenshell.api.run("spatial.assign_container", ifc, product=wall, relating_structure=building) + ifcopenshell.api.run("spatial.assign_container", ifc, products=[wall], relating_structure=building) assert subject.get_object_storey(obj) is None def test_returning_nothing_if_uncontained(self): @@ -86,8 +86,8 @@ class TestGetStoreyHeight(test.bim.bootstrap.NewFile): storey.Elevation = 3000 storey2 = ifc.createIfcBuildingStorey() storey2.Elevation = 5000 - ifcopenshell.api.run("aggregate.assign_object", ifc, product=storey, relating_object=building) - ifcopenshell.api.run("aggregate.assign_object", ifc, product=storey2, relating_object=building) + ifcopenshell.api.run("aggregate.assign_object", ifc, products=[storey], relating_object=building) + ifcopenshell.api.run("aggregate.assign_object", ifc, products=[storey2], relating_object=building) assert subject.get_storey_height_in_si(storey, 1) == 2.0 def test_getting_a_double_storey_height(self): @@ -103,9 +103,9 @@ class TestGetStoreyHeight(test.bim.bootstrap.NewFile): storey2.Elevation = 5000 storey3 = ifc.createIfcBuildingStorey() storey3.Elevation = 9000 - ifcopenshell.api.run("aggregate.assign_object", ifc, product=storey, relating_object=building) - ifcopenshell.api.run("aggregate.assign_object", ifc, product=storey2, relating_object=building) - ifcopenshell.api.run("aggregate.assign_object", ifc, product=storey3, relating_object=building) + ifcopenshell.api.run("aggregate.assign_object", ifc, products=[storey], relating_object=building) + ifcopenshell.api.run("aggregate.assign_object", ifc, products=[storey2], relating_object=building) + ifcopenshell.api.run("aggregate.assign_object", ifc, products=[storey3], relating_object=building) assert subject.get_storey_height_in_si(storey, 2) == 6.0 def test_only_considering_storeys_in_the_same_building(self): @@ -116,7 +116,7 @@ class TestGetStoreyHeight(test.bim.bootstrap.NewFile): storey.Elevation = 3000 storey2 = ifc.createIfcBuildingStorey() storey2.Elevation = 5000 - ifcopenshell.api.run("aggregate.assign_object", ifc, product=storey, relating_object=building) + ifcopenshell.api.run("aggregate.assign_object", ifc, products=[storey], relating_object=building) assert subject.get_storey_height_in_si(storey, 1) is None def test_returning_none_if_the_storey_height_is_undefined(self): @@ -124,7 +124,7 @@ class TestGetStoreyHeight(test.bim.bootstrap.NewFile): tool.Ifc.set(ifc) building = ifc.createIfcBuilding() storey = ifc.createIfcBuildingStorey() - ifcopenshell.api.run("aggregate.assign_object", ifc, product=storey, relating_object=building) + ifcopenshell.api.run("aggregate.assign_object", ifc, products=[storey], relating_object=building) assert subject.get_storey_height_in_si(storey, 1) is None diff --git a/src/blenderbim/test/tool/test_nest.py b/src/blenderbim/test/tool/test_nest.py index fcf244b59a..f45a9c7556 100644 --- a/src/blenderbim/test/tool/test_nest.py +++ b/src/blenderbim/test/tool/test_nest.py @@ -70,5 +70,5 @@ class TestGetContainer(NewFile): tool.Ifc.set(ifc) element = ifc.createIfcWall() container = ifc.createIfcBuildingStorey() - ifcopenshell.api.run("spatial.assign_container", ifc, product=element, relating_structure=container) + ifcopenshell.api.run("spatial.assign_container", ifc, products=[element], relating_structure=container) assert subject.get_container(element) == container diff --git a/src/blenderbim/test/tool/test_root.py b/src/blenderbim/test/tool/test_root.py index 448fb4bc4d..7e0e40fed4 100644 --- a/src/blenderbim/test/tool/test_root.py +++ b/src/blenderbim/test/tool/test_root.py @@ -18,6 +18,7 @@ import bpy import ifcopenshell +import ifcopenshell.api import blenderbim.core.tool import blenderbim.tool as tool from test.bim.bootstrap import NewFile @@ -97,7 +98,7 @@ class TestGetElementType(NewFile): ifc = tool.Ifc.get() element = ifc.createIfcWall() type = ifc.createIfcWallType() - ifcopenshell.api.run("type.assign_type", ifc, related_object=element, relating_type=type) + ifcopenshell.api.run("type.assign_type", ifc, related_objects=[element], relating_type=type) assert subject.get_element_type(element) == type diff --git a/src/blenderbim/test/tool/test_spatial.py b/src/blenderbim/test/tool/test_spatial.py index 7aafc1e882..602a41b8f9 100644 --- a/src/blenderbim/test/tool/test_spatial.py +++ b/src/blenderbim/test/tool/test_spatial.py @@ -154,7 +154,7 @@ class TestGetContainer(NewFile): ifc = ifcopenshell.file() site = ifc.createIfcSite() wall = ifc.createIfcWall() - ifcopenshell.api.run("spatial.assign_container", ifc, product=wall, relating_structure=site) + ifcopenshell.api.run("spatial.assign_container", ifc, products=[wall], relating_structure=site) assert subject.get_container(wall) == site @@ -163,7 +163,7 @@ class TestGetDecomposedElements(NewFile): ifc = ifcopenshell.file() site = ifc.createIfcSite() wall = ifc.createIfcWall() - ifcopenshell.api.run("spatial.assign_container", ifc, product=wall, relating_structure=site) + ifcopenshell.api.run("spatial.assign_container", ifc, products=[wall], relating_structure=site) assert subject.get_decomposed_elements(site) == [wall] @@ -268,4 +268,4 @@ class TestSelectProducts(NewFile): bpy.context.scene.collection.objects.link(obj) tool.Ifc.link(product, obj) subject.select_products([product]) - assert obj in bpy.context.selected_objects \ No newline at end of file + assert obj in bpy.context.selected_objects diff --git a/src/blenderbim/test/tool/test_system.py b/src/blenderbim/test/tool/test_system.py index 419c46cc7f..93235545b1 100644 --- a/src/blenderbim/test/tool/test_system.py +++ b/src/blenderbim/test/tool/test_system.py @@ -262,7 +262,7 @@ class TestSelectSystemProducts(NewFile): tool.Ifc().set(ifc) element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcPump") system = ifcopenshell.api.run("system.add_system", ifc, ifc_class="IfcSystem") - ifcopenshell.api.run("system.assign_system", ifc, product=element, system=system) + ifcopenshell.api.run("system.assign_system", ifc, products=[element], system=system) obj = bpy.data.objects.new("Object", None) bpy.context.scene.collection.objects.link(obj) tool.Ifc.link(element, obj) diff --git a/src/blenderbim/test/tool/test_type.py b/src/blenderbim/test/tool/test_type.py index 4e3a7342f5..bf497e021c 100644 --- a/src/blenderbim/test/tool/test_type.py +++ b/src/blenderbim/test/tool/test_type.py @@ -154,7 +154,7 @@ class TestGetTypeOccurrences(NewFile): tool.Ifc.set(ifc) wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType") wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") - ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type) + ifcopenshell.api.run("type.assign_type", ifc, related_objects=[wall], relating_type=wall_type) assert subject.get_type_occurrences(wall_type) == (wall,) diff --git a/src/bsdd/Makefile b/src/bsdd/Makefile new file mode 100644 index 0000000000..6880922653 --- /dev/null +++ b/src/bsdd/Makefile @@ -0,0 +1,34 @@ +# bSDD - Python bSDD library +# Copyright (C) 2021 Dion Moult +# +# This file is part of bSDD. +# +# bSDD is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# bSDD is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with bSDD. If not, see . + +VERSION:=`date '+%y%m%d'` +SED:=sed -i +ifeq ($(UNAME_S),Darwin) +SED:=sed -i '' -e +endif + +.PHONY: test +test: + pytest -p no:pytest-blender test + +.PHONY: dist +dist: + rm -rf dist + $(SED) "s/999999/$(VERSION)/" pyproject.toml + python -m build + $(SED) "s/$(VERSION)/999999/" pyproject.toml diff --git a/src/bsdd/__about__.py b/src/bsdd/__about__.py deleted file mode 100644 index 47782d2864..0000000000 --- a/src/bsdd/__about__.py +++ /dev/null @@ -1,4 +0,0 @@ -# SPDX-FileCopyrightText: 2024-present IfcOpenShell Developers -# -# SPDX-License-Identifier: LGPL-3.0-or-later -__version__ = "0.1.0" diff --git a/src/bsdd/bsdd.py b/src/bsdd/bsdd.py index 8c5fe9298a..7cc7301668 100644 --- a/src/bsdd/bsdd.py +++ b/src/bsdd/bsdd.py @@ -23,7 +23,7 @@ import urllib import requests import webbrowser import http.server -from typing import TypedDict, Literal +from typing import TypedDict, Literal, Optional Status = Literal["Preview", "Active", "Inactive"] ClassTypes = Literal["Class", "GroupOfProperties", "AlternativeUse", "Material"] @@ -592,7 +592,7 @@ class Client: } return self.get(endpoint, params) - def get_properties(self, dictionary_uri: str, language_code: str = "", + def get_properties(self, dictionary_uri: str, offset: int = 0, limit: int = 100, language_code: str = "", version: int = 1) -> DictionaryPropertiesResponseContractV1: """ Get Dictionary with its properties @@ -601,11 +601,14 @@ class Client: params = { "Uri": dictionary_uri, "languageCode": language_code, + "offset": offset, + "limit": limit } return self.get(endpoint, params) def get_class(self, class_uri: str, include_class_properties: bool = True, include_child_class_reference: bool = True, include_class_relations: bool = True, + include_reverse_relations: bool = False, reverse_relation_dictionary_uris: Optional[list[str]]=None, language_code: str = "", version: int = 1) -> ClassContractV1: """ Get Class details @@ -617,8 +620,11 @@ class Client: "includeClassProperties": include_class_properties, "includeChildClassReferences": include_child_class_reference, "includeClassRelations": include_class_relations, + "IncludeReverseRelations": include_reverse_relations, + "ReverseRelationDictionaryUris": reverse_relation_dictionary_uris, "languageCode": language_code, } + params = {k: v for k, v in params.items() if v is not None} return self.get(endpoint, params) def get_property(self, uri, include_classes=False, language_code="", @@ -649,7 +655,7 @@ class Client: return self.get(endpoint, params) def search_text(self, search_text: str, type_filter="All", dictionary_uris=None, - version: int = 1) -> TextSearchResponseContractV1: + version: int = 1, offset: int = 0, limit: int = 100) -> TextSearchResponseContractV1: """ Search the bSDD database using free text, get list of Classes and/or Properties matching the text. Pagination options are for Classes and Properties combined. @@ -663,13 +669,15 @@ class Client: "SearchText": search_text, "TypeFilter": type_filter, "DictionaryUris": dictionary_uris, + "offset": offset, + "limit": limit } return self.get(endpoint, params) pass def search_in_dictionary(self, dictionary_uri: str, search_text: str = "", language_code: str = "", related_ifc_entity: str = "", - version: int = "1") -> SearchInDictionaryResponseContractV1: + version: int = 1, offset: int = 0, limit: int = 100) -> SearchInDictionaryResponseContractV1: """ Search the bSDD database, get list of Classes without details. This version uses new naming and returns one Dictionary instead of a list with always one Dictionary. @@ -681,11 +689,13 @@ class Client: "SearchText": search_text, "LanguageCode": language_code, "RelatedIfcEntity": related_ifc_entity, + "offset": offset, + "limit": limit } return self.get(endpoint, params) def search_class(self, search_text: str, dictionary_uris=None, related_ifc_entities=None, - version: int = 1) -> ClassSearchResponseContractV1: + version: int = 1, offset: int = 0, limit: int = 100) -> ClassSearchResponseContractV1: """ Search the bSDD database using free text, get list of Classes matching the text and optional additional filters. this API replaces ClassificationSearch @@ -700,6 +710,8 @@ class Client: "SearchText": search_text, "DictionaryUris": dictionary_uris, "RelatedIfcEntities": related_ifc_entities, + "offset": offset, + "limit": limit } return self.get(endpoint, params) diff --git a/src/bsdd/pyproject.toml b/src/bsdd/pyproject.toml index 72683258eb..2b9e471050 100644 --- a/src/bsdd/pyproject.toml +++ b/src/bsdd/pyproject.toml @@ -4,31 +4,32 @@ build-backend = "hatchling.build" [project] name = "bsdd" -dynamic = ["version"] -description = '' +version = "0.0.999999" +authors = [ + { name="Dion Moult", email="dion@thinkmoult.com" }, +] +description = "Library to interact with the buildingSMART Data Dictionary" readme = "README.md" requires-python = ">=3.8" -keywords = ["building smart data dictionaries"] +keywords = ["IFC", "bSDD", "BIM"] classifiers = [ - "Development Status :: 4 - Beta", - "Programming Language :: Python", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", ] dependencies = ["requests"] [project.urls] -Documentation = "https://github.com/IfcOpenShell/src/bsdd#readme" -Issues = "https://github.com/IfcOpenShell/issues" -Source = "https://github.com/IfcOpenShell/src/bsdd" - -[tool.hatch.version] -path = "__about__.py" +Homepage = "http://ifcopenshell.org" +Documentation = "https://docs.ifcopenshell.org" +Issues = "https://github.com/IfcOpenShell/IfcOpenShell/issues" [tool.hatch.envs.default] dependencies = [ @@ -39,6 +40,5 @@ dependencies = [ [[tool.hatch.envs.all.matrix]] python = ["3.8", "3.9", "3.10", "3.11", "3.12"] - [tool.pytest.ini_options] -pythonpath = [".", "src"] \ No newline at end of file +pythonpath = [".", "src"] diff --git a/src/bsdd/tests/test_bsdd.py b/src/bsdd/tests/test_bsdd.py index 56ae193737..2cc1fd3935 100644 --- a/src/bsdd/tests/test_bsdd.py +++ b/src/bsdd/tests/test_bsdd.py @@ -35,3 +35,7 @@ def test_search_class(): assert len(li) < 8 # I think it should be 4 but just validating it isn't overfetching with some space for future change for l in li: assert l in [_["name"] for _ in ss_heat_pump_sys["classes"]] + +def test_get_properties(): + pr = client.get_properties(ifc4x3_uri, offset=0, limit=5) + assert len(pr["properties"]) == 5 diff --git a/src/ifc4d/ifc4d/csv4d2ifc.py b/src/ifc4d/ifc4d/csv4d2ifc.py index a5f6a460a9..4da00339c6 100644 --- a/src/ifc4d/ifc4d/csv4d2ifc.py +++ b/src/ifc4d/ifc4d/csv4d2ifc.py @@ -118,6 +118,7 @@ class Csv2Ifc: def create_boilerplate_ifc(self): self.file = ifcopenshell.file(schema="IFC4") + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") self.work_plan = self.file.create_entity("IfcWorkPlan") def create_tasks(self, tasks, parent=None): diff --git a/src/ifc4d/ifc4d/msp2ifc.py b/src/ifc4d/ifc4d/msp2ifc.py index 159d8279e9..face82e7e9 100644 --- a/src/ifc4d/ifc4d/msp2ifc.py +++ b/src/ifc4d/ifc4d/msp2ifc.py @@ -18,6 +18,7 @@ import datetime from datetime import timedelta, date +from typing import List import ifcopenshell import ifcopenshell.api import ifcopenshell.util.date @@ -25,7 +26,7 @@ import xml.etree.ElementTree as ET class MSP2Ifc: - def __init__(self): + def __init__(self, optionalColumns: List[str] = []): self.xml = None self.file = None self.ns = None @@ -33,6 +34,7 @@ class MSP2Ifc: self.project = {} self.calendars = {} self.tasks = {} + self.optionalColumns = optionalColumns self.resources = {} self.RESOURCE_TYPES_MAPPING = {"1": "LABOR", "0": "MATERIAL", "2": None} @@ -106,6 +108,18 @@ class MSP2Ifc: "ifc": None, } + # retrieve optional columns + # If first column = "all" then retrieve all columns + if len(self.optionalColumns) and self.optionalColumns[0] == "all": + self.optionalColumns = [child.tag.split("}")[1] for child in task] + + for column in self.optionalColumns: + if not self.tasks[task_id].get(column): + self.tasks[task_id][column] = task.find(f"pr:{column}", self.ns).text if task.find(f"pr:{column}", self.ns) else None + + + + def parse_calendar_xml(self, project): def parse_working_times(day): working_times = [] @@ -194,12 +208,14 @@ class MSP2Ifc: def create_boilerplate_ifc(self): self.file = ifcopenshell.file(schema="IFC4") + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") self.work_plan = self.file.create_entity("IfcWorkPlan") def create_tasks(self, work_schedule): for task_id in self.tasks: task = self.tasks[task_id] - if task["OutlineLevel"] == 0: + # Outline Level can be None or 0 + if not task["OutlineLevel"]: self.create_task(task, work_schedule=work_schedule) def create_work_schedule(self): @@ -267,6 +283,18 @@ class MSP2Ifc: for subtask_id in task["subtasks"]: self.create_task(self.tasks[subtask_id], parent_task=task) + + # create pset for optional columns + if len(self.optionalColumns): + pset = ifcopenshell.api.run("pset.add_pset", self.file, product=task["ifc"] , name="Pset_MSP_Task") + + ifcopenshell.api.run( + "pset.edit_pset", + self.file, + pset=pset, + properties= {name: str(task[name]) for name in self.optionalColumns if task[name]} + ) + def process_working_week(self, week, calendar): day_map = { "1": 7, # Sunday diff --git a/src/ifc5d/ifc5d/csv2ifc.py b/src/ifc5d/ifc5d/csv2ifc.py index cb7b8869ff..1e708d1e8f 100644 --- a/src/ifc5d/ifc5d/csv2ifc.py +++ b/src/ifc5d/ifc5d/csv2ifc.py @@ -196,6 +196,7 @@ class Csv2Ifc: def create_boilerplate_ifc(self): self.file = ifcopenshell.file(schema="IFC4") + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") def has_property(self, product, property_name): diff --git a/src/ifcbimtester/CITATION.cff b/src/ifcbimtester/CITATION.cff index d2d6ca6abb..a7222b5a28 100644 --- a/src/ifcbimtester/CITATION.cff +++ b/src/ifcbimtester/CITATION.cff @@ -11,7 +11,7 @@ authors: - name: "IfcOpenShell contributors"authors: repository-code: >- https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.7.0/src/ifcbimtester -url: 'https://blenderbim.org/docs-python/bimtester.html' +url: 'https://docs.ifcopenshell.org/bimtester.html' abstract: Wrapper for Gherkin based unit testing for IFC models keywords: - Gherkin diff --git a/src/ifcclash/Makefile b/src/ifcclash/Makefile new file mode 100644 index 0000000000..ec05e23471 --- /dev/null +++ b/src/ifcclash/Makefile @@ -0,0 +1,34 @@ +# IfcClash - IFC-based clash detection. +# Copyright (C) 2020-2024 Dion Moult +# +# This file is part of IfcClash. +# +# IfcClash is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcClash is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcClash. If not, see . + +VERSION:=`date '+%y%m%d'` +SED:=sed -i +ifeq ($(UNAME_S),Darwin) +SED:=sed -i '' -e +endif + +.PHONY: qa +qa: + black . + +.PHONY: dist +dist: + rm -rf dist + $(SED) "s/999999/$(VERSION)/" pyproject.toml + python -m build + $(SED) "s/$(VERSION)/999999/" pyproject.toml diff --git a/src/ifcclash/ifcclash/collider.py b/src/ifcclash/ifcclash/collider.py deleted file mode 100644 index 62a912df5e..0000000000 --- a/src/ifcclash/ifcclash/collider.py +++ /dev/null @@ -1,121 +0,0 @@ -# IfcClash - IFC-based clash detection. -# Copyright (C) 2020, 2021 Dion Moult -# -# This file is part of IfcClash. -# -# IfcClash is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# IfcClash is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with IfcClash. If not, see . - -import hppfcl -import numpy as np -import ifcopenshell - - -class Collider: - def __init__(self, logger): - self.logger = logger - self.groups = {} - self.tree = ifcopenshell.geom.tree() - - def create_group(self, name): - self.logger.info(f"Creating group {name}") - self.groups[name] = {"elements": {}, "objects": {}} - - def create_objects(self, name, ifc_file, iterator, elements): - import time - - start = time.time() - self.logger.info(f"Adding objects {name}") - assert iterator.initialize() - while True: - self.tree.add_element(iterator.get_native()) - shape = iterator.get() - self.create_object(name, shape.guid, shape) - if not iterator.next(): - break - self.logger.info(f"Tree finished {time.time() - start}") - start = time.time() - self.groups[name]["elements"].update({e.GlobalId: e for e in elements}) - self.logger.info(f"Element metadata finished {time.time() - start}") - start = time.time() - - def create_object(self, group_name, id, shape): - obj = hppfcl.CollisionObject( - self.create_bvh(shape.geometry), self.create_transform(shape.transformation.matrix.data) - ) - self.groups[group_name]["objects"][id] = obj - - def collide_internal(self, name): - return self.collide_narrowphase(name, name, self.collide_broadphase(name, name)) - - def collide_group(self, name1, name2): - return self.collide_narrowphase(name1, name2, self.collide_broadphase(name1, name2)) - - def collide_broadphase(self, name1, name2): - import time - - start = time.time() - self.logger.info("Starting broadphase") - potential_collisions = [] - checked_collisions = set() - for id, element in self.groups[name1]["elements"].items(): - checked_collisions.add(id) - box_filter = self.tree.select_box(element) - pairs = [ - {"id1": id, "id2": e.GlobalId} - for e in box_filter - if e.GlobalId not in checked_collisions and e.GlobalId in self.groups[name2]["elements"] - ] - potential_collisions.extend(pairs) - self.logger.info(f"Finished broadphase {time.time() - start}") - return potential_collisions - - def collide_narrowphase(self, name1, name2, potential_collisions): - import time - - start = time.time() - self.logger.info("Starting narrowphase") - collisions = [] - for data in potential_collisions: - result = hppfcl.CollisionResult() - hppfcl.collide( - self.groups[name1]["objects"][data["id1"]], - self.groups[name2]["objects"][data["id2"]], - hppfcl.CollisionRequest(), - result, - ) - if result.isCollision(): - collisions.append({"id1": data["id1"], "id2": data["id2"], "collision": result}) - self.logger.info(f"Finished narrowphase {time.time() - start}") - return collisions - - def create_transform(self, m): - mat = np.array([[m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]]) - mat.transpose() - return hppfcl.Transform3f(mat[:3, :3], mat[:3, 3]) - - def create_bvh(self, mesh): - v = mesh.verts - f = mesh.faces - mesh_verts = np.array([[v[i], v[i + 1], v[i + 2]] for i in range(0, len(v), 3)]) - mesh_faces = [(int(f[i]), int(f[i + 1]), int(f[i + 2])) for i in range(0, len(f), 3)] - - bvh = hppfcl.BVHModelOBB() - bvh.beginModel(len(mesh_faces), len(mesh_verts)) - vertices = hppfcl.StdVec_Vec3f() - [vertices.append(v) for v in mesh_verts] - triangles = hppfcl.StdVec_Triangle() - [triangles.append(hppfcl.Triangle(f[0], f[1], f[2])) for f in mesh_faces] - bvh.addSubModel(vertices, triangles) - bvh.endModel() - return bvh diff --git a/src/ifcclash/ifcclash/ifcclash.py b/src/ifcclash/ifcclash/ifcclash.py index 1406f40aee..a967be3ce3 100644 --- a/src/ifcclash/ifcclash/ifcclash.py +++ b/src/ifcclash/ifcclash/ifcclash.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # IfcClash - IFC-based clash detection. -# Copyright (C) 2020, 2021 Dion Moult +# Copyright (C) 2020-2024 Dion Moult # # This file is part of IfcClash. # @@ -19,14 +19,13 @@ # along with IfcClash. If not, see . -import numpy as np import json -import sys +import time +import numpy as np import multiprocessing import ifcopenshell import ifcopenshell.geom import ifcopenshell.util.selector -from . import collider class Clasher: @@ -34,55 +33,78 @@ class Clasher: self.settings = settings self.geom_settings = ifcopenshell.geom.settings() self.clash_sets = [] - self.collider = collider.Collider(self.settings.logger) - self.selector = ifcopenshell.util.selector.Selector() + self.logger = self.settings.logger + self.groups = {} self.ifcs = {} + self.tree = None def clash(self): - existing_limit = sys.getrecursionlimit() for clash_set in self.clash_sets: self.process_clash_set(clash_set) def process_clash_set(self, clash_set): - self.collider.create_group("a") + self.tree = ifcopenshell.geom.tree() + self.create_group("a") for source in clash_set["a"]: source["ifc"] = self.load_ifc(source["file"]) self.add_collision_objects("a", source["ifc"], source.get("mode", None), source.get("selector", None)) - if "b" in clash_set: - self.collider.create_group("b") + if "b" in clash_set and clash_set["b"]: + self.create_group("b") for source in clash_set["b"]: source["ifc"] = self.load_ifc(source["file"]) self.add_collision_objects("b", source["ifc"], source.get("mode", None), source.get("selector", None)) - results = self.collider.collide_group("a", "b") + b = "b" else: - results = self.collider.collide_internal("a") + b = "a" + + mode = clash_set["mode"] + if mode == "intersection": + results = self.tree.clash_intersection_many( + list(self.groups["a"]["elements"].values()), + list(self.groups[b]["elements"].values()), + tolerance=clash_set["tolerance"], + check_all=clash_set["check_all"], + ) + elif mode == "collision": + results = self.tree.clash_collision_many( + list(self.groups["a"]["elements"].values()), + list(self.groups[b]["elements"].values()), + allow_touching=clash_set["allow_touching"], + ) + elif mode == "clearance": + results = self.tree.clash_clearance_many( + list(self.groups["a"]["elements"].values()), + list(self.groups[b]["elements"].values()), + clearance=clash_set["clearance"], + check_all=clash_set["check_all"], + ) processed_results = {} for result in results: - element1 = self.get_element(clash_set["a"], result["id1"]) - if "b" in clash_set: - element2 = self.get_element(clash_set["b"], result["id2"]) - else: - element2 = self.get_element(clash_set["a"], result["id2"]) + element1 = result.a + element2 = result.b - contact = result["collision"].getContacts()[0] - processed_results[f"{result['id1']}-{result['id2']}"] = { - "a_global_id": result["id1"], - "b_global_id": result["id2"], + processed_results[f"{element1.get_argument(0)}-{element2.get_argument(0)}"] = { + "a_global_id": element1.get_argument(0), + "b_global_id": element2.get_argument(0), "a_ifc_class": element1.is_a(), "b_ifc_class": element2.is_a(), - "a_name": element1.Name, - "b_name": element2.Name, - "normal": list(contact.normal), - "position": list(contact.pos), - "penetration_depth": contact.penetration_depth, + "a_name": element1.get_argument(2), + "b_name": element2.get_argument(2), + "type": ["protrusion", "pierce", "collision", "clearance"][result.clash_type], + "p1": list(result.p1), + "p2": list(result.p2), + "distance": result.distance, } clash_set["clashes"] = processed_results + self.logger.info(f"Found clashes: {len(processed_results.keys())}") + + def create_group(self, name): + self.logger.info(f"Creating group {name}") + self.groups[name] = {"elements": {}, "objects": {}} def load_ifc(self, path): - import time - start = time.time() self.settings.logger.info(f"Loading IFC {path}") ifc = self.ifcs.get(path, None) @@ -93,21 +115,35 @@ class Clasher: return ifc def add_collision_objects(self, name, ifc_file, mode=None, selector=None): - import time - start = time.time() self.settings.logger.info("Creating iterator") - if not mode: - elements = ifc_file.by_type("IfcElement") + if not mode or mode == "a" or not selector: + elements = set(ifc_file.by_type("IfcElement")) + elements -= set(ifc_file.by_type("IfcFeatureElement")) elif mode == "e": - elements = set(ifc_file.by_type("IfcElement")) - set(self.selector.parse(ifc_file, selector)) + elements = set(ifc_file.by_type("IfcElement")) + elements -= set(ifc_file.by_type("IfcFeatureElement")) + elements -= set(ifcopenshell.util.selector.filter_elements(ifc_file, selector)) elif mode == "i": - elements = self.selector.parse(ifc_file, selector) + elements = set(ifcopenshell.util.selector.filter_elements(ifc_file, selector)) iterator = ifcopenshell.geom.iterator( self.geom_settings, ifc_file, multiprocessing.cpu_count(), include=elements ) self.settings.logger.info(f"Iterator creation finished {time.time() - start}") - self.collider.create_objects(name, ifc_file, iterator, elements) + + start = time.time() + self.logger.info(f"Adding objects {name}") + assert iterator.initialize() + while True: + self.tree.add_element(iterator.get()) + shape = iterator.get() + if not iterator.next(): + break + self.logger.info(f"Tree finished {time.time() - start}") + start = time.time() + self.groups[name]["elements"].update({e.GlobalId: e for e in elements}) + self.logger.info(f"Element metadata finished {time.time() - start}") + start = time.time() def export(self): if len(self.settings.output) > 4 and self.settings.output[-4:] == ".bcf": @@ -123,7 +159,9 @@ class Clasher: title = f'{clash["a_ifc_class"]}/{clash["a_name"]} and {clash["b_ifc_class"]}/{clash["b_name"]}' topic = bcfxml.add_topic(title, title, "IfcClash") viewpoint = topic.add_viewpoint_from_point_and_guids( - np.array(clash["position"]), clash["a_global_id"], clash["b_global_id"], + np.array(clash["position"]), + clash["a_global_id"], + clash["b_global_id"], ) snapshot = self.get_viewpoint_snapshot(viewpoint) if snapshot: @@ -147,15 +185,6 @@ class Clasher: with open(self.settings.output, "w", encoding="utf-8") as clashes_file: json.dump(clash_sets, clashes_file, indent=4) - def get_element(self, clash_set, global_id): - for source in clash_set: - try: - element = source["ifc"].by_guid(global_id) - if element: - return element - except: - pass - def smart_group_clashes(self, clash_sets, max_clustering_distance): from sklearn.cluster import OPTICS from collections import defaultdict diff --git a/src/ifcclash/pyproject.toml b/src/ifcclash/pyproject.toml new file mode 100644 index 0000000000..148c16a0e4 --- /dev/null +++ b/src/ifcclash/pyproject.toml @@ -0,0 +1,29 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "ifcclash" +version = "0.0.999999" +authors = [ + { name="Dion Moult", email="dion@thinkmoult.com" }, +] +description = "IFC clash detection library to handle intersections, collisions, and clearance checks" +readme = "README.md" +keywords = ["IFC", "clash", "BIM"] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", +] +dependencies = [ + "ifcopenshell", +] + +[project.urls] +Homepage = "http://ifcopenshell.org" +Documentation = "https://docs.ifcopenshell.org" +Issues = "https://github.com/IfcOpenShell/IfcOpenShell/issues" + +[tool.setuptools.packages.find] +include = ["ifcclash"] +exclude = ["test*"] diff --git a/src/ifccsv/Makefile b/src/ifccsv/Makefile new file mode 100644 index 0000000000..b3cbbf8b49 --- /dev/null +++ b/src/ifccsv/Makefile @@ -0,0 +1,34 @@ +# IfcCSV - A utility to interact with IFC data through CSV. +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of IfcCSV. +# +# IfcCSV is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcCSV is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcCSV. If not, see . + +VERSION:=`date '+%y%m%d'` +SED:=sed -i +ifeq ($(UNAME_S),Darwin) +SED:=sed -i '' -e +endif + +.PHONY: qa +qa: + black . + +.PHONY: dist +dist: + rm -rf dist + $(SED) "s/999999/$(VERSION)/" pyproject.toml + python -m build + $(SED) "s/$(VERSION)/999999/" pyproject.toml diff --git a/src/ifccsv/ifccsv.py b/src/ifccsv/ifccsv.py index 474de0b93a..bcb7f4855f 100755 --- a/src/ifccsv/ifccsv.py +++ b/src/ifccsv/ifccsv.py @@ -29,6 +29,7 @@ import ifcopenshell.util.selector import ifcopenshell.util.element import ifcopenshell.util.schema from statistics import mean +from typing import Optional, Union try: from odf.namespaces import OFFICENS @@ -239,13 +240,19 @@ class IfcCsv: index = attributes.index(data["name"]) formatting_indices[index] = data["format"] - for row in self.results: - for index, format_query in formatting_indices.items(): + for index, format_query in formatting_indices.items(): + for row in self.results: if row[index] == null: continue row[index] = '"' + str(row[index]).replace('"', '\\"') + '"' row[index] = ifcopenshell.util.selector.format(format_query.replace("{{value}}", row[index])) + if self.summaries[index] is not None: + summary_label, summary_value = self.summaries[index].split(": ") + summary_value = '"' + str(summary_value).replace('"', '\\"') + '"' + summary_value = ifcopenshell.util.selector.format(format_query.replace("{{value}}", summary_value)) + self.summaries[index] = summary_label + ": " + str(summary_value) + def sort_results(self, sort, attributes, include_global_id): if not self.results: return @@ -269,7 +276,7 @@ class IfcCsv: elif not include_global_id: self.results = sorted(self.results, key=lambda x: x[0]) - def export_csv(self, output, delimiter=None): + def export_csv(self, output: str, delimiter: Optional[str] = None) -> None: with open(output, "w", newline="", encoding="utf-8") as f: writer = csv.writer(f, delimiter=delimiter) writer.writerow(self.headers) @@ -387,8 +394,16 @@ class IfcCsv: self.import_xlsx(ifc_file, table, attributes, null, empty, bool_true, bool_false) def import_csv( - self, ifc_file, table, attributes=None, delimiter=",", null="-", empty="", bool_true="YES", bool_false="NO" - ): + self, + ifc_file: ifcopenshell.file, + table: str, + attributes: Optional[list[Union[str, None]]] = None, + delimiter: str = ",", + null: str = "-", + empty: str = "", + bool_true: str = "YES", + bool_false: str = "NO", + ) -> None: with open(table, newline="", encoding="utf-8") as f: reader = csv.reader(f, delimiter=delimiter) headers = [] @@ -421,7 +436,17 @@ class IfcCsv: for _, row in df.iterrows(): self.process_row(ifc_file, row.tolist(), headers, attributes, null, empty, bool_true, bool_false) - def process_row(self, ifc_file, row, headers, attributes, null, empty, bool_true, bool_false): + def process_row( + self, + ifc_file: ifcopenshell.file, + row: list[str], + headers: list[str], + attributes: list[Union[str, None]], + null: str, + empty: str, + bool_true: str, + bool_false: str, + ) -> None: try: element = ifc_file.by_guid(row[0]) except: @@ -448,9 +473,7 @@ if __name__ == "__main__": parser.add_argument("-s", "--spreadsheet", type=str, default="data.csv", help="The spreadsheet file") parser.add_argument("-f", "--format", type=str, default="csv", help="The format, chosen from csv, ods, or xlsx") parser.add_argument("-d", "--delimiter", type=str, default=",", help="The delimiter in CSV. Defaults to a comma.") - parser.add_argument( - "-n", "--null", type=str, default="N/A", help="How to represent null values. Defaults to N/A." - ) + parser.add_argument("-n", "--null", type=str, default="N/A", help="How to represent null values. Defaults to N/A.") parser.add_argument( "-e", "--empty", type=str, default="-", help="How to represent empty strings. Defaults to a hyphen." ) diff --git a/src/ifccsv/pyproject.toml b/src/ifccsv/pyproject.toml new file mode 100644 index 0000000000..94d69c0ec7 --- /dev/null +++ b/src/ifccsv/pyproject.toml @@ -0,0 +1,28 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "ifccsv" +version = "0.0.999999" +authors = [ + { name="Dion Moult", email="dion@thinkmoult.com" }, +] +description = "IFC import and export from CSV, XLSX, ODS, and Pandas Dataframes" +readme = "README.md" +keywords = ["IFC", "CSV", "XLS", "XLSX", "ODS", "BIM"] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", +] +dependencies = [ + "ifcopenshell", +] + +[project.urls] +Homepage = "http://ifcopenshell.org" +Documentation = "https://docs.ifcopenshell.org" +Issues = "https://github.com/IfcOpenShell/IfcOpenShell/issues" + +[tool.setuptools] +py-modules = ["ifccsv"] diff --git a/src/ifcdiff/ifcdiff.py b/src/ifcdiff/ifcdiff.py index e41a9667ee..2ee8392f9d 100755 --- a/src/ifcdiff/ifcdiff.py +++ b/src/ifcdiff/ifcdiff.py @@ -54,7 +54,7 @@ class IfcDiff: that comparisons will take longer. :type is_shallow: bool :param filter_elements: An IFC filter query if you only want to compare a - subset of elements. For example: ``.IfcWall`` to only compare walls. + subset of elements. For example: ``IfcWall`` to only compare walls. :type filter_elements: string Example:: @@ -83,9 +83,12 @@ class IfcDiff: self.precision = self.get_precision() if self.filter_elements: - selector = ifcopenshell.util.selector.Selector() - old_elements = set(e.GlobalId for e in selector.parse(self.old, self.filter_elements)) - new_elements = set(e.GlobalId for e in selector.parse(self.new, self.filter_elements)) + old_elements = set( + e.GlobalId for e in ifcopenshell.util.selector.filter_elements(self.old, self.filter_elements) + ) + new_elements = set( + e.GlobalId for e in ifcopenshell.util.selector.filter_elements(self.new, self.filter_elements) + ) else: old_elements = self.old.by_type("IfcElement") if self.old.schema == "IFC2X3": @@ -277,7 +280,14 @@ class IfcDiff: return for relationship in self.relationships: if relationship == "type": - if ifcopenshell.util.element.get_type(old) != ifcopenshell.util.element.get_type(new): + old_type = ifcopenshell.util.element.get_type(old) + new_type = ifcopenshell.util.element.get_type(new) + if old_type is not None and new_type is not None: + if old_type.GlobalId != new_type.GlobalId: + self.change_register.setdefault(new.GlobalId, {}).update({"type_changed": True}) + return True + elif old_type != new_type: + # one of the types is None while the other is not None self.change_register.setdefault(new.GlobalId, {}).update({"type_changed": True}) return True elif relationship == "property": diff --git a/src/ifcgeom/IfcDerivedProfileDef.cpp b/src/ifcgeom/IfcDerivedProfileDef.cpp index 1bf666540a..2e022d887c 100644 --- a/src/ifcgeom/IfcDerivedProfileDef.cpp +++ b/src/ifcgeom/IfcDerivedProfileDef.cpp @@ -28,7 +28,19 @@ bool IfcGeom::Kernel::convert(const IfcSchema::IfcDerivedProfileDef* l, TopoDS_Shape& face) { TopoDS_Face f; gp_Trsf2d trsf2d; - if (convert_face(l->ParentProfile(), f) && IfcGeom::Kernel::convert(l->Operator(), trsf2d)) { + bool is_mirror = false; +#ifdef SCHEMA_HAS_IfcMirroredProfileDef + if (l->as()) { + trsf2d.SetMirror(gp::Origin2d()); + is_mirror = true; + } +#endif + if (!is_mirror) { + if (!IfcGeom::Kernel::convert(l->Operator(), trsf2d)) { + return false; + } + } + if (convert_face(l->ParentProfile(), f)) { gp_Trsf trsf = trsf2d; face = BRepBuilderAPI_Transform(f, trsf).Shape(); return true; diff --git a/src/ifcgeom/IfcGeom.cpp b/src/ifcgeom/IfcGeom.cpp index 72ebf0376e..ebbf24b89a 100644 --- a/src/ifcgeom/IfcGeom.cpp +++ b/src/ifcgeom/IfcGeom.cpp @@ -859,7 +859,9 @@ IfcSchema::IfcRepresentation* IfcGeom::Kernel::representation_mapped_to(const If } } } catch (const IfcParse::IfcException& e) { - Logger::Error(e); + // @todo the remove_boundingboxes() in IfcConvert leave dangling IfcRepresentationMap + // causing this error to be logged. + Logger::Error(e, representation); // @todo reset representation_mapped_to to zero? } return representation_mapped_to; diff --git a/src/ifcgeom/IfcGeomIteratorImplementation.h b/src/ifcgeom/IfcGeomIteratorImplementation.h index 5cf70522f9..1b5d9e8c6c 100644 --- a/src/ifcgeom/IfcGeomIteratorImplementation.h +++ b/src/ifcgeom/IfcGeomIteratorImplementation.h @@ -115,6 +115,7 @@ namespace IfcGeom { private: std::atomic finished_{ false }; + std::atomic terminating_{ false }; std::atomic progress_{ 0 }; std::vector tasks_; @@ -358,6 +359,10 @@ namespace IfcGeom { std::ref(settings), &rep); + if (terminating_) { + break; + } + threadpool.emplace_back(std::move(fu)); } @@ -367,8 +372,10 @@ namespace IfcGeom { finished_ = true; - Logger::Status("\rDone creating geometry (" + boost::lexical_cast(all_processed_elements_.size()) + - " objects) "); + if (!terminating_) { + Logger::Status("\rDone creating geometry (" + boost::lexical_cast(all_processed_elements_.size()) + + " objects) "); + } } /// Computes model's bounding box (bounds_min and bounds_max). @@ -1208,6 +1215,14 @@ namespace IfcGeom { } ~MAKE_TYPE_NAME(IteratorImplementation_)() { + if (num_threads_ != 1) { + terminating_ = true; + + if (init_future_.valid()) { + init_future_.wait(); + } + } + if (owns_ifc_file) { delete ifc_file; } diff --git a/src/ifcgeom_schema_agnostic/IfcGeomRepresentation.cpp b/src/ifcgeom_schema_agnostic/IfcGeomRepresentation.cpp index 7672eb381f..e30b9a3560 100644 --- a/src/ifcgeom_schema_agnostic/IfcGeomRepresentation.cpp +++ b/src/ifcgeom_schema_agnostic/IfcGeomRepresentation.cpp @@ -371,6 +371,11 @@ IfcGeom::Representation::Triangulation::Triangulation(const BRep& shape_model) triangles(i).Get(n3, n2, n1); else triangles(i).Get(n1, n2, n3); + if (dict[n1] == dict[n2] || dict[n2] == dict[n3] || dict[n3] == dict[n1]) { + Logger::Warning("Mesher generated a degenerate triangle, ignoring"); + continue; + } + /* An alternative would be to calculate normals based * on the coordinates of the mesh vertices */ /* diff --git a/src/ifcgeom_schema_agnostic/IfcGeomTree.h b/src/ifcgeom_schema_agnostic/IfcGeomTree.h index 8c7e99fde5..c72da5e59f 100644 --- a/src/ifcgeom_schema_agnostic/IfcGeomTree.h +++ b/src/ifcgeom_schema_agnostic/IfcGeomTree.h @@ -40,6 +40,31 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "clash_utils.h" + +#include "H5Cpp.h" + + namespace IfcGeom { struct ray_intersection_result { @@ -52,6 +77,15 @@ namespace IfcGeom { double dot_product; }; + struct clash { + int clash_type; // 0 = protrusion, 1 = pierce, 2 = collision, 3 = clearance + IfcUtil::IfcBaseClass* a; + IfcUtil::IfcBaseClass* b; + double distance; + std::array p1; + std::array p2; + }; + namespace { // Approximates the distance `other` protrudes into `volume` by finding the @@ -101,6 +135,740 @@ namespace IfcGeom { namespace impl { template class tree { + bool is_shape_manifold(const TopoDS_Shape& s) { + TopExp_Explorer exp(s, TopAbs_SHELL); + bool is_closed = false; + while (exp.More()) { + is_closed = true; + TopoDS_Shell shell = TopoDS::Shell(exp.Current()); + TopTools_IndexedDataMapOfShapeListOfShape edgeFaceMap; + TopExp::MapShapesAndAncestors(s, TopAbs_EDGE, TopAbs_FACE, edgeFaceMap); + + for (int i = 1; i <= edgeFaceMap.Extent(); ++i) { + if (edgeFaceMap(i).Extent() < 2) { + // This edge is not shared by two faces, indicating a potential opening + return false; + } + } + exp.Next(); + } + return is_closed; + } + + bool is_point_in_shape( + const gp_Pnt& v, + const opencascade::handle>& bvh, + const std::vector>& tris, + const std::vector& verts, + // In the case of "touching" rays, let's check again! + bool should_check_again = false + ) const { + ray v_ray; + v_ray.origin[0] = v.X(); + v_ray.origin[1] = v.Y(); + v_ray.origin[2] = v.Z(); + + if (should_check_again) { + // The first check may be incorrect if it intersects + // exactly between triangles or on edges of triangles. + // A second check is used to "double check" the results. + // The second check is perpendicular because AEC objects + // are typically symmetrical along an axis, and goes down + // because there's typically less stuff down there. + v_ray.dir[0] = 0.0f; + v_ray.dir[1] = 0.0f; + v_ray.dir[2] = -1.0f; + v_ray.dir_inv[0] = INFINITY; // 1.0f/dir[0] + v_ray.dir_inv[1] = INFINITY; // 1.0f/dir[1] + v_ray.dir_inv[2] = -1.0f; // 1.0f/dir[2] + } else { + v_ray.dir[0] = 1.0f; + v_ray.dir[1] = 0.0f; + v_ray.dir[2] = 0.0f; + v_ray.dir_inv[0] = 1.0f; // 1.0f/dir[0] + v_ray.dir_inv[1] = INFINITY; // 1.0f/dir[1] + v_ray.dir_inv[2] = INFINITY; // 1.0f/dir[2] + } + + gp_Vec ray_origin(v.X(), v.Y(), v.Z()); + gp_Vec ray_vector(v_ray.dir[0], v_ray.dir[1], v_ray.dir[2]); + + int total_intersections = 0; + + std::stack stack; + stack.push(0); + + while ( ! stack.empty()) { + int i = stack.top(); + stack.pop(); + + BVH_TreeBase::BVH_VecNt min_point = bvh->MinPoint(i); + BVH_TreeBase::BVH_VecNt max_point = bvh->MaxPoint(i); + + box box; + // + 1e-5 for tolerance + box.corners[0][0] = min_point[0] - 1e-5; + box.corners[0][1] = min_point[1] - 1e-5; + box.corners[0][2] = min_point[2] - 1e-5; + box.corners[1][0] = max_point[0] + 1e-5; + box.corners[1][1] = max_point[1] + 1e-5; + box.corners[1][2] = max_point[2] + 1e-5; + /* + std::cout << "Ray " + << v_ray.origin[0] << " " + << v_ray.origin[1] << " " + << v_ray.origin[2] << " " + << std::endl; + std::cout << "Box " + << min_point[0] << " " + << min_point[1] << " " + << min_point[2] << " " + << max_point[0] << " " + << max_point[1] << " " + << max_point[2] << " " + << std::endl; + */ + + if ( ! is_intersect_ray_box(&v_ray, &box)) { + continue; + } + //std::cout << "Ray hits box" << std::endl; + if (bvh->IsOuter(i)) { + //std::cout << "Ray hits leaf" << std::endl; + // Do ray triangle check. + for (int j=bvh->BegPrimitive(i); j<=bvh->EndPrimitive(i); ++j) { + const std::array& tri = tris[j]; + + gp_Vec ta(verts[tri[0]].XYZ()); + gp_Vec tb(verts[tri[1]].XYZ()); + gp_Vec tc(verts[tri[2]].XYZ()); + + /* + std::cout << "ray origin " << ray_origin.X() << " " << ray_origin.Y() << " " << ray_origin.Z() << std::endl; + std::cout << "inside-tri " << ta.X() << " " << ta.Y() << " " << ta.Z() << std::endl; + std::cout << "inside-tri " << tb.X() << " " << tb.Y() << " " << tb.Z() << std::endl; + std::cout << "inside-tri " << tc.X() << " " << tc.Y() << " " << tc.Z() << std::endl; + */ + double at, au, av; + if (intersectRayTriangle(ray_origin, ray_vector, ta, tb, tc, at, au, av, false)) { + if (std::abs(at) < 1e-4) { + // The point is basically lying on a face so inside/outside is ambiguous. + return false; + } + // At is a signed intersection distance (positive is along +ray_vector) + if (at > -1e-5) { + total_intersections++; + } + } + } + } else { + stack.push(bvh->Child<0>(i)); + stack.push(bvh->Child<1>(i)); + } + } + + return total_intersections % 2 != 0; + } + + std::tuple< + double, + std::array, + std::array + > pierce_shape( + const gp_Vec& e1, + const gp_Vec& e2, + const opencascade::handle>& bvh, + const std::vector>& tris, + const std::vector& verts, + const std::vector& normals + ) const { + const gp_Vec& ray_origin = e1; + gp_Vec ray_vector = e2 - e1; + double edge_length = ray_vector.Magnitude(); + + std::array min_int; + std::array max_int; + + ray_vector.Normalize(); + + ray v_ray; + v_ray.origin[0] = ray_origin.X(); + v_ray.origin[1] = ray_origin.Y(); + v_ray.origin[2] = ray_origin.Z(); + + v_ray.dir[0] = ray_vector.X(); + v_ray.dir[1] = ray_vector.Y(); + v_ray.dir[2] = ray_vector.Z(); + v_ray.dir_inv[0] = 1.0f / ray_vector.X(); + v_ray.dir_inv[1] = 1.0f / ray_vector.Y(); + v_ray.dir_inv[2] = 1.0f / ray_vector.Z(); + + double min_distance = std::numeric_limits::infinity(); + double max_distance = -std::numeric_limits::infinity(); + + std::stack stack; + stack.push(0); + + while ( ! stack.empty()) { + int i = stack.top(); + stack.pop(); + + BVH_TreeBase::BVH_VecNt min_point = bvh->MinPoint(i); + BVH_TreeBase::BVH_VecNt max_point = bvh->MaxPoint(i); + + box box; + // + 1e-5 for tolerance + box.corners[0][0] = min_point[0] - 1e-5; + box.corners[0][1] = min_point[1] - 1e-5; + box.corners[0][2] = min_point[2] - 1e-5; + box.corners[1][0] = max_point[0] + 1e-5; + box.corners[1][1] = max_point[1] + 1e-5; + box.corners[1][2] = max_point[2] + 1e-5; + + if ( ! is_intersect_ray_box(&v_ray, &box)) { + continue; + } + if (bvh->IsOuter(i)) { + // Do ray triangle check. + for (int j=bvh->BegPrimitive(i); j<=bvh->EndPrimitive(i); ++j) { + const std::array& tri = tris[j]; + const gp_Vec& normal = normals[j]; + + if (std::abs(normal.Dot(ray_vector)) < 1e-3) { + continue; // This ray is coplanar to the triangle + } + + gp_Vec ta(verts[tri[0]].XYZ()); + gp_Vec tb(verts[tri[1]].XYZ()); + gp_Vec tc(verts[tri[2]].XYZ()); + + double at, au, av; + // Do box check first? + if (intersectRayTriangle(ray_origin, ray_vector, ta, tb, tc, at, au, av, false)) { + // At is a signed intersection distance (positive is along +ray_vector) + if (at > 0 && at < edge_length) { + double aw = 1.0f - au - av; // Barycentric coordinate for ta + gp_Vec int_vec = aw * ta + au * tb + av * tc; // Intersection point + + if ( + is_point_on_line(int_vec, ta, tb) + || is_point_on_line(int_vec, ta, tc) + || is_point_on_line(int_vec, tb, tc) + || (ta - int_vec).Magnitude() < 1e-4 + || (tb - int_vec).Magnitude() < 1e-4 + || (tc - int_vec).Magnitude() < 1e-4 + ) { + continue; + } + + if (at < min_distance) { + min_distance = at; + min_int = {int_vec.X(), int_vec.Y(), int_vec.Z()}; + } + if (at > max_distance) { + max_distance = at; + max_int = {int_vec.X(), int_vec.Y(), int_vec.Z()}; + } + } + } + } + } else { + stack.push(bvh->Child<0>(i)); + stack.push(bvh->Child<1>(i)); + } + } + + if (min_distance == std::numeric_limits::infinity()) { + return std::make_tuple(-1, min_int, max_int); + } + return std::make_tuple(max_distance - min_distance, min_int, max_int); + } + + bool is_point_on_line(const gp_Pnt& point, const gp_Pnt& lineStart, const gp_Pnt& lineEnd) const { + // Create vectors + gp_Vec startToPoint(point.XYZ() - lineStart.XYZ()); + gp_Vec startToEnd(lineEnd.XYZ() - lineStart.XYZ()); + + // Check if the point is on the line defined by start and end + // by checking if the cross product is (near) zero vector, indicating collinearity. + gp_Vec crossProduct = startToPoint.Crossed(startToEnd); + if (crossProduct.Magnitude() > 1e-5) { + return false; // Not collinear, hence not on the line segment + } + return true; // The point is on the line segment + } + + // Vec variant? This _Pnt and _Vec difference is annoying. + bool is_point_on_line(const gp_Vec& point, const gp_Vec& lineStart, const gp_Vec& lineEnd) const { + // Create vectors + gp_Vec startToPoint = point - lineStart; + gp_Vec startToEnd = lineEnd - lineStart; + + // Check if the point is on the line defined by start and end + // by checking if the cross product is (near) zero vector, indicating collinearity. + gp_Vec crossProduct = startToPoint.Crossed(startToEnd); + if (crossProduct.Magnitude() > 1e-5) { + return false; // Not collinear, hence not on the line segment + } + return true; // The point is on the line segment + } + + std::unordered_map> clash_bvh( + opencascade::handle> bvh_a, + opencascade::handle> bvh_b, + double extend = 0.0 + ) const { + std::unordered_map> bvh_clashes; + for (int i=0; iLength(); ++i) { + if ( ! bvh_a->IsOuter(i)) { + continue; + } + + BVH_TreeBase::BVH_VecNt bvh_a_min = bvh_a->MinPoint(i); + BVH_TreeBase::BVH_VecNt bvh_a_max = bvh_a->MaxPoint(i); + bvh_a_min[0] -= 1e-3; + bvh_a_min[1] -= 1e-3; + bvh_a_min[2] -= 1e-3; + bvh_a_max[0] += 1e-3; + bvh_a_max[1] += 1e-3; + bvh_a_max[2] += 1e-3; + + BVH_Box box_a(bvh_a_min, bvh_a_max); + + std::stack stack; + stack.push(0); + + while ( ! stack.empty()) { + int j = stack.top(); + stack.pop(); + + BVH_TreeBase::BVH_VecNt bvh_b_min = bvh_b->MinPoint(j); + BVH_TreeBase::BVH_VecNt bvh_b_max = bvh_b->MaxPoint(j); + bvh_b_min[0] -= extend + 1e-3; + bvh_b_min[1] -= extend + 1e-3; + bvh_b_min[2] -= extend + 1e-3; + bvh_b_max[0] += extend + 1e-3; + bvh_b_max[1] += extend + 1e-3; + bvh_b_max[2] += extend + 1e-3; + + if (box_a.IsOut(bvh_b_min, bvh_b_max)) { + continue; + } + if (bvh_b->IsOuter(j)) { + if (bvh_clashes.find(i) != bvh_clashes.end()) { + bvh_clashes[i].push_back(j); + } else { + bvh_clashes[i] = {j}; + } + } else { + stack.push(bvh_b->Child<0>(j)); + stack.push(bvh_b->Child<1>(j)); + } + } + } + return bvh_clashes; + } + + clash test_intersection(const T& tA, const T& tB, double tolerance, bool check_all = true) const { + // If there are verts of A inside shape B (protrusion): + // 1. For each vert, find the shortest distance to the closest face + // 2. Find the innermost vert (i.e. the vert that has the longest distance) + // Otherwise (piercing): + // 1. Intersect each edge with shape B + // 2. Find the longest distance between intersections + + auto obb_b = obbs_.find(tB)->second; + obb_b.Enlarge(-tolerance); + + // No need to search beyond the distance of the max protrusion. + const double max_protrusion = max_protrusions_.find(tB)->second; + + // Collide BVH trees of shape A vs B + opencascade::handle> bvh_a = bvhs_.find(tA)->second; + opencascade::handle> bvh_b = bvhs_.find(tB)->second; + + std::unordered_map> bvh_clashes = clash_bvh(bvh_a, bvh_b, max_protrusion); + if (bvh_clashes.empty()) { + return {-1, tA, tB, 0, {0, 0, 0}, {0, 0, 0}}; + } + + const std::vector>& tris_a = tris_.find(tA)->second; + const std::vector>& tris_b = tris_.find(tB)->second; + const std::vector& verts_a = verts_.find(tA)->second; + const std::vector& verts_b = verts_.find(tB)->second; + const std::vector& normals_a = normals_.find(tA)->second; + const std::vector& normals_b = normals_.find(tB)->second; + + // ~10% faster? + std::unordered_set points_in_b_cache; + std::unordered_set points_not_in_b_cache; + + double protrusion = -std::numeric_limits::infinity(); + std::array protrusion_point; + std::array surface_point; + + double pierce = -std::numeric_limits::infinity(); + std::array pierce_point1; + std::array pierce_point2; + + for (const auto& pair : bvh_clashes) { + const int bvh_a_i = pair.first; + const std::vector& bvh_b_is = pair.second; + + for (int i=bvh_a->BegPrimitive(bvh_a_i); i<=bvh_a->EndPrimitive(bvh_a_i); ++i) { + const std::array& tri = tris_a[i]; + std::vector points_in_b; + + for (int v_id : tri) { + if (points_not_in_b_cache.find(v_id) != points_not_in_b_cache.end()) { + continue; + } + + const gp_Pnt& v = verts_a[v_id]; + + if (points_in_b_cache.find(v_id) != points_in_b_cache.end()) { + points_in_b.push_back(v); + continue; + } + + if (obb_b.IsOut(v)) { + points_not_in_b_cache.insert(v_id); + continue; + } + + if (is_point_in_shape(v, bvh_b, tris_b, verts_b) + && is_point_in_shape(v, bvh_b, tris_b, verts_b, true)) { + points_in_b.push_back(v); + points_in_b_cache.insert(v_id); + } else { + points_not_in_b_cache.insert(v_id); + } + } + + // If there are no points in b, this may be a "piercing" triangle. + if (points_in_b.empty()) { + gp_Vec v1_a_vec(verts_a[tri[0]].XYZ()); + gp_Vec v2_a_vec(verts_a[tri[1]].XYZ()); + gp_Vec v3_a_vec(verts_a[tri[2]].XYZ()); + + // Protrusions take priority over piercings. We only check for piercings if: + // - This is a piercing triangle (e.g. no points in b) + // - No protrusion was already found + // - We haven't yet found a piercing at the max protrusion limit + if (protrusion == -std::numeric_limits::infinity() && pierce != max_protrusion) { + std::array< + std::tuple, std::array>, 3 + > pierce_results = { + pierce_shape(v1_a_vec, v2_a_vec, bvh_b, tris_b, verts_b, normals_b), + pierce_shape(v1_a_vec, v3_a_vec, bvh_b, tris_b, verts_b, normals_b), + pierce_shape(v2_a_vec, v3_a_vec, bvh_b, tris_b, verts_b, normals_b) + }; + + for (const auto& pr : pierce_results) { + auto& p_dist = std::get<0>(pr); + auto& p_min = std::get<1>(pr); + auto& p_max = std::get<2>(pr); + if (p_dist > tolerance && p_dist > pierce) { + // Piercings are capped at max_protrusion for intuitive results + pierce = std::min(p_dist, max_protrusion); + pierce_point1 = p_min; + pierce_point2 = p_max; + if ( ! check_all) { + return {1, tA, tB, pierce, pierce_point1, pierce_point2}; + } + } + } + } + + // Since there were no points in b, we don't need to check for protrusions. + continue; + } + + const gp_Vec& normal_a = normals_a[i]; + double v_protrusion = std::numeric_limits::infinity(); + std::array v_protrusion_point; + std::array v_surface_point; + + // Check for protrusions. + for (const auto& bvh_b_i : bvh_b_is) { + for (int j=bvh_b->BegPrimitive(bvh_b_i); j<=bvh_b->EndPrimitive(bvh_b_i); ++j) { + const std::array& tri = tris_b[j]; + const gp_Vec& normal_b = normals_b[j]; + + tri_count_++; + + // We're penetrating _into_ a shape, so don't + // compare distances to faces with roughly the + // same normal as the penetration. + if (normal_a.Dot(normal_b) >= 0.9f) { + continue; + } + + gp_Vec ta(verts_b[tri[0]].XYZ()); + gp_Vec tb(verts_b[tri[1]].XYZ()); + gp_Vec tc(verts_b[tri[2]].XYZ()); + + for (const auto& v : points_in_b) { + gp_Vec ray_origin(v.XYZ()); + + /* + std::cout << "POINT IN B " << v.X() << " " << v.Y() << " " << v.Z() << std::endl; + std::cout << "dir-> " << normal_b.X() << " " << normal_b.Y() << " " << normal_b.Z() << std::endl; + std::cout << "->tri " << v1_b[0] << " " << v1_b[1] << " " << v1_b[2] << std::endl; + std::cout << "->tri " << v2_b[0] << " " << v2_b[1] << " " << v2_b[2] << std::endl; + std::cout << "->tri " << v3_b[0] << " " << v3_b[1] << " " << v3_b[2] << std::endl; + */ + + // Do (cheaper) line check. + double at, au, av; + if (intersectRayTriangle(ray_origin, normal_b, ta, tb, tc, at, au, av, false)) { + double current_v_protrusion = at; + + // std::cout << "We got a current protrusion " << current_v_protrusion << std::endl; + if (current_v_protrusion < v_protrusion) { + double aw = 1.0f - au - av; // Barycentric coordinate for ta + gp_Vec point_on_b = aw * ta + au * tb + av * tc; // Intersection point + // std::cout << "New v_protrusion winner of " << current_v_protrusion << std::endl; + v_protrusion = current_v_protrusion; + v_protrusion_point = {v.X(), v.Y(), v.Z()}; + v_surface_point = {point_on_b.X(), point_on_b.Y(), point_on_b.Z()}; + + if ( ! check_all && v_protrusion > tolerance) { + return {0, tA, tB, v_protrusion, v_protrusion_point, v_surface_point}; + } + } + } + } + } + } + + if (v_protrusion != std::numeric_limits::infinity()) { + if (v_protrusion > protrusion) { + // std::cout << "New actual protrusion winner of " << v_protrusion << std::endl; + protrusion = v_protrusion; + protrusion_point = v_protrusion_point; + surface_point = v_surface_point; + if (protrusion > (max_protrusion - 1e-3)) { + return {0, tA, tB, protrusion, protrusion_point, surface_point}; + } + } + } + } + } + + if (protrusion > tolerance) { + return {0, tA, tB, protrusion, protrusion_point, surface_point}; + } + + if (pierce > tolerance) { + return {1, tA, tB, pierce, pierce_point1, pierce_point2}; + } + + return {-1, tA, tB, 0, {0, 0, 0}, {0, 0, 0}}; + } + + clash test_collision(const T& tA, const T& tB, bool allow_touching) const { + // Collide BVH trees of shape A vs B + opencascade::handle> bvh_a = bvhs_.find(tA)->second; + opencascade::handle> bvh_b = bvhs_.find(tB)->second; + + std::unordered_map> bvh_clashes = clash_bvh(bvh_a, bvh_b); + if (bvh_clashes.empty()) { + return {-1, tA, tB, 0, {0, 0, 0}, {0, 0, 0}}; + } + + const std::vector>& tris_a = tris_.find(tA)->second; + const std::vector>& tris_b = tris_.find(tB)->second; + const std::vector& verts_a = verts_.find(tA)->second; + const std::vector& verts_b = verts_.find(tB)->second; + const std::vector& normals_a = normals_.find(tA)->second; + const std::vector& normals_b = normals_.find(tB)->second; + + for (const auto& pair : bvh_clashes) { + const int bvh_a_i = pair.first; + const std::vector& bvh_b_is = pair.second; + + for (int i=bvh_a->BegPrimitive(bvh_a_i); i<=bvh_a->EndPrimitive(bvh_a_i); ++i) { + const std::array& tri = tris_a[i]; + const gp_Pnt& v1_a_pnt = verts_a[tri[0]]; + const gp_Pnt& v2_a_pnt = verts_a[tri[1]]; + const gp_Pnt& v3_a_pnt = verts_a[tri[2]]; + const gp_Vec& normal_a = normals_a[i]; + + const gp_Vec v1_a_vec(v1_a_pnt.XYZ()); + const gp_Vec v2_a_vec(v2_a_pnt.XYZ()); + const gp_Vec v3_a_vec(v3_a_pnt.XYZ()); + + for (const auto& bvh_b_i : bvh_b_is) { + for (int j=bvh_b->BegPrimitive(bvh_b_i); j<=bvh_b->EndPrimitive(bvh_b_i); ++j) { + const std::array& tri = tris_b[j]; + const gp_Pnt& v1_b_pnt = verts_b[tri[0]]; + const gp_Pnt& v2_b_pnt = verts_b[tri[1]]; + const gp_Pnt& v3_b_pnt = verts_b[tri[2]]; + const gp_Vec& normal_b = normals_b[j]; + + tri_count_++; + + const gp_Vec v1_b_vec(v1_b_pnt.XYZ()); + const gp_Vec v2_b_vec(v2_b_pnt.XYZ()); + const gp_Vec v3_b_vec(v3_b_pnt.XYZ()); + + // Allow a deviation of 0.25 degrees in coplanarity check + if (std::abs(normal_a.Dot(normal_b)) >= 0.99999f) { + continue; + } + + gp_Vec int1, int2; + if (trianglesIntersect(v1_a_vec, v2_a_vec, v3_a_vec, v1_b_vec, v2_b_vec, v3_b_vec, int1, int2, ! allow_touching)) { + if (allow_touching) { + return {2, tA, tB, 0, {int1.X(), int1.Y(), int1.Z()}, {int2.X(), int2.Y(), int2.Z()}}; + } + + // A non-touching collision is defined as two triangles that: + // 1. Are not coplanar + // 2. The point of intersection is not along the edge of triangle A. + // 3. The point of intersection is not a vertex of triangle B. + + if ( + ! is_point_on_line(int1, v1_a_vec, v2_a_vec) + && ! is_point_on_line(int1, v1_a_vec, v3_a_vec) + && ! is_point_on_line(int1, v2_a_vec, v3_a_vec) + ) { + if ( + (v1_b_vec - int1).Magnitude() > 1e-4 + && (v2_b_vec - int1).Magnitude() > 1e-4 + && (v3_b_vec - int1).Magnitude() > 1e-4 + ) { + return {2, tA, tB, 0, {int1.X(), int1.Y(), int1.Z()}, {int2.X(), int2.Y(), int2.Z()}}; + } + } + + if ( + ! is_point_on_line(int1, v1_b_vec, v2_b_vec) + && ! is_point_on_line(int1, v1_b_vec, v3_b_vec) + && ! is_point_on_line(int1, v2_b_vec, v3_b_vec) + ) { + if ( + (v1_a_vec - int1).Magnitude() > 1e-4 + && (v2_a_vec - int1).Magnitude() > 1e-4 + && (v3_a_vec - int1).Magnitude() > 1e-4 + ) { + return {2, tA, tB, 0, {int1.X(), int1.Y(), int1.Z()}, {int2.X(), int2.Y(), int2.Z()}}; + } + } + + if ( + ! is_point_on_line(int2, v1_a_vec, v2_a_vec) + && ! is_point_on_line(int2, v1_a_vec, v3_a_vec) + && ! is_point_on_line(int2, v2_a_vec, v3_a_vec) + ) { + if ( + (v1_b_vec - int2).Magnitude() > 1e-4 + && (v2_b_vec - int2).Magnitude() > 1e-4 + && (v3_b_vec - int2).Magnitude() > 1e-4 + ) { + return {2, tA, tB, 0, {int2.X(), int2.Y(), int2.Z()}, {int1.X(), int1.Y(), int1.Z()}}; + } + } + + if ( + ! is_point_on_line(int2, v1_b_vec, v2_b_vec) + && ! is_point_on_line(int2, v1_b_vec, v3_b_vec) + && ! is_point_on_line(int2, v2_b_vec, v3_b_vec) + ) { + if ( + (v1_a_vec - int2).Magnitude() > 1e-4 + && (v2_a_vec - int2).Magnitude() > 1e-4 + && (v3_a_vec - int2).Magnitude() > 1e-4 + ) { + return {2, tA, tB, 0, {int2.X(), int2.Y(), int2.Z()}, {int1.X(), int1.Y(), int1.Z()}}; + } + } + } + } + } + } + } + return {-1, tA, tB, 0, {0, 0, 0}, {0, 0, 0}}; + } + + clash test_clearance(const T& tA, const T& tB, double clearance, bool check_all) const { + // Collide BVH trees of shape A vs B + opencascade::handle> bvh_a = bvhs_.find(tA)->second; + opencascade::handle> bvh_b = bvhs_.find(tB)->second; + + std::unordered_map> bvh_clashes = clash_bvh(bvh_a, bvh_b, clearance); + if (bvh_clashes.empty()) { + return {-1, tA, tB, 0, {0, 0, 0}, {0, 0, 0}}; + } + + const std::vector>& tris_a = tris_.find(tA)->second; + const std::vector>& tris_b = tris_.find(tB)->second; + const std::vector& verts_a = verts_.find(tA)->second; + const std::vector& verts_b = verts_.find(tB)->second; + + double min_clearance = std::numeric_limits::infinity(); + std::array clearance_point1; + std::array clearance_point2; + + for (const auto& pair : bvh_clashes) { + const int bvh_a_i = pair.first; + const std::vector& bvh_b_is = pair.second; + + for (int i=bvh_a->BegPrimitive(bvh_a_i); i<=bvh_a->EndPrimitive(bvh_a_i); ++i) { + const std::array& tri = tris_a[i]; + const gp_Pnt& v1_a_pnt = verts_a[tri[0]]; + const gp_Pnt& v2_a_pnt = verts_a[tri[1]]; + const gp_Pnt& v3_a_pnt = verts_a[tri[2]]; + + const gp_Vec v1_a_vec(v1_a_pnt.XYZ()); + const gp_Vec v2_a_vec(v2_a_pnt.XYZ()); + const gp_Vec v3_a_vec(v3_a_pnt.XYZ()); + + const std::array p = {v1_a_vec, v2_a_vec, v3_a_vec}; + + for (const auto& bvh_b_i : bvh_b_is) { + for (int j=bvh_b->BegPrimitive(bvh_b_i); j<=bvh_b->EndPrimitive(bvh_b_i); ++j) { + const std::array& tri = tris_b[j]; + const gp_Pnt& v1_b_pnt = verts_b[tri[0]]; + const gp_Pnt& v2_b_pnt = verts_b[tri[1]]; + const gp_Pnt& v3_b_pnt = verts_b[tri[2]]; + + tri_count_++; + + const gp_Vec v1_b_vec(v1_b_pnt.XYZ()); + const gp_Vec v2_b_vec(v2_b_pnt.XYZ()); + const gp_Vec v3_b_vec(v3_b_pnt.XYZ()); + + const std::array q = {v1_b_vec, v2_b_vec, v3_b_vec}; + + gp_Vec cp; + gp_Vec cq; + + // https://stackoverflow.com/questions/53602907/algorithm-to-find-minimum-distance-between-two-triangles + distanceTriangleTriangleSquared(cp, cq, p, q); + + double distance = (cq - cp).Magnitude(); + if (distance < clearance && distance < min_clearance) { + min_clearance = distance; + clearance_point1 = {cp.X(), cp.Y(), cp.Z()}; + clearance_point2 = {cq.X(), cq.Y(), cq.Z()}; + if ( ! check_all || min_clearance < 1e-4) { + return {3, tA, tB, min_clearance, clearance_point1, clearance_point2}; + } + } + } + } + } + } + + if (min_clearance < clearance) { + return {3, tA, tB, min_clearance, clearance_point1, clearance_point2}; + + } + + return {-1, tA, tB, 0, {0, 0, 0}, {0, 0, 0}}; + } bool test(const TopoDS_Shape& A, const TopoDS_Shape& B, bool completely_within, double extend) const { if (extend > 0.) { @@ -143,6 +911,7 @@ namespace IfcGeom { // @todo this is ugly, embed this in the return type mutable std::vector distances_; mutable std::vector protrusion_distances_; + mutable long long tri_count_ = 0; public: @@ -211,6 +980,333 @@ namespace IfcGeom { } } + std::unique_ptr> build_box_set(const std::vector& elements) const { + double x, y, z, X, Y, Z; + std::unique_ptr> box_set = std::make_unique>(); + for (int i=0; isecond; + aabb.Get(x, y, z, X, Y, Z); + const BVH_Box::BVH_VecNt min(x, y, z); + const BVH_Box::BVH_VecNt max(X, Y, Z); + BVH_Box bvh_box(min, max); + box_set->Add(i, bvh_box); + } + return box_set; + } + + struct clash_task { + T a, b; + }; + + std::vector> allocate_tasks_to_threads( + std::vector& task_queue) const { + int num_threads = std::thread::hardware_concurrency(); + std::vector> threaded_tasks(num_threads); + + size_t tasks_per_thread = task_queue.size() / num_threads; + for (int i = 0; i < num_threads; ++i) { + auto startIter = std::next(task_queue.begin(), i * tasks_per_thread); + auto endIter = (i == num_threads - 1) ? task_queue.end() : std::next(startIter, tasks_per_thread); + threaded_tasks[i] = std::vector(startIter, endIter); + } + return threaded_tasks; + } + + std::vector clash_intersection_many( + const std::vector& set_a, const std::vector& set_b, + double tolerance = 0.002, bool check_all = true + ) const { + std::vector task_queue; + std::vector results; + + std::unique_ptr> box_set_a = build_box_set(set_a); + std::unique_ptr> box_set_b = build_box_set(set_b); + + const opencascade::handle>& bvh_a = box_set_a->BVH(); + const opencascade::handle>& bvh_b = box_set_b->BVH(); + + std::unordered_map> bvh_clashes = clash_bvh(bvh_a, bvh_b, 0.0); + + if (bvh_clashes.empty()) { + return results; + } + + std::map> tested_pairs; + + for (const auto& pair : bvh_clashes) { + const int bvh_a_i = pair.first; + const std::vector& bvh_b_is = pair.second; + for (int i=bvh_a->BegPrimitive(bvh_a_i); i<=bvh_a->EndPrimitive(bvh_a_i); ++i) { + const T& t_a = set_a[box_set_a->Element(i)]; + for (const auto& bvh_b_i : bvh_b_is) { + for (int j=bvh_b->BegPrimitive(bvh_b_i); j<=bvh_b->EndPrimitive(bvh_b_i); ++j) { + const T& t_b = set_b[box_set_b->Element(j)]; + if (t_a == t_b) { + continue; + } + + if (tested_pairs[t_a].insert(t_b).second) { + tested_pairs[t_b].insert(t_a).second; + } else { + continue; + } + + task_queue.emplace_back(clash_task{t_a, t_b}); + } + } + } + } + + std::vector> threaded_tasks = allocate_tasks_to_threads(task_queue); + + std::vector threads; + std::mutex results_mutex; + + for (auto& tasks : threaded_tasks) { + threads.emplace_back([this, &tasks, &results, &results_mutex, tolerance, check_all] { + std::vector thread_results; + for (auto& task : tasks) { + const auto& obb_a = obbs_.find(task.a)->second; + auto obb_b = obbs_.find(task.b)->second; + obb_b.Enlarge(-tolerance); + if (obb_a.IsOut(obb_b)) { + continue; + } + + bool has_clash = false; + bool is_manifold = false; + clash result; + + if (is_manifold_.find(task.b)->second) { + is_manifold = true; + clash intersection = test_intersection(task.a, task.b, tolerance, check_all); + if (intersection.clash_type != -1) { + has_clash = true; + result = intersection; + if ( ! check_all) { + thread_results.push_back(result); + continue; + } + } + } + + if (is_manifold_.find(task.a)->second) { + is_manifold = true; + clash intersection = test_intersection(task.b, task.a, tolerance, check_all); + if (intersection.clash_type != -1) { + // Replace the clash result if any of these criteria apply: + // - We don't have a clash yet + // - Our previous clash is piercing, and our new one is a protrusion + // - We have the same clash type, but our clash is more severe + if ( + ! has_clash + || (result.clash_type == 1 && intersection.clash_type == 0) + || ( + result.clash_type == intersection.clash_type + && intersection.distance > result.distance + ) + ) { + has_clash = true; + result = intersection; + } + } + } + + if ( ! is_manifold) { + clash collision = test_collision(task.a, task.b, false); + if (collision.clash_type != -1) { + has_clash = true; + result = collision; + } + } + + if (has_clash) { + thread_results.push_back(result); + } + } + { + std::lock_guard lock(results_mutex); + results.insert(results.end(), thread_results.begin(), thread_results.end()); + } + }); + } + + for (auto& thread : threads) { + if (thread.joinable()) { + thread.join(); + } + } + + return results; + } + + std::vector clash_collision_many( + const std::vector& set_a, const std::vector& set_b, bool allow_touching = false + ) const { + std::vector task_queue; + std::vector results; + + std::unique_ptr> box_set_a = build_box_set(set_a); + std::unique_ptr> box_set_b = build_box_set(set_b); + + const opencascade::handle>& bvh_a = box_set_a->BVH(); + const opencascade::handle>& bvh_b = box_set_b->BVH(); + + std::unordered_map> bvh_clashes = clash_bvh(bvh_a, bvh_b, 0.0); + + if (bvh_clashes.empty()) { + return results; + } + + std::map> tested_pairs; + + for (const auto& pair : bvh_clashes) { + const int bvh_a_i = pair.first; + const std::vector& bvh_b_is = pair.second; + for (int i=bvh_a->BegPrimitive(bvh_a_i); i<=bvh_a->EndPrimitive(bvh_a_i); ++i) { + const T& t_a = set_a[box_set_a->Element(i)]; + for (const auto& bvh_b_i : bvh_b_is) { + for (int j=bvh_b->BegPrimitive(bvh_b_i); j<=bvh_b->EndPrimitive(bvh_b_i); ++j) { + const T& t_b = set_b[box_set_b->Element(j)]; + if (t_a == t_b) { + continue; + } + + if (tested_pairs[t_a].insert(t_b).second) { + tested_pairs[t_b].insert(t_a).second; + } else { + continue; + } + + task_queue.emplace_back(clash_task{t_a, t_b}); + } + } + } + } + + std::vector> threaded_tasks = allocate_tasks_to_threads(task_queue); + + std::vector threads; + std::mutex results_mutex; + + for (auto& tasks : threaded_tasks) { + threads.emplace_back([this, &tasks, &results, &results_mutex, allow_touching] { + std::vector thread_results; + for (auto& task : tasks) { + const auto& obb_a = obbs_.find(task.a)->second; + auto obb_b = obbs_.find(task.b)->second; + obb_b.Enlarge(-0.001); + if (obb_a.IsOut(obb_b)) { + continue; + } + + clash result = test_collision(task.a, task.b, allow_touching); + if (result.clash_type != -1) { + thread_results.push_back(result); + } + } + { + std::lock_guard lock(results_mutex); + results.insert(results.end(), thread_results.begin(), thread_results.end()); + } + }); + } + + for (auto& thread : threads) { + if (thread.joinable()) { + thread.join(); + } + } + + return results; + } + + std::vector clash_clearance_many( + const std::vector& set_a, const std::vector& set_b, + double clearance = 0.05, bool check_all = false + ) const { + std::vector task_queue; + std::vector results; + + std::unique_ptr> box_set_a = build_box_set(set_a); + std::unique_ptr> box_set_b = build_box_set(set_b); + + const opencascade::handle>& bvh_a = box_set_a->BVH(); + const opencascade::handle>& bvh_b = box_set_b->BVH(); + + std::unordered_map> bvh_clashes = clash_bvh(bvh_a, bvh_b, clearance); + + if (bvh_clashes.empty()) { + return results; + } + + std::map> tested_pairs; + + for (const auto& pair : bvh_clashes) { + const int bvh_a_i = pair.first; + const std::vector& bvh_b_is = pair.second; + for (int i=bvh_a->BegPrimitive(bvh_a_i); i<=bvh_a->EndPrimitive(bvh_a_i); ++i) { + const T& t_a = set_a[box_set_a->Element(i)]; + for (const auto& bvh_b_i : bvh_b_is) { + for (int j=bvh_b->BegPrimitive(bvh_b_i); j<=bvh_b->EndPrimitive(bvh_b_i); ++j) { + const T& t_b = set_b[box_set_b->Element(j)]; + if (t_a == t_b) { + continue; + } + + if (tested_pairs[t_a].insert(t_b).second) { + tested_pairs[t_b].insert(t_a).second; + } else { + continue; + } + + task_queue.emplace_back(clash_task{t_a, t_b}); + } + } + } + } + + std::vector> threaded_tasks = allocate_tasks_to_threads(task_queue); + + std::vector threads; + std::mutex results_mutex; + + for (auto& tasks : threaded_tasks) { + threads.emplace_back([this, &tasks, &results, &results_mutex, clearance, check_all] { + std::vector thread_results; + for (auto& task : tasks) { + const auto& obb_a = obbs_.find(task.a)->second; + auto obb_b = obbs_.find(task.b)->second; + obb_b.Enlarge(clearance); + if (obb_a.IsOut(obb_b)) { + continue; + } + + clash result = test_clearance(task.a, task.b, clearance, check_all); + if (result.clash_type != -1) { + thread_results.push_back(result); + } + } + { + std::lock_guard lock(results_mutex); + results.insert(results.end(), thread_results.begin(), thread_results.end()); + } + }); + } + + for (auto& thread : threads) { + if (thread.joinable()) { + thread.join(); + } + } + + return results; + } + std::vector select(const T& t, bool completely_within = false, double extend = 0.0) const { distances_.clear(); protrusion_distances_.clear(); @@ -322,6 +1418,24 @@ namespace IfcGeom { tree_t tree_; map_t shapes_; + std::map aabbs_; + std::map obbs_; + std::map max_protrusions_; + std::map>> bvhs_; + std::unordered_map is_manifold_; + std::unordered_map>> tris_; + std::unordered_map> verts_; + std::unordered_map> normals_; + + // Temporary structures for H5 + std::vector triangulation_elements_; + std::map global_ids_; + std::map names_; + std::map> placements_; + std::map> local_verts_; + std::map> local_faces_; + std::map> local_materials_; + std::map> local_material_ids_; bool enable_face_styles_ = false; @@ -390,14 +1504,441 @@ namespace IfcGeom { } } + void write_h5() { + H5::H5File file("filename.h5", H5F_ACC_TRUNC); + H5::Group shapes = file.createGroup("/shapes"); + + std::set processed_geometry_ids; + std::vector element_shape_ids; + std::unordered_map geometry_id_to_shape_id; + int geometry_index = 0; + + std::vector> matrices; + std::vector> colours; + std::vector names; + std::vector global_ids; + + const float tolerance = 0.01f; // Tolerance value for comparison + + for (const auto& elem : triangulation_elements_) { + const auto geometry_id = elem->geometry().id(); + + const auto& placement = placements_[elem->product()]; + matrices.emplace_back(placement.begin(), placement.end()); + + names.push_back(names_[elem->product()]); + global_ids.push_back(global_ids_[elem->product()]); + + if (processed_geometry_ids.find(geometry_id) != processed_geometry_ids.end()) { + element_shape_ids.push_back(geometry_id_to_shape_id[geometry_id]); + continue; + } + + processed_geometry_ids.insert(geometry_id); + H5::Group group = shapes.createGroup(std::to_string(geometry_index)); + geometry_id_to_shape_id[geometry_id] = geometry_index; + element_shape_ids.push_back(geometry_index); + + geometry_index++; + + const auto& faces = local_faces_[geometry_id]; + const auto& verts = local_verts_[geometry_id]; + const auto& materials = local_materials_[geometry_id]; + const auto& material_ids = local_material_ids_[geometry_id]; + + std::vector verts_float(verts.size()); + std::transform(verts.begin(), verts.end(), verts_float.begin(), + [](double val) { return static_cast(val); }); + + // Write faces + size_t total_verts = verts.size() / 3; + hsize_t faces_dims[1] = {faces.size()}; + H5::DataSpace faces_dataspace(1, faces_dims); + H5::DSetCreatPropList faces_propList; + faces_propList.setChunk(1, faces_dims); + faces_propList.setDeflate(9); + if (total_verts < (1 << 8)) { + H5::DataType dtype = H5::PredType::NATIVE_UINT8; + std::vector faces_dtype(faces.begin(), faces.end()); + H5::DataSet faces_dataset = group.createDataSet("faces", dtype, faces_dataspace, faces_propList); + faces_dataset.write(faces_dtype.data(), dtype); + } else if (total_verts < (1 << 16)) { + H5::DataType dtype = H5::PredType::NATIVE_UINT16; + std::vector faces_dtype(faces.begin(), faces.end()); + H5::DataSet faces_dataset = group.createDataSet("faces", dtype, faces_dataspace, faces_propList); + faces_dataset.write(faces_dtype.data(), dtype); + } else { + H5::DataType dtype = H5::PredType::NATIVE_UINT32; + H5::DataSet faces_dataset = group.createDataSet("faces", dtype, faces_dataspace, faces_propList); + faces_dataset.write(faces.data(), dtype); + } + + // Write verts + H5::DataType dtype = H5::PredType::NATIVE_FLOAT; + hsize_t dims[1] = {verts.size()}; + H5::DataSpace dataspace(1, dims); + H5::DSetCreatPropList propList; + propList.setChunk(1, dims); + propList.setDeflate(9); + H5::DataSet dataset = group.createDataSet("verts", dtype, dataspace, propList); + dataset.write(verts_float.data(), H5::PredType::NATIVE_FLOAT); + + // Write materials + std::vector material_keys; + for (const auto& material : materials) { + float alpha = 1.0; + if (material.hasTransparency() && material.transparency() > 0) { + alpha = 1.0 - material.transparency(); + } + + int i = 0; + bool is_existing_colour = false; + for (const auto& colour : colours) { + if (std::abs(colour[0] - static_cast(material.diffuse()[0])) < tolerance + && std::abs(colour[1] - static_cast(material.diffuse()[1])) < tolerance + && std::abs(colour[2] - static_cast(material.diffuse()[2])) < tolerance + && std::abs(colour[3] - alpha) < tolerance) { + is_existing_colour = true; + break; + } + i++; + } + + if ( ! is_existing_colour) { + colours.push_back({ + static_cast(material.diffuse()[0]), + static_cast(material.diffuse()[1]), + static_cast(material.diffuse()[2]), + alpha}); + } + material_keys.push_back(static_cast(i)); + } + + size_t total_material_keys = material_keys.size(); + if (total_material_keys) { + hsize_t dims[1] = {material_keys.size()}; + H5::DataSpace dataspace(1, dims); + H5::DSetCreatPropList propList; + propList.setChunk(1, dims); + propList.setDeflate(9); + H5::DataType dtype = H5::PredType::NATIVE_UINT8; + H5::DataSet dataset = group.createDataSet("materials", dtype, dataspace, propList); + dataset.write(material_keys.data(), dtype); + } + + if (total_material_keys > 1) { + hsize_t dims[1] = {material_ids.size()}; + H5::DataSpace dataspace(1, dims); + H5::DSetCreatPropList propList; + propList.setChunk(1, dims); + propList.setDeflate(9); + H5::DataType dtype = H5::PredType::NATIVE_UINT8; + H5::DataSet dataset = group.createDataSet("material_ids", dtype, dataspace, propList); + std::vector data_dtype(material_ids.begin(), material_ids.end()); + dataset.write(data_dtype.data(), dtype); + } + } + + // Write GlobalIds + std::vector uuids_array; + for (const auto& id_str : global_ids) { + for (size_t i = 0; i < id_str.length(); i += 2) { + uuids_array.push_back(std::stoi(id_str.substr(i, 2), 0, 16)); + } + } + + hsize_t global_ids_dims[2] = {global_ids.size(), 16}; // 16 bytes per UUID + H5::DataSpace global_ids_dataspace(2, global_ids_dims); + H5::DataSet global_ids_dataset = file.createDataSet("element_global_ids", H5::PredType::NATIVE_UINT8, global_ids_dataspace); + global_ids_dataset.write(uuids_array.data(), H5::PredType::NATIVE_UINT8); + + // Write names + H5::StrType strType(H5::PredType::C_S1, H5T_VARIABLE); + hsize_t names_dims[1] = {names.size()}; + H5::DataSpace names_dataspace(1, names_dims); + H5::DataSet names_dataset = file.createDataSet("element_names", strType, names_dataspace); + std::vector cstr_names; + for (const auto& name : names) { + cstr_names.push_back(name.c_str()); + } + names_dataset.write(&cstr_names[0], strType); + + // Write matrices + std::vector flat_matrices; + for (const auto& matrix : matrices) { + flat_matrices.insert(flat_matrices.end(), matrix.begin(), matrix.end()); + } + hsize_t dims[2] = {matrices.size(), matrices[0].size()}; + H5::DataSpace dataspace(2, dims); + H5::DSetCreatPropList propList; + propList.setChunk(2, dims); + propList.setDeflate(9); + H5::DataSet dataset = file.createDataSet("element_matrices", H5::PredType::NATIVE_FLOAT, dataspace, propList); + dataset.write(flat_matrices.data(), H5::PredType::NATIVE_FLOAT); + + // Write element_shape_ids + hsize_t element_shape_ids_dims[1] = {element_shape_ids.size()}; + H5::DataSpace element_shape_ids_dataspace(1, element_shape_ids_dims); + H5::DSetCreatPropList element_shape_ids_propList; + element_shape_ids_propList.setChunk(1, element_shape_ids_dims); + element_shape_ids_propList.setDeflate(9); + if (geometry_index < (1 << 8)) { + H5::DataType dtype = H5::PredType::NATIVE_UINT8; + std::vector element_shape_ids_dtype(element_shape_ids.begin(), element_shape_ids.end()); + H5::DataSet element_shape_ids_dataset = file.createDataSet("element_shape_ids", dtype, element_shape_ids_dataspace, element_shape_ids_propList); + element_shape_ids_dataset.write(element_shape_ids_dtype.data(), dtype); + } else if (geometry_index < (1 << 16)) { + H5::DataType dtype = H5::PredType::NATIVE_UINT16; + std::vector element_shape_ids_dtype(element_shape_ids.begin(), element_shape_ids.end()); + H5::DataSet element_shape_ids_dataset = file.createDataSet("element_shape_ids", dtype, element_shape_ids_dataspace, element_shape_ids_propList); + element_shape_ids_dataset.write(element_shape_ids_dtype.data(), dtype); + } else if (geometry_index < (1UL << 32)) { + H5::DataType dtype = H5::PredType::NATIVE_UINT32; + std::vector element_shape_ids_dtype(element_shape_ids.begin(), element_shape_ids.end()); + H5::DataSet element_shape_ids_dataset = file.createDataSet("element_shape_ids", dtype, element_shape_ids_dataspace, element_shape_ids_propList); + element_shape_ids_dataset.write(element_shape_ids_dtype.data(), dtype); + } + + // Write colours + if (colours.size()) { + std::vector flat_colours; + for (const auto& colour : colours) { + flat_colours.insert(flat_colours.end(), colour.begin(), colour.end()); + } + hsize_t colours_dims[2] = {colours.size(), colours[0].size()}; + H5::DataSpace colours_dataspace(2, colours_dims); + H5::DSetCreatPropList colours_propList; + colours_propList.setChunk(2, colours_dims); + colours_propList.setDeflate(9); + H5::DataSet colours_dataset = file.createDataSet("materials", H5::PredType::NATIVE_FLOAT, colours_dataspace, colours_propList); + colours_dataset.write(flat_colours.data(), H5::PredType::NATIVE_FLOAT); + } + } + + template + void apply_matrix_to_flat_verts(const std::vector& flat_list, const std::vector& matrix, std::vector& result) { + result.clear(); + result.reserve(flat_list.size()); + + for (size_t i = 0; i < flat_list.size(); i += 3) { + float x = flat_list[i]; + float y = flat_list[i + 1]; + float z = flat_list[i + 2]; + result.push_back(x * matrix[0] + y * matrix[3] + z * matrix[6] + matrix[9]); + result.push_back(x * matrix[1] + y * matrix[4] + z * matrix[7] + matrix[10]); + result.push_back(x * matrix[2] + y * matrix[5] + z * matrix[8] + matrix[11]); + } + } + + std::string uint8_to_b64(const std::vector& uuids_array) { + std::string hex_str; + for (auto byte : uuids_array) { + // Convert each byte to a two-digit hexadecimal string and append it to the result + char hex[3]; // Two characters for the hex value and one for the null terminator + snprintf(hex, sizeof(hex), "%02x", byte); + hex_str.append(hex); + } + return hex_str; + } + + static bool is_manifold(const std::vector& fs) { + // @nb this assumes geometry is processed with the WELD_VERTICES setting + std::unordered_set, boost::hash>> dict; + for (size_t i = 0; i < fs.size(); i += 3) { + for (size_t j = 0; j < 3; ++j) { + auto k = (j + 1) % 3; + auto it = dict.find({ fs[i + j], fs[i + k] }); + if (it != dict.end()) { + dict.erase(it); + } else { + dict.insert({ fs[i + k], fs[i + j] }); + } + } + } + return dict.empty(); + } + + void add_element(IfcGeom::TriangulationElement* elem) { + + Bnd_Box aabb; + Bnd_OBB obb; + + { + auto& trsf = elem->transformation().data(); + auto& vs = elem->geometry().verts(); + auto& fs = elem->geometry().faces(); + + std::vector vs_transformed; + vs_transformed.reserve(vs.size() / 3); + for (size_t i = 0; i < vs.size(); i += 3) { + gp_Pnt p(vs[i + 0], vs[i + 1], vs[i + 2]); + vs_transformed.push_back(p.Transformed(trsf)); + aabb.Add(vs_transformed.back()); + } + + std::cout << "aabb: "; + aabb.DumpJson(std::cout); + std::cout << std::endl; + + std::unordered_map, std::vector, boost::hash>> quantized_normal_counts; + + std::vector tri_areas; + std::vector tri_norms; + for (size_t i = 0; i < fs.size(); i += 3) { + auto& p = vs_transformed[fs[i+0]]; + auto& q = vs_transformed[fs[i+1]]; + auto& r = vs_transformed[fs[i+2]]; + auto cross = (q.XYZ() - p.XYZ()).Crossed(r.XYZ() - p.XYZ()); + auto mag = cross.Modulus(); + tri_areas.push_back(mag / 2.); + cross /= mag; + tri_norms.push_back(cross); + auto quantized = std::make_tuple( + static_cast(cross.X() * 1000), + static_cast(cross.Y() * 1000), + static_cast(cross.Z() * 1000) + ); + quantized_normal_counts[quantized].push_back(i / 3); + } + + std::vector> area_to_it; + + for (auto it = quantized_normal_counts.cbegin(); it != quantized_normal_counts.cend(); ++it) { + double area_sum = 0.; + for (auto& i : it->second) { + area_sum += tri_areas[i]; + } + area_to_it.push_back({ area_sum, it }); + } + + std::sort(area_to_it.begin(), area_to_it.end(), [](auto& p1, auto& p2) { return p1.first < p2.first; }); + + auto calc_average_norm = [&tri_norms](const std::vector& idxs) { + gp_XYZ normal_sum; + for (auto& i : idxs) { + normal_sum.Add(tri_norms[i]); + } + normal_sum.Normalize(); + return normal_sum; + }; + + auto Z = calc_average_norm(area_to_it.back().second->second); + + std::vector> candidates; + + size_t num_candidates = 0; + for (auto it = ++area_to_it.rbegin(); it != area_to_it.rend() && num_candidates < 10; ++it, ++num_candidates) { + auto ref = calc_average_norm(it->second->second); + candidates.push_back({ std::abs(Z.Dot(ref)), ref }); + } + + if (candidates.empty()) { + { + gp_XYZ ref(0, 0, 1); + candidates.push_back({ std::abs(Z.Dot(ref)), ref }); + } + { + gp_XYZ ref(1, 0, 0); + candidates.push_back({ std::abs(Z.Dot(ref)), ref }); + } + } + + auto X = std::min_element(candidates.begin(), candidates.end(), [](auto& p1, auto& p2) { return p1.first < p2.first; })->second; + + gp_Trsf trsf2; + gp_Ax3 ax3(gp::Origin(), Z, X); + trsf2.SetTransformation(gp::XOY(), ax3); + + Bnd_Box tmp; + + for (auto& p : vs_transformed) { + tmp.Add(p.Transformed(trsf2)); + } + + gp_Pnt cent = (tmp.CornerMax().XYZ() + tmp.CornerMin().XYZ()) / 2; + auto halfsize = tmp.CornerMax().XYZ() - cent.XYZ(); + + obb.SetXComponent(ax3.XDirection(), halfsize.X()); + obb.SetYComponent(ax3.YDirection(), halfsize.Y()); + obb.SetZComponent(ax3.Direction(), halfsize.Z()); + obb.SetCenter(cent.Transformed(trsf2.Inverted())); + + std::cout << "obb: "; + obb.DumpJson(std::cout); + std::cout << std::endl; + } + + const auto& t = elem->product(); + const std::vector& matrix = elem->transformation().matrix().data(); + const std::vector& elem_verts_local = elem->geometry().verts(); + const std::vector& elem_faces = elem->geometry().faces(); + std::vector elem_verts; + apply_matrix_to_flat_verts(elem_verts_local, matrix, elem_verts); + + int original_tris_index = 0; + std::vector> original_tris; + std::vector verts; + std::vector original_normals; + + // Attempt to copy exactly what BRepExtrema_TriangleSet is doing under the hood. + const auto builder = new BVH_LinearBuilder(BVH_Constants_LeafNodeSizeDefault, BVH_Constants_MaxTreeDepth); + BVH_Triangulation triangulation(builder); + + for (int i = 0; i < elem_verts.size(); i += 3) { + triangulation.Vertices.push_back(BVH_Vec3d(elem_verts[i], elem_verts[i + 1], elem_verts[i + 2])); + verts.push_back(gp_Pnt(elem_verts[i], elem_verts[i + 1], elem_verts[i + 2])); + } + + for (int i = 0; i < elem_faces.size(); i += 3) { + const auto& v1_pnt = verts[elem_faces[i]]; + const auto& v2_pnt = verts[elem_faces[i + 1]]; + const auto& v3_pnt = verts[elem_faces[i + 2]]; + gp_Vec dir1(v1_pnt, v2_pnt); + gp_Vec dir2(v1_pnt, v3_pnt); + gp_Vec cross_product = dir1.Crossed(dir2); + if (cross_product.Magnitude() > Precision::Confusion()) { + triangulation.Elements.push_back(BVH_Vec4i( + elem_faces[i], elem_faces[i + 1], elem_faces[i + 2], original_tris_index + )); + original_tris_index++; + original_tris.push_back({ + elem_faces[i], elem_faces[i + 1], elem_faces[i + 2] + }); + original_normals.push_back(cross_product.Normalized()); + } + } + + triangulation.MarkDirty(); + const auto bvh = triangulation.BVH(); + + // After BVH is constructed, triangles are reordered + std::vector> tris(triangulation.Size()); + std::vector normals(triangulation.Size()); + + for (int i = 0; i < triangulation.Size(); ++i) { + const auto& el = triangulation.Elements[i]; + tris[i] = original_tris[el[3]]; + normals[i] = original_normals[el[3]]; + } + + bvhs_[t] = bvh; + is_manifold_[t] = is_manifold(elem_faces); + tris_[t] = std::move(tris); + verts_[t] = std::move(verts); + normals_[t] = std::move(normals); + aabbs_[t] = aabb; + obbs_[t] = obb; + max_protrusions_[t] = std::min(std::min(obb.XHSize(), obb.YHSize()), obb.ZHSize()) * 2; + } + void add_element(IfcGeom::BRepElement* elem) { if (!elem) { return; } auto compound = elem->geometry().as_compound(); compound.Move(elem->transformation().data()); - add(elem->product(), compound); - auto git = elem->geometry().begin(); + add(elem->product(), compound); + auto git = elem->geometry().begin(); if (enable_face_styles_) { TopoDS_Iterator it(compound); diff --git a/src/ifcgeom_schema_agnostic/base_utils.cpp b/src/ifcgeom_schema_agnostic/base_utils.cpp index e9eff396d4..daf3712c7a 100644 --- a/src/ifcgeom_schema_agnostic/base_utils.cpp +++ b/src/ifcgeom_schema_agnostic/base_utils.cpp @@ -39,6 +39,8 @@ #include #include #include +#include +#include #include @@ -739,3 +741,43 @@ bool IfcGeom::util::create_solid_from_faces(const TopTools_ListOfShape& face_lis return valid_shell; } + +bool IfcGeom::util::validate_shape(const TopoDS_Shape& s) { + BRepCheck_Analyzer ana(s); + if (ana.IsValid()) { + return true; + } + + std::stringstream str; + bool any_emitted = false; + + std::function dump; + dump = [&ana, &str, &dump, &any_emitted](const TopoDS_Shape& s) { + if (!ana.Result(s).IsNull()) { + BRepCheck_ListIteratorOfListOfStatus itl; + itl.Initialize(ana.Result(s)->Status()); + for (; itl.More(); itl.Next()) { + if (itl.Value() != BRepCheck_NoError) { + if (any_emitted) { + str << ", "; + } + BRepCheck::Print(itl.Value(), str); + str.seekp(str.tellp() - (std::streamoff)1); + str << " on "; + TopAbs::Print(s.ShapeType(), str); + BRepTools::Dump(s, str); + any_emitted = true; + } + } + } + for (TopoDS_Iterator it(s); it.More(); it.Next()) { + dump(it.Value()); + } + }; + + dump(s); + + Logger::Warning(str.str()); + + return false; +} \ No newline at end of file diff --git a/src/ifcgeom_schema_agnostic/base_utils.h b/src/ifcgeom_schema_agnostic/base_utils.h index a5d59e5887..f00d399a56 100644 --- a/src/ifcgeom_schema_agnostic/base_utils.h +++ b/src/ifcgeom_schema_agnostic/base_utils.h @@ -70,6 +70,8 @@ namespace IfcGeom { TopoDS_Shape apply_transformation(const TopoDS_Shape&, const gp_Trsf&); TopoDS_Shape apply_transformation(const TopoDS_Shape&, const gp_GTrsf&); + + bool validate_shape(const TopoDS_Shape&); } } diff --git a/src/ifcgeom_schema_agnostic/clash_utils.cpp b/src/ifcgeom_schema_agnostic/clash_utils.cpp new file mode 100644 index 0000000000..d6050bf87c --- /dev/null +++ b/src/ifcgeom_schema_agnostic/clash_utils.cpp @@ -0,0 +1,588 @@ +#include "clash_utils.h" +#include + +#define GU_CULLING_EPSILON_RAY_TRIANGLE FLT_EPSILON*FLT_EPSILON +#define PX_MAX_F32 3.4028234663852885981170418348452e+38F + +typedef uint32_t PxU32; + +// Why can't I use std::clamp? +template +const TC& ios_clamp(const TC& v, const TC& lo, const TC& hi) { + assert(!(hi < lo)); + return (v < lo) ? lo : (hi < v) ? hi : v; +} + +// Branchless slab method. Note that this can still be optimised further by batching boxes. +// From Tavian Barnes - MIT License +// https://tavianator.com/2022/ray_box_boundary.html +bool is_intersect_ray_box(const struct ray *ray, const struct box *box) { + float tmin = 0.0, tmax = INFINITY; + + for (int d = 0; d < 3; ++d) { + bool sign = std::signbit(ray->dir_inv[d]); + float bmin = box->corners[sign][d]; + float bmax = box->corners[!sign][d]; + + float dmin = (bmin - ray->origin[d]) * ray->dir_inv[d]; + float dmax = (bmax - ray->origin[d]) * ray->dir_inv[d]; + + tmin = std::max(dmin, tmin); + tmax = std::min(dmax, tmax); + } + + return tmin < tmax; +} + +// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionRayTriangle.h +// With minor modifications to use gp_Vec type. +// More reading: https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm +bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir, + const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2, + Standard_Real& at, Standard_Real& au, Standard_Real& av, + bool cull, float enlarge) { + // Find vectors for two edges sharing vert0 + const gp_Vec edge1 = vert1 - vert0; + const gp_Vec edge2 = vert2 - vert0; + + // Begin calculating determinant - also used to calculate U parameter + const gp_Vec pvec = dir.Crossed(edge2); // error ~ |v2-v0| + + // If determinant is near zero, ray lies in plane of triangle + const Standard_Real det = edge1.Dot(pvec); // error ~ |v2-v0|*|v1-v0| + + if(cull) + { + if(detuvlimit2) + return false; + + // Prepare to test V parameter + const gp_Vec qvec = tvec.Crossed(edge1); + + // Calculate V parameter and test bounds + const Standard_Real v = dir.Dot(qvec); + if(vuvlimit2) + return false; + + // Calculate t, scale parameters, ray intersects triangle + const Standard_Real t = edge2.Dot(qvec); + + const Standard_Real inv_det = 1.0f / det; + at = t*inv_det; + au = u*inv_det; + av = v*inv_det; + } + else + { + // the non-culling branch + if(std::abs(det)1.0f+enlarge) + return false; + + // prepare to test V parameter + const gp_Vec qvec = tvec.Crossed(edge1); + + // Calculate V parameter and test bounds + const Standard_Real v = dir.Dot(qvec) * inv_det; + if(v<-enlarge || (u+v)>1.0f+enlarge) + return false; + + // Calculate t, ray intersects triangle + const Standard_Real t = edge2.Dot(qvec) * inv_det; + + at = t; + au = u; + av = v; + } + return true; +} + +// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/sweep/GuSweepCapsuleCapsule.cpp +// With minor modifications to use gp_Vec type. +void edgeEdgeDist(gp_Vec& x, gp_Vec& y, // closest points + const gp_Vec& p, const gp_Vec& a, // seg 1 origin, vector + const gp_Vec& q, const gp_Vec& b) // seg 2 origin, vector +{ + const gp_Vec Tx = q - p; + const double ADotA = a.Dot(a); + const double BDotB = b.Dot(b); + const double ADotB = a.Dot(b); + const double ADotT = a.Dot(Tx); + const double BDotT = b.Dot(Tx); + + // t parameterizes ray (p, a) + // u parameterizes ray (q, b) + + // Compute t for the closest point on ray (p, a) to ray (q, b) + const Standard_Real Denom = ADotA*BDotB - ADotB*ADotB; + + Standard_Real t; // We will clamp result so t is on the segment (p, a) + if(Denom!=0.0f) + t = ios_clamp((ADotT*BDotB - BDotT*ADotB) / Denom, 0.0, 1.0); + else + t = 0.0f; + + // find u for point on ray (q, b) closest to point at t + Standard_Real u; + if(BDotB!=0.0f) + { + u = (t*ADotB - BDotT) / BDotB; + + // if u is on segment (q, b), t and u correspond to closest points, otherwise, clamp u, recompute and clamp t + if(u<0.0f) + { + u = 0.0f; + if(ADotA!=0.0f) + t = ios_clamp(ADotT / ADotA, 0.0, 1.0); + else + t = 0.0f; + } + else if(u > 1.0f) + { + u = 1.0f; + if(ADotA!=0.0f) + t = ios_clamp((ADotB + ADotT) / ADotA, 0.0, 1.0); + else + t = 0.0f; + } + } + else + { + u = 0.0f; + if(ADotA!=0.0f) + t = ios_clamp(ADotT / ADotA, 0.0, 1.0); + else + t = 0.0f; + } + + x = p + a * t; + y = q + b * u; +} + +// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/distance/GuDistanceTriangleTriangle.cpp +// With minor modifications to use gp_Vec type. +float distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array p, const std::array q) +{ + std::array Sv; + Sv[0] = p[1] - p[0]; + Sv[1] = p[2] - p[1]; + Sv[2] = p[0] - p[2]; + + std::array Tv; + Tv[0] = q[1] - q[0]; + Tv[1] = q[2] - q[1]; + Tv[2] = q[0] - q[2]; + + gp_Vec minP, minQ; + bool shown_disjoint = false; + + float mindd = PX_MAX_F32; + + for(int i=0;i<3;i++) + { + for(int j=0;j<3;j++) + { + edgeEdgeDist(cp, cq, p[i], Sv[i], q[j], Tv[j]); + const gp_Vec V = cq - cp; + const float dd = V.Dot(V); + + if(dd<=mindd) + { + minP = cp; + minQ = cq; + mindd = dd; + + int id = i+2; + if(id>=3) + id-=3; + gp_Vec Z = p[id] - cp; + float a = Z.Dot(V); + id = j+2; + if(id>=3) + id-=3; + Z = q[id] - cq; + float b = Z.Dot(V); + + if((a<=0.0f) && (b>=0.0f)) + return V.Dot(V); + + if(a<=0.0f) a = 0.0f; + else if(b>0.0f) b = 0.0f; + + if((mindd - a + b) > 0.0f) + shown_disjoint = true; + } + } + } + + gp_Vec Sn = Sv[0].Crossed(Sv[1]); + float Snl = Sn.Dot(Sn); + + if(Snl>1e-15f) + { + const std::array Tp = {(p[0] - q[0]).Dot(Sn), + (p[0] - q[1]).Dot(Sn), + (p[0] - q[2]).Dot(Sn)}; + + int index = -1; + if((Tp[0]>0.0f) && (Tp[1]>0.0f) && (Tp[2]>0.0f)) + { + if(Tp[0]Tp[1]) index = 0; else index = 1; + if(Tp[2]>Tp[index]) index = 2; + } + + if(index >= 0) + { + shown_disjoint = true; + + const gp_Vec& qIndex = q[index]; + + gp_Vec V = qIndex - p[0]; + gp_Vec Z = Sn.Crossed(Sv[0]); + if(V.Dot(Z)>0.0f) + { + V = qIndex - p[1]; + Z = Sn.Crossed(Sv[1]); + if(V.Dot(Z)>0.0f) + { + V = qIndex - p[2]; + Z = Sn.Crossed(Sv[2]); + if(V.Dot(Z)>0.0f) + { + cp = qIndex + Sn * Tp[index]/Snl; + cq = qIndex; + return (cp - cq).SquareMagnitude(); + } + } + } + } + } + + gp_Vec Tn = Tv[0].Crossed(Tv[1]); + float Tnl = Tn.Dot(Tn); + + if(Tnl>1e-15f) + { + const std::array Sp = {(q[0] - p[0]).Dot(Tn), + (q[0] - p[1]).Dot(Tn), + (q[0] - p[2]).Dot(Tn)}; + + int index = -1; + if((Sp[0]>0.0f) && (Sp[1]>0.0f) && (Sp[2]>0.0f)) + { + if(Sp[0]Sp[1]) index = 0; else index = 1; + if(Sp[2]>Sp[index]) index = 2; + } + + if(index >= 0) + { + shown_disjoint = true; + + const gp_Vec& pIndex = p[index]; + + gp_Vec V = pIndex - q[0]; + gp_Vec Z = Tn.Crossed(Tv[0]); + if(V.Dot(Z)>0.0f) + { + V = pIndex - q[1]; + Z = Tn.Crossed(Tv[1]); + if(V.Dot(Z)>0.0f) + { + V = pIndex - q[2]; + Z = Tn.Crossed(Tv[2]); + if(V.Dot(Z)>0.0f) + { + cp = pIndex; + cq = pIndex + Tn * Sp[index]/Tnl; + return (cp - cq).SquareMagnitude(); + } + } + } + } + } + + if(shown_disjoint) + { + cp = minP; + cq = minQ; + return mindd; + } + else return 0.0f; +} + +// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp +// With minor modifications to use gp_Vec type. +//Based on the paper A Fast Triangle-Triangle Intersection Test by T. Moeller +//http://web.stanford.edu/class/cs277/resources/papers/Moller1997b.pdf +namespace { + struct Interval + { + Standard_Real min; + Standard_Real max; + gp_Vec minPoint; + gp_Vec maxPoint; + + Interval() : min(FLT_MAX), max(-FLT_MAX), minPoint(gp_Vec(NAN, NAN, NAN)), maxPoint(gp_Vec(NAN, NAN, NAN)) { } + + static bool overlapOrTouch(const Interval& a, const Interval& b) + { + return !(a.min > b.max || b.min > a.max); + } + + static Interval intersection(const Interval& a, const Interval& b) + { + Interval result; + if (!overlapOrTouch(a, b)) + return result; + + if (a.min > b.min) { + result.min = a.min; + result.minPoint = a.minPoint; + } else { + result.min = b.min; + result.minPoint = b.minPoint; + } + + if (a.max < b.max) { + result.max = a.max; + result.maxPoint = a.maxPoint; + } else { + result.max = b.max; + result.maxPoint = b.maxPoint; + } + return result; + } + + void include(Standard_Real d, const gp_Vec& p) + { + if (d < min) { min = d; minPoint = p; } + if (d > max) { max = d; maxPoint = p; } + } + }; +} + +// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp +// With minor modifications to use gp_Vec type. +static Interval computeInterval(Standard_Real distanceA, Standard_Real distanceB, Standard_Real distanceC, const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, const gp_Vec& dir) +{ + Interval i; + + const bool bA = distanceA > 0; + const bool bB = distanceB > 0; + const bool bC = distanceC > 0; + distanceA = std::abs(distanceA); + distanceB = std::abs(distanceB); + distanceC = std::abs(distanceC); + + if (bA != bB) + { + const gp_Vec p = (distanceA / (distanceA + distanceB)) * b + (distanceB / (distanceA + distanceB)) * a; + i.include(dir.Dot(p), p); + } + if (bA != bC) + { + const gp_Vec p = (distanceA / (distanceA + distanceC)) * c + (distanceC / (distanceA + distanceC)) * a; + i.include(dir.Dot(p), p); + } + if (bB != bC) + { + const gp_Vec p = (distanceB / (distanceB + distanceC)) * c + (distanceC / (distanceB + distanceC)) * b; + i.include(dir.Dot(p), p); + } + + return i; +} + +// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp +// With minor modifications to use gp_Vec type. +Standard_Real orient2d(const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, PxU32 x, PxU32 y) +{ + return (a.Coord(y) - c.Coord(y)) * (b.Coord(x) - c.Coord(x)) - (a.Coord(x) - c.Coord(x)) * (b.Coord(y) - c.Coord(y)); +} + +// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp +// With minor modifications to use gp_Vec type. +Standard_Real pointInTriangle(const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, const gp_Vec& point, PxU32 x, PxU32 y) +{ + const Standard_Real ab = orient2d(a, b, point, x, y); + const Standard_Real bc = orient2d(b, c, point, x, y); + const Standard_Real ca = orient2d(c, a, point, x, y); + + if ((ab >= 0) == (bc >= 0) && (ab >= 0) == (ca >= 0)) + return true; + + return false; +} + +// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp +// With minor modifications to use gp_Vec type. +Standard_Real linesIntersect(const gp_Vec& startA, const gp_Vec& endA, const gp_Vec& startB, const gp_Vec& endB, PxU32 x, PxU32 y) +{ + const Standard_Real aaS = orient2d(startA, endA, startB, x, y); + const Standard_Real aaE = orient2d(startA, endA, endB, x, y); + + if ((aaS >= 0) == (aaE >= 0)) + return false; + + const Standard_Real bbS = orient2d(startB, endB, startA, x, y); + const Standard_Real bbE = orient2d(startB, endB, endA, x, y); + + if ((bbS >= 0) == (bbE >= 0)) + return false; + + return true; +} + +// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp +// With minor modifications to use gp_Vec type. +void getProjectionIndices(gp_Vec normal, PxU32& x, PxU32& y) +{ + normal.SetCoord(std::abs(normal.X()), std::abs(normal.Y()), std::abs(normal.Z())); + + if (normal.X() >= normal.Y() && normal.X() >= normal.Z()) + { + //x is the dominant normal direction + x = 1; + y = 2; + } + else if (normal.Y() >= normal.X() && normal.Y() >= normal.Z()) + { + //y is the dominant normal direction + x = 2; + y = 0; + } + else + { + //z is the dominant normal direction + x = 0; + y = 1; + } +} + +// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp +// With minor modifications to use gp_Vec type. +bool trianglesIntersectCoplanar(const gp_Vec& p1_n, const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, const gp_Vec& a2, const gp_Vec& b2, const gp_Vec& c2) +{ + PxU32 x = 0; + PxU32 y = 0; + getProjectionIndices(p1_n, x, y); + + const Standard_Real third = (1.0f / 3.0f); + + //A bit of the computations done inside the following functions could be shared but it's kept simple since the + //difference is not very big and the coplanar case is not expected to be the most common case + if (linesIntersect(a1, b1, a2, b2, x, y) || linesIntersect(a1, b1, b2, c2, x, y) || linesIntersect(a1, b1, c2, a2, x, y) || + linesIntersect(b1, c1, a2, b2, x, y) || linesIntersect(b1, c1, b2, c2, x, y) || linesIntersect(b1, c1, c2, a2, x, y) || + linesIntersect(c1, a1, a2, b2, x, y) || linesIntersect(c1, a1, b2, c2, x, y) || linesIntersect(c1, a1, c2, a2, x, y) || + pointInTriangle(a1, b1, c1, third * (a2 + b2 + c2), x, y) || pointInTriangle(a2, b2, c2, third * (a1 + b1 + c1), x, y)) + return true; + + return false; +} + +// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md +// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp +// With minor modifications to use gp_Vec type. +// Also with minor modification to return intersection points. +bool trianglesIntersect(const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, const gp_Vec& a2, const gp_Vec& b2, const gp_Vec& c2/*, Segment* intersection*/, gp_Vec& int1, gp_Vec& int2, bool ignoreCoplanar) +{ + const Standard_Real tolerance = 1e-8f; + + gp_Vec p1_n((b1 - a1).Crossed(c1 - a1).Normalized()); + double p1_d = -a1.Dot(p1_n); + // const PxPlane p1(a1, b1, c1); + const Standard_Real p1ToA = a2.Dot(p1_n) + p1_d; + const Standard_Real p1ToB = b2.Dot(p1_n) + p1_d; + const Standard_Real p1ToC = c2.Dot(p1_n) + p1_d; + + if(std::abs(p1ToA) < tolerance && std::abs(p1ToB) < tolerance &&std::abs(p1ToC) < tolerance) + return ignoreCoplanar ? false : trianglesIntersectCoplanar(p1_n, a1, b1, c1, a2, b2, c2); //Coplanar triangles + + if ((p1ToA > 0) == (p1ToB > 0) && (p1ToA > 0) == (p1ToC > 0)) + return false; //All points of triangle 2 on same side of triangle 1 -> no intersection + + gp_Dir p2_n((b2 - a2).Crossed(c2 - a2).Normalized()); + double p2_d = -a2.Dot(p2_n); + // const PxPlane p2(a2, b2, c2); + const Standard_Real p2ToA = a1.Dot(p2_n) + p2_d; + const Standard_Real p2ToB = b1.Dot(p2_n) + p2_d; + const Standard_Real p2ToC = c1.Dot(p2_n) + p2_d; + + if ((p2ToA > 0) == (p2ToB > 0) && (p2ToA > 0) == (p2ToC > 0)) + return false; //All points of triangle 1 on same side of triangle 2 -> no intersection + + gp_Vec intersectionDirection = p1_n.Crossed(p2_n); + const Standard_Real l2 = intersectionDirection.SquareMagnitude(); + intersectionDirection *= 1.0f / std::sqrt(l2); + + const Interval i1 = computeInterval(p2ToA, p2ToB, p2ToC, a1, b1, c1, intersectionDirection); + const Interval i2 = computeInterval(p1ToA, p1ToB, p1ToC, a2, b2, c2, intersectionDirection); + + if (Interval::overlapOrTouch(i1, i2)) + { + /*if (intersection) + { + const Interval i = Interval::intersection(i1, i2); + intersection->p0 = i.minPoint; + intersection->p1 = i.maxPoint; + }*/ + const Interval i = Interval::intersection(i1, i2); + int1 = i.minPoint; + int2 = i.maxPoint; + return true; + } + return false; +} diff --git a/src/ifcgeom_schema_agnostic/clash_utils.h b/src/ifcgeom_schema_agnostic/clash_utils.h new file mode 100644 index 0000000000..9023dec96b --- /dev/null +++ b/src/ifcgeom_schema_agnostic/clash_utils.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +struct ray { + float origin[3]; + float dir[3]; + float dir_inv[3]; +}; + +struct box { + float corners[2][3]; +}; + +bool is_intersect_ray_box(const struct ray *ray, const struct box *box); + +bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir, + const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2, + Standard_Real& at, Standard_Real& au, Standard_Real& av, + bool cull, float enlarge=0.0f); + +void edgeEdgeDist(gp_Vec& x, gp_Vec& y, // closest points + const gp_Vec& p, const gp_Vec& a, // seg 1 origin, vector + const gp_Vec& q, const gp_Vec& b); // seg 2 origin, vector + +float distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array p, const std::array q); + +bool trianglesIntersect(const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, const gp_Vec& a2, const gp_Vec& b2, const gp_Vec& c2/*, Segment* intersection*/, gp_Vec& int1, gp_Vec& int2, bool ignoreCoplanar); diff --git a/src/ifcopenshell-python/Makefile b/src/ifcopenshell-python/Makefile index 1e2ceecdbc..dc85b4c4ad 100644 --- a/src/ifcopenshell-python/Makefile +++ b/src/ifcopenshell-python/Makefile @@ -18,15 +18,6 @@ SED:=sed -i '' -e endif endif -ifeq ($(PYVERSION), py36) -PYNUMBER:=36 -endif -ifeq ($(PYVERSION), py37) -PYNUMBER:=37 -endif -ifeq ($(PYVERSION), py38) -PYNUMBER:=38 -endif ifeq ($(PYVERSION), py39) PYNUMBER:=39 endif @@ -36,6 +27,9 @@ endif ifeq ($(PYVERSION), py311) PYNUMBER:=311 endif +ifeq ($(PYVERSION), py312) +PYNUMBER:=312 +endif ifeq ($(PLATFORM), linux) PLATFORMTAG:=manylinux_2_31_x86_64 @@ -50,20 +44,6 @@ ifeq ($(PLATFORM), win) PLATFORMTAG:=win_amd64 endif -ifeq ($(PLATFORM), macosm1) -ifeq ($(PYVERSION), py36) -# Pure Evil. Let me know if you're the first to file a bug report. -PYNUMBER:=37 -endif -endif - -ifeq ($(PLATFORM), macos) -ifeq ($(PYVERSION), py311) -# Pure Evil. Let me know if you're the first to file a bug report. -PYNUMBER:=310 -endif -endif - .PHONY: test test: pytest -p no:pytest-blender test @@ -109,7 +89,7 @@ endif mkdir -p dist/ifcopenshell cp -r ifcopenshell/* dist/ifcopenshell/ - cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-eaa2aa0-$(PLATFORM)64.zip + cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-f7c03db-$(PLATFORM)64.zip cd dist/working && unzip ifcopenshell-python* cp -r dist/working/ifcopenshell/ifcopenshell_wrapper.py dist/ifcopenshell/ ifeq ($(PLATFORM), win) diff --git a/src/ifcopenshell-python/docs/_static/custom.css b/src/ifcopenshell-python/docs/_static/custom.css index 69f9d25e32..c1e237ec6c 100644 --- a/src/ifcopenshell-python/docs/_static/custom.css +++ b/src/ifcopenshell-python/docs/_static/custom.css @@ -1,12 +1,44 @@ +@import url("https://fonts.googleapis.com/css2?family=Nunito:wght@200;300;400;500;600;700;800&display=swap"); +@import url('https://fonts.googleapis.com/css2?family=Inconsolata:wght@200;300;400;500;600;700&display=swap'); +h1, h2, h3, h4 { + font-weight: normal; + color: #7cbf33; + background: -webkit-linear-gradient(0deg, #7cbf33, #049344); + background-clip: border-box; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} +a { + text-decoration: none; +} +.toc-tree .reference:hover { + color: #d98014; +} +.sidebar-brand-text { + font-size: 1rem; +} .blockbutton { - border: 1px solid var(--color-admonition-title--seealso); - background-color: var(--color-admonition-title-background--seealso); - padding: 5px; - text-align: center; - border-radius: 5px; max-width: 500px; margin-left: auto; margin-right: auto; + text-align: center; +} +.blockbutton a { + display: block; + padding-top: 15px; + padding-bottom: 15px; + border-radius: 5px; + background: #40b74c; + width: 100%; + height: 100%; + color: #fff; + text-decoration: none; +} +.blockbutton a:hover { + background: #70ba35; +} +.highlight .hll { + background-color: #ffc2; } section img { display: block; diff --git a/src/ifcopenshell-python/docs/conf.py b/src/ifcopenshell-python/docs/conf.py index befbaae79b..23b45563c1 100644 --- a/src/ifcopenshell-python/docs/conf.py +++ b/src/ifcopenshell-python/docs/conf.py @@ -1,20 +1,20 @@ -# BlenderBIM Add-on - OpenBIM Blender Add-on -# Copyright (C) 2020, 2021 Dion Moult +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2020-2024 Dion Moult # -# This file is part of BlenderBIM Add-on. +# This file is part of IfcOpenShell. # -# BlenderBIM Add-on is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# BlenderBIM Add-on is distributed in the hope that it will be useful, +# IfcOpenShell is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. +# GNU Lesser General Public License for more details. # -# You should have received a copy of the GNU General Public License -# along with BlenderBIM Add-on. If not, see . +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . # Configuration file for the Sphinx documentation builder. # @@ -36,7 +36,7 @@ sys.path.insert(0, os.path.abspath('..')) # -- Project information ----------------------------------------------------- project = "IfcOpenShell" -copyright = "2011-2023, IfcOpenShell Contributors" +copyright = "2011-2024, IfcOpenShell Contributors" author = "IfcOpenShell Contributors" # The full version, including alpha/beta/rc tags @@ -54,7 +54,7 @@ release = "0.7.0" # - No subnav making it really hard to navigate # - Kinda hacky setup https://stackoverflow.com/questions/2701998/sphinx-autodoc-is-not-automatic-enough # - I couldn't customise the template to show submodules above members which makes API discovery hard for users -extensions = ["autoapi.extension", "sphinx.ext.autosectionlabel"] +extensions = ["autoapi.extension", "sphinx.ext.autosectionlabel", "sphinx_copybutton"] # Auto add document prefixes to help guarantee uniqueness of automatic section references. autosectionlabel_prefix_document = True @@ -67,6 +67,8 @@ autoapi_type = 'python' # autoapi works by reading source code instead of importing modules autoapi_dirs = ['../ifcopenshell', '../../bcf/src', '../../bsdd', '../../ifccsv', '../../ifcdiff', '../../ifcpatch/ifcpatch', '../../ifctester/ifctester'] +# autoapi_dirs = ['../../ifcdiff'] +# autoapi_dirs = ['../../ifcdiff', '../ifcopenshell/util'] # autoapi_dirs = ['../../bcf/src', '../../bsdd', '../../ifccsv', '../../ifcdiff', '../../ifcpatch/ifcpatch', '../../ifctester/ifctester'] # These are auto-generated based on the IFC schema, so exclude them @@ -113,3 +115,54 @@ html_css_files = ["custom.css"] # page even on light themes. pygments_style = "one-dark" pygments_dark_style = "one-dark" + +html_logo = "https://ifcopenshell.org/assets/images/logo.png" +html_theme_options = { + "source_repository": "https://github.com/IfcOpenShell/IfcOpenShell/", + "source_branch": "v0.7.0", + "source_directory": "src/ifcopenshell-python/docs/", + "light_css_variables": { + "color-brand-primary": "#39b54a", + "color-brand-content": "#39b54a", + "color-brand-visited": "#d9e021", + "color-background-primary": "#f7f7f6", + "color-background-secondary": "#eeeeec", + "color-background-border": "#cfd0cb", + "color-foreground-primary": "#2e3436", + "color-sidebar-item-background--hover": "#f7f7f6", + "color-link--hover": "#d98014", + "font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji" + }, + "dark_css_variables": { + "color-brand-primary": "#39b54a", + "color-brand-content": "#39b54a", + "color-brand-visited": "#d9e021", + "color-background-primary": "#2e3436", + "color-background-border": "#2e3436", + "color-foreground-primary": "#eeeeec", + "color-sidebar-item-background--hover": "#2e3436", + "color-link--hover": "#d98014", + "font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji" + }, + + "footer_icons": [ + { + "name": "IfcOpenShell", + "url": "https://ifcopenshell.org", + "html": """ + + """, + "class": "", + }, + { + "name": "GitHub", + "url": "https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.7.0/src/ifcopenshell-python/docs", + "html": """ + + + + """, + "class": "", + }, + ], +} diff --git a/src/ifcopenshell-python/docs/ifcconvert/installation.rst b/src/ifcopenshell-python/docs/ifcconvert/installation.rst index 4471962d0a..8f913327fe 100644 --- a/src/ifcopenshell-python/docs/ifcconvert/installation.rst +++ b/src/ifcopenshell-python/docs/ifcconvert/installation.rst @@ -20,11 +20,11 @@ Pre-built packages | build-linux64_ | build-win32_ | build-win64_ | build-macos64_ | build-macosm164_ | +----------------+----------------+----------------+----------------+------------------+ -.. _build-linux64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-eaa2aa0-linux64.zip -.. _build-win32: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-eaa2aa0-win32.zip -.. _build-win64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-eaa2aa0-win64.zip -.. _build-macos64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-eaa2aa0-macos64.zip -.. _build-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-eaa2aa0-macosm164.zip +.. _build-linux64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f7c03db-linux64.zip +.. _build-win32: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f7c03db-win32.zip +.. _build-win64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f7c03db-win64.zip +.. _build-macos64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f7c03db-macos64.zip +.. _build-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f7c03db-macosm164.zip 2. Unzip the downloaded file and run IfcConvert using the command line. diff --git a/src/ifcopenshell-python/docs/ifcconvert/usage.rst b/src/ifcopenshell-python/docs/ifcconvert/usage.rst index df77cc18c1..1231ea5291 100644 --- a/src/ifcopenshell-python/docs/ifcconvert/usage.rst +++ b/src/ifcopenshell-python/docs/ifcconvert/usage.rst @@ -40,6 +40,22 @@ If your IFC uses large map coordinates and your desired format cannot handle it: # Or you can specify a manual offset in X;Y;Z format $ IfcConvert --model-offset "10000;10000;0" /path/to/input.ifc /path/to/output.glb +IfcConvert can be used to convert only specific elements. + +:: + + # Convert only walls and slabs + $ IfcConvert --include entities IfcWall IfcSlab -v /path/to/input.ifc /path/to/output.glb + # Convert only these two particular elements filtered by GlobalId + $ IfcConvert --include attribute GlobalId 1yETHMphv6LwABqR4Pbs5g attribute GlobalId 1yETHMphv6LwABqR0Pbs5g -v /path/to/input.ifc /path/to/output.glb + # Convert all objects on level 1. Note how "+" is used. + $ IfcConvert --include+=attribute Name "Level 1" -v /path/to/input.ifc /path/to/output.glb + +.. warning:: + + The include (or exclude) arguments cannot be placed right before input file + argument and only single of each argument supported for now. + IfcConvert can also be used to generate SVG floorplans: :: diff --git a/src/ifcopenshell-python/docs/ifccsv.rst b/src/ifcopenshell-python/docs/ifccsv.rst index 76742b3ec7..3c9f93b766 100644 --- a/src/ifcopenshell-python/docs/ifccsv.rst +++ b/src/ifcopenshell-python/docs/ifccsv.rst @@ -153,7 +153,7 @@ interface to access the IfcOpenShell utilities. 1. Install the BlenderBIM Add-on by following the `BlenderBIM Add-on installation documentation - `_. + `_. 2. Launch Blender. Change to the **Scene Properties** tab in the **Properties Panel**. Scroll down to the **IFC Collaboration > IFC CSV Import / Export** diff --git a/src/ifcopenshell-python/docs/ifcdiff.rst b/src/ifcopenshell-python/docs/ifcdiff.rst index b8b5332566..1f82d4d400 100644 --- a/src/ifcopenshell-python/docs/ifcdiff.rst +++ b/src/ifcopenshell-python/docs/ifcdiff.rst @@ -88,7 +88,7 @@ interface to access the IfcOpenShell utilities. 1. Install the BlenderBIM Add-on by following the `BlenderBIM Add-on installation documentation - `_. + `_. 2. Launch Blender. Change to the **Scene Properties** tab in the **Properties Panel**. Scroll down to the **IFC Quality Control > IFC Diff** panel. diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/code_examples.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/code_examples.rst index 66f708e95e..b43652b975 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/code_examples.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/code_examples.rst @@ -236,9 +236,9 @@ Create a simple model from scratch # Since the site is our top level location, assign it to the project # Then place our building on the site, and our storey in the building - run("aggregate.assign_object", model, relating_object=project, product=site) - run("aggregate.assign_object", model, relating_object=site, product=building) - run("aggregate.assign_object", model, relating_object=building, product=storey) + run("aggregate.assign_object", model, relating_object=project, products=[site]) + run("aggregate.assign_object", model, relating_object=site, products=[building]) + run("aggregate.assign_object", model, relating_object=building, products=[storey]) # Let's create a new wall wall = run("root.create_entity", model, ifc_class="IfcWall") diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_creation.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_creation.rst index f79b7c8d80..099e85f007 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_creation.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_creation.rst @@ -648,7 +648,7 @@ efficient and implies that the type is interchangable (e.g. for maintenance). # Assign our furniture occurrence to the type. # That's it! The representation will automatically be mapped! - run("type.assign_type", model, related_object=element, relating_type=element_type) + run("type.assign_type", model, related_objects=[element], relating_type=element_type) Material layer sets ------------------- @@ -686,14 +686,14 @@ responsibility to make sure the geometry is correct. ifcopenshell.api.run("material.edit_layer", model, layer=layer, attributes={"LayerThickness": 13}) # Great! Let's assign our material set to our wall type. - ifcopenshell.api.run("material.assign_material", model, product=wall_type, material=material_set) + ifcopenshell.api.run("material.assign_material", model, products=[wall_type], material=material_set) # Now, let's create a wall at the origin. wall = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWall") ifcopenshell.api.run("geometry.edit_object_placement", model, product=wall) # The wall is a WAL01 wall type. The material layer set is inherited. - ifcopenshell.api.run("type.assign_type", model, related_object=wall, relating_type=wall_type) + ifcopenshell.api.run("type.assign_type", model, related_objects=[wall], relating_type=wall_type) # It's now our responsibility to create a compatible representation. # Notice how our thickness of 0.118 must equal .013 + .092 + .013 from our type @@ -742,14 +742,14 @@ responsibility to make sure the geometry is correct. ifcopenshell.api.run("material.add_profile", model, profile_set=material_set, material=steel, profile=hea100) # Great! Let's assign our material set to our beam type. - ifcopenshell.api.run("material.assign_material", model, product=beam_type, material=material_set) + ifcopenshell.api.run("material.assign_material", model, products=[beam_type], material=material_set) # Now, let's create a beam at the origin. beam = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBeam") ifcopenshell.api.run("geometry.edit_object_placement", model, product=beam) # The beam is a B1 beam type. The material profile set is inherited. - ifcopenshell.api.run("type.assign_type", model, related_object=beam, relating_type=beam_type) + ifcopenshell.api.run("type.assign_type", model, related_objects=[beam], relating_type=beam_type) # It's now our responsibility to create a compatible representation. # Notice how we reuse our profile instead of creating a new profile. diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_processing.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_processing.rst index 5f6aaa9175..92512eddc5 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_processing.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_processing.rst @@ -296,7 +296,9 @@ serialiser with Python to customise the conversion, such as by writing a script the modifies the IFC on the fly before converting it, or writing complex include and exclude filters. -Here is a typical example to serialising to glTF / glb. +Here is a typical example to serialising to glTF / glb. Example settings to +serialise to other formats are shown commented out. Different serialisations +may require different settings. .. code-block:: python @@ -305,6 +307,8 @@ Here is a typical example to serialising to glTF / glb. import multiprocessing settings = ifcopenshell.geom.settings() + + # Settings for glTF / glb settings.set(settings.STRICT_TOLERANCE, True) settings.set(settings.INCLUDE_CURVES, True) # Setting element GUIDs is optional, but useful to uniquely identify objects in non-semantic formats. @@ -312,9 +316,19 @@ Here is a typical example to serialising to glTF / glb. # Note that applying default materials is required in glTF serialisation. settings.set(settings.APPLY_DEFAULT_MATERIALS, True) + # Settings for obj + # settings.set(settings.STRICT_TOLERANCE, True) + # settings.set(settings.INCLUDE_CURVES, True) + # settings.set(settings.USE_ELEMENT_GUIDS, True) + # settings.set(settings.APPLY_DEFAULT_MATERIALS, True) + # settings.set(settings.USE_WORLD_COORDS, True) + + # Serialise to glTF / glb serialiser = ifcopenshell.geom.serializers.gltf("output.glb", settings) - # Alternatively, this is an example for OBJ + + # Serialise to obj # serialiser = ifcopenshell.geom.serializers.obj('output.obj', 'output.mtl', settings) + serialiser.setFile(self.file) serialiser.setUnitNameAndMagnitude("METER", 1.0) serialiser.writeHeader() diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_tree.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_tree.rst index 485113d735..0069dd341b 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_tree.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_tree.rst @@ -1,14 +1,16 @@ Geometry tree ============= -IfcOpenShell includes a utility to build a unbalanced binary tree of geometry -and their bounding boxes. After a tree is built, you can efficiently select -geometry by specifying a point, radius, or bounding box. +IfcOpenShell includes a utility to build trees of geometry and their bounding +boxes. Geometry trees can be used to efficiently select geometry or collide +geometry with one another. .. image:: images/geometry-tree.png -The most efficient way to build tree is by using the iterator, as shown in the -example below: +The most efficient way to build a tree is by using the iterator. If the native +OpenCASCADE shape is added to the tree, a **UB Tree** is built. Alternatively, +if triangulation is added to the tree, a **BVH Tree** is built. The type of +tree determines the type of operation you can perform. .. code-block:: python @@ -21,27 +23,208 @@ example below: iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count()) if iterator.initialize(): while True: - tree.add_element(iterator.get_native()) + # Use triangulation to build a BVH tree + tree.add_element(iterator.get()) + + # Alternatively, use this code to build an unbalanced binary tree + # tree.add_element(iterator.get_native()) + if not iterator.next(): break -Once built, there are three methods you can use to select elements in the tree: -``select_box``, ``select``, and ``select_ray``. +Clashing or selecting geometry from a geometry tree +--------------------------------------------------- -``select_box`` lets you query for elements that contain a point or another -element. However, it only checks the bounding box of elements instead of their -exact geometry. This is the fastest approach and is recommended if you don't -need precise geometry selection. +With a **BVH Tree**, you can efficiently clash sets of elements with other +elements. You can find elements that intersect, collide, or are within a +clearance distance threshold of one another. There are three methods you can +use to clash elements in the tree. Each function collides one set of elements +with another set of elements. -``select`` lets you query for elements that contain a point, a sphere, or -another element. ``select`` is similar to select box, but additionally -considers the actual geometry of the object. This is slower but more precise. +- `Detecting intersection clashes between elements`_ detects when an element intersects with another + element. This is the most common type of clash detection used when + coordinating designs. For example, you might want to know if any pipes go + through structural columns or beams. +- `Detecting collision clashes between elements`_ detects when an element + touches another element. It is the fastest type of clash detection but does + not consider the distance that an element goes inside another element. This + considers surfaces only so it works on non-manifold geometry but will not + detect if an element is completely within another element. +- `Detecting clearance clashes between elements`_ detects when an element comes + near to another element within a clearance threshold. This is the slowest + type of clash detection. It works on non-manifold geometry and does not + consider inside vs outside. Elements like pipe and ducts with insulation, + structural openings, and equipment will typically require clearance checks. -``select_ray`` lets you query for elements that intersect with a ray. +With a **UB Tree**, you can efficiently select geometry by specifying a point, +radius, or bounding box. There are three methods you can use to select elements +in the tree. + +- `Selecting elements using bounding boxes`_ lets you query for elements that + contain a point or another element. However, it only checks the bounding box + of elements instead of their exact geometry. This is the fastest approach and + is recommended if you don't need precise geometry selection. +- `Selecting elements using precise geometry`_ lets you query for elements that + contain a point, a sphere, or another element. This is similar to selecting + using bounding boxes, but additionally considers the actual geometry of the + element. This is slower but more precise. +- `Selecting elements using a ray`_ lets you query for elements that intersect + with a ray. + +Detecting intersection clashes between elements +----------------------------------------------- + +``clash_intersection_many`` detects when an element intersects with or is +contained within another element. + +.. code-block:: python + + clashes = tree.clash_intersection_many( + group_a_elements, # e.g. from model.by_type("IfcWall") + group_b_elements, # Group b can be the same as group a if you want to clash within a single set + tolerance=0.002, # Any protrusions less than 2mm are ignored + check_all=True, # Keep on checking all potential intersections to find a worst case protrusion distance + ) + + for clash in clashes: + # Get the two elements that clash and their metadata + element1 = clash.a + element2 = clash.b + a_global_id = element1.get_argument(0) + b_global_id = element2.get_argument(0) + a_ifc_class = element1.is_a() + b_ifc_class = element2.is_a() + a_name = element1.get_argument(2) + b_name = element2.get_argument(2) + + # Potential clash types that can be detected are protrusions, pierces, and collisions + clash_type = ["protrusion", "pierce", "collision", "clearance"][clash.clash_type], + + # P1 and P2 represents two XYZ coordinates. The meaning of the coordinate depends on the clash type. + p1 = list(clash.p1) + p2 = list(clash.p2) + + # This represents the protrusion or piercing distance in meters. + # It is also the distance between P1 and P2. + distance = clash.distance + +If you specify a ``tolerance`` value, intersections with a protrusion distance +smaller than this tolerance are excluded. It is recommended to specify a +non-zero tolerance to distinguish between when elements merely touch (e.g. a +GPO on a wall) versus if they are truly intersecting (e.g. a pipe going through +a beam). + +If ``check_all`` is ``False``, the clash check will return as soon as an +intersection is found. This is faster but may not return the worst-case +protrusion distance. If you are not interested in the protrusion distance, it +is recommended to set this to ``False``. If you want the protrusion distance, +such as to prioritise which clashes are more severe, set this to ``True``. + +This includes: + +1. When an element X protrudes inside element Y, where element Y is manifold. + In this case, a protrusion distance is calculated as the deepest point of + element X to the closest surface of element Y. ``P1`` is defined as the XYZ + coordinate on element X, and ``P2`` is defined as the nearest point on the + surface of element Y. +2. When an element X pierces element Y, such that an edge of element X enters + element Y and leaves through another face. In this case, a piercing distance + is calculated as the distance where that edge is inside element Y. ``P1`` is + defined as the point on an edge of element X which enters element Y, and + ``P2`` is the point where that edge leaves element Y. +3. When neither X or Y is manifold, we cannot detect protrusion or piercing, so + instead when X and Y have any touching face. This is the same as the + ``clash_collision_many`` check below. The distance is considered to be zero + and ignores your specified tolerance. ``P1`` and ``P2`` are equal and + represent an arbitrary XYZ point where the two elements touch. + +Detecting collision clashes between elements +-------------------------------------------- + +``clash_collision_many`` detects when the surface of an element collides with +another element. The surfaces may either merely touch (e.g. are coplanar) or +intersect. + +.. code-block:: python + + clashes = tree.clash_collision_many( + group_a_elements, # e.g. from model.by_type("IfcWall") + group_b_elements, # Group b can be the same as group a if you want to clash within a single set + allow_touching=True, # Include results where faces merely touch but do not intersect + ) + + for clash in clashes: + # Get the two elements that clash and their metadata + element1 = clash.a + element2 = clash.b + a_global_id = element1.get_argument(0) + b_global_id = element2.get_argument(0) + a_ifc_class = element1.is_a() + b_ifc_class = element2.is_a() + a_name = element1.get_argument(2) + b_name = element2.get_argument(2) + + # P1 and P2 represents two possible arbitrary points where a collision is found. + # P1 may or may not be equal to P2. + p1 = list(clash.p1) + p2 = list(clash.p2) + +A collision between two surface triangles may be "touching" or "intersecting". +Two touching triangles may be coplanar or merely have a single edge or vertex +touching the other triangle. An intersecting triangle will have at least one +edge that goes through the other triangle. + +Detecting clearance clashes between elements +-------------------------------------------- + +``clash_clearance_many`` detects with the surface of an element comes within a +clearance distance threshold of another element. + +.. code-block:: python + + clashes = tree.clash_clearance_many( + group_a_elements, # e.g. from model.by_type("IfcWall") + group_b_elements, # Group b can be the same as group a if you want to clash within a single set + clearance=0.1, # Any surface closer than than 100mm is a clash + check_all=False, # Stop measuring distances once the first clearance violation is found per element. + ) + + for clash in clashes: + # Get the two elements that clash and their metadata + element1 = clash.a + element2 = clash.b + a_global_id = element1.get_argument(0) + b_global_id = element2.get_argument(0) + a_ifc_class = element1.is_a() + b_ifc_class = element2.is_a() + a_name = element1.get_argument(2) + b_name = element2.get_argument(2) + + # P1 and P2 represents the two XYZ coordinates between element1 and element2. + p1 = list(clash.p1) + p2 = list(clash.p2) + + # This represents the distance between element1 and element2 that is less than the clearance. + # It is the distance between P1 and P2. It cannot be less than 0. + distance = clash.distance + +You cannot specify a ``clearance`` less than 0. + +If ``check_all`` is ``False``, the clash check will return as soon as a +clearance violation is found. This is faster but may not return the worst-case +distance. If you only interested whether there is a clearance issue, it is +recommended to set this to ``False``. If you want the exact worst case +clearance distance, such as to prioritise which clashes are more severe, set +this to ``True``. Selecting elements using bounding boxes --------------------------------------- +Elements may be queried using an axis aligned bounding box. An axis aligned +bounding box is the bounding box using global XYZ axes, not the element's local +XYZ axes. If you have a vertical construction project, this means that your +model should be oriented to project north to get the best results. + You may select all elements that have a bounding box containing the point with XYZ coordinates of ``(0., 0., 0.)``. @@ -128,11 +311,11 @@ geometry. It will return: .. code-block:: python - elements = tree.select_box(wall, completely_within=True) + elements = tree.select(wall, completely_within=True) # Alternatively, you may also specify an extension to dilate the geometry # of the wall. - elements = tree.select_box(wall, completely_within=True, extend=5.) + elements = tree.select(wall, completely_within=True, extend=5.) Selecting elements using a ray ------------------------------ diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst index e74358f77c..77f71fbf34 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst @@ -30,47 +30,35 @@ the API. +-------------+----------------+----------------+----------------+-------------------+---------------------+ | | Linux 64bit | Windows 32bit | Windows 64bit | MacOS Intel 64bit | MacOS Silicon 64bit | +=============+================+================+================+===================+=====================+ - | Python 3.7 | py37-linux64_ | py37-win32_ | py37-win64_ | py37-macos64_ | py37-macosm164_ | - +-------------+----------------+----------------+----------------+-------------------+---------------------+ - | Python 3.8 | py38-linux64_ | py38-win32_ | py38-win64_ | py38-macos64_ | py38-macosm164_ | - +-------------+----------------+----------------+----------------+-------------------+---------------------+ | Python 3.9 | py39-linux64_ | py39-win32_ | py39-win64_ | py39-macos64_ | py39-macosm164_ | +-------------+----------------+----------------+----------------+-------------------+---------------------+ | Python 3.10 | py310-linux64_ | py310-win32_ | py310-win64_ | py310-macos64_ | py310-macosm164_ | +-------------+----------------+----------------+----------------+-------------------+---------------------+ - | Python 3.11 | py311-linux64_ | py311-win32_ | py311-win64_ | N/A | py311-macosm164_ | + | Python 3.11 | py311-linux64_ | py311-win32_ | py311-win64_ | py311-macos64_ | py311-macosm164_ | +-------------+----------------+----------------+----------------+-------------------+---------------------+ - | Python 3.12 | py312-linux64_ | py312-win32_ | py312-win64_ | N/A | py312-macosm164_ | + | Python 3.12 | py312-linux64_ | py312-win32_ | py312-win64_ | py312-macos64_ | py312-macosm164_ | +-------------+----------------+----------------+----------------+-------------------+---------------------+ -.. _py37-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-eaa2aa0-linux64.zip -.. _py38-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-eaa2aa0-linux64.zip -.. _py39-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-eaa2aa0-linux64.zip -.. _py310-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-eaa2aa0-linux64.zip -.. _py311-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-eaa2aa0-linux64.zip -.. _py312-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-eaa2aa0-linux64.zip -.. _py37-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-eaa2aa0-win32.zip -.. _py38-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-eaa2aa0-win32.zip -.. _py39-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-eaa2aa0-win32.zip -.. _py310-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-eaa2aa0-win32.zip -.. _py311-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-eaa2aa0-win32.zip -.. _py312-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-eaa2aa0-win32.zip -.. _py37-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-eaa2aa0-win64.zip -.. _py38-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-eaa2aa0-win64.zip -.. _py39-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-eaa2aa0-win64.zip -.. _py310-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-eaa2aa0-win64.zip -.. _py311-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-eaa2aa0-win64.zip -.. _py312-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-eaa2aa0-win64.zip -.. _py37-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-eaa2aa0-macos64.zip -.. _py38-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-eaa2aa0-macos64.zip -.. _py39-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-eaa2aa0-macos64.zip -.. _py310-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-eaa2aa0-macos64.zip -.. _py37-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-eaa2aa0-macosm164.zip -.. _py38-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-eaa2aa0-macosm164.zip -.. _py39-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-eaa2aa0-macosm164.zip -.. _py310-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-eaa2aa0-macosm164.zip -.. _py311-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-eaa2aa0-macosm164.zip -.. _py312-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-eaa2aa0-macosm164.zip +.. _py39-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f7c03db-linux64.zip +.. _py310-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f7c03db-linux64.zip +.. _py311-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f7c03db-linux64.zip +.. _py312-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-f7c03db-linux64.zip +.. _py39-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f7c03db-win32.zip +.. _py310-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f7c03db-win32.zip +.. _py311-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f7c03db-win32.zip +.. _py312-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-f7c03db-win32.zip +.. _py39-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f7c03db-win64.zip +.. _py310-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f7c03db-win64.zip +.. _py311-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f7c03db-win64.zip +.. _py312-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-f7c03db-win64.zip +.. _py39-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f7c03db-macos64.zip +.. _py310-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f7c03db-macos64.zip +.. _py311-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f7c03db-macos64.zip +.. _py312-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-f7c03db-macos64.zip +.. _py39-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f7c03db-macosm164.zip +.. _py310-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f7c03db-macosm164.zip +.. _py311-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f7c03db-macosm164.zip +.. _py312-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-f7c03db-macosm164.zip 2. Unzip the downloaded file and copy the ``ifcopenshell`` directory into your Python path. If you're not sure where your Python path is, run the following @@ -239,7 +227,7 @@ The BlenderBIM Add-on is available either as a stable build or a daily build. 1. Install the BlenderBIM Add-on by following the `BlenderBIM Add-on installation documentation - `_. + `_. 2. Launch Blender. On the top left of the Viewport panel, click the **Editor Type** icon to change the viewport into a **Python Console**. @@ -273,7 +261,7 @@ and run your script using the **Text > Run Script** menu or by clicking on the Blender. This can help when learning how to write scripts as you can double check the results of your scripts with what you see in the graphical interface. `Read more - `_. + `_. From source with precompiled binaries ------------------------------------- diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst index f01963a2da..3ae94c26a8 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst @@ -232,5 +232,6 @@ nest formulas, for example ``concat(title("foo"), lower("Bar"))`` will produce "``title({{value}})``", "``title(""foo"")``", "``Foo``", "Titlecases a string." "``concat({{value}}[, {{value2}}]*)``", "``concat(""foo"", ""bar"")``", "``foobar``", "Concatenates two or more strings." "``round({{value}}, {{precision}})``", "``round(3.123, 0.1)``", "``3.1``", "Rounds ``{{value}}`` to the nearest ``{{precision}}``." + "``number({{value}}[, {{decimal_separator}}[, {{thousands_separator}}]])``", "``number(1234.56, "","", ""."")``", "123.4,56", "Formats {{value}} with an optional custom {{decimal_separator}} and {{thousands_separator}}. The default separators are ``.`` and ``,``." "``metric_length({{value}}, {{precision}}, {{decimals}})``", "``metric_length(3.123, 0.1, 2)``", "``3.10``", "Rounds ``{{value}}`` to the nearest ``{{precision}}`` then displays using a certain amount of decimal places." - "``imperial_length({{value}}, {{precision}}, {{unit}})``", "``imperial_length(3.22, 4, ""foot"")``", "``3' - 3 3/4""``", "Rounds ``{{value}}`` to the nearest ``1/{{precision}}`` inch then displays using fractional feet and inches. The ``{{value}}`` may be specified either as ``foot`` or ``inch`` depending on ``{{unit}}``." + "``imperial_length({{value}}, {{precision}}, {{input_unit}}, {{output_unit}})``", "``imperial_length(3.22, 4, ""foot"")``", "``3' - 3 3/4""``", "``The {{value}}`` may be specified either as ``foot`` or ``inch`` depending on ``{{input_unit}}``. The ``{{value}}`` is then rounded to the nearest ``1/{{precision}}`` inch then formatted using fractional feet and inches if ``{{output_unit}}`` is set to ``foot`` or just inches if ``{{output_unit}}`` is set to ``inch``." diff --git a/src/ifcopenshell-python/docs/ifcopenshell/installation.rst b/src/ifcopenshell-python/docs/ifcopenshell/installation.rst index 7d5aec6e00..2226847a0b 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell/installation.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell/installation.rst @@ -61,7 +61,8 @@ operating systems. GCC (4.7 or newer) or Clang (any version) is required. $ sudo apt-get install git cmake gcc g++ libboost-all-dev libcgal-dev -3. Install OpenCascade Technology (OCCT). +3. Install OpenCascade Technology (OCCT). Officially v7.5.0 is supported. Other + versions may have unexpected behaviour. :: diff --git a/src/ifcopenshell-python/docs/ifcpatch.rst b/src/ifcopenshell-python/docs/ifcpatch.rst index 2923e2227c..3ba68a6722 100644 --- a/src/ifcopenshell-python/docs/ifcpatch.rst +++ b/src/ifcopenshell-python/docs/ifcpatch.rst @@ -41,9 +41,9 @@ utility: Exactly how it is run depends on the recipe. A recipe may require zero or more arguments which are specific to the recipe. Here's an example which runs the `ExtractElements` recipe, which, as the same suggests, extracts out elements. -This recipe expects one argument, which uses the `IFC Query -syntax `_. -In this example, we'll extract out all `IfcWall` elements. +This recipe expects one argument, which uses the +:ref:`ifcopenshell-python/selector_syntax:filtering elements` syntax. In this +example, we'll extract out all `IfcWall` elements. :: diff --git a/src/ifcopenshell-python/docs/ifcsverchok/installation.rst b/src/ifcopenshell-python/docs/ifcsverchok/installation.rst index 775ab2bcb0..b49ff5e600 100644 --- a/src/ifcopenshell-python/docs/ifcsverchok/installation.rst +++ b/src/ifcopenshell-python/docs/ifcsverchok/installation.rst @@ -12,7 +12,7 @@ Packaged installation IfcSverchok is packaged like a regular Blender add-on, so installation is the same as any other Blender add-on. `Download IfcSverchok here -`__. +`__. Like all Blender add-ons, they can be installed using ``Edit > Preferences > Addons > Install > Choose Downloaded ZIP > Enable Add-on Checkbox``. You can diff --git a/src/ifcopenshell-python/docs/introduction.rst b/src/ifcopenshell-python/docs/introduction.rst index b4661218ed..d3c847fa86 100644 --- a/src/ifcopenshell-python/docs/introduction.rst +++ b/src/ifcopenshell-python/docs/introduction.rst @@ -87,3 +87,4 @@ IfcOpenShell is a modular ecosystem of tools that work together, where each tool introduction/introduction_to_bim introduction/introduction_to_ifc + introduction/how_to_contribute diff --git a/src/ifcopenshell-python/docs/introduction/how_to_contribute.rst b/src/ifcopenshell-python/docs/introduction/how_to_contribute.rst new file mode 100644 index 0000000000..59015fe339 --- /dev/null +++ b/src/ifcopenshell-python/docs/introduction/how_to_contribute.rst @@ -0,0 +1,49 @@ +How to contribute +================= + +IfcOpenShell is made possible by the contributions of hundreds of people. +Contributions range from developing code, writing documentation, creating +tutorials, responding to requests, reporting bugs, and marketing. + +Writing documentation +--------------------- + +A great way to contribute without writing code is to help writing +documentation. + +All documentation is written in ReStructured Text and is available in the +`IfcOpenShell docs directory +`_. +You can press the edit button on the top right on any documentation page to +quickly edit their content. + +Python API documentation is autogenerated from docstrings present in the source +code of the respective Python module. + +If you want to build the documentation locally, the documentation system uses +`Sphinx `_. First, install the theme and +theme dependencies: + +.. code-block:: console + + $ pip install furo + $ pip install sphinx-autoapi + $ pip install sphinx-copybutton + +Now you can generate the documentation: + +.. code-block:: console + + $ cd /path/to/ifcopenshell/src/ifcopenshell-python/docs/ + $ make html + $ cd _build/html + $ python -m http.server + +You will now have a local webserver running hosting the documentation. + +.. tip:: + + It may take some time to generate the API documentation. You can edit the + `list of autoapi directories + `_ + to make this faster diff --git a/src/ifcopenshell-python/ifcopenshell/__init__.py b/src/ifcopenshell-python/ifcopenshell/__init__.py index 1b827fda09..f23d64d933 100644 --- a/src/ifcopenshell-python/ifcopenshell/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/__init__.py @@ -97,10 +97,19 @@ def open(path: "os.PathLike | str", format: str = None, should_stream: bool = Fa You can specify a file format. If no format is given, it is guessed from its extension. Currently supported specified format : .ifc | .ifcZIP | .ifcXML - Examples: + You can then filter by element ID, class, etc, and subscript by id or guid. + + Example: + + .. code:: python + model = ifcopenshell.open("/path/to/model.ifc") model = ifcopenshell.open("/path/to/model.ifcXML") model = ifcopenshell.open("/path/to/model.any_extension", ".ifc") + + products = model.by_type("IfcProduct") + print(products[0].id(), products[0].GlobalId) # 122 2XQ$n5SLP5MBLyL442paFx + print(products[0] == model[122] == model["2XQ$n5SLP5MBLyL442paFx"]) # True """ path = Path(path) if format is None: diff --git a/src/ifcopenshell-python/ifcopenshell/api/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/__init__.py index 876071d223..f5a56279c0 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/__init__.py @@ -23,28 +23,131 @@ import numpy import importlib import ifcopenshell import ifcopenshell.api +from typing import Callable, Any, Optional +from functools import partial pre_listeners = {} post_listeners = {} -def run(usecase_path, ifc_file=None, should_run_listeners=True, **settings): +def batching_argument_deprecation( + usecase_path: str, settings: dict, prev_argument: str, new_argument: str, replace_usecase: Optional[str] = None +) -> tuple[str, dict]: + if replace_usecase is not None: + print(f"WARNING. `{usecase_path}` api method is deprecated and should be replaced with `{replace_usecase}`.") + + if prev_argument in settings: + print( + f"WARNING. `{prev_argument}` argument is deprecated for API method " + f'"{usecase_path}" and should be replaced with `{new_argument}`.' + ) + settings = settings | {new_argument: [settings[prev_argument]]} + settings.pop(prev_argument) + return (replace_usecase or usecase_path, settings) + + +ARGUMENTS_DEPRECATION = { + "spatial.assign_container": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), + "spatial.unassign_container": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), + "group.unassign_group": partial(batching_argument_deprecation, prev_argument="product", new_argument="products"), + "aggregate.assign_object": partial(batching_argument_deprecation, prev_argument="product", new_argument="products"), + "aggregate.unassign_object": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), + "layer.assign_layer": partial(batching_argument_deprecation, prev_argument="item", new_argument="items"), + "layer.unassign_layer": partial(batching_argument_deprecation, prev_argument="item", new_argument="items"), + "spatial.remove_container": partial( + batching_argument_deprecation, + prev_argument="product", + new_argument="products", + replace_usecase="spatial.unassign_container", + ), + "nest.assign_object": partial( + batching_argument_deprecation, prev_argument="related_object", new_argument="related_objects" + ), + "nest.unassign_object": partial( + batching_argument_deprecation, prev_argument="related_object", new_argument="related_objects" + ), + "type.assign_type": partial( + batching_argument_deprecation, prev_argument="related_object", new_argument="related_objects" + ), + "type.unassign_type": partial( + batching_argument_deprecation, prev_argument="related_object", new_argument="related_objects" + ), + "system.assign_system": partial(batching_argument_deprecation, prev_argument="product", new_argument="products"), + "system.unassign_system": partial(batching_argument_deprecation, prev_argument="product", new_argument="products"), + "material.assign_material": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), + "material.unassign_material": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), + "classification.add_reference": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), + "classification.remove_reference": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), + "library.assign_reference": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), + "library.unassign_reference": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), + "document.assign_document": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), + "document.unassign_document": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), + "spatial.reference_structure": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), + "spatial.dereference_structure": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), + "constraint.assign_constraint": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), + "constraint.unassign_constraint": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), +} + + +CACHED_USECASE_CLASSES = dict() + + +def run( + usecase_path: str, + ifc_file: Optional[ifcopenshell.file] = None, + should_run_listeners=True, + **settings: Any, +) -> Any: if should_run_listeners: for listener in pre_listeners.get(usecase_path, {}).values(): listener(usecase_path, ifc_file, settings) - def serialise_entity_instance(entity): - return {"cast_type": "entity_instance", "value": entity.id(), "Name": getattr(entity, "Name", None)} + # see #4531 + if usecase_path in ARGUMENTS_DEPRECATION: + usecase_path, settings = ARGUMENTS_DEPRECATION[usecase_path](usecase_path, settings) - vcs_settings = settings.copy() - for key, value in settings.items(): - if isinstance(value, ifcopenshell.entity_instance): - vcs_settings[key] = serialise_entity_instance(value) - elif isinstance(value, numpy.ndarray): - vcs_settings[key] = {"cast_type": "ndarray", "value": value.tolist()} - elif isinstance(value, list) and value and isinstance(value[0], ifcopenshell.entity_instance): - vcs_settings[key] = [serialise_entity_instance(i) for i in value] + # TODO: settings serialization for client-server systems + # def serialise_entity_instance(entity): + # return {"cast_type": "entity_instance", "value": entity.id(), "Name": getattr(entity, "Name", None)} + # vcs_settings = settings.copy() + # for key, value in settings.items(): + # if isinstance(value, ifcopenshell.entity_instance): + # vcs_settings[key] = serialise_entity_instance(value) + # elif isinstance(value, numpy.ndarray): + # vcs_settings[key] = {"cast_type": "ndarray", "value": value.tolist()} + # elif isinstance(value, list) and value and isinstance(value[0], ifcopenshell.entity_instance): + # vcs_settings[key] = [serialise_entity_instance(i) for i in value] if "add_representation" in usecase_path: pass # print(usecase_path, "{ ... settings too complex right now ... }") @@ -58,9 +161,12 @@ def run(usecase_path, ifc_file=None, should_run_listeners=True, **settings): # except: # print(usecase_path, vcs_settings) - importlib.import_module(f"ifcopenshell.api.{usecase_path}") - module, usecase = usecase_path.split(".") - usecase_class = getattr(getattr(getattr(ifcopenshell.api, module), usecase), "Usecase") + usecase_class = CACHED_USECASE_CLASSES.get(usecase_path) + if usecase_class is None: + importlib.import_module(f"ifcopenshell.api.{usecase_path}") + module, usecase = usecase_path.split(".") + usecase_class = getattr(getattr(getattr(ifcopenshell.api, module), usecase), "Usecase") + CACHED_USECASE_CLASSES[usecase_path] = usecase_class if ifc_file: result = usecase_class(ifc_file, **settings).execute() @@ -74,42 +180,44 @@ def run(usecase_path, ifc_file=None, should_run_listeners=True, **settings): return result -def add_pre_listener(usecase_path, name, callback): +def add_pre_listener(usecase_path: str, name: str, callback: Callable[[str, ifcopenshell.file, dict], None]) -> None: """Add a pre listener :param usecase_path: string, ifcopenshell api use case path :param name: string, name of listener - :param callback: callback function + :param callback: callback function with 3 arguments: `usecase_path`, `ifc_file`, `settings` """ pre_listeners.setdefault(usecase_path, {})[name] = callback -def add_post_listener(usecase_path, name, callback): +def add_post_listener(usecase_path: str, name: str, callback: Callable[[str, ifcopenshell.file, dict], None]) -> None: """Add a post listener :param usecase_path: string, ifcopenshell api use case path :param name: string, name of listener - :param callback: callback function + :param callback: callback function with 3 arguments: `usecase_path`, `ifc_file`, `settings` """ post_listeners.setdefault(usecase_path, {})[name] = callback -def remove_pre_listener(usecase_path, name, callback): +def remove_pre_listener(usecase_path: str, name: str, callback: Callable[[str, ifcopenshell.file, dict], None]) -> None: """Remove a pre listener :param usecase_path: string, ifcopenshell api use case path :param name: string, name of listener - :param callback: callback function + :param callback: callback function with 3 arguments: `usecase_path`, `ifc_file`, `settings` """ pre_listeners.get(usecase_path, {}).pop(name, None) -def remove_post_listener(usecase_path, name, callback): +def remove_post_listener( + usecase_path: str, name: str, callback: Callable[[str, ifcopenshell.file, dict], None] +) -> None: """Remove a post listener :param usecase_path: string, ifcopenshell api use case path :param name: string, name of listener - :param callback: callback function + :param callback: callback function with 3 arguments: `usecase_path`, `ifc_file`, `settings` """ post_listeners.get(usecase_path, {}).pop(name, None) diff --git a/src/ifcopenshell-python/ifcopenshell/api/aggregate/assign_object.py b/src/ifcopenshell-python/ifcopenshell/api/aggregate/assign_object.py index 8a868755be..58bf9daa13 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/aggregate/assign_object.py +++ b/src/ifcopenshell-python/ifcopenshell/api/aggregate/assign_object.py @@ -20,11 +20,17 @@ import ifcopenshell import ifcopenshell.api import ifcopenshell.util.element import ifcopenshell.util.placement +from typing import Union class Usecase: - def __init__(self, file, product=None, relating_object=None): - """Assigns an object as an aggregate to a product + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + relating_object: ifcopenshell.entity_instance, + ): + """Assigns object as an aggregate to the products All physical IFC model elements must be part of a hierarchical tree called the "spatial decomposition", where large things are made up of @@ -55,14 +61,15 @@ class Usecase: its parent in the spatial hierarchy. If your product has a placement, its placement will be recalculated to follow this convention. - :param product: The part of the aggregate, typically an IfcElement or + :param products: The list of parts of the aggregate, typically of IfcElement or IfcSpatialStructureElement subclass - :type product: ifcopenshell.entity_instance.entity_instance + :type product: list[ifcopenshell.entity_instance.entity_instance] :param relating_object: The whole of the aggregate, typically an IfcElement or IfcSpatialStructureElement subclass :type relating_object: ifcopenshell.entity_instance.entity_instance :return: The IfcRelAggregate relationship instance - :rtype: ifcopenshell.entity_instance.entity_instance + or `None` if `products` was empty list. + :rtype: Union[ifcopenshell.entity_instance.entity_instance, None] Example: @@ -73,40 +80,59 @@ class Usecase: subelement = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuilding") # The project contains a site (note that project aggregation is a special case in IFC) - ifcopenshell.api.run("aggregate.assign_object", model, product=element, relating_object=project) + ifcopenshell.api.run("aggregate.assign_object", model, products=[element], relating_object=project) # The site has a building - ifcopenshell.api.run("aggregate.assign_object", model, product=subelement, relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", model, products=[subelement], relating_object=element) """ self.file = file self.settings = { - "product": product, + "products": products, "relating_object": relating_object, } - def execute(self): - decomposes = None - if self.settings["product"].Decomposes: - decomposes = self.settings["product"].Decomposes[0] + def execute(self) -> Union[ifcopenshell.entity_instance, None]: + if not self.settings["products"]: + return - is_decomposed_by = None - for rel in self.settings["relating_object"].IsDecomposedBy: - if rel.is_a("IfcRelAggregates"): - is_decomposed_by = rel - break + products = set(self.settings["products"]) + relating_object = self.settings["relating_object"] + is_decomposed_by = next((i for i in relating_object.IsDecomposedBy if i.is_a("IfcRelAggregates")), None) - if decomposes and decomposes == is_decomposed_by: - return decomposes + previous_aggregates_rels: set[ifcopenshell.entity_instance] = set() + products_without_aggregates: list[ifcopenshell.entity_instance] = [] + products_with_aggregates: list[ifcopenshell.entity_instance] = [] - container = ifcopenshell.util.element.get_container(self.settings["product"], should_get_direct=True) - if container: - ifcopenshell.api.run("spatial.remove_container", self.file, product=self.settings["product"]) + # check if there is anything to change + for product in products: + product_rel = next(iter(product.Decomposes), None) - if decomposes: - related_objects = list(decomposes.RelatedObjects) - related_objects.remove(self.settings["product"]) + if product_rel is None: + products_without_aggregates.append(product) + continue + + # either is_decomposed_by is None or product is part of different rel + if product_rel != is_decomposed_by: + previous_aggregates_rels.add(product_rel) + products_with_aggregates.append(product) + + # products with already assigned aggregates will be skipped + + products_to_change = products_without_aggregates + products_with_aggregates + # nothing to change + if not products_to_change: + return is_decomposed_by + + # can be either only aggregated or only contained at the same time + # some product might not be able to have a container + possibly_contained_products = [p for p in products_without_aggregates if hasattr(p, "ContainedInStructure")] + ifcopenshell.api.run("spatial.unassign_container", self.file, products=possibly_contained_products) + + # unassign elements from previous aggregates + for decomposes in previous_aggregates_rels: + related_objects = set(decomposes.RelatedObjects) - products if related_objects: - decomposes.RelatedObjects = related_objects + decomposes.RelatedObjects = list(related_objects) ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": decomposes}) else: history = decomposes.OwnerHistory @@ -114,10 +140,9 @@ class Usecase: if history: ifcopenshell.util.element.remove_deep2(self.file, history) + # assign elements to a new aggregate if is_decomposed_by: - related_objects = set(is_decomposed_by.RelatedObjects) - related_objects.add(self.settings["product"]) - is_decomposed_by.RelatedObjects = list(related_objects) + is_decomposed_by.RelatedObjects = list(set(is_decomposed_by.RelatedObjects) | products) ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": is_decomposed_by}) else: is_decomposed_by = self.file.create_entity( @@ -125,19 +150,21 @@ class Usecase: **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), - "RelatedObjects": [self.settings["product"]], - "RelatingObject": self.settings["relating_object"], + "RelatedObjects": list(products), + "RelatingObject": relating_object, } ) - placement = getattr(self.settings["product"], "ObjectPlacement", None) - if placement and placement.is_a("IfcLocalPlacement"): - ifcopenshell.api.run( - "geometry.edit_object_placement", - self.file, - product=self.settings["product"], - matrix=ifcopenshell.util.placement.get_local_placement(self.settings["product"].ObjectPlacement), - is_si=False, - ) + # localize placement relative to a new aggregate for affected products + for product in products_to_change: + placement = getattr(product, "ObjectPlacement", None) + if placement and placement.is_a("IfcLocalPlacement"): + ifcopenshell.api.run( + "geometry.edit_object_placement", + self.file, + product=product, + matrix=ifcopenshell.util.placement.get_local_placement(product.ObjectPlacement), + is_si=False, + ) return is_decomposed_by diff --git a/src/ifcopenshell-python/ifcopenshell/api/aggregate/unassign_object.py b/src/ifcopenshell-python/ifcopenshell/api/aggregate/unassign_object.py index b4c4c42a79..e4d0e35def 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/aggregate/unassign_object.py +++ b/src/ifcopenshell-python/ifcopenshell/api/aggregate/unassign_object.py @@ -22,8 +22,8 @@ import ifcopenshell.util.element class Usecase: - def __init__(self, file, product=None): - """Unassigns a product from its aggregate + def __init__(self, file: ifcopenshell.file, products: list[ifcopenshell.entity_instance]): + """Unassigns products from their aggregate A product (i.e. a smaller part of a whole) may be aggregated into zero or one larger space or element. This function will remove that @@ -37,12 +37,11 @@ class Usecase: If the product is not part of an aggregation relationship, nothing will happen. - :param product: The part of the aggregate, typically an IfcElement or + :param products: The list of parts of the aggregate, typically of IfcElements or IfcSpatialStructureElement subclass - :type product: ifcopenshell.entity_instance.entity_instance - :return: The IfcRelAggregate relationship instance, only returned if the - whole still contains any other parts. - :rtype: ifcopenshell.entity_instance.entity_instance, None + :type product: list[ifcopenshell.entity_instance.entity_instance] + :return: None + :rtype: None Example: @@ -51,28 +50,31 @@ class Usecase: element = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcSite") subelement1 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuilding") subelement2 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuilding") - ifcopenshell.api.run("aggregate.assign_object", model, product=subelement1, relating_object=element) - ifcopenshell.api.run("aggregate.assign_object", model, product=subelement2, relating_object=element) - # The relationship is returned as element still has subelement2 - rel = ifcopenshell.api.run("aggregate.unassign_object", model, product=subelement1) - # Nothing is returned, as element is now empty - ifcopenshell.api.run("aggregate.unassign_object", model, product=subelement2) + ifcopenshell.api.run("aggregate.assign_object", model, products=[subelement1], relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", model, products=[subelement2], relating_object=element) + # nothing is returned + ifcopenshell.api.run("aggregate.unassign_object", model, products=[subelement1]) + # nothing is returned, relationship is removed + ifcopenshell.api.run("aggregate.unassign_object", model, products=[subelement2]) """ self.file = file - self.settings = { "product": product } + self.settings = {"products": products} - def execute(self): - for rel in self.settings["product"].Decomposes or []: - if not rel.is_a("IfcRelAggregates"): - continue - if len(rel.RelatedObjects) == 1: + def execute(self) -> None: + products = set(self.settings["products"]) + rels = set( + rel + for product in products + if (rel := next((rel for rel in product.Decomposes if rel.is_a("IfcRelAggregates")), None)) + ) + + for rel in rels: + related_objects = set(rel.RelatedObjects) - products + if related_objects: + rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + else: history = rel.OwnerHistory self.file.remove(rel) if history: ifcopenshell.util.element.remove_deep2(self.file, history) - return - related_objects = list(rel.RelatedObjects) - related_objects.remove(self.settings["product"]) - rel.RelatedObjects = related_objects - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) - return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py b/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py index 83bc15f0ec..42365ca802 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py +++ b/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py @@ -53,15 +53,30 @@ class Usecase: if hasattr(self.settings["product"], "PredefinedType"): if hasattr(self.settings["product"], "ElementType"): if ( - not self.settings["product"].ElementType + self.settings["product"].ElementType is None and self.settings["product"].PredefinedType == "USERDEFINED" ): self.settings["product"].PredefinedType = "NOTDEFINED" + elif ( + self.settings["product"].ElementType + and self.settings["product"].PredefinedType != "USERDEFINED" + ): + self.settings["product"].PredefinedType = "USERDEFINED" elif hasattr(self.settings["product"], "ObjectType"): - if ( - not self.settings["product"].ObjectType + relating_type = ifcopenshell.util.element.get_type(self.settings["product"]) + # Allow for None due to https://github.com/buildingSMART/IFC4.3.x-development/issues/818 + if relating_type and relating_type.PredefinedType not in ("NOTDEFINED", None): + self.settings["product"].ObjectType = None + self.settings["product"].PredefinedType = None + elif ( + self.settings["product"].ObjectType is None and self.settings["product"].PredefinedType == "USERDEFINED" ): self.settings["product"].PredefinedType = "NOTDEFINED" + elif ( + self.settings["product"].ObjectType + and self.settings["product"].PredefinedType != "USERDEFINED" + ): + self.settings["product"].PredefinedType = "USERDEFINED" if hasattr(self.settings["product"], "OwnerHistory"): ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": self.settings["product"]}) diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py b/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py index 0682ab7565..d59a4e9343 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py @@ -19,10 +19,11 @@ import ifcopenshell import ifcopenshell.util.schema import ifcopenshell.util.date +from typing import Union class Usecase: - def __init__(self, file, classification=None): + def __init__(self, file: ifcopenshell.file, classification: Union[str, ifcopenshell.entity_instance]): """Adds a new classification system to the project External classification systems such as Uniclass or Omniclass are @@ -81,7 +82,7 @@ class Usecase: "classification": classification, } - def execute(self): + def execute(self) -> ifcopenshell.entity_instance: if isinstance(self.settings["classification"], str): classification = self.file.createIfcClassification(Name=self.settings["classification"]) self.relate_to_project(classification) diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py index 320825e335..9057f7a25f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py @@ -17,12 +17,24 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.element import ifcopenshell.util.schema +from typing import Optional, Union class Usecase: - def __init__(self, file, product=None, reference=None, identification=None, name=None, classification=None, is_lightweight=True): - """Adds a new classification reference and assigns it to a product + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + reference: Optional[ifcopenshell.entity_instance] = None, + identification: Optional[str] = None, + name: Optional[str] = None, + classification: Optional[ifcopenshell.entity_instance] = None, + is_lightweight=True, + ): + """Adds a new classification reference and assigns it to the list of products A classification reference is a single entry such as "Pr_12_23_34" that is part of an external classification system (such as Uniclass or @@ -33,7 +45,7 @@ class Usecase: resources such as profiles, documents, libraries, and so on. Classification references can be added in two ways. Option 1) specify a - custom arbitrary reference, where you have the manually specify the + custom arbitrary reference, where you have to manually specify the identification (e.g. "Pr_12_23_45") and name (e.g. "Door Products"). Option 2) add a reference from an IFC classification library. The latter is preferred if you are using a common classification system such as @@ -52,13 +64,13 @@ class Usecase: assigned to both the type and an occurrence, then the assignment at the occurrence will override the type classification. - :param product: The IFC object, property, or resource you want to + :param product: The list of IFC objects, properties, or resources you want to associate the classification reference to. - :type product: ifcopenshell.entity_instance.entity_instance + :type product: list[ifcopenshell.entity_instance.entity_instance] :param reference: The classification reference entity taken from an IFC classification library. If you supply this parameter, you will use option 2. - :type product: ifcopenshell.entity_instance.entity_instance, optional + :type reference: ifcopenshell.entity_instance.entity_instance, optional :param identification: If you choose option 1 and do not specify a reference, you may manually specify an identification code. The code is typically a short identifier and may have punctuation to separate @@ -70,7 +82,7 @@ class Usecase: :param classification: The IfcClassification entity in your IFC model (not the library, if you are doing option 2) that the reference is part of. - :type product: ifcopenshell.entity_instance.entity_instance + :type classification: ifcopenshell.entity_instance.entity_instance :param is_lightweight: If you are doing option 2, choose whether or not to only add that particular reference (lighweight) or also add all of its parent references in the classification hierarchy (not @@ -81,8 +93,12 @@ class Usecase: is generally unnecessary. Using lightweight classifications are recommended and is the default. :type is_lightweight: bool, optional + + :raises TypeError: If file is IFC2X3 and `products` has non-IfcRoot elements. + :return: The newly added IfcClassificationReference - :rtype: ifcopenshell.entity_instance.entity_instance + or `None` if `products` was empty list. + :rtype: Union[ifcopenshell.entity_instance.entity_instance, None] Example: @@ -93,7 +109,7 @@ class Usecase: classification = ifcopenshell.api.run("classification.add_classification", model, classification="MyCustomClassification") ifcopenshell.api.run("classification.add_reference", model, - product=wall_type, classification=classification, + products=[wall_type], classification=classification, identification="W_01", name="Interior Walls") # Option 2: adding a popular classification from a library @@ -104,12 +120,12 @@ class Usecase: reference = [r for r in library.by_type("IfcClassificationReference") if r.Identification == "XYZ"][0] ifcopenshell.api.run("classification.add_reference", model, - product=wall_type, classification=classification, + products=[wall_type], classification=classification, reference=reference) """ self.file = file self.settings = { - "product": product, + "products": products, "reference": reference, "identification": identification, "name": name, @@ -117,8 +133,27 @@ class Usecase: "is_lightweight": is_lightweight, } - def execute(self): - self.is_rooted = self.settings["product"].is_a("IfcRoot") + def execute(self) -> Union[ifcopenshell.entity_instance, None]: + if not self.settings["products"]: + return + + if self.settings["reference"]: + referenced = ifcopenshell.util.element.get_referenced_elements(self.settings["reference"]) + if set(self.settings["products"]).issubset(referenced): + # nothing to do, all elements already have this reference assigned + return self.settings["reference"] + + self.rooted_products: set[ifcopenshell.entity_instance] = set() + self.non_rooted_products: set[ifcopenshell.entity_instance] = set() + for product in self.settings["products"]: + if product.is_a("IfcRoot"): + self.rooted_products.add(product) + else: + self.non_rooted_products.add(product) + + if self.non_rooted_products and self.file.schema == "IFC2X3": + raise TypeError(f"Cannot add reference to non-IfcRoot element in IFC2X3: {self.non_rooted_products}.") + if self.settings["reference"]: return self.add_from_library() return self.add_from_identification() @@ -134,14 +169,10 @@ class Usecase: else: reference.Identification = self.settings["identification"] - relationship = self.get_existing_relationship(reference) - if relationship: - self.add_to_existing_relationship(relationship) - else: - self.add_new_relationship(reference) + self.update_relationships(reference) return reference - def add_from_library(self): + def add_from_library(self) -> ifcopenshell.entity_instance: if hasattr(self.settings["reference"], "ItemReference"): identification = self.settings["reference"].ItemReference # IFC2X3 else: @@ -155,8 +186,9 @@ class Usecase: old_referenced_source = self.settings["reference"].ReferencedSource self.settings["reference"].ReferencedSource = None else: + classification_name = self.settings["classification"].Name existing_classification = [ - c for c in self.file.by_type("IfcClassification") if c.Name == self.settings["classification"].Name + c for c in self.file.by_type("IfcClassification") if c.Name == classification_name ] reference = migrator.migrate(self.settings["reference"], self.file) @@ -174,15 +206,10 @@ class Usecase: for element in to_delete: self.file.remove(element) - relationship = self.get_existing_relationship(reference) - if relationship: - self.add_to_existing_relationship(relationship) - else: - self.add_new_relationship(reference) - + self.update_relationships(reference) return reference - def get_existing_reference(self, identification): + def get_existing_reference(self, identification: Optional[str] = None) -> Union[ifcopenshell.entity_instance, None]: for reference in self.file.by_type("IfcClassificationReference"): if self.file.schema == "IFC2X3": if reference.ItemReference == identification: @@ -191,39 +218,39 @@ class Usecase: if reference.Identification == identification: return reference - def add_new_relationship(self, reference): - if self.is_rooted: - self.file.create_entity( - "IfcRelAssociatesClassification", - GlobalId=ifcopenshell.guid.new(), - RelatedObjects=[self.settings["product"]], - RelatingClassification=reference, - ) - else: - self.file.create_entity( - "IfcExternalReferenceRelationship", - RelatingReference=reference, - RelatedResourceObjects=[self.settings["product"]], - ) - - def add_to_existing_relationship(self, rel): - if self.is_rooted: - related_objects = set(rel.RelatedObjects) - related_objects.add(self.settings["product"]) - rel.RelatedObjects = list(related_objects) - else: - related_objects = set(rel.RelatedResourceObjects) - related_objects.add(self.settings["product"]) - rel.RelatedResourceObjects = list(related_objects) - - def get_existing_relationship(self, reference): - if self.is_rooted: + def update_relationships(self, reference: ifcopenshell.entity_instance) -> None: + root_rel, non_root_rel = None, None + if self.rooted_products: if self.file.schema == "IFC2X3": for rel in self.file.by_type("IfcRelAssociatesClassification"): if rel.RelatingClassification == reference: - return rel - elif reference.ClassificationRefForObjects: - return reference.ClassificationRefForObjects[0] - elif self.file.schema != "IFC2X3": - if reference.ExternalReferenceForResources: - return reference.ExternalReferenceForResources[0] + root_rel = rel + break + else: + root_rel = next(iter(reference.ClassificationRefForObjects), None) + + if root_rel: + related_objects = set(root_rel.RelatedObjects) | self.rooted_products + root_rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": root_rel}) + else: + self.file.create_entity( + "IfcRelAssociatesClassification", + OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", self.file), + GlobalId=ifcopenshell.guid.new(), + RelatedObjects=list(self.rooted_products), + RelatingClassification=reference, + ) + + if self.non_rooted_products: + # NOTE: Only Ifc4+. Ifc2x3 is already handled by raising TypeError + non_root_rel = next(iter(reference.ExternalReferenceForResources), None) + if non_root_rel: + related_objects = set(non_root_rel.RelatedResourceObjects) | self.non_rooted_products + non_root_rel.RelatedResourceObjects = list(related_objects) + else: + self.file.create_entity( + "IfcExternalReferenceRelationship", + RelatingReference=reference, + RelatedResourceObjects=list(self.non_rooted_products), + ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py index 48b55b3740..d4e8d802a6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py @@ -17,12 +17,18 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api import ifcopenshell.util.element class Usecase: - def __init__(self, file, reference=None, product=None): - """Removes a classification reference from a product + def __init__( + self, + file: ifcopenshell.file, + reference: ifcopenshell.entity_instance, + products: list[ifcopenshell.entity_instance], + ): + """Removes a classification reference from the list of products If the classification reference is no longer associated to any products, the classification reference itself is also removed. @@ -30,9 +36,12 @@ class Usecase: :param reference: The IfcClassificationReference entity of the relationship you want to remove. :type reference: ifcopenshell.entity_instance.entity_instance - :param product: The object entity of the relationship you want to + :param product: The list fo object entities of the relationship you want to remove. - :type reference: ifcopenshell.entity_instance.entity_instance + :type product: list[ifcopenshell.entity_instance.entity_instance] + + :raises TypeError: If file is IFC2X3 and `products` has non-IfcRoot elements. + :return: None :rtype: None @@ -44,42 +53,75 @@ class Usecase: classification = ifcopenshell.api.run("classification.add_classification", model, classification="MyCustomClassification") reference = ifcopenshell.api.run("classification.add_reference", model, - product=wall_type, classification=classification, + products=[wall_type], classification=classification, identification="W_01", name="Interior Walls") ifcopenshell.api.run("classification.remove_reference", model, - reference=reference, product=wall_type) + reference=reference, products=[wall_type]) """ self.file = file - self.settings = {"reference": reference, "product": product} + self.settings = {"reference": reference, "products": products} - def execute(self): - if self.settings["product"].is_a("IfcRoot"): - for rel in self.file.by_type("IfcRelAssociatesClassification"): - if rel.RelatingClassification == self.settings["reference"] and rel.RelatedObjects: - if self.settings["product"] in rel.RelatedObjects: - related_objects = list(rel.RelatedObjects) - related_objects.remove(self.settings["product"]) - if len(related_objects): - rel.RelatedObjects = related_objects - else: - history = rel.OwnerHistory - self.file.remove(rel) - if history: - ifcopenshell.util.element.remove_deep2(self.file, history) - else: - for rel in self.file.by_type("IfcExternalReferenceRelationship"): - if rel.RelatingReference == self.settings["reference"] and rel.RelatedResourceObjects: - if self.settings["product"] in rel.RelatedResourceObjects: - related_objects = list(rel.RelatedResourceObjects) - related_objects.remove(self.settings["product"]) - if len(related_objects): - rel.RelatedResourceObjects = related_objects - else: - self.file.remove(rel) + def execute(self) -> None: + is_ifc2x3 = self.file.schema == "IFC2X3" + products = set(self.settings["products"]) + referenced = ifcopenshell.util.element.get_referenced_elements(self.settings["reference"]) + products -= products.difference(referenced) + + # all products are already unassigned from a reference + if not products: + return + + rooted_products: set[ifcopenshell.entity_instance] = set() + non_rooted_products: set[ifcopenshell.entity_instance] = set() + for product in self.settings["products"]: + if product.is_a("IfcRoot"): + rooted_products.add(product) + else: + non_rooted_products.add(product) + + if non_rooted_products and is_ifc2x3: + raise TypeError(f"Cannot add reference to non-IfcRoot element in IFC2X3: {non_rooted_products}.") + + if rooted_products: + reference_rels: set[ifcopenshell.entity_instance] = set() + for product in rooted_products: + reference_rels.update(product.HasAssociations) + + reference_rels = { + rel + for rel in reference_rels + if rel.is_a("IfcRelAssociatesClassification") + and rel.RelatingClassification == self.settings["reference"] + } + + for rel in reference_rels: + related_objects = set(rel.RelatedObjects) - rooted_products + if related_objects: + rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + else: + history = rel.OwnerHistory + self.file.remove(rel) + if history: + ifcopenshell.util.element.remove_deep2(self.file, history) + + if non_rooted_products: + reference_rels: set[ifcopenshell.entity_instance] = set() + for product in non_rooted_products: + rels = getattr(product, "HasExternalReferences", None) + if rels is None: + rels = getattr(product, "HasExternalReference", []) + reference_rels.update(rels) + + reference_rels = {rel for rel in reference_rels if rel.RelatingReference == self.settings["reference"]} + for rel in reference_rels: + related_objects = set(rel.RelatedResourceObjects) - non_rooted_products + if related_objects: + rel.RelatedResourceObjects = list(related_objects) + else: + self.file.remove(rel) # TODO: we only handle lightweight classifications here - if ( - not self.settings["reference"].ClassificationRefForObjects - and not self.settings["reference"].ExternalReferenceForResources - ): + referenced_elements = ifcopenshell.util.element.get_referenced_elements(self.settings["reference"]) + if not referenced_elements: self.file.remove(self.settings["reference"]) diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py index 1d77a7bb74..759fab9a5e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py @@ -17,11 +17,18 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api +from typing import Union class Usecase: - def __init__(self, file, product=None, constraint=None): - """Assigns a constraint to a product + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + constraint: ifcopenshell.entity_instance, + ): + """Assigns a constraint to a list of products This assigns a relationship between a product and a constraint, so that when a product's properties and quantities do not match the requirements @@ -31,36 +38,58 @@ class Usecase: constraints are inherited from the type. This way, it is not necessary to create lots of constraint assignments. - :param product: The product the constraint applies to. This is anything + :param products: The list of products the constraint applies to. This is anything which can have properties or quantities. - :type product: ifcopenshell.entity_instance.entity_instance + :type products: list[ifcopenshell.entity_instance.entity_instance] :param constraint: The IfcObjective constraint :type constraint: ifcopenshell.entity_instance.entity_instance :return: The new or updated IfcRelAssociatesConstraint relationship + or `None` if `products` was an empty list. :rtype: ifcopenshell.entity_instance.entity_instance """ self.file = file self.settings = { - "product": product, + "products": products, "constraint": constraint, } - def execute(self): - rel = self.get_constraint_rel() - related_objects = set(rel.RelatedObjects) if rel.RelatedObjects else set() - related_objects.add(self.settings["product"]) - rel.RelatedObjects = list(related_objects) - return rel + def execute(self) -> Union[ifcopenshell.entity_instance, None]: + products = set(self.settings["products"]) + if not products: + return + + self.constraint = self.settings["constraint"] + + rels = self.get_constraint_rels() + related_objects = set() + for rel in rels: + related_objects.update(rel.RelatedObjects) + + products_to_assign = products - related_objects + if not products_to_assign: + return rels[0] + + rel = next(iter(rels), None) + + if rel: + related_objects = set(rel.RelatedObjects) | products_to_assign + rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + return rel - def get_constraint_rel(self): - for rel in self.file.by_type("IfcRelAssociatesConstraint"): - if rel.RelatingConstraint == self.settings["constraint"]: - return rel return self.file.create_entity( "IfcRelAssociatesConstraint", **{ "GlobalId": ifcopenshell.guid.new(), - # TODO: owner history - "RelatingConstraint": self.settings["constraint"], + "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), + "RelatingConstraint": self.constraint, + "RelatedObjects": list(products_to_assign), } ) + + def get_constraint_rels(self) -> list[ifcopenshell.entity_instance]: + rels = [] + for rel in self.file.get_inverse(self.constraint): + if rel.is_a("IfcRelAssociatesConstraint"): + rels.append(rel) + return rels diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py index 66b3bc94b9..dbc1e1b7fb 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py @@ -17,18 +17,24 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api import ifcopenshell.util.element class Usecase: - def __init__(self, file, product=None, constraint=None): - """Unassigns a constraint to a product + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + constraint: ifcopenshell.entity_instance, + ): + """Unassigns a constraint from a list of products The constraint will not be deleted and is available to be assigned to other products. - :param product: The product the constraint applies to. - :type product: ifcopenshell.entity_instance.entity_instance + :param products: The list of products the constraint applies to. + :type products: list[ifcopenshell.entity_instance.entity_instance] :param constraint: The IfcObjective constraint :type constraint: ifcopenshell.entity_instance.entity_instance :return: None @@ -36,14 +42,42 @@ class Usecase: """ self.file = file self.settings = { - "product": product, + "products": products, "constraint": constraint, } def execute(self): - for rel in self.settings["product"].HasAssociations: - if rel.is_a("IfcRelAssociatesConstraint") and rel.RelatingConstraint == self.settings["constraint"]: - history = rel.OwnerHistory - self.file.remove(rel) - if history: - ifcopenshell.util.element.remove_deep2(self.file, history) + products = set(self.settings["products"]) + if not products: + return + + self.constraint = self.settings["constraint"] + rels = self.get_constraint_rels() + related_objects = set() + for rel in rels: + related_objects.update(rel.RelatedObjects) + + if not related_objects.intersection(products): + return + + for rel in rels: + related_objects = set(rel.RelatedObjects) + if not related_objects.intersection(products): + continue + related_objects -= products + if related_objects: + rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + continue + + history = rel.OwnerHistory + self.file.remove(rel) + if history: + ifcopenshell.util.element.remove_deep2(self.file, history) + + def get_constraint_rels(self) -> list[ifcopenshell.entity_instance]: + rels = [] + for rel in self.file.get_inverse(self.constraint): + if rel.is_a("IfcRelAssociatesConstraint"): + rels.append(rel) + return rels diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item.py b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item.py index 13b4b41258..0674d1def4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item.py @@ -68,6 +68,6 @@ class Usecase: ) elif self.settings["cost_item"]: ifcopenshell.api.run( - "nest.assign_object", self.file, related_object=cost_item, relating_object=self.settings["cost_item"] + "nest.assign_object", self.file, related_objects=[cost_item], relating_object=self.settings["cost_item"] ) return cost_item diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py index 55c4660b9f..2127bde2a6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py @@ -17,42 +17,43 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api import ifcopenshell.util.element class Usecase: def __init__(self, file, cost_item=None): """Copies all cost items and related relationships - + The following relationships are also duplicated: - + * The copy will have the same attributes and property sets as the original cost item * The copy will be assigned to the parent cost schedule * The copy will have duplicated nested cost items - + :param cost_item: The cost item to be duplicated :type cost_item: ifcopenshell.entity_instance.entity_instance :return: The duplicated cost item or the list of duplicated cost items if the latter has children :rtype: ifcopenshell.entity_instance.entity_instance or list of ifcopenshell.entity_instance.entity_instance - + Example: .. code:: python - + # We have a cost item cost_item = CostItem(name="Design new feature", deadline="2023-03-01") - + # And now we have two - duplicated_cost_item = project.duplicate_cost_item(cost_item) - - + duplicated_cost_item = project.duplicate_cost_item(cost_item) + + """ self.file = file self.settings = {"cost_item": cost_item} - + def execute(self): self.new_cost_items = [] self.duplicate_cost_item(self.settings["cost_item"]) - + def duplicate_cost_item(self, cost_item): new_cost_item = ifcopenshell.util.element.copy_deep(self.file, cost_item) self.new_cost_items.append(new_cost_item) @@ -64,9 +65,7 @@ class Usecase: if inverse.is_a("IfcRelDefinesByProperties"): inverse = ifcopenshell.util.element.copy(self.file, inverse) inverse.RelatedObjects = [to_element] - pset = ifcopenshell.util.element.copy_deep( - self.file, inverse.RelatingPropertyDefinition - ) + pset = ifcopenshell.util.element.copy_deep(self.file, inverse.RelatingPropertyDefinition) inverse.RelatingPropertyDefinition = pset elif inverse.is_a("IfcRelNests") and inverse.RelatingObject == from_element: nested_cost_items = [e for e in inverse.RelatedObjects] @@ -78,16 +77,13 @@ class Usecase: inverse = ifcopenshell.util.element.copy(self.file, inverse) inverse.RelatingObject = to_element inverse.RelatedObjects = new_cost_items - for cost_item in new_cost_items: - ifcopenshell.api.run( - "nest.unassign_object", self.file, related_object=cost_item - ) - rel = ifcopenshell.api.run( - "nest.assign_object", - self.file, - related_object=cost_item, - relating_object=to_element, - ) + ifcopenshell.api.run("nest.unassign_object", self.file, related_objects=new_cost_items) + ifcopenshell.api.run( + "nest.assign_object", + self.file, + related_objects=new_cost_items, + relating_object=to_element, + ) # elif inverse.is_a("IfcRelAssignsToProduct"): # continue # to_element.IsDefinedBy = inverse.RelatingOrder @@ -107,4 +103,4 @@ class Usecase: elif isinstance(value, (tuple, list)) and from_element in value: new_value = list(value) new_value.append(to_element) - inverse[i] = new_value \ No newline at end of file + inverse[i] = new_value diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/add_reference.py b/src/ifcopenshell-python/ifcopenshell/api/document/add_reference.py index 47af28b089..bcec5da606 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/add_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/add_reference.py @@ -16,9 +16,11 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import ifcopenshell + class Usecase: - def __init__(self, file, information=None): + def __init__(self, file: ifcopenshell.file, information: ifcopenshell.entity_instance): """Creates a new reference to a document to assign to products A document may be associated with physical products, tasks, cost items, @@ -65,12 +67,14 @@ class Usecase: self.file = file self.settings = {"information": information} - def execute(self): + def execute(self) -> ifcopenshell.entity_instance: if self.file.schema == "IFC2X3": - reference = self.file.create_entity("IfcDocumentReference") + reference = self.file.create_entity("IfcDocumentReference", ItemReference="X") if self.settings["information"]: references = list(self.settings["information"].DocumentReferences or []) references.append(reference) self.settings["information"].DocumentReferences = references return reference - return self.file.create_entity("IfcDocumentReference", ReferencedDocument=self.settings["information"]) + return self.file.create_entity( + "IfcDocumentReference", ReferencedDocument=self.settings["information"], Identification="X" + ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py b/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py index c7f98e4517..f67cbe890a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py @@ -17,11 +17,19 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.element +from typing import Union class Usecase: - def __init__(self, file, product=None, document=None): - """Assigns a document to a product + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + document: ifcopenshell.entity_instance, + ): + """Assigns a document to a list of products An object may be assigned to zero, one, or multiple documents. Almost any object or property may be assigned to a document, though typically @@ -32,14 +40,16 @@ class Usecase: consistent with other external relationships (such as classification systems or libraries). - :param product: The object to associate the document to. This could be + :param product: The list of objects to associate the document to. This could be almost any sensible object in IFC. - :type product: ifcopenshell.entity_instance.entity_instance + :type product: list[ifcopenshell.entity_instance.entity_instance] :param document: The IfcDocumentReference to associate to, or alternatively an IfcDocumentInformation, though this is not recommended. :type document: ifcopenshell.entity_instance.entity_instance :return: The IfcRelAssociatesDocument relationship + or `None` if `products` was an empty list or all products were + already assigned to the `document`. :rtype: ifcopenshell.entity_instance.entity_instance Example: @@ -54,42 +64,51 @@ class Usecase: reference = ifcopenshell.api.run("document.add_reference", model, information=document) # Let's imagine storey represents an IfcBuildingStorey for the ground floor - ifcopenshell.api.run("document.assign_document", model, product=storey, document=reference) + ifcopenshell.api.run("document.assign_document", model, products=[storey], document=reference) """ self.file = file self.settings = { - "product": product, + "products": products, "document": document, } - def execute(self): - rel = self.get_document_rel() - related_objects = set(rel.RelatedObjects) if rel.RelatedObjects else set() - related_objects.add(self.settings["product"]) - rel.RelatedObjects = list(related_objects) + def execute(self) -> Union[ifcopenshell.entity_instance, None]: + # TODO: do we need to support non-ifcroot elements like we do in classification.add_reference? + # NOTE: reuses code from `library.assign_reference` + + referenced_elements = ifcopenshell.util.element.get_referenced_elements(self.settings["document"]) + products: set[ifcopenshell.entity_instance] = set(self.settings["products"]) + products = products - referenced_elements + + if not products: + return - def get_document_rel(self): if self.file.schema == "IFC2X3": - for rel in self.file.by_type("IfcRelAssociatesDocument"): - if rel.RelatingDocument == self.settings["document"]: - return rel + rel = next( + ( + r + for r in self.file.by_type("IfcRelAssociatesDocument") + if r.RelatingDocument == self.settings["document"] + ), + None, + ) else: - if ( - hasattr(self.settings["document"], "DocumentRefForObjects") - and self.settings["document"].DocumentRefForObjects - ): - return self.settings["document"].DocumentRefForObjects[0] - elif ( - hasattr(self.settings["document"], "DocumentInfoForObjects") - and self.settings["document"].DocumentInfoForObjects - ): - return self.settings["document"].DocumentInfoForObjects[0] + ifc_class = self.settings["document"].is_a() + if ifc_class == "IfcDocumentReference": + rel = next(iter(self.settings["document"].DocumentRefForObjects), None) + elif ifc_class == "IfcDocumentInformation": + rel = next(iter(self.settings["document"].DocumentInfoForObjects), None) - return self.file.create_entity( - "IfcRelAssociatesDocument", - **{ - "GlobalId": ifcopenshell.guid.new(), - "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), - "RelatingDocument": self.settings["document"], - } - ) + if not rel: + return self.file.create_entity( + "IfcRelAssociatesDocument", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", self.file), + RelatedObjects=list(products), + RelatingDocument=self.settings["document"], + ) + + related_objects = set(rel.RelatedObjects) | products + rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, element=rel) + return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/remove_reference.py b/src/ifcopenshell-python/ifcopenshell/api/document/remove_reference.py index 3a73f692bb..6eee90fb4d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/remove_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/remove_reference.py @@ -21,7 +21,7 @@ import ifcopenshell.util.element class Usecase: - def __init__(self, file, reference=None): + def __init__(self, file: ifcopenshell.file, reference: ifcopenshell.entity_instance): """Remove a document reference All associations with objects are removed. @@ -42,7 +42,7 @@ class Usecase: self.file = file self.settings = {"reference": reference} - def execute(self): + def execute(self) -> None: for rel in self.settings["reference"].DocumentRefForObjects or []: history = rel.OwnerHistory self.file.remove(rel) diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py b/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py index 5a08c13262..dd43573e65 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py @@ -17,16 +17,22 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api import ifcopenshell.util.element class Usecase: - def __init__(self, file, product=None, document=None): - """Unassigns a document and a product association + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + document: ifcopenshell.entity_instance, + ): + """Unassigns a document and an association to the list of products - :param product: The object that the document reference or information is + :param product: The list of objects that the document reference or information is related to. - :type product: ifcopenshell.entity_instance.entity_instance + :type product: list[ifcopenshell.entity_instance.entity_instance] :param document: The IfcDocumentReference (typically) or in rare cases the IfcDocumentInformation that is associated with the product :type document: ifcopenshell.entity_instance.entity_instance @@ -45,24 +51,39 @@ class Usecase: reference = ifcopenshell.api.run("document.add_reference", model, information=document) # Let's imagine storey represents an IfcBuildingStorey for the ground floor - ifcopenshell.api.run("document.assign_document", model, product=storey, document=reference) + ifcopenshell.api.run("document.assign_document", model, products=[storey], document=reference) # Now let's change our mind and remove the association - ifcopenshell.api.run("document.unassign_document", model, product=storey, document=reference) + ifcopenshell.api.run("document.unassign_document", model, products=[storey], document=reference) """ self.file = file self.settings = { - "product": product, + "products": products, "document": document, } def execute(self): - for rel in self.settings["product"].HasAssociations: - if rel.is_a("IfcRelAssociatesDocument") and rel.RelatingDocument == self.settings["document"]: - if len(rel.RelatedObjects) == 1: - history = rel.OwnerHistory - self.file.remove(rel) - if history: - ifcopenshell.util.element.remove_deep2(self.file, history) - else: - rel.RelatedObjects = [o for o in rel.RelatedObjects if o != self.settings["product"]] + # TODO: do we need to support non-ifcroot elements like we do in classification.add_reference? + # NOTE: reuses code from `library.un assign_reference` + + reference_rels: set[ifcopenshell.entity_instance] = set() + products = set(self.settings["products"]) + for product in products: + reference_rels.update(product.HasAssociations) + + reference_rels = { + rel + for rel in reference_rels + if rel.is_a("IfcRelAssociatesDocument") and rel.RelatingDocument == self.settings["document"] + } + + for rel in reference_rels: + related_objects = set(rel.RelatedObjects) - products + if related_objects: + rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + else: + history = rel.OwnerHistory + self.file.remove(rel) + if history: + ifcopenshell.util.element.remove_deep2(self.file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py index bb0ff285a1..ac2167a70e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py @@ -20,7 +20,7 @@ import ifcopenshell.util.unit class Usecase: - def __init__(self, file, **settings): + def __init__(self, file: ifcopenshell.file, **settings): self.file = file self.settings = { "context": None, # IfcGeometricRepresentationContext @@ -33,6 +33,7 @@ class Usecase: "faces": None, # A list of polygons, represented by vertex indices "coordinate_offset": None, # Optionally apply a vector offset to all coordinates "unit_scale": None, # A scale factor to apply for all vectors in case the unit is different + "force_faceted_brep": False, # Force using IfcFacetedBreps instead of IfcPolygonalFaceSets } for key, value in settings.items(): self.settings[key] = value @@ -43,7 +44,7 @@ class Usecase: return self.create_mesh_representation() def create_mesh_representation(self): - if self.file.schema == "IFC2X3": + if self.settings["force_faceted_brep"] or self.file.schema == "IFC2X3": return self.create_faceted_brep() return self.create_polygonal_face_set() diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py index 9aef069f0d..d7893ea317 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py @@ -20,7 +20,7 @@ import ifcopenshell.util.element class Usecase: - def __init__(self, file, **settings): + def __init__(self, file: ifcopenshell.file, representation: ifcopenshell.entity_instance): """Remove a representation. Also purges representation items and their related elements @@ -35,11 +35,9 @@ class Usecase: :rtype: None """ self.file = file - self.settings = {"representation": None} - for key, value in settings.items(): - self.settings[key] = value + self.settings = {"representation": representation} - def execute(self): + def execute(self) -> None: styled_items = set() presentation_layer_assignments = set() textures = set() diff --git a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py index 9e34626e4a..77554e1f7f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py +++ b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py @@ -26,7 +26,7 @@ class Usecase: surveyor, and a third-party digital engineer with expertise in IFC to moderate. For more information, read the BlenderBIM Add-on documentation for Georeferencing: - https://blenderbim.org/docs/users/georeferencing.html + https://docs.blenderbim.org/users/georeferencing.html For more information about the attributes and data types of an IfcMapConversion, consult the IFC documentation. diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py index 450dcd5c20..78d787f741 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py +++ b/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py @@ -18,10 +18,13 @@ import ifcopenshell import ifcopenshell.api +from typing import Union class Usecase: - def __init__(self, file, products=None, group=None): + def __init__( + self, file: ifcopenshell.file, products: list[ifcopenshell.entity_instance], group: ifcopenshell.entity_instance + ): """Assigns products to a group If a product is already assigned to the group, it will not be assigned @@ -32,7 +35,8 @@ class Usecase: :param group: The IfcGroup to assign the products to :type group: ifcopenshell.entity_instance.entity_instance :return: The IfcRelAssignsToGroup relationship - :rtype: ifcopenshell.entity_instance.entity_instance + or `None` if `products` was empty list. + :rtype: Union[ifcopenshell.entity_instance.entity_instance, None] Example: @@ -48,7 +52,10 @@ class Usecase: "group": group, } - def execute(self): + def execute(self) -> Union[ifcopenshell.entity_instance, None]: + if not self.settings["products"]: + return + if not self.settings["group"].IsGroupedBy: return self.file.create_entity( "IfcRelAssignsToGroup", @@ -61,7 +68,9 @@ class Usecase: ) rel = self.settings["group"].IsGroupedBy[0] related_objects = set(rel.RelatedObjects) or set() - for obj in self.settings["products"]: - related_objects.add(obj) - rel.RelatedObjects = list(related_objects) + products = set(self.settings["products"]) + if products.issubset(related_objects): + return rel + rel.RelatedObjects = list(related_objects | products) ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/unassign_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/unassign_group.py index 161b4b5fb5..57d8d88556 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/group/unassign_group.py +++ b/src/ifcopenshell-python/ifcopenshell/api/group/unassign_group.py @@ -22,13 +22,13 @@ import ifcopenshell.util.element class Usecase: - def __init__(self, file, product=None, group=None): - """Unassigns a product from a group + def __init__(self, file: ifcopenshell.file, products: list[ifcopenshell.entity_instance], group: ifcopenshell.entity_instance): + """Unassigns products from a group If the product isn't assigned to the group, nothing will happen. - :param product: A IfcProduct element to unassign from the group - :type product: ifcopenshell.entity_instance.entity_instance + :param products: A list of IfcProduct elements to unassign from the group + :type products: list[ifcopenshell.entity_instance.entity_instance] :param group: The IfcGroup to unassign from :type group: ifcopenshell.entity_instance.entity_instance :return: None @@ -43,21 +43,22 @@ class Usecase: ifcopenshell.api.run("group.assign_group", model, products=furniture, group=group) bad_furniture = furniture[0] - ifcopenshell.api.run("group.unassign_group", model, product=bad_furniture, group=group) + ifcopenshell.api.run("group.unassign_group", model, products=[bad_furniture], group=group) """ self.file = file self.settings = { - "product": product, + "products": products, "group": group, } - def execute(self): + def execute(self) -> None: if not self.settings["group"].IsGroupedBy: return rel = self.settings["group"].IsGroupedBy[0] related_objects = set(rel.RelatedObjects) or set() - related_objects.remove(self.settings["product"]) - if len(related_objects): + products = set(self.settings["products"]) + related_objects -= products + if related_objects: rel.RelatedObjects = list(related_objects) ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) else: diff --git a/src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py b/src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py index 2cdcb58269..21a4ceb2bf 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py +++ b/src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py @@ -20,8 +20,10 @@ import ifcopenshell class Usecase: - def __init__(self, file, item=None, layer=None): - """Assigns a representation item to a layer + def __init__( + self, file: ifcopenshell.file, items: list[ifcopenshell.entity_instance], layer: ifcopenshell.entity_instance + ): + """Assigns representation items to a layer In IFC, instead of objects being assigned to layers, representation items are assigned to layers. Representation items are portions of the @@ -30,9 +32,9 @@ class Usecase: its frame) assigned to one layer, and another portion (e.g. the glazing panels) assigned to another layer. - :param item: The IfcRepresentationItem to assign to the layer. This - should be one of the items in the object's IfcShapeRepresentation. - :type item: ifcopenshell.entity_instance.entity_instance + :param items: The list of IfcRepresentationItems to assign to the layer. This + should be the items from the object's IfcShapeRepresentation. + :type items: list[ifcopenshell.entity_instance.entity_instance] :param layer: The IfcPresentationLayerAssignment layer to assign the item to. :type layer: ifcopenshell.entity_instance.entity_instance @@ -62,15 +64,19 @@ class Usecase: # And assign our wall representation item (in this example, there is # only one item) to the layer. - ifcopenshell.api.run("layer.assign_layer", model, item=representation.Items[0], layer=layer) + ifcopenshell.api.run("layer.assign_layer", model, items=[representation.Items[0]], layer=layer) """ self.file = file self.settings = { - "item": item, + "items": items, "layer": layer, } - def execute(self): - assigned_items = set(self.settings["layer"].AssignedItems or []) - assigned_items.add(self.settings["item"]) - self.settings["layer"].AssignedItems = list(assigned_items) + def execute(self) -> None: + # support AssignedItems == None since layer might just got created + layer = self.settings["layer"] + assigned_items = set(layer.AssignedItems or []) + items = set(self.settings["items"]) + if items.issubset(assigned_items): + return + layer.AssignedItems = list(assigned_items | items) diff --git a/src/ifcopenshell-python/ifcopenshell/api/layer/unassign_layer.py b/src/ifcopenshell-python/ifcopenshell/api/layer/unassign_layer.py index 229c91a012..657db6ac9e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/layer/unassign_layer.py +++ b/src/ifcopenshell-python/ifcopenshell/api/layer/unassign_layer.py @@ -16,16 +16,23 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import ifcopenshell +import ifcopenshell.util.element + class Usecase: - def __init__(self, file, item=None, layer=None): - """Unassigns an item from a layer + def __init__( + self, file: ifcopenshell.file, items: list[ifcopenshell.entity_instance], layer: ifcopenshell.entity_instance + ): + """Unassigns representation items from a layer If the representation item isn't assigned to the layer, nothing will happen. + If after unassignment layer won't have any assigned items it will be + removed to keep IFC valid. - :param item: An IfcRepresentationItem element to unassign - :type item: ifcopenshell.entity_instance.entity_instance + :param items: A list IfcRepresentationItem elements to unassign + :type items: list[ifcopenshell.entity_instance.entity_instance] :param layer: The IfcPresentationLayerAssignment to unassign from :type layer: ifcopenshell.entity_instance.entity_instance :return: None @@ -54,18 +61,27 @@ class Usecase: # And assign our wall representation item (in this example, there is # only one item) to the layer. - ifcopenshell.api.run("layer.assign_layer", model, item=representation.Items[0], layer=layer) + ifcopenshell.api.run("layer.assign_layer", model, items=[representation.Items[0]], layer=layer) # Let's undo it! - ifcopenshell.api.run("layer.unassign_layer", model, item=representation.Items[0], layer=layer) + ifcopenshell.api.run("layer.unassign_layer", model, items=[representation.Items[0]], layer=layer) """ self.file = file self.settings = { - "item": item, + "items": items, "layer": layer, } def execute(self): - assigned_items = set(self.settings["layer"].AssignedItems) or set() - assigned_items.remove(self.settings["item"]) - self.settings["layer"].AssignedItems = list(assigned_items) + layer = self.settings["layer"] + assigned_items = set(layer.AssignedItems) or set() + items = set(self.settings["items"]) + if not items.issubset(assigned_items): + return + assigned_items = list(assigned_items - items) + + # keep IFC valid in case if there are no items left + if assigned_items: + layer.AssignedItems = assigned_items + else: + self.file.remove(layer) diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/add_reference.py b/src/ifcopenshell-python/ifcopenshell/api/library/add_reference.py index 1ac3f87eb9..0a109b2118 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/library/add_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/library/add_reference.py @@ -16,9 +16,11 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import ifcopenshell + class Usecase: - def __init__(self, file, library=None): + def __init__(self, file: ifcopenshell.file, library: ifcopenshell.entity_instance): """Adds a new reference to a library A library represents an external data source, such as a database, @@ -54,7 +56,7 @@ class Usecase: "library": library, } - def execute(self): + def execute(self) -> ifcopenshell.entity_instance: if self.file.schema == "IFC2X3": reference = self.file.createIfcLibraryReference() references = list(self.settings["library"].LibraryReference or []) diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/assign_reference.py b/src/ifcopenshell-python/ifcopenshell/api/library/assign_reference.py index a1f3f83d17..8ee4a21eb5 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/library/assign_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/library/assign_reference.py @@ -17,23 +17,30 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.element +from typing import Union class Usecase: - def __init__(self, file, product=None, reference=None): - """Associates a product with a library reference + def __init__( + self, file: ifcopenshell.file, products: ifcopenshell.entity_instance, reference: ifcopenshell.entity_instance + ): + """Associates a list products with a library reference A product may be associated with zero, one, or many references across multiple libraries. See ifcopenshell.api.library.add_reference for more detail about how references work. - :param product: The IfcProduct you want to associate with the reference - :type product: ifcopenshell.entity_instance.entity_instance + :param products: The list of IfcProducts you want to associate with the reference + :type products: list[ifcopenshell.entity_instance.entity_instance] :param reference: The IfcLibraryReference you want the product to be associated with. :type reference: ifcopenshell.entity_instance.entity_instance :return: The IfcRelAssociatesLibrary relationship entity - :rtype: ifcopenshell.entity_instance.entity_instance + or `None` if `products` was an empty list or all products were + already assigned to the `reference`. + :rtype: Union[ifcopenshell.entity_instance.entity_instance, None] Example: @@ -51,40 +58,46 @@ class Usecase: ifc_class="IfcUnitaryEquipment", predefined_type="AIRHANDLER") # And now assign the IFC model's AHU with its Brickschema counterpart - ifcopenshell.api.run("library.assign_reference", model, reference=reference, product=ahu) + ifcopenshell.api.run("library.assign_reference", model, reference=reference, products=[ahu]) """ self.file = file self.settings = { - "product": product, + "products": products, "reference": reference, } - def execute(self): + def execute(self) -> Union[ifcopenshell.entity_instance, None]: + # TODO: do we need to support non-ifcroot elements like we do in classification.add_reference? + + referenced_elements = ifcopenshell.util.element.get_referenced_elements(self.settings["reference"]) + products: set[ifcopenshell.entity_instance] = set(self.settings["products"]) + products = products - referenced_elements + + if not products: + return + if self.file.schema == "IFC2X3": - rels = self.get_ifc2x3_rels() + rel = next( + ( + r + for r in self.file.by_type("IfcRelAssociatesLibrary") + if r.RelatingLibrary == self.settings["reference"] + ), + None, + ) else: - rels = self.settings["reference"].LibraryRefForObjects - if not rels: + rel = next(iter(self.settings["reference"].LibraryRefForObjects), None) + + if not rel: return self.file.create_entity( "IfcRelAssociatesLibrary", GlobalId=ifcopenshell.guid.new(), OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", self.file), - RelatedObjects=[self.settings["product"]], + RelatedObjects=list(products), RelatingLibrary=self.settings["reference"], ) - for rel in rels: - if self.settings["product"] in rel.RelatedObjects: - return rel - - rel = rels[0] - related_objects = list(rel.RelatedObjects) - related_objects.append(self.settings["product"]) - rel.RelatedObjects = related_objects + related_objects = set(rel.RelatedObjects) | products + rel.RelatedObjects = list(related_objects) ifcopenshell.api.run("owner.update_owner_history", self.file, element=rel) return rel - - def get_ifc2x3_rels(self): - return [ - r for r in self.file.by_type("IfcRelAssociatesLibrary") if r.RelatingLibrary == self.settings["reference"] - ] diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/remove_reference.py b/src/ifcopenshell-python/ifcopenshell/api/library/remove_reference.py index c1b15847fc..9a5851827a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/library/remove_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/library/remove_reference.py @@ -21,7 +21,7 @@ import ifcopenshell.util.element class Usecase: - def __init__(self, file, reference=None): + def __init__(self, file: ifcopenshell.file, reference: ifcopenshell.entity_instance): """Removes a library reference Any products which have relationships to this reference will not be @@ -44,7 +44,7 @@ class Usecase: self.file = file self.settings = {"reference": reference} - def execute(self): + def execute(self) -> None: for rel in self.settings["reference"].LibraryRefForObjects: history = rel.OwnerHistory self.file.remove(rel) diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py b/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py index 639a3dd35b..b650ffba75 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py @@ -18,18 +18,24 @@ import ifcopenshell import ifcopenshell.util.element +import ifcopenshell.api class Usecase: - def __init__(self, file, reference=None, product=None): - """Unassigns a product from a reference + def __init__( + self, + file: ifcopenshell.file, + reference: ifcopenshell.entity_instance, + products: list[ifcopenshell.entity_instance], + ): + """Unassigns a product of products from a reference If the product isn't assigned to the reference, nothing will happen. :param reference: The IfcLibraryReference to unassign from :type reference: ifcopenshell.entity_instance.entity_instance - :param product: A IfcProduct element to unassign from the reference - :type product: ifcopenshell.entity_instance.entity_instance + :param products: A list of IfcProduct elements to unassign from the reference + :type products: list[ifcopenshell.entity_instance.entity_instance] :return: None :rtype: None @@ -49,27 +55,36 @@ class Usecase: ifc_class="IfcUnitaryEquipment", predefined_type="AIRHANDLER") # And now assign the IFC model's AHU with its Brickschema counterpart - ifcopenshell.api.run("library.assign_reference", model, reference=reference, product=ahu) + ifcopenshell.api.run("library.assign_reference", model, reference=reference, products=[ahu]) # Let's change our mind and unassign it. - ifcopenshell.api.run("library.unassign_reference", model, reference=reference, product=ahu) + ifcopenshell.api.run("library.unassign_reference", model, reference=reference, products=[ahu]) """ self.file = file - self.settings = {"reference": reference, "product": product} + self.settings = {"reference": reference, "products": products} def execute(self): - rels = self.settings["reference"].LibraryRefForObjects - if not rels: - return - for rel in rels: - if self.settings["product"] in rel.RelatedObjects: - if len(rel.RelatedObjects) == 1: - history = rel.OwnerHistory - self.file.remove(rel) - if history: - ifcopenshell.util.element.remove_deep2(self.file, history) - continue - related_objects = list(rel.RelatedObjects) - related_objects.remove(self.settings["product"]) - rel.RelatedObjects = related_objects + # TODO: do we need to support non-ifcroot elements like we do in classification.add_reference? + + reference_rels: set[ifcopenshell.entity_instance] = set() + products = set(self.settings["products"]) + for product in products: + reference_rels.update(product.HasAssociations) + + reference_rels = { + rel + for rel in reference_rels + if rel.is_a("IfcRelAssociatesLibrary") and rel.RelatingLibrary == self.settings["reference"] + } + + for rel in reference_rels: + related_objects = set(rel.RelatedObjects) - products + if related_objects: + rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + else: + history = rel.OwnerHistory + self.file.remove(rel) + if history: + ifcopenshell.util.element.remove_deep2(self.file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/add_constituent.py b/src/ifcopenshell-python/ifcopenshell/api/material/add_constituent.py index e3cb94ef95..c45b5fb14d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/add_constituent.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/add_constituent.py @@ -78,7 +78,7 @@ class Usecase: # our window too, but to keep this example simple, geometry is # optional and it is enough to say that this window is made out of # aluminium and glass. - ifcopenshell.api.run("material.assign_material", model, product=window_type, material=material_set) + ifcopenshell.api.run("material.assign_material", model, products=[window_type], material=material_set) """ self.file = file self.settings = {"constituent_set": constituent_set, "material": material} diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/add_layer.py b/src/ifcopenshell-python/ifcopenshell/api/material/add_layer.py index 89c3e6b2a1..d0b36b4ec6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/add_layer.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/add_layer.py @@ -76,7 +76,7 @@ class Usecase: ifcopenshell.api.run("material.edit_layer", model, layer=layer, attributes={"LayerThickness": 13}) # Great! Let's assign our material set to our wall type. - ifcopenshell.api.run("material.assign_material", model, product=wall_type, material=material_set) + ifcopenshell.api.run("material.assign_material", model, products=[wall_type], material=material_set) """ self.file = file self.settings = {"layer_set": layer_set, "material": material} diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/add_list_item.py b/src/ifcopenshell-python/ifcopenshell/api/material/add_list_item.py index 4c80a5758c..92b873b198 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/add_list_item.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/add_list_item.py @@ -77,7 +77,7 @@ class Usecase: # our window too, but to keep this example simple, geometry is # optional and it is enough to say that this window is made out of # aluminium and glass. - ifcopenshell.api.run("material.assign_material", model, product=window_type, material=material_set) + ifcopenshell.api.run("material.assign_material", model, products=[window_type], material=material_set) """ self.file = file self.settings = {"material_list": material_list, "material": material} diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/add_material.py b/src/ifcopenshell-python/ifcopenshell/api/material/add_material.py index 07b5882932..3959e4becd 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/add_material.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/add_material.py @@ -68,7 +68,7 @@ class Usecase: # Assign the concrete material to that bench. Note that no colour # "Style" has been specified. - ifcopenshell.api.run("material.assign_material", model, product=concrete_bench, material=concrete) + ifcopenshell.api.run("material.assign_material", model, products=[concrete_bench], material=concrete) """ self.file = file self.settings = {"name": name or "Unnamed", "category": category} diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/add_material_set.py b/src/ifcopenshell-python/ifcopenshell/api/material/add_material_set.py index 802d1d38cc..5ec8a36623 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/add_material_set.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/add_material_set.py @@ -100,7 +100,7 @@ class Usecase: ifcopenshell.api.run("material.edit_layer", model, layer=layer, attributes={"LayerThickness": 13}) # Great! Let's assign our material set to our wall type. - ifcopenshell.api.run("material.assign_material", model, product=wall_type, material=material_set) + ifcopenshell.api.run("material.assign_material", model, products=[wall_type], material=material_set) """ self.file = file self.settings = {"name": name or "Unnamed", "set_type": set_type or "IfcMaterialConstituentSet"} diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/add_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/add_profile.py index df53de69f8..8b322b71f7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/add_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/add_profile.py @@ -79,7 +79,7 @@ class Usecase: profile_set=material_set, material=steel, profile=hea100) # Great! Let's assign our material set to our beam type. - ifcopenshell.api.run("material.assign_material", model, product=beam_type, material=material_set) + ifcopenshell.api.run("material.assign_material", model, products=[beam_type], material=material_set) """ self.file = file self.settings = {"profile_set": profile_set, "material": material, "profile": profile} diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/assign_material.py b/src/ifcopenshell-python/ifcopenshell/api/material/assign_material.py index 0c27e4ea46..0445f2a995 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/assign_material.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/assign_material.py @@ -20,11 +20,20 @@ import ifcopenshell import ifcopenshell.api import ifcopenshell.util.element import ifcopenshell.util.representation +from typing import Optional, Union class Usecase: - def __init__(self, file, product=None, type="IfcMaterial", material=None): - """Assigns a material to a product + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + type: str = "IfcMaterial", + material: Optional[ifcopenshell.entity_instance] = None, + ): + """Assigns a material to the list of products + + Will unassign previously assigned material. When a material is assigned to a product, it means that the product is made out of that material. In its simplest form, a single material may @@ -53,19 +62,27 @@ class Usecase: This allows individual occurrences to override the layered or profiled construction offset from a reference line. - :param product: The IfcProduct to assign the material or material set + :param products: The list of IfcProducts to assign the material or material set to. - :type product: ifcopenshell.entity_instance.entity_instance + :type products: list[ifcopenshell.entity_instance.entity_instance] :param type: Choose from "IfcMaterial", "IfcMaterialConstituentSet", "IfcMaterialLayerSet", "IfcMaterialLayerSetUsage", "IfcMaterialProfileSet", "IfcMaterialProfileSetUsage", or "IfcMaterialList". Note that "Set Usages" may only be assigned to - occurrences, not types. + occurrences, not types. Defaults to "IfcMaterial". :type type: str :param material: The IfcMaterial or material set you are assigning here. - :type material: ifcopenshell.entity_instance.entity_instance - :return: The IfcRelAssociatesMaterial entity - :rtype: ifcopenshell.entity_instance.entity_instance + If type is Usage then no need to provide `material`, it will be deduced + from the element type automatically. + :type material: ifcopenshell.entity_instance.entity_instance, optional + :return: IfcRelAssociatesMaterial entity + or a list of IfcRelAssociatesMaterial entities + (possible if `type` is Usage + and `products` require different Usages) + or `None` if `products` was empty list. + :rtype: Union[ + ifcopenshell.entity_instance.entity_instance, + list[ifcopenshell.entity_instance.entity_instance], None] Example: @@ -78,15 +95,15 @@ class Usecase: # material. Let's assign it to the type. bench_type = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcFurnitureType") ifcopenshell.api.run("material.assign_material", model, - product=bench_type, type="IfcMaterial", material=concrete) + products=[bench_type], type="IfcMaterial", material=concrete) # Let's imagine there are a two occurrences of this bench. It's not # necessary to assign any material to these benches as they # automatically inherit the material from the type. bench1 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcFurniture") bench2 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcFurniture") - ifcopenshell.api.run("type.assign_type", model, related_object=bench1, relating_type=bench_type) - ifcopenshell.api.run("type.assign_type", model, related_object=bench2, relating_type=bench_type) + ifcopenshell.api.run("type.assign_type", model, related_objects=[bench1], relating_type=bench_type) + ifcopenshell.api.run("type.assign_type", model, related_objects=[bench2], relating_type=bench_type) # If we have a concrete wall, we should use a layer set. Again, # let's start with a wall type, not occurrences. @@ -102,11 +119,11 @@ class Usecase: # Our wall type now has the layer set assigned to it ifcopenshell.api.run("material.assign_material", model, - product=wall_type, type="IfcMaterialLayerSet", material=material_set) + products=[wall_type], type="IfcMaterialLayerSet", material=material_set) # Let's imagine an occurrence of this wall type. wall = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWall") - ifcopenshell.api.run("type.assign_type", model, related_object=wall, relating_type=wall_type) + ifcopenshell.api.run("type.assign_type", model, related_objects=[wall], relating_type=wall_type) # Our wall occurrence needs to have a "set usage" which describes # how the layers relate to a reference line (typically a 2D line @@ -114,7 +131,7 @@ class Usecase: # they automatically detect the inherited material set from the # type. You'd write similar code for a profile set. ifcopenshell.api.run("material.assign_material", model, - product=wall, type="IfcMaterialLayerSetUsage") + products=[wall], type="IfcMaterialLayerSetUsage") # To be complete, let's create the wall's axis and body # representation. Notice how the axis guides the walls "reference @@ -130,85 +147,130 @@ class Usecase: ifcopenshell.api.run("geometry.edit_object_placement", model, product=wall) """ self.file = file - self.settings = {"product": product, "type": type, "material": material} + self.settings = {"products": products, "type": type, "material": material} + + def execute(self) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance], None]: + self.products: set[ifcopenshell.entity_instance] = set(self.settings["products"]) + if not self.products: + return + + # NOTE: we always reassign material, even if it might be assigned before + products_to_unassign_material = [p for p in self.products if ifcopenshell.util.element.get_material(p)] + if products_to_unassign_material: + ifcopenshell.api.run("material.unassign_material", self.file, products=products_to_unassign_material) - def execute(self): - material = ifcopenshell.util.element.get_material(self.settings["product"]) - if material: - ifcopenshell.api.run("material.unassign_material", self.file, product=self.settings["product"]) if self.settings["type"] == "IfcMaterial" or ( self.settings["material"] and not self.settings["material"].is_a("IfcMaterial") ): return self.assign_ifc_material() + elif self.settings["type"] == "IfcMaterialConstituentSet": material_set = self.file.create_entity(self.settings["type"]) return self.create_material_association(material_set) + elif self.settings["type"] == "IfcMaterialLayerSet": material_set = self.file.create_entity(self.settings["type"]) return self.create_material_association(material_set) + elif self.settings["type"] == "IfcMaterialLayerSetUsage": - element_type = ifcopenshell.util.element.get_type(self.settings["product"]) - if element_type: - element_type_material = ifcopenshell.util.element.get_material(element_type) - if element_type_material and element_type_material.is_a("IfcMaterialLayerSet"): - material_set = element_type_material + # NOTE: might return list of rels + types_to_products_layers_sets: dict[ + tuple[Union[ifcopenshell.entity_instance, None], str], tuple[ifcopenshell.entity_instance, list] + ] = dict() + + AXIS3_CLASSES = [ + "IfcSlab", + "IfcSlabStandardCase", + "IfcSlabElementedCase", + "IfcRoof", + "IfcRamp", + "IfcPlate", + "IfcPlateStandardCase", + ] + for product in self.products: + element_type = ifcopenshell.util.element.get_type(product) + layer_set_direction = "AXIS3" if product.is_a() in AXIS3_CLASSES else "AXIS2" + material_layer_type = (element_type, layer_set_direction) + + if material_layer_type in types_to_products_layers_sets: + types_to_products_layers_sets[material_layer_type][1].append(product) + continue + + if element_type: + element_type_material = ifcopenshell.util.element.get_material(element_type) + if element_type_material and element_type_material.is_a("IfcMaterialLayerSet"): + material_set = element_type_material + else: + material_set = self.file.create_entity("IfcMaterialLayerSet") else: material_set = self.file.create_entity("IfcMaterialLayerSet") - else: - material_set = self.file.create_entity("IfcMaterialLayerSet") - material_set_usage = self.create_layer_set_usage(material_set) - return self.create_material_association(material_set_usage) + types_to_products_layers_sets[material_layer_type] = (material_set, [product]) + + rels = [ + self.create_layer_set_usage(material_set, layer_set_direction, products) + for (_, layer_set_direction), (material_set, products) in types_to_products_layers_sets.items() + ] + return rels[0] if len(rels) == 1 else rels + elif self.settings["type"] == "IfcMaterialProfileSet": material_set = self.file.create_entity(self.settings["type"]) return self.create_material_association(material_set) + elif self.settings["type"] == "IfcMaterialProfileSetUsage": - element_type = ifcopenshell.util.element.get_type(self.settings["product"]) - if element_type: - element_type_material = ifcopenshell.util.element.get_material(element_type) - if element_type_material and element_type_material.is_a("IfcMaterialProfileSet"): - material_set = element_type_material + # NOTE: might return list of rels + types_to_products_profile_sets: dict[ + Union[ifcopenshell.entity_instance, None], tuple[ifcopenshell.entity_instance, list] + ] = dict() + for product in self.products: + element_type = ifcopenshell.util.element.get_type(product) + if element_type in types_to_products_profile_sets: + types_to_products_profile_sets[element_type][1].append(product) + continue + if element_type: + element_type_material = ifcopenshell.util.element.get_material(element_type) + if element_type_material and element_type_material.is_a("IfcMaterialProfileSet"): + material_set = element_type_material + else: + material_set = self.file.create_entity("IfcMaterialProfileSet") else: material_set = self.file.create_entity("IfcMaterialProfileSet") - else: - material_set = self.file.create_entity("IfcMaterialProfileSet") + types_to_products_profile_sets[element_type] = (material_set, [product]) + + rels = [] + for _, (material_set, products) in types_to_products_profile_sets.items(): + self.update_representation_profile(material_set, products) + material_set_usage = self.create_profile_set_usage(material_set) + rels.append(self.create_material_association(material_set_usage, products)) + return rels[0] if len(rels) == 1 else rels - self.update_representation_profile(material_set) - material_set_usage = self.create_profile_set_usage(material_set) - return self.create_material_association(material_set_usage) elif self.settings["type"] == "IfcMaterialList": material_set = self.file.create_entity(self.settings["type"]) material_set.Materials = [self.settings["material"]] return self.create_material_association(material_set) - def update_representation_profile(self, material_set): + def update_representation_profile( + self, material_set: ifcopenshell.entity_instance, products: list[ifcopenshell.entity_instance] + ) -> None: profile = material_set.CompositeProfile if not profile and material_set.MaterialProfiles: profile = material_set.MaterialProfiles[0].Profile if not profile: return - representation = ifcopenshell.util.representation.get_representation( - self.settings["product"], "Model", "Body", "MODEL_VIEW" - ) - if not representation: - return - for subelement in self.file.traverse(representation): - if subelement.is_a("IfcSweptAreaSolid"): - subelement.SweptArea = profile + for product in products: + representation = ifcopenshell.util.representation.get_representation(product, "Model", "Body", "MODEL_VIEW") + if not representation: + return + for subelement in self.file.traverse(representation): + if subelement.is_a("IfcSweptAreaSolid"): + subelement.SweptArea = profile - def create_layer_set_usage(self, material_set): - if self.settings["product"].is_a() in [ - "IfcSlab", - "IfcSlabStandardCase", - "IfcSlabElementedCase", - "IfcRoof", - "IfcRamp", - "IfcPlate", - "IfcPlateStandardCase", - ]: - layer_set_direction = "AXIS3" - else: - layer_set_direction = "AXIS2" - return self.file.create_entity( + def create_layer_set_usage( + self, + material_set: ifcopenshell.entity_instance, + layer_set_direction: str, + products: list[ifcopenshell.entity_instance], + ) -> ifcopenshell.entity_instance: + usage = self.file.create_entity( "IfcMaterialLayerSetUsage", **{ "ForLayerSet": material_set, @@ -217,36 +279,48 @@ class Usecase: "OffsetFromReferenceLine": 0, } ) + return self.create_material_association(usage, products) - def create_profile_set_usage(self, material_set): + def create_profile_set_usage(self, material_set: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: return self.file.create_entity("IfcMaterialProfileSetUsage", **{"ForProfileSet": material_set}) - def assign_ifc_material(self): - rel = self.get_rel_associates_material(self.settings["material"]) + def assign_ifc_material(self) -> ifcopenshell.entity_instance: + material = self.settings["material"] or self.file.create_entity("IfcMaterial") + rel = self.get_rel_associates_material(material) if not rel: - return self.create_material_association(self.settings["material"]) - related_objects = list(rel.RelatedObjects) - related_objects.append(self.settings["product"]) - rel.RelatedObjects = related_objects + return self.create_material_association(material) + previous_related_objects = set(rel.RelatedObjects) + rel.RelatedObjects = list(previous_related_objects | self.products) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) return rel - def create_material_association(self, relating_material): + def create_material_association( + self, + relating_material: ifcopenshell.entity_instance, + products: Optional[list[ifcopenshell.entity_instance]] = None, + ) -> ifcopenshell.entity_instance: + if products is None: + products = list(self.products) return self.file.create_entity( "IfcRelAssociatesMaterial", **{ "GlobalId": ifcopenshell.guid.new(), - "RelatedObjects": [self.settings["product"]], + "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), + "RelatedObjects": products, "RelatingMaterial": relating_material, } ) - def get_rel_associates_material(self, material): + def get_rel_associates_material( + self, material: ifcopenshell.entity_instance + ) -> Union[ifcopenshell.entity_instance, None]: if self.file.schema == "IFC2X3" or material.is_a("IfcMaterialList"): - rel = [ - r - for r in self.file.by_type("IfcRelAssociatesMaterial") - if r.RelatingMaterial == self.settings["material"] - ] - return rel[0] if rel else None - if self.settings["material"].AssociatedTo: - return self.settings["material"].AssociatedTo[0] + return next( + ( + r + for r in self.file.by_type("IfcRelAssociatesMaterial") + if r.RelatingMaterial == self.settings["material"] + ), + None, + ) + return next(iter(material.AssociatedTo), None) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py index ea3ed7610a..4acd6637e7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py @@ -66,12 +66,12 @@ class Usecase: profile_set=material_set, material=steel, profile=hea100) # Great! Let's assign our material set to our beam type. - ifcopenshell.api.run("material.assign_material", model, product=beam_type, material=material_set) + ifcopenshell.api.run("material.assign_material", model, products=[beam_type], material=material_set) # Let's create an occurrence of this beam. beam = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBeam", name="B1.01") ifcopenshell.api.run("material.assign_material", model, - product=beam, type="IfcMaterialProfileSetUsage") + products=[beam], type="IfcMaterialProfileSetUsage") # Let's give a 1000mm long beam body representation. body = ifcopenshell.api.run("geometry.add_profile_representation", diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer_usage.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer_usage.py index d6900314ab..3e45c68c78 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer_usage.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer_usage.py @@ -55,11 +55,11 @@ class Usecase: # Our wall type now has the layer set assigned to it ifcopenshell.api.run("material.assign_material", model, - product=wall_type, type="IfcMaterialLayerSet", material=material_set) + products=[wall_type], type="IfcMaterialLayerSet", material=material_set) # Let's imagine an occurrence of this wall type. wall = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWall") - ifcopenshell.api.run("type.assign_type", model, related_object=wall, relating_type=wall_type) + ifcopenshell.api.run("type.assign_type", model, related_objects=[wall], relating_type=wall_type) # Our wall occurrence needs to have a "set usage" which describes # how the layers relate to a reference line (typically a 2D line @@ -67,7 +67,7 @@ class Usecase: # they automatically detect the inherited material set from the # type. You'd write similar code for a profile set. rel = ifcopenshell.api.run("material.assign_material", model, - product=wall, type="IfcMaterialLayerSetUsage") + products=[wall], type="IfcMaterialLayerSetUsage") # Let's change the offset from the reference line to be 200mm # instead of the default of 0mm. diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py index aace67e3d0..edd6f27fee 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py @@ -70,12 +70,12 @@ class Usecase: profile_set=material_set, material=steel, profile=hea100) # Great! Let's assign our material set to our beam type. - ifcopenshell.api.run("material.assign_material", model, product=beam_type, material=material_set) + ifcopenshell.api.run("material.assign_material", model, products=[beam_type], material=material_set) # Let's create an occurrence of this beam. beam = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBeam", name="B1.01") rel = ifcopenshell.api.run("material.assign_material", model, - product=beam, type="IfcMaterialProfileSetUsage") + products=[beam], type="IfcMaterialProfileSetUsage") # Let's give a 1000mm long beam body representation. body = ifcopenshell.api.run("geometry.add_profile_representation", diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/unassign_material.py b/src/ifcopenshell-python/ifcopenshell/api/material/unassign_material.py index e59e510c40..60fac1382e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/unassign_material.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/unassign_material.py @@ -17,12 +17,13 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api import ifcopenshell.util.element class Usecase: - def __init__(self, file, product=None): - """Removes any material relationship with a product + def __init__(self, file: ifcopenshell.file, products: list[ifcopenshell.entity_instance]): + """Removes any material relationship with the list of products A product can only have one material assigned to it, which is why it is not necessary to specify the material to unassign. The material is not @@ -30,8 +31,8 @@ class Usecase: If the product does not have a material, nothing happens. - :param product: The IfcProduct that may or may not have a material - :type product: ifcopenshell.entity_instance.entity_instance + :param products: The list IfcProducts that may or may not have a material + :type product: list[ifcopenshell.entity_instance.entity_instance] :return: None :rtype: None @@ -44,25 +45,39 @@ class Usecase: # Let's imagine a concrete bench made out of concrete. bench_type = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcFurnitureType") ifcopenshell.api.run("material.assign_material", model, - product=bench_type, type="IfcMaterial", material=concrete) + products=[bench_type], type="IfcMaterial", material=concrete) # Let's change our mind and remove the concrete assignment. The # concrete material still exists, but the bench is no longer made # out of concrete now. - ifcopenshell.api.run("material.unassign_material", model, product=bench_type) + ifcopenshell.api.run("material.unassign_material", model, products=[bench_type]) """ self.file = file - self.settings = {"product": product} + self.settings = {"products": products} - def execute(self): - if self.settings["product"].is_a("IfcTypeObject"): - material = ifcopenshell.util.element.get_material(self.settings["product"]) + def execute(self) -> None: + self.products = set(self.settings["products"]) + if not self.products: + return + + self.remove_material_usages_from_types() + self.unassign_materials() + + def remove_material_usages_from_types(self) -> None: + # remove material usages from types + for product in self.products: + if not product.is_a("IfcTypeObject"): + continue + material = ifcopenshell.util.element.get_material(product) + if not material: + continue if material.is_a() in ["IfcMaterialLayerSet", "IfcMaterialProfileSet"]: # Remove set usages for inverse in self.file.get_inverse(material): if self.file.schema == "IFC2X3": if not inverse.is_a("IfcMaterialLayerSetUsage"): continue + # in IFC2X3 there is no .AssociatedTo for inverse2 in self.file.get_inverse(inverse): if inverse2.is_a("IfcRelAssociatesMaterial"): history = inverse2.OwnerHistory @@ -73,21 +88,40 @@ class Usecase: if not inverse.is_a("IfcMaterialUsageDefinition"): continue for rel in inverse.AssociatedTo: + history = rel.OwnerHistory self.file.remove(rel) + if history: + ifcopenshell.util.element.remove_deep2(self.file, history) self.file.remove(inverse) - for rel in self.settings["product"].HasAssociations: - if rel.is_a("IfcRelAssociatesMaterial"): - if rel.RelatingMaterial.is_a() in ["IfcMaterialLayerSetUsage", "IfcMaterialProfileSetUsage"]: + def unassign_materials(self) -> None: + associations: set[ifcopenshell.entity_instance] = set() + for product in self.products: + associations.update(product.HasAssociations) + + # we ensure that `associations` won't have removed elements + # to avoid crash during `material_inverses.issubset(associations)` + while associations: + rel = next(iter(associations)) + + if not rel.is_a("IfcRelAssociatesMaterial"): + associations.remove(rel) + else: + material = rel.RelatingMaterial + related_objects = set(rel.RelatedObjects) - self.products + + if material.is_a() in ["IfcMaterialLayerSetUsage", "IfcMaterialProfileSetUsage"]: # Warning: this may leave the model in a non-compliant state. - if self.file.get_total_inverses(rel.RelatingMaterial) == 1 and len(rel.RelatedObjects) == 1: - self.file.remove(rel.RelatingMaterial) - if len(rel.RelatedObjects) == 1: + material_inverses = set(self.file.get_inverse(material)) + if material_inverses.issubset(associations) and not related_objects: + self.file.remove(material) + associations.remove(rel) + + if not related_objects: history = rel.OwnerHistory self.file.remove(rel) if history: ifcopenshell.util.element.remove_deep2(self.file, history) continue - related_objects = set(rel.RelatedObjects) - related_objects.remove(self.settings["product"]) rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) diff --git a/src/ifcopenshell-python/ifcopenshell/api/nest/assign_object.py b/src/ifcopenshell-python/ifcopenshell/api/nest/assign_object.py index e75b1ecf44..5617d39f06 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/nest/assign_object.py +++ b/src/ifcopenshell-python/ifcopenshell/api/nest/assign_object.py @@ -19,11 +19,17 @@ import ifcopenshell import ifcopenshell.api import ifcopenshell.util.element +from typing import Union class Usecase: - def __init__(self, file, related_object=None, relating_object=None): - """Assigns an object as a nested child to a parent host + def __init__( + self, + file: ifcopenshell.file, + related_objects: list[ifcopenshell.entity_instance], + relating_object: ifcopenshell.entity_instance, + ): + """Assigns objects as nested children to a parent host All physical IFC model elements must be part of a hierarchical tree called the "spatial decomposition", where large things are made up of @@ -72,14 +78,15 @@ class Usecase: ifcopenshell.api.cost.add_cost_item or ifcopenshell.api.sequence.add_task. - :param related_object: The child of the nesting relationship, typically - an IfcElement. - :type related_object: ifcopenshell.entity_instance.entity_instance + :param related_objects: The list of children of the nesting relationship, + typically IfcElements. + :type related_objects: list[ifcopenshell.entity_instance.entity_instance] :param relating_object: The host parent of the nesting relationship, typically an IfcElement. :type relating_object: ifcopenshell.entity_instance.entity_instance :return: The IfcRelNests relationship instance - :rtype: ifcopenshell.entity_instance.entity_instance + or `None` if `related_objects` was empty list. + :rtype: Union[ifcopenshell.entity_instance.entity_instance, None] Example: @@ -90,30 +97,58 @@ class Usecase: ifc_class="IfcSanitaryTerminal", predefined_type="SINK") faucet = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcValve", predefined_type="FAUCET") - ifcopenshell.api.run("nest.assign_object", model, related_object=faucet, relating_object=sink) + ifcopenshell.api.run("nest.assign_object", model, related_objects=[faucet], relating_object=sink) """ self.file = file - self.settings = {"related_object": related_object, "relating_object": relating_object} + self.settings = {"related_objects": related_objects, "relating_object": relating_object} - def execute(self): - nests = None - if self.settings["related_object"].Nests: - nests = self.settings["related_object"].Nests[0] - - is_nested_by = None - for rel in self.settings["relating_object"].IsNestedBy: - if rel.is_a("IfcRelNests"): - is_nested_by = rel - break - - if nests and nests == is_nested_by: + def execute(self) -> Union[ifcopenshell.entity_instance, None]: + if not self.settings["related_objects"]: return - if nests: - related_objects = list(nests.RelatedObjects) - related_objects.remove(self.settings["related_object"]) - if related_objects: - nests.RelatedObjects = related_objects + ifc2x3 = self.file.schema == "IFC2X3" + + related_objects = set(self.settings["related_objects"]) + relating_object = self.settings["relating_object"] + if ifc2x3: + is_nested_by = next((i for i in relating_object.IsDecomposedBy if i.is_a("IfcRelNests")), None) + else: + is_nested_by = next((i for i in relating_object.IsNestedBy), None) + + previous_nests_rels: set[ifcopenshell.entity_instance] = set() + objects_without_nests: list[ifcopenshell.entity_instance] = [] + objects_with_nests: list[ifcopenshell.entity_instance] = [] + + # check if there is anything to change + for object in related_objects: + if ifc2x3: + object_rel = next((i for i in object.Decomposes if i.is_a("IfcRelNests")), None) + else: + object_rel = next(iter(object.Nests), None) + + if object_rel is None: + objects_without_nests.append(object) + continue + + # either is_nested_by is None or product is part of different rel + if object_rel != is_nested_by: + previous_nests_rels.add(object_rel) + objects_with_nests.append(object) + + # products with already assigned nestings will be skipped + + objects_to_change = objects_without_nests + objects_with_nests + # nothing to change + if not objects_to_change: + return is_nested_by + + # NOTE: An object can both be nested and assigned to a container or an aggregate. + + # unassign elements from previous nests + for nests in previous_nests_rels: + cur_related_objects = set(nests.RelatedObjects) - related_objects + if cur_related_objects: + nests.RelatedObjects = list(cur_related_objects) ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": nests}) else: history = nests.OwnerHistory @@ -121,10 +156,9 @@ class Usecase: if history: ifcopenshell.util.element.remove_deep2(self.file, history) + # assign elements to a new nesting if is_nested_by: - related_objects = list(is_nested_by.RelatedObjects) - related_objects.append(self.settings["related_object"]) - is_nested_by.RelatedObjects = related_objects + is_nested_by.RelatedObjects = list(set(is_nested_by.RelatedObjects) | related_objects) ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": is_nested_by}) else: is_nested_by = self.file.create_entity( @@ -132,8 +166,12 @@ class Usecase: **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), - "RelatedObjects": [self.settings["related_object"]], - "RelatingObject": self.settings["relating_object"], + "RelatedObjects": list(related_objects), + "RelatingObject": relating_object, } ) + + # NOTE: Creating a nesting relationship doesn't localize the object's placement, + # unlike assigning it to an aggregate or a container. + return is_nested_by diff --git a/src/ifcopenshell-python/ifcopenshell/api/nest/change_nest.py b/src/ifcopenshell-python/ifcopenshell/api/nest/change_nest.py index 98ba363050..6d83a9e4e8 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/nest/change_nest.py +++ b/src/ifcopenshell-python/ifcopenshell/api/nest/change_nest.py @@ -44,6 +44,6 @@ class Usecase: ifcopenshell.api.run( "nest.assign_object", self.file, - related_object=self.settings["item"], + related_objects=[self.settings["item"]], relating_object=self.settings["new_parent"], ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/nest/unassign_object.py b/src/ifcopenshell-python/ifcopenshell/api/nest/unassign_object.py index bff97fd07a..1c986f2cd0 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/nest/unassign_object.py +++ b/src/ifcopenshell-python/ifcopenshell/api/nest/unassign_object.py @@ -22,48 +22,56 @@ import ifcopenshell.util.element class Usecase: - def __init__(self, file, related_object=None): - """Unassigns a related_object from its nest. + def __init__(self, file: ifcopenshell.file, related_objects: list[ifcopenshell.entity_instance]): + """Unassigns related_objects from their nests. An object (the whole within a decomposition) is Nested by zero or one more smaller objects. This function will remove this nesting relationship. If the object is not part of a nesting relationship, nothing will happen. - :param related_object: The child of the nesting relationship, typically - an IfcElement. - :type related_object: ifcopenshell.entity_instance.entity_instance - :return: None if the nest has only one child, otherwise the IfcRelNests relationship instance + :param related_objects: The list of children of the nesting relationship, + typically IfcElements. + :type related_objects: list[ifcopenshell.entity_instance.entity_instance] + :return: None + :rtype: None Example: .. code:: python - task = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcSite") - subtask1 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuilding") - subtask2 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuilding") - ifcopenshell.api.run("nest.assign_object", model, related_object=subtask1, relating_object=task) - ifcopenshell.api.run("nest.assign_object", model, related_object=subtask2, relating_object=task) - # The relationship is returned as task still has subtask2 - rel = ifcopenshell.api.run("nest.unassign_object", model, related_object=subtask1) - # Nothing is returned, as the relationship has no related objects - ifcopenshell.api.run("nest.unassign_object", model, related_object=subtask2) + task = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcTasks") + subtask1 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcTask") + subtask2 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcTask") + ifcopenshell.api.run("nest.assign_object", model, related_objects=[subtask1], relating_object=task) + ifcopenshell.api.run("nest.assign_object", model, related_objects=[subtask2], relating_object=task) + # nothing is returned + rel = ifcopenshell.api.run("nest.unassign_object", model, related_objects=[subtask1]) + # nothing is returned, relationship is removed + ifcopenshell.api.run("nest.unassign_object", model, related_objects=[subtask2]) """ self.file = file - self.settings = {"related_object": related_object} + self.settings = {"related_objects": related_objects} - def execute(self): - for rel in self.settings["related_object"].Nests or []: - if not rel.is_a("IfcRelNests"): - continue - if len(rel.RelatedObjects) == 1: + def execute(self) -> None: + related_objects = set(self.settings["related_objects"]) + ifc2x3 = self.file.schema == "IFC2X3" + if ifc2x3: + rels = set( + rel + for object in related_objects + if (rel := next((rel for rel in object.Decomposes if rel.is_a("IfcRelNests")), None)) + ) + else: + rels = set(rel for object in related_objects if (rel := next((rel for rel in object.Nests), None))) + + for rel in rels: + related_objects = set(rel.RelatedObjects) - related_objects + if related_objects: + rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + else: history = rel.OwnerHistory self.file.remove(rel) if history: ifcopenshell.util.element.remove_deep2(self.file, history) - return - related_objects = list(rel.RelatedObjects) - related_objects.remove(self.settings["related_object"]) - rel.RelatedObjects = related_objects - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) - return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py b/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py index 0d320df452..6c0e6a456f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py @@ -19,7 +19,11 @@ # Note: it is the intent for you to override these with your own functions -def get_application(ifc): +import ifcopenshell +from typing import Union + + +def get_application(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None]: """Returns the application representing the authoring software It is expected for you to overload this function with your own @@ -34,12 +38,12 @@ def get_application(ifc): if not app and ifc.schema == "IFC2X3": raise Exception( "Please create an application to continue. See the owner.create_owner_history docs for more info." - "https://blenderbim.org/docs-python/autoapi/ifcopenshell/api/owner/create_owner_history/index.html" + "https://docs.ifcopenshell.org/autoapi/ifcopenshell/api/owner/create_owner_history/index.html" ) return (app or [None])[0] -def get_user(ifc): +def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None]: """Returns the active authoring user It is expected for you to overload this function with your own @@ -54,7 +58,7 @@ def get_user(ifc): if not pao and ifc.schema == "IFC2X3": raise Exception( "Please create a user to continue. See the owner.create_owner_history docs for more info." - "https://blenderbim.org/docs-python/autoapi/ifcopenshell/api/owner/create_owner_history/index.html" + "https://docs.ifcopenshell.org/autoapi/ifcopenshell/api/owner/create_owner_history/index.html" ) return (pao or [None])[0] diff --git a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py index 7ac21e07f3..4bef25ee75 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py @@ -72,7 +72,7 @@ class Usecase: wall_type = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWallType", name="WAL01") concrete = ifcopenshell.api.run("material.add_material", self.file, name="CON", category="concrete") rel = ifcopenshell.api.run("material.assign_material", library, - product=wall_type, type="IfcMaterialLayerSet") + products=[wall_type], type="IfcMaterialLayerSet") layer = ifcopenshell.api.run("material.add_layer", library, layer_set=rel.RelatingMaterial, material=concrete) layer.Name = "Structure" @@ -204,7 +204,7 @@ class Usecase: "type.assign_type", self.file, should_run_listeners=False, - related_object=element, + related_objects=[element], relating_type=new_type, should_map_representations=False, ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/project/assign_declaration.py b/src/ifcopenshell-python/ifcopenshell/api/project/assign_declaration.py index 626f4968b4..c11e84be9a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/project/assign_declaration.py +++ b/src/ifcopenshell-python/ifcopenshell/api/project/assign_declaration.py @@ -65,7 +65,7 @@ class Usecase: wall_type = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWallType", name="WAL01") concrete = ifcopenshell.api.run("material.add_material", self.file, name="CON", category="concrete") rel = ifcopenshell.api.run("material.assign_material", library, - product=wall_type, type="IfcMaterialLayerSet") + products=[wall_type], type="IfcMaterialLayerSet") layer = ifcopenshell.api.run("material.add_layer", library, layer_set=rel.RelatingMaterial, material=concrete) layer.Name = "Structure" diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/add_resource.py b/src/ifcopenshell-python/ifcopenshell/api/resource/add_resource.py index 6a9ac81167..9580389ab6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/add_resource.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/add_resource.py @@ -97,7 +97,7 @@ class Usecase: ifcopenshell.api.run( "nest.assign_object", self.file, - related_object=resource, + related_objects=[resource], relating_object=self.settings["parent_resource"], ) else: diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/create_entity.py b/src/ifcopenshell-python/ifcopenshell/api/root/create_entity.py index 3d4c317df8..8599c00472 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/create_entity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/create_entity.py @@ -18,10 +18,17 @@ import ifcopenshell import ifcopenshell.api +from typing import Optional class Usecase: - def __init__(self, file, ifc_class="IfcBuildingElementProxy", predefined_type=None, name=None): + def __init__( + self, + file: ifcopenshell.file, + ifc_class: str = "IfcBuildingElementProxy", + predefined_type: Optional[str] = None, + name: Optional[str] = None, + ): """Create a new rooted product This is a critical function used to create almost any rooted product or @@ -70,7 +77,7 @@ class Usecase: "name": name, } - def execute(self): + def execute(self) -> ifcopenshell.entity_instance: element = self.file.create_entity( self.settings["ifc_class"], **{ diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py b/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py index d5a78bdf27..179e7ca572 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py @@ -17,10 +17,11 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.api +import ifcopenshell.util.element class Usecase: - def __init__(self, file, product=None): + def __init__(self, file: ifcopenshell.file, product: ifcopenshell.entity_instance): """Removes a product This is effectively a smart delete function that not only removes a @@ -58,7 +59,7 @@ class Usecase: self.file = file self.settings = {"product": product} - def execute(self): + def execute(self) -> None: representations = [] if self.settings["product"].is_a("IfcProduct"): if self.settings["product"].Representation: @@ -125,13 +126,12 @@ class Usecase: pset=inverse.RelatingPropertyDefinition, ) elif inverse.is_a("IfcRelAssociatesMaterial"): - ifcopenshell.api.run("material.unassign_material", self.file, product=self.settings["product"]) + ifcopenshell.api.run("material.unassign_material", self.file, products=[self.settings["product"]]) elif inverse.is_a("IfcRelDefinesByType"): if inverse.RelatingType == self.settings["product"]: - for related_object in inverse.RelatedObjects: - ifcopenshell.api.run("type.unassign_type", self.file, related_object=related_object) + ifcopenshell.api.run("type.unassign_type", self.file, related_objects=inverse.RelatedObjects) else: - ifcopenshell.api.run("type.unassign_type", self.file, related_object=self.settings["product"]) + ifcopenshell.api.run("type.unassign_type", self.file, related_objects=[self.settings["product"]]) elif inverse.is_a("IfcRelSpaceBoundary"): ifcopenshell.api.run("boundary.remove_boundary", self.file, boundary=inverse) elif inverse.is_a("IfcRelFillsElement"): diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py index 80244df3a5..bf42853d2c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py @@ -176,7 +176,7 @@ class Usecase: rel = ifcopenshell.api.run( "nest.assign_object", self.file, - related_object=task, + related_objects=[task], relating_object=self.settings["parent_task"], ) if self.settings["parent_task"].Identification: diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_schedule.py index a9c775d338..3da4d3b10e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_schedule.py @@ -107,7 +107,7 @@ class Usecase: "aggregate.assign_object", self.file, **{ - "product": work_schedule, + "products": [work_schedule], "relating_object": self.settings["work_plan"], } ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_workplan.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_workplan.py index b3439e69a6..a9e6f000eb 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_workplan.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_workplan.py @@ -64,7 +64,7 @@ class Usecase: "aggregate.assign_object", self.file, **{ - "product": self.settings["work_schedule"], + "products": [self.settings["work_schedule"]], "relating_object": self.settings["work_plan"], } ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py index b541a81670..a069bb4901 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py @@ -153,7 +153,8 @@ class Usecase: days = 0 if predecessor_duration.days == 0 else 1 duration_type = "WORKTIME" if rel.TimeLag: - days += self.get_lag_time_days(rel.TimeLag) + # updated to handle IfcRatioMeasure as a TimeLag value + days += self.get_lag_time_days(rel.TimeLag) if rel.TimeLag.LagValue.is_a("IfcDuration") else predecessor_duration.days * rel.TimeLag.LagValue.wrappedValue duration_type = rel.TimeLag.DurationType if days: starts.append( @@ -182,7 +183,7 @@ class Usecase: if not start: continue if rel.TimeLag: - days = self.get_lag_time_days(rel.TimeLag) + days = self.get_lag_time_days(rel.TimeLag) if rel.TimeLag.LagValue.is_a("IfcDuration") else predecessor_duration.days * rel.TimeLag.LagValue.wrappedValue duration_type = rel.TimeLag.DurationType starts.append( self.offset_date( @@ -201,7 +202,7 @@ class Usecase: if not finish: continue if rel.TimeLag: - days = self.get_lag_time_days(rel.TimeLag) + days = self.get_lag_time_days(rel.TimeLag) if rel.TimeLag.LagValue.is_a("IfcDuration") else predecessor_duration.days * rel.TimeLag.LagValue.wrappedValue duration_type = rel.TimeLag.DurationType finishes.append( self.offset_date( @@ -222,7 +223,7 @@ class Usecase: days = -1 duration_type = "WORKTIME" if rel.TimeLag: - days += self.get_lag_time_days(rel.TimeLag) + days += self.get_lag_time_days(rel.TimeLag) if rel.TimeLag.LagValue.is_a("IfcDuration") else predecessor_duration.days * rel.TimeLag.LagValue.wrappedValue duration_type = rel.TimeLag.DurationType if days or rel.TimeLag: finishes.append( diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py index 6356c8041f..195c5c71cd 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py @@ -17,6 +17,8 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.element import ifcopenshell.util.sequence @@ -50,9 +52,7 @@ class Usecase: def execute(self): self.tracker = {"current": [], "duplicate": []} self.duplicate_task(self.settings["task"]) - self.copy_sequence_relationship( - self.tracker["current"], self.tracker["duplicate"] - ) + self.copy_sequence_relationship(self.tracker["current"], self.tracker["duplicate"]) return self.tracker["current"], self.tracker["duplicate"] def duplicate_task(self, task): @@ -67,9 +67,7 @@ class Usecase: if inverse.is_a("IfcRelDefinesByProperties"): inverse = ifcopenshell.util.element.copy(self.file, inverse) inverse.RelatedObjects = [to_element] - pset = ifcopenshell.util.element.copy_deep( - self.file, inverse.RelatingPropertyDefinition - ) + pset = ifcopenshell.util.element.copy_deep(self.file, inverse.RelatingPropertyDefinition) inverse.RelatingPropertyDefinition = pset elif inverse.is_a("IfcRelNests") and inverse.RelatingObject == from_element: nested_tasks = [e for e in inverse.RelatedObjects] @@ -81,24 +79,19 @@ class Usecase: inverse = ifcopenshell.util.element.copy(self.file, inverse) inverse.RelatingObject = to_element inverse.RelatedObjects = new_tasks - for t in new_tasks: - ifcopenshell.api.run( - "nest.unassign_object", self.file, related_object=t - ) - rel = ifcopenshell.api.run( - "nest.assign_object", - self.file, - related_object=t, - relating_object=to_element, - ) + ifcopenshell.api.run("nest.unassign_object", self.file, related_objects=new_tasks) + ifcopenshell.api.run( + "nest.assign_object", + self.file, + related_objects=new_tasks, + relating_object=to_element, + ) + elif inverse.is_a("IfcRelSequence") and ( - inverse.RelatingProcess == from_element - or inverse.RelatedProcess == from_element + inverse.RelatingProcess == from_element or inverse.RelatedProcess == from_element ): continue - elif inverse.is_a( - "IfcRelAssignsToControl" - ) and inverse.RelatingControl.is_a("IfcWorkSchedule"): + elif inverse.is_a("IfcRelAssignsToControl") and inverse.RelatingControl.is_a("IfcWorkSchedule"): continue elif inverse.is_a("IfcRelDefinesByObject"): continue @@ -116,8 +109,7 @@ class Usecase: for i, original_task in enumerate(original_tasks): for inverse in self.file.get_inverse(original_task): if inverse.is_a("IfcRelSequence") and ( - inverse.RelatingProcess == original_task - or inverse.RelatedProcess == original_task + inverse.RelatingProcess == original_task or inverse.RelatedProcess == original_task ): original_task_index = original_tasks.index(original_task) duplicated_task = duplicated_tasks[original_task_index] @@ -127,16 +119,12 @@ class Usecase: else: related_process = duplicated_task if inverse.RelatedProcess in original_tasks: - related_process_index = original_tasks.index( - inverse.RelatedProcess - ) + related_process_index = original_tasks.index(inverse.RelatedProcess) related_process = duplicated_tasks[related_process_index] else: # thus the related process is not part of the duplicated tasks related_process = inverse.RelatedProcess if inverse.RelatingProcess in original_tasks: - relating_process_index = original_tasks.index( - inverse.RelatingProcess - ) + relating_process_index = original_tasks.index(inverse.RelatingProcess) relating_process = duplicated_tasks[relating_process_index] else: # thus the relating process is not part of the duplicated tasks relating_process = inverse.RelatingProcess @@ -152,11 +140,11 @@ class Usecase: "sequence.assign_lag_time", self.file, rel_sequence=rel, - lag_value=ifcopenshell.util.date.ifc2datetime( - inverse.TimeLag.LagValue.wrappedValue - ) - if inverse.TimeLag.LagValue - else None, + lag_value=( + ifcopenshell.util.date.ifc2datetime(inverse.TimeLag.LagValue.wrappedValue) + if inverse.TimeLag.LagValue + else None + ), duration_type=inverse.TimeLag.DurationType, ) @@ -168,17 +156,13 @@ class Usecase: related_objects = list(referenced_by.RelatedObjects) related_objects.append(related_object) referenced_by.RelatedObjects = related_objects - ifcopenshell.api.run( - "owner.update_owner_history", self.file, **{"element": referenced_by} - ) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": referenced_by}) else: referenced_by = self.file.create_entity( "IfcRelDefinesByObject", **{ "GlobalId": ifcopenshell.guid.new(), - "OwnerHistory": ifcopenshell.api.run( - "owner.create_owner_history", self.file - ), + "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), "RelatedObjects": [related_object], "RelatingObject": relating_object, } diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_time.py index c9c76dfee2..76a0521478 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_time.py @@ -17,7 +17,6 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.util.date -from ifcopenshell.util.data import WorkTimeDatesInterface class Usecase: @@ -55,10 +54,12 @@ class Usecase: self.settings = {"work_time": work_time, "attributes": attributes or {}} def execute(self): - work_time_dates = WorkTimeDatesInterface(self.settings["work_time"]) for name, value in self.settings["attributes"].items(): - if name in ("Start", "Finish", "StartDate", "FinishDate"): + if name in ("Start", "StartDate"): value = ifcopenshell.util.date.datetime2ifc(value, "IfcDate") - setattr(work_time_dates, name, value) + self.settings["work_time"][4] = value + elif name in ("Finish", "FinishDate"): + value = ifcopenshell.util.date.datetime2ifc(value, "IfcDate") + self.settings["work_time"][5] = value else: setattr(self.settings["work_time"], name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/assign_container.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/assign_container.py index c54cc64db4..70dacdce00 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/spatial/assign_container.py +++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/assign_container.py @@ -20,11 +20,17 @@ import ifcopenshell import ifcopenshell.api import ifcopenshell.util.element import ifcopenshell.util.placement +from typing import Union class Usecase: - def __init__(self, file, product=None, relating_structure=None): - """Assigns a product to be contained hierarchically in a space + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + relating_structure: ifcopenshell.entity_instance, + ): + """Assigns products to be contained hierarchically in a space All physical IFC model elements must be part of a hierarchical tree called the "spatial decomposition", where large things are made up of @@ -61,13 +67,14 @@ class Usecase: decomposition" tree, assigning an aggregate relationship will remove any previous aggregation, containment, or nesting relationships it may have. - :param product: The physical IfcElement that exists in the space. - :type product: ifcopenshell.entity_instance.entity_instance + :param products: A list of physical IfcElements existing in the space. + :type products: list[ifcopenshell.entity_instance.entity_instance] :param relating_structure: The IfcSpatialStructureElement element, such as IfcBuilding, IfcBuildingStorey, or IfcSpace that the element exists in. :return: The IfcRelContainedInSpatialStructure relationship instance - :rtype: ifcopenshell.entity_instance.entity_instance + or `None` if `products` was empty list. + :rtype: Union[ifcopenshell.entity_instance.entity_instance, None] Example: @@ -80,76 +87,99 @@ class Usecase: space = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcSpace") # The project contains a site (note that project aggregation is a special case in IFC) - ifcopenshell.api.run("aggregate.assign_object", model, product=site, relating_object=project) + ifcopenshell.api.run("aggregate.assign_object", model, products=[site], relating_object=project) # The site has a building, the building has a storey, and the storey has a space - ifcopenshell.api.run("aggregate.assign_object", model, product=building, relating_object=site) - ifcopenshell.api.run("aggregate.assign_object", model, product=storey, relating_object=building) - ifcopenshell.api.run("aggregate.assign_object", model, product=space, relating_object=storey) + ifcopenshell.api.run("aggregate.assign_object", model, products=[building], relating_object=site) + ifcopenshell.api.run("aggregate.assign_object", model, products=[storey], relating_object=building) + ifcopenshell.api.run("aggregate.assign_object", model, products=[space], relating_object=storey) # Create a wall and furniture wall = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWall") furniture = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcFurniture") # The wall is in the storey, and the furniture is in the space - ifcopenshell.api.run("spatial.assign_container", model, product=wall, relating_structure=storey) - ifcopenshell.api.run("spatial.assign_container", model, product=furniture, relating_structure=space) + ifcopenshell.api.run("spatial.assign_container", model, products=[wall], relating_structure=storey) + ifcopenshell.api.run("spatial.assign_container", model, products=[furniture], relating_structure=space) """ self.file = file self.settings = { - "product": product, + "products": products, "relating_structure": relating_structure, } - def execute(self): - contained_in_structure = self.settings["product"].ContainedInStructure - contains_elements = self.settings["relating_structure"].ContainsElements - - if contains_elements and contained_in_structure and contained_in_structure[0] == contains_elements[0]: + def execute(self) -> Union[ifcopenshell.entity_instance, None]: + if not self.settings["products"]: return - aggregate = ifcopenshell.util.element.get_aggregate(self.settings["product"]) - if aggregate: - ifcopenshell.api.run( - "aggregate.unassign_object", self.file, product=self.settings["product"] - ) + products = set(self.settings["products"]) + relating_structure = self.settings["relating_structure"] + structure_rel = next(iter(relating_structure.ContainsElements), None) - if contained_in_structure: - related_elements = list(contained_in_structure[0].RelatedElements) - related_elements.remove(self.settings["product"]) + previous_containers_rels: set[ifcopenshell.entity_instance] = set() + products_without_containers: list[ifcopenshell.entity_instance] = [] + products_with_containers: list[ifcopenshell.entity_instance] = [] + + # check if there is anything to change + for product in products: + product_rel = next(iter(product.ContainedInStructure), None) + + if product_rel is None: + products_without_containers.append(product) + continue + + # either structure_rel is None or product is part of different rel + if product_rel != structure_rel: + previous_containers_rels.add(product_rel) + products_with_containers.append(product) + + # products with already assigned containers will be skipped + + products_to_change = products_without_containers + products_with_containers + # nothing to change + if not products_to_change: + return structure_rel + + # can be either only aggregated or only contained at the same time + ifcopenshell.api.run("aggregate.unassign_object", self.file, products=products_without_containers) + + # unassign elements from previous containers + for rel in previous_containers_rels: + related_elements = set(rel.RelatedElements) - products if related_elements: - contained_in_structure[0].RelatedElements = related_elements - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": contained_in_structure[0]}) + rel.RelatedElements = list(related_elements) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) else: - history = contained_in_structure[0].OwnerHistory - self.file.remove(contained_in_structure[0]) + history = rel.OwnerHistory + self.file.remove(rel) if history: ifcopenshell.util.element.remove_deep2(self.file, history) - if contains_elements: - related_elements = list(contains_elements[0].RelatedElements) - related_elements.append(self.settings["product"]) - contains_elements[0].RelatedElements = related_elements - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": contains_elements[0]}) + # assign elements to a new container + if structure_rel: + structure_rel.RelatedElements = list(set(structure_rel.RelatedElements) | products) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": structure_rel}) else: - contains_elements = self.file.create_entity( + structure_rel = self.file.create_entity( "IfcRelContainedInSpatialStructure", **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), - "RelatedElements": [self.settings["product"]], + "RelatedElements": list(products), "RelatingStructure": self.settings["relating_structure"], } ) - placement = getattr(self.settings["product"], "ObjectPlacement", None) - if placement and placement.is_a("IfcLocalPlacement"): - ifcopenshell.api.run( - "geometry.edit_object_placement", - self.file, - product=self.settings["product"], - matrix=ifcopenshell.util.placement.get_local_placement(self.settings["product"].ObjectPlacement), - is_si=False, - ) + # localize placement relative to a new container for affected products + for product in products_to_change: + placement = getattr(product, "ObjectPlacement", None) + if placement and placement.is_a("IfcLocalPlacement"): + ifcopenshell.api.run( + "geometry.edit_object_placement", + self.file, + product=product, + matrix=ifcopenshell.util.placement.get_local_placement(product.ObjectPlacement), + is_si=False, + ) - return contains_elements + return structure_rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py index b47a9f976f..00cb94d8b3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py +++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py @@ -22,11 +22,16 @@ import ifcopenshell.util.element class Usecase: - def __init__(self, file, product=None, relating_structure=None): - """Dereferences the a product and space + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + relating_structure: ifcopenshell.entity_instance, + ): + """Dereferences a list of products and space - :param product: The physical IfcElement that exists in the space. - :type product: ifcopenshell.entity_instance.entity_instance + :param products: The list of physical IfcElements that exists in the space. + :type products: list[ifcopenshell.entity_instance.entity_instance] :param relating_structure: The IfcSpatialStructureElement element, such as IfcBuilding, IfcBuildingStorey, or IfcSpace that the element exists in. @@ -45,37 +50,38 @@ class Usecase: storey3 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuildingStorey") # The project contains a site (note that project aggregation is a special case in IFC) - ifcopenshell.api.run("aggregate.assign_object", model, product=site, relating_object=project) + ifcopenshell.api.run("aggregate.assign_object", model, products=[site], relating_object=project) # The site has a building, the building has a storey, and the storey has a space - ifcopenshell.api.run("aggregate.assign_object", model, product=building, relating_object=site) - ifcopenshell.api.run("aggregate.assign_object", model, product=storey, relating_object=building) - ifcopenshell.api.run("aggregate.assign_object", model, product=space, relating_object=storey) + ifcopenshell.api.run("aggregate.assign_object", model, products=[building], relating_object=site) + ifcopenshell.api.run("aggregate.assign_object", model, products=[storey], relating_object=building) + ifcopenshell.api.run("aggregate.assign_object", model, products=[space], relating_object=storey) # Create a column, this column spans 3 storeys column = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWall") # The column is contained in the lowermost storey - ifcopenshell.api.run("spatial.assign_container", model, product=column, relating_structure=storey1) + ifcopenshell.api.run("spatial.assign_container", model, products=[column], relating_structure=storey1) # And referenced in the others - ifcopenshell.api.run("spatial.reference_structure", model, product=column, relating_structure=storey2) - ifcopenshell.api.run("spatial.reference_structure", model, product=column, relating_structure=storey3) + ifcopenshell.api.run("spatial.reference_structure", model, products=[column], relating_structure=storey2) + ifcopenshell.api.run("spatial.reference_structure", model, products=[column], relating_structure=storey3) # Actually, it only goes up to storey 2. - ifcopenshell.api.run("spatial.dereference_structure", model, product=column, relating_structure=storey3) + ifcopenshell.api.run("spatial.dereference_structure", model, products=[column], relating_structure=storey3) """ self.file = file - self.settings = {"product": product, "relating_structure": relating_structure} + self.settings = {"products": products, "relating_structure": relating_structure} - def execute(self): - for rel in self.settings["product"].ReferencedInStructures: - if rel.RelatingStructure != self.settings["relating_structure"]: + def execute(self) -> None: + products = set(self.settings["products"]) + for rel in self.settings["relating_structure"].ReferencesElements: + related_elements = set(rel.RelatedElements) + if not related_elements.intersection(products): continue - related_elements = list(rel.RelatedElements) - related_elements.remove(self.settings["product"]) + related_elements = related_elements - products if related_elements: - rel.RelatedElements = related_elements + rel.RelatedElements = list(related_elements) ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) else: history = rel.OwnerHistory diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py index d0f0187cbf..6d8915828a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py +++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py @@ -18,11 +18,18 @@ import ifcopenshell import ifcopenshell.api +import ifcopenshell.util.element +from typing import Union class Usecase: - def __init__(self, file, product=None, relating_structure=None): - """Denote that a product is related to a spatial structure + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + relating_structure: ifcopenshell.entity_instance, + ): + """Denote that a list products is related to a list of spatial structures This is similar to ifcopenshell.api.spatial.assign_container, except that containment can only occur between a product and a single spatial @@ -39,13 +46,15 @@ class Usecase: Referencing is non-hierarchical, so a door may be referenced in multiple spaces simultaneously. - :param product: The physical IfcElement that exists in the space. - :type product: ifcopenshell.entity_instance.entity_instance + :param products: The list of physical IfcElements that exists in the space. + :type products: list[ifcopenshell.entity_instance.entity_instance] :param relating_structure: The IfcSpatialStructureElement element, such as IfcBuilding, IfcBuildingStorey, or IfcSpace that the element exists in. + :type relating_structure: ifcopenshell.entity_instance.entity_instance :return: The IfcRelReferencedInSpatialStructure relationship instance - :rtype: ifcopenshell.entity_instance.entity_instance + or `None` if `products` was an empty list. + :rtype: Union[ifcopenshell.entity_instance, None] Example: @@ -59,51 +68,57 @@ class Usecase: storey3 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuildingStorey") # The project contains a site (note that project aggregation is a special case in IFC) - ifcopenshell.api.run("aggregate.assign_object", model, product=site, relating_object=project) + ifcopenshell.api.run("aggregate.assign_object", model, products=[site], relating_object=project) # The site has a building, the building has a storey, and the storey has a space - ifcopenshell.api.run("aggregate.assign_object", model, product=building, relating_object=site) - ifcopenshell.api.run("aggregate.assign_object", model, product=storey, relating_object=building) - ifcopenshell.api.run("aggregate.assign_object", model, product=space, relating_object=storey) + ifcopenshell.api.run("aggregate.assign_object", model, products=[building], relating_object=site) + ifcopenshell.api.run("aggregate.assign_object", model, products=[storey], relating_object=building) + ifcopenshell.api.run("aggregate.assign_object", model, products=[space], relating_object=storey) # Create a column, this column spans 3 storeys column = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWall") # The column is contained in the lowermost storey - ifcopenshell.api.run("spatial.assign_container", model, product=column, relating_structure=storey1) + ifcopenshell.api.run("spatial.assign_container", model, products=[column], relating_structure=storey1) # And referenced in the others - ifcopenshell.api.run("spatial.reference_structure", model, product=column, relating_structure=storey2) - ifcopenshell.api.run("spatial.reference_structure", model, product=column, relating_structure=storey3) + ifcopenshell.api.run( + "spatial.reference_structure", model, products=[column], relating_structure=[storey2, storey3] + ) """ self.file = file self.settings = { - "product": product, + "products": products, "relating_structure": relating_structure, } - def execute(self): - referenced_in_structures = self.settings["product"].ReferencedInStructures - references_elements = self.settings["relating_structure"].ReferencesElements + def execute(self) -> Union[ifcopenshell.entity_instance, None]: + structure = self.settings["relating_structure"] + products = set(self.settings["products"]) - for rel in referenced_in_structures: - if rel.RelatingStructure == self.settings["relating_structure"]: - return + if not products: + return - if references_elements: - related_elements = list(references_elements[0].RelatedElements) - related_elements.append(self.settings["product"]) - references_elements[0].RelatedElements = related_elements - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": references_elements[0]}) - else: - references_elements = self.file.create_entity( + referenced = ifcopenshell.util.element.get_structure_referenced_elements(structure) + products_to_assign = products - referenced + rel = next(iter(structure.ReferencesElements), None) + + if not products_to_assign: + return rel + + if rel is None: + rel = self.file.create_entity( "IfcRelReferencedInSpatialStructure", **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), - "RelatedElements": [self.settings["product"]], - "RelatingStructure": self.settings["relating_structure"], + "RelatedElements": list(products_to_assign), + "RelatingStructure": structure, } ) + else: + related_elements = set(rel.RelatedElements) | products_to_assign + rel.RelatedElements = list(related_elements) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) - return references_elements + return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/remove_container.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/remove_container.py deleted file mode 100644 index 48a2f6dc38..0000000000 --- a/src/ifcopenshell-python/ifcopenshell/api/spatial/remove_container.py +++ /dev/null @@ -1,77 +0,0 @@ -# IfcOpenShell - IFC toolkit and geometry engine -# Copyright (C) 2021 Dion Moult -# -# This file is part of IfcOpenShell. -# -# IfcOpenShell is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# IfcOpenShell is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with IfcOpenShell. If not, see . - -import ifcopenshell -import ifcopenshell.api -import ifcopenshell.util.element - - -class Usecase: - def __init__(self, file, product=None): - """Unassigns a container from a product. - - Caution: this API function may be replaced by spatial.unassign_container - - :param product: The IfcProduct to remove the containment from. - :type product: ifcopenshell.entity_instance.entity_instance - :return: None - :rtype: None - - Example: - - .. code:: python - - project = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcProject") - site = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcSite") - building = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuilding") - storey = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuildingStorey") - - # The project contains a site (note that project aggregation is a special case in IFC) - ifcopenshell.api.run("aggregate.assign_object", model, product=site, relating_object=project) - - # The site has a building, the building has a storey, and the storey has a space - ifcopenshell.api.run("aggregate.assign_object", model, product=building, relating_object=site) - ifcopenshell.api.run("aggregate.assign_object", model, product=storey, relating_object=building) - - # Create a wall - wall = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWall") - - # The wall is in the storey - ifcopenshell.api.run("spatial.assign_container", model, product=wall, relating_structure=storey) - - # Not anymore! - ifcopenshell.api.run("spatial.remove_container", model, product=wall) - """ - self.file = file - self.settings = {"product": product} - - def execute(self): - contained_in_structure = self.settings["product"].ContainedInStructure - if not contained_in_structure: - return - - related_elements = list(contained_in_structure[0].RelatedElements) - related_elements.remove(self.settings["product"]) - if related_elements: - contained_in_structure[0].RelatedElements = related_elements - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": contained_in_structure[0]}) - else: - history = contained_in_structure[0].OwnerHistory - self.file.remove(contained_in_structure[0]) - if history: - ifcopenshell.util.element.remove_deep2(self.file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/unassign_container.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/unassign_container.py index 7c46bd3e49..f7c831b9fd 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/spatial/unassign_container.py +++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/unassign_container.py @@ -22,11 +22,11 @@ import ifcopenshell.util.element class Usecase: - def __init__(self, file, product=None): - """Unassigns a container from a product. + def __init__(self, file: ifcopenshell.file, products: list[ifcopenshell.entity_instance]): + """Unassigns a container from products. - :param product: The IfcProduct to remove the containment from. - :type product: ifcopenshell.entity_instance.entity_instance + :param product: A list of IfcProducts to remove the containment from. + :type product: list[ifcopenshell.entity_instance.entity_instance] :return: None :rtype: None @@ -40,32 +40,34 @@ class Usecase: storey = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuildingStorey") # The project contains a site (note that project aggregation is a special case in IFC) - ifcopenshell.api.run("aggregate.assign_object", model, product=site, relating_object=project) + ifcopenshell.api.run("aggregate.assign_object", model, products=[site], relating_object=project) # The site has a building, the building has a storey, and the storey has a space - ifcopenshell.api.run("aggregate.assign_object", model, product=building, relating_object=site) - ifcopenshell.api.run("aggregate.assign_object", model, product=storey, relating_object=building) + ifcopenshell.api.run("aggregate.assign_object", model, products=[building], relating_object=site) + ifcopenshell.api.run("aggregate.assign_object", model, products=[storey], relating_object=building) # Create a wall wall = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWall") # The wall is in the storey - ifcopenshell.api.run("spatial.assign_container", model, product=wall, relating_structure=storey) + ifcopenshell.api.run("spatial.assign_container", model, products=[wall], relating_structure=storey) # Not anymore! - ifcopenshell.api.run("spatial.unassign_container", model, product=wall) + ifcopenshell.api.run("spatial.unassign_container", model, products=[wall]) """ self.file = file self.settings = { - "product": product, + "products": products, } - def execute(self): - for rel in self.settings["product"].ContainedInStructure or []: - related_elements = list(rel.RelatedElements) - related_elements.remove(self.settings["product"]) + def execute(self) -> None: + products = set(self.settings["products"]) + rels = set(rel for product in products if (rel := next(iter(product.ContainedInStructure), None))) + + for rel in rels: + related_elements = set(rel.RelatedElements) - products if related_elements: - rel.RelatedElements = related_elements + rel.RelatedElements = list(related_elements) ifcopenshell.api.run("owner.update_owner_history", self.file, element=rel) else: history = rel.OwnerHistory diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py index 25dc506cc4..11a82097a6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py @@ -17,6 +17,8 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.element class Usecase: @@ -80,7 +82,7 @@ class Usecase: # Assign our concrete material to our wall ifcopenshell.api.run("material.assign_material", model, - product=wall, type="IfcMaterial", material=concrete) + products=[wall], type="IfcMaterial", material=concrete) # Create a new surface style style = ifcopenshell.api.run("style.add_style", model) @@ -139,7 +141,7 @@ class Usecase: def modify_existing_definition_representation(self): # NOTE: while it's theoritically possible to have multiple styles per 1 material # (either with multiple styled items or multiple styles in 1 item) - # we use an implicit convention that + # we use an implicit convention that there is only 1 style per material. definition_representation = self.settings["material"].HasRepresentation[0] representation = self.get_styled_representation(definition_representation) if representation: diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/add_system.py b/src/ifcopenshell-python/ifcopenshell/api/system/add_system.py index ed0152efe5..0fdf9992d7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/add_system.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/add_system.py @@ -21,7 +21,7 @@ import ifcopenshell.api class Usecase: - def __init__(self, file, ifc_class="IfcDistributionSystem"): + def __init__(self, file: ifcopenshell.file, ifc_class: str = "IfcDistributionSystem"): """Add a new distribution system A distribution system is a group of distribution elements, like ducts, @@ -48,9 +48,14 @@ class Usecase: self.file = file self.settings = {"ifc_class": ifc_class} - def execute(self): + def execute(self) -> ifcopenshell.entity_instance: + ifc_class = self.settings["ifc_class"] + # workaround for failing default argument in ifc2x3 + if self.file.schema == "IFC2X3" and ifc_class == "IfcDistributionSystem": + ifc_class = "IfcSystem" + return self.file.create_entity( - self.settings["ifc_class"], + ifc_class, **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/assign_system.py b/src/ifcopenshell-python/ifcopenshell/api/system/assign_system.py index d21e2b88e0..3df49d776d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/assign_system.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/assign_system.py @@ -22,17 +22,23 @@ import ifcopenshell.util.system class Usecase: - def __init__(self, file, product=None, system=None): - """Assigns a distribution element to a system + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + system: ifcopenshell.entity_instance, + ): + """Assigns distribution elements to a system Note that it is not necessary to assign distribution ports to a system. - :param product: The IfcDistributionElement to assign to the system. - :type product: ifcopenshell.entity_instance.entity_instance + :param products: The list of IfcDistributionElements to assign to the system. + :type products: list[ifcopenshell.entity_instance.entity_instance] :param system: The IfcSystem you want to assign the element to. :type system: ifcopenshell.entity_instance.entity_instance :return: The IfcRelAssignsToGroup relationship - :rtype: ifcopenshell.entity_instance.entity_instance + or `None` if `products` was empty list. + :rtype: [ifcopenshell.entity_instance.entity_instance, None] Example: @@ -46,32 +52,20 @@ class Usecase: ifc_class="IfcDuctSegment", predefined_type="RIGIDSEGMENT") # This duct is part of the system - ifcopenshell.api.run("system.assign_system", model, product=duct, system=system) + ifcopenshell.api.run("system.assign_system", model, products=[duct], system=system) """ self.file = file self.settings = { - "product": product, + "products": products, "system": system, } def execute(self): system = self.settings["system"] - product = self.settings["product"] - if not ifcopenshell.util.system.is_assignable(product, system): - raise TypeError(f"You cannot assign an {product.is_a()} to an {system.is_a()}") + products = self.settings["products"] - if not self.settings["system"].IsGroupedBy: - return self.file.create_entity( - "IfcRelAssignsToGroup", - **{ - "GlobalId": ifcopenshell.guid.new(), - "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), - "RelatedObjects": [self.settings["product"]], - "RelatingGroup": self.settings["system"], - }, - ) - rel = self.settings["system"].IsGroupedBy[0] - related_objects = set(rel.RelatedObjects) or set() - related_objects.add(self.settings["product"]) - rel.RelatedObjects = list(related_objects) - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + if not all(ifcopenshell.util.system.is_assignable(failed_product := product, system) for product in products): + raise TypeError(f"You cannot assign an {failed_product.is_a()} to an {system.is_a()}") + + rel = ifcopenshell.api.run("group.assign_group", self.file, products=products, group=system) + return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py index c919a153d0..4d0b75e8a6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py @@ -77,8 +77,8 @@ class Usecase: ifc_class="IfcDuctFitting", predefined_type="BEND") # The duct and fitting is part of the system - ifcopenshell.api.run("system.assign_system", model, product=duct, system=system) - ifcopenshell.api.run("system.assign_system", model, product=fitting, system=system) + ifcopenshell.api.run("system.assign_system", model, products=[duct], system=system) + ifcopenshell.api.run("system.assign_system", model, products=[fitting], system=system) # Create 2 ports, one for either end of both the duct and fitting. duct_port1 = ifcopenshell.api.run("system.add_port", model, element=duct) diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py index dd17078964..12d48af4b2 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py @@ -47,8 +47,8 @@ class Usecase: ifc_class="IfcDuctFitting", predefined_type="BEND") # The duct and fitting is part of the system - ifcopenshell.api.run("system.assign_system", model, product=duct, system=system) - ifcopenshell.api.run("system.assign_system", model, product=fitting, system=system) + ifcopenshell.api.run("system.assign_system", model, products=[duct], system=system) + ifcopenshell.api.run("system.assign_system", model, products=[fitting], system=system) # Create 2 ports, one for either end of both the duct and fitting. duct_port1 = ifcopenshell.api.run("system.add_port", model, element=duct) diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/unassign_system.py b/src/ifcopenshell-python/ifcopenshell/api/system/unassign_system.py index c96ffa02a1..b402407038 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/unassign_system.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/unassign_system.py @@ -22,11 +22,16 @@ import ifcopenshell.util.element class Usecase: - def __init__(self, file, product=None, system=None): - """Unassigns a product from a system + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + system: ifcopenshell.entity_instance, + ): + """Unassigns list of products from a system - :param product: The IfcDistributionElement to unassign from the system. - :type product: ifcopenshell.entity_instance.entity_instance + :param products: The list of IfcDistributionElements to unassign from the system. + :type products: list[ifcopenshell.entity_instance.entity_instance] :param system: The IfcSystem you want to unassign the element from. :type system: ifcopenshell.entity_instance.entity_instance :return: None @@ -44,28 +49,18 @@ class Usecase: ifc_class="IfcDuctSegment", predefined_type="RIGIDSEGMENT") # This duct is part of the system - ifcopenshell.api.run("system.assign_system", model, product=duct, system=system) + ifcopenshell.api.run("system.assign_system", model, products=[duct], system=system) # Not anymore! - ifcopenshell.api.run("system.unassign_system", model, product=duct, system=system) + ifcopenshell.api.run("system.unassign_system", model, products=[duct], system=system) """ self.file = file self.settings = { - "product": product, + "products": products, "system": system, } def execute(self): - if not self.settings["system"].IsGroupedBy: - return - rel = self.settings["system"].IsGroupedBy[0] - related_objects = set(rel.RelatedObjects) or set() - related_objects.remove(self.settings["product"]) - if len(related_objects): - rel.RelatedObjects = list(related_objects) - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) - else: - history = rel.OwnerHistory - self.file.remove(rel) - if history: - ifcopenshell.util.element.remove_deep2(self.file, history) + ifcopenshell.api.run( + "group.unassign_group", self.file, products=self.settings["products"], group=self.settings["system"] + ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py b/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py index 622f034b79..dcafff9e88 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py +++ b/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py @@ -19,11 +19,18 @@ import ifcopenshell import ifcopenshell.api import ifcopenshell.util.element +from typing import Union, Iterable class Usecase: - def __init__(self, file, related_object=None, relating_type=None, should_map_representations=True): - """Assigns a type to an occurrence of an object + def __init__( + self, + file: ifcopenshell.file, + related_objects: list[ifcopenshell.entity_instance], + relating_type: ifcopenshell.entity_instance, + should_map_representations=True, + ): + """Assigns a type to occurrences of an object IFC supports the concept of occurrences and types. An occurrence is an actual physical product in the real world: like a wall, a chair, a door, @@ -83,17 +90,19 @@ class Usecase: as-built or dilapidation models, where existing conditions are ambiguous, unknown or are so bespoke as to have no logical type. - :param related_object: The IfcElement occurrence. - :type related_object: ifcopenshell.entity_instance.entity_instance + :param related_objects: The IfcElement occurrences. + :type related_objects: list[ifcopenshell.entity_instance.entity_instance] :param relating_type: The IfcElementType type. :type relating_type: ifcopenshell.entity_instance.entity_instance :param should_map_representations: If a type has a representation map, IFC requires all occurrences to map those representations. Some IFC vendors might disobey this, or you might want to handle it yourself. In this scenario, you may set this to False. + This also enabled adding material usages mapping. :type should_map_representations: bool :return: The IfcRelDefinesByType relationship - :rtype: ifcopenshell.entity_instance.entity_instance + or `None` if `related_objects` was empty list. + :rtype: Union[ifcopenshell.entity_instance.entity_instance, None] Example: @@ -111,7 +120,7 @@ class Usecase: # had a representation, the furniture occurrence will also now have # the exact same representation. This is highly efficient as you # don't need to define the representation for every occurrence. - ifcopenshell.api.run("type.assign_type", model, related_object=furniture, relating_type=furniture_type) + ifcopenshell.api.run("type.assign_type", model, related_objects=[furniture], relating_type=furniture_type) # Let's imagine a parametric material layer set wall_type = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWallType", name="WAL01") @@ -140,13 +149,13 @@ class Usecase: ifcopenshell.api.run("material.edit_layer", model, layer=layer, attributes={"LayerThickness": .013}) # Great! Let's assign our material set to our wall type. - ifcopenshell.api.run("material.assign_material", model, product=wall_type, material=material_set) + ifcopenshell.api.run("material.assign_material", model, products=[wall_type], material=material_set) # Now, let's create a wall. wall = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWall") # The wall is a WAL01 wall type. - ifcopenshell.api.run("type.assign_type", model, related_object=wall, relating_type=wall_type) + ifcopenshell.api.run("type.assign_type", model, related_objects=[wall], relating_type=wall_type) # A bit of preparation, let's create some geometric contexts since # we want to create some geometry for our wall. @@ -167,81 +176,100 @@ class Usecase: """ self.file = file self.settings = { - "related_object": related_object, + "related_objects": related_objects, "relating_type": relating_type, "should_map_representations": should_map_representations, } - def execute(self): - if self.file.schema == "IFC2X3": - is_typed_by = None - is_defined_by = self.settings["related_object"].IsDefinedBy - for rel in is_defined_by: - if rel.is_a("IfcRelDefinesByType"): - is_typed_by = [rel] - break - types = self.settings["relating_type"].ObjectTypeOf - else: - is_typed_by = self.settings["related_object"].IsTypedBy - types = self.settings["relating_type"].Types - - if types and is_typed_by == types: + def execute(self) -> Union[ifcopenshell.entity_instance, None]: + if not self.settings["related_objects"]: return - if is_typed_by: - related_objects = list(is_typed_by[0].RelatedObjects) - related_objects.remove(self.settings["related_object"]) - if related_objects: - is_typed_by[0].RelatedObjects = related_objects - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": is_typed_by[0]}) + related_objects = set(self.settings["related_objects"]) + relating_type = self.settings["relating_type"] + + ifc2x3 = self.file.schema == "IFC2X3" + + related_objects = set(self.settings["related_objects"]) + relating_type = self.settings["relating_type"] + if ifc2x3: + types = next(iter(relating_type.ObjectTypeOf), None) + else: + types = next(iter(relating_type.Types), None) + + previous_types_rels: set[ifcopenshell.entity_instance] = set() + objects_without_types: list[ifcopenshell.entity_instance] = [] + objects_with_types: list[ifcopenshell.entity_instance] = [] + + # check if there is anything to change + for object in related_objects: + if ifc2x3: + object_rel = next((i for i in object.IsDefinedBy if i.is_a("IfcRelDefinesByType")), None) else: - history = is_typed_by[0].OwnerHistory - self.file.remove(is_typed_by[0]) + object_rel = next(iter(object.IsTypedBy), None) + + if object_rel is None: + objects_without_types.append(object) + continue + + # either is_nested_by is None or product is part of different rel + if object_rel != types: + previous_types_rels.add(object_rel) + objects_with_types.append(object) + + objects_to_change = objects_without_types + objects_with_types + # nothing to change + if not objects_to_change: + return types + + # unassign from previous types + for is_typed_by in previous_types_rels: + cur_related_objects = set(is_typed_by.RelatedObjects) - related_objects + if cur_related_objects: + is_typed_by.RelatedObjects = list(cur_related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": is_typed_by}) + else: + history = is_typed_by.OwnerHistory + self.file.remove(is_typed_by) if history: ifcopenshell.util.element.remove_deep2(self.file, history) + # assign objects to a new type if types: - related_objects = list(types[0].RelatedObjects) - related_objects.append(self.settings["related_object"]) - types[0].RelatedObjects = related_objects - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": types[0]}) + types.RelatedObjects = list(set(types.RelatedObjects) | related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": types}) else: types = self.file.create_entity( "IfcRelDefinesByType", **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), - "RelatedObjects": [self.settings["related_object"]], - "RelatingType": self.settings["relating_type"], - } + "RelatedObjects": list(related_objects), + "RelatingType": relating_type, + }, ) if self.settings["should_map_representations"]: - if getattr(self.settings["relating_type"], "RepresentationMaps", None): - ifcopenshell.api.run( - "type.map_type_representations", - self.file, - related_object=self.settings["related_object"], - relating_type=self.settings["relating_type"], - ) - self.map_material_usages() + if getattr(relating_type, "RepresentationMaps", None): + for related_object in objects_to_change: + ifcopenshell.api.run( + "type.map_type_representations", + self.file, + related_object=related_object, + relating_type=relating_type, + ) + self.map_material_usages(objects_to_change) return types - def map_material_usages(self): + def map_material_usages(self, related_objects: Iterable[ifcopenshell.entity_instance]) -> None: type_material = ifcopenshell.util.element.get_material(self.settings["relating_type"]) if not type_material: return - if type_material.is_a("IfcMaterialLayerSet"): + ifc_class = type_material.is_a() + if ifc_class in ("IfcMaterialLayerSet", "IfcMaterialProfileSet"): ifcopenshell.api.run( "material.assign_material", self.file, - product=self.settings["related_object"], - type="IfcMaterialLayerSetUsage", - ) - elif type_material.is_a("IfcMaterialProfileSet"): - ifcopenshell.api.run( - "material.assign_material", - self.file, - product=self.settings["related_object"], - type="IfcMaterialProfileSetUsage", + products=related_objects, + type=f"{ifc_class}Usage", ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/type/map_type_representations.py b/src/ifcopenshell-python/ifcopenshell/api/type/map_type_representations.py index b8480dc211..180e163477 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/type/map_type_representations.py +++ b/src/ifcopenshell-python/ifcopenshell/api/type/map_type_representations.py @@ -22,7 +22,12 @@ import ifcopenshell.util.element class Usecase: - def __init__(self, file, related_object=None, relating_type=None): + def __init__( + self, + file: ifcopenshell.file, + related_object: ifcopenshell.entity_instance, + relating_type: ifcopenshell.entity_instance, + ): """Ensures that all occurrences has the same representation as the type If a type has a representation, all occurrences must have the same @@ -55,7 +60,7 @@ class Usecase: # furniture type has no representation, so the furniture may also # have no representation, or any arbitrary representation that may # vary from occurrence to occurrence. - ifcopenshell.api.run("type.assign_type", model, related_object=furniture, relating_type=furniture_type) + ifcopenshell.api.run("type.assign_type", model, related_objects=[furniture], relating_type=furniture_type) # A bit of preparation, let's create some geometric contexts since # we want to create some geometry for our furniture type. @@ -87,7 +92,7 @@ class Usecase: "relating_type": relating_type, } - def execute(self): + def execute(self) -> None: if not self.settings["relating_type"].RepresentationMaps: return representations = [] diff --git a/src/ifcopenshell-python/ifcopenshell/api/type/unassign_type.py b/src/ifcopenshell-python/ifcopenshell/api/type/unassign_type.py index 843121f967..c376a58971 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/type/unassign_type.py +++ b/src/ifcopenshell-python/ifcopenshell/api/type/unassign_type.py @@ -18,14 +18,18 @@ import ifcopenshell import ifcopenshell.api +import ifcopenshell.util.element class Usecase: - def __init__(self, file, related_object=None): - """Unassigns a type of an occurrence + def __init__(self, file: ifcopenshell.file, related_objects: list[ifcopenshell.entity_instance]): + """Unassigns a type from occurrences - :param related_object: The IfcElement occurrence. - :type related_object: ifcopenshell.entity_instance.entity_instance + Note that unassigning a type doesn't automatically remove mapped representations + and material usages associated with the previously assigned type. + + :param related_objects: List of IfcElement occurrences. + :type related_objects: list[ifcopenshell.entity_instance.entity_instance] :return: None :rtype: None @@ -42,34 +46,33 @@ class Usecase: furniture = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcFurniture") # Assign the furniture to the furniture type. - ifcopenshell.api.run("type.assign_type", model, related_object=furniture, relating_type=furniture_type) + ifcopenshell.api.run("type.assign_type", model, related_objects=[furniture], relating_type=furniture_type) # Change our mind. Maybe it's a different type? - ifcopenshell.api.run("type.unassign_type", model, related_object=furniture) + ifcopenshell.api.run("type.unassign_type", model, related_objects=[furniture]) """ self.file = file - self.settings = {"related_object": related_object} + self.settings = {"related_objects": related_objects} + + def execute(self) -> None: + related_objects = set(self.settings["related_objects"]) - def execute(self): if self.file.schema == "IFC2X3": - is_typed_by = None - is_defined_by = self.settings["related_object"].IsDefinedBy - for rel in is_defined_by: - if rel.is_a("IfcRelDefinesByType"): - is_typed_by = rel + rels = set( + rel + for object in related_objects + if (rel := next((rel for rel in object.IsDefinedBy if rel.is_a("IfcRelDefinesByType")), None)) + ) else: - is_typed_by = self.settings["related_object"].IsTypedBy - if is_typed_by: - is_typed_by = is_typed_by[0] + rels = set(rel for object in related_objects if (rel := next((rel for rel in object.IsTypedBy), None))) - if is_typed_by: - related_objects = list(is_typed_by.RelatedObjects) - related_objects.remove(self.settings["related_object"]) + for rel in rels: + related_objects = set(rel.RelatedObjects) - related_objects if related_objects: - is_typed_by.RelatedObjects = related_objects - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": is_typed_by}) + rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) else: - history = is_typed_by.OwnerHistory - self.file.remove(is_typed_by) + history = rel.OwnerHistory + self.file.remove(rel) if history: ifcopenshell.util.element.remove_deep2(self.file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/entity_instance.py b/src/ifcopenshell-python/ifcopenshell/entity_instance.py index 7d364df900..00e441d9ac 100644 --- a/src/ifcopenshell-python/ifcopenshell/entity_instance.py +++ b/src/ifcopenshell-python/ifcopenshell/entity_instance.py @@ -30,7 +30,7 @@ import functools import subprocess import sys import time -from typing import Union +from typing import Union, Any, Callable, TypeVar, overload from . import ifcopenshell_wrapper from . import settings @@ -40,6 +40,8 @@ try: except ImportError as e: logging = type("logger", (object,), {"exception": staticmethod(lambda s: print(s))}) +T = TypeVar("T") + def set_derived_attribute(*args): raise TypeError("Unable to set derived attribute") @@ -196,7 +198,36 @@ class entity_instance(object): ) @staticmethod - def walk(f, g, value): + def walk(f: Callable[[Any], bool], g: Callable[[Any], Any], value: Any) -> Any: + """ + Applies transformation to `value` based on a given condition. + If value is a nested structure (e.g., a list or a tuple) will apply transformation to it's elements. + . + + :param f: A callable that takes a single argument and returns a boolean value. It represents the condition + :type f: Callable + :param g: A callable that takes a single argument and returns a transformed value. It represents the transformation + :type g: Callable + :param value: Any object, the input value to be processed + :type value: Any + :return: Transformed value + :rtype: Any + + Example: + + .. code:: python + + # Define condition and transformation functions + condition = lambda v: v == old + transform = lambda v: new + + # Usage example + attribute_value = element.RelatedElements + print(old in attribute_value, new in attribute_value) # True, False + result = element.walk(condition, transform, element.RelatedElements) + print(old in attribute_value, new in attribute_value) # False, True + """ + if isinstance(value, (tuple, list)): return tuple(map(functools.partial(entity_instance.walk, f, g), value)) elif f(value): @@ -243,16 +274,16 @@ class entity_instance(object): """ return self.wrapped_data.get_argument_name(attr_idx) - def __setattr__(self, key, value): + def __setattr__(self, key: str, value: Any) -> None: index = self.wrapped_data.get_argument_index(key) self[index] = value - def __getitem__(self, key): + def __getitem__(self, key: int) -> Any: if key < 0 or key >= len(self): raise IndexError("Attribute index {} out of range for instance of type {}".format(key, self.is_a())) return entity_instance.wrap_value(self.wrapped_data.get_argument(key), self.wrapped_data.file) - def __setitem__(self, idx, value): + def __setitem__(self, idx: int, value: T) -> T: if self.wrapped_data.file and self.wrapped_data.file.transaction: self.wrapped_data.file.transaction.store_edit(self, idx, value) @@ -286,15 +317,25 @@ class entity_instance(object): return self.wrapped_data.to_string(valid_spf) - def is_a(self, *args) -> Union[str, bool]: + @overload + def is_a(self) -> str: ... + @overload + def is_a(self, ifc_class: str) -> bool: ... + @overload + def is_a(self, with_schema: bool) -> str: ... + def is_a(self, *args: Union[str, bool]) -> Union[str, bool]: """Return the IFC class name of an instance, or checks if an instance belongs to a class. The check will also return true if a parent class name is provided. :param args: If specified, is a case insensitive IFC class name to check - :type args: string + or if specified as a boolean then will define whether + returned IFC class name should include schema name + (e.g. "IFC4.IfcWall" if `True` and "IfcWall" if `False`). + If omitted will act as `False`. + :type args: Union[str, bool] :returns: Either the name of the class, or a boolean if it passes the check - :rtype: string|bool + :rtype: Union[str, bool] Example: diff --git a/src/ifcopenshell-python/ifcopenshell/file.py b/src/ifcopenshell-python/ifcopenshell/file.py index 93828df396..8c7914a8b0 100644 --- a/src/ifcopenshell-python/ifcopenshell/file.py +++ b/src/ifcopenshell-python/ifcopenshell/file.py @@ -28,7 +28,7 @@ import numbers import zipfile import functools from pathlib import Path -from typing import Tuple, List +from typing import List, Optional import ifcopenshell.util.element import ifcopenshell.util.file @@ -191,15 +191,20 @@ class file(object): .. code:: python - ifc_file = ifcopenshell.open(file_path) - products = ifc_file.by_type("IfcProduct") + model = ifcopenshell.open(file_path) + products = model.by_type("IfcProduct") print(products[0].id(), products[0].GlobalId) # 122 2XQ$n5SLP5MBLyL442paFx - print(products[0] == ifc_file[122] == ifc_file["2XQ$n5SLP5MBLyL442paFx"]) # True + print(products[0] == model[122] == model["2XQ$n5SLP5MBLyL442paFx"]) # True """ wrapped_data: ifcopenshell_wrapper.file - def __init__(self, f: ifcopenshell_wrapper.file = None, schema: str = None, schema_version: Tuple[int] = None): + def __init__( + self, + f: Optional[ifcopenshell_wrapper.file] = None, + schema: Optional[str] = None, + schema_version: Optional[tuple[int, int, int, int]] = None, + ): """Create a new blank IFC model This IFC model does not have any entities in it yet. See the @@ -223,7 +228,7 @@ class file(object): ADD2 TC1, which is the official version approved by ISO when people refer to "IFC4". Generally you should not use this argument unless you are testing non-ISO IFC releases. - :type schema_version: tuple[int] + :type schema_version: tuple[int, int, int, int] Example: @@ -244,7 +249,7 @@ class file(object): prefixes = ("IFC", "X", "_ADD", "_TC") schema = "".join("".join(map(str, t)) if t[1] else "" for t in zip(prefixes, schema_version)) else: - schema = {"IFC4X3": "IFC4X3_ADD1"}.get(schema, schema) + schema = {"IFC4X3": "IFC4X3_ADD2"}.get(schema, schema) if f is not None: self.wrapped_data = f else: @@ -443,7 +448,7 @@ class file(object): [self.transaction.store_create(e) for e in reversed(added_elements)] return result - def by_type(self, type: str, include_subtypes=True) -> List[ifcopenshell.entity_instance]: + def by_type(self, type: str, include_subtypes=True) -> list[ifcopenshell.entity_instance]: """Return IFC objects filtered by IFC Type and wrapped with the entity_instance class. If an IFC type class has subclasses, all entities of those subclasses are also returned. @@ -453,7 +458,7 @@ class file(object): :param include_subtypes: Whether or not to return subtypes of the IFC class :type include_subtypes: bool :returns: A list of ifcopenshell.entity_instance.entity_instance objects - :rtype: list + :rtype: list[ifcopenshell.entity_instance.entity_instance] """ if include_subtypes: return [entity_instance(e, self) for e in self.wrapped_data.by_type(type)] @@ -461,7 +466,7 @@ class file(object): def traverse( self, inst: ifcopenshell.entity_instance, max_levels=None, breadth_first=False - ) -> List[ifcopenshell.entity_instance]: + ) -> list[ifcopenshell.entity_instance]: """Get a list of all referenced instances for a particular instance including itself :param inst: The entity instance to get all sub instances @@ -471,7 +476,7 @@ class file(object): :param breadth_first: Whether to use breadth-first search, the default is depth-first. :type max_levels: bool :returns: A list of ifcopenshell.entity_instance.entity_instance objects - :rtype: list + :rtype: list[ifcopenshell.entity_instance.entity_instance] """ if max_levels is None: max_levels = -1 @@ -485,7 +490,7 @@ class file(object): def get_inverse( self, inst: ifcopenshell.entity_instance, allow_duplicate=False, with_attribute_indices=False - ) -> List[ifcopenshell.entity_instance]: + ) -> list[ifcopenshell.entity_instance]: """Return a list of entities that reference this entity :param inst: The entity instance to get inverse relationships @@ -494,7 +499,7 @@ class file(object): :param with_attribute_indices: Returns pairs of where i[idx] is inst or contains inst. Requires allow_duplicate=True :returns: A list of ifcopenshell.entity_instance.entity_instance objects - :rtype: list + :rtype: list[ifcopenshell.entity_instance.entity_instance] """ if with_attribute_indices and not allow_duplicate: raise ValueError("with_attribute_indices requires allow_duplicate to be True") @@ -595,7 +600,7 @@ class file(object): return @staticmethod - def from_string(s: str) -> ifcopenshell.entity_instance: + def from_string(s: str) -> file: return file(ifcopenshell_wrapper.read(s)) @staticmethod diff --git a/src/ifcopenshell-python/ifcopenshell/geom/main.py b/src/ifcopenshell-python/ifcopenshell/geom/main.py index ad1a705e83..cbc7f4ce21 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/main.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/main.py @@ -20,6 +20,7 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import annotations import os import sys @@ -31,7 +32,7 @@ from ..entity_instance import entity_instance from . import has_occ -from typing import TypeVar +from typing import TypeVar, Union, Optional T = TypeVar("T") @@ -79,7 +80,14 @@ class settings(ifcopenshell_wrapper.SerializerSettings): # Make sure people are able to use python's platform agnostic paths class iterator(ifcopenshell_wrapper.Iterator): - def __init__(self, settings, file_or_filename, num_threads=1, include=None, exclude=None): + def __init__( + self, + settings: settings, + file_or_filename: Union[file, str], + num_threads: int = 1, + include: Optional[list[entity_instance]] = None, + exclude: Optional[list[entity_instance]] = None, + ): self.settings = settings if isinstance(file_or_filename, file): file_or_filename = file_or_filename.wrapped_data @@ -128,7 +136,7 @@ class iterator(ifcopenshell_wrapper.Iterator): class tree(ifcopenshell_wrapper.tree): - def __init__(self, file=None, settings=None): + def __init__(self, file: Optional[file] = None, settings: Optional[settings] = None): args = [self] if file is not None: args.append(file.wrapped_data) @@ -136,13 +144,19 @@ class tree(ifcopenshell_wrapper.tree): args.append(settings) ifcopenshell_wrapper.tree.__init__(*args) - def add_file(self, file, settings): + def add_file(self, file: file, settings: settings) -> None: ifcopenshell_wrapper.tree.add_file(self, file.wrapped_data, settings) - def add_iterator(self, iterator): + def add_iterator(self, iterator: iterator) -> None: ifcopenshell_wrapper.tree.add_file(self, iterator) - def select(self, value, **kwargs): + def select( + self, + value: Union[ + entity_instance, ifcopenshell_wrapper.BRepElement, tuple[float, float, float], TopoDS.TopoDS_Shape + ], + **kwargs, + ) -> list[entity_instance]: def unwrap(value): if isinstance(value, entity_instance): return value.wrapped_data @@ -166,7 +180,7 @@ class tree(ifcopenshell_wrapper.tree): args.append(kwargs["extend"]) return [entity_instance(e) for e in ifcopenshell_wrapper.tree.select(*args)] - def select_box(self, value, **kwargs): + def select_box(self, value, **kwargs) -> list[entity_instance]: def unwrap(value): if isinstance(value, entity_instance): return value.wrapped_data @@ -181,8 +195,22 @@ class tree(ifcopenshell_wrapper.tree): args.append(kwargs.get("extend", -1.0e-5)) return [entity_instance(e) for e in ifcopenshell_wrapper.tree.select_box(*args)] + def clash_intersection_many(self, set_a, set_b, tolerance=0.002, check_all=True): + args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], tolerance, check_all] + return ifcopenshell_wrapper.tree.clash_intersection_many(*args) -def create_shape(settings, inst, repr=None): + def clash_collision_many(self, set_a, set_b, allow_touching=False): + args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], allow_touching] + return ifcopenshell_wrapper.tree.clash_collision_many(*args) + + def clash_clearance_many(self, set_a, set_b, clearance=0.05, check_all=False): + args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], clearance, check_all] + return ifcopenshell_wrapper.tree.clash_clearance_many(*args) + + +def create_shape( + settings: settings, inst: entity_instance, repr: Optional[entity_instance] = None +) -> ifcopenshell_wrapper.TriangulationElement: """ Return a geometric representation from STEP-based IFCREPRESENTATIONSHAPE or @@ -285,7 +313,7 @@ class serializers: buffer = ifcopenshell_wrapper.buffer # gltf and hdf5 availability depend on IfcOpenShell configuration settings try: - gtlf = ifcopenshell_wrapper.GltfSerializer + gltf = ifcopenshell_wrapper.GltfSerializer except: pass try: diff --git a/src/ifcopenshell-python/ifcopenshell/sql.py b/src/ifcopenshell-python/ifcopenshell/sql.py index d258ca0378..546b2b4101 100644 --- a/src/ifcopenshell-python/ifcopenshell/sql.py +++ b/src/ifcopenshell-python/ifcopenshell/sql.py @@ -183,7 +183,7 @@ class sqlite(file): return True return False - def get_geometry(self, ids): + def get_geometry(self, ids: list[int]) -> dict[str, dict]: import numpy as np ids_csv = ",".join(map(str, ids)) diff --git a/src/ifcopenshell-python/ifcopenshell/util/attribute.py b/src/ifcopenshell-python/ifcopenshell/util/attribute.py index 3dd99df0fe..b52fea7e3f 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/attribute.py +++ b/src/ifcopenshell-python/ifcopenshell/util/attribute.py @@ -26,6 +26,8 @@ def get_primitive_type(attribute_or_data_type): return get_primitive_type(data_type[data_type[1:].find("<") + 1 :]) elif data_type.find(". import ifcopenshell.util.element +from typing import Optional -def get_references(element, should_inherit=True): +def get_references(element: ifcopenshell.entity_instance, should_inherit=True) -> set[ifcopenshell.entity_instance]: results = set() if not element.is_a("IfcRoot"): - if hasattr(element, "HasExternalReferences"): - return {r.RelatingReference for r in element.HasExternalReferences or []} - elif hasattr(element, "HasExternalReference"): # Seriously, IFC? - return {r.RelatingReference for r in element.HasExternalReference or []} + if (references := getattr(element, "HasExternalReferences", None)) is not None or ( + references := getattr(element, "HasExternalReference", None) + ) is not None: + return {r.RelatingReference for r in references} if should_inherit: element_type = ifcopenshell.util.element.get_type(element) if element_type and element_type != element: @@ -50,13 +51,13 @@ def get_references(element, should_inherit=True): return occurrence_results -def get_classification(reference): +def get_classification(reference: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: if reference.is_a("IfcClassification"): return reference return get_classification(reference.ReferencedSource) -def get_inherited_references(reference): +def get_inherited_references(reference: Optional[ifcopenshell.entity_instance]) -> list[ifcopenshell.entity_instance]: results = [] while True: if not reference or reference.is_a("IfcClassification"): @@ -64,4 +65,3 @@ def get_inherited_references(reference): results.append(reference) reference = reference.ReferencedSource return results - diff --git a/src/ifcopenshell-python/ifcopenshell/util/constraint.py b/src/ifcopenshell-python/ifcopenshell/util/constraint.py index b2a38d6060..b8f6aac14f 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/constraint.py +++ b/src/ifcopenshell-python/ifcopenshell/util/constraint.py @@ -18,20 +18,59 @@ # # -def get_constraints(product): +import ifcopenshell +from typing import Union + + +def get_constraints(product: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: + """ + Retrieves the constraints assigned to the `product`. + + :param product: The IFC element. + :type product: ifcopenshell.entity_instance.entity_instance + :return: List of assigned constraints. + :rtype: list[ifcopenshell.entity_instance.entity_instance] + """ constraints = [] for rel in product.HasAssociations or []: if rel.is_a("IfcRelAssociatesConstraint"): constraints.append(rel.RelatingConstraint) return constraints -def get_metrics(constraint): + +def get_constrained_elements(constraint: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]: + """ + Retrieves the elements constrained by a `constraint`. + + :param product: The IFC element. + :type product: ifcopenshell.entity_instance.entity_instance + :return: Set of elements constrained by a `constrant`. + :rtype: set[ifcopenshell.entity_instance.entity_instance] + """ + elements = set() + for rel in constraint.file.get_inverse(constraint): + if rel.is_a("IfcRelAssociatesConstraint"): + elements.update(rel.RelatedObjects) + return elements + + +def get_metrics(constraint: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: + """ + Retrieves the list of nested constraints for a IfcObjective `constraint`. + + :param product: IfcObjective constraint. + :type product: ifcopenshell.entity_instance.entity_instance + :return: List of nested constraints. + :rtype: list[ifcopenshell.entity_instance.entity_instance] + """ + metrics = [] for metric in constraint.BenchmarkValues or []: metrics.append(metric) return metrics -def get_metric_reference(metric, is_deep=True): + +def get_metric_reference(metric: ifcopenshell.entity_instance, is_deep=True): def get_reference_Attribute(ref, path): if ref: if is_deep: @@ -47,7 +86,10 @@ def get_metric_reference(metric, is_deep=True): reference = metric.ReferencePath return get_reference_Attribute(reference, "") -def get_metric_constraints(resource, attribute): + +def get_metric_constraints( + resource: ifcopenshell.entity_instance, attribute +) -> Union[list[ifcopenshell.entity_instance], None]: metrics = [] for constraint in get_constraints(resource) or []: for metric in get_metrics(constraint) or []: @@ -60,15 +102,14 @@ def get_metric_constraints(resource, attribute): return metrics return None -def is_hard_constraint(metric): - if metric.ConstraintGrade == "HARD" and metric.Benchmark == "EQUALTO": - return True -def is_attribute_locked(product, attribute): +def is_hard_constraint(metric: ifcopenshell.entity_instance) -> bool: + return metric.ConstraintGrade == "HARD" and metric.Benchmark == "EQUALTO" + + +def is_attribute_locked(product: ifcopenshell.entity_instance, attribute) -> bool: is_locked = False - metrics = get_metric_constraints( - product, attribute - ) + metrics = get_metric_constraints(product, attribute) for metric in metrics or []: if is_hard_constraint(metric): is_locked = True diff --git a/src/ifcopenshell-python/ifcopenshell/util/data.py b/src/ifcopenshell-python/ifcopenshell/util/data.py index e6761edd5d..a66ea80252 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/data.py +++ b/src/ifcopenshell-python/ifcopenshell/util/data.py @@ -93,37 +93,3 @@ class Clipping: second_operand = ifc_file.createIfcHalfSpaceSolid(plane, False) return ifc_file.createIfcBooleanClippingResult("DIFFERENCE", first_operand, second_operand) - - -class WorkTimeDatesInterface: - def __init__(self, work_time: ifcopenshell.entity_instance): - """Utility class for IfcWorkTime dates allowing refer - to it's dates as `.Start`/`.Finish` or `.StartDate`/`.FinishDate` - regardless of the schema version - """ - if hasattr(work_time, "StartDate"): # since IFC4X3 - self.start_attr, self.finish_attr = "StartDate", "FinishDate" - else: - self.start_attr, self.finish_attr = "Start", "Finish" - - self.work_time = work_time - - @property - def Start(self): - return getattr(self.work_time, self.start_attr) - - @Start.setter - def Start(self, value): - return setattr(self.work_time, self.start_attr, value) - - StartDate = Start - - @property - def Finish(self): - return getattr(self.work_time, self.finish_attr) - - @Finish.setter - def Finish(self, value): - return setattr(self.work_time, self.finish_attr, value) - - FinishDate = Finish diff --git a/src/ifcopenshell-python/ifcopenshell/util/doc.py b/src/ifcopenshell-python/ifcopenshell/util/doc.py index 6ee10ada7f..d041d6b628 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/doc.py +++ b/src/ifcopenshell-python/ifcopenshell/util/doc.py @@ -18,10 +18,10 @@ import json from pathlib import Path -from pprint import pprint import copy import ifcopenshell import ifcopenshell.util.attribute +import ifcopenshell.util.schema try: import glob @@ -103,7 +103,7 @@ def get_db(version): return db.get(version) -def get_schema_by_name(version): +def get_schema_by_name(version: str): global schema_by_name version = ifcopenshell.util.schema.get_fallback_schema(version) if not schema_by_name[version]: @@ -151,7 +151,7 @@ def get_predefined_type_doc(version, entity, predefined_type): if db: entity = db["entities"].get(entity) if entity: - return entity["predefined_types"].get(predefined_type) + return entity.get("predefined_types", {}).get(predefined_type) def get_property_set_doc(version, pset): diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 6ad072d48d..f8b8791787 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -18,10 +18,20 @@ from __future__ import annotations import ifcopenshell -from typing import List +import ifcopenshell.util.element +from typing import Any, Callable, Optional, Union, Literal, overload +from collections import namedtuple -def get_pset(element, name, prop=None, psets_only=False, qtos_only=False, should_inherit=True, verbose=False): +def get_pset( + element: ifcopenshell.entity_instance, + name: str, + prop: Optional[str] = None, + psets_only: bool = False, + qtos_only: bool = False, + should_inherit: bool = True, + verbose: bool = False, +) -> Union[Any, dict[str, Any]]: """Retrieve a single property set or single property This is more efficient than ifcopenshell.util.element.get_psets if you know @@ -44,13 +54,13 @@ def get_pset(element, name, prop=None, psets_only=False, qtos_only=False, should :type should_inherit: bool,optional :return: A dictionary of property names and values, or a single value if a property is specified. - :rtype: dict + :rtype: Union[Any, dict[str, Any]] Example: .. code:: python - element = ifcopenshell.by_type("IfcWall")[0] + element = ifc_file.by_type("IfcWall")[0] psets_and_qtos = ifcopenshell.util.element.get_pset(element, "Pset_WallCommon") """ pset = None @@ -66,12 +76,13 @@ def get_pset(element, name, prop=None, psets_only=False, qtos_only=False, should if definition.Name == name: pset = definition break - elif hasattr(element, "IsDefinedBy"): + elif (is_defined_by := getattr(element, "IsDefinedBy", None)) is not None: + # other IfcObjectDefinition if should_inherit: element_type = ifcopenshell.util.element.get_type(element) if element_type: type_pset = get_pset(element_type, name, prop, should_inherit=False, verbose=verbose) - for relationship in element.IsDefinedBy: + for relationship in is_defined_by: if relationship.is_a("IfcRelDefinesByProperties"): definition = relationship.RelatingPropertyDefinition if definition.Name == name: @@ -84,13 +95,15 @@ def get_pset(element, name, prop=None, psets_only=False, qtos_only=False, should elif qtos_only and not pset.is_a("IfcElementQuantity"): pset = None - if type_pset: - if psets_only and not type_pset.is_a("IfcPropertySet"): - type_pset = None - elif qtos_only and not type_pset.is_a("IfcElementQuantity"): - type_pset = None + 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"): + type_pset = None + elif qtos_only and not type_pset_element.is_a("IfcElementQuantity"): + type_pset = None - if not pset and not type_pset: + if pset is None and type_pset is None: return if not prop: @@ -107,7 +120,9 @@ def get_pset(element, name, prop=None, psets_only=False, qtos_only=False, should return value -def get_psets(element, psets_only=False, qtos_only=False, should_inherit=True, verbose=False): +def get_psets( + element: ifcopenshell.entity_instance, psets_only=False, qtos_only=False, should_inherit=True, verbose=False +) -> dict[str, dict[str, Any]]: """Retrieve property sets, their related properties' names & values and ids. If should_inherit is true, the pset "id" only refers to the ID of the @@ -121,14 +136,16 @@ def get_psets(element, psets_only=False, qtos_only=False, should_inherit=True, v :type qtos_only: bool,optional :param should_inherit: Default as True. Set to false if you don't want to inherit property sets from the Type. :type should_inherit: bool,optional + :param verbose: More detailed prop values, defaults to False. + :type verbose: bool,optional :return: Key, value pair of psets' names and their properties' names & values - :rtype: dict + :rtype: dict[str, dict[str, Any]] Example: .. code:: python - element = ifcopenshell.by_type("IfcWall")[0] + element = ifc_file.by_type("IfcWall")[0] psets = ifcopenshell.util.element.get_psets(element, psets_only=True) qsets = ifcopenshell.util.element.get_psets(element, qtos_only=True) psets_and_qtos = ifcopenshell.util.element.get_psets(element) @@ -146,12 +163,15 @@ def get_psets(element, psets_only=False, qtos_only=False, should_inherit=True, v if qtos_only: continue psets[definition.Name] = get_property_definition(definition, verbose=verbose) - elif hasattr(element, "IsDefinedBy"): + elif (is_defined_by := getattr(element, "IsDefinedBy", None)) is not None: + # other IfcObjectDefinition if should_inherit: element_type = ifcopenshell.util.element.get_type(element) if element_type: - psets = get_psets(element_type, psets_only=psets_only, qtos_only=qtos_only, should_inherit=False) - for relationship in element.IsDefinedBy: + psets = get_psets( + element_type, psets_only=psets_only, qtos_only=qtos_only, should_inherit=False, verbose=verbose + ) + for relationship in is_defined_by: if relationship.is_a("IfcRelDefinesByProperties"): definition = relationship.RelatingPropertyDefinition if psets_only and not definition.is_a("IfcPropertySet"): @@ -162,52 +182,81 @@ def get_psets(element, psets_only=False, qtos_only=False, should_inherit=True, v return psets -def get_property_definition(definition, prop=None, verbose=False): +@overload +def get_property_definition( + definition: Optional[ifcopenshell.entity_instance], prop: None = None, verbose=False +) -> dict[str, Any]: ... +@overload +def get_property_definition(definition: Optional[ifcopenshell.entity_instance], prop: str, verbose=False) -> Any: ... +@overload +def get_property_definition(definition: None, prop: None = None, verbose: bool = False) -> None: ... +def get_property_definition( + definition: Optional[ifcopenshell.entity_instance], prop: Optional[str] = None, verbose=False +) -> Union[Any, dict[str, Any]]: + """if prop name is not provided in `prop`, will return dict of all available properties + otherwise will return the value of the specified `prop`. + """ if not definition: return + ifc_class = definition.is_a() + if prop: - if definition.is_a("IfcElementQuantity"): + if ifc_class == "IfcElementQuantity": return get_quantity(definition.Quantities, prop, verbose=verbose) - elif definition.is_a("IfcPropertySet"): + elif ifc_class == "IfcPropertySet": return get_property(definition.HasProperties, prop, verbose=verbose) - elif definition.is_a("IfcMaterialProperties") or definition.is_a("IfcProfileProperties"): + elif ifc_class == "IfcMaterialProperties" or ifc_class == "IfcProfileProperties": + # IfcExtendedProperties return get_property(definition.Properties, prop, verbose=verbose) else: # Entity introduced in IFC4 # definition.is_a('IfcPreDefinedPropertySet'): for i in range(4, len(definition)): - if definition[i] is not None: - if definition.attribute_name(i) == prop: - return definition[i] + if definition.attribute_name(i) == prop: + if (v := definition[i]) is not None: + return v return props = {} - if definition.is_a("IfcElementQuantity"): - props.update(get_quantities(definition.Quantities, verbose=verbose)) - elif definition.is_a("IfcPropertySet"): - props.update(get_properties(definition.HasProperties, verbose=verbose)) - elif definition.is_a("IfcMaterialProperties") or definition.is_a("IfcProfileProperties"): - props.update(get_properties(definition.Properties, verbose=verbose)) + if ifc_class == "IfcElementQuantity": + # 5 IfcElementQuantity.Quantities + props.update(get_quantities(definition[5], verbose=verbose)) + elif ifc_class == "IfcPropertySet": + # 5 IfcPropertySet.HasProperties + props.update(get_properties(definition[4], verbose=verbose)) + elif ifc_class == "IfcMaterialProperties" or ifc_class == "IfcProfileProperties": + # 2 IfcExtendedProperties.Properties + props.update(get_properties(definition[2], verbose=verbose)) else: # Entity introduced in IFC4 # definition.is_a('IfcPreDefinedPropertySet'): - for prop in range(4, len(definition)): - if definition[prop] is not None: - props[definition.attribute_name(prop)] = definition[prop] + for prop_i in range(4, len(definition)): + if (v := definition[prop_i]) is not None: + props[definition.attribute_name(prop_i)] = v props["id"] = definition.id() return props -def get_quantity(quantities, name, verbose=False): +@overload +def get_quantity(quantities: list[ifcopenshell.entity_instance], name: str, verbose: Literal[False] = False) -> Any: ... +@overload +def get_quantity( + quantities: list[ifcopenshell.entity_instance], name: str, verbose: Literal[True] +) -> dict[str, Any]: ... +def get_quantity( + quantities: list[ifcopenshell.entity_instance], name: str, verbose=False +) -> Union[Any, dict[str, Any]]: for quantity in quantities or []: - if quantity.Name != name: + # 0 IfcPhysicalQuantity.Name + if quantity[0] != name: continue if quantity.is_a("IfcPhysicalSimpleQuantity"): + # 3 IfcPhysicalSimpleQuantity.Unit result = quantity[3] elif quantity.is_a("IfcPhysicalComplexQuantity"): data = {k: v for k, v in quantity.get_info().items() if v is not None and k != "Name"} - data["properties"] = get_quantities(quantity.HasQuantities) + data["properties"] = get_quantities(quantity.HasQuantities, verbose=verbose) del data["HasQuantities"] result = data if verbose: @@ -215,41 +264,65 @@ def get_quantity(quantities, name, verbose=False): return result -def get_quantities(quantities, verbose=False): +@overload +def get_quantities( + quantities: list[ifcopenshell.entity_instance], verbose: Literal[False] = False +) -> dict[str, Any]: ... +@overload +def get_quantities( + quantities: list[ifcopenshell.entity_instance], verbose: Literal[True] +) -> dict[str, dict[str, Any]]: ... +def get_quantities( + quantities: list[ifcopenshell.entity_instance], verbose=False +) -> dict[str, Union[Any, dict[str, Any]]]: results = {} for quantity in quantities or []: + # 0 IfcPhysicalQuantity.Name + quantity_name = quantity[0] if quantity.is_a("IfcPhysicalSimpleQuantity"): - results[quantity.Name] = quantity[3] + # 3 IfcPhysicalSimpleQuantity.Unit + results[quantity_name] = quantity[3] if verbose: - results[quantity.Name] = { + results[quantity_name] = { "id": quantity.id(), "class": quantity.is_a(), - "value": results[quantity.Name], + "value": results[quantity_name], } elif quantity.is_a("IfcPhysicalComplexQuantity"): data = {k: v for k, v in quantity.get_info().items() if v is not None and k != "Name"} - data["properties"] = get_quantities(quantity.HasQuantities) + data["properties"] = get_quantities(quantity.HasQuantities, verbose=verbose) del data["HasQuantities"] - results[quantity.Name] = data + results[quantity_name] = data if verbose: - results[quantity.Name] = { - "id": quantity.id(), - "class": quantity.is_a(), - "value": results[quantity.Name], + results[quantity_name] = { + "id": data["id"], + "class": data["class"], + "value": results[quantity_name], } return results -def get_property(properties, name, verbose=False): +@overload +def get_property(properties: list[ifcopenshell.entity_instance], name: str, verbose: Literal[False] = False) -> Any: ... +@overload +def get_property( + properties: list[ifcopenshell.entity_instance], name: str, verbose: Literal[True] +) -> dict[str, Any]: ... +def get_property( + properties: list[ifcopenshell.entity_instance], name: str, verbose=False +) -> Union[Any, dict[str, Any]]: for prop in properties or []: if prop.Name != name: continue if prop.is_a("IfcPropertySingleValue"): - result = prop.NominalValue.wrappedValue if prop.NominalValue else None + # 2 IfcPropertySingleValue.NominalValue + result = v.wrappedValue if (v := prop[2]) else None elif prop.is_a("IfcPropertyEnumeratedValue"): - result = [v.wrappedValue for v in prop.EnumerationValues] if prop.EnumerationValues else None + # 2 IfcPropertyEnumeratedValue.EnumerationValues + result = [v.wrappedValue for v in values] if (values := prop[2]) else None elif prop.is_a("IfcPropertyListValue"): - result = [v.wrappedValue for v in prop.ListValues] or None + # 2 IfcPropertyListValue.ListValues + result = [v.wrappedValue for v in values] if (values := prop[2]) else None elif prop.is_a("IfcPropertyBoundedValue"): data = prop.get_info() del data["Unit"] @@ -258,7 +331,7 @@ def get_property(properties, name, verbose=False): result = prop.get_info() elif prop.is_a("IfcComplexProperty"): data = {k: v for k, v in prop.get_info().items() if v is not None and k != "Name"} - data["properties"] = get_properties(prop.HasProperties) + data["properties"] = get_properties(prop.HasProperties, verbose=verbose) del data["HasProperties"] result = data if verbose: @@ -266,42 +339,78 @@ def get_property(properties, name, verbose=False): return result -def get_properties(properties, verbose=False): +@overload +def get_properties( + properties: list[ifcopenshell.entity_instance], verbose: Literal[False] = False +) -> dict[str, Any]: ... +@overload +def get_properties( + properties: list[ifcopenshell.entity_instance], verbose: Literal[True] +) -> dict[str, dict[str, Any]]: ... +def get_properties( + properties: list[ifcopenshell.entity_instance], verbose=False +) -> dict[str, Union[Any, dict[str, Any]]]: results = {} for prop in properties or []: - if prop.is_a("IfcPropertySingleValue"): - results[prop.Name] = prop.NominalValue.wrappedValue if prop.NominalValue else None + ifc_class = prop.is_a() + prop_name = prop[0] # 0 IfcProperty.Name + if ifc_class == "IfcPropertySingleValue": + # 2 IfcPropertySingleValue.NominalValue + results[prop_name] = v.wrappedValue if (v := prop[2]) else None if verbose: - results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]} - elif prop.is_a("IfcPropertyEnumeratedValue"): - results[prop.Name] = [v.wrappedValue for v in prop.EnumerationValues] if prop.EnumerationValues else None + results[prop_name] = { + "id": prop.id(), + "class": prop.is_a(), + "value": results[prop_name], + } + elif ifc_class == "IfcPropertyEnumeratedValue": + # 2 IfcPropertyEnumeratedValue.EnumerationValues + results[prop_name] = [v.wrappedValue for v in values] if (values := prop[2]) else None if verbose: - results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]} - elif prop.is_a("IfcPropertyListValue"): - results[prop.Name] = [v.wrappedValue for v in prop.ListValues] or None + results[prop_name] = { + "id": prop.id(), + "class": prop.is_a(), + "value": results[prop_name], + } + elif ifc_class == "IfcPropertyListValue": + # 2 IfcPropertyListValue.ListValues + results[prop_name] = [v.wrappedValue for v in values] if (values := prop[2]) else None if verbose: - results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]} - elif prop.is_a("IfcPropertyBoundedValue"): + results[prop_name] = { + "id": prop.id(), + "class": prop.is_a(), + "value": results[prop_name], + } + elif ifc_class == "IfcPropertyBoundedValue": data = prop.get_info() del data["Unit"] - results[prop.Name] = data + results[prop_name] = data if verbose: - results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]} - elif prop.is_a("IfcPropertyTableValue"): - results[prop.Name] = prop.get_info() + results[prop_name] = { + "id": data["id"], + "class": data["type"], + "value": results[prop_name], + } + elif ifc_class == "IfcPropertyTableValue": + data = prop.get_info() + results[prop_name] = data if verbose: - results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]} - elif prop.is_a("IfcComplexProperty"): + results[prop_name] = { + "id": data["id"], + "class": data["type"], + "value": results[prop_name], + } + elif ifc_class == "IfcComplexProperty": data = {k: v for k, v in prop.get_info().items() if v is not None and k != "Name"} - data["properties"] = get_properties(prop.HasProperties) + data["properties"] = get_properties(prop.HasProperties, verbose=verbose) del data["HasProperties"] - results[prop.Name] = data + results[prop_name] = data if verbose: - results[prop.Name] = {"id": prop.id(), "class": prop.is_a(), "value": results[prop.Name]} + results[prop_name] = {"id": data["id"], "class": data["class"], "value": results[prop_name]} return results -def get_predefined_type(element): +def get_predefined_type(element: ifcopenshell.entity_instance) -> str: """Retrieves the PrefefinedType attribute of an element. If the predefined type is user defined, the custom type (such as object @@ -321,20 +430,22 @@ def get_predefined_type(element): element = ifcopenshell.by_type("IfcWall")[0] predefined_type = ifcopenshell.util.element.get_predefined_type(element) """ - element_type = get_type(element) - if element_type: + if element_type := get_type(element): predefined_type = getattr(element_type, "PredefinedType", None) if predefined_type == "USERDEFINED" or not predefined_type: - predefined_type = getattr(element_type, "ElementType", getattr(element_type, "ProcessType", None)) + predefined_type = getattr(element_type, "ElementType", ...) + if predefined_type == ...: + predefined_type = getattr(element_type, "ProcessType", None) if predefined_type and predefined_type != "NOTDEFINED": return predefined_type + predefined_type = getattr(element, "PredefinedType", None) if predefined_type == "USERDEFINED" or not predefined_type: predefined_type = getattr(element, "ObjectType", None) return predefined_type -def get_type(element): +def get_type(element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: """Retrieves the construction type element of an element occurrence :param element: The element occurrence @@ -351,15 +462,15 @@ def get_type(element): """ if element.is_a("IfcTypeObject"): return element - elif hasattr(element, "IsTypedBy") and element.IsTypedBy: - return element.IsTypedBy[0].RelatingType - elif hasattr(element, "IsDefinedBy") and element.IsDefinedBy: # IFC2X3 - for relationship in element.IsDefinedBy: + elif (is_typed_by := getattr(element, "IsTypedBy", None)) is not None and is_typed_by: + return is_typed_by[0].RelatingType + elif (is_defined_by := getattr(element, "IsDefinedBy", None)) is not None and is_defined_by: # IFC2X3 + for relationship in is_defined_by: if relationship.is_a("IfcRelDefinesByType"): return relationship.RelatingType -def get_types(type): +def get_types(type: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: """Get all the occurrences of a type element :param type: The type element @@ -381,7 +492,7 @@ def get_types(type): return [] -def get_shape_aspects(element: ifcopenshell.entity_instance) -> List[ifcopenshell.entity_instance]: +def get_shape_aspects(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: """Gets element shape aspects :param element: The element to get the shape aspects of. @@ -398,8 +509,8 @@ def get_shape_aspects(element: ifcopenshell.entity_instance) -> List[ifcopenshel """ # IfcProduct - if hasattr(element, "Representation"): - return element.Representation.HasShapeAspects + if (representation := getattr(element, "Representation", ...)) != ...: + return representation.HasShapeAspects # IfcTypeProduct shape_aspects = [] @@ -408,7 +519,9 @@ def get_shape_aspects(element: ifcopenshell.entity_instance) -> List[ifcopenshel return shape_aspects -def get_material(element, should_skip_usage=False, should_inherit=True): +def get_material( + element: ifcopenshell.entity_instance, should_skip_usage=False, should_inherit=True +) -> Union[ifcopenshell.entity_instance, None]: """Gets the material of the element The material may be a single material, material set (layered, profiled, or @@ -424,8 +537,8 @@ def get_material(element, should_skip_usage=False, should_inherit=True): :param should_inherit: If True, any inherited materials from associated types will be considered. :type should_inherit: bool - :return: The associated material of the element. - :rtype: ifcopenshell.entity_instance.entity_instance + :return: The associated material of the element or `None`. + :rtype: Union[ifcopenshell.entity_instance.entity_instance, None] Example: @@ -434,22 +547,25 @@ def get_material(element, should_skip_usage=False, should_inherit=True): element = ifcopenshell.by_type("IfcWall")[0] material = ifcopenshell.util.element.get_material(element) """ - if hasattr(element, "HasAssociations") and element.HasAssociations: - for relationship in element.HasAssociations: + if (has_associations := getattr(element, "HasAssociations", None)) is not None and has_associations: + for relationship in has_associations: if relationship.is_a("IfcRelAssociatesMaterial"): if should_skip_usage: - if relationship.RelatingMaterial.is_a("IfcMaterialLayerSetUsage"): - return relationship.RelatingMaterial.ForLayerSet - elif relationship.RelatingMaterial.is_a("IfcMaterialProfileSetUsage"): - return relationship.RelatingMaterial.ForProfileSet + relating_material = relationship.RelatingMaterial + if relating_material.is_a("IfcMaterialLayerSetUsage"): + return relating_material.ForLayerSet + elif relating_material.is_a("IfcMaterialProfileSetUsage"): + return relating_material.ForProfileSet return relationship.RelatingMaterial if should_inherit: relating_type = get_type(element) - if relating_type != element and hasattr(relating_type, "HasAssociations") and relating_type.HasAssociations: + if relating_type != element and (has_associations := getattr(relating_type, "HasAssociations", None)): return get_material(relating_type, should_skip_usage) -def get_materials(element, should_inherit=True): +def get_materials( + element: ifcopenshell.entity_instance, should_inherit: bool = True +) -> list[ifcopenshell.entity_instance]: """Gets individual materials of an element If the element has a material set, the individual materials of that set are @@ -484,7 +600,7 @@ def get_materials(element, should_inherit=True): return list(material.Materials) -def get_styles(element): +def get_styles(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: """Retrieves the styles used in an element's representation. Styles may be retreived from the material or the body representation. @@ -528,7 +644,9 @@ def get_styles(element): return styles -def get_elements_by_material(ifc_file, material): +def get_elements_by_material( + ifc_file: ifcopenshell.file, material: ifcopenshell.entity_instance +) -> list[ifcopenshell.entity_instance]: """Retrieves the elements related to a material. This includes elements using the material as part of a material set or set @@ -570,7 +688,9 @@ def get_elements_by_material(ifc_file, material): return results -def get_elements_by_style(ifc_file, style): +def get_elements_by_style( + ifc_file: ifcopenshell.file, style: ifcopenshell.entity_instance +) -> list[ifcopenshell.entity_instance]: """Retrieves the elements whose geometric representation uses a style :param ifc_file: The IFC file @@ -610,7 +730,9 @@ def get_elements_by_style(ifc_file, style): return results -def get_elements_by_representation(ifc_file, representation): +def get_elements_by_representation( + ifc_file: ifcopenshell.file, representation: ifcopenshell.entity_instance +) -> list[ifcopenshell.entity_instance]: """Gets all elements using a geometric representation :param ifc_file: The IFC file @@ -642,7 +764,9 @@ def get_elements_by_representation(ifc_file, representation): return results -def get_elements_by_layer(ifc_file, layer): +def get_elements_by_layer( + ifc_file: ifcopenshell.file, layer: ifcopenshell.entity_instance +) -> list[ifcopenshell.entity_instance]: """Get all the elements that are used by a presentation layer :param ifc_file: The IFC file @@ -663,7 +787,9 @@ def get_elements_by_layer(ifc_file, layer): return results -def get_layers(ifc_file, element): +def get_layers( + ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance +) -> list[ifcopenshell.entity_instance]: """Get the CAD layers that an element is part of An element may have portions or all of its geometry assigned to a @@ -685,10 +811,10 @@ def get_layers(ifc_file, element): """ layers = [] representations = [] - if getattr(element, "Representation", None): - representations = [element.Representation] - elif getattr(element, "RepresentationMaps", None): - representations = element.RepresentationMaps + if representation := getattr(element, "Representation", None): + representations = [representation] + elif representation_maps := getattr(element, "RepresentationMaps", None): + representations = representation_maps for representation in representations: for subelement in ifc_file.traverse(representation): if subelement.is_a("IfcShapeRepresentation"): @@ -701,7 +827,9 @@ def get_layers(ifc_file, element): return layers -def get_container(element, should_get_direct=False, ifc_class=None): +def get_container( + element: ifcopenshell.entity_instance, should_get_direct: bool = False, ifc_class: Optional[str] = None +) -> ifcopenshell.entity_instance: """ Retrieves the spatial structure container of an element. @@ -715,8 +843,9 @@ def get_container(element, should_get_direct=False, ifc_class=None): :type should_get_direct: bool :param ifc_class: Optionally filter the type of container you're after. For example, you may be after the storey, not a space. - :type ifc_class: str + :type ifc_class: str, optional :return: The direct or indirect container of the element or None. + :rtype: ifcopenshell.entity_instance.entity_instance Example: @@ -726,8 +855,10 @@ def get_container(element, should_get_direct=False, ifc_class=None): container = ifcopenshell.util.element.get_container(element) """ if should_get_direct: - if hasattr(element, "ContainedInStructure") and element.ContainedInStructure: - container = element.ContainedInStructure[0].RelatingStructure + if ( + contained_in_structure := getattr(element, "ContainedInStructure", None) + ) is not None and contained_in_structure: + container = contained_in_structure[0].RelatingStructure if not ifc_class: return container if container.is_a(ifc_class): @@ -739,8 +870,10 @@ def get_container(element, should_get_direct=False, ifc_class=None): nest = get_nest(element) if nest: return get_container(nest, should_get_direct) - if hasattr(element, "ContainedInStructure") and element.ContainedInStructure: - container = element.ContainedInStructure[0].RelatingStructure + if ( + contained_in_structure := getattr(element, "ContainedInStructure", None) + ) is not None and contained_in_structure: + container = contained_in_structure[0].RelatingStructure if not ifc_class: return container while container: @@ -749,7 +882,7 @@ def get_container(element, should_get_direct=False, ifc_class=None): container = get_aggregate(container) -def get_referenced_structures(element): +def get_referenced_structures(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: """Retreives a list of referenced spatial elements Typically useful for multistorey elements, such as columns or facade @@ -758,6 +891,8 @@ def get_referenced_structures(element): :param element: The IFC element :type element: ifcopenshell.entity_instance.entity_instance + :return: A list of IfcSpatialElement + :rtype: list[ifcopenshell.entity_instance.entity_instance] Example: @@ -766,12 +901,31 @@ def get_referenced_structures(element): element = file.by_type("IfcWall")[0] print(ifcopenshell.util.element.get_referenced_structures(element)) """ - if hasattr(element, "ReferencedInStructures"): - return [r.RelatingStructure for r in element.ReferencedInStructures] - return [] + return [r.RelatingStructure for r in getattr(element, "ReferencedInStructures", [])] -def get_decomposition(element, is_recursive=True): +def get_structure_referenced_elements(structure: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]: + """Retreives a set of elements referenced by a structure + + :param structure: IfcSpatialElement + :type element: ifcopenshell.entity_instance.entity_instance + :return: A set of referenced elements, IfcSpatialReferenceSelect + :rtype: set[ifcopenshell.entity_instance.entity_instance] + + Example: + + .. code:: python + + element = file.by_type("IfcBuildingStorey")[0] + print(ifcopenshell.util.element.get_structure_referenced_elements(element)) + """ + referenced = set() + for rel in structure.ReferencesElements: + referenced.update(rel.RelatedElements) + return referenced + + +def get_decomposition(element: ifcopenshell.entity_instance, is_recursive=True) -> list[ifcopenshell.entity_instance]: """ Retrieves all subelements of an element based on the spatial decomposition hierarchy. This includes all subspaces and elements contained in subspaces, @@ -794,30 +948,37 @@ def get_decomposition(element, is_recursive=True): while queue: element = queue.pop() for rel in getattr(element, "ContainsElements", []): - queue.extend(rel.RelatedElements) - results.extend(rel.RelatedElements) + related = rel.RelatedElements + queue.extend(related) + results.extend(related) for rel in getattr(element, "IsDecomposedBy", []): - queue.extend(rel.RelatedObjects) - results.extend(rel.RelatedObjects) + related = rel.RelatedObjects + queue.extend(related) + results.extend(related) for rel in getattr(element, "HasOpenings", []): - queue.append(rel.RelatedOpeningElement) - results.append(rel.RelatedOpeningElement) + related = rel.RelatedOpeningElement + queue.append(related) + results.append(related) for rel in getattr(element, "HasFillings", []): - queue.append(rel.RelatedBuildingElement) - results.append(rel.RelatedBuildingElement) + related = rel.RelatedBuildingElement + queue.append(related) + results.append(related) for rel in getattr(element, "IsNestedBy", []): - queue.extend(rel.RelatedObjects) - results.extend(rel.RelatedObjects) + related = rel.RelatedObjects + queue.extend(related) + results.extend(related) if not is_recursive: break return results -def get_grouped_by(element): +def get_grouped_by(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: """Retrieves all subelements of an element based on the group. :param element: The IFC element + :type element: ifcopenshell.entity_instance.entity_instance :return: All subelements of the group + :rtype: list[ifcopenshell.entity_instance.entity_instance] Example: @@ -831,17 +992,42 @@ def get_grouped_by(element): while queue: element = queue.pop() for rel in getattr(element, "IsGroupedBy", []): - queue.extend(rel.RelatedObjects) - results.extend(rel.RelatedObjects) + related_objects = rel.RelatedObjects + queue.extend(related_objects) + results.extend(related_objects) return results -def get_aggregate(element): +def get_groups(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: + """ + Retrieves the groups of an element. + + :param element: The IFC element + :return: List of IfcGroups element is assigned to. + :rtype: list[ifcopenshell.entity_instance.entity_instance] + + Example: + + .. code:: python + + wall = file.by_type("IfcWall")[0] + group = ifcopenshell.util.element.get_groups(element)[0] + """ + groups = [] + for rel in element.HasAssignments: + if rel.is_a("IfcRelAssignsToGroup"): + groups.append(rel.RelatingGroup) + return groups + + +def get_aggregate(element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: """ Retrieves the aggregate parent of an element. :param element: The IFC element + :type element: ifcopenshell.entity_instance.entity_instance :return: The aggregate of the element + :rtype: ifcopenshell.entity_instance.entity_instance Example: @@ -850,17 +1036,19 @@ def get_aggregate(element): element = file.by_type("IfcBeam")[0] aggregate = ifcopenshell.util.element.get_aggregate(element) """ - if hasattr(element, "Decomposes") and element.Decomposes: - if element.Decomposes[0].is_a("IfcRelAggregates"): # IFC2X3 - return element.Decomposes[0].RelatingObject + if decomposes := getattr(element, "Decomposes", None): + if decomposes[0].is_a("IfcRelAggregates"): # IFC2X3 + return decomposes[0].RelatingObject -def get_nest(element): +def get_nest(element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: """ Retrieves the nest parent of an element. :param element: The IFC element + :type element: ifcopenshell.entity_instance.entity_instance :return: The nested whole of the element + :rtype: ifcopenshell.entity_instance.entity_instance Example: @@ -869,20 +1057,22 @@ def get_nest(element): element = file.by_type("IfcBeam")[0] aggregate = ifcopenshell.util.element.get_nest(element) """ - if hasattr(element, "Nests"): - if element.Nests: - return element.Nests[0].RelatingObject - elif hasattr(element, "Decomposes") and element.Decomposes: # IFC2X3 - if element.Decomposes[0].is_a("IfcRelNests"): - return element.Decomposes[0].RelatingObject + if (nests := getattr(element, "Nests", None)) is not None: + if nests: + return nests[0].RelatingObject + elif (decomposes := getattr(element, "Decomposes", None)) is not None and decomposes: # IFC2X3 + if decomposes[0].is_a("IfcRelNests"): + return decomposes[0].RelatingObject -def get_parts(element): +def get_parts(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: """ Retrieves the parts of an element that have an aggregation relationship. :param element: The IFC element + :type element: ifcopenshell.entity_instance.entity_instance :return: The parts of the element + :rtype: list[ifcopenshell.entity_instance.entity_instance] Example: @@ -891,21 +1081,22 @@ def get_parts(element): element = file.by_type("IfcElementAssembly")[0] parts = ifcopenshell.util.element.get_parts(element) """ - if hasattr(element, "IsDecomposedBy") and element.IsDecomposedBy: - if element.IsDecomposedBy[0].is_a("IfcRelAggregates"): - return element.IsDecomposedBy[0].RelatedObjects + if (is_decomposed_by := getattr(element, "IsDecomposedBy", None)) is not None and is_decomposed_by: + if is_decomposed_by[0].is_a("IfcRelAggregates"): + return is_decomposed_by[0].RelatedObjects -def get_components(element, include_ports=False): +def get_components(element: ifcopenshell.entity_instance, include_ports=False) -> list[ifcopenshell.entity_instance]: """ Retrieves the components of an element that have an nest relationship. For nested ports, see ifcopenshell.util.system. :param element: The IFC element - :return: The components of the element :param include_ports: Default as False. Set to true if you also want to get ports. :type include_ports: bool,optional + :return: The components of the element + :rtype: list[ifcopenshell.entity_instance.entity_instance] Example: @@ -914,23 +1105,83 @@ def get_components(element, include_ports=False): element = file.by_type("IfcElementAssembly")[0] components = ifcopenshell.util.element.get_components(element) """ - if hasattr(element, "IsNestedBy"): - if element.IsNestedBy: + if (is_nested_by := getattr(element, "IsNestedBy", None)) is not None: + if is_nested_by: if include_ports: - return element.IsNestedBy[0].RelatedObjects - return [e for e in element.IsNestedBy[0].RelatedObjects if not e.is_a("IfcPort")] - elif hasattr(element, "IsDecomposedBy") and element.IsDecomposedBy: - if element.IsDecomposedBy[0].is_a("IfcRelNests"): - return element.IsDecomposedBy[0].RelatedObjects + return is_nested_by[0].RelatedObjects + return [e for e in is_nested_by[0].RelatedObjects if not e.is_a("IfcPort")] + elif (is_decomposed_by := getattr(element, "IsDecomposedBy", None)) is not None and is_decomposed_by: + if is_decomposed_by[0].is_a("IfcRelNests"): + return is_decomposed_by[0].RelatedObjects -def replace_attribute(element, old, new): - for i, attribute in enumerate(element): - if has_element_reference(attribute, old): - element[i] = element.walk(lambda v: v == old, lambda v: new, attribute) +ReferenceData = namedtuple("ReferenceData", "inverse_attribute, rel_class, relating_element_attribute") + +# References below are omitted because they do not introduce +# any additional referenced objects besides the objects +# from their supertype IfcExternalReference +# - IfcExternallyDefinedHatchStyle +# - IfcExternallyDefinedSurfaceStyle +# - IfcExternallyDefinedTextFont + +REFERENCE_TYPES: dict[str, ReferenceData] = { + "IfcClassificationReference": ReferenceData( + "ClassificationRefForObjects", + "IfcRelAssociatesClassification", + "RelatingClassification", + ), + "IfcDocumentReference": ReferenceData("DocumentRefForObjects", "IfcRelAssociatesDocument", "RelatingDocument"), + "IfcLibraryReference": ReferenceData("LibraryRefForObjects", "IfcRelAssociatesLibrary", "RelatingLibrary"), +} -def has_element_reference(value, element): +def get_referenced_elements(reference: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]: + """Get all elements with assigned `reference` + + :param reference: IfcExternalReference subtype reference + :type reference: ifcopenshell.entity_instance.entity_instance + :return: The elements with assigned `reference` + :rtype: set[ifcopenshell.entity_instance.entity_instance] + + Example: + + .. code:: python + + reference = file.by_type("IfcClassificationReference")[0] + elements = ifcopenshell.util.element.get_referenced_elements(reference) + """ + + related_objects: set[ifcopenshell.entity_instance] = set() + ifc_file = reference.file + ifc_class = reference.is_a() + + if ifc_file.schema == "IFC2X3": + reference_data = REFERENCE_TYPES.get(ifc_class) + if reference_data: + for rel in ifc_file.by_type(reference_data.rel_class): + if getattr(rel, reference_data.relating_element_attribute) == reference: + related_objects.update(rel.RelatedObjects) + + else: + # IfcExternalReference + for external_rel in reference.ExternalReferenceForResources: + related_objects.update(external_rel.RelatedResourceObjects) + + reference_data = REFERENCE_TYPES.get(ifc_class) + if reference_data: + for rel in getattr(reference, reference_data.inverse_attribute): + related_objects.update(rel.RelatedObjects) + + return related_objects + + +def replace_attribute(element: ifcopenshell.entity_instance, old: Any, new: Any) -> None: + for i, attribute_value in enumerate(element): + if has_element_reference(attribute_value, old): + element[i] = element.walk(lambda v: v == old, lambda v: new, attribute_value) + + +def has_element_reference(value: Any, element: ifcopenshell.entity_instance) -> bool: if isinstance(value, (tuple, list)): for v in value: if has_element_reference(v, element): @@ -939,7 +1190,7 @@ def has_element_reference(value, element): return value == element -def remove_deep(ifc_file, element): +def remove_deep(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> None: """Recursively purges a subgraph safely. Do not use, use remove_deep2() instead. @@ -954,7 +1205,7 @@ def remove_deep(ifc_file, element): ifc_file.unbatch() -def batch_remove_deep2(ifc_file): +def batch_remove_deep2(ifc_file: ifcopenshell.file) -> None: """Enable batch removal after running remove_deep2 using serialisation See #944 and #3226. Removing elements in an IFC graph is slow as a lot of @@ -990,7 +1241,7 @@ def batch_remove_deep2(ifc_file): ifc_file.to_delete = set() -def unbatch_remove_deep2(ifc_file): +def unbatch_remove_deep2(ifc_file: ifcopenshell.file) -> ifcopenshell.file: """Finish removing elements batched from remove_deep2 using string replacement See documentation for batch_remove_deep2. @@ -1020,7 +1271,12 @@ def unbatch_remove_deep2(ifc_file): return ifcopenshell.file.from_string("\n".join(result)) -def remove_deep2(ifc_file, element, also_consider=[], do_not_delete=[]): +def remove_deep2( + ifc_file: ifcopenshell.file, + element: ifcopenshell.entity_instance, + also_consider: list[ifcopenshell.entity_instance] = [], + do_not_delete: list[ifcopenshell.entity_instance] = [], +) -> None: """Recursively purges a subgraph safely, starting at an element This should always be used instead of remove_deep. See #1812. The start @@ -1047,6 +1303,10 @@ def remove_deep2(ifc_file, element, also_consider=[], do_not_delete=[]): :param ifc_file: The IFC file object :type ifc_file: ifcopenshell.file.file + :param also_consider: elements to also consider as a part of a subgraph + :type also_consider: list[ifcopenshell.entity_instance.entity_instance], optional + :param do_not_delete: elements to protect from deletion + :type do_not_delete: list[ifcopenshell.entity_instance.entity_instance], optional :param element: The starting element that defines the subgraph :type element: ifcopenshell.entity_instance.entity_instance """ @@ -1088,7 +1348,7 @@ def remove_deep2(ifc_file, element, also_consider=[], do_not_delete=[]): # ifc_file.unbatch() -def copy(ifc_file, element): +def copy(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: """ Copy a single element. Any referenced elements are not copied. @@ -1112,7 +1372,13 @@ def copy(ifc_file, element): return new -def copy_deep(ifc_file, element, exclude=None, exclude_callback=None, copied_entities=None): +def copy_deep( + ifc_file: ifcopenshell.file, + element: ifcopenshell.entity_instance, + exclude: Optional[list[str]] = None, + exclude_callback: Optional[Callable[[ifcopenshell.entity_instance], bool]] = None, + copied_entities: Optional[dict[int, ifcopenshell.entity_instance]] = None, +) -> ifcopenshell.entity_instance: """ Recursively copy an element and all of its directly related subelements. @@ -1132,7 +1398,7 @@ def copy_deep(ifc_file, element, exclude=None, exclude_callback=None, copied_ent :param copied_entities: A dictionary of IDs as keys and entities as values to reuse when coming across the same entity twice. This can typically be left as None. - :type copied_entities: dict[int:ifcopenshell.entity_instance.entity_instance] + :type copied_entities: dict[int:ifcopenshell.entity_instance.entity_instance], optional :return: The newly copied element :rtype: ifcopenshell.entity_instance.entity_instance """ diff --git a/src/ifcopenshell-python/ifcopenshell/util/placement.py b/src/ifcopenshell-python/ifcopenshell/util/placement.py index 1f40fcc497..f05959677d 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/placement.py +++ b/src/ifcopenshell-python/ifcopenshell/util/placement.py @@ -18,9 +18,12 @@ import numpy as np import ifcopenshell +from typing import Literal, Iterable + +MatrixType = np.ndarray[np.ndarray[float]] -def a2p(o, z, x): +def a2p(o: Iterable[float], z: Iterable[float], x: Iterable[float]) -> MatrixType: """Converts a location, X, and Z axis vector to a 4x4 transformation matrix IFC uses a right-handed coordinate system, so it is not necessary to @@ -33,7 +36,7 @@ def a2p(o, z, x): :param x: The +X vector / axis of the matrix :type x: iterable[float] :return: A 4x4 numpy matrix - :rtype: np.array[np.array[float]] + :rtype: np.ndarray[np.ndarray[float]] """ x = x / np.linalg.norm(x) z = z / np.linalg.norm(z) @@ -45,7 +48,7 @@ def a2p(o, z, x): return r.T -def get_axis2placement(placement: ifcopenshell.entity_instance) -> np.array: +def get_axis2placement(placement: ifcopenshell.entity_instance) -> MatrixType: """Parses an IfcAxis2Placement (2D or 3D) to a 4x4 transformation matrix Note that this function only parses a single placement axis. If you want to @@ -56,7 +59,7 @@ def get_axis2placement(placement: ifcopenshell.entity_instance) -> np.array: :param placement: The IfcLocalPlacement enitity :type placement: ifcopenshell.entity_instance.entity_instance :return: A 4x4 numpy matrix - :rtype: np.array[np.array[float]] + :rtype: np.ndarray[np.ndarray[float]] """ if placement.is_a("IfcAxis2Placement3D"): z = np.array(placement.Axis.DirectionRatios if placement.Axis else (0, 0, 1)) @@ -73,7 +76,7 @@ def get_axis2placement(placement: ifcopenshell.entity_instance) -> np.array: return a2p(o, z, x) -def get_local_placement(placement): +def get_local_placement(placement: ifcopenshell.entity_instance) -> MatrixType: """Parse a local placement into a 4x4 transformation matrix This is typically used to find the location and rotation of an element. The @@ -96,7 +99,7 @@ def get_local_placement(placement): :param placement: The IfcLocalPlacement entity :type placement: ifcopenshell.entity_instance.entity_instance :return: A 4x4 numpy matrix - :rtype: np.array[np.array[float]] + :rtype: np.ndarray[np.ndarray[float]] """ if placement is None: return np.eye(4) @@ -107,7 +110,7 @@ def get_local_placement(placement): return np.dot(parent, get_axis2placement(placement.RelativePlacement)) -def get_cartesiantransformationoperator3d(inst): +def get_cartesiantransformationoperator3d(inst: ifcopenshell.entity_instance) -> MatrixType: """Parses an IfcCartesianTransformationOperator into a 4x4 transformation matrix Note that in general you will not need to call this directly. See @@ -116,7 +119,7 @@ def get_cartesiantransformationoperator3d(inst): :param item: The IfcCartesianTransformationOperator entity :type item: ifcopenshell.entity_instance.entity_instance :return: A 4x4 numpy transformation matrix - :rtype: np.array[np.array[float]] + :rtype: np.ndarray[np.ndarray[float]] """ origin = np.array(inst.LocalOrigin.Coordinates) axis1 = np.array((1.0, 0.0, 0.0)) @@ -152,7 +155,7 @@ def get_cartesiantransformationoperator3d(inst): return m4 -def get_mappeditem_transformation(item): +def get_mappeditem_transformation(item: ifcopenshell.entity_instance) -> MatrixType: """Parse an IfcMappedItem into a 4x4 transformation matrix Mapped items take a representation with an origin and transform them with a @@ -162,7 +165,7 @@ def get_mappeditem_transformation(item): :param item: The IfcMappedItem entity :type item: ifcopenshell.entity_instance.entity_instance :return: A 4x4 numpy transformation matrix - :rtype: np.array[np.array[float]] + :rtype: np.ndarray[np.ndarray[float]] """ m4 = get_axis2placement(item.MappingSource.MappingOrigin) # TODO 2d @@ -170,7 +173,7 @@ def get_mappeditem_transformation(item): return get_cartesiantransformationoperator3d(item.MappingTarget) @ m4 -def get_storey_elevation(storey): +def get_storey_elevation(storey: ifcopenshell.entity_instance) -> float: """Get the Z elevation in project units of a buildling storey Building storeys store elevation in two possible locations: the Z value of @@ -187,7 +190,7 @@ def get_storey_elevation(storey): return getattr(storey, "Elevation", 0.0) or 0.0 -def rotation(angle, axis, is_degrees=True): +def rotation(angle: float, axis: Literal["X", "Y", "Z"], is_degrees=True) -> MatrixType: """Create a 4x4 numpy matrix representing an euler rotation :param angle: The angle of rotation @@ -198,7 +201,7 @@ def rotation(angle, axis, is_degrees=True): radians. Defaults to true (i.e. degrees). :type is_degrees: bool :return: A 4x4 numpy rotation matrix - :rtype: np.array[np.array[float]] + :rtype: np.ndarray[np.ndarray[float]] """ theta = np.radians(angle) if is_degrees else angle cos, sin = np.cos(theta), np.sin(theta) diff --git a/src/ifcopenshell-python/ifcopenshell/util/pset.py b/src/ifcopenshell-python/ifcopenshell/util/pset.py index 579127605c..4a8ce5c89d 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/pset.py +++ b/src/ifcopenshell-python/ifcopenshell/util/pset.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +from __future__ import annotations import re import pathlib import ifcopenshell @@ -25,10 +26,10 @@ from ifcopenshell.entity_instance import entity_instance from functools import lru_cache from typing import List, Generator, Optional -templates = {} +templates: dict[str, PsetQto] = {} -def get_template(schema): +def get_template(schema: str) -> PsetQto: global templates if schema not in templates: templates[schema] = PsetQto(schema) @@ -36,11 +37,13 @@ def get_template(schema): class PsetQto: + # fmt: off templates_path = { "IFC2X3": "Pset_IFC2X3.ifc", "IFC4": "Pset_IFC4_ADD2.ifc", "IFC4X3": "Pset_IFC4X3.ifc" } + # fmt: on def __init__(self, schema: str, templates=None) -> None: self.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema) diff --git a/src/ifcopenshell-python/ifcopenshell/util/representation.py b/src/ifcopenshell-python/ifcopenshell/util/representation.py index 68842d3ef3..95e91490d0 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/representation.py +++ b/src/ifcopenshell-python/ifcopenshell/util/representation.py @@ -18,9 +18,16 @@ import numpy as np import ifcopenshell +import ifcopenshell.util.placement +from typing import Optional, Union, TypedDict -def get_context(ifc_file, context, subcontext=None, target_view=None): +def get_context( + ifc_file: ifcopenshell.file, + context: str, + subcontext: Optional[str] = None, + target_view: Optional[str] = None, +) -> Union[ifcopenshell.entity_instance, None]: if subcontext or target_view: elements = ifc_file.by_type("IfcGeometricRepresentationSubContext") else: @@ -35,7 +42,12 @@ def get_context(ifc_file, context, subcontext=None, target_view=None): return element -def is_representation_of_context(representation, context, subcontext=None, target_view=None): +def is_representation_of_context( + representation: ifcopenshell.entity_instance, + context: Union[ifcopenshell.entity_instance, str], + subcontext: Optional[str] = None, + target_view: Optional[str] = None, +) -> bool: if isinstance(context, ifcopenshell.entity_instance): return representation.ContextOfItems == context @@ -52,11 +64,16 @@ def is_representation_of_context(representation, context, subcontext=None, targe and representation.ContextOfItems.ContextIdentifier == subcontext and representation.ContextOfItems.ContextType == context ) - elif representation.ContextOfItems.ContextType == context: - return True + + return representation.ContextOfItems.ContextType == context -def get_representation(element, context, subcontext=None, target_view=None): +def get_representation( + element: ifcopenshell.entity_instance, + context: Union[ifcopenshell.entity_instance, str], + subcontext: Optional[str] = None, + target_view: Optional[str] = None, +) -> Union[ifcopenshell.entity_instance, None]: if element.is_a("IfcProduct") and element.Representation: for r in element.Representation.Representations: if is_representation_of_context(r, context, subcontext, target_view): @@ -67,16 +84,30 @@ def get_representation(element, context, subcontext=None, target_view=None): return r.MappedRepresentation -def resolve_representation(representation): +def resolve_representation(representation: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: + """Resolve possibly mapped representation. + + :param representation: IfcRepresentation + :type representation: ifcopenshell.entity_instance.entity_instance + :return: Representation resolved from mappings + :rtype: ifcopenshell.entity_instance.entity_instance + """ if len(representation.Items) == 1 and representation.Items[0].is_a("IfcMappedItem"): return resolve_representation(representation.Items[0].MappingSource.MappedRepresentation) return representation -def resolve_items(representation, matrix=None): +class ResolvedItemDict(TypedDict): + matrix: np.array + item: ifcopenshell.entity_instance + + +def resolve_items( + representation: ifcopenshell.entity_instance, matrix: Optional[np.array] = None +) -> list[ResolvedItemDict]: if matrix is None: matrix = np.eye(4) - results = [] + results: list[ResolvedItemDict] = [] for item in representation.Items or []: # Be forgiving of invalid IFCs because Revit :( if item.is_a("IfcMappedItem"): rep_matrix = ifcopenshell.util.placement.get_mappeditem_transformation(item) @@ -84,5 +115,5 @@ def resolve_items(representation, matrix=None): rep_matrix = rep_matrix @ matrix.copy() results.extend(resolve_items(item.MappingSource.MappedRepresentation, rep_matrix)) else: - results.append({"matrix": matrix.copy(), "item": item}) + results.append(ResolvedItemDict(matrix=matrix.copy(), item=item)) return results diff --git a/src/ifcopenshell-python/ifcopenshell/util/schema.py b/src/ifcopenshell-python/ifcopenshell/util/schema.py index eb9da034a2..6bba052bf2 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/schema.py +++ b/src/ifcopenshell-python/ifcopenshell/util/schema.py @@ -21,6 +21,7 @@ import json import time import ifcopenshell import ifcopenshell.util.attribute +import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper # This is highly experimental and incomplete, however, it may work for simple datasets. # In this simple implementation, we only support 2X3<->4 right now @@ -28,7 +29,7 @@ import ifcopenshell.util.attribute cwd = os.path.dirname(os.path.realpath(__file__)) -def get_fallback_schema(version): +def get_fallback_schema(version: str) -> str: """fallback to the schema version we do have docs and mapping for, needed to support IFC versions like 4X3_RC1, 4X1 etc""" if version.startswith("IFC4X3"): @@ -38,7 +39,7 @@ def get_fallback_schema(version): return version -def is_a(entity, ifc_class): +def is_a(entity: ifcopenshell.entity_instance, ifc_class: str) -> bool: ifc_class = ifc_class.upper() if entity.name_uc() == ifc_class: return True @@ -59,7 +60,9 @@ def get_subtypes(entity): return get_classes(entity) -def reassign_class(ifc_file, element, new_class): +def reassign_class( + ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance, new_class: str +) -> ifcopenshell.entity_instance: """ Attempts to change the class (entity name) of `element` to `new_class` by removing element and recreating a similar instance of type `new_class` @@ -74,7 +77,7 @@ def reassign_class(ifc_file, element, new_class): It's unlikely that this affects real-world usage of this function. """ - schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(ifc_file.schema) + schema: ifcopenshell_wrapper.schema_definition = ifcopenshell_wrapper.schema_by_name(ifc_file.schema) try: declaration = schema.declaration_by_name(new_class) except: @@ -116,11 +119,11 @@ def reassign_class(ifc_file, element, new_class): class BatchReassignClass: - def __init__(self, file): + def __init__(self, file: ifcopenshell.file): self.file = file self.purge() - def reassign(self, element, new_class): + def reassign(self, element: ifcopenshell.entity_instance, new_class: str) -> ifcopenshell.entity_instance: try: new_element = self.file.create_entity(new_class) except: @@ -150,9 +153,12 @@ class BatchReassignClass: self.file.remove(element) self.purge() - def purge(self): - self.replacements = {} - self.to_delete = set() + def purge(self) -> None: + # mapping {inverse: {attribute_index: {old_element: new_element} } } + self.replacements: dict[ + ifcopenshell.entity_instance, dict[int, dict[ifcopenshell.entity_instance, ifcopenshell.entity_instance]] + ] = {} + self.to_delete: set[ifcopenshell.entity_instance] = set() class Migrator: @@ -217,7 +223,7 @@ class Migrator: "User": None, } - def migrate(self, element, new_file): + def migrate(self, element: ifcopenshell.entity_instance, new_file: ifcopenshell.file) -> ifcopenshell.entity_instance: if element.id() == 0: return new_file.create_entity(element.is_a(), element.wrappedValue) try: diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index d252c756bb..ee94fbc310 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -18,6 +18,8 @@ import re import lark +import numpy as np +import ifcopenshell.api import ifcopenshell.util import ifcopenshell.util.fm import ifcopenshell.util.unit @@ -25,7 +27,10 @@ import ifcopenshell.util.element import ifcopenshell.util.placement import ifcopenshell.util.geolocation import ifcopenshell.util.classification +import ifcopenshell.util.schema +import ifcopenshell.util.shape from decimal import Decimal +from typing import Optional, Any, Union filter_elements_grammar = lark.Lark( @@ -124,9 +129,10 @@ get_element_grammar = lark.Lark( format_grammar = lark.Lark( """start: function - function: round | format_length | lower | upper | title | concat | substr | ESCAPED_STRING | NUMBER + function: round | number | format_length | lower | upper | title | concat | substr | ESCAPED_STRING | NUMBER round: "round(" function "," NUMBER ")" + number: "number(" function ["," ESCAPED_STRING ["," ESCAPED_STRING]] ")" format_length: metric_length | imperial_length metric_length: "metric_length(" function "," NUMBER "," NUMBER ")" imperial_length: "imperial_length(" function "," NUMBER ["," ESCAPED_STRING "," ESCAPED_STRING] ")" @@ -207,6 +213,15 @@ class FormatTransformer(lark.Transformer): return str(int(result)) return str(result) + def number(self, args): + if isinstance(args[0], str): + args[0] = float(args[0]) if "." in args[0] else int(args[0]) + if len(args) >= 3 and args[2]: + return "{:,}".format(args[0]).replace(".", "*").replace(",", args[2]).replace("*", args[1]) + elif len(args) >= 2 and args[1]: + return "{}".format(args[0]).replace(".", args[1]) + return "{:,}".format(args[0]) + def format_length(self, args): return args[0] @@ -253,25 +268,68 @@ class GetElementTransformer(lark.Transformer): return args[1:-1].replace("\\", "") -def format(query): +def format(query: str) -> str: return FormatTransformer().transform(format_grammar.parse(query)) -def get_element_value(element, query): - keys = GetElementTransformer().transform(get_element_grammar.parse(query)) +def get_element_value(element: ifcopenshell.entity_instance, query: str) -> Any: + keys: list[str] = GetElementTransformer().transform(get_element_grammar.parse(query)) return Selector.get_element_value(element, keys) -def filter_elements(ifc_file, query, elements=None, edit_in_place=False): +def filter_elements( + ifc_file: ifcopenshell.file, + query: str, + elements: Optional[set[ifcopenshell.entity_instance]] = None, + edit_in_place=False, +) -> set[ifcopenshell.entity_instance]: + """ + Filter elements based on the provided `query`. + + :param ifc_file: The IFC file object + :type ifc_file: ifcopenshell.file.file + :param query: Query to execute + :type query: str + :param elements: Base set of IFC elements for the query. + If provided, new elements found for the current query will be added to `elements`. + Elements explicitly excluded in the `query` will also be excluded from `elements` + :type elements: set[ifcopenshell.entity_instance.entity_instance], optional + :param edit_in_place: If `True`, mutate the provided `elements` in place. Defaults to `False` + :type edit_in_place: bool + :return: Set of filtered elements + :rtype: set[ifcopenshell.entity_instance.entity_instance] + + Example: + + .. code:: python + + # Select all walls in the file. + elements = ifcopenshell.util.selector.filter_elements(ifc_file, "IfcWall, IfcSlab") + + # Add doors to the elements too. + elements = ifcopenshell.util.selector.filter_elements(ifc_file, "IfcDoor", elements) + + # Changed our mind, exclude the slabs. + elements = ifcopenshell.util.selector.filter_elements(ifc_file, "! IfcSlab", elements) + + # {#1=IfcWall(...), #2=IfcDoor(...)} + print(elements) + """ + if not query: + return elements or set() if elements and not edit_in_place: elements = elements.copy() transformer = FacetTransformer(ifc_file, elements) transformer.transform(filter_elements_grammar.parse(query)) return transformer.get_results() - return transformer.elements -def set_element_value(ifc_file, element, query, value): +def set_element_value( + ifc_file: ifcopenshell.file, + element: ifcopenshell.entity_instance, + query: Union[str, list[str]], + value: Optional[str], +) -> Union[ifcopenshell.entity_instance, None]: if isinstance(query, (list, tuple)): keys = query else: @@ -313,14 +371,57 @@ def set_element_value(ifc_file, element, query, value): elif key == "classification": element = ifcopenshell.util.classification.get_references(element) elif key in ("x", "y", "z", "easting", "northing", "elevation") and hasattr(element, "ObjectPlacement"): - return + # TODO: add support + if key in ("easting", "northing", "elevation"): + return + + placement = element.ObjectPlacement + if placement is None: + matrix = np.eye(4) + else: + matrix = ifcopenshell.util.placement.get_local_placement(placement) + + # check if value is within tolerance to avoid api calls + coord_i = "xyz".index(key) + prev_value = matrix[coord_i][3] + new_value = float(value) if value else 0.0 + if ifcopenshell.util.shape.is_x(new_value, prev_value): + return + + matrix[coord_i][3] = new_value + ifcopenshell.api.run( + "geometry.edit_object_placement", ifc_file, product=element, matrix=matrix, is_si=False + ) elif isinstance(element, ifcopenshell.entity_instance): if key == "Name" and element.is_a("IfcMaterialLayerSet"): key = "LayerSetName" # This oddity in the IFC spec is annoying so we account for it. if isinstance(key, str) and hasattr(element, key): if getattr(element, key) != value: - return setattr(element, key, value) + try: + # Try our luck + return setattr(element, key, value) + except: + # Try to cast + data_type = ifcopenshell.util.attribute.get_primitive_type( + element.wrapped_data.declaration() + .as_entity() + .attribute_by_index(element.wrapped_data.get_argument_index(key)) + ) + if data_type == "string": + value = str(value) + elif data_type == "float": + value = float(value) + elif data_type == "integer": + value = int(value) + elif data_type == "boolean": + if value in ("True", "true", "TRUE", "Yes", "1"): + value = True + elif value in ("False", "false", "FALSE", "No", "0"): + value = True + else: + value = bool(value) + return setattr(element, key, value) else: # Try to extract pset if isinstance(key, re.Pattern): @@ -376,10 +477,10 @@ def set_element_value(ifc_file, element, query, value): class FacetTransformer(lark.Transformer): - def __init__(self, ifc_file, elements): + def __init__(self, ifc_file: ifcopenshell.file, elements: Optional[set[ifcopenshell.entity_instance]] = None): self.file = ifc_file self.results = [] - self.elements = elements or set() + self.elements = set() if elements is None else elements self.container_parents = {} self.container_trees = {} @@ -607,31 +708,37 @@ class FacetTransformer(lark.Transformer): if isinstance(element_value, (list, tuple)): return any(self.compare(ev, comparison, value) for ev in element_value) elif isinstance(value, str): - if isinstance(element_value, int): - value = int(value) - elif isinstance(element_value, float): - value = float(value) + try: + if isinstance(element_value, int): + value = int(value) + elif isinstance(element_value, float): + value = float(value) - if isinstance(element_value, (int, float)): - operator = comparison.lstrip("!") - if operator == ">=": - result = element_value >= value - elif operator == "<=": - result = element_value <= value - elif operator == ">": - result = element_value > value - elif operator == "<": - result = element_value < value - else: - result = element_value == value # Tolerance? - elif isinstance(element_value, str): - operator = comparison.lstrip("!") - if operator == "*=": - result = value in element_value + if isinstance(element_value, (int, float)): + operator = comparison.lstrip("!") + if operator == ">=": + result = element_value >= value + elif operator == "<=": + result = element_value <= value + elif operator == ">": + result = element_value > value + elif operator == "<": + result = element_value < value + else: + result = element_value == value # Tolerance? + elif isinstance(element_value, str): + operator = comparison.lstrip("!") + if operator == "*=": + result = value in element_value + else: + result = element_value == value else: result = element_value == value - else: - result = element_value == value + except: + # Potentially they are trying to compare a value which cannot + # be legally casted to the element_value, or cannot use the + # `in` or more / less than comparison operators. + result = False elif isinstance(value, re.Pattern): result = bool(value.match(element_value)) if element_value is not None else False elif value in (None, True, False): @@ -644,7 +751,9 @@ class FacetTransformer(lark.Transformer): class Selector: @classmethod - def parse(cls, ifc_file, query, elements=None): + def parse( + cls, ifc_file: ifcopenshell.file, query: str, elements: Optional[list[ifcopenshell.entity_instance]] = None + ) -> list[ifcopenshell.entity_instance]: cls.file = ifc_file cls.elements = elements l = lark.Lark( @@ -847,7 +956,7 @@ class Selector: return {"keys": keys, "is_regex": is_regex} @classmethod - def get_element_value(cls, element, keys): + def get_element_value(cls, element: ifcopenshell.entity_instance, keys: list[str]) -> Any: value = element for key in keys: if value is None: diff --git a/src/ifcopenshell-python/ifcopenshell/util/sequence.py b/src/ifcopenshell-python/ifcopenshell/util/sequence.py index f450dbf5ed..720b81c151 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/sequence.py +++ b/src/ifcopenshell-python/ifcopenshell/util/sequence.py @@ -18,7 +18,6 @@ import datetime import ifcopenshell.util.date -from ifcopenshell.util.data import WorkTimeDatesInterface from math import floor from functools import lru_cache from collections import namedtuple @@ -182,15 +181,14 @@ def is_day_in_work_time(day, work_time): is_day_in_work_time = True if isinstance(day, datetime.datetime): day = datetime.date(day.year, day.month, day.day) - work_time_dates = WorkTimeDatesInterface(work_time) - if work_time_dates.Start: - start = ifcopenshell.util.date.ifc2datetime(work_time_dates.Start) + if work_time[4]: + start = ifcopenshell.util.date.ifc2datetime(work_time[4]) if day > start: is_day_in_work_time = True else: is_day_in_work_time = False - if work_time_dates.Finish: - finish = ifcopenshell.util.date.ifc2datetime(work_time_dates.Finish) + if work_time[5]: + finish = ifcopenshell.util.date.ifc2datetime(work_time[5]) if day < finish: is_day_in_work_time = True else: @@ -207,17 +205,16 @@ def is_work_time_applicable_to_day(work_time, day): if isinstance(day, datetime.datetime): day = datetime.date(day.year, day.month, day.day) recurrence = work_time.RecurrencePattern - work_time_dates = WorkTimeDatesInterface(work_time) if recurrence.RecurrenceType == "DAILY": if not recurrence.Interval and not recurrence.Occurrences: return True - if not work_time_dates.Start: + if not work_time[4]: return False return False # TODO elif recurrence.RecurrenceType == "WEEKLY": if not recurrence.Interval and not recurrence.Occurrences: return (day.weekday() + 1) in recurrence.WeekdayComponent - if not work_time_dates.Start: + if not work_time[4]: return False return False # TODO elif recurrence.RecurrenceType == "MONTHLY_BY_DAY_OF_MONTH": diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape.py b/src/ifcopenshell-python/ifcopenshell/util/shape.py index b50b762498..798000005a 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape.py @@ -17,15 +17,19 @@ # along with IfcOpenShell. If not, see . import shapely +import shapely.ops import numpy as np import ifcopenshell.util.element import ifcopenshell.util.placement import ifcopenshell.util.representation +from typing import Optional, Literal, Union, Iterable tol = 1e-6 +AXIS_LITERAL = Literal["X", "Y", "Z"] +VECTOR_3D = tuple[float, float, float] -def is_x(value, x, tolerance=None): +def is_x(value: float, x: float, tolerance: Optional[float] = None) -> bool: """Checks whether a value is equivalent to X given a tolerance :param value: Input value @@ -42,7 +46,7 @@ def is_x(value, x, tolerance=None): return abs(x - value) < tolerance -def get_volume(geometry): +def get_volume(geometry) -> float: """Calculates the total internal volume of a geometry Volumes of non-manifold geometry will be unpredictable. @@ -73,7 +77,7 @@ def get_volume(geometry): return abs(sum(volumes)) -def get_x(geometry): +def get_x(geometry) -> float: """Calculates the X length of the geometry :param geometry: Geometry output calculated by IfcOpenShell @@ -85,7 +89,7 @@ def get_x(geometry): return max(x_values) - min(x_values) -def get_y(geometry): +def get_y(geometry) -> float: """Calculates the Y length of the geometry :param geometry: Geometry output calculated by IfcOpenShell @@ -97,7 +101,7 @@ def get_y(geometry): return max(y_values) - min(y_values) -def get_z(geometry): +def get_z(geometry) -> float: """Calculates the Z length of the geometry :param geometry: Geometry output calculated by IfcOpenShell @@ -109,7 +113,7 @@ def get_z(geometry): return max(z_values) - min(z_values) -def get_shape_matrix(shape): +def get_shape_matrix(shape) -> np.ndarray: """Formats the transformation matrix of a shape as a 4x4 numpy array :param shape: Shape output calculated by IfcOpenShell @@ -121,7 +125,7 @@ def get_shape_matrix(shape): return np.array(([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1])) -def get_bbox_centroid(geometry): +def get_bbox_centroid(geometry) -> tuple[float]: """Calculates the bounding box centroid of the geometry The centroid is in local coordinates relative to the object's placement. @@ -143,7 +147,7 @@ def get_bbox_centroid(geometry): return (minx + ((maxx - minx) / 2), miny + ((maxy - miny) / 2), minz + ((maxz - minz) / 2)) -def get_element_bbox_centroid(element, geometry): +def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry) -> tuple[float]: """Calculates the element's bounding box centroid The centroid is in global coordinates. Note that if you have the shape, it @@ -163,7 +167,7 @@ def get_element_bbox_centroid(element, geometry): return (mat @ np.array([*centroid, 1.0]))[0:3] -def get_shape_bbox_centroid(shape, geometry): +def get_shape_bbox_centroid(shape, geometry) -> tuple[float]: """Calculates the shape's bounding box centroid The centroid is in global coordinates. Note that if you do not have the @@ -180,7 +184,7 @@ def get_shape_bbox_centroid(shape, geometry): return (get_shape_matrix(shape) @ np.array([*centroid, 1.0]))[0:3] -def get_vertices(geometry): +def get_vertices(geometry) -> np.ndarray[np.ndarray[float]]: """Get all the vertices as a numpy array Vertices are in local coordinates. @@ -196,7 +200,7 @@ def get_vertices(geometry): return np.array([np.array([verts[i], verts[i + 1], verts[i + 2]]) for i in range(0, len(verts), 3)]) -def get_edges(geometry): +def get_edges(geometry) -> np.ndarray[np.ndarray[int]]: """Get all the edges as a numpy array Results are a nested numpy array e.g. [[e1v1, e1v2], [e2v1, e2v2], ...] @@ -214,7 +218,7 @@ def get_edges(geometry): return [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)] -def get_faces(geometry): +def get_faces(geometry) -> np.ndarray[np.ndarray[int]]: """Get all the faces as a numpy array Faces are always triangulated. If the shape is a BRep and you want to get @@ -231,7 +235,7 @@ def get_faces(geometry): return [[faces[i], faces[i + 1], faces[i + 2]] for i in range(0, len(faces), 3)] -def get_shape_vertices(shape, geometry): +def get_shape_vertices(shape, geometry) -> np.ndarray[np.ndarray[float]]: """Get the shape's vertices as a numpy array Vertices are in global coordinates. If you do not have the shape, you can @@ -251,7 +255,7 @@ def get_shape_vertices(shape, geometry): return np.delete((mat @ np.hstack((verts, np.ones((len(verts), 1)))).T).T, -1, axis=1) -def get_element_vertices(element, geometry): +def get_element_vertices(element: ifcopenshell.entity_instance, geometry) -> np.ndarray[np.ndarray[float]]: """Get the element's vertices as a numpy array Vertices are in global coordinates. Note that if you have the shape, it is @@ -273,7 +277,7 @@ def get_element_vertices(element, geometry): return np.delete((mat @ np.hstack((verts, np.ones((len(verts), 1)))).T).T, -1, axis=1) -def get_bottom_elevation(geometry): +def get_bottom_elevation(geometry) -> float: """Gets the lowest local Z ordinate of the geometry :param geometry: Geometry output calculated by IfcOpenShell @@ -285,7 +289,7 @@ def get_bottom_elevation(geometry): return min(z_values) -def get_top_elevation(geometry): +def get_top_elevation(geometry) -> float: """Gets the highest local Z ordinate of the geometry :param geometry: Geometry output calculated by IfcOpenShell @@ -297,7 +301,7 @@ def get_top_elevation(geometry): return max(z_values) -def get_shape_bottom_elevation(shape, geometry): +def get_shape_bottom_elevation(shape, geometry) -> float: """Gets the lowest global Z ordinate of the shape If you do not have the shape, you can use ``get_element_bottom_elevation`` @@ -313,7 +317,7 @@ def get_shape_bottom_elevation(shape, geometry): return min([v[2] for v in get_shape_vertices(shape, geometry)]) -def get_shape_top_elevation(shape, geometry): +def get_shape_top_elevation(shape, geometry) -> float: """Gets the highest global Z ordinate of the shape If you do not have the shape, you can use ``get_element_top_elevation`` @@ -329,7 +333,7 @@ def get_shape_top_elevation(shape, geometry): return max([v[2] for v in get_shape_vertices(shape, geometry)]) -def get_element_bottom_elevation(element, geometry): +def get_element_bottom_elevation(element: ifcopenshell.entity_instance, geometry) -> float: """Gets the lowest global Z ordinate of the element Note that if you have the shape, it is more efficient to use @@ -345,7 +349,7 @@ def get_element_bottom_elevation(element, geometry): return min([v[2] for v in get_element_vertices(element, geometry)]) -def get_element_top_elevation(element, geometry): +def get_element_top_elevation(element: ifcopenshell.entity_instance, geometry) -> float: """Gets the highest global Z ordinate of the element Note that if you have the shape, it is more efficient to use @@ -361,7 +365,7 @@ def get_element_top_elevation(element, geometry): return max([v[2] for v in get_element_vertices(element, geometry)]) -def get_bbox(vertices): +def get_bbox(vertices: Iterable[VECTOR_3D]) -> tuple[np.ndarray[float]]: """Gets the bounding box of vertices :param vertices: An iterable of vertices @@ -384,7 +388,7 @@ def get_bbox(vertices): return (np.array([minx, miny, minz]), np.array([maxx, maxy, maxz])) -def get_area_vf(vertices, faces): +def get_area_vf(vertices: np.ndarray[VECTOR_3D], faces: np.ndarray[Iterable[int]]) -> float: """Calculates the surface area given a list of vertices and triangulated faces :param vertices: A list of 3D vertices, such as returned from get_vertices. @@ -408,7 +412,7 @@ def get_area_vf(vertices, faces): return mesh_area -def get_area(geometry): +def get_area(geometry) -> float: """Calculates the surface area of the geometry :param geometry: Geometry output calculated by IfcOpenShell @@ -423,7 +427,11 @@ def get_area(geometry): return get_area_vf(vertices, faces) -def get_side_area(geometry, axis="Y", direction=None): +def get_side_area( + geometry, + axis: AXIS_LITERAL = "Y", + direction: Optional[VECTOR_3D] = None, +) -> float: """Calculates the total surface area of surfaces that are visible from the specified axis This is typically useful for calculating elevational areas. For example, @@ -471,7 +479,11 @@ def get_side_area(geometry, axis="Y", direction=None): return get_area_vf(vertices, filtered_faces) -def get_footprint_area(geometry, axis="Z", direction=None): +def get_footprint_area( + geometry, + axis: AXIS_LITERAL = "Z", + direction: Optional[VECTOR_3D] = None, +) -> float: """Calculates the total footprint (i.e. projected) surface area visible from along an axis This is typically useful for calculating footprint areas. For example, you @@ -551,7 +563,7 @@ def get_footprint_area(geometry, axis="Z", direction=None): return unioned_polygon.area -def get_outer_surface_area(geometry): +def get_outer_surface_area(geometry) -> float: """Calculates the outer surface area (i.e. all sides except for top and bottom) This is typically useful for calculating painted areas of beams which @@ -581,7 +593,7 @@ def get_outer_surface_area(geometry): return get_area_vf(vertices, filtered_faces) -def get_footprint_perimeter(geometry): +def get_footprint_perimeter(geometry) -> float: """Calculates the footprint perimeter of the geometry All faces with a negative Z normal are considered and the distance of all @@ -630,7 +642,7 @@ def get_footprint_perimeter(geometry): return sum([np.linalg.norm(vertices[e[0]] - vertices[e[1]]) for e in (all_edges - shared_edges)]) -def get_profiles(element): +def get_profiles(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: """Gets all 2D profiles used in the definition of a parametric shape Profiles may be retrieved either from material profile sets or from swept @@ -647,7 +659,7 @@ def get_profiles(element): return [e.SweptArea for e in get_extrusions(element)] -def get_extrusions(element): +def get_extrusions(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: """Gets all extruded area solids used to define an element's model body geometry :param element: The element occurrence diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py index 1e2d395ba2..dd1104c780 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py @@ -751,6 +751,8 @@ class ShapeBuilder: representation_type = "AdvancedSweptSolid" elif "IfcExtrudedAreaSolid" in item_types: representation_type = "SweptSolid" + elif "IfcCsgSolid" in item_types: + representation_type = "CSG" elif items[0].is_a("IfcTessellatedItem"): representation_type = "Tessellation" elif items[0].is_a("IfcCurve") and items[0].Dim == 3: diff --git a/src/ifcopenshell-python/ifcopenshell/util/system.py b/src/ifcopenshell-python/ifcopenshell/util/system.py index 0c8789a098..3a5be9693f 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/system.py +++ b/src/ifcopenshell-python/ifcopenshell/util/system.py @@ -17,9 +17,10 @@ # along with IfcOpenShell. If not, see . -import ifcopenshell.util +import ifcopenshell.util.system +from typing import Optional, Union, Literal -group_types = { +group_types: dict[str, tuple[str, ...]] = { "IfcZone": ("IfcZone", "IfcSpace", "IfcSpatialZone"), "IfcBuiltSystem": ( "IfcBuiltElement", @@ -39,22 +40,24 @@ group_types = { "IfcGroup": ("IfcObjectDefinition",), } +FLOW_DIRECTION = Literal["SINK", "SOURCE", "SOURCEANDSINK", "NOTEDEFINED"] -def is_assignable(product, system) -> bool: + +def is_assignable(product: ifcopenshell.entity_instance, system: ifcopenshell.entity_instance) -> bool: for assignable in group_types.get(system.is_a(), ()): if product.is_a(assignable): return True return False -def get_system_elements(system): +def get_system_elements(system: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: results = [] for rel in system.IsGroupedBy: results.extend(rel.RelatedObjects) return results -def get_element_systems(element): +def get_element_systems(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: results = [] for rel in element.HasAssignments: if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.is_a() in ( @@ -67,7 +70,9 @@ def get_element_systems(element): return results -def get_ports(element, flow_direction=None): +def get_ports( + element: ifcopenshell.entity_instance, flow_direction: Optional[FLOW_DIRECTION] = None +) -> list[ifcopenshell.entity_instance]: results = [] for rel in getattr(element, "IsNestedBy", []) or []: for port in rel.RelatedObjects: @@ -85,14 +90,14 @@ def get_ports(element, flow_direction=None): return results -def get_connected_port(port): +def get_connected_port(port: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: for rel in port.ConnectedTo: return rel.RelatedPort for rel in port.ConnectedFrom: return rel.RelatingPort -def get_port_element(port): +def get_port_element(port: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: if hasattr(port, "Nests"): for rel in port.Nests: return rel.RelatingObject @@ -102,7 +107,9 @@ def get_port_element(port): return rel.RelatedElement -def get_connected_to(element, flow_direction=None): +def get_connected_to( + element: ifcopenshell.entity_instance, flow_direction: Optional[FLOW_DIRECTION] = None +) -> list[ifcopenshell.entity_instance]: results = [] for port in ifcopenshell.util.system.get_ports(element, flow_direction=flow_direction): for rel in port.ConnectedTo: @@ -115,7 +122,9 @@ def get_connected_to(element, flow_direction=None): return results -def get_connected_from(element, flow_direction=None): +def get_connected_from( + element: ifcopenshell.entity_instance, flow_direction: Optional[FLOW_DIRECTION] = None +) -> list[ifcopenshell.entity_instance]: results = [] for port in ifcopenshell.util.system.get_ports(element, flow_direction=flow_direction): for rel in port.ConnectedFrom: diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py index 80cbf7d6af..a218a9504f 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/unit.py +++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py @@ -18,7 +18,7 @@ from fractions import Fraction from math import pi -from typing import Tuple, Iterable, Any +from typing import Iterable, Any, Union, Literal, Optional import ifcopenshell import ifcopenshell.api @@ -525,7 +525,7 @@ def convert_unit(value, from_unit, to_unit): ) -def convert(value, from_prefix, from_unit, to_prefix, to_unit): +def convert(value: float, from_prefix: Optional[str], from_unit: str, to_prefix: Optional[str], to_unit: str) -> float: """Converts between length, area, and volume units In this case, you manually specify the names and (optionally) prefixes to @@ -534,12 +534,12 @@ def convert(value, from_prefix, from_unit, to_prefix, to_unit): :param value: The numeric value you want to convert :type value: float - :param from_prefix: A prefix from IfcSIPrefix. Can be None. - :type from_prefix: str,optional + :param from_prefix: A prefix from IfcSIPrefix. Can be None + :type from_prefix: str, optional :param from_unit: IfcSIUnitName or IfcConversionBasedUnit.Name :type from_unit: str - :param to_prefix: A prefix from IfcSIPrefix. Can be None. - :type to_prefix: str,optional + :param to_prefix: A prefix from IfcSIPrefix. Can be None + :type to_prefix: str, optional :param to_unit: IfcSIUnitName or IfcConversionBasedUnit.Name :type to_unit: str :return: The converted value. @@ -566,8 +566,8 @@ def convert(value, from_prefix, from_unit, to_prefix, to_unit): return value -def calculate_unit_scale(ifc_file): - """Returns a unit scale factor to convert to and from IFC project length units and SI meters +def calculate_unit_scale(ifc_file: ifcopenshell.file, unit_type: str = "LENGTHUNIT") -> float: + """Returns a unit scale factor to convert to and from IFC project units and SI units. Example: @@ -578,6 +578,8 @@ def calculate_unit_scale(ifc_file): :param ifc_file: The IFC file. :type ifc_file: ifcopenshell.file.file + :param unit_type: The type of SI unit, defaults to "LENGTHUNIT" + :type unit_type: str :returns: The scale factor :rtype: float """ @@ -586,7 +588,7 @@ def calculate_unit_scale(ifc_file): units = ifc_file.by_type("IfcUnitAssignment")[0] unit_scale = 1 for unit in units.Units: - if not hasattr(unit, "UnitType") or unit.UnitType != "LENGTHUNIT": + if not hasattr(unit, "UnitType") or unit.UnitType != unit_type: continue while unit.is_a("IfcConversionBasedUnit"): unit_scale *= unit.ConversionFactor.ValueComponent.wrappedValue @@ -597,14 +599,14 @@ def calculate_unit_scale(ifc_file): def format_length( - value, - precision, - decimal_places=2, - suppress_zero_inches=True, - unit_system="imperial", - input_unit="foot", - output_unit="foot", -): + value: float, + precision: float, + decimal_places: int = 2, + suppress_zero_inches=True, + unit_system: Literal["metric", "imperial"] = "imperial", + input_unit="foot", + output_unit="foot", +) -> str: """Formats a length for readability and imperial formatting :param value: The value in meters if metric, or either decimal feet or @@ -666,8 +668,9 @@ def format_length( def is_attr_type( - content_type: ifcopenshell.ifcopenshell_wrapper.named_type | ifcopenshell.ifcopenshell_wrapper.type_declaration, - ifc_unit_type_name: str) -> ifcopenshell.ifcopenshell_wrapper.type_declaration | None: + content_type: Union[ifcopenshell.ifcopenshell_wrapper.named_type, ifcopenshell.ifcopenshell_wrapper.type_declaration], + ifc_unit_type_name: str, +) -> Union[ifcopenshell.ifcopenshell_wrapper.type_declaration, None]: cur_decl = content_type while hasattr(cur_decl, "declared_type") is True: cur_decl = cur_decl.declared_type() @@ -691,8 +694,9 @@ def is_attr_type( return None -def iter_element_and_attributes_per_type(ifc_file: ifcopenshell.file, attr_type_name: str) -> Iterable[ - Tuple[ifcopenshell.entity_instance, ifcopenshell.ifcopenshell_wrapper.attribute, Any, str]]: +def iter_element_and_attributes_per_type( + ifc_file: ifcopenshell.file, attr_type_name: str +) -> Iterable[tuple[ifcopenshell.entity_instance, ifcopenshell.ifcopenshell_wrapper.attribute, Any, str]]: schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(ifc_file.schema) for element in ifc_file: diff --git a/src/ifcopenshell-python/ifcopenshell/validate.py b/src/ifcopenshell-python/ifcopenshell/validate.py index debe8e9d09..2b7b8af5ea 100644 --- a/src/ifcopenshell-python/ifcopenshell/validate.py +++ b/src/ifcopenshell-python/ifcopenshell/validate.py @@ -16,7 +16,21 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . -"""Data validation module""" +"""Data validation module + + +Can be used to run validation on IFC file from the command line: + +.. code-block:: bash + + python -m ifcopenshell.validate /path/to/model.ifc --rules + +Available flags: + +- ``--rules``: Also check express rules. +- ``--json``: Produce JSON output. +- ``--fields``: Output more detailed information about failed entities (available only with ``--json``). +""" from __future__ import print_function @@ -26,8 +40,11 @@ import json import functools from collections import namedtuple +from typing import Union, Iterator, Any, Optional +from logging import Logger import ifcopenshell +import ifcopenshell.ifcopenshell_wrapper import ifcopenshell.express.rule_executor named_type = ifcopenshell.ifcopenshell_wrapper.named_type @@ -38,6 +55,10 @@ enumeration_type = ifcopenshell.ifcopenshell_wrapper.enumeration_type entity_type = ifcopenshell.ifcopenshell_wrapper.entity select_type = ifcopenshell.ifcopenshell_wrapper.select_type attribute = ifcopenshell.ifcopenshell_wrapper.attribute +inverse_attribute = ifcopenshell.ifcopenshell_wrapper.inverse_attribute +schema_definition = ifcopenshell.ifcopenshell_wrapper.schema_definition + +attribute_types = Union[simple_type, named_type, enumeration_type, select_type, aggregation_type, type_declaration] class ValidationError(Exception): @@ -76,8 +97,8 @@ simple_type_python_mapping = { } -def annotate_inst_attr_pos(inst, pos): - def get_pos(): +def annotate_inst_attr_pos(inst: ifcopenshell.entity_instance, pos: int) -> str: + def get_pos() -> Iterator[int]: depth = 0 idx = -1 for c in str(inst): @@ -104,23 +125,24 @@ def annotate_inst_attr_pos(inst, pos): return "".join(" ^"[i == pos] for i in get_pos()) -def format(val): +def format(val: Any) -> str: if isinstance(val, tuple) and val and isinstance(val[0], ifcopenshell.entity_instance): return "[\n%s\n ]" % "\n".join(" {}. {}".format(*x) for x in enumerate(val, start=1)) else: return repr(val) -def assert_valid_inverse(attr, val, schema): +def assert_valid_inverse( + attr: inverse_attribute, val: tuple[ifcopenshell.entity_instance], schema: schema_definition +) -> bool: b1, b2 = attr.bound1(), attr.bound2() - + if (b1, b2) == (-1, -1): invalid = len(val) != 1 else: invalid = len(val) < b1 or (b2 != -1 and len(val) > b2) - - if invalid: + if invalid: ent_ref = attr.entity_reference().name() attr_ref = attr.attribute_reference().name() aggr = attr.type_of_aggregation_string().upper() @@ -128,24 +150,26 @@ def assert_valid_inverse(attr, val, schema): if aggr: aggr_str = f'{aggr} [{b1}:{"?" if b2 == -1 else b2}] OF ' else: - aggr_str = '' + aggr_str = "" attr_formatted = f"{attr.name()} : {aggr_str}{ent_ref} FOR {attr_ref}" - raise ValidationError(f"With inverse:\n {attr_formatted}\nValue:\n {format(val)}\nNot valid\n", attr.name()) + raise ValidationError( + f"With inverse:\n {attr_formatted}\nValue:\n {format(val)}\nNot valid\n", attr.name() + ) return True -select_members_cache = {} +select_members_cache: dict[tuple[str, str], set[str]] = {} -def get_select_members(schema, ty): +def get_select_members(schema: schema_definition, ty: select_type) -> set[str]: cache_key = schema.name(), ty.name() from_cache = select_members_cache.get(cache_key) if from_cache: return from_cache - def inner(ty): + def inner(ty: select_type) -> Iterator[str]: if isinstance(ty, select_type): for st in ty.select_list(): yield from inner(st) @@ -166,7 +190,13 @@ def get_select_members(schema, ty): return v -def assert_valid(attr_type, val, schema, no_throw=False, attr=None): +def assert_valid( + attr_type: attribute_types, + val: Any, + schema: schema_definition, + no_throw=False, + attr: Optional[attribute] = None, +): type_wrappers = (named_type,) if not isinstance(val, ifcopenshell.entity_instance): # If val is not an entity instance we need to @@ -209,19 +239,26 @@ def assert_valid(attr_type, val, schema, no_throw=False, attr=None): elif isinstance(attr_type, aggregation_type): b1, b2 = attr_type.bound1(), attr_type.bound2() ty = attr_type.type_of_element() - invalid = len(val) < b1 or (b2 != -1 and len(val) > b2) or not all(assert_valid(ty, v, schema, attr=attr) for v in val) + invalid = ( + len(val) < b1 + or (b2 != -1 and len(val) > b2) + or not all(assert_valid(ty, v, schema, attr=attr) for v in val) + ) else: raise NotImplementedError("Not impl %s %s" % (type(attr_type), attr_type)) if no_throw: return not invalid elif invalid: - raise ValidationError(f"With attribute:\n {attr or attr_type}\nValue:\n {val}\nNot valid\n", *([attr.name()] if attr else [])) + raise ValidationError( + f"With attribute:\n {attr or attr_type}\nValue:\n {val}\nNot valid\n", + *([attr.name()] if attr else []), + ) else: return True -def log_internal_cpp_errors(filename, logger): +def log_internal_cpp_errors(filename: str, logger: Logger) -> None: import re import bisect @@ -231,7 +268,6 @@ def log_internal_cpp_errors(filename, logger): msgs = list(map(json.loads, filter(None, log.split("\n")))) chr_offsets = [chr_offset_re.findall(m["message"]) for m in msgs] if chr_offsets: - # The file is opened in binary mode, in order # to correspond with the offsets reported by # IfcOpenShell C++ @@ -246,17 +282,17 @@ def log_internal_cpp_errors(filename, logger): m = chr_offset_re.sub("", msg["message"]) if hasattr(logger, "set_state"): - logger.set_state('instance', line) - logger.set_state('attribute', None) + logger.set_state("instance", line) + logger.set_state("attribute", None) logger.error("%s:\n\n%s" % (m, line)) else: logger.error("For instance:\n %s\n%s", line, m) -entity_attribute_map = {} +entity_attribute_map: dict[tuple[str, str], tuple[entity_type, tuple[attribute]]] = {} -def get_entity_attributes(schema, entity): +def get_entity_attributes(schema: schema_definition, entity: str) -> tuple[entity_type, tuple[attribute]]: cache_key = schema.name(), entity from_cache = entity_attribute_map.get(cache_key) if from_cache: @@ -271,7 +307,7 @@ def get_entity_attributes(schema, entity): return entity_attrs -def validate(f, logger, express_rules=False): +def validate(f: Union[ifcopenshell.file, str], logger: Logger, express_rules=False) -> None: """ For an IFC population model `f` (or filepath to such a file) validate whether the entity attribute values are correctly supplied. As this is a function that is applied after a file has been parsed, certain types of errors in syntax, duplicate @@ -289,6 +325,15 @@ def validate(f, logger, express_rules=False): It is recommended to supply the path to the file, so that internal C++ errors reported during the parse stage are also captured. + + Example: + + .. code:: python + + logger = ifcopenshell.validate.json_logger() + ifcopenshell.validate.validate("/path/to/model.ifc", logger, express_rules=True) + from pprint import pprint + pprint(logger.statements) """ # Originally there was no way in Python to distinguish on an entity instance attribute value whether the @@ -300,11 +345,10 @@ def validate(f, logger, express_rules=False): filename = None - if hasattr(logger, 'set_state'): - logger.set_state('type', 'schema') + if hasattr(logger, "set_state"): + logger.set_state("type", "schema") if not isinstance(f, ifcopenshell.file): - # get_log() clears log existing output ifcopenshell.get_log() # @todo restore log format @@ -314,31 +358,49 @@ def validate(f, logger, express_rules=False): try: f = ifcopenshell.open(f) except ifcopenshell.SchemaError as e: - current_dir_files = {fn.lower(): fn for fn in os.listdir('.')} - schema_name = str(e).split(' ')[-1].lower() - exists = current_dir_files.get(schema_name + '.exp') + current_dir_files = {fn.lower(): fn for fn in os.listdir(".")} + schema_name = str(e).split(" ")[-1].lower() + exists = current_dir_files.get(schema_name + ".exp") if exists: schema = ifcopenshell.express.parse(exists) ifcopenshell.register_schema(schema) f = ifcopenshell.open(f) else: - logger.error(f'Unsupported schema: {schema_name}') + logger.error(f"Unsupported schema: {schema_name}") return log_internal_cpp_errors(filename, logger) schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(f.schema_identifier) + used_guids: dict[str, ifcopenshell.entity_instance] = dict() + for inst in f: if hasattr(logger, "set_state"): - logger.set_state('instance', inst) + logger.set_state("instance", inst) + + if hasattr(inst, "GlobalId"): + guid = inst.GlobalId + if guid is not None and guid in used_guids: + rule = "Rule IfcRoot.UR1:\n The attribute GlobalId should be unique" + previous_element = used_guids[guid] + logger.error( + "On instance:\n %s\n %s\n%s\nViolated by:\n %s\n %s", + inst, + annotate_inst_attr_pos(inst, 0), + rule, + previous_element, + annotate_inst_attr_pos(previous_element, 0), + ) + else: + used_guids[guid] = inst entity, attrs = get_entity_attributes(schema, inst.is_a()) if entity.is_abstract(): e = "Entity %s is abstract" % entity.name() if hasattr(logger, "set_state"): - logger.set_state('attribute', None) + logger.set_state("attribute", None) logger.error(e) else: logger.error("For instance:\n %s\n%s", inst, e) @@ -351,7 +413,7 @@ def validate(f, logger, express_rules=False): pass except: if hasattr(logger, "set_state"): - logger.set_state('attribute', f"{entity.name()}.{attrs[i].name()}") + logger.set_state("attribute", f"{entity.name()}.{attrs[i].name()}") logger.error("Invalid attribute value") else: logger.error( @@ -365,10 +427,9 @@ def validate(f, logger, express_rules=False): if not has_invalid_value: for i, (attr, val, is_derived) in enumerate(zip(attrs, values, entity.derived())): - if is_derived and not isinstance(val, ifcopenshell.ifcopenshell_wrapper.attribute_value_derived): if hasattr(logger, "set_state"): - logger.set_state('attribute', f"{entity.name()}.{attr.name()}") + logger.set_state("attribute", f"{entity.name()}.{attr.name()}") logger.error("Attribute is derived in subtype") else: logger.error( @@ -380,7 +441,7 @@ def validate(f, logger, express_rules=False): if val is None and not attr.optional() and not is_derived: if hasattr(logger, "set_state"): - logger.set_state('attribute', f"{entity.name()}.{attr.name()}") + logger.set_state("attribute", f"{entity.name()}.{attr.name()}") logger.error("Attribute not optional") else: logger.error( @@ -396,7 +457,7 @@ def validate(f, logger, express_rules=False): assert_valid(attr_type, val, schema, attr=attr) except ValidationError as e: if hasattr(logger, "set_state"): - logger.set_state('attribute', e.attribute) + logger.set_state("attribute", e.attribute) logger.error(str(e)) else: logger.error( @@ -411,7 +472,7 @@ def validate(f, logger, express_rules=False): val = getattr(inst, attr.name()) except Exception as e: if hasattr(logger, "set_state"): - logger.set_state('attribute', f"{entity.name()}.{attr.name()}") + logger.set_state("attribute", f"{entity.name()}.{attr.name()}") logger.error(str(e)) else: logger.error("For instance:\n %s\n%s", inst, e) @@ -420,7 +481,7 @@ def validate(f, logger, express_rules=False): assert_valid_inverse(attr, val, schema) except ValidationError as e: if hasattr(logger, "set_state"): - logger.set_state('attribute', f"{entity.name()}.{attr.name()}") + logger.set_state("attribute", f"{entity.name()}.{attr.name()}") logger.error(str(e)) else: logger.error("For instance:\n %s\n%s", inst, e) @@ -437,9 +498,9 @@ def validate(f, logger, express_rules=False): ifcopenshell.ifcopenshell_wrapper.set_feature("use_attribute_value_derived", attribute_value_derived_org) if express_rules: - if hasattr(logger, 'set_state'): - logger.set_state('instance', None) - logger.set_state('attribute', None) + if hasattr(logger, "set_state"): + logger.set_state("instance", None) + logger.set_state("attribute", None) ifcopenshell.express.rule_executor.run(f, logger) @@ -461,14 +522,16 @@ if __name__ == "__main__": validate(fn, logger, "--rules" in flags) if "--json" in flags: - sys.stdout.reconfigure(encoding='utf-8') + sys.stdout.reconfigure(encoding="utf-8") conv = str if "--spf" in flags: conv = lambda x: x.to_string() if isinstance(x, ifcopenshell.entity_instance) else str(x) if "--fields" in flags: + def conv(x): if isinstance(x, ifcopenshell.entity_instance): return x.get_info(scalar_only=True) else: return str(x) + print("\n".join(json.dumps(x, default=conv) for x in logger.statements)) diff --git a/src/ifcopenshell-python/pyproject.toml b/src/ifcopenshell-python/pyproject.toml index 2dc57e14a0..fc1bff6807 100644 --- a/src/ifcopenshell-python/pyproject.toml +++ b/src/ifcopenshell-python/pyproject.toml @@ -1,6 +1,7 @@ [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" + [project] name = "ifcopenshell" version = "0.7.0.999999" @@ -9,20 +10,21 @@ authors = [ ] description = "Python bindings, utility functions, and high-level API for IfcOpenShell" readme = "README.md" -requires-python = ">=3.6,<3.12" +requires-python = ">=3.9,<3.13" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", ] -[project.optional-dependencies] -geometry = ["mathutils", "shapely"] -date = ["isodate"] +dependencies = ["mathutils", "shapely", "numpy", "isodate", "python-dateutil", "lark"] + [project.urls] "Homepage" = "http://ifcopenshell.org" "Bug Tracker" = "https://github.com/ifcopenshell/ifcopenshell/issues" + [tool.setuptools.packages.find] where = ["dist"] include = ["ifcopenshell*"] + [tool.setuptools.package-data] ifcopenshell = ["*.pyd", "*.so", "*.json"] "ifcopenshell.util" = ["*.json", "schema/*.ifc"] diff --git a/src/ifcopenshell-python/test/api/aggregate/test_assign_object.py b/src/ifcopenshell-python/test/api/aggregate/test_assign_object.py index f4d23bfc69..c43733f426 100644 --- a/src/ifcopenshell-python/test/api/aggregate/test_assign_object.py +++ b/src/ifcopenshell-python/test/api/aggregate/test_assign_object.py @@ -25,19 +25,23 @@ import ifcopenshell.util.placement class TestAssignObject(test.bootstrap.IFC4): - def test_assigning_a_container(self): + def test_assigning_an_aggregate(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") - subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - rel = ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) - assert ifcopenshell.util.element.get_aggregate(subelement) == element + subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + rel = ifcopenshell.api.run( + "aggregate.assign_object", self.file, products=[subelement1, subelement2], relating_object=element + ) + assert ifcopenshell.util.element.get_aggregate(subelement1) == element + assert ifcopenshell.util.element.get_aggregate(subelement2) == element assert rel.is_a("IfcRelAggregates") def test_doing_nothing_if_the_aggregate_is_already_assigned(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement], relating_object=element) total_elements = len([e for e in self.file]) - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement], relating_object=element) assert len([e for e in self.file]) == total_elements def test_that_old_aggregate_relationships_are_updated_if_they_still_have_elements(self): @@ -45,20 +49,20 @@ class TestAssignObject(test.bootstrap.IFC4): element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement1, relating_object=element1) - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement2, relating_object=element1) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement1], relating_object=element1) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement2], relating_object=element1) rel = subelement1.Decomposes[0] assert len(rel.RelatedObjects) == 2 - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement1, relating_object=element2) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement1], relating_object=element2) assert len(rel.RelatedObjects) == 1 def test_that_old_aggregate_relationships_are_purged_if_no_more_elements_are_contained(self): element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement1, relating_object=element1) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement1], relating_object=element1) rel_id = subelement1.Decomposes[0].id() - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement1, relating_object=element2) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement1], relating_object=element2) with pytest.raises(RuntimeError): self.file.by_id(rel_id) @@ -68,7 +72,7 @@ class TestAssignObject(test.bootstrap.IFC4): element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element1) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement], relating_object=element1) matrix1 = numpy.array( ( (1.0, 0.0, 0.0, 1.0), @@ -94,7 +98,7 @@ class TestAssignObject(test.bootstrap.IFC4): ifcopenshell.api.run( "geometry.edit_object_placement", self.file, product=subelement, matrix=matrix1.copy(), is_si=False ) - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element2) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement], relating_object=element2) assert subelement.ObjectPlacement.PlacementRelTo.PlacesObject[0] == element2 assert numpy.array_equal(ifcopenshell.util.placement.get_local_placement(subelement.ObjectPlacement), matrix1) @@ -105,7 +109,7 @@ class TestAssignObject(test.bootstrap.IFC4): subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") placement = self.file.createIfcGridPlacement() subelement.ObjectPlacement = placement - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement], relating_object=element) assert subelement.ObjectPlacement == placement def test_removing_containment_if_it_exists(self): @@ -114,6 +118,6 @@ class TestAssignObject(test.bootstrap.IFC4): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcElementAssembly") container = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuildingStorey") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=container) - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=container) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement], relating_object=element) assert not ifcopenshell.util.element.get_container(subelement, should_get_direct=True) diff --git a/src/ifcopenshell-python/test/api/aggregate/test_unassign_object.py b/src/ifcopenshell-python/test/api/aggregate/test_unassign_object.py index de44b0727e..64e4fe4fcd 100644 --- a/src/ifcopenshell-python/test/api/aggregate/test_unassign_object.py +++ b/src/ifcopenshell-python/test/api/aggregate/test_unassign_object.py @@ -25,23 +25,27 @@ import ifcopenshell.util.element class TestUnassignObject(test.bootstrap.IFC4): def test_unassigning_an_object(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") - subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) - ifcopenshell.api.run("aggregate.unassign_object", self.file, product=subelement) - assert ifcopenshell.util.element.get_aggregate(subelement) is None + subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + ifcopenshell.api.run( + "aggregate.assign_object", self.file, products=[subelement1, subelement2], relating_object=element + ) + ifcopenshell.api.run("aggregate.unassign_object", self.file, products=[subelement1, subelement2]) + assert ifcopenshell.util.element.get_aggregate(subelement1) is None + assert ifcopenshell.util.element.get_aggregate(subelement2) is None def test_the_rel_is_kept_if_there_are_more_decomposed_elements(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement1, relating_object=element) - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement2, relating_object=element) - ifcopenshell.api.run("aggregate.unassign_object", self.file, product=subelement1) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement1], relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement2], relating_object=element) + ifcopenshell.api.run("aggregate.unassign_object", self.file, products=[subelement1]) assert len(self.file.by_type("IfcRelAggregates")) == 1 def test_the_rel_is_purged_if_there_are_no_more_decomposed_elements(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) - ifcopenshell.api.run("aggregate.unassign_object", self.file, product=subelement) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement], relating_object=element) + ifcopenshell.api.run("aggregate.unassign_object", self.file, products=[subelement]) assert len(self.file.by_type("IfcRelAggregates")) == 0 diff --git a/src/ifcopenshell-python/test/api/classification/test_add_classification_reference.py b/src/ifcopenshell-python/test/api/classification/test_add_reference.py similarity index 52% rename from src/ifcopenshell-python/test/api/classification/test_add_classification_reference.py rename to src/ifcopenshell-python/test/api/classification/test_add_reference.py index 7cf5722b8f..ed52b05d35 100644 --- a/src/ifcopenshell-python/test/api/classification/test_add_classification_reference.py +++ b/src/ifcopenshell-python/test/api/classification/test_add_reference.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import pytest import test.bootstrap import ifcopenshell.api import ifcopenshell.util.classification @@ -23,85 +24,127 @@ import ifcopenshell.util.classification class TestAddReference(test.bootstrap.IFC4): def test_adding_a_reference(self): + is_ifc2x3 = self.file.schema == "IFC2X3" + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element, element2], identification="X", name="Foobar", classification=result, ) references = list(ifcopenshell.util.classification.get_references(element)) assert len(references) == 1 - assert references[0].Identification == "X" + assert getattr(references[0], "ItemReference" if is_ifc2x3 else "Identification") == "X" assert references[0].Name == "Foobar" assert references[0].ReferencedSource == self.file.by_type("IfcClassification")[0] - element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run( - "classification.add_reference", - self.file, - product=element2, - identification="X", - name="Foobar", - classification=result, + references2 = list(ifcopenshell.util.classification.get_references(element)) + assert len(references2) == 1 + assert getattr(references2[0], "ItemReference" if is_ifc2x3 else "Identification") == "X" + assert references2[0].Name == "Foobar" + assert references2[0] == references[0] + + rel = next( + rel + for rel in self.file.by_type("IfcRelAssociatesClassification") + if rel.RelatingClassification == references[0] ) - assert list(ifcopenshell.util.classification.get_references(element2))[0].Identification == "X" - assert list(ifcopenshell.util.classification.get_references(element2))[0].Name == "Foobar" - assert list(ifcopenshell.util.classification.get_references(element2))[0] == references[0] + assert len(rel.RelatedObjects) == 2 def test_adding_a_library_based_reference(self): + is_ifc2x3 = self.file.schema == "IFC2X3" + library = ifcopenshell.file() classification = library.createIfcClassification(Name="Name") reference = library.createIfcClassificationReference(Identification="1", ReferencedSource=classification) ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") result = ifcopenshell.api.run("classification.add_classification", self.file, classification=classification) ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element, element2], reference=reference, classification=result, ) references = list(ifcopenshell.util.classification.get_references(element)) assert len(references) == 1 - assert references[0].Identification == "1" + assert getattr(references[0], "ItemReference" if is_ifc2x3 else "Identification") == "1" assert references[0].ReferencedSource == self.file.by_type("IfcClassification")[0] - def test_adding_a_reference_to_a_resource(self): + references2 = list(ifcopenshell.util.classification.get_references(element2)) + assert len(references2) == 1 + assert getattr(references2[0], "ItemReference" if is_ifc2x3 else "Identification") == "1" + assert references2[0].ReferencedSource == self.file.by_type("IfcClassification")[0] + assert references[0] == references2[0] + + rel = next( + rel + for rel in self.file.by_type("IfcRelAssociatesClassification") + if rel.RelatingClassification == references[0] + ) + assert len(rel.RelatedObjects) == 2 + + def test_adding_a_reference_to_a_resource_and_to_a_root(self): ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") element = self.file.createIfcMaterial() + element2 = self.file.createIfcCostValue() + element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") - ifcopenshell.api.run( - "classification.add_reference", - self.file, - product=element, - identification="X", - name="Foobar", - classification=result, - ) + + if self.file.schema == "IFC2X3": + with pytest.raises(TypeError): + ifcopenshell.api.run( + "classification.add_reference", + self.file, + products=[element, element2, element3], + identification="X", + name="Foobar", + classification=result, + ) + return + else: + ifcopenshell.api.run( + "classification.add_reference", + self.file, + products=[element, element2, element3], + identification="X", + name="Foobar", + classification=result, + ) + references = list(ifcopenshell.util.classification.get_references(element)) assert len(references) == 1 assert references[0].Identification == "X" assert references[0].Name == "Foobar" assert references[0].ReferencedSource == self.file.by_type("IfcClassification")[0] - element2 = self.file.createIfcCostValue() - ifcopenshell.api.run( - "classification.add_reference", - self.file, - product=element2, - identification="X", - name="Foobar", - classification=result, - ) - assert list(ifcopenshell.util.classification.get_references(element2))[0].Identification == "X" - assert list(ifcopenshell.util.classification.get_references(element2))[0].Name == "Foobar" - assert list(ifcopenshell.util.classification.get_references(element2))[0] == references[0] + references2 = list(ifcopenshell.util.classification.get_references(element2)) + assert references2[0].Identification == "X" + assert references2[0].Name == "Foobar" + assert references2[0] == references[0] + + references3 = list(ifcopenshell.util.classification.get_references(element3)) + assert references3[0].Identification == "X" + assert references3[0].Name == "Foobar" + assert references3[0] == references[0] assert len(self.file.by_type("IfcExternalReferenceRelationship")[0].RelatedResourceObjects) == 2 + rel = next( + rel + for rel in self.file.by_type("IfcRelAssociatesClassification") + if rel.RelatingClassification == references[0] + ) + assert len(rel.RelatedObjects) == 1 + + +class TestAddReferenceIFC2X3(test.bootstrap.IFC2X3, TestAddReference): + pass diff --git a/src/ifcopenshell-python/test/api/classification/test_remove_classification.py b/src/ifcopenshell-python/test/api/classification/test_remove_classification.py index 3652d6e8c8..fb3537ddf2 100644 --- a/src/ifcopenshell-python/test/api/classification/test_remove_classification.py +++ b/src/ifcopenshell-python/test/api/classification/test_remove_classification.py @@ -34,7 +34,7 @@ class TestRemoveClassification(test.bootstrap.IFC4): ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element], identification="X", name="Foobar", classification=result, @@ -51,7 +51,7 @@ class TestRemoveClassification(test.bootstrap.IFC4): ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element], identification="X", name="Foobar", classification=result, diff --git a/src/ifcopenshell-python/test/api/classification/test_remove_classification_reference.py b/src/ifcopenshell-python/test/api/classification/test_remove_reference.py similarity index 55% rename from src/ifcopenshell-python/test/api/classification/test_remove_classification_reference.py rename to src/ifcopenshell-python/test/api/classification/test_remove_reference.py index 597317f3bb..8e03fdef4f 100644 --- a/src/ifcopenshell-python/test/api/classification/test_remove_classification_reference.py +++ b/src/ifcopenshell-python/test/api/classification/test_remove_reference.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import pytest import test.bootstrap import ifcopenshell.api import ifcopenshell.util.classification @@ -25,58 +26,80 @@ class TestRemoveReference(test.bootstrap.IFC4): def test_removing_a_reference(self): ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") reference = ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element, element2], identification="X", name="Foobar", classification=result, ) - ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference) + ifcopenshell.api.run( + "classification.remove_reference", self.file, products=[element, element2], reference=reference + ) assert len(ifcopenshell.util.classification.get_references(element)) == 0 + assert len(ifcopenshell.util.classification.get_references(element2)) == 0 assert len(self.file.by_type("IfcClassificationReference")) == 0 - def test_removing_a_reference_from_a_resource(self): + def test_removing_a_reference_from_a_resource_and_from_a_root(self): ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") element = self.file.createIfcMaterial() + element2 = self.file.createIfcCostValue() + element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") - reference = ifcopenshell.api.run( - "classification.add_reference", - self.file, - product=element, - identification="X", - name="Foobar", - classification=result, + if self.file.schema == "IFC2X3": + with pytest.raises(TypeError): + reference = ifcopenshell.api.run( + "classification.add_reference", + self.file, + products=[element, element2, element3], + identification="X", + name="Foobar", + classification=result, + ) + return + else: + reference = ifcopenshell.api.run( + "classification.add_reference", + self.file, + products=[element, element2, element3], + identification="X", + name="Foobar", + classification=result, + ) + + ifcopenshell.api.run( + "classification.remove_reference", self.file, products=[element, element2, element3], reference=reference ) - ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference) assert len(ifcopenshell.util.classification.get_references(element)) == 0 + assert len(ifcopenshell.util.classification.get_references(element2)) == 0 + assert len(ifcopenshell.util.classification.get_references(element3)) == 0 assert len(self.file.by_type("IfcClassificationReference")) == 0 def test_retaining_the_reference_if_still_in_use(self): ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") - element = self.file.createIfcMaterial() - element2 = self.file.createIfcMaterial() + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") reference = ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, - identification="X", - name="Foobar", - classification=result, - ) - reference2 = ifcopenshell.api.run( - "classification.add_reference", - self.file, - product=element2, + products=[element, element2, element3], identification="X", name="Foobar", classification=result, ) assert len(self.file.by_type("IfcClassificationReference")) == 1 - ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference) + ifcopenshell.api.run( + "classification.remove_reference", self.file, products=[element, element2], reference=reference + ) assert len(self.file.by_type("IfcClassificationReference")) == 1 - ifcopenshell.api.run("classification.remove_reference", self.file, product=element2, reference=reference2) + ifcopenshell.api.run("classification.remove_reference", self.file, products=[element3], reference=reference) assert len(self.file.by_type("IfcClassificationReference")) == 0 + + +class TestRemoveReferenceIFC2X3(test.bootstrap.IFC2X3, TestRemoveReference): + pass diff --git a/src/ifcopenshell-python/test/api/constraint/test_assign_constraint.py b/src/ifcopenshell-python/test/api/constraint/test_assign_constraint.py new file mode 100644 index 0000000000..f881ae9651 --- /dev/null +++ b/src/ifcopenshell-python/test/api/constraint/test_assign_constraint.py @@ -0,0 +1,64 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import pytest +import test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.constraint + + +class TestAssignConstraint(test.bootstrap.IFC4): + def test_assign_a_constraint(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + constraint = ifcopenshell.api.run("constraint.add_objective", self.file) + ifcopenshell.api.run( + "constraint.assign_constraint", self.file, products=[element, element2], constraint=constraint + ) + assert ifcopenshell.util.constraint.get_constrained_elements(constraint) == {element, element2} + assert len(self.file.by_type("IfcRelAssociatesConstraint")) == 1 + + def test_doing_nothing_if_the_constraint_is_already_assigned(self): + constraint = ifcopenshell.api.run("constraint.add_objective", self.file) + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "constraint.assign_constraint", self.file, products=[element, element2], constraint=constraint + ) + total_elements = len([e for e in self.file]) + ifcopenshell.api.run( + "constraint.assign_constraint", self.file, products=[element, element2], constraint=constraint + ) + assert len([e for e in self.file]) == total_elements + + def test_that_old_relationships_are_updated_if_they_still_contain_elements(self): + constraint = ifcopenshell.api.run("constraint.add_objective", self.file) + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("constraint.assign_constraint", self.file, products=[element1], constraint=constraint) + rel = self.file.by_type("IfcRelAssociatesConstraint")[0] + + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "constraint.assign_constraint", self.file, products=[element2, element3], constraint=constraint + ) + assert len(rel.RelatedObjects) == 3 + + +class TestAssignConstraintIFC2X3(test.bootstrap.IFC2X3, TestAssignConstraint): + pass diff --git a/src/ifcopenshell-python/test/api/constraint/test_unassign_constraint.py b/src/ifcopenshell-python/test/api/constraint/test_unassign_constraint.py new file mode 100644 index 0000000000..0d8a21b8bf --- /dev/null +++ b/src/ifcopenshell-python/test/api/constraint/test_unassign_constraint.py @@ -0,0 +1,67 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import pytest +import test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.constraint + + +class TestUnassignConstraint(test.bootstrap.IFC4): + def test_unassigning_a_constraint(self): + constraint = ifcopenshell.api.run("constraint.add_objective", self.file) + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "constraint.assign_constraint", self.file, products=[element, element2], constraint=constraint + ) + ifcopenshell.api.run( + "constraint.unassign_constraint", self.file, products=[element, element2], constraint=constraint + ) + assert ifcopenshell.util.constraint.get_constrained_elements(element) == set() + assert len(self.file.by_type("IfcRelAssociatesConstraint")) == 0 + + def test_doing_nothing_if_no_constraint(self): + constraint = ifcopenshell.api.run("constraint.add_objective", self.file) + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "constraint.unassign_constraint", self.file, products=[element, element2], constraint=constraint + ) + assert ifcopenshell.util.constraint.get_constrained_elements(element) == set() + assert ifcopenshell.util.constraint.get_constrained_elements(element2) == set() + + def test_updating_the_rel_when_a_reference_is_removed_with_multipled_elements(self): + constraint = ifcopenshell.api.run("constraint.add_objective", self.file) + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("constraint.assign_constraint", self.file, products=[element1], constraint=constraint) + rel = self.file.by_type("IfcRelAssociatesConstraint")[0] + + ifcopenshell.api.run( + "constraint.assign_constraint", self.file, products=[element2, element3], constraint=constraint + ) + ifcopenshell.api.run( + "constraint.unassign_constraint", self.file, products=[element1, element2], constraint=constraint + ) + assert rel.RelatedObjects == (element3,) + + +class TestUnassignConstraintIFC2X3(test.bootstrap.IFC2X3, TestUnassignConstraint): + pass diff --git a/src/ifcopenshell-python/test/api/document/test_add_reference_.py b/src/ifcopenshell-python/test/api/document/test_add_reference.py similarity index 65% rename from src/ifcopenshell-python/test/api/document/test_add_reference_.py rename to src/ifcopenshell-python/test/api/document/test_add_reference.py index deb379cdaa..6cccd86d27 100644 --- a/src/ifcopenshell-python/test/api/document/test_add_reference_.py +++ b/src/ifcopenshell-python/test/api/document/test_add_reference.py @@ -32,19 +32,9 @@ class TestAddReference(test.bootstrap.IFC4): element = ifcopenshell.api.run("document.add_reference", self.file, information=information) assert element.is_a("IfcDocumentReference") assert len(self.file.by_type("IfcDocumentReference")) == 1 - assert element.ReferencedDocument == information + ifc2x3 = self.file.schema == "IFC2X3" + assert (element.ReferenceToDocument[0] if ifc2x3 else element.ReferencedDocument) == information -class TestAddReferenceIFC2X3(test.bootstrap.IFC2X3): - def test_adding_a_reference(self): - element = ifcopenshell.api.run("document.add_reference", self.file, information=None) - assert element.is_a("IfcDocumentReference") - assert len(self.file.by_type("IfcDocumentReference")) == 1 - - def test_adding_a_reference_to_an_information(self): - self.file.createIfcProject() - information = ifcopenshell.api.run("document.add_information", self.file, parent=None) - element = ifcopenshell.api.run("document.add_reference", self.file, information=information) - assert element.is_a("IfcDocumentReference") - assert len(self.file.by_type("IfcDocumentReference")) == 1 - assert element.ReferenceToDocument[0] == information +class TestAddReferenceIFC2X3(test.bootstrap.IFC2X3, TestAddReference): + pass diff --git a/src/ifcopenshell-python/test/api/document/test_assign_document.py b/src/ifcopenshell-python/test/api/document/test_assign_document.py index 6c76f844b4..32d584ac51 100644 --- a/src/ifcopenshell-python/test/api/document/test_assign_document.py +++ b/src/ifcopenshell-python/test/api/document/test_assign_document.py @@ -18,22 +18,25 @@ import test.bootstrap import ifcopenshell.api +import ifcopenshell.util.element class TestAssignDocument(test.bootstrap.IFC4): def test_assigning_a_document(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) - ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference) + ifcopenshell.api.run("document.assign_document", self.file, products=[element], document=reference) assert element.HasAssociations[0].RelatingDocument == reference + assert ifcopenshell.util.element.get_referenced_elements(reference) == {element} def test_assigning_multiple_documents(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) - ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference) - ifcopenshell.api.run("document.assign_document", self.file, product=element2, document=reference) + ifcopenshell.api.run("document.assign_document", self.file, products=[element, element2], document=reference) assert len(self.file.by_type("IfcRelAssociatesDocument")) == 1 - assert element.HasAssociations[0].RelatingDocument == reference - assert element2.HasAssociations[0].RelatingDocument == reference - assert element.HasAssociations[0] == element.HasAssociations[0] + assert ifcopenshell.util.element.get_referenced_elements(reference) == {element, element2} + + +class TestAssignDocumentIFC2X3(test.bootstrap.IFC2X3, TestAssignDocument): + pass diff --git a/src/ifcopenshell-python/test/api/document/test_remove_reference_.py b/src/ifcopenshell-python/test/api/document/test_remove_reference.py similarity index 98% rename from src/ifcopenshell-python/test/api/document/test_remove_reference_.py rename to src/ifcopenshell-python/test/api/document/test_remove_reference.py index e5f5892172..a1b3c260e2 100644 --- a/src/ifcopenshell-python/test/api/document/test_remove_reference_.py +++ b/src/ifcopenshell-python/test/api/document/test_remove_reference.py @@ -35,7 +35,7 @@ class TestRemoveReference(test.bootstrap.IFC4): wall = self.file.createIfcWall() information = ifcopenshell.api.run("document.add_information", self.file, parent=None) reference = ifcopenshell.api.run("document.add_reference", self.file, information=information) - ifcopenshell.api.run("document.assign_document", self.file, product=wall, document=reference) + ifcopenshell.api.run("document.assign_document", self.file, products=[wall], document=reference) assert len(self.file.by_type("IfcRelAssociatesDocument")) == 2 ifcopenshell.api.run("document.remove_reference", self.file, reference=reference) assert len(self.file.by_type("IfcDocumentReference")) == 0 diff --git a/src/ifcopenshell-python/test/api/document/test_unassign_document.py b/src/ifcopenshell-python/test/api/document/test_unassign_document.py index 8f227dd2b2..bce6c8d6a1 100644 --- a/src/ifcopenshell-python/test/api/document/test_unassign_document.py +++ b/src/ifcopenshell-python/test/api/document/test_unassign_document.py @@ -18,23 +18,29 @@ import test.bootstrap import ifcopenshell.api +import ifcopenshell.util.element class TestUnassignDocument(test.bootstrap.IFC4): def test_unassigning_a_document(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) - ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference) - ifcopenshell.api.run("document.unassign_document", self.file, product=element, document=reference) + ifcopenshell.api.run("document.assign_document", self.file, products=[element], document=reference) + ifcopenshell.api.run("document.unassign_document", self.file, products=[element], document=reference) assert not element.HasAssociations assert not len(self.file.by_type("IfcRelAssociatesDocument")) def test_unassigning_a_document_used_by_multiple_entities(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) - ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference) - ifcopenshell.api.run("document.assign_document", self.file, product=element2, document=reference) - ifcopenshell.api.run("document.unassign_document", self.file, product=element, document=reference) - assert not element.HasAssociations - assert element2.HasAssociations[0].RelatingDocument == reference + ifcopenshell.api.run( + "document.assign_document", self.file, products=[element, element2, element3], document=reference + ) + ifcopenshell.api.run("document.unassign_document", self.file, products=[element, element2], document=reference) + assert ifcopenshell.util.element.get_referenced_elements(reference) == {element3} + + +class TestUnassignDocumentIFC2X3(test.bootstrap.IFC2X3, TestUnassignDocument): + pass diff --git a/src/ifcopenshell-python/test/api/geometry/test_assign_representation.py b/src/ifcopenshell-python/test/api/geometry/test_assign_representation.py index 1bdd7f1ce8..654e4d383a 100644 --- a/src/ifcopenshell-python/test/api/geometry/test_assign_representation.py +++ b/src/ifcopenshell-python/test/api/geometry/test_assign_representation.py @@ -47,7 +47,7 @@ class TestAssignRepresentation(test.bootstrap.IFC4): def test_assigning_to_a_type_will_map_representations_to_instances(self): wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") walltype = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=wall, relating_type=walltype) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[wall], relating_type=walltype) context = self.file.createIfcGeometricRepresentationContext() rep = self.file.createIfcShapeRepresentation(ContextOfItems=context) rep2 = self.file.createIfcShapeRepresentation(ContextOfItems=context) @@ -69,7 +69,7 @@ class TestAssignRepresentation(test.bootstrap.IFC4): ifcopenshell.api.run("geometry.assign_representation", self.file, product=walltype, representation=rep) wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("type.assign_type", self.file, related_object=wall, relating_type=walltype) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[wall], relating_type=walltype) ifcopenshell.api.run("geometry.assign_representation", self.file, product=wall, representation=rep2) assert wall.Representation.Representations[0].RepresentationType == "MappedRepresentation" @@ -84,7 +84,7 @@ class TestAssignRepresentation(test.bootstrap.IFC4): rep = self.file.createIfcShapeRepresentation(ContextOfItems=context) walltype = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("type.assign_type", self.file, related_object=wall, relating_type=walltype) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[wall], relating_type=walltype) ifcopenshell.api.run("geometry.assign_representation", self.file, product=wall, representation=rep) assert wall.Representation.Representations[0].RepresentationType != "MappedRepresentation" assert wall.Representation.Representations[0] == rep diff --git a/src/ifcopenshell-python/test/api/geometry/test_edit_object_placement.py b/src/ifcopenshell-python/test/api/geometry/test_edit_object_placement.py index 7c79450d0a..73985fd627 100644 --- a/src/ifcopenshell-python/test/api/geometry/test_edit_object_placement.py +++ b/src/ifcopenshell-python/test/api/geometry/test_edit_object_placement.py @@ -172,7 +172,7 @@ class TestEditObjectPlacement(test.bootstrap.IFC4): ifcopenshell.api.run("unit.assign_unit", self.file) element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=element) matrix1 = numpy.array( ( (1.0, 0.0, 0.0, 1.0), @@ -221,7 +221,7 @@ class TestEditObjectPlacement(test.bootstrap.IFC4): (0.0, 0.0, 0.0, 1.0), ) ) - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=element) ifcopenshell.api.run( "geometry.edit_object_placement", self.file, product=element, matrix=matrix.copy(), is_si=False ) @@ -253,7 +253,7 @@ class TestEditObjectPlacement(test.bootstrap.IFC4): (0.0, 0.0, 0.0, 1.0), ) ) - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement], relating_object=element) ifcopenshell.api.run( "geometry.edit_object_placement", self.file, product=element, matrix=matrix.copy(), is_si=False ) @@ -301,7 +301,7 @@ class TestEditObjectPlacement(test.bootstrap.IFC4): site = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcOpeningElement") - ifcopenshell.api.run("spatial.assign_container", self.file, product=element, relating_structure=site) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[element], relating_structure=site) matrix = numpy.array( ( (1.0, 0.0, 0.0, 1.0), @@ -336,8 +336,8 @@ class TestEditObjectPlacement(test.bootstrap.IFC4): wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcOpeningElement") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcDoor") - ifcopenshell.api.run("spatial.assign_container", self.file, product=wall, relating_structure=site) - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=site) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[wall], relating_structure=site) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=site) matrix = numpy.array( ( (1.0, 0.0, 0.0, 1.0), @@ -402,7 +402,7 @@ class TestEditObjectPlacement(test.bootstrap.IFC4): "GlobalId": ifcopenshell.guid.new(), "RelatingElement": element, "RelatedFeatureElement": subelement, - } + }, ) ifcopenshell.api.run( "geometry.edit_object_placement", self.file, product=element, matrix=matrix.copy(), is_si=False @@ -435,7 +435,7 @@ class TestEditObjectPlacement(test.bootstrap.IFC4): (0.0, 0.0, 0.0, 1.0), ) ) - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=element) ifcopenshell.api.run( "geometry.edit_object_placement", self.file, product=element, matrix=matrix.copy(), is_si=False ) @@ -478,7 +478,7 @@ class TestEditObjectPlacement(test.bootstrap.IFC4): (0.0, 0.0, 0.0, 1.0), ) ) - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=element) ifcopenshell.api.run( "geometry.edit_object_placement", self.file, product=element, matrix=matrix.copy(), is_si=False ) @@ -528,7 +528,7 @@ class TestEditObjectPlacement(test.bootstrap.IFC4): (0.0, 0.0, 0.0, 1.0), ) ) - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=element) ifcopenshell.api.run( "geometry.edit_object_placement", self.file, product=element, matrix=matrix.copy(), is_si=False ) @@ -572,7 +572,9 @@ class TestEditObjectPlacement(test.bootstrap.IFC4): should_transform_children=False, ) assert numpy.array_equal(ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement), submatrix) - assert numpy.array_equal(ifcopenshell.util.placement.get_local_placement(subelement.ObjectPlacement), shifted_submatrix) + assert numpy.array_equal( + ifcopenshell.util.placement.get_local_placement(subelement.ObjectPlacement), shifted_submatrix + ) assert subelement.ObjectPlacement.PlacementRelTo == element.ObjectPlacement # old placement should be removed to avoid orphaned entities with pytest.raises(RuntimeError): @@ -609,7 +611,9 @@ class TestEditObjectPlacement(test.bootstrap.IFC4): should_transform_children=False, ) assert numpy.array_equal(ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement), submatrix) - assert numpy.array_equal(ifcopenshell.util.placement.get_local_placement(subelement.ObjectPlacement), shifted_submatrix) + assert numpy.array_equal( + ifcopenshell.util.placement.get_local_placement(subelement.ObjectPlacement), shifted_submatrix + ) assert subelement.ObjectPlacement.PlacementRelTo == element.ObjectPlacement # old placement should be removed to avoid orphaned entities with pytest.raises(RuntimeError): @@ -625,9 +629,9 @@ class TestEditObjectPlacement(test.bootstrap.IFC4): building = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") storey = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuildingStorey") - ifcopenshell.api.run("aggregate.assign_object", self.file, product=storey, relating_object=building) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[storey], relating_object=building) wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.assign_container", self.file, product=wall, relating_structure=storey) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[wall], relating_structure=storey) matrix = np_matrix_translation((1, 1, 1)) submatrix = np_matrix_translation((1, 2, 3)) diff --git a/src/ifcopenshell-python/test/api/group/test_assign_group.py b/src/ifcopenshell-python/test/api/group/test_assign_group.py new file mode 100644 index 0000000000..434dab2b32 --- /dev/null +++ b/src/ifcopenshell-python/test/api/group/test_assign_group.py @@ -0,0 +1,47 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.element + + +class TestAssignGroup(test.bootstrap.IFC4): + def test_assign_group_for_multiple_elements(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + group = ifcopenshell.api.run("group.add_group", self.file) + + # assign group for multiple elements + ifcopenshell.api.run("group.assign_group", self.file, products=[element, element2], group=group) + assert len(self.file.by_type("IfcRelAssignsToGroup")) == 1 + assert set(ifcopenshell.util.element.get_grouped_by(group)) == set(self.file.by_type("IfcWall")) + + def test_reuse_existing_relationship(self): + self.test_assign_group_for_multiple_elements() + rel = self.file.by_type("IfcRelAssignsToGroup")[0] + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + group = self.file.by_type("IfcGroup")[0] + ifcopenshell.api.run("group.assign_group", self.file, products=[element], group=group) + + assert len(self.file.by_type("IfcRelAssignsToGroup")) == 1 + assert set(ifcopenshell.util.element.get_grouped_by(group)) == set(self.file.by_type("IfcWall")) + + +class TestAssignGroupIFC2X3(test.bootstrap.IFC2X3, TestAssignGroup): + pass diff --git a/src/ifcopenshell-python/test/api/group/test_unassign_group.py b/src/ifcopenshell-python/test/api/group/test_unassign_group.py new file mode 100644 index 0000000000..d7b1179f03 --- /dev/null +++ b/src/ifcopenshell-python/test/api/group/test_unassign_group.py @@ -0,0 +1,43 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import test.bootstrap +import ifcopenshell.api + + +class TestAssignGroup(test.bootstrap.IFC4): + def test_group_unassignment(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcPump") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcPump") + element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcPump") + group = ifcopenshell.api.run("group.add_group", self.file) + ifcopenshell.api.run("group.assign_group", self.file, products=[element, element2, element3], group=group) + ifcopenshell.api.run("group.unassign_group", self.file, products=[element2, element3], group=group) + + assert len(rels := self.file.by_type("IfcRelAssignsToGroup")) == 1 + rel = rels[0] + assert rel.RelatingGroup == group + assert rel.RelatedObjects == (element,) + + def test_remove_relationship_unassigning_last_element(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcPump") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcPump") + group = ifcopenshell.api.run("group.add_group", self.file) + ifcopenshell.api.run("group.assign_group", self.file, products=[element, element2], group=group) + ifcopenshell.api.run("group.unassign_group", self.file, products=[element, element2], group=group) + assert len(self.file.by_type("IfcRelAssignsToGroup")) == 0 diff --git a/src/ifcopenshell-python/test/api/layer/__init__.py b/src/ifcopenshell-python/test/api/layer/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/ifcopenshell-python/test/api/layer/test_assign_layer.py b/src/ifcopenshell-python/test/api/layer/test_assign_layer.py new file mode 100644 index 0000000000..f2d4eca384 --- /dev/null +++ b/src/ifcopenshell-python/test/api/layer/test_assign_layer.py @@ -0,0 +1,39 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import test.bootstrap +import ifcopenshell.api + + +class TestAssignLayer(test.bootstrap.IFC4): + def test_assign_layer_to_items(self): + items = [self.file.createIfcExtrudedAreaSolid() for i in range(2)] + layer = self.file.createIfcPresentationLayerAssignment() + + ifcopenshell.api.run("layer.assign_layer", self.file, items=items, layer=layer) + assert len(layer.AssignedItems) == 2 + assert set(layer.AssignedItems) == set(items) + + def test_assign_additional_items(self): + items = [self.file.createIfcExtrudedAreaSolid() for i in range(4)] + layer = self.file.createIfcPresentationLayerAssignment() + + ifcopenshell.api.run("layer.assign_layer", self.file, items=items[:2], layer=layer) + ifcopenshell.api.run("layer.assign_layer", self.file, items=items[2:], layer=layer) + assert len(layer.AssignedItems) == 4 + assert set(layer.AssignedItems) == set(items) diff --git a/src/ifcopenshell-python/test/api/layer/test_unassign_layer.py b/src/ifcopenshell-python/test/api/layer/test_unassign_layer.py new file mode 100644 index 0000000000..7a29896500 --- /dev/null +++ b/src/ifcopenshell-python/test/api/layer/test_unassign_layer.py @@ -0,0 +1,39 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import test.bootstrap +import ifcopenshell.api + + +class TestUnassignLayer(test.bootstrap.IFC4): + def test_unassign_layer_from_items(self): + items = [self.file.createIfcExtrudedAreaSolid() for i in range(3)] + layer = self.file.createIfcPresentationLayerAssignment() + + ifcopenshell.api.run("layer.assign_layer", self.file, items=items, layer=layer) + ifcopenshell.api.run("layer.unassign_layer", self.file, items=items[2:], layer=layer) + assert len(layer.AssignedItems) == 2 + assert set(layer.AssignedItems) == set(items[:2]) + + def test_remove_layer_if_all_items_are_unassigned(self): + items = [self.file.createIfcExtrudedAreaSolid() for i in range(3)] + layer = self.file.createIfcPresentationLayerAssignment() + + ifcopenshell.api.run("layer.assign_layer", self.file, items=items, layer=layer) + ifcopenshell.api.run("layer.unassign_layer", self.file, items=items, layer=layer) + assert not self.file.by_type("IfcPresentationLayerAssignment") diff --git a/src/ifcopenshell-python/test/api/library/test_add_reference.py b/src/ifcopenshell-python/test/api/library/test_add_reference.py index 7cb13839b0..c0791e1855 100644 --- a/src/ifcopenshell-python/test/api/library/test_add_reference.py +++ b/src/ifcopenshell-python/test/api/library/test_add_reference.py @@ -25,12 +25,12 @@ class TestAddReference(test.bootstrap.IFC4): library = ifcopenshell.api.run("library.add_library", self.file, name="Name") reference = ifcopenshell.api.run("library.add_reference", self.file, library=library) assert reference.is_a("IfcLibraryReference") - assert reference.ReferencedLibrary == library + ifc2x3 = self.file.schema == "IFC2X3" + if ifc2x3: + assert library.LibraryReference == (reference,) + else: + assert reference.ReferencedLibrary == library -class TestAddReferenceIFC2X3(test.bootstrap.IFC2X3): - def test_adding_a_reference(self): - library = ifcopenshell.api.run("library.add_library", self.file, name="Name") - reference = ifcopenshell.api.run("library.add_reference", self.file, library=library) - assert reference.is_a("IfcLibraryReference") - assert library.LibraryReference == (reference,) +class TestAddReferenceIFC2X3(test.bootstrap.IFC2X3, TestAddReference): + pass diff --git a/src/ifcopenshell-python/test/api/library/test_assign_reference.py b/src/ifcopenshell-python/test/api/library/test_assign_reference.py index 1133b592d6..550df3940f 100644 --- a/src/ifcopenshell-python/test/api/library/test_assign_reference.py +++ b/src/ifcopenshell-python/test/api/library/test_assign_reference.py @@ -20,39 +20,48 @@ import test.bootstrap import ifcopenshell.api +def validate_ifc_file(ifc_file: ifcopenshell.file, use_json=True): + import ifcopenshell + import ifcopenshell.validate + + if use_json: + logger = ifcopenshell.validate.json_logger() + else: + import logging + + logger = logging.getLogger("validate") + logger.setLevel(logging.DEBUG) + + ifcopenshell.validate.validate(ifc_file, logger, express_rules=True) + if use_json: + if logger.statements: + from pprint import pprint + + pprint(logger.statements) + else: + print("IFC is completely valid.") + + class TestAssignReference(test.bootstrap.IFC4): def test_assigning_a_reference(self): reference = self.file.createIfcLibraryReference() product = self.file.createIfcWall() product2 = self.file.createIfcWall() - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) - assert reference.LibraryRefForObjects[0].RelatedObjects == (product,) - ifcopenshell.api.run("library.assign_reference", self.file, product=product2, reference=reference) - assert reference.LibraryRefForObjects[0].RelatedObjects == (product, product2) + product3 = self.file.createIfcWall() + ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference) + rel = self.file.by_type("IfcRelAssociatesLibrary")[0] + assert rel.RelatedObjects == (product,) + ifcopenshell.api.run("library.assign_reference", self.file, products=[product2, product3], reference=reference) + assert set(rel.RelatedObjects) == set((product, product2, product3)) def test_not_assigning_twice(self): reference = self.file.createIfcLibraryReference() product = self.file.createIfcWall() - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) - assert reference.LibraryRefForObjects[0].RelatedObjects == (product,) - - -class TestAssignReferenceIFC2X3(test.bootstrap.IFC2X3): - def test_assigning_a_reference(self): - reference = self.file.createIfcLibraryReference() - product = self.file.createIfcWall() - product2 = self.file.createIfcWall() - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) + ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference) + ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference) rel = self.file.by_type("IfcRelAssociatesLibrary")[0] assert rel.RelatedObjects == (product,) - ifcopenshell.api.run("library.assign_reference", self.file, product=product2, reference=reference) - assert rel.RelatedObjects == (product, product2) - def test_not_assigning_twice(self): - reference = self.file.createIfcLibraryReference() - product = self.file.createIfcWall() - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) - rel = self.file.by_type("IfcRelAssociatesLibrary")[0] - assert rel.RelatedObjects == (product,) + +class TestAssignReferenceIFC2X3(test.bootstrap.IFC2X3, TestAssignReference): + pass diff --git a/src/ifcopenshell-python/test/api/library/test_remove_reference.py b/src/ifcopenshell-python/test/api/library/test_remove_reference.py index 26a9106411..a98ed2f083 100644 --- a/src/ifcopenshell-python/test/api/library/test_remove_reference.py +++ b/src/ifcopenshell-python/test/api/library/test_remove_reference.py @@ -24,7 +24,7 @@ class TestRemoveReference(test.bootstrap.IFC4): def test_removing_a_reference(self): reference = self.file.createIfcLibraryReference() product = self.file.createIfcWall() - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) + ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference) ifcopenshell.api.run("library.remove_reference", self.file, reference=reference) assert len(self.file.by_type("IfcLibraryReference")) == 0 assert len(self.file.by_type("IfcRelAssociatesLibrary")) == 0 diff --git a/src/ifcopenshell-python/test/api/library/test_unassign_reference.py b/src/ifcopenshell-python/test/api/library/test_unassign_reference.py index 900dcdcc12..8962bb686a 100644 --- a/src/ifcopenshell-python/test/api/library/test_unassign_reference.py +++ b/src/ifcopenshell-python/test/api/library/test_unassign_reference.py @@ -18,12 +18,20 @@ import test.bootstrap import ifcopenshell.api +import ifcopenshell.util.element class TestUnassignReference(test.bootstrap.IFC4): def test_unassigning_a_reference(self): reference = self.file.createIfcLibraryReference() - product = self.file.createIfcWall() - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) - ifcopenshell.api.run("library.unassign_reference", self.file, product=product, reference=reference) + products = [self.file.createIfcWall() for i in range(3)] + ifcopenshell.api.run("library.assign_reference", self.file, products=products, reference=reference) + ifcopenshell.api.run("library.unassign_reference", self.file, products=products[:1], reference=reference) + assert ifcopenshell.util.element.get_referenced_elements(reference) == set(products[1:]) + + ifcopenshell.api.run("library.unassign_reference", self.file, products=products[1:], reference=reference) + assert ifcopenshell.util.element.get_referenced_elements(reference) == set() assert len(self.file.by_type("IfcRelAssociatesLibrary")) == 0 + +class TestUnassignReferenceIFC2X3(test.bootstrap.IFC2X3, TestUnassignReference): + pass diff --git a/src/ifcopenshell-python/test/api/material/test_assign_material.py b/src/ifcopenshell-python/test/api/material/test_assign_material.py index 9e616cbb95..4577e7eed5 100644 --- a/src/ifcopenshell-python/test/api/material/test_assign_material.py +++ b/src/ifcopenshell-python/test/api/material/test_assign_material.py @@ -16,82 +16,209 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import pytest import test.bootstrap import ifcopenshell.api import ifcopenshell.util.element -class TestAssignMaterial(test.bootstrap.IFC4): +class TestAssignMaterialIFC2X3(test.bootstrap.IFC2X3): def test_assign_element_single_material(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") - ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterial", material=material) + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element1, element2], type="IfcMaterial", material=material + ) assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 1 - assert element.HasAssociations[0].RelatingMaterial == material + assert ifcopenshell.util.element.get_material(element1) == material + assert ifcopenshell.util.element.get_material(element2) == material + + def test_raise_exception_creating_ifc_material_without(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element], type="IfcMaterial", material=None + ) + assert ifcopenshell.util.element.get_material(element) def test_assign_type_single_material(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") - ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterial", material=material) + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element1, element2], type="IfcMaterial", material=material + ) assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 1 - assert element.HasAssociations[0].RelatingMaterial == material + assert ifcopenshell.util.element.get_material(element1) == material + assert ifcopenshell.util.element.get_material(element2) == material def test_assign_type_material_layer_set(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") - ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialLayerSet") + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element1, element2], type="IfcMaterialLayerSet" + ) assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 1 - material_set = element.HasAssociations[0].RelatingMaterial + material_set = ifcopenshell.util.element.get_material(element1) assert material_set.is_a("IfcMaterialLayerSet") assert not material_set.MaterialLayers + assert ifcopenshell.util.element.get_material(element2) == material_set def test_assign_type_material_layer_set_and_element_layer_set_usage(self): element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) - material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") - ifcopenshell.api.run("material.assign_material", self.file, product=element_type, type="IfcMaterialLayerSet") - ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialLayerSetUsage") - material_set = element_type.HasAssociations[0].RelatingMaterial - material_usage = element.HasAssociations[0].RelatingMaterial - assert material_usage.is_a("IfcMaterialLayerSetUsage") - assert material_usage.ForLayerSet == material_set + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "type.assign_type", self.file, related_objects=[element1, element2], relating_type=element_type + ) + ifcopenshell.api.run("material.assign_material", self.file, products=[element_type], type="IfcMaterialLayerSet") + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element1, element2], type="IfcMaterialLayerSetUsage" + ) + material_set = ifcopenshell.util.element.get_material(element_type) + material_usage1 = ifcopenshell.util.element.get_material(element1, should_inherit=False) + assert material_usage1.is_a("IfcMaterialLayerSetUsage") + assert material_usage1.ForLayerSet == material_set + assert ifcopenshell.util.element.get_material(element2, should_inherit=False) == material_usage1 - def test_assign_type_material_profile_set(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + def test_assign_type_material_layer_set_and_element_layer_set_usage_is_different_for_different_types(self): + element_type1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element1], relating_type=element_type1) + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element_type1], type="IfcMaterialLayerSet" + ) + + element_type2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element2], relating_type=element_type2) + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element_type2], type="IfcMaterialLayerSet" + ) + + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element1, element2], type="IfcMaterialLayerSetUsage" + ) + material_set1 = ifcopenshell.util.element.get_material(element_type1) + material_usage1 = ifcopenshell.util.element.get_material(element1, should_inherit=False) + assert material_usage1.is_a("IfcMaterialLayerSetUsage") + assert material_usage1.ForLayerSet == material_set1 + + material_set2 = ifcopenshell.util.element.get_material(element_type2) + material_usage2 = ifcopenshell.util.element.get_material(element2, should_inherit=False) + assert material_usage2.is_a("IfcMaterialLayerSetUsage") + assert material_usage2.ForLayerSet == material_set2 + assert material_usage2 != material_usage1 + + def test_assign_element_layer_set_usage_is_different_for_different_layer_set_directions(self): + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSlab") + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element1, element2], type="IfcMaterialLayerSetUsage" + ) + + material_usage1 = ifcopenshell.util.element.get_material(element1, should_inherit=False) + assert material_usage1.is_a("IfcMaterialLayerSetUsage") + material_set1 = material_usage1.ForLayerSet + assert material_set1.is_a("IfcMaterialLayerSet") + assert material_usage1.LayerSetDirection == "AXIS2" + + material_usage2 = ifcopenshell.util.element.get_material(element2, should_inherit=False) + assert material_usage2.is_a("IfcMaterialLayerSetUsage") + material_set2 = material_usage2.ForLayerSet + assert material_set2.is_a("IfcMaterialLayerSet") + assert material_usage2.LayerSetDirection == "AXIS3" + + assert material_set1 != material_set2 + assert material_usage1 != material_usage2 + + def test_assign_element_material_list(self): + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") - ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialProfileSet") + ifcopenshell.api.run( + "material.assign_material", + self.file, + products=[element1, element2], + type="IfcMaterialList", + material=material, + ) assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 1 - material_set = element.HasAssociations[0].RelatingMaterial + material_list = ifcopenshell.util.element.get_material(element1) + assert material_list.is_a("IfcMaterialList") + assert material_list.Materials[0] == material + assert ifcopenshell.util.element.get_material(element2) == material_list + + +class TestAssignMaterialIFC4(test.bootstrap.IFC4, TestAssignMaterialIFC2X3): + def test_assign_type_material_profile_set(self): + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element1, element2], type="IfcMaterialProfileSet" + ) + assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 1 + material_set = ifcopenshell.util.element.get_material(element1) assert material_set.is_a("IfcMaterialProfileSet") assert not material_set.MaterialProfiles + assert ifcopenshell.util.element.get_material(element2) == material_set def test_assign_type_material_profile_set_and_element_profile_set_usage(self): element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) - material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") - ifcopenshell.api.run("material.assign_material", self.file, product=element_type, type="IfcMaterialProfileSet") - ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialProfileSetUsage") + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "type.assign_type", self.file, related_objects=[element1, element2], relating_type=element_type + ) + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element_type], type="IfcMaterialProfileSet" + ) + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element1, element2], type="IfcMaterialProfileSetUsage" + ) material_set = element_type.HasAssociations[0].RelatingMaterial - material_usage = element.HasAssociations[0].RelatingMaterial - assert material_usage.is_a("IfcMaterialProfileSetUsage") - assert material_usage.ForProfileSet == material_set + material_usage1 = element1.HasAssociations[0].RelatingMaterial + assert material_usage1.is_a("IfcMaterialProfileSetUsage") + assert material_usage1.ForProfileSet == material_set + assert ifcopenshell.util.element.get_material(element2, should_inherit=False) == material_usage1 + + def test_assign_type_material_profile_set_and_element_profile_set_usage_is_different_for_different_types(self): + element_type1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element1], relating_type=element_type1) + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element_type1], type="IfcMaterialProfileSet" + ) + + element_type2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element2], relating_type=element_type2) + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element_type2], type="IfcMaterialProfileSet" + ) + + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element1, element2], type="IfcMaterialProfileSetUsage" + ) + material_set1 = ifcopenshell.util.element.get_material(element_type1) + material_usage1 = ifcopenshell.util.element.get_material(element1, should_inherit=False) + assert material_usage1.is_a("IfcMaterialProfileSetUsage") + assert material_usage1.ForProfileSet == material_set1 + + material_set2 = ifcopenshell.util.element.get_material(element_type2) + material_usage2 = ifcopenshell.util.element.get_material(element2, should_inherit=False) + assert material_usage2.is_a("IfcMaterialProfileSetUsage") + assert material_usage2.ForProfileSet == material_set2 + assert material_usage2 != material_usage1 def test_assign_type_material_constituent_set(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") - ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialConstituentSet") + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element1, element2], type="IfcMaterialConstituentSet" + ) assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 1 - material_set = element.HasAssociations[0].RelatingMaterial + material_set = ifcopenshell.util.element.get_material(element1) assert material_set.is_a("IfcMaterialConstituentSet") assert not material_set.MaterialConstituents - - def test_assign_element_material_list(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") - ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialList", material=material) - assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 1 - material_list = element.HasAssociations[0].RelatingMaterial - assert material_list.is_a("IfcMaterialList") - assert material_list.Materials[0] == material + assert ifcopenshell.util.element.get_material(element2) == material_set diff --git a/src/ifcopenshell-python/test/api/material/test_copy_material.py b/src/ifcopenshell-python/test/api/material/test_copy_material.py index ea172ca163..ce52bbd769 100644 --- a/src/ifcopenshell-python/test/api/material/test_copy_material.py +++ b/src/ifcopenshell-python/test/api/material/test_copy_material.py @@ -31,7 +31,7 @@ class TestCopyMaterial(test.bootstrap.IFC4): def test_assignments_are_not_copied(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") - ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material) + ifcopenshell.api.run("material.assign_material", self.file, products=[element], material=material) new = ifcopenshell.api.run("material.copy_material", self.file, material=material) assert material.AssociatedTo assert not new.AssociatedTo diff --git a/src/ifcopenshell-python/test/api/material/test_remove_material.py b/src/ifcopenshell-python/test/api/material/test_remove_material.py index 80e0abb1f3..0670c09bbb 100644 --- a/src/ifcopenshell-python/test/api/material/test_remove_material.py +++ b/src/ifcopenshell-python/test/api/material/test_remove_material.py @@ -29,7 +29,7 @@ class TestRemoveMaterial(test.bootstrap.IFC4): def test_removing_material_with_associations(self): wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") material = ifcopenshell.api.run("material.add_material", self.file) - ifcopenshell.api.run("material.assign_material", self.file, product=wall, material=material) + ifcopenshell.api.run("material.assign_material", self.file, products=[wall], material=material) ifcopenshell.api.run("material.remove_material", self.file, material=material) assert len(self.file.by_type("IfcMaterial")) == 0 assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 0 @@ -41,7 +41,7 @@ class TestRemoveMaterial(test.bootstrap.IFC4): "material.add_material_set", self.file, set_type="IfcMaterialLayerSet" ) ifcopenshell.api.run("material.add_layer", self.file, layer_set=material_set, material=material) - ifcopenshell.api.run("material.assign_material", self.file, product=wall, material=material_set) + ifcopenshell.api.run("material.assign_material", self.file, products=[wall], material=material_set) assert len(self.file.by_type("IfcMaterialLayerSet")[0].MaterialLayers) == 1 ifcopenshell.api.run("material.remove_material", self.file, material=material) assert len(self.file.by_type("IfcMaterial")) == 0 @@ -55,7 +55,7 @@ class TestRemoveMaterial(test.bootstrap.IFC4): "material.add_material_set", self.file, set_type="IfcMaterialProfileSet" ) ifcopenshell.api.run("material.add_profile", self.file, profile_set=material_set, material=material) - ifcopenshell.api.run("material.assign_material", self.file, product=wall, material=material_set) + ifcopenshell.api.run("material.assign_material", self.file, products=[wall], material=material_set) assert len(self.file.by_type("IfcMaterialProfileSet")[0].MaterialProfiles) == 1 ifcopenshell.api.run("material.remove_material", self.file, material=material) assert len(self.file.by_type("IfcMaterial")) == 0 @@ -69,7 +69,7 @@ class TestRemoveMaterial(test.bootstrap.IFC4): "material.add_material_set", self.file, set_type="IfcMaterialConstituentSet" ) ifcopenshell.api.run("material.add_constituent", self.file, constituent_set=material_set, material=material) - ifcopenshell.api.run("material.assign_material", self.file, product=wall, material=material_set) + ifcopenshell.api.run("material.assign_material", self.file, products=[wall], material=material_set) assert len(self.file.by_type("IfcMaterialConstituentSet")[0].MaterialConstituents) == 1 ifcopenshell.api.run("material.remove_material", self.file, material=material) assert len(self.file.by_type("IfcMaterial")) == 0 @@ -83,7 +83,7 @@ class TestRemoveMaterial(test.bootstrap.IFC4): "material.add_material_set", self.file, set_type="IfcMaterialList" ) ifcopenshell.api.run("material.add_list_item", self.file, material_list=material_set, material=material) - ifcopenshell.api.run("material.assign_material", self.file, product=wall, material=material_set) + ifcopenshell.api.run("material.assign_material", self.file, products=[wall], material=material_set) assert len(self.file.by_type("IfcMaterialList")[0].Materials) == 1 ifcopenshell.api.run("material.remove_material", self.file, material=material) assert len(self.file.by_type("IfcMaterial")) == 0 diff --git a/src/ifcopenshell-python/test/api/material/test_remove_material_set.py b/src/ifcopenshell-python/test/api/material/test_remove_material_set.py index fe78ce351c..7554225efe 100644 --- a/src/ifcopenshell-python/test/api/material/test_remove_material_set.py +++ b/src/ifcopenshell-python/test/api/material/test_remove_material_set.py @@ -29,7 +29,7 @@ class TestRemoveMaterialSet(test.bootstrap.IFC4): def test_removing_material_set_with_associations(self): wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") material = ifcopenshell.api.run("material.add_material_set", self.file, set_type="IfcMaterialLayerSet") - ifcopenshell.api.run("material.assign_material", self.file, product=wall, material=material) + ifcopenshell.api.run("material.assign_material", self.file, products=[wall], material=material) ifcopenshell.api.run("material.remove_material_set", self.file, material=material) assert len(self.file.by_type("IfcMaterialLayerSet")) == 0 assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 0 diff --git a/src/ifcopenshell-python/test/api/material/test_unassign_material.py b/src/ifcopenshell-python/test/api/material/test_unassign_material.py index aea9e78ddf..f1f280f5ad 100644 --- a/src/ifcopenshell-python/test/api/material/test_unassign_material.py +++ b/src/ifcopenshell-python/test/api/material/test_unassign_material.py @@ -21,16 +21,17 @@ import ifcopenshell.api import ifcopenshell.util.element -class TestUnassignMaterial(test.bootstrap.IFC4): +class TestUnassignMaterialIFC2X3(test.bootstrap.IFC2X3): def test_unassign_single_material(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") ifcopenshell.api.run( - "material.assign_material", self.file, product=element, type="IfcMaterial", material=material + "material.assign_material", self.file, products=[element1, element2], type="IfcMaterial", material=material ) - ifcopenshell.api.run("material.unassign_material", self.file, product=element) + ifcopenshell.api.run("material.unassign_material", self.file, products=[element1, element2]) assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 0 - assert len(self.file.by_type("IfcWall")) == 1 + assert len(self.file.by_type("IfcWall")) == 2 assert len(self.file.by_type("IfcMaterial")) == 1 def test_unassign_single_material_with_multiple_elements(self): @@ -38,12 +39,9 @@ class TestUnassignMaterial(test.bootstrap.IFC4): element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") ifcopenshell.api.run( - "material.assign_material", self.file, product=element1, type="IfcMaterial", material=material + "material.assign_material", self.file, products=[element1, element2], type="IfcMaterial", material=material ) - ifcopenshell.api.run( - "material.assign_material", self.file, product=element2, type="IfcMaterial", material=material - ) - ifcopenshell.api.run("material.unassign_material", self.file, product=element2) + ifcopenshell.api.run("material.unassign_material", self.file, products=[element2]) assert element1.HasAssociations assert not element2.HasAssociations assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 1 @@ -51,45 +49,72 @@ class TestUnassignMaterial(test.bootstrap.IFC4): assert len(self.file.by_type("IfcMaterial")) == 1 def test_unassign_material_layer_set_from_type(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") - ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialLayerSet") - ifcopenshell.api.run("material.unassign_material", self.file, product=element) + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element1, element2], type="IfcMaterialLayerSet" + ) + ifcopenshell.api.run("material.unassign_material", self.file, products=[element1, element2]) assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 0 - assert len(self.file.by_type("IfcWallType")) == 1 + assert len(self.file.by_type("IfcWallType")) == 2 assert len(self.file.by_type("IfcMaterialLayerSet")) == 1 def test_unassign_material_layer_set_usage_from_element(self): element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) - material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") - ifcopenshell.api.run("material.assign_material", self.file, product=element_type, type="IfcMaterialLayerSet") - ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialLayerSetUsage") - ifcopenshell.api.run("material.unassign_material", self.file, product=element) + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "type.assign_type", self.file, related_objects=[element1, element2], relating_type=element_type + ) + ifcopenshell.api.run("material.assign_material", self.file, products=[element_type], type="IfcMaterialLayerSet") + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element1, element2], type="IfcMaterialLayerSetUsage" + ) + ifcopenshell.api.run("material.unassign_material", self.file, products=[element1, element2]) assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 1 assert element_type.HasAssociations assert len(self.file.by_type("IfcWallType")) == 1 - assert len(self.file.by_type("IfcWall")) == 1 + assert len(self.file.by_type("IfcWall")) == 2 assert len(self.file.by_type("IfcMaterialLayerSet")) == 1 assert len(self.file.by_type("IfcMaterialLayerSetUsage")) == 0 + def test_unassign_material_layer_set_usage_shouldnt_remove_other_usages_of_the_type(self): + element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "type.assign_type", self.file, related_objects=[element1, element2], relating_type=element_type + ) + ifcopenshell.api.run("material.assign_material", self.file, products=[element_type], type="IfcMaterialLayerSet") + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element1], type="IfcMaterialLayerSetUsage" + ) + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element2], type="IfcMaterialLayerSetUsage" + ) + ifcopenshell.api.run("material.unassign_material", self.file, products=[element1]) + assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 2 + assert element_type.HasAssociations + assert len(self.file.by_type("IfcWallType")) == 1 + assert len(self.file.by_type("IfcWall")) == 2 + assert len(self.file.by_type("IfcMaterialLayerSet")) == 1 + assert len(self.file.by_type("IfcMaterialLayerSetUsage")) == 1 + def test_unassign_material_layer_set_usage_from_element_with_multiple_invalid_usages(self): element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) - ifcopenshell.api.run("type.assign_type", self.file, related_object=element2, relating_type=element_type) - material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") - ifcopenshell.api.run("material.assign_material", self.file, product=element_type, type="IfcMaterialLayerSet") + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element2], relating_type=element_type) + ifcopenshell.api.run("material.assign_material", self.file, products=[element_type], type="IfcMaterialLayerSet") rel = ifcopenshell.api.run( - "material.assign_material", self.file, product=element, type="IfcMaterialLayerSetUsage" + "material.assign_material", self.file, products=[element], type="IfcMaterialLayerSetUsage" ) # In some invalid IFCs from Revit, they reuse usages. Let's recreate this invalid scenario rel.RelatedObjects = list(rel.RelatedObjects) + [element2] - ifcopenshell.api.run("material.unassign_material", self.file, product=element) + ifcopenshell.api.run("material.unassign_material", self.file, products=[element]) assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 2 for rel in self.file.by_type("IfcRelAssociatesMaterial"): @@ -102,46 +127,72 @@ class TestUnassignMaterial(test.bootstrap.IFC4): assert ifcopenshell.util.element.get_material(element, should_inherit=False) is None assert ifcopenshell.util.element.get_material(element2, should_inherit=False).is_a("IfcMaterialLayerSetUsage") - def test_unassign_material_profile_set_from_type(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") - ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialProfileSet") - ifcopenshell.api.run("material.unassign_material", self.file, product=element) + def test_unassign_element_material_list(self): + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + material1 = ifcopenshell.api.run("material.add_material", self.file, name="CON01") + ifcopenshell.api.run( + "material.assign_material", + self.file, + products=[element1, element2], + type="IfcMaterialList", + material=material1, + ) + element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + material3 = ifcopenshell.api.run("material.add_material", self.file, name="CON01") + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element3], type="IfcMaterialList", material=material3 + ) + ifcopenshell.api.run("material.unassign_material", self.file, products=[element1, element2, element3]) assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 0 - assert len(self.file.by_type("IfcWallType")) == 1 - assert len(self.file.by_type("IfcMaterialProfileSet")) == 1 + assert len(self.file.by_type("IfcMaterialList")) == 2 + assert len(self.file.by_type("IfcMaterial")) == 2 + + +class TestUnassignMaterialIFC4(test.bootstrap.IFC4, TestUnassignMaterialIFC2X3): + + def test_unassign_material_profile_set_from_type(self): + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + ifcopenshell.api.run("material.assign_material", self.file, products=[element1], type="IfcMaterialProfileSet") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + ifcopenshell.api.run("material.assign_material", self.file, products=[element2], type="IfcMaterialProfileSet") + ifcopenshell.api.run("material.unassign_material", self.file, products=[element1, element2]) + assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 0 + assert len(self.file.by_type("IfcWallType")) == 2 + assert len(self.file.by_type("IfcMaterialProfileSet")) == 2 def test_unassign_material_profile_set_usage_from_element(self): element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) - material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") - ifcopenshell.api.run("material.assign_material", self.file, product=element_type, type="IfcMaterialProfileSet") - ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialProfileSetUsage") - ifcopenshell.api.run("material.unassign_material", self.file, product=element) + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "type.assign_type", self.file, related_objects=[element1, element2], relating_type=element_type + ) + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element_type], type="IfcMaterialProfileSet" + ) + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element1, element2], type="IfcMaterialProfileSetUsage" + ) + ifcopenshell.api.run("material.unassign_material", self.file, products=[element1, element2]) assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 1 assert element_type.HasAssociations assert len(self.file.by_type("IfcWallType")) == 1 - assert len(self.file.by_type("IfcWall")) == 1 + assert len(self.file.by_type("IfcWall")) == 2 assert len(self.file.by_type("IfcMaterialProfileSet")) == 1 assert len(self.file.by_type("IfcMaterialProfileSetUsage")) == 0 def test_unassign_material_constituent_set_from_type(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") - ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialConstituentSet") - ifcopenshell.api.run("material.unassign_material", self.file, product=element) - assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 0 - assert len(self.file.by_type("IfcWallType")) == 1 - assert len(self.file.by_type("IfcMaterialConstituentSet")) == 1 - - def test_unassign_element_material_list(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") ifcopenshell.api.run( - "material.assign_material", self.file, product=element, type="IfcMaterialList", material=material + "material.assign_material", self.file, products=[element1, element2], type="IfcMaterialConstituentSet" ) - ifcopenshell.api.run("material.unassign_material", self.file, product=element) + element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element3], type="IfcMaterialConstituentSet" + ) + ifcopenshell.api.run("material.unassign_material", self.file, products=[element1, element2, element3]) assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 0 - assert len(self.file.by_type("IfcMaterialList")) == 1 - assert len(self.file.by_type("IfcMaterial")) == 1 + assert len(self.file.by_type("IfcWallType")) == 3 + assert len(self.file.by_type("IfcMaterialConstituentSet")) == 2 diff --git a/src/ifcopenshell-python/test/api/nest/__init__.py b/src/ifcopenshell-python/test/api/nest/__init__.py new file mode 100644 index 0000000000..8bed51a56f --- /dev/null +++ b/src/ifcopenshell-python/test/api/nest/__init__.py @@ -0,0 +1,17 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . diff --git a/src/ifcopenshell-python/test/api/nest/test_assign_object.py b/src/ifcopenshell-python/test/api/nest/test_assign_object.py new file mode 100644 index 0000000000..25d4259627 --- /dev/null +++ b/src/ifcopenshell-python/test/api/nest/test_assign_object.py @@ -0,0 +1,71 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import pytest +import test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.element +import ifcopenshell.util.placement + + +class TestAssignObject(test.bootstrap.IFC4): + def test_assigning_a_nesting(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + rel = ifcopenshell.api.run( + "nest.assign_object", self.file, related_objects=[subelement1, subelement2], relating_object=element + ) + assert ifcopenshell.util.element.get_nest(subelement1) == element + assert ifcopenshell.util.element.get_nest(subelement2) == element + assert rel.is_a("IfcRelNests") + + def test_doing_nothing_if_the_nesting_is_already_assigned(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement], relating_object=element) + total_elements = len([e for e in self.file]) + ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement], relating_object=element) + assert len([e for e in self.file]) == total_elements + + def test_that_old_nesting_relationships_are_updated_if_they_still_have_elements(self): + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + ifcopenshell.api.run( + "nest.assign_object", self.file, related_objects=[subelement1, subelement2], relating_object=element1 + ) + rel = subelement1.Decomposes[0] if self.file.schema == "IFC2X3" else subelement1.Nests[0] + assert len(rel.RelatedObjects) == 2 + ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement1], relating_object=element2) + assert len(rel.RelatedObjects) == 1 + + def test_that_old_nesting_relationships_are_purged_if_no_more_elements_are_nested(self): + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement1], relating_object=element1) + rel_id = (subelement1.Decomposes[0] if self.file.schema == "IFC2X3" else subelement1.Nests[0]).id() + ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement1], relating_object=element2) + with pytest.raises(RuntimeError): + self.file.by_id(rel_id) + + +class TestAssignObjectIFC2X3(test.bootstrap.IFC2X3, TestAssignObject): + pass diff --git a/src/ifcopenshell-python/test/api/nest/test_unassign_object.py b/src/ifcopenshell-python/test/api/nest/test_unassign_object.py new file mode 100644 index 0000000000..479ab0bcce --- /dev/null +++ b/src/ifcopenshell-python/test/api/nest/test_unassign_object.py @@ -0,0 +1,54 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import pytest +import test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.element + + +class TestUnassignObject(test.bootstrap.IFC4): + def test_unassigning_an_object(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + ifcopenshell.api.run( + "nest.assign_object", self.file, related_objects=[subelement1, subelement2], relating_object=element + ) + ifcopenshell.api.run("nest.unassign_object", self.file, related_objects=[subelement1, subelement2]) + assert ifcopenshell.util.element.get_nest(subelement1) is None + assert ifcopenshell.util.element.get_nest(subelement2) is None + + def test_the_rel_is_kept_if_there_are_more_nested_elements(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement1], relating_object=element) + ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement2], relating_object=element) + ifcopenshell.api.run("nest.unassign_object", self.file, related_objects=[subelement1]) + assert len(self.file.by_type("IfcRelNests")) == 1 + + def test_the_rel_is_purged_if_there_are_no_more_nested_elements(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement], relating_object=element) + ifcopenshell.api.run("nest.unassign_object", self.file, related_objects=[subelement]) + assert len(self.file.by_type("IfcRelNests")) == 0 + +class TestUnassignObjectIFC2X3(test.bootstrap.IFC2X3, TestUnassignObject): + pass diff --git a/src/ifcopenshell-python/test/api/project/test_append_asset.py b/src/ifcopenshell-python/test/api/project/test_append_asset.py index 3e03be43d5..84444218d6 100644 --- a/src/ifcopenshell-python/test/api/project/test_append_asset.py +++ b/src/ifcopenshell-python/test/api/project/test_append_asset.py @@ -18,6 +18,7 @@ import test.bootstrap import ifcopenshell.api +import ifcopenshell.util.element class TestAppendAsset(test.bootstrap.IFC4): @@ -69,8 +70,8 @@ class TestAppendAsset(test.bootstrap.IFC4): element = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWallType") element2 = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWallType") material = ifcopenshell.api.run("material.add_material", library, name="Material") - ifcopenshell.api.run("material.assign_material", library, product=element, material=material) - ifcopenshell.api.run("material.assign_material", library, product=element2, material=material) + ifcopenshell.api.run("material.assign_material", library, products=[element], material=material) + ifcopenshell.api.run("material.assign_material", library, products=[element2], material=material) ifcopenshell.api.run("project.append_asset", self.file, library=library, element=element) assert len(self.file.by_type("IfcWallType")) == 1 @@ -78,7 +79,7 @@ class TestAppendAsset(test.bootstrap.IFC4): library = ifcopenshell.api.run("project.create_file") element = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWallType") material = ifcopenshell.api.run("material.add_material", library, name="Material") - ifcopenshell.api.run("material.assign_material", library, product=element, material=material) + ifcopenshell.api.run("material.assign_material", library, products=[element], material=material) ifcopenshell.api.run("project.append_asset", self.file, library=library, element=element) assert self.file.by_type("IfcWallType")[0].HasAssociations[0].RelatingMaterial.Name == "Material" @@ -87,11 +88,11 @@ class TestAppendAsset(test.bootstrap.IFC4): element = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWallType") element_type2 = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", library, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", library, related_objects=[element], relating_type=element_type) material = ifcopenshell.api.run("material.add_material", library, name="Material") - ifcopenshell.api.run("material.assign_material", library, product=element, material=material) - ifcopenshell.api.run("material.assign_material", library, product=element_type, material=material) - ifcopenshell.api.run("material.assign_material", library, product=element_type2, material=material) + ifcopenshell.api.run("material.assign_material", library, products=[element], material=material) + ifcopenshell.api.run("material.assign_material", library, products=[element_type], material=material) + ifcopenshell.api.run("material.assign_material", library, products=[element_type2], material=material) # appending another type not connected to IfcWall directly ifcopenshell.api.run("project.append_asset", self.file, library=library, element=element_type2) @@ -110,8 +111,8 @@ class TestAppendAsset(test.bootstrap.IFC4): pset = ifcopenshell.api.run("pset.add_pset", library, product=material, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", library, pset=pset, properties={"Foo": "Bar"}) - ifcopenshell.api.run("material.assign_material", library, product=element1, material=material) - ifcopenshell.api.run("material.assign_material", library, product=element2, material=material) + ifcopenshell.api.run("material.assign_material", library, products=[element1], material=material) + ifcopenshell.api.run("material.assign_material", library, products=[element2], material=material) new1 = ifcopenshell.api.run("project.append_asset", self.file, library=library, element=element1) new2 = ifcopenshell.api.run("project.append_asset", self.file, library=library, element=element2) @@ -138,8 +139,8 @@ class TestAppendAsset(test.bootstrap.IFC4): ifcopenshell.api.run("material.add_layer", library, layer_set=layer_set2, material=material) ifcopenshell.api.run("material.add_layer", library, layer_set=layer_set2, material=material) - ifcopenshell.api.run("material.assign_material", library, product=element1, material=layer_set1) - ifcopenshell.api.run("material.assign_material", library, product=element2, material=layer_set2) + ifcopenshell.api.run("material.assign_material", library, products=[element1], material=layer_set1) + ifcopenshell.api.run("material.assign_material", library, products=[element2], material=layer_set2) new1 = ifcopenshell.api.run("project.append_asset", self.file, library=library, element=element1) new2 = ifcopenshell.api.run("project.append_asset", self.file, library=library, element=element2) @@ -159,7 +160,7 @@ class TestAppendAsset(test.bootstrap.IFC4): element.OwnerHistory = history material = ifcopenshell.api.run("material.add_material", library, name="Material") - rel = ifcopenshell.api.run("material.assign_material", library, product=element, material=material) + rel = ifcopenshell.api.run("material.assign_material", library, products=[element], material=material) # We share a history. This ensures that we continue to check all # whitelisted inverses even though one of the subelements (i.e. this # shared history) is already processed. See bug #2837. @@ -185,7 +186,7 @@ class TestAppendAsset(test.bootstrap.IFC4): element_type.RepresentationMaps = [library.createIfcRepresentationMap(MappedRepresentation=mapped_rep)] element = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWall") - ifcopenshell.api.run("type.assign_type", library, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", library, related_objects=[element], relating_type=element_type) ifcopenshell.api.run("project.append_asset", self.file, library=library, element=element) assert len(self.file.by_type("IfcStyledItem")) == 1 @@ -200,7 +201,7 @@ class TestAppendAsset(test.bootstrap.IFC4): element = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWallType") material = ifcopenshell.api.run("material.add_material", library, name="Material") - ifcopenshell.api.run("material.assign_material", library, product=element, material=material) + ifcopenshell.api.run("material.assign_material", library, products=[element], material=material) style = ifcopenshell.api.run("style.add_style", library) ifcopenshell.api.run("style.assign_material_style", library, material=material, style=style, context=context) ifcopenshell.api.run("project.append_asset", self.file, library=library, element=element) @@ -355,7 +356,7 @@ class TestAppendAsset(test.bootstrap.IFC4): library = ifcopenshell.api.run("project.create_file") element = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", library, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", library, related_objects=[element], relating_type=element_type) ifcopenshell.api.run("project.append_asset", self.file, library=library, element=element) assert ifcopenshell.util.element.get_type(self.file.by_type("IfcWall")[0]).is_a("IfcWallType") @@ -365,9 +366,9 @@ class TestAppendAsset(test.bootstrap.IFC4): element2 = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWall") element3 = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", library, related_object=element, relating_type=element_type) - ifcopenshell.api.run("type.assign_type", library, related_object=element2, relating_type=element_type) - ifcopenshell.api.run("type.assign_type", library, related_object=element3, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", library, related_objects=[element], relating_type=element_type) + ifcopenshell.api.run("type.assign_type", library, related_objects=[element2], relating_type=element_type) + ifcopenshell.api.run("type.assign_type", library, related_objects=[element3], relating_type=element_type) ifcopenshell.api.run("project.append_asset", self.file, library=library, element=element) ifcopenshell.api.run("project.append_asset", self.file, library=library, element=element2) assert len(ifcopenshell.util.element.get_types(self.file.by_type("IfcWallType")[0])) == 2 @@ -377,7 +378,7 @@ class TestAppendAsset(test.bootstrap.IFC4): library = ifcopenshell.api.run("project.create_file") element = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWall") material = ifcopenshell.api.run("material.add_material", library, name="Material") - ifcopenshell.api.run("material.assign_material", library, product=element, material=material) + ifcopenshell.api.run("material.assign_material", library, products=[element], material=material) ifcopenshell.api.run("project.append_asset", self.file, library=library, element=element) assert ifcopenshell.util.element.get_material(self.file.by_type("IfcWall")[0]).Name == "Material" @@ -386,11 +387,11 @@ class TestAppendAsset(test.bootstrap.IFC4): element = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWallType") element_type2 = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", library, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", library, related_objects=[element], relating_type=element_type) material = ifcopenshell.api.run("material.add_material", library, name="Material") - ifcopenshell.api.run("material.assign_material", library, product=element, material=material) - ifcopenshell.api.run("material.assign_material", library, product=element_type, material=material) - ifcopenshell.api.run("material.assign_material", library, product=element_type2, material=material) + ifcopenshell.api.run("material.assign_material", library, products=[element], material=material) + ifcopenshell.api.run("material.assign_material", library, products=[element_type], material=material) + ifcopenshell.api.run("material.assign_material", library, products=[element_type2], material=material) ifcopenshell.api.run("project.append_asset", self.file, library=library, element=element) assert [e.GlobalId for e in self.file.by_type("IfcWallType")] == [element_type.GlobalId] @@ -418,7 +419,7 @@ class TestAppendAssetIFC2X3(test.bootstrap.IFC2X3): library = ifcopenshell.api.run("project.create_file", version="IFC2X3") element = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", library, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", library, related_objects=[element], relating_type=element_type) ifcopenshell.api.run("project.append_asset", self.file, library=library, element=element) assert ifcopenshell.util.element.get_type(self.file.by_type("IfcWall")[0]).is_a("IfcWallType") @@ -428,9 +429,9 @@ class TestAppendAssetIFC2X3(test.bootstrap.IFC2X3): element2 = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWall") element3 = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", library, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", library, related_object=element, relating_type=element_type) - ifcopenshell.api.run("type.assign_type", library, related_object=element2, relating_type=element_type) - ifcopenshell.api.run("type.assign_type", library, related_object=element3, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", library, related_objects=[element], relating_type=element_type) + ifcopenshell.api.run("type.assign_type", library, related_objects=[element2], relating_type=element_type) + ifcopenshell.api.run("type.assign_type", library, related_objects=[element3], relating_type=element_type) ifcopenshell.api.run("project.append_asset", self.file, library=library, element=element) ifcopenshell.api.run("project.append_asset", self.file, library=library, element=element2) assert len(ifcopenshell.util.element.get_types(self.file.by_type("IfcWallType")[0])) == 2 diff --git a/src/ifcopenshell-python/test/api/pset/test_add_pset.py b/src/ifcopenshell-python/test/api/pset/test_add_pset.py index 3170b53abd..9d18e16ee2 100644 --- a/src/ifcopenshell-python/test/api/pset/test_add_pset.py +++ b/src/ifcopenshell-python/test/api/pset/test_add_pset.py @@ -18,6 +18,7 @@ import test.bootstrap import ifcopenshell.api +import ifcopenshell.util.element class TestAddPset(test.bootstrap.IFC4): diff --git a/src/ifcopenshell-python/test/api/pset/test_add_qto.py b/src/ifcopenshell-python/test/api/pset/test_add_qto.py index d4cbd74a5c..3afb847c89 100644 --- a/src/ifcopenshell-python/test/api/pset/test_add_qto.py +++ b/src/ifcopenshell-python/test/api/pset/test_add_qto.py @@ -18,6 +18,7 @@ import test.bootstrap import ifcopenshell.api +import ifcopenshell.util.element class TestAddQto(test.bootstrap.IFC4): diff --git a/src/ifcopenshell-python/test/api/root/test_copy_class.py b/src/ifcopenshell-python/test/api/root/test_copy_class.py index 36965258d4..25e2b06345 100644 --- a/src/ifcopenshell-python/test/api/root/test_copy_class.py +++ b/src/ifcopenshell-python/test/api/root/test_copy_class.py @@ -19,6 +19,8 @@ import numpy import test.bootstrap import ifcopenshell.api +import ifcopenshell.util +import ifcopenshell.util.system class TestCopyClass(test.bootstrap.IFC4): @@ -34,7 +36,7 @@ class TestCopyClass(test.bootstrap.IFC4): ifcopenshell.api.run("unit.assign_unit", self.file) element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=element) matrix = numpy.identity(4) ifcopenshell.api.run("geometry.edit_object_placement", self.file, product=element, matrix=matrix.copy()) ifcopenshell.api.run("geometry.edit_object_placement", self.file, product=subelement, matrix=matrix.copy()) @@ -74,7 +76,7 @@ class TestCopyClass(test.bootstrap.IFC4): def test_copying_a_container_only_and_not_its_contents(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=element) new = ifcopenshell.api.run("root.copy_class", self.file, product=element) assert element.ContainsElements assert not new.ContainsElements @@ -82,14 +84,14 @@ class TestCopyClass(test.bootstrap.IFC4): def test_copying_contents_of_a_container_and_maintaining_the_containment_relationship(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=element) new = ifcopenshell.api.run("root.copy_class", self.file, product=subelement) assert new.ContainedInStructure[0].RelatingStructure == element def test_copying_a_container_only_and_not_its_decomposition(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuildingStorey") - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement], relating_object=element) new = ifcopenshell.api.run("root.copy_class", self.file, product=element) assert element.IsDecomposedBy assert not new.IsDecomposedBy @@ -97,7 +99,7 @@ class TestCopyClass(test.bootstrap.IFC4): def test_copying_an_aggregate_only_and_not_its_decomposition(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcElementAssembly") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBeam") - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement], relating_object=element) new = ifcopenshell.api.run("root.copy_class", self.file, product=element) assert element.IsDecomposedBy assert not new.IsDecomposedBy @@ -105,7 +107,7 @@ class TestCopyClass(test.bootstrap.IFC4): def test_copying_an_aggregate_decomposition_and_maintaining_the_aggregate_relationship(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcElementAssembly") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBeam") - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement], relating_object=element) new = ifcopenshell.api.run("root.copy_class", self.file, product=subelement) assert new.Decomposes[0].RelatingObject == element @@ -196,13 +198,19 @@ class TestCopyClass(test.bootstrap.IFC4): new = ifcopenshell.api.run("root.copy_class", self.file, product=element) assert new.HasAssociations[0].RelatingMaterial.is_a("IfcMaterialLayerSet") assert new.HasAssociations[0].RelatingMaterial != element.HasAssociations[0].RelatingMaterial - assert new.HasAssociations[0].RelatingMaterial.MaterialLayers[0] != element.HasAssociations[0].RelatingMaterial.MaterialLayers[0] - assert new.HasAssociations[0].RelatingMaterial.MaterialLayers[0].Material == element.HasAssociations[0].RelatingMaterial.MaterialLayers[0].Material + assert ( + new.HasAssociations[0].RelatingMaterial.MaterialLayers[0] + != element.HasAssociations[0].RelatingMaterial.MaterialLayers[0] + ) + assert ( + new.HasAssociations[0].RelatingMaterial.MaterialLayers[0].Material + == element.HasAssociations[0].RelatingMaterial.MaterialLayers[0].Material + ) def test_copying_a_type_and_purging_type_relationships(self): type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=type) new = ifcopenshell.api.run("root.copy_class", self.file, product=type) assert not new.Types diff --git a/src/ifcopenshell-python/test/api/root/test_reassign_class.py b/src/ifcopenshell-python/test/api/root/test_reassign_class.py index fc9a7f4b2b..17b3fd1801 100644 --- a/src/ifcopenshell-python/test/api/root/test_reassign_class.py +++ b/src/ifcopenshell-python/test/api/root/test_reassign_class.py @@ -54,9 +54,9 @@ class TestReassignClass(test.bootstrap.IFC4): def test_reassign_class_for_type_occurrences(self): element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element1, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element1], relating_type=element_type) element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element2, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element2], relating_type=element_type) element_type = ifcopenshell.api.run( "root.reassign_class", self.file, product=element_type, ifc_class="IfcSlabType" @@ -74,9 +74,9 @@ class TestReassignClass(test.bootstrap.IFC4): def test_reassigning_type_class_and_its_occurrences_classes_if_entity_was_typed(self): element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element1, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element1], relating_type=element_type) element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element2, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element2], relating_type=element_type) element1 = ifcopenshell.api.run("root.reassign_class", self.file, product=element1, ifc_class="IfcSlab") diff --git a/src/ifcopenshell-python/test/api/root/test_remove_product.py b/src/ifcopenshell-python/test/api/root/test_remove_product.py index e221651525..ab2d2cf568 100644 --- a/src/ifcopenshell-python/test/api/root/test_remove_product.py +++ b/src/ifcopenshell-python/test/api/root/test_remove_product.py @@ -98,7 +98,7 @@ class TestRemoveProduct(test.bootstrap.IFC4): context = ifcopenshell.api.run("context.add_context", self.file, context_type="Model") element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) rep_map = self.file.createIfcRepresentationMap( MappingOrigin=self.file.createIfcAxis2Placement3D(), MappedRepresentation=self.file.createIfcShapeRepresentation( @@ -190,7 +190,7 @@ class TestRemoveProduct(test.bootstrap.IFC4): def test_removing_all_nesting_relationships_of_a_whole(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBeam") - ifcopenshell.api.run("nest.assign_object", self.file, related_object=subelement, relating_object=element) + ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement], relating_object=element) total_entities = len(list(self.file)) ifcopenshell.api.run("root.remove_product", self.file, product=element) assert len(list(self.file)) == total_entities - 2 @@ -201,7 +201,7 @@ class TestRemoveProduct(test.bootstrap.IFC4): def test_removing_all_nesting_relationships_of_a_part(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBeam") - ifcopenshell.api.run("nest.assign_object", self.file, related_object=subelement, relating_object=element) + ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement], relating_object=element) total_entities = len(list(self.file)) ifcopenshell.api.run("root.remove_product", self.file, product=subelement) assert len(list(self.file)) == total_entities - 2 @@ -212,7 +212,7 @@ class TestRemoveProduct(test.bootstrap.IFC4): def test_removing_all_aggregate_relationships_of_a_whole(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcElementAssembly") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBeam") - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement], relating_object=element) total_entities = len(list(self.file)) ifcopenshell.api.run("root.remove_product", self.file, product=element) assert len(list(self.file)) == total_entities - 2 @@ -223,7 +223,7 @@ class TestRemoveProduct(test.bootstrap.IFC4): def test_removing_all_aggregate_relationships_of_a_part(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcElementAssembly") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBeam") - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement], relating_object=element) total_entities = len(list(self.file)) ifcopenshell.api.run("root.remove_product", self.file, product=subelement) assert len(list(self.file)) == total_entities - 2 @@ -234,7 +234,7 @@ class TestRemoveProduct(test.bootstrap.IFC4): def test_removing_all_containment_relationships_of_a_container(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSpace") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=element) total_entities = len(list(self.file)) ifcopenshell.api.run("root.remove_product", self.file, product=element) assert len(list(self.file)) == total_entities - 2 @@ -245,7 +245,7 @@ class TestRemoveProduct(test.bootstrap.IFC4): def test_removing_all_containment_relationships_of_an_element(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSpace") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=element) total_entities = len(list(self.file)) ifcopenshell.api.run("root.remove_product", self.file, product=subelement) assert len(list(self.file)) == total_entities - 2 @@ -369,7 +369,7 @@ class TestRemoveProduct(test.bootstrap.IFC4): def test_removing_all_material_relationships_of_an_element(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") material = ifcopenshell.api.run("material.add_material", self.file, name="Foo") - ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material) + ifcopenshell.api.run("material.assign_material", self.file, products=[element], material=material) ifcopenshell.api.run("root.remove_product", self.file, product=element) assert not self.file.by_type("IfcRelAssociatesMaterial") assert self.file.by_type("IfcMaterial") @@ -377,7 +377,7 @@ class TestRemoveProduct(test.bootstrap.IFC4): def test_removing_all_type_relationships_of_an_element(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) ifcopenshell.api.run("root.remove_product", self.file, product=element) assert not self.file.by_type("IfcRelDefinesByType") assert self.file.by_type("IfcWallType") @@ -386,8 +386,8 @@ class TestRemoveProduct(test.bootstrap.IFC4): element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element1, relating_type=element_type) - ifcopenshell.api.run("type.assign_type", self.file, related_object=element2, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element1], relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element2], relating_type=element_type) ifcopenshell.api.run("root.remove_product", self.file, product=element_type) assert not self.file.by_type("IfcRelDefinesByType") assert self.file.by_type("IfcWall") diff --git a/src/ifcopenshell-python/test/api/sequence/test_edit_work_time.py b/src/ifcopenshell-python/test/api/sequence/test_edit_work_time.py index d91abe2a31..d200116878 100644 --- a/src/ifcopenshell-python/test/api/sequence/test_edit_work_time.py +++ b/src/ifcopenshell-python/test/api/sequence/test_edit_work_time.py @@ -19,7 +19,6 @@ import datetime import test.bootstrap import ifcopenshell.api -from ifcopenshell.util.data import WorkTimeDatesInterface class TestEditWorkTime(test.bootstrap.IFC4): @@ -35,13 +34,12 @@ class TestEditWorkTime(test.bootstrap.IFC4): "Finish": datetime.datetime(2020, 2, 1), } ifcopenshell.api.run("sequence.edit_work_time", self.file, work_time=work_time, attributes=attributes) - worktime_dates = WorkTimeDatesInterface(work_time) assert work_time.Name == attributes["Name"] assert work_time.DataOrigin == attributes["DataOrigin"] assert work_time.UserDefinedDataOrigin == attributes["UserDefinedDataOrigin"] assert work_time.RecurrencePattern == attributes["RecurrencePattern"] - assert worktime_dates.Start == "2020-01-01" - assert worktime_dates.Finish == "2020-02-01" + assert work_time[4] == "2020-01-01" + assert work_time[5] == "2020-02-01" class TestEditWorkTimeIFC4X3(test.bootstrap.IFC4X3): diff --git a/src/ifcopenshell-python/test/api/spatial/test_assign_container.py b/src/ifcopenshell-python/test/api/spatial/test_assign_container.py index 11f1e03401..533db20267 100644 --- a/src/ifcopenshell-python/test/api/spatial/test_assign_container.py +++ b/src/ifcopenshell-python/test/api/spatial/test_assign_container.py @@ -28,18 +28,20 @@ class TestAssignContainer(test.bootstrap.IFC4): def test_assigning_a_container(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") rel = ifcopenshell.api.run( - "spatial.assign_container", self.file, product=subelement, relating_structure=element + "spatial.assign_container", self.file, products=[subelement, subelement2], relating_structure=element ) assert ifcopenshell.util.element.get_container(subelement) == element + assert ifcopenshell.util.element.get_container(subelement2) == element assert rel.is_a("IfcRelContainedInSpatialStructure") def test_doing_nothing_if_the_container_is_already_assigned(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=element) total_elements = len([e for e in self.file]) - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=element) assert len([e for e in self.file]) == total_elements def test_that_old_containment_relationships_are_updated_if_they_still_contain_elements(self): @@ -47,20 +49,20 @@ class TestAssignContainer(test.bootstrap.IFC4): element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement1, relating_structure=element1) - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement2, relating_structure=element1) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement1], relating_structure=element1) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement2], relating_structure=element1) rel = subelement1.ContainedInStructure[0] assert len(rel.RelatedElements) == 2 - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement1, relating_structure=element2) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement1], relating_structure=element2) assert len(rel.RelatedElements) == 1 def test_that_old_containment_relationships_are_purged_if_no_more_elements_are_contained(self): element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement1, relating_structure=element1) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement1], relating_structure=element1) rel_id = subelement1.ContainedInStructure[0].id() - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement1, relating_structure=element2) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement1], relating_structure=element2) with pytest.raises(RuntimeError): self.file.by_id(rel_id) @@ -70,7 +72,7 @@ class TestAssignContainer(test.bootstrap.IFC4): element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element1) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=element1) matrix1 = numpy.array( ( (1.0, 0.0, 0.0, 1.0), @@ -96,7 +98,7 @@ class TestAssignContainer(test.bootstrap.IFC4): ifcopenshell.api.run( "geometry.edit_object_placement", self.file, product=subelement, matrix=matrix1.copy(), is_si=False ) - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element2) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=element2) assert subelement.ObjectPlacement.PlacementRelTo.PlacesObject[0] == element2 assert numpy.array_equal(ifcopenshell.util.placement.get_local_placement(subelement.ObjectPlacement), matrix1) @@ -107,7 +109,7 @@ class TestAssignContainer(test.bootstrap.IFC4): subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") placement = self.file.createIfcGridPlacement() subelement.ObjectPlacement = placement - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=element) assert subelement.ObjectPlacement == placement def test_removing_aggregation_if_it_exists(self): @@ -116,6 +118,6 @@ class TestAssignContainer(test.bootstrap.IFC4): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") aggregate = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcElementAssembly") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=aggregate) - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement], relating_object=aggregate) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=element) assert not ifcopenshell.util.element.get_aggregate(subelement) diff --git a/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py b/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py index 1cf2f3108e..474f8bbcaf 100644 --- a/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py +++ b/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py @@ -26,28 +26,42 @@ class TestDereferenceStructure(test.bootstrap.IFC4): def test_removing_a_container(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement, relating_structure=element) - ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement, relating_structure=element) + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "spatial.reference_structure", self.file, products=[subelement, subelement2], relating_structure=element + ) + ifcopenshell.api.run( + "spatial.dereference_structure", self.file, products=[subelement, subelement2], relating_structure=element + ) assert ifcopenshell.util.element.get_referenced_structures(subelement) == [] + assert len(self.file.by_type("IfcRelReferencedInSpatialStructure")) == 0 def test_doing_nothing_if_no_container(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement, relating_structure=element) + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "spatial.dereference_structure", self.file, products=[subelement, subelement2], relating_structure=element + ) assert ifcopenshell.util.element.get_referenced_structures(subelement) == [] + assert ifcopenshell.util.element.get_referenced_structures(subelement2) == [] def test_updating_the_rel_when_a_reference_is_removed_with_multipled_elements(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement1, relating_structure=element) - ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement2, relating_structure=element) - ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement1, relating_structure=element) - assert self.file.by_type("IfcRelReferencedInSpatialStructure")[0].RelatedElements == (subelement2,) + subelement3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "spatial.reference_structure", self.file, products=[subelement1], relating_structure=element + ) + ifcopenshell.api.run( + "spatial.reference_structure", self.file, products=[subelement2, subelement3], relating_structure=element + ) + ifcopenshell.api.run( + "spatial.dereference_structure", self.file, products=[subelement1, subelement2], relating_structure=element + ) + assert element.ReferencesElements[0].RelatedElements == (subelement3,) - def test_deleting_the_rel_when_a_container_is_removed_with_no_elements(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement, relating_structure=element) - ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement, relating_structure=element) - assert len(self.file.by_type("IfcRelReferencedInSpatialStructure")) == 0 + +class TestDereferenceStructureIFC2X3(test.bootstrap.IFC2X3, TestDereferenceStructure): + pass diff --git a/src/ifcopenshell-python/test/api/spatial/test_reference_structure.py b/src/ifcopenshell-python/test/api/spatial/test_reference_structure.py index 21b6b0e8e8..0ef22d708a 100644 --- a/src/ifcopenshell-python/test/api/spatial/test_reference_structure.py +++ b/src/ifcopenshell-python/test/api/spatial/test_reference_structure.py @@ -26,25 +26,39 @@ class TestReferenceStructure(test.bootstrap.IFC4): def test_referencing_a_structure(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - rel = ifcopenshell.api.run( - "spatial.reference_structure", self.file, product=subelement, relating_structure=element + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "spatial.reference_structure", self.file, products=[subelement, subelement2], relating_structure=element ) - assert ifcopenshell.util.element.get_referenced_structures(subelement) == [element] - assert rel.is_a("IfcRelReferencedInSpatialStructure") + assert ifcopenshell.util.element.get_structure_referenced_elements(element) == {subelement, subelement2} def test_doing_nothing_if_the_structure_is_already_referenced(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement, relating_structure=element) + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "spatial.reference_structure", self.file, products=[subelement, subelement2], relating_structure=element + ) total_elements = len([e for e in self.file]) - ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement, relating_structure=element) + ifcopenshell.api.run( + "spatial.reference_structure", self.file, products=[subelement, subelement2], relating_structure=element + ) assert len([e for e in self.file]) == total_elements def test_that_old_relationships_are_updated_if_they_still_contain_elements(self): - element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "spatial.reference_structure", self.file, products=[subelement1], relating_structure=element + ) subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement1, relating_structure=element1) - ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement2, relating_structure=element1) + subelement3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "spatial.reference_structure", self.file, products=[subelement2, subelement3], relating_structure=element + ) rel = subelement1.ReferencedInStructures[0] - assert len(rel.RelatedElements) == 2 + assert len(rel.RelatedElements) == 3 + + +class TestReferenceStructureIFC2X3(test.bootstrap.IFC2X3, TestReferenceStructure): + pass diff --git a/src/ifcopenshell-python/test/api/spatial/test_remove_container.py b/src/ifcopenshell-python/test/api/spatial/test_remove_container.py deleted file mode 100644 index 84e26ed3a1..0000000000 --- a/src/ifcopenshell-python/test/api/spatial/test_remove_container.py +++ /dev/null @@ -1,54 +0,0 @@ -# IfcOpenShell - IFC toolkit and geometry engine -# Copyright (C) 2021 Dion Moult -# -# This file is part of IfcOpenShell. -# -# IfcOpenShell is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# IfcOpenShell is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with IfcOpenShell. If not, see . - -import numpy -import pytest -import test.bootstrap -import ifcopenshell.api -import ifcopenshell.util.element -import ifcopenshell.util.placement - - -class TestRemoveContainer(test.bootstrap.IFC4): - def test_removing_a_container(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element) - ifcopenshell.api.run("spatial.remove_container", self.file, product=subelement) - assert ifcopenshell.util.element.get_container(subelement) is None - - def test_doing_nothing_if_no_container(self): - subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.remove_container", self.file, product=subelement) - assert ifcopenshell.util.element.get_container(subelement) is None - - def test_updating_the_rel_when_a_container_is_removed_with_multipled_elements(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement1, relating_structure=element) - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement2, relating_structure=element) - ifcopenshell.api.run("spatial.remove_container", self.file, product=subelement1) - assert self.file.by_type("IfcRelContainedInSpatialStructure")[0].RelatedElements == (subelement2,) - - def test_deleting_the_rel_when_a_container_is_removed_with_no_elements(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element) - ifcopenshell.api.run("spatial.remove_container", self.file, product=subelement) - assert len(self.file.by_type("IfcRelContainedInSpatialStructure")) == 0 diff --git a/src/ifcopenshell-python/test/api/spatial/test_unassign_container.py b/src/ifcopenshell-python/test/api/spatial/test_unassign_container.py index df65897ab2..033b5cd333 100644 --- a/src/ifcopenshell-python/test/api/spatial/test_unassign_container.py +++ b/src/ifcopenshell-python/test/api/spatial/test_unassign_container.py @@ -24,20 +24,36 @@ import ifcopenshell.util.element import ifcopenshell.util.placement -class TestAssignContainer(test.bootstrap.IFC4): +class TestUnassignContainer(test.bootstrap.IFC4): def test_unassigning_a_container(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element) - ifcopenshell.api.run("spatial.unassign_container", self.file, product=subelement) + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "spatial.assign_container", self.file, products=[subelement, subelement2], relating_structure=element + ) + ifcopenshell.api.run("spatial.unassign_container", self.file, products=[subelement, subelement2]) assert not self.file.by_type("IfcRelContainedInSpatialStructure") - def test_unassigning_a_container_with_other_elements(self): + def test_doing_nothing_if_no_container(self): + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("spatial.unassign_container", self.file, products=[subelement]) + assert ifcopenshell.util.element.get_container(subelement) is None + + def test_updating_the_rel_when_a_container_is_removed_with_multipled_elements(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element) - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement2, relating_structure=element) - ifcopenshell.api.run("spatial.unassign_container", self.file, product=subelement) + ifcopenshell.api.run( + "spatial.assign_container", self.file, products=[subelement, subelement2], relating_structure=element + ) + ifcopenshell.api.run("spatial.unassign_container", self.file, products=[subelement]) rel = self.file.by_type("IfcRelContainedInSpatialStructure")[0] assert list(rel.RelatedElements) == [subelement2] + + def test_deleting_the_rel_when_a_container_is_removed_with_no_elements(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=element) + ifcopenshell.api.run("spatial.unassign_container", self.file, products=[subelement]) + assert len(self.file.by_type("IfcRelContainedInSpatialStructure")) == 0 diff --git a/src/ifcopenshell-python/test/api/style/test_assign_material_style.py b/src/ifcopenshell-python/test/api/style/test_assign_material_style.py index 7c25ba578f..01325d8b99 100644 --- a/src/ifcopenshell-python/test/api/style/test_assign_material_style.py +++ b/src/ifcopenshell-python/test/api/style/test_assign_material_style.py @@ -75,7 +75,7 @@ class TestAssignMaterialStyle(test.bootstrap.IFC4): "material.add_constituent", self.file, constituent_set=material_set, material=material ) constituent.Name = name - ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material_set) + ifcopenshell.api.run("material.assign_material", self.file, products=[element], material=material_set) ifcopenshell.api.run("style.assign_material_style", self.file, material=material, style=style, context=context) diff --git a/src/ifcopenshell-python/test/api/style/test_unassign_material_style.py b/src/ifcopenshell-python/test/api/style/test_unassign_material_style.py index 936af3aa96..47c6e6a928 100644 --- a/src/ifcopenshell-python/test/api/style/test_unassign_material_style.py +++ b/src/ifcopenshell-python/test/api/style/test_unassign_material_style.py @@ -71,7 +71,7 @@ class TestAssignMaterialStyle(test.bootstrap.IFC4): "material.add_constituent", self.file, constituent_set=material_set, material=material ) constituent.Name = name - ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material_set) + ifcopenshell.api.run("material.assign_material", self.file, products=[element], material=material_set) ifcopenshell.api.run("style.assign_material_style", self.file, material=material, style=style, context=context) ifcopenshell.api.run( diff --git a/src/ifcopenshell-python/test/api/system/test_assign_port.py b/src/ifcopenshell-python/test/api/system/test_assign_port.py index 836222298c..f78183cf49 100644 --- a/src/ifcopenshell-python/test/api/system/test_assign_port.py +++ b/src/ifcopenshell-python/test/api/system/test_assign_port.py @@ -19,58 +19,20 @@ import numpy import test.bootstrap import ifcopenshell.api +import ifcopenshell.util.placement import ifcopenshell.util.system class TestAssignPort(test.bootstrap.IFC4): - def test_assigning_a_port_once_only(self): - port = self.file.createIfcDistributionPort() - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcChiller") - ifcopenshell.api.run("system.assign_port", self.file, element=element, port=port) - assert element.IsNestedBy[0].RelatedObjects == (port,) - assert ifcopenshell.util.system.get_ports(element) == [port] - ifcopenshell.api.run("system.assign_port", self.file, element=element, port=port) - assert ifcopenshell.util.system.get_ports(element) == [port] - - def test_updating_the_placement_to_be_relative_if_it_exists(self): - ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") - ifcopenshell.api.run("unit.assign_unit", self.file) - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcChiller") - subelement = ifcopenshell.api.run("system.add_port", self.file) - matrix = numpy.array( - ( - (1.0, 0.0, 0.0, 1.0), - (0.0, 1.0, 0.0, 1.0), - (0.0, 0.0, 1.0, 1.0), - (0.0, 0.0, 0.0, 1.0), - ) - ) - submatrix = numpy.array( - ( - (1.0, 0.0, 0.0, 1.0), - (0.0, 1.0, 0.0, 2.0), - (0.0, 0.0, 1.0, 3.0), - (0.0, 0.0, 0.0, 1.0), - ) - ) - ifcopenshell.api.run( - "geometry.edit_object_placement", self.file, product=element, matrix=matrix.copy(), is_si=False - ) - ifcopenshell.api.run( - "geometry.edit_object_placement", self.file, product=subelement, matrix=submatrix.copy(), is_si=False - ) - ifcopenshell.api.run("system.assign_port", self.file, element=element, port=subelement) - assert numpy.array_equal(ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement), matrix) - assert numpy.array_equal(ifcopenshell.util.placement.get_local_placement(subelement.ObjectPlacement), submatrix) - assert subelement.ObjectPlacement.PlacementRelTo == element.ObjectPlacement - - -class TestAssignPortIFC2X3(test.bootstrap.IFC2X3): def test_assigning_a_port_once_only(self): port = self.file.createIfcDistributionPort() element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcFlowSegment") ifcopenshell.api.run("system.assign_port", self.file, element=element, port=port) - assert element.HasPorts[0].RelatingPort == port + ifc2x3 = self.file.schema == "IFC2X3" + if ifc2x3: + assert element.HasPorts[0].RelatingPort == port + else: + assert element.IsNestedBy[0].RelatedObjects == (port,) assert ifcopenshell.util.system.get_ports(element) == [port] ifcopenshell.api.run("system.assign_port", self.file, element=element, port=port) assert ifcopenshell.util.system.get_ports(element) == [port] @@ -106,3 +68,7 @@ class TestAssignPortIFC2X3(test.bootstrap.IFC2X3): assert numpy.array_equal(ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement), matrix) assert numpy.array_equal(ifcopenshell.util.placement.get_local_placement(subelement.ObjectPlacement), submatrix) assert subelement.ObjectPlacement.PlacementRelTo == element.ObjectPlacement + + +class TestAssignPortIFC2X3(test.bootstrap.IFC2X3, TestAssignPort): + pass diff --git a/src/ifcopenshell-python/test/api/system/test_assign_system.py b/src/ifcopenshell-python/test/api/system/test_assign_system.py new file mode 100644 index 0000000000..0026866835 --- /dev/null +++ b/src/ifcopenshell-python/test/api/system/test_assign_system.py @@ -0,0 +1,45 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import pytest +import test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.system + + +class TestAssignSystem(test.bootstrap.IFC4): + def test_assign_system(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcFlowSegment") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcFlowSegment") + system = ifcopenshell.api.run("system.add_system", self.file) + ifcopenshell.api.run("system.assign_system", self.file, products=[element, element2], system=system) + assert len(self.file.by_type("IfcRelAssignsToGroup")) == 1 + assert set(ifcopenshell.util.system.get_system_elements(system)) == set(self.file.by_type("IfcFlowSegment")) + + def test_exception_on_unassignable_elements(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcFlowSegment") + proj = self.file.createIfcProject() + system = ifcopenshell.api.run("system.add_system", self.file) + with pytest.raises(TypeError): + ifcopenshell.api.run("system.assign_system", self.file, products=[element, proj], system=system) + with pytest.raises(TypeError): + ifcopenshell.api.run("system.assign_system", self.file, products=[element], system=proj) + + +class TestAssignSystemIFC2X3(test.bootstrap.IFC2X3, TestAssignSystem): + pass diff --git a/src/ifcopenshell-python/test/api/system/test_unassign_port.py b/src/ifcopenshell-python/test/api/system/test_unassign_port.py index 2edafa6c1d..3ba59bd72a 100644 --- a/src/ifcopenshell-python/test/api/system/test_unassign_port.py +++ b/src/ifcopenshell-python/test/api/system/test_unassign_port.py @@ -22,18 +22,13 @@ import ifcopenshell.util.system class TestAssignPort(test.bootstrap.IFC4): - def test_assigning_a_port_once_only(self): - port = self.file.createIfcDistributionPort() - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcChiller") - ifcopenshell.api.run("system.assign_port", self.file, element=element, port=port) - ifcopenshell.api.run("system.unassign_port", self.file, element=element, port=port) - assert ifcopenshell.util.system.get_ports(element) == [] - - -class TestAssignPortIFC2X3(test.bootstrap.IFC2X3): def test_assigning_a_port_once_only(self): port = self.file.createIfcDistributionPort() element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcFlowSegment") ifcopenshell.api.run("system.assign_port", self.file, element=element, port=port) ifcopenshell.api.run("system.unassign_port", self.file, element=element, port=port) assert ifcopenshell.util.system.get_ports(element) == [] + + +class TestAssignPortIFC2X3(test.bootstrap.IFC2X3, TestAssignPort): + pass diff --git a/src/ifcopenshell-python/test/api/system/test_unassign_system.py b/src/ifcopenshell-python/test/api/system/test_unassign_system.py new file mode 100644 index 0000000000..1bf3433825 --- /dev/null +++ b/src/ifcopenshell-python/test/api/system/test_unassign_system.py @@ -0,0 +1,40 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import pytest +import test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.system + + +class TestUnassignSystem(test.bootstrap.IFC4): + def test_unassign_system(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcFlowSegment") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcFlowSegment") + element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcFlowSegment") + system = ifcopenshell.api.run("system.add_system", self.file) + ifcopenshell.api.run("system.assign_system", self.file, products=[element, element2, element3], system=system) + ifcopenshell.api.run("system.unassign_system", self.file, products=[element2, element3], system=system) + assert ifcopenshell.util.system.get_system_elements(system) == [element] + + ifcopenshell.api.run("system.unassign_system", self.file, products=[element], system=system) + assert ifcopenshell.util.system.get_system_elements(system) == [] + + +class TestUnassignSystemIFC2X3(test.bootstrap.IFC2X3, TestUnassignSystem): + pass diff --git a/src/ifcopenshell-python/test/api/test_api.py b/src/ifcopenshell-python/test/api/test_api.py new file mode 100644 index 0000000000..72d20392f3 --- /dev/null +++ b/src/ifcopenshell-python/test/api/test_api.py @@ -0,0 +1,313 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.classification +import ifcopenshell.util.constraint +import ifcopenshell.util.element +import ifcopenshell.util.system +from datetime import datetime + + +def deprecation_check(test): + def new_test(self): + assert datetime.now().date() < datetime(2024, 6, 1).date(), "API arguments are completely deprecated" + test(self) + + return new_test + + +class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4): + @deprecation_check + def test_assigning_a_container(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + rel = ifcopenshell.api.run( + "spatial.assign_container", self.file, product=subelement, relating_structure=element + ) + assert ifcopenshell.util.element.get_container(subelement) == element + assert rel.is_a("IfcRelContainedInSpatialStructure") + + @deprecation_check + def test_unassigning_a_container_with_other_elements(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "spatial.assign_container", self.file, products=[subelement, subelement2], relating_structure=element + ) + ifcopenshell.api.run("spatial.unassign_container", self.file, product=subelement) + rel = self.file.by_type("IfcRelContainedInSpatialStructure")[0] + assert list(rel.RelatedElements) == [subelement2] + + @deprecation_check + def test_group_unassignment(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcPump") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcPump") + group = ifcopenshell.api.run("group.add_group", self.file) + ifcopenshell.api.run("group.assign_group", self.file, products=[element, element2], group=group) + ifcopenshell.api.run("group.unassign_group", self.file, product=element2, group=group) + + assert len(rels := self.file.by_type("IfcRelAssignsToGroup")) == 1 + rel = rels[0] + assert rel.RelatingGroup == group + assert rel.RelatedObjects == (element,) + + @deprecation_check + def test_assign_layer_to_items(self): + item = self.file.createIfcExtrudedAreaSolid() + layer = self.file.createIfcPresentationLayerAssignment() + + ifcopenshell.api.run("layer.assign_layer", self.file, item=item, layer=layer) + assert layer.AssignedItems == (item,) + + @deprecation_check + def test_unassign_layer_from_items(self): + items = [self.file.createIfcExtrudedAreaSolid() for i in range(3)] + layer = self.file.createIfcPresentationLayerAssignment() + + ifcopenshell.api.run("layer.assign_layer", self.file, items=items, layer=layer) + ifcopenshell.api.run("layer.unassign_layer", self.file, item=items[2], layer=layer) + assert len(layer.AssignedItems) == 2 + assert set(layer.AssignedItems) == set(items[:2]) + + @deprecation_check + def test_assigning_an_aggregate(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + rel = ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + assert ifcopenshell.util.element.get_aggregate(subelement) == element + assert rel.is_a("IfcRelAggregates") + + @deprecation_check + def test_unassigning_an_aggregate(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + ifcopenshell.api.run("aggregate.unassign_object", self.file, product=subelement) + assert ifcopenshell.util.element.get_aggregate(subelement) is None + + @deprecation_check + def test_removing_a_container(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=element) + ifcopenshell.api.run("spatial.remove_container", self.file, product=subelement) + assert ifcopenshell.util.element.get_container(subelement) is None + + @deprecation_check + def test_assigning_a_nesting(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSanitaryTerminal") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcValve") + rel = ifcopenshell.api.run("nest.assign_object", self.file, related_object=subelement, relating_object=element) + assert ifcopenshell.util.element.get_nest(subelement) == element + assert rel.is_a("IfcRelNests") + + @deprecation_check + def test_unassigning_an_nesting(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement1], relating_object=element) + ifcopenshell.api.run("nest.unassign_object", self.file, related_object=subelement1) + assert ifcopenshell.util.element.get_nest(subelement1) is None + + @deprecation_check + def test_assigning_a_type(self): + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + rel = ifcopenshell.api.run("type.assign_type", self.file, related_object=element1, relating_type=element_type) + assert ifcopenshell.util.element.get_type(element1) == element_type + assert rel.is_a("IfcRelDefinesByType") + + @deprecation_check + def test_unassigning_a_type(self): + element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) + ifcopenshell.api.run("type.unassign_type", self.file, related_object=element) + assert ifcopenshell.util.element.get_type(element) is None + + @deprecation_check + def test_assign_system(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcPump") + system = ifcopenshell.api.run("system.add_system", self.file) + + # assign group for multiple elements + ifcopenshell.api.run("system.assign_system", self.file, product=element, system=system) + assert len(rels := self.file.by_type("IfcRelAssignsToGroup")) == 1 + rel = rels[0] + assert rel.RelatingGroup == system + assert rel.RelatedObjects == (element,) + + @deprecation_check + def test_unassign_system(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcFlowSegment") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcFlowSegment") + system = ifcopenshell.api.run("system.add_system", self.file) + ifcopenshell.api.run("system.assign_system", self.file, products=[element, element2], system=system) + ifcopenshell.api.run("system.unassign_system", self.file, product=element, system=system) + assert ifcopenshell.util.system.get_system_elements(system) == [element2] + + ifcopenshell.api.run("system.unassign_system", self.file, product=element2, system=system) + assert ifcopenshell.util.system.get_system_elements(system) == [] + + @deprecation_check + def test_assign_element_single_material(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") + ifcopenshell.api.run( + "material.assign_material", self.file, product=element, type="IfcMaterial", material=material + ) + assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 1 + assert element.HasAssociations[0].RelatingMaterial == material + + @deprecation_check + def test_unassign_single_material(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element], type="IfcMaterial", material=material + ) + ifcopenshell.api.run("material.unassign_material", self.file, product=element) + assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 0 + assert len(self.file.by_type("IfcWall")) == 1 + assert len(self.file.by_type("IfcMaterial")) == 1 + + @deprecation_check + def test_adding_a_reference(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") + ifcopenshell.api.run( + "classification.add_reference", + self.file, + product=element, + identification="X", + name="Foobar", + classification=result, + ) + references = list(ifcopenshell.util.classification.get_references(element)) + assert len(references) == 1 + assert references[0].Identification == "X" + assert references[0].Name == "Foobar" + assert references[0].ReferencedSource == self.file.by_type("IfcClassification")[0] + + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "classification.add_reference", + self.file, + product=element2, + identification="X", + name="Foobar", + classification=result, + ) + assert list(ifcopenshell.util.classification.get_references(element2))[0].Identification == "X" + assert list(ifcopenshell.util.classification.get_references(element2))[0].Name == "Foobar" + assert list(ifcopenshell.util.classification.get_references(element2))[0] == references[0] + + @deprecation_check + def test_removing_a_reference(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") + reference = ifcopenshell.api.run( + "classification.add_reference", + self.file, + products=[element], + identification="X", + name="Foobar", + classification=result, + ) + ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference) + assert len(ifcopenshell.util.classification.get_references(element)) == 0 + assert len(self.file.by_type("IfcClassificationReference")) == 0 + + @deprecation_check + def test_assigning_a_reference(self): + reference = self.file.createIfcLibraryReference() + product = self.file.createIfcWall() + product2 = self.file.createIfcWall() + ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) + assert reference.LibraryRefForObjects[0].RelatedObjects == (product,) + ifcopenshell.api.run("library.assign_reference", self.file, product=product2, reference=reference) + assert set(reference.LibraryRefForObjects[0].RelatedObjects) == set((product, product2)) + + @deprecation_check + def test_unassigning_a_reference(self): + reference = self.file.createIfcLibraryReference() + product = self.file.createIfcWall() + ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference) + ifcopenshell.api.run("library.unassign_reference", self.file, product=product, reference=reference) + assert len(self.file.by_type("IfcRelAssociatesLibrary")) == 0 + + @deprecation_check + def test_assigning_a_document(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) + ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference) + assert element.HasAssociations[0].RelatingDocument == reference + + @deprecation_check + def test_unassigning_a_document(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) + ifcopenshell.api.run("document.assign_document", self.file, products=[element], document=reference) + ifcopenshell.api.run("document.unassign_document", self.file, product=element, document=reference) + assert not element.HasAssociations + assert not len(self.file.by_type("IfcRelAssociatesDocument")) + + @deprecation_check + def test_referencing_a_structure(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + rel = ifcopenshell.api.run( + "spatial.reference_structure", self.file, product=subelement, relating_structure=element + ) + assert ifcopenshell.util.element.get_referenced_structures(subelement) == [element] + assert rel.is_a("IfcRelReferencedInSpatialStructure") + + @deprecation_check + def test_removing_a_container(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "spatial.reference_structure", self.file, products=[subelement], relating_structure=element + ) + ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement, relating_structure=element) + assert ifcopenshell.util.element.get_referenced_structures(subelement) == [] + + @deprecation_check + def test_assign_a_constraint(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + constraint = ifcopenshell.api.run("constraint.add_objective", self.file) + ifcopenshell.api.run("constraint.assign_constraint", self.file, product=element, constraint=constraint) + assert ifcopenshell.util.constraint.get_constrained_elements(constraint) == {element} + + @deprecation_check + def test_unassigning_a_constraint(self): + constraint = ifcopenshell.api.run("constraint.add_objective", self.file) + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "constraint.assign_constraint", self.file, product=element, constraint=constraint + ) + ifcopenshell.api.run( + "constraint.unassign_constraint", self.file, product=element, constraint=constraint + ) + assert ifcopenshell.util.constraint.get_constrained_elements(element) == set() + assert len(self.file.by_type("IfcRelAssociatesConstraint")) == 0 diff --git a/src/ifcopenshell-python/test/api/type/test_assign_type.py b/src/ifcopenshell-python/test/api/type/test_assign_type.py new file mode 100644 index 0000000000..b7a9550b80 --- /dev/null +++ b/src/ifcopenshell-python/test/api/type/test_assign_type.py @@ -0,0 +1,148 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.element +import ifcopenshell.util.representation +import pytest + + +class TestAssignType(test.bootstrap.IFC4): + def test_assigning_a_type(self): + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + rel = ifcopenshell.api.run( + "type.assign_type", self.file, related_objects=[element1, element2], relating_type=element_type + ) + assert ifcopenshell.util.element.get_type(element1) == element_type + assert ifcopenshell.util.element.get_type(element2) == element_type + assert rel.is_a("IfcRelDefinesByType") + + def test_doing_nothing_if_type_is_already_assigned(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) + total_elements = len([e for e in self.file]) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) + assert len([e for e in self.file]) == total_elements + + def test_that_old_typing_relationships_are_updated_if_they_still_have_elements(self): + element_type1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element_type2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "type.assign_type", self.file, related_objects=[element1, element2], relating_type=element_type1 + ) + rel = element1.IsDefinedBy[0] if self.file.schema == "IFC2X3" else element1.IsTypedBy[0] + assert len(rel.RelatedObjects) == 2 + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element1], relating_type=element_type2) + assert len(rel.RelatedObjects) == 1 + + def test_that_old_typing_relationships_are_purged_if_no_more_elements_are_nested(self): + element_type1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element_type2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element1], relating_type=element_type1) + rel_id = (element1.IsDefinedBy[0] if self.file.schema == "IFC2X3" else element1.IsTypedBy[0]).id() + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element1], relating_type=element_type2) + with pytest.raises(RuntimeError): + self.file.by_id(rel_id) + + def test_map_representation_disabled(self): + element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + context = self.file.createIfcGeometricRepresentationContext() + rep = self.file.createIfcShapeRepresentation(ContextOfItems=context) + ifcopenshell.api.run("geometry.assign_representation", self.file, product=element_type, representation=rep) + ifcopenshell.api.run("material.assign_material", self.file, products=[element_type], type="IfcMaterialLayerSet") + + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "type.assign_type", + self.file, + related_objects=[element], + relating_type=element_type, + should_map_representations=False, + ) + + # no representation mapping and no material usage + assert ifcopenshell.util.representation.get_representation(element, context=context) == None + assert ifcopenshell.util.element.get_material(element, should_inherit=False) == None + + def test_map_representation(self): + element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + context = self.file.createIfcGeometricRepresentationContext() + rep = self.file.createIfcShapeRepresentation(ContextOfItems=context) + ifcopenshell.api.run("geometry.assign_representation", self.file, product=element_type, representation=rep) + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) + mapped_rep = ifcopenshell.util.representation.get_representation(element, context=context) + assert mapped_rep.RepresentationType == "MappedRepresentation" + assert mapped_rep.Items[0].MappingSource.MappedRepresentation == rep + + def test_do_not_map_representation_if_type_was_assigned_previously(self): + element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + context = self.file.createIfcGeometricRepresentationContext() + rep = self.file.createIfcShapeRepresentation(ContextOfItems=context) + ifcopenshell.api.run("geometry.assign_representation", self.file, product=element_type, representation=rep) + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element1], relating_type=element_type) + assert (mapped_rep := ifcopenshell.util.representation.get_representation(element1, context=context)) + mapped_rep_id = mapped_rep.id() + + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "type.assign_type", self.file, related_objects=[element1, element2], relating_type=element_type + ) + assert (mapped_rep := ifcopenshell.util.representation.get_representation(element1, context=context)) + assert mapped_rep.id() == mapped_rep_id + + def test_map_material_usages(self): + material_types = ("IfcMaterialLayerSet",) + if self.file.schema != "IFC2X3": + material_types += ("IfcMaterialProfileSet",) + for material_type in material_types: + element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + ifcopenshell.api.run("material.assign_material", self.file, products=[element_type], type=material_type) + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) + material = ifcopenshell.util.element.get_material(element) + assert material + assert material.is_a(f"{material_type}Usage") + + def test_do_not_reassign_material_if_it_was_assigned_previously(self): + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + ifcopenshell.api.run("material.assign_material", self.file, products=[element_type], type="IfcMaterialLayerSet") + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element1], relating_type=element_type) + assert (material := ifcopenshell.util.element.get_material(element1)) + material_id = material.id() + + # use 2 elements to trigger material assignment code block + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "type.assign_type", self.file, related_objects=[element1, element2], relating_type=element_type + ) + assert (material := ifcopenshell.util.element.get_material(element1)) + assert material.id() == material_id + + +class TestAssignTypeIFC2X3(test.bootstrap.IFC2X3, TestAssignType): + pass diff --git a/src/ifcopenshell-python/test/api/type/test_map_type_representation.py b/src/ifcopenshell-python/test/api/type/test_map_type_representation.py index 50e63cb90d..a43430a040 100644 --- a/src/ifcopenshell-python/test/api/type/test_map_type_representation.py +++ b/src/ifcopenshell-python/test/api/type/test_map_type_representation.py @@ -24,7 +24,7 @@ class TestMapTypeRepresentations(test.bootstrap.IFC4): def test_doing_nothing_if_the_type_has_no_representation_maps(self): element = self.file.createIfcWall() type = self.file.createIfcWallType() - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=type) total_elements = len([e for e in self.file]) ifcopenshell.api.run("type.map_type_representations", self.file, related_object=element, relating_type=type) assert len([e for e in self.file]) == total_elements diff --git a/src/ifcopenshell-python/test/api/type/test_unassign_type.py b/src/ifcopenshell-python/test/api/type/test_unassign_type.py new file mode 100644 index 0000000000..2df994f4a9 --- /dev/null +++ b/src/ifcopenshell-python/test/api/type/test_unassign_type.py @@ -0,0 +1,60 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import pytest +import test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.element + + +class TestUnassignType(test.bootstrap.IFC4): + def test_unassigning_a_type(self): + element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "type.assign_type", self.file, related_objects=[element1, element2], relating_type=element_type + ) + ifcopenshell.api.run("type.unassign_type", self.file, related_objects=[element1, element2]) + assert ifcopenshell.util.element.get_type(element1) is None + assert ifcopenshell.util.element.get_type(element2) is None + + def test_the_rel_is_kept_if_there_are_more_typed_elements(self): + element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element1], relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element2], relating_type=element_type) + ifcopenshell.api.run("type.unassign_type", self.file, related_objects=[element1]) + assert len(self.file.by_type("IfcRelDefinesByType")) == 1 + + def test_the_rel_is_purged_if_there_are_no_more_typed_elements(self): + element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + context = self.file.createIfcGeometricRepresentationContext() + rep = self.file.createIfcShapeRepresentation(ContextOfItems=context) + ifcopenshell.api.run("geometry.assign_representation", self.file, product=element_type, representation=rep) + ifcopenshell.api.run("material.assign_material", self.file, products=[element_type], type="IfcMaterialLayerSet") + + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) + ifcopenshell.api.run("type.unassign_type", self.file, related_objects=[element]) + assert len(self.file.by_type("IfcRelDefinesByType")) == 0 + + +class TestUnassignTypeIFC2X3(test.bootstrap.IFC2X3, TestUnassignType): + pass diff --git a/src/ifcopenshell-python/test/fixtures/validate/fail-duplicated-guids-ifc2x3.ifc b/src/ifcopenshell-python/test/fixtures/validate/fail-duplicated-guids-ifc2x3.ifc new file mode 100644 index 0000000000..9a6eb58e05 --- /dev/null +++ b/src/ifcopenshell-python/test/fixtures/validate/fail-duplicated-guids-ifc2x3.ifc @@ -0,0 +1,20 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); +FILE_NAME('','2024-04-02T17:16:50',(),(),'IfcOpenShell v0.7.0-e38eafd03','IfcOpenShell v0.7.0-e38eafd03',''); +FILE_SCHEMA(('IFC2X3')); +ENDSEC; +DATA; +#1=IFCACTORROLE(.USERDEFINED.,'CONTRIBUTOR',$); +#2=IFCTELECOMADDRESS(.USERDEFINED.,$,'WEBPAGE',$,$,$,$,'https://ifcopenshell.org'); +#3=IFCORGANIZATION('IfcOpenShell','IfcOpenShell','IfcOpenShell is an open source software library that helps users and software developers to work with IFC data.',(#1),(#2)); +#4=IFCAPPLICATION(#3,'v0.7.0-e38eafd03','IfcOpenShell','IfcOpenShell'); +#5=IFCPERSON('HSeldon','Seldon','Hari',$,$,$,$,$); +#6=IFCORGANIZATION('APTR','Aperture Science',$,$,$); +#7=IFCPERSONANDORGANIZATION(#5,#6,$); +#8=IFCOWNERHISTORY(#7,#4,.READWRITE.,.ADDED.,1712060210,#7,#4,1712060210); +#9=IFCBUILDINGELEMENTPROXY('3pdqyORIn8KBHZAhhtJ72T',#8,$,$,$,$,$,$,$); +#10=IFCOWNERHISTORY(#7,#4,.READWRITE.,.ADDED.,1712060210,#7,#4,1712060210); +#11=IFCBUILDINGELEMENTPROXY('3pdqyORIn8KBHZAhhtJ72T',#10,$,$,$,$,$,$,$); +ENDSEC; +END-ISO-10303-21; diff --git a/src/ifcopenshell-python/test/fixtures/validate/fail-duplicated-guids-ifc4.ifc b/src/ifcopenshell-python/test/fixtures/validate/fail-duplicated-guids-ifc4.ifc new file mode 100644 index 0000000000..eeca2cf84f --- /dev/null +++ b/src/ifcopenshell-python/test/fixtures/validate/fail-duplicated-guids-ifc4.ifc @@ -0,0 +1,11 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); +FILE_NAME('','2024-04-02T17:16:50',(),(),'IfcOpenShell v0.7.0-e38eafd03','IfcOpenShell v0.7.0-e38eafd03',''); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#1=IFCBUILDINGELEMENTPROXY('3pdqyORIn8KBHZAhhtJ72T',$,$,$,$,$,$,$,$); +#2=IFCBUILDINGELEMENTPROXY('3pdqyORIn8KBHZAhhtJ72T',$,$,$,$,$,$,$,$); +ENDSEC; +END-ISO-10303-21; diff --git a/src/ifcopenshell-python/test/fixtures/validate/fail-duplicated-guids-ifc4x3.ifc b/src/ifcopenshell-python/test/fixtures/validate/fail-duplicated-guids-ifc4x3.ifc new file mode 100644 index 0000000000..9bf812583a --- /dev/null +++ b/src/ifcopenshell-python/test/fixtures/validate/fail-duplicated-guids-ifc4x3.ifc @@ -0,0 +1,11 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); +FILE_NAME('','2024-04-02T17:16:50',(),(),'IfcOpenShell v0.7.0-e38eafd03','IfcOpenShell v0.7.0-e38eafd03',''); +FILE_SCHEMA(('IFC4X3')); +ENDSEC; +DATA; +#1=IFCBUILDINGELEMENTPROXY('3pdqyORIn8KBHZAhhtJ72T',$,$,$,$,$,$,$,$); +#2=IFCBUILDINGELEMENTPROXY('3pdqyORIn8KBHZAhhtJ72T',$,$,$,$,$,$,$,$); +ENDSEC; +END-ISO-10303-21; diff --git a/src/ifcopenshell-python/test/fixtures/validate/generate_00.py b/src/ifcopenshell-python/test/fixtures/validate/generate_00.py new file mode 100644 index 0000000000..e552a18970 --- /dev/null +++ b/src/ifcopenshell-python/test/fixtures/validate/generate_00.py @@ -0,0 +1,26 @@ +import ifcopenshell +import ifcopenshell.api +from pathlib import Path + + +guid1 = "3pdqyORIn8KBHZAhhtJ72T" +guid2 = "1sEzC8v31DshmvW5t5P631" + +for schema in ("ifc2x3", "ifc4", "ifc4x3"): + for status in ("fail", "pass"): + f = ifcopenshell.file(schema=schema) + + # setup IfcOwnerHistory + if schema == "ifc2x3": + ifcopenshell.api.run("owner.add_application", f) + person = ifcopenshell.api.run("owner.add_person", f) + organization = ifcopenshell.api.run("owner.add_organisation", f) + ifcopenshell.api.run("owner.add_person_and_organisation", f, person=person, organisation=organization) + + wall1 = ifcopenshell.api.run("root.create_entity", f) + wall2 = ifcopenshell.api.run("root.create_entity", f) + wall1.GlobalId = guid1 + wall2.GlobalId = guid1 if status == "fail" else guid2 + + filename = f"{status}-{'not-' if status == 'pass' else ''}duplicated-guids-{schema}.ifc" + f.write(Path(__file__).parent / filename) diff --git a/src/ifcopenshell-python/test/fixtures/validate/pass-not-duplicated-guids-ifc2x3.ifc b/src/ifcopenshell-python/test/fixtures/validate/pass-not-duplicated-guids-ifc2x3.ifc new file mode 100644 index 0000000000..37d71d03e4 --- /dev/null +++ b/src/ifcopenshell-python/test/fixtures/validate/pass-not-duplicated-guids-ifc2x3.ifc @@ -0,0 +1,20 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); +FILE_NAME('','2024-04-02T17:16:50',(),(),'IfcOpenShell v0.7.0-e38eafd03','IfcOpenShell v0.7.0-e38eafd03',''); +FILE_SCHEMA(('IFC2X3')); +ENDSEC; +DATA; +#1=IFCACTORROLE(.USERDEFINED.,'CONTRIBUTOR',$); +#2=IFCTELECOMADDRESS(.USERDEFINED.,$,'WEBPAGE',$,$,$,$,'https://ifcopenshell.org'); +#3=IFCORGANIZATION('IfcOpenShell','IfcOpenShell','IfcOpenShell is an open source software library that helps users and software developers to work with IFC data.',(#1),(#2)); +#4=IFCAPPLICATION(#3,'v0.7.0-e38eafd03','IfcOpenShell','IfcOpenShell'); +#5=IFCPERSON('HSeldon','Seldon','Hari',$,$,$,$,$); +#6=IFCORGANIZATION('APTR','Aperture Science',$,$,$); +#7=IFCPERSONANDORGANIZATION(#5,#6,$); +#8=IFCOWNERHISTORY(#7,#4,.READWRITE.,.ADDED.,1712060210,#7,#4,1712060210); +#9=IFCBUILDINGELEMENTPROXY('3pdqyORIn8KBHZAhhtJ72T',#8,$,$,$,$,$,$,$); +#10=IFCOWNERHISTORY(#7,#4,.READWRITE.,.ADDED.,1712060210,#7,#4,1712060210); +#11=IFCBUILDINGELEMENTPROXY('1sEzC8v31DshmvW5t5P631',#10,$,$,$,$,$,$,$); +ENDSEC; +END-ISO-10303-21; diff --git a/src/ifcopenshell-python/test/fixtures/validate/pass-not-duplicated-guids-ifc4.ifc b/src/ifcopenshell-python/test/fixtures/validate/pass-not-duplicated-guids-ifc4.ifc new file mode 100644 index 0000000000..e839daaba6 --- /dev/null +++ b/src/ifcopenshell-python/test/fixtures/validate/pass-not-duplicated-guids-ifc4.ifc @@ -0,0 +1,11 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); +FILE_NAME('','2024-04-02T17:16:50',(),(),'IfcOpenShell v0.7.0-e38eafd03','IfcOpenShell v0.7.0-e38eafd03',''); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#1=IFCBUILDINGELEMENTPROXY('3pdqyORIn8KBHZAhhtJ72T',$,$,$,$,$,$,$,$); +#2=IFCBUILDINGELEMENTPROXY('1sEzC8v31DshmvW5t5P631',$,$,$,$,$,$,$,$); +ENDSEC; +END-ISO-10303-21; diff --git a/src/ifcopenshell-python/test/fixtures/validate/pass-not-duplicated-guids-ifc4x3.ifc b/src/ifcopenshell-python/test/fixtures/validate/pass-not-duplicated-guids-ifc4x3.ifc new file mode 100644 index 0000000000..d96dc23002 --- /dev/null +++ b/src/ifcopenshell-python/test/fixtures/validate/pass-not-duplicated-guids-ifc4x3.ifc @@ -0,0 +1,11 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); +FILE_NAME('','2024-04-02T17:16:50',(),(),'IfcOpenShell v0.7.0-e38eafd03','IfcOpenShell v0.7.0-e38eafd03',''); +FILE_SCHEMA(('IFC4X3')); +ENDSEC; +DATA; +#1=IFCBUILDINGELEMENTPROXY('3pdqyORIn8KBHZAhhtJ72T',$,$,$,$,$,$,$,$); +#2=IFCBUILDINGELEMENTPROXY('1sEzC8v31DshmvW5t5P631',$,$,$,$,$,$,$,$); +ENDSEC; +END-ISO-10303-21; diff --git a/src/ifcopenshell-python/test/test_file.py b/src/ifcopenshell-python/test/test_file.py index 4be713442f..af4decdc82 100644 --- a/src/ifcopenshell-python/test/test_file.py +++ b/src/ifcopenshell-python/test/test_file.py @@ -162,8 +162,8 @@ class TestFile(test.bootstrap.IFC4): def test_creating_an_ifc4x3_file(self): f = ifcopenshell.file(schema="IFC4X3") assert f.schema == "IFC4X3" - assert f.schema_identifier == "IFC4X3_ADD1" - assert f.schema_version == (4, 3, 1, 0) + assert f.schema_identifier == "IFC4X3_ADD2" + assert f.schema_version == (4, 3, 2, 0) def test_creating_a_specific_version(self): f = ifcopenshell.file(schema_version=(4, 3, 1, 0)) diff --git a/src/ifcopenshell-python/test/util/test_brick.py b/src/ifcopenshell-python/test/util/test_brick.py index 52a99b1a1a..78b7771d57 100644 --- a/src/ifcopenshell-python/test/util/test_brick.py +++ b/src/ifcopenshell-python/test/util/test_brick.py @@ -36,5 +36,5 @@ class TestGetBrickTypeIFC2X3(test.bootstrap.IFC2X3): def test_run(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcFlowController") type_element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcAirTerminalBoxType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=type_element) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=type_element) assert subject.get_brick_type(element) == "https://brickschema.org/schema/Brick#TerminalUnit" diff --git a/src/ifcopenshell-python/test/util/test_classification.py b/src/ifcopenshell-python/test/util/test_classification.py index 58a5e47de2..7fe054e1e3 100644 --- a/src/ifcopenshell-python/test/util/test_classification.py +++ b/src/ifcopenshell-python/test/util/test_classification.py @@ -34,14 +34,14 @@ class TestGetReferences(test.bootstrap.IFC4): ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element], reference=reference1, classification=classification, ) ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element], reference=reference2, classification=classification, ) @@ -54,7 +54,7 @@ class TestGetReferences(test.bootstrap.IFC4): ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element], identification="X", name="Foobar", classification=result, @@ -69,19 +69,19 @@ class TestGetReferences(test.bootstrap.IFC4): project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) ifcopenshell.api.run("classification.add_classification", self.file, classification=classification) ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element], reference=reference1, classification=classification, ) ifcopenshell.api.run( "classification.add_reference", self.file, - product=element_type, + products=[element_type], reference=reference2, classification=classification, ) @@ -98,19 +98,19 @@ class TestGetReferences(test.bootstrap.IFC4): project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) ifcopenshell.api.run("classification.add_classification", self.file, classification=classification) ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element], reference=reference1, classification=classification, ) ifcopenshell.api.run( "classification.add_reference", self.file, - product=element_type, + products=[element_type], reference=reference2, classification=classification, ) diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index a534ca2b4e..ccb548a076 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -55,7 +55,7 @@ class TestGetPsetIFC4(test.bootstrap.IFC4): def test_getting_inherited_psets(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") type_element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=type_element) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=type_element) pset = ifcopenshell.api.run("pset.add_pset", self.file, product=type_element, name="name") ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"a": 1, "x": 1}) pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="name") @@ -68,11 +68,18 @@ class TestGetPsetIFC4(test.bootstrap.IFC4): assert subject.get_pset(element, "name", "a") == 2 assert subject.get_pset(element, "name", "x") == 1 assert subject.get_pset(element, "name", "b") == 3 + pset = ifcopenshell.api.run("pset.add_pset", self.file, product=type_element, name="name2") + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"z": False}) + assert subject.get_pset(element, "name2", "z") is False + + # test filters with inherited psets + assert subject.get_pset(element, "name", psets_only=True) == result + assert subject.get_pset(element, "name", qtos_only=True) == None def test_excluding_inherited_psets(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") type_element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=type_element) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=type_element) pset = ifcopenshell.api.run("pset.add_pset", self.file, product=type_element, name="name") ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"a": 1, "x": 1}) pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="name") @@ -126,7 +133,7 @@ class TestGetPsetsIFC4(test.bootstrap.IFC4): def test_getting_inherited_psets(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") type_element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=type_element) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=type_element) pset = ifcopenshell.api.run("pset.add_pset", self.file, product=type_element, name="name") ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"a": 1, "x": 1}) pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="name") @@ -256,14 +263,14 @@ class TestGetPredefinedTypeIFC4(test.bootstrap.IFC4): def test_getting_an_inherited_predefined_type(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) element_type.PredefinedType = "PARTITIONING" assert subject.get_predefined_type(element) == "PARTITIONING" def test_getting_an_inherited_userdefined_type_for_an_element_type(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) element_type.PredefinedType = "USERDEFINED" element_type.ElementType = "FOOBAR" assert subject.get_predefined_type(element) == "FOOBAR" @@ -271,7 +278,7 @@ class TestGetPredefinedTypeIFC4(test.bootstrap.IFC4): def test_getting_an_overriden_predefined_type(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) element_type.PredefinedType = "NOTDEFINED" element.PredefinedType = "PARTITIONING" assert subject.get_predefined_type(element) == "PARTITIONING" @@ -279,44 +286,45 @@ class TestGetPredefinedTypeIFC4(test.bootstrap.IFC4): def test_getting_an_inherited_userdefined_type_for_a_process_type(self): element = ifcopenshell.api.run("sequence.add_task", self.file) element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTaskType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) element_type.PredefinedType = "USERDEFINED" element_type.ProcessType = "FOOBAR" assert subject.get_predefined_type(element) == "FOOBAR" + def test_getting_an_element_type_predefined_type(self): + element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element_type.PredefinedType = "PARTITIONING" + assert subject.get_predefined_type(element_type) == "PARTITIONING" + + def test_getting_an_element_type_null_predefined_type(self): + element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element_type.PredefinedType = "NOTDEFINED" + assert subject.get_predefined_type(element_type) == "NOTDEFINED" + class TestGetTypeIFC4(test.bootstrap.IFC4): def test_getting_the_type_of_a_product(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) assert subject.get_type(element) == element_type assert subject.get_type(element_type) == element_type -class TestGetTypeIFC2X3(test.bootstrap.IFC2X3): - def test_getting_the_type_of_a_product(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) - assert subject.get_type(element) == element_type - assert subject.get_type(element_type) == element_type +class TestGetTypeIFC2X3(test.bootstrap.IFC2X3, TestGetTypeIFC4): + pass class TestGetTypes(test.bootstrap.IFC4): def test_getting_the_type_of_a_product(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) assert subject.get_types(element_type) == (element,) -class TestGetTypesIFC2X3(test.bootstrap.IFC2X3): - def test_getting_the_type_of_a_product(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) - assert subject.get_types(element_type) == (element,) +class TestGetTypesIFC2X3(test.bootstrap.IFC2X3, TestGetTypes): + pass class TestGetShapeAspects(test.bootstrap.IFC4): @@ -341,79 +349,83 @@ class TestGetMaterial(test.bootstrap.IFC4): def test_getting_the_material_of_a_product(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") material = ifcopenshell.api.run("material.add_material", self.file) - ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material) + ifcopenshell.api.run("material.assign_material", self.file, products=[element], material=material) assert subject.get_material(element) == material def test_getting_a_material_list_of_a_product(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") material = ifcopenshell.api.run("material.add_material", self.file) rel = ifcopenshell.api.run( - "material.assign_material", self.file, product=element, type="IfcMaterialList", material=material + "material.assign_material", self.file, products=[element], type="IfcMaterialList", material=material ) assert subject.get_material(element) == rel.RelatingMaterial def test_getting_a_material_layer_set_of_a_product(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialLayerSet") + rel = ifcopenshell.api.run( + "material.assign_material", self.file, products=[element], type="IfcMaterialLayerSet" + ) assert subject.get_material(element) == rel.RelatingMaterial def test_getting_a_material_profile_set_of_a_product(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialProfileSet") + rel = ifcopenshell.api.run( + "material.assign_material", self.file, products=[element], type="IfcMaterialProfileSet" + ) assert subject.get_material(element) == rel.RelatingMaterial def test_getting_a_material_layer_set_usage_of_a_product(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") rel = ifcopenshell.api.run( - "material.assign_material", self.file, product=element, type="IfcMaterialLayerSetUsage" + "material.assign_material", self.file, products=[element], type="IfcMaterialLayerSetUsage" ) assert subject.get_material(element) == rel.RelatingMaterial def test_getting_a_material_profile_set_usage_of_a_product(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") rel = ifcopenshell.api.run( - "material.assign_material", self.file, product=element, type="IfcMaterialProfileSetUsage" + "material.assign_material", self.file, products=[element], type="IfcMaterialProfileSetUsage" ) assert subject.get_material(element) == rel.RelatingMaterial def test_getting_a_material_layer_set_indirectly_from_an_assigned_usage(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") rel = ifcopenshell.api.run( - "material.assign_material", self.file, product=element, type="IfcMaterialLayerSetUsage" + "material.assign_material", self.file, products=[element], type="IfcMaterialLayerSetUsage" ) assert subject.get_material(element, should_skip_usage=True) == rel.RelatingMaterial.ForLayerSet def test_getting_a_material_profile_set_indirectly_from_an_assigned_usage(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") rel = ifcopenshell.api.run( - "material.assign_material", self.file, product=element, type="IfcMaterialProfileSetUsage" + "material.assign_material", self.file, products=[element], type="IfcMaterialProfileSetUsage" ) assert subject.get_material(element, should_skip_usage=True) == rel.RelatingMaterial.ForProfileSet def test_getting_an_inherited_material_from_the_elements_type(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) material = ifcopenshell.api.run("material.add_material", self.file) - ifcopenshell.api.run("material.assign_material", self.file, product=element_type, material=material) + ifcopenshell.api.run("material.assign_material", self.file, products=[element_type], material=material) assert subject.get_material(element) == material def test_getting_an_overridden_material_from_the_elements_occurrence(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) material = ifcopenshell.api.run("material.add_material", self.file) - ifcopenshell.api.run("material.assign_material", self.file, product=element_type, material=material) + ifcopenshell.api.run("material.assign_material", self.file, products=[element_type], material=material) material = ifcopenshell.api.run("material.add_material", self.file) - ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material) + ifcopenshell.api.run("material.assign_material", self.file, products=[element], material=material) assert subject.get_material(element) == material def test_getting_direct_materials_without_checking_inheritance(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) material = ifcopenshell.api.run("material.add_material", self.file) - ifcopenshell.api.run("material.assign_material", self.file, product=element_type, material=material) + ifcopenshell.api.run("material.assign_material", self.file, products=[element_type], material=material) assert subject.get_material(element, should_inherit=False) is None @@ -421,7 +433,7 @@ class TestGetMaterials(test.bootstrap.IFC4): def test_getting_the_materials_of_a_product(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") material = ifcopenshell.api.run("material.add_material", self.file) - ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material) + ifcopenshell.api.run("material.assign_material", self.file, products=[element], material=material) assert subject.get_materials(element) == [material] @@ -442,7 +454,7 @@ class TestGetStyles(test.bootstrap.IFC4): ) material = ifcopenshell.api.run("material.add_material", self.file) - ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material) + ifcopenshell.api.run("material.assign_material", self.file, products=[element], material=material) style = ifcopenshell.api.run("style.add_style", self.file) ifcopenshell.api.run( @@ -489,18 +501,18 @@ class TestGetElementsByMaterial(test.bootstrap.IFC4): def test_getting_elements_of_a_material(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") material = ifcopenshell.api.run("material.add_material", self.file) - ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material) + ifcopenshell.api.run("material.assign_material", self.file, products=[element], material=material) assert subject.get_elements_by_material(self.file, material) == {element} def test_getting_elements_of_a_material_layer_set(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) material = ifcopenshell.api.run("material.add_material", self.file) material_set = ifcopenshell.api.run("material.add_material_set", self.file, set_type="IfcMaterialLayerSet") ifcopenshell.api.run("material.add_layer", self.file, layer_set=material_set, material=material) - ifcopenshell.api.run("material.assign_material", self.file, product=element_type, material=material_set) - ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialLayerSetUsage") + ifcopenshell.api.run("material.assign_material", self.file, products=[element_type], material=material_set) + ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialLayerSetUsage") usage = self.file.by_type("IfcMaterialLayerSetUsage")[0] assert subject.get_elements_by_material(self.file, material) == {element, element_type} assert subject.get_elements_by_material(self.file, material_set) == {element, element_type} @@ -509,12 +521,14 @@ class TestGetElementsByMaterial(test.bootstrap.IFC4): def test_getting_elements_of_a_material_profile_set(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) material = ifcopenshell.api.run("material.add_material", self.file) material_set = ifcopenshell.api.run("material.add_material_set", self.file, set_type="IfcMaterialProfileSet") ifcopenshell.api.run("material.add_profile", self.file, profile_set=material_set, material=material) - ifcopenshell.api.run("material.assign_material", self.file, product=element_type, material=material_set) - ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialProfileSetUsage") + ifcopenshell.api.run("material.assign_material", self.file, products=[element_type], material=material_set) + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element], type="IfcMaterialProfileSetUsage" + ) usage = self.file.by_type("IfcMaterialProfileSetUsage")[0] assert subject.get_elements_by_material(self.file, material) == {element, element_type} assert subject.get_elements_by_material(self.file, material_set) == {element, element_type} @@ -527,7 +541,7 @@ class TestGetElementsByMaterial(test.bootstrap.IFC4): "material.add_material_set", self.file, set_type="IfcMaterialConstituentSet" ) ifcopenshell.api.run("material.add_constituent", self.file, constituent_set=material_set, material=material) - ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material_set) + ifcopenshell.api.run("material.assign_material", self.file, products=[element], material=material_set) assert subject.get_elements_by_material(self.file, material) == {element} assert subject.get_elements_by_material(self.file, material_set) == {element} @@ -536,7 +550,7 @@ class TestGetElementsByMaterial(test.bootstrap.IFC4): material = ifcopenshell.api.run("material.add_material", self.file) material_set = ifcopenshell.api.run("material.add_material_set", self.file, set_type="IfcMaterialList") ifcopenshell.api.run("material.add_list_item", self.file, material_list=material_set, material=material) - ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material_set) + ifcopenshell.api.run("material.assign_material", self.file, products=[element], material=material_set) assert subject.get_elements_by_material(self.file, material) == {element} assert subject.get_elements_by_material(self.file, material_set) == {element} @@ -587,7 +601,7 @@ class TestGetElementsByStyle(test.bootstrap.IFC4): def test_getting_elements_of_a_styled_material(self): element = self.file.createIfcWall() material = ifcopenshell.api.run("material.add_material", self.file) - ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material) + ifcopenshell.api.run("material.assign_material", self.file, products=[element], material=material) style = self.file.createIfcSurfaceStyle() self.file.createIfcMaterialDefinitionRepresentation( RepresentedMaterial=material, @@ -634,46 +648,10 @@ class TestGetElementsByLayer(test.bootstrap.IFC4): layer = ifcopenshell.api.run("layer.add_layer", self.file) representation = self.file.createIfcShapeRepresentation() element.Representation = self.file.createIfcProductDefinitionShape(Representations=[representation]) - ifcopenshell.api.run("layer.assign_layer", self.file, item=representation, layer=layer) + ifcopenshell.api.run("layer.assign_layer", self.file, items=[representation], layer=layer) assert list(subject.get_elements_by_layer(self.file, layer)) == [element] -class TestGetlayers(test.bootstrap.IFC4): - def test_getting_the_layer_of_a_product_representation(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - layer = ifcopenshell.api.run("layer.add_layer", self.file) - representation = self.file.createIfcShapeRepresentation() - element.Representation = self.file.createIfcProductDefinitionShape(Representations=[representation]) - ifcopenshell.api.run("layer.assign_layer", self.file, item=representation, layer=layer) - assert subject.get_layers(self.file, element) == [layer] - - def test_getting_the_layer_of_a_product_item(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - layer = ifcopenshell.api.run("layer.add_layer", self.file) - item = self.file.createIfcExtrudedAreaSolid() - representation = self.file.createIfcShapeRepresentation(Items=[item]) - element.Representation = self.file.createIfcProductDefinitionShape(Representations=[representation]) - ifcopenshell.api.run("layer.assign_layer", self.file, item=item, layer=layer) - assert subject.get_layers(self.file, element) == [layer] - - def test_getting_the_layer_of_a_type_product_representation(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - layer = ifcopenshell.api.run("layer.add_layer", self.file) - representation = self.file.createIfcShapeRepresentation() - element.RepresentationMaps = [self.file.createIfcRepresentationMap(MappedRepresentation=representation)] - ifcopenshell.api.run("layer.assign_layer", self.file, item=representation, layer=layer) - assert subject.get_layers(self.file, element) == [layer] - - def test_getting_the_layer_of_a_type_product_item(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - layer = ifcopenshell.api.run("layer.add_layer", self.file) - item = self.file.createIfcExtrudedAreaSolid() - representation = self.file.createIfcShapeRepresentation(Items=[item]) - element.RepresentationMaps = [self.file.createIfcRepresentationMap(MappedRepresentation=representation)] - ifcopenshell.api.run("layer.assign_layer", self.file, item=item, layer=layer) - assert subject.get_layers(self.file, element) == [layer] - - class TestGetlayersIFC2X3(test.bootstrap.IFC2X3): def test_getting_the_layer_of_a_product_item(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") @@ -681,7 +659,7 @@ class TestGetlayersIFC2X3(test.bootstrap.IFC2X3): item = self.file.createIfcExtrudedAreaSolid() representation = self.file.createIfcShapeRepresentation(Items=[item]) element.Representation = self.file.createIfcProductDefinitionShape(Representations=[representation]) - ifcopenshell.api.run("layer.assign_layer", self.file, item=item, layer=layer) + ifcopenshell.api.run("layer.assign_layer", self.file, items=[item], layer=layer) assert subject.get_layers(self.file, element) == [layer] def test_getting_the_layer_of_a_type_product_item(self): @@ -690,7 +668,25 @@ class TestGetlayersIFC2X3(test.bootstrap.IFC2X3): item = self.file.createIfcExtrudedAreaSolid() representation = self.file.createIfcShapeRepresentation(Items=[item]) element.RepresentationMaps = [self.file.createIfcRepresentationMap(MappedRepresentation=representation)] - ifcopenshell.api.run("layer.assign_layer", self.file, item=item, layer=layer) + ifcopenshell.api.run("layer.assign_layer", self.file, items=[item], layer=layer) + assert subject.get_layers(self.file, element) == [layer] + + +class TestGetlayers(test.bootstrap.IFC4, TestGetlayersIFC2X3): + def test_getting_the_layer_of_a_product_representation(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + layer = ifcopenshell.api.run("layer.add_layer", self.file) + representation = self.file.createIfcShapeRepresentation() + element.Representation = self.file.createIfcProductDefinitionShape(Representations=[representation]) + ifcopenshell.api.run("layer.assign_layer", self.file, items=[representation], layer=layer) + assert subject.get_layers(self.file, element) == [layer] + + def test_getting_the_layer_of_a_type_product_representation(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + layer = ifcopenshell.api.run("layer.add_layer", self.file) + representation = self.file.createIfcShapeRepresentation() + element.RepresentationMaps = [self.file.createIfcRepresentationMap(MappedRepresentation=representation)] + ifcopenshell.api.run("layer.assign_layer", self.file, items=[representation], layer=layer) assert subject.get_layers(self.file, element) == [layer] @@ -698,23 +694,23 @@ class TestGetContainerIFC4(test.bootstrap.IFC4): def test_getting_the_spatial_container_of_an_element(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") building = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - ifcopenshell.api.run("spatial.assign_container", self.file, product=element, relating_structure=building) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[element], relating_structure=building) assert subject.get_container(element) == building def test_getting_an_indirect_spatial_container_of_an_element(self): subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcElementAssembly") building = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - ifcopenshell.api.run("spatial.assign_container", self.file, product=element, relating_structure=building) - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[element], relating_structure=building) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement], relating_object=element) assert subject.get_container(subelement) == building def test_getting_nothing_if_we_enforce_only_getting_direct_spatial_containers(self): subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcElementAssembly") building = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - ifcopenshell.api.run("spatial.assign_container", self.file, product=element, relating_structure=building) - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[element], relating_structure=building) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement], relating_object=element) assert subject.get_container(subelement, should_get_direct=True) is None @@ -723,20 +719,40 @@ class TestGetReferencedStructures(test.bootstrap.IFC4): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") assert subject.get_referenced_structures(element) == [] building = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - ifcopenshell.api.run("spatial.reference_structure", self.file, product=element, relating_structure=building) + ifcopenshell.api.run("spatial.reference_structure", self.file, products=[element], relating_structure=building) assert subject.get_referenced_structures(element) == [building] building2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - ifcopenshell.api.run("spatial.reference_structure", self.file, product=element, relating_structure=building2) + ifcopenshell.api.run("spatial.reference_structure", self.file, products=[element], relating_structure=building2) assert subject.get_referenced_structures(element) == [building, building2] +class TestGetReferencedStructuresIFC2X3(test.bootstrap.IFC2X3, TestGetReferencedStructures): + pass + + +class TestGetStructureReferencedElements(test.bootstrap.IFC4): + def test_getting_references_of_an_element(self): + building = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + assert subject.get_structure_referenced_elements(building) == set() + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("spatial.reference_structure", self.file, products=[element], relating_structure=building) + assert subject.get_structure_referenced_elements(building) == {element} + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("spatial.reference_structure", self.file, products=[element2], relating_structure=building) + assert subject.get_structure_referenced_elements(building) == {element, element2} + + +class TestGetStructureReferencedElementsIFC2X3(test.bootstrap.IFC2X3, TestGetStructureReferencedElements): + pass + + class TestGetDecompositionIFC4(test.bootstrap.IFC4): def test_getting_decomposed_subelements_of_an_element(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcElementAssembly") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBeam") building = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - ifcopenshell.api.run("spatial.assign_container", self.file, product=element, relating_structure=building) - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[element], relating_structure=building) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement], relating_object=element) results = subject.get_decomposition(building) assert element in results assert subelement in results @@ -752,14 +768,87 @@ class TestGetDecompositionIFC4(test.bootstrap.IFC4): assert subsubelement in results +class TestGetGroupsIFC4(test.bootstrap.IFC4): + def test_run(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + group1 = ifcopenshell.api.run("group.add_group", self.file) + group2 = ifcopenshell.api.run("group.add_group", self.file) + + # assign group for multiple elements + ifcopenshell.api.run("group.assign_group", self.file, products=[element], group=group1) + ifcopenshell.api.run("group.assign_group", self.file, products=[element], group=group2) + + assert set(subject.get_groups(element)) == set([group1, group2]) + + +class TestGetGroupsIFC2X3(test.bootstrap.IFC2X3, TestGetGroupsIFC4): + pass + + class TestGetAggregateIFC4(test.bootstrap.IFC4): def test_getting_the_containing_aggregate_of_a_subelement(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcCovering") - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement], relating_object=element) assert subject.get_aggregate(subelement) == element +class TestGetNestIFC4(test.bootstrap.IFC4): + def test_getting_the_nest_parent_of_an_element(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement], relating_object=element) + assert subject.get_nest(subelement) == element + + +class TestGetNestIFC2X3(test.bootstrap.IFC2X3, TestGetNestIFC4): + pass + + +class TestGetReferencedElements(test.bootstrap.IFC4): + # TODO: test other references: + # IfcExternallyDefinedHatchStyle + # IfcExternallyDefinedSurfaceStyle + # IfcExternallyDefinedTextFont + def test_get_elements_referenced_by_classification_reference(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + elements = [ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")] + if self.file.schema != "IFC2X3": + elements.append(self.file.create_entity("IfcCostValue")) + result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") + reference = ifcopenshell.api.run( + "classification.add_reference", + self.file, + products=elements, + identification="X", + name="Foobar", + classification=result, + ) + assert subject.get_referenced_elements(reference) == set(elements) + + def test_get_elements_referenced_by_library_reference(self): + reference = self.file.createIfcLibraryReference() + elements = [ + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall"), + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall"), + ] + ifcopenshell.api.run("library.assign_reference", self.file, reference=reference, products=elements) + assert subject.get_referenced_elements(reference) == set(elements) + + def test_get_elements_referenced_by_document_reference(self): + reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) + elements = [ + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall"), + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall"), + ] + ifcopenshell.api.run("document.assign_document", self.file, document=reference, products=elements) + assert subject.get_referenced_elements(reference) == set(elements) + + +class TestGetReferencedElementsIFC2X3(test.bootstrap.IFC2X3, TestGetReferencedElements): + pass + + class TestReplaceAttributeIFC4(test.bootstrap.IFC4): def test_replacing_an_elements_attribute(self): element = self.file.createIfcWall("foo") diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py index b02ab7e811..571511d39f 100644 --- a/src/ifcopenshell-python/test/util/test_selector.py +++ b/src/ifcopenshell-python/test/util/test_selector.py @@ -20,6 +20,9 @@ import pytest import test.bootstrap import ifcopenshell.api import ifcopenshell.util.selector as subject +import ifcopenshell.util.placement +import ifcopenshell.util.pset +import numpy as np class TestFormat(): @@ -41,6 +44,13 @@ class TestFormat(): def test_number_formatting(self): assert subject.format("round(123, 5)") == "125" assert subject.format('round(\"123\", 5)') == "125" + assert subject.format('number(123)') == "123" + assert subject.format('number(1234.56)') == "1,234.56" + assert subject.format('number(123, ".")') == "123" + assert subject.format('number(\"123\", ".")') == "123" + assert subject.format('number(123.12, ".")') == "123.12" + assert subject.format('number(123.12, ",")') == "123,12" + assert subject.format('number(1234.12, ",", ".")') == "1.234,12" assert subject.format('metric_length(123, 5, 2)') == "125.00" assert subject.format('metric_length(123.123, 0.1, 2)') == "123.10" assert subject.format('metric_length(\"123\", 5, 2)') == "125.00" @@ -75,7 +85,7 @@ class TestGetElementValue(test.bootstrap.IFC4): layer = ifcopenshell.api.run("material.add_layer", self.file, layer_set=material_set, material=material) layer.Name = "L2" ifcopenshell.api.run("material.edit_layer", self.file, layer=layer, attributes={"LayerThickness": 13}) - ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material_set) + ifcopenshell.api.run("material.assign_material", self.file, products=[element], material=material_set) assert subject.get_element_value(element, "material.MaterialLayers.Name") == ["L1", "L2"] # Allow to use "item" to generically select an item in a material set assert subject.get_element_value(element, "material.item.Name") == ["L1", "L2"] @@ -99,7 +109,7 @@ class TestGetElementValue(test.bootstrap.IFC4): ) layer = ifcopenshell.api.run("material.add_layer", self.file, layer_set=material_set, material=material) layer.Name = "L1" - ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material_set) + ifcopenshell.api.run("material.assign_material", self.file, products=[element], material=material_set) assert subject.get_element_value(element, "material.item.Name.0") == "L1" assert subject.get_element_value(element, "material.item.Name.1") is None @@ -158,7 +168,7 @@ class TestFilterElements(test.bootstrap.IFC4): def test_selecting_by_type(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) assert subject.filter_elements(self.file, "IfcWall, type=Foo") == set() element_type.Name = "Foo" assert subject.filter_elements(self.file, "IfcWall, type=Foo") == {element} @@ -170,7 +180,7 @@ class TestFilterElements(test.bootstrap.IFC4): element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") assert subject.filter_elements(self.file, "IfcWall, material=NULL") == {element, element2} material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") - ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material) + ifcopenshell.api.run("material.assign_material", self.file, products=[element], material=material) assert subject.filter_elements(self.file, "IfcWall, material=CON01") == {element} assert subject.filter_elements(self.file, "IfcWall, material!=CON01") == {element2} @@ -215,7 +225,7 @@ class TestFilterElements(test.bootstrap.IFC4): ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element], identification="X", name="Foobar", classification=result, @@ -231,11 +241,11 @@ class TestFilterElements(test.bootstrap.IFC4): storey = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuildingStorey", name="G") building = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding", name="Building") project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject", name="Project") - ifcopenshell.api.run("spatial.assign_container", self.file, product=element, relating_structure=space) - ifcopenshell.api.run("spatial.assign_container", self.file, product=element2, relating_structure=storey) - ifcopenshell.api.run("aggregate.assign_object", self.file, product=space, relating_object=storey) - ifcopenshell.api.run("aggregate.assign_object", self.file, product=storey, relating_object=building) - ifcopenshell.api.run("aggregate.assign_object", self.file, product=building, relating_object=project) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[element], relating_structure=space) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[element2], relating_structure=storey) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[space], relating_object=storey) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[storey], relating_object=building) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[building], relating_object=project) assert subject.filter_elements(self.file, "IfcWall, location=NULL") == set() assert subject.filter_elements(self.file, "IfcWall, location=Space") == {element} assert subject.filter_elements(self.file, "IfcWall, location=G") == {element, element2} @@ -251,8 +261,57 @@ class TestFilterElements(test.bootstrap.IFC4): assert subject.filter_elements(self.file, "IfcWall, Name=Foo + IfcSlab") == {element, element2} assert subject.filter_elements(self.file, "IfcWall, Name=Foo + IfcSlab, Name=Bar") == {element, element2} + def test_using_elements_argument(self): + wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + slab = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSlab") + + # keep elements unaffected by expression + assert subject.filter_elements(self.file, "IfcWall", {slab}) == {wall, slab} + + # filter out excluded elements + assert subject.filter_elements(self.file, "IfcWall, ! IfcSlab", {slab}) == {wall} + + # edit_in_place to update original set + original_set = set() + new_set = subject.filter_elements(self.file, "IfcWall", original_set, edit_in_place=True) + assert new_set == original_set + + +class TestSetElementValue(test.bootstrap.IFC4): + def test_set_xyz_coordinates(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + ifcopenshell.api.run("unit.assign_unit", self.file) + + items = list(zip("xyz", ("5", "10", "15"))) + matrix = np.eye(4) + matrix[:, 3] = (5, 10, 15, 1) + + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("geometry.edit_object_placement", self.file, product=element, is_si=False) + for coord, value in items: + subject.set_element_value(self.file, element, coord, value) + assert np.array_equal(ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement), matrix) + + element_without_placement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + for coord, value in items: + subject.set_element_value(self.file, element_without_placement, coord, value) + assert np.array_equal( + ifcopenshell.util.placement.get_local_placement(element_without_placement.ObjectPlacement), matrix + ) + + def test_set_attribute(self): + element = self.file.createIfcWall() + subject.set_element_value(self.file, element, "Name", "Foo") + assert element.Name == "Foo" + subject.set_element_value(self.file, element, "Name", 123) + assert element.Name == "123" + class TestSelector(test.bootstrap.IFC4): + def test_selecting_from_specified_elements(self): + elements = [ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") for _ in range(2)] + assert subject.Selector.parse(self.file, ".IfcWall", elements[:1]) == [elements[0]] + def test_selecting_by_class(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSlab") @@ -406,18 +465,18 @@ class TestSelector(test.bootstrap.IFC4): def test_getting_occurrences_of_a_filtered_type(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", self.file, related_object=element2, relating_type=element_type2) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element2], relating_type=element_type2) assert set(subject.Selector.parse(self.file, "* .IfcWallType")) == {element, element2} def test_getting_decomposition_of_a_filtered_type(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcMember") building = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - ifcopenshell.api.run("spatial.assign_container", self.file, product=element, relating_structure=building) - ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[element], relating_structure=building) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement], relating_object=element) assert set(subject.Selector.parse(self.file, "@ .IfcBuilding")) == {element, subelement} def test_selecting_elements_from_a_prefiltered_list(self): @@ -446,13 +505,13 @@ class TestSelector(test.bootstrap.IFC4): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") element_type.Name = "Foo" - ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) assert set(subject.Selector.parse(self.file, '.IfcWall[type.Name="Foo"]')) == {element} def test_selecting_via_a_material(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") - ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material) + ifcopenshell.api.run("material.assign_material", self.file, products=[element], material=material) assert set(subject.Selector.parse(self.file, '.IfcWall[material.Name="CON01"]')) == {element} def test_selecting_via_a_material_set(self): @@ -463,7 +522,7 @@ class TestSelector(test.bootstrap.IFC4): ) layer = ifcopenshell.api.run("material.add_layer", self.file, layer_set=material_set, material=material) ifcopenshell.api.run("material.edit_layer", self.file, layer=layer, attributes={"LayerThickness": 13}) - ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material_set) + ifcopenshell.api.run("material.assign_material", self.file, products=[element], material=material_set) assert set(subject.Selector.parse(self.file, '.IfcWall[material.LayerSetName="FOO"]')) == {element} def test_selecting_via_a_material_set_item(self): @@ -475,7 +534,7 @@ class TestSelector(test.bootstrap.IFC4): ) layer = ifcopenshell.api.run("material.add_layer", self.file, layer_set=material_set, material=material) layer = ifcopenshell.api.run("material.add_layer", self.file, layer_set=material_set, material=material2) - ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material_set) + ifcopenshell.api.run("material.assign_material", self.file, products=[element], material=material_set) assert set(subject.Selector.parse(self.file, '.IfcWall[material.item.Material.Name="CON01"]')) == {element} assert set(subject.Selector.parse(self.file, '.IfcWall[material.item.Material.Name="CON02"]')) == {element} assert set(subject.Selector.parse(self.file, '.IfcWall[material.item.Material.Name="CON03"]')) == set() diff --git a/src/ifcopenshell-python/test/util/test_system.py b/src/ifcopenshell-python/test/util/test_system.py index a3951f5e88..3035f07da6 100644 --- a/src/ifcopenshell-python/test/util/test_system.py +++ b/src/ifcopenshell-python/test/util/test_system.py @@ -22,11 +22,21 @@ import ifcopenshell.api import ifcopenshell.util.system as subject +class TestIsAssignable(test.bootstrap.IFC4): + def test_run(self): + project = self.file.createIfcProject() + system = self.file.createIfcSystem() + pump = self.file.createIfcPump() + assert subject.is_assignable(pump, system) + assert not subject.is_assignable(project, system) + assert not subject.is_assignable(pump, project) + + class TestGetSystemElements(test.bootstrap.IFC4): def test_run(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcPump") system = ifcopenshell.api.run("system.add_system", self.file, ifc_class="IfcSystem") - ifcopenshell.api.run("system.assign_system", self.file, product=element, system=system) + ifcopenshell.api.run("system.assign_system", self.file, products=[element], system=system) assert subject.get_system_elements(system) == [element] @@ -34,7 +44,7 @@ class TestGetElementSystems(test.bootstrap.IFC4): def test_run(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcPump") system = ifcopenshell.api.run("system.add_system", self.file, ifc_class="IfcSystem") - ifcopenshell.api.run("system.assign_system", self.file, product=element, system=system) + ifcopenshell.api.run("system.assign_system", self.file, products=[element], system=system) assert subject.get_element_systems(element) == [system] def test_do_not_get_non_services_groups(self): @@ -42,7 +52,7 @@ class TestGetElementSystems(test.bootstrap.IFC4): ifcopenshell.api.run( "system.assign_system", self.file, - product=element, + products=[element], system=self.file.createIfcGroup(), ) for not_assignable_system_class in ("IfcZone", "IfcStructuralAnalysisModel"): @@ -50,21 +60,13 @@ class TestGetElementSystems(test.bootstrap.IFC4): ifcopenshell.api.run( "system.assign_system", self.file, - product=element, + products=[element], system=self.file.create_entity(not_assignable_system_class), ) assert not subject.get_element_systems(element) class TestGetPorts(test.bootstrap.IFC4): - def test_run(self): - port = self.file.createIfcDistributionPort() - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcChiller") - ifcopenshell.api.run("system.assign_port", self.file, element=element, port=port) - assert subject.get_ports(element) == [port] - - -class TestGetPortsIFC2X3(test.bootstrap.IFC2X3): def test_run(self): port = self.file.createIfcDistributionPort() element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcFlowSegment") @@ -72,6 +74,10 @@ class TestGetPortsIFC2X3(test.bootstrap.IFC2X3): assert subject.get_ports(element) == [port] +class TestGetPortsIFC2X3(test.bootstrap.IFC2X3, TestGetPorts): + pass + + class TestGetConnectedPort(test.bootstrap.IFC4): def test_run(self): port1 = ifcopenshell.api.run("system.add_port", self.file) @@ -104,6 +110,5 @@ class TestGetConnectedToFrom(test.bootstrap.IFC4): assert subject.get_connected_from(element2) == [] -class TestGetConnectedToFromIFC2X3(test.bootstrap.IFC2X3): - def test_run(self): - TestGetConnectedToFrom.test_run(self) +class TestGetConnectedToFromIFC2X3(test.bootstrap.IFC2X3, TestGetConnectedToFrom): + pass diff --git a/src/ifcopenshell-python/test/util/test_unit.py b/src/ifcopenshell-python/test/util/test_unit.py index cdf028a222..52a6a93178 100644 --- a/src/ifcopenshell-python/test/util/test_unit.py +++ b/src/ifcopenshell-python/test/util/test_unit.py @@ -20,6 +20,21 @@ import pytest import test.bootstrap import ifcopenshell.api import ifcopenshell.util.unit as subject +from math import pi + + +class TestCalculateUnitScale(test.bootstrap.IFC4): + def test_prefix_and_conversion_based_units_are_considered(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + length = ifcopenshell.api.run("unit.add_conversion_based_unit", self.file, name="foot") + length.ConversionFactor.UnitComponent.Prefix = "MILLI" + ifcopenshell.api.run("unit.assign_unit", self.file, units=[length]) + assert subject.calculate_unit_scale(self.file) == 0.3048 * 0.001 + + angle = ifcopenshell.api.run("unit.add_conversion_based_unit", self.file, name="degree") + angle.ConversionFactor.UnitComponent.Prefix = "MILLI" + ifcopenshell.api.run("unit.assign_unit", self.file, units=[angle]) + assert subject.calculate_unit_scale(self.file, "PLANEANGLEUNIT") == pi / 180 * 0.001 class TestFormatLength(test.bootstrap.IFC4): diff --git a/src/ifcparse/IfcParse.cpp b/src/ifcparse/IfcParse.cpp index d0ccb44aa6..cad24a3f9a 100644 --- a/src/ifcparse/IfcParse.cpp +++ b/src/ifcparse/IfcParse.cpp @@ -740,6 +740,8 @@ size_t IfcParse::IfcFile::load(unsigned entity_instance_name, const IfcParse::en filler.push_back(ea); } catch (IfcException& e) { Logger::Message(Logger::LOG_ERROR, e.what()); + // #4070 We didn't actually capture an aggregate entry, undo length increment. + return_value--; } } else { filler.push_back(new TokenArgument(next)); @@ -1455,6 +1457,39 @@ void IfcEntityInstanceData::setArgument(size_t i, Argument* a, IfcUtil::Argument new_attribute = copy; } + bool inverses_handled = false; + + /* + // #4474 This shaves us some milliseconds, but doesn't structurally change things. + if (this->file && attributes_[i] != 0 + && attributes_[i]->type() == IfcUtil::Argument_AGGREGATE_OF_ENTITY_INSTANCE + && new_attribute->type() == IfcUtil::Argument_AGGREGATE_OF_ENTITY_INSTANCE + && attributes_[i]->size() >= 16 && new_attribute->size() >= 16) { + aggregate_of_instance::ptr existing_aggregate = *attributes_[i]; + aggregate_of_instance::ptr replacing_aggregate = *new_attribute; + std::set existing(existing_aggregate->begin(), existing_aggregate->end()); + std::set replacing(replacing_aggregate->begin(), replacing_aggregate->end()); + std::vector removed; + std::vector added; + std::set_difference(existing.begin(), existing.end(), replacing.begin(), replacing.end(), std::back_inserter(removed)); + std::set_difference(replacing.begin(), replacing.end(), existing.begin(), existing.end(), std::back_inserter(added)); + register_inverse_visitor visitor(*this->file, *this); + { + unregister_inverse_visitor visitor(*this->file, *this); + for (auto& inst : removed) { + visitor(inst, i); + } + } + { + register_inverse_visitor visitor(*this->file, *this); + for (auto& inst : added) { + visitor(inst, i); + } + } + inverses_handled = true; + } + */ + if (attributes_[i] != 0) { Argument* current_attribute = attributes_[i]; if (this->file) { @@ -1472,14 +1507,16 @@ void IfcEntityInstanceData::setArgument(size_t i, Argument* a, IfcUtil::Argument } } - // Deregister inverse indices in file - unregister_inverse_visitor visitor(*this->file, *this); - apply_individual_instance_visitor(current_attribute, i).apply(visitor); + if (!inverses_handled) { + // Deregister inverse indices in file + unregister_inverse_visitor visitor(*this->file, *this); + apply_individual_instance_visitor(current_attribute, i).apply(visitor); + } } delete attributes_[i]; } - if (this->file) { + if (this->file && !inverses_handled) { // Register inverse indices in file register_inverse_visitor visitor(*this->file, *this); apply_individual_instance_visitor(new_attribute, i).apply(visitor); diff --git a/src/ifcparse/IfcWrite.cpp b/src/ifcparse/IfcWrite.cpp index a82a31d15f..3d125fb06c 100644 --- a/src/ifcparse/IfcWrite.cpp +++ b/src/ifcparse/IfcWrite.cpp @@ -295,4 +295,28 @@ void IfcWriteArgument::set(IfcUtil::IfcBaseInterface*const& v) { } else { container = boost::blank(); } -} \ No newline at end of file +} + +// Overloads to raise exceptions on non-finite values +void IfcWriteArgument::set(const double& v) { + if (!std::isfinite(v)) { + throw IfcParse::IfcException("Only finite values are allowed"); + } + container = v; +} + +void IfcWriteArgument::set(const std::vector& v) { + if (std::any_of(v.begin(), v.end(), [](double v) {return !std::isfinite(v); })) { + throw IfcParse::IfcException("Only finite values are allowed"); + } + container = v; +} + +void IfcWriteArgument::set(const std::vector< std::vector >& v) { + if (std::any_of(v.begin(), v.end(), [](const std::vector& vs) { + return std::any_of(vs.begin(), vs.end(), [](double v) {return !std::isfinite(v); }); + })) { + throw IfcParse::IfcException("Only finite values are allowed"); + } + container = v; +} diff --git a/src/ifcparse/IfcWrite.h b/src/ifcparse/IfcWrite.h index c69ca6052b..a79998a8ff 100644 --- a/src/ifcparse/IfcWrite.h +++ b/src/ifcparse/IfcWrite.h @@ -142,7 +142,12 @@ namespace IfcWrite { // Overload to detect null values void set(IfcUtil::IfcBaseInterface*const & v); - + + // Overloads to raise exceptions on non-finite values + void set(const double& v); + void set(const std::vector& v); + void set(const std::vector< std::vector >& v); + operator int() const; operator bool() const; operator boost::logic::tribool() const; diff --git a/src/ifcpatch/Makefile b/src/ifcpatch/Makefile index 1909562a75..385ed3ac1c 100644 --- a/src/ifcpatch/Makefile +++ b/src/ifcpatch/Makefile @@ -1,20 +1,26 @@ -# BlenderBIM Add-on - OpenBIM Blender Add-on -# Copyright (C) 2022 Dion Moult +# IfcPatch - IFC patching utiliy +# Copyright (C) 2020, 2021, 2023 Dion Moult # -# This file is part of BlenderBIM Add-on. +# This file is part of IfcPatch. # -# BlenderBIM Add-on is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by +# IfcPatch is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# BlenderBIM Add-on is distributed in the hope that it will be useful, +# IfcPatch is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. +# GNU Lesser General Public License for more details. # -# You should have received a copy of the GNU General Public License -# along with BlenderBIM Add-on. If not, see . +# You should have received a copy of the GNU Lesser General Public License +# along with IfcPatch. If not, see . + +VERSION:=`date '+%y%m%d'` +SED:=sed -i +ifeq ($(UNAME_S),Darwin) +SED:=sed -i '' -e +endif .PHONY: test test: @@ -23,3 +29,10 @@ test: .PHONY: qa qa: black . + +.PHONY: dist +dist: + rm -rf dist + $(SED) "s/999999/$(VERSION)/" pyproject.toml + python -m build + $(SED) "s/$(VERSION)/999999/" pyproject.toml diff --git a/src/ifcpatch/ifcpatch/__init__.py b/src/ifcpatch/ifcpatch/__init__.py index acb17fb92e..4fd9bf187c 100644 --- a/src/ifcpatch/ifcpatch/__init__.py +++ b/src/ifcpatch/ifcpatch/__init__.py @@ -19,6 +19,7 @@ # along with IfcPatch. If not, see . import os +import shutil import ifcopenshell import logging import typing @@ -99,7 +100,7 @@ def write(output: Union[ifcopenshell.file, str], filepath: str) -> None: return elif isinstance(output, str): if os.path.exists(output): - os.rename(output, filepath) + shutil.move(output, filepath) else: with open(filepath, "w") as text_file: text_file.write(output) diff --git a/src/ifcpatch/ifcpatch/recipes/ConvertLengthUnit.py b/src/ifcpatch/ifcpatch/recipes/ConvertLengthUnit.py index 48da1e1b37..7483569122 100644 --- a/src/ifcpatch/ifcpatch/recipes/ConvertLengthUnit.py +++ b/src/ifcpatch/ifcpatch/recipes/ConvertLengthUnit.py @@ -22,16 +22,17 @@ import ifcopenshell.api.owner.settings import ifcopenshell.util.pset import ifcopenshell.util.element import ifcopenshell.util.unit +from logging import Logger class Patcher: - def __init__(self, src, file, logger, unit="METERS"): + def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, unit: str = "METERS"): """Converts the length unit of a model to the specified unit Allowed metric units include METERS, MILLIMETERS, CENTIMETERS, etc. Allowed imperial units include INCHES, FEET, MILES. - :param unit: The name of the desired unit. + :param unit: The name of the desired unit, defaults to "METERS" :type unit: str Example: @@ -48,7 +49,7 @@ class Patcher: self.file = file self.logger = logger self.unit = unit - self.file_patched: ifcopenshell.file = None + self.file_patched: ifcopenshell.file def patch(self): self.file_patched = ifcopenshell.util.unit.convert_file_length_units(self.file, self.unit) diff --git a/src/ifcpatch/ifcpatch/recipes/ExtractElements.py b/src/ifcpatch/ifcpatch/recipes/ExtractElements.py index 35d3e2bc08..b2660a9d29 100644 --- a/src/ifcpatch/ifcpatch/recipes/ExtractElements.py +++ b/src/ifcpatch/ifcpatch/recipes/ExtractElements.py @@ -20,10 +20,11 @@ import ifcopenshell import ifcopenshell.api import ifcopenshell.util.selector from typing import Union +from logging import Logger class Patcher: - def __init__(self, src, file, logger, query: str = "IfcWall"): + def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, query: str = "IfcWall"): """Extract certain elements into a new model Extract a subset of elements from an existing IFC data set and save it @@ -52,8 +53,8 @@ class Patcher: self.query = query def patch(self): - self.contained_ins = {} - self.aggregates = {} + self.contained_ins: dict[str, set[ifcopenshell.entity_instance]] = {} + self.aggregates: dict[str, set[ifcopenshell.entity_instance]] = {} self.new = ifcopenshell.file(schema=self.file.wrapped_data.schema) self.owner_history = None self.reuse_identities: dict[int, ifcopenshell.entity_instance] = {} @@ -66,18 +67,14 @@ class Patcher: self.create_spatial_tree() self.file = self.new - def add_element(self, element) -> None: + def add_element(self, element: ifcopenshell.entity_instance) -> None: new_element = self.append_asset(element) if not new_element: return - for rel in getattr(element, "ContainedInStructure", []): - spatial_element = rel.RelatingStructure - new_spatial_element = self.append_asset(spatial_element) - self.contained_ins.setdefault(spatial_element.GlobalId, set()).add(new_element) - self.add_decomposition_parents(spatial_element, new_spatial_element) + self.add_spatial_structures(element, new_element) self.add_decomposition_parents(element, new_element) - def append_asset(self, element) -> Union[ifcopenshell.entity_instance, None]: + def append_asset(self, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: try: return self.new.by_guid(element.GlobalId) except: @@ -88,29 +85,43 @@ class Patcher: "project.append_asset", self.new, library=self.file, element=element, reuse_identities=self.reuse_identities ) - def add_decomposition_parents(self, element, new_element) -> None: + def add_spatial_structures( + self, element: ifcopenshell.entity_instance, new_element: ifcopenshell.entity_instance + ) -> None: + """element is IfcElement""" + for rel in getattr(element, "ContainedInStructure", []): + spatial_element = rel.RelatingStructure + new_spatial_element = self.append_asset(spatial_element) + self.contained_ins.setdefault(spatial_element.GlobalId, set()).add(new_element) + self.add_decomposition_parents(spatial_element, new_spatial_element) + + def add_decomposition_parents( + self, element: ifcopenshell.entity_instance, new_element: ifcopenshell.entity_instance + ) -> None: + """element is IfcObjectDefinition""" for rel in element.Decomposes: parent = rel.RelatingObject new_parent = self.append_asset(parent) self.aggregates.setdefault(parent.GlobalId, set()).add(new_element) self.add_decomposition_parents(parent, new_parent) + self.add_spatial_structures(parent, new_parent) def create_spatial_tree(self) -> None: - for relating_structure, related_elements in self.contained_ins.items(): + for relating_structure_guid, related_elements in self.contained_ins.items(): self.new.createIfcRelContainedInSpatialStructure( ifcopenshell.guid.new(), self.owner_history, None, None, list(related_elements), - self.new.by_guid(relating_structure), + self.new.by_guid(relating_structure_guid), ) - for relating_object, related_objects in self.aggregates.items(): + for relating_object_guid, related_objects in self.aggregates.items(): self.new.createIfcRelAggregates( ifcopenshell.guid.new(), self.owner_history, None, None, - self.new.by_guid(relating_object), + self.new.by_guid(relating_object_guid), list(related_objects), ) diff --git a/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py b/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py index 42df8170e8..f4c1761879 100644 --- a/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py +++ b/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py @@ -124,6 +124,42 @@ class Patcher: elif not isinstance(value, str): value = str(value) properties.append([i, pset_name, prop_name, value]) + + material = ifcopenshell.util.element.get_material(element, should_skip_usage=True) + if material: + name = getattr(material, "Name", getattr(material, "LayerSetName", None)) or "Unnamed" + properties.append([i, "IFC Material", "Name", name]) + properties.append([i, "IFC Material", "Class", material.is_a()]) + if material.is_a("IfcMaterial"): + materials = [] + elif material.is_a("IfcMaterialLayerSet"): + for idx, item in enumerate(material.MaterialLayers): + properties.append([i, "IFC Material", f"Layer {idx + 1} Name", item.Name]) + properties.append([i, "IFC Material", f"Layer {idx + 1} Material", item.Material.Name]) + if getattr(item.Material, "Category"): + properties.append([i, "IFC Material", f"Layer {idx + 1} Category", item.Material.Category]) + elif material.is_a("IfcMaterialProfileSet"): + for idx, item in enumerate(material.MaterialProfiles): + properties.append([i, "IFC Material", f"Profile {idx + 1} Name", item.Name]) + properties.append([i, "IFC Material", f"Profile {idx + 1} Material", item.Material.Name]) + if getattr(item.Material, "Category"): + properties.append([i, "IFC Material", f"Profile {idx + 1} Category", item.Material.Category]) + elif material.is_a("IfcMaterialConstituentSet"): + for idx, item in enumerate(material.MaterialConstituents): + properties.append([i, "IFC Material", f"Constituent {idx + 1} Name", item.Name]) + properties.append([i, "IFC Material", f"Constituent {idx + 1} Material", item.Material.Name]) + if getattr(item.Material, "Category"): + properties.append([i, "IFC Material", f"Constituent {idx + 1} Category", item.Material.Category]) + elif material.is_a("IfcMaterialList"): + for idx, material in enumerate(material.Materials): + properties.append([i, "IFC Material", f"Material {idx + 1} Name", material.Name]) + if getattr(material, "Category"): + properties.append([i, "IFC Material", f"Material {idx + 1} Category", material.Category]) + + layers = ifcopenshell.util.element.get_layers(self.file, element) + for idx, layer in enumerate(layers): + properties.append([i, "IFC Presentation Layer Assignment", f"Layer {idx + 1}", layer.Name]) + relating_type = ifcopenshell.util.element.get_type(element) if relating_type and relating_type != element: relationships.append([i, "IfcRelDefinesByType", id_map[relating_type.id()]]) diff --git a/src/ifcpatch/ifcpatch/recipes/MergeDuplicateTypes.py b/src/ifcpatch/ifcpatch/recipes/MergeDuplicateTypes.py index f2461652b7..49c80bf813 100644 --- a/src/ifcpatch/ifcpatch/recipes/MergeDuplicateTypes.py +++ b/src/ifcpatch/ifcpatch/recipes/MergeDuplicateTypes.py @@ -19,10 +19,11 @@ import ifcopenshell import ifcopenshell.api import ifcopenshell.util.element +from logging import Logger class Patcher: - def __init__(self, src, file, logger, attribute="Tag"): + def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, attribute: str = "Tag"): """Merge duplicate element types via the Tag or another attribute Revit is notorious for creating many duplicate element types. Element @@ -70,56 +71,26 @@ class Patcher: for element_type in self.file.by_type("IfcTypeObject"): original_type = keys.get(getattr(element_type, key), None) if original_type: - for element in ifcopenshell.util.element.get_types(element_type): - self.assign_type(element, original_type) + elements = ifcopenshell.util.element.get_types(element_type) + if elements: + self.assign_type(elements, original_type) for inverse in self.file.get_inverse(element_type): ifcopenshell.util.element.replace_attribute(inverse, element_type, original_type) self.file.remove(element_type) else: keys[getattr(element_type, key)] = element_type - def assign_type(self, related_object, relating_type): - # This is basically a portion of the type.assign_type API which only - # affects the IfcRelDefinesByType relationship. To be conservative, we - # don't use the API directly since that would do other things like + def assign_type( + self, related_objects: list[ifcopenshell.entity_instance], relating_type: ifcopenshell.entity_instance + ) -> None: + # To be conservative, we disable `should_map_representations` + # since that would do other things like # map type representations or recalculate material set usages which is # risky when we're patching an existing dataset. - if self.file.schema == "IFC2X3": - is_typed_by = None - is_defined_by = related_object.IsDefinedBy - for rel in is_defined_by: - if rel.is_a("IfcRelDefinesByType"): - is_typed_by = [rel] - break - types = relating_type.ObjectTypeOf - else: - is_typed_by = related_object.IsTypedBy - types = relating_type.Types - - if types and is_typed_by == types: - return - - if is_typed_by: - related_objects = list(is_typed_by[0].RelatedObjects) - related_objects.remove(related_object) - if related_objects: - is_typed_by[0].RelatedObjects = related_objects - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": is_typed_by[0]}) - else: - self.file.remove(is_typed_by[0]) - - if types: - related_objects = list(types[0].RelatedObjects) - related_objects.append(related_object) - types[0].RelatedObjects = related_objects - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": types[0]}) - else: - types = self.file.create_entity( - "IfcRelDefinesByType", - **{ - "GlobalId": ifcopenshell.guid.new(), - "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), - "RelatedObjects": [related_object], - "RelatingType": relating_type, - } - ) + ifcopenshell.api.run( + "type.assign_type", + self.file, + relating_type=relating_type, + related_objects=related_objects, + should_map_representations=False, + ) diff --git a/src/ifcpatch/ifcpatch/recipes/MergeProject.py b/src/ifcpatch/ifcpatch/recipes/MergeProject.py index 586e8a4eb7..260affcfa3 100644 --- a/src/ifcpatch/ifcpatch/recipes/MergeProject.py +++ b/src/ifcpatch/ifcpatch/recipes/MergeProject.py @@ -18,10 +18,12 @@ import ifcopenshell import ifcopenshell.util.element +from typing import Union +from logging import Logger class Patcher: - def __init__(self, src, file, logger, filepath=None): + def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, filepath: str): """Merge two IFC models into one Note that other than combining the two IfcProject elements into one, no @@ -46,18 +48,25 @@ class Patcher: def patch(self): source = ifcopenshell.open(self.filepath) - self.existing_contexts = self.file.by_type("IfcGeometricRepresentationContext") - self.added_contexts = set() + self.existing_contexts: list[ifcopenshell.entity_instance] = self.file.by_type( + "IfcGeometricRepresentationContext" + ) + self.added_contexts: set[ifcopenshell.entity_instance] = set() + original_project = self.file.by_type("IfcProject")[0] merged_project = self.file.add(source.by_type("IfcProject")[0]) + for element in source.by_type("IfcGeometricRepresentationContext"): new = self.file.add(element) self.added_contexts.add(new) + for element in source: self.file.add(element) + for inverse in self.file.get_inverse(merged_project): ifcopenshell.util.element.replace_attribute(inverse, merged_project, original_project) self.file.remove(merged_project) + self.reuse_existing_contexts() def reuse_existing_contexts(self): @@ -73,7 +82,9 @@ class Patcher: for added_context in to_delete: ifcopenshell.util.element.remove_deep2(self.file, added_context) - def get_equivalent_existing_context(self, added_context): + def get_equivalent_existing_context( + self, added_context: ifcopenshell.entity_instance + ) -> Union[ifcopenshell.entity_instance, None]: for context in self.existing_contexts: if context.is_a() != added_context.is_a(): continue diff --git a/src/ifcpatch/ifcpatch/recipes/Migrate.py b/src/ifcpatch/ifcpatch/recipes/Migrate.py index a82230a92f..5280875ce9 100644 --- a/src/ifcpatch/ifcpatch/recipes/Migrate.py +++ b/src/ifcpatch/ifcpatch/recipes/Migrate.py @@ -18,10 +18,11 @@ import ifcopenshell import ifcopenshell.util.schema +from logging import Logger class Patcher: - def __init__(self, src, file, logger, schema="IFC4"): + def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, schema: str = "IFC4"): """Migrate from one IFC version to another Note that this is experimental and will try to preserve as much data as diff --git a/src/ifcpatch/ifcpatch/recipes/PurgeData.py b/src/ifcpatch/ifcpatch/recipes/PurgeData.py index e2cdc4d140..9f55b47af9 100644 --- a/src/ifcpatch/ifcpatch/recipes/PurgeData.py +++ b/src/ifcpatch/ifcpatch/recipes/PurgeData.py @@ -17,10 +17,12 @@ # along with IfcPatch. If not, see . import datetime +import ifcopenshell +from logging import Logger class Patcher: - def __init__(self, src, file, logger): + def __init__(self, src: str, file: ifcopenshell.file, logger: Logger): """Purge IFC properties, relationships, and other data In some rare cases (i.e. "resetting" a model or for security purposes) @@ -94,17 +96,25 @@ class Patcher: element.Name = "Rabbit" element.Description = "Rabbit" - for element in self.file.by_type("IfcContext"): + if self.file.schema == "IFC2X3": + elements = self.file.by_type("IfcProject") + else: + elements = self.file.by_type("IfcContext") + for element in elements: element.ObjectType = None element.LongName = None element.Phase = None - for element in self.file.by_type("IfcSpatialElement"): + if self.file.schema == "IFC2X3": + elements = self.file.by_type("IfcSpatialStructureElement") + else: + elements = self.file.by_type("IfcSpatialElement") + for element in elements: element.LongName = None for element in self.file.by_type("IfcOwnerHistory"): element.State = None - element.ChangeAction = None + element.ChangeAction = "NOCHANGE" if self.file.schema == "IFC2X3" else None element.LastModifiedDate = None element.LastModifyingUser = None element.LastModifyingApplication = None @@ -123,7 +133,11 @@ class Patcher: for element in self.file.by_type("IfcPropertyDefinition"): self.file.remove(element) - for element in self.file.by_type("IfcPropertyAbstraction"): + if self.file.schema == "IFC2X3": + elements = self.file.by_type("IfcProperty") + else: + elements = self.file.by_type("IfcPropertyAbstraction") + for element in elements: self.file.remove(element) for element in self.file.by_type("IfcPhysicalQuantity"): @@ -132,13 +146,27 @@ class Patcher: for element in self.file.by_type("IfcRelAssociates"): self.file.remove(element) - for element in self.file.by_type("IfcMaterialDefinition"): + if self.file.schema == "IFC2X3": + elements = ( + self.file.by_type("IfcMaterial") + + self.file.by_type("IfcMaterialLayer") + + self.file.by_type("IfcMaterialLayerSet") + + self.file.by_type("IfcMaterialLayerSetUsage") + + self.file.by_type("IfcMaterialList") + ) + else: + elements = self.file.by_type("IfcMaterialDefinition") + for element in elements: self.file.remove(element) for element in self.file.by_type("IfcMaterialDefinitionRepresentation"): self.file.remove(element) - for element in self.file.by_type("IfcMaterialUsageDefinition"): + if self.file.schema == "IFC2X3": + elements = [] + else: + elements = self.file.by_type("IfcMaterialUsageDefinition") + for element in elements: self.file.remove(element) for element in self.file.by_type("IfcMaterialList"): @@ -153,10 +181,40 @@ class Patcher: for element in self.file.by_type("IfcPresentationStyle"): self.file.remove(element) - for element in self.file.by_type("IfcPresentationItem"): + if self.file.schema == "IFC2X3": + elements = ( + self.file.by_type("IfcColourSpecification") + + self.file.by_type("IfcCurveStyleFont") + + self.file.by_type("IfcCurveStyleFontAndScaling") + + self.file.by_type("IfcCurveStyleFontPattern") + + self.file.by_type("IfcPreDefinedItem") + + self.file.by_type("IfcSurfaceStyleLighting") + + self.file.by_type("IfcSurfaceStyleRefraction") + + self.file.by_type("IfcSurfaceStyleShading") + + self.file.by_type("IfcSurfaceStyleWithTextures") + + self.file.by_type("IfcSurfaceTexture") + + self.file.by_type("IfcTextStyleForDefinedFont") + + self.file.by_type("IfcTextStyleTextModel") + + self.file.by_type("IfcTextStyleWithBoxCharacteristics") + + self.file.by_type("IfcTextureCoordinate") + + self.file.by_type("IfcTextureVertex") + + self.file.by_type("IfcTextStyleForDefinedFont") + + self.file.by_type("IfcTextStyleForDefinedFont") + ) + else: + elements = self.file.by_type("IfcPresentationItem") + for element in elements: self.file.remove(element) - for element in self.file.by_type("IfcExternalInformation"): + if self.file.schema == "IFC2X3": + elements = ( + self.file.by_type("IfcClassification") + + self.file.by_type("IfcDocumentInformation") + + self.file.by_type("IfcLibraryInformation") + ) + else: + elements = self.file.by_type("IfcExternalInformation") + for element in elements: self.file.remove(element) for element in self.file.by_type("IfcExternalReference"): diff --git a/src/ifcpatch/ifcpatch/recipes/TessellateElements.py b/src/ifcpatch/ifcpatch/recipes/TessellateElements.py index db71e5ae06..e16f4c600f 100644 --- a/src/ifcpatch/ifcpatch/recipes/TessellateElements.py +++ b/src/ifcpatch/ifcpatch/recipes/TessellateElements.py @@ -17,14 +17,24 @@ # along with IfcPatch. If not, see . import ifcopenshell +import ifcopenshell.api import ifcopenshell.geom import ifcopenshell.util.selector +import ifcopenshell.util.shape import ifcopenshell.util.representation import multiprocessing +from logging import Logger class Patcher: - def __init__(self, src, file, logger, query="IfcBeam"): + def __init__( + self, + src: str, + file: ifcopenshell.file, + logger: Logger, + query: str = "IfcBeam", + force_faceted_brep: bool = False, + ): """Convert element body representations to tessellations or faceted breps Some software have bugs that result in incorrect geometry being @@ -35,20 +45,31 @@ class Patcher: See example bug: https://github.com/Autodesk/revit-ifc/issues/707 + :param query: Query string to filter out elements to convert, defaults to "IfcBeam" + :type query: str + :param force_faceted_brep: Force using IfcFacetedBreps instead of IfcPolygonalFaceSets, + defaults to `False` + :type force_faceted_brep: bool + Example: .. code:: python - ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "TessellateElements", "arguments": ["IfcBeam"]}) + ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "TessellateElements", "arguments": ["IfcBeam", False]}) """ self.src = src self.file = file self.logger = logger self.query = query + self.force_faceted_brep = force_faceted_brep def patch(self): context = ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW") elements = ifcopenshell.util.selector.filter_elements(self.file, self.query) + + if not elements: + return + settings = ifcopenshell.geom.settings() settings.set(settings.STRICT_TOLERANCE, True) settings.set_context_ids([context.id()]) @@ -68,7 +89,12 @@ class Patcher: for element, geometry in replacements.items(): v, f = geometry mesh = ifcopenshell.api.run( - "geometry.add_mesh_representation", self.file, context=context, vertices=v, faces=f + "geometry.add_mesh_representation", + self.file, + context=context, + vertices=v, + faces=f, + force_faceted_brep=self.force_faceted_brep, ) representations = element.Representation.Representations representations = [r for r in representations if r.ContextOfItems != context] diff --git a/src/ifcpatch/pyproject.toml b/src/ifcpatch/pyproject.toml new file mode 100644 index 0000000000..7607cf6a00 --- /dev/null +++ b/src/ifcpatch/pyproject.toml @@ -0,0 +1,27 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "ifcpatch" +version = "0.0.999999" +authors = [ + { name="Dion Moult", email="dion@thinkmoult.com" }, +] +description = "IFC patching utility" +readme = "README.md" +keywords = ["IFC", "BIM"] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", +] +dependencies = ["ifcopenshell", "toposort", "numpy"] + +[project.urls] +Homepage = "http://ifcopenshell.org" +Documentation = "https://docs.ifcopenshell.org" +Issues = "https://github.com/IfcOpenShell/IfcOpenShell/issues" + +[tool.setuptools.packages.find] +include = ["ifcpatch"] +exclude = ["test*"] diff --git a/src/ifcpatch/test/bootstrap.py b/src/ifcpatch/test/bootstrap.py new file mode 100644 index 0000000000..1603fcdb26 --- /dev/null +++ b/src/ifcpatch/test/bootstrap.py @@ -0,0 +1,52 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Thomas Krijnen +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import pytest +import ifcopenshell +import ifcopenshell.api +import ifcopenshell.api.owner.settings + + +class IFC4X3: + @pytest.fixture(autouse=True) + def setup(self): + self.file: ifcopenshell.file = ifcopenshell.api.run("project.create_file", version="IFC4X3") + ifcopenshell.api.owner.settings.get_user = lambda ifc: (ifc.by_type("IfcPersonAndOrganization") or [None])[0] + ifcopenshell.api.owner.settings.get_application = lambda ifc: (ifc.by_type("IfcApplication") or [None])[0] + ifcopenshell.api.pre_listeners = {} + ifcopenshell.api.post_listeners = {} + + +class IFC4: + @pytest.fixture(autouse=True) + def setup(self): + self.file: ifcopenshell.file = ifcopenshell.api.run("project.create_file") + ifcopenshell.api.owner.settings.get_user = lambda ifc: (ifc.by_type("IfcPersonAndOrganization") or [None])[0] + ifcopenshell.api.owner.settings.get_application = lambda ifc: (ifc.by_type("IfcApplication") or [None])[0] + ifcopenshell.api.pre_listeners = {} + ifcopenshell.api.post_listeners = {} + + +class IFC2X3: + @pytest.fixture(autouse=True) + def setup(self): + self.file: ifcopenshell.file = ifcopenshell.api.run("project.create_file", version="IFC2X3") + ifcopenshell.api.owner.settings.get_user = lambda ifc: ifc.createIfcPersonAndOrganization() + ifcopenshell.api.owner.settings.get_application = lambda ifc: ifc.createIfcApplication() + ifcopenshell.api.pre_listeners = {} + ifcopenshell.api.post_listeners = {} diff --git a/src/ifcpatch/test/test_ExtractElements.py b/src/ifcpatch/test/test_ExtractElements.py index 5878a9a32b..10b55bad09 100644 --- a/src/ifcpatch/test/test_ExtractElements.py +++ b/src/ifcpatch/test/test_ExtractElements.py @@ -20,11 +20,61 @@ import os import pytest import ifcpatch import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.element +import test.bootstrap -class TestExtractElements: +class TestExtractElements(test.bootstrap.IFC4): + def test_basic(self): + project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + output = ifcpatch.execute({"file": self.file, "recipe": "ExtractElements", "arguments": ["IfcWall"]}) + + assert output.by_type("IfcProject")[0].GlobalId == project.GlobalId + assert output.by_type("IfcWall")[0].GlobalId == wall.GlobalId + + def test_keep_spatial_structure(self): + project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + + site = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") + building = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + storey = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuildingStorey") + wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[building], relating_object=site) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[storey], relating_object=building) + ifcopenshell.api.run("spatial.assign_container", self.file, products=[wall], relating_structure=storey) + + output = ifcpatch.execute({"file": self.file, "recipe": "ExtractElements", "arguments": ["IfcWall"]}) + + wall_new = output.by_type("IfcWall")[0] + assert (storey_new := ifcopenshell.util.element.get_container(wall_new)).GlobalId == storey.GlobalId + assert (building_new := ifcopenshell.util.element.get_aggregate(storey_new)).GlobalId == building.GlobalId + assert (site_new := ifcopenshell.util.element.get_aggregate(building_new)).GlobalId == site.GlobalId + + def test_keep_aggregate_in_spatial_structure(self): + project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcElementAssembly") + container = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuildingStorey") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("spatial.assign_container", self.file, products=[element], relating_structure=container) + ifcopenshell.api.run("aggregate.assign_object", self.file, products=[subelement], relating_object=element) + + output = ifcpatch.execute({"file": self.file, "recipe": "ExtractElements", "arguments": ["IfcWall"]}) + + wall_new = output.by_type("IfcWall")[0] + assembly = output.by_type("IfcElementAssembly")[0] + + assert ifcopenshell.util.element.get_aggregate(wall_new).GlobalId == element.GlobalId + assert ifcopenshell.util.element.get_container(assembly).GlobalId == container.GlobalId + def test_getting_the_psets_of_a_product_as_a_dictionary(self): ifc = ifcopenshell.open(os.path.join(os.getcwd(), "test", "files", "basic.ifc")) - output = ifcpatch.execute({"input": ifc, "recipe": "ExtractElements", "arguments": [".IfcWall"]}) + output = ifcpatch.execute({"file": ifc, "recipe": "ExtractElements", "arguments": ["IfcWall"]}) assert output.by_type("IfcWall") assert not output.by_type("IfcSlab") + + +class TestExtractElementsIFC2X3(test.bootstrap.IFC2X3, TestExtractElements): + pass diff --git a/src/ifcpatch/test/test_MergeDuplicateTypes.py b/src/ifcpatch/test/test_MergeDuplicateTypes.py new file mode 100644 index 0000000000..3149f30ca4 --- /dev/null +++ b/src/ifcpatch/test/test_MergeDuplicateTypes.py @@ -0,0 +1,77 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import os +import pytest +import ifcpatch +import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.element +import ifcopenshell.util.representation +import test.bootstrap + + +class TestMergeDuplicateTypes(test.bootstrap.IFC4): + def test_run(self): + context = self.file.createIfcGeometricRepresentationContext() + + wall1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + wall_type1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType", name="WallType") + rep = self.file.createIfcShapeRepresentation(ContextOfItems=context) + ifcopenshell.api.run("geometry.assign_representation", self.file, product=wall_type1, representation=rep) + ifcopenshell.api.run("material.assign_material", self.file, products=[wall_type1], type="IfcMaterialLayerSet") + ifcopenshell.api.run( + "type.assign_type", + self.file, + related_objects=[wall1], + relating_type=wall_type1, + should_map_representations=False, + ) + + wall2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + wall_type2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType", name="WallType") + rep = self.file.createIfcShapeRepresentation(ContextOfItems=context) + ifcopenshell.api.run("geometry.assign_representation", self.file, product=wall_type2, representation=rep) + ifcopenshell.api.run("material.assign_material", self.file, products=[wall_type2], type="IfcMaterialLayerSet") + ifcopenshell.api.run( + "type.assign_type", + self.file, + related_objects=[wall2], + relating_type=wall_type2, + should_map_representations=False, + ) + + output = ifcpatch.execute({"file": self.file, "recipe": "MergeDuplicateTypes", "arguments": ["Name"]}) + + assert len(output.by_type("IfcWall")) == 2 + wall_types = output.by_type("IfcWallType") + assert len(wall_types) == 1 + wall_type = wall_types[0] + assert set(ifcopenshell.util.element.get_types(wall_type)) == set(output.by_type("IfcWall")) + + # test not to remap representation + assert ifcopenshell.util.representation.get_representation(wall1, context=context) == None + assert ifcopenshell.util.representation.get_representation(wall2, context=context) == None + + # and not to create material usages + assert ifcopenshell.util.element.get_material(wall1, should_inherit=False) == None + assert ifcopenshell.util.element.get_material(wall2, should_inherit=False) == None + + +class TestMergeDuplicateTypesIFC2X3(test.bootstrap.IFC2X3, TestMergeDuplicateTypes): + pass diff --git a/src/ifcpatch/test/test_RegenerateGlobalIds.py b/src/ifcpatch/test/test_RegenerateGlobalIds.py new file mode 100644 index 0000000000..ae33b60747 --- /dev/null +++ b/src/ifcpatch/test/test_RegenerateGlobalIds.py @@ -0,0 +1,51 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import os +import pytest +import ifcpatch +import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.element +import test.bootstrap + + +class TestRegenerateGlobalIds(test.bootstrap.IFC4): + def test_run(self): + project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + used_guids = {project.GlobalId, wall.GlobalId} + ifcpatch.execute({"file": self.file, "recipe": "RegenerateGlobalIds", "arguments": [False]}) + new_guids = {project.GlobalId, wall.GlobalId} + assert not new_guids.intersection(used_guids) + + def test_regenerate_guids_for_duplicates(self): + project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + wall1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + wall2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + wall1.GlobalId = wall2.GlobalId + used_guids = {project, wall1.GlobalId, wall2.GlobalId} + + ifcpatch.execute({"file": self.file, "recipe": "RegenerateGlobalIds", "arguments": [True]}) + new_guids = {project, wall1.GlobalId, wall2.GlobalId} + assert len(new_guids) == 3 + assert len(new_guids.intersection(used_guids)) == 2 + + +class TestRegenerateGlobalIdsIFC2X3(test.bootstrap.IFC2X3, TestRegenerateGlobalIds): + pass diff --git a/src/ifcsverchok/README.md b/src/ifcsverchok/README.md index cdc605f17b..e2c5944dfa 100644 --- a/src/ifcsverchok/README.md +++ b/src/ifcsverchok/README.md @@ -3,9 +3,9 @@ N.B.! IfcSverchok nodes are WIP. You can experience Blender crashes while using them, especially if you're undoing (ctrl/cmd-Z) something in the node tree. -## Packaged installation[](https://blenderbim.org/docs-python/ifcsverchok/installation.html#packaged-installation "Permalink to this headline") +## Packaged installation[](https://docs.ifcopenshell.org/ifcsverchok/installation.html#packaged-installation "Permalink to this headline") -IfcSverchok is packaged like a regular Blender add-on, so installation is the same as any other Blender add-on. [Download IfcSverchok here](https://blenderbim.org/builds/ifcsverchok-230823.zip). +IfcSverchok is packaged like a regular Blender add-on, so installation is the same as any other Blender add-on. [Download IfcSverchok here](https://github.com/IfcOpenShell/IfcOpenShell/releases/download/ifcsverchok-240417/ifcsverchok-240417.zip). Like all Blender add-ons, they can be installed using `Edit > Preferences > Addons > Install > Choose Downloaded ZIP > Enable Add-on Checkbox`. You can enable add-ons permanently by using `Save User Settings` from the Addons menu. diff --git a/src/ifcsverchok/__init__.py b/src/ifcsverchok/__init__.py index ad52a86687..8b45c3f712 100644 --- a/src/ifcsverchok/__init__.py +++ b/src/ifcsverchok/__init__.py @@ -30,9 +30,31 @@ bl_info = { import importlib import logging -logger = logging.getLogger('sverchok.ifc') + +logger = logging.getLogger("sverchok.ifc") + + +def ensure_addons_are_enabled(*addon_names: str) -> None: + errors = [] + for addon_name in addon_names: + try: + module = importlib.import_module(addon_name) + # `__addon_enabled__` is not present if addon wasn't enabled before + if not getattr(module, "__addon_enabled__", False): + errors.append(f"- Addon {addon_name} appears to be disabled, it should be enabled before IFC Sverchok.") + except ModuleNotFoundError: + errors.append(f"- Addon {addon_name} is not installed.") + + if errors: + raise Exception("Some issues were found trying to enable IFC Sverchok:\n" + "\n".join(errors)) + + +ensure_addons_are_enabled("blenderbim", "sverchok") + + from sverchok.ui.nodeview_space_menu import add_node_menu + def nodes_index(): return [ ( @@ -67,36 +89,15 @@ def nodes_index(): ] -node_categories = [ - { - "IFC": [ - "SvIfcCreateFile", - "SvIfcReadFile", - "SvIfcWriteFile", - "SvIfcCreateEntity", - "SvIfcCreateShape", - "SvIfcReadEntity", - "SvIfcPickIfcClass", - "SvIfcById", - "SvIfcByGuid", - "SvIfcByType", - "SvIfcByQuery", - "SvIfcAdd", - "SvIfcAddPset", - "SvIfcAddSpatialElement", - "SvIfcRemove", - "SvIfcGenerateGuid", - "SvIfcGetProperty", - "SvIfcGetAttribute", - "SvIfcSelectBlenderObjects", - "SvIfcApi", - "SvIfcBMeshToIfcRepr", - "SvIfcSverchokToIfcRepr", - "SvIfcCreateProject", - "SvIfcQuickProjectSetup", - ] - } -] +def make_node_categories() -> list[dict[str, list[str]]]: + node_categories = [{}] + for category, nodes in nodes_index(): + nodes = [node_name for idname, node_name in nodes] + node_categories[0][category] = nodes + return node_categories + + +node_categories = make_node_categories() def make_node_list(): @@ -117,6 +118,8 @@ reload_event = False import bpy from os.path import splitext import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.element from ifcsverchok.ifcstore import SvIfcStore @@ -175,7 +178,7 @@ class IFC_Sv_write_file(bpy.types.Operator): if not (spatial.is_a("IfcSite") or spatial.is_a("IfcBuilding")) and (spatial not in elements_in_buildings): elements = ifcopenshell.util.element.get_decomposition(spatial) ifcopenshell.api.run( - "aggregate.assign_object", file, product=spatial, relating_object=file.by_type("IfcBuilding")[0] + "aggregate.assign_object", file, products=[spatial], relating_object=file.by_type("IfcBuilding")[0] ) for building in file.by_type("IfcBuilding"): @@ -186,7 +189,10 @@ class IFC_Sv_write_file(bpy.types.Operator): for element in elements: if element not in elements_in_buildings: ifcopenshell.api.run( - "spatial.assign_container", file, product=element, relating_structure=file.by_type("IfcBuilding")[0] + "spatial.assign_container", + file, + products=[element], + relating_structure=file.by_type("IfcBuilding")[0], ) for building in file.by_type("IfcBuilding"): @@ -195,7 +201,7 @@ class IFC_Sv_write_file(bpy.types.Operator): if len(file.by_type("IfcSite")) == 0: ifcopenshell.api.run("root.create_entity", file, ifc_class="IfcSite", name="My Site") ifcopenshell.api.run( - "aggregate.assign_object", file, product=building, relating_object=file.by_type("IfcSite")[0] + "aggregate.assign_object", file, products=[building], relating_object=file.by_type("IfcSite")[0] ) try: if file.by_type("IfcSite")[0].Decomposes[0].RelatingObject.is_a("IfcProject"): @@ -205,7 +211,7 @@ class IFC_Sv_write_file(bpy.types.Operator): ifcopenshell.api.run( "aggregate.assign_object", file, - product=file.by_type("IfcSite")[0], + products=[file.by_type("IfcSite")[0]], relating_object=file.by_type("IfcProject")[0], ) self.file = file diff --git a/src/ifcsverchok/nodes/ifc/add_spatial_element.py b/src/ifcsverchok/nodes/ifc/add_spatial_element.py index 3948bc1e17..8015b4168c 100644 --- a/src/ifcsverchok/nodes/ifc/add_spatial_element.py +++ b/src/ifcsverchok/nodes/ifc/add_spatial_element.py @@ -21,6 +21,8 @@ import bpy from itertools import chain import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.element import ifcsverchok.helper from ifcsverchok.ifcstore import SvIfcStore @@ -34,9 +36,7 @@ from sverchok.data_structure import ( ) -class SvIfcAddSpatialElement( - bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore -): +class SvIfcAddSpatialElement(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore): bl_idname = "SvIfcAddSpatialElement" bl_label = "IFC Add Spatial Element" node_dict = {} @@ -66,16 +66,14 @@ class SvIfcAddSpatialElement( self.node_dict[hash(self)] = {} def draw_buttons(self, context, layout): - layout.operator( - "node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False - ).tooltip = "Add IfcElements to an IfcSpatialElement." + layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( + "Add IfcElements to an IfcSpatialElement." + ) def process(self): self.sv_input_names = [i.name for i in self.inputs] if hash(self) not in self.node_dict: - self.node_dict[ - hash(self) - ] = {} # happens if node is already on canvas when blender loads + self.node_dict[hash(self)] = {} # happens if node is already on canvas when blender loads if not self.node_dict[hash(self)]: self.node_dict[hash(self)].update(dict.fromkeys(self.sv_input_names, 0)) @@ -84,9 +82,7 @@ class SvIfcAddSpatialElement( edit = False edit_elements = False for i in range(len(self.inputs)): - input = self.inputs[self.sv_input_names[i]].sv_get( - deepcopy=True, default=[] - ) + input = self.inputs[self.sv_input_names[i]].sv_get(deepcopy=True, default=[]) if ( isinstance(self.node_dict[hash(self)][self.inputs[i].name], list) and input != self.node_dict[hash(self)][self.inputs[i].name] @@ -97,9 +93,7 @@ class SvIfcAddSpatialElement( self.node_dict[hash(self)][self.inputs[i].name] = input.copy() self.names = flatten_data(self.inputs["Names"].sv_get(), target_level=1) - self.ifc_class = flatten_data(self.inputs["IfcClass"].sv_get(), target_level=1)[ - 0 - ] + self.ifc_class = flatten_data(self.inputs["IfcClass"].sv_get(), target_level=1)[0] self.elements = ensure_min_nesting(self.inputs["Elements"].sv_get(), 2) if isinstance(self.elements[0][0], list): self.elements = [list(chain.from_iterable(el)) for el in self.elements] @@ -107,18 +101,13 @@ class SvIfcAddSpatialElement( raise Exception('Mandatory input "Element(s)" is missing.') return self.file = SvIfcStore.get_file() - self.elements = [ - [self.file.by_id(step_id) for step_id in element] - for element in self.elements - ] + self.elements = [[self.file.by_id(step_id) for step_id in element] for element in self.elements] if "len" not in self.node_dict[hash(self)]: self.node_dict[hash(self)]["len"] = 0 self.names = self.repeat_input_unique(self.names, len(self.elements)) - if (self.node_id not in SvIfcStore.id_map) or ( - len(self.elements) != self.node_dict[hash(self)]["len"] - ): + if (self.node_id not in SvIfcStore.id_map) or (len(self.elements) != self.node_dict[hash(self)]["len"]): self.remove() elements = self.create() self.node_dict[hash(self)]["len"] = len(self.elements) @@ -142,20 +131,18 @@ class SvIfcAddSpatialElement( ifc_class=self.ifc_class, ) for items in self.elements[i]: - if items.is_a("IfcSpatialElement") or items.is_a( - "IfcSpatialStructureElement" - ): + if items.is_a("IfcSpatialElement") or items.is_a("IfcSpatialStructureElement"): ifcopenshell.api.run( "aggregate.assign_object", self.file, - product=items, + products=[items], relating_object=result, ) else: ifcopenshell.api.run( "spatial.assign_container", self.file, - product=items, + products=[items], relating_structure=result, ) SvIfcStore.id_map.setdefault(self.node_id, []).append(result.id()) @@ -180,37 +167,35 @@ class SvIfcAddSpatialElement( for element in self.elements[i]: element_set = set([element]) for removed_element in subelements - element_set: - if removed_element.is_a( - "IfcSpatialElement" - ) or removed_element.is_a("IfcSpatialStructureElement"): + if removed_element.is_a("IfcSpatialElement") or removed_element.is_a( + "IfcSpatialStructureElement" + ): ifcopenshell.api.run( "aggregate.unassign_object", self.file, - product=removed_element, + products=[removed_element], relating_object=result, ) else: ifcopenshell.api.run( "spatial.unassign_container", self.file, - product=removed_element, + products=[removed_element], relating_object=result, ) for added_element in element_set - subelements: - if added_element.is_a( - "IfcSpatialElement" - ) or added_element.is_a("IfcSpatialStructureElement"): + if added_element.is_a("IfcSpatialElement") or added_element.is_a("IfcSpatialStructureElement"): ifcopenshell.api.run( "aggregate.assign_object", self.file, - product=added_element, + products=[added_element], relating_object=result, ) else: ifcopenshell.api.run( "spatial.assign_container", self.file, - product=added_element, + products=[added_element], relating_structure=result, ) spatial_ids.append(result.id()) @@ -228,8 +213,7 @@ class SvIfcAddSpatialElement( input = repeat_last_for_length(input, count, deepcopy=False) if input[0]: input = [ - a if not (s := sum(j == a for j in input[:i])) else f"{a}-{s+1}" - for i, a in enumerate(input) + a if not (s := sum(j == a for j in input[:i])) else f"{a}-{s+1}" for i, a in enumerate(input) ] # add number to duplicates return input diff --git a/src/ifcsverchok/nodes/ifc/write_file.py b/src/ifcsverchok/nodes/ifc/write_file.py index d832d590cf..b9f03c6e1a 100644 --- a/src/ifcsverchok/nodes/ifc/write_file.py +++ b/src/ifcsverchok/nodes/ifc/write_file.py @@ -19,6 +19,8 @@ from os.path import abspath, splitext import bpy import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.element import ifcsverchok.helper from ifcsverchok.ifcstore import SvIfcStore from bpy.props import StringProperty, BoolProperty @@ -26,9 +28,7 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, flatten_data -class SvIfcWriteFile( - bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore -): +class SvIfcWriteFile(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore): """ Triggers: Ifc write to file Tooltip: Write active Sverchok Ifc file to path @@ -39,9 +39,7 @@ class SvIfcWriteFile( self.process() self.refresh_local = False - refresh_local: BoolProperty( - name="Write", description="Write to file", update=refresh_node_local - ) + refresh_local: BoolProperty(name="Write", description="Write to file", update=refresh_node_local) bl_idname = "SvIfcWriteFile" bl_label = "IFC Write File" @@ -57,9 +55,12 @@ class SvIfcWriteFile( def draw_buttons(self, context, layout): row = layout.row(align=True) - row.operator( - "node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False - ).tooltip = "Writes active Ifc file to path.\n It will overwrite an existing file.\n N.B.! It's recommended to create a fresh IFC File using the 're-run all nodes' button in IfcSverchok panel before saving." + tooltip = ( + "Writes active Ifc file to path.\n " + "It will overwrite an existing file.\n" + "N.B.! It's recommended to create a fresh IFC File using the 're-run all nodes' button in IfcSverchok panel before saving." + ) + row.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = tooltip row.prop(self, "refresh_local", icon="FILE_REFRESH") def process(self): @@ -79,26 +80,20 @@ class SvIfcWriteFile( def ensure_hirarchy(self, file): elements_in_buildings = [] if not 0 <= 0 < len(file.by_type("IfcBuilding")): - my_building = ifcopenshell.api.run( - "root.create_entity", file, ifc_class="IfcBuilding", name="My Building" - ) + my_building = ifcopenshell.api.run("root.create_entity", file, ifc_class="IfcBuilding", name="My Building") elements = ifcopenshell.util.element.get_decomposition(my_building) else: for building in file.by_type("IfcBuilding"): elements = ifcopenshell.util.element.get_decomposition(building) elements_in_buildings.extend(elements) - for spatial in file.by_type("IfcSpatialElement") or file.by_type( - "IfcSpatialStructureElement" - ): - if not (spatial.is_a("IfcSite") or spatial.is_a("IfcBuilding")) and ( - spatial not in elements_in_buildings - ): + for spatial in file.by_type("IfcSpatialElement") or file.by_type("IfcSpatialStructureElement"): + if not (spatial.is_a("IfcSite") or spatial.is_a("IfcBuilding")) and (spatial not in elements_in_buildings): elements = ifcopenshell.util.element.get_decomposition(spatial) ifcopenshell.api.run( "aggregate.assign_object", file, - product=spatial, + products=[spatial], relating_object=file.by_type("IfcBuilding")[0], ) @@ -113,7 +108,7 @@ class SvIfcWriteFile( ifcopenshell.api.run( "spatial.assign_container", file, - product=element, + products=[element], relating_structure=file.by_type("IfcBuilding")[0], ) @@ -121,28 +116,22 @@ class SvIfcWriteFile( elements = ifcopenshell.util.element.get_decomposition(building) if not building.Decomposes: if not 0 <= 0 < len(file.by_type("IfcSite")): - ifcopenshell.api.run( - "root.create_entity", file, ifc_class="IfcSite", name="My Site" - ) + ifcopenshell.api.run("root.create_entity", file, ifc_class="IfcSite", name="My Site") ifcopenshell.api.run( "aggregate.assign_object", file, - product=building, + products=[building], relating_object=file.by_type("IfcSite")[0], ) try: - if ( - file.by_type("IfcSite")[0] - .Decomposes[0] - .RelatingObject.is_a("IfcProject") - ): + if file.by_type("IfcSite")[0].Decomposes[0].RelatingObject.is_a("IfcProject"): continue except IndexError: pass ifcopenshell.api.run( "aggregate.assign_object", file, - product=file.by_type("IfcSite")[0], + products=[file.by_type("IfcSite")[0]], relating_object=file.by_type("IfcProject")[0], ) self.file = file diff --git a/src/ifctester/Makefile b/src/ifctester/Makefile index 597a8ccb99..7ae89cba16 100644 --- a/src/ifctester/Makefile +++ b/src/ifctester/Makefile @@ -1,3 +1,21 @@ +# IfcTester - IDS based model auditing +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcTester. +# +# IfcTester is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcTester is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcTester. If not, see . + VERSION:=`date '+%y%m%d'` SED:=sed -i ifeq ($(UNAME_S),Darwin) diff --git a/src/ifctester/README.md b/src/ifctester/README.md index 59b9a4af20..f145f00804 100644 --- a/src/ifctester/README.md +++ b/src/ifctester/README.md @@ -4,6 +4,23 @@ With **IfcTester**, you can author and read **Information Delivery Specification ## How to use it +### Command line use + +.. code-block:: bash + + # run console reporter + python -m ifctester example.ids example.ifc + python -m ifctester example.ids example.ifc -r Html -o report.html + +Available flags: + +- ``-r`` / ``--reporter``: The reporting method to view audit results. Availabe reporters: Console, Txt, Json, Html, Ods, Bcf +- ``--no-color``: Disable colour output (supported by Console reporting). +- ``--excel-safe``: Make sure exported ODS is safely exported for Excel. +- ``-o`` / ``--output``: Output file (supported for all types of reporting except Console). + +### Code example + ```python import ifcopenshell from ifctester import ids, reporter @@ -16,14 +33,13 @@ my_ids = ids.Ids(title="My IDS") my_spec = ids.Specification(name="My first specification") my_spec.applicability.append(ids.Entity(name="IFCWALL")) property = ids.Property( - name="IsExternal", + baseName="IsExternal", value="TRUE", propertySet="Pset_WallCommon", - datatype="IfcBoolean", + dataType="IfcBoolean", uri="https://identifier.buildingsmart.org/uri/.../prop/LoadBearing", instructions="Walls need to be load bearing.", - minOccurs=1, - maxOccurs="unbounded") + cardinality="required") my_spec.requirements.append(property) my_ids.specifications.append(my_spec) diff --git a/src/ifctester/ifctester/__main__.py b/src/ifctester/ifctester/__main__.py index c5c70cd9c2..a49c4c1094 100644 --- a/src/ifctester/ifctester/__main__.py +++ b/src/ifctester/ifctester/__main__.py @@ -31,10 +31,13 @@ parser.add_argument( "-r", "--reporter", type=str, help="The reporting method to view audit results", default="Console" ) parser.add_argument( - "--no-color", help="Disable colour output supported by Console reporting", action="store_true" + "--no-color", help="Disable colour output (supported by Console reporting)", action="store_true" ) parser.add_argument( - "-o", "--output", help="Output file supported by Json reporting" + "--excel-safe", help="Make sure exported ODS is safely exported for Excel", action="store_true" +) +parser.add_argument( + "-o", "--output", help="Output file (supported for all types of reporting except Console)" ) args = parser.parse_args() @@ -56,7 +59,7 @@ elif args.reporter == "Json": elif args.reporter == "Html": engine = reporter.Html(specs) elif args.reporter == "Ods": - engine = reporter.Ods(specs) + engine = reporter.Ods(specs, excel_safe=args.excel_safe) elif args.reporter == "Bcf": engine = reporter.Bcf(specs) diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py index d57e56d06b..867e06b36c 100644 --- a/src/ifctester/ifctester/facet.py +++ b/src/ifctester/ifctester/facet.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcTester. If not, see . +from __future__ import annotations import re import builtins import ifcopenshell.util.unit @@ -23,7 +24,11 @@ import ifcopenshell.util.element import ifcopenshell.util.classification from functools import lru_cache from xmlschema.validators import identities -from typing import List, Union +from typing import Union, Optional, Any, Literal, TYPE_CHECKING, TypedDict +from logging import Logger + +if TYPE_CHECKING: + from .ids import Specification def cast_to_value(from_value, to_value): @@ -53,52 +58,66 @@ def get_psets(element): return ifcopenshell.util.element.get_psets(element) +Cardinality = Literal["required", "optional", "prohibited"] + + +class FacetFailure(TypedDict): + element: ifcopenshell.entity_instance + reason: str + + class Facet: + cardinality: Cardinality + def __init__(self, *parameters): self.status = None - self.failed_entities: List[Facet] = [] - self.failed_reasons: List[str] = [] + self.failures: list[FacetFailure] = [] for i, name in enumerate(self.parameters): setattr(self, name.replace("@", ""), parameters[i]) - def asdict(self, clause_type): + def asdict(self, clause_type: str) -> dict[str, Any]: results = {} for name in self.parameters: value = getattr(self, name.replace("@", "")) if value is not None: + if name == "@dataType": + value = value.upper() results[name] = value if "@" in name else self.to_ids_value(value) if clause_type == "applicability": - for key in ["@uri", "@instructions", "@minOccurs", "@maxOccurs"]: + for key in ["@uri", "@instructions", "@cardinality"]: results.pop(key, None) return results def parse(self, xml): - setattr(self, "minOccurs", 1) - setattr(self, "maxOccurs", 1) + setattr(self, "cardinality", "required") for name, value in xml.items(): name = name.replace("@", "") if isinstance(value, dict) and "simpleValue" in value.keys(): setattr(self, name, value["simpleValue"]) elif isinstance(value, dict) and "restriction" in value.keys(): - setattr(self, name, Restriction().parse(value["restriction"][0])) - # TODO handle more than one restriction: return [restriction(r) for r in v["restriction"]] + setattr(self, name, Restriction().parse(value["restriction"])) else: setattr(self, name, value) return self def filter( - self, ifc_file: ifcopenshell.file, elements: List[ifcopenshell.entity_instance] - ) -> List[ifcopenshell.entity_instance]: + self, ifc_file: ifcopenshell.file, elements: list[ifcopenshell.entity_instance] + ) -> list[ifcopenshell.entity_instance]: return [e for e in elements if self(e)] - def to_string(self, clause_type, specification=None, requirement=None): + def to_string( + self, + clause_type: str, + specification: Optional[Specification] = None, + requirement: Optional[Facet] = None, + ) -> str: if clause_type == "applicability": templates = self.applicability_templates elif clause_type == "requirement": is_prohibited = False if specification.maxOccurs == 0: is_prohibited = not is_prohibited - if requirement.maxOccurs == 0: + if requirement.cardinality == "prohibited": is_prohibited = not is_prohibited templates = self.prohibited_templates if is_prohibited else self.requirement_templates @@ -115,7 +134,7 @@ class Facet: if total_replacements == total_variables: return template - def to_ids_value(self, parameter): + def to_ids_value(self, parameter: Union[str, Restriction, list]) -> dict[str, Any]: if isinstance(parameter, str): parameter_dict = {"simpleValue": parameter} elif isinstance(parameter, Restriction): @@ -130,13 +149,11 @@ class Facet: raise Exception(str(parameter) + " was not able to be converted into 'Parameter_dict'") return parameter_dict - def get_usage(self): - if self.minOccurs != 0: - return "required" - elif self.minOccurs == 0 and self.maxOccurs != 0: - return "optional" - elif self.maxOccurs == 0: - return "prohibited" + def get_usage(self) -> Cardinality: + return self.cardinality + + def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> Result: + raise NotImplementedError class Entity(Facet): @@ -156,7 +173,9 @@ class Entity(Facet): ] super().__init__(name, predefinedType, instructions) - def filter(self, ifc_file, elements): + def filter( + self, ifc_file: ifcopenshell.file, elements: Optional[list[ifcopenshell.entity_instance]] = None + ) -> list[ifcopenshell.entity_instance]: if isinstance(elements, list): return super().filter(ifc_file, elements) @@ -179,7 +198,7 @@ class Entity(Facet): return [r for r in results if self(r)] return results - def __call__(self, inst, logger=None): + def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> EntityResult: is_pass = inst.is_a().upper() == self.name reason = None @@ -197,8 +216,8 @@ class Entity(Facet): class Attribute(Facet): - def __init__(self, name="Name", value=None, minOccurs=None, maxOccurs=None, instructions=None): - self.parameters = ["name", "value", "@minOccurs", "@maxOccurs", "@instructions"] + def __init__(self, name="Name", value=None, cardinality: Cardinality = "required", instructions=None): + self.parameters = ["name", "value", "@cardinality", "@instructions"] self.applicability_templates = [ "Data where the {name} is {value}", "Data where the {name} is provided", @@ -211,11 +230,11 @@ class Attribute(Facet): "The {name} shall not be {value}", "The {name} shall not be provided", ] - super().__init__(name, value, minOccurs, maxOccurs, instructions) + super().__init__(name, value, cardinality, instructions) def filter( - self, ifc_file: ifcopenshell.file, elements: Union[ifcopenshell.entity_instance, None] - ) -> List[ifcopenshell.entity_instance]: + self, ifc_file: ifcopenshell.file, elements: Optional[list[ifcopenshell.entity_instance]] + ) -> list[ifcopenshell.entity_instance]: if isinstance(elements, list): return super().filter(ifc_file, elements) @@ -241,8 +260,8 @@ class Attribute(Facet): return results - def __call__(self, inst, logger=None): - if self.minOccurs == 0 and self.maxOccurs != 0: + def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> AttributeResult: + if self.cardinality == "optional": return AttributeResult(True) if isinstance(self.name, str): @@ -323,45 +342,42 @@ class Attribute(Facet): reason = {"type": "VALUE", "actual": value} break - if self.maxOccurs == 0: + if self.cardinality == "prohibited": return AttributeResult(not is_pass, {"type": "PROHIBITED"}) return AttributeResult(is_pass, reason) class Classification(Facet): - def __init__(self, value=None, system=None, uri=None, minOccurs=None, maxOccurs="unbounded", instructions=None): - self.parameters = ["value", "system", "@uri", "@minOccurs", "@maxOccurs", "@instructions"] + def __init__(self, value=None, system=None, uri=None, cardinality: Cardinality = "required", instructions=None): + self.parameters = ["value", "system", "@uri", "@cardinality", "@instructions"] self.applicability_templates = [ "Data having a {system} reference of {value}", "Data classified using {system}", "Data classified as {value}", - "Classified data", ] self.requirement_templates = [ "Shall have a {system} reference of {value}", "Shall be classified using {system}", "Shall be classified as {value}", - "Shall be classified", ] self.prohibited_templates = [ "Shall not have a {system} reference of {value}", "Shall not be classified using {system}", "Shall not be classified as {value}", - "Shall not be classified", ] - super().__init__(value, system, uri, minOccurs, maxOccurs, instructions) + super().__init__(value, system, uri, cardinality, instructions) def filter( - self, ifc_file: ifcopenshell.file, elements: Union[ifcopenshell.entity_instance, None] - ) -> List[ifcopenshell.entity_instance]: + self, ifc_file: ifcopenshell.file, elements: Optional[list[ifcopenshell.entity_instance]] + ) -> list[ifcopenshell.entity_instance]: if isinstance(elements, list): return super().filter(ifc_file, elements) return ifc_file.by_type("IfcObjectDefinition") - def __call__(self, inst, logger=None): - if self.minOccurs == 0 and self.maxOccurs != 0: - return ClassificationResult(True) + def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> ClassificationResult: + if self.cardinality == "optional": + return ClassificationResult(True) # Is this really the correct behaviour? leaf_references = ifcopenshell.util.classification.get_references(inst) @@ -381,13 +397,13 @@ class Classification(Facet): if not is_pass: reason = {"type": "VALUE", "actual": values} - if is_pass and self.system: + if is_pass: systems = [ifcopenshell.util.classification.get_classification(r).Name for r in references] is_pass = any([self.system == s for s in systems]) if not is_pass: reason = {"type": "SYSTEM", "actual": systems} - if self.maxOccurs == 0: + if self.cardinality == "prohibited": return ClassificationResult(not is_pass, {"type": "PROHIBITED"}) return ClassificationResult(is_pass, reason) @@ -398,11 +414,10 @@ class PartOf(Facet): name="IFCWALL", predefinedType=None, relation=None, - minOccurs=None, - maxOccurs="unbounded", + cardinality: Cardinality = "required", instructions=None, ): - self.parameters = ["name", "predefinedType", "@relation", "@minOccurs", "@maxOccurs", "@instructions"] + self.parameters = ["name", "predefinedType", "@relation", "@cardinality", "@instructions"] self.applicability_templates = [ "An element with an {relation} relationship with an {name}", "An element with an {relation} relationship", @@ -415,16 +430,16 @@ class PartOf(Facet): "An element must not have an {relation} relationship with an {name}", "An element must not have an {relation} relationship", ] - super().__init__(name, predefinedType, relation, minOccurs, maxOccurs, instructions) + super().__init__(name, predefinedType, relation, cardinality, instructions) def filter( - self, ifc_file: ifcopenshell.file, elements: Union[ifcopenshell.entity_instance, None] - ) -> List[ifcopenshell.entity_instance]: + self, ifc_file: ifcopenshell.file, elements: Optional[list[ifcopenshell.entity_instance]] + ) -> list[ifcopenshell.entity_instance]: if isinstance(elements, list): return super().filter(ifc_file, elements) return list(ifc_file) # Lazy - def asdict(self, clause_type): + def asdict(self, clause_type: str) -> dict[str, Any]: results = super().asdict(clause_type) entity = {} if "name" in results: @@ -443,10 +458,7 @@ class PartOf(Facet): del xml["entity"] return super().parse(xml) - def __call__(self, inst, logger=None): - if self.minOccurs == 0 and self.maxOccurs != 0: - return PartOfResult(True) - + def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> PartOfResult: reason = None if not self.relation: is_pass = False @@ -536,8 +548,14 @@ class PartOf(Facet): nest = self.get_nested_whole(nest) if not is_pass: reason = {"type": "ENTITY", "actual": ancestors} - elif self.relation == "IFCRELVOIDSELEMENT": - building_element = self.get_voided_element(inst) + elif self.relation == "IFCRELVOIDSELEMENT IFCRELFILLSELEMENT": + if inst.is_a("IfcOpeningElement"): + building_element = self.get_voided_element(inst) + else: + building_element = None + opening = self.get_filled_opening(inst) + if opening: + building_element = self.get_voided_element(opening) is_pass = building_element is not None if not is_pass: reason = {"type": "NOVALUE"} @@ -551,23 +569,8 @@ class PartOf(Facet): is_pass = True if not is_pass: reason = {"type": "ENTITY", "actual": building_element} - elif self.relation == "IFCRELFILLSELEMENT": - opening = self.filled_opening(inst) - is_pass = opening is not None - if not is_pass: - reason = {"type": "NOVALUE"} - if is_pass and self.name: - is_pass = False - if opening.is_a().upper() == self.name: - if self.predefinedType: - if ifcopenshell.util.element.get_predefined_type(opening) == self.predefinedType: - is_pass = True - else: - is_pass = True - if not is_pass: - reason = {"type": "ENTITY", "actual": opening} - if self.maxOccurs == 0: + if self.cardinality == "prohibited": return PartOfResult(not is_pass, {"type": "PROHIBITED"}) return PartOfResult(is_pass, reason) @@ -605,41 +608,39 @@ class Property(Facet): def __init__( self, propertySet="Property_Set", - name="PropertyName", + baseName="PropertyName", value=None, - datatype=None, + dataType=None, uri=None, - minOccurs=None, - maxOccurs="unbounded", + cardinality: Cardinality = "required", instructions=None, ): self.parameters = [ "propertySet", - "name", + "baseName", "value", - "@datatype", + "@dataType", "@uri", - "@minOccurs", - "@maxOccurs", + "@cardinality", "@instructions", ] self.applicability_templates = [ - "Elements with {name} data of {value} in the dataset {propertySet}", - "Elements with {name} data in the dataset {propertySet}", + "Elements with {baseName} data of {value} in the dataset {propertySet}", + "Elements with {baseName} data in the dataset {propertySet}", ] self.requirement_templates = [ - "{name} data shall be {value} and in the dataset {propertySet}", - "{name} data shall be provided in the dataset {propertySet}", + "{baseName} data shall be {value} and in the dataset {propertySet}", + "{baseName} data shall be provided in the dataset {propertySet}", ] self.prohibited_templates = [ - "{name} data shall not be {value} and in the dataset {propertySet}", - "{name} data shall not be provided in the dataset {propertySet}", + "{baseName} data shall not be {value} and in the dataset {propertySet}", + "{baseName} data shall not be provided in the dataset {propertySet}", ] - super().__init__(propertySet, name, value, datatype, uri, minOccurs, maxOccurs, instructions) + super().__init__(propertySet, baseName, value, dataType, uri, cardinality, instructions) def filter( - self, ifc_file: ifcopenshell.file, elements: Union[ifcopenshell.entity_instance, None] - ) -> List[ifcopenshell.entity_instance]: + self, ifc_file: ifcopenshell.file, elements: Optional[list[ifcopenshell.entity_instance]] + ) -> list[ifcopenshell.entity_instance]: if isinstance(elements, list): return super().filter(ifc_file, elements) if ifc_file.schema == "IFC2X3": @@ -650,8 +651,8 @@ class Property(Facet): + ifc_file.by_type("IfcProfileDef") ) - def __call__(self, inst, logger=None): - if self.minOccurs == 0 and self.maxOccurs != 0: + def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> PropertyResult: + if self.cardinality == "optional": return PropertyResult(True) if isinstance(self.propertySet, str): @@ -671,18 +672,18 @@ class Property(Facet): props = {} for pset_name, pset_props in psets.items(): props[pset_name] = {} - if isinstance(self.name, str): - prop = pset_props.get(self.name) + if isinstance(self.baseName, str): + prop = pset_props.get(self.baseName) if prop == "UNKNOWN" and [ p for p in self.get_properties(inst.wrapped_data.file.by_id(pset_props["id"])) - if p.Name == self.name + if p.Name == self.baseName ][0].NominalValue.is_a("IfcLogical"): pass elif prop is not None and prop != "": - props[pset_name][self.name] = prop + props[pset_name][self.baseName] = prop else: - props[pset_name] = {k: v for k, v in pset_props.items() if k == self.name} + props[pset_name] = {k: v for k, v in pset_props.items() if k == self.baseName} if not bool(props[pset_name]): is_pass = False @@ -701,9 +702,9 @@ class Property(Facet): elif prop_entity.is_a("IfcPropertySingleValue"): data_type = prop_entity.NominalValue.is_a() - if data_type.lower() != self.datatype.lower(): + if self.dataType and data_type.lower() != self.dataType.lower(): is_pass = False - reason = {"type": "DATATYPE", "actual": data_type, "datatype": self.datatype} + reason = {"type": "DATATYPE", "actual": data_type, "dataType": self.dataType} break unit = ifcopenshell.util.unit.get_property_unit(prop_entity, inst.wrapped_data.file) @@ -720,9 +721,9 @@ class Property(Facet): prop_schema = prop_entity.wrapped_data.declaration().as_entity() data_type = prop_schema.attribute_by_index(3).type_of_attribute().declared_type().name() - if data_type.lower() != self.datatype.lower(): + if self.dataType and data_type.lower() != self.dataType.lower(): is_pass = False - reason = {"type": "DATATYPE", "actual": data_type, "datatype": self.datatype} + reason = {"type": "DATATYPE", "actual": data_type, "dataType": self.dataType} break unit = ifcopenshell.util.unit.get_property_unit(prop_entity, inst.wrapped_data.file) @@ -740,9 +741,9 @@ class Property(Facet): reason = {"type": "NOVALUE"} break data_type = prop_entity.EnumerationValues[0].is_a() - if data_type.lower() != self.datatype.lower(): + if self.dataType and data_type.lower() != self.dataType.lower(): is_pass = False - reason = {"type": "DATATYPE", "actual": data_type, "datatype": self.datatype} + reason = {"type": "DATATYPE", "actual": data_type, "dataType": self.dataType} break elif prop_entity.is_a("IfcPropertyListValue"): if not prop_entity.ListValues: @@ -750,9 +751,9 @@ class Property(Facet): reason = {"type": "NOVALUE"} break data_type = prop_entity.ListValues[0].is_a() - if data_type.lower() != self.datatype.lower(): + if self.dataType and data_type.lower() != self.dataType.lower(): is_pass = False - reason = {"type": "DATATYPE", "actual": data_type, "datatype": self.datatype} + reason = {"type": "DATATYPE", "actual": data_type, "dataType": self.dataType} break unit = ifcopenshell.util.unit.get_property_unit(prop_entity, inst.wrapped_data.file) if unit: @@ -773,9 +774,9 @@ class Property(Facet): if value is not None: data_type = value.is_a() values.append(value.wrappedValue) - if data_type.lower() != self.datatype.lower(): + if self.dataType and data_type.lower() != self.dataType.lower(): is_pass = False - reason = {"type": "DATATYPE", "actual": data_type, "datatype": self.datatype} + reason = {"type": "DATATYPE", "actual": data_type, "dataType": self.dataType} break unit = ifcopenshell.util.unit.get_property_unit(prop_entity, inst.wrapped_data.file) if unit: @@ -798,7 +799,7 @@ class Property(Facet): if not column_values: continue data_type = column_values[0].is_a() - if data_type.lower() == self.datatype.lower(): + if self.dataType and data_type.lower() == self.dataType.lower(): column_values = [v.wrappedValue for v in column_values] unit = units[f"{attribute}Unit"] if unit: @@ -815,7 +816,7 @@ class Property(Facet): values.extend(column_values) if not values: is_pass = False - reason = {"type": "DATATYPE", "actual": data_type, "datatype": self.datatype} + reason = {"type": "DATATYPE", "actual": data_type, "dataType": self.dataType} break props[pset_name][prop_entity.Name] = values else: @@ -868,7 +869,7 @@ class Property(Facet): reason = {"type": "VALUE", "actual": value} break - if self.maxOccurs == 0: + if self.cardinality == "prohibited": return PropertyResult(not is_pass, {"type": "PROHIBITED"}) return PropertyResult(is_pass, reason) @@ -888,8 +889,8 @@ class Property(Facet): class Material(Facet): - def __init__(self, value=None, uri=None, minOccurs=None, maxOccurs="unbounded", instructions=None): - self.parameters = ["value", "@uri", "@minOccurs", "@maxOccurs", "@instructions"] + def __init__(self, value=None, uri=None, cardinality: Cardinality = "required", instructions=None): + self.parameters = ["value", "@uri", "@cardinality", "@instructions"] self.applicability_templates = [ "All data with a {value} material", "All data with a material", @@ -902,17 +903,17 @@ class Material(Facet): "Shall not have a material of {value}", "Shall not have a material", ] - super().__init__(value, uri, minOccurs, maxOccurs, instructions) + super().__init__(value, uri, cardinality, instructions) def filter( - self, ifc_file: ifcopenshell.file, elements: Union[ifcopenshell.entity_instance, None] - ) -> List[ifcopenshell.entity_instance]: + self, ifc_file: ifcopenshell.file, elements: Optional[list[ifcopenshell.entity_instance]] + ) -> list[ifcopenshell.entity_instance]: if isinstance(elements, list): return super().filter(ifc_file, elements) return ifc_file.by_type("IfcObjectDefinition") - def __call__(self, inst, logger=None): - if self.minOccurs == 0 and self.maxOccurs != 0: + def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> MaterialResult: + if self.cardinality == "optional": return MaterialResult(True) material = ifcopenshell.util.element.get_material(inst, should_skip_usage=True) @@ -963,7 +964,7 @@ class Material(Facet): if not is_pass: reason = {"type": "VALUE", "actual": values} - if self.maxOccurs == 0: + if self.cardinality == "prohibited": return MaterialResult(not is_pass, {"type": "PROHIBITED"}) return MaterialResult(is_pass, reason) @@ -989,7 +990,7 @@ class Restriction: self.options[key] = [v["@value"] for v in value] return self - def asdict(self): + def asdict(self) -> dict[str, Any]: result = {"@base": "xs:" + self.base} for constraint, value in self.options.items(): value = [value] if not isinstance(value, list) else value @@ -1108,7 +1109,7 @@ class PropertyResult(Result): elif self.reason["type"] == "NOVALUE": return "The property set does not contain the required property" elif self.reason["type"] == "DATATYPE": - return f"The property's data type \"{str(self.reason['actual'])}\" does not match the required data type of \"{str(self.reason['datatype'])}\"" + return f"The property's data type \"{str(self.reason['actual'])}\" does not match the required data type of \"{str(self.reason['dataType'])}\"" elif self.reason["type"] == "VALUE": if isinstance(self.reason["actual"], list): if len(self.reason["actual"]) == 1: diff --git a/src/ifctester/ifctester/ids.py b/src/ifctester/ifctester/ids.py index b18b162e94..4703b0445a 100644 --- a/src/ifctester/ifctester/ids.py +++ b/src/ifctester/ifctester/ids.py @@ -16,19 +16,38 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcTester. If not, see . +from __future__ import annotations import os import datetime +import ifcopenshell from xmlschema import XMLSchema from xmlschema import etree_tostring from xml.etree import ElementTree as ET -from .facet import Facet, Entity, Attribute, Classification, Property, PartOf, Material, Restriction, get_pset, get_psets -from typing import List, Set +from .facet import ( + Facet, + Entity, + Attribute, + Classification, + Property, + PartOf, + Material, + Restriction, + get_pset, + get_psets, + Cardinality, + FacetFailure, +) +from typing import List, Optional, Union, overload, Literal cwd = os.path.dirname(os.path.realpath(__file__)) schema = None -def open(filepath, validate=False): +@overload +def open(filepath: str, validate: Literal[False] = False) -> Ids: ... +@overload +def open(filepath: str, validate: Literal[True]) -> None: ... +def open(filepath: str, validate=False) -> Union[Ids, None]: if validate: get_schema().validate(filepath) return Ids().parse( @@ -46,7 +65,7 @@ def get_schema(): class Ids: def __init__( self, - title="Untitled", + title: Optional[str] = "Untitled", copyright=None, version=None, description=None, @@ -89,7 +108,7 @@ class Ids: "@xmlns": "http://standards.buildingsmart.org/IDS", "@xmlns:xs": "http://www.w3.org/2001/XMLSchema", "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", - "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/0.9.6/ids.xsd", + "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/0.9.7/ids.xsd", "info": info, "specifications": {"specification": []}, } @@ -120,7 +139,7 @@ class Ids: ET.ElementTree(get_schema().encode(self.asdict())).write(filepath, encoding="utf-8", xml_declaration=True) return get_schema().is_valid(filepath) - def validate(self, ifc_file, filter_version=False, filepath=None): + def validate(self, ifc_file: ifcopenshell.file, filter_version=False, filepath: Optional[str] = None) -> None: if filepath: self.filepath = filepath self.filename = os.path.basename(filepath) @@ -147,14 +166,14 @@ class Specification: self.name = name or "Unnamed" self.applicability: List[Facet] = [] self.requirements: List[Facet] = [] - self.minOccurs = minOccurs - self.maxOccurs = maxOccurs + self.minOccurs: Union[int, str] = minOccurs + self.maxOccurs: Union[int, str] = maxOccurs self.ifcVersion = ifcVersion self.identifier = identifier self.description = description self.instructions = instructions - self.applicable_entities: List[Entity] = [] + self.applicable_entities: list[ifcopenshell.entity_instance] = [] self.status = None def asdict(self): @@ -164,7 +183,7 @@ class Specification: "applicability": {}, "requirements": {}, } - for attribute in ["identifier", "description", "instructions", "minOccurs", "maxOccurs"]: + for attribute in ["identifier", "description", "instructions"]: value = getattr(self, attribute) if value is not None: results[f"@{attribute}"] = value @@ -181,20 +200,25 @@ class Specification: for facet_type in ("entity", "partOf", "classification", "attribute", "property", "material"): if facet_type in facets: results[clause_type][facet_type] = facets[facet_type] + if clause_type == "applicability": + for attribute in ["minOccurs", "maxOccurs"]: + value = getattr(self, attribute) + if value is not None: + results[clause_type][f"@{attribute}"] = value return results def parse(self, ids_dict): self.name = ids_dict.get("@name", "") self.description = ids_dict.get("@description", "") self.instructions = ids_dict.get("@instructions", "") - self.minOccurs = ids_dict["@minOccurs"] - self.maxOccurs = ids_dict["@maxOccurs"] + self.minOccurs = ids_dict.get("applicability", {}).get("@minOccurs", 0) + self.maxOccurs = ids_dict.get("applicability", {}).get("@maxOccurs", "unbounded") self.ifcVersion = ids_dict["@ifcVersion"] self.applicability = ( - self.parse_clause(ids_dict["applicability"]) if ids_dict.get("applicability") is not None else [] + self.parse_clause(ids_dict["applicability"]) if ids_dict.get("applicability", None) is not None else [] ) self.requirements = ( - self.parse_clause(ids_dict["requirements"]) if ids_dict.get("requirements") is not None else [] + self.parse_clause(ids_dict["requirements"]) if ids_dict.get("requirements", None) is not None else [] ) return self @@ -213,13 +237,13 @@ class Specification: def reset_status(self): self.applicable_entities.clear() - self.failed_entities: Set[Entity] = set() + self.failed_entities: set[ifcopenshell.entity_instance] = set() for facet in self.requirements: facet.status = None - facet.failed_entities.clear() + facet.failures.clear() self.status = None - def validate(self, ifc_file, filter_version=False): + def validate(self, ifc_file: ifcopenshell.file, filter_version=False) -> None: if filter_version and ifc_file.schema not in self.ifcVersion: return @@ -243,42 +267,46 @@ class Specification: self.applicable_entities.append(element) for facet in self.requirements: result = facet(element) - if self.maxOccurs == 0: - prohibited = bool(result) - else: - prohibited = not bool(result) - if prohibited: - self.failed_entities.add(element) - facet.failed_entities.append(element) - facet.failed_reasons.append(str(result)) - - for facet in self.requirements: - if facet.minOccurs != 0: - facet.status = not bool(facet.failed_entities) - elif facet.minOccurs == 0 and facet.maxOccurs != 0: - facet.status = True - elif facet.maxOccurs == 0: - facet.status = bool(facet.failed_entities) + is_pass = bool(result) + if self.maxOccurs != 0: # This is a required or optional specification + if not is_pass: + self.failed_entities.add(element) + facet.failures.append(FacetFailure(element=element, reason=str(result))) + else: # This is a prohibited specification + if is_pass: + self.failed_entities.add(element) + facet.failures.append(FacetFailure(element=element, reason=str(result))) self.status = True - if self.minOccurs != 0: + for facet in self.requirements: + facet.status = not bool(facet.failures) + if not facet.status: + self.status = False + + if self.minOccurs != 0: # Required specification if not self.applicable_entities: self.status = False for facet in self.requirements: facet.status = False - elif self.failed_entities: - self.status = False - elif self.minOccurs == 0 and self.maxOccurs != 0: - if self.failed_entities: - self.status = False - elif self.maxOccurs == 0: - if (len(self.applicable_entities)) > 0: + elif self.maxOccurs == 0: # Prohibited specification + if self.applicable_entities and not self.requirements: self.status = False - def get_usage(self): + def get_usage(self) -> Cardinality: if self.minOccurs != 0: return "required" elif self.minOccurs == 0 and self.maxOccurs != 0: return "optional" elif self.maxOccurs == 0: return "prohibited" + + def set_usage(self, usage: Cardinality) -> None: + if usage == "optional": + self.minOccurs = 0 + self.maxOccurs = "unbounded" + elif usage == "prohibited": + self.minOccurs = 0 + self.maxOccurs = 0 + else: # required + self.minOccurs = 1 + self.maxOccurs = "unbounded" diff --git a/src/ifctester/ifctester/ids.xsd b/src/ifctester/ifctester/ids.xsd index 0d2a595e72..62bb820697 100644 --- a/src/ifctester/ifctester/ids.xsd +++ b/src/ifctester/ifctester/ids.xsd @@ -1,5 +1,5 @@ - - + + @@ -40,20 +40,20 @@ - + - + - + @@ -62,26 +62,53 @@ - + + + - - + + + + the moniker 'baseName' is chosen to clarify that the data needs to reference the property name as stored in the IFC file, + which might differ from the multiple language-dependent presentations (e.g. 'FireRating' vs. 'Fire rating'). + + + + + + + Depending on the dataType attribute, values are expressed in the default unit documented at + https://github.com/buildingSMART/IDS/blob/master/Documentation/units.md, and unit conversion might be required. + + + - + - This is the name of an IFC Defined Type. See the full list for IFC 4 on https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/link/alphabeticalorder-defined-types.htm Documentation and default units on https://github.com/buildingSMART/IDS/blob/master/Documentation/units.md + This is the name of an IFC Defined Type, all uppercase. - - + + + + Depending on the IFC type of the attribute, values are expressed in the default unit documented at + https://github.com/buildingSMART/IDS/blob/master/Documentation/units.md, and unit conversion might be required. + + + @@ -98,6 +125,13 @@ + Author of the IDS can leave instructions for the authors of the IFC. This text could/should be displayed in the BIM/IFC authoring tool. @@ -111,7 +145,7 @@ - + Author of the IDS can leave instructions for the authors of the IFC. This text could/should be displayed in the BIM/IFC authoring tool. @@ -126,7 +160,7 @@ - + Author of the IDS can leave instructions for the authors of the IFC. This text could/should be displayed in the BIM/IFC authoring tool. @@ -140,6 +174,7 @@ + Author of the IDS can leave instructions for the authors of the IFC. This text could/should be displayed in the BIM/IFC authoring tool. @@ -154,7 +189,7 @@ - + Author of the IDS can leave instructions for the authors of the IFC. This text could/should be displayed in the BIM/IFC authoring tool. @@ -164,12 +199,12 @@ - + - + Author of the IDS can leave instructions for the authors of the IFC. This text could/should be displayed in the BIM/IFC authoring tool. @@ -195,7 +230,6 @@ - @@ -210,7 +244,7 @@ - + Author of the IDS can provide an identifier to the specification. This is intended to be a machine readable identifier. Beware: because of the possibility to combine different 'specification' elements from several ids files this cannot be enforced/assumed as (global) unique. @@ -229,12 +263,30 @@ - - - - - - + + + + + - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + diff --git a/src/ifctester/ifctester/reporter.py b/src/ifctester/ifctester/reporter.py index ff78d9c586..90f7fd1c36 100644 --- a/src/ifctester/ifctester/reporter.py +++ b/src/ifctester/ifctester/reporter.py @@ -16,22 +16,25 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcTester. If not, see . +from __future__ import annotations import os +import re import sys import math import logging import datetime -import numpy as np import ifcopenshell import ifcopenshell.util.unit import ifcopenshell.util.element -import ifcopenshell.util.placement +from .ids import Specification, Ids +from .facet import Facet, FacetFailure +from typing import TypedDict, Union, Literal, Optional cwd = os.path.dirname(os.path.realpath(__file__)) class Reporter: - def __init__(self, ids): + def __init__(self, ids: Ids): self.ids = ids def report(self, ids): @@ -44,8 +47,79 @@ class Reporter: pass +ResultsPercent = Union[int, Literal["N/A"]] + + +class Results(TypedDict): + title: str + date: str + filepath: str + filename: str + specifications: list[ResultsSpecification] + status: bool + total_specifications: int + total_specifications: int + total_specifications_pass: int + total_specifications_fail: int + percent_specifications_pass: ResultsPercent + total_requirements: int + total_requirements_pass: int + total_requirements_fail: int + percent_requirements_pass: ResultsPercent + total_checks: int + total_checks_pass: int + total_checks_fail: int + percent_checks_pass: ResultsPercent + + +class ResultsSpecification(TypedDict): + name: str + description: str + instructions: str + status: bool + total_applicable: int + total_applicable_pass: int + total_applicable_fail: int + percent_applicable_pass: ResultsPercent + total_checks: int + total_checks_pass: int + total_checks_fail: int + percent_checks_pass: ResultsPercent + required: bool + applicability: list[str] + requirements: list[ResultsRequirement] + + +class ResultsRequirement(TypedDict): + description: str + status: bool + failed_entities: list[ResultsFailedEntity] + total_applicable: int + total_pass: int + total_fail: int + percent_pass: ResultsPercent + + +# use different syntax because of the "class" key +ResultsFailedEntity = TypedDict( + "ResultsFailedEntity", + { + "reason": str, + "element": str, + "element_type": str, + "class": str, + "predefined_type": str, + "name": Union[str, None], + "description": Union[str, None], + "id": int, + "global_id": Union[str, None], + "tag": Union[str, None], + }, +) + + class Console(Reporter): - def __init__(self, ids, use_colour=True): + def __init__(self, ids: Ids, use_colour=True): super().__init__(ids) self.use_colour = use_colour self.colours = { @@ -61,14 +135,14 @@ class Console(Reporter): "reverse": "\033[;7m", } - def report(self): + def report(self) -> None: self.set_style("bold", "blue") self.print(self.ids.info.get("title", "Untitled IDS")) for specification in self.ids.specifications: self.report_specification(specification) self.set_style("reset") - def report_specification(self, specification): + def report_specification(self, specification: Specification) -> None: if specification.status is True: self.set_style("bold", "green") self.print("[PASS] ", end="") @@ -105,19 +179,19 @@ class Console(Reporter): for requirement in specification.requirements: self.set_style("reset") - self.set_style("red") if requirement.failed_entities else self.set_style("green") + self.set_style("red") if requirement.failures else self.set_style("green") self.print(" " * 8 + requirement.to_string("requirement", specification, requirement)) self.set_style("reset") - for i, element in enumerate(requirement.failed_entities[0:10]): + for failure in requirement.failures[0:10]: self.print(" " * 12, end="") - self.report_reason(requirement.failed_reasons[i], element) - if len(requirement.failed_entities) > 10: - self.print(" " * 12 + f"... {len(requirement.failed_entities)} in total ...") + self.report_reason(failure) + if len(requirement.failures) > 10: + self.print(" " * 12 + f"... {len(requirement.failures)} in total ...") self.set_style("reset") - def report_reason(self, reason, element): + def report_reason(self, failure: FacetFailure) -> None: is_bold = False - for substring in reason.split('"'): + for substring in failure["reason"].split('"'): if is_bold: self.set_style("purple") else: @@ -125,14 +199,14 @@ class Console(Reporter): self.print(substring, end="") is_bold = not is_bold self.set_style("grey") - self.print(" - " + str(element)) + self.print(" - " + str(failure["element"])) self.set_style("reset") - def set_style(self, *colours): + def set_style(self, *colours: str): if self.use_colour: sys.stdout.write("".join([self.colours[c] for c in colours])) - def print(self, txt, end=None): + def print(self, txt: str, end: Optional[str] = None): if end is not None: print(txt, end=end) else: @@ -140,27 +214,27 @@ class Console(Reporter): class Txt(Console): - def __init__(self, ids): + def __init__(self, ids: Ids): super().__init__(ids, use_colour=False) self.text = "" - def print(self, txt, end=None): - self.text += txt + "\n" if end is None else txt + def print(self, txt: str, end: Optional[str] = None): + self.text += txt + "\n" if end is None else end - def to_string(self): + def to_string(self) -> None: print(self.text) - def to_file(self, filepath): + def to_file(self, filepath: str) -> None: with open(filepath, "w", encoding="utf-8") as outfile: return outfile.write(self.text) class Json(Reporter): - def __init__(self, ids): + def __init__(self, ids: Ids): super().__init__(ids) - self.results = {} + self.results = Results() - def report(self): + def report(self) -> Results: self.results["title"] = self.ids.info.get("title", "Untitled IDS") self.results["date"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.results["filepath"] = self.ids.filepath @@ -205,75 +279,78 @@ class Json(Reporter): ) return self.results - def report_specification(self, specification): + def report_specification(self, specification: Specification) -> ResultsSpecification: applicability = [a.to_string("applicability") for a in specification.applicability] total_applicable = len(specification.applicable_entities) total_checks = 0 total_checks_pass = 0 requirements = [] for requirement in specification.requirements: - total_fail = len(requirement.failed_entities) + total_fail = len(requirement.failures) total_pass = total_applicable - total_fail percent_pass = math.floor((total_pass / total_applicable) * 100) if total_applicable else "N/A" total_checks += total_applicable total_checks_pass += total_pass requirements.append( - { - "description": requirement.to_string("requirement", specification, requirement), - "status": requirement.status, - "failed_entities": self.report_failed_entities(requirement), - "total_applicable": total_applicable, - "total_pass": total_pass, - "total_fail": total_fail, - "percent_pass": percent_pass, - } + ResultsRequirement( + description=requirement.to_string("requirement", specification, requirement), + status=requirement.status, + failed_entities=self.report_failed_entities(requirement), + total_applicable=total_applicable, + total_pass=total_pass, + total_fail=total_fail, + percent_pass=percent_pass, + ) ) total_applicable_pass = total_applicable - len(specification.failed_entities) percent_applicable_pass = ( math.floor((total_applicable_pass / total_applicable) * 100) if total_applicable else "N/A" ) percent_checks_pass = math.floor((total_checks_pass / total_checks) * 100) if total_checks else "N/A" - return { - "name": specification.name, - "description": specification.description, - "instructions": specification.instructions, - "status": specification.status, - "total_applicable": total_applicable, - "total_applicable_pass": total_applicable_pass, - "total_applicable_fail": total_applicable - total_applicable_pass, - "percent_applicable_pass": percent_applicable_pass, - "total_checks": total_checks, - "total_checks_pass": total_checks_pass, - "total_checks_fail": total_checks - total_checks_pass, - "percent_checks_pass": percent_checks_pass, - "required": specification.minOccurs != 0, - "applicability": applicability, - "requirements": requirements, - } - def report_failed_entities(self, requirement): + return ResultsSpecification( + name=specification.name, + description=specification.description, + instructions=specification.instructions, + status=specification.status, + total_applicable=total_applicable, + total_applicable_pass=total_applicable_pass, + total_applicable_fail=total_applicable - total_applicable_pass, + percent_applicable_pass=percent_applicable_pass, + total_checks=total_checks, + total_checks_pass=total_checks_pass, + total_checks_fail=total_checks - total_checks_pass, + percent_checks_pass=percent_checks_pass, + required=specification.minOccurs != 0, + applicability=applicability, + requirements=requirements, + ) + + def report_failed_entities(self, requirement: Facet) -> list[ResultsFailedEntity]: return [ - { - "reason": requirement.failed_reasons[i], - "element": str(e), - "element_type": str(ifcopenshell.util.element.get_type(e)), - "class": e.is_a(), - "predefined_type": ifcopenshell.util.element.get_predefined_type(e), - "name": getattr(e, "Name", None), - "description": getattr(e, "Description", None), - "id": e.id(), - "global_id": getattr(e, "GlobalId", None), - "tag": getattr(e, "Tag", None), - } - for i, e in enumerate(requirement.failed_entities) + ResultsFailedEntity( + { + "reason": f["reason"], + "element": str(f["element"]), + "element_type": str(ifcopenshell.util.element.get_type(f["element"])), + "class": f["element"].is_a(), + "predefined_type": ifcopenshell.util.element.get_predefined_type(f["element"]), + "name": getattr(f["element"], "Name", None), + "description": getattr(f["element"], "Description", None), + "id": f["element"].id(), + "global_id": getattr(f["element"], "GlobalId", None), + "tag": getattr(f["element"], "Tag", None), + } + ) + for f in requirement.failures ] - def to_string(self): + def to_string(self) -> str: import json return json.dumps(self.results) - def to_file(self, filepath): + def to_file(self, filepath: str) -> None: import json with open(filepath, "w", encoding="utf-8") as outfile: @@ -281,11 +358,10 @@ class Json(Reporter): class Html(Json): - def __init__(self, ids): + def __init__(self, ids: Ids): super().__init__(ids) - self.results = {} - def report(self): + def report(self) -> None: super().report() entity_limit = 100 for spec in self.results["specifications"]: @@ -296,13 +372,13 @@ class Html(Json): requirement["total_entities"] = total requirement["total_omitted"] = total - entity_limit - def to_string(self): + def to_string(self) -> str: import pystache with open(os.path.join(cwd, "templates", "report.html"), "r") as file: return pystache.render(file.read(), self.results) - def to_file(self, filepath): + def to_file(self, filepath: str) -> None: import pystache with open(os.path.join(cwd, "templates", "report.html"), "r") as file: @@ -311,17 +387,44 @@ class Html(Json): class Ods(Json): - def __init__(self, ids): + def __init__(self, ids: Ids, excel_safe=False): super().__init__(ids) + self.excel_safe = excel_safe self.colours = { "h": "cccccc", # Header "p": "97cc64", # Pass "f": "fb5a3e", # Fail "t": "ffffff", # Regular text } - self.results = {} - def to_file(self, filepath): + def excel_safe_spreadsheet_name(self, name: str) -> str: + if not self.excel_safe: + return name + + warning = ( + f'WARNING. Sheet name "{name}" is not valid for Excel and will be changed. ' + "See: https://support.microsoft.com/en-us/office/rename-a-worksheet-3f1f7148-ee83-404d-8ef0-9ff99fbad1f9" + ) + + if not name or name == "History": + print(warning) + return "placeholder spreadsheet name" + + if name.startswith("'") or name.endswith("'"): + print(warning) + name = name.strip("'") + + pattern = r"[\\\/\?\*\:\[\]]" + if re.search(pattern, name): + name = re.sub(pattern, "", name) + print(warning) + + if len(name) > 31: + name = name[:31] + print(warning) + return name + + def to_file(self, filepath: str) -> None: from odf.opendocument import OpenDocumentSpreadsheet from odf.style import Style, TableCellProperties from odf.table import Table, TableRow, TableCell @@ -336,7 +439,7 @@ class Ods(Json): self.doc.automaticstyles.addElement(style) self.cell_formats[key] = style - table = Table(name=self.results["title"]) + table = Table(name=self.excel_safe_spreadsheet_name(self.results["title"])) tr = TableRow() for header in ["Specification", "Status", "Total Pass", "Total Checks", "Percentage Pass"]: tc = TableCell(valuetype="string", stylename="h") @@ -373,7 +476,7 @@ class Ods(Json): for specification in self.results["specifications"]: if specification["status"]: continue - table = Table(name=specification["name"]) + table = Table(name=self.excel_safe_spreadsheet_name(specification["name"])) tr = TableRow() for header in [ "Requirement", @@ -421,16 +524,16 @@ class Ods(Json): table.addElement(tr) self.doc.spreadsheet.addElement(table) - self.doc.save(filepath, True) + self.doc.save(filepath, addsuffix=not filepath.lower().endswith(".ods")) class Bcf(Json): - def report_failed_entities(self, requirement): - return [ - {"reason": requirement.failed_reasons[i], "element": e} for i, e in enumerate(requirement.failed_entities) - ] + def report_failed_entities(self, requirement: Facet) -> list[FacetFailure]: + return [FacetFailure(f) for f in requirement.failures] - def to_file(self, filepath): + def to_file(self, filepath: str) -> None: + import numpy as np + import ifcopenshell.util.placement from bcf.v2.bcfxml import BcfXml unit_scale = None diff --git a/src/ifctester/pyproject.toml b/src/ifctester/pyproject.toml index 1936495312..a4644df54c 100644 --- a/src/ifctester/pyproject.toml +++ b/src/ifctester/pyproject.toml @@ -1,6 +1,7 @@ [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" + [project] name = "ifctester" version = "0.0.999999" @@ -14,14 +15,16 @@ classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", ] -dependencies = [ - "ifcopenshell", -] +dependencies = ["ifcopenshell", "python-dateutil", "xmlschema", "numpy"] + [project.urls] -"Homepage" = "http://ifcopenshell.org" -"Bug Tracker" = "https://github.com/ifcopenshell/ifcopenshell/issues" +Homepage = "http://ifcopenshell.org" +Documentation = "https://docs.ifcopenshell.org" +Issues = "https://github.com/IfcOpenShell/IfcOpenShell/issues" + [tool.setuptools.packages.find] include = ["ifctester"] exclude = ["test*"] + [tool.setuptools.package-data] ifctester = ["*.xsd", "templates/*.html"] diff --git a/src/ifctester/test/ids_doc_generator.py b/src/ifctester/test/ids_doc_generator.py index 0b0f593576..2543daec23 100644 --- a/src/ifctester/test/ids_doc_generator.py +++ b/src/ifctester/test/ids_doc_generator.py @@ -22,6 +22,7 @@ import uuid import pytest import functools import ifcopenshell +import ifcopenshell.api import ifctester import test_facet import test_ids @@ -83,7 +84,7 @@ class FacetDocGenerator: specs = ids.Ids(title=name) # todo: to resume IFC2X3 we need to ensure that entities and attributes are consistent with that schema in order to pass audit - spec = ids.Specification(name=name, minOccurs=1, ifcVersion=["IFC4"]) + spec = ids.Specification(name=name, minOccurs=1, ifcVersion=["IFC4"]) spec.applicability.append(ids.Entity(name=inst.is_a().upper())) spec.requirements.append(facet) specs.specifications.append(spec) @@ -319,15 +320,15 @@ site = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcSite", na building = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuilding", name="Building A") storey = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuildingStorey", name="Ground Floor") -ifcopenshell.api.run("aggregate.assign_object", model, relating_object=project, product=site) -ifcopenshell.api.run("aggregate.assign_object", model, relating_object=site, product=building) -ifcopenshell.api.run("aggregate.assign_object", model, relating_object=building, product=storey) +ifcopenshell.api.run("aggregate.assign_object", model, relating_object=project, products=[site]) +ifcopenshell.api.run("aggregate.assign_object", model, relating_object=site, products=[building]) +ifcopenshell.api.run("aggregate.assign_object", model, relating_object=building, products=[storey]) wall_types = [] for i in range(0, 4): wall_type = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWallType", name=f"DEMO{i + 1}") wall = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWall", name=f"WALL {i + 1}") - ifcopenshell.api.run("type.assign_type", model, related_object=wall, relating_type=wall_type) + ifcopenshell.api.run("type.assign_type", model, related_objects=[wall], relating_type=wall_type) representation = ifcopenshell.api.run( "geometry.add_wall_representation", model, context=body, length=5, height=3, thickness=(i + 1) * 0.05 ) @@ -346,6 +347,6 @@ for i in range(0, 4): location = np.eye(4) location[1][3] += i * 1 ifcopenshell.api.run("geometry.edit_object_placement", model, product=wall, matrix=location) - ifcopenshell.api.run("spatial.assign_container", model, relating_structure=storey, product=wall) + ifcopenshell.api.run("spatial.assign_container", model, relating_structure=storey, products=[wall]) model.write(os.path.join(outdir, "library", "sample.ifc")) diff --git a/src/ifctester/test/test_facet.py b/src/ifctester/test/test_facet.py index 7d9307dba1..ddee91ab36 100644 --- a/src/ifctester/test/test_facet.py +++ b/src/ifctester/test/test_facet.py @@ -37,9 +37,9 @@ def run(name, *, facet, inst, expected): class TestEntity: def test_creating_an_entity_facet(self): facet = Entity(name="IfcName") - assert facet.asdict() == {"name": {"simpleValue": "IfcName"}} + assert facet.asdict("applicability") == {"name": {"simpleValue": "IfcName"}} facet = Entity(name="IfcName", predefinedType="predefinedType", instructions="instructions") - assert facet.asdict() == { + assert facet.asdict("requirement") == { "name": {"simpleValue": "IfcName"}, "predefinedType": {"simpleValue": "predefinedType"}, "@instructions": "instructions", @@ -168,7 +168,7 @@ class TestEntity: ifc = ifcopenshell.file() wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType", predefined_type="X") - ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type) + ifcopenshell.api.run("type.assign_type", ifc, related_objects=[wall], relating_type=wall_type) facet = Entity(name="IFCWALL", predefinedType="X") run("Inherited predefined types should pass", facet=facet, inst=wall, expected=True) @@ -177,7 +177,7 @@ class TestEntity: wall_type = ifcopenshell.api.run( "root.create_entity", ifc, ifc_class="IfcWallType", predefined_type="NOTDEFINED" ) - ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type) + ifcopenshell.api.run("type.assign_type", ifc, related_objects=[wall], relating_type=wall_type) facet = Entity(name="IFCWALL", predefinedType="X") run("Overridden predefined types should pass", facet=facet, inst=wall, expected=True) @@ -223,17 +223,14 @@ class TestEntity: class TestAttribute: def test_creating_an_attribute_facet(self): attribute = Attribute(name="name") - assert attribute.asdict() == {"name": {"simpleValue": "name"}} + assert attribute.asdict("applicability") == {"name": {"simpleValue": "name"}} attribute = Attribute(name="name", value="value") - assert attribute.asdict() == {"name": {"simpleValue": "name"}, "value": {"simpleValue": "value"}} - attribute = Attribute( - name="name", value="value", minOccurs="0", maxOccurs="unbounded", instructions="instructions" - ) - assert attribute.asdict() == { + assert attribute.asdict("applicability") == {"name": {"simpleValue": "name"}, "value": {"simpleValue": "value"}} + attribute = Attribute(name="name", value="value", cardinality="required", instructions="instructions") + assert attribute.asdict("requirement") == { "name": {"simpleValue": "name"}, "value": {"simpleValue": "value"}, - "@minOccurs": "0", - "@maxOccurs": "unbounded", + "@cardinality": "required", "@instructions": "instructions", } @@ -258,12 +255,12 @@ class TestAttribute: element = ifc.createIfcWall(Name="Foobar") run("A required facet checks all parameters as normal", facet=facet, inst=element, expected=True) - # facet = Attribute(name="Name", minOccurs=0, maxOccurs=0) - # run("A prohibited facet returns the opposite of a required facet", facet=facet, inst=element, expected=False) - # facet = Attribute(name="Name", minOccurs=0) - # run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element, expected=True) - # facet = Attribute(name="Rabbit", minOccurs=0) - # run("An optional facet always passes regardless of outcome 2/2", facet=facet, inst=element, expected=True) + facet = Attribute(name="Name", cardinality="prohibited") + run("A prohibited facet returns the opposite of a required facet", facet=facet, inst=element, expected=False) + facet = Attribute(name="Name", cardinality="optional") + run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element, expected=True) + facet = Attribute(name="Rabbit", cardinality="optional") + run("An optional facet always passes regardless of outcome 2/2", facet=facet, inst=element, expected=True) ifc = ifcopenshell.file() facet = Attribute(name="Name") @@ -640,7 +637,7 @@ class TestAttribute: ifc = ifcopenshell.file() wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type) + ifcopenshell.api.run("type.assign_type", ifc, related_objects=[wall], relating_type=wall_type) wall_type.Description = "Foobar" facet = Attribute(name="Description", value="Foobar") run("Attributes are not inherited by the occurrence", facet=facet, inst=wall, expected=False) @@ -668,26 +665,26 @@ class TestAttribute: class TestClassification: def test_creating_a_classification_facet(self): - facet = Classification() - assert facet.asdict() == { - "@maxOccurs": "unbounded" - } + facet = Classification(system="system") + assert facet.asdict("requirement") == {"system": {"simpleValue": "system"}, "@cardinality": "required"} facet = Classification(value="value", system="system") - assert facet.asdict() == {"value": {"simpleValue": "value"}, "system": {"simpleValue": "system"}, "@maxOccurs": "unbounded" } + assert facet.asdict("requirement") == { + "value": {"simpleValue": "value"}, + "system": {"simpleValue": "system"}, + "@cardinality": "required", + } facet = Classification( value="value", system="system", uri="https://test.com", - minOccurs="0", - maxOccurs="unbounded", + cardinality="required", instructions="instructions", ) - assert facet.asdict() == { + assert facet.asdict("requirement") == { "value": {"simpleValue": "value"}, "system": {"simpleValue": "system"}, "@uri": "https://test.com", - "@minOccurs": "0", - "@maxOccurs": "unbounded", + "@cardinality": "required", "@instructions": "instructions", } @@ -709,49 +706,49 @@ class TestClassification: element0 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") element1 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSlab") ifcopenshell.api.run( - "classification.add_reference", ifc, product=element1, reference=ref1, classification=system_a + "classification.add_reference", ifc, products=[element1], reference=ref1, classification=system_a ) element11 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcColumn") ifcopenshell.api.run( - "classification.add_reference", ifc, product=element11, reference=ref11, classification=system_a + "classification.add_reference", ifc, products=[element11], reference=ref11, classification=system_a ) element22 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBeam") ifcopenshell.api.run( "classification.add_reference", ifc, - product=element22, + products=[element22], reference=ref22, classification=system_a, is_lightweight=False, ) material = ifc.createIfcMaterial(Name="Material") ifcopenshell.api.run( - "classification.add_reference", ifc, product=material, reference=ref1, classification=system_a + "classification.add_reference", ifc, products=[material], reference=ref1, classification=system_a ) - facet = Classification() + facet = Classification(system="Foobar") run( - "A classification facet with no data matches any classification 1/2", + "A classification facet with no value matches any classification 1/2", facet=facet, inst=element0, expected=False, ) run( - "A classification facet with no data matches any classification 2/2", + "A classification facet with no value matches any classification 2/2", facet=facet, inst=element1, expected=True, ) run("A required facet checks all parameters as normal", facet=facet, inst=element1, expected=True) - facet = Classification(minOccurs=0, maxOccurs=0) + facet = Classification(system="Foobar", cardinality="prohibited") run("A prohibited facet returns the opposite of a required facet", facet=facet, inst=element1, expected=False) - facet = Classification(minOccurs=0) + facet = Classification(system="Foobar", cardinality="optional") run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element0, expected=True) - facet = Classification(minOccurs=0) + facet = Classification(system="Foobar", cardinality="optional") run("An optional facet always passes regardless of outcome 2/2", facet=facet, inst=element1, expected=True) - facet = Classification(value="1") + facet = Classification(system="Foobar", value="1") run( "Values should match exactly if lightweight classifications are used", facet=facet, @@ -759,7 +756,7 @@ class TestClassification: expected=True, ) - facet = Classification(value="2") + facet = Classification(system="Foobar", value="2") run( "Values match subreferences if full classifications are used (e.g. EF_25_10 should match EF_25_10_25, EF_25_10_30, etc)", facet=facet, @@ -767,7 +764,7 @@ class TestClassification: expected=True, ) - facet = Classification(value="1") + facet = Classification(system="Foobar", value="1") run( "Non-rooted resources that have external classification references should also pass", facet=facet, @@ -783,7 +780,7 @@ class TestClassification: run("Systems should match exactly 5/5", facet=facet, inst=element22, expected=True) restriction = Restriction(options={"pattern": "1.*"}) - facet = Classification(value=restriction) + facet = Classification(system="Foobar", value=restriction) run("Restrictions can be used for values 1/3", facet=facet, inst=element1, expected=True) run("Restrictions can be used for values 2/3", facet=facet, inst=element11, expected=True) run("Restrictions can be used for values 3/3", facet=facet, inst=element22, expected=False) @@ -811,53 +808,51 @@ class TestClassification: # https://github.com/buildingSMART/IFC4.3.x-development/issues/475 wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type) + ifcopenshell.api.run("type.assign_type", ifc, related_objects=[wall], relating_type=wall_type) ifcopenshell.api.run( - "classification.add_reference", ifc, product=wall, reference=ref11, classification=system_a + "classification.add_reference", ifc, products=[wall], reference=ref11, classification=system_a ) ifcopenshell.api.run( - "classification.add_reference", ifc, product=wall_type, reference=ref22, classification=system_a + "classification.add_reference", ifc, products=[wall_type], reference=ref22, classification=system_a ) system_b = ifcopenshell.api.run("classification.add_classification", ifc, classification=system_b) ifcopenshell.api.run( - "classification.add_reference", ifc, product=wall_type, reference=refx, classification=system_b + "classification.add_reference", ifc, products=[wall_type], reference=refx, classification=system_b ) - facet = Classification(value="11") + facet = Classification(system="Foobar", value="11") run("Occurrences override the type classification per system 1/3", facet=facet, inst=wall, expected=True) - facet = Classification(value="22") + facet = Classification(system="Foobar", value="22") run("Occurrences override the type classification per system 2/3", facet=facet, inst=wall, expected=False) - facet = Classification(value="X") + facet = Classification(system="Foobaz", value="X") run("Occurrences override the type classification per system 3/3", facet=facet, inst=wall, expected=True) class TestProperty: def test_creating_a_property_facet(self): facet = Property() - assert facet.asdict() == { + assert facet.asdict("requirement") == { "propertySet": {"simpleValue": "Property_Set"}, - "name": {"simpleValue": "PropertyName"}, - "@maxOccurs": "unbounded" + "baseName": {"simpleValue": "PropertyName"}, + "@cardinality": "required", } facet = Property( propertySet="propertySet", - name="name", + baseName="baseName", value="value", - datatype="datatype", + dataType="dataType", uri="https://test.com", - minOccurs="0", - maxOccurs="unbounded", + cardinality="required", instructions="instructions", ) - assert facet.asdict() == { + assert facet.asdict("requirement") == { "propertySet": {"simpleValue": "propertySet"}, - "name": {"simpleValue": "name"}, + "baseName": {"simpleValue": "baseName"}, "value": {"simpleValue": "value"}, - "@datatype": "datatype", + "@dataType": "DATATYPE", "@uri": "https://test.com", - "@minOccurs": "0", - "@maxOccurs": "unbounded", + "@cardinality": "required", "@instructions": "instructions", } @@ -866,7 +861,7 @@ class TestProperty: ifc = self.setup_ifc() - facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCLABEL") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") run("Elements with no properties always fail", facet=facet, inst=element, expected=False) pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") @@ -878,27 +873,27 @@ class TestProperty: run("A name check will match any property with any string value", facet=facet, inst=element, expected=True) ifc = self.setup_ifc() - facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCLABEL") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"}) run("A required facet checks all parameters as normal", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL", minOccurs=0, maxOccurs=0) + facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCLABEL", cardinality="prohibited") run("A prohibited facet returns the opposite of a required facet", facet=facet, inst=element, expected=False) - facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL", minOccurs=0) + facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCLABEL", cardinality="optional") run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Bar", datatype="IFCLABEL", minOccurs=0) + facet = Property(propertySet="Foo_Bar", baseName="Bar", dataType="IFCLABEL", cardinality="optional") run("An optional facet always passes regardless of outcome 2/2", facet=facet, inst=element, expected=True) ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ""}) - facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLOGICAL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCLOGICAL") run("An empty string is considered falsey and will not pass", facet=facet, inst=element, expected=False) ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcLogical("UNKNOWN")}) - facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCDURATION") + facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCDURATION") run("A logical unknown is considered falsey and will not pass", facet=facet, inst=element, expected=False) ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcDuration("P0D")}) run("A zero duration will pass", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCBOOLEAN") + facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCBOOLEAN") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcBoolean(True)}) run("A property set to true will pass a name check", facet=facet, inst=element, expected=True) ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": False}) @@ -910,7 +905,7 @@ class TestProperty: ) ifc = self.setup_ifc() - facet = Property(propertySet="Foo_Bar", name="Foo", value="Bar", datatype="IFCLABEL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="Bar", dataType="IFCLABEL") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"}) @@ -920,55 +915,55 @@ class TestProperty: ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Baz"}) run("Specifying a value fails against different values", facet=facet, inst=element, expected=False) - facet = Property(propertySet="Foo_Bar", name="Foo", value="♫Don'tÄrgerhôtelЊет", datatype="IFCLABEL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="♫Don'tÄrgerhôtelЊет", dataType="IFCLABEL") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "♫Don'tÄrgerhôtelЊет"}) run("Non-ascii characters are treated without encoding", facet=facet, inst=element, expected=True) identifier = "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345" facet = Property( - propertySet="Foo_Bar", name="Foo", value=identifier + "_extra_characters", datatype="IFCIDENTIFIER" + propertySet="Foo_Bar", baseName="Foo", value=identifier + "_extra_characters", dataType="IFCIDENTIFIER" ) ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcIdentifier(identifier)}) run("IDS does not handle string truncation such as for identifiers", facet=facet, inst=element, expected=False) ifc = self.setup_ifc() - facet = Property(propertySet="Foo_Bar", name="Foo", value="1", datatype="IFCLABEL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="1", dataType="IFCLABEL") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "1"}) run("A number specified as a string is treated as a string", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Foo", value="42", datatype="IFCINTEGER") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="42", dataType="IFCINTEGER") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcInteger(42)}) run("Integer values are checked using type casting 1/4", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Foo", value="42.", datatype="IFCINTEGER") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="42.", dataType="IFCINTEGER") run("Integer values are checked using type casting 2/4", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Foo", value="42.0", datatype="IFCINTEGER") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="42.0", dataType="IFCINTEGER") run("Integer values are checked using type casting 3/4", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Foo", value="42.3", datatype="IFCINTEGER") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="42.3", dataType="IFCINTEGER") run("Integer values are checked using type casting 4/4", facet=facet, inst=element, expected=False) - facet = Property(propertySet="Foo_Bar", name="Foo", value="42", datatype="IFCREAL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="42", dataType="IFCREAL") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.0)}) run("Real values are checked using type casting 1/3", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Foo", value="42.0", datatype="IFCREAL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="42.0", dataType="IFCREAL") run("Real values are checked using type casting 2/3", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Foo", value="42.3", datatype="IFCREAL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="42.3", dataType="IFCREAL") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.3)}) run("Real values are checked using type casting 3/3", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Foo", value="42,3", datatype="IFCREAL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="42,3", dataType="IFCREAL") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.3)}) run("Only specifically formatted numbers are allowed 1/4", facet=facet, inst=element, expected=False) - facet = Property(propertySet="Foo_Bar", name="Foo", value="123,4.5", datatype="IFCREAL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="123,4.5", dataType="IFCREAL") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(1234.5)}) run("Only specifically formatted numbers are allowed 2/4", facet=facet, inst=element, expected=False) - facet = Property(propertySet="Foo_Bar", name="Foo", value="1.2345e3", datatype="IFCREAL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="1.2345e3", dataType="IFCREAL") run("Only specifically formatted numbers are allowed 3/4", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Foo", value="1.2345E3", datatype="IFCREAL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="1.2345E3", dataType="IFCREAL") run("Only specifically formatted numbers are allowed 4/4", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Foo", value="42.", datatype="IFCREAL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="42.", dataType="IFCREAL") ifcopenshell.api.run( "pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.0 * (1.0 + 1e-6))} ) @@ -986,15 +981,15 @@ class TestProperty: ) run("Floating point numbers are compared with a 1e-6 tolerance 4/4", facet=facet, inst=element, expected=False) - facet = Property(propertySet="Foo_Bar", name="Foo", value="TRUE", datatype="IFCBOOLEAN") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="TRUE", dataType="IFCBOOLEAN") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcBoolean(False)}) run("Booleans must be specified as uppercase strings 1/3", facet=facet, inst=element, expected=False) - facet = Property(propertySet="Foo_Bar", name="Foo", value="FALSE", datatype="IFCBOOLEAN") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="FALSE", dataType="IFCBOOLEAN") run("Booleans must be specified as uppercase strings 2/3", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Foo", value="False", datatype="IFCBOOLEAN") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="False", dataType="IFCBOOLEAN") run("Booleans must be specified as uppercase strings 3/3", facet=facet, inst=element, expected=False) - facet = Property(propertySet="Foo_Bar", name="Foo", value="2022-01-01", datatype="IFCDATE") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="2022-01-01", dataType="IFCDATE") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcDate("2022-01-01")}) run("Dates are treated as strings 1/2", facet=facet, inst=element, expected=True) ifcopenshell.api.run( @@ -1002,7 +997,7 @@ class TestProperty: ) run("Dates are treated as strings 2/2", facet=facet, inst=element, expected=False) - facet = Property(propertySet="Foo_Bar", name="Foo", value="PT16H", datatype="IFCDURATION") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="PT16H", dataType="IFCDURATION") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcDuration("PT16H")}) run("Durations are treated as strings 1/2", facet=facet, inst=element, expected=True) ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcDuration("P2D")}) @@ -1019,11 +1014,11 @@ class TestProperty: properties={"Status": ["EXISTING", "DEMOLISH"]}, pset_template=pset_template, ) - facet = Property(propertySet="Pset_WallCommon", name="Status", value="EXISTING", datatype="IFCLABEL") + facet = Property(propertySet="Pset_WallCommon", baseName="Status", value="EXISTING", dataType="IFCLABEL") run("Any matching value in an enumerated property will pass 1/3", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Pset_WallCommon", name="Status", value="DEMOLISH", datatype="IFCLABEL") + facet = Property(propertySet="Pset_WallCommon", baseName="Status", value="DEMOLISH", dataType="IFCLABEL") run("Any matching value in an enumerated property will pass 2/3", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Pset_WallCommon", name="Status", value="NEW", datatype="IFCLABEL") + facet = Property(propertySet="Pset_WallCommon", baseName="Status", value="NEW", dataType="IFCLABEL") run("Any matching value in an enumerated property will pass 3/3", facet=facet, inst=element, expected=False) ifc = self.setup_ifc() @@ -1033,11 +1028,11 @@ class TestProperty: Name="Foo", ListValues=[ifc.createIfcLabel("X"), ifc.createIfcLabel("Y")] ) pset.HasProperties = [list_property] - facet = Property(propertySet="Foo_Bar", name="Foo", value="X", datatype="IFCLABEL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="X", dataType="IFCLABEL") run("Any matching value in a list property will pass 1/3", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Foo", value="Y", datatype="IFCLABEL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="Y", dataType="IFCLABEL") run("Any matching value in a list property will pass 2/3", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Foo", value="Z", datatype="IFCLABEL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="Z", dataType="IFCLABEL") run("Any matching value in a list property will pass 3/3", facet=facet, inst=element, expected=False) ifc = self.setup_ifc() @@ -1050,13 +1045,13 @@ class TestProperty: SetPointValue=ifc.createIfcLengthMeasure(3000), ) pset.HasProperties = [bounded_property] - facet = Property(propertySet="Foo_Bar", name="Foo", value="1", datatype="IFCLENGTHMEASURE") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="1", dataType="IFCLENGTHMEASURE") run("Any matching value in a bounded property will pass 1/4", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Foo", value="5", datatype="IFCLENGTHMEASURE") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="5", dataType="IFCLENGTHMEASURE") run("Any matching value in a bounded property will pass 2/4", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Foo", value="3", datatype="IFCLENGTHMEASURE") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="3", dataType="IFCLENGTHMEASURE") run("Any matching value in a bounded property will pass 3/4", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Foo", value="2", datatype="IFCLENGTHMEASURE") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="2", dataType="IFCLENGTHMEASURE") run("Any matching value in a bounded property will pass 4/4", facet=facet, inst=element, expected=False) ifc = self.setup_ifc() @@ -1066,18 +1061,18 @@ class TestProperty: Name="Foo", DefiningValues=[ifc.createIfcLabel("X")], DefinedValues=[ifc.createIfcLengthMeasure(1000)] ) pset.HasProperties = [table_property] - facet = Property(propertySet="Foo_Bar", name="Foo", value="X", datatype="IFCLABEL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="X", dataType="IFCLABEL") run("Any matching value in a table property will pass 1/3", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Foo", value="1", datatype="IFCLENGTHMEASURE") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="1", dataType="IFCLENGTHMEASURE") run("Any matching value in a table property will pass 2/3", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Foo", value="Y", datatype="IFCLABEL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="Y", dataType="IFCLABEL") run("Any matching value in a table property will pass 3/3", facet=facet, inst=element, expected=False) ifc = self.setup_ifc() element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") pset.HasProperties = [ifc.createIfcPropertyReferenceValue(Name="Foo")] - facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCLABEL") run("Reference properties are treated as objects and not supported", facet=facet, inst=element, expected=False) ifc = self.setup_ifc() @@ -1096,21 +1091,21 @@ class TestProperty: RelatingPropertyDefinition=pset, ) facet = Property( - propertySet="Foo_Bar", name="PanelOperation", value="SWINGING", datatype="IFCDOORPANELOPERATIONENUM" + propertySet="Foo_Bar", baseName="PanelOperation", value="SWINGING", dataType="IFCDOORPANELOPERATIONENUM" ) run("Predefined properties are supported but discouraged 1/2", facet=facet, inst=element, expected=True) facet = Property( - propertySet="Foo_Bar", name="PanelOperation", value="SWONGING", datatype="IFCDOORPANELOPERATIONENUM" + propertySet="Foo_Bar", baseName="PanelOperation", value="SWONGING", dataType="IFCDOORPANELOPERATIONENUM" ) run("Predefined properties are supported but discouraged 2/2", facet=facet, inst=element, expected=False) ifc = self.setup_ifc() - facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLENGTHMEASURE") + facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCLENGTHMEASURE") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") qto = ifcopenshell.api.run("pset.add_qto", ifc, product=element, name="Foo_Bar") ifcopenshell.api.run("pset.edit_qto", ifc, qto=qto, properties={"Foo": ifc.createIfcLengthMeasure(42)}) run("A name check will match any quantity with any value", facet=facet, inst=element, expected=True) - facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCAREAMEASURE") + facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCAREAMEASURE") run("Quantities must also match the appropriate measure", facet=facet, inst=element, expected=False) ifc = self.setup_ifc() @@ -1119,9 +1114,9 @@ class TestProperty: complex_property = ifc.createIfcComplexProperty(Name="Foo", UsageName="RabbitAgilityTraining") ifcopenshell.api.run("pset.edit_pset", ifc, pset=complex_property, properties={"Rabbits": "Awesome"}) pset.HasProperties = [complex_property] - facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCLABEL") run("Complex properties are not supported 1/2", facet=facet, inst=element, expected=False) - facet = Property(propertySet="Foo", name="Rabbits", datatype="IFCLABEL") + facet = Property(propertySet="Foo", baseName="Rabbits", dataType="IFCLABEL") run("Complex properties are not supported 2/2", facet=facet, inst=element, expected=False) ifc = self.setup_ifc() @@ -1132,14 +1127,14 @@ class TestProperty: "pset.edit_qto", ifc, qto=complex_quantity, properties={"MyLength": ifc.createIfcLengthMeasure(42)} ) qto.Quantities = [complex_quantity] - facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLENGTHMEASURE") + facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCLENGTHMEASURE") run("Complex properties are not supported 1/2", facet=facet, inst=element, expected=False) - facet = Property(propertySet="Foo", name="MyLength", datatype="IFCLENGTHMEASURE") + facet = Property(propertySet="Foo", baseName="MyLength", dataType="IFCLENGTHMEASURE") run("Complex properties are not supported 2/2", facet=facet, inst=element, expected=False) ifc = self.setup_ifc() restriction = Restriction(options={"pattern": "Foo_.*"}) - facet = Property(propertySet=restriction, name="Foo", datatype="IFCLABEL") + facet = Property(propertySet=restriction, baseName="Foo", dataType="IFCLABEL") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"}) @@ -1152,7 +1147,7 @@ class TestProperty: ifc = self.setup_ifc() restriction = Restriction(options={"pattern": "Foo.*"}) - facet = Property(propertySet="Foo_Bar", name=restriction, value="x", datatype="IFCLABEL") + facet = Property(propertySet="Foo_Bar", baseName=restriction, value="x", dataType="IFCLABEL") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": "x"}) @@ -1165,7 +1160,7 @@ class TestProperty: ifc = self.setup_ifc() restriction1 = Restriction(options={"pattern": "Foo.*"}) restriction2 = Restriction(options={"enumeration": ["x", "y"]}) - facet = Property(propertySet="Foo_Bar", name=restriction1, value=restriction2, datatype="IFCLABEL") + facet = Property(propertySet="Foo_Bar", baseName=restriction1, value=restriction2, dataType="IFCLABEL") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": "x", "Foobaz": "y"}) @@ -1184,7 +1179,7 @@ class TestProperty: ) ifc = self.setup_ifc() - facet = Property(propertySet="Foo_Bar", name="Foo", value="2", datatype="IFCTIMEMEASURE") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="2", dataType="IFCTIMEMEASURE") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcMassMeasure(2)}) @@ -1193,7 +1188,7 @@ class TestProperty: run("Measures are used to specify an IFC data type 2/2", facet=facet, inst=element, expected=True) ifc = self.setup_ifc() - facet = Property(propertySet="Foo_Bar", name="Foo", value="2", datatype="IFCLENGTHMEASURE") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="2", dataType="IFCLENGTHMEASURE") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcLengthMeasure(2)}) @@ -1214,22 +1209,22 @@ class TestProperty: ifc = self.setup_ifc() wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type) + ifcopenshell.api.run("type.assign_type", ifc, related_objects=[wall], relating_type=wall_type) pset = ifcopenshell.api.run("pset.add_pset", ifc, product=wall_type, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"}) - facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", dataType="IFCLABEL") run("Properties can be inherited from the type 1/2", facet=facet, inst=wall, expected=True) run("Properties can be inherited from the type 2/2", facet=facet, inst=wall_type, expected=True) ifc = self.setup_ifc() wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type) + ifcopenshell.api.run("type.assign_type", ifc, related_objects=[wall], relating_type=wall_type) pset = ifcopenshell.api.run("pset.add_pset", ifc, product=wall_type, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Baz"}) pset = ifcopenshell.api.run("pset.add_pset", ifc, product=wall, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"}) - facet = Property(propertySet="Foo_Bar", name="Foo", value="Bar", datatype="IFCLABEL") + facet = Property(propertySet="Foo_Bar", baseName="Foo", value="Bar", dataType="IFCLABEL") run("Properties can be overriden by an occurrence 1/2", facet=facet, inst=wall, expected=True) run("Properties can be overriden by an occurrence 2/2", facet=facet, inst=wall_type, expected=False) @@ -1248,15 +1243,12 @@ class TestProperty: class TestMaterial: def test_creating_a_material_facet(self): facet = Material() - assert facet.asdict() == {"@maxOccurs": "unbounded"} - facet = Material( - value="value", uri="https://test.com", minOccurs="0", maxOccurs="unbounded", instructions="instructions" - ) - assert facet.asdict() == { + assert facet.asdict("requirement") == {"@cardinality": "required"} + facet = Material(value="value", uri="https://test.com", cardinality="required", instructions="instructions") + assert facet.asdict("requirement") == { "value": {"simpleValue": "value"}, "@uri": "https://test.com", - "@minOccurs": "0", - "@maxOccurs": "unbounded", + "@cardinality": "required", "@instructions": "instructions", } @@ -1268,22 +1260,22 @@ class TestMaterial: element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") run("Elements without a material always fail", facet=facet, inst=element, expected=False) material = ifcopenshell.api.run("material.add_material", ifc) - ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material) + ifcopenshell.api.run("material.assign_material", ifc, products=[element], material=material) run("Elements with any material will pass an empty material facet", facet=facet, inst=element, expected=True) run("A required facet checks all parameters as normal", facet=facet, inst=element, expected=True) - facet = Material(minOccurs=0, maxOccurs=0) + facet = Material(cardinality="prohibited") run("A prohibited facet returns the opposite of a required facet", facet=facet, inst=element, expected=False) - facet = Material(minOccurs=0) + facet = Material(cardinality="optional") run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element, expected=True) - facet = Material(value="Foo", minOccurs=0) + facet = Material(value="Foo", cardinality="optional") run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element, expected=True) ifc = ifcopenshell.file() facet = Material(value="Foo") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") material = ifcopenshell.api.run("material.add_material", ifc) - ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material) + ifcopenshell.api.run("material.assign_material", ifc, products=[element], material=material) material.Name = "Foo" run("A material name may pass the value check", facet=facet, inst=element, expected=True) material.Name = "Bar" @@ -1294,7 +1286,7 @@ class TestMaterial: facet = Material(value="Foo") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") material_set = ifcopenshell.api.run("material.add_material_set", ifc, set_type="IfcMaterialList") - ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material_set) + ifcopenshell.api.run("material.assign_material", ifc, products=[element], material=material_set) material = ifcopenshell.api.run("material.add_material", ifc) ifcopenshell.api.run("material.add_list_item", ifc, material_list=material_set, material=material) material.Name = "Foo" @@ -1307,7 +1299,7 @@ class TestMaterial: facet = Material(value="Foo") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") material_set = ifcopenshell.api.run("material.add_material_set", ifc, set_type="IfcMaterialLayerSet") - ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material_set) + ifcopenshell.api.run("material.assign_material", ifc, products=[element], material=material_set) material = ifcopenshell.api.run("material.add_material", ifc) layer = ifcopenshell.api.run("material.add_layer", ifc, layer_set=material_set, material=material) layer.Name = "Foo" @@ -1326,7 +1318,7 @@ class TestMaterial: facet = Material(value="Foo") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") material_set = ifcopenshell.api.run("material.add_material_set", ifc, set_type="IfcMaterialProfileSet") - ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material_set) + ifcopenshell.api.run("material.assign_material", ifc, products=[element], material=material_set) material = ifcopenshell.api.run("material.add_material", ifc) profile = ifcopenshell.api.run("material.add_profile", ifc, profile_set=material_set, material=material) profile.Name = "Foo" @@ -1346,7 +1338,7 @@ class TestMaterial: facet = Material(value="Foo") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") material_set = ifcopenshell.api.run("material.add_material_set", ifc, set_type="IfcMaterialConstituentSet") - ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material_set) + ifcopenshell.api.run("material.assign_material", ifc, products=[element], material=material_set) run("A constituent set with no data will fail a value check", facet=facet, inst=element, expected=False) material = ifcopenshell.api.run("material.add_material", ifc) constituent = ifcopenshell.api.run( @@ -1382,9 +1374,9 @@ class TestMaterial: ifc = ifcopenshell.file() element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", ifc, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", ifc, related_objects=[element], relating_type=element_type) material = ifcopenshell.api.run("material.add_material", ifc) - ifcopenshell.api.run("material.assign_material", ifc, product=element_type, material=material) + ifcopenshell.api.run("material.assign_material", ifc, products=[element_type], material=material) material.Name = "Foo" facet = Material(value="Foo") run("Occurrences can inherit materials from their types", facet=facet, inst=element, expected=True) @@ -1392,12 +1384,12 @@ class TestMaterial: ifc = ifcopenshell.file() element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType") - ifcopenshell.api.run("type.assign_type", ifc, related_object=element, relating_type=element_type) + ifcopenshell.api.run("type.assign_type", ifc, related_objects=[element], relating_type=element_type) material = ifcopenshell.api.run("material.add_material", ifc) - ifcopenshell.api.run("material.assign_material", ifc, product=element_type, material=material) + ifcopenshell.api.run("material.assign_material", ifc, products=[element_type], material=material) material.Name = "Bar" material = ifcopenshell.api.run("material.add_material", ifc) - ifcopenshell.api.run("material.assign_material", ifc, product=element, material=material) + ifcopenshell.api.run("material.assign_material", ifc, products=[element], material=material) material.Name = "Foo" facet = Material(value="Foo") run("Occurrences can override materials from their types", facet=facet, inst=element, expected=True) @@ -1406,23 +1398,24 @@ class TestMaterial: class TestPartOf: def test_creating_a_partof_facet(self): facet = PartOf() - assert facet.asdict() == {"entity": {"name": {"simpleValue": "IFCWALL"}}, "@maxOccurs": "unbounded" } + assert facet.asdict("requirement") == { + "entity": {"name": {"simpleValue": "IFCWALL"}}, + "@cardinality": "required", + } facet = PartOf( name="IFCGROUP", predefinedType="predefinedType", relation="IFCRELASSIGNSTOGROUP", - minOccurs="0", - maxOccurs="unbounded", + cardinality="required", instructions="instructions", ) - assert facet.asdict() == { + assert facet.asdict("requirement") == { "entity": { "name": {"simpleValue": "IFCGROUP"}, "predefinedType": {"simpleValue": "predefinedType"}, }, "@relation": "IFCRELASSIGNSTOGROUP", - "@minOccurs": "0", - "@maxOccurs": "unbounded", + "@cardinality": "required", "@instructions": "instructions", } @@ -1435,21 +1428,18 @@ class TestPartOf: subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") facet = PartOf(name="IFCELEMENTASSEMBLY", relation="IFCRELAGGREGATES") run("A non aggregated element fails an aggregate relationship", facet=facet, inst=subelement, expected=False) - ifcopenshell.api.run("aggregate.assign_object", ifc, product=subelement, relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", ifc, products=[subelement], relating_object=element) run("The aggregated whole fails an aggregate relationship", facet=facet, inst=element, expected=False) run("The aggregated part passes an aggregate relationship", facet=facet, inst=subelement, expected=True) run("A required facet checks all parameters as normal", facet=facet, inst=subelement, expected=True) - facet = PartOf(name="IFCELEMENTASSEMBLY", relation="IFCRELAGGREGATES", minOccurs=0, maxOccurs=0) + facet = PartOf(name="IFCELEMENTASSEMBLY", relation="IFCRELAGGREGATES", cardinality="prohibited") run("A prohibited facet returns the opposite of a required facet", facet=facet, inst=subelement, expected=False) - facet = PartOf(name="IFCELEMENTASSEMBLY", relation="IFCRELAGGREGATES", minOccurs=0) - run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element, expected=True) - run("An optional facet always passes regardless of outcome 2/2", facet=facet, inst=subelement, expected=True) ifc = ifcopenshell.file() element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSlab") subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBeam") - ifcopenshell.api.run("aggregate.assign_object", ifc, product=subelement, relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", ifc, products=[subelement], relating_object=element) facet = PartOf(name="IFCSLAB", relation="IFCRELAGGREGATES") run("An aggregate may specify the entity of the whole 1/2", facet=facet, inst=subelement, expected=True) facet = PartOf(name="IFCWALL", relation="IFCRELAGGREGATES") @@ -1472,8 +1462,8 @@ class TestPartOf: element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly") subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSlab") subsubelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBeam") - ifcopenshell.api.run("aggregate.assign_object", ifc, product=subelement, relating_object=element) - ifcopenshell.api.run("aggregate.assign_object", ifc, product=subsubelement, relating_object=subelement) + ifcopenshell.api.run("aggregate.assign_object", ifc, products=[subelement], relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", ifc, products=[subsubelement], relating_object=subelement) facet = PartOf(name="IFCELEMENTASSEMBLY", relation="IFCRELAGGREGATES") run("An aggregate entity may pass any ancestral whole passes", facet=facet, inst=subsubelement, expected=True) @@ -1505,14 +1495,14 @@ class TestPartOf: container = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSpace") facet = PartOf(name="IFCSPACE", relation="IFCRELCONTAINEDINSPATIALSTRUCTURE") run("Any contained element passes a containment relationship 1/2", facet=facet, inst=element, expected=False) - ifcopenshell.api.run("spatial.assign_container", ifc, product=element, relating_structure=container) + ifcopenshell.api.run("spatial.assign_container", ifc, products=[element], relating_structure=container) run("Any contained element passes a containment relationship 2/2", facet=facet, inst=element, expected=True) run("The container itself always fails", facet=facet, inst=container, expected=False) ifc = ifcopenshell.file() element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly") container = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSpace") - ifcopenshell.api.run("spatial.assign_container", ifc, product=element, relating_structure=container) + ifcopenshell.api.run("spatial.assign_container", ifc, products=[element], relating_structure=container) facet = PartOf(relation="IFCRELCONTAINEDINSPATIALSTRUCTURE", name="IFCSITE") run("The container entity must match exactly 1/2", facet=facet, inst=element, expected=False) facet = PartOf(relation="IFCRELCONTAINEDINSPATIALSTRUCTURE", name="IFCSPACE") @@ -1527,16 +1517,16 @@ class TestPartOf: ifc = ifcopenshell.file() element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSlab") subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBeam") - ifcopenshell.api.run("aggregate.assign_object", ifc, product=subelement, relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", ifc, products=[subelement], relating_object=element) container = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSpace") - ifcopenshell.api.run("spatial.assign_container", ifc, product=element, relating_structure=container) + ifcopenshell.api.run("spatial.assign_container", ifc, products=[element], relating_structure=container) facet = PartOf(relation="IFCRELCONTAINEDINSPATIALSTRUCTURE", name="IFCSPACE") run("The container may be indirect", facet=facet, inst=subelement, expected=True) ifc = ifcopenshell.file() element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture") subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcDiscreteAccessory") - ifcopenshell.api.run("nest.assign_object", ifc, related_object=subelement, relating_object=element) + ifcopenshell.api.run("nest.assign_object", ifc, related_objects=[subelement], relating_object=element) facet = PartOf(name="IFCFURNITURE", relation="IFCRELNESTS") run("Any nested part passes a nest relationship", facet=facet, inst=subelement, expected=True) run("Any nested whole fails a nest relationship", facet=facet, inst=element, expected=False) @@ -1544,7 +1534,7 @@ class TestPartOf: ifc = ifcopenshell.file() element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture") subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcDiscreteAccessory") - ifcopenshell.api.run("nest.assign_object", ifc, related_object=subelement, relating_object=element) + ifcopenshell.api.run("nest.assign_object", ifc, related_objects=[subelement], relating_object=element) facet = PartOf(relation="IFCRELNESTS", name="IFCBEAM") run("The nest entity must match exactly 1/2", facet=facet, inst=subelement, expected=False) facet = PartOf(relation="IFCRELNESTS", name="IFCFURNITURE") @@ -1561,8 +1551,8 @@ class TestPartOf: element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture") subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcDiscreteAccessory") subsubelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcMechanicalFastener") - ifcopenshell.api.run("nest.assign_object", ifc, related_object=subelement, relating_object=element) - ifcopenshell.api.run("nest.assign_object", ifc, related_object=subsubelement, relating_object=subelement) + ifcopenshell.api.run("nest.assign_object", ifc, related_objects=[subelement], relating_object=element) + ifcopenshell.api.run("nest.assign_object", ifc, related_objects=[subsubelement], relating_object=subelement) facet = PartOf(relation="IFCRELNESTS", name="IFCFURNITURE") run("Nesting may be indirect", facet=facet, inst=subsubelement, expected=True) diff --git a/src/ifctester/test/test_ids.py b/src/ifctester/test/test_ids.py index ea2b63dd55..9059519b3e 100644 --- a/src/ifctester/test/test_ids.py +++ b/src/ifctester/test/test_ids.py @@ -21,9 +21,17 @@ import pytest import xmlschema import ifcopenshell from ifctester import ids +from typing import Optional -def run(name, ids, ifc, expected, applicable_entities=None, failed_entities=None): +def run( + name: str, + ids: ids.Ids, + ifc: ifcopenshell.file, + expected: bool, + applicable_entities: Optional[list[ifcopenshell.entity_instance]] = None, + failed_entities: Optional[list[ifcopenshell.entity_instance]] = None, +): ids.validate(ifc) all_applicable = set() all_failures = set() @@ -36,7 +44,7 @@ def run(name, ids, ifc, expected, applicable_entities=None, failed_entities=None all_applicable.update(spec.applicable_entities) for requirement in spec.requirements: if requirement.status is False: - all_failures.update(requirement.failed_entities) + all_failures.update([f["element"] for f in requirement.failures]) assert set(all_applicable) == set(applicable_entities) assert set(all_failures) == set(failed_entities) @@ -48,12 +56,11 @@ class TestIds: def test_create_an_ids_with_minimal_information(self): specs = ids.Ids() - print('AAA', specs.asdict()) assert specs.asdict() == { "@xmlns": "http://standards.buildingsmart.org/IDS", "@xmlns:xs": "http://www.w3.org/2001/XMLSchema", "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", - "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/0.9.6/ids.xsd", + "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/0.9.7/ids.xsd", "info": {"title": "Untitled"}, "specifications": {"specification": []}, } @@ -73,7 +80,7 @@ class TestIds: "@xmlns": "http://standards.buildingsmart.org/IDS", "@xmlns:xs": "http://www.w3.org/2001/XMLSchema", "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", - "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/0.9.6/ids.xsd", + "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/0.9.7/ids.xsd", "info": { "title": "title", "copyright": "copyright", @@ -93,7 +100,7 @@ class TestIds: "@xmlns": "http://standards.buildingsmart.org/IDS", "@xmlns:xs": "http://www.w3.org/2001/XMLSchema", "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", - "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/0.9.6/ids.xsd", + "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/0.9.7/ids.xsd", "info": {"title": "Untitled"}, "specifications": {"specification": []}, } @@ -117,7 +124,7 @@ class TestIds: specs = ids.Ids(title="Title") spec = ids.Specification(name="Name") spec.applicability.append(ids.Entity(name="IFCWALL")) - spec.requirements.append(name_attr := ids.Attribute(name="Name", value="Waldo")) + spec.requirements.append(ids.Attribute(name="Name", value="Waldo")) specs.specifications.append(spec) assert "http://standards.buildingsmart.org/IDS" in specs.to_string() assert spec.status == None @@ -135,11 +142,11 @@ class TestIds: specs, model, True, - [wall, waldo] + [wall, waldo], ) spec.ifcVersion = [] - spec.minOccurs = 1 + spec.set_usage("required") model = ifcopenshell.file() waldo = model.createIfcWall(Name="Waldo") run("Required specifications need at least one applicable entity 1/2", specs, model, True, [waldo]) @@ -147,25 +154,37 @@ class TestIds: waldo = model.createIfcSlab(Name="Waldo") run("Required specifications need at least one applicable entity 2/2", specs, model, False) - spec.minOccurs = 0 + spec.set_usage("optional") model = ifcopenshell.file() waldo = model.createIfcSlab(Name="Waldo") run("Optional specifications may still pass if nothing is applicable", specs, model, True) - spec.minOccurs = 0 - spec.maxOccurs = 0 + spec.set_usage("prohibited") model = ifcopenshell.file() wall = model.createIfcSlab(Name="Waldo") run("Prohibited specifications fail if at least one entity passes all requirements 1/3", specs, model, True) model = ifcopenshell.file() wall = model.createIfcWall(Name="Wally") - run("Prohibited specifications fail if at least one entity passes all requirements 2/3", specs, model, False, [wall], [wall]) + run( + "Prohibited specifications fail if at least one entity passes all requirements 2/3", + specs, + model, + True, + [wall], + [], + ) model = ifcopenshell.file() wall = model.createIfcWall(Name="Waldo") - run("Prohibited specifications fail if at least one entity passes all requirements 3/3", specs, model, False, [wall]) + run( + "Prohibited specifications fail if at least one entity passes all requirements 3/3", + specs, + model, + False, + [wall], + [wall], + ) - spec.minOccurs = 0 - spec.maxOccurs = "unbounded" + spec.set_usage("optional") model = ifcopenshell.file() wall = model.createIfcWall(Name="Waldo") spec.requirements.append(description_attr := ids.Attribute(name="Description", value="Foobar")) @@ -180,8 +199,7 @@ class TestIds: # run("Specification optionality and facet optionality can be combined", specs, model, True, [wall]) # double negative / required attributes - # spec.minOccurs = 0 - # spec.maxOccurs = 0 + # spec.set_usage("prohibited") # name_attr.minOccurs = 0 # name_attr.maxOccurs = 0 # description_attr.minOccurs = 0 @@ -226,19 +244,18 @@ class TestIds: assert spec.status == False assert set(spec.applicable_entities) == {wall, waldo} - assert spec.requirements[0].failed_entities == [wall] - assert spec2.requirements[0].failed_entities == [wall] + assert len(spec.requirements[0].failures) == 1 + assert len(spec2.requirements[0].failures) == 1 + assert spec.requirements[0].failures[0]["element"] == wall + assert spec2.requirements[0].failures[0]["element"] == wall class TestSpecification: def test_create_specification_with_minimal_information(self): spec = ids.Specification() - print(spec.asdict()) assert spec.asdict() == { "@name": "Unnamed", "@ifcVersion": ["IFC2X3", "IFC4"], - "@minOccurs": 0, - "@maxOccurs": "unbounded", "applicability": {}, "requirements": {}, } @@ -255,8 +272,6 @@ class TestSpecification: ) assert spec.asdict() == { "@name": "name", - "@minOccurs": 1, - "@maxOccurs": 1, "@ifcVersion": "IFC4", "@identifier": "identifier", "@description": "description", @@ -264,43 +279,95 @@ class TestSpecification: "applicability": {}, "requirements": {}, } - + def test_specification_has_no_requirements(self): model = ifcopenshell.file() wall = model.createIfcWall() waldo = model.createIfcWall(Name="Waldo") - + test_ids = ids.Ids(title="Title") spec = ids.Specification(name="Name") spec.applicability.append(ids.Entity(name="IFCWALL")) test_ids.specifications.append(spec) - spec.minOccurs = 1 - run("A specification that is required and has at least one applicable entity but no requirements shall pass", test_ids, model, True, [wall, waldo], None) - + spec.set_usage("required") + run( + "A specification that is required and has at least one applicable entity but no requirements shall pass", + test_ids, + model, + True, + [wall, waldo], + None, + ) + test_ids = ids.Ids(title="Title") spec = ids.Specification(name="Name") test_ids.specifications.append(spec) - spec.minOccurs = 1 - run("A specification that is required but has no applicable entities or requirements shall fail", test_ids, model, False, None, None) - + spec.set_usage("required") + run( + "A specification that is required but has no applicable entities or requirements shall fail", + test_ids, + model, + False, + None, + None, + ) + test_ids = ids.Ids(title="Title") spec = ids.Specification(name="Name") spec.applicability.append(ids.Entity(name="IFCWALL")) test_ids.specifications.append(spec) - spec.minOccurs = 0 - run("A specification that is optional and has at least one applicable entity but no requirements shall pass", test_ids, model, True, [wall, waldo], None) - + spec.set_usage("optional") + run( + "A specification that is optional and has at least one applicable entity but no requirements shall pass", + test_ids, + model, + True, + [wall, waldo], + None, + ) + test_ids = ids.Ids(title="Title") spec = ids.Specification(name="Name") spec.applicability.append(ids.Entity(name="IFCWALL")) test_ids.specifications.append(spec) - spec.minOccurs = 0 - spec.maxOccurs = 0 - run("A specification that is prohibited and has at least one applicable entity but no requirements shall fail", test_ids, model, False, [wall, waldo], None) - + spec.set_usage("prohibited") + run( + "A specification that is prohibited and has at least one applicable entity but no requirements shall fail", + test_ids, + model, + False, + [wall, waldo], + None, + ) + test_ids = ids.Ids(title="Title") spec = ids.Specification(name="Name") test_ids.specifications.append(spec) - spec.minOccurs = 0 - spec.maxOccurs = 0 - run("A specification that is prohibited but has no applicable entities or requirements shall pass", test_ids, model, True, None, None) + spec.set_usage("prohibited") + run( + "A specification that is prohibited but has no applicable entities or requirements shall pass", + test_ids, + model, + True, + None, + None, + ) + + def test_prohibited_facet(self): + specs = ids.Ids(title="Title") + spec = ids.Specification(name="Name") + spec.applicability.append(ids.Entity(name="IFCWALL")) + spec.requirements.append(ids.Attribute(name="Name", value="Waldo", cardinality="prohibited")) + specs.specifications.append(spec) + + spec.set_usage("required") + model = ifcopenshell.file() + wall = model.createIfcWall(Name="Wally") + run( + "Prohibited facet not to fail if no entity passes it", + specs, + model, + True, + [wall], + [], + ) diff --git a/src/ifcwrap/IfcGeomWrapper.i b/src/ifcwrap/IfcGeomWrapper.i index 7eec66d5eb..66808dcf61 100644 --- a/src/ifcwrap/IfcGeomWrapper.i +++ b/src/ifcwrap/IfcGeomWrapper.i @@ -82,6 +82,8 @@ std::pair vector_to_buffer(const T& t) { %template(ray_intersection_results) std::vector; +%template(clashes) std::vector; + // A Template instantantation should be defined before it is used as a base class. // But frankly I don't care as most methods are subtlely different anyway. %include "../ifcgeom_schema_agnostic/IfcGeomTree.h" @@ -114,6 +116,80 @@ std::pair vector_to_buffer(const T& t) { return IfcGeom_tree_vector_to_list(ps); } + + %typemap(in) const std::vector& (std::vector temp) { + if (!PyList_Check($input)) { + PyErr_SetString(PyExc_TypeError, "Expected a list."); + return NULL; + } + $1 = &temp; // Set $1 to the address of temp, which SWIG will use as the argument in the wrapped function + temp.reserve(PyList_Size($input)); // Pre-allocate memory for efficiency + for (Py_ssize_t i = 0; i < PyList_Size($input); ++i) { + PyObject* pyObj = PyList_GetItem($input, i); + void* ptr = 0; + int res = SWIG_ConvertPtr(pyObj, &ptr, SWIGTYPE_p_IfcUtil__IfcBaseClass, 0); + if (!SWIG_IsOK(res)) { + PyErr_SetString(PyExc_TypeError, "List item is not of type IfcBaseClass."); + return NULL; + } + temp.push_back(reinterpret_cast(ptr)); + } + } + + std::vector clash_intersection_many(const std::vector& set_a, const std::vector& set_b, double tolerance, bool check_all) const { + std::vector set_a_entities; + std::vector set_b_entities; + for (auto* e : set_a) { + if (!e->declaration().is("IfcProduct")) { + throw IfcParse::IfcException("All instances should be of type IfcProduct"); + } + set_a_entities.push_back(static_cast(e)); + } + for (auto* e : set_b) { + if (!e->declaration().is("IfcProduct")) { + throw IfcParse::IfcException("All instances should be of type IfcProduct"); + } + set_b_entities.push_back(static_cast(e)); + } + return $self->clash_intersection_many(set_a_entities, set_b_entities, tolerance, check_all); + } + + std::vector clash_collision_many(const std::vector& set_a, const std::vector& set_b, bool allow_touching) const { + std::vector set_a_entities; + std::vector set_b_entities; + for (auto* e : set_a) { + if (!e->declaration().is("IfcProduct")) { + throw IfcParse::IfcException("All instances should be of type IfcProduct"); + } + set_a_entities.push_back(static_cast(e)); + } + for (auto* e : set_b) { + if (!e->declaration().is("IfcProduct")) { + throw IfcParse::IfcException("All instances should be of type IfcProduct"); + } + set_b_entities.push_back(static_cast(e)); + } + return $self->clash_collision_many(set_a_entities, set_b_entities, allow_touching); + } + + std::vector clash_clearance_many(const std::vector& set_a, const std::vector& set_b, double clearance, bool check_all) const { + std::vector set_a_entities; + std::vector set_b_entities; + for (auto* e : set_a) { + if (!e->declaration().is("IfcProduct")) { + throw IfcParse::IfcException("All instances should be of type IfcProduct"); + } + set_a_entities.push_back(static_cast(e)); + } + for (auto* e : set_b) { + if (!e->declaration().is("IfcProduct")) { + throw IfcParse::IfcException("All instances should be of type IfcProduct"); + } + set_b_entities.push_back(static_cast(e)); + } + return $self->clash_clearance_many(set_a_entities, set_b_entities, clearance, check_all); + } + aggregate_of_instance::ptr select(IfcUtil::IfcBaseClass* e, bool completely_within = false, double extend = 0.0) const { if (!e->declaration().is("IfcProduct")) { throw IfcParse::IfcException("Instance should be an IfcProduct"); diff --git a/src/ifcwrap/IfcParseWrapper.i b/src/ifcwrap/IfcParseWrapper.i index b9fcd5aadd..3f2800b62e 100644 --- a/src/ifcwrap/IfcParseWrapper.i +++ b/src/ifcwrap/IfcParseWrapper.i @@ -582,14 +582,20 @@ static IfcUtil::ArgumentType helper_fn_attribute_type(const IfcUtil::IfcBaseClas %inline %{ IfcParse::IfcFile* open(const std::string& fn) { - IfcParse::IfcFile* f = new IfcParse::IfcFile(fn); + IfcParse::IfcFile* f; + Py_BEGIN_ALLOW_THREADS; + f = new IfcParse::IfcFile(fn); + Py_END_ALLOW_THREADS; return f; } IfcParse::IfcFile* read(const std::string& data) { char* copiedData = new char[data.length()]; memcpy(copiedData, data.c_str(), data.length()); - IfcParse::IfcFile* f = new IfcParse::IfcFile((void *)copiedData, data.length()); + IfcParse::IfcFile* f; + Py_BEGIN_ALLOW_THREADS; + f = new IfcParse::IfcFile((void *)copiedData, data.length()); + Py_END_ALLOW_THREADS; return f; } diff --git a/src/ifcwrap/IfcPython.i b/src/ifcwrap/IfcPython.i index 4c259c68a4..060c2a731a 100644 --- a/src/ifcwrap/IfcPython.i +++ b/src/ifcwrap/IfcPython.i @@ -203,7 +203,14 @@ %include "IfcGeomWrapper.i" %include "IfcParseWrapper.i" +%include "std_vector.i" namespace std { %template(float_array_3) array; + %template(FloatVector) vector; + %template(IntVector) std::vector; + %template(DoubleVector) std::vector; + %template(StringVector) std::vector; + %template(FloatVectorVector) std::vector>; + %template(DoubleVectorVector) std::vector>; } diff --git a/src/opencdeserver/README.md b/src/opencdeserver/README.md index 2623000fbc..87d2fb8147 100644 --- a/src/opencdeserver/README.md +++ b/src/opencdeserver/README.md @@ -250,7 +250,7 @@ https://technical.buildingsmart.org/standards/ifc/ifc-schema-specifications/ https://www.sciencedirect.com/science/article/pii/S0926580523000389 - IFCOpenShell documentation -https://blenderbim.org/docs-python/ +https://docs.ifcopenshell.org/ ### Frontend diff --git a/src/serializers/SvgSerializer.cpp b/src/serializers/SvgSerializer.cpp index cef46ee05c..36b3cdb1d5 100644 --- a/src/serializers/SvgSerializer.cpp +++ b/src/serializers/SvgSerializer.cpp @@ -729,6 +729,10 @@ void SvgSerializer::write(const IfcGeom::BRepElement* brep_obj) { compound_local = comp2; } + if (only_valid_ && !IfcGeom::util::validate_shape(compound_local)) { + return; + } + geometry_data data{ compound_local, dash_arrays, trsf, brep_obj->product(), storey, elev, brep_obj->name(), nameElement(storey, brep_obj) }; if (auto_section_ || auto_elevation_ || section_ref_ || elevation_ref_ || elevation_ref_guid_ || deferred_section_data_) { diff --git a/src/serializers/SvgSerializer.h b/src/serializers/SvgSerializer.h index 9aeff1e0e6..0586b79f72 100644 --- a/src/serializers/SvgSerializer.h +++ b/src/serializers/SvgSerializer.h @@ -400,6 +400,13 @@ namespace { Bnd_Box box; BRepBndLib::AddClose(s, box); + if (box.IsVoid()) { + // false or true, it doesn't really matter, just don't + // proceed, because asking for a corner of a void box + // throws an exception. + return false; + } + auto lower = large_ortho_faces_.lower_bound(0.); auto upper = large_ortho_faces_.upper_bound(min_d); @@ -550,6 +557,7 @@ protected: bool unify_inputs_; bool mirror_y_; bool mirror_x_; + bool only_valid_; int profile_threshold_; @@ -717,6 +725,14 @@ public: return unify_inputs_; } + void setOnlyValid(bool b) { + only_valid_ = b; + } + + bool getOnlyValid(bool b) const { + return only_valid_; + } + void setScale(double s) { scale_ = s; } void setDrawingCenter(double x, double y) { center_x_ = x; center_y_ = y; diff --git a/win/build-deps.cmd b/win/build-deps.cmd index 6e25634f89..304dad5d4a 100644 --- a/win/build-deps.cmd +++ b/win/build-deps.cmd @@ -378,7 +378,7 @@ set DEPENDENCY_NAME=FreeType set DEPENDENCY_DIR=%DEPS_DIR%\freetype-2.7.1 set FREETYPE_ZIP=ft271.zip cd "%DEPS_DIR%" -call :DownloadFile http://download.savannah.gnu.org/releases/freetype/%FREETYPE_ZIP% "%DEPS_DIR%" %FREETYPE_ZIP% +call :DownloadFile https://sourceforge.net/projects/freetype/files/freetype2/2.7.1/%FREETYPE_ZIP%/download "%DEPS_DIR%" %FREETYPE_ZIP% if not %ERRORLEVEL%==0 goto :Error call :ExtractArchive %FREETYPE_ZIP% "%DEPS_DIR%" "%DEPENDENCY_DIR%" if not %ERRORLEVEL%==0 goto :Error