diff --git a/.github/workflows/ci-blenderbim-daily.yml b/.github/workflows/ci-blenderbim-daily.yml deleted file mode 100644 index 0e4321aece..0000000000 --- a/.github/workflows/ci-blenderbim-daily.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: ci-blenderbim-daily - -on: - push: - paths: - - '.github/workflows/ci-blenderbim-daily.yml' - - 'src/blenderbim/**' - - 'src/ifcopenshell-python/ifcopenshell/**' - - 'src/bcf/bcf/**' - - 'src/ifcclash/ifcclash/**' - - 'src/ifccobie/**' - - 'src/ifcdiff/**' - - 'src/ifccsv/**' - - 'src/ifcpatch/ifcpatch/**' - - 'src/ifc4d/ifc4d/**' - - 'src/ifc5d/ifc5d/**' - - 'src/ifccityjson/**' - branches: - - v0.8.0 - workflow_dispatch: - -jobs: - activate: - runs-on: ubuntu-latest - if: | - github.repository == 'IfcOpenShell/IfcOpenShell' - steps: - - name: Set env - run: echo ok go - - build: - needs: activate - name: ${{ matrix.config.name }}-${{ matrix.pyver }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - pyver: [py39, py310, py311, py312] - config: - - { - name: "Windows Build", - short_name: win, - } - - { - name: "Linux Build", - short_name: linux, - } - - { - name: "MacOS Build", - short_name: macos, - } - - { - name: "MacOS ARM Build", - short_name: macosm1, - } - 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' - - name: Get current version - id: version - run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT - - name: Get current date - id: date - run: echo "date=$(date +'%y%m%d')" >> $GITHUB_OUTPUT - - name: Compile - run: | - cd src/blenderbim && make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }} - - name: Find zip file name - id: find_zip - run: | - filepath=$(ls src/blenderbim/dist/blenderbim_*.zip) - echo "filepath=$filepath" >> $GITHUB_OUTPUT - echo "filename=$(basename $filepath)" >> $GITHUB_OUTPUT - - name: Upload zip file to release - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ${{ steps.find_zip.outputs.filepath }} - asset_name: ${{ steps.find_zip.outputs.filename }} - release_name: "blenderbim-${{steps.version.outputs.version}}-alpha${{steps.date.outputs.date}} (unstable)" - tag: "blenderbim-${{steps.version.outputs.version}}-alpha${{steps.date.outputs.date}}" - overwrite: true diff --git a/.github/workflows/ci-blenderbim-choco.yml b/.github/workflows/ci-bonsai-choco.yml similarity index 92% rename from .github/workflows/ci-blenderbim-choco.yml rename to .github/workflows/ci-bonsai-choco.yml index d0e6ebeb4d..8be16269ba 100644 --- a/.github/workflows/ci-blenderbim-choco.yml +++ b/.github/workflows/ci-bonsai-choco.yml @@ -1,4 +1,4 @@ -name: Publish-blenderbim-chocolatey package +name: ci-bonsai-choco on: schedule: @@ -9,11 +9,12 @@ on: # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) # * * * * * - cron: "30 0 * * *" # 30min past utc midnight + workflow_dispatch: env: major: 0 minor: 0 - name: blenderbim + name: bonsai choco_version: 1.1.0 CHOCO_TOKEN: ${{ secrets.CHOCO_TOKEN }} @@ -41,5 +42,6 @@ jobs: - name: Check in release tags if we should do a choco release and perform the release if needed id: do_choco_release run: | - cd /home/runner/work/IfcOpenShell/IfcOpenShell/choco/blenderbim/ && + pip install pygithub + cd /home/runner/work/IfcOpenShell/IfcOpenShell/choco/bonsai/ && python3 choco_release.py diff --git a/.github/workflows/ci-bonsai-daily.yml b/.github/workflows/ci-bonsai-daily.yml new file mode 100644 index 0000000000..a0e5640c2c --- /dev/null +++ b/.github/workflows/ci-bonsai-daily.yml @@ -0,0 +1,149 @@ +name: ci-bonsai-daily + +on: + push: + paths: + - '.github/workflows/ci-bonsai-daily.yml' + - 'src/bonsai/**' + - 'src/ifcopenshell-python/ifcopenshell/**' + - 'src/bcf/bcf/**' + - 'src/ifcclash/ifcclash/**' + - 'src/ifccobie/**' + - 'src/ifcdiff/**' + - 'src/ifccsv/**' + - 'src/ifcpatch/ifcpatch/**' + - 'src/ifc4d/ifc4d/**' + - 'src/ifc5d/ifc5d/**' + - 'src/ifccityjson/**' + branches: + - v0.8.0 + workflow_dispatch: + +jobs: + activate: + runs-on: ubuntu-latest + if: | + github.repository == 'IfcOpenShell/IfcOpenShell' + steps: + - name: Set env + run: echo ok go + + build: + needs: activate + name: ${{ matrix.config.name }}-${{ matrix.pyver }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + pyver: [py311, py312] + config: + - { + name: "Windows Build", + short_name: win, + } + - { + name: "Linux Build", + short_name: linux, + } + - { + name: "MacOS Build", + short_name: macos, + } + - { + name: "MacOS ARM Build", + short_name: macosm1, + } + 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' + - name: Get current version + id: version + run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT + - name: Get current date + id: date + run: echo "date=$(date +'%y%m%d')" >> $GITHUB_OUTPUT + - name: Compile + run: | + cd src/bonsai && make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }} + - name: Find zip file name + id: find_zip + run: | + filepath=$(ls src/bonsai/dist/bonsai_*.zip) + echo "filepath=$filepath" >> $GITHUB_OUTPUT + echo "filename=$(basename $filepath)" >> $GITHUB_OUTPUT + - name: Upload zip file to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ${{ steps.find_zip.outputs.filepath }} + asset_name: ${{ steps.find_zip.outputs.filename }} + release_name: "bonsai-${{steps.version.outputs.version}}-alpha${{steps.date.outputs.date}} (unstable)" + tag: "bonsai-${{steps.version.outputs.version}}-alpha${{steps.date.outputs.date}}" + overwrite: true + body: "See README in https://github.com/IfcOpenShell/bonsai_unstable_repo/ on how to setup autoupdates for daily Bonsai builds." + + update-extensions-repo-and-run-tests: + needs: [build] + runs-on: ubuntu-latest + steps: + - name: Checkout bonsai_unstable_repo repository + uses: actions/checkout@v2 + with: + repository: IfcOpenShell/bonsai_unstable_repo + token: ${{ secrets.IOS_TO_BLENDER_REPO }} + path: bonsai_unstable_repo + + - name: Update index.json on extensions repo + run: | + set -x -e + + # Download Blender. + wget -q -O blender.tar.xz https://ftp.nluug.nl/pub/graphics/blender/release/Blender4.2/blender-4.2.0-linux-x64.tar.xz + tar -xf blender.tar.xz + + # Setup Blender. + BLENDER_PATH=$(find blender-*/ -maxdepth 0 -exec readlink -f {} \;) + export PATH="$PATH:$BLENDER_PATH" + blender --version + + cd bonsai_unstable_repo + pip install -r requirements.txt + python setup_extensions_repo.py --last-tag + git config --global user.name 'IfcOpenBot' + git config --global user.email 'IfcOpenBot@users.noreply.github.com' + git add index.json + git add readme.md + git commit -m "Update index.json" + git push + - name: Run bonsai tests + run: | + set -x -e + BLENDER_PATH=$(find blender-*/ -maxdepth 0 -exec readlink -f {} \;) + export PATH="$PATH:$BLENDER_PATH" + bonsai_zip="$(pwd)/$(ls bonsai_unstable_repo/bonsai_py311*-linux-x64.zip)" + blender --version + blender --command extension install-file -r user_default -e $bonsai_zip + blender --command extension list + + wget -q -O sverchok.zip https://github.com/nortikin/sverchok/archive/refs/heads/master.zip + # ifcsverchok expecting sverchok to be named "sverchok" and not "sverchok-master". + unzip -q sverchok.zip + mv sverchok-master sverchok + zip -q -r sverchok.zip sverchok + rm -r sverchok + blender --command extension install-file -r user_default sverchok.zip + + git clone https://github.com/IfcOpenShell/IfcOpenShell.git IfcOpenShell + cd IfcOpenShell/src/ifcsverchok + make dist + sverchok_zip="$(pwd)/dist/$(ls dist)" + blender --command extension install-file -r user_default $sverchok_zip + + cd ../bonsai + pip install pytest-blender + blender --background --python scripts/setup_pytest.py + blender --python-expr "import bonsai; print(bonsai.bbim_semver); import ifcopenshell; print(ifcopenshell.version)" --background + make test diff --git a/.github/workflows/ci-blenderbim.yml b/.github/workflows/ci-bonsai.yml similarity index 84% rename from .github/workflows/ci-blenderbim.yml rename to .github/workflows/ci-bonsai.yml index f683594063..362f8ecbda 100644 --- a/.github/workflows/ci-blenderbim.yml +++ b/.github/workflows/ci-bonsai.yml @@ -1,6 +1,6 @@ -name: ci-blenderbim +name: ci-bonsai -# Differences from ci-blenderbim-daily.yml: +# Differences from ci-bonsai-daily.yml: # - make has IS_STABLE=TRUE # - action is never triggered and executed only manually # - doesn't add a current date to the release and tag @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - pyver: [py39, py310, py311, py312] + pyver: [py310, py311, py312] config: - { name: "Windows Build", @@ -53,12 +53,12 @@ jobs: run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT - name: Compile run: | - cd src/blenderbim && + cd src/bonsai && make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }} IS_STABLE=TRUE - name: Find zip file name id: find_zip run: | - filepath=$(ls src/blenderbim/dist/blenderbim_*.zip) + filepath=$(ls src/bonsai/dist/bonsai_*.zip) echo "filepath=$filepath" >> $GITHUB_OUTPUT echo "filename=$(basename $filepath)" >> $GITHUB_OUTPUT - name: Upload zip file to release @@ -67,6 +67,6 @@ jobs: repo_token: ${{ secrets.GITHUB_TOKEN }} file: ${{ steps.find_zip.outputs.filepath }} asset_name: ${{ steps.find_zip.outputs.filename }} - release_name: "blenderbim-${{steps.version.outputs.version}}" - tag: "blenderbim-${{steps.version.outputs.version}}" + release_name: "bonsai-${{steps.version.outputs.version}}" + tag: "bonsai-${{steps.version.outputs.version}}" overwrite: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f671da1a8..f821b01bfa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,7 +107,7 @@ jobs: - name: Run IfcConvert on Sample files run: | - (find test/input src/blenderbim/test/files -name '*.ifc' | while read i; do \ + (find test/input src/bonsai/test/files -name '*.ifc' | while read i; do \ echo $i | tee -a log; \ timeout 1m "$(which IfcConvert)" -yv "$i" "$i.obj" --validate >> log 2>&1; \ echo $i $? >> statuses; \ @@ -130,5 +130,7 @@ jobs: cd ../bcf && make test pip install requests cd ../bsdd && make test + pip install deepdiff + cd ../ifcdiff && make test cd ../ifcpatch && make test cd ../ifctester && make test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1633fa254b..aa51b4235e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,12 +8,12 @@ on: jobs: activate: if: github.repository == 'IfcOpenShell/IfcOpenShell' - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - run: echo ok go build: needs: activate - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v2 @@ -52,8 +52,8 @@ jobs: -DOCC_INCLUDE_DIR=/usr/include/opencascade \ -DOCC_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \ -DPYTHON_EXECUTABLE:FILEPATH=/usr/bin/python3 \ - -DPYTHON_INCLUDE_DIR:PATH=/usr/include/python3.8 \ - -DPYTHON_LIBRARY:FILEPATH=/usr/lib/x86_64-linux-gnu/libpython3.8.so \ + -DPYTHON_INCLUDE_DIR:PATH=/usr/include/python3.10 \ + -DPYTHON_LIBRARY:FILEPATH=/usr/lib/x86_64-linux-gnu/libpython3.10.so \ -DLIBXML2_INCLUDE_DIR=/usr/include/libxml2 \ -DLIBXML2_LIBRARIES=/usr/lib/x86_64-linux-gnu/libxml2.so \ -DGLTF_SUPPORT=On \ diff --git a/.gitignore b/.gitignore index 61037d93bf..c219a65be7 100644 --- a/.gitignore +++ b/.gitignore @@ -73,19 +73,16 @@ src/ifcopenshell-python/test/build # mypy cache .mypy_cache -# blenderbim libs -src/blenderbim/blenderbim/libs +# bonsai i18n +src/bonsai/bonsai/translations.py -# blenderbim i18n -src/blenderbim/blenderbim/translations.py +# bonsai test temp files +src/bonsai/test/files/temp +src/bonsai/test/files/basic.ifc.cache.blend +src/bonsai/test/files/basic.ifc.cache.sqlite -# 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 +src/bonsai/drawings +src/bonsai/layouts # ifcopenshell swig and compiled files src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper.so @@ -100,4 +97,4 @@ src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py .cache # Brickschema -src/blenderbim/blenderbim/bim/schema/Brick.ttl +src/bonsai/bonsai/bim/schema/Brick.ttl diff --git a/Dockerfile b/Dockerfile index 1e140c8f15..60689beb09 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # -*- mode: Dockerfile -*- -FROM ubuntu:focal +FROM ubuntu:22.04 ARG CHANNEL ENV CHANNEL=${CHANNEL:-latest} @@ -23,7 +23,7 @@ RUN echo "deb http://archive.ubuntu.com/ubuntu focal-proposed main restricted" | echo "deb http://archive.ubuntu.com/ubuntu focal-proposed multiverse" | tee -a /etc/apt/sources.list; \ apt-get -qq update; \ apt-get -y install tzdata dos2unix rsync; \ - apt-get -y install python3 libxml2 libpython3.8 \ + apt-get -y install python3 libxml2 libpython3.10 \ libboost-all-dev \ libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev \ libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \ diff --git a/README.md b/README.md index 21ba13c948..626c66f0f8 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,10 @@ For more information, see: * [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](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) +* [Bonsai Website](https://bonsaibim.org) +* [Bonsai Documentation](https://docs.bonsaibim.org/index.html) + * [Add-on Installation](https://docs.bonsaibim.org/users/installation.html) + * [Exploring an IFC model](https://docs.bonsaibim.org/users/exploring_an_ifc_model.html) Development is sponsored through your generous donations! @@ -37,7 +37,7 @@ Contents | Name | Description | License | Service | | ------------------------- | --------------------------------------------------------------------- | ------------------- | ------- | | bcf | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bcf-client?label=PyPI&color=006dad)](https://pypi.org/project/bcf-client/) | -| blenderbim | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [![Official](https://img.shields.io/badge/BlenderBIM.org-Download-70ba35)](https://blenderbim.org/download.html) [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=blenderbim-*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=blenderbim&expanded=true) [![Chocolatey](https://img.shields.io/chocolatey/v/blenderbim-nightly?label=Chocolatey&color=5c9fd8)](https://community.chocolatey.org/packages/blenderbim-nightly/) | +| blenderbim | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [![Official](https://img.shields.io/badge/BonsaiBIM.org-Download-70ba35)](https://bonsaibim.org/download.html) [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=blenderbim-*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=blenderbim&expanded=true) [![Chocolatey](https://img.shields.io/chocolatey/v/blenderbim-nightly?label=Chocolatey&color=5c9fd8)](https://community.chocolatey.org/packages/blenderbim-nightly/) | | bsdd | Library to query the bSDD API | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bsdd?label=PyPI&color=006dad)](https://pypi.org/project/bsdd/) | | ifc2ca | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later | | ifc4d | Convert to and from IFC and project management software | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifc4d?label=PyPI&color=006dad)](https://pypi.org/project/ifc4d/) | diff --git a/choco/blenderbim/blenderbim.nuspec b/choco/bonsai/blenderbim.nuspec similarity index 97% rename from choco/blenderbim/blenderbim.nuspec rename to choco/bonsai/blenderbim.nuspec index 7efaab665e..a6ca7b107d 100644 --- a/choco/blenderbim/blenderbim.nuspec +++ b/choco/bonsai/blenderbim.nuspec @@ -15,7 +15,7 @@ https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.8.0/COPYING true https://github.com/IfcOpenShell/IfcOpenShell - https://docs.blenderbim.org/ + https://docs.bonsaibim.org/ https://github.com/IfcOpenShell/IfcOpenShell/issues blender bim blenderbim ifc python opensource foss diff --git a/choco/blenderbim/blenderbim.png b/choco/bonsai/blenderbim.png similarity index 100% rename from choco/blenderbim/blenderbim.png rename to choco/bonsai/blenderbim.png diff --git a/choco/blenderbim/choco_release.py b/choco/bonsai/choco_release.py similarity index 89% rename from choco/blenderbim/choco_release.py rename to choco/bonsai/choco_release.py index 4673d97bda..b63ee3bf89 100644 --- a/choco/blenderbim/choco_release.py +++ b/choco/bonsai/choco_release.py @@ -14,9 +14,11 @@ import os import pathlib import re from urllib import request +from github import Github +from typing import NoReturn -def get_repo_tag_names(): +def get_repo_tag_names() -> list[str]: git_return = os.popen("git tag -l").read() tag_names = [tag_name for tag_name in git_return.split("\n") if tag_name] print(f"{len(tag_names)} tag_names found in repo") @@ -43,7 +45,7 @@ def get_latest_choco_blender_version() -> list: return re.findall(RE_BLENDER_VERSION_MIN_MAJ_PAT, html_txt) -def get_file_sha256_hash(file_path): +def get_file_sha256_hash(file_path: str) -> str: BLOCKSIZE = 65536 hasher = hashlib.sha256() @@ -56,16 +58,29 @@ def get_file_sha256_hash(file_path): return hasher.hexdigest() -def quit_with_error_message(message: str): +def quit_with_error_message(message: str) -> NoReturn: print(f"ERROR: {message}") quit(0) +def get_release_zip(tag: str) -> tuple[str, str]: + g = Github() + repo = g.get_repo("IfcOpenShell/IfcOpenShell") + release = repo.get_release(tag) + for asset in release.get_assets(): + asset_name = asset.name + if python_version not in asset_name: + continue + if TARGET_OS not in asset_name: + continue + return (asset_name, asset.browser_download_url) + raise Exception(f"Couldn't find the release matching '{python_version}' and '{TARGET_OS}' in tag '{tag}'.") + + start = datetime.datetime.now() URL_CHOCO_PACKAGE = "https://community.chocolatey.org/packages/blender" URL_BLENDER_CMAKE = "https://raw.githubusercontent.com/blender/blender/{}/build_files/cmake/Modules/FindPythonLibsUnix.cmake" -URL_IFCOS_RELEASES = "https://github.com/IfcOpenShell/IfcOpenShell/releases/download/" RE_BLENDER_VERSION_MIN_MAJ = r"Latest Version.+Blender (\d+\.\d+)\..+" RE_BLENDER_VERSION_MIN_MAJ_PAT = r"Latest Version.+Blender (\d+\.\d+\.\d+)" RE_BLENDER_PYTHON_VERSION_MAJ_MIN = r"\(_PYTHON_VERSION_SUPPORTED (\d+\.\d+)\)" @@ -79,7 +94,7 @@ os.chdir(BLENDERBIM_DIR) blenderbim_date_yesterday = (datetime.datetime.now() - datetime.timedelta(days=1)).strftime("%y%m%d") should_release = False target_release_tag = "" -target_os = "win" +TARGET_OS = "windows-x64" git_status = os.popen("git status").read() print(git_status) @@ -130,10 +145,7 @@ print(f"{python_version=}") blenderbim_build_version = target_release_tag.replace("blenderbim-", "") # url_blenderbim_py3x_win_zip -release_zip_file_name = f"{target_release_tag}-{python_version}-{target_os}.zip" - -# download release -url_blenderbim_py3x_win_zip = f"{URL_IFCOS_RELEASES}{target_release_tag}/{release_zip_file_name}" +release_zip_file_name, url_blenderbim_py3x_win_zip = get_release_zip(target_release_tag) os.popen(f"wget {url_blenderbim_py3x_win_zip} --no-verbose").read() # sha256sum_blenderbim_py310_win_zip diff --git a/choco/blenderbim/tools/chocolateybeforemodify.ps1 b/choco/bonsai/tools/chocolateybeforemodify.ps1 similarity index 100% rename from choco/blenderbim/tools/chocolateybeforemodify.ps1 rename to choco/bonsai/tools/chocolateybeforemodify.ps1 diff --git a/choco/blenderbim/tools/chocolateyinstall.ps1 b/choco/bonsai/tools/chocolateyinstall.ps1 similarity index 100% rename from choco/blenderbim/tools/chocolateyinstall.ps1 rename to choco/bonsai/tools/chocolateyinstall.ps1 diff --git a/choco/blenderbim/tools/chocolateyuninstall.ps1 b/choco/bonsai/tools/chocolateyuninstall.ps1 similarity index 100% rename from choco/blenderbim/tools/chocolateyuninstall.ps1 rename to choco/bonsai/tools/chocolateyuninstall.ps1 diff --git a/choco/blenderbim/tools/disable_blenderbim_addon.py b/choco/bonsai/tools/disable_blenderbim_addon.py similarity index 100% rename from choco/blenderbim/tools/disable_blenderbim_addon.py rename to choco/bonsai/tools/disable_blenderbim_addon.py diff --git a/choco/blenderbim/tools/enable_blenderbim_addon.py b/choco/bonsai/tools/enable_blenderbim_addon.py similarity index 100% rename from choco/blenderbim/tools/enable_blenderbim_addon.py rename to choco/bonsai/tools/enable_blenderbim_addon.py diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 60294c2256..1990faffef 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -221,7 +221,7 @@ endif() if(GLTF_SUPPORT OR CITYJSON_SUPPORT) UNIFY_ENVVARS_AND_CACHE(JSON_INCLUDE_DIR) - find_path(json_header_path "json.hpp" ${JSON_INCLUDE_DIR} PATH_SUFFIXES "nlohmann") + find_path(json_header_path "nlohmann/json.hpp" HINTS ${JSON_INCLUDE_DIR}) set(JSON_INCLUDE_DIR ${json_header_path}) if(json_header_path) diff --git a/conda/meta.yaml b/conda/meta.yaml index cb7a6b3600..53ad90be3f 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -76,14 +76,14 @@ about: 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) - * [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) + * [IfcOpenShell Documentation](http://bonsaibim.org/docs-python) + * [IfcOpenShell C++ Installation](https://bonsaibim.org/docs-python/ifcopenshell/installation.html) + * [IfcOpenShell Python Installation](https://bonsaibim.org/docs-python/ifcopenshell-python/installation.html) + * [IfcOpenShell Python Hello World Tutorial](https://bonsaibim.org/docs-python/ifcopenshell-python/hello_world.html) + * [Bonsai Website](https://bonsaibim.org) + * [Bonsai Documentation](http://bonsaibim.org/docs) + * [Add-on Installation](https://bonsaibim.org/docs/users/installation.html) + * [Exploring an IFC model](https://bonsaibim.org/docs/users/exploring_an_ifc_model.html) diff --git a/src/bcf/Makefile b/src/bcf/Makefile index ef03ba0d43..c89c31afae 100644 --- a/src/bcf/Makefile +++ b/src/bcf/Makefile @@ -30,8 +30,8 @@ license: # TODO: make this based on xsd file presence .PHONY: models models: - cd src && xsdata generate -p bcf.v2.model --unnest-classes --kw-only --slots -ds Google bcf/v2/xsd - cd src && xsdata generate -p bcf.v3.model --unnest-classes --kw-only --slots -ds Google bcf/v3/xsd + xsdata generate -p bcf.v2.model --unnest-classes --kw-only --slots -ds Google bcf/v2/xsd + xsdata generate -p bcf.v3.model --unnest-classes --kw-only --slots -ds Google bcf/v3/xsd .PHONY: test test: diff --git a/src/bcf/README.md b/src/bcf/README.md index aa1e4951e3..71a503f500 100644 --- a/src/bcf/README.md +++ b/src/bcf/README.md @@ -3,3 +3,7 @@ A simple Python implementation of the BCF standard. Manipulation of BCF-XML is available via `bcfxml.py` and manipulation of BCF-API is available via `bcfapi.py`. + +Python files in 'model' folder are automatically generated from .xsd files (located in 'xsd' folder). +To regenerate them you can use `make models`. +The only exception is 'v2/model/extensions.py', see it's note for the details. diff --git a/src/bcf/bcf/agnostic/extensions.py b/src/bcf/bcf/agnostic/extensions.py new file mode 100644 index 0000000000..c5d4a7f556 --- /dev/null +++ b/src/bcf/bcf/agnostic/extensions.py @@ -0,0 +1,28 @@ +import bcf.v2.model.extensions +import bcf.v3.model.extensions +from typing import NamedTuple, Union +from dataclasses import fields + + +class AttributeData(NamedTuple): + attr_type: type + subattr_name: str + subattr_xsd_name: str + + +Extensions = Union[bcf.v2.model.extensions.Extensions, bcf.v3.model.extensions.Extensions] + + +def get_extensions_attributes(extensions: Extensions) -> dict[str, AttributeData]: + """Return mapping of xsd attribute name to a tuple that consists of: + - Extensions attribute name + - Extensions attribute type type + - subattribute name""" + possible_attributes = {} + for field in fields(type(extensions)): + field_type = field.type.__args__[0] # type: ignore [reportAttributeAccessIssue] + subfield = next(iter(fields(field_type))) + xsd_name = subfield.metadata["name"] + possible_attributes[field.name] = AttributeData(field_type, subfield.name, xsd_name) + + return possible_attributes diff --git a/src/bcf/bcf/agnostic/model.py b/src/bcf/bcf/agnostic/model.py new file mode 100644 index 0000000000..4afd25d1bc --- /dev/null +++ b/src/bcf/bcf/agnostic/model.py @@ -0,0 +1,10 @@ +import bcf.v2.model +import bcf.v3.model +from typing import Union + +BimSnippet = Union[bcf.v2.model.BimSnippet, bcf.v3.model.BimSnippet] +BitMap = Union[bcf.v2.model.VisualizationInfoBitmap, bcf.v3.model.Bitmap] +DocumentReference = Union[bcf.v2.model.TopicDocumentReference, bcf.v3.model.DocumentReference] +HeaderFile = Union[bcf.v2.model.HeaderFile, bcf.v3.model.File] +Topic = Union[bcf.v2.model.Topic, bcf.v3.model.Topic] +ViewPoint = Union[bcf.v2.model.ViewPoint, bcf.v3.model.ViewPoint] diff --git a/src/bcf/bcf/agnostic/topic.py b/src/bcf/bcf/agnostic/topic.py new file mode 100644 index 0000000000..1493d44bd7 --- /dev/null +++ b/src/bcf/bcf/agnostic/topic.py @@ -0,0 +1,101 @@ +import tempfile +import bcf.v2.bcfxml +import bcf.v2.model +import bcf.v2.topic +import bcf.v3.bcfxml +import bcf.v3.model +import bcf.v3.topic +import bcf.agnostic.model as mdl +from pathlib import Path +from typing import Union, Optional +from typing_extensions import assert_never + +TopicHandler = Union[bcf.v2.topic.TopicHandler, bcf.v3.topic.TopicHandler] + + +def extract_file( + topic: TopicHandler, + entity: Union[mdl.HeaderFile, mdl.BimSnippet, mdl.DocumentReference, mdl.BitMap], + bcfxml: Optional[Union[bcf.v2.bcfxml.BcfXml, bcf.v3.bcfxml.BcfXml]] = None, + outfile: Optional[Path] = None, +) -> Union[Path, str, None]: + """Extracts an element with a file into a temporary directory + + These include header files, bim snippets, document references, and + viewpoint bitmaps. External reference are not downloaded. Instead, the + URI reference is returned. + + :param entity: The entity with a file reference to extract + :param outfile: If provided, save the header file to that location. + Otherwise, a temporary directory is created and the filename is + derived from the header's original filename. + :param bcfxml: The BCF XML file to use for resolving document references files. + Required only for BCF v3 document references (in BCF v3 internal documents + are stored at BCF root, not in the topic). + :return: The filepath of the extracted file. It may be a URL if the + header file is external. + """ + if isinstance(entity, mdl.DocumentReference): + if isinstance(entity, bcf.v2.model.TopicDocumentReference): + reference = entity.referenced_document + else: + reference = entity.document_guid + else: + reference = entity.reference + + if not reference: + return None + + # For v3 document references external documents are detected by empty document_guid. + # External bitmaps are not supported by bcf. + if not isinstance(entity, (bcf.v3.model.DocumentReference, mdl.BitMap)) and entity.is_external: + return reference + + if isinstance(entity, bcf.v3.model.DocumentReference): + # Extract document reference filename and contents. + if not bcfxml: + raise TypeError("bcfxml is required for BCF v3 document references.") + assert isinstance(bcfxml, bcf.v3.bcfxml.BcfXml) + error_msg = f"BCF XML is missing document with guid '{reference}'." + if not bcfxml.documents: + raise Exception(error_msg) + definition_docs = bcfxml.documents.definition.documents + if not definition_docs: + raise Exception(error_msg) + docs = next((doc for doc in definition_docs.document if doc.guid == reference), None) + if not docs: + raise Exception(error_msg) + filename = docs.filename + bytes_data = bcfxml.documents.documents[filename] + else: + if isinstance(entity, mdl.BimSnippet): + bytes_data = topic.bim_snippet + assert isinstance(bytes_data, bytes) + elif isinstance(entity, mdl.HeaderFile): + bytes_data = topic.reference_files[reference] + elif isinstance(entity, mdl.BitMap): + bytes_data = next( + byte_data + for vp in topic.viewpoints.values() + for data_reference, byte_data in vp.bitmaps.items() + if data_reference == reference + ) + elif isinstance(entity, bcf.v2.model.TopicDocumentReference): + assert isinstance(topic, bcf.v2.topic.TopicHandler) + bytes_data = topic.document_references[reference] + else: + assert_never(entity) + + # We don't really need it if 'outfile' is None, just keeping type checker happy. + if isinstance(entity, mdl.HeaderFile) and entity.filename: + filename = entity.filename + else: + filename = Path(reference).name + + if not outfile: + outfile = Path(tempfile.mkdtemp()) / filename + + with open(outfile, "wb") as f: + f.write(bytes_data) + + return outfile diff --git a/src/bcf/bcf/agnostic/visinfo.py b/src/bcf/bcf/agnostic/visinfo.py new file mode 100644 index 0000000000..27adf56bdf --- /dev/null +++ b/src/bcf/bcf/agnostic/visinfo.py @@ -0,0 +1,5 @@ +import bcf.v2.visinfo +import bcf.v3.visinfo +from typing import Union + +VisualizationInfoHandler = Union[bcf.v2.visinfo.VisualizationInfoHandler, bcf.v3.visinfo.VisualizationInfoHandler] diff --git a/src/bcf/bcf/bcfxml.py b/src/bcf/bcf/bcfxml.py index 9aeb726c51..073e50af39 100644 --- a/src/bcf/bcf/bcfxml.py +++ b/src/bcf/bcf/bcfxml.py @@ -30,9 +30,10 @@ from bcf.v3.model import Version as Version3 from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer -def load( - filepath: Path, xml_handler: Optional[AbstractXmlParserSerializer] = None -) -> Optional[Union[BcfXml2, BcfXml3]]: +BcfXml = Union[BcfXml2, BcfXml3] + + +def load(filepath: Path, xml_handler: Optional[AbstractXmlParserSerializer] = None) -> Optional[BcfXml]: """ Load a BCF file. diff --git a/src/blenderbim/blenderbim/bim/data/diagrams/.gitkeep b/src/bcf/bcf/extensions.py similarity index 100% rename from src/blenderbim/blenderbim/bim/data/diagrams/.gitkeep rename to src/bcf/bcf/extensions.py diff --git a/src/bcf/bcf/v2/bcfxml.py b/src/bcf/bcf/v2/bcfxml.py index fe1d482d61..b15199f4dc 100644 --- a/src/bcf/bcf/v2/bcfxml.py +++ b/src/bcf/bcf/v2/bcfxml.py @@ -6,7 +6,9 @@ import zipfile from pathlib import Path from typing import Any, NoReturn, Optional, TypeVar +import bcf.agnostic.extensions import bcf.v2.model as mdl +import bcf.v2.model.extensions as mdl_extensions from bcf.inmemory_zipfile import InMemoryZipFile, ZipFileInterface from bcf.v2.topic import TopicHandler from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer @@ -24,7 +26,8 @@ class BcfXml: self._xml_handler = xml_handler or XmlParserSerializer() self._version: Optional[mdl.Version] = None self._project_info: Optional[mdl.ProjectExtension] = None - self._topics: dict[str, TopicHandler] = {} + self._extensions: Optional[mdl_extensions.Extensions] = None + self._topics: Optional[dict[str, TopicHandler]] = None self._extension_schema: Optional[bytes] = None self._zip_file = self._load_zip_file() @@ -85,24 +88,63 @@ class BcfXml: def extension_schema(self, value: bytes) -> None: self._extension_schema = value + @property + def extensions(self) -> Optional[mdl_extensions.Extensions]: + """BCF extensions.""" + + if not self._extensions and self.extension_schema: + import io + from xml.etree import ElementTree as etree + + extensions = mdl_extensions.Extensions() + + xs = "{http://www.w3.org/2001/XMLSchema}" + root = etree.parse(io.BytesIO((self.extension_schema))) + + attrs = bcf.agnostic.extensions.get_extensions_attributes(extensions) + xsd_to_attrs = {v.subattr_xsd_name: k for k, v in attrs.items()} + for node in root.findall(f".//{xs}restriction"): + attr_type = node.attrib.get("base") + if not attr_type: + continue + attr_name = xsd_to_attrs.get(attr_type) + if attr_name is None: + continue + values = [] + for enum in node.findall(f".//{xs}enumeration"): + values.append(enum.attrib["value"]) + if not values: + continue + attr_data = attrs[attr_name] + attr = attr_data.attr_type() + setattr(attr, attr_data.subattr_name, values) + setattr(extensions, attr_name, attr) + + self._extensions = extensions + return self._extensions + + @extensions.setter + def extensions(self, value: Optional[mdl_extensions.Extensions]) -> None: + self._extensions = value + @property def topics(self) -> dict[str, TopicHandler]: """BCF topics.""" - if not self._topics and self._zip_file: - self._topics = self._load_topics(self._zip_file, self._xml_handler) + if self._topics is None: + self._topics = self._load_topics() return self._topics - def _load_topics( - self, zip_file: zipfile.ZipFile, xml_handler: AbstractXmlParserSerializer - ) -> dict[str, TopicHandler]: + def _load_topics(self) -> dict[str, TopicHandler]: topics = {} - for topic_dir in zipfile.Path(zip_file).iterdir(): + if self._zip_file is None: + return topics + for topic_dir in zipfile.Path(self._zip_file).iterdir(): if not topic_dir.is_dir(): continue markup_path = topic_dir.joinpath("markup.bcf") if not markup_path.exists(): continue - topics[topic_dir.name] = TopicHandler(topic_dir, xml_handler) + topics[topic_dir.name] = TopicHandler(topic_dir, self._xml_handler) return topics @classmethod diff --git a/src/bcf/bcf/v2/model/extensions.py b/src/bcf/bcf/v2/model/extensions.py new file mode 100644 index 0000000000..fb25a47c64 --- /dev/null +++ b/src/bcf/bcf/v2/model/extensions.py @@ -0,0 +1,197 @@ +# NOTE: This file is not generated from +# https://github.com/buildingSMART/BCF-XML/blob/release_2_1/Extension%20Schemas/extensions.xsd +# because in bcf 2.1 there is no extensions.xml - I guess, the schema assumes that each .bcf +# will use their own schema patched by it's own extensions.xsd. +# +# To make things simpler we just mimic extensions structures from bcf 3, so they'll have common API, +# and parse extensions.xsd inside .bcf as .xml and fill our structures. +# +# Preferably if we could generate some kind of extensions.xml from extensions.xsd +# and leave all the handling to xsdata. +# +# We also don't add it to __init__.py not to mess with generator. +# +# Currently extensions support for v2 is only read-only. + + +from dataclasses import dataclass, field, fields +from typing import List, NamedTuple, Optional + + +@dataclass(slots=True, kw_only=True) +class ExtensionsPriorities: + class Meta: + global_type = False + + priority: List[str] = field( + default_factory=list, + metadata={ + "name": "Priority", + "type": "Element", + "namespace": "", + "min_length": 1, + "white_space": "collapse", + }, + ) + + +@dataclass(slots=True, kw_only=True) +class ExtensionsSnippetTypes: + class Meta: + global_type = False + + snippet_type: List[str] = field( + default_factory=list, + metadata={ + "name": "SnippetType", + "type": "Element", + "namespace": "", + "min_length": 1, + "white_space": "collapse", + }, + ) + + +@dataclass(slots=True, kw_only=True) +class ExtensionsStages: + class Meta: + global_type = False + + stage: List[str] = field( + default_factory=list, + metadata={ + "name": "Stage", + "type": "Element", + "namespace": "", + "min_length": 1, + "white_space": "collapse", + }, + ) + + +@dataclass(slots=True, kw_only=True) +class ExtensionsTopicLabels: + class Meta: + global_type = False + + topic_label: List[str] = field( + default_factory=list, + metadata={ + "name": "TopicLabel", + "type": "Element", + "namespace": "", + "min_length": 1, + "white_space": "collapse", + }, + ) + + +@dataclass(slots=True, kw_only=True) +class ExtensionsTopicStatuses: + class Meta: + global_type = False + + topic_status: List[str] = field( + default_factory=list, + metadata={ + "name": "TopicStatus", + "type": "Element", + "namespace": "", + "min_length": 1, + "white_space": "collapse", + }, + ) + + +@dataclass(slots=True, kw_only=True) +class ExtensionsTopicTypes: + class Meta: + global_type = False + + topic_type: List[str] = field( + default_factory=list, + metadata={ + "name": "TopicType", + "type": "Element", + "namespace": "", + "min_length": 1, + "white_space": "collapse", + }, + ) + + +@dataclass(slots=True, kw_only=True) +class ExtensionsUsers: + class Meta: + global_type = False + + user: List[str] = field( + default_factory=list, + metadata={ + "name": "UserIdType", + "type": "Element", + "namespace": "", + "min_length": 1, + "white_space": "collapse", + }, + ) + + +@dataclass(slots=True, kw_only=True) +class Extensions: + topic_types: Optional[ExtensionsTopicTypes] = field( + default=None, + metadata={ + "name": "TopicTypes", + "type": "Element", + "namespace": "", + }, + ) + topic_statuses: Optional[ExtensionsTopicStatuses] = field( + default=None, + metadata={ + "name": "TopicStatuses", + "type": "Element", + "namespace": "", + }, + ) + priorities: Optional[ExtensionsPriorities] = field( + default=None, + metadata={ + "name": "Priorities", + "type": "Element", + "namespace": "", + }, + ) + topic_labels: Optional[ExtensionsTopicLabels] = field( + default=None, + metadata={ + "name": "TopicLabels", + "type": "Element", + "namespace": "", + }, + ) + users: Optional[ExtensionsUsers] = field( + default=None, + metadata={ + "name": "Users", + "type": "Element", + "namespace": "", + }, + ) + snippet_types: Optional[ExtensionsSnippetTypes] = field( + default=None, + metadata={ + "name": "SnippetTypes", + "type": "Element", + "namespace": "", + }, + ) + stages: Optional[ExtensionsStages] = field( + default=None, + metadata={ + "name": "Stages", + "type": "Element", + "namespace": "", + }, + ) diff --git a/src/bcf/bcf/v2/topic.py b/src/bcf/bcf/v2/topic.py index 82f09c54a4..0a26cbcc93 100644 --- a/src/bcf/bcf/v2/topic.py +++ b/src/bcf/bcf/v2/topic.py @@ -5,7 +5,7 @@ import tempfile import uuid import zipfile from pathlib import Path -from typing import Any, NoReturn, Optional +from typing import Any, NoReturn, Optional, Union import numpy as np from ifcopenshell import entity_instance @@ -27,9 +27,9 @@ class TopicHandler: xml_handler: Optional[AbstractXmlParserSerializer] = None, ) -> None: self._markup: Optional[mdl.Markup] = None - self._viewpoints: dict[str, VisualizationInfoHandler] = {} - self._reference_files: dict[str, bytes] = {} - self._document_references: dict[str, bytes] = {} + self._viewpoints: Optional[dict[str, VisualizationInfoHandler]] = None + self._reference_files: Optional[dict[str, bytes]] = None + self._document_references: Optional[dict[str, bytes]] = None self._bim_snippet: Optional[bytes] = None self._xml_handler = xml_handler or XmlParserSerializer() self._topic_dir = topic_dir @@ -63,11 +63,21 @@ class TopicHandler: """Return the header of the topic.""" return self.markup.header if self.markup else None + @header.setter + def header(self, header: mdl.Header) -> None: + """Set the header of the topic.""" + self.markup.header = header + @property def comments(self) -> list[mdl.Comment]: """Return the comments of the topic.""" return self.markup.comment if self.markup else [] + @comments.setter + def comments(self, comments: list[mdl.Comment]) -> None: + assert self.markup + self.markup.comment = comments + @property def bim_snippet(self) -> Optional[bytes]: if not self._bim_snippet and self._topic_dir: @@ -80,14 +90,24 @@ class TopicHandler: @property def viewpoints(self) -> dict[str, VisualizationInfoHandler]: - if not self._viewpoints and self._topic_dir: + if self._viewpoints is None: self._viewpoints = self._load_viewpoints() return self._viewpoints + def _load_viewpoints(self) -> dict[str, VisualizationInfoHandler]: + if self._topic_dir and self.markup and (viewpoints := self.markup.viewpoints): + return VisualizationInfoHandler.from_topic_viewpoints(self._topic_dir, viewpoints) + return {} + @property def reference_files(self) -> dict[str, bytes]: - if self._reference_files or not self.header: + if self._reference_files is not None: return self._reference_files + + self._reference_files = {} + if not self.header: + return self._reference_files + for ref in self.header.file: if ref.is_external: continue @@ -99,8 +119,13 @@ class TopicHandler: @property def document_references(self) -> dict[str, bytes]: - if self._document_references or not self.topic: + if self._document_references is not None: return self._document_references + + self._document_references = {} + if not self.topic: + return self._document_references + for doc in self.topic.document_reference: if doc.is_external or not doc.referenced_document: continue @@ -118,11 +143,6 @@ class TopicHandler: return bim_snippet_path.read_bytes() return None - def _load_viewpoints(self) -> dict[str, VisualizationInfoHandler]: - if self.markup and (viewpoints := self.markup.viewpoints): - return VisualizationInfoHandler.from_topic_viewpoints(self._topic_dir, viewpoints) - return {} - @classmethod def create_new( cls, @@ -220,55 +240,7 @@ class TopicHandler: real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part) destination_zip.writestr(real_path.at, self.document_references[doc.referenced_document]) - def extract_file(self, entity, outfile: Optional[Path] = None) -> Path: - """Extracts an element with a file into a temporary directory - - These include header files, bim snippets, document references, and - viewpoint bitmaps. External reference are not downloaded. Instead, the - URI reference is returned. - - :param entity: The entity with a file reference to extract - :type entity: bcf.v2.model.HeaderFile,bcf.v2.model.BimSnippet,bcf.v2.model.TopicDocumentReference - :param outfile: If provided, save the header file to that location. - Otherwise, a temporary directory is created and the filename is - derived from the header's original filename. - :type outfile: pathlib.Path,optional - :return: The filepath of the extracted file. It may be a URL if the - header file is external. - :rtype: Path - """ - if hasattr(entity, "reference"): - reference = entity.reference - else: - reference = entity.referenced_document - - if not reference: - return - - if getattr(entity, "is_external", False): - return entity.reference - - resolved_reference = self._topic_dir - - for part in Path(reference).parts: - if part == "..": - resolved_reference = resolved_reference.parent - else: - resolved_reference = resolved_reference.joinpath(part) - - if not outfile: - if getattr(entity, "filename", None): - filename = entity.filename - else: - filename = resolved_reference.name - outfile = Path(tempfile.mkdtemp()) / filename - - with open(outfile, "wb") as f: - f.write(resolved_reference.read_bytes()) - - return outfile - - def add_viewpoint(self, element: entity_instance) -> None: + def add_viewpoint(self, element: entity_instance) -> VisualizationInfoHandler: """Add a viewpoint pointed at the placement of an IFC element to the topic. Args: @@ -278,7 +250,9 @@ class TopicHandler: self.add_visinfo_handler(new_viewpoint) return new_viewpoint - def add_viewpoint_from_point_and_guids(self, position: NDArray[np.float64], *guids: str) -> None: + def add_viewpoint_from_point_and_guids( + self, position: NDArray[np.float64], *guids: str + ) -> VisualizationInfoHandler: """Add a viewpoint pointing at an XYZ point in space Args: @@ -291,9 +265,17 @@ class TopicHandler: self.add_visinfo_handler(vi_handler) return vi_handler - def add_visinfo_handler(self, new_viewpoint: VisualizationInfoHandler) -> None: + def add_visinfo_handler( + self, new_viewpoint: VisualizationInfoHandler, snapshot_filename: Optional[str] = None + ) -> mdl.ViewPoint: self.viewpoints[new_viewpoint.guid + ".bcfv"] = new_viewpoint - self.markup.viewpoints.append(mdl.ViewPoint(viewpoint=new_viewpoint.guid + ".bcfv", guid=new_viewpoint.guid)) + viewpoint = mdl.ViewPoint( + viewpoint=new_viewpoint.guid + ".bcfv", + snapshot=snapshot_filename, + guid=new_viewpoint.guid, + ) + self.markup.viewpoints.append(viewpoint) + return viewpoint def __eq__(self, other: object) -> bool | NoReturn: return ( diff --git a/src/bcf/bcf/v2/visinfo.py b/src/bcf/bcf/v2/visinfo.py index 08694b8111..359fbc0ab9 100644 --- a/src/bcf/bcf/v2/visinfo.py +++ b/src/bcf/bcf/v2/visinfo.py @@ -1,7 +1,6 @@ import uuid import zipfile -from functools import lru_cache -from typing import Any, Iterable, Optional +from typing import Any, Iterable, Optional, Literal, Union import numpy as np from ifcopenshell import entity_instance @@ -134,6 +133,15 @@ class VisualizationInfoHandler: self._save_bitmaps(bcf_zip, topic_dir) def _save_snapshot(self, bcf_zip: ZipFileInterface, topic_dir: str, filename: Optional[str]) -> None: + if bool(self.snapshot) ^ bool(filename): + data = ["data (VisualizationInfoHandler.snapshot)", "filename (ViewPoint.snapshot)"] + provided_data, missing_data = data if self.snapshot else data[::-1] + print( + f"WARNING. Snapshot with viewpoint guid '{self.guid}' won't be saved to bcf. " + f"Only snapshot {provided_data} is provided but snapshot {missing_data} is missing." + ) + return + if self.snapshot and filename: bcf_zip.writestr(f"{topic_dir}/{filename}", self.snapshot) @@ -196,13 +204,110 @@ class VisualizationInfoHandler: visualization_info=build_viewpoint_from_position_and_guids(position, *guids), xml_handler=xml_handler ) + def get_selected_guids(self) -> Union[list[str], None]: + """ + Return viewpoint selected elements IFC guids. + + Returns: + If viewpoint has no selection settings, return `None`. + Otherwise return a list of selected elements IFC guids. + """ + visualization_info = self.visualization_info + components = visualization_info.components + if not components: + return None + + selection = components.selection + if not selection: + return None + return [guid for c in selection.component if (guid := c.ifc_guid)] + + def set_selected_elements(self, elements: list[ifcopenshell.entity_instance]) -> None: + visualization_info = self.visualization_info + + guids = [e.GlobalId for e in elements] + components = visualization_info.components + if not components: + visibility = mdl.ComponentVisibility(default_visibility=True) + components = mdl.Components(visibility=visibility) + visualization_info.components = components + + selection = components.selection + components_list = [mdl.Component(ifc_guid=guid) for guid in guids] + if not selection: + selection = mdl.ComponentSelection() + components.selection = selection + selection.component = components_list + + def set_visible_elements(self, elements: list[ifcopenshell.entity_instance]) -> None: + self.set_visibility(elements, elements_visibility="VISIBLE") + + def set_hidden_elements(self, elements: list[ifcopenshell.entity_instance]) -> None: + self.set_visibility(elements, elements_visibility="HIDDEN") + + def set_visibility( + self, elements: list[ifcopenshell.entity_instance], elements_visibility: Literal["VISIBLE", "HIDDEN"] + ) -> None: + visualization_info = self.visualization_info + default_visibility = elements_visibility == "HIDDEN" + + guids = [e.GlobalId for e in elements] + components_list = [mdl.Component(ifc_guid=guid) for guid in guids] + components = visualization_info.components + if not components: + visibility = mdl.ComponentVisibility(default_visibility=default_visibility) + components = mdl.Components(visibility=visibility) + visualization_info.components = components + + visibility = components.visibility + if not visibility: + visibility = mdl.ComponentVisibility(default_visibility=default_visibility) + components.visibility = visibility + elif visibility.default_visibility != default_visibility: + visibility.default_visibility = default_visibility + + exceptions = visibility.exceptions + if not exceptions: + exceptions = mdl.ComponentVisibilityExceptions() + visibility.exceptions = exceptions + exceptions.component = components_list + + def get_elements_visibility(self) -> Union[tuple[bool, list[str]], None]: + """ + Return viewpoint elements visibility settings. + + Returns: + If viewpoint has no visibility settings, return `None`. + Otherwise return a tuple containing the default visibility + and a list of IFC element GUIDs listed as exceptions. + + If default visibility is `True`, all elements are visible except the exceptions. + + If default visibility is `False`, all elements are hidden except the exceptions. + """ + + visualization_info = self.visualization_info + components = visualization_info.components + if not components: + return None + + visibility = components.visibility + if not visibility: + return None + default_visibility = visibility.default_visibility or False + + exceptions = visibility.exceptions + if not exceptions: + return default_visibility, [] + guids = [guid for c in exceptions.component if (guid := c.ifc_guid)] + return default_visibility, guids + -@lru_cache(maxsize=None) def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo: """ Return a BCF viewpoint of an IFC element. - This function is cached to speedudp the creation of multiple BCF topics regarding the same element. + This function is cached to speed up the creation of multiple BCF topics regarding the same element. Args: element: The IFC element to point at. @@ -228,7 +333,7 @@ def build_viewpoint_from_position_and_guids(position: NDArray[np.float64], *guid """ Return a BCF viewpoint of an IFC element. - This function is cached to speedudp the creation of multiple BCF topics regarding the same element. + This function is cached to speed up the creation of multiple BCF topics regarding the same element. Args: position: target point coordinates. @@ -249,7 +354,7 @@ def build_components(*guids: str) -> mdl.Components: Return the BCF components from an IFC element GUID. Args: - *guids: One or more IFC element GUID. + *guids: One or more selected IFC element GUID. Returns: The BCF components definition. diff --git a/src/bcf/bcf/v3/bcfxml.py b/src/bcf/bcf/v3/bcfxml.py index 3318264670..16d920737a 100644 --- a/src/bcf/bcf/v3/bcfxml.py +++ b/src/bcf/bcf/v3/bcfxml.py @@ -26,7 +26,7 @@ class BcfXml: self._version: Optional[mdl.Version] = None self._project_info: Optional[mdl.ProjectInfo] = None self._extensions: Optional[mdl.Extensions] = None - self._topics: dict[str, TopicHandler] = {} + self._topics: Optional[dict[str, TopicHandler]] = None self._documents: Optional[DocumentsHandler] = None self._zip_file = self._load_zip_file() @@ -91,18 +91,22 @@ class BcfXml: @property def topics(self) -> dict[str, TopicHandler]: """BCF topics.""" - if not self._topics and self._zip_file: - self._load_topics() + if self._topics is None: + self._topics = self._load_topics() return self._topics - def _load_topics(self) -> None: + def _load_topics(self) -> dict[str, TopicHandler]: + topics = {} + if self._zip_file is None: + return topics for topic_dir in zipfile.Path(self._zip_file).iterdir(): if not topic_dir.is_dir(): continue markup_path = topic_dir.joinpath("markup.bcf") if not markup_path.exists(): continue - self._topics[topic_dir.name] = TopicHandler(topic_dir, self._xml_handler) + topics[topic_dir.name] = TopicHandler(topic_dir, self._xml_handler) + return topics @property def documents(self) -> Optional[DocumentsHandler]: diff --git a/src/bcf/bcf/v3/topic.py b/src/bcf/bcf/v3/topic.py index 44109a21db..b0cfa4268d 100644 --- a/src/bcf/bcf/v3/topic.py +++ b/src/bcf/bcf/v3/topic.py @@ -26,7 +26,8 @@ class TopicHandler: xml_handler: Optional[AbstractXmlParserSerializer] = None, ) -> None: self._markup: Optional[mdl.Markup] = None - self._viewpoints: dict[str, VisualizationInfoHandler] = {} + self._viewpoints: Optional[dict[str, VisualizationInfoHandler]] = None + self._reference_files: Optional[dict[str, bytes]] = None self._bim_snippet: Optional[bytes] = None self._xml_handler = xml_handler or XmlParserSerializer() self._topic_dir = topic_dir @@ -49,22 +50,36 @@ class TopicHandler: return self.markup.topic @property - def guid(self) -> Optional[str]: + def guid(self) -> str: """Return the GUID of the topic.""" if self._markup: return self.topic.guid - return self._topic_dir.name if self._topic_dir else None + return self._topic_dir.name if self._topic_dir else "" @property def header(self) -> Optional[mdl.Header]: """Return the header of the topic.""" return self.markup.header + @header.setter + def header(self, header: mdl.Header) -> None: + """Set the header of the topic.""" + self.markup.header = header + @property def comments(self) -> list[mdl.Comment]: """Return the comments of the topic.""" return self.topic.comments.comment if self.topic.comments else [] + @comments.setter + def comments(self, comments: list[mdl.Comment]) -> None: + topic_comments = self.topic.comments + if topic_comments is None: + if not comments: + return + self.topic.comments = (topic_comments := mdl.TopicComments()) + topic_comments.comment = comments + @property def bim_snippet(self) -> Optional[bytes]: if not self._bim_snippet and self._topic_dir: @@ -77,15 +92,15 @@ class TopicHandler: @property def viewpoints(self) -> dict[str, "VisualizationInfoHandler"]: - if ( - not self._viewpoints - and self._topic_dir - and self.topic.viewpoints - and (viewpoints := self.topic.viewpoints.view_point) - ): - self._viewpoints = VisualizationInfoHandler.from_topic_viewpoints(self._topic_dir, viewpoints) + if self._viewpoints is None: + self._viewpoints = self._load_viewpoints() return self._viewpoints + def _load_viewpoints(self) -> dict[str, "VisualizationInfoHandler"]: + if self._topic_dir and self.topic.viewpoints and (viewpoints := self.topic.viewpoints.view_point): + return VisualizationInfoHandler.from_topic_viewpoints(self._topic_dir, viewpoints) + return {} + def _load_bim_snippet(self) -> Optional[bytes]: bim_snippet_obj = self.topic.bim_snippet if bim_snippet_obj and not bim_snippet_obj.is_external and self._topic_dir: @@ -94,6 +109,27 @@ class TopicHandler: return bim_snippet_path.read_bytes() return None + @property + def reference_files(self) -> dict[str, bytes]: + if self._reference_files is not None: + return self._reference_files + + self._reference_files = {} + if not self.header: + return self._reference_files + + if not self.header.files: + return self._reference_files + + for ref in self.header.files.file: + if ref.is_external: + continue + real_path = self._topic_dir + for path_part in ref.reference.split("/"): + real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part) + self._reference_files[ref.reference] = real_path.read_bytes() + return self._reference_files + @classmethod def create_new( cls, @@ -145,6 +181,7 @@ class TopicHandler: self._save_xml(destination_zip, self._markup, "markup.bcf") self._save_viewpoints(destination_zip, topic_dir) self._save_bim_snippet(destination_zip) + self._save_reference_files(destination_zip) def _save_viewpoints(self, destination_zip: ZipFileInterface, topic_dir: str) -> None: if not self.topic.viewpoints or not (viewpoints := self.topic.viewpoints.view_point): @@ -165,7 +202,20 @@ class TopicHandler: if self.bim_snippet: destination_zip.writestr(f"{self.topic.guid}/{ref_filename}", self.bim_snippet) - def add_viewpoint(self, element: entity_instance) -> None: + def _save_reference_files(self, destination_zip: ZipFileInterface) -> None: + if not self.header: + return + if not self.header.files: + return + for ref in self.header.files.file: + if ref.is_external or not ref.reference: + continue + real_path = self._topic_dir + for path_part in ref.reference.split("/"): + real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part) + destination_zip.writestr(real_path.at, self.reference_files[ref.reference]) + + def add_viewpoint(self, element: entity_instance) -> VisualizationInfoHandler: """ Add a viewpoint tergeting an IFC element to the topic. @@ -174,8 +224,11 @@ class TopicHandler: """ new_viewpoint = VisualizationInfoHandler.create_new(element, self._xml_handler) self.add_visinfo_handler(new_viewpoint) + return new_viewpoint - def add_viewpoint_from_point_and_guids(self, position: NDArray[np.float64], *guids: str) -> None: + def add_viewpoint_from_point_and_guids( + self, position: NDArray[np.float64], *guids: str + ) -> VisualizationInfoHandler: """ Add a viewpoint tergeting an IFC element to the topic. @@ -186,14 +239,21 @@ class TopicHandler: position, *guids, xml_handler=self._xml_handler ) self.add_visinfo_handler(vi_handler) + return vi_handler - def add_visinfo_handler(self, new_viewpoint: VisualizationInfoHandler) -> None: + def add_visinfo_handler( + self, new_viewpoint: VisualizationInfoHandler, snapshot_filename: Optional[str] = None + ) -> mdl.ViewPoint: self.viewpoints[new_viewpoint.guid + ".bcfv"] = new_viewpoint if self.topic.viewpoints is None: self.topic.viewpoints = mdl.TopicViewpoints() - self.topic.viewpoints.view_point.append( - mdl.ViewPoint(viewpoint=new_viewpoint.guid + ".bcfv", guid=new_viewpoint.guid) + viewpoint = mdl.ViewPoint( + viewpoint=new_viewpoint.guid + ".bcfv", + snapshot=snapshot_filename, + guid=new_viewpoint.guid, ) + self.topic.viewpoints.view_point.append(viewpoint) + return viewpoint def __eq__(self, other: object) -> bool | NoReturn: return ( diff --git a/src/bcf/bcf/v3/visinfo.py b/src/bcf/bcf/v3/visinfo.py index f2666288d9..6fb2be6a39 100644 --- a/src/bcf/bcf/v3/visinfo.py +++ b/src/bcf/bcf/v3/visinfo.py @@ -1,11 +1,11 @@ import uuid import zipfile -from functools import lru_cache -from typing import Any, Iterable, Optional +from typing import Any, Iterable, Optional, Literal, Union import numpy as np from ifcopenshell import entity_instance -from ifcopenshell.util import placement +import ifcopenshell.util.unit +import ifcopenshell.util.placement from numpy.typing import NDArray import bcf.v3.model as mdl @@ -133,6 +133,15 @@ class VisualizationInfoHandler: self._save_bitmaps(bcf_zip, topic_dir) def _save_snapshot(self, bcf_zip: ZipFileInterface, topic_dir: str, filename: Optional[str]) -> None: + if bool(self.snapshot) ^ bool(filename): + data = ["data (VisualizationInfoHandler.snapshot)", "filename (ViewPoint.snapshot)"] + provided_data, missing_data = data if self.snapshot else data[::-1] + print( + f"WARNING. Snapshot with viewpoint guid '{self.guid}' won't be saved to bcf. " + f"Only snapshot {provided_data} is provided but snapshot {missing_data} is missing." + ) + return + if self.snapshot and filename: bcf_zip.writestr(f"{topic_dir}/{filename}", self.snapshot) @@ -195,13 +204,110 @@ class VisualizationInfoHandler: visualization_info=build_viewpoint_from_position_and_guids(position, *guids), xml_handler=xml_handler ) + def get_selected_guids(self) -> Union[list[str], None]: + """ + Return viewpoint selected elements IFC guids. + + Returns: + If viewpoint has no selection settings, return `None`. + Otherwise return a list of selected elements IFC guids. + """ + visualization_info = self.visualization_info + components = visualization_info.components + if not components: + return None + + selection = components.selection + if not selection: + return None + return [guid for c in selection.component if (guid := c.ifc_guid)] + + def set_selected_elements(self, elements: list[ifcopenshell.entity_instance]) -> None: + visualization_info = self.visualization_info + + guids = [e.GlobalId for e in elements] + components = visualization_info.components + if not components: + visibility = mdl.ComponentVisibility(default_visibility=True) + components = mdl.Components(visibility=visibility) + visualization_info.components = components + + selection = components.selection + components_list = [mdl.Component(ifc_guid=guid) for guid in guids] + if not selection: + selection = mdl.ComponentSelection() + components.selection = selection + selection.component = components_list + + def set_visible_elements(self, elements: list[ifcopenshell.entity_instance]) -> None: + self.set_visibility(elements, elements_visibility="VISIBLE") + + def set_hidden_elements(self, elements: list[ifcopenshell.entity_instance]) -> None: + self.set_visibility(elements, elements_visibility="HIDDEN") + + def set_visibility( + self, elements: list[ifcopenshell.entity_instance], elements_visibility: Literal["VISIBLE", "HIDDEN"] + ) -> None: + visualization_info = self.visualization_info + default_visibility = elements_visibility == "HIDDEN" + + guids = [e.GlobalId for e in elements] + components_list = [mdl.Component(ifc_guid=guid) for guid in guids] + components = visualization_info.components + if not components: + visibility = mdl.ComponentVisibility(default_visibility=default_visibility) + components = mdl.Components(visibility=visibility) + visualization_info.components = components + + visibility = components.visibility + if not visibility: + visibility = mdl.ComponentVisibility(default_visibility=default_visibility) + components.visibility = visibility + elif visibility.default_visibility != default_visibility: + visibility.default_visibility = default_visibility + + exceptions = visibility.exceptions + if not exceptions: + exceptions = mdl.ComponentVisibilityExceptions() + visibility.exceptions = exceptions + exceptions.component = components_list + + def get_elements_visibility(self) -> Union[tuple[bool, list[str]], None]: + """ + Return viewpoint elements visibility settings. + + Returns: + If viewpoint has no visibility settings, return `None`. + Otherwise return a tuple containing the default visibility + and a list of IFC element GUIDs listed as exceptions. + + If default visibility is `True`, all elements are visible except the exceptions. + + If default visibility is `False`, all elements are hidden except the exceptions. + """ + + visualization_info = self.visualization_info + components = visualization_info.components + if not components: + return None + + visibility = components.visibility + if not visibility: + return None + default_visibility = visibility.default_visibility or False + + exceptions = visibility.exceptions + if not exceptions: + return default_visibility, [] + guids = [guid for c in exceptions.component if (guid := c.ifc_guid)] + return default_visibility, guids + -@lru_cache(maxsize=None) def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo: """ Return a BCF viewpoint of an IFC element. - This function is cached to speedudp the creation of multiple BCF topics regarding the same element. + This function is cached to speed up the creation of multiple BCF topics regarding the same element. Args: element: The IFC element to point at. @@ -209,7 +315,12 @@ def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo: Returns: The BCF viewpoint definition. """ - elem_placement = placement.get_local_placement(element.ObjectPlacement) + ifc_file = element.wrapped_data.file + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) + elem_placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement) + elem_placement[0][3] *= unit_scale + elem_placement[1][3] *= unit_scale + elem_placement[2][3] *= unit_scale return mdl.VisualizationInfo( guid=str(uuid.uuid4()), @@ -222,7 +333,7 @@ def build_viewpoint_from_position_and_guids(position: NDArray[np.float64], *guid """ Return a BCF viewpoint of an IFC element. - This function is cached to speedudp the creation of multiple BCF topics regarding the same element. + This function is cached to speed up the creation of multiple BCF topics regarding the same element. Args: position: target point coordinates. @@ -243,7 +354,7 @@ def build_components(*guids: str) -> mdl.Components: Return the BCF components from an IFC element GUID. Args: - *guids: One or more IFC element GUID. + *guids: One or more selected IFC element GUID. Returns: The BCF components definition. diff --git a/src/bcf/tests/v2/test_example_files.py b/src/bcf/tests/v2/test_example_files.py index 09aa9e4115..3821569c28 100644 --- a/src/bcf/tests/v2/test_example_files.py +++ b/src/bcf/tests/v2/test_example_files.py @@ -55,9 +55,32 @@ def test_save_maximum_information() -> None: def assert_everything_in_place(bcf: BcfXml): assert bcf.version.version_id == "2.1" + assert bcf.project assert bcf.project.name == "BCF API Implementation" + assert bcf.project_info assert bcf.project_info.extension_schema == "extensions.xsd" + assert bcf.extensions + assert bcf.extensions.topic_types + assert bcf.extensions.topic_types.topic_type == ["Architecture", "Hidden Type", "Structural"] + assert bcf.extensions.topic_statuses + assert bcf.extensions.topic_statuses.topic_status == ["Finished status", "Open", "Closed"] + assert bcf.extensions.priorities + assert bcf.extensions.priorities.priority == ["Low", "High", "Medium"] + assert bcf.extensions.topic_labels + assert bcf.extensions.topic_labels.topic_label == [ + "Architecture", + "IT Development", + "Management", + "Mechanical", + "Structural", + ] + assert bcf.extensions.users + assert bcf.extensions.users.user == ["dangl@iabi.eu", "linhard@iabi.eu"] + assert bcf.extensions.snippet_types + assert bcf.extensions.snippet_types.snippet_type == ["IFC2X3", "PDF", "XLSX"] + assert bcf.extensions.stages is None + assert len(bcf.topics) == 2 assert_first_topic_handler(bcf.topics["7ddc3ef0-0ab7-43f1-918a-45e38b42369c"]) second_th = bcf.topics["d1068c81-af04-4546-b63c-348810f6c716"] diff --git a/src/bcf/tests/v3/test_example_files.py b/src/bcf/tests/v3/test_example_files.py index ec0081a0ea..e385b8634f 100644 --- a/src/bcf/tests/v3/test_example_files.py +++ b/src/bcf/tests/v3/test_example_files.py @@ -39,6 +39,22 @@ def assert_everything_in_place(bcf: BcfXml): assert bcf.project.name == "BCF 3.0 test cases" assert bcf.project.project_id == "de894a86-3a08-4ea0-b2d1-6c222b5602d1" + assert bcf.extensions + assert bcf.extensions.topic_types + assert bcf.extensions.topic_types.topic_type == ["ERROR", "WARNING", "INFORMATION", "CLASH", "OTHER"] + assert bcf.extensions.topic_statuses + assert bcf.extensions.topic_statuses.topic_status == ["OPEN", "IN_PROGRESS", "SOLVED", "CLOSED"] + assert bcf.extensions.priorities + assert bcf.extensions.priorities.priority == ["LOW", "MEDIUM", "HIGH", "CRITICAL"] + assert bcf.extensions.topic_labels + assert bcf.extensions.topic_labels.topic_label == [] + assert bcf.extensions.users + assert bcf.extensions.users.user == ["Architect@example.com", "Engineer@example.com", "MEPDesigner@example.com"] + assert bcf.extensions.snippet_types + assert bcf.extensions.snippet_types.snippet_type == [] + assert bcf.extensions.stages + assert bcf.extensions.stages.stage == [] + assert len(bcf.topics) == 1 topic_handler = bcf.topics["8ac9822a-761a-4deb-9f39-f61286acbf6a"] diff --git a/src/blenderbim/README.md b/src/blenderbim/README.md deleted file mode 100644 index f4b9031fe8..0000000000 --- a/src/blenderbim/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# BlenderBIM Add-on - -An add-on to Blender to allow BIM functionality. - -More information on the [BlenderBIM Add-on website](https://blenderbim.org). diff --git a/src/blenderbim/blenderbim/bim/data/webui/static/css/drawings.css b/src/blenderbim/blenderbim/bim/data/webui/static/css/drawings.css deleted file mode 100644 index 9dd2437ecf..0000000000 --- a/src/blenderbim/blenderbim/bim/data/webui/static/css/drawings.css +++ /dev/null @@ -1,249 +0,0 @@ -:root { - --font-family: Arial, sans-serif; - --base-font-size: 16px; - --margin-tiny: 0.125rem; - --margin-small: 0.625rem; - --margin-medium: 1.25rem; - --margin-large: 2.5rem; - --padding-tiny: 0.125rem; - --padding-small: 0.625rem; - --padding-medium: 1rem; - --font-size-large: 1.2rem; - --logo-height: 2.5rem; - --nav-height: 2.5rem; -} - -:root.dark { - color-scheme: dark; - --bg-color: #252525; - --text-color: #e0e0e0; - --nav-bg-color: #121212; - --nav-border-color: #25682a; - --nav-link-color: #fff; - --nav-link-hover-color: #3fb449; - --warning-color: #FFDB8F; - --border-color: #464444; -} - -:root.light { - color-scheme: light; - --bg-color: #ffffff; - --text-color: #000000; - --nav-bg-color: #f8f8f8; - --nav-border-color: #cccccc; - --nav-link-color: #000000; - --nav-link-hover-color: #38a63d; - --warning-color: #FF4500; - --border-color: #222; -} - -html { - font-size: var(--base-font-size); - color: var(--text-color); -} - -body { - background-color: var(--bg-color); - color: var(--text-color); - margin: 0; - font-family: var(--font-family); -} - -#container { - margin-top: var(--margin-medium); - margin-left: var(--margin-small); - margin-right: var(--margin-small); - margin-bottom: var(--margin-medium); - height: calc(100vh - var(--nav-height)); - box-sizing: border-box; - display: flex; -} - -h3 { - margin-top: 0; -} - -nav { - background-color: var(--nav-bg-color); - height: var(--nav-height); - padding: var(--padding-medium) 0; - display: flex; - align-items: center; - position: relative; - border-bottom: 2px solid var(--nav-border-color); -} - -nav .logo { - margin-left: var(--margin-large); - height: var(--logo-height); -} - -nav ul { - list-style-type: none; - margin: 0; - margin-left: 0.0625rem; - display: flex; - flex-grow: 1; - justify-content: center; -} - -nav ul li { - margin-right: 3.125rem; -} - -nav ul li a { - text-decoration: none; - color: var(--nav-link-color); - font-size: var(--font-size-large); -} - -nav ul li a:hover, -nav ul li a.active { - color: var(--nav-link-hover-color); -} - -.warning { - color: var(--warning-color); - margin-bottom: var(--margin-small); - padding: var(--padding-tiny); - display: none; -} - -#toggle-theme { - border: none; - background: none; - cursor: pointer; - font-size: var(--font-size-large); - margin-right: var(--margin-medium); -} - -#toggle-theme:focus { - outline: none; -} - -#client-list { - position: absolute; - top: calc(0 + var(--nav-height)); - overflow-y: auto; - overflow: hidden; - transition: opacity 0.3s ease-out, visibility 0.3s ease-out; - border: 1px solid var(--table-border-color); - background-color: var(--nav-bg-color); - z-index: 1; - opacity: 0; - visibility: hidden; - width: auto; -} - -#client-list.show { - opacity: 1; - visibility: visible; -} - -#connected-list-div { - display: inline-block; -} - -.client { - padding: var(--margin-small); - border-bottom: 1px solid var(--table-border-color); - cursor: pointer; -} - -.client-details { - max-height: 0; - overflow: hidden; - padding-left: var(--padding-small); - transition: max-height 0.2s ease-out, padding 0.2s ease-out, opacity 0.1s ease-out; - background-color: var(--nav-bg-color); - margin-top: var(--margin-tiny); - opacity: 0; -} - -.client-details.show { - max-height: none; - opacity: 1; -} - -.client-detail { - border-bottom: 1px solid var(--table-border-color); - padding: var(--padding-small); -} - -#show-connected-button { - width: auto; - background-color: var(--bg-color); - border: none; - font-size: var(--base-font-size); -} - -#show-connected-button:hover, -.scroll-button:hover { - cursor: pointer; -} - -.scroll-button { - font-size: var(--base-font-size); - margin-left: var(--margin-tiny); - margin-top: var(--margin-small); -} - - -.left { - flex: 1; - background-color: var(--bg-color); - display: flex; - flex-direction: column; - padding: 20px; -} - -.right { - flex: 4; - background-color: var(--bg-color); - border: 1px solid var(--border-color); -} - -#svg-container { - height: 100%; -} - -.panel { - background-color: var(--nav-bg-color); - border: 1px solid var(--border-color); - padding: 10px; - flex-grow: 1; - display: flex; - flex-direction: column; -} - -.card-header, -.card-body { - background-color: var(--bg-color); - border: 1px solid var(--border-color); - overflow: auto; -} - -.card { - background-color: var(--nav-bg-color); - overflow: auto; -} - -li.svg-name { - list-style-type: none; -} - -#dropdown-menu:focus { - outline: none; -} - -/* remove bootstrap css confilcts */ -*, -::after, -::before { - box-sizing: unset; -} - -.btn, -.btn:hover { - color: var(--nav-link-hover-color); -} \ No newline at end of file diff --git a/src/blenderbim/blenderbim/bim/data/webui/static/css/index.css b/src/blenderbim/blenderbim/bim/data/webui/static/css/index.css deleted file mode 100644 index 37386ebea9..0000000000 --- a/src/blenderbim/blenderbim/bim/data/webui/static/css/index.css +++ /dev/null @@ -1,224 +0,0 @@ -:root { - --font-family: Arial, sans-serif; - --base-font-size: 16px; - --margin-tiny: 0.125rem; - --margin-small: 0.625rem; - --margin-medium: 1.25rem; - --margin-large: 2.5rem; - --padding-tiny: 0.125rem; - --padding-small: 0.625rem; - --padding-medium: 1rem; - --font-size-large: 1.2rem; - --logo-height: 2.5rem; - --nav-height: 2.5rem; -} - -:root.dark { - color-scheme: dark; - --bg-color: #252525; - --text-color: #e0e0e0; - --nav-bg-color: #121212; - --nav-border-color: #25682a; - --nav-link-color: #fff; - --nav-link-hover-color: #3fb449; - --warning-color: #FFDB8F; - --table-border-color: #464444; -} - -:root.light { - color-scheme: light; - --bg-color: #ffffff; - --text-color: #000000; - --nav-bg-color: #f8f8f8; - --nav-border-color: #cccccc; - --nav-link-color: #000000; - --nav-link-hover-color: #38a63d; - --warning-color: #FF4500; - --table-border-color: #222; -} - -html { - font-size: var(--base-font-size); -} - -body { - background-color: var(--bg-color); - color: var(--text-color); - margin: 0; - font-family: var(--font-family); - display: flex; - flex-direction: column; - min-height: 100vh; -} - -#container { - flex: 1; - margin-top: var(--margin-medium); - margin-left: var(--margin-small); - margin-right: var(--margin-small); - margin-bottom: var(--margin-medium); -} - -h3 { - margin-top: 0; -} - -nav { - background-color: var(--nav-bg-color); - height: var(--nav-height); - padding: var(--padding-medium) 0; - display: flex; - align-items: center; - position: relative; - border-bottom: 2px solid var(--nav-border-color); -} - -nav .logo { - margin-left: var(--margin-large); - height: var(--logo-height); -} - -nav ul { - list-style-type: none; - margin: 0; - margin-left: 0.0625rem; - display: flex; - flex-grow: 1; - justify-content: center; -} - -nav ul li { - margin-right: 3.125rem; -} - -nav ul li a { - text-decoration: none; - color: var(--nav-link-color); - font-size: var(--font-size-large); -} - -nav ul li a:hover, -nav ul li a.active { - color: var(--nav-link-hover-color); -} - -.warning { - color: var(--warning-color); - margin-bottom: var(--margin-small); - padding: var(--padding-tiny); - display: none; -} - -#toggle-theme { - border: none; - background: none; - cursor: pointer; - font-size: var(--font-size-large); - margin-right: var(--margin-medium); -} - -#toggle-theme:focus { - outline: none; -} - -#client-list { - position: absolute; - top: calc(0 + var(--nav-height)); - overflow-y: auto; - overflow: hidden; - transition: opacity 0.3s ease-out, visibility 0.3s ease-out; - border: 1px solid var(--table-border-color); - background-color: var(--nav-bg-color); - z-index: 1; - opacity: 0; - visibility: hidden; - width: auto; -} - -#client-list.show { - opacity: 1; - visibility: visible; -} - -#connected-list-div { - display: inline-block; -} - -.client { - padding: var(--margin-small); - border-bottom: 1px solid var(--table-border-color); - cursor: pointer; -} - -.client-details { - max-height: 0; - overflow: hidden; - padding-left: var(--padding-small); - transition: max-height 0.2s ease-out, padding 0.2s ease-out, opacity 0.1s ease-out; - background-color: var(--nav-bg-color); - margin-top: var(--margin-tiny); - opacity: 0; -} - -.client-details.show { - max-height: none; - opacity: 1; -} - -.client-detail { - border-bottom: 1px solid var(--table-border-color); - padding: var(--padding-small); -} - -#show-connected-button { - width: auto; - background-color: var(--bg-color); - border: none; - font-size: var(--base-font-size); -} - -#show-connected-button:hover, -.scroll-button:hover { - cursor: pointer; -} - -.scroll-button { - font-size: var(--base-font-size); - margin-left: var(--margin-tiny); - margin-top: var(--margin-small); -} - -footer { - background-color: var(--nav-bg-color); - text-align: right; - padding: var(--padding-tiny); - border-top: 1px solid var(--nav-border-color); -} - -footer p { - margin: 0; - color: var(--text-color); - margin-right: var(--margin-small); - font-size: 0.8rem; -} - -.table-container { - margin-bottom: var(--margin-large); -} - -.csv-table { - margin-top: var(--margin-small); -} - -/* ------------ Overwriting Tabulator CSS rules ------------ */ - -:root.light .tabulator-header, -:root.light .tabulator .tabulator-header .tabulator-col { - background: var(--nav-bg-color) !important; - color: var(--text-color); -} - -:root.light .tabulator { - border-top: 1px solid var(--table-border-color); - border-bottom: 2px solid var(--table-border-color); -} \ No newline at end of file diff --git a/src/blenderbim/blenderbim/bim/data/webui/static/js/gantt.js b/src/blenderbim/blenderbim/bim/data/webui/static/js/gantt.js deleted file mode 100644 index 926d6cf5ad..0000000000 --- a/src/blenderbim/blenderbim/bim/data/webui/static/js/gantt.js +++ /dev/null @@ -1,466 +0,0 @@ -// keeps track of blenders connected in form of -// shown:bool, workSchedule: {}, ganttTasks: {}, -const connectedClients = {}; -let socket; - -// print options -const pageSizes = [ - { value: "210,297", text: "A4 Portrait" }, - { value: "297,210", text: "A4 Landscape" }, - { value: "297,420", text: "A3 Portrait" }, - { value: "420,297", text: "A3 Landscape" }, - { value: "420,594", text: "A2 Portrait" }, - { value: "594,420", text: "A2 Landscape" }, - { value: "594,841", text: "A1 Portrait" }, - { value: "841,594", text: "A1 Landscape" }, - { value: "841,1189", text: "A0 Portrait" }, - { value: "1189,841", text: "A0 Landscape" }, -]; - -// Document ready function -$(document).ready(function () { - var systemTheme = window.matchMedia("(prefers-color-scheme: light)").matches - ? "light" - : "dark"; - var theme = localStorage.getItem("theme") || systemTheme; - setTheme(theme); - - connectSocket(); -}); - -// Function to connect to Socket.IO server -function connectSocket() { - const url = "ws://localhost:" + SOCKET_PORT + "/web"; - socket = io(url); - console.log("socket: ", socket); - - // Register socket event handlers - socket.on("blender_connect", handleBlenderConnect); - socket.on("blender_disconnect", handleBlenderDisconnect); - socket.on("gantt_data", handleGanttData); - socket.on("default_data", handleDefaultData); -} - -// Function to handle 'blender_connect' event -function handleBlenderConnect(blenderId) { - console.log("blender_connect: ", blenderId); - if (!connectedClients.hasOwnProperty(blenderId)) { - connectedClients[blenderId] = { - shown: false, - workSchedule: {}, - ganttTasks: {}, - }; - } -} - -// Function to handle 'blender_disconnect' event -function handleBlenderDisconnect(blenderId) { - console.log("blender_disconnect: ", blenderId); - if (connectedClients.hasOwnProperty(blenderId)) { - delete connectedClients[blenderId]; - removeGanttElement(blenderId); - } - - $("#blender-count").text(function (i, text) { - return parseInt(text, 10) - 1; - }); -} - -// Function to handle 'gantt_data' event -function handleGanttData(data) { - const blenderId = data["blenderId"]; - - console.log(data); - - const filename = data["data"]["ifc_file"]; - const ganttTasks = data["data"]["gantt_data"]["tasks"]; - const ganttWorkSched = data["data"]["gantt_data"]["work_schedule"]; - // const ganttWorkSched = {}; - - if (connectedClients.hasOwnProperty(blenderId)) { - if (!connectedClients[blenderId].shown) { - connectedClients[blenderId] = { - shown: true, - ifc_file: filename, - ganttTasks: ganttTasks, - workSchedule: ganttWorkSched, - }; - addGanttElement(blenderId, ganttTasks, ganttWorkSched, filename); - } else { - updateGanttElement(blenderId, ganttTasks, ganttWorkSched, filename); - connectedClients[blenderId].workSchedule = ganttWorkSched; - connectedClients[blenderId].ganttTasks = ganttTasks; - } - } else { - connectedClients[blenderId] = { - shown: true, - ifc_file: filename, - ganttTasks: ganttTasks, - workSchedule: ganttWorkSched, - }; - addGanttElement(blenderId, ganttTasks, ganttWorkSched, filename); - } -} - -function handleDefaultData(data) { - const blenderId = data["blenderId"]; - const isDirty = data["data"]["is_dirty"]; - showWarning(blenderId, isDirty); - console.log(data); -} - -// Function to add a new gantt with data and filename -function addGanttElement(blenderId, tasks, workSched, filename) { - $("#blender-count").text(function (i, text) { - return parseInt(text, 10) + 1; - }); - - const ganttContainer = $("
") - .addClass("gantt-container") - .attr("id", "container-" + blenderId); - - const ganttTitle = $("

