mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-07 16:31:37 +00:00
Compare commits
375 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| edfcdbb865 | |||
| a77125ff12 | |||
| c8841bcb6a | |||
| b20997dea8 | |||
| 292aca6685 | |||
| 3a824ec969 | |||
| bea9f42f87 | |||
| e4a50b32ce | |||
| 76611024b6 | |||
| 92ee82374c | |||
| af81aa1fae | |||
| 16f9f91561 | |||
| 3a52a848ae | |||
| a50c633cf4 | |||
| 6251750133 | |||
| acef6379e8 | |||
| c2860e6e25 | |||
| b8e5cd740a | |||
| 9188dc60ab | |||
| 8ce56db0c3 | |||
| b91b1401f4 | |||
| 0e37528915 | |||
| 9991c4aa56 | |||
| 0ef3983035 | |||
| 0f6e6a48f4 | |||
| 3d225beafd | |||
| 6a91bbb720 | |||
| 1f23e4ba18 | |||
| 2ebcffc7d4 | |||
| 3d4321f713 | |||
| f8799305e1 | |||
| 7c38705958 | |||
| f6fc0cfa0f | |||
| 686e9c946b | |||
| 3085072751 | |||
| 0328be4093 | |||
| 6218d3902e | |||
| be1836fa48 | |||
| 22b5db8a1e | |||
| 4268e1376c | |||
| e2c1a54e0a | |||
| d1be660469 | |||
| 3ae5227b57 | |||
| cdc994ca76 | |||
| 0ad4087f0a | |||
| 4b7ca648c6 | |||
| 17bd6e6b0c | |||
| 7b80803cbe | |||
| eaf67d83f9 | |||
| 41f754e15b | |||
| b7cdb86327 | |||
| 809a20dc99 | |||
| f5e41d18c6 | |||
| 60a8c45ad7 | |||
| 73f98bfe4b | |||
| 958686958b | |||
| e0b61c5794 | |||
| 5aa5552759 | |||
| 82a19cee7b | |||
| 43d4adbd9d | |||
| 1d3c71985f | |||
| 381fca0812 | |||
| 0f37eca333 | |||
| 6bf9b8b0c9 | |||
| 9361cfee28 | |||
| 1c205d494a | |||
| 4d1d224a34 | |||
| 4976af5e66 | |||
| be657748f3 | |||
| def9352ade | |||
| a20002ecca | |||
| b7a0f230ad | |||
| 76529a06bb | |||
| dcdae67a2d | |||
| 5a7a368169 | |||
| ec13294430 | |||
| 653b88ba18 | |||
| f8489779f6 | |||
| 64df82a76b | |||
| af4fbd6750 | |||
| 1619ae8543 | |||
| 41eadf735e | |||
| 172775d4ed | |||
| d539cad1c2 | |||
| 94cf56e080 | |||
| cf253824ca | |||
| 9c73f17303 | |||
| a6f523039b | |||
| c2f9bfeb8d | |||
| 3ab1c2e0f9 | |||
| 0ed964daa2 | |||
| 03f619a77c | |||
| e5ad35e2c8 | |||
| 297f15737d | |||
| 374b95efdf | |||
| 744ed522b6 | |||
| f4bc3fffa3 | |||
| 06a2cecb51 | |||
| b0f2785e99 | |||
| c6c852c7f6 | |||
| 5d920d5e31 | |||
| 2b522b71cb | |||
| d00c3735ec | |||
| bbdc9c45dd | |||
| 5eb9cdf40d | |||
| 689289bb60 | |||
| d401e4c948 | |||
| cbe8fc1ebd | |||
| 9f11568aff | |||
| 968e193f99 | |||
| 2d06a79842 | |||
| 808a16ba4a | |||
| a67a6034a5 | |||
| 8fe1a63339 | |||
| 1ce78f3c65 | |||
| 57b35c68e4 | |||
| ff57b91691 | |||
| 6e15219fad | |||
| 979964f211 | |||
| 4ce2f81479 | |||
| dbdde52435 | |||
| 84568f34f0 | |||
| fe9c548ae0 | |||
| 1714516a81 | |||
| c4c3a2056b | |||
| fad35b14f2 | |||
| 2382f8d7f8 | |||
| 9ae7a68c43 | |||
| 7a5e5fe602 | |||
| 839b2c9c1f | |||
| 4aaaf6e8f5 | |||
| 1b38b03532 | |||
| cbf0fa88eb | |||
| 6961ca8ce7 | |||
| d758d4aef5 | |||
| 84240c870c | |||
| 4483d8e958 | |||
| 70440050db | |||
| bc7527a73b | |||
| c8ad1ba0ee | |||
| c7a29d5604 | |||
| 814c16d3ee | |||
| 3bedbfb82c | |||
| 07f42b9126 | |||
| 5b1da583d9 | |||
| c624162f09 | |||
| bc221cfa99 | |||
| 5d02afa47e | |||
| 406962287c | |||
| 241f7679b0 | |||
| 0001071185 | |||
| 8d476c792d | |||
| 97c7aefad7 | |||
| 34cafc761a | |||
| 3fbde8db28 | |||
| 0b8fb41ac4 | |||
| dc46afc227 | |||
| b8ee1d4d8d | |||
| ecf461e1e0 | |||
| 1856eeddeb | |||
| afb2b9fdea | |||
| af2cb2896a | |||
| e792cf8fa0 | |||
| 7b71e2ce2a | |||
| ce72337d3a | |||
| 0e9ca541c4 | |||
| 784576b249 | |||
| 5e26e55a8c | |||
| 1b110c4a84 | |||
| b94629133c | |||
| 55e270c135 | |||
| 308d628b1e | |||
| 5e16588eb7 | |||
| e919a0643f | |||
| 9835ef977f | |||
| 10f84537de | |||
| f9a0081b33 | |||
| 11462c388a | |||
| 4bcd9639f4 | |||
| b856c40fe7 | |||
| dd1c529544 | |||
| c9fbec4d46 | |||
| c9437329ef | |||
| a09e971674 | |||
| 8ac04e32e5 | |||
| 0634dc83d4 | |||
| 5dc4b18d3f | |||
| f84bca8017 | |||
| 31a6c91c4b | |||
| 30242d859e | |||
| 330799c799 | |||
| e2c2234199 | |||
| 34efa9f459 | |||
| 261de9b2be | |||
| 52af302bef | |||
| 529c74f6a9 | |||
| fd30a6b5eb | |||
| 82f25f50cc | |||
| d0dc31588d | |||
| 68f71c715e | |||
| 6913060f0c | |||
| 4427c73816 | |||
| d188155fac | |||
| 598dfd75e7 | |||
| 5c63cd7981 | |||
| 0b22333b69 | |||
| 69481c2d63 | |||
| 671c887201 | |||
| 0a6fbeccb0 | |||
| 4b3ca11c12 | |||
| f9bf8897bc | |||
| 932939099a | |||
| 8d31d93531 | |||
| 5c4f157624 | |||
| f87c694432 | |||
| 38eedffd80 | |||
| 305439d27e | |||
| 908bf02098 | |||
| c6176d364f | |||
| 068814c095 | |||
| 476604202a | |||
| 558bda1aea | |||
| 426f1b91f2 | |||
| d1b1b08174 | |||
| c1513190d4 | |||
| 50e7728f95 | |||
| 4e00a8d596 | |||
| 266cc32336 | |||
| f8ad05cb54 | |||
| 311f9b96e7 | |||
| 56bfe80b3b | |||
| 2db6b860b2 | |||
| 5af2c3914a | |||
| f8ebc2d156 | |||
| f3228c31f8 | |||
| 86d481f4f8 | |||
| 05f79c844a | |||
| b87a7b3d0a | |||
| e54acd83f8 | |||
| f8e52c8a12 | |||
| 0608f92bc4 | |||
| df18108406 | |||
| b64dcfd4b8 | |||
| d1493b4234 | |||
| fb34af9162 | |||
| bb598d848a | |||
| 502cf6f439 | |||
| 9875fce579 | |||
| 90198115b6 | |||
| 4896a69981 | |||
| 3390f58c88 | |||
| 917d28a7b1 | |||
| bebaa4d1bd | |||
| 93a4f4980a | |||
| 1e30eae1d7 | |||
| ff90396bcf | |||
| a030b84fc5 | |||
| 0b1c049e81 | |||
| 74390c0529 | |||
| 21d39d8a23 | |||
| ad0f62f88f | |||
| 85d8fa49ab | |||
| c5f4b53555 | |||
| 016cfa94d2 | |||
| 007af80234 | |||
| 576a206d5b | |||
| 4cf6411a61 | |||
| 37a0b3f6a9 | |||
| 209caec49a | |||
| bebb29f486 | |||
| deb6e3779e | |||
| a6c37771fc | |||
| 95f754d2df | |||
| 030dc6460a | |||
| 360e08f268 | |||
| dc84d85f7b | |||
| e16d8907f3 | |||
| 52cada180b | |||
| 1999216ea5 | |||
| 5a6476a57f | |||
| c45e3b0099 | |||
| c9fc543e91 | |||
| 9994fec571 | |||
| b0aeabf2f6 | |||
| ddd7dff75e | |||
| 7acd13cb82 | |||
| 6499ac41cf | |||
| e8b74245ce | |||
| ecee3781e4 | |||
| 52f949b2dc | |||
| cb7e04f61a | |||
| b59e5b076d | |||
| 1516237537 | |||
| 53cd5f834e | |||
| 6dc6fddd0e | |||
| 52c8cc0a72 | |||
| 463473b959 | |||
| fd0c578995 | |||
| 74fbe94ed4 | |||
| 38002e1ee2 | |||
| 58b070d22d | |||
| e2b119bf9b | |||
| 8cf3807c2c | |||
| ad3d0fecb2 | |||
| fcfe6f97c3 | |||
| c0190b5065 | |||
| e596ac6b1b | |||
| d9ed6a4950 | |||
| a49ccfcfc8 | |||
| 7974d8a0d0 | |||
| f00e77ef6c | |||
| fc37cca1d0 | |||
| 53da00a5b7 | |||
| b5bc53bdf6 | |||
| bc0fbf744a | |||
| 53b26a9b66 | |||
| 858e73c9d9 | |||
| 952400bd58 | |||
| ffffcc576e | |||
| d8102d848d | |||
| 9407faf1f9 | |||
| 1e3f1e7dfc | |||
| 8aad28c730 | |||
| 81943ec9e5 | |||
| 5b15de50b6 | |||
| 4d50dce16d | |||
| 69b5d0940f | |||
| 273d6c1039 | |||
| 24ef03c96e | |||
| cc4553d3c9 | |||
| cf1f74e260 | |||
| 3052c6e578 | |||
| 0eb70b7fc3 | |||
| eb863bb2aa | |||
| 345a6e72d9 | |||
| 8b775ea30f | |||
| b712f30ad3 | |||
| 0bbe1aad5b | |||
| 29de264b43 | |||
| f4714d2012 | |||
| 8bfdabab80 | |||
| 50c1220730 | |||
| c280550e30 | |||
| 2bf303686c | |||
| bb91416c4e | |||
| 47e8b978af | |||
| 50eae4ae19 | |||
| 979183b64b | |||
| dc2f788326 | |||
| 7d12880eb9 | |||
| 6676c7f62e | |||
| c0e8c3d1b5 | |||
| e9245efd37 | |||
| de287395d2 | |||
| 938d72ebe3 | |||
| 605c86afc5 | |||
| 68cf8ee247 | |||
| a452d1d361 | |||
| 71067e4a14 | |||
| 7bacdbf8dd | |||
| 54c1dfdb77 | |||
| 834123ad14 | |||
| 3cbab975d5 | |||
| 0315542237 | |||
| 156cca5932 | |||
| 4672fc341f | |||
| f69b02cad9 | |||
| 40af3f0495 | |||
| 0dd42c1fb2 | |||
| 0990dfd372 | |||
| 9316e0bff0 | |||
| bc709f06f7 | |||
| cc031a23cb | |||
| 614b71e0c6 | |||
| 66329f07cb |
@@ -1,30 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Error Description and Steps to Reproduce**
|
||||
|
||||
<!--
|
||||
Describe 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.
|
||||
-->
|
||||
@@ -0,0 +1,23 @@
|
||||
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
|
||||
@@ -0,0 +1,8 @@
|
||||
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?
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
**Feature Description**
|
||||
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
@@ -0,0 +1,8 @@
|
||||
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,5 +118,10 @@ 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: |
|
||||
aws s3 cp $env:USERPROFILE\output s3://ifcopenshell-builds/ --recursive
|
||||
dir "$env:USERPROFILE\output"
|
||||
aws s3 cp "$env:USERPROFILE\output" s3://ifcopenshell-builds/ --recursive --debug
|
||||
|
||||
@@ -19,55 +19,34 @@ jobs:
|
||||
with:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Step 1 - install dependencies
|
||||
shell: bash
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install 'black>=24.10.0'
|
||||
|
||||
# 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 .
|
||||
|
||||
# QA STEP
|
||||
- name: QA Step - check linting
|
||||
shell: bash
|
||||
id: linting
|
||||
# black doesn't catch all syntax errors, so we check them explicitly.
|
||||
- name: Check syntax errors
|
||||
id: syntax-errors
|
||||
run: |
|
||||
python3 -m black --check . \
|
||||
&& exit 0 \
|
||||
|| (echo "exit_code=$?" >> "$GITHUB_OUTPUT" && exit 1);
|
||||
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
|
||||
|
||||
# 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;
|
||||
- name: Black formatter
|
||||
id: linting
|
||||
run: |
|
||||
python3 -m black --diff --check .
|
||||
continue-on-error: true
|
||||
|
||||
# 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;
|
||||
- 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
|
||||
|
||||
@@ -104,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.3/blender-4.3.2-linux-x64.tar.xz
|
||||
wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.4/blender-4.4.0-linux-x64.tar.xz
|
||||
tar -xf blender.tar.xz
|
||||
|
||||
# Setup Blender.
|
||||
@@ -175,6 +175,7 @@ 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 }}-${{ matrix.variant }}
|
||||
name: ${{ matrix.platform.distver }}-${{ matrix.pyver.name }}
|
||||
needs: activate
|
||||
runs-on: ${{ matrix.platform.distver }}
|
||||
defaults:
|
||||
@@ -48,57 +48,67 @@ 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, distver: macos-latest, pkg_dir: 'osx-64' }
|
||||
{ name: macOS-arm, distver: macos-latest, pkg_dir: 'osx-arm64' },
|
||||
{ name: macOS-x86, distver: macos-13, pkg_dir: 'osx-64' }
|
||||
]
|
||||
steps:
|
||||
- name: Set up swap space
|
||||
- name: Set Swap Space
|
||||
if: runner.os == 'Linux'
|
||||
uses: pierotofy/set-swap-space@v1.0
|
||||
uses: pierotofy/set-swap-space@master
|
||||
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: ${{ runner.os == 'macOS' }}
|
||||
if: ${{ matrix.platform.name == 'macOS-x86' }}
|
||||
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@v1 # https://github.com/mamba-org/setup-micromamba
|
||||
- uses: mamba-org/setup-micromamba@v2 # https://github.com/mamba-org/setup-micromamba
|
||||
with:
|
||||
environment-name: test-env
|
||||
create-args: >-
|
||||
python=3.11
|
||||
python=3.12
|
||||
anaconda-client
|
||||
boa
|
||||
rattler-build
|
||||
|
||||
- name: create conda package dist dir
|
||||
run: |
|
||||
mkdir -p ${{ github.workspace }}/dist
|
||||
mkdir -p ${{ env.ARTIFACTS_DIR }}
|
||||
|
||||
- name: build & test ifcopenshell
|
||||
run: |
|
||||
conda mambabuild . --python ${{ matrix.pyver.distver }} --no-remove-work-dir --output-folder '${{ github.workspace }}/dist' --variants='{variant: ${{ matrix.variant }}}'
|
||||
working-directory: ./conda
|
||||
rattler-build build -r conda/recipe.yaml --output-dir '${{ env.ARTIFACTS_DIR }}'
|
||||
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 '${{ github.workspace }}/dist/${{ matrix.platform.pkg_dir }}/*.tar.bz2'
|
||||
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell '${{ env.ARTIFACTS_DIR }}/${{ matrix.platform.pkg_dir }}/*.conda'
|
||||
|
||||
- name: upload to anaconda
|
||||
if: ${{ matrix.platform.name != 'win' }}
|
||||
run: |
|
||||
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell ${{ github.workspace }}/dist/${{ matrix.platform.pkg_dir }}/*.tar.bz2
|
||||
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell ${{ env.ARTIFACTS_DIR }}/${{ matrix.platform.pkg_dir }}/*.conda
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
jobs:
|
||||
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
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-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs: activate
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
-
|
||||
name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
|
||||
-
|
||||
name: Build ifcopenshell
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
make package
|
||||
working-directory: build
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
# Artifact name
|
||||
name: ifcos-artifacts
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
path: build/assets/Ifc*
|
||||
|
||||
deliver:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
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:latest' }}
|
||||
tags: aecgeeks/ifcopenshell:${{ github.ref_name }}${{ github.ref_name == github.event.repository.default_branch && ',aecgeeks/ifcopenshell:22.04' }}
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
|
||||
@@ -4,6 +4,7 @@ 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,3 +49,7 @@ 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
|
||||
+3
-1
@@ -46,4 +46,6 @@ cmake ${CMAKE_ARGS} -G Ninja \
|
||||
|
||||
ninja
|
||||
|
||||
ninja install -j 1
|
||||
ninja install -j 1
|
||||
|
||||
python "${RECIPE_DIR}/update_version_init.py" "${PKG_VERSION}" "${SP_DIR}/ifcopenshell/__init__.py"
|
||||
@@ -1,7 +1,3 @@
|
||||
variant:
|
||||
- novtk
|
||||
- all
|
||||
|
||||
occt:
|
||||
- 7.8.1
|
||||
|
||||
@@ -24,12 +20,13 @@ cxx_compiler_version:
|
||||
- '12' # [linux]
|
||||
- '16' # [osx]
|
||||
c_stdlib_version:
|
||||
- '2.12' # [linux]
|
||||
- '10.13' # [osx]
|
||||
- 2.17 # [linux]
|
||||
- 10.13 # [osx and x86_64]
|
||||
- 11.0 # [osx and arm64]
|
||||
hdf5:
|
||||
- 1.14.3
|
||||
- 1.14.6
|
||||
libboost_devel:
|
||||
- '1.84'
|
||||
- '1.86'
|
||||
libxml2:
|
||||
- '2'
|
||||
mpfr:
|
||||
@@ -47,12 +44,12 @@ target_platform:
|
||||
- win-64 # [win]
|
||||
- linux-64 # [linux]
|
||||
- osx-64 # [osx]
|
||||
macos_machine:
|
||||
- x86_64-apple-darwin13.4.0
|
||||
MACOSX_DEPLOYMENT_TARGET:
|
||||
- '10.13'
|
||||
MACOSX_SDK_VERSION:
|
||||
- '10.13'
|
||||
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]
|
||||
|
||||
CONDA_BUILD_SYSROOT:
|
||||
- "/Users/runner/work/MacOSX10.13.sdk" # [osx]
|
||||
CONDA_BUILD_SYSROOT: # [osx]
|
||||
- "/Users/runner/work/MacOSX10.13.sdk" # [osx and x86_64]
|
||||
@@ -1,64 +1,78 @@
|
||||
{% 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"]
|
||||
context:
|
||||
version: ${{ env.get("VERSION_OVERRIDE", default="0.8.2.1") }}
|
||||
build: 1
|
||||
|
||||
package:
|
||||
name: ifcopenshell
|
||||
version: {{ version }}
|
||||
version: ${{ version }}
|
||||
|
||||
source:
|
||||
path: ../
|
||||
- path: ..
|
||||
|
||||
|
||||
build:
|
||||
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 }}*
|
||||
|
||||
number: ${{ build }}
|
||||
dynamic_linking:
|
||||
binary_relocation: ${{ true if osx }}
|
||||
|
||||
requirements:
|
||||
build:
|
||||
- cmake
|
||||
- if: build_platform != target_platform
|
||||
then:
|
||||
- python
|
||||
- cross-python_${{ target_platform }}
|
||||
- cmake <4
|
||||
- ninja
|
||||
- swig >=4.1.1
|
||||
- {{ stdlib("c") }}
|
||||
- {{ compiler('c') }}
|
||||
- {{ compiler('cxx') }}
|
||||
- ${{ stdlib("c") }}
|
||||
- ${{ compiler('c') }}
|
||||
- ${{ compiler('cxx') }}
|
||||
host:
|
||||
- python
|
||||
- libboost-devel
|
||||
- occt *=*{{ variant }}*
|
||||
- occt
|
||||
- libxml2
|
||||
- cgal-cpp
|
||||
- hdf5
|
||||
- eigen
|
||||
- mpfr
|
||||
- nlohmann_json
|
||||
- gmp # [unix]
|
||||
- mpir # [win]
|
||||
- gmp
|
||||
- zlib
|
||||
run:
|
||||
- python
|
||||
- {{ pin_compatible('occt', max_pin='x.x.x') }} *{{ variant }}*
|
||||
- {{ pin_compatible('cgal-cpp', max_pin='x.x') }}
|
||||
|
||||
|
||||
test:
|
||||
imports:
|
||||
- ifcopenshell
|
||||
- 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') }}
|
||||
|
||||
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
|
||||
============
|
||||
@@ -293,8 +307,9 @@ 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"
|
||||
doc_url: https://ifcopenshell.org/
|
||||
dev_url: https://github.com/IfcOpenShell/IfcOpenShell
|
||||
homepage: https://ifcopenshell.org
|
||||
repository: https://github.com/IfcOpenShell/IfcOpenShell
|
||||
documentation: https://ifcopenshell.org/
|
||||
|
||||
extra:
|
||||
recipe-maintainers:
|
||||
@@ -0,0 +1,32 @@
|
||||
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,3 +35,4 @@ extend-exclude = '''
|
||||
[tool.pyright]
|
||||
reportInvalidTypeForm = false
|
||||
disableBytesTypePromotions = true
|
||||
reportUnnecessaryTypeIgnoreComment = true
|
||||
|
||||
@@ -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/>.
|
||||
|
||||
SHELL := sh
|
||||
PYTHON:=python3.11
|
||||
PIP:=pip3.11
|
||||
PATCH:=patch
|
||||
|
||||
@@ -28,6 +28,10 @@ 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
|
||||
@@ -218,17 +222,21 @@ if IN_BLENDER:
|
||||
info["binary_python_version"] = version
|
||||
return info
|
||||
|
||||
try:
|
||||
import git
|
||||
def update_commit_data() -> None:
|
||||
try:
|
||||
import git
|
||||
|
||||
# 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
|
||||
last_commit_date = repo.head.object.committed_datetime.isoformat()
|
||||
except:
|
||||
pass
|
||||
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()
|
||||
|
||||
try:
|
||||
import ifcopenshell.api
|
||||
@@ -244,6 +252,12 @@ 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()
|
||||
@@ -261,6 +275,7 @@ if IN_BLENDER:
|
||||
bonsai.REINSTALLED_BBIM_VERSION = current_version
|
||||
|
||||
bonsai.bim.register()
|
||||
purge_cache()
|
||||
|
||||
def unregister():
|
||||
if platform.system() == "Windows":
|
||||
|
||||
@@ -20,6 +20,7 @@ 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
|
||||
|
||||
@@ -100,6 +101,7 @@ 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,
|
||||
@@ -121,6 +123,7 @@ classes = [
|
||||
operator.SelectURIAttribute,
|
||||
operator.SetTab,
|
||||
operator.SwitchTab,
|
||||
operator.ShowSystemInfo,
|
||||
prop.StrProperty,
|
||||
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
|
||||
prop.ObjProperty,
|
||||
@@ -221,7 +224,12 @@ 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,8 +47,6 @@
|
||||
"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,
|
||||
@@ -147,7 +145,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": "MATERIAL",
|
||||
"scene.display.shading.wireframe_color_type": "THEME",
|
||||
"scene.display.shading.xray_alpha": 0.5,
|
||||
"scene.display.shading.xray_alpha_wireframe": 0.0,
|
||||
"scene.display.light_direction": [
|
||||
@@ -162,7 +160,6 @@
|
||||
"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,
|
||||
@@ -189,7 +186,6 @@
|
||||
"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,
|
||||
@@ -208,7 +204,6 @@
|
||||
"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,
|
||||
@@ -298,8 +293,6 @@
|
||||
"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,
|
||||
@@ -398,7 +391,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": "MATERIAL",
|
||||
"scene.display.shading.wireframe_color_type": "THEME",
|
||||
"scene.display.shading.xray_alpha": 0.5,
|
||||
"scene.display.shading.xray_alpha_wireframe": 0.0,
|
||||
"scene.display.light_direction": [
|
||||
@@ -413,7 +406,6 @@
|
||||
"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,
|
||||
@@ -440,7 +432,6 @@
|
||||
"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,
|
||||
@@ -459,7 +450,6 @@
|
||||
"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,
|
||||
@@ -520,7 +510,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",
|
||||
"scene.render.engine": "BLENDER_EEVEE_NEXT",
|
||||
"scene.render.film_transparent": false,
|
||||
"scene.render.filter_size": 1.5,
|
||||
"scene.render.fps": 24,
|
||||
@@ -549,8 +539,6 @@
|
||||
"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,
|
||||
@@ -649,7 +637,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": "MATERIAL",
|
||||
"scene.display.shading.wireframe_color_type": "THEME",
|
||||
"scene.display.shading.xray_alpha": 0.5,
|
||||
"scene.display.shading.xray_alpha_wireframe": 0.0,
|
||||
"scene.display.light_direction": [
|
||||
@@ -664,7 +652,6 @@
|
||||
"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,
|
||||
@@ -691,7 +678,6 @@
|
||||
"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,
|
||||
@@ -710,7 +696,6 @@
|
||||
"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,
|
||||
|
||||
@@ -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',(#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));
|
||||
#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));
|
||||
#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','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.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.);
|
||||
#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,5 +32,6 @@ 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;
|
||||
|
||||
@@ -47,7 +47,6 @@ 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":
|
||||
@@ -101,41 +100,12 @@ 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)
|
||||
if tool.Geometry.is_scaled(obj):
|
||||
|
||||
@@ -347,8 +347,8 @@ def load_post(scene):
|
||||
|
||||
# Bonsai overlays
|
||||
georeference_props = tool.Georeference.get_georeference_props()
|
||||
aggregate_props = bpy.context.scene.BIMAggregateProperties
|
||||
nest_props = bpy.context.scene.BIMNestProperties
|
||||
aggregate_props = tool.Aggregate.get_aggregate_props()
|
||||
nest_props = tool.Nest.get_nest_props()
|
||||
model_props = tool.Model.get_model_props()
|
||||
if georeference_props.should_visualise:
|
||||
GeoreferenceDecorator.install(bpy.context)
|
||||
@@ -361,7 +361,7 @@ def load_post(scene):
|
||||
if model_props.show_slab_direction:
|
||||
SlabDirectionDecorator.install(bpy.context)
|
||||
|
||||
if scene := bpy.context.scene:
|
||||
if preferences.should_use_snap and (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
|
||||
|
||||
@@ -392,6 +392,7 @@ 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,9 +31,10 @@ 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
|
||||
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired, Literal
|
||||
|
||||
|
||||
IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
|
||||
@@ -417,6 +418,32 @@ class IfcStore:
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def execute_ifc_operator(
|
||||
operator: tool.Ifc.Operator,
|
||||
@@ -446,7 +473,9 @@ class IfcStore:
|
||||
if tool.Ifc.get():
|
||||
tool.Ifc.get().end_transaction()
|
||||
IfcStore.add_transaction_operation(
|
||||
operator, rollback=lambda d: tool.Ifc.get().undo(), commit=lambda d: tool.Ifc.get().redo()
|
||||
operator,
|
||||
rollback=IfcStore.get_ifc_file_undo_callback("UNDO"),
|
||||
commit=IfcStore.get_ifc_file_undo_callback("REDO"),
|
||||
)
|
||||
if BrickStore.graph is not None: # `if BrickStore.graph` by itself takes ages.
|
||||
BrickStore.end_transaction()
|
||||
|
||||
@@ -38,7 +38,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
|
||||
from typing import Dict, Union, Optional, Any, Literal, Iterable
|
||||
from ifcopenshell.util.shape import MatrixType
|
||||
|
||||
|
||||
@@ -198,11 +198,16 @@ 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
|
||||
@@ -300,6 +305,7 @@ 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.")
|
||||
@@ -319,16 +325,15 @@ class IfcImporter:
|
||||
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:
|
||||
self.elements = self.ifc_import_settings.elements
|
||||
if isinstance(self.elements, set):
|
||||
self.elements = list(self.elements)
|
||||
elements = list(self.ifc_import_settings.elements)
|
||||
# TODO: enable filtering for annotations
|
||||
else:
|
||||
if self.file.schema in ("IFC2X3", "IFC4"):
|
||||
self.elements = self.file.by_type("IfcElement") + self.file.by_type("IfcProxy")
|
||||
elements = self.file.by_type("IfcElement") + self.file.by_type("IfcProxy")
|
||||
else:
|
||||
self.elements = self.file.by_type("IfcElement")
|
||||
elements = self.file.by_type("IfcElement")
|
||||
|
||||
drawing_groups = [g for g in self.file.by_type("IfcGroup") if g.ObjectType == "DRAWING"]
|
||||
drawing_annotations = set()
|
||||
@@ -338,16 +343,16 @@ class IfcImporter:
|
||||
self.annotations = set([a for a in self.file.by_type("IfcAnnotation")])
|
||||
self.annotations -= drawing_annotations
|
||||
|
||||
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")]
|
||||
elements = [e for e in elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")]
|
||||
if self.ifc_import_settings.element_limit_mode == "UNLIMITED":
|
||||
self.elements = set(self.elements)
|
||||
self.elements = set(elements)
|
||||
else:
|
||||
offset = self.ifc_import_settings.element_offset
|
||||
offset_limit = offset + self.ifc_import_settings.element_limit
|
||||
self.elements = set(self.elements[offset:offset_limit])
|
||||
self.elements = set(elements[offset:offset_limit])
|
||||
|
||||
if self.ifc_import_settings.has_filter or self.ifc_import_settings.element_limit_mode != "UNLIMITED":
|
||||
self.element_types = set([ifcopenshell.util.element.get_type(e) for e in self.elements])
|
||||
self.element_types = {t for e in self.elements if (t := ifcopenshell.util.element.get_type(e))}
|
||||
else:
|
||||
self.element_types = set(self.file.by_type("IfcTypeProduct"))
|
||||
|
||||
@@ -385,6 +390,7 @@ 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"):
|
||||
@@ -422,7 +428,8 @@ class IfcImporter:
|
||||
|
||||
rep = representation
|
||||
while True:
|
||||
if len(rep.Items) == 1 and rep.Items[0].is_a("IfcMappedItem"):
|
||||
# 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"):
|
||||
rep_matrix = ifcopenshell.util.placement.get_mappeditem_transformation(rep.Items[0])
|
||||
if not np.allclose(rep_matrix, np.eye(4)):
|
||||
matrix = rep_matrix @ matrix
|
||||
@@ -589,6 +596,7 @@ 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
|
||||
)
|
||||
@@ -717,9 +725,15 @@ 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: ifcopenshell.entity_instance = next(
|
||||
rep for rep in product.Representation.Representations if rep.RepresentationType == "Vertex"
|
||||
)
|
||||
|
||||
representation = tool.Structural.get_vertex_representation(product)
|
||||
if not representation:
|
||||
print(
|
||||
"WARNING. Skipping invalid IfcStructuralPointConnection - "
|
||||
f"element has no valid representation:\n{product}."
|
||||
)
|
||||
continue
|
||||
|
||||
mesh = tool.Loader.create_structural_point_connection_mesh(representation)
|
||||
if mesh is None:
|
||||
continue
|
||||
@@ -1124,7 +1138,22 @@ class IfcImporter:
|
||||
bpy.ops.view3d.view_selected()
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
|
||||
def setup_arrays(self):
|
||||
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
|
||||
|
||||
for pset in self.file.by_type("IfcPropertySet"):
|
||||
if pset.Name != "BBIM_Array":
|
||||
continue
|
||||
@@ -1132,16 +1161,24 @@ 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
|
||||
@@ -1162,17 +1199,19 @@ class IfcImportSettings:
|
||||
self.element_limit_mode = "UNLIMITED"
|
||||
self.element_offset = 0
|
||||
self.element_limit = 30000
|
||||
self.has_filter = None
|
||||
self.has_filter = False
|
||||
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: set[ifcopenshell.entity_instance] = set()
|
||||
self.elements: Iterable[ifcopenshell.entity_instance] = set()
|
||||
self.load_indexed_maps = False
|
||||
|
||||
@staticmethod
|
||||
def factory(context=None, input_file=None, logger=None):
|
||||
def factory(
|
||||
context=None, input_file: Optional[str] = None, logger: Optional[logging.Logger] = None
|
||||
) -> IfcImportSettings:
|
||||
scene_diff = tool.Blender.get_diff_props()
|
||||
props = tool.Project.get_project_props()
|
||||
settings = IfcImportSettings()
|
||||
|
||||
@@ -20,6 +20,7 @@ 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
|
||||
@@ -131,6 +132,9 @@ 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]
|
||||
@@ -141,6 +145,7 @@ class AggregateDecorator:
|
||||
shader,
|
||||
"LINE_STRIP",
|
||||
{"position": coords, "arcLength": arc_lengths},
|
||||
indices=indices,
|
||||
)
|
||||
|
||||
matrix = bpy.context.region_data.perspective_matrix
|
||||
@@ -150,13 +155,16 @@ 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):
|
||||
if context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
if props.in_aggregate_mode:
|
||||
return
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
decorator_color_special = self.addon_prefs.decorator_color_special
|
||||
@@ -204,7 +212,7 @@ 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)])
|
||||
if context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
if props.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]
|
||||
@@ -243,6 +251,8 @@ 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)
|
||||
@@ -253,7 +263,7 @@ class AggregateModeDecorator:
|
||||
return
|
||||
region = context.region
|
||||
rv3d = region.data
|
||||
props = context.scene.BIMAggregateProperties
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
|
||||
aggregate_obj = props.editing_aggregate
|
||||
if not aggregate_obj:
|
||||
@@ -284,7 +294,7 @@ class AggregateModeDecorator:
|
||||
def draw_aggregate_empty(self, context):
|
||||
if context.mode == "EDIT_MESH":
|
||||
return
|
||||
props = context.scene.BIMAggregateProperties
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
aggregate_obj = props.editing_aggregate
|
||||
if not aggregate_obj:
|
||||
return
|
||||
|
||||
@@ -23,6 +23,7 @@ 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):
|
||||
@@ -30,23 +31,30 @@ 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 Bonsai UI, then either 'Relating Whole' or 'Related Part' must be provided.\n"
|
||||
"If called from Object Aggregates 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)
|
||||
elif context.active_object:
|
||||
assert relating_obj
|
||||
else:
|
||||
relating_obj = context.active_object
|
||||
|
||||
if not relating_obj:
|
||||
self.report({"ERROR"}, "No relating object is provided.")
|
||||
return
|
||||
@@ -66,7 +74,7 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
|
||||
relating_obj=relating_obj,
|
||||
related_obj=obj,
|
||||
)
|
||||
props = context.scene.BIMAggregateProperties
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
if relating_obj == props.editing_aggregate and props.in_aggregate_mode:
|
||||
new_editing_obj = props.editing_objects.add()
|
||||
new_editing_obj.obj = obj
|
||||
@@ -132,12 +140,11 @@ class BIM_OT_disable_editing_aggregate(bpy.types.Operator):
|
||||
|
||||
|
||||
class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"""Add aggregate to IFC element"""
|
||||
"""Add aggregate to selected IFC elements."""
|
||||
|
||||
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")
|
||||
|
||||
@@ -157,7 +164,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 context.selected_objects:
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
@@ -182,7 +189,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, ifc_class, aggregate_name):
|
||||
def create_aggregate(self, context: bpy.types.Context, ifc_class: str, aggregate_name: str) -> bpy.types.Object:
|
||||
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)
|
||||
@@ -378,7 +385,7 @@ class BIM_OT_disable_aggregate_mode(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate)
|
||||
bonsai.core.aggregate.exit_aggregate_mode(tool.Aggregate)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -388,7 +395,7 @@ class BIM_OT_toggle_aggregate_mode_local_view(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMAggregateProperties
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
objs = [o.obj for o in props.editing_objects]
|
||||
if props.in_aggregate_mode:
|
||||
if context.space_data.local_view:
|
||||
|
||||
@@ -33,6 +33,7 @@ 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:
|
||||
@@ -98,16 +99,27 @@ 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(
|
||||
@@ -115,3 +127,16 @@ 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
|
||||
|
||||
@@ -49,11 +49,14 @@ class BIM_PT_aggregate(Panel):
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
row.label(text="Aggregate Decorator")
|
||||
row.prop(context.scene.BIMAggregateProperties, "aggregate_decorator", icon="HIDE_OFF", text="")
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
icon = "HIDE_OFF" if props.aggregate_decorator else "HIDE_ON"
|
||||
row.prop(props, "aggregate_decorator", icon=icon, text="")
|
||||
if not AggregateData.is_loaded:
|
||||
AggregateData.load()
|
||||
|
||||
props = context.active_object.BIMObjectAggregateProperties
|
||||
assert (obj := context.active_object)
|
||||
props = tool.Aggregate.get_object_aggregate_props(obj)
|
||||
|
||||
if props.is_editing:
|
||||
row = layout.row()
|
||||
@@ -76,10 +79,8 @@ 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="")
|
||||
@@ -131,9 +132,8 @@ class BIM_PT_linked_aggregate(Panel):
|
||||
if not AggregateData.is_loaded:
|
||||
AggregateData.load()
|
||||
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
props = obj.BIMObjectAggregateProperties
|
||||
assert (obj := context.active_object)
|
||||
assert (element := tool.Ifc.get_entity(obj))
|
||||
row = layout.row(align=True)
|
||||
|
||||
if element.Decomposes:
|
||||
|
||||
@@ -16,21 +16,51 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ############################################################################ #
|
||||
|
||||
# Hey there! Welcome to the Bonsai code. Please feel free to reach
|
||||
# out if you have any questions or need further guidance. Happy hacking!
|
||||
|
||||
# ############################################################################ #
|
||||
|
||||
# Every module has a __init__.py file to load all of its classes. Every
|
||||
# operation, property, and interface needs to be registered with the Blender
|
||||
# system when the add-on loads. This is where it happens.
|
||||
|
||||
import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
# from . import ui, prop, operator
|
||||
from . import operator
|
||||
|
||||
classes = (operator.ImportAlignmentCSV,)
|
||||
# You'll need to provide a list of every one of your classes here. If you forget
|
||||
# to specify your class, it won't load and you won't be able to use that
|
||||
# operator, property, or interface.
|
||||
classes = (
|
||||
operator.ImportAlignmentCSV,
|
||||
operator.BuildAlignment,
|
||||
operator.SurveyPoint,
|
||||
prop.BIMAlignmentBuilderProperties,
|
||||
ui.BIM_PT_alignment,
|
||||
)
|
||||
|
||||
|
||||
def menu_func_import(self, context):
|
||||
self.layout.operator(operator.ImportAlignmentCSV.bl_idname, text="Alignment (.csv)")
|
||||
|
||||
|
||||
|
||||
# When the add-on loads, this register function is called. This allows you to
|
||||
# perform additional tasks during startup. If you need to store custom
|
||||
# properties, this is where you tell Blender where they are going to be stored.
|
||||
# You might see more advanced registrations happening in other modules.
|
||||
def register():
|
||||
# Properties are usually stored on bpy.types.Scene when they are something
|
||||
# that affects everything in the project, or bpy.types.Object when they
|
||||
# affect a single BIM element.
|
||||
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
|
||||
bpy.types.Scene.BIMAlignmentBuilderProperties = bpy.props.PointerProperty(type=prop.BIMAlignmentBuilderProperties)
|
||||
|
||||
|
||||
# When someone disables the add-on, we need to unload everything we loaded. This
|
||||
# does the reverse of the register function.
|
||||
def unregister():
|
||||
del bpy.types.Scene.BIMAlignmentBuilderProperties
|
||||
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# 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/>.
|
||||
|
||||
# ############################################################################ #
|
||||
|
||||
# Hey there! Welcome to the Bonsai code. Please feel free to reach
|
||||
# out if you have any questions or need further guidance. Happy hacking!
|
||||
|
||||
# ############################################################################ #
|
||||
|
||||
# Every module has a data.py file to load data for its interface panels to
|
||||
# display. When a panel needs to show information that doesn't come from a
|
||||
# Blender property, it gets that data from one of these classes. This separation
|
||||
# of "fetching data" and "displaying data" is a common concept that makes UI
|
||||
# code much simpler, more efficient, and less error-prone to refresh data.
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
# All data must have a refresh function. The refresh function simply sets all
|
||||
# data class to be not yet loaded. So the next time the interface is drawn to
|
||||
# the user, it will force the data to be reloaded.
|
||||
def refresh():
|
||||
# When you define your own data classes, just add to this list!
|
||||
AlignmentBuilderData.is_loaded = False
|
||||
|
||||
|
||||
# This is a sample data class. It correlates to a single interface panel. Panels
|
||||
# should not share data classes. This makes it easy to write your interface
|
||||
# without having your code mixed in with other parts of the interface. As a
|
||||
# convention, the class is named the same name as the panel.
|
||||
class AlignmentBuilderData:
|
||||
# All data classes must have two variables. One to store all the data it has
|
||||
# loaded and another to store the load state.
|
||||
data = {}
|
||||
is_loaded = False
|
||||
x = 0.0
|
||||
y = 0.0
|
||||
|
||||
# Every data class must have a load function. This lets us load data in a
|
||||
# predictable manner.
|
||||
@classmethod
|
||||
def load(cls):
|
||||
# The load function always has two responsibilities: populate the data,
|
||||
# and set is_loaded to true.
|
||||
cls.data = {
|
||||
"has_project": cls.has_project(),
|
||||
"x" : cls.x(),
|
||||
"y" : cls.y()
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def has_project(cls):
|
||||
# Here, we check whether or not there is an active IFC project in our
|
||||
# Blender session.
|
||||
return bool(tool.Ifc.get())
|
||||
|
||||
@classmethod
|
||||
def x(cls):
|
||||
return cls.x
|
||||
|
||||
@classmethod
|
||||
def y(cls):
|
||||
return cls.y
|
||||
@@ -18,6 +18,17 @@
|
||||
|
||||
# pyright: reportUnnecessaryTypeIgnoreComment=error
|
||||
|
||||
# ############################################################################ #
|
||||
|
||||
# Hey there! Welcome to the Bonsai code. Please feel free to reach
|
||||
# out if you have any questions or need further guidance. Happy hacking!
|
||||
|
||||
# ############################################################################ #
|
||||
|
||||
# Every module has an operator.py file to define all of the buttons that a user
|
||||
# can click on from the Blender interface. Blender calls these buttons
|
||||
# "Operators", since they correlate to a single user operation.
|
||||
|
||||
import os
|
||||
|
||||
import ifcopenshell.api.alignment
|
||||
@@ -28,7 +39,7 @@ import json
|
||||
import time
|
||||
import calendar
|
||||
import isodate
|
||||
import bonsai.core.sequence as core
|
||||
import bonsai.core.alignment as core
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.module.sequence.helper as helper
|
||||
import ifcopenshell.api.spatial
|
||||
@@ -45,6 +56,7 @@ 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"})
|
||||
@@ -111,3 +123,41 @@ class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
tool.Collector.assign(alignment_obj, should_clean_users_collection=False)
|
||||
|
||||
self.report({"INFO"}, "Imported in %s seconds" % (time.time() - start))
|
||||
|
||||
|
||||
|
||||
# Each button correlates to a class like the one below. In this case, we're
|
||||
# creating a new button that will execute a hello world feature.
|
||||
class BuildAlignment(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# Every operator has a unique ID. If you enable Python tooltips and hover
|
||||
# over any button in the interface, you will see each button will run a
|
||||
# function that uses this ID. For example, hovering over this button will
|
||||
# show that the code it executes is "bpy.ops.bim.demonstrate_hello_world()"
|
||||
bl_idname = "bim.build_alignment"
|
||||
|
||||
# In the interface, this button will have the text "Build Alignment".
|
||||
bl_label = "Build Alignment"
|
||||
|
||||
# This code means that the user can undo or redo after pressing the button.
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
# When hovering over the button, this helpful description will be shown.
|
||||
bl_description = "Builds a dummy alignment"
|
||||
|
||||
# When the button is pressed, this _execute() function will run.
|
||||
def _execute(self, context):
|
||||
# Every operator should do one thing only: execute a core function. In
|
||||
# order to execute a core function, the operator's responsibility is to
|
||||
# pass in all of the inputs the core needs to do its job.
|
||||
|
||||
# A core function simply tells tools what to do, so a core function will
|
||||
# always need at least one tool as an input.
|
||||
core.build_alignment(tool.Alignment)
|
||||
|
||||
class SurveyPoint(bpy.types.Operator,tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_survey_point"
|
||||
bl_label = "Add Survey Point"
|
||||
bl_options = {"REGISTER","UNDO"}
|
||||
bl_description = "Adds a survey point"
|
||||
def _execute(self,context):
|
||||
core.add_survey_point(tool.Alignment,x=bpy.context.scene.BIMAlignmentBuilderProperties.x,y=bpy.context.scene.BIMAlignmentBuilderProperties.y)
|
||||
@@ -0,0 +1,64 @@
|
||||
# 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/>.
|
||||
|
||||
# ############################################################################ #
|
||||
|
||||
# Hey there! Welcome to the Bonsai code. Please feel free to reach
|
||||
# out if you have any questions or need further guidance. Happy hacking!
|
||||
|
||||
# ############################################################################ #
|
||||
|
||||
# Every module has a prop.py file to define Blender properties. Any time you
|
||||
# want an interface widget like an input field, dropdown, checkbox, or number
|
||||
# slider, you need a Blender property to store that widget's data. If you want
|
||||
# to store data that will affect the Blender interface, you also need a
|
||||
# property. Properties are stored in the .blend file, so when your user closes
|
||||
# their Blender session, and reopens it, things are how they left it.
|
||||
|
||||
import bpy
|
||||
from bpy.types import PropertyGroup
|
||||
|
||||
# Properties have many different data types. We won't use all of them in this
|
||||
# demo module, but this is a list for your reference.
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
EnumProperty,
|
||||
BoolProperty,
|
||||
IntProperty,
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
|
||||
|
||||
# All properties must belong in a property group. Usually, you'd have a group
|
||||
# named after your module.
|
||||
class BIMAlignmentBuilderProperties(PropertyGroup):
|
||||
# This first property is a string. This means that in the interface, it will
|
||||
# represent a text input field. We can give it a name and a default value.
|
||||
# The name will be the label shown next to the input field in the interface.
|
||||
name: StringProperty(name="Name", default="New Project Name")
|
||||
# Not all properties need to be shown using their equivalent input widget.
|
||||
# In this case, we can store a message string, but we will never show it as
|
||||
# an input text field in the ui.py.
|
||||
message: StringProperty(name="Message")
|
||||
show_hints: BoolProperty(name="Show Hints", default=False)
|
||||
#webui_message: StringProperty(name="Web UI Message", default="Hello, Web UI!")
|
||||
x: FloatProperty(name="x")
|
||||
y: FloatProperty(name="y")
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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/>.
|
||||
|
||||
# ############################################################################ #
|
||||
|
||||
# Hey there! Welcome to the Bonsai code. Please feel free to reach
|
||||
# out if you have any questions or need further guidance. Happy hacking!
|
||||
|
||||
# ############################################################################ #
|
||||
|
||||
# Every module has a ui.py file to define its interface. Interfaces describe how
|
||||
# panels, buttons, labels, and input fields are laid out.
|
||||
|
||||
import bpy
|
||||
from bonsai.bim.module.alignment.data import AlignmentBuilderData
|
||||
|
||||
|
||||
# Every panel in the interface correlates to one of these classes. If you enable
|
||||
# the "Developer Extras" option in Blender, then you can actually right click on
|
||||
# any panel in Blender, and click "Edit Source". This will bring you to a ui.py
|
||||
# file like this one where you can see this code. Pretty neat!
|
||||
class BIM_PT_alignment(bpy.types.Panel):
|
||||
# Every panel has a title.
|
||||
bl_label = "Alignment Demo"
|
||||
|
||||
# Every panel must have an ID. It must be unique. For example, you may want
|
||||
# to later reference the panel (such as if you want to create a nested
|
||||
# subpanel).
|
||||
bl_idname = "BIM_PT_alignment"
|
||||
|
||||
# This tells the panel to appear in the properties section (in the bottom
|
||||
# right by default) of the Blender interface.
|
||||
bl_space_type = "VIEW_3D" #"PROPERTIES"
|
||||
bl_region_type = "UI" #"WINDOW"
|
||||
bl_category = "Survey"
|
||||
|
||||
# This tells the panel to appear in the "scene" tab of the properties panel.
|
||||
#bl_context = "scene"
|
||||
|
||||
# Every panel has a draw function. This draws all the layout of the panel,
|
||||
# including all of its labels, buttons, and so on. Note that if the panel is
|
||||
# hidden, this draw function will not be called.
|
||||
def draw(self, context):
|
||||
# Before drawing any content, the panel must load any dynamic variables
|
||||
# that it will show. Most panels have dynamic content, which display
|
||||
# changing data from your BIM model. You must load that data first. The
|
||||
# two lines are always the same, "if not loaded, then load".
|
||||
if not AlignmentBuilderData.is_loaded:
|
||||
# Each panel should have its own data class that it loads data from.
|
||||
# Note that we only load data if it hasn't already been loaded.
|
||||
# This is because interface panels are drawn continuously. This
|
||||
# draw() function will be called every time you scroll or move your
|
||||
# mouse over it. Loading data is slow, so we only refresh data when
|
||||
# we have to.
|
||||
AlignmentBuilderData.load()
|
||||
|
||||
# Interface panels often show properties. For convenience, define where
|
||||
# the properties are stored for the module.
|
||||
self.props = context.scene.BIMAlignmentBuilderProperties
|
||||
|
||||
# This defines a new "row" in our layout. When a new row is defined, the
|
||||
# things on that row, like buttons, labels, and input fields, show on a
|
||||
# new line.
|
||||
row = self.layout.row()
|
||||
# This is the simplest interface element - a label placed in our row.
|
||||
# If you enable the "Icon Viewer" add-on, you can view a list of
|
||||
# Blender's built-in icons to choose from in the Blender text editor's
|
||||
# side panel.
|
||||
row.label(text="This is a demo panel", icon="INFO")
|
||||
|
||||
# Our interface can contain simple logic. For example, if we don't have
|
||||
# an IFC project, show an error message and don't draw anything else.
|
||||
# The ui.py should always have very simple logic. Details like how to
|
||||
# determine whether we have a project is delegated to the data loader.
|
||||
if not AlignmentBuilderData.data["has_project"]:
|
||||
row = self.layout.row()
|
||||
row.label(text="Load or create an IFC project first", icon="ERROR")
|
||||
return
|
||||
|
||||
row = self.layout.row()
|
||||
# When you want to show a button in the interface, you have to specify
|
||||
# an operator. Operators are defined in operator.py. You reference an
|
||||
# operator using its unique ID.
|
||||
row.operator("bim.build_alignment")
|
||||
|
||||
# Blender properties can be used to affect what your panel shows. Notice
|
||||
# how the logic is very simple. Only use simple boolean predicates and
|
||||
# loops in your layout. No complex logic should ever been seen in the
|
||||
# interface code.
|
||||
#if self.props.message:
|
||||
# row = self.layout.row()
|
||||
# row.label(text=self.props.message)
|
||||
|
||||
#row = self.layout.row(align=True)
|
||||
#row.label(text="Build Dummy Alignment")
|
||||
# Sometimes you need data that doesn't come from Blender properties,
|
||||
# like from your IFC model. In this case, just get some data from your
|
||||
# data loader. For those familiar with how templating languages work,
|
||||
# this is exactly the same.
|
||||
#row.label(text=AlignmentData.data["project_name"])
|
||||
|
||||
#row = self.layout.row()
|
||||
# We've seen how to show text labels and buttons using operators. What
|
||||
# about text input fields, number sliders, dropdowns, checkboxes, and
|
||||
# scrollable lists? The way Blender works is that you simply show a
|
||||
# Blender property in your interface. The data type of that property
|
||||
# determines which UI widget is shown. A text property will show a text
|
||||
# field. A number property will show a number slider. An enum property
|
||||
# will show a drop down. And so on. In this case, our name property is a
|
||||
# text data type, so expect to see a text input field show up here.
|
||||
#row.prop(self.props, "name")
|
||||
#row = self.layout.row()
|
||||
# Here's another button, referencing another operator.
|
||||
#row.operator("bim.demonstrate_rename_project")
|
||||
|
||||
row = self.layout.row()
|
||||
row.label(text="Survey Point")
|
||||
row.prop(self.props,"x")
|
||||
row.prop(self.props,"y")
|
||||
row = self.layout.row()
|
||||
row.operator("bim.add_survey_point")
|
||||
|
||||
#if self.props.show_hints:
|
||||
# row = self.layout.row()
|
||||
# row.label(text="Name cannot be blank!")
|
||||
@@ -26,30 +26,42 @@ 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, context):
|
||||
def get_objs_for_operation(
|
||||
operator_properties: "AttributesOperator", context: bpy.types.Context
|
||||
) -> list[bpy.types.Object]:
|
||||
if operator_properties.obj:
|
||||
return [bpy.data.objects[operator_properties.obj]]
|
||||
if operator_properties.mass_operation:
|
||||
return context.selected_objects[:]
|
||||
return [context.active_object]
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
return [obj]
|
||||
|
||||
|
||||
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"}
|
||||
class AttributesOperator:
|
||||
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)
|
||||
|
||||
def enable_editing_attribute_on_obj(self, obj):
|
||||
props = obj.BIMAttributeProperties
|
||||
|
||||
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)
|
||||
props.attributes.clear()
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -87,20 +99,14 @@ class EnableEditingAttributes(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableEditingAttributes(bpy.types.Operator):
|
||||
class DisableEditingAttributes(bpy.types.Operator, AttributesOperator):
|
||||
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 invoke(self, context, event):
|
||||
self.mass_operation = event.alt
|
||||
return self.execute(context)
|
||||
|
||||
def disable_editing_attributes_on_obj(self, obj):
|
||||
props = obj.BIMAttributeProperties
|
||||
def disable_editing_attributes_on_obj(self, obj: bpy.types.Object) -> None:
|
||||
props = tool.Blender.get_object_attribute_props(obj)
|
||||
props.is_editing_attributes = False
|
||||
|
||||
def execute(self, context):
|
||||
@@ -118,7 +124,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 (element := tool.Ifc.get_entity(obj)):
|
||||
if not obj or not (element := tool.Ifc.get_entity(obj)):
|
||||
return
|
||||
|
||||
def callback(attributes, prop):
|
||||
@@ -130,7 +136,7 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
|
||||
attributes[prop.name] = None
|
||||
return True
|
||||
|
||||
props = obj.BIMAttributeProperties
|
||||
props = tool.Blender.get_object_attribute_props(obj)
|
||||
attributes = bonsai.bim.helper.export_attributes(props.attributes, callback=callback)
|
||||
ifcopenshell.api.attribute.edit_attributes(self.file, product=element, attributes=attributes)
|
||||
|
||||
@@ -165,13 +171,12 @@ class GenerateGlobalId(bpy.types.Operator, tool.Ifc.Operator):
|
||||
element.GlobalId = ifcopenshell.guid.new()
|
||||
|
||||
obj = context.active_object
|
||||
if not obj or not obj.BIMAttributeProperties.is_editing_attributes:
|
||||
if not obj or not (props := tool.Blender.get_object_attribute_props(obj)).is_editing_attributes:
|
||||
return {"FINISHED"}
|
||||
|
||||
props = obj.BIMAttributeProperties
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
if not element.is_a("IfcRoot"):
|
||||
if not element or not element.is_a("IfcRoot"):
|
||||
return {"FINISHED"}
|
||||
|
||||
if self.use_selected and obj in context.selected_objects:
|
||||
@@ -191,7 +196,10 @@ class CopyAttributeToSelection(bpy.types.Operator, tool.Ifc.Operator):
|
||||
name: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
value = tool.Blender.get_active_object().BIMAttributeProperties.attributes.get(self.name).get_value()
|
||||
obj = tool.Blender.get_active_object()
|
||||
assert obj
|
||||
props = tool.Blender.get_object_attribute_props(obj)
|
||||
value = props.attributes[self.name].get_value()
|
||||
total = core.copy_attribute_to_selection(
|
||||
tool.Ifc, tool.Blender, tool.Root, tool.Spatial, name=self.name, value=value
|
||||
)
|
||||
|
||||
@@ -29,8 +29,13 @@ 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,14 +17,16 @@
|
||||
# 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, layout, attributes):
|
||||
def draw_ui(context: bpy.types.Context, layout: bpy.types.UILayout, attributes) -> None:
|
||||
obj = context.active_object
|
||||
props = obj.BIMAttributeProperties
|
||||
assert obj
|
||||
props = tool.Blender.get_object_attribute_props(obj)
|
||||
|
||||
if props.is_editing_attributes:
|
||||
row = layout.row(align=True)
|
||||
|
||||
@@ -1283,6 +1283,7 @@ 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"
|
||||
@@ -1400,6 +1401,7 @@ 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")
|
||||
|
||||
@@ -43,6 +43,8 @@ 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)
|
||||
|
||||
@@ -49,6 +49,8 @@ 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)
|
||||
|
||||
@@ -30,6 +30,7 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
class BIMContextProperties(PropertyGroup):
|
||||
@@ -66,3 +67,10 @@ 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,13 +33,14 @@ class BIM_PT_context(bpy.types.Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Ifc.get()
|
||||
return bool(tool.Ifc.get())
|
||||
|
||||
def draw(self, context):
|
||||
if not ContextData.is_loaded:
|
||||
ContextData.load()
|
||||
|
||||
props = context.scene.BIMContextProperties
|
||||
assert self.layout
|
||||
props = tool.Context.get_context_props()
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "contexts", text="")
|
||||
|
||||
@@ -61,6 +61,7 @@ classes = (
|
||||
operator.ExportCostSchedules,
|
||||
operator.HighlightProductCostItem,
|
||||
operator.ImportCostScheduleCsv,
|
||||
operator.RefreshCostScheduleCsv,
|
||||
operator.LoadCostItemElementQuantities,
|
||||
operator.LoadCostItemQuantities,
|
||||
operator.LoadCostItemResourceQuantities,
|
||||
@@ -82,6 +83,7 @@ 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
|
||||
from typing import Any, Union
|
||||
|
||||
|
||||
def refresh():
|
||||
@@ -33,13 +33,19 @@ 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):
|
||||
def load(cls) -> None:
|
||||
cls.data = {
|
||||
"predefined_types": cls.get_cost_schedule_types(),
|
||||
"total_cost_schedules": cls.total_cost_schedules(),
|
||||
@@ -54,18 +60,18 @@ class CostSchedulesData:
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def currency(cls):
|
||||
def currency(cls) -> Union[Currency, None]:
|
||||
unit = tool.Unit.get_project_currency_unit()
|
||||
if unit:
|
||||
return {"id": unit.id(), "name": unit.Currency}
|
||||
|
||||
@classmethod
|
||||
def total_cost_schedules(cls):
|
||||
def total_cost_schedules(cls) -> int:
|
||||
return len(tool.Ifc.get().by_type("IfcCostSchedule"))
|
||||
|
||||
@classmethod
|
||||
def schedules(cls):
|
||||
results = []
|
||||
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)
|
||||
@@ -88,19 +94,19 @@ class CostSchedulesData:
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def is_editing_rates(cls):
|
||||
def is_editing_rates(cls) -> bool:
|
||||
props = tool.Cost.get_cost_props()
|
||||
ifc_id = props.active_cost_schedule_id
|
||||
if not ifc_id:
|
||||
return
|
||||
return False
|
||||
return tool.Ifc.get().by_id(ifc_id).PredefinedType == "SCHEDULEOFRATES"
|
||||
|
||||
@classmethod
|
||||
def cost_items(cls):
|
||||
def cost_items(cls) -> dict[int, CostItem]:
|
||||
cls._cost_values = {}
|
||||
results = {}
|
||||
results: dict[int, CostItem] = {}
|
||||
for cost_item in tool.Ifc.get().by_type("IfcCostItem"):
|
||||
data = {}
|
||||
data: CostItem = {}
|
||||
cls._load_cost_item_quantities(cost_item, data)
|
||||
cls._load_cost_values(cost_item, data)
|
||||
cls._load_nesting_index(cost_item, data)
|
||||
@@ -108,13 +114,13 @@ class CostSchedulesData:
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def _load_nesting_index(cls, cost_item, data):
|
||||
def _load_nesting_index(cls, cost_item: ifcopenshell.entity_instance, data: CostItem) -> None:
|
||||
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, data):
|
||||
def _load_cost_values(cls, root_element: ifcopenshell.entity_instance, data: CostItem) -> None:
|
||||
# data["CostValues"] = []
|
||||
data["CategoryValues"] = {}
|
||||
data["UnitBasisValueComponent"] = None
|
||||
@@ -123,6 +129,7 @@ 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"):
|
||||
@@ -150,7 +157,7 @@ class CostSchedulesData:
|
||||
data["TotalAppliedValue"] = None
|
||||
|
||||
@classmethod
|
||||
def _load_cost_item_quantities(cls, cost_item, data):
|
||||
def _load_cost_item_quantities(cls, cost_item: ifcopenshell.entity_instance, data: CostItem) -> None:
|
||||
# parametric_quantities = []
|
||||
# for rel in cost_item.Controls:
|
||||
# for related_object in rel.RelatedObjects or []:
|
||||
@@ -158,8 +165,9 @@ class CostSchedulesData:
|
||||
# parametric_quantities.extend(quantities)
|
||||
data["TotalCostQuantity"] = ifcopenshell.util.cost.get_total_quantity(cost_item)
|
||||
data["UnitSymbol"] = "-"
|
||||
if cost_item.CostQuantities:
|
||||
quantity = cost_item.CostQuantities[0]
|
||||
quantities: list[ifcopenshell.entity_instance] = cost_item.CostQuantities
|
||||
if quantities:
|
||||
quantity = quantities[0]
|
||||
data["QuantityType"] = quantity.is_a()
|
||||
unit = ifcopenshell.util.unit.get_property_unit(quantity, tool.Ifc.get())
|
||||
if unit:
|
||||
@@ -213,23 +221,31 @@ class CostSchedulesData:
|
||||
) -> list[int]:
|
||||
if not element.is_a("IfcObject"):
|
||||
return []
|
||||
cost_quantities = cost_item.CostQuantities
|
||||
cost_quantities: list[ifcopenshell.entity_instance] = cost_item.CostQuantities
|
||||
if not cost_quantities:
|
||||
return []
|
||||
results = []
|
||||
|
||||
results: list[int] = []
|
||||
relationship: ifcopenshell.entity_instance
|
||||
for relationship in element.IsDefinedBy:
|
||||
if not relationship.is_a("IfcRelDefinesByProperties"):
|
||||
continue
|
||||
qto = relationship.RelatingPropertyDefinition
|
||||
qto: ifcopenshell.entity_instance = 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, root_element_data, cost_value):
|
||||
def _load_cost_value(
|
||||
cls,
|
||||
root_element: ifcopenshell.entity_instance,
|
||||
root_element_data: CostItem,
|
||||
cost_value: ifcopenshell.entity_instance,
|
||||
) -> None:
|
||||
value_data = cost_value.get_info()
|
||||
del value_data["AppliedValue"]
|
||||
if value_data["UnitBasis"]:
|
||||
@@ -256,8 +272,8 @@ class CostSchedulesData:
|
||||
cls._load_cost_value(root_element, root_element_data, component)
|
||||
|
||||
@classmethod
|
||||
def cost_quantities(cls):
|
||||
results = []
|
||||
def cost_quantities(cls) -> list[CostQuantity]:
|
||||
results: list[CostQuantity] = []
|
||||
props = tool.Cost.get_cost_props()
|
||||
ifc_id = props.active_cost_item_id
|
||||
if not ifc_id:
|
||||
@@ -267,7 +283,7 @@ class CostSchedulesData:
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def cost_values(cls):
|
||||
def cost_values(cls) -> list[dict[str, str]]:
|
||||
props = tool.Cost.get_cost_props()
|
||||
ifc_id = props.active_cost_item_id
|
||||
if not ifc_id:
|
||||
@@ -275,14 +291,14 @@ class CostSchedulesData:
|
||||
return ifcopenshell.util.cost.get_cost_values(tool.Ifc.get().by_id(ifc_id))
|
||||
|
||||
@classmethod
|
||||
def quantity_types(cls):
|
||||
def quantity_types(cls) -> list[tuple[str, str, str]]:
|
||||
return [
|
||||
(t.name(), t.name(), "")
|
||||
for t in tool.Ifc.schema().declaration_by_name("IfcPhysicalSimpleQuantity").subtypes()
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_cost_schedule_types(cls):
|
||||
def get_cost_schedule_types(cls) -> list[tuple[str, str, str]]:
|
||||
types = ifcopenshell.util.cost.get_cost_schedule_types(tool.Ifc.get())
|
||||
return [(t["name"], t["name"], t["description"]) for t in types]
|
||||
|
||||
@@ -299,7 +315,7 @@ class CostItemRatesData:
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def schedule_of_rates(cls):
|
||||
def schedule_of_rates(cls) -> list[tuple[str, str, str]]:
|
||||
return [
|
||||
(str(s.id()), s.Name or "Unnamed", "")
|
||||
for s in tool.Ifc.get().by_type("IfcCostSchedule")
|
||||
|
||||
@@ -24,6 +24,7 @@ import bonsai.tool as tool
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.cost as core
|
||||
from pathlib import Path
|
||||
from typing import get_args, TYPE_CHECKING, Literal
|
||||
|
||||
|
||||
@@ -536,6 +537,7 @@ 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"})
|
||||
@@ -550,7 +552,19 @@ class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator)
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
core.import_cost_schedule_csv(tool.Cost, self.filepath, self.is_schedule_of_rates)
|
||||
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)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -686,8 +700,9 @@ class ExportCostSchedules(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.export_cost_schedules"
|
||||
bl_label = "Export Cost Schedule"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Export a cost schedule to a CSV, XSLX OR ODS file"
|
||||
cost_schedule: bpy.props.IntProperty()
|
||||
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"})
|
||||
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(
|
||||
@@ -696,7 +711,15 @@ class ExportCostSchedules(bpy.types.Operator, ExportHelper):
|
||||
)
|
||||
|
||||
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:
|
||||
@@ -705,15 +728,12 @@ class ExportCostSchedules(bpy.types.Operator, ExportHelper):
|
||||
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, filepath=self.directory, format=self.format, cost_schedule=cost_schedule
|
||||
tool.Cost, dirpath=self.directory, format=self.format, cost_schedule=cost_schedule
|
||||
)
|
||||
if isinstance(r, str):
|
||||
self.report({"ERROR"}, r)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
return ExportHelper.invoke(self, context, event)
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.label(text="Choose a format")
|
||||
self.layout.prop(self, "format")
|
||||
|
||||
@@ -90,7 +90,7 @@ 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.get("Identification")
|
||||
attribute = props.cost_item_attributes["Identification"]
|
||||
attribute.string_value = self.identification
|
||||
|
||||
|
||||
@@ -105,7 +105,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.get("Name")
|
||||
attribute = props.cost_item_attributes["Name"]
|
||||
attribute.string_value = self.name
|
||||
|
||||
|
||||
@@ -167,6 +167,15 @@ class CostItemQuantity(PropertyGroup):
|
||||
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")
|
||||
@@ -272,6 +281,7 @@ 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
|
||||
@@ -326,3 +336,4 @@ class BIMCostProperties(PropertyGroup):
|
||||
show_cost_item_operators: bool
|
||||
currency: str
|
||||
custom_currency: str
|
||||
cost_schedule_files: bpy.types.bpy_prop_collection_idprop[CostItemsMapping]
|
||||
|
||||
@@ -71,6 +71,26 @@ 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)
|
||||
@@ -364,8 +384,29 @@ 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")
|
||||
# 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 = col.row()
|
||||
row2.template_list(
|
||||
|
||||
@@ -44,6 +44,7 @@ classes = (
|
||||
operator.SelectHighPolygonMeshes,
|
||||
operator.SelectHighestPolygonMeshes,
|
||||
operator.ToggleDetailedIOSLogs,
|
||||
operator.ValidateIfcAssets,
|
||||
operator.ValidateIfcFile,
|
||||
prop.BIMDebugProperties,
|
||||
ui.BIM_PT_debug,
|
||||
|
||||
@@ -37,6 +37,7 @@ 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
|
||||
@@ -152,6 +153,67 @@ 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"
|
||||
@@ -962,7 +1024,7 @@ class RestartBlender(bpy.types.Operator):
|
||||
if not ms_store_app_id:
|
||||
path = bpy.app.binary_path
|
||||
if platform.system() == "Windows":
|
||||
args = sys.argv[1:]
|
||||
args = sys.argv[1:]
|
||||
command_line = subprocess.list2cmdline([path] + args)
|
||||
os.execv(path, [command_line])
|
||||
else:
|
||||
|
||||
@@ -50,6 +50,8 @@ 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")
|
||||
|
||||
|
||||
@@ -132,13 +132,14 @@ 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.get(self.obj)] if self.obj else tool.Blender.get_selected_objects()
|
||||
objs = [bpy.data.objects[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
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
|
||||
def update_document_name(self: "Document", context: bpy.types.Context) -> None:
|
||||
@@ -79,3 +79,7 @@ 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,14 +61,17 @@ 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 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
|
||||
elif active_document:
|
||||
ifc_definition_id = active_document.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
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ classes = (
|
||||
operator.AddAnnotationType,
|
||||
operator.AddDrawing,
|
||||
operator.AddDrawingStyle,
|
||||
operator.AddDrawingStyleAttribute,
|
||||
operator.AddDrawingToSheet,
|
||||
operator.AddReference,
|
||||
operator.AddReferenceImage,
|
||||
@@ -80,12 +79,10 @@ 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,52 +29,6 @@ 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 = tool.Blender.get_object_bim_props(bpy.context.scene.camera).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
|
||||
@@ -129,8 +83,14 @@ 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)
|
||||
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
|
||||
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)
|
||||
matrix_world = tool.Drawing.get_camera_matrix(camera)
|
||||
matrix_world.translation = co1
|
||||
collection = tool.Blender.get_object_bim_props(camera).collection
|
||||
|
||||
@@ -34,9 +34,7 @@ def refresh():
|
||||
DrawingsData.is_loaded = False
|
||||
ElementFiltersData.is_loaded = False
|
||||
AnnotationData.is_loaded = False
|
||||
DecoratorData.data = {}
|
||||
DecoratorData.cut_cache = {}
|
||||
DecoratorData.layerset_cache = {}
|
||||
DecoratorData.is_loaded = False
|
||||
|
||||
|
||||
class ProductAssignmentsData:
|
||||
@@ -120,7 +118,7 @@ class DrawingsData:
|
||||
return len([e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"])
|
||||
|
||||
@classmethod
|
||||
def location_hint(cls):
|
||||
def location_hint(cls) -> list[tuple[tool.Drawing.LocationHintType, str, str]]:
|
||||
props = tool.Drawing.get_document_props()
|
||||
if props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
|
||||
results = [("0", "Origin", "")]
|
||||
@@ -134,12 +132,10 @@ class DrawingsData:
|
||||
|
||||
@classmethod
|
||||
def active_drawing_pset_data(cls):
|
||||
ifc_file = tool.Ifc.get()
|
||||
props = tool.Drawing.get_document_props()
|
||||
drawing_id = props.active_drawing_id
|
||||
if drawing_id == 0:
|
||||
drawing = props.get_active_drawing()
|
||||
if drawing is None:
|
||||
return {}
|
||||
drawing = ifc_file.by_id(drawing_id)
|
||||
return ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing")
|
||||
|
||||
|
||||
@@ -239,6 +235,25 @@ class DecoratorData:
|
||||
slice_cache = {}
|
||||
fill_cache = {}
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.cut_cache = {}
|
||||
cls.layerset_cache = {}
|
||||
|
||||
text = {}
|
||||
dimension = {}
|
||||
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_ifc_text_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)
|
||||
cls.data = {"text": text, "dimension": dimension}
|
||||
|
||||
@classmethod
|
||||
def get_batting_thickness(cls, obj):
|
||||
"""used by IfcAnnotations with ObjectType = "BATTING" """
|
||||
@@ -296,17 +311,10 @@ class DecoratorData:
|
||||
return display_data
|
||||
|
||||
@classmethod
|
||||
def get_ifc_text_data(cls, obj: bpy.types.Object) -> dict[str, Any]:
|
||||
def get_ifc_text_data(cls, obj: bpy.types.Object) -> dict:
|
||||
"""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 {}
|
||||
@@ -344,9 +352,7 @@ class DecoratorData:
|
||||
|
||||
literals_data.append(literal_data)
|
||||
|
||||
text_data = {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at}
|
||||
cls.data[obj.name] = text_data
|
||||
return text_data
|
||||
return {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at}
|
||||
|
||||
@classmethod
|
||||
def get_symbol(cls, obj: bpy.types.Object) -> Union[str, None]:
|
||||
@@ -359,19 +365,7 @@ 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")
|
||||
@@ -390,7 +384,7 @@ class DecoratorData:
|
||||
custom_unit_list = pset_data.get("CustomUnit", None) or ""
|
||||
custom_unit = custom_unit_list[0] if custom_unit_list else ""
|
||||
|
||||
dimension_data = {
|
||||
return {
|
||||
"dimension_style": dimension_style,
|
||||
"show_description_only": show_description_only,
|
||||
"suppress_zero_inches": suppress_zero_inches,
|
||||
@@ -399,8 +393,6 @@ class DecoratorData:
|
||||
"fill_bg": fill_bg,
|
||||
"custom_unit": custom_unit,
|
||||
}
|
||||
cls.data[obj.name] = dimension_data
|
||||
return dimension_data
|
||||
|
||||
|
||||
class AnnotationData:
|
||||
|
||||
@@ -318,6 +318,8 @@ 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)
|
||||
@@ -588,7 +590,7 @@ 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.location
|
||||
text_world_position = obj.matrix_world.translation
|
||||
|
||||
region = context.region
|
||||
region3d = context.region_data
|
||||
@@ -597,7 +599,7 @@ class BaseDecorator:
|
||||
if not (pos := location_3d_to_region_2d(region, region3d, text_world_position)):
|
||||
return
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
text_data = DecoratorData.get_ifc_text_data(obj)
|
||||
text_data = DecoratorData.data["text"].get(obj.name, None)
|
||||
if props.is_editing:
|
||||
text_data = text_data | props.get_text_edited_data()
|
||||
literals_data = text_data["Literals"]
|
||||
@@ -654,7 +656,10 @@ class DimensionDecorator(BaseDecorator):
|
||||
viewportDrawingScale = self.get_viewport_drawing_scale(context)
|
||||
|
||||
# setup geometry parameters
|
||||
dimension_style = DecoratorData.get_dimension_data(obj)["dimension_style"]
|
||||
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
|
||||
if not dimension_data:
|
||||
return
|
||||
dimension_style = dimension_data["dimension_style"]
|
||||
if dimension_style == "oblique":
|
||||
size = viewportDrawingScale * 10 # OLBIQUE_SYMBOL_SIZE
|
||||
angle = radians(45)
|
||||
@@ -716,7 +721,9 @@ class DimensionDecorator(BaseDecorator):
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
description = element.Description
|
||||
dimension_data = DecoratorData.get_dimension_data(obj)
|
||||
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
|
||||
if not dimension_data:
|
||||
return
|
||||
show_description_only = dimension_data["show_description_only"]
|
||||
text_prefix = dimension_data["text_prefix"]
|
||||
text_suffix = dimension_data["text_suffix"]
|
||||
@@ -950,7 +957,9 @@ class RadiusDecorator(BaseDecorator):
|
||||
return
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
description = element.Description
|
||||
dimension_data = DecoratorData.get_dimension_data(obj)
|
||||
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
|
||||
if not dimension_data:
|
||||
return
|
||||
viewportDrawingScale = self.get_viewport_drawing_scale(context)
|
||||
text_offset = 20 * viewportDrawingScale
|
||||
|
||||
@@ -1177,7 +1186,9 @@ class PlanLevelDecorator(BaseDecorator):
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
description = element.Description
|
||||
dimension_data = DecoratorData.get_dimension_data(obj)
|
||||
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
|
||||
if not dimension_data:
|
||||
return
|
||||
|
||||
for verts in splines:
|
||||
p0, p1 = [location_3d_to_region_2d(region, region3d, v) for v in verts[:2]]
|
||||
@@ -1260,7 +1271,9 @@ class SectionLevelDecorator(BaseDecorator):
|
||||
storey = tool.Drawing.get_annotation_element(element)
|
||||
tag = storey.Name if storey else element.Description
|
||||
|
||||
dimension_data = DecoratorData.get_dimension_data(obj)
|
||||
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
|
||||
if not dimension_data:
|
||||
return
|
||||
|
||||
for verts in splines:
|
||||
|
||||
@@ -1699,6 +1712,8 @@ 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)
|
||||
@@ -1717,11 +1732,11 @@ class CutDecorator:
|
||||
self.recalculate_fill(context, obj, element)
|
||||
|
||||
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
|
||||
DecoratorData.cut_cache[element.id()] = (False, False)
|
||||
else:
|
||||
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)
|
||||
else:
|
||||
DecoratorData.cut_cache[element.id()] = (False, False)
|
||||
|
||||
def recalculate_fill(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
element_id = element.id()
|
||||
@@ -1905,6 +1920,8 @@ class DecorationsHandler:
|
||||
def install(cls, context):
|
||||
if cls.installed:
|
||||
cls.uninstall()
|
||||
if not DecoratorData.is_loaded:
|
||||
DecoratorData.load()
|
||||
handler = cls()
|
||||
# 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
|
||||
@@ -1965,10 +1982,16 @@ class DecorationsHandler:
|
||||
return results
|
||||
|
||||
def __call__(self, context):
|
||||
collection, _ = helper.get_active_drawing(context.scene)
|
||||
if collection is None:
|
||||
props = tool.Drawing.get_document_props()
|
||||
drawing = props.get_active_drawing()
|
||||
if 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
|
||||
|
||||
if not DrawingsData.is_loaded:
|
||||
DrawingsData.load()
|
||||
|
||||
|
||||
@@ -414,6 +414,8 @@ class ExtrusionGuidesGizmo(CustomGizmo, types.Gizmo):
|
||||
shader_wrapper = ExtrusionGuidesShader()
|
||||
verts = [Vector((0, 0, 0)), Vector((0, 0, 1))]
|
||||
verts, edges = shader_wrapper.process_geometry(verts)
|
||||
if not tool.Blender.validate_shader_batch_data(verts, edges):
|
||||
verts, edges = [], []
|
||||
self.custom_shape = shader_wrapper, shader_wrapper.batch(
|
||||
pos=verts,
|
||||
indices=edges,
|
||||
|
||||
@@ -37,29 +37,23 @@ def depsgraph_update_pre_handler(scene):
|
||||
|
||||
|
||||
def set_active_camera_resolution(scene: bpy.types.Scene) -> None:
|
||||
"""Sync scene render resolution with the active drawing
|
||||
and prevent user from manually changing ``ortho_scale`` on IFC camera."""
|
||||
props = tool.Drawing.get_document_props()
|
||||
if not scene.camera or "/" not in scene.camera.name or not props.drawings:
|
||||
camera_obj = scene.camera
|
||||
if not camera_obj or "/" not in camera_obj.name or not props.drawings:
|
||||
return
|
||||
assert isinstance(scene.camera.data, bpy.types.Camera)
|
||||
props = scene.camera.data.BIMCameraProperties
|
||||
ortho_scale = max((props.width, props.height))
|
||||
aspect_ratio = props.width / props.height
|
||||
if (scene.camera.data.ortho_scale != ortho_scale) or (
|
||||
scene.render.resolution_x / scene.render.resolution_y != aspect_ratio
|
||||
assert isinstance((camera := camera_obj.data), bpy.types.Camera)
|
||||
props = tool.Drawing.get_camera_props(camera)
|
||||
|
||||
if camera.type != props.camera_type:
|
||||
camera.type = props.camera_type
|
||||
|
||||
ortho_scale, aspect_ratio = props.get_scale_and_aspect_ratio()
|
||||
scene_render = scene.render
|
||||
if (camera.ortho_scale != ortho_scale) or not tool.Cad.is_x(
|
||||
scene_render.resolution_x / scene_render.resolution_y, aspect_ratio
|
||||
):
|
||||
scene.camera.data.ortho_scale = ortho_scale
|
||||
|
||||
diagram_scale = tool.Drawing.get_diagram_scale(scene.camera)
|
||||
scale_ratio = tool.Drawing.get_scale_ratio(diagram_scale["Scale"])
|
||||
|
||||
if props.width > props.height:
|
||||
aspect_ratio = props.height / props.width
|
||||
raster_x = ortho_scale * scale_ratio * props.dpi / 0.0254
|
||||
raster_y = ortho_scale * aspect_ratio * scale_ratio * props.dpi / 0.0254
|
||||
else:
|
||||
aspect_ratio = props.width / props.height
|
||||
raster_x = ortho_scale * aspect_ratio * scale_ratio * props.dpi / 0.0254
|
||||
raster_y = ortho_scale * scale_ratio * props.dpi / 0.0254
|
||||
|
||||
scene.render.resolution_x = scene.camera.data.BIMCameraProperties.raster_x = int(raster_x)
|
||||
scene.render.resolution_y = scene.camera.data.BIMCameraProperties.raster_y = int(raster_y)
|
||||
raster_x, raster_y = props.update_camera_resolution()
|
||||
scene_render.resolution_x = raster_x
|
||||
scene_render.resolution_y = raster_y
|
||||
|
||||
@@ -305,7 +305,7 @@ def format_distance(
|
||||
if unit_length == "MILLIMETERS":
|
||||
value = value / 1000
|
||||
|
||||
if precision:
|
||||
if precision and isinstance(precision, float):
|
||||
value = precision * round(float(value) / precision)
|
||||
|
||||
if decimal_places is not None:
|
||||
@@ -392,18 +392,6 @@ def format_distance(
|
||||
return tx_dist
|
||||
|
||||
|
||||
def get_active_drawing(
|
||||
scene: bpy.types.Scene,
|
||||
) -> Union[tuple[bpy.types.Collection, bpy.types.Camera], tuple[None, None]]:
|
||||
"""Get active drawing collection and camera"""
|
||||
props = tool.Drawing.get_document_props()
|
||||
try:
|
||||
camera = tool.Ifc.get_object(tool.Ifc.get().by_id(props.active_drawing_id))
|
||||
return tool.Blender.get_object_bim_props(camera).collection, camera
|
||||
except:
|
||||
return None, None
|
||||
|
||||
|
||||
def get_project_collection(scene):
|
||||
"""Get main project collection"""
|
||||
|
||||
@@ -413,13 +401,15 @@ def get_project_collection(scene):
|
||||
return colls[0]
|
||||
|
||||
|
||||
def parse_diagram_scale(camera):
|
||||
def parse_diagram_scale(camera: bpy.types.Camera) -> float:
|
||||
"""Returns numeric value of scale"""
|
||||
if camera.BIMCameraProperties.diagram_scale == "CUSTOM":
|
||||
_, fraction = camera.BIMCameraProperties.custom_diagram_scale.split("|")
|
||||
props = tool.Drawing.get_camera_props(camera)
|
||||
if props.diagram_scale == "CUSTOM":
|
||||
numerator = props.custom_scale_numerator
|
||||
denominator = props.custom_scale_denominator
|
||||
else:
|
||||
_, fraction = camera.BIMCameraProperties.diagram_scale.split("|")
|
||||
numerator, denominator = fraction.split("/")
|
||||
_, fraction = props.diagram_scale.split("|")
|
||||
numerator, denominator = fraction.split("/")
|
||||
return float(numerator) / float(denominator)
|
||||
|
||||
|
||||
@@ -431,12 +421,11 @@ def ortho_view_frame(
|
||||
Similar to `bpy.types.Camera.view_frame`
|
||||
|
||||
:arg camera: camera of drawing
|
||||
:type camera: bpy.types.Camera + BIMCameraProperties
|
||||
:arg margin: margins, in scene units
|
||||
:type margin: float
|
||||
:return: (xmin, xmax, ymin, ymax, zmin, zmax) in local camera coordinates
|
||||
"""
|
||||
aspect = camera.BIMCameraProperties.raster_y / camera.BIMCameraProperties.raster_x
|
||||
props = tool.Drawing.get_camera_props(camera)
|
||||
aspect = props.raster_y / props.raster_x
|
||||
size = camera.ortho_scale
|
||||
hwidth = size * 0.5
|
||||
hheight = size * 0.5 * aspect
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,11 +22,13 @@ import json
|
||||
import enum
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.util.element
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.drawing as core
|
||||
import bonsai.bim.module.drawing.annotation as annotation
|
||||
import bonsai.bim.module.drawing.decoration as decoration
|
||||
from mathutils import Matrix
|
||||
from bonsai.bim.prop import BIMFilterGroup
|
||||
from bonsai.bim.module.drawing.data import DrawingsData, DecoratorData, SheetsData, AnnotationData
|
||||
from bonsai.bim.module.drawing.data import refresh as refresh_drawing_data
|
||||
@@ -44,7 +46,7 @@ from bpy.props import (
|
||||
CollectionProperty,
|
||||
BoolVectorProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Literal, Any
|
||||
from typing import TYPE_CHECKING, Literal, Any, Callable, get_args, Union
|
||||
|
||||
|
||||
diagram_scales_enum = []
|
||||
@@ -55,23 +57,23 @@ def purge():
|
||||
diagram_scales_enum = []
|
||||
|
||||
|
||||
def update_target_view(self, context):
|
||||
def update_target_view_doc(self: "DocProperties", context: bpy.types.Context) -> None:
|
||||
DrawingsData.data["location_hint"] = DrawingsData.location_hint()
|
||||
|
||||
|
||||
def get_location_hint(self, context):
|
||||
def get_location_hint(self: "DocProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
if not DrawingsData.is_loaded:
|
||||
DrawingsData.load()
|
||||
return DrawingsData.data["location_hint"]
|
||||
|
||||
|
||||
def update_diagram_scale(self, context):
|
||||
def update_diagram_scale(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
|
||||
if not self.update_props:
|
||||
return
|
||||
if not context.scene.camera or context.scene.camera.data != self.id_data:
|
||||
assert context.scene
|
||||
if not (camera := context.scene.camera) or camera.data != self.id_data:
|
||||
return
|
||||
element = tool.Ifc.get_entity(context.scene.camera)
|
||||
if not element:
|
||||
if not (element := tool.Ifc.get_entity(camera)):
|
||||
return
|
||||
try:
|
||||
element = (
|
||||
@@ -89,13 +91,15 @@ def update_diagram_scale(self, context):
|
||||
if pset:
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
else:
|
||||
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="EPset_Drawing")
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties=diagram_scale)
|
||||
pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=element, name="EPset_Drawing")
|
||||
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties=diagram_scale)
|
||||
self.update_camera_resolution()
|
||||
|
||||
|
||||
def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
|
||||
if not self.update_props:
|
||||
return
|
||||
assert context.scene
|
||||
if not context.scene.camera or context.scene.camera.data != self.id_data:
|
||||
return
|
||||
element = tool.Ifc.get_entity(context.scene.camera)
|
||||
@@ -118,8 +122,9 @@ def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> No
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"IsNTS": self.is_nts})
|
||||
|
||||
|
||||
def get_diagram_scales(self, context):
|
||||
def get_diagram_scales(self: "BIMCameraProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
global diagram_scales_enum
|
||||
assert context.scene
|
||||
if (
|
||||
len(diagram_scales_enum) < 1
|
||||
or (context.scene.unit_settings.system == "IMPERIAL" and len(diagram_scales_enum) == 13)
|
||||
@@ -200,44 +205,37 @@ def set_drawing_style_name(self: "DrawingStyle", new_value: str) -> None:
|
||||
bpy.ops.bim.save_drawing_styles_data(rename_style=True, rename_style_from=old_value, rename_style_to=new_value)
|
||||
|
||||
|
||||
def update_document_name(self, context):
|
||||
def update_document_name(self: "Document", context: bpy.types.Context) -> None:
|
||||
document = tool.Ifc.get().by_id(self.ifc_definition_id)
|
||||
core.update_document_name(tool.Ifc, tool.Drawing, document=document, name=self.name)
|
||||
|
||||
|
||||
def update_has_underlay(self, context):
|
||||
def update_has_underlay(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
|
||||
update_layer(self, context, "HasUnderlay", self.has_underlay)
|
||||
assert context.scene
|
||||
# making sure that camera is active
|
||||
if self.has_underlay and (context.scene.camera and context.scene.camera.data == self.id_data):
|
||||
bpy.ops.bim.reload_drawing_styles()
|
||||
bpy.ops.bim.activate_drawing_style()
|
||||
|
||||
|
||||
def update_has_linework(self, context):
|
||||
update_layer(self, context, "HasLinework", self.has_linework)
|
||||
def get_update_layer_callback(
|
||||
camera_prop_name: str, pset_prop_name: str
|
||||
) -> Callable[["BIMCameraProperties", bpy.types.Context], None]:
|
||||
def update_layer_callback(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
|
||||
update_layer(self, context, pset_prop_name, getattr(self, camera_prop_name))
|
||||
|
||||
return update_layer_callback
|
||||
|
||||
|
||||
def update_has_annotation(self, context):
|
||||
update_layer(self, context, "HasAnnotation", self.has_annotation)
|
||||
def update_target_view_camera(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
|
||||
if self.update_props and self.target_view != "MODEL_VIEW":
|
||||
self.camera_type = "ORTHO"
|
||||
update_layer(self, context, "TargetView", self.target_view)
|
||||
|
||||
|
||||
def update_dpi(self, context):
|
||||
update_layer(self, context, "DPI", self.dpi)
|
||||
|
||||
|
||||
def update_linework_mode(self, context):
|
||||
update_layer(self, context, "LineworkMode", self.linework_mode)
|
||||
|
||||
|
||||
def update_fill_mode(self, context):
|
||||
update_layer(self, context, "FillMode", self.fill_mode)
|
||||
|
||||
|
||||
def update_cut_mode(self, context):
|
||||
update_layer(self, context, "CutMode", self.cut_mode)
|
||||
|
||||
|
||||
def update_layer(self, context, name, value):
|
||||
def update_layer(self: "BIMCameraProperties", context: bpy.types.Context, name: str, value: Any) -> None:
|
||||
assert context.scene
|
||||
if not self.update_props:
|
||||
return
|
||||
if not context.scene.camera or context.scene.camera.data != self.id_data:
|
||||
@@ -287,10 +285,24 @@ class Variable(PropertyGroup):
|
||||
prop_key: StringProperty(name="Property Key")
|
||||
|
||||
|
||||
TargetView = Literal["PLAN_VIEW", "ELEVATION_VIEW", "SECTION_VIEW", "REFLECTED_PLAN_VIEW", "MODEL_VIEW"]
|
||||
TARGET_VIEW_ITEMS: list[tuple[TargetView, str, str]] = [
|
||||
("PLAN_VIEW", "Plan", ""),
|
||||
("ELEVATION_VIEW", "Elevation", ""),
|
||||
("SECTION_VIEW", "Section", ""),
|
||||
("REFLECTED_PLAN_VIEW", "RCP", ""),
|
||||
("MODEL_VIEW", "Model", ""),
|
||||
]
|
||||
|
||||
|
||||
class Drawing(PropertyGroup):
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
name: StringProperty(name="Name", update=update_drawing_name)
|
||||
target_view: StringProperty(name="Target View")
|
||||
target_view: EnumProperty(
|
||||
name="Target View",
|
||||
default="PLAN_VIEW",
|
||||
items=TARGET_VIEW_ITEMS,
|
||||
)
|
||||
is_selected: BoolProperty(name="Is Selected", default=True)
|
||||
is_drawing: BoolProperty(name="Is Drawing", default=False)
|
||||
is_expanded: BoolProperty(name="Is Expanded", default=True)
|
||||
@@ -298,7 +310,7 @@ class Drawing(PropertyGroup):
|
||||
if TYPE_CHECKING:
|
||||
ifc_definition_id: int
|
||||
name: str
|
||||
target_view: str
|
||||
target_view: TargetView
|
||||
is_selected: bool
|
||||
is_drawing: bool
|
||||
is_expanded: bool
|
||||
@@ -334,21 +346,34 @@ class Sheet(PropertyGroup):
|
||||
is_expanded: bool
|
||||
|
||||
|
||||
RenderType = Literal["DEFAULT", "VIEWPORT"]
|
||||
RENDER_TYPE_ENUM_ITEMS: dict[RenderType, tuple[str, str]] = {
|
||||
"DEFAULT": ("Default", "Use default Blender render."),
|
||||
"VIEWPORT": ("Viewport", "Render active viewport."),
|
||||
}
|
||||
|
||||
|
||||
class DrawingStyle(PropertyGroup):
|
||||
name: StringProperty(name="Name", get=get_drawing_style_name, set=set_drawing_style_name)
|
||||
raster_style: StringProperty(name="Raster Style", default="{}")
|
||||
raster_style: StringProperty(
|
||||
name="Raster Style",
|
||||
description="JSON string for drawing style settings.",
|
||||
default="{}",
|
||||
)
|
||||
render_type: EnumProperty(
|
||||
items=[
|
||||
("NONE", "None", ""),
|
||||
("DEFAULT", "Default", ""),
|
||||
("VIEWPORT", "Viewport", ""),
|
||||
],
|
||||
items=[(k, *v) for k, v in RENDER_TYPE_ENUM_ITEMS.items()],
|
||||
name="Render Type",
|
||||
default="VIEWPORT",
|
||||
)
|
||||
include_query: StringProperty(name="Include Query")
|
||||
exclude_query: StringProperty(name="Exclude Query")
|
||||
attributes: CollectionProperty(name="Attributes", type=StrProperty)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
raster_style: str
|
||||
render_type: RenderType
|
||||
include_query: str
|
||||
exclude_query: str
|
||||
|
||||
|
||||
class RasterStyleProperty(enum.Enum):
|
||||
@@ -375,16 +400,10 @@ class DocProperties(PropertyGroup):
|
||||
is_editing_schedules: BoolProperty(name="Is Editing Schedules", default=False)
|
||||
is_editing_references: BoolProperty(name="Is Editing References", default=False)
|
||||
target_view: EnumProperty(
|
||||
items=[
|
||||
("PLAN_VIEW", "Plan", ""),
|
||||
("ELEVATION_VIEW", "Elevation", ""),
|
||||
("SECTION_VIEW", "Section", ""),
|
||||
("REFLECTED_PLAN_VIEW", "RCP", ""),
|
||||
("MODEL_VIEW", "Model", ""),
|
||||
],
|
||||
items=TARGET_VIEW_ITEMS,
|
||||
name="Target View",
|
||||
default="PLAN_VIEW",
|
||||
update=update_target_view,
|
||||
update=update_target_view_doc,
|
||||
)
|
||||
location_hint: EnumProperty(items=get_location_hint, name="Location Hint")
|
||||
drawings: CollectionProperty(name="Drawings", type=Drawing)
|
||||
@@ -439,7 +458,7 @@ class DocProperties(PropertyGroup):
|
||||
is_editing_schedules: bool
|
||||
is_editing_references: bool
|
||||
target_view: Literal["PLAN_VIEW", "ELEVATION_VIEW", "SECTION_VIEW", "REFLECTED_PLAN_VIEW", "MODEL_VIEW"]
|
||||
location_hint: str
|
||||
location_hint: tool.Drawing.LocationHintType
|
||||
drawings: bpy.types.bpy_prop_collection_idprop[Drawing]
|
||||
active_drawing_id: int
|
||||
active_drawing_index: int
|
||||
@@ -472,6 +491,28 @@ class DocProperties(PropertyGroup):
|
||||
tolerance: float
|
||||
classes_to_wireframe: str
|
||||
|
||||
def get_active_drawing(self) -> Union[ifcopenshell.entity_instance, None]:
|
||||
drawing_id = self.active_drawing_id
|
||||
if drawing_id == 0:
|
||||
return None
|
||||
return tool.Ifc.get().by_id(drawing_id)
|
||||
|
||||
|
||||
def update_width_height(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
|
||||
self.update_camera_resolution()
|
||||
|
||||
|
||||
def update_camera_type(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
|
||||
assert isinstance(camera := self.id_data, bpy.types.Camera)
|
||||
camera.type = self.camera_type
|
||||
|
||||
|
||||
CameraType = Literal["PERSP", "ORTHO"]
|
||||
CAMERA_TYPE_ENUM_ITEMS: dict[CameraType, tuple[str, str]] = {
|
||||
"ORTHO": ("Orthographic", "Most common camera for the drawings, supporting all of the features."),
|
||||
"PERSP": ("Perspective", "The only avilable features for perspective camera: freestyle linework, underlay."),
|
||||
}
|
||||
|
||||
|
||||
class BIMCameraProperties(PropertyGroup):
|
||||
linework_mode: EnumProperty(
|
||||
@@ -481,7 +522,7 @@ class BIMCameraProperties(PropertyGroup):
|
||||
],
|
||||
default="OPENCASCADE",
|
||||
name="Linework Mode",
|
||||
update=update_linework_mode,
|
||||
update=get_update_layer_callback("LineworkMode", "linework_mode"),
|
||||
)
|
||||
fill_mode: EnumProperty(
|
||||
items=[
|
||||
@@ -491,7 +532,7 @@ class BIMCameraProperties(PropertyGroup):
|
||||
],
|
||||
default="NONE",
|
||||
name="Fill Mode",
|
||||
update=update_fill_mode,
|
||||
update=get_update_layer_callback("FillMode", "fill_mode"),
|
||||
)
|
||||
cut_mode: EnumProperty(
|
||||
items=[
|
||||
@@ -500,11 +541,32 @@ class BIMCameraProperties(PropertyGroup):
|
||||
],
|
||||
default="BISECT",
|
||||
name="Cut Mode",
|
||||
update=update_cut_mode,
|
||||
update=get_update_layer_callback("CutMode", "cut_mode"),
|
||||
)
|
||||
has_underlay: BoolProperty(name="Underlay", default=False, update=update_has_underlay)
|
||||
has_linework: BoolProperty(name="Linework", default=True, update=update_has_linework)
|
||||
has_annotation: BoolProperty(name="Annotation", default=True, update=update_has_annotation)
|
||||
|
||||
# EPset_Drawing.
|
||||
has_underlay: BoolProperty(
|
||||
name="Underlay",
|
||||
default=False,
|
||||
update=update_has_underlay,
|
||||
)
|
||||
has_linework: BoolProperty(
|
||||
name="Linework",
|
||||
default=True,
|
||||
update=get_update_layer_callback("has_linework", "HasLinework"),
|
||||
)
|
||||
has_annotation: BoolProperty(
|
||||
name="Annotation",
|
||||
default=True,
|
||||
update=get_update_layer_callback("has_annotation", "HasAnnotation"),
|
||||
)
|
||||
target_view: EnumProperty(
|
||||
name="Target View",
|
||||
default="PLAN_VIEW",
|
||||
items=TARGET_VIEW_ITEMS,
|
||||
update=update_target_view_camera,
|
||||
)
|
||||
|
||||
representation: StringProperty(name="Representation")
|
||||
view_name: StringProperty(name="View Name")
|
||||
diagram_scale: EnumProperty(items=get_diagram_scales, name="Drawing Scale", update=update_diagram_scale)
|
||||
@@ -512,26 +574,85 @@ class BIMCameraProperties(PropertyGroup):
|
||||
custom_scale_denominator: bpy.props.StringProperty(default="100", update=update_diagram_scale)
|
||||
raster_x: IntProperty(name="Raster X", default=1000)
|
||||
raster_y: IntProperty(name="Raster Y", default=1000)
|
||||
dpi: IntProperty(name="DPI", default=75, update=update_dpi)
|
||||
width: FloatProperty(name="Width", default=50, subtype="DISTANCE")
|
||||
height: FloatProperty(name="Height", default=50, subtype="DISTANCE")
|
||||
dpi: IntProperty(name="DPI", default=75, update=get_update_layer_callback("DPI", "dpi"))
|
||||
width: FloatProperty(name="Width", default=50, subtype="DISTANCE", update=update_width_height)
|
||||
height: FloatProperty(name="Height", default=50, subtype="DISTANCE", update=update_width_height)
|
||||
# Bonsai property is needed to prevent user from using unsupported panoramic camera.
|
||||
camera_type: EnumProperty(
|
||||
name="Camera Type",
|
||||
default="ORTHO",
|
||||
items=[(k, *v) for k, v in CAMERA_TYPE_ENUM_ITEMS.items()],
|
||||
update=update_camera_type,
|
||||
)
|
||||
is_nts: BoolProperty(name="Is NTS", update=update_is_nts)
|
||||
active_drawing_style_index: IntProperty(name="Active Drawing Style Index")
|
||||
filter_mode: StringProperty(name="Filter Mode", default="NONE")
|
||||
include_filter_groups: CollectionProperty(type=BIMFilterGroup, name="Include Filter")
|
||||
exclude_filter_groups: CollectionProperty(type=BIMFilterGroup, name="Exclude Filter")
|
||||
update_props: BoolProperty(name="Enable Props Auto Update", default=True)
|
||||
update_props: BoolProperty(
|
||||
name="Enable Props Auto Update",
|
||||
description="Update related EPset_Drawing pset on any change in camera properties.",
|
||||
default=True,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
linework_mode: Literal["OPENCASCADE", "FREESTYLE"]
|
||||
fill_mode: Literal["NONE", "SHAPELY", "SVGFILL"]
|
||||
cut_mode: Literal["BISECT", "OPENCASCADE"]
|
||||
|
||||
has_underlay: bool
|
||||
has_linework: bool
|
||||
has_annotation: bool
|
||||
target_view: TargetView
|
||||
|
||||
representation: str
|
||||
view_name: str
|
||||
diagram_scale: str
|
||||
custom_scale_numerator: str
|
||||
custom_scale_denominator: str
|
||||
raster_x: int
|
||||
raster_y: int
|
||||
dpi: int
|
||||
width: float
|
||||
height: float
|
||||
camera_type: CameraType
|
||||
is_nts: bool
|
||||
active_drawing_style_index: int
|
||||
filter_mode: str
|
||||
include_filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
|
||||
exclude_filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
|
||||
update_props: bool
|
||||
|
||||
def get_active_drawing_style(self) -> Union[DrawingStyle, None]:
|
||||
dprops = tool.Drawing.get_document_props()
|
||||
return tool.Blender.get_active_uilist_element(dprops.drawing_styles, self.active_drawing_style_index)
|
||||
|
||||
# For now, this JSON dump are all the parameters that determine a camera's "Block representation"
|
||||
# By checking this, you will know whether or not the camera IFC representation needs to be refreshed
|
||||
def update_representation(self, obj):
|
||||
def update_representation(self, matrix_world: Matrix) -> bool:
|
||||
"""Update ``representation`` based on current camera properties and the provided world matrix.
|
||||
|
||||
:return: ``True`` if ``representation`` was updated and
|
||||
representation should also be updated in IFC.
|
||||
"""
|
||||
# Matrix is used instead of Object so this works before the Object exists,
|
||||
# allowing all camera initialization to stay encapsulated in `create_camera`.
|
||||
camera = self.id_data
|
||||
assert isinstance(camera, bpy.types.Camera)
|
||||
|
||||
# Rounding is necessary to avoid float garbage differences
|
||||
# forcing unnecessary representation update.
|
||||
def round_(f: float) -> float:
|
||||
return round(f, 6)
|
||||
|
||||
representation = json.dumps(
|
||||
{
|
||||
"matrix": [list(x) for x in obj.matrix_world],
|
||||
"type": self.camera_type,
|
||||
"matrix": [[round_(v) for v in row] for row in matrix_world],
|
||||
"raster_x": self.raster_x,
|
||||
"raster_y": self.raster_y,
|
||||
"ortho_scale": obj.data.ortho_scale,
|
||||
"clip_end": obj.data.clip_end,
|
||||
"ortho_scale": round_(camera.ortho_scale),
|
||||
"clip_end": round_(camera.clip_end),
|
||||
}
|
||||
)
|
||||
if self.representation != representation:
|
||||
@@ -539,6 +660,41 @@ class BIMCameraProperties(PropertyGroup):
|
||||
return True
|
||||
return False
|
||||
|
||||
def update_camera_resolution(self) -> tuple[int, int]:
|
||||
"""Update ``camera.ortho_scale``, ``raster_x`` and ``raster_y``
|
||||
based on current ``width`` and ``height`` and diagram scale props.
|
||||
|
||||
:return: tuple[resolution_x, resolution_y]
|
||||
"""
|
||||
assert isinstance(camera := self.id_data, bpy.types.Camera)
|
||||
ortho_scale, aspect_ratio = self.get_scale_and_aspect_ratio()
|
||||
aspect_ratio = self.width / self.height
|
||||
|
||||
camera.ortho_scale = ortho_scale
|
||||
diagram_scale = tool.Drawing.get_diagram_scale(camera)
|
||||
scale_ratio = tool.Drawing.get_scale_ratio(diagram_scale["Scale"])
|
||||
|
||||
if self.width > self.height:
|
||||
aspect_ratio = self.height / self.width
|
||||
raster_x = ortho_scale * scale_ratio * self.dpi / 0.0254
|
||||
raster_y = ortho_scale * aspect_ratio * scale_ratio * self.dpi / 0.0254
|
||||
else:
|
||||
aspect_ratio = self.width / self.height
|
||||
raster_x = ortho_scale * aspect_ratio * scale_ratio * self.dpi / 0.0254
|
||||
raster_y = ortho_scale * scale_ratio * self.dpi / 0.0254
|
||||
|
||||
raster_x, raster_y = int(raster_x), int(raster_y)
|
||||
self.raster_x, self.raster_y = raster_x, raster_y
|
||||
return raster_x, raster_y
|
||||
|
||||
def get_scale_and_aspect_ratio(self) -> tuple[float, float]:
|
||||
"""
|
||||
:return: A tuple of calculated ortho scale and aspect ratio values.
|
||||
"""
|
||||
ortho_scale = max(self.width, self.height)
|
||||
aspect_ratio = self.width / self.height
|
||||
return ortho_scale, aspect_ratio
|
||||
|
||||
|
||||
DEFAULT_BOX_ALIGNMENT = [False] * 6 + [True] + [False] * 2
|
||||
BOX_ALIGNMENT_POSITIONS = [
|
||||
@@ -596,6 +752,12 @@ class LiteralProps(PropertyGroup):
|
||||
}
|
||||
return text_data
|
||||
|
||||
if TYPE_CHECKING:
|
||||
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
value: str
|
||||
box_alignment: str
|
||||
ifc_definition_id: int
|
||||
|
||||
|
||||
class BIMTextProperties(PropertyGroup):
|
||||
is_editing: BoolProperty(name="Is Editing", default=False)
|
||||
@@ -646,6 +808,10 @@ class BIMAssignedProductProperties(PropertyGroup):
|
||||
is_editing_product: BoolProperty(name="Is Editing Product", default=False)
|
||||
relating_product: PointerProperty(name="Relating Product", type=bpy.types.Object, poll=relating_product_poll)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_editing_product: bool
|
||||
relating_product: Union[bpy.types.Object, None]
|
||||
|
||||
|
||||
annotation_classes = [
|
||||
(x, *tool.Drawing.ANNOTATION_TYPES_DATA[x][:3], i) for i, x in enumerate(tool.Drawing.ANNOTATION_TYPES_DATA)
|
||||
|
||||
@@ -90,7 +90,6 @@ class SheetBuilder:
|
||||
layout_dir = os.path.dirname(layout_path)
|
||||
|
||||
drawing_path = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_reference(drawing))
|
||||
underlay_path = os.path.splitext(drawing_path)[0] + "-underlay.png"
|
||||
|
||||
if not os.path.exists(layout_path) or not os.path.exists(drawing_path):
|
||||
raise FileNotFoundError
|
||||
@@ -116,16 +115,6 @@ class SheetBuilder:
|
||||
|
||||
x, y = self.next_drawing_location(layout_root, view_width)
|
||||
|
||||
# add background
|
||||
if os.path.isfile(underlay_path):
|
||||
background = ET.SubElement(view, "image")
|
||||
background.attrib["data-type"] = "background"
|
||||
background.attrib["xlink:href"] = os.path.relpath(underlay_path, layout_dir)
|
||||
background.attrib["x"] = str(x)
|
||||
background.attrib["y"] = str(y)
|
||||
background.attrib["width"] = str(view_width)
|
||||
background.attrib["height"] = str(view_height)
|
||||
|
||||
# add foreground
|
||||
if os.path.isfile(drawing_path):
|
||||
foreground = ET.SubElement(view, "image")
|
||||
@@ -406,14 +395,11 @@ class SheetBuilder:
|
||||
|
||||
images = view.findall("{http://www.w3.org/2000/svg}image")
|
||||
|
||||
background = None
|
||||
foreground = None
|
||||
view_title = None
|
||||
|
||||
for image in images:
|
||||
if image.attrib["data-type"] == "background":
|
||||
background = image
|
||||
elif image.attrib["data-type"] == "foreground":
|
||||
if image.attrib["data-type"] == "foreground":
|
||||
foreground = image
|
||||
elif image.attrib["data-type"] == "view-title":
|
||||
view_title = image
|
||||
@@ -423,12 +409,6 @@ class SheetBuilder:
|
||||
svg = self.ensure_drawing_unique_styles(svg, drawing_id)
|
||||
view.append(svg)
|
||||
|
||||
if background is not None:
|
||||
background_path = os.path.join(self.layout_dir, self.get_href(background))
|
||||
raster_path = os.path.join(self.sheets_dir, os.path.basename(background_path))
|
||||
shutil.copy(background_path, raster_path)
|
||||
self.references["RASTER"].append(raster_path)
|
||||
|
||||
if view_title is not None:
|
||||
foreground_path = self.get_href(foreground)
|
||||
data = reference.get_info()
|
||||
@@ -521,8 +501,14 @@ class SheetBuilder:
|
||||
self.scale = embedded.attrib.get("data-scale")
|
||||
images = embedded.findall("{http://www.w3.org/2000/svg}image")
|
||||
for image in images:
|
||||
new_href = ntpath.basename(image.attrib.get("{http://www.w3.org/1999/xlink}href"))
|
||||
image.attrib["{http://www.w3.org/1999/xlink}href"] = new_href
|
||||
old_href = Path(image.attrib.get("{http://www.w3.org/1999/xlink}href"))
|
||||
if not os.path.isabs(old_href):
|
||||
template_dir = Path(os.path.join(self.layout_dir, svg_path)).resolve().parent
|
||||
old_href = Path(os.path.join(template_dir, old_href))
|
||||
old_href = old_href.absolute().resolve().as_posix()
|
||||
new_href = Path(os.path.join(self.sheets_dir, Path(old_href).name)).absolute().resolve().as_posix()
|
||||
shutil.copy(old_href, new_href)
|
||||
image.attrib["{http://www.w3.org/1999/xlink}href"] = Path(old_href).name
|
||||
for child in embedded:
|
||||
if "namedview" in child.tag:
|
||||
continue
|
||||
|
||||
@@ -22,6 +22,7 @@ import bpy
|
||||
import math
|
||||
import bmesh
|
||||
import shutil
|
||||
from bpy.types import SplineBezierPoints, SplinePoints
|
||||
import mathutils
|
||||
import xml.etree.ElementTree as ET
|
||||
import svgwrite
|
||||
@@ -37,7 +38,7 @@ from bonsai.bim.module.drawing.data import DrawingsData
|
||||
from bonsai.bim.module.drawing.data import DecoratorData
|
||||
from math import pi, ceil, atan, degrees, acos
|
||||
from mathutils import geometry, Vector
|
||||
from typing import Optional, Self
|
||||
from typing import Optional, Self, Union, Sequence, Callable
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -59,6 +60,8 @@ class External(svgwrite.container.Group):
|
||||
|
||||
|
||||
class SvgWriter:
|
||||
metadata: list[str]
|
||||
|
||||
def __init__(self):
|
||||
self.data_dir = None
|
||||
self.human_scale = "NTS"
|
||||
@@ -176,12 +179,20 @@ class SvgWriter:
|
||||
for child in root:
|
||||
self.svg.defs.add(External(child))
|
||||
|
||||
def draw_annotations(self, annotations, precision, decimal_places):
|
||||
def draw_annotations(
|
||||
self,
|
||||
annotations: list[ifcopenshell.entity_instance],
|
||||
precision: Union[float, None],
|
||||
decimal_places: Union[int, None],
|
||||
) -> Self:
|
||||
self.precision = precision
|
||||
self.decimal_places = decimal_places
|
||||
for element in annotations:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if not obj or (object_type := ifcopenshell.util.element.get_predefined_type(element)) == "DRAWING":
|
||||
if (
|
||||
not isinstance(obj, bpy.types.Object)
|
||||
or (object_type := ifcopenshell.util.element.get_predefined_type(element)) == "DRAWING"
|
||||
):
|
||||
continue
|
||||
elif object_type == "GRID":
|
||||
self.draw_grid_annotation(obj)
|
||||
@@ -256,11 +267,12 @@ class SvgWriter:
|
||||
get_text, tag, dimension_data, text_position=text_position, angle=angle, class_str="SECTIONLEVEL"
|
||||
)
|
||||
|
||||
def draw_stair_annotation(self, obj):
|
||||
def draw_stair_annotation(self, obj: bpy.types.Object) -> None:
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
matrix_world = obj.matrix_world
|
||||
classes = self.get_attribute_classes(obj)
|
||||
assert isinstance(obj.data, bpy.types.Curve)
|
||||
for spline in obj.data.splines:
|
||||
points = self.get_spline_points(spline)
|
||||
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
|
||||
@@ -284,7 +296,7 @@ class SvgWriter:
|
||||
)
|
||||
)
|
||||
|
||||
def draw_grid_annotation(self, obj):
|
||||
def draw_grid_annotation(self, obj: bpy.types.Object) -> None:
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
matrix_world = obj.matrix_world
|
||||
@@ -358,6 +370,7 @@ class SvgWriter:
|
||||
|
||||
def get_attribute_classes(self, obj: bpy.types.Object) -> list[str]:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
global_id = "GlobalId-{}".format(element.GlobalId)
|
||||
predefined_type = "PredefinedType-" + tool.Drawing.canonicalise_class_name(
|
||||
str(ifcopenshell.util.element.get_predefined_type(element))
|
||||
@@ -374,7 +387,7 @@ class SvgWriter:
|
||||
)
|
||||
return classes
|
||||
|
||||
def draw_line_annotation(self, obj):
|
||||
def draw_line_annotation(self, obj: bpy.types.Object) -> None:
|
||||
# TODO: properly scope these offsets
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
@@ -397,7 +410,7 @@ class SvgWriter:
|
||||
elif isinstance(obj.data, bpy.types.Mesh):
|
||||
self.draw_edge_annotation(obj, classes)
|
||||
|
||||
def draw_edge_annotation(self, obj, classes):
|
||||
def draw_edge_annotation(self, obj: bpy.types.Object, classes: list[str]) -> None:
|
||||
predefined_type = classes[2].split("-", 1)[1]
|
||||
|
||||
x_offset = self.raw_width / 2
|
||||
@@ -415,7 +428,7 @@ class SvgWriter:
|
||||
self.svg.line(start=start * self.svg_scale, end=end * self.svg_scale, class_=" ".join(classes))
|
||||
)
|
||||
|
||||
def draw_batting_annotation():
|
||||
def draw_batting_annotation() -> None:
|
||||
v0_global = matrix_world @ obj.data.vertices[0].co.xyz
|
||||
v1_global = matrix_world @ obj.data.vertices[1].co.xyz
|
||||
v0 = self.project_point_onto_camera(v0_global)
|
||||
@@ -481,41 +494,42 @@ class SvgWriter:
|
||||
)
|
||||
self.svg.add(self.svg.polyline(points=points, class_=" ".join(classes), style=polyline_style))
|
||||
|
||||
def draw_revision_cloud_annotation():
|
||||
def draw_revision_cloud_annotation() -> None:
|
||||
segment_width = 15.0
|
||||
base_height = 1
|
||||
width = 5
|
||||
|
||||
def get_svg_half_circle(height, width):
|
||||
def get_svg_half_circle(height: float, width: float) -> str:
|
||||
cp0 = f"0,-{height}"
|
||||
cp1 = f"{width},-{height}"
|
||||
end_point = f"{width},0"
|
||||
circle = f"c{cp0} {cp1} {end_point}"
|
||||
return circle
|
||||
|
||||
def get_revision_pattern(base_offset):
|
||||
def get_revision_pattern(base_offset: Vector) -> str:
|
||||
pattern = f"m{base_offset.x},{base_offset.y}"
|
||||
pattern += " " + get_svg_half_circle(2 * base_height, width)
|
||||
pattern += " " + get_svg_half_circle(2.5 * base_height, width)
|
||||
pattern += " " + get_svg_half_circle(1.5 * base_height, width)
|
||||
return pattern
|
||||
|
||||
def get_scale(size, direction):
|
||||
def get_scale(size: float, direction: Vector) -> str:
|
||||
original_edge = direction * size
|
||||
current_svg_segments = ceil(size / segment_width) * segment_width * direction
|
||||
scale = [1 if original_edge[i] == 0 else original_edge[i] / current_svg_segments[i] for i in range(2)]
|
||||
return "scale(%f, %f)" % (scale[0], scale[1])
|
||||
|
||||
def poly_to_edges(poly):
|
||||
edges = []
|
||||
def poly_to_edges(poly: Sequence[int]) -> list[tuple[int, int]]:
|
||||
edges: list[tuple[int, int]] = []
|
||||
n_verts = len(poly)
|
||||
lats_index = n_verts - 1
|
||||
last_index = n_verts - 1
|
||||
for i in range(len(poly)):
|
||||
edge = [poly[i], (poly[i + 1]) if i != lats_index else poly[0]]
|
||||
edge = (poly[i], (poly[i + 1]) if i != last_index else poly[0])
|
||||
edges.append(edge)
|
||||
return edges
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
safe_offset_x = 2.0
|
||||
marker_width = segment_width + safe_offset_x * 2
|
||||
market_height = 15.0
|
||||
@@ -573,7 +587,7 @@ class SvgWriter:
|
||||
)
|
||||
self.svg.add(polyline)
|
||||
|
||||
def draw_section_annotation():
|
||||
def draw_section_annotation() -> None:
|
||||
display_data = DecoratorData.get_section_markers_display_data(obj)
|
||||
connect_markers = display_data["connect_markers"]
|
||||
|
||||
@@ -624,7 +638,7 @@ class SvgWriter:
|
||||
for edge in obj.data.edges:
|
||||
draw_simple_edge_annotation(*edge.vertices[:])
|
||||
|
||||
def draw_leader_annotation(self, obj):
|
||||
def draw_leader_annotation(self, obj: bpy.types.Object) -> None:
|
||||
self.draw_line_annotation(obj)
|
||||
spline = obj.data.splines[0]
|
||||
spline_points = spline.bezier_points if spline.bezier_points else spline.points
|
||||
@@ -634,7 +648,7 @@ class SvgWriter:
|
||||
position = Vector((0, 0, 0))
|
||||
self.draw_text_annotation(obj, position)
|
||||
|
||||
def draw_section_annotation(self, obj):
|
||||
def draw_section_annotation(self, obj: bpy.types.Object) -> None:
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
|
||||
@@ -688,7 +702,7 @@ class SvgWriter:
|
||||
)
|
||||
)
|
||||
|
||||
def draw_elevation_annotation(self, obj):
|
||||
def draw_elevation_annotation(self, obj: bpy.types.Object) -> None:
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
symbol_position = self.project_point_onto_camera(obj.location)
|
||||
@@ -927,7 +941,7 @@ class SvgWriter:
|
||||
symbol_transform = self.get_symbol_transform(svg_position_str)
|
||||
self.draw_symbol(svg_id, symbol_transform)
|
||||
|
||||
def draw_break_annotations(self, obj):
|
||||
def draw_break_annotations(self, obj: bpy.types.Object) -> None:
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
|
||||
@@ -950,18 +964,20 @@ class SvgWriter:
|
||||
d = "M{}".format(d[1:])
|
||||
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
|
||||
|
||||
def draw_plan_level_annotation(self, obj):
|
||||
def draw_plan_level_annotation(self, obj: bpy.types.Object) -> None:
|
||||
offset = Vector([self.raw_width, self.raw_height]) / 2
|
||||
matrix_world = obj.matrix_world
|
||||
classes = self.get_attribute_classes(obj)
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
description = element.Description
|
||||
|
||||
dimension_data = DecoratorData.get_dimension_data(obj)
|
||||
suppress_zero_inches = dimension_data["suppress_zero_inches"]
|
||||
base_offset_y = 1.0
|
||||
|
||||
assert isinstance(obj.data, bpy.types.Curve)
|
||||
for spline in obj.data.splines:
|
||||
points = self.get_spline_points(spline)
|
||||
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
|
||||
@@ -1000,7 +1016,8 @@ class SvgWriter:
|
||||
box_alignment=box_alignment,
|
||||
)
|
||||
|
||||
def draw_angle_annotations(self, obj):
|
||||
def draw_angle_annotations(self, obj: bpy.types.Object) -> None:
|
||||
assert isinstance(obj.data, bpy.types.Curve)
|
||||
points = obj.data.splines[0].points
|
||||
# [1, 2, 3, 4, 5] -> [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
|
||||
points_chunked = [points[i : i + 3] for i in range(len(points) - 2)]
|
||||
@@ -1009,18 +1026,18 @@ class SvgWriter:
|
||||
points_chunk = [obj.matrix_world @ p.co.xyz for p in points_chunk]
|
||||
self.draw_svg_3point_arc(obj, points_chunk)
|
||||
|
||||
def draw_svg_3point_arc(self, obj, angle_points):
|
||||
def draw_svg_3point_arc(self, obj: bpy.types.Object, angle_points: list[Vector]) -> None:
|
||||
"""`angle_points` are expected to be already in world space"""
|
||||
# This implementation uses an SVG arc, which means that it can only draw
|
||||
# arcs that are orthogonal to the view (e.g. not arcs in 3D).
|
||||
# Gosh this is bad code :(
|
||||
|
||||
def position_on_svg(p):
|
||||
def position_on_svg(p: Vector) -> Vector:
|
||||
p = self.project_point_onto_camera(p)
|
||||
offset = Vector([self.raw_width / 2, self.raw_height / 2])
|
||||
return (offset + p.xy * Vector((1, -1))) * self.svg_scale
|
||||
|
||||
def get_angle_value():
|
||||
def get_angle_value() -> float:
|
||||
"""points should be in world space"""
|
||||
# calculate arc angle, need to make sure we do it in world space
|
||||
v0, v1, v2 = angle_points
|
||||
@@ -1104,10 +1121,11 @@ class SvgWriter:
|
||||
classes = self.get_attribute_classes(obj)
|
||||
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
|
||||
|
||||
def draw_radius_annotations(self, obj):
|
||||
def draw_radius_annotations(self, obj: bpy.types.Object) -> None:
|
||||
offset = Vector([self.raw_width, self.raw_height]) / 2
|
||||
classes = self.get_attribute_classes(obj)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
tag = element.Description
|
||||
matrix_world = obj.matrix_world
|
||||
|
||||
@@ -1141,7 +1159,7 @@ class SvgWriter:
|
||||
get_text, tag, dimension_data, text_position=text_position, class_str="RADIUS", box_alignment="center"
|
||||
)
|
||||
|
||||
def draw_dimension_text(self, get_text, tag, dimension_data, **create_text_kwargs):
|
||||
def draw_dimension_text(self, get_text: Callable[[], str], tag, dimension_data, **create_text_kwargs) -> None:
|
||||
prefix = dimension_data["text_prefix"]
|
||||
suffix = dimension_data["text_suffix"]
|
||||
show_description_only = dimension_data["show_description_only"]
|
||||
@@ -1166,12 +1184,13 @@ class SvgWriter:
|
||||
for text in text_tags:
|
||||
self.svg.add(text)
|
||||
|
||||
def draw_fall_annotations(self, obj):
|
||||
def draw_fall_annotations(self, obj: bpy.types.Object) -> None:
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
classes = self.get_attribute_classes(obj)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
matrix_world = obj.matrix_world
|
||||
assert isinstance(obj.data, bpy.types.Curve)
|
||||
for spline in obj.data.splines:
|
||||
points = self.get_spline_points(spline)
|
||||
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
|
||||
@@ -1236,13 +1255,14 @@ class SvgWriter:
|
||||
text_style = SvgWriter.get_box_alignment_parameters("center")
|
||||
self.svg.add(self.svg.text(tag, insert=tuple(text_position), class_="RADIUS", **text_style))
|
||||
|
||||
def draw_diameter_annotations(self, obj):
|
||||
def draw_diameter_annotations(self, obj: bpy.types.Object) -> None:
|
||||
classes = self.get_attribute_classes(obj)
|
||||
matrix_world = obj.matrix_world
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
dimension_text = element.Description
|
||||
dimension_data = DecoratorData.get_dimension_data(obj)
|
||||
|
||||
assert isinstance(obj.data, bpy.types.Curve)
|
||||
for spline in obj.data.splines:
|
||||
points = self.get_spline_points(spline)
|
||||
for i, p in enumerate(points):
|
||||
@@ -1264,13 +1284,14 @@ class SvgWriter:
|
||||
custom_unit=dimension_data["custom_unit"],
|
||||
)
|
||||
|
||||
def draw_dimension_annotations(self, obj):
|
||||
def draw_dimension_annotations(self, obj: bpy.types.Object) -> None:
|
||||
classes = self.get_attribute_classes(obj)
|
||||
matrix_world = obj.matrix_world
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
dimension_text = element.Description
|
||||
dimension_data = DecoratorData.get_dimension_data(obj)
|
||||
|
||||
assert isinstance(obj.data, bpy.types.Curve)
|
||||
for spline in obj.data.splines:
|
||||
points = self.get_spline_points(spline)
|
||||
for i in range(len(points) - 1):
|
||||
@@ -1289,7 +1310,7 @@ class SvgWriter:
|
||||
custom_unit=dimension_data["custom_unit"],
|
||||
)
|
||||
|
||||
def draw_measureit_arch_dimension_annotations(self):
|
||||
def draw_measureit_arch_dimension_annotations(self) -> None:
|
||||
try:
|
||||
import MeasureIt_ARCH.measureit_arch_external_utils
|
||||
|
||||
@@ -1303,9 +1324,9 @@ class SvgWriter:
|
||||
|
||||
def draw_dimension_annotation(
|
||||
self,
|
||||
v0_global,
|
||||
v1_global,
|
||||
classes,
|
||||
v0_global: Vector,
|
||||
v1_global: Vector,
|
||||
classes: list[str],
|
||||
dimension_text=None,
|
||||
text_format=lambda x: x,
|
||||
show_description_only=False,
|
||||
@@ -1314,7 +1335,7 @@ class SvgWriter:
|
||||
text_suffix="",
|
||||
fill_bg=False,
|
||||
custom_unit=None,
|
||||
):
|
||||
) -> None:
|
||||
offset = Vector([self.raw_width, self.raw_height]) / 2
|
||||
v0 = self.project_point_onto_camera(v0_global)
|
||||
v1 = self.project_point_onto_camera(v1_global)
|
||||
@@ -1446,7 +1467,7 @@ class SvgWriter:
|
||||
Vector(self.camera_projection),
|
||||
)
|
||||
|
||||
def get_spline_points(self, spline):
|
||||
def get_spline_points(self, spline: bpy.types.Spline) -> Union[SplineBezierPoints, SplinePoints]:
|
||||
return spline.bezier_points if spline.bezier_points else spline.points
|
||||
|
||||
def draw_polygon(self, polygon, position):
|
||||
|
||||
@@ -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/>.
|
||||
|
||||
from __future__ import annotations
|
||||
import bpy
|
||||
import bonsai.bim.helper
|
||||
import bonsai.tool as tool
|
||||
@@ -28,6 +29,10 @@ from bonsai.bim.module.drawing.data import (
|
||||
ElementFiltersData,
|
||||
DecoratorData,
|
||||
)
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.drawing.prop import DocProperties, Drawing
|
||||
|
||||
|
||||
class BIM_PT_camera(Panel):
|
||||
@@ -39,18 +44,21 @@ class BIM_PT_camera(Panel):
|
||||
bl_parent_id = "BIM_PT_tab_drawings"
|
||||
|
||||
def draw(self, context):
|
||||
if not (context.scene.camera and hasattr(context.scene.camera.data, "BIMCameraProperties")):
|
||||
assert context.scene and self.layout
|
||||
camera = context.scene.camera
|
||||
if not camera:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Active Drawing", icon="ERROR")
|
||||
return
|
||||
|
||||
if "/" not in context.scene.camera.name:
|
||||
if not tool.Ifc.get_entity(camera):
|
||||
self.layout.label(text="This is not a BIM camera.")
|
||||
return
|
||||
|
||||
assert isinstance(camera_data := camera.data, bpy.types.Camera)
|
||||
props = tool.Drawing.get_camera_props(camera)
|
||||
self.layout.use_property_split = True
|
||||
dprops = tool.Drawing.get_document_props()
|
||||
props = context.scene.camera.data.BIMCameraProperties
|
||||
|
||||
col = self.layout.column(align=True)
|
||||
row = col.row(align=True)
|
||||
@@ -63,6 +71,13 @@ class BIM_PT_camera(Panel):
|
||||
row.prop(props, "has_annotation", icon="MOD_EDGESPLIT")
|
||||
row.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "target_view")
|
||||
|
||||
if props.target_view == "MODEL_VIEW":
|
||||
row = self.layout.row()
|
||||
row.prop(props, "camera_type")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(props, "linework_mode")
|
||||
if props.linework_mode == "OPENCASCADE":
|
||||
@@ -77,7 +92,7 @@ class BIM_PT_camera(Panel):
|
||||
row.prop(props, "height")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(context.scene.camera.data, "clip_end", text="Depth")
|
||||
row.prop(camera_data, "clip_end", text="Depth")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "diagram_scale", text="Scale")
|
||||
@@ -110,7 +125,8 @@ class BIM_PT_element_filters(Panel):
|
||||
if not ElementFiltersData.is_loaded:
|
||||
ElementFiltersData.load()
|
||||
|
||||
props = context.scene.camera.data.BIMCameraProperties
|
||||
assert context.scene and (camera := context.scene.camera)
|
||||
props = tool.Drawing.get_camera_props(camera)
|
||||
|
||||
if props.filter_mode == "INCLUDE":
|
||||
bonsai.bim.helper.draw_filter(
|
||||
@@ -157,13 +173,13 @@ class BIM_PT_drawing_underlay(Panel):
|
||||
return bool((camera := context.scene.camera) and tool.Ifc.get_entity(camera))
|
||||
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
camera = context.scene.camera
|
||||
assert camera
|
||||
assert context.scene and (camera := context.scene.camera)
|
||||
dprops = tool.Drawing.get_document_props()
|
||||
props = camera.data.BIMCameraProperties
|
||||
drawing_index_is_valid = props.active_drawing_style_index < len(dprops.drawing_styles)
|
||||
props = tool.Drawing.get_camera_props(camera)
|
||||
drawing_style = props.get_active_drawing_style()
|
||||
|
||||
if not DrawingsData.is_loaded:
|
||||
DrawingsData.load()
|
||||
@@ -177,7 +193,7 @@ class BIM_PT_drawing_underlay(Panel):
|
||||
row.label(text="Current Shading Style:")
|
||||
row.label(text=current_shading_style)
|
||||
row.operator("bim.add_drawing_style", icon="ADD", text="")
|
||||
if drawing_index_is_valid:
|
||||
if drawing_style:
|
||||
row.operator("bim.remove_drawing_style", icon="X", text="").index = props.active_drawing_style_index
|
||||
row.operator("bim.reload_drawing_styles", icon="FILE_REFRESH", text="")
|
||||
|
||||
@@ -192,9 +208,8 @@ class BIM_PT_drawing_underlay(Panel):
|
||||
"active_drawing_style_index",
|
||||
)
|
||||
|
||||
if not drawing_index_is_valid:
|
||||
if drawing_style is None:
|
||||
return
|
||||
drawing_style = dprops.drawing_styles[props.active_drawing_style_index]
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(drawing_style, "name")
|
||||
@@ -206,14 +221,6 @@ class BIM_PT_drawing_underlay(Panel):
|
||||
row = layout.row(align=True)
|
||||
row.prop(drawing_style, "exclude_query")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.add_drawing_style_attribute")
|
||||
|
||||
for index, attribute in enumerate(drawing_style.attributes):
|
||||
row = layout.row(align=True)
|
||||
row.prop(attribute, "name", text="")
|
||||
row.operator("bim.remove_drawing_style_attribute", icon="X", text="").index = index
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.save_drawing_style")
|
||||
row.operator("bim.activate_drawing_style")
|
||||
@@ -266,9 +273,6 @@ class BIM_PT_drawings(Panel):
|
||||
row3 = row.row(align=True)
|
||||
row3.alignment = "RIGHT"
|
||||
|
||||
row3.operator("bim.activate_drawing", icon="OUTLINER_OB_CAMERA", text="").drawing = (
|
||||
active_drawing.ifc_definition_id
|
||||
)
|
||||
row3.operator("bim.activate_model", icon="VIEW3D", text="")
|
||||
|
||||
row3.separator(factor=0.5, type="SPACE")
|
||||
@@ -490,17 +494,19 @@ class BIM_PT_product_assignments(Panel):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get() or not context.active_object:
|
||||
return
|
||||
return False
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
if not element:
|
||||
return
|
||||
return False
|
||||
return element.is_a("IfcAnnotation")
|
||||
|
||||
def draw(self, context):
|
||||
if not ProductAssignmentsData.is_loaded:
|
||||
ProductAssignmentsData.load()
|
||||
|
||||
props = context.active_object.BIMAssignedProductProperties
|
||||
assert self.layout
|
||||
assert (obj := context.active_object)
|
||||
props = tool.Drawing.get_object_assigned_product_props(obj)
|
||||
|
||||
if props.is_editing_product:
|
||||
row = self.layout.row(align=True)
|
||||
@@ -607,7 +613,16 @@ class BIM_PT_text(Panel):
|
||||
|
||||
|
||||
class BIM_UL_drawinglist(bpy.types.UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
def draw_item(
|
||||
self,
|
||||
context,
|
||||
layout: bpy.types.UILayout,
|
||||
data: DocProperties,
|
||||
item: Drawing,
|
||||
icon,
|
||||
active_data,
|
||||
active_propname,
|
||||
):
|
||||
if not item:
|
||||
layout.label(text="", translate=False)
|
||||
return
|
||||
@@ -617,14 +632,22 @@ class BIM_UL_drawinglist(bpy.types.UIList):
|
||||
row.label(text="", icon="BLANK1")
|
||||
selected_icon = "CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT"
|
||||
row.prop(item, "is_selected", text="", icon=selected_icon, emboss=False)
|
||||
row.separator(factor=0.5, type="SPACE")
|
||||
row.prop(item, "name", text="", emboss=False)
|
||||
row.separator(factor=0.25, type="SPACE")
|
||||
self.props = tool.Drawing.get_document_props()
|
||||
if (
|
||||
self.props.drawings
|
||||
and self.props.active_drawing_id
|
||||
and item.ifc_definition_id == self.props.active_drawing_id
|
||||
):
|
||||
row.label(text="", icon="OUTLINER_OB_CAMERA")
|
||||
row.operator("bim.activate_drawing", text="", icon="VIEW_CAMERA", emboss=True, depress=True).drawing = (
|
||||
item.ifc_definition_id
|
||||
)
|
||||
else:
|
||||
row.operator("bim.activate_drawing", text="", icon="VIEW_CAMERA_UNSELECTED", emboss=False).drawing = (
|
||||
item.ifc_definition_id
|
||||
)
|
||||
else:
|
||||
if item.target_view == "PLAN_VIEW":
|
||||
icon = "UV_FACESEL"
|
||||
|
||||
@@ -56,7 +56,7 @@ classes = (
|
||||
operator.OverrideMeshSeparate,
|
||||
operator.OverrideModeSetEdit,
|
||||
operator.OverrideModeSetObject,
|
||||
operator.OverrideMove,
|
||||
operator.OverrideMoveSelect,
|
||||
operator.OverrideMoveMacro,
|
||||
operator.OverrideOriginSet,
|
||||
operator.OverrideOutlinerDelete,
|
||||
@@ -95,7 +95,7 @@ classes = (
|
||||
)
|
||||
|
||||
|
||||
addon_keymaps = []
|
||||
addon_keymaps: list[tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
|
||||
|
||||
|
||||
@persistent
|
||||
@@ -124,9 +124,9 @@ def register():
|
||||
operator.OverrideDuplicateMoveLinkedMacro.define("BIM_OT_override_object_duplicate_move_linked")
|
||||
operator.OverrideDuplicateMoveLinkedMacro.define("TRANSFORM_OT_translate")
|
||||
operator.DuplicateMoveLinkedAggregateMacro.define("BIM_OT_object_duplicate_move_linked_aggregate")
|
||||
operator.DuplicateMoveLinkedAggregateMacro.define("BIM_OT_override_move")
|
||||
operator.DuplicateMoveLinkedAggregateMacro.define("BIM_OT_override_move_select")
|
||||
operator.DuplicateMoveLinkedAggregateMacro.define("TRANSFORM_OT_translate")
|
||||
operator.OverrideMoveMacro.define("BIM_OT_override_move")
|
||||
operator.OverrideMoveMacro.define("BIM_OT_override_move_select")
|
||||
operator.OverrideMoveMacro.define("TRANSFORM_OT_translate")
|
||||
|
||||
bpy.types.Object.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMObjectGeometryProperties)
|
||||
@@ -156,11 +156,19 @@ def register():
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.override_mode_set_edit", "TAB", "PRESS")
|
||||
addon_keymaps.append((km, kmi))
|
||||
# Deletion.
|
||||
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS")
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS", shift=True)
|
||||
kmi.properties.use_global = True
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.override_object_delete", "DEL", "PRESS")
|
||||
kmi.properties.confirm = False
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.override_object_delete", "DEL", "PRESS", shift=True)
|
||||
kmi.properties.confirm = False
|
||||
kmi.properties.use_global = True
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Mesh", space_type="EMPTY")
|
||||
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
|
||||
|
||||
@@ -23,6 +23,7 @@ import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.schema
|
||||
import ifcopenshell.util.unit
|
||||
import bonsai.tool as tool
|
||||
from typing import Any, Union
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
@@ -45,13 +46,12 @@ class ViewportData:
|
||||
cls.data = {"mode": cls.mode()}
|
||||
|
||||
@classmethod
|
||||
def mode(cls):
|
||||
def mode(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
obj_mode = ("OBJECT", "IFC Object Mode", "View and move the placements of objects", "OBJECT_DATAMODE", 0)
|
||||
item_mode = ("ITEM", "IFC Item Mode", "View individual representation items", "MESH_DATA", 1)
|
||||
edit_mode = ("EDIT", "IFC Edit Mode", "Edit representation items", "EDITMODE_HLT", 2)
|
||||
|
||||
obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
modes: list[tuple[str, str, str, str, int]] = [obj_mode]
|
||||
gprops = tool.Geometry.get_geometry_props()
|
||||
@@ -61,13 +61,14 @@ class ViewportData:
|
||||
if not obj:
|
||||
return modes
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
pprops = tool.Project.get_project_props()
|
||||
if obj in pprops.clipping_planes_objs:
|
||||
pass
|
||||
elif element:
|
||||
if tool.Geometry.is_locked(element):
|
||||
pass
|
||||
elif obj.data and tool.Geometry.is_profile_based(obj.data):
|
||||
elif obj.data and tool.Geometry.has_mesh_properties(obj.data) and tool.Geometry.is_profile_based(obj.data):
|
||||
modes.append(edit_mode)
|
||||
elif element.is_a("IfcRelSpaceBoundary"):
|
||||
modes.append(edit_mode)
|
||||
@@ -88,7 +89,7 @@ class ViewportData:
|
||||
|
||||
|
||||
class RepresentationsData:
|
||||
data = {}
|
||||
data: dict[str, Any] = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
@@ -102,10 +103,12 @@ class RepresentationsData:
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def representations(cls):
|
||||
results = []
|
||||
def representations(cls) -> list[dict[str, Any]]:
|
||||
results: list[dict[str, Any]] = []
|
||||
obj = tool.Geometry.get_active_or_representation_obj()
|
||||
assert obj
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
|
||||
active_representation_id = None
|
||||
active_representation = tool.Geometry.get_active_representation(obj)
|
||||
@@ -132,8 +135,8 @@ class RepresentationsData:
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def contexts(cls):
|
||||
results = []
|
||||
def contexts(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
results: list[tuple[str, str, str]] = []
|
||||
for element in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
|
||||
results.append((str(element.id()), element.ContextType or "Unnamed", ""))
|
||||
for element in tool.Ifc.get().by_type("IfcGeometricRepresentationSubContext", include_subtypes=False):
|
||||
@@ -219,9 +222,12 @@ class ConnectionsData:
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def connections(cls):
|
||||
results = []
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
def connections(cls) -> list[dict[str, Any]]:
|
||||
results: list[dict[str, Any]] = []
|
||||
obj = bpy.context.active_object
|
||||
assert obj
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
|
||||
connected_to = getattr(element, "ConnectedTo", [])
|
||||
connected_from = getattr(element, "ConnectedFrom", [])
|
||||
@@ -285,13 +291,16 @@ class ConnectionsData:
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def is_connection_realization(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
def is_connection_realization(cls) -> Union[list[dict[str, Any]], None]:
|
||||
obj = bpy.context.active_object
|
||||
assert obj
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
connections = getattr(element, "IsConnectionRealization", None)
|
||||
if not connections:
|
||||
return
|
||||
|
||||
results = []
|
||||
results: list[dict[str, Any]] = []
|
||||
for rel in connections:
|
||||
data = {
|
||||
"realizing_elements_connection_type": rel.ConnectionType,
|
||||
@@ -322,16 +331,19 @@ class DerivedCoordinatesData:
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def load_z_values(cls):
|
||||
def load_z_values(cls) -> None:
|
||||
cls.z_values = [
|
||||
(bpy.context.active_object.matrix_world @ Vector(co))[2] for co in bpy.context.active_object.bound_box
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def load_collection(cls):
|
||||
def load_collection(cls) -> None:
|
||||
cls.collection = None
|
||||
cls.collection_z = 0
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
obj = bpy.context.active_object
|
||||
if not obj:
|
||||
return
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
parent = ifcopenshell.util.element.get_aggregate(element)
|
||||
@@ -392,7 +404,7 @@ class PlacementData:
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {"has_placement": cls.has_placement()}
|
||||
cls.data: dict[str, Any] = {"has_placement": cls.has_placement()}
|
||||
|
||||
props = tool.Georeference.get_georeference_props()
|
||||
obj = bpy.context.active_object
|
||||
@@ -415,13 +427,14 @@ class PlacementData:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def original_xyz(cls, obj):
|
||||
def original_xyz(cls, obj: bpy.types.Object) -> list[float]:
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
props = tool.Georeference.get_georeference_props()
|
||||
translation = obj.matrix_world.translation
|
||||
xyz = ifcopenshell.util.geolocation.xyz2enh(
|
||||
obj.matrix_world[0][3],
|
||||
obj.matrix_world[1][3],
|
||||
obj.matrix_world[2][3],
|
||||
translation[0],
|
||||
translation[1],
|
||||
translation[2],
|
||||
float(props.blender_offset_x) * unit_scale,
|
||||
float(props.blender_offset_y) * unit_scale,
|
||||
float(props.blender_offset_z) * unit_scale,
|
||||
|
||||
@@ -128,6 +128,8 @@ class ItemDecorator:
|
||||
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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,13 +35,13 @@ from bpy.props import (
|
||||
from typing import Optional, TYPE_CHECKING, Union, Literal
|
||||
|
||||
|
||||
def get_contexts(self, context):
|
||||
def get_contexts(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
if not RepresentationsData.is_loaded:
|
||||
RepresentationsData.load()
|
||||
return RepresentationsData.data["contexts"]
|
||||
|
||||
|
||||
def update_mode(self, context):
|
||||
def update_mode(self: "BIMGeometryProperties", context: bpy.types.Context) -> None:
|
||||
if self.is_changing_mode:
|
||||
return
|
||||
if context.mode.startswith("EDIT"):
|
||||
@@ -62,7 +62,7 @@ def update_mode(self, context):
|
||||
bpy.ops.bim.override_mode_set_edit("INVOKE_DEFAULT")
|
||||
|
||||
|
||||
def update_representation_obj(self, context):
|
||||
def update_representation_obj(self: "BIMGeometryProperties", context: bpy.types.Context) -> None:
|
||||
for item_obj in self.item_objs:
|
||||
if item_obj.obj:
|
||||
data = item_obj.obj.data
|
||||
@@ -76,13 +76,13 @@ def update_representation_obj(self, context):
|
||||
self.is_changing_mode = False
|
||||
|
||||
|
||||
def get_mode(self, context):
|
||||
def get_mode(self: "BIMGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str, str, int]]:
|
||||
if not ViewportData.is_loaded:
|
||||
ViewportData.load()
|
||||
return ViewportData.data["mode"]
|
||||
|
||||
|
||||
def get_styles(self, context):
|
||||
def get_styles(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
# postponed import to avoid circular import
|
||||
from bonsai.bim.module.material.data import MaterialsData
|
||||
|
||||
@@ -91,13 +91,13 @@ def get_styles(self, context):
|
||||
return MaterialsData.data["styles"]
|
||||
|
||||
|
||||
def get_shape_aspects(self, context):
|
||||
def get_shape_aspects(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
if not RepresentationsData.is_loaded:
|
||||
RepresentationsData.load()
|
||||
return RepresentationsData.data["shape_aspects"]
|
||||
|
||||
|
||||
def get_material_constituents(self, context, edit_text):
|
||||
def get_material_constituents(self: "ShapeAspect", context: bpy.types.Context, edit_text: str) -> list[str]:
|
||||
from bonsai.bim.module.material.data import ObjectMaterialData
|
||||
|
||||
if not ObjectMaterialData.is_loaded:
|
||||
@@ -105,7 +105,7 @@ def get_material_constituents(self, context, edit_text):
|
||||
return ObjectMaterialData.data["active_material_constituents"]
|
||||
|
||||
|
||||
def get_layers(self, context):
|
||||
def get_layers(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
from bonsai.bim.module.layer.data import LayersData
|
||||
|
||||
if not LayersData.is_loaded:
|
||||
@@ -113,7 +113,7 @@ def get_layers(self, context):
|
||||
return LayersData.data["layers_enum"]
|
||||
|
||||
|
||||
def get_layers_no_active(self, context):
|
||||
def get_layers_no_active(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
from bonsai.bim.module.layer.data import LayersData
|
||||
|
||||
if not LayersData.is_loaded:
|
||||
@@ -162,6 +162,11 @@ class RepresentationItemObject(PropertyGroup):
|
||||
obj: PointerProperty(type=bpy.types.Object)
|
||||
ifc_definition_id: IntProperty()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
obj: Union[bpy.types.Object, None]
|
||||
ifc_definition_id: int
|
||||
|
||||
|
||||
class ShapeAspect(PropertyGroup):
|
||||
name: StringProperty(
|
||||
@@ -283,7 +288,14 @@ class BIMGeometryProperties(PropertyGroup):
|
||||
is_changing_mode: BoolProperty(name="Is Changing Mode", default=False)
|
||||
mode: EnumProperty(items=get_mode, name="IFC Interaction Mode", update=update_mode)
|
||||
representation_obj: PointerProperty(
|
||||
name="Representation Object", type=bpy.types.Object, update=update_representation_obj
|
||||
name="Representation Object",
|
||||
description=(
|
||||
"Only used for Item Mode. When element is in Item Mode, new objects are imported "
|
||||
"for each element's representation item, original element's object is hidden and "
|
||||
"representation_obj pointing to it. None if no object in Item Mode."
|
||||
),
|
||||
type=bpy.types.Object,
|
||||
update=update_representation_obj,
|
||||
)
|
||||
item_objs: CollectionProperty(name="Item Objects", type=RepresentationItemObject)
|
||||
|
||||
@@ -297,6 +309,14 @@ class BIMGeometryProperties(PropertyGroup):
|
||||
blender_item.name = name
|
||||
return blender_item
|
||||
|
||||
def remove_item_object_by_entity(self, item: ifcopenshell.entity_instance) -> None:
|
||||
ifc_id = item.id()
|
||||
for i, item_obj in enumerate(self.item_objs):
|
||||
if item_obj.ifc_definition_id == ifc_id:
|
||||
self.item_objs.remove(i)
|
||||
return
|
||||
assert False
|
||||
|
||||
def is_object_valid_for_representation_copy(self, obj: bpy.types.Object) -> bool:
|
||||
return bool(obj != bpy.context.active_object and obj.data)
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ def object_menu(self, context):
|
||||
self.layout.operator("bim.override_object_delete", icon="PLUGIN")
|
||||
self.layout.operator("bim.override_paste_buffer", icon="PLUGIN")
|
||||
self.layout.menu("BIM_MT_object_set_origin", icon="PLUGIN")
|
||||
self.layout.menu("BIM_MT_separate", icon="PLUGIN")
|
||||
|
||||
|
||||
def edit_mesh_menu(self, context):
|
||||
@@ -76,25 +77,20 @@ class BIM_MT_separate(Menu):
|
||||
bl_label = "IFC Separate"
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC Selection").type = "SELECTED"
|
||||
self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC By Material").type = "MATERIAL"
|
||||
self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC By Loose Parts").type = "LOOSE"
|
||||
assert self.layout
|
||||
self.layout.label(text="IFC Separate", icon_value=bonsai.bim.icons["IFC"].icon_id)
|
||||
self.layout.operator_enum("bim.override_mesh_separate", "type")
|
||||
|
||||
|
||||
# TODO: remove as it's the same as BIM_MT_separate?
|
||||
class BIM_MT_hotkey_separate(Menu):
|
||||
bl_idname = "BIM_MT_hotkey_separate"
|
||||
bl_label = "Separate"
|
||||
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
self.layout.label(text="IFC Separate", icon_value=bonsai.bim.icons["IFC"].icon_id)
|
||||
self.layout.operator("bim.override_mesh_separate", text="Selection").type = "SELECTED"
|
||||
self.layout.operator("bim.override_mesh_separate", text="By Material").type = "MATERIAL"
|
||||
self.layout.operator("bim.override_mesh_separate", text="By Loose Parts").type = "LOOSE"
|
||||
self.layout.separator()
|
||||
self.layout.label(text="Blender Separate", icon="BLENDER")
|
||||
self.layout.operator("mesh.separate", text="Selection").type = "SELECTED"
|
||||
self.layout.operator("mesh.separate", text="By Material").type = "MATERIAL"
|
||||
self.layout.operator("mesh.separate", text="By Loose Parts").type = "LOOSE"
|
||||
self.layout.operator_enum("bim.override_mesh_separate", "type")
|
||||
|
||||
|
||||
class BIM_MT_object_set_origin(Menu):
|
||||
@@ -102,21 +98,9 @@ class BIM_MT_object_set_origin(Menu):
|
||||
bl_label = "IFC Set Origin"
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.operator("bim.override_origin_set", icon="PLUGIN", text="IFC Geometry to Origin").origin_type = (
|
||||
"GEOMETRY_ORIGIN"
|
||||
)
|
||||
self.layout.operator("bim.override_origin_set", icon="PLUGIN", text="IFC Origin to Geometry").origin_type = (
|
||||
"ORIGIN_GEOMETRY"
|
||||
)
|
||||
self.layout.operator("bim.override_origin_set", icon="PLUGIN", text="IFC Origin to 3D Cursor").origin_type = (
|
||||
"ORIGIN_CURSOR"
|
||||
)
|
||||
self.layout.operator(
|
||||
"bim.override_origin_set", icon="PLUGIN", text="IFC Origin to Center of Mass (Surface)"
|
||||
).origin_type = "ORIGIN_CENTER_OF_MASS"
|
||||
self.layout.operator(
|
||||
"bim.override_origin_set", icon="PLUGIN", text="IFC Origin to Center of Mass (Volume)"
|
||||
).origin_type = "ORIGIN_CENTER_OF_VOLUME"
|
||||
assert self.layout
|
||||
self.layout.label(text="IFC Set Origin", icon_value=bonsai.bim.icons["IFC"].icon_id)
|
||||
self.layout.operator_enum("bim.override_origin_set", property="origin_type")
|
||||
|
||||
|
||||
def outliner_menu(self, context):
|
||||
@@ -225,7 +209,7 @@ class BIM_PT_representation_items(Panel):
|
||||
RepresentationItemsData.load()
|
||||
|
||||
props = tool.Geometry.get_geometry_props()
|
||||
obj = props.representation_obj or tool.Blender.get_active_object()
|
||||
obj = tool.Geometry.get_active_or_representation_obj()
|
||||
assert obj
|
||||
props = tool.Geometry.get_object_geometry_props(obj)
|
||||
|
||||
|
||||
@@ -54,6 +54,8 @@ class GeoreferenceDecorator:
|
||||
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
|
||||
props = tool.Georeference.get_georeference_props()
|
||||
self.scale = props.visualization_scale
|
||||
content_pos = [v * self.scale for v in content_pos]
|
||||
|
||||
@@ -112,7 +112,9 @@ def update_blender_coordinates(self: "BIMGeoreferenceProperties", context: bpy.t
|
||||
props.is_updating_coordinates = True
|
||||
blender_coordinates = tool.Georeference.get_coordinates("blender")
|
||||
local_coordinates = ifcopenshell.util.geolocation.xyz2enh(
|
||||
*blender_coordinates,
|
||||
blender_coordinates[0],
|
||||
blender_coordinates[1],
|
||||
blender_coordinates[2],
|
||||
float(props.blender_offset_x),
|
||||
float(props.blender_offset_y),
|
||||
float(props.blender_offset_z),
|
||||
|
||||
@@ -38,6 +38,7 @@ classes = (
|
||||
operator.RefreshGit,
|
||||
operator.SwitchRevision,
|
||||
operator.InstallGit,
|
||||
operator.RunGitDiff,
|
||||
prop.IfcGitTag,
|
||||
prop.IfcGitListItem,
|
||||
prop.IfcGitProperties,
|
||||
|
||||
@@ -409,3 +409,25 @@ class InstallGit(bpy.types.Operator):
|
||||
core.install_git(tool.IfcGit, self)
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RunGitDiff(bpy.types.Operator):
|
||||
"""Run `git diff` for the current version of IFC file and the last saved one."""
|
||||
|
||||
bl_label = "Git Diff"
|
||||
bl_idname = "ifcgit.git_diff"
|
||||
bl_options = set()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get():
|
||||
cls.poll_message_set("No IFC file loaded.")
|
||||
return False
|
||||
if not tool.Ifc.get_path():
|
||||
cls.poll_message_set("Current IFC file was never saved.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
core.run_git_diff(tool.IfcGit, self)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -283,3 +283,5 @@ class IFCGIT_PT_revision_inspector(bpy.types.Panel):
|
||||
"ifcgit.object_log",
|
||||
icon="TEXT",
|
||||
)
|
||||
row = layout.row()
|
||||
row.operator("ifcgit.git_diff")
|
||||
|
||||
@@ -52,6 +52,8 @@ class SolarDecorator:
|
||||
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)
|
||||
|
||||
@@ -392,6 +392,7 @@ ground_glow source ground
|
||||
scene.add_surface(scene_path)
|
||||
scene.add_source(sky_file_path)
|
||||
print("Setting up view...")
|
||||
assert isinstance(camera.data, bpy.types.Camera)
|
||||
if camera.data.type == "PERSP":
|
||||
# Perspective camera
|
||||
camera_fov = camera.data.angle
|
||||
@@ -547,9 +548,11 @@ class ViewFromSun(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
if not (camera := bpy.data.objects.get("SunPathCamera")):
|
||||
camera = bpy.data.objects.new("SunPathCamera", bpy.data.cameras.new("SunPathCamera"))
|
||||
assert isinstance(camera.data, bpy.types.Camera)
|
||||
assert context.scene
|
||||
camera.data.type = "ORTHO"
|
||||
camera.data.ortho_scale = 100 # The default of 6m is too small
|
||||
bpy.context.scene.collection.objects.link(camera)
|
||||
context.scene.collection.objects.link(camera)
|
||||
tool.Blender.activate_camera(camera)
|
||||
props = context.scene.BIMSolarProperties
|
||||
props.hour = props.hour # Just to refresh camera position
|
||||
|
||||
@@ -394,7 +394,7 @@ class ObjectMaterialData:
|
||||
return [(m, m, ifcopenshell.util.doc.get_entity_doc(version, m).get("description", "")) for m in material_types]
|
||||
|
||||
@classmethod
|
||||
def active_material_constituents(cls):
|
||||
def active_material_constituents(cls) -> list[str]:
|
||||
material = cls.material
|
||||
if not cls.material or not material.is_a("IfcMaterialConstituentSet"):
|
||||
return []
|
||||
|
||||
@@ -625,6 +625,26 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
wall.DumbWallPlaner().regenerate_from_layer_set(layer_set)
|
||||
slab.DumbSlabPlaner().regenerate_from_layer_set(layer_set)
|
||||
|
||||
if material_set_usage.is_a("IfcMaterialProfileSetUsage"):
|
||||
attributes["CardinalPoint"] = int(attributes["CardinalPoint"])
|
||||
ifcopenshell.api.material.edit_profile_usage(
|
||||
self.file,
|
||||
usage=material_set_usage,
|
||||
attributes=attributes,
|
||||
)
|
||||
profile_sets_to_regenerate = set()
|
||||
|
||||
for obj in objects:
|
||||
obj_element = tool.Ifc.get_entity(obj)
|
||||
obj_material_usage = ifcopenshell.util.element.get_material(obj_element)
|
||||
|
||||
if obj_material_usage and obj_material_usage.is_a("IfcMaterialProfileSetUsage"):
|
||||
profile_sets_to_regenerate.add(obj_material_usage.ForProfileSet)
|
||||
|
||||
for profile_set in profile_sets_to_regenerate:
|
||||
for material_profile in profile_set.MaterialProfiles:
|
||||
model_profile.DumbProfileRegenerator().regenerate_from_profile_def(material_profile.Profile)
|
||||
|
||||
bpy.ops.bim.disable_editing_assigned_material(obj=active_obj.name)
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ from . import (
|
||||
railing,
|
||||
roof,
|
||||
mep,
|
||||
external,
|
||||
)
|
||||
from typing import NamedTuple
|
||||
|
||||
@@ -76,6 +77,7 @@ classes = (
|
||||
wall.ExtendWallsToWall,
|
||||
wall.FlipWall,
|
||||
wall.MergeWall,
|
||||
wall.OffsetWalls,
|
||||
wall.RecalculateWall,
|
||||
wall.SplitWall,
|
||||
wall.UnjoinWalls,
|
||||
@@ -138,6 +140,7 @@ classes = (
|
||||
prop.BIMRoofProperties,
|
||||
prop.BIMPolylineProperties,
|
||||
prop.BIMProductPreviewProperties,
|
||||
prop.BIMExternalParametricGeometryProperties,
|
||||
ui.BIM_PT_array,
|
||||
ui.BIM_PT_stair,
|
||||
ui.BIM_PT_sverchok,
|
||||
@@ -147,6 +150,7 @@ classes = (
|
||||
ui.BIM_PT_roof,
|
||||
ui.BIM_MT_type_manager_menu,
|
||||
ui.BIM_MT_type_menu,
|
||||
ui.BIM_PT_external_parametric_geometry,
|
||||
ui.LaunchTypeMenu,
|
||||
ui.LaunchTypeManager,
|
||||
grid.BIM_OT_add_object,
|
||||
@@ -198,6 +202,7 @@ classes = (
|
||||
mep.MEPAddObstruction,
|
||||
mep.MEPAddTransition,
|
||||
mep.MEPAddBend,
|
||||
external.ApplyExternalParametricGeometry,
|
||||
)
|
||||
|
||||
addon_keymaps = []
|
||||
@@ -255,6 +260,9 @@ def register():
|
||||
bpy.types.Object.BIMDoorProperties = bpy.props.PointerProperty(type=prop.BIMDoorProperties)
|
||||
bpy.types.Object.BIMRailingProperties = bpy.props.PointerProperty(type=prop.BIMRailingProperties)
|
||||
bpy.types.Object.BIMRoofProperties = bpy.props.PointerProperty(type=prop.BIMRoofProperties)
|
||||
bpy.types.Object.BIMExternalParametricGeometryProperties = bpy.props.PointerProperty(
|
||||
type=prop.BIMExternalParametricGeometryProperties
|
||||
)
|
||||
|
||||
bpy.types.VIEW3D_MT_add.prepend(ui.add_menu)
|
||||
bpy.app.handlers.load_post.append(handler.load_post)
|
||||
@@ -277,6 +285,7 @@ def unregister():
|
||||
del bpy.types.Object.BIMDoorProperties
|
||||
del bpy.types.Object.BIMRailingProperties
|
||||
del bpy.types.Object.BIMRoofProperties
|
||||
del bpy.types.Object.BIMExternalParametricGeometryProperties
|
||||
|
||||
bpy.app.handlers.load_post.remove(handler.load_post)
|
||||
bpy.types.VIEW3D_MT_add.remove(ui.add_menu)
|
||||
|
||||
@@ -20,6 +20,7 @@ import bpy
|
||||
import json
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.unit
|
||||
import bonsai.tool as tool
|
||||
@@ -33,8 +34,23 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert (obj := context.active_object)
|
||||
assert (element := tool.Ifc.get_entity(obj))
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
allowed_types = (
|
||||
"IfcElement",
|
||||
"IfcAnnotation",
|
||||
"IfcOpeningElement",
|
||||
"IfcSpatialElement",
|
||||
)
|
||||
|
||||
if not any(element.is_a(c) for c in allowed_types):
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"Adding array to element of type '{element.is_a()}' is not supported. Supported types: {','.join(allowed_types)}.",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
array = {
|
||||
"children": [],
|
||||
@@ -54,14 +70,13 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
data.append(array)
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
else:
|
||||
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Array")
|
||||
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="BBIM_Array")
|
||||
data = [array]
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
ifc_file,
|
||||
pset=pset,
|
||||
properties={"Parent": element.GlobalId, "Data": tool.Ifc.get().createIfcText(json.dumps(data))},
|
||||
properties={"Parent": element.GlobalId, "Data": ifc_file.create_entity("IfcText", json.dumps(data))},
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -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/>.
|
||||
|
||||
from __future__ import annotations
|
||||
import bpy
|
||||
import blf
|
||||
import bpy
|
||||
@@ -34,6 +35,7 @@ from gpu_extras.presets import draw_circle_2d
|
||||
from typing import Union
|
||||
from bonsai.bim.module.drawing.helper import format_distance
|
||||
from itertools import chain
|
||||
from typing import Union, Any
|
||||
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
@@ -72,6 +74,8 @@ class ProfileDecorator:
|
||||
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)
|
||||
@@ -346,21 +350,28 @@ class PolylineDecorator:
|
||||
cls.is_installed = False
|
||||
|
||||
@classmethod
|
||||
def update(cls, event, tool_state, input_ui, snapping_point):
|
||||
def update(
|
||||
cls,
|
||||
event: bpy.types.Event,
|
||||
tool_state: tool.Polyline.ToolState,
|
||||
input_ui: tool.Polyline.PolylineUI,
|
||||
snapping_point: Vector,
|
||||
) -> None:
|
||||
cls.event = event
|
||||
cls.tool_state = tool_state
|
||||
cls.input_ui = input_ui
|
||||
|
||||
# TODO: unused?
|
||||
@classmethod
|
||||
def set_input_ui(cls, input_ui):
|
||||
def set_input_ui(cls, input_ui: tool.Polyline.PolylineUI) -> None:
|
||||
cls.input_ui = input_ui
|
||||
|
||||
@classmethod
|
||||
def set_angle_axis_line(cls, start, end):
|
||||
def set_angle_axis_line(cls, start: Vector, end: Vector) -> None:
|
||||
cls.axis_start = start
|
||||
cls.axis_end = end
|
||||
|
||||
def calculate_measurement_x_y_and_z(self, context):
|
||||
def calculate_measurement_x_y_and_z(self, context: bpy.types.Context) -> None:
|
||||
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
|
||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||
|
||||
@@ -383,7 +394,7 @@ class PolylineDecorator:
|
||||
return (x_axis, y_axis, z_axis), (x_middle, y_middle, z_middle)
|
||||
|
||||
@classmethod
|
||||
def calculate_polygon(self, points):
|
||||
def calculate_polygon(cls, points: list[Vector]) -> dict[str, Any]:
|
||||
bm = bmesh.new()
|
||||
|
||||
new_verts = [bm.verts.new(v) for v in points]
|
||||
@@ -405,12 +416,14 @@ class PolylineDecorator:
|
||||
return {"verts": verts, "edges": edges, "tris": tris}
|
||||
|
||||
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_input_ui(self, context):
|
||||
def draw_input_ui(self, context: bpy.types.Context) -> None:
|
||||
texts = {
|
||||
"D": "Distance: ",
|
||||
"A": "Angle: ",
|
||||
@@ -420,6 +433,7 @@ class PolylineDecorator:
|
||||
"AREA": "Area:",
|
||||
}
|
||||
try:
|
||||
assert self.event
|
||||
mouse_pos = self.event.mouse_region_x, self.event.mouse_region_y
|
||||
except:
|
||||
mouse_pos = (None, None)
|
||||
@@ -534,7 +548,7 @@ class PolylineDecorator:
|
||||
polyline_verts = [Vector((p.x, p.y, p.z)) for p in polyline_points]
|
||||
|
||||
# Area
|
||||
if measure_type == "AREA" and polyline_data.area:
|
||||
if measure_type == "POLY_AREA" and polyline_data.area:
|
||||
if len(polyline_verts) < 3:
|
||||
return
|
||||
center = sum(polyline_verts, Vector()) / len(polyline_verts) # Center between all polyline points
|
||||
@@ -552,7 +566,7 @@ class PolylineDecorator:
|
||||
blf.draw(self.font_id, text)
|
||||
|
||||
# Length
|
||||
if measure_type in {"POLYLINE", "AREA"}:
|
||||
if measure_type in {"POLYLINE", "POLY_AREA"}:
|
||||
if len(polyline_verts) < 3:
|
||||
return
|
||||
total_length_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, polyline_verts[-1])
|
||||
@@ -722,8 +736,8 @@ class PolylineDecorator:
|
||||
polyline_points = polyline_data.polyline_points
|
||||
else:
|
||||
polyline_points = []
|
||||
polyline_verts = []
|
||||
polyline_edges = []
|
||||
polyline_verts: list[Vector] = []
|
||||
polyline_edges: list[list[int]] = []
|
||||
for point_prop in polyline_points:
|
||||
point = Vector((point_prop.x, point_prop.y, point_prop.z))
|
||||
polyline_verts.append(point)
|
||||
@@ -761,7 +775,7 @@ class PolylineDecorator:
|
||||
# Area highlight
|
||||
if polyline_data:
|
||||
area = polyline_data.area.split(" ")[0]
|
||||
if area:
|
||||
if polyline_data.measurement_type == "POLY_AREA" and area:
|
||||
if float(area) > 0:
|
||||
tris = self.calculate_polygon(polyline_verts)["tris"]
|
||||
self.draw_batch("TRIS", polyline_verts, transparent_color(decorator_color_special), tris)
|
||||
@@ -815,6 +829,8 @@ class ProductDecorator:
|
||||
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)
|
||||
@@ -882,6 +898,8 @@ class WallAxisDecorator:
|
||||
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)
|
||||
@@ -946,6 +964,8 @@ class SlabDirectionDecorator:
|
||||
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)
|
||||
@@ -1001,6 +1021,8 @@ class FaceAreaDecorator:
|
||||
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)
|
||||
|
||||
@@ -616,6 +616,9 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.is_editing = False
|
||||
|
||||
update_door_modifier_representation(obj)
|
||||
element_type = ifcopenshell.util.element.get_type(element)
|
||||
if element_type:
|
||||
tool.Model.mark_thumbnail_for_update(element_type)
|
||||
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
|
||||
door_data = tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021, 2022 Dion Moult <dion@thinkmoult.com>, @Andrej730
|
||||
#
|
||||
# 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 bmesh
|
||||
|
||||
import ifcopenshell
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
class ApplyExternalParametricGeometry(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.apply_external_parametric_geometry"
|
||||
bl_label = "Apply External Parametric Geometry"
|
||||
bl_description = "Apply external parametric geometry to the active object."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
assert (active_representation := tool.Geometry.get_active_representation(obj))
|
||||
ifc_context = active_representation.ContextOfItems
|
||||
tool.Model.add_representation(obj, ifc_context)
|
||||
props = tool.Model.get_epg_props(obj)
|
||||
props.is_editing = False
|
||||
return {"FINISHED"}
|
||||
@@ -18,14 +18,18 @@
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.grid
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.geometry
|
||||
import bonsai.core.root
|
||||
from bpy.types import Operator
|
||||
from bpy.props import FloatProperty, IntProperty
|
||||
from mathutils import Vector
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
def add_object(self, context):
|
||||
def add_object(self: "BIM_OT_add_object", context: bpy.types.Context) -> None:
|
||||
ifc_file = tool.Ifc.get()
|
||||
obj = bpy.data.objects.new("Grid", None)
|
||||
obj.name = "Grid"
|
||||
|
||||
@@ -33,6 +37,11 @@ def add_object(self, context):
|
||||
tool.Ifc, tool.Collector, tool.Root, obj=obj, ifc_class="IfcGrid", should_add_representation=False
|
||||
)
|
||||
grid = tool.Ifc.get_entity(obj)
|
||||
assert grid
|
||||
|
||||
# Requirement in IFC4+.
|
||||
if tool.Ifc.get_schema() != "IFC2X3":
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj)
|
||||
|
||||
for i in range(0, self.total_u):
|
||||
verts = [
|
||||
@@ -46,9 +55,7 @@ def add_object(self, context):
|
||||
tag = chr(ord("A") + i)
|
||||
obj = bpy.data.objects.new(f"IfcGridAxis/{tag}", mesh)
|
||||
|
||||
result = ifcopenshell.api.run(
|
||||
"grid.create_grid_axis", tool.Ifc.get(), axis_tag=tag, uvw_axes="UAxes", grid=grid
|
||||
)
|
||||
result = ifcopenshell.api.grid.create_grid_axis(ifc_file, axis_tag=tag, uvw_axes="UAxes", grid=grid)
|
||||
tool.Ifc.link(result, obj)
|
||||
tool.Model.create_axis_curve(obj, result)
|
||||
tool.Collector.assign(obj)
|
||||
@@ -65,9 +72,7 @@ def add_object(self, context):
|
||||
tag = str(i + 1).zfill(2)
|
||||
obj = bpy.data.objects.new(f"IfcGridAxis/{tag}", mesh)
|
||||
|
||||
result = ifcopenshell.api.run(
|
||||
"grid.create_grid_axis", tool.Ifc.get(), axis_tag=tag, uvw_axes="VAxes", grid=grid
|
||||
)
|
||||
result = ifcopenshell.api.grid.create_grid_axis(ifc_file, axis_tag=tag, uvw_axes="VAxes", grid=grid)
|
||||
tool.Ifc.link(result, obj)
|
||||
tool.Model.create_axis_curve(obj, result)
|
||||
tool.Collector.assign(obj)
|
||||
@@ -78,13 +83,20 @@ def add_object(self, context):
|
||||
class BIM_OT_add_object(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "mesh.add_grid"
|
||||
bl_label = "Grid"
|
||||
bl_description = "Add IfcGrid."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
u_spacing: FloatProperty(name="U Spacing", default=10)
|
||||
u_spacing: FloatProperty(name="U Spacing", default=10, subtype="DISTANCE")
|
||||
total_u: IntProperty(name="Number of U Grids", default=3)
|
||||
v_spacing: FloatProperty(name="V Spacing", default=10)
|
||||
v_spacing: FloatProperty(name="V Spacing", default=10, subtype="DISTANCE")
|
||||
total_v: IntProperty(name="Number of V Grids", default=3)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
u_spacing: float
|
||||
total_u: int
|
||||
v_spacing: float
|
||||
total_v: int
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Ifc.get() and context.mode == "OBJECT"
|
||||
|
||||
@@ -36,12 +36,6 @@ def load_post(*args):
|
||||
"sequence.edit_task_time", "Bonsai.Task.CalculateQuantities", task.calculate_quantities
|
||||
)
|
||||
|
||||
ifcopenshell.api.add_post_listener(
|
||||
"material.edit_profile_usage",
|
||||
"Bonsai.Product.RegenerateProfileUsage",
|
||||
product.regenerate_profile_usage,
|
||||
)
|
||||
|
||||
ifcopenshell.api.add_post_listener(
|
||||
"type.assign_type", "Bonsai.DumbWall.RegenerateFromType", wall.DumbWallPlaner().regenerate_from_type
|
||||
)
|
||||
|
||||
@@ -484,32 +484,21 @@ class AddBoolean(Operator, tool.Ifc.Operator):
|
||||
|
||||
class ShowOpenings(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.show_openings"
|
||||
bl_label = "Show"
|
||||
bl_description = "Show Openings"
|
||||
bl_label = "Show Openings"
|
||||
bl_description = "Show openings for selected objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
objs = list(context.selected_objects)
|
||||
# If several parts of an aggregation are selected, the aggregate openings may be queried more than once
|
||||
objects_element_map = set()
|
||||
while objs:
|
||||
obj = objs.pop(0)
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
if getattr(element, "Decomposes", None):
|
||||
# Select aggregate recursively
|
||||
if element.Decomposes and (aggregate := element.Decomposes[0].RelatingObject):
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
objs.append(aggregate_obj)
|
||||
objects_element_map.add((obj, element))
|
||||
|
||||
for obj, element in objects_element_map:
|
||||
if element is None:
|
||||
continue
|
||||
self.show_object_openings(obj, element)
|
||||
DecorationsHandler.install(bpy.context)
|
||||
bpy.ops.bim.update_openings_focus()
|
||||
return {"FINISHED"}
|
||||
|
||||
def show_object_openings(self, obj, element):
|
||||
def show_object_openings(self, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
openings_elements = [rel.RelatedOpeningElement for rel in tool.Geometry.get_openings(element)]
|
||||
if not openings_elements:
|
||||
return
|
||||
@@ -531,15 +520,18 @@ class UpdateOpeningsFocus(Operator):
|
||||
preferences = tool.Blender.get_addon_preferences()
|
||||
if preferences.opening_focus_opacity == 100:
|
||||
return {"FINISHED"}
|
||||
openings = set()
|
||||
building_objects = set()
|
||||
openings: set[bpy.types.Object] = set()
|
||||
building_objects: set[bpy.types.Object] = set()
|
||||
props = tool.Model.get_model_props()
|
||||
for opening in props.openings:
|
||||
if opening.obj:
|
||||
openings.add(opening.obj)
|
||||
opening_element = tool.Ifc.get_entity(opening.obj)
|
||||
assert opening_element
|
||||
building_element = opening_element.VoidsElements[0].RelatingBuildingElement
|
||||
building_objects.add(tool.Ifc.get_object(building_element))
|
||||
building_obj = tool.Ifc.get_object(building_element)
|
||||
assert isinstance(building_obj, bpy.types.Object)
|
||||
building_objects.add(building_obj)
|
||||
|
||||
for obj in context.scene.objects:
|
||||
obj.color = [
|
||||
@@ -896,6 +888,8 @@ class DecorationsHandler:
|
||||
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)
|
||||
|
||||
@@ -550,6 +550,8 @@ class PolylineOperator:
|
||||
# TODO Fill doc strings
|
||||
""" """
|
||||
|
||||
objs_2d_bbox: list[tuple[bpy.types.Object, list[float]]]
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return context.space_data.type == "VIEW_3D"
|
||||
@@ -597,6 +599,7 @@ class PolylineOperator:
|
||||
"Increment Angle": {"icons": True, "keys": ["EVENT_SHIFT", "MOUSE_MMB_SCROLL"]},
|
||||
"Modify Snap Point": {"icons": True, "keys": ["EVENT_M"]},
|
||||
"Close Polyline": {"icons": True, "keys": ["EVENT_C"]},
|
||||
"Offset": {"icons": True, "keys": ["EVENT_O"]},
|
||||
"Remove Point": {"icons": True, "keys": ["EVENT_BACKSPACE"]},
|
||||
}
|
||||
|
||||
@@ -680,7 +683,10 @@ class PolylineOperator:
|
||||
for action, settings in instructions.items():
|
||||
if settings["icons"]:
|
||||
for key in settings["keys"]:
|
||||
self.layout.label(text="", icon=key)
|
||||
if bpy.app.version < (4, 3, 0) and key == "MOUSE_MMB_SCROLL":
|
||||
self.layout.label(text="MMB")
|
||||
else:
|
||||
self.layout.label(text="", icon=key)
|
||||
self.layout.label(text=action)
|
||||
else:
|
||||
key = settings["keys"][0]
|
||||
@@ -899,7 +905,8 @@ class PolylineOperator:
|
||||
if self.mousemove_count == 2:
|
||||
self.objs_2d_bbox = []
|
||||
for obj in self.visible_objs:
|
||||
self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj))
|
||||
if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj):
|
||||
self.objs_2d_bbox.append(bbox_2d)
|
||||
|
||||
if self.mousemove_count > 3:
|
||||
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state)
|
||||
@@ -988,7 +995,8 @@ class PolylineOperator:
|
||||
self.tool_state.mode = "Mouse"
|
||||
self.visible_objs = tool.Raycast.get_visible_objects(context)
|
||||
for obj in self.visible_objs:
|
||||
self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj))
|
||||
if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj):
|
||||
self.objs_2d_bbox.append(bbox_2d)
|
||||
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state)
|
||||
self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps)
|
||||
tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state)
|
||||
|
||||
@@ -577,49 +577,12 @@ class AlignProduct(bpy.types.Operator):
|
||||
align_type: AlignType
|
||||
|
||||
def execute(self, context):
|
||||
selected_objs = context.selected_objects
|
||||
if len(selected_objs) < 2 or not context.active_object:
|
||||
self.report({"ERROR"}, "Please select atleast 2 objects.")
|
||||
return {"FINISHED"}
|
||||
if self.align_type == "CENTERLINE":
|
||||
point = context.active_object.matrix_world @ (
|
||||
Vector(context.active_object.bound_box[0]) + (context.active_object.dimensions / 2)
|
||||
)
|
||||
elif self.align_type == "POSITIVE":
|
||||
point = context.active_object.matrix_world @ Vector(context.active_object.bound_box[6])
|
||||
elif self.align_type == "NEGATIVE":
|
||||
point = context.active_object.matrix_world @ Vector(context.active_object.bound_box[0])
|
||||
else:
|
||||
assert_never(self.align_type)
|
||||
|
||||
active_x_axis = context.active_object.matrix_world.to_quaternion() @ Vector((1, 0, 0))
|
||||
active_y_axis = context.active_object.matrix_world.to_quaternion() @ Vector((0, 1, 0))
|
||||
active_z_axis = context.active_object.matrix_world.to_quaternion() @ Vector((0, 0, 1))
|
||||
|
||||
x_distances = self.get_axis_distances(point, active_x_axis, context)
|
||||
y_distances = self.get_axis_distances(point, active_y_axis, context)
|
||||
if abs(sum(x_distances)) < abs(sum(y_distances)):
|
||||
for i, obj in enumerate(selected_objs):
|
||||
obj.matrix_world = Matrix.Translation(active_x_axis * -x_distances[i]) @ obj.matrix_world
|
||||
else:
|
||||
for i, obj in enumerate(selected_objs):
|
||||
obj.matrix_world = Matrix.Translation(active_y_axis * -y_distances[i]) @ obj.matrix_world
|
||||
try:
|
||||
core.align_objects(tool.Blender, tool.Model, self.align_type)
|
||||
except core.RequireAtLeastTwoElements as e:
|
||||
self.report({"ERROR"}, str(e))
|
||||
return {"FINISHED"}
|
||||
|
||||
def get_axis_distances(self, point: Vector, axis: Vector, context: bpy.types.Context) -> list[float]:
|
||||
results = []
|
||||
for obj in context.selected_objects:
|
||||
if self.align_type == "CENTERLINE":
|
||||
obj_point = obj.matrix_world @ (Vector(obj.bound_box[0]) + (obj.dimensions / 2))
|
||||
elif self.align_type == "POSITIVE":
|
||||
obj_point = obj.matrix_world @ Vector(obj.bound_box[6])
|
||||
elif self.align_type == "NEGATIVE":
|
||||
obj_point = obj.matrix_world @ Vector(obj.bound_box[0])
|
||||
else:
|
||||
assert_never(self.align_type)
|
||||
results.append(mathutils.geometry.distance_point_to_plane(obj_point, point, axis))
|
||||
return results
|
||||
|
||||
|
||||
class LoadTypeThumbnails(bpy.types.Operator):
|
||||
bl_idname = "bim.load_type_thumbnails"
|
||||
@@ -645,8 +608,7 @@ class LoadTypeThumbnails(bpy.types.Operator):
|
||||
while queue:
|
||||
# if bpy.app.is_job_running("RENDER_PREVIEW") does not seem to reflect asset preview generation
|
||||
element = queue.pop()
|
||||
if tool.Model.update_thumbnail_for_element(element):
|
||||
queue.append(element)
|
||||
tool.Model.mark_thumbnail_for_update(element)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ import bonsai.core.root
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from math import pi, degrees, atan2
|
||||
from mathutils import Vector, Matrix
|
||||
from bonsai.bim.module.model.wall import DumbWallRecalculator
|
||||
from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator
|
||||
from bonsai.bim.module.model.polyline import PolylineOperator
|
||||
from typing import Union, Any, Optional
|
||||
@@ -213,7 +212,7 @@ class DumbProfileRegenerator:
|
||||
|
||||
# update related thumbnails
|
||||
for element in self.get_element_types_using_profile(profile):
|
||||
tool.Model.update_thumbnail_for_element(element, refresh=True)
|
||||
tool.Model.mark_thumbnail_for_update(element)
|
||||
|
||||
def regenerate_from_profile(self, usecase_path, ifc_file, settings):
|
||||
self.file = ifc_file
|
||||
@@ -969,7 +968,7 @@ class Rotate90(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj.matrix_world @= rotate_matrix
|
||||
bpy.context.view_layer.update()
|
||||
DumbProfileRecalculator().recalculate(profile_objs)
|
||||
DumbWallRecalculator().recalculate(layer2_objs)
|
||||
tool.Model.recalculate_walls(layer2_objs)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -221,13 +221,13 @@ class BIMModelProperties(PropertyGroup):
|
||||
items=[("EXTERIOR", "Exterior", ""), ("CENTER", "Center", ""), ("INTERIOR", "Interior", "")],
|
||||
name="Vertical Layer Offset Type",
|
||||
default="EXTERIOR",
|
||||
description="It's a convention that affects the offset to reference line",
|
||||
description="Offset convention to reference line",
|
||||
)
|
||||
offset_type_horizontal: bpy.props.EnumProperty(
|
||||
items=[("TOP", "Top", ""), ("CENTER", "Center", ""), ("BOTTOM", "Bottom", "")],
|
||||
name="Horizontal Layer Offset Type",
|
||||
default="TOP",
|
||||
description="It's a convention that affects the offset to reference line",
|
||||
description="Offset convention to reference line",
|
||||
)
|
||||
offset: bpy.props.FloatProperty(name="Offset", default=0.0, description="Material usage offset from reference line")
|
||||
show_wall_axis: bpy.props.BoolProperty(
|
||||
@@ -1142,3 +1142,43 @@ class BIMProductPreviewProperties(PropertyGroup):
|
||||
verts: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||
edges: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||
tris: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||
|
||||
|
||||
def update_is_editing(self: "BIMExternalParametricGeometryProperties", context: bpy.types.Context) -> None:
|
||||
if self.is_editing:
|
||||
return
|
||||
|
||||
tool.Model.clean_up_parametric_geometry(self.id_data)
|
||||
del self["is_editing"]
|
||||
del self["geo_nodes"]
|
||||
|
||||
|
||||
def update_geo_nodes(self: "BIMExternalParametricGeometryProperties", context: bpy.types.Context) -> None:
|
||||
if self.geo_nodes:
|
||||
tool.Model.setup_parametric_geometry(self.id_data)
|
||||
return
|
||||
|
||||
modifier = tool.Model.get_epg_modifier(self.id_data)
|
||||
assert modifier
|
||||
modifier.show_viewport = False
|
||||
del self["geo_nodes"]
|
||||
|
||||
|
||||
class BIMExternalParametricGeometryProperties(bpy.types.PropertyGroup):
|
||||
is_editing: bpy.props.BoolProperty(
|
||||
name="Is Editing Paramteric Geometry",
|
||||
description="Toggle editing parametric geometry.",
|
||||
default=False,
|
||||
update=update_is_editing,
|
||||
)
|
||||
geo_nodes: bpy.props.PointerProperty(
|
||||
name="Geometry Nodes",
|
||||
description="Geometry nodes tree to use as a source for representation.",
|
||||
type=bpy.types.GeometryNodeTree,
|
||||
update=update_geo_nodes,
|
||||
poll=lambda self, node_tree: not node_tree.name.startswith("BBIM_EPG"),
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_editing: bool
|
||||
geo_nodes: Union[bpy.types.GeometryNodeTree, None]
|
||||
|
||||
@@ -36,7 +36,6 @@ from math import cos, pi
|
||||
from mathutils import Vector, Matrix
|
||||
from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator
|
||||
from bonsai.bim.module.model.polyline import PolylineOperator
|
||||
from bonsai.bim.module.model.wall import DumbWallRecalculator
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@@ -677,8 +676,9 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
# Restore the position to before it was changed by the offset and x_angle
|
||||
rot_matrix = Matrix.Rotation(existing_x_angle, 4, "X")
|
||||
offset = Vector((0.0, 0.0, -layer_params["offset"]))
|
||||
rot_offset = offset @ rot_matrix
|
||||
perpendicular_offset = layer_params["offset"] * abs(1 / cos(existing_x_angle))
|
||||
offset_vector = Vector((0.0, 0.0, -perpendicular_offset))
|
||||
rot_offset = offset_vector @ rot_matrix
|
||||
tranlation_matrix = Matrix.Translation(rot_offset)
|
||||
position = position @ tranlation_matrix
|
||||
|
||||
@@ -725,8 +725,9 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
# Restore the position to after it was changed by the offset and x_angle
|
||||
rot_matrix = Matrix.Rotation(existing_x_angle, 4, "X")
|
||||
offset = Vector((0.0, 0.0, -layer_params["offset"]))
|
||||
rot_offset = offset @ rot_matrix
|
||||
perpendicular_offset = layer_params["offset"] * abs(1 / cos(existing_x_angle))
|
||||
offset_vector = Vector((0.0, 0.0, -perpendicular_offset))
|
||||
rot_offset = offset_vector @ rot_matrix
|
||||
tranlation_matrix = Matrix.Translation(rot_offset)
|
||||
position = position @ tranlation_matrix
|
||||
|
||||
@@ -1033,5 +1034,5 @@ class RecalculateSlab(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement.is_a("IfcWall"):
|
||||
walls.append(tool.Ifc.get_object(rel.RelatedElement))
|
||||
|
||||
DumbWallRecalculator().recalculate(walls)
|
||||
tool.Model.recalculate_walls(walls)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -701,6 +701,49 @@ class BIM_PT_roof(bpy.types.Panel):
|
||||
row.operator("bim.add_roof", icon="ADD", text="")
|
||||
|
||||
|
||||
class BIM_PT_external_parametric_geometry(bpy.types.Panel):
|
||||
bl_label = "External Parametric Geometry"
|
||||
bl_idname = "BIM_PT_external_parametric_geometry"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_parent_id = "BIM_PT_tab_parametric_geometry"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (obj := context.active_object) and obj.type == "MESH"
|
||||
|
||||
def draw(self, context):
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
layout = self.layout
|
||||
assert layout
|
||||
props = tool.Model.get_epg_props(obj)
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
|
||||
if not props.is_editing:
|
||||
row.prop(props, "is_editing", icon="GREASEPENCIL", text="")
|
||||
return
|
||||
|
||||
row.operator("bim.apply_external_parametric_geometry", icon="CHECKMARK", text="")
|
||||
row.prop(props, "is_editing", icon="CANCEL", text="")
|
||||
row = layout.row(align=True)
|
||||
row.prop_search(props, "geo_nodes", bpy.data, "node_groups")
|
||||
if props.geo_nodes:
|
||||
assert (modifier := tool.Model.get_epg_modifier(obj))
|
||||
inputs = tool.Model.get_parametric_geometry_inputs(modifier)
|
||||
# NOTE: users won't be able to see inputs descriptions.
|
||||
# If we add group node inputs as modifiers inputs, descriptions will be visible.
|
||||
# But then we need to ensure inputs are up to date
|
||||
# (e.g. probably just by adding a refresh button).
|
||||
for input in inputs:
|
||||
row = layout.row(align=True)
|
||||
row.prop(input, "default_value", text=input.name)
|
||||
|
||||
|
||||
def add_menu(self: bpy.types.Menu, context: bpy.types.Context) -> None:
|
||||
self.layout.operator_context = "INVOKE_REGION_WIN"
|
||||
self.layout.operator("bim.add_element", icon_value=bonsai.bim.icons["IFC"].icon_id, text="IFC Element")
|
||||
|
||||
@@ -24,10 +24,12 @@ import math
|
||||
import numpy as np
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.geometry
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.shape_builder
|
||||
import ifcopenshell.util.type
|
||||
import mathutils.geometry
|
||||
import bonsai.core.type
|
||||
@@ -52,7 +54,10 @@ class UnjoinWalls(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
if not tool.Model.has_selected_ifc_objects():
|
||||
cls.poll_message_set("No IFC objects selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
core.unjoin_walls(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model)
|
||||
@@ -66,7 +71,7 @@ class ExtendWallsToUnderside(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
slab = None
|
||||
walls = []
|
||||
walls: list[bpy.types.Object] = []
|
||||
if (obj := tool.Blender.get_active_object(is_selected=True)) and (element := tool.Ifc.get_entity(obj)):
|
||||
slab = obj
|
||||
for obj in tool.Blender.get_selected_objects(include_active=False):
|
||||
@@ -112,8 +117,8 @@ class ExtendWallsToWall(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ifcopenshell.api.geometry.connect_wall(
|
||||
tool.Ifc.get(), wall1=element, wall2=target_element, is_atpath=True
|
||||
)
|
||||
joiner.recreate_wall(element, obj)
|
||||
joiner.recreate_wall(target_element, target_obj)
|
||||
tool.Model.recreate_wall(element, obj)
|
||||
tool.Model.recreate_wall(target_element, target_obj)
|
||||
else:
|
||||
self.report({"ERROR"}, "Please select at least one LAYER2 element and one active LAYER2 element")
|
||||
|
||||
@@ -124,10 +129,10 @@ class AlignWall(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = """ Align the selected walls to the active wall:
|
||||
'Ext.': align to the EXTERIOR face
|
||||
'C/L': align to wall CENTERLINE
|
||||
'C/L': align to wall CENTER
|
||||
'Int.': align to the INTERIOR face"""
|
||||
|
||||
AlignType = Literal["CENTERLINE", "EXTERIOR", "INTERIOR"]
|
||||
AlignType = Literal["CENTER", "EXTERIOR", "INTERIOR"]
|
||||
align_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
|
||||
items=((i, i, "") for i in get_args(AlignType))
|
||||
)
|
||||
@@ -135,25 +140,11 @@ class AlignWall(bpy.types.Operator):
|
||||
if TYPE_CHECKING:
|
||||
align_type: AlignType
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
selected_valid_objects = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
|
||||
return context.active_object and len(selected_valid_objects) > 1
|
||||
|
||||
def execute(self, context):
|
||||
selected_objects = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
|
||||
for obj in selected_objects:
|
||||
if obj == context.active_object:
|
||||
continue
|
||||
aligner = DumbWallAligner(obj, context.active_object)
|
||||
if self.align_type == "CENTERLINE":
|
||||
aligner.align_centerline()
|
||||
elif self.align_type == "EXTERIOR":
|
||||
aligner.align_first_layer()
|
||||
elif self.align_type == "INTERIOR":
|
||||
aligner.align_last_layer()
|
||||
else:
|
||||
assert_never(self.align_type)
|
||||
try:
|
||||
core.align_walls(tool.Ifc, tool.Blender, tool.Model, DumbWallAligner(), self.align_type)
|
||||
except core.RequireAtLeastTwoLayeredElements as e:
|
||||
self.report({"ERROR"}, str(e))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -165,10 +156,13 @@ class FlipWall(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
if not tool.Model.has_selected_ifc_objects():
|
||||
cls.poll_message_set("No IFC objects selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
|
||||
selected_objs = tool.Model.get_selected_mesh_objects()
|
||||
joiner = DumbWallJoiner()
|
||||
for obj in selected_objs:
|
||||
joiner.flip(obj)
|
||||
@@ -185,10 +179,13 @@ class SplitWall(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
if not tool.Model.has_selected_ifc_objects():
|
||||
cls.poll_message_set("No IFC objects selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
|
||||
selected_objs = tool.Model.get_selected_mesh_objects()
|
||||
for obj in selected_objs:
|
||||
DumbWallJoiner().split(obj, context.scene.cursor.location)
|
||||
return {"FINISHED"}
|
||||
@@ -202,12 +199,23 @@ class MergeWall(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
if not context.active_object:
|
||||
cls.poll_message_set("No active object selected.")
|
||||
return False
|
||||
elif not tool.Model.has_selected_ifc_objects():
|
||||
cls.poll_message_set("No mesh IFC objects selected.")
|
||||
return False
|
||||
mesh_objects = [o for o in tool.Model.get_selected_ifc_objects() if o.type == "MESH"]
|
||||
if len(mesh_objects) != 2:
|
||||
cls.poll_message_set("Please select exactly two mesh IFC objects.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
|
||||
if len(selected_objs) == 2:
|
||||
DumbWallJoiner().merge([o for o in selected_objs if o != context.active_object][0], context.active_object)
|
||||
active_obj = context.active_object
|
||||
assert active_obj
|
||||
selected_objs = tool.Model.get_selected_mesh_objects()
|
||||
DumbWallJoiner().merge(next(o for o in selected_objs if o != active_obj), active_obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -218,82 +226,94 @@ class RecalculateWall(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
if not tool.Model.has_selected_mesh_ifc_objects():
|
||||
cls.poll_message_set("No mesh IFC objects selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
DumbWallRecalculator().recalculate(context.selected_objects)
|
||||
objects = tool.Model.get_selected_mesh_ifc_objects()
|
||||
tool.Model.recalculate_walls(objects)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.change_extrusion_depth"
|
||||
bl_label = "Update"
|
||||
bl_description = "Update Height"
|
||||
bl_description = "Update height for the selected objects."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
depth: bpy.props.FloatProperty()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
if not tool.Model.has_selected_mesh_ifc_objects():
|
||||
cls.poll_message_set("No mesh IFC objects selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
layer2_objs = []
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
for obj in context.selected_objects:
|
||||
layer2_objs: list[bpy.types.Object] = []
|
||||
ifc_file = tool.Ifc.get()
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
selected_objs = tool.Model.get_selected_mesh_ifc_objects()
|
||||
|
||||
for obj in selected_objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
assert element
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if not representation:
|
||||
return
|
||||
continue
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
if not extrusion:
|
||||
return
|
||||
continue
|
||||
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
|
||||
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
|
||||
extrusion.Depth = self.depth / si_conversion * (1 / cos(x_angle))
|
||||
if tool.Model.get_usage_type(element) == "LAYER2":
|
||||
for rel in element.ConnectedFrom:
|
||||
if rel.is_a() == "IfcRelConnectsElements":
|
||||
ifcopenshell.api.run(
|
||||
"geometry.disconnect_element",
|
||||
tool.Ifc.get(),
|
||||
ifcopenshell.api.geometry.disconnect_element(
|
||||
ifc_file,
|
||||
relating_element=rel.RelatingElement,
|
||||
related_element=element,
|
||||
)
|
||||
layer2_objs.append(obj)
|
||||
|
||||
if layer2_objs:
|
||||
DumbWallRecalculator().recalculate(layer2_objs)
|
||||
tool.Model.recalculate_walls(layer2_objs)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.change_extrusion_x_angle"
|
||||
bl_label = "Update"
|
||||
bl_description = "Update Angle"
|
||||
bl_description = "Update angle for the selected objects."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
if not tool.Model.has_selected_mesh_ifc_objects():
|
||||
cls.poll_message_set("No mesh IFC objects selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
layer2_objs = []
|
||||
other_objs = []
|
||||
layer2_objs: list[bpy.types.Object] = []
|
||||
x_angle = 0 if tool.Cad.is_x(self.x_angle, 0, tolerance=0.001) else self.x_angle
|
||||
x_angle = 0 if tool.Cad.is_x(self.x_angle, pi, tolerance=0.001) else self.x_angle
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
for obj in context.selected_objects:
|
||||
selected_objs = tool.Model.get_selected_mesh_ifc_objects()
|
||||
|
||||
for obj in selected_objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
assert element
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if not representation:
|
||||
return
|
||||
continue
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
if not extrusion:
|
||||
return
|
||||
continue
|
||||
existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle
|
||||
@@ -373,26 +393,47 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj.rotation_euler.z = current_z_rot
|
||||
|
||||
if layer2_objs:
|
||||
DumbWallRecalculator().recalculate(layer2_objs)
|
||||
tool.Model.recalculate_walls(layer2_objs)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ChangeLayerLength(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.change_layer_length"
|
||||
bl_label = "Update"
|
||||
bl_description = "Update Length"
|
||||
bl_description = "Update length for the selected objects."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
length: bpy.props.FloatProperty()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
if not tool.Model.has_selected_mesh_ifc_objects():
|
||||
cls.poll_message_set("No mesh IFC objects selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
joiner = DumbWallJoiner()
|
||||
for obj in context.selected_objects:
|
||||
selected_objs = tool.Model.get_selected_mesh_ifc_objects()
|
||||
for obj in selected_objs:
|
||||
joiner.set_length(obj, self.length)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OffsetWalls(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.offset_walls"
|
||||
bl_label = "Offset Walls"
|
||||
bl_description = "Offset selected objects from their reference line."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Model.has_selected_mesh_ifc_objects():
|
||||
cls.poll_message_set("No mesh IFC objects selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
props = tool.Model.get_model_props()
|
||||
core.offset_walls(tool.Ifc, tool.Blender, tool.Model, props.offset_type_vertical)
|
||||
|
||||
|
||||
class AddWallsFromSlab(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -463,12 +504,8 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
|
||||
material_set_usage = model.by_id(material.id())
|
||||
# if material.is_a("IfcMaterialLayerSetUsage"):
|
||||
attributes = {"OffsetFromReferenceLine": offset, "DirectionSense": direction_sense}
|
||||
ifcopenshell.api.run(
|
||||
"material.edit_layer_usage",
|
||||
model,
|
||||
**{"usage": material_set_usage, "attributes": attributes},
|
||||
)
|
||||
DumbWallRecalculator().recalculate([wall["obj"]])
|
||||
ifcopenshell.api.run("material.edit_layer_usage", model, usage=material_set_usage, attributes=attributes)
|
||||
tool.Model.recalculate_walls([wall["obj"]])
|
||||
|
||||
if walls:
|
||||
if is_polyline_closed:
|
||||
@@ -569,11 +606,11 @@ class DumbWallAligner:
|
||||
# An alignment shifts the origin of all walls to the closest point on the
|
||||
# local X axis of the reference wall. In addition, the Z rotation is copied.
|
||||
# Z translations are ignored for alignment.
|
||||
def __init__(self, wall: bpy.types.Object, reference_wall: bpy.types.Object):
|
||||
self.wall = wall
|
||||
def set_reference_wall(self, reference_wall: bpy.types.Object):
|
||||
self.reference_wall = reference_wall
|
||||
|
||||
def align_centerline(self) -> None:
|
||||
def align_centerline(self, wall: bpy.types.Object) -> None:
|
||||
self.wall = wall
|
||||
self.align_rotation()
|
||||
|
||||
l_start = Vector(self.reference_wall.bound_box[0]).lerp(Vector(self.reference_wall.bound_box[3]), 0.5)
|
||||
@@ -591,7 +628,8 @@ class DumbWallAligner:
|
||||
new_origin = point - offset
|
||||
self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy
|
||||
|
||||
def align_last_layer(self) -> None:
|
||||
def align_last_layer(self, wall: bpy.types.Object) -> None:
|
||||
self.wall = wall
|
||||
self.align_rotation()
|
||||
|
||||
if self.is_rotation_flipped():
|
||||
@@ -614,7 +652,8 @@ class DumbWallAligner:
|
||||
new_origin = point - offset
|
||||
self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy
|
||||
|
||||
def align_first_layer(self) -> None:
|
||||
def align_first_layer(self, wall: bpy.types.Object) -> None:
|
||||
self.wall = wall
|
||||
self.align_rotation()
|
||||
|
||||
if self.is_rotation_flipped():
|
||||
@@ -656,30 +695,6 @@ class DumbWallAligner:
|
||||
return round(degrees(angle) % 360) == 180
|
||||
|
||||
|
||||
class DumbWallRecalculator:
|
||||
def recalculate(self, walls: list[bpy.types.Object]) -> None:
|
||||
queue: set[tuple[ifcopenshell.entity_instance, bpy.types.Object]] = set()
|
||||
for wall in walls:
|
||||
element = tool.Ifc.get_entity(wall)
|
||||
if tool.Ifc.is_moved(wall):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall)
|
||||
queue.add((element, wall))
|
||||
for rel in getattr(element, "ConnectedTo", []):
|
||||
obj = tool.Ifc.get_object(rel.RelatedElement)
|
||||
if tool.Ifc.is_moved(obj):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
queue.add((rel.RelatedElement, obj))
|
||||
for rel in getattr(element, "ConnectedFrom", []):
|
||||
obj = tool.Ifc.get_object(rel.RelatingElement)
|
||||
if tool.Ifc.is_moved(obj):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
queue.add((rel.RelatingElement, obj))
|
||||
joiner = DumbWallJoiner()
|
||||
for element, wall in queue:
|
||||
if tool.Model.get_usage_type(element) == "LAYER2" and wall:
|
||||
joiner.recreate_wall(element, wall)
|
||||
|
||||
|
||||
class DumbWallGenerator:
|
||||
def __init__(self, relating_type):
|
||||
self.relating_type = relating_type
|
||||
@@ -911,7 +926,7 @@ class DumbWallPlaner:
|
||||
else:
|
||||
for rel in inverse.AssociatedTo:
|
||||
walls.extend([tool.Ifc.get_object(e) for e in rel.RelatedObjects])
|
||||
DumbWallRecalculator().recalculate([w for w in set(walls) if w])
|
||||
tool.Model.recalculate_walls([w for w in set(walls) if w])
|
||||
|
||||
def regenerate_from_type(self, usecase_path, ifc_file, settings):
|
||||
relating_type = settings["relating_type"]
|
||||
@@ -942,7 +957,7 @@ class DumbWallPlaner:
|
||||
if layer_set_direction:
|
||||
material.LayerSetDirection = layer_set_direction
|
||||
if material.LayerSetDirection == "AXIS2":
|
||||
DumbWallRecalculator().recalculate([obj])
|
||||
tool.Model.recalculate_walls([obj])
|
||||
|
||||
|
||||
class DumbWallJoiner:
|
||||
@@ -962,7 +977,7 @@ class DumbWallJoiner:
|
||||
axis1 = tool.Model.get_wall_axis(wall1)
|
||||
axis = copy.deepcopy(axis1["reference"])
|
||||
body = copy.deepcopy(axis1["reference"])
|
||||
self.recreate_wall(element1, wall1, axis, body)
|
||||
tool.Model.recreate_wall(element1, wall1)
|
||||
|
||||
def split(self, wall1: bpy.types.Object, target: Vector) -> None:
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
@@ -975,7 +990,6 @@ class DumbWallJoiner:
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1)
|
||||
|
||||
axis1 = tool.Model.get_wall_axis(wall1)
|
||||
axis2 = copy.deepcopy(axis1)
|
||||
intersect, cut_percentage = mathutils.geometry.intersect_point_line(target.to_2d(), *axis1["reference"])
|
||||
if cut_percentage < 0 or cut_percentage > 1 or tool.Cad.is_x(cut_percentage, (0, 1)):
|
||||
return
|
||||
@@ -1036,20 +1050,36 @@ class DumbWallJoiner:
|
||||
# During the duplication process, filled voids are not copied. So we
|
||||
# only need to check fillings on the original element1.
|
||||
for opening in [r.RelatedOpeningElement for r in element1.HasOpenings if r.RelatedOpeningElement.HasFillings]:
|
||||
filling_obj = tool.Ifc.get_object(opening.HasFillings[0].RelatedBuildingElement)
|
||||
rel = opening.HasFillings[0]
|
||||
filling = rel.RelatedBuildingElement
|
||||
filling_obj = tool.Ifc.get_object(filling)
|
||||
filling_location = filling_obj.matrix_world.translation
|
||||
_, filling_position = mathutils.geometry.intersect_point_line(filling_location.to_2d(), *axis1["reference"])
|
||||
if filling_position > cut_percentage:
|
||||
# The filling should be moved from element1 to element2.
|
||||
FilledOpeningGenerator().generate(filling_obj, wall2, target=filling_obj.matrix_world.translation)
|
||||
new_opening = ifcopenshell.api.root.copy_class(tool.Ifc.get(), product=opening)
|
||||
new_opening.VoidsElements[0].RelatingBuildingElement = element2
|
||||
if new_opening.ObjectPlacement and new_opening.ObjectPlacement.is_a("IfcLocalPlacement"):
|
||||
if element2.ObjectPlacement:
|
||||
new_opening.ObjectPlacement.PlacementRelTo = element2.ObjectPlacement
|
||||
# For now, we do copy opening representations
|
||||
if opening.Representation:
|
||||
new_opening.Representation = ifcopenshell.util.element.copy_deep(
|
||||
tool.Ifc.get(), opening.Representation, exclude=["IfcGeometricRepresentationContext"]
|
||||
)
|
||||
|
||||
rel.RelatedBuildingElement = element2
|
||||
|
||||
# Remove the old opening
|
||||
ifcopenshell.api.run("feature.remove_feature", tool.Ifc.get(), feature=opening)
|
||||
|
||||
p1, p2 = ifcopenshell.util.representation.get_reference_line(element1)
|
||||
p3 = (wall1.matrix_world.inverted() @ intersect.to_3d()).to_2d() / unit_scale
|
||||
self.set_axis(element1, p1, p3)
|
||||
self.set_axis(element2, p3, p2)
|
||||
|
||||
self.recreate_wall(element1, wall1)
|
||||
self.recreate_wall(element2, wall2)
|
||||
tool.Model.recreate_wall(element1, wall1)
|
||||
tool.Model.recreate_wall(element2, wall2)
|
||||
|
||||
def flip(self, wall1: bpy.types.Object) -> None:
|
||||
if tool.Ifc.is_moved(wall1):
|
||||
@@ -1076,9 +1106,9 @@ class DumbWallJoiner:
|
||||
ifcopenshell.api.geometry.edit_object_placement(
|
||||
tool.Ifc.get(), product=element1, matrix=matrix, is_si=False, should_transform_children=False
|
||||
)
|
||||
self.recreate_wall(element1, wall1)
|
||||
tool.Model.recreate_wall(element1, wall1)
|
||||
|
||||
def merge(self, wall1, wall2):
|
||||
def merge(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None:
|
||||
if tool.Ifc.is_moved(wall1):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1)
|
||||
if tool.Ifc.is_moved(wall2):
|
||||
@@ -1086,6 +1116,7 @@ class DumbWallJoiner:
|
||||
|
||||
element1 = tool.Ifc.get_entity(wall1)
|
||||
element2 = tool.Ifc.get_entity(wall2)
|
||||
assert element1 and element2
|
||||
|
||||
p1, p2 = ifcopenshell.util.representation.get_reference_line(element1)
|
||||
p3, p4 = ifcopenshell.util.representation.get_reference_line(element2)
|
||||
@@ -1128,7 +1159,7 @@ class DumbWallJoiner:
|
||||
related_connection=rel.RelatedConnectionType,
|
||||
)
|
||||
|
||||
self.recreate_wall(element1, wall1)
|
||||
tool.Model.recreate_wall(element1, wall1)
|
||||
|
||||
tool.Geometry.delete_ifc_object(wall2)
|
||||
|
||||
@@ -1161,7 +1192,7 @@ class DumbWallJoiner:
|
||||
description="TOP",
|
||||
)
|
||||
|
||||
self.recreate_wall(element1, wall1)
|
||||
tool.Model.recreate_wall(element1, wall1)
|
||||
|
||||
def set_axis(self, wall, p1, p2):
|
||||
axis = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW")
|
||||
@@ -1190,24 +1221,23 @@ class DumbWallJoiner:
|
||||
self.set_axis(element1, p1, intersect)
|
||||
else:
|
||||
self.set_axis(element1, intersect, p2)
|
||||
self.recreate_wall(element1, wall1)
|
||||
tool.Model.recreate_wall(element1, wall1)
|
||||
|
||||
def set_length(self, wall1, si_length):
|
||||
def set_length(self, wall1: bpy.types.Object, si_length: float) -> None:
|
||||
element1 = tool.Ifc.get_entity(wall1)
|
||||
if not element1:
|
||||
return
|
||||
assert element1
|
||||
if tool.Ifc.is_moved(wall1):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1)
|
||||
|
||||
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type="ATEND")
|
||||
ifcopenshell.api.geometry.disconnect_path(tool.Ifc.get(), element=element1, connection_type="ATEND")
|
||||
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
p1, p2 = ifcopenshell.util.representation.get_reference_line(element1)
|
||||
p2[0] = p1[0] + si_length / unit_scale
|
||||
self.set_axis(element1, p1, p2)
|
||||
self.recreate_wall(element1, wall1)
|
||||
tool.Model.recreate_wall(element1, wall1)
|
||||
|
||||
def join_T(self, wall1, wall2):
|
||||
def join_T(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None:
|
||||
element1 = tool.Ifc.get_entity(wall1)
|
||||
element2 = tool.Ifc.get_entity(wall2)
|
||||
axis1 = tool.Model.get_wall_axis(wall1)
|
||||
@@ -1219,8 +1249,7 @@ class DumbWallJoiner:
|
||||
return
|
||||
connection = "ATEND" if tool.Cad.edge_percent(intersect, axis1["reference"]) > 0.5 else "ATSTART"
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"geometry.connect_path",
|
||||
ifcopenshell.api.geometry.connect_path(
|
||||
tool.Ifc.get(),
|
||||
related_element=element1,
|
||||
relating_element=element2,
|
||||
@@ -1229,9 +1258,9 @@ class DumbWallJoiner:
|
||||
description="BUTT",
|
||||
)
|
||||
|
||||
self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
|
||||
tool.Model.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
|
||||
|
||||
def connect(self, obj1, obj2):
|
||||
def connect(self, obj1: bpy.types.Object, obj2: bpy.types.Object) -> None:
|
||||
wall1 = tool.Ifc.get_entity(obj1)
|
||||
wall2 = tool.Ifc.get_entity(obj2)
|
||||
if tool.Ifc.is_moved(obj1):
|
||||
@@ -1239,27 +1268,8 @@ class DumbWallJoiner:
|
||||
if tool.Ifc.is_moved(obj2):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj2)
|
||||
ifcopenshell.api.geometry.connect_wall(tool.Ifc.get(), wall1=wall1, wall2=wall2)
|
||||
self.recreate_wall(wall1, obj1)
|
||||
self.recreate_wall(wall2, obj2)
|
||||
|
||||
def recreate_wall(self, element: ifcopenshell.entity_instance, obj: bpy.types.Object, axis=None, body=None) -> None:
|
||||
rep = ifcopenshell.api.geometry.regenerate_wall_representation(tool.Ifc.get(), element)
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=rep,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
tool.Geometry.record_object_materials(obj)
|
||||
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
matrix = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
||||
matrix[:, 3] *= unit_scale
|
||||
obj.matrix_world = tool.Loader.apply_blender_offset_to_matrix_world(obj, matrix)
|
||||
tool.Geometry.record_object_position(obj)
|
||||
tool.Model.recreate_wall(wall1, obj1)
|
||||
tool.Model.recreate_wall(wall2, obj2)
|
||||
|
||||
def create_matrix(self, p, x, y, z):
|
||||
return Matrix([x, y, z, p]).to_4x4().transposed()
|
||||
|
||||
@@ -529,6 +529,9 @@ class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.is_editing = False
|
||||
|
||||
update_window_modifier_representation(context)
|
||||
element_type = ifcopenshell.util.element.get_type(element)
|
||||
if element_type:
|
||||
tool.Model.mark_thumbnail_for_update(element_type)
|
||||
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Window")
|
||||
window_data = tool.Ifc.get().createIfcText(json.dumps(window_data, default=list))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user