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 | [](https://pypi.org/project/bcf-client/) |
-| blenderbim | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [](https://blenderbim.org/download.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=blenderbim&expanded=true) [](https://community.chocolatey.org/packages/blenderbim-nightly/) |
+| blenderbim | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [](https://bonsaibim.org/download.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=blenderbim&expanded=true) [](https://community.chocolatey.org/packages/blenderbim-nightly/) |
| bsdd | Library to query the bSDD API | LGPL-3.0-or-later | [](https://pypi.org/project/bsdd/) |
| ifc2ca | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later |
| ifc4d | Convert to and from IFC and project management software | LGPL-3.0-or-later | [](https://pypi.org/project/ifc4d/) |
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 = $("