") - .attr("id", "title-" + blenderId) - .text(filename) - .addClass("no-print") - .css("margin-bottom", "10px"); - - const warning = $("
") - .attr("id", "warning-" + blenderId) - .html( - "⚠ Warning: This Gantt Chart may contain outdated data due to recent changes in Blender." - ) - .addClass("warning no-print"); - - const workSchedDiv = $("
").attr("id", "workSched" + blenderId); - - const scheduleTable = $("
") - .addClass("no-print table-description") - .attr("id", "workSchedTable-" + blenderId) - .hide(); - - $.each(workSched, (key, value) => { - value = value ? value : "null"; - $("") - .append($("").text(key)) - .append($("").text(value)) - .appendTo(scheduleTable); - }); - - const toggleButton = $("") - .text("Show Schedule Info") - .addClass("btn no-print") - .on("click", function () { - scheduleTable.toggle(); - const buttonText = scheduleTable.is(":visible") - ? "Hide Schedule Info" - : "Show Schedule Info"; - toggleButton.text(buttonText); - }); - - var ganttInfoDiv = $("
") - .addClass("gantt-info") - .attr("id", "gantt-info-" + blenderId); - - const scheduleName = $("").text("Schedule: " + workSched.Name); - - const createdOn = $("") - .text("Created: " + new Date(workSched.CreationDate).toLocaleDateString()) - .css("float", "right"); - - const ganttDiv = $("
") - .addClass("gantt-chart") - .attr("id", "gantt-" + blenderId); - - ganttInfoDiv.append(scheduleName); - ganttInfoDiv.append(createdOn); - - workSchedDiv.append(toggleButton); - workSchedDiv.append(scheduleTable); - - ganttContainer.append(ganttTitle); - ganttContainer.append(warning); - ganttContainer.append(workSchedDiv); - ganttContainer.append(ganttInfoDiv); - ganttContainer.append(ganttDiv); - - $("#container").append(ganttContainer); - - let g = new JSGantt.GanttChart($("#gantt-" + blenderId)[0], "week"); - g.setOptions({ - vCaptionType: "Caption", // Set to Show Caption : None,Caption,Resource,Duration,Complete, - vQuarterColWidth: 36, - vDateTaskDisplayFormat: "day dd month yyyy", // Shown in tool tip box - vDayMajorDateDisplayFormat: "mon yyyy - Week ww", // Set format to dates in the "Major" header of the "Day" view - vWeekMinorDateDisplayFormat: "dd mon", // Set format to display dates in the "Minor" header of the "Week" view - vLang: "en", - vShowTaskInfoLink: 1, // Show link in tool tip (0/1) - vShowEndWeekDate: 0, // Show/Hide the date for the last day of the week in header for daily - vUseSingleCell: 10000, // Set the threshold cell per table row (Helps performance for large data. - vFormatArr: ["Day", "Week", "Month", "Quarter"], // Even with setUseSingleCell using Hour format on such a large chart can cause issues in some browsers, - vShowRes: true, // Disable the resource column. - vShowComp: false, // Disable the completion column. - vShowDur: false, // Disable the duration column, because jsgantt doesn't calculate durations the way we want. - vAdditionalHeaders: { - ifcduration: { title: "Duration" }, - resourceUsage: { title: "Resource Usage" }, - }, - vUseToolTip: true, // Disable tooltips. - vTooltipTemplate: generateTooltip, - vTotalHeight: 900, - - vEventsChange: { - taskname: editValue, // if you need to use the this scope, do: editValue.bind(this) - res: editValue, - dur: editValue, - comp: editValue, - start: editValue, - end: editValue, - planstart: editValue, - planend: editValue, - cost: editValue, - additional_category: editValue, - }, - }); - JSGantt.addJSONTask(g, tasks); - g.setEditable(true); - g.Draw(); - - connectedClients[blenderId]["gantt"] = g; - - let printButton = $(" + + +
+
+ + + +
+ + + + + +
+

