mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 07:51:47 +00:00
Compare commits
476 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 0039f376ca | |||
| c307871d37 | |||
| 31c9f60bca | |||
| 93e7d92076 | |||
| 16f09655b8 | |||
| b6115a6d71 | |||
| f0b5e93737 | |||
| 9895302c9d | |||
| 8f5f346fa4 | |||
| 6adf5d957a | |||
| e6be79f2ad | |||
| e3228513c0 | |||
| 547506d626 | |||
| 3fe29fa842 | |||
| e18e2caea8 | |||
| 1cac12dc53 | |||
| 8d530eba68 | |||
| 4593e5aea6 | |||
| cba6161043 | |||
| 42c49af4e3 | |||
| 7bdbfee041 | |||
| f37bda2d7c | |||
| 0bf065c4bd | |||
| c58b84ddb5 | |||
| 81b5fc30c7 | |||
| fd423f21e2 | |||
| 4afed8f2f3 | |||
| 9254601924 | |||
| e9460a2c9a | |||
| f08f9ad310 | |||
| 69ddf52ff0 | |||
| 2fd02e281d | |||
| fdb283b7af | |||
| 013d7f6659 | |||
| 5b33c6018a | |||
| 50324a566f | |||
| 64b8842055 | |||
| 05be0f69bd | |||
| 3f823573cf | |||
| a3c5c0ce43 | |||
| 2e61532919 | |||
| c3107e37c0 | |||
| c85f9f02ba | |||
| f888a32fa4 | |||
| d35a2dcf5b | |||
| 244320dd49 | |||
| 0d7439c7a5 | |||
| bd92821b2e | |||
| 3744b81ccd | |||
| 9311f50206 | |||
| eec8c88e6f | |||
| dd53f9b494 | |||
| c6e3ac4155 | |||
| 4594f596c2 | |||
| 4f091af935 | |||
| 2f34f997dd | |||
| 1600e00295 | |||
| 7a55e837f7 | |||
| 595ee3fa47 | |||
| c84f8e55a6 | |||
| b63d9090a1 | |||
| 58c7976a5e | |||
| 384a74734e | |||
| b269a806eb | |||
| f43e666583 | |||
| 26b21ddcb3 | |||
| f1138d1aac | |||
| 51eef295c8 | |||
| 212da8b6d6 | |||
| f7163b8db8 | |||
| 9ee947b730 | |||
| 328fb8bc0e | |||
| 75fe548bb0 | |||
| 5830573cda | |||
| 775106d0a0 | |||
| 25b7003d2d | |||
| c80c1e9e15 | |||
| b5c062415d | |||
| a98a7ac856 | |||
| ee2b836b84 | |||
| e075c32a5a | |||
| a567af370b | |||
| 544b61b95a | |||
| 45c45d6862 | |||
| 6880e31a66 | |||
| e327932f4e | |||
| f2283e5895 | |||
| ea84f829af | |||
| 3231752fef | |||
| 963ba5fc30 | |||
| 8e1e0aec79 | |||
| 9c4ec34de3 | |||
| 4c2844292e | |||
| 35fe79dfb2 | |||
| 58a5f881e6 | |||
| 4d5a3ec633 | |||
| 1feb00678c | |||
| f10ebe0166 | |||
| 5f8fc1ba8c | |||
| f8f36cf2f0 | |||
| 2a2bfea870 | |||
| 8bdf8369b5 | |||
| 2bc230919e | |||
| 53fd1284b5 | |||
| 3c777a0b85 | |||
| fa3c57b016 | |||
| e658b00768 | |||
| 26b55216ca | |||
| d912012f76 | |||
| d868d8ceac | |||
| 8def5ec30b | |||
| ad03a0c8d9 | |||
| 09a43f1339 | |||
| c176b9d607 | |||
| 6cf5cc002e | |||
| 0abe6329d6 | |||
| 5109168533 | |||
| 0b4deee73f | |||
| 1f43f7f807 | |||
| 73a83c6d4e | |||
| 9c5b1a04ff | |||
| e67753eb4f | |||
| 778a839e5f | |||
| 102de32737 | |||
| cf3a554b15 | |||
| a8ff6ecf14 | |||
| 55be5cc289 | |||
| 9af00d4e72 | |||
| d8592a2a95 | |||
| 16ba10f957 | |||
| 0d1298aac1 | |||
| 210257de8e | |||
| 37ec2bf0e4 | |||
| 6e7c8ad640 | |||
| bfc4490c97 | |||
| d50e8060ac | |||
| a21939dc0d | |||
| 8845440edd | |||
| ba37754068 | |||
| ed48afc633 | |||
| 7262a50184 | |||
| 67b0ab6685 | |||
| 2941a4ad1a | |||
| b2e2f765ca | |||
| 2270185f8c | |||
| faef2df2a6 | |||
| bef7ff33f7 | |||
| 615ca24825 | |||
| 7e42b10159 | |||
| 04e07db0a3 | |||
| eed47c8c87 | |||
| 9ba3b6a61f | |||
| 6cfd15e819 | |||
| e55523ce3e | |||
| f36830d6dc | |||
| 2f87029434 | |||
| 9f78d9881e | |||
| 1d447e6e23 | |||
| 4c53c2ab1f | |||
| 86ebc968e5 | |||
| 1de67ffd9c | |||
| 1408beeac5 | |||
| 791a2e40f1 | |||
| 3f6a9a55cd | |||
| ec702e2ec1 | |||
| 4f6dbbe72b | |||
| 8ca344259f | |||
| e038f968f2 | |||
| 0e8810c1f5 | |||
| 10ecac33b8 | |||
| 069b630b16 | |||
| 8f6cd18288 | |||
| 7b7108b938 | |||
| ebd719783c | |||
| 46a31386da | |||
| c80c7c6d36 | |||
| ed7cf248ec | |||
| b40f630783 | |||
| f99015e123 | |||
| be76d4cb44 | |||
| 6949f9ceae | |||
| 4daf4693f3 | |||
| edd60a73ca | |||
| 6c04669543 | |||
| 4eae112087 | |||
| 5be076cb96 | |||
| ef67d866b6 | |||
| 609f468ce1 | |||
| 3852e3c63f | |||
| 5cb9e1c68d | |||
| 52fbe09dd4 | |||
| 4cff2f1fc1 | |||
| 6517cafd98 | |||
| 5114576457 | |||
| 9bafe07c36 | |||
| 6b85e9ec85 | |||
| d89957faab | |||
| ba8753d55e | |||
| 5df9c45795 | |||
| 5cc0aa71c7 | |||
| b06ddbcad4 | |||
| 18431dec31 | |||
| 7e9985758f | |||
| ece91a94db | |||
| dccfb634d9 | |||
| ffc6c6a7ad | |||
| dbf4a2a5ed | |||
| f6640d3904 | |||
| 63aada25dd | |||
| 6596f5bc4b | |||
| acdc40fb41 | |||
| 9be0a60c0a | |||
| e9a3d06352 | |||
| dfd30fbcaa | |||
| a941eb3f90 | |||
| 226736eb41 | |||
| f5a1c4aae3 | |||
| a7a4198fc1 | |||
| 6e0e19d690 | |||
| 72bbf244f2 | |||
| 41c1c6e8fa | |||
| a32735ac6a | |||
| 031540e982 | |||
| db1d8d938f | |||
| aa478ab359 | |||
| 19c103c627 | |||
| 7af672712f | |||
| 6bd8636c92 | |||
| a9f7d4ab3b | |||
| 977d814d39 | |||
| 5b95bb37e6 | |||
| 85b9906e20 | |||
| 64f51d733e | |||
| ed0b1c5009 | |||
| c1bc36ab86 | |||
| 9cfcaeb587 | |||
| d9e48467cd | |||
| f08dba6483 | |||
| 1e97d2c5fa | |||
| bf54132805 | |||
| 570513b60b | |||
| e1ba98c5f8 | |||
| 8ed9c32404 | |||
| 5b9ef23bdd | |||
| f3f7939c8b | |||
| 469b4f3502 | |||
| c4b57eb903 | |||
| 3886c79a2c | |||
| 566fe32ab2 | |||
| 7fb6adc025 | |||
| c03cf4ce58 | |||
| 41188e26e1 | |||
| 823f27cd6a | |||
| 4260313a99 | |||
| 1259607a9f | |||
| 9f1ffe6381 | |||
| ba1b6b5b10 | |||
| 2318132ea2 | |||
| b6b553e726 | |||
| 076f39e2d6 | |||
| abfe8ec315 | |||
| 9d7fe179cf | |||
| 8ffb5c56e4 | |||
| 4109b19c3a | |||
| 7503530741 | |||
| e78127d54f | |||
| 5fa4f2e79b | |||
| e5027e0be8 | |||
| af2dd1d7c6 | |||
| 5d9528d791 | |||
| 47d11daa15 | |||
| 01ec750814 | |||
| 8c9a657a8c | |||
| 48acfad8bb | |||
| 01b63d87d0 | |||
| 36744d391e | |||
| f0e5430daa | |||
| 197281eb5a | |||
| 7b370e6142 | |||
| 71a7d0c086 | |||
| b86e735530 | |||
| 46104f5bf5 | |||
| d68068a84b | |||
| fc7dd3b234 | |||
| 85fc97eb84 | |||
| 9076deddbc | |||
| eb0d3c38b2 | |||
| 590288496e | |||
| 269e137ea2 | |||
| ed722c6f13 | |||
| 2b9e3d2a2e | |||
| 7dee888ecf | |||
| a67927ae67 | |||
| 1271e4b7e3 | |||
| a44d0e713a | |||
| a358cff1b7 | |||
| 188ca3e402 | |||
| b5260d3005 | |||
| 79940641b2 | |||
| 7458040949 | |||
| 14eb543662 | |||
| e786687882 | |||
| 37e131193b | |||
| dfb00f27bb | |||
| 53dea65d8b | |||
| 2c16839de7 | |||
| c4f2d556f1 | |||
| 74900193ee | |||
| 2987d38a31 | |||
| 08b50bb9ea | |||
| 1257eb0872 | |||
| 1c8ef176b1 | |||
| e1e51cbf26 | |||
| 4c97cc8a41 | |||
| 7c6e124fe9 | |||
| 3fc312752b | |||
| d0cb5456b3 | |||
| 86072079de | |||
| 9440096106 | |||
| 649a913bbc | |||
| 53a3afaded | |||
| 33d3984990 | |||
| 5f86c6d1c2 | |||
| 10d5378ee3 | |||
| 7f27785927 | |||
| fed063a0c6 | |||
| 855062e36b | |||
| 7b74b6c68f | |||
| a01278aa8d | |||
| 99c49736fa | |||
| c1efdbb8ea | |||
| e240ae2669 | |||
| 945099b817 | |||
| f4b0fe6560 | |||
| efc6842a9d | |||
| 43931e951e | |||
| 134a480b03 | |||
| 0eb32ed7b7 | |||
| 1855ac421c | |||
| 7a69d6f20e | |||
| 2609a8293c | |||
| e68c47a897 | |||
| 06fa2b05d6 | |||
| db52e70121 | |||
| c81fe97cc2 | |||
| b9f4503206 | |||
| bd4dc501a2 | |||
| 8d1255f0bc | |||
| cc7abe6a8d | |||
| edfcc3f74b | |||
| b1c87b1eb4 | |||
| d37bda6871 | |||
| bb0d156bbf | |||
| b89ffcb6d0 | |||
| 323967b9fd | |||
| 4dd10b16e6 | |||
| 15a0d43180 | |||
| 5a1f7000ec | |||
| 277aaca6c5 | |||
| 9187f2d2b5 | |||
| 62d53ba62d | |||
| 9055f14bb9 | |||
| 0bd608b6cb | |||
| 497542d7a0 | |||
| 26b2a7d584 | |||
| 2d0bd9782a | |||
| 746a61a199 | |||
| 24df52e2f7 | |||
| 257f0b11f5 | |||
| b4f2039367 | |||
| cfb7d026d1 | |||
| 34acd6de35 | |||
| 0f3de599a6 | |||
| 0104a2cc9f | |||
| 81f4b08b6d | |||
| 10776b7de8 | |||
| b0efb648f2 | |||
| a20a8a68a6 | |||
| f8ec8402fe | |||
| aaf008f007 | |||
| f63d6b80c5 | |||
| 2f6ae1745f | |||
| 5f4098e5d4 | |||
| b49cc6c5e9 | |||
| dcc681ac74 | |||
| 240c78c12a | |||
| 6de409031e | |||
| ea274588b0 | |||
| e46e1512fa |
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Replace this text describing what problem occurred and what you expected to happen instead.
|
||||
|
||||
1. To reproduce this, open file '...'
|
||||
2. Click on '....'
|
||||
3. See error
|
||||
|
||||
**Attachments**
|
||||
If applicable, add screenshots to help explain your problem. Please also drag-drop any files necessary to show the error (rename the file extension from .ifc to .txt to upload). Private files can be uploaded to https://ifcopenshell.org/upload.html - only viewed by core developers and will be deleted afterwards.
|
||||
|
||||
**Debug information**
|
||||
If this is in Bonsai, paste the output from the Copy Debug Information option in Bonsai. It can be found under Quality and Coordination -> Quality Control -> Debug. If this is a general software issue, if relevant include details about IfcOpenShell version, operating system, Python version, etc.
|
||||
@@ -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,10 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Replace this text and describe a feature you'd like us to add. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable.
|
||||
@@ -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."
|
||||
@@ -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://ftp.nluug.nl/pub/graphics/blender/release/Blender4.3/blender-4.3.0-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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,6 +9,8 @@ on:
|
||||
paths:
|
||||
- 'src/pyodide/**'
|
||||
- '.github/workflows/publish-pyodide-demo-app.yml'
|
||||
branches:
|
||||
- v0.8.0
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -46,6 +46,7 @@ VERSION_MINOR:=$(shell cat '../../VERSION' | cut -d '.' -f 2)
|
||||
VERSION_PATCH:=$(shell cat '../../VERSION' | cut -d '.' -f 3)
|
||||
VERSION_DATE:=$(shell date '+%y%m%d')
|
||||
LAST_COMMIT_HASH:=$(shell git rev-parse HEAD)
|
||||
LAST_COMMIT_DATE:=$(shell git show -s --format=%cI)
|
||||
PYVERSION:=py310
|
||||
PYPI_IMP:=cp
|
||||
|
||||
@@ -254,6 +255,7 @@ ifeq ($(IS_STABLE), TRUE)
|
||||
else
|
||||
$(SED) "s/0.0.0/$(VERSION)-alpha$(VERSION_DATE)/" build/bonsai/blender_manifest.toml
|
||||
$(SED) "s/8888888/$(LAST_COMMIT_HASH)/" build/bonsai/__init__.py
|
||||
$(SED) "s/9999999/$(LAST_COMMIT_DATE)/" build/bonsai/__init__.py
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/pyproject.toml
|
||||
endif
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ from typing import Union, Any, Generator
|
||||
|
||||
|
||||
last_commit_hash = "8888888"
|
||||
last_commit_date = "9999999"
|
||||
|
||||
|
||||
def get_last_commit_hash() -> Union[str, None]:
|
||||
@@ -51,6 +52,12 @@ def get_last_commit_hash() -> Union[str, None]:
|
||||
return last_commit_hash[:7]
|
||||
|
||||
|
||||
def get_last_commit_date() -> Union[str, None]:
|
||||
if last_commit_date == str(9_999999):
|
||||
return None
|
||||
return last_commit_date
|
||||
|
||||
|
||||
# Accessed from bonsai extension:
|
||||
bbim_semver: dict[str, Any] = {}
|
||||
|
||||
@@ -96,6 +103,7 @@ def get_debug_info():
|
||||
"blender_version": bpy.app.version_string,
|
||||
"bonsai_version": bbim_version,
|
||||
"bonsai_commit_hash": get_last_commit_hash(),
|
||||
"bonsai_commit_date": get_last_commit_date(),
|
||||
"last_actions": last_actions,
|
||||
"last_error": last_error,
|
||||
}
|
||||
@@ -218,6 +226,7 @@ if IN_BLENDER:
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -83,6 +84,7 @@ modules = {
|
||||
"covering": None,
|
||||
"web": None,
|
||||
"light": None,
|
||||
"alignment": None,
|
||||
# Uncomment this line to enable loading of the demo module. Happy hacking!
|
||||
# The name "demo" must correlate to a folder name in `bim/module/`.
|
||||
# "demo": None,
|
||||
@@ -220,7 +222,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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1');
|
||||
FILE_NAME('/dev/null','2024-03-03T16:03:58+11:00',(),(),'IfcOpenShell v0.7.0-eaa2aa05a','IfcOpenShell v0.7.0-eaa2aa05a','Nobody');
|
||||
FILE_NAME('/dev/null','2025-03-27T17:30:27+05:00',(''),(''),'IfcOpenShell 0.0.0','IfcOpenShell 0.0.0','Nobody');
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
@@ -29,7 +29,7 @@ DATA;
|
||||
#22=IFCMATERIALLAYERSET((#24),$,$);
|
||||
#23=IFCRELASSOCIATESMATERIAL('0khP_Smdj12f2JrOTRbRKj',$,$,$,(#21),#22);
|
||||
#24=IFCMATERIALLAYER(#20,0.05,$,$,$,$,$);
|
||||
#25=IFCRELDECLARES('31JriXdev6BP2sC9lcxdC1',$,$,$,#2,(#26,#48,#72,#59,#92,#30,#96,#63,#21,#54,#87,#210,#153,#34,#67,#38,#82,#77,#42));
|
||||
#25=IFCRELDECLARES('31JriXdev6BP2sC9lcxdC1',$,$,$,#2,(#59,#92,#38,#82,#30,#96,#77,#63,#42,#54,#87,#153,#34,#21,#67,#210,#26,#48,#72));
|
||||
#26=IFCWALLTYPE('3R5onkTtX1IxdCMPoThRaw',$,'WAL100',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#27=IFCMATERIALLAYERSET((#29),$,$);
|
||||
#28=IFCRELASSOCIATESMATERIAL('0$mXRPCT16nP6Q70XFNvZv',$,$,$,(#26),#27);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -28,11 +28,13 @@ DATA;
|
||||
#21=IFCSIMPLEPROPERTYTEMPLATE('1UDakJ5_f7kBhggNSW4$h5',$,'SymbolsPath','Default symbols SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#22=IFCSIMPLEPROPERTYTEMPLATE('0d53LEtgLDQxnv__NfgH7i',$,'PatternsPath','Default patterns SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#23=IFCSIMPLEPROPERTYTEMPLATE('26qFNMv7nCHgU6Jd7Anga5',$,'ShadingStylesPath','Default shading styles',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcTypeProduct',(#25,#26,#27,#28));
|
||||
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcAnnotation/RADIUS,IfcAnnotation/DIAMETER,IfcTypeProduct',(#25,#26,#27,#28,#30));
|
||||
#25=IFCSIMPLEPROPERTYTEMPLATE('1rL2AbQsXD8RbpoWH5pYOV',$,'ShowDescriptionOnly','Hide the measurement values and show only annotation description',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#26=IFCSIMPLEPROPERTYTEMPLATE('0SVyOfB0rC2xNfdRYf3XvY',$,'SuppressZeroInches','Suppress 0 inch values in dimension annotation text (for example: 12'' - 0" -> 12'')',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#27=IFCSIMPLEPROPERTYTEMPLATE('2bUmj458PBqPAtUoI3MXsb',$,'TextPrefix','Text to add before annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#28=IFCSIMPLEPROPERTYTEMPLATE('0bnzttUb9BPuN597uNTXOE',$,'TextSuffix','Text to add after annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#29=IFCSIMPLEPROPERTYTEMPLATE('2pJmUDpB50VBdCOib1zcJJ',$,'Newline_At','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
|
||||
#30=IFCSIMPLEPROPERTYTEMPLATE('2TJn72t_v2cvBUG916Dpev',$,'CustomUnit','Dimension''s custom unit',.P_ENUMERATEDVALUE.,'IfcText',$,#31,$,$,$,.READWRITE.);
|
||||
#31=IFCPROPERTYENUMERATION('CustomUnit',(IFCTEXT('Feet and Inches - Fractional'),IFCTEXT('Feet - Decimal'),IFCTEXT('Inches - Fractional'),IFCTEXT('Inches - Decimal'),IFCTEXT('Meters'),IFCTEXT('Decimeters'),IFCTEXT('Centimeters'),IFCTEXT('Millimeters')),$);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
../../libraries/IFC2X3 Demo Library.ifc
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
../../libraries/IFC4 Demo Library.ifc
|
||||
@@ -0,0 +1 @@
|
||||
../../libraries/IFC4X3 Demo Library.ifc
|
||||
@@ -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,43 +100,14 @@ 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(obj.BIMObjectProperties.ifc_definition_id)
|
||||
element = self.file.by_id(tool.Blender.get_object_bim_props(obj).ifc_definition_id)
|
||||
if tool.Geometry.is_scaled(obj):
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
# update_representation might not apply scale if the object has openings
|
||||
|
||||
@@ -24,17 +24,15 @@ import ifcopenshell.util.unit
|
||||
import ifcopenshell.api.owner.settings
|
||||
import bonsai.bim
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.owner as core_owner
|
||||
from bpy.app.handlers import persistent
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.module.owner.prop import get_user_person, get_user_organisation
|
||||
from bonsai.bim.module.model.data import AuthoringData
|
||||
from bonsai.bim.module.aggregate.decorator import AggregateDecorator
|
||||
from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator
|
||||
from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator
|
||||
from bonsai.bim.module.nest.decorator import NestDecorator
|
||||
from mathutils import Vector
|
||||
from math import cos, degrees
|
||||
from math import cos
|
||||
from typing import Union, Callable
|
||||
|
||||
|
||||
@@ -55,23 +53,24 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
|
||||
return
|
||||
|
||||
if isinstance(obj, bpy.types.Material):
|
||||
props = obj.BIMStyleProperties
|
||||
props = tool.Style.get_material_style_props(obj)
|
||||
if ifc_definition_id := props.ifc_definition_id:
|
||||
if props.is_renaming:
|
||||
props.is_renmaing = False
|
||||
props.is_renaming = False
|
||||
return
|
||||
tool.Ifc.get().by_id(ifc_definition_id).Name = obj.name
|
||||
refresh_ui_data()
|
||||
return
|
||||
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
if not props.ifc_definition_id:
|
||||
return
|
||||
|
||||
if obj.BIMObjectProperties.is_renaming:
|
||||
obj.BIMObjectProperties.is_renaming = False
|
||||
if props.is_renaming:
|
||||
props.is_renaming = False
|
||||
return
|
||||
|
||||
element = tool.Ifc.get().by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
element = tool.Ifc.get().by_id(props.ifc_definition_id)
|
||||
if "/" in obj.name:
|
||||
object_name = obj.name
|
||||
element_name = obj.name.split("/", 1)[1]
|
||||
@@ -87,8 +86,8 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
|
||||
if not element.is_a("IfcRoot"):
|
||||
return
|
||||
element.Name = element_name
|
||||
if obj.BIMObjectProperties.collection:
|
||||
obj.BIMObjectProperties.collection.name = object_name
|
||||
if props.collection:
|
||||
props.collection.name = object_name
|
||||
refresh_ui_data()
|
||||
|
||||
|
||||
@@ -288,7 +287,7 @@ def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None
|
||||
def viewport_shading_changed_callback(area: bpy.types.Area) -> None:
|
||||
shading = area.spaces.active.shading.type
|
||||
if shading == "RENDERED":
|
||||
bpy.context.scene.BIMStylesProperties.active_style_type = "External"
|
||||
tool.Style.get_style_props().active_style_type = "External"
|
||||
|
||||
|
||||
def subscribe_to_viewport_shading_changes():
|
||||
@@ -348,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)
|
||||
@@ -362,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
|
||||
|
||||
@@ -76,8 +76,8 @@ def draw_attribute(
|
||||
elif value_name == "filepath_value":
|
||||
attribute.filepath_value.layout_file_select(layout, filter_glob=attribute.filter_glob, text=attribute.name)
|
||||
elif attribute.name in ("ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"):
|
||||
propis = bpy.context.scene.BIMWorkScheduleProperties
|
||||
for item in propis.durations_attributes:
|
||||
props = tool.Sequence.get_work_schedule_props()
|
||||
for item in props.durations_attributes:
|
||||
if item.name == attribute.name:
|
||||
duration_props = item
|
||||
layout.label(text=attribute.name)
|
||||
@@ -293,7 +293,10 @@ def prop_with_search(
|
||||
should_click_ok: bool = False,
|
||||
original_operator_path: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
) -> bpy.types.UILayout:
|
||||
"""
|
||||
:return: Added row.
|
||||
"""
|
||||
# kwargs are layout.prop arguments (text, icon, etc.)
|
||||
row = layout.row(align=True)
|
||||
row.prop(data, prop_name, **kwargs)
|
||||
@@ -307,6 +310,7 @@ def prop_with_search(
|
||||
op.original_operator_path = original_operator_path or ""
|
||||
except TypeError: # Prop is not iterable
|
||||
pass
|
||||
return row
|
||||
|
||||
|
||||
def get_enum_items(
|
||||
|
||||
@@ -33,7 +33,7 @@ import bonsai.bim.handler
|
||||
import bonsai.tool as tool
|
||||
from pathlib import Path
|
||||
from bonsai.tool.brick import BrickStore
|
||||
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired, cast
|
||||
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired
|
||||
|
||||
|
||||
IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
|
||||
@@ -106,10 +106,7 @@ class IfcStore:
|
||||
def get_file():
|
||||
if IfcStore.file is None:
|
||||
props = tool.Blender.get_bim_props()
|
||||
IfcStore.path = props.ifc_file
|
||||
# Interpret relative paths as relative to .blend file.
|
||||
if IfcStore.path and not os.path.isabs(IfcStore.path):
|
||||
IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path))
|
||||
IfcStore.set_path(props.ifc_file)
|
||||
if IfcStore.path:
|
||||
try:
|
||||
IfcStore.load_file(IfcStore.path)
|
||||
@@ -117,6 +114,13 @@ class IfcStore:
|
||||
print(f"Failed to load file {IfcStore.path}. Error details: {e}")
|
||||
return IfcStore.file
|
||||
|
||||
@staticmethod
|
||||
def set_path(value):
|
||||
IfcStore.path = value
|
||||
# Interpret relative paths as relative to .blend file.
|
||||
if IfcStore.path and not os.path.isabs(IfcStore.path):
|
||||
IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path))
|
||||
|
||||
@staticmethod
|
||||
def get_cache():
|
||||
if IfcStore.cache is None and IfcStore.path:
|
||||
@@ -265,9 +269,11 @@ class IfcStore:
|
||||
IfcStore.guid_map[global_id] = obj
|
||||
|
||||
if element.is_a("IfcSurfaceStyle"):
|
||||
obj.BIMStyleProperties.ifc_definition_id = element.id()
|
||||
props = tool.Style.get_material_style_props(obj)
|
||||
props.ifc_definition_id = element.id()
|
||||
else:
|
||||
obj.BIMObjectProperties.ifc_definition_id = element.id()
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
props.ifc_definition_id = element.id()
|
||||
|
||||
tool.Ifc.setup_listeners(obj)
|
||||
|
||||
@@ -405,9 +411,11 @@ class IfcStore:
|
||||
@staticmethod
|
||||
def purge_blender_ifc_data(obj: IFC_CONNECTED_TYPE) -> None:
|
||||
if isinstance(obj, bpy.types.Material):
|
||||
obj.BIMStyleProperties.ifc_definition_id = 0
|
||||
props = tool.Style.get_material_style_props(obj)
|
||||
props.ifc_definition_id = 0
|
||||
else: # bpy.types.Object
|
||||
obj.BIMObjectProperties.ifc_definition_id = 0
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
props.ifc_definition_id = 0
|
||||
|
||||
@staticmethod
|
||||
def execute_ifc_operator(
|
||||
|
||||
@@ -20,6 +20,7 @@ from __future__ import annotations
|
||||
import bpy
|
||||
import time
|
||||
import json
|
||||
import ifcpatch
|
||||
import logging
|
||||
import traceback
|
||||
import mathutils
|
||||
@@ -38,6 +39,7 @@ 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 ifcopenshell.util.shape import MatrixType
|
||||
|
||||
|
||||
class MaterialCreator:
|
||||
@@ -85,7 +87,7 @@ class MaterialCreator:
|
||||
|
||||
def load_existing_materials(self) -> None:
|
||||
for material in bpy.data.materials:
|
||||
if ifc_definition_id := material.BIMStyleProperties.ifc_definition_id:
|
||||
if ifc_definition_id := tool.Blender.get_ifc_definition_id(material):
|
||||
self.styles[ifc_definition_id] = material
|
||||
|
||||
def parse_element_type_material_styles(self, element: ifcopenshell.entity_instance) -> None:
|
||||
@@ -298,6 +300,20 @@ class IfcImporter:
|
||||
bpy.context.window_manager.progress_end()
|
||||
|
||||
def process_context_filter(self) -> None:
|
||||
contexts = self.file.by_type("IfcGeometricRepresentationContext")
|
||||
if len(contexts) > 100: # Probably something strange happening. Encountered from Revizto.
|
||||
print("Warning! Excessive contexts were found and merged where applicable.")
|
||||
uniques = {}
|
||||
i = 0
|
||||
for element in contexts:
|
||||
data = "-".join([str(a) for a in element])
|
||||
if unique := uniques.get(data, None):
|
||||
ifcopenshell.util.element.replace_element(element, unique)
|
||||
self.file.remove(element)
|
||||
i += 1
|
||||
else:
|
||||
uniques[data] = element
|
||||
print(f"Replaced {i} IfcGeometricRepresentationContext")
|
||||
tool.Loader.settings.contexts = ifcopenshell.util.representation.get_prioritised_contexts(self.file)
|
||||
tool.Loader.settings.context_settings = tool.Loader.create_settings()
|
||||
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
|
||||
@@ -479,14 +495,16 @@ class IfcImporter:
|
||||
if grid.WAxes:
|
||||
self.create_grid_axes(grid.WAxes, grid_obj, grid_placement)
|
||||
|
||||
def create_grid_axes(self, axes, grid_obj, grid_placement):
|
||||
def create_grid_axes(
|
||||
self, axes: list[ifcopenshell.entity_instance], grid_obj: bpy.types.Object, grid_placement: MatrixType
|
||||
) -> None:
|
||||
for axis in axes:
|
||||
shape = tool.Loader.create_generic_shape(axis.AxisCurve)
|
||||
mesh = self.create_mesh(axis, shape)
|
||||
obj = bpy.data.objects.new(tool.Loader.get_name(axis), mesh)
|
||||
obj.show_in_front = True
|
||||
self.link_element(axis, obj)
|
||||
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, grid_placement.copy()))
|
||||
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, grid_placement))
|
||||
|
||||
def create_element_types(self):
|
||||
for element_type in self.element_types:
|
||||
@@ -511,8 +529,9 @@ class IfcImporter:
|
||||
pass
|
||||
elif shape:
|
||||
mesh = self.create_mesh(element, shape)
|
||||
tool.Loader.link_mesh(shape, mesh)
|
||||
self.meshes[mesh_name] = mesh
|
||||
if mesh is not None:
|
||||
tool.Loader.link_mesh(shape, mesh)
|
||||
self.meshes[mesh_name] = mesh
|
||||
else:
|
||||
self.ifc_import_settings.logger.error("Failed to generate shape for %s", element)
|
||||
break
|
||||
@@ -698,9 +717,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
|
||||
@@ -742,12 +767,9 @@ class IfcImporter:
|
||||
def create_pointclouds(self, products: set[ifcopenshell.entity_instance]) -> set[ifcopenshell.entity_instance]:
|
||||
result = set()
|
||||
for product in products:
|
||||
representation = self.get_pointcloud_representation(product)
|
||||
if representation is not None:
|
||||
pointcloud = self.create_pointcloud(product, representation)
|
||||
if pointcloud is not None:
|
||||
if representation := self.get_pointcloud_representation(product):
|
||||
if pointcloud := self.create_pointcloud(product, representation):
|
||||
result.add(pointcloud)
|
||||
|
||||
return result
|
||||
|
||||
def create_pointcloud(
|
||||
@@ -795,8 +817,9 @@ class IfcImporter:
|
||||
materials_updated = bool(mesh)
|
||||
if mesh is None:
|
||||
mesh = self.create_mesh(element, shape)
|
||||
tool.Loader.link_mesh(shape, mesh)
|
||||
self.meshes[mesh_name] = mesh
|
||||
if mesh is not None:
|
||||
tool.Loader.link_mesh(shape, mesh)
|
||||
self.meshes[mesh_name] = mesh
|
||||
else:
|
||||
mesh = None
|
||||
|
||||
@@ -807,11 +830,10 @@ class IfcImporter:
|
||||
if element.is_a(ifcclass):
|
||||
obj.display_type = "WIRE"
|
||||
|
||||
if shape:
|
||||
if shape and mesh:
|
||||
# We use numpy here because Blender mathutils.Matrix is not accurate enough
|
||||
mat = np.array(shape.transformation.matrix).reshape((4, 4), order="F")
|
||||
mat = ifcopenshell.util.shape.get_shape_matrix(shape)
|
||||
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, mat))
|
||||
assert mesh # Type checker.
|
||||
if not materials_updated:
|
||||
self.material_creator.create(element, obj, mesh, tool.Geometry.does_shape_has_openings(shape))
|
||||
elif mesh: # When does this occur?
|
||||
@@ -933,7 +955,8 @@ class IfcImporter:
|
||||
self.project = {"ifc": project}
|
||||
obj = tool.Ifc.get_object(project)
|
||||
if obj:
|
||||
self.project["blender"] = obj.BIMObjectProperties.collection
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
self.project["blender"] = props.collection
|
||||
self.has_existing_project = True
|
||||
return
|
||||
self.project["blender"] = bpy.data.collections.new(
|
||||
@@ -943,10 +966,16 @@ class IfcImporter:
|
||||
obj.hide_select = True
|
||||
self.project["blender"].objects.link(obj)
|
||||
self.project["blender"].BIMCollectionProperties.obj = obj
|
||||
obj.BIMObjectProperties.collection = self.collections[project.GlobalId] = self.project["blender"]
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
props.collection = self.collections[project.GlobalId] = self.project["blender"]
|
||||
|
||||
def create_styles(self) -> None:
|
||||
for style in self.file.by_type("IfcSurfaceStyle"):
|
||||
styles = self.file.by_type("IfcSurfaceStyle")
|
||||
if len(styles) > self.ifc_import_settings.style_limit: # Probably something strange happening
|
||||
print("Warning! Excessive styles were found and merged where applicable.")
|
||||
ifcpatch.execute({"file": self.file, "recipe": "MergeStyles", "arguments": []})
|
||||
styles = self.file.by_type("IfcSurfaceStyle")
|
||||
for style in styles:
|
||||
self.create_style(style)
|
||||
|
||||
def create_style(self, style: ifcopenshell.entity_instance) -> None:
|
||||
@@ -959,10 +988,11 @@ class IfcImporter:
|
||||
self.material_creator.styles[style.id()] = blender_material
|
||||
|
||||
style_elements = tool.Style.get_style_elements(blender_material)
|
||||
props = tool.Style.get_material_style_props(blender_material)
|
||||
if tool.Style.has_blender_external_style(style_elements):
|
||||
blender_material.BIMStyleProperties.active_style_type = "External"
|
||||
props.active_style_type = "External"
|
||||
else:
|
||||
blender_material.BIMStyleProperties.active_style_type = "Shading"
|
||||
props.active_style_type = "Shading"
|
||||
|
||||
def place_objects_in_collections(self) -> None:
|
||||
for ifc_definition_id, obj in self.added_data.items():
|
||||
@@ -1027,7 +1057,7 @@ class IfcImporter:
|
||||
element: ifcopenshell.entity_instance,
|
||||
shape: Union[ifcopenshell.geom.ShapeElementType, ifcopenshell.geom.ShapeType],
|
||||
cartesian_point_offset: Union[npt.NDArray[np.float64], Literal[False]] = None,
|
||||
) -> bpy.types.Mesh:
|
||||
) -> Union[bpy.types.Mesh, None]:
|
||||
try:
|
||||
if hasattr(shape, "geometry"):
|
||||
# shape is ShapeElementType
|
||||
@@ -1068,7 +1098,10 @@ class IfcImporter:
|
||||
verts=verts,
|
||||
load_indexed_maps=self.ifc_import_settings.load_indexed_maps,
|
||||
)
|
||||
return tool.Loader.slice_layerset_mesh(element, mesh)
|
||||
# E.g. `create_mesh` is also used for IfcRelSpaceBoundary.
|
||||
if element.is_a("IfcObjectDefinition"):
|
||||
return tool.Loader.slice_layerset_mesh(element, mesh)
|
||||
return mesh
|
||||
except:
|
||||
self.ifc_import_settings.logger.error("Could not create mesh for %s", element)
|
||||
import traceback
|
||||
@@ -1125,6 +1158,7 @@ class IfcImportSettings:
|
||||
self.deflection_tolerance = 0.001
|
||||
self.angular_tolerance = 0.5
|
||||
self.void_limit = 30
|
||||
self.style_limit = 300
|
||||
# Locations greater than 1km are not considered "small sites" according to the georeferencing guide
|
||||
# Users can configure this if they have to handle larger sites but beware of surveying precision
|
||||
self.distance_limit = 1000
|
||||
@@ -1145,7 +1179,7 @@ class IfcImportSettings:
|
||||
|
||||
@staticmethod
|
||||
def factory(context=None, input_file=None, logger=None):
|
||||
scene_diff = bpy.context.scene.DiffProperties
|
||||
scene_diff = tool.Blender.get_diff_props()
|
||||
props = tool.Project.get_project_props()
|
||||
settings = IfcImportSettings()
|
||||
settings.input_file = input_file
|
||||
@@ -1162,6 +1196,7 @@ class IfcImportSettings:
|
||||
settings.deflection_tolerance = props.deflection_tolerance
|
||||
settings.angular_tolerance = props.angular_tolerance
|
||||
settings.void_limit = props.void_limit
|
||||
settings.style_limit = props.style_limit
|
||||
settings.distance_limit = props.distance_limit
|
||||
settings.false_origin_mode = props.false_origin_mode
|
||||
try:
|
||||
|
||||
@@ -20,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
|
||||
@@ -182,7 +190,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)
|
||||
@@ -388,7 +396,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,10 +99,21 @@ 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):
|
||||
@@ -114,3 +126,10 @@ class BIMAggregateProperties(PropertyGroup):
|
||||
default=False,
|
||||
update=update_aggregate_decorator,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
in_aggregate_mode: bool
|
||||
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
|
||||
|
||||
@@ -34,9 +34,9 @@ class BIM_PT_aggregate(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
if not (obj := context.active_object):
|
||||
return False
|
||||
props = context.active_object.BIMObjectProperties
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
if not props.ifc_definition_id:
|
||||
return False
|
||||
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
|
||||
@@ -49,11 +49,13 @@ 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()
|
||||
row.prop(props, "aggregate_decorator", icon="HIDE_OFF", 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()
|
||||
@@ -66,9 +68,9 @@ class BIM_PT_aggregate(Panel):
|
||||
col.enabled = False
|
||||
op = col.operator("bim.aggregate_assign_object", icon="CHECKMARK")
|
||||
if props.relating_object:
|
||||
op.relating_object = props.relating_object.BIMObjectProperties.ifc_definition_id
|
||||
op.relating_object = tool.Blender.get_object_bim_props(props.relating_object).ifc_definition_id
|
||||
elif props.related_object:
|
||||
op.related_object = props.related_object.BIMObjectProperties.ifc_definition_id
|
||||
op.related_object = tool.Blender.get_object_bim_props(props.related_object).ifc_definition_id
|
||||
row.operator("bim.disable_editing_aggregate", icon="CANCEL", text="")
|
||||
return
|
||||
else:
|
||||
@@ -115,9 +117,9 @@ class BIM_PT_linked_aggregate(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
if not (obj := context.active_object):
|
||||
return False
|
||||
props = context.active_object.BIMObjectProperties
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
if not props.ifc_definition_id:
|
||||
return False
|
||||
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
|
||||
@@ -131,9 +133,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:
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
|
||||
# from . import ui, prop, operator
|
||||
from . import operator
|
||||
|
||||
classes = (operator.ImportAlignmentCSV,)
|
||||
|
||||
|
||||
def menu_func_import(self, context):
|
||||
self.layout.operator(operator.ImportAlignmentCSV.bl_idname, text="Alignment (.csv)")
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
|
||||
@@ -0,0 +1,114 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>, 2022 Yassine Oualid <yassine@sigmadimensions.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# pyright: reportUnnecessaryTypeIgnoreComment=error
|
||||
|
||||
import os
|
||||
|
||||
import ifcopenshell.api.alignment
|
||||
import ifcopenshell.api.alignment.add_stationing_to_alignment
|
||||
|
||||
import bpy
|
||||
import json
|
||||
import time
|
||||
import calendar
|
||||
import isodate
|
||||
import bonsai.core.sequence as core
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.module.sequence.helper as helper
|
||||
import ifcopenshell.api.spatial
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.sequence
|
||||
import ifcopenshell.util.selector
|
||||
from datetime import datetime
|
||||
from dateutil import parser, relativedelta
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
from typing import get_args, TYPE_CHECKING
|
||||
from typing_extensions import assert_never
|
||||
|
||||
|
||||
class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
bl_idname = "bim.import_alignment_csv"
|
||||
bl_label = "Import Alignment CSV"
|
||||
bl_description = " Import alignment from the provided .csv file."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filename_ext = ".csv"
|
||||
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
ifc_file = tool.Ifc.get()
|
||||
if ifc_file is None:
|
||||
cls.poll_message_set("No IFC file is loaded.")
|
||||
return False
|
||||
elif ifc_file.schema != "IFC4X3":
|
||||
cls.poll_message_set("Schema must be IFC4x3.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = tool.Ifc.get()
|
||||
start = time.time()
|
||||
alignment = ifcopenshell.api.alignment.create_alignment_from_csv(self.file, self.filepath)
|
||||
ifcopenshell.api.alignment.create_geometric_representation(self.file, alignment)
|
||||
ifcopenshell.api.alignment.add_stationing_to_alignment(self.file, alignment=alignment, start_station=0.0)
|
||||
|
||||
# IFC 4.1.5.1 alignments cannot be contained in spatial structures, but can be referenced into them
|
||||
sites = self.file.by_type("IfcSite")
|
||||
for site in sites:
|
||||
ifcopenshell.api.spatial.reference_structure(self.file, products=[alignment], relating_structure=site)
|
||||
|
||||
# process the generated IfcReferent for the alignment
|
||||
for rel in alignment.IsNestedBy:
|
||||
for referent in rel.RelatedObjects:
|
||||
if referent.is_a("IfcReferent"):
|
||||
referent_obj = bpy.data.objects.new(tool.Loader.get_name(referent), None)
|
||||
tool.Geometry.link(referent, referent_obj)
|
||||
tool.Collector.assign(referent_obj, should_clean_users_collection=False)
|
||||
|
||||
# an alignment can be an aggregation of multiple child alignments (ie. multiple verticals for a single horizontal)
|
||||
# get all the alignment curves
|
||||
curves = []
|
||||
for rel in alignment.IsDecomposedBy:
|
||||
for agg in rel.RelatedObjects:
|
||||
if agg.is_a("IfcAlignment"):
|
||||
curves.append(ifcopenshell.api.alignment.get_curve(agg)) # 3D curve
|
||||
|
||||
# if there aren't any curves from aggregation, then there is only a single vertical or no vertical
|
||||
if len(curves) == 0:
|
||||
curves.append(ifcopenshell.api.alignment.get_curve(alignment))
|
||||
|
||||
settings = ifcopenshell.geom.settings()
|
||||
for curve in curves:
|
||||
shape = ifcopenshell.geom.create_shape(settings, curve)
|
||||
|
||||
# create a new Blender mesh
|
||||
mesh_name = tool.Loader.get_mesh_name_from_shape(shape)
|
||||
mesh = bpy.data.meshes.new(mesh_name)
|
||||
m = tool.Loader.convert_geometry_to_mesh(shape, mesh)
|
||||
|
||||
# create a new Blender object
|
||||
alignment_obj = bpy.data.objects.new(tool.Loader.get_name(alignment), m)
|
||||
|
||||
# link the blender object to with the alignment element
|
||||
tool.Geometry.link(alignment, alignment_obj)
|
||||
|
||||
# assign the object to the blender collections
|
||||
tool.Collector.assign(alignment_obj, should_clean_users_collection=False)
|
||||
|
||||
self.report({"INFO"}, "Imported in %s seconds" % (time.time() - start))
|
||||
@@ -26,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)
|
||||
|
||||
@@ -42,6 +42,7 @@ import ifcopenshell.util.unit
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.module.bcf.prop as bcf_prop
|
||||
import bonsai.bim.module.bcf.bcfstore as bcfstore
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
from pathlib import Path
|
||||
from math import radians, degrees, atan, tan, cos, sin
|
||||
from mathutils import Vector, Matrix, Euler, geometry
|
||||
@@ -63,12 +64,14 @@ class NewBcfProject(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LoadBcfProject(bpy.types.Operator):
|
||||
class LoadBcfProject(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.load_bcf_project"
|
||||
bl_label = "Load BCF Project"
|
||||
bl_description = "Load the BCF file."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
|
||||
filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"})
|
||||
filename_ext = ".bcf"
|
||||
|
||||
def execute(self, context):
|
||||
# Operator is also used when new project is created by not yet saved.
|
||||
@@ -108,10 +111,6 @@ class LoadBcfProject(bpy.types.Operator):
|
||||
self.report({"INFO"}, f"BCF Project '{Path(self.filepath).name}' is loaded.")
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class UnloadBcfProject(bpy.types.Operator):
|
||||
bl_idname = "bim.unload_bcf_project"
|
||||
@@ -341,13 +340,15 @@ class EditBcfTopic(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SaveBcfProject(bpy.types.Operator):
|
||||
class SaveBcfProject(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.save_bcf_project"
|
||||
bl_label = "Save BCF Project"
|
||||
bl_description = "Save active BCF project by the provided filepath."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"})
|
||||
save_current_bcf: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
filename_ext = ".bcf"
|
||||
|
||||
def execute(self, context):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
@@ -364,8 +365,7 @@ class SaveBcfProject(bpy.types.Operator):
|
||||
self.filepath = str(path)
|
||||
return self.execute(context)
|
||||
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
return ExportHelper.invoke(self, context, event)
|
||||
|
||||
|
||||
class AddBcfTopic(bpy.types.Operator):
|
||||
@@ -1534,12 +1534,13 @@ class OpenBcfReferenceLink(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectBcfHeaderFile(bpy.types.Operator):
|
||||
class SelectBcfHeaderFile(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_bcf_header_file"
|
||||
bl_label = "Select BCF Header File"
|
||||
bl_description = "Select filepath for BCF header reference."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"})
|
||||
filename_ext = ".ifc"
|
||||
|
||||
def execute(self, context):
|
||||
if self.filepath:
|
||||
@@ -1547,16 +1548,12 @@ class SelectBcfHeaderFile(bpy.types.Operator):
|
||||
props.file_reference = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class SelectBcfBimSnippetReference(bpy.types.Operator):
|
||||
class SelectBcfBimSnippetReference(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_bcf_bim_snippet_reference"
|
||||
bl_label = "Select BCF BIM Snippet Reference"
|
||||
bl_description = "Select filepath for BCF snippet reference."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def execute(self, context):
|
||||
if self.filepath:
|
||||
@@ -1564,16 +1561,12 @@ class SelectBcfBimSnippetReference(bpy.types.Operator):
|
||||
props.bim_snippet_reference = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class SelectBcfDocumentReference(bpy.types.Operator):
|
||||
class SelectBcfDocumentReference(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_bcf_document_reference"
|
||||
bl_label = "Select BCF Document Reference"
|
||||
bl_description = "Select filepath for BCF document reference."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def execute(self, context):
|
||||
if self.filepath:
|
||||
@@ -1581,10 +1574,6 @@ class SelectBcfDocumentReference(bpy.types.Operator):
|
||||
props.document_reference = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class LoadBcfHeaderIfcFile(bpy.types.Operator):
|
||||
bl_idname = "bim.load_bcf_header_ifc_file"
|
||||
|
||||
@@ -43,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)
|
||||
|
||||
@@ -26,6 +26,7 @@ import numpy as np
|
||||
import multiprocessing
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.boundary
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.shape
|
||||
@@ -41,6 +42,7 @@ from bonsai.bim.module.model.decorator import ProfileDecorator
|
||||
from bonsai.bim.module.boundary.decorator import BoundaryDecorator
|
||||
import bonsai.core
|
||||
import bonsai.core.geometry
|
||||
from typing import Union, Optional
|
||||
|
||||
|
||||
def disable_editing_boundary_geometry(context):
|
||||
@@ -58,7 +60,7 @@ def disable_editing_boundary_geometry(context):
|
||||
|
||||
|
||||
class Loader:
|
||||
def __init__(self, operator=None):
|
||||
def __init__(self, operator: Optional[bpy.types.Operator] = None):
|
||||
self.operator = operator
|
||||
self.ifc_file = None
|
||||
self.logger = None
|
||||
@@ -67,17 +69,20 @@ class Loader:
|
||||
self.fallback_settings = self.load_fallback_settings()
|
||||
self.load_importer()
|
||||
|
||||
def create_mesh(self, boundary):
|
||||
def create_mesh(self, boundary: ifcopenshell.entity_instance) -> Union[bpy.types.Mesh, None]:
|
||||
# ConnectionGeometry is optional in IFC schema for some reasons.
|
||||
if not boundary.ConnectionGeometry:
|
||||
return None
|
||||
assert self.ifc_importer
|
||||
assert self.operator
|
||||
surface = boundary.ConnectionGeometry.SurfaceOnRelatingElement
|
||||
# Workaround for invalid geometry provided by Revit. See https://github.com/Autodesk/revit-ifc/issues/270
|
||||
if surface.is_a("IfcCurveBoundedPlane") and not getattr(surface, "InnerBoundaries", None):
|
||||
surface.InnerBoundaries = ()
|
||||
try:
|
||||
shape = ifcopenshell.geom.create_shape(self.settings, surface)
|
||||
mesh = self.ifc_importer.create_mesh(None, shape)
|
||||
mesh = self.ifc_importer.create_mesh(boundary, shape)
|
||||
assert mesh
|
||||
tool.Loader.link_mesh(shape, mesh)
|
||||
except RuntimeError:
|
||||
# Fallback solution for invalid geometry provided by Revit. (InnerBoundaries cuting OuterBoundary)
|
||||
@@ -97,7 +102,8 @@ class Loader:
|
||||
return None
|
||||
mesh = bpy.data.meshes.new(str(surface.id()))
|
||||
bm = bmesh.new()
|
||||
verts = [bm.verts.new(shape.verts[i : i + 3]) for i in range(0, len(shape.verts), 3)]
|
||||
coords = ifcopenshell.util.shape.get_vertices(shape)
|
||||
verts = [bm.verts.new(coord) for coord in coords]
|
||||
bm.faces.new(verts)
|
||||
for inner_boundary in surface.InnerBoundaries:
|
||||
try:
|
||||
@@ -108,7 +114,9 @@ class Loader:
|
||||
f"Skipping an inner boundary for IfcRelSpaceBoundary with ID {boundary.id()}. Geometry might be invalid",
|
||||
)
|
||||
return None
|
||||
verts = [bm.verts.new(shape.verts[i : i + 3]) for i in range(0, len(shape.verts), 3)]
|
||||
coords = ifcopenshell.util.shape.get_vertices(shape)
|
||||
verts = [bm.verts.new(coord) for coord in coords]
|
||||
bm.faces.new(verts)
|
||||
for i in range(len(verts) - 1):
|
||||
bm.edges.new(verts[i : i + 2])
|
||||
bm.edges.new((verts[-1], verts[0]))
|
||||
@@ -131,7 +139,7 @@ class Loader:
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
return settings
|
||||
|
||||
def load_importer(self):
|
||||
def load_importer(self) -> None:
|
||||
self.ifc_file = tool.Ifc.get()
|
||||
self.logger = logging.getLogger("ImportIFC")
|
||||
ifc_import_settings = import_ifc.IfcImportSettings.factory(bpy.context, IfcStore.path, self.logger)
|
||||
@@ -280,7 +288,7 @@ class SelectProjectBoundaries(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def get_colour(ifc_boundary):
|
||||
def get_colour(ifc_boundary: ifcopenshell.entity_instance) -> tuple[float, float, float, float]:
|
||||
"""Return a color depending on IfcClass given"""
|
||||
product_colors = {
|
||||
"IfcWall": (0.7, 0.3, 0, 1),
|
||||
@@ -316,7 +324,8 @@ class ColourByRelatedBuildingElement(bpy.types.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
for obj in context.visible_objects:
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
if not props.ifc_definition_id:
|
||||
continue
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element.is_a("IfcRelSpaceBoundary"):
|
||||
@@ -583,47 +592,70 @@ class DecorateBoundaries(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_boundary"
|
||||
bl_label = "Add Boundary"
|
||||
bl_description = (
|
||||
"There are two options for this operator:\n"
|
||||
"- just 1 IfcSpace is selected - generate a boundary based on "
|
||||
"potential nearby (0.1m) boundary elements.\n"
|
||||
"- 1 IfcSpace and 1 IfcElement are selected - generate a boundary "
|
||||
"for IfcSpace and use selected IfcElement as boundary's building element."
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
relating_space = None
|
||||
related_building_element = None
|
||||
relating_space_obj = None
|
||||
related_building_element_obj = None
|
||||
parent_boundaries = []
|
||||
parent_boundaries: list[ifcopenshell.entity_instance] = []
|
||||
|
||||
objs = context.selected_objects
|
||||
objs = tool.Blender.get_selected_objects()
|
||||
ifc_objects = {element: obj for obj in objs if (element := tool.Ifc.get_entity(obj))}
|
||||
if len(ifc_objects) not in (1, 2):
|
||||
self.report({"ERROR"}, "Please select one or two IFC elements.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if not any((element := tool.Ifc.get_entity(obj)) and element.is_a("IfcSpace") for obj in objs):
|
||||
self.report({"INFO"}, "No IfcSpace elements selected.")
|
||||
return {"FINISHED"}
|
||||
if not any(element.is_a("IfcSpace") for element in ifc_objects):
|
||||
self.report({"ERROR"}, "One of the selected objects must be an IfcSpace.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if len(objs) == 2:
|
||||
# The user may select two objects, a space and its related building element
|
||||
for obj in objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
if element.is_a("IfcSpace"):
|
||||
relating_space = element
|
||||
relating_space_obj = obj
|
||||
else:
|
||||
related_building_element = element
|
||||
related_building_element_obj = obj
|
||||
# Validate input.
|
||||
space = next((e for e in ifc_objects.keys() if e.is_a("IfcSpace")), None)
|
||||
if not space:
|
||||
self.report({"ERROR"}, "One of the selected objects must be an IfcSpace.")
|
||||
return {"CANCELLED"}
|
||||
element = next((e for e in ifc_objects.keys() if e.is_a("IfcElement")), None)
|
||||
if not element:
|
||||
self.report({"ERROR"}, "One of the selected objects must be an IfcElement.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
parent_boundary = self.create_element_boundary(
|
||||
context, relating_space, relating_space_obj, related_building_element, related_building_element_obj
|
||||
context, space, ifc_objects[space], element, ifc_objects[element]
|
||||
)
|
||||
if parent_boundary:
|
||||
parent_boundaries.append(parent_boundary)
|
||||
|
||||
if not parent_boundary:
|
||||
self.report({"ERROR"}, "Failed to create a boundary.")
|
||||
return {"FINISHED"}
|
||||
parent_boundaries.append(parent_boundary)
|
||||
|
||||
elif len(objs) == 1:
|
||||
space = next(iter(ifc_objects))
|
||||
if not space.is_a("IfcSpace"):
|
||||
self.report({"ERROR"}, "1 element selected but it's not an IfcSpace - please, select IfcSpace..")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# New prototype, old code not yet removed. Still testing.
|
||||
space = tool.Ifc.get_entity(objs[0])
|
||||
if space.is_a("IfcSpace"):
|
||||
self.auto_generate_boundaries(space, objs[0])
|
||||
elif len(objs) == 1:
|
||||
res = self.auto_generate_boundaries(space, ifc_objects[space])
|
||||
if isinstance(res, str):
|
||||
self.report({"ERROR"}, res)
|
||||
return {"FINISHED"}
|
||||
parent_boundaries.extend(res)
|
||||
|
||||
elif len(objs) == 1 and False:
|
||||
# Optionally the user may select just the space, and the building element shall be auto-detected
|
||||
# TODO : refactor to be able to generate all boundaries for selected space automatically or with an option
|
||||
|
||||
relating_space = None
|
||||
related_building_element = None
|
||||
relating_space_obj = None
|
||||
related_building_element_obj = None
|
||||
|
||||
def msg(self, context):
|
||||
self.layout.label(text="Please set an active container to detect space boundaries from.")
|
||||
|
||||
@@ -660,8 +692,17 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj = tool.Ifc.get_object(parent_boundary)
|
||||
obj.select_set(True)
|
||||
|
||||
def auto_generate_boundaries(self, space, space_obj):
|
||||
def auto_generate_boundaries(
|
||||
self, space: ifcopenshell.entity_instance, space_obj: bpy.types.Object
|
||||
) -> Union[str, list[ifcopenshell.entity_instance]]:
|
||||
"""
|
||||
:return: list of created boundaries or a string with error description.
|
||||
"""
|
||||
ifc_file = tool.Ifc.get()
|
||||
props = tool.Model.get_model_props()
|
||||
boundaries: list[ifcopenshell.entity_instance] = []
|
||||
assert isinstance(space_obj.data, bpy.types.Mesh)
|
||||
|
||||
# Identify all potential building elements
|
||||
# TODO: don't select everything, use AABB culling in Blender
|
||||
building_elements = (
|
||||
@@ -681,7 +722,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
shapes = {}
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("disable-opening-subtractions", True)
|
||||
iterator = ifcopenshell.geom.iterator(settings, tool.Ifc.get(), multiprocessing.cpu_count(), include=include)
|
||||
iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=include)
|
||||
if iterator.initialize():
|
||||
while True:
|
||||
tree.add_element(iterator.get_native())
|
||||
@@ -694,12 +735,12 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
building_elements = [e for e in tree.select(space, extend=0.1) if e != space]
|
||||
|
||||
if not building_elements:
|
||||
return
|
||||
return "No building elements found to create boundaries."
|
||||
|
||||
# Create a dissolved bmesh for the space
|
||||
space_bm = bmesh.new()
|
||||
space_bm.from_mesh(space_obj.data)
|
||||
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts, edges=space_bm.edges)
|
||||
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts[:], edges=space_bm.edges[:])
|
||||
|
||||
# Create dissolved bmeshes for all boundary elements
|
||||
building_element_bms = {}
|
||||
@@ -777,7 +818,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# we cheat by using the exterior boundary to mean "gross".
|
||||
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
|
||||
|
||||
parent_boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
|
||||
parent_boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
|
||||
if building_element.is_a("IfcVirtualElement"):
|
||||
parent_boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
|
||||
else:
|
||||
@@ -809,6 +850,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
exterior_boundary_polygon, space_face_matrix
|
||||
)
|
||||
self.set_boundary_name(parent_boundary)
|
||||
boundaries.append(parent_boundary)
|
||||
|
||||
for rel in getattr(building_element, "HasOpenings", []):
|
||||
opening = rel.RelatedOpeningElement
|
||||
@@ -854,7 +896,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if opening_polygon.intersection(exterior_boundary_polygon).area == 0:
|
||||
continue
|
||||
|
||||
boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
|
||||
boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
|
||||
boundary.RelatingSpace = space
|
||||
boundary.RelatedBuildingElement = filling or opening
|
||||
boundary.ConnectionGeometry = self.create_connection_geometry_from_polygon(
|
||||
@@ -868,29 +910,39 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if boundary.is_a() != "IfcRelSpaceBoundary":
|
||||
boundary.ParentBoundary = parent_boundary
|
||||
self.set_boundary_name(boundary)
|
||||
boundaries.append(boundary)
|
||||
|
||||
return boundaries
|
||||
|
||||
def create_element_boundary(
|
||||
self, context, relating_space, relating_space_obj, related_building_element, related_building_element_obj
|
||||
):
|
||||
if not relating_space or not related_building_element:
|
||||
return
|
||||
self,
|
||||
context: bpy.types.Context,
|
||||
space: ifcopenshell.entity_instance,
|
||||
space_obj: bpy.types.Object,
|
||||
building_element: ifcopenshell.entity_instance,
|
||||
building_element_obj: bpy.types.Object,
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""Create element boundary for IfcSpace and IfcElement as building element.
|
||||
|
||||
:return: IfcRelSpaceBoundary or ``None`` if couldn't find a face on ``space_obj``
|
||||
that would be close enough to ``related_building_element_obj``.
|
||||
"""
|
||||
props = tool.Model.get_model_props()
|
||||
# Find which face on space should be bounded to related building element
|
||||
# TODO: Handle round wall where multiple faces need to be bound to the same related building element
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(relating_space_obj.data)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi * 2 / 360, verts=bm.verts, edges=bm.edges)
|
||||
space_bm = bmesh.new()
|
||||
assert isinstance(space_obj.data, bpy.types.Mesh)
|
||||
space_bm.from_mesh(space_obj.data)
|
||||
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts[:], edges=space_bm.edges[:])
|
||||
|
||||
target_distance = inf
|
||||
target_face = None
|
||||
for face in bm.faces:
|
||||
centroid = relating_space_obj.matrix_world @ face.calc_center_median()
|
||||
raycast = related_building_element_obj.closest_point_on_mesh(
|
||||
related_building_element_obj.matrix_world.inverted() @ centroid, distance=1
|
||||
)
|
||||
to_building_obj_space = building_element_obj.matrix_world.inverted()
|
||||
for face in space_bm.faces:
|
||||
centroid = space_obj.matrix_world @ face.calc_center_median()
|
||||
raycast = building_element_obj.closest_point_on_mesh(to_building_obj_space @ centroid, distance=1)
|
||||
if raycast[0]:
|
||||
distance = (related_building_element_obj.matrix_world @ raycast[1] - centroid).length
|
||||
distance = (building_element_obj.matrix_world @ raycast[1] - centroid).length
|
||||
if distance < target_distance:
|
||||
target_face = face
|
||||
target_distance = distance
|
||||
@@ -905,9 +957,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
target_face_polygon = shapely.Polygon([tuple((target_face_matrix_i @ v).xy) for v in target_face_verts])
|
||||
|
||||
related_building_element_polygon = self.get_flattened_polygon(
|
||||
related_building_element, relating_space_obj, target_face_matrix_i
|
||||
)
|
||||
related_building_element_polygon = self.get_flattened_polygon(building_element, space_obj, target_face_matrix_i)
|
||||
|
||||
gross_boundary_polygon = target_face_polygon.intersection(related_building_element_polygon)
|
||||
|
||||
@@ -923,7 +973,8 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
):
|
||||
return
|
||||
|
||||
parent_boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
|
||||
ifc_file = tool.Ifc.get()
|
||||
parent_boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
|
||||
parent_boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
|
||||
# Set to EXTERNAL by default and turn later to internal if there is a corresponding boundary relating to an
|
||||
# internal space
|
||||
@@ -936,14 +987,14 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# space.
|
||||
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
|
||||
|
||||
for rel in getattr(related_building_element, "HasOpenings", []):
|
||||
for rel in getattr(building_element, "HasOpenings", []):
|
||||
opening = rel.RelatedOpeningElement
|
||||
if not opening.HasFillings:
|
||||
continue
|
||||
# TODO: HasFilling is a zero to many relationship. How to handle many ?
|
||||
filling = opening.HasFillings[0].RelatedBuildingElement
|
||||
|
||||
opening_polygon = self.get_flattened_polygon(opening, relating_space_obj, target_face_matrix_i)
|
||||
opening_polygon = self.get_flattened_polygon(opening, space_obj, target_face_matrix_i)
|
||||
|
||||
# An inner boundary is supposed to overlap its parent boundary according to IFC4 documentation
|
||||
# so we extend our exterior boundary with all opening which has a filling
|
||||
@@ -959,10 +1010,10 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
connection_geometry = self.create_connection_geometry_from_polygon(opening_polygon, target_face_matrix)
|
||||
boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
|
||||
boundary.RelatingSpace = relating_space
|
||||
boundary.RelatingSpace = space
|
||||
boundary.RelatedBuildingElement = filling
|
||||
boundary.ConnectionGeometry = connection_geometry
|
||||
if related_building_element.is_a("IfcVirtualElement"):
|
||||
if building_element.is_a("IfcVirtualElement"):
|
||||
boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
|
||||
else:
|
||||
boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
|
||||
@@ -974,20 +1025,23 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
connection_geometry = self.create_connection_geometry_from_polygon(
|
||||
exterior_boundary_polygon, target_face_matrix
|
||||
)
|
||||
parent_boundary.RelatingSpace = relating_space
|
||||
parent_boundary.RelatedBuildingElement = related_building_element
|
||||
parent_boundary.RelatingSpace = space
|
||||
parent_boundary.RelatedBuildingElement = building_element
|
||||
parent_boundary.ConnectionGeometry = connection_geometry
|
||||
self.set_boundary_name(parent_boundary)
|
||||
return parent_boundary
|
||||
|
||||
def get_face_matrix(self, p1, p2, p3):
|
||||
def get_face_matrix(self, p1: Vector, p2: Vector, p3: Vector) -> Matrix:
|
||||
edge1 = p2 - p1
|
||||
edge2 = p3 - p1
|
||||
normal = edge1.cross(edge2)
|
||||
assert isinstance(normal, Vector)
|
||||
|
||||
z_axis = normal.normalized()
|
||||
x_axis = p2 - p1
|
||||
x_axis.normalize()
|
||||
y_axis = z_axis.cross(x_axis)
|
||||
assert isinstance(y_axis, Vector)
|
||||
|
||||
mat = Matrix()
|
||||
mat.col[0][:3] = x_axis
|
||||
@@ -1057,7 +1111,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
return surface
|
||||
|
||||
def set_boundary_name(self, boundary):
|
||||
def set_boundary_name(self, boundary: ifcopenshell.entity_instance):
|
||||
"""
|
||||
By convention 1stLevel and 2ndLevel boundary have specific name and description
|
||||
See https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRelSpaceBoundary.htm
|
||||
|
||||
@@ -52,9 +52,9 @@ class BIM_PT_Boundary(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
if not (obj := context.active_object):
|
||||
return False
|
||||
props = context.active_object.BIMObjectProperties
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
if not props.ifc_definition_id:
|
||||
return False
|
||||
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
|
||||
@@ -65,7 +65,7 @@ class BIM_PT_Boundary(Panel):
|
||||
def draw(self, context):
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
props = obj.BIMObjectProperties
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
ifc_file = tool.Ifc.get()
|
||||
boundary = ifc_file.by_id(props.ifc_definition_id)
|
||||
self.bprops = tool.Boundary.get_object_boundary_props(obj)
|
||||
@@ -128,9 +128,9 @@ class BIM_PT_SpaceBoundaries(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
if not (obj := context.active_object):
|
||||
return False
|
||||
props = context.active_object.BIMObjectProperties
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
if not props.ifc_definition_id:
|
||||
return False
|
||||
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
|
||||
@@ -145,7 +145,9 @@ class BIM_PT_SpaceBoundaries(Panel):
|
||||
if not SpaceBoundariesData.is_loaded:
|
||||
SpaceBoundariesData.load()
|
||||
|
||||
self.props = context.active_object.BIMObjectProperties
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
self.props = tool.Blender.get_object_bim_props(obj)
|
||||
self.ifc_file = tool.Ifc.get()
|
||||
row = self.layout.row()
|
||||
row.operator("bim.load_space_boundaries")
|
||||
|
||||
@@ -52,7 +52,7 @@ class BrickschemaData:
|
||||
def active_relations(cls):
|
||||
if BrickStore.graph is None:
|
||||
return []
|
||||
props = bpy.context.scene.BIMBrickProperties
|
||||
props = tool.Brick.get_brick_props()
|
||||
try:
|
||||
brick = props.bricks[props.active_brick_index]
|
||||
except:
|
||||
|
||||
@@ -22,29 +22,26 @@ import ifcopenshell.api
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.brick as core
|
||||
import bonsai.bim.handler
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.tool.brick import BrickStore
|
||||
|
||||
|
||||
class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
bl_idname = "bim.load_brick_project"
|
||||
bl_label = "Load Brickschema Project"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Load in a Brick project from a file"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"})
|
||||
filename_ext = ".ttl"
|
||||
|
||||
def _execute(self, context):
|
||||
if os.path.exists(self.filepath) and "ttl" in os.path.splitext(self.filepath)[1].lower():
|
||||
root = context.scene.BIMBrickProperties.brick_list_root
|
||||
root = tool.Brick.get_brick_props().brick_list_root
|
||||
core.load_brick_project(tool.Brick, filepath=self.filepath, brick_root=root)
|
||||
else:
|
||||
self.report({"ERROR"}, f"Failed to load {self.filepath}")
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class ViewBrickClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.view_brick_class"
|
||||
@@ -111,10 +108,10 @@ class AssignBrickReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Assign the selected Ifc entity to the selected Brick entity"
|
||||
|
||||
def _execute(self, context):
|
||||
if not context.active_object:
|
||||
if not (obj := context.active_object) or not (element := tool.Ifc.get_entity(obj)):
|
||||
self.report({"ERROR"}, f"No Ifc selected")
|
||||
return
|
||||
props = context.scene.BIMBrickProperties
|
||||
props = tool.Brick.get_brick_props()
|
||||
try:
|
||||
props.bricks[props.active_brick_index]
|
||||
except:
|
||||
@@ -123,7 +120,7 @@ class AssignBrickReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
core.assign_brick_reference(
|
||||
tool.Ifc,
|
||||
tool.Brick,
|
||||
element=tool.Ifc.get_entity(context.active_object),
|
||||
element=element,
|
||||
library=tool.Ifc.get().by_id(int(props.libraries)),
|
||||
brick_uri=props.bricks[props.active_brick_index].uri,
|
||||
)
|
||||
@@ -136,7 +133,7 @@ class AddBrick(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Create the Brick entity"
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBrickProperties
|
||||
props = tool.Brick.get_brick_props()
|
||||
core.add_brick(
|
||||
tool.Ifc,
|
||||
tool.Brick,
|
||||
@@ -155,7 +152,7 @@ class AddBrickRelation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Create the Brick relationship"
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBrickProperties
|
||||
props = tool.Brick.get_brick_props()
|
||||
brick = props.bricks[props.active_brick_index]
|
||||
if props.new_brick_relation_type == "http://www.w3.org/2000/01/rdf-schema#label":
|
||||
object = props.new_brick_relation_object
|
||||
@@ -173,7 +170,7 @@ class ConvertIfcToBrick(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Convert Ifc entities and relations to Brick entities and relations"
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBrickProperties
|
||||
props = tool.Brick.get_brick_props()
|
||||
library = None
|
||||
if props.libraries:
|
||||
library = tool.Ifc.get().by_id(int(props.libraries))
|
||||
@@ -201,7 +198,8 @@ class NewBrickFile(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
def _execute(self, context):
|
||||
root = context.scene.BIMBrickProperties.brick_list_root
|
||||
props = tool.Brick.get_brick_props()
|
||||
root = props.brick_list_root
|
||||
core.new_brick_file(tool.Brick, brick_root=root)
|
||||
|
||||
def rollback(self, data):
|
||||
@@ -230,7 +228,7 @@ class RemoveBrick(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Delete this entity"
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBrickProperties
|
||||
props = tool.Brick.get_brick_props()
|
||||
core.remove_brick(
|
||||
tool.Ifc,
|
||||
tool.Brick,
|
||||
@@ -239,18 +237,20 @@ class RemoveBrick(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
|
||||
|
||||
class SerializeBrick(bpy.types.Operator):
|
||||
class SerializeBrick(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.serialize_brick"
|
||||
bl_label = "Serialize Brick"
|
||||
# Prevents crash on Blender 4.4.0.
|
||||
bl_description = "Save active Brick project by the provided filepath."
|
||||
|
||||
filename_ext = ".ttl"
|
||||
filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
if self.should_save_as or not BrickStore.path:
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
return ExportHelper.invoke(self, context, event)
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
@@ -273,7 +273,7 @@ class AddBrickNamespace(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Bind a new namespace to the Brick project"
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBrickProperties
|
||||
props = tool.Brick.get_brick_props()
|
||||
alias = props.new_brick_namespace_alias
|
||||
uri = props.new_brick_namespace_uri
|
||||
core.add_brick_namespace(tool.Brick, alias=alias, uri=uri)
|
||||
@@ -288,6 +288,6 @@ class RemoveBrickRelation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
object: bpy.props.StringProperty(name="Object")
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBrickProperties
|
||||
props = tool.Brick.get_brick_props()
|
||||
brick = props.bricks[props.active_brick_index]
|
||||
core.remove_brick_relation(tool.Brick, brick_uri=brick.uri, predicate=self.predicate, object=self.object)
|
||||
|
||||
@@ -33,6 +33,7 @@ from bpy.props import (
|
||||
import bonsai.core.brick as core
|
||||
import bonsai.tool.brick as tool
|
||||
from bonsai.tool.brick import BrickStore
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
def update_active_brick_index(self, context):
|
||||
@@ -74,13 +75,13 @@ def get_brick_relations(self, context):
|
||||
return BRICK_RELATIONS_ENUM_ITEMS
|
||||
|
||||
|
||||
def update_view(self, context):
|
||||
root = context.scene.BIMBrickProperties.brick_list_root
|
||||
def update_view(self: "BIMBrickProperties", context: bpy.types.Context) -> None:
|
||||
root = self.brick_list_root
|
||||
core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=False)
|
||||
|
||||
|
||||
def split_screen_update_view(self, context):
|
||||
root = context.scene.BIMBrickProperties.split_screen_brick_list_root
|
||||
def split_screen_update_view(self: "BIMBrickProperties", context: bpy.types.Context) -> None:
|
||||
root = self.split_screen_brick_list_root
|
||||
core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=True)
|
||||
|
||||
|
||||
@@ -90,6 +91,11 @@ class Brick(PropertyGroup):
|
||||
uri: StringProperty(name="URI")
|
||||
total_items: IntProperty(name="Total Items")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
label: str
|
||||
uri: str
|
||||
total_items: int
|
||||
|
||||
|
||||
class BIMBrickProperties(PropertyGroup):
|
||||
active_brick_class: StringProperty(name="Active Brick Class")
|
||||
@@ -124,3 +130,28 @@ class BIMBrickProperties(PropertyGroup):
|
||||
split_screen_brick_list_root: EnumProperty(
|
||||
name="Split Screen Brick List Root", items=get_brick_roots, update=split_screen_update_view
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
active_brick_class: str
|
||||
brick_breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
bricks: bpy.types.bpy_prop_collection_idprop[Brick]
|
||||
active_brick_index: int
|
||||
libraries: str
|
||||
set_list_root_toggled: bool
|
||||
brick_list_root: str
|
||||
namespace: str
|
||||
new_brick_namespace_alias: str
|
||||
new_brick_namespace_uri: str
|
||||
new_brick_label: str
|
||||
brick_entity_create_type: str
|
||||
brick_entity_class: str
|
||||
brick_create_relations_toggled: bool
|
||||
brick_edit_relations_toggled: bool
|
||||
new_brick_relation_type: str
|
||||
new_brick_relation_object: str
|
||||
split_screen_toggled: bool
|
||||
split_screen_bricks: bpy.types.bpy_prop_collection_idprop[Brick]
|
||||
split_screen_active_brick_index: int
|
||||
split_screen_active_brick_class: str
|
||||
split_screen_brick_breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
split_screen_brick_list_root: str
|
||||
|
||||
@@ -45,7 +45,7 @@ class BIM_PT_brickschema_project_info(Panel):
|
||||
bl_parent_id = "BIM_PT_brickschema"
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMBrickProperties
|
||||
self.props = tool.Brick.get_brick_props()
|
||||
|
||||
if not BrickschemaData.data["is_loaded"]:
|
||||
row = self.layout.row(align=True)
|
||||
@@ -87,7 +87,7 @@ class BIM_PT_brickschema_namespaces(Panel):
|
||||
return BrickStore.graph != None
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMBrickProperties
|
||||
self.props = tool.Brick.get_brick_props()
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Active Namespace:")
|
||||
@@ -122,7 +122,7 @@ class BIM_PT_brickschema_create_entity(Panel):
|
||||
return BrickStore.graph != None
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMBrickProperties
|
||||
self.props = tool.Brick.get_brick_props()
|
||||
# TO DO: hide this if selected entity already has a reference, or something similar
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
@@ -156,7 +156,7 @@ class BIM_PT_brickschema_viewport(Panel):
|
||||
return BrickStore.graph != None
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMBrickProperties
|
||||
self.props = tool.Brick.get_brick_props()
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.column().alignment = "RIGHT"
|
||||
@@ -178,7 +178,14 @@ class BIM_PT_brickschema_viewport(Panel):
|
||||
|
||||
row = grid_left.row()
|
||||
BIM_UL_bricks.split_screen = False
|
||||
row.template_list("BIM_UL_bricks", "", self.props, "bricks", self.props, "active_brick_index")
|
||||
row.template_list(
|
||||
BIM_UL_bricks.__name__,
|
||||
BIM_UL_bricks.__name__ + "_bricks",
|
||||
self.props,
|
||||
"bricks",
|
||||
self.props,
|
||||
"active_brick_index",
|
||||
)
|
||||
|
||||
if self.props.split_screen_toggled:
|
||||
grid_right = grid.column(align=True)
|
||||
@@ -195,7 +202,12 @@ class BIM_PT_brickschema_viewport(Panel):
|
||||
row = grid_right.row()
|
||||
BIM_UL_bricks.split_screen = True
|
||||
row.template_list(
|
||||
"BIM_UL_bricks", "", self.props, "split_screen_bricks", self.props, "split_screen_active_brick_index"
|
||||
BIM_UL_bricks.__name__,
|
||||
BIM_UL_bricks.__name__ + "_split_screen_bricks",
|
||||
self.props,
|
||||
"split_screen_bricks",
|
||||
self.props,
|
||||
"split_screen_active_brick_index",
|
||||
)
|
||||
|
||||
if BrickschemaData.data["active_relations"]:
|
||||
@@ -292,7 +304,7 @@ class BIM_PT_ifc_brickschema_references(Panel):
|
||||
def draw(self, context):
|
||||
if not BrickschemaReferencesData.is_loaded:
|
||||
BrickschemaReferencesData.load()
|
||||
self.props = context.scene.BIMBrickProperties
|
||||
self.props = tool.Brick.get_brick_props()
|
||||
|
||||
if not BrickschemaReferencesData.data["is_loaded"]:
|
||||
row = self.layout.row()
|
||||
|
||||
@@ -128,9 +128,10 @@ class CadMitre(bpy.types.Operator):
|
||||
class CadFillet(bpy.types.Operator):
|
||||
bl_idname = "bim.cad_fillet"
|
||||
bl_label = "CAD Fillet"
|
||||
bl_description = "Add fillet to the 2 selected edges."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
resolution: bpy.props.IntProperty(name="Arc Resolution", min=0, default=1)
|
||||
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
|
||||
radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -143,18 +144,15 @@ class CadFillet(bpy.types.Operator):
|
||||
layout.prop(self, prop)
|
||||
|
||||
def execute(self, context):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
self.radius = self.radius * si_conversion
|
||||
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
obj = bpy.context.active_object
|
||||
cursor = obj.matrix_world.inverted() @ bpy.context.scene.cursor.location
|
||||
mesh = obj.data
|
||||
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
|
||||
bm = bmesh.from_edit_mesh(mesh)
|
||||
selected_edges = [e for e in bm.edges if e.select]
|
||||
if len(selected_edges) != 2:
|
||||
self.report({"ERROR"}, "Exactly 2 edges should be selected.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Assume the user has selected two edges sharing a vert, but merge verts
|
||||
@@ -219,6 +217,7 @@ class CadFillet(bpy.types.Operator):
|
||||
class CadArcFrom2Points(bpy.types.Operator):
|
||||
bl_idname = "bim.cad_arc_from_2_points"
|
||||
bl_label = "CAD Arc from 2 Points"
|
||||
bl_description = "Add an arc to the active mesh based on 2 selected vertices."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
|
||||
should_flip: bpy.props.BoolProperty(name="Flip", description="Flip arc", default=False)
|
||||
@@ -235,6 +234,7 @@ class CadArcFrom2Points(bpy.types.Operator):
|
||||
if bpy.context.mode != "EDIT_MESH":
|
||||
return {"CANCELLED"}
|
||||
obj = bpy.context.active_object
|
||||
assert obj
|
||||
region = bpy.context.region
|
||||
region_3d = bpy.context.area.spaces.active.region_3d
|
||||
cursor = bpy.context.scene.cursor.location
|
||||
@@ -242,10 +242,12 @@ class CadArcFrom2Points(bpy.types.Operator):
|
||||
if not center:
|
||||
return {"CANCELLED"}
|
||||
mesh = obj.data
|
||||
assert isinstance(mesh, bpy.types.Mesh)
|
||||
mw = obj.matrix_world
|
||||
bm = bmesh.from_edit_mesh(mesh)
|
||||
selected_verts = [v for v in bm.verts if v.select]
|
||||
if len(selected_verts) != 2:
|
||||
self.report({"ERROR"}, "Exactly 2 vertices should be selected.")
|
||||
return {"CANCELLED"}
|
||||
v1 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[0].co)
|
||||
v2 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[1].co)
|
||||
@@ -342,11 +344,12 @@ class CadArcFrom3Points(bpy.types.Operator):
|
||||
class CadOffset(bpy.types.Operator):
|
||||
bl_idname = "bim.cad_offset"
|
||||
bl_label = "CAD Offset"
|
||||
bl_description = "Copy selected mesh geometry at provided offset. Mesh copied based on the current viewport angle."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
|
||||
distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE")
|
||||
|
||||
@classmethod
|
||||
def poll(self, context):
|
||||
def poll(cls, context):
|
||||
return context.mode == "EDIT_MESH"
|
||||
|
||||
def draw(self, context):
|
||||
@@ -369,9 +372,6 @@ class CadOffset(bpy.types.Operator):
|
||||
# effect of converting an unclosed loop into a closed loop which is also
|
||||
# not what users expect.
|
||||
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
self.distance = self.distance * si_conversion
|
||||
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
@@ -543,7 +543,8 @@ class CadOffset(bpy.types.Operator):
|
||||
class AddIfcCircle(bpy.types.Operator):
|
||||
bl_idname = "bim.add_ifccircle"
|
||||
bl_label = "Add IfcCircle"
|
||||
radius: bpy.props.FloatProperty(name="Radius", default=0.5)
|
||||
bl_description = "Add IfcCircle to the currently active mesh."
|
||||
radius: bpy.props.FloatProperty(name="Radius", default=0.5, subtype="DISTANCE")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -551,18 +552,19 @@ class AddIfcCircle(bpy.types.Operator):
|
||||
return bool(obj) and obj.type == "MESH"
|
||||
|
||||
def execute(self, context):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
self.radius = self.radius * si_conversion
|
||||
|
||||
obj = context.active_object
|
||||
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
|
||||
self.obj = obj
|
||||
self.mesh = mesh
|
||||
if self.has_selected_existing_circle(context):
|
||||
self.change_radius(context)
|
||||
else:
|
||||
self.create_circle(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
def has_selected_existing_circle(self, context):
|
||||
obj = context.active_object
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
def has_selected_existing_circle(self, context: bpy.types.Context) -> bool:
|
||||
obj = self.obj
|
||||
bm = bmesh.from_edit_mesh(self.mesh)
|
||||
verts = [v for v in bm.verts if v.select and not v.hide]
|
||||
if len(verts) != 2:
|
||||
return False
|
||||
@@ -575,32 +577,32 @@ class AddIfcCircle(bpy.types.Operator):
|
||||
for group in groups:
|
||||
if group in verts[0][deform_layer] and group in verts[1][deform_layer]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def change_radius(self, context):
|
||||
obj = context.active_object
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
def change_radius(self, context: bpy.types.Object) -> None:
|
||||
bm = bmesh.from_edit_mesh(self.mesh)
|
||||
verts = [v for v in bm.verts if v.select and not v.hide]
|
||||
center = verts[0].co.lerp(verts[1].co, 0.5)
|
||||
verts[0].co = center + ((verts[0].co - center).normalized() * self.radius)
|
||||
verts[1].co = center + ((verts[1].co - center).normalized() * self.radius)
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
bmesh.update_edit_mesh(self.mesh)
|
||||
|
||||
def create_circle(self, context):
|
||||
obj = context.active_object
|
||||
def create_circle(self, context: bpy.types.Context) -> None:
|
||||
obj = self.obj
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
# The last group may be the result of a prior run of this operator.
|
||||
# I tried looping through all groups but Blender group indices seem to behave unpredictably.
|
||||
if len(obj.vertex_groups):
|
||||
last_group = obj.vertex_groups[-1]
|
||||
verts_in_group = [v for v in obj.data.vertices if last_group.index in [vg.group for vg in v.groups]]
|
||||
verts_in_group = [v for v in self.mesh.vertices if last_group.index in [vg.group for vg in v.groups]]
|
||||
if "IFCCIRCLE" in last_group.name and len(verts_in_group) != 2:
|
||||
obj.vertex_groups.remove(last_group)
|
||||
group = obj.vertex_groups.new(name="IFCCIRCLE")
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
bm = bmesh.from_edit_mesh(self.mesh)
|
||||
|
||||
bm.verts.layers.deform.verify()
|
||||
deform_layer = bm.verts.layers.deform.active
|
||||
@@ -617,7 +619,7 @@ class AddIfcCircle(bpy.types.Operator):
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
bmesh.update_edit_mesh(self.mesh)
|
||||
|
||||
|
||||
class AddIfcArcIndexFillet(bpy.types.Operator):
|
||||
@@ -638,18 +640,19 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
|
||||
layout.prop(self, prop)
|
||||
|
||||
def execute(self, context):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
self.radius = self.radius * si_conversion
|
||||
|
||||
obj = context.active_object
|
||||
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
|
||||
self.obj = obj
|
||||
self.mesh = mesh
|
||||
if self.has_selected_existing_arc(context):
|
||||
self.change_radius(context)
|
||||
else:
|
||||
self.create_arc(context)
|
||||
return self.create_arc(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
def has_selected_existing_arc(self, context: bpy.types.Context) -> bool:
|
||||
obj = context.active_object
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
obj = self.obj
|
||||
bm = bmesh.from_edit_mesh(self.mesh)
|
||||
verts = [v for v in bm.verts if v.select and not v.hide]
|
||||
if len(verts) != 3:
|
||||
return False
|
||||
@@ -668,13 +671,14 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
|
||||
return False
|
||||
|
||||
def change_radius(self, context: bpy.types.Context) -> None:
|
||||
obj = context.active_object
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
obj = self.obj
|
||||
bm = bmesh.from_edit_mesh(self.mesh)
|
||||
edges = [e for e in bm.edges if e.select and not e.hide]
|
||||
|
||||
mid = list(set(edges[0].verts) & set(edges[1].verts))[0]
|
||||
v1 = edges[0].other_vert(mid)
|
||||
v2 = edges[1].other_vert(mid)
|
||||
assert v1 and v2
|
||||
center = tool.Cad.get_center_of_arc([v1.co, mid.co, v2.co])
|
||||
|
||||
if len(v1.link_edges) != 2 or len(v2.link_edges) != 2:
|
||||
@@ -682,6 +686,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
|
||||
|
||||
v3 = v1.link_edges[1].other_vert(v1) if mid in v1.link_edges[0].verts else v1.link_edges[0].other_vert(v1)
|
||||
v4 = v2.link_edges[1].other_vert(v2) if mid in v2.link_edges[0].verts else v2.link_edges[0].other_vert(v2)
|
||||
assert v3 and v4
|
||||
dir1 = (v3.co - v1.co).normalized()
|
||||
dir2 = (v4.co - v2.co).normalized()
|
||||
|
||||
@@ -702,11 +707,10 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
|
||||
def create_arc(self, context):
|
||||
obj = bpy.context.active_object
|
||||
bmesh.update_edit_mesh(self.mesh)
|
||||
|
||||
def create_arc(self, context: bpy.types.Context) -> set[str]:
|
||||
obj = self.obj
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
# The last group may be the result of a prior run of this operator.
|
||||
# I tried looping through all groups but Blender group indices seem to behave unpredictably.
|
||||
@@ -718,7 +722,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
|
||||
group = obj.vertex_groups.new(name="IFCARCINDEX")
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
bm = bmesh.from_edit_mesh(self.mesh)
|
||||
|
||||
bm.verts.layers.deform.verify()
|
||||
deform_layer = bm.verts.layers.deform.active
|
||||
@@ -737,6 +741,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
|
||||
shared_vert = list(set(selected_edges[0].verts) & set(selected_edges[1].verts))[0]
|
||||
v1 = selected_edges[0].other_vert(shared_vert)
|
||||
v2 = selected_edges[1].other_vert(shared_vert)
|
||||
assert v1 and v2
|
||||
dir1 = (v1.co - shared_vert.co).normalized()
|
||||
dir2 = (v2.co - shared_vert.co).normalized()
|
||||
edge_angle = dir1.angle(dir2)
|
||||
@@ -770,7 +775,8 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
bmesh.update_edit_mesh(self.mesh)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AlignViewToProfile(bpy.types.Operator):
|
||||
@@ -806,8 +812,9 @@ class AlignViewToProfile(bpy.types.Operator):
|
||||
class AddRectangle(bpy.types.Operator):
|
||||
bl_idname = "bim.add_rectangle"
|
||||
bl_label = "Add Rectangle"
|
||||
x: bpy.props.FloatProperty(name="X", default=1)
|
||||
y: bpy.props.FloatProperty(name="Y", default=1)
|
||||
bl_description = "Add rectangle shape to the active mesh."
|
||||
x: bpy.props.FloatProperty(name="X", default=1, subtype="DISTANCE")
|
||||
y: bpy.props.FloatProperty(name="Y", default=1, subtype="DISTANCE")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -815,11 +822,8 @@ class AddRectangle(bpy.types.Operator):
|
||||
return bool(obj) and obj.type == "MESH"
|
||||
|
||||
def execute(self, context):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
self.x = self.x * si_conversion
|
||||
self.y = self.y * si_conversion
|
||||
|
||||
obj = context.active_object
|
||||
assert obj and isinstance(obj.data, bpy.types.Mesh)
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
cursor = obj.matrix_world.inverted() @ context.scene.cursor.location
|
||||
new_verts = [
|
||||
|
||||
@@ -20,6 +20,7 @@ import bpy
|
||||
from bonsai.bim.module.model.data import AuthoringData
|
||||
from bpy.types import PropertyGroup
|
||||
from math import pi
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
class BIMCadProperties(PropertyGroup):
|
||||
@@ -31,3 +32,11 @@ class BIMCadProperties(PropertyGroup):
|
||||
gable_roof_edge_angle: bpy.props.FloatProperty(
|
||||
name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE"
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
resolution: int
|
||||
radius: float
|
||||
distance: float
|
||||
x: float
|
||||
y: float
|
||||
gable_roof_edge_angle: float
|
||||
|
||||
@@ -23,7 +23,7 @@ import bonsai.bim.module.type.prop as type_prop
|
||||
import ifcopenshell.util.unit
|
||||
from bpy.types import WorkSpaceTool
|
||||
from bonsai.bim.module.model.data import AuthoringData, RailingData, RoofData
|
||||
from typing import Union
|
||||
from functools import partial
|
||||
|
||||
|
||||
def load_custom_icons():
|
||||
@@ -114,11 +114,11 @@ class CadTool(WorkSpaceTool):
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
|
||||
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "Rectangle", "S_R", "Rectangle", ui_context)
|
||||
add_layout_hotkey_operator(row, "Rectangle", "S_R", bpy.ops.bim.add_rectangle.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "Circle", "S_C", "Circle", ui_context)
|
||||
add_layout_hotkey_operator(row, "Circle", "S_C", bpy.ops.bim.add_ifccircle.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.set_arc_index.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
@@ -138,15 +138,15 @@ class CadTool(WorkSpaceTool):
|
||||
row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context
|
||||
)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "Fillet", "S_F", "Fillet", ui_context)
|
||||
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.cad_fillet.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
|
||||
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
|
||||
|
||||
else:
|
||||
if (
|
||||
(RailingData.is_loaded or not RailingData.load())
|
||||
and RailingData.data["pset_data"]
|
||||
and obj.BIMRailingProperties.is_editing_path
|
||||
and tool.Model.get_railing_props(obj).is_editing_path
|
||||
):
|
||||
add_header_apply_button(
|
||||
layout,
|
||||
@@ -159,7 +159,7 @@ class CadTool(WorkSpaceTool):
|
||||
elif (
|
||||
(RoofData.is_loaded or not RoofData.load())
|
||||
and RoofData.data["pset_data"]
|
||||
and obj.BIMRoofProperties.is_editing_path
|
||||
and tool.Model.get_roof_props(obj).is_editing_path
|
||||
):
|
||||
add_header_apply_button(
|
||||
layout, "Edit Roof Path", "bim.finish_editing_roof_path", "bim.cancel_editing_roof_path", ui_context
|
||||
@@ -176,17 +176,17 @@ class CadTool(WorkSpaceTool):
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
|
||||
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "2-Point Arc", "S_C", "2-Point Arc", ui_context)
|
||||
add_layout_hotkey_operator(row, "2-Point Arc", "S_C", bpy.ops.bim.cad_arc_from_2_points.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.cad_arc_from_3_points.__doc__, ui_context)
|
||||
|
||||
|
||||
class CadHotkey(bpy.types.Operator):
|
||||
bl_idname = "bim.cad_hotkey"
|
||||
bl_label = "CAD Hotkey"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_label = ""
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
hotkey: bpy.props.StringProperty()
|
||||
description: bpy.props.StringProperty()
|
||||
|
||||
@@ -195,12 +195,14 @@ class CadHotkey(bpy.types.Operator):
|
||||
return operator.description or ""
|
||||
|
||||
def execute(self, context):
|
||||
self.props = context.scene.BIMCadProperties
|
||||
self.props = tool.Cad.get_cad_props()
|
||||
getattr(self, f"hotkey_{self.hotkey}")()
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
props = context.scene.BIMCadProperties
|
||||
props = tool.Cad.get_cad_props()
|
||||
obj = context.active_object
|
||||
|
||||
if self.hotkey == "S_C":
|
||||
if tool.Geometry.is_profile_object_active():
|
||||
row = self.layout.row()
|
||||
@@ -226,7 +228,7 @@ class CadHotkey(bpy.types.Operator):
|
||||
elif (
|
||||
(RoofData.is_loaded or not RoofData.load())
|
||||
and RoofData.data["pset_data"]
|
||||
and bpy.context.active_object.BIMRoofProperties.is_editing_path
|
||||
and tool.Model.get_roof_props(obj).is_editing_path
|
||||
):
|
||||
self.layout.row().prop(props, "gable_roof_edge_angle")
|
||||
|
||||
@@ -236,9 +238,8 @@ class CadHotkey(bpy.types.Operator):
|
||||
row.prop(props, "resolution")
|
||||
|
||||
def hotkey_S_C(self):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
if tool.Geometry.is_profile_object_active():
|
||||
bpy.ops.bim.add_ifccircle(radius=self.props.radius / si_conversion)
|
||||
bpy.ops.bim.add_ifccircle(radius=self.props.radius)
|
||||
else:
|
||||
bpy.ops.bim.cad_arc_from_2_points()
|
||||
|
||||
@@ -246,15 +247,13 @@ class CadHotkey(bpy.types.Operator):
|
||||
bpy.ops.bim.cad_trim_extend()
|
||||
|
||||
def hotkey_S_F(self):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
if tool.Geometry.is_profile_object_active():
|
||||
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius / si_conversion)
|
||||
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius)
|
||||
else:
|
||||
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius / si_conversion)
|
||||
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius)
|
||||
|
||||
def hotkey_S_O(self):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
bpy.ops.bim.cad_offset(distance=self.props.distance / si_conversion)
|
||||
bpy.ops.bim.cad_offset(distance=self.props.distance)
|
||||
|
||||
def hotkey_S_Q(self):
|
||||
obj = bpy.context.active_object
|
||||
@@ -281,13 +280,16 @@ class CadHotkey(bpy.types.Operator):
|
||||
bpy.ops.bim.edit_extrusion_axis()
|
||||
|
||||
def hotkey_S_R(self):
|
||||
obj = bpy.context.active_object
|
||||
if not obj:
|
||||
return
|
||||
|
||||
if tool.Geometry.is_profile_object_active():
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
bpy.ops.bim.add_rectangle(x=self.props.x / si_conversion, y=self.props.y / si_conversion)
|
||||
bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y)
|
||||
elif (
|
||||
(RoofData.is_loaded or not RoofData.load())
|
||||
and RoofData.data["pset_data"]
|
||||
and bpy.context.active_object.BIMRoofProperties.is_editing_path
|
||||
and tool.Model.get_roof_props(obj).is_editing_path
|
||||
):
|
||||
bpy.ops.bim.set_gable_roof_edge_angle(angle=self.props.gable_roof_edge_angle)
|
||||
|
||||
@@ -305,7 +307,9 @@ class CadHotkey(bpy.types.Operator):
|
||||
bpy.ops.bim.reset_vertex()
|
||||
|
||||
|
||||
def add_header_apply_button(layout, text, apply_operator, cancel_operator, ui_context=""):
|
||||
def add_header_apply_button(
|
||||
layout: bpy.types.UILayout, text: str, apply_operator: str, cancel_operator: str, ui_context: str = ""
|
||||
) -> None:
|
||||
custom_icon = custom_icon_previews.get(text.upper().replace(" ", "_"), custom_icon_previews["IFC"]).icon_id
|
||||
row = layout.row(align=True)
|
||||
row.label(text=f"{text} Mode", icon_value=custom_icon)
|
||||
@@ -324,34 +328,8 @@ def add_header_apply_button(layout, text, apply_operator, cancel_operator, ui_co
|
||||
row.label(text="Tools")
|
||||
|
||||
|
||||
def add_layout_hotkey_operator(
|
||||
layout: bpy.types.UILayout, text: str, hotkey: str, description: Union[str, None], ui_context: str = ""
|
||||
) -> bpy.types.OperatorProperties:
|
||||
parts = hotkey.split("_")
|
||||
modifier, key = parts
|
||||
op_text = "" if ui_context == "TOOL_HEADER" else text
|
||||
custom_icon = custom_icon_previews.get(text.upper().replace(" ", "_"), custom_icon_previews["IFC"]).icon_id
|
||||
modifier_icon, modifier_str = MODIFIERS.get(modifier, ("NONE", ""))
|
||||
|
||||
row = layout if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
op = row.operator("bim.cad_hotkey", text=op_text, icon_value=custom_icon)
|
||||
|
||||
if ui_context != "TOOL_HEADER" and len(parts) == 2:
|
||||
layout = layout.row(align=True) # Create a new line for hotkey display
|
||||
layout.label(text="", icon=modifier_icon)
|
||||
layout.label(text="", icon=f"EVENT_{key}")
|
||||
|
||||
hotkey_description = f"Hotkey: {modifier_str} {key}"
|
||||
description = "\n\n".join(filter(None, [hotkey_description if description else ""]))
|
||||
op.hotkey = hotkey
|
||||
op.description = description or hotkey_description
|
||||
|
||||
return op
|
||||
add_layout_hotkey_operator = partial(tool.Blender.add_layout_hotkey_operator, tool_name="cad", module_name=__name__)
|
||||
|
||||
|
||||
custom_icon_previews = None
|
||||
display_mode = None
|
||||
|
||||
MODIFIERS = {
|
||||
"S": ("EVENT_SHIFT", "⇧"),
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -60,8 +62,9 @@ class ClashDecorator:
|
||||
unselected_elements_color = self.addon_prefs.decorator_color_unselected
|
||||
special_elements_color = self.addon_prefs.decorator_color_special
|
||||
|
||||
text = context.scene.BIMClashProperties.active_clash_text
|
||||
p = context.scene.BIMClashProperties.p1.lerp(context.scene.BIMClashProperties.p2, 0.5)
|
||||
props = tool.Clash.get_clash_props()
|
||||
text = props.active_clash_text
|
||||
p = props.p1.lerp(props.p2, 0.5)
|
||||
|
||||
font_id = 0
|
||||
blf.size(font_id, 12)
|
||||
@@ -92,7 +95,8 @@ class ClashDecorator:
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
selected_vertices = [context.scene.BIMClashProperties.p1, context.scene.BIMClashProperties.p2]
|
||||
props = tool.Clash.get_clash_props()
|
||||
selected_vertices = [props.p1, props.p2]
|
||||
selected_edges = []
|
||||
if selected_vertices[0] != selected_vertices[1]:
|
||||
selected_edges = [[0, 1]]
|
||||
|
||||
@@ -24,26 +24,20 @@ import logging
|
||||
import numpy as np
|
||||
import ifcopenshell
|
||||
import bonsai.tool as tool
|
||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||
from math import radians
|
||||
from mathutils import Matrix, Vector
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.module.clash.decorator import ClashDecorator
|
||||
|
||||
|
||||
class ExportClashSets(bpy.types.Operator):
|
||||
class ExportClashSets(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.export_clash_sets"
|
||||
bl_label = "Export Clash Sets"
|
||||
bl_description = "Export clash sets to a selected file"
|
||||
filename_ext = ".json"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
|
||||
clash_sets = tool.Clash.export_clash_sets()
|
||||
@@ -52,26 +46,24 @@ class ExportClashSets(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ImportClashSets(bpy.types.Operator):
|
||||
class ImportClashSets(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.import_clash_sets"
|
||||
bl_label = "Import Clash Sets"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Import clash sets from a selected file"
|
||||
filename_ext = ".json"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
return ImportHelper.invoke(self, context, event)
|
||||
|
||||
def execute(self, context):
|
||||
tool.Clash.load_clash_sets(self.filepath)
|
||||
context.scene.BIMClashProperties.clash_sets.clear()
|
||||
props = tool.Clash.get_clash_props()
|
||||
props.clash_sets.clear()
|
||||
for clash_set in tool.Clash.get_clash_sets():
|
||||
new = context.scene.BIMClashProperties.clash_sets.add()
|
||||
new = props.clash_sets.add()
|
||||
new.name = clash_set["name"]
|
||||
new.mode = clash_set["mode"]
|
||||
if new.mode == "intersection":
|
||||
@@ -106,7 +98,8 @@ class AddClashSet(bpy.types.Operator):
|
||||
bl_description = "Add a clash set"
|
||||
|
||||
def execute(self, context):
|
||||
new = context.scene.BIMClashProperties.clash_sets.add()
|
||||
props = tool.Clash.get_clash_props()
|
||||
new = props.clash_sets.add()
|
||||
new.name = "New Clash Set"
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -119,7 +112,8 @@ class RemoveClashSet(bpy.types.Operator):
|
||||
index: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.BIMClashProperties.clash_sets.remove(self.index)
|
||||
props = tool.Clash.get_clash_props()
|
||||
props.clash_sets.remove(self.index)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -131,7 +125,8 @@ class AddClashSource(bpy.types.Operator):
|
||||
group: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
clash_set = context.scene.BIMClashProperties.active_clash_set
|
||||
props = tool.Clash.get_clash_props()
|
||||
clash_set = props.active_clash_set
|
||||
source = getattr(clash_set, self.group).add()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -145,78 +140,74 @@ class RemoveClashSource(bpy.types.Operator):
|
||||
group: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
clash_set = context.scene.BIMClashProperties.active_clash_set
|
||||
props = tool.Clash.get_clash_props()
|
||||
clash_set = props.active_clash_set
|
||||
getattr(clash_set, self.group).remove(self.index)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectClashSource(bpy.types.Operator):
|
||||
class SelectClashSource(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_clash_source"
|
||||
bl_label = "Select Clash Source"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Select an IFC file to add as a clash source"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
|
||||
index: bpy.props.IntProperty()
|
||||
group: bpy.props.StringProperty()
|
||||
filename_ext = ".ifc"
|
||||
|
||||
def execute(self, context):
|
||||
clash_set = context.scene.BIMClashProperties.active_clash_set
|
||||
props = tool.Clash.get_clash_props()
|
||||
clash_set = props.active_clash_set
|
||||
getattr(clash_set, self.group)[self.index].name = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class SelectClashResults(bpy.types.Operator):
|
||||
class SelectClashResults(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_clash_results"
|
||||
bl_label = "Select Clash Results"
|
||||
bl_description = "Select filepath for clash results."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.BIMClashProperties.clash_results_path = self.filepath
|
||||
props = tool.Clash.get_clash_props()
|
||||
props.clash_results_path = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class SelectSmartGroupedClashesPath(bpy.types.Operator):
|
||||
class SelectSmartGroupedClashesPath(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_smart_grouped_clashes_path"
|
||||
bl_label = "Select Smart-Grouped Clashes Path"
|
||||
bl_description = "Select filepath for smart-grouped clashes."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.BIMClashProperties.smart_grouped_clashes_path = self.filepath
|
||||
props = tool.Clash.get_clash_props()
|
||||
props.smart_grouped_clashes_path = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class ExecuteIfcClash(bpy.types.Operator):
|
||||
class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.execute_ifc_clash"
|
||||
bl_label = "Execute IFC Clash"
|
||||
bl_description = "Execute clash detection and save the information to a .bcf or .json file"
|
||||
filter_glob: bpy.props.StringProperty(default="*.bcf;*.json", options={"HIDDEN"})
|
||||
format: bpy.props.EnumProperty(name="Format", items=[(i, i, "") for i in ("bcf", "json")])
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
|
||||
|
||||
@property
|
||||
def filename_ext(self) -> str:
|
||||
return f".{self.format.lower()}"
|
||||
|
||||
def invoke(self, context, event):
|
||||
if self.filepath:
|
||||
return self.execute(context)
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
return ExportHelper.invoke(self, context, event)
|
||||
|
||||
def execute(self, context):
|
||||
from ifcclash import ifcclash
|
||||
|
||||
self.props = context.scene.BIMClashProperties
|
||||
self.props = tool.Clash.get_clash_props()
|
||||
|
||||
_, extension = os.path.splitext(self.filepath)
|
||||
if extension != ".bcf":
|
||||
@@ -284,19 +275,17 @@ class ExecuteIfcClash(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectIfcClashResults(bpy.types.Operator):
|
||||
class SelectIfcClashResults(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_ifc_clash_results"
|
||||
bl_label = "Select IFC Clash Results"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Select the clashing IFC geometry stored in a file"
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
filename_ext = ".json"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
return ImportHelper.invoke(self, context, event)
|
||||
|
||||
def execute(self, context):
|
||||
# TODO refactor into new clash results system
|
||||
@@ -304,7 +293,9 @@ class SelectIfcClashResults(bpy.types.Operator):
|
||||
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
|
||||
with open(self.filepath) as f:
|
||||
clash_sets = json.load(f)
|
||||
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
|
||||
clash_props = tool.Clash.get_clash_props()
|
||||
assert clash_props.active_clash_set
|
||||
clash_set_name = clash_props.active_clash_set.name
|
||||
global_ids = []
|
||||
for clash_set in clash_sets:
|
||||
if clash_set["name"] != clash_set_name:
|
||||
@@ -316,7 +307,8 @@ class SelectIfcClashResults(bpy.types.Operator):
|
||||
global_ids.extend([clash["a_global_id"], clash["b_global_id"]])
|
||||
|
||||
for obj in context.visible_objects:
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
if not props.ifc_definition_id:
|
||||
continue
|
||||
|
||||
ifc_file = ""
|
||||
@@ -335,7 +327,7 @@ class SelectIfcClashResults(bpy.types.Operator):
|
||||
element_file = self.file
|
||||
|
||||
try:
|
||||
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
element = element_file.by_id(props.ifc_definition_id)
|
||||
except:
|
||||
continue
|
||||
|
||||
@@ -355,7 +347,7 @@ class SelectClash(bpy.types.Operator):
|
||||
index: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
self.props = context.scene.BIMClashProperties
|
||||
self.props = tool.Clash.get_clash_props()
|
||||
clash_set = tool.Clash.get_clash_set(self.props.active_clash_set.name)
|
||||
active_clash = self.props.active_clash
|
||||
clash = tool.Clash.get_clash(clash_set, active_clash.a_global_id, active_clash.b_global_id)
|
||||
@@ -389,13 +381,15 @@ class SmartClashGroup(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.scene.BIMClashProperties.clash_results_path
|
||||
props = tool.Clash.get_clash_props()
|
||||
return bool(props.clash_results_path)
|
||||
|
||||
def execute(self, context):
|
||||
from ifcclash import ifcclash
|
||||
|
||||
settings = ifcclash.ClashSettings()
|
||||
self.filepath = bpy.path.ensure_ext(context.scene.BIMClashProperties.clash_results_path, ".json")
|
||||
props = tool.Clash.get_clash_props()
|
||||
self.filepath = bpy.path.ensure_ext(props.clash_results_path, ".json")
|
||||
settings.output = self.filepath
|
||||
settings.logger = logging.getLogger("Clash")
|
||||
settings.logger.setLevel(logging.DEBUG)
|
||||
@@ -405,19 +399,18 @@ class SmartClashGroup(bpy.types.Operator):
|
||||
clash_sets = json.load(f)
|
||||
|
||||
# execute the smart grouping
|
||||
save_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json")
|
||||
smart_grouped_clashes = ifc_clasher.smart_group_clashes(
|
||||
clash_sets, context.scene.BIMClashProperties.smart_clash_grouping_max_distance
|
||||
)
|
||||
save_path = bpy.path.ensure_ext(props.smart_grouped_clashes_path, ".json")
|
||||
smart_grouped_clashes = ifc_clasher.smart_group_clashes(clash_sets, props.smart_clash_grouping_max_distance)
|
||||
|
||||
# save smart_groups to json
|
||||
with open(save_path, "w") as f:
|
||||
f.write(json.dumps(smart_grouped_clashes))
|
||||
|
||||
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
|
||||
assert props.active_clash_set
|
||||
clash_set_name = props.active_clash_set.name
|
||||
|
||||
# Reset the list of smart_clash_groups for the UI
|
||||
context.scene.BIMClashProperties.smart_clash_groups.clear()
|
||||
props.smart_clash_groups.clear()
|
||||
|
||||
for clash_set, smart_groups in smart_grouped_clashes.items():
|
||||
# Only select the clashes that correspond to the actively selected IFC Clash Set
|
||||
@@ -425,7 +418,7 @@ class SmartClashGroup(bpy.types.Operator):
|
||||
continue
|
||||
else:
|
||||
for smart_group, global_id_pairs in smart_groups[0].items():
|
||||
new_group = context.scene.BIMClashProperties.smart_clash_groups.add()
|
||||
new_group = props.smart_clash_groups.add()
|
||||
new_group.number = f"{smart_group}"
|
||||
|
||||
for pair in global_id_pairs:
|
||||
@@ -443,18 +436,21 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.scene.BIMClashProperties.active_clash_set
|
||||
props = tool.Clash.get_clash_props()
|
||||
return bool(props.active_clash_set)
|
||||
|
||||
def execute(self, context):
|
||||
smart_groups_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json")
|
||||
props = tool.Clash.get_clash_props()
|
||||
smart_groups_path = bpy.path.ensure_ext(props.smart_grouped_clashes_path, ".json")
|
||||
|
||||
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
|
||||
assert props.active_clash_set
|
||||
clash_set_name = props.active_clash_set.name
|
||||
|
||||
with open(smart_groups_path) as f:
|
||||
smart_grouped_clashes = json.load(f)
|
||||
|
||||
# Reset the list of smart_clash_groups for the UI
|
||||
context.scene.BIMClashProperties.smart_clash_groups.clear()
|
||||
props.smart_clash_groups.clear()
|
||||
|
||||
for clash_set, smart_groups in smart_grouped_clashes.items():
|
||||
# Only select the clashes that correspond to the actively selected IFC Clash Set
|
||||
@@ -462,12 +458,12 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator):
|
||||
continue
|
||||
else:
|
||||
for smart_group, global_id_pairs in smart_groups[0].items():
|
||||
new_group = context.scene.BIMClashProperties.smart_clash_groups.add()
|
||||
new_group = props.smart_clash_groups.add()
|
||||
new_group.number = f"{smart_group}"
|
||||
for pair in global_id_pairs:
|
||||
for guid in pair:
|
||||
new_global_id = new_group.global_ids.add()
|
||||
new_global_id.guid = guid
|
||||
new_global_id.name = guid
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -479,15 +475,19 @@ class SelectSmartGroup(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Ifc.get() and context.visible_objects and context.scene.BIMClashProperties.active_smart_group
|
||||
props = tool.Clash.get_clash_props()
|
||||
return tool.Ifc.get() and context.visible_objects and props.active_smart_group
|
||||
|
||||
def execute(self, context):
|
||||
selected_smart_group = context.scene.BIMClashProperties.active_smart_group
|
||||
products = []
|
||||
ifc_file = tool.Ifc.get()
|
||||
props = tool.Clash.get_clash_props()
|
||||
selected_smart_group = props.active_smart_group
|
||||
assert selected_smart_group
|
||||
products: list[ifcopenshell.entity_instance] = []
|
||||
for global_id in selected_smart_group.global_ids:
|
||||
try:
|
||||
products.append(tool.Ifc.get().by_guid(global_id.guid))
|
||||
except:
|
||||
products.append(ifc_file.by_guid(global_id.name))
|
||||
except RuntimeError:
|
||||
continue
|
||||
tool.Spatial.select_products(products, unhide=True)
|
||||
context_override = tool.Blender.get_viewport_context()
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.prop import StrProperty, Attribute, BIMFilterGroup
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
@@ -29,6 +30,8 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from mathutils import Vector
|
||||
from typing import TYPE_CHECKING, Literal, Union
|
||||
|
||||
|
||||
class ClashSource(PropertyGroup):
|
||||
@@ -43,6 +46,10 @@ class ClashSource(PropertyGroup):
|
||||
name="Mode",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
|
||||
mode: Literal["a", "i", "e"]
|
||||
|
||||
|
||||
class Clash(PropertyGroup):
|
||||
a_global_id: StringProperty(name="A")
|
||||
@@ -51,9 +58,15 @@ class Clash(PropertyGroup):
|
||||
b_name: StringProperty(name="B Name")
|
||||
status: BoolProperty(name="Status", default=False)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
a_global_id: str
|
||||
b_global_id: str
|
||||
a_name: str
|
||||
b_name: str
|
||||
status: bool
|
||||
|
||||
|
||||
class ClashSet(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
mode: EnumProperty(
|
||||
items=[
|
||||
(
|
||||
@@ -76,11 +89,25 @@ class ClashSet(PropertyGroup):
|
||||
b: CollectionProperty(name="Group B", type=ClashSource)
|
||||
clashes: CollectionProperty(name="Clashes", type=Clash)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
mode: Literal["intersection", "collision", "clearance"]
|
||||
tolerance: float
|
||||
clearance: float
|
||||
allow_touching: bool
|
||||
check_all: bool
|
||||
a: bpy.types.bpy_prop_collection_idprop[ClashSource]
|
||||
b: bpy.types.bpy_prop_collection_idprop[ClashSource]
|
||||
clashes: bpy.types.bpy_prop_collection_idprop[Clash]
|
||||
|
||||
|
||||
class SmartClashGroup(PropertyGroup):
|
||||
number: StringProperty(name="Number")
|
||||
global_ids: CollectionProperty(name="GlobalIDs", type=StrProperty)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
number: str
|
||||
global_ids: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
|
||||
|
||||
class BIMClashProperties(PropertyGroup):
|
||||
blender_clash_set_a: CollectionProperty(name="Blender Clash Set A", type=StrProperty)
|
||||
@@ -107,17 +134,33 @@ class BIMClashProperties(PropertyGroup):
|
||||
subtype="FILE_PATH",
|
||||
)
|
||||
|
||||
@property
|
||||
def active_clash_set(self):
|
||||
if self.active_clash_set_index < len(self.clash_sets):
|
||||
return self.clash_sets[self.active_clash_set_index]
|
||||
if TYPE_CHECKING:
|
||||
blender_clash_set_a: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
blender_clash_set_b: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
clash_sets: bpy.types.bpy_prop_collection_idprop[ClashSet]
|
||||
should_create_clash_snapshots: bool
|
||||
clash_results_path: str
|
||||
smart_grouped_clashes_path: str
|
||||
active_clash_set_index: int
|
||||
active_clash_index: int
|
||||
smart_clash_groups: bpy.types.bpy_prop_collection_idprop[SmartClashGroup]
|
||||
active_smart_group_index: int
|
||||
smart_clash_grouping_max_distance: int
|
||||
p1: Vector
|
||||
p2: Vector
|
||||
active_clash_text: str
|
||||
export_path: str
|
||||
|
||||
@property
|
||||
def active_smart_group(self):
|
||||
if self.active_smart_group_index < len(self.smart_clash_groups):
|
||||
return self.smart_clash_groups[self.active_smart_group_index]
|
||||
def active_clash_set(self) -> Union[ClashSet, None]:
|
||||
return tool.Blender.get_active_uilist_element(self.clash_sets, self.active_clash_set_index)
|
||||
|
||||
@property
|
||||
def active_clash(self):
|
||||
if self.active_clash_index < len(self.active_clash_set.clashes):
|
||||
return self.active_clash_set.clashes[self.active_clash_index]
|
||||
def active_smart_group(self) -> Union[SmartClashGroup, None]:
|
||||
return tool.Blender.get_active_uilist_element(self.smart_clash_groups, self.active_smart_group_index)
|
||||
|
||||
@property
|
||||
def active_clash(self) -> Union[Clash, None]:
|
||||
if not (clash_set := self.active_clash_set):
|
||||
return None
|
||||
return tool.Blender.get_active_uilist_element(clash_set.clashes, self.active_clash_index)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.helper
|
||||
from bpy.types import Panel
|
||||
from bonsai.bim.module.clash.data import ClashData
|
||||
@@ -36,9 +37,7 @@ class BIM_PT_ifcclash(Panel):
|
||||
ClashData.load()
|
||||
|
||||
layout = self.layout
|
||||
|
||||
scene = context.scene
|
||||
props = scene.BIMClashProperties
|
||||
props = tool.Clash.get_clash_props()
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.add_clash_set")
|
||||
@@ -157,7 +156,7 @@ class BIM_PT_smart_clash_manager(Panel):
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = context.scene.BIMClashProperties
|
||||
props = tool.Clash.get_clash_props()
|
||||
|
||||
row = layout.row()
|
||||
layout.label(text="Select clash results to group:")
|
||||
|
||||
@@ -145,11 +145,8 @@ class CostClassificationsData(ReferencesData):
|
||||
@classmethod
|
||||
def references(cls):
|
||||
results = []
|
||||
element = tool.Ifc.get().by_id(
|
||||
bpy.context.scene.BIMCostProperties.cost_items[
|
||||
bpy.context.scene.BIMCostProperties.active_cost_item_index
|
||||
].ifc_definition_id
|
||||
)
|
||||
props = tool.Cost.get_cost_props()
|
||||
element = tool.Ifc.get().by_id(props.cost_items[props.active_cost_item_index].ifc_definition_id)
|
||||
if element:
|
||||
for reference in ifcopenshell.util.classification.get_references(element):
|
||||
data = reference.get_info()
|
||||
|
||||
@@ -25,19 +25,16 @@ import ifcopenshell.util.classification
|
||||
import ifcopenshell.util.element
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.helper
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
|
||||
|
||||
class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
bl_idname = "bim.load_classification_library"
|
||||
bl_label = "Load Classification Library"
|
||||
bl_description = "Load classification library from the provided filepath."
|
||||
filename_ext = ".ifc"
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def _execute(self, context):
|
||||
IfcStore.classification_file = ifcopenshell.open(self.filepath)
|
||||
@@ -460,6 +457,7 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
|
||||
for prop in blender_properties:
|
||||
properties[prop.name] = prop.get_value()
|
||||
|
||||
# TODO: is this still the correct approach?
|
||||
if classification_pset.name == "undefined_set":
|
||||
if "ObjectType" in properties:
|
||||
if hasattr(element, "ObjectType"):
|
||||
|
||||
@@ -338,7 +338,8 @@ class BIM_PT_cost_classifications(Panel, ReferenceUI):
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get():
|
||||
return False
|
||||
return bool(context.scene.BIMCostProperties.cost_items)
|
||||
props = tool.Cost.get_cost_props()
|
||||
return bool(props.cost_items)
|
||||
|
||||
def draw(self, context):
|
||||
if not CostClassificationsData.is_loaded:
|
||||
|
||||
@@ -161,8 +161,12 @@ class AssignConstraint(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = tool.Ifc.get()
|
||||
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
|
||||
products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)]
|
||||
objs = [bpy.data.objects[self.obj]] if self.obj else context.selected_objects
|
||||
products = [
|
||||
self.file.by_id(obj_id)
|
||||
for obj in objs
|
||||
if (obj_id := tool.Blender.get_object_bim_props(obj).ifc_definition_id)
|
||||
]
|
||||
if products:
|
||||
ifcopenshell.api.run(
|
||||
"constraint.assign_constraint",
|
||||
@@ -184,8 +188,12 @@ class UnassignConstraint(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = tool.Ifc.get()
|
||||
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
|
||||
products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)]
|
||||
objs = [bpy.data.objects[self.obj]] if self.obj else context.selected_objects
|
||||
products = [
|
||||
self.file.by_id(obj_id)
|
||||
for obj in objs
|
||||
if (obj_id := tool.Blender.get_object_bim_props(obj).ifc_definition_id)
|
||||
]
|
||||
if products:
|
||||
ifcopenshell.api.run(
|
||||
"constraint.unassign_constraint",
|
||||
|
||||
@@ -79,18 +79,18 @@ class BIM_PT_object_constraints(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
if not (obj := context.active_object):
|
||||
return False
|
||||
if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id):
|
||||
return False
|
||||
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
return bool(tool.Ifc.get_object_by_identifier(props.ifc_definition_id))
|
||||
|
||||
def draw(self, context):
|
||||
if not ObjectConstraintsData.is_loaded:
|
||||
ObjectConstraintsData.load()
|
||||
|
||||
obj = context.active_object
|
||||
self.oprops = obj.BIMObjectProperties
|
||||
assert obj
|
||||
self.oprops = tool.Blender.get_object_bim_props(obj)
|
||||
self.sprops = context.scene.BIMConstraintProperties
|
||||
self.props = obj.BIMObjectConstraintProperties
|
||||
self.file = tool.Ifc.get()
|
||||
|
||||
@@ -66,8 +66,9 @@ class CostSchedulesData:
|
||||
@classmethod
|
||||
def schedules(cls):
|
||||
results = []
|
||||
if bpy.context.scene.BIMCostProperties.active_cost_schedule_id:
|
||||
schedule = tool.Ifc.get().by_id(bpy.context.scene.BIMCostProperties.active_cost_schedule_id)
|
||||
props = tool.Cost.get_cost_props()
|
||||
if props.active_cost_schedule_id:
|
||||
schedule = tool.Ifc.get().by_id(props.active_cost_schedule_id)
|
||||
results.append(
|
||||
{
|
||||
"id": schedule.id(),
|
||||
@@ -88,7 +89,8 @@ class CostSchedulesData:
|
||||
|
||||
@classmethod
|
||||
def is_editing_rates(cls):
|
||||
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_schedule_id
|
||||
props = tool.Cost.get_cost_props()
|
||||
ifc_id = props.active_cost_schedule_id
|
||||
if not ifc_id:
|
||||
return
|
||||
return tool.Ifc.get().by_id(ifc_id).PredefinedType == "SCHEDULEOFRATES"
|
||||
@@ -256,7 +258,8 @@ class CostSchedulesData:
|
||||
@classmethod
|
||||
def cost_quantities(cls):
|
||||
results = []
|
||||
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_item_id
|
||||
props = tool.Cost.get_cost_props()
|
||||
ifc_id = props.active_cost_item_id
|
||||
if not ifc_id:
|
||||
return results
|
||||
for quantity in tool.Ifc.get().by_id(ifc_id).CostQuantities or []:
|
||||
@@ -265,7 +268,8 @@ class CostSchedulesData:
|
||||
|
||||
@classmethod
|
||||
def cost_values(cls):
|
||||
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_item_id
|
||||
props = tool.Cost.get_cost_props()
|
||||
ifc_id = props.active_cost_item_id
|
||||
if not ifc_id:
|
||||
return []
|
||||
return ifcopenshell.util.cost.get_cost_values(tool.Ifc.get().by_id(ifc_id))
|
||||
@@ -324,7 +328,8 @@ class CostItemQuantitiesData:
|
||||
|
||||
@classmethod
|
||||
def process_quantity_names(cls):
|
||||
active_task_index = bpy.context.scene.BIMWorkScheduleProperties.active_task_index
|
||||
props = tool.Sequence.get_work_schedule_props()
|
||||
active_task_index = props.active_task_index
|
||||
tprops = tool.Sequence.get_task_tree_props()
|
||||
total_tasks = len(tprops.tasks)
|
||||
if not total_tasks or active_task_index >= total_tasks:
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
import bpy
|
||||
import ifcopenshell.api
|
||||
import bonsai.tool as tool
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.cost as core
|
||||
from typing import get_args, TYPE_CHECKING
|
||||
from typing import get_args, TYPE_CHECKING, Literal
|
||||
|
||||
|
||||
class AddCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -36,16 +36,18 @@ class AddCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
|
||||
object_type: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
predefined_type = context.scene.BIMCostProperties.cost_schedule_predefined_types
|
||||
props = tool.Cost.get_cost_props()
|
||||
predefined_type = props.cost_schedule_predefined_types
|
||||
if predefined_type == "USERDEFINED":
|
||||
predefined_type = self.object_type
|
||||
core.add_cost_schedule(tool.Ifc, name=self.name, predefined_type=predefined_type)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = tool.Cost.get_cost_props()
|
||||
layout.prop(self, "name", text="Name")
|
||||
layout.prop(context.scene.BIMCostProperties, "cost_schedule_predefined_types", text="Type")
|
||||
if context.scene.BIMCostProperties.cost_schedule_predefined_types == "USERDEFINED":
|
||||
layout.prop(props, "cost_schedule_predefined_types", text="Type")
|
||||
if props.cost_schedule_predefined_types == "USERDEFINED":
|
||||
layout.prop(self, "object_type", text="Object type")
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -58,10 +60,11 @@ class EditCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = tool.Cost.get_cost_props()
|
||||
core.edit_cost_schedule(
|
||||
tool.Ifc,
|
||||
tool.Cost,
|
||||
cost_schedule=tool.Ifc.get().by_id(context.scene.BIMCostProperties.active_cost_schedule_id),
|
||||
cost_schedule=tool.Ifc.get().by_id(props.active_cost_schedule_id),
|
||||
)
|
||||
|
||||
|
||||
@@ -533,6 +536,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"})
|
||||
@@ -559,7 +563,8 @@ class AddCostColumn(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.scene.BIMCostProperties.cost_column:
|
||||
props = tool.Cost.get_cost_props()
|
||||
if not props.cost_column:
|
||||
cls.poll_message_set("Cost column name is empty")
|
||||
return False
|
||||
return True
|
||||
@@ -678,7 +683,7 @@ class CalculateCostItemResourceValue(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ExportCostSchedules(bpy.types.Operator):
|
||||
class ExportCostSchedules(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.export_cost_schedules"
|
||||
bl_label = "Export Cost Schedule"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
@@ -691,6 +696,13 @@ class ExportCostSchedules(bpy.types.Operator):
|
||||
default=True,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
format: Literal["CSV", "XLSX", "ODS"]
|
||||
|
||||
@property
|
||||
def filename_ext(self) -> str:
|
||||
return f".{self.format.lower()}"
|
||||
|
||||
def execute(self, context):
|
||||
cost_schedule = tool.Ifc.get().by_id(self.cost_schedule) if self.cost_schedule else None
|
||||
r = core.export_cost_schedules(
|
||||
@@ -701,9 +713,7 @@ class ExportCostSchedules(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
wm.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
return ExportHelper.invoke(self, context, event)
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.label(text="Choose a format")
|
||||
@@ -740,15 +750,14 @@ class LoadProductCostItems(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get() or not (obj := context.active_object) or not (obj.BIMObjectProperties.ifc_definition_id):
|
||||
if not tool.Ifc.get() or not (obj := context.active_object) or not (tool.Blender.get_ifc_definition_id(obj)):
|
||||
cls.poll_message_set("No IFC object is active.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
core.load_product_cost_items(
|
||||
tool.Cost, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id)
|
||||
)
|
||||
obj = context.active_object
|
||||
core.load_product_cost_items(tool.Cost, product=tool.Ifc.get_entity(obj))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
|
||||
def get_schedule_of_rates(self, context):
|
||||
@@ -78,8 +79,8 @@ def update_active_cost_item_index(self, context):
|
||||
CostClassificationsData.load()
|
||||
|
||||
|
||||
def update_cost_item_identification(self, context):
|
||||
props = context.scene.BIMCostProperties
|
||||
def update_cost_item_identification(self: "CostItem", context: bpy.types.Context):
|
||||
props = tool.Cost.get_cost_props()
|
||||
if not props.is_cost_update_enabled or self.identification == "XXX":
|
||||
return
|
||||
self.file = tool.Ifc.get()
|
||||
@@ -93,8 +94,8 @@ def update_cost_item_identification(self, context):
|
||||
attribute.string_value = self.identification
|
||||
|
||||
|
||||
def update_cost_item_name(self, context):
|
||||
props = context.scene.BIMCostProperties
|
||||
def update_cost_item_name(self: "CostItem", context: bpy.types.Context) -> None:
|
||||
props = tool.Cost.get_cost_props()
|
||||
if not props.is_cost_update_enabled or self.name == "Unnamed":
|
||||
return
|
||||
self.file = tool.Ifc.get()
|
||||
@@ -142,6 +143,14 @@ class CostItem(PropertyGroup):
|
||||
is_expanded: BoolProperty(name="Is Expanded")
|
||||
level_index: IntProperty(name="Level Index")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
identification: str
|
||||
ifc_definition_id: int
|
||||
has_children: bool
|
||||
is_expanded: bool
|
||||
level_index: int
|
||||
|
||||
|
||||
class CostItemQuantity(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
@@ -150,11 +159,22 @@ class CostItemQuantity(PropertyGroup):
|
||||
unit_symbol: StringProperty(name="Unit Symbol")
|
||||
total_cost_quantity: FloatProperty(name="Total Quantity")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
ifc_definition_id: int
|
||||
total_quantity: float
|
||||
unit_symbol: str
|
||||
total_cost_quantity: float
|
||||
|
||||
|
||||
class CostItemType(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
ifc_definition_id: int
|
||||
|
||||
|
||||
def update_cost_item_parent(self, context):
|
||||
cost_item = tool.Cost.get_highlighted_cost_item()
|
||||
@@ -176,6 +196,9 @@ def update_active_cost_item_resources(self, context):
|
||||
class ScheduleColumn(PropertyGroup):
|
||||
schedule_id: IntProperty()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
schedule_id: int
|
||||
|
||||
|
||||
class BIMCostProperties(PropertyGroup):
|
||||
cost_schedule_predefined_types: EnumProperty(
|
||||
@@ -249,3 +272,57 @@ class BIMCostProperties(PropertyGroup):
|
||||
custom_currency: StringProperty(
|
||||
name="Custom Currency", default="USD", description="Custom Currency in ISO 4217 format"
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
cost_schedule_predefined_types: str
|
||||
is_cost_update_enabled: bool
|
||||
cost_schedule_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
is_editing: str
|
||||
active_cost_schedule_id: int
|
||||
cost_items: bpy.types.bpy_prop_collection_idprop[CostItem]
|
||||
active_cost_item_id: int
|
||||
cost_item_editing_type: str
|
||||
active_cost_item_index: int
|
||||
cost_item_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
contracted_cost_items: str
|
||||
quantity_types: str
|
||||
product_quantity_names: str
|
||||
process_quantity_names: str
|
||||
resource_quantity_names: str
|
||||
active_cost_item_quantity_id: int
|
||||
quantity_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
cost_types: Literal["FIXED", "SUM", "CATEGORY"]
|
||||
cost_category: str
|
||||
fixed_cost_value: float
|
||||
active_cost_value_id: int
|
||||
cost_value_editing_type: str
|
||||
cost_value_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
cost_value_formula: str
|
||||
cost_column: str
|
||||
should_show_column_ui: bool
|
||||
should_show_currency_ui: bool
|
||||
columns: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
columns_storage: bpy.types.bpy_prop_collection_idprop[ScheduleColumn]
|
||||
active_column_index: int
|
||||
cost_item_products: bpy.types.bpy_prop_collection_idprop[CostItemQuantity]
|
||||
active_cost_item_product_index: int
|
||||
cost_item_processes: bpy.types.bpy_prop_collection_idprop[CostItemQuantity]
|
||||
active_cost_item_process_index: int
|
||||
cost_item_resources: bpy.types.bpy_prop_collection_idprop[CostItemQuantity]
|
||||
active_cost_item_resource_index: int
|
||||
cost_item_type_products: bpy.types.bpy_prop_collection_idprop[CostItemType]
|
||||
active_cost_item_type_product_index: int
|
||||
schedule_of_rates: str
|
||||
cost_item_rates: bpy.types.bpy_prop_collection_idprop[CostItem]
|
||||
active_cost_item_rate_index: int
|
||||
contracted_cost_item_rates: str
|
||||
product_cost_items: bpy.types.bpy_prop_collection_idprop[CostItemQuantity]
|
||||
active_product_cost_item_index: int
|
||||
enable_reorder: bool
|
||||
show_nested_elements: bool
|
||||
show_nested_tasks: bool
|
||||
show_nested_resources: bool
|
||||
change_cost_item_parent: bool
|
||||
show_cost_item_operators: bool
|
||||
currency: str
|
||||
custom_currency: str
|
||||
|
||||
@@ -16,13 +16,17 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
import bpy
|
||||
import bonsai.bim.helper
|
||||
import bonsai.bim.module.cost.prop as CostProp
|
||||
import bonsai.tool as tool
|
||||
from bpy.types import Panel, UIList
|
||||
from bonsai.bim.module.cost.data import CostSchedulesData
|
||||
from typing import Any
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.cost.prop import BIMCostProperties, CostItemQuantity
|
||||
|
||||
|
||||
class BIM_PT_cost_schedules(Panel):
|
||||
@@ -43,7 +47,7 @@ class BIM_PT_cost_schedules(Panel):
|
||||
if not CostSchedulesData.is_loaded:
|
||||
CostSchedulesData.load()
|
||||
|
||||
self.props = context.scene.BIMCostProperties
|
||||
self.props = tool.Cost.get_cost_props()
|
||||
if not self.props.active_cost_schedule_id:
|
||||
row = self.layout.row(align=True)
|
||||
if CostSchedulesData.data["total_cost_schedules"]:
|
||||
@@ -299,7 +303,7 @@ class BIM_PT_cost_item_types(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.BIMCostProperties
|
||||
props = tool.Cost.get_cost_props()
|
||||
total_cost_items = len(props.cost_items)
|
||||
if not props.active_cost_schedule_id:
|
||||
return False
|
||||
@@ -312,7 +316,7 @@ class BIM_PT_cost_item_types(Panel):
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMCostProperties
|
||||
self.props = tool.Cost.get_cost_props()
|
||||
cost_item = self.props.cost_items[self.props.active_cost_item_index]
|
||||
grid = self.layout.grid_flow(columns=3, even_columns=True)
|
||||
|
||||
@@ -348,8 +352,8 @@ class BIM_PT_cost_item_types(Panel):
|
||||
|
||||
row2 = col.row()
|
||||
row2.template_list(
|
||||
"BIM_UL_cost_item_quantities",
|
||||
"",
|
||||
BIM_UL_cost_item_quantities.__name__,
|
||||
BIM_UL_cost_item_quantities.__name__ + "_cost_item_processes",
|
||||
self.props,
|
||||
"cost_item_processes",
|
||||
self.props,
|
||||
@@ -365,8 +369,8 @@ class BIM_PT_cost_item_types(Panel):
|
||||
|
||||
row2 = col.row()
|
||||
row2.template_list(
|
||||
"BIM_UL_cost_item_quantities",
|
||||
"",
|
||||
BIM_UL_cost_item_quantities.__name__,
|
||||
BIM_UL_cost_item_quantities.__name__ + "_cost_item_resources",
|
||||
self.props,
|
||||
"cost_item_resources",
|
||||
self.props,
|
||||
@@ -385,7 +389,7 @@ class BIM_PT_cost_item_quantities(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.BIMCostProperties
|
||||
props = tool.Cost.get_cost_props()
|
||||
total_cost_items = len(props.cost_items)
|
||||
if not props.active_cost_schedule_id:
|
||||
return False
|
||||
@@ -398,7 +402,7 @@ class BIM_PT_cost_item_quantities(Panel):
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMCostProperties
|
||||
self.props = tool.Cost.get_cost_props()
|
||||
|
||||
cost_item = self.props.cost_items[self.props.active_cost_item_index]
|
||||
|
||||
@@ -435,8 +439,8 @@ class BIM_PT_cost_item_quantities(Panel):
|
||||
row2.prop(self.props, "show_nested_elements", text="Show nested")
|
||||
row2 = col.row()
|
||||
row2.template_list(
|
||||
"BIM_UL_cost_item_quantities",
|
||||
"",
|
||||
BIM_UL_cost_item_quantities.__name__,
|
||||
BIM_UL_cost_item_quantities.__name__ + "_cost_item_products",
|
||||
self.props,
|
||||
"cost_item_products",
|
||||
self.props,
|
||||
@@ -462,7 +466,7 @@ class BIM_PT_cost_item_quantities(Panel):
|
||||
row2.label(text="Tasks ({})".format(total_cost_item_processes))
|
||||
|
||||
tprops = tool.Sequence.get_task_tree_props()
|
||||
wprops = context.scene.BIMWorkScheduleProperties
|
||||
wprops = tool.Sequence.get_work_schedule_props()
|
||||
if tprops.tasks and wprops.active_task_index < len(tprops.tasks):
|
||||
if has_quantity_names:
|
||||
op = row2.operator("bim.assign_cost_item_quantity", text="", icon="PROPERTIES")
|
||||
@@ -480,8 +484,8 @@ class BIM_PT_cost_item_quantities(Panel):
|
||||
|
||||
row2 = col.row()
|
||||
row2.template_list(
|
||||
"BIM_UL_cost_item_quantities",
|
||||
"",
|
||||
BIM_UL_cost_item_quantities.__name__,
|
||||
BIM_UL_cost_item_quantities.__name__ + "_cost_item_processes",
|
||||
self.props,
|
||||
"cost_item_processes",
|
||||
self.props,
|
||||
@@ -528,8 +532,8 @@ class BIM_PT_cost_item_quantities(Panel):
|
||||
|
||||
row2 = col.row()
|
||||
row2.template_list(
|
||||
"BIM_UL_cost_item_quantities",
|
||||
"",
|
||||
BIM_UL_cost_item_quantities.__name__,
|
||||
BIM_UL_cost_item_quantities.__name__ + "_cost_item_resources",
|
||||
self.props,
|
||||
"cost_item_resources",
|
||||
self.props,
|
||||
@@ -557,7 +561,7 @@ class BIM_PT_cost_item_rates(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.BIMCostProperties
|
||||
props = tool.Cost.get_cost_props()
|
||||
total_cost_items = len(props.cost_items)
|
||||
if not props.active_cost_schedule_id:
|
||||
return False
|
||||
@@ -570,7 +574,7 @@ class BIM_PT_cost_item_rates(Panel):
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMCostProperties
|
||||
self.props = tool.Cost.get_cost_props()
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "schedule_of_rates", text="")
|
||||
if self.props.active_cost_item_rate_index < len(self.props.cost_item_rates):
|
||||
@@ -606,13 +610,14 @@ class BIM_UL_cost_items_trait:
|
||||
split2.label(text="Quantity")
|
||||
split2.label(text="Value")
|
||||
|
||||
for column in bpy.context.scene.BIMCostProperties.columns:
|
||||
props = tool.Cost.get_cost_props()
|
||||
for column in props.columns:
|
||||
split2.label(text=column.name)
|
||||
split2.label(text="Total Cost")
|
||||
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
self.props = context.scene.BIMCostProperties
|
||||
self.props = tool.Cost.get_cost_props()
|
||||
cost_item = CostSchedulesData.data["cost_items"][item.ifc_definition_id]
|
||||
row = layout.row(align=True)
|
||||
|
||||
@@ -708,18 +713,16 @@ class BIM_UL_cost_items_trait:
|
||||
|
||||
|
||||
class BIM_UL_cost_items(BIM_UL_cost_items_trait, UIList):
|
||||
def __init__(self):
|
||||
self.contract_operator = "bim.contract_cost_item"
|
||||
self.expand_operator = "bim.expand_cost_item"
|
||||
contract_operator = "bim.contract_cost_item"
|
||||
expand_operator = "bim.expand_cost_item"
|
||||
|
||||
|
||||
class BIM_UL_cost_item_rates(BIM_UL_cost_items_trait, UIList):
|
||||
# A schedule of rates UIList is identical to a regular cost items UIList but
|
||||
# we want a separate UIList instance so that you can browse both lists
|
||||
# independently in Blender. So we use a trait.
|
||||
def __init__(self):
|
||||
self.contract_operator = "bim.contract_cost_item_rate"
|
||||
self.expand_operator = "bim.expand_cost_item_rate"
|
||||
contract_operator = "bim.contract_cost_item_rate"
|
||||
expand_operator = "bim.expand_cost_item_rate"
|
||||
|
||||
def draw_quantity_column(self, layout, cost_item):
|
||||
self.draw_uom_column(layout, cost_item)
|
||||
@@ -730,7 +733,6 @@ class BIM_UL_cost_item_rates(BIM_UL_cost_items_trait, UIList):
|
||||
|
||||
class BIM_UL_cost_columns(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
props = context.scene.BIMCostProperties
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
row.prop(item, "name", emboss=False, text="")
|
||||
@@ -739,7 +741,7 @@ class BIM_UL_cost_columns(UIList):
|
||||
|
||||
class BIM_UL_cost_item_types(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
props = context.scene.BIMCostProperties
|
||||
props = tool.Cost.get_cost_props()
|
||||
cost_item = props.cost_items[props.active_cost_item_index]
|
||||
|
||||
if item:
|
||||
@@ -751,9 +753,17 @@ class BIM_UL_cost_item_types(UIList):
|
||||
|
||||
|
||||
class BIM_UL_cost_item_quantities(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
props = context.scene.BIMCostProperties
|
||||
cost_item = props.cost_items[props.active_cost_item_index]
|
||||
def draw_item(
|
||||
self,
|
||||
context: bpy.types.Context,
|
||||
layout: bpy.types.UILayout,
|
||||
data: BIMCostProperties,
|
||||
item: CostItemQuantity,
|
||||
icon,
|
||||
active_data,
|
||||
active_propname,
|
||||
):
|
||||
cost_item = data.cost_items[data.active_cost_item_index]
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
op = row.operator("bim.select_product", text="", icon="RESTRICT_SELECT_OFF")
|
||||
@@ -790,15 +800,15 @@ class BIM_PT_Costing_Tools(Panel):
|
||||
bl_parent_id = "BIM_PT_tab_cost"
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMCostProperties
|
||||
props = tool.Cost.get_cost_props()
|
||||
row = self.layout.row()
|
||||
row.operator("bim.load_product_cost_items", icon="FILE_REFRESH")
|
||||
row = self.layout.row()
|
||||
row.template_list(
|
||||
"BIM_UL_product_cost_items",
|
||||
"",
|
||||
self.props,
|
||||
props,
|
||||
"product_cost_items",
|
||||
self.props,
|
||||
props,
|
||||
"active_product_cost_item_index",
|
||||
)
|
||||
|
||||
@@ -18,9 +18,13 @@
|
||||
|
||||
import bpy
|
||||
from bpy.types import PropertyGroup
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
class BIMCoveringProperties(PropertyGroup):
|
||||
ceiling_height: bpy.props.FloatProperty(
|
||||
name="ceiling_height", default=2.7, subtype="DISTANCE", description="Ceiling height"
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
ceiling_height: float
|
||||
|
||||
@@ -24,6 +24,7 @@ import bonsai.tool as tool
|
||||
from bonsai.bim.helper import prop_with_search
|
||||
from bonsai.bim.module.model.data import AuthoringData
|
||||
from bpy.types import WorkSpaceTool
|
||||
from functools import partial
|
||||
|
||||
|
||||
class CoveringTool(WorkSpaceTool):
|
||||
@@ -45,9 +46,7 @@ class CoveringTool(WorkSpaceTool):
|
||||
CoveringToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
|
||||
|
||||
|
||||
def add_layout_hotkey(layout: bpy.types.UILayout, text: str, hotkey: str, description: str) -> None:
|
||||
args = ("covering", layout, text, hotkey, description)
|
||||
tool.Blender.add_layout_hotkey_operator(*args)
|
||||
add_layout_hotkey = partial(tool.Blender.add_layout_hotkey_operator, tool_name="covering", module_name=__name__)
|
||||
|
||||
|
||||
class CoveringToolUI:
|
||||
@@ -55,7 +54,7 @@ class CoveringToolUI:
|
||||
def draw(cls, context, layout, ifc_element_type=None):
|
||||
cls.layout = layout
|
||||
cls.props = tool.Model.get_model_props()
|
||||
cls.covering_props = context.scene.BIMCoveringProperties
|
||||
cls.covering_props = tool.Covering.get_covering_props()
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
if not tool.Ifc.get():
|
||||
@@ -88,7 +87,7 @@ class CoveringToolUI:
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_A")
|
||||
row.operator("bim.add_constr_type_instance", text="Add")
|
||||
row.operator("bim.add_occurrence", text="Add")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
@@ -136,8 +135,8 @@ class CoveringToolUI:
|
||||
if AuthoringData.data["ifc_classes"]:
|
||||
if cls.props.ifc_class:
|
||||
box = cls.layout.box()
|
||||
if AuthoringData.data["type_thumbnail"]:
|
||||
box.template_icon(icon_value=AuthoringData.data["type_thumbnail"], scale=5)
|
||||
if thumbnail := AuthoringData.data["relating_type_data"].get("thumbnail"):
|
||||
box.template_icon(icon_value=thumbnail, scale=5)
|
||||
else:
|
||||
op = box.operator("bim.load_type_thumbnails", text="Load Thumbnails", icon="FILE_REFRESH")
|
||||
op.ifc_class = cls.props.ifc_class
|
||||
@@ -145,8 +144,8 @@ class CoveringToolUI:
|
||||
|
||||
class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.covering_hotkey"
|
||||
bl_label = "Hotkey"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_label = ""
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
hotkey: bpy.props.StringProperty()
|
||||
description: bpy.props.StringProperty()
|
||||
|
||||
@@ -159,12 +158,11 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return operator.description or ""
|
||||
|
||||
def _execute(self, context):
|
||||
# self.props = context.scene.BIMCoveringProperties
|
||||
self.props = tool.Covering.get_covering_props()
|
||||
getattr(self, f"hotkey_{self.hotkey}")()
|
||||
|
||||
def invoke(self, context, event):
|
||||
# https://blender.stackexchange.com/questions/276035/how-do-i-make-operators-remember-their-property-values-when-called-from-a-hotkey
|
||||
# self.props = context.scene.BIMSpatialProperties
|
||||
return self.execute(context)
|
||||
|
||||
def draw(self, context):
|
||||
@@ -175,22 +173,22 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
container = tool.Root.get_default_container()
|
||||
|
||||
if AuthoringData.data["predefined_type"] == "FLOORING":
|
||||
if AuthoringData.data["relating_type_data"].get("predefined_type") == "FLOORING":
|
||||
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
|
||||
bpy.ops.bim.add_instance_flooring_coverings_from_walls()
|
||||
elif container:
|
||||
bpy.ops.bim.add_instance_flooring_covering_from_cursor()
|
||||
else:
|
||||
bpy.ops.bim.add_constr_type_instance()
|
||||
elif AuthoringData.data["predefined_type"] == "CEILING":
|
||||
bpy.ops.bim.add_occurrence()
|
||||
elif AuthoringData.data["relating_type_data"].get("predefined_type") == "CEILING":
|
||||
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
|
||||
bpy.ops.bim.add_instance_ceiling_coverings_from_walls()
|
||||
elif container:
|
||||
bpy.ops.bim.add_instance_ceiling_covering_from_cursor()
|
||||
else:
|
||||
bpy.ops.bim.add_constr_type_instance()
|
||||
bpy.ops.bim.add_occurrence()
|
||||
else:
|
||||
bpy.ops.bim.add_constr_type_instance()
|
||||
bpy.ops.bim.add_occurrence()
|
||||
|
||||
def hotkey_S_G(self):
|
||||
active_obj = bpy.context.active_object
|
||||
|
||||
@@ -181,12 +181,12 @@ class RunAnalysis(bpy.types.Operator):
|
||||
if modifier.type == "TRIANGULATE":
|
||||
return True
|
||||
|
||||
def get_covetool_category(self, obj):
|
||||
def get_covetool_category(self, obj: bpy.types.Object):
|
||||
if not hasattr(obj, "data") or not isinstance(obj.data, bpy.types.Mesh):
|
||||
return
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
|
||||
return
|
||||
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
element = self.file.by_id(ifc_id)
|
||||
ifc_class = element.is_a()
|
||||
if "IfcSlab" in ifc_class:
|
||||
return "floors"
|
||||
|
||||
@@ -27,6 +27,7 @@ import ifcopenshell
|
||||
import ifcopenshell.util.selector
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.module.drawing.scheduler as scheduler
|
||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||
from bonsai.bim.handler import refresh_ui_data
|
||||
from typing import TYPE_CHECKING
|
||||
from collections import Counter
|
||||
@@ -42,7 +43,8 @@ class AddCsvAttribute(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
attribute = context.scene.CsvProperties.csv_attributes.add()
|
||||
props = tool.Blender.get_csv_props()
|
||||
props.csv_attributes.add()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -53,7 +55,8 @@ class RemoveCsvAttribute(bpy.types.Operator):
|
||||
index: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.CsvProperties.csv_attributes.remove(self.index)
|
||||
props = tool.Blender.get_csv_props()
|
||||
props.csv_attributes.remove(self.index)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -64,7 +67,8 @@ class RemoveAllCsvAttributes(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.CsvProperties.csv_attributes.clear()
|
||||
props = tool.Blender.get_csv_props()
|
||||
props.csv_attributes.clear()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -76,8 +80,9 @@ class ReorderCsvAttribute(bpy.types.Operator):
|
||||
new_index: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
old = context.scene.CsvProperties.csv_attributes[self.old_index]
|
||||
new = context.scene.CsvProperties.csv_attributes[self.new_index]
|
||||
props = tool.Blender.get_csv_props()
|
||||
old = props.csv_attributes[self.old_index]
|
||||
new = props.csv_attributes[self.new_index]
|
||||
props = ["name", "header", "sort", "group", "varies_value", "summary", "formatting"]
|
||||
for prop in props:
|
||||
value = getattr(new, prop)
|
||||
@@ -86,16 +91,16 @@ class ReorderCsvAttribute(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ImportCsvAttributes(bpy.types.Operator):
|
||||
class ImportCsvAttributes(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.import_csv_attributes"
|
||||
bl_label = "Load CSV Settings"
|
||||
bl_description = "Import a json template for CSV export"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filename_ext = ".json"
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.CsvProperties
|
||||
props = tool.Blender.get_csv_props()
|
||||
data = json.load(open(self.filepath))
|
||||
tool.Search.import_filter_query(data["query"], props.filter_groups)
|
||||
|
||||
@@ -121,22 +126,17 @@ class ImportCsvAttributes(bpy.types.Operator):
|
||||
setattr(new, prop, attribute[prop])
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class ExportCsvAttributes(bpy.types.Operator):
|
||||
class ExportCsvAttributes(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.export_csv_attributes"
|
||||
bl_label = "Save CSV Settings"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Save a json template for CSV export"
|
||||
filename_ext = ".json"
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.CsvProperties
|
||||
props = tool.Blender.get_csv_props()
|
||||
|
||||
settings = {}
|
||||
for prop in [
|
||||
@@ -175,40 +175,42 @@ class ExportCsvAttributes(bpy.types.Operator):
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class ExportIfcCsv(bpy.types.Operator):
|
||||
class ExportIfcCsv(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.export_ifccsv"
|
||||
bl_label = "Export IFC"
|
||||
bl_description = "Export IFC data as a spreadsheet."
|
||||
filename_ext = ".csv"
|
||||
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.CsvProperties
|
||||
props = tool.Blender.get_csv_props()
|
||||
if not props.should_load_from_memory and not props.csv_ifc_file:
|
||||
cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.")
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
props = tool.Blender.get_csv_props()
|
||||
if props.format == "web":
|
||||
return "Open Web UI for spreadsheet data export."
|
||||
return f"Export IFC data as a spreadsheet by the provided filepath in '{props.format}' format ."
|
||||
|
||||
def invoke(self, context, event):
|
||||
props = context.scene.CsvProperties
|
||||
props = tool.Blender.get_csv_props()
|
||||
if props.format == "web":
|
||||
return self.execute(context)
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
self.filter_glob = f"*.{props.format}"
|
||||
self.filename_ext = f".{props.format}"
|
||||
return ExportHelper.invoke(self, context, event)
|
||||
|
||||
def execute(self, context):
|
||||
import ifccsv
|
||||
|
||||
props = context.scene.CsvProperties
|
||||
props = tool.Blender.get_csv_props()
|
||||
self.filepath = bpy.path.ensure_ext(self.filepath, f".{props.format}")
|
||||
if props.should_load_from_memory:
|
||||
ifc_file = tool.Ifc.get()
|
||||
@@ -287,16 +289,16 @@ class ExportIfcCsv(bpy.types.Operator):
|
||||
]
|
||||
|
||||
|
||||
class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
bl_idname = "bim.import_ifccsv"
|
||||
bl_label = "Import to IFC"
|
||||
bl_description = "Import IFC data from a spreadsheet."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filter_glob: bpy.props.StringProperty(default="*.csv;*.ods;*.xlsx", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.CsvProperties
|
||||
props = tool.Blender.get_csv_props()
|
||||
if not props.should_load_from_memory and not props.csv_ifc_file:
|
||||
cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.")
|
||||
return False
|
||||
@@ -304,14 +306,12 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".csv")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
return ImportHelper.invoke(self, context, event)
|
||||
|
||||
def _execute(self, context):
|
||||
import ifccsv
|
||||
|
||||
props = context.scene.CsvProperties
|
||||
props = tool.Blender.get_csv_props()
|
||||
if props.should_load_from_memory:
|
||||
ifc_file = tool.Ifc.get()
|
||||
else:
|
||||
@@ -337,18 +337,15 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectCsvIfcFile(bpy.types.Operator):
|
||||
class SelectCsvIfcFile(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_csv_ifc_file"
|
||||
bl_label = "Select CSV IFC File"
|
||||
bl_description = "Select IFC file for spreadsheet import/export."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filename_ext = ".ifc"
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.CsvProperties.csv_ifc_file = self.filepath
|
||||
props = tool.Blender.get_csv_props()
|
||||
props.csv_ifc_file = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
@@ -29,6 +29,7 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
|
||||
class CsvAttribute(PropertyGroup):
|
||||
@@ -59,6 +60,15 @@ class CsvAttribute(PropertyGroup):
|
||||
)
|
||||
formatting: StringProperty(default="{{value}}", name="Formatting")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
header: str
|
||||
sort: Literal["NONE", "ASC", "DESC"]
|
||||
group: Literal["NONE", "GROUP", "CONCAT", "VARIES", "SUM", "AVERAGE", "MIN", "MAX"]
|
||||
varies_value: str
|
||||
summary: Literal["NONE", "SUM", "AVERAGE", "MIN", "MAX"]
|
||||
formatting: str
|
||||
|
||||
|
||||
class CsvProperties(PropertyGroup):
|
||||
csv_ifc_file: StringProperty(default="", name="IFC File")
|
||||
@@ -104,3 +114,26 @@ class CsvProperties(PropertyGroup):
|
||||
name="Load from Memory",
|
||||
description="Use IFC file currently loaded in Bonsai",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
csv_ifc_file: str
|
||||
ifc_selector: str
|
||||
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
|
||||
csv_attributes: bpy.types.bpy_prop_collection_idprop[CsvAttribute]
|
||||
should_generate_svg: bool
|
||||
should_preserve_existing: bool
|
||||
include_global_id: bool
|
||||
null_value: str
|
||||
empty_value: str
|
||||
true_value: str
|
||||
false_value: str
|
||||
concat_value: str
|
||||
csv_delimiter: Literal["NONE", "ASC", "DESC"]
|
||||
format: Literal["csv", "xlsx", "ods", "web"]
|
||||
csv_custom_delimiter: str
|
||||
should_show_settings: bool
|
||||
should_show_sort: bool
|
||||
should_show_group: bool
|
||||
should_show_summary: bool
|
||||
should_show_formatting: bool
|
||||
should_load_from_memory: bool
|
||||
|
||||
@@ -33,9 +33,7 @@ class BIM_PT_ifccsv(Panel):
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
scene = context.scene
|
||||
props = scene.CsvProperties
|
||||
props = tool.Blender.get_csv_props()
|
||||
|
||||
if tool.Ifc.get():
|
||||
row = layout.row(align=True)
|
||||
@@ -126,5 +124,8 @@ class BIM_PT_ifccsv(Panel):
|
||||
row.operator("bim.remove_csv_attribute", icon="X", text="").index = index
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.export_ifccsv", icon="EXPORT", text="Export IFC to " + props.format.upper())
|
||||
if props.format == "web":
|
||||
row.operator("bim.export_ifccsv", icon="EXPORT", text="Open Web UI")
|
||||
else:
|
||||
row.operator("bim.export_ifccsv", icon="EXPORT", text="Export IFC to " + props.format.upper())
|
||||
row.operator("bim.import_ifccsv", icon="IMPORT")
|
||||
|
||||
@@ -44,6 +44,7 @@ classes = (
|
||||
operator.SelectHighPolygonMeshes,
|
||||
operator.SelectHighestPolygonMeshes,
|
||||
operator.ToggleDetailedIOSLogs,
|
||||
operator.ValidateIfcAssets,
|
||||
operator.ValidateIfcFile,
|
||||
prop.BIMDebugProperties,
|
||||
ui.BIM_PT_debug,
|
||||
|
||||
@@ -23,6 +23,7 @@ import time
|
||||
import random
|
||||
import logging
|
||||
import subprocess
|
||||
import platform
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.geom
|
||||
@@ -36,6 +37,8 @@ import bonsai.core.profile
|
||||
import bonsai.core.type
|
||||
import bonsai.bim.handler
|
||||
import bonsai.bim.import_ifc as import_ifc
|
||||
from collections import defaultdict
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
from pathlib import Path
|
||||
from bonsai import get_debug_info, format_debug_info
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
@@ -150,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"
|
||||
@@ -382,7 +446,7 @@ class InspectFromObject(bpy.types.Operator):
|
||||
def get_active_object_ifc_definition(cls, context: bpy.types.Context) -> Union[int, None]:
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
if ifc_id := obj.BIMObjectProperties.ifc_definition_id:
|
||||
if ifc_id := tool.Blender.get_ifc_definition_id(obj):
|
||||
return ifc_id
|
||||
if (
|
||||
(data := obj.data)
|
||||
@@ -446,12 +510,11 @@ class ParseExpress(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectExpressFile(bpy.types.Operator):
|
||||
class SelectExpressFile(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_express_file"
|
||||
bl_label = "Select Express File"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Select an IFC EXPRESS definition"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.exp", options={"HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
@@ -460,10 +523,6 @@ class SelectExpressFile(bpy.types.Operator):
|
||||
props.express_file = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class PurgeHdf5Cache(bpy.types.Operator):
|
||||
bl_idname = "bim.purge_hdf5_cache"
|
||||
@@ -513,6 +572,7 @@ class PrintUnusedElementStats(bpy.types.Operator):
|
||||
if self.ignore_styled_items:
|
||||
ignore_classes += ["IfcStyledItem"]
|
||||
ignore_classes += [
|
||||
"IfcDocumentReference", # Document references for sheet elements (drawings, schedules, etc).
|
||||
"IfcIndexedColourMap", # Only referenced by inverse attributes.
|
||||
"IfcIndexedTextureMap", # Only referenced by inverse attributes.
|
||||
]
|
||||
@@ -522,21 +582,22 @@ class PrintUnusedElementStats(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator, ExportHelper):
|
||||
bl_idname = "bim.purge_unused_elements_by_class"
|
||||
bl_label = "Purge Unused Elements By Class"
|
||||
bl_description = (
|
||||
"Will find all elements of class that have no inverse references and will remove them, use very carefully.\n"
|
||||
"If IFC class is provided in neighbour field, will purge only elemnts of the provided class. Otherwise will purge all white-listed elements.\n"
|
||||
"If IFC class is provided in neighbour field, will purge only elements of the provided class. Otherwise will purge all white-listed elements.\n"
|
||||
"ALT+CLICK to provide a path where to save the IFC file with the removed elements (note changes will be applied to the current IFC too)"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filename_ext = ".ifc"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.type == "LEFTMOUSE" and event.alt:
|
||||
context.window_manager.fileselect_add(self)
|
||||
return ExportHelper.invoke(self, context, event)
|
||||
return self.execute(context)
|
||||
|
||||
@classmethod
|
||||
@@ -613,18 +674,30 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"IfcUnitAssignment",
|
||||
"IfcVirtualGridIntersection",
|
||||
]
|
||||
whitelist_exceptions = {
|
||||
# Document references for sheet elements (drawings, schedules, etc).
|
||||
"IfcExternalReference": ("IfcDocumentReference",),
|
||||
}
|
||||
|
||||
total_purged = 0
|
||||
schema = tool.Ifc.schema()
|
||||
while True:
|
||||
total_batches = 0
|
||||
print("*" * 100)
|
||||
total_batch_purged = 0
|
||||
for ifc_class in whitelisted_classes:
|
||||
total_class_purged = 0
|
||||
|
||||
# Ensure class is present in the schema.
|
||||
try:
|
||||
elements = tool.Ifc.get().by_type(ifc_class)
|
||||
except:
|
||||
continue # Probably not in this schema?
|
||||
schema.declaration_by_name(ifc_class)
|
||||
except RuntimeError:
|
||||
continue
|
||||
|
||||
elements = tool.Ifc.get().by_type(ifc_class)
|
||||
if ifc_class in whitelist_exceptions:
|
||||
elements = [e for e in elements if not any(e.is_a(c) for c in whitelist_exceptions[ifc_class])]
|
||||
|
||||
to_purge = set()
|
||||
for element in elements:
|
||||
try:
|
||||
@@ -691,13 +764,13 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if purged == 0:
|
||||
return
|
||||
|
||||
scene = context.scene
|
||||
if object_type == "PROFILE":
|
||||
props = tool.Profile.get_profile_props()
|
||||
if props.is_editing:
|
||||
bpy.ops.bim.load_profiles()
|
||||
elif object_type == "STYLE":
|
||||
if scene.BIMStylesProperties.is_editing:
|
||||
props = tool.Style.get_style_props()
|
||||
if props.is_editing:
|
||||
bpy.ops.bim.load_styles()
|
||||
elif object_type == "MATERIAL":
|
||||
props = tool.Material.get_material_props()
|
||||
@@ -741,13 +814,13 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if merged == 0:
|
||||
return
|
||||
|
||||
scene = context.scene
|
||||
if object_type == "PROFILE":
|
||||
props = tool.Profile.get_profile_props()
|
||||
if props.is_editing:
|
||||
bpy.ops.bim.load_profiles()
|
||||
elif object_type == "STYLE":
|
||||
if scene.BIMStylesProperties.is_editing:
|
||||
props = tool.Style.get_style_props()
|
||||
if props.is_editing:
|
||||
bpy.ops.bim.load_styles()
|
||||
elif object_type == "MATERIAL":
|
||||
props = tool.Material.get_material_props()
|
||||
@@ -950,7 +1023,12 @@ class RestartBlender(bpy.types.Operator):
|
||||
ms_store_app_id = tool.Blender.get_microsoft_store_app_id()
|
||||
if not ms_store_app_id:
|
||||
path = bpy.app.binary_path
|
||||
os.execv(path, sys.argv)
|
||||
if platform.system() == "Windows":
|
||||
args = sys.argv[1:]
|
||||
command_line = subprocess.list2cmdline([path] + args)
|
||||
os.execv(path, [command_line])
|
||||
else:
|
||||
os.execv(path, sys.argv)
|
||||
else:
|
||||
# Microsoft apps do not allow launching blender.exe directly
|
||||
# since Blender folder is kind of private.
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class DiffData:
|
||||
|
||||
@classmethod
|
||||
def diff_json(cls):
|
||||
props = bpy.context.scene.DiffProperties
|
||||
props = tool.Blender.get_diff_props()
|
||||
if not props.diff_json_file:
|
||||
cls.diff = None
|
||||
return
|
||||
|
||||
@@ -24,24 +24,23 @@ import ifcopenshell
|
||||
import bonsai.bim.handler
|
||||
import bonsai.bim.import_ifc
|
||||
import bonsai.tool as tool
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
|
||||
|
||||
class SelectDiffJsonFile(bpy.types.Operator):
|
||||
class SelectDiffJsonFile(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_diff_json_file"
|
||||
bl_label = "Select Diff JSON File"
|
||||
bl_description = "Select filepath for IFC diff results."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
filename_ext = ".json"
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.DiffProperties.diff_json_file = self.filepath
|
||||
props = tool.Blender.get_diff_props()
|
||||
props.diff_json_file = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class VisualiseDiff(bpy.types.Operator):
|
||||
bl_idname = "bim.visualise_diff"
|
||||
@@ -50,7 +49,8 @@ class VisualiseDiff(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
ifc_file = tool.Ifc.get()
|
||||
with open(context.scene.DiffProperties.diff_json_file, "r") as file:
|
||||
props = tool.Blender.get_diff_props()
|
||||
with open(props.diff_json_file, "r") as file:
|
||||
diff = json.load(file)
|
||||
for obj in context.visible_objects:
|
||||
obj.color = (1.0, 1.0, 1.0, 1.0)
|
||||
@@ -65,7 +65,7 @@ class VisualiseDiff(bpy.types.Operator):
|
||||
obj.color = (0.0, 0.0, 0.7, 1.0)
|
||||
continue
|
||||
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
|
||||
continue
|
||||
|
||||
ifc_file = ""
|
||||
@@ -84,7 +84,7 @@ class VisualiseDiff(bpy.types.Operator):
|
||||
element_file = ifc_file
|
||||
|
||||
try:
|
||||
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
element = element_file.by_id(ifc_id)
|
||||
except:
|
||||
continue
|
||||
global_id = getattr(element, "GlobalId", None)
|
||||
@@ -101,55 +101,45 @@ class VisualiseDiff(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectDiffOldFile(bpy.types.Operator):
|
||||
class SelectDiffOldFile(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_diff_old_file"
|
||||
bl_label = "Select Diff Old File"
|
||||
bl_description = "Select filepath for an old IFC file to compare."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
|
||||
filename_ext = ".ifc"
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.DiffProperties.old_file = self.filepath
|
||||
props = tool.Blender.get_diff_props()
|
||||
props.old_file = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class SelectDiffNewFile(bpy.types.Operator):
|
||||
class SelectDiffNewFile(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_diff_new_file"
|
||||
bl_label = "Select Diff New File"
|
||||
bl_description = "Select filepath for a new IFC file to compare."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
|
||||
filename_ext = ".ifc"
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.DiffProperties.new_file = self.filepath
|
||||
props = tool.Blender.get_diff_props()
|
||||
props.new_file = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class ExecuteIfcDiff(bpy.types.Operator):
|
||||
class ExecuteIfcDiff(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.execute_ifc_diff"
|
||||
bl_label = "Execute IFC Diff"
|
||||
bl_description = "Compare two IFC files and save a json diff report by the provided filepath."
|
||||
filename_ext = ".json"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
import ifcdiff
|
||||
|
||||
self.props = context.scene.DiffProperties
|
||||
self.props = tool.Blender.get_diff_props()
|
||||
|
||||
if tool.Ifc.get():
|
||||
if self.props.active_file == "NONE":
|
||||
@@ -238,7 +228,8 @@ class SelectDiffObjects(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
ifc_file = tool.Ifc.get()
|
||||
with open(context.scene.DiffProperties.diff_json_file, "r") as file:
|
||||
props = tool.Blender.get_diff_props()
|
||||
with open(props.diff_json_file, "r") as file:
|
||||
diff = json.load(file)
|
||||
for obj in context.visible_objects:
|
||||
obj.select_set(False)
|
||||
@@ -253,7 +244,7 @@ class SelectDiffObjects(bpy.types.Operator):
|
||||
obj.select_set(True)
|
||||
continue
|
||||
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
|
||||
continue
|
||||
|
||||
ifc_file = ""
|
||||
@@ -272,7 +263,7 @@ class SelectDiffObjects(bpy.types.Operator):
|
||||
element_file = ifc_file
|
||||
|
||||
try:
|
||||
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
element = element_file.by_id(ifc_id)
|
||||
except:
|
||||
continue
|
||||
global_id = getattr(element, "GlobalId", None)
|
||||
|
||||
@@ -30,18 +30,25 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Literal, get_args
|
||||
|
||||
|
||||
def update_diff_json_file(self, context):
|
||||
def update_diff_json_file(self: "DiffProperties", context: bpy.types.Context) -> None:
|
||||
DiffData.data["diff_json"] = DiffData.diff_json()
|
||||
|
||||
|
||||
RelationshipType = Literal["type", "property", "container", "aggregate", "classification"]
|
||||
|
||||
|
||||
class Relationships(PropertyGroup):
|
||||
relationship: EnumProperty(
|
||||
name="Relationship",
|
||||
items=[(r, r.capitalize(), r) for r in ["type", "property", "container", "aggregate", "classification"]],
|
||||
items=[(r, r.capitalize(), r) for r in get_args(RelationshipType)],
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
relationship: RelationshipType
|
||||
|
||||
|
||||
class DiffProperties(PropertyGroup):
|
||||
diff_json_file: StringProperty(default="", name="JSON Output", update=update_diff_json_file)
|
||||
@@ -59,3 +66,12 @@ class DiffProperties(PropertyGroup):
|
||||
name="Active File",
|
||||
default="NEW",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
diff_json_file: str
|
||||
old_file: str
|
||||
new_file: str
|
||||
diff_relationships: bpy.types.bpy_prop_collection_idprop[Relationships]
|
||||
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
|
||||
should_load_changed_elements: bool
|
||||
active_file: Literal["NONE", "OLD", "NEW"]
|
||||
|
||||
@@ -37,9 +37,7 @@ class BIM_PT_diff(Panel):
|
||||
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
|
||||
scene = context.scene
|
||||
props = scene.DiffProperties
|
||||
props = tool.Blender.get_diff_props()
|
||||
|
||||
layout.label(text="IFC Diff Setup:")
|
||||
|
||||
|
||||
@@ -90,18 +90,20 @@ class BIM_PT_object_documents(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
if not (obj := context.active_object):
|
||||
return False
|
||||
if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id):
|
||||
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
|
||||
return False
|
||||
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
|
||||
if not tool.Ifc.get_object_by_identifier(ifc_id):
|
||||
return False
|
||||
return True
|
||||
|
||||
def draw(self, context):
|
||||
if not ObjectDocumentData.is_loaded:
|
||||
ObjectDocumentData.load()
|
||||
|
||||
obj = context.active_object
|
||||
self.oprops = obj.BIMObjectProperties
|
||||
self.oprops = tool.Blender.get_object_bim_props(obj)
|
||||
self.props = tool.Document.get_document_props()
|
||||
self.file = tool.Ifc.get()
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class Annotator:
|
||||
curve.font = font
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
props.font_size = "2.5"
|
||||
collection = bpy.context.scene.camera.BIMObjectProperties.collection
|
||||
collection = tool.Blender.get_object_bim_props(bpy.context.scene.camera).collection
|
||||
collection.objects.link(obj)
|
||||
Annotator.resize_text(obj)
|
||||
return obj
|
||||
@@ -133,7 +133,7 @@ class Annotator:
|
||||
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
|
||||
matrix_world = tool.Drawing.get_camera_matrix(camera)
|
||||
matrix_world.translation = co1
|
||||
collection = camera.BIMObjectProperties.collection
|
||||
collection = tool.Blender.get_object_bim_props(camera).collection
|
||||
|
||||
if object_type == "TEXT":
|
||||
obj = bpy.data.objects.new(object_type, None)
|
||||
|
||||
@@ -236,7 +236,8 @@ class DecoratorData:
|
||||
# stores 1 type of data per object
|
||||
data = {}
|
||||
cut_cache = {}
|
||||
layerset_cache = {}
|
||||
slice_cache = {}
|
||||
fill_cache = {}
|
||||
|
||||
@classmethod
|
||||
def get_batting_thickness(cls, obj):
|
||||
@@ -386,6 +387,8 @@ class DecoratorData:
|
||||
suppress_zero_inches = pset_data.get("SuppressZeroInches", False)
|
||||
text_prefix = pset_data.get("TextPrefix", None) or ""
|
||||
text_suffix = pset_data.get("TextSuffix", None) or ""
|
||||
custom_unit_list = pset_data.get("CustomUnit", None) or ""
|
||||
custom_unit = custom_unit_list[0] if custom_unit_list else ""
|
||||
|
||||
dimension_data = {
|
||||
"dimension_style": dimension_style,
|
||||
@@ -394,6 +397,7 @@ class DecoratorData:
|
||||
"text_prefix": text_prefix,
|
||||
"text_suffix": text_suffix,
|
||||
"fill_bg": fill_bg,
|
||||
"custom_unit": custom_unit,
|
||||
}
|
||||
cls.data[obj.name] = dimension_data
|
||||
return dimension_data
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import gpu
|
||||
import bpy
|
||||
import blf
|
||||
@@ -29,7 +28,7 @@ import ifcopenshell.util.unit
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.module.drawing.helper as helper
|
||||
from pathlib import Path
|
||||
from math import pi, sin, cos, tan, acos, atan, degrees, radians, ceil
|
||||
from math import pi, sin, cos, acos, atan, degrees, radians
|
||||
from bpy.types import SpaceView3D
|
||||
from mathutils import Vector, Matrix
|
||||
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
||||
@@ -319,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)
|
||||
@@ -495,14 +496,20 @@ class BaseDecorator:
|
||||
self.draw_label(context, text=text, line_no=line_number_start, multiline=True, **draw_label_kwargs)
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def format_value(self, context, value):
|
||||
def format_value(self, context, value, custom_unit=None):
|
||||
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
|
||||
precision = drawing_pset_data.get("MetricPrecision", None)
|
||||
if not precision:
|
||||
precision = drawing_pset_data.get("ImperialPrecision", None)
|
||||
|
||||
decimal_places = drawing_pset_data.get("DecimalPlaces", None)
|
||||
return format_distance(value, precision=precision, decimal_places=decimal_places)
|
||||
return format_distance(
|
||||
value,
|
||||
precision=precision,
|
||||
decimal_places=decimal_places,
|
||||
suppress_zero_inches=True,
|
||||
custom_unit=custom_unit,
|
||||
)
|
||||
|
||||
def draw_asterisk(self, context: bpy.types.Context, pos: Vector, rotation: float = 0.0, scale: float = 1.0) -> None:
|
||||
"""`pos` is a world space position\n
|
||||
@@ -589,7 +596,8 @@ class BaseDecorator:
|
||||
region3d = context.region_data
|
||||
text_dir = self.get_annotation_direction(context, obj)
|
||||
|
||||
pos = location_3d_to_region_2d(region, region3d, text_world_position)
|
||||
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)
|
||||
if props.is_editing:
|
||||
@@ -722,6 +730,8 @@ class DimensionDecorator(BaseDecorator):
|
||||
v1 = Vector(vertices[i1])
|
||||
p0 = location_3d_to_region_2d(region, region3d, v0)
|
||||
p1 = location_3d_to_region_2d(region, region3d, v1)
|
||||
if not p0 or not p1:
|
||||
return
|
||||
text_dir = p1 - p0
|
||||
if text_dir.length < 1:
|
||||
continue
|
||||
@@ -737,7 +747,7 @@ class DimensionDecorator(BaseDecorator):
|
||||
|
||||
if not show_description_only:
|
||||
length = (v1 - v0).length
|
||||
text = self.format_value(context, length)
|
||||
text = self.format_value(context, length, custom_unit=dimension_data["custom_unit"])
|
||||
if isinstance(self, DiameterDecorator):
|
||||
text = "D" + text
|
||||
text = text_prefix + text + text_suffix
|
||||
@@ -872,6 +882,8 @@ class AngleDecorator(BaseDecorator):
|
||||
|
||||
# calculate angle position
|
||||
p0, p1, p2 = [location_3d_to_region_2d(region, region3d, p) for p in vertices[i0 : i1 + 2]]
|
||||
if not p0 or not p1 or not p2:
|
||||
continue
|
||||
edge0 = p0 - p1
|
||||
edge1 = p2 - p1
|
||||
radius = min(edge0.length_squared, edge1.length_squared) ** 0.5
|
||||
@@ -936,6 +948,8 @@ class RadiusDecorator(BaseDecorator):
|
||||
spline_points = self.get_spline_points(obj)
|
||||
|
||||
p0, p1 = [location_3d_to_region_2d(region, region3d, p) for p in self.get_spline_points(obj)]
|
||||
if not p0 or not p1:
|
||||
return
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
description = element.Description
|
||||
dimension_data = DecoratorData.get_dimension_data(obj)
|
||||
@@ -946,7 +960,7 @@ class RadiusDecorator(BaseDecorator):
|
||||
|
||||
def get_text():
|
||||
length = (spline_points[-1] - spline_points[-2]).length
|
||||
return "R" + self.format_value(context, length)
|
||||
return "R" + self.format_value(context, length, custom_unit=dimension_data["custom_unit"])
|
||||
|
||||
self.draw_dimension_text(
|
||||
context, get_text, description, dimension_data, pos=pos, text_dir=Vector((1, 0)), box_alignment="center"
|
||||
@@ -968,7 +982,8 @@ class FallDecorator(BaseDecorator):
|
||||
region = context.region
|
||||
region3d = context.region_data
|
||||
dir = Vector((1, 0))
|
||||
pos = location_3d_to_region_2d(region, region3d, self.get_spline_end(obj))
|
||||
if not (pos := location_3d_to_region_2d(region, region3d, self.get_spline_end(obj))):
|
||||
return
|
||||
|
||||
spline = obj.data.splines[0]
|
||||
spline_points = spline.bezier_points if spline.bezier_points else spline.points
|
||||
@@ -1159,7 +1174,6 @@ class PlanLevelDecorator(BaseDecorator):
|
||||
self.draw_labels(context, obj, self.get_splines(obj))
|
||||
|
||||
def draw_labels(self, context, obj, splines):
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
region = context.region
|
||||
region3d = context.region_data
|
||||
|
||||
@@ -1169,6 +1183,8 @@ class PlanLevelDecorator(BaseDecorator):
|
||||
|
||||
for verts in splines:
|
||||
p0, p1 = [location_3d_to_region_2d(region, region3d, v) for v in verts[:2]]
|
||||
if not p0 or not p1:
|
||||
continue
|
||||
text_dir = p1 - p0
|
||||
if text_dir.length < 1:
|
||||
continue
|
||||
@@ -1435,8 +1451,10 @@ class GridDecorator(BaseDecorator):
|
||||
p1 = location_3d_to_region_2d(region, region3d, v1)
|
||||
dir = Vector((1, 0))
|
||||
text = obj.name.split("/")[1].split(".")[0]
|
||||
self.draw_label(context, text, p0, dir, vcenter=True, gap=0)
|
||||
self.draw_label(context, text, p1, dir, vcenter=True, gap=0)
|
||||
if p0:
|
||||
self.draw_label(context, text, p0, dir, vcenter=True, gap=0)
|
||||
if p1:
|
||||
self.draw_label(context, text, p1, dir, vcenter=True, gap=0)
|
||||
|
||||
|
||||
class ElevationDecorator(BaseDecorator):
|
||||
@@ -1589,6 +1607,9 @@ class CutDecorator:
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
DecoratorData.cut_cache.clear()
|
||||
DecoratorData.slice_cache.clear()
|
||||
DecoratorData.fill_cache.clear()
|
||||
if cls.installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
@@ -1603,38 +1624,50 @@ class CutDecorator:
|
||||
cls.installed = None
|
||||
|
||||
def __call__(self, context):
|
||||
if not context.scene.camera:
|
||||
return
|
||||
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
self.fallback_colour = (0.3, 0.3, 0.3, 1)
|
||||
|
||||
all_vertices = []
|
||||
all_edges = []
|
||||
selected_vertices = []
|
||||
selected_edges = []
|
||||
layer_vertices = []
|
||||
layer_edges = []
|
||||
fills = {}
|
||||
all_vertex_i_offset = 0
|
||||
selected_vertex_i_offset = 0
|
||||
layer_vertex_i_offset = 0
|
||||
|
||||
for obj in [o for o in bpy.context.visible_objects if o.type == "MESH"]:
|
||||
verts, edges = self.decorate(context, obj)
|
||||
if not verts:
|
||||
if not (element := tool.Ifc.get_entity(obj)):
|
||||
continue
|
||||
self.decorate(context, obj, element)
|
||||
|
||||
obj_layer_verts, obj_layer_edges = self.slice_layersets(context, obj, verts, edges)
|
||||
if obj_layer_verts:
|
||||
layer_vertices.extend(obj_layer_verts)
|
||||
layer_edges.extend([[vi + layer_vertex_i_offset for vi in e] for e in obj_layer_edges])
|
||||
layer_vertex_i_offset += len(obj_layer_verts)
|
||||
verts, edges = DecoratorData.cut_cache[element.id()]
|
||||
if verts:
|
||||
if obj.select_get():
|
||||
selected_vertices.extend(verts)
|
||||
selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges])
|
||||
selected_vertex_i_offset += len(verts)
|
||||
else:
|
||||
all_vertices.extend(verts)
|
||||
all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges])
|
||||
all_vertex_i_offset += len(verts)
|
||||
|
||||
if obj.select_get():
|
||||
selected_vertices.extend(verts)
|
||||
selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges])
|
||||
selected_vertex_i_offset += len(verts)
|
||||
else:
|
||||
all_vertices.extend(verts)
|
||||
all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges])
|
||||
all_vertex_i_offset += len(verts)
|
||||
verts, edges = DecoratorData.slice_cache.get(element.id(), (None, None))
|
||||
if verts:
|
||||
if obj.select_get():
|
||||
selected_vertices.extend(verts)
|
||||
selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges])
|
||||
selected_vertex_i_offset += len(verts)
|
||||
else:
|
||||
all_vertices.extend(verts)
|
||||
all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges])
|
||||
all_vertex_i_offset += len(verts)
|
||||
|
||||
for colour, element_fills in DecoratorData.fill_cache[element.id()].items():
|
||||
fills.setdefault(colour, []).append(element_fills)
|
||||
|
||||
gpu.state.point_size_set(1)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
@@ -1652,9 +1685,10 @@ class CutDecorator:
|
||||
|
||||
black = (0, 0, 0, 1)
|
||||
|
||||
if layer_vertices:
|
||||
self.draw_batch("LINES", layer_vertices, black, layer_edges)
|
||||
self.draw_batch("POINTS", layer_vertices, black)
|
||||
for colour, element_fills in fills.items():
|
||||
for verts_tris in element_fills:
|
||||
for verts, tris in verts_tris:
|
||||
self.draw_batch("TRIS", verts, colour, tris)
|
||||
|
||||
gpu.state.point_size_set(2)
|
||||
self.line_shader.uniform_float("lineWidth", 3.0)
|
||||
@@ -1667,315 +1701,183 @@ class CutDecorator:
|
||||
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def decorate(self, context, obj):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return None, None
|
||||
def decorate(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
has_cut_cache = element.id() in DecoratorData.cut_cache
|
||||
has_fill_cache = element.id() in DecoratorData.fill_cache
|
||||
|
||||
# Currently selected objects shall not be cached as they may be being moved / edited.
|
||||
# If the camera is selected, we also disable the cache as the user may be moving the camera.
|
||||
if obj.select_get() or context.scene.camera.select_get():
|
||||
verts, edges = None, None
|
||||
else:
|
||||
verts, edges = DecoratorData.cut_cache.get(element.id(), (None, None))
|
||||
# Currently selected objects must be recalculated as they may be being moved / edited.
|
||||
# If the camera is selected, we also recalculate as the user may be moving the camera.
|
||||
|
||||
if verts is False:
|
||||
return None, None
|
||||
elif verts:
|
||||
return verts, edges
|
||||
if not has_cut_cache or obj.select_get() or context.scene.camera.select_get():
|
||||
self.recalculate_cut(context, obj, element)
|
||||
if not has_fill_cache or obj.select_get() or context.scene.camera.select_get():
|
||||
self.recalculate_fill(context, obj, element)
|
||||
|
||||
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
|
||||
DecoratorData.cut_cache[element.id()] = (False, False)
|
||||
return None, None
|
||||
|
||||
if verts is None:
|
||||
else:
|
||||
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
|
||||
DecoratorData.cut_cache[element.id()] = (verts, edges)
|
||||
return verts, edges
|
||||
|
||||
def slice_layersets(self, context, obj, cut_verts, cut_edges):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
# Currently selected objects shall not be cached as they may be being moved / edited.
|
||||
# If the camera is selected, we also disable the cache as the user may be moving the camera.
|
||||
if obj.select_get() or context.scene.camera.select_get():
|
||||
verts, edges = None, None
|
||||
else:
|
||||
verts, edges = DecoratorData.layerset_cache.get(element.id(), (None, None))
|
||||
|
||||
if verts is False:
|
||||
return None, None
|
||||
elif verts is not None:
|
||||
return verts, edges
|
||||
def recalculate_fill(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
element_id = element.id()
|
||||
|
||||
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
|
||||
DecoratorData.layerset_cache[element.id()] = (False, False)
|
||||
return None, None
|
||||
DecoratorData.fill_cache[element_id] = {}
|
||||
return
|
||||
|
||||
if tool.Model.get_usage_type(element) != "LAYER2":
|
||||
DecoratorData.layerset_cache[element.id()] = (False, False)
|
||||
return None, None
|
||||
DecoratorData.fill_cache[element_id] = {}
|
||||
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
layers = self.get_layer_data(element)
|
||||
mesh = obj.data
|
||||
bm_original = bmesh.new()
|
||||
bm_original.from_mesh(mesh)
|
||||
|
||||
if not layers:
|
||||
DecoratorData.layerset_cache[element.id()] = (False, False)
|
||||
return None, None
|
||||
|
||||
minx = min([co[0] for co in obj.bound_box])
|
||||
maxx = max([co[0] for co in obj.bound_box])
|
||||
min_edge = [Vector((minx, layers["offset"])), Vector((maxx, layers["offset"]))]
|
||||
max_edge = [
|
||||
Vector((minx, layers["offset"] + layers["thickness"])),
|
||||
Vector((maxx, layers["offset"] + layers["thickness"])),
|
||||
]
|
||||
centerline = [
|
||||
Vector((minx, layers["offset"] + layers["thickness"] / 2)),
|
||||
Vector((maxx, layers["offset"] + layers["thickness"] / 2)),
|
||||
]
|
||||
connections = self.get_connections(element, obj, centerline, min_edge, max_edge)
|
||||
|
||||
start_point = None
|
||||
end_point = None
|
||||
if connections["ATSTART"]:
|
||||
boundary_edge = min_edge if connections["ATSTART"]["angle"] > 0 else max_edge
|
||||
rel_dir = connections["ATSTART"]["centerline"][1] - connections["ATSTART"]["centerline"][0]
|
||||
offset, _ = tool.Cad.intersect_edges(boundary_edge, [centerline[0], centerline[0] + rel_dir])
|
||||
offset_x = (offset - centerline[0]).length + abs(offset.x)
|
||||
intersect, _ = tool.Cad.intersect_edges(boundary_edge, connections["ATSTART"]["centerline"])
|
||||
if tool.Cad.is_point_on_edge(intersect, boundary_edge):
|
||||
centerline[0] = centerline[0] + Vector((offset_x, 0))
|
||||
start_point = centerline[0] + rel_dir
|
||||
if connections["ATEND"]:
|
||||
boundary_edge = min_edge if connections["ATEND"]["angle"] > 0 else max_edge
|
||||
rel_dir = connections["ATEND"]["centerline"][1] - connections["ATEND"]["centerline"][0]
|
||||
offset, _ = tool.Cad.intersect_edges(boundary_edge, [centerline[1], centerline[1] + rel_dir])
|
||||
|
||||
offset_x = (offset - centerline[1]).length + abs((maxx - abs(offset.x)))
|
||||
intersect, _ = tool.Cad.intersect_edges(boundary_edge, connections["ATEND"]["centerline"])
|
||||
if tool.Cad.is_point_on_edge(intersect, boundary_edge):
|
||||
centerline[1] = centerline[1] - Vector((offset_x, 0))
|
||||
end_point = centerline[1] + rel_dir
|
||||
|
||||
min_segments = self.get_segments(connections["MINPATH"], centerline, start_point, end_point)
|
||||
max_segments = self.get_segments(connections["MAXPATH"], centerline, start_point, end_point)
|
||||
|
||||
final_segments = []
|
||||
for segment in min_segments:
|
||||
segment_line = shapely.LineString([co for co in segment])
|
||||
for distance in layers["min_layers"]:
|
||||
distance *= -1
|
||||
layer_line = shapely.offset_curve(segment_line, distance, join_style=shapely.BufferJoinStyle.mitre)
|
||||
final_segments.append(list(layer_line.coords))
|
||||
for segment in max_segments:
|
||||
segment_line = shapely.LineString([co for co in segment])
|
||||
for distance in layers["max_layers"]:
|
||||
layer_line = shapely.offset_curve(segment_line, distance, join_style=shapely.BufferJoinStyle.mitre)
|
||||
final_segments.append(list(layer_line.coords))
|
||||
|
||||
# Extrude and bisect layers
|
||||
bm = bmesh.new()
|
||||
verts = []
|
||||
edges = []
|
||||
offset = 0
|
||||
for segment in final_segments:
|
||||
if not segment:
|
||||
# Why does this occur?
|
||||
continue
|
||||
if isinstance(segment[0], tuple):
|
||||
segment = [Vector(co) for co in segment]
|
||||
verts.extend([bm.verts.new(co.to_3d()) for co in segment])
|
||||
[bm.edges.new((verts[i + offset], verts[i + 1 + offset])) for i in range(0, len(segment) - 1)]
|
||||
offset += len(segment)
|
||||
|
||||
extrusion_dir = (0, 0, 3)
|
||||
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=bm.edges[:])
|
||||
bmesh.ops.translate(
|
||||
bm, vec=extrusion_dir, verts=[v for v in extruded_geom["geom"] if isinstance(v, bmesh.types.BMVert)]
|
||||
)
|
||||
|
||||
verts, edges = self.bisect_mesh(obj, bm, context.scene.camera)
|
||||
layer_linestrings = [shapely.LineString((verts[e[0]], verts[e[1]])) for e in edges]
|
||||
|
||||
clipped_linestrings = []
|
||||
|
||||
polygons = shapely.polygonize([shapely.LineString((cut_verts[e[0]], cut_verts[e[1]])) for e in cut_edges])
|
||||
for polygon in polygons.geoms:
|
||||
clipped_linestrings.extend([polygon.intersection(ls) for ls in layer_linestrings])
|
||||
|
||||
verts = []
|
||||
edges = []
|
||||
offset = 0
|
||||
for linestring in clipped_linestrings:
|
||||
try:
|
||||
linestring = list(linestring.coords)
|
||||
except:
|
||||
print("Failed ... ", linestring)
|
||||
continue
|
||||
verts.extend(linestring)
|
||||
edges.extend([(i + offset, i + 1 + offset) for i in range(0, len(linestring) - 1)])
|
||||
offset += len(linestring)
|
||||
|
||||
DecoratorData.layerset_cache[element.id()] = (verts, edges)
|
||||
return verts, edges
|
||||
|
||||
def bisect_mesh(self, obj, bm, camera):
|
||||
camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world
|
||||
# Slice our mesh into a 2D drawing cut (2D is always easier)
|
||||
camera_matrix = obj.matrix_world.inverted() @ context.scene.camera.matrix_world
|
||||
global_offset = context.scene.camera.matrix_world.col[2].xyz * -context.scene.camera.data.clip_start
|
||||
plane_co = camera_matrix.translation
|
||||
plane_no = camera_matrix.col[2].xyz
|
||||
geom = bm_original.verts[:] + bm_original.edges[:] + bm_original.faces[:]
|
||||
bmesh.ops.bisect_plane(
|
||||
bm_original,
|
||||
geom=geom,
|
||||
dist=0.0001,
|
||||
plane_co=plane_co,
|
||||
plane_no=plane_no,
|
||||
clear_outer=True,
|
||||
clear_inner=True,
|
||||
)
|
||||
bmesh.ops.remove_doubles(bm_original, verts=bm_original.verts, dist=0.000001)
|
||||
bmesh.ops.triangle_fill(bm_original, use_dissolve=True, edges=bm_original.edges)
|
||||
|
||||
global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start
|
||||
if not (material := ifcopenshell.util.element.get_material(element)):
|
||||
verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera)
|
||||
DecoratorData.fill_cache[element_id].setdefault(self.fallback_colour, []).append((verts, tris))
|
||||
return
|
||||
|
||||
# Run the bisect operation
|
||||
geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
|
||||
results = bmesh.ops.bisect_plane(bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no)
|
||||
if material.is_a() not in ("IfcMaterialLayerSet", "IfcMaterialLayerSetUsage"):
|
||||
# Constituents, lists, and item styles not supported yet
|
||||
material = ifcopenshell.util.element.get_materials(element)[0]
|
||||
verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera)
|
||||
colour = self.get_material_colour(material)
|
||||
DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris))
|
||||
return
|
||||
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
if material.is_a("IfcMaterialLayerSetUsage"):
|
||||
usage = material
|
||||
layer_set = material.ForLayerSet
|
||||
offset = usage.OffsetFromReferenceLine * self.unit_scale
|
||||
sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1
|
||||
elif material.is_a("IfcMaterialLayerSet"):
|
||||
usage = None
|
||||
layer_set = material
|
||||
offset = 0
|
||||
sense_factor = 1
|
||||
|
||||
if len(layer_set.MaterialLayers) == 1:
|
||||
material = layer_set.MaterialLayers[0].Material
|
||||
verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera)
|
||||
colour = self.get_material_colour(material)
|
||||
DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris))
|
||||
return
|
||||
|
||||
bm = bm_original.copy()
|
||||
prev_co = None
|
||||
if not usage:
|
||||
sense_factor = 1 # Assume the extrusion vector points in the direction sense
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
elif usage.LayerSetDirection == "AXIS2":
|
||||
co = Vector((0.0, offset, 0.0))
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = no.cross(Vector([1.0, 0.0, 0.0]))
|
||||
elif usage.LayerSetDirection == "AXIS3":
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = Vector([0.0, 0.0, 1.0])
|
||||
elif usage.LayerSetDirection == "AXIS1":
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = Vector([1.0, 0.0, 0.0])
|
||||
no *= sense_factor
|
||||
last_i = len(layer_set.MaterialLayers) - 1
|
||||
|
||||
vert_map = {}
|
||||
verts = []
|
||||
edges = []
|
||||
j = 0
|
||||
|
||||
for i, layer in enumerate(layer_set.MaterialLayers):
|
||||
prev_co = co.copy()
|
||||
co += no * layer.LayerThickness * self.unit_scale
|
||||
if i != last_i:
|
||||
bisect = bmesh.ops.bisect_plane(
|
||||
bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no
|
||||
)
|
||||
for geom in bisect["geom_cut"]:
|
||||
if isinstance(geom, bmesh.types.BMVert):
|
||||
verts.append(tuple((obj.matrix_world @ geom.co) + global_offset))
|
||||
vert_map[geom.index] = j
|
||||
j += 1
|
||||
else:
|
||||
# It seems as though edges always appear after verts
|
||||
edges.append([vert_map[v.index] for v in geom.verts])
|
||||
|
||||
colour = self.get_material_colour(layer.Material)
|
||||
bm_fill = bm_original.copy()
|
||||
if i != last_i:
|
||||
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
|
||||
bmesh.ops.bisect_plane(bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True)
|
||||
if i != 0:
|
||||
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
|
||||
bmesh.ops.bisect_plane(bm_fill, geom=geom, dist=0.0001, plane_co=prev_co, plane_no=no, clear_inner=True)
|
||||
|
||||
DecoratorData.fill_cache[element_id].setdefault(colour, []).append(
|
||||
self.get_bmesh_tris(obj, bm_fill, context.scene.camera)
|
||||
)
|
||||
|
||||
DecoratorData.slice_cache[element.id()] = (verts, edges)
|
||||
bm_original.free()
|
||||
|
||||
def get_material_colour(self, material: ifcopenshell.entity_instance) -> tuple[float, float, float, float]:
|
||||
body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
style = ifcopenshell.util.representation.get_material_style(material, body)
|
||||
if not style:
|
||||
return self.fallback_colour
|
||||
if not (styles := [s for s in style.Styles if s.is_a("IfcSurfaceStyleShading")]):
|
||||
return self.fallback_colour
|
||||
colour = styles[0].SurfaceColour
|
||||
return (colour.Red, colour.Green, colour.Blue, 1)
|
||||
|
||||
def get_bmesh_tris(self, obj, bm, camera):
|
||||
global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start
|
||||
triangulate = bmesh.ops.triangulate(bm, faces=bm.faces)
|
||||
vert_map = {}
|
||||
verts = []
|
||||
tris = []
|
||||
i = 0
|
||||
for geom in results["geom_cut"]:
|
||||
if isinstance(geom, bmesh.types.BMVert):
|
||||
verts.append(tuple((obj.matrix_world @ geom.co) + global_offset))
|
||||
vert_map[geom.index] = i
|
||||
i += 1
|
||||
else:
|
||||
# It seems as though edges always appear after verts
|
||||
edges.append([vert_map[v.index] for v in geom.verts])
|
||||
|
||||
bm.free()
|
||||
|
||||
return verts, edges
|
||||
|
||||
def get_layer_data(self, element):
|
||||
usage = ifcopenshell.util.element.get_material(element)
|
||||
offset = usage.OffsetFromReferenceLine * self.unit_scale
|
||||
layer_set = usage.ForLayerSet
|
||||
|
||||
if len(layer_set.MaterialLayers) == 1:
|
||||
return # No use slicing if there's only one layer
|
||||
|
||||
total_thickness = layer_set.TotalThickness
|
||||
half_thickness = total_thickness / 2
|
||||
min_layers = []
|
||||
max_layers = []
|
||||
current_thickness = 0
|
||||
for layer in layer_set.MaterialLayers:
|
||||
current_thickness += layer.LayerThickness
|
||||
if current_thickness / total_thickness < 0.5:
|
||||
min_layers.append((half_thickness - current_thickness) * self.unit_scale)
|
||||
else:
|
||||
max_layers.append((current_thickness - half_thickness) * self.unit_scale)
|
||||
min_layers.reverse()
|
||||
max_layers.pop()
|
||||
if usage.DirectionSense == "NEGATIVE":
|
||||
total_thickness *= -1
|
||||
offset *= -1
|
||||
min_layers = [l * -1 for l in min_layers]
|
||||
max_layers = [l * -1 for l in max_layers]
|
||||
return {
|
||||
"offset": offset,
|
||||
"min_layers": min_layers,
|
||||
"max_layers": max_layers,
|
||||
"thickness": total_thickness * self.unit_scale,
|
||||
}
|
||||
|
||||
def get_segments(self, connections, centerline, start_point, end_point):
|
||||
segments = []
|
||||
total_connections = len(connections)
|
||||
if total_connections:
|
||||
for i in range(0, total_connections + 1):
|
||||
if i == 0:
|
||||
connection = connections[i]
|
||||
segments.append([centerline[0], connection["intersection"], connection["out_point"]])
|
||||
elif i == total_connections:
|
||||
segments.append([segments[-1][-1], segments[-1][-2], centerline[1]])
|
||||
for face in triangulate["faces"]:
|
||||
tri = []
|
||||
for vert in face.verts:
|
||||
if index := vert_map.get(vert.index, None):
|
||||
tri.append(index)
|
||||
else:
|
||||
connection = connections[i]
|
||||
segments.append(
|
||||
[segments[-1][-1], segments[-1][-2], connection["intersection"], connection["out_point"]]
|
||||
)
|
||||
else:
|
||||
segments.append([centerline[0], centerline[1]])
|
||||
if start_point:
|
||||
segments[0].insert(0, start_point)
|
||||
if end_point:
|
||||
segments[-1].append(end_point)
|
||||
return segments
|
||||
|
||||
def get_connections(self, wall, obj, centerline, min_edge, max_edge):
|
||||
connections = {"ATEND": None, "ATSTART": None, "ATPATH": [], "MINPATH": [], "MAXPATH": []}
|
||||
for rel in wall.ConnectedTo:
|
||||
# How do you join to a non layered element? Not sure.
|
||||
if tool.Model.get_usage_type(rel.RelatedElement) != "LAYER2":
|
||||
continue
|
||||
if rel.RelatingConnectionType == "ATPATH":
|
||||
metadata = self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
|
||||
if not metadata:
|
||||
continue
|
||||
connections["ATPATH"].append(metadata)
|
||||
else:
|
||||
metadata = self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
|
||||
if not metadata:
|
||||
continue
|
||||
connections[rel.RelatingConnectionType] = metadata
|
||||
for rel in wall.ConnectedFrom:
|
||||
if tool.Model.get_usage_type(rel.RelatingElement) != "LAYER2":
|
||||
continue
|
||||
# We only consider ATPATH since in this situation, we have the
|
||||
# priority. The non-priority wall never has any layers that need to
|
||||
# "turn a corner".
|
||||
if rel.RelatedConnectionType == "ATPATH":
|
||||
metadata = self.get_connection_metadata(obj, rel.RelatingElement, centerline, min_edge, max_edge)
|
||||
if not metadata:
|
||||
continue
|
||||
connections["ATPATH"].append(metadata)
|
||||
connections["ATPATH"] = sorted(connections["ATPATH"], key=lambda c: c["intersection"].x)
|
||||
for connection in connections["ATPATH"]:
|
||||
if connection["angle"] > 0:
|
||||
connections["MINPATH"].append(connection)
|
||||
else:
|
||||
connections["MAXPATH"].append(connection)
|
||||
return connections
|
||||
|
||||
def get_connection_metadata(self, obj, rel_element, centerline, min_edge, max_edge):
|
||||
rel_obj = tool.Ifc.get_object(rel_element)
|
||||
layers = self.get_layer_data(rel_element)
|
||||
if not layers:
|
||||
return
|
||||
minx = min([co[0] for co in rel_obj.bound_box])
|
||||
maxx = max([co[0] for co in rel_obj.bound_box])
|
||||
rel_centerline = [
|
||||
Vector((minx, layers["offset"] + layers["thickness"] / 2)),
|
||||
Vector((maxx, layers["offset"] + layers["thickness"] / 2)),
|
||||
]
|
||||
rel_centerline = [obj.matrix_world.inverted() @ rel_obj.matrix_world @ v.to_3d() for v in rel_centerline]
|
||||
rel_centerline = [v.to_2d() for v in rel_centerline]
|
||||
intersection = tool.Cad.intersect_edges(centerline, rel_centerline)
|
||||
if intersection:
|
||||
intersection, _ = intersection
|
||||
else:
|
||||
return
|
||||
closest_centerline_point = tool.Cad.closest_vector(intersection, tuple(rel_centerline))
|
||||
if closest_centerline_point == rel_centerline[1]:
|
||||
rel_centerline = [rel_centerline[1], rel_centerline[0]]
|
||||
angle = tool.Cad.angle_edges(centerline, rel_centerline, signed=True)
|
||||
# A little extreme, but maybe it's OK?
|
||||
out_point = tool.Cad.furthest_vector(intersection, tuple(rel_centerline))
|
||||
return {
|
||||
"element": rel_element,
|
||||
"obj": rel_obj,
|
||||
"centerline": rel_centerline,
|
||||
"intersection": intersection,
|
||||
"angle": angle,
|
||||
"out_point": out_point,
|
||||
}
|
||||
verts.append(tuple((obj.matrix_world @ vert.co) + global_offset))
|
||||
vert_map[vert.index] = i
|
||||
tri.append(i)
|
||||
i += 1
|
||||
tris.append(tri)
|
||||
return verts, tris
|
||||
|
||||
|
||||
class DecorationsHandler:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -20,6 +20,7 @@ import bpy
|
||||
import math
|
||||
import mathutils.geometry
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.unit
|
||||
import bonsai.tool as tool
|
||||
from mathutils import Vector
|
||||
from typing import Union
|
||||
@@ -130,18 +131,53 @@ def format_distance(
|
||||
decimal_places=None,
|
||||
suppress_zero_inches=False,
|
||||
in_unit_length=False,
|
||||
custom_unit=None,
|
||||
):
|
||||
s_code = "\u00b2" # Superscript two THIS IS LEGACY (but being kept for when Area Measurements are re-implimented)
|
||||
|
||||
# Get Scene Unit Settings
|
||||
scaleFactor = bpy.context.scene.unit_settings.scale_length
|
||||
# Get Blender Scene Unit Settings
|
||||
unit_scale = bpy.context.scene.unit_settings.scale_length
|
||||
unit_system = bpy.context.scene.unit_settings.system
|
||||
unit_length = bpy.context.scene.unit_settings.length_unit
|
||||
area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol(
|
||||
ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT")
|
||||
)
|
||||
area_unit_symbol = " m2" if unit_system == "METRIC" else " ft2"
|
||||
|
||||
value *= scaleFactor
|
||||
# Get IFC Unit Settings
|
||||
if tool.Ifc.get():
|
||||
unit_scale = 1
|
||||
if length_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "LENGTHUNIT"):
|
||||
unit_system = "METRIC" if length_unit.Name == "METRE" else "IMPERIAL"
|
||||
unit_length = length_unit.Name.upper()
|
||||
if hasattr(length_unit, "Prefix") and length_unit.Prefix:
|
||||
unit_length = length_unit.Prefix + length_unit.Name
|
||||
unit_length_mapping = {
|
||||
"FOOT": "FEET",
|
||||
"INCH": "INCHES",
|
||||
"METRE": "METERS",
|
||||
"DECIMETRE": "DECIMETERS",
|
||||
"CENTIMETRE": "CENTIMETERS",
|
||||
"MILLIMETRE": "MILLIMETERS",
|
||||
}
|
||||
unit_length = unit_length_mapping[unit_length]
|
||||
# For now we only format area in IFC Units
|
||||
if area_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT"):
|
||||
area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol(area_unit)
|
||||
|
||||
unit_fraction = True if unit_system == "IMPERIAL" else False
|
||||
|
||||
# Custom Unit Settings
|
||||
if custom_unit:
|
||||
unit_mapping = {
|
||||
"Feet and Inches - Fractional": ("IMPERIAL", "FEET", True),
|
||||
"Feet - Decimal": ("IMPERIAL", "FEET", False),
|
||||
"Inches - Fractional": ("IMPERIAL", "INCHES", True),
|
||||
"Inches - Decimal": ("IMPERIAL", "INCHES", False),
|
||||
"Meters": ("METRIC", "METERS", False),
|
||||
"Decimeters": ("METRIC", "DECIMETERS", False),
|
||||
"Centimeters": ("METRIC", "CENTIMETERS", False),
|
||||
"Millimeters": ("METRIC", "MILLIMETERS", False),
|
||||
}
|
||||
if custom_unit in unit_mapping:
|
||||
unit_system, unit_length, unit_fraction = unit_mapping[custom_unit]
|
||||
|
||||
value *= unit_scale
|
||||
|
||||
# Imperial Formatting
|
||||
if unit_system == "IMPERIAL":
|
||||
@@ -166,10 +202,14 @@ def format_distance(
|
||||
|
||||
base = int(precision)
|
||||
decInches = value * toInches
|
||||
decFeet = decInches / 12
|
||||
|
||||
# Separate ft and inches
|
||||
# Unless Inches are the specified Length Unit
|
||||
if unit_length != "INCHES":
|
||||
# Unless Inches are the specified Length Unit or unit_fraction is False
|
||||
if unit_length == "FEET" and not unit_fraction:
|
||||
feet = round(decInches / inPerFoot, 3) # keep decimal
|
||||
decInches = 0
|
||||
elif unit_length != "INCHES":
|
||||
feet = int(decInches / inPerFoot) # remove decimal
|
||||
decInches -= feet * inPerFoot
|
||||
else:
|
||||
@@ -204,6 +244,10 @@ def format_distance(
|
||||
feet += 1
|
||||
inches = 0
|
||||
|
||||
# Check whether decimal or fractional
|
||||
if not unit_fraction:
|
||||
inches = round(decInches, 3)
|
||||
frac = None
|
||||
if not isArea:
|
||||
add_inches = bool(inches) or not suppress_zero_inches or (inches == 0 and frac)
|
||||
tx_dist = ""
|
||||
@@ -229,6 +273,8 @@ def format_distance(
|
||||
tx_dist += str(frac) + "/" + str(base)
|
||||
if add_inches or frac:
|
||||
tx_dist += '"'
|
||||
if precision == "12" and unit_system == "IMPERIAL":
|
||||
tx_dist = str(round(decFeet)) + "'"
|
||||
else:
|
||||
fmt = "%1.3f"
|
||||
sq_feet = round(value * toInches / inPerFoot, 4)
|
||||
@@ -252,6 +298,8 @@ def format_distance(
|
||||
# METRIC FORMATTING
|
||||
elif unit_system == "METRIC":
|
||||
if in_unit_length:
|
||||
if unit_length == "DECIMETERS":
|
||||
value = value / 10
|
||||
if unit_length == "CENTIMETERS":
|
||||
value = value / 100
|
||||
if unit_length == "MILLIMETERS":
|
||||
@@ -270,6 +318,14 @@ def format_distance(
|
||||
if hide_units is False:
|
||||
fmt += " m"
|
||||
tx_dist = fmt % value
|
||||
# Decimeters
|
||||
elif unit_length == "DECIMETERS":
|
||||
if decimal_places is None:
|
||||
fmt = "%1.1f"
|
||||
if hide_units is False:
|
||||
fmt += " dm"
|
||||
d_dm = value * (10)
|
||||
tx_dist = fmt % d_dm
|
||||
# Centimeters
|
||||
elif unit_length == "CENTIMETERS":
|
||||
if decimal_places is None:
|
||||
@@ -343,7 +399,7 @@ def get_active_drawing(
|
||||
props = tool.Drawing.get_document_props()
|
||||
try:
|
||||
camera = tool.Ifc.get_object(tool.Ifc.get().by_id(props.active_drawing_id))
|
||||
return camera.BIMObjectProperties.collection, camera
|
||||
return tool.Blender.get_object_bim_props(camera).collection, camera
|
||||
except:
|
||||
return None, None
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import bonsai.bim.module.drawing.svgwriter as svgwriter
|
||||
import bonsai.bim.module.drawing.annotation as annotation
|
||||
import bonsai.bim.module.drawing.sheeter as sheeter
|
||||
import bonsai.bim.export_ifc
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
from bonsai.bim.module.drawing.decoration import CutDecorator
|
||||
from bonsai.bim.module.drawing.data import DecoratorData, DrawingsData
|
||||
from typing import NamedTuple, List, Union, Optional, Literal
|
||||
@@ -246,7 +247,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
self.props = tool.Drawing.get_document_props()
|
||||
|
||||
active_drawing_id = context.scene.camera.BIMObjectProperties.ifc_definition_id
|
||||
active_drawing_id = tool.Blender.get_ifc_definition_id(context.scene.camera)
|
||||
if self.print_all:
|
||||
original_drawing_id = active_drawing_id
|
||||
drawings_to_print = [d.ifc_definition_id for d in self.props.drawings if d.is_selected and d.is_drawing]
|
||||
@@ -391,7 +392,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
bpy.ops.render.render(write_still=True)
|
||||
else:
|
||||
previous_visibility = {}
|
||||
for obj in self.camera.BIMObjectProperties.collection.objects:
|
||||
for obj in tool.Blender.get_object_bim_props(self.camera).collection.objects:
|
||||
if bpy.context.view_layer.objects.get(obj.name):
|
||||
previous_visibility[obj.name] = obj.hide_get()
|
||||
obj.hide_set(True)
|
||||
@@ -562,6 +563,112 @@ class CreateDrawing(bpy.types.Operator):
|
||||
path.attrib["d"] = d
|
||||
group.append(g)
|
||||
|
||||
def generate_wall_layers(self, context: bpy.types.Context, root):
|
||||
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
|
||||
if "projection" in el.get("class", "").split():
|
||||
continue
|
||||
element = self.get_element_by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
|
||||
if not (obj := tool.Ifc.get_object(element)):
|
||||
continue
|
||||
if not (material := ifcopenshell.util.element.get_material(element)):
|
||||
continue
|
||||
if material.is_a() not in ("IfcMaterialLayerSet", "IfcMaterialLayerSetUsage"):
|
||||
continue
|
||||
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
if material.is_a("IfcMaterialLayerSetUsage"):
|
||||
usage = material
|
||||
layer_set = material.ForLayerSet
|
||||
offset = usage.OffsetFromReferenceLine * self.unit_scale
|
||||
sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1
|
||||
elif material.is_a("IfcMaterialLayerSet"):
|
||||
usage = None
|
||||
layer_set = material
|
||||
offset = 0
|
||||
sense_factor = 1
|
||||
|
||||
camera_matrix_i = context.scene.camera.matrix_world.inverted()
|
||||
|
||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||
raw_width, raw_height = self.get_camera_dimensions()
|
||||
x_offset = raw_width / 2
|
||||
y_offset = raw_height / 2
|
||||
svg_scale = self.scale * 1000 # IFC is in meters, SVG is in mm
|
||||
|
||||
mesh = obj.data
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
|
||||
# Slice our mesh into a 2D drawing cut (2D is always easier)
|
||||
camera_matrix = obj.matrix_world.inverted() @ context.scene.camera.matrix_world
|
||||
plane_co = camera_matrix.translation
|
||||
plane_no = camera_matrix.col[2].xyz
|
||||
geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
|
||||
bmesh.ops.bisect_plane(
|
||||
bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no, clear_outer=True, clear_inner=True
|
||||
)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.000001)
|
||||
bmesh.ops.triangle_fill(bm, use_dissolve=True, edges=bm.edges)
|
||||
|
||||
prev_co = None
|
||||
if not usage:
|
||||
sense_factor = 1 # Assume the extrusion vector points in the direction sense
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
elif usage.LayerSetDirection == "AXIS2":
|
||||
co = Vector((0.0, offset, 0.0))
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = no.cross(Vector([1.0, 0.0, 0.0]))
|
||||
elif usage.LayerSetDirection == "AXIS3":
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = Vector([0.0, 0.0, 1.0])
|
||||
elif usage.LayerSetDirection == "AXIS1":
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = Vector([1.0, 0.0, 0.0])
|
||||
no *= sense_factor
|
||||
last_i = len(layer_set.MaterialLayers) - 1
|
||||
for i, layer in enumerate(layer_set.MaterialLayers):
|
||||
prev_co = co.copy()
|
||||
co += no * layer.LayerThickness * self.unit_scale
|
||||
|
||||
bm_fill = bm.copy()
|
||||
if i != last_i:
|
||||
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
|
||||
bmesh.ops.bisect_plane(bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True)
|
||||
if i != 0:
|
||||
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
|
||||
bmesh.ops.bisect_plane(
|
||||
bm_fill, geom=geom, dist=0.0001, plane_co=prev_co, plane_no=no, clear_inner=True
|
||||
)
|
||||
|
||||
bm_fill.verts.ensure_lookup_table()
|
||||
bm_fill.edges.ensure_lookup_table()
|
||||
verts = [tuple(obj.matrix_world @ v.co) for v in bm_fill.verts]
|
||||
edges = [[v.index for v in e.verts] for e in bm_fill.edges]
|
||||
|
||||
g = etree.SubElement(root, "{http://www.w3.org/2000/svg}g")
|
||||
g.attrib["{http://www.ifcopenshell.org/ns}guid"] = element.GlobalId
|
||||
g.attrib["{http://www.ifcopenshell.org/ns}name"] = element.Name or ""
|
||||
g.attrib["{http://www.ifcopenshell.org/ns}layer-id"] = str(layer.id())
|
||||
|
||||
lines = []
|
||||
for edge in edges:
|
||||
start = [o for o in (camera_matrix_i @ Vector(verts[edge[0]])).xy]
|
||||
end = [o for o in (camera_matrix_i @ Vector(verts[edge[1]])).xy]
|
||||
coords = [start, end]
|
||||
d = " ".join(
|
||||
["L{},{}".format((x_offset + p[0]) * svg_scale, (y_offset - p[1]) * svg_scale) for p in coords]
|
||||
)
|
||||
d = "M{}".format(d[1:])
|
||||
path = etree.SubElement(g, "{http://www.w3.org/2000/svg}path")
|
||||
path.attrib["d"] = d
|
||||
group.append(g)
|
||||
|
||||
bm.free()
|
||||
|
||||
def generate_freestyle_linework(self, context: bpy.types.Context) -> str | None:
|
||||
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"):
|
||||
return
|
||||
@@ -692,7 +799,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
exporter = bonsai.bim.export_ifc.IfcExporter(None)
|
||||
exporter.file = tool.Ifc.get()
|
||||
invalidated_elements = exporter.sync_all_objects()
|
||||
invalidated_elements += exporter.sync_edited_objects()
|
||||
invalidated_guids = [e.GlobalId for e in invalidated_elements if hasattr(e, "GlobalId")]
|
||||
if cache := IfcStore.get_cache():
|
||||
[cache.remove(guid) for guid in invalidated_guids]
|
||||
@@ -717,6 +823,8 @@ class CreateDrawing(bpy.types.Operator):
|
||||
|
||||
props = tool.Project.get_project_props()
|
||||
for link in props.links:
|
||||
if not link.is_loaded:
|
||||
continue
|
||||
if link.name not in IfcStore.session_files:
|
||||
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
|
||||
files[link.name] = IfcStore.session_files[link.name]
|
||||
@@ -769,10 +877,12 @@ class CreateDrawing(bpy.types.Operator):
|
||||
if self.camera.data.BIMCameraProperties.cut_mode == "BISECT":
|
||||
self.remove_cut_linework(root)
|
||||
self.generate_bisect_linework(context, root)
|
||||
self.generate_wall_layers(context, root)
|
||||
self.merge_linework_and_add_metadata(root)
|
||||
self.move_elements_to_top(root)
|
||||
elif self.camera.data.BIMCameraProperties.cut_mode == "OPENCASCADE":
|
||||
self.move_projection_to_bottom(root)
|
||||
self.generate_wall_layers(context, root)
|
||||
self.merge_linework_and_add_metadata(root)
|
||||
self.move_elements_to_top(root)
|
||||
|
||||
@@ -1101,7 +1211,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
# the style of the face when running tree.select_ray()
|
||||
# tree.enable_face_styles(True)
|
||||
|
||||
def get_svg_classes(self, element):
|
||||
def get_svg_classes(self, element, layer=None):
|
||||
classes = [element.is_a()]
|
||||
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
||||
material_name = ""
|
||||
@@ -1113,7 +1223,13 @@ class CreateDrawing(bpy.types.Operator):
|
||||
material_name = tool.Drawing.canonicalise_class_name(material_name)
|
||||
classes.append(f"material-{material_name}")
|
||||
else:
|
||||
classes.append(f"material-null")
|
||||
classes.append("material-null")
|
||||
|
||||
if layer:
|
||||
classes.append(layer.is_a())
|
||||
material_name = layer.Material.Name or "null"
|
||||
material_name = tool.Drawing.canonicalise_class_name(material_name)
|
||||
classes.append(f"layer-material-{material_name}")
|
||||
|
||||
for key in self.metadata:
|
||||
value = ifcopenshell.util.selector.get_element_value(element, key)
|
||||
@@ -1152,6 +1268,23 @@ class CreateDrawing(bpy.types.Operator):
|
||||
except:
|
||||
continue
|
||||
|
||||
def get_element_by_id(self, step_id):
|
||||
try:
|
||||
step_id = int(step_id)
|
||||
except:
|
||||
return
|
||||
try:
|
||||
return tool.Ifc.get().by_id(step_id)
|
||||
except:
|
||||
props = tool.Project.get_project_props()
|
||||
for link in props.links:
|
||||
if link.name not in IfcStore.session_files:
|
||||
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
|
||||
try:
|
||||
return IfcStore.session_files[link.name].by_id(step_id)
|
||||
except:
|
||||
continue
|
||||
|
||||
def remove_cut_linework(self, root):
|
||||
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
|
||||
if "projection" not in el.get("class", "").split():
|
||||
@@ -1163,15 +1296,15 @@ class CreateDrawing(bpy.types.Operator):
|
||||
join_criteria = join_criteria.split(",")
|
||||
else:
|
||||
# Drawing convention states that same objects classes with the same material are merged when cut.
|
||||
join_criteria = ["class", "material.Name", "/Pset_.*Common/.Status", "EPset_Status.Status"]
|
||||
join_criteria = ["class", "material.Name", "/Pset_.*Common/.Status", "EPset_Status.Status", "Material.Name"]
|
||||
|
||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||
joined_paths = {}
|
||||
self.is_manifold_cache = {}
|
||||
|
||||
ifc = tool.Ifc.get()
|
||||
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
|
||||
element = self.get_element_by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
|
||||
layer = self.get_element_by_id(el.get("{http://www.ifcopenshell.org/ns}layer-id"))
|
||||
|
||||
if "projection" in el.get("class", "").split():
|
||||
classes = self.get_svg_classes(element)
|
||||
@@ -1179,7 +1312,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
el.set("class", " ".join(classes))
|
||||
continue
|
||||
else:
|
||||
classes = self.get_svg_classes(element)
|
||||
classes = self.get_svg_classes(element, layer)
|
||||
classes.append("cut")
|
||||
el.set("class", " ".join(classes))
|
||||
|
||||
@@ -1188,7 +1321,12 @@ class CreateDrawing(bpy.types.Operator):
|
||||
if not obj: # This is a linked model object. For now, do nothing.
|
||||
continue
|
||||
|
||||
if not self.is_manifold(obj):
|
||||
if (material := ifcopenshell.util.element.get_material(element)) and material.is_a() in (
|
||||
"IfcMaterialLayerSet",
|
||||
"IfcMaterialLayerSetUsage",
|
||||
):
|
||||
pass # These are always manifold
|
||||
elif not self.is_manifold(obj):
|
||||
continue
|
||||
|
||||
# An element group will contain a bunch of paths representing the
|
||||
@@ -1273,6 +1411,15 @@ class CreateDrawing(bpy.types.Operator):
|
||||
else:
|
||||
keys.append(key)
|
||||
|
||||
if layer:
|
||||
for query in join_criteria:
|
||||
key = ifcopenshell.util.selector.get_element_value(layer, query)
|
||||
print("got layer key", query, key)
|
||||
if isinstance(key, (list, tuple)):
|
||||
keys.extend(key)
|
||||
else:
|
||||
keys.append(key)
|
||||
|
||||
hash_keys = hash(tuple(keys))
|
||||
|
||||
if el.findall("{http://www.w3.org/2000/svg}path"):
|
||||
@@ -1289,7 +1436,8 @@ class CreateDrawing(bpy.types.Operator):
|
||||
for path in el.findall("{http://www.w3.org/2000/svg}path"):
|
||||
for subpath in path.attrib["d"].split("M")[1:]:
|
||||
subpath_co = "M" + subpath.strip(" Z")
|
||||
coords = [[float(o) for o in co[1:].split(",")] for co in subpath_co.split()]
|
||||
# Round due to inaccuracies from Blender meshes and bisection
|
||||
coords = [[round(float(o), 3) for o in co[1:].split(",")] for co in subpath_co.split()]
|
||||
if subpath.strip().lower().endswith("z"):
|
||||
coords.append(coords[0])
|
||||
if len(coords) > 2 and coords[0] == coords[-1]:
|
||||
@@ -2020,6 +2168,8 @@ class ActivateDrawingBase:
|
||||
camera_props = camera.data.BIMCameraProperties
|
||||
if camera_props.update_representation(camera):
|
||||
bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="")
|
||||
# See 6452 and 6478.
|
||||
# bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -2040,7 +2190,6 @@ class ActivateDrawing(bpy.types.Operator, ActivateDrawingBase):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = tool.Drawing.get_document_props()
|
||||
if not tool.Drawing.get_active_drawing_item():
|
||||
cls.poll_message_set("No drawing selected.")
|
||||
return False
|
||||
@@ -2070,12 +2219,14 @@ class ActivateDrawingFromSheet(bpy.types.Operator, ActivateDrawingBase):
|
||||
return True
|
||||
|
||||
|
||||
class SelectDocIfcFile(bpy.types.Operator):
|
||||
# TODO: not exposed to the UI.
|
||||
class SelectDocIfcFile(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_doc_ifc_file"
|
||||
bl_label = "Select Documentation IFC File"
|
||||
bl_description = "Selection .ifc file for documentation."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filename_ext = ".ifc"
|
||||
index: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
@@ -2083,10 +2234,6 @@ class SelectDocIfcFile(bpy.types.Operator):
|
||||
props.ifc_files[self.index].name = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class ResizeText(bpy.types.Operator):
|
||||
bl_idname = "bim.resize_text"
|
||||
@@ -2095,7 +2242,7 @@ class ResizeText(bpy.types.Operator):
|
||||
# TODO: check undo redo
|
||||
|
||||
def execute(self, context):
|
||||
for obj in context.scene.camera.BIMObjectProperties.collection.objects:
|
||||
for obj in tool.Blender.get_object_bim_props(context.scene.camera).collection.objects:
|
||||
if isinstance(obj.data, bpy.types.TextCurve):
|
||||
annotation.Annotator.resize_text(obj)
|
||||
return {"FINISHED"}
|
||||
@@ -2465,13 +2612,12 @@ class RemoveSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
core.remove_sheet(tool.Ifc, tool.Drawing, sheet=tool.Ifc.get().by_id(self.sheet))
|
||||
|
||||
|
||||
class AddSchedule(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class AddSchedule(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
bl_idname = "bim.add_schedule"
|
||||
bl_label = "Add Schedule"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Add an .ods, .xls or .xlsx file as a schedule"
|
||||
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.ods;*.xls;*.xlsx", options={"HIDDEN"})
|
||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
|
||||
|
||||
@@ -2479,10 +2625,6 @@ class AddSchedule(bpy.types.Operator, tool.Ifc.Operator):
|
||||
filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)
|
||||
core.add_document(tool.Ifc, tool.Drawing, "SCHEDULE", uri=filepath)
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class RemoveSchedule(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_schedule"
|
||||
@@ -2652,24 +2794,20 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Drawing.import_sheets()
|
||||
|
||||
|
||||
class AddReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class AddReference(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
bl_idname = "bim.add_reference"
|
||||
bl_label = "Add Reference"
|
||||
bl_description = "Import a .svg file to the project as a reference"
|
||||
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
filter_glob: bpy.props.StringProperty(default="*.svg", options={"HIDDEN"})
|
||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
|
||||
filename_ext = ".svg"
|
||||
|
||||
def _execute(self, context):
|
||||
filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)
|
||||
core.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=filepath)
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class RemoveReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_reference"
|
||||
@@ -3152,14 +3290,22 @@ class DisableEditingDrawings(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class ExpandTargetView(bpy.types.Operator):
|
||||
bl_idname = "bim.expand_target_view"
|
||||
bl_label = "Expand Target View"
|
||||
bl_description = "Show views in this category"
|
||||
bl_description = "\nSHIFT+CLICK to expand all view categories "
|
||||
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
target_view: bpy.props.StringProperty()
|
||||
expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Expanding all categories on shift+click.
|
||||
# Make sure to use SKIP_SAVE on property, otherwise it might get stuck (copied from #4771).
|
||||
if event.type == "LEFTMOUSE" and event.shift:
|
||||
self.expand_all = True
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Drawing.get_document_props()
|
||||
for drawing in [d for d in props.drawings if d.target_view == self.target_view]:
|
||||
for drawing in [d for d in props.drawings if self.expand_all or d.target_view == self.target_view]:
|
||||
drawing.is_expanded = True
|
||||
core.load_drawings(tool.Drawing)
|
||||
return {"FINISHED"}
|
||||
@@ -3168,14 +3314,22 @@ class ExpandTargetView(bpy.types.Operator):
|
||||
class ContractTargetView(bpy.types.Operator):
|
||||
bl_idname = "bim.contract_target_view"
|
||||
bl_label = "Contract Target View"
|
||||
bl_description = "Hide views in this category"
|
||||
bl_description = "\n\nSHIFT+CLICK to hide all view categories"
|
||||
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
target_view: bpy.props.StringProperty()
|
||||
contract_all: bpy.props.BoolProperty(name="Contract All", default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Contracting all categories on shift+click.
|
||||
# Make sure to use SKIP_SAVE on property, otherwise it might get stuck (copied from #4771).
|
||||
if event.type == "LEFTMOUSE" and event.shift:
|
||||
self.contract_all = True
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Drawing.get_document_props()
|
||||
for drawing in [d for d in props.drawings if d.target_view == self.target_view]:
|
||||
for drawing in [d for d in props.drawings if self.contract_all or d.target_view == self.target_view]:
|
||||
drawing.is_expanded = False
|
||||
core.load_drawings(tool.Drawing)
|
||||
return {"FINISHED"}
|
||||
@@ -3184,14 +3338,20 @@ class ContractTargetView(bpy.types.Operator):
|
||||
class ExpandSheet(bpy.types.Operator):
|
||||
bl_idname = "bim.expand_sheet"
|
||||
bl_label = "Expand Sheet"
|
||||
bl_description = "Show views, schedules, references etc\nplaced on this sheet"
|
||||
|
||||
bl_description = "Show views, schedules, references etc\nplaced on this sheet.\n\nShift+click to expand all sheets."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
sheet: bpy.props.IntProperty()
|
||||
expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.type == "LEFTMOUSE" and event.shift:
|
||||
self.expand_all = True
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Drawing.get_document_props()
|
||||
for sheet in [s for s in props.sheets if s.ifc_definition_id == self.sheet]:
|
||||
for sheet in [s for s in props.sheets if self.expand_all or s.ifc_definition_id == self.sheet]:
|
||||
sheet.is_expanded = True
|
||||
core.load_sheets(tool.Drawing)
|
||||
return {"FINISHED"}
|
||||
@@ -3200,14 +3360,22 @@ class ExpandSheet(bpy.types.Operator):
|
||||
class ContractSheet(bpy.types.Operator):
|
||||
bl_idname = "bim.contract_sheet"
|
||||
bl_label = "Contract Sheet"
|
||||
bl_description = "Hide views, schedules, references etc\nplaced on this sheet"
|
||||
|
||||
bl_description = (
|
||||
"Hide views, schedules, references etc\nplaced on this sheet.\n\nShift+click to contract all sheets."
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
sheet: bpy.props.IntProperty()
|
||||
expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.type == "LEFTMOUSE" and event.shift:
|
||||
self.expand_all = True
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Drawing.get_document_props()
|
||||
for sheet in [s for s in props.sheets if s.ifc_definition_id == self.sheet]:
|
||||
for sheet in [s for s in props.sheets if self.expand_all or s.ifc_definition_id == self.sheet]:
|
||||
sheet.is_expanded = False
|
||||
core.load_sheets(tool.Drawing)
|
||||
return {"FINISHED"}
|
||||
@@ -3268,7 +3436,7 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.ops.bim.activate_drawing(drawing=element.id(), should_view_from_camera=False)
|
||||
|
||||
|
||||
class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
bl_idname = "bim.add_reference_image"
|
||||
bl_label = "Add Reference Image"
|
||||
bl_description = "Add or import reference image to the IFC project"
|
||||
@@ -3276,9 +3444,6 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
|
||||
filepath: bpy.props.StringProperty(
|
||||
name="File Path", description="Filepath used to import from", maxlen=1024, default="", subtype="FILE_PATH"
|
||||
)
|
||||
filter_image: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"})
|
||||
filter_folder: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"})
|
||||
|
||||
@@ -3306,10 +3471,6 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator):
|
||||
layout.prop(self, "override_existing_image")
|
||||
layout.prop(self, "use_existing_object_by_name")
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def _execute(self, context):
|
||||
abs_path = Path(self.filepath).absolute().resolve()
|
||||
image_filepath = Path(tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path))
|
||||
@@ -3353,7 +3514,8 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj.material_slots[0].material = material
|
||||
bpy.ops.bim.add_style()
|
||||
|
||||
style = ifc_file.by_id(material.BIMStyleProperties.ifc_definition_id)
|
||||
style = tool.Ifc.get_entity(material)
|
||||
assert style
|
||||
tool.Style.assign_style_to_object(style, obj)
|
||||
|
||||
# TODO: IfcSurfaceStyleRendering is unnecessary here, added it only because
|
||||
|
||||
@@ -266,7 +266,7 @@ def update_titleblocks(self, context):
|
||||
def update_should_draw_decorations(self, context: bpy.types.Context) -> None:
|
||||
if self.should_draw_decorations:
|
||||
# TODO: design a proper text variable templating renderer
|
||||
collection = context.scene.camera.BIMObjectProperties.collection
|
||||
collection = tool.Blender.get_object_bim_props(context.scene.camera).collection
|
||||
for obj in collection.objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
|
||||
|
||||
@@ -32,6 +32,7 @@ from mathutils import Vector
|
||||
import re
|
||||
|
||||
VIEW_TITLE_OFFSET_Y = 5
|
||||
DRAWING_PADDING = 10
|
||||
DEFAULT_POSITION = Vector((30, 30))
|
||||
SVG = "{http://www.w3.org/2000/svg}"
|
||||
XLINK = "{http://www.w3.org/1999/xlink}"
|
||||
@@ -113,13 +114,15 @@ class SheetBuilder:
|
||||
view_width = self.convert_to_mm(view_root.attrib.get("width"))
|
||||
view_height = self.convert_to_mm(view_root.attrib.get("height"))
|
||||
|
||||
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(DEFAULT_POSITION.x)
|
||||
background.attrib["y"] = str(DEFAULT_POSITION.y)
|
||||
background.attrib["x"] = str(x)
|
||||
background.attrib["y"] = str(y)
|
||||
background.attrib["width"] = str(view_width)
|
||||
background.attrib["height"] = str(view_height)
|
||||
|
||||
@@ -128,16 +131,49 @@ class SheetBuilder:
|
||||
foreground = ET.SubElement(view, "image")
|
||||
foreground.attrib["data-type"] = "foreground"
|
||||
foreground.attrib["xlink:href"] = os.path.relpath(drawing_path, layout_dir)
|
||||
foreground.attrib["x"] = str(DEFAULT_POSITION.x)
|
||||
foreground.attrib["y"] = str(DEFAULT_POSITION.y)
|
||||
foreground.attrib["x"] = str(x)
|
||||
foreground.attrib["y"] = str(y)
|
||||
foreground.attrib["width"] = str(view_width)
|
||||
foreground.attrib["height"] = str(view_height)
|
||||
|
||||
self.add_view_title(
|
||||
DEFAULT_POSITION.x, view_height + DEFAULT_POSITION.y + VIEW_TITLE_OFFSET_Y, view, layout_dir
|
||||
)
|
||||
self.add_view_title(x, view_height + y + VIEW_TITLE_OFFSET_Y, view, layout_dir)
|
||||
layout_tree.write(layout_path)
|
||||
|
||||
def next_drawing_location(self, layout_root: ET.Element, next_width: float) -> list:
|
||||
titleblocks = layout_root.findall(f'{SVG}g[@data-type="titleblock"]')
|
||||
drawings = layout_root.findall(f'{SVG}g[@data-type="drawing"]')
|
||||
|
||||
# how wide is the title block frame
|
||||
try:
|
||||
titleblock_width = self.convert_to_mm(titleblocks[0][0].attrib.get("width"))
|
||||
except (IndexError, AttributeError):
|
||||
titleblock_width = 840.0
|
||||
|
||||
# where does the last drawing finish
|
||||
try:
|
||||
last = drawings[-1][0]
|
||||
last_width = self.convert_to_mm(last.attrib.get("width"))
|
||||
last_x = self.convert_to_mm(last.attrib.get("x"))
|
||||
last_y = self.convert_to_mm(last.attrib.get("y"))
|
||||
except (IndexError, AttributeError):
|
||||
return [DEFAULT_POSITION.x, DEFAULT_POSITION.y]
|
||||
|
||||
# check if the new drawing fits in the current row
|
||||
if last_x + last_width + DRAWING_PADDING + next_width + DEFAULT_POSITION.x < titleblock_width:
|
||||
return [last_x + last_width + DRAWING_PADDING, last_y]
|
||||
|
||||
# start a new row, find the y
|
||||
for drawing in drawings:
|
||||
for image in drawing:
|
||||
try:
|
||||
image_y = self.convert_to_mm(image.attrib.get("y"))
|
||||
image_height = self.convert_to_mm(image.attrib.get("height"))
|
||||
except AttributeError:
|
||||
return [DEFAULT_POSITION.x, DEFAULT_POSITION.y]
|
||||
if image_y + image_height + DRAWING_PADDING > last_y:
|
||||
last_y = image_y + image_height + DRAWING_PADDING
|
||||
return [DEFAULT_POSITION.x, last_y]
|
||||
|
||||
def update_sheet_drawing_sizes(self, sheet: ifcopenshell.entity_instance) -> None:
|
||||
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
||||
|
||||
@@ -221,6 +257,8 @@ class SheetBuilder:
|
||||
view_width = self.convert_to_mm(view_root.attrib.get("width"))
|
||||
view_height = self.convert_to_mm(view_root.attrib.get("height"))
|
||||
|
||||
x, y = self.next_drawing_location(layout_root, view_width)
|
||||
|
||||
view = ET.SubElement(layout_root, "g")
|
||||
view.attrib["data-id"] = str(reference.id())
|
||||
view.attrib["data-type"] = document.Scope.lower()
|
||||
@@ -229,14 +267,12 @@ class SheetBuilder:
|
||||
foreground = ET.SubElement(view, "image")
|
||||
foreground.attrib["data-type"] = "content"
|
||||
foreground.attrib["xlink:href"] = os.path.relpath(view_path, layout_dir)
|
||||
foreground.attrib["x"] = str(DEFAULT_POSITION.x)
|
||||
foreground.attrib["y"] = str(DEFAULT_POSITION.y)
|
||||
foreground.attrib["x"] = str(x)
|
||||
foreground.attrib["y"] = str(y)
|
||||
foreground.attrib["width"] = str(view_width)
|
||||
foreground.attrib["height"] = str(view_height)
|
||||
|
||||
self.add_view_title(
|
||||
DEFAULT_POSITION.x, view_height + DEFAULT_POSITION.y + VIEW_TITLE_OFFSET_Y, view, layout_dir
|
||||
)
|
||||
self.add_view_title(x, view_height + y + VIEW_TITLE_OFFSET_Y, view, layout_dir)
|
||||
layout_tree.write(layout_path)
|
||||
|
||||
def add_view_title(self, x: float, y: float, parent: ET.Element, layout_dir: str) -> None:
|
||||
|
||||
@@ -1128,7 +1128,12 @@ class SvgWriter:
|
||||
|
||||
def get_text():
|
||||
radius = (points[-1].co - points[-2].co).length
|
||||
radius = helper.format_distance(radius, precision=self.precision, decimal_places=self.decimal_places)
|
||||
radius = helper.format_distance(
|
||||
radius,
|
||||
precision=self.precision,
|
||||
decimal_places=self.decimal_places,
|
||||
custom_unit=dimension_data["custom_unit"],
|
||||
)
|
||||
text = f"R{radius}"
|
||||
return text
|
||||
|
||||
@@ -1256,6 +1261,7 @@ class SvgWriter:
|
||||
text_prefix=dimension_data["text_prefix"],
|
||||
text_suffix=dimension_data["text_suffix"],
|
||||
fill_bg=dimension_data["fill_bg"],
|
||||
custom_unit=dimension_data["custom_unit"],
|
||||
)
|
||||
|
||||
def draw_dimension_annotations(self, obj):
|
||||
@@ -1280,6 +1286,7 @@ class SvgWriter:
|
||||
text_prefix=dimension_data["text_prefix"],
|
||||
text_suffix=dimension_data["text_suffix"],
|
||||
fill_bg=dimension_data["fill_bg"],
|
||||
custom_unit=dimension_data["custom_unit"],
|
||||
)
|
||||
|
||||
def draw_measureit_arch_dimension_annotations(self):
|
||||
@@ -1306,6 +1313,7 @@ class SvgWriter:
|
||||
text_prefix="",
|
||||
text_suffix="",
|
||||
fill_bg=False,
|
||||
custom_unit=None,
|
||||
):
|
||||
offset = Vector([self.raw_width, self.raw_height]) / 2
|
||||
v0 = self.project_point_onto_camera(v0_global)
|
||||
@@ -1339,6 +1347,7 @@ class SvgWriter:
|
||||
precision=self.precision,
|
||||
decimal_places=self.decimal_places,
|
||||
suppress_zero_inches=suppress_zero_inches,
|
||||
custom_unit=custom_unit,
|
||||
)
|
||||
text = text_prefix + str(dimension) + text_suffix
|
||||
else:
|
||||
|
||||
@@ -183,7 +183,14 @@ class BIM_PT_drawing_underlay(Panel):
|
||||
|
||||
if not dprops.drawing_styles:
|
||||
return
|
||||
layout.template_list("BIM_UL_generic", "", dprops, "drawing_styles", props, "active_drawing_style_index")
|
||||
layout.template_list(
|
||||
"BIM_UL_generic",
|
||||
"BIM_UL_generic_drawing_styles",
|
||||
dprops,
|
||||
"drawing_styles",
|
||||
props,
|
||||
"active_drawing_style_index",
|
||||
)
|
||||
|
||||
if not drawing_index_is_valid:
|
||||
return
|
||||
@@ -326,7 +333,12 @@ class BIM_PT_schedules(Panel):
|
||||
row.operator("bim.remove_schedule", icon="X", text="").schedule = active_schedule.ifc_definition_id
|
||||
|
||||
self.layout.template_list(
|
||||
"BIM_UL_generic", "", self.props, "schedules", self.props, "active_schedule_index"
|
||||
"BIM_UL_generic",
|
||||
"BIM_UL_generic_schedules",
|
||||
self.props,
|
||||
"schedules",
|
||||
self.props,
|
||||
"active_schedule_index",
|
||||
)
|
||||
|
||||
|
||||
@@ -373,7 +385,12 @@ class BIM_PT_references(Panel):
|
||||
row.operator("bim.remove_reference", icon="X", text="").reference = active_reference.ifc_definition_id
|
||||
|
||||
self.layout.template_list(
|
||||
"BIM_UL_generic", "", self.props, "references", self.props, "active_reference_index"
|
||||
"BIM_UL_generic",
|
||||
"BIM_UL_generic_references",
|
||||
self.props,
|
||||
"references",
|
||||
self.props,
|
||||
"active_reference_index",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import ifcopenshell.util.representation
|
||||
from bonsai.bim.module.drawing.data import DecoratorData, AnnotationData
|
||||
from bonsai.bim.helper import prop_with_search
|
||||
from bpy.types import WorkSpaceTool
|
||||
from typing import Union
|
||||
from functools import partial
|
||||
|
||||
|
||||
class LaunchAnnotationTypeManager(bpy.types.Operator):
|
||||
@@ -135,23 +135,9 @@ class AnnotationTool(WorkSpaceTool):
|
||||
AnnotationToolUI.draw(context, layout)
|
||||
|
||||
|
||||
def add_layout_hotkey_operator(
|
||||
layout: bpy.types.UILayout, text: str, hotkey: str, description: Union[str, None]
|
||||
) -> tuple[bpy.types.OperatorProperties, bpy.types.UILayout]:
|
||||
modifiers = {
|
||||
"A": "EVENT_ALT",
|
||||
"S": "EVENT_SHIFT",
|
||||
}
|
||||
modifier, key = hotkey.split("_")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon=modifiers[modifier])
|
||||
row.label(text="", icon=f"EVENT_{key}")
|
||||
|
||||
op = row.operator("bim.annotation_hotkey", text=text)
|
||||
op.hotkey = hotkey
|
||||
op.description = description
|
||||
return op, row
|
||||
add_layout_hotkey_operator = partial(
|
||||
tool.Blender.add_layout_hotkey_operator, tool_name="annotation", module_name=__name__
|
||||
)
|
||||
|
||||
|
||||
# TODO: move to operator
|
||||
@@ -265,8 +251,8 @@ class AnnotationToolUI:
|
||||
|
||||
class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.annotation_hotkey"
|
||||
bl_label = "Hotkey"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_label = ""
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
hotkey: bpy.props.StringProperty()
|
||||
description: bpy.props.StringProperty()
|
||||
|
||||
|
||||
@@ -24,14 +24,25 @@ import logging
|
||||
import tempfile
|
||||
import ifcopenshell
|
||||
import bonsai.tool as tool
|
||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||
|
||||
|
||||
class ExecuteIfcFM(bpy.types.Operator):
|
||||
class ExecuteIfcFM(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.execute_ifcfm"
|
||||
bl_label = "Execute IfcFM"
|
||||
file_format: bpy.props.StringProperty()
|
||||
bl_description = "Export IfcFM data as a spreadsheet."
|
||||
|
||||
filter_glob: bpy.props.StringProperty(default="*.csv;*.ods;*.xlsx", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
@property
|
||||
def filename_ext(self) -> str:
|
||||
props = bpy.context.scene.BIMFMProperties
|
||||
return f".{props.format}"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = context.scene.BIMFMProperties
|
||||
layout.prop(props, "format")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -41,13 +52,6 @@ class ExecuteIfcFM(bpy.types.Operator):
|
||||
return False
|
||||
return True
|
||||
|
||||
def invoke(self, context, event):
|
||||
props = context.scene.BIMFMProperties
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMFMProperties
|
||||
ifc_file = tool.Ifc.get()
|
||||
@@ -82,12 +86,12 @@ class ExecuteIfcFM(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectFMSpreadsheetFiles(bpy.types.Operator):
|
||||
class SelectFMSpreadsheetFiles(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_fm_spreadsheet_files"
|
||||
bl_label = "Select FM Spreadsheet Files"
|
||||
bl_description = "Select FM spreadsheets to merge."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filter_glob: bpy.props.StringProperty(default="*.ods;*.xlsx", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
files: bpy.props.CollectionProperty(name="File Path", type=bpy.types.OperatorFileListElement)
|
||||
|
||||
def execute(self, context):
|
||||
@@ -99,16 +103,22 @@ class SelectFMSpreadsheetFiles(bpy.types.Operator):
|
||||
new.name = os.path.join(dirname, f.name)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class ExecuteIfcFMFederate(bpy.types.Operator):
|
||||
class ExecuteIfcFMFederate(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.execute_ifcfm_federate"
|
||||
bl_label = "Merge IfcFM SpreadSheets"
|
||||
bl_description = "Merge added IfcFM spreadsheets."
|
||||
filter_glob: bpy.props.StringProperty(default="*.ods;*.xlsx", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
@property
|
||||
def filename_ext(self) -> str:
|
||||
props = bpy.context.scene.BIMFMProperties
|
||||
return f".{props.format}"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = context.scene.BIMFMProperties
|
||||
layout.prop(props, "format")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -118,13 +128,6 @@ class ExecuteIfcFMFederate(bpy.types.Operator):
|
||||
return False
|
||||
return True
|
||||
|
||||
def invoke(self, context, event):
|
||||
props = context.scene.BIMFMProperties
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMFMProperties
|
||||
parser = ifcfm.Parser(preset=props.engine)
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import bpy
|
||||
from . import ui, prop, operator
|
||||
from bpy.app.handlers import persistent
|
||||
import ifcopenshell.util.element
|
||||
|
||||
classes = (
|
||||
operator.AddCurvelikeItem,
|
||||
@@ -102,9 +103,14 @@ def block_scale(scene: bpy.types.Scene) -> None:
|
||||
import bonsai.tool as tool
|
||||
|
||||
if obj := (getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active):
|
||||
if isinstance(obj, bpy.types.Object) and obj.BIMObjectProperties.ifc_definition_id:
|
||||
if obj.scale != (1, 1, 1):
|
||||
obj.scale = (1, 1, 1)
|
||||
if isinstance(obj, bpy.types.Object) and tool.Blender.get_ifc_definition_id(obj):
|
||||
if obj.type == "CAMERA":
|
||||
camera = tool.Ifc.get_entity(obj)
|
||||
if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW":
|
||||
obj.scale = (-1, -1, -1)
|
||||
else:
|
||||
if obj.scale != (1, 1, 1):
|
||||
obj.scale = (1, 1, 1)
|
||||
elif isinstance(obj, bpy.types.Mesh) and tool.Geometry.get_mesh_props(obj).ifc_definition_id:
|
||||
if obj.scale != (1, 1, 1):
|
||||
obj.scale = (1, 1, 1)
|
||||
@@ -118,6 +124,7 @@ 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("TRANSFORM_OT_translate")
|
||||
operator.OverrideMoveMacro.define("BIM_OT_override_move")
|
||||
operator.OverrideMoveMacro.define("TRANSFORM_OT_translate")
|
||||
|
||||
@@ -157,15 +157,17 @@ class RepresentationsData:
|
||||
if not cls.data["representations"]:
|
||||
return []
|
||||
obj = tool.Geometry.get_active_or_representation_obj()
|
||||
assert obj
|
||||
if not obj.data:
|
||||
return []
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
base_representation = tool.Geometry.get_active_representation(obj)
|
||||
if not base_representation:
|
||||
return [] # Maybe in profile editing mode
|
||||
|
||||
# shape aspects matching context of the active representation
|
||||
matching_shape_aspects = []
|
||||
matching_shape_aspects: list[ifcopenshell.entity_instance] = []
|
||||
for shape_aspect in ifcopenshell.util.element.get_shape_aspects(element):
|
||||
matching_representation = tool.Geometry.get_shape_aspect_representation(shape_aspect, base_representation)
|
||||
if matching_representation:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -464,7 +464,8 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
assert tool.Geometry.is_data_supported_for_adding_representation(data)
|
||||
mprops = tool.Geometry.get_mesh_props(data)
|
||||
|
||||
product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
product = tool.Ifc.get_entity(obj)
|
||||
assert product
|
||||
material = ifcopenshell.util.element.get_material(product, should_skip_usage=True)
|
||||
|
||||
# NOTE: Currently iterator doesn't detect whether opening is actually affected the representation
|
||||
@@ -903,7 +904,7 @@ class OverrideOutlinerDelete(bpy.types.Operator):
|
||||
if element := tool.Ifc.get_entity(obj):
|
||||
if tool.Geometry.is_locked(element):
|
||||
self.report({"ERROR"}, lock_error_message(obj.name))
|
||||
if collection := obj.BIMObjectProperties.collection:
|
||||
if collection := tool.Blender.get_object_bim_props(obj).collection:
|
||||
collections_to_delete.discard(collection)
|
||||
continue
|
||||
tool.Geometry.delete_ifc_object(obj)
|
||||
@@ -1067,7 +1068,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
continue
|
||||
|
||||
# clear object's collection so it will be able to have it's own
|
||||
new_obj.BIMObjectProperties.collection = None
|
||||
tool.Blender.get_object_bim_props(new_obj).collection = None
|
||||
# copy the actual class
|
||||
new = bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
|
||||
|
||||
@@ -1774,8 +1775,8 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj.select_set(False)
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"IFC join failed - object '{obj}' has a different representation type "
|
||||
f"({obj_rep.RepresentationType}) than target '{self.target}' ({representation_type}).",
|
||||
f"IFC join failed - object '{obj.name}' has a different representation type "
|
||||
f"({obj_rep.RepresentationType})\nthan target '{self.target.name}' ({representation_type}).",
|
||||
)
|
||||
return
|
||||
|
||||
@@ -1928,7 +1929,7 @@ class OverrideEscape(bpy.types.Operator):
|
||||
tool.Geometry.disable_item_mode()
|
||||
elif tool.Model.get_model_props().openings:
|
||||
bpy.ops.bim.hide_all_openings()
|
||||
elif context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
elif tool.Aggregate.get_aggregate_props().in_aggregate_mode:
|
||||
bpy.ops.bim.disable_aggregate_mode()
|
||||
elif active_object := context.active_object:
|
||||
if tool.Blender.Modifier.try_canceling_editing_modifier_parameters_or_path(active_object):
|
||||
@@ -1952,24 +1953,26 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
selected_objs = context.selected_objects # Purposely exclude active object
|
||||
aprops = tool.Aggregate.get_aggregate_props()
|
||||
nprops = tool.Nest.get_nest_props()
|
||||
|
||||
if self.has_aggregates(selected_objs):
|
||||
if not context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
if not aprops.in_aggregate_mode:
|
||||
bonsai.core.aggregate.enable_aggregate_mode(tool.Aggregate, context.active_object)
|
||||
return {"FINISHED"}
|
||||
|
||||
if self.has_nests(selected_objs):
|
||||
if not context.scene.BIMNestProperties.in_nest_mode:
|
||||
if not nprops.in_nest_mode:
|
||||
bonsai.core.nest.enable_nest_mode(tool.Nest, context.active_object)
|
||||
return {"FINISHED"}
|
||||
|
||||
if len(selected_objs) == 1 and context.active_object == selected_objs[0]:
|
||||
self.handle_single_object(context, context.active_object)
|
||||
elif len(selected_objs) == 0:
|
||||
if context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
if aprops.in_aggregate_mode:
|
||||
bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate)
|
||||
return {"FINISHED"}
|
||||
if context.scene.BIMNestProperties.in_nest_mode:
|
||||
if nprops.in_nest_mode:
|
||||
bonsai.core.nest.disable_nest_mode(tool.Nest)
|
||||
return {"FINISHED"}
|
||||
tool.Geometry.disable_item_mode()
|
||||
@@ -1980,9 +1983,10 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
props = tool.Geometry.get_geometry_props()
|
||||
pprops = tool.Project.get_project_props()
|
||||
aprops = tool.Aggregate.get_aggregate_props()
|
||||
if obj == props.representation_obj:
|
||||
self.report({"ERROR"}, f"Element '{obj.name}' is in item mode and cannot be edited directly")
|
||||
elif obj in [o.obj for o in context.scene.BIMAggregateProperties.not_editing_objects]:
|
||||
elif obj in [o.obj for o in aprops.not_editing_objects]:
|
||||
obj.select_set(False)
|
||||
self.report(
|
||||
{"ERROR"}, f"Element '{obj.name}' does not belong to this aggregate and cannot be edited directly"
|
||||
@@ -2098,7 +2102,8 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
continue
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
parts = ifcopenshell.util.element.get_parts(element)
|
||||
if (aggregate or parts) and not bpy.context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
if (aggregate or parts) and not props.in_aggregate_mode:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -2110,7 +2115,8 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
continue
|
||||
nest = ifcopenshell.util.element.get_nest(element)
|
||||
components = ifcopenshell.util.element.get_components(element)
|
||||
if (nest or components) and not bpy.context.scene.BIMNestProperties.in_nest_mode:
|
||||
props = tool.Nest.get_nest_props()
|
||||
if (nest or components) and not props.in_nest_mode:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -2256,8 +2262,10 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return
|
||||
|
||||
old_profile = item.SweptArea
|
||||
profile.ProfileName = old_profile.ProfileName
|
||||
for inverse in tool.Ifc.get().get_inverse(old_profile):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile)
|
||||
tool.Profile.replace_profile_in_profiles_ui(old_profile.id(), profile.id())
|
||||
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_profile)
|
||||
|
||||
tool.Geometry.reload_representation(props.representation_obj)
|
||||
@@ -2506,11 +2514,12 @@ class RemoveRepresentationItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
obj = tool.Geometry.get_active_or_representation_obj()
|
||||
assert (obj := tool.Geometry.get_active_or_representation_obj())
|
||||
assert (element := tool.Ifc.get_entity(obj))
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
representation_item = ifc_file.by_id(self.representation_item_id)
|
||||
tool.Geometry.remove_representation_item(representation_item)
|
||||
tool.Geometry.remove_representation_item(representation_item, element)
|
||||
tool.Geometry.reload_representation(obj)
|
||||
|
||||
# reload items ui
|
||||
@@ -2885,6 +2894,15 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
|
||||
else:
|
||||
assert False, "Unexpected mesh type."
|
||||
|
||||
if not item_ids:
|
||||
# It is possible that the user has created a shape that
|
||||
# IfcOpenShell cannot render (i.e. boolean clipped everything), but
|
||||
# we still want to edit items. I'm not sure the best way to handle
|
||||
# this, but for now perhaps we can detect when there are no
|
||||
# item_ids at all.
|
||||
representation = tool.Ifc.get_entity(data)
|
||||
item_ids = [i["item"].id() for i in ifcopenshell.util.representation.resolve_items(representation)]
|
||||
|
||||
queue = list(set(item_ids))
|
||||
processed_ids = set()
|
||||
boolean_ids = set()
|
||||
@@ -3257,7 +3275,7 @@ class OverrideMove(bpy.types.Operator):
|
||||
self.new_active_obj = obj
|
||||
|
||||
# Get aggregates
|
||||
props = context.scene.BIMAggregateProperties
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
not_editing_objs = [o.obj for o in props.not_editing_objects]
|
||||
aggregates_to_move = []
|
||||
for obj in context.selected_objects:
|
||||
@@ -3292,7 +3310,7 @@ class OverrideMove(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
# Get nests
|
||||
props = context.scene.BIMNestProperties
|
||||
props = tool.Nest.get_nest_props()
|
||||
not_editing_objs = [o.obj for o in props.not_editing_objects]
|
||||
nests_to_move = []
|
||||
for obj in context.selected_objects:
|
||||
|
||||
@@ -121,7 +121,7 @@ def get_layers_no_active(self, context):
|
||||
return LayersData.data["layers_enum_no_active"]
|
||||
|
||||
|
||||
def update_shape_aspect(self, context):
|
||||
def update_shape_aspect(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> None:
|
||||
shape_aspect_id = self.representation_item_shape_aspect
|
||||
attrs = self.shape_aspect_attrs
|
||||
|
||||
@@ -209,6 +209,7 @@ def update_is_editing_item_layer(self: "BIMObjectGeometryProperties", context: b
|
||||
item = ifc_file.by_id(active_ui_item.ifc_definition_id)
|
||||
if layer := next(iter(item.LayerAssignment), None):
|
||||
self.representation_item_layer = str(layer.id())
|
||||
return
|
||||
|
||||
if "representation_item_layer" in self:
|
||||
del self["representation_item_layer"]
|
||||
@@ -250,9 +251,8 @@ class BIMObjectGeometryProperties(PropertyGroup):
|
||||
representation_item_layer: EnumProperty(items=get_layers, name="Representation Item's Layer")
|
||||
|
||||
@property
|
||||
def active_item(self):
|
||||
if 0 <= self.active_item_index < len(self.items):
|
||||
return self.items[self.active_item_index]
|
||||
def active_item(self) -> Union[RepresentationItem, None]:
|
||||
return tool.Blender.get_active_uilist_element(self.items, self.active_item_index)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
contexts: str
|
||||
@@ -283,7 +283,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)
|
||||
|
||||
|
||||
@@ -225,7 +225,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)
|
||||
|
||||
@@ -334,9 +334,9 @@ class BIM_PT_connections(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
if not (obj := context.active_object):
|
||||
return False
|
||||
if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id):
|
||||
if not tool.Ifc.get_object_by_identifier(tool.Blender.get_ifc_definition_id(obj)):
|
||||
return False
|
||||
return tool.Ifc.get()
|
||||
|
||||
@@ -345,7 +345,6 @@ class BIM_PT_connections(Panel):
|
||||
ConnectionsData.load()
|
||||
|
||||
layout = self.layout
|
||||
props = context.active_object.BIMObjectProperties
|
||||
|
||||
if not ConnectionsData.data["connections"] and not ConnectionsData.data["is_connection_realization"]:
|
||||
layout.label(text="No connections found")
|
||||
@@ -480,12 +479,16 @@ class BIM_PT_placement(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (obj := context.active_object) and obj.BIMObjectProperties.ifc_definition_id
|
||||
return (obj := context.active_object) and tool.Blender.get_ifc_definition_id(obj)
|
||||
|
||||
def draw(self, context):
|
||||
if not PlacementData.is_loaded:
|
||||
PlacementData.load()
|
||||
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
|
||||
if not PlacementData.data["has_placement"]:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Object Placement Found")
|
||||
@@ -496,12 +499,12 @@ class BIM_PT_placement(Panel):
|
||||
row = self.layout.row()
|
||||
row.prop(context.active_object, "rotation_euler", text="Rotation")
|
||||
|
||||
if context.active_object.BIMObjectProperties.blender_offset_type != "NONE":
|
||||
if props.blender_offset_type != "NONE":
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Blender Offset", icon="TRACKING_REFINE_FORWARDS")
|
||||
row.label(text=context.active_object.BIMObjectProperties.blender_offset_type)
|
||||
row.label(text=props.blender_offset_type)
|
||||
|
||||
if context.active_object.BIMObjectProperties.blender_offset_type != "NOT_APPLICABLE":
|
||||
if props.blender_offset_type != "NOT_APPLICABLE":
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=PlacementData.data["original_x"], icon="EMPTY_AXIS")
|
||||
row.label(text=PlacementData.data["original_y"])
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -20,6 +20,7 @@ import bpy
|
||||
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.georeference as core
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator
|
||||
|
||||
|
||||
@@ -87,22 +88,18 @@ class GetCursorLocation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
core.get_cursor_location(tool.Georeference)
|
||||
|
||||
|
||||
class ImportPlot(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class ImportPlot(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
bl_idname = "bim.import_plot"
|
||||
bl_label = "Import Plot"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Import plot"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
bl_description = "Import plot from a csv file."
|
||||
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
|
||||
filename_ext = ".csv"
|
||||
|
||||
def execute(self, context):
|
||||
def _execute(self, context):
|
||||
core.import_plot(tool.Georeference, filepath=self.filepath)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class EnableEditingWCS(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_wcs"
|
||||
|
||||
@@ -35,9 +35,13 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.expanded_groups = json.loads(context.scene.ExpandedGroups.json_string)
|
||||
self.props.groups.clear()
|
||||
|
||||
for group in tool.Ifc.get().by_type("IfcGroup", include_subtypes=False):
|
||||
if not group.HasAssignments:
|
||||
self.load_group(group)
|
||||
groups = [
|
||||
group for group in tool.Ifc.get().by_type("IfcGroup", include_subtypes=False) if not group.HasAssignments
|
||||
]
|
||||
sorted_groups = sorted(groups, key=lambda group: group.Name or "Unnamed")
|
||||
|
||||
for group in sorted_groups:
|
||||
self.load_group(group)
|
||||
|
||||
self.props.is_editing = True
|
||||
bpy.ops.bim.disable_editing_group()
|
||||
@@ -51,14 +55,19 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator):
|
||||
new.has_children = False
|
||||
new.is_expanded = group.id() in self.expanded_groups
|
||||
|
||||
for rel in group.IsGroupedBy or []:
|
||||
for related_object in rel.RelatedObjects:
|
||||
if not related_object.is_a("IfcGroup"):
|
||||
continue
|
||||
new.has_children = True
|
||||
if not new.is_expanded:
|
||||
return
|
||||
self.load_group(related_object, tree_depth=tree_depth + 1)
|
||||
related_groups = [
|
||||
related_object
|
||||
for rel in group.IsGroupedBy or []
|
||||
for related_object in rel.RelatedObjects
|
||||
if related_object.is_a("IfcGroup")
|
||||
]
|
||||
sorted_related_groups = sorted(related_groups, key=lambda group: group.Name or "Unnamed")
|
||||
|
||||
for related_group in sorted_related_groups:
|
||||
new.has_children = True
|
||||
if not new.is_expanded:
|
||||
return
|
||||
self.load_group(related_group, tree_depth=tree_depth + 1)
|
||||
|
||||
|
||||
class ToggleGroup(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@@ -91,9 +91,10 @@ class BIM_PT_object_groups(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
if not (obj := context.active_object):
|
||||
return False
|
||||
return tool.Ifc.get() and context.active_object.BIMObjectProperties.ifc_definition_id
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
return tool.Ifc.get() and props.ifc_definition_id
|
||||
|
||||
def draw(self, context):
|
||||
if not ObjectGroupsData.is_loaded:
|
||||
|
||||
@@ -374,16 +374,17 @@ class ObjectLog(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
if not (obj := context.active_object):
|
||||
cls.poll_message_set("No Active Object")
|
||||
elif not context.active_object.BIMObjectProperties.ifc_definition_id:
|
||||
elif not tool.Blender.get_ifc_definition_id(obj):
|
||||
cls.poll_message_set("Active Object doesn't have an IFC definition")
|
||||
else:
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
step_id = context.active_object.BIMObjectProperties.ifc_definition_id
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
step_id = tool.Blender.get_ifc_definition_id(obj)
|
||||
core.entity_log(tool.IfcGit, tool.Ifc, step_id, self)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -638,6 +638,7 @@ class RADIANCE_OT_export_material_mappings(bpy.types.Operator, ExportHelper):
|
||||
bl_description = "Export material mappings to a JSON file"
|
||||
|
||||
filename_ext = ".json"
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.radiance_exporter_properties
|
||||
|
||||
@@ -71,8 +71,8 @@ def update_sun_path_size(self, context):
|
||||
update_sun_path(self)
|
||||
|
||||
|
||||
def update_display_shadows(self, context):
|
||||
if self.display_shadows:
|
||||
def update_shadow_mode(self, context):
|
||||
if self.shadow_mode == "SHADING":
|
||||
update_sun_path(self)
|
||||
context.scene.render.engine = "BLENDER_WORKBENCH"
|
||||
context.scene.display.shading.light = "FLAT"
|
||||
@@ -82,6 +82,20 @@ def update_display_shadows(self, context):
|
||||
context.scene.view_settings.view_transform = "Standard" # Preserve shading colours
|
||||
space = tool.Blender.get_view3d_space()
|
||||
space.shading.type = "RENDERED"
|
||||
elif self.shadow_mode == "RENDERING":
|
||||
if context.scene.sun_pos_properties.sun_object is None:
|
||||
bpy.ops.object.light_add(type="SUN", radius=1, align="WORLD", location=(0, 0, 0), scale=(1, 1, 1))
|
||||
bpy.ops.object.move_to_collection(collection_index=0)
|
||||
context.scene.sun_pos_properties.sun_object = bpy.context.active_object
|
||||
update_sun_path(self)
|
||||
context.scene.render.engine = "BLENDER_EEVEE_NEXT"
|
||||
context.scene.display.shading.light = "FLAT"
|
||||
context.scene.display.shading.show_shadows = True
|
||||
context.scene.display.shading.show_object_outline = True
|
||||
context.scene.display.shadow_focus = 1.0
|
||||
context.scene.view_settings.view_transform = "Standard" # Preserve shading colours
|
||||
space = tool.Blender.get_view3d_space()
|
||||
space.shading.type = "RENDERED"
|
||||
else:
|
||||
space = tool.Blender.get_view3d_space()
|
||||
space.shading.type = "SOLID"
|
||||
@@ -394,11 +408,27 @@ class BIMSolarProperties(PropertyGroup):
|
||||
azimuth: FloatProperty(name="Azimuth")
|
||||
elevation: FloatProperty(name="Elevation")
|
||||
UTC_zone: FloatProperty(name="UTC Zone")
|
||||
display_shadows: BoolProperty(
|
||||
name="Display Shadows",
|
||||
default=False,
|
||||
description="Enables a visual style to display shadows easily",
|
||||
update=update_display_shadows,
|
||||
shadow_mode: bpy.props.EnumProperty(
|
||||
items=(
|
||||
("NONE", "No Shadows", "No shadows"),
|
||||
(
|
||||
"SHADING",
|
||||
"Shaded",
|
||||
"Fast shadows sufficient for external shadow analysis based on shading styles",
|
||||
"SHADING_SOLID",
|
||||
1,
|
||||
),
|
||||
(
|
||||
"RENDERING",
|
||||
"Rendered",
|
||||
"Raycast (Eevee) shadows considering transparency based on rendering styles",
|
||||
"SHADING_RENDERED",
|
||||
2,
|
||||
),
|
||||
),
|
||||
name="Shadow Mode",
|
||||
description="How to display shadows in the scene",
|
||||
update=update_shadow_mode,
|
||||
)
|
||||
display_sun_path: BoolProperty(
|
||||
name="Display Sun Path",
|
||||
|
||||
@@ -236,8 +236,14 @@ class BIM_PT_solar(bpy.types.Panel):
|
||||
row.operator("bim.move_sun_path_to_3d_cursor")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "display_shadows", icon="SHADING_RENDERED")
|
||||
row.prop(context.scene.display.shading, "shadow_intensity", text="Shadow Intensity")
|
||||
row.prop(props, "shadow_mode", icon="SHADING_RENDERED", expand=True)
|
||||
|
||||
if props.shadow_mode == "SHADING":
|
||||
row = self.layout.row()
|
||||
row.prop(context.scene.display.shading, "shadow_intensity", text="Shadow Intensity")
|
||||
elif props.shadow_mode == "RENDERING":
|
||||
row = self.layout.row()
|
||||
row.prop(context.scene.sun_pos_properties.sun_object.data, "energy", text="Sun Intensity")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.view_from_sun", icon="LIGHT_HEMI")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user