Compare commits

..

42 Commits

Author SHA1 Message Date
Dion Moult feba8af4c7 Reconcile changes between master and branch 2024-02-29 17:02:06 +11:00
Dion Moult 0842df0bde Purge all Python test code 2024-02-29 16:08:54 +11:00
Dion Moult 0f1b00769e Chunk all code related to chunking which can now be done in Python 2024-02-29 15:58:55 +11:00
Dion Moult dbd3cb6b29 Forgot to commit test numpy chunking code and workspace tools 2024-02-29 15:24:42 +11:00
Dion Moult 2558518c00 Implement numpy based material chunking thanks to aothms 2024-02-28 22:33:15 +11:00
Dion Moult ce328a049a More test operators 2024-02-28 21:30:59 +11:00
Dion Moult 8d44b0ac4c Test operators to compare C++ chunking vs numpy chunking 2024-02-28 12:45:36 +11:00
Dion Moult e0eea85cfb Basic implementation of clipping planes 2024-02-26 23:10:45 +11:00
Dion Moult 6f56d78e83 Optional culling during navigation to increase FPS
- Only affects non-chunked instances (where verts are higher so the impact will be greater)
 - Things far away or not in view of the camera will be rendered as bounds
2024-02-25 23:11:28 +11:00
Dion Moult 0c4bcb8a12 Hybrid chunking and instancing strategy when loading linked models to balance RAM (instancing) vs speed (chunking) 2024-02-25 23:08:40 +11:00
Dion Moult 67753a0a3c Type properties are now shown and direct linking of IFC (background blend generation) is now possible 2024-02-24 20:31:59 +11:00
Dion Moult d02239902f New workspace tool to query chunked models 2024-02-23 23:05:48 +11:00
Dion Moult d7a7701005 SQLite property extraction IfcPatch recipe for querying chunked models 2024-02-23 23:04:01 +11:00
Dion Moult a97678d30e GUID chunking now implemented when loading either from iterator or H5 2024-02-23 23:03:24 +11:00
Dion Moult 6b82cb4d5d Test operators to try out the various loading combinations 2024-02-22 19:27:56 +11:00
Dion Moult a3006eb028 The iterator can now return chunked triangles instead of per element results 2024-02-22 19:24:30 +11:00
Dion Moult 48bf67f82e Minor fixes to bugs in H5 saving 2024-02-22 19:19:27 +11:00
Dion Moult 6065c0e2fc Naive implementation of loading chunked models in C++ 2024-02-21 17:30:33 +11:00
Dion Moult 847f5ff4c8 First naive attempt at porting Python H5 converter to C++. 2024-02-20 23:13:45 +11:00
Dion Moult 45278467d3 Finish refactor to use BVH_Triangulation instead of BRepExtrema_TriangleSet for more control over how BVHs are constructed sans OCCT shapes, and also remove need to track valid_tris. 2024-02-19 17:35:06 +11:00
Dion Moult 2af1e2196f WIP attempt to use BVH_Triangulation 2024-02-15 21:34:19 +11:00
Dion Moult 5c5f078221 Three experimental minimal operators to load IFCs (xxx), load H5 (zzz), and load chunked H5 (zxc) 2024-02-14 17:44:13 +11:00
Dion Moult a5cf4cd421 Remove 1:N clash functions 2024-02-12 11:57:23 +11:00
Dion Moult 911b7153d7 Implement naive multithreading for N:N clashes. The clash task queue is divided by num_threads equally. 2024-02-12 11:47:20 +11:00
Dion Moult 6cec57816e Implement BVH based many-many clash with return struct. 2024-02-11 22:18:08 +11:00
Dion Moult c90f9c374f Forgot to commit some critical bugfixes. 2024-02-08 16:28:06 +11:00
Dion Moult 6caacd6024 Attempt a manifold check. If it isn't closed, then we use a collision test instead of intersection which is much faster. 2024-02-08 15:54:04 +11:00
Dion Moult 2cb280932c Indirection using vertex indices saves 20% memory but speed is now worse than when we used unordered maps. 2024-02-08 14:36:20 +11:00
Dion Moult e089d4c78a Use vectors instead of unordered_map for 15% speed boost 2024-02-08 11:44:08 +11:00
Dion Moult 155f0abbfe Refactor out BVH-BVH clash into its own function. 2024-02-08 10:35:35 +11:00
Dion Moult 77d9688296 Replace threeyd tri-tri intersect library with physx implementation. 2024-02-08 10:21:56 +11:00
Dion Moult 09efd68e6e Move vendor functions into clash_utils 2024-02-07 23:01:21 +11:00
Dion Moult 0b3e9e7c5a Reuse geom iterator triangulation data, don't use optimal OBBs for speed, verts now use gp_Pnt instead of BVH_Vec3d, and optimise add_triangulated. 2024-02-07 17:49:12 +11:00
Dion Moult b658b74b90 Fix missing clearance AABB check and add check_all clearance bool. Minor cleanup to use vert/normal maps. 2024-02-07 09:28:19 +11:00
Dion Moult 47dd8ec4dd Use map for verts and normals, basic implementation of piercing checks. 2024-02-06 22:21:33 +11:00
Dion Moult 9ba1596e30 Implement early return bool for intersection tests. 2024-02-05 18:47:08 +11:00
Dion Moult 930020bf05 Use NVIDIA's ray-tri intersection implementation under BSD-3 license. 2024-02-04 22:51:51 +11:00
Dion Moult 1fe69e5f19 Simple implementation of clearance clash. 2024-02-04 16:02:45 +11:00
Dion Moult 988c8f16f3 Implement collision clash type (pure tri intersection regardless of protrusion)
This should be equivalent to select(element) but faster and with an allow_touching toggle.
2024-02-04 09:01:35 +11:00
Dion Moult 080b628df5 Optimise by removing unnecessary (very slow) triangle shrinking, more optimal OBBs, check OBB first for points_in_b prior to doing raycast (much faster now), and return the surface point of the protrusion too for convenience. 2024-02-03 16:14:26 +11:00
Dion Moult d0333ac7cd Penetration distances now are influenced by the normal for better results.
Add cache for points that have already been checked.

Separate out new add_triangulated function with new kwarg add_element(should_triangulate=True) so that you can still have the old geom tree using aabb only if you want, also because add() is used in boolean_utils. Tolerance is now a kwarg.

I misunderstood the OBB dimension and should *2 not /2 which now makes it much slower since there's more triangles to check.
2024-02-02 23:35:06 +11:00
Dion Moult e95e16c102 First attempt at implementation of intersection clash checks.
This considers only protruding clashes. It does not consider touching nor encroaching clashes and does not consider protrusion direction in the protrusion distance.
2024-02-01 18:30:20 +11:00
1125 changed files with 33650 additions and 69966 deletions
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
- run: echo ok go
build:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
needs: activate
steps:
- uses: actions/checkout@v2
@@ -80,7 +80,7 @@ jobs:
path: build/assets/Ifc*
deliver:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
needs: build
name: Docker Build, Tag, Push
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pyver: [py39, py310, py311, py312]
pyver: [py39, py310, py311]
config:
- {
name: "Windows Build",
-48
View File
@@ -1,48 +0,0 @@
name: ci-bsdd-pypi
on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# * * * * *
- cron: "0 0 18 * *"
workflow_dispatch:
env:
major: 0
minor: 0
name: ifcopenshell
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
run: |
pip install build
cd src/bsdd &&
make dist
- name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/bsdd/dist
-48
View File
@@ -1,48 +0,0 @@
name: ci-ifcclash-pypi
on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# * * * * *
- cron: "0 0 18 * *"
workflow_dispatch:
env:
major: 0
minor: 0
name: ifcopenshell
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
run: |
pip install build
cd src/ifcclash &&
make dist
- name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/ifcclash/dist
-48
View File
@@ -1,48 +0,0 @@
name: ci-ifccsv-pypi
on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# * * * * *
- cron: "0 0 18 * *"
workflow_dispatch:
env:
major: 0
minor: 0
name: ifcopenshell
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
run: |
pip install build
cd src/ifccsv &&
make dist
- name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/ifccsv/dist
+2 -3
View File
@@ -1,7 +1,6 @@
name: ci-ifcopenshell-pypi
on:
workflow_dispatch:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
@@ -36,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pyver: [py39, py310, py311, py312]
pyver: [py36, py37, py38, py39, py310, py311]
config:
- {
name: "Windows Build",
@@ -58,7 +57,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
- run: echo ${{ env.DATE }}
- name: Get current date
-48
View File
@@ -1,48 +0,0 @@
name: ci-ifcpatch-pypi
on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# * * * * *
- cron: "0 0 18 * *"
workflow_dispatch:
env:
major: 0
minor: 0
name: ifcopenshell
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
- name: Compile
run: |
pip install build
cd src/ifcpatch &&
make dist
- name: Publish a Python distribution to PyPI
uses: ortega2247/pypi-upload-action@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/ifcpatch/dist
@@ -1,53 +0,0 @@
name: Publish-ifcsverchok
on:
push:
paths:
- '.github/workflows/ci-ifcsverchok-build.yml'
- 'src/ifcsverchok/*'
branches:
- v0.7.0
env:
major: 0
minor: 0
name: ifcsverchok
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
name: ifcsverchok
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
- run: echo ${{ env.DATE }}
- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%y%m%d')"
- name: Compile
run: |
cd src/ifcsverchok
make dist
- name: Upload Zip file to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: src/ifcsverchok/dist/ifcsverchok-${{steps.date.outputs.date}}.zip
asset_name: ifcsverchok-${{steps.date.outputs.date}}.zip
tag: "ifcsverchok-${{steps.date.outputs.date}}"
overwrite: true
body: "ifcsverchok build for ${{steps.date.outputs.date}}"
+10 -1
View File
@@ -9,6 +9,9 @@ on:
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# * * * * *
- cron: "0 0 18 * *"
push:
paths:
- '.github/workflows/ci-ifctester-pypi.yml'
workflow_dispatch:
env:
@@ -27,6 +30,7 @@ jobs:
build:
needs: activate
name: ${{ matrix.config.name }}-${{ matrix.pyver }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -34,7 +38,12 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
- run: echo ${{ env.DATE }}
- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%y%m%d')"
- name: Compile
run: |
pip install build
+2 -2
View File
@@ -46,7 +46,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil
pip install src/bcf --no-deps
pip install https://github.com/Andrej730/aud/archive/refs/heads/master-reduced-size.zip
@@ -90,7 +90,7 @@ jobs:
-DPYTHON_INCLUDE_DIR:PATH=${{ env.pythonLocation }}/include/python3.11 \
-DPYTHON_LIBRARY:FILEPATH=${{ env.pythonLocation }}/lib/libpython3.11.so \
-DCOLLADA_SUPPORT=Off \
"-DSCHEMA_VERSIONS=2x3;4;4x3;4x3_add1;4x3_add2" \
"-DSCHEMA_VERSIONS=2x3;4;4x3_add1" \
-DGLTF_SUPPORT=On \
-DJSON_INCLUDE_DIR=/usr/include \
-DCGAL_INCLUDE_DIR=/usr/include \
-6
View File
@@ -76,14 +76,8 @@ src/ifcopenshell-python/test/build
# blenderbim libs
src/blenderbim/blenderbim/libs
# blenderbim i18n
src/blenderbim/blenderbim/translations.py
# blenderbim test temp files
src/blenderbim/test/files/temp
src/blenderbim/test/files/basic.ifc.cache.blend
src/blenderbim/test/files/basic.ifc.cache.sqlite
src/blenderbim/drawings
src/blenderbim/layouts
+7 -7
View File
@@ -18,14 +18,14 @@ and many other libraries, CLI apps, and more. Support is also provided for auxil
For more information, see:
* [IfcOpenShell Website](http://ifcopenshell.org)
* [IfcOpenShell Documentation](https://docs.ifcopenshell.org)
* [IfcOpenShell C++ Installation](https://docs.ifcopenshell.org/ifcopenshell/installation.html)
* [IfcOpenShell Python Installation](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html)
* [IfcOpenShell Python Hello World Tutorial](https://docs.ifcopenshell.org/ifcopenshell-python/hello_world.html)
* [IfcOpenShell Documentation](http://blenderbim.org/docs-python)
* [IfcOpenShell C++ Installation](https://blenderbim.org/docs-python/ifcopenshell/installation.html)
* [IfcOpenShell Python Installation](https://blenderbim.org/docs-python/ifcopenshell-python/installation.html)
* [IfcOpenShell Python Hello World Tutorial](https://blenderbim.org/docs-python/ifcopenshell-python/hello_world.html)
* [BlenderBIM Add-on Website](https://blenderbim.org)
* [BlenderBIM Add-on Documentation](https://docs.blenderbim.org/index.html)
* [Add-on Installation](https://docs.blenderbim.org/users/installation.html)
* [Exploring an IFC model](https://docs.blenderbim.org/users/exploring_an_ifc_model.html)
* [BlenderBIM Add-on Documentation](http://blenderbim.org/docs)
* [Add-on Installation](https://blenderbim.org/docs/users/installation.html)
* [Exploring an IFC model](https://blenderbim.org/docs/users/exploring_an_ifc_model.html)
| Service | Status |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
+1 -1
View File
@@ -12,7 +12,7 @@ RUN apt-get -y update && apt-get -y install unzip curl
# Install AWS Lambda runtime interface client
RUN pip install --target ${FUNCTION_DIR} awslambdaric
# Set the IfcOpenShell build version (check available builds at: https://docs.ifcopenshell.org/ifcopenshell-python/installation.html)
# Set the IfcOpenShell build version (check available builds at: https://blenderbim.org/docs-python/ifcopenshell-python/installation.html)
ARG IFC_OPENSHELL_BUILD="39-v0.7.0-476ab50"
# Download and extract IfcOpenShell
+1 -1
View File
@@ -15,7 +15,7 @@
<licenseUrl>https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.7.0/COPYING</licenseUrl>
<requireLicenseAcceptance>true</requireLicenseAcceptance>
<projectSourceUrl>https://github.com/IfcOpenShell/IfcOpenShell</projectSourceUrl>
<docsUrl>https://docs.blenderbim.org/</docsUrl>
<docsUrl>https://blenderbim.org/docs/</docsUrl>
<!--<mailingListUrl></mailingListUrl>-->
<bugTrackerUrl>https://github.com/IfcOpenShell/IfcOpenShell/issues</bugTrackerUrl>
<tags>blender bim blenderbim ifc python opensource foss</tags>
+18 -51
View File
@@ -17,7 +17,7 @@
# #
################################################################################
cmake_minimum_required(VERSION 3.5)
cmake_minimum_required(VERSION 3.1.3)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON) # not necessary, but encouraged
@@ -27,18 +27,13 @@ cmake_policy(SET CMP0048 NEW)
cmake_policy(SET CMP0074 NEW)
cmake_policy(SET CMP0078 OLD)
cmake_policy(SET CMP0086 NEW)
if (POLICY CMP0144)
cmake_policy(SET CMP0144 NEW) # find_package() uses upper-case <PACKAGENAME>_ROOT variables.
endif()
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Release")
endif()
# use extra version to make pre-release using eg semver
if(NOT DEFINED EXTRA_VERSION)
set(EXTRA_VERSION "-alpha.3")
endif()
set(EXTRA_VERSION "-alpha.3")
option(MINIMAL_BUILD "The build is to make a minimal version of IFC converter from OCCT into IFC." OFF)
option(WASM_BUILD "Build a WebAssembly binary." OFF)
@@ -191,7 +186,7 @@ if(GLTF_SUPPORT AND BUILD_CONVERT)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_GLTF)
endif()
# Add USD support to serializers
# Add USD support to serializers
if(USD_SUPPORT)
UNIFY_ENVVARS_AND_CACHE(USD_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(USD_LIBRARY_DIR)
@@ -214,13 +209,13 @@ if(USD_SUPPORT)
endif()
set(USD_LIBRARIES
usd_usd
usd_usd
usd_usdGeom
usd_usdShade
usd_usdLux
usd_vt
usd_sdf
usd_tf
usd_usdShade
usd_usdLux
usd_vt
usd_sdf
usd_tf
usd_gf
)
@@ -378,34 +373,13 @@ if(BUILD_IFCGEOM)
message(STATUS "Looking for Open CASCADE include files in: ${OCC_INCLUDE_DIR}")
endif()
if(OCC_INCLUDE_DIR)
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MAJOR
REGEX "#define OCC_VERSION_MAJOR.*"
)
string(REGEX MATCH "[0-9]+" OCC_MAJOR ${OCC_MAJOR})
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MINOR
REGEX "#define OCC_VERSION_MINOR.*"
)
string(REGEX MATCH "[0-9]+" OCC_MINOR ${OCC_MINOR})
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MAINT
REGEX "#define OCC_VERSION_MAINTENANCE.*"
)
string(REGEX MATCH "[0-9]+" OCC_MAINT ${OCC_MAINT})
set(OCC_VERSION_STRING "${OCC_MAJOR}.${OCC_MINOR}.${OCC_MAINT}")
endif(OCC_INCLUDE_DIR)
set(OPENCASCADE_LIBRARY_NAMES
TKernel TKMath TKBRep TKGeomBase TKGeomAlgo TKG3d TKG2d TKShHealing TKTopAlgo TKMesh TKPrim TKBool TKBO
TKFillet TKXSBase TKOffset TKHLR
TKFillet TKSTEP TKSTEPBase TKSTEPAttr TKXSBase TKSTEP209 TKIGES TKOffset TKHLR
# @todo investigate the exact conditions when this is necessary
TKBin
)
if(OCC_VERSION_STRING VERSION_LESS 7.8.0)
list(APPEND OPENCASCADE_LIBRARY_NAMES TKIGES TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP)
else(OCC_VERSION_STRING VERSION_LESS 7.8.0)
list(APPEND OPENCASCADE_LIBRARY_NAMES TKDESTEP TKDEIGES)
endif(OCC_VERSION_STRING VERSION_LESS 7.8.0)
if("${OCC_LIBRARY_DIR}" STREQUAL "")
find_library(OCC_LIBRARY TKernel
@@ -461,7 +435,7 @@ if(BUILD_IFCGEOM)
if(OCCT_STATIC)
find_package(Threads)
if(WASM_BUILD)
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
else()
@@ -473,7 +447,7 @@ if(BUILD_IFCGEOM)
set(OPENCASCADE_LIBRARIES -Wl,--start-group ${OPENCASCADE_LIBRARIES} -Wl,--end-group ${CMAKE_THREAD_LIBS_INIT})
endif()
endif()
if (NOT APPLE AND NOT WIN32)
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} "rt")
endif()
@@ -682,10 +656,6 @@ if(ENABLE_BUILD_OPTIMIZATIONS)
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELEASE} -O3")
endif()
else()
if(MSVC)
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} /Od")
endif()
endif()
if(MSVC)
@@ -785,8 +755,6 @@ if(NOT SCHEMA_VERSIONS)
endif()
endif()
message(STATUS "IFC SCHEMA_VERSIONS that will be used for the build: ${SCHEMA_VERSIONS}.")
foreach(schema ${SCHEMA_VERSIONS})
add_definitions(-DHAS_SCHEMA_${schema})
endforeach()
@@ -869,7 +837,7 @@ if (BUILD_IFCGEOM)
if (WASM_BUILD)
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
else()
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
endif()
endif()
@@ -932,7 +900,7 @@ if(BUILD_IFCGEOM)
add_library(IfcGeom_ifc${schema} STATIC ${IFCGEOM_FILES})
set_target_properties(IfcGeom_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
if (NOT WASM_BUILD)
target_link_libraries(IfcGeom_ifc${schema} IfcParse ${OPENCASCADE_LIBRARIES})
target_link_libraries(IfcGeom_ifc${schema} IfcParse ${OPENCASCADE_LIBRARIES})
endif()
endforeach()
@@ -968,7 +936,7 @@ if(BUILD_CONVERT OR BUILD_IFCPYTHON)
foreach(schema ${SCHEMA_VERSIONS})
add_library(Serializers_ifc${schema} STATIC ${SERIALIZERS_S_FILES})
set_target_properties(Serializers_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
if (WASM_BUILD)
target_link_libraries(Serializers_ifc${schema} ${HDF5_LIBRARIES})
else()
@@ -1149,10 +1117,9 @@ endif()
# Packaging
list(APPEND CPACK_SOURCE_IGNORE_FILES
"/\\\\.git"
"/build/"
.git
.gitignore
)
set(CPACK_SOURCE_INSTALLED_DIRECTORIES "${CMAKE_SOURCE_DIR}/..;/")
set(CPACK_PACKAGE_NAME "${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}")
set(CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}${EXTRA_VERSION}")
SET(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}-${CMAKE_SYSTEM_NAME}")
@@ -1175,7 +1142,7 @@ endforeach(COMPONENT)
set(CPACK_DEBIAN_PACKAGE_NAME "${PROJECT_NAME}")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${CPACK_PACKAGE_CONTACT}")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "python3, libxml2, libocct-foundation-dev, libocct-modeling-algorithms-dev, libocct-modeling-data-dev, libocct-ocaf-dev, libocct-visualization-dev, libocct-data-exchange-dev, libhdf5-serial-dev, libpython3-dev, python3-pytest ${BOOST_DEPS}")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "python3, libxml2, libocct-foundation-dev, libocct-modeling-algorithms-dev, libocct-modeling-data-dev, libocct-ocaf-dev, libocct-visualization-dev, libocct-data-exchange-dev, libhdf5-serial-dev, libpython3.8, python3-pytest ${BOOST_DEPS}")
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION_SUMMARY "${CPACK_PACKAGE_DESCRIPTION_SUMMARY}")
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "${CPACK_PACKAGE_DESCRIPTION}")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
+3 -9
View File
@@ -76,7 +76,7 @@ PROJECT_NAME = "IfcOpenShell"
USE_CURRENT_PYTHON_VERSION = os.getenv("USE_CURRENT_PYTHON_VERSION")
ADD_COMMIT_SHA = os.getenv("ADD_COMMIT_SHA")
PYTHON_VERSIONS = ["3.9.11", "3.10.3", "3.11.8", "3.12.1"]
PYTHON_VERSIONS = ["3.6.14", "3.7.13", "3.8.13", "3.9.11", "3.10.3", "3.11.7", "3.12.1"]
JSON_VERSION = "v3.6.1"
OCE_VERSION = "0.18.3"
OCCT_VERSION = "7.5.3"
@@ -254,11 +254,6 @@ if not os.path.exists(LOG_FILE):
open(LOG_FILE, "w").close()
logger.info(f"using command log file '{LOG_FILE}'")
# Causing havoc in python 3.11 build
try:
del os.environ['__PYVENV_LAUNCHER__']
except: pass
def run(cmds, cwd=None, can_fail=False):
"""
@@ -289,9 +284,8 @@ if platform.system() == "Darwin":
# Apparently not supported
PYTHON_VERSIONS = [pv for pv in PYTHON_VERSIONS if tuple(map(int, pv.split("."))) >= (3, 7)]
if run(["sw_vers", "-productVersion"]) < "10.16":
# This is now solved with the '__PYVENV_LAUNCHER__' hack
# PYTHON_VERSIONS = [pv for pv in PYTHON_VERSIONS if tuple(map(int, pv.split("."))) < (3, 11)]
pass
# Apparently not supported
PYTHON_VERSIONS = [pv for pv in PYTHON_VERSIONS if tuple(map(int, pv.split("."))) < (3, 11)]
BOOST_VERSION_UNDERSCORE = BOOST_VERSION.replace(".", "_")
+1 -1
View File
@@ -13,7 +13,7 @@ readme = "README.md"
requires-python = ">=3.8"
keywords = ["IFC", "BCF", "BIM"]
dependencies = [
"xsdata>=24.4",
"xsdata",
"numpy",
"ifcopenshell",
]
+5 -7
View File
@@ -10,14 +10,14 @@ from xsdata.formats.dataclass.serializers.config import SerializerConfig
def build_xml_parser(context: Optional[XmlContext] = None) -> XmlParser:
"""Return a parser for an XML file."""
parser = XmlParser(context=context or XmlContext())
parser.register_namespace(ns_map=parser.ns_map, prefix="xs", uri="http://www.w3.org/2001/XMLSchema")
parser.register_namespace("xs", "http://www.w3.org/2001/XMLSchema")
return parser
def build_serializer(context: Optional[XmlContext] = None) -> XmlSerializer:
"""Return a serializer for an XML file."""
return XmlSerializer(
config=SerializerConfig(indent=" "),
config=SerializerConfig(pretty_print=True),
context=context or XmlContext(),
)
@@ -36,9 +36,8 @@ class AbstractXmlParserSerializer(Protocol):
xml: The XML file as bytes.
clazz: The class to parse to.
"""
...
def serialize(self, obj: object, ns_map: Optional[dict[str, str]] = None) -> str:
def serialize(self, obj: T, ns_map: Optional[dict[str, str]] = None) -> str:
"""
Serialize an object to XML.
@@ -49,7 +48,6 @@ class AbstractXmlParserSerializer(Protocol):
Returns:
The XML as string.
"""
...
class XmlParserSerializer:
@@ -70,7 +68,7 @@ class XmlParserSerializer:
"""
return self.parser.from_bytes(xml, clazz)
def serialize(self, obj: object, ns_map: Optional[dict[Optional[str], str]] = None) -> str:
def serialize(self, obj: T, ns_map: Optional[dict[str, str]] = None) -> str:
"""
Serialize an object to XML.
@@ -81,5 +79,5 @@ class XmlParserSerializer:
Returns:
The XML as string.
"""
ns_map = ns_map or self.parser.ns_map or {"xs": "http://www.w3.org/2001/XMLSchema"}
ns_map = ns_map or {"xs": "http://www.w3.org/2001/XMLSchema"}
return self.serializer.render(obj, ns_map)
+470 -47
View File
@@ -55,27 +55,143 @@ PYLIBDIR:=python3.11
PYNUMBER:=311
PYPI_VERSION:=3.11
endif
ifeq ($(PYVERSION), py312)
PYLIBDIR:=python3.12
PYNUMBER:=312
PYPI_VERSION:=3.12
endif
ifeq ($(PLATFORM), linux)
PYPI_PLATFORM:=--platform manylinux_2_17_x86_64
LXML_ARCH:=manylinux_2_28_x86_64
SHAPELY_ARCH:=manylinux_2_17_x86_64.manylinux2014_x86_64
PILLOW_ARCH:=manylinux_2_28_x86_64
ifeq ($(PYVERSION), py39)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/linux-64/hpp-fcl-2.4.0-py39h359ec1e_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/linux-64/eigenpy-3.2.0-py39hb1babde_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/linux-64/boost-1.78.0-py39h7c9e3ff_4.tar.bz2
endif
ifeq ($(PYVERSION), py310)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/linux-64/hpp-fcl-2.4.0-py310hd002f03_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/linux-64/eigenpy-3.2.0-py310hcfe0b05_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/linux-64/boost-1.78.0-py310hc4a4660_4.tar.bz2
endif
ifeq ($(PYVERSION), py311)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/linux-64/hpp-fcl-2.4.0-py311h04f086a_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/linux-64/eigenpy-3.2.0-py311h995a085_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/linux-64/boost-1.78.0-py311h59ea3da_3.tar.bz2
endif
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/linux-64/assimp-5.0.1-hedfc422_6.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/linux-64/qhull-2020.2-h4bd325d_2.tar.bz2
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.8/download/linux-64/octomap-1.9.8-h924138e_0.tar.bz2
BOOSTCPP_URL:=https://anaconda.org/conda-forge/boost-cpp/1.78.0/download/linux-64/boost-cpp-1.78.0-h6582d0a_3.conda
ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/linux-64/zlib-1.2.11-h516909a_1010.tar.bz2
endif
ifeq ($(PLATFORM), macos)
PYPI_PLATFORM:=--platform macosx_10_10_x86_64
PYPI_PLATFORM:=--platform macosx_10_9_x86_64
SHAPELY_ARCH:=macosx_10_9_x86_64
PILLOW_ARCH:=macosx_10_10_x86_64
ifeq ($(PYVERSION), py39)
LXML_ARCH:=macosx_11_0_x86_64
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/osx-64/hpp-fcl-2.4.0-py39h26350b9_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/osx-64/eigenpy-3.2.0-py39h3323527_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-64/boost-1.78.0-py39h953a6b8_4.tar.bz2
endif
ifeq ($(PYVERSION), py310)
LXML_ARCH:=macosx_11_0_x86_64
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/osx-64/hpp-fcl-2.4.0-py310ha216469_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/osx-64/eigenpy-3.2.0-py310h33a8c9b_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-64/boost-1.78.0-py310h3e792ce_4.tar.bz2
endif
ifeq ($(PYVERSION), py311)
LXML_ARCH:=macosx_11_0_universal2
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/osx-64/hpp-fcl-2.4.0-py311hebdd1a2_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/osx-64/eigenpy-3.2.0-py311h62e90b6_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-64/boost-1.78.0-py311h349b758_4.tar.bz2
endif
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/osx-64/assimp-5.0.1-h1224e73_6.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/osx-64/qhull-2020.2-h940c156_2.tar.bz2
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.8/download/osx-64/octomap-1.9.8-hb8565cd_0.tar.bz2
BOOSTCPP_URL:=https://anaconda.org/conda-forge/boost-cpp/1.78.0/download/osx-64/boost-cpp-1.78.0-hf5ba120_3.conda
ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/osx-64/zlib-1.2.11-h7795811_1010.tar.bz2
endif
ifeq ($(PLATFORM), macosm1)
PYPI_PLATFORM:=--platform macosx_11_0_arm64
LXML_ARCH:=macosx_11_0_universal2
SHAPELY_ARCH:=macosx_11_0_arm64
PILLOW_ARCH:=macosx_11_0_arm64
ifeq ($(PYVERSION), py39)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/osx-arm64/hpp-fcl-2.4.0-py39h82b2ff2_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/osx-arm64/eigenpy-3.2.0-py39h3a9e884_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py39h99de9ae_4.tar.bz2
endif
ifeq ($(PYVERSION), py310)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/osx-arm64/hpp-fcl-2.4.0-py310hdb6c3eb_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/osx-arm64/eigenpy-3.2.0-py310h96e9823_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py310h629746b_4.tar.bz2
endif
ifeq ($(PYVERSION), py311)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/osx-arm64/hpp-fcl-2.4.0-py311hc0c29b9_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/osx-arm64/eigenpy-3.2.0-py311h24fa84d_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py311h5c64717_4.tar.bz2
endif
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/osx-arm64/assimp-5.0.1-h0f81e16_7.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/osx-arm64/qhull-2020.2-hc021e02_2.tar.bz2
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.8/download/osx-arm64/octomap-1.9.8-hffc8910_0.tar.bz2
BOOSTCPP_URL:=https://anaconda.org/conda-forge/boost-cpp/1.78.0/download/osx-arm64/boost-cpp-1.78.0-h9ed8d21_3.conda
ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/osx-arm64/zlib-1.2.11-h90dfc92_1014.tar.bz2
endif
ifeq ($(PLATFORM), win)
PYPI_PLATFORM:=--platform win_amd64
LXML_ARCH:=win_amd64
SHAPELY_ARCH:=win_amd64
PILLOW_ARCH:=win_amd64
ifeq ($(PYVERSION), py39)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/win-64/hpp-fcl-2.4.0-py39hf3ebc58_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/win-64/eigenpy-3.2.0-py39ha1f6a47_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/win-64/boost-1.78.0-py39hea4d8d1_4.tar.bz2
endif
ifeq ($(PYVERSION), py310)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/win-64/hpp-fcl-2.4.0-py310h1bf3f0a_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/win-64/eigenpy-3.2.0-py310h7fb871d_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/win-64/boost-1.78.0-py310h220cb41_4.tar.bz2
endif
ifeq ($(PYVERSION), py311)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.4.0/download/win-64/hpp-fcl-2.4.0-py311hd263b2c_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.2.0/download/win-64/eigenpy-3.2.0-py311hd354db1_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/win-64/boost-1.78.0-py311h12feb9d_3.tar.bz2
endif
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.2.5/download/win-64/assimp-5.2.5-h4dcb625_0.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/win-64/qhull-2020.2-h70d2c02_2.tar.bz2
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.8/download/win-64/octomap-1.9.8-h91493d7_0.tar.bz2
BOOSTCPP_URL:=https://anaconda.org/conda-forge/boost-cpp/1.78.0/download/win-64/boost-cpp-1.78.0-h9f4b32c_3.conda
ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/win-64/zlib-1.2.11-h62dcd97_1010.tar.bz2
endif
LXML_NAME:=lxml
LXML_VER:=4.9.4
SHAPELY_NAME:=shapely
SHAPELY_VER:=2.0.2
PILLOW_NAME:=pillow
PILLOW_PKG_NAME:=Pillow
PILLOW_VER:=9.5.0
SHAPELY_URL:=https://files.pythonhosted.org/packages/cp$(PYNUMBER)/s/$(SHAPELY_NAME)/$(SHAPELY_NAME)-$(SHAPELY_VER)-cp$(PYNUMBER)-cp$(PYNUMBER)-$(SHAPELY_ARCH).whl
PILLOW_URL:=https://files.pythonhosted.org/packages/cp$(PYNUMBER)/p/$(PILLOW_NAME)/$(PILLOW_PKG_NAME)-$(PILLOW_VER)-cp$(PYNUMBER)-cp$(PYNUMBER)-$(PILLOW_ARCH).whl
ifeq ($(PLATFORM), macosm1)
ifeq ($(PYVERSION), py39)
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py39h0520ce3_1.tar.bz2
endif
ifeq ($(PYVERSION), py310)
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py310h02f21da_0.tar.bz2
endif
ifeq ($(PYVERSION), py311)
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py311h246f609_1.tar.bz2
endif
else
LXML_URL:=https://files.pythonhosted.org/packages/cp$(PYNUMBER)/l/$(LXML_NAME)/$(LXML_NAME)-$(LXML_VER)-cp$(PYNUMBER)-cp$(PYNUMBER)-$(LXML_ARCH).whl
endif
$(info $$LXML_URL is [${LXML_URL}])
$(info $$SHAPELY_URL is [${SHAPELY_URL}])
$(info $$PILLOW_URL is [${PILLOW_URL}])
.PHONY: bump
bump:
@@ -95,7 +211,7 @@ endif
cp -r blenderbim/* dist/blenderbim/
# Provides IfcOpenShell Python functionality
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-c7830e9-$(PLATFORM)64.zip
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-6c9e130-$(PLATFORM)64.zip
cd dist/working && unzip ifcopenshell-python*
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
@@ -109,10 +225,8 @@ endif
cd dist/working && wget https://github.com/IfcOpenShell/IfcOpenShell/archive/v0.7.0.zip
cd dist/working && unzip v0.7.0.zip
# IfcOpenBot sometimes lags behind, so we hotfix the Python utilities
rm -rf dist/blenderbim/libs/site/packages/ifcopenshell/util/
rm -rf dist/blenderbim/libs/site/packages/ifcopenshell/api/
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/util dist/blenderbim/libs/site/packages/ifcopenshell/
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/api dist/blenderbim/libs/site/packages/ifcopenshell/
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/util/* dist/blenderbim/libs/site/packages/ifcopenshell/util/
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/api/* dist/blenderbim/libs/site/packages/ifcopenshell/api/
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/*.py dist/blenderbim/libs/site/packages/ifcopenshell/
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/express/rule_executor.py dist/blenderbim/libs/site/packages/ifcopenshell/express/
# Provides bcf functionality
@@ -150,6 +264,49 @@ endif
cp -r dist/working/site-packages/git dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Provides Mustache templating in construction documentation
# TODO: remove this dependency, it seems overkill and we seem to get by with str replaces
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/3f/e7/8750ba6c6101d6aa5ceeb20c013adf2c6f3554a12c71d75654b468404bfa/pystache-0.6.0.tar.gz
cd dist/working && tar -xzvf pystache*
cd dist/working/pystache-0.6.0/ && $(PYTHON) setup.py build && cp -r build/lib/pystache ../../blenderbim/libs/site/packages/
rm -rf dist/working
# Provides SVG export in construction documentation
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/79/e8/7eb2ba188eda14a4b47e33b51f3b4978985f4116655c699bcd18c79279b5/svgwrite-1.3.1.zip
cd dist/working && unzip svgwrite*
cp -r dist/working/svgwrite-1.3.1/svgwrite dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Provides fuzzy date parsing for construction sequencing
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/be/ed/5bbc91f03fa4c839c4c7360375da77f9659af5f7086b7a7bdda65771c8e0/python-dateutil-2.8.1.tar.gz
cd dist/working && tar -xzvf python-dateutil*
cp -r dist/working/python-dateutil-2.8.1/dateutil dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Provides duration parsing for construction sequencing
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/b1/80/fb8c13a4cd38eb5021dc3741a9e588e4d1de88d895c1910c6fc8a08b7a70/isodate-0.6.0.tar.gz
cd dist/working && tar -xzvf isodate*
cp -r dist/working/isodate-0.6.0/src/isodate dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Provides networkx graph analysis for project dependency calculations
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/b0/21/adfbf6168631e28577e4af9eb9f26d75fe72b2bb1d33762a5f2c425e6c2a/networkx-2.5.1.tar.gz
cd dist/working && tar -xzvf networkx*
cp -r dist/working/networkx-2.5.1/networkx dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by networkx
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/4f/51/15a4f6b8154d292e130e5e566c730d8ec6c9802563d58760666f1818ba58/decorator-5.0.9.tar.gz
cd dist/working && tar -xzvf decorator*
cp -r dist/working/decorator-5.0.9/src/decorator.py dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Provides audio playback for costing
mkdir -p dist/working
git clone https://github.com/Andrej730/aud.git --branch master-reduced-size --depth 1 dist/working/aud
@@ -164,13 +321,112 @@ endif
cp dist/working/jsgantt* dist/blenderbim/bim/data/gantt/
rm -rf dist/working
# Required by IFCDiff
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/0f/ca/caead2949fbb824c7142e3774fa841aa853bb4d4331b440da8c8514dfc6f/deepdiff-5.8.1.tar.gz
cd dist/working && tar -xzvf deepdiff*
cp -r dist/working/deepdiff-5.8.1/deepdiff dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by deepdiff
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/00/55/ce2cbc6d64034b30cad81a29ba61bdba456f190f5e83c09831304bf68d6b/jsonpickle-1.2.tar.gz
cd dist/working && tar -xzvf jsonpickle*
cp -r dist/working/jsonpickle-1.2/jsonpickle dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by deepdiff
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/a3/b7/d4d69641cbe707a45c23b190f2d717466ba5accc4c70b5f7a8a450387895/ordered-set-3.1.1.tar.gz
cd dist/working && tar -xzvf ordered-set*
cp -r dist/working/ordered-set-3.1.1/ordered_set.py dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by lark
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/00/32/8076fa13e832bb4dcff379f18f228e5a53412be0631808b9ca2610c0f566/pyparsing-2.4.5.tar.gz
cd dist/working && tar -xzvf pyparsing*
cp -r dist/working/pyparsing-2.4.5/pyparsing.py dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by bcf
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/5b/ef/f97c3e1a7efa00e989a793fe15297214fc95ad7d9e3810586bd08ce9f0f3/xmlschema-2.0.2.tar.gz
cd dist/working && tar -xzvf xmlschema*
cp -r dist/working/xmlschema-2.0.2/xmlschema dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by bcf
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/ad/c7/17c9d16320d8e2cfdb27d2fd298b5e8f7a3f211025b0c1bc7a39a85bd690/xsdata-22.11.tar.gz
cd dist/working && tar -xzvf xsdata*
cp -r dist/working/xsdata-22.11/xsdata dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by bcf
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/11/bc/5afb61dd5d863e5cf77cd952445c50c17e65953405986f19e97e4389692a/elementpath-3.0.2.tar.gz
cd dist/working && tar -xzvf elementpath*
cp -r dist/working/elementpath-3.0.2/elementpath dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by bcf
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/21/9f/b251f7f8a76dec1d6651be194dfba8fb8d7781d10ab3987190de8391d08e/six-1.14.0.tar.gz
cd dist/working && tar -xzvf six*
cp -r dist/working/six-1.14.0/six.py dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by IFCCSV and ifcopenshell.util.selector
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/18/4d/8d522136c37d9e1ea74062b41b8d5e1318ebf45063ae46ce72ed60af223b/lark-parser-0.8.5.tar.gz
cd dist/working && tar -xzvf lark-parser*
cp -r dist/working/lark-parser-0.8.5/lark dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by IFC4D
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/b0/bb/9c4dddd3ca173cb56241cfb2eddfae24690dc676d357ac4cab17d0a36d9d/PyP6Xer-1.13.0.tar.gz
cd dist/working && tar -xzvf PyP6Xer*
cp -r dist/working/PyP6Xer-1.13.0/xerparser dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by QTOCalculator
mkdir dist/working
cd dist/working && wget $(SHAPELY_URL)
cd dist/working && cp *.whl shapely.zip && unzip shapely.zip
cp -r dist/working/shapely dist/blenderbim/libs/site/packages/
ifeq ($(PLATFORM), win)
cp -r dist/working/shapely.libs dist/blenderbim/libs/site/packages/
endif
ifeq ($(PLATFORM), linux)
cp -r dist/working/shapely.libs dist/blenderbim/libs/site/packages/
endif
rm -rf dist/working
# Required by the BIM tool thumbnail generator
mkdir dist/working
cd dist/working && wget $(PILLOW_URL)
cd dist/working && cp *.whl pillow.zip && unzip pillow.zip
cp -r dist/working/PIL dist/blenderbim/libs/site/packages/
ifeq ($(PLATFORM), linux)
cp -r dist/working/Pillow.libs dist/blenderbim/libs/site/packages/
endif
rm -rf dist/working
# Required by xerparser and IFC4D
# TODO: remove this dependency. It's only used to show a progress bar.
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/9f/7b/76c4e5ef1a1b528fcaada4133f972e77d33c252831676cf414119ca6093d/tqdm-4.50.0.tar.gz
cd dist/working && tar -xzvf tqdm*
cp -r dist/working/tqdm-4.50.0/tqdm dist/blenderbim/libs/site/packages/
rm -rf dist/working
mkdir dist/working
cd dist/working && $(PYTHON) -m venv env
cd dist/working && mkdir site-packages
# Required by IfcCSV
cd dist/working && . env/bin/activate && $(PIP) install pandas $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --target=./site-packages
# Pandas ships with very heavy tests
cd dist/working/site-packages/pandas && rm -rf tests
cd dist/working && . env/bin/activate && $(PIP) install openpyxl --target=./site-packages
cd dist/working && . env/bin/activate && $(PIP) install odfpy --target=./site-packages
# Required by IFCCityJSON
@@ -178,39 +434,161 @@ endif
# Provides express rule validation for ifcopenshell.validate
cd dist/working && . env/bin/activate && $(PIP) install pytest --target=./site-packages
# Provides Brickschema functionality
cd dist/working && . env/bin/activate && $(PIP) install "brickschema[persistence]==0.7.6a2" --target=./site-packages
# Required for SVG to DXF conversion
cd dist/working && . env/bin/activate && $(PIP) install ezdxf --target=./site-packages
# Required by bcf
cd dist/working && . env/bin/activate && $(PIP) install xsdata --target=./site-packages
cd dist/working && . env/bin/activate && $(PIP) install xmlschema --target=./site-packages
cd dist/working && . env/bin/activate && $(PIP) install elementpath --target=./site-packages
cd dist/working && . env/bin/activate && $(PIP) install six --target=./site-packages
# Required by drawing module
cd dist/working && . env/bin/activate && $(PIP) install lxml $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --target=./site-packages
# Required by qto and drawing module
cd dist/working && . env/bin/activate && $(PIP) install shapely $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --target=./site-packages
# Required by the BIM type manager thumbnail generator
cd dist/working && . env/bin/activate && $(PIP) install pillow $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --target=./site-packages
# Provides mustache templating in construction docs and web UI data
cd dist/working && . env/bin/activate && $(PIP) install pystache --target=./site-packages
# Provides SVG export in construction documentation
cd dist/working && . env/bin/activate && $(PIP) install svgwrite --target=./site-packages
# Provides fuzzy date parsing for construction sequencing
cd dist/working && . env/bin/activate && $(PIP) install python-dateutil --target=./site-packages
# Provides duration parsing for construction sequencing
cd dist/working && . env/bin/activate && $(PIP) install isodate --target=./site-packages
# Provides networkx graph analysis for project dependency calculations
cd dist/working && . env/bin/activate && $(PIP) install networkx --target=./site-packages
# Required by IFCDiff
cd dist/working && . env/bin/activate && $(PIP) install deepdiff --target=./site-packages
# Required by IFCCSV and ifcopenshell.util.selector
cd dist/working && . env/bin/activate && $(PIP) install lark --target=./site-packages
# Required by IFC4D
cd dist/working && . env/bin/activate && $(PIP) install PyP6Xer --target=./site-packages
cd dist/working && . env/bin/activate && $(PIP) install brickschema[persistence] --target=./site-packages
cp -r dist/working/site-packages/* dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by IFCClash
mkdir dist/working
cd dist/working && wget $(HPPFCL_URL)
cd dist/working && unzip hpp-fcl*
cd dist/working && tar -I zstd -xvf pkg*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/$(PYLIBDIR)/site-packages/hppfcl dist/blenderbim/libs/site/packages/
cp -r dist/working/lib/*.so* dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/$(PYLIBDIR)/site-packages/hppfcl dist/blenderbim/libs/site/packages/
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/$(PYLIBDIR)/site-packages/hppfcl dist/blenderbim/libs/site/packages/
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Lib/site-packages/hppfcl dist/blenderbim/libs/site/packages/
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
rm -rf dist/working
# Required by hpp-fcl
mkdir dist/working
cd dist/working && wget $(ASSIMP_URL)
cd dist/working && tar -xf assimp*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/*.so* dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
rm -rf dist/working
# Required by hpp-fcl
mkdir dist/working
cd dist/working && wget $(QHULL_URL)
cd dist/working && tar -xf qhull*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/*.so* dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
rm -rf dist/working
# Required by hpp-fcl
mkdir dist/working
cd dist/working && wget $(EIGENPY_URL)
cd dist/working && unzip eigenpy*
cd dist/working && tar -I zstd -xvf pkg*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/*.so* dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
rm -rf dist/working
# Required by hpp-fcl
mkdir dist/working
cd dist/working && wget $(OCTOMAP_URL)
cd dist/working && tar -xf octomap*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/*.so* dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
rm -rf dist/working
# Required by hpp-fcl
mkdir dist/working
cd dist/working && wget $(BOOST_URL)
cd dist/working && tar -xf boost*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/*.so* dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
rm -rf dist/working
# Required by hpp-fcl
mkdir dist/working
cd dist/working && wget $(BOOSTCPP_URL)
cd dist/working && unzip boost-cpp*
cd dist/working && tar -I zstd -xvf pkg*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/libboost_serialization.so* dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/libboost_serialization.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/libboost_serialization.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/boost_serialization.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
rm -rf dist/working
# Required by hpp-fcl
mkdir dist/working
cd dist/working && wget $(ZLIB_URL)
cd dist/working && tar -xf zlib*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/*.so* dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
rm -rf dist/working
# Required by BIMTester
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/c8/4b/d0a8c23b6c8985e5544ea96d27105a273ea22051317f850c2cdbf2029fe4/behave-1.2.6.tar.gz
@@ -226,6 +604,24 @@ endif
cd dist/working/ && $(PATCH) ../blenderbim/libs/site/packages/behave/reporter/junit.py < junit.patch
rm -rf dist/working
# Required by ids - uh, not any more, but I'm trying it out in the drawing module
ifeq ($(PLATFORM), macosm1)
# No wheels available for MacOS M1, but turns out this Anaconda build works
mkdir dist/working
cd dist/working && wget $(LXML_URL)
cd dist/working && tar -xf lxml*
cp -r dist/working/lib/$(PYLIBDIR)/site-packages/lxml dist/blenderbim/libs/site/packages/
rm -rf dist/working
else
# Wheels are preferred for other OSes. A first attempt at using Anaconda
# builds for everything shows it breaks on windows. See #2422.
mkdir dist/working
cd dist/working && wget $(LXML_URL)
cd dist/working && cp *.whl lxml.zip && unzip lxml.zip
cp -r dist/working/lxml dist/blenderbim/libs/site/packages/
rm -rf dist/working
endif
# Required by behave
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/f4/65/220bb4075fddb09d5b3ea2c1c1fa66c1c72be9361ec187aab50fa161e576/parse-1.15.0.tar.gz
@@ -240,6 +636,34 @@ endif
cd dist/working/parse_type-0.5.2/ && cp -r parse_type ../../blenderbim/libs/site/packages/
rm -rf dist/working
# Required by augin
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/76/b4/b7baffbda025efd5dc8fcd8d2e953e3aa939c236a484084fa8f4c3588ee9/boto3-1.17.17.tar.gz
cd dist/working && tar -xzvf boto3*
cd dist/working/boto3-1.17.17/ && cp -r boto3 ../../blenderbim/libs/site/packages/
rm -rf dist/working
# Required by boto3
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/7f/2e/e02fdfd0f0377c4e44e61de27b05d5cfe93575770661ef9ded80ed90fa88/botocore-1.20.17.tar.gz
cd dist/working && tar -xzvf botocore*
cd dist/working/botocore-1.20.17/ && cp -r botocore ../../blenderbim/libs/site/packages/
rm -rf dist/working
# Required by boto3
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/3c/56/3f325b1eef9791759784aa5046a8f6a1aff8f7c898a2e34506771d3b99d8/jmespath-0.10.0.tar.gz
cd dist/working && tar -xzvf jmespath*
cd dist/working/jmespath-0.10.0/ && cp -r jmespath ../../blenderbim/libs/site/packages/
rm -rf dist/working
# Required by boto3
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/08/e1/3ee2096ebaeeb8c186d20ed16c8faf4a503913e5c9a0e14cd6b8ffc405a3/s3transfer-0.3.4.tar.gz
cd dist/working && tar -xzvf s3transfer*
cd dist/working/s3transfer-0.3.4/ && cp -r s3transfer ../../blenderbim/libs/site/packages/
rm -rf dist/working
# Provides IFCJSON functionality
mkdir dist/working
cd dist/working && wget https://github.com/IFCJSON-Team/IFC2JSON_python/archive/master.zip
@@ -257,7 +681,7 @@ endif
# Required by brickschema
# This is a bit of a dodgy one, it should be provided by setuptools which Blender doesn't ship.
mkdir dist/working
cd dist/working && wget https://github.com/IfcOpenShell/IfcOpenShell/raw/v0.7.0/src/blenderbim/scripts/pkg_resources.tar.gz
cd dist/working && wget https://blenderbim.org/builds/pkg_resources.tar.gz
cd dist/working && tar -xzvf pkg_resources*
cd dist/working/ && cp -r pkg_resources ../blenderbim/libs/site/packages/
rm -rf dist/working
@@ -272,9 +696,9 @@ endif
# Required for Desktop icon and file association
cp -r blenderbim/libs/desktop dist/blenderbim/libs/
# Generate translations module for BBIM build
# generate translations module for BBIM build
git clone https://github.com/IfcOpenShell/blenderbim-translations.git dist/working
$(PYTHON) scripts/bbim_translations.py -i "dist/working" -o "dist/blenderbim"
python scripts/bbim_translations.py -i "dist/working" -o "dist/blenderbim"
rm -rf dist/working
# Remove dependencies also bundled with Blender
@@ -282,7 +706,6 @@ endif
rm -rf dist/blenderbim/libs/site/packages/numpy.libs
cd dist/blenderbim && $(SED) "s/999999/$(VERSION)/" __init__.py
cd dist/blenderbim && $(SED) "s/999999/$(VERSION)/" blender_manifest.toml
cd dist/blenderbim/bim && $(SED) "s/8888888/$(LAST_COMMIT_HASH)/" __init__.py
cd dist && zip -r blenderbim-$(VERSION)-$(PYVERSION)-$(PLATFORM).zip ./*
rm -rf dist/blenderbim
+8 -169
View File
@@ -18,21 +18,7 @@
import os
import sys
# Ensure we don't try to import bpy or blenderbim.bim
# to support running core tests.
# We assume if bpy was never loaded in current python session
# then we're not in Blender. It's still possible to use
# bpy in core and core tests for annotations using TYPE_CHECKING.
IN_BLENDER = sys.modules.get("bpy", None)
if IN_BLENDER:
import bpy
import platform
import traceback
import subprocess
import webbrowser
import addon_utils
from collections import deque
import site
bl_info = {
"name": "BlenderBIM",
@@ -41,168 +27,21 @@ bl_info = {
"blender": (3, 1, 0),
"version": (0, 0, 999999),
"location": "File Menu, Scene Properties Tab. See documentation for more.",
"doc_url": "https://docs.blenderbim.org/",
"doc_url": "https://blenderbim.org/docs",
"tracker_url": "https://github.com/IfcOpenShell/IfcOpenShell/issues",
"category": "System",
}
last_error = None
last_actions: deque = deque(maxlen=10)
def get_debug_info():
version = ".".join(
[
str(x)
for x in [
addon.bl_info.get("version", (-1, -1, -1))
for addon in addon_utils.modules()
if addon.bl_info["name"] == "BlenderBIM"
][0]
]
)
return {
"os": platform.system(),
"os_version": platform.version(),
"python_version": platform.python_version(),
"architecture": platform.architecture(),
"machine": platform.machine(),
"processor": platform.processor(),
"blender_version": bpy.app.version_string,
"blenderbim_version": version,
"last_actions": last_actions,
"last_error": last_error,
}
def format_debug_info(info: dict):
last_actions = ""
for action in info["last_actions"]:
last_actions += f"\n# {action['type']}: {action['name']}"
if settings := action.get("settings"):
last_actions += f"\n>>> {settings}"
info["last_actions"] = last_actions
text = "\n".join(f"{k}: {v}" for k, v in info.items())
return text.strip()
if IN_BLENDER:
if sys.modules.get("bpy", None):
# Process *.pth in /libs/site/packages to setup globally importable modules
# This is 3 levels deep as required by the static RPATH of ../../ from dependencies taken from Anaconda
# site.addsitedir(os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
try:
import blenderbim.bim
import ifcopenshell.api
import blenderbim.bim
def log_api(usecase_path, ifc_file, settings):
last_actions.append(
{
"type": "ifcopenshell.api",
"name": usecase_path,
"settings": ifcopenshell.api.serialise_settings(settings),
}
)
def register():
blenderbim.bim.register()
ifcopenshell.api.add_pre_listener("*", "action_logger", log_api)
def register():
blenderbim.bim.register()
def unregister():
blenderbim.bim.unregister()
except:
last_error = traceback.format_exc()
print(last_error)
print(format_debug_info(get_debug_info()))
print("\nFATAL ERROR: Unable to load the BlenderBIM Add-on")
class BIM_PT_fatal_error(bpy.types.Panel):
bl_label = "BlenderBIM Fatal Error"
bl_idname = "SCENE_PT_error_message"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
def draw(self, context):
info = get_debug_info()
layout = self.layout
layout.alert = True
layout.label(text="BlenderBIM could not load.", icon="ERROR")
if info["os"] == "Windows":
layout.operator("wm.console_toggle", text="View the console for full logs.", icon="CONSOLE")
else:
layout.label(text="View the console for full logs.", icon="CONSOLE")
box = layout.box()
py = ".".join(info["python_version"].split(".")[0:2])
b3d = ".".join(info["blender_version"].split(".")[0:2])
box.label(text=f"Blender {b3d} {info['os']} {info['machine']}", icon="BLENDER")
box.label(text=f"Python {py} BBIM {info['blenderbim_version']}", icon="SCRIPTPLUGINS")
layout.operator("bim.copy_debug_information", text="Copy Error Message To Clipboard")
op = layout.operator("bim.open_uri", text="How Can I Fix This?")
op.uri = "https://docs.blenderbim.org/users/troubleshooting.html#installation-issues"
layout.label(text="Try Reinstalling:", icon="IMPORT")
op = layout.operator("bim.open_uri", text="Re-download Add-on")
bbim_date = info["blenderbim_version"].split(".")[-1]
py_tag = py.replace(".", "")
if "Linux" in info["os"]:
os = "linux"
elif "Darwin" in info["os"]:
if "arm64" in info["machine"]:
os = "macosm1"
else:
os = "macos"
else:
os = "win"
op.uri = f"https://github.com/IfcOpenShell/IfcOpenShell/releases/download/blenderbim-{bbim_date}/blenderbim-{bbim_date}-py{py_tag}-{os}.zip"
class OpenUri(bpy.types.Operator):
bl_idname = "bim.open_uri"
bl_label = "Open URI"
uri: bpy.props.StringProperty()
def execute(self, context):
webbrowser.open(self.uri)
return {"FINISHED"}
class CopyDebugInformation(bpy.types.Operator):
bl_idname = "bim.copy_debug_information"
bl_label = "Copy Debug Information"
bl_description = "Copies debugging information to your clipboard for use in bugreports"
def execute(self, context):
info = format_debug_info(get_debug_info())
context.window_manager.clipboard = info
return {"FINISHED"}
class HiddenPanel:
@classmethod
def false_poll(cls, context):
return False
def register():
# Only show our error panel and nothing else in the scene tab
for item_name in dir(bpy.types):
item = getattr(bpy.types, item_name)
if not hasattr(item, "bl_rna") or not isinstance(item.bl_rna, bpy.types.Panel):
continue
if getattr(item, "bl_context", None) != "scene":
continue
# Reregister scene panel with a new poll to hide it
item.poll = HiddenPanel.false_poll
bpy.utils.unregister_class(item)
bpy.utils.register_class(item)
bpy.utils.register_class(BIM_PT_fatal_error)
bpy.utils.register_class(CopyDebugInformation)
bpy.utils.register_class(OpenUri)
def unregister():
bpy.utils.unregister_class(OpenUri)
bpy.utils.unregister_class(CopyDebugInformation)
bpy.utils.unregister_class(BIM_PT_fatal_error)
def unregister():
blenderbim.bim.unregister()
+8 -19
View File
@@ -17,13 +17,12 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
from pathlib import Path
import bpy
import bpy.utils.previews
import blenderbim
import importlib
from pathlib import Path
from . import handler, ui, prop, operator, helper
from typing import Callable, Union
try:
from blenderbim.translations import translations_dict
@@ -97,13 +96,9 @@ for name in modules.keys():
classes = [
operator.AddIfcFile,
operator.BIM_OT_add_section_plane,
operator.BIM_OT_delete_object,
operator.BIM_OT_open_webbrowser,
operator.BIM_OT_remove_section_plane,
operator.BIM_OT_select_object,
operator.BIM_OT_show_description,
operator.ClippingPlaneCutWithCappings,
operator.CloseError,
operator.EditBlenderCollection,
operator.FileAssociate,
operator.FileUnassociate,
@@ -111,7 +106,6 @@ classes = [
operator.OpenUri,
operator.ReloadIfcFile,
operator.RemoveIfcFile,
operator.RevertClippingPlaneCut,
operator.SelectDataDir,
operator.SelectIfcFile,
operator.SelectSchemaDir,
@@ -134,7 +128,6 @@ classes = [
prop.BIMMeshProperties,
prop.BIMFacet,
prop.BIMFilterGroup,
ui.BIM_UL_clipping_plane,
ui.BIM_UL_generic,
ui.BIM_UL_topics,
ui.BIM_ADDON_preferences,
@@ -142,7 +135,6 @@ classes = [
ui.BIM_PT_tabs,
# Project overview
ui.BIM_PT_tab_project_info,
ui.BIM_PT_tab_spatial_decomposition,
ui.BIM_PT_tab_project_setup,
ui.BIM_PT_tab_geometry,
ui.BIM_PT_tab_stakeholders,
@@ -155,10 +147,9 @@ classes = [
ui.BIM_PT_tab_representations,
ui.BIM_PT_tab_geometric_relationships,
ui.BIM_PT_tab_parametric_geometry,
ui.BIM_PT_tab_object_materials,
ui.BIM_PT_tab_profiles,
ui.BIM_PT_tab_materials,
ui.BIM_PT_tab_styles,
ui.BIM_PT_tab_profiles,
# Drawings and documents
ui.BIM_PT_tab_sheets,
ui.BIM_PT_tab_drawings,
@@ -171,7 +162,6 @@ classes = [
ui.BIM_PT_tab_structural,
# Construction scheduling
ui.BIM_PT_tab_status,
ui.BIM_PT_tab_qto,
ui.BIM_PT_tab_resources,
ui.BIM_PT_tab_cost,
ui.BIM_PT_tab_sequence,
@@ -185,7 +175,6 @@ classes = [
ui.BIM_PT_tab_sandbox,
# TODO: move this somewhere else and clean it up
ui.BIM_PT_section_plane,
ui.BIM_PT_section_with_cappings,
]
for mod in modules.values():
@@ -195,7 +184,7 @@ addon_keymaps = []
icons = None
is_registering = False
last_commit_hash = "8888888"
original_scene_panels_polls: dict[bpy.types.Panel, Union[Callable, None]] = dict()
overridden_scene_panels = dict()
def on_register(scene):
@@ -303,10 +292,10 @@ def unregister():
km.keymap_items.remove(kmi)
addon_keymaps.clear()
import blenderbim.tool as tool
# use tuple() as method will be removing keys from dict
for panel in tuple(original_scene_panels_polls.keys()):
tool.Blender.remove_scene_panel_override(panel)
for panel in tuple(overridden_scene_panels.keys()):
original_panel, override_panel = overridden_scene_panels[panel]
bpy.utils.unregister_class(override_panel)
bpy.utils.register_class(original_panel)
del overridden_scene_panels[panel]
bpy.app.translations.unregister("blenderbim")
@@ -10,7 +10,7 @@
* (at your option) any later version.
*
* BlenderBIM Add-on is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY, without even the implied warranty of
* 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.
*
@@ -10,7 +10,7 @@
* (at your option) any later version.
*
* BlenderBIM Add-on is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY, without even the implied warranty of
* 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.
*
@@ -1,45 +0,0 @@
/*
* BlenderBIM Add-on - OpenBIM Blender Add-on
* Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
*
* This file is part of BlenderBIM Add-on.
*
* BlenderBIM Add-on 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.
*
* BlenderBIM Add-on 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 BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* You may copy this `schedule.css` template alongside your input schedule
* document with the same filename. For example if you have a schedule called
* `door_types.ods`, you can create a `door_types.css` in the same folder to
* style that schedule.
*/
/**
* If you specify a font size in CSS, such as text { font-size: 5; }, all fonts
* will be overriden to match that size.
*
* If your CSS, ODS, XLSX does not specify a font size, the variables below
* will specify the default font size.
*
* If your ODS, XLSX does specify a font size, they will scale linearly based
* on the variables below.
*/
:root {
--font-size-pt: 12;
--font-size-px: 4.13;
--font-width: 0.45;
}
text, tspan { /* 2.5mm */ fill: black; stroke: none; font-family: 'OpenGost Type B TT', 'DejaVu Sans Condensed', 'Liberation Sans', 'Arial Narrow', 'Arial'; }
@@ -1,17 +1,17 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1');
FILE_NAME('/dev/null','2024-03-03T16:03:58+11:00',(),(),'IfcOpenShell v0.7.0-eaa2aa05a','IfcOpenShell v0.7.0-eaa2aa05a','Nobody');
FILE_NAME('/dev/null','2023-09-07T13:40:06+10:00',(),(),'IfcOpenShell v0.7.0-fbd8ea1ed','IfcOpenShell v0.7.0-fbd8ea1ed','Nobody');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROJECT('3NJUxHyTzEv96oezyDYEqf',$,'BlenderBIM Demo',$,$,$,$,(#12,#16),#7);
#2=IFCPROJECTLIBRARY('0D39k7hbT5ce$jWaCd2niG',$,'BlenderBIM Demo Library',$,$,$,$,$,$);
#3=IFCRELDECLARES('2RWVohyP57PBHT9zCs4H2O',$,$,$,#1,(#2));
#1=IFCPROJECT('1arVsMni92iPUaaryGTUz2',$,'BlenderBIM Demo',$,$,$,$,(#12,#16),#7);
#2=IFCPROJECTLIBRARY('1FfDG5b21DuBDOO4eUE9rF',$,'BlenderBIM Demo Library',$,$,$,$,$,$);
#3=IFCRELDECLARES('3tfDIC3Tf9wP8EUOoFeycK',$,$,$,#1,(#2));
#4=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#5=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#6=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#7=IFCUNITASSIGNMENT((#6,#4,#5));
#7=IFCUNITASSIGNMENT((#5,#6,#4));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCDIRECTION((0.,0.,1.));
#10=IFCDIRECTION((1.,0.,0.));
@@ -23,84 +23,84 @@ DATA;
#16=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Plan',2,1.E-05,#15,$);
#17=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#12,$,.MODEL_VIEW.,$);
#18=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Plan',*,*,*,*,#16,$,.PLAN_VIEW.,$);
#19=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#16,$,.MODEL_VIEW.,$);
#19=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Plan',*,*,*,*,#16,$,.PLAN_VIEW.,$);
#20=IFCMATERIAL('Unknown',$,$);
#21=IFCWALLTYPE('2F1k78lI53fwS0rg3rqrRb',$,'WAL50',$,$,$,$,$,$,.NOTDEFINED.);
#21=IFCWALLTYPE('2o8NqQQbHA5BeErrgwfr7d',$,'WAL50',$,$,$,$,$,$,.NOTDEFINED.);
#22=IFCMATERIALLAYERSET((#24),$,$);
#23=IFCRELASSOCIATESMATERIAL('0khP_Smdj12f2JrOTRbRKj',$,$,$,(#21),#22);
#23=IFCRELASSOCIATESMATERIAL('1Q$5DZKr98n82Eitz2dWey',$,$,$,(#21),#22);
#24=IFCMATERIALLAYER(#20,0.05,$,$,$,$,$);
#25=IFCRELDECLARES('31JriXdev6BP2sC9lcxdC1',$,$,$,#2,(#26,#48,#72,#59,#92,#30,#96,#63,#21,#54,#87,#210,#153,#34,#67,#38,#82,#77,#42));
#26=IFCWALLTYPE('3R5onkTtX1IxdCMPoThRaw',$,'WAL100',$,$,$,$,$,$,.NOTDEFINED.);
#25=IFCRELDECLARES('1KXKUEn8z0g8lvyEsCTWD3',$,$,$,#2,(#38,#82,#77,#42,#26,#48,#72,#59,#92,#30,#96,#63,#21,#54,#87,#153,#34,#67,#210));
#26=IFCWALLTYPE('3KlIOv_P9A79tkt8D_jq_m',$,'WAL100',$,$,$,$,$,$,.NOTDEFINED.);
#27=IFCMATERIALLAYERSET((#29),$,$);
#28=IFCRELASSOCIATESMATERIAL('0$mXRPCT16nP6Q70XFNvZv',$,$,$,(#26),#27);
#28=IFCRELASSOCIATESMATERIAL('32B9Wx8Z1FxhIN7m77MiHG',$,$,$,(#26),#27);
#29=IFCMATERIALLAYER(#20,0.1,$,$,$,$,$);
#30=IFCWALLTYPE('1juanculbDJPqvqT2FCm8m',$,'WAL200',$,$,$,$,$,$,.NOTDEFINED.);
#30=IFCWALLTYPE('2DqrSeel1AGw6h3b$ujafZ',$,'WAL200',$,$,$,$,$,$,.NOTDEFINED.);
#31=IFCMATERIALLAYERSET((#33),$,$);
#32=IFCRELASSOCIATESMATERIAL('0FsB2A$frFn9AZW2IG38ny',$,$,$,(#30),#31);
#32=IFCRELASSOCIATESMATERIAL('2FeYUpt3D4tAvwgGmwXWZn',$,$,$,(#30),#31);
#33=IFCMATERIALLAYER(#20,0.2,$,$,$,$,$);
#34=IFCWALLTYPE('05UYduR9r1vRxZZgP64Rdw',$,'WAL300',$,$,$,$,$,$,.NOTDEFINED.);
#34=IFCWALLTYPE('2jNdEOFIP1eQb3WZePlFGY',$,'WAL300',$,$,$,$,$,$,.NOTDEFINED.);
#35=IFCMATERIALLAYERSET((#37),$,$);
#36=IFCRELASSOCIATESMATERIAL('2FYhYrvmv4SBoZHVWDTTte',$,$,$,(#34),#35);
#36=IFCRELASSOCIATESMATERIAL('2bFeM6xevAwucuwMlauX77',$,$,$,(#34),#35);
#37=IFCMATERIALLAYER(#20,0.3,$,$,$,$,$);
#38=IFCCOVERINGTYPE('0iLxgfHB9F2hRH2vMcw7Yv',$,'COV10',$,$,$,$,$,$,.NOTDEFINED.);
#38=IFCCOVERINGTYPE('3310gCgH59w9tu$DfmiCEN',$,'COV10',$,$,$,$,$,$,.NOTDEFINED.);
#39=IFCMATERIALLAYERSET((#41),$,$);
#40=IFCRELASSOCIATESMATERIAL('0yvFX0zgb9uvJ3ARnf4mfe',$,$,$,(#38),#39);
#40=IFCRELASSOCIATESMATERIAL('3pTNmWd_jBoOWyHc8Yf4qI',$,$,$,(#38),#39);
#41=IFCMATERIALLAYER(#20,0.01,$,$,$,$,$);
#42=IFCCOVERINGTYPE('15Riw2WTrAUPwvfLyKXJ2g',$,'COV20',$,$,(#46),$,$,$,.NOTDEFINED.);
#42=IFCCOVERINGTYPE('2sksWFuPzCgeNk1ofXS6PZ',$,'COV20',$,$,(#46),$,$,$,.NOTDEFINED.);
#43=IFCMATERIALLAYERSET((#45),$,$);
#44=IFCRELASSOCIATESMATERIAL('1iAOG6NK9FBxitFtzN9qP5',$,$,$,(#42),#43);
#44=IFCRELASSOCIATESMATERIAL('36rl5NddT1ux6ejkQAkT2z',$,$,$,(#42),#43);
#45=IFCMATERIALLAYER(#20,0.02,$,$,$,$,$);
#46=IFCPROPERTYSET('18ICCZ0fjDG9UXMyzrsFBA',$,'EPset_Parametric',$,(#47));
#46=IFCPROPERTYSET('2VZJ9xfdn1NeOY6uEmnnYW',$,'EPset_Parametric',$,(#47));
#47=IFCPROPERTYSINGLEVALUE('LayerSetDirection',$,IFCLABEL('AXIS2'),$);
#48=IFCCOVERINGTYPE('0gEOtYULD0F9KD9zPoA6cJ',$,'COV30',$,$,(#52),$,$,$,.NOTDEFINED.);
#48=IFCCOVERINGTYPE('1uU6rxt95AC91lZ0jTRG6G',$,'COV30',$,$,(#52),$,$,$,.NOTDEFINED.);
#49=IFCMATERIALLAYERSET((#51),$,$);
#50=IFCRELASSOCIATESMATERIAL('0xd6dsjf13JA98q9fnAMpQ',$,$,$,(#48),#49);
#50=IFCRELASSOCIATESMATERIAL('2lFv4xQYr5bwtiSsMTVCgz',$,$,$,(#48),#49);
#51=IFCMATERIALLAYER(#20,0.03,$,$,$,$,$);
#52=IFCPROPERTYSET('3qCbs1tuDCvuOXam5RrA$a',$,'EPset_Parametric',$,(#53));
#52=IFCPROPERTYSET('0HnrRhcd16Yfbgtci4sqCG',$,'EPset_Parametric',$,(#53));
#53=IFCPROPERTYSINGLEVALUE('LayerSetDirection',$,IFCLABEL('AXIS3'),$);
#54=IFCRAMPTYPE('2yW1ePlyb8cOR6HdEntpRh',$,'RAM200',$,$,$,$,$,$,.NOTDEFINED.);
#54=IFCRAMPTYPE('3nonXCVn58jhVV08N8c97B',$,'RAM200',$,$,$,$,$,$,.NOTDEFINED.);
#55=IFCMATERIALLAYERSET((#57),$,$);
#56=IFCRELASSOCIATESMATERIAL('1x9ZyVim19AQtJhfEE06XR',$,$,$,(#54),#55);
#56=IFCRELASSOCIATESMATERIAL('2NdIrmbUP6bh4x$p_RY31C',$,$,$,(#54),#55);
#57=IFCMATERIALLAYER(#20,0.2,$,$,$,$,$);
#58=IFCCIRCLEPROFILEDEF(.AREA.,$,$,0.3);
#59=IFCPILETYPE('25MTkMtaH5QeCf$gFbvSSw',$,'P1',$,$,$,$,$,$,.NOTDEFINED.);
#59=IFCPILETYPE('2uuf5kq5TDDfO9jGN7XIHh',$,'P1',$,$,$,$,$,$,.NOTDEFINED.);
#60=IFCMATERIALPROFILESET($,$,(#62),$);
#61=IFCRELASSOCIATESMATERIAL('09FXUZCnrFEffxBrtg16gx',$,$,$,(#59),#60);
#61=IFCRELASSOCIATESMATERIAL('3hi821Ffj8YR9yFjp226LU',$,$,$,(#59),#60);
#62=IFCMATERIALPROFILE($,$,#20,#58,$,$);
#63=IFCSLABTYPE('1Akbal7tP5DAPW0ti$Qp30',$,'FLR150',$,$,$,$,$,$,.NOTDEFINED.);
#63=IFCSLABTYPE('2janmz3nH4P9IHaOVvGY_B',$,'FLR150',$,$,$,$,$,$,.NOTDEFINED.);
#64=IFCMATERIALLAYERSET((#66),$,$);
#65=IFCRELASSOCIATESMATERIAL('0jzkXlkBL4HhmQ_FTWW6Cj',$,$,$,(#63),#64);
#65=IFCRELASSOCIATESMATERIAL('0og5Zg4ib67g6XWgOgqBWy',$,$,$,(#63),#64);
#66=IFCMATERIALLAYER(#20,0.2,$,$,$,$,$);
#67=IFCSLABTYPE('0$gpYNi3T08BrxaR67CxgI',$,'FLR250',$,$,$,$,$,$,.NOTDEFINED.);
#67=IFCSLABTYPE('26UKpEaTb9fh4qrqi3Ymzj',$,'FLR250',$,$,$,$,$,$,.NOTDEFINED.);
#68=IFCMATERIALLAYERSET((#70),$,$);
#69=IFCRELASSOCIATESMATERIAL('3a6kjEM29DleYTlSkA8_DK',$,$,$,(#67),#68);
#69=IFCRELASSOCIATESMATERIAL('1BeBXPqen9TQEE42SxzhjF',$,$,$,(#67),#68);
#70=IFCMATERIALLAYER(#20,0.3,$,$,$,$,$);
#71=IFCRECTANGLEPROFILEDEF(.AREA.,'500x600',$,0.5,0.6);
#72=IFCCOLUMNTYPE('278sjptdDDzgOZseERj390',$,'C1',$,$,$,$,$,$,.NOTDEFINED.);
#72=IFCCOLUMNTYPE('1KnWzBBoLCNfJ3_J79LT7d',$,'C1',$,$,$,$,$,$,.NOTDEFINED.);
#73=IFCMATERIALPROFILESET($,$,(#75),$);
#74=IFCRELASSOCIATESMATERIAL('1ppYXw39v6kBwi3rrIm4ZE',$,$,$,(#72),#73);
#74=IFCRELASSOCIATESMATERIAL('1W8OWQjEL05gH5hEpve6Lh',$,$,$,(#72),#73);
#75=IFCMATERIALPROFILE($,$,#20,#71,$,$);
#76=IFCCIRCLEHOLLOWPROFILEDEF(.AREA.,'500.0x5.0 CHS',$,0.25,0.005);
#77=IFCCOLUMNTYPE('3aSZOvGmr7jggSNwsq5PJE',$,'C2',$,$,$,$,$,$,.NOTDEFINED.);
#77=IFCCOLUMNTYPE('33jPFERfL9bfTeWfTL5kZ7',$,'C2',$,$,$,$,$,$,.NOTDEFINED.);
#78=IFCMATERIALPROFILESET($,$,(#80),$);
#79=IFCRELASSOCIATESMATERIAL('1AtNJPaD14DgINbAjXvbU4',$,$,$,(#77),#78);
#79=IFCRELASSOCIATESMATERIAL('3czOrQw2v9BfS0PeQCrzWq',$,$,$,(#77),#78);
#80=IFCMATERIALPROFILE($,$,#20,#76,$,$);
#81=IFCRECTANGLEHOLLOWPROFILEDEF(.AREA.,'150x75x2.0 RHS',$,0.075,0.15,0.002,0.005,0.005);
#82=IFCCOLUMNTYPE('28iv3Kru12yQ7R7RRwNjvD',$,'C3',$,$,$,$,$,$,.NOTDEFINED.);
#82=IFCCOLUMNTYPE('21YcNdKNj2$95U55yNx1Nw',$,'C3',$,$,$,$,$,$,.NOTDEFINED.);
#83=IFCMATERIALPROFILESET($,$,(#85),$);
#84=IFCRELASSOCIATESMATERIAL('2M_oL$n3j6rBRnOW5RSk87',$,$,$,(#82),#83);
#84=IFCRELASSOCIATESMATERIAL('3n6Cru3xr6jvDptU9u7QEF',$,$,$,(#82),#83);
#85=IFCMATERIALPROFILE($,$,#20,#81,$,$);
#86=IFCISHAPEPROFILEDEF(.AREA.,'DEMO-I',$,0.1,0.2,0.005,0.01,0.005,$,$);
#87=IFCBEAMTYPE('2E$V5l4b54dxuToJ1A6IHp',$,'B1',$,$,$,$,$,$,.NOTDEFINED.);
#87=IFCBEAMTYPE('2CsGpD$6nDCxWv9jXTtvq9',$,'B1',$,$,$,$,$,$,.NOTDEFINED.);
#88=IFCMATERIALPROFILESET($,$,(#90),$);
#89=IFCRELASSOCIATESMATERIAL('3PUNnY7cj8LhyXbHKVqZnf',$,$,$,(#87),#88);
#89=IFCRELASSOCIATESMATERIAL('2LJExc74j9rgBSBx51HDPK',$,$,$,(#87),#88);
#90=IFCMATERIALPROFILE($,$,#20,#86,$,$);
#91=IFCCSHAPEPROFILEDEF(.AREA.,'DEMO-C',$,0.2,0.1,0.0015,0.03,0.005);
#92=IFCBEAMTYPE('3qutoZTvP0lvLWNXyjPhPm',$,'B2',$,$,$,$,$,$,.NOTDEFINED.);
#92=IFCBEAMTYPE('32pHN2b0P0awGw$U8PpneO',$,'B2',$,$,$,$,$,$,.NOTDEFINED.);
#93=IFCMATERIALPROFILESET($,$,(#95),$);
#94=IFCRELASSOCIATESMATERIAL('3dGyQf42H2ahch2bSYPPHP',$,$,$,(#92),#93);
#94=IFCRELASSOCIATESMATERIAL('2wg8R1Drb2xhQ2Y_pRSh3c',$,$,$,(#92),#93);
#95=IFCMATERIALPROFILE($,$,#20,#91,$,$);
#96=IFCWINDOWTYPE('0bK3c4PWL3eOMNwkPN$rlg',$,'WT01',$,$,$,(#135,#152),$,$,.NOTDEFINED.,.NOTDEFINED.,$,$);
#96=IFCWINDOWTYPE('1Gg9HfZEr69u8SdWfDs2J3',$,'WT01',$,$,$,(#135,#152),$,$,.NOTDEFINED.,.NOTDEFINED.,$,$);
#97=IFCINDEXEDPOLYGONALFACE((13,17,18,14));
#98=IFCINDEXEDPOLYGONALFACE((5,6,3,4));
#99=IFCINDEXEDPOLYGONALFACE((7,8,2,1));
@@ -157,7 +157,7 @@ DATA;
#150=IFCDIRECTION((0.,0.,1.));
#151=IFCAXIS2PLACEMENT3D(#148,#150,#149);
#152=IFCREPRESENTATIONMAP(#151,#147);
#153=IFCDOORTYPE('2j0vIBgkH0ERnBCP0cpcCz',$,'DT01',$,$,$,(#196,#209),$,$,.NOTDEFINED.,.NOTDEFINED.,$,$);
#153=IFCDOORTYPE('0NBUmPKyT9WecsIeYJrEqg',$,'DT01',$,$,$,(#196,#209),$,$,.NOTDEFINED.,.NOTDEFINED.,$,$);
#154=IFCINDEXEDPOLYGONALFACE((17,16,15,14));
#155=IFCINDEXEDPOLYGONALFACE((2,3,29,28));
#156=IFCINDEXEDPOLYGONALFACE((27,28,29,30,32,31));
@@ -214,7 +214,7 @@ DATA;
#207=IFCDIRECTION((0.,0.,1.));
#208=IFCAXIS2PLACEMENT3D(#205,#207,#206);
#209=IFCREPRESENTATIONMAP(#208,#204);
#210=IFCFURNITURETYPE('02XxQ_3oT0SPrmFPATrt7o',$,'BUN01',$,$,$,(#931,#952),$,$,.NOTDEFINED.,.NOTDEFINED.);
#210=IFCFURNITURETYPE('3Kyc6IyarAUw3_8fkNtXIg',$,'BUN01',$,$,$,(#931,#952),$,$,.NOTDEFINED.,.NOTDEFINED.);
#211=IFCINDEXEDPOLYGONALFACE((187,278,44));
#212=IFCINDEXEDPOLYGONALFACE((21,52,60));
#213=IFCINDEXEDPOLYGONALFACE((91,100,31));
@@ -957,35 +957,35 @@ DATA;
#950=IFCDIRECTION((0.,0.,1.));
#951=IFCAXIS2PLACEMENT3D(#948,#950,#949);
#952=IFCREPRESENTATIONMAP(#951,#947);
#953=IFCTYPEPRODUCT('0UrP0fLdD5OwzwD41aRKao',$,'SETOUT-POINT',$,'IfcAnnotation/SYMBOL',(#954),$,$);
#954=IFCPROPERTYSET('27lmSbeAXC08EWkEq8XdUG',$,'EPset_Annotation',$,(#955));
#953=IFCTYPEPRODUCT('0TBMBnD_b66QLUWKD9HLsc',$,'SETOUT-POINT',$,'IfcAnnotation/SYMBOL',(#954),$,$);
#954=IFCPROPERTYSET('2dQzX_K4r1Xet6dz9zBlgs',$,'EPset_Annotation',$,(#955));
#955=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('setout-point'),$);
#956=IFCTYPEPRODUCT('3IQrQOSFP0WfkuLk0ak7_0',$,'CONTROL-POINT',$,'IfcAnnotation/SYMBOL',(#957),$,$);
#957=IFCPROPERTYSET('23aN9DsdjFcfWq8KGSIVoN',$,'EPset_Annotation',$,(#958));
#956=IFCTYPEPRODUCT('0vo_7PU3H9ygTnrIipqPRI',$,'CONTROL-POINT',$,'IfcAnnotation/SYMBOL',(#957),$,$);
#957=IFCPROPERTYSET('3APQOw$FP1ivxp08UxHsl6',$,'EPset_Annotation',$,(#958));
#958=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('control-point'),$);
#959=IFCTYPEPRODUCT('3wvbaiaIz8agQgDRzt01vz',$,'TRAVERSE-POINT',$,'IfcAnnotation/SYMBOL',(#960),$,$);
#960=IFCPROPERTYSET('1j7aFM1Rb9BRuKgUEn1U10',$,'EPset_Annotation',$,(#961));
#959=IFCTYPEPRODUCT('32V27G3$T1OO3TbXg6948F',$,'TRAVERSE-POINT',$,'IfcAnnotation/SYMBOL',(#960),$,$);
#960=IFCPROPERTYSET('0cTSYXMc9B$PTXVDrhBYYW',$,'EPset_Annotation',$,(#961));
#961=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('traverse-point'),$);
#962=IFCTYPEPRODUCT('0p5ZTfTnX9ZOywroa7Ffql',$,'DASHED',$,'IfcAnnotation/LINEWORK',(#963),$,$);
#963=IFCPROPERTYSET('27smSHyiv39vhBRSwfDVCD',$,'EPset_Annotation',$,(#964));
#962=IFCTYPEPRODUCT('2PXIC7Bg914gJhRh2XeGnN',$,'DASHED',$,'IfcAnnotation/LINEWORK',(#963),$,$);
#963=IFCPROPERTYSET('0RiYsOxp529gXnCkw44wF8',$,'EPset_Annotation',$,(#964));
#964=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('dashed'),$);
#965=IFCTYPEPRODUCT('2EvrG9Vuf6t9etgMeWFuJ2',$,'FINE',$,'IfcAnnotation/LINEWORK',(#966),$,$);
#966=IFCPROPERTYSET('0gVTuDcZ5ByRdR3sZnEZUR',$,'EPset_Annotation',$,(#967));
#965=IFCTYPEPRODUCT('1Ou1kA3Vb4HhuNBvM0uGe0',$,'FINE',$,'IfcAnnotation/LINEWORK',(#966),$,$);
#966=IFCPROPERTYSET('2gzly9D0L1qPK2MExeXgRO',$,'EPset_Annotation',$,(#967));
#967=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('fine'),$);
#968=IFCTYPEPRODUCT('0IeM1ywXn1qhR_6NhB6N4s',$,'THIN',$,'IfcAnnotation/LINEWORK',(#969),$,$);
#969=IFCPROPERTYSET('0t6c$uCeT9092GYkbm8hDS',$,'EPset_Annotation',$,(#970));
#968=IFCTYPEPRODUCT('3lx$KQPRbEZwbQ7Xdfm5gw',$,'THIN',$,'IfcAnnotation/LINEWORK',(#969),$,$);
#969=IFCPROPERTYSET('0vsVpqs6zArvA2bfBtvQew',$,'EPset_Annotation',$,(#970));
#970=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('thin'),$);
#971=IFCTYPEPRODUCT('1jZVbCwrTCGhZdKbH4uqTP',$,'MEDIUM',$,'IfcAnnotation/LINEWORK',(#972),$,$);
#972=IFCPROPERTYSET('2uTJ11lF98GeN8pPIf340N',$,'EPset_Annotation',$,(#973));
#971=IFCTYPEPRODUCT('00j$y97p903w2HOb35lAQ2',$,'MEDIUM',$,'IfcAnnotation/LINEWORK',(#972),$,$);
#972=IFCPROPERTYSET('240rEBDOn8PAvfnm_43s55',$,'EPset_Annotation',$,(#973));
#973=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('medium'),$);
#974=IFCTYPEPRODUCT('38zW9E1uH2ae$zat9KrieS',$,'THICK',$,'IfcAnnotation/LINEWORK',(#975),$,$);
#975=IFCPROPERTYSET('2XQxgO16v9vxjvuIJpaPpG',$,'EPset_Annotation',$,(#976));
#974=IFCTYPEPRODUCT('2tVdFGorj6dfrL4uA1kyW8',$,'THICK',$,'IfcAnnotation/LINEWORK',(#975),$,$);
#975=IFCPROPERTYSET('2IEGHncr1D$R8fKA9zCEJP',$,'EPset_Annotation',$,(#976));
#976=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('thick'),$);
#977=IFCTYPEPRODUCT('3G2s7ZLzfDJh5J$2iIzHw9',$,'STRONG',$,'IfcAnnotation/LINEWORK',(#978),$,$);
#978=IFCPROPERTYSET('0TcB8Gal96vebTrLWa5CEw',$,'EPset_Annotation',$,(#979));
#977=IFCTYPEPRODUCT('1T5C$$ONTBB8A9a7vv0Gn6',$,'STRONG',$,'IfcAnnotation/LINEWORK',(#978),$,$);
#978=IFCPROPERTYSET('1gRomAS1L2WguA51ZI0E40',$,'EPset_Annotation',$,(#979));
#979=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('strong'),$);
#980=IFCTYPEPRODUCT('3pq2B$0k52ZOv2bFuRYkxO',$,'SETOUT-TAG',$,'IfcAnnotation/TEXT',(#981),(#1000),$);
#981=IFCPROPERTYSET('39$oNFI052cBhtpZCVVLfj',$,'EPset_Annotation',$,(#982));
#980=IFCTYPEPRODUCT('2LmKYrAEr2K8EwJyCAsyc4',$,'SETOUT-TAG',$,'IfcAnnotation/TEXT',(#981),(#1000),$);
#981=IFCPROPERTYSET('0nF9du8qDAF9aldqjCUsbX',$,'EPset_Annotation',$,(#982));
#982=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('setout-tag'),$);
#983=IFCCARTESIANPOINT((0.,0.,0.));
#984=IFCDIRECTION((0.,0.,1.));
@@ -1005,8 +1005,8 @@ DATA;
#998=IFCDIRECTION((0.,0.,1.));
#999=IFCAXIS2PLACEMENT3D(#996,#998,#997);
#1000=IFCREPRESENTATIONMAP(#999,#995);
#1001=IFCTYPEPRODUCT('11M3ahhrr9NBdB29YEazzw',$,'DOOR-TAG',$,'IfcAnnotation/TEXT',(#1002),(#1021),$);
#1002=IFCPROPERTYSET('3xUNoLKPT9kB4LMdoctm5k',$,'EPset_Annotation',$,(#1003));
#1001=IFCTYPEPRODUCT('1cdhVmqEPF6e13_TFOdlQw',$,'DOOR-TAG',$,'IfcAnnotation/TEXT',(#1002),(#1021),$);
#1002=IFCPROPERTYSET('1MUPGQolnDEgp0NxcMN56E',$,'EPset_Annotation',$,(#1003));
#1003=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('door-tag'),$);
#1004=IFCCARTESIANPOINT((0.,0.,0.));
#1005=IFCDIRECTION((0.,0.,1.));
@@ -1026,8 +1026,8 @@ DATA;
#1019=IFCDIRECTION((0.,0.,1.));
#1020=IFCAXIS2PLACEMENT3D(#1017,#1019,#1018);
#1021=IFCREPRESENTATIONMAP(#1020,#1016);
#1022=IFCTYPEPRODUCT('1eE8Y$BVDFDgG8Fj6d9wiV',$,'WINDOW-TAG',$,'IfcAnnotation/TEXT',(#1023),(#1036),$);
#1023=IFCPROPERTYSET('2MqzSGkXDBcPSolaAQvYO4',$,'EPset_Annotation',$,(#1024));
#1022=IFCTYPEPRODUCT('0OuDi3gRH2CxW9mrtE0vXw',$,'WINDOW-TAG',$,'IfcAnnotation/TEXT',(#1023),(#1036),$);
#1023=IFCPROPERTYSET('2X7dAo_1P5ruRnlKA4kIl6',$,'EPset_Annotation',$,(#1024));
#1024=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('window-tag'),$);
#1025=IFCCARTESIANPOINT((0.,0.,0.));
#1026=IFCDIRECTION((0.,0.,1.));
@@ -1041,8 +1041,8 @@ DATA;
#1034=IFCDIRECTION((0.,0.,1.));
#1035=IFCAXIS2PLACEMENT3D(#1032,#1034,#1033);
#1036=IFCREPRESENTATIONMAP(#1035,#1031);
#1037=IFCTYPEPRODUCT('0vvfHSiaPBxewA$32C4LdW',$,'SPACE-TAG',$,'IfcAnnotation/TEXT',(#1038),(#1063),$);
#1038=IFCPROPERTYSET('2zKuFdTPj3DhNdQ0U2kd5u',$,'EPset_Annotation',$,(#1039));
#1037=IFCTYPEPRODUCT('3WEV_9wQn6AQTESgjW36PH',$,'SPACE-TAG',$,'IfcAnnotation/TEXT',(#1038),(#1063),$);
#1038=IFCPROPERTYSET('02FAX8dIzAlunWD5ak$6sU',$,'EPset_Annotation',$,(#1039));
#1039=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('space-tag'),$);
#1040=IFCCARTESIANPOINT((0.,0.,0.));
#1041=IFCDIRECTION((0.,0.,1.));
@@ -1068,8 +1068,8 @@ DATA;
#1061=IFCDIRECTION((0.,0.,1.));
#1062=IFCAXIS2PLACEMENT3D(#1059,#1061,#1060);
#1063=IFCREPRESENTATIONMAP(#1062,#1058);
#1064=IFCTYPEPRODUCT('36lTcd9yT8UBNDejQUNWrq',$,'MATERIAL-TAG',$,'IfcAnnotation/TEXT',(#1065),(#1078),$);
#1065=IFCPROPERTYSET('1x86VNVk1Drwe5XOj$GZX3',$,'EPset_Annotation',$,(#1066));
#1064=IFCTYPEPRODUCT('1evqvQLLL1zxdsIWqEn0lK',$,'MATERIAL-TAG',$,'IfcAnnotation/TEXT',(#1065),(#1078),$);
#1065=IFCPROPERTYSET('2KPJlGyer0UBmphfR7952k',$,'EPset_Annotation',$,(#1066));
#1066=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('rectangle-tag'),$);
#1067=IFCCARTESIANPOINT((0.,0.,0.));
#1068=IFCDIRECTION((0.,0.,1.));
@@ -1083,8 +1083,8 @@ DATA;
#1076=IFCDIRECTION((0.,0.,1.));
#1077=IFCAXIS2PLACEMENT3D(#1074,#1076,#1075);
#1078=IFCREPRESENTATIONMAP(#1077,#1073);
#1079=IFCTYPEPRODUCT('1rxhEX6I16oPqglXaGZ7Sw',$,'TYPE-TAG',$,'IfcAnnotation/TEXT',(#1080),(#1093),$);
#1080=IFCPROPERTYSET('3BeqEbDzX0xORamqqSx_kZ',$,'EPset_Annotation',$,(#1081));
#1079=IFCTYPEPRODUCT('3Nem6d4xX87O3deyWDi3AW',$,'TYPE-TAG',$,'IfcAnnotation/TEXT',(#1080),(#1093),$);
#1080=IFCPROPERTYSET('15bspgnA9CEuFox6xFjoTL',$,'EPset_Annotation',$,(#1081));
#1081=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('capsule-tag'),$);
#1082=IFCCARTESIANPOINT((0.,0.,0.));
#1083=IFCDIRECTION((0.,0.,1.));
@@ -1098,8 +1098,8 @@ DATA;
#1091=IFCDIRECTION((0.,0.,1.));
#1092=IFCAXIS2PLACEMENT3D(#1089,#1091,#1090);
#1093=IFCREPRESENTATIONMAP(#1092,#1088);
#1094=IFCTYPEPRODUCT('0sws1hxNb2Og1etXqyoEh1',$,'NAME-TAG',$,'IfcAnnotation/TEXT',(#1095),(#1108),$);
#1095=IFCPROPERTYSET('11L$PtdnX8LfYigzRZ$g0a',$,'EPset_Annotation',$,(#1096));
#1094=IFCTYPEPRODUCT('0klFX9AjnEnPBkdIURv8XD',$,'NAME-TAG',$,'IfcAnnotation/TEXT',(#1095),(#1108),$);
#1095=IFCPROPERTYSET('1d53tifbv2rwcoFFJosiHf',$,'EPset_Annotation',$,(#1096));
#1096=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('capsule-tag'),$);
#1097=IFCCARTESIANPOINT((0.,0.,0.));
#1098=IFCDIRECTION((0.,0.,1.));
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',(),(),'EPset_Drawing','EPset
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22,#23));
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22));
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
@@ -27,6 +27,5 @@ DATA;
#20=IFCSIMPLEPROPERTYTEMPLATE('0joEq0Rd10cxweEh0NeHT6',$,'JoinCriteria','Comma separated selection keys which determine what cut objects are to be joined.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#21=IFCSIMPLEPROPERTYTEMPLATE('0nYMT3OSj5gArVniCWZRtv',$,'ShadingStyles','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#22=IFCSIMPLEPROPERTYTEMPLATE('3VWG22eZXBdQwdKlzMeVQH',$,'CurrentShadingStyle','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#23=IFCSIMPLEPROPERTYTEMPLATE('28mJ$GDxb7kBbKFH_rACMQ',$,'AngleDecimalPlaces','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
ENDSEC;
END-ISO-10303-21;
@@ -5,7 +5,6 @@ FILE_NAME('EQto_BodyGeometryValidation.ifc','2020-01-01T00:00:00',(),(),'EQto_Bo
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
/* This quantity set is Qto_BodyGeometryValidation backport from IFC4X3 to support IFC4. */
#1=IFCPROPERTYSETTEMPLATE('2KS9su6r517uLXTGKwwDdn',$,'EQto_BodyGeometryValidation','Quantities supplied for validating the correct interpretation of the body shape representation at import. In case of multiple representation items, the quantities are summed for each of the items (irrespective of any overlap). Choosing a suitable tolerance value for comparing the supplied numbers to the numbers calculated from the reconstructed geometry is at the discretion of the importing application.',.QTO_OCCURRENCEDRIVEN.,'IfcProduct',(#2,#3,#4,#5,#6,#7));
#2=IFCSIMPLEPROPERTYTEMPLATE('3iCdOOLRjCwQ_HRTB41Xh8',$,'GrossSurfaceArea','Total gross area of the object, normally generated as perimeter * length + 2 * cross section area. It is the sum of OuterSurfaceArea + (2 x CrossSectionArea) and shall only be given, if the OuterSurfaceArea and CrossSectionArea cannot be established separately.\X2\000A000A\X0\Total gross surface area of the element before applying product-level geometric features such as openings and projections.',.Q_AREA.,$,$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('0juemEqF5889vg7HwR87Fv',$,'NetSurfaceArea','Net surface area of the object, normally generated as perimeter * length + 2 * cross section area taking into account possible processing features (cut-out''s, etc.) or openings and recesses.\X2\000A000A\X0\Total net surface area of the element after applying product-level geometric features such as openings and projections.',.Q_AREA.,$,$,$,$,$,$,.READWRITE.);
@@ -5,20 +5,20 @@ FILE_NAME('Psets_BBIM_Annotation.ifc','2020-01-01T00:00:00',(),(),'Psets_BBIM_An
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#2,#3,#4));
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#2,#3,#4));
#2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','Classes separarated by spaces that end up in classes for this element in svg. Can be used to specify the text font size: small - 1.8mm; regular - 2.5mm; large - 3.5mm; header - 5mm; title - 7mm. By default regular size is used.',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/BATTING,IfcTypeProduct',(#6,#7));
#5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#6,#7));
#6=IFCSIMPLEPROPERTYTEMPLATE('0t2LEesGT1QRQtrIZUAR8L',$,'Thickness','Batting thickness',.P_SINGLEVALUE.,'IfcPositiveLengthMeasure',$,$,$,$,$,.READWRITE.);
#7=IFCSIMPLEPROPERTYTEMPLATE('082PndS6v2kBOiJoSboMnh',$,'Reverse pattern direction','Reverse batting pattern (swap starting and ending points)',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#8=IFCPROPERTYSETTEMPLATE('1Dx2EiZnP67xotInXpwz90',$,'BBIM_Section','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/SECTION,IfcTypeProduct',(#9,#10,#11,#12,#13));
#8=IFCPROPERTYSETTEMPLATE('1Dx2EiZnP67xotInXpwz90',$,'BBIM_Section','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#9,#10,#11,#12,#13));
#9=IFCSIMPLEPROPERTYTEMPLATE('2a_9s8spHDc9dZHtgg71XL',$,'ShowStartArrow','Display start arrow.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('3i6SH_GbT7zhKea$wVE56A',$,'StartArrowSymbol','Custom symbol for the start of the section marker arrow. Need to make sure it''s present in "symbols.svg".',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#11=IFCSIMPLEPROPERTYTEMPLATE('1DtsPn5a9FG8$zXHDDMavY',$,'ShowEndArrow','Display end arrow.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#12=IFCSIMPLEPROPERTYTEMPLATE('2$6U0mLI9AiPRWeBabdY3u',$,'EndArrowSymbol','Custom symbol for the end of the section marker arrow. Need to make sure it''s present in "symbols.svg".',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#13=IFCSIMPLEPROPERTYTEMPLATE('1naFqntIL7igCY7hCaE7kq',$,'HasConnectedSectionLine','Connect or disconnect section markers with line (by default = True).',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#14=IFCPROPERTYSETTEMPLATE('3V8oZ8YRD3_O7uR5vcUleS',$,'BBIM_Documentation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcProject',(#15,#16,#17,#18,#19,#20,#21,#22,#23));
#14=IFCPROPERTYSETTEMPLATE('3V8oZ8YRD3_O7uR5vcUleS',$,'BBIM_Documentation','',.PSET_OCCURRENCEDRIVEN.,'IfcProject',(#15,#16,#17,#18,#19,#20,#21,#22,#23));
#15=IFCSIMPLEPROPERTYTEMPLATE('0ulAhgk3v9qfGlDILauR6J',$,'SheetsDir','Default sheets directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#16=IFCSIMPLEPROPERTYTEMPLATE('2yvlVKiQXASucfH40deCvu',$,'LayoutsDir','Default layouts directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#17=IFCSIMPLEPROPERTYTEMPLATE('2kXZqXicL3jRwsOnLM_0ho',$,'TitleblocksDir','Default titleblocks directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
@@ -28,7 +28,7 @@ DATA;
#21=IFCSIMPLEPROPERTYTEMPLATE('1UDakJ5_f7kBhggNSW4$h5',$,'SymbolsPath','Default symbols SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#22=IFCSIMPLEPROPERTYTEMPLATE('0d53LEtgLDQxnv__NfgH7i',$,'PatternsPath','Default patterns SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#23=IFCSIMPLEPROPERTYTEMPLATE('26qFNMv7nCHgU6Jd7Anga5',$,'ShadingStylesPath','Default shading styles',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcTypeProduct',(#25,#26,#27,#28));
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#25,#26,#27,#28));
#25=IFCSIMPLEPROPERTYTEMPLATE('1rL2AbQsXD8RbpoWH5pYOV',$,'ShowDescriptionOnly','Hide the measurement values and show only annotation description',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#26=IFCSIMPLEPROPERTYTEMPLATE('0SVyOfB0rC2xNfdRYf3XvY',$,'SuppressZeroInches','Suppress 0 inch values in dimension annotation text (for example: 12'' - 0" -> 12'')',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#27=IFCSIMPLEPROPERTYTEMPLATE('2bUmj458PBqPAtUoI3MXsb',$,'TextPrefix','Text to add before annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
File diff suppressed because one or more lines are too long
Binary file not shown.
+14 -21
View File
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import os
import bpy
import json
@@ -28,7 +27,6 @@ import addon_utils
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.placement
import ifcopenshell.util.unit
import blenderbim.tool as tool
import blenderbim.core.geometry
import blenderbim.core.aggregate
@@ -36,8 +34,6 @@ import blenderbim.core.spatial
import blenderbim.core.style
from blenderbim.bim.ifc import IfcStore
from mathutils import Vector
from typing import Union
from logging import Logger
class IfcExporter:
@@ -89,8 +85,8 @@ class IfcExporter:
self.get_application_name(), tool.Blender.get_blenderbim_version()
)
def sync_all_objects(self, skip_unlinking=False) -> list[ifcopenshell.entity_instance]:
results: list[ifcopenshell.entity_instance] = []
def sync_all_objects(self):
results = []
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
for ifc_definition_id in list(IfcStore.id_map.keys()):
obj = IfcStore.id_map[ifc_definition_id]
@@ -99,21 +95,19 @@ class IfcExporter:
continue
if obj.library:
continue
tool.Collector.sync(obj, skip_unlinking)
tool.Collector.sync(obj)
result = self.sync_object_placement(obj)
if result:
results.append(result)
result = self.sync_object_material(obj)
# TODO: sync_object_material always returns None
# so it's never really appended
if result:
results.append(result)
except ReferenceError:
pass # The object is likely deleted
return results
def sync_edited_objects(self) -> list[ifcopenshell.entity_instance]:
results: list[ifcopenshell.entity_instance] = []
def sync_edited_objects(self):
results = []
for obj in IfcStore.edited_objs.copy():
if not obj:
continue
@@ -131,7 +125,7 @@ class IfcExporter:
IfcStore.edited_objs.clear()
return results
def sync_object_material(self, obj: bpy.types.Object) -> None:
def sync_object_material(self, obj):
if not obj.data or not isinstance(obj.data, bpy.types.Mesh):
return
if not self.has_changed_materials(obj):
@@ -142,10 +136,11 @@ class IfcExporter:
checksum = obj.data.BIMMeshProperties.material_checksum
return checksum != str([s.id() for s in tool.Geometry.get_styles(obj) if s])
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
def sync_object_placement(self, obj):
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
if not tool.Ifc.is_moved(obj):
return
blender_matrix = np.array(obj.matrix_world)
if (obj.scale - Vector((1.0, 1.0, 1.0))).length > 1e-4:
bpy.ops.bim.update_representation(obj=obj.name)
return element
@@ -156,7 +151,7 @@ class IfcExporter:
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
return element
def sync_grid_axis_object_placement(self, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
def sync_grid_axis_object_placement(self, obj, element):
grid = (element.PartOfU or element.PartOfV or element.PartOfW)[0]
grid_obj = tool.Ifc.get_object(grid)
if grid_obj:
@@ -165,10 +160,10 @@ class IfcExporter:
bpy.ops.bim.update_representation(obj=obj.name)
tool.Geometry.record_object_position(obj)
def get_application_name(self) -> str:
def get_application_name(self):
return "BlenderBIM"
def get_application_version(self) -> str:
def get_application_version(self):
version = ".".join(
[
str(x)
@@ -186,13 +181,11 @@ class IfcExporter:
class IfcExportSettings:
def __init__(self):
self.logger: Logger = None
self.output_file: str = None
self.json_version: str = None
self.json_compact: bool = None
self.logger = None
self.output_file = None
@staticmethod
def factory(context: bpy.types.Context, output_file: str, logger: Logger) -> IfcExportSettings:
def factory(context, output_file, logger):
settings = IfcExportSettings()
settings.output_file = output_file
settings.logger = logger
+69 -28
View File
@@ -31,7 +31,6 @@ from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.model.workspace import LIST_OF_TOOLS, TOOLS_TO_CLASSES_MAP
from mathutils import Vector
from math import cos, degrees
from typing import Union
cwd = os.path.dirname(os.path.realpath(__file__))
@@ -192,8 +191,6 @@ def refresh_ui_data():
if isinstance(tool.Ifc.get(), ifcopenshell.sqlite):
tool.Ifc.get().clear_cache()
bpy.context.scene.DocProperties.should_draw_decorations = bpy.context.scene.DocProperties.should_draw_decorations
@persistent
def loadIfcStore(scene):
@@ -223,7 +220,7 @@ def redo_post(scene):
tool.Ifc.rebuild_element_maps()
def get_application(ifc: ifcopenshell.file) -> ifcopenshell.entity_instance:
def get_application(ifc):
# TODO: cache this for even faster application retrieval. It honestly makes a difference on long scripts.
version = get_application_version()
for element in ifc.by_type("IfcApplication"):
@@ -238,20 +235,7 @@ def get_application(ifc: ifcopenshell.file) -> ifcopenshell.entity_instance:
)
def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None]:
# TODO: cache this for even faster application retrieval. It honestly makes a difference on long scripts.
if pao := next(iter(ifc.by_type("IfcPersonAndOrganization")), None):
return pao
elif ifc.schema == "IFC2X3":
if (person := next(iter(ifc.by_type("IfcPerson")), None)) is None:
person = tool.Ifc.run("owner.add_person")
if (organization := next(iter(ifc.by_type("IfcOrganization")), None)) is None:
organization = tool.Ifc.run("owner.add_organisation")
pao = tool.Ifc.run("owner.add_person_and_organisation", person=person, organisation=organization)
return pao
def get_application_version() -> str:
def get_application_version():
return ".".join(
[
str(x)
@@ -270,6 +254,60 @@ def viewport_shading_changed_callback(area):
bpy.context.scene.BIMStylesProperties.active_style_type = "External"
@classmethod
def poll_check_blender_tab(cls, context):
return tool.Blender.is_tab(context, "BLENDER")
def get_override_scene_panel(panel_name):
original_panel = getattr(bpy.types, panel_name)
override_panel = type(f"Override_{panel_name}", (original_panel,), {})
override_panel.bl_idname = f"{panel_name}_override"
# some blender panel depend on other and register_class will throw an error
# if we won't override their `bl_parent_id`
bl_parent_id = getattr(override_panel, "bl_parent_id", None)
if bl_parent_id is not None:
override_panel.bl_parent_id = f"{bl_parent_id}_override"
# override poll method
poll = getattr(override_panel, "poll", None)
if poll is None:
override_panel.poll = poll_check_blender_tab
else:
@classmethod
def wrapped_poll(cls, context):
return super(override_panel, cls).poll(context) and poll_check_blender_tab.__func__(cls, context)
override_panel.poll = wrapped_poll
return override_panel
# TODO: possibly override scene panels from other addons too?
# list can be updated from
# https://projects.blender.org/blender/blender/src/branch/main/scripts/startup/bl_ui/properties_scene.py#L421
OVERRIDE_SCENE_PANELS = (
"SCENE_PT_scene",
"SCENE_PT_unit",
"SCENE_PT_physics",
"SCENE_PT_rigid_body_world",
"SCENE_PT_rigid_body_world_settings",
"SCENE_PT_rigid_body_cache",
"SCENE_PT_rigid_body_field_weights",
"SCENE_PT_audio",
"SCENE_PT_keying_sets",
"SCENE_PT_custom_props",
# after SCENE_PT_keying_sets
"SCENE_PT_keying_set_paths",
"SCENE_PT_keyframing_settings",
)
if bpy.app.version >= (4, 0):
OVERRIDE_SCENE_PANELS += ("SCENE_PT_simulation",)
@persistent
def load_post(scene):
global global_subscription_owner
@@ -292,7 +330,7 @@ def load_post(scene):
key=key, owner=global_subscription_owner, args=(area,), notify=viewport_shading_changed_callback
)
ifcopenshell.api.owner.settings.get_user = get_user
ifcopenshell.api.owner.settings.get_user = lambda ifc: core_owner.get_user(tool.Owner)
ifcopenshell.api.owner.settings.get_application = get_application
AuthoringData.type_thumbnails = {}
@@ -301,16 +339,19 @@ def load_post(scene):
if bpy.context.preferences.addons["blenderbim"].preferences.should_setup_workspace:
if "BIM" in bpy.data.workspaces:
if bpy.context.preferences.addons["blenderbim"].preferences.activate_workspace:
bpy.context.window.workspace = bpy.data.workspaces["BIM"]
bpy.context.window.workspace = bpy.data.workspaces["BIM"]
else:
bpy.ops.workspace.append_activate(idname="BIM", filepath=os.path.join(cwd, "data", "workspace.blend"))
# To improve usability for new users, we hijack the scene properties
# tab. We override default scene properties panels with our own poll
# to hide them unless the user has chosen to view Blender properties.
for panel in tool.Blender.get_scene_panels_list():
if panel in blenderbim.bim.original_scene_panels_polls:
continue
tool.Blender.override_scene_panel(panel)
# To improve usability for new users, we hijack the scene properties
# tab. We override default scene properties panels with our own poll
# to hide them unless the user has chosen to view Blender properties.
for panel in OVERRIDE_SCENE_PANELS:
if panel in blenderbim.bim.overridden_scene_panels:
continue
override_panel = get_override_scene_panel(panel)
original_panel = getattr(bpy.types, panel)
bpy.utils.register_class(override_panel)
bpy.utils.unregister_class(original_panel)
blenderbim.bim.overridden_scene_panels[panel] = (original_panel, override_panel)
tool.Blender.setup_tabs()
+50 -18
View File
@@ -29,7 +29,6 @@ from mathutils import geometry
from mathutils import Vector
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from typing import Optional, Callable, Any, Union
def draw_attributes(props, layout, copy_operator=None, popup_active_attribute=None):
@@ -76,10 +75,6 @@ def draw_attribute(attribute, layout, copy_operator=None):
op.data_path = attribute.path_from_id("string_value")
if attribute.is_optional:
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
if attribute.name == "GlobalId":
layout.operator("bim.generate_global_id", icon="FILE_REFRESH", text="")
if copy_operator:
op = layout.operator(f"{copy_operator}", text="", icon="COPYDOWN")
op.name = attribute.name
@@ -91,11 +86,7 @@ def import_attributes(ifc_class, props, data, callback=None):
# A more elegant attribute importer signature, intended to supersede import_attributes
def import_attributes2(
element: Union[str, ifcopenshell.entity_instance],
props: bpy.types.PropertyGroup,
callback: Optional[Callable] = None,
) -> None:
def import_attributes2(element, props, callback=None):
if isinstance(element, str):
attributes = tool.Ifc.schema().declaration_by_name(element).as_entity().all_attributes()
info = {a.name(): None for a in attributes}
@@ -125,7 +116,7 @@ def import_attribute(attribute, props, data, callback=None):
elif is_handled_by_callback is False:
props.remove(len(props) - 1)
elif data_type == "string":
new.string_value = "" if new.is_null else str(data[attribute.name()]).replace("\n", "\\n")
new.string_value = "" if new.is_null else str(data[attribute.name()])
if attribute.type_of_attribute().declared_type().name() == "IfcURIReference":
new.is_uri = True
elif data_type == "boolean":
@@ -187,7 +178,7 @@ def add_attribute_description(attribute_blender, attribute_ifc=None):
attribute_blender.description = description
def export_attributes(props, callback: Optional[Callable] = None) -> dict[str, Any]:
def export_attributes(props, callback=None):
attributes = {}
for prop in props:
is_handled_by_callback = callback(attributes, prop) if callback else False
@@ -225,6 +216,48 @@ def get_enum_items(data, prop_name, context=None):
return items
def get_obj_ifc_definition_id(context, obj, obj_type):
if obj_type == "Object":
return bpy.data.objects.get(obj).BIMObjectProperties.ifc_definition_id
elif obj_type == "Material":
return bpy.data.materials.get(obj).BIMObjectProperties.ifc_definition_id
elif obj_type == "MaterialSet":
return ifcopenshell.util.element.get_material(
tool.Ifc.get_entity(bpy.data.objects.get(obj)), should_skip_usage=True
).id()
elif obj_type == "MaterialSetItem":
return bpy.data.objects.get(obj).BIMObjectMaterialProperties.active_material_set_item_id
elif obj_type == "Task":
return context.scene.BIMTaskTreeProperties.tasks[
context.scene.BIMWorkScheduleProperties.active_task_index
].ifc_definition_id
elif obj_type == "Cost":
return context.scene.BIMCostProperties.cost_items[
context.scene.BIMCostProperties.active_cost_item_index
].ifc_definition_id
elif obj_type == "Resource":
return context.scene.BIMResourceTreeProperties.resources[
context.scene.BIMResourceProperties.active_resource_index
].ifc_definition_id
elif obj_type == "Profile":
return context.scene.BIMProfileProperties.profiles[
context.scene.BIMProfileProperties.active_profile_index
].ifc_definition_id
elif obj_type == "WorkSchedule":
return context.scene.BIMWorkScheduleProperties.active_work_schedule_id
elif obj_type == "Group":
prop = context.scene.BIMGroupProperties
return prop.groups[prop.active_group_index].ifc_definition_id
# hack to close popup
# https://blender.stackexchange.com/a/202576/130742
def close_operator_panel(event):
x, y = event.mouse_x, event.mouse_y
bpy.context.window.cursor_warp(10, 10)
move_back = lambda: bpy.context.window.cursor_warp(x, y)
bpy.app.timers.register(move_back, first_interval=0.01)
def convert_property_group_from_si(property_group, skip_props=()):
"""Method converts property group values from si to current ifc project units
@@ -271,9 +304,6 @@ def draw_filter(layout, filter_groups, data, module):
op.type = sprops.facet
op.index = i
op.module = module
op = row.operator("bim.remove_filter_group", text="", icon="X")
op.index = i
op.module = module
for j, ifc_filter in enumerate(filter_group.filters):
if ifc_filter.type == "entity":
@@ -304,9 +334,6 @@ def draw_filter(layout, filter_groups, data, module):
elif ifc_filter.type == "group":
row = box.row(align=True)
row.prop(ifc_filter, "value", text="", icon="OUTLINER_COLLECTION")
elif ifc_filter.type == "parent":
row = box.row(align=True)
row.prop(ifc_filter, "value", text="", icon="FILE_PARENT")
elif ifc_filter.type == "query":
row = box.row(align=True)
row.prop(ifc_filter, "name", text="", icon="POINTCLOUD_DATA")
@@ -323,6 +350,11 @@ def draw_filter(layout, filter_groups, data, module):
op.index = j
op.module = module
row = box.row()
op = row.operator("bim.remove_filter_group", icon="X")
op.index = i
op.module = module
# TODO this should move into ifcopenshell.util
+37 -59
View File
@@ -19,46 +19,37 @@
import os
import bpy
import uuid
import shutil
import hashlib
import zipfile
import tempfile
import traceback
import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper
import blenderbim
import blenderbim.bim.handler
import blenderbim.tool as tool
from pathlib import Path
from blenderbim.tool.brick import BrickStore
from typing import Set, Union, Optional
IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
class IfcStore:
path: str = ""
file: Optional[ifcopenshell.file] = None
schema: Optional[ifcopenshell.ifcopenshell_wrapper.schema_definition] = None
cache: Optional[ifcopenshell.ifcopenshell_wrapper.HdfSerializer] = None
cache_path: Optional[str] = None
id_map: dict[int, IFC_CONNECTED_TYPE] = {}
guid_map: dict[str, IFC_CONNECTED_TYPE] = {}
edited_objs: Set[bpy.types.Object] = set()
pset_template_path: str = ""
pset_template_file: Optional[ifcopenshell.file] = None
classification_path: str = ""
classification_file: Optional[ifcopenshell.file] = None
library_path: str = ""
library_file: Optional[ifcopenshell.file] = None
path = ""
file = None
schema = None
cache = None
cache_path = None
id_map = {}
guid_map = {}
edited_objs = set()
pset_template_path = ""
pset_template_file = None
classification_path = ""
classification_file = None
library_path = ""
library_file = None
current_transaction = ""
last_transaction = ""
history = []
future = []
schema_identifiers = ["IFC4", "IFC2X3", "IFC4X3"]
session_files: dict[str, ifcopenshell.file] = {}
session_files = {}
@staticmethod
def purge():
@@ -89,8 +80,8 @@ class IfcStore:
if IfcStore.path:
try:
IfcStore.load_file(IfcStore.path)
except Exception as e:
print(f"Failed to load file {IfcStore.path}. Error details: {e}")
except:
pass
return IfcStore.file
@staticmethod
@@ -122,17 +113,11 @@ class IfcStore:
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
new_cache_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
IfcStore.cache = None
try:
shutil.move(IfcStore.cache_path, new_cache_path)
except PermissionError:
try:
shutil.copy2(IfcStore.cache_path, new_cache_path)
except PermissionError:
pass # Well we tried. No cache for you!
os.replace(IfcStore.cache_path, new_cache_path)
IfcStore.get_cache()
@staticmethod
def load_file(path) -> None:
def load_file(path):
if not os.path.isfile(path):
return
extension = path.split(".")[-1]
@@ -161,7 +146,7 @@ class IfcStore:
return IfcStore.schema
@staticmethod
def get_element(id_or_guid: Union[int, str]) -> IFC_CONNECTED_TYPE:
def get_element(id_or_guid):
if isinstance(id_or_guid, int):
map_object = IfcStore.id_map
else:
@@ -174,7 +159,7 @@ class IfcStore:
return obj
@staticmethod
def relink_all_objects() -> None:
def relink_all_objects():
if not IfcStore.get_file():
return
for obj in bpy.data.objects:
@@ -187,7 +172,7 @@ class IfcStore:
IfcStore.relink_object(obj)
@staticmethod
def relink_object(obj: IFC_CONNECTED_TYPE) -> None:
def relink_object(obj):
if not obj:
return
if obj.BIMObjectProperties.ifc_definition_id:
@@ -208,7 +193,7 @@ class IfcStore:
IfcStore.commit_link_element(data)
@staticmethod
def link_element(element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None:
def link_element(element, obj):
# Please use tool.Ifc.link() instead of this method. We want to
# refactor this class and deprecate usage of IfcStore in favour of
# tools.
@@ -222,7 +207,7 @@ class IfcStore:
elif existing_obj:
try:
existing_obj.name
IfcStore.unlink_element(element=element, obj=existing_obj)
IfcStore.unlink_element(obj=existing_obj)
except:
pass
IfcStore.id_map[element.id()] = obj
@@ -274,7 +259,7 @@ class IfcStore:
# TODO We're handling id_map and guid_map, but what about edited_objs? This might cause big problems.
@staticmethod
def rollback_unlink_element(data) -> None:
def rollback_unlink_element(data):
if "id" not in data or "obj" not in data:
return
obj = bpy.data.objects.get(data["obj"])
@@ -283,15 +268,13 @@ class IfcStore:
IfcStore.guid_map[data["guid"]] = obj
@staticmethod
def commit_unlink_element(data) -> None:
def commit_unlink_element(data):
del IfcStore.id_map[data["id"]]
if data["guid"]:
del IfcStore.guid_map[data["guid"]]
@staticmethod
def unlink_element(
element: Optional[ifcopenshell.entity_instance] = None, obj: Optional[IFC_CONNECTED_TYPE] = None
) -> None:
def unlink_element(element=None, obj=None):
if element is None:
try:
element = tool.Ifc.get_entity(obj)
@@ -337,8 +320,7 @@ class IfcStore:
)
@staticmethod
def execute_ifc_operator(operator: bpy.types.Operator, context: bpy.types.Context, is_invoke=False):
blenderbim.last_actions.append({"type": "operator", "name": operator.bl_idname})
def execute_ifc_operator(operator, context, is_invoke=False):
bpy.context.scene.BIMProperties.is_dirty = True
is_top_level_operator = not bool(IfcStore.current_transaction)
@@ -353,14 +335,10 @@ class IfcStore:
else:
operator.transaction_key = IfcStore.current_transaction
try:
if is_invoke:
result = getattr(operator, "_invoke")(context, None)
else:
result = getattr(operator, "_execute")(context)
except:
blenderbim.last_error = traceback.format_exc()
raise
if is_invoke:
result = getattr(operator, "_invoke")(context, None)
else:
result = getattr(operator, "_execute")(context)
if is_top_level_operator:
if tool.Ifc.get():
@@ -376,17 +354,17 @@ class IfcStore:
return result
@staticmethod
def begin_transaction(operator: bpy.types.Operator) -> None:
def begin_transaction(operator):
IfcStore.current_transaction = str(uuid.uuid4())
operator.transaction_key = IfcStore.current_transaction
@staticmethod
def end_transaction(operator: bpy.types.Operator) -> None:
def end_transaction(operator):
IfcStore.current_transaction = ""
operator.transaction_key = ""
@staticmethod
def add_transaction_operation(operator: bpy.types.Operator, rollback=None, commit=None) -> None:
def add_transaction_operation(operator, rollback=None, commit=None):
key = getattr(operator, "transaction_key", None)
data = getattr(operator, "transaction_data", None)
bpy.context.scene.BIMProperties.last_transaction = key
@@ -402,7 +380,7 @@ class IfcStore:
IfcStore.future = []
@staticmethod
def undo(until_key=None) -> None:
def undo(until_key=None):
BrickStore.undo()
if not IfcStore.history:
return
@@ -417,7 +395,7 @@ class IfcStore:
IfcStore.future.append(event)
@staticmethod
def redo(until_key=None) -> None:
def redo(until_key=None):
BrickStore.redo()
if not IfcStore.future:
+106 -203
View File
@@ -26,37 +26,29 @@ import bmesh
import logging
import mathutils
import numpy as np
import numpy.typing as npt
import multiprocessing
import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.geolocation
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.shape
import blenderbim.tool as tool
import blenderbim.core.spatial
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
from itertools import chain, accumulate
from blenderbim.bim.ifc import IfcStore, IFC_CONNECTED_TYPE
from blenderbim.tool.loader import OBJECT_DATA_TYPE
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.drawing.prop import ANNOTATION_TYPES_DATA
from typing import Dict, Union, Optional, Any
class MaterialCreator:
def __init__(self, ifc_import_settings: IfcImportSettings, ifc_importer: IfcImporter):
self.mesh: bpy.types.Mesh = None
self.obj: bpy.types.Object = None
self.materials: Dict[int, bpy.types.Material] = {}
self.styles: Dict[int, bpy.types.Material] = {}
self.parsed_meshes: set[str] = set()
self.mesh = None
self.obj = None
self.materials = {}
self.styles = {}
self.parsed_meshes = set()
self.ifc_import_settings = ifc_import_settings
self.ifc_importer = ifc_importer
def create(self, element: ifcopenshell.entity_instance, obj: bpy.types.Object, mesh: bpy.types.Mesh) -> None:
def create(self, element, obj, mesh):
self.mesh = mesh
# as ifcopenshell triangulates the mesh, we need to merge it to quads again
self.obj = obj
@@ -72,7 +64,7 @@ class MaterialCreator:
self.assign_material_slots_to_faces()
tool.Geometry.record_object_materials(obj)
def add_default_material(self, element: ifcopenshell.entity_instance) -> None:
def add_default_material(self, element):
element_material = ifcopenshell.util.element.get_material(element)
if not element_material:
return
@@ -90,17 +82,14 @@ class MaterialCreator:
self.mesh.materials.append(self.materials[material.id()])
return
def load_existing_materials(self) -> None:
def load_existing_materials(self):
for material in bpy.data.materials:
if material.BIMObjectProperties.ifc_definition_id:
self.materials[material.BIMObjectProperties.ifc_definition_id] = material
if material.BIMMaterialProperties.ifc_style_id:
self.styles[material.BIMMaterialProperties.ifc_style_id] = material
def parse_representations(self, element: ifcopenshell.entity_instance) -> bool:
"""Search for styles in in all `element`'s representation items
and adds them to `self.mesh.materials`.\n
Returns `True` if any styles were found and added, returns `False` otherwise."""
def parse_representations(self, element):
has_parsed = False
if hasattr(element, "Representation"):
for representation in element.Representation.Representations:
@@ -114,7 +103,7 @@ class MaterialCreator:
has_parsed = True
return has_parsed
def parse_representation(self, representation: ifcopenshell.entity_instance) -> bool:
def parse_representation(self, representation):
has_parsed = False
representation_items = self.resolve_all_stylable_representation_items(representation)
for item in representation_items:
@@ -122,9 +111,9 @@ class MaterialCreator:
has_parsed = True
return has_parsed
def parse_representation_item(self, item: ifcopenshell.entity_instance) -> bool:
def parse_representation_item(self, item):
if not item.StyledByItem:
return False
return
style_ids = []
styles = list(item.StyledByItem[0].Styles)
while styles:
@@ -134,12 +123,11 @@ class MaterialCreator:
elif style.is_a("IfcPresentationStyleAssignment"):
styles.extend(style.Styles)
if not style_ids:
return False
return
for style_id in style_ids:
material = self.styles[style_id]
def get_ifc_coordinate(material: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
"""returns IfcTextureCoordinate"""
def get_ifc_coordinate(material):
texture_style = tool.Style.get_texture_style(material)
if not texture_style:
return
@@ -160,7 +148,7 @@ class MaterialCreator:
self.mesh.materials.append(material)
return True
def assign_material_slots_to_faces(self) -> None:
def assign_material_slots_to_faces(self):
if "ios_materials" not in self.mesh or not self.mesh["ios_materials"]:
return
if len(self.mesh.materials) == 1:
@@ -168,12 +156,8 @@ class MaterialCreator:
material_to_slot = {}
if len(self.mesh.polygons) == len(self.mesh["ios_material_ids"]):
for i, style_or_material_id in enumerate(self.mesh["ios_materials"]):
if style_or_material_id in self.styles:
blender_material = self.styles[style_or_material_id]
else:
blender_material = self.materials[style_or_material_id]
slot_index = self.mesh.materials.find(blender_material.name)
for i, material in enumerate(self.mesh["ios_materials"]):
slot_index = self.mesh.materials.find(self.styles[material].name)
material_to_slot[i] = slot_index
material_index = [
@@ -181,10 +165,7 @@ class MaterialCreator:
]
self.mesh.polygons.foreach_set("material_index", material_index)
def resolve_all_stylable_representation_items(
self, representation: ifcopenshell.entity_instance
) -> list[ifcopenshell.entity_instance]:
"""returns list of resolved IfcRepresentationItems"""
def resolve_all_stylable_representation_items(self, representation):
items = []
for item in representation.Items:
if item.is_a("IfcMappedItem"):
@@ -202,30 +183,24 @@ class MaterialCreator:
class IfcImporter:
def __init__(self, ifc_import_settings: IfcImportSettings):
def __init__(self, ifc_import_settings):
self.ifc_import_settings = ifc_import_settings
self.diff = None
self.file: ifcopenshell.file = None
self.context_settings: list[ifcopenshell.geom.main.settings] = []
self.gross_context_settings: list[ifcopenshell.geom.main.settings] = []
self.file = None
self.context_settings = []
self.gross_context_settings = []
self.contexts = []
self.project = None
self.has_existing_project = False
# element guids to blender collections mapping
self.collections: dict[str, bpy.types.Collection] = {}
self.elements: set[ifcopenshell.entity_instance] = set()
self.annotations: set[ifcopenshell.entity_instance] = set()
self.gross_elements: set[ifcopenshell.entity_instance] = set()
self.element_types: set[ifcopenshell.entity_instance] = set()
self.spatial_elements: set[ifcopenshell.entity_instance] = set()
self.type_collection: bpy.types.Collection = None
self.collections = {}
self.elements = set()
self.type_collection = None
self.type_products = {}
self.meshes = {}
self.mesh_shapes = {}
self.time = 0
self.unit_scale = 1
# ifc definition ids to blender elements mapping
self.added_data: dict[int, IFC_CONNECTED_TYPE] = {}
self.added_data = {}
self.native_elements = set()
self.native_data = {}
self.progress = 0
@@ -301,27 +276,25 @@ class IfcImporter:
if self.ifc_import_settings.should_setup_viewport_camera:
self.setup_viewport_camera()
self.setup_arrays()
self.profile_code("Setup arrays")
blenderbim.core.spatial.import_spatial_decomposition(tool.Spatial)
self.update_progress(100)
bpy.context.window_manager.progress_end()
def is_element_far_away(self, element: ifcopenshell.entity_instance) -> bool:
def is_element_far_away(self, element):
try:
placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
point = placement[:, 3][0:3]
return self.is_point_far_away(point, is_meters=False)
except:
return False
pass
def is_point_far_away(
self, point: Union[ifcopenshell.entity_instance, npt.NDArray[np.float64]], is_meters: bool = True
) -> bool:
def is_point_far_away(self, point, is_meters=True):
# Locations greater than 1km are not considered "small sites" according to the georeferencing guide
# Users can configure this if they have to handle larger sites but beware of surveying precision
limit = self.ifc_import_settings.distance_limit
limit = limit if is_meters else (limit / self.unit_scale)
coords = getattr(point, "Coordinates", point)
coords = point
if hasattr(point, "Coordinates"):
coords = point.Coordinates
return abs(coords[0]) > limit or abs(coords[1]) > limit or abs(coords[2]) > limit
def process_context_filter(self):
@@ -435,7 +408,7 @@ class IfcImporter:
self.annotations = set([a for a in self.file.by_type("IfcAnnotation")])
self.annotations -= drawing_annotations
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")]
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement")]
if self.ifc_import_settings.is_coordinating:
self.elements = [e for e in self.elements if e.Representation]
@@ -466,9 +439,7 @@ class IfcImporter:
for element in self.gross_elements:
print(element)
def get_spatial_elements_filtered_by_elements(
self, elements: set[ifcopenshell.entity_instance]
) -> set[ifcopenshell.entity_instance]:
def get_spatial_elements_filtered_by_elements(self, elements):
leaf_spatial_elements = set([ifcopenshell.util.element.get_container(e) for e in elements])
results = set()
for spatial_element in leaf_spatial_elements:
@@ -606,14 +577,9 @@ class IfcImporter:
return products
def predict_dense_mesh(self):
if self.ifc_import_settings.should_use_native_meshes:
return
threshold = 10000 # Just from experience.
# The check for CfsFaces/Faces/CoordIndex accommodates invalid data from Cadwork
# 0 IfcClosedShell.CfsFaces
faces = [len(faces) for e in self.file.by_type("IfcClosedShell") if (faces := e[0])]
faces = [len(e.CfsFaces) for e in self.file.by_type("IfcClosedShell")]
if faces and max(faces) > threshold:
self.ifc_import_settings.should_use_native_meshes = True
return
@@ -621,14 +587,12 @@ class IfcImporter:
if self.file.schema == "IFC2X3":
return
# 2 IfcPolygonalFaceSet.Faces
faces = [len(faces) for e in self.file.by_type("IfcPolygonalFaceSet") if (faces := e[2])]
faces = [len(e.Faces) for e in self.file.by_type("IfcPolygonalFaceSet")]
if faces and max(faces) > threshold:
self.ifc_import_settings.should_use_native_meshes = True
return
# 3 IfcTriangulatedFaceSet.CoordIndex
faces = [len(index) for e in self.file.by_type("IfcTriangulatedFaceSet") if (index := e[3])]
faces = [len(e.CoordIndex) for e in self.file.by_type("IfcTriangulatedFaceSet")]
if faces and max(faces) > threshold:
self.ifc_import_settings.should_use_native_meshes = True
@@ -686,7 +650,7 @@ class IfcImporter:
props.blender_orthogonal_height = str(offset_point[2])
props.has_blender_offset = True
def get_offset_point(self) -> Union[npt.NDArray[np.float64], None]:
def get_offset_point(self):
elements_checked = 0
# If more than these elements aren't far away, the file probably isn't absolutely positioned
element_checking_threshold = 10
@@ -709,19 +673,17 @@ class IfcImporter:
mat = np.array(
([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1])
)
point = mat @ np.array(
point = np.array(
(
shape.geometry.verts[0],
shape.geometry.verts[1],
shape.geometry.verts[2],
shape.geometry.verts[0] / self.unit_scale,
shape.geometry.verts[1] / self.unit_scale,
shape.geometry.verts[2] / self.unit_scale,
0.0,
)
)
point = point / self.unit_scale
if self.is_point_far_away(point, is_meters=False):
return point
return mat @ point
def does_element_likely_have_geometry_far_away(self, element: ifcopenshell.entity_instance) -> bool:
def does_element_likely_have_geometry_far_away(self, element):
for representation in element.Representation.Representations:
items = []
for item in representation.Items:
@@ -738,14 +700,13 @@ class IfcImporter:
if subelement.is_a("IfcCartesianPoint"):
if len(subelement.Coordinates) == 3 and self.is_point_far_away(subelement, is_meters=False):
return True
return False
def apply_blender_offset_to_matrix_world(self, obj: bpy.types.Object, matrix: np.ndarray) -> mathutils.Matrix:
def apply_blender_offset_to_matrix_world(self, obj, matrix):
props = bpy.context.scene.BIMGeoreferenceProperties
if props.has_blender_offset:
if obj.data and obj.data.get("has_cartesian_point_offset", None):
obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT"
elif self.is_point_far_away((matrix[:3, 3])):
elif self.is_point_far_away((matrix[0, 3], matrix[1, 3], matrix[2, 3])):
obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT"
matrix = ifcopenshell.util.geolocation.global2local(
matrix,
@@ -758,9 +719,7 @@ class IfcImporter:
return mathutils.Matrix(matrix.tolist())
def find_decomposed_ifc_class(
self, element: ifcopenshell.entity_instance, ifc_class: str
) -> Union[ifcopenshell.entity_instance, None]:
def find_decomposed_ifc_class(self, element, ifc_class):
if element.is_a(ifc_class):
return element
rel_aggregates = element.IsDecomposedBy
@@ -816,7 +775,7 @@ class IfcImporter:
continue
self.create_element_type(element_type)
def create_element_type(self, element: ifcopenshell.entity_instance) -> None:
def create_element_type(self, element):
self.ifc_import_settings.logger.info("Creating object %s", element)
mesh = None
if self.ifc_import_settings.should_load_geometry:
@@ -873,22 +832,17 @@ class IfcImporter:
self.create_product(element, mesh=mesh)
print("Done creating geometry")
def create_spatial_elements(self) -> None:
if bpy.context.preferences.addons["blenderbim"].preferences.spatial_elements_unselectable:
self.create_generic_elements(self.spatial_elements, unselectable=True)
else:
self.create_generic_elements(self.spatial_elements, unselectable=False)
def create_spatial_elements(self):
self.create_generic_elements(self.spatial_elements)
def create_elements(self) -> None:
def create_elements(self):
self.create_generic_elements(self.elements)
tmp = self.context_settings
self.context_settings = self.gross_context_settings
self.create_generic_elements(self.gross_elements)
self.context_settings = tmp
def create_generic_shape(
self, element: ifcopenshell.entity_instance
) -> Union[ifcopenshell_wrapper.TriangulationElement, None]:
def create_generic_shape(self, element):
for settings in self.context_settings:
try:
result = ifcopenshell.geom.create_shape(settings, element)
@@ -897,7 +851,7 @@ class IfcImporter:
except:
pass
def create_generic_elements(self, elements: set[ifcopenshell.entity_instance], unselectable=False) -> None:
def create_generic_elements(self, elements):
if isinstance(self.file, ifcopenshell.sqlite):
return self.create_generic_sqlite_elements(elements)
@@ -911,17 +865,12 @@ class IfcImporter:
elements -= products
total = len(elements)
objects = set()
for i, element in enumerate(elements):
if i % 250 == 0:
print("{} / {} elements processed ...".format(i, total))
objects.add(self.create_product(element))
self.create_product(element)
if unselectable:
for obj in objects:
obj.hide_select = True
def create_generic_sqlite_elements(self, elements: set[ifcopenshell.entity_instance]) -> None:
def create_generic_sqlite_elements(self, elements):
self.geometry_cache = self.file.get_geometry([e.id() for e in elements])
for geometry_id, geometry in self.geometry_cache["geometry"].items():
mesh_name = tool.Loader.get_mesh_name(type("Geometry", (), {"id": geometry_id}))
@@ -968,11 +917,7 @@ class IfcImporter:
mesh = self.meshes.get(mesh_name)
self.create_product(element, mesh=mesh)
def create_products(
self,
products: set[ifcopenshell.entity_instance],
settings: Optional[ifcopenshell.geom.main.settings] = None,
) -> set[ifcopenshell.entity_instance]:
def create_products(self, products, settings=None):
results = set()
if not products:
return results
@@ -1095,7 +1040,7 @@ class IfcImporter:
return mapped_representation
return None
def create_pointclouds(self, products: set[ifcopenshell.entity_instance]) -> set[ifcopenshell.entity_instance]:
def create_pointclouds(self, products):
result = set()
for product in products:
representation = self.get_pointcloud_representation(product)
@@ -1106,9 +1051,7 @@ class IfcImporter:
return result
def create_pointcloud(
self, product: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
def create_pointcloud(self, product, representation):
placement_matrix = self.get_element_matrix(product)
vertex_list = []
for item in representation.Items:
@@ -1136,12 +1079,7 @@ class IfcImporter:
self.link_element(product, obj)
return product
def create_product(
self,
element: ifcopenshell.entity_instance,
shape: Optional[Any] = None,
mesh: Optional[OBJECT_DATA_TYPE] = None,
) -> Union[bpy.types.Object, None]:
def create_product(self, element, shape=None, mesh=None):
if element is None:
return
@@ -1186,9 +1124,6 @@ class IfcImporter:
return obj
def load_existing_meshes(self) -> None:
self.meshes.update({m.name: m for m in bpy.data.meshes})
def get_representation_item_material_name(self, item):
if not item.StyledByItem:
return
@@ -1510,10 +1445,7 @@ class IfcImporter:
# Occurs when reloading a project
pass
project_collection = bpy.context.view_layer.layer_collection.children[self.project["blender"].name]
types_collection = project_collection.children[self.type_collection.name]
types_collection.hide_viewport = False
for obj in types_collection.collection.objects: # turn off all objects inside Types collection.
obj.hide_set(True)
project_collection.children[self.type_collection.name].hide_viewport = True
def clean_mesh(self):
obj = None
@@ -1598,19 +1530,18 @@ class IfcImporter:
"{}/{}".format(self.project["ifc"].is_a(), self.project["ifc"].Name)
)
obj = self.create_product(self.project["ifc"])
obj.hide_select = True
self.project["blender"].objects.link(obj)
self.project["blender"].BIMCollectionProperties.obj = obj
obj.BIMObjectProperties.collection = self.collections[project.GlobalId] = self.project["blender"]
def create_collections(self) -> None:
def create_collections(self):
self.create_spatial_decomposition_collections()
if self.ifc_import_settings.collection_mode == "DECOMPOSITION":
self.create_aggregate_and_nest_collections()
elif self.ifc_import_settings.collection_mode == "SPATIAL_DECOMPOSITION":
pass
def create_spatial_decomposition_collections(self) -> None:
def create_spatial_decomposition_collections(self):
for rel_aggregate in self.project["ifc"].IsDecomposedBy or []:
self.create_spatial_decomposition_collection(self.project["blender"], rel_aggregate.RelatedObjects)
@@ -1623,9 +1554,7 @@ class IfcImporter:
tool.Loader.create_project_collection("Views")
self.type_collection = tool.Loader.create_project_collection("Types")
def create_spatial_decomposition_collection(
self, parent: Union[bpy.types.Collection, bpy.types.Object], related_objects: list[ifcopenshell.entity_instance]
) -> None:
def create_spatial_decomposition_collection(self, parent, related_objects):
for element in related_objects:
if element not in self.spatial_elements:
continue
@@ -1648,34 +1577,26 @@ class IfcImporter:
if self.ifc_import_settings.has_filter:
rel_aggregates = set()
for element in self.elements:
if decomposed_by := element.IsDecomposedBy:
rel_aggregates.add(decomposed_by[0])
elif decomposes := element.Decomposes:
rel_aggregates.add(decomposes[0])
elif nested_by := getattr(element, "IsNestedBy", []): # IFC2X3 does not have IsNestedBy
if next((e for e in nested_by[0].RelatedObjects if not e.is_a("IfcPort")), None):
rel_aggregates.add(nested_by[0])
elif nests := getattr(element, "Nests", []):
rel_aggregates.add(nests[0])
elif element.is_a("IfcSurfaceFeature") and self.file.schema == "IFC4X3":
rel_aggregates.add(element.AdheresToElement[0])
if element.IsDecomposedBy:
rel_aggregates.add(element.IsDecomposedBy[0])
elif element.Decomposes:
rel_aggregates.add(element.Decomposes[0])
elif getattr(element, "IsNestedBy", []): # IFC2X3 does not have IsNestedBy
if [e for e in element.IsNestedBy[0].RelatedObjects if not e.is_a("IfcPort")]:
rel_aggregates.add(element.IsNestedBy[0])
elif getattr(element, "Nests", []):
rel_aggregates.add(element.Nests[0])
else:
rel_aggregates = [
r
for r in self.file.by_type("IfcRelAggregates")
if (relating_obj := r.RelatingObject).is_a("IfcElement") or relating_obj.is_a("IfcElementType")
if r.RelatingObject.is_a("IfcElement") or r.RelatingObject.is_a("IfcElementType")
] + [
r
for r in self.file.by_type("IfcRelNests")
if (
(relating_obj := r.RelatingObject).is_a("IfcElement")
or relating_obj.is_a("IfcElementType")
or (relating_obj.is_a("IfcPositioningElement") and not relating_obj.is_a("IfcGrid"))
)
if (r.RelatingObject.is_a("IfcElement") or r.RelatingObject.is_a("IfcElementType"))
and [e for e in r.RelatedObjects if not e.is_a("IfcPort")]
]
if self.file.schema == "IFC4X3":
rel_aggregates += [r for r in self.file.by_type("IfcRelAdheresToElement")]
if len(rel_aggregates) > 10000:
# More than 10,000 collections makes Blender unhappy
@@ -1683,11 +1604,9 @@ class IfcImporter:
self.ifc_import_settings.collection_mode = "SPATIAL_DECOMPOSITION"
return
aggregates: dict[str, dict] = {}
aggregates = {}
for rel_aggregate in rel_aggregates:
element: ifcopenshell.entity_instance = getattr(rel_aggregate, "RelatingObject", None) or getattr(
rel_aggregate, "RelatingElement"
)
element = rel_aggregate.RelatingObject
collection = bpy.data.collections.new(tool.Loader.get_name(element))
aggregates[element.GlobalId] = {"element": element, "collection": collection}
self.collections[element.GlobalId] = collection
@@ -1704,20 +1623,19 @@ class IfcImporter:
if aggregate["element"].is_a("IfcElementType"):
self.type_collection.children.link(aggregate["collection"])
continue
self.project["blender"].children.link(aggregate["collection"])
def create_materials(self) -> None:
def create_materials(self):
for material in self.file.by_type("IfcMaterial"):
self.create_material(material)
def create_material(self, material: ifcopenshell.entity_instance) -> bpy.types.Material:
def create_material(self, material):
blender_material = bpy.data.materials.new(material.Name)
self.link_element(material, blender_material)
self.material_creator.materials[material.id()] = blender_material
blender_material.use_fake_user = True
return blender_material
def create_styles(self) -> None:
def create_styles(self):
parsed_styles = set()
for material_definition_representation in self.file.by_type("IfcMaterialDefinitionRepresentation"):
@@ -1740,9 +1658,7 @@ class IfcImporter:
continue
self.create_style(style)
def create_style(
self, style: ifcopenshell.entity_instance, blender_material: Optional[bpy.types.Material] = None
) -> None:
def create_style(self, style, blender_material=None):
if not blender_material:
name = style.Name or str(style.id())
blender_material = bpy.data.materials.new(name)
@@ -1759,20 +1675,18 @@ class IfcImporter:
else:
blender_material.BIMStyleProperties.active_style_type = "Shading"
def place_objects_in_collections(self) -> None:
def place_objects_in_collections(self):
for ifc_definition_id, obj in self.added_data.items():
if isinstance(obj, bpy.types.Object):
self.place_object_in_collection(self.file.by_id(ifc_definition_id), obj)
def place_object_in_collection(self, element: ifcopenshell.entity_instance, obj: bpy.types.Object) -> None:
def place_object_in_collection(self, element, obj):
if self.ifc_import_settings.collection_mode == "DECOMPOSITION":
self.place_object_in_decomposition_collection(element, obj)
elif self.ifc_import_settings.collection_mode == "SPATIAL_DECOMPOSITION":
self.place_object_in_spatial_decomposition_collection(element, obj)
def place_object_in_decomposition_collection(
self, element: ifcopenshell.entity_instance, obj: bpy.types.Object
) -> None:
def place_object_in_decomposition_collection(self, element, obj):
if element.is_a("IfcProject"):
return
elif element.is_a("IfcGridAxis"):
@@ -1789,15 +1703,10 @@ class IfcImporter:
elif getattr(element, "Nests", None) and not element.is_a("IfcPort"):
nest = ifcopenshell.util.element.get_nest(element)
return self.collections[nest.GlobalId].objects.link(obj)
elif element.is_a("IfcSurfaceFeature") and self.file.schema == "IFC4X3":
adherend = element.AdheresToElement[0].RelatingElement
return self.collections[adherend.GlobalId].objects.link(obj)
else:
return self.place_object_in_spatial_decomposition_collection(element, obj)
return self.place_object_in_spatial_decomposition_collection(element, obj)
def place_object_in_spatial_decomposition_collection(
self, element: ifcopenshell.entity_instance, obj: bpy.types.Object
) -> None:
def place_object_in_spatial_decomposition_collection(self, element, obj):
if element.is_a("IfcProject"):
return
elif element.is_a("IfcGridAxis"):
@@ -1841,7 +1750,7 @@ class IfcImporter:
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING":
return rel.RelatingGroup
def get_element_matrix(self, element: ifcopenshell.entity_instance) -> np.ndarray:
def get_element_matrix(self, element):
if isinstance(element, ifcopenshell.sqlite_entity):
result = self.geometry_cache["shapes"][element.id()]["matrix"]
else:
@@ -1851,7 +1760,7 @@ class IfcImporter:
result[2][3] *= self.unit_scale
return result
def scale_matrix(self, matrix: np.array) -> np.array:
def scale_matrix(self, matrix):
matrix[0][3] *= self.unit_scale
matrix[1][3] *= self.unit_scale
matrix[2][3] *= self.unit_scale
@@ -1883,7 +1792,7 @@ class IfcImporter:
):
return representation.Items[0].MappingTarget
def create_curve(self, element: ifcopenshell.entity_instance, shape) -> bpy.types.Curve:
def create_curve(self, element, shape):
if hasattr(shape, "geometry"):
geometry = shape.geometry
else:
@@ -1908,13 +1817,12 @@ class IfcImporter:
polyline.points[-1].co = mathutils.Vector(v2)
return curve
def create_mesh(self, element: ifcopenshell.entity_instance, shape) -> bpy.types.Mesh:
def create_mesh(self, element, shape):
try:
if hasattr(shape, "geometry"):
# shape is ifcopenshell_wrapper.TriangulationElement
geometry: ifcopenshell_wrapper.Triangulation = shape.geometry
geometry = shape.geometry
else:
geometry: ifcopenshell_wrapper.Triangulation = shape
geometry = shape
mesh = bpy.data.meshes.new(tool.Loader.get_mesh_name(geometry))
@@ -1924,7 +1832,11 @@ class IfcImporter:
and geometry.verts
and self.is_point_far_away((geometry.verts[0], geometry.verts[1], geometry.verts[2]))
):
offset_point = np.array(
m = shape.transformation.matrix.data
mat = np.array(
([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1])
)
offset_point = np.linalg.inv(mat) @ np.array(
(
float(props.blender_eastings),
float(props.blender_northings),
@@ -1932,15 +1844,8 @@ class IfcImporter:
0.0,
)
)
if geometry != shape:
m = shape.transformation.matrix.data
mat = np.array(
([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1])
)
offset_point = np.linalg.inv(mat) @ offset_point
verts = [None] * len(geometry.verts)
for i in range(0, len(geometry.verts), 3):
# Note: this enh2xyz call is crazy slow.
verts[i], verts[i + 1], verts[i + 2] = ifcopenshell.util.geolocation.enh2xyz(
geometry.verts[i],
geometry.verts[i + 1],
@@ -1966,9 +1871,7 @@ class IfcImporter:
# See bug 3546
# ios_edges holds true edges that aren't triangulated.
#
# we do `.tolist()` because Blender can't assign `np.int32` to it's custom attributes
mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry).tolist()))
mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry)))
mesh.vertices.add(num_vertices)
mesh.vertices.foreach_set("co", verts)
@@ -1995,14 +1898,14 @@ class IfcImporter:
print(traceback.format_exc())
def a2p(self, o: mathutils.Vector, z: mathutils.Vector, x: mathutils.Vector) -> mathutils.Matrix:
def a2p(self, o, z, x):
y = z.cross(x)
r = mathutils.Matrix((x, y, z, o))
r.resize_4x4()
r.transpose()
return r
def get_axis2placement(self, plc: ifcopenshell.entity_instance) -> mathutils.Matrix:
def get_axis2placement(self, plc):
if plc.is_a("IfcAxis2Placement3D"):
z = mathutils.Vector(plc.Axis.DirectionRatios if plc.Axis else (0, 0, 1))
x = mathutils.Vector(plc.RefDirection.DirectionRatios if plc.RefDirection else (1, 0, 0))
@@ -2022,7 +1925,7 @@ class IfcImporter:
o = plc.LocalOrigin.Coordinates
return self.a2p(o, z, x)
def get_local_placement(self, plc: Optional[ifcopenshell.entity_instance] = None) -> mathutils.Matrix:
def get_local_placement(self, plc):
if plc is None:
return mathutils.Matrix()
if plc.PlacementRelTo is None:
@@ -2037,11 +1940,11 @@ class IfcImporter:
bpy.context.scene.BIMRootProperties.contexts = str(subcontext.id())
break
def link_element(self, element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None:
def link_element(self, element, obj):
self.added_data[element.id()] = obj
tool.Ifc.link(element, obj)
def set_matrix_world(self, obj: bpy.types.Object, matrix_world: mathutils.Matrix) -> None:
def set_matrix_world(self, obj, matrix_world):
obj.matrix_world = matrix_world
tool.Geometry.record_object_position(obj)
@@ -2064,7 +1967,7 @@ class IfcImporter:
class IfcImportSettings:
def __init__(self):
self.logger: logging.Logger = None
self.logger = None
self.input_file = None
self.diff_file = None
self.should_use_cpu_multiprocessing = True
@@ -2085,7 +1988,7 @@ class IfcImportSettings:
self.has_filter = None
self.should_filter_spatial_elements = True
self.should_setup_viewport_camera = True
self.elements: set[ifcopenshell.entity_instance] = set()
self.elements = set()
self.collection_mode = "DECOMPOSITION"
@staticmethod
@@ -28,11 +28,8 @@ classes = (
operator.BIM_OT_select_aggregate,
operator.BIM_OT_select_parts,
operator.BIM_OT_aggregate_unassign_object,
operator.BIM_OT_break_link_to_other_aggregates,
operator.BIM_OT_select_linked_aggregates,
prop.BIMObjectAggregateProperties,
ui.BIM_PT_aggregate,
ui.BIM_PT_linked_aggregate,
)
@@ -36,7 +36,7 @@ class AggregateData:
"relating_object_label": cls.get_relating_object_label(),
"has_related_objects": cls.has_related_objects(),
"total_parts": cls.total_parts(),
"total_linked_aggregate": cls.total_linked_aggregate(),
"ifc_class": cls.ifc_class(),
}
cls.is_loaded = True
@@ -68,24 +68,7 @@ class AggregateData:
return bool(cls.get_related_objects())
@classmethod
def total_linked_aggregate(cls):
def ifc_class(cls) -> str:
element = tool.Ifc.get_entity(bpy.context.active_object)
aggregate = ifcopenshell.util.element.get_aggregate(element)
if not aggregate:
return []
if not element:
return []
product_linked_agg_group = [
r
for r in getattr(aggregate, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
if not product_linked_agg_group:
return []
total = len(product_linked_agg_group[0].RelatedObjects)
return total
if element:
return element.is_a()
@@ -40,39 +40,31 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, Operator):
bl_label = "Assign Object To Aggregation"
bl_options = {"REGISTER", "UNDO"}
relating_object: bpy.props.IntProperty()
related_object: bpy.props.IntProperty()
def _execute(self, context):
relating_obj = None
if self.relating_object:
relating_obj = tool.Ifc.get_object(tool.Ifc.get().by_id(self.relating_object))
elif self.related_object:
aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get().by_id(self.related_object))
if aggregate:
relating_obj = tool.Ifc.get_object(aggregate)
elif context.active_object:
relating_obj = context.active_object
if not relating_obj:
return
for obj in tool.Blender.get_selected_objects():
for obj in bpy.context.selected_objects:
if obj == relating_obj:
continue
element = tool.Ifc.get_entity(obj)
if not element:
continue
try:
core.assign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=relating_obj,
related_obj=obj,
)
except core.IncompatibleAggregateError:
self.report({"ERROR"}, f"Cannot aggregate {obj.name} to {relating_obj.name}")
except core.AggregateRepresentationError:
self.report({"ERROR"}, f"Cannot aggregate to {relating_obj.name} with a body representation")
result = core.assign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=relating_obj,
related_obj=obj,
)
if not result:
self.report({"ERROR"}, f" Cannot aggregate {obj.name} to {relating_obj.name}")
class BIM_OT_aggregate_unassign_object(bpy.types.Operator, Operator):
@@ -98,14 +90,7 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, Operator):
related_obj=tool.Ifc.get_object(element),
)
# Removes Pset related to Linked Aggregates
if not element.is_a('IfcElementAssembly'):
pset = ifcopenshell.util.element.get_pset(element, 'BBIM_Linked_Aggregate')
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
class BIM_OT_enable_editing_aggregate(bpy.types.Operator, Operator):
"""Enable editing aggregation relationship"""
@@ -136,7 +121,6 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
ifc_class: bpy.props.StringProperty(name="IFC Class", default="IfcElementAssembly")
aggregate_name: bpy.props.StringProperty(name="Name", default="Default_Name")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -144,15 +128,13 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
def draw(self, context):
row = self.layout
row.prop(self, "ifc_class")
row = self.layout
row.prop(self, "aggregate_name")
def _execute(self, context):
try:
ifc_class = tool.Ifc.schema().declaration_by_name(self.ifc_class).name()
except:
return
aggregate = self.create_aggregate(context, ifc_class, self.aggregate_name)
aggregate = self.create_aggregate(context, ifc_class)
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
@@ -180,8 +162,8 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
)
core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj)
def create_aggregate(self, context, ifc_class, aggregate_name):
aggregate = bpy.data.objects.new(aggregate_name, None)
def create_aggregate(self, context, ifc_class):
aggregate = bpy.data.objects.new("Assembly", None)
aggregate.location = context.scene.cursor.location
bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class=ifc_class)
return aggregate
@@ -197,17 +179,12 @@ class BIM_OT_select_parts(bpy.types.Operator):
def execute(self, context):
self.file = IfcStore.get_file()
# obj = bpy.data.objects.get(self.obj) or context.active_object
for obj in context.selected_objects:
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(obj))
if parts:
parts_objs = set(tool.Ifc.get_object(part) for part in parts)
selectable_parts_objs = set(context.selectable_objects).intersection(parts_objs)
for selectable_part_obj in selectable_parts_objs:
selectable_part_obj.select_set(True)
else:
obj.select_set(False)
obj = bpy.data.objects.get(self.obj) or context.active_object
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(obj))
parts_objs = set(tool.Ifc.get_object(part) for part in parts)
selectable_parts_objs = set(context.selectable_objects).intersection(parts_objs)
for selectable_part_obj in selectable_parts_objs:
selectable_part_obj.select_set(True)
return {"FINISHED"}
@@ -218,54 +195,15 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
bl_label = "Select Aggregate"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
select_parts: bpy.props.BoolProperty(default=False)
@classmethod
def description(cls, context, properties):
if properties.select_parts:
return "Select Aggregate and Parts"
else:
return "Select Aggregate"
def execute(self, context):
self.file = IfcStore.get_file()
# obj = bpy.data.objects.get(self.obj) or context.active_object
# aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(obj))
# aggregate_obj = tool.Ifc.get_object(aggregate)
all_parts = []
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if element:
aggregate = ifcopenshell.util.element.get_aggregate(element)
if aggregate:
all_parts.append(aggregate)
obj.select_set(False)
else:
pass
if not element:
obj.select_set(False)
if self.select_parts:
all_objs = []
for part in all_parts:
if part.IsDecomposedBy:
for subpart in part.IsDecomposedBy[0].RelatedObjects:
all_parts.append(subpart)
all_objs.append(part)
for element in all_objs:
obj = tool.Ifc.get_object(element)
if obj:
obj.select_set(True)
else:
for aggregate_element in all_parts:
aggregate_obj = tool.Ifc.get_object(aggregate_element)
aggregate_obj.select_set(True)
bpy.context.view_layer.objects.active = aggregate_obj
obj = bpy.data.objects.get(self.obj) or context.active_object
aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(obj))
aggregate_obj = tool.Ifc.get_object(aggregate)
if aggregate_obj in context.selectable_objects:
aggregate_obj.select_set(True)
bpy.context.view_layer.objects.active = aggregate_obj
return {"FINISHED"}
@@ -275,105 +213,20 @@ class BIM_OT_add_part_to_object(bpy.types.Operator, Operator):
bl_options = {"REGISTER", "UNDO"}
part_class: bpy.props.StringProperty(name="Class", options={"HIDDEN"})
part_name: bpy.props.StringProperty(name="Name")
element: bpy.props.IntProperty(options={"HIDDEN"})
obj: bpy.props.StringProperty(options={"HIDDEN"})
def invoke(self, context, event):
self.part_name = "My " + self.part_class.lstrip("Ifc")
return context.window_manager.invoke_props_dialog(self)
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) or context.active_object
core.add_part_to_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
tool.Blender,
obj=tool.Ifc.get_object(tool.Ifc.get().by_id(self.element)) if self.element else context.active_object,
obj=obj,
part_class=self.part_class,
part_name=self.part_name,
)
tool.Spatial.load_container_manager()
class BIM_OT_break_link_to_other_aggregates(bpy.types.Operator, Operator):
bl_idname = "bim.break_link_to_other_aggregates"
bl_label = "Break link to other aggregates"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
element = tool.Ifc.get_entity(bpy.context.active_object)
aggregate = ifcopenshell.util.element.get_aggregate(element)
if not aggregate:
return []
if not element:
return []
parts = ifcopenshell.util.element.get_parts(aggregate)
for part in parts:
pset = ifcopenshell.util.element.get_pset(part, "BBIM_Linked_Aggregate")
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
linked_aggregate_group = [
r.RelatingGroup
for r in getattr(aggregate, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
tool.Ifc.run("group.unassign_group", group=linked_aggregate_group[0], products=[aggregate])
return {"FINISHED"}
class BIM_OT_select_linked_aggregates(bpy.types.Operator, Operator):
bl_idname = "bim.select_linked_aggregates"
bl_label = "Select linked aggregates"
bl_options = {"REGISTER", "UNDO"}
select_parts: bpy.props.BoolProperty(default=False)
@classmethod
def description(cls, context, properties):
if properties.select_parts:
return "Select all aggregates, subaggregates and all their parts"
else:
return "Select all aggregates"
def _execute(self, context):
for obj in context.selected_objects:
obj.select_set(False)
element = tool.Ifc.get_entity(obj)
aggregate = ifcopenshell.util.element.get_aggregate(element)
if not aggregate:
continue
if not element:
continue
linked_aggregate_group = [
r.RelatingGroup
for r in getattr(aggregate, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
group_rel = linked_aggregate_group[0].IsGroupedBy or []
for group_link in group_rel:
parts = list(group_link.RelatedObjects)
if self.select_parts:
parts_objs = []
for part in parts:
if part.IsDecomposedBy:
for subpart in part.IsDecomposedBy[0].RelatedObjects:
parts.append(subpart)
parts_objs.append(part)
for element in parts_objs:
obj = tool.Ifc.get_object(element)
if obj:
obj.select_set(True)
else:
for element in parts:
obj = tool.Ifc.get_object(element)
obj.select_set(True)
return {"FINISHED"}
@@ -34,27 +34,15 @@ from bpy.props import (
def update_relating_object(self, context):
def message(self, context):
self.layout.label(text="Please select a valid IFC Element")
self.layout.label(text="Please select a valid Ifc Element")
if self.relating_object:
self.related_object = None
if not self.relating_object.BIMObjectProperties.ifc_definition_id:
context.window_manager.popup_menu(message, title="Invalid Element Selected", icon="INFO")
self.relating_object = None
def update_related_object(self, context):
def message(self, context):
self.layout.label(text="Please select a valid IFC Element")
if self.related_object:
if self.relating_object is None:
return
if not self.relating_object.BIMObjectProperties.ifc_definition_id:
context.window_manager.popup_menu(message, title="Invalid Element Selected", icon="INFO")
self.relating_object = None
if not self.related_object.BIMObjectProperties.ifc_definition_id:
context.window_manager.popup_menu(message, title="Invalid Element Selected", icon="INFO")
self.related_object = None
class BIMObjectAggregateProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing")
relating_object: PointerProperty(name="Relating Whole", type=bpy.types.Object, update=update_relating_object)
related_object: PointerProperty(name="Related Part", type=bpy.types.Object, update=update_related_object)
relating_object: PointerProperty(name="Aggregate", type=bpy.types.Object, update=update_relating_object)
@@ -18,9 +18,7 @@
from bpy.types import Panel
from blenderbim.bim.module.aggregate.data import AggregateData
from blenderbim.bim.module.group.data import GroupsData, ObjectGroupsData
from blenderbim.bim.ifc import IfcStore
import blenderbim.tool as tool
class BIM_PT_aggregate(Panel):
@@ -53,36 +51,23 @@ class BIM_PT_aggregate(Panel):
props = context.active_object.BIMObjectAggregateProperties
if props.is_editing:
row = layout.row()
row.prop(props, "relating_object", text="Whole")
row = layout.row()
row.prop(props, "related_object", text="Or Part")
row = layout.row(align=True)
col = row.column(align=True)
if not props.relating_object and not props.related_object:
col.enabled = False
op = col.operator("bim.aggregate_assign_object", icon="CHECKMARK")
row.prop(props, "relating_object", text="")
if props.relating_object:
op = row.operator("bim.aggregate_assign_object", icon="CHECKMARK", text="")
op.relating_object = props.relating_object.BIMObjectProperties.ifc_definition_id
elif props.related_object:
op.related_object = props.related_object.BIMObjectProperties.ifc_definition_id
row.operator("bim.disable_editing_aggregate", icon="CANCEL", text="")
return
else:
row = layout.row(align=True)
if AggregateData.data["has_relating_object"]:
row.label(text=AggregateData.data["relating_object_label"], icon="TRIA_UP")
op = row.operator("bim.select_aggregate", icon="OBJECT_DATA", text="")
op.obj = context.active_object.name
op.select_parts = False
op = row.operator("bim.select_aggregate", icon="RESTRICT_SELECT_OFF", text="")
op.obj = context.active_object.name
op.select_parts = True
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
row.operator("bim.add_aggregate", icon="ADD", text="")
op = row.operator("bim.aggregate_unassign_object", icon="X", text="")
else:
row.label(text="No Whole Relationship Found", icon="TRIA_UP")
row.label(text="No Aggregate", icon="TRIA_UP")
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
row.operator("bim.add_aggregate", icon="ADD", text="")
@@ -99,58 +84,15 @@ class BIM_PT_aggregate(Panel):
op = row.operator("bim.select_parts", icon="RESTRICT_SELECT_OFF", text="")
op.obj = context.active_object.name
class BIM_PT_linked_aggregate(Panel):
bl_label = "Linked Aggregates"
bl_idname = "BIM_PT_linked_aggregate"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_order = 2
bl_parent_id = "BIM_PT_aggregate"
@classmethod
def poll(cls, context):
if not context.active_object:
return False
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
if not IfcStore.get_element(props.ifc_definition_id):
return False
if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
return False
return True
def draw(self, context):
layout = self.layout
if not AggregateData.is_loaded:
AggregateData.load()
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMObjectAggregateProperties
row = layout.row(align=True)
if element.Decomposes:
Number_Linked_Aggregates = AggregateData.data['total_linked_aggregate']
if not Number_Linked_Aggregates:
row.label(text="Not a Linked Aggregate")
else:
row.label(text=f"{Number_Linked_Aggregates} Linked Aggregates")
op = row.operator("bim.duplicate_linked_aggregate_to_3d_cursor", text="", icon="DUPLICATE")
if type(Number_Linked_Aggregates) is int:
if Number_Linked_Aggregates > 0:
op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_DATA_POINTCLOUD")
op.select_parts = False
op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_OB_POINTCLOUD")
op.select_parts = True
row.operator("bim.refresh_linked_aggregate", text="", icon="FILE_REFRESH")
op = row.operator("bim.break_link_to_other_aggregates", text="", icon="X")
else:
row.label(text="Not an Aggregate")
ifc_class = AggregateData.data["ifc_class"]
part_class = ""
if ifc_class == "IfcBuilding":
part_class = "IfcBuildingStorey"
elif ifc_class == "IfcSite":
part_class = "IfcBuilding"
elif ifc_class == "IfcProject":
part_class = "IfcSite"
if part_class != "":
op = layout.operator("bim.add_part_to_object", text="Add " + part_class.lstrip("Ifc"))
op.part_class = part_class
op.obj = context.active_object.name
@@ -27,12 +27,15 @@ classes = (
operator.CopyAttributeToSelection,
prop.BIMAttributeProperties,
ui.BIM_PT_object_attributes,
ui.BIM_PT_material_attributes,
)
def register():
bpy.types.Object.BIMAttributeProperties = bpy.props.PointerProperty(type=prop.BIMAttributeProperties)
bpy.types.Material.BIMAttributeProperties = bpy.props.PointerProperty(type=prop.BIMAttributeProperties)
def unregister():
del bpy.types.Object.BIMAttributeProperties
del bpy.types.Material.BIMAttributeProperties
@@ -23,6 +23,7 @@ import blenderbim.tool as tool
def refresh():
AttributesData.is_loaded = False
MaterialAttributesData.is_loaded = False
class AttributesData:
@@ -50,3 +51,28 @@ class AttributesData:
key = "STEP ID"
results.append({"name": key, "value": str(value)})
return results
class MaterialAttributesData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {"ifc_definition_id": cls.ifc_definition_id(), "attributes": cls.attributes()}
cls.is_loaded = True
@classmethod
def ifc_definition_id(cls):
return bpy.context.active_object.active_material.BIMObjectProperties.ifc_definition_id
@classmethod
def attributes(cls):
results = []
element = tool.Ifc.get_entity(bpy.context.active_object.active_material)
data = element.get_info()
for key, value in data.items():
if value is None or isinstance(value, ifcopenshell.entity_instance) or key in ["id", "type"]:
continue
results.append({"name": key, "value": str(value)})
return results
@@ -20,7 +20,6 @@ import bpy
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.guid
import blenderbim.bim.helper
import blenderbim.bim.handler
import blenderbim.tool as tool
@@ -40,22 +39,18 @@ class EnableEditingAttributes(bpy.types.Operator):
bl_label = "Enable Editing Attributes"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def execute(self, context):
self.file = IfcStore.get_file()
obj = bpy.data.objects.get(self.obj)
if self.obj_type == "Object":
obj = bpy.data.objects.get(self.obj)
elif self.obj_type == "Material":
obj = bpy.data.materials.get(self.obj)
oprops = obj.BIMObjectProperties
props = obj.BIMAttributeProperties
props.attributes.clear()
element = tool.Ifc.get_entity(obj)
has_inherited_predefined_type = False
if not element.is_a("IfcTypeObject") and (element_type := ifcopenshell.util.element.get_type(element)):
# Allow for None due to https://github.com/buildingSMART/IFC4.3.x-development/issues/818
has_inherited_predefined_type = ifcopenshell.util.element.get_predefined_type(element_type) not in (
"NOTDEFINED",
None,
)
def callback(name, prop, data):
if name in ("RefLatitude", "RefLongitude"):
new = props.attributes.add()
@@ -67,11 +62,10 @@ class EnableEditingAttributes(bpy.types.Operator):
new.string_value = "" if new.is_null else json.dumps(data[name])
blenderbim.bim.helper.add_attribute_description(new)
new.description += " The degrees, minutes and seconds should follow this format : [12,34,56]"
if name in ("PredefinedType", "ObjectType") and has_inherited_predefined_type:
props.attributes.remove(len(props.attributes) - 1)
return True
blenderbim.bim.helper.import_attributes2(element, props.attributes, callback=callback)
blenderbim.bim.helper.import_attributes2(
tool.Ifc.get().by_id(oprops.ifc_definition_id), props.attributes, callback=callback
)
props.is_editing_attributes = True
return {"FINISHED"}
@@ -81,9 +75,13 @@ class DisableEditingAttributes(bpy.types.Operator):
bl_label = "Disable Editing Attributes"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def execute(self, context):
obj = bpy.data.objects.get(self.obj)
if self.obj_type == "Object":
obj = bpy.data.objects.get(self.obj)
elif self.obj_type == "Material":
obj = bpy.data.materials.get(self.obj)
props = obj.BIMAttributeProperties
props.is_editing_attributes = False
return {"FINISHED"}
@@ -94,10 +92,14 @@ class EditAttributes(bpy.types.Operator, Operator):
bl_label = "Edit Attributes"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def _execute(self, context):
self.file = IfcStore.get_file()
obj = bpy.data.objects.get(self.obj)
if self.obj_type == "Object":
obj = bpy.data.objects.get(self.obj)
elif self.obj_type == "Material":
obj = bpy.data.materials.get(self.obj)
props = obj.BIMAttributeProperties
product = tool.Ifc.get_entity(obj)
@@ -114,50 +116,24 @@ class EditAttributes(bpy.types.Operator, Operator):
attributes = blenderbim.bim.helper.export_attributes(props.attributes, callback=callback)
ifcopenshell.api.run("attribute.edit_attributes", self.file, product=product, attributes=attributes)
bpy.ops.bim.disable_editing_attributes(obj=obj.name)
bpy.ops.bim.disable_editing_attributes(obj=obj.name, obj_type=self.obj_type)
return {"FINISHED"}
class GenerateGlobalId(bpy.types.Operator, Operator):
class GenerateGlobalId(bpy.types.Operator):
bl_idname = "bim.generate_global_id"
bl_label = "Regenerate GlobalId"
bl_description = "Regenerate GlobalId\n\nSHIFT+CLICK to regenerate GlobalIds for all selected objects"
bl_options = {"REGISTER", "UNDO"}
use_selected: bpy.props.BoolProperty(name="Use All Selected Objects", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# using all selected objects on shift+click
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
if event.type == "LEFTMOUSE" and event.shift:
self.use_selected = True
return self.execute(context)
def _execute(self, context):
if self.use_selected:
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcRoot"):
continue
element.GlobalId = ifcopenshell.guid.new()
obj = context.active_object
if not obj or not obj.BIMAttributeProperties.is_editing_attributes:
return {"FINISHED"}
props = obj.BIMAttributeProperties
element = tool.Ifc.get_entity(obj)
if not element.is_a("IfcRoot"):
return {"FINISHED"}
if self.use_selected and obj in context.selected_objects:
# guid value was already regenerated, just update the ui prop
guid_value = element.GlobalId
def execute(self, context):
index = context.active_object.BIMAttributeProperties.attributes.find("GlobalId")
if index >= 0:
global_id = context.active_object.BIMAttributeProperties.attributes[index]
else:
guid_value = ifcopenshell.guid.new()
props.attributes["GlobalId"].string_value = guid_value
global_id = context.active_object.BIMAttributeProperties.attributes.add()
global_id.name = "GlobalId"
global_id.data_type = "string"
global_id.string_value = ifcopenshell.guid.new()
return {"FINISHED"}
@@ -19,34 +19,34 @@
import blenderbim.bim.helper
from bpy.types import Panel
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.attribute.data import AttributesData
from blenderbim.bim.module.attribute.data import AttributesData, MaterialAttributesData
def draw_ui(context, layout, attributes):
obj = context.active_object
def draw_ui(context, layout, obj_type, attributes):
obj = context.active_object if obj_type == "Object" else context.active_object.active_material
oprops = obj.BIMObjectProperties
props = obj.BIMAttributeProperties
if props.is_editing_attributes:
row = layout.row(align=True)
op = row.operator("bim.edit_attributes", icon="CHECKMARK", text="Save Attributes")
op.obj_type = obj_type
op.obj = obj.name
op = row.operator("bim.disable_editing_attributes", icon="CANCEL", text="")
op.obj_type = obj_type
op.obj = obj.name
blenderbim.bim.helper.draw_attributes(props.attributes, layout, copy_operator="bim.copy_attribute_to_selection")
else:
row = layout.row()
op = row.operator("bim.enable_editing_attributes", icon="GREASEPENCIL", text="Edit")
op.obj_type = obj_type
op.obj = obj.name
for attribute in attributes:
row = layout.row(align=True)
row.label(text=attribute["name"])
# row.label(text=attribute["value"])
op = row.operator("bim.select_similar", text=attribute["value"], icon="NONE", emboss=False)
op.key = attribute['name']
row.label(text=attribute["value"])
# TODO: reimplement, see #1222
# if "IfcSite/" in context.active_object.name or "IfcBuilding/" in context.active_object.name:
@@ -72,4 +72,32 @@ class BIM_PT_object_attributes(Panel):
def draw(self, context):
if not AttributesData.is_loaded:
AttributesData.load()
draw_ui(context, self.layout, AttributesData.data["attributes"])
draw_ui(context, self.layout, "Object", AttributesData.data["attributes"])
class BIM_PT_material_attributes(Panel):
bl_label = "Material Attributes"
bl_idname = "BIM_PT_material_attributes"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "material"
@classmethod
def poll(cls, context):
if not IfcStore.get_file():
return False
try:
return bool(context.active_object.active_material.BIMObjectProperties.ifc_definition_id)
except:
return False
def draw(self, context):
if not MaterialAttributesData.is_loaded:
MaterialAttributesData.load()
elif (
context.active_object.active_material.BIMObjectProperties.ifc_definition_id
!= MaterialAttributesData.data["ifc_definition_id"]
):
MaterialAttributesData.load()
draw_ui(context, self.layout, "Material", MaterialAttributesData.data["attributes"])
@@ -938,7 +938,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
# Operators with context overrides are used because they are
# significantly faster than looping through all objects
exception_global_ids = {v.ifc_guid for v in viewpoint.visualization_info.components.visibility.exceptions.component or []}
exception_global_ids = {v.ifc_guid for v in viewpoint.visualization_info.components.visibility.exceptions or []}
if viewpoint.visualization_info.components.visibility.default_visibility:
old = context.area.type
@@ -958,13 +958,11 @@ class ActivateBcfViewpoint(bpy.types.Operator):
if objs:
old = context.area.type
context.area.type = "VIEW_3D"
bpy.ops.object.hide_view_clear()
context_override = {}
context_override["object"] = context_override["active_object"] = objs[0]
context_override["selected_objects"] = context_override["selected_editable_objects"] = objs
with context.temp_override(**context_override):
bpy.ops.object.hide_view_set(unselected=True)
bpy.data.objects["Viewpoint"].hide_set(False)
context.area.type = old
if viewpoint.visualization_info.components.view_setup_hints:
@@ -1002,7 +1000,6 @@ class ActivateBcfViewpoint(bpy.types.Operator):
obj = IfcStore.get_element(global_id)
if obj:
obj.select_set(True)
obj.hide_set(False)
def set_colours(self, viewpoint):
global_id_colours = {}
@@ -22,7 +22,6 @@ from . import ui, operator, prop
classes = (
operator.AddBoundary,
operator.ColourByRelatedBuildingElement,
operator.DecorateBoundaries,
operator.DisableEditingBoundary,
operator.DisableEditingBoundaryGeometry,
operator.EditBoundaryAttributes,
@@ -20,12 +20,10 @@ import bpy
import bmesh
import logging
import shapely
import shapely.ops
import mathutils
import numpy as np
import multiprocessing
import ifcopenshell.api
import ifcopenshell.geom
import ifcopenshell.util.unit
import ifcopenshell.util.shape
import ifcopenshell.util.element
@@ -39,7 +37,6 @@ from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.decorator import ProfileDecorator
from blenderbim.bim.module.boundary.decorator import BoundaryDecorator
import blenderbim.core
import blenderbim.core.geometry
def get_boundaries_collection(blender_space):
@@ -150,9 +147,7 @@ class Loader:
self.ifc_importer = import_ifc.IfcImporter(ifc_import_settings)
self.ifc_importer.file = self.ifc_file
def load_boundary(
self, boundary: ifcopenshell.entity_instance, blender_space: bpy.types.Object = None
) -> bpy.types.Object:
def load_boundary(self, boundary, blender_space=None):
if not blender_space:
if not boundary.RelatingSpace:
self.operator.report(
@@ -514,10 +509,10 @@ class DisableEditingBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
class ShowBoundaries(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.show_boundaries"
bl_label = "Show Boundaries"
bl_description = "Load selected objects boundaries if they are not loaded"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMBoundaryProperties
loader = Loader(self)
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
@@ -529,7 +524,7 @@ class ShowBoundaries(bpy.types.Operator, tool.Ifc.Operator):
for rel in element.BoundedBy or []:
boundary_obj = loader.load_boundary(rel, obj)
tool.Boundary.decorate_boundary(boundary_obj)
BoundaryDecorator.install(context)
BoundaryDecorator.install(bpy.context)
return {"FINISHED"}
@@ -562,36 +557,6 @@ class HideBoundaries(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
class DecorateBoundaries(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.decorate_boundaries"
bl_label = "Toggle Boundaries Decorations"
bl_description = "Add special decorations for all boundaries in the scene or hide them if they are already added"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.scene.BIMBoundaryProperties
# filter not decorated boundaries and add decorations for them
decorated_boundaries = set([i.obj for i in props.boundaries])
active_boundaries = set()
for element in tool.Ifc.get().by_type("IfcRelSpaceBoundary"):
obj = tool.Ifc.get_object(element)
if obj is not None:
active_boundaries.add(obj)
boundaries_to_decorate = active_boundaries - decorated_boundaries
if boundaries_to_decorate:
for obj in boundaries_to_decorate:
tool.Boundary.decorate_boundary(obj)
BoundaryDecorator.install(context)
else:
for obj in decorated_boundaries:
tool.Boundary.undecorate_boundary(obj)
props.boundaries.clear()
BoundaryDecorator.uninstall()
tool.Blender.update_viewport()
return {"FINISHED"}
class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_boundary"
bl_label = "Add Boundary"
@@ -37,7 +37,7 @@ classes = (
def register():
if not bpy.app.background:
bpy.utils.register_tool(workspace.CadTool, after={"builtin.transform"}, separator=True, group=False)
bpy.utils.register_tool(workspace.CadTool, after={"builtin.transform"}, separator=True, group=True)
bpy.types.Scene.BIMCadProperties = bpy.props.PointerProperty(type=prop.BIMCadProperties)
@@ -24,10 +24,10 @@ from math import pi
class BIMCadProperties(PropertyGroup):
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE")
distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE")
x: bpy.props.FloatProperty(name="X", default=0.2, subtype="DISTANCE")
y: bpy.props.FloatProperty(name="Y", default=0.1, subtype="DISTANCE")
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
x: bpy.props.FloatProperty(name="X", default=0.2)
y: bpy.props.FloatProperty(name="Y", default=0.1)
gable_roof_edge_angle: bpy.props.FloatProperty(
name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE"
)
@@ -20,7 +20,6 @@ import os
import bpy
import blenderbim.tool as tool
import blenderbim.bim.module.type.prop as type_prop
import ifcopenshell.util.unit
from bpy.types import WorkSpaceTool
from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData
@@ -240,9 +239,8 @@ class CadHotkey(bpy.types.Operator):
row.prop(props, "resolution")
def hotkey_S_C(self):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if self.is_profile():
bpy.ops.bim.add_ifccircle(radius=self.props.radius / si_conversion)
bpy.ops.bim.add_ifccircle(radius=self.props.radius)
else:
bpy.ops.bim.cad_arc_from_2_points()
@@ -250,15 +248,13 @@ class CadHotkey(bpy.types.Operator):
bpy.ops.bim.cad_trim_extend()
def hotkey_S_F(self):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if self.is_profile():
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius / si_conversion)
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius)
else:
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius / si_conversion)
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius)
def hotkey_S_O(self):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bpy.ops.bim.cad_offset(distance=self.props.distance / si_conversion)
bpy.ops.bim.cad_offset(distance=self.props.distance)
def hotkey_S_Q(self):
element = tool.Ifc.get_entity(bpy.context.active_object)
@@ -274,8 +270,7 @@ class CadHotkey(bpy.types.Operator):
def hotkey_S_R(self):
if self.is_profile():
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bpy.ops.bim.add_rectangle(x=self.props.x / si_conversion, y=self.props.y / si_conversion)
bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y)
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
@@ -25,12 +25,15 @@ classes = (
operator.ExecuteIfcClash,
operator.ExportClashSets,
operator.ImportClashSets,
operator.LoadIfcClashes,
operator.SaveIfcClashes,
operator.LoadSmartGroupsForActiveClashSet,
operator.RemoveClashSet,
operator.RemoveClashSource,
operator.SelectClash,
operator.SelectClashResults,
operator.SelectClashSource,
operator.SelectIfcClashResults,
operator.SelectSmartGroup,
operator.SelectSmartGroupedClashesPath,
operator.SmartClashGroup,
@@ -41,6 +44,7 @@ classes = (
prop.BIMClashProperties,
ui.BIM_PT_ifcclash,
ui.BIM_PT_clash_manager,
ui.BIM_PT_dumb_clash_manager,
ui.BIM_PT_smart_clash_manager,
ui.BIM_UL_clashes,
ui.BIM_UL_clash_sets,
@@ -1,52 +0,0 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on 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.
#
# BlenderBIM Add-on 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 BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import json
import ifcopenshell.util.element
import blenderbim.tool as tool
def refresh():
ClashData.is_loaded = False
class ClashData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data = {}
cls.data["saved_searches"] = cls.saved_searches()
@classmethod
def saved_searches(cls):
if not tool.Ifc.get():
return []
groups = tool.Ifc.get().by_type("IfcGroup")
results = []
for group in groups:
try:
data = json.loads(group.Description)
if isinstance(data, dict) and data.get("type", None) == "BBIM_Search" and data.get("query", None):
results.append(group)
except:
pass
return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")]
@@ -1,102 +0,0 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on 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.
#
# BlenderBIM Add-on 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 BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import blf
import gpu
import bmesh
import blenderbim.tool as tool
from bpy.types import SpaceView3D
from mathutils import Vector
from gpu_extras.batch import batch_for_shader
from bpy_extras.view3d_utils import location_3d_to_region_2d
class ClashDecorator:
is_installed = False
handlers = []
@classmethod
def install(cls, context):
if cls.is_installed:
cls.uninstall()
handler = cls()
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_geometry, (context,), "WINDOW", "POST_VIEW"))
cls.is_installed = True
@classmethod
def uninstall(cls):
for handler in cls.handlers:
try:
SpaceView3D.draw_handler_remove(handler, "WINDOW")
except ValueError:
pass
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def draw_text(self, context):
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
text = context.scene.BIMClashProperties.active_clash_text
p = context.scene.BIMClashProperties.p1.lerp(context.scene.BIMClashProperties.p2, 0.5)
font_id = 0
blf.size(font_id, 12)
coords_2d = location_3d_to_region_2d(context.region, context.region_data, p)
color = self.addon_prefs.decorations_colour
blf.color(font_id, *color)
if coords_2d:
w, h = blf.dimensions(font_id, text)
coords_2d -= Vector((w * .5, 0))
blf.position(font_id, coords_2d[0], coords_2d[1], 0)
blf.draw(font_id, text) # Set your text here
def draw_geometry(self, context):
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind() # required to be able to change uniforms of the shader
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
selected_vertices = [context.scene.BIMClashProperties.p1, context.scene.BIMClashProperties.p2]
selected_edges = []
if selected_vertices[0] != selected_vertices[1]:
selected_edges = [[0, 1]]
self.draw_batch("POINTS", selected_vertices, special_elements_color)
if selected_edges:
self.draw_batch("LINES", selected_vertices, special_elements_color, selected_edges)
@@ -23,11 +23,10 @@ import bmesh
import logging
import numpy as np
import ifcopenshell
import blenderbim.tool as tool
from math import radians
from mathutils import Matrix, Vector
from math import radians
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.clash.decorator import ClashDecorator
import blenderbim.tool as tool
class ExportClashSets(bpy.types.Operator):
@@ -46,7 +45,18 @@ class ExportClashSets(bpy.types.Operator):
def execute(self, context):
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
clash_sets = tool.Clash.export_clash_sets()
clash_sets = []
for clash_set in context.scene.BIMClashProperties.clash_sets:
self.a = []
self.b = []
for ab in ["a", "b"]:
for data in getattr(clash_set, ab):
clash_source = {"file": data.name}
if data.selector:
clash_source["selector"] = data.selector
clash_source["mode"] = data.mode
getattr(self, ab).append(clash_source)
clash_sets.append({"name": clash_set.name, "tolerance": clash_set.tolerance, "a": self.a, "b": self.b})
with open(self.filepath, "w") as destination:
destination.write(json.dumps(clash_sets, indent=4))
return {"FINISHED"}
@@ -68,34 +78,25 @@ class ImportClashSets(bpy.types.Operator):
return {"RUNNING_MODAL"}
def execute(self, context):
tool.Clash.load_clash_sets(self.filepath)
context.scene.BIMClashProperties.clash_sets.clear()
for clash_set in tool.Clash.get_clash_sets():
with open(self.filepath) as f:
clash_sets = json.load(f)
for clash_set in clash_sets:
new = context.scene.BIMClashProperties.clash_sets.add()
new.name = clash_set["name"]
new.mode = clash_set["mode"]
if new.mode == "intersection":
new.tolerance = clash_set["tolerance"]
new.check_all = clash_set["check_all"]
elif new.mode == "collision":
new.allow_touching = clash_set["allow_touching"]
elif new.mode == "clearance":
new.clearance = clash_set["clearance"]
new.check_all = clash_set["check_all"]
new.tolerance = clash_set["tolerance"]
for clash_source in clash_set["a"]:
new_source = new.a.add()
new_source.name = clash_source["file"]
if "selector" in clash_source:
tool.Search.import_filter_query(clash_source["selector"], new_source.filter_groups)
new_source.selector = clash_source["selector"]
new_source.mode = clash_source["mode"]
if "b" in clash_set and clash_set["b"]:
if clash_set["b"]:
for clash_source in clash_set["b"]:
new_source = new.b.add()
new_source.name = clash_source["file"]
if "selector" in clash_source:
tool.Search.import_filter_query(clash_source["selector"], new_source.filter_groups)
new_source.selector = clash_source["selector"]
new_source.mode = clash_source["mode"]
tool.Clash.import_active_clashes()
return {"FINISHED"}
@@ -108,6 +109,7 @@ class AddClashSet(bpy.types.Operator):
def execute(self, context):
new = context.scene.BIMClashProperties.clash_sets.add()
new.name = "New Clash Set"
new.tolerance = 0.01
return {"FINISHED"}
@@ -209,11 +211,8 @@ class ExecuteIfcClash(bpy.types.Operator):
def invoke(self, context, event):
_, extension = os.path.splitext(self.filepath)
if extension != ".bcf":
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
# TODO Temporarily until BCF support comes back
# if extension != ".json":
# self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".bcf")
if extension != ".json":
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".bcf")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
@@ -221,12 +220,7 @@ class ExecuteIfcClash(bpy.types.Operator):
def execute(self, context):
from ifcclash import ifcclash
self.props = context.scene.BIMClashProperties
_, extension = os.path.splitext(self.filepath)
if extension != ".bcf":
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
# TODO Temporarily until BCF support comes back
if extension != ".json":
self.filepath = bpy.path.ensure_ext(self.filepath, ".bcf")
@@ -236,7 +230,7 @@ class ExecuteIfcClash(bpy.types.Operator):
settings.logger.setLevel(logging.DEBUG)
clasher = ifcclash.Clasher(settings)
if self.props.should_create_clash_snapshots:
if context.scene.BIMClashProperties.should_create_clash_snapshots:
def get_viewpoint_snapshot(viewpoint):
camera = bpy.data.objects.get("IFC Clash Camera")
@@ -277,13 +271,23 @@ class ExecuteIfcClash(bpy.types.Operator):
clasher.get_viewpoint_snapshot = get_viewpoint_snapshot
clasher.clash_sets = tool.Clash.export_clash_sets()
clasher.clash_sets = []
for clash_set in context.scene.BIMClashProperties.clash_sets:
self.a = []
self.b = []
for ab in ["a", "b"]:
for data in getattr(clash_set, ab):
clash_source = {"file": data.name}
if data.selector:
clash_source["selector"] = data.selector
clash_source["mode"] = data.mode
getattr(self, ab).append(clash_source)
clash_set_data = {"name": clash_set.name, "tolerance": clash_set.tolerance, "a": self.a}
if self.b:
clash_set_data["b"] = self.b
clasher.clash_sets.append(clash_set_data)
clasher.clash()
clasher.export()
if extension == ".json":
tool.Clash.load_clash_sets(self.filepath)
tool.Clash.import_active_clashes()
return {"FINISHED"}
@@ -302,7 +306,6 @@ class SelectIfcClashResults(bpy.types.Operator):
return {"RUNNING_MODAL"}
def execute(self, context):
# TODO refactor into new clash results system
self.file = IfcStore.get_file()
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
with open(self.filepath) as f:
@@ -349,6 +352,83 @@ class SelectIfcClashResults(bpy.types.Operator):
return {"FINISHED"}
class LoadIfcClashes(bpy.types.Operator):
bl_idname = "bim.load_ifc_clashes"
bl_label = "Load IFC Clashes"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Load the clashing IFC geometry stored in a file"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
self.file = IfcStore.get_file()
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
with open(self.filepath) as f:
clash_sets_json = json.load(f)
active_clash_set = context.scene.BIMClashProperties.active_clash_set
for clash_set in clash_sets_json:
if not "clashes" in clash_set.keys():
self.report({"WARNING"}, "No clashes found for the selected Clash Set.")
return {"CANCELLED"}
active_clash_set.clashes.clear()
for clash in clash_set["clashes"].values():
blender_clash = active_clash_set.clashes.add()
blender_clash.a_global_id = clash["a_global_id"]
blender_clash.b_global_id = clash["b_global_id"]
blender_clash.a_name = "{}/{}".format(clash["a_ifc_class"], clash["a_name"])
blender_clash.b_name = "{}/{}".format(clash["b_ifc_class"], clash["b_name"])
blender_clash.status = False if not "status" in clash.keys() else clash["status"]
return {"FINISHED"}
class SaveIfcClashes(bpy.types.Operator):
bl_idname = "bim.save_ifc_clashes"
bl_label = "Save IFC Clashes"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Save the clashing IFC geometry stored in a file"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
self.file = IfcStore.get_file()
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
clash_sets_json = self.load_json()
active_clash_set = context.scene.BIMClashProperties.active_clash_set
self.update_clash_sets(clash_sets_json, active_clash_set)
self.save_json(clash_sets_json)
return {"FINISHED"}
def load_json(self):
with open(self.filepath) as f:
return json.load(f)
def update_clash_sets(self, clash_sets_json, active_clash_set):
for clash_set in clash_sets_json:
if clash_set["name"] != active_clash_set.name or "clashes" not in clash_set.keys():
continue
for clash in active_clash_set.clashes:
clash_key = clash.a_global_id + "-" + clash.b_global_id
if clash_set.get("clashes", {}).get(clash_key, None) is not None:
clash_set["clashes"][clash_key]["status"] = clash.status
def save_json(self, clash_sets_json):
with open(self.filepath, "w") as destination:
json.dump(clash_sets_json, destination, indent=4)
class SelectClash(bpy.types.Operator):
bl_idname = "bim.select_clash"
bl_label = "Select Clash"
@@ -357,29 +437,31 @@ class SelectClash(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
self.props = context.scene.BIMClashProperties
clash_set = tool.Clash.get_clash_set(self.props.active_clash_set.name)
active_clash = self.props.active_clash
clash = tool.Clash.get_clash(clash_set, active_clash.a_global_id, active_clash.b_global_id)
if not clash:
return {"FINISHED"}
clash = context.scene.BIMClashProperties.active_clash
products = []
for global_id in (clash["a_global_id"], clash["b_global_id"]):
try:
products.append(tool.Ifc.get().by_guid(global_id))
except:
pass
linked_objects = []
try:
products.append(tool.Ifc.get().by_guid(clash.a_global_id))
except:
linked_objects.append(clash.a_name)
try:
products.append(tool.Ifc.get().by_guid(clash.b_global_id))
except:
linked_objects.append(clash.b_name)
tool.Spatial.select_products(products, unhide=True)
ClashDecorator.install(bpy.context)
target = Vector(clash["p1"])
tool.Clash.look_at(target, target + Vector((5, 5, 5)))
self.props.p1 = clash["p1"]
self.props.p2 = clash["p2"]
self.props.active_clash_text = clash["type"].title() + " " + str(round(clash["distance"] * 1000)) + "mm"
print(linked_objects)
for obj_name in linked_objects:
obj = bpy.data.objects.get(obj_name)
if obj:
obj.select_set(True)
else:
print("Object not found:", obj_name)
if context.scene.BIMClashProperties.sould_focus_on_clash:
context_override = tool.Blender.get_viewport_context()
with bpy.context.temp_override(**context_override):
bpy.ops.view3d.view_selected()
return {"FINISHED"}
@@ -17,7 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from blenderbim.bim.prop import StrProperty, Attribute, BIMFilterGroup
from blenderbim.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
@@ -33,12 +33,11 @@ from bpy.props import (
class ClashSource(PropertyGroup):
name: StringProperty(name="File")
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
selector: StringProperty(name="Selector")
mode: EnumProperty(
items=[
("a", "All Elements", "All elements will be used for clashing"),
("i", "Include", "Only the selected elements are included for clashing"),
("e", "Exclude", "All elements except the selected elements are included for clashing"),
("i", "Include", "Only the selected objects are included for clashing"),
("e", "Exclude", "All objects except the selected objects are included for clashing"),
],
name="Mode",
)
@@ -54,24 +53,7 @@ class Clash(PropertyGroup):
class ClashSet(PropertyGroup):
name: StringProperty(name="Name")
mode: EnumProperty(
items=[
(
"intersection",
"Intersection",
"Detect objects that protrude or pierce another object",
"PIVOT_MEDIAN",
1,
),
("collision", "Collision", "Detect touching objects with any surface collision", "PIVOT_INDIVIDUAL", 2),
("clearance", "Clearance", "Detect objects within a proximity threshold", "PIVOT_ACTIVE", 3),
],
name="Mode",
)
tolerance: FloatProperty(name="Tolerance", default=0.002)
clearance: FloatProperty(name="Clearance", default=0.01)
allow_touching: BoolProperty(name="Allow Touching", default=False)
check_all: BoolProperty(name="Check All", default=False)
tolerance: FloatProperty(name="Tolerance")
a: CollectionProperty(name="Group A", type=ClashSource)
b: CollectionProperty(name="Group B", type=ClashSource)
clashes: CollectionProperty(name="Clashes", type=Clash)
@@ -96,21 +78,22 @@ class BIMClashProperties(PropertyGroup):
smart_clash_grouping_max_distance: IntProperty(
name="Smart Clash Grouping Max Distance", default=3, soft_min=1, soft_max=10
)
p1: FloatVectorProperty(name="P1", default=(0.0, 0.0, 0.0), subtype="XYZ")
p2: FloatVectorProperty(name="P2", default=(0.0, 0.0, 0.0), subtype="XYZ")
active_clash_text: StringProperty(name="Active Clash Text")
sould_focus_on_clash: BoolProperty(name="Show Focus Clash", default=False)
@property
def active_clash_set(self):
if self.active_clash_set_index < len(self.clash_sets):
return self.clash_sets[self.active_clash_set_index]
if not self.clash_sets:
return None
return self.clash_sets[self.active_clash_set_index]
@property
def active_smart_group(self):
if self.active_smart_group_index < len(self.smart_clash_groups):
return self.smart_clash_groups[self.active_smart_group_index]
if not self.smart_clash_groups:
return None
return self.smart_clash_groups[self.active_smart_group_index]
@property
def active_clash(self):
if self.active_clash_index < len(self.active_clash_set.clashes):
return self.active_clash_set.clashes[self.active_clash_index]
if not self.active_clash_set:
return None
return self.active_clash_set.clashes[self.active_clash_index]
@@ -17,9 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import blenderbim.bim.helper
from bpy.types import Panel
from blenderbim.bim.module.clash.data import ClashData
class BIM_PT_ifcclash(Panel):
@@ -32,9 +30,6 @@ class BIM_PT_ifcclash(Panel):
bl_parent_id = "BIM_PT_tab_clash_detection"
def draw(self, context):
if not ClashData.is_loaded:
ClashData.load()
layout = self.layout
scene = context.scene
@@ -50,83 +45,57 @@ class BIM_PT_ifcclash(Panel):
layout.template_list("BIM_UL_clash_sets", "", props, "clash_sets", props, "active_clash_set_index")
if not props.active_clash_set:
return
if props.active_clash_set_index < len(props.clash_sets):
clash_set = props.active_clash_set
clash_set = props.active_clash_set
row = layout.row(align=True)
row.prop(clash_set, "name")
row.operator("bim.remove_clash_set", icon="X", text="").index = props.active_clash_set_index
row = layout.row(align=True)
row.prop(clash_set, "name")
row.operator("bim.remove_clash_set", icon="X", text="").index = props.active_clash_set_index
row = layout.row()
row.prop(clash_set, "mode")
if clash_set.mode == "intersection":
row = layout.row()
row = layout.row(align=True)
row.prop(clash_set, "tolerance")
row = layout.row()
row.prop(clash_set, "check_all")
elif clash_set.mode == "collision":
row = layout.row()
row.prop(clash_set, "allow_touching")
elif clash_set.mode == "clearance":
row = layout.row()
row.prop(clash_set, "clearance")
row = layout.row()
row.prop(clash_set, "check_all")
row = layout.row(align=True)
row.label(text="Group A:", icon="OUTLINER_OB_POINTCLOUD")
row.operator("bim.add_clash_source", icon="ADD", text="").group = "a"
layout.label(text="Group A:")
row = layout.row()
row.operator("bim.add_clash_source").group = "a"
for index, source in enumerate(clash_set.a):
for index, source in enumerate(clash_set.a):
row = layout.row(align=True)
row.prop(source, "name", text="")
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = "a"
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = "a"
row = layout.row(align=True)
row.prop(source, "mode", text="")
row.prop(source, "selector", text="")
layout.label(text="Group B:")
row = layout.row()
row.operator("bim.add_clash_source").group = "b"
for index, source in enumerate(clash_set.b):
row = layout.row(align=True)
row.prop(source, "name", text="")
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = "b"
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = "b"
row = layout.row(align=True)
row.prop(source, "mode", text="")
row.prop(source, "selector", text="")
row = layout.row()
row.prop(props, "should_create_clash_snapshots")
row = layout.row(align=True)
row.prop(source, "name", text="")
row.prop(source, "mode", text="")
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = "a"
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = "a"
if source.mode != "a":
blenderbim.bim.helper.draw_filter(
layout, source.filter_groups, ClashData, f"clash_{props.active_clash_set_index}_a_{index}"
)
row = layout.row(align=True)
row.label(text="Group B:", icon="OUTLINER_OB_POINTCLOUD")
row.operator("bim.add_clash_source", icon="ADD", text="").group = "b"
for index, source in enumerate(clash_set.b):
row = layout.row(align=True)
row.prop(source, "name", text="")
row.prop(source, "mode", text="")
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = "b"
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = "b"
if source.mode != "a":
blenderbim.bim.helper.draw_filter(
layout, source.filter_groups, ClashData, f"clash_{props.active_clash_set_index}_b_{index}"
)
row = layout.row()
row.prop(props, "should_create_clash_snapshots")
row = layout.row()
row.operator("bim.execute_ifc_clash")
row = layout.row()
row.label(text=f"{len(clash_set.clashes)} Clashes Found", icon="PIVOT_CURSOR")
layout.template_list("BIM_UL_clashes", "", props.active_clash_set, "clashes", props, "active_clash_index")
row = layout.row()
row.operator("bim.select_clash")
row.operator("bim.execute_ifc_clash")
row.operator("bim.select_ifc_clash_results")
class BIM_PT_clash_manager(Panel):
@@ -142,6 +111,29 @@ class BIM_PT_clash_manager(Panel):
pass
class BIM_PT_dumb_clash_manager(Panel):
bl_idname = "BIM_PT_dumb_clash_manager"
bl_label = "Dumb Manager"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_clash_manager"
def draw(self, context):
layout = self.layout
props = context.scene.BIMClashProperties
row = layout.row(align=True)
row.operator("bim.load_ifc_clashes", text="LOAD IFC CLASHES", icon="IMPORT")
row.operator("bim.save_ifc_clashes", text="SAVE CLASH RESULTS", icon="EXPORT")
layout.template_list("BIM_UL_clashes", "", props.active_clash_set, "clashes", props, "active_clash_index")
# bim.select_clash
row = layout.row(align=True)
row.operator("bim.select_clash", text="SELECT CLASH", icon="IMPORT")
row.prop(props, "sould_focus_on_clash", icon="RESTRICT_VIEW_OFF")
class BIM_PT_smart_clash_manager(Panel):
bl_idname = "BIM_PT_smart_clash_manager"
bl_label = "Smart Clash Manager"
@@ -54,11 +54,11 @@ classes = (
def register():
bpy.types.Scene.BIMClassificationProperties = bpy.props.PointerProperty(type=prop.BIMClassificationProperties)
bpy.types.Scene.BIMClassificationReferenceProperties = bpy.props.PointerProperty(
bpy.types.Object.BIMClassificationReferenceProperties = bpy.props.PointerProperty(
type=prop.BIMClassificationReferenceProperties
)
def unregister():
del bpy.types.Scene.BIMClassificationProperties
del bpy.types.Scene.BIMClassificationReferenceProperties
del bpy.types.Object.BIMClassificationReferenceProperties
@@ -88,7 +88,7 @@ class ClassificationReferencesData(ReferencesData):
cls.data["references"] = cls.references()
cls.data["active_classification_library"] = cls.active_classification_library()
cls.data["classifications"] = cls.classifications()
cls.data["object_type"] = "Object"
cls.data["object_type"] = "OBJECT"
@classmethod
def references(cls):
@@ -112,21 +112,17 @@ class MaterialClassificationsData(ReferencesData):
cls.data["references"] = cls.references()
cls.data["active_classification_library"] = cls.active_classification_library()
cls.data["classifications"] = cls.classifications()
cls.data["object_type"] = "Material"
cls.data["object_type"] = "MATERIAL"
@classmethod
def references(cls):
results = []
props = bpy.context.scene.BIMMaterialProperties
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index]
if material.ifc_definition_id:
element = tool.Ifc.get().by_id(material.ifc_definition_id)
for reference in ifcopenshell.util.classification.get_references(element):
data = reference.get_info()
del data["ReferencedSource"]
results.append(data)
element = tool.Ifc.get_entity(bpy.context.active_object.active_material)
if element:
for reference in ifcopenshell.util.classification.get_references(element):
data = reference.get_info()
del data["ReferencedSource"]
results.append(data)
return results
@@ -140,7 +136,7 @@ class CostClassificationsData(ReferencesData):
cls.data["references"] = cls.references()
cls.data["active_classification_library"] = cls.active_classification_library()
cls.data["classifications"] = cls.classifications()
cls.data["object_type"] = "Cost"
cls.data["object_type"] = "COST"
@classmethod
def references(cls):
@@ -20,8 +20,6 @@ import bpy
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.classification
import ifcopenshell.util.element
import blenderbim.tool as tool
import blenderbim.bim.helper
from blenderbim.bim.ifc import IfcStore
@@ -77,37 +75,26 @@ class AddManualClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_manual_classification_reference"
bl_label = "Add Manual Classification Reference"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def _execute(self, context):
if self.obj_type == "Object":
if context.selected_objects:
objects = [o.name for o in context.selected_objects]
else:
objects = [context.active_object.name]
else:
objects = [self.obj]
props = context.scene.BIMClassificationReferenceProperties
obj = context.active_object
props = obj.BIMClassificationReferenceProperties
attributes = blenderbim.bim.helper.export_attributes(props.reference_attributes)
products = [
tool.Ifc.get().by_id(ifc_definition_id)
for obj in objects
if (ifc_definition_id := tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context))
]
if products:
classification = tool.Ifc.get().by_id(int(props.classifications))
reference = ifcopenshell.api.run(
"classification.add_reference",
tool.Ifc.get(),
products=products,
classification=classification,
identification="X",
name="Unnamed",
)
ifcopenshell.api.run(
"classification.edit_reference", tool.Ifc.get(), reference=reference, attributes=attributes
)
product = tool.Ifc.get_entity(obj)
# TODO: refactor and support material and cost item classifications
classification = tool.Ifc.get().by_id(int(props.classifications))
reference = ifcopenshell.api.run(
"classification.add_reference",
tool.Ifc.get(),
product=product,
classification=classification,
identification="X",
name="Unnamed",
)
ifcopenshell.api.run(
"classification.edit_reference", tool.Ifc.get(), reference=reference, attributes=attributes
)
props.is_adding = False
@@ -161,7 +148,8 @@ class EnableAddingManualClassificationReference(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BIMClassificationReferenceProperties
obj = context.active_object
props = obj.BIMClassificationReferenceProperties
props.is_adding = True
props.reference_attributes.clear()
blenderbim.bim.helper.import_attributes2("IfcClassificationReference", props.reference_attributes)
@@ -174,7 +162,8 @@ class DisableAddingManualClassificationReference(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BIMClassificationReferenceProperties
obj = context.active_object
props = obj.BIMClassificationReferenceProperties
props.is_adding = False
return {"FINISHED"}
@@ -218,10 +207,6 @@ class DisableEditingClassification(bpy.types.Operator):
class RemoveClassification(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_classification"
bl_label = "Remove Classification"
bl_description = (
"The classification and all of its relationships, children references, "
"and relationships between objects and child references will be completely removed from a project"
)
bl_options = {"REGISTER", "UNDO"}
classification: bpy.props.IntProperty()
@@ -266,7 +251,8 @@ class EnableEditingClassificationReference(bpy.types.Operator):
obj: bpy.props.StringProperty()
def execute(self, context):
props = context.scene.BIMClassificationReferenceProperties
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
props = obj.BIMClassificationReferenceProperties
props.reference_attributes.clear()
blenderbim.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.reference), props.reference_attributes)
props.active_reference_id = self.reference
@@ -280,7 +266,8 @@ class DisableEditingClassificationReference(bpy.types.Operator):
obj: bpy.props.StringProperty()
def execute(self, context):
context.scene.BIMClassificationReferenceProperties.active_reference_id = 0
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
obj.BIMClassificationReferenceProperties.active_reference_id = 0
return {"FINISHED"}
@@ -305,25 +292,20 @@ class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
active_reference = tool.Ifc.get().by_id(self.reference)
identification = active_reference[1]
elements_by_references: dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]] = {}
for obj in objects:
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
element = tool.Ifc.get().by_id(ifc_definition_id)
references = ifcopenshell.util.classification.get_references(element, should_inherit=False)
for reference in references:
if (identification and reference[1] == identification) or (
not identification and reference == active_reference
):
elements_by_references.setdefault(reference, []).append(element)
if elements_by_references:
for reference, products in elements_by_references.items():
ifcopenshell.api.run(
"classification.remove_reference",
tool.Ifc.get(),
reference=reference,
products=products,
)
ifcopenshell.api.run(
"classification.remove_reference",
tool.Ifc.get(),
reference=reference,
product=element,
)
class EditClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
@@ -333,7 +315,8 @@ class EditClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
obj: bpy.props.StringProperty()
def _execute(self, context):
props = context.scene.BIMClassificationReferenceProperties
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
props = obj.BIMClassificationReferenceProperties
attributes = {}
for attribute in props.reference_attributes:
if attribute.is_null:
@@ -374,17 +357,15 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
classification = element
break
products = [
tool.Ifc.get().by_id(ifc_definition_id)
for obj in objects
if (ifc_definition_id := tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context))
]
if products:
for obj in objects:
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
if not ifc_definition_id:
continue
ifcopenshell.api.run(
"classification.add_reference",
tool.Ifc.get(),
reference=IfcStore.classification_file.by_id(self.reference),
products=products,
product=tool.Ifc.get().by_id(ifc_definition_id),
classification=classification,
)
@@ -425,14 +406,14 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
classification.Location = bsdd_classification.domain_namespace_uri
for obj in objects:
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
if not ifc_definition_id:
continue
element = tool.Ifc.get().by_id(ifc_definition_id)
reference = ifcopenshell.api.run(
"classification.add_reference",
tool.Ifc.get(),
products=[element],
product=element,
classification=classification,
identification=bsdd_classification.reference_code,
name=bsdd_classification.name,
@@ -118,9 +118,10 @@ class BIM_PT_classifications(Panel):
class ReferenceUI:
def draw_ui(self, context):
obj = context.active_object
self.oprops = obj.BIMObjectProperties
self.sprops = context.scene.BIMClassificationProperties
self.bprops = context.scene.BIMBSDDProperties
self.props = context.scene.BIMClassificationReferenceProperties
self.props = obj.BIMClassificationReferenceProperties
self.file = IfcStore.get_file()
self.draw_add_ui(context)
@@ -154,7 +155,7 @@ class ReferenceUI:
blenderbim.bim.helper.draw_attributes(self.props.reference_attributes, self.layout)
row = self.layout.row(align=True)
op = row.operator("bim.add_manual_classification_reference", text="Save", icon="CHECKMARK")
op.obj_type = self.data.data["object_type"]
op.type = self.data.data["object_type"]
row.operator("bim.disable_adding_manual_classification_reference", text="", icon="CANCEL")
else:
row = self.layout.row()
@@ -294,31 +295,28 @@ class BIM_PT_material_classifications(Panel, ReferenceUI):
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_materials"
bl_context = "material"
@classmethod
def poll(cls, context):
if not tool.Ifc.get():
return False
props = context.scene.BIMMaterialProperties
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index]
if material.ifc_definition_id:
return True
return False
try:
return bool(context.active_object.active_material.BIMObjectProperties.ifc_definition_id)
except:
return False
def draw(self, context):
if not MaterialClassificationsData.is_loaded:
MaterialClassificationsData.load()
self.data = MaterialClassificationsData
self.obj = ""
self.obj = context.active_object.active_material.name
self.obj_type = "Material"
self.draw_ui(context)
class BIM_PT_cost_classifications(Panel, ReferenceUI):
bl_label = "Cost Item Classifications"
bl_label = "Cost Classifications"
bl_idname = "BIM_PT_cost_classifications"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
@@ -114,7 +114,7 @@ class EditObjective(bpy.types.Operator):
ifcopenshell.api.run(
"constraint.edit_objective",
self.file,
**{"objective": self.file.by_id(props.active_constraint_id), "attributes": attributes},
**{"objective": self.file.by_id(props.active_constraint_id), "attributes": attributes}
)
bpy.ops.bim.load_objectives()
return {"FINISHED"}
@@ -179,17 +179,19 @@ class AssignConstraint(bpy.types.Operator):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
self.file = tool.Ifc.get()
self.file = IfcStore.get_file()
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)]
if products:
for obj in objs:
obj_id = obj.BIMObjectProperties.ifc_definition_id
if not obj_id:
continue
ifcopenshell.api.run(
"constraint.assign_constraint",
self.file,
**{
"products": products,
"product": self.file.by_id(obj_id),
"constraint": self.file.by_id(self.constraint),
},
}
)
return {"FINISHED"}
@@ -205,16 +207,18 @@ class UnassignConstraint(bpy.types.Operator):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
self.file = tool.Ifc.get()
self.file = IfcStore.get_file()
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)]
if products:
for obj in objs:
obj_id = obj.BIMObjectProperties.ifc_definition_id
if not obj_id:
continue
ifcopenshell.api.run(
"constraint.unassign_constraint",
self.file,
**{
"products": products,
"product": self.file.by_id(obj_id),
"constraint": self.file.by_id(self.constraint),
},
}
)
return {"FINISHED"}
@@ -53,10 +53,6 @@ class AddContext(bpy.types.Operator, Operator):
class RemoveContext(bpy.types.Operator, Operator):
bl_idname = "bim.remove_context"
bl_label = "Remove Context"
bl_description = (
"Remove representation context. Any representation geometry that is assigned to the context is also removed. "
"If a context is removed, then any subcontexts are also removed"
)
bl_options = {"REGISTER", "UNDO"}
context: bpy.props.IntProperty()
@@ -82,7 +82,6 @@ classes = (
prop.CostItem,
prop.CostItemQuantity,
prop.CostItemType,
prop.ScheduleColumn,
prop.BIMCostProperties,
ui.BIM_PT_cost_schedules,
ui.BIM_PT_cost_item_quantities,
@@ -19,13 +19,10 @@
import bpy
import ifcopenshell
import ifcopenshell.util.cost
import ifcopenshell.util.date
import ifcopenshell.util.element
import ifcopenshell.util.unit
import blenderbim.tool as tool
import blenderbim.bim.schema
from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc
from typing import Any
def refresh():
@@ -37,7 +34,6 @@ def refresh():
class CostSchedulesData:
data = {}
is_loaded = False
_cost_values: dict[int, dict[str, Any]]
@classmethod
def load(cls):
@@ -140,7 +136,7 @@ class CostSchedulesData:
data["UnitBasisUnitSymbol"] = "U"
if cost_value.Category == "*":
is_sum = True
cost_quantity = 1 if data["TotalCostQuantity"] is None else data["TotalCostQuantity"]
cost_quantity = data["TotalCostQuantity"] or 1
if has_unit_basis:
data["TotalCost"] = data["TotalAppliedValue"] * cost_quantity / data["UnitBasisValueComponent"]
else:
@@ -159,7 +155,6 @@ class CostSchedulesData:
data["UnitSymbol"] = "-"
if cost_item.CostQuantities:
quantity = cost_item.CostQuantities[0]
data["QuantityType"] = quantity.is_a()
unit = ifcopenshell.util.unit.get_property_unit(quantity, tool.Ifc.get())
if unit:
data["UnitSymbol"] = ifcopenshell.util.unit.get_unit_symbol(unit)
@@ -198,16 +193,10 @@ class CostSchedulesData:
# data["DerivedUnitSymbol"] = "?"
# print("Total Cost", data["DerivedTotalCostQuantity"], cost_item.Name)
# TODO: dead code?
@classmethod
def _get_object_quantities(
cls, cost_item: ifcopenshell.entity_instance, element: ifcopenshell.entity_instance
) -> list[int]:
def _get_object_quantities(cls, cost_item, element):
if not element.is_a("IfcObject"):
return []
cost_quantities = cost_item.CostQuantities
if not cost_quantities:
return []
results = []
for relationship in element.IsDefinedBy:
if not relationship.is_a("IfcRelDefinesByProperties"):
@@ -216,7 +205,7 @@ class CostSchedulesData:
if not qto.is_a("IfcElementQuantity"):
continue
for prop in qto.Quantities:
if prop in cost_quantities:
if prop in cost_item.CostQuantities or []:
results.append(prop.id())
return results
@@ -73,7 +73,7 @@ class RemoveCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
cost_schedule: bpy.props.IntProperty()
def _execute(self, context):
core.remove_cost_schedule(tool.Ifc, tool.Cost, cost_schedule=tool.Ifc.get().by_id(self.cost_schedule))
core.remove_cost_schedule(tool.Ifc, cost_schedule=tool.Ifc.get().by_id(self.cost_schedule))
class EnableEditingCostSchedule(bpy.types.Operator):
@@ -223,40 +223,36 @@ class EditCostItem(bpy.types.Operator, tool.Ifc.Operator):
class AssignCostItemType(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_cost_item_type"
bl_label = "Assign Cost Item To Product Types"
bl_description = "Assign cost item to currently selected or active product types"
bl_label = "Assign Cost Item Type Product"
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
prop_name: bpy.props.StringProperty()
def _execute(self, context):
product_types = core.assign_cost_item_type(
core.assign_cost_item_type(
tool.Ifc,
tool.Cost,
tool.Spatial,
cost_item=tool.Ifc.get().by_id(self.cost_item),
prop_name=self.prop_name, # TODO: REVIEW PROP_NAME USABILITY
)
self.report({"INFO"}, f"Cost item was assigned to {len(product_types)} product types.")
class UnassignCostItemType(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unassign_cost_item_type"
bl_label = "Unassign Cost Item Type"
bl_description = "Unassign cost item from currently selected or active product types"
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
related_object: bpy.props.IntProperty()
def _execute(self, context):
product_types = core.unassign_cost_item_type(
core.unassign_cost_item_type(
tool.Ifc,
tool.Cost,
tool.Spatial,
cost_item=tool.Ifc.get().by_id(self.cost_item),
product_types=[tool.Ifc.get().by_id(self.related_object)] if self.related_object else None,
product_types=[tool.Ifc.get().by_id(self.related_object)] if self.related_object else [],
)
self.report({"INFO"}, f"Cost item was unassigned from {len(product_types)} product types.")
return {"FINISHED"}
@@ -291,11 +287,9 @@ class UnassignCostItemQuantity(bpy.types.Operator, tool.Ifc.Operator):
tool.Ifc,
tool.Cost,
cost_item=tool.Ifc.get().by_id(self.cost_item),
products=(
[tool.Ifc.get().by_id(self.related_object)]
if self.related_object
else tool.Spatial.get_selected_products()
),
products=[tool.Ifc.get().by_id(self.related_object)]
if self.related_object
else tool.Spatial.get_selected_products(),
)
@@ -515,7 +509,7 @@ class SelectCostScheduleProducts(bpy.types.Operator):
class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
bl_idname = "bim.import_cost_schedule_csv"
bl_idname = "import_cost_schedule_csv.bim"
bl_label = "Import Cost Schedule CSV"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
@@ -533,13 +527,6 @@ class AddCostColumn(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty()
@classmethod
def poll(cls, context):
if not context.scene.BIMCostProperties.cost_column:
cls.poll_message_set("Cost column name is empty")
return False
return True
def execute(self, context):
core.add_cost_column(tool.Cost, self.name)
return {"FINISHED"}
@@ -551,7 +538,7 @@ class RemoveCostColumn(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty()
def execute(self, context):
def _execute(self, context):
core.remove_cost_column(tool.Cost, self.name)
return {"FINISHED"}
@@ -669,17 +656,13 @@ class ExportCostSchedules(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Export a cost schedule to a CSV, XSLX OR ODS file"
cost_schedule: bpy.props.IntProperty()
format: bpy.props.EnumProperty(name="Format", items=(("CSV", "CSV", ""), ("XLSX", "XLSX", ""), ("ODS", "ODS", "")))
directory: bpy.props.StringProperty(subtype="FILE_PATH")
filter_folder: bpy.props.BoolProperty(
name="Filter Folders",
default=True,
)
format: bpy.props.EnumProperty("Format", items=(("CSV", "CSV", ""), ("XLSX", "XLSX", ""), ("ODS", "ODS", "")))
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
cost_schedule = tool.Ifc.get().by_id(self.cost_schedule) if self.cost_schedule else None
r = core.export_cost_schedules(
tool.Cost, filepath=self.directory, format=self.format, cost_schedule=cost_schedule
tool.Cost, filepath=self.filepath, format=self.format, cost_schedule=cost_schedule
)
if isinstance(r, str):
self.report({"ERROR"}, r)
@@ -693,7 +676,6 @@ class ExportCostSchedules(bpy.types.Operator):
def draw(self, context):
self.layout.label(text="Choose a format")
self.layout.prop(self, "format")
self.layout.label(text="Select a directory.")
class ClearCostItemAssignments(bpy.types.Operator, tool.Ifc.Operator):
@@ -175,10 +175,6 @@ def update_active_cost_item_resources(self, context):
bpy.ops.bim.load_cost_item_resource_quantities()
class ScheduleColumn(PropertyGroup):
schedule_id: IntProperty()
class BIMCostProperties(PropertyGroup):
cost_schedule_predefined_types: EnumProperty(
items=get_schedule_predefined_types, name="Predefined Type", default=None
@@ -214,14 +210,9 @@ class BIMCostProperties(PropertyGroup):
cost_value_attributes: CollectionProperty(name="Cost Value Attributes", type=Attribute)
cost_value_formula: StringProperty(name="Cost Value Formula")
cost_column: StringProperty(name="Cost Column")
should_show_column_ui: BoolProperty(
name="Should Show Column UI",
description="Display UI for adding cost schedule columns, column names represent a category for cost item values",
default=False,
)
should_show_column_ui: BoolProperty(name="Should Show Column UI", default=False)
should_show_currency_ui: BoolProperty(name="Should Show Currency UI", default=False)
columns: CollectionProperty(name="Active Schedule Columns", type=StrProperty)
columns_storage: CollectionProperty(name="Columns", type=ScheduleColumn)
columns: CollectionProperty(name="Columns", type=StrProperty)
active_column_index: IntProperty(name="Active Column Index")
cost_item_products: CollectionProperty(name="Cost Item Products", type=CostItemQuantity)
active_cost_item_product_index: IntProperty(name="Active Cost Item Product Index")
+10 -22
View File
@@ -21,7 +21,6 @@ import blenderbim.bim.module.cost.prop as CostProp
from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.cost.data import CostSchedulesData
from typing import Any
class BIM_PT_cost_schedules(Panel):
@@ -54,7 +53,7 @@ class BIM_PT_cost_schedules(Panel):
row.label(text="No Cost Schedules found.", icon="TEXT")
row.operator("bim.add_cost_schedule", icon="ADD", text="")
row.operator("bim.import_cost_schedule_csv",icon="IMPORT",text="")
row.operator("import_cost_schedule_csv.bim",icon="IMPORT",text="")
for schedule in CostSchedulesData.data["schedules"]:
self.draw_cost_schedule_ui(schedule)
@@ -179,33 +178,24 @@ class BIM_PT_cost_schedules(Panel):
"active_cost_item_index",
)
if self.props.active_cost_item_id:
cost_item = CostSchedulesData.data["cost_items"][ifc_definition_id]
if self.props.cost_item_editing_type == "ATTRIBUTES":
self.draw_editable_cost_item_attributes_ui()
elif self.props.cost_item_editing_type == "QUANTITIES":
self.draw_editable_cost_item_quantities_ui(cost_item)
self.draw_editable_cost_item_quantities_ui()
elif self.props.cost_item_editing_type == "VALUES":
self.draw_editable_cost_item_values_ui()
def draw_editable_cost_item_attributes_ui(self):
blenderbim.bim.helper.draw_attributes(self.props.cost_item_attributes, self.layout)
def draw_editable_cost_item_quantities_ui(self, cost_item: dict[str, Any]):
quantities = CostSchedulesData.data["cost_quantities"]
def draw_editable_cost_item_quantities_ui(self):
row = self.layout.row(align=True)
# In IFC, all quantities of IfcCostTime should have 1 type.
if quantities:
quantity_class = cost_item["QuantityType"]
row.label(text=quantity_class)
else:
row.prop(self.props, "quantity_types", text="")
quantity_class = self.props.quantity_types
row.prop(self.props, "quantity_types", text="")
op = row.operator("bim.add_cost_item_quantity", text="", icon="ADD")
op.cost_item = self.props.active_cost_item_id
op.ifc_class = quantity_class
op.ifc_class = self.props.quantity_types
for quantity in quantities:
for quantity in CostSchedulesData.data["cost_quantities"]:
row = self.layout.row(align=True)
row.label(text=quantity["name"])
row.label(text=quantity["value"])
@@ -644,7 +634,7 @@ class BIM_UL_cost_items_trait:
layout.label(text=cost_item["UnitBasisUnitSymbol"])
def draw_total_quantity_column(self, layout, cost_item):
if cost_item["TotalCostQuantity"] is not None:
if cost_item["TotalCostQuantity"]:
label = "{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '-'}"
layout.label(text=label)
else:
@@ -755,11 +745,9 @@ class BIM_UL_product_cost_items(UIList):
class BIM_PT_Costing_Tools(Panel):
bl_label = "5D Tools"
bl_idname = "BIM_PT_Costing_Tools"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_cost"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "4D/5D Toolkit"
def draw(self, context):
self.props = context.scene.BIMCostProperties
@@ -29,9 +29,13 @@ def register():
if not bpy.app.background:
bpy.utils.register_tool(workspace.CoveringTool, after={"bim.structural_tool"}, separator=False, group=False)
bpy.types.Scene.BIMCoveringProperties = bpy.props.PointerProperty(type=prop.BIMCoveringProperties)
# bpy.types.Object.BIMObjectSpatialProperties = bpy.props.PointerProperty(type=prop.BIMObjectSpatialProperties)
# bpy.types.Scene.BIMSpatialManagerProperties = bpy.props.PointerProperty(type=prop.BIMSpatialManagerProperties)
def unregister():
if not bpy.app.background:
bpy.utils.unregister_tool(workspace.CoveringTool)
del bpy.types.Scene.BIMCoveringProperties
# del bpy.types.Object.BIMObjectSpatialProperties
# del bpy.types.Scene.BIMSpatialManagerProperties
@@ -107,88 +107,59 @@ class CoveringToolUI:
# elif element and bpy.context.selected_objects and element.is_a("IfcSpace"):
# op. = row.operator("bim.add_istance_flooring_from_spaces"):
# if (type_material_usage == "IfcMaterialLayerSet" and
# not bpy.context.selected_objects):
# row = cls.layout.row(align=True)
# row.label(text="", icon="EVENT_SHIFT")
# row.label(text="", icon="EVENT_A")
# if tool.Ifc.get_entity(collection_obj):
# if AuthoringData.data["predefined_type"] == "FLOORING":
# op = row.operator("bim.add_instance_flooring_covering_from_cursor")
# elif AuthoringData.data["predefined_type"] == "CEILING":
# op = row.operator("bim.add_instance_ceiling_covering_from_cursor")
# else:
# op = row.operator("bim.add_constr_type_instance", text="Add")
# op.from_invoke = True
# if cls.props.relating_type_id.isnumeric():
# op.relating_type_id = int(cls.props.relating_type_id)
#
# else:
# op = row.operator("bim.add_constr_type_instance", text="Add")
# op.from_invoke = True
# if cls.props.relating_type_id.isnumeric():
# op.relating_type_id = int(cls.props.relating_type_id)
#
# elif (AuthoringData.data["predefined_type"] == "FLOORING" and
# type_material_usage == "IfcMaterialLayerSet" and
# element and
# bpy.context.selected_objects and
# element.is_a("IfcWall")):
# row = cls.layout.row(align=True)
# row.label(text="", icon="EVENT_SHIFT")
# row.label(text="", icon="EVENT_A")
# op = row.operator("bim.add_instance_flooring_coverings_from_walls")
#
# elif (AuthoringData.data["predefined_type"] == "CEILING" and
# type_material_usage == "IfcMaterialLayerSet" and
# element and
# bpy.context.selected_objects and
# element.is_a("IfcWall")):
# row = cls.layout.row(align=True)
# row.label(text="", icon="EVENT_SHIFT")
# row.label(text="", icon="EVENT_A")
# op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
#
# elif (element and
# bpy.context.selected_objects and
# element.is_a("IfcCovering") and
## AuthoringData.data["predefined_type"] == "FLOORING" and
# AuthoringData.data["active_material_usage"] == "LAYER3"):
# row = cls.layout.row(align=True)
# row.label(text="", icon="EVENT_SHIFT")
# row.label(text="", icon="EVENT_G")
# op = row.operator("bim.regen_selected_covering_object")
if (type_material_usage == "IfcMaterialLayerSet" and
not bpy.context.selected_objects):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
if tool.Ifc.get_entity(collection_obj):
if AuthoringData.data["predefined_type"] == "FLOORING":
op = row.operator("bim.add_instance_flooring_covering_from_cursor")
elif AuthoringData.data["predefined_type"] == "CEILING":
op = row.operator("bim.add_instance_ceiling_covering_from_cursor")
else:
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
else:
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
elif (AuthoringData.data["predefined_type"] == "FLOORING" and
type_material_usage == "IfcMaterialLayerSet" and
element and
bpy.context.selected_objects and
element.is_a("IfcWall")):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_flooring_coverings_from_walls")
elif (AuthoringData.data["predefined_type"] == "CEILING" and
type_material_usage == "IfcMaterialLayerSet" and
element and
bpy.context.selected_objects and
element.is_a("IfcWall")):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
elif (element and
bpy.context.selected_objects and
element.is_a("IfcCovering") and
# AuthoringData.data["predefined_type"] == "FLOORING" and
AuthoringData.data["active_material_usage"] == "LAYER3"):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
op = row.operator("bim.regen_selected_covering_object")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_constr_type_instance", text="Add")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_flooring_covering_from_cursor")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_ceiling_covering_from_cursor")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_flooring_coverings_from_walls")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
op = row.operator("bim.regen_selected_covering_object")
# elif AuthoringData.data["predefined_type"] == "CEILING":
# row = cls.layout.row(align=True)
@@ -203,14 +174,14 @@ class CoveringToolUI:
# op.from_invoke = True
# if cls.props.relating_type_id.isnumeric():
# op.relating_type_id = int(cls.props.relating_type_id)
# else:
# row = cls.layout.row(align=True)
# row.label(text="", icon="EVENT_SHIFT")
# row.label(text="", icon="EVENT_A")
# op = row.operator("bim.add_constr_type_instance", text="Add")
# op.from_invoke = True
# if cls.props.relating_type_id.isnumeric():
# op.relating_type_id = int(cls.props.relating_type_id)
else:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
@classmethod
def draw_type_selection_interface(cls):
@@ -220,11 +191,11 @@ class CoveringToolUI:
row = cls.layout.row(align=True)
row.label(text="", icon="FILE_3D")
prop_with_search(row, cls.props, "relating_type_id", text="")
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="")
else:
row.label(text=f"No {AuthoringData.data['ifc_element_type']} Found", icon="ERROR")
row = cls.layout.row()
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="Launch Type Manager")
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="Launch Type Manager")
@classmethod
def draw_basic_bim_tool_interface(cls):
@@ -23,7 +23,6 @@ import ifccsv
import logging
import tempfile
import ifcopenshell
import ifcopenshell.util.selector
import blenderbim.tool as tool
import blenderbim.bim.module.drawing.scheduler as scheduler
from blenderbim.bim.ifc import IfcStore
@@ -73,7 +72,7 @@ class ReorderCsvAttribute(bpy.types.Operator):
def execute(self, context):
old = context.scene.CsvProperties.csv_attributes[self.old_index]
new = context.scene.CsvProperties.csv_attributes[self.new_index]
props = ["name", "header", "sort", "group", "varies_value", "summary", "formatting"]
props = ["name", "header", "sort", "group", "varies_value", "summary"]
for prop in props:
value = getattr(new, prop)
setattr(new, prop, getattr(old, prop))
@@ -247,7 +246,6 @@ class ExportIfcCsv(bpy.types.Operator):
if props.format != "csv" and props.should_generate_svg:
schedule_creator = scheduler.Scheduler()
schedule_creator.schedule(self.filepath, tool.Drawing.get_path_with_ext(self.filepath, "svg"))
self.report({"INFO"}, f"Data is exported to {props.format.upper()}.")
return {"FINISHED"}
@@ -287,7 +285,6 @@ class ImportIfcCsv(bpy.types.Operator):
if not props.should_load_from_memory:
ifc_file.write(props.csv_ifc_file)
refresh_ui_data()
self.report({"INFO"}, "Data is imported to IFC.")
return {"FINISHED"}
@@ -24,23 +24,22 @@ classes = (
operator.CopyDebugInformation,
operator.CreateAllShapes,
operator.CreateShapeFromStepId,
operator.DebugActiveDrawing,
operator.InspectFromObject,
operator.InspectFromStepId,
operator.OverrideDisplayType,
operator.ParseExpress,
operator.PipInstall,
operator.PrintIfcFile,
operator.PrintObjectPlacement,
operator.PrintUnusedElementStats,
operator.ProfileImportIFC,
operator.PurgeHdf5Cache,
operator.PurgeUnusedElementsByClass,
operator.PurgeIfcLinks,
operator.RewindInspector,
operator.SelectExpressFile,
operator.SelectHighPolygonMeshes,
operator.SelectHighestPolygonMeshes,
operator.ValidateIfcFile,
operator.PrintUnusedElementStats,
operator.PurgeUnusedElementsByClass,
prop.BIMDebugProperties,
ui.BIM_PT_debug,
)
@@ -17,33 +17,51 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import bpy
import time
import random
import logging
import platform
import subprocess
import addon_utils
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import blenderbim.tool as tool
import blenderbim.core.debug as core
import blenderbim.bim.handler
import blenderbim.bim.import_ifc as import_ifc
from blenderbim import get_debug_info, format_debug_info
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
class CopyDebugInformation(bpy.types.Operator):
bl_idname = "bim.copy_debug_information"
bl_label = "Copy Debug Information"
bl_description = "Copies debugging information to your clipboard for use in bug reports"
bl_description = "Copies debugging information to your clipboard for use in bugreports"
def execute(self, context):
info = get_debug_info()
version = ".".join(
[
str(x)
for x in [
addon.bl_info.get("version", (-1, -1, -1))
for addon in addon_utils.modules()
if addon.bl_info["name"] == "BlenderBIM"
][0]
]
)
info = {
"os": platform.system(),
"os_version": platform.version(),
"python_version": platform.python_version(),
"architecture": platform.architecture(),
"machine": platform.machine(),
"processor": platform.processor(),
"blender_version": bpy.app.version_string,
"blenderbim_version": version,
"ifc": False,
}
if tool.Ifc.get():
info.update(
{
@@ -54,15 +72,17 @@ class CopyDebugInformation(bpy.types.Operator):
}
)
text = format_debug_info(info)
# Format it in a readable way
text = "\n".join(f"{k}: {v}" for k, v in info.items())
print(text)
text_with_backticks = f"```\n{text}\n```"
print("-" * 80)
print(text_with_backticks)
print("-" * 80)
context.window_manager.clipboard = text_with_backticks
if platform.system() == "Windows":
command = "echo | set /p nul=" + text.strip()
elif platform.system() == "Darwin": # for MacOS
command = 'printf "' + text.strip().replace("\n", "\\n").replace('"', "") + '" | pbcopy'
else: # Linux
command = 'printf "' + text.strip().replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard'
subprocess.run(command, shell=True, check=True)
return {"FINISHED"}
@@ -80,11 +100,10 @@ class PrintIfcFile(bpy.types.Operator):
return {"FINISHED"}
class ConvertToBlender(bpy.types.Operator):
bl_idname = "bim.convert_to_blender"
bl_label = "Convert To Blender File"
bl_description = "Removes all IFC data and revert to basic Blender objects.\nWarning : Cannot be undone."
bl_options = {"REGISTER", "UNDO"}
class PurgeIfcLinks(bpy.types.Operator):
bl_idname = "bim.purge_ifc_links"
bl_label = "Purge IFC Links"
bl_description = "Purge all definitions and references from the file.\nWarning : Cannot be undone."
def execute(self, context):
for obj in bpy.data.objects:
@@ -93,8 +112,7 @@ class ConvertToBlender(bpy.types.Operator):
if obj.data:
obj.data.BIMMeshProperties.ifc_definition_id = 0
for material in bpy.data.materials:
material.BIMObjectProperties.ifc_definition_id = 0
material.BIMMaterialProperties.ifc_style_id = 0
material.BIMMaterialProperties.ifc_style_id = False
context.scene.BIMProperties.ifc_file = ""
context.scene.BIMDebugProperties.attributes.clear()
IfcStore.purge()
@@ -102,6 +120,26 @@ class ConvertToBlender(bpy.types.Operator):
return {"FINISHED"}
class ConvertToBlender(bpy.types.Operator):
bl_idname = "bim.convert_to_blender"
bl_label = "Convert To Blender File"
bl_description = "Removes all IFC data, and converts the file to a simple Blender file."
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
for o in bpy.data.objects:
if o.type in {"MESH", "EMPTY"}:
o.BIMObjectProperties.ifc_definition_id = 0
if o.data:
o.data.BIMMeshProperties.ifc_definition_id = 0
for m in bpy.data.materials:
m.BIMMaterialProperties.ifc_style_id = False
bpy.context.scene.BIMProperties.ifc_file = ""
IfcStore.purge()
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
class ValidateIfcFile(bpy.types.Operator):
bl_idname = "bim.validate_ifc_file"
bl_label = "Validate IFC File"
@@ -585,140 +623,3 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
print("Finished 20 batches. Manually stopping in case of infinite loop.")
self.report({"INFO"}, f"Auto purged {total_purged} orphaned elements")
tool.Ifc.get().write(self.filepath)
class PipInstall(bpy.types.Operator):
bl_idname = "bim.pip_install"
bl_label = "Pip Install"
bl_description = "Installs a package from PyPI"
name: bpy.props.StringProperty()
def execute(self, context):
import blenderbim.bim
target = [p for p in sys.path if p.endswith("packages") and "addons" in p][0]
py_exec = str(sys.executable)
print("Detected executable:", py_exec)
print("Installing to:", target)
subprocess.call([py_exec, "-m", "ensurepip", "--user"])
subprocess.call([py_exec, "-m", "pip", "install", "--upgrade", "pip"])
# Trusted host to make things simpler due to some enterprise network restrictions
subprocess.call(
[
py_exec,
"-m",
"pip",
"install",
f"--target={target}",
"--upgrade",
self.name,
"--trusted-host",
"pypi.org",
"--trusted-host",
"files.pythonhosted.org",
]
)
return {"FINISHED"}
class DebugActiveDrawing(bpy.types.Operator):
bl_idname = "bim.debug_active_drawing"
bl_label = "Search Active Drawing For Failing Elements"
bl_description = (
"Will iterate over all visible drawing's objects, trying to narrow down the list of possible failing objects"
)
def execute(self, context: bpy.types.Context):
props = context.scene.DocProperties
drawing_item = props.drawings[props.active_drawing_index]
drawing = tool.Ifc.get().by_id(drawing_item.ifc_definition_id)
GREEN = "\033[92m"
CYAN = "\033[96m"
END = "\033[0m"
ATTEMPS = 10
# run create drawing with sync for once
# to make sure everything is actually in sync
try:
bpy.ops.bim.create_drawing(sync=True)
except:
pass
else:
self.report({"INFO"}, "No errors creating drawing, nothing to investigate.")
return {"FINISHED"}
all_elements = [e for obj in context.visible_objects if (e := tool.Ifc.get_entity(obj))]
all_elements = set(all_elements)
original_exclude = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "Exclude")
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
def drawing_fails_to_load(chunk_to_include: set) -> bool:
current_elements = all_elements - chunk_to_include
excluded_guids = ", ".join([e.GlobalId for e in current_elements if hasattr(e, "GlobalId")])
tool.Ifc.run("pset.edit_pset", pset=pset, properties={"Exclude": f"{original_exclude}, {excluded_guids}"})
try:
bpy.ops.bim.create_drawing(sync=False)
result = False
except Exception as e:
# print(e)
result = True
tool.Ifc.run("pset.edit_pset", pset=pset, properties={"Exclude": original_exclude})
return result
def test_elements(elements: list[ifcopenshell.entity_instance], attempts: int = ATTEMPS) -> None:
print(f"{CYAN}processing {len(elements)} elements{END}")
if not elements:
print(f"Empty list of elements, will stop...")
return
n_elements = len(elements)
middle = int(n_elements / 2)
chunk1, chunk2 = elements[:middle], elements[middle:]
test_chunk_1 = drawing_fails_to_load(set(chunk1))
test_chunk_2 = drawing_fails_to_load(set(chunk2))
if (
# both chunks do not fail anymore
(not test_chunk_1 and not test_chunk_2)
# or we have 1 element chunk that is still failing
or (test_chunk_1 and not chunk2)
or (test_chunk_2 and not chunk1)
):
if attempts == 0 or n_elements in (1, 2):
print(f"{GREEN}Couldn't narrow it down any further.{END}")
print(f"It's some of the {n_elements} elements:")
print(elements)
print("Let's test excluding them...")
for element in elements:
test = drawing_fails_to_load(all_elements - {element})
if test:
print(f"{CYAN}Excluding element didn't fixed the drawing: {END}")
print(element)
else:
print(f"{GREEN}Excluding element fixed the drawing: {END}")
print(element)
else:
print(f"{CYAN}Will try to reshuffle elements and try again, attempt {ATTEMPS-attempts+1}/{ATTEMPS}")
attempts -= 1
random.shuffle(elements)
test_elements(elements, attempts)
return
# if chunk fails we need to investigate it further
if test_chunk_1:
test_elements(chunk1)
if test_chunk_2:
test_elements(chunk2)
test_elements(list(all_elements))
self.report({"INFO"}, "See system console for the results")
return {"FINISHED"}
@@ -51,4 +51,3 @@ class BIMDebugProperties(PropertyGroup):
default="BOUNDS",
)
ifc_class_purge: StringProperty(name="Unused Elements IFC Class", default="")
package_name: StringProperty(name="Package Name", default="")
@@ -44,10 +44,6 @@ class BIM_PT_debug(Panel):
row.operator("bim.parse_express", icon="IMPORT", text="")
row.operator("bim.select_express_file", icon="FILE_FOLDER", text="")
row = self.layout.row(align=True)
row.prop(props, "package_name", text="")
row.operator("bim.pip_install", icon="EVENT_PAGEDOWN").name = props.package_name
row = layout.row()
row.operator("bim.reload_ifc_file", text="Incrementally Reload Changes")
@@ -60,6 +56,9 @@ class BIM_PT_debug(Panel):
row = layout.row()
row.operator("bim.purge_hdf5_cache")
row = layout.row()
row.operator("bim.purge_ifc_links")
row = layout.row()
row.operator("bim.update_representation", text="Manually Save Representation")
@@ -72,9 +71,6 @@ class BIM_PT_debug(Panel):
row = layout.row()
row.operator("bim.profile_import_ifc")
row = layout.row()
row.operator("bim.debug_active_drawing")
row = layout.split(factor=0.5, align=True)
row.operator("bim.create_shape_from_step_id").should_include_curves = False
row.operator("bim.create_shape_from_step_id", text="", icon="IPO_ELASTIC").should_include_curves = True
@@ -38,7 +38,6 @@ class DiffData:
cls.data["total_deleted"] = cls.total_deleted()
cls.data["total_changed"] = cls.total_changed()
cls.data["changes"] = cls.changes()
cls.data["saved_searches"] = cls.saved_searches()
cls.is_loaded = True
@classmethod
@@ -85,18 +84,3 @@ class DiffData:
elif element.GlobalId in diff["deleted"]:
changes["Deleted"] = True
return changes
@classmethod
def saved_searches(cls):
if not tool.Ifc.get():
return []
groups = tool.Ifc.get().by_type("IfcGroup")
results = []
for group in groups:
try:
data = json.loads(group.Description)
if isinstance(data, dict) and data.get("type", None) == "BBIM_Search" and data.get("query", None):
results.append(group)
except:
pass
return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")]
@@ -135,7 +135,6 @@ class ExecuteIfcDiff(bpy.types.Operator):
bl_label = "Execute IFC Diff"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
@@ -163,7 +162,7 @@ class ExecuteIfcDiff(bpy.types.Operator):
new = ifcopenshell.open(self.props.new_file)
relationships = [r.relationship for r in self.props.diff_relationships]
query = tool.Search.export_filter_query(self.props.filter_groups) or None
query = self.props.diff_filter_elements
ifc_diff = ifcdiff.IfcDiff(old, new, relationships=relationships, filter_elements=query)
ifc_diff.diff()
@@ -17,7 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from blenderbim.bim.prop import StrProperty, BIMFilterGroup
from blenderbim.bim.prop import StrProperty
from blenderbim.bim.module.diff.data import DiffData
from bpy.types import PropertyGroup
from bpy.props import (
@@ -48,7 +48,7 @@ class DiffProperties(PropertyGroup):
old_file: StringProperty(default="", name="Old IFC File")
new_file: StringProperty(default="", name="New IFC File")
diff_relationships: CollectionProperty(type=Relationships, name="Relationships")
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
diff_filter_elements: StringProperty(default="", name="Filter")
should_load_changed_elements: BoolProperty(name="Load Changed Elements", default=True)
active_file: EnumProperty(
items=[
@@ -19,8 +19,8 @@
from bpy.types import Panel
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.diff.data import DiffData
import blenderbim.bim.helper
import blenderbim.tool as tool
import json
class BIM_PT_diff(Panel):
@@ -93,7 +93,9 @@ class BIM_PT_diff(Panel):
remove.collection = "diff_relationships"
remove.index = index
blenderbim.bim.helper.draw_filter(self.layout, props.filter_groups, DiffData, "diff")
row = layout.row(align=True)
row.prop(props, "diff_filter_elements")
row.operator("bim.ifc_selector", icon="FILTER", text="")
row = layout.row()
row.operator("bim.execute_ifc_diff")
@@ -41,7 +41,6 @@ classes = (
operator.CleanWireframes,
operator.ContractSheet,
operator.ContractTargetView,
operator.ConvertSVGToDXF,
operator.CreateDrawing,
operator.CreateSheets,
operator.DisableAddAnnotationType,
@@ -127,7 +126,7 @@ def register():
bpy.types.Object.BIMAssignedProductProperties = bpy.props.PointerProperty(type=prop.BIMAssignedProductProperties)
bpy.types.Object.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
bpy.types.TextCurve.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
bpy.app.handlers.load_post.append(handler.load_post)
bpy.app.handlers.load_post.append(handler.toggleDecorationsOnLoad)
bpy.app.handlers.depsgraph_update_pre.append(handler.depsgraph_update_pre_handler)
bpy.types.VIEW3D_MT_image_add.append(ui.add_object_button)
@@ -141,6 +140,6 @@ def unregister():
del bpy.types.Object.BIMAssignedProductProperties
del bpy.types.Object.BIMTextProperties
del bpy.types.TextCurve.BIMTextProperties
bpy.app.handlers.load_post.remove(handler.load_post)
bpy.app.handlers.load_post.remove(handler.toggleDecorationsOnLoad)
bpy.app.handlers.depsgraph_update_pre.remove(handler.depsgraph_update_pre_handler)
bpy.types.VIEW3D_MT_image_add.remove(ui.add_object_button)
@@ -23,7 +23,6 @@ import ifcopenshell.util.element
import ifcopenshell.util.representation
import blenderbim.tool as tool
from pathlib import Path
from typing import Any, Union
def refresh():
@@ -235,9 +234,9 @@ class DecoratorData:
cut_cache = {}
layerset_cache = {}
# used by Ifc Annotations with ObjectType = "BATTING"
@classmethod
def get_batting_thickness(cls, obj):
"""used by IfcAnnotations with ObjectType = "BATTING" """
result = cls.data.get(obj.name, None)
if result is not None:
return result
@@ -249,9 +248,9 @@ class DecoratorData:
cls.data[obj.name] = thickness
return thickness
# used by Ifc Annotations with ObjectType = "SECTION"
@classmethod
def get_section_markers_display_data(cls, obj):
"""used by IfcAnnotations with ObjectType = "SECTION" """
result = cls.data.get(obj.name, None)
if result is not None:
return result
@@ -291,10 +290,10 @@ class DecoratorData:
cls.data[obj.name] = display_data
return display_data
# used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER"
@classmethod
def get_ifc_text_data(cls, obj: bpy.types.Object) -> dict[str, Any]:
"""used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER"\n
returns font size in mm for current ifc text object"""
def get_ifc_text_data(cls, obj):
"""returns font size in mm for current ifc text object"""
result = cls.data.get(obj.name, None)
if result is not None:
return result
@@ -318,7 +317,7 @@ class DecoratorData:
font_size = FONT_SIZES[font_size_type]
# get symbol
symbol = tool.Drawing.get_annotation_symbol(element)
symbol = pset_data.get("Symbol", None)
# other attributes
props_literals = props.literals
@@ -341,11 +340,6 @@ class DecoratorData:
cls.data[obj.name] = text_data
return text_data
@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:
@@ -418,12 +412,10 @@ class AnnotationData:
relating_types = []
for relating_type in tool.Ifc.get().by_type("IfcTypeProduct"):
if tool.Drawing.is_annotation_object_type(relating_type, object_type):
relating_types.append(
{
"id": relating_type.id(),
"name": relating_type.Name or "Unnamed",
"description": relating_type.Description or "No Description",
}
)
relating_types.append({
"id": relating_type.id(),
"name": relating_type.Name or "Unnamed",
"description": relating_type.Description or "No Description",
})
return sorted(relating_types, key=lambda x: x["name"])
@@ -37,7 +37,6 @@ from blenderbim.bim.module.drawing.shaders import add_verts_sequence, add_offset
from blenderbim.bim.module.drawing.helper import format_distance
from timeit import default_timer as timer
from functools import lru_cache
from typing import Optional, Iterator, Type, Union
UNSPECIAL_ELEMENT_COLOR = (0.2, 0.2, 0.2, 1) # GREY
@@ -70,7 +69,7 @@ class profile_consequential:
cls.lines = []
def worldspace_to_winspace(verts: list[Vector], context: bpy.types.Context) -> list[Vector]:
def worldspace_to_winspace(verts, context):
"""Convert world space verts to window space"""
region = context.region
region3d = context.region_data
@@ -81,7 +80,7 @@ def worldspace_to_winspace(verts: list[Vector], context: bpy.types.Context) -> l
return winspace_verts
def winspace_to_worldspace(verts: list[Vector], context: bpy.types.Context) -> list[Vector]:
def winspace_to_worldspace(verts, context):
"""Convert winspace verts to world space"""
region = context.region
region3d = context.region_data
@@ -122,18 +121,17 @@ def get_callout_head(edge_dir, edge_side, callout_size, callout_gap):
return head
def get_circle_head(size: float, segments: int = 20, rotation: float = 0.0) -> list[Vector]:
"""`rotation` value in radians"""
def get_circle_head(size, segments=20):
angle_d = 2 * pi / segments
head = []
for i in range(segments):
angle = angle_d * i + rotation
angle = angle_d * i
head.append(Vector([cos(angle), sin(angle), 0]) * size)
return head
def get_circle_head_asterisk(size: float, segments: int = 6, rotation: float = 0.0) -> Iterator[tuple[Vector, Vector]]:
circle_head = get_circle_head(size, segments, rotation)
def get_circle_head_asterisk(size, segments=6):
circle_head = get_circle_head(size, segments)
middle = segments // 2
return zip(circle_head[:middle], circle_head[middle:])
@@ -159,7 +157,9 @@ class BaseDecorator:
objecttype = "NOTDEFINED"
def __init__(self):
self.font_id = 0 # 0 is the default font
self.font_id = blf.load(
os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", "OpenGost Type B TT.ttf")
)
# POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
@@ -264,7 +264,7 @@ class BaseDecorator:
vertices = [obj.matrix_world @ v.co for v in bm.verts]
return vertices, indices
def decorate(self, context: bpy.types.Context, obj: bpy.types.Object):
def decorate(self, context, obj):
"""perform actual drawing stuff"""
raise NotImplementedError()
@@ -310,28 +310,13 @@ class BaseDecorator:
self.draw_lines(context, obj, output_verts, output_edges)
def draw_batch(
self,
shader_type: str,
content_pos: list[Vector],
color: tuple[float, float, float, float],
indices: Optional[list[tuple[int, int]]] = None,
) -> None:
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.base_shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def draw_lines(
self,
context: bpy.types.Context,
obj: Optional[bpy.types.Object],
vertices: list[Vector],
indices: list[tuple[int, int]],
color: Optional[tuple[float, float, float, float]] = None,
line_width: float = 1.0,
) -> None:
# TODO: `obj` is actually never used, need to remove it
def draw_lines(self, context, obj, vertices, indices, color=None, line_width=1.0):
"""`verts` should be in winspace with `(0,0,0)` in the screen left bottom corner, not in the center"""
region = context.region
if not color:
@@ -344,7 +329,7 @@ class BaseDecorator:
gpu.state.blend_set("ALPHA")
self.draw_batch("LINES", vertices, color, indices)
def get_viewport_drawing_scale(self, context: bpy.types.Context) -> float:
def get_viewport_drawing_scale(self, context):
# Horrific prototype code
factor = self.camera_zoom_to_factor(context.space_data.region_3d.view_camera_zoom)
camera_width_px = factor * context.region.width
@@ -402,6 +387,7 @@ class BaseDecorator:
line_no += 1 if multiline_to_bottom else -1
return
# 0 is the default font, but we're fancier than that
font_id = self.font_id
color = context.preferences.addons["blenderbim"].preferences.decorations_colour
@@ -415,14 +401,12 @@ class BaseDecorator:
factor = self.camera_zoom_to_factor(context.space_data.region_3d.view_camera_zoom)
camera_width_px = factor * context.region.width
mm_to_px = camera_width_px / self.get_camera_width_mm()
# magic_font_scale's default of (0.004118616) is a magic constant number I visually discovered to get the right number.
# 0.004118616 is a magic constant number I visually discovered to get the right number.
# In particular it works only for the OpenGOST font and produces a 2.5mm font size.
# It probably should be dynamically calculated using system.dpi or something.
# font_size = 16 <-- this is a good default
# TODO: need to synchronize it better with svg
magic_font_scale = bpy.context.scene.DocProperties.magic_font_scale
font_size_px = int(magic_font_scale * mm_to_px) * font_size_mm / 2.5
font_size_px = int(0.004118616 * mm_to_px) * font_size_mm / 2.5
pos = pos - line_no * font_size_px * rotation_matrix[1]
blf.size(font_id, font_size_px)
@@ -501,16 +485,14 @@ class BaseDecorator:
decimal_places = drawing_pset_data.get("DecimalPlaces", None)
return format_distance(value, precision=precision, decimal_places=decimal_places)
def draw_asterisk(self, context: bpy.types.Context, pos: Vector, rotation: float = 0.0, scale: float = 1.0) -> None:
"""`pos` is a world space position\n
`rotation` value in radians"""
def draw_asterisk(self, context, obj):
# gather geometry data and convert to winspace
verts = [pos]
verts = [obj.location]
viewportDrawingScale = self.get_viewport_drawing_scale(context)
winspace_verts = worldspace_to_winspace(verts, context)
# setup geometry parameters
circle_size = viewportDrawingScale * 4 * scale
circle_size = viewportDrawingScale * 4
output_verts = []
output_edges = []
@@ -520,63 +502,13 @@ class BaseDecorator:
}
v0 = winspace_verts[0]
asterisk_head = get_circle_head_asterisk(circle_size, rotation=rotation)
asterisk_head = get_circle_head_asterisk(circle_size)
start_i = 0
for segment in asterisk_head:
start_i = add_verts_sequence(add_offsets(v0, segment), start_i, **out_kwargs)
self.draw_lines(context, None, output_verts, output_edges)
self.draw_lines(context, obj, output_verts, output_edges)
def get_annotation_direction(self, context: bpy.types.Context, obj: bpy.types.Object) -> Vector:
camera = context.scene.camera
def get_basis_vector(matrix, i=0):
"""returns basis vector for i in world space, unaffected by object scale"""
return matrix.inverted()[i].to_3d().normalized()
text_dir_world_x_axis = get_basis_vector(obj.matrix_world)
camera_matrix = tool.Drawing.get_camera_matrix(camera)
text_dir = (camera_matrix.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized()
return text_dir
def draw_symbol(self, context: bpy.types.Context, obj: bpy.types.Object, annotation_dir: Vector) -> None:
if obj.type not in ("MESH", "EMPTY"):
return
if obj.type == "MESH":
mesh: bpy.types.Mesh = obj.data
if len(mesh.vertices) == 0:
return
if mesh.edges:
MiscDecorator.decorate(self, context, obj)
return
symbol = DecoratorData.get_symbol(obj)
if not symbol:
return
for vert in mesh.vertices:
self.draw_asterisk(context, obj.matrix_world @ vert.co)
return
# EMPTY objects
symbol = DecoratorData.get_symbol(obj)
if not symbol:
return
rotation = -Vector((1, 0)).angle_signed(annotation_dir)
# NOTE: for now we assume that scale is uniform
self.draw_asterisk(context, obj.location, rotation, obj.scale.x)
def draw_text(
self,
context: bpy.types.Context,
obj: bpy.types.Object,
text_world_position: Optional[Vector] = None,
reverse_lines_order=False,
) -> None:
def draw_text(self, context, obj, text_world_position=None, reverse_lines_order=False):
"""if `text_world_position` is not provided, the object's location will be used"""
if not text_world_position:
@@ -584,7 +516,17 @@ class BaseDecorator:
region = context.region
region3d = context.region_data
text_dir = self.get_annotation_direction(context, obj)
camera = context.scene.camera
def get_basis_vector(matrix, i=0):
"""returns basis vector for i in world space, unaffected by object scale"""
return matrix.inverted()[i].to_3d().normalized()
text_dir_world_x_axis = get_basis_vector(obj.matrix_world)
camera_matrix = tool.Drawing.get_camera_matrix(camera)
text_dir = (camera_matrix.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized()
pos = location_3d_to_region_2d(region, region3d, text_world_position)
props = obj.BIMTextProperties
@@ -597,7 +539,8 @@ class BaseDecorator:
# draw asterisk symbol to indicate that there is some symbol that's not shown in viewport
if symbol:
self.draw_symbol(context, obj, text_dir)
self.draw_asterisk(context, obj)
# NOTE: for now we assume that scale is uniform
text_scale = obj.scale.x
line_i = 0
@@ -605,7 +548,7 @@ class BaseDecorator:
for literal_data in literals_data:
box_alignment = literal_data["BoxAlignment"]
for line in literal_data["CurrentValue"].split("\n"):
for line in literal_data["CurrentValue"].split("\\n"):
self.draw_label(
context,
line,
@@ -869,9 +812,8 @@ class AngleDecorator(BaseDecorator):
base_edge = edge0 if tool.Cad.is_counter_clockwise_order(p0, p1, p2) else edge1
text_offset = (Matrix.Rotation(-angle_rad / 2, 2) @ base_edge).normalized() * (radius + ANGLE_LABEL_OFFSET)
label_position = p1 + text_offset
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
decimal_places = drawing_pset_data.get("AngleDecimalPlaces", None)
text = f"{round(angle, decimal_places)}°"
text = f"{int(angle)}deg"
label_dir = Vector((1, 0))
self.draw_label(context, text, label_position, label_dir, box_alignment="center")
@@ -1061,7 +1003,7 @@ class StairDecorator(BaseDecorator):
class MiscDecorator(BaseDecorator):
objecttype = "MISC"
def decorate(self, context: bpy.types.Context, obj: bpy.types.Object):
def decorate(self, context, obj):
if obj.data.is_editmode:
verts, idxs = self.get_editmesh_geom(obj)
else:
@@ -1563,14 +1505,6 @@ class TextDecorator(BaseDecorator):
self.draw_text(context, obj)
class SymbolDecorator(BaseDecorator):
objecttype = "SYMBOL"
def decorate(self, context: bpy.types.Context, obj: bpy.types.Object) -> None:
annotation_dir = self.get_annotation_direction(context, obj)
self.draw_symbol(context, obj, annotation_dir)
class CutDecorator:
installed = None
cache = {}
@@ -1967,7 +1901,7 @@ class CutDecorator:
class DecorationsHandler:
decorators_classes: list[Type[BaseDecorator]] = [
decorators_classes = [
DimensionDecorator,
AngleDecorator,
GridDecorator,
@@ -1984,7 +1918,6 @@ class DecorationsHandler:
SectionDecorator,
ElevationDecorator,
TextDecorator,
SymbolDecorator,
BattingDecorator,
FallDecorator,
]
@@ -2012,14 +1945,6 @@ class DecorationsHandler:
self.decorators = {cls.objecttype: cls() for cls in self.decorators_classes}
for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
self.decorators[object_type] = self.decorators["FALL"]
self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"]
if drawing_font := bpy.context.scene.DocProperties.drawing_font:
drawing_font_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", drawing_font)
if os.path.exists(drawing_font_path):
font_id = blf.load(drawing_font_path)
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?
@@ -2037,7 +1962,7 @@ class DecorationsHandler:
if not element.is_a("IfcAnnotation"):
continue
object_type: Union[str, None] = element.ObjectType
object_type = element.ObjectType
if object_type == "DRAWING":
continue
@@ -23,7 +23,7 @@ from bpy.app.handlers import persistent
@persistent
def load_post(*args):
def toggleDecorationsOnLoad(*args):
if bpy.context.scene.DocProperties.should_draw_decorations:
decoration.DecorationsHandler.install(bpy.context)
else:
@@ -188,6 +188,7 @@ def format_distance(
feet += 1
inches = 0
if not isArea:
add_inches = bool(inches) or not suppress_zero_inches
tx_dist = ""
@@ -298,9 +299,7 @@ def parse_diagram_scale(camera):
return float(numerator) / float(denominator)
def ortho_view_frame(
camera: bpy.types.Camera, margin: float = 0.015
) -> tuple[float, float, float, float, float, float]:
def ortho_view_frame(camera, margin=0.015):
"""Calculates 2d bounding box of camera view area.
Similar to `bpy.types.Camera.view_frame`
@@ -29,8 +29,6 @@ import subprocess
import numpy as np
import multiprocessing
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.ifcopenshell_wrapper
import ifcopenshell.geom
import ifcopenshell.util.selector
import ifcopenshell.util.representation
@@ -46,7 +44,6 @@ import blenderbim.bim.module.drawing.helper as helper
import blenderbim.bim.export_ifc
from blenderbim.bim.module.drawing.decoration import CutDecorator
from blenderbim.bim.module.drawing.data import DecoratorData, DrawingsData
from typing import NamedTuple, List, Union, Optional
from lxml import etree
from mathutils import Vector, Color, Matrix
from timeit import default_timer as timer
@@ -80,11 +77,6 @@ class Operator:
return {"FINISHED"}
class LineworkContexts(NamedTuple):
body: List[List[int]]
annotation: List[List[int]]
class AddAnnotationType(bpy.types.Operator, Operator):
bl_idname = "bim.add_annotation_type"
bl_label = "Add Annotation Type"
@@ -201,11 +193,6 @@ class CreateDrawing(bpy.types.Operator):
+ "SHIFT+CLICK to print all selected drawings"
)
print_all: bpy.props.BoolProperty(name="Print All", default=False, options={"SKIP_SAVE"})
sync: bpy.props.BoolProperty(
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,
)
@classmethod
def poll(cls, context):
@@ -221,15 +208,13 @@ class CreateDrawing(bpy.types.Operator):
def execute(self, context):
self.props = context.scene.DocProperties
active_drawing_id = context.scene.camera.BIMObjectProperties.ifc_definition_id
if self.print_all:
original_drawing_id = active_drawing_id
original_drawing_id = self.props.active_drawing_id
drawings_to_print = [d.ifc_definition_id for d in self.props.drawings if d.is_selected and d.is_drawing]
else:
drawings_to_print = [active_drawing_id]
drawings_to_print = [self.props.active_drawing_id]
for drawing_i, drawing_id in enumerate(drawings_to_print):
self.drawing_index = drawing_i
for drawing_id in drawings_to_print:
if self.print_all:
bpy.ops.bim.activate_drawing(drawing=drawing_id, camera_view_point=False)
@@ -344,13 +329,6 @@ class CreateDrawing(bpy.types.Operator):
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasUnderlay"):
return
svg_path = self.get_svg_path(cache_type="underlay")
if os.path.isfile(svg_path) and self.props.should_use_underlay_cache:
return svg_path
visible_object_names = {obj.name for obj in bpy.context.visible_objects}
for obj in bpy.context.view_layer.objects:
obj.hide_render = obj.name not in visible_object_names
context.scene.render.filepath = svg_path[0:-4] + ".png"
drawing_style = context.scene.DocProperties.drawing_styles[self.cprops.active_drawing_style_index]
@@ -389,129 +367,20 @@ class CreateDrawing(bpy.types.Operator):
self.svg_writer.create_blank_svg(svg_path).draw_underlay(context.scene.render.filepath).save()
return svg_path
def get_linework_contexts(self, ifc, target_view) -> LineworkContexts:
plan_body_target_contexts = []
plan_body_model_contexts = []
model_body_target_contexts = []
model_body_model_contexts = []
plan_annotation_target_contexts = []
plan_annotation_model_contexts = []
model_annotation_target_contexts = []
model_annotation_model_contexts = []
for rep_context in ifc.by_type("IfcGeometricRepresentationContext"):
if rep_context.is_a("IfcGeometricRepresentationSubContext"):
if rep_context.ContextType == "Plan":
if rep_context.ContextIdentifier in ["Body", "Facetation"]:
if rep_context.TargetView == target_view:
plan_body_target_contexts.append(rep_context.id())
elif rep_context.TargetView == "MODEL_VIEW":
plan_body_model_contexts.append(rep_context.id())
elif rep_context.ContextIdentifier == "Annotation":
if rep_context.TargetView == target_view:
plan_annotation_target_contexts.append(rep_context.id())
elif rep_context.TargetView == "MODEL_VIEW":
plan_annotation_model_contexts.append(rep_context.id())
elif rep_context.ContextType == "Model":
if rep_context.ContextIdentifier in ["Body", "Facetation"]:
if rep_context.TargetView == target_view:
model_body_target_contexts.append(rep_context.id())
elif rep_context.TargetView == "MODEL_VIEW":
model_body_model_contexts.append(rep_context.id())
elif rep_context.ContextIdentifier == "Annotation":
if rep_context.TargetView == target_view:
model_annotation_target_contexts.append(rep_context.id())
elif rep_context.TargetView == "MODEL_VIEW":
model_annotation_model_contexts.append(rep_context.id())
elif rep_context.ContextType == "Model":
# You should never purely assign to a "Model" context, but
# if you do, this is what we assume your intention is.
model_body_model_contexts.append(rep_context.id())
continue
body_contexts = (
[
plan_body_target_contexts,
plan_body_model_contexts,
model_body_target_contexts,
model_body_model_contexts,
]
if target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]
else [
model_body_target_contexts,
model_body_model_contexts,
]
)
annotation_contexts = (
[
plan_annotation_target_contexts,
plan_annotation_model_contexts,
model_annotation_target_contexts,
model_annotation_model_contexts,
]
if target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]
else [
model_annotation_target_contexts,
model_annotation_model_contexts,
]
)
return LineworkContexts(body_contexts, annotation_contexts)
def serialize_contexts_elements(
self,
ifc: ifcopenshell.file,
tree: ifcopenshell.geom.tree,
contexts: LineworkContexts,
context_type: str,
drawing_elements: set[ifcopenshell.entity_instance],
target_view: str,
) -> None:
drawing_elements = drawing_elements.copy()
contexts = getattr(contexts, context_type)
for context in contexts:
with profile(f"Processing {context_type} context"):
if not context or not drawing_elements:
continue
geom_settings = ifcopenshell.geom.settings(
DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True, INCLUDE_CURVES=True
)
if ifc.by_id(context[0]).ContextType == "Plan" and "PLAN_VIEW" in target_view:
offset = ifcopenshell.ifcopenshell_wrapper.float_array_3()
# A 2mm Z offset to combat Z-fighting in plan or RCPs
offset[2] = 0.002 if target_view == "PLAN_VIEW" else -0.002
geom_settings.offset = offset
geom_settings.set_context_ids(context)
it = ifcopenshell.geom.iterator(
geom_settings, ifc, multiprocessing.cpu_count(), include=drawing_elements
)
processed = set()
for elem in it:
processed.add(ifc.by_id(elem.id))
self.serialiser.write(elem)
tree.add_element(elem)
drawing_elements -= processed
def generate_linework(self, context: bpy.types.Context) -> Union[str, None]:
def generate_linework(self, context):
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"):
return
svg_path = self.get_svg_path(cache_type="linework")
if os.path.isfile(svg_path) and self.props.should_use_linework_cache:
return svg_path
# in case of printing multiple drawings we need to sync just once
if self.sync and self.drawing_index == 0:
with profile("sync"):
# All very hackish whilst prototyping
exporter = blenderbim.bim.export_ifc.IfcExporter(None)
exporter.file = tool.Ifc.get()
invalidated_elements = exporter.sync_all_objects(skip_unlinking=True)
invalidated_elements += exporter.sync_edited_objects()
invalidated_guids = [e.GlobalId for e in invalidated_elements if hasattr(e, "GlobalId")]
cache = IfcStore.get_cache()
[cache.remove(guid) for guid in invalidated_guids]
with profile("sync"):
# All very hackish whilst prototyping
exporter = blenderbim.bim.export_ifc.IfcExporter(None)
exporter.file = tool.Ifc.get()
invalidated_elements = exporter.sync_all_objects()
invalidated_elements += exporter.sync_edited_objects()
invalidated_guids = [e.GlobalId for e in invalidated_elements if hasattr(e, "GlobalId")]
# If we have already calculated it in the SVG in the past, don't recalculate
edited_guids = set()
@@ -530,40 +399,148 @@ class CreateDrawing(bpy.types.Operator):
files = {context.scene.BIMProperties.ifc_file: tool.Ifc.get()}
for link in context.scene.BIMProjectProperties.links:
if link.name not in IfcStore.session_files:
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
files[link.name] = IfcStore.session_files[link.name]
target_view = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]["TargetView"]
self.setup_serialiser(target_view)
tree = ifcopenshell.geom.tree()
tree.enable_face_styles(True)
for ifc_path, ifc in files.items():
# Don't use draw.main() just whilst we're prototyping and experimenting
# TODO: hash paths are never used
ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest()
ifc_cache_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
self.serialiser.setFile(ifc)
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element, ifc_file=ifc)
# Get all representation contexts to see what we're dealing with.
# Drawings only draw bodies and annotations (and facetation, due to a Revit bug).
# A drawing prioritises a target view context first, followed by a model view context as a fallback.
# Specifically for PLAN_VIEW and REFLECTED_PLAN_VIEW, any Plan context is also prioritised.
contexts = self.get_linework_contexts(ifc, target_view)
self.serialize_contexts_elements(ifc, tree, contexts, "body", drawing_elements, target_view)
self.serialize_contexts_elements(ifc, tree, contexts, "annotation", drawing_elements, target_view)
if tool.Ifc.get() == ifc and self.camera_element not in drawing_elements:
plan_body_target_contexts = []
plan_body_model_contexts = []
model_body_target_contexts = []
model_body_model_contexts = []
plan_annotation_target_contexts = []
plan_annotation_model_contexts = []
model_annotation_target_contexts = []
model_annotation_model_contexts = []
target_view = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]["TargetView"]
for rep_context in ifc.by_type("IfcGeometricRepresentationContext"):
if rep_context.is_a("IfcGeometricRepresentationSubContext"):
if rep_context.ContextType == "Plan":
if rep_context.ContextIdentifier in ["Body", "Facetation"]:
if rep_context.TargetView == target_view:
plan_body_target_contexts.append(rep_context.id())
elif rep_context.TargetView == "MODEL_VIEW":
plan_body_model_contexts.append(rep_context.id())
elif rep_context.ContextIdentifier == "Annotation":
if rep_context.TargetView == target_view:
plan_annotation_target_contexts.append(rep_context.id())
elif rep_context.TargetView == "MODEL_VIEW":
plan_annotation_model_contexts.append(rep_context.id())
elif rep_context.ContextType == "Model":
if rep_context.ContextIdentifier in ["Body", "Facetation"]:
if rep_context.TargetView == target_view:
model_body_target_contexts.append(rep_context.id())
elif rep_context.TargetView == "MODEL_VIEW":
model_body_model_contexts.append(rep_context.id())
elif rep_context.ContextIdentifier == "Annotation":
if rep_context.TargetView == target_view:
model_annotation_target_contexts.append(rep_context.id())
elif rep_context.TargetView == "MODEL_VIEW":
model_annotation_model_contexts.append(rep_context.id())
elif rep_context.ContextType == "Model":
# You should never purely assign to a "Model" context, but
# if you do, this is what we assume your intention is.
model_body_model_contexts.append(rep_context.id())
continue
body_contexts = (
[
plan_body_target_contexts,
plan_body_model_contexts,
model_body_target_contexts,
model_body_model_contexts,
]
if target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]
else [
model_body_target_contexts,
model_body_model_contexts,
]
)
annotation_contexts = (
[
plan_annotation_target_contexts,
plan_annotation_model_contexts,
model_annotation_target_contexts,
model_annotation_model_contexts,
]
if target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]
else [
model_annotation_target_contexts,
model_annotation_model_contexts,
]
)
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element)
self.setup_serialiser(ifc, target_view)
cache = IfcStore.get_cache()
[cache.remove(guid) for guid in invalidated_guids]
tree = ifcopenshell.geom.tree()
tree.enable_face_styles(True)
elements = drawing_elements.copy()
for body_context in body_contexts:
with profile("Processing body context"):
if body_context and elements:
geom_settings = ifcopenshell.geom.settings(
DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True, INCLUDE_CURVES=True
)
if ifc.by_id(body_context[0]).ContextType == "Plan" and "PLAN_VIEW" in target_view:
offset = ifcopenshell.ifcopenshell_wrapper.float_array_3()
# A 2mm Z offset to combat Z-fighting in plan or RCPs
offset[2] = 0.002 if target_view == "PLAN_VIEW" else -0.002
geom_settings.offset = offset
geom_settings.set_context_ids(body_context)
it = ifcopenshell.geom.iterator(
geom_settings, ifc, multiprocessing.cpu_count(), include=elements
)
it.set_cache(cache)
processed = set()
for elem in self.yield_from_iterator(it):
processed.add(ifc.by_id(elem.id))
self.serialiser.write(elem)
tree.add_element(elem)
elements -= processed
elements = drawing_elements.copy()
for annotation_context in annotation_contexts:
with profile("Processing annotation context"):
if annotation_context and elements:
geom_settings = ifcopenshell.geom.settings(
DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True, INCLUDE_CURVES=True
)
if ifc.by_id(annotation_context[0]).ContextType == "Plan" and "PLAN_VIEW" in target_view:
offset = ifcopenshell.ifcopenshell_wrapper.float_array_3()
# A 2mm Z offset to combat Z-fighting in plan or RCPs
offset[2] = 0.002 if target_view == "PLAN_VIEW" else -0.002
geom_settings.offset = offset
geom_settings.set_context_ids(annotation_context)
it = ifcopenshell.geom.iterator(
geom_settings, ifc, multiprocessing.cpu_count(), include=elements
)
it.set_cache(cache)
processed = set()
for elem in self.yield_from_iterator(it):
processed.add(ifc.by_id(elem.id))
self.serialiser.write(elem)
tree.add_element(elem)
elements -= processed
if self.camera_element not in drawing_elements:
with profile("Camera element"):
# The camera must always be included, regardless of any include/exclude filters.
geom_settings = ifcopenshell.geom.settings(DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True)
it = ifcopenshell.geom.iterator(geom_settings, ifc, include=[self.camera_element])
for elem in it:
for elem in self.yield_from_iterator(it):
self.serialiser.write(elem)
with profile("Finalizing"):
@@ -611,7 +588,8 @@ class CreateDrawing(bpy.types.Operator):
if global_id not in elements_with_faces:
continue
for path in projection.findall("./{http://www.w3.org/2000/svg}path"):
start, end = [[float(o) for o in co[1:].split(",")] for co in path.attrib["d"].split()]
# Rounding is necessary to ensure coincident points are coincident
start, end = [[round(float(o), 1) for o in co[1:].split(",")] for co in path.attrib["d"].split()]
if start == end:
continue
# Extension by 0.5mm is necessary to ensure lines overlap with other diagonal lines
@@ -873,12 +851,13 @@ class CreateDrawing(bpy.types.Operator):
return svg_path
def setup_serialiser(self, target_view):
def setup_serialiser(self, ifc, target_view):
self.svg_settings = ifcopenshell.geom.settings(
DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True, INCLUDE_CURVES=True
)
self.svg_buffer = ifcopenshell.geom.serializers.buffer()
self.serialiser = ifcopenshell.geom.serializers.svg(self.svg_buffer, self.svg_settings)
self.serialiser.setFile(ifc)
self.serialiser.setWithoutStoreys(True)
self.serialiser.setPolygonal(True)
self.serialiser.setUseHlrPoly(True)
@@ -905,6 +884,13 @@ class CreateDrawing(bpy.types.Operator):
# the style of the face when running tree.select_ray()
# tree.enable_face_styles(True)
def yield_from_iterator(self, it):
if it.initialize():
while True:
yield it.get()
if not it.next():
break
def get_svg_classes(self, element):
classes = [element.is_a()]
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
@@ -942,18 +928,6 @@ class CreateDrawing(bpy.types.Operator):
self.is_manifold_cache[obj.data.name] = True
return True
def get_element_by_guid(self, guid):
try:
return tool.Ifc.get().by_guid(guid)
except:
for link in bpy.context.scene.BIMProjectProperties.links:
if link.name not in IfcStore.session_files:
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
try:
return IfcStore.session_files[link.name].by_guid(guid)
except:
continue
def merge_linework_and_add_metadata(self, root):
join_criteria = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "JoinCriteria")
if join_criteria:
@@ -968,7 +942,7 @@ class CreateDrawing(bpy.types.Operator):
ifc = tool.Ifc.get()
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
element = self.get_element_by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
element = ifc.by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
if "projection" in el.get("class", "").split():
classes = self.get_svg_classes(element)
@@ -981,10 +955,6 @@ class CreateDrawing(bpy.types.Operator):
el.set("class", " ".join(classes))
obj = tool.Ifc.get_object(element)
if not obj: # This is a linked model object. For now, do nothing.
continue
if not self.is_manifold(obj):
continue
@@ -1000,7 +970,7 @@ class CreateDrawing(bpy.types.Operator):
for path in el.findall("{http://www.w3.org/2000/svg}path"):
for subpath in path.attrib["d"].split("M")[1:]:
subpath = "M" + subpath.strip()
coords = [[float(o) for o in co[1:].split(",")] for co in subpath.split()]
coords = [[round(float(o), 1) for o in co[1:].split(",")] for co in subpath.split()]
if coords[0] != coords[-1]:
has_open_paths = True
line_strings.append(shapely.LineString(coords))
@@ -1064,7 +1034,7 @@ class CreateDrawing(bpy.types.Operator):
for path in el.findall("{http://www.w3.org/2000/svg}path"):
for subpath in path.attrib["d"].split("M")[1:]:
subpath_co = "M" + subpath.strip(" Z")
coords = [[float(o) for o in co[1:].split(",")] for co in subpath_co.split()]
coords = [[round(float(o), 1) for o in co[1:].split(",")] for co in subpath_co.split()]
if subpath.strip().lower().endswith("z"):
coords.append(coords[0])
if len(coords) > 2 and coords[0] == coords[-1]:
@@ -1162,14 +1132,14 @@ class CreateDrawing(bpy.types.Operator):
continue
return space
def get_material_name(self, element: ifcopenshell.entity_instance) -> str:
def get_material_name(self, element):
if hasattr(element, "Name") and element.Name:
return element.Name
elif hasattr(element, "LayerSetName") and element.LayerSetName:
return element.LayerSetName
return "mat-" + str(element.id())
def get_svg_path(self, cache_type: Optional[str] = None) -> str:
def get_svg_path(self, cache_type=None):
drawing_path = tool.Drawing.get_document_uri(self.camera_document)
drawings_dir = os.path.dirname(drawing_path)
@@ -1266,14 +1236,12 @@ class AddDrawingToSheet(bpy.types.Operator, Operator):
return
reference = tool.Ifc.run("document.add_reference", information=sheet)
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE")]) + 1
),
Location=drawing_reference.Location,
Description="DRAWING",
)
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
attributes = {
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "SCHEDULE")]) + 1),
"Location": drawing_reference.Location,
"Description": "DRAWING",
}
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
@@ -1341,10 +1309,9 @@ class CreateSheets(bpy.types.Operator, Operator):
has_sheet_reference = False
for reference in tool.Drawing.get_document_references(sheet):
reference_description = tool.Drawing.get_reference_description(reference)
if reference_description == "SHEET":
if reference.Description == "SHEET":
has_sheet_reference = True
elif reference_description == "RASTER":
elif reference.Description == "RASTER":
if reference.Location in raster_references:
raster_references.remove(reference.Location)
else:
@@ -1355,9 +1322,7 @@ class CreateSheets(bpy.types.Operator, Operator):
tool.Ifc.run(
"document.edit_reference",
reference=reference,
attributes=tool.Drawing.generate_reference_attributes(
reference, Location=tool.Ifc.get_relative_uri(svg), Description="SHEET"
),
attributes={"Location": tool.Ifc.get_relative_uri(svg), "Description": "SHEET"},
)
for raster_reference in raster_references:
@@ -1365,9 +1330,7 @@ class CreateSheets(bpy.types.Operator, Operator):
tool.Ifc.run(
"document.edit_reference",
reference=reference,
attributes=tool.Drawing.generate_reference_attributes(
reference, Location=tool.Ifc.get_relative_uri(raster_reference), Description="RASTER"
),
attributes={"Location": tool.Ifc.get_relative_uri(raster_reference), "Description": "RASTER"},
)
svg2pdf_command = context.preferences.addons["blenderbim"].preferences.svg2pdf_command
@@ -1399,7 +1362,7 @@ class SelectAllDrawings(bpy.types.Operator):
bl_label = "Select All Drawings"
view: bpy.props.StringProperty()
bl_description = "Select all drawings in the drawing list.\n\n" + "SHIFT+CLICK to deselect all drawings"
select_all: bpy.props.BoolProperty(name="Open All", default=True, options={"SKIP_SAVE"})
select_all: bpy.props.BoolProperty(name="Open All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# deselect all drawings on shift+click
@@ -1476,19 +1439,13 @@ class ActivateModel(bpy.types.Operator):
CutDecorator.uninstall()
# save current visibility statuses for Views and Types collections
visibility_status: dict[bpy.types.Object, bool] = {}
for col in bpy.data.collections["Views"].children:
for obj in col.objects:
visibility_status[obj] = obj.hide_get()
for obj in bpy.data.collections["Types"].objects:
visibility_status[obj] = obj.hide_get()
if not bpy.app.background:
with context.temp_override(**tool.Blender.get_viewport_context()):
bpy.ops.object.hide_view_clear()
bpy.ops.bim.activate_status_filters()
subcontext = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
for obj in context.visible_objects:
element = tool.Ifc.get_entity(obj)
if not element:
@@ -1506,61 +1463,52 @@ class ActivateModel(bpy.types.Operator):
is_global=True,
should_sync_changes_first=True,
)
# restore visibility after hide_view_clear()
for obj, hide_status in visibility_status.items():
obj.hide_set(hide_status)
tool.Blender.update_viewport()
return {"FINISHED"}
class ActivateDrawing(bpy.types.Operator):
"""
Activates the selected drawing view
"""
bl_idname = "bim.activate_drawing"
bl_label = "Activate Drawing"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Activates the selected drawing view.\n\n" + "ALT+CLICK to keep the viewport position.\n\n" + "SHIFT+CLICK activiate drawing without turning objects on/off."
bl_description = "Activates the selected drawing view.\n\n" + "ALT+CLICK to keep the viewport position"
drawing: bpy.props.IntProperty()
camera_view_point: bpy.props.BoolProperty(name="Camera View Point", default=True, options={"SKIP_SAVE"})
switch_camera_only: bpy.props.BoolProperty(name="Only Changes Camera View", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# keep the viewport position on alt+click
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
if event.type == "LEFTMOUSE" and event.alt:
self.camera_view_point = False
# Only activates the camera view on alt+shift. Does not turn on/off objects in scene
if event.type == "LEFTMOUSE" and event.shift:
self.switch_camera_only = True
return self.execute(context)
def execute(self, context):
drawing = tool.Ifc.get().by_id(self.drawing)
dprops = bpy.context.scene.DocProperties
camera = tool.Drawing.import_drawing(drawing)
if self.switch_camera_only:
tool.Blender.activate_camera(camera)
else:
if not self.camera_view_point:
viewport_position = tool.Blender.get_viewport_position()
if not self.camera_view_point:
viewport_position = tool.Blender.get_viewport_position()
core.activate_drawing_view(tool.Ifc, tool.Drawing, drawing=drawing)
if not self.camera_view_point:
tool.Blender.set_viewport_position(viewport_position)
if not self.camera_view_point:
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()
bpy.ops.bim.activate_drawing_style()
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
CutDecorator.install(context)
tool.Drawing.show_decorations()
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()
bpy.ops.bim.activate_drawing_style()
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
CutDecorator.install(context)
tool.Drawing.show_decorations()
return {"FINISHED"}
@@ -1640,7 +1588,7 @@ class ReloadDrawingStyles(bpy.types.Operator):
if not DrawingsData.is_loaded:
DrawingsData.load()
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
camera_props = context.scene.camera.data.BIMCameraProperties
camera_props = context.active_object.data.BIMCameraProperties
# added this part as a temporary fallback
# TODO: should remove it a bit later when projects get more accommodated
@@ -1669,14 +1617,8 @@ class ReloadDrawingStyles(bpy.types.Operator):
json_path = Path(tool.Ifc.resolve_uri(rel_path))
if not json_path.exists():
ootb_resource = Path(context.scene.BIMProperties.data_dir) / "assets" / "shading_styles.json"
print(
f"WARNING. Couldn't find shading_styles for the drawing by the path: {json_path}. "
f"Default BBIM resource will be copied from {ootb_resource}"
)
if ootb_resource.exists():
os.makedirs(json_path.parent, exist_ok=True)
shutil.copy(ootb_resource, json_path)
self.report({"ERROR"}, "Shading styles file not found: {}".format(json_path))
return {"CANCELLED"}
with open(json_path, "r") as fi:
shading_styles_json = json.load(fi)
@@ -2039,14 +1981,12 @@ class AddScheduleToSheet(bpy.types.Operator, Operator):
return
reference = tool.Ifc.run("document.add_reference", information=sheet)
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE")]) + 1
),
Location=schedule_location,
Description="SCHEDULE",
)
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
attributes = {
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "SCHEDULE")]) + 1),
"Location": schedule_location,
"Description": "SCHEDULE",
}
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
@@ -2095,15 +2035,12 @@ class AddReferenceToSheet(bpy.types.Operator, Operator):
return
reference = tool.Ifc.run("document.add_reference", information=sheet)
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "REFERENCE")])
+ 1
),
Location=extref_location,
Description="REFERENCE",
)
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
attributes = {
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "REFERENCE")]) + 1),
"Location": extref_location,
"Description": "REFERENCE",
}
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
@@ -2362,16 +2299,8 @@ class AssignSelectedObjectAsProduct(bpy.types.Operator):
def execute(self, context):
objs = context.selected_objects[:]
obj1 = objs[0]
element1 = tool.Ifc.get_entity(obj1)
obj2 = objs[1]
element2 = tool.Ifc.get_entity(obj2)
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
objs.remove(context.active_object)
other_selected_object = next(obj for obj in context.selected_objects if obj != context.active_object)
context.active_object.BIMAssignedProductProperties.relating_product = other_selected_object
bpy.ops.bim.edit_assigned_product()
return {"FINISHED"}
@@ -2429,7 +2358,6 @@ class LoadSheets(bpy.types.Operator, Operator):
if not filepath.is_file():
sheet_name = f"{sheet_prop.identification} - {sheet_prop.name}"
sheets_not_found.append(f'"{sheet_name}" - {document_uri}')
core.regenerate_sheet(tool.Drawing, sheet)
if sheets_not_found:
self.report({"ERROR"}, "Some sheets svg files are missing:\n" + "\n".join(sheets_not_found))
@@ -2448,8 +2376,8 @@ class EditSheet(bpy.types.Operator, Operator):
if sheet.is_a("IfcDocumentInformation"):
self.document_type = "SHEET"
self.name = sheet.Name
self.identification = sheet.DocumentId if tool.Ifc.get_schema() == "IFC2X3" else sheet.Identification
elif sheet.is_a("IfcDocumentReference") and tool.Drawing.get_reference_description(sheet) == "TITLEBLOCK":
self.identification = sheet.Identification
elif sheet.is_a("IfcDocumentReference") and sheet.Description == "TITLEBLOCK":
self.document_type = "TITLEBLOCK"
else:
self.document_type = "EMBEDDED"
@@ -2475,7 +2403,7 @@ class EditSheet(bpy.types.Operator, Operator):
if self.document_type == "SHEET":
core.rename_sheet(tool.Ifc, tool.Drawing, sheet=sheet, identification=self.identification, name=self.name)
elif self.document_type == "EMBEDDED":
core.rename_reference(tool.Ifc, tool.Drawing, reference=sheet, identification=self.identification)
core.rename_reference(tool.Ifc, reference=sheet, identification=self.identification)
elif self.document_type == "TITLEBLOCK":
titleblock = self.props.titleblock
reference = sheet
@@ -2627,16 +2555,8 @@ class EnableEditingElementFilter(bpy.types.Operator, Operator):
def _execute(self, context):
obj = bpy.context.scene.camera
if not obj:
return
obj.data.BIMCameraProperties.filter_mode = self.filter_mode
element = tool.Ifc.get_entity(obj)
if query := ifcopenshell.util.element.get_pset(element, "EPset_Drawing", self.filter_mode.title()):
filter_groups = tool.Search.get_filter_groups(f"drawing_{self.filter_mode.lower()}")
try:
tool.Search.import_filter_query(query, filter_groups)
except:
pass
if obj:
obj.data.BIMCameraProperties.filter_mode = self.filter_mode
class EditElementFilter(bpy.types.Operator, Operator):
@@ -2778,50 +2698,3 @@ class AddReferenceImage(bpy.types.Operator, Operator):
)
tool.Style.reload_material_from_ifc(material)
tool.Geometry.record_object_materials(obj)
class ConvertSVGToDXF(bpy.types.Operator):
bl_idname = "bim.convert_svg_to_dxf"
bl_label = "Convert SVG to DXF"
bl_options = {"REGISTER", "UNDO"}
view: bpy.props.StringProperty()
bl_description = "Convert current drawing's .svg to .dxf.\n\nSHIFT+CLICK to convert all selected drawings"
convert_all: bpy.props.BoolProperty(name="Convert All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# convert all drawings on shift+click
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
if event.type == "LEFTMOUSE" and event.shift:
self.open_all = True
return self.execute(context)
def execute(self, context):
if self.convert_all:
drawings = [
tool.Ifc.get().by_id(d.ifc_definition_id) for d in context.scene.DocProperties.drawings if d.is_selected
]
else:
drawings = [tool.Ifc.get().by_id(context.scene.DocProperties.drawings.get(self.view).ifc_definition_id)]
drawing_uris: list[Path] = []
drawings_not_found: list[str] = []
for drawing in drawings:
drawing_uri = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_document(drawing))
if drawing_uri is None or not os.path.exists(drawing_uri):
drawings_not_found.append(drawing.Name)
else:
drawing_uris.append(Path(drawing_uri))
if drawings_not_found:
msg = "Some drawings .svg files were not found, need to print them first: \n{}.".format(
"\n".join(drawings_not_found)
)
self.report({"ERROR"}, msg)
return {"CANCELLED"}
for drawing_uri in drawing_uris:
tool.Drawing.convert_svg_to_dxf(drawing_uri, drawing_uri.with_suffix(".dxf"))
self.report({"INFO"}, f"{len(drawing_uris)} drawings were converted to .dxf.")
return {"FINISHED"}
@@ -66,11 +66,6 @@ def get_location_hint(self, context):
def update_diagram_scale(self, context):
if not context.scene.camera or context.scene.camera.data != self.id_data:
return
element = tool.Ifc.get_entity(context.scene.camera)
if not element:
return
try:
element = (
tool.Ifc.get()
@@ -92,11 +87,6 @@ def update_diagram_scale(self, context):
def update_is_nts(self, context):
if not context.scene.camera or context.scene.camera.data != self.id_data:
return
element = tool.Ifc.get_entity(context.scene.camera)
if not element:
return
try:
element = (
tool.Ifc.get()
@@ -204,7 +194,7 @@ def update_document_name(self, context):
def update_has_underlay(self, context):
update_layer(self, context, "HasUnderlay", self.has_underlay)
# making sure that camera is active
if self.has_underlay and (context.scene.camera and context.scene.camera.data == self.id_data):
if self.has_underlay and (context.active_object and context.active_object.data == self.id_data):
bpy.ops.bim.reload_drawing_styles()
bpy.ops.bim.activate_drawing_style()
@@ -218,12 +208,10 @@ def update_has_annotation(self, context):
def update_layer(self, context, name, value):
if not context.scene.camera or context.scene.camera.data != self.id_data:
return
element = tool.Ifc.get_entity(context.scene.camera)
element = tool.Ifc.get_entity(context.active_object)
if not element:
return
pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing")
pset = ifcopenshell.util.element.get_psets(element).get("EPset_Drawing")
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
else:
@@ -241,8 +229,9 @@ def update_titleblocks(self, context):
SheetsData.data["titleblocks"] = SheetsData.titleblocks()
def update_should_draw_decorations(self, context):
if self.should_draw_decorations:
def toggleDecorations(self, context):
toggle = self.should_draw_decorations
if toggle:
# TODO: design a proper text variable templating renderer
collection = context.scene.camera.BIMObjectProperties.collection
for obj in collection.objects:
@@ -315,7 +304,9 @@ class RasterStyleProperty(enum.Enum):
SPACE_SHADING = "space.shading"
RASTER_STYLE_PROPERTIES_EXCLUDE = ("scene.render.filepath",)
RASTER_STYLE_PROPERTIES_EXCLUDE = (
"scene.render.filepath",
)
class DocProperties(PropertyGroup):
@@ -352,7 +343,7 @@ class DocProperties(PropertyGroup):
active_sheet_index: IntProperty(name="Active Sheet Index")
ifc_files: CollectionProperty(name="IFCs", type=StrProperty)
drawing_styles: CollectionProperty(name="Drawing Styles", type=DrawingStyle)
should_draw_decorations: BoolProperty(name="Should Draw Decorations", update=update_should_draw_decorations)
should_draw_decorations: BoolProperty(name="Should Draw Decorations", update=toggleDecorations)
sheets_dir: StringProperty(default=os.path.join("sheets") + os.path.sep, name="Default Sheets Directory")
layouts_dir: StringProperty(default=os.path.join("layouts") + os.path.sep, name="Default Layouts Directory")
titleblocks_dir: StringProperty(
@@ -369,8 +360,6 @@ class DocProperties(PropertyGroup):
default=os.path.join("drawings", "assets", "shading_styles.json"), name="Default Shading Styles"
)
shadingstyle_default: StringProperty(default="Blender Default", name="Default Shading Style")
drawing_font: StringProperty(default="OpenGost Type B TT.ttf", name="Drawing Font")
magic_font_scale: bpy.props.FloatProperty(default=0.004118616, name="Font Scale Factor")
class BIMCameraProperties(PropertyGroup):
@@ -16,21 +16,22 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
import re
import bpy
import string
from blenderbim.bim.module.drawing.svgwriter import SvgWriter
import svgwrite
import openpyxl
from blenderbim.bim.module.drawing.svgwriter import SvgWriter
from odf.opendocument import load as load_ods
from odf.table import Table, TableRow, TableColumn, TableCell
from odf.text import P
from odf.style import Style
from textwrap import wrap
from pathlib import Path
import string
FONT_SIZE = 4.13
FONT_WIDTH = lambda size: size * 0.45
FONT_SIZE_PT = 12
FONT_FAMILY = "OpenGost Type B TT"
DEBUG = False
@@ -62,29 +63,11 @@ class Scheduler:
)
self.padding = 1
self.margin = 1
self.parse_css(infile)
if infile.endswith("ods"):
self.schedule_ods(infile, outfile)
elif infile.endswith("xlsx"):
self.schedule_xlsx(infile, outfile)
def parse_css(self, infile):
stylesheet_path = os.path.splitext(infile)[0] + ".css"
if not os.path.exists(stylesheet_path):
stylesheet_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", "schedule.css")
with open(stylesheet_path, "r") as stylesheet:
css = stylesheet.read()
matches = re.search("--font-size-pt:\s*([0-9.]+);", css)
self.font_size_pt = float(matches.groups()[0]) if matches else 12 # Default to 12pt
matches = re.search("--font-size-px:\s*([0-9.]+);", css)
self.font_size_px = float(matches.groups()[0]) if matches else 4.13 # Magic number 4.13px ~= 12pt
matches = re.search("--font-width:\s*([0-9.]+);", css)
self.font_width = float(matches.groups()[0]) if matches else 0.45 # A magic number for OpenGost
self.svg.defs.add(self.svg.style(css))
def schedule_xlsx(self, infile, outfile):
workbook = openpyxl.open(infile, data_only=True)
sheet = workbook.active
@@ -161,8 +144,8 @@ class Scheduler:
x += unmerged_width
continue
font_size = cell.font.size or self.font_size_pt # 12pt default
font_size = font_size / self.font_size_pt * self.font_size_px # Magic?
font_size = cell.font.size or 11 # 11pt default
font_size = font_size / FONT_SIZE_PT * FONT_SIZE # Magic?
text_position = [0.0, 0.0]
if cell.alignment.horizontal == "left":
@@ -441,9 +424,9 @@ class Scheduler:
italic_text = final_cell_style.get("font-style", None) == "italic"
# NOTE: very naive since we're scaling text proportionally
font_size = (
float(final_cell_style.get("font-size", f"{self.font_size_pt}pt")[:-2])
/ self.font_size_pt
* self.font_size_px
float(final_cell_style.get("font-size", f"{FONT_SIZE_PT}pt")[:-2])
/ FONT_SIZE_PT
* FONT_SIZE
)
if p_tags:
@@ -540,7 +523,10 @@ class Scheduler:
"""
text_lines = [str(p) for p in p_tags]
box_alignment_params = SvgWriter.get_box_alignment_parameters(box_alignment)
text_params = {"font-size": font_size}
text_params = {
"font-size": font_size,
"font-family": FONT_FAMILY,
}
if bold:
text_params["font-weight"] = "bold"
if italic:
@@ -560,13 +546,13 @@ class Scheduler:
text_tag = self.svg.text("", **(text_params | {"font-size": "0"} | box_alignment_params))
# TODO: Should be done without using magic number for self.font_width
# TODO: should be done in less naive way
# without using magic number for FONT_WIDTH
# currently it might not work for all fonts and font sizes
if wrap_text:
wrapped_lines = []
for line in text_lines:
wrapped_line = wrap(
line, width=int(cell_width // (font_size * self.font_width)), break_long_words=False
)
wrapped_line = wrap(line, width=int(cell_width // FONT_WIDTH(font_size)), break_long_words=False)
wrapped_lines.extend(wrapped_line)
else:
wrapped_lines = text_lines
@@ -41,7 +41,7 @@ class SheetBuilder:
self.data_dir = None
self.scale = "NTS"
def create(self, layout_path: str, titleblock_name: str) -> None:
def create(self, layout_path, titleblock_name):
root = ET.Element("svg")
root.attrib["xmlns"] = "http://www.w3.org/2000/svg"
root.attrib["xmlns:xlink"] = "http://www.w3.org/1999/xlink"
@@ -76,12 +76,7 @@ class SheetBuilder:
with open(layout_path, "w") as f:
f.write(minidom.parseString(ET.tostring(root)).toprettyxml(indent=" "))
def add_drawing(
self,
reference: ifcopenshell.entity_instance,
drawing: ifcopenshell.entity_instance,
sheet: ifcopenshell.entity_instance,
) -> None:
def add_drawing(self, reference, drawing, sheet):
filename = drawing.Name
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
layout_dir = os.path.dirname(layout_path)
@@ -136,7 +131,7 @@ class SheetBuilder:
)
layout_tree.write(layout_path)
def update_sheet_drawing_sizes(self, sheet: ifcopenshell.entity_instance) -> None:
def update_sheet_drawing_sizes(self, sheet):
ET.register_namespace("", "http://www.w3.org/2000/svg")
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
@@ -176,7 +171,7 @@ class SheetBuilder:
layout_tree.write(layout_path)
def remove_drawing(self, reference: ifcopenshell.entity_instance, sheet: ifcopenshell.entity_instance) -> None:
def remove_drawing(self, reference, sheet):
ET.register_namespace("", "http://www.w3.org/2000/svg")
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
@@ -192,12 +187,7 @@ class SheetBuilder:
layout_tree.write(layout_path)
def add_document(
self,
reference: ifcopenshell.entity_instance,
document: ifcopenshell.entity_instance,
sheet: ifcopenshell.entity_instance,
) -> None:
def add_document(self, reference, document, sheet):
view_path = tool.Drawing.get_path_with_ext(tool.Drawing.get_document_uri(document), "svg")
if not os.path.exists(view_path):
tool.Drawing.create_svg_document(document)
@@ -234,7 +224,7 @@ class SheetBuilder:
)
layout_tree.write(layout_path)
def add_view_title(self, x: float, y: float, parent: ET.Element, layout_dir: str) -> None:
def add_view_title(self, x, y, parent, layout_dir):
title_path = os.path.join(layout_dir, "assets", "view-title.svg")
os.makedirs(os.path.dirname(title_path), exist_ok=True)
if not os.path.exists(title_path):
@@ -251,7 +241,7 @@ class SheetBuilder:
title.attrib["width"] = str(self.convert_to_mm(title_root.attrib.get("width")))
title.attrib["height"] = str(self.convert_to_mm(title_root.attrib.get("height")))
def build(self, sheet: ifcopenshell.entity_instance) -> dict:
def build(self, sheet):
self.references = {"SHEET": None, "RASTER": []}
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
@@ -282,7 +272,7 @@ class SheetBuilder:
return self.references
def build_titleblock(self, root: ET.Element, sheet: ifcopenshell.entity_instance) -> None:
def build_titleblock(self, root, sheet):
titleblock = root.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
image = titleblock.findall("{http://www.w3.org/2000/svg}image")[0]
g = self.parse_embedded_svg(image, sheet.get_info())
@@ -295,7 +285,7 @@ class SheetBuilder:
titleblock.append(g)
titleblock.remove(image)
def ensure_drawing_unique_styles(self, svg: ET.Element, drawing_id: int) -> ET.Element:
def ensure_drawing_unique_styles(self, svg, drawing_id):
"""ensures all drawing's classes and ids will be unique for the whole sheet
by adding `drawing_id` based prefix
"""
@@ -323,7 +313,7 @@ class SheetBuilder:
brackets_level -= 1
text += l
def replace_urls(text: str) -> str:
def replace_urls(text):
"""replace urls `url(#marker)` with `url(#prefix-marker)`
since `url(#marker.prefix)` doesn't seem to work
"""
@@ -353,7 +343,7 @@ class SheetBuilder:
return svg
def build_drawings(self, root: ET.Element, sheet: ifcopenshell.entity_instance):
def build_drawings(self, root, sheet):
for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="drawing"]'):
drawing_id = int(view.attrib["data-id"])
try:
@@ -400,7 +390,7 @@ class SheetBuilder:
for image in images:
view.remove(image)
def build_documents(self, root: ET.Element, sheet: ifcopenshell.entity_instance) -> None:
def build_documents(self, root, sheet):
schedules = root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]')
references = root.findall('{http://www.w3.org/2000/svg}g[@data-type="reference"]')
documents = schedules + references
@@ -437,10 +427,10 @@ class SheetBuilder:
for image in images:
view.remove(image)
def get_href(self, element: ET.Element) -> str:
return urllib.parse.unquote(element.attrib.get("{http://www.w3.org/1999/xlink}href")).replace("\\", "/")
def get_href(self, element):
return urllib.parse.unquote(element.attrib.get("{http://www.w3.org/1999/xlink}href")).replace('\\','/')
def parse_embedded_svg(self, image: ET.Element, data: dict) -> ET.Element:
def parse_embedded_svg(self, image, data):
group = ET.Element("g")
group.attrib["transform"] = "translate({},{})".format(
self.convert_to_mm(image.attrib.get("x")), self.convert_to_mm(image.attrib.get("y"))
@@ -476,7 +466,7 @@ class SheetBuilder:
group.append(child)
return group
def change_titleblock(self, sheet: ifcopenshell.entity_instance, titleblock_name: str) -> None:
def change_titleblock(self, sheet, titleblock_name):
ootb_titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
titleblock_path = tool.Drawing.get_default_titleblock_path(titleblock_name)
sheet_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
@@ -509,7 +499,7 @@ class SheetBuilder:
sheet_tree.write(sheet_path)
def convert_to_mm(self, value: str) -> float:
def convert_to_mm(self, value):
# CSS is what defines these possibilities
# https://www.w3.org/TR/SVG/refs.html#ref-css-values-3
# https://www.w3.org/TR/css-values-3/#absolute-lengths
@@ -530,5 +520,5 @@ class SheetBuilder:
return float(value[0:-2]) * (1 / 96) * 2.54 * 10
return float(value)
def mm_to_px(self, value: float) -> float:
def mm_to_px(self, value):
return (value / 25.4) * 96
@@ -22,17 +22,16 @@ import bpy
import math
import bmesh
import shutil
import pystache
import mathutils
import xml.etree.ElementTree as ET
import svgwrite
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.selector
import blenderbim.tool as tool
import blenderbim.bim.module.drawing.helper as helper
import blenderbim.bim.module.drawing.annotation as annotation
from blenderbim.bim.module.drawing.data import DrawingsData
from blenderbim.bim.module.drawing.data import DecoratorData
from blenderbim.bim.ifc import IfcStore
@@ -40,7 +39,6 @@ from blenderbim.bim.ifc import IfcStore
from math import pi, ceil, atan, degrees, acos
from mathutils import geometry, Vector
from bpy_extras import view3d_utils
from typing import Optional
class External(svgwrite.container.Group):
@@ -92,7 +90,7 @@ class SvgWriter:
def setup_drawing_resource_paths(self, element):
pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing")
for resource in ("Stylesheet", "Markers", "Symbols", "Patterns", "ShadingStyles"):
for resource in ("Stylesheet", "Markers", "Symbols", "Patterns"):
resource_path = pset.get(resource)
if not resource_path:
self.resource_paths[resource] = None
@@ -324,7 +322,7 @@ class SvgWriter:
)
)
def draw_misc_annotation(self, obj: bpy.types.Object) -> None:
def draw_misc_annotation(self, obj):
# We have to decide whether this should come from Blender or from IFC.
# For the moment, for convenience of experimenting with ideas, it comes
# from Blender. In the future, it should probably come from IFC.
@@ -358,14 +356,14 @@ class SvgWriter:
d = "M{}".format(d[1:])
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
def get_attribute_classes(self, obj: bpy.types.Object) -> list[str]:
def get_attribute_classes(self, obj):
element = tool.Ifc.get_entity(obj)
global_id = "GlobalId-{}".format(element.GlobalId)
predefined_type = "PredefinedType-" + tool.Drawing.canonicalise_class_name(
str(ifcopenshell.util.element.get_predefined_type(element))
)
classes = [global_id, element.is_a(), predefined_type]
custom_classes: str = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
custom_classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
if custom_classes:
classes.extend(custom_classes.split())
for key in self.metadata:
@@ -718,15 +716,14 @@ class SvgWriter:
self.svg.text(sheet_id, insert=(text_position[0], text_position[1] + 2.5), class_="ELEVATION", **text_style)
)
def get_reference_and_sheet_id_from_annotation(self, element: ifcopenshell.entity_instance) -> tuple[str, str]:
def get_reference_and_sheet_id_from_annotation(self, element):
reference_id = "-"
sheet_id = "-"
drawing = tool.Drawing.get_annotation_element(element)
reference = tool.Drawing.get_drawing_reference(drawing)
if reference:
for sheet_reference in tool.Ifc.get().by_type("IfcDocumentReference"):
reference_description = tool.Drawing.get_reference_description(sheet_reference)
if reference_description != "DRAWING" or sheet_reference.Location != reference.Location:
if sheet_reference.Description != "DRAWING" or sheet_reference.Location != reference.Location:
continue
sheet = tool.Drawing.get_reference_document(sheet_reference)
if sheet:
@@ -790,7 +787,7 @@ class SvgWriter:
attrib["filter"] = "url(#fill-background)"
return element
def draw_text_annotation(self, text_obj: bpy.types.Object, position: Vector) -> None:
def draw_text_annotation(self, text_obj, position):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
element = tool.Ifc.get_entity(text_obj)
@@ -802,7 +799,17 @@ class SvgWriter:
text_position_svg = text_position * self.svg_scale
text_position_svg_str = ", ".join(map(str, text_position_svg))
angle = self.get_empty_object_angle(text_obj)
def get_basis_vector(matrix, i=0):
"""returns basis vector for i in world space, unaffected by object scale"""
return matrix.inverted()[i].to_3d().normalized()
text_dir_world_x_axis = get_basis_vector(text_obj.matrix_world)
# RCP cameras may be scaled, so reset scales.
camera_matrix = tool.Drawing.get_camera_matrix(self.camera)
text_dir = (camera_matrix.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized()
angle = math.degrees(-text_dir.angle_signed(Vector((1, 0))))
classes = self.get_attribute_classes(text_obj)
classes_str = " ".join(classes)
fill_bg = "fill-bg" in classes
@@ -810,7 +817,8 @@ class SvgWriter:
symbol = tool.Drawing.get_annotation_symbol(element)
template_text_fields = []
if symbol:
symbol_transform = self.get_symbol_transform(text_position_svg_str, angle, text_obj)
# NOTE: for now we assume that scale is uniform
symbol_transform = f"translate({text_position_svg_str}) rotate({angle}) scale({text_obj.scale.x})"
symbol_svg = self.find_xml_symbol_by_id(symbol)
if symbol_svg:
@@ -839,7 +847,7 @@ class SvgWriter:
return None
if not symbol_svg or not template_text_fields:
self.draw_symbol(symbol, symbol_transform)
self.svg.add(self.svg.use(f"#{symbol}", transform=symbol_transform))
line_number = 0
for text_literal in text_literals:
@@ -857,14 +865,17 @@ class SvgWriter:
self.svg.add(tag)
line_number += len(tag.elements)
def draw_empty_annotation(self, obj: bpy.types.Object, classes: list[str]) -> None:
def draw_empty_annotation(self, obj, classes):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
point = self.project_point_onto_camera(obj.matrix_world.translation)
element = tool.Ifc.get_entity(obj)
svg_id = tool.Drawing.get_annotation_symbol(element)
svg_id = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Symbol")
if not svg_id:
# EPset_AnnotationSurveyArea is not standard! See bSI-4.3 proposal #660.
svg_id = ifcopenshell.util.element.get_pset(element, "EPset_AnnotationSurveyArea", "PointType")
if not svg_id:
svg_id = str(ifcopenshell.util.element.get_predefined_type(element))
if not svg_id:
@@ -872,37 +883,9 @@ class SvgWriter:
point = Vector(((x_offset + point.x), (y_offset - point.y)))
symbol_position_svg = point * self.svg_scale
svg_position_str = ", ".join(map(str, symbol_position_svg))
self.svg.add(self.svg.use(f"#{svg_id}", insert=symbol_position_svg))
angle = self.get_empty_object_angle(obj)
symbol_transform = self.get_symbol_transform(svg_position_str, angle, obj)
self.draw_symbol(svg_id, symbol_transform)
def get_empty_object_angle(self, obj: bpy.types.Object) -> float:
def get_basis_vector(matrix: bpy.types.Object, i: int = 0) -> Vector:
"""returns basis vector for i in world space, unaffected by object scale"""
return matrix.inverted()[i].to_3d().normalized()
text_dir_world_x_axis = get_basis_vector(obj.matrix_world)
# RCP cameras may be scaled, so reset scales.
camera_matrix = tool.Drawing.get_camera_matrix(self.camera)
text_dir = (camera_matrix.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized()
return math.degrees(-text_dir.angle_signed(Vector((1, 0))))
def get_symbol_transform(
self, svg_position_str: str, angle: float = 0.0, obj: Optional[bpy.types.Object] = None
) -> str:
"""`obj` will be used to identify symbol scale,
ignore it if object scale doesn't match the symbol scale"""
# NOTE: for now we assume that scale is uniform
scale = 1.0 if not obj else obj.scale.x
return f"translate({svg_position_str}) rotate({angle}) scale({scale})"
def draw_symbol(self, symbol_id: str, symbol_transform: str) -> None:
self.svg.add(self.svg.use(f"#{symbol_id}", transform=symbol_transform))
def draw_point_annotation(self, obj: bpy.types.Object, classes: list[str]) -> None:
def draw_point_annotation(self, obj, classes):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
@@ -910,7 +893,10 @@ class SvgWriter:
projected_points = [self.project_point_onto_camera(matrix_world @ v.co) for v in obj.data.vertices]
element = tool.Ifc.get_entity(obj)
svg_id = tool.Drawing.get_annotation_symbol(element)
svg_id = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Symbol")
if not svg_id:
# EPset_AnnotationSurveyArea is not standard! See bSI-4.3 proposal #660.
svg_id = ifcopenshell.util.element.get_pset(element, "EPset_AnnotationSurveyArea", "PointType")
if not svg_id:
svg_id = str(ifcopenshell.util.element.get_predefined_type(element))
if not svg_id:
@@ -919,10 +905,7 @@ class SvgWriter:
for symbol_position in projected_points:
symbol_position = Vector(((x_offset + symbol_position.x), (y_offset - symbol_position.y)))
symbol_position_svg = symbol_position * self.svg_scale
svg_position_str = ", ".join(map(str, symbol_position_svg))
symbol_transform = self.get_symbol_transform(svg_position_str)
self.draw_symbol(svg_id, symbol_transform)
self.svg.add(self.svg.use(f"#{svg_id}", insert=symbol_position_svg))
def draw_break_annotations(self, obj):
x_offset = self.raw_width / 2
@@ -1083,9 +1066,7 @@ class SvgWriter:
# calculating text parameters and adding text
text_position = arc_mid_point + arc_mid_dir * 5
text_style = SvgWriter.get_box_alignment_parameters("center")
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
decimal_places = drawing_pset_data.get("AngleDecimalPlaces", None)
angle_text = f"{round(angle, decimal_places)}°"
angle_text = f"{int(angle)}deg"
self.svg.add(self.svg.text(angle_text, insert=text_position, class_="ANGLE", **text_style))
# Draw SVG arc, see for details: http://xahlee.info/js/svg_circle_arc.html
@@ -1421,7 +1402,7 @@ class SvgWriter:
return text_tags
def project_point_onto_camera(self, point: Vector) -> Vector:
def project_point_onto_camera(self, point):
# TODO is this needlessly complex?
return self.camera.matrix_world.inverted() @ geometry.intersect_line_plane(
point.xyz,
@@ -268,11 +268,6 @@ class BIM_PT_drawings(Panel):
col = row.column()
col.alignment = "RIGHT"
convert_to_dxf = row.row(align=True)
op = convert_to_dxf.operator("bim.convert_svg_to_dxf", text="", icon="IMAGE_DATA")
op.view = active_drawing.name
convert_to_dxf.enabled = active_drawing.ifc_definition_id > 0
op = row.operator("bim.select_all_drawings", icon="SELECT_SUBTRACT", text="")
open_drawing_button = row.row(align=True)
@@ -250,7 +250,7 @@ class AnnotationToolUI:
row = cls.layout.row(align=True)
row.label(text="", icon="FILE_3D")
prop_with_search(row, cls.props, "relating_type_id", text="")
row.operator("bim.launch_annotation_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
row.operator("bim.launch_annotation_type_manager", icon="LIGHTPROBE_GRID", text="")
add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation")
@@ -22,20 +22,13 @@ from . import ui, prop, operator
classes = (
operator.AddRepresentation,
operator.CopyRepresentation,
operator.DisableEditingRepresentationItemShapeAspect,
operator.DisableEditingRepresentationItemStyle,
operator.DisableEditingRepresentationItems,
operator.DuplicateLinkedAggregateTo3dCursor,
operator.DuplicateMoveLinkedAggregate,
operator.DuplicateMoveLinkedAggregateMacro,
operator.EditObjectPlacement,
operator.EditRepresentationItemShapeAspect,
operator.EditRepresentationItemStyle,
operator.EnableEditingRepresentationItemShapeAspect,
operator.EnableEditingRepresentationItemStyle,
operator.EnableEditingRepresentationItems,
operator.FlipObject,
operator.GetRepresentationIfcParameters,
operator.DuplicateMoveLinkedAggregate,
operator.DuplicateMoveLinkedAggregateMacro,
operator.OverrideDelete,
operator.OverrideDuplicateMove,
operator.OverrideDuplicateMoveLinked,
@@ -52,15 +45,11 @@ classes = (
operator.RefreshLinkedAggregate,
operator.RemoveConnection,
operator.RemoveRepresentation,
operator.RemoveRepresentationItem,
operator.RemoveRepresentationItemFromShapeAspect,
operator.SelectConnection,
operator.SwitchRepresentation,
operator.UnassignRepresentationItemStyle,
operator.UpdateParametricRepresentation,
operator.UpdateRepresentation,
prop.RepresentationItem,
prop.ShapeAspect,
prop.BIMObjectGeometryProperties,
prop.BIMGeometryProperties,
ui.BIM_PT_placement,
@@ -104,9 +93,7 @@ def register():
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_object_duplicate_move_linked_macro", "D", "PRESS", alt=True)
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new(
"bim.object_duplicate_move_linked_aggregate_macro", "D", "PRESS", ctrl=True, shift=True
)
kmi = km.keymap_items.new("bim.object_duplicate_move_linked_aggregate_macro", "D", "PRESS", ctrl=True, shift=True)
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
addon_keymaps.append((km, kmi))
@@ -120,7 +107,6 @@ def register():
km = wm.keyconfigs.addon.keymaps.new(name="Mesh", space_type="EMPTY")
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
kmi.properties.should_save = True
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("wm.call_menu", "P", "PRESS")
kmi.properties.name = ui.BIM_MT_hotkey_separate.bl_idname
@@ -17,7 +17,6 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifcopenshell.util.element
import blenderbim.tool as tool
import ifcopenshell.util.placement
from mathutils import Vector
@@ -39,7 +38,6 @@ class RepresentationsData:
def load(cls):
cls.data = {"representations": cls.representations()}
cls.data["contexts"] = cls.contexts()
cls.data["shape_aspects"] = cls.shape_aspects()
cls.is_loaded = True
@classmethod
@@ -94,26 +92,6 @@ class RepresentationsData:
)
return results
@classmethod
def shape_aspects(cls):
obj = bpy.context.active_object
if not obj.data:
return []
element = tool.Ifc.get_entity(obj)
active_representation_id = obj.data.BIMMeshProperties.ifc_definition_id
base_representation = tool.Ifc.get().by_id(active_representation_id)
# shape aspects matching context of the active representation
matching_shape_aspects = []
for shape_aspect in ifcopenshell.util.element.get_shape_aspects(element):
matching_representation = tool.Geometry.get_shape_aspect_representation(shape_aspect, base_representation)
if matching_representation:
matching_shape_aspects.append(shape_aspect)
# blender enum items
new_shape_aspect = [("NEW", "Create A New Shape Aspect", "")]
return new_shape_aspect + [(str(s.id()), s.Name or "Unnamed", "") for s in matching_shape_aspects]
class RepresentationItemsData:
data = {}
@@ -223,7 +201,7 @@ class ConnectionsData:
@classmethod
def is_connection_realization(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
connections = getattr(element, "IsConnectionRealization", None)
connections = element.IsConnectionRealization
if not connections:
return
@@ -21,14 +21,12 @@ import bmesh
import mathutils
import ifcopenshell
import ifcopenshell.util.unit
import blenderbim.tool as tool
from math import pi, pow
from mathutils import Vector, Matrix, geometry
from typing import Union
class Helper:
def __init__(self, file: ifcopenshell.file):
def __init__(self, file):
self.file = file
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
@@ -36,7 +34,7 @@ class Helper:
# edge that shares a single vertex only with that face to find the extrusion
# edge. A face with the normal facing down is prioritised. A limited
# dissolve ensure that faces are quads and not tris.
def auto_detect_rectangle_profile_extruded_area_solid(self, mesh: bpy.types.Mesh) -> dict:
def auto_detect_rectangle_profile_extruded_area_solid(self, mesh):
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
@@ -117,9 +115,7 @@ class Helper:
return {"profile": profile, "extrusion": extrusion}
def auto_detect_arbitrary_profile_with_voids(
self, obj: bpy.types.Object, mesh: bpy.types.Mesh
) -> Union[tuple, dict]:
def auto_detect_arbitrary_profile_with_voids(self, obj, mesh):
groups = {"IFCARCINDEX": [], "IFCCIRCLE": []}
for i, group in enumerate(obj.vertex_groups):
if "IFCARCINDEX" in group.name:
@@ -142,31 +138,28 @@ class Helper:
total_groups = 0
is_circle = False
for group_type, group_indices in groups.items():
if not group_indices:
continue
is_special, group_index = tool.Blender.bmesh_check_vertex_in_groups(vert, deform_layer, group_indices)
if not is_special:
continue
if group_type == "IFCCIRCLE":
is_circle = True
group_verts[group_type].setdefault(group_index, 0)
group_verts[group_type][group_index] += 1
total_groups += 0
for group_index in group_indices:
if group_index in vert[deform_layer]:
if group_type == "IFCCIRCLE":
is_circle = True
group_verts[group_type].setdefault(group_index, 0)
group_verts[group_type][group_index] += 1
total_groups += 0
if total_groups > 1: # A vert can only belong to one group
return (False, "AMBIGUOUS_SPECIAL_VERTEX")
elif is_circle:
pass # Circles are allowed to be unclosed
pass # Circles are allowed to be unclosed
elif total_groups == 0 and len(vert.link_edges) != 2: # Unclosed loop or forked loop
return (False, "UNCLOSED_LOOP")
for group_type, group_counts in group_verts.items():
if group_type == "IFCARCINDEX":
for group_count in group_counts.values():
if group_count != 3: # Each arc needs 3 verts
if group_count != 3: # Each arc needs 3 verts
return (False, "3POINT_ARC")
elif group_type == "IFCCIRCLE":
for group_count in group_counts.values():
if group_count != 2: # Each circle needs 2 verts
if group_count != 2: # Each circle needs 2 verts
return (False, "CIRCLE")
loop_edges = set(bm.edges)
@@ -276,7 +269,7 @@ class Helper:
inner_loops.remove(outer_loop)
# Copy vectors to prevent random data mangling after bmesh is freed.
points = [v.co.copy() for v in bm.verts]
points = [Vector(list(v.co)) for v in bm.verts]
bm.to_mesh(mesh)
mesh.update()
@@ -538,7 +531,7 @@ class Helper:
)
position = None
if self.file.schema == "IFC2X3":
position = self.file.createIfcAxis2Placement2D(self.file.createIfcCartesianPoint([0.0, 0.0]))
position = self.file.createIfcAxis2Placement2D(self.file.createIfcCartesianPoint([0.0, 0.0, 0.0]))
curve = self.file.createIfcRectangleProfileDef("AREA", None, position, xdim, ydim)
return {"curve_ucs": curve_ucs, "curve": curve}
@@ -21,14 +21,11 @@ import bmesh
import logging
import numpy as np
import json
import re
import ifcopenshell
import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.placement
import ifcopenshell.api
import blenderbim.core.geometry
import blenderbim.core.geometry as core
import blenderbim.core.aggregate
import blenderbim.core.style
@@ -39,8 +36,6 @@ import blenderbim.bim.handler
from mathutils import Vector, Matrix
from time import time
from blenderbim.bim.ifc import IfcStore
from ifcopenshell.util.shape_builder import ShapeBuilder
from typing import Any
class Operator:
@@ -138,23 +133,19 @@ class AddRepresentation(bpy.types.Operator, Operator):
"for Profile - 2D bounding box by local XZ axes.\n"
"For other contexts - bounding box is 3d.",
),
("OBJECT", "From Object", "Copies geometry from another object"),
("PROJECT", "Full Representation", "Reuses the current representation"),
("PROJECT", "Full Representation", ""),
],
name="Representation Conversion Method",
)
def _execute(self, context):
obj = context.active_object
props = context.scene.BIMGeometryProperties
oprops = obj.BIMGeometryProperties
ifc_context = int(oprops.contexts or "0") or None
props = obj.BIMGeometryProperties
ifc_context = int(props.contexts or "0") or None
if not ifc_context:
return
ifc_context = tool.Ifc.get().by_id(ifc_context)
original_data = obj.data
if self.representation_conversion_method == "OUTLINE":
if ifc_context.ContextType == "Plan":
data = tool.Geometry.generate_outline_mesh(obj, axis="+Z")
@@ -171,31 +162,17 @@ class AddRepresentation(bpy.types.Operator, Operator):
else:
data = tool.Geometry.generate_3d_box_mesh(obj)
tool.Geometry.change_object_data(obj, data, is_global=True)
elif (
self.representation_conversion_method == "OBJECT"
and props.representation_from_object
and props.representation_from_object.data
):
data = tool.Geometry.duplicate_object_data(props.representation_from_object)
tool.Geometry.change_object_data(obj, data, is_global=True)
try:
core.add_representation(
tool.Ifc,
tool.Geometry,
tool.Style,
tool.Surveyor,
obj=obj,
context=ifc_context,
ifc_representation_class=None,
profile_set_usage=None,
)
except core.IncompatibleRepresentationError:
if obj.data != original_data:
tool.Geometry.change_object_data(obj, original_data, is_global=True)
bpy.data.meshes.remove(data)
self.report({"ERROR"}, "No compatible representation for the context could be created.")
return {"CANCELLED"}
core.add_representation(
tool.Ifc,
tool.Geometry,
tool.Style,
tool.Surveyor,
obj=obj,
context=ifc_context,
ifc_representation_class=None,
profile_set_usage=None,
)
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -203,9 +180,6 @@ class AddRepresentation(bpy.types.Operator, Operator):
def draw(self, context):
row = self.layout.row()
row.prop(self, "representation_conversion_method", text="")
if self.representation_conversion_method == "OBJECT":
row = self.layout.row()
row.prop(context.scene.BIMGeometryProperties, "representation_from_object", text="")
class SelectConnection(bpy.types.Operator, Operator):
@@ -238,13 +212,6 @@ class SwitchRepresentation(bpy.types.Operator, Operator):
disable_opening_subtractions: bpy.props.BoolProperty()
should_switch_all_meshes: bpy.props.BoolProperty()
@classmethod
def poll(cls, context):
if context.active_object.mode == "OBJECT":
return True
cls.poll_message_set("Only available in OBJECT mode - Press TAB in the viewport")
return False
def _execute(self, context):
target_representation = tool.Ifc.get().by_id(self.ifc_definition_id)
target = target_representation.ContextOfItems
@@ -253,8 +220,6 @@ class SwitchRepresentation(bpy.types.Operator, Operator):
element = tool.Ifc.get_entity(obj)
if not element:
continue
if not obj.mode == "OBJECT":
continue
if obj == context.active_object:
representation = target_representation
else:
@@ -356,8 +321,8 @@ class UpdateRepresentation(bpy.types.Operator, 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.run("material.unassign_material", tool.Ifc.get(), products=[product])
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), product=element_type)
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), product=product)
else:
# These objects are parametrically based on an axis and should not be modified as a mesh
return
@@ -402,11 +367,7 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
representation_data["profile_set_usage"] = tool.Geometry.get_profile_set_usage(product)
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)
if new_representation is None:
self.report({"ERROR"}, "Error creating representation for Blender object.")
return {"CANCELLED"}
if tool.Geometry.is_body_representation(new_representation):
[
@@ -574,8 +535,6 @@ class OverrideDelete(bpy.types.Operator):
row.prop(self, "is_batch", text="Enable Faster Deletion")
def _execute(self, context):
start_time = time()
if self.is_batch:
ifcopenshell.util.element.batch_remove_deep2(tool.Ifc.get())
@@ -600,11 +559,6 @@ class OverrideDelete(bpy.types.Operator):
IfcStore.add_transaction_operation(self)
# Required otherwise gizmos are still visible
context.view_layer.objects.active = None
operator_time = time() - start_time
if operator_time > 10:
self.report({"INFO"}, "IFC Delete was finished in {:.2f} seconds".format(operator_time))
return {"FINISHED"}
def rollback(self, data):
@@ -715,10 +669,6 @@ class OverrideOutlinerDelete(bpy.types.Operator):
else:
bpy.data.objects.remove(obj)
for collection in collections_to_delete:
# Removing an aggregate object would also remove it's collection
# making the collection data-block invalid.
if not tool.Blender.is_valid_data_block(collection):
continue
bpy.data.collections.remove(collection)
if self.is_batch:
old_file = tool.Ifc.get()
@@ -730,7 +680,7 @@ class OverrideOutlinerDelete(bpy.types.Operator):
IfcStore.add_transaction_operation(self)
return {"FINISHED"}
def get_collection_objects_and_children(self, collection: bpy.types.Collection) -> dict[str, Any]:
def get_collection_objects_and_children(self, collection):
objects = set()
children = set()
queue = [collection]
@@ -817,13 +767,11 @@ class OverrideDuplicateMove(bpy.types.Operator):
element = tool.Ifc.get_entity(obj)
if element:
if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
self.report(
{"INFO"}, "Did not duplicate. Duplicate drawings through the Drawings and Documents UI."
)
self.report({'INFO'}, "Did not duplicate. Duplicate drawings through the Drawings and Documents UI.")
obj.select_set(False)
continue # For now, don't copy drawings until we stabilise a bit more. It's tricky.
elif element.is_a("IfcProject"):
self.report({"INFO"}, "Did not duplicate. IFC can only have one project.")
self.report({'INFO'}, "Did not duplicate. IFC can only have one project.")
obj.select_set(False)
continue # Only one IfcProject is allowed.
@@ -890,7 +838,6 @@ class OverrideDuplicateMove(bpy.types.Operator):
# Recreate decompositions
tool.Root.recreate_decompositions(decomposition_relationships, old_to_new)
OverrideDuplicateMove.remove_linked_aggregate_data(old_to_new)
blenderbim.bim.handler.refresh_ui_data()
return old_to_new
@@ -943,22 +890,6 @@ class OverrideDuplicateMove(bpy.types.Operator):
if entity in old_to_new.keys():
core.remove_connection(tool.Geometry, connection=connection)
def remove_linked_aggregate_data(old_to_new):
for old, new in old_to_new.items():
pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate")
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new[0],pset=pset)
if new[0].is_a("IfcElementAssembly"):
linked_aggregate_group = [
r.RelatingGroup
for r in getattr(new[0], "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
tool.Ifc.run("group.unassign_group", group=linked_aggregate_group[0], products=[new[0]])
class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro):
bl_idname = "bim.override_object_duplicate_move_linked_macro"
@@ -983,15 +914,14 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
class DuplicateMoveLinkedAggregateMacro(bpy.types.Macro):
bl_description = "Create and move a new linked aggregate"
bl_idname = "bim.object_duplicate_move_linked_aggregate_macro"
bl_label = "IFC Duplicate and Move Linked Aggregate"
bl_label = "IFC Duplicate Linked Aggregate"
bl_options = {"REGISTER", "UNDO"}
class DuplicateMoveLinkedAggregate(bpy.types.Operator):
bl_idname = "bim.object_duplicate_move_linked_aggregate"
bl_label = "IFC Duplicate and Move Linked Aggregate"
bl_label = "IFC Duplicate Linked Aggregate"
bl_options = {"REGISTER", "UNDO"}
is_interactive: bpy.props.BoolProperty(name="Is Interactive", default=True)
@@ -1006,7 +936,7 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
return DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(self, context)
@staticmethod
def execute_ifc_duplicate_linked_aggregate_operator(self, context, location_from_3d_cursor=False):
def execute_ifc_duplicate_linked_aggregate_operator(self, context):
self.new_active_obj = None
self.group_name = "BBIM_Linked_Aggregate"
self.pset_name = "BBIM_Linked_Aggregate"
@@ -1018,38 +948,17 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
obj.select_set(True)
parts = ifcopenshell.util.element.get_parts(element)
if parts:
index = get_max_index(parts)
add_linked_aggregate_pset(element, index)
index += 1
index = 0
for part in parts:
if part.is_a("IfcElementAssembly"):
select_objects_and_add_data(part)
else:
index = add_linked_aggregate_pset(part, index)
# index += 1
add_linked_aggregate_pset(part, index)
index += 1
obj = tool.Ifc.get_object(part)
obj.select_set(True)
def add_linked_aggregate_pset(part, index):
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
if not pset:
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name)
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Index": index},
)
index += 1
else:
pass
return index
def add_linked_aggregate_group(element):
linked_aggregate_group = None
product_groups_name = [
@@ -1060,74 +969,25 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
if self.group_name in product_groups_name:
return
linked_aggregate_group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), name=self.group_name)
linked_aggregate_group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.group_name)
ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), products=[element], group=linked_aggregate_group)
def custom_incremental_naming_for_element_assembly(old_to_new):
for new in old_to_new.values():
if new[0].is_a("IfcElementAssembly"):
group_elements = [
r.RelatedObjects
for r in getattr(new[0], "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
][0]
def add_linked_aggregate_pset(part, index):
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
number = len(group_elements) - 1
number = f"{number:02d}"
new_obj = tool.Ifc.get_object(new[0])
pattern1 = r"_\d"
if re.findall(pattern1, new_obj.name):
split_name = new_obj.name.split("_")
new_obj.name = split_name[0] + "_" + number
continue
pattern2 = r"\.\d{3}"
if re.findall(pattern2, new_obj.name):
split_name = new_obj.name.split(".")
new_obj.name = split_name[0] + "_" + number
def get_max_index(parts):
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
index = [i["Index"] for i in psets if i]
if len(index) > 0:
index = max(index)
return index
if not pset:
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name)
else:
return 0
pset = tool.Ifc.get().by_id(pset["id"])
def copy_linked_aggregate_data(old_to_new):
for old, new in old_to_new.items():
pset = ifcopenshell.util.element.get_pset(old, "BBIM_Linked_Aggregate")
if pset:
new_pset = ifcopenshell.api.run(
"pset.add_pset", tool.Ifc.get(), product=new[0], name=self.pset_name
)
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=new_pset,
properties={"Index": pset["Index"]},
)
if new[0].is_a("IfcElementAssembly"):
linked_aggregate_group = [
r.RelatingGroup
for r in getattr(old, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
tool.Ifc.run("group.assign_group", group=linked_aggregate_group[0], products=new)
def get_location_from_3d_cursor(old_to_new, aggregate):
base_obj = tool.Ifc.get_object(aggregate)
base_obj_location = base_obj.location.copy()
for new in old_to_new.values():
new_obj = tool.Ifc.get_object(new[0])
location_diff = new_obj.location - base_obj_location
new_obj.location = context.scene.cursor.location + location_diff
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Index": index},
)
return index
if len(context.selected_objects) != 1:
return {"FINISHED"}
@@ -1149,35 +1009,16 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
tool.Root.recreate_aggregate(old_to_new)
copy_linked_aggregate_data(old_to_new)
custom_incremental_naming_for_element_assembly(old_to_new)
if location_from_3d_cursor:
get_location_from_3d_cursor(old_to_new, selected_element)
# Recreate aggregate relationship
for old in old_to_new.keys():
if old.is_a("IfcElementAssembly"):
tool.Root.recreate_aggregate(old_to_new)
blenderbim.bim.handler.refresh_ui_data()
return old_to_new
class DuplicateLinkedAggregateTo3dCursor(bpy.types.Operator):
bl_idname = "bim.duplicate_linked_aggregate_to_3d_cursor"
bl_label = "IFC Duplicate Linked Aggregate to 3d Cursor"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return len(context.selected_objects) > 0
def execute(self, context):
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False)
def _execute(self, context):
return DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(self, context, location_from_3d_cursor=True)
class RefreshLinkedAggregate(bpy.types.Operator):
bl_idname = "bim.refresh_linked_aggregate"
bl_label = "IFC Refresh Linked Aggregate"
@@ -1201,7 +1042,6 @@ class RefreshLinkedAggregate(bpy.types.Operator):
self.pset_name = "BBIM_Linked_Aggregate"
refresh_start_time = time()
old_to_new = {}
original_names = {}
def delete_objects(element):
parts = ifcopenshell.util.element.get_parts(element)
@@ -1214,61 +1054,6 @@ class RefreshLinkedAggregate(bpy.types.Operator):
tool.Geometry.delete_ifc_object(tool.Ifc.get_object(element))
def get_original_names(element):
group = [
r.RelatingGroup
for r in getattr(element, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if self.group_name in r.RelatingGroup.Name
][0].id()
original_names[group] = {}
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
index = pset["Index"]
original_names[group][index] = tool.Ifc.get_object(element).name
parts = ifcopenshell.util.element.get_parts(element)
if parts:
for part in parts:
if part.is_a("IfcElementAssembly"):
original_names | get_original_names(part)
else:
try:
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
except:
continue
index = pset["Index"]
original_names[group][index] = tool.Ifc.get_object(part).name
return original_names
def set_original_name(obj, original_names):
element = tool.Ifc.get_entity(obj)
aggregate = ifcopenshell.util.element.get_aggregate(element)
if ifcopenshell.util.element.get_parts(
element
): # if element has parts it means it is the base of and aggregate or sub-aggregate
aggregate = element
group = [
r.RelatingGroup
for r in getattr(aggregate, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if self.group_name in r.RelatingGroup.Name
]
if not group:
return
group = group[0].id()
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
index = pset["Index"]
try:
obj.name = original_names[group][index]
except:
return
def get_element_assembly(element):
if element.is_a("IfcElementAssembly"):
return element
@@ -1310,22 +1095,6 @@ class RefreshLinkedAggregate(bpy.types.Operator):
return list(set(linked_aggregate_groups)), selected_parents
def get_original_matrix(element, base_instance):
selected_obj = tool.Ifc.get_object(base_instance)
selected_matrix = selected_obj.matrix_world
object_duplicate = tool.Ifc.get_object(element)
duplicate_matrix = object_duplicate.matrix_world.decompose()
return selected_matrix, duplicate_matrix
def set_new_matrix(selected_matrix, duplicate_matrix, old_to_new):
for old, new in old_to_new.items():
new_obj = tool.Ifc.get_object(new[0])
new_base_matrix = Matrix.LocRotScale(*duplicate_matrix)
matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world
new_obj_matrix = new_base_matrix @ matrix_diff
new_obj.matrix_world = new_obj_matrix
active_element = tool.Ifc.get_entity(context.active_object)
if not active_element:
self.report({"INFO"}, "Object has no Ifc metadata.")
@@ -1365,9 +1134,10 @@ class RefreshLinkedAggregate(bpy.types.Operator):
element_aggregate = ifcopenshell.util.element.get_aggregate(element)
selected_matrix, duplicate_matrix = get_original_matrix(element, base_instance)
original_names = get_original_names(element)
selected_obj = tool.Ifc.get_object(base_instance)
selected_matrix = selected_obj.matrix_world
object_duplicate = tool.Ifc.get_object(element)
duplicate_matrix = object_duplicate.matrix_world.decompose()
delete_objects(element)
@@ -1377,10 +1147,13 @@ class RefreshLinkedAggregate(bpy.types.Operator):
tool.Ifc.get_object(base_instance).select_set(True)
old_to_new = DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(self, context)
set_new_matrix(selected_matrix, duplicate_matrix, old_to_new)
for old, new in old_to_new.items():
new_obj = tool.Ifc.get_object(new[0])
new_base_matrix = Matrix.LocRotScale(*duplicate_matrix)
matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world
new_obj_matrix = new_base_matrix @ matrix_diff
new_obj.matrix_world = new_obj_matrix
if element_aggregate and new[0].is_a("IfcElementAssembly"):
new_aggregate = ifcopenshell.util.element.get_aggregate(new[0])
@@ -1393,10 +1166,6 @@ class RefreshLinkedAggregate(bpy.types.Operator):
related_obj=tool.Ifc.get_object(new[0]),
)
for old, new in old_to_new.items():
new_obj = tool.Ifc.get_object(new[0])
set_original_name(new_obj, original_names)
blenderbim.bim.handler.refresh_ui_data()
operator_time = time() - refresh_start_time
@@ -1429,23 +1198,18 @@ class OverrideJoin(bpy.types.Operator, Operator):
return self.join_blender_obj()
def join_ifc_obj(self):
ifc_file = tool.Ifc.get()
builder = ShapeBuilder(ifc_file)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
representation = ifc_file.by_id(self.target.data.BIMMeshProperties.ifc_definition_id)
representation = tool.Ifc.get().by_id(self.target.data.BIMMeshProperties.ifc_definition_id)
if representation.RepresentationType in ("Tessellation", "Brep"):
for obj in bpy.context.selected_objects:
if obj == self.target:
continue
element = tool.Ifc.get_entity(obj)
if element:
ifcopenshell.api.run("root.remove_product", ifc_file, product=element)
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
bpy.ops.object.join()
bpy.ops.bim.update_representation(obj=self.target.name, ifc_representation_class="")
elif representation.RepresentationType == "SweptSolid":
target_placement = np.array(self.target.matrix_world)
target_placement[:, 3][:3] /= si_conversion
items = list(representation.Items)
for obj in bpy.context.selected_objects:
if obj == self.target:
@@ -1458,28 +1222,31 @@ class OverrideJoin(bpy.types.Operator, Operator):
continue
# Only objects of the same representation type can be joined
obj_rep = ifc_file.by_id(obj.data.BIMMeshProperties.ifc_definition_id)
obj_rep = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
if obj_rep.RepresentationType != "SweptSolid":
obj.select_set(False)
continue
placement = np.array(obj.matrix_world)
placement[:, 3][:3] /= si_conversion
for item in obj_rep.Items:
copied_item = ifcopenshell.util.element.copy_deep(ifc_file, item)
copied_item = ifcopenshell.util.element.copy_deep(tool.Ifc.get(), item)
for style in item.StyledByItem:
copied_style = ifcopenshell.util.element.copy(ifc_file, style)
copied_style = ifcopenshell.util.element.copy(tool.Ifc.get(), style)
copied_style.Item = copied_item
if copied_item.Position:
position = ifcopenshell.util.placement.get_axis2placement(copied_item.Position)
else:
position = np.eye(4, dtype=float)
position = np.eye(4)
position = placement @ position
position = np.linalg.inv(target_placement) @ position
copied_item.Position = builder.create_axis2_placement_3d_from_matrix(position)
copied_item.Position = tool.Ifc.get().createIfcAxis2Placement3D(
tool.Ifc.get().createIfcCartesianPoint([float(n) for n in position[:, 3][:3]]),
tool.Ifc.get().createIfcDirection([float(n) for n in position[:, 2][:3]]),
tool.Ifc.get().createIfcDirection([float(n) for n in position[:, 0][:3]]),
)
items.append(copied_item)
ifcopenshell.api.run("root.remove_product", ifc_file, product=element)
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
representation.Items = items
bpy.ops.object.join()
core.switch_representation(
@@ -1510,13 +1277,14 @@ class OverridePasteBuffer(bpy.types.Operator):
def execute(self, context):
bpy.ops.view3d.pastebuffer()
for obj in context.selected_objects:
# Pasted objects may come from another Blender session, or even
# from the same session where the original object has since
# been deleted. As the source element may not exist, paste will
# always unlink the element. If you want to duplicate an
# element, use the duplicate commands.
tool.Root.unlink_object(obj)
if IfcStore.get_file():
for obj in context.selected_objects:
# Pasted objects may come from another Blender session, or even
# from the same session where the original object has since
# been deleted. As the source element may not exist, paste will
# always unlink the element. If you want to duplicate an
# element, use the duplicate commands.
tool.Root.unlink_object(obj)
return {"FINISHED"}
@@ -1543,9 +1311,7 @@ class OverrideModeSetEdit(bpy.types.Operator):
for obj in selected_objs:
if not obj:
continue
obj_supports_edit_mode, message = tool.Blender.object_supports_edit_mode(obj)
if not obj_supports_edit_mode:
self.report({"INFO"}, message)
if not obj.data:
obj.select_set(False)
continue
element = tool.Ifc.get_entity(obj)
@@ -1689,11 +1455,20 @@ class OverrideModeSetObject(bpy.types.Operator):
apply_openings=True,
)
def draw(self, context):
if self.is_valid:
row = self.layout.row()
row.prop(self, "should_save")
else:
row = self.layout.row()
row.label(text="No Geometry Found: Object will revert to previous state.")
def invoke(self, context, event):
return IfcStore.execute_ifc_operator(self, context, is_invoke=True)
def _invoke(self, context, event):
self.is_valid = True
self.should_save = True
bpy.ops.object.mode_set(mode="EDIT", toggle=True)
@@ -1755,6 +1530,8 @@ class OverrideModeSetObject(bpy.types.Operator):
else:
tool.Ifc.finish_edit(obj)
if self.edited_objs:
return context.window_manager.invoke_props_dialog(self)
return self.execute(context)
@@ -1813,16 +1590,6 @@ class EnableEditingRepresentationItems(bpy.types.Operator, Operator):
new.surface_style = style.Name or "Unnamed"
elif inverse.is_a("IfcPresentationLayerAssignment"):
new.layer = inverse.Name or "Unnamed"
elif inverse.is_a("IfcShapeRepresentation"):
if inverse.OfShapeAspect:
shape_aspect = inverse.OfShapeAspect[0]
new.shape_aspect = shape_aspect.Name
new.shape_aspect_id = shape_aspect.id()
# sort created items
sorted_items = sorted(props.items[:], key=lambda i: (not i.shape_aspect, i.shape_aspect))
for i, item in enumerate(sorted_items[:-1]): # last item is sorted automatically
props.items.move(props.items[:].index(item), i)
class DisableEditingRepresentationItems(bpy.types.Operator, Operator):
@@ -1833,270 +1600,3 @@ class DisableEditingRepresentationItems(bpy.types.Operator, Operator):
def _execute(self, context):
obj = context.active_object
obj.BIMGeometryProperties.is_editing = False
class RemoveRepresentationItem(bpy.types.Operator, Operator):
bl_idname = "bim.remove_representation_item"
bl_label = "Remove Representation Item"
bl_options = {"REGISTER", "UNDO"}
representation_item_id: bpy.props.IntProperty()
@classmethod
def poll(cls, context):
if context.active_object is None or len(context.active_object.BIMGeometryProperties.items) <= 1:
cls.poll_message_set(
"Active object need to have more than 1 representation items to keep representation valid"
)
return False
return True
def _execute(self, context):
obj = context.active_object
props = obj.BIMGeometryProperties
ifc_file = tool.Ifc.get()
representation_item = ifc_file.by_id(self.representation_item_id)
tool.Geometry.remove_representation_item(representation_item)
tool.Geometry.reload_representation(obj)
# reload items ui
bpy.ops.bim.disable_editing_representation_items()
bpy.ops.bim.enable_editing_representation_items()
def poll_editing_representaiton_item_style(cls, context):
if not (obj := getattr(context, "active_object", None)):
return False
props = obj.BIMGeometryProperties
if not props.is_editing:
return False
if not 0 <= props.active_item_index < len(props.items):
return False
item = props.items[props.active_item_index]
shape_aspect = item.shape_aspect
if shape_aspect == "":
return True
from blenderbim.bim.module.material.data import ObjectMaterialData
if not ObjectMaterialData.is_loaded:
ObjectMaterialData.load()
constituents = ObjectMaterialData.data["active_material_constituents"]
if any(c for c in constituents if c == shape_aspect):
cls.poll_message_set(
"Style comes from item's shape aspect related to the material constituent "
"with the same name and cannot be edited on representation item directly - "
"you can change it from the material constituent"
)
return False
return True
class EnableEditingRepresentationItemStyle(bpy.types.Operator, Operator):
bl_idname = "bim.enable_editing_representation_item_style"
bl_label = "Enable Editing Representation Item Style"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return poll_editing_representaiton_item_style(cls, context)
def _execute(self, context):
props = context.active_object.BIMGeometryProperties
props.is_editing_item_style = True
ifc_file = tool.Ifc.get()
# set dropdown to currently active style
representation_item_id = props.items[props.active_item_index].ifc_definition_id
representation_item = ifc_file.by_id(representation_item_id)
style = tool.Style.get_representation_item_style(representation_item)
if style:
props.representation_item_style = str(style.id())
class EditRepresentationItemStyle(bpy.types.Operator, Operator):
bl_idname = "bim.edit_representation_item_style"
bl_label = "Edit Representation Item Style"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
props = obj.BIMGeometryProperties
props.is_editing_item_style = False
ifc_file = tool.Ifc.get()
surface_style = ifc_file.by_id(int(props.representation_item_style))
representation_item_id = props.items[props.active_item_index].ifc_definition_id
representation_item = ifc_file.by_id(representation_item_id)
tool.Style.assign_style_to_representation_item(representation_item, surface_style)
tool.Geometry.reload_representation(obj)
# reload items ui
bpy.ops.bim.disable_editing_representation_items()
bpy.ops.bim.enable_editing_representation_items()
class DisableEditingRepresentationItemStyle(bpy.types.Operator, Operator):
bl_idname = "bim.disable_editing_representation_item_style"
bl_label = "Disable Editing Representation Item Style"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.active_object.BIMGeometryProperties
props.is_editing_item_style = False
class UnassignRepresentationItemStyle(bpy.types.Operator, Operator):
bl_idname = "bim.unassign_representation_item_style"
bl_label = "Unassign Representation Item Style"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return poll_editing_representaiton_item_style(cls, context)
def _execute(self, context):
obj = context.active_object
props = obj.BIMGeometryProperties
props.is_editing_item_style = False
# get active representation item
representation_item_id = props.items[props.active_item_index].ifc_definition_id
representation_item = tool.Ifc.get_entity_by_id(representation_item_id)
if not representation_item:
self.report({"ERROR"}, f"Couldn't find representation item by id {representation_item_id}.")
return {"CANCELLED"}
tool.Style.assign_style_to_representation_item(representation_item, None)
tool.Geometry.reload_representation(obj)
# reload items ui
bpy.ops.bim.disable_editing_representation_items()
bpy.ops.bim.enable_editing_representation_items()
class EnableEditingRepresentationItemShapeAspect(bpy.types.Operator, Operator):
bl_idname = "bim.enable_editing_representation_item_shape_aspect"
bl_label = "Enable Editing Representation Item Shape Aspect"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.active_object.BIMGeometryProperties
props.is_editing_item_shape_aspect = True
# set dropdown to currently active shape aspect
shape_aspect_id = props.items[props.active_item_index].shape_aspect_id
if shape_aspect_id != 0:
props.representation_item_shape_aspect = str(shape_aspect_id)
class EditRepresentationItemShapeAspect(bpy.types.Operator, Operator):
bl_idname = "bim.edit_representation_item_shape_aspect"
bl_label = "Edit Representation Item Shape Aspect"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMGeometryProperties
props.is_editing_item_shape_aspect = False
ifc_file = tool.Ifc.get()
representation_item_id = props.items[props.active_item_index].ifc_definition_id
representation_item = ifc_file.by_id(representation_item_id)
if props.representation_item_shape_aspect == "NEW":
active_representation = tool.Geometry.get_active_representation(obj)
# find IfcProductRepresentationSelect based on current representation
if hasattr(element, "Representation"): # IfcProduct
product_shape = element.Representation
else: # IfcTypeProduct
for representation_map in element.RepresentationMaps:
if representation_map.MappedRepresentation == active_representation:
product_shape = representation_map
previous_shape_aspect_id = props.items[props.active_item_index].shape_aspect_id
# will be None if item didn't had a shape aspect
previous_shape_aspect = tool.Ifc.get_entity_by_id(previous_shape_aspect_id)
shape_aspect = tool.Geometry.create_shape_aspect(
product_shape, active_representation, [representation_item], previous_shape_aspect
)
else:
shape_aspect = ifc_file.by_id(int(props.representation_item_shape_aspect))
tool.Geometry.add_representation_item_to_shape_aspect([representation_item], shape_aspect)
# set attributes from UI
shape_aspect_attrs = props.shape_aspect_attrs
shape_aspect.Name = shape_aspect_attrs.name
shape_aspect.Description = shape_aspect_attrs.description
shape_aspect_representation = tool.Geometry.get_shape_aspect_representation_for_item(
shape_aspect, representation_item
)
styles = tool.Geometry.get_shape_aspect_styles(element, shape_aspect, representation_item)
tool.Ifc.run(
"style.assign_representation_styles",
shape_representation=shape_aspect_representation,
styles=styles,
)
tool.Geometry.get_shape_aspect_styles(element, shape_aspect, representation_item)
tool.Geometry.reload_representation(obj)
# reload items ui
bpy.ops.bim.disable_editing_representation_items()
bpy.ops.bim.enable_editing_representation_items()
class DisableEditingRepresentationItemShapeAspect(bpy.types.Operator, Operator):
bl_idname = "bim.disable_editing_representation_item_shape_aspect"
bl_label = "Disable Editing Representation Item Shape Aspect"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.active_object.BIMGeometryProperties
props.is_editing_item_shape_aspect = False
class RemoveRepresentationItemFromShapeAspect(bpy.types.Operator, Operator):
bl_idname = "bim.remove_representation_item_from_shape_aspect"
bl_label = "Remove Representation Item From Shape Aspect"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMGeometryProperties
ifc_file = tool.Ifc.get()
representation_item_id = props.items[props.active_item_index].ifc_definition_id
representation_item = ifc_file.by_id(representation_item_id)
shape_aspect = ifc_file.by_id(props.items[props.active_item_index].shape_aspect_id)
# unassign items before removing items as removing items
# might remove shape aspect
if representation_item.StyledByItem:
styles = tool.Geometry.get_shape_aspect_styles(element, shape_aspect, representation_item)
self.remove_styles_from_item(representation_item, styles)
tool.Geometry.reload_representation(obj)
tool.Geometry.remove_representation_items_from_shape_aspect([representation_item], shape_aspect)
# reload items ui
bpy.ops.bim.disable_editing_representation_items()
bpy.ops.bim.enable_editing_representation_items()
def remove_styles_from_item(self, representation_item, styles):
ifc_file = tool.Ifc.get()
styled_item = representation_item.StyledByItem[0]
new_styles = [s for s in styled_item.Styles if s not in styles]
styled_item.Styles = new_styles
for style_ in new_styles:
if style_.is_a("IfcPresentationStyleAssignment"):
new_assignment_styles = [s for s in styled_item.Styles if s not in styles]
if not new_assignment_styles:
ifc_file.remove(style_)
else:
style_.Styles = new_assignment_styles
if not styled_item.Styles:
ifc_file.remove(styled_item)
@@ -17,7 +17,6 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import blenderbim.tool as tool
from blenderbim.bim.prop import StrProperty, Attribute
from blenderbim.bim.module.geometry.data import RepresentationsData
from bpy.types import PropertyGroup
@@ -48,63 +47,11 @@ def update_mode(self, context):
bpy.ops.bim.override_mode_set_edit("INVOKE_DEFAULT")
def get_styles(self, context):
# postponed import to avoid circular import
from blenderbim.bim.module.material.data import MaterialsData
if not MaterialsData.is_loaded:
MaterialsData.load()
return MaterialsData.data["styles"]
def get_shape_aspects(self, context):
if not RepresentationsData.is_loaded:
RepresentationsData.load()
return RepresentationsData.data["shape_aspects"]
def get_material_constituents(self, context, edit_text):
from blenderbim.bim.module.material.data import ObjectMaterialData
if not ObjectMaterialData.is_loaded:
ObjectMaterialData.load()
return ObjectMaterialData.data["active_material_constituents"]
def update_shape_aspect(self, context):
shape_aspect_id = self.representation_item_shape_aspect
attrs = self.shape_aspect_attrs
if shape_aspect_id == "NEW": # new shape aspect
attrs.name = tool.Blender.get_blender_prop_default_value(attrs, "name")
attrs.description = tool.Blender.get_blender_prop_default_value(attrs, "description")
else:
shape_aspect = tool.Ifc.get().by_id(int(shape_aspect_id))
attrs.name = shape_aspect.Name or ""
attrs.description = shape_aspect.Description or ""
class RepresentationItem(PropertyGroup):
name: StringProperty(name="Name")
surface_style: StringProperty(name="Surface Style")
layer: StringProperty(name="Layer")
ifc_definition_id: IntProperty(name="IFC Definition ID")
shape_aspect: StringProperty(name="Shape Aspect")
shape_aspect_id: IntProperty(name="Shape Aspect IFC ID")
class ShapeAspect(PropertyGroup):
name: StringProperty(
name="Name",
description=(
"Note that IfcMaterialConstituent is applied based on shape aspects using the same name as material constituent.\n"
"In dropdown suggestions you can see names of existing material constituents."
),
**({} if bpy.app.version < (3, 3, 0) else {"search": get_material_constituents}),
)
description: StringProperty(
name="Description",
)
class BIMObjectGeometryProperties(PropertyGroup):
@@ -112,13 +59,6 @@ class BIMObjectGeometryProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing", default=False)
items: CollectionProperty(name="Representation Items", type=RepresentationItem)
active_item_index: IntProperty(name="Active Representation Item Index")
is_editing_item_style: BoolProperty(name="Is Editing Item's Style", default=False)
representation_item_style: EnumProperty(items=get_styles, name="Representation Item Style")
is_editing_item_shape_aspect: BoolProperty(name="Is Editing Item's Shape Aspect", default=False)
representation_item_shape_aspect: EnumProperty(
items=get_shape_aspects, name="Representation Item Shape Aspect", update=update_shape_aspect
)
shape_aspect_attrs: PointerProperty(type=ShapeAspect)
class BIMGeometryProperties(PropertyGroup):
@@ -138,4 +78,3 @@ class BIMGeometryProperties(PropertyGroup):
name="IFC Interaction Mode",
update=update_mode,
)
representation_from_object: PointerProperty(type=bpy.types.Object)
@@ -29,13 +29,10 @@ from blenderbim.bim.module.geometry.data import (
PlacementData,
DerivedCoordinatesData,
)
from blenderbim.bim.module.layer.data import LayersData
def mode_menu(self, context):
row = self.layout.row(align=True)
if context.scene.BIMGeometryProperties.mode == "EDIT":
row.operator("bim.override_mode_set_object", icon="CANCEL", text="Discard Changes").should_save = False
row.prop(context.scene.BIMGeometryProperties, "mode", text="", icon_value=blenderbim.bim.icons["IFC"].icon_id)
@@ -136,7 +133,6 @@ class BIM_PT_representations(Panel):
if not RepresentationsData.data["representations"]:
layout.label(text="No Representations Found")
return
for representation in RepresentationsData.data["representations"]:
row = self.layout.row(align=True)
@@ -154,18 +150,6 @@ class BIM_PT_representations(Panel):
op.ifc_definition_id = representation["id"]
op.disable_opening_subtractions = False
row.operator("bim.remove_representation", icon="X", text="").representation_id = representation["id"]
if representation["is_active"]:
active_representation = representation
layout.separator()
if not LayersData.is_loaded:
LayersData.load()
if LayersData.data["active_layers"]:
layout.label(text="Representation Presentation Layers:")
for layer_name in LayersData.data["active_layers"].values():
layout.label(text=layer_name, icon="STICKY_UVS_LOC")
else:
layout.label(text="Representation Has No Presentation Layers", icon="STICKY_UVS_LOC")
class BIM_PT_representation_items(Panel):
@@ -203,47 +187,20 @@ class BIM_PT_representation_items(Panel):
self.layout.template_list("BIM_UL_representation_items", "", props, "items", props, "active_item_index")
item_is_active = props.active_item_index < len(props.items)
if not item_is_active:
return
active_item = props.items[props.active_item_index]
surface_style = active_item.surface_style
shape_aspect = active_item.shape_aspect
row = self.layout.row(align=True)
if props.is_editing_item_style:
# NOTE: we currently support 1 item having just 1 style
# when IfcStyledItem can actually have multiple styles
prop_with_search(row, props, "representation_item_style", icon="SHADING_RENDERED", text="")
row.operator("bim.edit_representation_item_style", icon="CHECKMARK", text="")
row.operator("bim.disable_editing_representation_item_style", icon="CANCEL", text="")
if item_is_active:
surface_style = props.items[props.active_item_index].surface_style
presentation_layer = props.items[props.active_item_index].layer
else:
if surface_style:
row.label(text=surface_style, icon="SHADING_RENDERED")
else:
row.label(text="No Surface Style", icon="MESH_UVSPHERE")
row.operator("bim.enable_editing_representation_item_style", icon="GREASEPENCIL", text="")
if surface_style:
row.operator("bim.unassign_representation_item_style", icon="X", text="")
surface_style, presentation_layer = None, None
row = self.layout.row()
row.label(text=active_item.layer or "No Presentation Layer", icon="STICKY_UVS_LOC")
row = self.layout.row(align=True)
if props.is_editing_item_shape_aspect:
row.prop(props, "representation_item_shape_aspect", icon="SHAPEKEY_DATA", text="")
row.operator("bim.edit_representation_item_shape_aspect", icon="CHECKMARK", text="")
row.operator("bim.disable_editing_representation_item_shape_aspect", icon="CANCEL", text="")
shape_aspect_attrs = props.shape_aspect_attrs
self.layout.label(text="Shape Aspect Attributes:")
self.layout.prop(shape_aspect_attrs, "name")
self.layout.prop(shape_aspect_attrs, "description")
if surface_style:
row.label(text=surface_style, icon="SHADING_RENDERED")
else:
row.label(text=shape_aspect or "No Shape Aspect", icon="SHAPEKEY_DATA")
row.operator("bim.enable_editing_representation_item_shape_aspect", icon="GREASEPENCIL", text="")
if shape_aspect:
row.operator("bim.remove_representation_item_from_shape_aspect", icon="X", text="")
row.label(text="No Surface Style", icon="MESH_UVSPHERE")
row = self.layout.row()
row.label(text=presentation_layer or "No Presentation Layer", icon="STICKY_UVS_LOC")
class BIM_PT_connections(Panel):
@@ -509,11 +466,6 @@ class BIM_UL_representation_items(UIList):
if item:
icon = "MATERIAL" if item.surface_style else "MESH_UVSPHERE"
row = layout.row(align=True)
item_name = item.name
if item.shape_aspect:
item_name = f"{item.shape_aspect} {item_name}"
row.label(text=item_name, icon=icon)
row.label(text=item.name, icon=icon)
if item.layer:
row.label(text="", icon="STICKY_UVS_LOC")
op = row.operator("bim.remove_representation_item", icon="X", text="")
op.representation_item_id = item.ifc_definition_id
@@ -212,7 +212,7 @@ class UnassignGroup(bpy.types.Operator, tool.Ifc.Operator):
"group.unassign_group",
self.file,
**{
"products": [self.file.by_id(product.BIMObjectProperties.ifc_definition_id)],
"product": self.file.by_id(product.BIMObjectProperties.ifc_definition_id),
"group": self.file.by_id(self.group),
}
)
@@ -247,11 +247,9 @@ class IFCGIT_PT_revision_inspector(bpy.types.Panel):
bl_idname = "IFCGIT_PT_revision_inspector"
bl_label = "Git History"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_options = {"DEFAULT_CLOSED"}
bl_parent_id = "IFCGIT_PT_panel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "BlenderBIM"
def draw(self, context):

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