Compare commits

...

70 Commits

Author SHA1 Message Date
Bruno Postle 219b7d0c24 Put webui PID file in a writable location
Currently a file called running_pid.json is written in the same folder
as sioserver.py which is icky. It also doesn't work on a system where
bonsai is read-only. This fix puts this file in a platform-suitable
location:

  Linux: ~/.cache/bonsai/running_pid.json
  Windows: %LOCALAPPDATA%\bonsai\Cache\running_pid.json
  macOS: ~/Library/Caches/bonsai/running_pid.json
2025-10-08 23:56:15 +01:00
Thomas Krijnen 97dd37b345 Fix circular definition issue in cmake #7180 2025-10-08 15:25:38 +02:00
Thomas Krijnen e4877e8246 Ignore steal_instances() in python 2025-10-08 14:55:28 +02:00
Thomas Krijnen e216d345c1 Reimplement cecho.cmd to not depend on starting powershell.exe for speed boost 2025-10-08 14:18:52 +02:00
Thomas Krijnen 14c1b70768 Address some compiler warnings 2025-10-08 14:18:17 +02:00
Thomas Krijnen e15f222a19 Address segfault #7215 #7185 2025-10-08 14:17:47 +02:00
Ryan Schultz 4ab2664529 closes #7134 - Wall layer cuts exported out to a SVG during Freestyle linework mode. 2025-10-07 14:43:11 -05:00
Dion Moult bd0cfa5366 Fix bug where grid decorator only considered hide states, not hide viewport state or local isolation, etc 2025-10-07 14:44:57 +11:00
Dion Moult 2e4b0e83a2 Add feature to have work schedule columns for total inputs, outputs, and elements (inputs + outputs) 2025-10-07 13:34:07 +11:00
Dion Moult cf61111386 Fix bug where nested elements didn't show elements of the active task (only children) 2025-10-07 13:06:26 +11:00
Dion Moult 3aa5ddb070 Implement auto element selection for tasks in work schedule. Also add manual selection (instead of going into ICOM panel for speed) 2025-10-07 12:27:38 +11:00
Dion Moult 25dacc8fed Fix #7208. Merge duplicates patch recipe can now control merging by empty attributes. 2025-10-06 23:18:32 +11:00
Dion Moult 7cba63af9c Add IfcTester webapp workflow (#7209) 2025-10-06 23:15:16 +11:00
Dion Moult 3c1d6abc7e Update ci-ifctester-org.yml attempt 2 2025-10-06 17:51:50 +11:00
Dion Moult 8da4b9f23f Create ci-ifctester-org.yml
This definitely won't work the first time
2025-10-06 17:41:21 +11:00
Dion Moult 99fa77edee Fix #6873. Bug where geometry created through sync_references didn't have item editing metadata. 2025-10-06 17:23:29 +11:00
Andrej730 839c5ad932 black . 2025-10-06 11:02:33 +05:00
Dion Moult b68ce08811 Deprecate should_reload and is_global in switch representation as they don't do anything now.
Superseded by item editing mode, so always reload, and always global.
2025-10-06 16:57:54 +11:00
Dion Moult e662308665 Deprecate should_sync_changes_first for switching representation, since it is no longer relevant in item editing. 2025-10-06 16:19:39 +11:00
Andrea Ghensi a2ccff67ca fix(bcf): Create directory entries in the zip file's central directory (#7175)
This is done to comply with the Implementation Agreements
https://github.com/buildingSMART/BCF-XML/tree/release_3_0/Documentation#create-directory-folder-entries-in-the-zip-files-central-directory
2025-10-06 15:50:03 +11:00
Christoph Mellüh 8e045d772c Add "hide empty Specifications" to Bonsai IFCTester (#7190)
* add filter

* apply filter property

* allow name filter

* black .

* add hide-skipped to html

* rename prop

* only hide optional specifications
2025-10-06 15:49:35 +11:00
Christoph Mellüh 16fd614886 Add instructions to IFCTester in Blender (#7195)
* add instructions to blender ui

* add requirement instructions to html

* black .
2025-10-06 15:48:45 +11:00
Dion Moult f31df307c6 Fix #6674. Bug where newly created annotation types didn't have the necessary metadata for item editing. 2025-10-06 15:46:16 +11:00
Dion Moult 11873a7c51 Fix #7203. Help indicate that author should be an email. Add error styling. 2025-10-06 14:48:22 +11:00
Dion Moult ccccbe23f4 Fix #6709. Support same spatial element type even if not explicit in concept template. 2025-10-06 14:32:53 +11:00
Dion Moult 2173a562be Fix #7157. Don't fail if the bin dir isn't where we expect it (maybe it's packaged differently)
Honestly someone with motivation and coffee needs to take a long hard
look at our packaging and see if we can do a better job.
2025-10-06 14:14:49 +11:00
Dion Moult a11f81f2ce Fix #5701. Spaces are now invisible by default and use viewport hiding. 2025-10-06 14:07:20 +11:00
Dion Moult d3b553c4fb Blender tools for hiding and showing objects 2025-10-06 14:05:57 +11:00
Dion Moult 5eb11c3516 Remove "make spatial elements unselectable" setting because it's superseded by spatial locking. 2025-10-06 14:05:31 +11:00
Dion Moult c195799801 See #7202. Revert to safer way to control visibility.
Pity can't control hide_viewport in bulk, but this is perhaps the next
best thing.
2025-10-06 12:19:03 +11:00
Massimo Fabbro ac5a44a962 See #6570. Now it's possible to specify Description in csv import
Now it is possible to specify the column Description in the csv file in order to specify also the cost item description
2025-10-05 17:49:23 +02:00
Dion Moult 86880e9e62 See #4481. Optimise object visibility even further by using foreach_set.
Note: now we are using hide_viewport not hide_set anymore. There might
be pending issues from this. Also, now because we use foreach_set, we
can quickly also apply hide_render, which saves time during underlay
generation.
2025-10-05 23:20:32 +11:00
Dion Moult f8ccdf1170 See #4481. Optimise first load of drawing by lazy loading annotations. 2025-10-05 23:17:42 +11:00
Dion Moult e3d8473d47 Fix #7166. Don't create glazing constituent for doors without transoms. Clarify framing name. 2025-10-05 20:24:13 +11:00
Dion Moult bd5d4cecbf See #7166. Only show total thickness for layer sets. 2025-10-05 20:13:40 +11:00
Dion Moult 2ede8a2206 Fix #7158. UI didn't expect to find a material constituent set with no constituents.
It's technically valid but makes no sense.
2025-10-05 19:41:52 +11:00
Dion Moult cb77f9bd26 Fix #7179. Allow drawing polylines with segments as short as 1mm (previously 100mm which was too high) 2025-10-05 19:30:48 +11:00
Dion Moult 0bb21fedae Fix #7169. Bug where unit scale wasn't set for geometric operations on new projects. 2025-10-05 19:09:47 +11:00
Dion Moult 478f90c2d4 Update IfcTester webapp to use IfcTester logo 2025-10-05 18:46:05 +11:00
Dion Moult a016f6e5c8 Fix #7186. Support upgrading IFC2X3 IfcExtendedMaterialProperties to IFC4 2025-10-05 18:44:14 +11:00
Dion Moult 1b2fa4f994 Fix #7113. IfcTester webapp should use uppercase entity names 2025-10-05 15:32:55 +11:00
Christoph Mellüh 2d8383efc6 Fix MASSUNIT bug (#7173)
* add exception

* black
2025-10-05 15:31:06 +11:00
Dion Moult add5132dec Fix launching of IfcTester webapp with Bonsai 2025-10-05 15:11:18 +11:00
Andrej730 fe333051df build-deps - note on cloning opencollada 2025-10-04 21:40:07 +05:00
Andrej730 8d1b533f91 run-cmake.bat - echo SWIG_INSTALL_DIR
as `SWIG_DIR` is no longer used
2025-10-04 21:40:07 +05:00
Andrej730 fb304e141a build-all.py - note on doubled compilation times 2025-10-04 21:40:07 +05:00
Andrej730 bd094f930e build_rocky - support ccache 2025-10-04 21:40:07 +05:00
Andrej730 b61000d2fe ci.yml - bump ccache action version 2025-10-04 21:40:07 +05:00
Andrej730 c72cadb234 build_rocky_arm - use ccache 2025-10-04 21:40:06 +05:00
Andrej730 10642177a9 build_osx - bump ccache workflow version 2025-10-04 21:40:06 +05:00
Andrej730 0b8ea28463 aud - use main repo
As PR with a fix was merged
2025-10-04 21:40:06 +05:00
Andrej730 9b24f54f0d cmake - override CGAL_Boost_USE_STATIC_LIBS
The issue was with ci.yml - it was using system boost shared libraries (since Boost_USE_STATIC_LIBS by default is OFF), but CGALConfig was overriding leading to compilation issues.
2025-10-04 21:40:06 +05:00
Andrej730 289645dacc ci.yml - let cmake figure out system dependencies automatically 2025-10-04 21:40:06 +05:00
Andrej730 c365acb517 ci.yml - don't provide ccache explicitly
as it's covered automatically by cmake
2025-10-04 21:40:06 +05:00
Andrej730 785cf56621 cmake - specify cxx component for hdf5 package
by default it's targeting `C` and this was part of the issue why ci.yml was failing
2025-10-04 21:40:06 +05:00
Andrej730 60617eecce ci.yml - add ccache for rocksdb
Revert "ci.yml - add ccache for rocksdb"

This reverts commit 71178d4bbc023c793b0800f5987a979fdddc1c30.

Update ci.yml
2025-10-04 21:40:06 +05:00
Andrej730 26c68c00b7 cmake - proceed to package search if HDF5_LIBRARY_DIR not provided
Fixes issue with ci.yml
2025-10-04 21:40:05 +05:00
Esteban Dugueperoux ab0a602f9f black: Format incorrectly formated files (#7194) 2025-10-04 18:59:42 +05:00
Osyotr 066326ab3b Fix warning C4275
It's not enough to disable this warning by adding /wd4257 at build time because it's triggered when the library is consumed.
2025-10-03 18:12:56 +02:00
Thomas Krijnen 02713dbd39 nix build script add occt 7.9.1 patch 2025-10-03 18:08:37 +02:00
Thomas Krijnen d58dae5d2f nix build script add occt 7.9.1 patch 2025-10-03 18:05:39 +02:00
Thomas Krijnen ca3685900f Update clear_schemas() which is now handled by unique_ptr 2025-10-03 14:18:50 +02:00
Thomas Krijnen 5e10e61aca Clean-up parsed simple types #7185 2025-10-03 14:18:07 +02:00
Osyotr 92e25ce3cb Fix warning C4702: unreachable code 2025-10-03 13:56:10 +02:00
Thomas Krijnen 804a670454 Revert revert 2025-10-03 09:04:43 +02:00
Thomas Krijnen efa8382fc5 ifcopenshell.draw --arange-zones see http://blog.ifcopenshell.org/2025/10/bim-legal-2d-notorial-drawings-directly.html 2025-10-02 21:44:52 +02:00
Thomas Krijnen 92f9d5f506 Writing .ifcxml is not supported #7181 2025-10-02 20:28:17 +02:00
Ryan Schultz 8800c14105 extended previous commit to add functionality to printed svg, as well. 2025-10-01 23:21:14 -05:00
Ryan Schultz ec2d0bfa4a Add support for reversing and customizing separators in text annotation lists
Text annotations can now reverse the order of list values (e.g., materials) and use custom separators instead of the default comma-space. Two new properties have been added to EPset_Annotation: Reverse_List (boolean) and List_Separator (string). When text literals contain IFC list/tuple values, they are now detected during variable replacement and can be reversed and joined with the specified separator. The list separator supports escape sequences like \n for newlines, enabling multi-line list displays. These properties are exposed in the text editing UI and properly persist to the IFC file through the new update_text_annotation_properties method, which consolidates all annotation property updates into a single efficient operation.
2025-10-01 22:44:34 -05:00
Thomas Krijnen d684d47dc8 Make sure compound structure is retained as SvgSerializer depends on it 2025-10-01 15:01:50 +02:00
121 changed files with 1108 additions and 614 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ jobs:
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
- name: ccache
uses: hendrikmuhs/ccache-action@v1
uses: hendrikmuhs/ccache-action@v1.2
with:
key: mac-${{ matrix.arch }}
+5 -5
View File
@@ -47,11 +47,11 @@ jobs:
install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true)
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
# Not supported on docker
# - name: ccache
# uses: hendrikmuhs/ccache-action@v1
# with:
# key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
- name: ccache
# Using fork to support Rocky.
uses: Andrej730/ccache-action@main
with:
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
- name: Run Build Script
shell: bash
+5 -5
View File
@@ -47,11 +47,11 @@ jobs:
install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true)
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
# Not supported on docker
# - name: ccache
# uses: hendrikmuhs/ccache-action@v1
# with:
# key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
- name: ccache
# Using fork to support Rocky.
uses: Andrej730/ccache-action@main
with:
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
- name: Run Build Script
shell: bash
+32
View File
@@ -0,0 +1,32 @@
name: ci-ifctester-org
on:
workflow_dispatch:
push:
paths:
- src/ifctester/**
jobs:
publish_website:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Checkout ifctester_org_static_html
uses: actions/checkout@v4
with:
repository: IfcOpenShell/ifctester_org_static_html
token: ${{ secrets.IFCOPENBOT_TOKEN }}
path: ifctester_org_static_html
- name: Build webapp
working-directory: ./src/ifctester
run: |
sudo apt update && sudo apt install -y nodejs
git config --global user.name 'IfcOpenBot'
git config --global user.email 'IfcOpenBot@users.noreply.github.com'
make webapp-build
- name: Commit and push
run: |
cp -r src/ifctester/webapp/dist/* ifctester_org_static_html/
git -C ifctester_org_static_html add .
git -C ifctester_org_static_html commit --allow-empty -m "$(git log --oneline -1)"
git -C ifctester_org_static_html push
+5 -12
View File
@@ -49,7 +49,7 @@ jobs:
python -m pip install --upgrade pip
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely
pip install src/bcf --no-deps
pip install https://github.com/Andrej730/aud/archive/refs/heads/master-reduced-size.zip
pip install git+https://github.com/zdhoward/aud
- name: Install C++ dependencies
run: |
@@ -69,7 +69,7 @@ jobs:
libhdf5-dev libcgal-dev libeigen3-dev
- name: ccache
uses: hendrikmuhs/ccache-action@v1
uses: hendrikmuhs/ccache-action@v1.2
with:
key: ubuntu-22.04-${{ runner.arch }}
@@ -79,6 +79,7 @@ jobs:
git clone https://github.com/facebook/rocksdb --branch v9.11.2
cd rocksdb
mkdir build && cd build
# rocksdb is using ccache automatically.
cmake -DFAIL_ON_WARNINGS=Off \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr/local \
@@ -99,9 +100,9 @@ jobs:
echo ${{ env.pythonLocation }}
mkdir build && cd build
# Ubuntu 22.04's libocct-foundation-dev package doesn't have Config.cmake, so we provide OCC paths directly.
# In later versions of Ubuntu, this can be simplified and the OCC paths can be removed.
cmake \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_STANDARD=17 \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=/usr \
@@ -114,14 +115,6 @@ jobs:
-DCOLLADA_SUPPORT=Off \
"-DSCHEMA_VERSIONS=2x3;4;4x3_add2" \
-DGLTF_SUPPORT=On \
-DJSON_INCLUDE_DIR=/usr/include \
-DCGAL_INCLUDE_DIR=/usr/include \
-DGMP_INCLUDE_DIR=/usr/include \
-DMPFR_INCLUDE_DIR=/usr/include \
-DGMP_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
-DMPFR_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
-DHDF5_INCLUDE_DIR=/usr/include/hdf5/serial \
-DEIGEN_DIR=/usr/include/eigen3 \
-DWITH_ROCKSDB=On \
../cmake
sudo make -j $(nproc)
+12 -9
View File
@@ -151,11 +151,6 @@ if(MSVC AND MSVC_PARALLEL_BUILD)
add_definitions("/MP")
endif()
if (MSVC AND BUILD_SHARED_LIBS)
# @todo how do projects normally deal with this regarding classes derived from std::exception?
add_compile_options(/wd4275)
endif()
if(NO_WARN)
if(MSVC)
add_compile_options("/w")
@@ -259,6 +254,13 @@ endmacro()
if(WITH_CGAL)
if(NOT CGAL_INCLUDE_DIR)
# CGAL is not respecting default Boost_USE_STATIC_LIBS value
# and sometiems it's getting in the way.
if(NOT DEFINED Boost_USE_STATIC_LIBS)
set(CGAL_Boost_USE_STATIC_LIBS OFF)
else()
set(CGAL_Boost_USE_STATIC_LIBS "${Boost_USE_STATIC_LIBS}")
endif()
find_package(CGAL REQUIRED)
if(NOT CGAL_DIR)
message(
@@ -791,7 +793,7 @@ if(HDF5_SUPPORT)
endif()
endif()
if(NOT HDF5_INCLUDE_DIR)
if(NOT HDF5_INCLUDE_DIR OR NOT HDF5_LIBRARY_DIR)
# First try to find it as a config.
find_package(HDF5 CONFIG)
if(HDF5_DIR)
@@ -801,10 +803,11 @@ if(HDF5_SUPPORT)
# If it failed, still try to find as a module.
# E.g. on Ubuntu `libhdf5-dev` doesn't provie hdf5-config.cmake.
# Will automatically fill HDF5_LIBRARIES and HDF5_INCLUDE_DIR.
find_package(HDF5)
find_package(HDF5 COMPONENTS CXX)
if(NOT HDF5_INCLUDE_DIR)
message(
FATAL_ERROR
"HDF5_INCLUDE_DIR is not provided (current value: '${HDF5_INCLUDE_DIR}'). "
"HDF5_LIBRARY_DIR is not provided (current value: '${HDF5_LIBRARY_DIR}'). "
"Also could not find HDF5 package (neither module or config)."
)
@@ -1043,7 +1046,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} IfcGeom)
endif()
endif()
@@ -1173,7 +1176,7 @@ if(BUILD_IFCGEOM)
add_library(geometry_mapping_ifc${schema} STATIC ${IFCGEOM_FILES})
set_target_properties(geometry_mapping_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
target_link_libraries(geometry_mapping_ifc${schema} IfcParse IfcGeom)
target_link_libraries(geometry_mapping_ifc${schema} IfcParse)
list(APPEND mapping_libraries geometry_mapping_ifc${schema})
endforeach()
+11
View File
@@ -344,6 +344,11 @@ PIC = "-fPIC" if BUILD_STATIC else ""
if any(f.startswith("py-") for f in flags):
PYTHON_VERSIONS = [pyv for pyv in PYTHON_VERSIONS if f"py-{''.join(pyv.split('.')[:2])}" in flags]
if any(f.startswith("occt-") for f in flags):
OCCT_VERSION = next(f.split("-", 1)[1] for f in flags if f.startswith("occt-"))
print(OCCT_VERSION)
if explicit_targets:
targets = {dep for target in explicit_targets for dep in gather_dependencies(target)}
else:
@@ -883,6 +888,9 @@ if USE_OCCT and "occ" in targets:
if OCCT_VERSION == "7.8.1":
patches.append("./patches/occt/no_ExpToCasExe_7_8_1.patch")
if OCCT_VERSION == "7.9.1":
patches.append("./patches/occt/no_ExpToCasExe_7_9_1.patch")
if "wasm" in flags:
patches.append("./patches/occt/no_em_js.patch")
@@ -1360,6 +1368,9 @@ if "IfcOpenShell-Python" in targets:
if "wasm" in flags:
ADDITIONAL_ARGS = f"-Wl,-undefined,suppress -sSIDE_MODULE=2 -sEXPORTED_FUNCTIONS=_PyInit__ifcopenshell_wrapper"
# NOTE: We don't use `CXXFLAGS` for wrappers, so wrapper is compiled with different flags
# (e.g. ` -fdata-sections` is missing, which is set by default for executables)
# So cache doesn't match and running build-all.py builds most of ifcopenshell libraries twice.
os.environ["CPPFLAGS"] = f"{CXXFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["CXXFLAGS"] = f"{CXXFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["CFLAGS"] = f"{CFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
@@ -0,0 +1,13 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 34300d41ad..09b2e0d45f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -721,6 +721,8 @@ else()
OCCT_CHECK_AND_UNSET ("3RDPARTY_DOT_EXECUTABLE")
endif()
+list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe)
+
# bison
if (BUILD_YACCLEX)
list (APPEND OCCT_3RDPARTY_CMAKE_LIST "adm/cmake/bison")
+2
View File
@@ -192,6 +192,8 @@ class TopicHandler:
bcf_zip: The BCF zip file to save to.
"""
topic_dir = self.guid
# simulating directory creation (ZipFile in python < 3.11 doesn't have mkdir)
destination_zip.writestr(f"{topic_dir}/", "")
self._save_xml(destination_zip, self._markup, "markup.bcf")
self._save_viewpoints(destination_zip, topic_dir)
self._save_bim_snippet(destination_zip)
+2
View File
@@ -179,6 +179,8 @@ class TopicHandler:
bcf_zip: The BCF zip file to save to.
"""
topic_dir = self.guid
# simulating directory creation (ZipFile in python < 3.11 doesn't have mkdir)
destination_zip.writestr(f"{topic_dir}/", "")
self._save_xml(destination_zip, self._markup, "markup.bcf")
self._save_viewpoints(destination_zip, topic_dir)
self._save_bim_snippet(destination_zip)
+1 -1
View File
@@ -127,7 +127,7 @@ endif
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download GitPython --dest=./wheels
# Provides audio playback for costing
# This is a REALLY IMPORTANT feature
cd build && . env/$(VENV_ACTIVATE) && $(PIP) wheel git+https://github.com/Andrej730/aud.git --wheel-dir=./wheels
cd build && . env/$(VENV_ACTIVATE) && $(PIP) wheel git+https://github.com/zdhoward/aud --wheel-dir=./wheels
# IfcOpenShell dependency - support for new typing features
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download typing_extensions --dest=./wheels
# Required by IfcCSV
@@ -5,10 +5,10 @@ FILE_NAME('Psets_BBIM_Annotation.ifc','2020-01-01T00:00:00',$,$,'Psets_BBIM_Anno
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#2,#3,#4,#29));
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#4,#33,#29,#32,#3,#2));
#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.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','Classes separated 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));
#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.);
@@ -36,5 +36,7 @@ DATA;
#29=IFCSIMPLEPROPERTYTEMPLATE('2pJmUDpB50VBdCOib1zcJJ',$,'Newline_At','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#30=IFCSIMPLEPROPERTYTEMPLATE('2TJn72t_v2cvBUG916Dpev',$,'CustomUnit','Dimension''s custom unit',.P_ENUMERATEDVALUE.,'IfcText',$,#31,$,$,$,.READWRITE.);
#31=IFCPROPERTYENUMERATION('CustomUnit',(IFCTEXT('Feet and Inches - Fractional'),IFCTEXT('Feet - Decimal'),IFCTEXT('Inches - Fractional'),IFCTEXT('Inches - Decimal'),IFCTEXT('Meters'),IFCTEXT('Decimeters'),IFCTEXT('Centimeters'),IFCTEXT('Millimeters')),$);
#32=IFCSIMPLEPROPERTYTEMPLATE('0gjJzDYBX8P85qn1xcAOOo',$,'Reverse_List','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#33=IFCSIMPLEPROPERTYTEMPLATE('22TrcxF8jFNB4buSmzjGEF',$,'List_Separator','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
ENDSEC;
END-ISO-10303-21;
+12 -3
View File
@@ -15,6 +15,8 @@ import pystache
import json
import base64
import xml.etree.ElementTree as ET
import platformdirs
from pathlib import Path
sio_port = 8080 # default port
@@ -233,10 +235,17 @@ async def demo(request):
return web.Response(text=html_content, content_type="text/html")
async def on_startup(app):
pid_file = "running_pid.json"
def get_pid_file_path():
"""Get the path to the PID file in the user's cache directory."""
cache_dir = Path(platformdirs.user_cache_dir("bonsai"))
cache_dir.mkdir(parents=True, exist_ok=True)
return cache_dir / "running_pid.json"
if os.path.exists(pid_file):
async def on_startup(app):
pid_file = get_pid_file_path()
if pid_file.exists():
with open(pid_file, "r") as f:
pids = json.load(f)
else:
+3 -11
View File
@@ -625,18 +625,13 @@ class IfcImporter:
print("Done creating geometry")
def create_spatial_elements(self) -> None:
if tool.Blender.get_addon_preferences().spatial_elements_unselectable:
self.create_generic_elements(self.spatial_elements, unselectable=True)
else:
self.create_generic_elements(self.spatial_elements, unselectable=False)
self.create_generic_elements(self.spatial_elements)
def create_elements(self) -> None:
self.create_generic_elements(self.elements)
self.create_generic_elements(self.gross_elements, is_gross=True)
def create_generic_elements(
self, elements: set[ifcopenshell.entity_instance], unselectable=False, is_gross=False
) -> None:
def create_generic_elements(self, elements: set[ifcopenshell.entity_instance], is_gross=False) -> None:
if isinstance(self.file, ifcopenshell.sqlite):
return self.create_generic_sqlite_elements(elements)
@@ -660,10 +655,6 @@ class IfcImporter:
print("{} / {} elements processed ...".format(i, total))
objects.add(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:
assert isinstance(self.file, ifcopenshell.sql.sqlite)
self.geometry_cache = self.file.get_geometry([e.id() for e in elements])
@@ -1018,6 +1009,7 @@ class IfcImporter:
)
obj = self.create_product(self.project["ifc"])
obj.hide_select = True
obj.hide_viewport = True
self.project["blender"].objects.link(obj)
self.project["blender"].BIMCollectionProperties.obj = obj
props = tool.Blender.get_object_bim_props(obj)
+13 -6
View File
@@ -347,12 +347,10 @@ class DecoratorData:
(font_size_type for font_size_type in FONT_SIZES if font_size_type in classes_split), "regular"
)
font_size = FONT_SIZES[font_size_type]
# get symbol
symbol = tool.Drawing.get_annotation_symbol(element)
# get newline_at
newline_at = pset_data.get("Newline_At", 0)
reverse_list = pset_data.get("Reverse_List", False)
list_separator = pset_data.get("List_Separator") or ", "
# other attributes
literals = tool.Drawing.get_text_literal(obj, return_list=True)
@@ -364,11 +362,20 @@ class DecoratorData:
literal_data = {
"Literal": literal_value,
"BoxAlignment": literal.BoxAlignment,
"CurrentValue": tool.Drawing.replace_text_literal_variables(literal_value, product),
"CurrentValue": tool.Drawing.replace_text_literal_variables(
literal_value, product, reverse_list, list_separator
),
}
literals_data.append(literal_data)
return {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at}
return {
"Literals": literals_data,
"FontSize": font_size,
"Symbol": symbol,
"Newline_At": newline_at,
"Reverse_List": reverse_list,
"List_Separator": list_separator,
}
@classmethod
def get_dimension_data(cls, obj: bpy.types.Object) -> dict[str, Any]:
@@ -106,14 +106,17 @@ class AddAnnotationType(bpy.types.Operator, tool.Ifc.Operator):
obj = bpy.data.objects.new(object_type, None)
obj.name = props.type_name
ifc_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Annotation", "MODEL_VIEW")
element = tool.Drawing.run_root_assign_class(
obj=obj,
ifc_class="IfcTypeProduct",
predefined_type=object_type,
should_add_representation=has_representation,
context=ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Annotation", "MODEL_VIEW"),
context=ifc_context,
ifc_representation_class=tool.Drawing.get_ifc_representation_class(object_type),
)
if representation := tool.Drawing.get_representation(element, ifc_context):
tool.Drawing.reload_representation(obj=obj, representation=representation)
element.ApplicableOccurrence = f"IfcAnnotation/{object_type}"
if props.create_representation_for_type and object_type == "IMAGE":
@@ -435,10 +438,6 @@ class CreateDrawing(bpy.types.Operator):
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
assert context.scene and context.view_layer and context.screen
context.scene.render.filepath = str(Path(svg_path).with_suffix(".png"))
assert (drawing_style := self.cprops.get_active_drawing_style())
@@ -449,7 +448,7 @@ class CreateDrawing(bpy.types.Operator):
previous_visibility: dict[str, bool] = {}
collection = tool.Blender.get_object_bim_props(self.camera).collection
assert collection
# Hie annotations.
# Hide annotations.
for obj in collection.objects:
if context.view_layer.objects.get(obj.name):
previous_visibility[obj.name] = obj.hide_get()
@@ -840,6 +839,7 @@ class CreateDrawing(bpy.types.Operator):
if tool.Drawing.is_camera_orthographic():
self.generate_bisect_linework(context, root)
self.generate_wall_layers(context, root)
self.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root)
@@ -2171,27 +2171,10 @@ class ActivateModel(bpy.types.Operator):
if model_props.show_cut_decorator:
CutDecorator.uninstall()
# Preserve current visibility statuses for:
# - non-ifc objects
# - type product
# - annotations (so we won't unhide other drawings)
ifc_file = tool.Ifc.get()
visibility_status: dict[bpy.types.Object, bool] = {}
for obj in bpy.data.objects:
element = tool.Ifc.get_entity(obj)
if not element:
hide = obj.hide_get()
elif element.is_a("IfcAnnotation"):
hide = True
elif element.is_a("IfcTypeProduct"):
hide = obj.hide_get()
else:
continue
visibility_status[obj] = hide
if not bpy.app.background:
with context.temp_override(**tool.Blender.get_viewport_context()):
bpy.ops.object.hide_view_clear(select=False)
bpy.ops.bim.activate_status_filters(only_if_enabled=True)
elements = {e for obj in context.visible_objects if (e := tool.Ifc.get_entity(obj))}
@@ -2236,15 +2219,9 @@ class ActivateModel(bpy.types.Operator):
tool.Geometry,
obj=obj,
representation=model,
should_reload=False,
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.reset_object_visibility()
tool.Blender.update_viewport()
bonsai.bim.handler.refresh_ui_data()
@@ -744,6 +744,11 @@ class BIMTextProperties(PropertyGroup):
name="Font Size",
)
newline_at: IntProperty(name="Newline At")
reverse_list: BoolProperty(name="Reverse List", description="Reverses the order of any list.", default=False)
list_separator: StringProperty( # pyright: ignore[reportRedeclaration]
name="List Separator",
description="Text used to separate lists. Uses a comma (, ) if empty.",
)
symbol: EnumProperty( # pyright: ignore[reportRedeclaration]
name="Symbol",
description="Symbol from symbols.svg to use for this text.",
@@ -760,6 +765,8 @@ class BIMTextProperties(PropertyGroup):
literals: bpy.types.bpy_prop_collection_idprop[LiteralProps]
font_size: str
newline_at: int
reverse_list: bool
list_separator: str
symbol: Union[str, Literal["NO SYMBOL", "CUSTOM SYMBOL"]]
custom_symbol: str
@@ -796,6 +803,8 @@ class BIMTextProperties(PropertyGroup):
"FontSize": float(self.font_size),
"Newline_At": int(self.newline_at),
"Symbol": self.get_symbol(),
"Reverse_List": self.reverse_list,
"List_Separator": self.list_separator or ", ",
}
return text_data
@@ -839,6 +839,12 @@ class SvgWriter:
symbol = tool.Drawing.get_annotation_symbol(element)
newline_at = tool.Drawing.get_newline_at(element)
# Get reverse_list and list_separator from EPset_Annotation
pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {}
reverse_list = pset_data.get("Reverse_List", False)
list_separator = pset_data.get("List_Separator") or ", "
template_text_fields = []
if symbol:
symbol_transform = self.get_symbol_transform(text_position_svg_str, angle, text_obj)
@@ -855,7 +861,7 @@ class SvgWriter:
# NOTE: zip makes sure that we iterate over the shortest list
for field, text_literal in zip(template_text_fields, text_literals):
field.text = tool.Drawing.replace_text_literal_variables(
text_literal.Literal, product or element
text_literal.Literal, product or element, reverse_list, list_separator
)
field.attrib["class"] = classes_str
@@ -875,7 +881,9 @@ class SvgWriter:
line_number = 0
for text_literal in text_literals:
text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product or element)
text = tool.Drawing.replace_text_literal_variables(
text_literal.Literal, product or element, reverse_list, list_separator
)
text_tags = self.create_text_tag(
text,
text_position_svg,
@@ -590,6 +590,10 @@ class BIM_PT_text(Panel):
row.prop(props, "font_size")
row = self.layout.row(align=True)
row.prop(props, "newline_at")
row = self.layout.row(align=True)
row.prop(props, "reverse_list")
row = self.layout.row(align=True)
row.prop(props, "list_separator")
row = self.layout.row(align=True)
row.prop(props, "symbol")
@@ -648,6 +652,12 @@ class BIM_PT_text(Panel):
row = self.layout.row(align=True)
row.label(text="Newline_At")
row.label(text=str(text_data["Newline_At"]))
row = self.layout.row(align=True)
row.label(text="Reverse_List")
row.label(text=str(text_data["Reverse_List"]))
row = self.layout.row(align=True)
row.label(text="List_Separator")
row.label(text=str(text_data["List_Separator"]))
for literal_data in text_data["Literals"]:
box = self.layout.box()
@@ -180,9 +180,6 @@ def create_annotation_occurrence(context):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
if obj.data and not relating_type_rep:
@@ -427,9 +427,7 @@ class SwitchRepresentation(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
ifc_definition_id: bpy.props.IntProperty()
should_reload: bpy.props.BoolProperty()
disable_opening_subtractions: bpy.props.BoolProperty()
should_switch_all_meshes: bpy.props.BoolProperty()
@classmethod
def poll(cls, context):
@@ -465,9 +463,6 @@ class SwitchRepresentation(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=self.should_reload,
is_global=self.should_switch_all_meshes,
should_sync_changes_first=True,
)
@@ -721,9 +716,6 @@ class UpdateParametricRepresentation(bpy.types.Operator):
tool.Geometry,
obj=obj,
representation=tool.Ifc.get().by_id(props.ifc_definition_id),
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
if show_representation_parameters:
core.get_representation_ifc_parameters(tool.Geometry, obj=obj)
@@ -1904,9 +1896,6 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=self.target,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=True,
)
@@ -157,8 +157,6 @@ class BIM_PT_representations(Panel):
icon="FILE_REFRESH" if representation["is_active"] else "OUTLINER_DATA_MESH",
text="",
)
op.should_switch_all_meshes = True
op.should_reload = True
op.ifc_definition_id = representation["id"]
op.disable_opening_subtractions = False
row.operator("bim.remove_representation", icon="X", text="").representation_id = representation["id"]
+18 -15
View File
@@ -323,20 +323,23 @@ class ObjectMaterialData:
@classmethod
def total_thickness(cls):
if cls.material:
layers = []
if cls.material.is_a("IfcMaterialLayerSetUsage"):
layers = cls.material.ForLayerSet.MaterialLayers
elif cls.material.is_a("IfcMaterialLayerSet"):
layers = cls.material.MaterialLayers
thickness = sum([l.LayerThickness for l in layers or []])
prefs = tool.Blender.get_addon_preferences()
assert bpy.context.scene
unit_system = bpy.context.scene.unit_settings.system
precision = None
if unit_system == "IMPERIAL":
precision = prefs.doc.imperial_precision
return format_distance(thickness, precision=precision, suppress_zero_inches=True, in_unit_length=True)
if not cls.material:
return
layers = []
if cls.material.is_a("IfcMaterialLayerSetUsage"):
layers = cls.material.ForLayerSet.MaterialLayers
elif cls.material.is_a("IfcMaterialLayerSet"):
layers = cls.material.MaterialLayers
if not layers:
return
thickness = sum([l.LayerThickness for l in layers or []])
prefs = tool.Blender.get_addon_preferences()
assert bpy.context.scene
unit_system = bpy.context.scene.unit_settings.system
precision = None
if unit_system == "IMPERIAL":
precision = prefs.doc.imperial_precision
return format_distance(thickness, precision=precision, suppress_zero_inches=True, in_unit_length=True)
@classmethod
def set_item_name(cls) -> Union[str, None]:
@@ -404,7 +407,7 @@ class ObjectMaterialData:
material = cls.material
if not cls.material or not material.is_a("IfcMaterialConstituentSet"):
return []
return [m.Name for m in material.MaterialConstituents if m.Name]
return [m.Name for m in material.MaterialConstituents or [] if m.Name]
@classmethod
def is_type_material_overridden(cls) -> bool:
@@ -254,6 +254,10 @@ class BIM_PT_object_material(Panel):
active_object = bpy.context.active_object
self.layerset_bounds(box, active_object, location="Top_Exterior")
if not ObjectMaterialData.data["set_items"]:
row = box.row()
row.label(text="No Materials Found")
for set_item in ObjectMaterialData.data["set_items"]:
if (
len(self.props.material_set_item_profile_attributes)
@@ -357,6 +361,10 @@ class BIM_PT_object_material(Panel):
active_object = bpy.context.active_object
self.layerset_bounds(box, active_object, location="Top_Interior")
if not ObjectMaterialData.data["set_items"]:
row = box.row()
row.label(text="No Materials Found")
for set_item in ObjectMaterialData.data["set_items"]:
material_name = set_item["material"]
material_id = set_item["material_id"]
@@ -193,9 +193,6 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=False,
)
@@ -224,9 +221,6 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=True,
)
+7 -11
View File
@@ -95,15 +95,17 @@ def update_door_modifier_representation(obj: bpy.types.Object) -> None:
representation_data["part_of_product"] = None
tool.Model.replace_object_ifc_representation(body, obj, model_representation)
if fallback_material := (int(props.lining_material) or int(props.framing_material) or int(props.glazing_material)):
materials = {
"Lining": tool.Ifc.get().by_id(int(props.lining_material) or fallback_material),
"Framing": tool.Ifc.get().by_id(int(props.framing_material) or fallback_material),
}
if props.transom_thickness:
materials["Glazing"] = tool.Ifc.get().by_id(int(props.glazing_material) or fallback_material)
ifcopenshell.api.material.set_shape_aspect_constituents(
ifc_file,
element=element,
context=body,
materials={
"Lining": tool.Ifc.get().by_id(int(props.lining_material) or fallback_material),
"Framing": tool.Ifc.get().by_id(int(props.framing_material) or fallback_material),
"Glazing": tool.Ifc.get().by_id(int(props.glazing_material) or fallback_material),
},
materials=materials,
)
elif material := ifcopenshell.util.element.get_material(element):
ifcopenshell.api.material.unassign_material(ifc_file, products=[element])
@@ -143,9 +145,6 @@ def update_door_modifier_representation(obj: bpy.types.Object) -> None:
tool.Geometry,
obj=obj,
representation=ifcopenshell.util.representation.get_representation(element, active_context),
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
# type attributes
@@ -579,9 +578,6 @@ class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
props.is_editing = False
@@ -186,9 +186,6 @@ class FilledOpeningGenerator:
tool.Geometry,
obj=voided_obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
def regenerate_from_type(self, usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
@@ -249,9 +246,6 @@ class FilledOpeningGenerator:
tool.Geometry,
obj=voided_obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
def generate_opening_from_filling(
@@ -420,9 +414,6 @@ class RecalculateFill(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=building_obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
@@ -798,9 +789,6 @@ class CloneOpening(Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
@@ -580,9 +580,6 @@ def get_generic_product_preview_data(context, relating_type):
tool.Geometry,
obj_type,
representation,
should_reload=True,
is_global=False,
should_sync_changes_first=False,
)
context.view_layer.update()
break
@@ -406,9 +406,6 @@ class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
elif self.representation_template == "EXTRUSION":
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
@@ -422,9 +419,6 @@ class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return
@@ -453,9 +447,6 @@ class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
# Update required as core.type.assign_type may change obj.data
@@ -738,7 +729,4 @@ def regenerate_profile_usage(usecase_path, ifc_file, settings):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
+3 -14
View File
@@ -98,7 +98,8 @@ class DumbProfileGenerator:
vec1 = Vector((polyline_points[i].x, polyline_points[i].y, polyline_points[i].z))
vec2 = Vector((polyline_points[i + 1].x, polyline_points[i + 1].y, polyline_points[i + 1].z))
coords = (vec1, vec2)
profiles.append(self.create_profile_from_2_points(coords))
if profile := self.create_profile_from_2_points(coords):
profiles.append(profile)
return profiles, is_polyline_closed
def derive_from_cursor(self) -> bpy.types.Object:
@@ -166,9 +167,6 @@ class DumbProfileGenerator:
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
pset = ifcopenshell.api.pset.add_pset(self.file, product=element, name="EPset_Parametric")
@@ -183,7 +181,7 @@ class DumbProfileGenerator:
) -> ProfileFrom2PointsReturn:
self.direction = coords[1] - coords[0]
length = self.direction.length
if round(length, 4) < 0.1:
if round(length, 4) < 0.001:
return
data: dict[str, Any] = {"coords": coords}
@@ -567,9 +565,6 @@ class DumbProfileJoiner:
tool.Geometry,
obj=obj,
representation=new_body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
tool.Geometry.record_object_materials(obj)
if element.is_a("IfcFlowSegment") or element.is_a("IfcFlowFitting"):
@@ -1036,9 +1031,6 @@ def disable_editing_extrusion_axis(context):
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
@@ -1100,9 +1092,6 @@ class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
bpy.context.view_layer.update()
@@ -538,9 +538,6 @@ def cancel_editing_railing_path(context: bpy.types.Context) -> set[str]:
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
@@ -167,9 +167,6 @@ class DumbSlabGenerator:
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
obj.matrix_world = obj.matrix_world @ Matrix.Rotation(self.x_angle, 4, "X")
@@ -360,9 +357,6 @@ class DumbSlabPlaner:
tool.Geometry,
obj=obj,
representation=new_rep,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
bonsai.core.geometry.remove_representation(
tool.Ifc, tool.Geometry, obj=obj, representation=representation
@@ -386,9 +380,6 @@ class DumbSlabPlaner:
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
@@ -588,9 +579,6 @@ class EditSketchExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
@@ -632,9 +620,6 @@ def disable_editing_extrusion_profile(context):
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
@@ -767,9 +752,6 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
# Only certain classes should have a footprint
+1 -1
View File
@@ -550,7 +550,7 @@ class BIM_PT_door(bpy.types.Panel):
row = prop_with_search(self.layout, props, "lining_material")
tool.Model.draw_material_ui_select(row, props.lining_material)
row = prop_with_search(self.layout, props, "framing_material", text="Panel Material")
row = prop_with_search(self.layout, props, "framing_material", text="Panel Framing Material")
tool.Model.draw_material_ui_select(row, props.framing_material)
if props.transom_thickness:
row = prop_with_search(self.layout, props, "glazing_material")
@@ -517,9 +517,6 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
# Object rotation
@@ -1024,9 +1021,6 @@ class DumbWallGenerator:
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
pset = ifcopenshell.api.pset.add_pset(self.file, product=element, name="EPset_Parametric")
ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties={"Engine": "Bonsai.DumbLayer2"})
@@ -123,9 +123,6 @@ def update_window_modifier_representation(context: bpy.types.Context) -> None:
tool.Geometry,
obj=obj,
representation=ifcopenshell.util.representation.get_representation(element, active_context),
should_reload=True,
is_global=True,
should_sync_changes_first=True,
)
# type attributes
@@ -495,9 +492,6 @@ class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
props.is_editing = False
@@ -155,6 +155,7 @@ class CreateProject(bpy.types.Operator):
tool.Ifc, tool.Georeference, tool.Project, tool.Spatial, schema=props.export_schema, template=template
)
tool.Blender.register_toolbar()
tool.Loader.set_unit_scale(ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()))
def rollback(self, data):
IfcStore.file = None
@@ -333,9 +333,6 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
else:
@@ -579,9 +576,6 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
if not tool.Ifc.get_entity(props.representation_obj):
bpy.data.objects.remove(props.representation_obj)
@@ -601,9 +595,6 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
elif representation_template == "EXTRUSION":
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
@@ -650,9 +641,6 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
elif representation_template in ("LAYERSET_AXIS2", "LAYERSET_AXIS3"):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
@@ -763,9 +751,6 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
elif representation_template == "EDGE":
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
@@ -778,9 +763,6 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
elif representation_template == "FACE":
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
@@ -793,9 +775,6 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
bpy.context.view_layer.update() # Ensures obj.matrix_world is correct
@@ -105,6 +105,7 @@ classes = (
operator.RemoveWorkTime,
operator.ReorderTask,
operator.SaveAnimationColorScheme,
operator.SelectTaskElements,
operator.SelectTaskRelatedInputs,
operator.SelectTaskRelatedProducts,
operator.SelectUnassignedWorkScheduleProducts,
@@ -243,6 +243,9 @@ class SequenceData:
data["NestingIndex"] = None
for rel in task.Nests or []:
data["NestingIndex"] = rel.RelatedObjects.index(task)
data["TotalInputs"] = len(tool.Sequence.get_task_inputs(task))
data["TotalOutputs"] = len(tool.Sequence.get_task_outputs(task))
data["TotalElements"] = data["TotalInputs"] + data["TotalOutputs"]
cls.data["tasks"][task.id()] = data
@classmethod
@@ -1281,6 +1281,17 @@ class DisableEditingSequence(bpy.types.Operator):
return {"FINISHED"}
class SelectTaskElements(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.select_task_elements"
bl_label = "Select All Task Elements"
bl_options = {"REGISTER", "UNDO"}
task: bpy.props.IntProperty()
def _execute(self, context):
core.select_task_inputs(tool.Sequence, tool.Spatial, task=tool.Ifc.get().by_id(self.task))
core.select_task_outputs(tool.Sequence, tool.Spatial, task=tool.Ifc.get().by_id(self.task))
class SelectTaskRelatedProducts(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.select_task_related_products"
bl_label = "Select All Output Products"
+11 -3
View File
@@ -75,6 +75,8 @@ def update_active_task_index(self, context):
bonsai.bim.module.pset.data.refresh()
if self.editing_task_type == "SEQUENCE":
tool.Sequence.load_task_properties()
if self.should_auto_select:
tool.Sequence.select_active_task_elements(task)
def update_active_task_outputs(self, context):
@@ -485,6 +487,9 @@ class BIMWorkScheduleProperties(PropertyGroup):
other_columns: EnumProperty(
items=[
("Controls.Calendar", "Calendar", ""),
("Controls.TotalElements", "Total Elements", "The total number of assigned elements to this task"),
("Controls.TotalInputs", "Total Inputs", "The total number of input elements to this task"),
("Controls.TotalOutputs", "Total Outputs", "The total number of input elements to this task"),
],
name="Special Columns",
)
@@ -518,9 +523,9 @@ class BIMWorkScheduleProperties(PropertyGroup):
active_task_input_index: IntProperty(name="Active Task Input Index")
task_outputs: CollectionProperty(name="Task Outputs", type=TaskProduct)
active_task_output_index: IntProperty(name="Active Task Output Index")
show_nested_outputs: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_outputs)
show_nested_resources: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_resources)
show_nested_inputs: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_inputs)
show_nested_outputs: BoolProperty(name="Show Nested Task Elements", default=False, update=update_active_task_outputs)
show_nested_resources: BoolProperty(name="Show Nested Task Elements", default=False, update=update_active_task_resources)
show_nested_inputs: BoolProperty(name="Show Nested Task Elements", default=False, update=update_active_task_inputs)
product_input_tasks: CollectionProperty(name="Product Task Inputs", type=TaskProduct)
product_output_tasks: CollectionProperty(name="Product Task Outputs", type=TaskProduct)
active_product_output_task_index: IntProperty(name="Active Product Output Task Index")
@@ -531,6 +536,9 @@ class BIMWorkScheduleProperties(PropertyGroup):
filter_by_active_schedule: BoolProperty(
name="Filter By Active Schedule", default=False, update=update_filter_by_active_schedule
)
should_auto_select: BoolProperty(
name="Auto Select", description="Auto select elements when task is selected", default=False
)
if TYPE_CHECKING:
work_schedule_predefined_types: str
+36 -14
View File
@@ -290,13 +290,21 @@ class BIM_PT_work_schedules(Panel):
row2 = self.layout.row(align=True)
row2.alignment = "RIGHT"
row2.prop(self.props, "enable_reorder", text="", icon="SORTALPHA")
row2.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING")
row2.operator("bim.enable_editing_task_time", text="", icon="TIME").task = ifc_definition_id
row2.operator("bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO").task = (
col = row2.column()
row_ = col.row(align=True)
op = row_.operator("bim.select_task_elements", icon="RESTRICT_SELECT_OFF", text="Select Elements")
op.task = task.ifc_definition_id
row_.prop(self.props, "should_auto_select", icon="OBJECT_HIDDEN", text="")
col = row2.column()
row_ = col.row(align=True)
row_.prop(self.props, "enable_reorder", text="", icon="SORTALPHA")
row_.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING")
row_.operator("bim.enable_editing_task_time", text="", icon="TIME").task = ifc_definition_id
row_.operator("bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO").task = (
ifc_definition_id
)
row2.operator("bim.enable_editing_task_attributes", text="", icon="GREASEPENCIL").task = (
row_.operator("bim.enable_editing_task_attributes", text="", icon="GREASEPENCIL").task = (
ifc_definition_id
)
row.operator("bim.add_task", text="Add", icon="ADD").task = ifc_definition_id
@@ -716,11 +724,10 @@ class BIM_PT_task_icom(Panel):
input_id = self.props.task_inputs[self.props.active_task_input_index].ifc_definition_id
op.related_object = input_id
op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="Select")
op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="")
op.task = task.ifc_definition_id
row2.prop(self.props, "show_nested_inputs", icon="OUTLINER", text="")
row2 = col.row()
row2.prop(self.props, "show_nested_inputs", text="Show Nested")
row2 = col.row()
row2.template_list("BIM_UL_task_inputs", "", self.props, "task_inputs", self.props, "active_task_input_index")
@@ -744,8 +751,7 @@ class BIM_PT_task_icom(Panel):
op.related_object_type = "RESOURCE"
op.resource = self.props.task_resources[self.props.active_task_resource_index].ifc_definition_id
row2 = col.row()
row2.prop(self.props, "show_nested_resources", text="Show Nested")
row2.prop(self.props, "show_nested_resources", icon="OUTLINER", text="")
row2 = col.row()
row2.template_list(
@@ -773,10 +779,10 @@ class BIM_PT_task_icom(Panel):
output_id = self.props.task_outputs[self.props.active_task_output_index].ifc_definition_id
op.relating_product = output_id
op = row2.operator("bim.select_task_related_products", icon="RESTRICT_SELECT_OFF", text="Select")
op = row2.operator("bim.select_task_related_products", icon="RESTRICT_SELECT_OFF", text="")
op.task = task.ifc_definition_id
row2 = col.row()
row2.prop(self.props, "show_nested_outputs", text="Show Nested")
row2.prop(self.props, "show_nested_outputs", icon="OUTLINER", text="")
row2 = col.row()
row2.template_list(
"BIM_UL_task_outputs", "", self.props, "task_outputs", self.props, "active_task_output_index"
@@ -893,7 +899,7 @@ class BIM_UL_tasks(UIList):
split2 = split1.split(factor=0.9 - min(0.5, 0.15 * len(self.props.columns)))
split2.prop(item, "name", emboss=False, text="")
BIM_UL_tasks.draw_custom_columns(self.props, split2, item, task)
BIM_UL_tasks.draw_custom_columns(self.props, split2, item, task, item.ifc_definition_id)
if self.props.active_task_id and self.props.editing_task_type == "ATTRIBUTES":
row.prop(
@@ -960,6 +966,7 @@ class BIM_UL_tasks(UIList):
row: bpy.types.UILayout,
item: Optional[bpy.types.PropertyGroup] = None,
task: Optional[dict[str, Any]] = None,
ifc_definition_id: int = 0,
*,
header: bool = False,
) -> None:
@@ -999,6 +1006,21 @@ class BIM_UL_tasks(UIList):
row.label(text=item.derived_calendar + "*")
else:
row.label(text=item.calendar or "-")
elif column.name == "Controls.TotalInputs":
if header:
row.label(text="# Inputs")
else:
row.label(text=str(SequenceData.data["tasks"][ifc_definition_id]["TotalInputs"]))
elif column.name == "Controls.TotalOutputs":
if header:
row.label(text="# Outputs")
else:
row.label(text=str(SequenceData.data["tasks"][ifc_definition_id]["TotalOutputs"]))
elif column.name == "Controls.TotalElements":
if header:
row.label(text="# Elements")
else:
row.label(text=str(SequenceData.data["tasks"][ifc_definition_id]["TotalElements"]))
else:
ifc_class, name = column.name.split(".")
if header:
+5 -5
View File
@@ -148,14 +148,14 @@ class SpatialDecompositionData:
elif tool.Ifc.get_schema() == "IFC4X3":
results = {
"IfcBridge": ["IfcBridge", "IfcBridgePart", "IfcSpace"],
"IfcBridgePart": ["IfcSpace"],
"IfcBridgePart": ["IfcBridgePart", "IfcSpace"],
"IfcBuilding": ["IfcBuilding", "IfcBuildingStorey", "IfcSpace"],
"IfcBuildingStorey": ["IfcBuildingStorey", "IfcSpace"],
"IfcExternalSpatialElement": ["IfcExternalSpatialElement"],
"IfcFacility": ["IfcFacility", "IfcFacilityPartCommon", "IfcSpace"],
"IfcFacilityPartCommon": ["IfcSpace"],
"IfcFacilityPartCommon": ["IfcFacilityPartCommon", "IfcSpace"],
"IfcMarineFacility": ["IfcMarineFacility", "IfcMarinePart", "IfcSpace"],
"IfcMarinePart": ["IfcSpace"],
"IfcMarinePart": ["IfcMarinePart", "IfcSpace"],
"IfcProject": [
"IfcBridge",
"IfcBuilding",
@@ -168,9 +168,9 @@ class SpatialDecompositionData:
"IfcSpace",
],
"IfcRailway": ["IfcRailway", "IfcRailwayPart", "IfcSpace"],
"IfcRailwayPart": ["IfcSpace"],
"IfcRailwayPart": ["IfcRailwayPart", "IfcSpace"],
"IfcRoad": ["IfcRoad", "IfcRoadPart", "IfcSpace"],
"IfcRoadPart": ["IfcSpace"],
"IfcRoadPart": ["IfcRoadPart", "IfcSpace"],
"IfcSite": [
"IfcBridge",
"IfcBuilding",
@@ -70,7 +70,7 @@ class GridDecorator:
grid_props = tool.Spatial.get_grid_props()
for axis in grid_props.grid_axes:
if not (obj := axis.obj) or obj.hide_get() == True:
if not (obj := axis.obj) or obj.visible_get() is False:
continue
if obj.select_get() and context.mode != "OBJECT":
continue
@@ -125,21 +125,22 @@ class GridDecorator:
unselected_edges = []
grid_props = tool.Spatial.get_grid_props()
for axis in grid_props.grid_axes:
if (obj := axis.obj) and obj.hide_get() == False:
if obj.select_get():
if context.mode != "OBJECT":
continue
edges = selected_edges
verts = selected_verts
else:
edges = unselected_edges
verts = unselected_verts
i = len(verts)
edges.append([i, i + 1])
matrix_world = obj.matrix_world
v1 = matrix_world @ obj.data.vertices[0].co
v2 = matrix_world @ obj.data.vertices[1].co
verts.extend([v1, v2])
if not (obj := axis.obj) or obj.visible_get() is False:
continue
if obj.select_get():
if context.mode != "OBJECT":
continue
edges = selected_edges
verts = selected_verts
else:
edges = unselected_edges
verts = unselected_verts
i = len(verts)
edges.append([i, i + 1])
matrix_world = obj.matrix_world
v1 = matrix_world @ obj.data.vertices[0].co
v2 = matrix_world @ obj.data.vertices[1].co
verts.extend([v1, v2])
if unselected_verts:
self.draw_batch("LINES", unselected_verts, decorator_color_background, unselected_edges)
@@ -550,9 +550,7 @@ class ToggleGrids(bpy.types.Operator):
is_visible: bpy.props.BoolProperty(name="Is Visible", default=False, options={"SKIP_SAVE"})
def execute(self, context):
for element in tool.Ifc.get().by_type("IfcGrid") + tool.Ifc.get().by_type("IfcGridAxis"):
if obj := tool.Ifc.get_object(element):
obj.hide_set(not self.is_visible)
tool.Spatial.set_grid_visibility(self.is_visible)
return {"FINISHED"}
@@ -564,11 +562,5 @@ class ToggleSpatialElements(bpy.types.Operator):
is_visible: bpy.props.BoolProperty(name="Is Visible", default=False, options={"SKIP_SAVE"})
def execute(self, context):
if tool.Ifc.get().schema == "IFC2X3":
elements = tool.Ifc.get().by_type("IfcSpatialStructureElement")
else:
elements = tool.Ifc.get().by_type("IfcSpatialElement")
for element in elements:
if obj := tool.Ifc.get_object(element):
obj.hide_set(not self.is_visible)
tool.Spatial.set_space_visibility(self.is_visible)
return {"FINISHED"}
+1 -1
View File
@@ -224,7 +224,7 @@ class BIMSpatialDecompositionProperties(PropertyGroup):
is_visible: BoolProperty(
name="Is Visible",
description="Show or hide spatial elements, such as buildings, sites, etc",
default=True,
default=False,
update=update_spatial_is_visible,
)
container_filter: StringProperty(name="Container Filter", default="", options={"TEXTEDIT_UPDATE"})
@@ -135,6 +135,7 @@ class IfcTesterNamespace(socketio.AsyncNamespace):
try:
request_id = data.get("id")
ids_string = data.get("ids")
props = tool.Tester.get_tester_props()
if not request_id:
await self.emit("error", {"error": "No request ID provided"}, room=sid)
@@ -176,7 +177,7 @@ class IfcTesterNamespace(socketio.AsyncNamespace):
json_report = json_reporter.to_string()
# HTML report
html_reporter = ifctester.reporter.Html(ids)
html_reporter = ifctester.reporter.Html(ids, hide_skipped=props.hide_skipped_specs)
html_reporter.report()
html_report = html_reporter.to_string()
@@ -267,7 +268,7 @@ class ExecuteIfcTester(bpy.types.Operator):
start = time.time()
if props.generate_html_report:
engine = ifctester.reporter.Html(specs)
engine = ifctester.reporter.Html(specs, props.hide_skipped_specs)
engine.report()
output_path = output.as_posix()
engine.to_file(output_path)
@@ -337,9 +338,21 @@ class StartIfcTesterWebapp(bpy.types.Operator):
websocket_server_thread = threading.Thread(target=run_websocket_server, daemon=True)
websocket_server_thread.start()
py_version = sys.version_info
bonsai_lib_path = (
Path(bpy.utils.user_resource("EXTENSIONS"))
/ ".local"
/ "lib"
/ f"python{py_version.major}.{py_version.minor}"
/ "site-packages"
)
env = os.environ.copy()
env["BONSAI_LIB_PATH"] = str(bonsai_lib_path)
env["BONSAI_VERSION"] = tool.Blender.get_bonsai_version()
# Start the Flask server as subprocess
webapp_process = subprocess.Popen(
[sys.executable, webapp_serve_path, "--host", "127.0.0.1", "--port", str(webapp_port)]
[sys.executable, webapp_serve_path, "--host", "127.0.0.1", "--port", str(webapp_port)], env=env
)
# Update properties
@@ -472,6 +485,7 @@ class SelectRequirement(bpy.types.Operator):
props.n_entities = len(failed_entities)
props.has_entities = True if props.n_entities > 0 else False
props.failed_entities.clear()
props.active_requirement_index = self.req_index
for e in failed_entities:
new_entity = props.failed_entities.add()
@@ -71,6 +71,8 @@ class IfcTesterProperties(PropertyGroup):
generate_ods_report: BoolProperty(default=False, name="Generate ODS report", options=set())
flag: BoolProperty(default=False, name="Flag Failed Entities", options=set())
active_specification_index: IntProperty(name="Active Specification Index", update=update_active_specification_index)
active_requirement_index: IntProperty(name="Active Requirement Index")
old_index: IntProperty(name="", default=0)
active_failed_entity_index: IntProperty(name="Active Failed Entity Index")
specifications: CollectionProperty(name="Specifications", type=Specification)
@@ -80,6 +82,7 @@ class IfcTesterProperties(PropertyGroup):
webapp_server_port: IntProperty(name="Webapp Server Port", default=0)
webapp_is_running: BoolProperty(default=False, name="Webapp Is Running", options=set())
websocket_server_port: IntProperty(name="WebSocket Server Port", default=0)
hide_skipped_specs: BoolProperty(default=False, name="Hide skipped Specifications", options=set())
if TYPE_CHECKING:
specs: MultipleFileSelect
@@ -89,6 +92,7 @@ class IfcTesterProperties(PropertyGroup):
generate_ods_report: bool
flag: bool
active_specification_index: int
active_requirement_index: int
old_index: int
active_failed_entity_index: int
specifications: bpy.types.bpy_prop_collection_idprop[Specification]
+54 -3
View File
@@ -20,6 +20,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from bonsai.bim.ui import draw_multiline_text
import bpy
from bpy.types import Panel, UIList
import bonsai.tool as tool
@@ -60,7 +62,8 @@ class BIM_PT_tester(Panel):
row.prop(props, "generate_ods_report")
row = self.layout.row()
row.prop(props, "flag")
row = self.layout.row()
row.prop(props, "hide_skipped_specs")
if not tool.Ifc.get() or not props.should_load_from_memory:
row = self.layout.row(align=True)
props.ifc_files.layout_file_select(row, "*.ifc;*.ifczip;*.ifcxml", "IFC File(s)")
@@ -95,13 +98,16 @@ class BIM_PT_tester(Panel):
"active_specification_index",
)
self.draw_editable_ui()
self.draw_editable_ui(context)
row = self.layout.row()
row.operator("bim.export_bcf", text="Export BCF", icon="EXPORT")
def draw_editable_ui(self) -> None:
def draw_editable_ui(self, context: bpy.types.Context) -> None:
props = tool.Tester.get_tester_props()
specification = TesterData.data["specification"]
is_skipped = specification["total_checks"] == 0 and specification["cardinality"] == "optional"
if props.hide_skipped_specs and is_skipped:
return
n_requirements = len(specification["requirements"])
@@ -110,6 +116,13 @@ class BIM_PT_tester(Panel):
row.label(
text=f'Passed: {specification["total_checks_pass"]}/{specification["total_checks"]} ({specification["percent_checks_pass"]}%)'
)
row = self.layout.row()
if specification.get("instructions"):
row.label(text="Instructions:")
box = self.layout.box()
column = box.column(align=True)
draw_multiline_text(column, specification.get("instructions"), context=context)
row = self.layout.row()
row.label(text=f"Requirements ({n_requirements}):")
if props.flag:
@@ -132,6 +145,16 @@ class BIM_PT_tester(Panel):
and props.n_entities > 0
and len(props.failed_entities) > 0
):
requirement = specification["requirements"][props.active_requirement_index]
metadata = requirement.get("metadata")
if metadata and metadata.get("@instructions"):
row = self.layout.row()
row.label(text="Instructions:")
box = self.layout.box()
column = box.column(align=True)
draw_multiline_text(column, metadata.get("@instructions"), context=context)
row = self.layout.row()
row.label(text=f"Failed entities [{props.n_entities}]:")
self.layout.template_list(
@@ -160,6 +183,34 @@ class BIM_UL_tester_specifications(UIList):
row.label(text=item.name, icon="CHECKMARK" if item.status else "CANCEL")
row.label(text=item.description)
def filter_items(self, context: bpy.types.Context, data: IfcTesterProperties, propname: str):
items = getattr(data, propname)
filter_flags = [self.bitflag_filter_item] * len(items)
props = tool.Tester.get_tester_props()
if props.hide_skipped_specs:
for idx, item in enumerate(items):
report = tool.Tester.report[idx]
if report["total_checks"] != 0:
filter_flags[idx] |= self.bitflag_filter_item
else:
filter_flags[idx] &= ~self.bitflag_filter_item
filter_name = self.filter_name
if filter_name:
name_filtered = bpy.types.UI_UL_list.filter_items_by_name(
filter_name,
self.bitflag_filter_item,
items,
"name",
)
if len(name_filtered) == len(filter_flags):
for idx, flag in enumerate(name_filtered):
if flag == 0:
filter_flags[idx] &= ~self.bitflag_filter_item
return filter_flags, []
class BIM_UL_tester_failed_entities(UIList):
def draw_item(
@@ -121,9 +121,6 @@ class UnassignType(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=obj,
representation=new_active_representation,
should_reload=False,
is_global=False,
should_sync_changes_first=False,
)
return {"FINISHED"}
@@ -148,11 +148,6 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=voided_obj,
representation=representation,
should_reload=True,
is_global=True,
# Don't sync changes because object has an opening,
# therefore bim.update_representaiton wouldn't work either way.
should_sync_changes_first=False,
)
tool.Geometry.lock_scale(voided_obj)
@@ -196,9 +191,6 @@ class RemoveOpening(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry,
obj=building_obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
tool.Geometry.unlock_scale_object_with_openings(obj)
tool.Geometry.clear_cache(element)
+26 -7
View File
@@ -40,6 +40,7 @@ from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDProperty
from bonsai.bim.module.pset.prop import IfcProperty
from typing import Optional, TYPE_CHECKING, Literal
from natsort import natsorted
import textwrap
if TYPE_CHECKING:
@@ -373,11 +374,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
description="Path to create and store temporary files. If left blank, a system default will be used.",
subtype="DIR_PATH",
)
spatial_elements_unselectable: BoolProperty(
name="Make Spatial Elements Unselectable By Default",
default=True,
description="If disabled, it will be possible to select spatial elements in the 3D viewport.\nIt is recommended to keep this `Enabled`, as this can have unintended consequences",
)
decorations_colour: bpy.props.FloatVectorProperty(
name="Decorations Color", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
)
@@ -503,7 +499,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
activate_workspace: bool
should_setup_toolbar: bool
should_play_chaching_sound: bool
spatial_elements_unselectable: bool
tmp_dir: str
decorations_colour: tuple[float, float, float, float]
decorator_color_selected: tuple[float, float, float, float]
@@ -565,7 +560,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
layout.prop(self, "should_setup_toolbar")
layout.prop(self, "should_use_snap")
layout.prop(self, "should_play_chaching_sound")
layout.prop(self, "spatial_elements_unselectable")
def draw_model_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "occurrence_name_style")
@@ -1498,6 +1492,31 @@ def draw_custom_context_menu(self: bpy.types.Menu, context: bpy.types.Context) -
url_op.uri = url
def draw_multiline_text(
layout: bpy.types.UILayout,
text: str,
*,
context: bpy.types.Context | None = None,
) -> None:
"""Render a read-only text box that wraps long text."""
assert layout
region_width = 200
if context and context.region:
region_width = context.region.width
approximate_char_width = 7 # Empirical average width for Blender UI font (px)
wrap_width = max(20, int(region_width / approximate_char_width))
for paragraph in text.splitlines():
if not paragraph:
layout.label(text="")
continue
for line in textwrap.wrap(paragraph, width=wrap_width):
layout.label(text=line)
class BIM_PT_decorators_overlay(Panel):
bl_space_type = "VIEW_3D"
bl_region_type = "HEADER"
+2 -3
View File
@@ -39,7 +39,7 @@ def disable_editing_text(drawing: type[tool.Drawing], obj: bpy.types.Object) ->
def edit_text(drawing: type[tool.Drawing], obj: bpy.types.Object) -> None:
drawing.synchronise_ifc_and_text_attributes(obj)
drawing.update_text_size_pset(obj)
drawing.update_newline_at_and_symbol(obj)
drawing.update_text_annotation_properties(obj)
drawing.disable_editing_text(obj)
@@ -440,8 +440,7 @@ def add_annotation(
drawing_tool.show_decorations()
obj = drawing_tool.create_annotation_object(drawing, object_type)
element = ifc.get_entity(obj)
# TODO: element is never None?
if not element:
if not element: # Brand new annotation
relating_type_rep = drawing_tool.get_annotation_representation(relating_type) if relating_type else None
element = drawing_tool.run_root_assign_class(
obj=obj,
+2 -19
View File
@@ -115,24 +115,9 @@ def switch_representation(
geometry: type[tool.Geometry],
obj: bpy.types.Object,
representation: ifcopenshell.entity_instance,
should_reload: bool = True,
is_global: bool = True,
should_sync_changes_first: bool = False,
apply_openings: bool = True,
) -> None:
"""Function can switch to representation that wasn't yet assigned to that object. See #2766.
:param should_sync_changes_first: sync ifc representation with current state of `obj.data`
:param should_reload: reload `obj.data` from ifc representation
:param is_global: replace mesh data for all users of `obj.data`, not just `obj`
"""
if should_sync_changes_first and ifc.is_edited(obj) and not geometry.is_box_representation(representation):
representation_id = geometry.get_representation_id(representation)
geometry.run_geometry_update_representation(obj=obj)
if not geometry.does_representation_id_exist(representation_id):
return
"""Switch obj representation (even if not yet assigned to that object. See #2766.)"""
if not geometry.get_object_data(obj) and geometry.is_text_literal(representation):
return
@@ -142,9 +127,7 @@ def switch_representation(
geometry.reimport_element_representations(obj, representation, apply_openings=apply_openings)
def get_representation_ifc_parameters(
geometry: type[tool.Geometry], obj: bpy.types.Object, should_sync_changes_first: bool = False
) -> None:
def get_representation_ifc_parameters(geometry: type[tool.Geometry], obj: bpy.types.Object) -> None:
geometry.import_representation_parameters(geometry.get_object_data(obj))
+4
View File
@@ -72,6 +72,10 @@ def assign_class(
"""
Args:
context: is not optional if `should_add_representation` is True
TODO: Do NOT use should_add_representation. Because it internally calls
geometry.add_representation which is 1,000 lines of Blender -> IFC magic.
Instead, explicitly create a representation using tool.Geometry.
"""
if ifc.get_entity(obj):
return
+2 -2
View File
@@ -394,7 +394,7 @@ class Drawing:
def sync_object_placement(cls, obj): pass
def synchronise_ifc_and_text_attributes(cls, obj): pass
def update_embedded_svg_location(cls, uri, old_location, new_location): pass
def update_newline_at_and_symbol(cls, obj): pass
def update_text_annotation_properties(cls, obj): pass
def update_text_size_pset(cls, obj): pass
@@ -1092,7 +1092,7 @@ class Type:
def get_type_occurrences(cls, element_type): pass
def has_material_usage(cls, element): pass
def run_geometry_add_representation(cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None): pass
def run_geometry_switch_representation(cls, obj=None, representation=None, should_reload=None, is_global=None): pass
def run_geometry_switch_representation(cls, obj=None, representation=None): pass
@interface
+71
View File
@@ -477,6 +477,8 @@ class Blender(bonsai.core.tool.Blender):
def ensure_bin_in_path(cls) -> None:
"""Check 'bin' folder is in PATH, if not add for this session"""
bin_dir = str(Path(__file__).parent.parent.resolve() / "libs" / "bin")
if not os.path.isdir(bin_dir):
return # Maybe the user is using a system-wide Python package. See #7157.
current_path = os.environ["PATH"]
if bin_dir not in current_path:
os.environ["PATH"] = current_path + os.pathsep + bin_dir
@@ -1859,6 +1861,75 @@ class Blender(bonsai.core.tool.Blender):
return unit_scale
@classmethod
def reset_object_visibility(cls):
override = cls.get_viewport_context()
with bpy.context.temp_override(**override):
bpy.ops.object.hide_view_clear(select=False)
@classmethod
def isolate_objects(cls, objs):
previously_selected = {o.name for o in bpy.context.selected_objects}
previously_active = bpy.context.view_layer.objects.active
override = cls.get_viewport_context()
with bpy.context.temp_override(**override):
bpy.ops.object.hide_view_clear(select=False)
bpy.ops.object.select_all(action="DESELECT")
for obj in objs:
obj.select_set(True)
with bpy.context.temp_override(**override):
bpy.ops.object.hide_view_set(unselected=True)
bpy.ops.object.select_all(action="DESELECT")
for name in previously_selected:
obj = bpy.data.objects.get(name)
if obj:
obj.select_set(True)
bpy.context.view_layer.objects.active = previously_active
@classmethod
def hide_objects(cls, objs):
previously_selected = {o.name for o in bpy.context.selected_objects}
previously_active = bpy.context.view_layer.objects.active
override = cls.get_viewport_context()
bpy.ops.object.select_all(action="DESELECT")
for obj in objs:
obj.select_set(True)
with bpy.context.temp_override(**override):
bpy.ops.object.hide_view_set(unselected=False)
for name in previously_selected:
obj = bpy.data.objects.get(name)
if obj:
obj.select_set(True)
bpy.context.view_layer.objects.active = previously_active
@classmethod
def show_objects(cls, objs):
previously_selected = {o.name for o in bpy.context.selected_objects}
previously_active = bpy.context.view_layer.objects.active
bpy.ops.object.select_all(action="DESELECT")
override = cls.get_viewport_context()
with bpy.context.temp_override(**override):
bpy.ops.object.hide_view_clear(select=True)
for obj in bpy.context.selected_objects:
if obj in objs:
obj.select_set(False)
with bpy.context.temp_override(**override):
bpy.ops.object.hide_view_set(unselected=False)
bpy.ops.object.select_all(action="DESELECT")
for name in previously_selected:
obj = bpy.data.objects.get(name)
if obj:
obj.select_set(True)
bpy.context.view_layer.objects.active = previously_active
@classmethod
def validate_shader_batch_data(cls, pos: Any, indices: Optional[Any]) -> bool:
"""Validate shader batch data.
+5
View File
@@ -65,8 +65,11 @@ class Collector(bonsai.core.tool.Collector):
collection = cls._create_project_child_collection("IfcTypeProduct")
cls.link_collection_object_safe(collection, obj)
elif element.is_a("IfcSpace"):
if tool.Geometry.is_locked(element):
tool.Geometry.lock_object(obj)
collection = cls._create_project_child_collection("IfcSpace")
cls.link_collection_object_safe(collection, obj)
obj.hide_viewport = True
elif element.is_a("IfcStructuralItem"):
collection = cls._create_project_child_collection("IfcStructuralItem")
cls.link_collection_object_safe(collection, obj)
@@ -90,6 +93,7 @@ class Collector(bonsai.core.tool.Collector):
cls.link_collection_object_safe(collection, obj)
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection)
obj.hide_viewport = True
elif (
tool.Ifc.get_schema() != "IFC2X3"
and element.is_a("IfcSpatialElement")
@@ -101,6 +105,7 @@ class Collector(bonsai.core.tool.Collector):
cls.link_collection_object_safe(collection, obj)
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection)
obj.hide_viewport = True
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
if collection := cls._create_own_collection(obj):
cls.link_collection_object_safe(collection, obj)
+46 -49
View File
@@ -820,7 +820,11 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def import_annotations_in_group(cls, group: ifcopenshell.entity_instance) -> None:
elements = set(
[e for e in cls.get_group_elements(group) if e.is_a("IfcAnnotation") and e.ObjectType != "DRAWING"]
[
e
for e in cls.get_group_elements(group)
if e.is_a("IfcAnnotation") and e.ObjectType != "DRAWING" and not tool.Ifc.get_object(e)
]
)
logger = logging.getLogger("ImportIFC")
ifc_import_settings = bonsai.bim.import_ifc.IfcImportSettings.factory(bpy.context, None, logger)
@@ -1098,6 +1102,8 @@ class Drawing(bonsai.core.tool.Drawing):
props.font_size = str(text_data["FontSize"])
props.newline_at = text_data["Newline_At"]
props.set_symbol(text_data["Symbol"])
props.reverse_list = text_data["Reverse_List"]
props.list_separator = text_data["List_Separator"]
@classmethod
def import_assigned_product(cls, obj: bpy.types.Object) -> None:
@@ -1172,9 +1178,6 @@ class Drawing(bonsai.core.tool.Drawing):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
@classmethod
@@ -1235,22 +1238,25 @@ class Drawing(bonsai.core.tool.Drawing):
)
@classmethod
def update_newline_at_and_symbol(cls, obj: bpy.types.Object) -> None:
def update_text_annotation_properties(cls, obj: bpy.types.Object) -> None:
"""Update all EPset_Annotation properties from the text props"""
props = cls.get_text_props(obj)
element = tool.Ifc.get_entity(obj)
assert element
newline_at = int(props.newline_at)
symbol = props.get_symbol()
ifc_file = tool.Ifc.get()
pset = tool.Pset.get_element_pset(element, "EPset_Annotation")
if not pset:
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="EPset_Annotation")
ifcopenshell.api.pset.edit_pset(
ifc_file,
pset=pset,
properties={
"Newline_At": newline_at,
"Symbol": symbol,
"Newline_At": int(props.newline_at),
"Symbol": props.get_symbol(),
"Reverse_List": props.reverse_list,
"List_Separator": props.list_separator or "",
},
)
@@ -1461,10 +1467,8 @@ class Drawing(bonsai.core.tool.Drawing):
def ensure_referenced_drawing_obj_exists(drawing: ifcopenshell.entity_instance):
obj = tool.Ifc.get_object(drawing)
if obj is None:
# Annotations in that drawing are lazy loaded as needed
obj = cls.import_drawing(drawing)
# there is no need for other drawing annotations in that case
# but we import them anyway so we won't break that drawing activation
cls.import_annotations_in_group(cls.get_drawing_group(drawing))
tool.Blender.get_layer_collection(obj.users_collection[0]).hide_viewport = True
target_view = ifcopenshell.util.element.get_pset(reference_element, "EPset_Drawing", "TargetView")
@@ -1548,12 +1552,10 @@ class Drawing(bonsai.core.tool.Drawing):
context=context,
ifc_representation_class=None,
)
if representation := ifcopenshell.util.representation.get_representation(element, context):
cls.reload_representation(obj=obj, representation=representation)
bpy.data.curves.remove(data)
assert element
representation = ifcopenshell.util.representation.get_representation(element, context)
assert representation
obj.data["ios_edges_item_ids"] = (representation.Items[0].id(),)
return element
@classmethod
@@ -1641,6 +1643,8 @@ class Drawing(bonsai.core.tool.Drawing):
context=context,
ifc_representation_class=None,
)
if representation := ifcopenshell.util.representation.get_representation(element, context):
cls.reload_representation(obj=obj, representation=representation)
return element
@classmethod
@@ -1678,6 +1682,8 @@ class Drawing(bonsai.core.tool.Drawing):
context=context,
ifc_representation_class=None,
)
if representation := ifcopenshell.util.representation.get_representation(element, context):
cls.reload_representation(obj=obj, representation=representation)
return element
@classmethod
@@ -1778,6 +1784,8 @@ class Drawing(bonsai.core.tool.Drawing):
context=context,
ifc_representation_class=None,
)
if representation := ifcopenshell.util.representation.get_representation(element, context):
cls.reload_representation(obj=obj, representation=representation)
return element
@classmethod
@@ -1834,9 +1842,17 @@ class Drawing(bonsai.core.tool.Drawing):
return bool(a_tree.overlap(b_tree))
@classmethod
def replace_text_literal_variables(cls, text: str, product: Optional[ifcopenshell.entity_instance] = None) -> str:
def replace_text_literal_variables(
cls,
text: str,
product: Optional[ifcopenshell.entity_instance] = None,
reverse_list: bool = False,
list_separator: str = ", ",
) -> str:
if not product:
return text
if list_separator:
list_separator = list_separator.encode().decode("unicode_escape")
for command in re.findall("``.*?``", text):
original_command = command
@@ -1845,13 +1861,14 @@ class Drawing(bonsai.core.tool.Drawing):
value = '"' + str(value).replace('"', '\\"') + '"'
command = command.replace(variable, value)
text = text.replace(original_command, ifcopenshell.util.selector.format(command[2:-2]))
for variable in re.findall("{{.*?}}", text):
value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2])
if isinstance(value, (list, tuple)):
value = ", ".join(str(v) for v in value)
if reverse_list:
value = list_separator.join(str(v) for v in reversed(value))
else:
value = list_separator.join(str(v) for v in value)
text = text.replace(variable, str(value))
return text
@classmethod
@@ -2172,12 +2189,10 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def activate_drawing(cls, camera: bpy.types.Object) -> None:
selected_objects_before = bpy.context.selected_objects
non_ifc_objects_hide = {o: o.hide_get() for o in bpy.context.view_layer.objects if not tool.Ifc.get_entity(o)}
# Sync viewport objects visibility with selectors from EPset_Drawing/Include and /Exclude
drawing = tool.Ifc.get_entity(camera)
assert drawing and isinstance(camera.data, bpy.types.Camera)
cls.import_annotations_in_group(cls.get_drawing_group(drawing))
filtered_elements = cls.get_drawing_elements(drawing) | cls.get_drawing_spaces(drawing)
filtered_elements.add(drawing)
@@ -2185,13 +2200,7 @@ class Drawing(bonsai.core.tool.Drawing):
target_view = cls.get_drawing_target_view(drawing)
subcontexts = cls.get_drawing_subcontexts(target_view)
# Hide everything first, then selectively show. This is significantly faster.
with bpy.context.temp_override(**tool.Blender.get_viewport_context()):
bpy.ops.object.hide_view_set(unselected=False)
bpy.ops.object.hide_view_set(unselected=True)
# switch representations and hide elements without representations
element_obj_names = set()
# Switch representations
for element in filtered_elements:
obj = tool.Ifc.get_object(element)
if not obj:
@@ -2215,34 +2224,22 @@ class Drawing(bonsai.core.tool.Drawing):
tool.Geometry,
obj=obj,
representation=priority_representation,
should_reload=False,
is_global=True,
should_sync_changes_first=True,
)
has_context = True
break
# Don't hide IfcAnnotations or Aggregates as some of them might exist without representations
if has_context or element.is_a("IfcAnnotation") or element.IsDecomposedBy:
element_obj_names.add(obj.name)
# Note that render visibility is only set on drawing generation time for speed.
visible_objects = []
for obj in bpy.context.view_layer.objects:
if obj.name in element_obj_names:
obj.hide_set(False)
obj.hide_render = False
continue
if (hide := non_ifc_objects_hide.get(obj)) is not None:
obj.hide_set(hide)
obj.hide_render = hide
if element := tool.Ifc.get_entity(obj):
if element in filtered_elements:
visible_objects.append(obj)
else:
obj.hide_render = True
if obj.hide_get() is False:
visible_objects.append(obj)
tool.Blender.isolate_objects(visible_objects)
cls.import_camera_props(drawing, camera.data)
for obj in selected_objects_before:
obj.select_set(True)
@classmethod
def get_elements_in_camera_view(
cls, camera: bpy.types.Object, objs: list[bpy.types.Object]
-6
View File
@@ -1350,9 +1350,6 @@ class Geometry(bonsai.core.tool.Geometry):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=True,
)
@@ -1388,9 +1385,6 @@ class Geometry(bonsai.core.tool.Geometry):
tool.Geometry,
obj=obj,
representation=new_representation,
should_reload=False,
should_sync_changes_first=False,
is_global=True,
)
@classmethod
+1 -1
View File
@@ -248,7 +248,7 @@ class Ifc(bonsai.core.tool.Ifc):
Marking object as edited is an optimization mechanism - instead of saving
changed geometry to IFC, we mark it as changed and then it's saved later
(typically during project save or switch_representation(should_sync_changes_first=True)).
(typically during project save).
Other caveat of using edited objects is that it won't have an effect for objects with openings,
since we can't deduce non-openings representation from edited representation with openings.
-12
View File
@@ -1091,9 +1091,6 @@ class Model(bonsai.core.tool.Model):
tool.Geometry,
obj=obj,
representation=new_representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
@classmethod
@@ -1306,9 +1303,6 @@ class Model(bonsai.core.tool.Model):
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
@classmethod
@@ -1652,9 +1646,6 @@ class Model(bonsai.core.tool.Model):
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=True,
)
@@ -2468,9 +2459,6 @@ class Model(bonsai.core.tool.Model):
tool.Geometry,
obj=obj,
representation=rep,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
tool.Geometry.record_object_materials(obj)
-1
View File
@@ -155,7 +155,6 @@ class Raycast(bonsai.core.tool.Raycast):
@classmethod
def point_is_visible_in_clipping_plane(cls, vertex):
normals = tool.Project.get_clipping_planes_normals()
print("NORMALS", normals)
if not normals:
return True
for normal in normals:
-3
View File
@@ -326,9 +326,6 @@ class Root(bonsai.core.tool.Root):
tool.Geometry,
obj=voided_obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
@classmethod
+16 -9
View File
@@ -512,14 +512,12 @@ class Sequence(bonsai.core.tool.Sequence):
@classmethod
def get_task_inputs(cls, task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
props = cls.get_work_schedule_props()
is_deep = props.show_nested_inputs
return ifcopenshell.util.sequence.get_task_inputs(task, is_deep)
return ifcopenshell.util.sequence.get_task_inputs(task, is_recursive=props.show_nested_inputs)
@classmethod
def get_task_outputs(cls, task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
props = cls.get_work_schedule_props()
is_deep = props.show_nested_outputs
return ifcopenshell.util.sequence.get_task_outputs(task, is_deep)
return ifcopenshell.util.sequence.get_task_outputs(task, is_recursive=props.show_nested_outputs)
@classmethod
def are_entities_same_class(cls, entities: list[ifcopenshell.entity_instance]) -> bool:
@@ -540,8 +538,7 @@ class Sequence(bonsai.core.tool.Sequence):
if not task:
return
props = cls.get_work_schedule_props()
is_deep = props.show_nested_resources
return ifcopenshell.util.sequence.get_task_resources(task, is_deep)
return ifcopenshell.util.sequence.get_task_resources(task, props.show_nested_resources)
@classmethod
def load_task_inputs(cls, inputs: list[ifcopenshell.entity_instance]) -> None:
@@ -1582,7 +1579,7 @@ class Sequence(bonsai.core.tool.Sequence):
@classmethod
def create_new_task_json(cls, task, json, type_map=None, baseline_schedule=None):
task_time = task.TaskTime
resources = ifcopenshell.util.sequence.get_task_resources(task, is_deep=False)
resources = ifcopenshell.util.sequence.get_task_resources(task, is_recursive=False)
string_resources = ""
resources_usage = ""
@@ -1689,8 +1686,8 @@ class Sequence(bonsai.core.tool.Sequence):
) -> list[ifcopenshell.entity_instance]:
products = []
for task in ifcopenshell.util.sequence.get_root_tasks(work_schedule):
products.extend(ifcopenshell.util.sequence.get_task_inputs(task, is_deep=True))
products.extend(ifcopenshell.util.sequence.get_task_outputs(task, is_deep=True))
products.extend(ifcopenshell.util.sequence.get_task_inputs(task, is_recursive=True))
products.extend(ifcopenshell.util.sequence.get_task_outputs(task, is_recursive=True))
return products
@classmethod
@@ -1789,6 +1786,16 @@ class Sequence(bonsai.core.tool.Sequence):
cls.load_task_outputs(outputs)
cls.load_task_resources(task)
@classmethod
def select_active_task_elements(cls, task):
if not task:
return
bpy.ops.object.select_all(action="DESELECT")
props = cls.get_work_schedule_props()
for element in cls.get_task_inputs(task) | cls.get_task_outputs(task):
if obj := tool.Ifc.get_object(element):
obj.select_set(True)
@classmethod
def refresh_task_resources(cls):
task = cls.get_highlighted_task()
+22 -4
View File
@@ -175,7 +175,6 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def select_products(cls, products: Iterable[ifcopenshell.entity_instance], unhide: bool = False) -> None:
bpy.ops.object.select_all(action="DESELECT")
for product in products:
obj = tool.Ifc.get_object(product)
if obj and bpy.context.view_layer.objects.get(obj.name):
@@ -1198,11 +1197,30 @@ class Spatial(bonsai.core.tool.Spatial):
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
@classmethod
def set_space_visibility(cls, is_visible: bool) -> None:
if tool.Ifc.get().schema == "IFC2X3":
elements = tool.Ifc.get().by_type("IfcSpatialStructureElement")
else:
elements = tool.Ifc.get().by_type("IfcSpatialElement")
for element in elements:
if obj := tool.Ifc.get_object(element):
if obj.hide_viewport is True and is_visible:
obj.hide_viewport = False
elif obj.hide_viewport is False and not is_visible:
obj.hide_viewport = True
@classmethod
def set_grid_visibility(cls, is_visible: bool) -> None:
for element in tool.Ifc.get().by_type("IfcGrid") + tool.Ifc.get().by_type("IfcGridAxis"):
if obj := tool.Ifc.get_object(element):
if obj.hide_viewport is True and is_visible:
obj.hide_viewport = False
elif obj.hide_viewport is False and not is_visible:
obj.hide_viewport = True
@classmethod
def toggle_spaces_visibility_wired_and_textured(cls, spaces: list[ifcopenshell.entity_instance]) -> None:
first_obj = tool.Ifc.get_object(spaces[0])
-5
View File
@@ -131,15 +131,10 @@ class Type(bonsai.core.tool.Type):
cls,
obj: bpy.types.Object,
representation: ifcopenshell.entity_instance,
should_reload: bool = False,
is_global: bool = False,
) -> None:
return bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=should_reload,
is_global=is_global,
should_sync_changes_first=False,
)
+10 -2
View File
@@ -41,6 +41,7 @@ import socketio
import threading
import queue
import json
import platformdirs
from time import sleep
from pathlib import Path
import bonsai.core.sequence
@@ -65,6 +66,13 @@ IFC_TASK_ATTRIBUTE_MAP = {
}
def get_pid_file_path():
"""Get the path to the PID file in the user's cache directory."""
cache_dir = Path(platformdirs.user_cache_dir("bonsai"))
cache_dir.mkdir(parents=True, exist_ok=True)
return cache_dir / "running_pid.json"
class Web(bonsai.core.tool.Web):
@classmethod
def get_web_props(cls) -> WebProperties:
@@ -213,7 +221,7 @@ class Web(bonsai.core.tool.Web):
cls.disconnect_websocket_server()
# sleep(0.5)
pid_file = tool.Blender.get_data_dir_path("webui") / "running_pid.json"
pid_file = get_pid_file_path()
with open(pid_file, "r") as f:
pids = json.load(f)
@@ -250,7 +258,7 @@ class Web(bonsai.core.tool.Web):
while True:
if time.time() - start > max_time:
return False
pid_file = tool.Blender.get_data_dir_path("webui") / "running_pid.json"
pid_file = get_pid_file_path()
try:
with open(pid_file, "r") as f:
data = json.load(f)
@@ -174,6 +174,14 @@ If there are changes to the IfcOpenShell binaries, you may replace the two
``*ifcopenshell_wrapper*`` files with new ones downloaded from the automated
`IfcOpenShell builds directory <https://builds.ifcopenshell.org/>`__.
If you wish to run the IfcTester webapp, you will also need to have ``npm`` and
setup IfcTester:
.. code-block:: bash
cd src/ifctester
make webapp-prepare
.. seealso::
There is a `useful Blender Addon
+1 -1
View File
@@ -37,7 +37,7 @@ class TestEditText:
def test_run(self, drawing):
drawing.synchronise_ifc_and_text_attributes("obj").should_be_called()
drawing.update_text_size_pset("obj").should_be_called()
drawing.update_newline_at_and_symbol("obj").should_be_called()
drawing.update_text_annotation_properties("obj").should_be_called()
drawing.disable_editing_text("obj").should_be_called()
subject.edit_text(drawing, obj="obj")
+1 -10
View File
@@ -203,9 +203,6 @@ class TestSwitchRepresentation:
geometry,
obj="obj",
representation="mapped_rep",
should_reload=True,
is_global=True,
should_sync_changes_first=True,
apply_openings=True,
)
@@ -222,9 +219,6 @@ class TestSwitchRepresentation:
geometry,
obj="obj",
representation="mapped_rep",
should_reload=False,
is_global=False,
should_sync_changes_first=True,
)
def test_not_switching_if_an_updated_representation_is_the_same_one_we_were_going_to_switch_to(self, ifc, geometry):
@@ -238,9 +232,6 @@ class TestSwitchRepresentation:
geometry,
obj="obj",
representation="mapped_rep",
should_reload=False,
is_global=False,
should_sync_changes_first=True,
)
@@ -248,7 +239,7 @@ class TestGetRepresentationIfcParameters:
def test_run(self, geometry):
geometry.get_object_data("obj").should_be_called().will_return("data")
geometry.import_representation_parameters("data").should_be_called()
subject.get_representation_ifc_parameters(geometry, obj="obj", should_sync_changes_first=False)
subject.get_representation_ifc_parameters(geometry, obj="obj")
class TestRemoveRepresentation:
+5 -9
View File
@@ -36,6 +36,7 @@ from typing_extensions import NotRequired
class CsvHeader(TypedDict):
Index: int
Name: int
Description: NotRequired[str]
Unit: int
Identification: NotRequired[int]
Value: NotRequired[int]
@@ -80,6 +81,7 @@ class CostItem(TypedDict):
Identification: Union[str, None]
Name: Union[str, None]
Description: Union[str, None]
Unit: Union[str, None]
CostValues: Union[dict[str, float], float, None]
@@ -183,15 +185,6 @@ class Csv2Ifc:
available_fields = set(self.headers.keys())
missing_fields = mandatory_fields - available_fields
# TODO: 25-04-17 Deprecated 'Description' argument, should fully remove later.
if missing_fields and "Name" in missing_fields and "Description" in available_fields:
print(
"WARNING. 'Description' column is deprecated and should be renamed to 'Name'. It will be deprecated soon completely."
)
self.headers["Name"] = self.headers["Description"]
del self.headers["Description"]
missing_fields.remove("Name")
if missing_fields:
raise Exception(f"Missing mandatory fields in CSV header: {', '.join(missing_fields)}")
@@ -212,6 +205,7 @@ class Csv2Ifc:
def get_row_cost_data(self, row: list[str]) -> CostItem:
name = row[self.headers["Name"]]
description = row[self.headers["Description"]] if "Description" in self.headers else None
identification = row[self.headers["Identification"]] if "Identification" in self.headers else None
quantity = row[(self.headers["Quantity"])] if "Quantity" in self.headers else None
unit = row[self.headers["Unit"]]
@@ -241,6 +235,7 @@ class Csv2Ifc:
return {
"Identification": str(identification) if identification else None,
"Name": str(name) if name else None,
"Description": str(description) if description else None,
"Unit": str(unit) if unit else None,
"CostValues": cost_values,
"Quantity": float(quantity) if quantity else None,
@@ -276,6 +271,7 @@ class Csv2Ifc:
cost_item["ifc"] = ifcopenshell.api.cost.add_cost_item(self.file, cost_item=parent)
cost_item["ifc"].Name = cost_item["Name"]
cost_item["ifc"].Description = cost_item["Description"]
cost_item["ifc"].Identification = cost_item["Identification"]
cost_values = cost_item["CostValues"]
+8
View File
@@ -7,6 +7,14 @@
using namespace ifcopenshell::geometry;
const char* ifcopenshell::not_implemented_error::what() const noexcept {
return "Not implemented.";
}
const char* ifcopenshell::not_supported_error::what() const noexcept {
return "Not supported.";
}
bool ifcopenshell::geometry::kernels::AbstractKernel::convert(const taxonomy::ptr item, IfcGeom::ConversionResults& results) {
if (settings_.get<settings::CacheShapes>().get()) {
auto it = cache_.find(item);
+13 -8
View File
@@ -36,21 +36,26 @@ inline static bool ALMOST_THE_SAME(const T& a, const T& b, double tolerance = AL
}
namespace ifcopenshell {
#if defined(_MSC_VER)
#pragma warning(push)
#pragma warning(disable: 4275)
#endif
class IFC_GEOM_API not_implemented_error : public std::exception {
public:
const char* what() const noexcept override {
return "Not implemented.";
}
const char* what() const noexcept override;
};
class IFC_GEOM_API not_supported_error : public std::exception {
public:
const char* what() const noexcept override {
return "Not supported.";
}
const char* what() const noexcept override;
};
#if defined(_MSC_VER)
#pragma warning(pop)
#endif
namespace geometry { namespace kernels {
class IFC_GEOM_API AbstractKernel {
+1
View File
@@ -282,6 +282,7 @@ namespace IfcGeom {
virtual ConversionResultShape* halfspaces() = 0;
virtual ConversionResultShape* box() = 0;
virtual ConversionResultShape* solid() = 0;
virtual ConversionResultShape* wrap_in_compound() = 0;
virtual std::vector<ConversionResultShape*> vertices() = 0;
virtual std::vector<ConversionResultShape*> edges() = 0;
+4
View File
@@ -94,3 +94,7 @@ std::istream& ifcopenshell::geometry::settings::operator>>(std::istream& in, Tri
}
return in;
}
IfcGeom::geometry_exception::~geometry_exception() = default;
IfcGeom::too_many_faces_exception::~too_many_faces_exception() = default;
+15 -1
View File
@@ -84,8 +84,9 @@ namespace ifcopenshell {
}
if constexpr (HasDefault<Derived>()) {
return Derived::defaultvalue;
} else {
throw std::runtime_error("Setting not set");
}
throw std::runtime_error("Setting not set");
}
}
@@ -654,6 +655,12 @@ namespace ifcopenshell {
// @todo find a place
namespace IfcGeom {
#if defined(_MSC_VER)
#pragma warning(push)
#pragma warning(disable: 4275)
#endif
class IFC_GEOM_API geometry_exception : public std::runtime_error {
protected:
std::string message;
@@ -661,12 +668,19 @@ namespace IfcGeom {
geometry_exception(const std::string& m)
: std::runtime_error(m)
{}
~geometry_exception() override;
};
class IFC_GEOM_API too_many_faces_exception : public geometry_exception {
public:
too_many_faces_exception()
: geometry_exception("Too many faces for operation") {}
~too_many_faces_exception() override;
};
}
#if defined(_MSC_VER)
#pragma warning(pop)
#endif
#endif
+1 -1
View File
@@ -68,7 +68,7 @@ IfcGeom::ConversionResultShape* IfcGeom::Representation::BRep::as_compound(bool
delete accum;
accum = n;
} else {
accum = s;
accum = s->wrap_in_compound();
}
}
@@ -610,6 +610,11 @@ ConversionResultShape * ifcopenshell::geometry::CgalShape::box()
throw std::runtime_error("Not implemented");
}
ConversionResultShape* ifcopenshell::geometry::CgalShape::wrap_in_compound()
{
return new CgalShape(poly(), convex_tag_);
}
std::vector<ConversionResultShape*> ifcopenshell::geometry::CgalShape::vertices()
{
// @todo this is ridiculous
@@ -1001,4 +1006,10 @@ void ifcopenshell::geometry::CgalShapeHalfSpaceDecomposition::map(const std::vec
shape_ = std::move(nw);
}
ConversionResultShape* ifcopenshell::geometry::CgalShapeHalfSpaceDecomposition::wrap_in_compound()
{
throw std::runtime_error("Not implemented");
}
#endif
@@ -244,6 +244,7 @@ namespace ifcopenshell { namespace geometry {
virtual ConversionResultShape* halfspaces();
virtual ConversionResultShape* solid();
virtual ConversionResultShape* box();
virtual ConversionResultShape* wrap_in_compound();
virtual std::vector<ConversionResultShape*> vertices();
virtual std::vector<ConversionResultShape*> edges();
@@ -312,6 +313,7 @@ namespace ifcopenshell { namespace geometry {
virtual ConversionResultShape* halfspaces();
virtual ConversionResultShape* solid();
virtual ConversionResultShape* box();
virtual ConversionResultShape* wrap_in_compound();
virtual std::vector<ConversionResultShape*> vertices();
virtual std::vector<ConversionResultShape*> edges();
@@ -484,6 +484,15 @@ ConversionResultShape * ifcopenshell::geometry::OpenCascadeShape::box()
throw std::runtime_error("Not implemented");
}
ConversionResultShape* ifcopenshell::geometry::OpenCascadeShape::wrap_in_compound()
{
TopoDS_Compound compound;
BRep_Builder builder;
builder.MakeCompound(compound);
builder.Add(compound, shape_);
return new OpenCascadeShape(std::move(compound));
}
std::vector<ConversionResultShape*> ifcopenshell::geometry::OpenCascadeShape::vertices()
{
TopTools_IndexedMapOfShape map;
@@ -556,7 +565,9 @@ ConversionResultShape* ifcopenshell::geometry::OpenCascadeShape::concat(Conversi
auto& left = shape_;
auto& right = ((ifcopenshell::geometry::OpenCascadeShape*)other)->shape_;
if (left.ShapeType() == TopAbs_COMPOUND) {
// This reads a bit strange, but we want to specifically avoid compounds of faces that are
// the result of shell instances that are not sewn into a shell (yet).
if (left.ShapeType() == TopAbs_COMPOUND && !IfcGeom::util::is_compound_of_faces(left)) {
compound = TopoDS::Compound(left);
} else {
builder.MakeCompound(compound);
@@ -90,6 +90,7 @@ namespace ifcopenshell {
virtual ConversionResultShape* halfspaces();
virtual ConversionResultShape* solid();
virtual ConversionResultShape* box();
virtual ConversionResultShape* wrap_in_compound();
virtual std::vector<ConversionResultShape*> vertices();
virtual std::vector<ConversionResultShape*> edges();
@@ -607,7 +607,7 @@ gp_Pnt IfcGeom::util::point_above_plane(const gp_Pln& pln, bool agree) {
}
}
bool IfcGeom::util::is_compound(const TopoDS_Shape& shape) {
bool IfcGeom::util::is_compound_of_faces(const TopoDS_Shape& shape) {
bool has_solids = TopExp_Explorer(shape, TopAbs_SOLID).More() != 0;
bool has_shells = TopExp_Explorer(shape, TopAbs_SHELL).More() != 0;
bool has_compounds = TopExp_Explorer(shape, TopAbs_COMPOUND).More() != 0;
+1 -1
View File
@@ -51,7 +51,7 @@ namespace IfcGeom {
IFC_GEOMLIBRARY_API bool create_solid_from_compound(const TopoDS_Shape& compound, TopoDS_Shape& solid, double tol);
IFC_GEOMLIBRARY_API bool shape_to_face_list(const TopoDS_Shape& s, TopTools_ListOfShape& li);
IFC_GEOMLIBRARY_API bool create_solid_from_faces(const TopTools_ListOfShape& face_list, TopoDS_Shape& solid, double tol, bool force_sewing = false);
IFC_GEOMLIBRARY_API bool is_compound(const TopoDS_Shape& shape);
IFC_GEOMLIBRARY_API bool is_compound_of_faces(const TopoDS_Shape& shape);
IFC_GEOMLIBRARY_API bool is_convex(const TopoDS_Wire& wire, double tol);
IFC_GEOMLIBRARY_API TopoDS_Shape halfspace_from_plane(const gp_Pln& pln, const gp_Pnt& cent);
IFC_GEOMLIBRARY_API gp_Pln plane_from_face(const TopoDS_Face& face);
@@ -1431,7 +1431,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
}
TopoDS_Shape IfcGeom::util::ensure_fit_for_subtraction(const TopoDS_Shape& shape, double tol) {
const bool is_comp = is_compound(shape);
const bool is_comp = is_compound_of_faces(shape);
if (!is_comp) {
return shape;
}
+2
View File
@@ -223,6 +223,8 @@ namespace {
};
}
ifcopenshell::geometry::taxonomy::topology_error::~topology_error() = default;
bool ifcopenshell::geometry::taxonomy::less(item::const_ptr a, item::const_ptr b) {
if (a == b) {
return false;
+10
View File
@@ -85,14 +85,24 @@ typedef std::uniqe_ptr<const item> ptr;
#define DECLARE_PTR(item) \
typedef item* ptr; \
typedef item const* ptr;
#endif
#if defined(_MSC_VER)
#pragma warning(push)
#pragma warning(disable: 4275)
#endif
class IFC_GEOM_API topology_error : public std::runtime_error {
public:
topology_error() : std::runtime_error("Generic topology error") {}
topology_error(const char* const s) : std::runtime_error(s) {}
~topology_error() override;
};
#if defined(_MSC_VER)
#pragma warning(pop)
#endif
// Implementer note: If you add a new item type, be sure to do the following
// 1) Add a new kind to this list
// 2) Update the values array used by kind_to_string()
@@ -143,7 +143,7 @@ class Usecase:
return next(e for e in self.file.get_inverse(self.task_time) if e.is_a("IfcTask"))
def handle_resource_calculation(self):
resources = ifcopenshell.util.sequence.get_task_resources(self.task, is_deep=False)
resources = ifcopenshell.util.sequence.get_task_resources(self.task, is_recursive=False)
for resource in resources:
if ifcopenshell.util.constraint.is_attribute_locked(resource, "Usage.ScheduleWork"):
ifcopenshell.api.resource.calculate_resource_usage(self.file, resource=resource)
+223 -43
View File
@@ -18,14 +18,18 @@
"""2D drawing generation and serialisation"""
from functools import reduce
import itertools
import math
import json
import functools
import re
import operator
import warnings
import shapely
import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.util.element
import ifcopenshell.util.selector
from xml.dom.minidom import parseString
from dataclasses import dataclass, fields, field
@@ -53,8 +57,9 @@ class draw_settings:
door_arcs: bool = False
subtract_before_hlr: bool = False
cache: bool = False
css: bool = True
css: str = ""
storey_heights: str = "none"
storey_filter: str = ""
include_entities: str = ""
exclude_entities: str = "IfcOpeningElement"
drawing_guid: str = field(
@@ -75,9 +80,24 @@ class draw_settings:
include_curves: bool = False
unify_inputs: bool = True
arrange_spaces: bool = False
arrange_zones: bool = False
zone_filter: str = ""
zone_label: str = ""
mirror_y: bool = False
class type_unsafe_value:
def __init__(self, value):
self.value = value
def __eq__(self, other):
try:
v = type(other)(self.value)
except:
return False
return v == other
def main(
settings: draw_settings,
files: list[ifcopenshell.file],
@@ -86,6 +106,13 @@ def main(
progress_function: Callable = DO_NOTHING,
):
def by_guid(g):
for f in files:
try:
return f.by_guid(g)
except:
pass
geom_settings = ifcopenshell.geom.settings(
# when not doing booleans, proper solids from shells isn't a requirement
REORIENT_SHELLS=settings.subtract_before_hlr,
@@ -106,11 +133,26 @@ def main(
elif settings.exclude_entities:
iterator_kwargs["exclude"] = settings.exclude_entities.split(",")
def yield_parents(el):
yield el
if par := ifcopenshell.util.element.get_parent(el):
yield from yield_parents(par)
def has_selected_parent(el):
return any(settings.storey_filter in (x.Name or x.GlobalId) for x in yield_parents(el))
def create_iter(f):
if settings.storey_filter and iterator_kwargs.get("include"):
iterator_kwargs["include"] = list(
filter(has_selected_parent, sum((f.by_type(x) for x in iterator_kwargs["include"]), []))
)
return ifcopenshell.geom.iterator(geom_settings, f, **iterator_kwargs)
# We have to keep the iterator in memory because otherwise
# the styles are cleared up.
iterators = list(
map(
functools.partial(ifcopenshell.geom.iterator, geom_settings, **iterator_kwargs),
create_iter,
files,
)
)
@@ -135,14 +177,7 @@ def main(
# it's up to user to keep them valid for the provided projects.
if settings.drawing_guid or settings.drawing_object_type:
if settings.drawing_guid:
found_guid = False
for f in files:
try:
f.by_guid(settings.drawing_guid)
found_guid = True
except:
pass
if not found_guid:
if not by_guid(settings.drawing_guid):
raise ValueError(f"Unable to find guid {settings.drawing_guid!r}")
sr.setElevationRefGuid(settings.drawing_guid)
elif settings.drawing_object_type:
@@ -163,7 +198,7 @@ def main(
sr.setPrintSpaceNames(settings.space_names)
sr.setPrintSpaceAreas(settings.space_areas)
sr.setDrawDoorArcs(settings.door_arcs)
sr.setNoCSS(not settings.css)
sr.setNoCSS(not not settings.css)
if settings.subtract_before_hlr:
sr.setSubtractionSettings(W.ALWAYS)
@@ -421,26 +456,25 @@ def main(
# This generally shouldn't happen
g1.appendChild(g2)
if settings.arrange_spaces:
if settings.arrange_spaces or settings.arrange_zones:
if settings.storey_filter:
# delete storey groups not selected by filter
# sometimes happens in case of elements protruding multiple stories
# are assigned wrongly in the decomposition tree
for rg in (g for g in yield_groups(svg1) if g.parentNode.tagName == "svg"):
storey_guid = ifcopenshell.guid.compress(rg.attributes["id"].value.split("-", 1)[1].replace("-", ""))
storey = by_guid(storey_guid)
if settings.storey_filter not in (storey.Name or storey.GlobalId):
rg.parentNode.removeChild(rg)
root_groups = [g for g in yield_groups(svg1) if g.parentNode.tagName == "svg"]
ref_node = root_groups[-1].nextSibling
parent = root_groups[0].parentNode
zone_groups = []
for i in range(len(root_groups)):
for j in range(len(root_groups)):
if i != j:
parent.removeChild(root_groups[j])
for grp in root_groups:
path_objects = [p for p in yield_groups(grp, "path") if "IfcSpace" in p.parentNode.getAttribute("class")]
# wasteful and looses data for unknown reasons
# svg_contents = svg1.toxml()
# polies = W.svg_to_polygons(svg_contents, "IfcSpace")
polies = [
p.getAttribute("d")
for p in yield_groups(svg1, "path")
if "IfcSpace" in p.parentNode.getAttribute("class")
]
polies = [p.getAttribute("d") for p in path_objects]
def break_at_second(char, s, offset=1):
i = s.find(char, offset)
@@ -450,39 +484,185 @@ def main(
warnings.warn("Polygons with holes are not supported")
return s[0:i]
polies = [
[[*map(float, s[1:].split(","))] for s in break_at_second("M", d).split(" ")[:-1]] for d in polies
]
def svg_to_coordlist(str):
return [[*map(float, s[1:].split(","))] for s in break_at_second("M", str).split(" ")[:-1]]
polies = [svg_to_coordlist(d) for d in polies]
def create_poly(b):
p = ifcopenshell.ifcopenshell_wrapper.polygon_2()
p.boundary = b
return p
polies = [create_poly(p) for p in polies]
def min_bound_extent(p):
def min_bound_extent_p2(p):
arr = numpy.array(p.boundary)
return (arr.max(axis=0) - arr.min(axis=0)).min() > 0.5
polies = type(polies)(filter(min_bound_extent, polies))
def min_bound_extent_li(p):
arr = numpy.array(p)
return (arr.max(axis=0) - arr.min(axis=0)).min() > 0.5
def polygon_to_svg_path(doc, polygon: shapely.Polygon) -> str:
def ring_to_path(coords):
coords = list(coords)[:-1]
return "M " + " L ".join(f"{x},{y}" for x, y in coords) + " Z"
rings = [polygon.exterior, *polygon.interiors]
d = " ".join(ring_to_path(r.coords) for r in rings)
path = doc.createElement("path")
path.setAttribute("d", d)
path.setAttribute("fill-rule", "evenodd")
return path
included = list(map(min_bound_extent_li, polies))
polies = [p for p, incl in zip(polies, included) if incl]
path_objects = [p for p, incl in zip(path_objects, included) if incl]
section_polies = list(map(shapely.Polygon, polies))
polies = [create_poly(p) for p in polies]
def has_relevant_zone(i):
inst = by_guid(path_objects[i].parentNode.attributes["ifc:guid"].value)
groups = [rel.RelatingGroup for rel in inst.HasAssignments if rel.is_a("IfcRelAssignsToGroup")]
if settings.zone_filter:
query, val = settings.zone_filter.rsplit("=", 1)
# cast to type encountered in pset prop
val = type_unsafe_value(val)
groups = [g for g in groups if ifcopenshell.util.selector.get_element_value(g, query) == val]
return len(groups) > 0
if settings.arrange_zones:
path_objects, section_polies, polies = zip(
*(tup for i, tup in enumerate(zip(path_objects, section_polies, polies)) if has_relevant_zone(i))
)
arranged = W.arrange_polygons(polies)
svg_data_3 = W.polygons_to_svg(arranged, False)
dom3 = parseString(svg_data_3)
svg3 = dom3.childNodes[0]
g3 = next(yield_groups(svg3))
arranged_polies = []
for p in g3.getElementsByTagName("path"):
p.setAttribute("style", "fill: none; stroke: black; stroke-width: 0.2")
zone_groups.append(g3)
parent.removeChild(root_groups[i])
for j in range(len(root_groups)):
parent.insertBefore(root_groups[j], ref_node)
# arranged path
sp = shapely.Polygon(svg_to_coordlist(p.getAttribute("d")))
arranged_polies.append(sp)
# Mapping of arrange_poly_idx (stored as list) -> instance
# Obtained by point containment of sectionpoly in arranged
mapping = [None] * len(arranged_polies)
for i, p in enumerate(section_polies):
g = by_guid(path_objects[i].parentNode.attributes["ifc:guid"].value)
contains = [ap.contains(p.representative_point()) for ap in arranged_polies]
# arrangement, so this can generally be assumed
if sum(contains) == 1:
idx = contains.index(True)
# mapping[idx].append(g)
if mapping[idx] is None:
mapping[idx] = g
else:
warnings.warn(f"Not applying {g}; overlapping with {mapping[idx]}")
else:
warnings.warn(f"Point {p.representative_point()} not covered by arrangement")
if settings.arrange_zones:
# @todo test whether output with only arrange_spaces is unaltered for bimlegal to function
zone_assignment = [None] * len(arranged_polies)
for i, m in enumerate(mapping):
if m is not None:
groups = [rel.RelatingGroup for rel in m.HasAssignments if rel.is_a("IfcRelAssignsToGroup")]
if settings.zone_filter:
query, val = settings.zone_filter.rsplit("=", 1)
# cast to type encountered in pset prop
val = type_unsafe_value(val)
groups = [
g for g in groups if ifcopenshell.util.selector.get_element_value(g, query) == val
]
zone_assignment[i] = frozenset(groups)
all_zones = list(set(itertools.chain.from_iterable(a or () for a in zone_assignment)))
all_zone_memberships = [
{i for i, zns in enumerate(zone_assignment) if zns is not None and zn in zns} for zn in all_zones
]
def union_find(sets):
merged = True
while merged:
merged = False
result = []
while sets:
first, *rest = sets
first = set(first)
# merge any overlapping sets into `first`
changed = True
while changed:
changed = False
new_rest = []
for r in rest:
if first & r: # overlap?
first |= r
changed = True
merged = True
else:
new_rest.append(r)
rest = new_rest
result.append(first)
sets = rest
sets[:] = result
return sets
to_merge = union_find(all_zone_memberships)
zone_groups.append(dom1.createElement("g"))
for idx_set in to_merge:
zone = next(iter(zone_assignment[max(idx_set)]))
poly = shapely.unary_union([arranged_polies[i] for i in idx_set])
poly = poly.geoms if poly.geom_type == "MultiPolygon" else [poly]
g = dom1.createElement("g")
def classes():
yield zone.is_a()
for propname, propval in reduce(
operator.or_, ifcopenshell.util.element.get_psets(zone).values()
).items():
if propval is True:
yield propname
g.setAttribute("class", " ".join(classes()))
for p in poly:
g.appendChild(polygon_to_svg_path(dom1, p))
if (lbl := settings.zone_label) and (
val := ifcopenshell.util.selector.get_element_value(zone, lbl)
):
pt = p.representative_point()
text_el = dom1.createElement("text")
text_el.setAttribute("x", str(pt.x))
text_el.setAttribute("y", str(pt.y))
text_el.appendChild(dom1.createTextNode(str(val)))
g.appendChild(text_el)
zone_groups[-1].appendChild(g)
else:
zone_groups.append(g3)
for rg, zg in zip(root_groups, zone_groups):
for p in rg.getElementsByTagName("path"):
p.setAttribute("style", "fill: none; stroke: black; stroke-width: 0.05")
if not settings.arrange_zones:
for p in rg.getElementsByTagName("path"):
p.setAttribute("style", "fill: none; stroke: black; stroke-width: 0.05")
rg.appendChild(zg)
if settings.css:
svg = dom1.documentElement
style = dom1.createElement("style")
style.setAttribute("type", "text/css")
style.appendChild(
dom1.createTextNode(open(settings.css[1:]).read() if settings.css.startswith("@") else settings.css)
)
svg.insertBefore(style, svg.firstChild)
data = dom1.toprettyxml()
data = data.encode("ascii", "xmlcharrefreplace")
+4 -15
View File
@@ -976,9 +976,8 @@ class file:
"""Write ifc model to file.
:param format: Force use of a specific format. Guessed from file name
if None. Supported formats : .ifc, .ifcXML, .ifcZIP (equivalent to
format=".ifc" with zipped=True) For zipped .ifcXML use
format=".ifcXML" with zipped=True
if None. Supported formats : .ifc, .ifcZIP (equivalent to
format=".ifc" with zipped=True)
:param zipped: zip the file after it is written
Example:
@@ -986,10 +985,8 @@ class file:
.. code:: python
model.write("path/to/model.ifc")
model.write("path/to/model.ifcXML")
model.write("path/to/model.ifcZIP")
model.write("path/to/model.ifcZIP", format=".ifcXML", zipped=True)
model.write("path/to/model.anyextension", format=".ifcXML")
model.write("path/to/model.anyextension", format=".ifc")
"""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
@@ -997,15 +994,7 @@ class file:
if format == None:
format = ifcopenshell.guess_format(path)
if format == ".ifcXML":
serializer = ifcopenshell_wrapper.XmlSerializer(self, str(path))
serializer.finalize()
if zipped:
unzipped_path = path.with_suffix(format)
path.rename(unzipped_path)
with zipfile.ZipFile(path, "w") as zip_file:
zip_file.write(unzipped_path, unzipped_path.name, compress_type=zipfile.ZIP_DEFLATED)
unzipped_path.unlink()
return
raise NotImplementedError("Writing .ifcXML files is not supported")
if format == ".ifcZIP":
return self.write(path, ".ifc", zipped=True)
self.wrapped_data.write(str(path))
@@ -173,7 +173,7 @@
"Identification": "ItemReference"
},
"IfcMaterialProperties": {
"Name": "Material"
"Properties": "ExtendedProperties"
},
"IfcBlobTexture": {
"Mode": "TextureType",
@@ -5,4 +5,5 @@
, "IfcAnnotationSurfaceOccurrence": "IfcStyledItem"
, "IfcAnnotationSymbolOccurrence": "IfcStyledItem"
, "IfcAnnotationTextOccurrence": "IfcStyledItem"
, "IfcExtendedMaterialProperties": "IfcMaterialProperties"
}
@@ -329,61 +329,28 @@ def guess_date_range(work_schedule: ifcopenshell.entity_instance):
return earliest, latest
def get_direct_task_outputs(task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
return [rel.RelatingProduct for rel in task.HasAssignments if rel.is_a("IfcRelAssignsToProduct")]
def get_task_outputs(
task: ifcopenshell.entity_instance, is_recursive: bool = False
) -> set[ifcopenshell.entity_instance]:
if is_recursive:
return {o for subtask in [task] + list(get_all_nested_tasks(task)) for o in get_task_outputs(subtask)}
return {rel.RelatingProduct for rel in task.HasAssignments if rel.is_a("IfcRelAssignsToProduct")}
def get_task_outputs(task: ifcopenshell.entity_instance, is_deep: bool = False) -> list[ifcopenshell.entity_instance]:
if not is_deep:
return get_direct_task_outputs(task)
else:
return [output for nested_task in get_all_nested_tasks(task) for output in get_direct_task_outputs(nested_task)]
def get_task_inputs(
task: ifcopenshell.entity_instance, is_recursive: bool = False
) -> set[ifcopenshell.entity_instance]:
if is_recursive:
return {o for subtask in [task] + list(get_all_nested_tasks(task)) for o in get_task_inputs(subtask)}
return {o for rel in task.OperatesOn for o in rel.RelatedObjects if o.is_a("IfcProduct")}
def get_task_inputs(task: ifcopenshell.entity_instance, is_deep: bool = False) -> list[ifcopenshell.entity_instance]:
if not is_deep:
return [
object
for rel in task.OperatesOn
if rel.is_a("IfcRelAssignsToProcess")
for object in rel.RelatedObjects
if object.is_a("IfcProduct")
]
else:
return [
output
for nested_task in get_all_nested_tasks(task)
for output in [
object
for rel in nested_task.OperatesOn
if rel.is_a("IfcRelAssignsToProcess")
for object in rel.RelatedObjects
if object.is_a("IfcProduct")
]
]
def get_task_resources(task: ifcopenshell.entity_instance, is_deep: bool = False) -> list[ifcopenshell.entity_instance]:
if not is_deep:
return [
object
for rel in task.OperatesOn
if rel.is_a("IfcRelAssignsToProcess")
for object in rel.RelatedObjects
if object.is_a("IfcResource")
]
else:
return [
resource
for nested_task in get_all_nested_tasks(task)
for resource in [
object
for rel in nested_task.OperatesOn
if rel.is_a("IfcRelAssignsToProcess")
for object in rel.RelatedObjects
if object.is_a("IfcResource")
]
]
def get_task_resources(
task: ifcopenshell.entity_instance, is_recursive: bool = False
) -> set[ifcopenshell.entity_instance]:
if is_recursive:
return {r for subtask in [task] + list(get_all_nested_tasks(task)) for r in get_task_resources(subtask)}
return {o for rel in task.OperatesOn for o in rel.RelatedObjects if o.is_a("IfcResource")}
def has_task_outputs(task: ifcopenshell.entity_instance) -> bool:
@@ -118,3 +118,20 @@ END-ISO-10303-21;
assert qt_float_count_measure_ifc4x3.is_a("IfcQuantityNumber")
assert isinstance(qt_float_count_measure_ifc4x3[3], float)
assert qt_float_count_measure_ifc4x3[3] == 723.0
def test_migrate_extended_material_properties_ifc2x3_ifc4(self):
ifc2x3_file = ifcopenshell.api.project.create_file(version="IFC2X3")
material = ifc2x3_file.createIfcMaterial(Name="Material")
props = [ifc2x3_file.createIfcPropertySingleValue(Name="Foo")]
original_element = ifc2x3_file.createIfcExtendedMaterialProperties(Material=material, ExtendedProperties=props)
ifc4_file = ifcopenshell.api.project.create_file()
migrator = subject.Migrator()
for element in ifc2x3_file:
migrator.migrate(element, ifc4_file)
new_element = ifc4_file.by_type("IfcMaterialProperties")[0]
assert new_element.Material.is_a("IfcMaterial")
assert new_element.Material.Name == "Material"
assert new_element.Properties[0].is_a("IfcPropertySingleValue")
assert new_element.Properties[0].Name == "Foo"
+17 -8
View File
@@ -38,7 +38,7 @@ namespace {
}
#ifdef IFOPSH_WITH_ROCKSDB
else {
T val;
T val = T{};
const bool is_header = entity_or_type->schema() == &Header_section_schema::get_schema();
if constexpr (
// the following types cannot be directly deserialized from rocksdb, but need to be constructed
@@ -51,6 +51,12 @@ namespace {
(is_header ? entity_or_type->name() : std::to_string(instance_name_)) + "|" +
std::to_string(index_), &str);
impl::deserialize(array_.db_ptr, str, val);
} else {
static_assert(
std::is_same_v<T, EnumerationReference> ||
std::is_same_v<std::remove_cv_t<std::remove_pointer_t<T>>, IfcUtil::IfcBaseClass>,
"RocksDB deserialization must be specialized for this EnumerationReference and IfcBaseClass*"
);
}
return val;
}
@@ -194,13 +200,15 @@ AttributeValue::operator IfcUtil::IfcBaseClass* () const
std::to_string(index_), &str);
size_t v;
memcpy(&v, str.data() + 2, sizeof(size_t));
if (str[1] == 'i') {
if (str.size() > 1 && str[1] == 'i') {
// entity reference, by #Name
return array_.db_ptr->assert_existance(v, IfcParse::impl::rocks_db_file_storage::entityinstance_ref);
} else if (str[1] == 't') {
} else if (str.size() > 1 && str[1] == 't') {
// type reference by Identity
return array_.db_ptr->assert_existance(v, IfcParse::impl::rocks_db_file_storage::typedecl_ref);
}
} else {
throw std::runtime_error("Invalid data encountered");
}
}
#endif
}
@@ -339,28 +347,28 @@ bool impl::serialize(std::string& val, const aggregate_of_aggregate_of_instance:
return true;
}
bool impl::serialize(std::string& val, const Blank& t)
bool impl::serialize(std::string& val, const Blank&)
{
val.resize(1);
val[0] = TypeEncoder::encode_type<Blank>();
return true;
}
bool impl::serialize(std::string& val, const Derived& t)
bool impl::serialize(std::string& val, const Derived&)
{
val.resize(1);
val[0] = TypeEncoder::encode_type<Derived>();
return true;
}
bool impl::serialize(std::string& val, const empty_aggregate_t& t)
bool impl::serialize(std::string& val, const empty_aggregate_t&)
{
val.resize(1);
val[0] = TypeEncoder::encode_type<empty_aggregate_t>();
return true;
}
bool impl::serialize(std::string& val, const empty_aggregate_of_aggregate_t& t)
bool impl::serialize(std::string& val, const empty_aggregate_of_aggregate_t&)
{
val.resize(1);
val[0] = TypeEncoder::encode_type<empty_aggregate_of_aggregate_t>();
@@ -397,6 +405,7 @@ bool impl::deserialize(IfcParse::impl::rocks_db_file_storage*, const std::string
} else {
return false;
}
return true;
}
bool impl::deserialize(IfcParse::impl::rocks_db_file_storage*, const std::string& val, boost::dynamic_bitset<>& t) {
+1 -2
View File
@@ -266,7 +266,6 @@ namespace impl {
if (val[0] != TypeEncoder::encode_type<T>()) {
return false;
}
auto s = (val.size() - 1) / sizeof(T);
memcpy(&t, val.data() + 1, sizeof(T));
return true;
}
@@ -404,7 +403,7 @@ public:
template<typename Visitor>
auto apply_visitor(void* storage, const IfcParse::declaration* decl, std::size_t identity, std::size_t index, Visitor&& visitor) const {
// @todo do we need visitation on all data/storage/attribute levels?
AttributeValue((IfcParse::impl::rocks_db_file_storage*)storage, identity, decl, index).apply_visitor(std::forward<Visitor>(visitor));
AttributeValue((IfcParse::impl::rocks_db_file_storage*)storage, identity, decl, (uint8_t) index).apply_visitor(std::forward<Visitor>(visitor));
}
#endif
};
+7
View File
@@ -0,0 +1,7 @@
#include "IfcException.h"
IfcParse::IfcException::~IfcException() = default;
IfcParse::IfcAttributeOutOfRangeException::~IfcAttributeOutOfRangeException() = default;
IfcParse::IfcInvalidTokenException::~IfcInvalidTokenException() = default;
+4 -4
View File
@@ -41,8 +41,8 @@ class IFC_PARSE_API IfcException : public std::exception {
public:
IfcException(const std::string& message)
: message_(message) {}
virtual ~IfcException() throw() {}
virtual const char* what() const throw() {
~IfcException() override;
const char* what() const noexcept override {
return message_.c_str();
}
};
@@ -51,7 +51,7 @@ class IFC_PARSE_API IfcAttributeOutOfRangeException : public IfcException {
public:
IfcAttributeOutOfRangeException(const std::string& exception)
: IfcException(exception) {}
~IfcAttributeOutOfRangeException() throw() {}
~IfcAttributeOutOfRangeException() override;
};
class IFC_PARSE_API IfcInvalidTokenException : public IfcException {
@@ -70,7 +70,7 @@ class IFC_PARSE_API IfcInvalidTokenException : public IfcException {
: IfcException(
std::string("Unexpected '") + std::string(1, character) + "' at offset " +
boost::lexical_cast<std::string>(token_start)) {}
~IfcInvalidTokenException() throw() {}
~IfcInvalidTokenException() override;
};
} // namespace IfcParse
+4
View File
@@ -127,6 +127,10 @@ public:
return storage_.byref_excl_;
}
std::vector<std::unique_ptr<IfcUtil::IfcBaseClass>> steal_instances() {
return storage_.steal_instances();
}
InstanceStreamer(const std::string& fn);
InstanceStreamer(void* data, int length);
+4
View File
@@ -725,6 +725,7 @@ void IfcParse::impl::in_memory_file_storage::load(unsigned entity_instance_name,
// the 2nd pass.
load(entity_instance_name, entity, ps, attribute_index == -1 ? (int)attribute_index_within_data : attribute_index);
auto* simple_type_instance = (schema ? schema : file->schema())->instantiate(decl, ps.construct(entity_instance_name, *references_to_resolve, decl, boost::none, attribute_index == -1 ? (int)attribute_index_within_data : attribute_index));
read_simple_type_instances.emplace_back(simple_type_instance);
//@todo decide addEntity(((IfcUtil::IfcBaseClass*)*entity));
context.push(simple_type_instance);
simple_type_instance->file_ = file;
@@ -1650,6 +1651,9 @@ void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::IfcSpfSt
good_ = streamer.status();
byref_excl_ = streamer.inverses();
// Move the storage of simple type instances so that they are retained during the lifetime of the file
read_simple_type_instances = streamer.steal_instances();
Logger::Status("\rDone scanning file ");

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