mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-24 16:59:55 +00:00
Merge parametric_dimensions (PR #8083) to resolve build conflict
Both PR #7798 (ManualDrawingReference) and PR #8083 (parametric_dimensions) independently added properties after entry #33 in Psets_BBIM_Annotation.ifc and new BoolProperty declarations after `type_name` in prop.py and workspace.py UI rows at the same locations. Resolution: keep #8083's IDs (#34-#40) unchanged, renumber #7798's IsManualDrawingReference and IsDocumentReference entries to #41 and #42, update EPset_Annotation reference list accordingly, keep both UI rows, and combine hotkey_S_A to use #8083's parametric-dimension routing with #7798's "INVOKE_DEFAULT" argument for add_annotation.
This commit is contained in:
Executable
+95
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env -S uv run
|
||||||
|
# /// script
|
||||||
|
# dependencies = [
|
||||||
|
# "PyGithub",
|
||||||
|
# "requests",
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from github import Github
|
||||||
|
from github.GitReleaseAsset import GitReleaseAsset
|
||||||
|
|
||||||
|
EXTENSION_ID = "bonsai"
|
||||||
|
CURRENT_PYTHON_VERSION = "py313"
|
||||||
|
CURRENT_PLATFORMS = ["linux-x64", "macos-arm64", "windows-x64"]
|
||||||
|
|
||||||
|
|
||||||
|
def publish_asset(asset: GitReleaseAsset, token: str, repo_root: Path) -> None:
|
||||||
|
"""
|
||||||
|
Publish an asset to Blender Extensions.
|
||||||
|
Reference: https://extensions.blender.org/api/v1/swagger
|
||||||
|
"""
|
||||||
|
temp_path = repo_root / asset.name
|
||||||
|
|
||||||
|
response = requests.get(asset.browser_download_url)
|
||||||
|
response.raise_for_status()
|
||||||
|
temp_path.write_bytes(response.content)
|
||||||
|
|
||||||
|
url = f"https://extensions.blender.org/api/v1/extensions/{EXTENSION_ID}/versions/upload/"
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
|
files = {"version_file": temp_path.read_bytes()}
|
||||||
|
response = requests.post(url, headers=headers, files=files)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
temp_path.unlink()
|
||||||
|
|
||||||
|
print(f"✓ Published {asset.name}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
token = os.getenv("BLENDER_EXTENSIONS_TOKEN")
|
||||||
|
if not token:
|
||||||
|
raise Exception("BLENDER_EXTENSIONS_TOKEN environment variable not set")
|
||||||
|
|
||||||
|
# Get the repository root
|
||||||
|
repo_root = Path(__file__).parent.parent.parent
|
||||||
|
|
||||||
|
# Read VERSION file
|
||||||
|
version_file = repo_root / "VERSION"
|
||||||
|
version = version_file.read_text().strip()
|
||||||
|
|
||||||
|
print(f"Current VERSION: {version}")
|
||||||
|
|
||||||
|
tag_name = f"bonsai-{version}"
|
||||||
|
|
||||||
|
# Get release from GitHub
|
||||||
|
gh = Github()
|
||||||
|
gh_repo = gh.get_repo("IfcOpenShell/IfcOpenShell")
|
||||||
|
release = gh_repo.get_release(tag_name)
|
||||||
|
|
||||||
|
assets = release.get_assets()
|
||||||
|
|
||||||
|
asset_platform_map: dict[str, tuple[GitReleaseAsset, str]] = {}
|
||||||
|
for asset in assets:
|
||||||
|
if CURRENT_PYTHON_VERSION not in asset.name:
|
||||||
|
continue
|
||||||
|
for platform in CURRENT_PLATFORMS:
|
||||||
|
if platform in asset.name:
|
||||||
|
asset_platform_map[asset.name] = (asset, platform)
|
||||||
|
break
|
||||||
|
|
||||||
|
if len(asset_platform_map) != len(CURRENT_PLATFORMS):
|
||||||
|
found_platforms = {platform for _, (_, platform) in asset_platform_map.items()}
|
||||||
|
missing_platforms = set(CURRENT_PLATFORMS) - found_platforms
|
||||||
|
raise Exception(
|
||||||
|
f"Expected {len(CURRENT_PLATFORMS)} assets but found {len(asset_platform_map)}. "
|
||||||
|
f"Missing: {', '.join(sorted(missing_platforms))}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nRelease assets:")
|
||||||
|
for asset_name in sorted(asset_platform_map.keys()):
|
||||||
|
print(f"- {asset_name}")
|
||||||
|
|
||||||
|
# https://extensions.blender.org/api/v1/swagger
|
||||||
|
print("\nPublishing assets to Blender Extensions:")
|
||||||
|
for asset_name, (asset, platform) in asset_platform_map.items():
|
||||||
|
publish_asset(asset, token, repo_root)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
python ../nix/cache_dependencies.py unpack
|
python ../nix/cache_dependencies.py unpack
|
||||||
|
|
||||||
- name: ccache
|
- name: ccache
|
||||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||||
with:
|
with:
|
||||||
key: mac-${{ matrix.arch }}
|
key: mac-${{ matrix.arch }}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
python ../IfcOpenShell/nix/cache_dependencies.py unpack
|
python ../IfcOpenShell/nix/cache_dependencies.py unpack
|
||||||
|
|
||||||
- name: ccache
|
- name: ccache
|
||||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||||
with:
|
with:
|
||||||
key: ubuntu-22.04-${{ runner.arch }}
|
key: ubuntu-22.04-${{ runner.arch }}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ jobs:
|
|||||||
container: rockylinux:9
|
container: rockylinux:9
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Set up uv
|
||||||
|
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||||
|
|
||||||
|
- name: Install Python
|
||||||
|
# Installs latest Python version so it's preferred by uv over Rocky's system Python.
|
||||||
|
run: uv python install
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: |
|
run: |
|
||||||
dnf update -y
|
dnf update -y
|
||||||
@@ -17,7 +24,6 @@ jobs:
|
|||||||
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
|
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
|
||||||
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
|
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
|
||||||
findutils xz byacc
|
findutils xz byacc
|
||||||
python3 -m pip install typing_extensions
|
|
||||||
git config --global --add safe.directory '*'
|
git config --global --add safe.directory '*'
|
||||||
|
|
||||||
- name: Install aws cli
|
- name: Install aws cli
|
||||||
@@ -45,10 +51,10 @@ jobs:
|
|||||||
- name: Unpack Dependencies
|
- name: Unpack Dependencies
|
||||||
run: |
|
run: |
|
||||||
cd build
|
cd build
|
||||||
python3 ../nix/cache_dependencies.py unpack
|
uv run ../nix/cache_dependencies.py unpack
|
||||||
|
|
||||||
- name: ccache
|
- name: ccache
|
||||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||||
with:
|
with:
|
||||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
|
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
|
||||||
|
|
||||||
@@ -56,7 +62,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release python3 ./nix/build-all.py -v --diskcleanup 2>&1 | tee build.log
|
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release uv run ./nix/build-all.py -v --diskcleanup 2>&1 | tee build.log
|
||||||
|
|
||||||
- name: Upload Build Logs
|
- name: Upload Build Logs
|
||||||
if: always()
|
if: always()
|
||||||
@@ -71,7 +77,7 @@ jobs:
|
|||||||
- name: Pack Dependencies
|
- name: Pack Dependencies
|
||||||
run: |
|
run: |
|
||||||
cd build
|
cd build
|
||||||
python3 ../nix/cache_dependencies.py pack
|
uv run ../nix/cache_dependencies.py pack
|
||||||
|
|
||||||
- name: Commit and Push Changes to Build Repository
|
- name: Commit and Push Changes to Build Repository
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ jobs:
|
|||||||
container: arm64v8/rockylinux:9
|
container: arm64v8/rockylinux:9
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Set up uv
|
||||||
|
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||||
|
|
||||||
|
- name: Install Python
|
||||||
|
# Installs latest Python version so it's preferred by uv over Rocky's system Python.
|
||||||
|
run: uv python install
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: |
|
run: |
|
||||||
dnf update -y
|
dnf update -y
|
||||||
@@ -17,7 +24,6 @@ jobs:
|
|||||||
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
|
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
|
||||||
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
|
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
|
||||||
findutils xz byacc
|
findutils xz byacc
|
||||||
python3 -m pip install typing_extensions
|
|
||||||
git config --global --add safe.directory '*'
|
git config --global --add safe.directory '*'
|
||||||
|
|
||||||
- name: Install aws cli
|
- name: Install aws cli
|
||||||
@@ -45,10 +51,10 @@ jobs:
|
|||||||
- name: Unpack Dependencies
|
- name: Unpack Dependencies
|
||||||
run: |
|
run: |
|
||||||
cd build
|
cd build
|
||||||
python3 ../nix/cache_dependencies.py unpack
|
uv run ../nix/cache_dependencies.py unpack
|
||||||
|
|
||||||
- name: ccache
|
- name: ccache
|
||||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||||
with:
|
with:
|
||||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
|
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
|
||||||
|
|
||||||
@@ -56,7 +62,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release python3 ./nix/build-all.py -v --diskcleanup 2>&1 | tee build.log
|
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release uv run ./nix/build-all.py -v --diskcleanup 2>&1 | tee build.log
|
||||||
|
|
||||||
- name: Upload Build Logs
|
- name: Upload Build Logs
|
||||||
if: always()
|
if: always()
|
||||||
@@ -71,7 +77,7 @@ jobs:
|
|||||||
- name: Pack Dependencies
|
- name: Pack Dependencies
|
||||||
run: |
|
run: |
|
||||||
cd build
|
cd build
|
||||||
python3 ../nix/cache_dependencies.py pack
|
uv run ../nix/cache_dependencies.py pack
|
||||||
|
|
||||||
- name: Commit and Push Changes to Build Repository
|
- name: Commit and Push Changes to Build Repository
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
- name: ccache
|
- name: ccache
|
||||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||||
with:
|
with:
|
||||||
key: win-${{ matrix.arch }}
|
key: win-${{ matrix.arch }}
|
||||||
# Windows ccache needs ~1GB
|
# Windows ccache needs ~1GB
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ jobs:
|
|||||||
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
|
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
|
||||||
|
|
||||||
# Download Blender.
|
# Download Blender.
|
||||||
wget -q -O blender.tar.xz https://download.blender.org/release/Blender5.0/blender-5.0.1-linux-x64.tar.xz
|
wget -q -O blender.tar.xz https://download.blender.org/release/Blender5.1/blender-5.1.0-linux-x64.tar.xz
|
||||||
tar -xf blender.tar.xz
|
tar -xf blender.tar.xz
|
||||||
|
|
||||||
# Setup Blender.
|
# Setup Blender.
|
||||||
@@ -122,7 +122,7 @@ jobs:
|
|||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
python setup_extensions_repo.py --last-tag
|
python setup_extensions_repo.py --last-tag
|
||||||
cd ..
|
cd ..
|
||||||
bonsai_zip="$(pwd)/$(ls bonsai_unstable_repo/bonsai_py311*-linux-x64.zip)"
|
bonsai_zip="$(pwd)/$(ls bonsai_unstable_repo/bonsai_py313*-linux-x64.zip)"
|
||||||
|
|
||||||
# Install Bonsai.
|
# Install Bonsai.
|
||||||
blender --command extension install-file -r user_default -e $bonsai_zip
|
blender --command extension install-file -r user_default -e $bonsai_zip
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
pyver: [py311, py312]
|
pyver: [py311, py312, py313]
|
||||||
config:
|
config:
|
||||||
- {
|
- {
|
||||||
name: "Windows Build",
|
name: "Windows Build",
|
||||||
@@ -42,6 +42,11 @@ jobs:
|
|||||||
name: "MacOS ARM Build",
|
name: "MacOS ARM Build",
|
||||||
short_name: macosm1,
|
short_name: macosm1,
|
||||||
}
|
}
|
||||||
|
exclude:
|
||||||
|
# Python 3.13 is needed for Blender 5.1+ and Blender dropped Intel Mac support in 5.0.
|
||||||
|
- pyver: py313
|
||||||
|
config:
|
||||||
|
short_name: macos
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name: ci-ifcedit-pypi
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
activate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||||
|
steps:
|
||||||
|
- name: Set env
|
||||||
|
run: echo ok go
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: activate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
- name: Compile
|
||||||
|
run: |
|
||||||
|
pip install build
|
||||||
|
cd src/ifcedit &&
|
||||||
|
make dist IS_STABLE=TRUE
|
||||||
|
- name: Publish a Python distribution to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
with:
|
||||||
|
user: __token__
|
||||||
|
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||||
|
packages_dir: src/ifcedit/dist
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
name: ci-ifcmcp-pypi
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
activate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||||
|
steps:
|
||||||
|
- name: Set env
|
||||||
|
run: echo ok go
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: activate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
- name: Compile
|
||||||
|
run: |
|
||||||
|
pip install build
|
||||||
|
cd src/ifcmcp &&
|
||||||
|
make dist IS_STABLE=TRUE
|
||||||
|
- name: Publish a Python distribution to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
with:
|
||||||
|
user: __token__
|
||||||
|
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||||
|
packages_dir: src/ifcmcp/dist
|
||||||
|
verbose: true
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
if: |
|
if: |
|
||||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||||
steps:
|
steps:
|
||||||
- uses: mamba-org/setup-micromamba@v2 # https://github.com/mamba-org/setup-micromamba
|
- uses: mamba-org/setup-micromamba@v3 # https://github.com/mamba-org/setup-micromamba
|
||||||
with:
|
with:
|
||||||
environment-name: test-env
|
environment-name: test-env
|
||||||
create-args: >-
|
create-args: >-
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
curl -L https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX10.13.sdk.tar.xz | tar -xvJf - -C /Users/runner/work/
|
curl -L https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX10.13.sdk.tar.xz | tar -xvJf - -C /Users/runner/work/
|
||||||
|
|
||||||
- uses: mamba-org/setup-micromamba@v2 # https://github.com/mamba-org/setup-micromamba
|
- uses: mamba-org/setup-micromamba@v3 # https://github.com/mamba-org/setup-micromamba
|
||||||
with:
|
with:
|
||||||
environment-name: test-env
|
environment-name: test-env
|
||||||
create-args: >-
|
create-args: >-
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
|
|
||||||
-
|
-
|
||||||
name: ccache
|
name: ccache
|
||||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||||
|
|
||||||
-
|
-
|
||||||
name: Build ifcopenshell
|
name: Build ifcopenshell
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
pyver: [py39, py310, py311, py312, py313, py314]
|
pyver: [py310, py311, py312, py313, py314]
|
||||||
config:
|
config:
|
||||||
- {
|
- {
|
||||||
name: "Windows 64bit",
|
name: "Windows 64bit",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
pyver: [py39, py310, py311, py312, py313, py314]
|
pyver: [py310, py311, py312, py313, py314]
|
||||||
config:
|
config:
|
||||||
- {
|
- {
|
||||||
name: "Windows 64bit",
|
name: "Windows 64bit",
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name: ci-ifcquery-pypi
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
activate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||||
|
steps:
|
||||||
|
- name: Set env
|
||||||
|
run: echo ok go
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: activate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
- name: Compile
|
||||||
|
run: |
|
||||||
|
pip install build
|
||||||
|
cd src/ifcquery &&
|
||||||
|
make dist IS_STABLE=TRUE
|
||||||
|
- name: Publish a Python distribution to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
with:
|
||||||
|
user: __token__
|
||||||
|
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||||
|
packages_dir: src/ifcquery/dist
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: ci-black-formatting
|
name: ci-lint
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -7,6 +7,9 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
lint-formatting:
|
lint-formatting:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
MIN_IOS_PY_VERSION: "3.10"
|
||||||
|
MIN_BLENDER_PY_VERSION: "3.11"
|
||||||
steps:
|
steps:
|
||||||
- name: Action - checkout repository
|
- name: Action - checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@@ -14,12 +17,12 @@ jobs:
|
|||||||
- name: Action - install python
|
- name: Action - install python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: ${{ env.MIN_IOS_PY_VERSION }}
|
||||||
|
|
||||||
- name: Action - install python
|
- name: Action - install python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: ${{ env.MIN_BLENDER_PY_VERSION }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -27,6 +30,7 @@ jobs:
|
|||||||
uv tool install ruff
|
uv tool install ruff
|
||||||
uv tool install black
|
uv tool install black
|
||||||
uv tool install poethepoet
|
uv tool install poethepoet
|
||||||
|
uv tool install ty
|
||||||
|
|
||||||
# black doesn't catch all syntax errors, so we check them explicitly.
|
# black doesn't catch all syntax errors, so we check them explicitly.
|
||||||
- name: Check syntax errors
|
- name: Check syntax errors
|
||||||
@@ -35,8 +39,8 @@ jobs:
|
|||||||
ERROR=0
|
ERROR=0
|
||||||
# Using 2 Python versions - one minimum required for IfcOpenShell
|
# Using 2 Python versions - one minimum required for IfcOpenShell
|
||||||
# and other that's used by Blender currently.
|
# and other that's used by Blender currently.
|
||||||
python3.10 -W error -m compileall -q src/ifcopenshell-python || ERROR=1
|
python${{ env.MIN_IOS_PY_VERSION }} -W error -m compileall -q src/ifcopenshell-python || ERROR=1
|
||||||
python3.11 -W error -m compileall -q src/bonsai || ERROR=1
|
python${{ env.MIN_BLENDER_PY_VERSION }} -W error -m compileall -q src/bonsai || ERROR=1
|
||||||
exit $ERROR
|
exit $ERROR
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
@@ -54,6 +58,13 @@ jobs:
|
|||||||
black --diff --check . | black-codeclimate | python .github/workflows/black_to_github_annotations.py
|
black --diff --check . | black-codeclimate | python .github/workflows/black_to_github_annotations.py
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: ty check
|
||||||
|
id: ty
|
||||||
|
run: |
|
||||||
|
poe ty-venv
|
||||||
|
poe ty
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Ruff check
|
- name: Ruff check
|
||||||
id: ruff
|
id: ruff
|
||||||
run: |
|
run: |
|
||||||
@@ -84,8 +95,7 @@ jobs:
|
|||||||
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||||
}
|
}
|
||||||
|
|
||||||
run_check poe ruff-main
|
run_check poe ruff
|
||||||
run_check poe ruff-old
|
|
||||||
|
|
||||||
exit $ERROR
|
exit $ERROR
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
@@ -102,4 +112,7 @@ jobs:
|
|||||||
if [ "${{ steps.ruff.outcome }}" != "success" ]; then
|
if [ "${{ steps.ruff.outcome }}" != "success" ]; then
|
||||||
echo "::error::Ruff check failed, see Summary or 'ruff' step for the details." && ERROR=1
|
echo "::error::Ruff check failed, see Summary or 'ruff' step for the details." && ERROR=1
|
||||||
fi
|
fi
|
||||||
|
if [ "${{ steps.ty.outcome }}" != "success" ]; then
|
||||||
|
echo "::error::ty check failed, see 'ty check' step for the details." && ERROR=1
|
||||||
|
fi
|
||||||
exit $ERROR
|
exit $ERROR
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
name: Release Pyodide WASM Wheel
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout IfcOpenShell
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v7
|
||||||
|
|
||||||
|
- name: Build wheel
|
||||||
|
working-directory: pyodide
|
||||||
|
run: uv run pack_wheel.py --build
|
||||||
|
|
||||||
|
- name: Find wheel
|
||||||
|
id: wheel
|
||||||
|
run: |
|
||||||
|
WHEEL=$(ls pyodide/dist/ifcopenshell-*.whl)
|
||||||
|
echo "path=$WHEEL" >> $GITHUB_OUTPUT
|
||||||
|
echo "name=$(basename $WHEEL)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Checkout wasm-wheels
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
repository: IfcOpenShell/wasm-wheels
|
||||||
|
path: wasm-wheels
|
||||||
|
token: ${{ secrets.BUILD_REPO_TOKEN }}
|
||||||
|
|
||||||
|
- name: Commit and push wheel to wasm-wheels
|
||||||
|
run: |
|
||||||
|
WHEEL_NAME="${{ steps.wheel.outputs.name }}"
|
||||||
|
cp "${{ steps.wheel.outputs.path }}" "wasm-wheels/$WHEEL_NAME"
|
||||||
|
cd wasm-wheels
|
||||||
|
git config user.name "IfcOpenBot"
|
||||||
|
git config user.email "ifcopenbot@ifcopenshell.org"
|
||||||
|
git add "$WHEEL_NAME"
|
||||||
|
git commit -m "Add $WHEEL_NAME"
|
||||||
|
VERSION=$(cat ../VERSION)
|
||||||
|
git tag "v${VERSION}"
|
||||||
|
git push origin main
|
||||||
|
git push origin "v${VERSION}"
|
||||||
@@ -79,7 +79,7 @@ jobs:
|
|||||||
libhdf5-dev libcgal-dev libeigen3-dev
|
libhdf5-dev libcgal-dev libeigen3-dev
|
||||||
|
|
||||||
- name: ccache
|
- name: ccache
|
||||||
uses: hendrikmuhs/ccache-action@v1.2.20
|
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||||
with:
|
with:
|
||||||
key: ubuntu-22.04-${{ runner.arch }}
|
key: ubuntu-22.04-${{ runner.arch }}
|
||||||
|
|
||||||
@@ -254,6 +254,7 @@ jobs:
|
|||||||
cd ../ifcpatch && make test || ERROR=1
|
cd ../ifcpatch && make test || ERROR=1
|
||||||
pip install -e ../ifctester --no-deps
|
pip install -e ../ifctester --no-deps
|
||||||
cd ../ifctester && make test || ERROR=1
|
cd ../ifctester && make test || ERROR=1
|
||||||
|
make build-ids-docs || ERROR=1
|
||||||
# Run mathutils related tests at the end to ensure no other code is relying on mathutils.
|
# Run mathutils related tests at the end to ensure no other code is relying on mathutils.
|
||||||
cd ../ifcopenshell-python
|
cd ../ifcopenshell-python
|
||||||
pip install mathutils
|
pip install mathutils
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
name: Build and Deploy Stable Documentation
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch: # Manual trigger
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: '3.x'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
cd src/bonsai/docs
|
|
||||||
pip install -r requirements.txt # Run pip install from the docs directory
|
|
||||||
|
|
||||||
- name: Build documentation
|
|
||||||
run: |
|
|
||||||
cd src/bonsai/docs
|
|
||||||
make html
|
|
||||||
|
|
||||||
- name: Deploy to GitHub Pages (Stable)
|
|
||||||
uses: peaceiris/actions-gh-pages@v4
|
|
||||||
with:
|
|
||||||
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
|
|
||||||
external_repository: IfcOpenShell/bonsaibim_org_docs
|
|
||||||
publish_branch: main
|
|
||||||
cname: docs.bonsaibim.org
|
|
||||||
publish_dir: src/bonsai/docs/_build/html
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
name: Deploy AI chat App to static page repo
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
pages: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'src/ifcchat/**'
|
||||||
|
- '.github/workflows/publish-aichat-app.yaml'
|
||||||
|
branches:
|
||||||
|
- v0.8.0
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
activate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||||
|
steps:
|
||||||
|
- name: Set env
|
||||||
|
run: echo ok go
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: activate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout (recursive)
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Checkout intermediate Pages repo
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
repository: IfcOpenShell/aichat_ifcopenshell_org_static_html
|
||||||
|
ref: gh-pages
|
||||||
|
path: output
|
||||||
|
token: ${{ secrets.WEBSITE_PUBLISH }}
|
||||||
|
- name: Sync demo app into target subfolder
|
||||||
|
run: |
|
||||||
|
rsync -av --delete --exclude='.git/' src/ifcchat/ output/
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
- name: Download wheels
|
||||||
|
working-directory: output/
|
||||||
|
run: |
|
||||||
|
pip download ifcquery==0.8.5 ifcopenshell-mcp==0.8.5 ifcedit==0.8.5 lark==1.3.1 isodate==0.7.2 --no-deps -d ./dist
|
||||||
|
- name: Commit and push if changed
|
||||||
|
working-directory: output
|
||||||
|
run: |
|
||||||
|
git config --global user.name 'IfcOpenBot'
|
||||||
|
git config --global user.email 'IfcOpenBot@users.noreply.github.com'
|
||||||
|
|
||||||
|
git add .
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No changes to commit"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git commit -m "$(git log --oneline -1)"
|
||||||
|
git push origin gh-pages
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
name: Publish Bonsai Releases
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: astral-sh/setup-uv@v7
|
||||||
|
|
||||||
|
- run: uv run .github/scripts/publish-bonsai-releases.py
|
||||||
|
env:
|
||||||
|
BLENDER_EXTENSIONS_TOKEN: ${{ secrets.BLENDER_EXTENSIONS_TOKEN }}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Deploy Pyodide Demo App to GitHub Pages
|
name: Deploy Pyodide Demo App to static page repo
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
@@ -11,6 +11,7 @@ on:
|
|||||||
- '.github/workflows/publish-pyodide-demo-app.yml'
|
- '.github/workflows/publish-pyodide-demo-app.yml'
|
||||||
branches:
|
branches:
|
||||||
- v0.8.0
|
- v0.8.0
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
activate:
|
activate:
|
||||||
@@ -30,21 +31,27 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Pages
|
- name: Checkout intermediate Pages repo
|
||||||
uses: actions/configure-pages@v5
|
uses: actions/checkout@v6
|
||||||
- name: Upload static files as artifact
|
|
||||||
id: deployment
|
|
||||||
uses: actions/upload-pages-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
path: src/pyodide/demo-app/
|
repository: IfcOpenShell/wasm_ifcopenshell_org_static_html
|
||||||
|
ref: gh-pages
|
||||||
|
path: output
|
||||||
|
token: ${{ secrets.WEBSITE_PUBLISH }}
|
||||||
|
- name: Sync demo app into target subfolder
|
||||||
|
run: |
|
||||||
|
rsync -av --delete --exclude='.git/' src/pyodide/demo-app/ output/
|
||||||
|
- name: Commit and push if changed
|
||||||
|
working-directory: output
|
||||||
|
run: |
|
||||||
|
git config --global user.name 'IfcOpenBot'
|
||||||
|
git config --global user.email 'IfcOpenBot@users.noreply.github.com'
|
||||||
|
|
||||||
deploy:
|
git add .
|
||||||
environment:
|
if git diff --cached --quiet; then
|
||||||
name: github-pages
|
echo "No changes to commit"
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
exit 0
|
||||||
runs-on: ubuntu-latest
|
fi
|
||||||
needs: build
|
|
||||||
steps:
|
git commit -m "$(git log --oneline -1)"
|
||||||
- name: Deploy to GitHub Pages
|
git push origin gh-pages
|
||||||
id: deployment
|
|
||||||
uses: actions/deploy-pages@v4
|
|
||||||
|
|||||||
+10
-3
@@ -5,6 +5,8 @@
|
|||||||
/_installed-vs*-x*/
|
/_installed-vs*-x*/
|
||||||
/build/
|
/build/
|
||||||
/src/examples/build/
|
/src/examples/build/
|
||||||
|
# ifctester docs output
|
||||||
|
/src/ifctester/test/build/
|
||||||
|
|
||||||
# output directories
|
# output directories
|
||||||
/cmake/out/
|
/cmake/out/
|
||||||
@@ -12,6 +14,7 @@
|
|||||||
/src/ifcmax/out/
|
/src/ifcmax/out/
|
||||||
/src/ifcwrap/out/
|
/src/ifcwrap/out/
|
||||||
/src/qtviewer/out/
|
/src/qtviewer/out/
|
||||||
|
/src/ifctester/webapp/public/pyodide/
|
||||||
|
|
||||||
/win/BuildDepsCache*.txt
|
/win/BuildDepsCache*.txt
|
||||||
|
|
||||||
@@ -80,10 +83,14 @@ src/ifcopenshell-python/test/build
|
|||||||
# bonsai i18n
|
# bonsai i18n
|
||||||
src/bonsai/bonsai/translations.py
|
src/bonsai/bonsai/translations.py
|
||||||
|
|
||||||
# bonsai test temp files
|
# bonsai external dependencies (cloned for just ty checks)
|
||||||
|
src/bonsai/external_dependencies/
|
||||||
|
|
||||||
|
# bonsai test temp/cache files
|
||||||
src/bonsai/test/files/temp
|
src/bonsai/test/files/temp
|
||||||
src/bonsai/test/files/basic.ifc.cache.blend
|
src/bonsai/test/files/*.cache.blend
|
||||||
src/bonsai/test/files/basic.ifc.cache.sqlite
|
src/bonsai/test/files/*.cache.json
|
||||||
|
src/bonsai/test/files/*.cache.sqlite
|
||||||
|
|
||||||
# bonsai data
|
# bonsai data
|
||||||
src/bonsai/bonsai/bim/data/build/
|
src/bonsai/bonsai/bim/data/build/
|
||||||
|
|||||||
@@ -50,11 +50,14 @@ Contents
|
|||||||
| [ifcconvert](https://docs.ifcopenshell.org/ifcconvert.html) | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcconvert/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true)
|
| [ifcconvert](https://docs.ifcopenshell.org/ifcconvert.html) | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcconvert/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true)
|
||||||
| [ifccsv](https://docs.ifcopenshell.org/ifccsv.html) | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifccsv/) |
|
| [ifccsv](https://docs.ifcopenshell.org/ifccsv.html) | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifccsv/) |
|
||||||
| [ifcdiff](https://docs.ifcopenshell.org/ifcdiff.html) | Compare changes between IFC models | LGPL-3.0-or-later | [](https://pypi.org/project/ifcdiff/) |
|
| [ifcdiff](https://docs.ifcopenshell.org/ifcdiff.html) | Compare changes between IFC models | LGPL-3.0-or-later | [](https://pypi.org/project/ifcdiff/) |
|
||||||
|
| [ifcedit](https://docs.ifcopenshell.org/ifcedit.html) | CLI wrapper for ifcopenshell.api IFC model mutation functions | LGPL-3.0-or-later | [](https://pypi.org/project/ifcedit/) |
|
||||||
| [ifcfm](https://docs.ifcopenshell.org/ifcfm.html) | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [](https://pypi.org/project/ifcfm/) |
|
| [ifcfm](https://docs.ifcopenshell.org/ifcfm.html) | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [](https://pypi.org/project/ifcfm/) |
|
||||||
| [ifcmax](https://docs.ifcopenshell.org/ifcmax.html) | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcmax.html)
|
| [ifcmax](https://docs.ifcopenshell.org/ifcmax.html) | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcmax.html)
|
||||||
| [ifcopenshell-python](https://docs.ifcopenshell.org/ifcopenshell-python.html) | Python library for IFC manipulation | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [](https://pypi.org/project/ifcopenshell/) [](https://anaconda.org/conda-forge/ifcopenshell) [](https://anaconda.org/ifcopenshell/ifcopenshell) [](https://hub.docker.com/r/aecgeeks/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell-git) [Pyodide WASM Wheels](https://github.com/IfcOpenShell/wasm-wheels#pyodide-test-wheels) |
|
| [ifcmcp](https://docs.ifcopenshell.org/ifcmcp.html) | MCP server for querying and editing IFC building models | LGPL-3.0-or-later | [](https://pypi.org/project/ifcopenshell-mcp/) |
|
||||||
|
| [ifcopenshell-python](https://docs.ifcopenshell.org/ifcopenshell-python.html) | Python library for IFC manipulation | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [](https://pypi.org/project/ifcopenshell/) [](https://anaconda.org/conda-forge/ifcopenshell) [](https://anaconda.org/ifcopenshell/ifcopenshell) [](https://hub.docker.com/r/aecgeeks/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell-git) [](https://github.com/IfcOpenShell/wasm-wheels) |
|
||||||
| [ifcpatch](https://docs.ifcopenshell.org/ifcpatch.html) | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [](https://pypi.org/project/ifcpatch/) |
|
| [ifcpatch](https://docs.ifcopenshell.org/ifcpatch.html) | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [](https://pypi.org/project/ifcpatch/) |
|
||||||
| [ifcsverchok](https://docs.ifcopenshell.org/ifcsverchok.html) | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
|
| [ifcquery](https://docs.ifcopenshell.org/ifcquery.html) | CLI tool for querying and inspecting IFC building models | LGPL-3.0-or-later | [](https://pypi.org/project/ifcquery/) |
|
||||||
|
| [ifcsverchok](https://docs.ifcopenshell.org/ifcsverchok.html) | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
|
||||||
| [ifctester](https://docs.ifcopenshell.org/ifctester.html) | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [](https://pypi.org/project/ifctester/) |
|
| [ifctester](https://docs.ifcopenshell.org/ifctester.html) | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [](https://pypi.org/project/ifctester/) |
|
||||||
|
|
||||||
The IfcOpenShell C++ codebase is split into multiple interal libraries:
|
The IfcOpenShell C++ codebase is split into multiple interal libraries:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import hashlib
|
|||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
|
import subprocess
|
||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
from urllib import request
|
from urllib import request
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ from github import Github
|
|||||||
|
|
||||||
|
|
||||||
def get_repo_tag_names() -> list[str]:
|
def get_repo_tag_names() -> list[str]:
|
||||||
git_return = os.popen("git tag -l").read()
|
git_return = subprocess.check_output("git tag -l", text=True)
|
||||||
tag_names = [tag_name for tag_name in git_return.split("\n") if tag_name]
|
tag_names = [tag_name for tag_name in git_return.split("\n") if tag_name]
|
||||||
print(f"{len(tag_names)} tag_names found in repo")
|
print(f"{len(tag_names)} tag_names found in repo")
|
||||||
return tag_names
|
return tag_names
|
||||||
@@ -78,6 +79,10 @@ def get_release_zip(tag: str) -> tuple[str, str]:
|
|||||||
raise Exception(f"Couldn't find the release matching '{python_version}' and '{TARGET_OS}' in tag '{tag}'.")
|
raise Exception(f"Couldn't find the release matching '{python_version}' and '{TARGET_OS}' in tag '{tag}'.")
|
||||||
|
|
||||||
|
|
||||||
|
def run(command: str) -> None:
|
||||||
|
subprocess.check_output(command)
|
||||||
|
|
||||||
|
|
||||||
start = datetime.datetime.now()
|
start = datetime.datetime.now()
|
||||||
|
|
||||||
URL_CHOCO_PACKAGE = "https://community.chocolatey.org/packages/blender"
|
URL_CHOCO_PACKAGE = "https://community.chocolatey.org/packages/blender"
|
||||||
@@ -97,7 +102,7 @@ should_release = False
|
|||||||
target_release_tag = ""
|
target_release_tag = ""
|
||||||
TARGET_OS = "windows-x64"
|
TARGET_OS = "windows-x64"
|
||||||
|
|
||||||
git_status = os.popen("git status").read()
|
git_status = subprocess.check_output("git status", text=True)
|
||||||
print(git_status)
|
print(git_status)
|
||||||
|
|
||||||
for tag_name in get_repo_tag_names():
|
for tag_name in get_repo_tag_names():
|
||||||
@@ -147,7 +152,7 @@ blenderbim_build_version = target_release_tag.replace("blenderbim-", "")
|
|||||||
|
|
||||||
# url_blenderbim_py3x_win_zip
|
# url_blenderbim_py3x_win_zip
|
||||||
release_zip_file_name, url_blenderbim_py3x_win_zip = get_release_zip(target_release_tag)
|
release_zip_file_name, url_blenderbim_py3x_win_zip = get_release_zip(target_release_tag)
|
||||||
os.popen(f"wget {url_blenderbim_py3x_win_zip} --no-verbose").read()
|
subprocess.check_call(f"wget {url_blenderbim_py3x_win_zip} --no-verbose")
|
||||||
|
|
||||||
# sha256sum_blenderbim_py310_win_zip
|
# sha256sum_blenderbim_py310_win_zip
|
||||||
sha256sum_blenderbim_py3x_win_zip = get_file_sha256_hash(release_zip_file_name)
|
sha256sum_blenderbim_py3x_win_zip = get_file_sha256_hash(release_zip_file_name)
|
||||||
@@ -201,13 +206,13 @@ print("[INFO] inserting dynamic chocolatey package parameters successful")
|
|||||||
print("\n_____ build choco.exe with mono")
|
print("\n_____ build choco.exe with mono")
|
||||||
|
|
||||||
choco_version = "1.1.0"
|
choco_version = "1.1.0"
|
||||||
os.popen(f"wget https://github.com/chocolatey/choco/archive/refs/tags/{choco_version}.tar.gz --quiet").read()
|
run(f"wget https://github.com/chocolatey/choco/archive/refs/tags/{choco_version}.tar.gz --quiet")
|
||||||
os.popen(f"tar -xzf {choco_version}.tar.gz").read()
|
run(f"tar -xzf {choco_version}.tar.gz")
|
||||||
print("choco tar unpack successful")
|
print("choco tar unpack successful")
|
||||||
os.chdir("choco-1.1.0")
|
os.chdir("choco-1.1.0")
|
||||||
os.popen("./build.sh").read()
|
run("./build.sh")
|
||||||
|
|
||||||
os.popen("cp -r build_output/chocolatey /opt/chocolatey").read()
|
run("cp -r build_output/chocolatey /opt/chocolatey")
|
||||||
os.chdir(BLENDERBIM_DIR)
|
os.chdir(BLENDERBIM_DIR)
|
||||||
|
|
||||||
if pathlib.Path("/opt/chocolatey/choco.exe").exists():
|
if pathlib.Path("/opt/chocolatey/choco.exe").exists():
|
||||||
@@ -215,11 +220,15 @@ if pathlib.Path("/opt/chocolatey/choco.exe").exists():
|
|||||||
|
|
||||||
print("\n_____ build choco pack")
|
print("\n_____ build choco pack")
|
||||||
|
|
||||||
os.popen("mono /opt/chocolatey/choco.exe pack --allow-unofficial").read()
|
run("mono /opt/chocolatey/choco.exe pack --allow-unofficial")
|
||||||
os.popen('mono /opt/chocolatey/choco.exe setapikey --key="{choco_token}" --source="https://push.chocolatey.org/" --allow-unofficial').read()
|
run(
|
||||||
|
'mono /opt/chocolatey/choco.exe setapikey --key="{choco_token}" --source="https://push.chocolatey.org/" --allow-unofficial'
|
||||||
|
)
|
||||||
|
|
||||||
print("\n_____ build choco push")
|
print("\n_____ build choco push")
|
||||||
os.popen('mono /opt/chocolatey/choco.exe push --source="https://push.chocolatey.org/" --key="$CHOCO_TOKEN" --allow-unofficial --verbose').read()
|
run(
|
||||||
|
'mono /opt/chocolatey/choco.exe push --source="https://push.chocolatey.org/" --key="$CHOCO_TOKEN" --allow-unofficial --verbose'
|
||||||
|
)
|
||||||
|
|
||||||
print(f"choco push of version: {target_release_tag} successful!")
|
print(f"choco push of version: {target_release_tag} successful!")
|
||||||
print(f"it took: {datetime.datetime.now() - start}")
|
print(f"it took: {datetime.datetime.now() - start}")
|
||||||
|
|||||||
+14
-9
@@ -1,4 +1,6 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/python
|
||||||
|
# /// script
|
||||||
|
# ///
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# #
|
# #
|
||||||
# This file is part of IfcOpenShell. #
|
# This file is part of IfcOpenShell. #
|
||||||
@@ -126,13 +128,7 @@ from collections.abc import Generator, Sequence
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.request import urlretrieve
|
from urllib.request import urlretrieve
|
||||||
|
|
||||||
try:
|
from typing import Literal, Union
|
||||||
from typing import Literal, Union
|
|
||||||
except:
|
|
||||||
# python 3.6 compatibility for rocky 8
|
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
from typing_extensions import Literal
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
@@ -1094,10 +1090,19 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
|
|||||||
f"http://www.python.org/ftp/python/{PYTHON_VERSION}/",
|
f"http://www.python.org/ftp/python/{PYTHON_VERSION}/",
|
||||||
f"Python-{PYTHON_VERSION}.tgz",
|
f"Python-{PYTHON_VERSION}.tgz",
|
||||||
)
|
)
|
||||||
python_bin = INSTALL_DIR / f"python-{PYTHON_VERSION}" / "bin" / "python3"
|
python_install = INSTALL_DIR / f"python-{PYTHON_VERSION}"
|
||||||
|
python_bin = python_install / "bin" / "python3"
|
||||||
# `_ssl` module is present -> we will be able to install `numpy` later
|
# `_ssl` module is present -> we will be able to install `numpy` later
|
||||||
# to verify IfcOpenShell installation
|
# to verify IfcOpenShell installation
|
||||||
|
try:
|
||||||
run([str(python_bin), "-c", "import _ssl"])
|
run([str(python_bin), "-c", "import _ssl"])
|
||||||
|
except RuntimeError:
|
||||||
|
print(
|
||||||
|
"ERROR: Python was built without SSL support (_ssl module is missing). "
|
||||||
|
f"To fix this: remove the installed Python at {python_install}; "
|
||||||
|
"install OpenSSL development libraries and re-run."
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
if MAC_CROSS_COMPILE_INTEL:
|
if MAC_CROSS_COMPILE_INTEL:
|
||||||
assert original_path
|
assert original_path
|
||||||
@@ -1515,7 +1520,7 @@ if "IfcOpenShell-Python" in targets:
|
|||||||
)
|
)
|
||||||
# Copy setup.py where pyodide build system expects it.
|
# Copy setup.py where pyodide build system expects it.
|
||||||
shutil.copy(REPO_PATH / "pyodide" / "setup.py", REPO_PATH)
|
shutil.copy(REPO_PATH / "pyodide" / "setup.py", REPO_PATH)
|
||||||
# Empty pyproject so it's contents won't affect the resulting wheelthe the
|
# Empty pyproject so it's contents won't affect the resulting wheel
|
||||||
# otherwise the wheel will use version and dependencies from toml, not setup.py.
|
# otherwise the wheel will use version and dependencies from toml, not setup.py.
|
||||||
(REPO_PATH / "pyproject.toml").write_text("")
|
(REPO_PATH / "pyproject.toml").write_text("")
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# /// script
|
||||||
|
# ///
|
||||||
"""
|
"""
|
||||||
Cache built dependencies for builds.
|
Cache built dependencies for builds.
|
||||||
|
|
||||||
@@ -41,6 +43,9 @@ def pack_dependencies(install_dir: Path) -> None:
|
|||||||
if not dependency_path.is_dir():
|
if not dependency_path.is_dir():
|
||||||
continue
|
continue
|
||||||
dependency_name = dependency_path.name
|
dependency_name = dependency_path.name
|
||||||
|
# Skip ifcopenshell - it's a build output, not a dependency to reuse across builds.
|
||||||
|
if dependency_name == "ifcopenshell":
|
||||||
|
continue
|
||||||
tar_path = install_dir / f"{CACHE_PREFIX}{dependency_name}.tar.gz"
|
tar_path = install_dir / f"{CACHE_PREFIX}{dependency_name}.tar.gz"
|
||||||
if tar_path.exists():
|
if tar_path.exists():
|
||||||
print(f"Skipping existing cache: '{tar_path}'")
|
print(f"Skipping existing cache: '{tar_path}'")
|
||||||
|
|||||||
+11
-12
@@ -1,6 +1,11 @@
|
|||||||
#!/usr/bin/bash
|
#!/usr/bin/bash
|
||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
|
PYODIDE_VERSION=0.29.3
|
||||||
|
PYODIDE_BUILD_VERSION=0.33.0
|
||||||
|
PYODIDE_XBUILDENV_ROOT="${HOME}/.cache/.pyodide-xbuildenv-${PYODIDE_BUILD_VERSION}"
|
||||||
|
PYODIDE_XBUILDENV="${PYODIDE_XBUILDENV_ROOT}/${PYODIDE_VERSION}"
|
||||||
|
|
||||||
# Script is assuming that it will be possible to execute it multiple times
|
# Script is assuming that it will be possible to execute it multiple times
|
||||||
# therefore we're clearing venv each time and ignoring existing 'emsdk' folder.
|
# therefore we're clearing venv each time and ignoring existing 'emsdk' folder.
|
||||||
|
|
||||||
@@ -11,21 +16,15 @@ source .venv/bin/activate
|
|||||||
|
|
||||||
# Install pyodide cross build environment.
|
# Install pyodide cross build environment.
|
||||||
# Instructions: https://pyodide.org/en/stable/development/building-packages.html
|
# Instructions: https://pyodide.org/en/stable/development/building-packages.html
|
||||||
uv pip install pyodide-build
|
uv pip install "pyodide-build==${PYODIDE_BUILD_VERSION}"
|
||||||
# `uv run` is required, so xbuildenv would skip using `pip`.
|
# `uv run` is required, so xbuildenv would skip using `pip`.
|
||||||
uv run pyodide xbuildenv install
|
uv run pyodide xbuildenv install "${PYODIDE_VERSION}"
|
||||||
|
uv run pyodide xbuildenv install-emscripten
|
||||||
|
|
||||||
# Emscripten doesn't come with xbuildenv.
|
EMSDK_ROOT="${PYODIDE_XBUILDENV}/emsdk"
|
||||||
if [ ! -d emsdk ]; then
|
source "${EMSDK_ROOT}/emsdk_env.sh"
|
||||||
git clone https://github.com/emscripten-core/emsdk
|
|
||||||
fi
|
|
||||||
pushd emsdk
|
|
||||||
PYODIDE_EMSCRIPTEN_VERSION=$(pyodide config get emscripten_version)
|
|
||||||
./emsdk install ${PYODIDE_EMSCRIPTEN_VERSION}
|
|
||||||
./emsdk activate ${PYODIDE_EMSCRIPTEN_VERSION}
|
|
||||||
source emsdk_env.sh
|
|
||||||
which emcc
|
which emcc
|
||||||
popd
|
emcc --version
|
||||||
|
|
||||||
mkdir -p packages/ifcopenshell
|
mkdir -p packages/ifcopenshell
|
||||||
VERSION=`cat IfcOpenShell/VERSION`
|
VERSION=`cat IfcOpenShell/VERSION`
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
#
|
||||||
|
# /// script
|
||||||
|
# # Latest Pyodide build env versions are listed here:
|
||||||
|
# # https://pyodide.github.io/pyodide/api/pyodide-cross-build-environments.json
|
||||||
|
# # https://github.com/pyodide/pyodide-build/blob/main/pyodide_build/xbuildenv_releases.py
|
||||||
|
# requires-python = "==3.13.2"
|
||||||
|
# dependencies = [
|
||||||
|
# "requests",
|
||||||
|
# "setuptools",
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
"""
|
||||||
|
Pack an IfcOpenShell WASM wheel using Pyodide build system.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
uv run make_wheel.py # Show this help
|
||||||
|
uv run make_wheel.py --build # Build wheel
|
||||||
|
uv run make_wheel.py --clean # Clean build artifacts and exit
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Get repo root (parent of this script's parent directory)
|
||||||
|
REPO_ROOT = Path(__file__).parent.parent
|
||||||
|
PYODIDE_DIR = REPO_ROOT / "pyodide"
|
||||||
|
BUILD_DIR = PYODIDE_DIR / "build"
|
||||||
|
|
||||||
|
# Hardcoded path (Windows packing workaround with --dev flag)
|
||||||
|
PYODIDE_BUILD = Path(r"L:\Projects\Github\pyodide-build")
|
||||||
|
|
||||||
|
# Wheel platform tag (from PYODIDE_EMSCRIPTEN_VERSION in pyodide-build/Makefile.envs)
|
||||||
|
WHEEL_PLATFORM_TAG = "emscripten_4_0_9_wasm32"
|
||||||
|
|
||||||
|
# Location where ifcopenshell will be extracted
|
||||||
|
IFCOPENSHELL_DIR = PYODIDE_DIR / "ifcopenshell"
|
||||||
|
|
||||||
|
|
||||||
|
class WheelBuilder:
|
||||||
|
@staticmethod
|
||||||
|
def extract_ifcopenshell_from_git(dst: Path) -> None:
|
||||||
|
"""Extract ifcopenshell directory from git repo into destination."""
|
||||||
|
Tools.rmrf(dst)
|
||||||
|
|
||||||
|
print(f"Extracting ifcopenshell from git to {dst}...")
|
||||||
|
# Use git ls-files piped to git checkout-index to avoid copying
|
||||||
|
# untracked or ignored files from the actual repo.
|
||||||
|
ls_proc = subprocess.Popen(
|
||||||
|
["git", "ls-files", "-z", "src/ifcopenshell-python/ifcopenshell"],
|
||||||
|
cwd=REPO_ROOT,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
checkout_proc = subprocess.Popen(
|
||||||
|
["git", "checkout-index", "-z", "--prefix", "pyodide/", "--stdin"],
|
||||||
|
cwd=REPO_ROOT,
|
||||||
|
stdin=ls_proc.stdout,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
assert ls_proc.stdout is not None
|
||||||
|
ls_proc.stdout.close()
|
||||||
|
checkout_proc.communicate()
|
||||||
|
|
||||||
|
if checkout_proc.returncode != 0:
|
||||||
|
assert checkout_proc.stderr is not None
|
||||||
|
raise RuntimeError(f"Failed to extract: {checkout_proc.stderr.decode()}")
|
||||||
|
|
||||||
|
# Move src/ifcopenshell-python/ifcopenshell to ifcopenshell.
|
||||||
|
temp_src = PYODIDE_DIR / "src" / "ifcopenshell-python" / "ifcopenshell"
|
||||||
|
shutil.move(temp_src, dst)
|
||||||
|
|
||||||
|
# Clean up temporary src directory.
|
||||||
|
Tools.rmrf(PYODIDE_DIR / "src")
|
||||||
|
|
||||||
|
print("✓ Extracted ifcopenshell from git")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_wheel_url(makefile_path: Path) -> str:
|
||||||
|
"""Get S3 wheel URL based on BINARY_VERSION and BUILD_COMMIT from Makefile."""
|
||||||
|
|
||||||
|
def parse_makefile_vars() -> dict[str, str]:
|
||||||
|
content = makefile_path.read_text()
|
||||||
|
vars: dict[str, str] = {}
|
||||||
|
for match in re.finditer(r"^(BINARY_VERSION|BUILD_COMMIT):=(.+)$", content, re.MULTILINE):
|
||||||
|
vars[match.group(1)] = match.group(2).strip()
|
||||||
|
return vars
|
||||||
|
|
||||||
|
vars: dict[str, str] = parse_makefile_vars()
|
||||||
|
binary_version = vars["BINARY_VERSION"]
|
||||||
|
build_commit = vars["BUILD_COMMIT"]
|
||||||
|
filename = f"ifcopenshell-{binary_version}+{build_commit}-cp313-cp313-pyodide_2025_0_wasm32.whl"
|
||||||
|
encoded_filename = quote(filename, safe="")
|
||||||
|
return f"https://s3.amazonaws.com/ifcopenshell-builds/{encoded_filename}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def download_and_extract_so(url: str, build_dir: Path) -> tuple[Path, Path]:
|
||||||
|
"""Download wheel from URL and extract .so and .py files."""
|
||||||
|
py_wrapper_filename = "ifcopenshell_wrapper.py"
|
||||||
|
build_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
wheel_path = build_dir / url.rsplit("/", 1)[-1]
|
||||||
|
|
||||||
|
if wheel_path.exists():
|
||||||
|
print(f"Using cached wheel: {wheel_path}")
|
||||||
|
else:
|
||||||
|
print(f"Downloading {url}...")
|
||||||
|
response = requests.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
wheel_path.write_bytes(response.content)
|
||||||
|
|
||||||
|
print("Extracting _ifcopenshell_wrapper files...")
|
||||||
|
with zipfile.ZipFile(wheel_path) as zf:
|
||||||
|
so_files = [f for f in zf.namelist() if f.endswith(".so")]
|
||||||
|
py_files = [f for f in zf.namelist() if f.endswith(py_wrapper_filename)]
|
||||||
|
|
||||||
|
assert so_files, "No .so file found in wheel"
|
||||||
|
assert py_files, f"No {py_wrapper_filename} file found in wheel"
|
||||||
|
|
||||||
|
so_file = so_files[0]
|
||||||
|
so_dst = build_dir / Path(so_file).name
|
||||||
|
so_dst.write_bytes(zf.read(so_file))
|
||||||
|
|
||||||
|
py_file = py_files[0]
|
||||||
|
py_dst = build_dir / Path(py_file).name
|
||||||
|
py_dst.write_bytes(zf.read(py_file))
|
||||||
|
|
||||||
|
return so_dst, py_dst
|
||||||
|
|
||||||
|
|
||||||
|
class Tools:
|
||||||
|
@staticmethod
|
||||||
|
def run(
|
||||||
|
cmd: list[str],
|
||||||
|
cwd: Path | None = None,
|
||||||
|
) -> None:
|
||||||
|
print(f"$ {' '.join(cmd)}")
|
||||||
|
subprocess.check_call(cmd, cwd=cwd)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_symlink(dst: Path, src: Path) -> None:
|
||||||
|
Tools.rmrf(dst)
|
||||||
|
dst.symlink_to(src)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def rmrf(path: Path) -> None:
|
||||||
|
if path.exists() or path.is_symlink():
|
||||||
|
if path.is_dir() and not path.is_symlink():
|
||||||
|
shutil.rmtree(path)
|
||||||
|
else:
|
||||||
|
path.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def clean() -> None:
|
||||||
|
"""Remove build artifacts."""
|
||||||
|
paths_to_remove = (
|
||||||
|
BUILD_DIR,
|
||||||
|
PYODIDE_DIR / ".pyodide_build",
|
||||||
|
PYODIDE_DIR / "dist",
|
||||||
|
PYODIDE_DIR / "ifcopenshell.egg-info",
|
||||||
|
PYODIDE_DIR / "src",
|
||||||
|
IFCOPENSHELL_DIR,
|
||||||
|
)
|
||||||
|
for path in paths_to_remove:
|
||||||
|
if path.exists() or path.is_symlink():
|
||||||
|
print(f"Removing {path}...")
|
||||||
|
Tools.rmrf(path)
|
||||||
|
print("✓ Clean complete")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__, add_help=False)
|
||||||
|
parser.add_argument("--build", action="store_true", help="Build the wheel")
|
||||||
|
parser.add_argument("--clean", action="store_true", help="Clean build folder")
|
||||||
|
parser.add_argument(
|
||||||
|
"--dev",
|
||||||
|
action="store_true",
|
||||||
|
help="Use editable pyodide-build from hardcoded path (Windows packing workaround)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.build and not args.clean:
|
||||||
|
print(__doc__)
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.clean:
|
||||||
|
clean()
|
||||||
|
return
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
WheelBuilder.extract_ifcopenshell_from_git(IFCOPENSHELL_DIR)
|
||||||
|
|
||||||
|
print("Downloading and extracting _ifcopenshell_wrapper files...")
|
||||||
|
makefile = REPO_ROOT / "src" / "ifcopenshell-python" / "Makefile"
|
||||||
|
wheel_url = WheelBuilder.get_wheel_url(makefile)
|
||||||
|
so_file, py_file = WheelBuilder.download_and_extract_so(wheel_url, BUILD_DIR)
|
||||||
|
|
||||||
|
Tools.create_symlink(IFCOPENSHELL_DIR / Path(so_file).name, so_file)
|
||||||
|
Tools.create_symlink(IFCOPENSHELL_DIR / Path(py_file).name, py_file)
|
||||||
|
|
||||||
|
print("Installing pyodide-build...")
|
||||||
|
if args.dev:
|
||||||
|
Tools.run(["uv", "pip", "install", "-e", str(PYODIDE_BUILD)])
|
||||||
|
else:
|
||||||
|
Tools.run(["uv", "pip", "install", "pyodide-build"])
|
||||||
|
|
||||||
|
print("Building with pyodide...")
|
||||||
|
# Use --no-isolation due to pyodide-build Windows support issues:
|
||||||
|
# symlink_unisolated_packages fails with missing `_sysconfigdata_$(CPYTHON_ABI_FLAGS)_emscripten_wasm32-emscripten.py`.
|
||||||
|
# Hardcode platform name since pyodide doesn't yet support overriding wheel tags on Windows.
|
||||||
|
#
|
||||||
|
# Use `LEGACY_PLATFORM` since pyodide 0.34.1 introduced new tag for wheels `pyemscripten`,
|
||||||
|
# which doesn't work with pyodide itself yet - https://github.com/pyodide/pyodide/issues/6177.
|
||||||
|
os.environ["USE_LEGACY_PLATFORM"] = "1"
|
||||||
|
Tools.run(["pyodide", "build", f"-C--build-option=--plat-name={WHEEL_PLATFORM_TAG}"])
|
||||||
|
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
print(f"\n✓ Done! ({elapsed:.1f}s)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+39
-1
@@ -2,12 +2,16 @@
|
|||||||
# because `tool.setuptools.ext-modules` is still experimental in pyproject.toml
|
# because `tool.setuptools.ext-modules` is still experimental in pyproject.toml
|
||||||
# and we need it to get the wheel suffix right.
|
# and we need it to get the wheel suffix right.
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import tomllib
|
import tomllib
|
||||||
from setuptools import Extension, find_packages, setup
|
from setuptools import Extension, find_packages, setup
|
||||||
|
from setuptools.command.build_ext import build_ext
|
||||||
|
|
||||||
REPO_FOLDER = Path(__file__).parent
|
# Detect repo folder: if setup.py is in pyodide folder, go to parent
|
||||||
|
SETUP_DIR = Path(__file__).parent
|
||||||
|
REPO_FOLDER = SETUP_DIR.parent if SETUP_DIR.name == "pyodide" else SETUP_DIR
|
||||||
|
|
||||||
|
|
||||||
def get_version() -> str:
|
def get_version() -> str:
|
||||||
@@ -25,6 +29,39 @@ def get_dependencies() -> list[str]:
|
|||||||
return dependencies
|
return dependencies
|
||||||
|
|
||||||
|
|
||||||
|
class UnixBuildExt(build_ext):
|
||||||
|
"""Customize ``build_ext`` to support packing on Windows."""
|
||||||
|
|
||||||
|
def finalize_options(self):
|
||||||
|
from distutils import sysconfig
|
||||||
|
|
||||||
|
super().finalize_options()
|
||||||
|
if sys.platform == "win32":
|
||||||
|
self.compiler = "unix"
|
||||||
|
|
||||||
|
# Configure sysconfig for Windows builds
|
||||||
|
# CCSHARED is the only variable that's not customizable with env vars.
|
||||||
|
# Basically avoiding this:
|
||||||
|
# File ".venv\Lib\site-packages\setuptools\_distutils\sysconfig.py", line 366, in customize_compiler
|
||||||
|
# compiler_so=cc_cmd + ' ' + ccshared,
|
||||||
|
# ~~~~~~~~~~~~~^~~~~~~~~~
|
||||||
|
# TypeError: can only concatenate str (not "NoneType") to str
|
||||||
|
sysconfig.get_config_vars() # Initialize config cache
|
||||||
|
if sysconfig._config_vars.get("CCSHARED") is None:
|
||||||
|
sysconfig._config_vars["CCSHARED"] = "-fPIC"
|
||||||
|
# Override compiler type before it's instantiated
|
||||||
|
|
||||||
|
# Set Emscripten compiler environment variables
|
||||||
|
os.environ["CC"] = "emcc"
|
||||||
|
os.environ["CXX"] = "em++"
|
||||||
|
os.environ["CFLAGS"] = ""
|
||||||
|
os.environ["CXXFLAGS"] = ""
|
||||||
|
os.environ["LDSHARED"] = "emcc -shared"
|
||||||
|
os.environ["AR"] = "emar"
|
||||||
|
os.environ["ARFLAGS"] = "rcs"
|
||||||
|
os.environ["SETUPTOOLS_EXT_SUFFIX"] = ".cpython-313-wasm32-emscripten.so"
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="ifcopenshell",
|
name="ifcopenshell",
|
||||||
version=get_version(),
|
version=get_version(),
|
||||||
@@ -44,4 +81,5 @@ setup(
|
|||||||
},
|
},
|
||||||
# Has to provide extension to get the correct wheel suffix.
|
# Has to provide extension to get the correct wheel suffix.
|
||||||
ext_modules=[Extension("ifcopenshell._ifcopenshell_wrapper", sources=[])],
|
ext_modules=[Extension("ifcopenshell._ifcopenshell_wrapper", sources=[])],
|
||||||
|
cmdclass={"build_ext": UnixBuildExt},
|
||||||
)
|
)
|
||||||
|
|||||||
+179
-6
@@ -3,8 +3,9 @@ name = "IfcOpenShell"
|
|||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"black==26.3.1",
|
"black==26.3.1",
|
||||||
"ruff==0.15.6",
|
"ruff==0.15.12",
|
||||||
"poethepoet",
|
"poethepoet",
|
||||||
|
"ty==0.0.32",
|
||||||
"gersemi==0.26.1",
|
"gersemi==0.26.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -28,6 +29,9 @@ extend-exclude = '''
|
|||||||
reportInvalidTypeForm = false
|
reportInvalidTypeForm = false
|
||||||
disableBytesTypePromotions = true
|
disableBytesTypePromotions = true
|
||||||
reportUnnecessaryTypeIgnoreComment = true
|
reportUnnecessaryTypeIgnoreComment = true
|
||||||
|
reportRedeclaration = false
|
||||||
|
# Ignore warnings from bpy stubs missing actual source files.
|
||||||
|
reportMissingModuleSource = false
|
||||||
# Pylance doesn't respect gitignore, so we have to exclude files manually here
|
# Pylance doesn't respect gitignore, so we have to exclude files manually here
|
||||||
# to avoid VS Code slowing down.
|
# to avoid VS Code slowing down.
|
||||||
# https://github.com/microsoft/pylance-release/issues/5169
|
# https://github.com/microsoft/pylance-release/issues/5169
|
||||||
@@ -78,15 +82,184 @@ ignore = [
|
|||||||
"UP032", # Replace .format with f-string
|
"UP032", # Replace .format with f-string
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.ty.rules]
|
||||||
|
all = "ignore"
|
||||||
|
|
||||||
|
# Structural rules (no deep type inference needed, easier to adapt).
|
||||||
|
abstract-method-in-final-class = "error"
|
||||||
|
ambiguous-protocol-member = "error"
|
||||||
|
conflicting-declarations = "error"
|
||||||
|
conflicting-metaclass = "error"
|
||||||
|
cyclic-class-definition = "error"
|
||||||
|
cyclic-type-alias-definition = "error"
|
||||||
|
dataclass-field-order = "error"
|
||||||
|
duplicate-base = "error"
|
||||||
|
duplicate-kw-only = "error"
|
||||||
|
empty-body = "error"
|
||||||
|
escape-character-in-forward-annotation = "error"
|
||||||
|
final-on-non-method = "error"
|
||||||
|
final-without-value = "error"
|
||||||
|
ignore-comment-unknown-rule = "error"
|
||||||
|
implicit-concatenated-string-type-annotation = "error"
|
||||||
|
inconsistent-mro = "error"
|
||||||
|
ineffective-final = "error"
|
||||||
|
instance-layout-conflict = "error"
|
||||||
|
invalid-dataclass = "error"
|
||||||
|
invalid-dataclass-override = "error"
|
||||||
|
invalid-enum-member-annotation = "error"
|
||||||
|
invalid-explicit-override = "error"
|
||||||
|
invalid-frozen-dataclass-subclass = "error"
|
||||||
|
invalid-generic-class = "error"
|
||||||
|
invalid-generic-enum = "error"
|
||||||
|
invalid-ignore-comment = "error"
|
||||||
|
invalid-legacy-positional-parameter = "error"
|
||||||
|
invalid-legacy-type-variable = "error"
|
||||||
|
invalid-named-tuple = "error"
|
||||||
|
invalid-newtype = "error"
|
||||||
|
invalid-overload = "error"
|
||||||
|
invalid-paramspec = "error"
|
||||||
|
invalid-protocol = "error"
|
||||||
|
invalid-syntax-in-forward-annotation = "error"
|
||||||
|
invalid-total-ordering = "error"
|
||||||
|
invalid-type-alias-type = "error"
|
||||||
|
invalid-type-checking-constant = "error"
|
||||||
|
invalid-type-guard-definition = "error"
|
||||||
|
invalid-type-variable-bound = "error"
|
||||||
|
invalid-type-variable-constraints = "error"
|
||||||
|
invalid-typed-dict-header = "error"
|
||||||
|
invalid-typed-dict-statement = "error"
|
||||||
|
override-of-final-method = "error"
|
||||||
|
override-of-final-variable = "error"
|
||||||
|
possibly-missing-import = "error"
|
||||||
|
possibly-missing-submodule = "error"
|
||||||
|
# Has false positives due to ty walrus operator bug.
|
||||||
|
# possibly-unresolved-reference = "error"
|
||||||
|
raw-string-type-annotation = "error"
|
||||||
|
redundant-final-classvar = "error"
|
||||||
|
shadowed-type-variable = "error"
|
||||||
|
subclass-of-final-class = "error"
|
||||||
|
super-call-in-named-tuple-method = "error"
|
||||||
|
unavailable-implicit-super-arguments = "error"
|
||||||
|
unbound-type-variable = "error"
|
||||||
|
undefined-reveal = "error"
|
||||||
|
unresolved-global = "error"
|
||||||
|
unresolved-import = "error"
|
||||||
|
unresolved-reference = "error"
|
||||||
|
unused-ignore-comment = "error"
|
||||||
|
unused-type-ignore-comment = "error"
|
||||||
|
useless-overload-body = "error"
|
||||||
|
|
||||||
|
# Non-structural rules:
|
||||||
|
deprecated = "error"
|
||||||
|
zero-stepsize-in-slice = "error"
|
||||||
|
possibly-missing-implicit-call = "error"
|
||||||
|
unused-awaitable = "error"
|
||||||
|
|
||||||
|
# Function argument rules:
|
||||||
|
# Conflicts with `ifcopenshell.api.geometry.add_representation` type of callables we have, confusing them with a module.
|
||||||
|
# call-non-callable = "error"
|
||||||
|
conflicting-argument-forms = "error"
|
||||||
|
# Too many false positives.
|
||||||
|
# invalid-argument-type = "error"
|
||||||
|
missing-argument = "error"
|
||||||
|
parameter-already-assigned = "error"
|
||||||
|
positional-only-parameter-as-kwarg = "error"
|
||||||
|
too-many-positional-arguments = "error"
|
||||||
|
unknown-argument = "error"
|
||||||
|
# Has a lot of warnings due to current ty walrus operator issues.
|
||||||
|
# index-out-of-bounds = "error"
|
||||||
|
# unresolved-attribute = "error"
|
||||||
|
|
||||||
|
[tool.ty.environment]
|
||||||
|
extra-paths = [
|
||||||
|
"src/bonsai/external_dependencies",
|
||||||
|
"src/bcf",
|
||||||
|
"src/bsdd",
|
||||||
|
"src/bonsai",
|
||||||
|
"src/ifc4d",
|
||||||
|
"src/ifc5d",
|
||||||
|
"src/ifccityjson",
|
||||||
|
"src/ifcclash",
|
||||||
|
"src/ifccsv",
|
||||||
|
"src/ifcdiff",
|
||||||
|
"src/ifcfm",
|
||||||
|
"src/ifcopenshell-python",
|
||||||
|
"src/ifcpatch",
|
||||||
|
"src/ifctester",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ty.src]
|
||||||
|
exclude = [
|
||||||
|
# External dependencies cloned for type checking only.
|
||||||
|
"src/bonsai/external_dependencies",
|
||||||
|
# Submodules.
|
||||||
|
"src/ifcopenshell-python/ifcopenshell/express",
|
||||||
|
"src/ifcopenshell-python/ifcopenshell/mvd",
|
||||||
|
"src/ifcopenshell-python/ifcopenshell/simple_spf",
|
||||||
|
"src/svgfill/3rdparty",
|
||||||
|
# Has special dependencies.
|
||||||
|
"src/ifcopenshell-python/ifcopenshell/geom/app.py",
|
||||||
|
"src/ifcopenshell-python/ifcopenshell/geom/code_editor_pane.py",
|
||||||
|
"src/ifcopenshell-python/ifcopenshell/util/doc.py",
|
||||||
|
"src/ifcopenshell-python/ifcopenshell/util/generate_pset_templates.py",
|
||||||
|
"src/ifcopenshell-python/ifcopenshell/util/ifc4x3dev_scrape_data_for_docs.py",
|
||||||
|
# Too esoteric.
|
||||||
|
"src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py",
|
||||||
|
"src/ifc2ca/templates",
|
||||||
|
# Too dev.
|
||||||
|
"src/bcf/setup.py",
|
||||||
|
"src/bsdd/yml_to_classes.py",
|
||||||
|
# Deprecated.
|
||||||
|
"src/ifc2ca/_deprecated",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.poe.tasks]
|
[tool.poe.tasks]
|
||||||
|
|
||||||
ruff-main = "ruff check --extend-exclude nix/build-all.py"
|
ruff = "ruff check"
|
||||||
# It's actually Python 3.6, but ruff only supports 3.7+, but it should do.
|
|
||||||
ruff-old = "ruff check nix/build-all.py --target-version py37"
|
|
||||||
ruff.sequence = ["ruff-main", "ruff-old"]
|
|
||||||
|
|
||||||
black = "black ."
|
black = "black ."
|
||||||
|
|
||||||
format.sequence = ["black", "ruff-main", "ruff-old"]
|
ty.sequence = ["ty-bonsai", "ty-ios"]
|
||||||
|
ty.help = "Run ty type checker. Requires ty-venv to be set up first."
|
||||||
|
ty-bonsai = "ty check src/bonsai --python=src/bonsai/.venv"
|
||||||
|
|
||||||
|
ty-venv.sequence = ["bonsai-deps", "ty-venv-bonsai", "ty-venv-ios"]
|
||||||
|
|
||||||
|
ty-venv-bonsai.sequence = [
|
||||||
|
{cmd = "uv venv src/bonsai/.venv --python=3.11 --allow-existing"},
|
||||||
|
{cmd = "uv pip install -r src/bonsai/type-check-requirements.txt --python=src/bonsai/.venv"},
|
||||||
|
]
|
||||||
|
|
||||||
|
ty-venv-ios.sequence = [
|
||||||
|
{cmd = "uv venv src/ifcopenshell-python/.venv --python=3.10 --allow-existing"},
|
||||||
|
{cmd = "uv pip install -r src/ifcopenshell-python/type-check-requirements.txt --python=src/ifcopenshell-python/.venv"},
|
||||||
|
]
|
||||||
|
|
||||||
|
format.sequence = ["black", "ruff"]
|
||||||
|
|
||||||
cmake-format = "gersemi . --in-place"
|
cmake-format = "gersemi . --in-place"
|
||||||
|
|
||||||
|
[tool.poe.tasks.ty-ios]
|
||||||
|
# --ignore unresolved-reference: walrus operator false positives in ty.
|
||||||
|
cmd = """
|
||||||
|
ty check
|
||||||
|
src/bcf
|
||||||
|
src/bsdd
|
||||||
|
src/ifc2ca
|
||||||
|
src/ifc4d
|
||||||
|
src/ifc5d
|
||||||
|
src/ifccityjson
|
||||||
|
src/ifcclash
|
||||||
|
src/ifccsv
|
||||||
|
src/ifcdiff
|
||||||
|
src/ifcfm
|
||||||
|
src/ifcopenshell-python
|
||||||
|
src/ifcpatch
|
||||||
|
src/ifctester
|
||||||
|
--python=src/ifcopenshell-python/.venv
|
||||||
|
--ignore unresolved-reference
|
||||||
|
"""
|
||||||
|
|
||||||
|
[tool.poe.tasks.bonsai-deps]
|
||||||
|
help = "Clone or update Bonsai external dependencies."
|
||||||
|
cmd = "python src/bonsai/scripts/bonsai_deps.py"
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ client_id, client_secret = "", ""
|
|||||||
class OAuthReceiver(http.server.BaseHTTPRequestHandler):
|
class OAuthReceiver(http.server.BaseHTTPRequestHandler):
|
||||||
def do_GET(self) -> None:
|
def do_GET(self) -> None:
|
||||||
query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
|
query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
|
||||||
self.server.auth_code = query.get("code", [""])[0] # type: ignore
|
self.server.auth_code = query.get("code", [""])[0]
|
||||||
self.server.auth_state = query.get("state", [""])[0] # type: ignore
|
self.server.auth_state = query.get("state", [""])[0]
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-type", "text/plain")
|
self.send_header("Content-type", "text/plain")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
@@ -255,7 +255,7 @@ class BcfClient:
|
|||||||
project_id: str = "",
|
project_id: str = "",
|
||||||
topics: str = "",
|
topics: str = "",
|
||||||
query_string: Optional[str] = None,
|
query_string: Optional[str] = None,
|
||||||
) -> list[Any]:
|
) -> None:
|
||||||
# return self.get(
|
# return self.get(
|
||||||
# f"/projects/{project_id}/topics",
|
# f"/projects/{project_id}/topics",
|
||||||
# {
|
# {
|
||||||
|
|||||||
@@ -173,16 +173,17 @@ def assert_viewpoints(viewpoints):
|
|||||||
assert viewpoint.snapshot is not None
|
assert viewpoint.snapshot is not None
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: dead code - ported from v2 but buildingSMART/BCF-XML has no v3 MaximumInformation.bcf equivalent
|
||||||
def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
|
def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
|
||||||
expected_vp = mdl.VisualizationInfo(
|
expected_vp = mdl.VisualizationInfo(
|
||||||
components=mdl.Components(
|
components=mdl.Components(
|
||||||
|
selection=expected_selection,
|
||||||
|
visibility=mdl.ComponentVisibility(
|
||||||
view_setup_hints=mdl.ViewSetupHints(
|
view_setup_hints=mdl.ViewSetupHints(
|
||||||
spaces_visible=False,
|
spaces_visible=False,
|
||||||
space_boundaries_visible=False,
|
space_boundaries_visible=False,
|
||||||
openings_visible=False,
|
openings_visible=False,
|
||||||
),
|
),
|
||||||
selection=expected_selection,
|
|
||||||
visibility=mdl.ComponentVisibility(
|
|
||||||
exceptions=expected_exception,
|
exceptions=expected_exception,
|
||||||
default_visibility=False,
|
default_visibility=False,
|
||||||
),
|
),
|
||||||
@@ -193,6 +194,7 @@ def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, e
|
|||||||
camera_direction=mdl.Direction(x=0.6745243072509766, y=-0.6599355936050415, z=-0.33091068267822266),
|
camera_direction=mdl.Direction(x=0.6745243072509766, y=-0.6599355936050415, z=-0.33091068267822266),
|
||||||
camera_up_vector=mdl.Direction(x=0.2271970510482788, y=-0.24091780185699463, z=0.9435783624649048),
|
camera_up_vector=mdl.Direction(x=0.2271970510482788, y=-0.24091780185699463, z=0.9435783624649048),
|
||||||
field_of_view=60,
|
field_of_view=60,
|
||||||
|
aspect_ratio=1.0,
|
||||||
),
|
),
|
||||||
guid="21dd4807-e9af-439e-a980-04d913a6b1ce",
|
guid="21dd4807-e9af-439e-a980-04d913a6b1ce",
|
||||||
)
|
)
|
||||||
@@ -200,16 +202,17 @@ def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, e
|
|||||||
assert viewpoint.snapshot is not None
|
assert viewpoint.snapshot is not None
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: dead code - ported from v2 but buildingSMART/BCF-XML has no v3 MaximumInformation.bcf equivalent
|
||||||
def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
|
def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
|
||||||
expected_vp = mdl.VisualizationInfo(
|
expected_vp = mdl.VisualizationInfo(
|
||||||
components=mdl.Components(
|
components=mdl.Components(
|
||||||
|
selection=expected_selection,
|
||||||
|
visibility=mdl.ComponentVisibility(
|
||||||
view_setup_hints=mdl.ViewSetupHints(
|
view_setup_hints=mdl.ViewSetupHints(
|
||||||
spaces_visible=False,
|
spaces_visible=False,
|
||||||
space_boundaries_visible=False,
|
space_boundaries_visible=False,
|
||||||
openings_visible=True,
|
openings_visible=True,
|
||||||
),
|
),
|
||||||
selection=expected_selection,
|
|
||||||
visibility=mdl.ComponentVisibility(
|
|
||||||
exceptions=expected_exception,
|
exceptions=expected_exception,
|
||||||
default_visibility=True,
|
default_visibility=True,
|
||||||
),
|
),
|
||||||
@@ -220,6 +223,7 @@ def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, ex
|
|||||||
camera_direction=mdl.Direction(x=0.7232745289802551, y=0.5967116951942444, z=-0.3475759029388428),
|
camera_direction=mdl.Direction(x=0.7232745289802551, y=0.5967116951942444, z=-0.3475759029388428),
|
||||||
camera_up_vector=mdl.Direction(x=0.27662187814712524, y=0.21082592010498047, z=0.937567412853241),
|
camera_up_vector=mdl.Direction(x=0.27662187814712524, y=0.21082592010498047, z=0.937567412853241),
|
||||||
field_of_view=60,
|
field_of_view=60,
|
||||||
|
aspect_ratio=1.0,
|
||||||
),
|
),
|
||||||
guid="81daa431-bf01-4a49-80a2-1ab07c177717",
|
guid="81daa431-bf01-4a49-80a2-1ab07c177717",
|
||||||
)
|
)
|
||||||
|
|||||||
+8
-7
@@ -17,8 +17,8 @@
|
|||||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
SHELL := sh
|
SHELL := sh
|
||||||
PYTHON:=python3.11
|
PYTHON:=python3
|
||||||
PIP:=pip3.11
|
PIP:=pip3
|
||||||
PATCH:=patch
|
PATCH:=patch
|
||||||
SED:=sed -i
|
SED:=sed -i
|
||||||
VENV_ACTIVATE:=bin/activate
|
VENV_ACTIVATE:=bin/activate
|
||||||
@@ -48,6 +48,7 @@ VERSION_PATCH:=$(shell cat '../../VERSION' | cut -d '.' -f 3)
|
|||||||
VERSION_DATE:=$(shell date '+%y%m%d')
|
VERSION_DATE:=$(shell date '+%y%m%d')
|
||||||
LAST_COMMIT_HASH:=$(shell git rev-parse HEAD)
|
LAST_COMMIT_HASH:=$(shell git rev-parse HEAD)
|
||||||
LAST_COMMIT_DATE:=$(shell git show -s --format=%cI)
|
LAST_COMMIT_DATE:=$(shell git show -s --format=%cI)
|
||||||
|
LAST_GIT_BRANCH:=$(shell git rev-parse --abbrev-ref HEAD)
|
||||||
PYPI_IMP:=cp
|
PYPI_IMP:=cp
|
||||||
|
|
||||||
ifdef PYVERSION
|
ifdef PYVERSION
|
||||||
@@ -63,6 +64,7 @@ PYNUMBER:=3$(PYMINOR)
|
|||||||
PYPI_VERSION:=3.$(PYMINOR)
|
PYPI_VERSION:=3.$(PYMINOR)
|
||||||
endif # def PYVERSION
|
endif # def PYVERSION
|
||||||
|
|
||||||
|
IFCMERGE_VERSION:=2026-04-07
|
||||||
|
|
||||||
ifdef PLATFORM
|
ifdef PLATFORM
|
||||||
SUPPORTED_PLATFORMS := linux macos macosm1 win
|
SUPPORTED_PLATFORMS := linux macos macosm1 win
|
||||||
@@ -232,18 +234,16 @@ endif
|
|||||||
cd build/bonsai/bim/data/brick/ && wget https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl
|
cd build/bonsai/bim/data/brick/ && wget https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl
|
||||||
|
|
||||||
# Required for hipped roof generation
|
# Required for hipped roof generation
|
||||||
# TODO: Use official repo once https://github.com/prochitecture/bpypolyskel/pull/22 is merged.
|
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m pip wheel "git+https://github.com/prochitecture/bpypolyskel" --no-deps -w wheels/
|
||||||
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m pip wheel "git+https://github.com/Andrej730/bpypolyskel.git@pyproject_toml" --no-deps -w wheels/
|
|
||||||
|
|
||||||
# folder for executable files
|
# folder for executable files
|
||||||
mkdir -p build/bonsai/libs/bin
|
mkdir -p build/bonsai/libs/bin
|
||||||
|
|
||||||
# required for three-way git merging
|
# required for three-way git merging
|
||||||
ifeq ($(PLATFORM), win)
|
ifeq ($(PLATFORM), win)
|
||||||
cd build/bonsai/libs/bin && wget https://github.com/brunopostle/ifcmerge/releases/download/2025-01-26/ifcmerge.zip
|
cd build/bonsai/libs/bin && wget https://github.com/brunopostle/ifcmerge/releases/download/$(IFCMERGE_VERSION)/ifcmerge.exe
|
||||||
cd build/bonsai/libs/bin && unzip ifcmerge.zip && rm ifcmerge.zip
|
|
||||||
else
|
else
|
||||||
cd build/bonsai/libs/bin && wget https://raw.githubusercontent.com/brunopostle/ifcmerge/main/ifcmerge && chmod +x ifcmerge
|
cd build/bonsai/libs/bin && wget https://raw.githubusercontent.com/brunopostle/ifcmerge/$(IFCMERGE_VERSION)/ifcmerge && chmod +x ifcmerge
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# Generate translations module for Bonsai build
|
# Generate translations module for Bonsai build
|
||||||
@@ -262,6 +262,7 @@ else
|
|||||||
$(SED) "s/0.0.0/$(VERSION)-alpha$(VERSION_DATE)/" build/bonsai/blender_manifest.toml
|
$(SED) "s/0.0.0/$(VERSION)-alpha$(VERSION_DATE)/" build/bonsai/blender_manifest.toml
|
||||||
$(SED) "s/8888888/$(LAST_COMMIT_HASH)/" build/bonsai/__init__.py
|
$(SED) "s/8888888/$(LAST_COMMIT_HASH)/" build/bonsai/__init__.py
|
||||||
$(SED) "s/9999999/$(LAST_COMMIT_DATE)/" build/bonsai/__init__.py
|
$(SED) "s/9999999/$(LAST_COMMIT_DATE)/" build/bonsai/__init__.py
|
||||||
|
$(SED) "s/7777777/$(LAST_GIT_BRANCH)/" build/bonsai/__init__.py
|
||||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/pyproject.toml
|
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/pyproject.toml
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ from typing import TYPE_CHECKING, Any, Union
|
|||||||
|
|
||||||
last_commit_hash = "8888888"
|
last_commit_hash = "8888888"
|
||||||
last_commit_date = "9999999"
|
last_commit_date = "9999999"
|
||||||
|
last_git_branch = "7777777"
|
||||||
|
|
||||||
|
|
||||||
def get_last_commit_hash() -> Union[str, None]:
|
def get_last_commit_hash() -> Union[str, None]:
|
||||||
@@ -60,6 +61,15 @@ def get_last_commit_date() -> Union[str, None]:
|
|||||||
return last_commit_date
|
return last_commit_date
|
||||||
|
|
||||||
|
|
||||||
|
def get_git_branch() -> Union[str, None]:
|
||||||
|
# Using this weird way to write 7777777,
|
||||||
|
# so makefile won't accidentally replace it here
|
||||||
|
# we'll be able to distinguish branch from placeholder value.
|
||||||
|
if last_git_branch == str(7_777777):
|
||||||
|
return None
|
||||||
|
return last_git_branch
|
||||||
|
|
||||||
|
|
||||||
# Accessed from bonsai extension:
|
# Accessed from bonsai extension:
|
||||||
bbim_semver: dict[str, Any] = {}
|
bbim_semver: dict[str, Any] = {}
|
||||||
|
|
||||||
@@ -125,6 +135,7 @@ def get_debug_info(*, bonsai_failed_to_load: bool = False) -> dict[str, Any]:
|
|||||||
"bonsai_version": bbim_version,
|
"bonsai_version": bbim_version,
|
||||||
"bonsai_commit_hash": get_last_commit_hash(),
|
"bonsai_commit_hash": get_last_commit_hash(),
|
||||||
"bonsai_commit_date": get_last_commit_date(),
|
"bonsai_commit_date": get_last_commit_date(),
|
||||||
|
"bonsai_git_branch": get_git_branch(),
|
||||||
"last_actions": last_actions,
|
"last_actions": last_actions,
|
||||||
"last_error": last_error,
|
"last_error": last_error,
|
||||||
}
|
}
|
||||||
@@ -251,10 +262,12 @@ if IN_BLENDER:
|
|||||||
|
|
||||||
global last_commit_hash
|
global last_commit_hash
|
||||||
global last_commit_date
|
global last_commit_date
|
||||||
|
global last_git_branch
|
||||||
path = Path(__file__).resolve().parent
|
path = Path(__file__).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
|
||||||
last_commit_date = repo.head.object.committed_datetime.isoformat()
|
last_commit_date = repo.head.object.committed_datetime.isoformat()
|
||||||
|
last_git_branch = repo.active_branch.name
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
Copyright (c) 2011-2012, Nikita Volchenkov (<nikitavolchenkov@gmail.com>),
|
||||||
|
with Reserved Font Name OpenGost Type B.
|
||||||
|
|
||||||
|
Copyright (c) 2012, Valek Filippov (<frob@gnome.org>).
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -5,7 +5,7 @@ FILE_NAME('Psets_BBIM_Annotation.ifc','2020-01-01T00:00:00',$,$,'Psets_BBIM_Anno
|
|||||||
FILE_SCHEMA(('IFC4'));
|
FILE_SCHEMA(('IFC4'));
|
||||||
ENDSEC;
|
ENDSEC;
|
||||||
DATA;
|
DATA;
|
||||||
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#4,#33,#29,#32,#3,#2,#34,#35));
|
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#4,#33,#29,#32,#3,#2,#41,#42));
|
||||||
#2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
|
#2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
|
||||||
#3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
#3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||||
#4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','Classes separated by spaces that end up in classes for this element in svg. Can be used to specify the text font size: small - 1.8mm; regular - 2.5mm; large - 3.5mm; header - 5mm; title - 7mm. By default regular size is used.',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
#4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','Classes separated by spaces that end up in classes for this element in svg. Can be used to specify the text font size: small - 1.8mm; regular - 2.5mm; large - 3.5mm; header - 5mm; title - 7mm. By default regular size is used.',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||||
@@ -28,7 +28,7 @@ DATA;
|
|||||||
#21=IFCSIMPLEPROPERTYTEMPLATE('1UDakJ5_f7kBhggNSW4$h5',$,'SymbolsPath','Default symbols SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
#21=IFCSIMPLEPROPERTYTEMPLATE('1UDakJ5_f7kBhggNSW4$h5',$,'SymbolsPath','Default symbols SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||||
#22=IFCSIMPLEPROPERTYTEMPLATE('0d53LEtgLDQxnv__NfgH7i',$,'PatternsPath','Default patterns SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
#22=IFCSIMPLEPROPERTYTEMPLATE('0d53LEtgLDQxnv__NfgH7i',$,'PatternsPath','Default patterns SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||||
#23=IFCSIMPLEPROPERTYTEMPLATE('26qFNMv7nCHgU6Jd7Anga5',$,'ShadingStylesPath','Default shading styles',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
#23=IFCSIMPLEPROPERTYTEMPLATE('26qFNMv7nCHgU6Jd7Anga5',$,'ShadingStylesPath','Default shading styles',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||||
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcAnnotation/RADIUS,IfcAnnotation/DIAMETER,IfcTypeProduct',(#25,#26,#27,#28,#30));
|
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcAnnotation/RADIUS,IfcAnnotation/DIAMETER,IfcAnnotation/ANGLE,IfcAnnotation/PLAN_LEVEL,IfcAnnotation/SECTION_LEVEL,IfcTypeProduct',(#25,#26,#35,#36,#27,#28,#30,#34,#37,#38,#39,#40));
|
||||||
#25=IFCSIMPLEPROPERTYTEMPLATE('1rL2AbQsXD8RbpoWH5pYOV',$,'ShowDescriptionOnly','Hide the measurement values and show only annotation description',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
#25=IFCSIMPLEPROPERTYTEMPLATE('1rL2AbQsXD8RbpoWH5pYOV',$,'ShowDescriptionOnly','Hide the measurement values and show only annotation description',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||||
#26=IFCSIMPLEPROPERTYTEMPLATE('0SVyOfB0rC2xNfdRYf3XvY',$,'SuppressZeroInches','Suppress 0 inch values in dimension annotation text (for example: 12'' - 0" -> 12'')',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
#26=IFCSIMPLEPROPERTYTEMPLATE('0SVyOfB0rC2xNfdRYf3XvY',$,'SuppressZeroInches','Suppress 0 inch values in dimension annotation text (for example: 12'' - 0" -> 12'')',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||||
#27=IFCSIMPLEPROPERTYTEMPLATE('2bUmj458PBqPAtUoI3MXsb',$,'TextPrefix','Text to add before annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
#27=IFCSIMPLEPROPERTYTEMPLATE('2bUmj458PBqPAtUoI3MXsb',$,'TextPrefix','Text to add before annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||||
@@ -38,7 +38,14 @@ DATA;
|
|||||||
#31=IFCPROPERTYENUMERATION('CustomUnit',(IFCTEXT('Feet and Inches - Fractional'),IFCTEXT('Feet - Decimal'),IFCTEXT('Inches - Fractional'),IFCTEXT('Inches - Decimal'),IFCTEXT('Meters'),IFCTEXT('Decimeters'),IFCTEXT('Centimeters'),IFCTEXT('Millimeters')),$);
|
#31=IFCPROPERTYENUMERATION('CustomUnit',(IFCTEXT('Feet and Inches - Fractional'),IFCTEXT('Feet - Decimal'),IFCTEXT('Inches - Fractional'),IFCTEXT('Inches - Decimal'),IFCTEXT('Meters'),IFCTEXT('Decimeters'),IFCTEXT('Centimeters'),IFCTEXT('Millimeters')),$);
|
||||||
#32=IFCSIMPLEPROPERTYTEMPLATE('0gjJzDYBX8P85qn1xcAOOo',$,'Reverse_List','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
#32=IFCSIMPLEPROPERTYTEMPLATE('0gjJzDYBX8P85qn1xcAOOo',$,'Reverse_List','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||||
#33=IFCSIMPLEPROPERTYTEMPLATE('22TrcxF8jFNB4buSmzjGEF',$,'List_Separator','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
#33=IFCSIMPLEPROPERTYTEMPLATE('22TrcxF8jFNB4buSmzjGEF',$,'List_Separator','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||||
#34=IFCSIMPLEPROPERTYTEMPLATE('0FauxIsAnnotFaux0001aB',$,'IsManualDrawingReference','Marks this annotation as a manually placed drawing reference, exempt from automatic drawing regeneration.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
#34=IFCSIMPLEPROPERTYTEMPLATE('1Kx4Pm9nR8vBwZqTs2uYeL',$,'Separator','Characters placed between multiple dimension values when CustomUnit has more than one unit selected (default: '' / '')',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||||
#35=IFCSIMPLEPROPERTYTEMPLATE('0FauxIsDocRefFaux001aB',$,'IsDocumentReference','Marks this annotation as pointing to an external document reference (not a Bonsai drawing camera).',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
#35=IFCSIMPLEPROPERTYTEMPLATE('3Nf6Qs1mT0pWxBuCvDyEzA',$,'SuppressZeroFeet','Suppress 0 feet in dimension annotation text (for example: 0'' - 3 1/2" -> 3 1/2")',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||||
|
#36=IFCSIMPLEPROPERTYTEMPLATE('2Rg7Hn5jK4mLpNqOsVwXtY',$,'IsOrdinate','Show accumulated distance from the first vertex instead of individual segment lengths',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||||
|
#37=IFCSIMPLEPROPERTYTEMPLATE('1XpRnKoT2sGuW7vYcZaMqb',$,'Anchors','JSON array of parametric anchor descriptors — one per polyline vertex. Each entry: {"guid": str|null, "type": "FACE"|"CIRCLE_CENTER"|"WORLD", "addr": {...}, "hint": [x,y,z]|null, "pt": [x,y,z]}',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||||
|
#38=IFCSIMPLEPROPERTYTEMPLATE('2YqSmLoU3tHvX8wZdaNrjc',$,'MeasureAxis','Axis along which distances are projected: X | Y | Z | TRUE | PERPENDICULAR',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||||
|
#39=IFCSIMPLEPROPERTYTEMPLATE('3Ny31Go6T5Z9fh8j4yQC0p',$,'ForcePerpendicularToFace','When enabled the polyline is constrained to follow the face normal of the first anchor vertex so the dimension measures straight-line distance perpendicular to that face',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||||
|
#40=IFCSIMPLEPROPERTYTEMPLATE('1LoNpKqR3sTuVwXyZaBcDe',$,'LinePosition','Absolute world-space coordinate (metres) of the dimension line along the horizontal offset axis (perpendicular to the dimension direction). When set, the dimension line is held at this fixed global position even if the measured geometry moves. When absent the line sits at the anchor points.',.P_SINGLEVALUE.,'IfcLengthMeasure',$,$,$,$,$,.READWRITE.);
|
||||||
|
#41=IFCSIMPLEPROPERTYTEMPLATE('0FauxIsAnnotFaux0001aB',$,'IsManualDrawingReference','Marks this annotation as a manually placed drawing reference, exempt from automatic drawing regeneration.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||||
|
#42=IFCSIMPLEPROPERTYTEMPLATE('0FauxIsDocRefFaux001aB',$,'IsDocumentReference','Marks this annotation as pointing to an external document reference (not a Bonsai drawing camera).',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||||
ENDSEC;
|
ENDSEC;
|
||||||
END-ISO-10303-21;
|
END-ISO-10303-21;
|
||||||
|
|||||||
@@ -72,9 +72,7 @@ class IfcExporter:
|
|||||||
|
|
||||||
def set_header(self):
|
def set_header(self):
|
||||||
self.file.header.file_name.name = os.path.basename(self.ifc_export_settings.output_file)
|
self.file.header.file_name.name = os.path.basename(self.ifc_export_settings.output_file)
|
||||||
self.file.header.file_name.time_stamp = (
|
self.file.header.file_name.time_stamp = datetime.datetime.now().astimezone().replace(microsecond=0).isoformat()
|
||||||
datetime.datetime.utcnow().replace(tzinfo=datetime.UTC).astimezone().replace(microsecond=0).isoformat()
|
|
||||||
)
|
|
||||||
self.file.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version)
|
self.file.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version)
|
||||||
self.file.header.file_name.originating_system = "{} {}".format(
|
self.file.header.file_name.originating_system = "{} {}".format(
|
||||||
self.get_application_name(), tool.Blender.get_bonsai_version()
|
self.get_application_name(), tool.Blender.get_bonsai_version()
|
||||||
|
|||||||
@@ -45,16 +45,19 @@ from bonsai.bim.module.nest.decorator import NestDecorator
|
|||||||
|
|
||||||
cwd = os.path.dirname(os.path.realpath(__file__))
|
cwd = os.path.dirname(os.path.realpath(__file__))
|
||||||
global_subscription_owner = object()
|
global_subscription_owner = object()
|
||||||
|
# Separate owner for per-object msgbus subscriptions (name, active_material_index).
|
||||||
|
# Using a dedicated owner allows clearing all per-object subscriptions at once
|
||||||
|
# during undo/redo without affecting other global subscriptions.
|
||||||
|
object_subscription_owner = object()
|
||||||
|
|
||||||
|
|
||||||
def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -> None:
|
def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -> None:
|
||||||
try:
|
try:
|
||||||
obj.name
|
obj.name
|
||||||
except:
|
except:
|
||||||
# The object is invalid but somehow still has a callback. Clear all
|
# The object is invalid but somehow still has a callback.
|
||||||
# msgbus subscriptions to prevent useless further triggers.
|
# This can occur during undo/redo when the Python wrapper is stale.
|
||||||
bpy.msgbus.clear_by_owner(obj)
|
return
|
||||||
return # In case the object RNA is gone during an undo / redo operation
|
|
||||||
# Blender names are up to 63 UTF-8 bytes
|
# Blender names are up to 63 UTF-8 bytes
|
||||||
if len(bytes(obj.name, "utf-8")) >= 63:
|
if len(bytes(obj.name, "utf-8")) >= 63:
|
||||||
return
|
return
|
||||||
@@ -189,7 +192,7 @@ def subscribe_to(obj: bpy.types.ID, data_path: str, callback: Callable[[bpy.type
|
|||||||
return
|
return
|
||||||
bpy.msgbus.subscribe_rna(
|
bpy.msgbus.subscribe_rna(
|
||||||
key=subscribe_to,
|
key=subscribe_to,
|
||||||
owner=obj,
|
owner=object_subscription_owner,
|
||||||
args=(
|
args=(
|
||||||
obj,
|
obj,
|
||||||
data_path,
|
data_path,
|
||||||
|
|||||||
@@ -316,11 +316,8 @@ class IfcStore:
|
|||||||
del IfcStore.id_map[data["id"]]
|
del IfcStore.id_map[data["id"]]
|
||||||
if "guid" in data:
|
if "guid" in data:
|
||||||
del IfcStore.guid_map[data["guid"]]
|
del IfcStore.guid_map[data["guid"]]
|
||||||
obj = IfcStore.get_object_by_name(data["obj"])
|
# Note: msgbus subscriptions are cleared globally during
|
||||||
if obj is None:
|
# rebuild_element_maps which runs after every undo/redo.
|
||||||
# obj was just created during this step and didn't existed before.
|
|
||||||
return
|
|
||||||
bpy.msgbus.clear_by_owner(obj)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def commit_link_element(data: OperationData) -> None:
|
def commit_link_element(data: OperationData) -> None:
|
||||||
@@ -367,10 +364,8 @@ class IfcStore:
|
|||||||
del IfcStore.id_map[data["id"]]
|
del IfcStore.id_map[data["id"]]
|
||||||
if "guid" in data:
|
if "guid" in data:
|
||||||
del IfcStore.guid_map[data["guid"]]
|
del IfcStore.guid_map[data["guid"]]
|
||||||
obj = IfcStore.get_object_by_name(data["obj"])
|
# Note: msgbus subscriptions are cleared globally during
|
||||||
# obj might be removed after unlink.
|
# rebuild_element_maps which runs after every undo/redo.
|
||||||
if not obj:
|
|
||||||
bpy.msgbus.clear_by_owner(obj)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def unlink_element(
|
def unlink_element(
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ class MaterialCreator:
|
|||||||
mesh: Union[OBJECT_DATA_TYPE, None],
|
mesh: Union[OBJECT_DATA_TYPE, None],
|
||||||
shape_has_openings: bool,
|
shape_has_openings: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
if ((rep := getattr(element, "Representation", ...) is not ...) and not rep) or (
|
if ((rep := getattr(element, "Representation", ...)) is not ... and not rep) or (
|
||||||
(rep := getattr(element, "RepresentationMaps", ...) is not ...) and not rep
|
(rep := getattr(element, "RepresentationMaps", ...)) is not ... and not rep
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,22 @@ def poll_related_object(self: "BIMObjectAggregateProperties", related_obj: bpy.t
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def update_relating_object(self, context):
|
||||||
|
if self.relating_object:
|
||||||
|
ifc_id = tool.Blender.get_object_bim_props(self.relating_object).ifc_definition_id
|
||||||
|
if ifc_id:
|
||||||
|
bpy.ops.bim.aggregate_assign_object(relating_object=ifc_id)
|
||||||
|
bpy.ops.bim.disable_editing_aggregate()
|
||||||
|
|
||||||
|
|
||||||
|
def update_related_object(self, context):
|
||||||
|
if self.related_object:
|
||||||
|
ifc_id = tool.Blender.get_object_bim_props(self.related_object).ifc_definition_id
|
||||||
|
if ifc_id:
|
||||||
|
bpy.ops.bim.aggregate_assign_object(related_object=ifc_id)
|
||||||
|
bpy.ops.bim.disable_editing_aggregate()
|
||||||
|
|
||||||
|
|
||||||
def update_aggregate_decorator(self, context):
|
def update_aggregate_decorator(self, context):
|
||||||
if self.aggregate_decorator:
|
if self.aggregate_decorator:
|
||||||
AggregateDecorator.install(bpy.context)
|
AggregateDecorator.install(bpy.context)
|
||||||
@@ -89,12 +105,15 @@ def update_aggregate_mode_decorator(self, context):
|
|||||||
|
|
||||||
class BIMObjectAggregateProperties(PropertyGroup):
|
class BIMObjectAggregateProperties(PropertyGroup):
|
||||||
is_editing: BoolProperty(name="Is Editing")
|
is_editing: BoolProperty(name="Is Editing")
|
||||||
relating_object: PointerProperty(name="Relating Whole", type=bpy.types.Object, poll=poll_relating_object)
|
relating_object: PointerProperty(
|
||||||
|
name="Relating Whole", type=bpy.types.Object, poll=poll_relating_object, update=update_relating_object
|
||||||
|
)
|
||||||
related_object: PointerProperty(
|
related_object: PointerProperty(
|
||||||
name="Related Part",
|
name="Related Part",
|
||||||
description="Related Part, will be used to derive the Relating Object",
|
description="Related Part, will be used to derive the Relating Object",
|
||||||
type=bpy.types.Object,
|
type=bpy.types.Object,
|
||||||
poll=poll_related_object,
|
poll=poll_related_object,
|
||||||
|
update=update_related_object,
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|||||||
@@ -295,13 +295,13 @@ class ExplorerShowUIPopup(bpy.types.Operator):
|
|||||||
bl_description = "Show Explorer UI to select element as attribute value or edit it."
|
bl_description = "Show Explorer UI to select element as attribute value or edit it."
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
ifc_class: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
ifc_class: bpy.props.StringProperty()
|
||||||
"""Element IFC class."""
|
"""Element IFC class."""
|
||||||
attribute_name: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
attribute_name: bpy.props.StringProperty()
|
||||||
"""IFC class attribute name."""
|
"""IFC class attribute name."""
|
||||||
data_path: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
data_path: bpy.props.StringProperty()
|
||||||
"""Full data path"""
|
"""Full data path"""
|
||||||
preselect_ifc_id: bpy.props.IntProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
|
preselect_ifc_id: bpy.props.IntProperty(options={"SKIP_SAVE"})
|
||||||
"""IFC id to preselect in the popup."""
|
"""IFC id to preselect in the popup."""
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class BIMAttributeProperties(PropertyGroup):
|
|||||||
|
|
||||||
|
|
||||||
class ExplorerEntity(PropertyGroup):
|
class ExplorerEntity(PropertyGroup):
|
||||||
ifc_definition_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
ifc_definition_id: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
ifc_definition_id: int
|
ifc_definition_id: int
|
||||||
@@ -60,7 +60,7 @@ class BIMExplorerProperties(PropertyGroup):
|
|||||||
self.property_unset("editing_entity_id")
|
self.property_unset("editing_entity_id")
|
||||||
self.entity_attributes.clear()
|
self.entity_attributes.clear()
|
||||||
|
|
||||||
is_loaded: BoolProperty( # pyright: ignore[reportRedeclaration]
|
is_loaded: BoolProperty(
|
||||||
name="Toggle Explorer UI",
|
name="Toggle Explorer UI",
|
||||||
update=update_is_loaded,
|
update=update_is_loaded,
|
||||||
)
|
)
|
||||||
@@ -76,15 +76,15 @@ class BIMExplorerProperties(PropertyGroup):
|
|||||||
def update_ifc_class(self, context: object) -> None:
|
def update_ifc_class(self, context: object) -> None:
|
||||||
tool.Attribute.refresh_uilist_entities()
|
tool.Attribute.refresh_uilist_entities()
|
||||||
|
|
||||||
ifc_class: EnumProperty( # pyright: ignore[reportRedeclaration]
|
ifc_class: EnumProperty(
|
||||||
name="IFC Class To Search",
|
name="IFC Class To Search",
|
||||||
items=get_ifc_class,
|
items=get_ifc_class,
|
||||||
update=update_ifc_class,
|
update=update_ifc_class,
|
||||||
)
|
)
|
||||||
entities: CollectionProperty(type=ExplorerEntity) # pyright: ignore[reportRedeclaration]
|
entities: CollectionProperty(type=ExplorerEntity)
|
||||||
active_entity_index: IntProperty() # pyright: ignore[reportRedeclaration]
|
active_entity_index: IntProperty()
|
||||||
editing_entity_id: IntProperty() # pyright: ignore[reportRedeclaration]
|
editing_entity_id: IntProperty()
|
||||||
entity_attributes: CollectionProperty(type=Attribute) # pyright: ignore[reportRedeclaration]
|
entity_attributes: CollectionProperty(type=Attribute)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
is_loaded: bool
|
is_loaded: bool
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ class BcfTopic(PropertyGroup):
|
|||||||
|
|
||||||
|
|
||||||
def get_related_topics(self: "BCFProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
def get_related_topics(self: "BCFProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||||
global RELATED_TOPICS_ENUM_ITEMS
|
global RELATED_TOPICS_ENUM_ITEMS # ty: ignore[unresolved-global]
|
||||||
props = self
|
props = self
|
||||||
active_topic = props.active_topic
|
active_topic = props.active_topic
|
||||||
active_related_topics = active_topic.related_topics.keys()
|
active_related_topics = active_topic.related_topics.keys()
|
||||||
|
|||||||
@@ -377,6 +377,8 @@ class EnableEditingBoundary(bpy.types.Operator):
|
|||||||
obj = tool.Ifc.get_object(entity)
|
obj = tool.Ifc.get_object(entity)
|
||||||
if entity and obj:
|
if entity and obj:
|
||||||
setattr(bprops, blender_property, obj)
|
setattr(bprops, blender_property, obj)
|
||||||
|
bprops.physical_or_virtual = boundary.PhysicalOrVirtualBoundary or "NOTDEFINED"
|
||||||
|
bprops.internal_or_external = boundary.InternalOrExternalBoundary or "NOTDEFINED"
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -392,6 +394,8 @@ class DisableEditingBoundary(bpy.types.Operator):
|
|||||||
bprops.is_editing = False
|
bprops.is_editing = False
|
||||||
for ifc_attribute, blender_property in EDITABLE_ATTRIBUTES.items():
|
for ifc_attribute, blender_property in EDITABLE_ATTRIBUTES.items():
|
||||||
setattr(bprops, blender_property, None)
|
setattr(bprops, blender_property, None)
|
||||||
|
bprops.physical_or_virtual = "NOTDEFINED"
|
||||||
|
bprops.internal_or_external = "NOTDEFINED"
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -411,6 +415,8 @@ class EditBoundaryAttributes(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
obj = getattr(bprops, blender_property, None)
|
obj = getattr(bprops, blender_property, None)
|
||||||
entity = tool.Ifc.get_entity(obj)
|
entity = tool.Ifc.get_entity(obj)
|
||||||
attributes[blender_property] = entity
|
attributes[blender_property] = entity
|
||||||
|
attributes["physical_or_virtual"] = bprops.physical_or_virtual
|
||||||
|
attributes["internal_or_external"] = bprops.internal_or_external
|
||||||
ifcopenshell.api.boundary.edit_attributes(tool.Ifc.get(), entity=boundary, **attributes)
|
ifcopenshell.api.boundary.edit_attributes(tool.Ifc.get(), entity=boundary, **attributes)
|
||||||
bpy.ops.bim.disable_editing_boundary()
|
bpy.ops.bim.disable_editing_boundary()
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from typing import TYPE_CHECKING, Union
|
|||||||
import bpy
|
import bpy
|
||||||
from bpy.props import (
|
from bpy.props import (
|
||||||
BoolProperty,
|
BoolProperty,
|
||||||
|
EnumProperty,
|
||||||
PointerProperty,
|
PointerProperty,
|
||||||
)
|
)
|
||||||
from bpy.types import PropertyGroup
|
from bpy.types import PropertyGroup
|
||||||
@@ -50,12 +51,43 @@ def element_filter(self: "BIMObjectBoundaryProperties", object: bpy.types.Object
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_internal_or_external_items(
|
||||||
|
self: "BIMObjectBoundaryProperties", context: bpy.types.Context | None
|
||||||
|
) -> list[tuple[str, str, str]]:
|
||||||
|
items = [
|
||||||
|
("INTERNAL", "Internal", ""),
|
||||||
|
("EXTERNAL", "External", ""),
|
||||||
|
]
|
||||||
|
ifc = tool.Ifc.get()
|
||||||
|
if not ifc or ifc.schema != "IFC2X3":
|
||||||
|
items += [
|
||||||
|
("EXTERNAL_EARTH", "External Earth", ""),
|
||||||
|
("EXTERNAL_WATER", "External Water", ""),
|
||||||
|
("EXTERNAL_FIRE", "External Fire", ""),
|
||||||
|
]
|
||||||
|
items.append(("NOTDEFINED", "Not Defined", ""))
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
class BIMObjectBoundaryProperties(PropertyGroup):
|
class BIMObjectBoundaryProperties(PropertyGroup):
|
||||||
is_editing: BoolProperty(name="Is Editing")
|
is_editing: BoolProperty(name="Is Editing")
|
||||||
relating_space: PointerProperty(name="RelatingSpace", type=bpy.types.Object, poll=space_filter)
|
relating_space: PointerProperty(name="RelatingSpace", type=bpy.types.Object, poll=space_filter)
|
||||||
related_building_element: PointerProperty(name="RelatedBuildingElement", type=bpy.types.Object, poll=element_filter)
|
related_building_element: PointerProperty(name="RelatedBuildingElement", type=bpy.types.Object, poll=element_filter)
|
||||||
parent_boundary: PointerProperty(name="ParentBoundary", type=bpy.types.Object, poll=boundary_filter)
|
parent_boundary: PointerProperty(name="ParentBoundary", type=bpy.types.Object, poll=boundary_filter)
|
||||||
corresponding_boundary: PointerProperty(name="CorrespondingBoundary", type=bpy.types.Object, poll=boundary_filter)
|
corresponding_boundary: PointerProperty(name="CorrespondingBoundary", type=bpy.types.Object, poll=boundary_filter)
|
||||||
|
physical_or_virtual: EnumProperty(
|
||||||
|
name="PhysicalOrVirtualBoundary",
|
||||||
|
items=[
|
||||||
|
("PHYSICAL", "Physical", ""),
|
||||||
|
("VIRTUAL", "Virtual", ""),
|
||||||
|
("NOTDEFINED", "Not Defined", ""),
|
||||||
|
],
|
||||||
|
default="NOTDEFINED",
|
||||||
|
)
|
||||||
|
internal_or_external: EnumProperty(
|
||||||
|
name="InternalOrExternalBoundary",
|
||||||
|
items=get_internal_or_external_items,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
is_editing: bool
|
is_editing: bool
|
||||||
@@ -63,6 +95,8 @@ class BIMObjectBoundaryProperties(PropertyGroup):
|
|||||||
related_building_element: Union[bpy.types.Object, None]
|
related_building_element: Union[bpy.types.Object, None]
|
||||||
parent_boundary: Union[bpy.types.Object, None]
|
parent_boundary: Union[bpy.types.Object, None]
|
||||||
corresponding_boundary: Union[bpy.types.Object, None]
|
corresponding_boundary: Union[bpy.types.Object, None]
|
||||||
|
physical_or_virtual: str
|
||||||
|
internal_or_external: str # values depend on schema: IFC2X3 omits EXTERNAL_EARTH/WATER/FIRE
|
||||||
|
|
||||||
|
|
||||||
class BIMBoundaryProperties(PropertyGroup):
|
class BIMBoundaryProperties(PropertyGroup):
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ class BIM_PT_Boundary(Panel):
|
|||||||
self.draw_relation_editor(boundary, "RelatedBuildingElement", "related_building_element")
|
self.draw_relation_editor(boundary, "RelatedBuildingElement", "related_building_element")
|
||||||
self.draw_relation_editor(boundary, "ParentBoundary", "parent_boundary")
|
self.draw_relation_editor(boundary, "ParentBoundary", "parent_boundary")
|
||||||
self.draw_relation_editor(boundary, "CorrespondingBoundary", "corresponding_boundary")
|
self.draw_relation_editor(boundary, "CorrespondingBoundary", "corresponding_boundary")
|
||||||
|
row = self.layout.row()
|
||||||
|
row.prop(self.bprops, "physical_or_virtual")
|
||||||
|
row = self.layout.row()
|
||||||
|
row.prop(self.bprops, "internal_or_external")
|
||||||
else:
|
else:
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.operator("bim.enable_editing_boundary", icon="GREASEPENCIL", text="Edit")
|
row.operator("bim.enable_editing_boundary", icon="GREASEPENCIL", text="Edit")
|
||||||
@@ -84,6 +88,8 @@ class BIM_PT_Boundary(Panel):
|
|||||||
self.draw_relation_data(boundary, "RelatedBuildingElement")
|
self.draw_relation_data(boundary, "RelatedBuildingElement")
|
||||||
self.draw_relation_data(boundary, "ParentBoundary")
|
self.draw_relation_data(boundary, "ParentBoundary")
|
||||||
self.draw_relation_data(boundary, "CorrespondingBoundary")
|
self.draw_relation_data(boundary, "CorrespondingBoundary")
|
||||||
|
self.draw_enum_data(boundary, "PhysicalOrVirtualBoundary")
|
||||||
|
self.draw_enum_data(boundary, "InternalOrExternalBoundary")
|
||||||
if hasattr(boundary, "InnerBoundaries"):
|
if hasattr(boundary, "InnerBoundaries"):
|
||||||
for i, inner_boundary in enumerate(getattr(boundary, "InnerBoundaries", ())):
|
for i, inner_boundary in enumerate(getattr(boundary, "InnerBoundaries", ())):
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
@@ -110,6 +116,11 @@ class BIM_PT_Boundary(Panel):
|
|||||||
else:
|
else:
|
||||||
row.label(text="")
|
row.label(text="")
|
||||||
|
|
||||||
|
def draw_enum_data(self, boundary, ifc_attribute: str):
|
||||||
|
row = self.layout.row(align=True)
|
||||||
|
row.label(text=ifc_attribute)
|
||||||
|
row.label(text=getattr(boundary, ifc_attribute, "") or "")
|
||||||
|
|
||||||
def draw_relation_editor(self, boundary, ifc_attribute: str, blender_property: str):
|
def draw_relation_editor(self, boundary, ifc_attribute: str, blender_property: str):
|
||||||
if hasattr(boundary, ifc_attribute):
|
if hasattr(boundary, ifc_attribute):
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
|
|||||||
@@ -46,26 +46,26 @@ def get_libraries(self, context):
|
|||||||
|
|
||||||
|
|
||||||
def get_namespaces(self, context):
|
def get_namespaces(self, context):
|
||||||
global NAMESPACES_ENUM_ITEMS
|
global NAMESPACES_ENUM_ITEMS # ty: ignore[unresolved-global]
|
||||||
NAMESPACES_ENUM_ITEMS = [(uri, f"{alias}: {uri}", "") for alias, uri in BrickStore.namespaces]
|
NAMESPACES_ENUM_ITEMS = [(uri, f"{alias}: {uri}", "") for alias, uri in BrickStore.namespaces]
|
||||||
return NAMESPACES_ENUM_ITEMS
|
return NAMESPACES_ENUM_ITEMS
|
||||||
|
|
||||||
|
|
||||||
def get_brick_entity_classes(self, context):
|
def get_brick_entity_classes(self, context):
|
||||||
global ENTITY_CLASSES_ENUM_ITEMS
|
global ENTITY_CLASSES_ENUM_ITEMS # ty: ignore[unresolved-global]
|
||||||
entity = self.brick_entity_create_type
|
entity = self.brick_entity_create_type
|
||||||
ENTITY_CLASSES_ENUM_ITEMS = [(uri, uri.split("#")[-1], "") for uri in BrickStore.entity_classes[entity]]
|
ENTITY_CLASSES_ENUM_ITEMS = [(uri, uri.split("#")[-1], "") for uri in BrickStore.entity_classes[entity]]
|
||||||
return ENTITY_CLASSES_ENUM_ITEMS
|
return ENTITY_CLASSES_ENUM_ITEMS
|
||||||
|
|
||||||
|
|
||||||
def get_brick_roots(self, context):
|
def get_brick_roots(self, context):
|
||||||
global BRICK_ROOTS_ENUM_ITEMS
|
global BRICK_ROOTS_ENUM_ITEMS # ty: ignore[unresolved-global]
|
||||||
BRICK_ROOTS_ENUM_ITEMS = [(root, root, "") for root in BrickStore.root_classes]
|
BRICK_ROOTS_ENUM_ITEMS = [(root, root, "") for root in BrickStore.root_classes]
|
||||||
return BRICK_ROOTS_ENUM_ITEMS
|
return BRICK_ROOTS_ENUM_ITEMS
|
||||||
|
|
||||||
|
|
||||||
def get_brick_relations(self, context):
|
def get_brick_relations(self, context):
|
||||||
global BRICK_RELATIONS_ENUM_ITEMS
|
global BRICK_RELATIONS_ENUM_ITEMS # ty: ignore[unresolved-global]
|
||||||
BRICK_RELATIONS_ENUM_ITEMS = [(uri, uri.split("#")[-1], "") for uri in BrickStore.relationships]
|
BRICK_RELATIONS_ENUM_ITEMS = [(uri, uri.split("#")[-1], "") for uri in BrickStore.relationships]
|
||||||
for relation in BrickschemaData.data["active_relations"]:
|
for relation in BrickschemaData.data["active_relations"]:
|
||||||
if relation["predicate_name"] == "label":
|
if relation["predicate_name"] == "label":
|
||||||
|
|||||||
@@ -201,16 +201,10 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
|
|||||||
"ALT+click to run a quick clash without selecting a file to save."
|
"ALT+click to run a quick clash without selecting a file to save."
|
||||||
)
|
)
|
||||||
|
|
||||||
filter_glob: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
|
filter_glob: bpy.props.StringProperty(default="*.bcf;*.json", options={"HIDDEN"})
|
||||||
default="*.bcf;*.json", options={"HIDDEN"}
|
format: bpy.props.EnumProperty(name="Format", items=[(i, i, "") for i in ("bcf", "json")])
|
||||||
)
|
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
|
||||||
format: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
quick_clash: bpy.props.BoolProperty(
|
||||||
name="Format", items=[(i, i, "") for i in ("bcf", "json")]
|
|
||||||
)
|
|
||||||
filepath: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
|
|
||||||
subtype="FILE_PATH", options={"SKIP_SAVE"}
|
|
||||||
)
|
|
||||||
quick_clash: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
|
||||||
options={"SKIP_SAVE"},
|
options={"SKIP_SAVE"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -37,12 +37,12 @@ from bonsai.bim.prop import BIMFilterGroup, StrProperty
|
|||||||
|
|
||||||
|
|
||||||
class ClashSource(PropertyGroup):
|
class ClashSource(PropertyGroup):
|
||||||
name: StringProperty( # pyright: ignore[reportRedeclaration]
|
name: StringProperty(
|
||||||
name="File",
|
name="File",
|
||||||
description="Absolute filepath to existing .ifc file to use as a clash source.",
|
description="Absolute filepath to existing .ifc file to use as a clash source.",
|
||||||
)
|
)
|
||||||
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups") # pyright: ignore[reportRedeclaration]
|
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
|
||||||
mode: EnumProperty( # pyright: ignore[reportRedeclaration]
|
mode: EnumProperty(
|
||||||
items=[
|
items=[
|
||||||
("a", "All Elements", "All elements will be used for clashing"),
|
("a", "All Elements", "All elements will be used for clashing"),
|
||||||
("i", "Include", "Only the selected elements are included for clashing"),
|
("i", "Include", "Only the selected elements are included for clashing"),
|
||||||
@@ -62,7 +62,7 @@ class Clash(PropertyGroup):
|
|||||||
b_global_id: StringProperty(name="B")
|
b_global_id: StringProperty(name="B")
|
||||||
a_name: StringProperty(name="A Name")
|
a_name: StringProperty(name="A Name")
|
||||||
b_name: StringProperty(name="B Name")
|
b_name: StringProperty(name="B Name")
|
||||||
clash_type: EnumProperty( # pyright: ignore[reportRedeclaration]
|
clash_type: EnumProperty(
|
||||||
name="Clash Type",
|
name="Clash Type",
|
||||||
items=tuple((i, i, "") for i in CLASH_TYPE_ITEMS),
|
items=tuple((i, i, "") for i in CLASH_TYPE_ITEMS),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class CopyCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_label = "Copy Cost Schedule"
|
bl_label = "Copy Cost Schedule"
|
||||||
bl_description = "Create a duplicate of the provided cost schedule."
|
bl_description = "Create a duplicate of the provided cost schedule."
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
cost_schedule: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
cost_schedule: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
cost_schedule: int
|
cost_schedule: int
|
||||||
|
|||||||
@@ -260,14 +260,14 @@ class CreateAllShapes(bpy.types.Operator):
|
|||||||
)
|
)
|
||||||
bl_options = {"REGISTER"}
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
geometry_library: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
geometry_library: bpy.props.EnumProperty(
|
||||||
name="Geometry Library",
|
name="Geometry Library",
|
||||||
description="Geometry library to use for testing shape creation.",
|
description="Geometry library to use for testing shape creation.",
|
||||||
items=[(i, i, "") for i in get_args(ifcopenshell.geom.GEOMETRY_LIBRARY)],
|
items=[(i, i, "") for i in get_args(ifcopenshell.geom.GEOMETRY_LIBRARY)],
|
||||||
# By default use the same library as used for importing ifc project.
|
# By default use the same library as used for importing ifc project.
|
||||||
default="hybrid-cgal-simple-opencascade",
|
default="hybrid-cgal-simple-opencascade",
|
||||||
)
|
)
|
||||||
custom_geometry_library: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
|
custom_geometry_library: bpy.props.StringProperty(
|
||||||
name="Custom Geometry Library",
|
name="Custom Geometry Library",
|
||||||
description="Provide a custom geometry library name, will override the 'geometry library' property.",
|
description="Provide a custom geometry library name, will override the 'geometry library' property.",
|
||||||
)
|
)
|
||||||
@@ -781,7 +781,7 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_label = "Purge Unused Objects"
|
bl_label = "Purge Unused Objects"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
object_type: bpy.props.EnumProperty(
|
||||||
name="Object Type",
|
name="Object Type",
|
||||||
items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)),
|
items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)),
|
||||||
)
|
)
|
||||||
@@ -827,7 +827,7 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
)
|
)
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
object_type: bpy.props.EnumProperty(
|
||||||
name="Object Type",
|
name="Object Type",
|
||||||
items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)),
|
items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)),
|
||||||
)
|
)
|
||||||
@@ -1073,7 +1073,7 @@ class ChangeLogLevel(bpy.types.Operator):
|
|||||||
bl_options = {"REGISTER"}
|
bl_options = {"REGISTER"}
|
||||||
bl_description = "Change general log level across all Python code in Blender"
|
bl_description = "Change general log level across all Python code in Blender"
|
||||||
|
|
||||||
log_level: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
log_level: bpy.props.EnumProperty(
|
||||||
name="Log Level",
|
name="Log Level",
|
||||||
items=[(i, i, "") for i in get_args(LogLevelType)],
|
items=[(i, i, "") for i in get_args(LogLevelType)],
|
||||||
default="WARNING",
|
default="WARNING",
|
||||||
|
|||||||
@@ -108,6 +108,11 @@ classes = (
|
|||||||
operator.ToggleTargetView,
|
operator.ToggleTargetView,
|
||||||
operator.OpenDocumentationWebUi,
|
operator.OpenDocumentationWebUi,
|
||||||
operator.FilterSelectedObjectsIfIntersectedByCamera,
|
operator.FilterSelectedObjectsIfIntersectedByCamera,
|
||||||
|
operator.DrawParametricDimension,
|
||||||
|
operator.SetDimensionAnchor,
|
||||||
|
operator.RegenerateDimensions,
|
||||||
|
operator.ClickNearestDimensionAnchor,
|
||||||
|
operator.DebugDimensionClicks,
|
||||||
prop.Variable,
|
prop.Variable,
|
||||||
prop.Drawing,
|
prop.Drawing,
|
||||||
prop.Document,
|
prop.Document,
|
||||||
@@ -149,11 +154,17 @@ classes = (
|
|||||||
gizmos.UglyDotGizmo,
|
gizmos.UglyDotGizmo,
|
||||||
gizmos.ExtrusionGuidesGizmo,
|
gizmos.ExtrusionGuidesGizmo,
|
||||||
gizmos.ExtrusionWidget,
|
gizmos.ExtrusionWidget,
|
||||||
|
gizmos.GizmoAnchorHandle,
|
||||||
|
gizmos.DimensionAnchorWidget,
|
||||||
|
gizmos.DimensionLinePositionWidget,
|
||||||
workspace.LaunchAnnotationTypeManager,
|
workspace.LaunchAnnotationTypeManager,
|
||||||
workspace.Hotkey,
|
workspace.Hotkey,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_keymaps = []
|
||||||
|
|
||||||
|
|
||||||
def menu_func(self, context):
|
def menu_func(self, context):
|
||||||
active_obj = context.active_object
|
active_obj = context.active_object
|
||||||
if active_obj:
|
if active_obj:
|
||||||
@@ -173,9 +184,17 @@ def register():
|
|||||||
bpy.types.TextCurve.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
|
bpy.types.TextCurve.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
|
||||||
bpy.app.handlers.load_post.append(handler.load_post)
|
bpy.app.handlers.load_post.append(handler.load_post)
|
||||||
bpy.app.handlers.depsgraph_update_pre.append(handler.depsgraph_update_pre_handler)
|
bpy.app.handlers.depsgraph_update_pre.append(handler.depsgraph_update_pre_handler)
|
||||||
|
bpy.app.handlers.depsgraph_update_post.append(handler.depsgraph_update_post_handler)
|
||||||
bpy.types.VIEW3D_MT_image_add.append(ui.add_object_button)
|
bpy.types.VIEW3D_MT_image_add.append(ui.add_object_button)
|
||||||
bpy.types.VIEW3D_MT_object_context_menu.append(menu_func)
|
bpy.types.VIEW3D_MT_object_context_menu.append(menu_func)
|
||||||
|
|
||||||
|
wm = bpy.context.window_manager
|
||||||
|
kc = wm.keyconfigs.addon
|
||||||
|
if kc:
|
||||||
|
km = kc.keymaps.new(name="3D View", space_type="VIEW_3D")
|
||||||
|
kmi = km.keymap_items.new("bim.click_nearest_dimension_anchor", "LEFTMOUSE", "PRESS")
|
||||||
|
_keymaps.append((km, kmi))
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
if not bpy.app.background:
|
if not bpy.app.background:
|
||||||
@@ -188,5 +207,10 @@ def unregister():
|
|||||||
del bpy.types.TextCurve.BIMTextProperties
|
del bpy.types.TextCurve.BIMTextProperties
|
||||||
bpy.app.handlers.load_post.remove(handler.load_post)
|
bpy.app.handlers.load_post.remove(handler.load_post)
|
||||||
bpy.app.handlers.depsgraph_update_pre.remove(handler.depsgraph_update_pre_handler)
|
bpy.app.handlers.depsgraph_update_pre.remove(handler.depsgraph_update_pre_handler)
|
||||||
|
bpy.app.handlers.depsgraph_update_post.remove(handler.depsgraph_update_post_handler)
|
||||||
|
|
||||||
|
for km, kmi in _keymaps:
|
||||||
|
km.keymap_items.remove(kmi)
|
||||||
|
_keymaps.clear()
|
||||||
bpy.types.VIEW3D_MT_image_add.remove(ui.add_object_button)
|
bpy.types.VIEW3D_MT_image_add.remove(ui.add_object_button)
|
||||||
bpy.types.VIEW3D_MT_object_context_menu.remove(menu_func)
|
bpy.types.VIEW3D_MT_object_context_menu.remove(menu_func)
|
||||||
|
|||||||
@@ -807,19 +807,24 @@ class DecoratorData:
|
|||||||
pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Dimension") or {}
|
pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Dimension") or {}
|
||||||
show_description_only = pset_data.get("ShowDescriptionOnly", False)
|
show_description_only = pset_data.get("ShowDescriptionOnly", False)
|
||||||
suppress_zero_inches = pset_data.get("SuppressZeroInches", False)
|
suppress_zero_inches = pset_data.get("SuppressZeroInches", False)
|
||||||
|
suppress_zero_feet = pset_data.get("SuppressZeroFeet", False)
|
||||||
|
is_ordinate = pset_data.get("IsOrdinate", False)
|
||||||
text_prefix = pset_data.get("TextPrefix", None) or ""
|
text_prefix = pset_data.get("TextPrefix", None) or ""
|
||||||
text_suffix = pset_data.get("TextSuffix", None) or ""
|
text_suffix = pset_data.get("TextSuffix", None) or ""
|
||||||
custom_unit_list = pset_data.get("CustomUnit", None) or ""
|
custom_units = list(pset_data.get("CustomUnit", None) or [])
|
||||||
custom_unit = custom_unit_list[0] if custom_unit_list else ""
|
separator = pset_data.get("Separator", None) or " / "
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"dimension_style": dimension_style,
|
"dimension_style": dimension_style,
|
||||||
"show_description_only": show_description_only,
|
"show_description_only": show_description_only,
|
||||||
"suppress_zero_inches": suppress_zero_inches,
|
"suppress_zero_inches": suppress_zero_inches,
|
||||||
|
"suppress_zero_feet": suppress_zero_feet,
|
||||||
|
"is_ordinate": is_ordinate,
|
||||||
"text_prefix": text_prefix,
|
"text_prefix": text_prefix,
|
||||||
"text_suffix": text_suffix,
|
"text_suffix": text_suffix,
|
||||||
"fill_bg": fill_bg,
|
"fill_bg": fill_bg,
|
||||||
"custom_unit": custom_unit,
|
"custom_units": custom_units,
|
||||||
|
"separator": separator,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -490,7 +490,7 @@ class BaseDecorator:
|
|||||||
self.draw_label(context, text=text, line_no=line_number_start, multiline=True, **draw_label_kwargs)
|
self.draw_label(context, text=text, line_no=line_number_start, multiline=True, **draw_label_kwargs)
|
||||||
|
|
||||||
@cache
|
@cache
|
||||||
def format_value(self, context, value, suppress_zero_inches=False, custom_unit=None, in_unit_length=False):
|
def format_value(self, context, value, suppress_zero_inches=False, suppress_zero_feet=False, custom_unit=None, in_unit_length=False):
|
||||||
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
|
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
|
||||||
precision = drawing_pset_data.get("MetricPrecision", None)
|
precision = drawing_pset_data.get("MetricPrecision", None)
|
||||||
if not precision:
|
if not precision:
|
||||||
@@ -502,6 +502,7 @@ class BaseDecorator:
|
|||||||
precision=precision,
|
precision=precision,
|
||||||
decimal_places=decimal_places,
|
decimal_places=decimal_places,
|
||||||
suppress_zero_inches=suppress_zero_inches,
|
suppress_zero_inches=suppress_zero_inches,
|
||||||
|
suppress_zero_feet=suppress_zero_feet,
|
||||||
custom_unit=custom_unit,
|
custom_unit=custom_unit,
|
||||||
in_unit_length=in_unit_length,
|
in_unit_length=in_unit_length,
|
||||||
)
|
)
|
||||||
@@ -718,11 +719,13 @@ class DimensionDecorator(BaseDecorator):
|
|||||||
if not dimension_data:
|
if not dimension_data:
|
||||||
return
|
return
|
||||||
show_description_only = dimension_data["show_description_only"]
|
show_description_only = dimension_data["show_description_only"]
|
||||||
|
is_ordinate = dimension_data["is_ordinate"]
|
||||||
text_prefix = dimension_data["text_prefix"]
|
text_prefix = dimension_data["text_prefix"]
|
||||||
text_suffix = dimension_data["text_suffix"]
|
text_suffix = dimension_data["text_suffix"]
|
||||||
viewportDrawingScale = self.get_viewport_drawing_scale(context)
|
viewportDrawingScale = self.get_viewport_drawing_scale(context)
|
||||||
text_offset_value = viewportDrawingScale * 3
|
text_offset_value = viewportDrawingScale * 3
|
||||||
|
|
||||||
|
ordinate_total = 0.0
|
||||||
for i0, i1 in indices:
|
for i0, i1 in indices:
|
||||||
v0 = Vector(vertices[i0])
|
v0 = Vector(vertices[i0])
|
||||||
v1 = Vector(vertices[i1])
|
v1 = Vector(vertices[i1])
|
||||||
@@ -741,16 +744,25 @@ class DimensionDecorator(BaseDecorator):
|
|||||||
"multiline": True,
|
"multiline": True,
|
||||||
"text_dir": text_dir,
|
"text_dir": text_dir,
|
||||||
}
|
}
|
||||||
base_pos = p0 + text_dir * 0.5
|
base_pos = p1 if is_ordinate else p0 + text_dir * 0.5
|
||||||
|
|
||||||
if not show_description_only:
|
if not show_description_only:
|
||||||
length = (v1 - v0).length
|
segment_length = (v1 - v0).length
|
||||||
text = self.format_value(
|
if is_ordinate:
|
||||||
|
ordinate_total += segment_length
|
||||||
|
length = ordinate_total if is_ordinate else segment_length
|
||||||
|
units_to_format = dimension_data["custom_units"] if dimension_data["custom_units"] else [None]
|
||||||
|
parts = [
|
||||||
|
self.format_value(
|
||||||
context,
|
context,
|
||||||
length,
|
length,
|
||||||
suppress_zero_inches=dimension_data["suppress_zero_inches"],
|
suppress_zero_inches=dimension_data["suppress_zero_inches"],
|
||||||
custom_unit=dimension_data["custom_unit"],
|
suppress_zero_feet=dimension_data["suppress_zero_feet"],
|
||||||
|
custom_unit=unit,
|
||||||
)
|
)
|
||||||
|
for unit in units_to_format
|
||||||
|
]
|
||||||
|
text = dimension_data["separator"].join(str(p) for p in parts)
|
||||||
if isinstance(self, DiameterDecorator):
|
if isinstance(self, DiameterDecorator):
|
||||||
text = "D" + text
|
text = "D" + text
|
||||||
text = text_prefix + text + text_suffix
|
text = text_prefix + text + text_suffix
|
||||||
@@ -761,15 +773,18 @@ class DimensionDecorator(BaseDecorator):
|
|||||||
|
|
||||||
self.draw_label(
|
self.draw_label(
|
||||||
text=text,
|
text=text,
|
||||||
pos=base_pos + text_offset,
|
pos=base_pos + text_offset + (Vector((0, text_offset_value)) if is_ordinate else Vector((0, 0))),
|
||||||
box_alignment="bottom-middle",
|
box_alignment="bottom-right" if is_ordinate else "bottom-middle",
|
||||||
multiline_to_bottom=False,
|
multiline_to_bottom=False,
|
||||||
**common_label_attrs,
|
**common_label_attrs,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not show_description_only and description:
|
if not show_description_only and description:
|
||||||
self.draw_label(
|
self.draw_label(
|
||||||
text=description, pos=base_pos - text_offset, box_alignment="top-middle", **common_label_attrs
|
text=description,
|
||||||
|
pos=base_pos - text_offset + (Vector((0, text_offset_value)) if is_ordinate else Vector((0, 0))),
|
||||||
|
box_alignment="top-right" if is_ordinate else "top-middle",
|
||||||
|
**common_label_attrs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -965,7 +980,9 @@ class RadiusDecorator(BaseDecorator):
|
|||||||
|
|
||||||
def get_text():
|
def get_text():
|
||||||
length = (spline_points[-1] - spline_points[-2]).length
|
length = (spline_points[-1] - spline_points[-2]).length
|
||||||
return "R" + self.format_value(context, length, custom_unit=dimension_data["custom_unit"])
|
units_to_format = dimension_data["custom_units"] if dimension_data["custom_units"] else [None]
|
||||||
|
parts = [self.format_value(context, length, suppress_zero_feet=dimension_data["suppress_zero_feet"], custom_unit=unit) for unit in units_to_format]
|
||||||
|
return "R" + dimension_data["separator"].join(str(p) for p in parts)
|
||||||
|
|
||||||
self.draw_dimension_text(
|
self.draw_dimension_text(
|
||||||
context, get_text, description, dimension_data, pos=pos, text_dir=Vector((1, 0)), box_alignment="center"
|
context, get_text, description, dimension_data, pos=pos, text_dir=Vector((1, 0)), box_alignment="center"
|
||||||
@@ -2104,4 +2121,8 @@ class DecorationsHandler:
|
|||||||
|
|
||||||
object_decorators = DecoratorData.data.get("object_decorators", [])
|
object_decorators = DecoratorData.data.get("object_decorators", [])
|
||||||
for obj, decorator in object_decorators:
|
for obj, decorator in object_decorators:
|
||||||
|
try:
|
||||||
decorator.decorate(context, obj)
|
decorator.decorate(context, obj)
|
||||||
|
except ReferenceError:
|
||||||
|
DecoratorData.is_loaded = False
|
||||||
|
break
|
||||||
|
|||||||
@@ -1285,7 +1285,7 @@ class SnapManager:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
coords = np.empty(vertex_count * 3, dtype=np.float32)
|
coords = np.empty(vertex_count * 3, dtype=np.float32)
|
||||||
mesh.vertices.foreach_get("co", coords) # type: ignore[arg-type]
|
mesh.vertices.foreach_get("co", coords)
|
||||||
coords = coords.reshape(-1, 3)
|
coords = coords.reshape(-1, 3)
|
||||||
|
|
||||||
matrix = np.array(obj_eval.matrix_world, dtype=np.float32)
|
matrix = np.array(obj_eval.matrix_world, dtype=np.float32)
|
||||||
@@ -1789,6 +1789,19 @@ DISC = (
|
|||||||
(1.0, 0.0, 0),
|
(1.0, 0.0, 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Anchor index currently being edited by SetDimensionAnchor (-1 = none).
|
||||||
|
_active_anchor_idx: int = -1
|
||||||
|
# The annotation curve object being edited (kept so the gizmo group stays
|
||||||
|
# visible even when SetDimensionAnchor temporarily changes the active object).
|
||||||
|
_editing_annotation_obj = None
|
||||||
|
|
||||||
|
|
||||||
|
def set_active_anchor(idx: int, annotation_obj=None) -> None:
|
||||||
|
global _active_anchor_idx, _editing_annotation_obj
|
||||||
|
_active_anchor_idx = idx
|
||||||
|
_editing_annotation_obj = annotation_obj if idx >= 0 else None
|
||||||
|
|
||||||
|
|
||||||
X3DISC = (
|
X3DISC = (
|
||||||
(0.0, 0.0, 0.0),
|
(0.0, 0.0, 0.0),
|
||||||
(1.0, 0.0, 0),
|
(1.0, 0.0, 0),
|
||||||
@@ -2120,6 +2133,340 @@ class ExtrusionWidget(types.GizmoGroup):
|
|||||||
self.handle.target_set_prop("offset", prop, "value")
|
self.handle.target_set_prop("offset", prop, "value")
|
||||||
self.guides.target_set_prop("depth", prop, "value")
|
self.guides.target_set_prop("depth", prop, "value")
|
||||||
|
|
||||||
|
|
||||||
|
class GizmoAnchorHandle(bpy.types.Gizmo):
|
||||||
|
"""Visual-only dot at a parametric dimension vertex.
|
||||||
|
|
||||||
|
No draw_select/invoke — any draw_select entry puts the gizmo in Blender's
|
||||||
|
select buffer, which causes the gizmo system to consume the click even
|
||||||
|
without an explicit invoke. All click handling is done by the
|
||||||
|
bim.click_nearest_dimension_anchor keymap operator.
|
||||||
|
"""
|
||||||
|
|
||||||
|
bl_idname = "BIM_GT_anchor_handle"
|
||||||
|
|
||||||
|
__slots__ = ("anchor_index", "custom_shape")
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
self.anchor_index = 0
|
||||||
|
self.custom_shape = self.new_custom_shape(type="TRIS", verts=X3DISC)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
self.draw_custom_shape(self.custom_shape)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class DimensionAnchorWidget(types.GizmoGroup):
|
||||||
|
"""Anchor handle gizmos at each vertex of the active parametric dimension.
|
||||||
|
|
||||||
|
Green dots indicate vertices that are anchored to an IFC element face;
|
||||||
|
orange dots are free world-point anchors. Clicking any dot fires
|
||||||
|
``bim.set_dimension_anchor`` pre-targeted at that vertex index.
|
||||||
|
"""
|
||||||
|
|
||||||
|
bl_idname = "BIM_GGT_dimension_anchors"
|
||||||
|
bl_label = "Dimension Anchor Handles"
|
||||||
|
bl_space_type = "VIEW_3D"
|
||||||
|
bl_region_type = "WINDOW"
|
||||||
|
bl_options = {"3D", "PERSISTENT", "SHOW_MODAL_ALL"}
|
||||||
|
|
||||||
|
_DIM_TYPES = frozenset(("DIMENSION", "RADIUS", "DIAMETER", "ANGLE", "PLAN_LEVEL", "SECTION_LEVEL"))
|
||||||
|
_MAX_ANCHORS = 16
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: bpy.types.Context) -> bool:
|
||||||
|
if not tool.Ifc.get():
|
||||||
|
return False
|
||||||
|
# Stay visible while SetDimensionAnchor is running (active obj may temporarily
|
||||||
|
# be an IFC element in the face-picking phase rather than the annotation).
|
||||||
|
if _active_anchor_idx >= 0 and _editing_annotation_obj is not None:
|
||||||
|
active = context.active_object
|
||||||
|
if active is _editing_annotation_obj:
|
||||||
|
return True # annotation still active
|
||||||
|
if active is not None and tool.Ifc.get_entity(active) is not None:
|
||||||
|
return True # face-picking phase: active obj is a target element
|
||||||
|
# Active object is None or a non-IFC object — the modal ended without
|
||||||
|
# calling set_active_anchor(-1). Reset stale state and fall through.
|
||||||
|
set_active_anchor(-1)
|
||||||
|
obj = context.active_object
|
||||||
|
if not obj or obj.type != "CURVE":
|
||||||
|
return False
|
||||||
|
if not obj.select_get():
|
||||||
|
return False
|
||||||
|
element = tool.Ifc.get_entity(obj)
|
||||||
|
if not element or not element.is_a("IfcAnnotation"):
|
||||||
|
return False
|
||||||
|
import ifcopenshell.util.element as _ue
|
||||||
|
if _ue.get_predefined_type(element) not in cls._DIM_TYPES:
|
||||||
|
return False
|
||||||
|
pset = _ue.get_pset(element, "BBIM_Dimension")
|
||||||
|
return bool(pset and pset.get("Anchors"))
|
||||||
|
|
||||||
|
def setup(self, context: bpy.types.Context) -> None:
|
||||||
|
self._handles: list = []
|
||||||
|
for _ in range(self._MAX_ANCHORS):
|
||||||
|
gz = self.gizmos.new("BIM_GT_anchor_handle")
|
||||||
|
gz.scale_basis = 0.2
|
||||||
|
gz.use_draw_modal = True
|
||||||
|
gz.hide = True
|
||||||
|
self._handles.append(gz)
|
||||||
|
|
||||||
|
def refresh(self, context: bpy.types.Context) -> None:
|
||||||
|
import json
|
||||||
|
import ifcopenshell.util.element as _ue
|
||||||
|
|
||||||
|
obj = _editing_annotation_obj if _active_anchor_idx >= 0 and _editing_annotation_obj else context.active_object
|
||||||
|
if not obj or not obj.data or not getattr(obj.data, "splines", None):
|
||||||
|
for gz in self._handles:
|
||||||
|
gz.hide = True
|
||||||
|
return
|
||||||
|
|
||||||
|
element = tool.Ifc.get_entity(obj)
|
||||||
|
if not element:
|
||||||
|
for gz in self._handles:
|
||||||
|
gz.hide = True
|
||||||
|
return
|
||||||
|
|
||||||
|
pset = _ue.get_pset(element, "BBIM_Dimension")
|
||||||
|
if not pset or not pset.get("Anchors"):
|
||||||
|
for gz in self._handles:
|
||||||
|
gz.hide = True
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
anchors = json.loads(pset["Anchors"])
|
||||||
|
except Exception:
|
||||||
|
for gz in self._handles:
|
||||||
|
gz.hide = True
|
||||||
|
return
|
||||||
|
|
||||||
|
spline = obj.data.splines[0]
|
||||||
|
n = min(len(spline.points), len(anchors), self._MAX_ANCHORS)
|
||||||
|
|
||||||
|
for i in range(n):
|
||||||
|
gz = self._handles[i]
|
||||||
|
raw_co = spline.points[i].co
|
||||||
|
world_co = obj.matrix_world @ raw_co.to_3d()
|
||||||
|
gz.matrix_basis = Matrix.Translation(world_co)
|
||||||
|
gz.anchor_index = i
|
||||||
|
if i == _active_anchor_idx and obj is _editing_annotation_obj:
|
||||||
|
gz.color = (0.2, 0.7, 1.0)
|
||||||
|
gz.color_highlight = (0.4, 0.85, 1.0)
|
||||||
|
elif anchors[i].get("guid"):
|
||||||
|
gz.color = (0.2, 0.85, 0.2)
|
||||||
|
gz.color_highlight = (0.4, 1.0, 0.4)
|
||||||
|
else:
|
||||||
|
gz.color = (0.9, 0.6, 0.1)
|
||||||
|
gz.color_highlight = (1.0, 0.85, 0.2)
|
||||||
|
gz.alpha = 0.85
|
||||||
|
gz.alpha_highlight = 1.0
|
||||||
|
gz.hide = False
|
||||||
|
|
||||||
|
for i in range(n, self._MAX_ANCHORS):
|
||||||
|
self._handles[i].hide = True
|
||||||
|
|
||||||
|
def draw_prepare(self, context: bpy.types.Context) -> None:
|
||||||
|
self.refresh(context)
|
||||||
|
|
||||||
|
|
||||||
|
class DimensionLinePositionWidget(types.GizmoGroup):
|
||||||
|
"""Drag handle for the LinePosition of a parametric dimension annotation.
|
||||||
|
|
||||||
|
Shows two opposing cones at the midpoint of the dimension curve, oriented
|
||||||
|
along the horizontal offset axis (cross(world_Z, dim_direction)). Dragging
|
||||||
|
either cone updates BBIM_Dimension.LinePosition and regenerates the curve in
|
||||||
|
real time. The forward cone points in +offset_dir; the reverse cone in
|
||||||
|
-offset_dir — both respond to mouse movement along the shared axis so the
|
||||||
|
user can drag in either direction from either handle.
|
||||||
|
"""
|
||||||
|
|
||||||
|
bl_idname = "BIM_GGT_dimension_line_position"
|
||||||
|
bl_label = "Dimension Line Position"
|
||||||
|
bl_space_type = "VIEW_3D"
|
||||||
|
bl_region_type = "WINDOW"
|
||||||
|
bl_options = {"3D", "PERSISTENT", "SHOW_MODAL_ALL"}
|
||||||
|
|
||||||
|
_DIM_TYPES = frozenset(("DIMENSION", "RADIUS", "DIAMETER", "ANGLE", "PLAN_LEVEL", "SECTION_LEVEL"))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: bpy.types.Context) -> bool:
|
||||||
|
if not tool.Ifc.get():
|
||||||
|
return False
|
||||||
|
obj = context.active_object
|
||||||
|
if not obj or obj.type != "CURVE":
|
||||||
|
return False
|
||||||
|
element = tool.Ifc.get_entity(obj)
|
||||||
|
if not element or not element.is_a("IfcAnnotation"):
|
||||||
|
return False
|
||||||
|
import ifcopenshell.util.element as _ue
|
||||||
|
if _ue.get_predefined_type(element) not in cls._DIM_TYPES:
|
||||||
|
return False
|
||||||
|
pset = _ue.get_pset(element, "BBIM_Dimension")
|
||||||
|
return bool(pset and pset.get("Anchors") and pset.get("ForcePerpendicularToFace"))
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _offset_dir(obj: bpy.types.Object) -> "Vector | None":
|
||||||
|
"""World-space unit direction perpendicular to the dimension line and world_Z."""
|
||||||
|
if not obj.data or not hasattr(obj.data, "splines") or not obj.data.splines:
|
||||||
|
return None
|
||||||
|
spline = obj.data.splines[0]
|
||||||
|
if len(spline.points) < 2:
|
||||||
|
return None
|
||||||
|
a = obj.matrix_world @ spline.points[0].co.to_3d()
|
||||||
|
b = obj.matrix_world @ spline.points[-1].co.to_3d()
|
||||||
|
dim = b - a
|
||||||
|
if dim.length < 1e-10:
|
||||||
|
return None
|
||||||
|
dim.normalize()
|
||||||
|
world_z = Vector((0.0, 0.0, 1.0))
|
||||||
|
od = world_z.cross(dim)
|
||||||
|
if od.length < 1e-6:
|
||||||
|
od = Vector((1.0, 0.0, 0.0)).cross(dim)
|
||||||
|
if od.length < 1e-6:
|
||||||
|
return None
|
||||||
|
return od.normalized()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _midpoint(obj: bpy.types.Object) -> "Vector":
|
||||||
|
spline = obj.data.splines[0]
|
||||||
|
pts = [obj.matrix_world @ p.co.to_3d() for p in spline.points]
|
||||||
|
return sum(pts, Vector()) / len(pts)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _basis(origin: "Vector", x_axis: "Vector") -> "Matrix":
|
||||||
|
"""4×4 matrix with translation=origin, local-X=x_axis."""
|
||||||
|
ref = Vector((0.0, 0.0, 1.0)) if abs(x_axis.dot(Vector((0.0, 0.0, 1.0)))) < 0.9 else Vector((1.0, 0.0, 0.0))
|
||||||
|
y_ax = x_axis.cross(ref).normalized()
|
||||||
|
z_ax = x_axis.cross(y_ax)
|
||||||
|
return Matrix([
|
||||||
|
[x_axis.x, y_ax.x, z_ax.x, origin.x],
|
||||||
|
[x_axis.y, y_ax.y, z_ax.y, origin.y],
|
||||||
|
[x_axis.z, y_ax.z, z_ax.z, origin.z],
|
||||||
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
|
])
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Value callbacks
|
||||||
|
|
||||||
|
def _get_pos(self) -> float:
|
||||||
|
obj = bpy.context.active_object
|
||||||
|
if not obj:
|
||||||
|
return 0.0
|
||||||
|
element = tool.Ifc.get_entity(obj)
|
||||||
|
if not element:
|
||||||
|
return 0.0
|
||||||
|
import ifcopenshell.util.element as _ue
|
||||||
|
pset = _ue.get_pset(element, "BBIM_Dimension")
|
||||||
|
if not pset:
|
||||||
|
return 0.0
|
||||||
|
stored = pset.get("LinePosition")
|
||||||
|
if stored is not None:
|
||||||
|
return float(stored)
|
||||||
|
# Natural position: projection of midpoint onto offset axis
|
||||||
|
od = self._offset_dir(obj)
|
||||||
|
if od is None:
|
||||||
|
return 0.0
|
||||||
|
return self._midpoint(obj).dot(od)
|
||||||
|
|
||||||
|
def _set_pos(self, value: float) -> None:
|
||||||
|
bpy.ops.ed.undo_push(message="Set Line Position")
|
||||||
|
import json
|
||||||
|
import numpy as np
|
||||||
|
import ifcopenshell.util.element as _ue
|
||||||
|
import ifcopenshell.api.pset as _pset_api
|
||||||
|
import ifcopenshell.api.drawing as drawing_api
|
||||||
|
from bonsai.bim.module.drawing.operator import _update_blender_curve
|
||||||
|
|
||||||
|
obj = bpy.context.active_object
|
||||||
|
if not obj:
|
||||||
|
return
|
||||||
|
file = tool.Ifc.get()
|
||||||
|
if not file:
|
||||||
|
return
|
||||||
|
element = tool.Ifc.get_entity(obj)
|
||||||
|
if not element:
|
||||||
|
return
|
||||||
|
pset_data = _ue.get_pset(element, "BBIM_Dimension")
|
||||||
|
if not pset_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
pset_entity = file.by_id(pset_data["id"])
|
||||||
|
_pset_api.edit_pset(file, pset=pset_entity, properties={"LinePosition": value})
|
||||||
|
|
||||||
|
anchors = json.loads(pset_data.get("Anchors") or "[]")
|
||||||
|
placement_override: dict = {}
|
||||||
|
for a in anchors:
|
||||||
|
guid = a.get("guid")
|
||||||
|
if not guid:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
elem = file.by_guid(guid)
|
||||||
|
elem_obj = tool.Ifc.get_object(elem)
|
||||||
|
if elem_obj:
|
||||||
|
placement_override[elem.id()] = np.array(elem_obj.matrix_world)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
resolved_pts = drawing_api.regenerate_dimension(file, element, placement_override=placement_override)
|
||||||
|
if resolved_pts:
|
||||||
|
_update_blender_curve(element, resolved_pts)
|
||||||
|
tool.Blender.update_viewport()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GizmoGroup interface
|
||||||
|
|
||||||
|
def _make_cone(self, color: tuple, highlight: tuple) -> "bpy.types.Gizmo":
|
||||||
|
gz = self.gizmos.new("BIM_GT_gizmo_cone")
|
||||||
|
gz.color = color
|
||||||
|
gz.alpha = 0.8
|
||||||
|
gz.color_highlight = highlight
|
||||||
|
gz.alpha_highlight = 1.0
|
||||||
|
gz.scale_basis = 0.15
|
||||||
|
gz.use_draw_modal = True
|
||||||
|
gz.prop_name = "Line Position"
|
||||||
|
gz.move_get_cb = self._get_pos
|
||||||
|
gz.move_set_cb = self._set_pos
|
||||||
|
gz.gizmo_group = self
|
||||||
|
gz.delta_scale = 1.0
|
||||||
|
return gz
|
||||||
|
|
||||||
|
def setup(self, context: bpy.types.Context) -> None:
|
||||||
|
color = (0.9, 0.6, 0.1)
|
||||||
|
highlight = (1.0, 0.9, 0.2)
|
||||||
|
self.gz_fwd = self._make_cone(color, highlight)
|
||||||
|
self.gz_rev = self._make_cone(color, highlight)
|
||||||
|
|
||||||
|
def refresh(self, context: bpy.types.Context) -> None:
|
||||||
|
obj = context.active_object
|
||||||
|
if not obj:
|
||||||
|
self.gz_fwd.hide = self.gz_rev.hide = True
|
||||||
|
return
|
||||||
|
|
||||||
|
od = self._offset_dir(obj)
|
||||||
|
if od is None:
|
||||||
|
self.gz_fwd.hide = self.gz_rev.hide = True
|
||||||
|
return
|
||||||
|
|
||||||
|
mid = self._midpoint(obj)
|
||||||
|
# Lift each cone off the dimension line so the arrow base doesn't
|
||||||
|
# overlap anchor dots. 0.3 m gives clear separation at typical zoom.
|
||||||
|
_GAP = 0.15
|
||||||
|
fwd_origin = mid + _GAP * od
|
||||||
|
rev_origin = mid - _GAP * od
|
||||||
|
|
||||||
|
self.gz_fwd.matrix_basis = self._basis(fwd_origin, od)
|
||||||
|
self.gz_fwd.axis = od.copy()
|
||||||
|
self.gz_fwd.hide = False
|
||||||
|
|
||||||
|
# Reverse cone: visually points in -od; same drag axis so both cones
|
||||||
|
# respond identically — drag toward either tip to move the line.
|
||||||
|
self.gz_rev.matrix_basis = self._basis(rev_origin, -od)
|
||||||
|
self.gz_rev.axis = od.copy()
|
||||||
|
self.gz_rev.hide = False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_scale_value(system: str, length_unit: str) -> float:
|
def get_scale_value(system: str, length_unit: str) -> float:
|
||||||
scale_value = 1
|
scale_value = 1
|
||||||
|
|||||||
@@ -16,15 +16,141 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
import numpy as np
|
||||||
from bpy.app.handlers import persistent
|
from bpy.app.handlers import persistent
|
||||||
|
|
||||||
import bonsai.bim.module.drawing.decoration as decoration
|
import bonsai.bim.module.drawing.decoration as decoration
|
||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Parametric dimension auto-regeneration state
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Maps element GUID → list of annotation STEP IDs that reference it.
|
||||||
|
_dim_guid_index: dict = {}
|
||||||
|
# Persistent tessellation cache for the depsgraph handler (element id → shape).
|
||||||
|
_dim_shape_cache: dict = {}
|
||||||
|
# Set True whenever BBIM_Dimension anchors change or a new file loads.
|
||||||
|
_dim_index_dirty: bool = True
|
||||||
|
# Re-entry guard so curve updates don't trigger a second handler call.
|
||||||
|
_dim_handler_running: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_dim_index() -> None:
|
||||||
|
"""Mark the GUID index as stale so it is rebuilt on the next handler call."""
|
||||||
|
global _dim_index_dirty, _dim_shape_cache
|
||||||
|
_dim_index_dirty = True
|
||||||
|
_dim_shape_cache.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def _rebuild_dim_guid_index(file) -> None:
|
||||||
|
global _dim_guid_index, _dim_index_dirty
|
||||||
|
import ifcopenshell.util.element
|
||||||
|
|
||||||
|
_dim_guid_index = {}
|
||||||
|
for annotation in file.by_type("IfcAnnotation"):
|
||||||
|
pset_data = ifcopenshell.util.element.get_pset(annotation, "BBIM_Dimension")
|
||||||
|
if not pset_data or not pset_data.get("Anchors"):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
anchors = json.loads(pset_data["Anchors"])
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
ann_id = annotation.id()
|
||||||
|
for anchor in anchors:
|
||||||
|
guid = anchor.get("guid")
|
||||||
|
if not guid:
|
||||||
|
continue
|
||||||
|
ids = _dim_guid_index.setdefault(guid, [])
|
||||||
|
if ann_id not in ids:
|
||||||
|
ids.append(ann_id)
|
||||||
|
_dim_index_dirty = False
|
||||||
|
|
||||||
|
|
||||||
|
def regenerate_dims_for_layer(file, layer) -> None:
|
||||||
|
"""Regenerate all parametric dimensions anchored to elements that use *layer*."""
|
||||||
|
global _dim_shape_cache, _dim_index_dirty, _dim_guid_index
|
||||||
|
|
||||||
|
if _dim_index_dirty:
|
||||||
|
_rebuild_dim_guid_index(file)
|
||||||
|
|
||||||
|
affected_guids: set = set()
|
||||||
|
for layer_set in file.get_inverse(layer):
|
||||||
|
if not layer_set.is_a("IfcMaterialLayerSet"):
|
||||||
|
continue
|
||||||
|
for inv in file.get_inverse(layer_set):
|
||||||
|
if inv.is_a("IfcRelAssociatesMaterial"):
|
||||||
|
rels = [inv]
|
||||||
|
elif inv.is_a("IfcMaterialLayerSetUsage"):
|
||||||
|
rels = [r for r in file.get_inverse(inv) if r.is_a("IfcRelAssociatesMaterial")]
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
for rel in rels:
|
||||||
|
for element in rel.RelatedObjects:
|
||||||
|
if hasattr(element, "GlobalId"):
|
||||||
|
affected_guids.add(element.GlobalId)
|
||||||
|
_dim_shape_cache.pop(element.id(), None)
|
||||||
|
|
||||||
|
if not affected_guids:
|
||||||
|
return
|
||||||
|
|
||||||
|
annotation_ids: set = set()
|
||||||
|
for guid in affected_guids:
|
||||||
|
for ann_id in _dim_guid_index.get(guid, []):
|
||||||
|
annotation_ids.add(ann_id)
|
||||||
|
|
||||||
|
if not annotation_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
import ifcopenshell.util.element
|
||||||
|
import ifcopenshell.api.drawing as drawing_api
|
||||||
|
import ifcopenshell.geom
|
||||||
|
from bonsai.bim.module.drawing.operator import _update_blender_curve
|
||||||
|
|
||||||
|
geom_settings = ifcopenshell.geom.settings()
|
||||||
|
geom_settings.set("APPLY_DEFAULT_MATERIALS", False)
|
||||||
|
|
||||||
|
for ann_id in annotation_ids:
|
||||||
|
try:
|
||||||
|
annotation = file.by_id(ann_id)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
pset = ifcopenshell.util.element.get_pset(annotation, "BBIM_Dimension")
|
||||||
|
if not pset:
|
||||||
|
continue
|
||||||
|
placement_override: dict = {}
|
||||||
|
try:
|
||||||
|
anchors_raw = json.loads(pset.get("Anchors") or "[]")
|
||||||
|
for anchor in anchors_raw:
|
||||||
|
guid = anchor.get("guid")
|
||||||
|
if not guid:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
elem = file.by_guid(guid)
|
||||||
|
elem_obj = tool.Ifc.get_object(elem)
|
||||||
|
if elem_obj:
|
||||||
|
placement_override[elem.id()] = np.array(elem_obj.matrix_world)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
resolved_pts = drawing_api.regenerate_dimension(
|
||||||
|
file,
|
||||||
|
annotation,
|
||||||
|
settings=geom_settings,
|
||||||
|
shape_cache=_dim_shape_cache,
|
||||||
|
placement_override=placement_override,
|
||||||
|
)
|
||||||
|
if resolved_pts:
|
||||||
|
_update_blender_curve(annotation, resolved_pts)
|
||||||
|
|
||||||
|
|
||||||
@persistent
|
@persistent
|
||||||
def load_post(*args):
|
def load_post(*args):
|
||||||
|
invalidate_dim_index()
|
||||||
props = tool.Drawing.get_document_props()
|
props = tool.Drawing.get_document_props()
|
||||||
if props.should_draw_decorations:
|
if props.should_draw_decorations:
|
||||||
decoration.DecorationsHandler.install(bpy.context)
|
decoration.DecorationsHandler.install(bpy.context)
|
||||||
@@ -58,3 +184,177 @@ def set_active_camera_resolution(scene: bpy.types.Scene) -> None:
|
|||||||
raster_x, raster_y = props.update_camera_resolution()
|
raster_x, raster_y = props.update_camera_resolution()
|
||||||
scene_render.resolution_x = raster_x
|
scene_render.resolution_x = raster_x
|
||||||
scene_render.resolution_y = raster_y
|
scene_render.resolution_y = raster_y
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_dimension_anchors_to_curve(file, annotation, obj) -> bool:
|
||||||
|
"""Sync BBIM_Dimension.Anchors length to match the curve's spline point count.
|
||||||
|
|
||||||
|
Called when the user adds or removes vertices from a dimension annotation in
|
||||||
|
Edit Mode. New vertices get a free WORLD-type anchor at their current world
|
||||||
|
position; removed tail vertices simply lose their anchor entries.
|
||||||
|
|
||||||
|
Returns True if the pset was changed.
|
||||||
|
"""
|
||||||
|
import ifcopenshell.util.element
|
||||||
|
import ifcopenshell.api.pset
|
||||||
|
|
||||||
|
if not obj.data or not getattr(obj.data, "splines", None) or not obj.data.splines:
|
||||||
|
return False
|
||||||
|
|
||||||
|
pset_data = ifcopenshell.util.element.get_pset(annotation, "BBIM_Dimension")
|
||||||
|
if not pset_data or not pset_data.get("Anchors"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
anchors: list = json.loads(pset_data["Anchors"])
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
spline = obj.data.splines[0]
|
||||||
|
spline_world = [obj.matrix_world @ p.co.to_3d() for p in spline.points]
|
||||||
|
n_pts = len(spline_world)
|
||||||
|
n_anchors = len(anchors)
|
||||||
|
|
||||||
|
if n_pts == n_anchors:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Match each spline point to the nearest unused anchor by proximity.
|
||||||
|
# This handles insertions (subdivide) and deletions correctly regardless
|
||||||
|
# of where in the polyline the edit happened.
|
||||||
|
_MATCH_THRESH_SQ = 1e-4 # 1 cm² — distinguishes existing pts from new midpoints
|
||||||
|
used: set = set()
|
||||||
|
new_anchors: list = []
|
||||||
|
|
||||||
|
for pt in spline_world:
|
||||||
|
best_idx, best_sq = None, float("inf")
|
||||||
|
for i, anc in enumerate(anchors):
|
||||||
|
if i in used:
|
||||||
|
continue
|
||||||
|
stored = anc.get("pt")
|
||||||
|
if not stored:
|
||||||
|
continue
|
||||||
|
dx, dy, dz = stored[0] - pt.x, stored[1] - pt.y, stored[2] - pt.z
|
||||||
|
sq = dx * dx + dy * dy + dz * dz
|
||||||
|
if sq < best_sq:
|
||||||
|
best_sq, best_idx = sq, i
|
||||||
|
if best_idx is not None and best_sq < _MATCH_THRESH_SQ:
|
||||||
|
new_anchors.append(anchors[best_idx])
|
||||||
|
used.add(best_idx)
|
||||||
|
else:
|
||||||
|
new_anchors.append({
|
||||||
|
"guid": None,
|
||||||
|
"type": "WORLD",
|
||||||
|
"addr": {},
|
||||||
|
"hint": None,
|
||||||
|
"pt": [pt.x, pt.y, pt.z],
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
pset_entity = file.by_id(pset_data["id"])
|
||||||
|
ifcopenshell.api.pset.edit_pset(file, pset=pset_entity, properties={"Anchors": json.dumps(new_anchors)})
|
||||||
|
invalidate_dim_index()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@persistent
|
||||||
|
def depsgraph_update_post_handler(scene, depsgraph):
|
||||||
|
"""Auto-regenerate parametric dimensions when referenced elements are moved."""
|
||||||
|
global _dim_handler_running, _dim_index_dirty, _dim_guid_index, _dim_shape_cache
|
||||||
|
|
||||||
|
if _dim_handler_running:
|
||||||
|
return
|
||||||
|
|
||||||
|
file = tool.Ifc.get()
|
||||||
|
if not file:
|
||||||
|
return
|
||||||
|
|
||||||
|
if _dim_index_dirty:
|
||||||
|
_rebuild_dim_guid_index(file)
|
||||||
|
|
||||||
|
import ifcopenshell.util.element
|
||||||
|
|
||||||
|
moved_guids: set = set()
|
||||||
|
edited_annotation_ids: set = set()
|
||||||
|
|
||||||
|
for update in depsgraph.updates:
|
||||||
|
obj = update.id
|
||||||
|
if not isinstance(obj, bpy.types.Object):
|
||||||
|
continue
|
||||||
|
if not (update.is_updated_transform or update.is_updated_geometry):
|
||||||
|
continue
|
||||||
|
element = tool.Ifc.get_entity(obj)
|
||||||
|
if element is None or not hasattr(element, "GlobalId"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
if update.is_updated_geometry and obj.type == "CURVE" and element.is_a("IfcAnnotation"):
|
||||||
|
import ifcopenshell.util.element as _ue
|
||||||
|
ptype = _ue.get_predefined_type(element)
|
||||||
|
if ptype in ("DIMENSION", "RADIUS", "DIAMETER", "ANGLE", "PLAN_LEVEL", "SECTION_LEVEL"):
|
||||||
|
changed = _sync_dimension_anchors_to_curve(file, element, obj)
|
||||||
|
if changed:
|
||||||
|
edited_annotation_ids.add(element.id())
|
||||||
|
continue
|
||||||
|
|
||||||
|
moved_guids.add(element.GlobalId)
|
||||||
|
if update.is_updated_geometry:
|
||||||
|
_dim_shape_cache.pop(element.id(), None)
|
||||||
|
|
||||||
|
annotation_ids: set = set(edited_annotation_ids)
|
||||||
|
for guid in moved_guids:
|
||||||
|
for ann_id in _dim_guid_index.get(guid, []):
|
||||||
|
annotation_ids.add(ann_id)
|
||||||
|
|
||||||
|
if not annotation_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
import ifcopenshell.api.drawing as drawing_api
|
||||||
|
import ifcopenshell.geom
|
||||||
|
from bonsai.bim.module.drawing.operator import _update_blender_curve
|
||||||
|
|
||||||
|
geom_settings = ifcopenshell.geom.settings()
|
||||||
|
geom_settings.set("APPLY_DEFAULT_MATERIALS", False)
|
||||||
|
|
||||||
|
_dim_handler_running = True
|
||||||
|
try:
|
||||||
|
for ann_id in annotation_ids:
|
||||||
|
try:
|
||||||
|
annotation = file.by_id(ann_id)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
pset = ifcopenshell.util.element.get_pset(annotation, "BBIM_Dimension")
|
||||||
|
if not pset:
|
||||||
|
continue
|
||||||
|
|
||||||
|
placement_override: dict = {}
|
||||||
|
try:
|
||||||
|
anchors_raw = json.loads(pset.get("Anchors") or "[]")
|
||||||
|
for anchor in anchors_raw:
|
||||||
|
guid = anchor.get("guid")
|
||||||
|
if not guid:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
elem = file.by_guid(guid)
|
||||||
|
elem_id = elem.id()
|
||||||
|
if elem_id in placement_override:
|
||||||
|
continue
|
||||||
|
elem_obj = tool.Ifc.get_object(elem)
|
||||||
|
if elem_obj:
|
||||||
|
placement_override[elem_id] = np.array(elem_obj.matrix_world)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
resolved_pts = drawing_api.regenerate_dimension(
|
||||||
|
file,
|
||||||
|
annotation,
|
||||||
|
settings=geom_settings,
|
||||||
|
shape_cache=_dim_shape_cache,
|
||||||
|
placement_override=placement_override,
|
||||||
|
)
|
||||||
|
if resolved_pts:
|
||||||
|
_update_blender_curve(annotation, resolved_pts)
|
||||||
|
finally:
|
||||||
|
_dim_handler_running = False
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ def format_distance(
|
|||||||
precision=None,
|
precision=None,
|
||||||
decimal_places=None,
|
decimal_places=None,
|
||||||
suppress_zero_inches=False,
|
suppress_zero_inches=False,
|
||||||
|
suppress_zero_feet=False,
|
||||||
in_unit_length=False,
|
in_unit_length=False,
|
||||||
custom_unit=None,
|
custom_unit=None,
|
||||||
):
|
):
|
||||||
@@ -310,10 +311,10 @@ def format_distance(
|
|||||||
tx_dist = ""
|
tx_dist = ""
|
||||||
if feet:
|
if feet:
|
||||||
tx_dist += str(feet) + "'"
|
tx_dist += str(feet) + "'"
|
||||||
if not feet and not add_inches:
|
if not feet and not add_inches and not suppress_zero_feet:
|
||||||
tx_dist += str(feet) + "'"
|
tx_dist += str(feet) + "'"
|
||||||
|
|
||||||
if not feet and add_inches:
|
if not feet and add_inches and unit_length != "INCHES" and not suppress_zero_feet:
|
||||||
if value < 0:
|
if value < 0:
|
||||||
tx_dist += "-0' - "
|
tx_dist += "-0' - "
|
||||||
else:
|
else:
|
||||||
@@ -456,7 +457,8 @@ def format_distance(
|
|||||||
tx_dist = fmt % d_cm
|
tx_dist = fmt % d_cm
|
||||||
|
|
||||||
else:
|
else:
|
||||||
tx_dist = fmt % value
|
assert f"Unexpected unit_system - '{unit_system}'."
|
||||||
|
# tx_dist = fmt % value
|
||||||
|
|
||||||
return tx_dist
|
return tx_dist
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,6 @@ import ifcopenshell.api.pset
|
|||||||
import ifcopenshell.util.element
|
import ifcopenshell.util.element
|
||||||
from bpy.props import (
|
from bpy.props import (
|
||||||
BoolProperty,
|
BoolProperty,
|
||||||
BoolVectorProperty,
|
|
||||||
CollectionProperty,
|
CollectionProperty,
|
||||||
EnumProperty,
|
EnumProperty,
|
||||||
FloatProperty,
|
FloatProperty,
|
||||||
@@ -861,13 +860,13 @@ class BIMTextProperties(PropertyGroup):
|
|||||||
is_editing: BoolProperty(name="Is Editing", default=False)
|
is_editing: BoolProperty(name="Is Editing", default=False)
|
||||||
literals: CollectionProperty(name="Literals", type=LiteralProps)
|
literals: CollectionProperty(name="Literals", type=LiteralProps)
|
||||||
newline_at: IntProperty(name="Newline At")
|
newline_at: IntProperty(name="Newline At")
|
||||||
symbol: EnumProperty( # pyright: ignore[reportRedeclaration]
|
symbol: EnumProperty(
|
||||||
name="Symbol",
|
name="Symbol",
|
||||||
description="Symbol from symbols.svg to use for this text.",
|
description="Symbol from symbols.svg to use for this text.",
|
||||||
items=[(s, s, "") for s in ["NO SYMBOL", "CUSTOM SYMBOL"] + tool.Drawing.DEFAULT_SYMBOLS],
|
items=[(s, s, "") for s in ["NO SYMBOL", "CUSTOM SYMBOL"] + tool.Drawing.DEFAULT_SYMBOLS],
|
||||||
default="NO SYMBOL",
|
default="NO SYMBOL",
|
||||||
)
|
)
|
||||||
custom_symbol: StringProperty( # pyright: ignore[reportRedeclaration]
|
custom_symbol: StringProperty(
|
||||||
name="Custom Symbol",
|
name="Custom Symbol",
|
||||||
description="Non-default symbol to use for this text.",
|
description="Non-default symbol to use for this text.",
|
||||||
)
|
)
|
||||||
@@ -987,6 +986,160 @@ def update_sheet_data(self, context):
|
|||||||
SheetsData.is_loaded = False
|
SheetsData.is_loaded = False
|
||||||
|
|
||||||
|
|
||||||
|
def _update_force_perpendicular(self, context):
|
||||||
|
"""Apply ForcePerpendicularToFace to all selected dimension annotations and regenerate them."""
|
||||||
|
import json
|
||||||
|
import numpy as np
|
||||||
|
import ifcopenshell.util.element
|
||||||
|
import ifcopenshell.api.pset
|
||||||
|
import ifcopenshell.api.drawing as drawing_api
|
||||||
|
import bonsai.tool as tool
|
||||||
|
|
||||||
|
file = tool.Ifc.get()
|
||||||
|
if not file:
|
||||||
|
return
|
||||||
|
|
||||||
|
new_value = self.force_perpendicular_to_face
|
||||||
|
_DIM_TYPES = frozenset(("DIMENSION", "RADIUS", "DIAMETER", "ANGLE", "PLAN_LEVEL", "SECTION_LEVEL"))
|
||||||
|
|
||||||
|
targets = []
|
||||||
|
for obj in context.selected_objects:
|
||||||
|
element = tool.Ifc.get_entity(obj)
|
||||||
|
if not element or not element.is_a("IfcAnnotation"):
|
||||||
|
continue
|
||||||
|
if ifcopenshell.util.element.get_predefined_type(element) not in _DIM_TYPES:
|
||||||
|
continue
|
||||||
|
pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Dimension")
|
||||||
|
if not pset_data:
|
||||||
|
continue
|
||||||
|
targets.append((obj, element, pset_data))
|
||||||
|
|
||||||
|
if not targets:
|
||||||
|
return
|
||||||
|
|
||||||
|
from bonsai.bim.module.drawing.operator import _update_blender_curve
|
||||||
|
|
||||||
|
for obj, element, pset_data in targets:
|
||||||
|
pset_entity = file.by_id(pset_data["id"])
|
||||||
|
ifcopenshell.api.pset.edit_pset(file, pset=pset_entity, properties={"ForcePerpendicularToFace": new_value})
|
||||||
|
|
||||||
|
anchors = json.loads(pset_data.get("Anchors") or "[]")
|
||||||
|
placement_override = {}
|
||||||
|
for a in anchors:
|
||||||
|
guid = a.get("guid")
|
||||||
|
if not guid:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
elem = file.by_guid(guid)
|
||||||
|
elem_obj = tool.Ifc.get_object(elem)
|
||||||
|
if elem_obj:
|
||||||
|
placement_override[elem.id()] = np.array(elem_obj.matrix_world)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
resolved_pts = drawing_api.regenerate_dimension(file, element, placement_override=placement_override)
|
||||||
|
if resolved_pts:
|
||||||
|
_update_blender_curve(element, resolved_pts)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_line_position(self) -> float:
|
||||||
|
"""Return LinePosition from the active annotation's BBIM_Dimension pset.
|
||||||
|
|
||||||
|
Falls back to the natural anchor projection when LinePosition has not been
|
||||||
|
explicitly set, so the field always shows a meaningful value.
|
||||||
|
"""
|
||||||
|
import math
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
import bpy as _bpy
|
||||||
|
import ifcopenshell.util.element as _ue
|
||||||
|
import bonsai.tool as _tool
|
||||||
|
obj = getattr(_bpy.context, "active_object", None)
|
||||||
|
if obj:
|
||||||
|
element = _tool.Ifc.get_entity(obj)
|
||||||
|
if element and element.is_a("IfcAnnotation"):
|
||||||
|
pset = _ue.get_pset(element, "BBIM_Dimension")
|
||||||
|
if pset:
|
||||||
|
stored = pset.get("LinePosition")
|
||||||
|
if stored is not None:
|
||||||
|
return float(stored)
|
||||||
|
raw = pset.get("Anchors")
|
||||||
|
if raw:
|
||||||
|
anchors = json.loads(raw)
|
||||||
|
if len(anchors) >= 2 and anchors[0].get("pt") and anchors[1].get("pt"):
|
||||||
|
a, b = anchors[0]["pt"], anchors[1]["pt"]
|
||||||
|
dx, dy, dz = b[0] - a[0], b[1] - a[1], b[2] - a[2]
|
||||||
|
m = math.sqrt(dx * dx + dy * dy + dz * dz)
|
||||||
|
if m > 1e-10:
|
||||||
|
ddx, ddy, ddz = dx / m, dy / m, dz / m
|
||||||
|
# cross(world_Z=(0,0,1), dim_dir) = (-ddy, ddx, 0)
|
||||||
|
ox, oy, oz = -ddy, ddx, 0.0
|
||||||
|
om = math.sqrt(ox * ox + oy * oy)
|
||||||
|
if om > 1e-6:
|
||||||
|
od = (ox / om, oy / om, 0.0)
|
||||||
|
pt = anchors[0]["pt"]
|
||||||
|
return float(pt[0] * od[0] + pt[1] * od[1])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _set_line_position(self, value: float) -> None:
|
||||||
|
"""Write LinePosition to all selected dimension annotations and regenerate."""
|
||||||
|
import json
|
||||||
|
import numpy as np
|
||||||
|
import ifcopenshell.util.element
|
||||||
|
import ifcopenshell.api.pset
|
||||||
|
import ifcopenshell.api.drawing as drawing_api
|
||||||
|
import bonsai.tool as tool
|
||||||
|
|
||||||
|
file = tool.Ifc.get()
|
||||||
|
if not file:
|
||||||
|
return
|
||||||
|
|
||||||
|
_DIM_TYPES = frozenset(("DIMENSION", "RADIUS", "DIAMETER", "ANGLE", "PLAN_LEVEL", "SECTION_LEVEL"))
|
||||||
|
|
||||||
|
targets = []
|
||||||
|
import bpy as _bpy
|
||||||
|
for obj in getattr(_bpy.context, "selected_objects", []):
|
||||||
|
element = tool.Ifc.get_entity(obj)
|
||||||
|
if not element or not element.is_a("IfcAnnotation"):
|
||||||
|
continue
|
||||||
|
if ifcopenshell.util.element.get_predefined_type(element) not in _DIM_TYPES:
|
||||||
|
continue
|
||||||
|
pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Dimension")
|
||||||
|
if not pset_data:
|
||||||
|
continue
|
||||||
|
targets.append((obj, element, pset_data))
|
||||||
|
|
||||||
|
if not targets:
|
||||||
|
return
|
||||||
|
|
||||||
|
from bonsai.bim.module.drawing.operator import _update_blender_curve
|
||||||
|
|
||||||
|
for obj, element, pset_data in targets:
|
||||||
|
pset_entity = file.by_id(pset_data["id"])
|
||||||
|
ifcopenshell.api.pset.edit_pset(file, pset=pset_entity, properties={"LinePosition": value})
|
||||||
|
|
||||||
|
anchors = json.loads(pset_data.get("Anchors") or "[]")
|
||||||
|
placement_override = {}
|
||||||
|
for a in anchors:
|
||||||
|
guid = a.get("guid")
|
||||||
|
if not guid:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
elem = file.by_guid(guid)
|
||||||
|
elem_obj = tool.Ifc.get_object(elem)
|
||||||
|
if elem_obj:
|
||||||
|
placement_override[elem.id()] = np.array(elem_obj.matrix_world)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
resolved_pts = drawing_api.regenerate_dimension(file, element, placement_override=placement_override)
|
||||||
|
if resolved_pts:
|
||||||
|
_update_blender_curve(element, resolved_pts)
|
||||||
|
|
||||||
|
|
||||||
class BIMAnnotationProperties(PropertyGroup):
|
class BIMAnnotationProperties(PropertyGroup):
|
||||||
object_type: bpy.props.EnumProperty(
|
object_type: bpy.props.EnumProperty(
|
||||||
name="Annotation Object Type", items=annotation_classes, default="TEXT", update=update_annotation_object_type
|
name="Annotation Object Type", items=annotation_classes, default="TEXT", update=update_annotation_object_type
|
||||||
@@ -1000,6 +1153,19 @@ class BIMAnnotationProperties(PropertyGroup):
|
|||||||
)
|
)
|
||||||
is_adding_type: bpy.props.BoolProperty(default=False)
|
is_adding_type: bpy.props.BoolProperty(default=False)
|
||||||
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
|
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
|
||||||
|
force_perpendicular_to_face: bpy.props.BoolProperty(
|
||||||
|
name="Force ⊥ to Face",
|
||||||
|
description="Constrain dimension vertices to the face normal of the first anchor. When dimensions are selected, toggling this updates them all.",
|
||||||
|
default=False,
|
||||||
|
update=_update_force_perpendicular,
|
||||||
|
)
|
||||||
|
line_position: bpy.props.FloatProperty(
|
||||||
|
name="Line Position",
|
||||||
|
description="Absolute world position of the dimension line along the horizontal axis perpendicular to the dimension. The line is held at this fixed global coordinate even when the measured geometry moves. Updates all selected dimensions.",
|
||||||
|
unit="LENGTH",
|
||||||
|
get=_get_line_position,
|
||||||
|
set=_set_line_position,
|
||||||
|
)
|
||||||
is_manual_reference: bpy.props.BoolProperty(
|
is_manual_reference: bpy.props.BoolProperty(
|
||||||
name="Is a Reference",
|
name="Is a Reference",
|
||||||
default=False,
|
default=False,
|
||||||
|
|||||||
@@ -366,9 +366,6 @@ class BaseLinesShader(BaseShader):
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, gap_size=16):
|
|
||||||
super().__init__(gap_size=gap_size)
|
|
||||||
|
|
||||||
def glenable(self):
|
def glenable(self):
|
||||||
super().glenable()
|
super().glenable()
|
||||||
|
|
||||||
|
|||||||
@@ -1397,14 +1397,18 @@ class SvgWriter:
|
|||||||
|
|
||||||
def get_text():
|
def get_text():
|
||||||
radius = (points[-1].co - points[-2].co).length
|
radius = (points[-1].co - points[-2].co).length
|
||||||
radius = helper.format_distance(
|
units_to_format = dimension_data["custom_units"] if dimension_data["custom_units"] else [None]
|
||||||
|
parts = [
|
||||||
|
helper.format_distance(
|
||||||
radius,
|
radius,
|
||||||
precision=self.precision,
|
precision=self.precision,
|
||||||
decimal_places=self.decimal_places,
|
decimal_places=self.decimal_places,
|
||||||
custom_unit=dimension_data["custom_unit"],
|
suppress_zero_feet=dimension_data["suppress_zero_feet"],
|
||||||
|
custom_unit=unit,
|
||||||
)
|
)
|
||||||
text = f"R{radius}"
|
for unit in units_to_format
|
||||||
return text
|
]
|
||||||
|
return "R" + dimension_data["separator"].join(str(p) for p in parts)
|
||||||
|
|
||||||
self.draw_dimension_text(
|
self.draw_dimension_text(
|
||||||
get_text, tag, dimension_data, text_position=text_position, class_str="RADIUS", box_alignment="center"
|
get_text, tag, dimension_data, text_position=text_position, class_str="RADIUS", box_alignment="center"
|
||||||
@@ -1529,10 +1533,12 @@ class SvgWriter:
|
|||||||
text_format=lambda x: "D" + x,
|
text_format=lambda x: "D" + x,
|
||||||
show_description_only=dimension_data["show_description_only"],
|
show_description_only=dimension_data["show_description_only"],
|
||||||
suppress_zero_inches=dimension_data["suppress_zero_inches"],
|
suppress_zero_inches=dimension_data["suppress_zero_inches"],
|
||||||
|
suppress_zero_feet=dimension_data["suppress_zero_feet"],
|
||||||
text_prefix=dimension_data["text_prefix"],
|
text_prefix=dimension_data["text_prefix"],
|
||||||
text_suffix=dimension_data["text_suffix"],
|
text_suffix=dimension_data["text_suffix"],
|
||||||
fill_bg=dimension_data["fill_bg"],
|
fill_bg=dimension_data["fill_bg"],
|
||||||
custom_unit=dimension_data["custom_unit"],
|
custom_units=dimension_data["custom_units"],
|
||||||
|
separator=dimension_data["separator"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def draw_dimension_annotations(self, obj: bpy.types.Object) -> None:
|
def draw_dimension_annotations(self, obj: bpy.types.Object) -> None:
|
||||||
@@ -1543,11 +1549,15 @@ class SvgWriter:
|
|||||||
dimension_data = DecoratorData.get_dimension_data(obj)
|
dimension_data = DecoratorData.get_dimension_data(obj)
|
||||||
|
|
||||||
assert isinstance(obj.data, bpy.types.Curve)
|
assert isinstance(obj.data, bpy.types.Curve)
|
||||||
|
is_ordinate = dimension_data["is_ordinate"]
|
||||||
for spline in obj.data.splines:
|
for spline in obj.data.splines:
|
||||||
points = self.get_spline_points(spline)
|
points = self.get_spline_points(spline)
|
||||||
|
ordinate_total = 0.0
|
||||||
for i in range(len(points) - 1):
|
for i in range(len(points) - 1):
|
||||||
v0_global = matrix_world @ points[i].co.xyz
|
v0_global = matrix_world @ points[i].co.xyz
|
||||||
v1_global = matrix_world @ points[i + 1].co.xyz
|
v1_global = matrix_world @ points[i + 1].co.xyz
|
||||||
|
if is_ordinate:
|
||||||
|
ordinate_total += (v1_global - v0_global).length
|
||||||
self.draw_dimension_annotation(
|
self.draw_dimension_annotation(
|
||||||
v0_global,
|
v0_global,
|
||||||
v1_global,
|
v1_global,
|
||||||
@@ -1555,10 +1565,13 @@ class SvgWriter:
|
|||||||
dimension_text=dimension_text,
|
dimension_text=dimension_text,
|
||||||
show_description_only=dimension_data["show_description_only"],
|
show_description_only=dimension_data["show_description_only"],
|
||||||
suppress_zero_inches=dimension_data["suppress_zero_inches"],
|
suppress_zero_inches=dimension_data["suppress_zero_inches"],
|
||||||
|
suppress_zero_feet=dimension_data["suppress_zero_feet"],
|
||||||
text_prefix=dimension_data["text_prefix"],
|
text_prefix=dimension_data["text_prefix"],
|
||||||
text_suffix=dimension_data["text_suffix"],
|
text_suffix=dimension_data["text_suffix"],
|
||||||
fill_bg=dimension_data["fill_bg"],
|
fill_bg=dimension_data["fill_bg"],
|
||||||
custom_unit=dimension_data["custom_unit"],
|
custom_units=dimension_data["custom_units"],
|
||||||
|
separator=dimension_data["separator"],
|
||||||
|
distance_override=ordinate_total if is_ordinate else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
def draw_measureit_arch_dimension_annotations(self) -> None:
|
def draw_measureit_arch_dimension_annotations(self) -> None:
|
||||||
@@ -1582,10 +1595,13 @@ class SvgWriter:
|
|||||||
text_format=lambda x: x,
|
text_format=lambda x: x,
|
||||||
show_description_only=False,
|
show_description_only=False,
|
||||||
suppress_zero_inches=False,
|
suppress_zero_inches=False,
|
||||||
|
suppress_zero_feet=False,
|
||||||
text_prefix="",
|
text_prefix="",
|
||||||
text_suffix="",
|
text_suffix="",
|
||||||
fill_bg=False,
|
fill_bg=False,
|
||||||
custom_unit=None,
|
custom_units=None,
|
||||||
|
separator=" / ",
|
||||||
|
distance_override=None,
|
||||||
) -> None:
|
) -> None:
|
||||||
offset = Vector([self.raw_width, self.raw_height]) / 2
|
offset = Vector([self.raw_width, self.raw_height]) / 2
|
||||||
v0 = self.project_point_onto_camera(v0_global)
|
v0 = self.project_point_onto_camera(v0_global)
|
||||||
@@ -1598,6 +1614,9 @@ class SvgWriter:
|
|||||||
sheet_dimension = (end - start).length
|
sheet_dimension = (end - start).length
|
||||||
|
|
||||||
# if annotation can't fit offset text to the right of marker
|
# if annotation can't fit offset text to the right of marker
|
||||||
|
if distance_override is not None:
|
||||||
|
text_position = end
|
||||||
|
else:
|
||||||
text_position = mid if sheet_dimension > 5 else (end + (3 * vector.normalized()))
|
text_position = mid if sheet_dimension > 5 else (end + (3 * vector.normalized()))
|
||||||
angle = math.degrees(vector.angle_signed(Vector((1, 0))))
|
angle = math.degrees(vector.angle_signed(Vector((1, 0))))
|
||||||
|
|
||||||
@@ -1613,15 +1632,20 @@ class SvgWriter:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if not show_description_only:
|
if not show_description_only:
|
||||||
dimension = (v1_global - v0_global).length
|
dimension = distance_override if distance_override is not None else (v1_global - v0_global).length
|
||||||
dimension = helper.format_distance(
|
units_to_format = custom_units if custom_units else [None]
|
||||||
|
parts = [
|
||||||
|
helper.format_distance(
|
||||||
dimension,
|
dimension,
|
||||||
precision=self.precision,
|
precision=self.precision,
|
||||||
decimal_places=self.decimal_places,
|
decimal_places=self.decimal_places,
|
||||||
suppress_zero_inches=suppress_zero_inches,
|
suppress_zero_inches=suppress_zero_inches,
|
||||||
custom_unit=custom_unit,
|
suppress_zero_feet=suppress_zero_feet,
|
||||||
|
custom_unit=unit,
|
||||||
)
|
)
|
||||||
text = text_prefix + str(dimension) + text_suffix
|
for unit in units_to_format
|
||||||
|
]
|
||||||
|
text = text_prefix + separator.join(str(p) for p in parts) + text_suffix
|
||||||
else:
|
else:
|
||||||
if not dimension_text:
|
if not dimension_text:
|
||||||
return
|
return
|
||||||
@@ -1629,8 +1653,8 @@ class SvgWriter:
|
|||||||
|
|
||||||
text_tags += self.create_text_tag(
|
text_tags += self.create_text_tag(
|
||||||
text,
|
text,
|
||||||
text_position + perpendicular,
|
text_position + perpendicular + (Vector((0, 1.5)) if distance_override is not None else Vector((0, 0))),
|
||||||
box_alignment="bottom-middle",
|
box_alignment="bottom-right" if distance_override is not None else "bottom-middle",
|
||||||
multiline_to_bottom=False,
|
multiline_to_bottom=False,
|
||||||
**text_tag_kwargs,
|
**text_tag_kwargs,
|
||||||
)
|
)
|
||||||
@@ -1638,8 +1662,8 @@ class SvgWriter:
|
|||||||
if not show_description_only and dimension_text:
|
if not show_description_only and dimension_text:
|
||||||
text_tags += self.create_text_tag(
|
text_tags += self.create_text_tag(
|
||||||
dimension_text,
|
dimension_text,
|
||||||
text_position - perpendicular,
|
text_position - perpendicular + (Vector((0, 1.5)) if distance_override is not None else Vector((0, 0))),
|
||||||
box_alignment="top-middle",
|
box_alignment="top-right" if distance_override is not None else "top-middle",
|
||||||
multiline_to_bottom=True,
|
multiline_to_bottom=True,
|
||||||
**text_tag_kwargs,
|
**text_tag_kwargs,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -563,6 +563,7 @@ class BIM_PT_product_assignments(Panel):
|
|||||||
col.enabled = bool(ProductAssignmentsData.data["relating_product"])
|
col.enabled = bool(ProductAssignmentsData.data["relating_product"])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_category_icon(category_name):
|
def get_category_icon(category_name):
|
||||||
"""Get appropriate icon for each category"""
|
"""Get appropriate icon for each category"""
|
||||||
icons = {
|
icons = {
|
||||||
@@ -827,7 +828,6 @@ class BIM_PT_text(Panel):
|
|||||||
|
|
||||||
for i, literal_data in enumerate(text_data["Literals"]):
|
for i, literal_data in enumerate(text_data["Literals"]):
|
||||||
box = self.layout.box()
|
box = self.layout.box()
|
||||||
box.label(text=f"Literal[{i}]:")
|
|
||||||
|
|
||||||
# Combine both approaches: clickable attributes from PR #7292 and display from PR #7106
|
# Combine both approaches: clickable attributes from PR #7292 and display from PR #7106
|
||||||
for attribute in literal_data:
|
for attribute in literal_data:
|
||||||
|
|||||||
@@ -114,7 +114,11 @@ class AnnotationTool(WorkSpaceTool):
|
|||||||
bl_description = "Gives you Annotation related superpowers"
|
bl_description = "Gives you Annotation related superpowers"
|
||||||
bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.annotation")
|
bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.annotation")
|
||||||
bl_widget = None
|
bl_widget = None
|
||||||
bl_keymap = tool.Blender.get_default_selection_keypmap() + (
|
bl_keymap = (
|
||||||
|
# Before view3d.select: tool keymaps take priority over the addon keymap
|
||||||
|
# where ClickNearestDimensionAnchor is also registered.
|
||||||
|
("bim.click_nearest_dimension_anchor", {"type": "LEFTMOUSE", "value": "PRESS"}, None),
|
||||||
|
) + tool.Blender.get_default_selection_keypmap() + (
|
||||||
("bim.annotation_hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
|
("bim.annotation_hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
|
||||||
("bim.annotation_hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}),
|
("bim.annotation_hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}),
|
||||||
("bim.annotation_hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}),
|
("bim.annotation_hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}),
|
||||||
@@ -221,12 +225,29 @@ class AnnotationToolUI:
|
|||||||
props = tool.Drawing.get_document_props()
|
props = tool.Drawing.get_document_props()
|
||||||
row.prop(props, "should_draw_decorations", text="Viewport Annotations")
|
row.prop(props, "should_draw_decorations", text="Viewport Annotations")
|
||||||
|
|
||||||
|
_DIMENSION_TYPES = frozenset(("DIMENSION", "RADIUS", "DIAMETER", "ANGLE", "PLAN_LEVEL", "SECTION_LEVEL"))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def draw_edit_object_interface(cls, context):
|
def draw_edit_object_interface(cls, context):
|
||||||
obj = bpy.context.active_object
|
obj = bpy.context.active_object
|
||||||
if tool.Ifc.get_entity(obj) and DecoratorData.get_text_data(obj):
|
if tool.Ifc.get_entity(obj) and DecoratorData.get_text_data(obj):
|
||||||
add_layout_hotkey_operator(cls.layout, "Edit Text", "S_E", "")
|
add_layout_hotkey_operator(cls.layout, "Edit Text", "S_E", "")
|
||||||
|
|
||||||
|
obj = context.active_object
|
||||||
|
element = tool.Ifc.get_entity(obj) if obj else None
|
||||||
|
if element and element.is_a("IfcAnnotation"):
|
||||||
|
ptype = ifcopenshell.util.element.get_predefined_type(element)
|
||||||
|
if ptype in cls._DIMENSION_TYPES:
|
||||||
|
cls.layout.separator()
|
||||||
|
ann_props = tool.Drawing.get_annotation_props()
|
||||||
|
if ann_props.force_perpendicular_to_face:
|
||||||
|
row = cls.layout.row(align=True)
|
||||||
|
row.prop(ann_props, "line_position")
|
||||||
|
cls.layout.separator()
|
||||||
|
row = cls.layout.row(align=True)
|
||||||
|
op = row.operator("bim.regenerate_dimensions", icon="FILE_REFRESH", text="Regenerate")
|
||||||
|
op.active_only = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def draw_type_selection_interface(cls):
|
def draw_type_selection_interface(cls):
|
||||||
# shared by both sidebar and header
|
# shared by both sidebar and header
|
||||||
@@ -249,6 +270,11 @@ class AnnotationToolUI:
|
|||||||
|
|
||||||
add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation")
|
add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation")
|
||||||
|
|
||||||
|
_DIMENSION_TYPES = {"DIMENSION", "RADIUS", "DIAMETER", "ANGLE", "PLAN_LEVEL", "SECTION_LEVEL"}
|
||||||
|
if object_type in _DIMENSION_TYPES:
|
||||||
|
row = cls.layout.row(align=True)
|
||||||
|
row.prop(cls.props, "force_perpendicular_to_face")
|
||||||
|
|
||||||
if object_type in ("ELEVATION", "SECTION"):
|
if object_type in ("ELEVATION", "SECTION"):
|
||||||
row = cls.layout.row(align=True)
|
row = cls.layout.row(align=True)
|
||||||
row.prop(cls.props, "is_manual_reference")
|
row.prop(cls.props, "is_manual_reference")
|
||||||
@@ -335,8 +361,16 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
if created_objects:
|
if created_objects:
|
||||||
bpy.context.view_layer.objects.active = created_objects[-1]
|
bpy.context.view_layer.objects.active = created_objects[-1]
|
||||||
|
|
||||||
|
_PARAMETRIC_DIMENSION_TYPES = frozenset(
|
||||||
|
("DIMENSION", "RADIUS", "DIAMETER", "ANGLE", "PLAN_LEVEL", "SECTION_LEVEL")
|
||||||
|
)
|
||||||
|
|
||||||
def hotkey_S_A(self):
|
def hotkey_S_A(self):
|
||||||
if bpy.ops.bim.add_annotation.poll():
|
props = tool.Drawing.get_annotation_props()
|
||||||
|
if props.object_type in self._PARAMETRIC_DIMENSION_TYPES:
|
||||||
|
if bpy.ops.bim.draw_parametric_dimension.poll():
|
||||||
|
bpy.ops.bim.draw_parametric_dimension("INVOKE_DEFAULT")
|
||||||
|
elif bpy.ops.bim.add_annotation.poll():
|
||||||
bpy.ops.bim.add_annotation("INVOKE_DEFAULT")
|
bpy.ops.bim.add_annotation("INVOKE_DEFAULT")
|
||||||
|
|
||||||
def hotkey_S_E(self):
|
def hotkey_S_E(self):
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class EditObjectPlacement(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator):
|
class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
bl_idname = "bim.override_mesh_separate"
|
bl_idname = "bim.override_mesh_separate"
|
||||||
bl_label = "IFC Mesh Separate"
|
bl_label = "IFC Mesh Separate"
|
||||||
blender_op = bpy.ops.mesh.separate.get_rna_type()
|
blender_op = bpy.ops.mesh.separate.get_rna_type() # ty: ignore[missing-argument]
|
||||||
bl_description = blender_op.description + ".\nAlso makes sure changes are in sync with IFC."
|
bl_description = blender_op.description + ".\nAlso makes sure changes are in sync with IFC."
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
blender_type_prop = blender_op.properties["type"]
|
blender_type_prop = blender_op.properties["type"]
|
||||||
@@ -246,7 +246,7 @@ class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
|
|
||||||
class OverrideOriginSet(bpy.types.Operator, tool.Ifc.Operator):
|
class OverrideOriginSet(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
bl_idname = "bim.override_origin_set"
|
bl_idname = "bim.override_origin_set"
|
||||||
blender_op = bpy.ops.object.origin_set.get_rna_type()
|
blender_op = bpy.ops.object.origin_set.get_rna_type() # ty: ignore[missing-argument]
|
||||||
bl_label = "IFC Origin Set"
|
bl_label = "IFC Origin Set"
|
||||||
bl_description = (
|
bl_description = (
|
||||||
blender_op.description + ".\nAlso makes sure changes are in sync with IFC (operator works only on IFC objects)"
|
blender_op.description + ".\nAlso makes sure changes are in sync with IFC (operator works only on IFC objects)"
|
||||||
@@ -801,7 +801,7 @@ def calc_delete_is_batch(ifc_file: ifcopenshell.file, context: bpy.types.Context
|
|||||||
class OverrideDelete(bpy.types.Operator):
|
class OverrideDelete(bpy.types.Operator):
|
||||||
bl_idname = "bim.override_object_delete"
|
bl_idname = "bim.override_object_delete"
|
||||||
bl_label = "IFC Delete"
|
bl_label = "IFC Delete"
|
||||||
blender_op = bpy.ops.object.delete.get_rna_type()
|
blender_op = bpy.ops.object.delete.get_rna_type() # ty: ignore[missing-argument]
|
||||||
bl_description = (
|
bl_description = (
|
||||||
blender_op.description
|
blender_op.description
|
||||||
+ ".\nAlso makes sure changes in sync with IFC."
|
+ ".\nAlso makes sure changes in sync with IFC."
|
||||||
@@ -821,7 +821,7 @@ class OverrideDelete(bpy.types.Operator):
|
|||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
# Match `object.delete` poll for consistency.
|
# Match `object.delete` poll for consistency.
|
||||||
# `object.delete` poll just checks for OBJECT mode.
|
# `object.delete` poll just checks for OBJECT mode.
|
||||||
poll = bpy.ops.object.delete.poll()
|
poll = bpy.ops.object.delete.poll() # ty: ignore[missing-argument]
|
||||||
if poll:
|
if poll:
|
||||||
return True
|
return True
|
||||||
cls.poll_message_set("Only available in OBJECT mode")
|
cls.poll_message_set("Only available in OBJECT mode")
|
||||||
@@ -1045,7 +1045,7 @@ class SelectedIdsData(NamedTuple):
|
|||||||
class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
|
class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
bl_idname = "bim.override_outliner_delete"
|
bl_idname = "bim.override_outliner_delete"
|
||||||
bl_label = "IFC Delete"
|
bl_label = "IFC Delete"
|
||||||
blender_op = bpy.ops.outliner.delete.get_rna_type()
|
blender_op = bpy.ops.outliner.delete.get_rna_type() # ty: ignore[missing-argument]
|
||||||
bl_description = (
|
bl_description = (
|
||||||
blender_op.description
|
blender_op.description
|
||||||
+ ".\nAlso makes sure changes in sync with IFC."
|
+ ".\nAlso makes sure changes in sync with IFC."
|
||||||
@@ -1060,13 +1060,13 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
def poll(cls, context) -> bool:
|
def poll(cls, context) -> bool:
|
||||||
# Match `outliner.delete` poll for consistency.
|
# Match `outliner.delete` poll for consistency.
|
||||||
# `outliner.delete` just checks `area.type` == `OUTLINER`.
|
# `outliner.delete` just checks `area.type` == `OUTLINER`.
|
||||||
poll = bpy.ops.outliner.delete.poll()
|
poll = bpy.ops.outliner.delete.poll() # ty: ignore[missing-argument]
|
||||||
if poll:
|
if poll:
|
||||||
return True
|
return True
|
||||||
cls.poll_message_set("Only available from Outliner.")
|
cls.poll_message_set("Only available from Outliner.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context): # ty:ignore[override-of-final-method]
|
||||||
if len(getattr(context, "selected_ids", [])) == 0:
|
if len(getattr(context, "selected_ids", [])) == 0:
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@@ -1164,7 +1164,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
|||||||
def poll(cls, context) -> bool:
|
def poll(cls, context) -> bool:
|
||||||
# Match `object.duplicate_move` poll for consistency.
|
# Match `object.duplicate_move` poll for consistency.
|
||||||
# `object.duplicate_move` poll checks for OBJECT mode.
|
# `object.duplicate_move` poll checks for OBJECT mode.
|
||||||
poll = bpy.ops.object.duplicate_move.poll()
|
poll = bpy.ops.object.duplicate_move.poll() # ty: ignore[missing-argument]
|
||||||
if poll:
|
if poll:
|
||||||
return True
|
return True
|
||||||
cls.poll_message_set("Only available in OBJECT mode")
|
cls.poll_message_set("Only available in OBJECT mode")
|
||||||
@@ -1908,7 +1908,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
|
class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
bl_idname = "bim.override_object_join"
|
bl_idname = "bim.override_object_join"
|
||||||
bl_label = "IFC Join"
|
bl_label = "IFC Join"
|
||||||
blender_op = bpy.ops.mesh.separate.get_rna_type()
|
blender_op = bpy.ops.mesh.separate.get_rna_type() # ty: ignore[missing-argument]
|
||||||
bl_description = (
|
bl_description = (
|
||||||
blender_op.description
|
blender_op.description
|
||||||
+ ".\nAlso makes sure changes are in sync with IFC."
|
+ ".\nAlso makes sure changes are in sync with IFC."
|
||||||
@@ -1926,7 +1926,7 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
if not bpy.ops.object.join.poll():
|
if not bpy.ops.object.join.poll(): # ty: ignore[missing-argument]
|
||||||
cls.poll_message_set("Active object is not EDITable.")
|
cls.poll_message_set("Active object is not EDITable.")
|
||||||
return False
|
return False
|
||||||
if not context.selected_editable_objects:
|
if not context.selected_editable_objects:
|
||||||
@@ -2289,7 +2289,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
elif obj in pprops.clipping_planes_objs:
|
elif obj in pprops.clipping_planes_objs:
|
||||||
self.report({"ERROR"}, "Clipping planes cannot be edited")
|
self.report({"ERROR"}, "Clipping planes cannot be edited")
|
||||||
elif element:
|
elif element:
|
||||||
if not obj.data:
|
if not obj.data or obj.type not in ("MESH", "CURVE"):
|
||||||
self.report({"INFO"}, "No geometry to edit")
|
self.report({"INFO"}, "No geometry to edit")
|
||||||
elif tool.Geometry.is_locked(element):
|
elif tool.Geometry.is_locked(element):
|
||||||
self.report({"ERROR"}, lock_error_message(obj.name))
|
self.report({"ERROR"}, lock_error_message(obj.name))
|
||||||
|
|||||||
@@ -139,7 +139,9 @@ def update_local_coordinates(self: "BIMGeoreferenceProperties", context: bpy.typ
|
|||||||
tool.Georeference.set_coordinates(
|
tool.Georeference.set_coordinates(
|
||||||
"blender",
|
"blender",
|
||||||
ifcopenshell.util.geolocation.enh2xyz(
|
ifcopenshell.util.geolocation.enh2xyz(
|
||||||
*local_coordinates,
|
local_coordinates[0],
|
||||||
|
local_coordinates[1],
|
||||||
|
local_coordinates[2],
|
||||||
float(props.blender_offset_x),
|
float(props.blender_offset_x),
|
||||||
float(props.blender_offset_y),
|
float(props.blender_offset_y),
|
||||||
float(props.blender_offset_z),
|
float(props.blender_offset_z),
|
||||||
@@ -162,7 +164,9 @@ def update_map_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types
|
|||||||
tool.Georeference.set_coordinates(
|
tool.Georeference.set_coordinates(
|
||||||
"blender",
|
"blender",
|
||||||
ifcopenshell.util.geolocation.enh2xyz(
|
ifcopenshell.util.geolocation.enh2xyz(
|
||||||
*local_coordinates,
|
local_coordinates[0],
|
||||||
|
local_coordinates[1],
|
||||||
|
local_coordinates[2],
|
||||||
float(props.blender_offset_x),
|
float(props.blender_offset_x),
|
||||||
float(props.blender_offset_y),
|
float(props.blender_offset_y),
|
||||||
float(props.blender_offset_z),
|
float(props.blender_offset_z),
|
||||||
@@ -267,6 +271,8 @@ class BIMGeoreferenceProperties(PropertyGroup):
|
|||||||
x_axis_ordinate: str
|
x_axis_ordinate: str
|
||||||
x_axis_is_null: bool
|
x_axis_is_null: bool
|
||||||
|
|
||||||
|
model_is_georeferenced: bool
|
||||||
|
model_crs: str
|
||||||
model_origin: str
|
model_origin: str
|
||||||
model_origin_si: str
|
model_origin_si: str
|
||||||
model_project_north: str
|
model_project_north: str
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from bonsai.bim.prop import StrProperty
|
|||||||
|
|
||||||
class BIMCityJsonProperties(PropertyGroup):
|
class BIMCityJsonProperties(PropertyGroup):
|
||||||
def get_lods(self, context):
|
def get_lods(self, context):
|
||||||
global LODS_ENUM_ITEMS
|
global LODS_ENUM_ITEMS # ty: ignore[unresolved-global]
|
||||||
LODS_ENUM_ITEMS = [(item.name, "LOD" + item.name, "Level of Detail " + item.name) for item in self.lods]
|
LODS_ENUM_ITEMS = [(item.name, "LOD" + item.name, "Level of Detail " + item.name) for item in self.lods]
|
||||||
return LODS_ENUM_ITEMS
|
return LODS_ENUM_ITEMS
|
||||||
|
|
||||||
|
|||||||
@@ -43,11 +43,11 @@ class ToggleGroup(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_label = "Toggle Group"
|
bl_label = "Toggle Group"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
ifc_definition_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
ifc_definition_id: bpy.props.IntProperty()
|
||||||
group_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
group_type: bpy.props.EnumProperty(
|
||||||
items=[(i, i, "") for i in get_args(tool.Group.GroupType)],
|
items=[(i, i, "") for i in get_args(tool.Group.GroupType)],
|
||||||
)
|
)
|
||||||
option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
option: bpy.props.EnumProperty(
|
||||||
items=[(i, i, "") for i in get_args(tool.Group.ToggleOption)],
|
items=[(i, i, "") for i in get_args(tool.Group.ToggleOption)],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -34,8 +34,10 @@ classes = (
|
|||||||
operator.Fetch,
|
operator.Fetch,
|
||||||
operator.Merge,
|
operator.Merge,
|
||||||
operator.ObjectLog,
|
operator.ObjectLog,
|
||||||
|
operator.SelectConflictEntity,
|
||||||
operator.Push,
|
operator.Push,
|
||||||
operator.RefreshGit,
|
operator.RefreshGit,
|
||||||
|
operator.RenameBranch,
|
||||||
operator.SwitchRevision,
|
operator.SwitchRevision,
|
||||||
operator.InstallGit,
|
operator.InstallGit,
|
||||||
operator.RunGitDiff,
|
operator.RunGitDiff,
|
||||||
|
|||||||
@@ -21,65 +21,71 @@ class IfcGitData:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls):
|
def load(cls):
|
||||||
|
repo = None
|
||||||
|
if bool(tool.Ifc.get()):
|
||||||
|
path_ifc = tool.Ifc.get_path()
|
||||||
|
if os.path.isfile(path_ifc):
|
||||||
|
repo = tool.IfcGit.repo_from_path(path_ifc)
|
||||||
|
|
||||||
cls.data = {
|
cls.data = {
|
||||||
"repo": cls.repo(),
|
"repo": repo,
|
||||||
"remotes": cls.remotes(),
|
"remotes": repo.remotes if repo else None,
|
||||||
"branch_names": cls.branch_names(),
|
"branch_names": cls.branch_names(repo),
|
||||||
"remote_names": cls.remote_names(),
|
"tag_names": cls.tag_names(repo),
|
||||||
"remote_urls": cls.remote_urls(),
|
"remote_names": cls.remote_names(repo),
|
||||||
|
"remote_urls": {r.name: r.url for r in repo.remotes} if repo else {},
|
||||||
"path_ifc": cls.path_ifc(),
|
"path_ifc": cls.path_ifc(),
|
||||||
"branches_by_hexsha": cls.branches_by_hexsha(),
|
"branches_by_hexsha": cls.branches_by_hexsha(),
|
||||||
"tags_by_hexsha": cls.tags_by_hexsha(),
|
"tags_by_hexsha": cls.tags_by_hexsha(),
|
||||||
"name_ifc": cls.name_ifc(),
|
"name_ifc": cls.name_ifc(repo),
|
||||||
"dir_name": cls.dir_name(),
|
"dir_name": cls.dir_name(),
|
||||||
"base_name": cls.base_name(),
|
"base_name": cls.base_name(),
|
||||||
"working_dir": cls.working_dir(),
|
"working_dir": repo.working_dir if repo else None,
|
||||||
"untracked_files": cls.untracked_files(),
|
"ifc_is_untracked": cls.ifc_is_untracked(repo),
|
||||||
"is_detached": cls.is_detached(),
|
"is_detached": repo.head.is_detached if repo else None,
|
||||||
"active_branch_name": cls.active_branch_name(),
|
"active_branch_name": repo.active_branch.name if repo and not repo.head.is_detached else None,
|
||||||
"is_dirty": cls.is_dirty(),
|
"is_dirty": cls.is_dirty(repo),
|
||||||
"commit": cls.commit(),
|
"current_revision": cls.current_revision(repo),
|
||||||
"current_revision": cls.current_revision(),
|
|
||||||
"git_exe": cls.git_exe(),
|
"git_exe": cls.git_exe(),
|
||||||
"ifcmerge_exe": cls.ifcmerge_exe(),
|
"ifcmerge_exe": cls.ifcmerge_exe(),
|
||||||
}
|
}
|
||||||
cls.is_loaded = True
|
cls.is_loaded = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def repo(cls):
|
def branch_names(cls, repo):
|
||||||
if bool(tool.Ifc.get()):
|
if not repo or not repo.heads:
|
||||||
path_ifc = tool.Ifc.get_path()
|
|
||||||
if os.path.isfile(path_ifc):
|
|
||||||
return tool.IfcGit.repo_from_path(path_ifc)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def remotes(cls):
|
|
||||||
if cls.repo():
|
|
||||||
return cls.repo().remotes
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def branch_names(cls):
|
|
||||||
return []
|
return []
|
||||||
|
names = sorted([b.name for b in repo.branches])
|
||||||
|
if "main" in names:
|
||||||
|
names.remove("main")
|
||||||
|
names = ["main"] + names
|
||||||
|
if repo.remotes:
|
||||||
|
for remote in repo.remotes:
|
||||||
|
for ref in remote.refs:
|
||||||
|
names.append(ref.name)
|
||||||
|
return names
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def remote_names(cls):
|
def tag_names(cls, repo):
|
||||||
|
if not repo:
|
||||||
return []
|
return []
|
||||||
|
return [t.name for t in repo.tags]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def remote_urls(cls):
|
def remote_names(cls, repo):
|
||||||
result = {}
|
if not repo:
|
||||||
if cls.repo():
|
return []
|
||||||
for remote in cls.repo().remotes:
|
names = sorted([r.name for r in repo.remotes])
|
||||||
result[remote.name] = remote.url
|
if "origin" in names:
|
||||||
return result
|
names.remove("origin")
|
||||||
|
names = ["origin"] + names
|
||||||
|
return names
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def path_ifc(cls):
|
def path_ifc(cls):
|
||||||
path_ifc = tool.Ifc.get_path()
|
path_ifc = tool.Ifc.get_path()
|
||||||
if os.path.isfile(path_ifc):
|
if os.path.isfile(path_ifc):
|
||||||
return tool.Ifc.get_path()
|
return path_ifc
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -88,6 +94,7 @@ class IfcGitData:
|
|||||||
if tool.IfcGitRepo.repo.branches:
|
if tool.IfcGitRepo.repo.branches:
|
||||||
return tool.IfcGit.branches_by_hexsha(tool.IfcGitRepo.repo)
|
return tool.IfcGit.branches_by_hexsha(tool.IfcGitRepo.repo)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
pass
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -97,12 +104,11 @@ class IfcGitData:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def name_ifc(cls):
|
def name_ifc(cls, repo):
|
||||||
if bool(tool.Ifc.get()):
|
if bool(tool.Ifc.get()) and repo:
|
||||||
path_ifc = tool.Ifc.get_path()
|
path_ifc = tool.Ifc.get_path()
|
||||||
if tool.IfcGitRepo.repo and os.path.isfile(path_ifc):
|
if os.path.isfile(path_ifc):
|
||||||
working_dir = tool.IfcGitRepo.repo.working_dir
|
return os.path.relpath(path_ifc, repo.working_dir)
|
||||||
return os.path.relpath(path_ifc, working_dir)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -122,49 +128,28 @@ class IfcGitData:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def working_dir(cls):
|
def ifc_is_untracked(cls, repo):
|
||||||
if cls.repo():
|
"""Return True if the IFC file exists in the repo but has not been added to git."""
|
||||||
return cls.repo().working_dir
|
if not repo:
|
||||||
|
return False
|
||||||
|
path_ifc = tool.Ifc.get_path()
|
||||||
|
if not os.path.isfile(path_ifc):
|
||||||
|
return False
|
||||||
|
return not bool(repo.git.ls_files(path_ifc))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def untracked_files(cls):
|
def is_dirty(cls, repo):
|
||||||
if cls.repo():
|
if repo and cls.git_exe():
|
||||||
return cls.repo().untracked_files
|
|
||||||
return []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def is_detached(cls):
|
|
||||||
if cls.repo():
|
|
||||||
return cls.repo().head.is_detached
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def active_branch_name(cls):
|
|
||||||
if cls.repo() and not cls.is_detached():
|
|
||||||
return cls.repo().active_branch.name
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def is_dirty(cls):
|
|
||||||
if cls.repo() and cls.git_exe():
|
|
||||||
path_ifc = tool.Ifc.get_path()
|
path_ifc = tool.Ifc.get_path()
|
||||||
if os.path.isfile(path_ifc):
|
if os.path.isfile(path_ifc):
|
||||||
return cls.repo().is_dirty(path=path_ifc)
|
return repo.is_dirty(path=path_ifc)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def commit(cls):
|
def current_revision(cls, repo):
|
||||||
props = tool.IfcGit.get_ifcgit_props()
|
props = tool.IfcGit.get_ifcgit_props()
|
||||||
if cls.repo() and len(props.ifcgit_commits) > 0:
|
if repo and repo.head.is_valid() and len(props.ifcgit_commits) > 0:
|
||||||
item = props.ifcgit_commits[props.commit_index]
|
return repo.commit()
|
||||||
try:
|
|
||||||
return cls.repo().commit(rev=item.hexsha)
|
|
||||||
except ValueError:
|
|
||||||
return
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def current_revision(cls):
|
|
||||||
props = tool.IfcGit.get_ifcgit_props()
|
|
||||||
if cls.repo() and cls.repo().head.is_valid() and len(props.ifcgit_commits) > 0:
|
|
||||||
return tool.IfcGitRepo.repo.commit()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def git_exe(cls):
|
def git_exe(cls):
|
||||||
|
|||||||
@@ -120,11 +120,11 @@ class CommitChanges(bpy.types.Operator):
|
|||||||
if props.commit_message == "":
|
if props.commit_message == "":
|
||||||
return False
|
return False
|
||||||
if repo:
|
if repo:
|
||||||
if props.new_branch_name in [branch.name for branch in repo.branches]:
|
if props.new_branch_name in IfcGitData.data["branch_names"]:
|
||||||
cls.poll_message_set("Branch already exists!")
|
cls.poll_message_set("Branch already exists!")
|
||||||
return False
|
return False
|
||||||
elif not tool.IfcGit.is_valid_ref_format(props.new_branch_name):
|
elif not tool.IfcGit.is_valid_ref_format(props.new_branch_name):
|
||||||
if repo.head.is_detached:
|
if IfcGitData.data["is_detached"]:
|
||||||
cls.poll_message_set("Branch name is invalid or empty!")
|
cls.poll_message_set("Branch name is invalid or empty!")
|
||||||
return False
|
return False
|
||||||
elif props.new_branch_name != "":
|
elif props.new_branch_name != "":
|
||||||
@@ -134,10 +134,17 @@ class CommitChanges(bpy.types.Operator):
|
|||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
|
|
||||||
repo = IfcGitData.data["repo"]
|
props = tool.IfcGit.get_ifcgit_props()
|
||||||
core.commit_changes(tool.IfcGit, tool.Ifc, repo)
|
commit_message = props.commit_message
|
||||||
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
|
new_branch_name = props.new_branch_name
|
||||||
|
core.commit_changes(tool.IfcGit, tool.Ifc, commit_message, new_branch_name)
|
||||||
|
props.new_branch_name = ""
|
||||||
|
props.commit_message = ""
|
||||||
|
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
|
||||||
refresh()
|
refresh()
|
||||||
|
IfcGitData.load()
|
||||||
|
if new_branch_name:
|
||||||
|
props.display_branch = new_branch_name
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -157,7 +164,7 @@ class AddTag(bpy.types.Operator):
|
|||||||
repo = IfcGitData.data["repo"]
|
repo = IfcGitData.data["repo"]
|
||||||
if repo and (
|
if repo and (
|
||||||
not tool.IfcGit.is_valid_ref_format(props.new_tag_name)
|
not tool.IfcGit.is_valid_ref_format(props.new_tag_name)
|
||||||
or props.new_tag_name in [tag.name for tag in repo.tags]
|
or props.new_tag_name in IfcGitData.data["tag_names"]
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
@@ -165,8 +172,12 @@ class AddTag(bpy.types.Operator):
|
|||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
|
|
||||||
repo = IfcGitData.data["repo"]
|
repo = IfcGitData.data["repo"]
|
||||||
core.add_tag(tool.IfcGit, repo)
|
props = tool.IfcGit.get_ifcgit_props()
|
||||||
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
|
item = props.ifcgit_commits[props.commit_index]
|
||||||
|
core.add_tag(tool.IfcGit, repo, item.hexsha, props.new_tag_name, props.new_tag_message)
|
||||||
|
props.new_tag_name = ""
|
||||||
|
props.new_tag_message = ""
|
||||||
|
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
|
||||||
refresh()
|
refresh()
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@@ -183,7 +194,7 @@ class DeleteTag(bpy.types.Operator):
|
|||||||
|
|
||||||
repo = IfcGitData.data["repo"]
|
repo = IfcGitData.data["repo"]
|
||||||
core.delete_tag(tool.IfcGit, repo, self.tag_name)
|
core.delete_tag(tool.IfcGit, repo, self.tag_name)
|
||||||
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
|
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
|
||||||
refresh()
|
refresh()
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@@ -191,7 +202,7 @@ class DeleteTag(bpy.types.Operator):
|
|||||||
class RefreshGit(bpy.types.Operator):
|
class RefreshGit(bpy.types.Operator):
|
||||||
"""Refresh revision list"""
|
"""Refresh revision list"""
|
||||||
|
|
||||||
bl_label = ""
|
bl_label = "Refresh"
|
||||||
bl_idname = "ifcgit.refresh"
|
bl_idname = "ifcgit.refresh"
|
||||||
bl_options = {"REGISTER"}
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
@@ -205,8 +216,7 @@ class RefreshGit(bpy.types.Operator):
|
|||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
|
|
||||||
repo = IfcGitData.data["repo"]
|
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
|
||||||
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
|
|
||||||
refresh()
|
refresh()
|
||||||
tool.IfcGit.decolourise()
|
tool.IfcGit.decolourise()
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
@@ -215,7 +225,7 @@ class RefreshGit(bpy.types.Operator):
|
|||||||
class DisplayRevision(bpy.types.Operator):
|
class DisplayRevision(bpy.types.Operator):
|
||||||
"""Colourise objects by selected revision"""
|
"""Colourise objects by selected revision"""
|
||||||
|
|
||||||
bl_label = ""
|
bl_label = "Colourise Revision"
|
||||||
bl_idname = "ifcgit.display_revision"
|
bl_idname = "ifcgit.display_revision"
|
||||||
bl_options = {"REGISTER"}
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
@@ -250,7 +260,7 @@ class DisplayUncommitted(bpy.types.Operator):
|
|||||||
class SwitchRevision(bpy.types.Operator):
|
class SwitchRevision(bpy.types.Operator):
|
||||||
"""Switches the repository to the selected revision and reloads the IFC file"""
|
"""Switches the repository to the selected revision and reloads the IFC file"""
|
||||||
|
|
||||||
bl_label = ""
|
bl_label = "Switch Revision"
|
||||||
bl_idname = "ifcgit.switch_revision"
|
bl_idname = "ifcgit.switch_revision"
|
||||||
bl_options = {"REGISTER"}
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
@@ -268,7 +278,7 @@ class SwitchRevision(bpy.types.Operator):
|
|||||||
|
|
||||||
|
|
||||||
class Merge(bpy.types.Operator):
|
class Merge(bpy.types.Operator):
|
||||||
"""Merges the selected branch into working branch"""
|
"""Merges the selected branch into working branch.\nCtrl+click to preview without merging"""
|
||||||
|
|
||||||
bl_label = "Merge this branch"
|
bl_label = "Merge this branch"
|
||||||
bl_idname = "ifcgit.merge"
|
bl_idname = "ifcgit.merge"
|
||||||
@@ -282,15 +292,84 @@ class Merge(bpy.types.Operator):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def execute(self, context):
|
def invoke(self, context, event):
|
||||||
|
if event.ctrl:
|
||||||
|
core.dry_run_merge(tool.IfcGit, tool.Ifc, self)
|
||||||
|
refresh()
|
||||||
|
return {"FINISHED"}
|
||||||
|
return self.execute(context)
|
||||||
|
|
||||||
if core.merge_branch(tool.IfcGit, tool.Ifc, self):
|
def execute(self, context):
|
||||||
|
if core.merge_branch(tool.IfcGit, tool.Ifc, self) is not False:
|
||||||
refresh()
|
refresh()
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
else:
|
else:
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
|
||||||
|
class SelectConflictEntity(bpy.types.Operator):
|
||||||
|
"""Select the conflicting entity in the viewport"""
|
||||||
|
|
||||||
|
bl_label = "Select Conflict Entity"
|
||||||
|
bl_idname = "ifcgit.select_conflict_entity"
|
||||||
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
|
step_id: bpy.props.IntProperty()
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
step_id: int
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
model = tool.Ifc.get()
|
||||||
|
if not model:
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
entity = model.by_id(self.step_id)
|
||||||
|
except Exception:
|
||||||
|
self.report({"WARNING"}, f"Entity #{self.step_id} not found (may have been deleted locally)")
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
obj = tool.Ifc.get_object(entity)
|
||||||
|
if obj is None:
|
||||||
|
# Walk inverse references up to 5 hops to find nearest entity with a Blender object
|
||||||
|
visited = {entity.id()}
|
||||||
|
queue = [entity]
|
||||||
|
for _ in range(5):
|
||||||
|
next_queue = []
|
||||||
|
for ent in queue:
|
||||||
|
for inv in model.get_inverse(ent):
|
||||||
|
if inv.id() in visited:
|
||||||
|
continue
|
||||||
|
visited.add(inv.id())
|
||||||
|
obj = tool.Ifc.get_object(inv)
|
||||||
|
if obj is not None:
|
||||||
|
break
|
||||||
|
next_queue.append(inv)
|
||||||
|
if obj is not None:
|
||||||
|
break
|
||||||
|
if obj is not None:
|
||||||
|
break
|
||||||
|
queue = next_queue
|
||||||
|
|
||||||
|
if obj is None:
|
||||||
|
self.report({"INFO"}, f"No viewport representation found for #{self.step_id} ({entity.is_a()})")
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
bpy.ops.object.select_all(action="DESELECT")
|
||||||
|
obj.select_set(True)
|
||||||
|
context.view_layer.objects.active = obj
|
||||||
|
for area in context.screen.areas:
|
||||||
|
if area.type == "VIEW_3D":
|
||||||
|
region = next((r for r in area.regions if r.type == "WINDOW"), None)
|
||||||
|
if region:
|
||||||
|
with context.temp_override(area=area, region=region):
|
||||||
|
bpy.ops.view3d.view_selected()
|
||||||
|
break
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class Push(bpy.types.Operator):
|
class Push(bpy.types.Operator):
|
||||||
"""Pushes the working branch to selected remote"""
|
"""Pushes the working branch to selected remote"""
|
||||||
|
|
||||||
@@ -314,9 +393,9 @@ class Fetch(bpy.types.Operator):
|
|||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
props = tool.IfcGit.get_ifcgit_props()
|
props = tool.IfcGit.get_ifcgit_props()
|
||||||
repo = IfcGitData.data["repo"]
|
core.fetch(tool.IfcGit, props.select_remote)
|
||||||
remote = repo.remotes[props.select_remote]
|
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
|
||||||
remote.fetch()
|
refresh()
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -336,7 +415,7 @@ class AddRemote(bpy.types.Operator):
|
|||||||
not repo
|
not repo
|
||||||
or not tool.IfcGit.is_valid_ref_format(props.remote_name)
|
or not tool.IfcGit.is_valid_ref_format(props.remote_name)
|
||||||
or not props.remote_url
|
or not props.remote_url
|
||||||
or props.remote_name in [remote.name for remote in repo.remotes]
|
or props.remote_name in IfcGitData.data["remote_names"]
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
@@ -344,8 +423,11 @@ class AddRemote(bpy.types.Operator):
|
|||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
|
|
||||||
repo = IfcGitData.data["repo"]
|
repo = IfcGitData.data["repo"]
|
||||||
core.add_remote(tool.IfcGit, repo)
|
props = tool.IfcGit.get_ifcgit_props()
|
||||||
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
|
core.add_remote(tool.IfcGit, repo, props.remote_name, props.remote_url)
|
||||||
|
props.remote_name = ""
|
||||||
|
props.remote_url = ""
|
||||||
|
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
|
||||||
refresh()
|
refresh()
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@@ -360,8 +442,19 @@ class DeleteRemote(bpy.types.Operator):
|
|||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
|
|
||||||
repo = IfcGitData.data["repo"]
|
repo = IfcGitData.data["repo"]
|
||||||
core.delete_remote(tool.IfcGit, repo)
|
props = tool.IfcGit.get_ifcgit_props()
|
||||||
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
|
remote_name = props.select_remote
|
||||||
|
if props.display_branch.startswith(remote_name + "/"):
|
||||||
|
active = IfcGitData.data["active_branch_name"]
|
||||||
|
if active:
|
||||||
|
props.display_branch = active
|
||||||
|
else:
|
||||||
|
local_branches = [b for b in IfcGitData.data["branch_names"] if "/" not in b]
|
||||||
|
if local_branches:
|
||||||
|
props.display_branch = local_branches[0]
|
||||||
|
core.delete_remote(tool.IfcGit, repo, remote_name)
|
||||||
|
tool.IfcGit.select_first_remote()
|
||||||
|
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
|
||||||
refresh()
|
refresh()
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@@ -375,8 +468,8 @@ class ObjectLog(bpy.types.Operator):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
if not (obj := context.active_object):
|
if not (obj := context.active_object) or not obj.select_get():
|
||||||
cls.poll_message_set("No Active Object")
|
cls.poll_message_set("No selected object")
|
||||||
elif not tool.Blender.get_ifc_definition_id(obj):
|
elif not tool.Blender.get_ifc_definition_id(obj):
|
||||||
cls.poll_message_set("Active Object doesn't have an IFC definition")
|
cls.poll_message_set("Active Object doesn't have an IFC definition")
|
||||||
else:
|
else:
|
||||||
@@ -422,7 +515,7 @@ class RunGitDiff(bpy.types.Operator):
|
|||||||
)
|
)
|
||||||
bl_options = set()
|
bl_options = set()
|
||||||
|
|
||||||
save_to_temp: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
|
save_to_temp: bpy.props.BoolProperty(options={"SKIP_SAVE"})
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
save_to_temp: bool
|
save_to_temp: bool
|
||||||
@@ -445,3 +538,37 @@ class RunGitDiff(bpy.types.Operator):
|
|||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
core.run_git_diff(tool.IfcGit, self, self.save_to_temp)
|
core.run_git_diff(tool.IfcGit, self, self.save_to_temp)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class RenameBranch(bpy.types.Operator):
|
||||||
|
"""Rename the current branch"""
|
||||||
|
|
||||||
|
bl_label = "Rename Branch"
|
||||||
|
bl_idname = "ifcgit.rename_branch"
|
||||||
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
|
new_name: bpy.props.StringProperty(name="New name")
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
new_name: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
IfcGitData.make_sure_is_loaded()
|
||||||
|
if not IfcGitData.data["repo"]:
|
||||||
|
return False
|
||||||
|
if IfcGitData.data["is_detached"]:
|
||||||
|
return False
|
||||||
|
if IfcGitData.data["is_dirty"]:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
self.new_name = IfcGitData.data["active_branch_name"]
|
||||||
|
return context.window_manager.invoke_props_dialog(self)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
repo = IfcGitData.data["repo"]
|
||||||
|
core.rename_branch(tool.IfcGit, repo, self.new_name)
|
||||||
|
refresh()
|
||||||
|
return {"FINISHED"}
|
||||||
|
|||||||
@@ -17,28 +17,14 @@ from bonsai.bim.module.ifcgit.data import IfcGitData
|
|||||||
def git_branches(self: "IfcGitProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
def git_branches(self: "IfcGitProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||||
# NOTE "Python must keep a reference to the strings returned by
|
# NOTE "Python must keep a reference to the strings returned by
|
||||||
# the callback or Blender will misbehave or even crash"
|
# the callback or Blender will misbehave or even crash"
|
||||||
IfcGitData.data["branch_names"] = sorted([branch.name for branch in IfcGitData.data["repo"].heads])
|
# Branch list (local + remote, main first) is computed once in IfcGitData.load()
|
||||||
|
IfcGitData.make_sure_is_loaded()
|
||||||
if "main" in IfcGitData.data["branch_names"]:
|
return [(name, name, name) for name in IfcGitData.data["branch_names"]]
|
||||||
IfcGitData.data["branch_names"].remove("main")
|
|
||||||
IfcGitData.data["branch_names"] = ["main"] + IfcGitData.data["branch_names"]
|
|
||||||
|
|
||||||
if IfcGitData.data["remotes"]:
|
|
||||||
for remote in IfcGitData.data["remotes"]:
|
|
||||||
for remote_branch in remote.refs:
|
|
||||||
IfcGitData.data["branch_names"].append(remote_branch.name)
|
|
||||||
|
|
||||||
return [(myname, myname, myname) for myname in IfcGitData.data["branch_names"]]
|
|
||||||
|
|
||||||
|
|
||||||
def git_remotes(self: "IfcGitProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
def git_remotes(self: "IfcGitProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||||
IfcGitData.data["remote_names"] = sorted([remote.name for remote in IfcGitData.data["remotes"]])
|
IfcGitData.make_sure_is_loaded()
|
||||||
|
return [(name, name, name) for name in IfcGitData.data["remote_names"]]
|
||||||
if "origin" in IfcGitData.data["remote_names"]:
|
|
||||||
IfcGitData.data["remote_names"].remove("origin")
|
|
||||||
IfcGitData.data["remote_names"] = ["origin"] + IfcGitData.data["remote_names"]
|
|
||||||
|
|
||||||
return [(myname, myname, myname) for myname in IfcGitData.data["remote_names"]]
|
|
||||||
|
|
||||||
|
|
||||||
def update_revlist(self: "IfcGitProperties", context: bpy.types.Context) -> None:
|
def update_revlist(self: "IfcGitProperties", context: bpy.types.Context) -> None:
|
||||||
@@ -90,6 +76,7 @@ class IfcGitListItem(PropertyGroup):
|
|||||||
name="Commit Message",
|
name="Commit Message",
|
||||||
default="",
|
default="",
|
||||||
)
|
)
|
||||||
|
committed_date: IntProperty(name="Committed Date", default=0)
|
||||||
tags: CollectionProperty(type=IfcGitTag, name="List of revision tags")
|
tags: CollectionProperty(type=IfcGitTag, name="List of revision tags")
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -98,6 +85,7 @@ class IfcGitListItem(PropertyGroup):
|
|||||||
author_name: str
|
author_name: str
|
||||||
author_email: str
|
author_email: str
|
||||||
message: str
|
message: str
|
||||||
|
committed_date: int
|
||||||
tags: bpy.types.bpy_prop_collection_idprop[IfcGitTag]
|
tags: bpy.types.bpy_prop_collection_idprop[IfcGitTag]
|
||||||
|
|
||||||
|
|
||||||
@@ -151,6 +139,11 @@ class IfcGitProperties(PropertyGroup):
|
|||||||
],
|
],
|
||||||
update=update_revlist,
|
update=update_revlist,
|
||||||
)
|
)
|
||||||
|
merge_conflicts: StringProperty(
|
||||||
|
name="Merge Conflicts",
|
||||||
|
description="JSON report from last failed merge attempt",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
ifcgit_commits: bpy.types.bpy_prop_collection_idprop[IfcGitListItem]
|
ifcgit_commits: bpy.types.bpy_prop_collection_idprop[IfcGitListItem]
|
||||||
@@ -165,3 +158,4 @@ class IfcGitProperties(PropertyGroup):
|
|||||||
display_branch: str
|
display_branch: str
|
||||||
select_remote: str
|
select_remote: str
|
||||||
ifcgit_filter: Literal["all", "tagged", "relevant"]
|
ifcgit_filter: Literal["all", "tagged", "relevant"]
|
||||||
|
merge_conflicts: str
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class IFCGIT_PT_panel(bpy.types.Panel):
|
|||||||
if IfcGitData.data["repo"] and os.path.exists(IfcGitData.data["repo"].git_dir):
|
if IfcGitData.data["repo"] and os.path.exists(IfcGitData.data["repo"].git_dir):
|
||||||
name_ifc = IfcGitData.data["name_ifc"]
|
name_ifc = IfcGitData.data["name_ifc"]
|
||||||
row.label(text=IfcGitData.data["working_dir"], icon="SYSTEM")
|
row.label(text=IfcGitData.data["working_dir"], icon="SYSTEM")
|
||||||
if name_ifc in IfcGitData.data["untracked_files"]:
|
if IfcGitData.data["ifc_is_untracked"]:
|
||||||
row.operator(
|
row.operator(
|
||||||
"ifcgit.addfile",
|
"ifcgit.addfile",
|
||||||
text="Add '" + name_ifc + "' to repository",
|
text="Add '" + name_ifc + "' to repository",
|
||||||
@@ -112,15 +112,13 @@ class IFCGIT_PT_panel(bpy.types.Panel):
|
|||||||
row.label(text="Working branch: Detached HEAD")
|
row.label(text="Working branch: Detached HEAD")
|
||||||
else:
|
else:
|
||||||
row.label(text="Working branch: " + IfcGitData.data["active_branch_name"])
|
row.label(text="Working branch: " + IfcGitData.data["active_branch_name"])
|
||||||
|
row.operator("ifcgit.rename_branch", icon="GREASEPENCIL", text="")
|
||||||
|
|
||||||
grouped = layout.row()
|
row = layout.row()
|
||||||
column = grouped.column()
|
|
||||||
row = column.row()
|
|
||||||
row.prop(props, "display_branch", text="Browse branch")
|
row.prop(props, "display_branch", text="Browse branch")
|
||||||
row.prop(props, "ifcgit_filter", text="Filter revisions")
|
row.prop(props, "ifcgit_filter", text="Filter revisions")
|
||||||
|
|
||||||
row = column.row()
|
layout.template_list(
|
||||||
row.template_list(
|
|
||||||
"COMMIT_UL_List",
|
"COMMIT_UL_List",
|
||||||
"The_List",
|
"The_List",
|
||||||
props,
|
props,
|
||||||
@@ -128,20 +126,64 @@ class IFCGIT_PT_panel(bpy.types.Panel):
|
|||||||
props,
|
props,
|
||||||
"commit_index",
|
"commit_index",
|
||||||
)
|
)
|
||||||
column = grouped.column()
|
|
||||||
row = column.row()
|
row = layout.row(align=True)
|
||||||
row.operator("ifcgit.refresh", icon="FILE_REFRESH")
|
row.operator("ifcgit.refresh", icon="FILE_REFRESH")
|
||||||
|
|
||||||
if not is_dirty:
|
if not is_dirty:
|
||||||
|
|
||||||
row = column.row()
|
|
||||||
row.operator("ifcgit.display_revision", icon="SELECT_DIFFERENCE")
|
row.operator("ifcgit.display_revision", icon="SELECT_DIFFERENCE")
|
||||||
|
|
||||||
row = column.row()
|
|
||||||
row.operator("ifcgit.switch_revision", icon="CURRENT_FILE")
|
row.operator("ifcgit.switch_revision", icon="CURRENT_FILE")
|
||||||
|
row.operator("ifcgit.merge", icon="SYSTEM")
|
||||||
|
|
||||||
row = column.row()
|
conflicts = tool.IfcGit.get_merge_conflicts()
|
||||||
row.operator("ifcgit.merge", icon="EXPERIMENTAL", text="")
|
if conflicts is not None:
|
||||||
|
box = layout.box()
|
||||||
|
box.alert = True
|
||||||
|
row = box.row()
|
||||||
|
row.label(
|
||||||
|
text=f"Merge failed \u2014 {len(conflicts)} conflict(s)",
|
||||||
|
icon="ERROR",
|
||||||
|
)
|
||||||
|
for conflict in conflicts:
|
||||||
|
col = box.column(align=True)
|
||||||
|
conflict_type = conflict.get("type", "")
|
||||||
|
entity_id = conflict.get("entity_id", "?")
|
||||||
|
local_id = conflict.get("original_local_id")
|
||||||
|
|
||||||
|
if conflict_type == "attribute_conflict":
|
||||||
|
entity_class = conflict.get("entity_class", "Entity")
|
||||||
|
attr_idx = conflict.get("attribute_index", "?")
|
||||||
|
desc = f"#{entity_id} {entity_class}: attribute {attr_idx} conflict"
|
||||||
|
elif conflict_type == "entity_deleted_and_modified":
|
||||||
|
entity_class = conflict.get("entity_class", "Entity")
|
||||||
|
desc = f"#{entity_id} {entity_class}: " + conflict.get("message", "deleted/modified conflict")
|
||||||
|
elif conflict_type == "class_changed":
|
||||||
|
desc = (
|
||||||
|
f"#{entity_id}: class changed "
|
||||||
|
+ conflict.get("base_class", "?")
|
||||||
|
+ " \u2192 "
|
||||||
|
+ conflict.get("modified_class", "?")
|
||||||
|
)
|
||||||
|
elif conflict_type == "required_entity_deleted":
|
||||||
|
desc = f"#{entity_id}: " + conflict.get("message", "required entity deleted")
|
||||||
|
else:
|
||||||
|
desc = f"#{entity_id}: {conflict_type}"
|
||||||
|
|
||||||
|
row = col.row(align=True)
|
||||||
|
row.label(text=desc)
|
||||||
|
if local_id:
|
||||||
|
op = row.operator(
|
||||||
|
"ifcgit.select_conflict_entity",
|
||||||
|
text="",
|
||||||
|
icon="RESTRICT_SELECT_OFF",
|
||||||
|
)
|
||||||
|
op.step_id = local_id
|
||||||
|
|
||||||
|
if conflict_type == "attribute_conflict":
|
||||||
|
sub = col.column(align=True)
|
||||||
|
sub.scale_y = 0.75
|
||||||
|
sub.label(text=f" Base: {conflict.get('base_value', '')}")
|
||||||
|
sub.label(text=f" Local: {conflict.get('local_value', '')}")
|
||||||
|
sub.label(text=f" Remote: {conflict.get('remote_value', '')}")
|
||||||
|
|
||||||
if not props.ifcgit_commits:
|
if not props.ifcgit_commits:
|
||||||
return
|
return
|
||||||
@@ -216,13 +258,7 @@ class COMMIT_UL_List(bpy.types.UIList):
|
|||||||
):
|
):
|
||||||
|
|
||||||
current_revision = IfcGitData.data["current_revision"]
|
current_revision = IfcGitData.data["current_revision"]
|
||||||
|
current_hexsha = current_revision.hexsha if current_revision else None
|
||||||
# TODO Figure how this "item" can be acesse in "data.py"
|
|
||||||
# so it's possible to move the ".commit"
|
|
||||||
try:
|
|
||||||
commit = IfcGitData.data["repo"].commit(rev=item.hexsha)
|
|
||||||
except ValueError:
|
|
||||||
return
|
|
||||||
|
|
||||||
lookup = IfcGitData.data["branches_by_hexsha"]
|
lookup = IfcGitData.data["branches_by_hexsha"]
|
||||||
refs = ""
|
refs = ""
|
||||||
@@ -236,11 +272,11 @@ class COMMIT_UL_List(bpy.types.UIList):
|
|||||||
for tag in lookup[item.hexsha]:
|
for tag in lookup[item.hexsha]:
|
||||||
refs += "{" + tag.name + "} "
|
refs += "{" + tag.name + "} "
|
||||||
|
|
||||||
if commit == current_revision:
|
if item.hexsha == current_hexsha:
|
||||||
layout.label(text="[HEAD] " + refs + commit.message.split("\n")[0], icon="DECORATE_KEYFRAME")
|
layout.label(text="[HEAD] " + refs + item.message.split("\n")[0], icon="DECORATE_KEYFRAME")
|
||||||
else:
|
else:
|
||||||
layout.label(text=refs + commit.message.split("\n")[0], icon="DECORATE_ANIMATE")
|
layout.label(text=refs + item.message.split("\n")[0], icon="DECORATE_ANIMATE")
|
||||||
layout.label(text=time.strftime("%c", time.localtime(commit.committed_date)))
|
layout.label(text=time.strftime("%c", time.localtime(item.committed_date)))
|
||||||
|
|
||||||
def draw_filter(self, context, layout):
|
def draw_filter(self, context, layout):
|
||||||
|
|
||||||
|
|||||||
@@ -566,7 +566,7 @@ class LightPickCoordinates(bpy.types.Operator):
|
|||||||
)
|
)
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
use_current_location: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
|
use_current_location: bpy.props.BoolProperty(options={"SKIP_SAVE"})
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
use_current_location: bool
|
use_current_location: bool
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ class RadianceExporterProperties(PropertyGroup):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_subcategories(self, context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
def get_subcategories(self, context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||||
global SUBCATEGORIES_ENUM_ITEMS
|
global SUBCATEGORIES_ENUM_ITEMS # ty: ignore[unresolved-global]
|
||||||
if self.category in spectraldb:
|
if self.category in spectraldb:
|
||||||
SUBCATEGORIES_ENUM_ITEMS = [(k, k, "") for k in spectraldb[self.category].keys()]
|
SUBCATEGORIES_ENUM_ITEMS = [(k, k, "") for k in spectraldb[self.category].keys()]
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -630,7 +630,7 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
slab.DumbSlabPlaner().regenerate_from_layer_set(layer_set)
|
slab.DumbSlabPlaner().regenerate_from_layer_set(layer_set)
|
||||||
|
|
||||||
if material_set_usage.is_a("IfcMaterialProfileSetUsage"):
|
if material_set_usage.is_a("IfcMaterialProfileSetUsage"):
|
||||||
if "CardinalPoint" in attributes:
|
if "CardinalPoint" in attributes and attributes["CardinalPoint"] is not None:
|
||||||
attributes["CardinalPoint"] = int(attributes["CardinalPoint"])
|
attributes["CardinalPoint"] = int(attributes["CardinalPoint"])
|
||||||
ifcopenshell.api.material.edit_profile_usage(
|
ifcopenshell.api.material.edit_profile_usage(
|
||||||
self.file,
|
self.file,
|
||||||
@@ -717,7 +717,11 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
|
|||||||
self.props.material_set_item_material = str(material_set_item.Material.id())
|
self.props.material_set_item_material = str(material_set_item.Material.id())
|
||||||
|
|
||||||
self.props.material_set_item_attributes.clear()
|
self.props.material_set_item_attributes.clear()
|
||||||
bonsai.bim.helper.import_attributes(material_set_item, self.props.material_set_item_attributes)
|
bonsai.bim.helper.import_attributes(
|
||||||
|
material_set_item,
|
||||||
|
self.props.material_set_item_attributes,
|
||||||
|
callback=self.import_attributes_callback,
|
||||||
|
)
|
||||||
|
|
||||||
if material_set_item.is_a("IfcMaterialProfile"):
|
if material_set_item.is_a("IfcMaterialProfile"):
|
||||||
if material_set_item.Profile and material_set_item.Profile.ProfileName:
|
if material_set_item.Profile and material_set_item.Profile.ProfileName:
|
||||||
@@ -725,6 +729,29 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
|
|||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
def import_attributes_callback(
|
||||||
|
self, name: str, prop: Union["Attribute", None], data: dict[str, Any]
|
||||||
|
) -> None | Literal[True]:
|
||||||
|
if data["type"] != "IfcMaterialLayer" or name != "IsVentilated" or not prop:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Keep null semantics unchanged on export, but avoid an empty UI selection.
|
||||||
|
prop.data_type = "enum"
|
||||||
|
prop.special_type = "LOGICAL"
|
||||||
|
prop.enum_items = json.dumps(("TRUE", "FALSE", "UNKNOWN"))
|
||||||
|
|
||||||
|
value = data[name]
|
||||||
|
if value == "UNKNOWN":
|
||||||
|
prop.enum_value = "UNKNOWN"
|
||||||
|
elif value is None:
|
||||||
|
# Keep visible default as FALSE, but preserve null semantics on save.
|
||||||
|
prop.enum_value = "FALSE"
|
||||||
|
prop.is_null = True
|
||||||
|
else:
|
||||||
|
prop.enum_value = "TRUE" if value else "FALSE"
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class DisableEditingMaterialSetItem(bpy.types.Operator):
|
class DisableEditingMaterialSetItem(bpy.types.Operator):
|
||||||
bl_idname = "bim.disable_editing_material_set_item"
|
bl_idname = "bim.disable_editing_material_set_item"
|
||||||
@@ -777,6 +804,8 @@ class EditMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
)
|
)
|
||||||
slab.DumbSlabPlaner().regenerate_from_layer(layer)
|
slab.DumbSlabPlaner().regenerate_from_layer(layer)
|
||||||
wall.DumbWallPlaner().regenerate_from_layer(layer)
|
wall.DumbWallPlaner().regenerate_from_layer(layer)
|
||||||
|
from bonsai.bim.module.drawing.handler import regenerate_dims_for_layer
|
||||||
|
regenerate_dims_for_layer(self.file, layer)
|
||||||
elif material.is_a("IfcMaterialProfileSet"):
|
elif material.is_a("IfcMaterialProfileSet"):
|
||||||
profile_def = None
|
profile_def = None
|
||||||
if mprops.profiles:
|
if mprops.profiles:
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
"Will unassign element from a type if type has a representation."
|
"Will unassign element from a type if type has a representation."
|
||||||
)
|
)
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
mode: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
mode: bpy.props.EnumProperty(
|
||||||
default="BOOLEAN",
|
default="BOOLEAN",
|
||||||
items=tuple((i, i, "") for i in get_args(SplitAlongEdgeMode)),
|
items=tuple((i, i, "") for i in get_args(SplitAlongEdgeMode)),
|
||||||
)
|
)
|
||||||
@@ -359,7 +359,7 @@ class ConfirmQuickFavoriteOperator(bpy.types.Operator):
|
|||||||
bl_idname = "bim.confirm_quick_favorite_operator"
|
bl_idname = "bim.confirm_quick_favorite_operator"
|
||||||
bl_label = "Confirm Operator"
|
bl_label = "Confirm Operator"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
index: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
index: int
|
index: int
|
||||||
@@ -452,10 +452,8 @@ class MoveQuickFavoritesItem(bpy.types.Operator):
|
|||||||
bl_idname = "bim.move_quick_favorites_item"
|
bl_idname = "bim.move_quick_favorites_item"
|
||||||
bl_label = "Move Quick Favorites Item"
|
bl_label = "Move Quick Favorites Item"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
index: bpy.props.IntProperty()
|
||||||
direction: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
direction: bpy.props.EnumProperty(items=[("UP", "Up", ""), ("DOWN", "Down", "")])
|
||||||
items=[("UP", "Up", ""), ("DOWN", "Down", "")]
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
index: int
|
index: int
|
||||||
@@ -474,7 +472,7 @@ class RemoveQuickFavoritesItem(bpy.types.Operator):
|
|||||||
bl_idname = "bim.remove_quick_favorites_item"
|
bl_idname = "bim.remove_quick_favorites_item"
|
||||||
bl_label = "Remove Quick Favorites Item"
|
bl_label = "Remove Quick Favorites Item"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
index: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
index: int
|
index: int
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ QuickFavoriteValueType = Literal["float_value", "bool_value", "int_value", "stri
|
|||||||
|
|
||||||
|
|
||||||
class QuickFavoriteEnumItem(PropertyGroup):
|
class QuickFavoriteEnumItem(PropertyGroup):
|
||||||
name: StringProperty(name="Name", default="") # pyright: ignore[reportRedeclaration]
|
name: StringProperty(name="Name", default="")
|
||||||
display_name: StringProperty(name="Display Name", default="") # pyright: ignore[reportRedeclaration]
|
display_name: StringProperty(name="Display Name", default="")
|
||||||
description: StringProperty(name="Description", default="") # pyright: ignore[reportRedeclaration]
|
description: StringProperty(name="Description", default="")
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
name: str
|
name: str
|
||||||
@@ -51,19 +51,19 @@ def get_enum_items(self: "QuickFavoriteProperty", context: bpy.types.Context | N
|
|||||||
|
|
||||||
|
|
||||||
class QuickFavoriteProperty(PropertyGroup):
|
class QuickFavoriteProperty(PropertyGroup):
|
||||||
name: StringProperty(name="Name", default="") # pyright: ignore[reportRedeclaration]
|
name: StringProperty(name="Name", default="")
|
||||||
display_name: StringProperty(name="Display Name", default="") # pyright: ignore[reportRedeclaration]
|
display_name: StringProperty(name="Display Name", default="")
|
||||||
value_prop: EnumProperty( # pyright: ignore[reportRedeclaration]
|
value_prop: EnumProperty(
|
||||||
name="Value Prop",
|
name="Value Prop",
|
||||||
items=tuple((v, v, "") for v in get_args(QuickFavoriteValueType)),
|
items=tuple((v, v, "") for v in get_args(QuickFavoriteValueType)),
|
||||||
)
|
)
|
||||||
string_value: StringProperty(name="String Value", default="") # pyright: ignore[reportRedeclaration]
|
string_value: StringProperty(name="String Value", default="")
|
||||||
float_value: FloatProperty(name="Float Value", default=0.0) # pyright: ignore[reportRedeclaration]
|
float_value: FloatProperty(name="Float Value", default=0.0)
|
||||||
int_value: IntProperty(name="Int Value", default=0) # pyright: ignore[reportRedeclaration]
|
int_value: IntProperty(name="Int Value", default=0)
|
||||||
bool_value: BoolProperty(name="Bool Value", default=False) # pyright: ignore[reportRedeclaration]
|
bool_value: BoolProperty(name="Bool Value", default=False)
|
||||||
enum_value: EnumProperty(name="Enum Value", items=get_enum_items) # pyright: ignore[reportRedeclaration]
|
enum_value: EnumProperty(name="Enum Value", items=get_enum_items)
|
||||||
enum_items: CollectionProperty(type=QuickFavoriteEnumItem) # pyright: ignore[reportRedeclaration]
|
enum_items: CollectionProperty(type=QuickFavoriteEnumItem)
|
||||||
is_active: BoolProperty( # pyright: ignore[reportRedeclaration]
|
is_active: BoolProperty(
|
||||||
name="Is Active",
|
name="Is Active",
|
||||||
description="Only active properties will be added to the operator when invoked from Quick Favorites",
|
description="Only active properties will be added to the operator when invoked from Quick Favorites",
|
||||||
default=False,
|
default=False,
|
||||||
@@ -100,20 +100,20 @@ def get_operator_suggestions(self: "QuickFavoritesItem", context: bpy.types.Cont
|
|||||||
|
|
||||||
|
|
||||||
class QuickFavoritesItem(PropertyGroup):
|
class QuickFavoritesItem(PropertyGroup):
|
||||||
is_expanded: BoolProperty(name="Is Expanded", default=False) # pyright: ignore[reportRedeclaration]
|
is_expanded: BoolProperty(name="Is Expanded", default=False)
|
||||||
search: StringProperty( # pyright: ignore[reportRedeclaration]
|
search: StringProperty(
|
||||||
name="Search",
|
name="Search",
|
||||||
default="",
|
default="",
|
||||||
search=get_operator_suggestions,
|
search=get_operator_suggestions,
|
||||||
# Resetting `search_options`, allowing users only to use suggestions.
|
# Resetting `search_options`, allowing users only to use suggestions.
|
||||||
search_options=set(),
|
search_options=set(),
|
||||||
)
|
)
|
||||||
properties: CollectionProperty(type=QuickFavoriteProperty) # pyright: ignore[reportRedeclaration]
|
properties: CollectionProperty(type=QuickFavoriteProperty)
|
||||||
operator_id: StringProperty( # pyright: ignore[reportRedeclaration]
|
operator_id: StringProperty(
|
||||||
name="Operator ID",
|
name="Operator ID",
|
||||||
default="",
|
default="",
|
||||||
)
|
)
|
||||||
label: StringProperty( # pyright: ignore[reportRedeclaration]
|
label: StringProperty(
|
||||||
name="Label",
|
name="Label",
|
||||||
description="Label that will be used in Quick Favorites for this operator",
|
description="Label that will be used in Quick Favorites for this operator",
|
||||||
default="",
|
default="",
|
||||||
@@ -139,15 +139,15 @@ class QuickFavoritesItem(PropertyGroup):
|
|||||||
|
|
||||||
|
|
||||||
class BIMMiscProperties(PropertyGroup):
|
class BIMMiscProperties(PropertyGroup):
|
||||||
total_storeys: IntProperty( # pyright: ignore[reportRedeclaration]
|
total_storeys: IntProperty(
|
||||||
name="Total Storeys",
|
name="Total Storeys",
|
||||||
description="Number of storeys above object's storey to take into account for resizing",
|
description="Number of storeys above object's storey to take into account for resizing",
|
||||||
default=1,
|
default=1,
|
||||||
)
|
)
|
||||||
override_colour: FloatVectorProperty( # pyright: ignore[reportRedeclaration]
|
override_colour: FloatVectorProperty(
|
||||||
name="Override Colour", subtype="COLOR", default=(1, 0, 0, 1), min=0.0, max=1.0, size=4
|
name="Override Colour", subtype="COLOR", default=(1, 0, 0, 1), min=0.0, max=1.0, size=4
|
||||||
)
|
)
|
||||||
quick_favorites: CollectionProperty(type=QuickFavoritesItem) # pyright: ignore[reportRedeclaration]
|
quick_favorites: CollectionProperty(type=QuickFavoritesItem)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
total_storeys: int
|
total_storeys: int
|
||||||
|
|||||||
@@ -753,6 +753,8 @@ class PolylineDecorator:
|
|||||||
rv3d = region.data
|
rv3d = region.data
|
||||||
|
|
||||||
polyline_props = tool.Model.get_polyline_props()
|
polyline_props = tool.Model.get_polyline_props()
|
||||||
|
if not polyline_props.snap_mouse_point:
|
||||||
|
return
|
||||||
snap_prop = polyline_props.snap_mouse_point[0]
|
snap_prop = polyline_props.snap_mouse_point[0]
|
||||||
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
|
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
|
||||||
|
|
||||||
@@ -820,6 +822,8 @@ class PolylineDecorator:
|
|||||||
gpu.state.point_size_set(6)
|
gpu.state.point_size_set(6)
|
||||||
|
|
||||||
polyline_props = tool.Model.get_polyline_props()
|
polyline_props = tool.Model.get_polyline_props()
|
||||||
|
if not polyline_props.snap_mouse_point:
|
||||||
|
return
|
||||||
snap_prop = polyline_props.snap_mouse_point[0]
|
snap_prop = polyline_props.snap_mouse_point[0]
|
||||||
# Point related to the mouse
|
# Point related to the mouse
|
||||||
mouse_point = [Vector((snap_prop.x, snap_prop.y, snap_prop.z))]
|
mouse_point = [Vector((snap_prop.x, snap_prop.y, snap_prop.z))]
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
is_parallel21 = tool.Cad.is_x(angle21, (0, 180), tolerance=0.001)
|
is_parallel21 = tool.Cad.is_x(angle21, (0, 180), tolerance=0.001)
|
||||||
is_parallel23 = tool.Cad.is_x(angle23, (0, 180), tolerance=0.001)
|
is_parallel23 = tool.Cad.is_x(angle23, (0, 180), tolerance=0.001)
|
||||||
|
|
||||||
if not all(is_parallel12, is_parallel13, is_parallel21, is_parallel23):
|
if not all([is_parallel12, is_parallel13, is_parallel21, is_parallel23]):
|
||||||
fitting_type = "WYE"
|
fitting_type = "WYE"
|
||||||
|
|
||||||
if not fitting_type:
|
if not fitting_type:
|
||||||
@@ -903,7 +903,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
start_segment_id: bpy.props.IntProperty(name="Start Segment Element ID", default=0)
|
start_segment_id: bpy.props.IntProperty(name="Start Segment Element ID", default=0)
|
||||||
end_segment_id: bpy.props.IntProperty(name="End Segment Element ID", default=0)
|
end_segment_id: bpy.props.IntProperty(name="End Segment Element ID", default=0)
|
||||||
radius: bpy.props.FloatProperty(
|
radius: bpy.props.FloatProperty(
|
||||||
"Bend Inner Radius", description="Bend inner radius in SI units", default=0.2, subtype="DISTANCE", min=0
|
name="Bend Inner Radius", description="Bend inner radius in SI units", default=0.2, subtype="DISTANCE", min=0
|
||||||
)
|
)
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
|
|||||||
@@ -151,25 +151,7 @@ class FilledOpeningGenerator:
|
|||||||
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
|
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
|
||||||
)
|
)
|
||||||
assert representation
|
assert representation
|
||||||
|
|
||||||
# Check if mapped representation - PRESERVE the mapping structure
|
|
||||||
if (
|
|
||||||
representation.RepresentationType == "MappedRepresentation"
|
|
||||||
and len(representation.Items) == 1
|
|
||||||
and representation.Items[0].is_a("IfcMappedItem")
|
|
||||||
):
|
|
||||||
# Store the existing RepresentationMap to reuse it
|
|
||||||
existing_mapping_source = representation.Items[0].MappingSource
|
|
||||||
reuse_mapped_representation = True
|
|
||||||
else:
|
|
||||||
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||||
|
|
||||||
if not reuse_mapped_representation:
|
|
||||||
# Check for library template before generating from filling
|
|
||||||
template_rep = self.get_opening_template_from_type(filling)
|
|
||||||
|
|
||||||
if template_rep:
|
|
||||||
representation = template_rep
|
|
||||||
else:
|
else:
|
||||||
representation = self.generate_opening_from_filling(
|
representation = self.generate_opening_from_filling(
|
||||||
filling, filling_obj, opening_thickness_si=opening_thickness_si
|
filling, filling_obj, opening_thickness_si=opening_thickness_si
|
||||||
@@ -247,109 +229,38 @@ class FilledOpeningGenerator:
|
|||||||
voided_element = opening.VoidsElements[0].RelatingBuildingElement
|
voided_element = opening.VoidsElements[0].RelatingBuildingElement
|
||||||
|
|
||||||
opening_rep = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
|
opening_rep = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
|
||||||
|
|
||||||
# ALWAYS preserve the existing opening representation (Tessellation, SweptSolid, etc.)
|
|
||||||
preserved_representation = None
|
|
||||||
if opening_rep:
|
|
||||||
if (
|
|
||||||
opening_rep.RepresentationType == "MappedRepresentation"
|
|
||||||
and len(opening_rep.Items) == 1
|
|
||||||
and opening_rep.Items[0].is_a("IfcMappedItem")
|
|
||||||
):
|
|
||||||
# For mapped representations, copy the underlying representation
|
|
||||||
preserved_representation = ifcopenshell.util.element.copy_deep(
|
|
||||||
tool.Ifc.get(),
|
|
||||||
opening_rep.Items[0].MappingSource.MappedRepresentation,
|
|
||||||
exclude=["IfcGeometricRepresentationContext"],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# For direct representations (non-mapped), copy them too
|
|
||||||
preserved_representation = ifcopenshell.util.element.copy_deep(
|
|
||||||
tool.Ifc.get(), opening_rep, exclude=["IfcGeometricRepresentationContext"]
|
|
||||||
)
|
|
||||||
|
|
||||||
ifcopenshell.api.geometry.unassign_representation(tool.Ifc.get(), product=opening, representation=opening_rep)
|
ifcopenshell.api.geometry.unassign_representation(tool.Ifc.get(), product=opening, representation=opening_rep)
|
||||||
ifcopenshell.api.geometry.remove_representation(tool.Ifc.get(), representation=opening_rep)
|
ifcopenshell.api.geometry.remove_representation(tool.Ifc.get(), representation=opening_rep)
|
||||||
|
|
||||||
existing_opening_occurrence = self.get_existing_opening_occurrence_if_any(filling)
|
existing_opening_occurrence = self.get_existing_opening_occurrence_if_any(filling)
|
||||||
|
|
||||||
# Priority order for choosing representation:
|
|
||||||
# 1. Existing occurrence with MappedRepresentation (preserve mapping!)
|
|
||||||
# 2. Library template with Tessellation
|
|
||||||
# 3. Preserved representation from old opening (maintain user's work)
|
|
||||||
# 4. Generate from filling (last resort)
|
|
||||||
|
|
||||||
representation_to_use = None
|
|
||||||
reuse_mapped_representation = False
|
|
||||||
existing_mapping_source = None
|
|
||||||
|
|
||||||
if existing_opening_occurrence:
|
if existing_opening_occurrence:
|
||||||
representation = ifcopenshell.util.representation.get_representation(
|
representation = ifcopenshell.util.representation.get_representation(
|
||||||
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
|
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
|
||||||
)
|
)
|
||||||
|
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||||
if (
|
mapped_representation = ifcopenshell.api.geometry.map_representation(
|
||||||
representation
|
tool.Ifc.get(), representation=representation
|
||||||
and representation.RepresentationType == "MappedRepresentation"
|
)
|
||||||
and len(representation.Items) == 1
|
ifcopenshell.api.geometry.assign_representation(
|
||||||
and representation.Items[0].is_a("IfcMappedItem")
|
tool.Ifc.get(), product=opening, representation=mapped_representation
|
||||||
):
|
)
|
||||||
# PRESERVE the mapped structure - reuse the same RepresentationMap
|
|
||||||
existing_mapping_source = representation.Items[0].MappingSource
|
|
||||||
reuse_mapped_representation = True
|
|
||||||
else:
|
else:
|
||||||
representation_to_use = ifcopenshell.util.representation.resolve_representation(representation)
|
|
||||||
|
|
||||||
if not representation_to_use and not reuse_mapped_representation:
|
|
||||||
template_rep = self.get_opening_template_from_type(filling)
|
|
||||||
if template_rep and template_rep.RepresentationType == "Tessellation":
|
|
||||||
representation_to_use = template_rep
|
|
||||||
|
|
||||||
if not representation_to_use and not reuse_mapped_representation and preserved_representation:
|
|
||||||
representation_to_use = preserved_representation
|
|
||||||
|
|
||||||
if not representation_to_use and not reuse_mapped_representation:
|
|
||||||
opening_obj = tool.Ifc.get_object(opening)
|
opening_obj = tool.Ifc.get_object(opening)
|
||||||
if opening_obj:
|
if opening_obj:
|
||||||
tool.Ifc.unlink(element=opening)
|
tool.Ifc.unlink(element=opening)
|
||||||
tool.Blender.remove_data_blocks([opening_obj], remove_unused_data=True)
|
tool.Blender.remove_data_blocks([opening_obj], remove_unused_data=True)
|
||||||
|
|
||||||
filling_obj = tool.Ifc.get_object(filling)
|
filling_obj = tool.Ifc.get_object(filling)
|
||||||
representation_to_use = self.generate_opening_from_filling(filling, filling_obj)
|
representation = self.generate_opening_from_filling(filling, filling_obj)
|
||||||
|
|
||||||
# Create the mapped representation
|
|
||||||
if reuse_mapped_representation:
|
|
||||||
# Reuse existing RepresentationMap - don't create a new one!
|
|
||||||
context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
|
||||||
new_mapped_item = tool.Ifc.get().create_entity(
|
|
||||||
"IfcMappedItem",
|
|
||||||
MappingSource=existing_mapping_source,
|
|
||||||
MappingTarget=tool.Ifc.get().create_entity(
|
|
||||||
"IfcCartesianTransformationOperator3D",
|
|
||||||
Axis1=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(1.0, 0.0, 0.0)),
|
|
||||||
Axis2=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0.0, 1.0, 0.0)),
|
|
||||||
LocalOrigin=tool.Ifc.get().create_entity("IfcCartesianPoint", Coordinates=(0.0, 0.0, 0.0)),
|
|
||||||
Scale=1.0,
|
|
||||||
Axis3=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0.0, 0.0, 1.0)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
mapped_representation = tool.Ifc.get().create_entity(
|
|
||||||
"IfcShapeRepresentation",
|
|
||||||
ContextOfItems=context,
|
|
||||||
RepresentationIdentifier="Body",
|
|
||||||
RepresentationType="MappedRepresentation",
|
|
||||||
Items=[new_mapped_item],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
mapped_representation = ifcopenshell.api.geometry.map_representation(
|
mapped_representation = ifcopenshell.api.geometry.map_representation(
|
||||||
tool.Ifc.get(), representation=representation_to_use
|
tool.Ifc.get(), representation=representation
|
||||||
)
|
)
|
||||||
|
|
||||||
ifcopenshell.api.geometry.assign_representation(
|
ifcopenshell.api.geometry.assign_representation(
|
||||||
tool.Ifc.get(), product=opening, representation=mapped_representation
|
tool.Ifc.get(), product=opening, representation=mapped_representation
|
||||||
)
|
)
|
||||||
|
|
||||||
# update voided object representation...
|
# update voided object representation or all it's parts if it's an aggregate
|
||||||
voided_elements = ifcopenshell.util.element.get_parts(voided_element) or [voided_element]
|
voided_elements = ifcopenshell.util.element.get_parts(voided_element) or [voided_element]
|
||||||
for voided_element in voided_elements:
|
for voided_element in voided_elements:
|
||||||
voided_obj = tool.Ifc.get_object(voided_element)
|
voided_obj = tool.Ifc.get_object(voided_element)
|
||||||
@@ -363,36 +274,6 @@ class FilledOpeningGenerator:
|
|||||||
representation=representation,
|
representation=representation,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_opening_template_from_type(
|
|
||||||
self, filling: ifcopenshell.entity_instance
|
|
||||||
) -> Union[ifcopenshell.entity_instance, None]:
|
|
||||||
"""
|
|
||||||
Check if the filling's type has a stored opening template from library import.
|
|
||||||
"""
|
|
||||||
element_type = ifcopenshell.util.element.get_type(filling)
|
|
||||||
|
|
||||||
if not element_type:
|
|
||||||
return None
|
|
||||||
|
|
||||||
desc = element_type.Description
|
|
||||||
|
|
||||||
if not desc or "||BonsaiOpeningTemplate:" not in desc:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Extract template ID
|
|
||||||
marker = desc.split("||BonsaiOpeningTemplate:")[-1]
|
|
||||||
template_id = int(marker.split("||")[0])
|
|
||||||
|
|
||||||
try:
|
|
||||||
template_rep = tool.Ifc.get().by_id(template_id)
|
|
||||||
# Make a copy so we don't reuse the same representation instance
|
|
||||||
copied = ifcopenshell.util.element.copy_deep(
|
|
||||||
tool.Ifc.get(), template_rep, exclude=["IfcGeometricRepresentationContext"]
|
|
||||||
)
|
|
||||||
return copied
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def generate_opening_from_filling(
|
def generate_opening_from_filling(
|
||||||
self,
|
self,
|
||||||
filling: ifcopenshell.entity_instance,
|
filling: ifcopenshell.entity_instance,
|
||||||
@@ -659,6 +540,16 @@ class AddBoolean(Operator, tool.Ifc.Operator):
|
|||||||
booleans = ifcopenshell.api.geometry.add_boolean(tool.Ifc.get(), first_item, second_items, props.operator)
|
booleans = ifcopenshell.api.geometry.add_boolean(tool.Ifc.get(), first_item, second_items, props.operator)
|
||||||
|
|
||||||
rep_obj = tool.Geometry.get_geometry_props().representation_obj
|
rep_obj = tool.Geometry.get_geometry_props().representation_obj
|
||||||
|
if booleans:
|
||||||
|
# Users typically select two top-level items and expect the
|
||||||
|
# operand to be absorbed into the boolean, not remain as a
|
||||||
|
# standalone item alongside it.
|
||||||
|
representation = tool.Geometry.get_active_representation(rep_obj)
|
||||||
|
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||||
|
second_items_set = set(second_items)
|
||||||
|
new_items = [i for i in representation.Items if i not in second_items_set]
|
||||||
|
if new_items:
|
||||||
|
representation.Items = new_items
|
||||||
rep_element = tool.Ifc.get_entity(rep_obj)
|
rep_element = tool.Ifc.get_entity(rep_obj)
|
||||||
tool.Model.mark_manual_booleans(rep_element, booleans)
|
tool.Model.mark_manual_booleans(rep_element, booleans)
|
||||||
tool.Geometry.reload_representation(rep_obj)
|
tool.Geometry.reload_representation(rep_obj)
|
||||||
|
|||||||
@@ -461,13 +461,26 @@ class PolylineOperator:
|
|||||||
self.tool_state.axis_method = None
|
self.tool_state.axis_method = None
|
||||||
self.tool_state.plane_method = None
|
self.tool_state.plane_method = None
|
||||||
self.tool_state.mode = "Mouse"
|
self.tool_state.mode = "Mouse"
|
||||||
|
# Do not call clear_snap_objs() here — create_snap_obj() validates stale
|
||||||
|
# entries per-object (vertex count + position check), so the BVH cache can
|
||||||
|
# safely persist across invocations. Clearing it caused an 11-second stall
|
||||||
|
# on every Shift+A because SnapObj rebuilds a pure-Python BVH tree.
|
||||||
self.visible_objs = tool.Raycast.get_visible_objects(context)
|
self.visible_objs = tool.Raycast.get_visible_objects(context)
|
||||||
for obj in self.visible_objs:
|
for obj in self.visible_objs:
|
||||||
if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj):
|
if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj):
|
||||||
self.objs_2d_bbox.append(bbox_2d)
|
self.objs_2d_bbox.append(bbox_2d)
|
||||||
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state)
|
self._init_snapping_points(context, event)
|
||||||
self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps)
|
|
||||||
tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state)
|
tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state)
|
||||||
|
|
||||||
tool.Blender.update_viewport()
|
tool.Blender.update_viewport()
|
||||||
context.window_manager.modal_handler_add(self)
|
context.window_manager.modal_handler_add(self)
|
||||||
|
|
||||||
|
def _init_snapping_points(self, context: bpy.types.Context, event: bpy.types.Event) -> None:
|
||||||
|
"""Populate self.snapping_points at operator start.
|
||||||
|
|
||||||
|
Override in subclasses to skip the full BVH snap detection when a cheap
|
||||||
|
placeholder is sufficient. The default runs the full detection pass.
|
||||||
|
"""
|
||||||
|
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state)
|
||||||
|
self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps)
|
||||||
|
|
||||||
|
|||||||
@@ -545,7 +545,7 @@ class ChangeTypePage(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_idname = "bim.change_type_page"
|
bl_idname = "bim.change_type_page"
|
||||||
bl_label = "Change Type Page"
|
bl_label = "Change Type Page"
|
||||||
bl_options = {"REGISTER"}
|
bl_options = {"REGISTER"}
|
||||||
page: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
page: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
page: int
|
page: int
|
||||||
@@ -694,10 +694,14 @@ def generate_box(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[
|
|||||||
|
|
||||||
new_settings = settings.copy()
|
new_settings = settings.copy()
|
||||||
new_settings["context"] = box_context
|
new_settings["context"] = box_context
|
||||||
new_box = ifcopenshell.api.geometry.add_representation(ifc_file, should_run_listeners=False, **new_settings)
|
new_box = ifcopenshell.api.geometry.add_representation(
|
||||||
|
ifc_file,
|
||||||
|
should_run_listeners=False, # ty:ignore[unknown-argument]
|
||||||
|
**new_settings,
|
||||||
|
)
|
||||||
ifcopenshell.api.geometry.assign_representation(
|
ifcopenshell.api.geometry.assign_representation(
|
||||||
ifc_file,
|
ifc_file,
|
||||||
should_run_listeners=False,
|
should_run_listeners=False, # ty:ignore[unknown-argument]
|
||||||
product=product,
|
product=product,
|
||||||
representation=new_box,
|
representation=new_box,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
from math import atan2, degrees, pi, radians
|
from math import atan2, degrees, pi, radians
|
||||||
from typing import Any, Literal, Optional, Union
|
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
@@ -49,7 +49,7 @@ ProfileFrom2PointsReturn = Union[dict[str, Any], None]
|
|||||||
|
|
||||||
|
|
||||||
class DumbProfileGenerator:
|
class DumbProfileGenerator:
|
||||||
def __init__(self, relating_type):
|
def __init__(self, relating_type: ifcopenshell.entity_instance):
|
||||||
self.relating_type = relating_type
|
self.relating_type = relating_type
|
||||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ class DumbProfileGenerator:
|
|||||||
|
|
||||||
|
|
||||||
class DumbProfileRegenerator:
|
class DumbProfileRegenerator:
|
||||||
def regenerate_from_profile_def(self, profile):
|
def regenerate_from_profile_def(self, profile: ifcopenshell.entity_instance) -> None:
|
||||||
self.file = tool.Ifc.get()
|
self.file = tool.Ifc.get()
|
||||||
objs = []
|
objs = []
|
||||||
if not profile:
|
if not profile:
|
||||||
@@ -221,7 +221,7 @@ class DumbProfileRegenerator:
|
|||||||
for element in self.get_element_types_using_profile(profile):
|
for element in self.get_element_types_using_profile(profile):
|
||||||
tool.Model.mark_thumbnail_for_update(element)
|
tool.Model.mark_thumbnail_for_update(element)
|
||||||
|
|
||||||
def regenerate_from_profile(self, usecase_path, ifc_file, settings):
|
def regenerate_from_profile(self, usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
|
||||||
self.file = ifc_file
|
self.file = ifc_file
|
||||||
objs = []
|
objs = []
|
||||||
profile = settings["profile"].Profile
|
profile = settings["profile"].Profile
|
||||||
@@ -233,7 +233,7 @@ class DumbProfileRegenerator:
|
|||||||
objs.append(obj)
|
objs.append(obj)
|
||||||
DumbProfileRecalculator().recalculate(objs)
|
DumbProfileRecalculator().recalculate(objs)
|
||||||
|
|
||||||
def get_elements_using_profile(self, profile):
|
def get_elements_using_profile(self, profile: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
||||||
results = []
|
results = []
|
||||||
profile_sets = [
|
profile_sets = [
|
||||||
mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile")
|
mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile")
|
||||||
@@ -252,7 +252,9 @@ class DumbProfileRegenerator:
|
|||||||
results.extend(rel.RelatedObjects)
|
results.extend(rel.RelatedObjects)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def get_element_types_using_profile(self, profile):
|
def get_element_types_using_profile(
|
||||||
|
self, profile: ifcopenshell.entity_instance
|
||||||
|
) -> list[ifcopenshell.entity_instance]:
|
||||||
results = []
|
results = []
|
||||||
profile_sets = [
|
profile_sets = [
|
||||||
mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile")
|
mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile")
|
||||||
@@ -269,12 +271,18 @@ class ExtendProfile(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_idname = "bim.extend_profile"
|
bl_idname = "bim.extend_profile"
|
||||||
bl_label = "Extend Profile"
|
bl_label = "Extend Profile"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
join_type: bpy.props.StringProperty()
|
join_type: bpy.props.EnumProperty(
|
||||||
|
items=[("-", "Unjoin", ""), ("L", "L", ""), ("V", "V", ""), ("T", "T", "")],
|
||||||
|
default="-",
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
join_type: Literal["-", "L", "V", "T"]
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
selected_objs = context.selected_objects
|
selected_objs = context.selected_objects
|
||||||
joiner = DumbProfileJoiner()
|
joiner = DumbProfileJoiner()
|
||||||
if not self.join_type:
|
if self.join_type == "-":
|
||||||
for obj in selected_objs:
|
for obj in selected_objs:
|
||||||
joiner.unjoin(obj)
|
joiner.unjoin(obj)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
@@ -626,11 +634,15 @@ class DumbProfileJoiner:
|
|||||||
if connection1 == "ATEND":
|
if connection1 == "ATEND":
|
||||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
||||||
plane = self.get_profile_plane(profile2, furthest_plane)
|
plane = self.get_profile_plane(profile2, furthest_plane)
|
||||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
intersect = mathutils.geometry.intersect_line_plane(
|
||||||
|
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||||
|
)
|
||||||
self.body[1] = intersect
|
self.body[1] = intersect
|
||||||
else:
|
else:
|
||||||
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
|
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
|
||||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
intersect = mathutils.geometry.intersect_line_plane(
|
||||||
|
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||||
|
)
|
||||||
max_dim = self.get_max_bound_box_dimension(profile1)
|
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||||
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||||
|
|
||||||
@@ -673,11 +685,15 @@ class DumbProfileJoiner:
|
|||||||
elif connection1 == "ATSTART":
|
elif connection1 == "ATSTART":
|
||||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
||||||
plane = self.get_profile_plane(profile2, furthest_plane)
|
plane = self.get_profile_plane(profile2, furthest_plane)
|
||||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
intersect = mathutils.geometry.intersect_line_plane(
|
||||||
|
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||||
|
)
|
||||||
self.body[0] = intersect
|
self.body[0] = intersect
|
||||||
else:
|
else:
|
||||||
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
|
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
|
||||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
intersect = mathutils.geometry.intersect_line_plane(
|
||||||
|
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||||
|
)
|
||||||
max_dim = self.get_max_bound_box_dimension(profile1)
|
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||||
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||||
|
|
||||||
@@ -721,7 +737,9 @@ class DumbProfileJoiner:
|
|||||||
if connection1 == "ATEND":
|
if connection1 == "ATEND":
|
||||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
||||||
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
|
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
|
||||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
intersect = mathutils.geometry.intersect_line_plane(
|
||||||
|
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||||
|
)
|
||||||
self.body[1] = intersect
|
self.body[1] = intersect
|
||||||
else:
|
else:
|
||||||
plane = self.get_profile_plane(
|
plane = self.get_profile_plane(
|
||||||
@@ -729,7 +747,9 @@ class DumbProfileJoiner:
|
|||||||
furthest_plane if is_relating else closest_plane,
|
furthest_plane if is_relating else closest_plane,
|
||||||
z_inwards=False if is_relating else True,
|
z_inwards=False if is_relating else True,
|
||||||
)
|
)
|
||||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
intersect = mathutils.geometry.intersect_line_plane(
|
||||||
|
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||||
|
)
|
||||||
max_dim = self.get_max_bound_box_dimension(profile1)
|
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||||
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||||
self.clippings.append(
|
self.clippings.append(
|
||||||
@@ -742,7 +762,9 @@ class DumbProfileJoiner:
|
|||||||
elif connection1 == "ATSTART":
|
elif connection1 == "ATSTART":
|
||||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
||||||
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
|
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
|
||||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
intersect = mathutils.geometry.intersect_line_plane(
|
||||||
|
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||||
|
)
|
||||||
self.body[0] = intersect
|
self.body[0] = intersect
|
||||||
else:
|
else:
|
||||||
plane = self.get_profile_plane(
|
plane = self.get_profile_plane(
|
||||||
@@ -750,7 +772,9 @@ class DumbProfileJoiner:
|
|||||||
furthest_plane if is_relating else closest_plane,
|
furthest_plane if is_relating else closest_plane,
|
||||||
z_inwards=False if is_relating else True,
|
z_inwards=False if is_relating else True,
|
||||||
)
|
)
|
||||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
intersect = mathutils.geometry.intersect_line_plane(
|
||||||
|
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
|
||||||
|
)
|
||||||
max_dim = self.get_max_bound_box_dimension(profile1)
|
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||||
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||||
self.clippings.append(
|
self.clippings.append(
|
||||||
|
|||||||
@@ -1729,20 +1729,20 @@ def poll_sverchok_nodes(self: "BIMExternalParametricGeometryProperties", node_tr
|
|||||||
|
|
||||||
|
|
||||||
class BIMExternalParametricGeometryProperties(bpy.types.PropertyGroup):
|
class BIMExternalParametricGeometryProperties(bpy.types.PropertyGroup):
|
||||||
is_editing: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
|
is_editing: bpy.props.BoolProperty(
|
||||||
name="Is Editing Paramteric Geometry",
|
name="Is Editing Paramteric Geometry",
|
||||||
description="Toggle editing parametric geometry.",
|
description="Toggle editing parametric geometry.",
|
||||||
default=False,
|
default=False,
|
||||||
update=update_is_editing,
|
update=update_is_editing,
|
||||||
)
|
)
|
||||||
geometry_source: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
geometry_source: bpy.props.EnumProperty(
|
||||||
name="Geometry Source",
|
name="Geometry Source",
|
||||||
items=[
|
items=[
|
||||||
("GEONODES", "Geometry Nodes", ""),
|
("GEONODES", "Geometry Nodes", ""),
|
||||||
("IFCSVERCHOK", "IFC Sverchok", ""),
|
("IFCSVERCHOK", "IFC Sverchok", ""),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
geo_nodes: bpy.props.PointerProperty( # pyright: ignore[reportRedeclaration]
|
geo_nodes: bpy.props.PointerProperty(
|
||||||
name="Geometry Nodes",
|
name="Geometry Nodes",
|
||||||
description="Geometry nodes tree to use as a source for representation.",
|
description="Geometry nodes tree to use as a source for representation.",
|
||||||
type=bpy.types.GeometryNodeTree,
|
type=bpy.types.GeometryNodeTree,
|
||||||
@@ -1750,7 +1750,7 @@ class BIMExternalParametricGeometryProperties(bpy.types.PropertyGroup):
|
|||||||
poll=lambda self, node_tree: not node_tree.name.startswith("BBIM_EPG"),
|
poll=lambda self, node_tree: not node_tree.name.startswith("BBIM_EPG"),
|
||||||
)
|
)
|
||||||
|
|
||||||
sverchok_nodes: bpy.props.PointerProperty( # pyright: ignore[reportRedeclaration]
|
sverchok_nodes: bpy.props.PointerProperty(
|
||||||
name="Sverchok Nodes",
|
name="Sverchok Nodes",
|
||||||
description="Sverchok node tree to use as a source for representation.",
|
description="Sverchok node tree to use as a source for representation.",
|
||||||
type=bpy.types.NodeTree,
|
type=bpy.types.NodeTree,
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ def calculate_quantities(usecase_path, ifc_file: ifcopenshell.file, settings):
|
|||||||
return
|
return
|
||||||
task = next(e for e in ifc_file.get_inverse(element) if e.is_a("IfcTask"))
|
task = next(e for e in ifc_file.get_inverse(element) if e.is_a("IfcTask"))
|
||||||
qto = ifcopenshell.api.pset.add_qto(
|
qto = ifcopenshell.api.pset.add_qto(
|
||||||
ifc_file, should_run_listeners=False, product=task, name="Qto_TaskBaseQuantities"
|
ifc_file, should_run_listeners=False, product=task, name="Qto_TaskBaseQuantities" # ty:ignore[unknown-argument]
|
||||||
)
|
)
|
||||||
ifcopenshell.api.pset.edit_qto(
|
ifcopenshell.api.pset.edit_qto(
|
||||||
ifc_file,
|
ifc_file,
|
||||||
should_run_listeners=False,
|
should_run_listeners=False, # ty:ignore[unknown-argument]
|
||||||
qto=qto,
|
qto=qto,
|
||||||
properties={
|
properties={
|
||||||
"StandardWork": ifcopenshell.util.date.ifc2datetime(element.ScheduleDuration).days,
|
"StandardWork": ifcopenshell.util.date.ifc2datetime(element.ScheduleDuration).days,
|
||||||
|
|||||||
@@ -468,14 +468,16 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle
|
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle
|
||||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle
|
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle
|
||||||
|
|
||||||
coord_list = builder.get_polyline_coords(extrusion.SweptArea.OuterCurve)
|
profiles = extrusion.SweptArea.Profiles if extrusion.SweptArea.is_a("IfcCompositeProfileDef") else [extrusion.SweptArea]
|
||||||
|
for profile in profiles:
|
||||||
|
coord_list = builder.get_polyline_coords(profile.OuterCurve)
|
||||||
coord_list = [
|
coord_list = [
|
||||||
(p[0], p[1] * abs(cos(existing_x_angle))) for p in coord_list
|
(p[0], p[1] * abs(cos(existing_x_angle))) for p in coord_list
|
||||||
] # Reset the transformation and returns to the original points with 0 degrees
|
] # Reset the transformation and returns to the original points with 0 degrees
|
||||||
coord_list = [
|
coord_list = [
|
||||||
(p[0], p[1] * abs(1 / cos(x_angle))) for p in coord_list
|
(p[0], p[1] * abs(1 / cos(x_angle))) for p in coord_list
|
||||||
] # Apply the transformation for the new x_angle
|
] # Apply the transformation for the new x_angle
|
||||||
builder.set_polyline_coords(extrusion.SweptArea.OuterCurve, coord_list)
|
builder.set_polyline_coords(profile.OuterCurve, coord_list)
|
||||||
|
|
||||||
# The extrusion direction calculated previously default to the positive direction
|
# The extrusion direction calculated previously default to the positive direction
|
||||||
# Here we set the extrusion direction to negative if that's the case
|
# Here we set the extrusion direction to negative if that's the case
|
||||||
@@ -1268,27 +1270,6 @@ class DumbWallJoiner:
|
|||||||
bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=wall2)
|
bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=wall2)
|
||||||
return wall2
|
return wall2
|
||||||
|
|
||||||
def join_Z(self, wall1, slab2):
|
|
||||||
element1 = tool.Ifc.get_entity(wall1)
|
|
||||||
element2 = tool.Ifc.get_entity(slab2)
|
|
||||||
|
|
||||||
for rel in element1.ConnectedFrom:
|
|
||||||
if rel.is_a() == "IfcRelConnectsElements" and rel.Description == "TOP":
|
|
||||||
ifcopenshell.api.geometry.disconnect_element(
|
|
||||||
tool.Ifc.get(),
|
|
||||||
relating_element=rel.RelatingElement,
|
|
||||||
related_element=element1,
|
|
||||||
)
|
|
||||||
|
|
||||||
ifcopenshell.api.geometry.connect_element(
|
|
||||||
tool.Ifc.get(),
|
|
||||||
relating_element=element2,
|
|
||||||
related_element=element1,
|
|
||||||
description="TOP",
|
|
||||||
)
|
|
||||||
|
|
||||||
tool.Model.recreate_wall(element1, wall1)
|
|
||||||
|
|
||||||
def set_axis(self, wall, p1, p2):
|
def set_axis(self, wall, p1, p2):
|
||||||
axis = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW")
|
axis = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW")
|
||||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||||
@@ -1333,29 +1314,6 @@ class DumbWallJoiner:
|
|||||||
self.set_axis(element1, p1, p2)
|
self.set_axis(element1, p1, p2)
|
||||||
tool.Model.recreate_wall(element1, wall1)
|
tool.Model.recreate_wall(element1, wall1)
|
||||||
|
|
||||||
def join_T(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None:
|
|
||||||
element1 = tool.Ifc.get_entity(wall1)
|
|
||||||
element2 = tool.Ifc.get_entity(wall2)
|
|
||||||
axis1 = tool.Model.get_wall_axis(wall1)
|
|
||||||
axis2 = tool.Model.get_wall_axis(wall2)
|
|
||||||
intersect = tool.Cad.intersect_edges(axis1["reference"], axis2["reference"])
|
|
||||||
if intersect:
|
|
||||||
intersect, _ = intersect
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
connection = "ATEND" if tool.Cad.edge_percent(intersect, axis1["reference"]) > 0.5 else "ATSTART"
|
|
||||||
|
|
||||||
ifcopenshell.api.geometry.connect_path(
|
|
||||||
tool.Ifc.get(),
|
|
||||||
related_element=element1,
|
|
||||||
relating_element=element2,
|
|
||||||
relating_connection="ATPATH",
|
|
||||||
related_connection=connection,
|
|
||||||
description="BUTT",
|
|
||||||
)
|
|
||||||
|
|
||||||
tool.Model.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
|
|
||||||
|
|
||||||
def connect(self, obj1: bpy.types.Object, obj2: bpy.types.Object) -> None:
|
def connect(self, obj1: bpy.types.Object, obj2: bpy.types.Object) -> None:
|
||||||
wall1 = tool.Ifc.get_entity(obj1)
|
wall1 = tool.Ifc.get_entity(obj1)
|
||||||
wall2 = tool.Ifc.get_entity(obj2)
|
wall2 = tool.Ifc.get_entity(obj2)
|
||||||
|
|||||||
@@ -943,7 +943,7 @@ class EditObjectUI:
|
|||||||
if "LAYER2" in AuthoringData.data["selected_material_usages"]:
|
if "LAYER2" in AuthoringData.data["selected_material_usages"]:
|
||||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||||
add_layout_hotkey_operator(
|
add_layout_hotkey_operator(
|
||||||
cls.layout, "Extend To Underside", "S_E", bpy.ops.bim.extend_to_underside.__doc__, ui_context
|
cls.layout, "Extend To Underside", "S_E", bpy.ops.bim.extend_walls_to_underside.__doc__, ui_context
|
||||||
)
|
)
|
||||||
|
|
||||||
if AuthoringData.data["is_flippable_element"]:
|
if AuthoringData.data["is_flippable_element"]:
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class EnableEditingPerson(bpy.types.Operator):
|
|||||||
bl_idname = "bim.enable_editing_person"
|
bl_idname = "bim.enable_editing_person"
|
||||||
bl_label = "Enable Editing Person"
|
bl_label = "Enable Editing Person"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
person: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
person: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
person: int
|
person: int
|
||||||
@@ -75,7 +75,7 @@ class RemovePerson(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_idname = "bim.remove_person"
|
bl_idname = "bim.remove_person"
|
||||||
bl_label = "Remove Person"
|
bl_label = "Remove Person"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
person: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
person: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
person: int
|
person: int
|
||||||
@@ -88,7 +88,7 @@ class AddPersonAttribute(bpy.types.Operator):
|
|||||||
bl_idname = "bim.add_person_attribute"
|
bl_idname = "bim.add_person_attribute"
|
||||||
bl_label = "Add Person Attribute"
|
bl_label = "Add Person Attribute"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
name: bpy.props.EnumProperty(
|
||||||
items=tuple((i, i, "") for i in get_args(tool.Owner.PersonAttributeType)),
|
items=tuple((i, i, "") for i in get_args(tool.Owner.PersonAttributeType)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -104,10 +104,10 @@ class RemovePersonAttribute(bpy.types.Operator):
|
|||||||
bl_idname = "bim.remove_person_attribute"
|
bl_idname = "bim.remove_person_attribute"
|
||||||
bl_label = "Remove Person Attribute"
|
bl_label = "Remove Person Attribute"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
name: bpy.props.EnumProperty(
|
||||||
items=tuple((i, i, "") for i in get_args(tool.Owner.PersonAttributeType)),
|
items=tuple((i, i, "") for i in get_args(tool.Owner.PersonAttributeType)),
|
||||||
)
|
)
|
||||||
id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
id: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
name: tool.Owner.PersonAttributeType # pyright: ignore[reportIncompatibleVariableOverride]
|
name: tool.Owner.PersonAttributeType # pyright: ignore[reportIncompatibleVariableOverride]
|
||||||
@@ -122,7 +122,7 @@ class EnableEditingRole(bpy.types.Operator):
|
|||||||
bl_idname = "bim.enable_editing_role"
|
bl_idname = "bim.enable_editing_role"
|
||||||
bl_label = "Enable Editing Role"
|
bl_label = "Enable Editing Role"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
role: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
role: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
role: int
|
role: int
|
||||||
@@ -146,7 +146,7 @@ class AddRole(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_idname = "bim.add_role"
|
bl_idname = "bim.add_role"
|
||||||
bl_label = "Add Role"
|
bl_label = "Add Role"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
parent: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
parent: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
parent: int
|
parent: int
|
||||||
@@ -168,7 +168,7 @@ class RemoveRole(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_idname = "bim.remove_role"
|
bl_idname = "bim.remove_role"
|
||||||
bl_label = "Remove Role"
|
bl_label = "Remove Role"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
role: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
role: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
role: int
|
role: int
|
||||||
@@ -181,8 +181,8 @@ class AddAddress(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_idname = "bim.add_address"
|
bl_idname = "bim.add_address"
|
||||||
bl_label = "Add Address"
|
bl_label = "Add Address"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
parent: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
parent: bpy.props.IntProperty()
|
||||||
ifc_class: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
ifc_class: bpy.props.EnumProperty(
|
||||||
items=tuple((i, i, "") for i in get_args(ADDRESS_TYPE)),
|
items=tuple((i, i, "") for i in get_args(ADDRESS_TYPE)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ class AddAddressAttribute(bpy.types.Operator):
|
|||||||
bl_idname = "bim.add_address_attribute"
|
bl_idname = "bim.add_address_attribute"
|
||||||
bl_label = "Add Address Attribute"
|
bl_label = "Add Address Attribute"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
name: bpy.props.EnumProperty(
|
||||||
items=tuple((i, i, "") for i in get_args(tool.Owner.AddressAttributeType)),
|
items=tuple((i, i, "") for i in get_args(tool.Owner.AddressAttributeType)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -214,10 +214,10 @@ class RemoveAddressAttribute(bpy.types.Operator):
|
|||||||
bl_idname = "bim.remove_address_attribute"
|
bl_idname = "bim.remove_address_attribute"
|
||||||
bl_label = "Remove Address Attribute"
|
bl_label = "Remove Address Attribute"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
name: bpy.props.EnumProperty(
|
||||||
items=tuple((i, i, "") for i in get_args(tool.Owner.AddressAttributeType)),
|
items=tuple((i, i, "") for i in get_args(tool.Owner.AddressAttributeType)),
|
||||||
)
|
)
|
||||||
id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
id: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
name: tool.Owner.AddressAttributeType # pyright: ignore[reportIncompatibleVariableOverride]
|
name: tool.Owner.AddressAttributeType # pyright: ignore[reportIncompatibleVariableOverride]
|
||||||
@@ -232,7 +232,7 @@ class EnableEditingAddress(bpy.types.Operator):
|
|||||||
bl_idname = "bim.enable_editing_address"
|
bl_idname = "bim.enable_editing_address"
|
||||||
bl_label = "Enable Editing Address"
|
bl_label = "Enable Editing Address"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
address: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
address: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
address: int
|
address: int
|
||||||
@@ -265,7 +265,7 @@ class RemoveAddress(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_idname = "bim.remove_address"
|
bl_idname = "bim.remove_address"
|
||||||
bl_label = "Remove Address"
|
bl_label = "Remove Address"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
address: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
address: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
address: int
|
address: int
|
||||||
@@ -278,7 +278,7 @@ class EnableEditingOrganisation(bpy.types.Operator):
|
|||||||
bl_idname = "bim.enable_editing_organisation"
|
bl_idname = "bim.enable_editing_organisation"
|
||||||
bl_label = "Enable Editing Organisation"
|
bl_label = "Enable Editing Organisation"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
organisation: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
organisation: int
|
organisation: int
|
||||||
@@ -320,7 +320,7 @@ class RemoveOrganisation(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_idname = "bim.remove_organisation"
|
bl_idname = "bim.remove_organisation"
|
||||||
bl_label = "Remove Organisation"
|
bl_label = "Remove Organisation"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
organisation: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
organisation: int
|
organisation: int
|
||||||
@@ -333,8 +333,8 @@ class AddPersonAndOrganisation(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_idname = "bim.add_person_and_organisation"
|
bl_idname = "bim.add_person_and_organisation"
|
||||||
bl_label = "Add Person And Organisation"
|
bl_label = "Add Person And Organisation"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
person: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
person: bpy.props.IntProperty()
|
||||||
organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
organisation: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
person: int
|
person: int
|
||||||
@@ -350,7 +350,7 @@ class RemovePersonAndOrganisation(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_idname = "bim.remove_person_and_organisation"
|
bl_idname = "bim.remove_person_and_organisation"
|
||||||
bl_label = "Remove Person And Organisation"
|
bl_label = "Remove Person And Organisation"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
person_and_organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
person_and_organisation: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
person_and_organisation: int
|
person_and_organisation: int
|
||||||
@@ -365,7 +365,7 @@ class SetUser(bpy.types.Operator):
|
|||||||
bl_idname = "bim.set_user"
|
bl_idname = "bim.set_user"
|
||||||
bl_label = "Set User"
|
bl_label = "Set User"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
user: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
user: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
user: int
|
user: int
|
||||||
@@ -401,7 +401,7 @@ class EnableEditingActor(bpy.types.Operator):
|
|||||||
bl_idname = "bim.enable_editing_actor"
|
bl_idname = "bim.enable_editing_actor"
|
||||||
bl_label = "Enable Editing Actor"
|
bl_label = "Enable Editing Actor"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
actor: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
actor: int
|
actor: int
|
||||||
@@ -434,7 +434,7 @@ class RemoveActor(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_idname = "bim.remove_actor"
|
bl_idname = "bim.remove_actor"
|
||||||
bl_label = "Remove Actor"
|
bl_label = "Remove Actor"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
actor: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
actor: int
|
actor: int
|
||||||
@@ -447,7 +447,7 @@ class AssignActor(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_idname = "bim.assign_actor"
|
bl_idname = "bim.assign_actor"
|
||||||
bl_label = "Assign Actor"
|
bl_label = "Assign Actor"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
actor: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
actor: int
|
actor: int
|
||||||
@@ -462,7 +462,7 @@ class UnassignActor(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_idname = "bim.unassign_actor"
|
bl_idname = "bim.unassign_actor"
|
||||||
bl_label = "Unassign Actor"
|
bl_label = "Unassign Actor"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
actor: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
actor: int
|
actor: int
|
||||||
@@ -481,7 +481,7 @@ class RemoveApplication(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
"Remove provided IfcApplication."
|
"Remove provided IfcApplication."
|
||||||
"\n\nFor safety will only work on applications without inverses (they are typically marked as '(unused)'."
|
"\n\nFor safety will only work on applications without inverses (they are typically marked as '(unused)'."
|
||||||
)
|
)
|
||||||
application_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
application_id: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
application_id: int
|
application_id: int
|
||||||
@@ -525,7 +525,7 @@ class EnableEditingApplication(bpy.types.Operator):
|
|||||||
bl_idname = "bim.enable_editing_application"
|
bl_idname = "bim.enable_editing_application"
|
||||||
bl_label = "Enable Editing Application"
|
bl_label = "Enable Editing Application"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
application_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
application_id: bpy.props.IntProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
application_id: int
|
application_id: int
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ classes = (
|
|||||||
operator.UnlinkIfc,
|
operator.UnlinkIfc,
|
||||||
operator.UnloadLink,
|
operator.UnloadLink,
|
||||||
workspace.ExploreHotkey,
|
workspace.ExploreHotkey,
|
||||||
|
operator.GenerateUVMap,
|
||||||
prop.LibraryBreadcrumb,
|
prop.LibraryBreadcrumb,
|
||||||
prop.LibraryElement,
|
prop.LibraryElement,
|
||||||
prop.FilterCategory,
|
prop.FilterCategory,
|
||||||
|
|||||||
@@ -86,9 +86,7 @@ class NewProject(bpy.types.Operator):
|
|||||||
bl_label = "New Project"
|
bl_label = "New Project"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Start a new IFC project in a fresh session"
|
bl_description = "Start a new IFC project in a fresh session"
|
||||||
preset: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
preset: bpy.props.EnumProperty(items=[(i, i, "") for i in get_args(PresetType)])
|
||||||
items=[(i, i, "") for i in get_args(PresetType)]
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
preset: PresetType
|
preset: PresetType
|
||||||
@@ -182,6 +180,11 @@ class SelectLibraryFile(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
|||||||
append_all: bpy.props.BoolProperty(default=False)
|
append_all: bpy.props.BoolProperty(default=False)
|
||||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
|
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
filter_glob: str
|
||||||
|
append_all: bool
|
||||||
|
use_relative_path: bool
|
||||||
|
|
||||||
reload_previous_file = False
|
reload_previous_file = False
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
@@ -558,8 +561,12 @@ class AppendEntireLibrary(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
class AppendLibraryElementByQuery(bpy.types.Operator, tool.Ifc.Operator):
|
class AppendLibraryElementByQuery(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
bl_idname = "bim.append_library_element_by_query"
|
bl_idname = "bim.append_library_element_by_query"
|
||||||
bl_label = "Append Library Element By Query"
|
bl_label = "Append Library Element By Query"
|
||||||
|
|
||||||
query: bpy.props.StringProperty(name="Query")
|
query: bpy.props.StringProperty(name="Query")
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
query: str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return tool.Ifc.get()
|
return tool.Ifc.get()
|
||||||
@@ -591,6 +598,11 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
prop_index: bpy.props.IntProperty()
|
prop_index: bpy.props.IntProperty()
|
||||||
assume_unique_by_name: bpy.props.BoolProperty(name="Assume Unique By Name", default=True, options={"SKIP_SAVE"})
|
assume_unique_by_name: bpy.props.BoolProperty(name="Assume Unique By Name", default=True, options={"SKIP_SAVE"})
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
definition: int
|
||||||
|
prop_index: int
|
||||||
|
assume_unique_by_name: bool
|
||||||
|
|
||||||
file: ifcopenshell.file
|
file: ifcopenshell.file
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -618,8 +630,6 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
if not element:
|
if not element:
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
if element.is_a("IfcTypeProduct"):
|
if element.is_a("IfcTypeProduct"):
|
||||||
# Store opening template from library if it exists
|
|
||||||
self.store_opening_template_from_library(element, library_file)
|
|
||||||
self.import_type_from_ifc(element, context)
|
self.import_type_from_ifc(element, context)
|
||||||
elif element.is_a("IfcProduct"):
|
elif element.is_a("IfcProduct"):
|
||||||
# NOTE: Non-types are not exposed in UI directly
|
# NOTE: Non-types are not exposed in UI directly
|
||||||
@@ -720,53 +730,6 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
if element.is_a("IfcSurfaceStyle") and not tool.Ifc.get_object_by_identifier(element.id()):
|
if element.is_a("IfcSurfaceStyle") and not tool.Ifc.get_object_by_identifier(element.id()):
|
||||||
ifc_importer.create_style(element)
|
ifc_importer.create_style(element)
|
||||||
|
|
||||||
def store_opening_template_from_library(
|
|
||||||
self, element: ifcopenshell.entity_instance, library_file: ifcopenshell.file
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Find an opening representation in the library and copy it to the current file
|
|
||||||
as a template. Store the template ID on the type for later retrieval.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
library_element = library_file.by_guid(element.GlobalId)
|
|
||||||
except:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Find occurrences with openings in the library
|
|
||||||
library_occurrences = ifcopenshell.util.element.get_types(library_element)
|
|
||||||
|
|
||||||
for occurrence in library_occurrences:
|
|
||||||
if not getattr(occurrence, "FillsVoids", None):
|
|
||||||
continue
|
|
||||||
|
|
||||||
library_opening = occurrence.FillsVoids[0].RelatingOpeningElement
|
|
||||||
library_opening_rep = ifcopenshell.util.representation.get_representation(
|
|
||||||
library_opening, "Model", "Body", "MODEL_VIEW"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not library_opening_rep:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check if mapped representation
|
|
||||||
if (
|
|
||||||
library_opening_rep.RepresentationType == "MappedRepresentation"
|
|
||||||
and len(library_opening_rep.Items) == 1
|
|
||||||
and library_opening_rep.Items[0].is_a("IfcMappedItem")
|
|
||||||
):
|
|
||||||
|
|
||||||
mapped_rep = library_opening_rep.Items[0].MappingSource.MappedRepresentation
|
|
||||||
|
|
||||||
# Store ALL representation types (Tessellation, SweptSolid, etc.)
|
|
||||||
template_rep = ifcopenshell.util.element.copy_deep(
|
|
||||||
self.file, mapped_rep, exclude=["IfcGeometricRepresentationContext"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Store reference in type's Description
|
|
||||||
current_desc = element.Description or ""
|
|
||||||
element.Description = f"{current_desc}||BonsaiOpeningTemplate:{template_rep.id()}"
|
|
||||||
return
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
class EditProjectLibrary(bpy.types.Operator):
|
class EditProjectLibrary(bpy.types.Operator):
|
||||||
bl_idname = "bim.edit_project_library"
|
bl_idname = "bim.edit_project_library"
|
||||||
@@ -1016,6 +979,15 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
|||||||
use_detailed_tooltip: bpy.props.BoolProperty(default=False, options={"HIDDEN"})
|
use_detailed_tooltip: bpy.props.BoolProperty(default=False, options={"HIDDEN"})
|
||||||
filename_ext = ".ifc"
|
filename_ext = ".ifc"
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
filepath: str
|
||||||
|
filter_glob: str
|
||||||
|
is_advanced: bool
|
||||||
|
use_relative_path: bool
|
||||||
|
should_start_fresh_session: bool
|
||||||
|
import_without_ifc_data: bool
|
||||||
|
use_detailed_tooltip: bool
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def description(cls, context, properties):
|
def description(cls, context, properties):
|
||||||
tooltip = cls.bl_description
|
tooltip = cls.bl_description
|
||||||
@@ -1116,7 +1088,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
|||||||
else:
|
else:
|
||||||
return self.finish_loading_project(context)
|
return self.finish_loading_project(context)
|
||||||
|
|
||||||
def finish_loading_project(self, context):
|
def finish_loading_project(self, context: bpy.types.Context) -> set["rna_enums.OperatorReturnItems"]:
|
||||||
try:
|
try:
|
||||||
filepath = self.get_filepath()
|
filepath = self.get_filepath()
|
||||||
if not self.is_existing_ifc_file():
|
if not self.is_existing_ifc_file():
|
||||||
@@ -1316,6 +1288,9 @@ class ToggleFilterCategories(bpy.types.Operator):
|
|||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
should_select: bpy.props.BoolProperty(name="Should Select", default=True)
|
should_select: bpy.props.BoolProperty(name="Should Select", default=True)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
should_select: bool
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
props = tool.Project.get_project_props()
|
props = tool.Project.get_project_props()
|
||||||
for filter_category in props.filter_categories:
|
for filter_category in props.filter_categories:
|
||||||
@@ -1338,6 +1313,14 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
|
|||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True)
|
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True)
|
||||||
|
query: bpy.props.StringProperty(
|
||||||
|
name="Query",
|
||||||
|
description=(
|
||||||
|
"Custom selector query to use to load element from a linked model. E.g. 'IfcElement'.\n\n"
|
||||||
|
"Default query - IfcElement, but excluding IfcProxy, IfcSpatialStructureElement, IfcSpatialElement, IfcFeatureElement."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
filename_ext = ".ifc"
|
filename_ext = ".ifc"
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -1347,20 +1330,25 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
|
|||||||
filter_glob: str
|
filter_glob: str
|
||||||
use_relative_path: bool
|
use_relative_path: bool
|
||||||
use_cache: bool
|
use_cache: bool
|
||||||
|
query: str
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
|
assert self.layout
|
||||||
pprops = tool.Project.get_project_props()
|
pprops = tool.Project.get_project_props()
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.prop(self, "use_relative_path")
|
row.prop(self, "use_relative_path")
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.prop(self, "use_cache")
|
row.prop(self, "use_cache")
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.prop(pprops, "false_origin_mode")
|
row.label(text="False Origin Mode:")
|
||||||
|
row = self.layout.row()
|
||||||
|
row.prop(pprops, "false_origin_mode", text="")
|
||||||
if pprops.false_origin_mode == "MANUAL":
|
if pprops.false_origin_mode == "MANUAL":
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.prop(pprops, "false_origin")
|
row.prop(pprops, "false_origin")
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.prop(pprops, "project_north")
|
row.prop(pprops, "project_north")
|
||||||
|
self.layout.prop(self, "query", placeholder="IfcElement")
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
start = time.time()
|
start = time.time()
|
||||||
@@ -1393,7 +1381,7 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
|
|||||||
new.ifc_definition_id = reference.id()
|
new.ifc_definition_id = reference.id()
|
||||||
new.name = filepath
|
new.name = filepath
|
||||||
new.filepath = filepath
|
new.filepath = filepath
|
||||||
bpy.ops.bim.load_link(link_index=-1, use_cache=self.use_cache)
|
bpy.ops.bim.load_link(link_index=-1, use_cache=self.use_cache, query=self.query)
|
||||||
|
|
||||||
|
|
||||||
class UnlinkIfc(bpy.types.Operator, tool.Ifc.Operator):
|
class UnlinkIfc(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
@@ -1401,8 +1389,12 @@ class UnlinkIfc(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_label = "Unlink IFC"
|
bl_label = "Unlink IFC"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Remove the selected file from the link list"
|
bl_description = "Remove the selected file from the link list"
|
||||||
|
|
||||||
link_index: bpy.props.IntProperty(name="Link Index")
|
link_index: bpy.props.IntProperty(name="Link Index")
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
link_index: int
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
props = tool.Project.get_project_props()
|
props = tool.Project.get_project_props()
|
||||||
link = props.links[self.link_index]
|
link = props.links[self.link_index]
|
||||||
@@ -1421,8 +1413,12 @@ class UnloadLink(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_label = "Unload Link"
|
bl_label = "Unload Link"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Unload the selected linked file"
|
bl_description = "Unload the selected linked file"
|
||||||
|
|
||||||
link_index: bpy.props.IntProperty(name="Link Index")
|
link_index: bpy.props.IntProperty(name="Link Index")
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
link_index: int
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
link = tool.Project.get_project_props().links[self.link_index]
|
link = tool.Project.get_project_props().links[self.link_index]
|
||||||
if obj := tool.Project.get_link_empty_handle(link):
|
if obj := tool.Project.get_link_empty_handle(link):
|
||||||
@@ -1444,12 +1440,14 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Load the selected file"
|
bl_description = "Load the selected file"
|
||||||
|
|
||||||
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
|
link_index: bpy.props.IntProperty(name="Link Index")
|
||||||
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) # pyright: ignore[reportRedeclaration]
|
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True)
|
||||||
|
query: bpy.props.StringProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
link_index: int
|
link_index: int
|
||||||
use_cache: bool
|
use_cache: bool
|
||||||
|
query: str
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
self.link = tool.Project.get_project_props().links[self.link_index]
|
self.link = tool.Project.get_project_props().links[self.link_index]
|
||||||
@@ -1491,8 +1489,20 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
def link_ifc(self) -> Union[set[str], None]:
|
def link_ifc(self) -> Union[set[str], None]:
|
||||||
blend_filepath = self.filepath_.with_suffix(".ifc.cache.blend")
|
blend_filepath = self.filepath_.with_suffix(".ifc.cache.blend")
|
||||||
h5_filepath = self.filepath_.with_suffix(".ifc.cache.h5")
|
h5_filepath = self.filepath_.with_suffix(".ifc.cache.h5")
|
||||||
|
json_filepath = self.filepath_.with_suffix(".ifc.cache.json")
|
||||||
|
|
||||||
if not self.use_cache and blend_filepath.exists():
|
def should_clear_cache() -> bool:
|
||||||
|
if not self.use_cache:
|
||||||
|
return True
|
||||||
|
if not blend_filepath.exists():
|
||||||
|
return False
|
||||||
|
data = json.loads(json_filepath.read_text())
|
||||||
|
# Empty 'query' - model loaded without custom query.
|
||||||
|
# Missing 'query' - model was loaded before custom queries were introduced in Bonsai.
|
||||||
|
query = data.get("query", "")
|
||||||
|
return query != self.query
|
||||||
|
|
||||||
|
if should_clear_cache():
|
||||||
os.remove(blend_filepath)
|
os.remove(blend_filepath)
|
||||||
|
|
||||||
if not blend_filepath.exists():
|
if not blend_filepath.exists():
|
||||||
@@ -1520,7 +1530,7 @@ def run():
|
|||||||
pprops.project_north = "{pprops.project_north}"
|
pprops.project_north = "{pprops.project_north}"
|
||||||
# Use absolute path to be safe from cwd changes.
|
# Use absolute path to be safe from cwd changes.
|
||||||
try:
|
try:
|
||||||
bpy.ops.bim.load_linked_project(filepath=r"{str(self.filepath_)}")
|
bpy.ops.bim.load_linked_project(filepath=r"{str(self.filepath_)}", query={repr(self.query)})
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
# Operator failed (returned CANCELLED with error report)
|
# Operator failed (returned CANCELLED with error report)
|
||||||
print(f"Failed to load linked project: {{e}}")
|
print(f"Failed to load linked project: {{e}}")
|
||||||
@@ -1606,8 +1616,12 @@ class ReloadLink(bpy.types.Operator):
|
|||||||
bl_label = "Reload Link"
|
bl_label = "Reload Link"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Reload the selected file"
|
bl_description = "Reload the selected file"
|
||||||
|
|
||||||
link_index: bpy.props.IntProperty(name="Link Index")
|
link_index: bpy.props.IntProperty(name="Link Index")
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
link_index: int
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
bpy.ops.bim.unload_link(link_index=self.link_index)
|
bpy.ops.bim.unload_link(link_index=self.link_index)
|
||||||
return bpy.ops.bim.load_link(link_index=self.link_index, use_cache=False) or {"FINISHED"}
|
return bpy.ops.bim.load_link(link_index=self.link_index, use_cache=False) or {"FINISHED"}
|
||||||
@@ -1618,8 +1632,12 @@ class ToggleLinkSelectability(bpy.types.Operator):
|
|||||||
bl_label = "Toggle Link Selectability"
|
bl_label = "Toggle Link Selectability"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Toggle selectability"
|
bl_description = "Toggle selectability"
|
||||||
|
|
||||||
link_index: bpy.props.IntProperty(name="Link Index")
|
link_index: bpy.props.IntProperty(name="Link Index")
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
link_index: int
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
props = tool.Project.get_project_props()
|
props = tool.Project.get_project_props()
|
||||||
link = props.links[self.link_index]
|
link = props.links[self.link_index]
|
||||||
@@ -1647,8 +1665,8 @@ class ToggleLinkVisibility(bpy.types.Operator):
|
|||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Toggle visibility between SOLID and WIREFRAME"
|
bl_description = "Toggle visibility between SOLID and WIREFRAME"
|
||||||
|
|
||||||
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
|
link_index: bpy.props.IntProperty(name="Link Index")
|
||||||
mode: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
mode: bpy.props.EnumProperty(
|
||||||
name="Visibility Mode",
|
name="Visibility Mode",
|
||||||
items=((i, i, "") for i in ("WIREFRAME", "VISIBLE")),
|
items=((i, i, "") for i in ("WIREFRAME", "VISIBLE")),
|
||||||
)
|
)
|
||||||
@@ -1788,8 +1806,12 @@ class SelectLinkHandle(bpy.types.Operator):
|
|||||||
bl_label = "Select Link Handle"
|
bl_label = "Select Link Handle"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Select link empty object handle"
|
bl_description = "Select link empty object handle"
|
||||||
|
|
||||||
link_index: bpy.props.IntProperty(name="Link Index")
|
link_index: bpy.props.IntProperty(name="Link Index")
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
link_index: int
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
props = tool.Project.get_project_props()
|
props = tool.Project.get_project_props()
|
||||||
link = props.links[self.link_index]
|
link = props.links[self.link_index]
|
||||||
@@ -1807,7 +1829,7 @@ class SelectLinkedModelElement(bpy.types.Operator):
|
|||||||
bl_options = {"REGISTER"}
|
bl_options = {"REGISTER"}
|
||||||
bl_description = "Select an element in the currently selected linked model by providing GlobalId."
|
bl_description = "Select an element in the currently selected linked model by providing GlobalId."
|
||||||
|
|
||||||
guid: bpy.props.StringProperty(name="GlobalId") # pyright: ignore[reportRedeclaration]
|
guid: bpy.props.StringProperty(name="GlobalId")
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
guid: str
|
guid: str
|
||||||
@@ -1852,6 +1874,13 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
|
|||||||
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"})
|
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"})
|
||||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
|
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
filter_glob: str
|
||||||
|
json_version: str
|
||||||
|
json_compact: bool
|
||||||
|
should_save_as: bool
|
||||||
|
use_relative_path: bool
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return tool.Ifc.get()
|
return tool.Ifc.get()
|
||||||
@@ -2000,6 +2029,12 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
|
|||||||
bl_description = "Operator is used to load a project .cache.blend to then link it to the IFC file."
|
bl_description = "Operator is used to load a project .cache.blend to then link it to the IFC file."
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
query: bpy.props.StringProperty()
|
||||||
|
"""See ``bim.link_ifc``."""
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
query: str
|
||||||
|
|
||||||
file: ifcopenshell.file
|
file: ifcopenshell.file
|
||||||
meshes: dict[str, bpy.types.Mesh]
|
meshes: dict[str, bpy.types.Mesh]
|
||||||
# Material names is derived from diffuse as in 'r-g-b-a'.
|
# Material names is derived from diffuse as in 'r-g-b-a'.
|
||||||
@@ -2049,6 +2084,9 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
|
|||||||
tool.Loader.settings.context_settings = tool.Loader.create_settings()
|
tool.Loader.settings.context_settings = tool.Loader.create_settings()
|
||||||
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
|
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
|
||||||
|
|
||||||
|
if self.query:
|
||||||
|
self.elements = ifcopenshell.util.selector.filter_elements(self.file, self.query)
|
||||||
|
else:
|
||||||
self.elements = set(self.file.by_type("IfcElement"))
|
self.elements = set(self.file.by_type("IfcElement"))
|
||||||
if self.file.schema in ("IFC2X3", "IFC4"):
|
if self.file.schema in ("IFC2X3", "IFC4"):
|
||||||
self.elements |= set(self.file.by_type("IfcProxy"))
|
self.elements |= set(self.file.by_type("IfcProxy"))
|
||||||
@@ -2081,6 +2119,7 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
|
|||||||
"false_origin_mode": pprops.false_origin_mode,
|
"false_origin_mode": pprops.false_origin_mode,
|
||||||
"false_origin": pprops.false_origin,
|
"false_origin": pprops.false_origin,
|
||||||
"project_north": pprops.project_north,
|
"project_north": pprops.project_north,
|
||||||
|
"query": self.query,
|
||||||
}
|
}
|
||||||
with open(self.json_filepath, "w") as f:
|
with open(self.json_filepath, "w") as f:
|
||||||
json.dump(data, f)
|
json.dump(data, f)
|
||||||
@@ -2380,8 +2419,8 @@ class HideQueriedLinkedElement(bpy.types.Operator):
|
|||||||
)
|
)
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
unhide_all: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
|
unhide_all: bpy.props.BoolProperty(options={"SKIP_SAVE"})
|
||||||
hide_all_except: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
|
hide_all_except: bpy.props.BoolProperty(options={"SKIP_SAVE"})
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
unhide_all: bool
|
unhide_all: bool
|
||||||
@@ -2495,7 +2534,7 @@ class EnableCulling(bpy.types.Operator):
|
|||||||
self.total_mousemoves = 0
|
self.total_mousemoves = 0
|
||||||
self.cullable_objects = []
|
self.cullable_objects = []
|
||||||
|
|
||||||
def modal(self, context, event):
|
def modal(self, context, event) -> set["rna_enums.OperatorReturnItems"]:
|
||||||
if not LinksData.enable_culling:
|
if not LinksData.enable_culling:
|
||||||
for obj in bpy.context.visible_objects:
|
for obj in bpy.context.visible_objects:
|
||||||
if obj.type == "MESH" and obj.name.startswith("Ifc"):
|
if obj.type == "MESH" and obj.name.startswith("Ifc"):
|
||||||
@@ -2526,7 +2565,7 @@ class EnableCulling(bpy.types.Operator):
|
|||||||
|
|
||||||
return {"PASS_THROUGH"}
|
return {"PASS_THROUGH"}
|
||||||
|
|
||||||
def is_view_changed(self, context):
|
def is_view_changed(self, context: bpy.types.Context) -> bool:
|
||||||
view_matrix = context.region_data.view_matrix
|
view_matrix = context.region_data.view_matrix
|
||||||
projection_matrix = context.region_data.window_matrix
|
projection_matrix = context.region_data.window_matrix
|
||||||
vp_matrix = projection_matrix @ view_matrix
|
vp_matrix = projection_matrix @ view_matrix
|
||||||
@@ -2541,7 +2580,7 @@ class EnableCulling(bpy.types.Operator):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def is_object_in_view(self, obj, context, camera_position):
|
def is_object_in_view(self, obj: bpy.types.Object, context: bpy.types.Context, camera_position: Vector) -> bool:
|
||||||
# Get the view matrix and the projection matrix from the active viewport
|
# Get the view matrix and the projection matrix from the active viewport
|
||||||
view_matrix = context.region_data.view_matrix
|
view_matrix = context.region_data.view_matrix
|
||||||
projection_matrix = context.region_data.window_matrix
|
projection_matrix = context.region_data.window_matrix
|
||||||
@@ -2568,7 +2607,7 @@ class EnableCulling(bpy.types.Operator):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set["rna_enums.OperatorReturnItems"]:
|
||||||
LinksData.enable_culling = True
|
LinksData.enable_culling = True
|
||||||
self.cullable_objects = []
|
self.cullable_objects = []
|
||||||
for obj in bpy.context.visible_objects:
|
for obj in bpy.context.visible_objects:
|
||||||
@@ -2858,6 +2897,10 @@ class IFCFileHandlerOperator(bpy.types.Operator):
|
|||||||
directory: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE", "HIDDEN"})
|
directory: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE", "HIDDEN"})
|
||||||
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={"SKIP_SAVE", "HIDDEN"})
|
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={"SKIP_SAVE", "HIDDEN"})
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
directory: str
|
||||||
|
files: list[bpy.types.OperatorFileListElement]
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
# Keeping code in .invoke() as we'll probably add some
|
# Keeping code in .invoke() as we'll probably add some
|
||||||
# popup windows later.
|
# popup windows later.
|
||||||
@@ -2909,6 +2952,9 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
|
|||||||
|
|
||||||
measure_type: bpy.props.StringProperty()
|
measure_type: bpy.props.StringProperty()
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
measure_type: str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return context.space_data.type == "VIEW_3D"
|
return context.space_data.type == "VIEW_3D"
|
||||||
@@ -3005,6 +3051,9 @@ class MeasureFaceAreaTool(bpy.types.Operator, PolylineOperator):
|
|||||||
|
|
||||||
measure_type: bpy.props.StringProperty()
|
measure_type: bpy.props.StringProperty()
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
measure_type: str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return context.space_data.type == "VIEW_3D"
|
return context.space_data.type == "VIEW_3D"
|
||||||
@@ -3107,7 +3156,10 @@ class ClearMeasurement(bpy.types.Operator):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
polyline_props = tool.Model.get_polyline_props()
|
polyline_props = tool.Model.get_polyline_props()
|
||||||
return len(polyline_props.measurement_polyline) > 0
|
if len(polyline_props.measurement_polyline) > 0:
|
||||||
|
return True
|
||||||
|
cls.poll_message_set("No measurement to clear.")
|
||||||
|
return False
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
polyline_props = tool.Model.get_polyline_props()
|
polyline_props = tool.Model.get_polyline_props()
|
||||||
@@ -3211,7 +3263,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
|
|||||||
super().invoke(context, event)
|
super().invoke(context, event)
|
||||||
return {"RUNNING_MODAL"}
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
def cancel_tool(self, context):
|
def cancel_tool(self, context: bpy.types.Context) -> set["rna_enums.OperatorReturnItems"]:
|
||||||
context.workspace.status_text_set(text=None)
|
context.workspace.status_text_set(text=None)
|
||||||
if hasattr(self, "tool_state"):
|
if hasattr(self, "tool_state"):
|
||||||
self.tool_state.plane_method = None
|
self.tool_state.plane_method = None
|
||||||
@@ -3219,7 +3271,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
|
|||||||
tool.Blender.update_viewport()
|
tool.Blender.update_viewport()
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
def handle_custom_instructions(self, context):
|
def handle_custom_instructions(self, context: bpy.types.Context) -> None:
|
||||||
if len(self.selected_points) == 0:
|
if len(self.selected_points) == 0:
|
||||||
instruction_text = "Click First Point on Image"
|
instruction_text = "Click First Point on Image"
|
||||||
elif len(self.selected_points) == 1:
|
elif len(self.selected_points) == 1:
|
||||||
@@ -3234,14 +3286,14 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
|
|||||||
|
|
||||||
context.workspace.status_text_set(text=instruction_text)
|
context.workspace.status_text_set(text=instruction_text)
|
||||||
|
|
||||||
def calculate_distance(self):
|
def calculate_distance(self) -> None:
|
||||||
if len(self.selected_points) == 2:
|
if len(self.selected_points) == 2:
|
||||||
point1 = self.selected_points[0]
|
point1 = self.selected_points[0]
|
||||||
point2 = self.selected_points[1]
|
point2 = self.selected_points[1]
|
||||||
distance_3d = (point2 - point1).length
|
distance_3d = (point2 - point1).length
|
||||||
self.calculated_distance = distance_3d / self.unit_scale
|
self.calculated_distance = distance_3d / self.unit_scale
|
||||||
|
|
||||||
def apply_scaling(self, context):
|
def apply_scaling(self, context: bpy.types.Context) -> set["rna_enums.OperatorReturnItems"]:
|
||||||
if len(self.selected_points) != 2:
|
if len(self.selected_points) != 2:
|
||||||
self.report({"ERROR"}, "Two points must be selected")
|
self.report({"ERROR"}, "Two points must be selected")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
@@ -3301,6 +3353,9 @@ class LoadBlendMetadataAndIFC(bpy.types.Operator):
|
|||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
filepath: bpy.props.StringProperty(name="IFC File Path", default="")
|
filepath: bpy.props.StringProperty(name="IFC File Path", default="")
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
filepath: str
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
ifc_file = self.filepath
|
ifc_file = self.filepath
|
||||||
if not ifc_file:
|
if not ifc_file:
|
||||||
@@ -3333,3 +3388,19 @@ class LoadBlendMetadataAndIFC(bpy.types.Operator):
|
|||||||
bpy.app.handlers.load_post.append(load_handler)
|
bpy.app.handlers.load_post.append(load_handler)
|
||||||
bpy.ops.wm.open_mainfile(filepath=metadata_path)
|
bpy.ops.wm.open_mainfile(filepath=metadata_path)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class GenerateUVMap(bpy.types.Operator):
|
||||||
|
bl_idname = "bim.generate_uv_map"
|
||||||
|
bl_label = "Generate UV Map"
|
||||||
|
bl_description = "Generate UV map for selected mesh."
|
||||||
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
obj = context.active_object
|
||||||
|
if not obj or not isinstance(obj.data, bpy.types.Mesh):
|
||||||
|
self.report({"ERROR"}, "No valid mesh selected.")
|
||||||
|
return {"CANCELLED"}
|
||||||
|
tool.Loader.load_generated_uv_map(obj.data)
|
||||||
|
self.report({"INFO"}, "Generated UV map for selected mesh.")
|
||||||
|
return {"FINISHED"}
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ class BIMProjectProperties(PropertyGroup):
|
|||||||
),
|
),
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
should_cache: BoolProperty( # pyright: ignore[reportRedeclaration]
|
should_cache: BoolProperty(
|
||||||
name="Cache",
|
name="Cache",
|
||||||
description=(
|
description=(
|
||||||
"Cache loaded geometry to .h5 file in your cache directory (see in preferences) "
|
"Cache loaded geometry to .h5 file in your cache directory (see in preferences) "
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
@@ -384,6 +385,18 @@ class BIM_PT_new_project_wizard(Panel):
|
|||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.operator("bim.create_project")
|
row.operator("bim.create_project")
|
||||||
|
|
||||||
|
if shutil.which("git"):
|
||||||
|
git_props = context.scene.IfcGitProperties
|
||||||
|
box = self.layout.box()
|
||||||
|
row = box.row()
|
||||||
|
row.label(text="Clone a remote Git repository")
|
||||||
|
row = box.row()
|
||||||
|
row.prop(git_props, "remote_url")
|
||||||
|
row = box.row()
|
||||||
|
row.prop(git_props, "local_folder")
|
||||||
|
row = box.row()
|
||||||
|
row.operator("ifcgit.clone_repo", icon="IMPORT")
|
||||||
|
|
||||||
|
|
||||||
class BIM_PT_project_library(Panel):
|
class BIM_PT_project_library(Panel):
|
||||||
bl_label = "Project Library"
|
bl_label = "Project Library"
|
||||||
|
|||||||
@@ -71,21 +71,26 @@ class ExploreTool(bpy.types.WorkSpaceTool):
|
|||||||
row = layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
row.label(text="", icon="EVENT_SHIFT")
|
row.label(text="", icon="EVENT_SHIFT")
|
||||||
row.label(text="", icon="EVENT_M")
|
row.label(text="", icon="EVENT_M")
|
||||||
row = layout.row(align=True)
|
|
||||||
op = row.operator("bim.explore_hotkey", text="Measure Tool", icon="CON_DISTLIMIT")
|
op = row.operator("bim.explore_hotkey", text="Measure Tool", icon="CON_DISTLIMIT")
|
||||||
op.hotkey = "S_M"
|
op.hotkey = "S_M"
|
||||||
row = layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
row.prop(prop, "measurement_type", text="Measure Type", expand=True, icon_only=True, emboss=True)
|
row.prop(prop, "measurement_type", text="Measure Type", expand=True, icon_only=True, emboss=True)
|
||||||
row = layout.row(align=True)
|
|
||||||
op = row.operator("bim.clear_measurement", text="", icon="X")
|
op = row.operator("bim.clear_measurement", text="", icon="X")
|
||||||
|
|
||||||
row = layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
row.label(text="", icon="EVENT_SHIFT")
|
row.label(text="", icon="EVENT_SHIFT")
|
||||||
row.label(text="", icon="EVENT_S")
|
row.label(text="", icon="EVENT_S")
|
||||||
row = layout.row(align=True)
|
|
||||||
op = row.operator("bim.explore_hotkey", text="Image Scaling Tool", icon="IMAGE_PLANE")
|
op = row.operator("bim.explore_hotkey", text="Image Scaling Tool", icon="IMAGE_PLANE")
|
||||||
op.hotkey = "S_S"
|
op.hotkey = "S_S"
|
||||||
op.description = "Scale Image Annotation. Allows to scale an IfcReferenceImage. Select image, select tool. Check lower left corner instructions to select two points and provide real distance between them"
|
op.description = (
|
||||||
|
"Scale Image Annotation.\n\n"
|
||||||
|
"Allows to scale an IfcReferenceImage.\n\n"
|
||||||
|
"Select image, select tool. "
|
||||||
|
"Check lower left corner instructions to select two points and provide real distance between them"
|
||||||
|
)
|
||||||
|
|
||||||
|
row = layout.row(align=True)
|
||||||
|
row.operator("bim.generate_uv_map", icon="UV")
|
||||||
|
|
||||||
|
|
||||||
class ExploreHotkey(bpy.types.Operator):
|
class ExploreHotkey(bpy.types.Operator):
|
||||||
|
|||||||
@@ -88,6 +88,44 @@ class DisablePsetEditing(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
props.active_pset_type = "-"
|
props.active_pset_type = "-"
|
||||||
|
|
||||||
|
|
||||||
|
def _regenerate_parametric_dimension(file, annotation):
|
||||||
|
"""Regenerate a single parametric dimension annotation after a pset edit."""
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
import numpy as np
|
||||||
|
import ifcopenshell.util.element
|
||||||
|
import ifcopenshell.api.drawing as drawing_api
|
||||||
|
import bonsai.tool as _tool
|
||||||
|
from bonsai.bim.module.drawing.operator import _update_blender_curve
|
||||||
|
|
||||||
|
pset_data = ifcopenshell.util.element.get_pset(annotation, "BBIM_Dimension")
|
||||||
|
if not pset_data or not pset_data.get("Anchors"):
|
||||||
|
return
|
||||||
|
|
||||||
|
anchors = json.loads(pset_data["Anchors"])
|
||||||
|
placement_override = {}
|
||||||
|
for a in anchors:
|
||||||
|
guid = a.get("guid")
|
||||||
|
if not guid:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
elem = file.by_guid(guid)
|
||||||
|
elem_obj = _tool.Ifc.get_object(elem)
|
||||||
|
if elem_obj:
|
||||||
|
placement_override[elem.id()] = np.array(elem_obj.matrix_world)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
resolved_pts = drawing_api.regenerate_dimension(
|
||||||
|
file, annotation, placement_override=placement_override
|
||||||
|
)
|
||||||
|
if resolved_pts:
|
||||||
|
_update_blender_curve(annotation, resolved_pts)
|
||||||
|
except Exception:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
class EditPset(bpy.types.Operator, tool.Ifc.Operator):
|
class EditPset(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
bl_idname = "bim.edit_pset"
|
bl_idname = "bim.edit_pset"
|
||||||
bl_label = "Edit Pset"
|
bl_label = "Edit Pset"
|
||||||
@@ -150,7 +188,12 @@ class EditPset(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
)
|
)
|
||||||
if tool.Cost.has_schedules():
|
if tool.Cost.has_schedules():
|
||||||
tool.Cost.update_cost_items(pset=pset)
|
tool.Cost.update_cost_items(pset=pset)
|
||||||
|
is_bbim_dimension = props.active_pset_name == "BBIM_Dimension" and element.is_a("IfcAnnotation")
|
||||||
|
|
||||||
bpy.ops.bim.disable_pset_editing(obj=self.obj, obj_type=self.obj_type)
|
bpy.ops.bim.disable_pset_editing(obj=self.obj, obj_type=self.obj_type)
|
||||||
|
if is_bbim_dimension:
|
||||||
|
_regenerate_parametric_dimension(self.file, element)
|
||||||
|
|
||||||
tool.Blender.update_viewport()
|
tool.Blender.update_viewport()
|
||||||
|
|
||||||
|
|
||||||
@@ -240,7 +283,7 @@ class CopyPropertyToSelection(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_label = "Copy Property To Selection"
|
bl_label = "Copy Property To Selection"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
name: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
|
name: bpy.props.StringProperty()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
name: str
|
name: str
|
||||||
@@ -280,10 +323,10 @@ class BIM_OT_add_property_to_edit(bpy.types.Operator):
|
|||||||
bl_label = "Add Property to Edit"
|
bl_label = "Add Property to Edit"
|
||||||
bl_idname = "bim.add_property_to_edit"
|
bl_idname = "bim.add_property_to_edit"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
option: bpy.props.EnumProperty(
|
||||||
items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES],
|
items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES],
|
||||||
)
|
)
|
||||||
index: bpy.props.IntProperty(default=-1) # pyright: ignore[reportRedeclaration]
|
index: bpy.props.IntProperty(default=-1)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
option: tool.Pset.BulkOperationType
|
option: tool.Pset.BulkOperationType
|
||||||
@@ -307,9 +350,9 @@ class BIM_OT_remove_property_to_edit(bpy.types.Operator):
|
|||||||
bl_label = "Remove Property from Editing"
|
bl_label = "Remove Property from Editing"
|
||||||
bl_idname = "bim.remove_property_to_edit"
|
bl_idname = "bim.remove_property_to_edit"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
index: bpy.props.IntProperty()
|
||||||
index2: bpy.props.IntProperty(default=-1) # pyright: ignore[reportRedeclaration]
|
index2: bpy.props.IntProperty(default=-1)
|
||||||
option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
option: bpy.props.EnumProperty(
|
||||||
items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES],
|
items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -336,7 +379,7 @@ class BIM_OT_bulk_edit_clear_list(bpy.types.Operator):
|
|||||||
bl_label = "Clear List of Properties"
|
bl_label = "Clear List of Properties"
|
||||||
bl_idname = "bim.pset_bulk_edit_clear_list"
|
bl_idname = "bim.pset_bulk_edit_clear_list"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
option: bpy.props.EnumProperty(
|
||||||
items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES],
|
items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -368,9 +368,9 @@ class GlobalPsetProperties(PropertyGroup):
|
|||||||
qto_filter: StringProperty(name="Qto Filter", options={"TEXTEDIT_UPDATE"})
|
qto_filter: StringProperty(name="Qto Filter", options={"TEXTEDIT_UPDATE"})
|
||||||
|
|
||||||
# Bulk operations.
|
# Bulk operations.
|
||||||
psets_to_delete: CollectionProperty(type=DeletePsetEntry) # pyright: ignore[reportRedeclaration]
|
psets_to_delete: CollectionProperty(type=DeletePsetEntry)
|
||||||
psets_to_rename: CollectionProperty(type=RenamePropertyEntry) # pyright: ignore[reportRedeclaration]
|
psets_to_rename: CollectionProperty(type=RenamePropertyEntry)
|
||||||
psets_to_add_edit: CollectionProperty(type=AddEditPropertyEntry) # pyright: ignore[reportRedeclaration]
|
psets_to_add_edit: CollectionProperty(type=AddEditPropertyEntry)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
pset_filter: str
|
pset_filter: str
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ def get_gross_perimeter(o: bpy.types.Object) -> float:
|
|||||||
return gross_perimeter
|
return gross_perimeter
|
||||||
|
|
||||||
|
|
||||||
def get_space_net_perimeter(obj: bpy.types.Object) -> float:
|
def get_space_net_perimeter(obj: bpy.types.Object) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user