mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 07:51:47 +00:00
Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9adc8e484a | |||
| e8eb5e4d0a | |||
| 080222ffe6 | |||
| abef655482 | |||
| 85b3f831dc | |||
| d8ece32d62 | |||
| 0df7117d37 | |||
| 91d67e6371 | |||
| 87d1b0637a | |||
| b8adddc648 | |||
| 25d3edc537 | |||
| 7f8cb80281 | |||
| a2ce2ea23a | |||
| 7f7aef0e6e | |||
| 821b5380ea | |||
| e8bcbc83a4 | |||
| 091af3c1c9 | |||
| 4cc50301dd | |||
| f643a1615e | |||
| 71e78c67f8 | |||
| e7c4a19d9e | |||
| 6c31d43e72 | |||
| 83b56d591b | |||
| e5b26067ab | |||
| a4fae94be8 | |||
| af4851f03a | |||
| 155d4c44ae | |||
| 48119baf97 | |||
| dd1989aebb | |||
| 0806430700 | |||
| 2d97f6638b | |||
| b10e5ad4d6 | |||
| 24f991ceb1 | |||
| ac11baa0c4 | |||
| 1609a77b77 | |||
| dd2f091a52 | |||
| a7bf0e7d5e | |||
| 19bdd3e622 | |||
| e1bf891ec6 | |||
| 4009834b97 | |||
| 77f5e1fbad | |||
| c4eb77d544 | |||
| 8a4ea43239 | |||
| f056e22f60 | |||
| 05e81040ce | |||
| 8d8133e780 | |||
| dceb35782e | |||
| 2c5f0c6cf2 | |||
| abd667684d | |||
| 31b5f88497 | |||
| 77a7026d33 | |||
| 9c5f3f9754 | |||
| 5d56025014 | |||
| ad62c75f4f | |||
| 7a5bd03216 | |||
| 593e0e28a9 | |||
| ed2b2de79a | |||
| 5a511eecc1 | |||
| 83e0fa1287 | |||
| a2dbdbf8cc | |||
| 3d68bbc0ca | |||
| 40f9f261ae | |||
| c08cc58904 | |||
| ebdb122a22 | |||
| 5de2752685 | |||
| d2245a95c7 | |||
| 47075e95bd | |||
| 9d4c980e02 | |||
| 53c1a1537d | |||
| 7db2f66685 | |||
| 6dd34b1fa4 | |||
| 862713cda4 | |||
| 06c1668e42 | |||
| 5f9f0f26eb | |||
| 8db22cdaf7 | |||
| 3bed9262e5 | |||
| 0c171a457b | |||
| b5e8dfd863 | |||
| c9df87fc25 | |||
| 30925a4b3c | |||
| ccf0efffce | |||
| 169c4392a9 | |||
| e543785959 | |||
| 3617d119cb | |||
| 5d1f28c3cb | |||
| 6f04eb12d0 | |||
| b46136e9f8 | |||
| c3fe2d0ee3 | |||
| aeed371888 | |||
| a50c8869a5 | |||
| 04d1fe4052 | |||
| 4e1d0bcf22 | |||
| d2126e5929 | |||
| 3bdfbebc02 | |||
| 1a9bd1891e | |||
| 1599bc14b3 | |||
| 30b74ab09c | |||
| 24bee01f01 | |||
| 7329c485ea | |||
| 9c93b12084 | |||
| b97c7d39ad | |||
| fa0c1c5d2a | |||
| e4c49ce57b | |||
| 3ae77c68e8 | |||
| 8b1cb8e7ae | |||
| ce0348518d | |||
| a660027b93 | |||
| 44ef409335 | |||
| 089edff1db | |||
| bd5a1b5bda | |||
| ba3517f3aa | |||
| 7f144b7b95 | |||
| 19ac0b6c38 | |||
| 3562437293 | |||
| e8ece71d22 |
@@ -34,6 +34,10 @@ jobs:
|
||||
name: "Linux 64bit",
|
||||
short_name: linux64
|
||||
}
|
||||
- {
|
||||
name: "Linux ARM 64bit",
|
||||
short_name: linuxarm64
|
||||
}
|
||||
- {
|
||||
name: "MacOS Intel 64bit",
|
||||
short_name: macos64
|
||||
|
||||
@@ -88,9 +88,6 @@ src/bonsai/bonsai/bim/data/gantt/jsgantt.js
|
||||
src/bonsai/bonsai/bim/data/gantt/jsgantt.css
|
||||
src/bonsai/bonsai/bim/data/webui/static/js/jquery.min.js
|
||||
|
||||
src/bonsai/drawings
|
||||
src/bonsai/layouts
|
||||
|
||||
# ifcopenshell swig and compiled files
|
||||
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper*.so
|
||||
src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
|
||||
|
||||
+8
-145
@@ -104,6 +104,9 @@ endif()
|
||||
|
||||
project(IfcOpenShell VERSION ${RELEASE_VERSION})
|
||||
|
||||
# Make sure CMake modules in this project are found first
|
||||
list(PREPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR})
|
||||
|
||||
if(MINIMAL_BUILD)
|
||||
message(STATUS "Setting options for minimal build")
|
||||
set(BUILD_GEOMSERVER OFF)
|
||||
@@ -259,7 +262,7 @@ if(WITH_CGAL)
|
||||
else()
|
||||
set(CGAL_Boost_USE_STATIC_LIBS "${Boost_USE_STATIC_LIBS}")
|
||||
endif()
|
||||
find_package(CGAL REQUIRED)
|
||||
find_package(CGAL REQUIRED)
|
||||
if(NOT CGAL_DIR)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
@@ -730,91 +733,7 @@ endif()
|
||||
|
||||
|
||||
if(HDF5_SUPPORT)
|
||||
if("${HDF5_INCLUDE_DIR}" STREQUAL "")
|
||||
message(STATUS "No HDF5 include directory specified")
|
||||
else()
|
||||
set(HDF5_INCLUDE_DIR "${HDF5_INCLUDE_DIR}" CACHE FILEPATH "HDF5 header files")
|
||||
endif()
|
||||
|
||||
if("${HDF5_LIBRARY_DIR}" STREQUAL "")
|
||||
message(STATUS "No HDF5 library directory specified")
|
||||
else()
|
||||
set(HDF5_LIBRARY_DIR "${HDF5_LIBRARY_DIR}" CACHE FILEPATH "HDF5 library files")
|
||||
endif()
|
||||
|
||||
if(HDF5_LIBRARY_DIR)
|
||||
# result of the HDF5 ctest package
|
||||
# Find zlib using cmake find_library. How should this be implemented?
|
||||
# FIND_LIBRARY(NAMES z libz libz_debug PATHS ... NO_DEFAULT_PATH)
|
||||
if("$ENV{CONDA_BUILD}" STREQUAL "")
|
||||
# result of the HDF5 ctest package
|
||||
if(WIN32)
|
||||
set(zlib_post lib)
|
||||
set(lib_ext lib)
|
||||
else()
|
||||
set(lib_ext a)
|
||||
endif()
|
||||
|
||||
if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
|
||||
set(debug_postfix "_debug")
|
||||
endif()
|
||||
|
||||
set(HDF5_LIBRARIES
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5_cpp${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libz${zlib_post}${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libsz${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libaec${debug_postfix}.${lib_ext}"
|
||||
)
|
||||
|
||||
else()
|
||||
message(STATUS "Packaging hdf5 and zlib for conda distribution")
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
|
||||
# macOS
|
||||
set(zlib_post libz)
|
||||
set(lib_ext dylib)
|
||||
set(HDF5_LIBRARIES
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5_cpp.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/${zlib_post}.${lib_ext}"
|
||||
)
|
||||
else()
|
||||
# linux and windows
|
||||
# Find HDF5 package
|
||||
find_package(HDF5 REQUIRED COMPONENTS C CXX)
|
||||
# Find ZLIB package
|
||||
find_package(ZLIB REQUIRED)
|
||||
# Include directories
|
||||
include_directories(${HDF5_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS})
|
||||
# Link libraries
|
||||
set(HDF5_LIBRARIES ${HDF5_LIBRARIES} ${ZLIB_LIBRARIES})
|
||||
message(STATUS "HDF5 libraries: ${HDF5_LIBRARIES}")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
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)
|
||||
message(STATUS "HDF5: found config at '${HDF5_DIR}'.")
|
||||
set(HDF5_LIBRARIES hdf5_cpp-static)
|
||||
else()
|
||||
# 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 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)."
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
find_package(HDF5 REQUIRED COMPONENTS C CXX)
|
||||
|
||||
add_definitions(-DWITH_HDF5)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_HDF5)
|
||||
@@ -948,7 +867,7 @@ endif()
|
||||
|
||||
|
||||
include_directories(${INCLUDE_DIRECTORIES} ${OCC_INCLUDE_DIR} ${OPENCOLLADA_INCLUDE_DIRS}
|
||||
${Boost_INCLUDE_DIRS} ${LIBXML2_INCLUDE_DIR} ${JSON_INCLUDE_DIR} ${HDF5_INCLUDE_DIR}
|
||||
${Boost_INCLUDE_DIRS} ${LIBXML2_INCLUDE_DIR} ${JSON_INCLUDE_DIR} ${HDF5_INCLUDE_DIRS}
|
||||
${EIGEN_DIR} ${CGAL_INCLUDE_DIR} ${GMP_INCLUDE_DIR} ${MPFR_INCLUDE_DIR} ${USD_INCLUDE_DIR}
|
||||
${TBB_INCLUDE_DIR}
|
||||
)
|
||||
@@ -1038,6 +957,7 @@ if(NOT Boost_VERSION LESS 105800)
|
||||
add_definitions(-DBOOST_OPTIONAL_USE_OLD_DEFINITION_OF_NONE)
|
||||
endif()
|
||||
|
||||
add_subdirectory(../src/ifcparse ifcparse)
|
||||
set(IFCOPENSHELL_LIBRARIES IfcParse)
|
||||
|
||||
if(BUILD_IFCGEOM)
|
||||
@@ -1070,52 +990,6 @@ if(BUILD_CONVERT OR BUILD_IFCPYTHON)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# IfcParse
|
||||
file(GLOB IFCPARSE_H_FILES_ALL ../src/ifcparse/*.h)
|
||||
file(GLOB IFCPARSE_CPP_FILES_ALL ../src/ifcparse/*.cpp)
|
||||
|
||||
foreach(file ${IFCPARSE_H_FILES_ALL})
|
||||
get_filename_component(filename "${file}" NAME)
|
||||
|
||||
if(NOT "${filename}" MATCHES "[0-9]")
|
||||
list(APPEND IFCPARSE_H_FILES "${file}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
foreach(file ${IFCPARSE_CPP_FILES_ALL})
|
||||
get_filename_component(filename "${file}" NAME)
|
||||
|
||||
if(NOT "${filename}" MATCHES "[0-9]")
|
||||
list(APPEND IFCPARSE_CPP_FILES "${file}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
foreach(schema ${SCHEMA_VERSIONS})
|
||||
list(APPEND IFCPARSE_H_FILES
|
||||
../src/ifcparse/Ifc${schema}.h
|
||||
../src/ifcparse/Ifc${schema}-definitions.h
|
||||
)
|
||||
list(APPEND IFCPARSE_CPP_FILES
|
||||
../src/ifcparse/Ifc${schema}.cpp
|
||||
../src/ifcparse/Ifc${schema}-schema.cpp
|
||||
)
|
||||
endforeach()
|
||||
|
||||
set(IFCPARSE_FILES ${IFCPARSE_CPP_FILES} ${IFCPARSE_H_FILES})
|
||||
|
||||
add_library(IfcParse ${IFCPARSE_FILES})
|
||||
target_link_libraries(IfcParse ${STDCPPFS})
|
||||
set_target_properties(IfcParse PROPERTIES COMPILE_FLAGS -DIFC_PARSE_EXPORTS VERSION "${PROJECT_VERSION}" SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
|
||||
|
||||
if(LibXml2_DIR)
|
||||
target_compile_definitions(IfcParse PRIVATE ${LIBXML2_DEFINITIONS})
|
||||
endif()
|
||||
if(WASM_BUILD)
|
||||
target_link_libraries(IfcParse ${BCRYPT_LIBRARIES} ${LIBXML2_LIBRARIES})
|
||||
else()
|
||||
target_link_libraries(IfcParse ${Boost_LIBRARIES} ${BCRYPT_LIBRARIES} ${LIBXML2_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(BUILD_IFCGEOM)
|
||||
# CGAL::CGAL target already has dependencies resolved.
|
||||
if(WITH_CGAL AND CGAL_DIR)
|
||||
@@ -1309,7 +1183,7 @@ if(BUILD_GEOMSERVER)
|
||||
file(GLOB H_FILES ../src/ifcgeomserver/*.h)
|
||||
set(SOURCE_FILES ${CPP_FILES} ${H_FILES})
|
||||
add_executable(IfcGeomServer ${SOURCE_FILES})
|
||||
target_link_libraries(IfcGeomServer IfcGeom IfcParse Serializers ${kernel_libraries} ${OPENCASCADE_LIBRARIES} ${Boost_LIBRARIES})
|
||||
target_link_libraries(IfcGeomServer IfcGeom IfcParse ${kernel_libraries} ${OPENCASCADE_LIBRARIES} ${Boost_LIBRARIES})
|
||||
|
||||
if((NOT WIN32) AND BUILD_SHARED_LIBS)
|
||||
SET_INSTALL_RPATHS(IfcGeomServer "${IFCOPENSHELL_LIBRARY_DIR};${OCC_LIBRARY_DIR};${Boost_LIBRARY_DIRS}")
|
||||
@@ -1406,17 +1280,6 @@ if(BUILD_QTVIEWER)
|
||||
add_subdirectory(../src/qtviewer qtviewer)
|
||||
endif()
|
||||
|
||||
# CMake installation targets
|
||||
install(FILES ${IFCPARSE_H_FILES}
|
||||
DESTINATION ${INCLUDEDIR}/ifcparse
|
||||
)
|
||||
|
||||
install(TARGETS IfcParse
|
||||
ARCHIVE DESTINATION ${LIBDIR}
|
||||
LIBRARY DESTINATION ${LIBDIR}
|
||||
RUNTIME DESTINATION ${BINDIR}
|
||||
)
|
||||
|
||||
if(BUILD_IFCGEOM)
|
||||
# install(FILES ${IFCGEOM_H_FILES}
|
||||
# DESTINATION ${INCLUDEDIR}/ifcgeom
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# To avoid cyclic calls to this file
|
||||
list(REMOVE_ITEM CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR})
|
||||
|
||||
if("${HDF5_INCLUDE_DIR}" STREQUAL "")
|
||||
message(STATUS "No HDF5 include directory specified")
|
||||
else()
|
||||
set(HDF5_INCLUDE_DIR "${HDF5_INCLUDE_DIR}" CACHE FILEPATH "HDF5 header files")
|
||||
endif()
|
||||
|
||||
if("${HDF5_LIBRARY_DIR}" STREQUAL "")
|
||||
message(STATUS "No HDF5 library directory specified")
|
||||
else()
|
||||
set(HDF5_LIBRARY_DIR "${HDF5_LIBRARY_DIR}" CACHE FILEPATH "HDF5 library files")
|
||||
endif()
|
||||
|
||||
if(HDF5_LIBRARY_DIR)
|
||||
# result of the HDF5 ctest package
|
||||
# Find zlib using cmake find_library. How should this be implemented?
|
||||
# FIND_LIBRARY(NAMES z libz libz_debug PATHS ... NO_DEFAULT_PATH)
|
||||
if("$ENV{CONDA_BUILD}" STREQUAL "")
|
||||
# result of the HDF5 ctest package
|
||||
if(WIN32)
|
||||
set(zlib_post lib)
|
||||
set(lib_ext lib)
|
||||
else()
|
||||
set(lib_ext a)
|
||||
endif()
|
||||
|
||||
if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
|
||||
set(debug_postfix "_debug")
|
||||
endif()
|
||||
|
||||
set(HDF5_LIBRARIES
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5_cpp${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libz${zlib_post}${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libsz${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libaec${debug_postfix}.${lib_ext}"
|
||||
)
|
||||
|
||||
else()
|
||||
message(STATUS "Packaging hdf5 and zlib for conda distribution")
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
|
||||
# macOS
|
||||
set(zlib_post libz)
|
||||
set(lib_ext dylib)
|
||||
set(HDF5_LIBRARIES
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5_cpp.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/${zlib_post}.${lib_ext}"
|
||||
)
|
||||
else()
|
||||
# linux and windows
|
||||
# Find HDF5 package
|
||||
find_package(HDF5 REQUIRED COMPONENTS C CXX)
|
||||
# Find ZLIB package
|
||||
find_package(ZLIB REQUIRED)
|
||||
# Include directories
|
||||
include_directories(${HDF5_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS})
|
||||
# Link libraries
|
||||
set(HDF5_LIBRARIES ${HDF5_LIBRARIES} ${ZLIB_LIBRARIES})
|
||||
message(STATUS "HDF5 libraries: ${HDF5_LIBRARIES}")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
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)
|
||||
message(STATUS "HDF5: found config at '${HDF5_DIR}'.")
|
||||
set(HDF5_LIBRARIES hdf5_cpp-static)
|
||||
else()
|
||||
# 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 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)."
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
+8
-6
@@ -382,11 +382,13 @@ if MAC_CROSS_COMPILE_INTEL:
|
||||
MAC_CROSS_COMPILE_INTEL_BJAM_ARGS = ["architecture=x86"]
|
||||
MAC_CROSS_COMPILE_INTEL_CXX = "clang++ -arch x86_64"
|
||||
MAC_CROSS_COMPILE_INTEL_CC = "clang -arch x86_64"
|
||||
MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS = ["--host=x86_64-apple-darwin"]
|
||||
else:
|
||||
MAC_CROSS_COMPILE_INTEL_ARGS = []
|
||||
MAC_CROSS_COMPILE_INTEL_BJAM_ARGS = []
|
||||
MAC_CROSS_COMPILE_INTEL_CXX = ""
|
||||
MAC_CROSS_COMPILE_INTEL_CC = ""
|
||||
MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS = []
|
||||
|
||||
OFF_ON = ["OFF", "ON"]
|
||||
BUILD_STATIC = "shared" not in flags
|
||||
@@ -1195,7 +1197,9 @@ if "cgal" in targets:
|
||||
OLD_CC = None
|
||||
if MAC_CROSS_COMPILE_INTEL:
|
||||
OLD_CC = os.environ.get("CC")
|
||||
# Otherwise it's using arm64 `gcc` and fails to build gmp.
|
||||
os.environ["CC"] = MAC_CROSS_COMPILE_INTEL_CC
|
||||
gmp_args.extend(MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS)
|
||||
|
||||
build_dependency(
|
||||
name=f"gmp-{GMP_VERSION}",
|
||||
@@ -1374,6 +1378,7 @@ if "cgal" in targets:
|
||||
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/cgal-{CGAL_VERSION}")
|
||||
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/gmp-{GMP_VERSION}")
|
||||
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/mpfr-{MPFR_VERSION}")
|
||||
cmake_args.append(f"-DCGAL_WITH_GMPXX=Off")
|
||||
|
||||
if "occ" in targets and USE_OCCT:
|
||||
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/occt-{OCCT_VERSION}")
|
||||
@@ -1441,17 +1446,14 @@ if not WASM and (not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServe
|
||||
|
||||
logger.info("\rBuilding executables... ")
|
||||
|
||||
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}"], cwd=executables_dir)
|
||||
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "VERBOSE=1"], cwd=executables_dir)
|
||||
run([make, "install/strip" if BUILD_CFG == "Release" else "install"], cwd=executables_dir)
|
||||
|
||||
if "IfcOpenShell-Python" in targets:
|
||||
# On OSX the actual Python library is not linked against.
|
||||
ADDITIONAL_ARGS = ""
|
||||
if platform.system() == "Darwin":
|
||||
ADDITIONAL_ARGS = "-Wl,-flat_namespace,-undefined,suppress"
|
||||
|
||||
if "wasm" in flags:
|
||||
ADDITIONAL_ARGS = "-Wl,-undefined,suppress"
|
||||
ADDITIONAL_ARGS = "-Wl,-undefined,dynamic_lookup"
|
||||
|
||||
# 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)
|
||||
@@ -1516,7 +1518,7 @@ if "IfcOpenShell-Python" in targets:
|
||||
|
||||
logger.info(f"\rBuilding python {python_version} wrapper... ")
|
||||
|
||||
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "ifcopenshell_wrapper"], cwd=python_dir)
|
||||
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "ifcopenshell_wrapper", "VERBOSE=1"], cwd=python_dir)
|
||||
run([make, "install/local"], cwd=os.path.join(python_dir, "ifcwrap"))
|
||||
|
||||
if python_executable:
|
||||
|
||||
+4
-1
@@ -87,7 +87,7 @@ BLENDER_PLATFORM:=windows-x64
|
||||
endif
|
||||
|
||||
# Current build commit hash.
|
||||
OLD:=6924012
|
||||
OLD:=e8eb5e4
|
||||
.PHONY: bump
|
||||
bump:
|
||||
ifndef NEW
|
||||
@@ -155,6 +155,9 @@ endif
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download six --dest=./wheels
|
||||
# Required by drawing module
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download lxml $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
|
||||
# Required by drawing module for markdown hyperlinks in annotations
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download markdown-it-py --dest=./wheels
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download mdurl --dest=./wheels
|
||||
# Required by qto and drawing module
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download shapely $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
|
||||
# Required by the BIM type manager thumbnail generator
|
||||
|
||||
@@ -7,12 +7,6 @@
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"scene.render.bake_bias": 0.0010000000474974513,
|
||||
"scene.render.bake_margin": 16,
|
||||
"scene.render.bake_margin_type": "ADJACENT_FACES",
|
||||
"scene.render.bake_samples": 256,
|
||||
"scene.render.bake_type": "NORMALS",
|
||||
"scene.render.bake_user_scale": 0.0,
|
||||
"scene.render.border_max_x": 1.0,
|
||||
"scene.render.border_max_y": 1.0,
|
||||
"scene.render.border_min_x": 0.0,
|
||||
@@ -54,11 +48,6 @@
|
||||
"scene.render.stamp_note_text": "",
|
||||
"scene.render.threads": 20,
|
||||
"scene.render.threads_mode": "AUTO",
|
||||
"scene.render.use_bake_clear": true,
|
||||
"scene.render.use_bake_lores_mesh": false,
|
||||
"scene.render.use_bake_multires": false,
|
||||
"scene.render.use_bake_selected_to_active": false,
|
||||
"scene.render.use_bake_user_scale": false,
|
||||
"scene.render.use_border": false,
|
||||
"scene.render.use_compositing": true,
|
||||
"scene.render.use_crop_to_border": false,
|
||||
@@ -253,12 +242,6 @@
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"scene.render.bake_bias": 0.0010000000474974513,
|
||||
"scene.render.bake_margin": 16,
|
||||
"scene.render.bake_margin_type": "ADJACENT_FACES",
|
||||
"scene.render.bake_samples": 256,
|
||||
"scene.render.bake_type": "NORMALS",
|
||||
"scene.render.bake_user_scale": 0.0,
|
||||
"scene.render.border_max_x": 1.0,
|
||||
"scene.render.border_max_y": 1.0,
|
||||
"scene.render.border_min_x": 0.0,
|
||||
@@ -300,11 +283,6 @@
|
||||
"scene.render.stamp_note_text": "",
|
||||
"scene.render.threads": 20,
|
||||
"scene.render.threads_mode": "AUTO",
|
||||
"scene.render.use_bake_clear": true,
|
||||
"scene.render.use_bake_lores_mesh": false,
|
||||
"scene.render.use_bake_multires": false,
|
||||
"scene.render.use_bake_selected_to_active": false,
|
||||
"scene.render.use_bake_user_scale": false,
|
||||
"scene.render.use_border": false,
|
||||
"scene.render.use_compositing": true,
|
||||
"scene.render.use_crop_to_border": false,
|
||||
@@ -377,7 +355,7 @@
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"scene.display.shading.studio_light": "Default",
|
||||
"scene.display.shading.studio_light": "forest.exr",
|
||||
"scene.display.shading.studiolight_background_alpha": 0.0,
|
||||
"scene.display.shading.studiolight_background_blur": 0.0,
|
||||
"scene.display.shading.studiolight_intensity": 0.0,
|
||||
@@ -503,18 +481,12 @@
|
||||
"scene.eevee.shadow_ray_count": 1,
|
||||
"scene.eevee.shadow_step_count": 6,
|
||||
"scene.eevee.shadow_resolution_scale": 1.0,
|
||||
"scene.render.bake_bias": 0.0010000000474974513,
|
||||
"scene.render.bake_margin": 16,
|
||||
"scene.render.bake_margin_type": "ADJACENT_FACES",
|
||||
"scene.render.bake_samples": 256,
|
||||
"scene.render.bake_type": "NORMALS",
|
||||
"scene.render.bake_user_scale": 0.0,
|
||||
"scene.render.border_max_x": 1.0,
|
||||
"scene.render.border_max_y": 1.0,
|
||||
"scene.render.border_min_x": 0.0,
|
||||
"scene.render.border_min_y": 0.0,
|
||||
"scene.render.dither_intensity": 1.0,
|
||||
"scene.render.engine": "BLENDER_EEVEE_NEXT",
|
||||
"scene.render.engine": "BLENDER_EEVEE",
|
||||
"scene.render.film_transparent": false,
|
||||
"scene.render.filter_size": 1.5,
|
||||
"scene.render.fps": 24,
|
||||
@@ -550,11 +522,6 @@
|
||||
"scene.render.stamp_note_text": "",
|
||||
"scene.render.threads": 20,
|
||||
"scene.render.threads_mode": "AUTO",
|
||||
"scene.render.use_bake_clear": true,
|
||||
"scene.render.use_bake_lores_mesh": false,
|
||||
"scene.render.use_bake_multires": false,
|
||||
"scene.render.use_bake_selected_to_active": false,
|
||||
"scene.render.use_bake_user_scale": false,
|
||||
"scene.render.use_border": false,
|
||||
"scene.render.use_compositing": true,
|
||||
"scene.render.use_crop_to_border": false,
|
||||
@@ -627,7 +594,7 @@
|
||||
0.800000011920929,
|
||||
0.800000011920929
|
||||
],
|
||||
"scene.display.shading.studio_light": "Default",
|
||||
"scene.display.shading.studio_light": "forest.exr",
|
||||
"scene.display.shading.studiolight_background_alpha": 0.0,
|
||||
"scene.display.shading.studiolight_background_blur": 0.0,
|
||||
"scene.display.shading.studiolight_intensity": 0.0,
|
||||
|
||||
@@ -321,6 +321,21 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
|
||||
aggregate_obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = aggregate_obj
|
||||
|
||||
# copy selection query to clipboard
|
||||
result = ""
|
||||
for aggregate in all_parts:
|
||||
aggregate_class = aggregate.is_a()
|
||||
aggregate_name = aggregate.Name or ""
|
||||
query = f'parent = "{aggregate_name}"'
|
||||
if not result:
|
||||
result = query
|
||||
else:
|
||||
result += f" + {query}"
|
||||
|
||||
if result:
|
||||
bpy.context.window_manager.clipboard = result
|
||||
self.report({"INFO"}, f"({result}) was copied to the clipboard.")
|
||||
|
||||
self.one_level_deep = False # <-- forcibly reset
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -235,8 +235,8 @@ class DecoratorData:
|
||||
cut_cache = {}
|
||||
slice_cache = {}
|
||||
fill_cache = {}
|
||||
camera_location_checksum = None
|
||||
camera_rotation_checksum = None
|
||||
camera_location_checksum = ""
|
||||
camera_rotation_checksum = ""
|
||||
|
||||
@classmethod
|
||||
def clear_cache(cls):
|
||||
|
||||
@@ -1641,6 +1641,13 @@ class CutDecorator:
|
||||
if not context.scene.camera:
|
||||
return
|
||||
|
||||
# Check if any viewport is in local view - skip decorations if so
|
||||
for area in context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
space = area.spaces.active
|
||||
if isinstance(space, bpy.types.SpaceView3D) and space.local_view:
|
||||
return # Don't draw decorations in local view
|
||||
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
self.fallback_colour = (0.3, 0.3, 0.3, 1)
|
||||
@@ -1731,20 +1738,23 @@ class CutDecorator:
|
||||
|
||||
def cache_camera_matrix(self):
|
||||
obj = bpy.context.scene.camera
|
||||
DecoratorData.camera_location_checksum = repr(np.array(obj.matrix_world.translation).tobytes())
|
||||
DecoratorData.camera_rotation_checksum = repr(np.array(obj.matrix_world.to_3x3()).tobytes())
|
||||
# Explicit `dtype` for Blender <5.0 compatibility.
|
||||
DecoratorData.camera_location_checksum = repr(
|
||||
np.array(obj.matrix_world.translation, dtype=np.float32).tobytes()
|
||||
)
|
||||
DecoratorData.camera_rotation_checksum = repr(np.array(obj.matrix_world.to_3x3(), dtype=np.float32).tobytes())
|
||||
|
||||
def is_camera_moved(self):
|
||||
if not DecoratorData.camera_location_checksum:
|
||||
self.cache_camera_matrix()
|
||||
return True # Let's be conservative
|
||||
obj = bpy.context.scene.camera
|
||||
loc_check = np.frombuffer(eval(DecoratorData.camera_location_checksum))
|
||||
loc_check = np.frombuffer(eval(DecoratorData.camera_location_checksum), dtype=np.float32)
|
||||
loc_real = np.array(obj.matrix_world.translation).flatten()
|
||||
if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm
|
||||
self.cache_camera_matrix()
|
||||
return True
|
||||
rot_check = np.frombuffer(eval(DecoratorData.camera_rotation_checksum)).reshape(3, 3)
|
||||
rot_check = np.frombuffer(eval(DecoratorData.camera_rotation_checksum), dtype=np.float32).reshape(3, 3)
|
||||
rot_real = np.array(obj.matrix_world.to_3x3())
|
||||
rot_dot = np.dot(rot_check, rot_real.T)
|
||||
angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1))
|
||||
@@ -1983,6 +1993,13 @@ class DecorationsHandler:
|
||||
decorator.font_id = font_id
|
||||
|
||||
def __call__(self, context):
|
||||
# Check if any viewport is in local view - skip decorations if so
|
||||
for area in context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
space = area.spaces.active
|
||||
if isinstance(space, bpy.types.SpaceView3D) and space.local_view:
|
||||
return # Don't draw decorations in local view
|
||||
|
||||
if not DrawingsData.is_loaded:
|
||||
DrawingsData.load()
|
||||
|
||||
|
||||
@@ -932,6 +932,31 @@ class CreateDrawing(bpy.types.Operator):
|
||||
|
||||
return svg_path
|
||||
|
||||
# Add target_view and scale classes to the parent group from IFC data
|
||||
if group is not None:
|
||||
existing_classes = group.get("class", "").split()
|
||||
|
||||
# Add target_view class
|
||||
if hasattr(self, "cprops") and getattr(self.cprops, "target_view", None):
|
||||
target_view_class = tool.Drawing.canonicalise_class_name(str(self.cprops.target_view))
|
||||
target_view_full_class = f"target-view-{target_view_class}"
|
||||
if target_view_full_class not in existing_classes:
|
||||
existing_classes.append(target_view_full_class)
|
||||
|
||||
# Add scale class from EPset_Drawing.Scale
|
||||
drawing_pset = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing")
|
||||
if drawing_pset and drawing_pset.get("Scale"):
|
||||
scale_value = drawing_pset["Scale"]
|
||||
# Remove "1/" prefix if it exists
|
||||
if isinstance(scale_value, str) and scale_value.startswith("1/"):
|
||||
scale_value = scale_value[2:]
|
||||
scale_class = tool.Drawing.canonicalise_class_name(str(scale_value))
|
||||
scale_full_class = f"scale-{scale_class}"
|
||||
if scale_full_class not in existing_classes:
|
||||
existing_classes.append(scale_full_class)
|
||||
|
||||
group.set("class", " ".join(existing_classes))
|
||||
|
||||
if self.cprops.cut_mode == "BISECT":
|
||||
self.remove_cut_linework(root)
|
||||
self.generate_bisect_linework(context, root)
|
||||
@@ -1287,11 +1312,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
tool.Drawing.canonicalise_class_name(key) + "-" + tool.Drawing.canonicalise_class_name(str(value))
|
||||
)
|
||||
|
||||
# ─── Target View ───────────────────────────────────────────
|
||||
if getattr(self.cprops, "target_view", None):
|
||||
target_view_class = tool.Drawing.canonicalise_class_name(str(self.cprops.target_view))
|
||||
classes.append(f"target-view-{target_view_class}")
|
||||
|
||||
return classes
|
||||
|
||||
def is_manifold(self, obj) -> bool:
|
||||
@@ -2701,21 +2721,59 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def set_raster_style(self, context: bpy.types.Context) -> None:
|
||||
scene = context.scene # Do not remove. It is used in exec later
|
||||
assert (space := tool.Blender.get_view3d_space()) # Do not remove. It is used in exec later
|
||||
style = json.loads(self.drawing_style.raster_style)
|
||||
style: dict[str, Any] = json.loads(self.drawing_style.raster_style)
|
||||
|
||||
VIEWPORT_SHADING_TYPE = "scene.display.shading.type"
|
||||
|
||||
def preprocess(path: str, value: Any) -> tuple[str, Any, bool, bool]:
|
||||
warning = False
|
||||
skip = False
|
||||
# @25.05.12
|
||||
if path == "scene.render.engine" and value == "BLENDER_EEVEE":
|
||||
value = "BLENDER_EEVEE_NEXT"
|
||||
|
||||
BLENDER_5_REMOVED = (
|
||||
"scene.render.bake_bias",
|
||||
"scene.render.bake_margin",
|
||||
"scene.render.bake_margin_type",
|
||||
"scene.render.bake_samples",
|
||||
"scene.render.bake_type",
|
||||
"scene.render.bake_user_scale",
|
||||
"scene.render.use_bake_clear",
|
||||
"scene.render.use_bake_lores_mesh",
|
||||
"scene.render.use_bake_multires",
|
||||
"scene.render.use_bake_selected_to_active",
|
||||
"scene.render.use_bake_user_scale",
|
||||
)
|
||||
|
||||
# 25.11.07, Blender 5+
|
||||
if path == "scene.render.engine" and value in ("BLENDER_EEVEE", "BLENDER_EEVEE_NEXT"):
|
||||
if value == "BLENDER_EEVEE_NEXT":
|
||||
print(
|
||||
f"Warning: Value 'BLENDER_EEVEE_NEXT' is outdated for property '{path}' "
|
||||
"since Blender 5.0 and should be replaced with 'BLENDER_EEVEE' in shading_styles.json."
|
||||
)
|
||||
warning = True
|
||||
value = tool.Blender.get_eevee_name()
|
||||
elif tool.Blender.BLENDER_5 and path in BLENDER_5_REMOVED:
|
||||
print(
|
||||
f"Warning: Value 'BLENDER_EEVEE' is outdated for property '{path}' "
|
||||
"since Blender 4.2 and should be replaced with 'BLENDER_EEVEE_NEXT' in shading_styles.json."
|
||||
f"Warning: Property '{path}' is removed "
|
||||
"since Blender 5.0 and should be also removed from shading_styles.json."
|
||||
)
|
||||
warning = True
|
||||
skip = True
|
||||
# @25.11.11
|
||||
elif (
|
||||
path == "scene.display.shading.studio_light"
|
||||
and value == "Default"
|
||||
and style[VIEWPORT_SHADING_TYPE] in ("RENDERED", "MATERIAL")
|
||||
):
|
||||
value = "forest.exr"
|
||||
print(
|
||||
f"Warning: Value 'Default' for property '{path}' and "
|
||||
f"'{VIEWPORT_SHADING_TYPE}' = '{style[VIEWPORT_SHADING_TYPE]}' is outdated "
|
||||
"and should be replaced with 'forest.exr' in shading_styles.json."
|
||||
)
|
||||
warning = True
|
||||
# @25.05.12
|
||||
if path == "scene.display.shading.wireframe_color_type" and value == "MATERIAL":
|
||||
elif path == "scene.display.shading.wireframe_color_type" and value == "MATERIAL":
|
||||
value = "THEME"
|
||||
print(
|
||||
f"Warning: Value 'MATERIAL' is outdated for property '{path}' "
|
||||
@@ -2749,7 +2807,16 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
return path, value, warning, skip
|
||||
|
||||
for path, value in style.items():
|
||||
paths = list(style.keys())
|
||||
PRIORITY_PATHS = (
|
||||
# `scene.display.shading.studio_light` values depend on `.type`
|
||||
# so we got to set it first.
|
||||
VIEWPORT_SHADING_TYPE,
|
||||
)
|
||||
paths.sort(key=lambda p: p not in PRIORITY_PATHS)
|
||||
|
||||
for path in paths:
|
||||
value = style[path]
|
||||
path, value, warning, skip = preprocess(path, value)
|
||||
self.has_warnings_during_activation |= warning
|
||||
|
||||
@@ -3414,6 +3481,15 @@ class LoadDrawings(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
# Operator is not accessible through UI if IFC project is not saved,
|
||||
# but adding poll check to avoid using Drawings UI in scripts with unsaved project.
|
||||
if tool.Ifc.get_path():
|
||||
return True
|
||||
cls.poll_message_set("IFC project is not saved.")
|
||||
return False
|
||||
|
||||
def _execute(self, context):
|
||||
core.load_drawings(tool.Drawing)
|
||||
|
||||
|
||||
@@ -341,17 +341,29 @@ class SheetBuilder:
|
||||
assert style_data is not None
|
||||
text = ""
|
||||
brackets_level = 0
|
||||
selector_buffer = "" # Buffer to accumulate selectors across lines
|
||||
|
||||
for l in style_data:
|
||||
if l == "{":
|
||||
if brackets_level == 0:
|
||||
cur_line = text.splitlines()[-1]
|
||||
text = text[: -len(cur_line)]
|
||||
# Get all accumulated selector text (may span multiple lines)
|
||||
# Find where the last rule ended (after last }) or start of text
|
||||
last_close = text.rfind("}")
|
||||
if last_close == -1:
|
||||
selector_text = text
|
||||
text = ""
|
||||
else:
|
||||
selector_text = text[last_close + 1 :]
|
||||
text = text[: last_close + 1]
|
||||
|
||||
# Process all selectors (split by comma)
|
||||
css_selectors = []
|
||||
# making sure cases like "text, tspan" will be
|
||||
# converted to "text.prefix, tspan.prefix"
|
||||
for css_selector in cur_line.split(","):
|
||||
css_selector = f"{css_selector.strip()}.{prefix}"
|
||||
css_selectors.append(css_selector)
|
||||
for css_selector in selector_text.split(","):
|
||||
css_selector = css_selector.strip()
|
||||
if css_selector: # Only process non-empty selectors
|
||||
css_selector = f"{css_selector}.{prefix}"
|
||||
css_selectors.append(css_selector)
|
||||
|
||||
text += ", ".join(css_selectors) + " "
|
||||
brackets_level += 1
|
||||
elif l == "}":
|
||||
|
||||
@@ -202,6 +202,15 @@ class SvgWriter:
|
||||
) -> Self:
|
||||
self.precision = precision
|
||||
self.decimal_places = decimal_places
|
||||
|
||||
# Sort annotations to ensure FILLAREA types are drawn first (at the bottom)
|
||||
def sort_key(element):
|
||||
predefined_type = ifcopenshell.util.element.get_predefined_type(element)
|
||||
# Return 0 for FILLAREA to draw them first, 1 for everything else
|
||||
return 0 if predefined_type == "FILL_AREA" else 1
|
||||
|
||||
annotations = sorted(annotations, key=sort_key)
|
||||
|
||||
for element in annotations:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if (
|
||||
|
||||
@@ -116,7 +116,7 @@ class BIM_PT_camera(Panel):
|
||||
# See #6686.
|
||||
if (
|
||||
props.has_underlay
|
||||
and str(render.engine) == "BLENDER_EEVEE_NEXT"
|
||||
and str(render.engine) == tool.Blender.get_eevee_name()
|
||||
and ((megapixels := (render.resolution_x * render.resolution_y / 10**6)) > MEGAPIXELS_WARNING_THRESHOLD)
|
||||
):
|
||||
box = self.layout.box()
|
||||
|
||||
@@ -809,9 +809,17 @@ class OverrideDelete(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return len(context.selected_objects) > 0
|
||||
# Match `object.delete` poll for consistency.
|
||||
# `object.delete` poll just checks for OBJECT mode.
|
||||
poll = bpy.ops.object.delete.poll()
|
||||
if poll:
|
||||
return True
|
||||
cls.poll_message_set("Only available in OBJECT mode")
|
||||
return False
|
||||
|
||||
def execute(self, context):
|
||||
if not context.selected_objects:
|
||||
return {"FINISHED"}
|
||||
# Deep magick from the dawn of time
|
||||
if tool.Ifc.get() is None:
|
||||
bpy.ops.object.delete(use_global=self.use_global, confirm=self.confirm)
|
||||
@@ -823,6 +831,8 @@ class OverrideDelete(bpy.types.Operator):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def invoke(self, context, event):
|
||||
if not context.selected_objects:
|
||||
return {"FINISHED"}
|
||||
assert context.window_manager
|
||||
ifc_file = tool.Ifc.get()
|
||||
if ifc_file is None:
|
||||
@@ -1030,9 +1040,18 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context) -> bool:
|
||||
return len(getattr(context, "selected_ids", [])) > 0
|
||||
# Match `outliner.delete` poll for consistency.
|
||||
# `outliner.delete` just checks `area.type` == `OUTLINER`.
|
||||
poll = bpy.ops.outliner.delete.poll()
|
||||
if poll:
|
||||
return True
|
||||
cls.poll_message_set("Only available from Outliner.")
|
||||
return False
|
||||
|
||||
def execute(self, context):
|
||||
if len(getattr(context, "selected_ids", [])) == 0:
|
||||
return {"FINISHED"}
|
||||
|
||||
# In this override, we don't check self.hierarchy. This effectively
|
||||
# makes Delete and Delete Hierarchy identical. This is on purpose, since
|
||||
# non-hierarchical deletion may imply a whole bunch of potentially
|
||||
@@ -1051,6 +1070,9 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
if len(getattr(context, "selected_ids", [])) == 0:
|
||||
return {"FINISHED"}
|
||||
|
||||
assert context.window_manager
|
||||
ifc_file = tool.Ifc.get()
|
||||
if ifc_file:
|
||||
@@ -1121,10 +1143,18 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
is_interactive: bpy.props.BoolProperty(name="Is Interactive", default=True)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return len(context.selected_objects) > 0
|
||||
def poll(cls, context) -> bool:
|
||||
# Match `object.duplicate_move` poll for consistency.
|
||||
# `object.duplicate_move` poll checks for OBJECT mode.
|
||||
poll = bpy.ops.object.duplicate_move.poll()
|
||||
if poll:
|
||||
return True
|
||||
cls.poll_message_set("Only available in OBJECT mode")
|
||||
return False
|
||||
|
||||
def execute(self, context):
|
||||
if not context.selected_objects:
|
||||
return {"FINISHED"}
|
||||
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False)
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -1147,7 +1177,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
|
||||
@staticmethod
|
||||
def execute_ifc_duplicate_operator(operator: bpy.types.Operator, context: bpy.types.Context, linked: bool = False):
|
||||
objects_to_remove = set()
|
||||
objects_to_remove: set[bpy.types.Object] = set()
|
||||
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -1205,6 +1235,9 @@ class DuplicateMoveLinkedAggregateMacro(bpy.types.Macro):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
|
||||
OldToNewType = dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]
|
||||
|
||||
|
||||
class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
bl_idname = "bim.object_duplicate_move_linked_aggregate"
|
||||
bl_label = "IFC Duplicate and Move Linked Aggregate"
|
||||
@@ -1227,9 +1260,9 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
self.new_active_obj = None
|
||||
self.group_name = "BBIM_Linked_Aggregate"
|
||||
self.pset_name = "BBIM_Linked_Aggregate"
|
||||
old_to_new = {}
|
||||
all_old_to_new = {} # Track all duplicates created
|
||||
|
||||
def select_objects_and_add_data(element):
|
||||
def select_objects_and_add_data(element: ifcopenshell.entity_instance) -> None:
|
||||
add_linked_aggregate_group(element)
|
||||
obj = tool.Ifc.get_object(element)
|
||||
obj.select_set(True)
|
||||
@@ -1243,8 +1276,6 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
select_objects_and_add_data(part)
|
||||
else:
|
||||
index = add_linked_aggregate_pset(part, index)
|
||||
# index += 1
|
||||
|
||||
obj = tool.Ifc.get_object(part)
|
||||
obj.select_set(True)
|
||||
|
||||
@@ -1257,20 +1288,15 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
|
||||
if not pset:
|
||||
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name=self.pset_name)
|
||||
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
ifc_file,
|
||||
pset=pset,
|
||||
properties=properties,
|
||||
)
|
||||
|
||||
index += 1
|
||||
else:
|
||||
pass
|
||||
|
||||
return index
|
||||
|
||||
def add_linked_aggregate_group(element):
|
||||
def add_linked_aggregate_group(element: ifcopenshell.entity_instance) -> None:
|
||||
linked_aggregate_group = None
|
||||
product_groups_name = [
|
||||
r.RelatingGroup.Name
|
||||
@@ -1283,25 +1309,21 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
linked_aggregate_group = ifcopenshell.api.group.add_group(ifc_file, name=self.group_name)
|
||||
ifcopenshell.api.group.assign_group(ifc_file, products=[element], group=linked_aggregate_group)
|
||||
|
||||
def custom_incremental_naming_for_element_assembly(old_to_new):
|
||||
def custom_incremental_naming_for_element_assembly(old_to_new: OldToNewType) -> None:
|
||||
for new in old_to_new.values():
|
||||
if new[0].is_a("IfcElementAssembly"):
|
||||
group_elements: list[ifcopenshell.entity_instance] = next(
|
||||
r.RelatedObjects
|
||||
for r in getattr(new[0], "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
)
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate")
|
||||
new_obj = tool.Ifc.get_object(new[0])
|
||||
new_obj.name = pset["Name"] + "_" + str(pset["Aggregate_Index"])
|
||||
new_obj.name = f"{pset['Name']}_{pset['Aggregate_Index']:02d}"
|
||||
|
||||
def get_max_index(parts):
|
||||
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
|
||||
index = [i["Index"] for i in psets if i]
|
||||
if len(index) > 0:
|
||||
index = max(index)
|
||||
psets = [
|
||||
ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate")
|
||||
for p in parts
|
||||
if ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate")
|
||||
]
|
||||
if psets:
|
||||
index = max([i["Index"] for i in psets if i])
|
||||
return index
|
||||
else:
|
||||
return 0
|
||||
@@ -1315,23 +1337,28 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
]
|
||||
ifcopenshell.api.group.assign_group(ifc_file, group=linked_aggregate_group[0], products=new)
|
||||
|
||||
group_elements: list[ifcopenshell.entity_instance] = next(
|
||||
r.RelatedObjects
|
||||
for r in getattr(new[0], "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
)
|
||||
if linked_aggregate_group:
|
||||
ifcopenshell.api.group.assign_group(ifc_file, group=linked_aggregate_group[0], products=new)
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(old, "BBIM_Linked_Aggregate")
|
||||
if pset:
|
||||
new_pset = ifcopenshell.api.pset.add_pset(ifc_file, product=new[0], name=self.pset_name)
|
||||
if pset["Index"] == 0:
|
||||
group_elements = []
|
||||
if new[0].is_a("IfcElementAssembly"):
|
||||
group_elements = next(
|
||||
(
|
||||
r.RelatedObjects
|
||||
for r in getattr(new[0], "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
and "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
),
|
||||
[],
|
||||
)
|
||||
properties = {
|
||||
"Index": pset["Index"],
|
||||
"Name": pset["Name"],
|
||||
"Aggregate_Index": len(group_elements) - 1,
|
||||
"Aggregate_Index": len(group_elements) - 1 if group_elements else 0,
|
||||
}
|
||||
else:
|
||||
properties = {"Index": pset["Index"]}
|
||||
@@ -1351,43 +1378,73 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
location_diff = new_obj.location - base_obj_location
|
||||
new_obj.location = context.scene.cursor.location + location_diff
|
||||
|
||||
if len(context.selected_objects) != 1:
|
||||
# Process multiple selected aggregates
|
||||
selected_aggregates = []
|
||||
for obj in context.selected_objects:
|
||||
selected_element = tool.Ifc.get_entity(obj)
|
||||
if not selected_element:
|
||||
continue
|
||||
|
||||
# Find the aggregate element
|
||||
if selected_element.is_a("IfcElementAssembly"):
|
||||
selected_aggregates.append(selected_element)
|
||||
elif selected_element.Decomposes:
|
||||
if selected_element.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"):
|
||||
selected_aggregates.append(selected_element.Decomposes[0].RelatingObject)
|
||||
|
||||
# Remove duplicates
|
||||
selected_aggregates = list(set(selected_aggregates))
|
||||
|
||||
if not selected_aggregates:
|
||||
self.report({"INFO"}, "No Linked Aggregates selected.")
|
||||
return {"FINISHED"}
|
||||
|
||||
selected_obj = context.selected_objects[0]
|
||||
selected_element = tool.Ifc.get_entity(selected_obj)
|
||||
assert selected_element
|
||||
|
||||
if selected_element.is_a("IfcElementAssembly"):
|
||||
pass
|
||||
elif selected_element.Decomposes:
|
||||
if selected_element.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"):
|
||||
selected_element = selected_element.Decomposes[0].RelatingObject
|
||||
selected_obj = tool.Ifc.get_object(selected_element)
|
||||
else:
|
||||
self.report({"INFO"}, "Object is not part of a IfcElementAssembly.")
|
||||
return {"FINISHED"}
|
||||
|
||||
select_objects_and_add_data(selected_element)
|
||||
|
||||
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
|
||||
|
||||
tool.Root.recreate_aggregate(old_to_new)
|
||||
|
||||
copy_linked_aggregate_data(old_to_new)
|
||||
|
||||
custom_incremental_naming_for_element_assembly(old_to_new)
|
||||
|
||||
if location_from_3d_cursor:
|
||||
get_location_from_3d_cursor(old_to_new, selected_element)
|
||||
|
||||
# Deselect all first
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
new_aggregate = old_to_new[selected_element][0]
|
||||
tool.Blender.set_active_object(tool.Ifc.get_object(new_aggregate))
|
||||
|
||||
# Process each selected aggregate
|
||||
for aggregate in selected_aggregates:
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
|
||||
# Select and prepare the aggregate for duplication
|
||||
select_objects_and_add_data(aggregate)
|
||||
|
||||
# Duplicate the aggregate
|
||||
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
|
||||
all_old_to_new.update(old_to_new) # Collect all duplicates
|
||||
|
||||
# Recreate aggregate structure
|
||||
tool.Root.recreate_aggregate(old_to_new)
|
||||
|
||||
# Copy linked aggregate data
|
||||
copy_linked_aggregate_data(old_to_new)
|
||||
|
||||
# Apply custom naming
|
||||
custom_incremental_naming_for_element_assembly(old_to_new)
|
||||
|
||||
# Apply 3D cursor location if requested
|
||||
if location_from_3d_cursor:
|
||||
get_location_from_3d_cursor(old_to_new, aggregate)
|
||||
|
||||
# Deselect for next iteration
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
|
||||
# Select all newly created aggregates
|
||||
for aggregate in selected_aggregates:
|
||||
if aggregate in all_old_to_new:
|
||||
new_aggregate = all_old_to_new[aggregate][0]
|
||||
new_aggregate_obj = tool.Ifc.get_object(new_aggregate)
|
||||
if new_aggregate_obj:
|
||||
new_aggregate_obj.select_set(True)
|
||||
|
||||
# Set active object to the first new aggregate
|
||||
if all_old_to_new:
|
||||
first_new_aggregate = next(iter(all_old_to_new.values()))[0]
|
||||
context.view_layer.objects.active = tool.Ifc.get_object(first_new_aggregate)
|
||||
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
|
||||
return old_to_new
|
||||
return all_old_to_new
|
||||
|
||||
|
||||
class DuplicateLinkedAggregateTo3dCursor(bpy.types.Operator):
|
||||
@@ -1423,7 +1480,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.pset_name = "BBIM_Linked_Aggregate"
|
||||
refresh_start_time = time()
|
||||
old_to_new = {}
|
||||
original_data: dict[int, dict[int, str]] = {}
|
||||
original_data: dict[int, dict[int, dict[str, Any]]] = {}
|
||||
|
||||
def delete_objects(element: ifcopenshell.entity_instance) -> None:
|
||||
"""Remove IfcElementAssembly and it's parts."""
|
||||
@@ -1469,8 +1526,8 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
def get_original_data(element: ifcopenshell.entity_instance) -> dict[int, dict[int, str]]:
|
||||
group = next(
|
||||
def get_original_data(element: ifcopenshell.entity_instance) -> dict[int, dict[int, dict[str, Any]]]:
|
||||
group: int = next(
|
||||
r.RelatingGroup
|
||||
for r in getattr(element, "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
@@ -1478,8 +1535,8 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
).id()
|
||||
original_data[group] = {}
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
|
||||
index = pset["Index"]
|
||||
pset: dict[str, Any] = ifcopenshell.util.element.get_pset(element, self.pset_name)
|
||||
index: int = pset["Index"]
|
||||
annotations = get_assignments(element)
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
original_data[group][index] = {
|
||||
@@ -1493,6 +1550,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if parts:
|
||||
for part in parts:
|
||||
if part.is_a("IfcElementAssembly"):
|
||||
# TODO: unused expression.
|
||||
original_data | get_original_data(part)
|
||||
else:
|
||||
try:
|
||||
|
||||
@@ -96,11 +96,12 @@ def update_shadow_mode(self: "BIMSolarProperties", context: bpy.types.Context) -
|
||||
sun_props = tool.Blender.get_sun_props()
|
||||
assert sun_props
|
||||
if sun_props.sun_object is None:
|
||||
bpy.ops.object.light_add(type="SUN", radius=1, align="WORLD", location=(0, 0, 0), scale=(1, 1, 1))
|
||||
bpy.ops.object.move_to_collection(collection_index=0)
|
||||
sun_props.sun_object = bpy.context.active_object
|
||||
light = bpy.data.lights.new(name="Sun", type="SUN")
|
||||
sun = bpy.data.objects.new("Sun", light)
|
||||
context.scene.collection.objects.link(sun)
|
||||
sun_props.sun_object = sun
|
||||
update_sun_path(self)
|
||||
context.scene.render.engine = "BLENDER_EEVEE_NEXT"
|
||||
context.scene.render.engine = tool.Blender.get_eevee_name()
|
||||
assert context.scene.display
|
||||
assert context.scene.display.shading
|
||||
context.scene.display.shading.light = "FLAT"
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import calendar
|
||||
import bpy
|
||||
import bonsai.tool as tool
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -192,24 +193,7 @@ class BIM_PT_solar(bpy.types.Panel):
|
||||
row = self.layout.row()
|
||||
row.prop(props, "year")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(
|
||||
props,
|
||||
"month",
|
||||
text={
|
||||
1: "January",
|
||||
2: "February",
|
||||
3: "March",
|
||||
4: "April",
|
||||
5: "May",
|
||||
6: "June",
|
||||
7: "July",
|
||||
8: "August",
|
||||
9: "September",
|
||||
10: "October",
|
||||
11: "November",
|
||||
12: "December",
|
||||
}[props.month],
|
||||
)
|
||||
row.prop(props, "month", text=calendar.month_name[props.month])
|
||||
row.prop(props, "day")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
@@ -348,24 +348,136 @@ class BIMArrayProperties(PropertyGroup):
|
||||
|
||||
|
||||
def update_total_length_target(self: "BIMStairProperties", context: bpy.types.Context) -> None:
|
||||
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
|
||||
"""Update tread_run when total_length_target changes"""
|
||||
# Calculate available length for default treads
|
||||
available_length = self.total_length_target
|
||||
n_default_treads = self.number_of_treads + 1 # number of risers
|
||||
|
||||
# Subtract custom first tread if not locked and not zero
|
||||
if not self.custom_tread_lock and self.custom_first_last_tread_run[0] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
|
||||
# Subtract custom last tread if not locked and not zero
|
||||
if not self.custom_tread_lock and self.custom_first_last_tread_run[1] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
# Calculate tread_run for remaining treads
|
||||
if n_default_treads > 0:
|
||||
self["tread_run"] = available_length / n_default_treads
|
||||
else:
|
||||
# All treads are custom, just use target length
|
||||
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
|
||||
|
||||
|
||||
def update_tread_run(self: "BIMStairProperties", context: bpy.types.Context) -> None:
|
||||
"""Update either number_of_treads or total_length_target when tread_run changes"""
|
||||
if self.total_length_lock:
|
||||
self["number_of_treads"] = int((self.total_length_target / self.tread_run) - 1)
|
||||
# Calculate how much length custom treads take up
|
||||
custom_length = 0
|
||||
n_custom_treads = 0
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
custom_length += self.custom_first_last_tread_run[0]
|
||||
n_custom_treads += 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
custom_length += self.custom_first_last_tread_run[1]
|
||||
n_custom_treads += 1
|
||||
|
||||
# Calculate how many default treads fit in remaining space
|
||||
available_length = self.total_length_target - custom_length
|
||||
if self.tread_run > 0:
|
||||
n_default_treads = available_length / self.tread_run
|
||||
total_treads = n_default_treads + n_custom_treads
|
||||
# number_of_treads = number_of_risers - 1
|
||||
self["number_of_treads"] = int(total_treads - 1)
|
||||
else:
|
||||
self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run
|
||||
# Calculate total length from current settings
|
||||
n_default_treads = self.number_of_treads + 1
|
||||
total_length = 0
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
total_length += self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
total_length += self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
total_length += n_default_treads * self.tread_run
|
||||
self["total_length_target"] = total_length
|
||||
|
||||
|
||||
def update_number_of_treads(self: "BIMStairProperties", context: bpy.types.Context) -> None:
|
||||
"""Update either tread_run or total_length_target when number_of_treads changes"""
|
||||
if self.total_length_lock:
|
||||
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
|
||||
# Calculate available length for default treads
|
||||
available_length = self.total_length_target
|
||||
n_default_treads = self.number_of_treads + 1
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
if n_default_treads > 0:
|
||||
self["tread_run"] = available_length / n_default_treads
|
||||
else:
|
||||
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
|
||||
else:
|
||||
self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run
|
||||
# Calculate total length from current settings
|
||||
n_default_treads = self.number_of_treads + 1
|
||||
total_length = 0
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
total_length += self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
total_length += self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
total_length += n_default_treads * self.tread_run
|
||||
self["total_length_target"] = total_length
|
||||
|
||||
|
||||
StairType = Literal["CONCRETE", "WOOD/STEEL", "GENERIC"]
|
||||
def update_custom_first_last_tread_run(self: "BIMStairProperties", context: bpy.types.Context) -> None:
|
||||
"""Update tread_run or total_length when custom treads change"""
|
||||
if self.total_length_lock:
|
||||
# Recalculate tread_run to maintain total length
|
||||
available_length = self.total_length_target
|
||||
n_default_treads = self.number_of_treads + 1
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
if n_default_treads > 0:
|
||||
self["tread_run"] = available_length / n_default_treads
|
||||
else:
|
||||
# Recalculate total length
|
||||
n_default_treads = self.number_of_treads + 1
|
||||
total_length = 0
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
total_length += self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
total_length += self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
total_length += n_default_treads * self.tread_run
|
||||
self["total_length_target"] = total_length
|
||||
|
||||
|
||||
class BIMStairProperties(PropertyGroup):
|
||||
@@ -408,7 +520,7 @@ class BIMStairProperties(PropertyGroup):
|
||||
has_top_nib: bpy.props.BoolProperty(name="Has Top Nib", default=True)
|
||||
stair_type: bpy.props.EnumProperty(
|
||||
name="Stair Type",
|
||||
items=[(i, i.replace("/", " / ").title(), "") for i in get_args(StairType)],
|
||||
items=[(i, i.replace("/", " / ").title(), "") for i in get_args(tool.Model.StairType)],
|
||||
default="CONCRETE",
|
||||
update=validate_nosing_value,
|
||||
)
|
||||
@@ -425,6 +537,7 @@ class BIMStairProperties(PropertyGroup):
|
||||
min=0,
|
||||
unit="LENGTH",
|
||||
size=2,
|
||||
update=update_custom_first_last_tread_run, # Added update callback
|
||||
)
|
||||
nosing_length: bpy.props.FloatProperty(
|
||||
name="Nosing Length",
|
||||
@@ -492,16 +605,18 @@ class BIMStairProperties(PropertyGroup):
|
||||
}
|
||||
stair_kwargs.update(generic_props)
|
||||
|
||||
non_si_units_props = self.non_si_units_props
|
||||
# If locked, use tread_run for both first and last treads
|
||||
if self.custom_tread_lock:
|
||||
stair_kwargs["custom_first_last_tread_run"] = (self.tread_run, self.tread_run)
|
||||
non_si_units_props += ("custom_first_last_tread_run",)
|
||||
stair_kwargs["custom_first_last_tread_run"] = (None, None)
|
||||
else:
|
||||
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
|
||||
|
||||
if not convert_to_project_units:
|
||||
return stair_kwargs
|
||||
|
||||
stair_kwargs = tool.Model.convert_data_to_project_units(stair_kwargs, self.non_si_units_props)
|
||||
stair_kwargs = tool.Model.convert_data_to_project_units(stair_kwargs, non_si_units_props)
|
||||
return stair_kwargs
|
||||
|
||||
def get_props_kwargs_for_ifc_export(self, convert_to_project_units=False, stair_type=None):
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import bl_ui_utils.layout
|
||||
import bonsai.bim
|
||||
import bonsai.tool as tool
|
||||
from bpy.types import Panel, Menu
|
||||
@@ -36,7 +35,12 @@ from bonsai.bim.module.model.stair import regenerate_stair_mesh
|
||||
from bonsai.bim.module.model.railing import update_railing_modifier_bmesh
|
||||
from bonsai.bim.module.model.roof import update_roof_modifier_bmesh
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING or bpy.app.version >= (5, 0, 0):
|
||||
import _bl_ui_utils.layout as bl_ui_utils_layout
|
||||
else:
|
||||
import bl_ui_utils.layout as bl_ui_utils_layout
|
||||
|
||||
|
||||
class BIM_MT_type_manager_menu(bpy.types.Menu):
|
||||
@@ -56,7 +60,7 @@ class BIM_MT_type_menu(bpy.types.Menu):
|
||||
def draw(self, context):
|
||||
props = tool.Model.get_model_props()
|
||||
layout = self.layout
|
||||
with bl_ui_utils.layout.operator_context(layout, "INVOKE_REGION_WIN"):
|
||||
with bl_ui_utils_layout.operator_context(layout, "INVOKE_REGION_WIN"):
|
||||
op = layout.operator("bim.rename_type", icon="GREASEPENCIL", text="Rename Type")
|
||||
op.element = props.menu_relating_type_id
|
||||
op = layout.operator("bim.select_type", icon="OBJECT_DATA")
|
||||
|
||||
@@ -1322,6 +1322,20 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not bpy.context.selected_objects:
|
||||
return
|
||||
if self.active_material_usage == "LAYER2":
|
||||
if len(bpy.context.selected_objects) != 2:
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
"Exactly two LAYER2 items (walls, railings, etc) must be selected to perform a merge.",
|
||||
)
|
||||
return
|
||||
for item in bpy.context.selected_objects:
|
||||
element = tool.Ifc.get_entity(item)
|
||||
if tool.Model.get_usage_type(element) != "LAYER2":
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
"Both selected items must be LAYER2 (walls, railings, etc) to perform a merge.",
|
||||
)
|
||||
return
|
||||
bpy.ops.bim.merge_wall()
|
||||
else:
|
||||
if len(bpy.context.selected_objects) == 1:
|
||||
|
||||
@@ -859,8 +859,23 @@ def get_gross_top_area(obj: bpy.types.Object, angle: float = 45) -> float:
|
||||
|
||||
entity = ifc.by_guid(opening_id)
|
||||
open_obj = tool.Ifc.get_object(entity)
|
||||
assert isinstance(open_obj, bpy.types.Object)
|
||||
opening_area += get_net_top_area(open_obj, angle=angle)
|
||||
|
||||
# Check if the opening has a Blender object representation
|
||||
if open_obj is None:
|
||||
# If no Blender object exists, create a temporary mesh from the IFC geometry
|
||||
try:
|
||||
mesh = get_gross_element_mesh(entity)
|
||||
temp_obj = bpy.data.objects.new("TempOpening", mesh)
|
||||
opening_area += get_net_top_area(temp_obj, angle=angle)
|
||||
delete_obj(temp_obj)
|
||||
delete_mesh(mesh)
|
||||
except Exception as e:
|
||||
# If we can't create geometry, skip this opening
|
||||
print(f"Warning: Could not calculate opening area for {opening_id}: {e}")
|
||||
continue
|
||||
else:
|
||||
# Opening has a Blender representation, use it directly
|
||||
opening_area += get_net_top_area(open_obj, angle=angle)
|
||||
else:
|
||||
continue
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import ifcopenshell.api
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.qto as core
|
||||
from bonsai.bim.module.qto import helper
|
||||
from ifcopenshell.util.profiler import Profiler
|
||||
|
||||
|
||||
class CalculateCircleRadius(bpy.types.Operator):
|
||||
@@ -148,7 +149,8 @@ class CalculateSingleQuantity(bpy.types.Operator, tool.Ifc.Operator):
|
||||
}
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
results = ifc5d.qto.quantify(ifc_file, elements, rules)
|
||||
with Profiler("Quantify function time:"):
|
||||
results = ifc5d.qto.quantify(ifc_file, elements, rules)
|
||||
ifc5d.qto.edit_qtos(ifc_file, results)
|
||||
|
||||
not_quantified_elements = elements - set(results.keys())
|
||||
@@ -190,7 +192,8 @@ class PerformQuantityTakeOff(bpy.types.Operator, tool.Ifc.Operator):
|
||||
) -> set[ifcopenshell.entity_instance]:
|
||||
rules = ifc5d.qto.rules[rule]
|
||||
ifc_file = tool.Ifc.get()
|
||||
results = ifc5d.qto.quantify(ifc_file, elements, rules)
|
||||
with Profiler("Quantify function time:"):
|
||||
results = ifc5d.qto.quantify(ifc_file, elements, rules)
|
||||
ifc5d.qto.edit_qtos(ifc_file, results)
|
||||
not_quantified_elements = elements - set(results.keys())
|
||||
return not_quantified_elements
|
||||
|
||||
@@ -21,6 +21,7 @@ import ifcopenshell.util.element
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.spatial as core
|
||||
import bonsai.bim.handler
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
class ReferenceStructure(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -233,7 +234,7 @@ class RemoveContainer(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class CopyToContainer(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"""
|
||||
Copies selected 3D elements in the viewport to the selected spatial containers
|
||||
Copies selected 3D elements in the viewport to the container selected in Spatial Manager.
|
||||
|
||||
Example: bulk copy a wall to multiple storeys
|
||||
|
||||
@@ -242,9 +243,13 @@ class CopyToContainer(bpy.types.Operator, tool.Ifc.Operator):
|
||||
Copying containers to other containers currently is not supported."""
|
||||
|
||||
bl_idname = "bim.copy_to_container"
|
||||
bl_label = "Copy To Container"
|
||||
bl_label = "Copy to Container"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
container: bpy.props.IntProperty()
|
||||
|
||||
container: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
container: int
|
||||
|
||||
def _execute(self, context):
|
||||
objs = tool.Spatial.get_selected_objects_without_containers()
|
||||
|
||||
@@ -455,7 +455,7 @@ class ActivateExternalStyle(bpy.types.Operator):
|
||||
return {"CANCELLED"}
|
||||
|
||||
self.copy_material_attributes(db["data_block"], material)
|
||||
if material.use_nodes:
|
||||
if tool.Style.get_use_nodes(material):
|
||||
tool.Blender.copy_node_graph(material, db["data_block"])
|
||||
bpy.data.materials.remove(db["data_block"])
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -99,7 +99,8 @@ class BIMSystemProperties(PropertyGroup):
|
||||
systems: CollectionProperty(name="Systems", type=System)
|
||||
expanded_groups_json: StringProperty(name="Expanded Systems JSON", default="[]")
|
||||
"""See `expanded_groups_json`. We name it "groups" for compatibility with Group UI code."""
|
||||
active_system_index: IntProperty(name="Active System Index")
|
||||
# Named not as `active_system_index` to match `BIMGroupProperties`.
|
||||
active_group_index: IntProperty(name="Active System Index")
|
||||
active_system_id: IntProperty(name="Active System Id")
|
||||
edited_system_id: IntProperty(name="Edited System Id")
|
||||
system_class: EnumProperty(items=get_system_class, name="Class")
|
||||
@@ -113,7 +114,7 @@ class BIMSystemProperties(PropertyGroup):
|
||||
is_adding: bool
|
||||
systems: bpy.types.bpy_prop_collection_idprop[System]
|
||||
expanded_groups_json: str
|
||||
active_system_index: int
|
||||
active_group_index: int
|
||||
active_system_id: int
|
||||
edited_system_id: int
|
||||
system_class: str
|
||||
@@ -121,7 +122,7 @@ class BIMSystemProperties(PropertyGroup):
|
||||
|
||||
@property
|
||||
def active_system_ui_item(self) -> Union[System, None]:
|
||||
return tool.Blender.get_active_uilist_element(self.systems, self.active_system_index)
|
||||
return tool.Blender.get_active_uilist_element(self.systems, self.active_group_index)
|
||||
|
||||
|
||||
def update_active_zone_index(self: "BIMZoneProperties", context: object) -> None:
|
||||
|
||||
@@ -114,7 +114,7 @@ class BIM_PT_systems(Panel):
|
||||
self.props,
|
||||
"systems",
|
||||
self.props,
|
||||
"active_system_index",
|
||||
"active_group_index",
|
||||
)
|
||||
|
||||
if self.props.edited_system_id:
|
||||
|
||||
@@ -786,7 +786,7 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
|
||||
material = bpy.data.materials.get("Section Override")
|
||||
if not material:
|
||||
material = bpy.data.materials.new("Section Override")
|
||||
material.use_nodes = True
|
||||
tool.Style.set_use_nodes(material, True)
|
||||
|
||||
props = tool.Blender.get_bim_props()
|
||||
if props.should_section_selected_objects:
|
||||
@@ -809,7 +809,7 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
|
||||
def override_materials(self):
|
||||
override = bpy.data.node_groups.get("Section Override")
|
||||
for material in bpy.data.materials:
|
||||
material.use_nodes = True
|
||||
tool.Style.set_use_nodes(material, True)
|
||||
if material.node_tree.nodes.get("Section Override"):
|
||||
continue
|
||||
# In EEVEE rendering engine, `blend_mode` is deprecated and replaced by `surface_render_method`
|
||||
|
||||
@@ -20,7 +20,12 @@ import ifcopenshell
|
||||
import ifcopenshell.util.pset
|
||||
import bonsai.tool as tool
|
||||
import bpy
|
||||
import bpy_restrict_state
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING or bpy.app.version >= (5, 0, 0):
|
||||
import _bpy_restrict_state as bpy_restrict_state
|
||||
else:
|
||||
import bpy_restrict_state
|
||||
|
||||
|
||||
class IfcSchema:
|
||||
|
||||
@@ -60,10 +60,21 @@ def edit_assigned_product(
|
||||
) -> None:
|
||||
element = ifc.get_entity(obj)
|
||||
assert element
|
||||
existing_product = drawing.get_assigned_product(element)
|
||||
if existing_product != product:
|
||||
if existing_product:
|
||||
|
||||
# Get ALL existing products, not just one
|
||||
existing_products = []
|
||||
if hasattr(element, "HasAssignments"):
|
||||
for rel in element.HasAssignments:
|
||||
if rel.is_a("IfcRelAssignsToProduct"):
|
||||
existing_products.append(rel.RelatingProduct)
|
||||
|
||||
# Only proceed if the assignment is different
|
||||
if not (len(existing_products) == 1 and existing_products[0] == product):
|
||||
# Unassign from ALL existing products
|
||||
for existing_product in existing_products:
|
||||
ifc.run("drawing.unassign_product", relating_product=existing_product, related_object=element)
|
||||
|
||||
# Assign to the new product
|
||||
if product:
|
||||
ifc.run("drawing.assign_product", relating_product=product, related_object=element)
|
||||
|
||||
|
||||
@@ -308,6 +308,7 @@ class Document:
|
||||
class Drawing:
|
||||
def activate_drawing(cls, camera): pass
|
||||
def add_literal(cls, **attributes): pass
|
||||
def clear_annotation_relationships(cls, drawing): pass
|
||||
def copy_representation(cls, source, dest): pass
|
||||
def create_annotation_context(cls, target_view, object_type=None): pass
|
||||
def create_annotation_object(cls, drawing, object_type): pass
|
||||
@@ -485,6 +486,19 @@ class Georeference:
|
||||
def xyz2enh(cls, coordinates): pass
|
||||
|
||||
|
||||
@interface
|
||||
class Group:
|
||||
def get_group_props(cls): pass
|
||||
def get_groups_data(cls, group_type): pass
|
||||
def import_groups(cls, group_type): pass
|
||||
def enable_group_editing_ui(cls) : pass
|
||||
def disable_group_editing_ui(cls): pass
|
||||
def disable_editing_group(cls): pass
|
||||
def set_active_group_to_edit(cls, group): pass
|
||||
def toggle_group(cls, group, group_type, option): pass
|
||||
def update_uilist_index(cls, group_type): pass
|
||||
|
||||
|
||||
@interface
|
||||
class Ifc:
|
||||
def get(cls): pass
|
||||
|
||||
@@ -100,6 +100,7 @@ class Blender(bonsai.core.tool.Blender):
|
||||
- (identifier, name, description, icon, number)
|
||||
"""
|
||||
BLENDER_ENUM_ITEMS = Iterable[BLENDER_ENUM_ITEM]
|
||||
BLENDER_5 = bpy.app.version >= (5, 0, 0)
|
||||
|
||||
@classmethod
|
||||
def activate_camera(cls, obj: bpy.types.Object) -> None:
|
||||
@@ -416,7 +417,7 @@ class Blender(bonsai.core.tool.Blender):
|
||||
cls, blender_material: bpy.types.Material, node_type: str, kwargs: Optional[dict] = {}
|
||||
) -> Union[bpy.types.ShaderNode, None]:
|
||||
"""returns first node from the `blender_material` shader graph with type `node_type`"""
|
||||
if not blender_material.use_nodes:
|
||||
if not tool.Style.get_use_nodes(blender_material):
|
||||
return
|
||||
nodes = blender_material.node_tree.nodes
|
||||
for node in nodes:
|
||||
@@ -709,7 +710,7 @@ class Blender(bonsai.core.tool.Blender):
|
||||
|
||||
new_selected_objects = [o for o in selected_objects if cls.is_valid_data_block(o) and o in view_layer_objects]
|
||||
|
||||
if active_object and (not cls.is_valid_data_block(active_object) or active_object not in view_layer_objects):
|
||||
if active_object and not cls.is_valid_data_block(active_object):
|
||||
active_object = None
|
||||
|
||||
return context, active_object, new_selected_objects
|
||||
@@ -2083,3 +2084,14 @@ class Blender(bonsai.core.tool.Blender):
|
||||
assert bpy.context.preferences
|
||||
if props_updated and bpy.context.preferences.use_preferences_save:
|
||||
bpy.ops.wm.save_userpref()
|
||||
|
||||
@classmethod
|
||||
def get_eevee_name(cls) -> Literal["BLENDER_EEVEE"] | Literal["BLENDER_EEVEE_NEXT"]:
|
||||
"""Convenience method to get correct eevee render engine name in multiple Blender versions.
|
||||
|
||||
In Blender 4.2 eevee was renamed to "eevee next".
|
||||
In Blender 5 eevee next is now just "eevee" again.
|
||||
"""
|
||||
if cls.BLENDER_5:
|
||||
return "BLENDER_EEVEE"
|
||||
return "BLENDER_EEVEE_NEXT"
|
||||
|
||||
@@ -1969,7 +1969,8 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
@classmethod
|
||||
def get_default_annotation_matrix(cls, camera, matrix_world=None):
|
||||
if matrix_world is None:
|
||||
matrix_world = camera.matrix_world.copy()
|
||||
# Use normalized camera matrix (without scale) for RCP compatibility
|
||||
matrix_world = cls.get_camera_matrix(camera)
|
||||
annotation_offset = Vector((0, 0, -camera.data.clip_start - 0.05))
|
||||
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
|
||||
matrix_world.translation += annotation_offset
|
||||
|
||||
@@ -1146,8 +1146,9 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
def record_object_position(cls, obj: bpy.types.Object) -> None:
|
||||
# These are recorded separately because they have different numerical tolerances
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
props.location_checksum = repr(np.array(obj.matrix_world.translation).tobytes())
|
||||
props.rotation_checksum = repr(np.array(obj.matrix_world.to_3x3()).tobytes())
|
||||
# Explicit dtype for Blender <5.0 compatibility.
|
||||
props.location_checksum = repr(np.array(obj.matrix_world.translation, dtype=np.float32).tobytes())
|
||||
props.rotation_checksum = repr(np.array(obj.matrix_world.to_3x3(), dtype=np.float32).tobytes())
|
||||
|
||||
@classmethod
|
||||
def remove_connection(cls, connection: ifcopenshell.entity_instance) -> None:
|
||||
@@ -1261,7 +1262,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
if tool.Ifc.get().schema == "IFC2X3":
|
||||
return False
|
||||
for slot in obj.material_slots:
|
||||
if slot.material and slot.material.use_nodes:
|
||||
if slot.material and tool.Style.get_use_nodes(slot.material):
|
||||
for node in slot.material.node_tree.nodes:
|
||||
if node.type == "TEX_COORD" and node.outputs["UV"].links:
|
||||
return True
|
||||
@@ -2020,7 +2021,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
continue
|
||||
if not (style := tool.Ifc.get_entity(material)):
|
||||
style = ifcopenshell.api.style.add_style(ifc_file, name=material.name)
|
||||
if material.use_nodes:
|
||||
if tool.Style.get_use_nodes(material):
|
||||
ifc_class = "IfcSurfaceStyleRendering"
|
||||
attributes = tool.Style.get_surface_rendering_attributes(material)
|
||||
else:
|
||||
|
||||
@@ -110,11 +110,11 @@ class Ifc(bonsai.core.tool.Ifc):
|
||||
oprops = tool.Blender.get_object_bim_props(obj)
|
||||
if not oprops.location_checksum:
|
||||
return True # Let's be conservative
|
||||
loc_check = np.frombuffer(eval(oprops.location_checksum))
|
||||
loc_check = np.frombuffer(eval(oprops.location_checksum), dtype=np.float32)
|
||||
loc_real = np.array(obj.matrix_world.translation).flatten()
|
||||
if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm
|
||||
return True
|
||||
rot_check = np.frombuffer(eval(oprops.rotation_checksum)).reshape(3, 3)
|
||||
rot_check = np.frombuffer(eval(oprops.rotation_checksum), dtype=np.float32).reshape(3, 3)
|
||||
rot_real = np.array(obj.matrix_world.to_3x3())
|
||||
rot_dot = np.dot(rot_check, rot_real.T)
|
||||
angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1))
|
||||
|
||||
@@ -116,7 +116,7 @@ class Loader(bonsai.core.tool.Loader):
|
||||
if transparency := surface_style.get("Transparency", None):
|
||||
alpha = 1 - transparency
|
||||
blender_material.diffuse_color = surface_style["SurfaceColour"] + (alpha,)
|
||||
blender_material.use_nodes = False
|
||||
tool.Style.set_use_nodes(blender_material, False)
|
||||
|
||||
@classmethod
|
||||
def restart_material_node_tree(cls, blender_material: bpy.types.Material) -> None:
|
||||
@@ -204,7 +204,7 @@ class Loader(bonsai.core.tool.Loader):
|
||||
|
||||
# TODO: reset pins to default values if no values passed
|
||||
if reflectance_method in ["PHYSICAL", "NOTDEFINED"]:
|
||||
blender_material.use_nodes = True
|
||||
tool.Style.set_use_nodes(blender_material, True)
|
||||
cls.restart_material_node_tree(blender_material)
|
||||
bsdf = tool.Blender.get_material_node(blender_material, "BSDF_PRINCIPLED")
|
||||
assert bsdf
|
||||
@@ -231,7 +231,7 @@ class Loader(bonsai.core.tool.Loader):
|
||||
blender_material.blend_method = "BLEND"
|
||||
|
||||
elif reflectance_method == "FLAT":
|
||||
blender_material.use_nodes = True
|
||||
tool.Style.set_use_nodes(blender_material, True)
|
||||
cls.restart_material_node_tree(blender_material)
|
||||
|
||||
output = tool.Blender.get_material_node(blender_material, "OUTPUT_MATERIAL")
|
||||
|
||||
+150
-46
@@ -44,11 +44,8 @@ import mathutils
|
||||
from math import atan, cos, degrees, pi, radians
|
||||
from mathutils import Matrix, Vector
|
||||
from copy import deepcopy
|
||||
from functools import partial
|
||||
from bonsai.bim import import_ifc
|
||||
|
||||
from bonsai.bim.module.model.data import AuthoringData, RailingData, RoofData, WindowData, DoorData
|
||||
from bonsai.bim.module.model.opening import FilledOpeningGenerator
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder, np_to_3d
|
||||
from typing import Optional, Union, TypeVar, Any, Literal, TYPE_CHECKING, TypedDict, assert_never
|
||||
from collections.abc import Iterable, Sequence
|
||||
@@ -1103,6 +1100,7 @@ class Model(bonsai.core.tool.Model):
|
||||
return
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
from bonsai.bim.module.model.data import AuthoringData
|
||||
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if not obj:
|
||||
@@ -1208,6 +1206,8 @@ class Model(bonsai.core.tool.Model):
|
||||
|
||||
Allows postponing the thumbnail update until it is actually needed by the user.
|
||||
"""
|
||||
from bonsai.bim.module.model.data import AuthoringData
|
||||
|
||||
element_id = element.id()
|
||||
if element_id not in AuthoringData.type_thumbnails:
|
||||
return
|
||||
@@ -1311,23 +1311,34 @@ class Model(bonsai.core.tool.Model):
|
||||
|
||||
@classmethod
|
||||
def is_parametric_roof_active(cls) -> bool:
|
||||
from bonsai.bim.module.model.data import RoofData
|
||||
|
||||
return bool((RoofData.is_loaded or not RoofData.load()) and RoofData.data["pset_data"])
|
||||
|
||||
@classmethod
|
||||
def is_parametric_railing_active(cls) -> bool:
|
||||
from bonsai.bim.module.model.data import RailingData
|
||||
|
||||
return bool((RailingData.is_loaded or not RailingData.load()) and RailingData.data["pset_data"])
|
||||
|
||||
@classmethod
|
||||
def is_parametric_window_active(cls) -> bool:
|
||||
from bonsai.bim.module.model.data import WindowData
|
||||
|
||||
return bool((WindowData.is_loaded or not WindowData.load()) and WindowData.data["pset_data"])
|
||||
|
||||
@classmethod
|
||||
def is_parametric_door_active(cls) -> bool:
|
||||
from bonsai.bim.module.model.data import DoorData
|
||||
|
||||
return bool((DoorData.is_loaded or not DoorData.load()) and DoorData.data["pset_data"])
|
||||
|
||||
CustomTreadRunType = Union[tuple[float, float], tuple[None, None]]
|
||||
|
||||
@classmethod
|
||||
def get_active_stair_calculated_params(cls, pset_data: Optional[dict[str, Any]] = None) -> dict[str, Any]:
|
||||
props = bpy.context.active_object.BIMStairProperties
|
||||
assert (obj := bpy.context.active_object)
|
||||
props = tool.Model.get_stair_props(obj)
|
||||
|
||||
if props.is_editing:
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
@@ -1338,38 +1349,48 @@ class Model(bonsai.core.tool.Model):
|
||||
last_tread_run = props.custom_first_last_tread_run[1] / si_conversion
|
||||
nosing_length = props.nosing_length / si_conversion
|
||||
else:
|
||||
number_of_treads = pset_data["number_of_treads"]
|
||||
height = pset_data["height"]
|
||||
tread_run = pset_data["tread_run"]
|
||||
assert pset_data
|
||||
number_of_treads: int = pset_data["number_of_treads"]
|
||||
height: float = pset_data["height"]
|
||||
tread_run: float = pset_data["tread_run"]
|
||||
# use .get to not break the old .ifc models
|
||||
custom_first_last_tread_run = pset_data.get("custom_first_last_tread_run", (0, 0))
|
||||
custom_first_last_tread_run: tool.Model.CustomTreadRunType = pset_data.get(
|
||||
"custom_first_last_tread_run", (0, 0)
|
||||
)
|
||||
first_tread_run, last_tread_run = custom_first_last_tread_run
|
||||
nosing_length = pset_data.get("nosing_length", 0)
|
||||
|
||||
calculated_params = {}
|
||||
calculated_params: dict[str, Any] = {}
|
||||
number_of_rises = number_of_treads + 1
|
||||
calculated_params["Number of Risers"] = number_of_rises
|
||||
calculated_params["Tread Rise"] = round(height / number_of_rises, 5)
|
||||
|
||||
# calculate stair length
|
||||
# Start with all treads using default tread_run
|
||||
n_default_tread_runs = number_of_rises
|
||||
length = 0
|
||||
if first_tread_run != 0:
|
||||
length = 0.0
|
||||
|
||||
# If first tread has custom width (non-None), use it instead of default.
|
||||
if first_tread_run is not None:
|
||||
n_default_tread_runs -= 1
|
||||
length += first_tread_run
|
||||
if last_tread_run != 0:
|
||||
|
||||
# If last tread has custom width (non-None), use it instead of default
|
||||
if last_tread_run is not None:
|
||||
n_default_tread_runs -= 1
|
||||
if n_default_tread_runs >= 0:
|
||||
length += last_tread_run
|
||||
length += tread_run * max(n_default_tread_runs, 0)
|
||||
# nosing overlaps
|
||||
# are not part of the tread run
|
||||
# so they don't affect the stair length
|
||||
# except the first tread's nosing
|
||||
length += last_tread_run
|
||||
|
||||
# Add remaining default tread runs
|
||||
length += tread_run * n_default_tread_runs
|
||||
|
||||
# Handle nosing length effects on total length
|
||||
# Nosing overlaps don't affect tread run spacing,
|
||||
# but the first tread's nosing extends the total length
|
||||
if nosing_length > 0: # nosing overlaps
|
||||
length += nosing_length
|
||||
if nosing_length < 0: # tread gaps
|
||||
if nosing_length < 0: # tread gaps between treads
|
||||
length += abs(nosing_length) * number_of_treads
|
||||
|
||||
calculated_params["Length"] = round(length, 5)
|
||||
pitch = height / length
|
||||
pitch_formatted = str(round(pitch * 100, 1)) + " % / " + str(round(degrees(atan(pitch)), 1)) + " deg"
|
||||
@@ -1377,35 +1398,60 @@ class Model(bonsai.core.tool.Model):
|
||||
|
||||
return calculated_params
|
||||
|
||||
StairType = Literal["CONCRETE", "WOOD/STEEL", "GENERIC"]
|
||||
|
||||
@classmethod
|
||||
def generate_stair_2d_profile(
|
||||
cls,
|
||||
number_of_treads,
|
||||
height,
|
||||
width,
|
||||
tread_run,
|
||||
stair_type,
|
||||
number_of_treads: int,
|
||||
height: float,
|
||||
width: float,
|
||||
tread_run: float,
|
||||
stair_type: StairType,
|
||||
# WOOD/STEEL CONCRETE STAIR ARGUMENTS
|
||||
tread_depth=None,
|
||||
tread_depth: Union[float, None] = None,
|
||||
# CONCRETE STAIR ARGUMENTS
|
||||
has_top_nib=None,
|
||||
top_slab_depth=None,
|
||||
base_slab_depth=None,
|
||||
custom_first_last_tread_run=(0, 0),
|
||||
nosing_length=0,
|
||||
has_top_nib: Union[bool, None] = None,
|
||||
top_slab_depth: Union[float, None] = None,
|
||||
base_slab_depth: Union[float, None] = None,
|
||||
custom_first_last_tread_run: Union[tuple[float, float], tuple[None, None]] = (None, None),
|
||||
nosing_length: float = 0.0,
|
||||
# CONCRETE GENERIC STAIR ARGUMENTS
|
||||
nosing_depth=0,
|
||||
):
|
||||
nosing_depth: float = 0.0,
|
||||
) -> tuple[list[Vector], list[tuple[int, ...]], list[[list[int]]]]:
|
||||
"""returns a tuple of stair profile data: (vertices, edges, faces)"""
|
||||
vertices = []
|
||||
edges = []
|
||||
faces = []
|
||||
vertices: list[Vector] = []
|
||||
edges: list[tuple[int, ...]] = []
|
||||
faces: list[[list[int]]] = []
|
||||
|
||||
number_of_risers = number_of_treads + 1
|
||||
tread_rise = height / number_of_risers
|
||||
nosing_overlap = max(nosing_length, 0)
|
||||
nosing_tread_gap = -min(nosing_length, 0)
|
||||
nosing_overlap_offset = -V_(nosing_overlap, 0)
|
||||
first_tread_run = custom_first_last_tread_run[0] if custom_first_last_tread_run[0] is not None else tread_run
|
||||
|
||||
# ============ DEBUG LOGGING START ============
|
||||
print("\n" + "=" * 80)
|
||||
print("STAIR GENERATION DEBUG - WOOD/STEEL TYPE")
|
||||
print("=" * 80)
|
||||
print(f"Input Parameters:")
|
||||
print(f" stair_type: {stair_type}")
|
||||
print(f" number_of_treads: {number_of_treads}")
|
||||
print(f" height: {height}")
|
||||
print(f" width: {width}")
|
||||
print(f" tread_run: {tread_run}")
|
||||
print(f" tread_depth: {tread_depth}")
|
||||
print(f" custom_first_last_tread_run: {custom_first_last_tread_run}")
|
||||
print(f" nosing_length: {nosing_length}")
|
||||
print(f"\nCalculated Parameters:")
|
||||
print(f" number_of_risers: {number_of_risers}")
|
||||
print(f" tread_rise: {tread_rise}")
|
||||
print(f" nosing_overlap: {nosing_overlap}")
|
||||
print(f" nosing_tread_gap: {nosing_tread_gap}")
|
||||
print(f" nosing_overlap_offset: {nosing_overlap_offset}")
|
||||
print("=" * 80 + "\n")
|
||||
# ============ DEBUG LOGGING END ============
|
||||
|
||||
def define_generic_stair_treads():
|
||||
vertices.append(Vector([0, 0]))
|
||||
@@ -1421,7 +1467,12 @@ class Model(bonsai.core.tool.Model):
|
||||
elif nosing_depth == 0:
|
||||
default_tread_verts = (V_(-nosing_overlap, tread_rise), V_(tread_run, tread_rise))
|
||||
else: # nosing_overlap > 0 nosing_depth > 0
|
||||
# kind of L shape
|
||||
# kind of L shape:
|
||||
# (2)●───────────────────────────●(3)
|
||||
# |
|
||||
# |
|
||||
# ●──────────────●
|
||||
# (1) (0)
|
||||
default_tread_verts = (
|
||||
V_(0, tread_rise - nosing_depth),
|
||||
V_(-nosing_overlap, tread_rise - nosing_depth),
|
||||
@@ -1470,16 +1521,25 @@ class Model(bonsai.core.tool.Model):
|
||||
current_offset += tread_offset
|
||||
|
||||
if stair_type == "WOOD/STEEL":
|
||||
builder = ShapeBuilder(None)
|
||||
print("\n" + "-" * 80)
|
||||
print("WOOD/STEEL STAIR GENERATION PROCESS")
|
||||
print("-" * 80)
|
||||
|
||||
assert tread_depth is not None
|
||||
|
||||
# full tread rectangle
|
||||
def get_tread_verts(*args, **kwargs):
|
||||
fn = partial(builder.get_rectangle_coords, position=V_(0, -(tread_depth - tread_rise)))
|
||||
return [Vector(x) for x in fn(*args, **kwargs)]
|
||||
def get_tread_verts(size: Vector) -> list[Vector]:
|
||||
coords = ShapeBuilder.get_rectangle_coords(position=V_(0, -(tread_depth - tread_rise)), size=size)
|
||||
return [Vector(x) for x in coords]
|
||||
|
||||
default_tread_verts = get_tread_verts(size=V_(tread_run + nosing_overlap, tread_depth))
|
||||
default_tread_offset = V_(tread_run + nosing_tread_gap, tread_rise)
|
||||
|
||||
print(f"\nDefault Tread Configuration:")
|
||||
print(f" default_tread_verts: {default_tread_verts}")
|
||||
print(f" default_tread_offset: {default_tread_offset}")
|
||||
print(f" tread rectangle size: ({tread_run + nosing_overlap}, {tread_depth})")
|
||||
|
||||
def get_tread_data(i):
|
||||
# Check if this is first or last tread with custom run
|
||||
current_tread_run = None
|
||||
@@ -1488,28 +1548,48 @@ class Model(bonsai.core.tool.Model):
|
||||
elif i == number_of_risers - 1 and custom_first_last_tread_run[1] is not None:
|
||||
current_tread_run = custom_first_last_tread_run[1]
|
||||
|
||||
print(f"\n Tread {i}:")
|
||||
print(f" Is first tread: {i == 0}")
|
||||
print(f" Is last tread: {i == number_of_risers - 1}")
|
||||
print(f" current_tread_run: {current_tread_run}")
|
||||
|
||||
if current_tread_run is not None:
|
||||
tread_offset = default_tread_offset.copy()
|
||||
tread_offset.x = current_tread_run + nosing_tread_gap
|
||||
|
||||
# Handle zero-width treads
|
||||
if current_tread_run == 0:
|
||||
print(f" → Zero-width tread, skipping geometry")
|
||||
print(f" → tread_offset: {tread_offset}")
|
||||
return tread_offset, ()
|
||||
|
||||
tread_verts = get_tread_verts(size=V_(current_tread_run + nosing_overlap, tread_depth))
|
||||
print(f" → Custom tread")
|
||||
print(f" → tread_offset: {tread_offset}")
|
||||
print(f" → tread_verts: {tread_verts}")
|
||||
print(f" → tread rectangle size: ({current_tread_run + nosing_overlap}, {tread_depth})")
|
||||
return tread_offset, tread_verts
|
||||
|
||||
print(f" → Using default tread")
|
||||
print(f" → tread_offset: {default_tread_offset}")
|
||||
return default_tread_offset, default_tread_verts
|
||||
|
||||
# each tread is a separate shape
|
||||
cur_offset = V_(0, 0)
|
||||
tread_index = 0
|
||||
|
||||
print(f"\nGenerating Treads:")
|
||||
print(f" Number of risers to process: {number_of_risers}")
|
||||
|
||||
for i in range(number_of_risers):
|
||||
tread_offset, tread_verts = get_tread_data(i)
|
||||
|
||||
# Skip adding vertices/edges for zero-width treads
|
||||
if tread_verts:
|
||||
cur_trade_shape = [v + cur_offset + nosing_overlap_offset for v in tread_verts]
|
||||
print(f" Adding geometry at cur_offset: {cur_offset}")
|
||||
print(f" Final vertices (after offset): {cur_trade_shape}")
|
||||
|
||||
vertices.extend(cur_trade_shape)
|
||||
|
||||
cur_vertex = tread_index * 4
|
||||
@@ -1520,9 +1600,19 @@ class Model(bonsai.core.tool.Model):
|
||||
(cur_vertex + 3, cur_vertex),
|
||||
)
|
||||
edges.extend(verts_to_add)
|
||||
print(f" Edges added: {verts_to_add}")
|
||||
tread_index += 1
|
||||
else:
|
||||
print(f" Skipped (no geometry)")
|
||||
|
||||
cur_offset += tread_offset
|
||||
print(f" New cur_offset: {cur_offset}")
|
||||
|
||||
print(f"\nFinal Results:")
|
||||
print(f" Total vertices: {len(vertices)}")
|
||||
print(f" Total edges: {len(edges)}")
|
||||
print(f" Total treads generated: {tread_index}")
|
||||
print("-" * 80 + "\n")
|
||||
|
||||
elif stair_type == "GENERIC":
|
||||
define_generic_stair_treads()
|
||||
@@ -1538,6 +1628,11 @@ class Model(bonsai.core.tool.Model):
|
||||
elif stair_type == "CONCRETE":
|
||||
define_generic_stair_treads()
|
||||
|
||||
assert has_top_nib is not None
|
||||
assert top_slab_depth is not None
|
||||
assert base_slab_depth is not None
|
||||
assert tread_depth is not None
|
||||
|
||||
# add the nibs
|
||||
# basically we define stair bottom line as a line at `tread_depth` distance
|
||||
# from the tread diagonal line
|
||||
@@ -1551,15 +1646,20 @@ class Model(bonsai.core.tool.Model):
|
||||
# s0 is just a sampled point from the bottom line
|
||||
# we stick to the third point as the first point
|
||||
# is affected by customized tread run
|
||||
s0 = V_(custom_first_last_tread_run[0] or tread_run, tread_rise) + td_vector
|
||||
s0 = V_(first_tread_run, tread_rise) + td_vector
|
||||
# comes from y = stair_tan * x + b
|
||||
b = s0.y - stair_tan * s0.x
|
||||
|
||||
def get_point_on_2d_line(x=None, y=None):
|
||||
if y is None:
|
||||
def get_point_on_2d_line(
|
||||
x: Union[float, None] = None,
|
||||
y: Union[float, None] = None,
|
||||
) -> Vector:
|
||||
if x is not None and y is None:
|
||||
y = stair_tan * x + b
|
||||
elif x is None:
|
||||
elif x is None and y is not None:
|
||||
x = (y - b) / stair_tan
|
||||
else:
|
||||
assert False
|
||||
return V_(x, y)
|
||||
|
||||
# top nib
|
||||
@@ -1603,11 +1703,13 @@ class Model(bonsai.core.tool.Model):
|
||||
else:
|
||||
raise Exception(f"Unsupported stair type: {stair_type}")
|
||||
|
||||
vertices = (v.to_3d().xzy for v in vertices)
|
||||
vertices = [v.to_3d().xzy for v in vertices]
|
||||
return (vertices, edges, faces)
|
||||
|
||||
@classmethod
|
||||
def update_simple_openings(cls, element: ifcopenshell.entity_instance) -> None:
|
||||
from bonsai.bim.module.model.opening import FilledOpeningGenerator
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
fillings = {e: tool.Ifc.get_object(e) for e in tool.Ifc.get_all_element_occurrences(element)}
|
||||
|
||||
@@ -2170,6 +2272,8 @@ class Model(bonsai.core.tool.Model):
|
||||
|
||||
@classmethod
|
||||
def add_filled_opening(cls, voided_obj: bpy.types.Object, filling_obj: bpy.types.Object) -> None:
|
||||
from bonsai.bim.module.model.opening import FilledOpeningGenerator
|
||||
|
||||
FilledOpeningGenerator().generate(filling_obj, voided_obj)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import shutil
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.document
|
||||
@@ -502,3 +503,11 @@ class Project(bonsai.core.tool.Project):
|
||||
d2 = v3 - v2
|
||||
normals.append((v1, d1.cross(d2).normalized()))
|
||||
return normals
|
||||
|
||||
@classmethod
|
||||
def save_test_project(cls) -> None:
|
||||
TMP = Path.cwd() / "test/files/temp"
|
||||
# Clean up previous test project.
|
||||
if TMP.exists():
|
||||
shutil.rmtree(TMP)
|
||||
bpy.ops.bim.save_project(filepath=(TMP / "test_project.ifc").__str__(), should_save_as=True)
|
||||
|
||||
@@ -1020,7 +1020,7 @@ class Sequence(bonsai.core.tool.Sequence):
|
||||
|
||||
def set_material(name, r, g, b):
|
||||
material = bpy.data.materials.new(name)
|
||||
material.use_nodes = True
|
||||
tool.Style.set_use_nodes(material, True)
|
||||
tool.Blender.get_material_node(material, "BSDF_PRINCIPLED").inputs[0].default_value = (r, g, b, 1.0)
|
||||
return material
|
||||
|
||||
|
||||
@@ -175,9 +175,12 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
|
||||
@classmethod
|
||||
def select_products(cls, products: Iterable[ifcopenshell.entity_instance], unhide: bool = False) -> None:
|
||||
assert (view_layer := bpy.context.view_layer)
|
||||
# Update view layer, otherwise `objects` might be missing just created objects.
|
||||
view_layer.update()
|
||||
for product in products:
|
||||
obj = tool.Ifc.get_object(product)
|
||||
if obj and bpy.context.view_layer.objects.get(obj.name):
|
||||
if obj and view_layer.objects.get(obj.name):
|
||||
if unhide:
|
||||
obj.hide_set(False)
|
||||
obj.select_set(True)
|
||||
|
||||
@@ -63,9 +63,23 @@ class Style(bonsai.core.tool.Style):
|
||||
def get_material_style_props(cls, material: bpy.types.Material) -> BIMStyleProperties:
|
||||
return material.BIMStyleProperties
|
||||
|
||||
@classmethod
|
||||
def get_use_nodes(cls, obj: bpy.types.Material) -> bool:
|
||||
"""Since Blender 5.0 ``use_nodes`` are always ``True`` and considered deprecated."""
|
||||
if tool.Blender.BLENDER_5:
|
||||
return True
|
||||
return obj.use_nodes
|
||||
|
||||
@classmethod
|
||||
def set_use_nodes(cls, obj: bpy.types.Material, use_nodes: bool) -> None:
|
||||
"""Since Blender 5.0 ``use_nodes`` are always ``True`` and considered deprecated."""
|
||||
if tool.Blender.BLENDER_5:
|
||||
return
|
||||
obj.use_nodes = use_nodes
|
||||
|
||||
@classmethod
|
||||
def can_support_rendering_style(cls, obj: bpy.types.Material) -> bool:
|
||||
return obj.use_nodes and hasattr(obj.node_tree, "nodes")
|
||||
return tool.Blender.BLENDER_5 or (obj.use_nodes and hasattr(obj.node_tree, "nodes"))
|
||||
|
||||
@classmethod
|
||||
def delete_object(cls, obj: bpy.types.Material) -> None:
|
||||
|
||||
@@ -28,7 +28,8 @@ import bonsai.tool as tool
|
||||
import ifcopenshell.api.sequence
|
||||
import ifcopenshell.api.classification
|
||||
import ifcopenshell.api.cost
|
||||
from typing import Any, Optional, TYPE_CHECKING
|
||||
from typing import Any, Optional, TYPE_CHECKING, Union
|
||||
from mathutils import Color
|
||||
import time
|
||||
import socket
|
||||
import sys
|
||||
@@ -790,8 +791,8 @@ class Web(bonsai.core.tool.Web):
|
||||
calculates the mixed colors, and sends the theme data to the Web UI.
|
||||
"""
|
||||
|
||||
def get_color(theme, attribute) -> tuple[Any, ...]:
|
||||
return tuple(getattr(theme, attribute)[:])
|
||||
def to_tuple(theme: Union[Color, bpy.types.bpy_prop_array[float]]) -> tuple[float, ...]:
|
||||
return tuple(theme[:])
|
||||
|
||||
def mix_colors(color1, color2):
|
||||
alpha1 = color1[3] * 255 if len(color1) > 3 else 255
|
||||
@@ -803,33 +804,50 @@ class Web(bonsai.core.tool.Web):
|
||||
blue = (color1[2] * (255 - alpha2) + color2[2] * alpha2) / 255
|
||||
return red, green, blue, alpha / 255
|
||||
|
||||
prefs = bpy.context.preferences.themes[0].preferences.space
|
||||
panel = prefs.panelcolors
|
||||
view_3d = bpy.context.preferences.themes[0].view_3d
|
||||
top_bar = bpy.context.preferences.themes[0].topbar.space
|
||||
info = bpy.context.preferences.themes[0].info
|
||||
outliner = bpy.context.preferences.themes[0].outliner
|
||||
ui = bpy.context.preferences.themes[0].user_interface
|
||||
assert (blender_preferences := bpy.context.preferences)
|
||||
blender_theme = blender_preferences.themes[0]
|
||||
prefs = blender_theme.preferences.space
|
||||
|
||||
if tool.Blender.BLENDER_5 or TYPE_CHECKING:
|
||||
theme_ui = blender_theme.user_interface
|
||||
theme_regions = blender_theme.regions
|
||||
panel_header, panel_back, panel_sub_back = (
|
||||
theme_ui.panel_header,
|
||||
theme_ui.panel_back,
|
||||
theme_ui.panel_sub_back,
|
||||
)
|
||||
tab_back, tab_outline = theme_regions.sidebars.tab_back, theme_ui.wcol_tab.outline
|
||||
else:
|
||||
# Removed in Blender 5.0.
|
||||
panel = prefs.panelcolors
|
||||
tab_back, tab_outline = prefs.tab_back, prefs.tab_outline
|
||||
panel_header, panel_back, panel_sub_back = panel.header, panel.back, panel.sub_back
|
||||
|
||||
view_3d = blender_theme.view_3d
|
||||
top_bar = blender_theme.topbar.space
|
||||
info = blender_theme.info
|
||||
outliner = blender_theme.outliner
|
||||
ui = blender_theme.user_interface
|
||||
|
||||
theme = {
|
||||
"window_background": get_color(prefs, "back"),
|
||||
"text": get_color(prefs, "text"),
|
||||
"tab_background": get_color(prefs, "tab_back"),
|
||||
"tab_outline": get_color(prefs, "tab_outline"),
|
||||
"panel_header": get_color(panel, "header"),
|
||||
"panel_background": mix_colors(get_color(panel, "back"), get_color(panel, "sub_back")),
|
||||
"top_bar_header": get_color(top_bar, "header"),
|
||||
"top_bar_header_text": get_color(top_bar, "header_text"),
|
||||
"active_object": get_color(view_3d, "object_active"),
|
||||
"selected_object": get_color(view_3d, "object_selected"),
|
||||
"info_warning": get_color(info, "info_warning_text"),
|
||||
"odd_row": get_color(outliner.space, "back"),
|
||||
"even_row": mix_colors(get_color(outliner.space, "back"), get_color(outliner, "row_alternate")),
|
||||
"active_highlight": get_color(outliner, "active"),
|
||||
"active_text_highlight": get_color(outliner, "active_object"),
|
||||
"button_background": get_color(ui.wcol_tool, "inner"),
|
||||
"button_test": get_color(ui.wcol_tool, "text"),
|
||||
"button_border": get_color(ui.wcol_tool, "outline"),
|
||||
"window_background": prefs.back[:],
|
||||
"text": to_tuple(prefs.text),
|
||||
"tab_background": to_tuple(tab_back),
|
||||
"tab_outline": to_tuple(tab_outline),
|
||||
"panel_header": to_tuple(panel_header),
|
||||
"panel_background": mix_colors(to_tuple(panel_back), to_tuple(panel_sub_back)),
|
||||
"top_bar_header": to_tuple(top_bar.header),
|
||||
"top_bar_header_text": to_tuple(top_bar.header_text),
|
||||
"active_object": to_tuple(view_3d.object_active),
|
||||
"selected_object": to_tuple(view_3d.object_selected),
|
||||
"info_warning": to_tuple(info.info_warning_text),
|
||||
"odd_row": to_tuple(outliner.space.back),
|
||||
"even_row": mix_colors(to_tuple(outliner.space.back), to_tuple(outliner.row_alternate)),
|
||||
"active_highlight": to_tuple(outliner.active),
|
||||
"active_text_highlight": to_tuple(outliner.active_object),
|
||||
"button_background": to_tuple(ui.wcol_tool.inner),
|
||||
"button_test": to_tuple(ui.wcol_tool.text),
|
||||
"button_border": to_tuple(ui.wcol_tool.outline),
|
||||
}
|
||||
cls.send_webui_data(theme, "theme", "theme_data", use_web_data=False)
|
||||
|
||||
|
||||
@@ -47,11 +47,11 @@ elif sys.platform == "darwin":
|
||||
elif sys.platform == "linux":
|
||||
BLENDER_PATH = Path.home() / f".config/blender/{BLENDER_VERSION}"
|
||||
else:
|
||||
raise RuntimeError("Unsupported platform")
|
||||
raise RuntimeError(f"Unsupported platform: {sys.platform}")
|
||||
|
||||
|
||||
BONSAI_PATH_CANDIDATES = (
|
||||
# Installed from Bonsai Unstalble Repo.
|
||||
# Installed from Bonsai Unstable Repo.
|
||||
BLENDER_PATH / r"extensions/raw_githubusercontent_com/bonsai",
|
||||
# Installed via offline installation.
|
||||
BLENDER_PATH / r"extensions/user_default/bonsai",
|
||||
|
||||
@@ -89,10 +89,17 @@ class SectionCutawayManager:
|
||||
SectionCutawayManager.offset_previous_nodes(previous_section_compare, offset_x, offset_y)
|
||||
|
||||
@staticmethod
|
||||
def override_materials():
|
||||
def set_use_nodes(obj: bpy.types.Material, use_nodes: bool) -> None:
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
# Since Blender 5.0 `use_nodes` are always `True` and considered deprecated.
|
||||
return
|
||||
obj.use_nodes = use_nodes
|
||||
|
||||
@classmethod
|
||||
def override_materials(cls):
|
||||
override = SectionCutawayManager.get_section_tree()
|
||||
for material in bpy.data.materials:
|
||||
material.use_nodes = True
|
||||
cls.set_use_nodes(material, True)
|
||||
if material.node_tree.nodes.get(override.name):
|
||||
continue
|
||||
material.blend_method = "HASHED"
|
||||
@@ -261,12 +268,12 @@ class SectionCutawayManager:
|
||||
|
||||
section_compare.name = "Last Section Compare"
|
||||
|
||||
@staticmethod
|
||||
def add_default_material_if_none_exists(context):
|
||||
@classmethod
|
||||
def add_default_material_if_none_exists(cls, context):
|
||||
material = bpy.data.materials.get("Section Override")
|
||||
if not material:
|
||||
material = bpy.data.materials.new("Section Override")
|
||||
material.use_nodes = True
|
||||
cls.set_use_nodes(material, True)
|
||||
|
||||
if context.scene.SectionProperties.should_section_selected_objects:
|
||||
objects = list(context.selected_objects)
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
Feature: bSDD
|
||||
|
||||
Scenario: Load bSDD dictionaries
|
||||
Given an empty Blender session
|
||||
Given an empty IFC project
|
||||
And I look at the "buildingSMART Data Dictionary" panel
|
||||
When I click "Load bSDD Dictionaries"
|
||||
Then I see "Selected dictionary"
|
||||
Then I see "Selected Dictionary:"
|
||||
And I see "LCA" in the "1st" list
|
||||
|
||||
Scenario: Add classification from bSDD - add all dictionaries
|
||||
@@ -15,8 +15,7 @@ Scenario: Add classification from bSDD - add all dictionaries
|
||||
When I click "is_active" in the row where I see "LCA" in the "1st" list
|
||||
And I click "is_active" in the row where I see "BonsaiTestDict" in the "1st" list
|
||||
And I look at the "Classifications" panel
|
||||
And I set the "classification_source" property to "buildingSMART Data Dictionary"
|
||||
And I set the "active_dictionary" property to "All Dictionaries"
|
||||
And I set the "classification_source" property to "All Active bSDDs"
|
||||
And I click "Add Classification From bSDD"
|
||||
Then I see "LCA"
|
||||
And I see "BonsaiTestDict"
|
||||
@@ -27,8 +26,7 @@ Scenario: Add classification from bSDD - add single dictionary
|
||||
And I click "Load bSDD Dictionaries"
|
||||
When I click "is_active" in the row where I see "BonsaiTestDict" in the "1st" list
|
||||
And I look at the "Classifications" panel
|
||||
And I set the "classification_source" property to "buildingSMART Data Dictionary"
|
||||
And I set the "active_dictionary" property to "BonsaiTestDict"
|
||||
And I set the "classification_source" property to "BonsaiTestDict"
|
||||
And I click "Add Classification From bSDD"
|
||||
Then I see "BonsaiTestDict"
|
||||
And I don't see "LCA"
|
||||
@@ -41,8 +39,7 @@ Scenario: Search bSDD classifications - search all dictionaries
|
||||
And I click "is_active" in the row where I see "BonsaiTestDict" in the "1st" list
|
||||
And I select the object "IfcSite/My Site"
|
||||
And I look at the "Classification References" panel
|
||||
And I set the "classification_source" property to "buildingSMART Data Dictionary"
|
||||
And I set the "active_dictionary" property to "All Dictionaries"
|
||||
And I set the "classification_source" property to "All Active bSDDs"
|
||||
And I click "VIEWZOOM"
|
||||
Then I see "Acidification" in the "1st" list
|
||||
And I see "BonsaiReferenceA" in the "1st" list
|
||||
@@ -54,8 +51,7 @@ Scenario: Search bSDD classifications - search single dictionary
|
||||
And I click "is_active" in the row where I see "BonsaiTestDict" in the "1st" list
|
||||
And I select the object "IfcSite/My Site"
|
||||
And I look at the "Classification References" panel
|
||||
When I set the "classification_source" property to "buildingSMART Data Dictionary"
|
||||
And I set the "active_dictionary" property to "BonsaiTestDict"
|
||||
When I set the "classification_source" property to "BonsaiTestDict"
|
||||
And I click "VIEWZOOM"
|
||||
Then I see "BonsaiReferenceA" in the "1st" list
|
||||
And I don't see "Acidification" in the "1st" list
|
||||
@@ -68,8 +64,7 @@ Scenario: Add classification reference from bSDD - add classification only
|
||||
And I click "is_active" in the row where I see "BonsaiTestDict" in the "1st" list
|
||||
And I select the object "IfcSite/My Site"
|
||||
And I look at the "Classification References" panel
|
||||
And I set the "classification_source" property to "buildingSMART Data Dictionary"
|
||||
And I set the "active_dictionary" property to "All Dictionaries"
|
||||
And I set the "classification_source" property to "All Active bSDDs"
|
||||
And I click "VIEWZOOM"
|
||||
Then I see "Acidification" in the "1st" list
|
||||
When I select the row where I see "Acidification" in the "1st" list
|
||||
@@ -77,29 +72,24 @@ Scenario: Add classification reference from bSDD - add classification only
|
||||
Then I see "Acidification"
|
||||
And I don't see "BonsaiReferenceA"
|
||||
|
||||
Scenario: Get bSDD classification properties - get associated properties then add classification
|
||||
Scenario: Get bSDD classification properties - get and add associated properties
|
||||
Given an empty IFC project
|
||||
And I look at the "buildingSMART Data Dictionary" panel
|
||||
And I click "Load bSDD Dictionaries"
|
||||
When I click "is_active" in the row where I see "LCA" in the "1st" list
|
||||
And I select the object "IfcSite/My Site"
|
||||
And I look at the "Classification References" panel
|
||||
And I set the "classification_source" property to "buildingSMART Data Dictionary"
|
||||
And I set the "active_dictionary" property to "All Dictionaries"
|
||||
And I click "VIEWZOOM"
|
||||
When I select the row where I see "Acidification" in the "1st" list
|
||||
And I click "COPY_ID"
|
||||
Then I see "SizeSet"
|
||||
And I see "Height"
|
||||
And I see "Volume"
|
||||
And I see "No References"
|
||||
When I set the "Height" property to "42"
|
||||
And I click "Add Classification Reference"
|
||||
Then I see "Acidification"
|
||||
And I don't see "No References"
|
||||
When I look at the "Property Sets" panel
|
||||
Then I see "SizeSet"
|
||||
And I see "Height"
|
||||
And I see "42"
|
||||
And I see "Volume"
|
||||
And I see "0"
|
||||
And I look at the "Property Sets" panel
|
||||
And I set the "pset_name" property to "All Data Dictionaries"
|
||||
And I click "FILE_REFRESH"
|
||||
When I select the row where I see "Height" in the "2nd" list
|
||||
And I click "is_selected" in the row where I see "Height" in the "2nd" list
|
||||
And I click "is_selected" in the row where I see "Volume" in the "2nd" list
|
||||
Then I look at the "Property Sets" panel
|
||||
And I set the "height" property to "42"
|
||||
And I set the "volume" property to "23"
|
||||
When I click "Add bSDD Properties"
|
||||
Then I look at the "Property Sets" panel
|
||||
And I see "height"
|
||||
And I see "42.0"
|
||||
And I see "volume"
|
||||
And I see "23.0"
|
||||
|
||||
@@ -9,7 +9,7 @@ Scenario: Duplicate drawing
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I save sample test files
|
||||
And I save IFC project
|
||||
And I look at the "Drawings" panel
|
||||
And I click "IMPORT"
|
||||
And I click "ADD"
|
||||
@@ -27,7 +27,7 @@ Scenario: Duplicate drawing - without duplicating annotations
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I save sample test files
|
||||
And I save IFC project
|
||||
And I look at the "Drawings" panel
|
||||
And I click "IMPORT"
|
||||
And I click "ADD"
|
||||
@@ -50,7 +50,7 @@ Scenario: Duplicate drawing - with duplicating annotations
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I save sample test files
|
||||
And I save IFC project
|
||||
And I look at the "Drawings" panel
|
||||
And I click "IMPORT"
|
||||
And I click "ADD"
|
||||
@@ -73,7 +73,7 @@ Scenario: Create drawing
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I save sample test files
|
||||
And I save IFC project
|
||||
And I look at the "Drawings" panel
|
||||
And I click "IMPORT"
|
||||
And I click "ADD"
|
||||
@@ -93,7 +93,7 @@ Scenario: Create drawing after deleting a duplicated object
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I duplicate the selected objects
|
||||
And I save sample test files
|
||||
And I save IFC project
|
||||
And I look at the "Drawings" panel
|
||||
And I click "IMPORT"
|
||||
And I click "ADD"
|
||||
@@ -112,7 +112,7 @@ Scenario: Activate drawing preserves visibility for non-ifc objects
|
||||
And I add a cube
|
||||
And the object "Cube" is visible
|
||||
And the object "Cube.001" is not visible
|
||||
And I save sample test files
|
||||
And I save IFC project
|
||||
And I look at the "Drawings" panel
|
||||
And I click "IMPORT"
|
||||
And I click "ADD"
|
||||
@@ -126,7 +126,7 @@ Scenario: Activate drawing preserves selection
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I save sample test files
|
||||
And I save IFC project
|
||||
And I look at the "Drawings" panel
|
||||
And I click "IMPORT"
|
||||
And I click "ADD"
|
||||
@@ -143,12 +143,13 @@ Scenario: Remove drawing
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I save sample test files
|
||||
And I save IFC project
|
||||
And I look at the "Drawings" panel
|
||||
And I click "IMPORT"
|
||||
And I click "ADD"
|
||||
And I press "bim.toggle_target_view(option="EXPAND", target_view='PLAN_VIEW')"
|
||||
And I select the "PLAN_VIEW" item in the "BIM_UL_drawinglist" list
|
||||
And the variable "drawing" is "IfcStore.get_file().by_type('IfcAnnotation')[0].id()"
|
||||
When I click "VIEW_CAMERA_UNSELECTED" in the row where I see "PLAN_VIEW" in the "1st" list
|
||||
Then the collection "IfcAnnotation/PLAN_VIEW" exists
|
||||
When I press "bim.remove_drawing(drawing={drawing})"
|
||||
@@ -156,13 +157,16 @@ Scenario: Remove drawing
|
||||
|
||||
Scenario: Remove drawing - via object deletion
|
||||
Given an empty IFC project
|
||||
And I save IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I press "bim.add_drawing"
|
||||
And I look at the "Drawings" panel
|
||||
And I click "IMPORT"
|
||||
And I click "ADD"
|
||||
And the variable "drawing" is "IfcStore.get_file().by_type('IfcAnnotation')[0].id()"
|
||||
And the collection "IfcAnnotation/PLAN_VIEW" exists
|
||||
And the object "IfcAnnotation/PLAN_VIEW" is selected
|
||||
@@ -177,7 +181,7 @@ Scenario: Remove drawing - deleting active drawing
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I save sample test files
|
||||
And I save IFC project
|
||||
And I look at the "Drawings" panel
|
||||
And I click "IMPORT"
|
||||
And I click "ADD"
|
||||
@@ -192,7 +196,7 @@ Scenario: Add annotation - text
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I save sample test files
|
||||
And I save IFC project
|
||||
And I look at the "Drawings" panel
|
||||
And I click "IMPORT"
|
||||
And I click "ADD"
|
||||
@@ -206,7 +210,7 @@ Scenario: Add annotation - auto create context if it doesn't exist
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I save sample test files
|
||||
And I save IFC project
|
||||
And I look at the "Geometric Representation Contexts" panel
|
||||
And I see "Plan"
|
||||
And I click the "X" after the text "Plan"
|
||||
@@ -227,7 +231,7 @@ Scenario: Create drawing - using shapely fill mode
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I save sample test files
|
||||
And I save IFC project
|
||||
And I look at the "Drawings" panel
|
||||
And I click "IMPORT"
|
||||
And I set the "location_hint" property to "My Storey"
|
||||
@@ -243,7 +247,7 @@ Scenario: Create drawing - using shapely fill mode
|
||||
|
||||
Scenario: Add sheet
|
||||
Given an empty IFC project
|
||||
And I save sample test files
|
||||
And I save IFC project
|
||||
And I look at the "Sheets" panel
|
||||
And I click "IMPORT"
|
||||
When I click "ADD"
|
||||
@@ -252,7 +256,7 @@ Scenario: Add sheet
|
||||
|
||||
Scenario: Create sheet
|
||||
Given an empty IFC project
|
||||
And I save sample test files
|
||||
And I save IFC project
|
||||
And I look at the "Sheets" panel
|
||||
And I click "IMPORT"
|
||||
And I click "ADD"
|
||||
@@ -269,7 +273,7 @@ Scenario: Add drawing to sheet
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I save sample test files
|
||||
And I save IFC project
|
||||
And I look at the "Drawings" panel
|
||||
And I click "IMPORT"
|
||||
And I click "ADD"
|
||||
@@ -294,7 +298,7 @@ Scenario: Create sheet - with a drawing added to it
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I save sample test files
|
||||
And I save IFC project
|
||||
And I look at the "Drawings" panel
|
||||
And I click "IMPORT"
|
||||
And I click "ADD"
|
||||
|
||||
@@ -292,14 +292,31 @@ Scenario: Override duplicate move - with active IFC data
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And additionally the object "IfcBuildingStorey/My Storey" is selected
|
||||
And I set "scene.BIMSpatialDecompositionProperties.is_locked" to "False"
|
||||
When I duplicate the selected objects
|
||||
Then the object "IfcWall/Cube" exists
|
||||
And the object "IfcWall/Cube" is an "IfcWall"
|
||||
And the object "IfcWall/Cube.001" exists
|
||||
And the object "IfcWall/Cube.001" is an "IfcWall"
|
||||
And the object "IfcWall/Cube.001" has a "Tessellation" representation of "Model/Body/MODEL_VIEW"
|
||||
|
||||
Scenario: Override duplicate move - with locked elements
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I look at the "Spatial Decomposition" panel
|
||||
And I set the "is_visible" property to "TRUE"
|
||||
And additionally the object "IfcBuildingStorey/My Storey" is selected
|
||||
Then I press "bim.override_object_duplicate_move" and expect error "Error: 'IfcBuildingStorey/My Storey' is locked. Unlock it via the Spatial panel in the Project Overview tab."
|
||||
And the object "IfcWall/Cube" exists
|
||||
And the object "IfcWall/Cube" is an "IfcWall"
|
||||
And the object "IfcWall/Cube.001" exists
|
||||
And the object "IfcWall/Cube.001" is an "IfcWall"
|
||||
And the object "IfcWall/Cube.001" has a "Tessellation" representation of "Model/Body/MODEL_VIEW"
|
||||
And the object "IfcBuildingStorey/My Storey.001" does not exist
|
||||
|
||||
Scenario: Override duplicate move - with unlocked elements
|
||||
@@ -310,8 +327,10 @@ Scenario: Override duplicate move - with unlocked elements
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I look at the "Spatial Decomposition" panel
|
||||
And I set the "is_visible" property to "TRUE"
|
||||
And I set the "is_locked" property to "FALSE"
|
||||
And the object "IfcBuildingStorey/My Storey" is selected
|
||||
And I set "scene.BIMSpatialDecompositionProperties.is_locked" to "False"
|
||||
When I duplicate the selected objects
|
||||
Then the object "IfcBuildingStorey/My Storey.001" exists
|
||||
And the object "IfcBuildingStorey/My Storey.001" is an "IfcBuildingStorey"
|
||||
@@ -507,6 +526,8 @@ Scenario: Override duplicate move linked - with active IFC data
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I look at the "Spatial Decomposition" panel
|
||||
And I set the "is_visible" property to "TRUE"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And additionally the object "IfcBuildingStorey/My Storey" is selected
|
||||
When I press "object.duplicate_move_linked"
|
||||
@@ -538,6 +559,9 @@ Scenario: Override paste buffer - with active IFC data
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I look at the "Spatial Decomposition" panel
|
||||
And I set the "is_visible" property to "TRUE"
|
||||
And I set the "is_locked" property to "FALSE"
|
||||
And additionally the object "IfcBuildingStorey/My Storey" is selected
|
||||
When I press "view3d.copybuffer"
|
||||
# IFC elements unlinked on paste for safety
|
||||
@@ -555,8 +579,9 @@ Scenario: Duplicate linked aggregate
|
||||
When I duplicate linked aggregate the selected objects
|
||||
Then the object "IfcWall/Wall_01.001" exists
|
||||
And the object "IfcWall/Wall_02.001" exists
|
||||
And the object "IfcElementAssembly/Assembly_01" exists
|
||||
Then the object "IfcElementAssembly/Assembly" and "IfcElementAssembly/Assembly_01" belong to the same Linked Aggregate Group
|
||||
# msgbus updates don't happen in background mode, so obj is not renamed to "IfcElementAssembly/Assembly_01"
|
||||
And the object "Assembly_01" exists
|
||||
Then the object "IfcElementAssembly/Assembly" and "Assembly_01" belong to the same Linked Aggregate Group
|
||||
|
||||
Scenario: Refresh linked aggregate
|
||||
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
|
||||
|
||||
@@ -872,8 +872,10 @@ Scenario: Export IFC - with basic contents and saving as a relative path
|
||||
|
||||
Scenario: Export IFC - with deleted objects synchronised
|
||||
Given an empty IFC project
|
||||
When the object "IfcBuildingStorey/My Storey" is selected
|
||||
And I set "scene.BIMSpatialDecompositionProperties.is_locked" to "False"
|
||||
And I look at the "Spatial Decomposition" panel
|
||||
When I set the "is_visible" property to "TRUE"
|
||||
And the object "IfcBuildingStorey/My Storey" is selected
|
||||
And I set the "is_locked" property to "FALSE"
|
||||
And I delete the selected objects
|
||||
And I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')"
|
||||
And an empty Blender session is started
|
||||
|
||||
@@ -167,14 +167,18 @@ Scenario: Copy a wall
|
||||
|
||||
Scenario: Copy a storey - when locked
|
||||
Given an empty IFC project
|
||||
And I look at the "Spatial Decomposition" panel
|
||||
And I set the "is_visible" property to "TRUE"
|
||||
And the object "IfcBuildingStorey/My Storey" is selected
|
||||
Then I expect an error "Error: 'IfcBuildingStorey/My Storey' is locked. Unlock it via the Spatial panel in the Project Overview tab." when "i_duplicate_the_selected_objects()"
|
||||
Then the object "IfcBuildingStorey/My Storey.001" does not exist
|
||||
|
||||
Scenario: Copy a storey - when unlocked
|
||||
Given an empty IFC project
|
||||
And I look at the "Spatial Decomposition" panel
|
||||
And I set the "is_visible" property to "TRUE"
|
||||
And the object "IfcBuildingStorey/My Storey" is selected
|
||||
And I set "scene.BIMSpatialDecompositionProperties.is_locked" to "False"
|
||||
And I set the "is_locked" property to "FALSE"
|
||||
When I duplicate the selected objects
|
||||
Then the object "IfcBuildingStorey/My Storey" and "IfcBuildingStorey/My Storey.001" are different elements
|
||||
And the object "IfcBuildingStorey/My Storey" is in the collection "IfcBuildingStorey/My Storey"
|
||||
|
||||
@@ -15,6 +15,7 @@ Scenario: Set default container
|
||||
Scenario: Select container
|
||||
Given an empty IFC project
|
||||
And I look at the "Spatial Decomposition" panel
|
||||
And I set the "is_visible" property to "TRUE"
|
||||
When I select the row where I see "My Building" in the "1st" list
|
||||
And I click "OBJECT_DATA"
|
||||
Then the object "IfcBuilding/My Building" is selected
|
||||
@@ -81,10 +82,10 @@ Scenario: Copy to container
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcSite/My Site" is selected
|
||||
And additionally the object "IfcWall/Cube" is selected
|
||||
And I press "bim.enable_editing_container"
|
||||
When I press "bim.copy_to_container"
|
||||
And I look at the "Spatial Decomposition" panel
|
||||
When I select the "My Site" item in the "BIM_UL_containers_manager" list
|
||||
And the object "IfcWall/Cube" is selected
|
||||
When I click "MOD_DISPLACE"
|
||||
Then the object "IfcWall/Cube.001" is in the collection "IfcSite/My Site"
|
||||
|
||||
Scenario: Reference structure
|
||||
@@ -222,6 +223,7 @@ Scenario: Set element visibility - Hide spatial container
|
||||
Scenario: Select spatial container
|
||||
Given an empty IFC project
|
||||
And I look at the "Spatial Decomposition" panel
|
||||
And I set the "is_visible" property to "TRUE"
|
||||
When I select the "My Site" item in the "BIM_UL_containers_manager" list
|
||||
And I click "OBJECT_DATA"
|
||||
Then the object "IfcSite/My Site" is selected
|
||||
|
||||
@@ -16,9 +16,13 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import types
|
||||
import bpy
|
||||
import pytest
|
||||
import shutil
|
||||
import pprint
|
||||
import traceback
|
||||
import webbrowser
|
||||
import numpy as np
|
||||
@@ -54,33 +58,47 @@ webbrowser.open = lambda x: True
|
||||
|
||||
tool.Bsdd.client = test.bim.stub.bSDDClientStub()
|
||||
|
||||
PYTEST_BLENDER_NO_BACKGROUND = bool(os.getenv("PYTEST_BLENDER_NO_BACKGROUND"))
|
||||
"""Option to run Blender not in background.
|
||||
|
||||
Can be useful for debugging, but has caveats - can't use ``wm.read_homefile``
|
||||
as resets the ``bpy.context`` and some it's members become `None`.
|
||||
"""
|
||||
|
||||
TMP = Path(f"{variables['cwd']}/test/files/temp")
|
||||
|
||||
|
||||
class PanelSpy:
|
||||
def __init__(self, panel: type[bpy.types.Panel]):
|
||||
def __init__(self, blender_panel: type[bpy.types.Panel]):
|
||||
self.is_spy_dirty = True
|
||||
self.panel = panel
|
||||
self.blender_panel = blender_panel
|
||||
|
||||
def refresh_spy(self):
|
||||
def refresh_spy(self) -> None:
|
||||
if self.is_spy_dirty:
|
||||
self.is_spy_dirty = False
|
||||
self.spied_attr: Union[str, None] = None
|
||||
self.spied_labels: list[str] = []
|
||||
self.spied_props: list[dict[str, Any]] = []
|
||||
self.spied_operators: list[dict[str, Any]] = []
|
||||
self.spied_lists: list[dict[str, Any]] = []
|
||||
self.panel.draw(self, bpy.context)
|
||||
self.spied_lists: list[TemplateListSpy] = []
|
||||
if hasattr(self.blender_panel, "draw_header"):
|
||||
self.blender_panel.draw_header(self, bpy.context)
|
||||
self.blender_panel.draw(self, bpy.context)
|
||||
|
||||
def __getattr__(self, attr):
|
||||
def __getattr__(self, attr: str) -> PanelSpy | Any:
|
||||
self.spied_attr = attr
|
||||
if annotation := self.panel.__annotations__.get(attr, None):
|
||||
if annotation := self.blender_panel.__annotations__.get(attr, None):
|
||||
return annotation.keywords.get("default", None) # An operator property
|
||||
if attr == "layout":
|
||||
return self
|
||||
if hasattr(self.panel, attr) and not callable(getattr(self.panel, attr)):
|
||||
return getattr(self.panel, attr)
|
||||
sentinel = object()
|
||||
attr_value = getattr(self.blender_panel, attr, sentinel)
|
||||
# Don't use `callable`, because we might have `Data` classes as panel attributes.
|
||||
if attr_value is not sentinel and not isinstance(attr_value, types.FunctionType):
|
||||
return attr_value
|
||||
return self
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
def __call__(self, *args: Any, **kwargs: Any) -> PanelSpy | TemplateListSpy | OperatorSpy | Any:
|
||||
if self.spied_attr in ("row", "column", "box", "separator", "menu", "operator_menu_enum", "split"):
|
||||
return self
|
||||
elif self.spied_attr == "template_list":
|
||||
@@ -149,15 +167,20 @@ class PanelSpy:
|
||||
spied_operator = {"operator": operator, "icon": icon, "text": text, "kwargs": {}, "after": after}
|
||||
self.spied_operators.append(spied_operator)
|
||||
return OperatorSpy(spied_operator)
|
||||
elif self.spied_attr == "panel":
|
||||
default_closed = kwargs.get("default_closed", False)
|
||||
header = self
|
||||
panel = None if default_closed else self
|
||||
return header, panel
|
||||
else:
|
||||
getattr(self.panel, self.spied_attr)(self, *args, **kwargs)
|
||||
return getattr(self.blender_panel, self.spied_attr)(self, *args, **kwargs)
|
||||
|
||||
|
||||
class OperatorSpy:
|
||||
def __init__(self, spied_data):
|
||||
def __init__(self, spied_data: dict[str, Any]):
|
||||
self.spied_data = spied_data
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
if name == "spied_data":
|
||||
# Allow direct setting of spied_data only during initialization
|
||||
super().__setattr__(name, value)
|
||||
@@ -166,17 +189,19 @@ class OperatorSpy:
|
||||
|
||||
|
||||
class TemplateListSpy(PanelSpy):
|
||||
def __init__(self, template_list: type[bpy.types.UIList], spied_data: dict) -> None:
|
||||
items: bpy.types.bpy_prop_collection_idprop[bpy.types.PropertyGroup]
|
||||
active_index: int
|
||||
active_item: bpy.types.PropertyGroup | None
|
||||
blender_panel: type[bpy.types.UIList]
|
||||
|
||||
def __init__(self, template_list: type[bpy.types.UIList], spied_data: dict[str, Any]):
|
||||
self.spied_data = spied_data
|
||||
self.items = getattr(self.spied_data["dataptr"], self.spied_data["propname"])
|
||||
self.active_index = getattr(self.spied_data["active_dataptr"], self.spied_data["active_propname"])
|
||||
try:
|
||||
self.active_item = self.items[self.active_index]
|
||||
except:
|
||||
self.active_item = None
|
||||
self.panel = template_list
|
||||
self.active_item = tool.Blender.get_active_uilist_element(self.items, self.active_index)
|
||||
self.blender_panel = template_list
|
||||
|
||||
self.rows = []
|
||||
self.rows: list[TemplateListItemSpy] = []
|
||||
for item in self.items:
|
||||
self.rows.append(TemplateListItemSpy(self, item))
|
||||
|
||||
@@ -188,16 +213,19 @@ class TemplateListSpy(PanelSpy):
|
||||
except:
|
||||
assert False, f"Could not set active index {index}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"TemplateListSpy({self.spied_data['listtype_name']}, {len(self.items)} items, active_index={self.active_index})"
|
||||
|
||||
|
||||
class TemplateListItemSpy(PanelSpy):
|
||||
def __init__(self, parent: TemplateListSpy, item):
|
||||
self.panel = parent.panel
|
||||
self.blender_panel = blender_panel = parent.blender_panel
|
||||
self.spied_attr: Union[str, None] = None
|
||||
self.spied_labels: list[str] = []
|
||||
self.spied_props: list[dict[str, Any]] = []
|
||||
self.spied_operators: list[dict[str, Any]] = []
|
||||
if len(signature(parent.panel.draw_item).parameters) == 8:
|
||||
parent.panel.draw_item(
|
||||
if len(signature(blender_panel.draw_item).parameters) == 8:
|
||||
blender_panel.draw_item(
|
||||
self,
|
||||
bpy.context,
|
||||
self,
|
||||
@@ -208,7 +236,7 @@ class TemplateListItemSpy(PanelSpy):
|
||||
parent.spied_data["active_propname"],
|
||||
)
|
||||
else:
|
||||
parent.panel.draw_item(
|
||||
blender_panel.draw_item(
|
||||
self,
|
||||
bpy.context,
|
||||
self,
|
||||
@@ -222,7 +250,8 @@ class TemplateListItemSpy(PanelSpy):
|
||||
)
|
||||
|
||||
|
||||
ui_name_cache = {}
|
||||
ui_name_cache: dict[str, str] = {}
|
||||
"""Mapping of UI panel names to their bl_idnames (e.g. `xxx` in `bpy.types.xxx`)."""
|
||||
panel_spy: Union[PanelSpy, None] = None
|
||||
|
||||
|
||||
@@ -271,7 +300,8 @@ def an_untestable_scenario():
|
||||
@when("an empty Blender session is started")
|
||||
def an_empty_blender_session():
|
||||
IfcStore.purge()
|
||||
bpy.ops.wm.read_homefile(app_template="")
|
||||
if not PYTEST_BLENDER_NO_BACKGROUND:
|
||||
bpy.ops.wm.read_homefile(app_template="")
|
||||
if len(bpy.data.objects) > 0:
|
||||
bpy.data.batch_remove(bpy.data.objects)
|
||||
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
|
||||
@@ -300,6 +330,17 @@ def an_empty_ifc_2x3_project():
|
||||
bpy.ops.bim.create_project()
|
||||
|
||||
|
||||
@given(parsers.parse("I save IFC project"))
|
||||
@when(parsers.parse("I save IFC project"))
|
||||
@then(parsers.parse("I save IFC project"))
|
||||
def saving_ifc_project() -> None:
|
||||
# Remove old temp files to avoid conflicts with non-updated assets.
|
||||
if TMP.exists():
|
||||
shutil.rmtree(TMP)
|
||||
filepath = TMP / "test_project.ifc"
|
||||
bpy.ops.bim.save_project(filepath=filepath.__str__(), should_save_as=True)
|
||||
|
||||
|
||||
@given("the Brickschema is stubbed")
|
||||
def the_brickschema_is_stubbed():
|
||||
# This makes things run faster since we don't need to load the entire brick schema
|
||||
@@ -415,18 +456,21 @@ def i_dont_see_text_in_the_nth_list(text, nth):
|
||||
@given(parsers.parse('I click "{button}" in the row where I see "{text}" in the "{nth}" list'))
|
||||
@when(parsers.parse('I click "{button}" in the row where I see "{text}" in the "{nth}" list'))
|
||||
@then(parsers.parse('I click "{button}" in the row where I see "{text}" in the "{nth}" list'))
|
||||
def i_click_button_in_the_row_where_i_see_text_in_the_nth_list(button, text, nth):
|
||||
def i_click_button_in_the_row_where_i_see_text_in_the_nth_list(button: str, text: str, nth: str) -> None:
|
||||
"""
|
||||
:param button: The text or icon of the button to click.
|
||||
:param button: The text or icon or name of the button/prop to click.
|
||||
:param text: The text to search for in the row.
|
||||
:param nth: E.g. "1st", "2nd", "5th".
|
||||
"""
|
||||
|
||||
assert panel_spy
|
||||
panel_spy.refresh_spy()
|
||||
nth = int("".join([c for c in nth if c.isnumeric()]))
|
||||
if len(panel_spy.spied_lists) < nth:
|
||||
assert False, f"{nth} list does not exist. Actual number of lists: {len(panel_spy.spied_lists)}"
|
||||
nth_ = int("".join([c for c in nth if c.isnumeric()]))
|
||||
if len(panel_spy.spied_lists) < nth_:
|
||||
assert False, f"{nth_} list does not exist. Actual number of lists: {len(panel_spy.spied_lists)}"
|
||||
debug = []
|
||||
for i, template_list in enumerate(panel_spy.spied_lists):
|
||||
if i + 1 != nth:
|
||||
if i + 1 != nth_:
|
||||
continue
|
||||
for row in template_list.rows:
|
||||
is_row = False
|
||||
@@ -440,7 +484,8 @@ def i_click_button_in_the_row_where_i_see_text_in_the_nth_list(button, text, nth
|
||||
is_row = True
|
||||
if is_row:
|
||||
_i_click_button_on_panel(button, row)
|
||||
return True
|
||||
# Success.
|
||||
return
|
||||
debug = "\n".join(debug)
|
||||
assert False, f"Could not see '{text}' in any list. We saw:\n{debug}"
|
||||
|
||||
@@ -502,7 +547,7 @@ def i_see_the_prop_property(prop):
|
||||
panel_spy.refresh_spy()
|
||||
assert [
|
||||
p for p in panel_spy.spied_props if prop in (p["name"], p["text"], p["icon"])
|
||||
], f"Property {prop} not found in {panel_spy.spied_props}"
|
||||
], f"Property {prop} not found in {pprint.pformat(panel_spy.spied_props)}"
|
||||
|
||||
|
||||
@given(parsers.parse('I don\'t see the "{prop}" property'))
|
||||
@@ -513,7 +558,7 @@ def i_dont_see_the_prop_property(prop):
|
||||
panel_spy.refresh_spy()
|
||||
assert not [
|
||||
p for p in panel_spy.spied_props if prop in (p["name"], p["text"], p["icon"])
|
||||
], f"Property {prop} not found in {panel_spy.spied_props}"
|
||||
], f"Property {prop} not found in {pprint.pformat(panel_spy.spied_props)}"
|
||||
|
||||
|
||||
@given(parsers.parse('I see the "{prop}" property is "{value}"'))
|
||||
@@ -528,7 +573,7 @@ def i_see_the_prop_property_is_value(prop, value):
|
||||
spied_prop["value"] == value
|
||||
), f"Property {prop} value is not {value} - it is actually {spied_prop['value']}"
|
||||
return
|
||||
assert False, f"Property {prop} not found in {panel_spy.spied_props}"
|
||||
assert False, f"Property {prop} not found in {pprint.pformat(panel_spy.spied_props)}"
|
||||
|
||||
|
||||
@given(parsers.parse('I set the "{prop}" property to "{value}"'))
|
||||
@@ -538,12 +583,14 @@ def i_set_the_prop_property_to_value(prop: str, value: str):
|
||||
"""
|
||||
:param prop: Could be either property name, property text, property icon
|
||||
or property index (e.g. "1st", "2nd", "5th").
|
||||
:param value:
|
||||
For boolean propeties - 'TRUE' or 'FALSE'.
|
||||
"""
|
||||
value = value.strip()
|
||||
assert panel_spy
|
||||
panel_spy.refresh_spy()
|
||||
is_nth = False
|
||||
if prop[0].isnumeric() and (prop.endswith("st") or prop.endswith("nd") or prop.endswith("th")):
|
||||
if prop[0].isnumeric() and prop.endswith(("st", "nd", "th")):
|
||||
is_nth = True
|
||||
for nth, spied_prop in enumerate(panel_spy.spied_props):
|
||||
if is_nth and nth != int(prop[:-2]) - 1:
|
||||
@@ -555,6 +602,8 @@ def i_set_the_prop_property_to_value(prop: str, value: str):
|
||||
setattr(spied_prop["props"], spied_prop["name"], True)
|
||||
elif value == "FALSE":
|
||||
setattr(spied_prop["props"], spied_prop["name"], False)
|
||||
else:
|
||||
assert False, f"Unexpected value for BOOLEAN property: '{value}'. Allowed values: TRUE, FALSE."
|
||||
elif spied_prop["prop_type"] == "FLOAT":
|
||||
setattr(spied_prop["props"], spied_prop["name"], float(value))
|
||||
elif spied_prop["prop_type"] == "INT":
|
||||
@@ -590,12 +639,16 @@ def the_name_list_has_total_items(name, total):
|
||||
@given(parsers.parse('I select the "{item_name}" item in the "{list_name}" list'))
|
||||
@when(parsers.parse('I select the "{item_name}" item in the "{list_name}" list'))
|
||||
@then(parsers.parse('I can select the "{item_name}" item in the "{list_name}" list'))
|
||||
def i_select_the_item_name_item_in_the_list_name_list(item_name, list_name):
|
||||
def i_select_the_item_name_item_in_the_list_name_list(item_name: str, list_name: str) -> None:
|
||||
"""
|
||||
:param item_name: The ``.name`` of the item to select.
|
||||
:param list_name: List type name, e.g. ``BIM_UL_containers_manager``.
|
||||
"""
|
||||
assert panel_spy
|
||||
panel_spy.refresh_spy()
|
||||
for template_list in panel_spy.spied_lists:
|
||||
if list_name == template_list.spied_data["listtype_name"]:
|
||||
item_names = []
|
||||
item_names: list[str] = []
|
||||
for i, item in enumerate(template_list.items):
|
||||
item_names.append(item.name)
|
||||
if item.name == item_name:
|
||||
@@ -739,9 +792,11 @@ def i_press_operator_and_expect_error(operator, error_msg):
|
||||
assert False, f"Operator bpy.ops.{operator} ran without exception '{error_msg}'"
|
||||
|
||||
|
||||
# TODO: We should rename 'I press' to 'I run' or 'I execute'
|
||||
# to avoid confusion with 'I click'.
|
||||
@given(parsers.parse('I press "{operator}"'))
|
||||
@when(parsers.parse('I press "{operator}"'))
|
||||
def i_press_operator(operator):
|
||||
def i_press_operator(operator: str) -> types.NoneType:
|
||||
operator = replace_variables(operator)
|
||||
try:
|
||||
if "(" in operator:
|
||||
@@ -753,7 +808,7 @@ def i_press_operator(operator):
|
||||
assert False, f"Failed to run operator bpy.ops.{operator} because of {e}"
|
||||
|
||||
|
||||
def _i_click_button_on_panel(button, panel_spy):
|
||||
def _i_click_button_on_panel(button: str, panel_spy: PanelSpy) -> None:
|
||||
for spied_operator in panel_spy.spied_operators:
|
||||
if spied_operator["text"] == button or spied_operator["icon"] == button:
|
||||
spied_operator["operator"]("INVOKE_DEFAULT", **spied_operator["kwargs"])
|
||||
@@ -766,11 +821,12 @@ def _i_click_button_on_panel(button, panel_spy):
|
||||
setattr(spied_prop["props"], spied_prop["name"], not bool(val))
|
||||
panel_spy.is_spy_dirty = True
|
||||
return
|
||||
if button == "OK" and panel_spy.panel.bl_rna.base.name == "Operator":
|
||||
if button == "OK" and panel_spy.blender_panel.bl_rna.base.name == "Operator":
|
||||
# Clicked confirm on an operator's draw dialog
|
||||
return i_press_operator(panel_spy.panel.bl_idname)
|
||||
return i_press_operator(panel_spy.blender_panel.bl_idname)
|
||||
debug = "\n".join([f"{i} {v}" for i, v in enumerate(panel_spy.spied_operators)])
|
||||
debug += f"\nHere is the text we see:\n{panel_spy.spied_labels}\n... and props:\n {panel_spy.spied_props}"
|
||||
debug_props = pprint.pformat(panel_spy.spied_props)
|
||||
debug += f"\nHere is the text we see:\n{panel_spy.spied_labels}\n... and props:\n {debug_props}"
|
||||
assert False, f"Could not find {button}:\n{debug}"
|
||||
|
||||
|
||||
@@ -996,11 +1052,9 @@ def the_object_name_exists(name: str) -> bpy.types.Object:
|
||||
|
||||
|
||||
@then(parsers.parse('the object "{name}" does not exist'))
|
||||
def the_object_name_does_not_exist(name) -> bpy.types.Object:
|
||||
def the_object_name_does_not_exist(name) -> None:
|
||||
obj = bpy.data.objects.get(name)
|
||||
if not obj:
|
||||
assert True, f'The object "{name}" exists'
|
||||
return obj
|
||||
assert obj is None, f'The object "{name}" exists'
|
||||
|
||||
|
||||
@given(parsers.parse('the collection "{name}" exists'))
|
||||
@@ -1729,19 +1783,19 @@ def i_fail():
|
||||
@given(parsers.parse("I save sample test files"))
|
||||
@when(parsers.parse("I save sample test files"))
|
||||
@then(parsers.parse("I save sample test files"))
|
||||
def saving_sample_test_files(and_open_in_blender=None):
|
||||
filepath = f"{variables['cwd']}/test/files/temp/sample_test_file"
|
||||
def saving_sample_test_files() -> None:
|
||||
filepath = TMP / "sample_test_file"
|
||||
print(f"Saved to {filepath}")
|
||||
bpy.ops.bim.save_project(filepath=f"{filepath}.ifc", should_save_as=True)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=f"{filepath}.blend")
|
||||
bpy.ops.bim.save_project(filepath=filepath.with_suffix(".ifc").__str__(), should_save_as=True)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=filepath.with_suffix(".blend").__str__())
|
||||
|
||||
|
||||
@given(parsers.parse("I load test blend file"))
|
||||
@when(parsers.parse("I load test blend file"))
|
||||
@then(parsers.parse("I load test blend file"))
|
||||
def opening_sample_test_files_in_blender():
|
||||
filepath = f"{variables['cwd']}/test/files/temp/sample_test_file.blend"
|
||||
bpy.ops.wm.open_mainfile(filepath=filepath, display_file_selector=False)
|
||||
filepath = TMP / "sample_test_file.blend"
|
||||
bpy.ops.wm.open_mainfile(filepath=filepath.__str__(), display_file_selector=False)
|
||||
|
||||
|
||||
# TODO: merge to single fixture with `saving_sample_test_files`; add "and wait"
|
||||
@@ -1763,3 +1817,16 @@ def run_pdb():
|
||||
import pdb
|
||||
|
||||
pdb.set_trace()
|
||||
|
||||
|
||||
@given(parsers.parse("I start debugpy"))
|
||||
@when(parsers.parse("I start debugpy"))
|
||||
@then(parsers.parse("I start debugpy"))
|
||||
def run_debugpy() -> types.NoneType:
|
||||
import debugpy
|
||||
|
||||
debugpy.listen(5678)
|
||||
debugpy.wait_for_client()
|
||||
|
||||
# Set a breakpoint here.
|
||||
print("debugpy connected")
|
||||
|
||||
@@ -107,6 +107,13 @@ def georeference():
|
||||
prophet.verify()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def group():
|
||||
prophet = Prophecy(bonsai.core.tool.Group)
|
||||
yield prophet
|
||||
prophet.verify()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def library():
|
||||
prophet = Prophecy(bonsai.core.tool.Library)
|
||||
|
||||
@@ -398,6 +398,7 @@ class TestDuplicateDrawing:
|
||||
drawing.get_name("drawing").should_be_called().will_return("name")
|
||||
drawing.ensure_unique_drawing_name("name").should_be_called().will_return("unique_name")
|
||||
ifc.run("root.copy_class", product="drawing").should_be_called().will_return("new_drawing")
|
||||
drawing.clear_annotation_relationships("new_drawing").should_be_called()
|
||||
drawing.copy_representation("drawing", "new_drawing").should_be_called()
|
||||
drawing.set_name("new_drawing", "unique_name").should_be_called()
|
||||
drawing.get_drawing_group("new_drawing").should_be_called().will_return("group")
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
|
||||
import bonsai.core.system as subject
|
||||
from test.core.bootstrap import ifc, system, spatial
|
||||
from test.core.bootstrap import ifc, system, spatial, group
|
||||
|
||||
|
||||
class TestLoadSystems:
|
||||
@@ -37,10 +37,17 @@ class TestDisableSystemEditingUI:
|
||||
|
||||
|
||||
class TestAddSystem:
|
||||
def test_run(self, ifc, system):
|
||||
def test_run(self, ifc, group, system):
|
||||
ifc.run("system.add_system", ifc_class="ifc_class").should_be_called()
|
||||
system.import_systems().should_be_called()
|
||||
subject.add_system(ifc, system, ifc_class="ifc_class")
|
||||
subject.add_system(ifc, group, system, ifc_class="ifc_class", parent_system=None)
|
||||
|
||||
def test_run_with_parent_system(self, ifc, group, system):
|
||||
ifc.run("system.add_system", ifc_class="ifc_class").should_be_called().will_return("new_system")
|
||||
ifc.run("group.assign_group", products=["new_system"], group="parent_system").should_be_called()
|
||||
group.toggle_group("parent_system", "IfcSystem", "EXPAND").should_be_called()
|
||||
system.import_systems().should_be_called()
|
||||
subject.add_system(ifc, group, system, ifc_class="ifc_class", parent_system="parent_system")
|
||||
|
||||
|
||||
class TestEditSystem:
|
||||
@@ -53,10 +60,11 @@ class TestEditSystem:
|
||||
|
||||
|
||||
class TestRemoveSystem:
|
||||
def test_run(self, ifc, system):
|
||||
def test_run(self, ifc, group, system):
|
||||
ifc.run("system.remove_system", system="system").should_be_called()
|
||||
system.import_systems().should_be_called()
|
||||
subject.remove_system(ifc, system, system="system")
|
||||
group.update_uilist_index("IfcSystem").should_be_called()
|
||||
subject.remove_system(ifc, group, system, system="system")
|
||||
|
||||
|
||||
class TestEnableEditingSystem:
|
||||
|
||||
@@ -37,7 +37,7 @@ class TestImplementsTool(NewFile):
|
||||
class TestCopyNodeGraph(NewFile):
|
||||
def test_run(self):
|
||||
material_to = bpy.data.materials.new("material_to")
|
||||
material_to.use_nodes = True
|
||||
tool.Style.set_use_nodes(material_to, True)
|
||||
assert material_to.node_tree
|
||||
material_to_nodes = material_to.node_tree.nodes
|
||||
assert len(material_to_nodes) == 2
|
||||
@@ -46,7 +46,7 @@ class TestCopyNodeGraph(NewFile):
|
||||
assert len(material_to_nodes) == 0
|
||||
|
||||
material_from = bpy.data.materials.new("material_from")
|
||||
material_from.use_nodes = True
|
||||
tool.Style.set_use_nodes(material_from, True)
|
||||
|
||||
subject.copy_node_graph(material_to, material_from)
|
||||
assert len(material_to_nodes) == 2
|
||||
|
||||
@@ -155,6 +155,9 @@ class TestDisableEditingText(NewFile):
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
props.is_editing = True
|
||||
subject.disable_editing_text(obj)
|
||||
|
||||
# Props might get invalidated, since no properties set.
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
assert props.is_editing == False
|
||||
|
||||
|
||||
@@ -812,8 +815,9 @@ class TestDrawingMaintainingSheetPosition(NewFile):
|
||||
def test_run(self):
|
||||
props = tool.Drawing.get_document_props()
|
||||
bpy.ops.bim.create_project()
|
||||
tool.Project.save_test_project()
|
||||
ifc = tool.Ifc.get()
|
||||
sheet_path = Path.cwd() / "layouts" / "A01 - UNTITLED.svg"
|
||||
sheet_path = Path(tool.Ifc.get_path()).parent / "layouts" / "A01 - UNTITLED.svg"
|
||||
|
||||
bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4))
|
||||
obj = bpy.data.objects["Cube"]
|
||||
@@ -867,6 +871,7 @@ class TestDrawingMaintainingSheetPosition(NewFile):
|
||||
class TestDrawingStyles(NewFile):
|
||||
def setup_project_with_drawing(self):
|
||||
bpy.ops.bim.create_project()
|
||||
tool.Project.save_test_project()
|
||||
bpy.ops.bim.load_drawings()
|
||||
bpy.ops.bim.add_drawing()
|
||||
ifc = tool.Ifc.get()
|
||||
@@ -878,19 +883,13 @@ class TestDrawingStyles(NewFile):
|
||||
props = tool.Drawing.get_document_props()
|
||||
self.drawing_styles = props.drawing_styles
|
||||
|
||||
def test_drawing_styles_not_loaded_if_underlay_is_inactive(self):
|
||||
def test_drawing_styles_are_loaded_even_if_underlay_is_inactive(self):
|
||||
self.setup_project_with_drawing()
|
||||
assert len(self.drawing_styles) == 0
|
||||
|
||||
def test_drawing_styles_loaded_on_underlay_enabled(self):
|
||||
self.setup_project_with_drawing()
|
||||
assert (scene := bpy.context.scene) and (camera := scene.camera)
|
||||
props = tool.Drawing.get_camera_props(camera)
|
||||
props.has_underlay = True
|
||||
assert len(self.drawing_styles) == 3
|
||||
|
||||
def test_drawing_styles_reload(self):
|
||||
self.setup_project_with_drawing()
|
||||
self.drawing_styles.clear()
|
||||
bpy.ops.bim.reload_drawing_styles()
|
||||
assert len(self.drawing_styles) == 3
|
||||
|
||||
|
||||
@@ -321,8 +321,8 @@ class TestRecordObjectPosition(NewFile):
|
||||
obj = bpy.data.objects.new("Object", None)
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
subject.record_object_position(obj)
|
||||
assert props.location_checksum == repr(np.array(obj.matrix_world.translation).tobytes())
|
||||
assert props.rotation_checksum == repr(np.array(obj.matrix_world.to_3x3()).tobytes())
|
||||
assert props.location_checksum == repr(np.array(obj.matrix_world.translation, dtype=np.float32).tobytes())
|
||||
assert props.rotation_checksum == repr(np.array(obj.matrix_world.to_3x3(), dtype=np.float32).tobytes())
|
||||
|
||||
|
||||
class TestRemoveConnection(NewFile):
|
||||
@@ -488,7 +488,7 @@ class TestShouldGenerateUVs(NewFile):
|
||||
obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh"))
|
||||
material = bpy.data.materials.new("Material")
|
||||
obj.data.materials.append(material)
|
||||
material.use_nodes = False
|
||||
tool.Style.set_use_nodes(material, False)
|
||||
assert subject.should_generate_uvs(obj) is False
|
||||
|
||||
def test_needs_texture_coordinates_with_a_uv_output(self):
|
||||
@@ -497,7 +497,7 @@ class TestShouldGenerateUVs(NewFile):
|
||||
obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh"))
|
||||
material = bpy.data.materials.new("Material")
|
||||
obj.data.materials.append(material)
|
||||
material.use_nodes = True
|
||||
tool.Style.set_use_nodes(material, True)
|
||||
|
||||
bsdf = tool.Blender.get_material_node(material, "BSDF_PRINCIPLED")
|
||||
node = material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
||||
@@ -513,7 +513,7 @@ class TestShouldGenerateUVs(NewFile):
|
||||
obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh"))
|
||||
material = bpy.data.materials.new("Material")
|
||||
obj.data.materials.append(material)
|
||||
material.use_nodes = True
|
||||
tool.Style.set_use_nodes(material, True)
|
||||
|
||||
bsdf = tool.Blender.get_material_node(material, "BSDF_PRINCIPLED")
|
||||
node = material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
||||
|
||||
@@ -30,6 +30,7 @@ import bonsai.core.tool
|
||||
import bonsai.tool as tool
|
||||
import numpy as np
|
||||
import json
|
||||
from typing import Any
|
||||
from test.bim.bootstrap import NewFile
|
||||
from bonsai.tool.model import Model as subject
|
||||
from ifcopenshell.util.shape_builder import V, ShapeBuilder
|
||||
@@ -159,7 +160,7 @@ class TestStairCalculatedParams(NewFile):
|
||||
"number_of_treads": 3,
|
||||
"height": 1.0,
|
||||
"tread_run": 0.3,
|
||||
"custom_first_last_tread_run": (0.0, 0.0),
|
||||
"custom_first_last_tread_run": (None, None),
|
||||
"nosing_length": 0.0,
|
||||
}
|
||||
calculated_data_base = {
|
||||
@@ -179,14 +180,14 @@ class TestStairCalculatedParams(NewFile):
|
||||
# zero-width first tread
|
||||
pset_data = pset_data_base.copy()
|
||||
calculated_data = calculated_data_base.copy()
|
||||
pset_data["custom_first_last_tread_run"] = (0.0, 0.0)
|
||||
pset_data["custom_first_last_tread_run"] = (0.0, None)
|
||||
calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each
|
||||
self.compare_data(pset_data, calculated_data)
|
||||
|
||||
# zero-width last tread
|
||||
pset_data = pset_data_base.copy()
|
||||
calculated_data = calculated_data_base.copy()
|
||||
pset_data["custom_first_last_tread_run"] = (0.3, 0.0)
|
||||
pset_data["custom_first_last_tread_run"] = (None, 0.0)
|
||||
calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each
|
||||
self.compare_data(pset_data, calculated_data)
|
||||
|
||||
@@ -222,18 +223,20 @@ class TestGenerateStair2DProfile(NewFile):
|
||||
for vert, vert_gen in zip(verts, verts_gen, strict=True):
|
||||
assert np.allclose(vert, V(vert_gen), atol=0.01)
|
||||
|
||||
CONCRETE_STAIR_KWARGS: dict[str, Any] = {
|
||||
"base_slab_depth": 0.25,
|
||||
"has_top_nib": False,
|
||||
"height": 1.0,
|
||||
"number_of_treads": 3,
|
||||
"stair_type": "CONCRETE",
|
||||
"top_slab_depth": 0.25,
|
||||
"tread_depth": 0.25,
|
||||
"tread_run": 0.3,
|
||||
"width": 1.2,
|
||||
}
|
||||
|
||||
def test_create_concrete_stair(self):
|
||||
kwargs = {
|
||||
"base_slab_depth": 0.25,
|
||||
"has_top_nib": False,
|
||||
"height": 1.0,
|
||||
"number_of_treads": 3,
|
||||
"stair_type": "CONCRETE",
|
||||
"top_slab_depth": 0.25,
|
||||
"tread_depth": 0.25,
|
||||
"tread_run": 0.3,
|
||||
"width": 1.2,
|
||||
}
|
||||
kwargs = self.CONCRETE_STAIR_KWARGS.copy()
|
||||
verts_data = (
|
||||
V(0.0, 0, 0.0),
|
||||
V(0.0, 0, 0.25),
|
||||
@@ -269,17 +272,8 @@ class TestGenerateStair2DProfile(NewFile):
|
||||
self.compare_data(generated_profile, expected_profile)
|
||||
|
||||
def test_create_concrete_stair_nib(self):
|
||||
kwargs = {
|
||||
"base_slab_depth": 0.25,
|
||||
"has_top_nib": True,
|
||||
"height": 1.0,
|
||||
"number_of_treads": 3,
|
||||
"stair_type": "CONCRETE",
|
||||
"top_slab_depth": 0.25,
|
||||
"tread_depth": 0.25,
|
||||
"tread_run": 0.3,
|
||||
"width": 1.2,
|
||||
}
|
||||
kwargs = self.CONCRETE_STAIR_KWARGS.copy()
|
||||
kwargs["has_top_nib"] = True
|
||||
verts_data = (
|
||||
V(0.0, 0, 0.0),
|
||||
V(0.0, 0, 0.25),
|
||||
@@ -318,19 +312,8 @@ class TestGenerateStair2DProfile(NewFile):
|
||||
self.compare_data(generated_profile, expected_profile)
|
||||
|
||||
def test_create_concrete_stair_zero_width_first_tread(self):
|
||||
"""Test concrete stair with zero-width first tread"""
|
||||
kwargs = {
|
||||
"base_slab_depth": 0.25,
|
||||
"has_top_nib": False,
|
||||
"height": 1.0,
|
||||
"number_of_treads": 3,
|
||||
"stair_type": "CONCRETE",
|
||||
"top_slab_depth": 0.25,
|
||||
"tread_depth": 0.25,
|
||||
"tread_run": 0.3,
|
||||
"width": 1.2,
|
||||
"custom_first_last_tread_run": (0.0, 0.0),
|
||||
}
|
||||
kwargs = self.CONCRETE_STAIR_KWARGS.copy()
|
||||
kwargs["custom_first_last_tread_run"] = (0.0, None)
|
||||
verts_data = (
|
||||
V(0.0, 0, 0.0),
|
||||
# First tread skipped - goes straight to second tread
|
||||
@@ -340,8 +323,8 @@ class TestGenerateStair2DProfile(NewFile):
|
||||
V(0.6, 0, 0.75),
|
||||
V(0.6, 0, 1.0),
|
||||
V(0.9, 0, 1.0),
|
||||
V(0.9, 0, 0.67457),
|
||||
V(0.0, 0, -0.25),
|
||||
V(0.9, 0, 0.6745729),
|
||||
V(0.0, 0, -0.0754271),
|
||||
)
|
||||
edges_data = (
|
||||
(0, 1),
|
||||
@@ -361,19 +344,8 @@ class TestGenerateStair2DProfile(NewFile):
|
||||
self.compare_data(generated_profile, expected_profile)
|
||||
|
||||
def test_create_concrete_stair_zero_width_last_tread(self):
|
||||
"""Test concrete stair with zero-width last tread"""
|
||||
kwargs = {
|
||||
"base_slab_depth": 0.25,
|
||||
"has_top_nib": False,
|
||||
"height": 1.0,
|
||||
"number_of_treads": 3,
|
||||
"stair_type": "CONCRETE",
|
||||
"top_slab_depth": 0.25,
|
||||
"tread_depth": 0.25,
|
||||
"tread_run": 0.3,
|
||||
"width": 1.2,
|
||||
"custom_first_last_tread_run": (0.0, 0.0),
|
||||
}
|
||||
kwargs = self.CONCRETE_STAIR_KWARGS.copy()
|
||||
kwargs["custom_first_last_tread_run"] = (None, 0.0)
|
||||
verts_data = (
|
||||
V(0.0, 0, 0.0),
|
||||
V(0.0, 0, 0.25),
|
||||
@@ -383,7 +355,7 @@ class TestGenerateStair2DProfile(NewFile):
|
||||
V(0.6, 0, 0.75),
|
||||
V(0.9, 0, 0.75),
|
||||
# Last tread skipped
|
||||
V(0.9, 0, 0.67457),
|
||||
V(0.9, 0, 0.42457),
|
||||
V(0.1, 0, -0.25),
|
||||
V(0.0, 0, -0.25),
|
||||
)
|
||||
@@ -405,15 +377,17 @@ class TestGenerateStair2DProfile(NewFile):
|
||||
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
||||
self.compare_data(generated_profile, expected_profile)
|
||||
|
||||
WOOD_STEEL_STAIR_KWARGS: dict[str, Any] = {
|
||||
"height": 1.0,
|
||||
"number_of_treads": 3,
|
||||
"stair_type": "WOOD/STEEL",
|
||||
"tread_depth": 0.25,
|
||||
"tread_run": 0.3,
|
||||
"width": 1.2,
|
||||
}
|
||||
|
||||
def test_create_wood_steel_stair(self):
|
||||
kwargs = {
|
||||
"height": 1.0,
|
||||
"number_of_treads": 3,
|
||||
"stair_type": "WOOD/STEEL",
|
||||
"tread_depth": 0.25,
|
||||
"tread_run": 0.3,
|
||||
"width": 1.2,
|
||||
}
|
||||
kwargs = self.WOOD_STEEL_STAIR_KWARGS.copy()
|
||||
verts_data = (
|
||||
V(0.0, 0, 0.0),
|
||||
V(0.3, 0, 0.0),
|
||||
@@ -458,16 +432,8 @@ class TestGenerateStair2DProfile(NewFile):
|
||||
self.compare_data(generated_profile, expected_profile)
|
||||
|
||||
def test_create_wood_steel_stair_zero_width_first_tread(self):
|
||||
"""Test wood/steel stair with zero-width first tread"""
|
||||
kwargs = {
|
||||
"height": 1.0,
|
||||
"number_of_treads": 3,
|
||||
"stair_type": "WOOD/STEEL",
|
||||
"tread_depth": 0.25,
|
||||
"tread_run": 0.3,
|
||||
"width": 1.2,
|
||||
"custom_first_last_tread_run": (0.0, 0.0),
|
||||
}
|
||||
kwargs = self.WOOD_STEEL_STAIR_KWARGS.copy()
|
||||
kwargs["custom_first_last_tread_run"] = (0.0, None)
|
||||
verts_data = (
|
||||
# First tread skipped - start at second tread
|
||||
V(0.0, 0, 0.25),
|
||||
@@ -506,15 +472,9 @@ class TestGenerateStair2DProfile(NewFile):
|
||||
|
||||
def test_create_wood_steel_stair_zero_width_last_tread(self):
|
||||
"""Test wood/steel stair with zero-width last tread"""
|
||||
kwargs = {
|
||||
"height": 1.0,
|
||||
"number_of_treads": 3,
|
||||
"stair_type": "WOOD/STEEL",
|
||||
"tread_depth": 0.25,
|
||||
"tread_run": 0.3,
|
||||
"width": 1.2,
|
||||
"custom_first_last_tread_run": (0.0, 0.0),
|
||||
}
|
||||
kwargs = self.WOOD_STEEL_STAIR_KWARGS.copy()
|
||||
kwargs["custom_first_last_tread_run"] = (None, 0.0)
|
||||
|
||||
verts_data = (
|
||||
V(0.0, 0, 0.0),
|
||||
V(0.3, 0, 0.0),
|
||||
@@ -551,8 +511,16 @@ class TestGenerateStair2DProfile(NewFile):
|
||||
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
||||
self.compare_data(generated_profile, expected_profile)
|
||||
|
||||
GENERIC_STAIR_KWARGS: dict[str, Any] = {
|
||||
"height": 1.0,
|
||||
"number_of_treads": 3,
|
||||
"stair_type": "GENERIC",
|
||||
"tread_run": 0.3,
|
||||
"width": 1.2,
|
||||
}
|
||||
|
||||
def test_create_generic_stair(self):
|
||||
kwargs = {"height": 1.0, "number_of_treads": 3, "stair_type": "GENERIC", "tread_run": 0.3, "width": 1.2}
|
||||
kwargs = self.GENERIC_STAIR_KWARGS.copy()
|
||||
verts_data = (
|
||||
V(0.0, 0, 0.0),
|
||||
V(0.0, 0, 0.25),
|
||||
@@ -585,15 +553,8 @@ class TestGenerateStair2DProfile(NewFile):
|
||||
self.compare_data(generated_profile, expected_profile)
|
||||
|
||||
def test_create_generic_stair_zero_width_treads(self):
|
||||
"""Test generic stair with zero-width first and last treads"""
|
||||
kwargs = {
|
||||
"height": 1.0,
|
||||
"number_of_treads": 3,
|
||||
"stair_type": "GENERIC",
|
||||
"tread_run": 0.3,
|
||||
"width": 1.2,
|
||||
"custom_first_last_tread_run": (0.0, 0.0),
|
||||
}
|
||||
kwargs = self.GENERIC_STAIR_KWARGS.copy()
|
||||
kwargs["custom_first_last_tread_run"] = (0.0, 0.0)
|
||||
verts_data = (
|
||||
V(0.0, 0, 0.0),
|
||||
# First tread skipped
|
||||
|
||||
@@ -22,6 +22,7 @@ import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.util.representation
|
||||
import pytest
|
||||
import bonsai.core.tool
|
||||
import bonsai.tool as tool
|
||||
from test.bim.bootstrap import NewFile
|
||||
@@ -37,12 +38,16 @@ class TestImplementsTool(NewFile):
|
||||
class TestCanSupportRenderingStyle(NewFile):
|
||||
def test_anything_with_nodes_can_support_a_rendering_style(self):
|
||||
obj = bpy.data.materials.new("Material")
|
||||
obj.use_nodes = True
|
||||
tool.Style.set_use_nodes(obj, True)
|
||||
assert subject.can_support_rendering_style(obj) is True
|
||||
|
||||
@pytest.mark.skipif(
|
||||
tool.Blender.BLENDER_5,
|
||||
reason="Since Blender 5.0 `use_nodes` are always `True` and deprecated.",
|
||||
)
|
||||
def test_without_nodes_we_do_not_support_rendering(self):
|
||||
obj = bpy.data.materials.new("Material")
|
||||
obj.use_nodes = False
|
||||
tool.Style.set_use_nodes(obj, False)
|
||||
assert subject.can_support_rendering_style(obj) is False
|
||||
|
||||
|
||||
@@ -125,7 +130,7 @@ class TestGetSurfaceRenderingAttributes(NewFile):
|
||||
def test_get_different_surface_and_diffuse_colours_from_a_principled_bsdf(self):
|
||||
obj = bpy.data.materials.new("Material")
|
||||
obj.diffuse_color = [1, 1, 1, 1]
|
||||
obj.use_nodes = True
|
||||
tool.Style.set_use_nodes(obj, True)
|
||||
node = tool.Blender.get_material_node(obj, "BSDF_PRINCIPLED")
|
||||
assert node
|
||||
node.inputs["Alpha"].default_value = 0.8
|
||||
@@ -153,7 +158,7 @@ class TestGetSurfaceRenderingAttributes(NewFile):
|
||||
def test_get_rendering_styles_from_a_glossy_bsdf(self):
|
||||
obj = bpy.data.materials.new("Material")
|
||||
obj.diffuse_color = [1, 1, 1, 1]
|
||||
obj.use_nodes = True
|
||||
tool.Style.set_use_nodes(obj, True)
|
||||
output = tool.Blender.get_material_node(obj, "OUTPUT_MATERIAL")
|
||||
node = tool.Blender.get_material_node(obj, "BSDF_PRINCIPLED")
|
||||
assert obj.node_tree and node
|
||||
@@ -185,7 +190,7 @@ class TestGetSurfaceRenderingAttributes(NewFile):
|
||||
def test_get_rendering_styles_from_a_diffuse_bsdf(self):
|
||||
obj = bpy.data.materials.new("Material")
|
||||
obj.diffuse_color = [1, 1, 1, 1]
|
||||
obj.use_nodes = True
|
||||
tool.Style.set_use_nodes(obj, True)
|
||||
output = tool.Blender.get_material_node(obj, "OUTPUT_MATERIAL")
|
||||
node = tool.Blender.get_material_node(obj, "BSDF_PRINCIPLED")
|
||||
assert obj.node_tree and node and output
|
||||
@@ -217,7 +222,7 @@ class TestGetSurfaceRenderingAttributes(NewFile):
|
||||
def test_get_rendering_styles_from_a_glass_bsdf(self):
|
||||
obj = bpy.data.materials.new("Material")
|
||||
obj.diffuse_color = [1, 1, 1, 1]
|
||||
obj.use_nodes = True
|
||||
tool.Style.set_use_nodes(obj, True)
|
||||
output = tool.Blender.get_material_node(obj, "OUTPUT_MATERIAL")
|
||||
node = tool.Blender.get_material_node(obj, "BSDF_PRINCIPLED")
|
||||
assert obj.node_tree and node and output
|
||||
@@ -249,7 +254,7 @@ class TestGetSurfaceRenderingAttributes(NewFile):
|
||||
def test_get_rendering_styles_from_a_emission_bsdf(self):
|
||||
obj = bpy.data.materials.new("Material")
|
||||
obj.diffuse_color = [1, 1, 1, 1]
|
||||
obj.use_nodes = True
|
||||
tool.Style.set_use_nodes(obj, True)
|
||||
output = tool.Blender.get_material_node(obj, "OUTPUT_MATERIAL")
|
||||
node = tool.Blender.get_material_node(obj, "BSDF_PRINCIPLED")
|
||||
assert obj.node_tree and node and output
|
||||
@@ -280,7 +285,7 @@ class TestGetSurfaceRenderingAttributes(NewFile):
|
||||
def test_other_unsupported_bsdfs_copy_the_rendering_style_from_the_shading_colours_as_a_fallback(self):
|
||||
obj = bpy.data.materials.new("Material")
|
||||
obj.diffuse_color = [1, 1, 1, 1]
|
||||
obj.use_nodes = True
|
||||
tool.Style.set_use_nodes(obj, True)
|
||||
output = tool.Blender.get_material_node(obj, "OUTPUT_MATERIAL")
|
||||
node = tool.Blender.get_material_node(obj, "BSDF_PRINCIPLED")
|
||||
assert obj.node_tree and node and output
|
||||
|
||||
+24
-9
@@ -92,24 +92,39 @@ def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_inst
|
||||
|
||||
for calculator, queries in rules["calculators"].items():
|
||||
calculator = calculators[calculator]
|
||||
if not set(m.lower() for m in queries.keys()) - lower_case_entity_names(ifc_file.schema_identifier):
|
||||
# all defined queries are actually simple entity names: instead of looping over all
|
||||
# calculators we loop over the entity types so that we don't have to repeatedly query
|
||||
# the model, especially when the set of elements is small.
|
||||
# Cache schema entity names once per file
|
||||
schema_entity_names = lower_case_entity_names(ifc_file.schema_identifier)
|
||||
|
||||
# Fast path: all queries are simple entity names
|
||||
if not set(m.lower() for m in queries.keys()) - schema_entity_names:
|
||||
casenorm = {k.lower(): k for k in queries.keys()}
|
||||
pred = lambda inst: inst.is_a().lower()
|
||||
for ty, elements in itertools.groupby(sorted(elements, key=pred), key=pred):
|
||||
|
||||
for ty, group in itertools.groupby(sorted(elements, key=pred), key=pred):
|
||||
group_elements = list(group)
|
||||
# check entity type and its supertypes for matching QTO rules
|
||||
for sty in entity_supertypes(ifc_file.schema_identifier, ty):
|
||||
if qtos := queries.get(casenorm.get(sty)):
|
||||
calculator.calculate(ifc_file, list(elements), qtos, results)
|
||||
calculator.calculate(ifc_file, group_elements, qtos, results)
|
||||
continue # Skip general path to avoid duplicate calculations
|
||||
|
||||
# Fallback: per-query evaluation
|
||||
for query, qtos in queries.items():
|
||||
if query.lower() in lower_case_entity_names(ifc_file.schema_identifier):
|
||||
# by_type is faster than selector parsing
|
||||
filtered_elements = ifc_file.by_type(query)
|
||||
# Simple entity name: use by_type but restrict to incoming elements
|
||||
if query.lower() in schema_entity_names:
|
||||
by_type_all = ifc_file.by_type(query)
|
||||
# ensure we don't expand beyond provided elements subset
|
||||
if isinstance(elements, set):
|
||||
filtered_elements = [e for e in by_type_all if e in elements]
|
||||
else:
|
||||
elements_set = set(elements)
|
||||
filtered_elements = [e for e in by_type_all if e in elements_set]
|
||||
else:
|
||||
filtered_elements = ifcopenshell.util.selector.filter_elements(ifc_file, query, elements)
|
||||
|
||||
if filtered_elements:
|
||||
calculator.calculate(ifc_file, filtered_elements, qtos, results)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
|
||||
@@ -176,7 +176,8 @@ def import_ifc(filename, use_names, process_relations, blender_booleans):
|
||||
mat.blend_method = "HASHED"
|
||||
mat.use_screen_refraction = True
|
||||
mat.refraction_depth = 0.1
|
||||
mat.use_nodes = True
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
mat.use_nodes = True
|
||||
bsdf = next(n for n in mat.node_tree.nodes if n.type == "BSDF_PRINCIPLED")
|
||||
bsdf.inputs[15].default_value = v
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# IfcClash
|
||||
|
||||
IFC clash detection library to handle intersections, collisions, and clearance checks between building elements in IFC models.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multiple detection modes**: intersection, collision, and clearance checking
|
||||
- **Flexible clash sets**: Compare elements within a single model or between multiple IFC files
|
||||
- **Smart grouping**: Automatically group related clashes
|
||||
- **Export options**: Export clash results to BCF-XML or JSON formats
|
||||
- **Element filtering**: Use selectors to specify which elements to check for clashes
|
||||
@@ -0,0 +1,12 @@
|
||||
# IfcCSV
|
||||
|
||||
A utility to interact with IFC data through CSV. Export and import IFC data from and to CSV, ODS, XLSX formats, and pandas DataFrames. Enables bulk editing of IFC properties and schedules using spreadsheet tools.
|
||||
|
||||
## Features
|
||||
|
||||
- **Export IFC data**: Extract element properties and attributes to spreadsheet formats
|
||||
- **Import changes**: Update IFC models from modified spreadsheets
|
||||
- **Multiple formats**: Support for CSV, ODS (OpenDocument Spreadsheet), XLSX (Excel), and pandas DataFrames
|
||||
- **Flexible queries**: Use selectors to specify which elements and attributes to export
|
||||
- **Data transformations**: Group, summarize, format, and sort results
|
||||
- **Preserve existing data**: Update existing spreadsheets while preserving formatting
|
||||
@@ -489,6 +489,9 @@ class IfcCsv:
|
||||
bool_false: str,
|
||||
concat: str,
|
||||
) -> None:
|
||||
# Patterns to skip during import (substrings)
|
||||
SKIP_PATTERNS = {"count", "material"}
|
||||
|
||||
try:
|
||||
element = ifc_file.by_guid(row[0])
|
||||
except:
|
||||
@@ -506,6 +509,11 @@ class IfcCsv:
|
||||
elif value == bool_false:
|
||||
value = False
|
||||
key = attributes[i] or headers[i]
|
||||
|
||||
# Skip keys containing certain patterns
|
||||
if any(pattern in key.lower() for pattern in SKIP_PATTERNS):
|
||||
continue
|
||||
|
||||
ifcopenshell.util.selector.set_element_value(ifc_file, element, key, value, concat=concat)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# IfcDiff
|
||||
|
||||
Compare IFC models for changes to geometry, properties, and structure. Compares the differences between two IFC models, including changes to geometry, properties, relationships, and spatial structure. Outputs differences in JSON format for further processing or reporting.
|
||||
|
||||
## Features
|
||||
|
||||
- **Element tracking**: Identify added and deleted elements between models
|
||||
- **Relationship comparison**: Track changes in multiple relationship types:
|
||||
- Geometry
|
||||
- Attributes
|
||||
- Type assignments
|
||||
- Properties
|
||||
- Spatial containers
|
||||
- Aggregations
|
||||
- Classifications
|
||||
- **Flexible filtering**: Compare only specific element types using IFC selectors
|
||||
- **Configurable depth**: Choose between shallow (first difference only) or deep (all differences) comparison
|
||||
- **JSON export**: Export comparison results in JSON format for further processing
|
||||
- **Precision control**: Configure geometric comparison precision
|
||||
@@ -8,7 +8,7 @@ version = "0.0.0"
|
||||
authors = [
|
||||
{ name="Dion Moult", email="dion@thinkmoult.com" },
|
||||
]
|
||||
description = "Compares the differences between two IFC models"
|
||||
description = "Compare IFC models for changes to geometry, properties, and structure"
|
||||
readme = "README.md"
|
||||
keywords = ["IFC", "Diff"]
|
||||
classifiers = [
|
||||
|
||||
@@ -670,12 +670,23 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSurfaceStyle* style) {
|
||||
if (rendering_style->TransmissionColour()) {
|
||||
// Not supported
|
||||
}
|
||||
#ifndef SCHEMA_IfcSurfaceStyleShading_HAS_Transparency
|
||||
// ifc2x3
|
||||
if (rendering_style->Transparency()) {
|
||||
const double d = *rendering_style->Transparency();
|
||||
surface_style->transparency = d;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
#ifdef SCHEMA_IfcSurfaceStyleShading_HAS_Transparency
|
||||
// ifc4 and onwards
|
||||
if (shading->Transparency()) {
|
||||
const double d = *shading->Transparency();
|
||||
surface_style->transparency = d;
|
||||
}
|
||||
#endif
|
||||
|
||||
return surface_style;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,9 @@ endif
|
||||
ifeq ($(PLATFORM), linux64)
|
||||
PLATFORMTAG:=manylinux_2_31_x86_64
|
||||
endif
|
||||
ifeq ($(PLATFORM), linuxarm64)
|
||||
PLATFORMTAG:=manylinux_2_31_aarch64
|
||||
endif
|
||||
ifeq ($(PLATFORM), macos64)
|
||||
PLATFORMTAG:=macosx_10_15_x86_64
|
||||
endif
|
||||
@@ -66,8 +69,9 @@ PLATFORMTAG:=win_amd64
|
||||
endif
|
||||
|
||||
BINARY_VERSION:=0.8.4
|
||||
IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v$(BINARY_VERSION)-6924012-$(PLATFORM).zip
|
||||
IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v$(BINARY_VERSION)-6924012-$(PLATFORM).zip
|
||||
BUILD_COMMIT:=e8eb5e4
|
||||
IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v$(BINARY_VERSION)-$(BUILD_COMMIT)-$(PLATFORM).zip
|
||||
IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v$(BINARY_VERSION)-$(BUILD_COMMIT)-$(PLATFORM).zip
|
||||
|
||||
.PHONY: build-urls
|
||||
build-urls:
|
||||
|
||||
@@ -25,6 +25,7 @@ import zipfile
|
||||
import functools
|
||||
import ifcopenshell
|
||||
import weakref
|
||||
import types
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, TYPE_CHECKING, Union, overload, Literal, TypedDict
|
||||
from collections.abc import Callable, Generator
|
||||
@@ -248,6 +249,8 @@ READ_ERROR = ifcopenshell_wrapper.file_open_status.READ_ERROR
|
||||
NO_HEADER = ifcopenshell_wrapper.file_open_status.NO_HEADER
|
||||
UNSUPPORTED_SCHEMA = ifcopenshell_wrapper.file_open_status.UNSUPPORTED_SCHEMA
|
||||
INVALID_SYNTAX = ifcopenshell_wrapper.file_open_status.INVALID_SYNTAX
|
||||
|
||||
# TODO: Workaround for old builds, remove after build stabilizes.
|
||||
try:
|
||||
UNKNOWN = ifcopenshell_wrapper.file_open_status.UNKNOWN
|
||||
except:
|
||||
@@ -535,7 +538,6 @@ class file:
|
||||
"""
|
||||
|
||||
wrapped_data: ifcopenshell_wrapper.file
|
||||
header: file_header
|
||||
units: dict[str, entity_instance] = {}
|
||||
history_size: int = 64
|
||||
history: list[Transaction]
|
||||
@@ -1049,10 +1051,13 @@ class file:
|
||||
return self.wrapped_data.to_string()
|
||||
|
||||
@property
|
||||
def header(self):
|
||||
try:
|
||||
def header(self) -> file_header:
|
||||
# TODO: Workaround for old builds, remove after build stabilizes.
|
||||
# TODO: No need for `wrapped_data.header` to be a method - should use `@property`?
|
||||
header = self.wrapped_data.header
|
||||
if isinstance(header, types.MethodType):
|
||||
return file_header(self, self.wrapped_data.header())
|
||||
except:
|
||||
else:
|
||||
return self.wrapped_data.header
|
||||
|
||||
@property
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
from __future__ import annotations
|
||||
import re
|
||||
import json
|
||||
import types
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
import ifcopenshell
|
||||
@@ -300,8 +301,9 @@ class sqlite(file):
|
||||
# Override to avoid clean up data unrelated to sqlite file.
|
||||
pass
|
||||
|
||||
def wrapped_data(self) -> NoReturn:
|
||||
class_name = str(type(self))
|
||||
@property
|
||||
def wrapped_data(self) -> NoReturn: # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
class_name = type(self).__name__
|
||||
raise Exception(
|
||||
f"No `wrapped_data` for {class_name}. `ifcopenshell.{class_name}` is probably confused with `ifcopenshell.file`."
|
||||
)
|
||||
@@ -315,6 +317,14 @@ class sqlite(file):
|
||||
# The best option we've got for mimicing `file.schema_identifier`.
|
||||
return self._schema
|
||||
|
||||
@property
|
||||
def header(self) -> types.SimpleNamespace:
|
||||
# Mimicking `file_header` object from `ifcopenshell.file`.
|
||||
header = types.SimpleNamespace(
|
||||
file_description=types.SimpleNamespace(description=(self.mvd_str,)),
|
||||
)
|
||||
return header
|
||||
|
||||
|
||||
class sqlite_entity(entity_instance):
|
||||
sqlite_wrapper: sqlite_wrapper
|
||||
|
||||
@@ -287,12 +287,17 @@ try:
|
||||
# Override to avoid clean up unrelated to stream file.
|
||||
pass
|
||||
|
||||
def wrapped_data(self) -> NoReturn:
|
||||
class_name = str(type(self))
|
||||
@property
|
||||
def wrapped_data(self) -> NoReturn: # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
class_name = type(self).__name__
|
||||
raise Exception(
|
||||
f"No `wrapped_data` for {class_name}. `ifcopenshell.{class_name}` is probably confused with `ifcopenshell.file`."
|
||||
)
|
||||
|
||||
@property
|
||||
def header(self) -> NoReturn:
|
||||
raise NotImplementedError("`stream.header` is not implemented yet.")
|
||||
|
||||
@property
|
||||
def schema(self) -> ifcopenshell.util.schema.IFC_SCHEMA:
|
||||
return self._schema
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from timeit import default_timer as timer
|
||||
|
||||
|
||||
class Profiler:
|
||||
"""
|
||||
A python context manager timing utility, useful for measure functions performances
|
||||
"""
|
||||
|
||||
def __init__(self, task):
|
||||
self.task = task
|
||||
|
||||
def __enter__(self):
|
||||
self.start = timer()
|
||||
|
||||
def __exit__(self, *args):
|
||||
print(f"{self.task} {timer() - self.start:.6f} s")
|
||||
@@ -225,10 +225,10 @@ class FormatTransformer(lark.Transformer):
|
||||
if not args:
|
||||
return True
|
||||
token = args[0]
|
||||
if hasattr(token, 'type'):
|
||||
return token.type == 'TRUE'
|
||||
if hasattr(token, "type"):
|
||||
return token.type == "TRUE"
|
||||
value = str(token).lower()
|
||||
if hasattr(token, 'value'):
|
||||
if hasattr(token, "value"):
|
||||
value = str(token.value).lower()
|
||||
return value in ("true", "1", "yes")
|
||||
|
||||
@@ -259,6 +259,7 @@ class FormatTransformer(lark.Transformer):
|
||||
)
|
||||
|
||||
def imperial_length(self, args):
|
||||
args = list(filter(lambda x: x is not None, args))
|
||||
if len(args) == 2:
|
||||
input_unit, output_unit = "foot", "foot"
|
||||
value, precision = args
|
||||
@@ -275,13 +276,14 @@ class FormatTransformer(lark.Transformer):
|
||||
value, precision, input_unit, output_unit, suppress_zero_inches = args
|
||||
input_unit = "inch" if input_unit == "inch" else "foot"
|
||||
output_unit = "inch" if output_unit == "inch" else "foot"
|
||||
|
||||
|
||||
return ifcopenshell.util.unit.format_length(
|
||||
float(value), int(precision),
|
||||
suppress_zero_inches=suppress_zero_inches,
|
||||
unit_system="imperial",
|
||||
input_unit=input_unit,
|
||||
output_unit=output_unit
|
||||
float(value),
|
||||
int(precision),
|
||||
suppress_zero_inches=(suppress_zero_inches if suppress_zero_inches is not None else False),
|
||||
unit_system="imperial",
|
||||
input_unit=input_unit,
|
||||
output_unit=output_unit,
|
||||
)
|
||||
|
||||
def int(self, args: list[str]) -> str:
|
||||
@@ -1026,13 +1028,6 @@ class FacetTransformer(lark.Transformer):
|
||||
):
|
||||
parents.add(parent)
|
||||
|
||||
for rel in self.file.by_type("IfcRelVoidsElement"):
|
||||
parent = rel.RelatingBuildingElement
|
||||
if parent and (
|
||||
self.compare(parent.Name, comparison, value) or self.compare(parent.GlobalId, comparison, value)
|
||||
):
|
||||
parents.add(parent)
|
||||
|
||||
for rel in self.file.by_type("IfcRelFillsElement"):
|
||||
parent = rel.RelatingOpeningElement
|
||||
if parent and (
|
||||
@@ -1040,15 +1035,19 @@ class FacetTransformer(lark.Transformer):
|
||||
):
|
||||
parents.add(parent)
|
||||
|
||||
# Get all children of the matched parents
|
||||
children: set[ifcopenshell.entity_instance] = set()
|
||||
for parent in parents:
|
||||
children |= set(ifcopenshell.util.element.get_decomposition(parent))
|
||||
|
||||
# Combine parents and children into a single result set
|
||||
result = parents | children
|
||||
|
||||
self.add_default_elements()
|
||||
if comparison == "=":
|
||||
self.elements = self.elements & children
|
||||
self.elements = self.elements & result
|
||||
else:
|
||||
self.elements -= children
|
||||
self.elements -= result
|
||||
|
||||
def query(self, args):
|
||||
keys, comparison, value = args
|
||||
|
||||
@@ -405,7 +405,8 @@ class ShapeBuilder:
|
||||
ifc_curve = self.file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
|
||||
return ifc_curve
|
||||
|
||||
def get_rectangle_coords(self, size: VectorType = (1.0, 1.0), position: Optional[VectorType] = None) -> np.ndarray:
|
||||
@staticmethod
|
||||
def get_rectangle_coords(size: VectorType = (1.0, 1.0), position: Optional[VectorType] = None) -> np.ndarray:
|
||||
"""
|
||||
Get rectangle coords arranged as below:
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ authors = [
|
||||
]
|
||||
description = "Python bindings, utility functions, and high-level API for IfcOpenShell"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9,<3.14"
|
||||
requires-python = ">=3.9,<3.15"
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)",
|
||||
|
||||
@@ -18,13 +18,14 @@
|
||||
|
||||
import http.client
|
||||
from pathlib import Path
|
||||
from collections.abc import Sequence
|
||||
from typing_extensions import assert_never
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import pytest
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.element
|
||||
import test.bootstrap
|
||||
try:
|
||||
from bs4 import BeautifulSoup
|
||||
except:
|
||||
pass
|
||||
|
||||
# Where it's also reflected:
|
||||
# - .github/workflows/ci-ifcopenshell-python.yml
|
||||
@@ -33,6 +34,10 @@ import test.bootstrap
|
||||
SUPPORTED_PY_VERSIONS = ("39", "310", "311", "312", "313", "314")
|
||||
SUPPORTED_PLATFORMS = ("win64", "linux64", "macos64", "macosm164")
|
||||
|
||||
WASM_SUPPORTED_PY_VERSIONS = ("313",)
|
||||
WASM_PLATFORM = "pyodide_2025_0_wasm32"
|
||||
WASM_TEMPLATE = "https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-{BINARY_VERSION}%2B{BUILD_COMMIT}-cp{PYNUMBER}-cp{PYNUMBER}-{PLATFORM}.whl"
|
||||
|
||||
|
||||
class TestPackageSupportedPlatforms:
|
||||
def test_run(self) -> None:
|
||||
@@ -52,16 +57,53 @@ class TestPackageSupportedPlatforms:
|
||||
return line.partition(":=")[2]
|
||||
|
||||
BINARY_VERSION = find_make_var("BINARY_VERSION")
|
||||
URL_TYPES = ("IOS_URL", "IFCCONVERT_URL")
|
||||
BUILD_COMMIT = find_make_var("BUILD_COMMIT")
|
||||
|
||||
missing_urls: set[str] = set()
|
||||
required_urls: list[str] = []
|
||||
|
||||
base_kwargs = {
|
||||
"BINARY_VERSION": BINARY_VERSION,
|
||||
"BUILD_COMMIT": BUILD_COMMIT,
|
||||
}
|
||||
|
||||
# Non-WASM binaries.
|
||||
URL_TYPES = ("IOS_URL", "IFCCONVERT_URL")
|
||||
for url_type in URL_TYPES:
|
||||
url_template = find_make_var(url_type)
|
||||
url_template = url_template.replace("$(", "{").replace(")", "}")
|
||||
for platform in SUPPORTED_PLATFORMS:
|
||||
for pyver in SUPPORTED_PY_VERSIONS:
|
||||
url = url_template.format(PYNUMBER=pyver, PLATFORM=platform, BINARY_VERSION=BINARY_VERSION)
|
||||
if url not in build_html:
|
||||
missing_urls.add(url)
|
||||
kwargs = base_kwargs | {"PLATFORM": platform}
|
||||
|
||||
if url_type == "IOS_URL":
|
||||
for pyver in SUPPORTED_PY_VERSIONS:
|
||||
url = url_template.format(**kwargs, PYNUMBER=pyver)
|
||||
required_urls.append(url)
|
||||
|
||||
elif url_type == "IFCCONVERT_URL":
|
||||
url = url_template.format(**kwargs)
|
||||
required_urls.append(url)
|
||||
|
||||
else:
|
||||
assert_never(url_type)
|
||||
|
||||
# WASM wheels.
|
||||
for pyver in WASM_SUPPORTED_PY_VERSIONS:
|
||||
url = WASM_TEMPLATE.format(
|
||||
PYNUMBER=pyver,
|
||||
PLATFORM=WASM_PLATFORM,
|
||||
BINARY_VERSION=BINARY_VERSION,
|
||||
BUILD_COMMIT=BUILD_COMMIT,
|
||||
)
|
||||
required_urls.append(url)
|
||||
|
||||
# Verify all required URLs are present in the build HTML.
|
||||
missing_urls: Sequence[str]
|
||||
if "BeautifulSoup" in globals():
|
||||
missing_urls = set(required_urls) - set(a["href"] for a in BeautifulSoup(build_html).find_all("a"))
|
||||
else:
|
||||
missing_urls = []
|
||||
for url in required_urls:
|
||||
if url not in build_html:
|
||||
missing_urls.append(url)
|
||||
|
||||
assert not missing_urls
|
||||
|
||||
@@ -73,6 +73,10 @@ class TestFormat:
|
||||
assert subject.format('imperial_length("3.123", 2)') == "3' - 1 1/2\""
|
||||
assert subject.format('imperial_length("123.123", 2, "inch", "foot")') == "10' - 3\""
|
||||
assert subject.format('imperial_length("123.123", 2, "inch", "inch")') == '123"'
|
||||
assert subject.format('imperial_length(3.0, 4, "foot", "foot", true)') == "3'"
|
||||
assert subject.format('imperial_length(3.0, 4, "foot", "foot", True)') == "3'"
|
||||
assert subject.format('imperial_length(3.0, 4, "foot", "foot", false)') == "3' - 0\""
|
||||
assert subject.format('imperial_length(3.0, 4, "foot", "foot", False)') == "3' - 0\""
|
||||
|
||||
|
||||
class TestGetElementValue(test.bootstrap.IFC4):
|
||||
@@ -302,7 +306,7 @@ class TestFilterElements(test.bootstrap.IFC4):
|
||||
assert subject.filter_elements(self.file, "IfcWall, parent=Project") == {element, element2, element3}
|
||||
assert subject.filter_elements(self.file, "IfcWall, parent=Space") == {element}
|
||||
assert subject.filter_elements(self.file, "IfcWall, parent=G") == {element, element2, element3}
|
||||
assert subject.filter_elements(self.file, "IfcWall, parent=Element2") == {element3}
|
||||
assert subject.filter_elements(self.file, "IfcWall, parent=Element2") == {element2, element3}
|
||||
assert subject.filter_elements(self.file, "IfcWall, parent=Space") == {element}
|
||||
|
||||
def test_selecting_multiple_filter_groups(self):
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
file(GLOB IFCPARSE_H_FILES_ALL *.h)
|
||||
file(GLOB IFCPARSE_CPP_FILES_ALL *.cpp)
|
||||
|
||||
foreach(file ${IFCPARSE_H_FILES_ALL})
|
||||
get_filename_component(filename "${file}" NAME)
|
||||
|
||||
if(NOT "${filename}" MATCHES "[0-9]")
|
||||
list(APPEND IFCPARSE_H_FILES "${file}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
foreach(file ${IFCPARSE_CPP_FILES_ALL})
|
||||
get_filename_component(filename "${file}" NAME)
|
||||
|
||||
if(NOT "${filename}" MATCHES "[0-9]")
|
||||
list(APPEND IFCPARSE_CPP_FILES "${file}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
message(STATUS "SCHEMA_VERSIONS from ifcparse: ${SCHEMA_VERSIONS}")
|
||||
foreach(schema ${SCHEMA_VERSIONS})
|
||||
list(APPEND IFCPARSE_H_FILES
|
||||
Ifc${schema}.h
|
||||
Ifc${schema}-definitions.h
|
||||
)
|
||||
list(APPEND IFCPARSE_CPP_FILES
|
||||
Ifc${schema}.cpp
|
||||
Ifc${schema}-schema.cpp
|
||||
)
|
||||
endforeach()
|
||||
|
||||
set(IFCPARSE_FILES ${IFCPARSE_CPP_FILES} ${IFCPARSE_H_FILES})
|
||||
|
||||
add_library(IfcParse ${IFCPARSE_FILES})
|
||||
set_target_properties(IfcParse PROPERTIES COMPILE_FLAGS -DIFC_PARSE_EXPORTS VERSION "${PROJECT_VERSION}" SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
|
||||
|
||||
if(IFCXML_SUPPORT)
|
||||
|
||||
if(LibXml2_DIR)
|
||||
target_compile_definitions(IfcParse PRIVATE ${LIBXML2_DEFINITIONS})
|
||||
endif()
|
||||
|
||||
add_definitions(-DWITH_IFCXML)
|
||||
target_link_libraries(IfcParse LibXml2::LibXml2)
|
||||
endif()
|
||||
|
||||
if(WASM_BUILD)
|
||||
target_link_libraries(IfcParse ${BCRYPT_LIBRARIES})
|
||||
else()
|
||||
target_link_libraries(IfcParse ${Boost_LIBRARIES} ${BCRYPT_LIBRARIES})
|
||||
endif()
|
||||
|
||||
# CMake installation targets
|
||||
install(FILES ${IFCPARSE_H_FILES}
|
||||
DESTINATION ${INCLUDEDIR}/ifcparse
|
||||
)
|
||||
|
||||
install(TARGETS IfcParse
|
||||
ARCHIVE DESTINATION ${LIBDIR}
|
||||
LIBRARY DESTINATION ${LIBDIR}
|
||||
RUNTIME DESTINATION ${BINDIR}
|
||||
)
|
||||
@@ -0,0 +1,59 @@
|
||||
# IfcPatch
|
||||
|
||||
Utility for applying modification recipes to IFC files. IfcPatch enables programmatic editing and correction of IFC models through a collection of patches that can fix common issues, migrate data, or transform models.
|
||||
|
||||
## Available Recipes
|
||||
|
||||
### Alignment and Linear Placement
|
||||
- **AddGeometricRepresentationToAlignment** - Add geometric representations to alignment elements
|
||||
- **AddLinearPlacementFallbackPosition** - Add fallback positions for linear placements
|
||||
- **AddZeroLengthSegmentToAlignment** - Add zero-length segments to alignments
|
||||
- **PatchStationReferentPosition** - Patch station referent positions
|
||||
|
||||
### Data Conversion and Migration
|
||||
- **AGS2IFC** - Convert AGS (geotechnical) data to IFC
|
||||
- **ConvertLengthUnit** - Convert between length units
|
||||
- **ConvertNestToAggregate** - Convert nesting relationships to aggregations
|
||||
- **ConvertPropertiesToQuantities** - Convert properties to quantities
|
||||
- **Migrate** - Migrate between IFC schema versions
|
||||
|
||||
### Element Extraction and Manipulation
|
||||
- **ExtractElements** - Extract specific elements into a new model
|
||||
- **ExtractPropertiesToSQLite** - Export properties to SQLite database
|
||||
- **Ifc2Sql** - Convert IFC to SQL database
|
||||
- **TessellateElements** - Tessellate elements into triangulated geometry
|
||||
|
||||
### Fixing Software-Specific Issues
|
||||
- **FixArchiCADToRevitDoorSwings** - Fix door swing orientation from ArchiCAD for Revit
|
||||
- **FixArchiCADToRevitSpaces** - Fix space data from ArchiCAD for Revit
|
||||
- **FixRevit2025TINs** - Fix TIN (Triangulated Irregular Network) issues from Revit 2025
|
||||
- **FixRevitClassificationCodeTypes** - Fix classification code data types from Revit
|
||||
- **FixRevitTINs** - Fix TIN issues from Revit
|
||||
|
||||
### Geometry and Representation
|
||||
- **AssignConstituentFractions** - Assign fractions to material constituents
|
||||
- **DowngradeIndexedPolyCurve** - Convert indexed poly curves to simpler representations
|
||||
- **RemoveSiteRepresentation** - Remove geometric representation from site elements
|
||||
|
||||
### Merging and Optimization
|
||||
- **MergeDuplicateTypes** - Merge duplicate element types
|
||||
- **MergeProjects** - Merge multiple IFC projects
|
||||
- **MergeStyles** - Merge duplicate styles
|
||||
- **Optimise** - Optimize file size and structure
|
||||
- **PurgeData** - Remove unused data from model
|
||||
|
||||
### Spatial and Coordinate Systems
|
||||
- **OffsetObjectPlacements** - Offset object placement coordinates
|
||||
- **OffsetStoreyElevations** - Offset storey elevations
|
||||
- **ResetAbsoluteCoordinates** - Reset absolute coordinates to origin
|
||||
- **ResetSpatialElementLocations** - Reset spatial element locations
|
||||
- **SetFalseOrigin** - Set a false origin for coordinates
|
||||
- **SetRefElevation** - Set reference elevation
|
||||
- **SetWorldCoordinateSystem** - Set world coordinate system
|
||||
- **SplitByBuildingStorey** - Split model by building storeys
|
||||
|
||||
### Utilities
|
||||
- **RecycleNonRootedElements** - Remove or recycle non-rooted elements
|
||||
- **RegenerateGlobalIds** - Regenerate GlobalId values for elements
|
||||
- **RemoveRevitUniformatClassification** - Remove Revit Uniformat classification
|
||||
- **UnsharePsets** - Unshare property sets between elements
|
||||
@@ -8,7 +8,7 @@ version = "0.0.0"
|
||||
authors = [
|
||||
{ name="Dion Moult", email="dion@thinkmoult.com" },
|
||||
]
|
||||
description = "IFC patching utility"
|
||||
description = "Utility for applying modification recipes to IFC files"
|
||||
readme = "README.md"
|
||||
keywords = ["IFC", "BIM"]
|
||||
classifiers = [
|
||||
|
||||
@@ -88,7 +88,8 @@ SET_PROPERTY(
|
||||
)
|
||||
|
||||
# On Windows there is '_d' prefix for debug builds - e.g. `_d.cp311-win_amd64.pyd`.
|
||||
if(WIN32)
|
||||
# In 4.2+ it's done automatically when discovering `Development.Module`.
|
||||
if(WIN32 AND CMAKE_VERSION VERSION_LESS "4.2")
|
||||
set(Python_DEBUG_POSTFIX "_d")
|
||||
endif()
|
||||
if (CMAKE_VERSION VERSION_GREATER_EQUAL "4.2")
|
||||
@@ -113,8 +114,8 @@ if (WASM_BUILD)
|
||||
# Possibly in the future we can clean up linked libs and try `=1`.
|
||||
# SIDE_MODULE=2 - add to .so only explicitly exported functions (`-s EXPORTED_FUNCTIONS).
|
||||
# Have to provide it here to override possible `=1` coming from pyodide.
|
||||
if(CMAKE_GENERATOR MATCHES "Makefile")
|
||||
# Accomodate bug in cmake makefile generator:
|
||||
if(CMAKE_GENERATOR MATCHES "Makefile" AND CMAKE_VERSION VERSION_LESS "4.2")
|
||||
# Accomodate bug in cmake makefile generator (fixed in 4.2+).
|
||||
# https://gitlab.kitware.com/cmake/cmake/-/issues/27326
|
||||
string(
|
||||
REPLACE "SIDE_MODULE=1" "SIDE_MODULE=2"
|
||||
|
||||
@@ -496,6 +496,9 @@ namespace {
|
||||
for (auto& rel : *rels) {
|
||||
if (rel->declaration().is("IfcRelDefinesByProperties")) {
|
||||
auto pset = ((IfcUtil::IfcBaseClass*) ((IfcUtil::IfcBaseEntity*) rel)->get("RelatingPropertyDefinition"))->as<IfcUtil::IfcBaseEntity>();
|
||||
if (!pset->declaration().is("IfcPropertySet")) {
|
||||
continue;
|
||||
}
|
||||
std::string pset_name;
|
||||
if (!pset->get("Name").isNull()) {
|
||||
pset_name = (std::string) pset->get("Name");
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
#include <TopoDS.hxx>
|
||||
#include <Bnd_Box.hxx>
|
||||
#include <gp_Pln.hxx>
|
||||
#include <TopoDS_Wire.hxx>
|
||||
#include <BRepBuilderAPI_MakeFace.hxx>
|
||||
#include <GProp_GProps.hxx>
|
||||
#include <BRepGProp.hxx>
|
||||
|
||||
#endif
|
||||
|
||||
@@ -396,56 +400,94 @@ void TtlWktSerializer::write(const IfcGeom::BRepElement* brep_obj) {
|
||||
|
||||
filename_.stream << ttl_object_id(brep_obj) << " geo:hasMetricLength " << height << " .\n\n";
|
||||
|
||||
gp_Pln pln(gp_Pnt(0, 0, zmin + section_height), gp::DZ());
|
||||
std::map<double, std::string> polygons_by_area;
|
||||
|
||||
Handle(TopTools_HSequenceOfShape) wires = new TopTools_HSequenceOfShape();
|
||||
double rectangle_area = (x2 - x1) * (y2 - y1);
|
||||
bool emitted_warning = false;
|
||||
|
||||
size_t N = 0;
|
||||
TopoDS_Iterator it(compound);
|
||||
// Iterate over components of compound to have better chance of matching section edges to closed wires
|
||||
for (; it.More(); it.Next()) {
|
||||
Handle(TopTools_HSequenceOfShape) edges = new TopTools_HSequenceOfShape();
|
||||
TopoDS_Shape result = BRepAlgoAPI_Section(it.Value(), pln);
|
||||
for (int iter = 0; iter < 10; ++iter) {
|
||||
|
||||
{
|
||||
TopExp_Explorer exp(result, TopAbs_EDGE);
|
||||
for (; exp.More(); exp.Next()) {
|
||||
edges->Append(exp.Current());
|
||||
}
|
||||
}
|
||||
gp_Pln pln(gp_Pnt(0, 0, zmin + section_height + iter * (height - 1.) / 10.), gp::DZ());
|
||||
|
||||
ShapeAnalysis_FreeBounds::ConnectEdgesToWires(edges, 1e-4, false, wires);
|
||||
for (int i = 1; i <= wires->Length(); ++i) {
|
||||
const TopoDS_Wire& wire = TopoDS::Wire(wires->Value(i));
|
||||
BRepTools_WireExplorer it(wire);
|
||||
std::vector<double> loop_coords;
|
||||
for (; it.More(); it.Next()) {
|
||||
const auto& v = it.CurrentVertex();
|
||||
auto pnt = BRep_Tool::Pnt(v);
|
||||
loop_coords.push_back(pnt.X());
|
||||
loop_coords.push_back(pnt.Y());
|
||||
loop_coords.push_back(pnt.Z());
|
||||
}
|
||||
std::vector<int> loop_idxs(loop_coords.size() / 3);
|
||||
for (int i = 0; i < loop_idxs.size(); ++i) {
|
||||
loop_idxs[i] = i;
|
||||
}
|
||||
Handle(TopTools_HSequenceOfShape) wires = new TopTools_HSequenceOfShape();
|
||||
|
||||
std::string postfix = "_section_geometry_" + std::to_string(N++);
|
||||
size_t N = 0;
|
||||
TopoDS_Iterator it(compound);
|
||||
// Iterate over components of compound to have better chance of matching section edges to closed wires
|
||||
for (; it.More(); it.Next()) {
|
||||
Handle(TopTools_HSequenceOfShape) edges = new TopTools_HSequenceOfShape();
|
||||
TopoDS_Shape result = BRepAlgoAPI_Section(it.Value(), pln);
|
||||
|
||||
filename_.stream << ttl_object_id(brep_obj) << " geo:hasGeometry " << ttl_object_id(brep_obj, postfix.c_str()) << " .\n\n";
|
||||
filename_.stream << ttl_object_id(brep_obj, postfix.c_str()) << " a geo:Geometry ;\n";
|
||||
filename_.stream << " geo:asWKT " << escape_for_turtle(
|
||||
IfcUtil::convert_utf8(
|
||||
capture_output(
|
||||
emit_line_component,
|
||||
loop_coords,
|
||||
loop_idxs,
|
||||
true,
|
||||
POLYGON))
|
||||
) << "^^geo:wktLiteral .\n\n";
|
||||
}
|
||||
}
|
||||
{
|
||||
TopExp_Explorer exp(result, TopAbs_EDGE);
|
||||
for (; exp.More(); exp.Next()) {
|
||||
edges->Append(exp.Current());
|
||||
}
|
||||
}
|
||||
|
||||
ShapeAnalysis_FreeBounds::ConnectEdgesToWires(edges, 1e-4, false, wires);
|
||||
for (int i = 1; i <= wires->Length(); ++i) {
|
||||
const TopoDS_Wire& wire = TopoDS::Wire(wires->Value(i));
|
||||
if (!wire.Closed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
BRepBuilderAPI_MakeFace mf(wire);
|
||||
if (!mf.IsDone()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto face = mf.Face();
|
||||
// calculate face area
|
||||
GProp_GProps props;
|
||||
BRepGProp::SurfaceProperties(face, props);
|
||||
auto area = props.Mass();
|
||||
|
||||
BRepTools_WireExplorer it(wire);
|
||||
std::vector<double> loop_coords;
|
||||
for (; it.More(); it.Next()) {
|
||||
const auto& v = it.CurrentVertex();
|
||||
auto pnt = BRep_Tool::Pnt(v);
|
||||
loop_coords.push_back(pnt.X());
|
||||
loop_coords.push_back(pnt.Y());
|
||||
loop_coords.push_back(pnt.Z());
|
||||
}
|
||||
std::vector<int> loop_idxs(loop_coords.size() / 3);
|
||||
for (int i = 0; i < loop_idxs.size(); ++i) {
|
||||
loop_idxs[i] = i;
|
||||
}
|
||||
|
||||
std::string postfix = "_section_geometry_" + std::to_string(N++);
|
||||
|
||||
std::ostringstream oss;
|
||||
|
||||
oss << ttl_object_id(brep_obj) << " geo:hasGeometry " << ttl_object_id(brep_obj, postfix.c_str()) << " .\n\n";
|
||||
oss << ttl_object_id(brep_obj, postfix.c_str()) << " a geo:Geometry ;\n";
|
||||
oss << " geo:asWKT " << escape_for_turtle(IfcUtil::convert_utf8(capture_output(emit_line_component, loop_coords, loop_idxs, true, POLYGON))) << "^^geo:wktLiteral .\n\n";
|
||||
|
||||
polygons_by_area[area] = oss.str();
|
||||
}
|
||||
}
|
||||
|
||||
if (polygons_by_area.size() > 0) {
|
||||
if ((polygons_by_area.rbegin()->first > (0.6 * rectangle_area)) || (height < (1. + 1.e-5))) {
|
||||
// Found sufficiently large polygon
|
||||
if (emitted_warning) {
|
||||
Logger::Warning("Found larger polygon area (" + std::to_string(polygons_by_area.rbegin()->first) + ").");
|
||||
}
|
||||
break;
|
||||
} else if (!emitted_warning) {
|
||||
Logger::Warning("Section polygon area is small compared to bounding box area (" + std::to_string(polygons_by_area.rbegin()->first) + " < " + std::to_string(0.6 * rectangle_area) + "). Trying again with different section height.");
|
||||
emitted_warning = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (polygons_by_area.size() > 0) {
|
||||
// Emit polygon with largest area
|
||||
auto it = polygons_by_area.rbegin();
|
||||
filename_.stream << it->second;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
+1
-1
Submodule src/svgfill updated: 113fe929c8...ba09c29b81
@@ -48,7 +48,6 @@ def build() -> None:
|
||||
"-DGLTF_SUPPORT=ON",
|
||||
"-DADD_COMMIT_SHA=ON",
|
||||
"-DVERSION_OVERRIDE=ON",
|
||||
"-DSCHEMA_VERSIONS=4",
|
||||
]
|
||||
)
|
||||
run([str(REPO_WIN / "install-ifcopenshell.bat"), "vs2022-x64", "Release"])
|
||||
|
||||
Reference in New Issue
Block a user