Compare commits

...

104 Commits

Author SHA1 Message Date
Dion Moult 5db6738ac2 Test to see if this has any impact on reloading the add-on. 2025-02-08 19:27:34 +11:00
Dion Moult eabdb5d281 Consolidate selection and visibility tools in the spatial manager into a single location with a single function for filtering
There were a number of inconsistencies between the modes and how keyword
filtering affected results. This should resolve it.
2025-02-08 16:38:29 +11:00
Andrej730 59dd105e99 fix get_applicable_entities / get_applicable_types missing schema #6108 2025-02-08 07:54:49 +11:00
Andrej730 a28070afef Hide Edit Profile for material sets without profiles #6103 2025-02-08 07:54:49 +11:00
Andrej730 689e2718e5 Fix conversion to profile for breps #6103 2025-02-08 07:54:49 +11:00
Andrej730 a2ab3972c8 bim.update_representation - add note about about material unassignment 2025-02-08 07:54:49 +11:00
Andrej730 667223da8e typing 2025-02-08 07:54:49 +11:00
tim 1e58a517a0 Dark mode extend icon (#6112)
dm_extend.png icon was missing

Co-authored-by: tim <tjrhyder@gmail.com>
2025-02-08 07:51:33 +11:00
Dion Moult fcee54c572 Fix regression when removing an active representation.
Some tests removed since they no longer make sense with the new item
editing mode.
2025-02-08 00:21:38 +11:00
Dion Moult 889a9e3891 Tweak failing test to now allow assigning elements to scaled multi-user objects
Previously this was not allowed, but I don't see why not
2025-02-08 00:20:25 +11:00
Dion Moult 7bc1992963 Fix regression where adding a new occurrence should not change your active context back to body if you've switched it. 2025-02-07 23:52:55 +11:00
Dion Moult 49c3a55456 Fix failing pset_template tests. Regression in 5bd24b276 for editing pset template properties. 2025-02-07 23:33:33 +11:00
Dion Moult f11d79f8c1 Fix cost and drawing feature tests 2025-02-07 22:40:17 +11:00
Dion Moult 2cc4f96b7f Fix failing project feature tests. Fix regression where appending an asset might append a named style twice. 2025-02-07 22:02:44 +11:00
Dion Moult bb46a1659d Fix light and georeference feature tests
Not setting the year meant our date calc tests would fail every year :)
2025-02-07 17:19:27 +11:00
Dion Moult f09002c7e2 Fix regression in caeabe2 where actor data didn't load. 2025-02-07 16:58:45 +11:00
Dion Moult 9f75a14204 Update issue templates 2025-02-07 13:31:59 +11:00
Dion Moult c40d21d945 Fix #6106. True north must always be 2D. 2025-02-07 13:19:33 +11:00
Dion Moult 0d3245102f Fix remaining failing tool tests 2025-02-07 13:12:02 +11:00
Dion Moult 3aa4fa580b Fix regression where you couldn't upgrade schema of IFCs with invalid headers 2025-02-07 12:38:52 +11:00
Dion Moult 4c255cca43 Fix failing model tests and regression 0899a2a8 where switching geometry can invalidate old meshes
The fix in 0899a2a8 ensured that when you switched geometry, all other
elements (sharing the same type or representation) would also switch
alongside it. However it would lead to mesh invalidation which broke
some assumptions in the style code. The style code also repeated a lot
of the logic of switch representation (i.e. finding shared
representations) so this seems simpler.

