mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 16:01:36 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dbe2c541c7 | |||
| 7d4a017c15 | |||
| 14f5aa7c4f | |||
| c585c789ac | |||
| 891752a569 | |||
| 6faef6564e | |||
| 62bd495a9e | |||
| b93cbbf989 |
@@ -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.
|
||||
@@ -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
|
||||
@@ -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?
|
||||
@@ -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."
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -4,7 +4,6 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9].[0-9].[0-9]*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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]
|
||||
@@ -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:
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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.")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"}
|
||||
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user