Welcome to the web UI demo page!

+

This demo showcases how Bonsai's Web UI interacts with a WebSocket server. The demo illustrates how the Web UI establishes a connection, exchanges data with the server, and updates the UI dynamically based on incoming data.

+

You can use the above textbox to send a message to Bonsai which will be printed to the console!

+

If you prefer, you can explore the source code directly in the data/webui directory:

+
    +
  • sioserver.py
  • +
  • templates/demo.html
  • +
  • static/js/demo.js
  • +
+

Each file contains detailed comments to help you understand the code. Alternatively, continue reading this demo page to understand how the Web UI operates

+

WebScoket Server (sioserver.py)

+

In the websocket server, there are two key namespaces.

+
    +
  • Blender Namespace: Connects all Bonsai instances.
  • +
  • Web Namespace: Connects all Web UI clients.
  • +
+

These namespaces are attached to the WebSocket server, allowing event-driven communication.

+ +
class WebNamespace(socketio.AsyncNamespace): + async def on_connect(self, sid, environ): + ... + async def on_disconnect(self, sid): + ... + async def on_web_operator(self, sid, data): + ... + +class BlenderNamespace(socketio.AsyncNamespace): + async def on_connect(self, sid, environ): + ... + async def on_disconnect(self, sid): + ... + async def on_demo_data(self, sid, data): + ... +
+ +

