Compare commits

..

8 Commits

Author SHA1 Message Date
Bruno Postle dbe2c541c7 Orientation slot feature now has limited scope
No support for setting up blender orientation slots for multiple
containers or for non-spatial elements.  Setting the default container
switches to an appropriate orientation slot if the orientation of this
container isn't global.
2025-03-22 08:40:20 +00:00
Bruno Postle 7d4a017c15 Don't create orientation slots for all spaces
Trying to create orientation slots for all spaces could potentially
involve a lot of duplicate method calls because blender has no way to
query the list of existing orientation slots, so we can't check to see
if setting a slot is necessary.

So this patch now simply creates/overwrites and uses a single
orientation slot whenever the user switches the default spatial
container, and then only if the spatial orientation is non-world.
2025-03-14 19:17:50 +00:00
Bruno Postle 14f5aa7c4f Merge branch 'v0.8.0' into orientation_slots 2025-02-24 22:07:27 +00:00
Bruno Postle c585c789ac Follow orientation through ContainedInStructure
create_orientation_slots() can be given an element and it will find the
highest matching parent product orientation via elements and/or
spatial elements
2025-02-24 21:53:42 +00:00
Bruno Postle 891752a569 Move blender object manipulation to inner loop
create_orientation_slots() can be called a lot of times recursively, so
only do creation and deletion of dummy objects when absolutely necessary
2025-02-24 19:56:37 +00:00
Bruno Postle 6faef6564e s/element/product/ in create_orientation_slots()
This method should work with anything that has an ObjectPlacement, ie.
an IfcProduct
2025-02-24 19:38:29 +00:00
Bruno Postle 62bd495a9e Create transform orientation slots #6128
Blender supports named UCS coordinate systems. Refreshing the spatial
decomposition panel now creates orientation slots for each unique
spatial element orientation.

Additionally, setting the default spatial container selects an
appropriate orientation slot. eg. if a building is rotated, selecting to
work in a storey will set the orientation to the building orientation.

These slots are only created/updated in the spatial decomposition panel,
maybe they should also be created when loading the file and other
places.

