Compare commits

..

105 Commits

Author SHA1 Message Date
Andrej730 e8f18b5b6d ci-bonsai-daily - include submodules for a test 2025-05-21 13:00:46 +05:00
Andrej730 718254fe5e Fix regression parsing iso duration with floats (d1b1b08) #6715 2025-05-21 12:16:07 +05:00
Andrej730 41227b89c8 Fix subtle bug with operators precedence in eabdb5d
`+` has a higher precedence than `or`, so it resulted in the error below

```
Traceback (most recent call last):
  File "\bonsai\bim\module\spatial\prop.py", line 86, in update_active_container_index
    tool.Spatial.load_contained_elements()
  File "\bonsai\tool\spatial.py", line 312, in load_contained_elements
    cls.load_contained_elements_by_type(container)
  File "\bonsai\tool\spatial.py", line 321, in load_contained_elements_by_type
    results = cls.get_container_elements_grouped_by_type(container)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\bonsai\tool\spatial.py", line 294, in get_container_elements_grouped_by_type
    element_type.is_a() + "/" + element_type.Name or "Unnamed"
    ~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~
TypeError: can only concatenate str (not "NoneType") to str
```
2025-05-20 19:06:25 +05:00
Andrej730 2547ee9ab5 bonsai.crash.txt
There seems to be cases when Blender might crash too unexpectedly.
E.g. #6686 - during viewport render system is running out of resources and crashing without leaving blender.crash.txt, leaving everyone clueless. Which is arguably a bug in Blender (both crash and lack of any crash report), but at least we'll have a fallback for this case.

What bonsai.crash.txt does - it's created in system temp folder with the current Python traceback, just before some dangerous operation. If operation didn't crashed Blender, then file will be unlinked. If crash did occurred, it might be the only clue for users and devs on what actually happened.
2025-05-20 19:06:25 +05:00
Andrej730 d22f1bcd71 'viewport' render type - add a note 2025-05-20 19:06:24 +05:00
Andrej730 0c2158089d disable update_has_underlay if update_props is disabled
Otherwise it was trying to load drawing styles too early, when drawing was not yet active. And then they were reloaded again during ActivateDrawingBase. Occurred after c2860e6, before this commit issue was kind of hidden.
2025-05-20 19:06:02 +05:00
Thomas Krijnen ccafc203b5 submodule 2025-05-20 10:05:21 +02:00
Andrej730 0efc2d25f1 Fix for assign_class and object with modifiers in Blender <4.4 #6701 2025-05-20 12:24:35 +05:00
Andrej730 ac50f002f0 Fix confused blender and pset props in 9586869 2025-05-19 19:28:55 +05:00
Andrej730 66f4c957ea black . 2025-05-19 19:28:55 +05:00
Dion Moult 67016d6f90 See #2999. Reimplement basic creation of structural items. 2025-05-19 17:36:12 +10:00
Thomas Krijnen 439f9c14ce OcctNoCleanTriangulation CacheShapes PermissiveShapeReuse setting; Cgal conditional copy during triangulation; occt cheaper check for triangulation existance #6712 2025-05-18 22:14:22 +02:00
Dion Moult 404bf13a18 Update colour by property test to reflect new behaviour of only showing geometry not empties
ALL (feature) TESTS PASS YESS
2025-05-18 19:23:34 +10:00
Dion Moult 3a37bcdcaa Update test cases to reflect new drawing activation UI 2025-05-18 19:09:29 +10:00
Dion Moult 992ad8e24a See #4699. Continue refactoring logic from drawing decorator into DecoratorData. 2025-05-18 15:32:55 +10:00
Dion Moult 1de2515fcd More bSDD tests to cover all current functionality 2025-05-18 12:05:33 +10:00
Thomas Krijnen 4bc88eac2f ifdefs around geometry handling for 4.3 rc schemas #6710 2025-05-17 16:01:28 +02:00
Dion Moult 22f10e5549 Fix #6669. bSDD now supports test dictionaries, refactor domain to dictionary, support zero length keyword searches, multi-dict support, and basic tests.
Feature tests can now have more sophisticated per UIList item spies.
2025-05-17 21:28:12 +10:00
Andrej730 8e34c1d7df Fix error using bim.updating_representation for object with material set
Possibly occurred after 4c255cc, but haven't investigated deeper.

@BIMvoice
The error you were reporting about Split Along Edge operator was caused by this issue, should be fixed now too.
See https://jmp.sh/hgJEHsgH

Example error traceback:
```
Error: Python: Traceback (most recent call last):
  File "C\bonsai\bim\ifc.py", line 487, in execute_ifc_operator
    result = getattr(operator, "_execute")(context)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C\bonsai\bim\module\geometry\operator.py", line 541, in _execute
    self.update_obj_mesh_representation(context, obj)
  File "C\bonsai\bim\module\geometry\operator.py", line 666, in update_obj_mesh_representation
    tool.Ifc.link(new_representation, data)
  File "C\bonsai\tool\ifc.py", line 230, in link
    IfcStore.link_element(element, obj)
  File "C\bonsai\bim\ifc.py", line 258, in link_element
    tool.Geometry.get_mesh_props(obj).ifc_definition_id = element.id()
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C\bonsai\tool\geometry.py", line 92, in get_mesh_props
    return mesh.BIMMeshProperties
           ^^^^^^^^^^^^^^^^^^^^^^
ReferenceError: StructRNA of type Mesh has been removed
```
2025-05-16 19:43:42 +05:00
Andrej730 4369baa226 bim.split_along_edge - improve descriptions, add error handling 2025-05-16 19:43:42 +05:00
Andrej730 7941d71feb SvgWriter - make required arguments more explicit 2025-05-16 19:43:42 +05:00
Andrej730 68d967c2e5 pyproject.toml - enableTypeIgnoreComments=false 2025-05-16 19:43:41 +05:00
Andrej730 4ea9a598c7 operator.drawing - remove unnecessary ifc operators 2025-05-16 19:43:41 +05:00
Andrej730 06d1c81954 black . 2025-05-16 19:43:41 +05:00
Andrej730 aa5f08880b typing 2025-05-16 19:43:41 +05:00
Ryan Schultz 9e6320d545 A preference to list those classes that are not affected by the 'cut decorator'. 2025-05-15 21:45:53 -05:00
Bruno Perdigão f39d3d4b9c Fix polyline plane method decorator to show properly.
The `plane_method` where not being cleared after canceling or confirming
the polyline operator.
2025-05-15 21:28:31 -03:00
Bruno Perdigão 5c72cf35bd See #6208. Change wall merge tolerance for more consistent behavior. 2025-05-15 19:08:04 -03:00
Bruno Perdigão b9a435e1c3 Fix polyline tool calculation when dealing with 180 degrees angles. 2025-05-15 17:41:32 -03:00
Bruno Perdigão 92f2559ce0 See #6536. Recalculate profile only for selected objects. 2025-05-15 17:41:32 -03:00
Bruno Perdigão acf9713943 Fix aggregate decorator after #6537. 2025-05-15 17:41:32 -03:00
Andrej730 a77125ff12 bim.add_drawing - document camera placement behaviour in description
See https://i.imgur.com/VfOjGHk.png
2025-05-15 20:01:02 +05:00
Andrej730 c8841bcb6a Add a test for be65774 2025-05-15 20:01:01 +05:00
Andrej730 b20997dea8 Fix tests missing assert statement - add assert and fix them 2025-05-15 19:45:11 +05:00
Andrej730 292aca6685 Remove unnecessary IFC operators 2025-05-15 19:45:11 +05:00
Andrej730 3a824ec969 Allow adding arrays to openings and spatial elements 0b8fb41 0e37528 2025-05-15 19:45:11 +05:00
Andrej730 bea9f42f87 Annotator.add_text - remove ancient dead code
It was deprecated a while ago in 43869c3, when we stopped using curves for text annotations. bim.resize_text was also using curve annotations and now also was unused
2025-05-15 19:45:11 +05:00
Andrej730 e4a50b32ce typing 2025-05-15 19:40:44 +05:00
Andrej730 76611024b6 Remove drawing styles attributes prop (a50c633cf) 2025-05-15 10:30:35 +05:00
Andrej730 92ee82374c EPset_Drawing.Metadata - add description
See https://i.imgur.com/0fR9fRj.png
2025-05-14 19:40:46 +05:00
Andrej730 af81aa1fae Reapply 06f1477 to fix not appearing IFC Description button
Not that skipping non-string properties was a bad idea, but inserting it right at the beginning was - non-string IFC properties were missing IFC descriptions because of it. Moved it past `Attribute` check to avoid this issue.

For anyone hearing about this feature for the first time - https://imgur.com/a/dNRao3R

I assume this was also the issue that was encountered in #5892
2025-05-14 19:40:45 +05:00
Andrej730 16f9f91561 util.doc - statically type json structures 2025-05-14 19:40:45 +05:00
Andrej730 3a52a848ae draw_custom_context_menu - small refactor to improve readibility 2025-05-14 19:40:45 +05:00
Andrej730 a50c633cf4 Remove drawing styles attributes
It's been awhile since they were unused (2c301d7) and now they're replaced with EPset_Drawing.Metadata
2025-05-14 19:40:45 +05:00
Andrej730 6251750133 common method for getting active drawing style 2025-05-14 19:40:45 +05:00
Andrej730 acef6379e8 drawing styles - add descriptions to render types, remove NONE type
NONE type removed as it was unused and basically meant the same as 'VIEWPORT'.

Example - https://i.imgur.com/7r6CJdg.png
2025-05-14 19:40:45 +05:00
Andrej730 c2860e6e25 drawing.operator not to use DrawingsData 2025-05-14 19:40:44 +05:00
Andrej730 b8e5cd740a Use common method for getting active drawing entity 2025-05-14 19:40:44 +05:00
Andrej730 9188dc60ab typing 2025-05-14 19:40:44 +05:00
Thomas Krijnen 8ce56db0c3 Align preprocessor flags #6706 2025-05-14 13:09:17 +02:00
Andrej730 b91b1401f4 util.get_aggregate - make it more readable (5597095) 2025-05-13 17:44:42 +05:00
Andrej730 0e37528915 Skip IfcOpeningElements arrays during base import, setup constraint on opening load #6688 2025-05-13 17:23:46 +05:00
Andrej730 9991c4aa56 IfcImporter.create_generic_elements - prevent argument mutation
as it may have unexpected consequences
2025-05-13 17:23:46 +05:00
Andrej730 0ef3983035 bim.show_opening - simplify code 2025-05-13 17:23:46 +05:00
Andrej730 0f6e6a48f4 bim.show_openings - expand description 2025-05-13 17:23:45 +05:00
Andrej730 3d225beafd Add notes on Tekla accommodations (7c3870595) 2025-05-13 16:59:12 +05:00
Andrej730 6a91bbb720 typing 2025-05-13 16:59:12 +05:00
Hannes 1f23e4ba18 Fixed typo in __init__.py
See https://github.com/IfcOpenShell/IfcOpenShell/issues/6625
2025-05-13 13:18:24 +02:00
Andrej730 2ebcffc7d4 Geometry changes will be lost - show warning only for IFC objects
Previously it would also occur for vanilla Blender objects not connected to IFC.
2025-05-13 13:26:54 +05:00
Andrej730 3d4321f713 Prevent class assignment in non-object mode #6697
Example - https://i.imgur.com/KQYQLQZ.png
2025-05-13 13:26:54 +05:00
Andrej730 f8799305e1 black . 2025-05-13 13:26:54 +05:00
Dion Moult 7c38705958 Accommodate invalid missing mapped representations and items coming from Tekla (2023) 2025-05-13 13:09:37 +10:00
Bruno Perdigão f6fc0cfa0f See #6537. Fix object display type when exiting aggregate mode. 2025-05-12 22:01:40 -03:00
Richard Brice 686e9c946b Adds object placement to stationing referent. 2025-05-12 16:14:53 -07:00
sboddy 3085072751 Improve drawing activation as per #6653 (#6659)
Drawings are now activated in a bit different way - not by the camera button located above the drawings list, but by using the camera indicator next to each drawing. Which allows do change active drawing very quickly. All activation options (shift+, alt+) are also available from this indicator.

Now - https://imgur.com/a/DmnWkul

Before - https://imgur.com/a/zjPDkPa
2025-05-12 19:46:34 +05:00
Andrej730 0328be4093 Drawing styles - remove deprecated properties / property values
Deprecated properties:
- scene.render.engine + BLENDER_EEVEE
- scene.render.simplify_shadows
- scene.render.simplify_shadows_render
- space.overlay.backwire_opacity
- space.overlay.show_edges
- space.overlay.show_occlude_wire
- scene.display.shading.wireframe_color_type + MATERIAL
2025-05-12 19:10:04 +05:00
Andrej730 6218d3902e bim.activate_drawing_style - show warnings if some property failed to be set
Otherwise errors were passing silently (just console errors) and remained unfixed. 'PYTEST_VERSION' is needed to be even more strict when we're running pytests, which allows us to catch outdated Blender properties in time.

Example - https://i.imgur.com/K7Trkxf.png
2025-05-12 19:10:04 +05:00
Andrej730 be1836fa48 Mark thumbnails as outdated instead of updating them right away
As user may never open them in type manager
2025-05-12 19:08:46 +05:00
Andrej730 22b5db8a1e ci-black-formatting - print diff 2025-05-12 19:08:46 +05:00
Andrej730 4268e1376c Address PR review changes #6660 2025-05-12 19:08:46 +05:00
Andrej730 e2c1a54e0a typing 2025-05-12 19:08:46 +05:00
falken10vdl d1be660469 Update type manager preview on updating paramteric door/window type (#6656) 2025-05-12 12:41:02 +05:00
Kristoffer Andersen 3ae5227b57 Update ci-ifcopenshell-conda-daily.yml
remove env var
2025-05-11 19:59:05 +02:00
Kristoffer Andersen cdc994ca76 Update ci-ifcopenshell-conda-daily.yml
move rattler build output dir outside of build dir
2025-05-11 19:58:27 +02:00
Kristoffer Andersen 0ad4087f0a Update ci-ifcopenshell-conda-daily.yml
remove dotnet to save space on ubuntu runner
2025-05-11 19:28:12 +02:00
Kristoffer Andersen 4b7ca648c6 Update ci-ifcopenshell-conda-daily.yml
try to get even more space on ubuntu ci runner
2025-05-11 19:23:28 +02:00
Kristoffer Andersen 17bd6e6b0c Update ci-ifcopenshell-conda-daily.yml
fix missing comma
2025-05-11 19:12:34 +02:00
Kristoffer Andersen 7b80803cbe separate osx-x86 and osx-arm64 for the conda daily builds (#6694)
* separate osx-x86 and osx-arm64 for the conda daily builds

* remove running this in PR. No need.
2025-05-11 18:59:52 +02:00
Kristoffer Andersen eaf67d83f9 Update ci-ifcopenshell-conda-daily.yml
strip daily conda build version date string
2025-05-11 16:04:21 +02:00
Kristoffer Andersen 41f754e15b Update ci-ifcopenshell-conda-daily.yml
Update conda ci
2025-05-11 15:53:29 +02:00
Kristoffer Andersen b7cdb86327 use rattler-build to speed up daily conda builds and fix deps (#6692)
* use rattler-build to speed up daily conda builds and add typing_extensions

* minor update
2025-05-11 09:37:02 +02:00
falken10vdl 809a20dc99 system info copied when clicking in Bonsai Version (#6660) 2025-05-09 16:09:37 -05:00
Andrej730 f5e41d18c6 drawing.operator - reuse blender.get_view3d_space 2025-05-09 19:26:28 +05:00
Andrej730 60a8c45ad7 Fix IFC operators issues for drawing styles operations (38cf88d) 2025-05-09 19:26:28 +05:00
Andrej730 73f98bfe4b Remove 2 years old fallback for missing ShadingStyles prop in drawing's pset (38cf88d) 2025-05-09 19:26:28 +05:00
Andrej730 958686958b drawing.prop - reuse generic pset update callback 2025-05-09 19:26:28 +05:00
Andrej730 e0b61c5794 Fix drawing styles update on underlay toggle after 9329390 2025-05-09 19:26:28 +05:00
Andrej730 5aa5552759 Fix drawing test after be657748f 2025-05-09 19:26:27 +05:00
Andrej730 82a19cee7b generate_drawing_matrix - simplify logic 2025-05-09 19:26:27 +05:00
Andrej730 43d4adbd9d Fix tests after specifying types more precisely in def9352ad 2025-05-09 19:26:27 +05:00
Andrej730 1d3c71985f typing 2025-05-09 19:26:27 +05:00
Andrej730 381fca0812 black . 2025-05-09 19:26:27 +05:00
Dion Moult 0f37eca333 Fix bug in IFC4X3 when convering properties to quantities 2025-05-09 15:24:30 +10:00
Dion Moult 6bf9b8b0c9 Fix bug where appending assets could accidentally create duplicate contexts 2025-05-09 15:24:17 +10:00
Bruno Perdigão 9361cfee28 See #6536. Update cardinal point when editing IfcMaterialProfileSetUsage. 2025-05-08 22:06:48 -03:00
Bruno Perdigão 1c205d494a See #6332. Fix issue where imperial precision were being used by metric formatting. 2025-05-08 19:15:51 -03:00
Ryan Schultz 4d1d224a34 fix #6683 - When placing Level (Plan) annotation, have it take on the 'z' dimension of the 3d cursor, verses the front plane of the drawing camera 2025-05-08 17:04:34 -05:00
Andrej730 4976af5e66 drawing.create_camera - remove setting location line that had no effect
Apparently it had no effect from the beginning in 074fe0d and lost any use in 6a8292d, when we started to base position on current cursor's location.
2025-05-08 19:23:24 +05:00
Andrej730 be657748f3 Align new Model drawings to viewport by default
Previously it was creating not very useful top-down camera at `(0,0,0)`, now it should be more intuitive.

Example - https://imgur.com/a/GHrvCSJ
2025-05-08 19:23:24 +05:00
Andrej730 def9352ade typing 2025-05-08 19:23:23 +05:00
Josef Wienerroither a20002ecca Update links and version info ifcmax.rst 2025-05-08 14:42:50 +02:00
Andrej730 b7a0f230ad Fix confused target_view callbacks (1619ae854)
Fix using the same function name twice leading to DocProperties using camera callback too.
2025-05-08 15:50:00 +05:00
Andrej730 76529a06bb Fix resetting perspective camera on drawing activation (1619ae854) 2025-05-08 15:41:34 +05:00
Andrej730 dcdae67a2d Fix typo in 1619ae854 2025-05-08 15:41:34 +05:00
Andrej730 5a7a368169 ifcopenshell.file.Transaction - add static typing 2025-05-08 15:41:34 +05:00
136 changed files with 3338 additions and 1497 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
- name: Black formatter
id: linting
run: |
python3 -m black --check .
python3 -m black --diff --check .
continue-on-error: true
- name: Final check
+2
View File
@@ -55,6 +55,8 @@ jobs:
}
steps:
- uses: actions/checkout@v2
with:
submodules: recursive
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
@@ -35,11 +35,11 @@ jobs:
- name: Version + date str
id: verdate
run: echo "verdate='${{ steps.version.outputs.version }}alpha${{ steps.date.outputs.date }}'" >> $GITHUB_OUTPUT
run: echo "verdate=${{ steps.version.outputs.version }}alpha${{ steps.date.outputs.date }}" >> $GITHUB_OUTPUT
test:
name: ${{ matrix.platform.distver }}-${{ matrix.pyver.name }}-${{ matrix.variant }}
name: ${{ matrix.platform.distver }}-${{ matrix.pyver.name }}
needs: activate
runs-on: ${{ matrix.platform.distver }}
defaults:
@@ -48,57 +48,67 @@ jobs:
strategy:
fail-fast: false
matrix:
variant: [novtk, all]
pyver: [
# { name: py311, distver: '3.11'},
{ name: py312, distver: '3.12'}
]
platform: [
{ name: win, distver: windows-latest, pkg_dir: 'win-64' },
{ name: linux, distver: ubuntu-latest, pkg_dir: 'linux-64' },
{ name: macOS, distver: macos-latest, pkg_dir: 'osx-64' }
{ name: macOS-arm, distver: macos-latest, pkg_dir: 'osx-arm64' },
{ name: macOS-x86, distver: macos-13, pkg_dir: 'osx-64' }
]
steps:
- name: Set up swap space
- name: Set Swap Space
if: runner.os == 'Linux'
uses: pierotofy/set-swap-space@v1.0
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- name: set ARTIFACTS ENV vars
shell: bash
run: |
pwd
if [[ "$RUNNER_OS" == "Windows" ]]; then
echo "ARTIFACTS_DIR=D:/a/artifacts" >> $GITHUB_ENV
elif [[ "$RUNNER_OS" == "macOS" ]]; then
echo "ARTIFACTS_DIR=/Users/runner/work/artifacts" >> $GITHUB_ENV
elif [[ "$RUNNER_OS" == "Linux" ]]; then
echo "ARTIFACTS_DIR=/home/runner/work/artifacts" >> $GITHUB_ENV
fi
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Download and extract MacOSX SDK
if: ${{ runner.os == 'macOS' }}
if: ${{ matrix.platform.name == 'macOS-x86' }}
run: |
curl -L https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX10.13.sdk.tar.xz | tar -xvJf - -C /Users/runner/work/
- uses: mamba-org/setup-micromamba@v1 # https://github.com/mamba-org/setup-micromamba
- uses: mamba-org/setup-micromamba@v2 # https://github.com/mamba-org/setup-micromamba
with:
environment-name: test-env
create-args: >-
python=3.11
python=3.12
anaconda-client
boa
rattler-build
- name: create conda package dist dir
run: |
mkdir -p ${{ github.workspace }}/dist
mkdir -p ${{ env.ARTIFACTS_DIR }}
- name: build & test ifcopenshell
run: |
conda mambabuild . --python ${{ matrix.pyver.distver }} --no-remove-work-dir --output-folder '${{ github.workspace }}/dist' --variants='{variant: ${{ matrix.variant }}}'
working-directory: ./conda
rattler-build build -r conda/recipe.yaml --output-dir '${{ env.ARTIFACTS_DIR }}'
env:
VERSION_OVERRIDE: ${{ needs.activate.outputs.verdate }}
- name: upload to anaconda
if: ${{ matrix.platform.name == 'win' }}
run: |
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell '${{ github.workspace }}/dist/${{ matrix.platform.pkg_dir }}/*.tar.bz2'
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell '${{ env.ARTIFACTS_DIR }}/${{ matrix.platform.pkg_dir }}/*.conda'
- name: upload to anaconda
if: ${{ matrix.platform.name != 'win' }}
run: |
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell ${{ github.workspace }}/dist/${{ matrix.platform.pkg_dir }}/*.tar.bz2
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell ${{ env.ARTIFACTS_DIR }}/${{ matrix.platform.pkg_dir }}/*.conda
+4
View File
@@ -49,3 +49,7 @@ if errorlevel 1 exit 1
ninja install -j 1
if errorlevel 1 exit 1
python %RECIPE_DIR%/update_version_init.py %PKG_VERSION% %SP_DIR%/ifcopenshell/__init__.py
if errorlevel 1 exit 1
+3 -1
View File
@@ -46,4 +46,6 @@ cmake ${CMAKE_ARGS} -G Ninja \
ninja
ninja install -j 1
ninja install -j 1
python "${RECIPE_DIR}/update_version_init.py" "${PKG_VERSION}" "${SP_DIR}/ifcopenshell/__init__.py"
+13 -16
View File
@@ -1,7 +1,3 @@
variant:
- novtk
- all
occt:
- 7.8.1
@@ -24,12 +20,13 @@ cxx_compiler_version:
- '12' # [linux]
- '16' # [osx]
c_stdlib_version:
- '2.12' # [linux]
- '10.13' # [osx]
- 2.17 # [linux]
- 10.13 # [osx and x86_64]
- 11.0 # [osx and arm64]
hdf5:
- 1.14.3
- 1.14.6
libboost_devel:
- '1.84'
- '1.86'
libxml2:
- '2'
mpfr:
@@ -47,12 +44,12 @@ target_platform:
- win-64 # [win]
- linux-64 # [linux]
- osx-64 # [osx]
macos_machine:
- x86_64-apple-darwin13.4.0
MACOSX_DEPLOYMENT_TARGET:
- '10.13'
MACOSX_SDK_VERSION:
- '10.13'
macos_machine: # [osx]
- x86_64-apple-darwin13.4.0 # [osx and x86_64]
- arm64-apple-darwin20.0.0 # [osx and arm64]
MACOSX_DEPLOYMENT_TARGET: # [osx]
- 11.0 # [osx and arm64]
- 10.13 # [osx and x86_64]
CONDA_BUILD_SYSROOT:
- "/Users/runner/work/MacOSX10.13.sdk" # [osx]
CONDA_BUILD_SYSROOT: # [osx]
- "/Users/runner/work/MacOSX10.13.sdk" # [osx and x86_64]
+48 -33
View File
@@ -1,64 +1,78 @@
{% set version = environ.get('VERSION_OVERRIDE', '0.8.0') %}
{% set build = 0 %}
# Higher number -> Always prioritize "novtk" variant over "all" variant
{% set build = build + 200 %} # [variant == "novtk"]
{% set build = build + 100 %} # [variant == "all"]
context:
version: ${{ env.get("VERSION_OVERRIDE", default="0.8.2.1") }}
build: 1
package:
name: ifcopenshell
version: {{ version }}
version: ${{ version }}
source:
path: ../
- path: ..
build:
number: {{ build }}
skip: true # [py<39]
binary_relocation: false # [osx]
string: py{{ CONDA_PY }}_{{ variant }}_h{{ PKG_HASH }}_{{ build }}
run_exports:
- {{ pin_subpackage('ifcopenshell', max_pin='x.x.x') }} *{{ variant }}*
number: ${{ build }}
dynamic_linking:
binary_relocation: ${{ true if osx }}
requirements:
build:
- cmake
- if: build_platform != target_platform
then:
- python
- cross-python_${{ target_platform }}
- cmake <4
- ninja
- swig >=4.1.1
- {{ stdlib("c") }}
- {{ compiler('c') }}
- {{ compiler('cxx') }}
- ${{ stdlib("c") }}
- ${{ compiler('c') }}
- ${{ compiler('cxx') }}
host:
- python
- libboost-devel
- occt *=*{{ variant }}*
- occt
- libxml2
- cgal-cpp
- hdf5
- eigen
- mpfr
- nlohmann_json
- gmp # [unix]
- mpir # [win]
- gmp
- zlib
run:
- python
- {{ pin_compatible('occt', max_pin='x.x.x') }} *{{ variant }}*
- {{ pin_compatible('cgal-cpp', max_pin='x.x') }}
test:
imports:
- ifcopenshell
- shapely
- typing_extensions
- ${{ pin_compatible('occt', upper_bound='x.x.x') }}
- ${{ pin_compatible('cgal-cpp', upper_bound='x.x') }}
run_exports:
- ${{ pin_subpackage('ifcopenshell', upper_bound='x.x.x') }}
tests:
- python:
imports:
- ifcopenshell
pip_check: false
- script:
- python -c "import ifcopenshell; assert ifcopenshell.version == '${{ version }}', 'print(ifcopenshell.version)'"
requirements:
run:
- occt * *novtk* # Ensure that even though compiled against OCCT with VTK, it can still run with OCCT without VTK
- pytest
- python-dateutil
- xmlschema
- xsdata
- lxml
- isodate
- lark
- networkx
- tabulate
- shapely
about:
home: https://ifcopenshell.org
license: LGPL-3.0-or-later
license_file: COPYING
summary: 'IfcOpenShell is a library to support the IFC file format'
summary: IfcOpenShell is a library to support the IFC file format
description: |
IfcOpenShell
============
@@ -293,8 +307,9 @@ about:
[MSYS2]: https://msys2.github.io/ "MSYS2"
[win/readme.md]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/win/readme.md "win/readme.md"
[nix/build-all.py]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/nix/build-all.py "nix/build-all.py"
doc_url: https://ifcopenshell.org/
dev_url: https://github.com/IfcOpenShell/IfcOpenShell
homepage: https://ifcopenshell.org
repository: https://github.com/IfcOpenShell/IfcOpenShell
documentation: https://ifcopenshell.org/
extra:
recipe-maintainers:
+32
View File
@@ -0,0 +1,32 @@
import re
import argparse
from pathlib import Path
def update_version(file_path: str, version: str) -> None:
"""Update the version string in the given __init__.py file."""
file_path = Path(file_path)
# Read the file and replace the version
file_contents = file_path.read_text(encoding="utf-8")
new_contents = re.sub(r'version = "0\.0\.0"', f'version = "{version}"', file_contents)
# Write the updated contents back to the file
file_path.write_text(new_contents, encoding="utf-8")
print(f"Updated version in {file_path} to {version}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Update the version string in a Python file.")
parser.add_argument(
"version",
type=str,
help="The version string to replace '0.0.0' with (e.g., '1.2.3')."
)
parser.add_argument(
"file",
type=str,
help="The path to the __init__.py file where the version will be updated."
)
args = parser.parse_args()
update_version(args.file, args.version)
+2
View File
@@ -36,3 +36,5 @@ extend-exclude = '''
reportInvalidTypeForm = false
disableBytesTypePromotions = true
reportUnnecessaryTypeIgnoreComment = true
# Just to add a quick insert of `# pyright: ignore[xxx]` comments in Pylance.
enableTypeIgnoreComments = false
+1 -1
View File
@@ -20,7 +20,7 @@ def get_extensions_attributes(extensions: Extensions) -> dict[str, AttributeData
- subattribute name"""
possible_attributes = {}
for field in fields(type(extensions)):
field_type = field.type.__args__[0] # type: ignore [reportAttributeAccessIssue]
field_type = field.type.__args__[0] # pyright: ignore [reportAttributeAccessIssue]
subfield = next(iter(fields(field_type)))
xsd_name = subfield.metadata["name"]
possible_attributes[field.name] = AttributeData(field_type, subfield.name, xsd_name)
+1
View File
@@ -123,6 +123,7 @@ classes = [
operator.SelectURIAttribute,
operator.SetTab,
operator.SwitchTab,
operator.ShowSystemInfo,
prop.StrProperty,
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
prop.ObjProperty,
@@ -47,8 +47,6 @@
"scene.render.simplify_gpencil_shader_fx": true,
"scene.render.simplify_gpencil_tint": true,
"scene.render.simplify_gpencil_view_fill": true,
"scene.render.simplify_shadows": 1.0,
"scene.render.simplify_shadows_render": 1.0,
"scene.render.simplify_subdivision": 6,
"scene.render.simplify_subdivision_render": 6,
"scene.render.simplify_volumes": 1.0,
@@ -147,7 +145,7 @@
"scene.display.shading.use_scene_world_render": false,
"scene.display.shading.use_studiolight_view_rotation": true,
"scene.display.shading.use_world_space_lighting": false,
"scene.display.shading.wireframe_color_type": "MATERIAL",
"scene.display.shading.wireframe_color_type": "THEME",
"scene.display.shading.xray_alpha": 0.5,
"scene.display.shading.xray_alpha_wireframe": 0.0,
"scene.display.light_direction": [
@@ -162,7 +160,6 @@
"scene.display.shadow_focus": 0.0,
"scene.display.shadow_shift": 0.10000000149011612,
"scene.display.viewport_aa": "FXAA",
"space.overlay.backwire_opacity": 0.5,
"space.overlay.bone_wire_alpha": 1.0,
"space.overlay.display_handle": "SELECTED",
"space.overlay.fade_inactive_alpha": 0.4000000059604645,
@@ -189,7 +186,6 @@
"space.overlay.show_edge_crease": true,
"space.overlay.show_edge_seams": true,
"space.overlay.show_edge_sharp": true,
"space.overlay.show_edges": false,
"space.overlay.show_extra_edge_angle": false,
"space.overlay.show_extra_edge_length": false,
"space.overlay.show_extra_face_angle": false,
@@ -208,7 +204,6 @@
"space.overlay.show_motion_paths": true,
"space.overlay.show_object_origins": false,
"space.overlay.show_object_origins_all": false,
"space.overlay.show_occlude_wire": false,
"space.overlay.show_onion_skins": false,
"space.overlay.show_ortho_grid": true,
"space.overlay.show_outline_selected": true,
@@ -298,8 +293,6 @@
"scene.render.simplify_gpencil_shader_fx": true,
"scene.render.simplify_gpencil_tint": true,
"scene.render.simplify_gpencil_view_fill": true,
"scene.render.simplify_shadows": 1.0,
"scene.render.simplify_shadows_render": 1.0,
"scene.render.simplify_subdivision": 6,
"scene.render.simplify_subdivision_render": 6,
"scene.render.simplify_volumes": 1.0,
@@ -398,7 +391,7 @@
"scene.display.shading.use_scene_world_render": false,
"scene.display.shading.use_studiolight_view_rotation": true,
"scene.display.shading.use_world_space_lighting": false,
"scene.display.shading.wireframe_color_type": "MATERIAL",
"scene.display.shading.wireframe_color_type": "THEME",
"scene.display.shading.xray_alpha": 0.5,
"scene.display.shading.xray_alpha_wireframe": 0.0,
"scene.display.light_direction": [
@@ -413,7 +406,6 @@
"scene.display.shadow_focus": 0.0,
"scene.display.shadow_shift": 0.10000000149011612,
"scene.display.viewport_aa": "FXAA",
"space.overlay.backwire_opacity": 0.5,
"space.overlay.bone_wire_alpha": 1.0,
"space.overlay.display_handle": "SELECTED",
"space.overlay.fade_inactive_alpha": 0.4000000059604645,
@@ -440,7 +432,6 @@
"space.overlay.show_edge_crease": true,
"space.overlay.show_edge_seams": true,
"space.overlay.show_edge_sharp": true,
"space.overlay.show_edges": false,
"space.overlay.show_extra_edge_angle": false,
"space.overlay.show_extra_edge_length": false,
"space.overlay.show_extra_face_angle": false,
@@ -459,7 +450,6 @@
"space.overlay.show_motion_paths": true,
"space.overlay.show_object_origins": false,
"space.overlay.show_object_origins_all": false,
"space.overlay.show_occlude_wire": false,
"space.overlay.show_onion_skins": false,
"space.overlay.show_ortho_grid": true,
"space.overlay.show_outline_selected": true,
@@ -520,7 +510,7 @@
"scene.render.border_min_x": 0.0,
"scene.render.border_min_y": 0.0,
"scene.render.dither_intensity": 1.0,
"scene.render.engine": "BLENDER_EEVEE",
"scene.render.engine": "BLENDER_EEVEE_NEXT",
"scene.render.film_transparent": false,
"scene.render.filter_size": 1.5,
"scene.render.fps": 24,
@@ -549,8 +539,6 @@
"scene.render.simplify_gpencil_shader_fx": true,
"scene.render.simplify_gpencil_tint": true,
"scene.render.simplify_gpencil_view_fill": true,
"scene.render.simplify_shadows": 1.0,
"scene.render.simplify_shadows_render": 1.0,
"scene.render.simplify_subdivision": 6,
"scene.render.simplify_subdivision_render": 6,
"scene.render.simplify_volumes": 1.0,
@@ -649,7 +637,7 @@
"scene.display.shading.use_scene_world_render": false,
"scene.display.shading.use_studiolight_view_rotation": true,
"scene.display.shading.use_world_space_lighting": false,
"scene.display.shading.wireframe_color_type": "MATERIAL",
"scene.display.shading.wireframe_color_type": "THEME",
"scene.display.shading.xray_alpha": 0.5,
"scene.display.shading.xray_alpha_wireframe": 0.0,
"scene.display.light_direction": [
@@ -664,7 +652,6 @@
"scene.display.shadow_focus": 0.0,
"scene.display.shadow_shift": 0.10000000149011612,
"scene.display.viewport_aa": "FXAA",
"space.overlay.backwire_opacity": 0.5,
"space.overlay.bone_wire_alpha": 1.0,
"space.overlay.display_handle": "SELECTED",
"space.overlay.fade_inactive_alpha": 0.4000000059604645,
@@ -691,7 +678,6 @@
"space.overlay.show_edge_crease": true,
"space.overlay.show_edge_seams": true,
"space.overlay.show_edge_sharp": true,
"space.overlay.show_edges": false,
"space.overlay.show_extra_edge_angle": false,
"space.overlay.show_extra_edge_length": false,
"space.overlay.show_extra_face_angle": false,
@@ -710,7 +696,6 @@
"space.overlay.show_motion_paths": true,
"space.overlay.show_object_origins": true,
"space.overlay.show_object_origins_all": false,
"space.overlay.show_occlude_wire": false,
"space.overlay.show_onion_skins": false,
"space.overlay.show_ortho_grid": true,
"space.overlay.show_outline_selected": true,
@@ -14,7 +14,7 @@ DATA;
#7=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#8=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','Comma separated list of selector expressions to evaluate for each drawing elementand add results to their ''class'' attribute.\X2\000A\X0\E.g. ''Name, id'' would add to ''class'' value similar to ''Name-Wall id-1220''.\X2\000A\X0\Then it can be used to applied css styles based on the resulting class.\X2\000A\X0\If attribute is not present on the element, then it won''t be added to it''s ''class''.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#13=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
+22 -6
View File
@@ -428,7 +428,8 @@ class IfcImporter:
rep = representation
while True:
if len(rep.Items) == 1 and rep.Items[0].is_a("IfcMappedItem"):
# Tekla 2023 has missing items, though it's invalid IFC.
if rep.Items and len(rep.Items) == 1 and rep.Items[0].is_a("IfcMappedItem"):
rep_matrix = ifcopenshell.util.placement.get_mappeditem_transformation(rep.Items[0])
if not np.allclose(rep_matrix, np.eye(4)):
matrix = rep_matrix @ matrix
@@ -595,6 +596,7 @@ class IfcImporter:
return self.create_generic_sqlite_elements(elements)
if self.ifc_import_settings.should_load_geometry:
elements = elements.copy() # Prevent argument mutation.
context_settings = (
tool.Loader.settings.gross_context_settings if is_gross else tool.Loader.settings.context_settings
)
@@ -1136,7 +1138,22 @@ class IfcImporter:
bpy.ops.view3d.view_selected()
bpy.ops.object.select_all(action="DESELECT")
def setup_arrays(self, annotations_to_import: Optional[set[ifcopenshell.entity_instance]] = None) -> None:
def setup_arrays(
self,
*,
annotations_to_import: Optional[set[ifcopenshell.entity_instance]] = None,
openings_to_import: Optional[set[ifcopenshell.entity_instance]] = None,
) -> None:
# During base import some elements are skipped.
is_base_import = True
elements_to_import: set[ifcopenshell.entity_instance] = set()
if annotations_to_import is not None:
is_base_import = False
elements_to_import = annotations_to_import
elif openings_to_import is not None:
is_base_import = False
elements_to_import = openings_to_import
for pset in self.file.by_type("IfcPropertySet"):
if pset.Name != "BBIM_Array":
continue
@@ -1146,12 +1163,11 @@ class IfcImporter:
for rel in pset.DefinesOccurrence:
element: ifcopenshell.entity_instance
for element in rel.RelatedObjects:
if annotations_to_import is None:
# IfcAnnotations are not loaded during base import.
if element.is_a("IfcAnnotation"):
if is_base_import:
if element.is_a("IfcAnnotation") or element.is_a("IfcOpeningElement"):
continue
else:
if element not in annotations_to_import:
if element not in elements_to_import:
continue
for i in range(len(data)):
tool.Blender.Modifier.Array.set_children_lock_state(element, i, True)
@@ -164,8 +164,6 @@ class AggregateDecorator:
def draw_aggregate(self, context):
props = tool.Aggregate.get_aggregate_props()
if props.in_aggregate_mode:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
decorator_color_special = self.addon_prefs.decorator_color_special
decorator_color_selected = self.addon_prefs.decorator_color_selected
@@ -194,7 +192,13 @@ class AggregateDecorator:
aggregates.append(obj)
continue
aggregate = ifcopenshell.util.element.get_aggregate(element)
aggregates_list = tool.Aggregate.get_aggregates_recursively(element)
if props.in_aggregate_mode and props.editing_aggregate:
index = aggregates_list.index(tool.Ifc.get_entity(props.editing_aggregate))
if index > 0:
aggregate = aggregates_list[index - 1]
else:
aggregate = aggregates_list[-1]
if aggregate:
aggregates.append(tool.Ifc.get_object(aggregate))
@@ -212,9 +216,7 @@ class AggregateDecorator:
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
if props.in_aggregate_mode:
return
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate))
parts = tool.Aggregate.get_parts_recursively(tool.Ifc.get_entity(aggregate))
parts_objs = [tool.Ifc.get_object(p) for p in parts]
indices, edges = create_bounding_box(parts_objs)
@@ -333,7 +335,7 @@ class AggregateModeDecorator:
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate_obj))
parts = tool.Aggregate.get_parts_recursively(tool.Ifc.get_entity(aggregate_obj))
color = self.addon_prefs.decorator_color_selected
parts_objs = [tool.Ifc.get_object(p) for p in parts]
@@ -1401,6 +1401,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
self.hide_spaces(context)
def hide_spaces(self, context: bpy.types.Context) -> None:
assert context.area
old = context.area.type
context.area.type = "VIEW_3D"
bpy.ops.object.select_all(action="DESELECT")
@@ -21,14 +21,13 @@ from . import ui, prop, operator
classes = (
operator.GetBSDDClassificationProperties,
operator.LoadBSDDDomains,
operator.LoadBSDDDictionaries,
operator.SearchBSDDClass,
operator.SetActiveBSDDDictionary,
prop.BSDDDomain,
prop.BSDDDictionary,
prop.BSDDClassification,
prop.BSDDPset,
prop.BIMBSDDProperties,
ui.BIM_UL_bsdd_domains,
ui.BIM_UL_bsdd_dictionaries,
ui.BIM_UL_bsdd_classifications,
ui.BIM_PT_bsdd,
)
+45
View File
@@ -0,0 +1,45 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifcopenshell
import ifcopenshell.util.date
import ifcopenshell.util.classification
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
def refresh():
BSDDData.is_loaded = False
class BSDDData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data["active_dictionary"] = cls.active_dictionary()
@classmethod
def active_dictionary(cls):
props = tool.Bsdd.get_bsdd_props()
results = [("ALL", "All Dictionaries", "All active dictionaries")]
results.extend([(d.uri, d.name, f"{d.status} - {d.version}") for d in props.dictionaries if d.is_active])
return results
+5 -25
View File
@@ -22,25 +22,13 @@ import bonsai.tool as tool
from bonsai.core import bsdd as core
class LoadBSDDDomains(bpy.types.Operator):
bl_idname = "bim.load_bsdd_domains"
class LoadBSDDDictionaries(bpy.types.Operator):
bl_idname = "bim.load_bsdd_dictionaries"
bl_label = "Load bSDD Dictionaries"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
core.load_bsdd(bsdd.Client(), tool.Bsdd)
return {"FINISHED"}
class SetActiveBSDDDictionary(bpy.types.Operator):
bl_idname = "bim.set_active_bsdd_domain"
bl_label = "Set bSDD Dictionary as active"
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty()
uri: bpy.props.StringProperty()
def execute(self, context):
core.set_active_bsdd_dictionary(self.name, self.uri, tool.Bsdd)
core.load_bsdd(tool.Bsdd)
return {"FINISHED"}
@@ -50,17 +38,9 @@ class SearchBSDDClass(bpy.types.Operator):
bl_description = "Search for bSDD classes by the provided keyword"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
# Requirement by buildingSMART bSDD api.
if len(context.scene.BIMBSDDProperties.keyword) < 3:
cls.poll_message_set("Search query has to be at least 3 characters long.")
return False
return True
def execute(self, context):
keyword = context.scene.BIMBSDDProperties.keyword
classes_found = core.search_class(keyword, bsdd.Client(), tool.Bsdd)
classes_found = core.search_class(keyword, tool.Bsdd)
self.report({"INFO"}, f"{classes_found} bSDD classes found for '{keyword}'.")
return {"FINISHED"}
@@ -72,7 +52,7 @@ class GetBSDDClassificationProperties(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
pset_data = core.get_class_properties(bsdd.Client(), tool.Bsdd)
pset_data = core.get_class_properties(tool.Bsdd)
psets_found = len(pset_data)
props_found = sum(len(pset) for pset in pset_data.values())
self.report({"INFO"}, f"{psets_found} psets found ({props_found} props) for the active classification.")
+30 -9
View File
@@ -18,6 +18,7 @@
import bpy
from bpy.types import PropertyGroup
from bonsai.bim.module.bsdd.data import BSDDData
from bonsai.bim.prop import Attribute, StrProperty
from bpy.props import (
PointerProperty,
@@ -31,21 +32,34 @@ from bpy.props import (
)
class BSDDDomain(PropertyGroup):
def get_active_dictionary(self, context):
if not BSDDData.is_loaded:
BSDDData.load()
return BSDDData.data["active_dictionary"]
def update_is_active(self: "BSDDDictionary", context: bpy.types.Context) -> None:
BSDDData.data["active_dictionary"] = BSDDData.active_dictionary()
class BSDDDictionary(PropertyGroup):
name: StringProperty(name="Name")
uri: StringProperty(name="URI")
default_language_code: StringProperty(name="Language")
organization_name_owner: StringProperty(name="Organization")
status: StringProperty(name="Status")
version: StringProperty(name="Version")
is_active: BoolProperty(
name="Is Active", description="Enable to search with this dictionary", default=False, update=update_is_active
)
class BSDDClassification(PropertyGroup):
name: StringProperty(name="Name")
reference_code: StringProperty(name="Reference Code")
uri: StringProperty(name="Namespace URI")
domain_name: StringProperty(name="Domain Name")
domain_namespace_uri: StringProperty(name="Domain Namespace URI")
dictionary_name: StringProperty(name="Dictionary Name")
dictionary_namespace_uri: StringProperty(name="Dictionary Namespace URI")
class BSDDPset(PropertyGroup):
@@ -54,10 +68,11 @@ class BSDDPset(PropertyGroup):
class BIMBSDDProperties(PropertyGroup):
active_domain: StringProperty(name="Active Domain")
active_dictionary: StringProperty(name="Active Dictionary")
active_dictionary: EnumProperty(items=get_active_dictionary, name="Active Dictionary")
active_uri: StringProperty(name="Active URI")
domains: CollectionProperty(name="Domains", type=BSDDDomain)
active_domain_index: IntProperty(name="Active Domain Index")
dictionaries: CollectionProperty(name="Dictionaries", type=BSDDDictionary)
active_dictionary_index: IntProperty(name="Active Dictionary Index")
classifications: CollectionProperty(name="Classifications", type=BSDDClassification)
active_classification_index: IntProperty(name="Active Classification Index")
keyword: StringProperty(name="Keyword", description="Query for bsdd classes search, case and accent insensitive")
@@ -69,9 +84,15 @@ class BIMBSDDProperties(PropertyGroup):
use_only_ifc_properties: BoolProperty(
name="Only IFC Properties",
description="Whether to display and assign only properties from IFC dictionary",
default=True,
default=False,
)
load_preview_domains: BoolProperty(
name="Load Preview Domains", description="Whether it should load preview and inactive domains", default=False
load_preview_dictionaries: BoolProperty(
name="Load Preview Dictionaries", description="Load dictionaries marked as Preview status", default=False
)
load_inactive_dictionaries: BoolProperty(
name="Load Inactive Dictionaries", description="Load dictionaries marked as Inactive status", default=False
)
load_test_dictionaries: BoolProperty(
name="Load Test Dictionaries", description="Load dictionaries that are for testing only", default=False
)
classification_psets: CollectionProperty(name="Classification Psets", type=BSDDPset)
+26 -32
View File
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bonsai.tool as tool
from bonsai.bim.module.bsdd.data import BSDDData
from bpy.types import Panel, UIList
@@ -30,65 +31,58 @@ class BIM_PT_bsdd(Panel):
bl_parent_id = "BIM_PT_tab_project_setup"
def draw(self, context):
if not BSDDData.is_loaded:
BSDDData.load()
props = context.scene.BIMBSDDProperties
layout = self.layout
row = self.layout.row(align=True)
row.prop(props, "load_preview_domains")
if len(props.domains):
row.operator("bim.load_bsdd_domains", text="", icon="FILE_REFRESH")
if props.active_domain:
if len(props.dictionaries):
row = self.layout.row()
row.label(text="Active: " + props.active_domain, icon="URL")
else:
row = self.layout.row()
row.label(text="No Active bSDD Domain", icon="ERROR")
row.operator("bim.load_bsdd_dictionaries", icon="FILE_REFRESH")
if len(props.domains):
if len(props.dictionaries):
self.layout.template_list(
"BIM_UL_bsdd_domains",
"BIM_UL_bsdd_dictionaries",
"",
props,
"domains",
"dictionaries",
props,
"active_domain_index",
"active_dictionary_index",
)
if 0 <= props.active_domain_index < len(props.domains):
selected_domain = props.domains[props.active_domain_index]
if 0 <= props.active_dictionary_index < len(props.dictionaries):
selected_dictionary = props.dictionaries[props.active_dictionary_index]
else:
selected_domain = None
selected_dictionary = None
if selected_domain:
layout.label(text="Selected domain:")
if selected_dictionary:
layout.label(text="Selected dictionary:")
box = layout.box()
row = box.row(align=True)
row.label(text="Language")
row.label(text=selected_domain.default_language_code)
row.label(text=selected_dictionary.default_language_code)
row = box.row(align=True)
row.label(text="Version")
row.label(text=selected_domain.version)
box.operator("bim.open_uri", text="Open bSDD In Browser", icon="URL").uri = selected_domain.uri
row.label(text=selected_dictionary.version)
box.operator("bim.open_uri", text="Open bSDD In Browser", icon="URL").uri = selected_dictionary.uri
else:
row = self.layout.row()
row.operator("bim.load_bsdd_domains")
row.operator("bim.load_bsdd_dictionaries")
class BIM_UL_bsdd_domains(UIList):
class BIM_UL_bsdd_dictionaries(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
props = context.scene.BIMBSDDProperties
row = layout.row(align=True)
if item.status != "Active":
row.label(text=f"{item.name} ({item.organization_name_owner}) - {item.status}", icon="ERROR")
row.label(
text=f"{item.name} ({item.organization_name_owner}) v{item.version} - {item.status}", icon="ERROR"
)
else:
row.label(text=f"{item.name} ({item.organization_name_owner})")
if item.uri == props.active_uri:
row.label(text="", icon="URL")
else:
op = row.operator("bim.set_active_bsdd_domain", text="", icon="RESTRICT_SELECT_OFF")
op.name = item.name
op.uri = item.uri
row.label(text=f"{item.name} ({item.organization_name_owner}) v{item.version}")
row.prop(
item, "is_active", icon="CHECKBOX_HLT" if item.is_active else "CHECKBOX_DEHLT", text="", emboss=False
)
class BIM_UL_bsdd_classifications(UIList):
@@ -116,28 +116,28 @@ class AddClassificationFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMBSDDProperties
domain = next((d for d in props.domains if d.name == props.active_domain and d.uri == props.active_uri), None)
# Maybe user loaded preview domains, set it as active
# and then reloaded them without preview domains.
if not domain:
self.report(
{"INFO"},
f"Couldn't find domain '{props.active_domain}' ({props.active_uri}). Try to reload bSDD dictionaries.",
if props.active_dictionary == "ALL":
dictionaries = [d.uri for d in props.dictionaries if d.is_active]
else:
dictionaries = [props.active_dictionary]
for uri in dictionaries:
if not (dictionary := tool.Bsdd.get_dictionary(uri)):
continue
has_classification = False
for element in tool.Ifc.get().by_type("IfcClassification"):
if element.Name == dictionary["name"] or (
tool.Classification.get_location(element) == dictionary["uri"]
):
self.report({"INFO"}, f"Classification '{dictionary['name']}' is already added to the project.")
has_classification = True
if has_classification:
continue
classification = ifcopenshell.api.run(
"classification.add_classification", tool.Ifc.get(), classification=dictionary["name"]
)
return
for element in tool.Ifc.get().by_type("IfcClassification"):
if element.Name == props.active_domain or (tool.Classification.get_location(element) == domain.uri):
self.report({"INFO"}, f"Classification '{props.active_domain}' is already added to the project.")
return
classification = ifcopenshell.api.run(
"classification.add_classification", tool.Ifc.get(), classification=props.active_domain
)
classification.Source = domain.organization_name_owner
classification.Edition = domain.version
tool.Classification.set_location(classification, domain.uri)
classification.Source = dictionary["organizationNameOwner"]
classification.Edition = dictionary["version"]
tool.Classification.set_location(classification, dictionary["uri"])
class EnableAddingManualClassification(bpy.types.Operator):
@@ -420,17 +420,17 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
classification = None
for element in tool.Ifc.get().by_type("IfcClassification"):
if element.Name == bsdd_classification.domain_name or (
tool.Classification.get_location(element) == bsdd_classification.domain_namespace_uri
if element.Name == bsdd_classification.dictionary_name or (
tool.Classification.get_location(element) == bsdd_classification.dictionary_namespace_uri
):
classification = element
break
if not classification:
classification = ifcopenshell.api.run(
"classification.add_classification", tool.Ifc.get(), classification=bsdd_classification.domain_name
"classification.add_classification", tool.Ifc.get(), classification=bsdd_classification.dictionary_name
)
tool.Classification.set_location(classification, bsdd_classification.domain_namespace_uri)
tool.Classification.set_location(classification, bsdd_classification.dictionary_namespace_uri)
for obj in objects:
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
@@ -78,13 +78,8 @@ class BIM_PT_classifications(Panel):
def draw_add_bsdd_ui(self, context):
self.bprops = context.scene.BIMBSDDProperties
if not self.bprops.active_domain:
row = self.layout.row()
row.label(text="No Active bSDD Domain", icon="ERROR")
return
row = self.layout.row()
row.label(text="Active: " + self.bprops.active_domain, icon="URL")
row.prop(self.bprops, "active_dictionary", text="")
row = self.layout.row()
row.operator("bim.add_classification_from_bsdd", icon="ADD")
@@ -163,13 +158,8 @@ class ReferenceUI:
row.operator("bim.enable_adding_manual_classification_reference", text="Add Reference", icon="ADD")
def draw_add_bsdd_ui(self, context):
if not self.bprops.active_domain:
row = self.layout.row()
row.label(text="No Active bSDD Domain", icon="ERROR")
return
row = self.layout.row()
row.label(text="Active: " + self.bprops.active_domain, icon="URL")
row.prop(self.bprops, "active_dictionary", text="")
row = self.layout.row(align=True)
row.prop(self.bprops, "keyword", text="")
@@ -30,6 +30,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING
class BIMContextProperties(PropertyGroup):
@@ -66,3 +67,10 @@ class BIMContextProperties(PropertyGroup):
)
active_context_id: IntProperty(name="Active Context Id")
context_attributes: CollectionProperty(name="Context Attributes", type=Attribute)
if TYPE_CHECKING:
contexts: str
subcontexts: str
target_views: str
active_context_id: int
context_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
+3 -2
View File
@@ -33,13 +33,14 @@ class BIM_PT_context(bpy.types.Panel):
@classmethod
def poll(cls, context):
return tool.Ifc.get()
return bool(tool.Ifc.get())
def draw(self, context):
if not ContextData.is_loaded:
ContextData.load()
props = context.scene.BIMContextProperties
assert self.layout
props = tool.Context.get_context_props()
row = self.layout.row(align=True)
row.prop(props, "contexts", text="")
@@ -268,7 +268,7 @@ class AssignCostItemQuantity(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Assign Cost Item Quantity"
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
related_object_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
related_object_type: bpy.props.EnumProperty( # pyright: ignore [reportRedeclaration]
items=[(i, i, "") for i in get_args(tool.Cost.RELATED_OBJECT_TYPE)],
)
prop_name: bpy.props.StringProperty()
@@ -745,7 +745,7 @@ class ClearCostItemAssignments(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Clear Cost Item Product Assignments"
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
related_object_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
related_object_type: bpy.props.EnumProperty( # pyright: ignore [reportRedeclaration]
items=[(i, i, "") for i in get_args(tool.Cost.RELATED_OBJECT_TYPE)],
)
+2 -2
View File
@@ -90,7 +90,7 @@ def update_cost_item_identification(self: "CostItem", context: bpy.types.Context
**{"cost_item": self.file.by_id(self.ifc_definition_id), "attributes": {"Identification": self.identification}},
)
if props.active_cost_item_id == self.ifc_definition_id:
attribute = props.cost_item_attributes.get("Identification")
attribute = props.cost_item_attributes["Identification"]
attribute.string_value = self.identification
@@ -105,7 +105,7 @@ def update_cost_item_name(self: "CostItem", context: bpy.types.Context) -> None:
**{"cost_item": self.file.by_id(self.ifc_definition_id), "attributes": {"Name": self.name}},
)
if props.active_cost_item_id == self.ifc_definition_id:
attribute = props.cost_item_attributes.get("Name")
attribute = props.cost_item_attributes["Name"]
attribute.string_value = self.name
@@ -28,7 +28,6 @@ classes = (
operator.AddAnnotationType,
operator.AddDrawing,
operator.AddDrawingStyle,
operator.AddDrawingStyleAttribute,
operator.AddDrawingToSheet,
operator.AddReference,
operator.AddReferenceImage,
@@ -80,12 +79,10 @@ classes = (
operator.RemoveDrawing,
operator.RemoveDrawingFromSheet,
operator.RemoveDrawingStyle,
operator.RemoveDrawingStyleAttribute,
operator.RemoveReference,
operator.RemoveSchedule,
operator.RemoveSheet,
operator.RemoveTextLiteral,
operator.ResizeText,
operator.SaveDrawingStyle,
operator.SaveDrawingStylesData,
operator.SelectAllDrawings,
@@ -29,52 +29,6 @@ from typing import Optional
class Annotator:
@staticmethod
def add_text(related_element: Optional[ifcopenshell.entity_instance] = None) -> bpy.types.Object:
curve = bpy.data.curves.new(type="FONT", name="Text")
curve.body = "TEXT"
obj = bpy.data.objects.new("Text", curve)
obj.matrix_world = bpy.context.scene.camera.matrix_world
if related_element is None:
location, _, _, _ = Annotator.get_placeholder_coords(bpy.context)
else:
curve.BIMAssignedProductProperties.related_element = related_element
location = related_element.location
obj.location = location
obj.hide_render = True
font = bpy.data.fonts.get("OpenGost TypeB TT")
if not font:
font = bpy.data.fonts.load(
tool.Blender.get_data_dir_path(Path("fonts") / "OpenGost Type B TT.ttf").__str__()
)
font.name = "OpenGost Type B TT"
curve.font = font
props = tool.Drawing.get_text_props(obj)
props.font_size = "2.5"
collection = tool.Blender.get_object_bim_props(bpy.context.scene.camera).collection
collection.objects.link(obj)
Annotator.resize_text(obj)
return obj
@staticmethod
def resize_text(text_obj: bpy.types.Object) -> None:
camera = None
group = tool.Drawing.get_drawing_group(tool.Ifc.get_entity(text_obj))
for element in tool.Drawing.get_drawing_elements(group):
if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
camera = tool.Ifc.get_object(element)
break
if not camera:
return
# This is a magic number for OpenGost
font_size = 1.6 / 1000
props = tool.Drawing.get_text_props(text_obj)
font_size *= float(props.font_size)
font_size /= tool.Drawing.get_scale_ratio(tool.Drawing.get_diagram_scale(camera)["Scale"])
text_obj.data.size = font_size
@staticmethod
def add_line_to_annotation(
obj: bpy.types.Object, co1: Optional[Vector] = None, co2: Optional[Vector] = None
@@ -129,8 +83,14 @@ class Annotator:
def get_annotation_obj(
drawing: ifcopenshell.entity_instance, object_type: str, data_type: tool.Drawing.ANNOTATION_DATA_TYPE
) -> bpy.types.Object:
assert bpy.context.scene
camera = tool.Ifc.get_object(drawing)
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
assert isinstance(camera, bpy.types.Object)
# those annotations you want to obey the depth of the 3d cursor
if object_type == "PLAN_LEVEL":
co1 = bpy.context.scene.cursor.location.copy()
else:
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
matrix_world = tool.Drawing.get_camera_matrix(camera)
matrix_world.translation = co1
collection = tool.Blender.get_object_bim_props(camera).collection
+77 -15
View File
@@ -118,7 +118,7 @@ class DrawingsData:
return len([e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"])
@classmethod
def location_hint(cls):
def location_hint(cls) -> list[tuple[tool.Drawing.LocationHintType, str, str]]:
props = tool.Drawing.get_document_props()
if props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
results = [("0", "Origin", "")]
@@ -132,12 +132,10 @@ class DrawingsData:
@classmethod
def active_drawing_pset_data(cls):
ifc_file = tool.Ifc.get()
props = tool.Drawing.get_document_props()
drawing_id = props.active_drawing_id
if drawing_id == 0:
drawing = props.get_active_drawing()
if drawing is None:
return {}
drawing = ifc_file.by_id(drawing_id)
return ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing")
@@ -238,23 +236,39 @@ class DecoratorData:
fill_cache = {}
@classmethod
def load(cls):
def load(cls, handler):
cls.is_loaded = True
cls.cut_cache = {}
cls.layerset_cache = {}
text = {}
dimension = {}
fall = {}
symbol = {}
for obj in bpy.context.visible_objects:
if not (element := tool.Ifc.get_entity(obj)):
continue
if tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
text[obj.name] = cls.get_ifc_text_data(obj)
if tool.Drawing.is_annotation_object_type(element, ("TEXT", "TEXT_LEADER")):
text[obj.name] = cls.get_text_data(obj)
if text[obj.name]["Symbol"]:
symbol[obj.name] = cls.get_symbol_data(obj)
elif tool.Drawing.is_annotation_object_type(
element, ("DIMENSION", "DIAMETER", "SECTION_LEVEL", "PLAN_LEVEL", "RADIUS")
):
dimension[obj.name] = cls.get_dimension_data(obj)
cls.data = {"text": text, "dimension": dimension}
elif tool.Drawing.is_annotation_object_type(
element, ("FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT")
):
fall[obj.name] = cls.get_fall_data(obj)
elif tool.Drawing.is_annotation_object_type(element, ("SYMBOL",)):
symbol[obj.name] = cls.get_symbol_data(obj)
cls.data = {
"text": text,
"dimension": dimension,
"fall": fall,
"symbol": symbol,
"object_decorators": cls.object_decorators(handler),
}
@classmethod
def get_batting_thickness(cls, obj):
@@ -313,7 +327,7 @@ class DecoratorData:
return display_data
@classmethod
def get_ifc_text_data(cls, obj: bpy.types.Object) -> dict:
def get_text_data(cls, obj: bpy.types.Object) -> dict:
"""used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER"\n
returns font size in mm for current ifc text object"""
element = tool.Ifc.get_entity(obj)
@@ -356,11 +370,6 @@ class DecoratorData:
return {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at}
@classmethod
def get_symbol(cls, obj: bpy.types.Object) -> Union[str, None]:
"""used by IfcAnnotations with ObjectType MULTI_SYMBOL"""
return tool.Drawing.get_annotation_symbol(tool.Ifc.get_entity(obj))
@classmethod
def get_dimension_data(cls, obj):
"""used by Ifc Annotations with ObjectType:
@@ -396,6 +405,59 @@ class DecoratorData:
"custom_unit": custom_unit,
}
@classmethod
def get_fall_data(cls, obj):
object_type = None
if element := tool.Ifc.get_entity(obj):
object_type = ifcopenshell.util.element.get_predefined_type(element)
return {"object_type": object_type}
@classmethod
def get_symbol_data(cls, obj):
return tool.Drawing.get_annotation_symbol(tool.Ifc.get_entity(obj))
@classmethod
def object_decorators(cls, handler):
import bonsai.bim.module.drawing.decoration
if not bonsai.bim.module.drawing.decoration.DecorationsHandler.installed:
return []
props = tool.Drawing.get_document_props()
if (drawing := props.get_active_drawing()) is None:
return []
camera = tool.Ifc.get_object(drawing)
assert isinstance(camera, bpy.types.Object)
collection = tool.Blender.get_object_bim_props(camera).collection
assert collection
results = []
viewport = tool.Blender.get_view3d_space()
for obj in collection.all_objects:
if not obj.visible_get(viewport=viewport):
continue
element = tool.Ifc.get_entity(obj)
if not element:
continue
if not element.is_a("IfcAnnotation"):
continue
object_type: Union[str, None] = ifcopenshell.util.element.get_predefined_type(element)
if object_type == "DRAWING":
continue
if dec := handler.decorators.get(object_type, None):
results.append((obj, dec))
elif isinstance(obj.data, bpy.types.Mesh):
if object_type == "LINEWORK" and "dashed" in str(
ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
).split(" "):
results.append((obj, handler.decorators["HIDDEN_LINE"]))
else:
results.append((obj, handler.decorators["MISC"]))
return results
class AnnotationData:
data = {}
@@ -562,8 +562,7 @@ class BaseDecorator:
MiscDecorator.decorate(self, context, obj)
return
symbol = DecoratorData.get_symbol(obj)
if not symbol:
if not (symbol := DecoratorData.data["symbol"].get(obj.name, None)):
return
for vert in mesh.vertices:
@@ -572,8 +571,7 @@ class BaseDecorator:
return
# EMPTY objects
symbol = DecoratorData.get_symbol(obj)
if not symbol:
if not (symbol := DecoratorData.data["symbol"].get(obj.name, None)):
return
rotation = -Vector((1, 0)).angle_signed(annotation_dir)
@@ -998,8 +996,6 @@ class FallDecorator(BaseDecorator):
# generate label text
# same function as in svgwriter.py
def get_label_text():
element = tool.Ifc.get_entity(obj)
assert element
B, A = [v.co.xyz for v in spline_points[:2]]
rise = abs(A.z - B.z)
O = A.copy()
@@ -1011,8 +1007,8 @@ class FallDecorator(BaseDecorator):
else:
angle = 90
# ues SLOPE_ANGLE as default
object_type = ifcopenshell.util.element.get_predefined_type(element)
# uses SLOPE_ANGLE as default
object_type = DecoratorData.data["fall"].get(obj, {}).get("object_type", None)
if object_type in ("FALL", "SLOPE_ANGLE"):
return f"{angle}°"
elif object_type == "SLOPE_FRACTION":
@@ -1023,6 +1019,7 @@ class FallDecorator(BaseDecorator):
if angle == 90:
return "-"
return f"{round(angle_tg * 100)} %"
return "NO DATA"
if spline_points:
text = get_label_text()
@@ -1650,9 +1647,15 @@ class CutDecorator:
all_vertex_i_offset = 0
selected_vertex_i_offset = 0
classes_no_cut_str = tool.Drawing.get_document_props().classes_no_cut
classes_no_cut = [word.strip() for word in classes_no_cut_str.split(",")]
for obj in [o for o in bpy.context.visible_objects if o.type == "MESH"]:
if not (element := tool.Ifc.get_entity(obj)):
continue
# Skip the elements in the following classes
if element.is_a() in classes_no_cut:
continue
self.decorate(context, obj, element)
verts, edges = DecoratorData.cut_cache[element.id()]
@@ -1915,17 +1918,18 @@ class DecorationsHandler:
]
installed = None
handler = None
@classmethod
def install(cls, context):
if cls.installed:
cls.uninstall()
if not DecoratorData.is_loaded:
DecoratorData.load()
handler = cls()
# NOTE: we USE POST_PIXEL here so that we can use both POLYLINE_UNIFORM_COLOR
# and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL")
if not DecoratorData.is_loaded:
DecoratorData.load(handler)
@classmethod
def uninstall(cls):
@@ -1948,47 +1952,9 @@ class DecorationsHandler:
for decorator in self.decorators.values():
decorator.font_id = font_id
def get_objects_and_decorators(self, collection):
# TODO: do it in data instead of the handler for performance?
results = []
viewport = bpy.context.space_data
for obj in collection.all_objects:
if not obj.visible_get(viewport=viewport):
continue
element = tool.Ifc.get_entity(obj)
if not element:
continue
if not element.is_a("IfcAnnotation"):
continue
object_type: Union[str, None] = ifcopenshell.util.element.get_predefined_type(element)
if object_type == "DRAWING":
continue
if dec := self.decorators.get(object_type, None):
results.append((obj, dec))
elif isinstance(obj.data, bpy.types.Mesh):
if object_type == "LINEWORK" and "dashed" in str(
ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
).split(" "):
results.append((obj, self.decorators["HIDDEN_LINE"]))
else:
results.append((obj, self.decorators["MISC"]))
return results
def __call__(self, context):
collection, _ = helper.get_active_drawing(context.scene)
if collection is None:
return
if not DrawingsData.is_loaded:
DrawingsData.load()
object_decorators = self.get_objects_and_decorators(collection)
for obj, decorator in object_decorators:
for obj, decorator in DecoratorData.data["object_decorators"]:
decorator.decorate(context, obj)
+1 -13
View File
@@ -305,7 +305,7 @@ def format_distance(
if unit_length == "MILLIMETERS":
value = value / 1000
if precision:
if precision and isinstance(precision, float):
value = precision * round(float(value) / precision)
if decimal_places is not None:
@@ -392,18 +392,6 @@ def format_distance(
return tx_dist
def get_active_drawing(
scene: bpy.types.Scene,
) -> Union[tuple[bpy.types.Collection, bpy.types.Camera], tuple[None, None]]:
"""Get active drawing collection and camera"""
props = tool.Drawing.get_document_props()
try:
camera = tool.Ifc.get_object(tool.Ifc.get().by_id(props.active_drawing_id))
return tool.Blender.get_object_bim_props(camera).collection, camera
except:
return None, None
def get_project_collection(scene):
"""Get main project collection"""
+233 -189
View File
@@ -29,6 +29,8 @@ import numpy as np
import multiprocessing
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.pset
import ifcopenshell.api.style
import ifcopenshell.ifcopenshell_wrapper
import ifcopenshell.geom
import ifcopenshell.util.selector
@@ -45,8 +47,8 @@ import bonsai.bim.module.drawing.sheeter as sheeter
import bonsai.bim.export_ifc
from bpy_extras.io_utils import ImportHelper
from bonsai.bim.module.drawing.decoration import CutDecorator
from bonsai.bim.module.drawing.data import DecoratorData, DrawingsData
from typing import NamedTuple, List, Union, Optional, Literal, TYPE_CHECKING, Any
from bonsai.bim.module.drawing.data import DecoratorData
from typing import NamedTuple, List, Union, Optional, Literal, TYPE_CHECKING, Any, TypedDict
from lxml import etree
from math import radians
from mathutils import Vector, Color, Matrix
@@ -57,6 +59,7 @@ from pathlib import Path
from bpy_extras.image_utils import load_image
if TYPE_CHECKING:
from bonsai.bim.module.drawing.prop import RenderType
from bonsai.bim.module.project.prop import Link
from bpy._typing import rna_enums
@@ -114,35 +117,43 @@ class AddAnnotationType(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", use_existing_object_by_name=obj.name)
class EnableAddAnnotationType(bpy.types.Operator, tool.Ifc.Operator):
class EnableAddAnnotationType(bpy.types.Operator):
bl_idname = "bim.enable_add_annotation_type"
bl_label = "Enable Add Annotation Type"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
tool.Drawing.get_annotation_props().is_adding_type = True
return {"FINISHED"}
class DisableAddAnnotationType(bpy.types.Operator, tool.Ifc.Operator):
class DisableAddAnnotationType(bpy.types.Operator):
bl_idname = "bim.disable_add_annotation_type"
bl_label = "Disable Add Annotation Type"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
tool.Drawing.get_annotation_props().is_adding_type = False
return {"FINISHED"}
class AddDrawing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_drawing"
bl_label = "Add Drawing"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add a drawing view to the IFC project"
bl_description = (
"Add a drawing view to the IFC project.\n\n"
"For all views besides MODEL_VIEW camera will be placed at the current cursor position,\n"
"for MODEL_VIEW camera will be aligned to the current viewport position and orientation."
)
def _execute(self, context):
props = tool.Drawing.get_document_props()
hint = props.location_hint
if props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
hint = int(hint)
else:
assert hint in tool.Drawing.LOCATION_HINT_LITERALS
core.add_drawing(
tool.Ifc,
tool.Collector,
@@ -150,11 +161,12 @@ class AddDrawing(bpy.types.Operator, tool.Ifc.Operator):
target_view=props.target_view,
location_hint=hint,
)
try:
drawing = tool.Ifc.get().by_id(props.active_drawing_id)
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
except:
pass
# TODO: Why need to resync active drawing, if it wasn't changed.
drawing = props.get_active_drawing()
if drawing is None:
return
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
@@ -167,7 +179,6 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -190,11 +201,12 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
drawing=tool.Ifc.get().by_id(self.drawing),
should_duplicate_annotations=self.should_duplicate_annotations,
)
try:
drawing = tool.Ifc.get().by_id(props.active_drawing_id)
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
except:
pass
# TODO: Why need to resync active drawing, if it wasn't changed.
drawing = props.get_active_drawing()
if drawing is None:
return
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
class CreateDrawing(bpy.types.Operator):
@@ -215,14 +227,27 @@ class CreateDrawing(bpy.types.Operator):
+ "Add the CTRL modifier to optionally open drawings to view them as\n"
+ "they are created"
)
print_all: bpy.props.BoolProperty(name="Print All", default=False, options={"SKIP_SAVE"})
open_viewer: bpy.props.BoolProperty(name="Open in Viewer", default=False, options={"SKIP_SAVE"})
sync: bpy.props.BoolProperty(
print_all: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
name="Print All",
default=False,
options={"SKIP_SAVE"},
)
open_viewer: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
name="Open in Viewer",
default=False,
options={"SKIP_SAVE"},
)
sync: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
name="Sync Before Creating Drawing",
description="Could save some time if you're sure IFC and current Blender session are already in sync",
default=True,
)
if TYPE_CHECKING:
print_all: bool
open_viewer: bool
sync: bool
drawing_name: str
is_manifold_cache: dict[str, bool]
@@ -258,6 +283,7 @@ class CreateDrawing(bpy.types.Operator):
assert context.scene and context.scene.camera
active_drawing_id = tool.Blender.get_ifc_definition_id(context.scene.camera)
original_drawing_id = None
if self.print_all:
original_drawing_id = active_drawing_id
drawings_to_print = [d.ifc_definition_id for d in self.props.drawings if d.is_selected and d.is_drawing]
@@ -287,16 +313,15 @@ class CreateDrawing(bpy.types.Operator):
# Reassign props as data is recreated during the update.
self.cprops = tool.Drawing.get_camera_props(self.camera)
self.svg_writer = svgwriter.SvgWriter()
self.svg_writer.human_scale = self.human_scale
self.svg_writer.scale = self.scale
self.svg_writer.camera = self.camera
self.svg_writer.camera_width, self.svg_writer.camera_height = self.get_camera_dimensions()
self.svg_writer.camera_projection = tuple(
self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
camera_dims = self.get_camera_dimensions()
self.svg_writer = svgwriter.SvgWriter(
camera_width=camera_dims[0],
camera_height=camera_dims[1],
camera=self.camera,
camera_projection=(self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))),
scale=self.scale,
human_scale=self.human_scale,
)
self.svg_writer.calculate_scale()
self.svg_writer.setup_drawing_resource_paths(self.camera_element)
underlay_svg = None
@@ -328,11 +353,14 @@ class CreateDrawing(bpy.types.Operator):
if not self.open_viewer:
self.report({"INFO"}, f"{len(drawings_to_print)} drawings created...")
if self.print_all:
assert original_drawing_id is not None
bpy.ops.bim.activate_drawing(drawing=original_drawing_id, should_view_from_camera=False)
return {"FINISHED"}
def get_camera_dimensions(self):
def get_camera_dimensions(self) -> tuple[float, float]:
assert bpy.context.scene
render = bpy.context.scene.render
assert isinstance(self.camera.data, bpy.types.Camera)
if self.is_landscape(render):
width = self.camera.data.ortho_scale
height = width / render.resolution_x * render.resolution_y
@@ -398,17 +426,23 @@ class CreateDrawing(bpy.types.Operator):
for obj in bpy.context.view_layer.objects:
obj.hide_render = obj.name not in visible_object_names
assert context.scene and context.view_layer and context.screen
context.scene.render.filepath = str(Path(svg_path).with_suffix(".png"))
drawing_style = self.props.drawing_styles[self.cprops.active_drawing_style_index]
assert (drawing_style := self.cprops.get_active_drawing_style())
if drawing_style.render_type == "DEFAULT":
bpy.ops.render.render(write_still=True)
else:
previous_visibility = {}
for obj in tool.Blender.get_object_bim_props(self.camera).collection.objects:
if bpy.context.view_layer.objects.get(obj.name):
previous_visibility: dict[str, bool] = {}
collection = tool.Blender.get_object_bim_props(self.camera).collection
assert collection
# Hie annotations.
for obj in collection.objects:
if context.view_layer.objects.get(obj.name):
previous_visibility[obj.name] = obj.hide_get()
obj.hide_set(True)
# Hide objects that shouldn't appear on render - empties, camera, grids, etc.
for obj in context.visible_objects:
if (
(not obj.data and not obj.instance_collection)
@@ -417,16 +451,19 @@ class CreateDrawing(bpy.types.Operator):
or "IfcGridAxis/" in obj.name
or "IfcOpeningElement/" in obj.name
):
if bpy.context.view_layer.objects.get(obj.name):
if context.view_layer.objects.get(obj.name):
previous_visibility[obj.name] = obj.hide_get()
obj.hide_set(True)
space = self.get_view_3d(context.screen.areas)
assert (space := tool.Blender.get_view3d_space())
previous_shading = space.shading.type
previous_format = context.scene.render.image_settings.file_format
space.shading.type = "RENDERED"
context.scene.render.image_settings.file_format = "PNG"
bpy.ops.render.opengl(write_still=True)
with tool.Blender.bonsai_crash_txt("render.opengl"):
bpy.ops.render.opengl(write_still=True)
space.shading.type = previous_shading
context.scene.render.image_settings.file_format = previous_format
@@ -437,15 +474,15 @@ class CreateDrawing(bpy.types.Operator):
return svg_path
def get_linework_contexts(self, ifc: ifcopenshell.file, target_view: str) -> LineworkContexts:
plan_body_target_contexts = []
plan_body_model_contexts = []
model_body_target_contexts = []
model_body_model_contexts = []
plan_body_target_contexts: list[int] = []
plan_body_model_contexts: list[int] = []
model_body_target_contexts: list[int] = []
model_body_model_contexts: list[int] = []
plan_annotation_target_contexts = []
plan_annotation_model_contexts = []
model_annotation_target_contexts = []
model_annotation_model_contexts = []
plan_annotation_target_contexts: list[int] = []
plan_annotation_model_contexts: list[int] = []
model_annotation_target_contexts: list[int] = []
model_annotation_model_contexts: list[int] = []
for rep_context in ifc.by_type("IfcGeometricRepresentationContext"):
if rep_context.is_a("IfcGeometricRepresentationSubContext"):
@@ -517,8 +554,8 @@ class CreateDrawing(bpy.types.Operator):
target_view: str,
) -> None:
drawing_elements = drawing_elements.copy()
contexts: list[list[int]] = getattr(contexts, context_type)
for context in contexts:
contexts_: list[list[int]] = getattr(contexts, context_type)
for context in contexts_:
with profile(f"Processing {context_type} context"):
if not context or not drawing_elements:
continue
@@ -541,7 +578,7 @@ class CreateDrawing(bpy.types.Operator):
tree.add_element(elem)
drawing_elements -= processed
def generate_bisect_linework(self, context: bpy.types.Context, root):
def generate_bisect_linework(self, context: bpy.types.Context, root) -> None:
camera_matrix_i = context.scene.camera.matrix_world.inverted()
group = root.find("{http://www.w3.org/2000/svg}g")
@@ -576,7 +613,7 @@ class CreateDrawing(bpy.types.Operator):
path.attrib["d"] = d
group.append(g)
def generate_wall_layers(self, context: bpy.types.Context, root):
def generate_wall_layers(self, context: bpy.types.Context, root) -> None:
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
if "projection" in el.get("class", "").split():
continue
@@ -1570,12 +1607,6 @@ class CreateDrawing(bpy.types.Operator):
def is_landscape(self, render):
return render.resolution_x > render.resolution_y
def get_view_3d(self, areas):
for area in areas:
if area.type != "VIEW_3D":
continue
return area.spaces.active
def get_material_name(self, element: ifcopenshell.entity_instance) -> str:
if hasattr(element, "Name") and element.Name:
return element.Name
@@ -1613,8 +1644,9 @@ class AddAnnotation(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = tool.Drawing.get_annotation_props()
if not (drawing := tool.Ifc.get_entity(context.scene.camera)):
self.report({"WARNING"}, "Not a BIM camera")
dprops = tool.Drawing.get_document_props()
if not (drawing := dprops.get_active_drawing()):
self.report({"WARNING"}, "No active drawing.")
return
obj = core.add_annotation(
@@ -1717,7 +1749,7 @@ class SelectAllSheets(bpy.types.Operator):
return {"FINISHED"}
class OpenSheet(bpy.types.Operator, tool.Ifc.Operator):
class OpenSheet(bpy.types.Operator):
bl_idname = "bim.open_sheet"
bl_label = "Open Sheet"
bl_description = (
@@ -2139,6 +2171,7 @@ class ActivateDrawingBase(tool.Ifc.Operator):
return self.execute(context)
def _execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
assert context.scene
props = tool.Drawing.get_document_props()
if props.is_editing_drawings == False:
bpy.ops.bim.load_drawings()
@@ -2150,17 +2183,17 @@ class ActivateDrawingBase(tool.Ifc.Operator):
tool.Blender.activate_camera(tool.Drawing.import_temporary_drawing_camera(drawing))
return {"FINISHED"}
viewport_position = None
if not self.should_view_from_camera:
viewport_position = tool.Blender.get_viewport_position()
core.activate_drawing_view(tool.Ifc, tool.Blender, tool.Drawing, drawing=drawing)
if not self.should_view_from_camera:
assert viewport_position
tool.Blender.set_viewport_position(viewport_position)
dprops.active_drawing_id = self.drawing
# reset DrawingsData to reload_drawing_styles work correctly
DrawingsData.is_loaded = False
dprops.drawing_styles.clear()
if ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "HasUnderlay"):
bpy.ops.bim.reload_drawing_styles()
@@ -2173,6 +2206,7 @@ class ActivateDrawingBase(tool.Ifc.Operator):
# Save drawing bounds to the .ifc file
camera = context.scene.camera
assert camera
camera_props = tool.Drawing.get_camera_props(camera)
if camera_props.update_representation(camera.matrix_world):
bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="")
@@ -2196,13 +2230,6 @@ class ActivateDrawing(bpy.types.Operator, ActivateDrawingBase):
should_view_from_camera: bpy.props.BoolProperty(name="Should View From Camera", default=True, options={"SKIP_SAVE"})
use_quick_preview: bpy.props.BoolProperty(name="Use Quick Preview", default=False, options={"SKIP_SAVE"})
@classmethod
def poll(cls, context):
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
return True
class ActivateDrawingFromSheet(bpy.types.Operator, ActivateDrawingBase):
bl_idname = "bim.activate_drawing_from_sheet"
@@ -2243,19 +2270,6 @@ class SelectDocIfcFile(bpy.types.Operator, ImportHelper):
return {"FINISHED"}
class ResizeText(bpy.types.Operator):
bl_idname = "bim.resize_text"
bl_label = "Resize Text"
bl_options = {"REGISTER", "UNDO"}
# TODO: check undo redo
def execute(self, context):
for obj in tool.Blender.get_object_bim_props(context.scene.camera).collection.objects:
if isinstance(obj.data, bpy.types.TextCurve):
annotation.Annotator.resize_text(obj)
return {"FINISHED"}
class RemoveDrawing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_drawing"
bl_label = "Remove Drawing"
@@ -2303,37 +2317,25 @@ class RemoveDrawing(bpy.types.Operator, tool.Ifc.Operator):
props.should_draw_decorations = False
class DrawingStyleJson(TypedDict):
render_type: "RenderType"
raster_style: dict[str, Any]
class ReloadDrawingStyles(bpy.types.Operator):
bl_idname = "bim.reload_drawing_styles"
bl_label = "Reload Drawing Styles"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Reload drawing styles for the active camera"
bl_description = "Reload drawing styles for the active camera from the related JSON file."
def execute(self, context):
if not DrawingsData.is_loaded:
DrawingsData.load()
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
props = tool.Drawing.get_document_props()
assert (drawing := props.get_active_drawing())
drawing_pset_data = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing")
assert context.scene and (camera := context.scene.camera)
camera_props = tool.Drawing.get_camera_props(camera)
# added this part as a temporary fallback
# TODO: should remove it a bit later when projects get more accommodated
# with saving shadingstyles to ifc and separate json file
if "ShadingStyles" not in drawing_pset_data:
ifc_file = tool.Ifc.get()
pset = ifc_file.by_id(drawing_pset_data["id"])
edit_properties = {
"ShadingStyles": (
shading_styles_path := tool.Drawing.get_default_drawing_resource_path("ShadingStyles")
),
"CurrentShadingStyle": tool.Drawing.get_default_shading_style(),
}
ifcopenshell.api.run("pset.edit_pset", ifc_file, pset=pset, properties=edit_properties)
tool.Drawing.setup_shading_styles_path(shading_styles_path)
DrawingsData.load()
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
if "ShadingStyles" not in drawing_pset_data:
self.report({"ERROR"}, "Could not find shading styles path in EPset_Drawing.ShadingStyles.")
return {"CANCELLED"}
@@ -2353,9 +2355,8 @@ class ReloadDrawingStyles(bpy.types.Operator):
shutil.copy(ootb_resource, json_path)
with open(json_path, "r") as fi:
shading_styles_json = json.load(fi)
shading_styles_json: dict[str, DrawingStyleJson] = json.load(fi)
props = tool.Drawing.get_document_props()
drawing_styles = props.drawing_styles
drawing_styles.clear()
styles = [style for style in shading_styles_json]
@@ -2367,20 +2368,22 @@ class ReloadDrawingStyles(bpy.types.Operator):
drawing_style.raster_style = json.dumps(style_data["raster_style"])
if current_style is not None:
try:
if current_style not in styles:
self.report({"WARNING"}, f"Could not find style {current_style} in EPset_Drawing.ShadingStyles.")
else:
camera_props.active_drawing_style_index = styles.index(current_style)
except ValueError:
self.report({"INFO"}, f"Could not find style {current_style} in EPset_Drawing.ShadingStyles.")
return {"FINISHED"}
# NOTE: Ifc Operator is not necessary for add and remove,
# as underlying save operator creates ifc undo step for us,
# but we keep it to make it more safe in case operators composition will change.
class AddDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_drawing_style"
bl_label = "Add Drawing Style"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
def _execute(self, context):
assert context.scene and (camera_obj := context.scene.camera)
props = tool.Drawing.get_document_props()
drawing_styles = props.drawing_styles
@@ -2398,7 +2401,7 @@ class RemoveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty()
def execute(self, context):
def _execute(self, context):
assert context.scene and (camera_obj := context.scene.camera)
props = tool.Drawing.get_document_props()
props.drawing_styles.remove(self.index)
@@ -2412,13 +2415,13 @@ class SaveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.save_drawing_style"
bl_label = "Save Drawing Style"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Save current render setting to currently selected drawing style. Also resaves styles data to IFC"
bl_description = "Save current render settings to currently selected drawing style. Also resaves styles data to IFC"
index: bpy.props.StringProperty()
# TODO: check undo redo
def execute(self, context):
space = self.get_view_3d(context) # Do not remove. It is used later in eval
def _execute(self, context):
assert (space := tool.Blender.get_view3d_space()) # Do not remove. It is used later in eval
scene = context.scene
assert scene
style = {}
@@ -2467,16 +2470,11 @@ class SaveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.save_drawing_styles_data()
return {"FINISHED"}
def get_view_3d(self, context):
for area in context.screen.areas:
if area.type != "VIEW_3D":
continue
return area.spaces.active
class SaveDrawingStylesData(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.save_drawing_styles_data"
bl_label = "Save Drawing Styles Data"
bl_description = "Save current drawing styles settings to IFC and JSON."
bl_options = {"REGISTER", "UNDO"}
skip_updating_current_style: bpy.props.BoolProperty(default=False)
@@ -2484,11 +2482,10 @@ class SaveDrawingStylesData(bpy.types.Operator, tool.Ifc.Operator):
rename_style_from: bpy.props.StringProperty(default="")
rename_style_to: bpy.props.StringProperty(default="")
def execute(self, context):
if not DrawingsData.is_loaded:
DrawingsData.load()
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
def _execute(self, context):
props = tool.Drawing.get_document_props()
assert (drawing := props.get_active_drawing())
drawing_pset_data = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing")
drawing_styles = props.drawing_styles
rel_path = drawing_pset_data["ShadingStyles"]
@@ -2498,10 +2495,9 @@ class SaveDrawingStylesData(bpy.types.Operator, tool.Ifc.Operator):
self.report({"ERROR"}, "Shading styles file not found: {}".format(json_path))
return {"CANCELLED"}
styles_data = {}
styles_data: dict[str, DrawingStyleJson] = {}
for style in drawing_styles:
style_data = {"render_type": style.render_type, "raster_style": json.loads(style.raster_style)}
styles_data[style.name] = style_data
styles_data[style.name] = {"render_type": style.render_type, "raster_style": json.loads(style.raster_style)}
with open(json_path, "w") as fo:
json.dump(styles_data, fo, indent=4)
@@ -2518,11 +2514,10 @@ class SaveDrawingStylesData(bpy.types.Operator, tool.Ifc.Operator):
new_style_name = None
ifc_file = tool.Ifc.get()
drawing = ifc_file.by_id(props.active_drawing_id)
assert (drawing := props.get_active_drawing())
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
ifcopenshell.api.run(
"pset.edit_pset", ifc_file, pset=pset, properties={"CurrentShadingStyle": new_style_name}
)
assert pset
ifcopenshell.api.pset.edit_pset(ifc_file, pset=pset, properties={"CurrentShadingStyle": new_style_name})
bonsai.bim.handler.refresh_ui_data()
return {"FINISHED"}
@@ -2533,44 +2528,115 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Activate Drawing Style"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
has_errors_during_activation = False
has_warnings_during_activation = False
def _execute(self, context):
scene = context.scene
assert scene and (camera := scene.camera)
camera_props = tool.Drawing.get_camera_props(camera)
ifc_file = tool.Ifc.get()
active_drawing_style_index = camera_props.active_drawing_style_index
props = tool.Drawing.get_document_props()
if active_drawing_style_index >= len(props.drawing_styles):
drawing_style = camera_props.get_active_drawing_style()
if drawing_style is None:
self.report({"ERROR"}, "Could not find active drawing style")
return {"CANCELLED"}
self.drawing_style = props.drawing_styles[active_drawing_style_index]
self.drawing_style = drawing_style
self.set_raster_style(context)
self.set_query(context)
drawing = ifc_file.by_id(props.active_drawing_id)
props = tool.Drawing.get_document_props()
assert (drawing := props.get_active_drawing())
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
ifcopenshell.api.run(
"pset.edit_pset", ifc_file, pset=pset, properties={"CurrentShadingStyle": self.drawing_style.name}
assert pset
ifcopenshell.api.pset.edit_pset(
ifc_file, pset=pset, properties={"CurrentShadingStyle": self.drawing_style.name}
)
bonsai.bim.handler.refresh_ui_data()
if self.has_warnings_during_activation:
self.report(
{"WARNING"},
"There were warnings setting some drawing style properies, see system console for the details.",
)
if self.has_errors_during_activation:
self.report(
{"WARNING"},
"There were errors setting some drawing style properies, see system console for the details.",
)
return {"FINISHED"}
def set_raster_style(self, context: bpy.types.Context) -> None:
scene = context.scene # Do not remove. It is used in exec later
space = self.get_view_3d(context) # Do not remove. It is used in exec later
assert (space := tool.Blender.get_view3d_space()) # Do not remove. It is used in exec later
style = json.loads(self.drawing_style.raster_style)
def preprocess(path: str, value: Any) -> tuple[str, Any, bool, bool]:
warning = False
skip = False
# @25.05.12
if path == "scene.render.engine" and value == "BLENDER_EEVEE":
value = "BLENDER_EEVEE_NEXT"
print(
f"Warning: Value 'BLENDER_EEVEE' is outdated for property '{path}' "
"since Blender 4.2 and should be replaced with 'BLENDER_EEVEE_NEXT' in shading_styles.json."
)
warning = True
# @25.05.12
if path == "scene.display.shading.wireframe_color_type" and value == "MATERIAL":
value = "THEME"
print(
f"Warning: Value 'MATERIAL' is outdated for property '{path}' "
"since Blender 4.0 and should be replaced with 'THEME' in shading_styles.json."
)
warning = True
# @25.05.12
elif path in ("scene.render.simplify_shadows", "scene.render.simplify_shadows_render"):
print(
f"Warning: Property '{path}' is removed "
"since Blender 4.2 and should be also removed from shading_styles.json."
)
warning = True
skip = True
# @25.05.12
elif path in ("space.overlay.backwire_opacity", "space.overlay.show_edges"):
print(
f"Warning: Property '{path}' is removed "
"since Blender 4.1 and should be also removed from shading_styles.json."
)
warning = True
skip = True
# @25.05.12
elif path in ("space.overlay.show_occlude_wire",):
print(
f"Warning: Property '{path}' is removed "
"since Blender 3.6 and should be also removed from shading_styles.json."
)
warning = True
skip = True
return path, value, warning, skip
for path, value in style.items():
path, value, warning, skip = preprocess(path, value)
self.has_warnings_during_activation |= warning
if skip:
continue
try:
if isinstance(value, str):
exec(f"{path} = '{value}'")
else:
exec(f"{path} = {value}")
except:
except Exception as e:
# Differences in Blender versions mean result in failures here
print(f"Failed to set shading style {path} to {value}")
print(f"Failed to set drawing style property '{path}' to '{value}'. Error: '{str(e)}'")
self.has_errors_during_activation = True
if "PYTEST_VERSION" in os.environ:
raise
def set_query(self, context: bpy.types.Context) -> None:
self.include_global_ids = []
@@ -2594,6 +2660,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
def parse_filter_query(self, mode: Literal["INCLUDE", "EXCLUDE"], context: bpy.types.Context) -> None:
if mode == "INCLUDE":
assert context.scene
objects = context.scene.objects
elif mode == "EXCLUDE":
objects = context.visible_objects
@@ -2611,13 +2678,6 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
if global_id in self.exclude_global_ids:
obj.hide_viewport = True # Note: this breaks alt-H
def get_view_3d(self, context: bpy.types.Context) -> bpy.types.SpaceView3D:
for area in context.screen.areas:
if area.type != "VIEW_3D":
continue
return area.spaces.active
assert False, "Space is not found."
class RemoveSheet(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_sheet"
@@ -2851,35 +2911,6 @@ class OpenReference(bpy.types.Operator, tool.Ifc.Operator):
core.open_reference(tool.Drawing, reference=tool.Ifc.get().by_id(self.reference))
# TODO: dead code - drawing style attributes are never used?
class AddDrawingStyleAttribute(bpy.types.Operator):
bl_idname = "bim.add_drawing_style_attribute"
bl_label = "Add Drawing Style Attribute"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
assert context.scene and (camera := context.scene.camera)
props = tool.Drawing.get_camera_props(camera)
dprops = tool.Drawing.get_document_props()
dprops.drawing_styles[props.active_drawing_style_index].attributes.add()
return {"FINISHED"}
# TODO: dead code - drawing style attributes are never used?
class RemoveDrawingStyleAttribute(bpy.types.Operator):
bl_idname = "bim.remove_drawing_style_attribute"
bl_label = "Remove Drawing Style Attribute"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty()
def execute(self, context):
assert context.scene and (camera := context.scene.camera)
props = tool.Drawing.get_camera_props(camera)
dprops = tool.Drawing.get_document_props()
dprops.drawing_styles[props.active_drawing_style_index].attributes.remove(self.index)
return {"FINISHED"}
class CleanWireframes(bpy.types.Operator):
bl_idname = "bim.clean_wireframes"
bl_label = "Clean Wireframes"
@@ -3090,6 +3121,7 @@ class OrderTextLiteralDown(bpy.types.Operator):
return {"FINISHED"}
# Ifc Operator is unnecessary, because suboperator is handling IFC changes.
class AssignSelectedObjectAsProduct(bpy.types.Operator):
bl_idname = "bim.assign_selected_as_product"
bl_label = "Assign Selected Object As Product"
@@ -3103,18 +3135,25 @@ class AssignSelectedObjectAsProduct(bpy.types.Operator):
return True
def execute(self, context):
assert bpy.context.view_layer
objs = context.selected_objects[:]
obj1 = objs[0]
obj1, obj2 = objs
element1 = tool.Ifc.get_entity(obj1)
obj2 = objs[1]
element2 = tool.Ifc.get_entity(obj2)
assert element1 and element2
if element1.is_a("IfcAnnotation"):
other_selected_object = obj2
bpy.context.view_layer.objects.active = obj1
elif element2.is_a("IfcAnnotation"):
other_selected_object = obj1
bpy.context.view_layer.objects.active = obj2
context.active_object.BIMAssignedProductProperties.relating_product = other_selected_object
else:
self.report({"ERROR"}, "One of the selected objects must be IfcAnnotation.")
return {"CANCELLED"}
assert (active_obj := context.active_object)
props = tool.Drawing.get_object_assigned_product_props(active_obj)
props.relating_product = other_selected_object
bpy.ops.bim.edit_assigned_product()
return {"FINISHED"}
@@ -3126,9 +3165,11 @@ class EditAssignedProduct(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
product = None
if context.active_object.BIMAssignedProductProperties.relating_product:
product = tool.Ifc.get_entity(context.active_object.BIMAssignedProductProperties.relating_product)
core.edit_assigned_product(tool.Ifc, tool.Drawing, obj=context.active_object, product=product)
assert (obj := context.active_object)
props = tool.Drawing.get_object_assigned_product_props(obj)
if props.relating_product:
product = tool.Ifc.get_entity(props.relating_product)
core.edit_assigned_product(tool.Ifc, tool.Drawing, obj=obj, product=product)
tool.Blender.update_viewport()
@@ -3138,6 +3179,7 @@ class EnableEditingAssignedProduct(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
assert context.active_object
core.enable_editing_assigned_product(tool.Drawing, obj=context.active_object)
@@ -3147,6 +3189,7 @@ class DisableEditingAssignedProduct(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
assert context.active_object
core.disable_editing_assigned_product(tool.Drawing, obj=context.active_object)
@@ -3190,6 +3233,7 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
document_type: Literal["SHEET", "TITLEBLOCK", "EMBEDDED"]
def invoke(self, context, event):
assert context.window_manager
self.props = tool.Drawing.get_document_props()
sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)
if sheet.is_a("IfcDocumentInformation"):
@@ -3452,12 +3496,13 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
assert element
pset = tool.Pset.get_element_pset(element, "EPset_Drawing")
assert pset
if self.filter_mode == "INCLUDE":
query = tool.Search.export_filter_query(props.include_filter_groups) or None
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Include": query})
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Include": query})
elif self.filter_mode == "EXCLUDE":
query = tool.Search.export_filter_query(props.exclude_filter_groups) or None
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Exclude": query})
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Exclude": query})
props.filter_mode = "NONE"
bpy.ops.bim.activate_drawing(drawing=element.id(), should_view_from_camera=False)
@@ -3555,8 +3600,7 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
"Transparency": 0.0,
"ReflectanceMethod": "NOTDEFINED",
}
ifcopenshell.api.run(
"style.add_surface_style",
ifcopenshell.api.style.add_surface_style(
tool.Ifc.get(),
style=style,
ifc_class="IfcSurfaceStyleRendering",
+62 -41
View File
@@ -46,7 +46,7 @@ from bpy.props import (
CollectionProperty,
BoolVectorProperty,
)
from typing import TYPE_CHECKING, Literal, Any, Callable, get_args
from typing import TYPE_CHECKING, Literal, Any, Callable, get_args, Union
diagram_scales_enum = []
@@ -57,7 +57,7 @@ def purge():
diagram_scales_enum = []
def update_target_view(self: "DocProperties", context: bpy.types.Context) -> None:
def update_target_view_doc(self: "DocProperties", context: bpy.types.Context) -> None:
DrawingsData.data["location_hint"] = DrawingsData.location_hint()
@@ -211,6 +211,8 @@ def update_document_name(self: "Document", context: bpy.types.Context) -> None:
def update_has_underlay(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
if not self.update_props:
return
update_layer(self, context, "HasUnderlay", self.has_underlay)
assert context.scene
# making sure that camera is active
@@ -228,32 +230,12 @@ def get_update_layer_callback(
return update_layer_callback
def update_has_linework(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
update_layer(self, context, "HasLinework", self.has_linework)
def update_target_view(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
if self.target_view != "MODEL_VIEW":
def update_target_view_camera(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
if self.update_props and self.target_view != "MODEL_VIEW":
self.camera_type = "ORTHO"
update_layer(self, context, "TargetView", self.target_view)
def update_dpi(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
update_layer(self, context, "DPI", self.dpi)
def update_linework_mode(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
update_layer(self, context, "LineworkMode", self.linework_mode)
def update_fill_mode(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
update_layer(self, context, "FillMode", self.fill_mode)
def update_cut_mode(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
update_layer(self, context, "CutMode", self.cut_mode)
def update_layer(self: "BIMCameraProperties", context: bpy.types.Context, name: str, value: Any) -> None:
assert context.scene
if not self.update_props:
@@ -366,21 +348,34 @@ class Sheet(PropertyGroup):
is_expanded: bool
RenderType = Literal["DEFAULT", "VIEWPORT"]
RENDER_TYPE_ENUM_ITEMS: dict[RenderType, tuple[str, str]] = {
"DEFAULT": ("Default", "Use default Blender render."),
"VIEWPORT": ("Viewport", "Render active viewport (using 'Rendered' viewport shading type)."),
}
class DrawingStyle(PropertyGroup):
name: StringProperty(name="Name", get=get_drawing_style_name, set=set_drawing_style_name)
raster_style: StringProperty(name="Raster Style", default="{}")
raster_style: StringProperty(
name="Raster Style",
description="JSON string for drawing style settings.",
default="{}",
)
render_type: EnumProperty(
items=[
("NONE", "None", ""),
("DEFAULT", "Default", ""),
("VIEWPORT", "Viewport", ""),
],
items=[(k, *v) for k, v in RENDER_TYPE_ENUM_ITEMS.items()],
name="Render Type",
default="VIEWPORT",
)
include_query: StringProperty(name="Include Query")
exclude_query: StringProperty(name="Exclude Query")
attributes: CollectionProperty(name="Attributes", type=StrProperty)
if TYPE_CHECKING:
name: str
raster_style: str
render_type: RenderType
include_query: str
exclude_query: str
class RasterStyleProperty(enum.Enum):
@@ -410,7 +405,7 @@ class DocProperties(PropertyGroup):
items=TARGET_VIEW_ITEMS,
name="Target View",
default="PLAN_VIEW",
update=update_target_view,
update=update_target_view_doc,
)
location_hint: EnumProperty(items=get_location_hint, name="Location Hint")
drawings: CollectionProperty(name="Drawings", type=Drawing)
@@ -456,6 +451,11 @@ class DocProperties(PropertyGroup):
name="Classes to Wireframe",
description="Upon import, these classes will display as wireframe.\nEx: IfcVirtualelement, IfcSpace",
)
classes_no_cut: StringProperty(
default="IfcVirtualElement, IfcSpace",
name="Classes that are not cut",
description="The cut decoractor will be turned off for these classes\nEx: IfcVirtualelement, IfcSpace",
)
if TYPE_CHECKING:
should_use_underlay_cache: bool
@@ -465,7 +465,7 @@ class DocProperties(PropertyGroup):
is_editing_schedules: bool
is_editing_references: bool
target_view: Literal["PLAN_VIEW", "ELEVATION_VIEW", "SECTION_VIEW", "REFLECTED_PLAN_VIEW", "MODEL_VIEW"]
location_hint: str
location_hint: tool.Drawing.LocationHintType
drawings: bpy.types.bpy_prop_collection_idprop[Drawing]
active_drawing_id: int
active_drawing_index: int
@@ -497,6 +497,13 @@ class DocProperties(PropertyGroup):
imperial_precision: str
tolerance: float
classes_to_wireframe: str
classes_no_cut: str
def get_active_drawing(self) -> Union[ifcopenshell.entity_instance, None]:
drawing_id = self.active_drawing_id
if drawing_id == 0:
return None
return tool.Ifc.get().by_id(drawing_id)
def update_width_height(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
@@ -510,7 +517,7 @@ def update_camera_type(self: "BIMCameraProperties", context: bpy.types.Context)
CameraType = Literal["PERSP", "ORTHO"]
CAMERA_TYPE_ENUM_ITEMS: dict[CameraType, tuple[str, str]] = {
"ORTHO": ("Ortographic", "Most common camera for the drawings, supporting all of the features."),
"ORTHO": ("Orthographic", "Most common camera for the drawings, supporting all of the features."),
"PERSP": ("Perspective", "The only avilable features for perspective camera: freestyle linework, underlay."),
}
@@ -523,7 +530,7 @@ class BIMCameraProperties(PropertyGroup):
],
default="OPENCASCADE",
name="Linework Mode",
update=update_linework_mode,
update=get_update_layer_callback("linework_mode", "LineworkMode"),
)
fill_mode: EnumProperty(
items=[
@@ -533,7 +540,7 @@ class BIMCameraProperties(PropertyGroup):
],
default="NONE",
name="Fill Mode",
update=update_fill_mode,
update=get_update_layer_callback("fill_mode", "FillMode"),
)
cut_mode: EnumProperty(
items=[
@@ -542,14 +549,14 @@ class BIMCameraProperties(PropertyGroup):
],
default="BISECT",
name="Cut Mode",
update=update_cut_mode,
update=get_update_layer_callback("cut_mode", "CutMode"),
)
# EPset_Drawing.
has_underlay: BoolProperty(
name="Underlay",
default=False,
update=get_update_layer_callback("has_underlay", "HasUnderlay"),
update=update_has_underlay,
)
has_linework: BoolProperty(
name="Linework",
@@ -565,7 +572,7 @@ class BIMCameraProperties(PropertyGroup):
name="Target View",
default="PLAN_VIEW",
items=TARGET_VIEW_ITEMS,
update=update_target_view,
update=update_target_view_camera,
)
representation: StringProperty(name="Representation")
@@ -575,7 +582,7 @@ class BIMCameraProperties(PropertyGroup):
custom_scale_denominator: bpy.props.StringProperty(default="100", update=update_diagram_scale)
raster_x: IntProperty(name="Raster X", default=1000)
raster_y: IntProperty(name="Raster Y", default=1000)
dpi: IntProperty(name="DPI", default=75, update=update_dpi)
dpi: IntProperty(name="DPI", default=75, update=get_update_layer_callback("dpi", "DPI"))
width: FloatProperty(name="Width", default=50, subtype="DISTANCE", update=update_width_height)
height: FloatProperty(name="Height", default=50, subtype="DISTANCE", update=update_width_height)
# Bonsai property is needed to prevent user from using unsupported panoramic camera.
@@ -624,6 +631,10 @@ class BIMCameraProperties(PropertyGroup):
exclude_filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
update_props: bool
def get_active_drawing_style(self) -> Union[DrawingStyle, None]:
dprops = tool.Drawing.get_document_props()
return tool.Blender.get_active_uilist_element(dprops.drawing_styles, self.active_drawing_style_index)
# For now, this JSON dump are all the parameters that determine a camera's "Block representation"
# By checking this, you will know whether or not the camera IFC representation needs to be refreshed
def update_representation(self, matrix_world: Matrix) -> bool:
@@ -749,6 +760,12 @@ class LiteralProps(PropertyGroup):
}
return text_data
if TYPE_CHECKING:
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
value: str
box_alignment: str
ifc_definition_id: int
class BIMTextProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing", default=False)
@@ -774,7 +791,7 @@ class BIMTextProperties(PropertyGroup):
def get_text_edited_data(self) -> dict[str, Any]:
"""should be called only if `is_editing`
otherwise should use `DecoratorData.get_ifc_text_data(obj)` instead
otherwise should use `DecoratorData.get_text_data(obj)` instead
because this data could be out of date
"""
literals_data = []
@@ -799,6 +816,10 @@ class BIMAssignedProductProperties(PropertyGroup):
is_editing_product: BoolProperty(name="Is Editing Product", default=False)
relating_product: PointerProperty(name="Relating Product", type=bpy.types.Object, poll=relating_product_poll)
if TYPE_CHECKING:
is_editing_product: bool
relating_product: Union[bpy.types.Object, None]
annotation_classes = [
(x, *tool.Drawing.ANNOTATION_TYPES_DATA[x][:3], i) for i, x in enumerate(tool.Drawing.ANNOTATION_TYPES_DATA)
@@ -22,6 +22,7 @@ import bpy
import math
import bmesh
import shutil
from bpy.types import SplineBezierPoints, SplinePoints
import mathutils
import xml.etree.ElementTree as ET
import svgwrite
@@ -37,7 +38,7 @@ from bonsai.bim.module.drawing.data import DrawingsData
from bonsai.bim.module.drawing.data import DecoratorData
from math import pi, ceil, atan, degrees, acos
from mathutils import geometry, Vector
from typing import Optional, Self
from typing import Optional, Self, Union, Sequence, Callable
from pathlib import Path
@@ -59,14 +60,29 @@ class External(svgwrite.container.Group):
class SvgWriter:
def __init__(self):
metadata: list[str]
resource_paths: dict[tool.Drawing.ResourceType, Union[str, None]]
def __init__(
self,
*,
camera_width: float,
camera_height: float,
camera: bpy.types.Object,
camera_projection: Vector,
scale: float = 1 / 100, # 1:100
human_scale: str = "NTS",
):
self.data_dir = None
self.human_scale = "NTS"
self.human_scale = human_scale
self.metadata = []
self.scale = 1 / 100 # 1:100
self.camera_width = None
self.camera_height = None
self.scale = scale
self.camera = camera
self.camera_width = camera_width
self.camera_height = camera_height
self.camera_projection = camera_projection
self.resource_paths = {}
self.calculate_scale()
def create_blank_svg(self, output_path: str) -> Self:
self.calculate_scale()
@@ -90,7 +106,7 @@ class SvgWriter:
def setup_drawing_resource_paths(self, element: ifcopenshell.entity_instance) -> None:
pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing")
for resource in ("Stylesheet", "Markers", "Symbols", "Patterns", "ShadingStyles"):
for resource in tool.Drawing.RESOURCE_TYPES:
resource_path = pset.get(resource)
if not resource_path:
self.resource_paths[resource] = None
@@ -176,12 +192,20 @@ class SvgWriter:
for child in root:
self.svg.defs.add(External(child))
def draw_annotations(self, annotations, precision, decimal_places):
def draw_annotations(
self,
annotations: list[ifcopenshell.entity_instance],
precision: Union[float, None],
decimal_places: Union[int, None],
) -> Self:
self.precision = precision
self.decimal_places = decimal_places
for element in annotations:
obj = tool.Ifc.get_object(element)
if not obj or (object_type := ifcopenshell.util.element.get_predefined_type(element)) == "DRAWING":
if (
not isinstance(obj, bpy.types.Object)
or (object_type := ifcopenshell.util.element.get_predefined_type(element)) == "DRAWING"
):
continue
elif object_type == "GRID":
self.draw_grid_annotation(obj)
@@ -256,11 +280,12 @@ class SvgWriter:
get_text, tag, dimension_data, text_position=text_position, angle=angle, class_str="SECTIONLEVEL"
)
def draw_stair_annotation(self, obj):
def draw_stair_annotation(self, obj: bpy.types.Object) -> None:
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
matrix_world = obj.matrix_world
classes = self.get_attribute_classes(obj)
assert isinstance(obj.data, bpy.types.Curve)
for spline in obj.data.splines:
points = self.get_spline_points(spline)
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
@@ -284,7 +309,7 @@ class SvgWriter:
)
)
def draw_grid_annotation(self, obj):
def draw_grid_annotation(self, obj: bpy.types.Object) -> None:
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
matrix_world = obj.matrix_world
@@ -358,6 +383,7 @@ class SvgWriter:
def get_attribute_classes(self, obj: bpy.types.Object) -> list[str]:
element = tool.Ifc.get_entity(obj)
assert element
global_id = "GlobalId-{}".format(element.GlobalId)
predefined_type = "PredefinedType-" + tool.Drawing.canonicalise_class_name(
str(ifcopenshell.util.element.get_predefined_type(element))
@@ -374,7 +400,7 @@ class SvgWriter:
)
return classes
def draw_line_annotation(self, obj):
def draw_line_annotation(self, obj: bpy.types.Object) -> None:
# TODO: properly scope these offsets
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
@@ -397,7 +423,7 @@ class SvgWriter:
elif isinstance(obj.data, bpy.types.Mesh):
self.draw_edge_annotation(obj, classes)
def draw_edge_annotation(self, obj, classes):
def draw_edge_annotation(self, obj: bpy.types.Object, classes: list[str]) -> None:
predefined_type = classes[2].split("-", 1)[1]
x_offset = self.raw_width / 2
@@ -415,7 +441,7 @@ class SvgWriter:
self.svg.line(start=start * self.svg_scale, end=end * self.svg_scale, class_=" ".join(classes))
)
def draw_batting_annotation():
def draw_batting_annotation() -> None:
v0_global = matrix_world @ obj.data.vertices[0].co.xyz
v1_global = matrix_world @ obj.data.vertices[1].co.xyz
v0 = self.project_point_onto_camera(v0_global)
@@ -481,41 +507,42 @@ class SvgWriter:
)
self.svg.add(self.svg.polyline(points=points, class_=" ".join(classes), style=polyline_style))
def draw_revision_cloud_annotation():
def draw_revision_cloud_annotation() -> None:
segment_width = 15.0
base_height = 1
width = 5
def get_svg_half_circle(height, width):
def get_svg_half_circle(height: float, width: float) -> str:
cp0 = f"0,-{height}"
cp1 = f"{width},-{height}"
end_point = f"{width},0"
circle = f"c{cp0} {cp1} {end_point}"
return circle
def get_revision_pattern(base_offset):
def get_revision_pattern(base_offset: Vector) -> str:
pattern = f"m{base_offset.x},{base_offset.y}"
pattern += " " + get_svg_half_circle(2 * base_height, width)
pattern += " " + get_svg_half_circle(2.5 * base_height, width)
pattern += " " + get_svg_half_circle(1.5 * base_height, width)
return pattern
def get_scale(size, direction):
def get_scale(size: float, direction: Vector) -> str:
original_edge = direction * size
current_svg_segments = ceil(size / segment_width) * segment_width * direction
scale = [1 if original_edge[i] == 0 else original_edge[i] / current_svg_segments[i] for i in range(2)]
return "scale(%f, %f)" % (scale[0], scale[1])
def poly_to_edges(poly):
edges = []
def poly_to_edges(poly: Sequence[int]) -> list[tuple[int, int]]:
edges: list[tuple[int, int]] = []
n_verts = len(poly)
lats_index = n_verts - 1
last_index = n_verts - 1
for i in range(len(poly)):
edge = [poly[i], (poly[i + 1]) if i != lats_index else poly[0]]
edge = (poly[i], (poly[i + 1]) if i != last_index else poly[0])
edges.append(edge)
return edges
element = tool.Ifc.get_entity(obj)
assert element
safe_offset_x = 2.0
marker_width = segment_width + safe_offset_x * 2
market_height = 15.0
@@ -573,7 +600,7 @@ class SvgWriter:
)
self.svg.add(polyline)
def draw_section_annotation():
def draw_section_annotation() -> None:
display_data = DecoratorData.get_section_markers_display_data(obj)
connect_markers = display_data["connect_markers"]
@@ -624,7 +651,7 @@ class SvgWriter:
for edge in obj.data.edges:
draw_simple_edge_annotation(*edge.vertices[:])
def draw_leader_annotation(self, obj):
def draw_leader_annotation(self, obj: bpy.types.Object) -> None:
self.draw_line_annotation(obj)
spline = obj.data.splines[0]
spline_points = spline.bezier_points if spline.bezier_points else spline.points
@@ -634,7 +661,7 @@ class SvgWriter:
position = Vector((0, 0, 0))
self.draw_text_annotation(obj, position)
def draw_section_annotation(self, obj):
def draw_section_annotation(self, obj: bpy.types.Object) -> None:
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
@@ -688,7 +715,7 @@ class SvgWriter:
)
)
def draw_elevation_annotation(self, obj):
def draw_elevation_annotation(self, obj: bpy.types.Object) -> None:
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
symbol_position = self.project_point_onto_camera(obj.location)
@@ -927,7 +954,7 @@ class SvgWriter:
symbol_transform = self.get_symbol_transform(svg_position_str)
self.draw_symbol(svg_id, symbol_transform)
def draw_break_annotations(self, obj):
def draw_break_annotations(self, obj: bpy.types.Object) -> None:
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
@@ -950,18 +977,20 @@ class SvgWriter:
d = "M{}".format(d[1:])
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
def draw_plan_level_annotation(self, obj):
def draw_plan_level_annotation(self, obj: bpy.types.Object) -> None:
offset = Vector([self.raw_width, self.raw_height]) / 2
matrix_world = obj.matrix_world
classes = self.get_attribute_classes(obj)
element = tool.Ifc.get_entity(obj)
assert element
description = element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
suppress_zero_inches = dimension_data["suppress_zero_inches"]
base_offset_y = 1.0
assert isinstance(obj.data, bpy.types.Curve)
for spline in obj.data.splines:
points = self.get_spline_points(spline)
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
@@ -1000,7 +1029,8 @@ class SvgWriter:
box_alignment=box_alignment,
)
def draw_angle_annotations(self, obj):
def draw_angle_annotations(self, obj: bpy.types.Object) -> None:
assert isinstance(obj.data, bpy.types.Curve)
points = obj.data.splines[0].points
# [1, 2, 3, 4, 5] -> [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
points_chunked = [points[i : i + 3] for i in range(len(points) - 2)]
@@ -1009,18 +1039,18 @@ class SvgWriter:
points_chunk = [obj.matrix_world @ p.co.xyz for p in points_chunk]
self.draw_svg_3point_arc(obj, points_chunk)
def draw_svg_3point_arc(self, obj, angle_points):
def draw_svg_3point_arc(self, obj: bpy.types.Object, angle_points: list[Vector]) -> None:
"""`angle_points` are expected to be already in world space"""
# This implementation uses an SVG arc, which means that it can only draw
# arcs that are orthogonal to the view (e.g. not arcs in 3D).
# Gosh this is bad code :(
def position_on_svg(p):
def position_on_svg(p: Vector) -> Vector:
p = self.project_point_onto_camera(p)
offset = Vector([self.raw_width / 2, self.raw_height / 2])
return (offset + p.xy * Vector((1, -1))) * self.svg_scale
def get_angle_value():
def get_angle_value() -> float:
"""points should be in world space"""
# calculate arc angle, need to make sure we do it in world space
v0, v1, v2 = angle_points
@@ -1068,7 +1098,7 @@ class SvgWriter:
# When rewriting to use polylines, we should use this normal instead.
# center = tool.Cad.get_center_of_arc([p.to_3d() for p in arc_points], None).to_2d()
# normal = mathutils.geometry.normal([arc_end_pts[0], arc_end_pts[1], center])
# normal = Vector(self.camera_projection)
# normal = self.camera_projection
# dir1 = (arc_end_pts[0] - center).normalized()
# arc_mid_dir = (arc_end_pts[1] - center).normalized()
@@ -1104,10 +1134,11 @@ class SvgWriter:
classes = self.get_attribute_classes(obj)
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
def draw_radius_annotations(self, obj):
def draw_radius_annotations(self, obj: bpy.types.Object) -> None:
offset = Vector([self.raw_width, self.raw_height]) / 2
classes = self.get_attribute_classes(obj)
element = tool.Ifc.get_entity(obj)
assert element
tag = element.Description
matrix_world = obj.matrix_world
@@ -1141,7 +1172,7 @@ class SvgWriter:
get_text, tag, dimension_data, text_position=text_position, class_str="RADIUS", box_alignment="center"
)
def draw_dimension_text(self, get_text, tag, dimension_data, **create_text_kwargs):
def draw_dimension_text(self, get_text: Callable[[], str], tag, dimension_data, **create_text_kwargs) -> None:
prefix = dimension_data["text_prefix"]
suffix = dimension_data["text_suffix"]
show_description_only = dimension_data["show_description_only"]
@@ -1166,12 +1197,13 @@ class SvgWriter:
for text in text_tags:
self.svg.add(text)
def draw_fall_annotations(self, obj):
def draw_fall_annotations(self, obj: bpy.types.Object) -> None:
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
classes = self.get_attribute_classes(obj)
element = tool.Ifc.get_entity(obj)
matrix_world = obj.matrix_world
assert isinstance(obj.data, bpy.types.Curve)
for spline in obj.data.splines:
points = self.get_spline_points(spline)
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
@@ -1236,13 +1268,14 @@ class SvgWriter:
text_style = SvgWriter.get_box_alignment_parameters("center")
self.svg.add(self.svg.text(tag, insert=tuple(text_position), class_="RADIUS", **text_style))
def draw_diameter_annotations(self, obj):
def draw_diameter_annotations(self, obj: bpy.types.Object) -> None:
classes = self.get_attribute_classes(obj)
matrix_world = obj.matrix_world
element = tool.Ifc.get_entity(obj)
dimension_text = element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
assert isinstance(obj.data, bpy.types.Curve)
for spline in obj.data.splines:
points = self.get_spline_points(spline)
for i, p in enumerate(points):
@@ -1264,13 +1297,14 @@ class SvgWriter:
custom_unit=dimension_data["custom_unit"],
)
def draw_dimension_annotations(self, obj):
def draw_dimension_annotations(self, obj: bpy.types.Object) -> None:
classes = self.get_attribute_classes(obj)
matrix_world = obj.matrix_world
element = tool.Ifc.get_entity(obj)
dimension_text = element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
assert isinstance(obj.data, bpy.types.Curve)
for spline in obj.data.splines:
points = self.get_spline_points(spline)
for i in range(len(points) - 1):
@@ -1289,7 +1323,7 @@ class SvgWriter:
custom_unit=dimension_data["custom_unit"],
)
def draw_measureit_arch_dimension_annotations(self):
def draw_measureit_arch_dimension_annotations(self) -> None:
try:
import MeasureIt_ARCH.measureit_arch_external_utils
@@ -1303,9 +1337,9 @@ class SvgWriter:
def draw_dimension_annotation(
self,
v0_global,
v1_global,
classes,
v0_global: Vector,
v1_global: Vector,
classes: list[str],
dimension_text=None,
text_format=lambda x: x,
show_description_only=False,
@@ -1314,7 +1348,7 @@ class SvgWriter:
text_suffix="",
fill_bg=False,
custom_unit=None,
):
) -> None:
offset = Vector([self.raw_width, self.raw_height]) / 2
v0 = self.project_point_onto_camera(v0_global)
v1 = self.project_point_onto_camera(v1_global)
@@ -1441,12 +1475,12 @@ class SvgWriter:
# TODO is this needlessly complex?
return self.camera.matrix_world.inverted() @ geometry.intersect_line_plane(
point.xyz,
point.xyz - Vector(self.camera_projection),
point.xyz - self.camera_projection,
self.camera.location,
Vector(self.camera_projection),
self.camera_projection,
)
def get_spline_points(self, spline):
def get_spline_points(self, spline: bpy.types.Spline) -> Union[SplineBezierPoints, SplinePoints]:
return spline.bezier_points if spline.bezier_points else spline.points
def draw_polygon(self, polygon, position):
+29 -21
View File
@@ -173,12 +173,13 @@ class BIM_PT_drawing_underlay(Panel):
return bool((camera := context.scene.camera) and tool.Ifc.get_entity(camera))
def draw(self, context):
assert self.layout
layout = self.layout
layout.use_property_split = True
assert context.scene and (camera := context.scene.camera)
dprops = tool.Drawing.get_document_props()
props = tool.Drawing.get_camera_props(camera)
drawing_index_is_valid = props.active_drawing_style_index < len(dprops.drawing_styles)
drawing_style = props.get_active_drawing_style()
if not DrawingsData.is_loaded:
DrawingsData.load()
@@ -192,7 +193,7 @@ class BIM_PT_drawing_underlay(Panel):
row.label(text="Current Shading Style:")
row.label(text=current_shading_style)
row.operator("bim.add_drawing_style", icon="ADD", text="")
if drawing_index_is_valid:
if drawing_style:
row.operator("bim.remove_drawing_style", icon="X", text="").index = props.active_drawing_style_index
row.operator("bim.reload_drawing_styles", icon="FILE_REFRESH", text="")
@@ -207,9 +208,8 @@ class BIM_PT_drawing_underlay(Panel):
"active_drawing_style_index",
)
if not drawing_index_is_valid:
if drawing_style is None:
return
drawing_style = dprops.drawing_styles[props.active_drawing_style_index]
row = layout.row(align=True)
row.prop(drawing_style, "name")
@@ -221,14 +221,6 @@ class BIM_PT_drawing_underlay(Panel):
row = layout.row(align=True)
row.prop(drawing_style, "exclude_query")
row = layout.row()
row.operator("bim.add_drawing_style_attribute")
for index, attribute in enumerate(drawing_style.attributes):
row = layout.row(align=True)
row.prop(attribute, "name", text="")
row.operator("bim.remove_drawing_style_attribute", icon="X", text="").index = index
row = layout.row(align=True)
row.operator("bim.save_drawing_style")
row.operator("bim.activate_drawing_style")
@@ -281,9 +273,6 @@ class BIM_PT_drawings(Panel):
row3 = row.row(align=True)
row3.alignment = "RIGHT"
row3.operator("bim.activate_drawing", icon="OUTLINER_OB_CAMERA", text="").drawing = (
active_drawing.ifc_definition_id
)
row3.operator("bim.activate_model", icon="VIEW3D", text="")
row3.separator(factor=0.5, type="SPACE")
@@ -505,17 +494,19 @@ class BIM_PT_product_assignments(Panel):
@classmethod
def poll(cls, context):
if not tool.Ifc.get() or not context.active_object:
return
return False
element = tool.Ifc.get_entity(context.active_object)
if not element:
return
return False
return element.is_a("IfcAnnotation")
def draw(self, context):
if not ProductAssignmentsData.is_loaded:
ProductAssignmentsData.load()
props = context.active_object.BIMAssignedProductProperties
assert self.layout
assert (obj := context.active_object)
props = tool.Drawing.get_object_assigned_product_props(obj)
if props.is_editing_product:
row = self.layout.row(align=True)
@@ -601,7 +592,7 @@ class BIM_PT_text(Panel):
col.label(text=f' {literal_props.attributes["BoxAlignment"].string_value}')
else:
text_data = DecoratorData.get_ifc_text_data(obj)
text_data = DecoratorData.get_text_data(obj)
row = self.layout.row()
row.operator("bim.enable_editing_text", icon="GREASEPENCIL")
@@ -622,7 +613,16 @@ class BIM_PT_text(Panel):
class BIM_UL_drawinglist(bpy.types.UIList):
def draw_item(self, context, layout, data: DocProperties, item: Drawing, icon, active_data, active_propname):
def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: DocProperties,
item: Drawing,
icon,
active_data,
active_propname,
):
if not item:
layout.label(text="", translate=False)
return
@@ -632,14 +632,22 @@ class BIM_UL_drawinglist(bpy.types.UIList):
row.label(text="", icon="BLANK1")
selected_icon = "CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT"
row.prop(item, "is_selected", text="", icon=selected_icon, emboss=False)
row.separator(factor=0.5, type="SPACE")
row.prop(item, "name", text="", emboss=False)
row.separator(factor=0.25, type="SPACE")
self.props = tool.Drawing.get_document_props()
if (
self.props.drawings
and self.props.active_drawing_id
and item.ifc_definition_id == self.props.active_drawing_id
):
row.label(text="", icon="OUTLINER_OB_CAMERA")
row.operator("bim.activate_drawing", text="", icon="VIEW_CAMERA", emboss=True, depress=True).drawing = (
item.ifc_definition_id
)
else:
row.operator("bim.activate_drawing", text="", icon="VIEW_CAMERA_UNSELECTED", emboss=False).drawing = (
item.ifc_definition_id
)
else:
if item.target_view == "PLAN_VIEW":
icon = "UV_FACESEL"
@@ -218,7 +218,7 @@ class AnnotationToolUI:
@classmethod
def draw_edit_object_interface(cls, context):
if DecoratorData.get_ifc_text_data(bpy.context.active_object):
if DecoratorData.get_text_data(bpy.context.active_object):
add_layout_hotkey_operator(cls.layout, "Edit Text", "S_E", "")
@classmethod
@@ -311,7 +311,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if not bpy.context.active_object:
return
if DecoratorData.get_ifc_text_data(bpy.context.active_object):
if DecoratorData.get_text_data(bpy.context.active_object):
bpy.ops.bim.edit_text_popup()
def hotkey_S_G(self):
+35 -22
View File
@@ -23,6 +23,7 @@ import ifcopenshell.util.placement
import ifcopenshell.util.schema
import ifcopenshell.util.unit
import bonsai.tool as tool
from typing import Any, Union
from mathutils import Vector
@@ -45,13 +46,12 @@ class ViewportData:
cls.data = {"mode": cls.mode()}
@classmethod
def mode(cls):
def mode(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
obj_mode = ("OBJECT", "IFC Object Mode", "View and move the placements of objects", "OBJECT_DATAMODE", 0)
item_mode = ("ITEM", "IFC Item Mode", "View individual representation items", "MESH_DATA", 1)
edit_mode = ("EDIT", "IFC Edit Mode", "Edit representation items", "EDITMODE_HLT", 2)
obj = bpy.context.active_object
element = tool.Ifc.get_entity(obj)
modes: list[tuple[str, str, str, str, int]] = [obj_mode]
gprops = tool.Geometry.get_geometry_props()
@@ -61,13 +61,14 @@ class ViewportData:
if not obj:
return modes
element = tool.Ifc.get_entity(obj)
pprops = tool.Project.get_project_props()
if obj in pprops.clipping_planes_objs:
pass
elif element:
if tool.Geometry.is_locked(element):
pass
elif obj.data and tool.Geometry.is_profile_based(obj.data):
elif obj.data and tool.Geometry.has_mesh_properties(obj.data) and tool.Geometry.is_profile_based(obj.data):
modes.append(edit_mode)
elif element.is_a("IfcRelSpaceBoundary"):
modes.append(edit_mode)
@@ -88,7 +89,7 @@ class ViewportData:
class RepresentationsData:
data = {}
data: dict[str, Any] = {}
is_loaded = False
@classmethod
@@ -102,10 +103,12 @@ class RepresentationsData:
cls.is_loaded = True
@classmethod
def representations(cls):
results = []
def representations(cls) -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
obj = tool.Geometry.get_active_or_representation_obj()
assert obj
element = tool.Ifc.get_entity(obj)
assert element
active_representation_id = None
active_representation = tool.Geometry.get_active_representation(obj)
@@ -132,8 +135,8 @@ class RepresentationsData:
return results
@classmethod
def contexts(cls):
results = []
def contexts(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
results: list[tuple[str, str, str]] = []
for element in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
results.append((str(element.id()), element.ContextType or "Unnamed", ""))
for element in tool.Ifc.get().by_type("IfcGeometricRepresentationSubContext", include_subtypes=False):
@@ -219,9 +222,12 @@ class ConnectionsData:
cls.is_loaded = True
@classmethod
def connections(cls):
results = []
element = tool.Ifc.get_entity(bpy.context.active_object)
def connections(cls) -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
obj = bpy.context.active_object
assert obj
element = tool.Ifc.get_entity(obj)
assert element
connected_to = getattr(element, "ConnectedTo", [])
connected_from = getattr(element, "ConnectedFrom", [])
@@ -285,13 +291,16 @@ class ConnectionsData:
return results
@classmethod
def is_connection_realization(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
def is_connection_realization(cls) -> Union[list[dict[str, Any]], None]:
obj = bpy.context.active_object
assert obj
element = tool.Ifc.get_entity(obj)
assert element
connections = getattr(element, "IsConnectionRealization", None)
if not connections:
return
results = []
results: list[dict[str, Any]] = []
for rel in connections:
data = {
"realizing_elements_connection_type": rel.ConnectionType,
@@ -322,16 +331,19 @@ class DerivedCoordinatesData:
cls.is_loaded = True
@classmethod
def load_z_values(cls):
def load_z_values(cls) -> None:
cls.z_values = [
(bpy.context.active_object.matrix_world @ Vector(co))[2] for co in bpy.context.active_object.bound_box
]
@classmethod
def load_collection(cls):
def load_collection(cls) -> None:
cls.collection = None
cls.collection_z = 0
element = tool.Ifc.get_entity(bpy.context.active_object)
obj = bpy.context.active_object
if not obj:
return
element = tool.Ifc.get_entity(obj)
if not element:
return
parent = ifcopenshell.util.element.get_aggregate(element)
@@ -392,7 +404,7 @@ class PlacementData:
@classmethod
def load(cls):
cls.data = {"has_placement": cls.has_placement()}
cls.data: dict[str, Any] = {"has_placement": cls.has_placement()}
props = tool.Georeference.get_georeference_props()
obj = bpy.context.active_object
@@ -415,13 +427,14 @@ class PlacementData:
return False
@classmethod
def original_xyz(cls, obj):
def original_xyz(cls, obj: bpy.types.Object) -> list[float]:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
props = tool.Georeference.get_georeference_props()
translation = obj.matrix_world.translation
xyz = ifcopenshell.util.geolocation.xyz2enh(
obj.matrix_world[0][3],
obj.matrix_world[1][3],
obj.matrix_world[2][3],
translation[0],
translation[1],
translation[2],
float(props.blender_offset_x) * unit_scale,
float(props.blender_offset_y) * unit_scale,
float(props.blender_offset_z) * unit_scale,
@@ -22,7 +22,9 @@ import bmesh
import numpy as np
import numpy.typing as npt
import ifcopenshell
import ifcopenshell.api.geometry
import ifcopenshell.api.layer
import ifcopenshell.api.material
import ifcopenshell.api.root
import ifcopenshell.api.style
import ifcopenshell.util.element
@@ -583,10 +585,11 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
# We are explicitly casting to a tessellation, so remove all parametric materials.
element_type = ifcopenshell.util.element.get_type(product)
if element_type: # Some invalid IFCs use material sets without a type.
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), products=[element_type])
ifcopenshell.api.material.unassign_material(self.file, products=[element_type])
tool.Material.ensure_material_unassigned([element_type])
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), products=[product])
ifcopenshell.api.material.unassign_material(self.file, products=[product])
tool.Material.ensure_material_unassigned([product])
data = obj.data # Required after ensure_material_unassigned
else:
# These objects are parametrically based on an axis and should not be modified as a mesh
if extrusion := tool.Model.get_extrusion(old_representation):
@@ -603,6 +606,7 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
# added this as a fallback for easier transition some annotation types to 3d
# if they were create before as 2d
element = tool.Ifc.get_entity(obj)
assert element
if tool.Drawing.is_annotation_object_type(element, ("FALL", "SECTION_LEVEL", "PLAN_LEVEL")):
context_of_items = tool.Drawing.get_annotation_context("MODEL_VIEW")
@@ -626,7 +630,7 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
representation_data["text_literal"] = tool.Geometry.get_text_literal(old_representation)
# TODO: replace with core.add_representation?
new_representation = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data)
new_representation = ifcopenshell.api.geometry.add_representation(self.file, **representation_data)
if new_representation is None:
self.report({"ERROR"}, "Error creating representation for Blender object.")
return {"CANCELLED"}
@@ -35,13 +35,13 @@ from bpy.props import (
from typing import Optional, TYPE_CHECKING, Union, Literal
def get_contexts(self, context):
def get_contexts(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
if not RepresentationsData.is_loaded:
RepresentationsData.load()
return RepresentationsData.data["contexts"]
def update_mode(self, context):
def update_mode(self: "BIMGeometryProperties", context: bpy.types.Context) -> None:
if self.is_changing_mode:
return
if context.mode.startswith("EDIT"):
@@ -62,7 +62,7 @@ def update_mode(self, context):
bpy.ops.bim.override_mode_set_edit("INVOKE_DEFAULT")
def update_representation_obj(self, context):
def update_representation_obj(self: "BIMGeometryProperties", context: bpy.types.Context) -> None:
for item_obj in self.item_objs:
if item_obj.obj:
data = item_obj.obj.data
@@ -76,13 +76,13 @@ def update_representation_obj(self, context):
self.is_changing_mode = False
def get_mode(self, context):
def get_mode(self: "BIMGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str, str, int]]:
if not ViewportData.is_loaded:
ViewportData.load()
return ViewportData.data["mode"]
def get_styles(self, context):
def get_styles(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
# postponed import to avoid circular import
from bonsai.bim.module.material.data import MaterialsData
@@ -91,13 +91,13 @@ def get_styles(self, context):
return MaterialsData.data["styles"]
def get_shape_aspects(self, context):
def get_shape_aspects(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
if not RepresentationsData.is_loaded:
RepresentationsData.load()
return RepresentationsData.data["shape_aspects"]
def get_material_constituents(self, context, edit_text):
def get_material_constituents(self: "ShapeAspect", context: bpy.types.Context, edit_text: str) -> list[str]:
from bonsai.bim.module.material.data import ObjectMaterialData
if not ObjectMaterialData.is_loaded:
@@ -105,7 +105,7 @@ def get_material_constituents(self, context, edit_text):
return ObjectMaterialData.data["active_material_constituents"]
def get_layers(self, context):
def get_layers(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
from bonsai.bim.module.layer.data import LayersData
if not LayersData.is_loaded:
@@ -113,7 +113,7 @@ def get_layers(self, context):
return LayersData.data["layers_enum"]
def get_layers_no_active(self, context):
def get_layers_no_active(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
from bonsai.bim.module.layer.data import LayersData
if not LayersData.is_loaded:
@@ -112,7 +112,9 @@ def update_blender_coordinates(self: "BIMGeoreferenceProperties", context: bpy.t
props.is_updating_coordinates = True
blender_coordinates = tool.Georeference.get_coordinates("blender")
local_coordinates = ifcopenshell.util.geolocation.xyz2enh(
*blender_coordinates,
blender_coordinates[0],
blender_coordinates[1],
blender_coordinates[2],
float(props.blender_offset_x),
float(props.blender_offset_y),
float(props.blender_offset_z),
@@ -394,7 +394,7 @@ class ObjectMaterialData:
return [(m, m, ifcopenshell.util.doc.get_entity_doc(version, m).get("description", "")) for m in material_types]
@classmethod
def active_material_constituents(cls):
def active_material_constituents(cls) -> list[str]:
material = cls.material
if not cls.material or not material.is_a("IfcMaterialConstituentSet"):
return []
@@ -625,6 +625,15 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator):
wall.DumbWallPlaner().regenerate_from_layer_set(layer_set)
slab.DumbSlabPlaner().regenerate_from_layer_set(layer_set)
if material_set_usage.is_a("IfcMaterialProfileSetUsage"):
attributes["CardinalPoint"] = int(attributes["CardinalPoint"])
ifcopenshell.api.material.edit_profile_usage(
self.file,
usage=material_set_usage,
attributes=attributes,
)
model_profile.DumbProfileRecalculator().recalculate(objects)
bpy.ops.bim.disable_editing_assigned_material(obj=active_obj.name)
+34 -5
View File
@@ -27,6 +27,8 @@ import bonsai.core.misc as core
import bonsai.core.geometry as core_geometry
import bonsai.core.root
from mathutils import Vector, Matrix, Euler
from typing import TYPE_CHECKING, Literal, get_args
from typing_extensions import assert_never
class SetOverrideColour(bpy.types.Operator):
@@ -117,23 +119,45 @@ class ResizeToStorey(bpy.types.Operator, tool.Ifc.Operator):
core.resize_to_storey(tool.Misc, tool.Ifc, obj=obj, total_storeys=self.total_storeys)
SplitAlongEdgeMode = Literal["BOOLEAN", "BISECT"]
class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.split_along_edge"
bl_label = "Split Along Edge"
bl_description = (
"Active object is considered to be a cutting object."
"Split selected objects by the face of the cutter object.\n"
"Active object is considered to be a cutting object. "
"It can be a simple Blender object (e.g. a plane), not connected to IFC. "
"Will unassign element from a type if type has a representation."
)
bl_options = {"REGISTER", "UNDO"}
mode: bpy.props.StringProperty(default="BOOLEAN")
mode: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
default="BOOLEAN",
items=tuple((i, i, "") for i in get_args(SplitAlongEdgeMode)),
)
if TYPE_CHECKING:
mode: SplitAlongEdgeMode
@classmethod
def poll(cls, context):
return context.selected_objects
if not (obj := context.active_object) or obj.type != "MESH":
cls.poll_message_set("No active mesh object is selected.")
return False
if not context.selected_objects:
cls.poll_message_set("No objects selected")
return False
return True
def _execute(self, context):
cutter = context.active_object
objs = [o for o in context.selected_objects if o != cutter]
assert cutter
objs = [o for o in context.selected_objects if o != cutter and o.type == "MESH"]
if not objs:
self.report({"ERROR"}, "No other mesh objects selected besides the cutter object.")
return {"CANCELLED"}
if not tool.Ifc.get():
if self.mode == "BOOLEAN":
@@ -142,9 +166,11 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
tool.Misc.bisect_objects_with_cutter(objs, cutter)
for obj in objs:
bpy.data.objects.remove(obj)
else:
assert_never(self.mode)
return
objs_to_cut = []
objs_to_cut: list[bpy.types.Object] = []
# Splitting only works on meshes
for obj in objs:
# You cannot split meshes if the representation is mapped.
@@ -183,6 +209,8 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
new_objs = tool.Misc.boolean_objects_with_cutter(objs_to_cut, cutter)
elif self.mode == "BISECT":
new_objs = tool.Misc.bisect_objects_with_cutter(objs_to_cut, cutter)
else:
assert_never(self.mode)
for obj in new_objs:
bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=obj)
@@ -191,6 +219,7 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.update_representation(obj=obj.name)
representation = tool.Geometry.get_active_representation(obj)
assert representation
core_geometry.switch_representation(
tool.Ifc,
tool.Geometry,
+9 -2
View File
@@ -38,10 +38,17 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator):
assert (element := tool.Ifc.get_entity(obj))
ifc_file = tool.Ifc.get()
if not element.is_a("IfcElement") and not element.is_a("IfcAnnotation"):
allowed_types = (
"IfcElement",
"IfcAnnotation",
"IfcOpeningElement",
"IfcSpatialElement",
)
if not any(element.is_a(c) for c in allowed_types):
self.report(
{"ERROR"},
f"Adding array to element of type '{element.is_a()}' is not supported. Supported types: IfcElement, IfcAnnotation.",
f"Adding array to element of type '{element.is_a()}' is not supported. Supported types: {','.join(allowed_types)}.",
)
return {"CANCELLED"}
@@ -616,6 +616,9 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
props.is_editing = False
update_door_modifier_representation(obj)
element_type = ifcopenshell.util.element.get_type(element)
if element_type:
tool.Model.mark_thumbnail_for_update(element_type)
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
door_data = tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))
@@ -36,12 +36,6 @@ def load_post(*args):
"sequence.edit_task_time", "Bonsai.Task.CalculateQuantities", task.calculate_quantities
)
ifcopenshell.api.add_post_listener(
"material.edit_profile_usage",
"Bonsai.Product.RegenerateProfileUsage",
product.regenerate_profile_usage,
)
ifcopenshell.api.add_post_listener(
"type.assign_type", "Bonsai.DumbWall.RegenerateFromType", wall.DumbWallPlaner().regenerate_from_type
)
+12 -20
View File
@@ -484,32 +484,21 @@ class AddBoolean(Operator, tool.Ifc.Operator):
class ShowOpenings(Operator, tool.Ifc.Operator):
bl_idname = "bim.show_openings"
bl_label = "Show"
bl_description = "Show Openings"
bl_label = "Show Openings"
bl_description = "Show openings for selected objects"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
objs = list(context.selected_objects)
# If several parts of an aggregation are selected, the aggregate openings may be queried more than once
objects_element_map = set()
while objs:
obj = objs.pop(0)
for obj in tool.Blender.get_selected_objects():
element = tool.Ifc.get_entity(obj)
if element:
if getattr(element, "Decomposes", None):
# Select aggregate recursively
if element.Decomposes and (aggregate := element.Decomposes[0].RelatingObject):
aggregate_obj = tool.Ifc.get_object(aggregate)
objs.append(aggregate_obj)
objects_element_map.add((obj, element))
for obj, element in objects_element_map:
if element is None:
continue
self.show_object_openings(obj, element)
DecorationsHandler.install(bpy.context)
bpy.ops.bim.update_openings_focus()
return {"FINISHED"}
def show_object_openings(self, obj, element):
def show_object_openings(self, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
openings_elements = [rel.RelatedOpeningElement for rel in tool.Geometry.get_openings(element)]
if not openings_elements:
return
@@ -531,15 +520,18 @@ class UpdateOpeningsFocus(Operator):
preferences = tool.Blender.get_addon_preferences()
if preferences.opening_focus_opacity == 100:
return {"FINISHED"}
openings = set()
building_objects = set()
openings: set[bpy.types.Object] = set()
building_objects: set[bpy.types.Object] = set()
props = tool.Model.get_model_props()
for opening in props.openings:
if opening.obj:
openings.add(opening.obj)
opening_element = tool.Ifc.get_entity(opening.obj)
assert opening_element
building_element = opening_element.VoidsElements[0].RelatingBuildingElement
building_objects.add(tool.Ifc.get_object(building_element))
building_obj = tool.Ifc.get_object(building_element)
assert isinstance(building_obj, bpy.types.Object)
building_objects.add(building_obj)
for obj in context.scene.objects:
obj.color = [
@@ -643,12 +643,22 @@ class PolylineOperator:
tool.Blender.update_viewport()
def choose_plane(self, event: bpy.types.Event, x: bool = True, y: bool = True, z: bool = True) -> None:
def get_plane_origin():
polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
if len(polyline_points) > 0:
reference_point = polyline_points[-1]
else:
reference_point = Vector((0.0, 0.0, 0.0))
self.tool_state.plane_origin = Vector((reference_point.x, reference_point.y, reference_point.z))
if x:
if event.shift and event.value == "PRESS" and event.type == "X":
self.tool_state.use_default_container = False
self.tool_state.plane_method = "YZ" if self.tool_state.plane_method != "YZ" else None
self.tool_state.axis_method = None
tool.Blender.update_viewport()
get_plane_origin()
if y:
if event.shift and event.value == "PRESS" and event.type == "Y":
@@ -656,6 +666,7 @@ class PolylineOperator:
self.tool_state.plane_method = "XZ" if self.tool_state.plane_method != "XZ" else None
self.tool_state.axis_method = None
tool.Blender.update_viewport()
get_plane_origin()
if z:
if event.shift and event.value == "PRESS" and event.type == "Z":
@@ -663,6 +674,7 @@ class PolylineOperator:
self.tool_state.plane_method = "XY" if self.tool_state.plane_method != "XY" else None
self.tool_state.axis_method = None
tool.Blender.update_viewport()
get_plane_origin()
def handle_instructions(
self, context: bpy.types.Context, custom_instructions: dict = {}, custom_info: str = "", overwrite: bool = False
@@ -880,6 +892,7 @@ class PolylineOperator:
else:
if event.value == "RELEASE" and event.type in {"ESC"}:
self.tool_state.axis_method = None
self.tool_state.plane_method = None
context.workspace.status_text_set(text=None)
PolylineDecorator.uninstall()
tool.Polyline.clear_polyline()
@@ -569,7 +569,7 @@ class AlignProduct(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
AlignType = Literal["CENTERLINE", "POSITIVE", "NEGATIVE"]
align_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
align_type: bpy.props.EnumProperty( # pyright: ignore [reportRedeclaration]
items=[(i, i, "") for i in get_args(AlignType)]
)
@@ -608,8 +608,7 @@ class LoadTypeThumbnails(bpy.types.Operator):
while queue:
# if bpy.app.is_job_running("RENDER_PREVIEW") does not seem to reflect asset preview generation
element = queue.pop()
if tool.Model.update_thumbnail_for_element(element):
queue.append(element)
tool.Model.mark_thumbnail_for_update(element)
return {"FINISHED"}
@@ -212,7 +212,7 @@ class DumbProfileRegenerator:
# update related thumbnails
for element in self.get_element_types_using_profile(profile):
tool.Model.update_thumbnail_for_element(element, refresh=True)
tool.Model.mark_thumbnail_for_update(element)
def regenerate_from_profile(self, usecase_path, ifc_file, settings):
self.file = ifc_file
@@ -1196,6 +1196,7 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato
):
self.create_profiles_from_polyline(context)
context.workspace.status_text_set(text=None)
self.tool_state.plane_method = None
ProductDecorator.uninstall()
PolylineDecorator.uninstall()
tool.Polyline.clear_polyline()
+3 -2
View File
@@ -133,7 +133,7 @@ class AlignWall(bpy.types.Operator):
'Int.': align to the INTERIOR face"""
AlignType = Literal["CENTER", "EXTERIOR", "INTERIOR"]
align_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
align_type: bpy.props.EnumProperty( # pyright: ignore [reportRedeclaration]
items=((i, i, "") for i in get_args(AlignType))
)
@@ -571,6 +571,7 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
):
self.create_walls_from_polyline(context)
context.workspace.status_text_set(text=None)
self.tool_state.plane_method = None
ProductDecorator.uninstall()
PolylineDecorator.uninstall()
tool.Polyline.clear_polyline()
@@ -1127,7 +1128,7 @@ class DumbWallJoiner:
p3 = (matrix1i @ matrix2 @ np.concatenate((p3, (0, 1))))[:2]
p4 = (matrix1i @ matrix2 @ np.concatenate((p4, (0, 1))))[:2]
if not np.isclose(p1[1], p4[1]) or not np.isclose(p3[1], p4[1]):
if not np.isclose(p1[1], p4[1], atol=1e-02) or not np.isclose(p3[1], p4[1], atol=1e-02):
return
x_ordinates = tuple(co[0] for co in (p1, p2, p3, p4))
@@ -529,6 +529,9 @@ class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
props.is_editing = False
update_window_modifier_representation(context)
element_type = ifcopenshell.util.element.get_type(element)
if element_type:
tool.Model.mark_thumbnail_for_update(element_type)
pset = tool.Pset.get_element_pset(element, "BBIM_Window")
window_data = tool.Ifc.get().createIfcText(json.dumps(window_data, default=list))
+101 -68
View File
@@ -17,7 +17,9 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifcopenshell
import bonsai.tool as tool
from typing import Any, Union
from ifcopenshell.util.doc import get_entity_doc
@@ -31,42 +33,46 @@ def refresh():
class RolesAddressesData:
@classmethod
def get_roles(cls, parent):
results = []
def get_roles(cls, parent: ifcopenshell.entity_instance) -> list[dict[str, Any]]:
props = tool.Owner.get_owner_props()
results: list[dict[str, Any]] = []
for role in parent.Roles or []:
results.append(
{
"id": role.id(),
"is_editing": bpy.context.scene.BIMOwnerProperties.active_role_id == role.id(),
"is_editing": props.active_role_id == role.id(),
"label": role.UserDefinedRole or role.Role,
"props": bpy.context.scene.BIMOwnerProperties.role_attributes,
"props": props.role_attributes,
}
)
return results
@classmethod
def get_addresses(cls, parent):
results = []
def get_addresses(cls, parent: ifcopenshell.entity_instance) -> list[dict[str, Any]]:
props = tool.Owner.get_owner_props()
results: list[dict[str, Any]] = []
for address in parent.Addresses or []:
results.append(
{
"id": address.id(),
"is_editing": bpy.context.scene.BIMOwnerProperties.active_address_id == address.id(),
"is_editing": props.active_address_id == address.id(),
"label": address.is_a(),
"props": bpy.context.scene.BIMOwnerProperties.address_attributes,
"props": props.address_attributes,
"list_attributes": cls.get_address_list_attributes(address),
}
)
return results
@classmethod
def get_address_list_attributes(cls, address):
results = []
props = bpy.context.scene.BIMOwnerProperties
def get_address_list_attributes(cls, address: ifcopenshell.entity_instance) -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
props = tool.Owner.get_owner_props()
if address.is_a("IfcPostalAddress"):
names = ["AddressLines"]
elif address.is_a("IfcTelecomAddress"):
names = ["TelephoneNumbers", "FacsimileNumbers", "ElectronicMailAddresses", "MessagingIDs"]
else:
assert False, f"Unexpected entity: {address}"
for name in names:
if name == "AddressLines":
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.address_lines)]
@@ -78,6 +84,8 @@ class RolesAddressesData:
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.electronic_mail_addresses)]
elif name == "MessagingIDs":
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.messaging_ids)]
else:
assert False, f"Unexpected name: {name}"
results.append({"name": name, "items": items})
return results
@@ -92,14 +100,15 @@ class PeopleData(RolesAddressesData):
cls.is_loaded = True
@classmethod
def get_people(cls):
people = []
def get_people(cls) -> list[dict[str, Any]]:
props = tool.Owner.get_owner_props()
people: list[dict[str, Any]] = []
for person in tool.Ifc.get().by_type("IfcPerson"):
roles = cls.get_roles(person)
people.append(
{
"id": person.id(),
"props": bpy.context.scene.BIMOwnerProperties.person_attributes,
"props": props.person_attributes,
"name": cls.get_person_name(person),
"roles_label": ", ".join([r["label"] for r in roles]),
"is_editing": cls.get_person_is_editing(person),
@@ -112,7 +121,7 @@ class PeopleData(RolesAddressesData):
return people
@classmethod
def get_person_name(cls, person):
def get_person_name(cls, person: ifcopenshell.entity_instance) -> str:
if tool.Ifc.get_schema() == "IFC2X3":
name = person.Id
else:
@@ -124,20 +133,24 @@ class PeopleData(RolesAddressesData):
return name
@classmethod
def get_person_is_editing(cls, person):
return bpy.context.scene.BIMOwnerProperties.active_person_id == person.id()
def get_person_is_editing(cls, person: ifcopenshell.entity_instance) -> bool:
props = tool.Owner.get_owner_props()
return props.active_person_id == person.id()
@classmethod
def get_person_list_attributes(cls, person):
results = []
props = bpy.context.scene.BIMOwnerProperties
for name in ["MiddleNames", "PrefixTitles", "SuffixTitles"]:
def get_person_list_attributes(cls, person: ifcopenshell.entity_instance) -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
props = tool.Owner.get_owner_props()
name: tool.Owner.PersonAttributeType
for name in ("MiddleNames", "PrefixTitles", "SuffixTitles"):
if name == "MiddleNames":
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.middle_names)]
elif name == "PrefixTitles":
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.prefix_titles)]
elif name == "SuffixTitles":
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.suffix_titles)]
else:
assert False, name
results.append({"name": name, "items": items})
return results
@@ -152,17 +165,18 @@ class OrganisationsData(RolesAddressesData):
cls.is_loaded = True
@classmethod
def get_organisations(cls):
organisations = []
def get_organisations(cls) -> list[dict[str, Any]]:
props = tool.Owner.get_owner_props()
organisations: list[dict[str, Any]] = []
for organisation in tool.Ifc.get().by_type("IfcOrganization"):
roles = cls.get_roles(organisation)
organisations.append(
{
"id": organisation.id(),
"props": bpy.context.scene.BIMOwnerProperties.organisation_attributes,
"props": props.organisation_attributes,
"name": organisation.Name,
"roles_label": ", ".join([r["label"] for r in roles]),
"is_editing": bpy.context.scene.BIMOwnerProperties.active_organisation_id == organisation.id(),
"is_editing": props.active_organisation_id == organisation.id(),
"is_engaged": bool(organisation.Engages),
"roles": roles,
"addresses": cls.get_addresses(organisation),
@@ -186,26 +200,27 @@ class OwnerData:
cls.is_loaded = True
@classmethod
def get_user_person(cls):
def get_user_person(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
return [(str(p.id()), p[0] or "Unnamed", "") for p in tool.Ifc.get().by_type("IfcPerson")]
@classmethod
def get_user_organisation(cls):
def get_user_organisation(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
return [(str(p.id()), p[0] or "Unnamed", "") for p in tool.Ifc.get().by_type("IfcOrganization")]
@classmethod
def can_add_user(cls):
return tool.Ifc.get().by_type("IfcPerson") and tool.Ifc.get().by_type("IfcOrganization")
def can_add_user(cls) -> bool:
return bool(tool.Ifc.get().by_type("IfcPerson") and tool.Ifc.get().by_type("IfcOrganization"))
@classmethod
def get_users(cls):
results = []
def get_users(cls) -> list[dict[str, Any]]:
props = tool.Owner.get_owner_props()
results: list[dict[str, Any]] = []
for user in tool.Ifc.get().by_type("IfcPersonAndOrganization"):
results.append(
{
"id": user.id(),
"label": "{} ({})".format(user.ThePerson[0] or "Unnamed", user.TheOrganization[0] or "Unnamed"),
"is_active": bpy.context.scene.BIMOwnerProperties.active_user_id == user.id(),
"is_active": props.active_user_id == user.id(),
}
)
return results
@@ -224,48 +239,60 @@ class ActorData:
cls.data["actors"] = cls.actors()
@classmethod
def the_actor(cls):
if not (ifc_class := bpy.context.scene.BIMOwnerProperties.actor_type):
def the_actor(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
props = tool.Owner.get_owner_props()
if not (ifc_class := props.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
def actors(cls):
actors = []
props = bpy.context.scene.BIMOwnerProperties
def actors(cls) -> list[dict[str, Any]]:
actors: list[dict[str, Any]] = []
props = tool.Owner.get_owner_props()
for actor in tool.Ifc.get().by_type(props.actor_class, include_subtypes=False):
is_editing = props.active_actor_id == actor.id()
if actor.TheActor.is_a("IfcPerson"):
the_actor = actor.TheActor.Identification or "N/A"
elif actor.TheActor.is_a("IfcOrganization"):
the_actor = actor.TheActor.Identification or "N/A"
elif actor.TheActor.is_a("IfcPersonAndOrganization"):
the_actor = actor.TheActor.ThePerson.Identification or "N/A"
the_actor += "-" + actor.TheActor.TheOrganization.Identification or "N/A"
the_actor: ifcopenshell.entity_instance = actor.TheActor
if the_actor.is_a("IfcPerson"):
the_actor_ = the_actor.Identification or "N/A"
elif the_actor.is_a("IfcOrganization"):
the_actor_ = the_actor.Identification or "N/A"
elif the_actor.is_a("IfcPersonAndOrganization"):
the_actor_ = the_actor.ThePerson.Identification or "N/A"
the_actor_ += "-" + the_actor.TheOrganization.Identification or "N/A"
else:
assert False, the_actor
actors.append(
{"id": actor.id(), "name": actor.Name or "Unnamed", "the_actor": the_actor, "is_editing": is_editing}
{
"id": actor.id(),
"name": actor.Name or "Unnamed",
"the_actor": the_actor_,
"is_editing": is_editing,
}
)
return actors
@classmethod
def actor_class(cls) -> list[tuple[str, str, str]]:
def actor_class(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
version = tool.Ifc.get_schema()
actor_doc = get_entity_doc(version, "IfcActor")
occupant_doc = get_entity_doc(version, "IfcOccupant")
assert actor_doc and occupant_doc
return [
("IfcActor", "Actor", get_entity_doc(version, "IfcActor").get("description", "")),
("IfcOccupant", "Occupant", get_entity_doc(version, "IfcOccupant").get("description", "")),
("IfcActor", "Actor", actor_doc.get("description", "")),
("IfcOccupant", "Occupant", occupant_doc.get("description", "")),
]
@classmethod
def actor_type(cls) -> list[tuple[str, str, str]]:
def actor_type(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
version = tool.Ifc.get_schema()
person_doc = get_entity_doc(version, "IfcPerson")
organization_doc = get_entity_doc(version, "IfcOrganization")
pao_doc = get_entity_doc(version, "IfcPersonAndOrganization")
assert person_doc and organization_doc and pao_doc
return [
("IfcPerson", "Person", get_entity_doc(version, "IfcPerson").get("description", "")),
("IfcOrganization", "Organisation", get_entity_doc(version, "IfcOrganization").get("description", "")),
(
"IfcPersonAndOrganization",
"User",
get_entity_doc(version, "IfcPersonAndOrganization").get("description", ""),
),
("IfcPerson", "Person", person_doc.get("description", "")),
("IfcOrganization", "Organisation", organization_doc.get("description", "")),
("IfcPersonAndOrganization", "User", pao_doc.get("description", "")),
]
@@ -279,25 +306,31 @@ class ObjectActorData:
cls.data = {"actor": cls.actor(), "actors": cls.actors()}
@classmethod
def actor(cls):
def actor(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
return [(str(p.id()), p.Name or "Unnamed", "") for p in tool.Ifc.get().by_type("IfcActor")]
@classmethod
def actors(cls):
results = []
element = tool.Ifc.get_entity(bpy.context.active_object)
def actors(cls) -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
obj = bpy.context.active_object
if not obj:
return results
element = tool.Ifc.get_entity(obj)
if not element:
return results
for rel in getattr(element, "HasAssignments", []):
if rel.is_a("IfcRelAssignsToActor"):
actor = rel.RelatingActor
if actor.TheActor.is_a("IfcPerson"):
roles = cls.get_roles(actor.TheActor)
elif actor.TheActor.is_a("IfcOrganization"):
roles = cls.get_roles(actor.TheActor)
elif actor.TheActor.is_a("IfcPersonAndOrganization"):
roles = cls.get_roles(actor.TheActor.ThePerson)
roles.extend(cls.get_roles(actor.TheActor.TheOrganization))
the_actor: ifcopenshell.entity_instance = actor.TheActor
if the_actor.is_a("IfcPerson"):
roles = cls.get_roles(the_actor)
elif the_actor.is_a("IfcOrganization"):
roles = cls.get_roles(the_actor)
elif the_actor.is_a("IfcPersonAndOrganization"):
roles = cls.get_roles(the_actor.ThePerson)
roles.extend(cls.get_roles(the_actor.TheOrganization))
else:
assert False, the_actor
role = ", ".join(roles)
results.append(
{"id": actor.id(), "name": actor.Name or "Unnamed", "role": role, "ifc_class": actor.is_a()}
@@ -305,5 +338,5 @@ class ObjectActorData:
return results
@classmethod
def get_roles(cls, parent):
def get_roles(cls, parent: ifcopenshell.entity_instance) -> list[Union[str, None]]:
return [r.UserDefinedRole or r.Role for r in parent.Roles or []]
+163 -65
View File
@@ -19,25 +19,35 @@
import bpy
import bonsai.tool as tool
import bonsai.core.owner as core
from ifcopenshell.api.owner.add_address import ADDRESS_TYPE
from typing import TYPE_CHECKING, get_args
if TYPE_CHECKING:
import bpy._typing.rna_enums as rna_enums
class EnableEditingPerson(bpy.types.Operator, tool.Ifc.Operator):
class EnableEditingPerson(bpy.types.Operator):
bl_idname = "bim.enable_editing_person"
bl_label = "Enable Editing Person"
bl_options = {"REGISTER", "UNDO"}
person: bpy.props.IntProperty()
person: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
def _execute(self, context):
if TYPE_CHECKING:
person: int
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
core.enable_editing_person(tool.Owner, person=tool.Ifc.get().by_id(self.person))
return {"FINISHED"}
class DisableEditingPerson(bpy.types.Operator, tool.Ifc.Operator):
class DisableEditingPerson(bpy.types.Operator):
bl_idname = "bim.disable_editing_person"
bl_label = "Disable Editing Person"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
core.disable_editing_person(tool.Owner)
return {"FINISHED"}
class AddPerson(bpy.types.Operator, tool.Ifc.Operator):
@@ -45,7 +55,7 @@ class AddPerson(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Add Person"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
def _execute(self, context) -> None:
core.add_person(tool.Ifc)
@@ -62,57 +72,81 @@ class RemovePerson(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_person"
bl_label = "Remove Person"
bl_options = {"REGISTER", "UNDO"}
person: bpy.props.IntProperty()
person: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
person: int
def _execute(self, context):
core.remove_person(tool.Ifc, person=tool.Ifc.get().by_id(self.person))
class AddPersonAttribute(bpy.types.Operator, tool.Ifc.Operator):
class AddPersonAttribute(bpy.types.Operator):
bl_idname = "bim.add_person_attribute"
bl_label = "Add Person Attribute"
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty()
name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
items=tuple((i, i, "") for i in get_args(tool.Owner.PersonAttributeType)),
)
def _execute(self, context):
if TYPE_CHECKING:
name: tool.Owner.PersonAttributeType # pyright: ignore[reportIncompatibleVariableOverride]
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
core.add_person_attribute(tool.Owner, name=self.name)
return {"FINISHED"}
class RemovePersonAttribute(bpy.types.Operator, tool.Ifc.Operator):
class RemovePersonAttribute(bpy.types.Operator):
bl_idname = "bim.remove_person_attribute"
bl_label = "Remove Person Attribute"
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty()
id: bpy.props.IntProperty()
name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
items=tuple((i, i, "") for i in get_args(tool.Owner.PersonAttributeType)),
)
id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
def _execute(self, context):
if TYPE_CHECKING:
name: tool.Owner.PersonAttributeType # pyright: ignore[reportIncompatibleVariableOverride]
id: int
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
core.remove_person_attribute(tool.Owner, name=self.name, id=self.id)
return {"FINISHED"}
class EnableEditingRole(bpy.types.Operator, tool.Ifc.Operator):
class EnableEditingRole(bpy.types.Operator):
bl_idname = "bim.enable_editing_role"
bl_label = "Enable Editing Role"
bl_options = {"REGISTER", "UNDO"}
role: bpy.props.IntProperty()
role: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
def _execute(self, context):
if TYPE_CHECKING:
role: int
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
core.enable_editing_role(tool.Owner, role=tool.Ifc.get().by_id(self.role))
return {"FINISHED"}
class DisableEditingRole(bpy.types.Operator, tool.Ifc.Operator):
class DisableEditingRole(bpy.types.Operator):
bl_idname = "bim.disable_editing_role"
bl_label = "Disable Editing Role"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
core.disable_editing_role(tool.Owner)
return {"FINISHED"}
class AddRole(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_role"
bl_label = "Add Role"
bl_options = {"REGISTER", "UNDO"}
parent: bpy.props.IntProperty()
parent: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
parent: int
def _execute(self, context):
core.add_role(tool.Ifc, parent=tool.Ifc.get().by_id(self.parent))
@@ -131,7 +165,10 @@ class RemoveRole(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_role"
bl_label = "Remove Role"
bl_options = {"REGISTER", "UNDO"}
role: bpy.props.IntProperty()
role: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
role: int
def _execute(self, context):
core.remove_role(tool.Ifc, role=tool.Ifc.get().by_id(self.role))
@@ -141,51 +178,75 @@ class AddAddress(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_address"
bl_label = "Add Address"
bl_options = {"REGISTER", "UNDO"}
parent: bpy.props.IntProperty()
ifc_class: bpy.props.StringProperty()
parent: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
ifc_class: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
items=tuple((i, i, "") for i in get_args(ADDRESS_TYPE)),
)
if TYPE_CHECKING:
parent: int
ifc_class: ADDRESS_TYPE
def _execute(self, context):
core.add_address(tool.Ifc, parent=tool.Ifc.get().by_id(self.parent), ifc_class=self.ifc_class)
class AddAddressAttribute(bpy.types.Operator, tool.Ifc.Operator):
class AddAddressAttribute(bpy.types.Operator):
bl_idname = "bim.add_address_attribute"
bl_label = "Add Address Attribute"
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty()
name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
items=tuple((i, i, "") for i in get_args(tool.Owner.AddressAttributeType)),
)
def _execute(self, context):
if TYPE_CHECKING:
name: tool.Owner.AddressAttributeType # pyright: ignore[reportIncompatibleVariableOverride]
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
core.add_address_attribute(tool.Owner, name=self.name)
return {"FINISHED"}
class RemoveAddressAttribute(bpy.types.Operator, tool.Ifc.Operator):
class RemoveAddressAttribute(bpy.types.Operator):
bl_idname = "bim.remove_address_attribute"
bl_label = "Remove Address Attribute"
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty()
id: bpy.props.IntProperty()
name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
items=tuple((i, i, "") for i in get_args(tool.Owner.AddressAttributeType)),
)
id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
def _execute(self, context):
if TYPE_CHECKING:
name: tool.Owner.AddressAttributeType # pyright: ignore[reportIncompatibleVariableOverride]
id: int
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
core.remove_address_attribute(tool.Owner, name=self.name, id=self.id)
return {"FINISHED"}
class EnableEditingAddress(bpy.types.Operator, tool.Ifc.Operator):
class EnableEditingAddress(bpy.types.Operator):
bl_idname = "bim.enable_editing_address"
bl_label = "Enable Editing Address"
bl_options = {"REGISTER", "UNDO"}
address: bpy.props.IntProperty()
address: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
def _execute(self, context):
if TYPE_CHECKING:
address: int
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
core.enable_editing_address(tool.Owner, address=tool.Ifc.get().by_id(self.address))
return {"FINISHED"}
class DisableEditingAddress(bpy.types.Operator, tool.Ifc.Operator):
class DisableEditingAddress(bpy.types.Operator):
bl_idname = "bim.disable_editing_address"
bl_label = "Disable Editing Address"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
core.disable_editing_address(tool.Owner)
return {"FINISHED"}
class EditAddress(bpy.types.Operator, tool.Ifc.Operator):
@@ -201,29 +262,37 @@ class RemoveAddress(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_address"
bl_label = "Remove Address"
bl_options = {"REGISTER", "UNDO"}
address: bpy.props.IntProperty()
address: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
address: int
def _execute(self, context):
core.remove_address(tool.Ifc, address=tool.Ifc.get().by_id(self.address))
class EnableEditingOrganisation(bpy.types.Operator, tool.Ifc.Operator):
class EnableEditingOrganisation(bpy.types.Operator):
bl_idname = "bim.enable_editing_organisation"
bl_label = "Enable Editing Organisation"
bl_options = {"REGISTER", "UNDO"}
organisation: bpy.props.IntProperty()
organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
def _execute(self, context):
if TYPE_CHECKING:
organisation: int
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
core.enable_editing_organisation(tool.Owner, organisation=tool.Ifc.get().by_id(self.organisation))
return {"FINISHED"}
class DisableEditingOrganisation(bpy.types.Operator, tool.Ifc.Operator):
class DisableEditingOrganisation(bpy.types.Operator):
bl_idname = "bim.disable_editing_organisation"
bl_label = "Disable Editing Organisation"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
core.disable_editing_organisation(tool.Owner)
return {"FINISHED"}
class AddOrganisation(bpy.types.Operator, tool.Ifc.Operator):
@@ -248,7 +317,10 @@ class RemoveOrganisation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_organisation"
bl_label = "Remove Organisation"
bl_options = {"REGISTER", "UNDO"}
organisation: bpy.props.IntProperty()
organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
organisation: int
def _execute(self, context):
core.remove_organisation(tool.Ifc, tool.Ifc.get().by_id(self.organisation))
@@ -258,8 +330,12 @@ class AddPersonAndOrganisation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_person_and_organisation"
bl_label = "Add Person And Organisation"
bl_options = {"REGISTER", "UNDO"}
person: bpy.props.IntProperty()
organisation: bpy.props.IntProperty()
person: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
person: int
organisation: int
def _execute(self, context):
core.add_person_and_organisation(
@@ -271,7 +347,10 @@ class RemovePersonAndOrganisation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_person_and_organisation"
bl_label = "Remove Person And Organisation"
bl_options = {"REGISTER", "UNDO"}
person_and_organisation: bpy.props.IntProperty()
person_and_organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
person_and_organisation: int
def _execute(self, context):
core.remove_person_and_organisation(
@@ -279,24 +358,29 @@ class RemovePersonAndOrganisation(bpy.types.Operator, tool.Ifc.Operator):
)
class SetUser(bpy.types.Operator, tool.Ifc.Operator):
class SetUser(bpy.types.Operator):
bl_idname = "bim.set_user"
bl_label = "Set User"
bl_options = {"REGISTER", "UNDO"}
user: bpy.props.IntProperty()
user: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
def _execute(self, context):
if TYPE_CHECKING:
user: int
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
core.set_user(tool.Owner, user=tool.Ifc.get().by_id(self.user))
return {"FINISHED"}
class ClearUser(bpy.types.Operator, tool.Ifc.Operator):
class ClearUser(bpy.types.Operator):
bl_idname = "bim.clear_user"
bl_label = "Clear User"
bl_options = {"REGISTER", "UNDO"}
user: bpy.props.IntProperty()
def _execute(self, context):
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
core.clear_user(tool.Owner)
return {"FINISHED"}
class AddActor(bpy.types.Operator, tool.Ifc.Operator):
@@ -305,28 +389,33 @@ class AddActor(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMOwnerProperties
props = tool.Owner.get_owner_props()
if props.the_actor:
core.add_actor(tool.Ifc, ifc_class=props.actor_class, actor=tool.Ifc.get().by_id(int(props.the_actor)))
class EnableEditingActor(bpy.types.Operator, tool.Ifc.Operator):
class EnableEditingActor(bpy.types.Operator):
bl_idname = "bim.enable_editing_actor"
bl_label = "Enable Editing Actor"
bl_options = {"REGISTER", "UNDO"}
actor: bpy.props.IntProperty()
actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
def _execute(self, context):
if TYPE_CHECKING:
actor: int
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
core.enable_editing_actor(tool.Owner, actor=tool.Ifc.get().by_id(self.actor))
return {"FINISHED"}
class DisableEditingActor(bpy.types.Operator, tool.Ifc.Operator):
class DisableEditingActor(bpy.types.Operator):
bl_idname = "bim.disable_editing_actor"
bl_label = "Disable Editing Actor"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
core.disable_editing_actor(tool.Owner)
return {"FINISHED"}
class EditActor(bpy.types.Operator, tool.Ifc.Operator):
@@ -342,7 +431,10 @@ class RemoveActor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_actor"
bl_label = "Remove Actor"
bl_options = {"REGISTER", "UNDO"}
actor: bpy.props.IntProperty()
actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
actor: int
def _execute(self, context):
core.remove_actor(tool.Ifc, actor=tool.Ifc.get().by_id(self.actor))
@@ -352,21 +444,27 @@ class AssignActor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_actor"
bl_label = "Assign Actor"
bl_options = {"REGISTER", "UNDO"}
actor: bpy.props.IntProperty()
actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
actor: int
def _execute(self, context):
core.assign_actor(
tool.Ifc, actor=tool.Ifc.get().by_id(self.actor), element=tool.Ifc.get_entity(context.active_object)
)
assert (obj := context.active_object)
assert (element := tool.Ifc.get_entity(obj))
core.assign_actor(tool.Ifc, actor=tool.Ifc.get().by_id(self.actor), element=element)
class UnassignActor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unassign_actor"
bl_label = "Unassign Actor"
bl_options = {"REGISTER", "UNDO"}
actor: bpy.props.IntProperty()
actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
actor: int
def _execute(self, context):
core.unassign_actor(
tool.Ifc, actor=tool.Ifc.get().by_id(self.actor), element=tool.Ifc.get_entity(context.active_object)
)
assert (obj := context.active_object)
assert (element := tool.Ifc.get_entity(obj))
core.unassign_actor(tool.Ifc, actor=tool.Ifc.get().by_id(self.actor), element=element)
+36 -8
View File
@@ -20,6 +20,7 @@ import bpy
import bonsai.tool as tool
from bonsai.bim.prop import StrProperty, Attribute
from bonsai.bim.module.owner.data import OwnerData, ActorData, ObjectActorData
from typing import TYPE_CHECKING, Literal
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
@@ -33,45 +34,45 @@ from bpy.props import (
)
def get_user_person(self, context):
def get_user_person(self: "BIMOwnerProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
if not OwnerData.is_loaded:
OwnerData.load()
return OwnerData.data["user_person"]
def get_user_organisation(self, context):
def get_user_organisation(self: "BIMOwnerProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
if not OwnerData.is_loaded:
OwnerData.load()
return OwnerData.data["user_organisation"]
def get_the_actor(self, context):
def get_the_actor(self: "BIMOwnerProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
if not ActorData.is_loaded:
ActorData.load()
return ActorData.data["the_actor"]
def get_actor(self, context):
def get_actor(self: "BIMOwnerProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
if not ObjectActorData.is_loaded:
ObjectActorData.load()
return ObjectActorData.data["actor"]
def update_actor_type(self, context):
def update_actor_type(self: "BIMOwnerProperties", context: bpy.types.Context) -> None:
ActorData.data["the_actor"] = ActorData.the_actor()
def update_actor_class(self, context):
def update_actor_class(self: "BIMOwnerProperties", context: bpy.types.Context) -> None:
ActorData.data["actors"] = ActorData.actors()
def get_actor_class(self, context):
def get_actor_class(self: "BIMOwnerProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
if not ActorData.is_loaded:
ActorData.load()
return ActorData.data["actor_class"]
def get_actor_type(self, context):
def get_actor_type(self: "BIMOwnerProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
if not ActorData.is_loaded:
ActorData.load()
return ActorData.data["actor_type"]
@@ -119,3 +120,30 @@ class BIMOwnerProperties(PropertyGroup):
items=get_the_actor, name="Actor", description="This entity represents an individual human being."
)
actor: EnumProperty(items=get_actor, name="Actor")
if TYPE_CHECKING:
active_person_id: int
person_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
middle_names: bpy.types.bpy_prop_collection_idprop[StrProperty]
prefix_titles: bpy.types.bpy_prop_collection_idprop[StrProperty]
suffix_titles: bpy.types.bpy_prop_collection_idprop[StrProperty]
active_organisation_id: int
organisation_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
active_role_id: int
role_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
active_address_id: int
address_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
address_lines: bpy.types.bpy_prop_collection_idprop[StrProperty]
telephone_numbers: bpy.types.bpy_prop_collection_idprop[StrProperty]
facsimile_numbers: bpy.types.bpy_prop_collection_idprop[StrProperty]
electronic_mail_addresses: bpy.types.bpy_prop_collection_idprop[StrProperty]
messaging_ids: bpy.types.bpy_prop_collection_idprop[StrProperty]
user_person: str
user_organisation: str
active_user_id: int
active_actor_id: int
actor_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
actor_class: Literal["IfcActor", "IfcOccupant"]
actor_type: str
the_actor: str
actor: str
+22 -13
View File
@@ -19,10 +19,11 @@
import bpy
import bonsai.bim.helper
import bonsai.tool as tool
from typing import Any
from bonsai.bim.module.owner.data import PeopleData, OrganisationsData, OwnerData, ActorData, ObjectActorData
def draw_roles(box, parent):
def draw_roles(box: bpy.types.UILayout, parent: dict[str, Any]) -> None:
row = box.row(align=True)
row.label(text="Roles")
op = row.operator("bim.add_role", icon="ADD", text="")
@@ -41,7 +42,7 @@ def draw_roles(box, parent):
row.operator("bim.remove_role", icon="X", text="").role = role["id"]
def draw_addresses(box, parent):
def draw_addresses(box: bpy.types.UILayout, parent: dict[str, Any]) -> None:
row = box.row(align=True)
row.label(text="Addresses")
op = row.operator("bim.add_address", icon="LINK_BLEND", text="")
@@ -87,12 +88,13 @@ class BIM_PT_people(bpy.types.Panel):
@classmethod
def poll(cls, context):
return tool.Ifc.get()
return bool(tool.Ifc.get())
def draw(self, context):
if not PeopleData.is_loaded:
PeopleData.load()
assert self.layout
self.layout.use_property_split = True
self.layout.use_property_decorate = False
@@ -102,7 +104,8 @@ class BIM_PT_people(bpy.types.Panel):
for person in PeopleData.data["people"]:
self.draw_person(person)
def draw_person(self, person):
def draw_person(self, person: dict[str, Any]) -> None:
assert self.layout
if person["is_editing"]:
box = self.layout.box()
row = box.row(align=True)
@@ -146,12 +149,13 @@ class BIM_PT_organisations(bpy.types.Panel):
@classmethod
def poll(cls, context):
return tool.Ifc.get()
return bool(tool.Ifc.get())
def draw(self, context):
if not OrganisationsData.is_loaded:
OrganisationsData.load()
assert self.layout
self.layout.use_property_split = True
self.layout.use_property_decorate = False
@@ -161,7 +165,8 @@ class BIM_PT_organisations(bpy.types.Panel):
for organisation in OrganisationsData.data["organisations"]:
self.draw_organisation(organisation)
def draw_organisation(self, organisation):
def draw_organisation(self, organisation: dict[str, Any]) -> None:
assert self.layout
if organisation["is_editing"]:
box = self.layout.box()
row = box.row(align=True)
@@ -193,15 +198,16 @@ class BIM_PT_owner(bpy.types.Panel):
@classmethod
def poll(cls, context):
return tool.Ifc.get()
return bool(tool.Ifc.get())
def draw(self, context):
if not OwnerData.is_loaded:
OwnerData.load()
assert self.layout
self.layout.use_property_split = True
self.layout.use_property_decorate = False
props = context.scene.BIMOwnerProperties
props = tool.Owner.get_owner_props()
if not OwnerData.data["user_person"]:
self.layout.label(text="No people found.")
@@ -244,13 +250,14 @@ class BIM_PT_actor(bpy.types.Panel):
@classmethod
def poll(cls, context):
return tool.Ifc.get()
return bool(tool.Ifc.get())
def draw(self, context):
if not ActorData.is_loaded:
ActorData.load()
self.props = context.scene.BIMOwnerProperties
assert self.layout
self.props = tool.Owner.get_owner_props()
self.layout.use_property_split = True
self.layout.use_property_decorate = False
@@ -268,7 +275,8 @@ class BIM_PT_actor(bpy.types.Panel):
for actor in ActorData.data["actors"]:
self.draw_actor(actor)
def draw_actor(self, actor):
def draw_actor(self, actor: dict[str, Any]) -> None:
assert self.layout
if actor["is_editing"]:
box = self.layout.box()
row = box.row(align=True)
@@ -295,13 +303,14 @@ class BIM_PT_object_actor(bpy.types.Panel):
@classmethod
def poll(cls, context):
return tool.Ifc.get()
return bool(tool.Ifc.get())
def draw(self, context):
if not ObjectActorData.is_loaded:
ObjectActorData.load()
self.props = context.scene.BIMOwnerProperties
assert self.layout
self.props = tool.Owner.get_owner_props()
if not ObjectActorData.data["actor"]:
row = self.layout.row(align=True)
@@ -2633,6 +2633,7 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}
) or single_mode:
context.workspace.status_text_set(text=None)
self.tool_state.plane_method = None
PolylineDecorator.uninstall()
tool.Polyline.move_polyline_to_measure(context, self.input_ui)
tool.Polyline.clear_polyline()
@@ -58,7 +58,7 @@ def updateResourceName(self, context):
attributes={"Name": self.name},
)
if props.active_resource_id == self.ifc_definition_id:
attribute = props.resource_attributes.get("Name")
attribute = props.resource_attributes["Name"]
attribute.string_value = self.name
bonsai.bim.module.resource.data.refresh()
tool.Sequence.refresh_task_resources()
+21 -19
View File
@@ -118,29 +118,31 @@ class IfcClassData:
def representation_template(cls):
rprops = tool.Root.get_root_props()
ifc_class = rprops.ifc_class
if ifc_class.startswith("IfcStructuralPoint"):
return [("VERTEX", "Vertex", "A single 3D point")]
elif ifc_class.startswith("IfcStructuralCurve"):
return [("EDGE", "Edge", "A straight edge between two points")]
elif ifc_class.startswith("IfcStructuralSurface"):
return [("FACE", "Face", "A planar face surface")]
templates = [
("EMPTY", "No Geometry", "Start with an empty object"),
None,
(
"OBJ",
"Tessellation From Object",
"Use an object as a template to create a new tessellation",
),
(
"MESH",
"Custom Tessellation",
"Create a basic tessellated or faceted cube",
),
(
"EXTRUSION",
"Custom Extruded Solid",
"An extrusion from an arbitrary profile",
),
]
templates.extend(
[
(
"OBJ",
"Tessellation From Object",
"Use an object as a template to create a new tessellation",
),
(
"MESH",
"Custom Tessellation",
"Create a basic tessellated or faceted cube",
),
(
"EXTRUSION",
"Custom Extruded Solid",
"An extrusion from an arbitrary profile",
),
]
)
if ifc_class.endswith("Type") or ifc_class.endswith("Style"):
templates.extend(
[
+70 -2
View File
@@ -195,6 +195,17 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
should_add_representation: bool
ifc_representation_class: str
@classmethod
def poll(cls, context):
if not tool.Ifc.get():
cls.poll_message_set("No IFC project loaded.")
return False
# object.select_all operator's requirement and it's generally more safe.
elif context.mode != "OBJECT":
cls.poll_message_set(f"Can only assign class in OBJECT mode, not in {context.mode} mode.")
return False
return True
def _execute(self, context):
ifc_file = tool.Ifc.get()
props = tool.Root.get_root_props()
@@ -263,8 +274,15 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
# Apply geometry.
if obj.modifiers:
ensure_single_user_mesh(obj.data)
# In older Blender versions 'object.convert'
# still requires object to be both selected and active.
is_old_blender = bpy.app.version < (4, 4, 0)
with context.temp_override(selected_editable_objects=[obj]):
if is_old_blender:
tool.Blender.set_active_object(obj)
bpy.ops.object.convert(target="MESH")
if is_old_blender:
tool.Blender.clear_objects_selection()
# Apply scale.
if obj.scale != (1, 1, 1):
@@ -456,8 +474,15 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
props.representation_template = "EXTRUSION"
props.representation_obj = None
elif (obj := tool.Blender.get_active_object(is_selected=True)) and obj.type == "MESH":
props.representation_template = "OBJ"
props.representation_obj = obj
if (
props.ifc_class.startswith("IfcStructuralPoint")
or props.ifc_class.startswith("IfcStructuralCurve")
or props.ifc_class.startswith("IfcStructuralSurface")
):
pass # Implement auto association?
else:
props.representation_template = "OBJ"
props.representation_obj = obj
# For convenience, preselect IFC class
if self.ifc_product:
props.ifc_product = self.ifc_product
@@ -666,6 +691,49 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
elif representation_template == "ROOF":
with context.temp_override(active_object=obj, selected_objects=[]):
bpy.ops.bim.add_roof()
elif representation_template == "VERTEX":
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
representation = builder.get_representation(ifc_context, [builder.vertex()])
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
elif representation_template == "EDGE":
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
end = Vector((1, 0, 0)) / unit_scale
representation = builder.get_representation(ifc_context, [builder.edge(end=end)])
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
elif representation_template == "FACE":
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
points = [Vector(p) / unit_scale for p in ((0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0))]
representation = builder.get_representation(ifc_context, [builder.face(points)])
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
bpy.context.view_layer.update() # Ensures obj.matrix_world is correct
@@ -512,7 +512,7 @@ class AssignProcess(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Assign Process"
bl_options = {"REGISTER", "UNDO"}
task: bpy.props.IntProperty()
related_object_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
related_object_type: bpy.props.EnumProperty( # pyright: ignore [reportRedeclaration]
items=[(i, i, "") for i in get_args(tool.Sequence.RELATED_OBJECT_TYPE)],
)
related_object: bpy.props.IntProperty()
@@ -549,7 +549,7 @@ class UnassignProcess(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Unassign Process"
bl_options = {"REGISTER", "UNDO"}
task: bpy.props.IntProperty()
related_object_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
related_object_type: bpy.props.EnumProperty( # pyright: ignore [reportRedeclaration]
items=[(i, i, "") for i in get_args(tool.Sequence.RELATED_OBJECT_TYPE)],
)
related_object: bpy.props.IntProperty()
@@ -426,7 +426,7 @@ class EnableEditingZone(bpy.types.Operator):
return {"FINISHED"}
class DisableEditingZone(bpy.types.Operator, tool.Ifc.Operator):
class DisableEditingZone(bpy.types.Operator):
bl_idname = "bim.disable_editing_zone"
bl_label = "Disable Editing Zone"
bl_options = {"REGISTER", "UNDO"}
+32
View File
@@ -1286,3 +1286,35 @@ class CopyTextToClipboard(bpy.types.Operator):
def execute(self, context):
context.window_manager.clipboard = self.text
return {"FINISHED"}
class ShowSystemInfo(bpy.types.Operator):
bl_idname = "bim.show_system_info"
bl_label = "System Info"
bl_description = "Show debug information and copy it to the clipboard."
bl_options = set()
info_text: bpy.props.StringProperty()
def execute(self, context):
# invoke_popup doesn't show a popup, if there's no `execute` method.
return {"FINISHED"}
def invoke(self, context, event):
assert context.window_manager
# Invoke the bim.copy_debug_information operator to get the info to the clipboard
bpy.ops.bim.copy_debug_information()
self.info_text = context.window_manager.clipboard
return context.window_manager.invoke_popup(self, width=600)
def draw(self, context):
assert self.layout
layout = self.layout
col = layout.column()
for line in self.info_text.split("\n"):
col.label(text=line)
col.separator()
col.label(text="(The information has been copied to the clipboard.)")
+4 -4
View File
@@ -287,11 +287,11 @@ AttributeSpecialType = Literal["", "DATE", "DATETIME", "LENGTH", "AREA", "VOLUME
class Attribute(PropertyGroup):
tooltip = "`Right Click > IFC Description` to read the attribute description and online documentation"
name: StringProperty(name="Name") # type: ignore [reportRedeclaration]
name: StringProperty(name="Name") # pyright: ignore [reportRedeclaration]
display_name: StringProperty(name="Display Name", get=get_display_name)
description: StringProperty(name="Description") # type: ignore [reportRedeclaration]
ifc_class: StringProperty(name="Ifc Class") # type: ignore [reportRedeclaration]
data_type: EnumProperty( # type: ignore [reportRedeclaration]
description: StringProperty(name="Description") # pyright: ignore [reportRedeclaration]
ifc_class: StringProperty(name="Ifc Class") # pyright: ignore [reportRedeclaration]
data_type: EnumProperty( # pyright: ignore [reportRedeclaration]
name="Data Type",
items=[(i, i, "") for i in get_args(AttributeDataType)],
)
+37 -9
View File
@@ -420,6 +420,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
layout.prop(dprops, "imperial_precision")
layout.prop(dprops, "tolerance")
layout.prop(dprops, "classes_to_wireframe")
layout.prop(dprops, "classes_no_cut")
def draw_decorator_colors(self, layout, context):
layout.row().prop(self, "decorations_colour")
@@ -434,6 +435,10 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
props = tool.Project.get_project_props()
layout.prop(props, "should_disable_undo_on_save")
layout.prop(props, "should_stream")
bprops = tool.Bsdd.get_bsdd_props()
layout.prop(bprops, "load_preview_dictionaries")
layout.prop(bprops, "load_inactive_dictionaries")
layout.prop(bprops, "load_test_dictionaries")
# Scene panel groups
@@ -531,11 +536,18 @@ class BIM_PT_tabs(Panel):
row.operator("bim.close_blend_warning", text="", icon="CANCEL")
gprops = tool.Geometry.get_geometry_props()
# Check that Blender mode and IFC Mode do match.
if context.mode == "OBJECT" and gprops.mode in ("OBJECT", "ITEM"):
pass
elif context.mode.startswith("EDIT") and gprops.mode == "EDIT":
pass
else:
# Occurs when user wasn't using IFC mode or TAB/Esc hotkeys to change mode.
# E.g. if user used TAB to set object's mode to EDIT and then used
# Blender mode property to set it back to OBJECT.
#
# We show warning only for objects connected to IFC,
# so users with vanilla Blender objects won't be affected.
elif tool.Geometry.get_active_or_representation_obj():
box = self.layout.box()
box.alert = True
row = box.row(align=True)
@@ -1182,8 +1194,10 @@ class UIData:
def draw_statusbar(self, context):
if not UIData.is_loaded:
UIData.load()
text = f"Bonsai v{UIData.data['version']}"
self.layout.label(text=text)
bonsai_version = f"Bonsai v{UIData.data['version']}"
layout = self.layout
row = layout.row()
row.operator("bim.show_system_info", text=bonsai_version, emboss=False)
def draw_custom_context_menu(self: bpy.types.Menu, context: bpy.types.Context) -> None:
@@ -1194,16 +1208,23 @@ def draw_custom_context_menu(self: bpy.types.Menu, context: bpy.types.Context) -
or not hasattr(context.button_prop, "identifier")
):
return
prop = context.button_pointer
prop_name = context.button_prop.identifier
prop_value = getattr(prop, prop_name, None)
if not isinstance(prop_value, str):
# E.g. `bim.props.Attribute`.
prop_struct: bpy.types.bpy_struct = context.button_pointer
# E.g. `IntProperty("int_value")`
prop: bpy.types.Property = context.button_prop
prop_name = prop.identifier
# TODO: when `prop_struct` doesn't have `prop_name`?
if not hasattr(prop_struct, prop_name):
return
prop_value = getattr(prop_struct, prop_name, ...)
if prop_value is ...:
return
version = tool.Ifc.get_schema()
assert self.layout
layout = self.layout
if isinstance(context.button_pointer, Attribute):
attr = context.button_pointer
if isinstance(prop_struct, Attribute):
attr = prop_struct
description = attr.description
ifc_class = attr.ifc_class
if ifc_class:
@@ -1226,6 +1247,13 @@ def draw_custom_context_menu(self: bpy.types.Menu, context: bpy.types.Context) -
op = layout.operator("bim.copy_text_to_clipboard", text="Copy Attribute Name", icon="COPYDOWN")
op.text = attr_name
else:
# Basically context menu for any Blender property will end up here,
# and will check 3 types of docs.
# So at least we're skipping all non-string properties.
if not isinstance(prop_value, str):
return
docs = None
# Ugly but we can't know which type of data is under the cursor so we test everything until it clicks
try:
docs = get_entity_doc(version, prop_value)
+7 -18
View File
@@ -26,9 +26,9 @@ if TYPE_CHECKING:
import bonsai.tool as tool
def get_class_properties(client: bsdd.Client, bsdd: tool.Bsdd) -> dict[str, dict[str, Any]]:
def get_class_properties(bsdd: tool.Bsdd) -> dict[str, dict[str, Any]]:
bsdd.clear_class_psets()
data = bsdd.get_active_class_data(client)
data = bsdd.get_active_class_data()
pset_dict = bsdd.get_property_dict(data)
if pset_dict is None:
return {}
@@ -36,23 +36,12 @@ def get_class_properties(client: bsdd.Client, bsdd: tool.Bsdd) -> dict[str, dict
return pset_dict
def load_bsdd(client: bsdd.Client, bsdd: tool.Bsdd) -> None:
bsdd.clear_domains()
if bsdd.should_load_preview_domains():
dictionaries = bsdd.get_dictionaries(client)
else:
dictionaries = bsdd.get_dictionaries(client, "Active")
bsdd.create_dictionaries(dictionaries)
def load_bsdd(bsdd: tool.Bsdd) -> None:
bsdd.clear_dictionaries()
bsdd.create_dictionaries(bsdd.get_dictionaries())
def search_class(keyword: str, client: bsdd.Client, bsdd: tool.Bsdd) -> int:
def search_class(keyword: str, bsdd: tool.Bsdd) -> int:
bsdd.clear_classes()
related_entities = bsdd.get_related_ifc_entities()
active_dictionary_uri = bsdd.get_active_dictionary_uri()
classes = bsdd.search_class(client, keyword, [active_dictionary_uri], related_entities)
bsdd.create_classes(classes)
return len(classes)
def set_active_bsdd_dictionary(name: str, uri: str, bsdd: tool.Bsdd) -> None:
bsdd.set_active_bsdd(name, uri)
return bsdd.search_class(keyword, related_entities)
+64 -46
View File
@@ -27,16 +27,16 @@ if TYPE_CHECKING:
import bonsai.tool as tool
def enable_editing_text(drawing: tool.Drawing, obj: bpy.types.Object) -> None:
def enable_editing_text(drawing: type[tool.Drawing], obj: bpy.types.Object) -> None:
drawing.enable_editing_text(obj)
drawing.import_text_attributes(obj)
def disable_editing_text(drawing: tool.Drawing, obj: bpy.types.Object) -> None:
def disable_editing_text(drawing: type[tool.Drawing], obj: bpy.types.Object) -> None:
drawing.disable_editing_text(obj)
def edit_text(drawing: tool.Drawing, obj: bpy.types.Object) -> None:
def edit_text(drawing: type[tool.Drawing], obj: bpy.types.Object) -> None:
drawing.synchronise_ifc_and_text_attributes(obj)
drawing.update_text_size_pset(obj)
drawing.update_newline_at(obj)
@@ -44,17 +44,20 @@ def edit_text(drawing: tool.Drawing, obj: bpy.types.Object) -> None:
drawing.disable_editing_text(obj)
def enable_editing_assigned_product(drawing: tool.Drawing, obj: bpy.types.Object) -> None:
def enable_editing_assigned_product(drawing: type[tool.Drawing], obj: bpy.types.Object) -> None:
drawing.enable_editing_assigned_product(obj)
drawing.import_assigned_product(obj)
def disable_editing_assigned_product(drawing: tool.Drawing, obj: bpy.types.Object) -> None:
def disable_editing_assigned_product(drawing: type[tool.Drawing], obj: bpy.types.Object) -> None:
drawing.disable_editing_assigned_product(obj)
def edit_assigned_product(
ifc: tool.Ifc, drawing: tool.Drawing, obj: bpy.types.Object, product: Optional[ifcopenshell.entity_instance] = None
ifc: type[tool.Ifc],
drawing: type[tool.Drawing],
obj: bpy.types.Object,
product: Optional[ifcopenshell.entity_instance] = None,
) -> None:
element = ifc.get_entity(obj)
assert element
@@ -70,16 +73,16 @@ def edit_assigned_product(
drawing.disable_editing_assigned_product(obj)
def load_sheets(drawing: tool.Drawing) -> None:
def load_sheets(drawing: type[tool.Drawing]) -> None:
drawing.import_sheets()
drawing.enable_editing_sheets()
def disable_editing_sheets(drawing: tool.Drawing) -> None:
def disable_editing_sheets(drawing: type[tool.Drawing]) -> None:
drawing.disable_editing_sheets()
def add_sheet(ifc: tool.Ifc, drawing: tool.Drawing, titleblock: ifcopenshell.entity_instance) -> None:
def add_sheet(ifc: type[tool.Ifc], drawing: type[tool.Drawing], titleblock: ifcopenshell.entity_instance) -> None:
sheet = ifc.run("document.add_information")
layout = ifc.run("document.add_reference", information=sheet)
titleblock_reference = ifc.run("document.add_reference", information=sheet)
@@ -105,7 +108,7 @@ def add_sheet(ifc: tool.Ifc, drawing: tool.Drawing, titleblock: ifcopenshell.ent
drawing.import_sheets()
def regenerate_sheet(drawing: tool.Drawing, sheet: ifcopenshell.entity_instance) -> None:
def regenerate_sheet(drawing: type[tool.Drawing], sheet: ifcopenshell.entity_instance) -> None:
titleblock_uri = drawing.get_document_uri(sheet, "TITLEBLOCK")
drawing.create_svg_sheet(sheet, drawing.sanitise_filename(Path(titleblock_uri).stem))
try:
@@ -116,11 +119,11 @@ def regenerate_sheet(drawing: tool.Drawing, sheet: ifcopenshell.entity_instance)
drawing.delete_file(path_layout)
def open_layout(drawing: tool.Drawing, sheet: ifcopenshell.entity_instance) -> None:
def open_layout(drawing: type[tool.Drawing], sheet: ifcopenshell.entity_instance) -> None:
drawing.open_layout_svg(drawing.get_document_uri(sheet, "LAYOUT"))
def remove_sheet(ifc: tool.Ifc, drawing: tool.Drawing, sheet: ifcopenshell.entity_instance) -> None:
def remove_sheet(ifc: type[tool.Ifc], drawing: type[tool.Drawing], sheet: ifcopenshell.entity_instance) -> None:
for reference in drawing.get_document_references(sheet):
if drawing.get_reference_description(reference) in ("LAYOUT", "SHEET", "REVISION", "RASTER"):
uri = ifc.resolve_uri(drawing.get_document_uri(reference))
@@ -131,7 +134,11 @@ def remove_sheet(ifc: tool.Ifc, drawing: tool.Drawing, sheet: ifcopenshell.entit
def rename_sheet(
ifc: tool.Ifc, drawing: tool.Drawing, sheet: ifcopenshell.entity_instance, identification: str, name: str
ifc: type[tool.Ifc],
drawing: type[tool.Drawing],
sheet: ifcopenshell.entity_instance,
identification: str,
name: str,
) -> None:
if ifc.get_schema() == "IFC2X3":
attributes = {"DocumentId": identification, "Name": name}
@@ -159,31 +166,33 @@ def rename_sheet(
def rename_reference(
ifc: tool.Ifc, drawing: tool.Drawing, reference: ifcopenshell.entity_instance, identification: str
ifc: type[tool.Ifc], drawing: type[tool.Drawing], reference: ifcopenshell.entity_instance, identification: str
) -> None:
attributes = drawing.generate_reference_attributes(reference, Identification=identification)
ifc.run("document.edit_reference", reference=reference, attributes=attributes)
def load_schedules(drawing: tool.Drawing) -> None:
def load_schedules(drawing: type[tool.Drawing]) -> None:
drawing.import_documents("SCHEDULE")
drawing.enable_editing_schedules()
def load_references(drawing: tool.Drawing) -> None:
def load_references(drawing: type[tool.Drawing]) -> None:
drawing.import_documents("REFERENCE")
drawing.enable_editing_references()
def disable_editing_schedules(drawing: tool.Drawing) -> None:
def disable_editing_schedules(drawing: type[tool.Drawing]) -> None:
drawing.disable_editing_schedules()
def disable_editing_references(drawing: tool.Drawing) -> None:
def disable_editing_references(drawing: type[tool.Drawing]) -> None:
drawing.disable_editing_references()
def add_document(ifc: tool.Ifc, drawing: tool.Drawing, document_type: tool.Drawing.DOCUMENT_TYPE, uri: str) -> None:
def add_document(
ifc: type[tool.Ifc], drawing: type[tool.Drawing], document_type: type[tool.Drawing].DOCUMENT_TYPE, uri: str
) -> None:
document = ifc.run("document.add_information")
reference = ifc.run("document.add_reference", information=document)
name = drawing.get_path_filename(uri)
@@ -197,49 +206,50 @@ def add_document(ifc: tool.Ifc, drawing: tool.Drawing, document_type: tool.Drawi
def remove_document(
ifc: tool.Ifc,
drawing: tool.Drawing,
document_type: tool.Drawing.DOCUMENT_TYPE,
ifc: type[tool.Ifc],
drawing: type[tool.Drawing],
document_type: type[tool.Drawing].DOCUMENT_TYPE,
document: ifcopenshell.entity_instance,
) -> None:
ifc.run("document.remove_information", information=document)
drawing.import_documents(document_type)
def open_schedule(drawing: tool.Drawing, schedule: ifcopenshell.entity_instance) -> None:
def open_schedule(drawing: type[tool.Drawing], schedule: ifcopenshell.entity_instance) -> None:
drawing.open_spreadsheet(drawing.get_document_uri(schedule))
def open_reference(drawing: tool.Drawing, reference: ifcopenshell.entity_instance) -> None:
def open_reference(drawing: type[tool.Drawing], reference: ifcopenshell.entity_instance) -> None:
drawing.open_svg(drawing.get_document_uri(reference))
def update_document_name(
ifc: tool.Ifc, drawing: tool.Drawing, document: ifcopenshell.entity_instance, name=None
ifc: type[tool.Ifc], drawing: type[tool.Drawing], document: ifcopenshell.entity_instance, name=None
) -> None:
if drawing.get_name(document) != name:
ifc.run("document.edit_information", information=document, attributes={"Name": name})
def load_drawings(drawing: tool.Drawing) -> None:
def load_drawings(drawing: type[tool.Drawing]) -> None:
drawing.import_drawings()
drawing.enable_editing_drawings()
def disable_editing_drawings(drawing: tool.Drawing) -> None:
def disable_editing_drawings(drawing: type[tool.Drawing]) -> None:
drawing.disable_editing_drawings()
def add_drawing(
ifc: tool.Ifc,
collector: tool.Collector,
drawing: tool.Drawing,
target_view: Union[ifcopenshell.util.representation.TARGET_VIEW, None] = None,
location_hint: Union[str, None] = None,
ifc: type[tool.Ifc],
collector: type[tool.Collector],
drawing: type[tool.Drawing],
target_view: ifcopenshell.util.representation.TARGET_VIEW,
location_hint: Union[tool.Drawing.LocationHintLiteral, int],
) -> None:
assert location_hint is not None
drawing_name = drawing.ensure_unique_drawing_name(drawing.generate_drawing_name(target_view, location_hint))
drawing_matrix = drawing.generate_drawing_matrix(target_view, location_hint)
camera = drawing.create_camera(drawing_name, drawing_matrix, location_hint)
camera = drawing.create_camera(drawing_name, drawing_matrix, location_hint, target_view)
element = drawing.run_root_assign_class(
obj=camera,
ifc_class="IfcAnnotation",
@@ -295,10 +305,10 @@ def add_drawing(
def duplicate_drawing(
ifc: tool.Ifc,
blender: tool.Blender,
drawing_tool: tool.Drawing,
geometry: tool.Geometry,
ifc: type[tool.Ifc],
blender: type[tool.Blender],
drawing_tool: type[tool.Drawing],
geometry: type[tool.Geometry],
drawing: ifcopenshell.entity_instance,
should_duplicate_annotations: bool = False,
) -> ifcopenshell.entity_instance:
@@ -341,7 +351,9 @@ def duplicate_drawing(
return new_drawing
def remove_drawing(ifc: tool.Ifc, drawing_tool: tool.Drawing, drawing: ifcopenshell.entity_instance) -> None:
def remove_drawing(
ifc: type[tool.Ifc], drawing_tool: type[tool.Drawing], drawing: ifcopenshell.entity_instance
) -> None:
if drawing_tool.is_active_drawing(drawing):
drawing_tool.run_drawing_activate_model()
@@ -370,7 +382,7 @@ def remove_drawing(ifc: tool.Ifc, drawing_tool: tool.Drawing, drawing: ifcopensh
def update_drawing_name(
ifc: tool.Ifc, drawing_tool: tool.Drawing, drawing: ifcopenshell.entity_instance, name: str
ifc: type[tool.Ifc], drawing_tool: type[tool.Drawing], drawing: ifcopenshell.entity_instance, name: str
) -> None:
if drawing_tool.get_name(drawing) != name:
ifc.run("attribute.edit_attributes", product=drawing, attributes={"Name": name})
@@ -406,9 +418,9 @@ def update_drawing_name(
def add_annotation(
ifc: tool.Ifc,
collector: tool.Collector,
drawing_tool: tool.Drawing,
ifc: type[tool.Ifc],
collector: type[tool.Collector],
drawing_tool: type[tool.Drawing],
drawing: ifcopenshell.entity_instance,
object_type: str,
relating_type: ifcopenshell.entity_instance,
@@ -444,13 +456,16 @@ def add_annotation(
return obj
def build_schedule(drawing: tool.Drawing, schedule: ifcopenshell.entity_instance) -> None:
def build_schedule(drawing: type[tool.Drawing], schedule: ifcopenshell.entity_instance) -> None:
drawing.create_svg_schedule(schedule)
drawing.open_svg(drawing.get_path_with_ext(drawing.get_document_uri(schedule), "svg"))
def sync_references(
ifc: tool.Ifc, collector: tool.Collector, drawing_tool: tool.Drawing, drawing: ifcopenshell.entity_instance
ifc: type[tool.Ifc],
collector: type[tool.Collector],
drawing_tool: type[tool.Drawing],
drawing: ifcopenshell.entity_instance,
) -> None:
if not drawing_tool.has_annotation(drawing):
return
@@ -496,12 +511,15 @@ def sync_references(
drawing_tool.sync_object_representation(reference_obj)
def select_assigned_product(drawing: tool.Drawing, context: bpy.types.Context) -> None:
def select_assigned_product(drawing: type[tool.Drawing], context: bpy.types.Context) -> None:
drawing.select_assigned_product(context)
def activate_drawing_view(
ifc: tool.Ifc, blender: tool.Blender, drawing_tool: tool.Drawing, drawing: ifcopenshell.entity_instance
ifc: type[tool.Ifc],
blender: type[tool.Blender],
drawing_tool: type[tool.Drawing],
drawing: ifcopenshell.entity_instance,
) -> None:
camera = ifc.get_object(drawing)
if not camera:
+1 -1
View File
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
import bonsai.tool as tool
def resize_to_storey(misc: tool.Misc, ifc: tool.Ifc, obj: bpy.types.Object, total_storeys: int) -> None:
def resize_to_storey(misc: type[tool.Misc], ifc: type[tool.Ifc], obj: bpy.types.Object, total_storeys: int) -> None:
storey = misc.get_object_storey(obj)
if not storey:
return
+40 -36
View File
@@ -27,167 +27,171 @@ if TYPE_CHECKING:
from ifcopenshell.api.owner.add_actor import ACTOR_TYPE
def add_person(ifc: tool.Ifc) -> ifcopenshell.entity_instance:
def add_person(ifc: type[tool.Ifc]) -> ifcopenshell.entity_instance:
return ifc.run("owner.add_person")
def remove_person(ifc: tool.Ifc, person: ifcopenshell.entity_instance) -> None:
def remove_person(ifc: type[tool.Ifc], person: ifcopenshell.entity_instance) -> None:
ifc.run("owner.remove_person", person=person)
def enable_editing_person(owner: tool.Owner, person: ifcopenshell.entity_instance) -> None:
def enable_editing_person(owner: type[tool.Owner], person: ifcopenshell.entity_instance) -> None:
owner.set_person(person)
owner.import_person_attributes()
def disable_editing_person(owner: tool.Owner) -> None:
def disable_editing_person(owner: type[tool.Owner]) -> None:
owner.clear_person()
def edit_person(ifc: tool.Ifc, owner: tool.Owner) -> None:
def edit_person(ifc: type[tool.Ifc], owner: type[tool.Owner]) -> None:
ifc.run("owner.edit_person", person=owner.get_person(), attributes=owner.export_person_attributes())
disable_editing_person(owner)
def add_person_attribute(owner: tool.Owner, name: str) -> None:
def add_person_attribute(owner: type[tool.Owner], name: tool.Owner.PersonAttributeType) -> None:
owner.add_person_attribute(name)
def remove_person_attribute(owner: tool.Owner, name: str, id: int) -> None:
def remove_person_attribute(owner: type[tool.Owner], name: tool.Owner.PersonAttributeType, id: int) -> None:
owner.remove_person_attribute(name, id)
def add_role(ifc: tool.Ifc, parent: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
def add_role(ifc: type[tool.Ifc], parent: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
return ifc.run("owner.add_role", assigned_object=parent)
def remove_role(ifc: tool.Ifc, role: ifcopenshell.entity_instance) -> None:
def remove_role(ifc: type[tool.Ifc], role: ifcopenshell.entity_instance) -> None:
ifc.run("owner.remove_role", role=role)
def enable_editing_role(owner: tool.Owner, role: ifcopenshell.entity_instance) -> None:
def enable_editing_role(owner: type[tool.Owner], role: ifcopenshell.entity_instance) -> None:
owner.set_role(role)
owner.import_role_attributes()
def disable_editing_role(owner: tool.Owner) -> None:
def disable_editing_role(owner: type[tool.Owner]) -> None:
owner.clear_role()
def edit_role(ifc: tool.Ifc, owner: tool.Owner) -> None:
def edit_role(ifc: type[tool.Ifc], owner: type[tool.Owner]) -> None:
ifc.run("owner.edit_role", role=owner.get_role(), attributes=owner.export_role_attributes())
owner.clear_role()
def add_address(
ifc: tool.Ifc, parent: ifcopenshell.entity_instance, ifc_class: ADDRESS_TYPE = "IfcPostalAddress"
ifc: type[tool.Ifc], parent: ifcopenshell.entity_instance, ifc_class: ADDRESS_TYPE = "IfcPostalAddress"
) -> ifcopenshell.entity_instance:
return ifc.run("owner.add_address", assigned_object=parent, ifc_class=ifc_class)
def remove_address(ifc: tool.Ifc, address: ifcopenshell.entity_instance) -> None:
def remove_address(ifc: type[tool.Ifc], address: ifcopenshell.entity_instance) -> None:
ifc.run("owner.remove_address", address=address)
def enable_editing_address(owner: tool.Owner, address: ifcopenshell.entity_instance) -> None:
def enable_editing_address(owner: type[tool.Owner], address: ifcopenshell.entity_instance) -> None:
owner.set_address(address)
owner.import_address_attributes()
def disable_editing_address(owner: tool.Owner) -> None:
def disable_editing_address(owner: type[tool.Owner]) -> None:
owner.clear_address()
def edit_address(ifc: tool.Ifc, owner: tool.Owner) -> None:
def edit_address(ifc: type[tool.Ifc], owner: type[tool.Owner]) -> None:
address = owner.get_address()
ifc.run("owner.edit_address", address=address, attributes=owner.export_address_attributes())
owner.clear_address()
def add_address_attribute(owner: tool.Owner, name: str) -> None:
def add_address_attribute(owner: type[tool.Owner], name: tool.Owner.AddressAttributeType) -> None:
owner.add_address_attribute(name)
def remove_address_attribute(owner: tool.Owner, name: str, id: int) -> None:
def remove_address_attribute(owner: type[tool.Owner], name: tool.Owner.AddressAttributeType, id: int) -> None:
owner.remove_address_attribute(name, id)
def add_organisation(ifc: tool.Ifc) -> ifcopenshell.entity_instance:
def add_organisation(ifc: type[tool.Ifc]) -> ifcopenshell.entity_instance:
return ifc.run("owner.add_organisation")
def remove_organisation(ifc: tool.Ifc, organisation: ifcopenshell.entity_instance) -> None:
def remove_organisation(ifc: type[tool.Ifc], organisation: ifcopenshell.entity_instance) -> None:
ifc.run("owner.remove_organisation", organisation=organisation)
def enable_editing_organisation(owner: tool.Owner, organisation: ifcopenshell.entity_instance) -> None:
def enable_editing_organisation(owner: type[tool.Owner], organisation: ifcopenshell.entity_instance) -> None:
owner.set_organisation(organisation)
owner.import_organisation_attributes()
def disable_editing_organisation(owner: tool.Owner) -> None:
def disable_editing_organisation(owner: type[tool.Owner]) -> None:
owner.clear_organisation()
def edit_organisation(ifc: tool.Ifc, owner: tool.Owner) -> None:
def edit_organisation(ifc: type[tool.Ifc], owner: type[tool.Owner]) -> None:
organisation = owner.get_organisation()
ifc.run("owner.edit_organisation", organisation=organisation, attributes=owner.export_organisation_attributes())
owner.clear_organisation()
def add_person_and_organisation(
ifc: tool.Ifc, person: ifcopenshell.entity_instance, organisation: ifcopenshell.entity_instance
ifc: type[tool.Ifc], person: ifcopenshell.entity_instance, organisation: ifcopenshell.entity_instance
) -> ifcopenshell.entity_instance:
return ifc.run("owner.add_person_and_organisation", person=person, organisation=organisation)
def remove_person_and_organisation(
ifc: tool.Ifc, owner: tool.Owner, person_and_organisation: ifcopenshell.entity_instance
ifc: type[tool.Ifc], owner: type[tool.Owner], person_and_organisation: ifcopenshell.entity_instance
) -> None:
if owner.get_user() == person_and_organisation:
owner.clear_user()
ifc.run("owner.remove_person_and_organisation", person_and_organisation=person_and_organisation)
def set_user(owner: tool.Owner, user: ifcopenshell.entity_instance) -> None:
def set_user(owner: type[tool.Owner], user: ifcopenshell.entity_instance) -> None:
owner.set_user(user)
def get_user(owner: tool.Owner) -> Union[ifcopenshell.entity_instance, None]:
def get_user(owner: type[tool.Owner]) -> Union[ifcopenshell.entity_instance, None]:
return owner.get_user()
def clear_user(owner: tool.Owner) -> None:
def clear_user(owner: type[tool.Owner]) -> None:
owner.clear_user()
def add_actor(
ifc: tool.Ifc, ifc_class: ACTOR_TYPE, actor: ifcopenshell.entity_instance
ifc: type[tool.Ifc], ifc_class: ACTOR_TYPE, actor: ifcopenshell.entity_instance
) -> ifcopenshell.entity_instance:
return ifc.run("owner.add_actor", ifc_class=ifc_class, actor=actor)
def remove_actor(ifc: tool.Ifc, actor: ifcopenshell.entity_instance) -> None:
def remove_actor(ifc: type[tool.Ifc], actor: ifcopenshell.entity_instance) -> None:
ifc.run("owner.remove_actor", actor=actor)
def enable_editing_actor(owner: tool.Owner, actor: ifcopenshell.entity_instance) -> None:
def enable_editing_actor(owner: type[tool.Owner], actor: ifcopenshell.entity_instance) -> None:
owner.set_actor(actor)
owner.import_actor_attributes(actor)
def disable_editing_actor(owner: tool.Owner) -> None:
def disable_editing_actor(owner: type[tool.Owner]) -> None:
owner.clear_actor()
def edit_actor(ifc: tool.Ifc, owner: tool.Owner) -> None:
def edit_actor(ifc: type[tool.Ifc], owner: type[tool.Owner]) -> None:
ifc.run("owner.edit_actor", actor=owner.get_actor(), attributes=owner.export_actor_attributes())
disable_editing_actor(owner)
def assign_actor(ifc: tool.Ifc, actor: ifcopenshell.entity_instance, element: ifcopenshell.entity_instance) -> None:
def assign_actor(
ifc: type[tool.Ifc], actor: ifcopenshell.entity_instance, element: ifcopenshell.entity_instance
) -> None:
ifc.run("owner.assign_actor", relating_actor=actor, related_object=element)
def unassign_actor(ifc: tool.Ifc, actor: ifcopenshell.entity_instance, element: ifcopenshell.entity_instance) -> None:
def unassign_actor(
ifc: type[tool.Ifc], actor: ifcopenshell.entity_instance, element: ifcopenshell.entity_instance
) -> None:
ifc.run("owner.unassign_actor", relating_actor=actor, related_object=element)
+8 -4
View File
@@ -26,7 +26,11 @@ if TYPE_CHECKING:
def copy_class(
ifc: tool.Ifc, collector: tool.Collector, geometry: tool.Geometry, root: tool.Root, obj: bpy.types.Object
ifc: type[tool.Ifc],
collector: type[tool.Collector],
geometry: type[tool.Geometry],
root: type[tool.Root],
obj: bpy.types.Object,
) -> ifcopenshell.entity_instance | None:
element = ifc.get_entity(obj)
if not element:
@@ -55,9 +59,9 @@ def copy_class(
def assign_class(
ifc: tool.Ifc,
collector: tool.Collector,
root: tool.Root,
ifc: type[tool.Ifc],
collector: type[tool.Collector],
root: type[tool.Root],
obj: bpy.types.Object,
ifc_class: str,
context: Optional[ifcopenshell.entity_instance] = None,
+3
View File
@@ -164,6 +164,9 @@ class Aggregate(bonsai.core.tool.Aggregate):
if obj.original not in parts_objs:
if obj == props.editing_aggregate:
continue
# Skips adding the object to not_editing_objects if it already exists in the list
if any(obj == existing_obj.obj.original for existing_obj in props.not_editing_objects):
continue
not_editing_obj = props.not_editing_objects.add()
not_editing_obj.obj = obj.original
not_editing_obj.previous_display_type = obj.original.display_type
+42 -3
View File
@@ -25,6 +25,8 @@ import os
import platform
import subprocess
import contextlib
import tempfile
import traceback
import numpy as np
import numpy.typing as npt
from ifcopenshell import entity_instance
@@ -71,7 +73,7 @@ class Blender(bonsai.core.tool.Blender):
OBJECT_TYPES_THAT_SUPPORT_EDIT_GPENCIL_MODE = ("GPENCIL",)
TYPE_MANAGER_ICON = "LIGHTPROBE_VOLUME"
BLENDER_ENUM_ITEM = Union[tuple[str, str, str], tuple[str, str, str, str], tuple[str, str, str, str, str]]
BLENDER_ENUM_ITEM = Union[tuple[str, str, str], tuple[str, str, str, int], tuple[str, str, str, str, int]]
"""
Options:
@@ -81,13 +83,17 @@ class Blender(bonsai.core.tool.Blender):
- (identifier, name, description, icon, number)
"""
BLENDER_ENUM_ITEMS = list[BLENDER_ENUM_ITEM]
@classmethod
def activate_camera(cls, obj: bpy.types.Object) -> None:
area = cls.get_view3d_area()
is_local_view = area.spaces[0].local_view is not None
assert area
assert isinstance((space := area.spaces[0]), bpy.types.SpaceView3D)
is_local_view = space.local_view is not None
assert bpy.context.screen and bpy.context.scene
if is_local_view:
# Turn off local view before activating drawing, and then turn it on again.
for a in bpy.context.screen.areas:
@@ -100,7 +106,8 @@ class Blender(bonsai.core.tool.Blender):
else:
bpy.context.scene.camera = obj
area.spaces[0].region_3d.view_perspective = "CAMERA"
assert space.region_3d
space.region_3d.view_perspective = "CAMERA"
@classmethod
def get_area_props(cls, context: bpy.types.Context) -> bpy.types.PropertyGroup:
@@ -1753,3 +1760,35 @@ class Blender(bonsai.core.tool.Blender):
def report_operator_errors(cls, operator: bpy.types.Operator, error_reports: list[str]) -> None:
for report in error_reports:
operator.report({"ERROR"}, report)
@classmethod
@contextlib.contextmanager
def bonsai_crash_txt(cls, s: str = "") -> Generator[Path, Any, None]:
"""Create a temporary bonsai.crash.txt file the with current traceback.
Useful in case Blender crash might occur too unexpectedly (e.g. #6686),
and at least we'll have a slightest clue on what happened.
Intended to be used via `with` block.
`atexit` wouldn't work for this as crash breaks everything
and no callbacks are called.
:param s: Optional string to add at the top of the txt file.
"""
# TODO: Indicate that crash occurred after Blender restart?
# Create a temp file with the traceback.
temp_dir = tempfile.gettempdir()
path = Path(temp_dir) / "bonsai.crash.txt"
traceback_ = "\n".join(traceback.format_stack())
output = ""
if s:
output += f"{s}\n\n"
time = datetime.now().isoformat()
output += f"Created at: {time} (local time).\n"
output += f"Traceback (most recent called last):\n{traceback_}"
path.write_text(output)
yield path
# Remove file if crash didn't happened.
path.unlink()
+63 -37
View File
@@ -3,10 +3,16 @@ import bonsai.tool as tool
import bpy
import json
import bsdd
from typing import Any, Union, Optional
from typing import Any, Union, Optional, TYPE_CHECKING
class Bsdd(bonsai.core.tool.Bsdd):
client = bsdd.Client()
@classmethod
def get_bsdd_props(cls):
return bpy.context.scene.BIMBSDDProperties
@classmethod
def clear_class_psets(cls) -> None:
bpy.context.scene.BIMBSDDProperties.classification_psets.clear()
@@ -16,8 +22,8 @@ class Bsdd(bonsai.core.tool.Bsdd):
bpy.context.scene.BIMBSDDProperties.classifications.clear()
@classmethod
def clear_domains(cls) -> None:
bpy.context.scene.BIMBSDDProperties.domains.clear()
def clear_dictionaries(cls) -> None:
bpy.context.scene.BIMBSDDProperties.dictionaries.clear()
@classmethod
def create_class_psets(cls, pset_dict: dict[str, dict[str, Any]]) -> None:
@@ -42,22 +48,11 @@ class Bsdd(bonsai.core.tool.Bsdd):
new2.ifc_class = data["ifc_class"]
new2.metadata = data["dictionary"]
@classmethod
def create_classes(cls, class_dict: list[bsdd.ClassSearchResponseClassContractV1]) -> None:
props = bpy.context.scene.BIMBSDDProperties
for _class in sorted(class_dict, key=lambda c: c["referenceCode"]):
prop = props.classifications.add()
prop.name = _class["name"]
prop.reference_code = _class["referenceCode"]
prop.uri = _class["uri"]
prop.domain_name = _class["dictionaryName"]
prop.domain_namespace_uri = _class["dictionaryUri"]
@classmethod
def create_dictionaries(cls, dictionaries: list[bsdd.DictionaryContractV1]) -> None:
props = bpy.context.scene.BIMBSDDProperties
for dictionary in sorted(dictionaries, key=lambda d: d["name"]):
new = props.domains.add()
new = props.dictionaries.add()
new.name = dictionary["name"]
new.uri = dictionary["uri"]
new.default_language_code = dictionary["defaultLanguageCode"]
@@ -66,24 +61,35 @@ class Bsdd(bonsai.core.tool.Bsdd):
new.version = dictionary["version"]
@classmethod
def get_active_class_data(cls, client: bsdd.Client) -> Union[bsdd.ClassContractV1, dict]:
def get_active_class_data(cls) -> Union[bsdd.ClassContractV1, dict]:
prop = bpy.context.scene.BIMBSDDProperties
bsdd_classification = prop.classifications[prop.active_classification_index]
if not bsdd_classification:
return {}
return client.get_class(bsdd_classification.uri)
return cls.client.get_class(bsdd_classification.uri)
@classmethod
def get_active_dictionary_uri(cls) -> str:
return bpy.context.scene.BIMBSDDProperties.active_uri
@classmethod
def get_dictionaries(cls, client: bsdd.Client, status: Optional[str] = None) -> list[bsdd.DictionaryContractV1]:
response = client.get_dictionary()
def get_dictionary(cls, uri: str) -> bsdd.DictionaryContractV1:
props = bpy.context.scene.BIMBSDDProperties
response = cls.client.get_dictionary(dictionary_uri=uri, include_test_dictionaries=props.load_test_dictionaries)
if dicts := response.get("dictionaries"):
return dicts[0]
@classmethod
def get_dictionaries(cls) -> list[bsdd.DictionaryContractV1]:
props = bpy.context.scene.BIMBSDDProperties
response = cls.client.get_dictionary(include_test_dictionaries=props.load_test_dictionaries)
dicts = response.get("dictionaries") or []
if status is not None:
dicts = list(filter(lambda d: d["status"] == status, dicts))
return dicts
statuses = ["Active"]
if props.load_preview_dictionaries:
statuses.append("Preview")
if props.load_inactive_dictionaries:
statuses.append("Inactive")
return list(filter(lambda d: d["status"] in statuses, dicts))
@classmethod
def get_property_dict(cls, class_data: Union[bsdd.ClassContractV1, dict]) -> Union[dict[str, dict[str, Any]], None]:
@@ -133,31 +139,51 @@ class Bsdd(bonsai.core.tool.Bsdd):
@classmethod
def search_class(
cls,
client: bsdd.Client,
keyword: str,
dictionary_uris: Union[list[str], None],
related_ifc_entities: Union[list[str], None],
offset: int = 0,
) -> list[bsdd.ClassSearchResponseClassContractV1]:
response = client.search_class(
keyword, dictionary_uris=dictionary_uris, related_ifc_entities=related_ifc_entities, offset=offset
limit: int = 100,
should_paginate: bool = True,
):
props = cls.get_bsdd_props()
dictionary_uris = (
[d.uri for d in props.dictionaries if d.is_active]
if props.active_dictionary == "ALL"
else [props.active_dictionary]
)
classes = response.get("classes", [])
# If count is 100, it might be hitting the limit.
if response["count"] == 100:
classes += cls.search_class(client, keyword, dictionary_uris, related_ifc_entities, offset=offset + 100)
return classes
for dictionary_uri in dictionary_uris:
response = cls.client.get_classes(
dictionary_uri=dictionary_uri,
use_nested_classes=False,
search_text=keyword,
related_ifc_entity=related_ifc_entities[0] if related_ifc_entities else None,
offset=offset,
limit=limit,
)
dictionary_name = response.get("name", "")
dictionary_namespace_uri = response.get("uri", "")
for _class in sorted(response.get("classes", []), key=lambda c: c["referenceCode"]):
prop = props.classifications.add()
prop.name = _class["name"]
prop.reference_code = _class["referenceCode"]
prop.uri = _class["uri"]
prop.dictionary_name = dictionary_name
prop.dictionary_namespace_uri = dictionary_namespace_uri
total_results = response.get("count", response.get("classesCount"))
# For now, hard limit at 1000 results because any more and Blender
# starts getting slow and they really should filter better
if offset < 1000 and should_paginate and total_results == limit:
cls.search_class(keyword, related_ifc_entities, offset=offset + limit, should_paginate=False)
return offset + total_results
@classmethod
def set_active_bsdd(cls, name: str, uri: str) -> None:
props = bpy.context.scene.BIMBSDDProperties
props.active_domain = name
props.active_dictionary = name
props.active_uri = uri
@classmethod
def should_filter_ifc_class(cls) -> bool:
return bpy.context.scene.BIMBSDDProperties.should_filter_ifc_class
@classmethod
def should_load_preview_domains(cls) -> bool:
return bpy.context.scene.BIMBSDDProperties.load_preview_domains
+19
View File
@@ -102,6 +102,10 @@ class Cad:
d1 = v1 - v2
d2 = v3 - v2
# Rounding avoids problems when dealing with 180 degrees
d1 = Vector((round(c, 6) for c in d1))
d2 = Vector((round(c, 6) for c in d2))
d1.normalize()
d2.normalize()
@@ -123,12 +127,27 @@ class Cad:
if new_angle is not None:
rot_mat = Matrix.Rotation(new_angle, 3, axis)
rot_vector = (d1 @ rot_mat) if parameter else (rot_mat @ d1)
# 180 degrees special cases
if abs(round(a, 4)) == round(math.pi, 4) and (
rot_vector.x == 0.0
and rot_vector.y == 0.0
or rot_vector.x == 0.0
and rot_vector.z == 0.0
or rot_vector.y == 0.0
and rot_vector.z == 0.0
):
rot_vector *= -1
return rot_vector
else:
sign = -1 if parameter else 1
if degrees:
a = math.degrees(a)
# 180 degrees special cases
if abs(round(a, 2)) == abs(180.00):
return -180.0
return a * sign
else:
return a
+22 -9
View File
@@ -16,22 +16,33 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import bonsai.bim.helper
import bonsai.tool as tool
import bonsai.core.tool
import ifcopenshell
from typing import Any, Union
from typing import Any, Union, TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.prop import Attribute
from bonsai.bim.module.context.prop import BIMContextProperties
class Context(bonsai.core.tool.Context):
@classmethod
def get_context_props(cls) -> BIMContextProperties:
assert bpy.context.scene
return bpy.context.scene.BIMContextProperties # pyright: ignore[reportAttributeAccessIssue]
@classmethod
def set_context(cls, context: ifcopenshell.entity_instance) -> None:
bpy.context.scene.BIMContextProperties.active_context_id = context.id()
props = cls.get_context_props()
props.active_context_id = context.id()
@classmethod
def import_attributes(cls) -> None:
props = bpy.context.scene.BIMContextProperties
props = cls.get_context_props()
props.context_attributes.clear()
context = cls.get_context()
@@ -52,20 +63,22 @@ class Context(bonsai.core.tool.Context):
@classmethod
def clear_context(cls) -> None:
bpy.context.scene.BIMContextProperties.active_context_id = 0
props = cls.get_context_props()
props.active_context_id = 0
@classmethod
def get_context(cls) -> ifcopenshell.entity_instance:
return tool.Ifc.get().by_id(bpy.context.scene.BIMContextProperties.active_context_id)
props = cls.get_context_props()
return tool.Ifc.get().by_id(props.active_context_id)
@classmethod
def export_attributes(cls) -> dict[str, Any]:
def callback(attributes, blender_attribute) -> bool:
props = cls.get_context_props()
def callback(attributes: dict[str, Any], blender_attribute: Attribute) -> bool:
if blender_attribute.name == "Precision":
attributes["Precision"] = float(blender_attribute.get_value())
return True
return False
return bonsai.bim.helper.export_attributes(
bpy.context.scene.BIMContextProperties.context_attributes, callback=callback
)
return bonsai.bim.helper.export_attributes(props.context_attributes, callback=callback)
+108 -51
View File
@@ -51,7 +51,7 @@ 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, TYPE_CHECKING, NamedTuple
from typing import Optional, Union, Iterable, Any, Literal, Sequence, TYPE_CHECKING, NamedTuple, get_args
from pathlib import Path
if TYPE_CHECKING:
@@ -61,6 +61,7 @@ if TYPE_CHECKING:
BIMAnnotationProperties,
BIMTextProperties,
BIMCameraProperties,
BIMAssignedProductProperties,
)
from bonsai.bim.module.drawing.prop import Drawing as DrawingProperties
@@ -68,6 +69,9 @@ if TYPE_CHECKING:
class Drawing(bonsai.core.tool.Drawing):
ANNOTATION_DATA_TYPE = Literal["empty", "curve", "mesh"]
DOCUMENT_TYPE = Literal["SCHEDULE", "REFERENCE"]
LocationHintLiteral = Literal["PERSPECTIVE", "ORTHOGRAPHIC", "NORTH", "SOUTH", "EAST", "WEST"]
LOCATION_HINT_LITERALS = ("PERSPECTIVE", "ORTHOGRAPHIC", "NORTH", "SOUTH", "EAST", "WEST")
LocationHintType = Union[LocationHintLiteral, str]
# ObjectType: annotation_name, description, icon, data_type
# fmt: off
@@ -102,15 +106,17 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def get_document_props(cls) -> DocProperties:
return bpy.context.scene.DocProperties
assert (scene := bpy.context.scene)
return scene.DocProperties # pyright: ignore[reportAttributeAccessIssue]
@classmethod
def get_annotation_props(cls) -> BIMAnnotationProperties:
return bpy.context.scene.BIMAnnotationProperties
assert (scene := bpy.context.scene)
return scene.BIMAnnotationProperties # pyright: ignore[reportAttributeAccessIssue]
@classmethod
def get_text_props(cls, obj: bpy.types.Object) -> BIMTextProperties:
return obj.BIMTextProperties
return obj.BIMTextProperties # pyright: ignore[reportAttributeAccessIssue]
@classmethod
def get_camera_props(cls, camera: Union[bpy.types.Object, bpy.types.Camera]) -> BIMCameraProperties:
@@ -121,7 +127,11 @@ class Drawing(bonsai.core.tool.Drawing):
data = camera
else:
assert isinstance(data := camera.data, bpy.types.Camera)
return data.BIMCameraProperties
return data.BIMCameraProperties # pyright: ignore[reportAttributeAccessIssue]
@classmethod
def get_object_assigned_product_props(cls, obj: bpy.types.Object) -> BIMAssignedProductProperties:
return obj.BIMAssignedProductProperties # pyright: ignore[reportAttributeAccessIssue]
@classmethod
def canonicalise_class_name(cls, name: str) -> str:
@@ -296,11 +306,14 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def create_camera(
cls, name: str, matrix: Matrix, location_hint: Literal["PERSPECTIVE", "ORTHOGRAPHIC"]
cls,
name: str,
matrix: Matrix,
location_hint: Union[LocationHintLiteral, int],
target_view: ifcopenshell.util.representation.TARGET_VIEW,
) -> bpy.types.Object:
camera = bpy.data.objects.new(name, (camera_data := bpy.data.cameras.new(name)))
props = cls.get_camera_props(camera_data)
camera.location = (0, 0, 1.5) # The view shall be 1.5m above the origin
camera_data.show_limits = True
if location_hint == "PERSPECTIVE":
props.camera_type = "PERSP"
@@ -308,7 +321,11 @@ class Drawing(bonsai.core.tool.Drawing):
props.camera_type = "ORTHO"
camera_data.ortho_scale = 50 # The default of 6m is too small
camera_data.clip_start = 0.002 # 2mm is close to zero but allows any GPU-drawn lines to be visible.
camera_data.clip_end = 10 # A slightly more reasonable default
if target_view == "MODEL_VIEW":
assert (space := tool.Blender.get_view3d_space())
camera_data.clip_end = max(space.clip_end, 10)
else:
camera_data.clip_end = 10 # A slightly more reasonable default
if bpy.context.scene.unit_settings.system == "IMPERIAL":
props.diagram_scale = '1/8"=1\'-0"|1/96'
else:
@@ -396,7 +413,8 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def disable_editing_assigned_product(cls, obj: bpy.types.Object) -> None:
obj.BIMAssignedProductProperties.is_editing_product = False
props = cls.get_object_assigned_product_props(obj)
props.is_editing_product = False
@classmethod
def enable_editing(cls, obj: bpy.types.Object) -> None:
@@ -437,7 +455,8 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def enable_editing_assigned_product(cls, obj: bpy.types.Object) -> None:
obj.BIMAssignedProductProperties.is_editing_product = True
props = cls.get_object_assigned_product_props(obj)
props.is_editing_product = True
@classmethod
def ensure_unique_drawing_name(cls, name: str) -> str:
@@ -588,48 +607,82 @@ class Drawing(bonsai.core.tool.Drawing):
def get_name(cls, element: ifcopenshell.entity_instance) -> Union[str, None]:
return element.Name
@classmethod
def update_camera_matrix(
cls,
matrix: Matrix,
*,
camera_dir: Vector,
up: Vector,
) -> None:
# Camera dir is actually Z-.
camera_dir = -camera_dir
right = up.cross(camera_dir)
assert isinstance(right, Vector)
matrix.col[0][:3] = right
matrix.col[1][:3] = up
matrix.col[2][:3] = camera_dir
@classmethod
def generate_drawing_matrix(
cls, target_view: ifcopenshell.util.representation.TARGET_VIEW, location_hint: str
cls,
target_view: ifcopenshell.util.representation.TARGET_VIEW,
location_hint: Union[LocationHintLiteral, int],
) -> Matrix:
assert bpy.context.scene
m = Matrix()
x, y, z = (0, 0, 0) if location_hint == 0 else bpy.context.scene.cursor.matrix.translation
if target_view == "PLAN_VIEW":
X, Y, Z = m.to_3x3()
if isinstance(location_hint, int):
if target_view == "REFLECTED_PLAN_VIEW":
# Flip Z axis.
m.col[2] *= -1
if location_hint:
z = tool.Ifc.get_object(tool.Ifc.get().by_id(location_hint)).matrix_world.translation.z
return mathutils.Matrix(((1, 0, 0, x), (0, 1, 0, y), (0, 0, 1, z + 1.6), (0, 0, 0, 1)))
elif target_view == "REFLECTED_PLAN_VIEW":
if location_hint:
z = tool.Ifc.get_object(tool.Ifc.get().by_id(location_hint)).matrix_world.translation.z
m = mathutils.Matrix()
m[2][2] = -1
m.translation = (x, y, z + 1.6)
return m
return mathutils.Matrix(((1, 0, 0, 0), (0, 1, 0, 0), (0, 0, -1, 0), (0, 0, 0, 1)))
storey = tool.Ifc.get_object(tool.Ifc.get().by_id(location_hint))
assert isinstance(storey, bpy.types.Object)
z = storey.matrix_world.translation.z
if target_view == "PLAN_VIEW":
# Keep default camera direction - Z-.
m.translation = (x, y, z + 1.6)
return m
elif target_view == "REFLECTED_PLAN_VIEW":
m.translation = (x, y, z + 1.6)
else:
assert False, target_view
return m
elif target_view == "ELEVATION_VIEW":
m.translation = (x, y, z)
if location_hint == "NORTH":
return mathutils.Matrix(((-1, 0, 0, x), (0, 0, 1, y), (0, 1, 0, z), (0, 0, 0, 1)))
cls.update_camera_matrix(m, camera_dir=-Y, up=Z)
elif location_hint == "SOUTH":
return mathutils.Matrix(((1, 0, 0, x), (0, 0, -1, y), (0, 1, 0, z), (0, 0, 0, 1)))
cls.update_camera_matrix(m, camera_dir=Y, up=Z)
elif location_hint == "EAST":
return mathutils.Matrix(((0, 0, 1, x), (1, 0, 0, y), (0, 1, 0, z), (0, 0, 0, 1)))
cls.update_camera_matrix(m, camera_dir=-X, up=Z)
elif location_hint == "WEST":
return mathutils.Matrix(((0, 0, -1, x), (-1, 0, 0, y), (0, 1, 0, z), (0, 0, 0, 1)))
cls.update_camera_matrix(m, camera_dir=X, up=Z)
return m
elif target_view == "SECTION_VIEW":
m.translation = (x, y, z)
X, Y, Z = m.to_3x3()
if location_hint == "NORTH":
return mathutils.Matrix(((1, 0, 0, x), (0, 0, -1, y), (0, 1, 0, z), (0, 0, 0, 1)))
cls.update_camera_matrix(m, camera_dir=Y, up=Z)
elif location_hint == "SOUTH":
return mathutils.Matrix(((-1, 0, 0, x), (0, 0, 1, y), (0, 1, 0, z), (0, 0, 0, 1)))
cls.update_camera_matrix(m, camera_dir=-Y, up=Z)
elif location_hint == "EAST":
return mathutils.Matrix(((0, 0, -1, x), (-1, 0, 0, y), (0, 1, 0, z), (0, 0, 0, 1)))
cls.update_camera_matrix(m, camera_dir=X, up=Z)
elif location_hint == "WEST":
return mathutils.Matrix(((0, 0, 1, x), (1, 0, 0, y), (0, 1, 0, z), (0, 0, 0, 1)))
cls.update_camera_matrix(m, camera_dir=-X, up=Z)
return m
elif target_view == "MODEL_VIEW":
return mathutils.Matrix(((1, 0, 0, x), (0, 1, 0, y), (0, 0, 1, z), (0, 0, 0, 1)))
return mathutils.Matrix()
assert (space := tool.Blender.get_view3d_space())
assert (r3d := space.region_3d)
return r3d.view_matrix.inverted()
return m
@classmethod
def generate_sheet_identification(cls) -> str:
number = len([d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "SHEET"])
number += 1
return "A" + str(number).zfill(2)
@classmethod
@@ -742,7 +795,7 @@ class Drawing(bonsai.core.tool.Drawing):
ifc_importer.load_existing_meshes()
ifc_importer.material_creator.load_existing_materials()
ifc_importer.create_generic_elements(elements)
ifc_importer.setup_arrays(elements)
ifc_importer.setup_arrays(annotations_to_import=elements)
for obj in ifc_importer.added_data.values():
tool.Collector.assign(obj)
@@ -995,18 +1048,21 @@ class Drawing(bonsai.core.tool.Drawing):
from bonsai.bim.module.drawing.data import DecoratorData
text_data = DecoratorData.get_ifc_text_data(obj)
text_data = DecoratorData.get_text_data(obj)
props.font_size = str(text_data["FontSize"])
props.newline_at = text_data["Newline_At"]
@classmethod
def import_assigned_product(cls, obj: bpy.types.Object) -> None:
element = tool.Ifc.get_entity(obj)
assert element
product = cls.get_assigned_product(element)
props = cls.get_object_assigned_product_props(obj)
if product:
obj.BIMAssignedProductProperties.relating_product = tool.Ifc.get_object(product)
assert isinstance(product_obj := tool.Ifc.get_object(product), bpy.types.Object)
props.relating_product = product_obj
else:
obj.BIMAssignedProductProperties.relating_product = None
props.relating_product = None
@classmethod
def open_with_user_command(cls, user_command: str, path: str) -> None:
@@ -1174,13 +1230,17 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def generate_drawing_name(
cls, target_view: ifcopenshell.util.representation.TARGET_VIEW, location_hint: str
cls,
target_view: ifcopenshell.util.representation.TARGET_VIEW,
location_hint: Union[LocationHintLiteral, int],
) -> str:
if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW") and location_hint:
location = tool.Ifc.get().by_id(location_hint)
if target_view == "REFLECTED_PLAN_VIEW":
target_view = "RCP_VIEW"
return (location.Name or "UNNAMED").upper() + " " + target_view.split("_")[0]
if isinstance(location_hint, int):
if location_hint:
location = tool.Ifc.get().by_id(location_hint)
target_view_ = target_view
if target_view == "REFLECTED_PLAN_VIEW":
target_view_ = "RCP_VIEW"
return (location.Name or "UNNAMED").upper() + " " + target_view_.split("_")[0]
elif target_view in ("SECTION_VIEW", "ELEVATION_VIEW") and location_hint:
return location_hint + " " + target_view.split("_")[0]
elif target_view == "MODEL_VIEW" and location_hint:
@@ -1225,6 +1285,9 @@ class Drawing(bonsai.core.tool.Drawing):
def sanitise_filename(cls, name: str) -> str:
return "".join(x for x in name if (x.isalnum() or x in "._- "))
ResourceType = Literal["Stylesheet", "Markers", "Symbols", "Patterns", "ShadingStyles"]
RESOURCE_TYPES = ("Stylesheet", "Markers", "Symbols", "Patterns", "ShadingStyles")
@classmethod
def get_default_drawing_resource_path(cls, resource: str) -> Union[str, None]:
project = tool.Ifc.get().by_type("IfcProject")[0]
@@ -1799,15 +1862,9 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def get_drawing_metadata(cls, drawing: ifcopenshell.entity_instance) -> list[str]:
# fmt: off
return [
v.strip()
for v in (
ifcopenshell.util.element.get_psets(drawing)["EPset_Drawing"].get("Metadata", "") or ""
).split(",")
if v
]
# fmt: on
pset_data = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing")
metadata_str = pset_data.get("Metadata", "") or ""
return [v_ for v in metadata_str.split(",") if (v_ := v.strip())]
@classmethod
def get_annotation_z_index(cls, drawing: ifcopenshell.entity_instance) -> float:
+1 -1
View File
@@ -1030,7 +1030,7 @@ class Geometry(bonsai.core.tool.Geometry):
return item.is_a("IfcSweptAreaSolid") or item.is_a("IfcHalfSpaceSolid")
@classmethod
def is_profile_based(cls, data: bpy.types.Mesh) -> bool:
def is_profile_based(cls, data: TYPES_WITH_MESH_PROPERTIES) -> bool:
props = tool.Geometry.get_mesh_props(data)
return props.subshape_type == "PROFILE"
+4 -1
View File
@@ -329,7 +329,10 @@ class Material(bonsai.core.tool.Material):
or some other may be applied now since it's no longer overridden,
therefore we need to make sure blender materials reflect correct styles.
Designed to be called after material.unassign_material API call."""
Designed to be called after material.unassign_material API call.
Will reload representations, invalidating existing object data.
"""
elements = elements.copy() # Avoid argument mutation.
for element in elements[:]:
if element.is_a("IfcElementType"):
+4 -1
View File
@@ -102,6 +102,9 @@ class Misc(bonsai.core.tool.Misc):
def boolean_objects_with_cutter(
cls, objs: list[bpy.types.Object], cutter: bpy.types.Object
) -> list[bpy.types.Object]:
"""
:return: List of newly added objects.
"""
cutter_mesh = cutter.data
assert isinstance(cutter_mesh, bpy.types.Mesh)
@@ -111,7 +114,7 @@ class Misc(bonsai.core.tool.Misc):
for f in bm_flipped.faces:
f.normal_flip()
new_objs = []
new_objs: list[bpy.types.Object] = []
for obj in objs:
mesh = obj.data
if not isinstance(mesh, bpy.types.Mesh) or obj == cutter:
+15 -1
View File
@@ -587,13 +587,15 @@ class Model(bonsai.core.tool.Model):
def load_openings(cls, openings: list[ifcopenshell.entity_instance]) -> Iterable[bpy.types.Object]:
if not openings:
return []
elements = set(openings)
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.material_creator.load_existing_materials()
ifc_importer.create_generic_elements(set(openings))
ifc_importer.create_generic_elements(elements)
ifc_importer.setup_arrays(openings_to_import=elements)
for opening_obj in ifc_importer.added_data.values():
tool.Collector.assign(opening_obj, should_clean_users_collection=False)
return ifc_importer.added_data.values()
@@ -1060,6 +1062,7 @@ class Model(bonsai.core.tool.Model):
if not refresh and element.id() in AuthoringData.type_thumbnails:
return # Already processed
assert isinstance(obj, bpy.types.Object)
obj.asset_generate_preview()
while not obj.preview:
pass
@@ -1144,6 +1147,17 @@ class Model(bonsai.core.tool.Model):
AuthoringData.type_thumbnails[element.id()] = obj.preview.icon_id
@classmethod
def mark_thumbnail_for_update(cls, element: ifcopenshell.entity_instance) -> None:
"""Mark the thumbnail for the provided element as outdated.
Allows postponing the thumbnail update until it is actually needed by the user.
"""
element_id = element.id()
if element_id not in AuthoringData.type_thumbnails:
return
del AuthoringData.type_thumbnails[element_id]
@classmethod
def get_selected_ifc_objects(cls) -> list[bpy.types.Object]:
return [obj for obj in tool.Blender.get_selected_objects() if tool.Ifc.get_entity(obj)]
+91 -46
View File
@@ -16,23 +16,35 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import bonsai.core.tool
import bonsai.tool as tool
import bonsai.bim.helper
import ifcopenshell
from typing import Union, Any
from typing import Union, Any, TYPE_CHECKING, Literal
from typing_extensions import assert_never
if TYPE_CHECKING:
from bonsai.bim.module.owner.prop import BIMOwnerProperties
class Owner(bonsai.core.tool.Owner):
@classmethod
def get_owner_props(cls) -> BIMOwnerProperties:
return bpy.context.scene.BIMOwnerProperties
@classmethod
def set_user(cls, user: ifcopenshell.entity_instance) -> None:
bpy.context.scene.BIMOwnerProperties.active_user_id = user.id()
props = cls.get_owner_props()
props.active_user_id = user.id()
@classmethod
def get_user(cls) -> Union[ifcopenshell.entity_instance, None]:
if bpy.context.scene.BIMOwnerProperties.active_user_id:
return tool.Ifc.get().by_id(bpy.context.scene.BIMOwnerProperties.active_user_id)
props = cls.get_owner_props()
active_user_id = props.active_user_id
if active_user_id:
return tool.Ifc.get().by_id(active_user_id)
elif tool.Ifc.get_schema() == "IFC2X3":
users = tool.Ifc.get().by_type("IfcPersonAndOrganization")
if users:
@@ -40,15 +52,17 @@ class Owner(bonsai.core.tool.Owner):
@classmethod
def clear_user(cls) -> None:
bpy.context.scene.BIMOwnerProperties.active_user_id = 0
props = cls.get_owner_props()
props.active_user_id = 0
@classmethod
def set_address(cls, address: ifcopenshell.entity_instance) -> None:
bpy.context.scene.BIMOwnerProperties.active_address_id = address.id()
props = cls.get_owner_props()
props.active_address_id = address.id()
@classmethod
def import_address_attributes(cls) -> None:
props = bpy.context.scene.BIMOwnerProperties
props = props = cls.get_owner_props()
props.address_attributes.clear()
props.address_lines.clear()
props.telephone_numbers.clear()
@@ -58,7 +72,7 @@ class Owner(bonsai.core.tool.Owner):
address = cls.get_address()
def callback(name, prop, data):
def callback(name: str, prop, data: dict[str, Any]) -> None:
if name == "AddressLines":
for line in data[name] or []:
props.address_lines.add().name = line
@@ -79,15 +93,17 @@ class Owner(bonsai.core.tool.Owner):
@classmethod
def clear_address(cls) -> None:
bpy.context.scene.BIMOwnerProperties.active_address_id = 0
props = cls.get_owner_props()
props.active_address_id = 0
@classmethod
def get_address(cls) -> ifcopenshell.entity_instance:
return tool.Ifc().get().by_id(bpy.context.scene.BIMOwnerProperties.active_address_id)
props = cls.get_owner_props()
return tool.Ifc().get().by_id(props.active_address_id)
@classmethod
def export_address_attributes(cls) -> dict[str, Any]:
props = bpy.context.scene.BIMOwnerProperties
props = cls.get_owner_props()
attributes = bonsai.bim.helper.export_attributes(props.address_attributes)
if cls.get_address().is_a("IfcPostalAddress"):
attributes["AddressLines"] = [l.name for l in props.address_lines] or None
@@ -98,9 +114,13 @@ class Owner(bonsai.core.tool.Owner):
attributes["MessagingIDs"] = [l.name for l in props.messaging_ids] or None
return attributes
AddressAttributeType = Literal[
"AddressLines", "TelephoneNumbers", "FacsimileNumbers", "ElectronicMailAddresses", "MessagingIDs"
]
@classmethod
def add_address_attribute(cls, name: str) -> None:
props = bpy.context.scene.BIMOwnerProperties
def add_address_attribute(cls, name: AddressAttributeType) -> None:
props = cls.get_owner_props()
if name == "AddressLines":
props.address_lines.add()
elif name == "TelephoneNumbers":
@@ -111,10 +131,12 @@ class Owner(bonsai.core.tool.Owner):
props.electronic_mail_addresses.add()
elif name == "MessagingIDs":
props.messaging_ids.add()
else:
assert_never(name)
@classmethod
def remove_address_attribute(cls, name: str, id: int) -> None:
props = bpy.context.scene.BIMOwnerProperties
def remove_address_attribute(cls, name: AddressAttributeType, id: int) -> None:
props = cls.get_owner_props()
if name == "AddressLines":
props.address_lines.remove(id)
elif name == "TelephoneNumbers":
@@ -125,47 +147,53 @@ class Owner(bonsai.core.tool.Owner):
props.electronic_mail_addresses.remove(id)
elif name == "MessagingIDs":
props.messaging_ids.remove(id)
else:
assert_never(name)
@classmethod
def set_organisation(cls, organisation: ifcopenshell.entity_instance) -> None:
bpy.context.scene.BIMOwnerProperties.active_organisation_id = organisation.id()
props = cls.get_owner_props()
props.active_organisation_id = organisation.id()
@classmethod
def import_organisation_attributes(cls) -> None:
organisation = tool.Ifc.get().by_id(bpy.context.scene.BIMOwnerProperties.active_organisation_id)
props = bpy.context.scene.BIMOwnerProperties
props = cls.get_owner_props()
organisation = tool.Ifc.get().by_id(props.active_organisation_id)
props.organisation_attributes.clear()
bonsai.bim.helper.import_attributes("IfcOrganization", props.organisation_attributes, organisation.get_info())
@classmethod
def clear_organisation(cls) -> None:
bpy.context.scene.BIMOwnerProperties.active_organisation_id = 0
props = cls.get_owner_props()
props.active_organisation_id = 0
@classmethod
def export_organisation_attributes(cls) -> dict[str, Any]:
props = bpy.context.scene.BIMOwnerProperties
props = cls.get_owner_props()
attributes = bonsai.bim.helper.export_attributes(props.organisation_attributes)
return attributes
@classmethod
def get_organisation(cls) -> ifcopenshell.entity_instance:
return tool.Ifc().get().by_id(bpy.context.scene.BIMOwnerProperties.active_organisation_id)
props = cls.get_owner_props()
return tool.Ifc().get().by_id(props.active_organisation_id)
@classmethod
def set_person(cls, person: ifcopenshell.entity_instance) -> None:
bpy.context.scene.BIMOwnerProperties.active_person_id = person.id()
props = cls.get_owner_props()
props.active_person_id = person.id()
@classmethod
def import_person_attributes(cls) -> None:
person = tool.Ifc.get().by_id(bpy.context.scene.BIMOwnerProperties.active_person_id)
props = bpy.context.scene.BIMOwnerProperties
props = cls.get_owner_props()
person = tool.Ifc.get().by_id(props.active_person_id)
props.person_attributes.clear()
props.middle_names.clear()
props.prefix_titles.clear()
props.suffix_titles.clear()
def callback(name, prop, data):
def callback(name: str, prop, data: dict[str, Any]) -> None:
if name == "MiddleNames":
for name in data["MiddleNames"] or []:
props.middle_names.add().name = name or ""
@@ -180,11 +208,12 @@ class Owner(bonsai.core.tool.Owner):
@classmethod
def clear_person(cls) -> None:
bpy.context.scene.BIMOwnerProperties.active_person_id = 0
props = cls.get_owner_props()
props.active_person_id = 0
@classmethod
def export_person_attributes(cls) -> dict[str, Any]:
props = bpy.context.scene.BIMOwnerProperties
props = cls.get_owner_props()
attributes = bonsai.bim.helper.export_attributes(props.person_attributes)
attributes["MiddleNames"] = [v.name for v in props.middle_names] if props.middle_names else None
attributes["PrefixTitles"] = [v.name for v in props.prefix_titles] if props.prefix_titles else None
@@ -193,69 +222,85 @@ class Owner(bonsai.core.tool.Owner):
@classmethod
def get_person(cls) -> ifcopenshell.entity_instance:
return tool.Ifc().get().by_id(bpy.context.scene.BIMOwnerProperties.active_person_id)
props = cls.get_owner_props()
return tool.Ifc().get().by_id(props.active_person_id)
PersonAttributeType = Literal["MiddleNames", "PrefixTitles", "SuffixTitles"]
@classmethod
def add_person_attribute(cls, name: str) -> None:
def add_person_attribute(cls, name: PersonAttributeType) -> None:
props = cls.get_owner_props()
if name == "MiddleNames":
bpy.context.scene.BIMOwnerProperties.middle_names.add()
props.middle_names.add()
elif name == "PrefixTitles":
bpy.context.scene.BIMOwnerProperties.prefix_titles.add()
props.prefix_titles.add()
elif name == "SuffixTitles":
bpy.context.scene.BIMOwnerProperties.suffix_titles.add()
props.suffix_titles.add()
else:
assert_never(name)
@classmethod
def remove_person_attribute(cls, name: str, id: int) -> None:
def remove_person_attribute(cls, name: PersonAttributeType, id: int) -> None:
props = cls.get_owner_props()
if name == "MiddleNames":
bpy.context.scene.BIMOwnerProperties.middle_names.remove(id)
props.middle_names.remove(id)
elif name == "PrefixTitles":
bpy.context.scene.BIMOwnerProperties.prefix_titles.remove(id)
props.prefix_titles.remove(id)
elif name == "SuffixTitles":
bpy.context.scene.BIMOwnerProperties.suffix_titles.remove(id)
props.suffix_titles.remove(id)
else:
assert_never(name)
@classmethod
def set_role(cls, role: ifcopenshell.entity_instance) -> None:
bpy.context.scene.BIMOwnerProperties.active_role_id = role.id()
props = cls.get_owner_props()
props.active_role_id = role.id()
@classmethod
def import_role_attributes(cls) -> None:
role = cls.get_role()
props = bpy.context.scene.BIMOwnerProperties
props = cls.get_owner_props()
props.role_attributes.clear()
bonsai.bim.helper.import_attributes("IfcActorRole", props.role_attributes, role.get_info())
@classmethod
def clear_role(cls) -> None:
bpy.context.scene.BIMOwnerProperties.active_role_id = 0
props = cls.get_owner_props()
props.active_role_id = 0
@classmethod
def get_role(cls) -> ifcopenshell.entity_instance:
return tool.Ifc().get().by_id(bpy.context.scene.BIMOwnerProperties.active_role_id)
props = cls.get_owner_props()
return tool.Ifc().get().by_id(props.active_role_id)
@classmethod
def export_role_attributes(cls) -> dict[str, Any]:
return bonsai.bim.helper.export_attributes(bpy.context.scene.BIMOwnerProperties.role_attributes)
props = cls.get_owner_props()
return bonsai.bim.helper.export_attributes(props.role_attributes)
@classmethod
def set_actor(cls, actor: ifcopenshell.entity_instance) -> None:
bpy.context.scene.BIMOwnerProperties.active_actor_id = actor.id()
props = cls.get_owner_props()
props.active_actor_id = actor.id()
@classmethod
def import_actor_attributes(cls, actor: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMOwnerProperties
props = cls.get_owner_props()
props.actor_attributes.clear()
bonsai.bim.helper.import_attributes2(actor, props.actor_attributes)
@classmethod
def clear_actor(cls) -> None:
bpy.context.scene.BIMOwnerProperties.active_actor_id = 0
props = cls.get_owner_props()
props.active_actor_id = 0
@classmethod
def export_actor_attributes(cls) -> dict[str, Any]:
props = bpy.context.scene.BIMOwnerProperties
props = cls.get_owner_props()
attributes = bonsai.bim.helper.export_attributes(props.actor_attributes)
return attributes
@classmethod
def get_actor(cls) -> ifcopenshell.entity_instance:
return tool.Ifc().get().by_id(bpy.context.scene.BIMOwnerProperties.active_actor_id)
props = cls.get_owner_props()
return tool.Ifc().get().by_id(props.active_actor_id)
+1 -1
View File
@@ -339,7 +339,7 @@ class Sequence(bonsai.core.tool.Sequence):
@classmethod
def get_task_attribute_value(cls, attribute_name: str) -> Any:
props = cls.get_work_schedule_props()
return props.task_attributes.get(attribute_name).get_value()
return props.task_attributes[attribute_name].get_value()
@classmethod
def get_active_task(cls) -> ifcopenshell.entity_instance:
+1 -1
View File
@@ -291,7 +291,7 @@ class Spatial(bonsai.core.tool.Spatial):
ifc_class = element.is_a()
ifc_definition_id = element_type.id() if element_type else 0
type_name = (
element_type.is_a() + "/" + element_type.Name or "Unnamed"
element_type.is_a() + "/" + (element_type.Name or "Unnamed")
if element_type
else f"Untyped {element.is_a()}"
)
+1
View File
@@ -4,6 +4,7 @@ markers =
attribute
boolean
brick
bsdd
classification
context
cost
+105
View File
@@ -0,0 +1,105 @@
@bsdd
Feature: bSDD
Scenario: Load bSDD dictionaries
Given an empty Blender session
And I look at the "buildingSMART Data Dictionary" panel
When I click "Load bSDD Dictionaries"
Then I see "Selected dictionary"
And I see "LCA" in the "1st" list
Scenario: Add classification from bSDD - add all dictionaries
Given an empty IFC project
And I look at the "buildingSMART Data Dictionary" panel
And I click "Load bSDD Dictionaries"
When I click "is_active" in the row where I see "LCA" in the "1st" list
And I click "is_active" in the row where I see "BonsaiTestDict" in the "1st" list
And I look at the "Classifications" panel
And I set the "classification_source" property to "buildingSMART Data Dictionary"
And I set the "active_dictionary" property to "All Dictionaries"
And I click "Add Classification From bSDD"
Then I see "LCA"
And I see "BonsaiTestDict"
Scenario: Add classification from bSDD - add single dictionary
Given an empty IFC project
And I look at the "buildingSMART Data Dictionary" panel
And I click "Load bSDD Dictionaries"
When I click "is_active" in the row where I see "BonsaiTestDict" in the "1st" list
And I look at the "Classifications" panel
And I set the "classification_source" property to "buildingSMART Data Dictionary"
And I set the "active_dictionary" property to "BonsaiTestDict"
And I click "Add Classification From bSDD"
Then I see "BonsaiTestDict"
And I don't see "LCA"
Scenario: Search bSDD classifications - search all dictionaries
Given an empty IFC project
And I look at the "buildingSMART Data Dictionary" panel
And I click "Load bSDD Dictionaries"
When I click "is_active" in the row where I see "LCA" in the "1st" list
And I click "is_active" in the row where I see "BonsaiTestDict" in the "1st" list
And I select the object "IfcSite/My Site"
And I look at the "Classification References" panel
And I set the "classification_source" property to "buildingSMART Data Dictionary"
And I set the "active_dictionary" property to "All Dictionaries"
And I click "VIEWZOOM"
Then I see "Acidification" in the "1st" list
And I see "BonsaiReferenceA" in the "1st" list
Scenario: Search bSDD classifications - search single dictionary
Given an empty IFC project
And I look at the "buildingSMART Data Dictionary" panel
And I click "Load bSDD Dictionaries"
And I click "is_active" in the row where I see "BonsaiTestDict" in the "1st" list
And I select the object "IfcSite/My Site"
And I look at the "Classification References" panel
When I set the "classification_source" property to "buildingSMART Data Dictionary"
And I set the "active_dictionary" property to "BonsaiTestDict"
And I click "VIEWZOOM"
Then I see "BonsaiReferenceA" in the "1st" list
And I don't see "Acidification" in the "1st" list
Scenario: Add classification reference from bSDD - add classification only
Given an empty IFC project
And I look at the "buildingSMART Data Dictionary" panel
And I click "Load bSDD Dictionaries"
When I click "is_active" in the row where I see "LCA" in the "1st" list
And I click "is_active" in the row where I see "BonsaiTestDict" in the "1st" list
And I select the object "IfcSite/My Site"
And I look at the "Classification References" panel
And I set the "classification_source" property to "buildingSMART Data Dictionary"
And I set the "active_dictionary" property to "All Dictionaries"
And I click "VIEWZOOM"
Then I see "Acidification" in the "1st" list
When I select the row where I see "Acidification" in the "1st" list
And I click "Add Classification Reference"
Then I see "Acidification"
And I don't see "BonsaiReferenceA"
Scenario: Get bSDD classification properties - get associated properties then add classification
Given an empty IFC project
And I look at the "buildingSMART Data Dictionary" panel
And I click "Load bSDD Dictionaries"
When I click "is_active" in the row where I see "LCA" in the "1st" list
And I select the object "IfcSite/My Site"
And I look at the "Classification References" panel
And I set the "classification_source" property to "buildingSMART Data Dictionary"
And I set the "active_dictionary" property to "All Dictionaries"
And I click "VIEWZOOM"
When I select the row where I see "Acidification" in the "1st" list
And I click "COPY_ID"
Then I see "SizeSet"
And I see "Height"
And I see "Volume"
And I see "No References"
When I set the "Height" property to "42"
And I click "Add Classification Reference"
Then I see "Acidification"
And I don't see "No References"
When I look at the "Property Sets" panel
Then I see "SizeSet"
And I see "Height"
And I see "42"
And I see "Volume"
And I see "0"
+15 -16
View File
@@ -33,12 +33,12 @@ Scenario: Duplicate drawing - without duplicating annotations
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 "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
And I press "bim.add_annotation"
And the variable "drawing" is "IfcStore.get_file().by_type('IfcAnnotation')[0].id()"
When I press "bim.duplicate_drawing(drawing={drawing})"
And I select the "PLAN_VIEW-X" item in the "BIM_UL_drawinglist" list
And I click "OUTLINER_OB_CAMERA"
And I click "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW-X" in the "1st" list
Then the object "IfcAnnotation/TEXT" is not selected
And the object "IfcAnnotation/TEXT.001" does not exist
@@ -56,12 +56,12 @@ Scenario: Duplicate drawing - with duplicating annotations
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 "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
And I press "bim.add_annotation"
And the variable "drawing" is "IfcStore.get_file().by_type('IfcAnnotation')[0].id()"
When I press "bim.duplicate_drawing(drawing={drawing}, should_duplicate_annotations=True)"
And I select the "PLAN_VIEW-X" item in the "BIM_UL_drawinglist" list
And I click "OUTLINER_OB_CAMERA"
And I click "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW-X" in the "1st" list
Then the object "IfcAnnotation/TEXT" is not selected
And the object "IfcAnnotation/TEXT.001" exists
@@ -79,7 +79,7 @@ Scenario: Create drawing
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 "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
And I click "OUTPUT"
Then the file "{ifc_dir}/drawings/PLAN_VIEW.svg" should contain "cut"
And the file "{ifc_dir}/drawings/PLAN_VIEW.svg" should contain "IfcWall"
@@ -99,7 +99,7 @@ Scenario: Create drawing after deleting a duplicated object
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 "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
And I click "OUTPUT"
And the object "IfcWall/Cube" is selected
And I delete the selected objects
@@ -118,7 +118,7 @@ Scenario: Activate drawing preserves visibility for non-ifc objects
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 "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
Then the object "Cube" is visible
And the object "Cube.001" is not visible
@@ -132,7 +132,7 @@ Scenario: Activate drawing preserves selection
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"
When I click "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
Then the object "Cube" is selected
Scenario: Remove drawing
@@ -149,7 +149,7 @@ Scenario: Remove drawing
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"
When I click "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
Then the collection "IfcAnnotation/PLAN_VIEW" exists
When I press "bim.remove_drawing(drawing={drawing})"
Then the collection "IfcAnnotation/PLAN_VIEW" does not exist
@@ -183,7 +183,7 @@ Scenario: Remove drawing - deleting active drawing
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 "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
When the object "IfcAnnotation/PLAN_VIEW" is selected
And I delete the selected objects
Then the collection "IfcAnnotation/PLAN_VIEW" does not exist
@@ -198,7 +198,7 @@ Scenario: Add annotation - text
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 "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
When I press "bim.add_annotation"
Then the object "IfcAnnotation/TEXT" is selected
@@ -215,7 +215,7 @@ Scenario: Add annotation - auto create context if it doesn't exist
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 "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
When I press "bim.add_annotation"
Then the object "IfcAnnotation/TEXT" is selected
@@ -234,7 +234,7 @@ Scenario: Create drawing - using shapely fill mode
And I click "ADD"
And I press "bim.expand_target_view(target_view='PLAN_VIEW')"
When I select the "MY STOREY PLAN" item in the "BIM_UL_drawinglist" list
And I click "OUTLINER_OB_CAMERA"
And I click "VIEW_CAMERA_UNSELECTED" in the row where I see "MY STOREY PLAN" in the "1st" list
And I look at the "Active Drawing" panel
And I set the "Fill Mode" property to "Shapely"
And I look at the "Drawings" panel
@@ -260,7 +260,6 @@ Scenario: Create sheet
When I click "OUTPUT"
Then the file "{ifc_dir}/sheets/A00 - UNTITLED.svg" should not contain "titleblocks/A1.svg"
And the file "{ifc_dir}/sheets/A00 - UNTITLED.svg" should contain "GRID NORTH"
And the file "{ifc_dir}/sheets/A00 - UNTITLED.svg" should not contain "IfcWall"
Scenario: Add drawing to sheet
Given an empty IFC project
@@ -276,7 +275,7 @@ Scenario: Add drawing to sheet
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 "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
And I click "OUTPUT"
And I look at the "Sheets" panel
And I click "IMPORT"
@@ -301,7 +300,7 @@ Scenario: Create sheet - with a drawing added to it
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 "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
And I click "OUTPUT"
And I look at the "Sheets" panel
And I click "IMPORT"
+4 -1
View File
@@ -24,9 +24,12 @@ Scenario: Edit filter query
Scenario: Colour by property - default class query
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcColumnType"
And I add the construction type
When I look at the "Colour By Property" panel
And I click "Colour by Property"
Then the "BIM_UL_colourscheme" list has 4 items
Then the "BIM_UL_colourscheme" list has 1 items
Scenario: Colour by property - no query
Given an empty IFC project
@@ -1,6 +1,45 @@
@structural
Feature: Structural
Scenario: Add element - a structural point connection
Given an empty IFC project
And I trigger "Add Element"
And I set the "Name" property to "Foo"
And I set the "Definition" property to "IfcStructuralItem"
And I set the "Class" property to "IfcStructuralPointConnection"
And I set the "Representation" property to "Vertex"
When I click "OK"
And I make the collection "IfcStructuralItem" visible
And I select the object "IfcStructuralPointConnection/Foo"
And I toggle edit mode
Then the object "Item/IfcVertexPoint/69" exists
Scenario: Add element - a structural curve member
Given an empty IFC project
And I trigger "Add Element"
And I set the "Name" property to "Foo"
And I set the "Definition" property to "IfcStructuralItem"
And I set the "Class" property to "IfcStructuralCurveMember"
And I set the "Representation" property to "Edge"
When I click "OK"
And I make the collection "IfcStructuralItem" visible
And I select the object "IfcStructuralCurveMember/Foo"
And I toggle edit mode
Then the object "Item/IfcEdge/72" exists
Scenario: Add element - a structural surface member
Given an empty IFC project
And I trigger "Add Element"
And I set the "Name" property to "Foo"
And I set the "Definition" property to "IfcStructuralItem"
And I set the "Class" property to "IfcStructuralSurfaceMember"
And I set the "Representation" property to "Face"
When I click "OK"
And I make the collection "IfcStructuralItem" visible
And I select the object "IfcStructuralSurfaceMember/Foo"
And I toggle edit mode
Then the object "Item/IfcFace/74" exists
Scenario: Load structural analysis models
Given an empty IFC project
When I press "bim.load_structural_analysis_models"
+274
View File
@@ -0,0 +1,274 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2025 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from typing import Union, Any, Optional
class bSDDClientStub:
def get_dictionary(self, dictionary_uri=None, include_test_dictionaries=False):
dicts = {
"dictionaries": [
{
"availableLanguages": [{"code": "EN", "name": "English"}],
"code": "LCA",
"uri": "https://identifier.buildingsmart.org/uri/LCA",
"name": "LCA indicators and modules",
"version": "3.0",
"organizationCodeOwner": "LCA",
"organizationNameOwner": "buildingSMART Sustainability Strategic Group",
"defaultLanguageCode": "EN",
"isLatestVersion": True,
"isVerified": False,
"isPrivate": False,
"license": "No license (rights reserved)",
"licenseUrl": "https://technical.buildingsmart.org/services/bsdd/license/",
"qualityAssuranceProcedure": "EN ISO 23386:2020",
"status": "Active",
"moreInfoUrl": "https://www.lignum.ch/leistungen/projekte/buildingsmart-data-dictionary-bsdd/",
"releaseDate": "2023-12-01T14:14:19Z",
"lastUpdatedUtc": "2023-12-01T14:17:53Z",
},
{
"availableLanguages": [{"code": "EN", "name": "English"}],
"code": "BonsaiTestDict",
"uri": "https://identifier.buildingsmart.org/uri/BonsaiTestDict",
"name": "BonsaiTestDict",
"version": "3.0",
"organizationCodeOwner": "LCA",
"organizationNameOwner": "buildingSMART Sustainability Strategic Group",
"defaultLanguageCode": "EN",
"isLatestVersion": True,
"isVerified": False,
"isPrivate": False,
"license": "No license (rights reserved)",
"licenseUrl": "https://technical.buildingsmart.org/services/bsdd/license/",
"qualityAssuranceProcedure": "EN ISO 23386:2020",
"status": "Active",
"moreInfoUrl": "https://www.lignum.ch/leistungen/projekte/buildingsmart-data-dictionary-bsdd/",
"releaseDate": "2023-12-01T14:14:19Z",
"lastUpdatedUtc": "2023-12-01T14:17:53Z",
},
],
"totalCount": 2,
"offset": 0,
"count": 2,
}
if not dictionary_uri:
return dicts
for dictionary in dicts["dictionaries"]:
if dictionary["uri"] == dictionary_uri:
dicts["dictionaries"] = [dictionary]
return dicts
assert False, f"Could not find dictionary uri {dictionary_uri}"
def get_classes(
self,
dictionary_uri: str,
use_nested_classes: bool = True,
class_type="Class",
search_text: str = "",
related_ifc_entity: str = "",
language_code: str = "",
version: int = 1,
offset=0,
limit=1000,
):
classes = {
"classes": [],
"classesTotalCount": 2,
"classesOffset": 0,
"classesCount": 2,
"code": "LCA",
"uri": "https://identifier.buildingsmart.org/uri/LCA/LCA/3.0",
"name": "LCA indicators and modules",
"version": "3.0",
"organizationCodeOwner": "LCA",
"organizationNameOwner": "buildingSMART Sustainability Strategic Group",
"defaultLanguageCode": "EN",
"isLatestVersion": True,
"isVerified": False,
"isPrivate": False,
"license": "No license (rights reserved)",
"licenseUrl": "https://technical.buildingsmart.org/services/bsdd/license/",
"qualityAssuranceProcedure": "EN ISO 23386:2020",
"status": "Preview",
"moreInfoUrl": "https://www.lignum.ch/leistungen/projekte/buildingsmart-data-dictionary-bsdd/",
"releaseDate": "2023-12-01T14:14:19Z",
"lastUpdatedUtc": "2023-12-01T14:17:53Z",
}
if dictionary_uri == "https://identifier.buildingsmart.org/uri/LCA":
classes["classes"].extend(
[
{
"uri": "https://identifier.buildingsmart.org/uri/LCA/LCA/3.0/class/Acidification",
"code": "Acidification",
"name": "Acidification",
"classType": "Class",
"referenceCode": "Acidification",
"descriptionPart": "",
},
{
"uri": "https://identifier.buildingsmart.org/uri/LCA/LCA/3.0/class/Biogeniccarboncontentatthefactorygate",
"code": "Biogeniccarboncontentatthefactorygate",
"name": "Biogenic carbon content at the factory gate",
"classType": "Class",
"referenceCode": "Biogeniccarboncontentatthefactorygate",
"descriptionPart": "",
},
]
)
elif dictionary_uri == "https://identifier.buildingsmart.org/uri/BonsaiTestDict":
classes["classes"].extend(
[
{
"uri": "https://identifier.buildingsmart.org/uri/BonsaiTestDict/class/BonsaiReferenceA",
"code": "BonsaiReferenceA",
"name": "BonsaiReferenceA",
"classType": "Class",
"referenceCode": "BonsaiReferenceA",
"descriptionPart": "",
},
{
"uri": "https://identifier.buildingsmart.org/uri/BonsaiTestDict/class/BonsaiReferenceB",
"code": "BonsaiReferenceB",
"name": "BonsaiReferenceB",
"classType": "Class",
"referenceCode": "BonsaiReferenceB",
"descriptionPart": "",
},
]
)
if offset != 0:
classes["classes"].clear()
classes["classesTotalCount"] = 0
classes["classesCount"] = 0
return classes
def get_class(
self,
class_uri: str,
include_class_properties: bool = True,
include_child_class_reference: bool = True,
include_class_relations: bool = True,
include_reverse_relations: bool = False,
reverse_relation_dictionary_uris: Optional[list[str]] = None,
language_code: str = "",
version: int = 1,
):
result = {
"classType": "Class",
"referenceCode": "apple",
"relatedIfcEntityNames": ["IfcCommunicationsAppliance"],
"parentClassReference": {
"uri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1/class/fruit",
"name": "Fruit",
"code": "fruit",
},
"hierarchy": [
{
"level": 1,
"name": "Fruit",
"code": "fruit",
"uri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1/class/fruit",
},
{
"level": 2,
"name": "Apple",
"code": "apple",
"uri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1/class/apple",
},
],
"classProperties": [
{
"name": "Height",
"uri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1/class/apple/prop/SizeSet/height",
"description": "The height of an apple",
"definition": "Distance from bottom to top of something standing upright",
"dataType": "Real",
"dimension": "1 0 0 0 0 0 0",
"dimensionLength": 1,
"dimensionMass": 0,
"dimensionTime": 0,
"dimensionElectricCurrent": 0,
"dimensionThermodynamicTemperature": 0,
"dimensionAmountOfSubstance": 0,
"dimensionLuminousIntensity": 0,
"isRequired": True,
"isWritable": True,
"maxInclusive": 25,
"minExclusive": 1,
"physicalQuantity": "Height",
"propertyCode": "height",
"propertyDictionaryName": "Fruit and vegetables",
"propertyDictionaryUri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1",
"propertyUri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1/prop/height",
"propertySet": "SizeSet",
"propertyStatus": "Active",
"propertyValueKind": "Single",
"units": ["cm"],
"qudtCodes": ["CentiM"],
},
{
"name": "Volume",
"uri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1/class/apple/prop/SizeSet/volume",
"description": "The volume of an apple",
"definition": "Volume is a scalar quantity expressing the amount of three-dimensional space enclosed by a closed surface. Volume is often quantified numerically using the SI derived unit, the cubic metre",
"dataType": "Real",
"dimension": "3 0 0 0 0 0 0",
"dimensionLength": 3,
"dimensionMass": 0,
"dimensionTime": 0,
"dimensionElectricCurrent": 0,
"dimensionThermodynamicTemperature": 0,
"dimensionAmountOfSubstance": 0,
"dimensionLuminousIntensity": 0,
"example": "For example, the space that a substance or 3D shape occupies or contains.",
"isRequired": True,
"isWritable": True,
"maxInclusive": 20,
"minExclusive": 1,
"physicalQuantity": "Volume",
"propertyCode": "volume",
"propertyDictionaryName": "Fruit and vegetables",
"propertyDictionaryUri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1",
"propertyUri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1/prop/volume",
"propertySet": "SizeSet",
"propertyStatus": "Active",
"propertyValueKind": "Single",
"units": ["cm³"],
"qudtCodes": ["CentiM3"],
},
],
"dictionaryUri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1",
"activationDateUtc": "2022-09-26T00:00:00Z",
"code": "apple",
"creatorLanguageCode": "en-GB",
"countriesOfUse": [],
"definition": "The round fruit of a tree of the rose family, which typically has thin green or red skin and crisp flesh",
"name": "Apple",
"uri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1/class/apple",
"replacedObjectCodes": [],
"replacingObjectCodes": [],
"revisionNumber": 0,
"status": "Active",
"subdivisionsOfUse": [],
"versionDateUtc": "2022-09-26T00:00:00Z",
"versionNumber": 1,
}
return result
+195 -21
View File
@@ -22,6 +22,7 @@ import pytest
import traceback
import webbrowser
import numpy as np
import test.bim.stub
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.representation
@@ -31,6 +32,7 @@ from bonsai.bim.ifc import IfcStore
from bonsai.tool.brick import BrickStore
from bonsai.bim.module.model.data import AuthoringData
from pytest_bdd import scenarios, given, when, then, parsers
from inspect import signature
from mathutils import Vector
from math import radians
from pathlib import Path
@@ -50,6 +52,9 @@ variables = {
webbrowser.open = lambda x: True
tool.Bsdd.client = test.bim.stub.bSDDClientStub()
class PanelSpy:
def __init__(self, panel: type[bpy.types.Panel]):
self.is_spy_dirty = True
@@ -71,6 +76,8 @@ class PanelSpy:
return annotation.keywords.get("default", None) # An operator property
if attr == "layout":
return self
if hasattr(self.panel, attr) and not callable(getattr(self.panel, attr)):
return getattr(self.panel, attr)
return self
def __call__(self, *args, **kwargs):
@@ -86,8 +93,9 @@ class PanelSpy:
"active_dataptr": active_dataptr,
"active_propname": active_propname,
}
self.spied_lists.append(spied_data)
return TemplateListSpy(spied_data)
template_list = TemplateListSpy(getattr(bpy.types, listtype_name), spied_data)
self.spied_lists.append(template_list)
return template_list
elif self.spied_attr == "context_pointer_set":
return lambda *args, **kwargs: None
elif self.spied_attr == "label":
@@ -157,9 +165,61 @@ class OperatorSpy:
self.spied_data["kwargs"][name] = value
class TemplateListSpy:
def __init__(self, spied_data):
class TemplateListSpy(PanelSpy):
def __init__(self, template_list: type[bpy.types.UIList], spied_data: dict) -> None:
self.spied_data = spied_data
self.items = getattr(self.spied_data["dataptr"], self.spied_data["propname"])
self.active_index = getattr(self.spied_data["active_dataptr"], self.spied_data["active_propname"])
try:
self.active_item = self.items[self.active_index]
except:
self.active_item = None
self.panel = template_list
self.rows = []
for item in self.items:
self.rows.append(TemplateListItemSpy(self, item))
def set_active_index(self, index: int) -> None:
self.active_index = index
setattr(self.spied_data["active_dataptr"], self.spied_data["active_propname"], self.active_index)
try:
self.active_item = self.items[self.active_index]
except:
assert False, f"Could not set active index {index}"
class TemplateListItemSpy(PanelSpy):
def __init__(self, parent: TemplateListSpy, item):
self.panel = parent.panel
self.spied_attr: Union[str, None] = None
self.spied_labels: list[str] = []
self.spied_props: list[dict[str, Any]] = []
self.spied_operators: list[dict[str, Any]] = []
if len(signature(parent.panel.draw_item).parameters) == 8:
parent.panel.draw_item(
self,
bpy.context,
self,
parent.spied_data["dataptr"],
item,
"",
parent.spied_data["active_dataptr"],
parent.spied_data["active_propname"],
)
else:
parent.panel.draw_item(
self,
bpy.context,
self,
parent.spied_data["dataptr"],
item,
"",
parent.spied_data["active_dataptr"],
parent.spied_data["active_propname"],
0, # Index?
None,
)
ui_name_cache = {}
@@ -181,6 +241,8 @@ def create_ui_name_cache():
if panel_type.bl_label == "Add" and bl_idname != "VIEW3D_MT_add":
continue # Non-unique, but "VIEW3D_MT_add" is the one we care about
ui_name_cache[panel_type.bl_label] = bl_idname
elif panel_type.bl_rna.base.name == "UIList":
ui_name_cache[panel_type.bl_rna.name] = bl_idname
except:
pass
@@ -293,6 +355,108 @@ def i_see_text(text):
assert [l for l in panel_spy.spied_labels if text in l], f"Text {text} not found in {panel_spy.spied_labels}"
@given(parsers.parse('I see "{text}" in the "{nth}" list'))
@when(parsers.parse('I see "{text}" in the "{nth}" list'))
@then(parsers.parse('I see "{text}" in the "{nth}" list'))
def i_see_text_in_the_nth_list(text, nth):
assert panel_spy
panel_spy.refresh_spy()
nth = int("".join([c for c in nth if c.isnumeric()]))
if len(panel_spy.spied_lists) < nth:
assert False, f"{nth} list does not exist. Actual number of lists: {len(panel_spy.spied_lists)}"
debug = []
for i, template_list in enumerate(panel_spy.spied_lists):
if i + 1 != nth:
continue
for row in template_list.rows:
for l in row.spied_labels:
debug.append(l)
if text in l:
return True
debug = "\n".join(debug)
assert False, f"Could not see '{text}' in any list. We saw:\n{debug}"
@given(parsers.parse('I don\'t see "{text}" in the "{nth}" list'))
@when(parsers.parse('I don\'t see "{text}" in the "{nth}" list'))
@then(parsers.parse('I don\'t see "{text}" in the "{nth}" list'))
def i_dont_see_text_in_the_nth_list(text, nth):
assert panel_spy
panel_spy.refresh_spy()
nth = int("".join([c for c in nth if c.isnumeric()]))
if len(panel_spy.spied_lists) < nth:
assert False, f"{nth} list does not exist. Actual number of lists: {len(panel_spy.spied_lists)}"
debug = []
for i, template_list in enumerate(panel_spy.spied_lists):
if i + 1 != nth:
continue
for row in template_list.rows:
for l in row.spied_labels:
debug.append(l)
if text in l:
debug = "\n".join(debug)
assert False, f"We see saw '{text}' in the {nth} list but should not have. We saw:\n{debug}"
@given(parsers.parse('I click "{button}" in the row where I see "{text}" in the "{nth}" list'))
@when(parsers.parse('I click "{button}" in the row where I see "{text}" in the "{nth}" list'))
@then(parsers.parse('I click "{button}" in the row where I see "{text}" in the "{nth}" list'))
def i_click_button_in_the_row_where_i_see_text_in_the_nth_list(button, text, nth):
"""
:param button: The text or icon of the button to click.
"""
assert panel_spy
panel_spy.refresh_spy()
nth = int("".join([c for c in nth if c.isnumeric()]))
if len(panel_spy.spied_lists) < nth:
assert False, f"{nth} list does not exist. Actual number of lists: {len(panel_spy.spied_lists)}"
debug = []
for i, template_list in enumerate(panel_spy.spied_lists):
if i + 1 != nth:
continue
for row in template_list.rows:
is_row = False
for l in row.spied_labels:
debug.append(l)
if text in l:
is_row = True
for p in row.spied_props:
debug.append(str(p))
if isinstance(p["value"], str) and text in p["value"]:
is_row = True
if is_row:
_i_click_button_on_panel(button, row)
return True
debug = "\n".join(debug)
assert False, f"Could not see '{text}' in any list. We saw:\n{debug}"
@given(parsers.parse('I select the row where I see "{text}" in the "{nth}" list'))
@when(parsers.parse('I select the row where I see "{text}" in the "{nth}" list'))
@then(parsers.parse('I select the row where I see "{text}" in the "{nth}" list'))
def i_select_the_row_where_i_see_text_in_the_nth_list(text, nth):
assert panel_spy
panel_spy.refresh_spy()
nth = int("".join([c for c in nth if c.isnumeric()]))
if len(panel_spy.spied_lists) < nth:
assert False, f"{nth} list does not exist. Actual number of lists: {len(panel_spy.spied_lists)}"
debug = []
for i, template_list in enumerate(panel_spy.spied_lists):
if i + 1 != nth:
continue
for i, row in enumerate(template_list.rows):
is_row = False
for l in row.spied_labels:
debug.append(l)
if text in l:
is_row = True
if is_row:
template_list.set_active_index(i)
return True
debug = "\n".join(debug)
assert False, f"Could not see '{text}' in any list. We saw:\n{debug}"
@given(parsers.parse('I don\'t see "{text}"'))
@when(parsers.parse('I don\'t see "{text}"'))
@then(parsers.parse('I don\'t see "{text}"'))
@@ -308,7 +472,7 @@ def i_dont_see_text(text):
def i_dont_see_the_name_list(name):
assert panel_spy
panel_spy.refresh_spy()
assert name not in [l["listtype_name"] for l in panel_spy.spied_lists]
assert name not in [l.spied_data["listtype_name"] for l in panel_spy.spied_lists]
@given(parsers.parse('I see the "{prop}" property'))
@@ -396,9 +560,9 @@ def the_name_list_has_total_items(name, total):
total = int(total)
assert panel_spy
panel_spy.refresh_spy()
for spied_list in panel_spy.spied_lists:
if name == spied_list["listtype_name"]:
actual_total = len(getattr(spied_list["dataptr"], spied_list["propname"]))
for template_list in panel_spy.spied_lists:
if name == template_list.spied_data["listtype_name"]:
actual_total = len(template_list.items)
assert actual_total == total, f"The actual number of items in {name} is {actual_total} not {total}"
return
assert False, f"List {name} not found in {panel_spy.spied_lists}"
@@ -410,13 +574,13 @@ def the_name_list_has_total_items(name, total):
def i_select_the_item_name_item_in_the_list_name_list(item_name, list_name):
assert panel_spy
panel_spy.refresh_spy()
for spied_list in panel_spy.spied_lists:
if list_name == spied_list["listtype_name"]:
for template_list in panel_spy.spied_lists:
if list_name == template_list.spied_data["listtype_name"]:
item_names = []
for i, item in enumerate(getattr(spied_list["dataptr"], spied_list["propname"])):
for i, item in enumerate(template_list.items):
item_names.append(item.name)
if item.name == item_name:
setattr(spied_list["active_dataptr"], spied_list["active_propname"], i)
template_list.set_active_index(i)
panel_spy.is_spy_dirty = True
return
assert False, f"Could not find item {item_name} in {item_names}"
@@ -494,6 +658,12 @@ def i_add_a_new_collection_item(collection):
assert False, "Collection does not exist"
@given(parsers.parse('I make the collection "{name}" visible'))
@when(parsers.parse('I make the collection "{name}" visible'))
def i_make_the_collection_name_visible(name):
tool.Blender.get_layer_collection(bpy.data.collections.get(name)).hide_viewport = False
@given(parsers.parse('the material "{name}" colour is set to "{colour}"'))
@when(parsers.parse('the material "{name}" colour is set to "{colour}"'))
def the_material_name_colour_is_set_to_colour(name, colour):
@@ -564,15 +734,7 @@ def i_press_operator(operator):
assert False, f"Failed to run operator bpy.ops.{operator} because of {e}"
@given(parsers.parse('I click "{button}"'))
@when(parsers.parse('I click "{button}"'))
@then(parsers.parse('I click "{button}"'))
def i_click_button(button):
"""
:param button: The text or icon of the button to click.
"""
assert panel_spy
panel_spy.refresh_spy()
def _i_click_button_on_panel(button, panel_spy):
for spied_operator in panel_spy.spied_operators:
if spied_operator["text"] == button or spied_operator["icon"] == button:
spied_operator["operator"]("INVOKE_DEFAULT", **spied_operator["kwargs"])
@@ -592,6 +754,18 @@ def i_click_button(button):
assert False, f"Could not find {button}:\n{debug}"
@given(parsers.parse('I click "{button}"'))
@when(parsers.parse('I click "{button}"'))
@then(parsers.parse('I click "{button}"'))
def i_click_button(button):
"""
:param button: The text or icon of the button to click.
"""
assert panel_spy
panel_spy.refresh_spy()
_i_click_button_on_panel(button, panel_spy)
@given(parsers.parse('I click the "{button}" after the text "{text}"'))
@when(parsers.parse('I click the "{button}" after the text "{text}"'))
@then(parsers.parse('I click the "{button}" after the text "{text}"'))
+6 -1
View File
@@ -22,8 +22,12 @@ import bonsai.core.tool
import bonsai.tool as tool
import pytest
from test.bim.bootstrap import NewFile
from typing import TYPE_CHECKING
from bonsai.tool.blender import Blender as subject
if TYPE_CHECKING:
import bpy._typing.rna_enums as rna_enums
class TestImplementsTool(NewFile):
def test_run(self):
@@ -34,6 +38,7 @@ class TestCopyNodeGraph(NewFile):
def test_run(self):
material_to = bpy.data.materials.new("material_to")
material_to.use_nodes = True
assert material_to.node_tree
material_to_nodes = material_to.node_tree.nodes
assert len(material_to_nodes) == 2
for node in material_to_nodes:
@@ -70,7 +75,7 @@ class TestBlenderErrorMessageExtraction(NewFile):
bl_idname = "object.test_fail_operator"
bl_label = "Test Fail Operator"
def execute(self, context):
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
self.report({"INFO"}, "Info message.")
subject.report_operator_errors(self, ERROR_REPORTS)
return {"FINISHED"}
+6 -4
View File
@@ -138,6 +138,7 @@ class TestAddBrickifcProject(NewFile):
class TestAddBrickifcReference(NewFile):
def test_run(self):
TestAddBrickifcProject().test_run()
assert BrickStore.graph
element = tool.Ifc.get().createIfcChiller(ifcopenshell.guid.new())
element.Name = "Chiller"
project = URIRef(f"http://example.org/digitaltwin#{tool.Ifc.get().by_type('IfcProject')[0].GlobalId}")
@@ -173,6 +174,7 @@ class TestAddRelation(NewFile):
class TestRemoveRelation(NewFile):
def test_run(self):
TestAddRelation().test_run()
assert BrickStore.graph
source, relation, destination = list(
BrickStore.graph.triples((None, URIRef("https://brickschema.org/schema/Brick#feeds"), None))
)[0]
@@ -321,7 +323,7 @@ class TestGetConvertableBrickSystems(NewFile):
class TestGetParentSpace(NewFile):
def test_run(cls):
def test_run(self):
ifc = ifcopenshell.file()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBuildingStorey")
subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSpace")
@@ -333,17 +335,17 @@ class TestGetParentSpace(NewFile):
class TestGetElementContainer(NewFile):
def test_nothing(cls):
def test_nothing(self):
pass
class TestGetElementSystems(NewFile):
def test_nothing(cls):
def test_nothing(self):
pass
class TestGetElementFeeds(NewFile):
def test_run(cls):
def test_run(self):
pass
+25 -8
View File
@@ -20,6 +20,9 @@ import bpy
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.aggregate
import ifcopenshell.api.feature
import ifcopenshell.api.group
import ifcopenshell.api.spatial
import ifcopenshell.util.element
import bonsai.core.tool
import bonsai.tool as tool
@@ -34,6 +37,7 @@ class TestImplementsTool(NewFile):
class TestAssign(NewIfc):
def test_walls_are_placed_in_its_spatial_collection(self):
assert bpy.context.scene
wall_obj = bpy.data.objects.new("Object", None)
wall_element = tool.Ifc.get().createIfcWall()
tool.Ifc.link(wall_element, wall_obj)
@@ -48,6 +52,7 @@ class TestAssign(NewIfc):
assert "IfcSite" in wall_obj.users_collection[0].name
def test_walls_are_unsorted_if_not_decomposes(self):
assert bpy.context.scene
wall_obj = bpy.data.objects.new("Object", None)
wall_element = tool.Ifc.get().createIfcWall()
tool.Ifc.link(wall_element, wall_obj)
@@ -57,6 +62,7 @@ class TestAssign(NewIfc):
assert "Unsorted" in wall_obj.users_collection[0].name
def test_spatial_structure_elements_are_placed_in_a_collection_of_the_same_name(self):
assert bpy.context.scene
building_obj = bpy.data.objects.new("IfcBuilding/Name", None)
building_element = tool.Ifc.get().createIfcBuilding()
tool.Ifc.link(building_element, building_obj)
@@ -71,6 +77,7 @@ class TestAssign(NewIfc):
assert building_obj.users_collection[0].name == building_obj.name
def test_spaces_are_special_and_are_placed_in_a_spaces_collection(self):
assert bpy.context.scene
space_obj = bpy.data.objects.new("IfcSpace/Name", None)
space_element = tool.Ifc.get().createIfcSpace()
tool.Ifc.link(space_element, space_obj)
@@ -85,6 +92,7 @@ class TestAssign(NewIfc):
assert space_obj.users_collection[0].name == "IfcSpace"
def test_multiple_assigns_do_not_create_duplicate_spatial_structure_collections(self):
assert bpy.context.scene
space_obj = bpy.data.objects.new("IfcSpace/Name", None)
space_element = tool.Ifc.get().createIfcSpace()
tool.Ifc.link(space_element, space_obj)
@@ -102,6 +110,7 @@ class TestAssign(NewIfc):
assert not bpy.data.collections.get("IfcSite/My Site.001")
def test_spatial_zone_elements_are_not_placed_in_a_collection_of_the_same_name(self):
assert bpy.context.scene
space_obj = bpy.data.objects.new("IfcSpaceZone/Name", None)
space_element = tool.Ifc.get().createIfcSpatialZone()
tool.Ifc.link(space_element, space_obj)
@@ -116,6 +125,7 @@ class TestAssign(NewIfc):
assert space_obj.users_collection[0].name != space_obj.name
def test_aggregates_are_also_placed_in_their_container(self):
assert bpy.context.scene
element_obj = bpy.data.objects.new("IfcElementAssembly/Name", None)
element = tool.Ifc.get().createIfcElementAssembly()
subelement_obj = bpy.data.objects.new("IfcBeam/Name", None)
@@ -142,6 +152,7 @@ class TestAssign(NewIfc):
assert "IfcSite" in subelement_obj.users_collection[0].name
def test_projects_are_placed_in_a_collection_of_the_same_name(self):
assert bpy.context.scene
tool.Ifc.set(ifcopenshell.file())
element_obj = bpy.data.objects.new("IfcProject/Name", None)
element = tool.Ifc.get().createIfcProject()
@@ -152,6 +163,7 @@ class TestAssign(NewIfc):
assert element_obj.users_collection[0].name == element_obj.name
def test_multiple_assigns_do_not_create_duplicate_collections(self):
assert bpy.context.scene
tool.Ifc.set(ifcopenshell.file())
element_obj = bpy.data.objects.new("IfcProject/Name", None)
element = tool.Ifc.get().createIfcProject()
@@ -163,6 +175,7 @@ class TestAssign(NewIfc):
assert not bpy.data.collections.get("IfcProject/Name.001")
def test_own_collections_are_retained_and_name_synced(self):
assert bpy.context.scene
building_obj = bpy.data.objects.new("IfcBuilding/Name", None)
building_element = tool.Ifc.get().createIfcBuilding()
tool.Ifc.link(building_element, building_obj)
@@ -178,22 +191,24 @@ class TestAssign(NewIfc):
)
subject.assign(building_obj)
assert bpy.context.scene.collection.children.find(building_collection.name) != -1
assert bpy.data.collections.get("IfcSite/My Site").children.find(building_collection.name) == -1
assert bpy.data.collections.get("IfcProject/My Project").children.find(building_collection.name) == -1
assert bpy.data.collections["IfcSite/My Site"].children.find(building_collection.name) == -1
assert bpy.data.collections["IfcProject/My Project"].children.find(building_collection.name) == -1
assert bpy.context.scene.collection.children.find(building_collection.name) != -1
assert building_collection.objects.find(building_obj.name) != -1
assert building_collection.name == "IfcBuilding/Name"
def test_types_are_placed_in_the_types_collection(self):
assert bpy.context.scene
element_obj = bpy.data.objects.new("IfcWallType/Name", None)
element = tool.Ifc.get().createIfcWallType()
tool.Ifc.link(element, element_obj)
bpy.context.scene.collection.objects.link(element_obj)
subject.assign(element_obj)
assert element_obj.users_collection[0].name == "IfcTypeProduct"
assert bpy.data.collections.get("IfcProject/My Project").children.get("IfcTypeProduct")
assert bpy.data.collections["IfcProject/My Project"].children.get("IfcTypeProduct")
def test_openings_are_placed_in_their_voided_elements_container(self):
assert bpy.context.scene
element_obj = bpy.data.objects.new("IfcOpeningElement/Name", None)
element = tool.Ifc.get().createIfcOpeningElement()
tool.Ifc.link(element, element_obj)
@@ -210,6 +225,7 @@ class TestAssign(NewIfc):
assert element_obj.users_collection[0].name == "IfcSite/My Site"
def test_grids_are_placed_in_their_container(self):
assert bpy.context.scene
element_obj = bpy.data.objects.new("IfcGrid/Name", None)
element = tool.Ifc.get().createIfcGrid()
tool.Ifc.link(element, element_obj)
@@ -223,6 +239,7 @@ class TestAssign(NewIfc):
assert element_obj.users_collection[0].name == "IfcSite/My Site"
def test_grids_axes_are_placed_in_the_grids_container(self):
assert bpy.context.scene
element_obj = bpy.data.objects.new("IfcGrid/Name", None)
axis_obj = bpy.data.objects.new("IfcGrid/Name", None)
axis = tool.Ifc.get().createIfcGridAxis()
@@ -250,7 +267,7 @@ class TestAssign(NewIfc):
subject.assign(element_obj)
assert element_obj.users_collection[0].name == "IfcAnnotation/DRAWING"
assert bpy.data.collections.get("IfcProject/My Project").children.get("IfcAnnotation/DRAWING")
assert bpy.data.collections["IfcProject/My Project"].children.get("IfcAnnotation/DRAWING")
def test_annotations_are_placed_in_their_drawings_collection(self):
self.test_drawings_are_placed_in_their_own_collection()
@@ -272,7 +289,7 @@ class TestAssign(NewIfc):
tool.Ifc.link(element, element_obj)
subject.assign(element_obj)
assert element_obj.users_collection[0].name == "IfcStructuralItem"
assert bpy.data.collections.get("IfcProject/My Project").children.get("IfcStructuralItem")
assert bpy.data.collections["IfcProject/My Project"].children.get("IfcStructuralItem")
def test_structural_connections_are_placed_in_a_connections_collection(self):
element_obj = bpy.data.objects.new("IfcStructuralCurveConnection/Name", None)
@@ -280,7 +297,7 @@ class TestAssign(NewIfc):
tool.Ifc.link(element, element_obj)
subject.assign(element_obj)
assert element_obj.users_collection[0].name == "IfcStructuralItem"
assert bpy.data.collections.get("IfcProject/My Project").children.get("IfcStructuralItem")
assert bpy.data.collections["IfcProject/My Project"].children.get("IfcStructuralItem")
class TestAssignIFC4X3(NewIfc4X3):
@@ -290,7 +307,7 @@ class TestAssignIFC4X3(NewIfc4X3):
tool.Ifc.link(element, element_obj)
subject.assign(element_obj)
assert element_obj.users_collection[0].name == "IfcLinearPositioningElement"
assert bpy.data.collections.get("IfcProject/My Project").children.get("IfcLinearPositioningElement")
assert bpy.data.collections["IfcProject/My Project"].children.get("IfcLinearPositioningElement")
def test_referents_are_placed_in_a_special_collection(self):
element_obj = bpy.data.objects.new("Name", None)
@@ -298,4 +315,4 @@ class TestAssignIFC4X3(NewIfc4X3):
tool.Ifc.link(element, element_obj)
subject.assign(element_obj)
assert element_obj.users_collection[0].name == "IfcReferent"
assert bpy.data.collections.get("IfcProject/My Project").children.get("IfcReferent")
assert bpy.data.collections["IfcProject/My Project"].children.get("IfcReferent")
+16 -14
View File
@@ -34,7 +34,8 @@ class TestSetContext(test.bim.bootstrap.NewFile):
ifc = ifcopenshell.file()
context = ifc.createIfcGeometricRepresentationContext()
subject.set_context(context)
assert bpy.context.scene.BIMContextProperties.active_context_id == context.id()
props = subject.get_context_props()
assert props.active_context_id == context.id()
class TestImportAttributes(test.bim.bootstrap.NewFile):
@@ -48,11 +49,11 @@ class TestImportAttributes(test.bim.bootstrap.NewFile):
context.Precision = 1
subject.set_context(context)
subject.import_attributes()
props = bpy.context.scene.BIMContextProperties
assert props.context_attributes.get("ContextIdentifier").string_value == "ContextIdentifier"
assert props.context_attributes.get("ContextType").string_value == "ContextType"
assert props.context_attributes.get("CoordinateSpaceDimension").int_value == 1
assert props.context_attributes.get("Precision").string_value == "1.0"
props = subject.get_context_props()
assert props.context_attributes["ContextIdentifier"].string_value == "ContextIdentifier"
assert props.context_attributes["ContextType"].string_value == "ContextType"
assert props.context_attributes["CoordinateSpaceDimension"].int_value == 1
assert props.context_attributes["Precision"].string_value == "1.0"
def test_importing_a_subcontext(self):
ifc = ifcopenshell.file()
@@ -63,11 +64,11 @@ class TestImportAttributes(test.bim.bootstrap.NewFile):
subcontext.UserDefinedTargetView = "UserDefinedTargetView"
subject.set_context(subcontext)
subject.import_attributes()
props = bpy.context.scene.BIMContextProperties
assert props.context_attributes.get("TargetScale").float_value == 0.5
assert props.context_attributes.get("TargetView").enum_value == "NOTDEFINED"
assert props.context_attributes.get("UserDefinedTargetView").string_value == "UserDefinedTargetView"
assert not props.context_attributes.get("Precision")
props = subject.get_context_props()
assert props.context_attributes["TargetScale"].float_value == 0.5
assert props.context_attributes["TargetView"].enum_value == "NOTDEFINED"
assert props.context_attributes["UserDefinedTargetView"].string_value == "UserDefinedTargetView"
assert not props.context_attributes["Precision"]
def test_importing_twice(self):
ifc = ifcopenshell.file()
@@ -78,15 +79,16 @@ class TestImportAttributes(test.bim.bootstrap.NewFile):
subject.import_attributes()
context.ContextIdentifier = "ContextIdentifier2"
subject.import_attributes()
props = bpy.context.scene.BIMContextProperties
assert props.context_attributes.get("ContextIdentifier").string_value == "ContextIdentifier2"
props = subject.get_context_props()
assert props.context_attributes["ContextIdentifier"].string_value == "ContextIdentifier2"
class TestClearContext(test.bim.bootstrap.NewFile):
def test_run(self):
TestSetContext().test_run()
subject.clear_context()
assert bpy.context.scene.BIMContextProperties.active_context_id == 0
props = subject.get_context_props()
assert props.active_context_id == 0
class TestGetContext(test.bim.bootstrap.NewFile):
+42 -14
View File
@@ -18,9 +18,11 @@
import os
from pathlib import Path
import numpy as np
import bpy
import mathutils
import ifcopenshell
import ifcopenshell.api.root
import ifcopenshell.guid
import ifcopenshell.util.element
import bonsai.core.tool
@@ -55,7 +57,7 @@ class TestCreateAnnotationObject(NewFile):
class TestCreateCamera(NewFile):
def test_run(self):
obj = subject.create_camera("Name", mathutils.Matrix(), "PERSPECTIVE")
obj = subject.create_camera("Name", mathutils.Matrix(), "PERSPECTIVE", "PLAN_VIEW")
assert obj.name == "Name"
assert obj.matrix_world == mathutils.Matrix()
assert isinstance(obj.data, bpy.types.Camera)
@@ -68,7 +70,7 @@ class TestCreateCamera(NewFile):
class TestCreateSvgSheet(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcProject")
tool.Ifc.set(ifc)
ifc_path = Path("test/files/temp/test.ifc").absolute()
bpy.ops.bim.save_project(filepath=str(ifc_path), should_save_as=True)
@@ -92,6 +94,7 @@ class TestDeleteCollection(NewFile):
class TestDeleteDrawingElements(NewFile):
def test_run(self):
assert bpy.context.scene
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh"))
@@ -155,13 +158,15 @@ class TestDisableEditingText(NewFile):
class TestDisableEditingAssignedProduct(NewFile):
def test_run(self):
obj = bpy.data.objects.new("Object", None)
obj.BIMAssignedProductProperties.is_editing_product = True
props = subject.get_object_assigned_product_props(obj)
props.is_editing_product = True
subject.disable_editing_assigned_product(obj)
assert obj.BIMAssignedProductProperties.is_editing_product == False
assert props.is_editing_product == False
class TestEnableEditing(NewFile):
def test_run(self):
assert bpy.context.scene
obj = bpy.data.objects.new("Object", None)
bpy.context.scene.collection.objects.link(obj)
subject.enable_editing(obj)
@@ -211,8 +216,9 @@ class TestEnableEditingText(NewFile):
class TestEnableEditingAssignedProduct(NewFile):
def test_run(self):
obj = bpy.data.objects.new("Object", None)
props = subject.get_object_assigned_product_props(obj)
subject.enable_editing_assigned_product(obj)
assert obj.BIMAssignedProductProperties.is_editing_product == True
assert props.is_editing_product == True
class TestEnsureUniqueDrawingName(NewFile):
@@ -314,6 +320,7 @@ class TestGetDocumentUri(NewFile):
class TestGetDrawingCollection(NewFile):
def test_run(self):
assert bpy.context.scene
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
obj = bpy.data.objects.new("Object", None)
@@ -375,9 +382,10 @@ class TestGetName(NewFile):
class TestGenerateDrawingMatrix(NewFile):
def test_returning_the_origin_as_a_fallback(self):
assert subject.generate_drawing_matrix("PLAN_VIEW", None) == mathutils.Matrix()
assert subject.generate_drawing_matrix("PLAN_VIEW", 0) == mathutils.Matrix()
def test_creating_a_plan_view_at_the_cursor_at_a_storey(self):
assert bpy.context.scene
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
obj = bpy.data.objects.new("Object", None)
@@ -392,11 +400,12 @@ class TestGenerateDrawingMatrix(NewFile):
assert round(m[2][3], 3) == 4.6
def test_creating_an_rcp_at_the_origin(self):
assert subject.generate_drawing_matrix("REFLECTED_PLAN_VIEW", None) == mathutils.Matrix(
assert subject.generate_drawing_matrix("REFLECTED_PLAN_VIEW", 0) == mathutils.Matrix(
((1, 0, 0, 0), (0, 1, 0, 0), (0, 0, -1, 0), (0, 0, 0, 1))
)
def test_creating_an_rcp_at_the_cursor_at_a_storey(self):
assert bpy.context.scene
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
obj = bpy.data.objects.new("Object", None)
@@ -410,61 +419,78 @@ class TestGenerateDrawingMatrix(NewFile):
)
def test_creating_a_north_elevation_at_the_cursor(self):
assert bpy.context.scene
bpy.context.scene.cursor.location = (1.0, 2.0, 3.0)
assert subject.generate_drawing_matrix("ELEVATION_VIEW", "NORTH") == mathutils.Matrix(
((-1, 0, 0, 1), (0, 0, 1, 2), (0, 1, 0, 3), (0, 0, 0, 1))
)
def test_creating_a_south_elevation_at_the_cursor(self):
assert bpy.context.scene
bpy.context.scene.cursor.location = (1.0, 2.0, 3.0)
assert subject.generate_drawing_matrix("ELEVATION_VIEW", "SOUTH") == mathutils.Matrix(
((1, 0, 0, 1), (0, 0, -1, 2), (0, 1, 0, 3), (0, 0, 0, 1))
)
def test_creating_an_east_elevation_at_the_cursor(self):
assert bpy.context.scene
bpy.context.scene.cursor.location = (1.0, 2.0, 3.0)
assert subject.generate_drawing_matrix("ELEVATION_VIEW", "EAST") == mathutils.Matrix(
((0, 0, 1, 1), (1, 0, 0, 2), (0, 1, 0, 3), (0, 0, 0, 1))
)
def test_creating_a_west_elevation_at_the_cursor(self):
assert bpy.context.scene
bpy.context.scene.cursor.location = (1.0, 2.0, 3.0)
assert subject.generate_drawing_matrix("ELEVATION_VIEW", "WEST") == mathutils.Matrix(
((0, 0, -1, 1), (-1, 0, 0, 2), (0, 1, 0, 3), (0, 0, 0, 1))
)
def test_creating_a_north_section_at_the_cursor(self):
assert bpy.context.scene
bpy.context.scene.cursor.location = (1.0, 2.0, 3.0)
assert subject.generate_drawing_matrix("SECTION_VIEW", "NORTH") == mathutils.Matrix(
((1, 0, 0, 1), (0, 0, -1, 2), (0, 1, 0, 3), (0, 0, 0, 1))
)
def test_creating_a_south_section_at_the_cursor(self):
assert bpy.context.scene
bpy.context.scene.cursor.location = (1.0, 2.0, 3.0)
assert subject.generate_drawing_matrix("SECTION_VIEW", "SOUTH") == mathutils.Matrix(
((-1, 0, 0, 1), (0, 0, 1, 2), (0, 1, 0, 3), (0, 0, 0, 1))
)
def test_creating_an_east_section_at_the_cursor(self):
assert bpy.context.scene
bpy.context.scene.cursor.location = (1.0, 2.0, 3.0)
assert subject.generate_drawing_matrix("SECTION_VIEW", "EAST") == mathutils.Matrix(
((0, 0, -1, 1), (-1, 0, 0, 2), (0, 1, 0, 3), (0, 0, 0, 1))
)
def test_creating_a_west_section_at_the_cursor(self):
assert bpy.context.scene
bpy.context.scene.cursor.location = (1.0, 2.0, 3.0)
assert subject.generate_drawing_matrix("SECTION_VIEW", "WEST") == mathutils.Matrix(
((0, 0, 1, 1), (1, 0, 0, 2), (0, 1, 0, 3), (0, 0, 0, 1))
)
def test_aligning_model_view_camera_to_viewport(self):
assert (space := tool.Blender.get_view3d_space())
assert (r3d := space.region_3d)
viewport = r3d.view_matrix.inverted()
generated = subject.generate_drawing_matrix("MODEL_VIEW", "PERSPECTIVE")
assert np.allclose(generated, viewport)
generated = subject.generate_drawing_matrix("MODEL_VIEW", "ORTHOGRAPHIC")
assert np.allclose(generated, viewport)
class TestGenerateSheetIdentification(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
subject.generate_sheet_identification() == "A01"
assert subject.generate_sheet_identification() == "A01"
document = ifc.createIfcDocumentInformation()
subject.generate_sheet_identification() == "A02"
assert subject.generate_sheet_identification() == "A02"
class TestGetTextLiteral(NewFile):
@@ -598,9 +624,9 @@ class TestImportTextAttributes(NewFile):
subject.import_text_attributes(obj)
props = tool.Drawing.get_text_props(obj)
literal_props = props.literals[0]
assert literal_props.attributes.get("Literal").string_value == "Literal"
assert literal_props.attributes.get("Path").enum_value == "RIGHT"
assert literal_props.attributes.get("BoxAlignment").string_value == "bottom-left"
assert literal_props.attributes["Literal"].string_value == "Literal"
assert literal_props.attributes["Path"].enum_value == "RIGHT"
assert literal_props.attributes["BoxAlignment"].string_value == "bottom-left"
class TestImportAssignedProduct(NewFile):
@@ -615,7 +641,8 @@ class TestImportAssignedProduct(NewFile):
tool.Ifc.link(wall, wall_obj)
tool.Ifc.link(label, label_obj)
subject.import_assigned_product(label_obj)
assert label_obj.BIMAssignedProductProperties.relating_product == wall_obj
props = subject.get_object_assigned_product_props(label_obj)
assert props.relating_product == wall_obj
def test_doing_nothing_if_no_product_to_import(self):
ifc = ifcopenshell.file()
@@ -624,7 +651,8 @@ class TestImportAssignedProduct(NewFile):
label_obj = bpy.data.objects.new("Object", None)
tool.Ifc.link(label, label_obj)
subject.import_assigned_product(label_obj)
assert label_obj.BIMAssignedProductProperties.relating_product is None
props = subject.get_object_assigned_product_props(label_obj)
assert props.relating_product is None
class TestOpenSchedule(NewFile):
+20 -19
View File
@@ -59,13 +59,13 @@ class TestImportProjectedCRS(NewFile):
projected_crs.MapUnit = unit
subject.import_projected_crs()
props = tool.Georeference.get_georeference_props()
assert props.projected_crs.get("Name").string_value == "Name"
assert props.projected_crs.get("Description").string_value == "Description"
assert props.projected_crs.get("GeodeticDatum").string_value == "GeodeticDatum"
assert props.projected_crs.get("VerticalDatum").string_value == "VerticalDatum"
assert props.projected_crs.get("MapProjection").string_value == "MapProjection"
assert props.projected_crs.get("MapZone").string_value == "MapZone"
assert props.projected_crs.get("MapUnit").enum_value == str(unit.id())
assert props.projected_crs["Name"].string_value == "Name"
assert props.projected_crs["Description"].string_value == "Description"
assert props.projected_crs["GeodeticDatum"].string_value == "GeodeticDatum"
assert props.projected_crs["VerticalDatum"].string_value == "VerticalDatum"
assert props.projected_crs["MapProjection"].string_value == "MapProjection"
assert props.projected_crs["MapZone"].string_value == "MapZone"
assert props.projected_crs["MapUnit"].enum_value == str(unit.id())
def test_run_ifc2x3(self):
ifc = ifcopenshell.file(schema="IFC2X3")
@@ -100,13 +100,13 @@ class TestImportCoordinateOperation(NewFile):
map_conversion.Scale = 6
subject.import_coordinate_operation()
props = tool.Georeference.get_georeference_props()
assert props.coordinate_operation.get("Eastings").string_value == "1.0"
assert props.coordinate_operation.get("Northings").string_value == "2.0"
assert props.coordinate_operation.get("OrthogonalHeight").string_value == "3.0"
assert props.coordinate_operation["Eastings"].string_value == "1.0"
assert props.coordinate_operation["Northings"].string_value == "2.0"
assert props.coordinate_operation["OrthogonalHeight"].string_value == "3.0"
assert props.x_axis_abscissa == "4.0"
assert props.x_axis_ordinate == "5.0"
assert props.grid_north_angle == "-51.3401917"
assert props.coordinate_operation.get("Scale").string_value == "6.0"
assert props.coordinate_operation["Scale"].string_value == "6.0"
def test_run_ifc2x3(self):
ifc = ifcopenshell.file(schema="IFC2X3")
@@ -220,6 +220,7 @@ class TestGetCursorLocation(NewFile):
ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
unit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT", prefix="MILLI")
ifcopenshell.api.run("unit.assign_unit", ifc, units=[unit])
assert bpy.context.scene
bpy.context.scene.cursor.location = (1.0, 2.0, 3.0)
assert subject.get_cursor_location() == [1000.0, 2000.0, 3000.0]
@@ -229,7 +230,7 @@ class TestXyz2Enh(NewFile):
ifc = ifcopenshell.file()
ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
tool.Ifc.set(ifc)
assert subject.xyz2enh([0.0, 0.0, 0.0]) == (0.0, 0.0, 0.0)
assert subject.xyz2enh((0.0, 0.0, 0.0)) == (0.0, 0.0, 0.0)
def test_using_the_blender_offset(self):
ifc = ifcopenshell.file()
@@ -238,7 +239,7 @@ class TestXyz2Enh(NewFile):
props = tool.Georeference.get_georeference_props()
props.has_blender_offset = True
props.blender_offset_x = "1.0"
assert subject.xyz2enh([0.0, 0.0, 0.0]) == (1.0, 0.0, 0.0)
assert subject.xyz2enh((0.0, 0.0, 0.0)) == (1.0, 0.0, 0.0)
def test_using_the_map_conversion(self):
ifc = ifcopenshell.file()
@@ -248,7 +249,7 @@ class TestXyz2Enh(NewFile):
ifcopenshell.api.run("georeference.add_georeferencing", ifc)
map_conversion = ifc.by_type("IfcMapConversion")[0]
map_conversion.Eastings = 1.0
assert subject.xyz2enh([0.0, 0.0, 0.0]) == (1.0, 0.0, 0.0)
assert subject.xyz2enh((0.0, 0.0, 0.0)) == (1.0, 0.0, 0.0)
def test_applying_both_blender_offset_and_map_conversion(self):
props = tool.Georeference.get_georeference_props()
@@ -261,7 +262,7 @@ class TestXyz2Enh(NewFile):
ifcopenshell.api.run("georeference.add_georeferencing", ifc)
map_conversion = ifc.by_type("IfcMapConversion")[0]
map_conversion.Northings = 1.0
assert subject.xyz2enh([0.0, 0.0, 0.0]) == (1.0, 1.0, 0.0)
assert subject.xyz2enh((0.0, 0.0, 0.0)) == (1.0, 1.0, 0.0)
class TestEnh2Xyz(NewFile):
@@ -269,7 +270,7 @@ class TestEnh2Xyz(NewFile):
ifc = ifcopenshell.file()
ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
tool.Ifc.set(ifc)
assert subject.enh2xyz([0.0, 0.0, 0.0]) == (0.0, 0.0, 0.0)
assert subject.enh2xyz((0.0, 0.0, 0.0)) == (0.0, 0.0, 0.0)
def test_using_the_blender_offset(self):
ifc = ifcopenshell.file()
@@ -278,7 +279,7 @@ class TestEnh2Xyz(NewFile):
props = tool.Georeference.get_georeference_props()
props.has_blender_offset = True
props.blender_offset_x = "1.0"
assert subject.enh2xyz([0.0, 0.0, 0.0]) == (-1.0, 0.0, 0.0)
assert subject.enh2xyz((0.0, 0.0, 0.0)) == (-1.0, 0.0, 0.0)
def test_using_the_map_conversion(self):
ifc = ifcopenshell.file()
@@ -288,7 +289,7 @@ class TestEnh2Xyz(NewFile):
ifcopenshell.api.run("georeference.add_georeferencing", ifc)
map_conversion = ifc.by_type("IfcMapConversion")[0]
map_conversion.Eastings = 1.0
assert subject.enh2xyz([0.0, 0.0, 0.0]) == (-1.0, 0.0, 0.0)
assert subject.enh2xyz((0.0, 0.0, 0.0)) == (-1.0, 0.0, 0.0)
def test_applying_both_blender_offset_and_map_conversion(self):
props = tool.Georeference.get_georeference_props()
@@ -301,4 +302,4 @@ class TestEnh2Xyz(NewFile):
ifcopenshell.api.run("georeference.add_georeferencing", ifc)
map_conversion = ifc.by_type("IfcMapConversion")[0]
map_conversion.Northings = 1.0
assert subject.enh2xyz([0.0, 0.0, 0.0]) == (-1.0, -1.0, 0.0)
assert subject.enh2xyz((0.0, 0.0, 0.0)) == (-1.0, -1.0, 0.0)
+10 -10
View File
@@ -79,11 +79,11 @@ class TestImportLibraryAttributes(NewFile):
library = ifc.createIfcLibraryInformation("Name", "Version", None, "VersionDate", "Location", "Description")
subject.import_library_attributes(library)
props = tool.Library.get_library_props()
assert props.library_attributes.get("Name").string_value == "Name"
assert props.library_attributes.get("Version").string_value == "Version"
assert props.library_attributes.get("VersionDate").string_value == "VersionDate"
assert props.library_attributes.get("Location").string_value == "Location"
assert props.library_attributes.get("Description").string_value == "Description"
assert props.library_attributes["Name"].string_value == "Name"
assert props.library_attributes["Version"].string_value == "Version"
assert props.library_attributes["VersionDate"].string_value == "VersionDate"
assert props.library_attributes["Location"].string_value == "Location"
assert props.library_attributes["Description"].string_value == "Description"
class TestImportReferenceAttributes(NewFile):
@@ -92,11 +92,11 @@ class TestImportReferenceAttributes(NewFile):
reference = ifc.createIfcLibraryReference("Location", "Identification", "Name", "Description", "Language")
subject.import_reference_attributes(reference)
props = tool.Library.get_library_props()
assert props.reference_attributes.get("Location").string_value == "Location"
assert props.reference_attributes.get("Identification").string_value == "Identification"
assert props.reference_attributes.get("Name").string_value == "Name"
assert props.reference_attributes.get("Description").string_value == "Description"
assert props.reference_attributes.get("Language").string_value == "Language"
assert props.reference_attributes["Location"].string_value == "Location"
assert props.reference_attributes["Identification"].string_value == "Identification"
assert props.reference_attributes["Name"].string_value == "Name"
assert props.reference_attributes["Description"].string_value == "Description"
assert props.reference_attributes["Language"].string_value == "Language"
class TestImportReferences(NewFile):
+2
View File
@@ -19,7 +19,9 @@
import bpy
import ifcopenshell
import ifcopenshell.api.material
import ifcopenshell.api.root
import ifcopenshell.api.style
import ifcopenshell.api.type
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.unit
+75 -65
View File
@@ -36,7 +36,7 @@ class TestAddAddressAttribute(NewFile):
subject().add_address_attribute("FacsimileNumbers")
subject().add_address_attribute("ElectronicMailAddresses")
subject().add_address_attribute("MessagingIDs")
props = bpy.context.scene.BIMOwnerProperties
props = subject.get_owner_props()
assert len(props.address_lines) == 1
assert len(props.telephone_numbers) == 1
assert len(props.facsimile_numbers) == 1
@@ -49,7 +49,7 @@ class TestAddPersonAttribute(NewFile):
subject().add_person_attribute("MiddleNames")
subject().add_person_attribute("PrefixTitles")
subject().add_person_attribute("SuffixTitles")
props = bpy.context.scene.BIMOwnerProperties
props = subject.get_owner_props()
assert len(props.middle_names) == 1
assert len(props.prefix_titles) == 1
assert len(props.suffix_titles) == 1
@@ -61,7 +61,8 @@ class TestClearActor(NewFile):
actor = ifc.createIfcActor()
subject().set_actor(actor)
subject().clear_actor()
assert bpy.context.scene.BIMOwnerProperties.active_actor_id == 0
props = subject.get_owner_props()
assert props.active_actor_id == 0
class TestClearAddress(NewFile):
@@ -70,12 +71,13 @@ class TestClearAddress(NewFile):
address = ifc.createIfcPostalAddress()
subject().set_address(address)
subject().clear_address()
assert bpy.context.scene.BIMOwnerProperties.active_address_id == 0
props = subject.get_owner_props()
assert props.active_address_id == 0
class TestClearOrganisation(NewFile):
def test_run(self):
props = bpy.context.scene.BIMOwnerProperties
props = subject.get_owner_props()
props.active_organisation_id = 1
subject().clear_organisation()
assert props.active_organisation_id == 0
@@ -83,7 +85,7 @@ class TestClearOrganisation(NewFile):
class TestClearPerson(NewFile):
def test_run(self):
props = bpy.context.scene.BIMOwnerProperties
props = subject.get_owner_props()
props.active_person_id = 1
subject().clear_person()
assert props.active_person_id == 0
@@ -94,14 +96,16 @@ class TestClearRole(NewFile):
role = ifcopenshell.file().createIfcActorRole()
subject().set_role(role)
subject().clear_role()
assert bpy.context.scene.BIMOwnerProperties.active_role_id == 0
props = subject.get_owner_props()
assert props.active_role_id == 0
class TestClearUser(NewFile):
def test_run(self):
TestSetUser().test_run()
subject.clear_user()
assert bpy.context.scene.BIMOwnerProperties.active_user_id == 0
props = subject.get_owner_props()
assert props.active_user_id == 0
class TestExportActorAttributes(NewFile):
@@ -288,11 +292,11 @@ class TestImportActorAttributes(NewFile):
actor.ObjectType = "ObjectType"
subject.set_actor(actor)
subject.import_actor_attributes(actor)
props = bpy.context.scene.BIMOwnerProperties
assert props.actor_attributes.get("GlobalId").string_value == "GlobalId"
assert props.actor_attributes.get("Name").string_value == "Name"
assert props.actor_attributes.get("Description").string_value == "Description"
assert props.actor_attributes.get("ObjectType").string_value == "ObjectType"
props = subject.get_owner_props()
assert props.actor_attributes["GlobalId"].string_value == "GlobalId"
assert props.actor_attributes["Name"].string_value == "Name"
assert props.actor_attributes["Description"].string_value == "Description"
assert props.actor_attributes["ObjectType"].string_value == "ObjectType"
def test_importing_an_occupant(self):
ifc = ifcopenshell.file()
@@ -305,12 +309,12 @@ class TestImportActorAttributes(NewFile):
actor.PredefinedType = "TENANT"
subject.set_actor(actor)
subject.import_actor_attributes(actor)
props = bpy.context.scene.BIMOwnerProperties
assert props.actor_attributes.get("GlobalId").string_value == "GlobalId"
assert props.actor_attributes.get("Name").string_value == "Name"
assert props.actor_attributes.get("Description").string_value == "Description"
assert props.actor_attributes.get("ObjectType").string_value == "ObjectType"
assert props.actor_attributes.get("PredefinedType").enum_value == "TENANT"
props = subject.get_owner_props()
assert props.actor_attributes["GlobalId"].string_value == "GlobalId"
assert props.actor_attributes["Name"].string_value == "Name"
assert props.actor_attributes["Description"].string_value == "Description"
assert props.actor_attributes["ObjectType"].string_value == "ObjectType"
assert props.actor_attributes["PredefinedType"].enum_value == "TENANT"
class TestImportAddressAttributes(NewFile):
@@ -330,16 +334,16 @@ class TestImportAddressAttributes(NewFile):
address.Country = "Country"
subject().set_address(address)
subject().import_address_attributes()
props = bpy.context.scene.BIMOwnerProperties
assert props.address_attributes.get("Purpose").enum_value == "USERDEFINED"
assert props.address_attributes.get("Description").string_value == "Description"
assert props.address_attributes.get("UserDefinedPurpose").string_value == "UserDefinedPurpose"
assert props.address_attributes.get("InternalLocation").string_value == "InternalLocation"
assert props.address_attributes.get("PostalBox").string_value == "PostalBox"
assert props.address_attributes.get("Town").string_value == "Town"
assert props.address_attributes.get("Region").string_value == "Region"
assert props.address_attributes.get("PostalCode").string_value == "PostalCode"
assert props.address_attributes.get("Country").string_value == "Country"
props = subject.get_owner_props()
assert props.address_attributes["Purpose"].enum_value == "USERDEFINED"
assert props.address_attributes["Description"].string_value == "Description"
assert props.address_attributes["UserDefinedPurpose"].string_value == "UserDefinedPurpose"
assert props.address_attributes["InternalLocation"].string_value == "InternalLocation"
assert props.address_attributes["PostalBox"].string_value == "PostalBox"
assert props.address_attributes["Town"].string_value == "Town"
assert props.address_attributes["Region"].string_value == "Region"
assert props.address_attributes["PostalCode"].string_value == "PostalCode"
assert props.address_attributes["Country"].string_value == "Country"
assert len(props.address_lines) == 2
assert props.address_lines[0].name == "Address"
assert props.address_lines[1].name == "Lines"
@@ -351,8 +355,8 @@ class TestImportAddressAttributes(NewFile):
address.Purpose = "OFFICE"
subject().set_address(address)
subject().import_address_attributes()
props = bpy.context.scene.BIMOwnerProperties
assert props.address_attributes.get("Purpose").enum_value == "OFFICE"
props = subject.get_owner_props()
assert props.address_attributes["Purpose"].enum_value == "OFFICE"
assert len(props.address_lines) == 0
def test_importing_a_telecom_address(self):
@@ -370,10 +374,10 @@ class TestImportAddressAttributes(NewFile):
address.MessagingIDs = ["Messaging", "IDs"]
subject().set_address(address)
subject().import_address_attributes()
props = bpy.context.scene.BIMOwnerProperties
assert props.address_attributes.get("Purpose").enum_value == "USERDEFINED"
assert props.address_attributes.get("Description").string_value == "Description"
assert props.address_attributes.get("UserDefinedPurpose").string_value == "UserDefinedPurpose"
props = subject.get_owner_props()
assert props.address_attributes["Purpose"].enum_value == "USERDEFINED"
assert props.address_attributes["Description"].string_value == "Description"
assert props.address_attributes["UserDefinedPurpose"].string_value == "UserDefinedPurpose"
assert [a.name for a in props.telephone_numbers] == ["Telephone", "Numbers"]
assert [a.name for a in props.facsimile_numbers] == ["Facsimile", "Numbers"]
assert [a.name for a in props.electronic_mail_addresses] == ["Electronic", "Mail", "Addresses"]
@@ -386,8 +390,8 @@ class TestImportAddressAttributes(NewFile):
address.Purpose = "OFFICE"
subject().set_address(address)
subject().import_address_attributes()
props = bpy.context.scene.BIMOwnerProperties
assert props.address_attributes.get("Purpose").enum_value == "OFFICE"
props = subject.get_owner_props()
assert props.address_attributes["Purpose"].enum_value == "OFFICE"
assert len(props.telephone_numbers) == 0
assert len(props.facsimile_numbers) == 0
assert len(props.electronic_mail_addresses) == 0
@@ -404,10 +408,10 @@ class TestImportOrganisationAttributes(NewFile):
organisation.Description = "Description"
subject().set_organisation(organisation)
subject().import_organisation_attributes()
props = bpy.context.scene.BIMOwnerProperties
assert props.organisation_attributes.get("Identification").string_value == "Identification"
assert props.organisation_attributes.get("Name").string_value == "Name"
assert props.organisation_attributes.get("Description").string_value == "Description"
props = subject.get_owner_props()
assert props.organisation_attributes["Identification"].string_value == "Identification"
assert props.organisation_attributes["Name"].string_value == "Name"
assert props.organisation_attributes["Description"].string_value == "Description"
def test_overwriting_a_previous_import(self):
ifc = ifcopenshell.file()
@@ -420,9 +424,9 @@ class TestImportOrganisationAttributes(NewFile):
organisation.Identification = "Identification2"
organisation.Description = None
subject().import_organisation_attributes()
props = bpy.context.scene.BIMOwnerProperties
assert props.organisation_attributes.get("Identification").string_value == "Identification2"
assert props.organisation_attributes.get("Description").string_value == ""
props = subject.get_owner_props()
assert props.organisation_attributes["Identification"].string_value == "Identification2"
assert props.organisation_attributes["Description"].string_value == ""
class TestImportPersonAttributes(NewFile):
@@ -438,10 +442,10 @@ class TestImportPersonAttributes(NewFile):
person.SuffixTitles = ("suffix", "titles")
subject().set_person(person)
subject().import_person_attributes()
props = bpy.context.scene.BIMOwnerProperties
assert props.person_attributes.get("Identification").string_value == "identification"
assert props.person_attributes.get("GivenName").string_value == "given_name"
assert props.person_attributes.get("FamilyName").string_value == "family_name"
props = subject.get_owner_props()
assert props.person_attributes["Identification"].string_value == "identification"
assert props.person_attributes["GivenName"].string_value == "given_name"
assert props.person_attributes["FamilyName"].string_value == "family_name"
assert len(props.middle_names) == 2
assert props.middle_names[0].name == "middle"
assert props.middle_names[1].name == "names"
@@ -463,9 +467,9 @@ class TestImportPersonAttributes(NewFile):
person.Identification = "identification2"
person.GivenName = None
subject().import_person_attributes()
props = bpy.context.scene.BIMOwnerProperties
assert props.person_attributes.get("Identification").string_value == "identification2"
assert props.person_attributes.get("GivenName").string_value == ""
props = subject.get_owner_props()
assert props.person_attributes["Identification"].string_value == "identification2"
assert props.person_attributes["GivenName"].string_value == ""
class TestImportRoleAttributes(NewFile):
@@ -478,10 +482,10 @@ class TestImportRoleAttributes(NewFile):
role.Description = "Description"
subject().set_role(role)
subject().import_role_attributes()
props = bpy.context.scene.BIMOwnerProperties
assert props.role_attributes.get("Role").enum_value == "USERDEFINED"
assert props.role_attributes.get("UserDefinedRole").string_value == "UserDefinedRole"
assert props.role_attributes.get("Description").string_value == "Description"
props = subject.get_owner_props()
assert props.role_attributes["Role"].enum_value == "USERDEFINED"
assert props.role_attributes["UserDefinedRole"].string_value == "UserDefinedRole"
assert props.role_attributes["Description"].string_value == "Description"
def test_importing_twice(self):
ifc = ifcopenshell.file()
@@ -492,8 +496,8 @@ class TestImportRoleAttributes(NewFile):
subject().import_role_attributes()
role.Role = "ARCHITECT"
subject().import_role_attributes()
props = bpy.context.scene.BIMOwnerProperties
assert props.role_attributes.get("Role").enum_value == "ARCHITECT"
props = subject.get_owner_props()
assert props.role_attributes["Role"].enum_value == "ARCHITECT"
class TestRemoveAddressAttribute(NewFile):
@@ -503,7 +507,7 @@ class TestRemoveAddressAttribute(NewFile):
subject().remove_address_attribute("FacsimileNumbers", 0)
subject().remove_address_attribute("ElectronicMailAddresses", 0)
subject().remove_address_attribute("MessagingIDs", 0)
props = bpy.context.scene.BIMOwnerProperties
props = subject.get_owner_props()
assert len(props.address_lines) == 0
assert len(props.telephone_numbers) == 0
assert len(props.facsimile_numbers) == 0
@@ -519,7 +523,7 @@ class TestRemovePersonAttribute(NewFile):
subject().remove_person_attribute("PrefixTitles", 0)
subject().add_person_attribute("SuffixTitles")
subject().remove_person_attribute("SuffixTitles", 0)
props = bpy.context.scene.BIMOwnerProperties
props = subject.get_owner_props()
assert len(props.middle_names) == 0
assert len(props.prefix_titles) == 0
assert len(props.suffix_titles) == 0
@@ -530,7 +534,8 @@ class TestSetActor(NewFile):
ifc = ifcopenshell.file()
actor = ifc.createIfcActor()
subject().set_actor(actor)
assert bpy.context.scene.BIMOwnerProperties.active_actor_id == actor.id()
props = subject.get_owner_props()
assert props.active_actor_id == actor.id()
class TestSetAddress(NewFile):
@@ -538,28 +543,32 @@ class TestSetAddress(NewFile):
ifc = ifcopenshell.file()
address = ifc.createIfcPostalAddress()
subject().set_address(address)
assert bpy.context.scene.BIMOwnerProperties.active_address_id == address.id()
props = subject.get_owner_props()
assert props.active_address_id == address.id()
class TestSetOrganisation(NewFile):
def test_run(self):
organisation = ifcopenshell.file().createIfcOrganization()
subject().set_organisation(organisation)
assert bpy.context.scene.BIMOwnerProperties.active_organisation_id == organisation.id()
props = subject.get_owner_props()
assert props.active_organisation_id == organisation.id()
class TestSetPerson(NewFile):
def test_run(self):
person = ifcopenshell.file().createIfcPerson()
subject().set_person(person)
assert bpy.context.scene.BIMOwnerProperties.active_person_id == person.id()
props = subject.get_owner_props()
assert props.active_person_id == person.id()
class TestSetRole(NewFile):
def test_run(self):
role = ifcopenshell.file().createIfcActorRole()
subject().set_role(role)
assert bpy.context.scene.BIMOwnerProperties.active_role_id == role.id()
props = subject.get_owner_props()
assert props.active_role_id == role.id()
class TestSetUser(NewFile):
@@ -568,4 +577,5 @@ class TestSetUser(NewFile):
tool.Ifc.set(ifc)
user = ifc.createIfcPersonAndOrganization()
subject.set_user(user)
assert bpy.context.scene.BIMOwnerProperties.active_user_id == user.id()
props = subject.get_owner_props()
assert props.active_user_id == user.id()

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