Each namespace defines event handlers that are triggered when a connected client emits specific events. For example, the on_demo_data function is invoked when a client emits the demo_data event.

+

The server also defines routes, such as the /demo URL, which are tied to specific request handlers.

+ +
async def demo(request): + with open("templates/demo.html", "r") as f: + template = f.read() + html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version}) + return web.Response(text=html_content, content_type="text/html") + +app.router.add_get("/demo", demo) +
+ +

In this example, when the /demo URL is accessed, the server reads the demo.html template, renders it with variables like port and version, and returns the fully rendered HTML to the client's browser.

+

HTML File (templates/demo.html)

+

When the Web UI URL is visited, the server follows these steps:

+
    +
  • Retrieves the HTML file from the templates directory.
  • +
  • uses pystache to render variables, such as SOCKET_PORT, into the HTML template.
  • +
  • Sends the fully rendered HTML page to the web browser.
  • +
+

The SOCKET_PORT variable is injected into the HTML and later used by the JavaScript file to establish a WebSocket connection.

+ +
<script> + var SOCKET_PORT = {{port}}; +</script> +
+ +

After the HTML page loads, the browser automatically requests static files, such as CSS and JavaScript, from the static directory specified in the sioserver.py file.

+ +
<link rel="stylesheet" href="/static/css/demo.css" id="demo-stylesheet" /> +<script defer src="./static/js/demo.js"></script> +
+ +