The blender API doesn't currently allow listing custom orientation slots
for deletion, so if a spatial element is deleted or renamed, the old
orientation slot will persist for the rest of the session.
2025-02-23 22:29:42 +00:00
Andrej730 b93cbbf989 black . 2025-02-23 13:29:12 +00:00
714 changed files with 13174 additions and 37976 deletions
+20
View File
@@ -0,0 +1,20 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
Replace this text describing what problem occurred and what you expected to happen instead.
1. To reproduce this, open file '...'
2. Click on '....'
3. See error
**Attachments**
If applicable, add screenshots to help explain your problem. Please also drag-drop any files necessary to show the error (rename the file extension from .ifc to .txt to upload). Private files can be uploaded to https://ifcopenshell.org/upload.html - only viewed by core developers and will be deleted afterwards.
**Debug information**
If this is in Bonsai, paste the output from the Copy Debug Information option in Bonsai. It can be found under Quality and Coordination -> Quality Control -> Debug. If this is a general software issue, if relevant include details about IfcOpenShell version, operating system, Python version, etc.
-23
View File
@@ -1,23 +0,0 @@
name: Bug Report
description: Crashes, error messages, and broken features
type: bug
body:
- type: textarea
attributes:
label: Bug Description
placeholder: |
Describe what problem occurred and what you expected to happen instead.
1. To reproduce this, open file '...'
2. Click on '....'
3. See error
- type: textarea
attributes:
label: Attachments
description: "Private files can be uploaded to https://ifcopenshell.org/upload.html - only viewed by core developers and will be deleted afterwards. Please submit your report first then upload private files afterwards."
placeholder: "If applicable, add screenshots to help explain your problem. Please also drag-drop any files necessary to show the error (rename the file extension from .ifc to .txt to upload)."
- type: textarea
attributes:
label: Debug and Error Output
description: "If this is in Bonsai, paste the output from the Copy Debug Information option in Bonsai. It can be found under Quality and Coordination -> Quality Control -> Debug. If this is a general software issue, if relevant include details about IfcOpenShell version, operating system, Python version, etc."
render: yes
-8
View File
@@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Community Forums
url: https://community.osarch.org/
about: Have a question? Want to discuss an idea? Try the OSArch forums instead.
- name: Live Chat
url: https://osarch.org/chat
about: Have a really confusing issue? Want real-time support?
+10
View File
@@ -0,0 +1,10 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
Replace this text and describe a feature you'd like us to add. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable.
@@ -1,8 +0,0 @@
name: Feature Request
description: Suggest a new feature or improvement
type: Feature
body:
- type: textarea
attributes:
label: Feature Description
placeholder: "Describe a feature you'd like us to add, or a change to the user interface, design, or workflow for usability. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable."
+1 -6
View File
@@ -118,10 +118,5 @@ jobs:
aws-region: us-east-1
- name: Upload .zip Archives to S3
env:
AWS_DEBUG: 1
AWS_RETRY_MODE: standard
AWS_MAX_ATTEMPTS: 3
run: |
dir "$env:USERPROFILE\output"
aws s3 cp "$env:USERPROFILE\output" s3://ifcopenshell-builds/ --recursive --debug
aws s3 cp $env:USERPROFILE\output s3://ifcopenshell-builds/ --recursive
+43 -22
View File
@@ -19,34 +19,55 @@ jobs:
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Install dependencies
- name: Step 1 - install dependencies
shell: bash
run: |
python3 -m pip install --upgrade pip
python3 -m pip install 'black>=24.10.0'
# black doesn't catch all syntax errors, so we check them explicitly.
- name: Check syntax errors
id: syntax-errors
run: |
ERROR=0
python3 -W error -m compileall -q src/ifcopenshell-python || ERROR=1
python3 -W error -m compileall -q src/bonsai || ERROR=1
exit $ERROR
continue-on-error: true
# NOTE: This would suffice, however it is less informative in terms of the 3 possible outcomes
# - name: QA Step - check linting
# shell: bash
# id: linting
# run: |
# python3 -m black .
- name: Black formatter
# QA STEP
- name: QA Step - check linting
shell: bash
id: linting
run: |
python3 -m black --diff --check .
python3 -m black --check . \
&& exit 0 \
|| (echo "exit_code=$?" >> "$GITHUB_OUTPUT" && exit 1);
continue-on-error: true
- name: Final check
run: |
ERROR=0
if [ "${{ steps.syntax-errors.outcome }}" != "success" ]; then
echo "::error::Syntax errors check failed, see 'syntax-errors' step for the details." && ERROR=1
fi
if [ "${{ steps.linting.outcome }}" != "success" ]; then
echo "::error::Black formatting check failed, see 'linting' step for the details." && ERROR=1
fi
exit $ERROR
# OUTCOME 1 of QA STEP
- name: QA Step - no linting errors
if: steps.linting.outcome == 'success'
shell: bash
run: |-
echo "::notice::QA step linting succeeded"
exit 0;
# OUTCOME 2i of QA STEP
- name: QA Step - unprettified code with no syntax errors
if: steps.linting.outputs.exit_code == 1
shell: bash
run: |-
echo "::group::QA step succeeded with warnings"
echo "::warning::one or more files contains unformatted code but no syntax errors";
echo "::notice::please run the linter before pushing!";
echo "::endgroup::"
exit 0;
# OUTCOME 2ii of QA STEP
- name: QA Step - code contains syntax errors
if: steps.linting.outputs.exit_code == 123
shell: bash
run: |-
echo "::group::QA step failed"
echo "::error::one or more files contains syntax errors";
echo "::notice::please run the linter and fix syntax errors before pushing!";
echo "::endgroup::"
exit 1;
+1 -4
View File
@@ -55,8 +55,6 @@ jobs:
}
steps:
- uses: actions/checkout@v2
with:
submodules: recursive
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
@@ -106,7 +104,7 @@ jobs:
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
# Download Blender.
wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.4/blender-4.4.0-linux-x64.tar.xz
wget -q -O blender.tar.xz https://ftp.nluug.nl/pub/graphics/blender/release/Blender4.3/blender-4.3.0-linux-x64.tar.xz
tar -xf blender.tar.xz
# Setup Blender.
@@ -177,7 +175,6 @@ jobs:
cd IfcOpenShell/src/bonsai
pip install pytest-blender
pip install pytest-bdd
blender --background --python scripts/setup_pytest.py
blender --python-expr "import bonsai; print(bonsai.bbim_semver); import ifcopenshell; print(ifcopenshell.version)" --background
make test
@@ -35,11 +35,11 @@ jobs:
- name: Version + date str
id: verdate
run: echo "verdate=${{ steps.version.outputs.version }}alpha${{ steps.date.outputs.date }}" >> $GITHUB_OUTPUT
run: echo "verdate='${{ steps.version.outputs.version }}alpha${{ steps.date.outputs.date }}'" >> $GITHUB_OUTPUT
test:
name: ${{ matrix.platform.distver }}-${{ matrix.pyver.name }}
name: ${{ matrix.platform.distver }}-${{ matrix.pyver.name }}-${{ matrix.variant }}
needs: activate
runs-on: ${{ matrix.platform.distver }}
defaults:
@@ -48,67 +48,57 @@ jobs:
strategy:
fail-fast: false
matrix:
variant: [novtk, all]
pyver: [
# { name: py311, distver: '3.11'},
{ name: py312, distver: '3.12'}
]
platform: [
{ name: win, distver: windows-latest, pkg_dir: 'win-64' },
{ name: linux, distver: ubuntu-latest, pkg_dir: 'linux-64' },
{ name: macOS-arm, distver: macos-latest, pkg_dir: 'osx-arm64' },
{ name: macOS-x86, distver: macos-13, pkg_dir: 'osx-64' }
{ name: macOS, distver: macos-latest, pkg_dir: 'osx-64' }
]
steps:
- name: Set Swap Space
- name: Set up swap space
if: runner.os == 'Linux'
uses: pierotofy/set-swap-space@master
uses: pierotofy/set-swap-space@v1.0
with:
swap-size-gb: 10
- name: set ARTIFACTS ENV vars
shell: bash
run: |
pwd
if [[ "$RUNNER_OS" == "Windows" ]]; then
echo "ARTIFACTS_DIR=D:/a/artifacts" >> $GITHUB_ENV
elif [[ "$RUNNER_OS" == "macOS" ]]; then
echo "ARTIFACTS_DIR=/Users/runner/work/artifacts" >> $GITHUB_ENV
elif [[ "$RUNNER_OS" == "Linux" ]]; then
echo "ARTIFACTS_DIR=/home/runner/work/artifacts" >> $GITHUB_ENV
fi
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Download and extract MacOSX SDK
if: ${{ matrix.platform.name == 'macOS-x86' }}
if: ${{ runner.os == 'macOS' }}
run: |
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@v1 # https://github.com/mamba-org/setup-micromamba
with:
environment-name: test-env
create-args: >-
python=3.12
python=3.11
anaconda-client
rattler-build
boa
- name: create conda package dist dir
run: |
mkdir -p ${{ env.ARTIFACTS_DIR }}
mkdir -p ${{ github.workspace }}/dist
- name: build & test ifcopenshell
run: |
rattler-build build -r conda/recipe.yaml --output-dir '${{ env.ARTIFACTS_DIR }}'
conda mambabuild . --python ${{ matrix.pyver.distver }} --no-remove-work-dir --output-folder '${{ github.workspace }}/dist' --variants='{variant: ${{ matrix.variant }}}'
working-directory: ./conda
env:
VERSION_OVERRIDE: ${{ needs.activate.outputs.verdate }}
- name: upload to anaconda
if: ${{ matrix.platform.name == 'win' }}
run: |
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell '${{ env.ARTIFACTS_DIR }}/${{ matrix.platform.pkg_dir }}/*.conda'
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell '${{ github.workspace }}/dist/${{ matrix.platform.pkg_dir }}/*.tar.bz2'
- name: upload to anaconda
if: ${{ matrix.platform.name != 'win' }}
run: |
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell ${{ env.ARTIFACTS_DIR }}/${{ matrix.platform.pkg_dir }}/*.conda
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell ${{ github.workspace }}/dist/${{ matrix.platform.pkg_dir }}/*.tar.bz2
+6 -6
View File
@@ -9,7 +9,7 @@ on:
jobs:
activate:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell' &&
!startsWith(github.event.head_commit.message, 'Release ') &&
@@ -18,7 +18,7 @@ jobs:
- run: echo ok go
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
needs: activate
steps:
- uses: actions/checkout@v2
@@ -35,7 +35,7 @@ jobs:
-
name: ccache
uses: hendrikmuhs/ccache-action@v1.2
uses: hendrikmuhs/ccache-action@v1
-
name: Build ifcopenshell
@@ -75,7 +75,7 @@ jobs:
make package
working-directory: build
- name: Upload
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
# Artifact name
name: ifcos-artifacts
@@ -83,7 +83,7 @@ jobs:
path: build/assets/Ifc*
deliver:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
needs: build
name: Docker Build, Tag, Push
@@ -118,6 +118,6 @@ jobs:
repository: aecgeeks/ifcopenshell
# Since the dispatch is set to `tag`, `github.ref_name` should evaluate to the pushed tag
# On a workflow dispatch, `ref_name` will take on the value from the dispatch payload
tags: aecgeeks/ifcopenshell:${{ github.ref_name }}${{ github.ref_name == github.event.repository.default_branch && ',aecgeeks/ifcopenshell:22.04' }}
tags: aecgeeks/ifcopenshell:${{ github.ref_name }}${{ github.ref_name == github.event.repository.default_branch && ',aecgeeks/ifcopenshell:latest' }}
file: ./Dockerfile
push: true
@@ -9,8 +9,6 @@ on:
paths:
- 'src/pyodide/**'
- '.github/workflows/publish-pyodide-demo-app.yml'
branches:
- v0.8.0
jobs:
activate:
-1
View File
@@ -4,7 +4,6 @@ on:
push:
tags:
- 'v[0-9].[0-9].[0-9]*'
workflow_dispatch:
jobs:
activate:
+1 -1
View File
@@ -91,7 +91,7 @@ src/bonsai/drawings
src/bonsai/layouts
# ifcopenshell swig and compiled files
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper*.so
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
# apple
+1 -1
View File
@@ -1 +1 @@
0.8.3
0.8.2
-4
View File
@@ -49,7 +49,3 @@ if errorlevel 1 exit 1
ninja install -j 1
if errorlevel 1 exit 1
python %RECIPE_DIR%/update_version_init.py %PKG_VERSION% %SP_DIR%/ifcopenshell/__init__.py
if errorlevel 1 exit 1
+1 -3
View File
@@ -46,6 +46,4 @@ cmake ${CMAKE_ARGS} -G Ninja \
ninja
ninja install -j 1
python "${RECIPE_DIR}/update_version_init.py" "${PKG_VERSION}" "${SP_DIR}/ifcopenshell/__init__.py"
ninja install -j 1
+16 -13
View File
@@ -1,3 +1,7 @@
variant:
- novtk
- all
occt:
- 7.8.1
@@ -20,13 +24,12 @@ cxx_compiler_version:
- '12' # [linux]
- '16' # [osx]
c_stdlib_version:
- 2.17 # [linux]
- 10.13 # [osx and x86_64]
- 11.0 # [osx and arm64]
- '2.12' # [linux]
- '10.13' # [osx]
hdf5:
- 1.14.6
- 1.14.3
libboost_devel:
- '1.86'
- '1.84'
libxml2:
- '2'
mpfr:
@@ -44,12 +47,12 @@ target_platform:
- win-64 # [win]
- linux-64 # [linux]
- osx-64 # [osx]
macos_machine: # [osx]
- x86_64-apple-darwin13.4.0 # [osx and x86_64]
- arm64-apple-darwin20.0.0 # [osx and arm64]
MACOSX_DEPLOYMENT_TARGET: # [osx]
- 11.0 # [osx and arm64]
- 10.13 # [osx and x86_64]
macos_machine:
- x86_64-apple-darwin13.4.0
MACOSX_DEPLOYMENT_TARGET:
- '10.13'
MACOSX_SDK_VERSION:
- '10.13'
CONDA_BUILD_SYSROOT: # [osx]
- "/Users/runner/work/MacOSX10.13.sdk" # [osx and x86_64]
CONDA_BUILD_SYSROOT:
- "/Users/runner/work/MacOSX10.13.sdk" # [osx]
+33 -48
View File
@@ -1,78 +1,64 @@
context:
version: ${{ env.get("VERSION_OVERRIDE", default="0.8.2.1") }}
build: 1
{% set version = environ.get('VERSION_OVERRIDE', '0.8.0') %}
{% set build = 0 %}
# Higher number -> Always prioritize "novtk" variant over "all" variant
{% set build = build + 200 %} # [variant == "novtk"]
{% set build = build + 100 %} # [variant == "all"]
package:
name: ifcopenshell
version: ${{ version }}
version: {{ version }}
source:
- path: ..
path: ../
build:
number: ${{ build }}
dynamic_linking:
binary_relocation: ${{ true if osx }}
number: {{ build }}
skip: true # [py<39]
binary_relocation: false # [osx]
string: py{{ CONDA_PY }}_{{ variant }}_h{{ PKG_HASH }}_{{ build }}
run_exports:
- {{ pin_subpackage('ifcopenshell', max_pin='x.x.x') }} *{{ variant }}*
requirements:
build:
- if: build_platform != target_platform
then:
- python
- cross-python_${{ target_platform }}
- cmake <4
- cmake
- ninja
- swig >=4.1.1
- ${{ stdlib("c") }}
- ${{ compiler('c') }}
- ${{ compiler('cxx') }}
- {{ stdlib("c") }}
- {{ compiler('c') }}
- {{ compiler('cxx') }}
host:
- python
- libboost-devel
- occt
- occt *=*{{ variant }}*
- libxml2
- cgal-cpp
- hdf5
- eigen
- mpfr
- nlohmann_json
- gmp
- gmp # [unix]
- mpir # [win]
- zlib
run:
- python
- shapely
- typing_extensions
- ${{ pin_compatible('occt', upper_bound='x.x.x') }}
- ${{ pin_compatible('cgal-cpp', upper_bound='x.x') }}
run_exports:
- ${{ pin_subpackage('ifcopenshell', upper_bound='x.x.x') }}
- {{ pin_compatible('occt', max_pin='x.x.x') }} *{{ variant }}*
- {{ pin_compatible('cgal-cpp', max_pin='x.x') }}
test:
imports:
- ifcopenshell
tests:
- python:
imports:
- ifcopenshell
pip_check: false
- script:
- python -c "import ifcopenshell; assert ifcopenshell.version == '${{ version }}', 'print(ifcopenshell.version)'"
requirements:
run:
- occt * *novtk* # Ensure that even though compiled against OCCT with VTK, it can still run with OCCT without VTK
- pytest
- python-dateutil
- xmlschema
- xsdata
- lxml
- isodate
- lark
- networkx
- tabulate
- shapely
about:
home: https://ifcopenshell.org
license: LGPL-3.0-or-later
license_file: COPYING
summary: IfcOpenShell is a library to support the IFC file format
summary: 'IfcOpenShell is a library to support the IFC file format'
description: |
IfcOpenShell
============
@@ -307,9 +293,8 @@ about:
[MSYS2]: https://msys2.github.io/ "MSYS2"
[win/readme.md]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/win/readme.md "win/readme.md"
[nix/build-all.py]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/nix/build-all.py "nix/build-all.py"
homepage: https://ifcopenshell.org
repository: https://github.com/IfcOpenShell/IfcOpenShell
documentation: https://ifcopenshell.org/
doc_url: https://ifcopenshell.org/
dev_url: https://github.com/IfcOpenShell/IfcOpenShell
extra:
recipe-maintainers:
-32
View File
@@ -1,32 +0,0 @@
import re
import argparse
from pathlib import Path
def update_version(file_path: str, version: str) -> None:
"""Update the version string in the given __init__.py file."""
file_path = Path(file_path)
# Read the file and replace the version
file_contents = file_path.read_text(encoding="utf-8")
new_contents = re.sub(r'version = "0\.0\.0"', f'version = "{version}"', file_contents)
# Write the updated contents back to the file
file_path.write_text(new_contents, encoding="utf-8")
print(f"Updated version in {file_path} to {version}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Update the version string in a Python file.")
parser.add_argument(
"version",
type=str,
help="The version string to replace '0.0.0' with (e.g., '1.2.3')."
)
parser.add_argument(
"file",
type=str,
help="The path to the __init__.py file where the version will be updated."
)
args = parser.parse_args()
update_version(args.file, args.version)
-3
View File
@@ -35,6 +35,3 @@ extend-exclude = '''
[tool.pyright]
reportInvalidTypeForm = false
disableBytesTypePromotions = true
reportUnnecessaryTypeIgnoreComment = true
# Just to add a quick insert of `# pyright: ignore[xxx]` comments in Pylance.
enableTypeIgnoreComments = false
+1 -1
View File
@@ -20,7 +20,7 @@ def get_extensions_attributes(extensions: Extensions) -> dict[str, AttributeData
- subattribute name"""
possible_attributes = {}
for field in fields(type(extensions)):
field_type = field.type.__args__[0] # pyright: ignore [reportAttributeAccessIssue]
field_type = field.type.__args__[0] # type: ignore [reportAttributeAccessIssue]
subfield = next(iter(fields(field_type)))
xsd_name = subfield.metadata["name"]
possible_attributes[field.name] = AttributeData(field_type, subfield.name, xsd_name)
-3
View File
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
SHELL := sh
PYTHON:=python3.11
PIP:=pip3.11
PATCH:=patch
@@ -47,7 +46,6 @@ VERSION_MINOR:=$(shell cat '../../VERSION' | cut -d '.' -f 2)
VERSION_PATCH:=$(shell cat '../../VERSION' | cut -d '.' -f 3)
VERSION_DATE:=$(shell date '+%y%m%d')
LAST_COMMIT_HASH:=$(shell git rev-parse HEAD)
LAST_COMMIT_DATE:=$(shell git show -s --format=%cI)
PYVERSION:=py310
PYPI_IMP:=cp
@@ -256,7 +254,6 @@ ifeq ($(IS_STABLE), TRUE)
else
$(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/9999999/$(LAST_COMMIT_DATE)/" build/bonsai/__init__.py
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/pyproject.toml
endif
+9 -33
View File
@@ -28,10 +28,6 @@ IN_BLENDER = sys.modules.get("bpy", None)
if IN_BLENDER:
import bpy
# This file is executed twice - first as a bonsai-extension
# and then as a bonsai-package.
IN_PACKAGE = __package__ == "bonsai"
import re
import platform
import traceback
@@ -44,7 +40,6 @@ from typing import Union, Any, Generator
last_commit_hash = "8888888"
last_commit_date = "9999999"
def get_last_commit_hash() -> Union[str, None]:
@@ -56,12 +51,6 @@ def get_last_commit_hash() -> Union[str, None]:
return last_commit_hash[:7]
def get_last_commit_date() -> Union[str, None]:
if last_commit_date == str(9_999999):
return None
return last_commit_date
# Accessed from bonsai extension:
bbim_semver: dict[str, Any] = {}
@@ -107,7 +96,6 @@ def get_debug_info():
"blender_version": bpy.app.version_string,
"bonsai_version": bbim_version,
"bonsai_commit_hash": get_last_commit_hash(),
"bonsai_commit_date": get_last_commit_date(),
"last_actions": last_actions,
"last_error": last_error,
}
@@ -222,21 +210,16 @@ if IN_BLENDER:
info["binary_python_version"] = version
return info
def update_commit_data() -> None:
try:
import git
try:
import git
global last_commit_hash
global last_commit_date
path = Path(__file__).resolve().parent
repo = git.Repo(str(path), search_parent_directories=True)
last_commit_hash = repo.head.object.hexsha
last_commit_date = repo.head.object.committed_datetime.isoformat()
except:
pass
if IN_PACKAGE:
update_commit_data()
# We can't just use __file__ as bonsai/__init__.py is typically not symlinked
# as Blender have errors symlinking main addon package file.
path = Path(__file__).resolve().parent
repo = git.Repo(str(path), search_parent_directories=True)
last_commit_hash = repo.head.object.hexsha
except:
pass
try:
import ifcopenshell.api
@@ -252,12 +235,6 @@ if IN_BLENDER:
ifcopenshell.api.add_pre_listener("*", "action_logger", log_api)
def purge_cache():
"""Purge cache left from previous session (e.g. after reload or update)."""
import bonsai.tool as tool
tool.Blender.get_bonsai_version.cache_clear()
def register():
if platform.system() == "Windows":
clean_up_dlls_safe_links()
@@ -275,7 +252,6 @@ if IN_BLENDER:
bonsai.REINSTALLED_BBIM_VERSION = current_version
bonsai.bim.register()
purge_cache()
def unregister():
if platform.system() == "Windows":
-9
View File
@@ -20,7 +20,6 @@ import os
import bpy
import bpy.utils.previews
import importlib
from bpy_extras.io_utils import ImportHelper, ExportHelper
from . import handler, ui, prop, operator
from typing import Callable, Union
@@ -84,7 +83,6 @@ modules = {
"covering": None,
"web": None,
"light": None,
"alignment": None,
# Uncomment this line to enable loading of the demo module. Happy hacking!
# The name "demo" must correlate to a folder name in `bim/module/`.
# "demo": None,
@@ -101,7 +99,6 @@ classes = [
operator.BIM_OT_delete_object,
operator.BIM_OT_remove_section_plane,
operator.BIM_OT_select_entity,
operator.BIM_OT_select_entity_by_guid,
operator.BIM_OT_select_object,
operator.BIM_OT_show_description,
operator.BIM_OT_multiple_file_selector,
@@ -123,7 +120,6 @@ classes = [
operator.SelectURIAttribute,
operator.SetTab,
operator.SwitchTab,
operator.ShowSystemInfo,
prop.StrProperty,
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
prop.ObjProperty,
@@ -224,12 +220,7 @@ def on_register(scene):
def register():
for cls in classes:
# Prevent crashes in Blender 4.4.0, see #6420.
if issubclass(cls, (ImportHelper, ExportHelper)):
assert getattr(cls, "bl_description", "") or cls.__doc__, cls
bpy.utils.register_class(cls)
bpy.app.handlers.depsgraph_update_post.append(on_register)
bpy.app.handlers.undo_post.append(handler.undo_post)
bpy.app.handlers.redo_post.append(handler.redo_post)
@@ -47,6 +47,8 @@
"scene.render.simplify_gpencil_shader_fx": true,
"scene.render.simplify_gpencil_tint": true,
"scene.render.simplify_gpencil_view_fill": true,
"scene.render.simplify_shadows": 1.0,
"scene.render.simplify_shadows_render": 1.0,
"scene.render.simplify_subdivision": 6,
"scene.render.simplify_subdivision_render": 6,
"scene.render.simplify_volumes": 1.0,
@@ -145,7 +147,7 @@
"scene.display.shading.use_scene_world_render": false,
"scene.display.shading.use_studiolight_view_rotation": true,
"scene.display.shading.use_world_space_lighting": false,
"scene.display.shading.wireframe_color_type": "THEME",
"scene.display.shading.wireframe_color_type": "MATERIAL",
"scene.display.shading.xray_alpha": 0.5,
"scene.display.shading.xray_alpha_wireframe": 0.0,
"scene.display.light_direction": [
@@ -160,6 +162,7 @@
"scene.display.shadow_focus": 0.0,
"scene.display.shadow_shift": 0.10000000149011612,
"scene.display.viewport_aa": "FXAA",
"space.overlay.backwire_opacity": 0.5,
"space.overlay.bone_wire_alpha": 1.0,
"space.overlay.display_handle": "SELECTED",
"space.overlay.fade_inactive_alpha": 0.4000000059604645,
@@ -186,6 +189,7 @@
"space.overlay.show_edge_crease": true,
"space.overlay.show_edge_seams": true,
"space.overlay.show_edge_sharp": true,
"space.overlay.show_edges": false,
"space.overlay.show_extra_edge_angle": false,
"space.overlay.show_extra_edge_length": false,
"space.overlay.show_extra_face_angle": false,
@@ -204,6 +208,7 @@
"space.overlay.show_motion_paths": true,
"space.overlay.show_object_origins": false,
"space.overlay.show_object_origins_all": false,
"space.overlay.show_occlude_wire": false,
"space.overlay.show_onion_skins": false,
"space.overlay.show_ortho_grid": true,
"space.overlay.show_outline_selected": true,
@@ -293,6 +298,8 @@
"scene.render.simplify_gpencil_shader_fx": true,
"scene.render.simplify_gpencil_tint": true,
"scene.render.simplify_gpencil_view_fill": true,
"scene.render.simplify_shadows": 1.0,
"scene.render.simplify_shadows_render": 1.0,
"scene.render.simplify_subdivision": 6,
"scene.render.simplify_subdivision_render": 6,
"scene.render.simplify_volumes": 1.0,
@@ -391,7 +398,7 @@
"scene.display.shading.use_scene_world_render": false,
"scene.display.shading.use_studiolight_view_rotation": true,
"scene.display.shading.use_world_space_lighting": false,
"scene.display.shading.wireframe_color_type": "THEME",
"scene.display.shading.wireframe_color_type": "MATERIAL",
"scene.display.shading.xray_alpha": 0.5,
"scene.display.shading.xray_alpha_wireframe": 0.0,
"scene.display.light_direction": [
@@ -406,6 +413,7 @@
"scene.display.shadow_focus": 0.0,
"scene.display.shadow_shift": 0.10000000149011612,
"scene.display.viewport_aa": "FXAA",
"space.overlay.backwire_opacity": 0.5,
"space.overlay.bone_wire_alpha": 1.0,
"space.overlay.display_handle": "SELECTED",
"space.overlay.fade_inactive_alpha": 0.4000000059604645,
@@ -432,6 +440,7 @@
"space.overlay.show_edge_crease": true,
"space.overlay.show_edge_seams": true,
"space.overlay.show_edge_sharp": true,
"space.overlay.show_edges": false,
"space.overlay.show_extra_edge_angle": false,
"space.overlay.show_extra_edge_length": false,
"space.overlay.show_extra_face_angle": false,
@@ -450,6 +459,7 @@
"space.overlay.show_motion_paths": true,
"space.overlay.show_object_origins": false,
"space.overlay.show_object_origins_all": false,
"space.overlay.show_occlude_wire": false,
"space.overlay.show_onion_skins": false,
"space.overlay.show_ortho_grid": true,
"space.overlay.show_outline_selected": true,
@@ -510,7 +520,7 @@
"scene.render.border_min_x": 0.0,
"scene.render.border_min_y": 0.0,
"scene.render.dither_intensity": 1.0,
"scene.render.engine": "BLENDER_EEVEE_NEXT",
"scene.render.engine": "BLENDER_EEVEE",
"scene.render.film_transparent": false,
"scene.render.filter_size": 1.5,
"scene.render.fps": 24,
@@ -539,6 +549,8 @@
"scene.render.simplify_gpencil_shader_fx": true,
"scene.render.simplify_gpencil_tint": true,
"scene.render.simplify_gpencil_view_fill": true,
"scene.render.simplify_shadows": 1.0,
"scene.render.simplify_shadows_render": 1.0,
"scene.render.simplify_subdivision": 6,
"scene.render.simplify_subdivision_render": 6,
"scene.render.simplify_volumes": 1.0,
@@ -637,7 +649,7 @@
"scene.display.shading.use_scene_world_render": false,
"scene.display.shading.use_studiolight_view_rotation": true,
"scene.display.shading.use_world_space_lighting": false,
"scene.display.shading.wireframe_color_type": "THEME",
"scene.display.shading.wireframe_color_type": "MATERIAL",
"scene.display.shading.xray_alpha": 0.5,
"scene.display.shading.xray_alpha_wireframe": 0.0,
"scene.display.light_direction": [
@@ -652,6 +664,7 @@
"scene.display.shadow_focus": 0.0,
"scene.display.shadow_shift": 0.10000000149011612,
"scene.display.viewport_aa": "FXAA",
"space.overlay.backwire_opacity": 0.5,
"space.overlay.bone_wire_alpha": 1.0,
"space.overlay.display_handle": "SELECTED",
"space.overlay.fade_inactive_alpha": 0.4000000059604645,
@@ -678,6 +691,7 @@
"space.overlay.show_edge_crease": true,
"space.overlay.show_edge_seams": true,
"space.overlay.show_edge_sharp": true,
"space.overlay.show_edges": false,
"space.overlay.show_extra_edge_angle": false,
"space.overlay.show_extra_edge_length": false,
"space.overlay.show_extra_face_angle": false,
@@ -696,6 +710,7 @@
"space.overlay.show_motion_paths": true,
"space.overlay.show_object_origins": true,
"space.overlay.show_object_origins_all": false,
"space.overlay.show_occlude_wire": false,
"space.overlay.show_onion_skins": false,
"space.overlay.show_ortho_grid": true,
"space.overlay.show_outline_selected": true,
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1');
FILE_NAME('/dev/null','2025-03-27T17:30:27+05:00',(''),(''),'IfcOpenShell 0.0.0','IfcOpenShell 0.0.0','Nobody');
FILE_NAME('/dev/null','2024-03-03T16:03:58+11:00',(),(),'IfcOpenShell v0.7.0-eaa2aa05a','IfcOpenShell v0.7.0-eaa2aa05a','Nobody');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
@@ -29,7 +29,7 @@ DATA;
#22=IFCMATERIALLAYERSET((#24),$,$);
#23=IFCRELASSOCIATESMATERIAL('0khP_Smdj12f2JrOTRbRKj',$,$,$,(#21),#22);
#24=IFCMATERIALLAYER(#20,0.05,$,$,$,$,$);
#25=IFCRELDECLARES('31JriXdev6BP2sC9lcxdC1',$,$,$,#2,(#59,#92,#38,#82,#30,#96,#77,#63,#42,#54,#87,#153,#34,#21,#67,#210,#26,#48,#72));
#25=IFCRELDECLARES('31JriXdev6BP2sC9lcxdC1',$,$,$,#2,(#26,#48,#72,#59,#92,#30,#96,#63,#21,#54,#87,#210,#153,#34,#67,#38,#82,#77,#42));
#26=IFCWALLTYPE('3R5onkTtX1IxdCMPoThRaw',$,'WAL100',$,$,$,$,$,$,.NOTDEFINED.);
#27=IFCMATERIALLAYERSET((#29),$,$);
#28=IFCRELASSOCIATESMATERIAL('0$mXRPCT16nP6Q70XFNvZv',$,$,$,(#26),#27);
File diff suppressed because one or more lines are too long
@@ -1,11 +1,11 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION($,'2;1');
FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',$,$,'EPset_Drawing','EPset_Drawing',$);
FILE_DESCRIPTION((),'2;1');
FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',(),(),'EPset_Drawing','EPset_Drawing',$);
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#23,#22,#27,#24,#19,#12,#26,#9,#8,#7,#6,#4,#18,#11,#5,#20,#25,#14,#10,#17,#28,#16,#3,#21,#13,#15,#2));
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22,#23,#24,#25,#26,#27));
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
@@ -14,7 +14,7 @@ DATA;
#7=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#8=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','Comma separated list of selector expressions to evaluate for each drawing elementand add results to their ''class'' attribute.\X2\000A\X0\E.g. ''Name, id'' would add to ''class'' value similar to ''Name-Wall id-1220''.\X2\000A\X0\Then it can be used to applied css styles based on the resulting class.\X2\000A\X0\If attribute is not present on the element, then it won''t be added to it''s ''class''.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#13=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
@@ -32,6 +32,5 @@ DATA;
#25=IFCSIMPLEPROPERTYTEMPLATE('3LHwCrOcb6Y8ozfJZ7Ay$c',$,'LineworkMode','Method to use for line work',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#26=IFCSIMPLEPROPERTYTEMPLATE('2iwERDOW55Pf4hCbuFRe1Q',$,'FillMode','Method to fill areas seen in projection',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#27=IFCSIMPLEPROPERTYTEMPLATE('1YF$qLzBzF19Io8aB2N8cE',$,'CutMode','Method for cutting geometry',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#28=IFCSIMPLEPROPERTYTEMPLATE('1YSnFzurrEyRNtoLdmmddP',$,'BringToFront','The objects with these SVG classes will render in front of all other objects.Ex: IfcBeam, IfcColumn',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
ENDSEC;
END-ISO-10303-21;
@@ -28,13 +28,11 @@ DATA;
#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.);
#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,IfcTypeProduct',(#25,#26,#27,#28));
#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.);
#27=IFCSIMPLEPROPERTYTEMPLATE('2bUmj458PBqPAtUoI3MXsb',$,'TextPrefix','Text to add before annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#28=IFCSIMPLEPROPERTYTEMPLATE('0bnzttUb9BPuN597uNTXOE',$,'TextSuffix','Text to add after annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#29=IFCSIMPLEPROPERTYTEMPLATE('2pJmUDpB50VBdCOib1zcJJ',$,'Newline_At','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#30=IFCSIMPLEPROPERTYTEMPLATE('2TJn72t_v2cvBUG916Dpev',$,'CustomUnit','Dimension''s custom unit',.P_ENUMERATEDVALUE.,'IfcText',$,#31,$,$,$,.READWRITE.);
#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')),$);
ENDSEC;
END-ISO-10303-21;
@@ -1 +0,0 @@
../../libraries/IFC2X3 Demo Library.ifc
@@ -1 +0,0 @@
../../libraries/IFC4 Demo Library.ifc
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
../../libraries/IFC4X3 Demo Library.ifc
+31 -1
View File
@@ -47,6 +47,7 @@ class IfcExporter:
self.set_header()
IfcStore.update_cache()
self.sync_all_objects()
self.sync_edited_objects()
tool.Project.save_linked_models_to_ifc()
extension = self.ifc_export_settings.output_file.split(".")[-1].lower()
if extension == "ifczip":
@@ -100,14 +101,43 @@ class IfcExporter:
result = self.sync_object_placement(obj)
if result:
results.append(result)
result = self.sync_object_material(obj)
# TODO: sync_object_material always returns None
# so it's never really appended
if result:
results.append(result)
except ReferenceError:
pass # The object is likely deleted
return results
def sync_edited_objects(self) -> list[ifcopenshell.entity_instance]:
results: list[ifcopenshell.entity_instance] = []
for obj in IfcStore.edited_objs.copy():
if not obj:
continue
if not tool.Blender.is_valid_data_block(obj):
continue
element = tool.Ifc.get_entity(obj)
if element:
results.append(element)
bpy.ops.bim.update_representation(obj=obj.name)
IfcStore.edited_objs.clear()
return results
def sync_object_material(self, obj: bpy.types.Object) -> None:
if not obj.data or not isinstance(obj.data, bpy.types.Mesh):
return
if not self.has_changed_materials(obj):
return
bpy.ops.bim.update_representation(obj=obj.name)
def has_changed_materials(self, obj: bpy.types.Object) -> bool:
mprops = tool.Geometry.get_mesh_props(obj.data)
checksum = mprops.material_checksum
return checksum != tool.Geometry.get_material_checksum(obj)
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
element = self.file.by_id(tool.Blender.get_object_bim_props(obj).ifc_definition_id)
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
if tool.Geometry.is_scaled(obj):
bpy.ops.bim.update_representation(obj=obj.name)
# update_representation might not apply scale if the object has openings
+15 -14
View File
@@ -24,15 +24,17 @@ import ifcopenshell.util.unit
import ifcopenshell.api.owner.settings
import bonsai.bim
import bonsai.tool as tool
import bonsai.core.owner as core_owner
from bpy.app.handlers import persistent
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.owner.prop import get_user_person, get_user_organisation
from bonsai.bim.module.model.data import AuthoringData
from bonsai.bim.module.aggregate.decorator import AggregateDecorator
from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator
from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator
from bonsai.bim.module.nest.decorator import NestDecorator
from mathutils import Vector
from math import cos
from math import cos, degrees
from typing import Union, Callable
@@ -53,24 +55,23 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
return
if isinstance(obj, bpy.types.Material):
props = tool.Style.get_material_style_props(obj)
props = obj.BIMStyleProperties
if ifc_definition_id := props.ifc_definition_id:
if props.is_renaming:
props.is_renaming = False
props.is_renmaing = False
return
tool.Ifc.get().by_id(ifc_definition_id).Name = obj.name
refresh_ui_data()
return
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
if not obj.BIMObjectProperties.ifc_definition_id:
return
if props.is_renaming:
props.is_renaming = False
if obj.BIMObjectProperties.is_renaming:
obj.BIMObjectProperties.is_renaming = False
return
element = tool.Ifc.get().by_id(props.ifc_definition_id)
element = tool.Ifc.get().by_id(obj.BIMObjectProperties.ifc_definition_id)
if "/" in obj.name:
object_name = obj.name
element_name = obj.name.split("/", 1)[1]
@@ -86,8 +87,8 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
if not element.is_a("IfcRoot"):
return
element.Name = element_name
if props.collection:
props.collection.name = object_name
if obj.BIMObjectProperties.collection:
obj.BIMObjectProperties.collection.name = object_name
refresh_ui_data()
@@ -287,7 +288,7 @@ def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None
def viewport_shading_changed_callback(area: bpy.types.Area) -> None:
shading = area.spaces.active.shading.type
if shading == "RENDERED":
tool.Style.get_style_props().active_style_type = "External"
bpy.context.scene.BIMStylesProperties.active_style_type = "External"
def subscribe_to_viewport_shading_changes():
@@ -347,8 +348,8 @@ def load_post(scene):
# Bonsai overlays
georeference_props = tool.Georeference.get_georeference_props()
aggregate_props = tool.Aggregate.get_aggregate_props()
nest_props = tool.Nest.get_nest_props()
aggregate_props = bpy.context.scene.BIMAggregateProperties
nest_props = bpy.context.scene.BIMNestProperties
model_props = tool.Model.get_model_props()
if georeference_props.should_visualise:
GeoreferenceDecorator.install(bpy.context)
@@ -361,7 +362,7 @@ def load_post(scene):
if model_props.show_slab_direction:
SlabDirectionDecorator.install(bpy.context)
if preferences.should_use_snap and (scene := bpy.context.scene):
if scene := bpy.context.scene:
# Snapping is off by default in Blender, but in BIM, it's more useful to be on
scene.tool_settings.use_snap = True
# Match default Bonsai snaps
+3 -8
View File
@@ -76,8 +76,8 @@ def draw_attribute(
elif value_name == "filepath_value":
attribute.filepath_value.layout_file_select(layout, filter_glob=attribute.filter_glob, text=attribute.name)
elif attribute.name in ("ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"):
props = tool.Sequence.get_work_schedule_props()
for item in props.durations_attributes:
propis = bpy.context.scene.BIMWorkScheduleProperties
for item in propis.durations_attributes:
if item.name == attribute.name:
duration_props = item
layout.label(text=attribute.name)
@@ -293,10 +293,7 @@ def prop_with_search(
should_click_ok: bool = False,
original_operator_path: Optional[str] = None,
**kwargs: Any,
) -> bpy.types.UILayout:
"""
:return: Added row.
"""
):
# kwargs are layout.prop arguments (text, icon, etc.)
row = layout.row(align=True)
row.prop(data, prop_name, **kwargs)
@@ -310,7 +307,6 @@ def prop_with_search(
op.original_operator_path = original_operator_path or ""
except TypeError: # Prop is not iterable
pass
return row
def get_enum_items(
@@ -392,7 +388,6 @@ def draw_filter(
row = layout.row(align=True)
row.label(text=f"{len(data.data['saved_searches'])} Saved Searches")
row.operator("bim.select_entity_by_guid", text="", icon="CON_OBJECTSOLVER")
if data.data["saved_searches"]:
row.operator("bim.load_search", text="", icon="IMPORT").module = module
row.operator("bim.save_search", text="", icon="EXPORT").module = module
+10 -47
View File
@@ -31,10 +31,9 @@ import ifcopenshell.ifcopenshell_wrapper
import bonsai
import bonsai.bim.handler
import bonsai.tool as tool
from ifcopenshell.file import UndoSystemError
from pathlib import Path
from bonsai.tool.brick import BrickStore
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired, Literal
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired, cast
IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
@@ -107,7 +106,10 @@ class IfcStore:
def get_file():
if IfcStore.file is None:
props = tool.Blender.get_bim_props()
IfcStore.set_path(props.ifc_file)
IfcStore.path = props.ifc_file
# Interpret relative paths as relative to .blend file.
if IfcStore.path and not os.path.isabs(IfcStore.path):
IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path))
if IfcStore.path:
try:
IfcStore.load_file(IfcStore.path)
@@ -115,13 +117,6 @@ class IfcStore:
print(f"Failed to load file {IfcStore.path}. Error details: {e}")
return IfcStore.file
@staticmethod
def set_path(value):
IfcStore.path = value
# Interpret relative paths as relative to .blend file.
if IfcStore.path and not os.path.isabs(IfcStore.path):
IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path))
@staticmethod
def get_cache():
if IfcStore.cache is None and IfcStore.path:
@@ -270,11 +265,9 @@ class IfcStore:
IfcStore.guid_map[global_id] = obj
if element.is_a("IfcSurfaceStyle"):
props = tool.Style.get_material_style_props(obj)
props.ifc_definition_id = element.id()
obj.BIMStyleProperties.ifc_definition_id = element.id()
else:
props = tool.Blender.get_object_bim_props(obj)
props.ifc_definition_id = element.id()
obj.BIMObjectProperties.ifc_definition_id = element.id()
tool.Ifc.setup_listeners(obj)
@@ -412,37 +405,9 @@ class IfcStore:
@staticmethod
def purge_blender_ifc_data(obj: IFC_CONNECTED_TYPE) -> None:
if isinstance(obj, bpy.types.Material):
props = tool.Style.get_material_style_props(obj)
props.ifc_definition_id = 0
obj.BIMStyleProperties.ifc_definition_id = 0
else: # bpy.types.Object
props = tool.Blender.get_object_bim_props(obj)
props.ifc_definition_id = 0
@staticmethod
def get_ifc_file_undo_callback(callback_type: Literal["UNDO", "REDO"]):
def callback(_) -> None:
try:
if callback_type == "UNDO":
tool.Ifc.get().undo()
else:
tool.Ifc.get().redo()
except Exception as e:
# Persistent callbacks errors are not visible from UI and we set `last_error`
# to make it visible.
error_msg = ""
# In theory it should always be UndoSystemError, but just to be safe.
if isinstance(e, UndoSystemError):
error_msg += "Undo transaction operations:\n"
transaction = e.transaction
for operation in transaction.operations:
error_msg += f"- {str(operation)}\n"
# Show it in system console too, not just in last message.
print(error_msg)
error_msg += traceback.format_exc()
bonsai.last_error = error_msg
raise
return callback
obj.BIMObjectProperties.ifc_definition_id = 0
@staticmethod
def execute_ifc_operator(
@@ -473,9 +438,7 @@ class IfcStore:
if tool.Ifc.get():
tool.Ifc.get().end_transaction()
IfcStore.add_transaction_operation(
operator,
rollback=IfcStore.get_ifc_file_undo_callback("UNDO"),
commit=IfcStore.get_ifc_file_undo_callback("REDO"),
operator, rollback=lambda d: tool.Ifc.get().undo(), commit=lambda d: tool.Ifc.get().redo()
)
if BrickStore.graph is not None: # `if BrickStore.graph` by itself takes ages.
BrickStore.end_transaction()
+44 -112
View File
@@ -20,7 +20,6 @@ from __future__ import annotations
import bpy
import time
import json
import ifcpatch
import logging
import traceback
import mathutils
@@ -38,8 +37,7 @@ import ifcopenshell.util.shape
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore, IFC_CONNECTED_TYPE
from bonsai.tool.loader import OBJECT_DATA_TYPE
from typing import Dict, Union, Optional, Any, Literal, Iterable
from ifcopenshell.util.shape import MatrixType
from typing import Dict, Union, Optional, Any, Literal
class MaterialCreator:
@@ -87,7 +85,7 @@ class MaterialCreator:
def load_existing_materials(self) -> None:
for material in bpy.data.materials:
if ifc_definition_id := tool.Blender.get_ifc_definition_id(material):
if ifc_definition_id := material.BIMStyleProperties.ifc_definition_id:
self.styles[ifc_definition_id] = material
def parse_element_type_material_styles(self, element: ifcopenshell.entity_instance) -> None:
@@ -198,16 +196,11 @@ class MaterialCreator:
class IfcImporter:
file: ifcopenshell.file
"""Either provided by user as an attribute or will be loaded from ``input_file`` during ``execute()``."""
elements: set[ifcopenshell.entity_instance]
"""Set of IfcElements to import. Excluding ``gross_elements`` and ```native_elements``."""
def __init__(self, ifc_import_settings: IfcImportSettings):
self.ifc_import_settings = ifc_import_settings
tool.Loader.set_settings(ifc_import_settings)
self.diff = None
self.file: ifcopenshell.file = None
self.project = None
self.has_existing_project = False
# element guids to blender collections mapping
@@ -305,35 +298,21 @@ class IfcImporter:
bpy.context.window_manager.progress_end()
def process_context_filter(self) -> None:
"""Setup contexts. Necessary for importing elements representations."""
contexts = self.file.by_type("IfcGeometricRepresentationContext")
if len(contexts) > 100: # Probably something strange happening. Encountered from Revizto.
print("Warning! Excessive contexts were found and merged where applicable.")
uniques = {}
i = 0
for element in contexts:
data = "-".join([str(a) for a in element])
if unique := uniques.get(data, None):
ifcopenshell.util.element.replace_element(element, unique)
self.file.remove(element)
i += 1
else:
uniques[data] = element
print(f"Replaced {i} IfcGeometricRepresentationContext")
tool.Loader.settings.contexts = ifcopenshell.util.representation.get_prioritised_contexts(self.file)
tool.Loader.settings.context_settings = tool.Loader.create_settings()
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
def process_element_filter(self) -> None:
elements: list[ifcopenshell.entity_instance]
if self.ifc_import_settings.has_filter:
elements = list(self.ifc_import_settings.elements)
self.elements = self.ifc_import_settings.elements
if isinstance(self.elements, set):
self.elements = list(self.elements)
# TODO: enable filtering for annotations
else:
if self.file.schema in ("IFC2X3", "IFC4"):
elements = self.file.by_type("IfcElement") + self.file.by_type("IfcProxy")
self.elements = self.file.by_type("IfcElement") + self.file.by_type("IfcProxy")
else:
elements = self.file.by_type("IfcElement")
self.elements = self.file.by_type("IfcElement")
drawing_groups = [g for g in self.file.by_type("IfcGroup") if g.ObjectType == "DRAWING"]
drawing_annotations = set()
@@ -343,16 +322,16 @@ class IfcImporter:
self.annotations = set([a for a in self.file.by_type("IfcAnnotation")])
self.annotations -= drawing_annotations
elements = [e for e in elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")]
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")]
if self.ifc_import_settings.element_limit_mode == "UNLIMITED":
self.elements = set(elements)
self.elements = set(self.elements)
else:
offset = self.ifc_import_settings.element_offset
offset_limit = offset + self.ifc_import_settings.element_limit
self.elements = set(elements[offset:offset_limit])
self.elements = set(self.elements[offset:offset_limit])
if self.ifc_import_settings.has_filter or self.ifc_import_settings.element_limit_mode != "UNLIMITED":
self.element_types = {t for e in self.elements if (t := ifcopenshell.util.element.get_type(e))}
self.element_types = set([ifcopenshell.util.element.get_type(e) for e in self.elements])
else:
self.element_types = set(self.file.by_type("IfcTypeProduct"))
@@ -390,7 +369,6 @@ class IfcImporter:
return results
def parse_native_elements(self) -> None:
# TODO: move to `process_element_filter` to incapsulate all `self.elements` logic.
if not self.ifc_import_settings.should_load_geometry:
return
if not self.file.by_type("IfcSweptDiskSolid"):
@@ -428,8 +406,7 @@ class IfcImporter:
rep = representation
while True:
# Tekla 2023 has missing items, though it's invalid IFC.
if rep.Items and len(rep.Items) == 1 and rep.Items[0].is_a("IfcMappedItem"):
if len(rep.Items) == 1 and rep.Items[0].is_a("IfcMappedItem"):
rep_matrix = ifcopenshell.util.placement.get_mappeditem_transformation(rep.Items[0])
if not np.allclose(rep_matrix, np.eye(4)):
matrix = rep_matrix @ matrix
@@ -502,16 +479,14 @@ class IfcImporter:
if grid.WAxes:
self.create_grid_axes(grid.WAxes, grid_obj, grid_placement)
def create_grid_axes(
self, axes: list[ifcopenshell.entity_instance], grid_obj: bpy.types.Object, grid_placement: MatrixType
) -> None:
def create_grid_axes(self, axes, grid_obj, grid_placement):
for axis in axes:
shape = tool.Loader.create_generic_shape(axis.AxisCurve)
mesh = self.create_mesh(axis, shape)
obj = bpy.data.objects.new(tool.Loader.get_name(axis), mesh)
obj.show_in_front = True
self.link_element(axis, obj)
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, grid_placement))
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, grid_placement.copy()))
def create_element_types(self):
for element_type in self.element_types:
@@ -536,9 +511,8 @@ class IfcImporter:
pass
elif shape:
mesh = self.create_mesh(element, shape)
if mesh is not None:
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
else:
self.ifc_import_settings.logger.error("Failed to generate shape for %s", element)
break
@@ -596,7 +570,6 @@ class IfcImporter:
return self.create_generic_sqlite_elements(elements)
if self.ifc_import_settings.should_load_geometry:
elements = elements.copy() # Prevent argument mutation.
context_settings = (
tool.Loader.settings.gross_context_settings if is_gross else tool.Loader.settings.context_settings
)
@@ -725,15 +698,9 @@ class IfcImporter:
def create_structural_point_connections(self):
for product in self.file.by_type("IfcStructuralPointConnection"):
# TODO: make this based off ifcopenshell. See #1409
representation = tool.Structural.get_vertex_representation(product)
if not representation:
print(
"WARNING. Skipping invalid IfcStructuralPointConnection - "
f"element has no valid representation:\n{product}."
)
continue
representation: ifcopenshell.entity_instance = next(
rep for rep in product.Representation.Representations if rep.RepresentationType == "Vertex"
)
mesh = tool.Loader.create_structural_point_connection_mesh(representation)
if mesh is None:
continue
@@ -775,9 +742,12 @@ class IfcImporter:
def create_pointclouds(self, products: set[ifcopenshell.entity_instance]) -> set[ifcopenshell.entity_instance]:
result = set()
for product in products:
if representation := self.get_pointcloud_representation(product):
if pointcloud := self.create_pointcloud(product, representation):
representation = self.get_pointcloud_representation(product)
if representation is not None:
pointcloud = self.create_pointcloud(product, representation)
if pointcloud is not None:
result.add(pointcloud)
return result
def create_pointcloud(
@@ -825,9 +795,8 @@ class IfcImporter:
materials_updated = bool(mesh)
if mesh is None:
mesh = self.create_mesh(element, shape)
if mesh is not None:
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
else:
mesh = None
@@ -838,10 +807,11 @@ class IfcImporter:
if element.is_a(ifcclass):
obj.display_type = "WIRE"
if shape and mesh:
if shape:
# We use numpy here because Blender mathutils.Matrix is not accurate enough
mat = ifcopenshell.util.shape.get_shape_matrix(shape)
mat = np.array(shape.transformation.matrix).reshape((4, 4), order="F")
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, mat))
assert mesh # Type checker.
if not materials_updated:
self.material_creator.create(element, obj, mesh, tool.Geometry.does_shape_has_openings(shape))
elif mesh: # When does this occur?
@@ -963,8 +933,7 @@ class IfcImporter:
self.project = {"ifc": project}
obj = tool.Ifc.get_object(project)
if obj:
props = tool.Blender.get_object_bim_props(obj)
self.project["blender"] = props.collection
self.project["blender"] = obj.BIMObjectProperties.collection
self.has_existing_project = True
return
self.project["blender"] = bpy.data.collections.new(
@@ -974,16 +943,10 @@ class IfcImporter:
obj.hide_select = True
self.project["blender"].objects.link(obj)
self.project["blender"].BIMCollectionProperties.obj = obj
props = tool.Blender.get_object_bim_props(obj)
props.collection = self.collections[project.GlobalId] = self.project["blender"]
obj.BIMObjectProperties.collection = self.collections[project.GlobalId] = self.project["blender"]
def create_styles(self) -> None:
styles = self.file.by_type("IfcSurfaceStyle")
if len(styles) > self.ifc_import_settings.style_limit: # Probably something strange happening
print("Warning! Excessive styles were found and merged where applicable.")
ifcpatch.execute({"file": self.file, "recipe": "MergeStyles", "arguments": []})
styles = self.file.by_type("IfcSurfaceStyle")
for style in styles:
for style in self.file.by_type("IfcSurfaceStyle"):
self.create_style(style)
def create_style(self, style: ifcopenshell.entity_instance) -> None:
@@ -996,11 +959,10 @@ class IfcImporter:
self.material_creator.styles[style.id()] = blender_material
style_elements = tool.Style.get_style_elements(blender_material)
props = tool.Style.get_material_style_props(blender_material)
if tool.Style.has_blender_external_style(style_elements):
props.active_style_type = "External"
blender_material.BIMStyleProperties.active_style_type = "External"
else:
props.active_style_type = "Shading"
blender_material.BIMStyleProperties.active_style_type = "Shading"
def place_objects_in_collections(self) -> None:
for ifc_definition_id, obj in self.added_data.items():
@@ -1065,7 +1027,7 @@ class IfcImporter:
element: ifcopenshell.entity_instance,
shape: Union[ifcopenshell.geom.ShapeElementType, ifcopenshell.geom.ShapeType],
cartesian_point_offset: Union[npt.NDArray[np.float64], Literal[False]] = None,
) -> Union[bpy.types.Mesh, None]:
) -> bpy.types.Mesh:
try:
if hasattr(shape, "geometry"):
# shape is ShapeElementType
@@ -1106,10 +1068,7 @@ class IfcImporter:
verts=verts,
load_indexed_maps=self.ifc_import_settings.load_indexed_maps,
)
# E.g. `create_mesh` is also used for IfcRelSpaceBoundary.
if element.is_a("IfcObjectDefinition"):
return tool.Loader.slice_layerset_mesh(element, mesh)
return mesh
return tool.Loader.slice_layerset_mesh(element, mesh)
except:
self.ifc_import_settings.logger.error("Could not create mesh for %s", element)
import traceback
@@ -1138,22 +1097,7 @@ class IfcImporter:
bpy.ops.view3d.view_selected()
bpy.ops.object.select_all(action="DESELECT")
def setup_arrays(
self,
*,
annotations_to_import: Optional[set[ifcopenshell.entity_instance]] = None,
openings_to_import: Optional[set[ifcopenshell.entity_instance]] = None,
) -> None:
# During base import some elements are skipped.
is_base_import = True
elements_to_import: set[ifcopenshell.entity_instance] = set()
if annotations_to_import is not None:
is_base_import = False
elements_to_import = annotations_to_import
elif openings_to_import is not None:
is_base_import = False
elements_to_import = openings_to_import
def setup_arrays(self):
for pset in self.file.by_type("IfcPropertySet"):
if pset.Name != "BBIM_Array":
continue
@@ -1161,24 +1105,16 @@ class IfcImporter:
continue
data = json.loads(data)
for rel in pset.DefinesOccurrence:
element: ifcopenshell.entity_instance
for element in rel.RelatedObjects:
if is_base_import:
if element.is_a("IfcAnnotation") or element.is_a("IfcOpeningElement"):
continue
else:
if element not in elements_to_import:
continue
for i in range(len(data)):
tool.Blender.Modifier.Array.set_children_lock_state(element, i, True)
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
class IfcImportSettings:
input_file: Union[str, None] = None
logger: Union[logging.Logger, None] = None
def __init__(self):
self.logger: logging.Logger = None
self.input_file = None
self.diff_file = None
self.geometry_library = "opencascade"
self.should_use_cpu_multiprocessing = True
@@ -1189,7 +1125,6 @@ class IfcImportSettings:
self.deflection_tolerance = 0.001
self.angular_tolerance = 0.5
self.void_limit = 30
self.style_limit = 300
# Locations greater than 1km are not considered "small sites" according to the georeferencing guide
# Users can configure this if they have to handle larger sites but beware of surveying precision
self.distance_limit = 1000
@@ -1199,20 +1134,18 @@ class IfcImportSettings:
self.element_limit_mode = "UNLIMITED"
self.element_offset = 0
self.element_limit = 30000
self.has_filter = False
self.has_filter = None
self.should_filter_spatial_elements = True
self.should_setup_viewport_camera = True
self.contexts: list[ifcopenshell.entity_instance] = []
self.context_settings: list[ifcopenshell.geom.main.settings] = []
self.gross_context_settings: list[ifcopenshell.geom.main.settings] = []
self.elements: Iterable[ifcopenshell.entity_instance] = set()
self.elements: set[ifcopenshell.entity_instance] = set()
self.load_indexed_maps = False
@staticmethod
def factory(
context=None, input_file: Optional[str] = None, logger: Optional[logging.Logger] = None
) -> IfcImportSettings:
scene_diff = tool.Blender.get_diff_props()
def factory(context=None, input_file=None, logger=None):
scene_diff = bpy.context.scene.DiffProperties
props = tool.Project.get_project_props()
settings = IfcImportSettings()
settings.input_file = input_file
@@ -1229,7 +1162,6 @@ class IfcImportSettings:
settings.deflection_tolerance = props.deflection_tolerance
settings.angular_tolerance = props.angular_tolerance
settings.void_limit = props.void_limit
settings.style_limit = props.style_limit
settings.distance_limit = props.distance_limit
settings.false_origin_mode = props.false_origin_mode
try:
@@ -20,7 +20,6 @@ import blf
import bpy
import gpu
import ifcopenshell
import ifcopenshell.util.element
import bonsai.tool as tool
from bpy.types import SpaceView3D
from bpy_extras import view3d_utils
@@ -132,9 +131,6 @@ class AggregateDecorator:
return shader
def draw_custom_batch(self, coords, color):
indices = None
if not tool.Blender.validate_shader_batch_data(coords, indices):
return
shader = self.dotted_line_shader()
arc_lengths = [0]
@@ -145,7 +141,6 @@ class AggregateDecorator:
shader,
"LINE_STRIP",
{"position": coords, "arcLength": arc_lengths},
indices=indices,
)
matrix = bpy.context.region_data.perspective_matrix
@@ -155,15 +150,14 @@ class AggregateDecorator:
batch.draw(shader)
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def draw_aggregate(self, context):
props = tool.Aggregate.get_aggregate_props()
if context.scene.BIMAggregateProperties.in_aggregate_mode:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
decorator_color_special = self.addon_prefs.decorator_color_special
decorator_color_selected = self.addon_prefs.decorator_color_selected
@@ -192,13 +186,7 @@ class AggregateDecorator:
aggregates.append(obj)
continue
aggregates_list = tool.Aggregate.get_aggregates_recursively(element)
if props.in_aggregate_mode and props.editing_aggregate:
index = aggregates_list.index(tool.Ifc.get_entity(props.editing_aggregate))
if index > 0:
aggregate = aggregates_list[index - 1]
else:
aggregate = aggregates_list[-1]
aggregate = ifcopenshell.util.element.get_aggregate(element)
if aggregate:
aggregates.append(tool.Ifc.get_object(aggregate))
@@ -216,7 +204,9 @@ class AggregateDecorator:
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
parts = tool.Aggregate.get_parts_recursively(tool.Ifc.get_entity(aggregate))
if context.scene.BIMAggregateProperties.in_aggregate_mode:
return
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate))
parts_objs = [tool.Ifc.get_object(p) for p in parts]
indices, edges = create_bounding_box(parts_objs)
@@ -253,8 +243,6 @@ class AggregateModeDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
@@ -265,7 +253,7 @@ class AggregateModeDecorator:
return
region = context.region
rv3d = region.data
props = tool.Aggregate.get_aggregate_props()
props = context.scene.BIMAggregateProperties
aggregate_obj = props.editing_aggregate
if not aggregate_obj:
@@ -296,7 +284,7 @@ class AggregateModeDecorator:
def draw_aggregate_empty(self, context):
if context.mode == "EDIT_MESH":
return
props = tool.Aggregate.get_aggregate_props()
props = context.scene.BIMAggregateProperties
aggregate_obj = props.editing_aggregate
if not aggregate_obj:
return
@@ -335,7 +323,7 @@ class AggregateModeDecorator:
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
parts = tool.Aggregate.get_parts_recursively(tool.Ifc.get_entity(aggregate_obj))
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate_obj))
color = self.addon_prefs.decorator_color_selected
parts_objs = [tool.Ifc.get_object(p) for p in parts]
@@ -23,7 +23,6 @@ import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.core.aggregate as core
import bonsai.core.spatial
from typing import TYPE_CHECKING
class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
@@ -31,30 +30,23 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Assign Object To Aggregation"
bl_description = (
"Assign object as an aggregate to the selected IFC elements.\n\n"
"If called from Object Aggregates UI, then either 'Relating Whole' or 'Related Part' must be provided.\n"
"If called from Bonsai UI, then either 'Relating Whole' or 'Related Part' must be provided.\n"
"If called directly, active object will be considered a relating whole"
)
bl_options = {"REGISTER", "UNDO"}
relating_object: bpy.props.IntProperty()
related_object: bpy.props.IntProperty()
if TYPE_CHECKING:
relating_object: int
related_object: int
def _execute(self, context):
relating_obj = None
if self.relating_object:
relating_obj = tool.Ifc.get_object(tool.Ifc.get().by_id(self.relating_object))
assert relating_obj
elif self.related_object:
aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get().by_id(self.related_object))
if aggregate:
relating_obj = tool.Ifc.get_object(aggregate)
assert relating_obj
else:
elif context.active_object:
relating_obj = context.active_object
if not relating_obj:
self.report({"ERROR"}, "No relating object is provided.")
return
@@ -74,7 +66,7 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
relating_obj=relating_obj,
related_obj=obj,
)
props = tool.Aggregate.get_aggregate_props()
props = context.scene.BIMAggregateProperties
if relating_obj == props.editing_aggregate and props.in_aggregate_mode:
new_editing_obj = props.editing_objects.add()
new_editing_obj.obj = obj
@@ -140,11 +132,12 @@ class BIM_OT_disable_editing_aggregate(bpy.types.Operator):
class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
"""Add aggregate to selected IFC elements."""
"""Add aggregate to IFC element"""
bl_idname = "bim.add_aggregate"
bl_label = "Add Aggregate"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
ifc_class: bpy.props.StringProperty(name="IFC Class", default="IfcElementAssembly")
aggregate_name: bpy.props.StringProperty(name="Name", default="Default_Name")
@@ -164,7 +157,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
return
aggregate = self.create_aggregate(context, ifc_class, self.aggregate_name)
for obj in tool.Blender.get_selected_objects():
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
@@ -189,7 +182,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
)
core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj)
def create_aggregate(self, context: bpy.types.Context, ifc_class: str, aggregate_name: str) -> bpy.types.Object:
def create_aggregate(self, context, ifc_class, aggregate_name):
aggregate = bpy.data.objects.new(aggregate_name, None)
aggregate.location = context.scene.cursor.location
bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class=ifc_class)
@@ -385,7 +378,7 @@ class BIM_OT_disable_aggregate_mode(bpy.types.Operator):
def execute(self, context):
bpy.ops.object.select_all(action="DESELECT")
bonsai.core.aggregate.exit_aggregate_mode(tool.Aggregate)
bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate)
return {"FINISHED"}
@@ -395,7 +388,7 @@ class BIM_OT_toggle_aggregate_mode_local_view(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = tool.Aggregate.get_aggregate_props()
props = context.scene.BIMAggregateProperties
objs = [o.obj for o in props.editing_objects]
if props.in_aggregate_mode:
if context.space_data.local_view:
@@ -33,7 +33,6 @@ from bpy.props import (
CollectionProperty,
)
from bonsai.bim.module.aggregate.decorator import AggregateDecorator, AggregateModeDecorator
from typing import TYPE_CHECKING, Union
def can_aggregate(relating_obj: bpy.types.Object, related_obj: bpy.types.Object) -> bool:
@@ -99,27 +98,15 @@ class BIMObjectAggregateProperties(PropertyGroup):
poll=poll_related_object,
)
if TYPE_CHECKING:
is_editing: bool
relating_object: Union[bpy.types.Object, None]
related_object: Union[bpy.types.Object, None]
class Objects(bpy.types.PropertyGroup):
obj: PointerProperty(type=bpy.types.Object)
previous_display_type: bpy.props.StringProperty(default="TEXTURED")
previous_hide_select: bpy.props.BoolProperty(default=False)
if TYPE_CHECKING:
obj: Union[bpy.types.Object, None]
previous_display_type: str
previous_hide_select: bool
class BIMAggregateProperties(PropertyGroup):
in_aggregate_mode: BoolProperty(name="In Edit Mode", update=update_aggregate_mode_decorator)
editing_aggregate: PointerProperty(name="Editing Aggregate", type=bpy.types.Object)
previous_editing_aggregate: PointerProperty(name="Editing Aggregate", type=bpy.types.Object)
editing_objects: CollectionProperty(type=Objects)
not_editing_objects: CollectionProperty(type=Objects)
aggregate_decorator: BoolProperty(
@@ -127,16 +114,3 @@ class BIMAggregateProperties(PropertyGroup):
default=False,
update=update_aggregate_decorator,
)
previous_state: BoolProperty(
name="True if it was previously in aggregate mode",
default=False,
)
if TYPE_CHECKING:
in_aggregate_mode: bool
editing_aggregate: Union[bpy.types.Object, None]
previous_editing_aggregate: Union[bpy.types.Object, None]
editing_objects: bpy.types.bpy_prop_collection_idprop[Objects]
not_editing_objects: bpy.types.bpy_prop_collection_idprop[Objects]
aggregate_decorator: bool
previous_state: bool
+13 -13
View File
@@ -34,9 +34,9 @@ class BIM_PT_aggregate(Panel):
@classmethod
def poll(cls, context):
if not (obj := context.active_object):
if not context.active_object:
return False
props = tool.Blender.get_object_bim_props(obj)
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
@@ -49,14 +49,11 @@ class BIM_PT_aggregate(Panel):
layout = self.layout
row = layout.row()
row.label(text="Aggregate Decorator")
props = tool.Aggregate.get_aggregate_props()
icon = "HIDE_OFF" if props.aggregate_decorator else "HIDE_ON"
row.prop(props, "aggregate_decorator", icon=icon, text="")
row.prop(context.scene.BIMAggregateProperties, "aggregate_decorator", icon="HIDE_OFF", text="")
if not AggregateData.is_loaded:
AggregateData.load()
assert (obj := context.active_object)
props = tool.Aggregate.get_object_aggregate_props(obj)
props = context.active_object.BIMObjectAggregateProperties
if props.is_editing:
row = layout.row()
@@ -69,9 +66,9 @@ class BIM_PT_aggregate(Panel):
col.enabled = False
op = col.operator("bim.aggregate_assign_object", icon="CHECKMARK")
if props.relating_object:
op.relating_object = tool.Blender.get_object_bim_props(props.relating_object).ifc_definition_id
op.relating_object = props.relating_object.BIMObjectProperties.ifc_definition_id
elif props.related_object:
op.related_object = tool.Blender.get_object_bim_props(props.related_object).ifc_definition_id
op.related_object = props.related_object.BIMObjectProperties.ifc_definition_id
row.operator("bim.disable_editing_aggregate", icon="CANCEL", text="")
return
else:
@@ -79,8 +76,10 @@ class BIM_PT_aggregate(Panel):
if AggregateData.data["has_relating_object"]:
row.label(text=AggregateData.data["relating_object_label"], icon="TRIA_UP")
op = row.operator("bim.select_aggregate", icon="OBJECT_DATA", text="")
op.obj = context.active_object.name
op.select_parts = False
op = row.operator("bim.select_aggregate", icon="RESTRICT_SELECT_OFF", text="")
op.obj = context.active_object.name
op.select_parts = True
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
row.operator("bim.add_aggregate", icon="ADD", text="")
@@ -116,9 +115,9 @@ class BIM_PT_linked_aggregate(Panel):
@classmethod
def poll(cls, context):
if not (obj := context.active_object):
if not context.active_object:
return False
props = tool.Blender.get_object_bim_props(obj)
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
@@ -132,8 +131,9 @@ class BIM_PT_linked_aggregate(Panel):
if not AggregateData.is_loaded:
AggregateData.load()
assert (obj := context.active_object)
assert (element := tool.Ifc.get_entity(obj))
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMObjectAggregateProperties
row = layout.row(align=True)
if element.Decomposes:
@@ -1,36 +0,0 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
# from . import ui, prop, operator
from . import operator
classes = (operator.ImportAlignmentCSV,)
def menu_func_import(self, context):
self.layout.operator(operator.ImportAlignmentCSV.bl_idname, text="Alignment (.csv)")
def register():
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
def unregister():
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
@@ -1,114 +0,0 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>, 2022 Yassine Oualid <yassine@sigmadimensions.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
# pyright: reportUnnecessaryTypeIgnoreComment=error
import os
import ifcopenshell.api.alignment
import ifcopenshell.api.alignment.add_stationing_to_alignment
import bpy
import json
import time
import calendar
import isodate
import bonsai.core.sequence as core
import bonsai.tool as tool
import bonsai.bim.module.sequence.helper as helper
import ifcopenshell.api.spatial
import ifcopenshell.geom
import ifcopenshell.util.sequence
import ifcopenshell.util.selector
from datetime import datetime
from dateutil import parser, relativedelta
from bpy_extras.io_utils import ImportHelper
from typing import get_args, TYPE_CHECKING
from typing_extensions import assert_never
class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.import_alignment_csv"
bl_label = "Import Alignment CSV"
bl_description = " Import alignment from the provided .csv file."
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
@classmethod
def poll(cls, context):
ifc_file = tool.Ifc.get()
if ifc_file is None:
cls.poll_message_set("No IFC file is loaded.")
return False
elif ifc_file.schema != "IFC4X3":
cls.poll_message_set("Schema must be IFC4x3.")
return False
return True
def _execute(self, context):
self.file = tool.Ifc.get()
start = time.time()
alignment = ifcopenshell.api.alignment.create_alignment_from_csv(self.file, self.filepath)
ifcopenshell.api.alignment.create_geometric_representation(self.file, alignment)
ifcopenshell.api.alignment.add_stationing_to_alignment(self.file, alignment=alignment, start_station=0.0)
# IFC 4.1.5.1 alignments cannot be contained in spatial structures, but can be referenced into them
sites = self.file.by_type("IfcSite")
for site in sites:
ifcopenshell.api.spatial.reference_structure(self.file, products=[alignment], relating_structure=site)
# process the generated IfcReferent for the alignment
for rel in alignment.IsNestedBy:
for referent in rel.RelatedObjects:
if referent.is_a("IfcReferent"):
referent_obj = bpy.data.objects.new(tool.Loader.get_name(referent), None)
tool.Geometry.link(referent, referent_obj)
tool.Collector.assign(referent_obj, should_clean_users_collection=False)
# an alignment can be an aggregation of multiple child alignments (ie. multiple verticals for a single horizontal)
# get all the alignment curves
curves = []
for rel in alignment.IsDecomposedBy:
for agg in rel.RelatedObjects:
if agg.is_a("IfcAlignment"):
curves.append(ifcopenshell.api.alignment.get_curve(agg)) # 3D curve
# if there aren't any curves from aggregation, then there is only a single vertical or no vertical
if len(curves) == 0:
curves.append(ifcopenshell.api.alignment.get_curve(alignment))
settings = ifcopenshell.geom.settings()
for curve in curves:
shape = ifcopenshell.geom.create_shape(settings, curve)
# create a new Blender mesh
mesh_name = tool.Loader.get_mesh_name_from_shape(shape)
mesh = bpy.data.meshes.new(mesh_name)
m = tool.Loader.convert_geometry_to_mesh(shape, mesh)
# create a new Blender object
alignment_obj = bpy.data.objects.new(tool.Loader.get_name(alignment), m)
# link the blender object to with the alignment element
tool.Geometry.link(alignment, alignment_obj)
# assign the object to the blender collections
tool.Collector.assign(alignment_obj, should_clean_users_collection=False)
self.report({"INFO"}, "Imported in %s seconds" % (time.time() - start))
@@ -26,42 +26,30 @@ import bonsai.bim.helper
import bonsai.tool as tool
import bonsai.core.attribute as core
import bonsai.core.spatial
from typing import TYPE_CHECKING
def get_objs_for_operation(
operator_properties: "AttributesOperator", context: bpy.types.Context
) -> list[bpy.types.Object]:
def get_objs_for_operation(operator_properties, context):
if operator_properties.obj:
return [bpy.data.objects[operator_properties.obj]]
if operator_properties.mass_operation:
return context.selected_objects[:]
obj = context.active_object
assert obj
return [obj]
return [context.active_object]
class AttributesOperator:
class EnableEditingAttributes(bpy.types.Operator):
bl_idname = "bim.enable_editing_attributes"
bl_label = "Enable Editing Attributes"
bl_description = "ALT + Left Click to enable editing attributes on all selected objects"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty(options={"SKIP_SAVE"})
mass_operation: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
if TYPE_CHECKING:
obj: str
mass_operation: bool
def invoke(self, context, event):
self.mass_operation = event.alt
return self.execute(context)
class EnableEditingAttributes(bpy.types.Operator, AttributesOperator):
bl_idname = "bim.enable_editing_attributes"
bl_label = "Enable Editing Attributes"
bl_description = "ALT + Left Click to enable editing attributes on all selected objects"
bl_options = {"REGISTER", "UNDO"}
def enable_editing_attribute_on_obj(self, obj: bpy.types.Object) -> None:
props = tool.Blender.get_object_attribute_props(obj)
def enable_editing_attribute_on_obj(self, obj):
props = obj.BIMAttributeProperties
props.attributes.clear()
element = tool.Ifc.get_entity(obj)
@@ -99,14 +87,20 @@ class EnableEditingAttributes(bpy.types.Operator, AttributesOperator):
return {"FINISHED"}
class DisableEditingAttributes(bpy.types.Operator, AttributesOperator):
class DisableEditingAttributes(bpy.types.Operator):
bl_idname = "bim.disable_editing_attributes"
bl_label = "Disable Editing Attributes"
bl_description = "ALT + Left Click to disable editing attributes on all selected objects"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty(options={"SKIP_SAVE"})
mass_operation: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
def disable_editing_attributes_on_obj(self, obj: bpy.types.Object) -> None:
props = tool.Blender.get_object_attribute_props(obj)
def invoke(self, context, event):
self.mass_operation = event.alt
return self.execute(context)
def disable_editing_attributes_on_obj(self, obj):
props = obj.BIMAttributeProperties
props.is_editing_attributes = False
def execute(self, context):
@@ -124,7 +118,7 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.file = tool.Ifc.get()
obj = tool.Blender.get_active_object(is_selected=False)
if not obj or not (element := tool.Ifc.get_entity(obj)):
if not (element := tool.Ifc.get_entity(obj)):
return
def callback(attributes, prop):
@@ -136,7 +130,7 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
attributes[prop.name] = None
return True
props = tool.Blender.get_object_attribute_props(obj)
props = obj.BIMAttributeProperties
attributes = bonsai.bim.helper.export_attributes(props.attributes, callback=callback)
ifcopenshell.api.attribute.edit_attributes(self.file, product=element, attributes=attributes)
@@ -171,12 +165,13 @@ class GenerateGlobalId(bpy.types.Operator, tool.Ifc.Operator):
element.GlobalId = ifcopenshell.guid.new()
obj = context.active_object
if not obj or not (props := tool.Blender.get_object_attribute_props(obj)).is_editing_attributes:
if not obj or not obj.BIMAttributeProperties.is_editing_attributes:
return {"FINISHED"}
props = obj.BIMAttributeProperties
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcRoot"):
if not element.is_a("IfcRoot"):
return {"FINISHED"}
if self.use_selected and obj in context.selected_objects:
@@ -196,10 +191,7 @@ class CopyAttributeToSelection(bpy.types.Operator, tool.Ifc.Operator):
name: bpy.props.StringProperty()
def _execute(self, context):
obj = tool.Blender.get_active_object()
assert obj
props = tool.Blender.get_object_attribute_props(obj)
value = props.attributes[self.name].get_value()
value = tool.Blender.get_active_object().BIMAttributeProperties.attributes.get(self.name).get_value()
total = core.copy_attribute_to_selection(
tool.Ifc, tool.Blender, tool.Root, tool.Spatial, name=self.name, value=value
)
@@ -29,13 +29,8 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING
class BIMAttributeProperties(PropertyGroup):
attributes: CollectionProperty(name="Attributes", type=Attribute)
is_editing_attributes: BoolProperty(name="Is Editing Attributes")
if TYPE_CHECKING:
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
is_editing_attributes: bool
+2 -4
View File
@@ -17,16 +17,14 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bonsai.bim.helper
import bpy.types
from bpy.types import Panel
from bonsai.bim.module.attribute.data import AttributesData
import bonsai.tool as tool
def draw_ui(context: bpy.types.Context, layout: bpy.types.UILayout, attributes) -> None:
def draw_ui(context, layout, attributes):
obj = context.active_object
assert obj
props = tool.Blender.get_object_attribute_props(obj)
props = obj.BIMAttributeProperties
if props.is_editing_attributes:
row = layout.row(align=True)
+26 -17
View File
@@ -42,7 +42,6 @@ import ifcopenshell.util.unit
import bonsai.tool as tool
import bonsai.bim.module.bcf.prop as bcf_prop
import bonsai.bim.module.bcf.bcfstore as bcfstore
from bpy_extras.io_utils import ImportHelper, ExportHelper
from pathlib import Path
from math import radians, degrees, atan, tan, cos, sin
from mathutils import Vector, Matrix, Euler, geometry
@@ -64,14 +63,12 @@ class NewBcfProject(bpy.types.Operator):
return {"FINISHED"}
class LoadBcfProject(bpy.types.Operator, ImportHelper):
class LoadBcfProject(bpy.types.Operator):
bl_idname = "bim.load_bcf_project"
bl_label = "Load BCF Project"
bl_description = "Load the BCF file."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"})
filename_ext = ".bcf"
def execute(self, context):
# Operator is also used when new project is created by not yet saved.
@@ -111,6 +108,10 @@ class LoadBcfProject(bpy.types.Operator, ImportHelper):
self.report({"INFO"}, f"BCF Project '{Path(self.filepath).name}' is loaded.")
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class UnloadBcfProject(bpy.types.Operator):
bl_idname = "bim.unload_bcf_project"
@@ -340,15 +341,13 @@ class EditBcfTopic(bpy.types.Operator):
return {"FINISHED"}
class SaveBcfProject(bpy.types.Operator, ExportHelper):
class SaveBcfProject(bpy.types.Operator):
bl_idname = "bim.save_bcf_project"
bl_label = "Save BCF Project"
bl_description = "Save active BCF project by the provided filepath."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"})
save_current_bcf: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
filename_ext = ".bcf"
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
@@ -365,7 +364,8 @@ class SaveBcfProject(bpy.types.Operator, ExportHelper):
self.filepath = str(path)
return self.execute(context)
return ExportHelper.invoke(self, context, event)
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class AddBcfTopic(bpy.types.Operator):
@@ -1283,7 +1283,6 @@ class ActivateBcfViewpoint(bpy.types.Operator):
cam_aspect: float,
context: bpy.types.Context,
) -> None:
assert isinstance(obj.data, bpy.types.Camera)
if viewpoint.visualization_info.orthogonal_camera:
camera = viewpoint.visualization_info.orthogonal_camera
obj.data.type = "ORTHO"
@@ -1401,7 +1400,6 @@ class ActivateBcfViewpoint(bpy.types.Operator):
self.hide_spaces(context)
def hide_spaces(self, context: bpy.types.Context) -> None:
assert context.area
old = context.area.type
context.area.type = "VIEW_3D"
bpy.ops.object.select_all(action="DESELECT")
@@ -1536,13 +1534,12 @@ class OpenBcfReferenceLink(bpy.types.Operator):
return {"FINISHED"}
class SelectBcfHeaderFile(bpy.types.Operator, ImportHelper):
class SelectBcfHeaderFile(bpy.types.Operator):
bl_idname = "bim.select_bcf_header_file"
bl_label = "Select BCF Header File"
bl_description = "Select filepath for BCF header reference."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"})
filename_ext = ".ifc"
def execute(self, context):
if self.filepath:
@@ -1550,12 +1547,16 @@ class SelectBcfHeaderFile(bpy.types.Operator, ImportHelper):
props.file_reference = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectBcfBimSnippetReference(bpy.types.Operator, ImportHelper):
class SelectBcfBimSnippetReference(bpy.types.Operator):
bl_idname = "bim.select_bcf_bim_snippet_reference"
bl_label = "Select BCF BIM Snippet Reference"
bl_description = "Select filepath for BCF snippet reference."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
if self.filepath:
@@ -1563,12 +1564,16 @@ class SelectBcfBimSnippetReference(bpy.types.Operator, ImportHelper):
props.bim_snippet_reference = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectBcfDocumentReference(bpy.types.Operator, ImportHelper):
class SelectBcfDocumentReference(bpy.types.Operator):
bl_idname = "bim.select_bcf_document_reference"
bl_label = "Select BCF Document Reference"
bl_description = "Select filepath for BCF document reference."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
if self.filepath:
@@ -1576,6 +1581,10 @@ class SelectBcfDocumentReference(bpy.types.Operator, ImportHelper):
props.document_reference = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class LoadBcfHeaderIfcFile(bpy.types.Operator):
bl_idname = "bim.load_bcf_header_ifc_file"
@@ -43,8 +43,6 @@ class BoundaryDecorator:
cls.installed = None
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
+66 -120
View File
@@ -26,7 +26,6 @@ import numpy as np
import multiprocessing
import ifcopenshell.api
import ifcopenshell.api.boundary
import ifcopenshell.api.root
import ifcopenshell.geom
import ifcopenshell.util.unit
import ifcopenshell.util.shape
@@ -42,7 +41,6 @@ from bonsai.bim.module.model.decorator import ProfileDecorator
from bonsai.bim.module.boundary.decorator import BoundaryDecorator
import bonsai.core
import bonsai.core.geometry
from typing import Union, Optional
def disable_editing_boundary_geometry(context):
@@ -60,7 +58,7 @@ def disable_editing_boundary_geometry(context):
class Loader:
def __init__(self, operator: Optional[bpy.types.Operator] = None):
def __init__(self, operator=None):
self.operator = operator
self.ifc_file = None
self.logger = None
@@ -69,20 +67,17 @@ class Loader:
self.fallback_settings = self.load_fallback_settings()
self.load_importer()
def create_mesh(self, boundary: ifcopenshell.entity_instance) -> Union[bpy.types.Mesh, None]:
def create_mesh(self, boundary):
# ConnectionGeometry is optional in IFC schema for some reasons.
if not boundary.ConnectionGeometry:
return None
assert self.ifc_importer
assert self.operator
surface = boundary.ConnectionGeometry.SurfaceOnRelatingElement
# Workaround for invalid geometry provided by Revit. See https://github.com/Autodesk/revit-ifc/issues/270
if surface.is_a("IfcCurveBoundedPlane") and not getattr(surface, "InnerBoundaries", None):
surface.InnerBoundaries = ()
try:
shape = ifcopenshell.geom.create_shape(self.settings, surface)
mesh = self.ifc_importer.create_mesh(boundary, shape)
assert mesh
mesh = self.ifc_importer.create_mesh(None, shape)
tool.Loader.link_mesh(shape, mesh)
except RuntimeError:
# Fallback solution for invalid geometry provided by Revit. (InnerBoundaries cuting OuterBoundary)
@@ -102,8 +97,7 @@ class Loader:
return None
mesh = bpy.data.meshes.new(str(surface.id()))
bm = bmesh.new()
coords = ifcopenshell.util.shape.get_vertices(shape)
verts = [bm.verts.new(coord) for coord in coords]
verts = [bm.verts.new(shape.verts[i : i + 3]) for i in range(0, len(shape.verts), 3)]
bm.faces.new(verts)
for inner_boundary in surface.InnerBoundaries:
try:
@@ -114,9 +108,7 @@ class Loader:
f"Skipping an inner boundary for IfcRelSpaceBoundary with ID {boundary.id()}. Geometry might be invalid",
)
return None
coords = ifcopenshell.util.shape.get_vertices(shape)
verts = [bm.verts.new(coord) for coord in coords]
bm.faces.new(verts)
verts = [bm.verts.new(shape.verts[i : i + 3]) for i in range(0, len(shape.verts), 3)]
for i in range(len(verts) - 1):
bm.edges.new(verts[i : i + 2])
bm.edges.new((verts[-1], verts[0]))
@@ -139,7 +131,7 @@ class Loader:
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
return settings
def load_importer(self) -> None:
def load_importer(self):
self.ifc_file = tool.Ifc.get()
self.logger = logging.getLogger("ImportIFC")
ifc_import_settings = import_ifc.IfcImportSettings.factory(bpy.context, IfcStore.path, self.logger)
@@ -288,7 +280,7 @@ class SelectProjectBoundaries(bpy.types.Operator):
return {"FINISHED"}
def get_colour(ifc_boundary: ifcopenshell.entity_instance) -> tuple[float, float, float, float]:
def get_colour(ifc_boundary):
"""Return a color depending on IfcClass given"""
product_colors = {
"IfcWall": (0.7, 0.3, 0, 1),
@@ -324,8 +316,7 @@ class ColourByRelatedBuildingElement(bpy.types.Operator):
def _execute(self, context):
for obj in context.visible_objects:
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
if not obj.BIMObjectProperties.ifc_definition_id:
continue
element = tool.Ifc.get_entity(obj)
if not element.is_a("IfcRelSpaceBoundary"):
@@ -592,70 +583,47 @@ class DecorateBoundaries(bpy.types.Operator, tool.Ifc.Operator):
class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_boundary"
bl_label = "Add Boundary"
bl_description = (
"There are two options for this operator:\n"
"- just 1 IfcSpace is selected - generate a boundary based on "
"potential nearby (0.1m) boundary elements.\n"
"- 1 IfcSpace and 1 IfcElement are selected - generate a boundary "
"for IfcSpace and use selected IfcElement as boundary's building element."
)
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
parent_boundaries: list[ifcopenshell.entity_instance] = []
relating_space = None
related_building_element = None
relating_space_obj = None
related_building_element_obj = None
parent_boundaries = []
objs = tool.Blender.get_selected_objects()
ifc_objects = {element: obj for obj in objs if (element := tool.Ifc.get_entity(obj))}
if len(ifc_objects) not in (1, 2):
self.report({"ERROR"}, "Please select one or two IFC elements.")
return {"CANCELLED"}
objs = context.selected_objects
if not any(element.is_a("IfcSpace") for element in ifc_objects):
self.report({"ERROR"}, "One of the selected objects must be an IfcSpace.")
return {"CANCELLED"}
if not any((element := tool.Ifc.get_entity(obj)) and element.is_a("IfcSpace") for obj in objs):
self.report({"INFO"}, "No IfcSpace elements selected.")
return {"FINISHED"}
if len(objs) == 2:
# Validate input.
space = next((e for e in ifc_objects.keys() if e.is_a("IfcSpace")), None)
if not space:
self.report({"ERROR"}, "One of the selected objects must be an IfcSpace.")
return {"CANCELLED"}
element = next((e for e in ifc_objects.keys() if e.is_a("IfcElement")), None)
if not element:
self.report({"ERROR"}, "One of the selected objects must be an IfcElement.")
return {"CANCELLED"}
# The user may select two objects, a space and its related building element
for obj in objs:
element = tool.Ifc.get_entity(obj)
if not element:
continue
if element.is_a("IfcSpace"):
relating_space = element
relating_space_obj = obj
else:
related_building_element = element
related_building_element_obj = obj
parent_boundary = self.create_element_boundary(
context, space, ifc_objects[space], element, ifc_objects[element]
context, relating_space, relating_space_obj, related_building_element, related_building_element_obj
)
if not parent_boundary:
self.report({"ERROR"}, "Failed to create a boundary.")
return {"FINISHED"}
parent_boundaries.append(parent_boundary)
if parent_boundary:
parent_boundaries.append(parent_boundary)
elif len(objs) == 1:
space = next(iter(ifc_objects))
if not space.is_a("IfcSpace"):
self.report({"ERROR"}, "1 element selected but it's not an IfcSpace - please, select IfcSpace..")
return {"CANCELLED"}
# New prototype, old code not yet removed. Still testing.
res = self.auto_generate_boundaries(space, ifc_objects[space])
if isinstance(res, str):
self.report({"ERROR"}, res)
return {"FINISHED"}
parent_boundaries.extend(res)
elif len(objs) == 1 and False:
space = tool.Ifc.get_entity(objs[0])
if space.is_a("IfcSpace"):
self.auto_generate_boundaries(space, objs[0])
elif len(objs) == 1:
# Optionally the user may select just the space, and the building element shall be auto-detected
# TODO : refactor to be able to generate all boundaries for selected space automatically or with an option
relating_space = None
related_building_element = None
relating_space_obj = None
related_building_element_obj = None
def msg(self, context):
self.layout.label(text="Please set an active container to detect space boundaries from.")
@@ -692,17 +660,8 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
obj = tool.Ifc.get_object(parent_boundary)
obj.select_set(True)
def auto_generate_boundaries(
self, space: ifcopenshell.entity_instance, space_obj: bpy.types.Object
) -> Union[str, list[ifcopenshell.entity_instance]]:
"""
:return: list of created boundaries or a string with error description.
"""
ifc_file = tool.Ifc.get()
def auto_generate_boundaries(self, space, space_obj):
props = tool.Model.get_model_props()
boundaries: list[ifcopenshell.entity_instance] = []
assert isinstance(space_obj.data, bpy.types.Mesh)
# Identify all potential building elements
# TODO: don't select everything, use AABB culling in Blender
building_elements = (
@@ -722,7 +681,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
shapes = {}
settings = ifcopenshell.geom.settings()
settings.set("disable-opening-subtractions", True)
iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=include)
iterator = ifcopenshell.geom.iterator(settings, tool.Ifc.get(), multiprocessing.cpu_count(), include=include)
if iterator.initialize():
while True:
tree.add_element(iterator.get_native())
@@ -735,12 +694,12 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
building_elements = [e for e in tree.select(space, extend=0.1) if e != space]
if not building_elements:
return "No building elements found to create boundaries."
return
# Create a dissolved bmesh for the space
space_bm = bmesh.new()
space_bm.from_mesh(space_obj.data)
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts[:], edges=space_bm.edges[:])
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts, edges=space_bm.edges)
# Create dissolved bmeshes for all boundary elements
building_element_bms = {}
@@ -818,7 +777,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
# we cheat by using the exterior boundary to mean "gross".
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
parent_boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
parent_boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
if building_element.is_a("IfcVirtualElement"):
parent_boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
else:
@@ -850,7 +809,6 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
exterior_boundary_polygon, space_face_matrix
)
self.set_boundary_name(parent_boundary)
boundaries.append(parent_boundary)
for rel in getattr(building_element, "HasOpenings", []):
opening = rel.RelatedOpeningElement
@@ -896,7 +854,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
if opening_polygon.intersection(exterior_boundary_polygon).area == 0:
continue
boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
boundary.RelatingSpace = space
boundary.RelatedBuildingElement = filling or opening
boundary.ConnectionGeometry = self.create_connection_geometry_from_polygon(
@@ -910,39 +868,29 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
if boundary.is_a() != "IfcRelSpaceBoundary":
boundary.ParentBoundary = parent_boundary
self.set_boundary_name(boundary)
boundaries.append(boundary)
return boundaries
def create_element_boundary(
self,
context: bpy.types.Context,
space: ifcopenshell.entity_instance,
space_obj: bpy.types.Object,
building_element: ifcopenshell.entity_instance,
building_element_obj: bpy.types.Object,
) -> Union[ifcopenshell.entity_instance, None]:
"""Create element boundary for IfcSpace and IfcElement as building element.
self, context, relating_space, relating_space_obj, related_building_element, related_building_element_obj
):
if not relating_space or not related_building_element:
return
:return: IfcRelSpaceBoundary or ``None`` if couldn't find a face on ``space_obj``
that would be close enough to ``related_building_element_obj``.
"""
props = tool.Model.get_model_props()
# Find which face on space should be bounded to related building element
# TODO: Handle round wall where multiple faces need to be bound to the same related building element
space_bm = bmesh.new()
assert isinstance(space_obj.data, bpy.types.Mesh)
space_bm.from_mesh(space_obj.data)
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts[:], edges=space_bm.edges[:])
bm = bmesh.new()
bm.from_mesh(relating_space_obj.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi * 2 / 360, verts=bm.verts, edges=bm.edges)
target_distance = inf
target_face = None
to_building_obj_space = building_element_obj.matrix_world.inverted()
for face in space_bm.faces:
centroid = space_obj.matrix_world @ face.calc_center_median()
raycast = building_element_obj.closest_point_on_mesh(to_building_obj_space @ centroid, distance=1)
for face in bm.faces:
centroid = relating_space_obj.matrix_world @ face.calc_center_median()
raycast = related_building_element_obj.closest_point_on_mesh(
related_building_element_obj.matrix_world.inverted() @ centroid, distance=1
)
if raycast[0]:
distance = (building_element_obj.matrix_world @ raycast[1] - centroid).length
distance = (related_building_element_obj.matrix_world @ raycast[1] - centroid).length
if distance < target_distance:
target_face = face
target_distance = distance
@@ -957,7 +905,9 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
target_face_polygon = shapely.Polygon([tuple((target_face_matrix_i @ v).xy) for v in target_face_verts])
related_building_element_polygon = self.get_flattened_polygon(building_element, space_obj, target_face_matrix_i)
related_building_element_polygon = self.get_flattened_polygon(
related_building_element, relating_space_obj, target_face_matrix_i
)
gross_boundary_polygon = target_face_polygon.intersection(related_building_element_polygon)
@@ -973,8 +923,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
):
return
ifc_file = tool.Ifc.get()
parent_boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
parent_boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
parent_boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
# Set to EXTERNAL by default and turn later to internal if there is a corresponding boundary relating to an
# internal space
@@ -987,14 +936,14 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
# space.
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
for rel in getattr(building_element, "HasOpenings", []):
for rel in getattr(related_building_element, "HasOpenings", []):
opening = rel.RelatedOpeningElement
if not opening.HasFillings:
continue
# TODO: HasFilling is a zero to many relationship. How to handle many ?
filling = opening.HasFillings[0].RelatedBuildingElement
opening_polygon = self.get_flattened_polygon(opening, space_obj, target_face_matrix_i)
opening_polygon = self.get_flattened_polygon(opening, relating_space_obj, target_face_matrix_i)
# An inner boundary is supposed to overlap its parent boundary according to IFC4 documentation
# so we extend our exterior boundary with all opening which has a filling
@@ -1010,10 +959,10 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
connection_geometry = self.create_connection_geometry_from_polygon(opening_polygon, target_face_matrix)
boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
boundary.RelatingSpace = space
boundary.RelatingSpace = relating_space
boundary.RelatedBuildingElement = filling
boundary.ConnectionGeometry = connection_geometry
if building_element.is_a("IfcVirtualElement"):
if related_building_element.is_a("IfcVirtualElement"):
boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
else:
boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
@@ -1025,23 +974,20 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
connection_geometry = self.create_connection_geometry_from_polygon(
exterior_boundary_polygon, target_face_matrix
)
parent_boundary.RelatingSpace = space
parent_boundary.RelatedBuildingElement = building_element
parent_boundary.RelatingSpace = relating_space
parent_boundary.RelatedBuildingElement = related_building_element
parent_boundary.ConnectionGeometry = connection_geometry
self.set_boundary_name(parent_boundary)
return parent_boundary
def get_face_matrix(self, p1: Vector, p2: Vector, p3: Vector) -> Matrix:
def get_face_matrix(self, p1, p2, p3):
edge1 = p2 - p1
edge2 = p3 - p1
normal = edge1.cross(edge2)
assert isinstance(normal, Vector)
z_axis = normal.normalized()
x_axis = p2 - p1
x_axis.normalize()
y_axis = z_axis.cross(x_axis)
assert isinstance(y_axis, Vector)
mat = Matrix()
mat.col[0][:3] = x_axis
@@ -1111,7 +1057,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
return surface
def set_boundary_name(self, boundary: ifcopenshell.entity_instance):
def set_boundary_name(self, boundary):
"""
By convention 1stLevel and 2ndLevel boundary have specific name and description
See https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRelSpaceBoundary.htm
+6 -8
View File
@@ -52,9 +52,9 @@ class BIM_PT_Boundary(Panel):
@classmethod
def poll(cls, context):
if not (obj := context.active_object):
if not context.active_object:
return False
props = tool.Blender.get_object_bim_props(obj)
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
@@ -65,7 +65,7 @@ class BIM_PT_Boundary(Panel):
def draw(self, context):
obj = context.active_object
assert obj
props = tool.Blender.get_object_bim_props(obj)
props = obj.BIMObjectProperties
ifc_file = tool.Ifc.get()
boundary = ifc_file.by_id(props.ifc_definition_id)
self.bprops = tool.Boundary.get_object_boundary_props(obj)
@@ -128,9 +128,9 @@ class BIM_PT_SpaceBoundaries(Panel):
@classmethod
def poll(cls, context):
if not (obj := context.active_object):
if not context.active_object:
return False
props = tool.Blender.get_object_bim_props(obj)
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
@@ -145,9 +145,7 @@ class BIM_PT_SpaceBoundaries(Panel):
if not SpaceBoundariesData.is_loaded:
SpaceBoundariesData.load()
obj = context.active_object
assert obj
self.props = tool.Blender.get_object_bim_props(obj)
self.props = context.active_object.BIMObjectProperties
self.ifc_file = tool.Ifc.get()
row = self.layout.row()
row.operator("bim.load_space_boundaries")
+1 -1
View File
@@ -52,7 +52,7 @@ class BrickschemaData:
def active_relations(cls):
if BrickStore.graph is None:
return []
props = tool.Brick.get_brick_props()
props = bpy.context.scene.BIMBrickProperties
try:
brick = props.bricks[props.active_brick_index]
except:
+21 -21
View File
@@ -22,26 +22,29 @@ import ifcopenshell.api
import bonsai.tool as tool
import bonsai.core.brick as core
import bonsai.bim.handler
from bpy_extras.io_utils import ImportHelper, ExportHelper
from bonsai.bim.ifc import IfcStore
from bonsai.tool.brick import BrickStore
class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.load_brick_project"
bl_label = "Load Brickschema Project"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Load in a Brick project from a file"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"})
filename_ext = ".ttl"
def _execute(self, context):
if os.path.exists(self.filepath) and "ttl" in os.path.splitext(self.filepath)[1].lower():
root = tool.Brick.get_brick_props().brick_list_root
root = context.scene.BIMBrickProperties.brick_list_root
core.load_brick_project(tool.Brick, filepath=self.filepath, brick_root=root)
else:
self.report({"ERROR"}, f"Failed to load {self.filepath}")
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ViewBrickClass(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.view_brick_class"
@@ -108,10 +111,10 @@ class AssignBrickReference(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Assign the selected Ifc entity to the selected Brick entity"
def _execute(self, context):
if not (obj := context.active_object) or not (element := tool.Ifc.get_entity(obj)):
if not context.active_object:
self.report({"ERROR"}, f"No Ifc selected")
return
props = tool.Brick.get_brick_props()
props = context.scene.BIMBrickProperties
try:
props.bricks[props.active_brick_index]
except:
@@ -120,7 +123,7 @@ class AssignBrickReference(bpy.types.Operator, tool.Ifc.Operator):
core.assign_brick_reference(
tool.Ifc,
tool.Brick,
element=element,
element=tool.Ifc.get_entity(context.active_object),
library=tool.Ifc.get().by_id(int(props.libraries)),
brick_uri=props.bricks[props.active_brick_index].uri,
)
@@ -133,7 +136,7 @@ class AddBrick(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Create the Brick entity"
def _execute(self, context):
props = tool.Brick.get_brick_props()
props = context.scene.BIMBrickProperties
core.add_brick(
tool.Ifc,
tool.Brick,
@@ -152,7 +155,7 @@ class AddBrickRelation(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Create the Brick relationship"
def _execute(self, context):
props = tool.Brick.get_brick_props()
props = context.scene.BIMBrickProperties
brick = props.bricks[props.active_brick_index]
if props.new_brick_relation_type == "http://www.w3.org/2000/01/rdf-schema#label":
object = props.new_brick_relation_object
@@ -170,7 +173,7 @@ class ConvertIfcToBrick(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Convert Ifc entities and relations to Brick entities and relations"
def _execute(self, context):
props = tool.Brick.get_brick_props()
props = context.scene.BIMBrickProperties
library = None
if props.libraries:
library = tool.Ifc.get().by_id(int(props.libraries))
@@ -198,8 +201,7 @@ class NewBrickFile(bpy.types.Operator):
return {"FINISHED"}
def _execute(self, context):
props = tool.Brick.get_brick_props()
root = props.brick_list_root
root = context.scene.BIMBrickProperties.brick_list_root
core.new_brick_file(tool.Brick, brick_root=root)
def rollback(self, data):
@@ -228,7 +230,7 @@ class RemoveBrick(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Delete this entity"
def _execute(self, context):
props = tool.Brick.get_brick_props()
props = context.scene.BIMBrickProperties
core.remove_brick(
tool.Ifc,
tool.Brick,
@@ -237,20 +239,18 @@ class RemoveBrick(bpy.types.Operator, tool.Ifc.Operator):
)
class SerializeBrick(bpy.types.Operator, ExportHelper):
class SerializeBrick(bpy.types.Operator):
bl_idname = "bim.serialize_brick"
bl_label = "Serialize Brick"
# Prevents crash on Blender 4.4.0.
bl_description = "Save active Brick project by the provided filepath."
filename_ext = ".ttl"
filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"})
def invoke(self, context, event):
if self.should_save_as or not BrickStore.path:
return ExportHelper.invoke(self, context, event)
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
else:
return self.execute(context)
@@ -273,7 +273,7 @@ class AddBrickNamespace(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Bind a new namespace to the Brick project"
def _execute(self, context):
props = tool.Brick.get_brick_props()
props = context.scene.BIMBrickProperties
alias = props.new_brick_namespace_alias
uri = props.new_brick_namespace_uri
core.add_brick_namespace(tool.Brick, alias=alias, uri=uri)
@@ -288,6 +288,6 @@ class RemoveBrickRelation(bpy.types.Operator, tool.Ifc.Operator):
object: bpy.props.StringProperty(name="Object")
def _execute(self, context):
props = tool.Brick.get_brick_props()
props = context.scene.BIMBrickProperties
brick = props.bricks[props.active_brick_index]
core.remove_brick_relation(tool.Brick, brick_uri=brick.uri, predicate=self.predicate, object=self.object)
+4 -35
View File
@@ -33,7 +33,6 @@ from bpy.props import (
import bonsai.core.brick as core
import bonsai.tool.brick as tool
from bonsai.tool.brick import BrickStore
from typing import TYPE_CHECKING
def update_active_brick_index(self, context):
@@ -75,13 +74,13 @@ def get_brick_relations(self, context):
return BRICK_RELATIONS_ENUM_ITEMS
def update_view(self: "BIMBrickProperties", context: bpy.types.Context) -> None:
root = self.brick_list_root
def update_view(self, context):
root = context.scene.BIMBrickProperties.brick_list_root
core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=False)
def split_screen_update_view(self: "BIMBrickProperties", context: bpy.types.Context) -> None:
root = self.split_screen_brick_list_root
def split_screen_update_view(self, context):
root = context.scene.BIMBrickProperties.split_screen_brick_list_root
core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=True)
@@ -91,11 +90,6 @@ class Brick(PropertyGroup):
uri: StringProperty(name="URI")
total_items: IntProperty(name="Total Items")
if TYPE_CHECKING:
label: str
uri: str
total_items: int
class BIMBrickProperties(PropertyGroup):
active_brick_class: StringProperty(name="Active Brick Class")
@@ -130,28 +124,3 @@ class BIMBrickProperties(PropertyGroup):
split_screen_brick_list_root: EnumProperty(
name="Split Screen Brick List Root", items=get_brick_roots, update=split_screen_update_view
)
if TYPE_CHECKING:
active_brick_class: str
brick_breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
bricks: bpy.types.bpy_prop_collection_idprop[Brick]
active_brick_index: int
libraries: str
set_list_root_toggled: bool
brick_list_root: str
namespace: str
new_brick_namespace_alias: str
new_brick_namespace_uri: str
new_brick_label: str
brick_entity_create_type: str
brick_entity_class: str
brick_create_relations_toggled: bool
brick_edit_relations_toggled: bool
new_brick_relation_type: str
new_brick_relation_object: str
split_screen_toggled: bool
split_screen_bricks: bpy.types.bpy_prop_collection_idprop[Brick]
split_screen_active_brick_index: int
split_screen_active_brick_class: str
split_screen_brick_breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
split_screen_brick_list_root: str
+7 -19
View File
@@ -45,7 +45,7 @@ class BIM_PT_brickschema_project_info(Panel):
bl_parent_id = "BIM_PT_brickschema"
def draw(self, context):
self.props = tool.Brick.get_brick_props()
self.props = context.scene.BIMBrickProperties
if not BrickschemaData.data["is_loaded"]:
row = self.layout.row(align=True)
@@ -87,7 +87,7 @@ class BIM_PT_brickschema_namespaces(Panel):
return BrickStore.graph != None
def draw(self, context):
self.props = tool.Brick.get_brick_props()
self.props = context.scene.BIMBrickProperties
row = self.layout.row(align=True)
row.label(text="Active Namespace:")
@@ -122,7 +122,7 @@ class BIM_PT_brickschema_create_entity(Panel):
return BrickStore.graph != None
def draw(self, context):
self.props = tool.Brick.get_brick_props()
self.props = context.scene.BIMBrickProperties
# TO DO: hide this if selected entity already has a reference, or something similar
row = self.layout.row(align=True)
@@ -156,7 +156,7 @@ class BIM_PT_brickschema_viewport(Panel):
return BrickStore.graph != None
def draw(self, context):
self.props = tool.Brick.get_brick_props()
self.props = context.scene.BIMBrickProperties
row = self.layout.row(align=True)
row.column().alignment = "RIGHT"
@@ -178,14 +178,7 @@ class BIM_PT_brickschema_viewport(Panel):
row = grid_left.row()
BIM_UL_bricks.split_screen = False
row.template_list(
BIM_UL_bricks.__name__,
BIM_UL_bricks.__name__ + "_bricks",
self.props,
"bricks",
self.props,
"active_brick_index",
)
row.template_list("BIM_UL_bricks", "", self.props, "bricks", self.props, "active_brick_index")
if self.props.split_screen_toggled:
grid_right = grid.column(align=True)
@@ -202,12 +195,7 @@ class BIM_PT_brickschema_viewport(Panel):
row = grid_right.row()
BIM_UL_bricks.split_screen = True
row.template_list(
BIM_UL_bricks.__name__,
BIM_UL_bricks.__name__ + "_split_screen_bricks",
self.props,
"split_screen_bricks",
self.props,
"split_screen_active_brick_index",
"BIM_UL_bricks", "", self.props, "split_screen_bricks", self.props, "split_screen_active_brick_index"
)
if BrickschemaData.data["active_relations"]:
@@ -304,7 +292,7 @@ class BIM_PT_ifc_brickschema_references(Panel):
def draw(self, context):
if not BrickschemaReferencesData.is_loaded:
BrickschemaReferencesData.load()
self.props = tool.Brick.get_brick_props()
self.props = context.scene.BIMBrickProperties
if not BrickschemaReferencesData.data["is_loaded"]:
row = self.layout.row()
@@ -21,13 +21,14 @@ from . import ui, prop, operator
classes = (
operator.GetBSDDClassificationProperties,
operator.LoadBSDDDictionaries,
operator.LoadBSDDDomains,
operator.SearchBSDDClass,
prop.BSDDDictionary,
operator.SetActiveBSDDDictionary,
prop.BSDDDomain,
prop.BSDDClassification,
prop.BSDDPset,
prop.BIMBSDDProperties,
ui.BIM_UL_bsdd_dictionaries,
ui.BIM_UL_bsdd_domains,
ui.BIM_UL_bsdd_classifications,
ui.BIM_PT_bsdd,
)
-45
View File
@@ -1,45 +0,0 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifcopenshell
import ifcopenshell.util.date
import ifcopenshell.util.classification
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
def refresh():
BSDDData.is_loaded = False
class BSDDData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data["active_dictionary"] = cls.active_dictionary()
@classmethod
def active_dictionary(cls):
props = tool.Bsdd.get_bsdd_props()
results = [("ALL", "All Dictionaries", "All active dictionaries")]
results.extend([(d.uri, d.name, f"{d.status} - {d.version}") for d in props.dictionaries if d.is_active])
return results
+25 -5
View File
@@ -22,13 +22,25 @@ import bonsai.tool as tool
from bonsai.core import bsdd as core
class LoadBSDDDictionaries(bpy.types.Operator):
bl_idname = "bim.load_bsdd_dictionaries"
class LoadBSDDDomains(bpy.types.Operator):
bl_idname = "bim.load_bsdd_domains"
bl_label = "Load bSDD Dictionaries"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
core.load_bsdd(tool.Bsdd)
core.load_bsdd(bsdd.Client(), tool.Bsdd)
return {"FINISHED"}
class SetActiveBSDDDictionary(bpy.types.Operator):
bl_idname = "bim.set_active_bsdd_domain"
bl_label = "Set bSDD Dictionary as active"
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty()
uri: bpy.props.StringProperty()
def execute(self, context):
core.set_active_bsdd_dictionary(self.name, self.uri, tool.Bsdd)
return {"FINISHED"}
@@ -38,9 +50,17 @@ class SearchBSDDClass(bpy.types.Operator):
bl_description = "Search for bSDD classes by the provided keyword"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
# Requirement by buildingSMART bSDD api.
if len(context.scene.BIMBSDDProperties.keyword) < 3:
cls.poll_message_set("Search query has to be at least 3 characters long.")
return False
return True
def execute(self, context):
keyword = context.scene.BIMBSDDProperties.keyword
classes_found = core.search_class(keyword, tool.Bsdd)
classes_found = core.search_class(keyword, bsdd.Client(), tool.Bsdd)
self.report({"INFO"}, f"{classes_found} bSDD classes found for '{keyword}'.")
return {"FINISHED"}
@@ -52,7 +72,7 @@ class GetBSDDClassificationProperties(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
pset_data = core.get_class_properties(tool.Bsdd)
pset_data = core.get_class_properties(bsdd.Client(), tool.Bsdd)
psets_found = len(pset_data)
props_found = sum(len(pset) for pset in pset_data.values())
self.report({"INFO"}, f"{psets_found} psets found ({props_found} props) for the active classification.")
+9 -30
View File
@@ -18,7 +18,6 @@
import bpy
from bpy.types import PropertyGroup
from bonsai.bim.module.bsdd.data import BSDDData
from bonsai.bim.prop import Attribute, StrProperty
from bpy.props import (
PointerProperty,
@@ -32,34 +31,21 @@ from bpy.props import (
)
def get_active_dictionary(self, context):
if not BSDDData.is_loaded:
BSDDData.load()
return BSDDData.data["active_dictionary"]
def update_is_active(self: "BSDDDictionary", context: bpy.types.Context) -> None:
BSDDData.data["active_dictionary"] = BSDDData.active_dictionary()
class BSDDDictionary(PropertyGroup):
class BSDDDomain(PropertyGroup):
name: StringProperty(name="Name")
uri: StringProperty(name="URI")
default_language_code: StringProperty(name="Language")
organization_name_owner: StringProperty(name="Organization")
status: StringProperty(name="Status")
version: StringProperty(name="Version")
is_active: BoolProperty(
name="Is Active", description="Enable to search with this dictionary", default=False, update=update_is_active
)
class BSDDClassification(PropertyGroup):
name: StringProperty(name="Name")
reference_code: StringProperty(name="Reference Code")
uri: StringProperty(name="Namespace URI")
dictionary_name: StringProperty(name="Dictionary Name")
dictionary_namespace_uri: StringProperty(name="Dictionary Namespace URI")
domain_name: StringProperty(name="Domain Name")
domain_namespace_uri: StringProperty(name="Domain Namespace URI")
class BSDDPset(PropertyGroup):
@@ -68,11 +54,10 @@ class BSDDPset(PropertyGroup):
class BIMBSDDProperties(PropertyGroup):
active_dictionary: StringProperty(name="Active Dictionary")
active_dictionary: EnumProperty(items=get_active_dictionary, name="Active Dictionary")
active_domain: StringProperty(name="Active Domain")
active_uri: StringProperty(name="Active URI")
dictionaries: CollectionProperty(name="Dictionaries", type=BSDDDictionary)
active_dictionary_index: IntProperty(name="Active Dictionary Index")
domains: CollectionProperty(name="Domains", type=BSDDDomain)
active_domain_index: IntProperty(name="Active Domain Index")
classifications: CollectionProperty(name="Classifications", type=BSDDClassification)
active_classification_index: IntProperty(name="Active Classification Index")
keyword: StringProperty(name="Keyword", description="Query for bsdd classes search, case and accent insensitive")
@@ -84,15 +69,9 @@ class BIMBSDDProperties(PropertyGroup):
use_only_ifc_properties: BoolProperty(
name="Only IFC Properties",
description="Whether to display and assign only properties from IFC dictionary",
default=False,
default=True,
)
load_preview_dictionaries: BoolProperty(
name="Load Preview Dictionaries", description="Load dictionaries marked as Preview status", default=False
)
load_inactive_dictionaries: BoolProperty(
name="Load Inactive Dictionaries", description="Load dictionaries marked as Inactive status", default=False
)
load_test_dictionaries: BoolProperty(
name="Load Test Dictionaries", description="Load dictionaries that are for testing only", default=False
load_preview_domains: BoolProperty(
name="Load Preview Domains", description="Whether it should load preview and inactive domains", default=False
)
classification_psets: CollectionProperty(name="Classification Psets", type=BSDDPset)
+33 -27
View File
@@ -17,7 +17,6 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bonsai.tool as tool
from bonsai.bim.module.bsdd.data import BSDDData
from bpy.types import Panel, UIList
@@ -31,58 +30,65 @@ class BIM_PT_bsdd(Panel):
bl_parent_id = "BIM_PT_tab_project_setup"
def draw(self, context):
if not BSDDData.is_loaded:
BSDDData.load()
props = context.scene.BIMBSDDProperties
layout = self.layout
if len(props.dictionaries):
row = self.layout.row()
row.operator("bim.load_bsdd_dictionaries", icon="FILE_REFRESH")
row = self.layout.row(align=True)
row.prop(props, "load_preview_domains")
if len(props.domains):
row.operator("bim.load_bsdd_domains", text="", icon="FILE_REFRESH")
if len(props.dictionaries):
if props.active_domain:
row = self.layout.row()
row.label(text="Active: " + props.active_domain, icon="URL")
else:
row = self.layout.row()
row.label(text="No Active bSDD Domain", icon="ERROR")
if len(props.domains):
self.layout.template_list(
"BIM_UL_bsdd_dictionaries",
"BIM_UL_bsdd_domains",
"",
props,
"dictionaries",
"domains",
props,
"active_dictionary_index",
"active_domain_index",
)
if 0 <= props.active_dictionary_index < len(props.dictionaries):
selected_dictionary = props.dictionaries[props.active_dictionary_index]
if 0 <= props.active_domain_index < len(props.domains):
selected_domain = props.domains[props.active_domain_index]
else:
selected_dictionary = None
selected_domain = None
if selected_dictionary:
layout.label(text="Selected dictionary:")
if selected_domain:
layout.label(text="Selected domain:")
box = layout.box()
row = box.row(align=True)
row.label(text="Language")
row.label(text=selected_dictionary.default_language_code)
row.label(text=selected_domain.default_language_code)
row = box.row(align=True)
row.label(text="Version")
row.label(text=selected_dictionary.version)
box.operator("bim.open_uri", text="Open bSDD In Browser", icon="URL").uri = selected_dictionary.uri
row.label(text=selected_domain.version)
box.operator("bim.open_uri", text="Open bSDD In Browser", icon="URL").uri = selected_domain.uri
else:
row = self.layout.row()
row.operator("bim.load_bsdd_dictionaries")
row.operator("bim.load_bsdd_domains")
class BIM_UL_bsdd_dictionaries(UIList):
class BIM_UL_bsdd_domains(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
props = context.scene.BIMBSDDProperties
row = layout.row(align=True)
if item.status != "Active":
row.label(
text=f"{item.name} ({item.organization_name_owner}) v{item.version} - {item.status}", icon="ERROR"
)
row.label(text=f"{item.name} ({item.organization_name_owner}) - {item.status}", icon="ERROR")
else:
row.label(text=f"{item.name} ({item.organization_name_owner}) v{item.version}")
row.prop(
item, "is_active", icon="CHECKBOX_HLT" if item.is_active else "CHECKBOX_DEHLT", text="", emboss=False
)
row.label(text=f"{item.name} ({item.organization_name_owner})")
if item.uri == props.active_uri:
row.label(text="", icon="URL")
else:
op = row.operator("bim.set_active_bsdd_domain", text="", icon="RESTRICT_SELECT_OFF")
op.name = item.name
op.uri = item.uri
class BIM_UL_bsdd_classifications(UIList):
+47 -51
View File
@@ -128,10 +128,9 @@ class CadMitre(bpy.types.Operator):
class CadFillet(bpy.types.Operator):
bl_idname = "bim.cad_fillet"
bl_label = "CAD Fillet"
bl_description = "Add fillet to the 2 selected edges."
bl_options = {"REGISTER", "UNDO"}
resolution: bpy.props.IntProperty(name="Arc Resolution", min=0, default=1)
radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE")
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
@classmethod
def poll(cls, context):
@@ -144,15 +143,18 @@ class CadFillet(bpy.types.Operator):
layout.prop(self, prop)
def execute(self, context):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.radius = self.radius * si_conversion
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.mode_set(mode="EDIT")
obj = bpy.context.active_object
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
cursor = obj.matrix_world.inverted() @ bpy.context.scene.cursor.location
mesh = obj.data
bm = bmesh.from_edit_mesh(mesh)
selected_edges = [e for e in bm.edges if e.select]
if len(selected_edges) != 2:
self.report({"ERROR"}, "Exactly 2 edges should be selected.")
return {"CANCELLED"}
# Assume the user has selected two edges sharing a vert, but merge verts
@@ -217,7 +219,6 @@ class CadFillet(bpy.types.Operator):
class CadArcFrom2Points(bpy.types.Operator):
bl_idname = "bim.cad_arc_from_2_points"
bl_label = "CAD Arc from 2 Points"
bl_description = "Add an arc to the active mesh based on 2 selected vertices."
bl_options = {"REGISTER", "UNDO"}
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
should_flip: bpy.props.BoolProperty(name="Flip", description="Flip arc", default=False)
@@ -234,7 +235,6 @@ class CadArcFrom2Points(bpy.types.Operator):
if bpy.context.mode != "EDIT_MESH":
return {"CANCELLED"}
obj = bpy.context.active_object
assert obj
region = bpy.context.region
region_3d = bpy.context.area.spaces.active.region_3d
cursor = bpy.context.scene.cursor.location
@@ -242,12 +242,10 @@ class CadArcFrom2Points(bpy.types.Operator):
if not center:
return {"CANCELLED"}
mesh = obj.data
assert isinstance(mesh, bpy.types.Mesh)
mw = obj.matrix_world
bm = bmesh.from_edit_mesh(mesh)
selected_verts = [v for v in bm.verts if v.select]
if len(selected_verts) != 2:
self.report({"ERROR"}, "Exactly 2 vertices should be selected.")
return {"CANCELLED"}
v1 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[0].co)
v2 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[1].co)
@@ -344,12 +342,11 @@ class CadArcFrom3Points(bpy.types.Operator):
class CadOffset(bpy.types.Operator):
bl_idname = "bim.cad_offset"
bl_label = "CAD Offset"
bl_description = "Copy selected mesh geometry at provided offset. Mesh copied based on the current viewport angle."
bl_options = {"REGISTER", "UNDO"}
distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE")
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
@classmethod
def poll(cls, context):
def poll(self, context):
return context.mode == "EDIT_MESH"
def draw(self, context):
@@ -372,6 +369,9 @@ class CadOffset(bpy.types.Operator):
# effect of converting an unclosed loop into a closed loop which is also
# not what users expect.
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.distance = self.distance * si_conversion
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.mode_set(mode="EDIT")
@@ -543,8 +543,7 @@ class CadOffset(bpy.types.Operator):
class AddIfcCircle(bpy.types.Operator):
bl_idname = "bim.add_ifccircle"
bl_label = "Add IfcCircle"
bl_description = "Add IfcCircle to the currently active mesh."
radius: bpy.props.FloatProperty(name="Radius", default=0.5, subtype="DISTANCE")
radius: bpy.props.FloatProperty(name="Radius", default=0.5)
@classmethod
def poll(cls, context):
@@ -552,19 +551,18 @@ class AddIfcCircle(bpy.types.Operator):
return bool(obj) and obj.type == "MESH"
def execute(self, context):
obj = context.active_object
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
self.obj = obj
self.mesh = mesh
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.radius = self.radius * si_conversion
if self.has_selected_existing_circle(context):
self.change_radius(context)
else:
self.create_circle(context)
return {"FINISHED"}
def has_selected_existing_circle(self, context: bpy.types.Context) -> bool:
obj = self.obj
bm = bmesh.from_edit_mesh(self.mesh)
def has_selected_existing_circle(self, context):
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
verts = [v for v in bm.verts if v.select and not v.hide]
if len(verts) != 2:
return False
@@ -577,32 +575,32 @@ class AddIfcCircle(bpy.types.Operator):
for group in groups:
if group in verts[0][deform_layer] and group in verts[1][deform_layer]:
return True
return False
def change_radius(self, context: bpy.types.Object) -> None:
bm = bmesh.from_edit_mesh(self.mesh)
def change_radius(self, context):
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
verts = [v for v in bm.verts if v.select and not v.hide]
center = verts[0].co.lerp(verts[1].co, 0.5)
verts[0].co = center + ((verts[0].co - center).normalized() * self.radius)
verts[1].co = center + ((verts[1].co - center).normalized() * self.radius)
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(self.mesh)
bmesh.update_edit_mesh(obj.data)
def create_circle(self, context: bpy.types.Context) -> None:
obj = self.obj
def create_circle(self, context):
obj = context.active_object
bpy.ops.object.mode_set(mode="OBJECT")
# The last group may be the result of a prior run of this operator.
# I tried looping through all groups but Blender group indices seem to behave unpredictably.
if len(obj.vertex_groups):
last_group = obj.vertex_groups[-1]
verts_in_group = [v for v in self.mesh.vertices if last_group.index in [vg.group for vg in v.groups]]
verts_in_group = [v for v in obj.data.vertices if last_group.index in [vg.group for vg in v.groups]]
if "IFCCIRCLE" in last_group.name and len(verts_in_group) != 2:
obj.vertex_groups.remove(last_group)
group = obj.vertex_groups.new(name="IFCCIRCLE")
bpy.ops.object.mode_set(mode="EDIT")
bm = bmesh.from_edit_mesh(self.mesh)
bm = bmesh.from_edit_mesh(obj.data)
bm.verts.layers.deform.verify()
deform_layer = bm.verts.layers.deform.active
@@ -619,7 +617,7 @@ class AddIfcCircle(bpy.types.Operator):
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(self.mesh)
bmesh.update_edit_mesh(obj.data)
class AddIfcArcIndexFillet(bpy.types.Operator):
@@ -640,19 +638,18 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
layout.prop(self, prop)
def execute(self, context):
obj = context.active_object
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
self.obj = obj
self.mesh = mesh
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.radius = self.radius * si_conversion
if self.has_selected_existing_arc(context):
self.change_radius(context)
else:
return self.create_arc(context)
self.create_arc(context)
return {"FINISHED"}
def has_selected_existing_arc(self, context: bpy.types.Context) -> bool:
obj = self.obj
bm = bmesh.from_edit_mesh(self.mesh)
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
verts = [v for v in bm.verts if v.select and not v.hide]
if len(verts) != 3:
return False
@@ -671,14 +668,13 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
return False
def change_radius(self, context: bpy.types.Context) -> None:
obj = self.obj
bm = bmesh.from_edit_mesh(self.mesh)
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
edges = [e for e in bm.edges if e.select and not e.hide]
mid = list(set(edges[0].verts) & set(edges[1].verts))[0]
v1 = edges[0].other_vert(mid)
v2 = edges[1].other_vert(mid)
assert v1 and v2
center = tool.Cad.get_center_of_arc([v1.co, mid.co, v2.co])
if len(v1.link_edges) != 2 or len(v2.link_edges) != 2:
@@ -686,7 +682,6 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
v3 = v1.link_edges[1].other_vert(v1) if mid in v1.link_edges[0].verts else v1.link_edges[0].other_vert(v1)
v4 = v2.link_edges[1].other_vert(v2) if mid in v2.link_edges[0].verts else v2.link_edges[0].other_vert(v2)
assert v3 and v4
dir1 = (v3.co - v1.co).normalized()
dir2 = (v4.co - v2.co).normalized()
@@ -707,10 +702,11 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(self.mesh)
bmesh.update_edit_mesh(obj.data)
def create_arc(self, context):
obj = bpy.context.active_object
def create_arc(self, context: bpy.types.Context) -> set[str]:
obj = self.obj
bpy.ops.object.mode_set(mode="OBJECT")
# The last group may be the result of a prior run of this operator.
# I tried looping through all groups but Blender group indices seem to behave unpredictably.
@@ -722,7 +718,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
group = obj.vertex_groups.new(name="IFCARCINDEX")
bpy.ops.object.mode_set(mode="EDIT")
bm = bmesh.from_edit_mesh(self.mesh)
bm = bmesh.from_edit_mesh(obj.data)
bm.verts.layers.deform.verify()
deform_layer = bm.verts.layers.deform.active
@@ -741,7 +737,6 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
shared_vert = list(set(selected_edges[0].verts) & set(selected_edges[1].verts))[0]
v1 = selected_edges[0].other_vert(shared_vert)
v2 = selected_edges[1].other_vert(shared_vert)
assert v1 and v2
dir1 = (v1.co - shared_vert.co).normalized()
dir2 = (v2.co - shared_vert.co).normalized()
edge_angle = dir1.angle(dir2)
@@ -775,8 +770,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(self.mesh)
return {"FINISHED"}
bmesh.update_edit_mesh(obj.data)
class AlignViewToProfile(bpy.types.Operator):
@@ -812,9 +806,8 @@ class AlignViewToProfile(bpy.types.Operator):
class AddRectangle(bpy.types.Operator):
bl_idname = "bim.add_rectangle"
bl_label = "Add Rectangle"
bl_description = "Add rectangle shape to the active mesh."
x: bpy.props.FloatProperty(name="X", default=1, subtype="DISTANCE")
y: bpy.props.FloatProperty(name="Y", default=1, subtype="DISTANCE")
x: bpy.props.FloatProperty(name="X", default=1)
y: bpy.props.FloatProperty(name="Y", default=1)
@classmethod
def poll(cls, context):
@@ -822,8 +815,11 @@ class AddRectangle(bpy.types.Operator):
return bool(obj) and obj.type == "MESH"
def execute(self, context):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.x = self.x * si_conversion
self.y = self.y * si_conversion
obj = context.active_object
assert obj and isinstance(obj.data, bpy.types.Mesh)
bm = bmesh.from_edit_mesh(obj.data)
cursor = obj.matrix_world.inverted() @ context.scene.cursor.location
new_verts = [
-9
View File
@@ -20,7 +20,6 @@ import bpy
from bonsai.bim.module.model.data import AuthoringData
from bpy.types import PropertyGroup
from math import pi
from typing import TYPE_CHECKING
class BIMCadProperties(PropertyGroup):
@@ -32,11 +31,3 @@ class BIMCadProperties(PropertyGroup):
gable_roof_edge_angle: bpy.props.FloatProperty(
name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE"
)
if TYPE_CHECKING:
resolution: int
radius: float
distance: float
x: float
y: float
gable_roof_edge_angle: float
+53 -31
View File
@@ -23,7 +23,7 @@ import bonsai.bim.module.type.prop as type_prop
import ifcopenshell.util.unit
from bpy.types import WorkSpaceTool
from bonsai.bim.module.model.data import AuthoringData, RailingData, RoofData
from functools import partial
from typing import Union
def load_custom_icons():
@@ -114,11 +114,11 @@ class CadTool(WorkSpaceTool):
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Rectangle", "S_R", bpy.ops.bim.add_rectangle.__doc__, ui_context)
add_layout_hotkey_operator(row, "Rectangle", "S_R", "Rectangle", ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Circle", "S_C", bpy.ops.bim.add_ifccircle.__doc__, ui_context)
add_layout_hotkey_operator(row, "Circle", "S_C", "Circle", ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.set_arc_index.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
@@ -138,15 +138,15 @@ class CadTool(WorkSpaceTool):
row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.cad_fillet.__doc__, ui_context)
add_layout_hotkey_operator(row, "Fillet", "S_F", "Fillet", ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
else:
if (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["pset_data"]
and tool.Model.get_railing_props(obj).is_editing_path
and obj.BIMRailingProperties.is_editing_path
):
add_header_apply_button(
layout,
@@ -159,7 +159,7 @@ class CadTool(WorkSpaceTool):
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
and tool.Model.get_roof_props(obj).is_editing_path
and obj.BIMRoofProperties.is_editing_path
):
add_header_apply_button(
layout, "Edit Roof Path", "bim.finish_editing_roof_path", "bim.cancel_editing_roof_path", ui_context
@@ -176,17 +176,17 @@ class CadTool(WorkSpaceTool):
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "2-Point Arc", "S_C", bpy.ops.bim.cad_arc_from_2_points.__doc__, ui_context)
add_layout_hotkey_operator(row, "2-Point Arc", "S_C", "2-Point Arc", ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.cad_arc_from_3_points.__doc__, ui_context)
class CadHotkey(bpy.types.Operator):
bl_idname = "bim.cad_hotkey"
bl_label = ""
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_label = "CAD Hotkey"
bl_options = {"REGISTER", "UNDO"}
hotkey: bpy.props.StringProperty()
description: bpy.props.StringProperty()
@@ -195,14 +195,12 @@ class CadHotkey(bpy.types.Operator):
return operator.description or ""
def execute(self, context):
self.props = tool.Cad.get_cad_props()
self.props = context.scene.BIMCadProperties
getattr(self, f"hotkey_{self.hotkey}")()
return {"FINISHED"}
def draw(self, context):
props = tool.Cad.get_cad_props()
obj = context.active_object
props = context.scene.BIMCadProperties
if self.hotkey == "S_C":
if tool.Geometry.is_profile_object_active():
row = self.layout.row()
@@ -228,7 +226,7 @@ class CadHotkey(bpy.types.Operator):
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
and tool.Model.get_roof_props(obj).is_editing_path
and bpy.context.active_object.BIMRoofProperties.is_editing_path
):
self.layout.row().prop(props, "gable_roof_edge_angle")
@@ -238,8 +236,9 @@ class CadHotkey(bpy.types.Operator):
row.prop(props, "resolution")
def hotkey_S_C(self):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if tool.Geometry.is_profile_object_active():
bpy.ops.bim.add_ifccircle(radius=self.props.radius)
bpy.ops.bim.add_ifccircle(radius=self.props.radius / si_conversion)
else:
bpy.ops.bim.cad_arc_from_2_points()
@@ -247,13 +246,15 @@ class CadHotkey(bpy.types.Operator):
bpy.ops.bim.cad_trim_extend()
def hotkey_S_F(self):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if tool.Geometry.is_profile_object_active():
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius)
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius / si_conversion)
else:
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius)
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius / si_conversion)
def hotkey_S_O(self):
bpy.ops.bim.cad_offset(distance=self.props.distance)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bpy.ops.bim.cad_offset(distance=self.props.distance / si_conversion)
def hotkey_S_Q(self):
obj = bpy.context.active_object
@@ -280,16 +281,13 @@ class CadHotkey(bpy.types.Operator):
bpy.ops.bim.edit_extrusion_axis()
def hotkey_S_R(self):
obj = bpy.context.active_object
if not obj:
return
if tool.Geometry.is_profile_object_active():
bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bpy.ops.bim.add_rectangle(x=self.props.x / si_conversion, y=self.props.y / si_conversion)
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
and tool.Model.get_roof_props(obj).is_editing_path
and bpy.context.active_object.BIMRoofProperties.is_editing_path
):
bpy.ops.bim.set_gable_roof_edge_angle(angle=self.props.gable_roof_edge_angle)
@@ -307,9 +305,7 @@ class CadHotkey(bpy.types.Operator):
bpy.ops.bim.reset_vertex()
def add_header_apply_button(
layout: bpy.types.UILayout, text: str, apply_operator: str, cancel_operator: str, ui_context: str = ""
) -> None:
def add_header_apply_button(layout, text, apply_operator, cancel_operator, ui_context=""):
custom_icon = custom_icon_previews.get(text.upper().replace(" ", "_"), custom_icon_previews["IFC"]).icon_id
row = layout.row(align=True)
row.label(text=f"{text} Mode", icon_value=custom_icon)
@@ -328,8 +324,34 @@ def add_header_apply_button(
row.label(text="Tools")
add_layout_hotkey_operator = partial(tool.Blender.add_layout_hotkey_operator, tool_name="cad", module_name=__name__)
def add_layout_hotkey_operator(
layout: bpy.types.UILayout, text: str, hotkey: str, description: Union[str, None], ui_context: str = ""
) -> bpy.types.OperatorProperties:
parts = hotkey.split("_")
modifier, key = parts
op_text = "" if ui_context == "TOOL_HEADER" else text
custom_icon = custom_icon_previews.get(text.upper().replace(" ", "_"), custom_icon_previews["IFC"]).icon_id
modifier_icon, modifier_str = MODIFIERS.get(modifier, ("NONE", ""))
row = layout if ui_context == "TOOL_HEADER" else layout.row(align=True)
op = row.operator("bim.cad_hotkey", text=op_text, icon_value=custom_icon)
if ui_context != "TOOL_HEADER" and len(parts) == 2:
layout = layout.row(align=True) # Create a new line for hotkey display
layout.label(text="", icon=modifier_icon)
layout.label(text="", icon=f"EVENT_{key}")
hotkey_description = f"Hotkey: {modifier_str} {key}"
description = "\n\n".join(filter(None, [hotkey_description if description else ""]))
op.hotkey = hotkey
op.description = description or hotkey_description
return op
custom_icon_previews = None
display_mode = None
MODIFIERS = {
"S": ("EVENT_SHIFT", ""),
}
@@ -49,8 +49,6 @@ class ClashDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
@@ -62,9 +60,8 @@ class ClashDecorator:
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
props = tool.Clash.get_clash_props()
text = props.active_clash_text
p = props.p1.lerp(props.p2, 0.5)
text = context.scene.BIMClashProperties.active_clash_text
p = context.scene.BIMClashProperties.p1.lerp(context.scene.BIMClashProperties.p2, 0.5)
font_id = 0
blf.size(font_id, 12)
@@ -95,8 +92,7 @@ class ClashDecorator:
# general shader
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
props = tool.Clash.get_clash_props()
selected_vertices = [props.p1, props.p2]
selected_vertices = [context.scene.BIMClashProperties.p1, context.scene.BIMClashProperties.p2]
selected_edges = []
if selected_vertices[0] != selected_vertices[1]:
selected_edges = [[0, 1]]
+73 -73
View File
@@ -24,20 +24,26 @@ import logging
import numpy as np
import ifcopenshell
import bonsai.tool as tool
from bpy_extras.io_utils import ExportHelper, ImportHelper
from math import radians
from mathutils import Matrix, Vector
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.clash.decorator import ClashDecorator
class ExportClashSets(bpy.types.Operator, ExportHelper):
class ExportClashSets(bpy.types.Operator):
bl_idname = "bim.export_clash_sets"
bl_label = "Export Clash Sets"
bl_description = "Export clash sets to a selected file"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
clash_sets = tool.Clash.export_clash_sets()
@@ -46,24 +52,26 @@ class ExportClashSets(bpy.types.Operator, ExportHelper):
return {"FINISHED"}
class ImportClashSets(bpy.types.Operator, ImportHelper):
class ImportClashSets(bpy.types.Operator):
bl_idname = "bim.import_clash_sets"
bl_label = "Import Clash Sets"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Import clash sets from a selected file"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
return ImportHelper.invoke(self, context, event)
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
tool.Clash.load_clash_sets(self.filepath)
props = tool.Clash.get_clash_props()
props.clash_sets.clear()
context.scene.BIMClashProperties.clash_sets.clear()
for clash_set in tool.Clash.get_clash_sets():
new = props.clash_sets.add()
new = context.scene.BIMClashProperties.clash_sets.add()
new.name = clash_set["name"]
new.mode = clash_set["mode"]
if new.mode == "intersection":
@@ -98,8 +106,7 @@ class AddClashSet(bpy.types.Operator):
bl_description = "Add a clash set"
def execute(self, context):
props = tool.Clash.get_clash_props()
new = props.clash_sets.add()
new = context.scene.BIMClashProperties.clash_sets.add()
new.name = "New Clash Set"
return {"FINISHED"}
@@ -112,8 +119,7 @@ class RemoveClashSet(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
props = tool.Clash.get_clash_props()
props.clash_sets.remove(self.index)
context.scene.BIMClashProperties.clash_sets.remove(self.index)
return {"FINISHED"}
@@ -125,8 +131,7 @@ class AddClashSource(bpy.types.Operator):
group: bpy.props.StringProperty()
def execute(self, context):
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
clash_set = context.scene.BIMClashProperties.active_clash_set
source = getattr(clash_set, self.group).add()
return {"FINISHED"}
@@ -140,74 +145,78 @@ class RemoveClashSource(bpy.types.Operator):
group: bpy.props.StringProperty()
def execute(self, context):
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
clash_set = context.scene.BIMClashProperties.active_clash_set
getattr(clash_set, self.group).remove(self.index)
return {"FINISHED"}
class SelectClashSource(bpy.types.Operator, ImportHelper):
class SelectClashSource(bpy.types.Operator):
bl_idname = "bim.select_clash_source"
bl_label = "Select Clash Source"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select an IFC file to add as a clash source"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
index: bpy.props.IntProperty()
group: bpy.props.StringProperty()
filename_ext = ".ifc"
def execute(self, context):
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
clash_set = context.scene.BIMClashProperties.active_clash_set
getattr(clash_set, self.group)[self.index].name = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectClashResults(bpy.types.Operator, ImportHelper):
class SelectClashResults(bpy.types.Operator):
bl_idname = "bim.select_clash_results"
bl_label = "Select Clash Results"
bl_description = "Select filepath for clash results."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
props = tool.Clash.get_clash_props()
props.clash_results_path = self.filepath
context.scene.BIMClashProperties.clash_results_path = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectSmartGroupedClashesPath(bpy.types.Operator, ImportHelper):
class SelectSmartGroupedClashesPath(bpy.types.Operator):
bl_idname = "bim.select_smart_grouped_clashes_path"
bl_label = "Select Smart-Grouped Clashes Path"
bl_description = "Select filepath for smart-grouped clashes."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
props = tool.Clash.get_clash_props()
props.smart_grouped_clashes_path = self.filepath
context.scene.BIMClashProperties.smart_grouped_clashes_path = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
class ExecuteIfcClash(bpy.types.Operator):
bl_idname = "bim.execute_ifc_clash"
bl_label = "Execute IFC Clash"
bl_description = "Execute clash detection and save the information to a .bcf or .json file"
filter_glob: bpy.props.StringProperty(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"})
@property
def filename_ext(self) -> str:
return f".{self.format.lower()}"
def invoke(self, context, event):
if self.filepath:
return self.execute(context)
return ExportHelper.invoke(self, context, event)
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
from ifcclash import ifcclash
self.props = tool.Clash.get_clash_props()
self.props = context.scene.BIMClashProperties
_, extension = os.path.splitext(self.filepath)
if extension != ".bcf":
@@ -275,17 +284,19 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
return {"FINISHED"}
class SelectIfcClashResults(bpy.types.Operator, ImportHelper):
class SelectIfcClashResults(bpy.types.Operator):
bl_idname = "bim.select_ifc_clash_results"
bl_label = "Select IFC Clash Results"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select the clashing IFC geometry stored in a file"
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
return ImportHelper.invoke(self, context, event)
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
# TODO refactor into new clash results system
@@ -293,9 +304,7 @@ class SelectIfcClashResults(bpy.types.Operator, ImportHelper):
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
with open(self.filepath) as f:
clash_sets = json.load(f)
clash_props = tool.Clash.get_clash_props()
assert clash_props.active_clash_set
clash_set_name = clash_props.active_clash_set.name
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
global_ids = []
for clash_set in clash_sets:
if clash_set["name"] != clash_set_name:
@@ -307,8 +316,7 @@ class SelectIfcClashResults(bpy.types.Operator, ImportHelper):
global_ids.extend([clash["a_global_id"], clash["b_global_id"]])
for obj in context.visible_objects:
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
if not obj.BIMObjectProperties.ifc_definition_id:
continue
ifc_file = ""
@@ -327,7 +335,7 @@ class SelectIfcClashResults(bpy.types.Operator, ImportHelper):
element_file = self.file
try:
element = element_file.by_id(props.ifc_definition_id)
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
except:
continue
@@ -347,7 +355,7 @@ class SelectClash(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
self.props = tool.Clash.get_clash_props()
self.props = context.scene.BIMClashProperties
clash_set = tool.Clash.get_clash_set(self.props.active_clash_set.name)
active_clash = self.props.active_clash
clash = tool.Clash.get_clash(clash_set, active_clash.a_global_id, active_clash.b_global_id)
@@ -381,15 +389,13 @@ class SmartClashGroup(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = tool.Clash.get_clash_props()
return bool(props.clash_results_path)
return context.scene.BIMClashProperties.clash_results_path
def execute(self, context):
from ifcclash import ifcclash
settings = ifcclash.ClashSettings()
props = tool.Clash.get_clash_props()
self.filepath = bpy.path.ensure_ext(props.clash_results_path, ".json")
self.filepath = bpy.path.ensure_ext(context.scene.BIMClashProperties.clash_results_path, ".json")
settings.output = self.filepath
settings.logger = logging.getLogger("Clash")
settings.logger.setLevel(logging.DEBUG)
@@ -399,18 +405,19 @@ class SmartClashGroup(bpy.types.Operator):
clash_sets = json.load(f)
# execute the smart grouping
save_path = bpy.path.ensure_ext(props.smart_grouped_clashes_path, ".json")
smart_grouped_clashes = ifc_clasher.smart_group_clashes(clash_sets, props.smart_clash_grouping_max_distance)
save_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json")
smart_grouped_clashes = ifc_clasher.smart_group_clashes(
clash_sets, context.scene.BIMClashProperties.smart_clash_grouping_max_distance
)
# save smart_groups to json
with open(save_path, "w") as f:
f.write(json.dumps(smart_grouped_clashes))
assert props.active_clash_set
clash_set_name = props.active_clash_set.name
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
# Reset the list of smart_clash_groups for the UI
props.smart_clash_groups.clear()
context.scene.BIMClashProperties.smart_clash_groups.clear()
for clash_set, smart_groups in smart_grouped_clashes.items():
# Only select the clashes that correspond to the actively selected IFC Clash Set
@@ -418,7 +425,7 @@ class SmartClashGroup(bpy.types.Operator):
continue
else:
for smart_group, global_id_pairs in smart_groups[0].items():
new_group = props.smart_clash_groups.add()
new_group = context.scene.BIMClashProperties.smart_clash_groups.add()
new_group.number = f"{smart_group}"
for pair in global_id_pairs:
@@ -436,21 +443,18 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = tool.Clash.get_clash_props()
return bool(props.active_clash_set)
return context.scene.BIMClashProperties.active_clash_set
def execute(self, context):
props = tool.Clash.get_clash_props()
smart_groups_path = bpy.path.ensure_ext(props.smart_grouped_clashes_path, ".json")
smart_groups_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json")
assert props.active_clash_set
clash_set_name = props.active_clash_set.name
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
with open(smart_groups_path) as f:
smart_grouped_clashes = json.load(f)
# Reset the list of smart_clash_groups for the UI
props.smart_clash_groups.clear()
context.scene.BIMClashProperties.smart_clash_groups.clear()
for clash_set, smart_groups in smart_grouped_clashes.items():
# Only select the clashes that correspond to the actively selected IFC Clash Set
@@ -458,12 +462,12 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator):
continue
else:
for smart_group, global_id_pairs in smart_groups[0].items():
new_group = props.smart_clash_groups.add()
new_group = context.scene.BIMClashProperties.smart_clash_groups.add()
new_group.number = f"{smart_group}"
for pair in global_id_pairs:
for guid in pair:
new_global_id = new_group.global_ids.add()
new_global_id.name = guid
new_global_id.guid = guid
return {"FINISHED"}
@@ -475,19 +479,15 @@ class SelectSmartGroup(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = tool.Clash.get_clash_props()
return tool.Ifc.get() and context.visible_objects and props.active_smart_group
return tool.Ifc.get() and context.visible_objects and context.scene.BIMClashProperties.active_smart_group
def execute(self, context):
ifc_file = tool.Ifc.get()
props = tool.Clash.get_clash_props()
selected_smart_group = props.active_smart_group
assert selected_smart_group
products: list[ifcopenshell.entity_instance] = []
selected_smart_group = context.scene.BIMClashProperties.active_smart_group
products = []
for global_id in selected_smart_group.global_ids:
try:
products.append(ifc_file.by_guid(global_id.name))
except RuntimeError:
products.append(tool.Ifc.get().by_guid(global_id.guid))
except:
continue
tool.Spatial.select_products(products, unhide=True)
context_override = tool.Blender.get_viewport_context()
+11 -54
View File
@@ -17,7 +17,6 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
from bonsai.bim.prop import StrProperty, Attribute, BIMFilterGroup
from bpy.types import PropertyGroup
from bpy.props import (
@@ -30,8 +29,6 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from mathutils import Vector
from typing import TYPE_CHECKING, Literal, Union
class ClashSource(PropertyGroup):
@@ -46,10 +43,6 @@ class ClashSource(PropertyGroup):
name="Mode",
)
if TYPE_CHECKING:
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
mode: Literal["a", "i", "e"]
class Clash(PropertyGroup):
a_global_id: StringProperty(name="A")
@@ -58,15 +51,9 @@ class Clash(PropertyGroup):
b_name: StringProperty(name="B Name")
status: BoolProperty(name="Status", default=False)
if TYPE_CHECKING:
a_global_id: str
b_global_id: str
a_name: str
b_name: str
status: bool
class ClashSet(PropertyGroup):
name: StringProperty(name="Name")
mode: EnumProperty(
items=[
(
@@ -89,25 +76,11 @@ class ClashSet(PropertyGroup):
b: CollectionProperty(name="Group B", type=ClashSource)
clashes: CollectionProperty(name="Clashes", type=Clash)
if TYPE_CHECKING:
mode: Literal["intersection", "collision", "clearance"]
tolerance: float
clearance: float
allow_touching: bool
check_all: bool
a: bpy.types.bpy_prop_collection_idprop[ClashSource]
b: bpy.types.bpy_prop_collection_idprop[ClashSource]
clashes: bpy.types.bpy_prop_collection_idprop[Clash]
class SmartClashGroup(PropertyGroup):
number: StringProperty(name="Number")
global_ids: CollectionProperty(name="GlobalIDs", type=StrProperty)
if TYPE_CHECKING:
number: str
global_ids: bpy.types.bpy_prop_collection_idprop[StrProperty]
class BIMClashProperties(PropertyGroup):
blender_clash_set_a: CollectionProperty(name="Blender Clash Set A", type=StrProperty)
@@ -134,33 +107,17 @@ class BIMClashProperties(PropertyGroup):
subtype="FILE_PATH",
)
if TYPE_CHECKING:
blender_clash_set_a: bpy.types.bpy_prop_collection_idprop[StrProperty]
blender_clash_set_b: bpy.types.bpy_prop_collection_idprop[StrProperty]
clash_sets: bpy.types.bpy_prop_collection_idprop[ClashSet]
should_create_clash_snapshots: bool
clash_results_path: str
smart_grouped_clashes_path: str
active_clash_set_index: int
active_clash_index: int
smart_clash_groups: bpy.types.bpy_prop_collection_idprop[SmartClashGroup]
active_smart_group_index: int
smart_clash_grouping_max_distance: int
p1: Vector
p2: Vector
active_clash_text: str
export_path: str
@property
def active_clash_set(self):
if self.active_clash_set_index < len(self.clash_sets):
return self.clash_sets[self.active_clash_set_index]
@property
def active_clash_set(self) -> Union[ClashSet, None]:
return tool.Blender.get_active_uilist_element(self.clash_sets, self.active_clash_set_index)
def active_smart_group(self):
if self.active_smart_group_index < len(self.smart_clash_groups):
return self.smart_clash_groups[self.active_smart_group_index]
@property
def active_smart_group(self) -> Union[SmartClashGroup, None]:
return tool.Blender.get_active_uilist_element(self.smart_clash_groups, self.active_smart_group_index)
@property
def active_clash(self) -> Union[Clash, None]:
if not (clash_set := self.active_clash_set):
return None
return tool.Blender.get_active_uilist_element(clash_set.clashes, self.active_clash_index)
def active_clash(self):
if self.active_clash_index < len(self.active_clash_set.clashes):
return self.active_clash_set.clashes[self.active_clash_index]
+4 -3
View File
@@ -17,7 +17,6 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
import bonsai.bim.helper
from bpy.types import Panel
from bonsai.bim.module.clash.data import ClashData
@@ -37,7 +36,9 @@ class BIM_PT_ifcclash(Panel):
ClashData.load()
layout = self.layout
props = tool.Clash.get_clash_props()
scene = context.scene
props = scene.BIMClashProperties
row = layout.row(align=True)
row.operator("bim.add_clash_set")
@@ -156,7 +157,7 @@ class BIM_PT_smart_clash_manager(Panel):
def draw(self, context):
layout = self.layout
props = tool.Clash.get_clash_props()
props = context.scene.BIMClashProperties
row = layout.row()
layout.label(text="Select clash results to group:")
@@ -145,8 +145,11 @@ class CostClassificationsData(ReferencesData):
@classmethod
def references(cls):
results = []
props = tool.Cost.get_cost_props()
element = tool.Ifc.get().by_id(props.cost_items[props.active_cost_item_index].ifc_definition_id)
element = tool.Ifc.get().by_id(
bpy.context.scene.BIMCostProperties.cost_items[
bpy.context.scene.BIMCostProperties.active_cost_item_index
].ifc_definition_id
)
if element:
for reference in ifcopenshell.util.classification.get_references(element):
data = reference.get_info()
@@ -25,16 +25,19 @@ import ifcopenshell.util.classification
import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.bim.helper
from bpy_extras.io_utils import ImportHelper
from bonsai.bim.ifc import IfcStore
class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.load_classification_library"
bl_label = "Load Classification Library"
bl_description = "Load classification library from the provided filepath."
filename_ext = ".ifc"
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def _execute(self, context):
IfcStore.classification_file = ifcopenshell.open(self.filepath)
@@ -116,28 +119,28 @@ class AddClassificationFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMBSDDProperties
if props.active_dictionary == "ALL":
dictionaries = [d.uri for d in props.dictionaries if d.is_active]
else:
dictionaries = [props.active_dictionary]
for uri in dictionaries:
if not (dictionary := tool.Bsdd.get_dictionary(uri)):
continue
has_classification = False
for element in tool.Ifc.get().by_type("IfcClassification"):
if element.Name == dictionary["name"] or (
tool.Classification.get_location(element) == dictionary["uri"]
):
self.report({"INFO"}, f"Classification '{dictionary['name']}' is already added to the project.")
has_classification = True
if has_classification:
continue
classification = ifcopenshell.api.run(
"classification.add_classification", tool.Ifc.get(), classification=dictionary["name"]
domain = next((d for d in props.domains if d.name == props.active_domain and d.uri == props.active_uri), None)
# Maybe user loaded preview domains, set it as active
# and then reloaded them without preview domains.
if not domain:
self.report(
{"INFO"},
f"Couldn't find domain '{props.active_domain}' ({props.active_uri}). Try to reload bSDD dictionaries.",
)
classification.Source = dictionary["organizationNameOwner"]
classification.Edition = dictionary["version"]
tool.Classification.set_location(classification, dictionary["uri"])
return
for element in tool.Ifc.get().by_type("IfcClassification"):
if element.Name == props.active_domain or (tool.Classification.get_location(element) == domain.uri):
self.report({"INFO"}, f"Classification '{props.active_domain}' is already added to the project.")
return
classification = ifcopenshell.api.run(
"classification.add_classification", tool.Ifc.get(), classification=props.active_domain
)
classification.Source = domain.organization_name_owner
classification.Edition = domain.version
tool.Classification.set_location(classification, domain.uri)
class EnableAddingManualClassification(bpy.types.Operator):
@@ -420,17 +423,17 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
classification = None
for element in tool.Ifc.get().by_type("IfcClassification"):
if element.Name == bsdd_classification.dictionary_name or (
tool.Classification.get_location(element) == bsdd_classification.dictionary_namespace_uri
if element.Name == bsdd_classification.domain_name or (
tool.Classification.get_location(element) == bsdd_classification.domain_namespace_uri
):
classification = element
break
if not classification:
classification = ifcopenshell.api.run(
"classification.add_classification", tool.Ifc.get(), classification=bsdd_classification.dictionary_name
"classification.add_classification", tool.Ifc.get(), classification=bsdd_classification.domain_name
)
tool.Classification.set_location(classification, bsdd_classification.dictionary_namespace_uri)
tool.Classification.set_location(classification, bsdd_classification.domain_namespace_uri)
for obj in objects:
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
@@ -457,7 +460,6 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
for prop in blender_properties:
properties[prop.name] = prop.get_value()
# TODO: is this still the correct approach?
if classification_pset.name == "undefined_set":
if "ObjectType" in properties:
if hasattr(element, "ObjectType"):
@@ -78,8 +78,13 @@ class BIM_PT_classifications(Panel):
def draw_add_bsdd_ui(self, context):
self.bprops = context.scene.BIMBSDDProperties
if not self.bprops.active_domain:
row = self.layout.row()
row.label(text="No Active bSDD Domain", icon="ERROR")
return
row = self.layout.row()
row.prop(self.bprops, "active_dictionary", text="")
row.label(text="Active: " + self.bprops.active_domain, icon="URL")
row = self.layout.row()
row.operator("bim.add_classification_from_bsdd", icon="ADD")
@@ -158,8 +163,13 @@ class ReferenceUI:
row.operator("bim.enable_adding_manual_classification_reference", text="Add Reference", icon="ADD")
def draw_add_bsdd_ui(self, context):
if not self.bprops.active_domain:
row = self.layout.row()
row.label(text="No Active bSDD Domain", icon="ERROR")
return
row = self.layout.row()
row.prop(self.bprops, "active_dictionary", text="")
row.label(text="Active: " + self.bprops.active_domain, icon="URL")
row = self.layout.row(align=True)
row.prop(self.bprops, "keyword", text="")
@@ -328,8 +338,7 @@ class BIM_PT_cost_classifications(Panel, ReferenceUI):
def poll(cls, context):
if not tool.Ifc.get():
return False
props = tool.Cost.get_cost_props()
return bool(props.cost_items)
return bool(context.scene.BIMCostProperties.cost_items)
def draw(self, context):
if not CostClassificationsData.is_loaded:
@@ -161,12 +161,8 @@ class AssignConstraint(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.file = tool.Ifc.get()
objs = [bpy.data.objects[self.obj]] if self.obj else context.selected_objects
products = [
self.file.by_id(obj_id)
for obj in objs
if (obj_id := tool.Blender.get_object_bim_props(obj).ifc_definition_id)
]
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)]
if products:
ifcopenshell.api.run(
"constraint.assign_constraint",
@@ -188,12 +184,8 @@ class UnassignConstraint(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.file = tool.Ifc.get()
objs = [bpy.data.objects[self.obj]] if self.obj else context.selected_objects
products = [
self.file.by_id(obj_id)
for obj in objs
if (obj_id := tool.Blender.get_object_bim_props(obj).ifc_definition_id)
]
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)]
if products:
ifcopenshell.api.run(
"constraint.unassign_constraint",
@@ -79,18 +79,18 @@ class BIM_PT_object_constraints(Panel):
@classmethod
def poll(cls, context):
if not (obj := context.active_object):
if not context.active_object:
return False
props = tool.Blender.get_object_bim_props(obj)
return bool(tool.Ifc.get_object_by_identifier(props.ifc_definition_id))
if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id):
return False
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
def draw(self, context):
if not ObjectConstraintsData.is_loaded:
ObjectConstraintsData.load()
obj = context.active_object
assert obj
self.oprops = tool.Blender.get_object_bim_props(obj)
self.oprops = obj.BIMObjectProperties
self.sprops = context.scene.BIMConstraintProperties
self.props = obj.BIMObjectConstraintProperties
self.file = tool.Ifc.get()
@@ -30,7 +30,6 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING
class BIMContextProperties(PropertyGroup):
@@ -67,10 +66,3 @@ class BIMContextProperties(PropertyGroup):
)
active_context_id: IntProperty(name="Active Context Id")
context_attributes: CollectionProperty(name="Context Attributes", type=Attribute)
if TYPE_CHECKING:
contexts: str
subcontexts: str
target_views: str
active_context_id: int
context_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
+2 -3
View File
@@ -33,14 +33,13 @@ class BIM_PT_context(bpy.types.Panel):
@classmethod
def poll(cls, context):
return bool(tool.Ifc.get())
return tool.Ifc.get()
def draw(self, context):
if not ContextData.is_loaded:
ContextData.load()
assert self.layout
props = tool.Context.get_context_props()
props = context.scene.BIMContextProperties
row = self.layout.row(align=True)
row.prop(props, "contexts", text="")
@@ -61,7 +61,6 @@ classes = (
operator.ExportCostSchedules,
operator.HighlightProductCostItem,
operator.ImportCostScheduleCsv,
operator.RefreshCostScheduleCsv,
operator.LoadCostItemElementQuantities,
operator.LoadCostItemQuantities,
operator.LoadCostItemResourceQuantities,
@@ -83,7 +82,6 @@ classes = (
prop.CostItem,
prop.CostItemQuantity,
prop.CostItemType,
prop.CostItemsMapping,
prop.ScheduleColumn,
prop.BIMCostProperties,
ui.BIM_PT_cost_schedules,
+32 -53
View File
@@ -24,7 +24,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.unit
import bonsai.tool as tool
from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc
from typing import Any, Union
from typing import Any
def refresh():
@@ -33,19 +33,13 @@ def refresh():
CostItemQuantitiesData.is_loaded = False
CostItem = dict[str, Any]
CostQuantity = dict[str, Any]
CostSchedule = dict[str, Any]
Currency = dict[str, Any]
class CostSchedulesData:
data = {}
is_loaded = False
_cost_values: dict[int, dict[str, Any]]
@classmethod
def load(cls) -> None:
def load(cls):
cls.data = {
"predefined_types": cls.get_cost_schedule_types(),
"total_cost_schedules": cls.total_cost_schedules(),
@@ -60,21 +54,20 @@ class CostSchedulesData:
cls.is_loaded = True
@classmethod
def currency(cls) -> Union[Currency, None]:
def currency(cls):
unit = tool.Unit.get_project_currency_unit()
if unit:
return {"id": unit.id(), "name": unit.Currency}
@classmethod
def total_cost_schedules(cls) -> int:
def total_cost_schedules(cls):
return len(tool.Ifc.get().by_type("IfcCostSchedule"))
@classmethod
def schedules(cls) -> list[CostSchedule]:
results: list[CostSchedule] = []
props = tool.Cost.get_cost_props()
if props.active_cost_schedule_id:
schedule = tool.Ifc.get().by_id(props.active_cost_schedule_id)
def schedules(cls):
results = []
if bpy.context.scene.BIMCostProperties.active_cost_schedule_id:
schedule = tool.Ifc.get().by_id(bpy.context.scene.BIMCostProperties.active_cost_schedule_id)
results.append(
{
"id": schedule.id(),
@@ -94,19 +87,18 @@ class CostSchedulesData:
return results
@classmethod
def is_editing_rates(cls) -> bool:
props = tool.Cost.get_cost_props()
ifc_id = props.active_cost_schedule_id
def is_editing_rates(cls):
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_schedule_id
if not ifc_id:
return False
return
return tool.Ifc.get().by_id(ifc_id).PredefinedType == "SCHEDULEOFRATES"
@classmethod
def cost_items(cls) -> dict[int, CostItem]:
def cost_items(cls):
cls._cost_values = {}
results: dict[int, CostItem] = {}
results = {}
for cost_item in tool.Ifc.get().by_type("IfcCostItem"):
data: CostItem = {}
data = {}
cls._load_cost_item_quantities(cost_item, data)
cls._load_cost_values(cost_item, data)
cls._load_nesting_index(cost_item, data)
@@ -114,13 +106,13 @@ class CostSchedulesData:
return results
@classmethod
def _load_nesting_index(cls, cost_item: ifcopenshell.entity_instance, data: CostItem) -> None:
def _load_nesting_index(cls, cost_item, data):
data["NestingIndex"] = None
for rel in cost_item.Nests or []:
data["NestingIndex"] = rel.RelatedObjects.index(cost_item)
@classmethod
def _load_cost_values(cls, root_element: ifcopenshell.entity_instance, data: CostItem) -> None:
def _load_cost_values(cls, root_element, data):
# data["CostValues"] = []
data["CategoryValues"] = {}
data["UnitBasisValueComponent"] = None
@@ -129,7 +121,6 @@ class CostSchedulesData:
data["TotalCost"] = 0.0
has_unit_basis = False
is_sum = False
values: list[ifcopenshell.entity_instance]
if root_element.is_a("IfcCostItem"):
values = root_element.CostValues
elif root_element.is_a("IfcConstructionResource"):
@@ -157,7 +148,7 @@ class CostSchedulesData:
data["TotalAppliedValue"] = None
@classmethod
def _load_cost_item_quantities(cls, cost_item: ifcopenshell.entity_instance, data: CostItem) -> None:
def _load_cost_item_quantities(cls, cost_item, data):
# parametric_quantities = []
# for rel in cost_item.Controls:
# for related_object in rel.RelatedObjects or []:
@@ -165,9 +156,8 @@ class CostSchedulesData:
# parametric_quantities.extend(quantities)
data["TotalCostQuantity"] = ifcopenshell.util.cost.get_total_quantity(cost_item)
data["UnitSymbol"] = "-"
quantities: list[ifcopenshell.entity_instance] = cost_item.CostQuantities
if quantities:
quantity = quantities[0]
if cost_item.CostQuantities:
quantity = cost_item.CostQuantities[0]
data["QuantityType"] = quantity.is_a()
unit = ifcopenshell.util.unit.get_property_unit(quantity, tool.Ifc.get())
if unit:
@@ -221,31 +211,23 @@ class CostSchedulesData:
) -> list[int]:
if not element.is_a("IfcObject"):
return []
cost_quantities: list[ifcopenshell.entity_instance] = cost_item.CostQuantities
cost_quantities = cost_item.CostQuantities
if not cost_quantities:
return []
results: list[int] = []
relationship: ifcopenshell.entity_instance
results = []
for relationship in element.IsDefinedBy:
if not relationship.is_a("IfcRelDefinesByProperties"):
continue
qto: ifcopenshell.entity_instance = relationship.RelatingPropertyDefinition
qto = relationship.RelatingPropertyDefinition
if not qto.is_a("IfcElementQuantity"):
continue
prop: ifcopenshell.entity_instance
for prop in qto.Quantities:
if prop in cost_quantities:
results.append(prop.id())
return results
@classmethod
def _load_cost_value(
cls,
root_element: ifcopenshell.entity_instance,
root_element_data: CostItem,
cost_value: ifcopenshell.entity_instance,
) -> None:
def _load_cost_value(cls, root_element, root_element_data, cost_value):
value_data = cost_value.get_info()
del value_data["AppliedValue"]
if value_data["UnitBasis"]:
@@ -272,10 +254,9 @@ class CostSchedulesData:
cls._load_cost_value(root_element, root_element_data, component)
@classmethod
def cost_quantities(cls) -> list[CostQuantity]:
results: list[CostQuantity] = []
props = tool.Cost.get_cost_props()
ifc_id = props.active_cost_item_id
def cost_quantities(cls):
results = []
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_item_id
if not ifc_id:
return results
for quantity in tool.Ifc.get().by_id(ifc_id).CostQuantities or []:
@@ -283,22 +264,21 @@ class CostSchedulesData:
return results
@classmethod
def cost_values(cls) -> list[dict[str, str]]:
props = tool.Cost.get_cost_props()
ifc_id = props.active_cost_item_id
def cost_values(cls):
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_item_id
if not ifc_id:
return []
return ifcopenshell.util.cost.get_cost_values(tool.Ifc.get().by_id(ifc_id))
@classmethod
def quantity_types(cls) -> list[tuple[str, str, str]]:
def quantity_types(cls):
return [
(t.name(), t.name(), "")
for t in tool.Ifc.schema().declaration_by_name("IfcPhysicalSimpleQuantity").subtypes()
]
@classmethod
def get_cost_schedule_types(cls) -> list[tuple[str, str, str]]:
def get_cost_schedule_types(cls):
types = ifcopenshell.util.cost.get_cost_schedule_types(tool.Ifc.get())
return [(t["name"], t["name"], t["description"]) for t in types]
@@ -315,7 +295,7 @@ class CostItemRatesData:
cls.is_loaded = True
@classmethod
def schedule_of_rates(cls) -> list[tuple[str, str, str]]:
def schedule_of_rates(cls):
return [
(str(s.id()), s.Name or "Unnamed", "")
for s in tool.Ifc.get().by_type("IfcCostSchedule")
@@ -344,8 +324,7 @@ class CostItemQuantitiesData:
@classmethod
def process_quantity_names(cls):
props = tool.Sequence.get_work_schedule_props()
active_task_index = props.active_task_index
active_task_index = bpy.context.scene.BIMWorkScheduleProperties.active_task_index
tprops = tool.Sequence.get_task_tree_props()
total_tasks = len(tprops.tasks)
if not total_tasks or active_task_index >= total_tasks:
+23 -51
View File
@@ -21,11 +21,10 @@
import bpy
import ifcopenshell.api
import bonsai.tool as tool
from bpy_extras.io_utils import ImportHelper, ExportHelper
from bpy_extras.io_utils import ImportHelper
import bonsai.tool as tool
import bonsai.core.cost as core
from pathlib import Path
from typing import get_args, TYPE_CHECKING, Literal
from typing import get_args, TYPE_CHECKING
class AddCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
@@ -37,18 +36,16 @@ class AddCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
object_type: bpy.props.StringProperty()
def _execute(self, context):
props = tool.Cost.get_cost_props()
predefined_type = props.cost_schedule_predefined_types
predefined_type = context.scene.BIMCostProperties.cost_schedule_predefined_types
if predefined_type == "USERDEFINED":
predefined_type = self.object_type
core.add_cost_schedule(tool.Ifc, name=self.name, predefined_type=predefined_type)
def draw(self, context):
layout = self.layout
props = tool.Cost.get_cost_props()
layout.prop(self, "name", text="Name")
layout.prop(props, "cost_schedule_predefined_types", text="Type")
if props.cost_schedule_predefined_types == "USERDEFINED":
layout.prop(context.scene.BIMCostProperties, "cost_schedule_predefined_types", text="Type")
if context.scene.BIMCostProperties.cost_schedule_predefined_types == "USERDEFINED":
layout.prop(self, "object_type", text="Object type")
def invoke(self, context, event):
@@ -61,11 +58,10 @@ class EditCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = tool.Cost.get_cost_props()
core.edit_cost_schedule(
tool.Ifc,
tool.Cost,
cost_schedule=tool.Ifc.get().by_id(props.active_cost_schedule_id),
cost_schedule=tool.Ifc.get().by_id(context.scene.BIMCostProperties.active_cost_schedule_id),
)
@@ -268,7 +264,7 @@ class AssignCostItemQuantity(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Assign Cost Item Quantity"
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
related_object_type: bpy.props.EnumProperty( # pyright: ignore [reportRedeclaration]
related_object_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
items=[(i, i, "") for i in get_args(tool.Cost.RELATED_OBJECT_TYPE)],
)
prop_name: bpy.props.StringProperty()
@@ -537,7 +533,6 @@ class SelectCostScheduleProducts(bpy.types.Operator):
class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
bl_idname = "bim.import_cost_schedule_csv"
bl_label = "Import Cost Schedule CSV"
bl_description = "Import cost schdule from the provided .csv file."
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
@@ -552,19 +547,7 @@ class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator)
return True
def _execute(self, context):
cost_schedule = core.import_cost_schedule_csv(tool.Cost, self.filepath, self.is_schedule_of_rates)
core.add_csv_filepath(tool.Cost, self.filepath, self.is_schedule_of_rates, cost_schedule)
return {"FINISHED"}
class RefreshCostScheduleCsv(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.refresh_cost_schedule_csv"
bl_label = "Refresh Cost Schedule CSV"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def _execute(self, context):
core.refresh_cost_schedule_csv(tool.Ifc, tool.Cost)
core.import_cost_schedule_csv(tool.Cost, self.filepath, self.is_schedule_of_rates)
return {"FINISHED"}
@@ -576,8 +559,7 @@ class AddCostColumn(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = tool.Cost.get_cost_props()
if not props.cost_column:
if not context.scene.BIMCostProperties.cost_column:
cls.poll_message_set("Cost column name is empty")
return False
return True
@@ -696,13 +678,12 @@ class CalculateCostItemResourceValue(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
class ExportCostSchedules(bpy.types.Operator, ExportHelper):
class ExportCostSchedules(bpy.types.Operator):
bl_idname = "bim.export_cost_schedules"
bl_label = "Export Cost Schedule"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Export current/all cost schedules as CSV, XSLX or ODS files to the provided directory."
cost_schedule: bpy.props.IntProperty(options={"SKIP_SAVE"})
bl_description = "Export a cost schedule to a CSV, XSLX OR ODS file"
cost_schedule: bpy.props.IntProperty()
format: bpy.props.EnumProperty(name="Format", items=(("CSV", "CSV", ""), ("XLSX", "XLSX", ""), ("ODS", "ODS", "")))
directory: bpy.props.StringProperty(subtype="FILE_PATH")
filter_folder: bpy.props.BoolProperty(
@@ -710,30 +691,20 @@ class ExportCostSchedules(bpy.types.Operator, ExportHelper):
default=True,
)
if TYPE_CHECKING:
cost_schedule: int
format: Literal["CSV", "XLSX", "ODS"]
directory: str
def check(self, context):
if self.filepath != self.directory:
self.filepath = self.directory
return True
return False
@property
def filename_ext(self) -> str:
return f".{self.format.lower()}"
def execute(self, context):
cost_schedule = tool.Ifc.get().by_id(self.cost_schedule) if self.cost_schedule else None
r = core.export_cost_schedules(
tool.Cost, dirpath=self.directory, format=self.format, cost_schedule=cost_schedule
tool.Cost, filepath=self.directory, format=self.format, cost_schedule=cost_schedule
)
if isinstance(r, str):
self.report({"ERROR"}, r)
return {"FINISHED"}
def invoke(self, context, event):
wm = context.window_manager
wm.fileselect_add(self)
return {"RUNNING_MODAL"}
def draw(self, context):
self.layout.label(text="Choose a format")
self.layout.prop(self, "format")
@@ -745,7 +716,7 @@ class ClearCostItemAssignments(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Clear Cost Item Product Assignments"
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
related_object_type: bpy.props.EnumProperty( # pyright: ignore [reportRedeclaration]
related_object_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
items=[(i, i, "") for i in get_args(tool.Cost.RELATED_OBJECT_TYPE)],
)
@@ -769,14 +740,15 @@ class LoadProductCostItems(bpy.types.Operator):
@classmethod
def poll(cls, context):
if not tool.Ifc.get() or not (obj := context.active_object) or not (tool.Blender.get_ifc_definition_id(obj)):
if not tool.Ifc.get() or not (obj := context.active_object) or not (obj.BIMObjectProperties.ifc_definition_id):
cls.poll_message_set("No IFC object is active.")
return False
return True
def execute(self, context):
obj = context.active_object
core.load_product_cost_items(tool.Cost, product=tool.Ifc.get_entity(obj))
core.load_product_cost_items(
tool.Cost, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id)
)
return {"FINISHED"}
+6 -94
View File
@@ -33,7 +33,6 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal
def get_schedule_of_rates(self, context):
@@ -79,8 +78,8 @@ def update_active_cost_item_index(self, context):
CostClassificationsData.load()
def update_cost_item_identification(self: "CostItem", context: bpy.types.Context):
props = tool.Cost.get_cost_props()
def update_cost_item_identification(self, context):
props = context.scene.BIMCostProperties
if not props.is_cost_update_enabled or self.identification == "XXX":
return
self.file = tool.Ifc.get()
@@ -90,12 +89,12 @@ def update_cost_item_identification(self: "CostItem", context: bpy.types.Context
**{"cost_item": self.file.by_id(self.ifc_definition_id), "attributes": {"Identification": self.identification}},
)
if props.active_cost_item_id == self.ifc_definition_id:
attribute = props.cost_item_attributes["Identification"]
attribute = props.cost_item_attributes.get("Identification")
attribute.string_value = self.identification
def update_cost_item_name(self: "CostItem", context: bpy.types.Context) -> None:
props = tool.Cost.get_cost_props()
def update_cost_item_name(self, context):
props = context.scene.BIMCostProperties
if not props.is_cost_update_enabled or self.name == "Unnamed":
return
self.file = tool.Ifc.get()
@@ -105,7 +104,7 @@ def update_cost_item_name(self: "CostItem", context: bpy.types.Context) -> None:
**{"cost_item": self.file.by_id(self.ifc_definition_id), "attributes": {"Name": self.name}},
)
if props.active_cost_item_id == self.ifc_definition_id:
attribute = props.cost_item_attributes["Name"]
attribute = props.cost_item_attributes.get("Name")
attribute.string_value = self.name
@@ -143,14 +142,6 @@ class CostItem(PropertyGroup):
is_expanded: BoolProperty(name="Is Expanded")
level_index: IntProperty(name="Level Index")
if TYPE_CHECKING:
name: str
identification: str
ifc_definition_id: int
has_children: bool
is_expanded: bool
level_index: int
class CostItemQuantity(PropertyGroup):
name: StringProperty(name="Name")
@@ -159,31 +150,11 @@ class CostItemQuantity(PropertyGroup):
unit_symbol: StringProperty(name="Unit Symbol")
total_cost_quantity: FloatProperty(name="Total Quantity")
if TYPE_CHECKING:
name: str
ifc_definition_id: int
total_quantity: float
unit_symbol: str
total_cost_quantity: float
class CostItemsMapping(PropertyGroup):
cost_item_id: IntProperty(name="cost_item_id")
csv_filepath: StringProperty(name="filepath")
if TYPE_CHECKING:
cost_item_id: int
csv_filepath: str
class CostItemType(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
if TYPE_CHECKING:
name: str
ifc_definition_id: int
def update_cost_item_parent(self, context):
cost_item = tool.Cost.get_highlighted_cost_item()
@@ -205,9 +176,6 @@ def update_active_cost_item_resources(self, context):
class ScheduleColumn(PropertyGroup):
schedule_id: IntProperty()
if TYPE_CHECKING:
schedule_id: int
class BIMCostProperties(PropertyGroup):
cost_schedule_predefined_types: EnumProperty(
@@ -281,59 +249,3 @@ class BIMCostProperties(PropertyGroup):
custom_currency: StringProperty(
name="Custom Currency", default="USD", description="Custom Currency in ISO 4217 format"
)
cost_schedule_files: CollectionProperty(name="Cost Schedule Files", type=CostItemsMapping)
if TYPE_CHECKING:
cost_schedule_predefined_types: str
is_cost_update_enabled: bool
cost_schedule_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
is_editing: str
active_cost_schedule_id: int
cost_items: bpy.types.bpy_prop_collection_idprop[CostItem]
active_cost_item_id: int
cost_item_editing_type: str
active_cost_item_index: int
cost_item_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
contracted_cost_items: str
quantity_types: str
product_quantity_names: str
process_quantity_names: str
resource_quantity_names: str
active_cost_item_quantity_id: int
quantity_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
cost_types: Literal["FIXED", "SUM", "CATEGORY"]
cost_category: str
fixed_cost_value: float
active_cost_value_id: int
cost_value_editing_type: str
cost_value_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
cost_value_formula: str
cost_column: str
should_show_column_ui: bool
should_show_currency_ui: bool
columns: bpy.types.bpy_prop_collection_idprop[StrProperty]
columns_storage: bpy.types.bpy_prop_collection_idprop[ScheduleColumn]
active_column_index: int
cost_item_products: bpy.types.bpy_prop_collection_idprop[CostItemQuantity]
active_cost_item_product_index: int
cost_item_processes: bpy.types.bpy_prop_collection_idprop[CostItemQuantity]
active_cost_item_process_index: int
cost_item_resources: bpy.types.bpy_prop_collection_idprop[CostItemQuantity]
active_cost_item_resource_index: int
cost_item_type_products: bpy.types.bpy_prop_collection_idprop[CostItemType]
active_cost_item_type_product_index: int
schedule_of_rates: str
cost_item_rates: bpy.types.bpy_prop_collection_idprop[CostItem]
active_cost_item_rate_index: int
contracted_cost_item_rates: str
product_cost_items: bpy.types.bpy_prop_collection_idprop[CostItemQuantity]
active_product_cost_item_index: int
enable_reorder: bool
show_nested_elements: bool
show_nested_tasks: bool
show_nested_resources: bool
change_cost_item_parent: bool
show_cost_item_operators: bool
currency: str
custom_currency: str
cost_schedule_files: bpy.types.bpy_prop_collection_idprop[CostItemsMapping]
+36 -87
View File
@@ -16,17 +16,13 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import bonsai.bim.helper
import bonsai.bim.module.cost.prop as CostProp
import bonsai.tool as tool
from bpy.types import Panel, UIList
from bonsai.bim.module.cost.data import CostSchedulesData
from typing import Any, TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.cost.prop import BIMCostProperties, CostItemQuantity
from typing import Any
class BIM_PT_cost_schedules(Panel):
@@ -47,7 +43,7 @@ class BIM_PT_cost_schedules(Panel):
if not CostSchedulesData.is_loaded:
CostSchedulesData.load()
self.props = tool.Cost.get_cost_props()
self.props = context.scene.BIMCostProperties
if not self.props.active_cost_schedule_id:
row = self.layout.row(align=True)
if CostSchedulesData.data["total_cost_schedules"]:
@@ -71,26 +67,6 @@ class BIM_PT_cost_schedules(Panel):
text="Currently editing: {}[{}]".format(cost_schedule["name"], cost_schedule["predefined_type"]),
icon="LINENUMBERS_ON",
)
row0 = self.layout.row(align=True)
col = row0.column()
col.label(text="Linked CSV:")
row_1 = col.row(align=True)
if self.props.active_cost_schedule_id in [item.cost_item_id for item in self.props.cost_schedule_files]:
file = next(
(
item.csv_filepath
for item in self.props.cost_schedule_files
if item.cost_item_id == self.props.active_cost_schedule_id
),
None,
)
row_1.label(text=file)
row_1.operator("bim.refresh_cost_schedule_csv", icon="FILE_REFRESH", text="")
else:
row_1.label(text="No CSV file found")
row_1.operator("bim.import_cost_schedule_csv", icon="IMPORT", text="")
grid = self.layout.grid_flow(columns=2, even_columns=True)
col = grid.column()
row1 = col.row(align=True)
@@ -323,7 +299,7 @@ class BIM_PT_cost_item_types(Panel):
@classmethod
def poll(cls, context):
props = tool.Cost.get_cost_props()
props = context.scene.BIMCostProperties
total_cost_items = len(props.cost_items)
if not props.active_cost_schedule_id:
return False
@@ -336,7 +312,7 @@ class BIM_PT_cost_item_types(Panel):
return False
def draw(self, context):
self.props = tool.Cost.get_cost_props()
self.props = context.scene.BIMCostProperties
cost_item = self.props.cost_items[self.props.active_cost_item_index]
grid = self.layout.grid_flow(columns=3, even_columns=True)
@@ -372,8 +348,8 @@ class BIM_PT_cost_item_types(Panel):
row2 = col.row()
row2.template_list(
BIM_UL_cost_item_quantities.__name__,
BIM_UL_cost_item_quantities.__name__ + "_cost_item_processes",
"BIM_UL_cost_item_quantities",
"",
self.props,
"cost_item_processes",
self.props,
@@ -384,34 +360,13 @@ class BIM_PT_cost_item_types(Panel):
# TODO
col = grid.column()
has_quantity_names = CostProp.get_resource_quantity_names(self, context)
row2 = col.row(align=True)
# row2.label(text="Resources")
total_cost_item_resources = len(self.props.cost_item_resources)
row2.label(text="Resources({})".format(total_cost_item_resources))
op = row2.operator("bim.calculate_cost_item_resource_value", text="", icon="DISC")
op.cost_item = cost_item.ifc_definition_id
rtprops = context.scene.BIMResourceTreeProperties
rprops = context.scene.BIMResourceProperties
if rtprops.resources and rprops.active_resource_index < len(rtprops.resources):
if has_quantity_names:
op = row2.operator("bim.assign_cost_item_quantity", text="", icon="PROPERTIES")
op.related_object_type = "RESOURCE"
op.cost_item = cost_item.ifc_definition_id
op.prop_name = self.props.resource_quantity_names
op = row2.operator("bim.assign_cost_item_quantity", text="", icon="ADD")
op.related_object_type = "RESOURCE"
op.cost_item = cost_item.ifc_definition_id
op.prop_name = ""
row2.label(text="Resources")
row2 = col.row()
row2.template_list(
BIM_UL_cost_item_quantities.__name__,
BIM_UL_cost_item_quantities.__name__ + "_cost_item_resources",
"BIM_UL_cost_item_quantities",
"",
self.props,
"cost_item_resources",
self.props,
@@ -430,7 +385,7 @@ class BIM_PT_cost_item_quantities(Panel):
@classmethod
def poll(cls, context):
props = tool.Cost.get_cost_props()
props = context.scene.BIMCostProperties
total_cost_items = len(props.cost_items)
if not props.active_cost_schedule_id:
return False
@@ -443,7 +398,7 @@ class BIM_PT_cost_item_quantities(Panel):
return False
def draw(self, context):
self.props = tool.Cost.get_cost_props()
self.props = context.scene.BIMCostProperties
cost_item = self.props.cost_items[self.props.active_cost_item_index]
@@ -480,8 +435,8 @@ class BIM_PT_cost_item_quantities(Panel):
row2.prop(self.props, "show_nested_elements", text="Show nested")
row2 = col.row()
row2.template_list(
BIM_UL_cost_item_quantities.__name__,
BIM_UL_cost_item_quantities.__name__ + "_cost_item_products",
"BIM_UL_cost_item_quantities",
"",
self.props,
"cost_item_products",
self.props,
@@ -507,7 +462,7 @@ class BIM_PT_cost_item_quantities(Panel):
row2.label(text="Tasks ({})".format(total_cost_item_processes))
tprops = tool.Sequence.get_task_tree_props()
wprops = tool.Sequence.get_work_schedule_props()
wprops = context.scene.BIMWorkScheduleProperties
if tprops.tasks and wprops.active_task_index < len(tprops.tasks):
if has_quantity_names:
op = row2.operator("bim.assign_cost_item_quantity", text="", icon="PROPERTIES")
@@ -525,8 +480,8 @@ class BIM_PT_cost_item_quantities(Panel):
row2 = col.row()
row2.template_list(
BIM_UL_cost_item_quantities.__name__,
BIM_UL_cost_item_quantities.__name__ + "_cost_item_processes",
"BIM_UL_cost_item_quantities",
"",
self.props,
"cost_item_processes",
self.props,
@@ -573,8 +528,8 @@ class BIM_PT_cost_item_quantities(Panel):
row2 = col.row()
row2.template_list(
BIM_UL_cost_item_quantities.__name__,
BIM_UL_cost_item_quantities.__name__ + "_cost_item_resources",
"BIM_UL_cost_item_quantities",
"",
self.props,
"cost_item_resources",
self.props,
@@ -602,7 +557,7 @@ class BIM_PT_cost_item_rates(Panel):
@classmethod
def poll(cls, context):
props = tool.Cost.get_cost_props()
props = context.scene.BIMCostProperties
total_cost_items = len(props.cost_items)
if not props.active_cost_schedule_id:
return False
@@ -615,7 +570,7 @@ class BIM_PT_cost_item_rates(Panel):
return False
def draw(self, context):
self.props = tool.Cost.get_cost_props()
self.props = context.scene.BIMCostProperties
row = self.layout.row(align=True)
row.prop(self.props, "schedule_of_rates", text="")
if self.props.active_cost_item_rate_index < len(self.props.cost_item_rates):
@@ -651,14 +606,13 @@ class BIM_UL_cost_items_trait:
split2.label(text="Quantity")
split2.label(text="Value")
props = tool.Cost.get_cost_props()
for column in props.columns:
for column in bpy.context.scene.BIMCostProperties.columns:
split2.label(text=column.name)
split2.label(text="Total Cost")
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
self.props = tool.Cost.get_cost_props()
self.props = context.scene.BIMCostProperties
cost_item = CostSchedulesData.data["cost_items"][item.ifc_definition_id]
row = layout.row(align=True)
@@ -754,16 +708,18 @@ class BIM_UL_cost_items_trait:
class BIM_UL_cost_items(BIM_UL_cost_items_trait, UIList):
contract_operator = "bim.contract_cost_item"
expand_operator = "bim.expand_cost_item"
def __init__(self):
self.contract_operator = "bim.contract_cost_item"
self.expand_operator = "bim.expand_cost_item"
class BIM_UL_cost_item_rates(BIM_UL_cost_items_trait, UIList):
# A schedule of rates UIList is identical to a regular cost items UIList but
# we want a separate UIList instance so that you can browse both lists
# independently in Blender. So we use a trait.
contract_operator = "bim.contract_cost_item_rate"
expand_operator = "bim.expand_cost_item_rate"
def __init__(self):
self.contract_operator = "bim.contract_cost_item_rate"
self.expand_operator = "bim.expand_cost_item_rate"
def draw_quantity_column(self, layout, cost_item):
self.draw_uom_column(layout, cost_item)
@@ -774,6 +730,7 @@ class BIM_UL_cost_item_rates(BIM_UL_cost_items_trait, UIList):
class BIM_UL_cost_columns(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
props = context.scene.BIMCostProperties
if item:
row = layout.row(align=True)
row.prop(item, "name", emboss=False, text="")
@@ -782,7 +739,7 @@ class BIM_UL_cost_columns(UIList):
class BIM_UL_cost_item_types(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
props = tool.Cost.get_cost_props()
props = context.scene.BIMCostProperties
cost_item = props.cost_items[props.active_cost_item_index]
if item:
@@ -794,17 +751,9 @@ class BIM_UL_cost_item_types(UIList):
class BIM_UL_cost_item_quantities(UIList):
def draw_item(
self,
context: bpy.types.Context,
layout: bpy.types.UILayout,
data: BIMCostProperties,
item: CostItemQuantity,
icon,
active_data,
active_propname,
):
cost_item = data.cost_items[data.active_cost_item_index]
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
props = context.scene.BIMCostProperties
cost_item = props.cost_items[props.active_cost_item_index]
if item:
row = layout.row(align=True)
op = row.operator("bim.select_product", text="", icon="RESTRICT_SELECT_OFF")
@@ -841,15 +790,15 @@ class BIM_PT_Costing_Tools(Panel):
bl_parent_id = "BIM_PT_tab_cost"
def draw(self, context):
props = tool.Cost.get_cost_props()
self.props = context.scene.BIMCostProperties
row = self.layout.row()
row.operator("bim.load_product_cost_items", icon="FILE_REFRESH")
row = self.layout.row()
row.template_list(
"BIM_UL_product_cost_items",
"",
props,
self.props,
"product_cost_items",
props,
self.props,
"active_product_cost_item_index",
)
@@ -18,13 +18,9 @@
import bpy
from bpy.types import PropertyGroup
from typing import TYPE_CHECKING
class BIMCoveringProperties(PropertyGroup):
ceiling_height: bpy.props.FloatProperty(
name="ceiling_height", default=2.7, subtype="DISTANCE", description="Ceiling height"
)
if TYPE_CHECKING:
ceiling_height: float
@@ -24,7 +24,6 @@ import bonsai.tool as tool
from bonsai.bim.helper import prop_with_search
from bonsai.bim.module.model.data import AuthoringData
from bpy.types import WorkSpaceTool
from functools import partial
class CoveringTool(WorkSpaceTool):
@@ -46,7 +45,9 @@ class CoveringTool(WorkSpaceTool):
CoveringToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
add_layout_hotkey = partial(tool.Blender.add_layout_hotkey_operator, tool_name="covering", module_name=__name__)
def add_layout_hotkey(layout: bpy.types.UILayout, text: str, hotkey: str, description: str) -> None:
args = ("covering", layout, text, hotkey, description)
tool.Blender.add_layout_hotkey_operator(*args)
class CoveringToolUI:
@@ -54,7 +55,7 @@ class CoveringToolUI:
def draw(cls, context, layout, ifc_element_type=None):
cls.layout = layout
cls.props = tool.Model.get_model_props()
cls.covering_props = tool.Covering.get_covering_props()
cls.covering_props = context.scene.BIMCoveringProperties
row = cls.layout.row(align=True)
if not tool.Ifc.get():
@@ -87,7 +88,7 @@ class CoveringToolUI:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
row.operator("bim.add_occurrence", text="Add")
row.operator("bim.add_constr_type_instance", text="Add")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
@@ -135,8 +136,8 @@ class CoveringToolUI:
if AuthoringData.data["ifc_classes"]:
if cls.props.ifc_class:
box = cls.layout.box()
if thumbnail := AuthoringData.data["relating_type_data"].get("thumbnail"):
box.template_icon(icon_value=thumbnail, scale=5)
if AuthoringData.data["type_thumbnail"]:
box.template_icon(icon_value=AuthoringData.data["type_thumbnail"], scale=5)
else:
op = box.operator("bim.load_type_thumbnails", text="Load Thumbnails", icon="FILE_REFRESH")
op.ifc_class = cls.props.ifc_class
@@ -144,8 +145,8 @@ class CoveringToolUI:
class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.covering_hotkey"
bl_label = ""
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_label = "Hotkey"
bl_options = {"REGISTER", "UNDO"}
hotkey: bpy.props.StringProperty()
description: bpy.props.StringProperty()
@@ -158,11 +159,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
return operator.description or ""
def _execute(self, context):
self.props = tool.Covering.get_covering_props()
# self.props = context.scene.BIMCoveringProperties
getattr(self, f"hotkey_{self.hotkey}")()
def invoke(self, context, event):
# https://blender.stackexchange.com/questions/276035/how-do-i-make-operators-remember-their-property-values-when-called-from-a-hotkey
# self.props = context.scene.BIMSpatialProperties
return self.execute(context)
def draw(self, context):
@@ -173,22 +175,22 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(active_obj)
container = tool.Root.get_default_container()
if AuthoringData.data["relating_type_data"].get("predefined_type") == "FLOORING":
if AuthoringData.data["predefined_type"] == "FLOORING":
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
bpy.ops.bim.add_instance_flooring_coverings_from_walls()
elif container:
bpy.ops.bim.add_instance_flooring_covering_from_cursor()
else:
bpy.ops.bim.add_occurrence()
elif AuthoringData.data["relating_type_data"].get("predefined_type") == "CEILING":
bpy.ops.bim.add_constr_type_instance()
elif AuthoringData.data["predefined_type"] == "CEILING":
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
bpy.ops.bim.add_instance_ceiling_coverings_from_walls()
elif container:
bpy.ops.bim.add_instance_ceiling_covering_from_cursor()
else:
bpy.ops.bim.add_occurrence()
bpy.ops.bim.add_constr_type_instance()
else:
bpy.ops.bim.add_occurrence()
bpy.ops.bim.add_constr_type_instance()
def hotkey_S_G(self):
active_obj = bpy.context.active_object
@@ -181,12 +181,12 @@ class RunAnalysis(bpy.types.Operator):
if modifier.type == "TRIANGULATE":
return True
def get_covetool_category(self, obj: bpy.types.Object):
def get_covetool_category(self, obj):
if not hasattr(obj, "data") or not isinstance(obj.data, bpy.types.Mesh):
return
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
if not obj.BIMObjectProperties.ifc_definition_id:
return
element = self.file.by_id(ifc_id)
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
ifc_class = element.is_a()
if "IfcSlab" in ifc_class:
return "floors"
+43 -40
View File
@@ -27,7 +27,6 @@ import ifcopenshell
import ifcopenshell.util.selector
import bonsai.tool as tool
import bonsai.bim.module.drawing.scheduler as scheduler
from bpy_extras.io_utils import ExportHelper, ImportHelper
from bonsai.bim.handler import refresh_ui_data
from typing import TYPE_CHECKING
from collections import Counter
@@ -43,8 +42,7 @@ class AddCsvAttribute(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = tool.Blender.get_csv_props()
props.csv_attributes.add()
attribute = context.scene.CsvProperties.csv_attributes.add()
return {"FINISHED"}
@@ -55,8 +53,7 @@ class RemoveCsvAttribute(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
props = tool.Blender.get_csv_props()
props.csv_attributes.remove(self.index)
context.scene.CsvProperties.csv_attributes.remove(self.index)
return {"FINISHED"}
@@ -67,8 +64,7 @@ class RemoveAllCsvAttributes(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = tool.Blender.get_csv_props()
props.csv_attributes.clear()
context.scene.CsvProperties.csv_attributes.clear()
return {"FINISHED"}
@@ -80,9 +76,8 @@ class ReorderCsvAttribute(bpy.types.Operator):
new_index: bpy.props.IntProperty()
def execute(self, context):
props = tool.Blender.get_csv_props()
old = props.csv_attributes[self.old_index]
new = props.csv_attributes[self.new_index]
old = context.scene.CsvProperties.csv_attributes[self.old_index]
new = context.scene.CsvProperties.csv_attributes[self.new_index]
props = ["name", "header", "sort", "group", "varies_value", "summary", "formatting"]
for prop in props:
value = getattr(new, prop)
@@ -91,16 +86,16 @@ class ReorderCsvAttribute(bpy.types.Operator):
return {"FINISHED"}
class ImportCsvAttributes(bpy.types.Operator, ImportHelper):
class ImportCsvAttributes(bpy.types.Operator):
bl_idname = "bim.import_csv_attributes"
bl_label = "Load CSV Settings"
bl_description = "Import a json template for CSV export"
bl_options = {"REGISTER", "UNDO"}
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
props = tool.Blender.get_csv_props()
props = context.scene.CsvProperties
data = json.load(open(self.filepath))
tool.Search.import_filter_query(data["query"], props.filter_groups)
@@ -126,17 +121,22 @@ class ImportCsvAttributes(bpy.types.Operator, ImportHelper):
setattr(new, prop, attribute[prop])
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ExportCsvAttributes(bpy.types.Operator, ExportHelper):
class ExportCsvAttributes(bpy.types.Operator):
bl_idname = "bim.export_csv_attributes"
bl_label = "Save CSV Settings"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Save a json template for CSV export"
filename_ext = ".json"
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
props = tool.Blender.get_csv_props()
props = context.scene.CsvProperties
settings = {}
for prop in [
@@ -175,42 +175,40 @@ class ExportCsvAttributes(bpy.types.Operator, ExportHelper):
return {"FINISHED"}
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ExportIfcCsv(bpy.types.Operator, ExportHelper):
class ExportIfcCsv(bpy.types.Operator):
bl_idname = "bim.export_ifccsv"
bl_label = "Export IFC"
bl_description = "Export IFC data as a spreadsheet."
filename_ext = ".csv"
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
@classmethod
def poll(cls, context):
props = tool.Blender.get_csv_props()
props = context.scene.CsvProperties
if not props.should_load_from_memory and not props.csv_ifc_file:
cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.")
return False
return True
@classmethod
def description(cls, context, properties):
props = tool.Blender.get_csv_props()
if props.format == "web":
return "Open Web UI for spreadsheet data export."
return f"Export IFC data as a spreadsheet by the provided filepath in '{props.format}' format ."
def invoke(self, context, event):
props = tool.Blender.get_csv_props()
props = context.scene.CsvProperties
if props.format == "web":
return self.execute(context)
self.filter_glob = f"*.{props.format}"
self.filename_ext = f".{props.format}"
return ExportHelper.invoke(self, context, event)
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
import ifccsv
props = tool.Blender.get_csv_props()
props = context.scene.CsvProperties
self.filepath = bpy.path.ensure_ext(self.filepath, f".{props.format}")
if props.should_load_from_memory:
ifc_file = tool.Ifc.get()
@@ -289,16 +287,16 @@ class ExportIfcCsv(bpy.types.Operator, ExportHelper):
]
class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.import_ifccsv"
bl_label = "Import to IFC"
bl_description = "Import IFC data from a spreadsheet."
bl_options = {"REGISTER", "UNDO"}
filter_glob: bpy.props.StringProperty(default="*.csv;*.ods;*.xlsx", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
@classmethod
def poll(cls, context):
props = tool.Blender.get_csv_props()
props = context.scene.CsvProperties
if not props.should_load_from_memory and not props.csv_ifc_file:
cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.")
return False
@@ -306,12 +304,14 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".csv")
return ImportHelper.invoke(self, context, event)
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def _execute(self, context):
import ifccsv
props = tool.Blender.get_csv_props()
props = context.scene.CsvProperties
if props.should_load_from_memory:
ifc_file = tool.Ifc.get()
else:
@@ -337,15 +337,18 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
return {"FINISHED"}
class SelectCsvIfcFile(bpy.types.Operator, ImportHelper):
class SelectCsvIfcFile(bpy.types.Operator):
bl_idname = "bim.select_csv_ifc_file"
bl_label = "Select CSV IFC File"
bl_description = "Select IFC file for spreadsheet import/export."
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".ifc"
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
props = tool.Blender.get_csv_props()
props.csv_ifc_file = self.filepath
context.scene.CsvProperties.csv_ifc_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
-33
View File
@@ -29,7 +29,6 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal
class CsvAttribute(PropertyGroup):
@@ -60,15 +59,6 @@ class CsvAttribute(PropertyGroup):
)
formatting: StringProperty(default="{{value}}", name="Formatting")
if TYPE_CHECKING:
name: str
header: str
sort: Literal["NONE", "ASC", "DESC"]
group: Literal["NONE", "GROUP", "CONCAT", "VARIES", "SUM", "AVERAGE", "MIN", "MAX"]
varies_value: str
summary: Literal["NONE", "SUM", "AVERAGE", "MIN", "MAX"]
formatting: str
class CsvProperties(PropertyGroup):
csv_ifc_file: StringProperty(default="", name="IFC File")
@@ -114,26 +104,3 @@ class CsvProperties(PropertyGroup):
name="Load from Memory",
description="Use IFC file currently loaded in Bonsai",
)
if TYPE_CHECKING:
csv_ifc_file: str
ifc_selector: str
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
csv_attributes: bpy.types.bpy_prop_collection_idprop[CsvAttribute]
should_generate_svg: bool
should_preserve_existing: bool
include_global_id: bool
null_value: str
empty_value: str
true_value: str
false_value: str
concat_value: str
csv_delimiter: Literal["NONE", "ASC", "DESC"]
format: Literal["csv", "xlsx", "ods", "web"]
csv_custom_delimiter: str
should_show_settings: bool
should_show_sort: bool
should_show_group: bool
should_show_summary: bool
should_show_formatting: bool
should_load_from_memory: bool
+4 -5
View File
@@ -33,7 +33,9 @@ class BIM_PT_ifccsv(Panel):
def draw(self, context):
layout = self.layout
props = tool.Blender.get_csv_props()
scene = context.scene
props = scene.CsvProperties
if tool.Ifc.get():
row = layout.row(align=True)
@@ -124,8 +126,5 @@ class BIM_PT_ifccsv(Panel):
row.operator("bim.remove_csv_attribute", icon="X", text="").index = index
row = layout.row(align=True)
if props.format == "web":
row.operator("bim.export_ifccsv", icon="EXPORT", text="Open Web UI")
else:
row.operator("bim.export_ifccsv", icon="EXPORT", text="Export IFC to " + props.format.upper())
row.operator("bim.export_ifccsv", icon="EXPORT", text="Export IFC to " + props.format.upper())
row.operator("bim.import_ifccsv", icon="IMPORT")
@@ -44,7 +44,6 @@ classes = (
operator.SelectHighPolygonMeshes,
operator.SelectHighestPolygonMeshes,
operator.ToggleDetailedIOSLogs,
operator.ValidateIfcAssets,
operator.ValidateIfcFile,
prop.BIMDebugProperties,
ui.BIM_PT_debug,
+18 -96
View File
@@ -23,7 +23,6 @@ import time
import random
import logging
import subprocess
import platform
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.geom
@@ -37,8 +36,6 @@ import bonsai.core.profile
import bonsai.core.type
import bonsai.bim.handler
import bonsai.bim.import_ifc as import_ifc
from collections import defaultdict
from bpy_extras.io_utils import ImportHelper, ExportHelper
from pathlib import Path
from bonsai import get_debug_info, format_debug_info
from bonsai.bim.ifc import IfcStore
@@ -153,67 +150,6 @@ class ValidateIfcFile(bpy.types.Operator):
return {"FINISHED"}
class ValidateIfcAssets(bpy.types.Operator):
bl_idname = "bim.validate_ifc_assets"
bl_label = "Validate IFC Assets"
bl_description = (
"Run Bonsai validation for IFC assets.\n\n"
"There's an internal Bonsai convention to treat some IFC assets "
"as unique based on their name (e.g. profiles, materials, styles).\n"
"Though it's not required by IFC, it is a generally good practice "
"to keep asset names unique and it also helps with various issues.\n"
"If it's not conformed, it could lead to duplicated assets or "
"the opposite - different assets of the same name treated as one."
)
bl_options = set()
@classmethod
def poll(cls, context):
if not tool.Ifc.get():
cls.poll_message_set("IFC file is not loaded.")
return False
return True
def execute(self, context):
ifc_file = tool.Ifc.get()
ifc_classes = {
"IfcMaterial": "Name",
"IfcProfileDef": "ProfileName",
"IfcPresentationStyle": "Name",
}
issues_found = False
unique_assets: defaultdict[str, list[ifcopenshell.entity_instance]]
for ifc_class, name_attr in ifc_classes.items():
unique_assets = defaultdict(list)
for asset in ifc_file.by_type(ifc_class):
asset_name: Union[str, None] = getattr(asset, name_attr)
if asset_name is None:
continue
unique_assets[asset_name].append(asset)
msg = ""
for asset_name, assets in unique_assets.items():
if len(assets) == 1:
continue
msg += f"{ifc_class} name '{asset_name}' is used by multiple assets:\n"
for asset in assets:
msg += f"- {asset}\n"
if msg:
issues_found = True
msg = f"Found issues validating {ifc_class} assets.\n" + msg
print(msg)
if issues_found:
self.report({"INFO"}, "Check asset validation results in the system console.")
else:
self.report({"INFO"}, "No asset validation issues found.")
return {"FINISHED"}
class ProfileImportIFC(bpy.types.Operator):
profile_filename = "blender.prof"
bl_idname = "bim.profile_import_ifc"
@@ -446,7 +382,7 @@ class InspectFromObject(bpy.types.Operator):
def get_active_object_ifc_definition(cls, context: bpy.types.Context) -> Union[int, None]:
obj = context.active_object
assert obj
if ifc_id := tool.Blender.get_ifc_definition_id(obj):
if ifc_id := obj.BIMObjectProperties.ifc_definition_id:
return ifc_id
if (
(data := obj.data)
@@ -510,11 +446,12 @@ class ParseExpress(bpy.types.Operator):
return {"FINISHED"}
class SelectExpressFile(bpy.types.Operator, ImportHelper):
class SelectExpressFile(bpy.types.Operator):
bl_idname = "bim.select_express_file"
bl_label = "Select Express File"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select an IFC EXPRESS definition"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.exp", options={"HIDDEN"})
def execute(self, context):
@@ -523,6 +460,10 @@ class SelectExpressFile(bpy.types.Operator, ImportHelper):
props.express_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class PurgeHdf5Cache(bpy.types.Operator):
bl_idname = "bim.purge_hdf5_cache"
@@ -572,7 +513,6 @@ class PrintUnusedElementStats(bpy.types.Operator):
if self.ignore_styled_items:
ignore_classes += ["IfcStyledItem"]
ignore_classes += [
"IfcDocumentReference", # Document references for sheet elements (drawings, schedules, etc).
"IfcIndexedColourMap", # Only referenced by inverse attributes.
"IfcIndexedTextureMap", # Only referenced by inverse attributes.
]
@@ -582,22 +522,21 @@ class PrintUnusedElementStats(bpy.types.Operator):
return {"FINISHED"}
class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator, ExportHelper):
class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.purge_unused_elements_by_class"
bl_label = "Purge Unused Elements By Class"
bl_description = (
"Will find all elements of class that have no inverse references and will remove them, use very carefully.\n"
"If IFC class is provided in neighbour field, will purge only elements of the provided class. Otherwise will purge all white-listed elements.\n"
"If IFC class is provided in neighbour field, will purge only elemnts of the provided class. Otherwise will purge all white-listed elements.\n"
"ALT+CLICK to provide a path where to save the IFC file with the removed elements (note changes will be applied to the current IFC too)"
)
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".ifc"
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.alt:
return ExportHelper.invoke(self, context, event)
context.window_manager.fileselect_add(self)
return self.execute(context)
@classmethod
@@ -674,30 +613,18 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator, ExportHe
"IfcUnitAssignment",
"IfcVirtualGridIntersection",
]
whitelist_exceptions = {
# Document references for sheet elements (drawings, schedules, etc).
"IfcExternalReference": ("IfcDocumentReference",),
}
total_purged = 0
schema = tool.Ifc.schema()
while True:
total_batches = 0
print("*" * 100)
total_batch_purged = 0
for ifc_class in whitelisted_classes:
total_class_purged = 0
# Ensure class is present in the schema.
try:
schema.declaration_by_name(ifc_class)
except RuntimeError:
continue
elements = tool.Ifc.get().by_type(ifc_class)
if ifc_class in whitelist_exceptions:
elements = [e for e in elements if not any(e.is_a(c) for c in whitelist_exceptions[ifc_class])]
elements = tool.Ifc.get().by_type(ifc_class)
except:
continue # Probably not in this schema?
to_purge = set()
for element in elements:
try:
@@ -764,13 +691,13 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
if purged == 0:
return
scene = context.scene
if object_type == "PROFILE":
props = tool.Profile.get_profile_props()
if props.is_editing:
bpy.ops.bim.load_profiles()
elif object_type == "STYLE":
props = tool.Style.get_style_props()
if props.is_editing:
if scene.BIMStylesProperties.is_editing:
bpy.ops.bim.load_styles()
elif object_type == "MATERIAL":
props = tool.Material.get_material_props()
@@ -814,13 +741,13 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
if merged == 0:
return
scene = context.scene
if object_type == "PROFILE":
props = tool.Profile.get_profile_props()
if props.is_editing:
bpy.ops.bim.load_profiles()
elif object_type == "STYLE":
props = tool.Style.get_style_props()
if props.is_editing:
if scene.BIMStylesProperties.is_editing:
bpy.ops.bim.load_styles()
elif object_type == "MATERIAL":
props = tool.Material.get_material_props()
@@ -1023,12 +950,7 @@ class RestartBlender(bpy.types.Operator):
ms_store_app_id = tool.Blender.get_microsoft_store_app_id()
if not ms_store_app_id:
path = bpy.app.binary_path
if platform.system() == "Windows":
args = sys.argv[1:]
command_line = subprocess.list2cmdline([path] + args)
os.execv(path, [command_line])
else:
os.execv(path, sys.argv)
os.execv(path, sys.argv)
else:
# Microsoft apps do not allow launching blender.exe directly
# since Blender folder is kind of private.
-2
View File
@@ -50,8 +50,6 @@ class BIM_PT_debug(Panel):
row.prop(props, "package_name", text="")
row.operator("bim.pip_install", icon="EVENT_PAGEDOWN").name = props.package_name
layout.operator("bim.validate_ifc_assets", icon="CHECKMARK")
row = layout.row()
row.operator("bim.reload_ifc_file", text="Incrementally Reload Changes")
+1 -1
View File
@@ -43,7 +43,7 @@ class DiffData:
@classmethod
def diff_json(cls):
props = tool.Blender.get_diff_props()
props = bpy.context.scene.DiffProperties
if not props.diff_json_file:
cls.diff = None
return
+36 -27
View File
@@ -24,23 +24,24 @@ import ifcopenshell
import bonsai.bim.handler
import bonsai.bim.import_ifc
import bonsai.tool as tool
from bpy_extras.io_utils import ImportHelper, ExportHelper
from bonsai.bim.ifc import IfcStore
class SelectDiffJsonFile(bpy.types.Operator, ImportHelper):
class SelectDiffJsonFile(bpy.types.Operator):
bl_idname = "bim.select_diff_json_file"
bl_label = "Select Diff JSON File"
bl_description = "Select filepath for IFC diff results."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
filename_ext = ".json"
def execute(self, context):
props = tool.Blender.get_diff_props()
props.diff_json_file = self.filepath
context.scene.DiffProperties.diff_json_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class VisualiseDiff(bpy.types.Operator):
bl_idname = "bim.visualise_diff"
@@ -49,8 +50,7 @@ class VisualiseDiff(bpy.types.Operator):
def execute(self, context):
ifc_file = tool.Ifc.get()
props = tool.Blender.get_diff_props()
with open(props.diff_json_file, "r") as file:
with open(context.scene.DiffProperties.diff_json_file, "r") as file:
diff = json.load(file)
for obj in context.visible_objects:
obj.color = (1.0, 1.0, 1.0, 1.0)
@@ -65,7 +65,7 @@ class VisualiseDiff(bpy.types.Operator):
obj.color = (0.0, 0.0, 0.7, 1.0)
continue
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
if not obj.BIMObjectProperties.ifc_definition_id:
continue
ifc_file = ""
@@ -84,7 +84,7 @@ class VisualiseDiff(bpy.types.Operator):
element_file = ifc_file
try:
element = element_file.by_id(ifc_id)
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
except:
continue
global_id = getattr(element, "GlobalId", None)
@@ -101,45 +101,55 @@ class VisualiseDiff(bpy.types.Operator):
return {"FINISHED"}
class SelectDiffOldFile(bpy.types.Operator, ImportHelper):
class SelectDiffOldFile(bpy.types.Operator):
bl_idname = "bim.select_diff_old_file"
bl_label = "Select Diff Old File"
bl_description = "Select filepath for an old IFC file to compare."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
filename_ext = ".ifc"
def execute(self, context):
props = tool.Blender.get_diff_props()
props.old_file = self.filepath
context.scene.DiffProperties.old_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectDiffNewFile(bpy.types.Operator, ImportHelper):
class SelectDiffNewFile(bpy.types.Operator):
bl_idname = "bim.select_diff_new_file"
bl_label = "Select Diff New File"
bl_description = "Select filepath for a new IFC file to compare."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
filename_ext = ".ifc"
def execute(self, context):
props = tool.Blender.get_diff_props()
props.new_file = self.filepath
context.scene.DiffProperties.new_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ExecuteIfcDiff(bpy.types.Operator, ExportHelper):
class ExecuteIfcDiff(bpy.types.Operator):
bl_idname = "bim.execute_ifc_diff"
bl_label = "Execute IFC Diff"
bl_description = "Compare two IFC files and save a json diff report by the provided filepath."
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
import ifcdiff
self.props = tool.Blender.get_diff_props()
self.props = context.scene.DiffProperties
if tool.Ifc.get():
if self.props.active_file == "NONE":
@@ -228,8 +238,7 @@ class SelectDiffObjects(bpy.types.Operator):
def execute(self, context):
ifc_file = tool.Ifc.get()
props = tool.Blender.get_diff_props()
with open(props.diff_json_file, "r") as file:
with open(context.scene.DiffProperties.diff_json_file, "r") as file:
diff = json.load(file)
for obj in context.visible_objects:
obj.select_set(False)
@@ -244,7 +253,7 @@ class SelectDiffObjects(bpy.types.Operator):
obj.select_set(True)
continue
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
if not obj.BIMObjectProperties.ifc_definition_id:
continue
ifc_file = ""
@@ -263,7 +272,7 @@ class SelectDiffObjects(bpy.types.Operator):
element_file = ifc_file
try:
element = element_file.by_id(ifc_id)
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
except:
continue
global_id = getattr(element, "GlobalId", None)
+2 -18
View File
@@ -30,25 +30,18 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal, get_args
def update_diff_json_file(self: "DiffProperties", context: bpy.types.Context) -> None:
def update_diff_json_file(self, context):
DiffData.data["diff_json"] = DiffData.diff_json()
RelationshipType = Literal["type", "property", "container", "aggregate", "classification"]
class Relationships(PropertyGroup):
relationship: EnumProperty(
name="Relationship",
items=[(r, r.capitalize(), r) for r in get_args(RelationshipType)],
items=[(r, r.capitalize(), r) for r in ["type", "property", "container", "aggregate", "classification"]],
)
if TYPE_CHECKING:
relationship: RelationshipType
class DiffProperties(PropertyGroup):
diff_json_file: StringProperty(default="", name="JSON Output", update=update_diff_json_file)
@@ -66,12 +59,3 @@ class DiffProperties(PropertyGroup):
name="Active File",
default="NEW",
)
if TYPE_CHECKING:
diff_json_file: str
old_file: str
new_file: str
diff_relationships: bpy.types.bpy_prop_collection_idprop[Relationships]
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
should_load_changed_elements: bool
active_file: Literal["NONE", "OLD", "NEW"]
+3 -1
View File
@@ -37,7 +37,9 @@ class BIM_PT_diff(Panel):
layout = self.layout
layout.use_property_split = True
props = tool.Blender.get_diff_props()
scene = context.scene
props = scene.DiffProperties
layout.label(text="IFC Diff Setup:")
@@ -132,14 +132,13 @@ class RemoveDocument(bpy.types.Operator, tool.Ifc.Operator):
class AssignDocument(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_document"
bl_label = "Assign Document"
bl_description = "Assign active document to the selected objects."
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
document: bpy.props.IntProperty()
def _execute(self, context):
document = tool.Ifc.get().by_id(self.document)
objs = [bpy.data.objects[self.obj]] if self.obj else tool.Blender.get_selected_objects()
objs = [bpy.data.objects.get(self.obj)] if self.obj else tool.Blender.get_selected_objects()
for obj in objs:
element = tool.Ifc.get_entity(obj)
if element:
@@ -30,7 +30,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Union
from typing import TYPE_CHECKING
def update_document_name(self: "Document", context: bpy.types.Context) -> None:
@@ -79,7 +79,3 @@ class BIMDocumentProperties(PropertyGroup):
breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
active_document_index: int
is_editing: bool
@property
def active_document(self) -> Union[Document, None]:
return tool.Blender.get_active_uilist_element(self.documents, self.active_document_index)
+6 -11
View File
@@ -61,17 +61,14 @@ class BIM_PT_documents(Panel):
if self.props.breadcrumbs:
row.operator("bim.add_document_reference", text="", icon="FILE_HIDDEN")
active_document = self.props.active_document
if self.props.active_document_id:
row.operator("bim.edit_document", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_document", text="", icon="CANCEL")
elif active_document:
ifc_definition_id = active_document.ifc_definition_id
elif self.props.documents and self.props.active_document_index < len(self.props.documents):
ifc_definition_id = self.props.documents[self.props.active_document_index].ifc_definition_id
row.operator("bim.select_document_objects", text="", icon="RESTRICT_SELECT_OFF").document = (
ifc_definition_id
)
row.operator("bim.assign_document", text="", icon="BRUSH_DATA").document = ifc_definition_id
row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL").document = ifc_definition_id
row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id
@@ -93,20 +90,18 @@ class BIM_PT_object_documents(Panel):
@classmethod
def poll(cls, context):
if not (obj := context.active_object):
if not context.active_object:
return False
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id):
return False
if not tool.Ifc.get_object_by_identifier(ifc_id):
return False
return True
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
def draw(self, context):
if not ObjectDocumentData.is_loaded:
ObjectDocumentData.load()
obj = context.active_object
self.oprops = tool.Blender.get_object_bim_props(obj)
self.oprops = obj.BIMObjectProperties
self.props = tool.Document.get_document_props()
self.file = tool.Ifc.get()
@@ -28,6 +28,7 @@ classes = (
operator.AddAnnotationType,
operator.AddDrawing,
operator.AddDrawingStyle,
operator.AddDrawingStyleAttribute,
operator.AddDrawingToSheet,
operator.AddReference,
operator.AddReferenceImage,
@@ -79,10 +80,12 @@ classes = (
operator.RemoveDrawing,
operator.RemoveDrawingFromSheet,
operator.RemoveDrawingStyle,
operator.RemoveDrawingStyleAttribute,
operator.RemoveReference,
operator.RemoveSchedule,
operator.RemoveSheet,
operator.RemoveTextLiteral,
operator.ResizeText,
operator.SaveDrawingStyle,
operator.SaveDrawingStylesData,
operator.SelectAllDrawings,
@@ -29,6 +29,52 @@ from typing import Optional
class Annotator:
@staticmethod
def add_text(related_element: Optional[ifcopenshell.entity_instance] = None) -> bpy.types.Object:
curve = bpy.data.curves.new(type="FONT", name="Text")
curve.body = "TEXT"
obj = bpy.data.objects.new("Text", curve)
obj.matrix_world = bpy.context.scene.camera.matrix_world
if related_element is None:
location, _, _, _ = Annotator.get_placeholder_coords(bpy.context)
else:
curve.BIMAssignedProductProperties.related_element = related_element
location = related_element.location
obj.location = location
obj.hide_render = True
font = bpy.data.fonts.get("OpenGost TypeB TT")
if not font:
font = bpy.data.fonts.load(
tool.Blender.get_data_dir_path(Path("fonts") / "OpenGost Type B TT.ttf").__str__()
)
font.name = "OpenGost Type B TT"
curve.font = font
props = tool.Drawing.get_text_props(obj)
props.font_size = "2.5"
collection = bpy.context.scene.camera.BIMObjectProperties.collection
collection.objects.link(obj)
Annotator.resize_text(obj)
return obj
@staticmethod
def resize_text(text_obj: bpy.types.Object) -> None:
camera = None
group = tool.Drawing.get_drawing_group(tool.Ifc.get_entity(text_obj))
for element in tool.Drawing.get_drawing_elements(group):
if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
camera = tool.Ifc.get_object(element)
break
if not camera:
return
# This is a magic number for OpenGost
font_size = 1.6 / 1000
props = tool.Drawing.get_text_props(text_obj)
font_size *= float(props.font_size)
font_size /= tool.Drawing.get_scale_ratio(tool.Drawing.get_diagram_scale(camera)["Scale"])
text_obj.data.size = font_size
@staticmethod
def add_line_to_annotation(
obj: bpy.types.Object, co1: Optional[Vector] = None, co2: Optional[Vector] = None
@@ -83,17 +129,11 @@ class Annotator:
def get_annotation_obj(
drawing: ifcopenshell.entity_instance, object_type: str, data_type: tool.Drawing.ANNOTATION_DATA_TYPE
) -> bpy.types.Object:
assert bpy.context.scene
camera = tool.Ifc.get_object(drawing)
assert isinstance(camera, bpy.types.Object)
# those annotations you want to obey the depth of the 3d cursor
if object_type == "PLAN_LEVEL":
co1 = bpy.context.scene.cursor.location.copy()
else:
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
matrix_world = tool.Drawing.get_camera_matrix(camera)
matrix_world.translation = co1
collection = tool.Blender.get_object_bim_props(camera).collection
collection = camera.BIMObjectProperties.collection
if object_type == "TEXT":
obj = bpy.data.objects.new(object_type, None)
+40 -100
View File
@@ -34,7 +34,9 @@ def refresh():
DrawingsData.is_loaded = False
ElementFiltersData.is_loaded = False
AnnotationData.is_loaded = False
DecoratorData.is_loaded = False
DecoratorData.data = {}
DecoratorData.cut_cache = {}
DecoratorData.layerset_cache = {}
class ProductAssignmentsData:
@@ -118,7 +120,7 @@ class DrawingsData:
return len([e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"])
@classmethod
def location_hint(cls) -> list[tuple[tool.Drawing.LocationHintType, str, str]]:
def location_hint(cls):
props = tool.Drawing.get_document_props()
if props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
results = [("0", "Origin", "")]
@@ -132,10 +134,12 @@ class DrawingsData:
@classmethod
def active_drawing_pset_data(cls):
ifc_file = tool.Ifc.get()
props = tool.Drawing.get_document_props()
drawing = props.get_active_drawing()
if drawing is None:
drawing_id = props.active_drawing_id
if drawing_id == 0:
return {}
drawing = ifc_file.by_id(drawing_id)
return ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing")
@@ -232,43 +236,7 @@ class DecoratorData:
# stores 1 type of data per object
data = {}
cut_cache = {}
slice_cache = {}
fill_cache = {}
@classmethod
def load(cls, handler):
cls.is_loaded = True
cls.cut_cache = {}
cls.layerset_cache = {}
text = {}
dimension = {}
fall = {}
symbol = {}
for obj in bpy.context.visible_objects:
if not (element := tool.Ifc.get_entity(obj)):
continue
if tool.Drawing.is_annotation_object_type(element, ("TEXT", "TEXT_LEADER")):
text[obj.name] = cls.get_text_data(obj)
if text[obj.name]["Symbol"]:
symbol[obj.name] = cls.get_symbol_data(obj)
elif tool.Drawing.is_annotation_object_type(
element, ("DIMENSION", "DIAMETER", "SECTION_LEVEL", "PLAN_LEVEL", "RADIUS")
):
dimension[obj.name] = cls.get_dimension_data(obj)
elif tool.Drawing.is_annotation_object_type(
element, ("FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT")
):
fall[obj.name] = cls.get_fall_data(obj)
elif tool.Drawing.is_annotation_object_type(element, ("SYMBOL",)):
symbol[obj.name] = cls.get_symbol_data(obj)
cls.data = {
"text": text,
"dimension": dimension,
"fall": fall,
"symbol": symbol,
"object_decorators": cls.object_decorators(handler),
}
layerset_cache = {}
@classmethod
def get_batting_thickness(cls, obj):
@@ -327,10 +295,17 @@ class DecoratorData:
return display_data
@classmethod
def get_text_data(cls, obj: bpy.types.Object) -> dict:
def get_ifc_text_data(cls, obj: bpy.types.Object) -> dict[str, Any]:
"""used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER"\n
returns font size in mm for current ifc text object"""
result = cls.data.get(obj.name, None)
if result is not None:
return result
element = tool.Ifc.get_entity(obj)
if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
return None
props = tool.Drawing.get_text_props(obj)
# getting font size
pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {}
@@ -368,7 +343,14 @@ class DecoratorData:
literals_data.append(literal_data)
return {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at}
text_data = {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at}
cls.data[obj.name] = text_data
return text_data
@classmethod
def get_symbol(cls, obj: bpy.types.Object) -> Union[str, None]:
"""used by IfcAnnotations with ObjectType MULTI_SYMBOL"""
return tool.Drawing.get_annotation_symbol(tool.Ifc.get_entity(obj))
@classmethod
def get_dimension_data(cls, obj):
@@ -376,7 +358,19 @@ class DecoratorData:
DIMENSION / DIAMETER / SECTION_LEVEL / PLAN_LEVEL / RADIUS
"""
result = cls.data.get(obj.name, None)
if result is not None:
return result
element = tool.Ifc.get_entity(obj)
supported_object_types = ("DIMENSION", "DIAMETER", "SECTION_LEVEL", "PLAN_LEVEL", "RADIUS")
if (
not element
or not element.is_a("IfcAnnotation")
or ifcopenshell.util.element.get_predefined_type(element) not in supported_object_types
):
return None
dimension_style = "arrow"
fill_bg = False
classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
@@ -392,71 +386,17 @@ class DecoratorData:
suppress_zero_inches = pset_data.get("SuppressZeroInches", False)
text_prefix = pset_data.get("TextPrefix", None) or ""
text_suffix = pset_data.get("TextSuffix", None) or ""
custom_unit_list = pset_data.get("CustomUnit", None) or ""
custom_unit = custom_unit_list[0] if custom_unit_list else ""
return {
dimension_data = {
"dimension_style": dimension_style,
"show_description_only": show_description_only,
"suppress_zero_inches": suppress_zero_inches,
"text_prefix": text_prefix,
"text_suffix": text_suffix,
"fill_bg": fill_bg,
"custom_unit": custom_unit,
}
@classmethod
def get_fall_data(cls, obj):
object_type = None
if element := tool.Ifc.get_entity(obj):
object_type = ifcopenshell.util.element.get_predefined_type(element)
return {"object_type": object_type}
@classmethod
def get_symbol_data(cls, obj):
return tool.Drawing.get_annotation_symbol(tool.Ifc.get_entity(obj))
@classmethod
def object_decorators(cls, handler):
import bonsai.bim.module.drawing.decoration
if not bonsai.bim.module.drawing.decoration.DecorationsHandler.installed:
return []
props = tool.Drawing.get_document_props()
if (drawing := props.get_active_drawing()) is None:
return []
camera = tool.Ifc.get_object(drawing)
assert isinstance(camera, bpy.types.Object)
collection = tool.Blender.get_object_bim_props(camera).collection
assert collection
results = []
viewport = tool.Blender.get_view3d_space()
for obj in collection.all_objects:
if not obj.visible_get(viewport=viewport):
continue
element = tool.Ifc.get_entity(obj)
if not element:
continue
if not element.is_a("IfcAnnotation"):
continue
object_type: Union[str, None] = ifcopenshell.util.element.get_predefined_type(element)
if object_type == "DRAWING":
continue
if dec := handler.decorators.get(object_type, None):
results.append((obj, dec))
elif isinstance(obj.data, bpy.types.Mesh):
if object_type == "LINEWORK" and "dashed" in str(
ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
).split(" "):
results.append((obj, handler.decorators["HIDDEN_LINE"]))
else:
results.append((obj, handler.decorators["MISC"]))
return results
cls.data[obj.name] = dimension_data
return dimension_data
class AnnotationData:
+369 -250
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import os
import gpu
import bpy
import blf
@@ -28,7 +29,7 @@ import ifcopenshell.util.unit
import bonsai.tool as tool
import bonsai.bim.module.drawing.helper as helper
from pathlib import Path
from math import pi, sin, cos, acos, atan, degrees, radians
from math import pi, sin, cos, tan, acos, atan, degrees, radians, ceil
from bpy.types import SpaceView3D
from mathutils import Vector, Matrix
from bpy_extras.view3d_utils import location_3d_to_region_2d
@@ -318,8 +319,6 @@ class BaseDecorator:
color: tuple[float, float, float, float],
indices: Optional[list[tuple[int, int]]] = None,
) -> None:
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.base_shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
@@ -496,20 +495,14 @@ class BaseDecorator:
self.draw_label(context, text=text, line_no=line_number_start, multiline=True, **draw_label_kwargs)
@lru_cache(maxsize=None)
def format_value(self, context, value, custom_unit=None):
def format_value(self, context, value):
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
precision = drawing_pset_data.get("MetricPrecision", None)
if not precision:
precision = drawing_pset_data.get("ImperialPrecision", None)
decimal_places = drawing_pset_data.get("DecimalPlaces", None)
return format_distance(
value,
precision=precision,
decimal_places=decimal_places,
suppress_zero_inches=True,
custom_unit=custom_unit,
)
return format_distance(value, precision=precision, decimal_places=decimal_places)
def draw_asterisk(self, context: bpy.types.Context, pos: Vector, rotation: float = 0.0, scale: float = 1.0) -> None:
"""`pos` is a world space position\n
@@ -562,7 +555,8 @@ class BaseDecorator:
MiscDecorator.decorate(self, context, obj)
return
if not (symbol := DecoratorData.data["symbol"].get(obj.name, None)):
symbol = DecoratorData.get_symbol(obj)
if not symbol:
return
for vert in mesh.vertices:
@@ -571,7 +565,8 @@ class BaseDecorator:
return
# EMPTY objects
if not (symbol := DecoratorData.data["symbol"].get(obj.name, None)):
symbol = DecoratorData.get_symbol(obj)
if not symbol:
return
rotation = -Vector((1, 0)).angle_signed(annotation_dir)
@@ -588,16 +583,15 @@ class BaseDecorator:
"""if `text_world_position` is not provided, the object's location will be used"""
if not text_world_position:
text_world_position = obj.matrix_world.translation
text_world_position = obj.location
region = context.region
region3d = context.region_data
text_dir = self.get_annotation_direction(context, obj)
if not (pos := location_3d_to_region_2d(region, region3d, text_world_position)):
return
pos = location_3d_to_region_2d(region, region3d, text_world_position)
props = tool.Drawing.get_text_props(obj)
text_data = DecoratorData.data["text"].get(obj.name, None)
text_data = DecoratorData.get_ifc_text_data(obj)
if props.is_editing:
text_data = text_data | props.get_text_edited_data()
literals_data = text_data["Literals"]
@@ -654,10 +648,7 @@ class DimensionDecorator(BaseDecorator):
viewportDrawingScale = self.get_viewport_drawing_scale(context)
# setup geometry parameters
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
if not dimension_data:
return
dimension_style = dimension_data["dimension_style"]
dimension_style = DecoratorData.get_dimension_data(obj)["dimension_style"]
if dimension_style == "oblique":
size = viewportDrawingScale * 10 # OLBIQUE_SYMBOL_SIZE
angle = radians(45)
@@ -719,9 +710,7 @@ class DimensionDecorator(BaseDecorator):
element = tool.Ifc.get_entity(obj)
description = element.Description
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
if not dimension_data:
return
dimension_data = DecoratorData.get_dimension_data(obj)
show_description_only = dimension_data["show_description_only"]
text_prefix = dimension_data["text_prefix"]
text_suffix = dimension_data["text_suffix"]
@@ -733,8 +722,6 @@ class DimensionDecorator(BaseDecorator):
v1 = Vector(vertices[i1])
p0 = location_3d_to_region_2d(region, region3d, v0)
p1 = location_3d_to_region_2d(region, region3d, v1)
if not p0 or not p1:
return
text_dir = p1 - p0
if text_dir.length < 1:
continue
@@ -750,7 +737,7 @@ class DimensionDecorator(BaseDecorator):
if not show_description_only:
length = (v1 - v0).length
text = self.format_value(context, length, custom_unit=dimension_data["custom_unit"])
text = self.format_value(context, length)
if isinstance(self, DiameterDecorator):
text = "D" + text
text = text_prefix + text + text_suffix
@@ -885,8 +872,6 @@ class AngleDecorator(BaseDecorator):
# calculate angle position
p0, p1, p2 = [location_3d_to_region_2d(region, region3d, p) for p in vertices[i0 : i1 + 2]]
if not p0 or not p1 or not p2:
continue
edge0 = p0 - p1
edge1 = p2 - p1
radius = min(edge0.length_squared, edge1.length_squared) ** 0.5
@@ -951,13 +936,9 @@ class RadiusDecorator(BaseDecorator):
spline_points = self.get_spline_points(obj)
p0, p1 = [location_3d_to_region_2d(region, region3d, p) for p in self.get_spline_points(obj)]
if not p0 or not p1:
return
element = tool.Ifc.get_entity(obj)
description = element.Description
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
if not dimension_data:
return
dimension_data = DecoratorData.get_dimension_data(obj)
viewportDrawingScale = self.get_viewport_drawing_scale(context)
text_offset = 20 * viewportDrawingScale
@@ -965,7 +946,7 @@ class RadiusDecorator(BaseDecorator):
def get_text():
length = (spline_points[-1] - spline_points[-2]).length
return "R" + self.format_value(context, length, custom_unit=dimension_data["custom_unit"])
return "R" + self.format_value(context, length)
self.draw_dimension_text(
context, get_text, description, dimension_data, pos=pos, text_dir=Vector((1, 0)), box_alignment="center"
@@ -987,8 +968,7 @@ class FallDecorator(BaseDecorator):
region = context.region
region3d = context.region_data
dir = Vector((1, 0))
if not (pos := location_3d_to_region_2d(region, region3d, self.get_spline_end(obj))):
return
pos = location_3d_to_region_2d(region, region3d, self.get_spline_end(obj))
spline = obj.data.splines[0]
spline_points = spline.bezier_points if spline.bezier_points else spline.points
@@ -996,6 +976,8 @@ class FallDecorator(BaseDecorator):
# generate label text
# same function as in svgwriter.py
def get_label_text():
element = tool.Ifc.get_entity(obj)
assert element
B, A = [v.co.xyz for v in spline_points[:2]]
rise = abs(A.z - B.z)
O = A.copy()
@@ -1007,8 +989,8 @@ class FallDecorator(BaseDecorator):
else:
angle = 90
# uses SLOPE_ANGLE as default
object_type = DecoratorData.data["fall"].get(obj, {}).get("object_type", None)
# ues SLOPE_ANGLE as default
object_type = ifcopenshell.util.element.get_predefined_type(element)
if object_type in ("FALL", "SLOPE_ANGLE"):
return f"{angle}°"
elif object_type == "SLOPE_FRACTION":
@@ -1019,7 +1001,6 @@ class FallDecorator(BaseDecorator):
if angle == 90:
return "-"
return f"{round(angle_tg * 100)} %"
return "NO DATA"
if spline_points:
text = get_label_text()
@@ -1178,19 +1159,16 @@ class PlanLevelDecorator(BaseDecorator):
self.draw_labels(context, obj, self.get_splines(obj))
def draw_labels(self, context, obj, splines):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
region = context.region
region3d = context.region_data
element = tool.Ifc.get_entity(obj)
description = element.Description
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
if not dimension_data:
return
dimension_data = DecoratorData.get_dimension_data(obj)
for verts in splines:
p0, p1 = [location_3d_to_region_2d(region, region3d, v) for v in verts[:2]]
if not p0 or not p1:
continue
text_dir = p1 - p0
if text_dir.length < 1:
continue
@@ -1268,9 +1246,7 @@ class SectionLevelDecorator(BaseDecorator):
storey = tool.Drawing.get_annotation_element(element)
tag = storey.Name if storey else element.Description
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
if not dimension_data:
return
dimension_data = DecoratorData.get_dimension_data(obj)
for verts in splines:
@@ -1459,10 +1435,8 @@ class GridDecorator(BaseDecorator):
p1 = location_3d_to_region_2d(region, region3d, v1)
dir = Vector((1, 0))
text = obj.name.split("/")[1].split(".")[0]
if p0:
self.draw_label(context, text, p0, dir, vcenter=True, gap=0)
if p1:
self.draw_label(context, text, p1, dir, vcenter=True, gap=0)
self.draw_label(context, text, p0, dir, vcenter=True, gap=0)
self.draw_label(context, text, p1, dir, vcenter=True, gap=0)
class ElevationDecorator(BaseDecorator):
@@ -1615,9 +1589,6 @@ class CutDecorator:
@classmethod
def install(cls, context):
DecoratorData.cut_cache.clear()
DecoratorData.slice_cache.clear()
DecoratorData.fill_cache.clear()
if cls.installed:
cls.uninstall()
handler = cls()
@@ -1632,56 +1603,38 @@ class CutDecorator:
cls.installed = None
def __call__(self, context):
if not context.scene.camera:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
selected_elements_color = self.addon_prefs.decorator_color_selected
self.fallback_colour = (0.3, 0.3, 0.3, 1)
all_vertices = []
all_edges = []
selected_vertices = []
selected_edges = []
fills = {}
layer_vertices = []
layer_edges = []
all_vertex_i_offset = 0
selected_vertex_i_offset = 0
classes_no_cut_str = tool.Drawing.get_document_props().classes_no_cut
classes_no_cut = [word.strip() for word in classes_no_cut_str.split(",")]
layer_vertex_i_offset = 0
for obj in [o for o in bpy.context.visible_objects if o.type == "MESH"]:
if not (element := tool.Ifc.get_entity(obj)):
verts, edges = self.decorate(context, obj)
if not verts:
continue
# Skip the elements in the following classes
if element.is_a() in classes_no_cut:
continue
self.decorate(context, obj, element)
verts, edges = DecoratorData.cut_cache[element.id()]
if verts:
if obj.select_get():
selected_vertices.extend(verts)
selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges])
selected_vertex_i_offset += len(verts)
else:
all_vertices.extend(verts)
all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges])
all_vertex_i_offset += len(verts)
obj_layer_verts, obj_layer_edges = self.slice_layersets(context, obj, verts, edges)
if obj_layer_verts:
layer_vertices.extend(obj_layer_verts)
layer_edges.extend([[vi + layer_vertex_i_offset for vi in e] for e in obj_layer_edges])
layer_vertex_i_offset += len(obj_layer_verts)
verts, edges = DecoratorData.slice_cache.get(element.id(), (None, None))
if verts:
if obj.select_get():
selected_vertices.extend(verts)
selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges])
selected_vertex_i_offset += len(verts)
else:
all_vertices.extend(verts)
all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges])
all_vertex_i_offset += len(verts)
for colour, element_fills in DecoratorData.fill_cache[element.id()].items():
fills.setdefault(colour, []).append(element_fills)
if obj.select_get():
selected_vertices.extend(verts)
selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges])
selected_vertex_i_offset += len(verts)
else:
all_vertices.extend(verts)
all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges])
all_vertex_i_offset += len(verts)
gpu.state.point_size_set(1)
gpu.state.blend_set("ALPHA")
@@ -1699,10 +1652,9 @@ class CutDecorator:
black = (0, 0, 0, 1)
for colour, element_fills in fills.items():
for verts_tris in element_fills:
for verts, tris in verts_tris:
self.draw_batch("TRIS", verts, colour, tris)
if layer_vertices:
self.draw_batch("LINES", layer_vertices, black, layer_edges)
self.draw_batch("POINTS", layer_vertices, black)
gpu.state.point_size_set(2)
self.line_shader.uniform_float("lineWidth", 3.0)
@@ -1715,183 +1667,315 @@ class CutDecorator:
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def decorate(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
has_cut_cache = element.id() in DecoratorData.cut_cache
has_fill_cache = element.id() in DecoratorData.fill_cache
def decorate(self, context, obj):
element = tool.Ifc.get_entity(obj)
if not element:
return None, None
# Currently selected objects must be recalculated as they may be being moved / edited.
# If the camera is selected, we also recalculate as the user may be moving the camera.
if not has_cut_cache or obj.select_get() or context.scene.camera.select_get():
self.recalculate_cut(context, obj, element)
if not has_fill_cache or obj.select_get() or context.scene.camera.select_get():
self.recalculate_fill(context, obj, element)
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
if tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
DecoratorData.cut_cache[element.id()] = (verts, edges)
# Currently selected objects shall not be cached as they may be being moved / edited.
# If the camera is selected, we also disable the cache as the user may be moving the camera.
if obj.select_get() or context.scene.camera.select_get():
verts, edges = None, None
else:
DecoratorData.cut_cache[element.id()] = (False, False)
verts, edges = DecoratorData.cut_cache.get(element.id(), (None, None))
def recalculate_fill(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
element_id = element.id()
if verts is False:
return None, None
elif verts:
return verts, edges
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
DecoratorData.fill_cache[element_id] = {}
return
DecoratorData.cut_cache[element.id()] = (False, False)
return None, None
DecoratorData.fill_cache[element_id] = {}
if verts is None:
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
DecoratorData.cut_cache[element.id()] = (verts, edges)
return verts, edges
mesh = obj.data
bm_original = bmesh.new()
bm_original.from_mesh(mesh)
def slice_layersets(self, context, obj, cut_verts, cut_edges):
element = tool.Ifc.get_entity(obj)
# Slice our mesh into a 2D drawing cut (2D is always easier)
camera_matrix = obj.matrix_world.inverted() @ context.scene.camera.matrix_world
global_offset = context.scene.camera.matrix_world.col[2].xyz * -context.scene.camera.data.clip_start
plane_co = camera_matrix.translation
plane_no = camera_matrix.col[2].xyz
geom = bm_original.verts[:] + bm_original.edges[:] + bm_original.faces[:]
bmesh.ops.bisect_plane(
bm_original,
geom=geom,
dist=0.0001,
plane_co=plane_co,
plane_no=plane_no,
clear_outer=True,
clear_inner=True,
)
bmesh.ops.remove_doubles(bm_original, verts=bm_original.verts, dist=0.000001)
bmesh.ops.triangle_fill(bm_original, use_dissolve=True, edges=bm_original.edges)
# Currently selected objects shall not be cached as they may be being moved / edited.
# If the camera is selected, we also disable the cache as the user may be moving the camera.
if obj.select_get() or context.scene.camera.select_get():
verts, edges = None, None
else:
verts, edges = DecoratorData.layerset_cache.get(element.id(), (None, None))
if not (material := ifcopenshell.util.element.get_material(element)):
verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera)
DecoratorData.fill_cache[element_id].setdefault(self.fallback_colour, []).append((verts, tris))
return
if verts is False:
return None, None
elif verts is not None:
return verts, edges
if material.is_a() not in ("IfcMaterialLayerSet", "IfcMaterialLayerSetUsage"):
# Constituents, lists, and item styles not supported yet
material = ifcopenshell.util.element.get_materials(element)[0]
verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera)
colour = self.get_material_colour(material)
DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris))
return
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
DecoratorData.layerset_cache[element.id()] = (False, False)
return None, None
if tool.Model.get_usage_type(element) != "LAYER2":
DecoratorData.layerset_cache[element.id()] = (False, False)
return None, None
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
layers = self.get_layer_data(element)
if material.is_a("IfcMaterialLayerSetUsage"):
usage = material
layer_set = material.ForLayerSet
offset = usage.OffsetFromReferenceLine * self.unit_scale
sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1
elif material.is_a("IfcMaterialLayerSet"):
usage = None
layer_set = material
offset = 0
sense_factor = 1
if not layers:
DecoratorData.layerset_cache[element.id()] = (False, False)
return None, None
if len(layer_set.MaterialLayers) == 1:
material = layer_set.MaterialLayers[0].Material
verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera)
colour = self.get_material_colour(material)
DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris))
return
minx = min([co[0] for co in obj.bound_box])
maxx = max([co[0] for co in obj.bound_box])
min_edge = [Vector((minx, layers["offset"])), Vector((maxx, layers["offset"]))]
max_edge = [
Vector((minx, layers["offset"] + layers["thickness"])),
Vector((maxx, layers["offset"] + layers["thickness"])),
]
centerline = [
Vector((minx, layers["offset"] + layers["thickness"] / 2)),
Vector((maxx, layers["offset"] + layers["thickness"] / 2)),
]
connections = self.get_connections(element, obj, centerline, min_edge, max_edge)
bm = bm_original.copy()
prev_co = None
if not usage:
sense_factor = 1 # Assume the extrusion vector points in the direction sense
no = tool.Drawing.get_extrusion_vector(element).normalized()
co = Vector((0.0, 0.0, offset))
elif usage.LayerSetDirection == "AXIS2":
co = Vector((0.0, offset, 0.0))
no = tool.Drawing.get_extrusion_vector(element).normalized()
no = no.cross(Vector([1.0, 0.0, 0.0]))
elif usage.LayerSetDirection == "AXIS3":
co = Vector((0.0, 0.0, offset))
no = tool.Drawing.get_extrusion_vector(element).normalized()
no = Vector([0.0, 0.0, 1.0])
elif usage.LayerSetDirection == "AXIS1":
co = Vector((0.0, 0.0, offset))
no = tool.Drawing.get_extrusion_vector(element).normalized()
no = Vector([1.0, 0.0, 0.0])
no *= sense_factor
last_i = len(layer_set.MaterialLayers) - 1
start_point = None
end_point = None
if connections["ATSTART"]:
boundary_edge = min_edge if connections["ATSTART"]["angle"] > 0 else max_edge
rel_dir = connections["ATSTART"]["centerline"][1] - connections["ATSTART"]["centerline"][0]
offset, _ = tool.Cad.intersect_edges(boundary_edge, [centerline[0], centerline[0] + rel_dir])
offset_x = (offset - centerline[0]).length + abs(offset.x)
intersect, _ = tool.Cad.intersect_edges(boundary_edge, connections["ATSTART"]["centerline"])
if tool.Cad.is_point_on_edge(intersect, boundary_edge):
centerline[0] = centerline[0] + Vector((offset_x, 0))
start_point = centerline[0] + rel_dir
if connections["ATEND"]:
boundary_edge = min_edge if connections["ATEND"]["angle"] > 0 else max_edge
rel_dir = connections["ATEND"]["centerline"][1] - connections["ATEND"]["centerline"][0]
offset, _ = tool.Cad.intersect_edges(boundary_edge, [centerline[1], centerline[1] + rel_dir])
offset_x = (offset - centerline[1]).length + abs((maxx - abs(offset.x)))
intersect, _ = tool.Cad.intersect_edges(boundary_edge, connections["ATEND"]["centerline"])
if tool.Cad.is_point_on_edge(intersect, boundary_edge):
centerline[1] = centerline[1] - Vector((offset_x, 0))
end_point = centerline[1] + rel_dir
min_segments = self.get_segments(connections["MINPATH"], centerline, start_point, end_point)
max_segments = self.get_segments(connections["MAXPATH"], centerline, start_point, end_point)
final_segments = []
for segment in min_segments:
segment_line = shapely.LineString([co for co in segment])
for distance in layers["min_layers"]:
distance *= -1
layer_line = shapely.offset_curve(segment_line, distance, join_style=shapely.BufferJoinStyle.mitre)
final_segments.append(list(layer_line.coords))
for segment in max_segments:
segment_line = shapely.LineString([co for co in segment])
for distance in layers["max_layers"]:
layer_line = shapely.offset_curve(segment_line, distance, join_style=shapely.BufferJoinStyle.mitre)
final_segments.append(list(layer_line.coords))
# Extrude and bisect layers
bm = bmesh.new()
verts = []
edges = []
offset = 0
for segment in final_segments:
if not segment:
# Why does this occur?
continue
if isinstance(segment[0], tuple):
segment = [Vector(co) for co in segment]
verts.extend([bm.verts.new(co.to_3d()) for co in segment])
[bm.edges.new((verts[i + offset], verts[i + 1 + offset])) for i in range(0, len(segment) - 1)]
offset += len(segment)
extrusion_dir = (0, 0, 3)
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=bm.edges[:])
bmesh.ops.translate(
bm, vec=extrusion_dir, verts=[v for v in extruded_geom["geom"] if isinstance(v, bmesh.types.BMVert)]
)
verts, edges = self.bisect_mesh(obj, bm, context.scene.camera)
layer_linestrings = [shapely.LineString((verts[e[0]], verts[e[1]])) for e in edges]
clipped_linestrings = []
polygons = shapely.polygonize([shapely.LineString((cut_verts[e[0]], cut_verts[e[1]])) for e in cut_edges])
for polygon in polygons.geoms:
clipped_linestrings.extend([polygon.intersection(ls) for ls in layer_linestrings])
verts = []
edges = []
offset = 0
for linestring in clipped_linestrings:
try:
linestring = list(linestring.coords)
except:
print("Failed ... ", linestring)
continue
verts.extend(linestring)
edges.extend([(i + offset, i + 1 + offset) for i in range(0, len(linestring) - 1)])
offset += len(linestring)
DecoratorData.layerset_cache[element.id()] = (verts, edges)
return verts, edges
def bisect_mesh(self, obj, bm, camera):
camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world
plane_co = camera_matrix.translation
plane_no = camera_matrix.col[2].xyz
global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start
# Run the bisect operation
geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
results = bmesh.ops.bisect_plane(bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no)
vert_map = {}
verts = []
edges = []
j = 0
for i, layer in enumerate(layer_set.MaterialLayers):
prev_co = co.copy()
co += no * layer.LayerThickness * self.unit_scale
if i != last_i:
bisect = bmesh.ops.bisect_plane(
bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no
)
for geom in bisect["geom_cut"]:
if isinstance(geom, bmesh.types.BMVert):
verts.append(tuple((obj.matrix_world @ geom.co) + global_offset))
vert_map[geom.index] = j
j += 1
else:
# It seems as though edges always appear after verts
edges.append([vert_map[v.index] for v in geom.verts])
colour = self.get_material_colour(layer.Material)
bm_fill = bm_original.copy()
if i != last_i:
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
bmesh.ops.bisect_plane(bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True)
if i != 0:
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
bmesh.ops.bisect_plane(bm_fill, geom=geom, dist=0.0001, plane_co=prev_co, plane_no=no, clear_inner=True)
DecoratorData.fill_cache[element_id].setdefault(colour, []).append(
self.get_bmesh_tris(obj, bm_fill, context.scene.camera)
)
DecoratorData.slice_cache[element.id()] = (verts, edges)
bm_original.free()
def get_material_colour(self, material: ifcopenshell.entity_instance) -> tuple[float, float, float, float]:
body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
style = ifcopenshell.util.representation.get_material_style(material, body)
if not style:
return self.fallback_colour
if not (styles := [s for s in style.Styles if s.is_a("IfcSurfaceStyleShading")]):
return self.fallback_colour
colour = styles[0].SurfaceColour
return (colour.Red, colour.Green, colour.Blue, 1)
def get_bmesh_tris(self, obj, bm, camera):
global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start
triangulate = bmesh.ops.triangulate(bm, faces=bm.faces)
vert_map = {}
verts = []
tris = []
i = 0
for face in triangulate["faces"]:
tri = []
for vert in face.verts:
if index := vert_map.get(vert.index, None):
tri.append(index)
for geom in results["geom_cut"]:
if isinstance(geom, bmesh.types.BMVert):
verts.append(tuple((obj.matrix_world @ geom.co) + global_offset))
vert_map[geom.index] = i
i += 1
else:
# It seems as though edges always appear after verts
edges.append([vert_map[v.index] for v in geom.verts])
bm.free()
return verts, edges
def get_layer_data(self, element):
usage = ifcopenshell.util.element.get_material(element)
offset = usage.OffsetFromReferenceLine * self.unit_scale
layer_set = usage.ForLayerSet
if len(layer_set.MaterialLayers) == 1:
return # No use slicing if there's only one layer
total_thickness = layer_set.TotalThickness
half_thickness = total_thickness / 2
min_layers = []
max_layers = []
current_thickness = 0
for layer in layer_set.MaterialLayers:
current_thickness += layer.LayerThickness
if current_thickness / total_thickness < 0.5:
min_layers.append((half_thickness - current_thickness) * self.unit_scale)
else:
max_layers.append((current_thickness - half_thickness) * self.unit_scale)
min_layers.reverse()
max_layers.pop()
if usage.DirectionSense == "NEGATIVE":
total_thickness *= -1
offset *= -1
min_layers = [l * -1 for l in min_layers]
max_layers = [l * -1 for l in max_layers]
return {
"offset": offset,
"min_layers": min_layers,
"max_layers": max_layers,
"thickness": total_thickness * self.unit_scale,
}
def get_segments(self, connections, centerline, start_point, end_point):
segments = []
total_connections = len(connections)
if total_connections:
for i in range(0, total_connections + 1):
if i == 0:
connection = connections[i]
segments.append([centerline[0], connection["intersection"], connection["out_point"]])
elif i == total_connections:
segments.append([segments[-1][-1], segments[-1][-2], centerline[1]])
else:
verts.append(tuple((obj.matrix_world @ vert.co) + global_offset))
vert_map[vert.index] = i
tri.append(i)
i += 1
tris.append(tri)
return verts, tris
connection = connections[i]
segments.append(
[segments[-1][-1], segments[-1][-2], connection["intersection"], connection["out_point"]]
)
else:
segments.append([centerline[0], centerline[1]])
if start_point:
segments[0].insert(0, start_point)
if end_point:
segments[-1].append(end_point)
return segments
def get_connections(self, wall, obj, centerline, min_edge, max_edge):
connections = {"ATEND": None, "ATSTART": None, "ATPATH": [], "MINPATH": [], "MAXPATH": []}
for rel in wall.ConnectedTo:
# How do you join to a non layered element? Not sure.
if tool.Model.get_usage_type(rel.RelatedElement) != "LAYER2":
continue
if rel.RelatingConnectionType == "ATPATH":
metadata = self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
if not metadata:
continue
connections["ATPATH"].append(metadata)
else:
metadata = self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
if not metadata:
continue
connections[rel.RelatingConnectionType] = metadata
for rel in wall.ConnectedFrom:
if tool.Model.get_usage_type(rel.RelatingElement) != "LAYER2":
continue
# We only consider ATPATH since in this situation, we have the
# priority. The non-priority wall never has any layers that need to
# "turn a corner".
if rel.RelatedConnectionType == "ATPATH":
metadata = self.get_connection_metadata(obj, rel.RelatingElement, centerline, min_edge, max_edge)
if not metadata:
continue
connections["ATPATH"].append(metadata)
connections["ATPATH"] = sorted(connections["ATPATH"], key=lambda c: c["intersection"].x)
for connection in connections["ATPATH"]:
if connection["angle"] > 0:
connections["MINPATH"].append(connection)
else:
connections["MAXPATH"].append(connection)
return connections
def get_connection_metadata(self, obj, rel_element, centerline, min_edge, max_edge):
rel_obj = tool.Ifc.get_object(rel_element)
layers = self.get_layer_data(rel_element)
if not layers:
return
minx = min([co[0] for co in rel_obj.bound_box])
maxx = max([co[0] for co in rel_obj.bound_box])
rel_centerline = [
Vector((minx, layers["offset"] + layers["thickness"] / 2)),
Vector((maxx, layers["offset"] + layers["thickness"] / 2)),
]
rel_centerline = [obj.matrix_world.inverted() @ rel_obj.matrix_world @ v.to_3d() for v in rel_centerline]
rel_centerline = [v.to_2d() for v in rel_centerline]
intersection = tool.Cad.intersect_edges(centerline, rel_centerline)
if intersection:
intersection, _ = intersection
else:
return
closest_centerline_point = tool.Cad.closest_vector(intersection, tuple(rel_centerline))
if closest_centerline_point == rel_centerline[1]:
rel_centerline = [rel_centerline[1], rel_centerline[0]]
angle = tool.Cad.angle_edges(centerline, rel_centerline, signed=True)
# A little extreme, but maybe it's OK?
out_point = tool.Cad.furthest_vector(intersection, tuple(rel_centerline))
return {
"element": rel_element,
"obj": rel_obj,
"centerline": rel_centerline,
"intersection": intersection,
"angle": angle,
"out_point": out_point,
}
class DecorationsHandler:
@@ -1918,7 +2002,6 @@ class DecorationsHandler:
]
installed = None
handler = None
@classmethod
def install(cls, context):
@@ -1928,8 +2011,6 @@ class DecorationsHandler:
# NOTE: we USE POST_PIXEL here so that we can use both POLYLINE_UNIFORM_COLOR
# and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL")
if not DecoratorData.is_loaded:
DecoratorData.load(handler)
@classmethod
def uninstall(cls):
@@ -1952,9 +2033,47 @@ class DecorationsHandler:
for decorator in self.decorators.values():
decorator.font_id = font_id
def get_objects_and_decorators(self, collection):
# TODO: do it in data instead of the handler for performance?
results = []
viewport = bpy.context.space_data
for obj in collection.all_objects:
if not obj.visible_get(viewport=viewport):
continue
element = tool.Ifc.get_entity(obj)
if not element:
continue
if not element.is_a("IfcAnnotation"):
continue
object_type: Union[str, None] = ifcopenshell.util.element.get_predefined_type(element)
if object_type == "DRAWING":
continue
if dec := self.decorators.get(object_type, None):
results.append((obj, dec))
elif isinstance(obj.data, bpy.types.Mesh):
if object_type == "LINEWORK" and "dashed" in str(
ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
).split(" "):
results.append((obj, self.decorators["HIDDEN_LINE"]))
else:
results.append((obj, self.decorators["MISC"]))
return results
def __call__(self, context):
collection, _ = helper.get_active_drawing(context.scene)
if collection is None:
return
if not DrawingsData.is_loaded:
DrawingsData.load()
for obj, decorator in DecoratorData.data["object_decorators"]:
object_decorators = self.get_objects_and_decorators(collection)
for obj, decorator in object_decorators:
decorator.decorate(context, obj)

Some files were not shown because too many files have changed in this diff Show More