In general the whole representation part of the code is messy and
hopefully over time it'll get better.
2025-02-07 12:35:08 +11:00
Dion Moult e603ebed04 Fix bug where loading a types ignored style assigned via a material
For occurrences, the iterator would populate ios_materials if there is a
material / style associated with it. However, for types we do
create_shape to the representation directly, so we need to do the
material / style check manually.
2025-02-07 12:29:17 +11:00
Andrej730 38e6f09c91 Fix #6096 2025-02-06 18:26:14 +05:00
Andrej730 6473fa4b67 Deprecate workaround for universal wheels
Tested on Blender 4.3.2 and universal wheels are now supported without this hack, see https://projects.blender.org/blender/blender/issues/125091
2025-02-06 18:26:14 +05:00
Andrej730 38adaf4d18 Fix running critical tests in ci 2025-02-06 18:26:14 +05:00
Dion Moult b7fb5c90bc Continue fixing failing tool tests. Fix regression where UV coordinates might have the wrong order. 2025-02-06 17:22:54 +11:00
Dion Moult 23efb09ba6 Add links to Linux packages 2025-02-06 09:46:59 +11:00
Dion Moult cf6280b928 Rename BlenderBIM to Bonsai in a few more spots 2025-02-06 09:24:25 +11:00
Andrej730 c16894cabf Fix bug in 124c028 saving defined unit as defining 2025-02-05 18:27:27 +05:00
Andrej730 5df297c23c Fix failing facet test after 124c028 2025-02-05 18:27:27 +05:00
Andrej730 b2b3a83463 ci - fix missing ifctester for tests 2025-02-05 18:27:27 +05:00
Andrej730 4b0b0191ba Test enabling/disabling Bonsai before uploading to unstable repo #6075 2025-02-05 18:27:27 +05:00
Dion Moult a92306d413 Fix #6097. Be more defensive against selection state when moving objects into new containers. 2025-02-05 20:28:29 +11:00
Dion Moult f457ce8270 Fix failing unit/system/geometry tool tests. Fix regression where object scales weren't applied in the new add element approach. 2025-02-05 18:50:37 +11:00
Dion Moult 590c2e120c Fix failing drawing tool tests. Also consolidate relative paths using tool.Ifc.get_uri.
The same issue of handling relative paths, doing as_posix, checking
drives, etc was solved again and again in slightly different ways. This
centralises it.
2025-02-05 17:17:09 +11:00
Dion Moult 13746eb55e Fix CI job a unique name so it can be used as a requirement for PRs
It seems as though the UI only selects by job name and we use the name
"build" everywhere.
2025-02-05 14:28:42 +11:00
Dion Moult 55b9c7d6bf Fix failing ifcpatch tests 2025-02-05 14:10:41 +11:00
Dion Moult e86624873b Fix segfault from editing a IFC2X3 georeferencing pset with a null map unit.
Doing createIfcIdentifier(None) segfaults.
2025-02-05 14:10:41 +11:00
Andrej730 ae11f205f3 Fix missing unregister for some tools #6075
PileTool, PlateTool, RampFlightTool, StairFlightTool were never unregistered leading to issues on Bonsai update
2025-02-04 17:37:57 +05:00
Andrej730 457f9c6aef Select pset template for the pset in pset template ui for editing
Example - https://imgur.com/a/h46Ewhk
2025-02-04 17:37:56 +05:00
Andrej730 cc88bd4a9d Fix issue adding project library if other library was already in edit 2025-02-04 17:37:56 +05:00
Andrej730 8f74f4d9f1 Change IfcProjectLibrary's parent library from UI
Example - https://imgur.com/a/4PZr0px
It's very clunky and mainly exposed just to be available in some way, it will all make more sense when we display project libraries as trees in project library ui.
2025-02-04 17:37:56 +05:00
Andrej730 56f0c19a85 append_asset to support IfcPresentationLayerAssignment #6023 2025-02-04 17:37:56 +05:00
Andrej730 76747bdf5a Recent files - dim button for filepaths that do not exist
Example - https://i.imgur.com/dwEBtG6.png
2025-02-04 17:37:56 +05:00
Andrej730 a4de196259 space module operators - remove unnecessary ifc operators 2025-02-04 17:37:56 +05:00
Andrej730 049b157c8a bim.toggle_space_visibility - description 2025-02-04 17:37:56 +05:00
Andrej730 7ec1bc5272 Fix error toggling boundary decorations after one was removed #6088 2025-02-04 17:37:56 +05:00
Andrej730 d4f44cb0a1 typing 2025-02-04 17:37:55 +05:00
Andrej730 ecd7282150 black . 2025-02-04 17:37:55 +05:00
Dion Moult bd2c3a0ca1 Continue fixing tool tests.
Just chipping away at it give me time.
2025-02-04 17:04:29 +11:00
Louis Trümpler f0f2dda40e Add assign constituent fractions recipe (#5979)
* Add AssignConstituentFractions recipe

* license, units & added context description

* removed src, using better unit handling and several other small improvements based on feedback
2025-02-04 14:23:08 +11:00
Dion Moult 0c41f0c32c Fix #6094. Fix failing core tests. 2025-02-04 13:36:28 +11:00
Dion Moult a35ef32ee6 Forgot to commit docs for serialiser settings 2025-02-04 07:35:34 +11:00
Andrej730 00eff23b74 Rename bim_boundary_properties -> BIMBoundaryProperties for consistency 2025-02-03 17:43:01 +05:00
Andrej730 88af3eb1a6 Explicitly enable Bonsai in the second Blender instance #6092
To cover cases when Bonsai was enabled in current Blender sesssion and user prefs are not saved.
2025-02-03 17:28:48 +05:00
Andrej730 a48d774337 typing 2025-02-03 17:28:47 +05:00
Andrej730 734cd47ea5 black . 2025-02-03 17:28:47 +05:00
Dion Moult 5159a69225 Fix #6070. Button was pointing to the wrong function to confirm/cancel editing an extrusion profile.
Probably a copy paste mistake during the UI refactor.
2025-02-03 23:07:16 +11:00
Dion Moult 2f2072cf7f Fix #6080. Bug where renaming introduced suffixes during bulk reassignment of classes. 2025-02-03 23:00:04 +11:00
Andrej730 8c47f2a72f Fix issue infinitely adding new ports on bim.show_ports #6074 2025-02-03 16:37:51 +05:00
Andrej730 a6632167be system module operators - remove unnecessary ifc operator 2025-02-03 16:37:46 +05:00
Andrej730 12d1fd221d Clean up orphaned curves creating new elevation/section annotations 2025-02-03 13:45:54 +05:00
Andrej730 10642618e5 Fix error entering item mode for elevation/section annotations #6081 2025-02-03 13:45:54 +05:00
Andrej730 be4f6ac222 small fix for 3eaf4367ad 2025-02-03 12:45:05 +05:00
Andrej730 3eaf4367ad Fix #6082 2025-02-03 12:40:36 +05:00
Andrej730 305b4c27b5 fix a typo 2025-02-03 11:45:41 +05:00
Andrej730 b56f2d7514 black . - ignore simple_spf submodule 2025-02-03 11:43:08 +05:00
Andrej730 cf026dccc2 bump ios build #5987 2025-02-03 11:43:08 +05:00
Dion Moult c5185aaf54 Fix #6085. Failing unit test. 2025-02-03 16:33:04 +11:00
Dion Moult 50ed756353 Found out layer names don't exist in IFC2X3 that's incredible 2025-02-03 16:04:56 +11:00
tim 0f0578e1c5 Geographic Element Tool (#6089)
New icon tool for adding landscape elements

Co-authored-by: tim <tjrhyder@gmail.com>
2025-02-03 15:38:10 +11:00
Herlianto 0d086fda33 delete unused import (#6093) 2025-02-03 15:35:55 +11:00
Dion Moult f3f7510b48 Fix #6087. (again)
I don't know why depress was used. To make it colour in blue?
2025-02-03 12:36:18 +11:00
Thomas Krijnen b2a4529904 Don't fail on empty build deps cache #6054 2025-02-02 10:00:56 +01:00
Thomas Krijnen 7de50ff0c5 Don't fail on empty build deps cache #6054 2025-02-02 09:46:13 +01:00
Thomas Krijnen 7b0a7e51bb Try 'Build IfcOpenShell Linux ARM' workflow #6054 2025-02-02 09:35:15 +01:00
Thomas Krijnen 507d13363d delete obsolete files 2025-02-02 09:21:01 +01:00
Thomas Krijnen 9713b06d75 delete obsolete files 2025-02-02 09:01:24 +01:00
Dion Moult be6a4de6f0 Fix #6087. High CPU usage due to function in draw call.
I assume this is the problem.
2025-02-02 17:05:56 +11:00
Dion Moult ea85a4247a Fix #6079. Regression in editing space boundaries due to leftover line in commit. 2025-02-01 16:42:06 +11:00
tim f377d794a0 Unify icon modes to work with default Blender Light theme (#6078)
Force the color of the icons in the Properties-Scene tab to match Regular Text
Use the icon display mode function to detect where to use dm_ifc.png or lm_ifc.png

Co-authored-by: tim <tjrhyder@gmail.com>
2025-02-01 16:18:48 +11:00
tim 16d077f1cb Rename dm_IFC.png to dm_ifc.png (#6076)
renaming to lowercase to match lm_ifc presidents.
2025-02-01 16:18:26 +11:00
Dion Moult 3f62f72f35 See #2981. First attempt at having the door generator also do shape aspects, material constituents, and styles all simultaneously 2025-02-01 16:15:58 +11:00
Dion Moult dc79e9e005 More utility functions to jump between representations, styles, and shape aspects
It's a bit uncertain where these belong best, we'll find out over time
2025-02-01 16:11:37 +11:00
Dion Moult d25f57ec40 See #2981. Adding a door representation now also adds associated shape aspects
It doesn't add materials though, that's a separate responsibility right
now
2025-02-01 16:02:57 +11:00
Dion Moult 9f93779ac2 Adding set items can now specify a name at creation for convenience 2025-02-01 16:00:43 +11:00
Dion Moult 8436817408 Update incorrect docs and raise suspicion about how shape aspect styles are assigned 2025-02-01 15:55:34 +11:00
Dion Moult 1a98504328 Fix bug and typo where getting shape aspects for non-geometric types (e.g. walls) would fail
This caused an error when updating a style of a material used in a wall
type
2025-02-01 15:54:57 +11:00
Bruno Postle 1680c3135a Update to latest ifcmerge on Windows
Handle missing IfcRelationship - eg. if all sofas are deleted the
IfcRelDefinesByType is deleted too, but we reincarnate as an empty list
so it can be merged.

Sanity check internal graph - If an entity is deleted in one branch but
has a new usage in another branch, merging would break the graph leaving
a dangling reference. ie.  now you can't delete types, materials etc..
in one branch while using them for new objects in another and still
expect the merge to succeed.
2025-01-31 15:05:34 +00:00
Dion Moult 79e4ba5b19 New usecase to add shape aspect with tests 2025-02-01 00:46:00 +11:00
Andrej730 d6d1f7714f black . to ignore ifcconvert's cityjson submodule 2025-01-31 18:21:23 +05:00
Andrej730 68543c7f05 Simplify types used in debug module express methods #6071 2025-01-31 18:21:14 +05:00
Andrej730 135655f8a0 bim.add_pset - remove unnecessary ifc operator 2025-01-31 17:12:32 +05:00
Andrej730 df49356104 Support adding a new IfcProjectLibrary from UI
Example - https://imgur.com/a/NUQkvHf
2025-01-31 17:12:31 +05:00
Andrej730 04bcf8e2a9 Fix error switching between libraries 2025-01-31 17:12:31 +05:00
Andrej730 9f17ec5bf5 Support editing project library attributes
Example - https://imgur.com/a/MFtLDlh
2025-01-31 17:12:31 +05:00
Andrej730 67aa8f1413 Update project library enum on changing library file 2025-01-31 17:12:31 +05:00
Andrej730 8469d10c85 Project library UI - make filter optional and allow assigning to different libraries 2025-01-31 17:12:31 +05:00
Andrej730 87cbc66c99 typing 2025-01-31 17:12:31 +05:00
Andrej730 c2ae3dcb08 append_asset - extend existing rels matching them by project guid
Mentioned in #6039
2025-01-31 17:12:31 +05:00
Andrej730 089f15a5cc fix a typo 2025-01-31 15:06:37 +05:00
Thomas Krijnen 919df92888 Virtual inheritance configurable at codegen time 2025-01-30 13:12:42 +01:00
Andrej730 553872ced2 append_asset - fix issue appending elements that have as_entity() -> None #6037 #6039 2025-01-30 16:06:19 +05:00
Andrej730 bc33cb6391 append_asset - remove unnecessary partial 2025-01-30 16:06:19 +05:00
172 changed files with 3687 additions and 2315 deletions
+20
View File
@@ -0,0 +1,20 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
Replace this text describing what problem occurred and what you expected to happen instead.
1. To reproduce this, open file '...'
2. Click on '....'
3. See error
**Attachments**
If applicable, add screenshots to help explain your problem. Please also drag-drop any files necessary to show the error (rename the file extension from .ifc to .txt to upload). Private files can be uploaded to https://ifcopenshell.org/upload.html - only viewed by core developers and will be deleted afterwards.
**Debug information**
If this is in Bonsai, paste the output from the Copy Debug Information option in Bonsai. It can be found under Quality and Coordination -> Quality Control -> Debug. If this is a general software issue, if relevant include details about IfcOpenShell version, operating system, Python version, etc.
+10
View File
@@ -0,0 +1,10 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
Replace this text and describe a feature you'd like us to add. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable.
+116
View File
@@ -0,0 +1,116 @@
name: Build IfcOpenShell Linux ARM
on:
workflow_dispatch:
jobs:
build_ifcopenshell:
runs-on: ubuntu-22.04-arm
container: arm64v8/rockylinux:8
steps:
- name: Install Dependencies
run: |
yum update -y
yum install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 \
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
findutils
git config --global --add safe.directory '*'
- name: Install aws cli
run: |
curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
./aws/install
rm -rf awscliv2.zip aws
aws --version
- name: Checkout Repository
uses: actions/checkout@v3
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v3
with:
repository: IfcOpenShell/build-outputs
path: ./build
ref: rockylinux8-arm64
lfs: true
token: ${{ secrets.BUILD_REPO_TOKEN }}
- name: Unpack Dependencies
run: |
install_root=$(find ./build -maxdepth 4 -name install)
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
# Not supported on docker
# - name: ccache
# uses: hendrikmuhs/ccache-action@v1
# with:
# key: ${GITHUB_WORKFLOW}-rockylinux8-x64
- name: Run Build Script
run: |
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release python3 ./nix/build-all.py --diskcleanup
- name: Pack Dependencies
run: |
cd build
for install_dir in $(find $(find . -maxdepth 4 -name install) -mindepth 1 -maxdepth 1 -type d); do
test -f $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz || tar -czf $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz -C $(dirname "$install_dir") $(basename "$install_dir");
done
- name: Commit and Push Changes to Build Repository
run: |
cd build
git config user.name "IfcOpenBot"
git config user.email "ifcopenbot@ifcopenshell.org"
git add "$(find . -maxdepth 4 -name install)/*.tar.gz"
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
git push || true
- name: Package .zip archives
run: |
VERSION=v`cat VERSION`
cd ./build/`uname`/*/install/ifcopenshell
mkdir ~/output
ls -d python-* | while read py_version; do
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
numbers=`echo $py_version | grep -oE '[0-9]+\.[0-9]+' | tr -d '.'`
py_version_major=python-${numbers}$postfix
pushd . > /dev/null
cd $py_version
if [ ! -d ifcopenshell ]; then
mkdir ../ifcopenshell_
mv * ../ifcopenshell_
mv ../ifcopenshell_ ifcopenshell
fi
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
find ifcopenshell -name "*.pyc" -delete
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip ifcopenshell/*
mv *.zip ~/output
popd > /dev/null
done
cd bin
rm *.zip || true
ls | while read exe; do
zip -qq -r ${exe}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip $exe
done
mv *.zip ~/output
cd ..
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Upload .zip archives to S3
run: |
aws s3 cp ~/output s3://ifcopenshell-builds/ --recursive
+40 -16
View File
@@ -99,9 +99,9 @@ jobs:
token: ${{ secrets.IOS_TO_BLENDER_REPO }}
path: bonsai_unstable_repo
- name: Update index.json on extensions repo
- name: Download Blender and run critical tests
run: |
set -x -e
# 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
@@ -112,25 +112,25 @@ jobs:
export PATH="$PATH:$BLENDER_PATH"
blender --version
# Setup unstable repo to get Bonsai build.
cd bonsai_unstable_repo
pip install -r requirements.txt
python setup_extensions_repo.py --last-tag
git config --global user.name 'IfcOpenBot'
git config --global user.email 'IfcOpenBot@users.noreply.github.com'
git add index.json
git add readme.md
git commit -m "Update index.json"
git push
- name: Run bonsai tests
run: |
set -x -e
BLENDER_PATH=$(find blender-*/ -maxdepth 0 -exec readlink -f {} \;)
export PATH="$PATH:$BLENDER_PATH"
cd ..
bonsai_zip="$(pwd)/$(ls bonsai_unstable_repo/bonsai_py311*-linux-x64.zip)"
blender --version
# Install Bonsai.
blender --command extension install-file -r user_default -e $bonsai_zip
blender --command extension list
git clone https://github.com/IfcOpenShell/IfcOpenShell.git IfcOpenShell
# Reregister Bonsai.
# Note that running it in background might miss some errors
# (e.g. tools are not registered in background mode).
blender --background --python IfcOpenShell/src/bonsai/scripts/reregister_bonsai.py
# Install sverchok.
wget -q -O sverchok.zip https://github.com/nortikin/sverchok/archive/refs/heads/master.zip
# ifcsverchok expecting sverchok to be named "sverchok" and not "sverchok-master".
unzip -q sverchok.zip
@@ -139,17 +139,41 @@ jobs:
rm -r sverchok
blender --command extension install-file -r user_default sverchok.zip
git clone https://github.com/IfcOpenShell/IfcOpenShell.git IfcOpenShell
# Install ifcsverchok.
cd IfcOpenShell/src/ifcsverchok
make dist
sverchok_zip="$(pwd)/dist/$(ls dist)"
blender --command extension install-file -r user_default $sverchok_zip
- name: Update index.json on extensions repo
run: |
set -x -e
# Setup Blender.
BLENDER_PATH=$(find blender-*/ -maxdepth 0 -exec readlink -f {} \;)
export PATH="$PATH:$BLENDER_PATH"
blender --version
cd bonsai_unstable_repo
git config --global user.name 'IfcOpenBot'
git config --global user.email 'IfcOpenBot@users.noreply.github.com'
git add index.json
git add readme.md
git commit -m "Update index.json"
git push
- name: Run bonsai tests
run: |
set -x -e
BLENDER_PATH=$(find blender-*/ -maxdepth 0 -exec readlink -f {} \;)
export PATH="$PATH:$BLENDER_PATH"
blender --version
# Install Sun Position extension.
blender --online-mode --command extension sync
blender --online-mode --command extension install --enable --sync sun_position
cd ../bonsai
cd IfcOpenShell/src/bonsai
pip install pytest-blender
blender --background --python scripts/setup_pytest.py
blender --python-expr "import bonsai; print(bonsai.bbim_semver); import ifcopenshell; print(ifcopenshell.version)" --background
+2 -1
View File
@@ -31,7 +31,7 @@ jobs:
steps:
- run: echo ok go
build:
compile-and-test:
runs-on: ubuntu-22.04
needs: activate
steps:
@@ -135,4 +135,5 @@ jobs:
pip install deepdiff
cd ../ifcdiff && make test
cd ../ifcpatch && make test
pip install -e ../ifctester --no-deps
cd ../ifctester && make test
+2
View File
@@ -27,7 +27,9 @@ include = '''
extend-exclude = '''
src/ifcopenshell-python/ifcopenshell/express/*
|src/ifcopenshell-python/ifcopenshell/mvd/*
|src/ifcopenshell-python/ifcopenshell/simple_spf/*
|src/ifc2ca/templates/*
|src/ifcconvert/cityjson/*
'''
[tool.pyright]
+8 -60
View File
@@ -85,7 +85,7 @@ BLENDER_PLATFORM:=windows-x64
endif
# Current build commit hash.
OLD:=1d27161
OLD:=463289e
.PHONY: bump
bump:
ifndef NEW
@@ -233,7 +233,7 @@ endif
# required for three-way git merging
ifeq ($(PLATFORM), win)
cd build/bonsai/libs/bin && wget https://github.com/brunopostle/ifcmerge/releases/download/2024-06-24/ifcmerge.zip
cd build/bonsai/libs/bin && wget https://github.com/brunopostle/ifcmerge/releases/download/2025-01-26/ifcmerge.zip
cd build/bonsai/libs/bin && unzip ifcmerge.zip && rm ifcmerge.zip
else
cd build/bonsai/libs/bin && wget https://raw.githubusercontent.com/brunopostle/ifcmerge/main/ifcmerge && chmod +x ifcmerge
@@ -278,64 +278,6 @@ endif
fi
mv build/wheels/*.whl build/bonsai/wheels/
# Ugly patch to accomodate MacOS universal builds not being recognized by Blender.
# Blender doesn't look into the wheel contents, it just checks the name.
# So this hack works without working out some way to repack those wheels.
# See: https://projects.blender.org/blender/blender/issues/125091
ifeq ($(PLATFORM), macosm1)
# Has universal and x86_64 builds.
prev_whl_name=$$(find build/bonsai/wheels/lxml-*.whl); \
whl_name=$$(echo $$prev_whl_name | sed "s/_universal2/_arm64/"); \
mv "$$prev_whl_name" "$$whl_name";
prev_whl_name=$$(find build/bonsai/wheels/tzfpy-*.whl); \
whl_name=$$(echo $$prev_whl_name | sed -E "s/macosx_10_([0-9]+)_x86_64\.macosx_11_0_arm64\.macosx_10_\1_universal2/macosx_10_\1_arm64/"); \
mv "$$prev_whl_name" "$$whl_name";
# Has only universal build.
prev_whl_name=$$(find build/bonsai/wheels/greenlet-*.whl); \
whl_name=$$(echo $$prev_whl_name | sed "s/_universal2/_arm64/"); \
mv "$$prev_whl_name" "$$whl_name";
# has universal and x86_64 builds.
prev_whl_name=$$(find build/bonsai/wheels/fonttools-*.whl); \
whl_name=$$(echo $$prev_whl_name | sed "s/_universal2/arm64/"); \
mv "$$prev_whl_name" "$$whl_name";
# Has only universal build.
prev_whl_name=$$(find build/bonsai/wheels/charset_normalizer-*.whl); \
whl_name=$$(echo $$prev_whl_name | sed "s/_universal2/_arm64/"); \
mv "$$prev_whl_name" "$$whl_name";
endif
ifeq ($(PLATFORM), macos)
# Has universal and arm64 builds.
prev_whl_name=$$(find build/bonsai/wheels/MarkupSafe-*.whl); \
whl_name=$$(echo $$prev_whl_name | sed "s/_universal2/_x86_64/"); \
mv "$$prev_whl_name" "$$whl_name";
prev_whl_name=$$(find build/bonsai/wheels/tzfpy-*.whl); \
whl_name=$$(echo $$prev_whl_name | sed -E "s/macosx_10_([0-9]+)_x86_64\.macosx_11_0_arm64\.macosx_10_\1_universal2/macosx_10_\1_x86_64/"); \
mv "$$prev_whl_name" "$$whl_name";
prev_whl_name=$$(find build/bonsai/wheels/greenlet-*.whl); \
whl_name=$$(echo $$prev_whl_name | sed "s/_universal2/_x86_64/"); \
mv "$$prev_whl_name" "$$whl_name";
prev_whl_name=$$(find build/bonsai/wheels/charset_normalizer-*.whl); \
whl_name=$$(echo $$prev_whl_name | sed "s/_universal2/_x86_64/"); \
mv "$$prev_whl_name" "$$whl_name";
endif
# Safeguard for unhandled universal files.
wheels=$$(find build/bonsai/wheels/*universal2*.whl); \
if [ -n "$$wheels" ]; then \
echo "Found universal2 wheel files:"; \
echo "$$wheels"; \
echo "Error: universal2 wheel files are not supported by Blender!"; \
exit 1; \
fi
# Temporary workaround for Blender not handling non-3.11 wheels #5743.
# Use '-n' as on macos x86-64 doesn't have a binary wheel and it autoincludes 'cp311'.
prev_whl_name=$$(find build/bonsai/wheels/tzfpy-*.whl); \
@@ -402,6 +344,12 @@ else
pytest test/tool/test_$(MODULE).py
endif
# Reregistering test is not added to the standard test suite because during unregister
# Blender removes all Bonsai dependencies breaking dev-environment symlinks.
.PHONY: test-reregister
test-reregister:
blender --python scripts/reregister_bonsai.py
.PHONY: qa
qa:
black .
Binary file not shown.
+1 -2
View File
@@ -20,8 +20,7 @@ import os
import bpy
import bpy.utils.previews
import importlib
from pathlib import Path
from . import handler, ui, prop, operator, helper
from . import handler, ui, prop, operator
from typing import Callable, Union
try:

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

-1
View File
@@ -20,7 +20,6 @@ from __future__ import annotations
import os
import bpy
import json
import numpy as np
import datetime
import zipfile
import tempfile
+3
View File
@@ -188,6 +188,9 @@ def refresh_ui_data():
and it need to be refreshed manually if needed.
"""
from bonsai.bim import modules
import bonsai.bim.ui
bonsai.bim.ui.refresh()
for name, value in modules.items():
try:
+6 -8
View File
@@ -20,21 +20,19 @@ from __future__ import annotations
import importlib
import bpy
import json
import math
import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.util.attribute
import ifcopenshell.util.element
import ifcopenshell.util.unit
from ifcopenshell.util.doc import get_attribute_doc, get_predefined_type_doc, get_property_doc
from mathutils import geometry
from mathutils import Vector
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from typing import Optional, Callable, Any, Union, Iterable, TYPE_CHECKING
if TYPE_CHECKING:
import bonsai.bim.prop
from bonsai.bim.prop import Attribute
# ImportCallback return values:
# - None - property should be imported by default workflow
@@ -48,7 +46,7 @@ if TYPE_CHECKING:
def draw_attributes(
props: list[bonsai.bim.prop.Attribute],
props: bpy.types.bpy_prop_collection_idprop[Attribute],
layout: bpy.types.UILayout,
copy_operator: Optional[str] = None,
popup_active_attribute: Optional[bonsai.bim.prop.Attribute] = None,
@@ -117,7 +115,7 @@ def draw_attribute(
def import_attributes(
ifc_class: str,
props: bpy.types.bpy_prop_collection,
props: bpy.types.bpy_prop_collection_idprop[Attribute],
data: dict[str, Any],
callback: Optional[ImportCallback] = None,
) -> None:
@@ -128,7 +126,7 @@ def import_attributes(
# A more elegant attribute importer signature, intended to supersede import_attributes
def import_attributes2(
element: Union[str, ifcopenshell.entity_instance],
props: bpy.types.bpy_prop_collection,
props: bpy.types.bpy_prop_collection_idprop[Attribute],
callback: Optional[ImportCallback] = None,
) -> None:
if isinstance(element, str):
@@ -144,7 +142,7 @@ def import_attributes2(
def import_attribute(
attribute: W.attribute,
props: bpy.types.bpy_prop_collection,
props: bpy.types.bpy_prop_collection_idprop[Attribute],
data: dict[str, Any],
callback: Optional[ImportCallback] = None,
) -> None:
@@ -258,7 +256,7 @@ def add_attribute_description(attribute_blender: bonsai.bim.prop.Attribute, attr
def export_attributes(
props: bpy.types.bpy_prop_collection,
props: bpy.types.bpy_prop_collection_idprop[Attribute],
callback: Optional[ExportCallback] = None,
) -> dict[str, Any]:
attributes: dict[str, Any] = {}
+16 -3
View File
@@ -34,10 +34,9 @@ import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.shape
import bonsai.tool as tool
from itertools import chain, accumulate
from bonsai.bim.ifc import IfcStore, IFC_CONNECTED_TYPE
from bonsai.tool.loader import OBJECT_DATA_TYPE
from typing import Dict, Union, Optional, Any, cast
from typing import Dict, Union, Optional, Any
class MaterialCreator:
@@ -71,6 +70,8 @@ class MaterialCreator:
self.mesh = mesh
self.obj = obj
if element.is_a("IfcTypeProduct"):
self.parse_element_type_material_styles(element)
self.parsed_meshes.add(self.mesh.name)
if not self.ifc_import_settings.load_indexed_maps:
self.load_texture_maps(shape_has_openings)
@@ -83,6 +84,18 @@ class MaterialCreator:
if ifc_definition_id := material.BIMStyleProperties.ifc_definition_id:
self.styles[ifc_definition_id] = material
def parse_element_type_material_styles(self, element: ifcopenshell.entity_instance) -> None:
if self.mesh["ios_materials"]:
return # Already has materials assign to the representation itself
# Otherwise, we need to check for material styles on the element, since
# create_shape on types only works on representations.
context = tool.Ifc.get().by_id(self.mesh.BIMMeshProperties.ifc_definition_id).ContextOfItems
for material in ifcopenshell.util.element.get_materials(element):
if style := ifcopenshell.util.representation.get_material_style(material, context):
self.mesh["ios_materials"] = (style.id(),)
self.mesh["ios_material_ids"] = [0] * len(self.mesh.polygons)
break
def get_ifc_coordinate(self, material: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]:
"""Get IfcTextureCoordinate"""
texture_style = tool.Style.get_texture_style(material)
@@ -96,7 +109,7 @@ class MaterialCreator:
return coords
# TODO: support IfcTextureMap
if coords.is_a("IfcTextureMap"):
print(f"WARNING. IfcTextureMap texture coordinates is not supported.")
print("WARNING. IfcTextureMap texture coordinates is not supported.")
return
def load_texture_maps(self, shape_has_openings: bool) -> None:
@@ -49,9 +49,9 @@ classes = (
def register():
bpy.types.Scene.BIMBoundaryProperties = bpy.props.PointerProperty(type=prop.BIMBoundaryProperties)
bpy.types.Object.bim_boundary_properties = bpy.props.PointerProperty(type=prop.BIMObjectBoundaryProperties)
bpy.types.Object.BIMBoundaryProperties = bpy.props.PointerProperty(type=prop.BIMObjectBoundaryProperties)
def unregister():
del bpy.types.Scene.BIMBoundaryProperties
del bpy.types.Object.bim_boundary_properties
del bpy.types.Object.BIMBoundaryProperties
@@ -78,7 +78,8 @@ class BoundaryDecorator:
unselected_edges = []
unselected_tris = []
for boundary in context.scene.BIMBoundaryProperties.boundaries:
props = tool.Boundary.get_boundary_props()
for boundary in props.boundaries:
obj = boundary.obj
if not obj or not obj.data: # A boundary may not have data if it has no connection geometry
continue
@@ -354,7 +354,9 @@ class EnableEditingBoundary(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
bprops = context.active_object.bim_boundary_properties
obj = context.active_object
assert obj
bprops = tool.Boundary.get_object_boundary_props(obj)
bprops.is_editing = True
boundary = tool.Ifc.get_entity(context.active_object)
for ifc_attribute, blender_property in EDITABLE_ATTRIBUTES.items():
@@ -373,7 +375,9 @@ class DisableEditingBoundary(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
bprops = context.active_object.bim_boundary_properties
obj = context.active_object
assert obj
bprops = tool.Boundary.get_object_boundary_props(obj)
bprops.is_editing = False
for ifc_attribute, blender_property in EDITABLE_ATTRIBUTES.items():
setattr(bprops, blender_property, None)
@@ -386,8 +390,10 @@ class EditBoundaryAttributes(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
bprops = context.active_object.bim_boundary_properties
boundary = tool.Ifc.get_entity(context.active_object)
obj = context.active_object
assert obj
bprops = tool.Boundary.get_object_boundary_props(obj)
boundary = tool.Ifc.get_entity(obj)
attributes = dict()
for ifc_attribute, blender_property in EDITABLE_ATTRIBUTES.items():
obj = getattr(bprops, blender_property, None)
@@ -519,6 +525,7 @@ class HideBoundaries(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = tool.Boundary.get_boundary_props()
to_delete = set()
spaces = set()
for obj in context.selected_objects:
@@ -538,7 +545,7 @@ class HideBoundaries(bpy.types.Operator, tool.Ifc.Operator):
for boundary, boundary_obj in to_delete:
tool.Ifc.unlink(element=boundary)
bpy.data.objects.remove(boundary_obj)
context.scene.BIMBoundaryProperties.boundaries.clear()
props.boundaries.clear()
return {"FINISHED"}
@@ -549,7 +556,7 @@ class DecorateBoundaries(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.scene.BIMBoundaryProperties
props = tool.Boundary.get_boundary_props()
# filter not decorated boundaries and add decorations for them
decorated_boundaries = set([i.obj for i in props.boundaries])
active_boundaries = set()
+15 -4
View File
@@ -30,23 +30,24 @@ from bpy.props import (
CollectionProperty,
)
import bonsai.tool as tool
from typing import TYPE_CHECKING, Union
def space_filter(self, object):
def space_filter(self: "BIMObjectBoundaryProperties", object: bpy.types.Object) -> bool:
entity = tool.Ifc.get_entity(object)
if entity:
return entity.is_a("IfcSpace") or entity.is_a("IfcExternalSpatialElement")
return False
def boundary_filter(self, object):
def boundary_filter(self: "BIMObjectBoundaryProperties", object: bpy.types.Object) -> bool:
entity = tool.Ifc.get_entity(object)
if entity:
return entity.is_a("IfcRelSpaceBoundary")
return False
def element_filter(self, object):
def element_filter(self: "BIMObjectBoundaryProperties", object: bpy.types.Object) -> bool:
entity = tool.Ifc.get_entity(object)
if entity:
return entity.is_a("IfcElement")
@@ -60,6 +61,16 @@ class BIMObjectBoundaryProperties(PropertyGroup):
parent_boundary: PointerProperty(name="ParentBoundary", type=bpy.types.Object, poll=boundary_filter)
corresponding_boundary: PointerProperty(name="CorrespondingBoundary", type=bpy.types.Object, poll=boundary_filter)
if TYPE_CHECKING:
is_editing: bool
relating_space: Union[bpy.types.Object, None]
related_building_element: Union[bpy.types.Object, None]
parent_boundary: Union[bpy.types.Object, None]
corresponding_boundary: Union[bpy.types.Object, None]
class BIMBoundaryProperties(PropertyGroup):
boundaries: bpy.props.CollectionProperty(type=ObjProperty)
boundaries: bpy.props.CollectionProperty(type=ObjProperty, description="Decorated boundaries")
if TYPE_CHECKING:
boundaries: bpy.types.bpy_prop_collection_idprop[ObjProperty]
+4 -2
View File
@@ -64,10 +64,12 @@ class BIM_PT_Boundary(Panel):
return entity.is_a("IfcRelSpaceBoundary")
def draw(self, context):
props = context.active_object.BIMObjectProperties
obj = context.active_object
assert obj
props = obj.BIMObjectProperties
ifc_file = tool.Ifc.get()
boundary = ifc_file.by_id(props.ifc_definition_id)
self.bprops = context.active_object.bim_boundary_properties
self.bprops = tool.Boundary.get_object_boundary_props(obj)
if self.bprops.is_editing:
row = self.layout.row(align=True)
row.operator("bim.edit_boundary_attributes", icon="CHECKMARK", text="Save Attributes")
@@ -123,7 +123,9 @@ class CadTool(WorkSpaceTool):
add_layout_hotkey_operator(row, "Reset Vertex", "S_X", bpy.ops.bim.reset_vertex.__doc__, ui_context)
elif hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.subshape_type == "AXIS":
add_header_apply_button(layout, "Edit Axis", "bim.set_arc_index", "bim.set_arc_index", ui_context)
add_header_apply_button(
layout, "Edit Axis", "bim.edit_extrusion_axis", "bim.disable_editing_extrusion_axis", ui_context
)
row = layout.row(align=True)
add_layout_hotkey_operator(row, "Extend", "S_E", "Extends/reduces element to 3D cursor", ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
+3 -2
View File
@@ -325,10 +325,11 @@ class CostItemQuantitiesData:
@classmethod
def process_quantity_names(cls):
active_task_index = bpy.context.scene.BIMWorkScheduleProperties.active_task_index
total_tasks = len(bpy.context.scene.BIMTaskTreeProperties.tasks)
tprops = tool.Sequence.get_task_tree_props()
total_tasks = len(tprops.tasks)
if not total_tasks or active_task_index >= total_tasks:
return []
ifc_definition_id = bpy.context.scene.BIMTaskTreeProperties.tasks[active_task_index].ifc_definition_id
ifc_definition_id = tprops.tasks[active_task_index].ifc_definition_id
element = tool.Ifc.get().by_id(ifc_definition_id)
names = set()
qtos = ifcopenshell.util.element.get_psets(element, qtos_only=True)
+2 -1
View File
@@ -19,6 +19,7 @@
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.ifc import IfcStore
from bonsai.bim.module.cost.data import CostSchedulesData
@@ -461,7 +462,7 @@ class BIM_PT_cost_item_quantities(Panel):
total_cost_item_processes = len(self.props.cost_item_processes)
row2.label(text="Tasks ({})".format(total_cost_item_processes))
tprops = context.scene.BIMTaskTreeProperties
tprops = tool.Sequence.get_task_tree_props()
wprops = context.scene.BIMWorkScheduleProperties
if tprops.tasks and wprops.active_task_index < len(tprops.tasks):
if has_quantity_names:
@@ -100,7 +100,7 @@ classes = (
prop.Sheet,
prop.DocProperties,
prop.BIMCameraProperties,
prop.Literal,
prop.LiteralProps,
prop.BIMTextProperties,
prop.BIMAssignedProductProperties,
prop.BIMAnnotationProperties,
@@ -422,7 +422,7 @@ class CreateDrawing(bpy.types.Operator):
self.svg_writer.create_blank_svg(svg_path).draw_underlay(context.scene.render.filepath).save()
return svg_path
def get_linework_contexts(self, ifc, target_view) -> LineworkContexts:
def get_linework_contexts(self, ifc: ifcopenshell.file, target_view: str) -> LineworkContexts:
plan_body_target_contexts = []
plan_body_model_contexts = []
model_body_target_contexts = []
@@ -694,8 +694,8 @@ class CreateDrawing(bpy.types.Operator):
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")]
cache = IfcStore.get_cache()
[cache.remove(guid) for guid in invalidated_guids]
if cache := IfcStore.get_cache():
[cache.remove(guid) for guid in invalidated_guids]
# If we have already calculated it in the SVG in the past, don't recalculate
edited_guids = set()
@@ -1686,20 +1686,13 @@ class RemoveDrawingFromSheet(bpy.types.Operator, tool.Ifc.Operator):
return False
if active_item.reference_type == "TITLEBLOCK":
cls.poll_message_set("No effect deleting this.")
cls.poll_message_set("No effect deleting titleblock reference.")
return False
return True
def _execute(self, context):
reference = tool.Ifc.get().by_id(self.reference)
sheet = tool.Drawing.get_reference_document(reference)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.remove_drawing(reference, sheet)
tool.Ifc.run("document.remove_reference", reference=reference)
tool.Drawing.import_sheets()
ifc_file = tool.Ifc.get()
tool.Drawing.remove_drawing_from_sheet(ifc_file.by_id(self.reference))
class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
@@ -1759,7 +1752,7 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
sheet_builder = sheeter.SheetBuilder()
references = sheet_builder.build(sheet)
raster_references = [tool.Ifc.get_relative_uri(r) for r in references["RASTER"]]
raster_references = [tool.Ifc.get_uri(r, use_relative_path=True) for r in references["RASTER"]]
# These variables will be made available to the evaluated commands
svg = references["SHEET"]
@@ -1790,7 +1783,7 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
"document.edit_reference",
reference=reference,
attributes=tool.Drawing.generate_reference_attributes(
reference, Location=tool.Ifc.get_relative_uri(svg), Description="SHEET"
reference, Location=tool.Ifc.get_uri(svg, use_relative_path=True), Description="SHEET"
),
)
@@ -1800,7 +1793,9 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
"document.edit_reference",
reference=reference,
attributes=tool.Drawing.generate_reference_attributes(
reference, Location=tool.Ifc.get_relative_uri(raster_reference), Description="RASTER"
reference,
Location=tool.Ifc.get_uri(raster_reference, use_relative_path=True),
Description="RASTER",
),
)
@@ -2133,7 +2128,7 @@ class RemoveDrawing(bpy.types.Operator, tool.Ifc.Operator):
for drawing in drawings:
sheet_references = tool.Drawing.get_sheet_references(drawing)
for reference in sheet_references:
bpy.ops.bim.remove_drawing_from_sheet(reference=reference.id())
tool.Drawing.remove_drawing_from_sheet(reference)
core.remove_drawing(tool.Ifc, tool.Drawing, drawing=drawing)
# In case we removed the active drawing.
@@ -2462,21 +2457,8 @@ class AddSchedule(bpy.types.Operator, tool.Ifc.Operator):
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
def _execute(self, context):
filepath = self.filepath
if self.use_relative_path:
ifc_path = tool.Ifc.get_path()
if os.path.isfile(ifc_path):
ifc_path = os.path.dirname(ifc_path)
# taking into account different drives on windows
if Path(filepath).drive == Path(ifc_path).drive:
filepath = os.path.relpath(filepath, ifc_path)
core.add_document(
tool.Ifc,
tool.Drawing,
"SCHEDULE",
uri=filepath,
)
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)
@@ -2660,21 +2642,8 @@ class AddReference(bpy.types.Operator, tool.Ifc.Operator):
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
def _execute(self, context):
filepath = self.filepath
if self.use_relative_path:
ifc_path = tool.Ifc.get_path()
if os.path.isfile(ifc_path):
ifc_path = os.path.dirname(ifc_path)
# taking into account different drives on windows
if Path(filepath).drive == Path(ifc_path).drive:
filepath = os.path.relpath(filepath, ifc_path)
core.add_document(
tool.Ifc,
tool.Drawing,
"REFERENCE",
uri=filepath,
)
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)
@@ -3308,11 +3277,8 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator):
return {"RUNNING_MODAL"}
def _execute(self, context):
abs_path = Path(self.filepath)
if self.use_relative_path:
image_filepath = abs_path.relative_to(Path(tool.Ifc.get_path()).parent)
else:
image_filepath = abs_path
abs_path = Path(self.filepath).absolute().resolve()
image_filepath = Path(tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path))
ifc_file = tool.Ifc.get()
if self.override_existing_image:
+66 -2
View File
@@ -44,6 +44,7 @@ from bpy.props import (
CollectionProperty,
BoolVectorProperty,
)
from typing import TYPE_CHECKING, Literal
diagram_scales_enum = []
@@ -294,12 +295,25 @@ class Drawing(PropertyGroup):
is_drawing: BoolProperty(name="Is Drawing", default=False)
is_expanded: BoolProperty(name="Is Expanded", default=True)
if TYPE_CHECKING:
ifc_definition_id: int
name: str
target_view: str
is_selected: bool
is_drawing: bool
is_expanded: bool
class Document(PropertyGroup):
ifc_definition_id: IntProperty(name="IFC Definition ID")
name: StringProperty(name="Name", update=update_document_name)
identification: StringProperty(name="Identification")
if TYPE_CHECKING:
ifc_definition_id: int
name: str
identification: str
class Sheet(PropertyGroup):
ifc_definition_id: IntProperty(name="IFC Definition ID")
@@ -310,6 +324,15 @@ class Sheet(PropertyGroup):
reference_type: StringProperty(name="Reference Type")
is_expanded: BoolProperty(name="Is Expanded", default=False)
if TYPE_CHECKING:
ifc_definition_id: int
identification: str
name: str
is_sheet: bool
is_selected: bool
reference_type: str
is_expanded: bool
class DrawingStyle(PropertyGroup):
name: StringProperty(name="Name", get=get_drawing_style_name, set=set_drawing_style_name)
@@ -408,6 +431,47 @@ class DocProperties(PropertyGroup):
description="Upon import, these classes will display as wireframe.\nEx: IfcVirtualelement, IfcSpace",
)
if TYPE_CHECKING:
should_use_underlay_cache: bool
should_use_linework_cache: bool
should_use_annotation_cache: bool
is_editing_drawings: bool
is_editing_schedules: bool
is_editing_references: bool
target_view: Literal["PLAN_VIEW", "ELEVATION_VIEW", "SECTION_VIEW", "REFLECTED_PLAN_VIEW", "MODEL_VIEW"]
location_hint: str
drawings: bpy.types.bpy_prop_collection_idprop[Drawing]
active_drawing_id: int
active_drawing_index: int
current_drawing_index: int
schedules: bpy.types.bpy_prop_collection_idprop[Document]
active_schedule_index: int
references: bpy.types.bpy_prop_collection_idprop[Document]
active_reference_index: int
titleblock: str
is_editing_sheets: bool
sheets: bpy.types.bpy_prop_collection_idprop[Sheet]
active_sheet_index: int
ifc_files: bpy.types.bpy_prop_collection_idprop[StrProperty]
drawing_styles: bpy.types.bpy_prop_collection_idprop[DrawingStyle]
should_draw_decorations: bool
sheets_dir: str
layouts_dir: str
titleblocks_dir: str
drawings_dir: str
stylesheet_path: str
schedules_stylesheet_path: str
markers_path: str
symbols_path: str
patterns_path: str
shadingstyles_path: str
shadingstyle_default: str
drawing_font: str
magic_font_scale: float
imperial_precision: str
tolerance: float
classes_to_wireframe: str
class BIMCameraProperties(PropertyGroup):
linework_mode: EnumProperty(
@@ -490,7 +554,7 @@ BOX_ALIGNMENT_POSITIONS = [
]
class Literal(PropertyGroup):
class LiteralProps(PropertyGroup):
def set_box_alignment(self, new_value):
markers = new_value.count(True)
if not markers:
@@ -535,7 +599,7 @@ class Literal(PropertyGroup):
class BIMTextProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing", default=False)
literals: CollectionProperty(name="Literals", type=Literal)
literals: CollectionProperty(name="Literals", type=LiteralProps)
font_size: EnumProperty(
items=[
("1.8", "1.8 - Small", ""),
@@ -402,6 +402,7 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Update Representation"
bl_description = (
"Write selected objects representations to IFC.\n"
"Converting to tesellation will also unassign material layer/profile sets.\n"
"A star in the operator name indicates that active object representation in IFC is not yet synced with Blender.\n"
"ALT+CLICK to apply openings to the mesh"
)
@@ -478,6 +479,7 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
if tool.Ifc.is_moved(obj) or tool.Geometry.is_scaled(obj):
core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
old_representation = self.file.by_id(obj.data.BIMMeshProperties.ifc_definition_id)
if material and material.is_a() in ["IfcMaterialProfileSet", "IfcMaterialLayerSet"]:
if self.ifc_representation_class == "IfcTessellatedFaceSet":
# We are explicitly casting to a tessellation, so remove all parametric materials.
@@ -489,9 +491,14 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
tool.Material.ensure_material_unassigned([product])
else:
# These objects are parametrically based on an axis and should not be modified as a mesh
return
if extrusion := tool.Model.get_extrusion(old_representation):
self.report(
{"INFO"},
f"Representation for '{obj.name}' wasn't updated because "
"it has material set and extrusion that probably was defined parametrically.",
)
return
old_representation = self.file.by_id(obj.data.BIMMeshProperties.ifc_definition_id)
context_of_items = old_representation.ContextOfItems
# TODO: remove this code a bit later
@@ -1075,7 +1082,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
@staticmethod
def duplicate_item(obj: bpy.types.Object) -> None:
props = bpy.context.scene.BIMGeometryProperties
props = tool.Geometry.get_geometry_props()
item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
new_item = ifcopenshell.util.element.copy_deep(tool.Ifc.get(), item)
new_obj = obj.copy()
@@ -2671,12 +2678,13 @@ class EditRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.Operator):
shape_aspect, representation_item
)
styles = tool.Geometry.get_shape_aspect_styles(element, shape_aspect, representation_item)
# TODO this looks wrong to me. In theory styles can be > 1 (e.g. curve
# styles) and then the usecase will assign the wrong style.
tool.Ifc.run(
"style.assign_representation_styles",
shape_representation=shape_aspect_representation,
styles=styles,
)
tool.Geometry.get_shape_aspect_styles(element, shape_aspect, representation_item)
tool.Geometry.reload_representation(obj)
# reload items ui
@@ -2749,17 +2757,17 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = context.active_object
assert obj
props = context.scene.BIMGeometryProperties
props = tool.Geometry.get_geometry_props()
if previous_obj := props.representation_obj:
previous_obj.hide_set(False)
props.representation_obj = obj
obj.hide_set(True)
tool.Geometry.lock_object(obj)
context.scene.BIMGeometryProperties.is_changing_mode = True
if context.scene.BIMGeometryProperties.mode != "ITEM":
context.scene.BIMGeometryProperties.mode = "ITEM"
context.scene.BIMGeometryProperties.is_changing_mode = False
props.is_changing_mode = True
if props.mode != "ITEM":
props.mode = "ITEM"
props.is_changing_mode = False
tool.Loader.load_settings()
+11 -1
View File
@@ -32,7 +32,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import Optional, TYPE_CHECKING
from typing import Optional, TYPE_CHECKING, Union
def get_contexts(self, context):
@@ -302,3 +302,13 @@ class BIMGeometryProperties(PropertyGroup):
type=bpy.types.Object,
poll=is_object_valid_for_representation_copy,
)
if TYPE_CHECKING:
should_use_presentation_style_assignment: bool
should_force_faceted_brep: bool
should_force_triangulation: bool
is_changing_mode: bool
mode: str
representation_obj: Union[bpy.types.Object, None]
item_objs: bpy.types.bpy_prop_collection_idprop[RepresentationItemObject]
representation_from_object: Union[bpy.types.Object, None]
+2 -2
View File
@@ -45,12 +45,12 @@ class BIMLibraryProperties(PropertyGroup):
editing_mode: EnumProperty(
name="Editing Mode",
items=(
("-", "-", ""),
("NONE", "NONE", ""),
("LIBRARY", "LIBRARY", ""),
("REFERENCES", "REFERENCES", ""),
("REFERENCE", "REFERENCE", ""),
),
default="-",
default="NONE",
)
library_attributes: CollectionProperty(name="Library Attributes", type=Attribute)
active_library_id: IntProperty(name="Active Library Id")
@@ -113,6 +113,7 @@ def update_sun_path(self):
sun_props.sun_distance = self.sun_path_size
sun_props.latitude = self.latitude
sun_props.longitude = self.longitude
sun_props.year = self.year
sun_props.month = self.month
sun_props.day = self.day
sun_props.time = self.hour + (self.minute / 60)
@@ -382,6 +383,7 @@ class BIMSolarProperties(PropertyGroup):
longitude: FloatProperty(name="Longitude", min=-180, max=180, update=update_latlong)
timezone: StringProperty(name="Timezone", default="Etc/GMT")
true_north: FloatProperty(name="True North", min=-180, max=180, update=update_true_north)
year: IntProperty(name="Year", min=1, max=9999, default=2025, update=update_date)
month: IntProperty(name="Month", min=1, max=12, default=1, update=update_date)
day: IntProperty(name="Date", min=1, max=31, default=1, update=update_date)
hour: IntProperty(name="Hour", min=0, max=23, default=12, update=update_hourminute)
+2
View File
@@ -172,6 +172,8 @@ class BIM_PT_solar(bpy.types.Panel):
if SolarData.data["true_north"] is not None:
row.operator("bim.import_true_north", icon="IMPORT", text="")
row = self.layout.row()
row.prop(props, "year")
row = self.layout.row(align=True)
row.prop(
props,
+42 -48
View File
@@ -41,6 +41,7 @@ from . import (
roof,
mep,
)
from typing import NamedTuple
classes = (
array.AddArray,
@@ -200,37 +201,47 @@ classes = (
addon_keymaps = []
class ToolsData(NamedTuple):
tool: type[bpy.types.WorkSpaceTool]
after: set[str]
separator: bool
group: bool
tools: tuple[ToolsData, ...] = (
ToolsData(workspace.BimTool, {"bim.explore_tool"}, False, False),
ToolsData(workspace.DuctTool, {"bim.explore_tool"}, False, True),
ToolsData(workspace.PipeTool, {"bim.duct_tool"}, False, False),
ToolsData(workspace.CableCarrierTool, {"bim.pipe_tool"}, False, False),
ToolsData(workspace.CableTool, {"bim.cable_carrier_tool"}, False, False),
ToolsData(workspace.FurnitureTool, {"bim.explore_tool"}, False, True),
ToolsData(workspace.SanitaryTerminalTool, {"bim.furniture_tool"}, False, False),
ToolsData(workspace.LightFixtureTool, {"bim.sanitary_terminal_tool"}, False, False),
ToolsData(workspace.ElectricApplianceTool, {"bim.light_fixture_tool"}, False, False),
ToolsData(workspace.GeographicElement, {"bim.electric_appliance_tool"}, False, False),
ToolsData(workspace.ColumnTool, {"bim.explore_tool"}, False, True),
ToolsData(workspace.BeamTool, {"bim.column_tool"}, False, False),
ToolsData(workspace.MemberTool, {"bim.beam_tool"}, False, False),
ToolsData(workspace.PlateTool, {"bim.member_tool"}, False, False),
ToolsData(workspace.FootingTool, {"bim.plate_tool"}, False, False),
ToolsData(workspace.PileTool, {"bim.footing_tool"}, False, False),
ToolsData(workspace.DoorTool, {"bim.explore_tool"}, False, True),
ToolsData(workspace.WindowTool, {"bim.door_tool"}, False, False),
ToolsData(workspace.SlabTool, {"bim.explore_tool"}, False, True),
ToolsData(workspace.RoofTool, {"bim.slab_tool"}, False, False),
ToolsData(workspace.StairFlightTool, {"bim.roof_tool"}, False, False),
ToolsData(workspace.RampFlightTool, {"bim.stair_flight_tool"}, False, False),
ToolsData(workspace.WallTool, {"bim.explore_tool"}, True, True),
ToolsData(workspace.RailingTool, {"bim.wall_tool"}, False, False),
)
def register():
if not bpy.app.background:
bpy.utils.register_tool(workspace.BimTool, after={"bim.explore_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.DuctTool, after={"bim.explore_tool"}, separator=False, group=True)
bpy.utils.register_tool(workspace.PipeTool, after={"bim.duct_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.CableCarrierTool, after={"bim.pipe_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.CableTool, after={"bim.cable_carrier_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.FurnitureTool, after={"bim.explore_tool"}, separator=False, group=True)
bpy.utils.register_tool(
workspace.SanitaryTerminalTool, after={"bim.furniture_tool"}, separator=False, group=False
)
bpy.utils.register_tool(
workspace.LightFixtureTool, after={"bim.sanitary_terminal_tool"}, separator=False, group=False
)
bpy.utils.register_tool(
workspace.ElectricApplianceTool, after={"bim.light_fixture_tool"}, separator=False, group=False
)
bpy.utils.register_tool(workspace.ColumnTool, after={"bim.explore_tool"}, separator=False, group=True)
bpy.utils.register_tool(workspace.BeamTool, after={"bim.column_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.MemberTool, after={"bim.beam_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.PlateTool, after={"bim.member_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.FootingTool, after={"bim.plate_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.PileTool, after={"bim.footing_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.DoorTool, after={"bim.explore_tool"}, separator=False, group=True)
bpy.utils.register_tool(workspace.WindowTool, after={"bim.door_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.SlabTool, after={"bim.explore_tool"}, separator=False, group=True)
bpy.utils.register_tool(workspace.RoofTool, after={"bim.slab_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.StairFlightTool, after={"bim.roof_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.RampFlightTool, after={"bim.stair_flight_tool"}, separator=False, group=False)
bpy.utils.register_tool(workspace.WallTool, after={"bim.explore_tool"}, separator=True, group=True)
bpy.utils.register_tool(workspace.RailingTool, after={"bim.wall_tool"}, separator=False, group=False)
for tool_data in tools:
bpy.utils.register_tool(
tool_data.tool, after=tool_data.after, separator=tool_data.separator, group=tool_data.group
)
bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties)
bpy.types.Scene.BIMPolylineProperties = bpy.props.PointerProperty(type=prop.BIMPolylineProperties)
@@ -251,25 +262,8 @@ def register():
def unregister():
if not bpy.app.background:
bpy.utils.unregister_tool(workspace.WallTool)
bpy.utils.unregister_tool(workspace.RailingTool)
bpy.utils.unregister_tool(workspace.SlabTool)
bpy.utils.unregister_tool(workspace.RoofTool)
bpy.utils.unregister_tool(workspace.DoorTool)
bpy.utils.unregister_tool(workspace.WindowTool)
bpy.utils.unregister_tool(workspace.ColumnTool)
bpy.utils.unregister_tool(workspace.BeamTool)
bpy.utils.unregister_tool(workspace.MemberTool)
bpy.utils.unregister_tool(workspace.FootingTool)
bpy.utils.unregister_tool(workspace.FurnitureTool)
bpy.utils.unregister_tool(workspace.SanitaryTerminalTool)
bpy.utils.unregister_tool(workspace.LightFixtureTool)
bpy.utils.unregister_tool(workspace.ElectricApplianceTool)
bpy.utils.unregister_tool(workspace.DuctTool)
bpy.utils.unregister_tool(workspace.PipeTool)
bpy.utils.unregister_tool(workspace.CableCarrierTool)
bpy.utils.unregister_tool(workspace.CableTool)
bpy.utils.unregister_tool(workspace.BimTool)
for tool_data in reversed(tools):
bpy.utils.unregister_tool(tool_data.tool)
del bpy.types.Scene.BIMModelProperties
del bpy.types.Scene.BIMPolylineProperties
@@ -76,11 +76,13 @@ class AuthoringData:
cls.data["paginated_relating_types"] = cls.paginated_relating_types()
cls.data["type_thumbnail"] = cls.type_thumbnail() # Only after .relating_type_id_current()
cls.data["materials"] = cls.materials()
cls.data["is_voidable_element"] = cls.is_voidable_element()
cls.data["has_visible_openings"] = cls.has_visible_openings()
cls.data["has_visible_boundaries"] = cls.has_visible_boundaries()
cls.data["active_class"] = cls.active_class()
cls.data["active_material_usage"] = cls.active_material_usage()
cls.data["has_extrusion"] = cls.has_extrusion()
cls.data["is_representation_item_active"] = cls.is_representation_item_active()
# After is_representation_item_active.
cls.data["is_representation_item_swept_solid"] = cls.is_representation_item_swept_solid()
@@ -114,6 +116,12 @@ class AuthoringData:
def type_thumbnail(cls):
return cls.type_thumbnails.get(int(cls.data["relating_type_id_current"] or 0), 0)
@classmethod
def materials(cls):
results = [("0", "None", "No material")]
results.extend([(str(m.id()), m.Name or "Unnamed", "") for m in tool.Ifc.get().by_type("IfcMaterial")])
return results
@classmethod
def total_types(cls):
return len(cls.data["type_elements_filtered"])
@@ -240,6 +248,19 @@ class AuthoringData:
if (obj := tool.Blender.get_active_object()) and (element := tool.Ifc.get_entity(obj)):
return tool.Model.get_usage_type(element)
@classmethod
def has_extrusion(cls) -> bool:
if not (obj := tool.Blender.get_active_object()) or not (
representation := tool.Geometry.get_active_representation(obj)
):
return False
# Skip representation items.
if not representation.is_a("IfcShapeRepresentation"):
return False
return bool(tool.Model.get_extrusion(representation))
@classmethod
def is_representation_item_active(cls) -> bool:
if not (obj := tool.Blender.get_active_object()):
@@ -88,7 +88,49 @@ def update_door_modifier_representation(obj: bpy.types.Object) -> None:
# (Model/Body defined only BEFORE Plan/Body to prevent #2744)
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
representation_data["context"] = body
representation_data["part_of_product"] = ifcopenshell.util.representation.get_part_of_product(element, body)
model_representation = ifcopenshell.api.run("geometry.add_door_representation", ifc_file, **representation_data)
representation_data["part_of_product"] = None
if fallback_material := (int(props.lining_material) or int(props.panel_material)):
lining_material = tool.Ifc.get().by_id(int(props.lining_material) or fallback_material)
panel_material = tool.Ifc.get().by_id(int(props.panel_material) or fallback_material)
should_create_new_material_set = False
if material := ifcopenshell.util.element.get_material(element):
if (
material.is_a("IfcMaterialConstituent")
and len(names := [c.Name for c in material.MaterialConstituents]) == 2
and set(names) == {"Lining", "Framing"}
):
should_create_new_material_set = False
else:
should_create_new_material_set = True
ifcopenshell.api.material.unassign_material(ifc_file, products=[element])
if not material.is_a("IfcMaterial") and not ifc_file.get_total_inverses(material):
ifcopenshell.api.material.remove_material_set(ifc_file, material=material)
else:
should_create_new_material_set = True
if should_create_new_material_set:
material_set = ifcopenshell.api.material.add_material_set(ifc_file, set_type="IfcMaterialConstituentSet")
ifcopenshell.api.material.add_constituent(
ifc_file, constituent_set=material_set, material=lining_material, name="Lining"
)
ifcopenshell.api.material.add_constituent(
ifc_file, constituent_set=material_set, material=panel_material, name="Framing"
)
ifcopenshell.api.material.assign_material(ifc_file, products=[element], material=material_set)
styles = {
"Lining": ifcopenshell.util.representation.get_material_style(lining_material, body),
"Framing": ifcopenshell.util.representation.get_material_style(panel_material, body),
}
for item in model_representation.Items:
if aspect := ifcopenshell.util.representation.get_item_shape_aspect(model_representation, item):
if style := styles.get(aspect.Name, None):
ifcopenshell.api.style.assign_item_style(ifc_file, item=item, style=style)
elif material := ifcopenshell.util.element.get_material(element):
ifcopenshell.api.material.unassign_material(ifc_file, products=[element])
if not material.is_a("IfcMaterial") and not ifc_file.get_total_inverses(material):
ifcopenshell.api.material.remove_material_set(ifc_file, material=material)
tool.Model.replace_object_ifc_representation(body, obj, model_representation)
# Body/PLAN_VIEW representation
+18 -2
View File
@@ -114,7 +114,7 @@ class AddDefaultType(bpy.types.Operator, tool.Ifc.Operator):
props.ifc_predefined_type = "FOOTING_BEAM"
props.representation_template = "PROFILESET"
elif self.ifc_element_type == "IfcPileType":
props.ifc_predefined_type = "BORED"
props.ifc_predefined_type = "COHESION"
props.representation_template = "PROFILESET"
elif self.ifc_element_type == "IfcDuctSegmentType":
@@ -143,6 +143,13 @@ class AddDefaultType(bpy.types.Operator, tool.Ifc.Operator):
elif self.ifc_element_type == "IfcElectricApplianceType":
props.ifc_predefined_type = "WASHINGMACHINE"
props.representation_template = "MESH"
elif self.ifc_element_type == "IfcGeographicElementType":
if tool.Ifc.get().schema == "IFC4":
props.ifc_predefined_type = "USERDEFINED"
props.ifc_userdefined_type = "VEGETATION"
elif tool.Ifc.get().schema == "IFC4X3":
props.ifc_predefined_type = "VEGETATION"
props.representation_template = "MESH"
bpy.ops.bim.add_element()
@@ -319,6 +326,12 @@ class AddConstrTypeInstance(bpy.types.Operator, tool.Ifc.Operator):
instance_class = ifcopenshell.util.type.get_applicable_entities(ifc_class, tool.Ifc.get().schema)[0]
material = ifcopenshell.util.element.get_material(relating_type)
existing_context = None
for existing_occurrence in ifcopenshell.util.element.get_types(relating_type):
if existing_obj := tool.Ifc.get_object(existing_occurrence):
existing_context = tool.Geometry.get_active_representation_context(existing_obj)
break
if material and material.is_a("IfcMaterialProfileSet"):
if obj := profile.DumbProfileGenerator(relating_type).generate():
tool.Blender.select_and_activate_single_object(context, obj)
@@ -408,7 +421,10 @@ class AddConstrTypeInstance(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
bonsai.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if existing_context:
representation = ifcopenshell.util.representation.get_representation(element, existing_context)
else:
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation and element.Representation:
representation = element.Representation.Representations[0]
@@ -49,6 +49,12 @@ def get_relating_type_id(self, context):
return AuthoringData.data["relating_type_id"]
def get_materials(self, context):
if not AuthoringData.is_loaded:
AuthoringData.load()
return AuthoringData.data["materials"]
def update_ifc_class(self, context):
bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class)
AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id()
@@ -621,6 +627,10 @@ class BIMDoorProperties(PropertyGroup):
frame_thickness: bpy.props.FloatProperty(name="Window Frame Thickness", default=0.035, subtype="DISTANCE")
frame_depth: bpy.props.FloatProperty(name="Window Frame Depth", default=0.035, subtype="DISTANCE")
# Material properties
panel_material: bpy.props.EnumProperty(name="Panel Material", items=get_materials, options=set())
lining_material: bpy.props.EnumProperty(name="Lining Material", items=get_materials, options=set())
def get_general_kwargs(self, convert_to_project_units=False):
kwargs = {
"door_type": self.door_type,
+3 -3
View File
@@ -94,18 +94,18 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
core.generate_spaces_from_walls(tool.Ifc, tool.Spatial, tool.Collector)
class ToggleSpaceVisibility(bpy.types.Operator, tool.Ifc.Operator):
class ToggleSpaceVisibility(bpy.types.Operator):
bl_idname = "bim.toggle_space_visibility"
bl_label = "Toggle Space Visibility"
bl_description = "Toggle spaces visilibility types between WIRED and TEXTURED."
bl_options = {"REGISTER"}
bl_description = "Change the space visibility"
def execute(self, context):
core.toggle_space_visibility(tool.Ifc, tool.Spatial)
return {"FINISHED"}
class ToggleHideSpaces(bpy.types.Operator, tool.Ifc.Operator):
class ToggleHideSpaces(bpy.types.Operator):
bl_idname = "bim.toggle_hide_spaces"
bl_label = "Toggle Hide Spaces"
bl_options = {"REGISTER"}
+7 -2
View File
@@ -534,15 +534,20 @@ class BIM_PT_door(bpy.types.Panel):
self.layout.prop(props, prop)
lining_props = props.get_lining_kwargs()
self.layout.label(text="Lining properties")
self.layout.label(text="Lining Properties")
for prop in lining_props:
self.layout.prop(props, prop)
panel_props = props.get_panel_kwargs()
self.layout.label(text="Panel properties")
self.layout.label(text="Panel Properties")
for prop in panel_props:
self.layout.prop(props, prop)
self.layout.use_property_split = True
self.layout.label(text="Material Properties")
self.layout.prop(props, "lining_material")
self.layout.prop(props, "panel_material")
update_door_modifier_bmesh(context)
else:
@@ -238,6 +238,17 @@ class ElectricApplianceTool(BimTool):
ifc_element_type = "IfcElectricApplianceType"
class GeographicElement(BimTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
bl_idname = "bim.geographic_element_tool"
bl_label = "Geographic Element Tool"
bl_description = "Create and edit terrain and vegetation landscape geographic elements"
bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.geographicelement")
bl_widget = None
ifc_element_type = "IfcGeographicElementType"
class ColumnTool(BimTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
@@ -1042,7 +1053,7 @@ class EditObjectUI:
row.label(text="Mode") if ui_context != "TOOL_HEADER" else row
if AuthoringData.data["active_material_usage"] == "LAYER3":
if len(context.selected_objects) == 1:
if len(context.selected_objects) == 1 and AuthoringData.data["has_extrusion"]:
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
add_layout_hotkey_operator(row, "Edit Profile", "S_E", "", ui_context)
elif (
@@ -1231,7 +1242,11 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if self.active_material_usage == "LAYER3":
# Edit LAYER3 profile
if bpy.context.active_object and bpy.context.active_object.mode == "OBJECT":
if (
active_object.mode == "OBJECT"
and (representation := tool.Geometry.get_active_representation(active_object))
and tool.Model.get_extrusion(representation)
):
bpy.ops.bim.enable_editing_extrusion_profile()
elif self.active_material_usage == "LAYER2":
# Extend LAYER2 to cursor
+8 -9
View File
@@ -217,17 +217,16 @@ class ActorData:
@classmethod
def load(cls):
cls.data = {
"the_actor": cls.the_actor(),
"actors": cls.actors(),
"actor_class_enum": cls.actor_class_enum(),
"get_actor_type_enum": cls.get_actor_type_enum(),
}
cls.is_loaded = True
cls.data["actor_class"] = cls.actor_class()
cls.data["actor_type"] = cls.actor_type()
cls.data["the_actor"] = cls.the_actor()
cls.data["actors"] = cls.actors()
@classmethod
def the_actor(cls):
ifc_class = bpy.context.scene.BIMOwnerProperties.actor_type
if not (ifc_class := bpy.context.scene.BIMOwnerProperties.actor_type):
ifc_class = cls.actor_type()[0][0]
return [(str(p.id()), p[0] or "Unnamed", "") for p in tool.Ifc.get().by_type(ifc_class)]
@classmethod
@@ -249,7 +248,7 @@ class ActorData:
return actors
@classmethod
def actor_class_enum(cls) -> list[tuple[str, str, str]]:
def actor_class(cls) -> list[tuple[str, str, str]]:
version = tool.Ifc.get_schema()
return [
("IfcActor", "Actor", get_entity_doc(version, "IfcActor").get("description", "")),
@@ -257,7 +256,7 @@ class ActorData:
]
@classmethod
def get_actor_type_enum(cls) -> list[tuple[str, str, str]]:
def actor_type(cls) -> list[tuple[str, str, str]]:
version = tool.Ifc.get_schema()
return [
("IfcPerson", "Person", get_entity_doc(version, "IfcPerson").get("description", "")),
+6 -6
View File
@@ -65,16 +65,16 @@ def update_actor_class(self, context):
ActorData.data["actors"] = ActorData.actors()
def get_actor_class_enum(self, context):
def get_actor_class(self, context):
if not ActorData.is_loaded:
ActorData.load()
return ActorData.data["actor_class_enum"]
return ActorData.data["actor_class"]
def get_actor_type_enum(self, context):
def get_actor_type(self, context):
if not ActorData.is_loaded:
ActorData.load()
return ActorData.data["actor_type_enum"]
return ActorData.data["actor_type"]
class BIMOwnerProperties(PropertyGroup):
@@ -106,12 +106,12 @@ class BIMOwnerProperties(PropertyGroup):
active_actor_id: IntProperty(name="Active Actor Id")
actor_attributes: CollectionProperty(name="Actor Attributes", type=Attribute)
actor_class: EnumProperty(
items=get_actor_class_enum,
items=get_actor_class,
name="Actor Type",
update=update_actor_class,
)
actor_type: EnumProperty(
items=get_actor_type_enum,
items=get_actor_type,
name="Actor Type",
update=update_actor_type,
)
@@ -20,6 +20,7 @@ import bpy
from . import ui, prop, operator, workspace, gizmo, decorator
classes = (
operator.AddProjectLibrary,
operator.AppendEntireLibrary,
operator.AppendInspectedLinkedElement,
operator.AppendLibraryElement,
@@ -35,6 +36,7 @@ classes = (
operator.DisableCulling,
operator.DisableEditingHeader,
operator.EditHeader,
operator.EditProjectLibrary,
operator.EnableCulling,
operator.EnableEditingHeader,
operator.ExportIFC,
+23 -5
View File
@@ -118,6 +118,7 @@ class ProjectLibraryData:
cls.data["project_libraries"] = cls.project_libraries()
# After .project_libraries().
cls.data["project_libraries_enum"] = cls.project_libraries_enum()
cls.data["parent_libraries_enum"] = cls.parent_libraries_enum()
cls.is_loaded = True
@classmethod
@@ -132,13 +133,30 @@ class ProjectLibraryData:
return results
@classmethod
def project_libraries_enum(cls) -> list[tuple[str, str, str]]:
def project_libraries_enum(cls) -> list[tuple[str, str, str, str, int]]:
results = [
("*", "All Libraries", "Show all elements"),
("-", "No Library", "Show elements without library assigned"),
("*", "All Libraries", "Show all elements", "", 0),
("-", "No Library", "Show elements without library assigned", "", 1),
]
for data in cls.data["project_libraries"].values():
results.append((str(data["id"]), data["Name"] or "", data["Description"] or ""))
props = tool.Project.get_project_props()
libs = []
for i, data in enumerate(cls.data["project_libraries"].values(), len(results)):
icon = "GREASEPENCIL" if props.editing_project_library_id == data["id"] else ""
libs.append((str(data["id"]), data["Name"] or "Unnamed", data["Description"] or "", icon, i))
libs.sort(key=lambda x: x[1])
results += libs
return results
@classmethod
def parent_libraries_enum(cls) -> list[tuple[str, str, str]]:
results: list[tuple[str, str, str]] = []
library_file = IfcStore.library_file
if library_file is None or library_file.schema == "IFC2X3":
return results
project = library_file.by_type("IfcProject")[0]
results.append((str(project.id()), f"IfcProject {project.Name or 'Unnamed'}", project.Description or ""))
for library_id, data in cls.data["project_libraries"].items():
results.append((str(library_id), data["Name"] or "Unnamed", data["Description"] or ""))
return results
+150 -35
View File
@@ -25,10 +25,13 @@ import tempfile
import traceback
import subprocess
import datetime
import ifcopenshell.api.attribute
import numpy as np
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.nest
import ifcopenshell.api.project
import ifcopenshell.api.root
import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.util.file
@@ -40,6 +43,7 @@ import ifcopenshell.util.representation
import ifcopenshell.util.shape
import ifcopenshell.util.unit
import bonsai.bim.handler
import bonsai.bim.helper
import bonsai.bim.schema
import bonsai.tool as tool
import bonsai.core.project as core
@@ -54,7 +58,7 @@ from collections import defaultdict
from mathutils import Vector, Matrix
from bpy.app.handlers import persistent
from ifcopenshell.geom import ShapeElementType
from bonsai.bim.module.project.data import LinksData
from bonsai.bim.module.project.data import LinksData, ProjectLibraryData
from bonsai.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator, MeasureDecorator
from bonsai.bim.module.model.decorator import PolylineDecorator
from bonsai.bim.module.model.polyline import PolylineOperator
@@ -288,11 +292,17 @@ class ChangeLibraryElement(bpy.types.Operator):
is_declarable = project_libraries_exist and element.is_a("IfcObjectDefinition")
new.is_declarable = is_declarable
selected_library = self.props.selected_project_library
# is_declared.
if not is_declarable:
new.is_declared = False
elif (has_context := element.HasContext) and has_context[0].RelatingContext.is_a("IfcProjectLibrary"):
new.is_declared = True
elif has_context := element.HasContext:
relating_context: ifcopenshell.entity_instance
relating_context = has_context[0].RelatingContext
if selected_library in ("-", "*"):
new.is_declared = relating_context.is_a("IfcProjectLibrary")
else:
new.is_declared = relating_context == self.library_file.by_id(int(selected_library))
# is_appended.
try:
@@ -313,7 +323,7 @@ class RewindLibrary(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
self.props = context.scene.BIMProjectProperties
self.props = tool.Project.get_project_props()
total_breadcrumbs = len(self.props.library_breadcrumb)
if total_breadcrumbs < 2:
bpy.ops.bim.refresh_library()
@@ -328,9 +338,13 @@ class RewindLibrary(bpy.types.Operator):
class AssignLibraryDeclaration(bpy.types.Operator):
bl_idname = "bim.assign_library_declaration"
bl_label = "Assign Library Declaration"
bl_description = "Assign element to the active library. If no specific library selected, will assign it to the first library in the file."
bl_options = {"REGISTER", "UNDO"}
definition: bpy.props.IntProperty()
if TYPE_CHECKING:
definition: int
def execute(self, context):
IfcStore.begin_transaction(self)
IfcStore.library_file.begin_transaction()
@@ -341,17 +355,22 @@ class AssignLibraryDeclaration(bpy.types.Operator):
return result
def _execute(self, context):
self.props = context.scene.BIMProjectProperties
self.file = IfcStore.library_file
ifcopenshell.api.run(
"project.assign_declaration",
self.file,
definitions=[self.file.by_id(self.definition)],
relating_context=self.file.by_type("IfcProjectLibrary")[0],
props = tool.Project.get_project_props()
library_file = IfcStore.library_file
assert library_file
if props.selected_project_library in ("*", "-"):
project_library = library_file.by_type("IfcProjectLibrary")[0]
else:
project_library = library_file.by_id(int(props.selected_project_library))
ifcopenshell.api.project.assign_declaration(
library_file,
definitions=[library_file.by_id(self.definition)],
relating_context=project_library,
)
element_name = self.props.active_library_element
bpy.ops.bim.rewind_library()
bpy.ops.bim.change_library_element(element_name=element_name)
tool.Project.update_current_library_page()
return {"FINISHED"}
def rollback(self, data):
@@ -367,6 +386,9 @@ class UnassignLibraryDeclaration(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
definition: bpy.props.IntProperty()
if TYPE_CHECKING:
definition: int
def execute(self, context):
IfcStore.begin_transaction(self)
IfcStore.library_file.begin_transaction()
@@ -377,17 +399,18 @@ class UnassignLibraryDeclaration(bpy.types.Operator):
return result
def _execute(self, context):
self.props = context.scene.BIMProjectProperties
self.file = IfcStore.library_file
ifcopenshell.api.run(
"project.unassign_declaration",
self.file,
definitions=[self.file.by_id(self.definition)],
relating_context=self.file.by_type("IfcProjectLibrary")[0],
props = tool.Project.get_project_props()
library_file = IfcStore.library_file
assert library_file
element = library_file.by_id(self.definition)
ifcopenshell.api.project.unassign_declaration(
library_file,
definitions=[library_file.by_id(self.definition)],
relating_context=element.HasContext[0].RelatingContext,
)
element_name = self.props.active_library_element
bpy.ops.bim.rewind_library()
bpy.ops.bim.change_library_element(element_name=element_name)
tool.Project.update_current_library_page()
return {"FINISHED"}
def rollback(self, data):
@@ -585,6 +608,90 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
ifc_importer.create_style(element)
class EditProjectLibrary(bpy.types.Operator):
bl_idname = "bim.edit_project_library"
bl_label = "Edit Project Library"
bl_description = "Apply changes for currently edited library."
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
IfcStore.begin_transaction(self)
library_file = IfcStore.library_file
assert library_file
library_file.begin_transaction()
result = self._execute(context)
library_file.end_transaction()
IfcStore.add_transaction_operation(self)
IfcStore.end_transaction(self)
return result
def _execute(self, context):
props = tool.Project.get_project_props()
library_file = IfcStore.library_file
assert library_file
project_library = library_file.by_id(props.editing_project_library_id)
attributes = bonsai.bim.helper.export_attributes(props.project_library_attributes)
ifcopenshell.api.attribute.edit_attributes(library_file, project_library, attributes)
# Update parent library.
previous_parent_library = tool.Project.get_parent_library(project_library)
new_parent_library = library_file.by_id(int(props.parent_library))
if previous_parent_library != new_parent_library:
if previous_parent_library.is_a("IfcProject"):
# Then new one is IfcProjectLibrary.
ifcopenshell.api.nest.assign_object(library_file, [project_library], new_parent_library)
else: # Previous is IfcProjectLibrary.
ifcopenshell.api.nest.unassign_object(library_file, [project_library])
# If new one is IfcProject, then it's already assigned by default.
if new_parent_library.is_a("IfcProjectLibrary"):
ifcopenshell.api.nest.assign_object(library_file, [project_library], new_parent_library)
props.is_editing_project_library = False
return {"FINISHED"}
def rollback(self, data):
IfcStore.library_file.undo()
def commit(self, data):
IfcStore.library_file.redo()
class AddProjectLibrary(bpy.types.Operator):
bl_idname = "bim.add_project_library"
bl_label = "Add Project Library"
bl_description = "Add new IfcProjectLibrary to the currently selected library."
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
IfcStore.begin_transaction(self)
library_file = IfcStore.library_file
assert library_file
library_file.begin_transaction()
result = self._execute(context)
library_file.end_transaction()
IfcStore.add_transaction_operation(self)
IfcStore.end_transaction(self)
return result
def _execute(self, context):
props = tool.Project.get_project_props()
library_file = IfcStore.library_file
assert library_file
project = library_file.by_type("IfcProject")[0]
project_library = ifcopenshell.api.root.create_entity(library_file, "IfcProjectLibrary")
ifcopenshell.api.project.assign_declaration(library_file, [project_library], project)
ProjectLibraryData.load() # Update enum.
props.selected_project_library = str(project_library.id())
props.is_editing_project_library = True
return {"FINISHED"}
def rollback(self, data):
IfcStore.library_file.undo()
def commit(self, data):
IfcStore.library_file.redo()
class EnableEditingHeader(bpy.types.Operator):
bl_idname = "bim.enable_editing_header"
bl_label = "Enable Editing Header"
@@ -1028,17 +1135,12 @@ class LinkIfc(bpy.types.Operator):
self.report({"INFO"}, "Can't link the current .blend file")
continue
new = context.scene.BIMProjectProperties.links.add()
if self.use_relative_path and (ifc_filepath := tool.Ifc.get_path()):
try:
filepath = filepath.relative_to(Path(ifc_filepath).parent)
except:
pass # Perhaps on another drive or something
# Store link paths as posix for cross-platform.
new.name = filepath.as_posix()
status = bpy.ops.bim.load_link(filepath=filepath.as_posix(), use_cache=self.use_cache)
filepath = tool.Ifc.get_uri(filepath, use_relative_path=self.use_relative_path)
new.name = filepath
status = bpy.ops.bim.load_link(filepath=filepath, use_cache=self.use_cache)
if status == {"CANCELLED"}:
error_msg = (
f'Error processing IFC file "{filepath.as_posix()}" '
f'Error processing IFC file "{filepath}" '
"was critical and blend file either wasn't saved or wasn't updated. "
"See logs above in system console for details."
)
@@ -1152,7 +1254,7 @@ class LoadLink(bpy.types.Operator):
os.remove(blend_filepath)
if not blend_filepath.exists():
pprops = bpy.context.scene.BIMProjectProperties
pprops = tool.Project.get_project_props()
gprops = bpy.context.scene.BIMGeoreferenceProperties
code = f"""
@@ -1191,7 +1293,20 @@ except Exception as e:
t = time.time()
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
temp_file.write(code)
run = subprocess.run([bpy.app.binary_path, "-b", "--python", temp_file.name, "--python-exit-code", "1"])
run = subprocess.run(
[
bpy.app.binary_path,
"-b",
# Explicitly ask to enable Bonsai for this Blender instance
# as Bonsai may be just enabled and user preferences are not saved.
"--addons",
tool.Blender.get_blender_addon_package_name(),
"--python",
temp_file.name,
"--python-exit-code",
"1",
]
)
if run.returncode == 1:
print("An error occurred while processing your IFC.")
if not blend_filepath.exists() or blend_filepath.stat().st_mtime < t:
+70 -2
View File
@@ -21,9 +21,10 @@ import bpy
import ifcopenshell.util.element
import ifcopenshell.util.placement
import bonsai.tool as tool
import bonsai.bim.helper
from bonsai.bim.module.project.data import ProjectData, ProjectLibraryData
from bonsai.bim.ifc import IfcStore
from bonsai.bim.prop import StrProperty, ObjProperty
from bonsai.bim.prop import StrProperty, ObjProperty, Attribute
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
@@ -64,10 +65,17 @@ def update_library_file(self: "BIMProjectProperties", context: bpy.types.Context
if self.library_file != "0":
filepath = next(p for p in tool.Blender.get_data_dir_paths("libraries", "*.ifc") if p.name == self.library_file)
bpy.ops.bim.select_library_file(filepath=filepath.__str__())
ProjectLibraryData.load()
props = tool.Project.get_project_props()
props.selected_project_library = "*"
def update_selected_project_library(self: "BIMProjectProperties", context: bpy.types.Context) -> None:
bpy.ops.bim.refresh_library()
if self.filter_by_library:
bpy.ops.bim.refresh_library()
else:
# Ensure `.is_declared` up to date.
tool.Project.update_current_library_page()
def get_project_libaries(self: "BIMProjectProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
@@ -76,6 +84,41 @@ def get_project_libaries(self: "BIMProjectProperties", context: bpy.types.Contex
return ProjectLibraryData.data["project_libraries_enum"]
def filter_by_library_update(self: "BIMProjectProperties", context: bpy.types.Context) -> None:
if self.filter_by_library and self.selected_project_library == "*":
# Filter is toggled from OFF to ON, so it was showing all elements previously either way.
return
bpy.ops.bim.refresh_library()
def is_editing_project_library_update(self: "BIMProjectProperties", context: bpy.types.Context) -> None:
if self.is_editing_project_library:
library_file = IfcStore.library_file
assert library_file
self.editing_project_library_id = int(self.selected_project_library)
project_library = library_file.by_id(int(self.selected_project_library))
self.project_library_attributes.clear()
bonsai.bim.helper.import_attributes2(project_library, self.project_library_attributes)
self.parent_library = str(tool.Project.get_parent_library(project_library).id())
ProjectLibraryData.load() # Show edit icon in enum.
return
del self["is_editing_project_library"]
del self["editing_project_library_id"]
self.project_library_attributes.clear()
del self["parent_library"]
ProjectLibraryData.load() # Hide edit icon in enum.
def get_parent_libaries(self: "BIMProjectProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
if not ProjectLibraryData.is_loaded:
ProjectLibraryData.load()
edited_library = str(self.editing_project_library_id)
# Prevent assigning library to itself.
enum_items = [i for i in ProjectLibraryData.data["parent_libraries_enum"] if i[0] != edited_library]
return enum_items
def update_filter_mode(self: "BIMProjectProperties", context: bpy.types.Context) -> None:
self.filter_categories.clear()
if self.filter_mode == "NONE":
@@ -282,6 +325,26 @@ class BIMProjectProperties(PropertyGroup):
description="Project library to display elements from",
update=update_selected_project_library,
)
filter_by_library: BoolProperty(
name="Filter by Library",
description="Filter library elements based on selected library. If unselected can be used to assign selected library to library elements.",
default=True,
update=filter_by_library_update,
)
is_editing_project_library: BoolProperty(
name="Is Editing Project Library",
description="Toggle editing for currently selected project library",
update=is_editing_project_library_update,
)
editing_project_library_id: IntProperty(
description="Needed to keep track of currently edited library when user changes currently selected library in dropdown."
)
project_library_attributes: CollectionProperty(name="Project Library Attributes", type=Attribute)
parent_library: EnumProperty(
name="Parent Library",
description="Parent library that library is assigned to (either IfcProject or IfcProjectLibrary).",
items=get_parent_libaries,
)
use_relative_project_path: BoolProperty(name="Use Relative Project Path", default=False)
queried_obj: bpy.props.PointerProperty(type=bpy.types.Object)
@@ -348,6 +411,11 @@ class BIMProjectProperties(PropertyGroup):
library_file: str
selected_project_library: Union[Literal["*", "-"], str]
filter_by_library: bool
is_editing_project_library: bool
editing_project_library_id: int
project_library_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
parent_library: str
use_relative_project_path: bool
queried_obj: Union[bpy.types.Object, None]
+40 -10
View File
@@ -21,7 +21,7 @@ import bpy
import os
import bonsai.bim
import bonsai.tool as tool
from bonsai.bim.helper import prop_with_search
from bonsai.bim.helper import prop_with_search, draw_attributes
from bpy.types import Panel, Menu, UIList
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.project.data import ProjectData, LinksData
@@ -54,16 +54,19 @@ class BIM_MT_recent_projects(Menu):
bl_label = "Open Recent IFC Project"
def draw(self, context):
layout = self.layout
paths = tool.Project.get_recent_ifc_projects()
if not paths:
self.layout.label(text="No Recent IFC Projects")
return
for path in paths:
op = self.layout.operator("bim.load_project", text=path.name, icon_value=bonsai.bim.icons["IFC"].icon_id)
row = layout.row()
op = row.operator("bim.load_project", text=path.name, icon_value=bonsai.bim.icons["IFC"].icon_id)
op.filepath = str(path)
op.should_start_fresh_session = True
op.use_detailed_tooltip = True
row.enabled = path.is_file()
self.layout.separator()
self.layout.operator("bim.clear_recent_ifc_projects", icon="TRASH")
@@ -128,7 +131,7 @@ class BIM_PT_project(Panel):
self.layout.use_property_decorate = False
self.layout.use_property_split = True
props = context.scene.BIMProperties
pprops = context.scene.BIMProjectProperties
pprops = self.props = tool.Project.get_project_props()
self.file = IfcStore.get_file()
if pprops.is_loading:
self.draw_advanced_loading_ui(context)
@@ -148,7 +151,7 @@ class BIM_PT_project(Panel):
self.draw_unsaved_project_ui(context)
def draw_advanced_loading_ui(self, context):
pprops = context.scene.BIMProjectProperties
pprops = self.props
prop_with_search(self.layout, pprops, "filter_mode")
if pprops.filter_mode in ["DECOMPOSITION", "IFC_CLASS", "IFC_TYPE"]:
row = self.layout.row(align=True)
@@ -222,7 +225,7 @@ class BIM_PT_project(Panel):
row.operator("bim.load_project_elements")
def draw_editing_buttons(self, context, row):
pprops = context.scene.BIMProjectProperties
pprops = self.props
if IfcStore.get_file():
if pprops.is_editing:
row.operator("bim.edit_header", icon="CHECKMARK", text="")
@@ -231,7 +234,7 @@ class BIM_PT_project(Panel):
row.operator("bim.enable_editing_header", icon="GREASEPENCIL", text="")
def draw_editable_file_info(self, context):
pprops = context.scene.BIMProjectProperties
pprops = self.props
if IfcStore.get_file():
row = self.layout.row(align=True)
@@ -316,7 +319,7 @@ class BIM_PT_new_project_wizard(Panel):
self.layout.use_property_split = True
props = context.scene.BIMProperties
pprops = context.scene.BIMProjectProperties
pprops = tool.Project.get_project_props()
prop_with_search(self.layout, pprops, "export_schema")
row = self.layout.row()
row.prop(context.scene.unit_settings, "system")
@@ -344,7 +347,7 @@ class BIM_PT_project_library(Panel):
def draw(self, context):
self.layout.use_property_decorate = False
self.layout.use_property_split = True
self.props = context.scene.BIMProjectProperties
self.props = tool.Project.get_project_props()
row = self.layout.row(align=True)
row.prop(self.props, "library_file", text="")
if self.props.library_file == "0":
@@ -363,11 +366,38 @@ class BIM_PT_project_library(Panel):
row.label(text="No Library Loaded", icon="ASSET_MANAGER")
def draw_library_ul(self):
layout = self.layout
props = self.props
library_file = IfcStore.library_file
assert library_file
library_is_selected = props.selected_project_library not in ("*", "-")
row = layout.row(align=True)
row.prop(self.props, "selected_project_library", text="")
if library_file.schema != "IFC2X3":
row.operator("bim.add_project_library", text="", icon="ADD")
if library_is_selected and not props.is_editing_project_library:
row.prop(props, "is_editing_project_library", text="", icon="GREASEPENCIL")
row.prop(self.props, "filter_by_library", text="", icon="FILTER")
if props.is_editing_project_library:
row = layout.row(align=True)
row.operator("bim.edit_project_library", text="Save Attributes", icon="GREASEPENCIL")
row.prop(props, "is_editing_project_library", text="", icon="CANCEL")
draw_attributes(props.project_library_attributes, layout)
row = layout.row()
row.prop(props, "parent_library", text="Parent Library")
layout.separator()
if not self.props.library_elements:
row = self.layout.row()
row.label(text="No Assets Found", icon="ERROR")
return
self.layout.prop(self.props, "selected_project_library", text="")
row = self.layout.row(align=True)
row.label(text=self.props.active_library_element or "Top Level Assets")
if self.props.active_library_element:
@@ -393,7 +423,7 @@ class BIM_PT_links(Panel):
bl_parent_id = "BIM_PT_tab_project_setup"
def draw(self, context):
self.props = context.scene.BIMProjectProperties
self.props = tool.Project.get_project_props()
row = self.layout.row(align=True)
row.operator("bim.link_ifc")
if self.props.links:
+17 -5
View File
@@ -55,7 +55,11 @@ class Data:
for name, data in sorted(psetqtos.items()):
pset = ifc_file.by_id(data["id"])
pset_uses = ifcopenshell.util.element.get_elements_by_pset(pset)
has_template = bool(tool.Pset.get_pset_template(name))
pset_template = tool.Pset.get_pset_template(name)
if has_template := bool(pset_template):
template_available_in_ui = pset_template
else:
template_available_in_ui = False
results.append(
{
"id": data["id"],
@@ -113,7 +117,10 @@ class ObjectPsetsData(Data):
if not element:
return []
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(
element.is_a(), ifcopenshell.util.element.get_predefined_type(element), pset_only=True
element.is_a(),
ifcopenshell.util.element.get_predefined_type(element),
pset_only=True,
schema=tool.Ifc.get_schema(),
)
psetnames = cls.format_pset_enum(psets)
assigned_names = ifcopenshell.util.element.get_psets(element, psets_only=True, should_inherit=False).keys()
@@ -126,7 +133,10 @@ class ObjectPsetsData(Data):
if not element:
return []
qtos = bonsai.bim.schema.ifc.psetqto.get_applicable(
element.is_a(), ifcopenshell.util.element.get_predefined_type(element), qto_only=True
element.is_a(),
ifcopenshell.util.element.get_predefined_type(element),
qto_only=True,
schema=tool.Ifc.get_schema(),
)
return cls.format_pset_enum(qtos)
@@ -184,7 +194,9 @@ class MaterialPsetsData(Data):
if material.ifc_definition_id:
material = tool.Ifc.get().by_id(material.ifc_definition_id)
category = getattr(material, "Category", None) or None
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(props.material_type, category, pset_only=True)
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(
props.material_type, category, pset_only=True, schema=tool.Ifc.get_schema()
)
psetnames = cls.format_pset_enum(psets)
assigned_names = ifcopenshell.util.element.get_psets(
material, psets_only=True, should_inherit=False
@@ -214,7 +226,7 @@ class TaskQtosData(Data):
@classmethod
def load(cls):
wprops = bpy.context.scene.BIMWorkScheduleProperties
tprops = bpy.context.scene.BIMTaskTreeProperties
tprops = tool.Sequence.get_task_tree_props()
ifc_definition_id = tprops.tasks[wprops.active_task_index].ifc_definition_id
cls.data = {"qtos": cls.psetqtos(tool.Ifc.get().by_id(ifc_definition_id), qtos_only=True)}
cls.is_loaded = True
@@ -180,15 +180,16 @@ class RemovePset(bpy.types.Operator, tool.Ifc.Operator):
)
class AddPset(bpy.types.Operator, tool.Ifc.Operator):
class AddPset(bpy.types.Operator):
bl_idname = "bim.add_pset"
bl_label = "Add Pset"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def _execute(self, context):
def execute(self, context):
core.add_pset(tool.Ifc, tool.Pset, tool.Blender, obj_name=self.obj, obj_type=self.obj_type)
return {"FINISHED"}
class UnsharePset(bpy.types.Operator, tool.Ifc.Operator):
@@ -490,14 +491,14 @@ class SavePsetAsTemplate(bpy.types.Operator, tool.PsetTemplate.PsetTemplateOpera
pset_id: bpy.props.IntProperty()
def invoke(self, context, event):
props = context.scene.BIMPsetTemplateProperties
props = tool.PsetTemplate.get_pset_template_props()
if tool.Blender.get_enum_safe(props, "pset_template_files") is None:
self.report({"ERROR"}, "No template files found. You can create one in Property Set Templates UI.")
return {"CANCELLED"}
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
props = context.scene.BIMPsetTemplateProperties
props = tool.PsetTemplate.get_pset_template_props()
self.layout.prop(props, "pset_template_files", text="Template File")
def _execute(self, context):
+22 -10
View File
@@ -38,7 +38,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import Union, Literal, TYPE_CHECKING, get_args
from typing import Union, Literal, TYPE_CHECKING, get_args, Literal
psetnames = {}
qtonames = {}
@@ -105,7 +105,7 @@ def get_material_set_pset_names(self, context):
if not ifc_class or "Set" not in ifc_class:
return []
if ifc_class not in psetnames:
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True)
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True, schema=tool.Ifc.get_schema())
psetnames[ifc_class] = blender_formatted_enum_from_psets(psets)
return psetnames[ifc_class]
@@ -117,7 +117,7 @@ def get_material_set_item_pset_names(self, context):
return []
ifc_class = tool.Ifc.get().by_id(ifc_definition_id).is_a()
if ifc_class not in psetnames:
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True)
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True, schema=tool.Ifc.get_schema())
psetnames[ifc_class] = blender_formatted_enum_from_psets(psets)
return psetnames[ifc_class]
@@ -126,7 +126,7 @@ def get_task_qto_names(self, context):
global qtonames
ifc_class = "IfcTask"
if ifc_class not in qtonames:
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, qto_only=True)
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, qto_only=True, schema=tool.Ifc.get_schema())
qtonames[ifc_class] = blender_formatted_enum_from_psets(psets)
return qtonames[ifc_class]
@@ -137,7 +137,7 @@ def get_resource_pset_names(self, context):
rtprops = context.scene.BIMResourceTreeProperties
ifc_class = IfcStore.get_file().by_id(rtprops.resources[rprops.active_resource_index].ifc_definition_id).is_a()
if ifc_class not in psetnames:
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True)
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True, schema=tool.Ifc.get_schema())
psetnames[ifc_class] = blender_formatted_enum_from_psets(psets)
return psetnames[ifc_class]
@@ -148,7 +148,7 @@ def get_resource_qto_names(self, context):
rtprops = context.scene.BIMResourceTreeProperties
ifc_class = IfcStore.get_file().by_id(rtprops.resources[rprops.active_resource_index].ifc_definition_id).is_a()
if ifc_class not in qtonames:
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, qto_only=True)
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, qto_only=True, schema=tool.Ifc.get_schema())
qtonames[ifc_class] = blender_formatted_enum_from_psets(psets)
return qtonames[ifc_class]
@@ -157,7 +157,7 @@ def get_group_pset_names(self, context):
global psetnames
ifc_class = "IfcGroup"
if ifc_class not in psetnames:
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True)
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True, schema=tool.Ifc.get_schema())
psetnames[ifc_class] = blender_formatted_enum_from_psets(psets)
return psetnames[ifc_class]
@@ -166,7 +166,7 @@ def get_group_qto_names(self, context):
global qtonames
ifc_class = "IfcGroup"
if ifc_class not in qtonames:
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, qto_only=True)
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, qto_only=True, schema=tool.Ifc.get_schema())
qtonames[ifc_class] = blender_formatted_enum_from_psets(psets)
return qtonames[ifc_class]
@@ -176,7 +176,7 @@ def get_profile_pset_names(self, context):
pprops = context.scene.BIMProfileProperties
ifc_class = IfcStore.get_file().by_id(pprops.profiles[pprops.active_profile_index].ifc_definition_id).is_a()
if ifc_class not in psetnames:
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True)
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True, schema=tool.Ifc.get_schema())
psetnames[ifc_class] = blender_formatted_enum_from_psets(psets)
return psetnames[ifc_class]
@@ -185,7 +185,7 @@ def get_work_schedule_pset_names(self, context):
global psetnames
ifc_class = "IfcWorkSchedule"
if ifc_class not in psetnames:
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True)
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True, schema=tool.Ifc.get_schema())
psetnames[ifc_class] = blender_formatted_enum_from_psets(psets)
return psetnames[ifc_class]
@@ -241,6 +241,7 @@ class IfcProperty(PropertyGroup):
if TYPE_CHECKING:
metadata: Attribute
value_type: IfcPropertyValueType
enumerated_value: IfcPropertyEnumeratedValue
class PsetProperties(PropertyGroup):
@@ -259,6 +260,17 @@ class PsetProperties(PropertyGroup):
prop_name: StringProperty(name="Property Name", default="MyProperty")
prop_value: StringProperty(name="Property Value", default="Some Value")
if TYPE_CHECKING:
active_pset_id: int
active_pset_has_template: bool
active_pset_name: str
active_pset_type: Literal["-", "PSET", "QTO"]
properties: bpy.types.bpy_prop_collection_idprop[IfcProperty]
pset_name: str
qto_name: str
prop_name: str
prop_value: str
class RenameProperties(PropertyGroup):
pset_name: StringProperty(name="Pset")
+8 -7
View File
@@ -40,7 +40,7 @@ from typing import Any, Optional, TYPE_CHECKING
from typing_extensions import assert_never
if TYPE_CHECKING:
from bonsai.bim.module.pset.prop import IfcProperty
from bonsai.bim.module.pset.prop import IfcProperty, PsetProperties
def draw_property(prop: IfcProperty, layout: bpy.types.UILayout, copy_operator: Optional[str] = None) -> None:
@@ -73,7 +73,7 @@ def draw_single_property(prop: IfcProperty, layout: bpy.types.UILayout, copy_ope
def draw_enumerated_property(
prop: bpy.types.PropertyGroup, layout: bpy.types.UILayout, copy_operator: Optional[str] = None
prop: IfcProperty, layout: bpy.types.UILayout, copy_operator: Optional[str] = None
) -> None:
value_name = prop.metadata.get_value_name()
if not value_name:
@@ -99,7 +99,7 @@ def draw_psetqto_ui(
context: bpy.types.Context,
pset_id: int,
pset: dict[str, Any],
props: bpy.types.PropertyGroup,
props: PsetProperties,
layout: bpy.types.UILayout,
obj_type: tool.Ifc.OBJECT_TYPE,
allow_removing: bool = True,
@@ -143,6 +143,8 @@ def draw_psetqto_ui(
op.obj_type = obj_type
if pset["has_template"]:
row.label(text="", icon="ASSET_MANAGER")
op = row.operator("bim.pset_templates_ui_select", text="", icon="ZOOM_SELECTED")
op.pset_id = pset_id
else:
op = row.operator("bim.save_pset_as_template", icon="ASSET_MANAGER", text="")
op.pset_id = pset_id
@@ -219,9 +221,7 @@ def draw_psetqto_ui(
row.label(text="No Properties")
def draw_psetqto_editable_ui(
box: bpy.types.UILayout, props: bpy.types.PropertyGroup, prop: bpy.types.PropertyGroup
) -> None:
def draw_psetqto_editable_ui(box: bpy.types.UILayout, props: PsetProperties, prop: IfcProperty) -> None:
row = box.row(align=True)
draw_property(prop, row, copy_operator="bim.copy_property_to_selection")
@@ -493,7 +493,8 @@ class BIM_PT_task_qtos(Panel):
props = context.scene.BIMWorkScheduleProperties
if not props.active_work_schedule_id:
return False
total_tasks = len(context.scene.BIMTaskTreeProperties.tasks)
tprops = tool.Sequence.get_task_tree_props()
total_tasks = len(tprops.tasks)
if total_tasks > 0 and props.active_task_index < total_tasks:
return True
return False
@@ -35,6 +35,7 @@ classes = (
operator.RemovePsetTemplate,
operator.RemovePsetTemplateFile,
operator.SavePsetTemplateFile,
operator.SelectPsetTemplatesInUI,
prop.PsetTemplate,
prop.EnumerationValues,
prop.PropTemplate,
@@ -111,14 +111,8 @@ class PsetTemplatesData:
@classmethod
def pset_template_files(cls) -> list[tuple[str, str, str]]:
results: list[tuple[str, str, str]] = []
for f in tool.Blender.get_data_dir_paths("pset", "*.ifc"):
results.append((f.__str__(), f.stem, "Global Pset Template"))
pset_dir = tool.Ifc.resolve_uri(bpy.context.scene.BIMProperties.pset_dir)
if os.path.isdir(pset_dir):
for path in pathlib.Path(pset_dir).glob("*.ifc"):
results.append((str(path), path.stem, "Project Pset Template"))
for path, pset_location in tool.PsetTemplate.get_pset_template_files():
results.append((str(path), path.stem, pset_location))
return sorted(results, key=lambda x: x[1])
@classmethod
@@ -126,13 +120,14 @@ class PsetTemplatesData:
if not cls.data["pset_template_files"]:
return []
if not IfcStore.pset_template_file:
IfcStore.pset_template_path = bpy.context.scene.BIMPsetTemplateProperties.pset_template_files
props = tool.PsetTemplate.get_pset_template_props()
IfcStore.pset_template_path = props.pset_template_files
IfcStore.pset_template_file = ifcopenshell.open(IfcStore.pset_template_path)
return [(str(t.id()), t.Name, "") for t in IfcStore.pset_template_file.by_type("IfcPropertySetTemplate")]
@classmethod
def pset_template(cls) -> dict[str, Any]:
props = bpy.context.scene.BIMPsetTemplateProperties
props = tool.PsetTemplate.get_pset_template_props()
template_id = props.pset_templates
if not template_id:
return {}
@@ -145,7 +140,7 @@ class PsetTemplatesData:
@classmethod
def prop_templates(cls) -> list[dict[str, Any]]:
props = bpy.context.scene.BIMPsetTemplateProperties
props = tool.PsetTemplate.get_pset_template_props()
template_id = props.pset_templates
if not template_id:
return []
@@ -49,7 +49,8 @@ class AddPsetTemplateFile(bpy.types.Operator):
template.write(filepath)
bonsai.bim.handler.refresh_ui_data()
bonsai.bim.schema.reload(tool.Ifc.get().schema)
context.scene.BIMPsetTemplateProperties.pset_template_files = filepath
props = tool.PsetTemplate.get_pset_template_props()
props.pset_template_files = filepath
tool.PsetTemplate.enable_editing_pset_template()
return {"FINISHED"}
@@ -67,7 +68,8 @@ class AddPsetTemplate(bpy.types.Operator, tool.PsetTemplate.PsetTemplateOperator
self.template_file.write(IfcStore.pset_template_path)
bonsai.bim.handler.refresh_ui_data()
bonsai.bim.schema.reload(tool.Ifc.get().schema)
context.scene.BIMPsetTemplateProperties.pset_templates = str(template.id())
props = tool.PsetTemplate.get_pset_template_props()
props.pset_templates = str(template.id())
class RemovePsetTemplate(bpy.types.Operator, tool.PsetTemplate.PsetTemplateOperator):
@@ -76,7 +78,7 @@ class RemovePsetTemplate(bpy.types.Operator, tool.PsetTemplate.PsetTemplateOpera
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.scene.BIMPsetTemplateProperties
props = tool.PsetTemplate.get_pset_template_props()
current_pset_template_id = int(props.pset_templates)
if props.active_pset_template_id == current_pset_template_id:
bpy.ops.bim.disable_editing_pset_template()
@@ -107,7 +109,7 @@ class DisableEditingPsetTemplate(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BIMPsetTemplateProperties
props = tool.PsetTemplate.get_pset_template_props()
props.active_pset_template_id = 0
return {"FINISHED"}
@@ -119,7 +121,7 @@ class EnableEditingPropTemplate(bpy.types.Operator):
prop_template: bpy.props.IntProperty()
def execute(self, context):
tool.PsetTemplate.enable_editing_prop_template(tool.Ifc.get().by_id(self.prop_template))
tool.PsetTemplate.enable_editing_prop_template(IfcStore.pset_template_file.by_id(self.prop_template))
return {"FINISHED"}
@@ -130,7 +132,8 @@ class DeletePropEnum(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
active_prop = context.scene.BIMPsetTemplateProperties.active_prop_template
props = tool.PsetTemplate.get_pset_template_props()
active_prop = props.active_prop_template
active_prop.enum_values.remove(self.index)
return {"FINISHED"}
@@ -142,7 +145,8 @@ class AddPropEnum(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
active_prop = context.scene.BIMPsetTemplateProperties.active_prop_template
props = tool.PsetTemplate.get_pset_template_props()
active_prop = props.active_prop_template
active_prop.enum_values.add()
return {"FINISHED"}
@@ -153,7 +157,7 @@ class DisableEditingPropTemplate(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BIMPsetTemplateProperties
props = tool.PsetTemplate.get_pset_template_props()
props.active_prop_template_id = 0
return {"FINISHED"}
@@ -164,7 +168,7 @@ class EditPsetTemplate(bpy.types.Operator, tool.PsetTemplate.PsetTemplateOperato
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.scene.BIMPsetTemplateProperties
props = tool.PsetTemplate.get_pset_template_props()
ifcopenshell.api.run(
"pset_template.edit_pset_template",
IfcStore.pset_template_file,
@@ -209,7 +213,7 @@ class RemovePsetTemplateFile(bpy.types.Operator):
bonsai.bim.schema.reload(tool.Ifc.get().schema)
# Ensure enum is valid after deletion.
self.props = context.scene.BIMPsetTemplateProperties
self.props = tool.PsetTemplate.get_pset_template_props()
if not tool.Blender.ensure_enum_is_valid(self.props, "pset_template_files"):
self.update_template_files_prop(context)
return {"FINISHED"}
@@ -226,7 +230,7 @@ class AddPropTemplate(bpy.types.Operator, tool.PsetTemplate.PsetTemplateOperator
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.scene.BIMPsetTemplateProperties
props = tool.PsetTemplate.get_pset_template_props()
pset_template_id = props.active_pset_template_id or int(props.pset_templates)
prop_template = ifcopenshell.api.run(
"pset_template.add_prop_template",
@@ -264,7 +268,7 @@ class EditPropTemplate(bpy.types.Operator, tool.PsetTemplate.PsetTemplateOperato
def _execute(self, context):
assert IfcStore.pset_template_file
props = context.scene.BIMPsetTemplateProperties
props = tool.PsetTemplate.get_pset_template_props()
active_prop_template = props.active_prop_template
if props.active_prop_template.template_type == "P_ENUMERATEDVALUE":
data_type = props.active_prop_template.get_value_name()
@@ -296,3 +300,38 @@ class EditPropTemplate(bpy.types.Operator, tool.PsetTemplate.PsetTemplateOperato
bonsai.bim.handler.refresh_ui_data()
if tool.Ifc.get():
bonsai.bim.schema.reload(tool.Ifc.get().schema)
class SelectPsetTemplatesInUI(bpy.types.Operator):
bl_idname = "bim.pset_templates_ui_select"
bl_label = "Select Pset Template in UI"
bl_description = "Select pset template for the provided pset in Pset Template UI."
bl_options = {"REGISTER", "UNDO"}
pset_id: bpy.props.IntProperty()
def execute(self, context):
props = tool.PsetTemplate.get_pset_template_props()
ifc_file = tool.Ifc.get()
pset = ifc_file.by_id(self.pset_id)
pset_template_file: ifcopenshell.file
pset_template = None
path = None
for path, _ in tool.PsetTemplate.get_pset_template_files():
pset_template_file = ifcopenshell.open(path)
for pset_template in pset_template_file.by_type("IfcPropertySetTemplate"):
if pset_template.Name == pset.Name:
break
if pset_template is None or path is None:
self.report({"INFO"}, f"Couldn't find pset template for the provided pset: '{pset.Name}'.")
return {"CANCELLED"}
props.pset_template_files = str(path)
props.pset_templates = str(pset_template.id())
self.report(
{"INFO"},
f"Pset Template '{pset.Name}' is selected in Pset Templates UI. See Property Set Templates in Project tab.",
)
return {"FINISHED"}
@@ -36,7 +36,7 @@ class BIM_PT_pset_template(Panel):
if not PsetTemplatesData.is_loaded:
PsetTemplatesData.load()
self.props = context.scene.BIMPsetTemplateProperties
self.props = tool.PsetTemplate.get_pset_template_props()
row = self.layout.row(align=True)
if PsetTemplatesData.data["pset_template_files"]:
+33 -23
View File
@@ -43,7 +43,7 @@ class EnableReassignClass(bpy.types.Operator):
def execute(self, context):
obj = context.active_object
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifc_class = obj.name.split("/")[0]
context.active_object.BIMObjectProperties.is_reassigning_class = True
ifc_products = [
@@ -93,11 +93,11 @@ class ReassignClass(bpy.types.Operator, tool.Ifc.Operator):
objects = [bpy.data.objects.get(self.obj)]
else:
objects = set(context.selected_objects + [context.active_object])
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
root_props = context.scene.BIMRootProperties
ifc_product: str = root_props.ifc_product
ifc_class: str = root_props.ifc_class
type_ifc_class = next(iter(ifcopenshell.util.type.get_applicable_types(ifc_class)), None)
type_ifc_class = next(iter(ifcopenshell.util.type.get_applicable_types(ifc_class, self.file.schema)), None)
predefined_type = root_props.ifc_predefined_type
if predefined_type == "USERDEFINED":
@@ -157,7 +157,7 @@ class ReassignClass(bpy.types.Operator, tool.Ifc.Operator):
reassigned_elements.add(element)
for obj in objects_to_update:
obj.name = tool.Loader.get_name(tool.Ifc.get_entity(obj))
tool.Root.set_object_name(obj, tool.Ifc.get_entity(obj))
tool.Collector.assign(obj)
return {"FINISHED"}
@@ -211,31 +211,28 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
)
continue
should_add_representation = self.should_add_representation
export_mesh_to_tesselation = False
if self.should_add_representation and isinstance(obj.data, bpy.types.Mesh) and obj.data.polygons:
should_add_representation = False
export_mesh_to_tesselation = True
if obj.scale != (1, 1, 1):
if obj.data.users > 1:
bpy.ops.object.make_single_user(
object=True, obdata=True, material=False, animation=False, obdata_animation=False
)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True, properties=False)
if tool.Geometry.mesh_has_loose_geometry(obj.data):
self.report(
{"WARNING"},
f"Mesh '{obj.data.name}' has loose geometry, loose geometry will be ignored to save mesh to IFC as a tessellation.",
)
element = core.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class=ifc_class,
predefined_type=predefined_type,
should_add_representation=should_add_representation,
context=ifc_context,
ifc_representation_class=self.ifc_representation_class,
)
if export_mesh_to_tesselation:
element = core.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class=ifc_class,
predefined_type=predefined_type,
should_add_representation=False,
)
representation = tool.Geometry.export_mesh_to_tessellation(obj, ifc_context)
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
bonsai.core.geometry.switch_representation(
@@ -247,6 +244,18 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
is_global=True,
should_sync_changes_first=False,
)
else:
element = core.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class=ifc_class,
predefined_type=predefined_type,
should_add_representation=self.should_add_representation,
context=ifc_context,
ifc_representation_class=self.ifc_representation_class,
)
context.view_layer.objects.active = active_object
@@ -412,7 +421,8 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
if representation_template == "EMTPY" or not ifc_context:
pass
elif representation_template == "OBJ" and props.representation_obj:
obj.matrix_world = props.representation_obj.matrix_world
obj.matrix_world = props.representation_obj.matrix_world.copy()
obj.scale = (1, 1, 1)
representation = tool.Geometry.export_mesh_to_tessellation(props.representation_obj, ifc_context)
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
bonsai.core.geometry.switch_representation(
+4 -2
View File
@@ -20,6 +20,7 @@ import bpy
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.schema
import ifcopenshell.util.type
import bonsai.tool as tool
from bonsai.bim.module.root.data import IfcClassData
from bpy.types import PropertyGroup
@@ -66,11 +67,12 @@ def refresh_classes(self, context):
self.representation_template = "OBJ"
self.representation_obj = obj
ifc_file = tool.Ifc.get()
# When switching between ElementType and Element, keep the same class and predefined type if possible
if self.ifc_product == "IfcElement":
ifc_class = next(iter(ifcopenshell.util.type.get_applicable_entities(old_class)), None)
ifc_class = next(iter(ifcopenshell.util.type.get_applicable_entities(old_class, ifc_file.schema)), None)
elif self.ifc_product == "IfcElementType":
ifc_class = next(iter(ifcopenshell.util.type.get_applicable_types(old_class)), None)
ifc_class = next(iter(ifcopenshell.util.type.get_applicable_types(old_class, ifc_file.schema)), None)
else:
return
if ifc_class:
@@ -40,6 +40,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING
def getTaskColumns(self, context):
@@ -343,6 +344,26 @@ class Task(PropertyGroup):
is_predecessor: BoolProperty(name="Is Predecessor")
is_successor: BoolProperty(name="Is Successor")
if TYPE_CHECKING:
name: str
identification: str
ifc_definition_id: int
has_children: bool
is_selected: bool
is_expanded: bool
has_bar_visual: bool
level_index: int
duration: str
start: str
finish: str
calendar: str
derived_start: str
derived_finish: str
derived_duration: str
derived_calendar: str
is_predecessor: bool
is_successor: bool
class WorkPlan(PropertyGroup):
name: StringProperty(name="Name")
@@ -479,6 +500,9 @@ class BIMTaskTreeProperties(PropertyGroup):
# In Blender if you add many collection items it makes other property access in the same group really slow.
tasks: CollectionProperty(name="Tasks", type=Task)
if TYPE_CHECKING:
tasks: bpy.types.bpy_prop_collection_idprop[Task]
class WorkCalendar(PropertyGroup):
name: StringProperty(name="Name")
+4 -3
View File
@@ -156,7 +156,7 @@ class BIM_PT_work_schedules(Panel):
if not WorkScheduleData.is_loaded:
WorkScheduleData.load()
self.props = context.scene.BIMWorkScheduleProperties
self.tprops = context.scene.BIMTaskTreeProperties
self.tprops = tool.Sequence.get_task_tree_props()
if not self.props.active_work_schedule_id:
row = self.layout.row(align=True)
@@ -649,7 +649,8 @@ class BIM_PT_task_icom(Panel):
props = context.scene.BIMWorkScheduleProperties
if not props.active_work_schedule_id:
return False
total_tasks = len(context.scene.BIMTaskTreeProperties.tasks)
tprops = tool.Sequence.get_task_tree_props()
total_tasks = len(tprops.tasks)
if total_tasks > 0 and props.active_task_index < total_tasks:
return True
return False
@@ -659,7 +660,7 @@ class BIM_PT_task_icom(Panel):
TaskICOMData.load()
self.props = context.scene.BIMWorkScheduleProperties
self.tprops = context.scene.BIMTaskTreeProperties
self.tprops = tool.Sequence.get_task_tree_props()
task = self.tprops.tasks[self.props.active_task_index]
grid = self.layout.grid_flow(columns=3, even_columns=True)
@@ -55,6 +55,8 @@ class GridDecorator:
batch.draw(shader)
def draw_text(self, context):
if not tool.Blender.is_addon_enabled():
return
self.addon_prefs = tool.Blender.get_addon_preferences()
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
@@ -94,6 +96,8 @@ class GridDecorator:
blf.disable(font_id, blf.SHADOW)
def draw(self, context):
if not tool.Blender.is_addon_enabled():
return
self.addon_prefs = tool.Blender.get_addon_preferences()
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
@@ -92,12 +92,18 @@ class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
return self.execute(context)
def _execute(self, context):
props = context.active_object.BIMObjectSpatialProperties
if self.container:
container = tool.Ifc.get().by_id(self.container)
elif (container_obj := props.container_obj) and (container := tool.Ifc.get_entity(container_obj)):
elif (
(obj := tool.Blender.get_active_object())
and (props := obj.BIMObjectSpatialProperties)
and (container_obj := props.container_obj)
and (container := tool.Ifc.get_entity(container_obj))
):
pass
for element_obj in context.selected_objects:
else:
return
for element_obj in tool.Blender.get_selected_objects():
if self.remove_from_other_containers:
for col in element_obj.users_collection[:]:
col.objects.unlink(element_obj)
@@ -311,14 +317,14 @@ class SelectDecomposedElement(bpy.types.Operator):
class SelectDecomposedElements(bpy.types.Operator):
bl_idname = "bim.select_decomposed_elements"
bl_label = "Select Children"
bl_label = "Select Elements"
bl_options = {"REGISTER", "UNDO"}
should_filter: bpy.props.BoolProperty(name="Should Filter", default=True, options={"SKIP_SAVE"})
container: bpy.props.IntProperty()
@classmethod
def description(cls, context, operator):
return "Select all contained elements filtered by this type" + "\nALT+CLICK to select all contained elements"
return "Select elements in the viewport based on the active item in the list" + "\nALT+CLICK to select all listed elements"
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.alt:
@@ -326,61 +332,7 @@ class SelectDecomposedElements(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
ifc_file = tool.Ifc.get()
container = ifc_file.by_id(self.container)
props = context.scene.BIMSpatialDecompositionProperties
element_filter = props.element_filter
active_element = props.active_element
if not self.should_filter and not element_filter:
tool.Spatial.select_products(tool.Spatial.get_decomposed_elements(container))
return {"CANCELLED"}
if props.element_mode == "TYPE":
if active_element.type == "OCCURRENCE":
if obj := tool.Ifc.get_object(ifc_file.by_id(active_element.ifc_definition_id)):
tool.Blender.set_active_object(obj)
return {"CANCELLED"}
ifc_class = relating_type = None
is_untyped = False
if self.should_filter:
if active_element.type == "CLASS":
ifc_class = active_element.name
elif active_element.type == "TYPE":
ifc_class = active_element.ifc_class
if ifc_id := active_element.ifc_definition_id:
relating_type = ifc_file.by_id(ifc_id)
elements = tool.Spatial.get_decomposed_elements(container)
elements = tool.Spatial.filter_elements(elements, ifc_class, relating_type, is_untyped, element_filter)
tool.Spatial.select_products(elements)
elif props.element_mode == "DECOMPOSITION":
occurrence = ifc_file.by_id(active_element.ifc_definition_id)
elements = ifcopenshell.util.element.get_decomposition(occurrence)
elements.add(occurrence)
tool.Spatial.select_products(elements)
elif props.element_mode == "CLASSIFICATION":
if active_element.type == "OCCURRENCE":
if obj := tool.Ifc.get_object(ifc_file.by_id(active_element.ifc_definition_id)):
tool.Blender.set_active_object(obj)
return {"CANCELLED"}
if active_element.type == "CLASSIFICATION":
identification = active_element.identification
elements = tool.Spatial.get_decomposed_elements(container)
def filter_element(element: ifcopenshell.entity_instance) -> bool:
references = ifcopenshell.util.classification.get_references(element)
if identification == "Unclassified":
if not references:
return True
elif any([r for r in references if r[1].startswith(identification)]):
return True
return False
tool.Spatial.select_products(filter(filter_element, elements))
tool.Spatial.select_products(tool.Spatial.get_filtered_elements(self.should_filter))
return {"FINISHED"}
@@ -401,7 +353,7 @@ class SetContainerVisibility(bpy.types.Operator):
bl_label = "Set Container Visibility"
bl_options = {"REGISTER", "UNDO"}
container: bpy.props.IntProperty()
should_include_children: bpy.props.BoolProperty(name="Should Include Children", default=True, options={"SKIP_SAVE"})
should_filter: bpy.props.BoolProperty(name="Should Filter", default=True, options={"SKIP_SAVE"})
mode: bpy.props.StringProperty(name="Mode")
@classmethod
@@ -414,7 +366,7 @@ class SetContainerVisibility(bpy.types.Operator):
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.alt:
self.should_include_children = False
self.should_filter = False
return self.execute(context)
def execute(self, context):
@@ -427,9 +379,7 @@ class SetContainerVisibility(bpy.types.Operator):
else:
should_hide = self.mode == "HIDE"
container = tool.Ifc.get().by_id(self.container)
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=self.should_include_children)
for element in elements:
for element in tool.Spatial.get_filtered_elements(self.should_filter):
if obj := tool.Ifc.get_object(element):
obj.hide_set(should_hide)
for collection in obj.users_collection:
+16 -19
View File
@@ -143,24 +143,6 @@ class BIM_PT_spatial_decomposition(Panel):
op = col.operator("bim.set_default_container", icon="OUTLINER_COLLECTION", text="Set Default")
op.container = ifc_definition_id
col = row.column(align=True)
col.enabled = non_ifc_project_active
op = col.operator("bim.set_container_visibility", icon="FULLSCREEN_EXIT", text="Isolate")
op.mode = "ISOLATE"
op.container = ifc_definition_id
col = row.column(align=True)
col.enabled = non_ifc_project_active
op = col.operator("bim.set_container_visibility", icon="HIDE_OFF", text="")
op.mode = "SHOW"
op.container = ifc_definition_id
col = row.column(align=True)
col.enabled = non_ifc_project_active
op = col.operator("bim.set_container_visibility", icon="HIDE_ON", text="")
op.mode = "HIDE"
op.container = ifc_definition_id
# The only operator that's enabled for IfcProject.
col = row.column(align=True)
col.operator("bim.select_container", icon="OBJECT_DATA", text="").container = ifc_definition_id
@@ -193,11 +175,26 @@ class BIM_PT_spatial_decomposition(Panel):
row.prop(self.props, "should_include_children", text="", icon="OUTLINER")
return
row = self.layout.row(align=True)
row = self.layout.row()
row.label(
text=f"{self.props.active_container.ifc_class} > {self.props.total_elements} Elements",
icon="FILE_3D",
)
row = self.layout.row(align=True)
op = row.operator("bim.set_container_visibility", icon="FULLSCREEN_EXIT", text="Isolate")
op.mode = "ISOLATE"
op.container = ifc_definition_id
op = row.operator("bim.set_container_visibility", icon="HIDE_OFF", text="")
op.mode = "SHOW"
op.container = ifc_definition_id
op = row.operator("bim.set_container_visibility", icon="HIDE_ON", text="")
op.mode = "HIDE"
op.container = ifc_definition_id
row.operator("bim.assign_container", icon="FOLDER_REDIRECT", text="").container = ifc_definition_id
op = row.operator("bim.select_decomposed_element", icon="OBJECT_DATA", text="")
if active_element := self.props.active_element:
+3 -18
View File
@@ -387,12 +387,7 @@ class BrowseExternalStyle(bpy.types.Operator):
bpy.data.materials.remove(db["data_block"])
if self.use_relative_path:
filepath = os.path.relpath(self.filepath, Path(tool.Ifc.get_path()).parent)
else:
filepath = self.filepath
filepath = Path(filepath).as_posix()
filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)
attributes = context.scene.BIMStylesProperties.external_style_attributes
attributes["Location"].string_value = filepath
attributes["Identification"].string_value = f"{self.data_block_type}/{self.data_block}"
@@ -565,19 +560,9 @@ class ChooseTextureMapPath(bpy.types.Operator):
self.report({"ERROR"}, "Provide a texture map index")
return {"CANCELLED"}
abs_path = Path(self.filepath)
if self.use_relative_path:
parent = Path(tool.Ifc.get_path()).parent
if abs_path.is_relative_to(parent):
image_filepath = abs_path.relative_to(parent)
else:
self.report({"INFO"}, "Path is not relative to the .ifc file, it will be saved as absolute.")
image_filepath = abs_path
else:
image_filepath = abs_path
filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)
texture = context.scene.BIMStylesProperties.textures[self.texture_map_index]
texture.path = image_filepath.as_posix()
texture.path = filepath
return {"FINISHED"}
+30 -19
View File
@@ -26,22 +26,24 @@ from bonsai.bim.module.system.data import PortData
from mathutils import Matrix
class LoadSystems(bpy.types.Operator, tool.Ifc.Operator):
class LoadSystems(bpy.types.Operator):
bl_idname = "bim.load_systems"
bl_label = "Load Systems"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
def execute(self, context):
core.load_systems(tool.System)
return {"FINISHED"}
class DisableSystemEditingUI(bpy.types.Operator, tool.Ifc.Operator):
class DisableSystemEditingUI(bpy.types.Operator):
bl_idname = "bim.disable_system_editing_ui"
bl_label = "Disable System Editing UI"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
def execute(self, context):
core.disable_system_editing_ui(tool.System)
return {"FINISHED"}
class AddSystem(bpy.types.Operator, tool.Ifc.Operator):
@@ -74,23 +76,25 @@ class RemoveSystem(bpy.types.Operator, tool.Ifc.Operator):
core.remove_system(tool.Ifc, tool.System, system=tool.Ifc.get().by_id(self.system))
class EnableEditingSystem(bpy.types.Operator, tool.Ifc.Operator):
class EnableEditingSystem(bpy.types.Operator):
bl_idname = "bim.enable_editing_system"
bl_label = "Enable Editing System"
bl_options = {"REGISTER", "UNDO"}
system: bpy.props.IntProperty()
def _execute(self, context):
def execute(self, context):
core.enable_editing_system(tool.System, system=tool.Ifc.get().by_id(self.system))
return {"FINISHED"}
class DisableEditingSystem(bpy.types.Operator, tool.Ifc.Operator):
class DisableEditingSystem(bpy.types.Operator):
bl_idname = "bim.disable_editing_system"
bl_label = "Disable Editing System"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.disable_editing_system(tool.System)
return {"FINISHED"}
class AssignSystem(bpy.types.Operator, tool.Ifc.Operator):
@@ -119,14 +123,15 @@ class UnassignSystem(bpy.types.Operator, tool.Ifc.Operator):
core.unassign_system(tool.Ifc, system=tool.Ifc.get().by_id(self.system), product=element)
class SelectSystemProducts(bpy.types.Operator, tool.Ifc.Operator):
class SelectSystemProducts(bpy.types.Operator):
bl_idname = "bim.select_system_products"
bl_label = "Select System Products"
bl_options = {"REGISTER", "UNDO"}
system: bpy.props.IntProperty()
def _execute(self, context):
def execute(self, context):
core.select_system_products(tool.System, system=tool.Ifc.get().by_id(self.system))
return {"FINISHED"}
class ShowPorts(bpy.types.Operator, tool.Ifc.Operator):
@@ -144,6 +149,7 @@ class ShowPorts(bpy.types.Operator, tool.Ifc.Operator):
return True
def _execute(self, context):
# Ifc.Operator - as operator will sync object's position with IFC.
core.show_ports(tool.Ifc, tool.System, tool.Spatial, element=tool.Ifc.get_entity(context.active_object))
@@ -157,6 +163,7 @@ class HidePorts(bpy.types.Operator, tool.Ifc.Operator):
return ShowPorts.poll(context)
def _execute(self, context):
# Ifc.Operator - as operator will sync object and ports positions with IFC.
core.hide_ports(tool.Ifc, tool.System, element=tool.Ifc.get_entity(context.active_object))
@@ -324,13 +331,13 @@ class SetFlowDirection(bpy.types.Operator, tool.Ifc.Operator):
return {"CANCELLED"}
class LoadZones(bpy.types.Operator, tool.Ifc.Operator):
class LoadZones(bpy.types.Operator):
bl_idname = "bim.load_zones"
bl_label = "Load Zones"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMZoneProperties
props = tool.System.get_zone_props()
props.zones.clear()
for zone in tool.Ifc.get().by_type("IfcZone"):
new = props.zones.add()
@@ -338,16 +345,18 @@ class LoadZones(bpy.types.Operator, tool.Ifc.Operator):
new.name = zone.Name or "Unnamed"
props.is_loaded = True
props.is_editing = 0
return {"FINISHED"}
class UnloadZones(bpy.types.Operator, tool.Ifc.Operator):
class UnloadZones(bpy.types.Operator):
bl_idname = "bim.unload_zones"
bl_label = "Unload Zones"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMZoneProperties
props = tool.System.get_zone_props()
props.is_loaded = False
return {"FINISHED"}
class AddZone(bpy.types.Operator, tool.Ifc.Operator):
@@ -370,17 +379,18 @@ class AddZone(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.load_zones()
class EnableEditingZone(bpy.types.Operator, tool.Ifc.Operator):
class EnableEditingZone(bpy.types.Operator):
bl_idname = "bim.enable_editing_zone"
bl_label = "Enable Editing Zone"
bl_options = {"REGISTER", "UNDO"}
zone: bpy.props.IntProperty()
def _execute(self, context):
props = bpy.context.scene.BIMZoneProperties
def execute(self, context):
props = tool.System.get_zone_props()
props.attributes.clear()
bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.zone), props.attributes)
props.is_editing = self.zone
return {"FINISHED"}
class DisableEditingZone(bpy.types.Operator, tool.Ifc.Operator):
@@ -388,9 +398,10 @@ class DisableEditingZone(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Disable Editing Zone"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMZoneProperties
def execute(self, context):
props = tool.System.get_zone_props()
props.is_editing = 0
return {"FINISHED"}
class EditZone(bpy.types.Operator, tool.Ifc.Operator):
@@ -399,7 +410,7 @@ class EditZone(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMZoneProperties
props = tool.System.get_zone_props()
zone = tool.Ifc.get().by_id(props.is_editing)
attributes = bonsai.bim.helper.export_attributes(props.attributes)
ifcopenshell.api.run("system.edit_system", tool.Ifc.get(), system=zone, attributes=attributes)
@@ -31,6 +31,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING
def get_system_class(self, context):
@@ -71,6 +72,17 @@ class BIMSystemProperties(PropertyGroup):
name="Should Draw Decorations", description="Toggle system decorations", update=toggle_decorations
)
if TYPE_CHECKING:
system_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
is_editing: bool
is_adding: bool
systems: bpy.types.bpy_prop_collection_idprop[System]
active_system_index: int
active_system_id: int
edited_system_id: int
system_class: str
should_draw_decorations: bool
class BIMZoneProperties(PropertyGroup):
attributes: CollectionProperty(name="Attributes", type=Attribute)
@@ -78,3 +90,10 @@ class BIMZoneProperties(PropertyGroup):
is_editing: IntProperty(name="Is Editing", default=0)
zones: CollectionProperty(name="Zones", type=Zone)
active_zone_index: IntProperty(name="Active Zone Index")
if TYPE_CHECKING:
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
is_loaded: bool
is_editing: int
zones: bpy.types.bpy_prop_collection_idprop[Zone]
active_zone_index: int
+2 -2
View File
@@ -350,7 +350,7 @@ class BIM_PT_zones(Panel):
def draw(self, context):
if not ZonesData.is_loaded:
ZonesData.load()
self.props = context.scene.BIMZoneProperties
self.props = tool.System.get_zone_props()
row = self.layout.row(align=True)
row.label(text="{} Zones Found".format(ZonesData.data["total_zones"]), icon="SEQ_STRIP_META")
@@ -394,7 +394,7 @@ class BIM_PT_active_object_zones(Panel):
def draw(self, context):
if not ActiveObjectZonesData.is_loaded:
ActiveObjectZonesData.load()
self.props = context.scene.BIMZoneProperties
self.props = tool.System.get_zone_props()
for zone in ActiveObjectZonesData.data["zones"]:
row = self.layout.row()
+2 -7
View File
@@ -19,7 +19,6 @@
import os
import bpy
import bmesh
import json
import time
import logging
import textwrap
@@ -32,11 +31,10 @@ import ifcopenshell
import bonsai.bim
import bonsai.tool as tool
from bonsai.bim import import_ifc
from bonsai.bim.ifc import IfcStore
from bonsai.bim.prop import StrProperty
from bonsai.bim.ui import IFCFileSelector
from bonsai.bim.helper import get_enum_items
from mathutils import Vector, Matrix, Euler
from mathutils import Vector, Euler
from math import radians
from pathlib import Path
from collections import namedtuple
@@ -168,10 +166,7 @@ class SelectURIAttribute(bpy.types.Operator):
# Do you know a better way?
pass
if attribute:
filepath = self.filepath
if self.use_relative_path:
filepath = os.path.relpath(filepath, os.path.dirname(tool.Ifc.get_path()))
attribute.string_value = filepath
attribute.string_value = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)
return {"FINISHED"}
def invoke(self, context, event):
+7 -3
View File
@@ -16,11 +16,9 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from pathlib import Path
import os
import bpy
import json
import importlib
import platformdirs
import ifcopenshell
import ifcopenshell.util.pset
@@ -36,7 +34,6 @@ import bonsai.bim
import bonsai.bim.schema
import bonsai.bim.handler
import bonsai.tool as tool
from collections import defaultdict
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
@@ -172,6 +169,9 @@ class StrProperty(PropertyGroup):
class ObjProperty(PropertyGroup):
obj: bpy.props.PointerProperty(type=bpy.types.Object)
if TYPE_CHECKING:
obj: Union[bpy.types.Object, None]
def update_single_file(self: "MultipleFileSelect", context: bpy.types.Context) -> None:
self.file_list.clear()
@@ -183,6 +183,10 @@ class MultipleFileSelect(PropertyGroup):
single_file: bpy.props.StringProperty(name="Single File Path", description="", update=update_single_file)
file_list: bpy.props.CollectionProperty(type=StrProperty)
if TYPE_CHECKING:
single_file: str
file_list: bpy.types.bpy_prop_collection_idprop[StrProperty]
def set_file_list(self, dirname: str, files: list[str]) -> None:
self.file_list.clear()
+15 -5
View File
@@ -403,8 +403,11 @@ class BIM_PT_tabs(Panel):
bl_options = {"HIDE_HEADER"}
def draw(self, context):
if not UIData.is_loaded:
UIData.load()
is_ifc_project = bool(tool.Ifc.get())
aprops = tool.Blender.get_area_props(context)
ifc_icon = f"{UIData.data['icon_color_mode']}_ifc"
row = self.layout.row()
row.alignment = "CENTER"
@@ -412,8 +415,7 @@ class BIM_PT_tabs(Panel):
"bim.set_tab",
text="",
emboss=aprops.tab == "PROJECT",
depress=True,
icon_value=bonsai.bim.icons["IFC"].icon_id,
icon_value=bonsai.bim.icons[ifc_icon].icon_id,
).tab = "PROJECT"
self.draw_tab_entry(row, "FILE_3D", "OBJECT", is_ifc_project, aprops.tab == "OBJECT")
self.draw_tab_entry(row, "MATERIAL", "GEOMETRY", is_ifc_project, aprops.tab == "GEOMETRY")
@@ -505,9 +507,9 @@ class BIM_PT_tabs(Panel):
op = row.operator("bim.open_uri", text="", icon="QUESTION")
op.uri = "https://docs.bonsaibim.org/guides/troubleshooting.html#incompatible-blender-features"
def draw_tab_entry(self, row, icon, tab_name, enabled=True, highlight=False):
def draw_tab_entry(self, row, icon, tab_name, enabled=True, highlight=True):
tab_entry = row.row(align=True)
tab_entry.operator("bim.set_tab", text="", emboss=highlight, icon=icon, depress=True).tab = tab_name
tab_entry.operator("bim.set_tab", text="", emboss=highlight, icon=icon).tab = tab_name
tab_entry.enabled = enabled
@@ -1106,19 +1108,27 @@ class BIM_PT_tab_operations(Panel):
pass
def refresh():
UIData.is_loaded = False
class UIData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {"version": cls.version()}
cls.data = {"version": cls.version(), "icon_color_mode": cls.icon_color_mode()}
cls.is_loaded = True
@classmethod
def version(cls):
return tool.Blender.get_bonsai_version()
@classmethod
def icon_color_mode(cls):
return tool.Blender.detect_icon_color_mode()
def draw_statusbar(self, context):
if not UIData.is_loaded:
+4 -1
View File
@@ -157,6 +157,7 @@ def remove_representation(
assert element
element_type = geometry.get_element_type(element)
data = None
has_switched_from_data = False
if element_type and (geometry.is_mapped_representation(representation) or geometry.is_type_product(element)):
representation = geometry.resolve_mapped_representation(representation)
data = geometry.get_representation_data(representation)
@@ -164,6 +165,7 @@ def remove_representation(
for element in geometry.get_elements_of_type(element_type):
obj = ifc.get_object(element)
if obj:
has_switched_from_data = True
geometry.switch_from_representation(obj, representation)
obj = ifc.get_object(element_type)
if obj:
@@ -172,11 +174,12 @@ def remove_representation(
else:
data = geometry.get_representation_data(representation)
if data and geometry.has_data_users(data):
has_switched_from_data = True
geometry.switch_from_representation(obj, representation)
ifc.run("geometry.unassign_representation", product=element, representation=representation)
ifc.run("geometry.remove_representation", representation=representation)
if data:
if data and not has_switched_from_data:
geometry.delete_data(data)
+1 -1
View File
@@ -161,7 +161,7 @@ def edit_style(ifc: tool.Ifc, style: tool.Style) -> None:
style.disable_editing()
load_styles(style, style.get_active_style_type())
if is_style_side_attribute_edited:
style.reload_repersentations(style_element)
style.reload_representations(style_element)
def load_styles(style: tool.Style, style_type: str) -> None:
+27 -17
View File
@@ -16,58 +16,68 @@
# 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
from typing import TYPE_CHECKING, Optional
def load_systems(system):
if TYPE_CHECKING:
import bpy
import ifcopenshell
import bonsai.tool as tool
def load_systems(system: tool.System) -> None:
system.import_systems()
system.enable_system_editing_ui()
system.disable_editing_system()
def disable_system_editing_ui(system):
def disable_system_editing_ui(system: tool.System) -> None:
system.disable_editing_system()
system.disable_system_editing_ui()
def add_system(ifc, system, ifc_class=None):
def add_system(ifc: tool.Ifc, system: tool.System, ifc_class: str) -> None:
ifc.run("system.add_system", ifc_class=ifc_class)
system.import_systems()
def edit_system(ifc, system_tool, system=None):
def edit_system(ifc: tool.Ifc, system_tool: tool.System, system: ifcopenshell.entity_instance) -> None:
attributes = system_tool.export_system_attributes()
ifc.run("system.edit_system", system=system, attributes=attributes)
system_tool.disable_editing_system()
system_tool.import_systems()
def remove_system(ifc, system_tool, system=None):
def remove_system(ifc: tool.Ifc, system_tool: tool.System, system: ifcopenshell.entity_instance) -> None:
ifc.run("system.remove_system", system=system)
system_tool.import_systems()
def enable_editing_system(system_tool, system=None):
def enable_editing_system(system_tool: tool.System, system: ifcopenshell.entity_instance) -> None:
system_tool.import_system_attributes(system)
system_tool.set_active_edited_system(system)
def disable_editing_system(system):
def disable_editing_system(system: tool.System) -> None:
system.disable_editing_system()
def assign_system(ifc, system=None, product=None):
def assign_system(ifc: tool.Ifc, system: ifcopenshell.entity_instance, product: ifcopenshell.entity_instance) -> None:
ifc.run("system.assign_system", products=[product], system=system)
def unassign_system(ifc, system=None, product=None):
def unassign_system(ifc: tool.Ifc, system: ifcopenshell.entity_instance, product: ifcopenshell.entity_instance) -> None:
ifc.run("system.unassign_system", products=[product], system=system)
def select_system_products(system_tool, system=None):
def select_system_products(system_tool: tool.System, system: ifcopenshell.entity_instance) -> None:
system_tool.select_system_products(system)
system_tool.set_active_system(system)
def show_ports(ifc, system, spatial, element=None):
def show_ports(
ifc: tool.Ifc, system: tool.System, spatial: tool.Spatial, element: ifcopenshell.entity_instance
) -> None:
obj = ifc.get_object(element)
if obj and ifc.is_moved(obj):
system.run_geometry_edit_object_placement(obj=obj)
@@ -77,7 +87,7 @@ def show_ports(ifc, system, spatial, element=None):
spatial.select_products(ports)
def hide_ports(ifc, system, element=None):
def hide_ports(ifc: tool.Ifc, system: tool.System, element: ifcopenshell.entity_instance) -> None:
obj = ifc.get_object(element)
if obj and ifc.is_moved(obj):
system.run_geometry_edit_object_placement(obj=obj)
@@ -91,27 +101,27 @@ def hide_ports(ifc, system, element=None):
system.delete_element_objects(ports)
def add_port(ifc, system, element=None):
def add_port(ifc: tool.Ifc, system: tool.System, element: ifcopenshell.entity_instance) -> None:
system.load_ports(element, system.get_ports(element))
obj = system.create_empty_at_cursor_with_element_orientation(element)
port = system.run_root_assign_class(obj=obj, ifc_class="IfcDistributionPort", should_add_representation=False)
ifc.run("system.assign_port", element=element, port=port)
def remove_port(ifc, system, port=None):
def remove_port(ifc: tool.Ifc, system: tool.System, port: ifcopenshell.entity_instance) -> None:
system.delete_element_objects([port])
ifc.run("root.remove_product", product=port)
def connect_port(ifc, port1=None, port2=None):
def connect_port(ifc: tool.Ifc, port1: ifcopenshell.entity_instance, port2: ifcopenshell.entity_instance) -> None:
ifc.run("system.connect_port", port1=port1, port2=port2)
def disconnect_port(ifc, port=None):
def disconnect_port(ifc: tool.Ifc, port: ifcopenshell.entity_instance) -> None:
ifc.run("system.disconnect_port", port=port)
def set_flow_direction(ifc, system, port=None, direction=None):
def set_flow_direction(ifc: tool.Ifc, system: tool.System, port: ifcopenshell.entity_instance, direction: str) -> None:
port2 = system.get_connected_port(port)
if not port2:
return
+11 -7
View File
@@ -90,7 +90,7 @@ class Blender:
def get_name(cls, ifc_class, name): pass
def get_obj_ifc_definition_id(cls, obj=None, obj_type=None, context=None): pass
def get_object_bounding_box(cls, obj): pass
def get_selected_objects(cls): pass
def get_selected_objects(cls, include_active=False): pass
def get_viewport_context(cls): pass
def is_ifc_class_active(cls, ifc_class): pass
def is_ifc_object(cls, obj): pass
@@ -183,7 +183,7 @@ class Classification:
@interface
class Collector:
def assign(cls, obj): pass
def assign(cls, obj, should_clean_users_collection=False): pass
@interface
@@ -323,8 +323,10 @@ class Drawing:
def export_text_literal_attributes(cls, obj): pass
def generate_drawing_matrix(cls, target_view, location_hint): pass
def generate_drawing_name(cls, target_view, location_hint): pass
def generate_reference_attributes(cls, reference, **attributes): pass
def generate_sheet_identification(cls): pass
def get_annotation_context(cls, target_view, object_type=None): pass
def get_annotation_representation(cls, element_type): pass
def get_assigned_product(cls, element): pass
def get_body_context(cls): pass
def get_default_drawing_path(cls, name): pass
@@ -345,10 +347,10 @@ class Drawing:
def get_name(cls, element): pass
def get_path_filename(cls, uri): pass
def get_reference_description(cls, reference): pass
def generate_reference_attributes(cls, reference, **attributes): pass
def get_reference_document(cls, reference): pass
def get_reference_location(cls, reference): pass
def get_references_with_location(cls, location): pass
def get_representation(cls, element, context): pass
def get_text_literal(cls, obj): pass
def get_unit_system(cls): pass
def import_assigned_product(cls, obj): pass
@@ -365,9 +367,11 @@ class Drawing:
def open_layout_svg(cls, uri): pass
def open_spreadsheet(cls, uri): pass
def open_svg(cls, filepath): pass
def reload_representation(cls, obj, representation): pass
def remove_literal_from_annotation(cls, obj, literal): pass
def run_drawing_activate_model(cls): pass
def run_root_assign_class(cls, obj=None, ifc_class=None, predefined_type=None, should_add_representation=True, context=None, ifc_representation_class=None): pass
def run_type_assign_type(cls, element=None, relating_type=None): pass
def select_assigned_product(cls, drawing): pass
def set_drawing_collection_name(cls, drawing, collection): pass
def set_name(cls, element, name): pass
@@ -377,6 +381,7 @@ class Drawing:
def sync_object_placement(cls, obj): pass
def synchronise_ifc_and_text_attributes(cls, obj): pass
def update_embedded_svg_location(cls, uri, old_location, new_location): pass
def update_newline_at(cls, obj): pass
def update_text_size_pset(cls, obj): pass
def update_text_value(cls, obj): pass
@@ -412,12 +417,10 @@ class Geometry:
def get_total_representation_items(cls, obj): pass
def has_data_users(cls, data): pass
def has_material_style_override(cls, obj): pass
def import_representation(cls, obj, representation, apply_openings=True): pass
def import_representation_parameters(cls, data): pass
def is_body_representation(cls, representation): pass
def is_box_representation(cls, representation): pass
def is_data_supported_for_adding_representation(cls, data): pass
def is_edited(cls, obj): pass
def is_mapped_representation(cls, representation): pass
def is_type_product(cls, element): pass
def link(cls, element, obj): pass
@@ -756,6 +759,8 @@ class Root:
def is_containable(cls, element): pass
def is_drawing_annotation(cls, element): pass
def is_element_a(cls, element, ifc_class): pass
def is_in_aggregate_mode(cls, element): pass
def is_in_nest_mode(cls, element): pass
def is_spatial_element(cls, element): pass
def link_object_data(cls, source_obj, destination_obj): pass
def recreate_decompositions(cls, relationships, old_to_new): pass
@@ -1010,7 +1015,6 @@ class Style:
def get_elements_by_style(cls, style): pass
def get_currently_edited_material(cls): pass
def get_name(cls, obj): pass
def get_style(cls, obj): pass
def get_style_elements(cls, blender_material): pass
def get_surface_rendering_attributes(cls, obj, verbose=True): pass
def get_surface_rendering_style(cls, obj): pass
@@ -1026,7 +1030,7 @@ class Style:
def is_editing_styles(cls): pass
def reload_material_from_ifc(cls, obj): pass
def is_style_side_attribute_edited(cls, style, new_attributes): pass
def reload_repersentations(cls, style): pass
def reload_representations(cls, style): pass
@interface
+6 -3
View File
@@ -194,9 +194,8 @@ class Blender(bonsai.core.tool.Blender):
elif obj_type == "MaterialSetItem":
return bpy.data.objects.get(obj).BIMObjectMaterialProperties.active_material_set_item_id
elif obj_type == "Task":
return context.scene.BIMTaskTreeProperties.tasks[
context.scene.BIMWorkScheduleProperties.active_task_index
].ifc_definition_id
tprops = tool.Sequence.get_task_tree_props()
return tprops.tasks[context.scene.BIMWorkScheduleProperties.active_task_index].ifc_definition_id
elif obj_type == "Cost":
return context.scene.BIMCostProperties.cost_items[
context.scene.BIMCostProperties.active_cost_item_index
@@ -1323,6 +1322,10 @@ class Blender(bonsai.core.tool.Blender):
name = cls.get_blender_addon_package_name()
return importlib.import_module(name)
@classmethod
def is_addon_enabled(cls) -> bool:
return cls.get_blender_addon_package_name() in bpy.context.preferences.addons
@classmethod
def get_addon_preferences(cls) -> bonsai.bim.ui.BIM_ADDON_preferences:
blender_package_name = cls.get_blender_addon_package_name()
+20 -2
View File
@@ -16,15 +16,27 @@
# 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 mathutils
import bonsai.core.tool
import bonsai.tool as tool
from mathutils import Matrix, Vector
from typing import Any, Sequence
from typing import Any, Sequence, TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.boundary.prop import BIMBoundaryProperties, BIMObjectBoundaryProperties
class Boundary(bonsai.core.tool.Boundary):
@classmethod
def get_boundary_props(cls) -> BIMBoundaryProperties:
return bpy.context.scene.BIMBoundaryProperties
@classmethod
def get_object_boundary_props(cls, obj: bpy.types.Object) -> BIMObjectBoundaryProperties:
return obj.BIMBoundaryProperties
@classmethod
def get_assign_connection_geometry_settings(cls, obj: bpy.types.Object) -> dict[str, Any]:
from bonsai.bim.module.geometry.helper import Helper
@@ -76,10 +88,16 @@ class Boundary(bonsai.core.tool.Boundary):
@classmethod
def decorate_boundary(cls, obj: bpy.types.Object) -> None:
new = bpy.context.scene.BIMBoundaryProperties.boundaries.add()
props = cls.get_boundary_props()
new = props.boundaries.add()
new.obj = obj
obj.show_in_front = True
@classmethod
def undecorate_boundary(cls, obj: bpy.types.Object) -> None:
obj.show_in_front = False
props = cls.get_boundary_props()
for i in reversed(range(len(props.boundaries))):
if props.boundaries[i].obj == obj:
props.boundaries.remove(i)
break
+1 -1
View File
@@ -248,7 +248,7 @@ class Cost(bonsai.core.tool.Cost):
cls, cost_item: ifcopenshell.entity_instance, related_object_type: RELATED_OBJECT_TYPE
) -> None:
def create_list_items(
collection: bpy.types.bpy_prop_collection, cost_item: ifcopenshell.entity_instance, is_deep: bool
collection: bpy.types.CollectionProperty, cost_item: ifcopenshell.entity_instance, is_deep: bool
) -> None:
products = cls.get_cost_item_assignments(cost_item, filter_by_type=related_object_type, is_deep=False)
for product in products:
+5 -6
View File
@@ -19,10 +19,9 @@
import os
import json
import bpy
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.api.material
import ifcopenshell.express
import ifcopenshell.express.schema
import ifcopenshell.express.schema_class
import ifcopenshell.util.element
import ifcopenshell.util.schema
import bonsai.core.style
@@ -36,14 +35,14 @@ from typing import Iterable, Literal
class Debug(bonsai.core.tool.Debug):
@classmethod
def add_schema_identifier(cls, schema: ifcopenshell.express.schema_class.SchemaClass) -> None:
IfcStore.schema_identifiers.append(schema.schema_name)
def add_schema_identifier(cls, schema: W.schema_definition) -> None:
IfcStore.schema_identifiers.append(schema.name())
@classmethod
def load_express(cls, filename: str) -> ifcopenshell.express.schema_class.SchemaClass:
def load_express(cls, filename: str) -> W.schema_definition:
schema = ifcopenshell.express.parse(filename)
ifcopenshell.register_schema(schema)
return schema
return schema.schema
@classmethod
def purge_hdf5_cache(cls) -> None:
+39 -11
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import os
import re
import collections
@@ -34,8 +35,10 @@ import subprocess
import numpy as np
import bonsai.core.tool
import bonsai.core.geometry
import bonsai.core.type
import bonsai.tool as tool
import ifcopenshell.api
import ifcopenshell.api.document
import ifcopenshell.geom
import ifcopenshell.util.representation
import ifcopenshell.util.element
@@ -48,9 +51,13 @@ from shapely.ops import unary_union
from lxml import etree
from mathutils import Vector, Matrix
from fractions import Fraction
from typing import Optional, Union, Iterable, Any, Literal, Sequence
from typing import Optional, Union, Iterable, Any, Literal, Sequence, TYPE_CHECKING
from pathlib import Path
if TYPE_CHECKING:
from bonsai.bim.module.drawing.prop import DocProperties, Sheet
from bonsai.bim.module.drawing.prop import Drawing as DrawingProperties
class Drawing(bonsai.core.tool.Drawing):
ANNOTATION_DATA_TYPE = Literal["empty", "curve", "mesh"]
@@ -80,6 +87,10 @@ class Drawing(bonsai.core.tool.Drawing):
}
# fmt: on
@classmethod
def get_document_props(cls) -> DocProperties:
return bpy.context.scene.DocProperties
@classmethod
def canonicalise_class_name(cls, name: str) -> str:
return re.sub("[^0-9a-zA-Z]+", "", name)
@@ -819,7 +830,7 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def import_documents(cls, document_type: DOCUMENT_TYPE) -> None:
dprops = bpy.context.scene.DocProperties
dprops = cls.get_document_props()
if document_type == "SCHEDULE":
documents_collection = dprops.schedules
elif document_type == "REFERENCE":
@@ -838,7 +849,7 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def import_sheets(cls) -> None:
props = bpy.context.scene.DocProperties
props = cls.get_document_props()
expanded_sheets = {s.ifc_definition_id for s in props.sheets if s.is_expanded}
if not hasattr(cls, "sheet_selected_states"):
cls.sheet_selected_states = {}
@@ -878,13 +889,13 @@ class Drawing(bonsai.core.tool.Drawing):
new.reference_type = reference_description
@classmethod
def get_active_sheet(cls, context: bpy.types.Context) -> bpy.types.PropertyGroup:
props = context.scene.DocProperties
def get_active_sheet(cls, context: bpy.types.Context) -> Sheet:
props = cls.get_document_props()
return next(s for s in props.sheets[: props.active_sheet_index + 1][::-1] if s.is_sheet)
@classmethod
def get_active_drawing_item(cls) -> Union[bpy.types.PropertyGroup, None]:
props = bpy.context.scene.DocProperties
def get_active_drawing_item(cls) -> Union[DrawingProperties, None]:
props = cls.get_document_props()
drawing_index = props.active_drawing_index
if len(props.drawings) > drawing_index >= 0:
item = props.drawings[drawing_index]
@@ -892,10 +903,8 @@ class Drawing(bonsai.core.tool.Drawing):
return item
@classmethod
def get_active_sheet_item(
cls, *, is_sheet: bool = False, reference_type: str = ""
) -> Union[bpy.types.PropertyGroup, None]:
props = bpy.context.scene.DocProperties
def get_active_sheet_item(cls, *, is_sheet: bool = False, reference_type: str = "") -> Union[Sheet, None]:
props = cls.get_document_props()
sheet_index = props.active_sheet_index
if len(props.sheets) > sheet_index >= 0:
item = props.sheets[sheet_index]
@@ -1320,6 +1329,12 @@ class Drawing(bonsai.core.tool.Drawing):
context=context,
ifc_representation_class=None,
)
bpy.data.curves.remove(data)
assert element
representation = ifcopenshell.util.representation.get_representation(element, context)
assert representation
obj.data["ios_edges_item_ids"] = (representation.Items[0].id(),)
return element
@classmethod
@@ -2252,3 +2267,16 @@ class Drawing(bonsai.core.tool.Drawing):
msp.add_line(*points)
finalize_dxf()
@classmethod
def remove_drawing_from_sheet(cls, reference: ifcopenshell.entity_instance) -> None:
import bonsai.bim.module.drawing.sheeter as sheeter
sheet = tool.Drawing.get_reference_document(reference)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.remove_drawing(reference, sheet)
ifcopenshell.api.document.remove_reference(tool.Ifc.get(), reference=reference)
tool.Drawing.import_sheets()
+9 -3
View File
@@ -57,10 +57,14 @@ from typing_extensions import TypeIs
if TYPE_CHECKING:
from bonsai.bim.prop import Attribute
from bonsai.bim.module.geometry.prop import BIMObjectGeometryProperties
from bonsai.bim.module.geometry.prop import BIMObjectGeometryProperties, BIMGeometryProperties
class Geometry(bonsai.core.tool.Geometry):
@classmethod
def get_geometry_props(cls) -> BIMGeometryProperties:
return bpy.context.scene.BIMGeometryProperties
@classmethod
def get_object_geometry_props(cls, object: bpy.types.Object) -> BIMObjectGeometryProperties:
return object.BIMGeometryProperties
@@ -191,6 +195,7 @@ class Geometry(bonsai.core.tool.Geometry):
return bonsai.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element)
elif element.is_a("IfcRelSpaceBoundary"):
ifcopenshell.api.run("boundary.remove_boundary", tool.Ifc.get(), boundary=element)
tool.Boundary.undecorate_boundary(obj)
return bpy.data.objects.remove(obj)
elif element.is_a("IfcGridAxis"):
is_last_axis = False
@@ -479,7 +484,7 @@ class Geometry(bonsai.core.tool.Geometry):
@classmethod
def get_active_representation(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
"""< IfcShapeRepresentation or None"""
""":return: IfcRepresentation/IfcRepresentationItem or None"""
if obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.ifc_definition_id:
return tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
@@ -842,13 +847,13 @@ class Geometry(bonsai.core.tool.Geometry):
representation = tool.Ifc.get().by_id(int(geometry.id.split("-")[0]))
if geometry:
mesh = ifc_importer.create_mesh(element, geometry)
tool.Loader.link_mesh(geometry, mesh)
ifc_importer.material_creator.load_existing_materials()
shape_has_openings = False
ifc_importer.material_creator.create(element, obj, mesh, shape_has_openings)
mesh.BIMMeshProperties.has_openings_applied = apply_openings
if not shape_has_openings:
tool.Loader.load_indexed_colour_map(representation, mesh)
tool.Loader.link_mesh(geometry, mesh)
meshes[mesh_name] = mesh
change_data(obj, element, mesh)
@@ -1837,6 +1842,7 @@ class Geometry(bonsai.core.tool.Geometry):
tool.Blender.select_and_activate_single_object(bpy.context, dup_obj)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True, properties=False)
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.separate(type="LOOSE")
+10 -5
View File
@@ -26,6 +26,7 @@ import ifcopenshell.util.schema
import bonsai.core.tool
import bonsai.bim.handler
import bonsai.tool as tool
from pathlib import Path
from bonsai.bim.ifc import IfcStore, IFC_CONNECTED_TYPE
from typing import Optional, Union, Any, final, Literal
@@ -247,13 +248,17 @@ class Ifc(bonsai.core.tool.Ifc):
return (uri if not uri else os.path.join(ifc_path, uri)).replace("\\", "/")
@classmethod
def get_relative_uri(cls, uri: str) -> str:
if not os.path.isabs(uri):
return uri
ifc_path = cls.get_path()
def get_uri(cls, uri: str | Path, use_relative_path: bool = False) -> str:
if not use_relative_path:
return Path(uri).absolute().resolve().as_posix()
uri = Path(uri)
if not os.path.isabs(uri) or not (ifc_path := cls.get_path()):
return uri.as_posix().replace("\\", "/")
if Path(uri).drive != Path(ifc_path).drive:
return uri.as_posix().replace("\\", "/")
if os.path.isfile(ifc_path):
ifc_path = os.path.dirname(ifc_path)
return os.path.relpath(uri, ifc_path).replace("\\", "/")
return Path(os.path.relpath(uri, ifc_path)).as_posix().replace("\\", "/")
@classmethod
def unlink(
+1 -1
View File
@@ -27,7 +27,7 @@ from typing import Literal, Any, Union
class Library(bonsai.core.tool.Library):
@classmethod
def clear_editing_mode(cls) -> None:
bpy.context.scene.BIMLibraryProperties.editing_mode = "-"
bpy.context.scene.BIMLibraryProperties.editing_mode = "NONE"
@classmethod
def export_library_attributes(cls) -> dict[str, Any]:
+4 -3
View File
@@ -95,7 +95,7 @@ class Loader(bonsai.core.tool.Loader):
def get_name(cls, element: ifcopenshell.entity_instance) -> str:
if element.is_a("IfcGridAxis"):
return "{}/{}".format(element.is_a(), element.AxisTag)
return "{}/{}".format(element.is_a(), getattr(element, "Name", "None"))
return "{}/{}".format(element.is_a(), getattr(element, "Name", "Unnamed") or "Unnamed")
@classmethod
def link_mesh(
@@ -298,6 +298,7 @@ class Loader(bonsai.core.tool.Loader):
if is_relative:
ifc_path = Path(tool.Ifc.get_path())
image_url = ifc_path.parent / image_url
image_url = image_url.absolute().resolve()
if not image_url.exists():
print(f"WARNING. Couldn't find texture by path {image_url}, it will be skipped.")
@@ -592,11 +593,11 @@ class Loader(bonsai.core.tool.Loader):
faces_tex_coord_data = {}
for tex_coord_index, face_remap in zip(texture_map, faces_remap, strict=True):
faces_tex_coord_data[frozenset(face_remap)] = (tex_coord_index, face_remap)
faces_tex_coord_data[tuple(face_remap)] = (tex_coord_index, face_remap)
# Apply attribute to each face
for bface in bm.faces:
face = frozenset(loop.vert.index for loop in bface.loops)
face = tuple(loop.vert.index for loop in bface.loops)
# Find the corresponding index in data list by matching ifc faceset with blender face.
data_index = None
if tex_coord_data := faces_tex_coord_data.get(face):
+12 -119
View File
@@ -207,7 +207,6 @@ class Model(bonsai.core.tool.Model):
)
surface.OuterBoundary = tool.Ifc.get().add(profile_def.OuterCurve)
surface.InnerBoundaries = results
if profile_def.is_a("IfcArbitraryProfileDefWithVoids"):
surface.InnerBoundaries = [tool.Ifc.get().add(c) for c in profile_def.InnerCurves]
@@ -1456,71 +1455,20 @@ class Model(bonsai.core.tool.Model):
`assigned_material` argument is there just to indicate whether we apply material changes
after material assignment or material unassignment.
"""
if assigned_material:
# NOTE: currently only IfcMaterials are supported
# for anyone else we just switch representation.
if not assigned_material.is_a("IfcMaterial"):
tool.Geometry.reload_representation([tool.Ifc.get_object(e) for e in elements])
return
# Since different elements can share meshes (e.g. occurrences without openings)
# we need to make sure not to process them multiple times.
meshes_users: dict[bpy.types.Mesh, set[bpy.types.Object]] = dict()
for obj in bpy.data.objects:
if not obj.data:
continue
meshes_users.setdefault(obj.data, set()).add(obj)
objects: set[bpy.types.Object] = set()
for element in elements:
obj: bpy.types.Object = tool.Ifc.get_object(element)
if not obj or not obj.data:
if not (obj := tool.Ifc.get_object(element)) or not obj.data:
continue
objects.add(obj)
meshes: set[bpy.types.Mesh] = {obj.data for obj in objects}
for mesh in meshes:
mesh_users = meshes_users[mesh]
if not mesh_users.issubset(objects):
# It's unsafe to make changes to the mesh
# as it's used by objects unrelated to the current change.
# E.g. material with a style was assigned to a particular occurrence
# and this change shouldn't be applied to other occurrences and type.
objs_to_reload = mesh_users.intersection(objects)
for obj in objs_to_reload:
tool.Geometry._reload_representation(obj)
continue
obj = next(iter(mesh_users))
element = tool.Ifc.get_entity(obj)
assert element # Type checker.
own_material = next(iter(ifcopenshell.util.element.get_materials(element, should_inherit=False)), None)
inherited_mstyle = tool.Geometry.get_inherited_material_style(element)
if assigned_material:
if own_material:
ms2 = tool.Material.get_style(own_material)
ms1 = inherited_mstyle
else:
ms1 = None
ms2 = inherited_mstyle
cls.replace_material_style(mesh, obj, ms1, ms2)
tool.Geometry.record_object_materials(obj)
else: # Material unnassignment.
if not tool.Geometry.has_geometry_without_styles(mesh):
tool.Geometry._reload_representation(obj)
continue
if not inherited_mstyle:
continue
cls.replace_material_style(mesh, obj, None, inherited_mstyle)
tool.Geometry.record_object_materials(obj)
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=True,
)
@classmethod
def get_occurrences_without_material_override(
@@ -1533,61 +1481,6 @@ class Model(bonsai.core.tool.Model):
]
return occurrences
@classmethod
def replace_material_style(
cls,
mesh: bpy.types.Mesh,
obj: bpy.types.Object,
mstyle1: Union[ifcopenshell.entity_instance, None],
mstyle2: Union[ifcopenshell.entity_instance, None],
) -> None:
if mstyle1 == mstyle2:
return
# Get Blender materials.
mbstyle1, mbstyle2 = None, None
if mstyle1:
mbstyle1 = tool.Ifc.get_object(mstyle1)
assert isinstance(mbstyle1, bpy.types.Material)
if mstyle2:
mbstyle2 = tool.Ifc.get_object(mstyle2)
assert isinstance(mbstyle2, bpy.types.Material)
# Copy data to the list as mesh.materials doesn't allow to search for None.
materials: list[Union[bpy.types.Material, None]] = mesh.materials[:]
# Material style is overridden by representation item, nothing to change.
if mesh.materials and mbstyle1 not in materials:
return
i1 = None
if mbstyle1 is None:
if not materials:
mesh.materials.append(None)
i1 = 0
else:
i1 = materials.index(None)
else:
rep = tool.Geometry.get_active_representation(obj)
assert rep # Type checker.
rep_styles = tool.Geometry.get_representation_styles(rep)
if mbstyle1 in rep_styles:
tool.Geometry._reload_representation(obj)
return
else:
i1 = materials.index(mbstyle1)
if mbstyle2 in materials:
i2 = materials.index(mbstyle2)
# Reassign faces.
buffer = np.empty(len(mesh.polygons), dtype=np.int32)
mesh.polygons.foreach_get("material_index", buffer)
buffer[buffer == i1] = i2
mesh.polygons.foreach_set("material_index", buffer)
mesh.materials.pop(index=i1)
else:
mesh.materials[i1] = mbstyle2
@classmethod
def add_body_representation(cls, obj: bpy.types.Object) -> None:
ifc_file = tool.Ifc.get()
+23 -4
View File
@@ -303,12 +303,14 @@ class Project(bonsai.core.tool.Project):
library_file = IfcStore.library_file
assert library_file
if props.selected_project_library == "*":
selected_project_library = props.selected_project_library if props.filter_by_library else "*"
if selected_project_library == "*":
def condition(elements: list[ifcopenshell.entity_instance]):
yield from elements
elif props.selected_project_library == "-":
elif selected_project_library == "-":
def condition(elements: list[ifcopenshell.entity_instance]):
for element in elements:
@@ -316,7 +318,7 @@ class Project(bonsai.core.tool.Project):
yield element
else:
project_library = library_file.by_id(int(props.selected_project_library))
project_library = library_file.by_id(int(selected_project_library))
project_library_rels = set(project_library.Declares)
if project_library_rels:
@@ -330,6 +332,23 @@ class Project(bonsai.core.tool.Project):
else:
def condition(elements: list[ifcopenshell.entity_instance]):
pass
# Hacky way to create empty generator.
return
yield
return condition
@classmethod
def update_current_library_page(cls):
props = cls.get_project_props()
element_name = props.active_library_element
bpy.ops.bim.rewind_library()
bpy.ops.bim.change_library_element(element_name=element_name)
@classmethod
def get_parent_library(cls, project_library: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
if nests := project_library.Nests:
# IfcProjectLibrary.
return nests[0].RelatingObject
# IfcProject.
return project_library.HasContext[0].RelatingContext
+16 -5
View File
@@ -16,18 +16,24 @@
# 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 ifcopenshell
import ifcopenshell.api.pset
import ifcopenshell.util.attribute
import ifcopenshell.util.element
import bonsai.bim.helper
import bonsai.core.tool
import bonsai.tool as tool
import bonsai.bim.schema
from typing import Union, Literal, Any
from typing import Union, Literal, Any, TYPE_CHECKING
from typing_extensions import assert_never
if TYPE_CHECKING:
from bonsai.bim.module.pset.prop import PsetProperties
class Pset(bonsai.core.tool.Pset):
PSET_TYPE = Literal["PSET", "QTO"]
@@ -40,7 +46,7 @@ class Pset(bonsai.core.tool.Pset):
return tool.Ifc.get().by_id(pset["id"])
@classmethod
def get_pset_props(cls, obj: str, obj_type: tool.Ifc.OBJECT_TYPE) -> bpy.types.PropertyGroup:
def get_pset_props(cls, obj: str, obj_type: tool.Ifc.OBJECT_TYPE) -> PsetProperties:
if obj_type == "Object":
return bpy.data.objects.get(obj).PsetProperties
elif obj_type == "Material":
@@ -75,7 +81,9 @@ class Pset(bonsai.core.tool.Pset):
predefined_type = ifcopenshell.util.element.get_predefined_type(element)
return bool(
pset_name
in bonsai.bim.schema.ifc.psetqto.get_applicable_names(element.is_a(), predefined_type, pset_only=True)
in bonsai.bim.schema.ifc.psetqto.get_applicable_names(
element.is_a(), predefined_type, pset_only=True, schema=tool.Ifc.get_schema()
)
)
@classmethod
@@ -130,7 +138,7 @@ class Pset(bonsai.core.tool.Pset):
return special_type
@classmethod
def import_pset_from_existing(cls, pset: ifcopenshell.entity_instance, props: bpy.types.PropertyGroup) -> None:
def import_pset_from_existing(cls, pset: ifcopenshell.entity_instance, props: PsetProperties) -> None:
pset_props = []
if pset.is_a("IfcElementQuantity"):
pset_props = pset.Quantities
@@ -295,7 +303,10 @@ class Pset(bonsai.core.tool.Pset):
# to prevent data loss and error casting data.
# Property will be added later by import_pset_from_existing.
for prop_template in sorted(pset_template.HasPropertyTemplates, key=lambda p: p.Name):
if not prop_template.is_a("IfcSimplePropertyTemplate"):
if (
not prop_template.is_a("IfcSimplePropertyTemplate")
or prop_template.PrimaryMeasureType == "IfcComplexNumber"
):
continue # Other types not yet supported
prop_data = data.get(prop_template.Name)
if prop_template.TemplateType == "P_SINGLEVALUE":
+19
View File
@@ -24,6 +24,7 @@ import ifcopenshell.util.attribute
import ifcopenshell.util.element
import bonsai.core.tool
import bonsai.tool as tool
from pathlib import Path
from typing import Union, Literal, Any, final
from typing_extensions import assert_never, TYPE_CHECKING
from bonsai.bim.ifc import IfcStore
@@ -119,3 +120,21 @@ class PsetTemplate(bonsai.core.tool.PsetTemplate):
# Disable because of the intersecting enums in data.py.
props.active_pset_template_id = 0
PSET_TEMPLATE_LOCATION = Literal["Global Pset Template", "Project Pset Template"]
@classmethod
def get_pset_template_files(cls) -> list[tuple[Path, "PSET_TEMPLATE_LOCATION"]]:
"""
:return: List of pset template files. Each template file is represented
by a tuple of the filepath and pset template location source.
"""
paths: list[tuple[Path, tool.PsetTemplate.PSET_TEMPLATE_LOCATION]] = []
for f in tool.Blender.get_data_dir_paths("pset", "*.ifc"):
paths.append((f, "Global Pset Template"))
pset_dir = Path(tool.Ifc.resolve_uri(bpy.context.scene.BIMProperties.pset_dir))
if pset_dir.is_dir():
for path in Path(pset_dir).glob("*.ifc"):
paths.append((path, "Project Pset Template"))
return paths
+11 -6
View File
@@ -16,7 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import json
import lark
@@ -26,7 +26,10 @@ import ifcopenshell.guid
import ifcopenshell.util.selector
from itertools import cycle
from bonsai.bim.prop import BIMFacet
from typing import Union, Literal
from typing import Union, Literal, TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.prop import BIMFilterGroup
class Search(bonsai.core.tool.Search):
@@ -35,7 +38,7 @@ class Search(bonsai.core.tool.Search):
return json.loads(group.Description)["query"]
@classmethod
def get_filter_groups(cls, module: str) -> bpy.types.bpy_prop_collection:
def get_filter_groups(cls, module: str) -> bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]:
if module == "search":
return bpy.context.scene.BIMSearchProperties.filter_groups
elif module == "csv":
@@ -54,13 +57,15 @@ class Search(bonsai.core.tool.Search):
assert False, f"Unsupported module: {module}"
@classmethod
def import_filter_query(cls, query: str, filter_groups: bpy.types.bpy_prop_collection) -> None:
def import_filter_query(
cls, query: str, filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
) -> None:
filter_groups.clear()
transformer = ImportFilterQueryTransformer(filter_groups)
transformer.transform(ifcopenshell.util.selector.filter_elements_grammar.parse(query))
@classmethod
def export_filter_query(cls, filter_groups: bpy.types.bpy_prop_collection) -> str:
def export_filter_query(cls, filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]) -> str:
query = []
for filter_group in filter_groups:
filter_group_query = []
@@ -300,7 +305,7 @@ class Search(bonsai.core.tool.Search):
class ImportFilterQueryTransformer(lark.Transformer):
def __init__(self, filter_groups):
def __init__(self, filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]):
self.filter_groups = filter_groups
def get_results(self):
+16 -10
View File
@@ -42,12 +42,17 @@ from typing import Optional, Any, Union, Literal, TYPE_CHECKING, Iterable
if TYPE_CHECKING:
import bonsai.bim.prop
from bonsai.bim.module.sequence.prop import BIMTaskTreeProperties
class Sequence(bonsai.core.tool.Sequence):
RELATED_OBJECT_TYPE = Literal["RESOURCE", "PRODUCT", "CONTROL"]
@classmethod
def get_task_tree_props(cls) -> BIMTaskTreeProperties:
return bpy.context.scene.BIMTaskTreeProperties
@classmethod
def get_work_plan_attributes(cls) -> dict[str, Any]:
import bonsai.bim.module.sequence.helper as helper
@@ -146,7 +151,8 @@ class Sequence(bonsai.core.tool.Sequence):
@classmethod
def load_task_tree(cls, work_schedule: ifcopenshell.entity_instance) -> None:
bpy.context.scene.BIMTaskTreeProperties.tasks.clear()
props = cls.get_task_tree_props()
props.tasks.clear()
props = bpy.context.scene.BIMWorkScheduleProperties
cls.contracted_tasks = json.loads(props.contracted_tasks)
@@ -185,7 +191,8 @@ class Sequence(bonsai.core.tool.Sequence):
@classmethod
def create_new_task_li(cls, related_object_id: int, level_index: int) -> None:
task = tool.Ifc.get().by_id(related_object_id)
new = bpy.context.scene.BIMTaskTreeProperties.tasks.add()
props = cls.get_task_tree_props()
new = props.tasks.add()
new.ifc_definition_id = related_object_id
new.is_expanded = related_object_id not in cls.contracted_tasks
new.level_index = level_index
@@ -199,7 +206,7 @@ class Sequence(bonsai.core.tool.Sequence):
@classmethod
def load_task_properties(cls, task: Optional[ifcopenshell.entity_instance] = None) -> None:
props = bpy.context.scene.BIMWorkScheduleProperties
task_props = bpy.context.scene.BIMTaskTreeProperties
task_props = cls.get_task_tree_props()
tasks_with_visual_bar = cls.get_task_bar_list()
props.is_task_update_enabled = False
@@ -281,8 +288,9 @@ class Sequence(bonsai.core.tool.Sequence):
@classmethod
def contract_all_tasks(cls) -> None:
props = bpy.context.scene.BIMWorkScheduleProperties
tprops = cls.get_task_tree_props()
contracted_tasks = json.loads(props.contracted_tasks)
for task_item in bpy.context.scene.BIMTaskTreeProperties.tasks:
for task_item in tprops.tasks:
if task_item.is_expanded:
contracted_tasks.append(task_item.ifc_definition_id)
props.contracted_tasks = json.dumps(contracted_tasks)
@@ -302,7 +310,7 @@ class Sequence(bonsai.core.tool.Sequence):
def disable_selecting_deleted_task(cls) -> None:
props = bpy.context.scene.BIMWorkScheduleProperties
if props.active_task_id not in [
task.ifc_definition_id for task in bpy.context.scene.BIMTaskTreeProperties.tasks
task.ifc_definition_id for task in cls.get_task_tree_props().tasks
]: # Task was deleted
bpy.context.scene.BIMWorkScheduleProperties.active_task_id = 0
bpy.context.scene.BIMWorkScheduleProperties.active_task_time_id = 0
@@ -310,9 +318,7 @@ class Sequence(bonsai.core.tool.Sequence):
@classmethod
def get_checked_tasks(cls) -> list[ifcopenshell.entity_instance]:
return [
tool.Ifc.get().by_id(task.ifc_definition_id)
for task in bpy.context.scene.BIMTaskTreeProperties.tasks
if task.is_selected
tool.Ifc.get().by_id(task.ifc_definition_id) for task in cls.get_task_tree_props().tasks if task.is_selected
] or []
@classmethod
@@ -486,7 +492,7 @@ class Sequence(bonsai.core.tool.Sequence):
@classmethod
def get_highlighted_task(cls) -> Union[ifcopenshell.entity_instance, None]:
tasks = bpy.context.scene.BIMTaskTreeProperties.tasks
tasks = cls.get_task_tree_props().tasks
if len(tasks) and len(tasks) > bpy.context.scene.BIMWorkScheduleProperties.active_task_index:
return tool.Ifc.get().by_id(
tasks[bpy.context.scene.BIMWorkScheduleProperties.active_task_index].ifc_definition_id
@@ -789,7 +795,7 @@ class Sequence(bonsai.core.tool.Sequence):
cls.load_task_tree(work_schedule)
cls.load_task_properties()
task_props = bpy.context.scene.BIMTaskTreeProperties
task_props = cls.get_task_tree_props()
expanded_tasks = [item.ifc_definition_id for item in task_props.tasks]
bpy.context.scene.BIMWorkScheduleProperties.active_task_index = expanded_tasks.index(task.id()) or 0
+138 -10
View File
@@ -212,7 +212,9 @@ class Spatial(bonsai.core.tool.Spatial):
if props.should_include_children:
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True)
else:
elements = ifcopenshell.util.element.get_contained(container)
elements = set(ifcopenshell.util.element.get_contained(container))
for e in elements:
elements.update(ifcopenshell.util.element.get_decomposition(e))
flat_results: dict[str, Any] = {}
reference_names: dict[str, str] = {}
for element in elements:
@@ -257,14 +259,20 @@ class Spatial(bonsai.core.tool.Spatial):
if props.should_include_children:
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True)
else:
elements = ifcopenshell.util.element.get_contained(container)
elements = set(ifcopenshell.util.element.get_contained(container))
for e in elements:
elements.update(ifcopenshell.util.element.get_decomposition(e))
for element in elements:
if element.is_a("IfcOpeningElement") or tool.Root.is_spatial_element(element):
continue
element_type = ifcopenshell.util.element.get_type(element)
ifc_class = element.is_a()
ifc_definition_id = element_type.id() if element_type else 0
type_name = element_type.Name or "Unnamed" if element_type else f"Untyped {element.is_a()}"
type_name = (
element_type.is_a() + "/" + element_type.Name or "Unnamed"
if element_type
else f"Untyped {element.is_a()}"
)
class_data = results.setdefault(ifc_class, {})
type_data = class_data.setdefault(ifc_definition_id, {"type_name": type_name, "elements": []})
type_data["elements"].append(element)
@@ -348,6 +356,8 @@ class Spatial(bonsai.core.tool.Spatial):
def add_elements(elements, level=0):
for element in sorted(elements, key=lambda x: f"{x.is_a()}/{x.Name or 'Unnamed'}"):
if not props.should_include_children and tool.Root.is_spatial_element(element):
continue
ifc_definition_id = element.id()
new = props.elements.add()
new.name = f"{element.is_a()}/{element.Name or 'Unnamed'}"
@@ -406,7 +416,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def filter_elements(
cls,
elements: List[ifcopenshell.entity_instance],
elements: Iterable[ifcopenshell.entity_instance],
ifc_class: str | None,
relating_type: ifcopenshell.entity_instance | None,
is_untyped: bool,
@@ -1133,23 +1143,27 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def toggle_spaces_visibility_wired_and_textured(cls, spaces: list[ifcopenshell.entity_instance]) -> None:
first_obj = tool.Ifc.get_object(spaces[0])
if bpy.data.objects[first_obj.name].display_type == "TEXTURED":
assert isinstance(first_obj, bpy.types.Object)
obj: bpy.types.Object
if first_obj.display_type == "TEXTURED":
for space in spaces:
obj = tool.Ifc.get_object(space)
bpy.data.objects[obj.name].show_wire = True
bpy.data.objects[obj.name].display_type = "WIRE"
obj.show_wire = True
obj.display_type = "WIRE"
return
elif bpy.data.objects[first_obj.name].display_type == "WIRE":
elif first_obj.display_type == "WIRE":
for space in spaces:
obj = tool.Ifc.get_object(space)
bpy.data.objects[obj.name].show_wire = False
bpy.data.objects[obj.name].display_type = "TEXTURED"
obj.show_wire = False
obj.display_type = "TEXTURED"
return
@classmethod
def toggle_hide_spaces(cls, spaces: list[ifcopenshell.entity_instance]) -> None:
first_obj = tool.Ifc.get_object(spaces[0])
assert isinstance(first_obj, bpy.types.Object)
obj: bpy.types.Object
if first_obj.hide_get() == False:
for space in spaces:
obj = tool.Ifc.get_object(space)
@@ -1228,3 +1242,117 @@ class Spatial(bonsai.core.tool.Spatial):
):
props = bpy.context.active_object.BIMObjectSpatialProperties
props.container_obj = obj
@classmethod
def get_filtered_elements(cls, should_filter: bool = True) -> Iterable[ifcopenshell.entity_instance]:
ifc_file = tool.Ifc.get()
props = bpy.context.scene.BIMSpatialDecompositionProperties
container = ifc_file.by_id(props.active_container.ifc_definition_id)
element_filter = props.element_filter
active_element = props.active_element
if not should_filter:
if props.should_include_children:
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True)
else:
elements = set(ifcopenshell.util.element.get_contained(container))
for e in elements:
elements.update(ifcopenshell.util.element.get_decomposition(e))
if not element_filter:
return elements
keyword = element_filter.lower()
if props.element_mode == "TYPE":
filtered_occurrences = set()
filtered_classes = set()
filtered_types = set()
for item in props.elements:
if item.type == "CLASS" and not item.is_expanded and keyword in item.name.lower():
filtered_classes.add(item.name)
elif item.type == "TYPE" and not item.is_expanded and keyword in item.name.lower():
if item.ifc_definition_id:
filtered_types.add(ifc_file.by_id(item.ifc_definition_id))
else:
filtered_types.add(item.name.split(" ")[1])
elif item.type == "OCCURRENCE" and keyword in item.name.lower():
filtered_occurrences.add(ifc_file.by_id(item.ifc_definition_id))
return {
e
for e in elements
if e.is_a() in filtered_classes
or ((e_type := ifcopenshell.util.element.get_type(e)) and e_type in filtered_types)
or (not e_type and e.is_a() in filtered_types)
} | filtered_occurrences
elif props.element_mode == "DECOMPOSITION":
return [ifc_file.by_id(i.ifc_definition_id) for i in props.elements if keyword in i.name.lower()]
elif props.element_mode == "CLASSIFICATION":
filtered_classifications = set()
filtered_occurrences = set()
for item in props.elements:
if item.type == "CLASSIFICATION" and not item.is_expanded and keyword in item.name.lower():
filtered_classifications.add(item.identification)
elif item.type == "OCCURRENCE" and keyword in item.name.lower():
filtered_occurrences.add(ifc_file.by_id(item.ifc_definition_id))
for element in elements:
if refs := ifcopenshell.util.classification.get_references(element):
for ref in refs:
for filtered_classification in filtered_classifications:
if ref[1].startswith(filtered_classification):
filtered_occurrences.add(element)
elif "Unclassified" in filtered_classifications:
filtered_occurrences.add(element)
return filtered_occurrences
return elements
if not active_element:
return []
if props.element_mode == "TYPE":
if active_element.type == "OCCURRENCE":
return {ifc_file.by_id(active_element.ifc_definition_id)}
ifc_class = relating_type = None
is_untyped = False
if active_element.type == "CLASS":
ifc_class = active_element.name
elif active_element.type == "TYPE":
ifc_class = active_element.ifc_class
if ifc_id := active_element.ifc_definition_id:
relating_type = ifc_file.by_id(ifc_id)
if props.should_include_children:
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True)
else:
elements = set(ifcopenshell.util.element.get_contained(container))
for e in elements:
elements.update(ifcopenshell.util.element.get_decomposition(e))
return cls.filter_elements(elements, ifc_class, relating_type, is_untyped, element_filter)
elif props.element_mode == "DECOMPOSITION":
occurrence = ifc_file.by_id(active_element.ifc_definition_id)
elements = ifcopenshell.util.element.get_decomposition(occurrence, is_recursive=True)
elements.add(occurrence)
return elements
elif props.element_mode == "CLASSIFICATION":
if active_element.type == "OCCURRENCE":
return {ifc_file.by_id(active_element.ifc_definition_id)}
if active_element.type == "CLASSIFICATION":
identification = active_element.identification
if props.should_include_children:
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True)
else:
elements = set(ifcopenshell.util.element.get_contained(container))
for e in elements:
elements.update(ifcopenshell.util.element.get_decomposition(e))
def filter_element(element: ifcopenshell.entity_instance) -> bool:
references = ifcopenshell.util.classification.get_references(element)
if identification == "Unclassified":
if not references:
return True
elif any([r for r in references if r[1].startswith(identification)]):
return True
return False
return filter(filter_element, elements)
+1 -1
View File
@@ -679,7 +679,7 @@ class Style(bonsai.core.tool.Style):
return old_value != new_value and "NEGATIVE" in (old_value, new_value)
@classmethod
def reload_repersentations(cls, style: ifcopenshell.entity_instance) -> None:
def reload_representations(cls, style: ifcopenshell.entity_instance) -> None:
elements = ifcopenshell.util.element.get_elements_by_style(tool.Ifc.get(), style)
objects = [tool.Ifc.get_object(e) for e in elements]
tool.Geometry.reload_representation(objects)
+68 -34
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import ifcopenshell.util.element
import ifcopenshell.util.system
@@ -31,11 +32,30 @@ from mathutils import Matrix, Vector
from bonsai.bim.module.system.data import ObjectSystemData, SystemDecorationData
from bonsai.bim.module.drawing.decoration import profile_consequential
from enum import Enum
from typing import TYPE_CHECKING, Optional, Any, Union
if TYPE_CHECKING:
from bonsai.bim.module.system.prop import BIMSystemProperties, BIMZoneProperties
class System(bonsai.core.tool.System):
@classmethod
def add_ports(cls, obj, add_start_port=True, add_end_port=True, end_port_pos=None, offset_end_port=None):
def get_system_props(cls) -> BIMSystemProperties:
return bpy.context.scene.BIMSystemProperties
@classmethod
def get_zone_props(cls) -> BIMZoneProperties:
return bpy.context.scene.BIMZoneProperties
@classmethod
def add_ports(
cls,
obj: bpy.types.Object,
add_start_port: bool = True,
add_end_port: bool = True,
end_port_pos: Optional[Vector] = None,
offset_end_port: Optional[Vector] = None,
) -> list[ifcopenshell.entity_instance]:
def add_port(mep_element, matrix):
port = tool.Ifc.run("system.add_port", element=mep_element)
port.FlowDirection = "NOTDEFINED"
@@ -66,7 +86,7 @@ class System(bonsai.core.tool.System):
return ports
@classmethod
def create_empty_at_cursor_with_element_orientation(cls, element):
def create_empty_at_cursor_with_element_orientation(cls, element: ifcopenshell.entity_instance) -> bpy.types.Object:
element_obj = tool.Ifc.get_object(element)
obj = bpy.data.objects.new("Port", None)
obj.matrix_world = element_obj.matrix_world
@@ -75,38 +95,38 @@ class System(bonsai.core.tool.System):
return obj
@classmethod
def delete_element_objects(cls, elements):
def delete_element_objects(cls, elements: list[ifcopenshell.entity_instance]) -> None:
for element in elements:
obj = tool.Ifc.get_object(element)
if obj:
bpy.data.objects.remove(obj)
@classmethod
def disable_editing_system(cls):
bpy.context.scene.BIMSystemProperties.edited_system_id = 0
def disable_editing_system(cls) -> None:
cls.get_system_props().edited_system_id = 0
@classmethod
def disable_system_editing_ui(cls):
bpy.context.scene.BIMSystemProperties.is_editing = False
def disable_system_editing_ui(cls) -> None:
cls.get_system_props().is_editing = False
@classmethod
def enable_system_editing_ui(cls):
bpy.context.scene.BIMSystemProperties.is_editing = True
def enable_system_editing_ui(cls) -> None:
cls.get_system_props().is_editing = True
@classmethod
def export_system_attributes(cls):
return bonsai.bim.helper.export_attributes(bpy.context.scene.BIMSystemProperties.system_attributes)
def export_system_attributes(cls) -> dict[str, Any]:
return bonsai.bim.helper.export_attributes(cls.get_system_props().system_attributes)
@classmethod
def get_connected_port(cls, port):
def get_connected_port(cls, port: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
return ifcopenshell.util.system.get_connected_port(port)
@classmethod
def get_ports(cls, element):
def get_ports(cls, element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
return ifcopenshell.util.system.get_ports(element)
@classmethod
def get_port_relating_element(cls, port):
def get_port_relating_element(cls, port: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
if tool.Ifc.get_schema() == "IFC2X3":
element = port.ContainedIn[0].RelatedElement
else:
@@ -114,7 +134,7 @@ class System(bonsai.core.tool.System):
return element
@classmethod
def get_port_predefined_type(cls, mep_element):
def get_port_predefined_type(cls, mep_element: ifcopenshell.entity_instance) -> str:
split_camel_case = lambda x: re.findall("[A-Z][^A-Z]*", x)
mep_class = mep_element.is_a()
if mep_class.endswith("Type"):
@@ -125,20 +145,20 @@ class System(bonsai.core.tool.System):
return class_name
@classmethod
def import_system_attributes(cls, system):
props = bpy.context.scene.BIMSystemProperties
def import_system_attributes(cls, system: ifcopenshell.entity_instance) -> None:
props = cls.get_system_props()
props.system_attributes.clear()
bonsai.bim.helper.import_attributes2(system, props.system_attributes)
@classmethod
def get_systems(cls):
def get_systems(cls) -> list[ifcopenshell.entity_instance]:
if tool.Ifc.get_schema() == "IFC2X3":
return tool.Ifc.get().by_type("IfcSystem") + tool.Ifc.get().by_type("IfcZone")
return tool.Ifc.get().by_type("IfcSystem")
@classmethod
def import_systems(cls):
props = bpy.context.scene.BIMSystemProperties
def import_systems(cls) -> None:
props = cls.get_system_props()
props.systems.clear()
for system in cls.get_systems():
if system.is_a() in ["IfcStructuralAnalysisModel"]:
@@ -152,13 +172,20 @@ class System(bonsai.core.tool.System):
def load_ports(cls, element: ifcopenshell.entity_instance, ports: list[ifcopenshell.entity_instance]) -> None:
if not ports:
return
ports_to_create = ports.copy()
for port in ports:
if tool.Ifc.get_object(port):
ports_to_create.remove(port)
obj = tool.Ifc.get_object(element)
assert isinstance(obj, bpy.types.Object)
ifc_import_settings = import_ifc.IfcImportSettings.factory()
ifc_importer = import_ifc.IfcImporter(ifc_import_settings)
ifc_importer.file = tool.Ifc.get()
ifc_importer.calculate_unit_scale()
ifc_importer.process_context_filter()
ifc_importer.create_generic_elements(set(ports))
ifc_importer.create_generic_elements(set(ports_to_create))
container = ifcopenshell.util.element.get_container(element)
if container:
@@ -167,11 +194,12 @@ class System(bonsai.core.tool.System):
ifc_importer.place_objects_in_collections()
for port_obj in ifc_importer.added_data.values():
assert isinstance(port_obj, bpy.types.Object)
port_obj.parent = obj
port_obj.matrix_parent_inverse = obj.matrix_world.inverted()
@classmethod
def run_geometry_edit_object_placement(cls, obj=None):
def run_geometry_edit_object_placement(cls, obj: bpy.types.Object) -> None:
return bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
@classmethod
@@ -197,24 +225,24 @@ class System(bonsai.core.tool.System):
)
@classmethod
def select_system_products(cls, system):
def select_system_products(cls, system: ifcopenshell.entity_instance) -> None:
tool.Spatial.select_products(ifcopenshell.util.system.get_system_elements(system))
@classmethod
def set_active_edited_system(cls, system):
bpy.context.scene.BIMSystemProperties.edited_system_id = system.id()
def set_active_edited_system(cls, system: ifcopenshell.entity_instance) -> None:
cls.get_system_props().edited_system_id = system.id()
@classmethod
def set_active_system(cls, system):
bpy.context.scene.BIMSystemProperties.active_system_id = system.id()
def set_active_system(cls, system: ifcopenshell.entity_instance) -> None:
cls.get_system_props().active_system_id = system.id()
@classmethod
def get_active_system(cls):
system_props = bpy.context.scene.BIMSystemProperties
def get_active_system(cls) -> Union[ifcopenshell.entity_instance, None]:
system_props = cls.get_system_props()
return tool.Ifc.get_entity_by_id(system_props.active_system_id)
@classmethod
def get_decoration_data(cls):
def get_decoration_data(cls) -> dict[str, Any]:
all_vertices = []
preview_edges = []
special_vertices = []
@@ -359,7 +387,11 @@ class System(bonsai.core.tool.System):
return decoration_data
@classmethod
def get_connected_elements(cls, element, traversed_elements=None):
def get_connected_elements(
cls,
element: ifcopenshell.entity_instance,
traversed_elements: Optional[set[ifcopenshell.entity_instance]] = None,
) -> set[ifcopenshell.entity_instance]:
"""Recursively retrieves all connected elements to the given `element`.
`traversed_elements` is a set to store connected elements fetched recursively,
@@ -379,17 +411,19 @@ class System(bonsai.core.tool.System):
return traversed_elements
@classmethod
def is_mep_element(cls, element):
def is_mep_element(cls, element: ifcopenshell.entity_instance) -> bool:
return element.is_a("IfcFlowSegment") or element.is_a("IfcFlowFitting")
@classmethod
def get_flow_element_controls(cls, element):
def get_flow_element_controls(cls, element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
if not element.HasControlElements:
return []
return [control for control in element.HasControlElements[0].RelatedControlElements]
@classmethod
def get_flow_control_flow_element(cls, element):
def get_flow_control_flow_element(
cls, element: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
if not element.AssignedToFlowElement:
return
return element.AssignedToFlowElement[0].RelatingFlowElement
@@ -52,13 +52,13 @@ sections on the same page, like
.. code-block:: restructuredtext
:ref:`contribute/writing_docs:Writing technical documentation`
:ref:`guides/development/writing_docs:Writing technical documentation`
or with
.. code-block:: restructuredtext
:ref:`custom text<contribute/writing_docs:writing technical documentation>`.
:ref:`custom text<guides/development/writing_docs:writing technical documentation>`.
Traditional references like
@@ -70,13 +70,13 @@ work too but are discouraged. You can link to other pages, like this:
.. code-block:: restructuredtext
:doc:`Hello World<hello_world>`
:doc:`Hello World<guides/development/writing_docs>`
or sections within other pages, like this:
.. code-block:: restructuredtext
:ref:`devs/installation:unstable installation`
:ref:`guides/development/installation:unstable installation`
We have ``autosectionlabel`` enabled so it is not necessary to manually create labels. The depth of sections
with automatic labels is set to 2, so the third level of titles
Binary file not shown.
+32
View File
@@ -0,0 +1,32 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2021 Nathan Hild <nathan.hild@gmail.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/>.
"""
Disabling addon without issues is crucial for extension update to work.
Use operators instead of `blender --command extension remove`
to ensure disable and enable occur in the same Blender session.
"""
import bpy
import bonsai.tool as tool
bonsai_name = tool.Blender.get_blender_addon_package_name()
bpy.ops.preferences.addon_disable(module=bonsai_name)
bpy.ops.preferences.addon_enable(module=bonsai_name)
+55 -40
View File
@@ -8,8 +8,12 @@ Scenario: Duplicate drawing
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()"
And I press "bim.add_drawing"
And I save sample test files
And I look at the "Drawings" panel
And I click "IMPORT"
And I click "ADD"
And I press "bim.expand_target_view(target_view='PLAN_VIEW')"
When I select the "PLAN_VIEW" item in the "BIM_UL_drawinglist" list
And the variable "drawing" is "IfcStore.get_file().by_type('IfcAnnotation')[0].id()"
When I press "bim.duplicate_drawing(drawing={drawing})"
Then nothing happens
@@ -21,12 +25,14 @@ Scenario: Create drawing
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()"
And I press "bim.add_drawing"
And the variable "drawing" is "IfcStore.get_file().by_type('IfcAnnotation')[0].id()"
And I set "scene.DocProperties.active_drawing_index" to "0"
And I press "bim.activate_drawing(drawing={drawing})"
When I press "bim.create_drawing"
And I save sample test files
And I look at the "Drawings" panel
And I click "IMPORT"
And I click "ADD"
And I press "bim.expand_target_view(target_view='PLAN_VIEW')"
When I select the "PLAN_VIEW" item in the "BIM_UL_drawinglist" list
And I click "OUTLINER_OB_CAMERA"
And I click "OUTPUT"
Then nothing happens
Scenario: Create drawing after deleting a duplicated object
@@ -36,19 +42,18 @@ Scenario: Create drawing after deleting a duplicated object
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()"
And I press "bim.copy_class(obj='IfcWall/Cube')"
And the variable "wall2" is "IfcStore.get_file().by_type('IfcWall')[-1].id()"
And I press "bim.load_drawings"
# And I set "scene.DocProperties.location_hint" to "('My Storey', 'My Storey', '')"
And I press "bim.add_drawing"
And the variable "drawing" is "IfcStore.get_file().by_type('IfcAnnotation')[0].id()"
And I set "scene.DocProperties.active_drawing_index" to "0"
And I press "bim.activate_drawing(drawing={drawing})"
And I press "bim.create_drawing"
And I duplicate the selected objects
And I save sample test files
And I look at the "Drawings" panel
And I click "IMPORT"
And I click "ADD"
And I press "bim.expand_target_view(target_view='PLAN_VIEW')"
And I select the "PLAN_VIEW" item in the "BIM_UL_drawinglist" list
And I click "OUTLINER_OB_CAMERA"
And I click "OUTPUT"
And the object "IfcWall/Cube" is selected
And I press "object.delete(use_global=False)"
When I press "bim.create_drawing"
And I delete the selected objects
When I click "OUTPUT"
Then nothing happens
Scenario: Activate drawing preserves visibility for non-ifc objects
@@ -57,10 +62,13 @@ Scenario: Activate drawing preserves visibility for non-ifc objects
And I add a cube
And the object "Cube" is visible
And the object "Cube.001" is not visible
And I press "bim.add_drawing"
And the variable "drawing" is "IfcStore.get_file().by_type('IfcAnnotation')[0].id()"
And I set "scene.DocProperties.active_drawing_index" to "0"
When I press "bim.activate_drawing(drawing={drawing})"
And I save sample test files
And I look at the "Drawings" panel
And I click "IMPORT"
And I click "ADD"
And I press "bim.expand_target_view(target_view='PLAN_VIEW')"
And I select the "PLAN_VIEW" item in the "BIM_UL_drawinglist" list
And I click "OUTLINER_OB_CAMERA"
Then the object "Cube" is visible
And the object "Cube.001" is not visible
@@ -68,10 +76,13 @@ Scenario: Activate drawing preserves selection
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I press "bim.add_drawing"
And the variable "drawing" is "IfcStore.get_file().by_type('IfcAnnotation')[0].id()"
And I set "scene.DocProperties.active_drawing_index" to "0"
When I press "bim.activate_drawing(drawing={drawing})"
And I save sample test files
And I look at the "Drawings" panel
And I click "IMPORT"
And I click "ADD"
And I press "bim.expand_target_view(target_view='PLAN_VIEW')"
And I select the "PLAN_VIEW" item in the "BIM_UL_drawinglist" list
When I click "OUTLINER_OB_CAMERA"
Then the object "Cube" is selected
Scenario: Remove drawing
@@ -81,10 +92,14 @@ Scenario: Remove drawing
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()"
And I press "bim.add_drawing"
And the variable "drawing" is "IfcStore.get_file().by_type('IfcAnnotation')[0].id()"
And the collection "IfcAnnotation/PLAN_VIEW" exists
And I save sample test files
And I look at the "Drawings" panel
And I click "IMPORT"
And I click "ADD"
And I press "bim.expand_target_view(target_view='PLAN_VIEW')"
And I select the "PLAN_VIEW" item in the "BIM_UL_drawinglist" list
When I click "OUTLINER_OB_CAMERA"
Then the collection "IfcAnnotation/PLAN_VIEW" exists
When I press "bim.remove_drawing(drawing={drawing})"
Then the collection "IfcAnnotation/PLAN_VIEW" does not exist
@@ -95,7 +110,6 @@ Scenario: Remove drawing - via object deletion
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()"
And I press "bim.add_drawing"
And the variable "drawing" is "IfcStore.get_file().by_type('IfcAnnotation')[0].id()"
And the collection "IfcAnnotation/PLAN_VIEW" exists
@@ -110,12 +124,13 @@ Scenario: Remove drawing - deleting active drawing
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()"
And I press "bim.add_drawing"
And the variable "drawing" is "IfcStore.get_file().by_type('IfcAnnotation')[0].id()"
And the collection "IfcAnnotation/PLAN_VIEW" exists
And I set "scene.DocProperties.active_drawing_index" to "0"
And I press "bim.activate_drawing(drawing={drawing})"
And I save sample test files
And I look at the "Drawings" panel
And I click "IMPORT"
And I click "ADD"
And I press "bim.expand_target_view(target_view='PLAN_VIEW')"
And I select the "PLAN_VIEW" item in the "BIM_UL_drawinglist" list
And I click "OUTLINER_OB_CAMERA"
When the object "IfcAnnotation/PLAN_VIEW" is selected
And I press "bim.override_object_delete"
And I delete the selected objects
Then the collection "IfcAnnotation/PLAN_VIEW" does not exist
+8 -43
View File
@@ -55,7 +55,7 @@ Scenario: Add representation - add a representation with a scale factor applied
Then the object "IfcWall/Cube" has no scale
And the object "IfcWall/Cube" dimensions are "4,4,4"
Scenario: Add representation - add a representation with a scale factor removed due to multiple users
Scenario: Add representation - add a representation with a scale factor still applied regardless of multiple users
Given an empty IFC project
And I add a cube
And I press "object.duplicate_move_linked"
@@ -65,7 +65,7 @@ Scenario: Add representation - add a representation with a scale factor removed
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
Then the object "IfcWall/Cube" has no scale
And the object "IfcWall/Cube" dimensions are "2,2,2"
And the object "IfcWall/Cube" dimensions are "4,4,4"
Scenario: Switch representation
Given an empty IFC project
@@ -78,39 +78,6 @@ Scenario: Switch representation
And I press "bim.switch_representation(obj='IfcWall/Cube', ifc_definition_id={representation})"
Then nothing happens
Scenario: Switch representation - current edited representation is updated prior to switch
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the variable "context" is "[c for c in {ifc}.by_type('IfcGeometricRepresentationSubContext') if c.ContextType == 'Plan' and c.ContextIdentifier=='Annotation'][0].id()"
And I set "active_object.BIMGeometryProperties.contexts" to "{context}"
And I press "bim.add_representation"
When the object "IfcWall/Cube" is scaled to "2"
And the variable "representation" is "[r for r in {ifc}.by_type('IfcShapeRepresentation') if r.RepresentationType=='Tessellation'][0].id()"
And I press "bim.switch_representation(ifc_definition_id={representation}, should_reload=True)"
And the variable "representation" is "[r for r in {ifc}.by_type('IfcShapeRepresentation') if r.RepresentationType=='Annotation2D'][0].id()"
And I press "bim.switch_representation(ifc_definition_id={representation}, should_reload=True)"
When I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')"
Then the object "IfcWall/Cube" dimensions are "4,4,0"
Scenario: Switch representation - current edited representation is discarded if switching to a box
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
When the object "IfcWall/Cube" is scaled to "2"
And the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[-1].id()"
And I press "bim.switch_representation(obj='IfcWall/Cube', ifc_definition_id={representation}, should_reload=True)"
And the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()"
And I press "bim.switch_representation(obj='IfcWall/Cube', ifc_definition_id={representation}, should_reload=True)"
When I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')"
Then the object "IfcWall/Cube" dimensions are "2,2,2"
Scenario: Switch representation - existing Blender modifiers must be purged
Given an empty IFC project
And I add a cube
@@ -132,9 +99,7 @@ Scenario: Remove representation - remove an active representation
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
When the variable "representation_body" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()"
And the variable "representation_bbox" is "{ifc}.by_type('IfcShapeRepresentation')[1].id()"
And I press "bim.remove_representation(representation_id={representation_body})"
And I press "bim.remove_representation(representation_id={representation_bbox})"
Then the object "IfcWall/Cube" has no data
Scenario: Remove representation - remove an unloaded representation
@@ -145,6 +110,9 @@ Scenario: Remove representation - remove an unloaded representation
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
When the variable "context" is "[c for c in {ifc}.by_type('IfcGeometricRepresentationSubContext') if c.ContextType == 'Plan' and c.ContextIdentifier == 'Body' and c.TargetView == 'PLAN_VIEW'][0].id()"
And I set "active_object.BIMGeometryProperties.contexts" to "{context}"
And I press "bim.add_representation"
When the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[1].id()"
And I press "bim.remove_representation(representation_id={representation})"
Then the object "IfcWall/Cube" has data which is an IFC representation
@@ -162,10 +130,8 @@ Scenario: Remove representation - remove an instanced representation from an act
And I press "bim.add_constr_type_instance"
And I press "bim.add_constr_type_instance"
And the object "IfcWallType/Cube" is selected
When the variable "representation_body" is "{ifc}.by_type('IfcWallType')[0].RepresentationMaps[1].MappedRepresentation.id()"
And the variable "representation_bbox" is "{ifc}.by_type('IfcWallType')[0].RepresentationMaps[0].MappedRepresentation.id()"
When the variable "representation_body" is "{ifc}.by_type('IfcWallType')[0].RepresentationMaps[0].MappedRepresentation.id()"
And I press "bim.remove_representation(representation_id={representation_body})"
And I press "bim.remove_representation(representation_id={representation_bbox})"
Then the object "IfcWallType/Cube" has no data
Then the object "IfcWall/Wall" has no data
Then the object "IfcWall/Wall.001" has no data
@@ -183,10 +149,8 @@ Scenario: Remove representation - remove an instanced representation from an act
And I press "bim.add_constr_type_instance"
And I press "bim.add_constr_type_instance"
And the object "IfcWall/Wall" is selected
When the variable "representation_body" is "{ifc}.by_type('IfcWall')[0].Representation.Representations[1].id()"
And the variable "representation_bbox" is "{ifc}.by_type('IfcWall')[0].Representation.Representations[0].id()"
When the variable "representation_body" is "{ifc}.by_type('IfcWall')[0].Representation.Representations[0].id()"
And I press "bim.remove_representation(representation_id={representation_body})"
And I press "bim.remove_representation(representation_id={representation_bbox})"
Then the object "IfcWallType/Cube" has no data
Then the object "IfcWall/Wall" has no data
Then the object "IfcWall/Wall.001" has no data
@@ -320,6 +284,7 @@ Scenario: Override duplicate move - with active IFC data
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
And additionally the object "IfcBuildingStorey/My Storey" is selected
And I set "scene.BIMSpatialDecompositionProperties.is_locked" to "False"
When I duplicate the selected objects
Then the object "IfcWall/Cube" exists
And the object "IfcWall/Cube" is an "IfcWall"
+5 -2
View File
@@ -9,14 +9,16 @@ Scenario: Viewing default solar settings
Scenario: Changing the month
Given an empty IFC project
And I look at the "Solar Access / Shadow" panel
When I set the "January" property to "3"
When I set the "Year" property to "2024"
And I set the "January" property to "3"
Then I see "March"
And I see "Sunrise: 06:08:54"
Scenario: Changing the date
Given an empty IFC project
And I look at the "Solar Access / Shadow" panel
When I set the "Date" property to "3"
When I set the "Year" property to "2024"
And I set the "Date" property to "3"
Then I see "Sunrise: 06:00:31"
Scenario: Changing the time
@@ -29,6 +31,7 @@ Scenario: Changing the time
Scenario: Automatic timezone detection based on lat / long
Given an empty IFC project
And I look at the "Solar Access / Shadow" panel
And I set the "Year" property to "2024"
When I set the "Latitude" property to "10.0"
And I set the "Longitude" property to "20.0"
Then I see "Africa/Ndjamena"
+7 -6
View File
@@ -802,10 +802,10 @@ Scenario: Toggle link visibility - visible mode
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='VISIBLE')"
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_hidden" is "True"
And the collection "IfcProject/basic.ifc" exclude status is "True"
And the object "IfcProject/basic.ifc" is not visible
When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='VISIBLE')"
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_hidden" is "False"
And the collection "IfcProject/basic.ifc" exclude status is "False"
And the object "IfcProject/basic.ifc" is visible
Scenario: Unload link
Given an empty Blender session
@@ -820,7 +820,7 @@ Scenario: Load link
And I press "bim.unload_link(filepath='{cwd}/test/files/basic.ifc')"
When I press "bim.load_link(filepath='{cwd}/test/files/basic.ifc')"
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "True"
And the collection "IfcProject/basic.ifc" exists in viewlayer
And the object "IfcProject/basic.ifc" exists
Scenario: Unlink IFC
Given an empty Blender session
@@ -867,12 +867,13 @@ Scenario: Export IFC - with basic contents and saving as a relative path
Given an empty Blender session
And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')"
When I press "wm.save_mainfile(filepath='{cwd}/test/files/temp/export.blend')"
And I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc', use_relative_path=True, save_as_invoked=True)"
And I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc', use_relative_path=True, should_save_as=True)"
Then "scene.BIMProperties.ifc_file" is "export.ifc"
Scenario: Export IFC - with deleted objects synchronised
Given an empty IFC project
When the object "IfcBuildingStorey/My Storey" is selected
And I set "scene.BIMSpatialDecompositionProperties.is_locked" to "False"
And I delete the selected objects
And I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')"
And an empty Blender session is started
@@ -896,7 +897,7 @@ Scenario: Export IFC - with moved grid axis location synchronised
And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')"
Then the object "IfcGridAxis/01" bottom left corner is at "1,-2,0"
Scenario: Export IFC - with changed object scale synchronised
Scenario: Export IFC - with changed object scale ignored
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
@@ -908,4 +909,4 @@ Scenario: Export IFC - with changed object scale synchronised
And I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')"
And an empty Blender session is started
And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')"
Then the object "IfcWall/Cube" dimensions are "4,4,4"
Then the object "IfcWall/Cube" dimensions are "2,2,2"
+10 -7
View File
@@ -70,7 +70,7 @@ class PanelSpy:
return self
def __call__(self, *args, **kwargs):
if self.spied_attr in ("row", "column", "box"):
if self.spied_attr in ("row", "column", "box", "separator"):
return self
elif self.spied_attr == "template_list":
listtype_name, list_id, dataptr, propname, active_dataptr, active_propname = args
@@ -274,8 +274,8 @@ def i_see_the_prop_property(prop):
@then(parsers.parse('I don\'t see the "{prop}" property'))
def i_dont_see_the_prop_property(prop):
panel_spy.refresh_spy()
assert [
p for p in panel_spy.spied_props if prop not in (p["name"], p["text"], p["icon"])
assert not [
p for p in panel_spy.spied_props if prop in (p["name"], p["text"], p["icon"])
], f"Property {prop} not found in {panel_spy.spied_props}"
@@ -334,6 +334,7 @@ def the_name_list_has_total_items(name, total):
assert False, f"List {name} not found in {panel_spy.spied_lists}"
@given(parsers.parse('I select the "{item_name}" item in the "{list_name}" list'))
@when(parsers.parse('I select the "{item_name}" item in the "{list_name}" list'))
def i_select_the_item_name_item_in_the_list_name_list(item_name, list_name):
panel_spy.refresh_spy()
@@ -352,7 +353,8 @@ def i_select_the_item_name_item_in_the_list_name_list(item_name, list_name):
@when("I load a new pset template file")
def i_load_a_new_pset_template_file():
IfcStore.pset_template_path = bpy.context.scene.BIMPsetTemplateProperties.pset_template_files
props = tool.PsetTemplate.get_pset_template_props()
IfcStore.pset_template_path = props.pset_template_files
IfcStore.pset_template_file = ifcopenshell.open(IfcStore.pset_template_path)
@@ -510,6 +512,7 @@ def i_evaluate_expression(expression):
exec(expression)
@given("I duplicate the selected objects")
@when("I duplicate the selected objects")
def i_duplicate_the_selected_objects():
bpy.ops.bim.override_object_duplicate_move()
@@ -634,6 +637,7 @@ def i_am_on_frame_number(number):
bpy.context.scene.frame_set(int(number))
@given("I delete the selected objects")
@when("I delete the selected objects")
def i_delete_the_selected_objects():
bpy.ops.bim.override_object_delete()
@@ -730,7 +734,7 @@ def the_object_name_has_a_representation_type_of_context(name, type, context):
context, subcontext, target_view = context.split("/")
rep = ifcopenshell.util.representation.get_representation(element, context, subcontext or None, target_view or None)
assert rep
assert rep.RepresentationType == type
assert rep.RepresentationType == type, f"The object {name} does not have a {type} representation"
@given(parsers.parse('the object "{name}" data is a "{type}" representation of "{context}"'))
@@ -740,7 +744,7 @@ def the_object_name_data_is_a_type_representation_of_context(name, type, context
context, subcontext, target_view = context.split("/")
rep = ifc.by_id(the_object_name_exists(name).data.BIMMeshProperties.ifc_definition_id)
assert rep
assert rep.RepresentationType == type
assert rep.RepresentationType == type, f"The object {name} is not a {type} representation"
assert rep.ContextOfItems.ContextType == context
assert rep.ContextOfItems.ContextIdentifier == subcontext
assert rep.ContextOfItems.TargetView == target_view
@@ -1348,7 +1352,6 @@ def run_test_code():
@then(parsers.parse("I save sample test files"))
def saving_sample_test_files(and_open_in_blender=None):
filepath = f"{variables['cwd']}/test/files/temp/sample_test_file"
blend_filepath = f"{filepath}.blend"
bpy.ops.bim.save_project(filepath=f"{filepath}.ifc", should_save_as=True)
bpy.ops.wm.save_as_mainfile(filepath=f"{filepath}.blend")
+4 -7
View File
@@ -257,13 +257,10 @@ class Prophecy:
call = {"name": attr, "args": args, "kwargs": kwargs}
# Ensure that signature is valid
getattr(self.subject, attr)(*args, **kwargs)
try:
key = json.dumps(call, sort_keys=True)
self.calls.append(call)
if key in self.return_values:
return self.return_values[key]
except:
pass
key = json.dumps(call, sort_keys=True)
self.calls.append(call)
if key in self.return_values:
return self.return_values[key]
return self
return decorate
+24 -5
View File
@@ -17,15 +17,34 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bonsai.core.attribute as subject
from test.core.bootstrap import ifc
from test.core.bootstrap import ifc, blender, root, spatial
class TestCopyAttributeToSelection:
def test_run(self, ifc):
def test_run(self, ifc, blender, root, spatial):
blender.get_selected_objects(include_active=False).should_be_called().will_return(["obj"])
ifc.get_entity("obj").should_be_called().will_return("element")
ifc.run("attribute.edit_attributes", product="element", attributes={"name": "value"}).should_be_called()
subject.copy_attribute_to_selection(ifc, name="name", value="value", obj="obj")
assert subject.copy_attribute_to_selection(ifc, blender, root, spatial, name="name", value="value") == 1
def test_do_nothing_if_object_is_not_an_element(self, ifc):
def test_do_nothing_if_object_is_not_an_element(self, ifc, blender, root, spatial):
blender.get_selected_objects(include_active=False).should_be_called().will_return(["obj"])
ifc.get_entity("obj").should_be_called().will_return(None)
subject.copy_attribute_to_selection(ifc, name="name", value="value", obj="obj")
assert subject.copy_attribute_to_selection(ifc, blender, root, spatial, name="name", value="value") == 0
def test_changing_object_name_in_blender_if_attribute_changed(self, ifc, blender, root, spatial):
blender.get_selected_objects(include_active=False).should_be_called().will_return(["obj"])
ifc.get_entity("obj").should_be_called().will_return("element")
ifc.run("attribute.edit_attributes", product="element", attributes={"Name": "value"}).should_be_called()
root.set_object_name("obj", "element").should_be_called()
root.is_spatial_element("element").should_be_called().will_return(False)
assert subject.copy_attribute_to_selection(ifc, blender, root, spatial, name="Name", value="value") == 1
def test_refreshing_spatial_decomposition_if_identification_changed(self, ifc, blender, root, spatial):
blender.get_selected_objects(include_active=False).should_be_called().will_return(["obj"])
ifc.get_entity("obj").should_be_called().will_return("element")
ifc.run("attribute.edit_attributes", product="element", attributes={"Name": "value"}).should_be_called()
root.set_object_name("obj", "element").should_be_called()
root.is_spatial_element("element").should_be_called().will_return(True)
spatial.import_spatial_decomposition().should_be_called()
assert subject.copy_attribute_to_selection(ifc, blender, root, spatial, name="Name", value="value") == 1
+33 -9
View File
@@ -34,9 +34,10 @@ class TestDisableEditingText:
class TestEditText:
def test_run(self, ifc, drawing):
def test_run(self, drawing):
drawing.synchronise_ifc_and_text_attributes("obj").should_be_called()
drawing.update_text_size_pset("obj").should_be_called()
drawing.update_newline_at("obj").should_be_called()
drawing.update_text_value("obj").should_be_called()
drawing.disable_editing_text("obj").should_be_called()
subject.edit_text(drawing, obj="obj")
@@ -161,11 +162,11 @@ class TestAddSheet:
subject.add_sheet(ifc, drawing, titleblock="titleblock")
class TestOpenSheet:
class TestOpenLayout:
def test_run(self, drawing):
drawing.get_document_uri("sheet", "LAYOUT").should_be_called().will_return("uri")
drawing.open_layout_svg("uri").should_be_called()
subject.open_sheet(drawing, sheet="sheet")
subject.open_layout(drawing, sheet="sheet")
class TestRemoveSheet:
@@ -532,19 +533,30 @@ class TestAddAnnotation:
drawing.create_annotation_object("drawing", "object_type").should_be_called().will_return("obj")
ifc.get_entity("obj").should_be_called().will_return(None)
drawing.get_ifc_representation_class("object_type").should_be_called().will_return("ifc_representation_class")
drawing.get_annotation_representation("element_type").should_be_called().will_return("type_rep")
drawing.run_root_assign_class(
obj="obj",
ifc_class="IfcAnnotation",
predefined_type="object_type",
should_add_representation=True,
should_add_representation=False,
context="context",
ifc_representation_class="ifc_representation_class",
).should_be_called().will_return("element")
drawing.get_drawing_group("drawing").should_be_called().will_return("group")
drawing.run_type_assign_type(element="element", relating_type="element_type").should_be_called()
ifc.run("group.assign_group", group="group", products=["element"]).should_be_called()
collector.assign("obj").should_be_called()
drawing.enable_editing("obj").should_be_called()
subject.add_annotation(ifc, collector, drawing, drawing="drawing", object_type="object_type")
drawing.get_representation("element", "context").should_be_called().will_return("rep")
drawing.reload_representation(obj="obj", representation="rep").should_be_called()
collector.assign("obj", should_clean_users_collection=True).should_be_called()
subject.add_annotation(
ifc,
collector,
drawing,
drawing="drawing",
object_type="object_type",
relating_type="element_type",
enable_editing=False,
)
def test_create_a_missing_annotation_context_on_the_fly(self, ifc, collector, drawing):
drawing.get_drawing_target_view("drawing").should_be_called().will_return("target_view")
@@ -554,6 +566,7 @@ class TestAddAnnotation:
drawing.create_annotation_object("drawing", "object_type").should_be_called().will_return("obj")
ifc.get_entity("obj").should_be_called().will_return(None)
drawing.get_ifc_representation_class("object_type").should_be_called().will_return("ifc_representation_class")
drawing.get_annotation_representation("element_type").should_be_called().will_return(None)
drawing.run_root_assign_class(
obj="obj",
ifc_class="IfcAnnotation",
@@ -562,8 +575,19 @@ class TestAddAnnotation:
context="context",
ifc_representation_class="ifc_representation_class",
).should_be_called().will_return("element")
drawing.run_type_assign_type(element="element", relating_type="element_type").should_be_called()
drawing.get_drawing_group("drawing").should_be_called().will_return("group")
ifc.run("group.assign_group", group="group", products=["element"]).should_be_called()
collector.assign("obj").should_be_called()
drawing.get_representation("element", "context").should_be_called().will_return("rep")
drawing.reload_representation(obj="obj", representation="rep").should_be_called()
collector.assign("obj", should_clean_users_collection=True).should_be_called()
drawing.enable_editing("obj").should_be_called()
subject.add_annotation(ifc, collector, drawing, drawing="drawing", object_type="object_type")
subject.add_annotation(
ifc,
collector,
drawing,
drawing="drawing",
object_type="object_type",
relating_type="element_type",
enable_editing=True,
)

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