JavaScript File (static/js/demo.js)

+

The JavaScript file is responsible for establishing and managing the WebSocket connection. It starts by ensuring the DOM is fully loaded before executing any scripts.

+ +
let socket; +$(document).ready(function () { + var defaultTheme = "blender"; // Default theme to be applied + var theme = localStorage.getItem("theme") || defaultTheme; // Retrieve the stored theme or use the default + setTheme(theme); // Apply the theme + + connectSocket(); // Establish WebSocket connection +}); +
+ +

Upon page load, the script sets the theme (either from local storage or the default) and then calls connectSocket function to initiate the WebSocket connection.

+

Establishing WebSocket connection

+

The connectSocket function constructs the WebSocket connection URL using the SOCKET_PORT variable and then initializes the connection.

+ +
function connectSocket() { + const url = "ws://localhost:" + SOCKET_PORT + "/web"; + socket = io(url); + console.log("socket: ", socket); + + // Register socket event handlers + socket.on("blender_connect", handleBlenderConnect); + socket.on("blender_disconnect", handleBlenderDisconnect); + socket.on("theme_data", handleThemeData); + socket.on("demo_data", handleDemoData); +} +
+ +

The socket variable is used to manage the WebSocket connection within the web namespace. After establishing the connection, the script registers event handlers for specific events emitted by the server.

+

Receiving Data from Bonsai

+

Event handlers process the data received from the server and manipulate the DOM to reflect the changes on the webpage. For instance, the handleDemoData function processes the demo_data event and updates the demo content on the page.

+ +
function handleDemoData(demoData) { + console.log(demoData); + const message = demoData["data"]["demo_message"]; + const blenderId = demoData["blenderId"]; + const id = "message-" + blenderId; + const messageHeader = $("#" + id); + const messageText = `Bonsai instance with ID: ${blenderId} sent the message: ${message}`; + + if (messageHeader[0] === undefined) { + const newMessageElement = $("<h3>", { id: id }).text(messageText); + newMessageElement.prependTo("#message-container"); + } else { + messageHeader.text(messageText); + messageHeader.prependTo("#message-container"); + } +} +
+ +

we use the BlenderId, which is a unique identifier given for by the server for each client connedted to it, to differentiate between different Bonsai instances.

+

This functionality is repeated for other event handlers, such as handleBlenderConnect, handleBlenderDisconnect, and handleThemeData, where specific actions are taken based on the event type and the data provided by the server.

+

Sending Data to Bonsai

+

Data is sent to the server in response to DOM events, such as onclick. These events trigger functions that use the socket variable to emit an event to the server, passing the appropriate data for the specific web operator.

+ +
function SendMessage() { + const inputMessage = $("#input-message").val(); + const msg = { + sourcePage: "demo", + // blenderId: BlenderId, + operator: { + type: "message", + message: inputMessage, + }, + }; + socket.emit("web_operator", msg); +} +
+ +

In this example, the blenderId field specifies which Bonsai instance should receive the operator. If blenderId is not set, the operator is broadcast to all instances.

+

The server forwards this operator to the appropriate Bonsai instance, where it is processed by functions like sio_listen_web_operator and check_operator_queue in tool/web.py.

+ +

Web Tool (tool/web.py)

+

the sio_listen_web_operator function is automatically called when the event web_operator is emitted to Bonsai. It takes the web operator and attempts to put it in a web operators queue.

+ +
@classmethod +async def sio_listen_web_operator(cls, data): + try: + web_operator_queue.put_nowait(data) + except queue.Full: + pass +
+ +

then the check_operator_queue function is called by a timer that is run every second to retrieve operators from the queue and handle them

+ +
@classmethod +def check_operator_queue(cls): + if not bpy.context.scene.WebProperties.is_connected: + with web_operator_queue.mutex: + web_operator_queue.queue.clear() + return None # unregister timer if not connected + + while not web_operator_queue.empty(): + operator = web_operator_queue.get_nowait() + if not operator: + continue + if operator["sourcePage"] == "csv": + cls.handle_csv_operator(operator["operator"]) + elif operator["sourcePage"] == "gantt": + cls.handle_gantt_operator(operator["operator"]) + elif operator["sourcePage"] == "drawings": + cls.handle_drawings_operator(operator["operator"]) + elif operator["sourcePage"] == "demo": + message = operator["operator"]["message"] + print(f"Message from demo page: {message}") + return 1.0 +
+ +

Here we check the source page of the operator and call the appropriate handler for that page.

+
+ +
+
+

Bonsai Version: {{version}}

+
+ + diff --git a/src/blenderbim/blenderbim/bim/data/webui/templates/drawings.html b/src/bonsai/bonsai/bim/data/webui/templates/drawings.html similarity index 68% rename from src/blenderbim/blenderbim/bim/data/webui/templates/drawings.html rename to src/bonsai/bonsai/bim/data/webui/templates/drawings.html index 41eebcd93a..858f1edcf9 100644 --- a/src/blenderbim/blenderbim/bim/data/webui/templates/drawings.html +++ b/src/bonsai/bonsai/bim/data/webui/templates/drawings.html @@ -1,14 +1,18 @@ - + - Web Client + Bonsai Web UI - +