This commit is contained in:
Chirag Singh
2024-07-24 12:15:08 +05:30
49 changed files with 1027 additions and 470 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
run: | run: |
pip install build pip install build
cd src/bcf && cd src/bcf &&
make dist make dist IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI - name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master uses: ortega2247/pypi-upload-action@master
with: with:
+1 -1
View File
@@ -6,7 +6,7 @@ on:
- '.github/workflows/ci-blenderbim-daily.yml' - '.github/workflows/ci-blenderbim-daily.yml'
- 'src/blenderbim/**' - 'src/blenderbim/**'
- 'src/ifcopenshell-python/ifcopenshell/**' - 'src/ifcopenshell-python/ifcopenshell/**'
- 'src/bcf/src/bcf/**' - 'src/bcf/bcf/**'
- 'src/ifcclash/ifcclash/**' - 'src/ifcclash/ifcclash/**'
- 'src/ifccobie/**' - 'src/ifccobie/**'
- 'src/ifcdiff/**' - 'src/ifcdiff/**'
+14 -4
View File
@@ -1,5 +1,10 @@
name: ci-blenderbim name: ci-blenderbim
# Differences from ci-blenderbim-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
on: on:
workflow_dispatch: workflow_dispatch:
@@ -48,15 +53,20 @@ jobs:
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
- name: Compile - name: Compile
run: | run: |
cp -r src/blenderbim src/blenderbim_${{ matrix.config.short_name }}_${{ matrix.pyver }} && cd src/blenderbim &&
cd src/blenderbim_${{ matrix.config.short_name }}_${{ matrix.pyver }} &&
make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }} IS_STABLE=TRUE 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)
echo "filepath=$filepath" >> $GITHUB_OUTPUT
echo "filename=$(basename $filepath)" >> $GITHUB_OUTPUT
- name: Upload zip file to release - name: Upload zip file to release
uses: svenstaro/upload-release-action@v2 uses: svenstaro/upload-release-action@v2
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
file: src/blenderbim_${{ matrix.config.short_name }}_${{ matrix.pyver }}/dist/blenderbim-${{steps.version.outputs.version}}-${{ matrix.pyver }}-${{ matrix.config.short_name }}.zip file: ${{ steps.find_zip.outputs.filepath }}
asset_name: blenderbim-${{steps.version.outputs.version}}-${{ matrix.pyver }}-${{ matrix.config.short_name }}.zip asset_name: ${{ steps.find_zip.outputs.filename }}
release_name: "blenderbim-${{steps.version.outputs.version}}" release_name: "blenderbim-${{steps.version.outputs.version}}"
tag: "blenderbim-${{steps.version.outputs.version}}" tag: "blenderbim-${{steps.version.outputs.version}}"
overwrite: true overwrite: true
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
run: | run: |
pip install build pip install build
cd src/bsdd && cd src/bsdd &&
make dist make dist IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI - name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master uses: ortega2247/pypi-upload-action@master
with: with:
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
run: | run: |
pip install build pip install build
cd src/ifc4d && cd src/ifc4d &&
make dist make dist IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI - name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master uses: ortega2247/pypi-upload-action@master
with: with:
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
run: | run: |
pip install build pip install build
cd src/ifc5d && cd src/ifc5d &&
make dist make dist IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI - name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master uses: ortega2247/pypi-upload-action@master
with: with:
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
run: | run: |
pip install build pip install build
cd src/ifccityjson && cd src/ifccityjson &&
make dist make dist IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI - name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master uses: ortega2247/pypi-upload-action@master
with: with:
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
run: | run: |
pip install build pip install build
cd src/ifcclash && cd src/ifcclash &&
make dist make dist IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI - name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master uses: ortega2247/pypi-upload-action@master
with: with:
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
run: | run: |
pip install build pip install build
cd src/ifccsv && cd src/ifccsv &&
make dist make dist IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI - name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master uses: ortega2247/pypi-upload-action@master
with: with:
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
run: | run: |
pip install build pip install build
cd src/ifcdiff && cd src/ifcdiff &&
make dist make dist IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI - name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master uses: ortega2247/pypi-upload-action@master
with: with:
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
run: | run: |
pip install build pip install build
cd src/ifcfm && cd src/ifcfm &&
make dist make dist IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI - name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master uses: ortega2247/pypi-upload-action@master
with: with:
@@ -57,7 +57,7 @@ jobs:
pip install build pip install build
cp -r src/ifcopenshell-python src/ifcopenshell_${{ matrix.config.short_name }}_${{ matrix.pyver }} && cp -r src/ifcopenshell-python src/ifcopenshell_${{ matrix.config.short_name }}_${{ matrix.pyver }} &&
cd src/ifcopenshell_${{ matrix.config.short_name }}_${{ matrix.pyver }} && cd src/ifcopenshell_${{ matrix.config.short_name }}_${{ matrix.pyver }} &&
make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }} make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }} IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI - name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master uses: ortega2247/pypi-upload-action@master
with: with:
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
run: | run: |
pip install build pip install build
cd src/ifcpatch && cd src/ifcpatch &&
make dist make dist IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI - name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master uses: ortega2247/pypi-upload-action@master
with: with:
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
run: | run: |
pip install build pip install build
cd src/ifctester && cd src/ifctester &&
make dist make dist IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI - name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master uses: ortega2247/pypi-upload-action@master
with: with:
+46 -13
View File
@@ -114,19 +114,18 @@ endif
mkdir -p build/wheels mkdir -p build/wheels
# Provides IfcOpenShell Python functionality # Provides IfcOpenShell Python functionality
cd ../ifcopenshell-python && make dist PLATFORM=$(PLATFORM)64 PYVERSION=$(PYVERSION) && cp dist/*.whl ../blenderbim/build/wheels/ cd ../ifcopenshell-python && make dist PLATFORM=$(PLATFORM)64 PYVERSION=$(PYVERSION) && mv dist/*.whl ../blenderbim/build/wheels/
cd ../bcf && make dist && cp dist/*.whl ../blenderbim/build/wheels/ cd ../bcf && make dist && mv dist/*.whl ../blenderbim/build/wheels/
cd ../ifcclash && make dist && cp dist/*.whl ../blenderbim/build/wheels/ cd ../ifcclash && make dist && mv dist/*.whl ../blenderbim/build/wheels/
cd ../ifcbimtester && make dist && cp dist/*.whl ../blenderbim/build/wheels/ cd ../ifctester && make dist && mv dist/*.whl ../blenderbim/build/wheels/
cd ../ifctester && make dist && cp dist/*.whl ../blenderbim/build/wheels/ cd ../ifcfm && make dist && mv dist/*.whl ../blenderbim/build/wheels/
cd ../ifcfm && make dist && cp dist/*.whl ../blenderbim/build/wheels/ cd ../bsdd && make dist && mv dist/*.whl ../blenderbim/build/wheels/
cd ../bsdd && make dist && cp dist/*.whl ../blenderbim/build/wheels/ cd ../ifcdiff && make dist && mv dist/*.whl ../blenderbim/build/wheels/
cd ../ifcdiff && make dist && cp dist/*.whl ../blenderbim/build/wheels/ cd ../ifccsv && make dist && mv dist/*.whl ../blenderbim/build/wheels/
cd ../ifccsv && make dist && cp dist/*.whl ../blenderbim/build/wheels/ cd ../ifcpatch && make dist && mv dist/*.whl ../blenderbim/build/wheels/
cd ../ifcpatch && make dist && cp dist/*.whl ../blenderbim/build/wheels/ cd ../ifc4d && make dist && mv dist/*.whl ../blenderbim/build/wheels/
cd ../ifc4d && make dist && cp dist/*.whl ../blenderbim/build/wheels/ cd ../ifc5d && make dist && mv dist/*.whl ../blenderbim/build/wheels/
cd ../ifc5d && make dist && cp dist/*.whl ../blenderbim/build/wheels/ cd ../ifccityjson && make dist && mv dist/*.whl ../blenderbim/build/wheels/
cd ../ifccityjson && make dist && cp dist/*.whl ../blenderbim/build/wheels/
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download GitPython --dest=./wheels cd build && . env/$(VENV_ACTIVATE) && $(PIP) download GitPython --dest=./wheels
# Provides audio playback for costing # Provides audio playback for costing
# This is a REALLY IMPORTANT feature # This is a REALLY IMPORTANT feature
@@ -252,9 +251,22 @@ endif
$(SED) "s/os-arch/$(BLENDER_PLATFORM)/" build/blenderbim/blender_manifest.toml $(SED) "s/os-arch/$(BLENDER_PLATFORM)/" build/blenderbim/blender_manifest.toml
# Provides BlenderBIM Add-on functionality # Provides BlenderBIM Add-on functionality
ifeq ($(IS_STABLE), TRUE)
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml $(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml
else
$(SED) 's/version = "0.0.0"/version = "$(VERSION)a$(VERSION_DATE)"/' build/pyproject.toml
endif
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m build cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m build
cp build/dist/*.whl build/wheels/ cp build/dist/*.whl build/wheels/
# Safeguard for non-wheel dependencies slipping in.
wheels=$$(find build/wheels/* ! -name "*.whl"); \
if [ -n "$$wheels" ]; then \
echo "Found non-wheel dependencies:"; \
echo "$$wheels"; \
echo "Error: non-wheel dependencies are not supported by Blender!"; \
exit 1; \
fi
mv build/wheels/*.whl build/blenderbim/wheels/ mv build/wheels/*.whl build/blenderbim/wheels/
# Ugly patch to accomodate MacOS universal builds not being recognized by Blender. # Ugly patch to accomodate MacOS universal builds not being recognized by Blender.
@@ -297,6 +309,27 @@ ifeq ($(PLATFORM), macos)
mv "$$prev_whl_name" "$$whl_name"; mv "$$prev_whl_name" "$$whl_name";
endif endif
# Safeguard for unhandled universal files.
wheels=$$(find build/blenderbim/wheels/*universal2*.whl); \
if [ -n "$$wheels" ]; then \
echo "Found universal2 wheel files:"; \
echo "$$wheels"; \
echo "Error: universal2 wheel files are not supported by Blender!"; \
exit 1; \
fi
ifneq ($(PLATFORM), linux)
# Safeguard: in case one of `pip download` will break,
# it will produce a linux wheel for non-linux build (our github action machine is using linux).
wheels=$$(find build/blenderbim/wheels/*manylinux_*.whl); \
if [ -n "$$wheels" ]; then \
echo "Found linux wheel files in non-linux build:"; \
echo "$$wheels"; \
echo "Error: linux wheels are incompatible with build $(PLATFORM)!"; \
exit 1; \
fi
endif
$(PYTHON) scripts/get_wheels.py build/blenderbim/wheels build/blenderbim/blender_manifest.toml $(PYTHON) scripts/get_wheels.py build/blenderbim/wheels build/blenderbim/blender_manifest.toml
rm -rf build/blenderbim/bim/ rm -rf build/blenderbim/bim/
rm -rf build/blenderbim/core/ rm -rf build/blenderbim/core/
+104 -20
View File
@@ -27,15 +27,16 @@ import sys
IN_BLENDER = sys.modules.get("bpy", None) IN_BLENDER = sys.modules.get("bpy", None)
if IN_BLENDER: if IN_BLENDER:
import bpy import bpy
import addon_utils
import re import re
import platform import platform
import traceback import traceback
import webbrowser import webbrowser
import uuid
import shutil
from collections import deque from collections import deque
from pathlib import Path from pathlib import Path
from typing import Union, Any from typing import Union, Any, Generator
last_commit_hash = "8888888" last_commit_hash = "8888888"
@@ -52,19 +53,35 @@ def get_last_commit_hash() -> Union[str, None]:
last_error = None last_error = None
last_actions: deque = deque(maxlen=10) last_actions: deque = deque(maxlen=10)
bbim_semver: dict[str, Any] = {}
def initialize_bbim_semver():
"""Initialize `bbim_semver` dictionary.
Blender doesn't seem to store a full extension version (only major-minor-patch)
in `addon_utils.modules()->bl_info['version']`,
therefore we just parse it from .toml.
"""
import tomllib
toml_path = Path(__file__).parent / "blender_manifest.toml"
with open(toml_path, "rb") as f:
manifest = tomllib.load(f)
semver_pattern = r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
version_str = manifest["version"]
re_version = re.match(semver_pattern, version_str)
assert re_version
global bbim_semver
bbim_semver = re_version.groupdict()
bbim_semver["version"] = version_str
initialize_bbim_semver()
def get_debug_info(): def get_debug_info():
version = ".".join( bbim_version = bbim_semver["version"]
[
str(x)
for x in [
addon.bl_info.get("version", (-1, -1, -1))
for addon in addon_utils.modules()
if addon.bl_info["name"] == "BlenderBIM"
][0]
]
)
return { return {
"os": platform.system(), "os": platform.system(),
@@ -74,7 +91,7 @@ def get_debug_info():
"machine": platform.machine(), "machine": platform.machine(),
"processor": platform.processor(), "processor": platform.processor(),
"blender_version": bpy.app.version_string, "blender_version": bpy.app.version_string,
"blenderbim_version": version, "blenderbim_version": bbim_version,
"blenderbim_commit_hash": get_last_commit_hash(), "blenderbim_commit_hash": get_last_commit_hash(),
"last_actions": last_actions, "last_actions": last_actions,
"last_error": last_error, "last_error": last_error,
@@ -92,6 +109,68 @@ def format_debug_info(info: dict):
return text.strip() return text.strip()
def get_binaries(path: Path) -> Generator[Path, None, None]:
yield from path.glob("**/*.pyd")
yield from path.glob("**/*.dll")
# pyradiance is using .so files on windows for some reason.
yield from path.glob("**/*.so")
def safe_link_dlls() -> None:
# Blender 4.2+ has a problem on Windows for disabling/enabling/reinstalling extensions
# with loaded binary dependencies (on Windows you can't remove a binary if it's loaded by some program).
# To avoid this issue we temporary hard link dlls to our temp directory on unregister()
# (unregister is executed before Blender will try to uninstall dependencies and the issue will arise).
# Then, Blender won't have a problem unlinking unloaded dlls as they are still linked somewhere.
# On register() we clean up our temp directory with binaries.
#
# TODO: If user uninstalls BlenderBIM to never use it again, temporary directory won't be cleared.
#
# See: https://projects.blender.org/blender/blender/issues/125049
import bpy
ext_path = Path(bpy.utils.user_resource("EXTENSIONS"))
local_path = ext_path / ".local"
# We use random hash subfolder as user may try to enable/disable addon multiple times.
random_hash = uuid.uuid4().hex[:8]
temp_local = ext_path / ".local_temp" / random_hash
temp_local.mkdir(parents=True)
for filepath in get_binaries(local_path):
dest_path = temp_local / filepath.relative_to(local_path)
dest_path.parent.mkdir(exist_ok=True, parents=True)
os.link(filepath, dest_path)
def clean_up_dlls_safe_links() -> None:
import bpy
ext_path = Path(bpy.utils.user_resource("EXTENSIONS"))
temp_path = ext_path / ".local_temp"
if not temp_path.exists():
return
for filepath in get_binaries(temp_path):
try:
os.unlink(filepath)
except PermissionError:
pass
def is_empty_directory(directory: Path) -> bool:
return not any(directory.iterdir())
def remove_empty_folders(folder: Path) -> None:
for subfolder in folder.iterdir():
if subfolder.is_dir():
remove_empty_folders(subfolder)
if is_empty_directory(folder):
folder.rmdir()
remove_empty_folders(temp_path)
if IN_BLENDER: if IN_BLENDER:
def get_binary_info() -> dict[str, Any]: def get_binary_info() -> dict[str, Any]:
@@ -132,8 +211,7 @@ if IN_BLENDER:
# We can't just use __file__ as blenderbim/__init__.py is typically not symlinked # We can't just use __file__ as blenderbim/__init__.py is typically not symlinked
# as Blender have errors symlinking main addon package file. # as Blender have errors symlinking main addon package file.
path = Path(__file__).parent / "bim" / "__init__.py" path = Path(__file__).resolve().parent
path = path.resolve().parent
repo = git.Repo(str(path), search_parent_directories=True) repo = git.Repo(str(path), search_parent_directories=True)
last_commit_hash = repo.head.object.hexsha last_commit_hash = repo.head.object.hexsha
except: except:
@@ -154,11 +232,17 @@ if IN_BLENDER:
ifcopenshell.api.add_pre_listener("*", "action_logger", log_api) ifcopenshell.api.add_pre_listener("*", "action_logger", log_api)
def register(): def register():
if platform.system() == "Windows":
clean_up_dlls_safe_links()
import blenderbim.bim import blenderbim.bim
blenderbim.bim.register() blenderbim.bim.register()
def unregister(): def unregister():
if platform.system() == "Windows":
safe_link_dlls()
import blenderbim.bim import blenderbim.bim
blenderbim.bim.unregister() blenderbim.bim.unregister()
@@ -229,15 +313,15 @@ if IN_BLENDER:
bbim_version = info["blenderbim_version"] bbim_version = info["blenderbim_version"]
py_tag = py.replace(".", "") py_tag = py.replace(".", "")
if "Linux" in info["os"]: if "Linux" in info["os"]:
os = "linux" os = "linux-x64"
elif "Darwin" in info["os"]: elif "Darwin" in info["os"]:
if "arm64" in info["machine"]: if "arm64" in info["machine"]:
os = "macosm1" os = "macos-arm64"
else: else:
os = "macos" os = "macos-x64"
else: else:
os = "win" os = "windows-x64"
op.uri = f"https://github.com/IfcOpenShell/IfcOpenShell/releases/download/blenderbim-{bbim_version}/blenderbim-{bbim_version}-py{py_tag}-{os}.zip" op.uri = f"https://github.com/IfcOpenShell/IfcOpenShell/releases/download/blenderbim-{bbim_version}/blenderbim_py{py_tag}-{bbim_version}-{os}.zip"
class OpenUri(bpy.types.Operator): class OpenUri(bpy.types.Operator):
bl_idname = "bim.open_uri" bl_idname = "bim.open_uri"
@@ -196,7 +196,6 @@ for mod in modules.values():
addon_keymaps = [] addon_keymaps = []
icons = None icons = None
is_registering = False is_registering = False
last_commit_hash = "8888888"
original_scene_panels_polls: dict[bpy.types.Panel, Union[Callable, None]] = dict() original_scene_panels_polls: dict[bpy.types.Panel, Union[Callable, None]] = dict()
@@ -1,3 +1,11 @@
import sys
import os
import webbrowser
blenderbim_lib_path = os.environ.get("blenderbim_lib_path")
if blenderbim_lib_path:
sys.path.insert(0, blenderbim_lib_path)
import argparse import argparse
from aiohttp import web from aiohttp import web
import socketio import socketio
@@ -21,6 +29,8 @@ sio = socketio.AsyncServer(
app = web.Application() app = web.Application()
sio.attach(app) sio.attach(app)
# represents a caching for blender messages
blender_messages = {} blender_messages = {}
@@ -31,6 +41,8 @@ class WebNamespace(socketio.AsyncNamespace):
async def on_connect(self, sid, environ): async def on_connect(self, sid, environ):
print(f"Web client connected: {sid}") print(f"Web client connected: {sid}")
if blender_messages:
await self.send_cached_messages(sid)
async def on_disconnect(self, sid): async def on_disconnect(self, sid):
print(f"Web client disconnected: {sid}") print(f"Web client disconnected: {sid}")
@@ -43,6 +55,14 @@ class WebNamespace(socketio.AsyncNamespace):
room=data["blenderId"], room=data["blenderId"],
) )
async def send_cached_messages(self, sid):
# Send cached messages to the connected web client
for blenderId, messages in blender_messages.items():
if "csv_data" in messages:
await self.emit("csv_data", {"blenderId": blenderId, "data": messages["csv_data"]}, room=sid)
if "gantt_data" in messages:
await self.emit("gantt_data", {"blenderId": blenderId, "data": messages["gantt_data"]}, room=sid)
# Blender namespace # Blender namespace
class BlenderNamespace(socketio.AsyncNamespace): class BlenderNamespace(socketio.AsyncNamespace):
@@ -65,7 +85,7 @@ class BlenderNamespace(socketio.AsyncNamespace):
async def on_data(self, sid, data): async def on_data(self, sid, data):
print(f"Data from Blender client {sid}") print(f"Data from Blender client {sid}")
# blender_messages[sid]["default_data"] = data # blender_messages[sid]["default_data"] = data
# await sio.emit("default_data", {"blenderId": sid, "data": data}, namespace="/web") await sio.emit("default_data", {"blenderId": sid, "data": data}, namespace="/web")
async def on_csv_data(self, sid, data): async def on_csv_data(self, sid, data):
print(f"CSV data from Blender client {sid}") print(f"CSV data from Blender client {sid}")
@@ -99,10 +119,15 @@ async def gantt(request):
return web.Response(text=html_content, content_type="text/html") return web.Response(text=html_content, content_type="text/html")
async def open_web_browser(app):
webbrowser.open(f"http://127.0.0.1:{sio_port}/")
app.router.add_get("/", index) app.router.add_get("/", index)
app.router.add_get("/gantt", gantt) app.router.add_get("/gantt", gantt)
app.router.add_static("/jsgantt/", path="../gantt", name="jsgantt") app.router.add_static("/jsgantt/", path="../gantt", name="jsgantt")
app.router.add_static("/static/", path="./static", name="static") app.router.add_static("/static/", path="./static", name="static")
app.on_startup.append(open_web_browser)
def main(): def main():
@@ -1,19 +1,65 @@
:root { :root {
--font-family: Arial, sans-serif;
--margin-small: 0.625rem;
--margin-medium: 1.25rem;
--margin-large: 2.5rem;
--padding-small: 0.125rem;
--padding-medium: 1rem;
--font-size-small: 1rem;
--font-size-large: 1.2rem;
--logo-height: 2.5rem;
--folder-collapse-font-size: 0.75rem;
--folder-collapse-font-family: Courier, "Courier New", monospace;
--box-shadow: 0 0 0.7rem #5f5f5f66;
}
:root.dark {
color-scheme: dark; color-scheme: dark;
--bg-color: #252525;
--primary-text-color: #e0e0e0;
--secondary-text-color: #c7c7c7;
--nav-bg-color: #121212;
--nav-border-color: #25682a;
--nav-link-color: #fff;
--nav-link-hover-color: #3fb449;
--warning-color: #FFDB8F;
--border-color: #464444;
--hover-bg-color: #3a3a3a;
--highlight-color: #009136;
--task-complete-bg-color: #777777;
--milestone-border-color: #ffffff;
--group-item-bg-color: #4b4b4b;
--input-bg-color: #3b3b3b;
--input-border-color: #000;
}
:root.light {
color-scheme: light;
--bg-color: #ffffff;
--primary-text-color: #000000;
--nav-bg-color: #f8f8f8;
--nav-border-color: #cccccc;
--nav-link-color: #000000;
--nav-link-hover-color: #38a63d;
--warning-color: #FF4500;
}
html {
font-size: var(--base-font-size);
} }
body { body {
background-color: #252525; background-color: var(--bg-color);
color: #e0e0e0; color: var(--primary-text-color);
margin: 0; margin: 0;
font-family: Arial, sans-serif; font-family: var(--font-family);
} }
#container { #container {
margin-top: 20px; margin-top: var(--margin-medium);
margin-left: 10px; margin-left: var(--margin-small);
margin-right: 10px; margin-right: var(--margin-small);
margin-bottom: 20px; margin-bottom: var(--margin-medium);
} }
h3 { h3 {
@@ -21,79 +67,89 @@ h3 {
} }
nav { nav {
background-color: #121212; background-color: var(--nav-bg-color);
padding: 1em 0; padding: var(--padding-medium) 0;
display: flex; display: flex;
align-items: center; align-items: center;
position: relative; position: relative;
border-bottom: 2px solid var(--nav-border-color);
} }
nav .logo { nav .logo {
margin-left: 40px; margin-left: var(--margin-large);
height: 40px; height: var(--logo-height);
/* Adjust height as needed */
} }
nav ul { nav ul {
list-style-type: none; list-style-type: none;
margin: 0; margin: 0;
margin-left: 1px; margin-left: 0.0625rem;
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
justify-content: center; justify-content: center;
} }
nav ul li { nav ul li {
margin-right: 50px; margin-right: 3.125rem;
} }
nav ul li a { nav ul li a {
text-decoration: none; text-decoration: none;
color: #fff; color: var(--nav-link-color);
font-size: 1.2em; font-size: var(--font-size-large);
} }
nav ul li a:hover, nav ul li a:hover,
nav ul li a.active { nav ul li a.active {
color: #3fb449; color: var(--nav-link-hover-color);
} }
nav .bottom-bar { .warning {
position: absolute; color: var(--warning-color);
bottom: 0; margin-bottom: var(--margin-small);
left: 0; padding: var(--padding-small);
width: 100%; display: none;
height: 2px;
background-color: #25682a;
} }
.table-description { .table-description {
margin-bottom: 1rem; margin-bottom: var(--margin-medium);
width: 50%; width: 50%;
background-color: transparent; background-color: transparent;
border-collapse: collapse; border-collapse: collapse;
border-spacing: 0; border-spacing: 0;
border-color: #464444; border-color: var(--border-color);
border: 1px solid #464444; border: 1px solid var(--border-color);
border-radius: 0.25rem; border-radius: 0.25rem;
box-shadow: 0 0 0.7rem rgba(95, 95, 95, 0.4); box-shadow: var(--box-shadow);
text-align: center; text-align: center;
} }
.table-description tr { .table-description tr {
display: table-row; display: table-row;
border-bottom: 1px solid #464444; border-bottom: 1px solid var(--border-color);
}
#toggle-theme {
border: none;
background: none;
cursor: pointer;
font-size: var(--font-size-large);
margin-right: var(--margin-medium);
}
#toggle-theme:focus {
outline: none;
} }
.gantt-info { .gantt-info {
margin: 0.5rem; margin: 0.5rem;
font-size: 1rem; font-size: var(--font-size-small);
font-weight: bold; font-weight: bold;
} }
.btn { .btn {
margin: 0.5rem; margin: 0.5rem;
font-size: 1rem; font-size: var(--font-size-small);
cursor: pointer; cursor: pointer;
} }
@@ -104,86 +160,76 @@ nav .bottom-bar {
} }
/* ------------ Overwriting JSGantt CSS rules ------------ */ /* ------------ Overwriting JSGantt CSS rules ------------ */
:root.dark div.gantt {
div.gantt { background-color: var(--bg-color);
background-color: #252525; color: var(--secondary-text-color);
color: #c7c7c7;
} }
.gantt table { :root.dark .gantt table,
border-color: #464444; :root.dark .gantt td {
; border-color: var(--border-color);
}
.gantt td {
border-color: #464444;
;
} }
/* Headings and cell defaults */ /* Headings and cell defaults */
.gmajorheading, :root.dark .gmajorheading,
.gminorheading, :root.dark .gminorheading,
.gminorheadingwkend, :root.dark .gminorheadingwkend,
.gtaskcell, :root.dark .gtaskcell,
.gtaskcellcurrent, :root.dark .gtaskcellcurrent,
.gtaskcellwkend, :root.dark .gtaskcellwkend,
.gname, :root.dark .gname,
.ggroupitem, :root.dark .ggroupitem,
.gtaskheading { :root.dark .gtaskheading {
background-color: #252525; background-color: var(--bg-color);
border-color: #464444; border-color: var(--border-color);
;
} }
.gtaskheading, :root.dark .gtaskheading,
.gname, :root.dark .gname,
.gtaskname, :root.dark .gtaskname,
.gres, :root.dark .gres,
.gdur, :root.dark .gdur,
.gcomp, :root.dark .gcomp,
.gstartdate, :root.dark .gstartdate,
.gplanstartdate, :root.dark .gplanstartdate,
.gplanenddate, :root.dark .gplanenddate,
.gcost, :root.dark .gcost,
.gchartlbl, :root.dark .gchartlbl,
.gcontainercol, :root.dark .gcontainercol,
.genddate { :root.dark .genddate {
color: #c7c7c7; color: var(--secondary-text-color);
border-color: #464444; border-color: var(--border-color);
;
} }
.gtaskname>div { :root.dark .gtaskname>div {
color: #c7c7c7; color: var(--secondary-text-color);
} }
.gtaskbarcontainer.gplan, :root.dark .gtaskbarcontainer.gplan,
.gchartlbl.gcontainercol { :root.dark .gchartlbl.gcontainercol {
background: #252525; background: var(--bg-color);
border-color: #464444; border-color: var(--border-color);
} }
.gtaskname div, :root.dark .gtaskname div,
.gtaskheading div, :root.dark .gtaskheading div,
.gtaskname span { :root.dark .gtaskname span {
color: #c7c7c7; color: var(--secondary-text-color);
} }
:root.dark .gtasklist,
.gtasklist, :root.dark .gadditional {
.gadditional { border: var(--border-color) 1px solid;
border: #464444 1px solid
} }
.gchartgrid { :root.dark .gchartgrid {
background-color: #252525; background-color: var(--bg-color);
} }
.glistgrid, :root.dark .glistgrid,
.glistlbl { :root.dark .glistlbl {
background-color: #252525; background-color: var(--bg-color);
border-color: #464444; border-color: var(--border-color);
;
} }
/* .gTaskInfo { /* .gTaskInfo {
@@ -193,88 +239,81 @@ div.gantt {
} */ } */
/* Dark mode scrollbar */ /* Dark mode scrollbar */
.frame::-webkit-scrollbar-thumb { :root.dark .frame::-webkit-scrollbar-thumb,
background-color: #252525; :root.dark .frame::-webkit-scrollbar-track {
} background-color: var(--bg-color);
.frame::-webkit-scrollbar-track {
background-color: #252525;
} }
/* Highlight row */ /* Highlight row */
.gitemhighlight td { :root.dark .gitemhighlight td {
background-color: #009136; background-color: var(--highlight-color);
color: #ffffff; color: var(--nav-link-color);
} }
/* Differentiate Group, Milestone and Ordinary task items (applied to row) */ /* Differentiate Group, Milestone and Ordinary task items (applied to row) */
.ggroupitem { :root.dark .ggroupitem {
background-color: #252525; background-color: var(--bg-color);
font-weight: bold; font-weight: bold;
border-color: #464444; border-color: var(--border-color);
} }
.gmileitem, :root.dark .gmileitem,
.glineitem { :root.dark .glineitem {
background-color: #252525; background-color: var(--bg-color);
} }
/* Task bar caption text styles */ /* Task bar caption text styles */
.gmilecaption, :root.dark .gmilecaption,
.ggroupcaption, :root.dark .ggroupcaption,
.gcaption { :root.dark .gcaption {
color: #ffffff; color: var(--nav-link-color);
} }
/* Task complete %age bar */ /* Task complete %age bar */
.gtaskcomplete { :root.dark .gtaskcomplete {
background-color: #777777; background-color: var(--task-complete-bg-color);
} }
/* Milestones */ /* Milestones */
.gmdtop { :root.dark .gmdtop,
border-bottom: 5px solid #ffffff; :root.dark .gmdbottom {
border-bottom: 5px solid var(--milestone-border-color);
} }
.gmdbottom { :root.dark .gfoldercollapse {
border-top: 5px solid #ffffff; color: var(--secondary-text-color);
}
.gfoldercollapse {
color: #c7c7c7;
cursor: pointer; cursor: pointer;
font-weight: bold; font-weight: bold;
font-size: var(--folder-collapse-font-size);
font-size: 12px; font-family: var(--folder-collapse-font-family);
font-family: Courier, "Courier New", monospace;
} }
/* Highlight for collapsible row */ /* Highlight for collapsible row */
.gname.ggroupitem { :root.dark .gname.ggroupitem {
background-color: rgb(75, 75, 75); background-color: var(--group-item-bg-color);
} }
/* Form label and selected highlighting */ /* Form label and selected highlighting */
.gformlabel { :root.dark .gformlabel {
background-color: #2e2e2e; background-color: var(--formlabel-bg-color);
color: #c7c7c7; color: var(--secondary-text-color);
border: #4a4a4a 1px solid; border: var(--formlabel-border-color) 1px solid;
} }
span.gformlabel:hover { :root.dark span.gformlabel:hover {
background-color: #3a3a3a; background-color: var(--hover-bg-color);
border-color: #565656; border-color: var(--border-color);
} }
span.gselected { :root.dark span.gselected {
background-color: #565656; background-color: var(--formlabel-selected-bg-color);
border-color: #777777; border-color: var(--task-complete-bg-color);
color: #ffffff; color: var(--nav-link-color);
} }
.gantt-inputtable { :root.dark .gantt-inputtable {
background-color: #3b3b3b; background-color: var(--input-bg-color);
box-sizing: border-box; box-sizing: border-box;
border: 1px solid #000; border: 1px solid var(--input-border-color);
color: #c7c7c7; color: var(--secondary-text-color);
} }
@@ -1,27 +1,60 @@
:root { :root {
--font-family: Arial, sans-serif;
--margin-small: 0.625rem;
--margin-medium: 1.25rem;
--margin-large: 2.5rem;
--padding-small: 0.125rem;
--padding-medium: 1rem;
--font-size-large: 1.2rem;
--logo-height: 2.5rem;
}
:root.dark {
color-scheme: 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;
}
: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;
}
html {
font-size: var(--base-font-size);
} }
body { body {
background-color: #252525; background-color: var(--bg-color);
color: #e0e0e0; color: var(--text-color);
margin: 0; margin: 0;
font-family: Arial, sans-serif; font-family: var(--font-family);
} }
#container { #container {
margin-top: 20px; margin-top: var(--margin-medium);
margin-left: 10px; margin-left: var(--margin-small);
margin-right: 10px; margin-right: var(--margin-small);
margin-bottom: 20px margin-bottom: var(--margin-medium);
} }
.table-container { .table-container {
margin-bottom: 40px; margin-bottom: var(--margin-large);
} }
.csv-table { .csv-table {
margin-top: 10px; margin-top: var(--margin-small);
} }
h3 { h3 {
@@ -29,48 +62,71 @@ h3 {
} }
nav { nav {
background-color: #121212; background-color: var(--nav-bg-color);
padding: 1em 0; padding: var(--padding-medium) 0;
display: flex; display: flex;
align-items: center; align-items: center;
position: relative; position: relative;
border-bottom: 2px solid var(--nav-border-color);
} }
nav .logo { nav .logo {
margin-left: 40px; margin-left: var(--margin-large);
height: 40px; height: var(--logo-height);
/* Adjust height as needed */
} }
nav ul { nav ul {
list-style-type: none; list-style-type: none;
margin: 0; margin: 0;
margin-left: 1px; margin-left: 0.0625rem;
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
justify-content: center; justify-content: center;
} }
nav ul li { nav ul li {
margin-right: 50px; margin-right: 3.125rem;
} }
nav ul li a { nav ul li a {
text-decoration: none; text-decoration: none;
color: #fff; color: var(--nav-link-color);
font-size: 1.2em; font-size: var(--font-size-large);
} }
nav ul li a:hover, nav ul li a:hover,
nav ul li a.active { nav ul li a.active {
color: #3fb449; color: var(--nav-link-hover-color);
} }
nav .bottom-bar { .warning {
position: absolute; color: var(--warning-color);
bottom: 0; margin-bottom: var(--margin-small);
left: 0; padding: var(--padding-small);
width: 100%; display: none;
height: 2px; }
background-color: #25682a;
#toggle-theme {
border: none;
background: none;
cursor: pointer;
font-size: var(--font-size-large);
margin-right: var(--margin-medium);
}
#toggle-theme:focus {
outline: none;
}
/* ------------ 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 #222;
border-bottom: 2px solid #222;
} }
@@ -19,6 +19,12 @@ const pageSizes = [
// Document ready function // Document ready function
$(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(); connectSocket();
}); });
@@ -32,6 +38,7 @@ function connectSocket() {
socket.on("blender_connect", handleBlenderConnect); socket.on("blender_connect", handleBlenderConnect);
socket.on("blender_disconnect", handleBlenderDisconnect); socket.on("blender_disconnect", handleBlenderDisconnect);
socket.on("gantt_data", handleGanttData); socket.on("gantt_data", handleGanttData);
socket.on("default_data", handleDefaultData);
} }
// Function to handle 'blender_connect' event // Function to handle 'blender_connect' event
@@ -61,7 +68,7 @@ function handleGanttData(data) {
console.log(data); console.log(data);
const filename = data["data"]["IFC_File"]; const filename = data["data"]["ifc_file"];
const ganttTasks = data["data"]["gantt_data"]["tasks"]; const ganttTasks = data["data"]["gantt_data"]["tasks"];
const ganttWorkSched = data["data"]["gantt_data"]["work_schedule"]; const ganttWorkSched = data["data"]["gantt_data"]["work_schedule"];
// const ganttWorkSched = {}; // const ganttWorkSched = {};
@@ -89,6 +96,13 @@ function handleGanttData(data) {
} }
} }
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 to add a new gantt with data and filename
function addGanttElement(blenderId, tasks, workSched, filename) { function addGanttElement(blenderId, tasks, workSched, filename) {
const ganttContainer = $("<div></div>") const ganttContainer = $("<div></div>")
@@ -101,6 +115,13 @@ function addGanttElement(blenderId, tasks, workSched, filename) {
.addClass("no-print") .addClass("no-print")
.css("margin-bottom", "10px"); .css("margin-bottom", "10px");
const warning = $("<div></div>")
.attr("id", "warning-" + blenderId)
.html(
"&#9888; Warning: This Gantt Chart may contain outdated data due to recent changes in Blender."
)
.addClass("warning no-print");
const workSchedDiv = $("<div></div>").attr("id", "workSched" + blenderId); const workSchedDiv = $("<div></div>").attr("id", "workSched" + blenderId);
const scheduleTable = $("<table></table>") const scheduleTable = $("<table></table>")
@@ -148,6 +169,7 @@ function addGanttElement(blenderId, tasks, workSched, filename) {
workSchedDiv.append(scheduleTable); workSchedDiv.append(scheduleTable);
ganttContainer.append(ganttTitle); ganttContainer.append(ganttTitle);
ganttContainer.append(warning);
ganttContainer.append(workSchedDiv); ganttContainer.append(workSchedDiv);
ganttContainer.append(ganttInfoDiv); ganttContainer.append(ganttInfoDiv);
ganttContainer.append(ganttDiv); ganttContainer.append(ganttDiv);
@@ -233,8 +255,7 @@ function addGanttElement(blenderId, tasks, workSched, filename) {
addEventListener("afterprint", (event) => { addEventListener("afterprint", (event) => {
g.setEditable(true); g.setEditable(true);
},); });
let css = let css =
"@media print {\n" + "@media print {\n" +
@@ -280,6 +301,7 @@ function updateGanttElement(blenderId, tasks, workSched, filename) {
g.Draw(); g.Draw();
$("#title-" + blenderId).text(filename); $("#title-" + blenderId).text(filename);
$("#warning-" + blenderId).css("display", "none");
} }
// Function to remove gantt element // Function to remove gantt element
@@ -287,6 +309,10 @@ function removeGanttElement(blenderId) {
$("#container-" + blenderId).remove(); $("#container-" + blenderId).remove();
} }
function showWarning(blenderId, isDirty) {
$("#warning-" + blenderId).css("display", "block");
}
// Utility function to create a tooltip for the gantt chars // Utility function to create a tooltip for the gantt chars
function generateTooltip(task) { function generateTooltip(task) {
var dataObject = task.getDataObject(); var dataObject = task.getDataObject();
@@ -337,3 +363,22 @@ function editValue(list, task, event, cell, column) {
}; };
socket.emit("web_operator", msg); socket.emit("web_operator", msg);
} }
function setTheme(theme) {
if (theme === "light") {
$("html").removeClass("dark").addClass("light");
$("#toggle-theme").html('<i class="fas fa-sun"></i>');
} else {
$("html").removeClass("light").addClass("dark");
$("#toggle-theme").html('<i class="fas fa-moon"></i>');
}
localStorage.setItem("theme", theme);
}
function toggleTheme() {
if ($("html").hasClass("dark")) {
setTheme("light");
} else {
setTheme("dark");
}
}
@@ -5,6 +5,12 @@ let socket;
// Document ready function // Document ready function
$(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(); connectSocket();
}); });
@@ -18,6 +24,7 @@ function connectSocket() {
socket.on("blender_connect", handleBlenderConnect); socket.on("blender_connect", handleBlenderConnect);
socket.on("blender_disconnect", handleBlenderDisconnect); socket.on("blender_disconnect", handleBlenderDisconnect);
socket.on("csv_data", handleCsvData); socket.on("csv_data", handleCsvData);
socket.on("default_data", handleDefaultData);
} }
// Function to handle 'blender_connect' event // Function to handle 'blender_connect' event
@@ -41,7 +48,7 @@ function handleBlenderDisconnect(blenderId) {
function handleCsvData(data) { function handleCsvData(data) {
const blenderId = data["blenderId"]; const blenderId = data["blenderId"];
const csvData = data["data"]["csv_data"]; const csvData = data["data"]["csv_data"];
const filename = data["data"]["IFC_File"]; const filename = data["data"]["ifc_file"];
console.log(data); console.log(data);
if (connectedClients.hasOwnProperty(blenderId)) { if (connectedClients.hasOwnProperty(blenderId)) {
@@ -65,6 +72,13 @@ function handleCsvData(data) {
} }
} }
function handleDefaultData(data) {
const blenderId = data["blenderId"];
const isDirty = data["data"]["is_dirty"];
showWarning(blenderId, isDirty);
console.log(data);
}
// Function to add a new table with data and filename // Function to add a new table with data and filename
function addTableElement(blenderId, csvData, filename) { function addTableElement(blenderId, csvData, filename) {
// store headers of the csv data // store headers of the csv data
@@ -82,11 +96,19 @@ function addTableElement(blenderId, csvData, filename) {
.text(filename) .text(filename)
.css("margin-bottom", "10px"); .css("margin-bottom", "10px");
const warning = $("<div></div>")
.attr("id", "warning-" + blenderId)
.html(
"&#9888; Warning: This table may contain outdated data due to recent changes in Blender."
)
.addClass("warning");
const tableDiv = $("<div></div>") const tableDiv = $("<div></div>")
.addClass("csv-table") .addClass("csv-table")
.attr("id", "table-" + blenderId); .attr("id", "table-" + blenderId);
tableContainer.append(tableTitle); tableContainer.append(tableTitle);
tableContainer.append(warning);
tableContainer.append(tableDiv); tableContainer.append(tableDiv);
$("#container").append(tableContainer); $("#container").append(tableContainer);
@@ -199,6 +221,7 @@ function updateTableElement(blenderId, csvData, filename) {
connectedClients[blenderId].headers = newHeaders; connectedClients[blenderId].headers = newHeaders;
} }
$("#title-" + blenderId).text(filename); $("#title-" + blenderId).text(filename);
$("#warning-" + blenderId).css("display", "none");
} }
} }
@@ -207,6 +230,10 @@ function removeTableElement(blenderId) {
$("#container-" + blenderId).remove(); $("#container-" + blenderId).remove();
} }
function showWarning(blenderId, isDirty) {
$("#warning-" + blenderId).css("display", "block");
}
// Utility function to compare two csv header // Utility function to compare two csv header
function compareHeaders(headers1, headers2) { function compareHeaders(headers1, headers2) {
if (headers1.length !== headers2.length) { if (headers1.length !== headers2.length) {
@@ -219,3 +246,31 @@ function compareHeaders(headers1, headers2) {
} }
return true; return true;
} }
function setTheme(theme) {
var stylesheet = $("#tabulator-stylesheet");
if (theme === "light") {
stylesheet.attr(
"href",
"https://unpkg.com/tabulator-tables/dist/css/tabulator_site.min.css"
);
$("html").removeClass("dark").addClass("light");
$("#toggle-theme").html('<i class="fas fa-sun"></i>');
} else {
stylesheet.attr(
"href",
"https://unpkg.com/tabulator-tables/dist/css/tabulator_site_dark.min.css"
);
$("html").removeClass("light").addClass("dark");
$("#toggle-theme").html('<i class="fas fa-moon"></i>');
}
localStorage.setItem("theme", theme);
}
function toggleTheme() {
if ($("html").hasClass("dark")) {
setTheme("light");
} else {
setTheme("dark");
}
}
@@ -1,11 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" class="dark">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Client</title> <title>Web Client</title>
<link rel="stylesheet" type="text/css" href="/jsgantt/jsgantt.css" /> <link rel="stylesheet" type="text/css" href="/jsgantt/jsgantt.css" />
<link rel="stylesheet" href="/static/css/gantt.css" /> <link rel="stylesheet" href="/static/css/gantt.css" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
/>
<script <script
type="text/javascript" type="text/javascript"
src="https://code.jquery.com/jquery-3.6.0.min.js" src="https://code.jquery.com/jquery-3.6.0.min.js"
@@ -22,12 +26,18 @@
</head> </head>
<body> <body>
<nav class="no-print"> <nav class="no-print">
<img src="https://blenderbim.org/assets/images/blender/blender-logo.png" alt="Logo" class="logo" /> <img
src="https://blenderbim.org/assets/images/blender/blender-logo.png"
alt="Logo"
class="logo"
/>
<ul> <ul>
<li><a href="/">IFC Data</a></li> <li><a href="/">IFC Data</a></li>
<li><a href="/gantt" class="active">Gantt Chart</a></li> <li><a href="/gantt" class="active">Gantt Chart</a></li>
</ul> </ul>
<div class="bottom-bar"></div> <button id="toggle-theme" onclick="toggleTheme()">
<i class="fas fa-moon"></i>
</button>
</nav> </nav>
<div id="container"></div> <div id="container"></div>
</body> </body>
@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" class="dark">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -7,8 +7,13 @@
<link rel="stylesheet" href="/static/css/index.css" /> <link rel="stylesheet" href="/static/css/index.css" />
<link <link
rel="stylesheet" rel="stylesheet"
id="tabulator-stylesheet"
href="https://unpkg.com/tabulator-tables/dist/css/tabulator_site_dark.min.css" href="https://unpkg.com/tabulator-tables/dist/css/tabulator_site_dark.min.css"
/> />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
/>
<script <script
type="text/javascript" type="text/javascript"
src="https://code.jquery.com/jquery-3.6.0.min.js" src="https://code.jquery.com/jquery-3.6.0.min.js"
@@ -28,12 +33,18 @@
</head> </head>
<body> <body>
<nav> <nav>
<img src="https://blenderbim.org/assets/images/blender/blender-logo.png" alt="Logo" class="logo" /> <img
src="https://blenderbim.org/assets/images/blender/blender-logo.png"
alt="Logo"
class="logo"
/>
<ul> <ul>
<li><a href="/" class="active">IFC Data</a></li> <li><a href="/" class="active">IFC Data</a></li>
<li><a href="/gantt">Gantt Chart</a></li> <li><a href="/gantt">Gantt Chart</a></li>
</ul> </ul>
<div class="bottom-bar"></div> <button id="toggle-theme" onclick="toggleTheme()">
<i class="fas fa-moon"></i>
</button>
</nav> </nav>
<div id="container"></div> <div id="container"></div>
</body> </body>
@@ -167,21 +167,6 @@ class IfcExporter:
def get_application_name(self) -> str: def get_application_name(self) -> str:
return "BlenderBIM" return "BlenderBIM"
def get_application_version(self) -> str:
version = ".".join(
[
str(x)
for x in [
addon.bl_info.get("version", (-1, -1, -1))
for addon in addon_utils.modules()
if addon.bl_info["name"] == "BlenderBIM"
][0]
]
)
if commit_hash := tool.Blender.get_last_commit_hash():
version += f"-{commit_hash}"
return version
class IfcExportSettings: class IfcExportSettings:
def __init__(self): def __init__(self):
+1 -14
View File
@@ -222,7 +222,7 @@ def redo_post(scene):
def get_application(ifc: ifcopenshell.file) -> ifcopenshell.entity_instance: def get_application(ifc: ifcopenshell.file) -> ifcopenshell.entity_instance:
# TODO: cache this for even faster application retrieval. It honestly makes a difference on long scripts. # TODO: cache this for even faster application retrieval. It honestly makes a difference on long scripts.
version = get_application_version() version = tool.Blender.get_blenderbim_version()
for element in ifc.by_type("IfcApplication"): for element in ifc.by_type("IfcApplication"):
if element.ApplicationIdentifier == "BlenderBIM" and element.Version == version: if element.ApplicationIdentifier == "BlenderBIM" and element.Version == version:
return element return element
@@ -248,19 +248,6 @@ def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None
return pao return pao
def get_application_version() -> str:
return ".".join(
[
str(x)
for x in [
addon.bl_info.get("version", (-1, -1, -1))
for addon in addon_utils.modules()
if addon.bl_info["name"] == "BlenderBIM"
][0]
]
)
def viewport_shading_changed_callback(area): def viewport_shading_changed_callback(area):
shading = area.spaces.active.shading.type shading = area.spaces.active.shading.type
if shading == "RENDERED": if shading == "RENDERED":
@@ -26,6 +26,7 @@ import requests
import tempfile import tempfile
import datetime import datetime
import addon_utils import addon_utils
import blenderbim.tool as tool
class AuginLogin(bpy.types.Operator): class AuginLogin(bpy.types.Operator):
@@ -77,16 +78,7 @@ class AuginCreateNewModel(bpy.types.Operator):
props.project_filename = "BBM{}{}".format( props.project_filename = "BBM{}{}".format(
datetime.datetime.now().strftime("%Y%m%d%H%M%S"), "".join(random.choice(chars) for _ in range(6)) datetime.datetime.now().strftime("%Y%m%d%H%M%S"), "".join(random.choice(chars) for _ in range(6))
) )
addon_version = ".".join( addon_version = tool.Blender.get_blenderbim_version()
[
str(x)
for x in [
addon.bl_info.get("version", (-1, -1, -1))
for addon in addon_utils.modules()
if addon.bl_info["name"] == "BlenderBIM"
][0]
]
)
payload = { payload = {
"user_token": props.token, "user_token": props.token,
@@ -254,9 +254,10 @@ class ExportIfcCsv(bpy.types.Operator):
schedule_creator = scheduler.Scheduler() schedule_creator = scheduler.Scheduler()
schedule_creator.schedule(self.filepath, tool.Drawing.get_path_with_ext(self.filepath, "svg")) schedule_creator.schedule(self.filepath, tool.Drawing.get_path_with_ext(self.filepath, "svg"))
if props.format == "web": if props.format == "web":
tool.Web.send_webui_data( if not context.scene.WebProperties.is_running:
extra_data=ifc_csv.dataframe.to_csv(index=False), extra_data_key="csv_data", event="csv_data" bpy.ops.bim.connect_websocket_server()
) tool.Web.send_webui_data(data=ifc_csv.dataframe.to_csv(index=False), data_key="csv_data", event="csv_data")
self.report({"INFO"}, f"Data is exported to {props.format.upper()}.") self.report({"INFO"}, f"Data is exported to {props.format.upper()}.")
return {"FINISHED"} return {"FINISHED"}
@@ -36,6 +36,7 @@ classes = (
operator.ProfileImportIFC, operator.ProfileImportIFC,
operator.PurgeHdf5Cache, operator.PurgeHdf5Cache,
operator.PurgeUnusedElementsByClass, operator.PurgeUnusedElementsByClass,
operator.RestartBlender,
operator.RewindInspector, operator.RewindInspector,
operator.SelectExpressFile, operator.SelectExpressFile,
operator.SelectHighPolygonMeshes, operator.SelectHighPolygonMeshes,
@@ -770,3 +770,15 @@ class ToggleDetailedIOSLogs(bpy.types.Operator):
wrapper.turn_off_detailed_logging() wrapper.turn_off_detailed_logging()
self.report({"INFO"}, "Detailed IfcOpenShell logs turned off.") self.report({"INFO"}, "Detailed IfcOpenShell logs turned off.")
return {"FINISHED"} return {"FINISHED"}
class RestartBlender(bpy.types.Operator):
bl_idname = "bim.restart_blender"
bl_label = "Restart Blender"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
import os
path = bpy.app.binary_path
os.execv(path, sys.argv)
@@ -78,6 +78,9 @@ class BIM_PT_debug(Panel):
row = layout.row() row = layout.row()
row.operator("bim.toggle_detailed_ios_logs") row.operator("bim.toggle_detailed_ios_logs")
row = layout.row()
row.operator("bim.restart_blender")
row = layout.split(factor=0.5, align=True) row = layout.split(factor=0.5, align=True)
row.operator("bim.create_shape_from_step_id").should_include_curves = False row.operator("bim.create_shape_from_step_id").should_include_curves = False
row.operator("bim.create_shape_from_step_id", text="", icon="IPO_ELASTIC").should_include_curves = True row.operator("bim.create_shape_from_step_id", text="", icon="IPO_ELASTIC").should_include_curves = True
@@ -26,6 +26,7 @@ from blenderbim.bim.ifc import IfcStore
class OpenPieClass(bpy.types.Operator): class OpenPieClass(bpy.types.Operator):
bl_idname = "bim.open_pie_class" bl_idname = "bim.open_pie_class"
bl_label = "Open Pie Class" bl_label = "Open Pie Class"
bl_description = "Assign the IFC Class to the selected objects"
def execute(self, context): def execute(self, context):
bpy.ops.wm.call_menu_pie(name="VIEW3D_MT_PIE_bim_class") bpy.ops.wm.call_menu_pie(name="VIEW3D_MT_PIE_bim_class")
@@ -166,7 +166,7 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_class" bl_idname = "bim.assign_class"
bl_label = "Assign IFC Class" bl_label = "Assign IFC Class"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_description = "Assign the IFC Class to the selected object" bl_description = "Assign the IFC Class to the selected objects"
obj: bpy.props.StringProperty() obj: bpy.props.StringProperty()
ifc_class: bpy.props.StringProperty() ifc_class: bpy.props.StringProperty()
predefined_type: bpy.props.StringProperty() predefined_type: bpy.props.StringProperty()
@@ -175,6 +175,13 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
should_add_representation: bpy.props.BoolProperty(default=True) should_add_representation: bpy.props.BoolProperty(default=True)
ifc_representation_class: bpy.props.StringProperty() ifc_representation_class: bpy.props.StringProperty()
@classmethod
def poll(cls, context):
if not context.active_object and not context.selected_objects:
cls.poll_message_set("No object selected.")
return False
return True
def _execute(self, context): def _execute(self, context):
props = context.scene.BIMRootProperties props = context.scene.BIMRootProperties
objects = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects or [context.active_object] objects = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects or [context.active_object]
@@ -18,6 +18,7 @@
import bpy import bpy
import blenderbim.tool as tool import blenderbim.tool as tool
import os
def refresh(): def refresh():
@@ -31,10 +32,16 @@ class WebData:
@classmethod @classmethod
def load(cls): def load(cls):
cls.data = { cls.data = {
"IFC_File": cls.ifc_file_name(), "ifc_file": cls.get_ifc_file_name(),
"is_dirty": cls.get_is_dirty(),
} }
cls.is_loaded = True cls.is_loaded = True
@classmethod @classmethod
def ifc_file_name(cls): def get_ifc_file_name(cls):
return bpy.context.scene.BIMProperties.ifc_file filename = os.path.basename(bpy.context.scene.BIMProperties.ifc_file)
return filename
@classmethod
def get_is_dirty(cls):
return bpy.context.scene.BIMProperties.is_dirty
-1
View File
@@ -18,7 +18,6 @@
import os import os
import bpy import bpy
import addon_utils
import platform import platform
from pathlib import Path from pathlib import Path
from bpy.types import Panel from bpy.types import Panel
+11 -13
View File
@@ -26,8 +26,8 @@ import ifcopenshell.util.element
import blenderbim.core.tool import blenderbim.core.tool
import blenderbim.tool as tool import blenderbim.tool as tool
import blenderbim.bim import blenderbim.bim
import addon_utils
import types import types
import importlib
from mathutils import Vector from mathutils import Vector
from pathlib import Path from pathlib import Path
from blenderbim.bim.ifc import IFC_CONNECTED_TYPE from blenderbim.bim.ifc import IFC_CONNECTED_TYPE
@@ -920,7 +920,8 @@ class Blender(blenderbim.core.tool.Blender):
@classmethod @classmethod
def get_last_commit_hash(cls) -> Union[str, None]: def get_last_commit_hash(cls) -> Union[str, None]:
"""Get 8 symbols of last commit hash if it's present or return None otherwise.""" """Get 8 symbols of last commit hash if it's present or return None otherwise."""
commit_hash = blenderbim.bim.last_commit_hash bbim = cls.get_bbim_extension_package()
commit_hash = bbim.last_commit_hash
# Commit hash is unset - user is using __init__ from repo # Commit hash is unset - user is using __init__ from repo
# without setting up git repository. # without setting up git repository.
@@ -930,17 +931,9 @@ class Blender(blenderbim.core.tool.Blender):
return commit_hash[:7] return commit_hash[:7]
@classmethod @classmethod
def get_blenderbim_version(cls): def get_blenderbim_version(cls) -> str:
version = ".".join( bbim = cls.get_bbim_extension_package()
[ version = bbim.bbim_semver["version"]
str(x)
for x in [
addon.bl_info.get("version", (-1, -1, -1))
for addon in addon_utils.modules()
if addon.bl_info["name"] == "BlenderBIM"
][0]
]
)
if commit_hash := cls.get_last_commit_hash(): if commit_hash := cls.get_last_commit_hash():
version += f"-{commit_hash}" version += f"-{commit_hash}"
return version return version
@@ -1114,6 +1107,11 @@ class Blender(blenderbim.core.tool.Blender):
return package_name return package_name
return "blenderbim" return "blenderbim"
@classmethod
def get_bbim_extension_package(cls) -> types.ModuleType:
name = cls.get_blender_addon_package_name()
return importlib.import_module(name)
@classmethod @classmethod
def get_addon_preferences(cls) -> blenderbim.bim.ui.BIM_ADDON_preferences: def get_addon_preferences(cls) -> blenderbim.bim.ui.BIM_ADDON_preferences:
blender_package_name = cls.get_blender_addon_package_name() blender_package_name = cls.get_blender_addon_package_name()
+5 -3
View File
@@ -1546,14 +1546,16 @@ class Sequence(blenderbim.core.tool.Sequence):
) -> None: ) -> None:
if bpy.context.scene.WebProperties.is_connected: if bpy.context.scene.WebProperties.is_connected:
gantt_data = {"tasks": task_json, "work_schedule": work_schedule.get_info(recursive=True)} gantt_data = {"tasks": task_json, "work_schedule": work_schedule.get_info(recursive=True)}
tool.Web.send_webui_data(extra_data=gantt_data, extra_data_key="gantt_data", event="gantt_data") tool.Web.send_webui_data(data=gantt_data, data_key="gantt_data", event="gantt_data")
return return
with open(os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.html"), "w") as f: with open(os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.html"), "w") as f:
with open(os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.mustache"), "r") as t: with open(os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.mustache"), "r") as t:
task_b64 = base64.b64encode(bytes(json.dumps(task_json), "utf-8")).decode("utf-8") task_b64 = base64.b64encode(bytes(json.dumps(task_json), "utf-8")).decode("utf-8")
f.write( f.write(
pystache.render(t.read(), {"json_data": task_b64, "data": json.dumps(work_schedule.get_info(recursive=True))}) pystache.render(
t.read(), {"json_data": task_b64, "data": json.dumps(work_schedule.get_info(recursive=True))}
)
) )
webbrowser.open("file://" + os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.html")) webbrowser.open("file://" + os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.html"))
+186 -39
View File
@@ -21,32 +21,47 @@ from blenderbim.bim.module.web.data import WebData
import blenderbim.core.tool import blenderbim.core.tool
import blenderbim.tool as tool import blenderbim.tool as tool
import ifcopenshell.api.sequence import ifcopenshell.api.sequence
from typing import Any, Dict, Optional
import socket import socket
import sys
import os import os
import errno
import subprocess import subprocess
import webbrowser import webbrowser
import asyncio import asyncio
import socketio import socketio
import threading import threading
import queue import queue
import json
from time import sleep from time import sleep
from pathlib import Path
sio = None sio = None
ws_process = None ws_process = None
ws_thread = None ws_thread = None
web_operator_queue = queue.Queue() web_operator_queue = queue.Queue()
RECONNECTION_ATTEMPTS = 3
RECONNECTION_DELAY = 2
IFC_TASK_ATTRIBUTE_MAP = { IFC_TASK_ATTRIBUTE_MAP = {
"pStart": "ScheduleStart", "pStart": "ScheduleStart",
"pEnd": "ScheduleFinish", "pEnd": "ScheduleFinish",
"pName": "Name", "pName": "Name",
"pRes": "",
} }
class Web(blenderbim.core.tool.Web): class Web(blenderbim.core.tool.Web):
@classmethod @classmethod
def generate_port_number(cls): def generate_port_number(cls) -> int:
"""
Generate a free port number.
This method creates a temporary socket to bind to a free port.
It then retrieves the port number, and returns it.
Returns:
int: The port number that was generated.
"""
print("Generating port number") print("Generating port number")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("localhost", 0)) # Bind to a free port s.bind(("localhost", 0)) # Bind to a free port
@@ -55,35 +70,104 @@ class Web(blenderbim.core.tool.Web):
return port return port
@classmethod @classmethod
def is_port_available(cls, port): def is_port_available(cls, port: int) -> bool:
"""
Attempts to connect to the specified port on localhost.
If the connection is refused, the port is available for use; otherwise, it is in use.
Args:
- port (int): The port number to check.
Returns:
bool: True if the port is available, False if it is in use.
"""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# connect_ex returns 0 if the connection succeeds # connect_ex returns errno.SUCCESS (0) if the connection succeeds
# indicating the port is in use # otherwise returns errno.ECONNREFUSED (111 or 10061) if no server is listening
# otherwise returns 10061 if no server is listening return s.connect_ex(("localhost", port)) == errno.ECONNREFUSED
# indicating the port is available for use
return s.connect_ex(("localhost", port)) == 10061
@classmethod @classmethod
def start_websocket_server(cls, port): def start_websocket_server(cls, port: int) -> None:
"""
Starts a WebSocket server on the specified port.
This method sets up the environment, locates paths, and starts
the WebSocket server process. It also handles the creation and updating of a PID file to keep track
of running server instances.
Args:
- port (int): The port number on which to start the WebSocket server.
"""
import addon_utils
global ws_process global ws_process
webui_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "webui") webui_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "webui")
ws_path = os.path.join(webui_path, "sioserver.py") ws_path = os.path.join(webui_path, "sioserver.py")
ws_process = subprocess.Popen(["python", ws_path, "--p", str(port), "--host", "127.0.0.1"], cwd=webui_path) py_version = sys.version_info
cls.open_web_browser(port)
if bpy.app.version >= (4, 2, 0):
blenderbim_lib_path = (
Path(bpy.utils.user_resource("EXTENSIONS"))
/ ".local"
/ "lib"
/ f"python{py_version.major}.{py_version.minor}"
/ "site-packages"
)
else:
addon = [a for a in addon_utils.modules() if a.bl_info["name"] == "BlenderBIM"][0]
blenderbim_path = os.path.dirname(addon.__file__)
blenderbim_lib_path = os.path.join(blenderbim_path, "libs", "site", "packages")
env = os.environ.copy()
env["blenderbim_lib_path"] = str(blenderbim_lib_path)
ws_process = subprocess.Popen(
[sys.executable, ws_path, "--p", str(port), "--host", "127.0.0.1"],
cwd=webui_path,
env=env,
)
pid_file = os.path.join(webui_path, "running_pid.json")
if os.path.exists(pid_file):
with open(pid_file, "r") as f:
pids = json.load(f)
else:
pids = {}
pids[str(ws_process.pid)] = port
with open(pid_file, "w") as f:
json.dump(pids, f, indent=4)
cls.set_is_running(True) cls.set_is_running(True)
@classmethod @classmethod
def connect_websocket_server(cls, port): def connect_websocket_server(cls, port: int) -> None:
"""
Connect to a WebSocket server on the specified port.
This method sets up an asynchronous Socket.IO client with
reconnection attempts, starts an asyncio thread, connects to the WebSocket server, and sets the connection status.
Args:
- port (int): The port number to connect to the WebSocket server.
"""
global ws_thread, sio global ws_thread, sio
if bpy.context.scene.WebProperties.is_connected: if bpy.context.scene.WebProperties.is_connected:
print(f"Already connected to websocket server on port: {port}") print(f"Already connected to websocket server on port: {port}")
return return
sio = socketio.AsyncClient(reconnection=True, reconnection_attempts=3, reconnection_delay=2, logger=True) sio = socketio.AsyncClient(
reconnection=True,
reconnection_attempts=RECONNECTION_ATTEMPTS,
reconnection_delay=RECONNECTION_DELAY,
logger=True,
)
ws_thread = AsyncioThread() ws_thread = AsyncioThread()
ws_thread.daemon = True ws_thread.daemon = True
@@ -93,10 +177,15 @@ class Web(blenderbim.core.tool.Web):
ws_thread.run_coro(cls.sio_connect(ws_url)) ws_thread.run_coro(cls.sio_connect(ws_url))
cls.set_is_connected(True) cls.set_is_connected(True)
bpy.app.timers.register(cls.check_operator_queue) bpy.app.timers.register(cls.check_operator_queue)
cls.send_webui_data()
@classmethod @classmethod
def disconnect_websocket_server(cls): def disconnect_websocket_server(cls) -> None:
"""
Disconnects the WebSocket server and stops the associated thread.
This method is responsible for disconnecting the WebSocket server, stopping the asyncio thread,
and resetting the global variables related to the WebSocket connection.
"""
global ws_thread, sio global ws_thread, sio
ws_thread.run_coro(cls.sio_disconnect()) ws_thread.run_coro(cls.sio_disconnect())
ws_thread.stop() ws_thread.stop()
@@ -105,7 +194,13 @@ class Web(blenderbim.core.tool.Web):
cls.set_is_connected(False) cls.set_is_connected(False)
@classmethod @classmethod
def kill_websocket_server(cls): def kill_websocket_server(cls) -> None:
"""
Terminate the currently running WebSocket server.
This method checks if there is an active WebSocket server process. If so, it disconnects it (if connected),
removes its PID from the PID file, terminates the process, and updates the server's running status.
"""
global ws_process global ws_process
if ws_process is None: if ws_process is None:
@@ -115,36 +210,62 @@ class Web(blenderbim.core.tool.Web):
if bpy.context.scene.WebProperties.is_connected: if bpy.context.scene.WebProperties.is_connected:
cls.disconnect_websocket_server() cls.disconnect_websocket_server()
sleep(0.5) # sleep(0.5)
webui_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "webui")
pid_file = os.path.join(webui_path, "running_pid.json")
with open(pid_file, "r") as f:
pids = json.load(f)
if str(ws_process.pid) in pids:
del pids[str(ws_process.pid)]
# Write the updated PIDs back to the file
with open(pid_file, "w") as f:
json.dump(pids, f, indent=4)
ws_process.terminate() ws_process.terminate()
ws_process.wait() ws_process.wait()
ws_process = None ws_process = None
cls.set_is_running(False) cls.set_is_running(False)
print("Websocket server terminated successfully") print("Websocket server terminated successfully")
@classmethod @classmethod
def send_webui_data( def send_webui_data(
cls, extra_data=None, extra_data_key="data", event="data", namespace="/blender", use_web_data=True cls,
): data: Optional[Any] = None,
data_key: str = "data",
event: str = "data",
namespace: str = "/blender",
use_web_data: bool = True,
) -> None:
"""
Sends data to the Web UI via Websocket connection.
Args:
- data (Optional[Any]): The data to send. If None, just sends data from WebData.
- data_key (str): The key under which to store the data in the payload. Defaults to "data".
- event (str): The WebSocket event to emit. Defaults to "data".
- namespace (str): The namespace for the WebSocket event. Defaults to "/blender".
- use_web_data (bool): Whether to use data from WebData. Defaults to True.
"""
global ws_thread global ws_thread
data = {} payload = {}
if use_web_data: if use_web_data:
if not WebData.is_loaded: if not WebData.is_loaded:
WebData.load() WebData.load()
data = WebData.data payload = WebData.data
if extra_data is not None: if data is not None:
data[extra_data_key] = extra_data payload[data_key] = data
if ws_thread is not None and bpy.context.scene.WebProperties.is_connected: if ws_thread is not None and bpy.context.scene.WebProperties.is_connected:
ws_thread.run_coro(cls.sio_send(data, event, namespace)) ws_thread.run_coro(cls.sio_send(payload, event, namespace))
@classmethod @classmethod
def check_operator_queue(cls): def check_operator_queue(cls) -> None | float:
if not bpy.context.scene.WebProperties.is_connected: if not bpy.context.scene.WebProperties.is_connected:
with web_operator_queue.mutex: with web_operator_queue.mutex:
web_operator_queue.queue.clear() web_operator_queue.queue.clear()
@@ -161,7 +282,7 @@ class Web(blenderbim.core.tool.Web):
return 1.0 return 1.0
@classmethod @classmethod
def handle_csv_operator(cls, operator_data): def handle_csv_operator(cls, operator_data: dict) -> None:
if operator_data["type"] == "selection": if operator_data["type"] == "selection":
bpy.ops.object.select_all(action="DESELECT") bpy.ops.object.select_all(action="DESELECT")
guid = operator_data["globalId"] guid = operator_data["globalId"]
@@ -171,7 +292,7 @@ class Web(blenderbim.core.tool.Web):
obj.select_set(True) obj.select_set(True)
@classmethod @classmethod
def handle_gantt_operator(cls, operator_data): def handle_gantt_operator(cls, operator_data: dict) -> None:
ifc_file = tool.Ifc.get() ifc_file = tool.Ifc.get()
if operator_data["type"] == "editTask": if operator_data["type"] == "editTask":
task_id = int(operator_data["taskId"]) task_id = int(operator_data["taskId"])
@@ -200,55 +321,81 @@ class Web(blenderbim.core.tool.Web):
work_schedule = ifc_file.by_id(operator_data["workScheduleId"]) work_schedule = ifc_file.by_id(operator_data["workScheduleId"])
task_json = tool.Sequence.create_tasks_json(work_schedule) task_json = tool.Sequence.create_tasks_json(work_schedule)
gantt_data = {"tasks": task_json, "work_schedule": work_schedule.get_info(recursive=True)} gantt_data = {"tasks": task_json, "work_schedule": work_schedule.get_info(recursive=True)}
cls.send_webui_data(extra_data=gantt_data, extra_data_key="gantt_data", event="gantt_data") cls.send_webui_data(data=gantt_data, data_key="gantt_data", event="gantt_data")
@classmethod @classmethod
def open_web_browser(cls, port): def open_web_browser(cls, port: int) -> None:
webbrowser.open(f"http://127.0.0.1:{port}/") webbrowser.open(f"http://127.0.0.1:{port}/")
@classmethod @classmethod
async def sio_connect(cls, url): async def sio_connect(cls, url: str) -> None:
await sio.connect(url, transports=["websocket"], namespaces="/blender") await sio.connect(url, transports=["websocket"], namespaces="/blender")
sio.on("web_operator", cls.sio_listen_web_operator, namespace="/blender") sio.on("web_operator", cls.sio_listen_web_operator, namespace="/blender")
@classmethod @classmethod
async def sio_disconnect(cls): async def sio_disconnect(cls) -> None:
await sio.disconnect() await sio.disconnect()
@classmethod @classmethod
async def sio_send(cls, data, event="data", namespace="/blender"): async def sio_send(cls, data: Any, event: str = "data", namespace: str = "/blender") -> None:
await sio.emit(event, data, namespace=namespace) await sio.emit(event, data, namespace=namespace)
@classmethod @classmethod
async def sio_listen_web_operator(cls, data): async def sio_listen_web_operator(cls, data: dict) -> None:
try: try:
web_operator_queue.put_nowait(data) web_operator_queue.put_nowait(data)
except queue.Full: except queue.Full:
pass pass
@classmethod @classmethod
def set_is_running(cls, is_running): def set_is_running(cls, is_running: bool) -> None:
bpy.context.scene.WebProperties.is_running = is_running bpy.context.scene.WebProperties.is_running = is_running
@classmethod @classmethod
def set_is_connected(cls, is_connected): def set_is_connected(cls, is_connected: bool) -> None:
bpy.context.scene.WebProperties.is_connected = is_connected bpy.context.scene.WebProperties.is_connected = is_connected
class AsyncioThread(threading.Thread): class AsyncioThread(threading.Thread):
def __init__(self, *args, loop=None, **kwargs): def __init__(self, *args, loop=None, **kwargs):
"""
Initialize an instance of AsyncioThread.
This class represents a thread that runs an asyncio event loop. It is used to handle asynchronous tasks
in a separate thread from the main thread.
Args:
- *args: Variable length argument list. These arguments are passed to the superclass constructor.
- loop: An existing asyncio event loop. If None, a new event loop is created.
- **kwargs: Arbitrary keyword arguments. These arguments are passed to the superclass constructor.
"""
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.loop = loop or asyncio.new_event_loop() self.loop = loop or asyncio.new_event_loop()
self.running = False self.running = False
def run(self): def run(self):
"""
Start the asyncio event loop and mark the thread as running.
"""
self.running = True self.running = True
self.loop.run_forever() self.loop.run_forever()
def run_coro(self, coro): def run_coro(self, coro) -> Any:
"""
Run a coroutine in the asyncio event loop from a separate thread.
Args:
- coro: The coroutine to be run.
Returns:
The result of the coroutine.
"""
return asyncio.run_coroutine_threadsafe(coro, loop=self.loop).result() return asyncio.run_coroutine_threadsafe(coro, loop=self.loop).result()
def stop(self): def stop(self) -> None:
"""
Stop the asyncio event loop and join the thread.
"""
self.loop.call_soon_threadsafe(self.loop.stop) self.loop.call_soon_threadsafe(self.loop.stop)
self.join() self.join()
self.running = False self.running = False
+8 -129
View File
@@ -22,8 +22,8 @@ You will need to choose which build to download.
- If you are on Blender >=4.1, choose py311 - If you are on Blender >=4.1, choose py311
- If you are on Blender >=3.1 and <=4.0, choose py310 - If you are on Blender >=3.1 and <=4.0, choose py310
- If you are on Blender >=2.93 and <3.1, choose py39 - If you are on Blender >=2.93 and <3.1, choose py39
- Choose ``linux``, ``macos`` (Apple Intel), ``macosm1`` (Apple Silicon), or - Choose ``linux-x64``, ``macos-x64`` (Apple Intel), ``macos-arm64`` (Apple Silicon), or
``win`` depending on your operating system ``windows-x64`` depending on your operating system
For users who don't follow the `VFX Platform <https://vfxplatform.com/>`_ For users who don't follow the `VFX Platform <https://vfxplatform.com/>`_
standard, we also provide py312 builds. standard, we also provide py312 builds.
@@ -75,138 +75,17 @@ restart Blender to see changes).
For Linux or Mac: For Linux or Mac:
.. code-block:: bash .. literalinclude:: ../../scripts/installation/dev_environment.sh
:language: bash
git clone https://github.com/IfcOpenShell/IfcOpenShell.git :caption: dev_environment.sh
cd IfcOpenShell
# path to BlenderBIM addon
# default path on Mac: "/Users/$USER/Library/Application Support/Blender/X.X/scripts/addons/blenderbim"
# default path on Linux: "$HOME/.config/blender/X.X/"
BLENDER_ADDON_PATH="/path/to/blender/X.XX/scripts/addons/blenderbim"
# Remove the Blender add-on Python code
rm -r $BLENDER_ADDON_PATH/core/
rm -r $BLENDER_ADDON_PATH/tool/
rm -r $BLENDER_ADDON_PATH/bim/
# Replace them with links to the Git repository
ln -s $PWD/src/blenderbim/blenderbim/core $BLENDER_ADDON_PATH/core
ln -s $PWD/src/blenderbim/blenderbim/tool $BLENDER_ADDON_PATH/tool
ln -s $PWD/src/blenderbim/blenderbim/bim $BLENDER_ADDON_PATH/bim
# Copy over compiled IfcOpenShell files
cp $BLENDER_ADDON_PATH/libs/site/packages/ifcopenshell/*_wrapper* $PWD/src/ifcopenshell-python/ifcopenshell/
# Remove the IfcOpenShell dependency
rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifcopenshell
# Replace them with links to the Git repository
ln -s $PWD/src/ifcopenshell-python/ifcopenshell $BLENDER_ADDON_PATH/libs/site/packages/ifcopenshell
# Remove and link other IfcOpenShell utilities
rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifccsv.py
rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifcdiff.py
rm -r $BLENDER_ADDON_PATH/libs/site/packages/bsdd.py
rm -r $BLENDER_ADDON_PATH/libs/site/packages/bcf
rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifc4d
rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifc5d
rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifccityjson
rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifcclash
rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifcpatch
rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifctester
rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifcfm
rm -r $BLENDER_ADDON_PATH/libs/Desktop
ln -s $PWD/src/ifccsv/ifccsv.py $BLENDER_ADDON_PATH/libs/site/packages/ifccsv.py
ln -s $PWD/src/ifcdiff/ifcdiff.py $BLENDER_ADDON_PATH/libs/site/packages/ifcdiff.py
ln -s $PWD/src/bsdd/bsdd.py $BLENDER_ADDON_PATH/libs/site/packages/bsdd.py
ln -s $PWD/src/bcf/src/bcf $BLENDER_ADDON_PATH/libs/site/packages/bcf
ln -s $PWD/src/ifc4d/ifc4d $BLENDER_ADDON_PATH/libs/site/packages/ifc4d
ln -s $PWD/src/ifc5d/ifc5d $BLENDER_ADDON_PATH/libs/site/packages/ifc5d
ln -s $PWD/src/ifccityjson/ifccityjson $BLENDER_ADDON_PATH/libs/site/packages/ifccityjson
ln -s $PWD/src/ifcclash/ifcclash $BLENDER_ADDON_PATH/libs/site/packages/ifcclash
ln -s $PWD/src/ifcpatch/ifcpatch $BLENDER_ADDON_PATH/libs/site/packages/ifcpatch
ln -s $PWD/src/ifctester/ifctester $BLENDER_ADDON_PATH/libs/site/packages/ifctester
ln -s $PWD/src/ifcfm/ifcfm $BLENDER_ADDON_PATH/libs/site/packages/ifcfm
ln -s $PWD/src/blenderbim/blenderbim/libs/desktop $BLENDER_ADDON_PATH/libs/Desktop
# Manually download some third party dependencies
cd $BLENDER_ADDON_PATH/bim/data/gantt
wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.js
wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.css
cd $BLENDER_ADDON_PATH/bim/schema
wget https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl
Or, if you're on Windows, you can use the batch script below. You need to run Or, if you're on Windows, you can use the batch script below. You need to run
it as an administrator. Before running it follow the instructions descibed it as an administrator. Before running it follow the instructions descibed
in the `rem` tags. in the `rem` tags.
.. code-block:: bat .. literalinclude:: ../../scripts/installation/dev_environment.bat
:language: bat
@echo off :caption: dev_environment.bat
rem SETUP BLENDER-BIM LIVE DEVELOPMENT ENVIRONMENT
rem Setup blenderbim addon location below (probably just need to change "x.x" for your Blender version).
rem Put the script to the folder where IfcOpenShell git repository is located
rem (script will try to clone IfcOpenShell.git if it's not present).
SET blenderbim=%appdata%\Blender Foundation\Blender\x.x\scripts\addons\blenderbim
git clone https://github.com/IfcOpenShell/IfcOpenShell.git
cd IfcOpenShell
echo Removing the Blender add-on Python code...
rd /S /Q "%blenderbim%\core\"
rd /S /Q "%blenderbim%\tool\"
rd /S /Q "%blenderbim%\bim\"
echo Replacing them with links to the Git repository...
mklink /D "%blenderbim%\core" "%cd%\src\blenderbim\blenderbim\core"
mklink /D "%blenderbim%\tool" "%cd%\src\blenderbim\blenderbim\tool"
mklink /D "%blenderbim%\bim" "%cd%\src\blenderbim\blenderbim\bim"
echo Copy over compiled IfcOpenShell files...
copy "%blenderbim%\libs\site\packages\ifcopenshell\*_wrapper*" "%cd%\src\ifcopenshell-python\ifcopenshell\"
echo Remove the IfcOpenShell dependency...
rd /S /Q "%blenderbim%\libs\site\packages\ifcopenshell"
echo Replace them with links to the Git repository...
mklink /D "%blenderbim%\libs\site\packages\ifcopenshell" "%cd%\src\ifcopenshell-python\ifcopenshell"
echo Remove and link other IfcOpenShell utilities...
del "%blenderbim%\libs\site\packages\ifccsv.py"
del "%blenderbim%\libs\site\packages\ifcdiff.py"
del "%blenderbim%\libs\site\packages\bsdd.py"
rd /S /Q "%blenderbim%\libs\site\packages\bcf"
rd /S /Q "%blenderbim%\libs\site\packages\ifc4d"
rd /S /Q "%blenderbim%\libs\site\packages\ifc5d"
rd /S /Q "%blenderbim%\libs\site\packages\ifccityjson"
rd /S /Q "%blenderbim%\libs\site\packages\ifcclash"
rd /S /Q "%blenderbim%\libs\site\packages\ifcpatch"
rd /S /Q "%blenderbim%\libs\site\packages\ifctester"
rd /S /Q "%blenderbim%\libs\site\packages\ifcfm"
rd /S /Q "%blenderbim%\libs\desktop"
mklink "%blenderbim%\libs\site\packages\ifccsv.py" "%cd%\src\ifccsv\ifccsv.py"
mklink "%blenderbim%\libs\site\packages\ifcdiff.py" "%cd%\src\ifcdiff\ifcdiff.py"
mklink "%blenderbim%\libs\site\packages\bsdd.py" "%cd%\src\bsdd\bsdd.py"
mklink /D "%blenderbim%\libs\site\packages\bcf" "%cd%\src\bcf\src\bcf"
mklink /D "%blenderbim%\libs\site\packages\ifc4d" "%cd%\src\ifc4d\ifc4d"
mklink /D "%blenderbim%\libs\site\packages\ifc5d" "%cd%\src\ifc5d\ifc5d"
mklink /D "%blenderbim%\libs\site\packages\ifccityjson" "%cd%\src\ifccityjson\ifccityjson"
mklink /D "%blenderbim%\libs\site\packages\ifcclash" "%cd%\src\ifcclash\ifcclash"
mklink /D "%blenderbim%\libs\site\packages\ifcpatch" "%cd%\src\ifcpatch\ifcpatch"
mklink /D "%blenderbim%\libs\site\packages\ifctester" "%cd%\src\ifctester\ifctester"
mklink /D "%blenderbim%\libs\site\packages\ifcfm" "%cd%\src\ifcfm\ifcfm"
mklink /D "%blenderbim%\libs\desktop" "%cd%\src\blenderbim\blenderbim\libs\desktop"
echo Manually downloading some third party dependencies...
curl https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.js -o "%blenderbim%\bim\data\gantt\jsgantt.js"
curl https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.css -o "%blenderbim%\bim\data\gantt\jsgantt.css"
curl -L https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl -o "%blenderbim%\bim\schema\Brick.ttl"
pause
After you modify your code in the Git repository, you will need to restart After you modify your code in the Git repository, you will need to restart
Blender for the changes to take effect. Blender for the changes to take effect.
@@ -1,5 +1,8 @@
try: try:
import bpy import bpy
# Ensure it's not fake-bpy-module.
if not hasattr(bpy, "context"):
raise ModuleNotFoundError
import bl_i18n_utils import bl_i18n_utils
import addon_utils import addon_utils
@@ -0,0 +1,66 @@
@echo off
rem SETUP BLENDER-BIM LIVE DEVELOPMENT ENVIRONMENT
rem Update BLENDER_VERSION, PYTHON_VERSION, BBIM_REPO in the script bellow.
rem Put the script to the folder where IfcOpenShell git repository is located
rem (script will try to clone IfcOpenShell.git if it's not present).
SET BLENDER_VERSION=4.2
SET PYTHON_VERSION=3.11
SET BBIM_REPO=user_default
SET BLENDER=%appdata%\Blender Foundation\Blender\%BLENDER_VERSION%
SET BLENDER_LOCAL=%BLENDER%\extensions\.local\lib\python%PYTHON_VERSION%\site-packages
SET BLENDERBIM=%BLENDER_LOCAL%\blenderbim
SET BLENDER_EXT=%BLENDER%\extensions\%BBIM_REPO%\blenderbim
git clone https://github.com/IfcOpenShell/IfcOpenShell.git
cd IfcOpenShell
echo Removing the Blender add-on Python code...
del "%BLENDER_EXT%\__init__.py"
rd /S /Q "%BLENDERBIM%"
echo Replacing them with links to the Git repository...
mklink "%BLENDER_EXT%\__init__.py" "%CD%\src\blenderbim\blenderbim\__init__.py"
mklink /D "%BLENDERBIM%" "%CD%\src\blenderbim\blenderbim"
echo Copy over compiled IfcOpenShell files...
copy "%BLENDER_LOCAL%\ifcopenshell\*_wrapper*" "%CD%\src\ifcopenshell-python\ifcopenshell\"
echo Remove the IfcOpenShell dependency...
rd /S /Q "%BLENDER_LOCAL%\ifcopenshell"
echo Replace them with links to the Git repository...
mklink /D "%BLENDER_LOCAL%\ifcopenshell" "%CD%\src\ifcopenshell-python\ifcopenshell"
echo Remove and link other IfcOpenShell utilities...
del "%BLENDER_LOCAL%\ifccsv.py"
del "%BLENDER_LOCAL%\ifcdiff.py"
del "%BLENDER_LOCAL%\bsdd.py"
rd /S /Q "%BLENDER_LOCAL%\bcf"
rd /S /Q "%BLENDER_LOCAL%\ifc4d"
rd /S /Q "%BLENDER_LOCAL%\ifc5d"
rd /S /Q "%BLENDER_LOCAL%\ifccityjson"
rd /S /Q "%BLENDER_LOCAL%\ifcclash"
rd /S /Q "%BLENDER_LOCAL%\ifcpatch"
rd /S /Q "%BLENDER_LOCAL%\ifctester"
rd /S /Q "%BLENDER_LOCAL%\ifcfm"
mklink "%BLENDER_LOCAL%\ifccsv.py" "%CD%\src\ifccsv\ifccsv.py"
mklink "%BLENDER_LOCAL%\ifcdiff.py" "%CD%\src\ifcdiff\ifcdiff.py"
mklink "%BLENDER_LOCAL%\bsdd.py" "%CD%\src\bsdd\bsdd.py"
mklink /D "%BLENDER_LOCAL%\bcf" "%CD%\src\bcf\bcf"
mklink /D "%BLENDER_LOCAL%\ifc4d" "%CD%\src\ifc4d\ifc4d"
mklink /D "%BLENDER_LOCAL%\ifc5d" "%CD%\src\ifc5d\ifc5d"
mklink /D "%BLENDER_LOCAL%\ifccityjson" "%CD%\src\ifccityjson\ifccityjson"
mklink /D "%BLENDER_LOCAL%\ifcclash" "%CD%\src\ifcclash\ifcclash"
mklink /D "%BLENDER_LOCAL%\ifcpatch" "%CD%\src\ifcpatch\ifcpatch"
mklink /D "%BLENDER_LOCAL%\ifctester" "%CD%\src\ifctester\ifctester"
mklink /D "%BLENDER_LOCAL%\ifcfm" "%CD%\src\ifcfm\ifcfm"
echo Manually downloading some third party dependencies...
curl https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.js -o "%BLENDERBIM%\bim\data\gantt\jsgantt.js"
curl https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.css -o "%BLENDERBIM%\bim\data\gantt\jsgantt.css"
curl -L https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl -o "%BLENDERBIM%\bim\schema\Brick.ttl"
pause
@@ -0,0 +1,60 @@
git clone https://github.com/IfcOpenShell/IfcOpenShell.git
cd IfcOpenShell
# path to BlenderBIM addon
# default path on Mac: "/Users/$USER/Library/Application Support/Blender/X.X/scripts/addons/blenderbim"
# default path on Linux: "$HOME/.config/blender/X.X/"
BLENDER_ADDON_PATH="/path/to/blender/X.XX/scripts/addons/blenderbim"
# Remove the Blender add-on Python code
rm -r $BLENDER_ADDON_PATH/core/
rm -r $BLENDER_ADDON_PATH/tool/
rm -r $BLENDER_ADDON_PATH/bim/
# Replace them with links to the Git repository
ln -s $PWD/src/blenderbim/blenderbim/core $BLENDER_ADDON_PATH/core
ln -s $PWD/src/blenderbim/blenderbim/tool $BLENDER_ADDON_PATH/tool
ln -s $PWD/src/blenderbim/blenderbim/bim $BLENDER_ADDON_PATH/bim
# Copy over compiled IfcOpenShell files
cp $BLENDER_ADDON_PATH/libs/site/packages/ifcopenshell/*_wrapper* $PWD/src/ifcopenshell-python/ifcopenshell/
# Remove the IfcOpenShell dependency
rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifcopenshell
# Replace them with links to the Git repository
ln -s $PWD/src/ifcopenshell-python/ifcopenshell $BLENDER_ADDON_PATH/libs/site/packages/ifcopenshell
# Remove and link other IfcOpenShell utilities
rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifccsv.py
rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifcdiff.py
rm -r $BLENDER_ADDON_PATH/libs/site/packages/bsdd.py
rm -r $BLENDER_ADDON_PATH/libs/site/packages/bcf
rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifc4d
rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifc5d
rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifccityjson
rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifcclash
rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifcpatch
rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifctester
rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifcfm
rm -r $BLENDER_ADDON_PATH/libs/Desktop
ln -s $PWD/src/ifccsv/ifccsv.py $BLENDER_ADDON_PATH/libs/site/packages/ifccsv.py
ln -s $PWD/src/ifcdiff/ifcdiff.py $BLENDER_ADDON_PATH/libs/site/packages/ifcdiff.py
ln -s $PWD/src/bsdd/bsdd.py $BLENDER_ADDON_PATH/libs/site/packages/bsdd.py
ln -s $PWD/src/bcf/bcf $BLENDER_ADDON_PATH/libs/site/packages/bcf
ln -s $PWD/src/ifc4d/ifc4d $BLENDER_ADDON_PATH/libs/site/packages/ifc4d
ln -s $PWD/src/ifc5d/ifc5d $BLENDER_ADDON_PATH/libs/site/packages/ifc5d
ln -s $PWD/src/ifccityjson/ifccityjson $BLENDER_ADDON_PATH/libs/site/packages/ifccityjson
ln -s $PWD/src/ifcclash/ifcclash $BLENDER_ADDON_PATH/libs/site/packages/ifcclash
ln -s $PWD/src/ifcpatch/ifcpatch $BLENDER_ADDON_PATH/libs/site/packages/ifcpatch
ln -s $PWD/src/ifctester/ifctester $BLENDER_ADDON_PATH/libs/site/packages/ifctester
ln -s $PWD/src/ifcfm/ifcfm $BLENDER_ADDON_PATH/libs/site/packages/ifcfm
ln -s $PWD/src/blenderbim/blenderbim/libs/desktop $BLENDER_ADDON_PATH/libs/Desktop
# Manually download some third party dependencies
cd $BLENDER_ADDON_PATH/bim/data/gantt
wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.js
wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.css
cd $BLENDER_ADDON_PATH/bim/schema
wget https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl
+11
View File
@@ -1,6 +1,8 @@
IS_STABLE:=FALSE
PYTHON:=python3.11 PYTHON:=python3.11
PIP:=pip3.11 PIP:=pip3.11
VERSION:=$(shell cat ../../VERSION) VERSION:=$(shell cat ../../VERSION)
VERSION_DATE:=$(shell date '+%y%m%d')
SED:=sed -i SED:=sed -i
VENV_BIN:=bin VENV_BIN:=bin
@@ -23,11 +25,20 @@ dist:
mkdir -p dist mkdir -p dist
cp -r $(PACKAGE_NAME) build/ cp -r $(PACKAGE_NAME) build/
cp pyproject.toml build/ cp pyproject.toml build/
ifeq ($(IS_STABLE), TRUE)
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml $(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml
ifdef IS_MODULE ifdef IS_MODULE
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/$(PACKAGE_NAME) $(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/$(PACKAGE_NAME)
else else
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/$(PACKAGE_NAME)/__init__.py $(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/$(PACKAGE_NAME)/__init__.py
endif
else
$(SED) 's/version = "0.0.0"/version = "$(VERSION)a$(VERSION_DATE)"/' build/pyproject.toml
ifdef IS_MODULE
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/$(PACKAGE_NAME)
else
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/$(PACKAGE_NAME)/__init__.py
endif
endif endif
cd build && $(PYTHON) -m venv env && . env/$(VENV_ACTIVATE) && $(PIP) install build cd build && $(PYTHON) -m venv env && . env/$(VENV_ACTIVATE) && $(PIP) install build
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m build cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m build
@@ -42,7 +42,7 @@ IfcGeom::OpenCascadeKernel::faceset_helper::faceset_helper(
loops.push_back(l); loops.push_back(l);
for (auto& e : l->children) { for (auto& e : l->children) {
// @todo make sure only cartesian points are provided here // @todo make sure only cartesian points are provided here
auto& p = boost::get<ifcopenshell::geometry::taxonomy::point3::ptr>(e->start); auto& p = boost::get<ifcopenshell::geometry::taxonomy::point3::ptr>(e->orientation.get_value_or(true) ? e->start : e->end);
if (point_identities_visited.find(p->identity()) == point_identities_visited.end()) { if (point_identities_visited.find(p->identity()) == point_identities_visited.end()) {
point_identities_visited.insert(p->identity()); point_identities_visited.insert(p->identity());
points.push_back(p); points.push_back(p);
@@ -201,10 +201,10 @@ void IfcGeom::OpenCascadeKernel::faceset_helper::loop_(const ifcopenshell::geome
return; return;
} }
auto a = boost::get<ifcopenshell::geometry::taxonomy::point3::ptr>(ps->children.back()->start); auto a = boost::get<ifcopenshell::geometry::taxonomy::point3::ptr>(ps->children.back()->orientation.get_value_or(true) ? ps->children.back()->start : ps->children.back()->end);
auto A = a->identity(); auto A = a->identity();
for (auto& b : ps->children) { for (auto& b : ps->children) {
auto B = boost::get<ifcopenshell::geometry::taxonomy::point3::ptr>(b->start)->identity(); auto B = boost::get<ifcopenshell::geometry::taxonomy::point3::ptr>(b->orientation.get_value_or(true) ? b->start : b->end)->identity();
auto C = vertex_mapping_[A], D = vertex_mapping_[B]; auto C = vertex_mapping_[A], D = vertex_mapping_[B];
bool fwd = C < D; bool fwd = C < D;
if (!b->orientation) { if (!b->orientation) {
@@ -52,7 +52,8 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in
auto pwf = taxonomy::dcast<taxonomy::piecewise_function>(dir); auto pwf = taxonomy::dcast<taxonomy::piecewise_function>(dir);
if (!pwf) { if (!pwf) {
// Only implement on alignment curves // Only implement on alignment curves
return nullptr; Logger::Warning("IfcSectionedSolidHorizontal is only implemented for piecewise function Directrix curves", inst);
return nullptr;
} }
{ {
@@ -111,6 +112,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in
loft->axis = nullptr; loft->axis = nullptr;
// @todo currently only the case is handled where directrix returns a piecewise_function // @todo currently only the case is handled where directrix returns a piecewise_function
// @todo this "if" statement is not really required because the function returns at the start if the Directrix is not a piecewise function
if (pwf) { if (pwf) {
double start = std::max(0., cross_sections.front().dist_along); double start = std::max(0., cross_sections.front().dist_along);
double end = std::min(pwf->length(), cross_sections.back().dist_along); double end = std::min(pwf->length(), cross_sections.back().dist_along);
+10 -2
View File
@@ -341,10 +341,10 @@ namespace {
return item; return item;
} }
while (item->declaration().is(IfcSchema::IfcBooleanClippingResult::Class())) { while (auto booleanresult = item->as<IfcSchema::IfcBooleanClippingResult>()) {
// All instantiations of IfcBooleanOperand (type of FirstOperand) are subtypes of // All instantiations of IfcBooleanOperand (type of FirstOperand) are subtypes of
// IfcGeometricRepresentationItem // IfcGeometricRepresentationItem
item = (IfcSchema::IfcGeometricRepresentationItem*) ((IfcSchema::IfcBooleanClippingResult*) item)->FirstOperand(); item = booleanresult->FirstOperand()->as<IfcSchema::IfcRepresentationItem>();
if (item->StyledByItem()->size()) { if (item->StyledByItem()->size()) {
return item; return item;
} }
@@ -756,6 +756,14 @@ void mapping::initialize_units_() {
if (!angle_unit_encountered) { if (!angle_unit_encountered) {
Logger::Warning("No plane angle unit encountered"); Logger::Warning("No plane angle unit encountered");
} }
// @todo move to a more descriptive function
if (settings_.get<settings::BuildingLocalPlacement>().get()) {
placement_rel_to_type_ = file_->schema()->declaration_by_name("IfcBuilding");
}
if (settings_.get<settings::SiteLocalPlacement>().get()) {
placement_rel_to_type_ = file_->schema()->declaration_by_name("IfcSite");
}
} }
void mapping::initialize_settings() { void mapping::initialize_settings() {
+13 -2
View File
@@ -1,4 +1,6 @@
IS_STABLE:=FALSE
VERSION:=$(shell cat ../../VERSION) VERSION:=$(shell cat ../../VERSION)
VERSION_DATE:=$(shell date '+%y%m%d')
PYVERSION:=py311 PYVERSION:=py311
PLATFORM:=linux64 PLATFORM:=linux64
@@ -120,11 +122,20 @@ endif
cp -r build/botbuild/ifcopenshell/*ifcopenshell_wrapper* build/ifcopenshell/ cp -r build/botbuild/ifcopenshell/*ifcopenshell_wrapper* build/ifcopenshell/
cp ../../README.md build/ cp ../../README.md build/
cp pyproject.toml build/ cp pyproject.toml build/
ifeq ($(IS_STABLE), TRUE)
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml $(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/ifcopenshell/__init__.py $(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/ifcopenshell/__init__.py
else
$(SED) 's/version = "0.0.0"/version = "$(VERSION)a$(VERSION_DATE)"/' build/pyproject.toml
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/ifcopenshell/__init__.py
endif
cd build && $(PYTHON) -m venv env && . env/$(VENV_ACTIVATE) && $(PIP) install build cd build && $(PYTHON) -m venv env && . env/$(VENV_ACTIVATE) && $(PIP) install build
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m build cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m build
mv build/dist/ifcopenshell-$(VERSION)-py3-none-any.whl dist/ifcopenshell-$(VERSION)-$(PYVERSION)-none-$(PLATFORMTAG).whl ifeq ($(IS_STABLE), TRUE)
rm -rf build mv build/dist/ifcopenshell-*.whl dist/ifcopenshell-$(VERSION)-$(PYVERSION)-none-$(PLATFORMTAG).whl
else
cp build/dist/ifcopenshell-*.whl dist/ifcopenshell-$(VERSION)a$(VERSION_DATE)-$(PYVERSION)-none-$(PLATFORMTAG).whl
endif
# rm -rf build
+2 -2
View File
@@ -81,10 +81,10 @@ autoapi_add_toctree_entry = True
autoapi_type = "python" autoapi_type = "python"
# autoapi works by reading source code instead of importing modules # autoapi works by reading source code instead of importing modules
autoapi_dirs = ['../ifcopenshell', '../../bcf/src', '../../bsdd', '../../ifccsv', '../../ifcdiff', '../../ifcpatch/ifcpatch', '../../ifctester/ifctester'] autoapi_dirs = ['../ifcopenshell', '../../bcf/bcf', '../../bsdd', '../../ifccsv', '../../ifcdiff', '../../ifcpatch/ifcpatch', '../../ifctester/ifctester']
# autoapi_dirs = ['../../ifcdiff'] # autoapi_dirs = ['../../ifcdiff']
# autoapi_dirs = ['../../ifcdiff', '../ifcopenshell/util'] # autoapi_dirs = ['../../ifcdiff', '../ifcopenshell/util']
# autoapi_dirs = ['../../bcf/src', '../../bsdd', '../../ifccsv', '../../ifcdiff', '../../ifcpatch/ifcpatch', '../../ifctester/ifctester'] # autoapi_dirs = ['../../bcf/bcf', '../../bsdd', '../../ifccsv', '../../ifcdiff', '../../ifcpatch/ifcpatch', '../../ifctester/ifctester']
# These are auto-generated based on the IFC schema, so exclude them # These are auto-generated based on the IFC schema, so exclude them
autoapi_ignore = ['*ifcopenshell/express/rules*'] autoapi_ignore = ['*ifcopenshell/express/rules*']
@@ -21,6 +21,14 @@
}, },
{ {
"name": "Capping" "name": "Capping"
},
{
"name": "Screed",
"predefined_type": "FLOORING"
},
{
"name": "Screed",
"predefined_type": "TOPPING"
} }
], ],
"IfcDistributionElement": [ "IfcDistributionElement": [