mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-07 08:21:42 +00:00
Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8f18b5b6d | |||
| 718254fe5e | |||
| 41227b89c8 | |||
| 2547ee9ab5 | |||
| d22f1bcd71 | |||
| 0c2158089d | |||
| ccafc203b5 | |||
| 0efc2d25f1 | |||
| ac50f002f0 | |||
| 66f4c957ea | |||
| 67016d6f90 | |||
| 439f9c14ce | |||
| 404bf13a18 | |||
| 3a37bcdcaa | |||
| 992ad8e24a | |||
| 1de2515fcd | |||
| 4bc88eac2f | |||
| 22f10e5549 | |||
| 8e34c1d7df | |||
| 4369baa226 | |||
| 7941d71feb | |||
| 68d967c2e5 | |||
| 4ea9a598c7 | |||
| 06d1c81954 | |||
| aa5f08880b | |||
| 9e6320d545 | |||
| f39d3d4b9c | |||
| 5c72cf35bd | |||
| b9a435e1c3 | |||
| 92f2559ce0 | |||
| acf9713943 | |||
| a77125ff12 | |||
| c8841bcb6a | |||
| b20997dea8 | |||
| 292aca6685 | |||
| 3a824ec969 | |||
| bea9f42f87 | |||
| e4a50b32ce | |||
| 76611024b6 | |||
| 92ee82374c | |||
| af81aa1fae | |||
| 16f9f91561 | |||
| 3a52a848ae | |||
| a50c633cf4 | |||
| 6251750133 | |||
| acef6379e8 | |||
| c2860e6e25 | |||
| b8e5cd740a | |||
| 9188dc60ab | |||
| 8ce56db0c3 | |||
| b91b1401f4 | |||
| 0e37528915 | |||
| 9991c4aa56 | |||
| 0ef3983035 | |||
| 0f6e6a48f4 | |||
| 3d225beafd | |||
| 6a91bbb720 | |||
| 1f23e4ba18 | |||
| 2ebcffc7d4 | |||
| 3d4321f713 | |||
| f8799305e1 | |||
| 7c38705958 | |||
| f6fc0cfa0f | |||
| 686e9c946b | |||
| 3085072751 | |||
| 0328be4093 | |||
| 6218d3902e | |||
| be1836fa48 | |||
| 22b5db8a1e | |||
| 4268e1376c | |||
| e2c1a54e0a | |||
| d1be660469 | |||
| 3ae5227b57 | |||
| cdc994ca76 | |||
| 0ad4087f0a | |||
| 4b7ca648c6 | |||
| 17bd6e6b0c | |||
| 7b80803cbe | |||
| eaf67d83f9 | |||
| 41f754e15b | |||
| b7cdb86327 | |||
| 809a20dc99 | |||
| f5e41d18c6 | |||
| 60a8c45ad7 | |||
| 73f98bfe4b | |||
| 958686958b | |||
| e0b61c5794 | |||
| 5aa5552759 | |||
| 82a19cee7b | |||
| 43d4adbd9d | |||
| 1d3c71985f | |||
| 381fca0812 | |||
| 0f37eca333 | |||
| 6bf9b8b0c9 | |||
| 9361cfee28 | |||
| 1c205d494a | |||
| 4d1d224a34 | |||
| 4976af5e66 | |||
| be657748f3 | |||
| def9352ade | |||
| a20002ecca | |||
| b7a0f230ad | |||
| 76529a06bb | |||
| dcdae67a2d | |||
| 5a7a368169 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -49,3 +49,7 @@ if errorlevel 1 exit 1
|
||||
ninja install -j 1
|
||||
|
||||
if errorlevel 1 exit 1
|
||||
|
||||
python %RECIPE_DIR%/update_version_init.py %PKG_VERSION% %SP_DIR%/ifcopenshell/__init__.py
|
||||
|
||||
if errorlevel 1 exit 1
|
||||
+3
-1
@@ -46,4 +46,6 @@ cmake ${CMAKE_ARGS} -G Ninja \
|
||||
|
||||
ninja
|
||||
|
||||
ninja install -j 1
|
||||
ninja install -j 1
|
||||
|
||||
python "${RECIPE_DIR}/update_version_init.py" "${PKG_VERSION}" "${SP_DIR}/ifcopenshell/__init__.py"
|
||||
@@ -1,7 +1,3 @@
|
||||
variant:
|
||||
- novtk
|
||||
- all
|
||||
|
||||
occt:
|
||||
- 7.8.1
|
||||
|
||||
@@ -24,12 +20,13 @@ cxx_compiler_version:
|
||||
- '12' # [linux]
|
||||
- '16' # [osx]
|
||||
c_stdlib_version:
|
||||
- '2.12' # [linux]
|
||||
- '10.13' # [osx]
|
||||
- 2.17 # [linux]
|
||||
- 10.13 # [osx and x86_64]
|
||||
- 11.0 # [osx and arm64]
|
||||
hdf5:
|
||||
- 1.14.3
|
||||
- 1.14.6
|
||||
libboost_devel:
|
||||
- '1.84'
|
||||
- '1.86'
|
||||
libxml2:
|
||||
- '2'
|
||||
mpfr:
|
||||
@@ -47,12 +44,12 @@ target_platform:
|
||||
- win-64 # [win]
|
||||
- linux-64 # [linux]
|
||||
- osx-64 # [osx]
|
||||
macos_machine:
|
||||
- x86_64-apple-darwin13.4.0
|
||||
MACOSX_DEPLOYMENT_TARGET:
|
||||
- '10.13'
|
||||
MACOSX_SDK_VERSION:
|
||||
- '10.13'
|
||||
macos_machine: # [osx]
|
||||
- x86_64-apple-darwin13.4.0 # [osx and x86_64]
|
||||
- arm64-apple-darwin20.0.0 # [osx and arm64]
|
||||
MACOSX_DEPLOYMENT_TARGET: # [osx]
|
||||
- 11.0 # [osx and arm64]
|
||||
- 10.13 # [osx and x86_64]
|
||||
|
||||
CONDA_BUILD_SYSROOT:
|
||||
- "/Users/runner/work/MacOSX10.13.sdk" # [osx]
|
||||
CONDA_BUILD_SYSROOT: # [osx]
|
||||
- "/Users/runner/work/MacOSX10.13.sdk" # [osx and x86_64]
|
||||
@@ -1,64 +1,78 @@
|
||||
{% set version = environ.get('VERSION_OVERRIDE', '0.8.0') %}
|
||||
{% set build = 0 %}
|
||||
|
||||
# Higher number -> Always prioritize "novtk" variant over "all" variant
|
||||
{% set build = build + 200 %} # [variant == "novtk"]
|
||||
{% set build = build + 100 %} # [variant == "all"]
|
||||
context:
|
||||
version: ${{ env.get("VERSION_OVERRIDE", default="0.8.2.1") }}
|
||||
build: 1
|
||||
|
||||
package:
|
||||
name: ifcopenshell
|
||||
version: {{ version }}
|
||||
version: ${{ version }}
|
||||
|
||||
source:
|
||||
path: ../
|
||||
- path: ..
|
||||
|
||||
|
||||
build:
|
||||
number: {{ build }}
|
||||
skip: true # [py<39]
|
||||
binary_relocation: false # [osx]
|
||||
string: py{{ CONDA_PY }}_{{ variant }}_h{{ PKG_HASH }}_{{ build }}
|
||||
run_exports:
|
||||
- {{ pin_subpackage('ifcopenshell', max_pin='x.x.x') }} *{{ variant }}*
|
||||
|
||||
number: ${{ build }}
|
||||
dynamic_linking:
|
||||
binary_relocation: ${{ true if osx }}
|
||||
|
||||
requirements:
|
||||
build:
|
||||
- cmake
|
||||
- if: build_platform != target_platform
|
||||
then:
|
||||
- python
|
||||
- cross-python_${{ target_platform }}
|
||||
- cmake <4
|
||||
- ninja
|
||||
- swig >=4.1.1
|
||||
- {{ stdlib("c") }}
|
||||
- {{ compiler('c') }}
|
||||
- {{ compiler('cxx') }}
|
||||
- ${{ stdlib("c") }}
|
||||
- ${{ compiler('c') }}
|
||||
- ${{ compiler('cxx') }}
|
||||
host:
|
||||
- python
|
||||
- libboost-devel
|
||||
- occt *=*{{ variant }}*
|
||||
- occt
|
||||
- libxml2
|
||||
- cgal-cpp
|
||||
- hdf5
|
||||
- eigen
|
||||
- mpfr
|
||||
- nlohmann_json
|
||||
- gmp # [unix]
|
||||
- mpir # [win]
|
||||
- gmp
|
||||
- zlib
|
||||
run:
|
||||
- python
|
||||
- {{ pin_compatible('occt', max_pin='x.x.x') }} *{{ variant }}*
|
||||
- {{ pin_compatible('cgal-cpp', max_pin='x.x') }}
|
||||
|
||||
|
||||
test:
|
||||
imports:
|
||||
- ifcopenshell
|
||||
- shapely
|
||||
- typing_extensions
|
||||
- ${{ pin_compatible('occt', upper_bound='x.x.x') }}
|
||||
- ${{ pin_compatible('cgal-cpp', upper_bound='x.x') }}
|
||||
run_exports:
|
||||
- ${{ pin_subpackage('ifcopenshell', upper_bound='x.x.x') }}
|
||||
|
||||
tests:
|
||||
- python:
|
||||
imports:
|
||||
- ifcopenshell
|
||||
pip_check: false
|
||||
- script:
|
||||
- python -c "import ifcopenshell; assert ifcopenshell.version == '${{ version }}', 'print(ifcopenshell.version)'"
|
||||
requirements:
|
||||
run:
|
||||
- occt * *novtk* # Ensure that even though compiled against OCCT with VTK, it can still run with OCCT without VTK
|
||||
- pytest
|
||||
- python-dateutil
|
||||
- xmlschema
|
||||
- xsdata
|
||||
- lxml
|
||||
- isodate
|
||||
- lark
|
||||
- networkx
|
||||
- tabulate
|
||||
- shapely
|
||||
|
||||
about:
|
||||
home: https://ifcopenshell.org
|
||||
license: LGPL-3.0-or-later
|
||||
license_file: COPYING
|
||||
summary: 'IfcOpenShell is a library to support the IFC file format'
|
||||
summary: IfcOpenShell is a library to support the IFC file format
|
||||
description: |
|
||||
IfcOpenShell
|
||||
============
|
||||
@@ -293,8 +307,9 @@ about:
|
||||
[MSYS2]: https://msys2.github.io/ "MSYS2"
|
||||
[win/readme.md]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/win/readme.md "win/readme.md"
|
||||
[nix/build-all.py]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/nix/build-all.py "nix/build-all.py"
|
||||
doc_url: https://ifcopenshell.org/
|
||||
dev_url: https://github.com/IfcOpenShell/IfcOpenShell
|
||||
homepage: https://ifcopenshell.org
|
||||
repository: https://github.com/IfcOpenShell/IfcOpenShell
|
||||
documentation: https://ifcopenshell.org/
|
||||
|
||||
extra:
|
||||
recipe-maintainers:
|
||||
@@ -0,0 +1,32 @@
|
||||
import re
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
def update_version(file_path: str, version: str) -> None:
|
||||
"""Update the version string in the given __init__.py file."""
|
||||
file_path = Path(file_path)
|
||||
|
||||
# Read the file and replace the version
|
||||
file_contents = file_path.read_text(encoding="utf-8")
|
||||
new_contents = re.sub(r'version = "0\.0\.0"', f'version = "{version}"', file_contents)
|
||||
|
||||
# Write the updated contents back to the file
|
||||
file_path.write_text(new_contents, encoding="utf-8")
|
||||
|
||||
print(f"Updated version in {file_path} to {version}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Update the version string in a Python file.")
|
||||
parser.add_argument(
|
||||
"version",
|
||||
type=str,
|
||||
help="The version string to replace '0.0.0' with (e.g., '1.2.3')."
|
||||
)
|
||||
parser.add_argument(
|
||||
"file",
|
||||
type=str,
|
||||
help="The path to the __init__.py file where the version will be updated."
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
update_version(args.file, args.version)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.);
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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.")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)],
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"""
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 []]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
[
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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.)")
|
||||
|
||||
@@ -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)],
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()}"
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ markers =
|
||||
attribute
|
||||
boolean
|
||||
brick
|
||||
bsdd
|
||||
classification
|
||||
context
|
||||
cost
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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}"'))
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user