diff --git a/.gersemirc b/.gersemirc index 4d74cb215a..44ff8cc11c 100644 --- a/.gersemirc +++ b/.gersemirc @@ -1,7 +1,8 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/BlankSpruce/gersemi/0.24.0/gersemi/configuration.schema.json -# Needed for gersemi to detect custom functions and macros. -definitions: ["./cmake"] +# Gersemi doesn't support autodetection of macros/functions from other files or from the current one +# and requires to explicitly list directories/cmake files that define them. +definitions: ["./cmake", "./src"] disable_formatting: false extensions: [] indent: 4 diff --git a/.github/workflows/build_osx.yml b/.github/workflows/build_osx.yml index 056cf13763..0f6bb702e9 100644 --- a/.github/workflows/build_osx.yml +++ b/.github/workflows/build_osx.yml @@ -40,6 +40,8 @@ jobs: # preinstalled: xz, cmake brew install git bison autoconf automake libffi findutils echo "$(brew --prefix findutils)/libexec/gnubin" >> $GITHUB_PATH + # Mac is using bison 2.5 by default, but we need 3.5+ for swig. + echo "$(brew --prefix bison)/bin" >> $GITHUB_PATH - name: Install aws cli run: | @@ -47,11 +49,11 @@ jobs: - name: Unpack Dependencies run: | - install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true) - [ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true + cd build + python ../nix/cache_dependencies.py unpack - name: ccache - uses: hendrikmuhs/ccache-action@v1.2 + uses: hendrikmuhs/ccache-action@v1.2.20 with: key: mac-${{ matrix.arch }} @@ -81,7 +83,7 @@ jobs: - name: Upload Build Logs if: always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: build-logs-osx-${{ matrix.arch }} path: | @@ -93,9 +95,7 @@ jobs: - name: Pack Dependencies run: | cd build - for install_dir in $(find $(find . -maxdepth 4 -name install) -mindepth 1 -maxdepth 1 -type d); do - test -f $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz || tar -czf $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz -C $(dirname "$install_dir") $(basename "$install_dir"); - done + python ../nix/cache_dependencies.py pack - name: Commit and Push Changes to Build Repository run: | diff --git a/.github/workflows/build_pyodide.yml b/.github/workflows/build_pyodide.yml index 744bd9e772..7da3bb408c 100644 --- a/.github/workflows/build_pyodide.yml +++ b/.github/workflows/build_pyodide.yml @@ -26,10 +26,10 @@ jobs: - name: Unpack Dependencies run: | cd ifcopenshell_build - python ../IfcOpenShell/pyodide/cache_dependencies.py unpack + python ../IfcOpenShell/nix/cache_dependencies.py unpack - name: ccache - uses: hendrikmuhs/ccache-action@v1.2 + uses: hendrikmuhs/ccache-action@v1.2.20 with: key: ubuntu-22.04-${{ runner.arch }} @@ -42,7 +42,7 @@ jobs: - name: Upload Build Logs if: always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: build-logs-pyodide path: | @@ -65,7 +65,7 @@ jobs: - name: Pack Dependencies run: | cd ifcopenshell_build - python ../IfcOpenShell/pyodide/cache_dependencies.py pack + python ../IfcOpenShell/nix/cache_dependencies.py pack - name: Commit and Push Changes to Build Repository run: | diff --git a/.github/workflows/build_rocky.yml b/.github/workflows/build_rocky.yml index 662aa04f26..710b918564 100644 --- a/.github/workflows/build_rocky.yml +++ b/.github/workflows/build_rocky.yml @@ -6,13 +6,13 @@ on: jobs: build_ifcopenshell: runs-on: ubuntu-22.04 - container: rockylinux:8 + container: rockylinux:9 steps: - name: Install Dependencies run: | - yum update -y - yum install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 \ + dnf update -y + dnf install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 python3-pip \ bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \ sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \ readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \ @@ -38,20 +38,19 @@ jobs: with: repository: IfcOpenShell/build-outputs path: ./build - ref: rockylinux8-x64 + ref: rockylinux9-x64 lfs: true token: ${{ secrets.BUILD_REPO_TOKEN }} - name: Unpack Dependencies run: | - install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true) - [ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true + cd build + python3 ../nix/cache_dependencies.py unpack - name: ccache - # TODO: Use tag after 1.2.20 releases. - uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639 + uses: hendrikmuhs/ccache-action@v1.2.20 with: - key: ubuntu-22.04-${{ runner.arch }}-rockylinux8 + key: ubuntu-22.04-${{ runner.arch }}-rockylinux9 - name: Run Build Script shell: bash @@ -61,7 +60,7 @@ jobs: - name: Upload Build Logs if: always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: build-logs-rocky path: | @@ -72,9 +71,7 @@ jobs: - name: Pack Dependencies run: | cd build - for install_dir in $(find $(find . -maxdepth 4 -name install) -mindepth 1 -maxdepth 1 -type d); do - test -f $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz || tar -czf $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz -C $(dirname "$install_dir") $(basename "$install_dir"); - done + python3 ../nix/cache_dependencies.py pack - name: Commit and Push Changes to Build Repository run: | diff --git a/.github/workflows/build_rocky_arm.yml b/.github/workflows/build_rocky_arm.yml index 445f49658a..b54e62ccef 100644 --- a/.github/workflows/build_rocky_arm.yml +++ b/.github/workflows/build_rocky_arm.yml @@ -6,13 +6,13 @@ on: jobs: build_ifcopenshell: runs-on: ubuntu-22.04-arm - container: arm64v8/rockylinux:8 + container: arm64v8/rockylinux:9 steps: - name: Install Dependencies run: | - yum update -y - yum install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 \ + dnf update -y + dnf install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 python3-pip \ bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \ sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \ readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \ @@ -38,20 +38,19 @@ jobs: with: repository: IfcOpenShell/build-outputs path: ./build - ref: rockylinux8-arm64 + ref: rockylinux9-arm64 lfs: true token: ${{ secrets.BUILD_REPO_TOKEN }} - name: Unpack Dependencies run: | - install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true) - [ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true + cd build + python3 ../nix/cache_dependencies.py unpack - name: ccache - # TODO: Use tag after 1.2.20 releases. - uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639 + uses: hendrikmuhs/ccache-action@v1.2.20 with: - key: ubuntu-22.04-${{ runner.arch }}-rockylinux8 + key: ubuntu-22.04-${{ runner.arch }}-rockylinux9 - name: Run Build Script shell: bash @@ -61,7 +60,7 @@ jobs: - name: Upload Build Logs if: always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: build-logs-rocky-arm64 path: | @@ -72,9 +71,7 @@ jobs: - name: Pack Dependencies run: | cd build - for install_dir in $(find $(find . -maxdepth 4 -name install) -mindepth 1 -maxdepth 1 -type d); do - test -f $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz || tar -czf $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz -C $(dirname "$install_dir") $(basename "$install_dir"); - done + python3 ../nix/cache_dependencies.py pack - name: Commit and Push Changes to Build Repository run: | diff --git a/.github/workflows/build_win.yml b/.github/workflows/build_win.yml index ab9d7b6f80..783084c1ae 100644 --- a/.github/workflows/build_win.yml +++ b/.github/workflows/build_win.yml @@ -37,8 +37,7 @@ jobs: } - name: ccache - # TODO: Use tag after 1.2.20 releases. - uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639 + uses: hendrikmuhs/ccache-action@v1.2.20 with: key: win-${{ matrix.arch }} # Windows ccache needs ~1GB diff --git a/.github/workflows/ci-bonsai-daily.yml b/.github/workflows/ci-bonsai-daily.yml index 3b52198f3a..ddfa64a47a 100644 --- a/.github/workflows/ci-bonsai-daily.yml +++ b/.github/workflows/ci-bonsai-daily.yml @@ -24,9 +24,15 @@ jobs: runs-on: ubuntu-latest if: | github.repository == 'IfcOpenShell/IfcOpenShell' + outputs: + timestamp: ${{ steps.timestamp.outputs.timestamp }} steps: - - name: Set env - run: echo ok go + - name: Get current timestamp + id: timestamp + # Include hours and minutes to release tag + # to avoid possibility of unstable repo's index.json + # pointing to the new file when index.json itself wasn't yet updated. + run: echo "timestamp=$(date +'%y%m%d%H%M')" >> $GITHUB_OUTPUT build: needs: activate @@ -67,12 +73,6 @@ jobs: - name: Get current version id: version run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT - - name: Get current date - id: date - # Include hours and minutes to release tag - # to avoid possibility of unstable repo's index.json - # pointing to the new file when index.json itself wasn't yet updated. - run: echo "date=$(date +'%y%m%d%H%M')" >> $GITHUB_OUTPUT - name: Compile run: | cd src/bonsai && make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }} @@ -88,8 +88,8 @@ jobs: repo_token: ${{ secrets.GITHUB_TOKEN }} file: ${{ steps.find_zip.outputs.filepath }} asset_name: ${{ steps.find_zip.outputs.filename }} - release_name: "bonsai-${{steps.version.outputs.version}}-alpha${{steps.date.outputs.date}} (unstable)" - tag: "bonsai-${{steps.version.outputs.version}}-alpha${{steps.date.outputs.date}}" + release_name: "bonsai-${{steps.version.outputs.version}}-alpha${{ needs.activate.outputs.timestamp }} (unstable)" + tag: "bonsai-${{steps.version.outputs.version}}-alpha${{ needs.activate.outputs.timestamp }}" overwrite: true body: "See README in https://github.com/IfcOpenShell/bonsai_unstable_repo/ on how to setup autoupdates for daily Bonsai builds." diff --git a/.github/workflows/ci-ifcopenshell-docker.yml b/.github/workflows/ci-ifcopenshell-docker.yml index d5691ecfce..e10be5a18b 100644 --- a/.github/workflows/ci-ifcopenshell-docker.yml +++ b/.github/workflows/ci-ifcopenshell-docker.yml @@ -35,7 +35,7 @@ jobs: - name: ccache - uses: hendrikmuhs/ccache-action@v1.2 + uses: hendrikmuhs/ccache-action@v1.2.20 - name: Build ifcopenshell @@ -73,7 +73,7 @@ jobs: make package working-directory: build - name: Upload - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: # Artifact name name: ifcos-artifacts @@ -91,7 +91,7 @@ jobs: lfs: true - name: Download - uses: actions/download-artifact@v7.0.0 + uses: actions/download-artifact@v8.0.0 with: # Artifact name name: ifcos-artifacts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65031e83d9..896a179758 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,10 +79,7 @@ jobs: libhdf5-dev libcgal-dev libeigen3-dev - name: ccache - # TODO: temporarily pointing to 1.2.19 to get notified by dependabot when 1.2.20 is released - # to update hardcoded references to commits in some other workflows. - # Then we can switch back to 1.2 in all actions. - uses: hendrikmuhs/ccache-action@v1.2.19 + uses: hendrikmuhs/ccache-action@v1.2.20 with: key: ubuntu-22.04-${{ runner.arch }} diff --git a/.gitignore b/.gitignore index bee04658cd..8a7482ab8e 100644 --- a/.gitignore +++ b/.gitignore @@ -82,6 +82,8 @@ src/bonsai/bonsai/translations.py # bonsai test temp files src/bonsai/test/files/temp +src/bonsai/test/files/basic.ifc.cache.blend +src/bonsai/test/files/basic.ifc.cache.sqlite # bonsai data src/bonsai/bonsai/bim/data/build/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..fd0e305245 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,153 @@ + + +# AGENTS.md + +Guidelines for AI coding agents contributing to IfcOpenShell. This file is +intended to be read by all AI agents regardless of platform (Claude Code, +Copilot, Cursor, etc.) in addition to any tool-specific configuration files. + +Human contributors using AI tools should also read this document carefully, +as they are responsible for ensuring their contributions comply with these +guidelines. + +## Project Overview + +IfcOpenShell is an open source library for working with Industry Foundation +Classes (IFC). It provides C++ and Python APIs, geometry processing, and an +ecosystem of tools including IfcConvert and the Bonsai Blender add-on. + +## Licensing + +All contributions must be compatible with the project's licensing: + +- **Library code** (everything except Bonsai): **LGPL-3.0-or-later** +- **Bonsai** (`src/bonsai/`): **GPL-3.0-or-later** + +There is no Contributor License Agreement (CLA). By submitting a pull request, +you agree that your contribution is licensed under the applicable license above. + +## Indicating AI-Generated Code + +Contributors must clearly indicate when code has been generated or +substantially written by an AI tool. + +### Commits + +Commits that modify existing code must include a note in the **body** of the +commit message (not the subject line) indicating that the change was +AI-generated. For example: + +``` +Fix off-by-one error in element iteration + +The loop termination condition was incorrect when processing +IfcRelAggregates relationships. + +Generated with the assistance of an AI coding tool. +``` + +### New Files + +New files that are AI-generated must include a comment near the top of the +file indicating this. Use the appropriate comment syntax for the language: + +```python +# This file was generated with the assistance of an AI coding tool. +``` + +```cpp +// This file was generated with the assistance of an AI coding tool. +``` + +### Pull Requests + +Pull requests containing AI-generated code must indicate in the PR description +which parts of the contribution are AI-generated. If the entire PR is +AI-generated, state that clearly. If only specific commits or files are +AI-generated, identify them. + +## Pull Request Guidelines + +### Scope and Size + +- Each pull request should address a **single issue or feature**. +- Do not mix unrelated changes (e.g., bug fixes with refactoring or style + changes) in the same PR. +- Large pull requests should be broken down into **multiple small, standalone + commits** that are each easy to review independently. Rewrite commit history + for this purpose if necessary. +- PRs that are minimal, focused solutions to a specific problem are much more + likely to be accepted. + +### What to Avoid + +- **Over-engineering**: Do not add features, abstractions, or configurability + beyond what is needed to solve the immediate problem. +- **Scope creep**: Do not make changes to files or code that are not directly + related to the task at hand. +- **Unnecessary additions**: Do not add docstrings, comments, type annotations, + or error handling to code you did not otherwise need to change. +- **Cosmetic changes**: Do not reformat, rename, or reorganize code that is + unrelated to your change. + +## Commit Messages + +- The **subject line** must be **50 characters or less**. +- Use the **imperative mood** (e.g., "Fix crash in geometry kernel", not + "Fixed crash" or "Fixes crash"). +- A commit message can be a single line if the purpose is obvious from the + subject alone. +- Otherwise, add a blank line after the subject followed by a short explanation + of a few lines in the body. + +## Code Style + +### Python + +- **Line length**: 120 characters +- **Formatter**: black +- **Linter**: ruff +- Configuration is in `pyproject.toml` + +### C++ + +- **Standard**: C++17 minimum +- **Formatter**: clang-format (configuration in `.clang-format`) +- **Linter**: clang-tidy (configuration in `.clang-tidy`) + +Run linters and formatters **before submitting** your pull request. Do not rely +on CI to catch formatting issues. + +## Testing + +- Pull requests with test coverage are **much more likely to be merged**. +- If tests are appropriate and feasible for your change, they should be + included. +- Tests are not required for every change (e.g., documentation-only changes), + but the expectation is that testable code changes come with tests. +- Python tests use **pytest** and are located in `test/` or `tests/` directories + within each package under `src/`. +- Run the existing test suite for the package you modified before submitting. + +## Architecture Quick Reference + +### Directory Structure + +- `src/ifcparse/` — C++ IFC file parsing +- `src/ifcgeom/` — C++ geometry processing (OpenCASCADE and CGAL kernels) +- `src/serializers/` — Output format serializers (glTF, Collada, SVG, etc.) +- `src/ifcwrap/` — SWIG Python bindings +- `src/ifcconvert/` — CLI conversion tool +- `src/ifcopenshell-python/` — Python API (`ifcopenshell` package) +- `src/bonsai/` — Blender add-on (GPL-3.0-or-later) +- `src/ifctester/` — IDS model auditing +- `src/ifcpatch/` — IFC file manipulation scripts +- `src/ifcdiff/` — IFC model comparison +- `src/ifcclash/` — Clash detection +- `src/ifccsv/` — Schedule import/export + +### IFC Schema Versions + +The library supports IFC2x3 TC1, IFC4 Add2 TC1, IFC4x1, IFC4x2, and +IFC4x3 Add2. Schema-specific code is compiled conditionally. Be aware of +which schema versions your change affects. diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 9e88026ac7..bf48427774 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -258,10 +258,10 @@ if(WITH_ROCKSDB) set(ROCKSDB_LIBRARIES "IFCOPENSHELL_RocksDB") target_compile_definitions(IFCOPENSHELL_RocksDB INTERFACE IFOPSH_WITH_ROCKSDB) set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_ROCKSDB) - target_link_libraries( - IFCOPENSHELL_RocksDB - INTERFACE $,RocksDB::rocksdb-shared,RocksDB::rocksdb> - ) + # Shared binaries for `rocksdb` only support limited API (only `c.h`), but we use `db.h` API. + # So rocksdb supported only as a static library. + # See https://github.com/facebook/rocksdb/issues/981. + target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE RocksDB::rocksdb) if(WITH_ZSTD) # @todo do we actually need the zstd include dir or rather just pass @@ -368,12 +368,20 @@ if(ENABLE_BUILD_OPTIMIZATIONS) # Linker # /OPT:REF enables also /OPT:ICF and disables INCREMENTAL - set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /LTCG /OPT:REF") - + set(LINKER_FLAGS_RELEASE "/LTCG /OPT:REF") # /OPT:NOICF is recommended when /DEBUG is used (http://msdn.microsoft.com/en-us/library/xe4t6fc1.aspx) - set(CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /DEBUG /OPT:NOICF") - set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /LTCG /OPT:REF") - set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /DEBUG /OPT:NOICF") + set(LINKER_FLAGS_RELWITHDEBINFO "/DEBUG /OPT:NOICF") + + set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELEASE}") + set(CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO + "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELWITHDEBINFO}" + ) + set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELEASE}") + set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_EXE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELWITHDEBINFO}") + set(CMAKE_MODULE_LINKER_FLAGS_RELEASE "${CMAKE_MODULE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELEASE}") + set(CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO + "${CMAKE_MODULE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELWITHDEBINFO}" + ) else() # GCC-like: Release should use O3 but RelWithDebInfo 02 so enforce 03. Anything other useful that could be added here? set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3") diff --git a/cmake/FindOpenCASCADE.cmake b/cmake/FindOpenCASCADE.cmake index f78ee33b99..aac5f0521e 100644 --- a/cmake/FindOpenCASCADE.cmake +++ b/cmake/FindOpenCASCADE.cmake @@ -43,6 +43,17 @@ if(NOT OCC_INCLUDE_DIR AND NOT OCC_LIBRARY_DIR) set_target_properties(TKernel PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${OpenCASCADE_INCLUDE_DIR}") endif() + if( + OpenCASCADE_VERSION VERSION_LESS "7.9.0" + AND CMAKE_VERSION GREATER_EQUAL "3.24" + AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU" + ) + # Before 7.9.0 targets in OCCT cmake configs are not linked to each other + # leading to missing symbols on Unix. Link them as a single group as a workaround. + # Only needed for gcc, because other compilers (e.g. Apple Clang, MSVC) do rescan automatically. + set(OpenCASCADE_LIBRARIES "$") + endif() + if(OpenCASCADE_VERSION VERSION_LESS "7.9.0" AND WIN32) # Bug in OCCT cmake configs < 7.9.0 - missing linked library. list(APPEND OpenCASCADE_LIBRARIES WSOCK32.lib) diff --git a/nix/build-all.py b/nix/build-all.py index 2a4318dc35..46b7b5c30c 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -75,27 +75,30 @@ Used environment variables: # # # for python37 to install correctly additionally: # # * libffi(-dev[el]) # -# for Python build we also needs ssl # +# for Python build we also needs ssl and zlib # # (since we do `pip install numpy` at the end) # # * libssl-dev # # # # on debian 7.8 these can be obtained with: # # $ apt-get install git gcc g++ autoconf bison bzip2 cmake # # mesa-common-dev libffi-dev libfontconfig1-dev # -# libssl-dev xz # +# libssl-dev xz zlib1g-dev # # # # on ubuntu 14.04: # # $ apt-get install git gcc g++ autoconf bison make cmake # # mesa-common-dev libffi-dev libfontconfig1-dev # -# libssl-dev xz-utils # +# libssl-dev xz-utils zlib1g-dev # # # # on OS X El Capitan with homebrew: # # $ brew install git bison autoconf automake libffi cmake # +# $ # `bison` shipped with Mac is too old for swig build, # +# $ # so we use `brew`. # +# $ export PATH=$(brew --prefix bison)/bin:$PATH # # # # on RHEL-related distros: # -# $ yum install git gcc gcc-c++ autoconf bison make cmake # +# $ dnf install git gcc gcc-c++ autoconf bison make cmake # # mesa-libGL-devel libffi-devel fontconfig-devel bzip2 # -# automake patch byacc xz # +# automake patch byacc xz zlib-devel openssl-devel # """ @@ -147,9 +150,8 @@ OCCT_VERSION = "7.8.1" BOOST_VERSION = "1.86.0" EIGEN_VERSION = "3.4.0" PCRE_VERSION = "8.41" -PCRE2_VERSION = "10.32" LIBXML2_VERSION = "2.13.8" -SWIG_VERSION = "4.1.0" +SWIG_VERSION = "4.2.1" OPENCOLLADA_VERSION = "v1.6.68" HDF5_VERSION = "1.13.1" @@ -246,15 +248,8 @@ if WASM: # https://github.com/pyodide/pyodide-build/pull/249 WASM_CMAKE_IS_USING_INIT_VARS = get_pyodide_build_version() >= (99, 0, 0) - # pyodide provide empty `CXXFLAGS`, leading to issues using C++ files compiled with `-fexceptions` - # which is used by OCCT. - # https://github.com/pyodide/pyodide-build/issues/251 - side_module_cxx_flags = os.environ.get("SIDE_MODULE_CXXFLAGS", "") - if side_module_cxx_flags.strip(): - print("SIDE_MODULE_CXXFLAGS are already passed from pyodide build ('{side_module_cxx_flags}').") - print("Maybe it's time to stop overriding them in the script?") - - os.environ["SIDE_MODULE_CXXFLAGS"] = os.environ["SIDE_MODULE_CFLAGS"] + # 0.31 is required for SIDE_MODULE_CXXFLAGS to be provided. + assert get_pyodide_build_version() >= (0, 31) # Set defaults for missing empty environment variables @@ -294,6 +289,7 @@ DEPS_DIR = os.getenv("DEPS_DIR", DEFAULT_DEPS_DIR) if not os.path.exists(DEPS_DIR): os.makedirs(DEPS_DIR) +INSTALL_DIR = Path(DEPS_DIR) / "install" BUILD_CFG = os.getenv("BUILD_CFG", "RelWithDebInfo") @@ -319,24 +315,18 @@ cecho(f"* Build Directory = {BUILD_DIR}", MAGENTA) cecho(f"* Dependency Directory = {DEPS_DIR}", MAGENTA) cecho(f" - The directory where {PROJECT_NAME} dependencies are installed.") cecho(f"* Build Config Type = {BUILD_CFG}", MAGENTA) -cecho( - """ - The used build configuration type for the dependencies. - Defaults to RelWithDebInfo if not specified.""" -) +cecho(""" - The used build configuration type for the dependencies. + Defaults to RelWithDebInfo if not specified.""") if BUILD_CFG == "MinSizeRel": cecho(" WARNING: MinSizeRel build can suffer from a significant performance loss.", RED) cecho(f"* IFCOS_NUM_BUILD_PROCS = {IFCOS_NUM_BUILD_PROCS}", MAGENTA) -cecho( - """ - How many compiler processes may be run in parallel. -""" -) +cecho(""" - How many compiler processes may be run in parallel. +""") cecho(f" * IFCOS_SCHEMAS = '{os.environ.get('IFCOS_SCHEMAS')}'", MAGENTA) -cecho( - """ - IFC Schemas to compile. If not provided, fallback to default provided in cmake. -""" -) +cecho(""" - IFC Schemas to compile. If not provided, fallback to default provided in cmake. +""") dependency_tree: "dict[str, tuple[str, ...]]" = { "IfcParse": ("boost", "libxml2", "hdf5", "rocksdb"), @@ -345,13 +335,12 @@ dependency_tree: "dict[str, tuple[str, ...]]" = { "OpenCOLLADA": ("libxml2", "pcre"), "IfcGeomServer": ("IfcGeom",), "IfcOpenShell-Python": ("python", "swig", "IfcGeom"), - "swig": ("pcre2",), + "swig": (), "boost": (), "libxml2": (), "python": (), "occ": (), "pcre": (), - "pcre2": (), "json": (), "hdf5": (), "cgal": (), @@ -418,7 +407,6 @@ if WASM: "opencollada", "swig", "pcre", - "pcre2", "IfcGeom", "IfcConvert", "IfcGeomServer", @@ -433,13 +421,16 @@ print("Building:", *sorted(targets, key=lambda t: len(list(gather_dependencies(t # Check that required tools are in PATH yacc = "yacc" # Used during swig building process, installed with `bison` on Debian / `byacc` on Red Hat. +bison = "bison" + missing_commands: "list[str]" = [] -required_commands = [git, bunzip2, tar, cc, cplusplus, autoconf, automake, make, "patch", "cmake", yacc, xz] +required_commands = [git, bunzip2, tar, cc, cplusplus, autoconf, automake, make, "patch", "cmake", yacc, xz, bison] if "wasm" in flags: # Skip swig build for WASM. required_commands.append("swig") required_commands.append("pyodide") required_commands.remove(yacc) + required_commands.remove(bison) for cmd in required_commands: if shutil.which(cmd) is None: @@ -498,7 +489,7 @@ def run(cmds: "Sequence[str]", cwd: "Union[str, None]" = None, can_fail: bool = collector.append(line) pipe.close() - logger.debug(f"running command {' '.join(cmds)} in directory {cwd}") + logger.debug(f"running command `{' '.join(cmds)}` in directory '{cwd}'") stdout: list[str] = [] stderr: list[str] = [] @@ -544,14 +535,14 @@ BOOST_LOCATION = f"https://github.com/boostorg/boost/releases/download/boost-{BO # Helper functions -def run_autoconf(arg1: str, configure_args: "list[str]", cwd: str) -> None: +def run_autoconf(dependency_name: str, configure_args: "list[str]", cwd: str) -> None: configure_path = os.path.realpath(os.path.join(cwd, "..", "configure")) if not os.path.exists(configure_path): run( [bash, "./autogen.sh"], cwd=os.path.realpath(os.path.join(cwd, "..")) ) # only run autogen.sh in the directory it is located and use cwd to achieve that in order to not mess up things # Using `sh` over `bash` fixes issues with building swig - prefix = os.path.realpath(f"{DEPS_DIR}/install/{arg1}") + prefix = os.path.realpath(f"{DEPS_DIR}/install/{dependency_name}") wasm = [] if "wasm" in flags: @@ -930,20 +921,15 @@ if "pcre" in targets: restore_env("CC", OLD_CC) restore_env("CXX", OLD_CXX) -if "pcre2" in targets: - build_dependency( - name=f"pcre2-{PCRE2_VERSION}", - mode="autoconf", - build_tool_args=[DISABLE_FLAG], - download_url=f"https://downloads.sourceforge.net/project/pcre/pcre2/{PCRE2_VERSION}/", - download_name=f"pcre2-{PCRE2_VERSION}.tar.bz2", - ) - if "swig" in targets: + dependency_name = f"swig-{SWIG_VERSION}" build_dependency( - name=f"swig-{SWIG_VERSION}", - mode="autoconf", - build_tool_args=["--disable-ccache", f"--with-pcre2-prefix={DEPS_DIR}/install/pcre2-{PCRE2_VERSION}"], + name=dependency_name, + mode="cmake", + build_tool_args=[ + "-DWITH_PCRE=OFF", + f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/{dependency_name}", + ], download_url="https://github.com/swig/swig.git", download_name="swig", download_tool=download_tool_git, @@ -951,21 +937,18 @@ if "swig" in targets: ) if USE_OCCT and "occ" in targets: - patches = [] + occt_args: "list[str]" = [] + patches: "list[str]" = [] if OCCT_VERSION < "7.4": patches.append("./patches/occt/enable-exception-handling.patch") - if OCCT_VERSION == "7.7.1": + # Skip ExpToCasExe as we don't need it and it requires additional dependencies. + # Before 7.7.2 ExpToCasExe is part of DataExchange, DETools doesn't exist yet. + # Since we do need DataExchange (used for IgesSerializer), we use a patch to skip only ExpToCasExe. + if "7.7.2" > OCCT_VERSION >= "7.7": patches.append("./patches/occt/no_ExpToCasExe.patch") - - if OCCT_VERSION == "7.7.2": - patches.append("./patches/occt/no_ExpToCasExe_7_7_2.patch") - - if OCCT_VERSION == "7.8.1": - patches.append("./patches/occt/no_ExpToCasExe_7_8_1.patch") - - if OCCT_VERSION == "7.9.1": - patches.append("./patches/occt/no_ExpToCasExe_7_9_1.patch") + elif OCCT_VERSION >= "7.7.2": + occt_args.append("-DBUILD_MODULE_DETools=OFF") if "wasm" in flags: patches.append("./patches/occt/no_em_js.patch") @@ -986,6 +969,7 @@ if USE_OCCT and "occ" in targets: f"-DUSE_GLES2=OFF", f"-DCMAKE_POLICY_VERSION_MINIMUM=3.5", *MAC_CROSS_COMPILE_INTEL_ARGS, + *occt_args, ], download_url="https://github.com/Open-Cascade-SAS/OCCT", download_name="occt", @@ -1101,23 +1085,19 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag PYTHON_CONFIGURE_ARGS.extend(["--with-universal-archs=intel-64", "--enable-universalsdk"]) for PYTHON_VERSION in PYTHON_VERSIONS: - try: - build_dependency( - f"python-{PYTHON_VERSION}", - "autoconf", - PYTHON_CONFIGURE_ARGS, - f"http://www.python.org/ftp/python/{PYTHON_VERSION}/", - f"Python-{PYTHON_VERSION}.tgz", - ) - except RuntimeError as e: - # Sometimes setting up modules such as pip/lzma can cause - # the python installer script to return a non zero exit - # code where actually the headers and dynamic libraries - # are installed correctly. This is all we need so we catch - # the exception and only reraise if a partially successful - # install is not detected. - if not os.path.exists(os.path.join(DEPS_DIR, "install", f"python-{PYTHON_VERSION}")): - raise e + # Don't fail silently on missing Python dependencies (e.g. openssl or zlib), + # because later ifcopenshell-python build will fail too but in a more confusing way. + build_dependency( + f"python-{PYTHON_VERSION}", + "autoconf", + PYTHON_CONFIGURE_ARGS, + f"http://www.python.org/ftp/python/{PYTHON_VERSION}/", + f"Python-{PYTHON_VERSION}.tgz", + ) + python_bin = INSTALL_DIR / f"python-{PYTHON_VERSION}" / "bin" / "python3" + # `_ssl` module is present -> we will be able to install `numpy` later + # to verify IfcOpenShell installation + run([str(python_bin), "-c", "import _ssl"]) if MAC_CROSS_COMPILE_INTEL: assert original_path @@ -1535,13 +1515,16 @@ if "IfcOpenShell-Python" in targets: ) # Copy setup.py where pyodide build system expects it. shutil.copy(REPO_PATH / "pyodide" / "setup.py", REPO_PATH) + # Empty pyproject so it's contents won't affect the resulting wheelthe the + # otherwise the wheel will use version and dependencies from toml, not setup.py. + (REPO_PATH / "pyproject.toml").write_text("") elif USE_CURRENT_PYTHON_VERSION: python_info = sysconfig.get_paths() compile_python_wrapper(platform.python_version(), python_info["include"], sys.executable) else: for python_version in PYTHON_VERSIONS: - python_path = Path(DEPS_DIR) / "install" / f"python-{python_version}" + python_path = INSTALL_DIR / f"python-{python_version}" module_dir = compile_python_wrapper(python_version, python_path=python_path) assert module_dir # Not sure why, but added after reading this in the logs diff --git a/pyodide/cache_dependencies.py b/nix/cache_dependencies.py similarity index 76% rename from pyodide/cache_dependencies.py rename to nix/cache_dependencies.py index 5800cba067..465d6002f1 100644 --- a/pyodide/cache_dependencies.py +++ b/nix/cache_dependencies.py @@ -5,9 +5,13 @@ This script is finding common install directory and either packs each folder into a tar.gz archive, if it wasn't packed before, or unpacks existing archives. +Expected to be executed from 'build' directory (e.g. that might contain 'Linux/x86_64/install'). + Usage: python cache_dependencies.py [pack|unpack] """ +import platform +import subprocess import sys import tarfile from pathlib import Path @@ -17,11 +21,20 @@ CACHE_PREFIX = "cache-" def get_install_dir() -> Path: - for data in Path.cwd().glob("*/*/install"): + if platform.system() == "Darwin": + pattern = "Darwin/*/*/install" + else: + pattern = "*/*/install" + for data in Path.cwd().glob(pattern): return data raise Exception("No install dir found") +def run(cmd: str) -> None: + print(f"Running command: `{cmd}`") + subprocess.check_call(cmd, shell=True) + + def pack_dependencies(install_dir: Path) -> None: # Process each install_dir for dependency_path in install_dir.iterdir(): @@ -32,8 +45,8 @@ def pack_dependencies(install_dir: Path) -> None: if tar_path.exists(): print(f"Skipping existing cache: '{tar_path}'") else: - with tarfile.open(tar_path, "w:gz") as tar: - tar.add(dependency_path, arcname=dependency_path.name) + # Python's `tarfile` is 10x slower than `tar` cli, so we use `tar`. + run(f'tar -czf "{tar_path}" -C "{install_dir}" "{dependency_name}"') print(f"Created cache: '{tar_path}'") diff --git a/nix/patches/occt/no_ExpToCasExe.patch b/nix/patches/occt/no_ExpToCasExe.patch index 6e10f2a9fb..2f99845925 100644 --- a/nix/patches/occt/no_ExpToCasExe.patch +++ b/nix/patches/occt/no_ExpToCasExe.patch @@ -1,13 +1,9 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index fd17283f77..6cecf9dad3 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -826,6 +826,8 @@ if (EMSCRIPTEN) - list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe) - endif() - -+list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe) -+ - # bison - if (BUILD_YACCLEX) - OCCT_INCLUDE_CMAKE_FILE ("adm/cmake/bison") +--- a/adm/MODULES ++++ b/adm/MODULES +@@ -3,5 +3,5 @@ ModelingData TKG2d TKG3d TKGeomBase TKBRep + ModelingAlgorithms TKGeomAlgo TKTopAlgo TKPrim TKBO TKBool TKHLR TKFillet TKOffset TKFeat TKMesh TKXMesh TKShHealing + Visualization TKService TKV3d TKOpenGl TKOpenGles TKMeshVS TKIVtk TKD3DHost + ApplicationFramework TKCDF TKLCAF TKCAF TKBinL TKXmlL TKBin TKXml TKStdL TKStd TKTObj TKBinTObj TKXmlTObj TKVCAF +-DataExchange TKXDE TKXSBase TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP TKIGES TKXCAF TKXDEIGES TKXDESTEP TKSTL TKVRML TKXmlXCAF TKBinXCAF TKRWMesh TKXDECascade TKExpress ExpToCasExe ++DataExchange TKXDE TKXSBase TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP TKIGES TKXCAF TKXDEIGES TKXDESTEP TKSTL TKVRML TKXmlXCAF TKBinXCAF TKRWMesh TKXDECascade TKExpress + Draw TKDraw TKTopTest TKOpenGlTest TKOpenGlesTest TKD3DHostTest TKViewerTest TKXSDRAW TKDCAF TKXDEDRAW TKTObjDRAW TKQADraw TKIVtkDraw DRAWEXE diff --git a/nix/patches/occt/no_ExpToCasExe_7_7_2.patch b/nix/patches/occt/no_ExpToCasExe_7_7_2.patch deleted file mode 100644 index 8b9f924e83..0000000000 --- a/nix/patches/occt/no_ExpToCasExe_7_7_2.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 1bacca1a48..11f931ad39 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -820,6 +820,8 @@ else() - OCCT_CHECK_AND_UNSET ("3RDPARTY_DOT_EXECUTABLE") - endif() - -+list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe) -+ - # bison - if (BUILD_YACCLEX) - OCCT_INCLUDE_CMAKE_FILE ("adm/cmake/bison") diff --git a/nix/patches/occt/no_ExpToCasExe_7_8_1.patch b/nix/patches/occt/no_ExpToCasExe_7_8_1.patch deleted file mode 100644 index 63d6fd3206..0000000000 --- a/nix/patches/occt/no_ExpToCasExe_7_8_1.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 86905287dc..9d0bce984c 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -828,6 +828,8 @@ else() - OCCT_CHECK_AND_UNSET ("3RDPARTY_DOT_EXECUTABLE") - endif() - -+list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe) -+ - # bison - if (BUILD_YACCLEX) - OCCT_INCLUDE_CMAKE_FILE ("adm/cmake/bison") diff --git a/nix/patches/occt/no_ExpToCasExe_7_9_1.patch b/nix/patches/occt/no_ExpToCasExe_7_9_1.patch deleted file mode 100644 index abe2b20ef9..0000000000 --- a/nix/patches/occt/no_ExpToCasExe_7_9_1.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 34300d41ad..09b2e0d45f 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -721,6 +721,8 @@ else() - OCCT_CHECK_AND_UNSET ("3RDPARTY_DOT_EXECUTABLE") - endif() - -+list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe) -+ - # bison - if (BUILD_YACCLEX) - list (APPEND OCCT_3RDPARTY_CMAKE_LIST "adm/cmake/bison") diff --git a/pyodide/build_pyodide.sh b/pyodide/build_pyodide.sh index 2994a44ae6..20ad946162 100755 --- a/pyodide/build_pyodide.sh +++ b/pyodide/build_pyodide.sh @@ -1,9 +1,12 @@ #!/usr/bin/bash set -ex +# Script is assuming that it will be possible to execute it multiple times +# therefore we're clearing venv each time and ignoring existing 'emsdk' folder. + # Install uv. curl -LsSf https://astral.sh/uv/install.sh | sh -uv venv --python 3.13 +uv venv --python 3.13 --clear source .venv/bin/activate # Install pyodide cross build environment. @@ -13,7 +16,9 @@ uv pip install pyodide-build uv run pyodide xbuildenv install # Emscripten doesn't come with xbuildenv. -git clone https://github.com/emscripten-core/emsdk +if [ ! -d emsdk ]; then + git clone https://github.com/emscripten-core/emsdk +fi pushd emsdk PYODIDE_EMSCRIPTEN_VERSION=$(pyodide config get emscripten_version) ./emsdk install ${PYODIDE_EMSCRIPTEN_VERSION} diff --git a/pyproject.toml b/pyproject.toml index 74c3b0ee18..bdc9bf1a64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,9 +3,9 @@ name = "IfcOpenShell" version = "0.0.0" dependencies = [ "black==26.1.0", - "ruff==0.15.0", + "ruff==0.15.4", "poethepoet", - "gersemi==0.25.4", + "gersemi==0.26.0", ] [tool.black] @@ -15,7 +15,8 @@ include = ''' |nix/.*.pyi?$ ''' extend-exclude = ''' - src/ifcopenshell-python/ifcopenshell/express/* + src/ifcopenshell-python/ifcopenshell/express/rules/* + |src/ifcopenshell-python/ifcopenshell/express/express_parser.py |src/ifcopenshell-python/ifcopenshell/mvd/* |src/ifcopenshell-python/ifcopenshell/simple_spf/* |src/ifc2ca/templates/* diff --git a/src/bcf/bcf/v3/bcfapi.py b/src/bcf/bcf/v3/bcfapi.py index 127d9e3732..3ac85b9685 100644 --- a/src/bcf/bcf/v3/bcfapi.py +++ b/src/bcf/bcf/v3/bcfapi.py @@ -34,8 +34,8 @@ client_id, client_secret = "", "" class OAuthReceiver(http.server.BaseHTTPRequestHandler): def do_GET(self) -> None: query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query) - self.server.auth_code = query.get("code", [""])[0] # type:ignore - self.server.auth_state = query.get("state", [""])[0] # type:ignore + self.server.auth_code = query.get("code", [""])[0] # type: ignore + self.server.auth_state = query.get("state", [""])[0] # type: ignore self.send_response(200) self.send_header("Content-type", "text/plain") self.end_headers() diff --git a/src/bonsai/Makefile b/src/bonsai/Makefile index 6b4704b7b3..310d31a4b0 100644 --- a/src/bonsai/Makefile +++ b/src/bonsai/Makefile @@ -104,7 +104,7 @@ endif endif # def PLATFORM # Current build commit hash. -OLD:=e8eb5e4 +OLD:=1c5b825 .PHONY: bump bump: ifndef NEW @@ -371,7 +371,7 @@ test-tool: ifndef MODULE pytest test/tool else - pytest test/tool/test_$(MODULE).py + pytest test/tool/test_$(MODULE).py --maxfail=1 endif # Reregistering test is not added to the standard test suite because during unregister diff --git a/src/bonsai/bonsai/__init__.py b/src/bonsai/bonsai/__init__.py index b4083b0eef..e30e2bf699 100644 --- a/src/bonsai/bonsai/__init__.py +++ b/src/bonsai/bonsai/__init__.py @@ -34,7 +34,6 @@ IN_PACKAGE = __package__ == "bonsai" import platform import re -import shutil import traceback import uuid import webbrowser diff --git a/src/bonsai/bonsai/bim/export_ifc.py b/src/bonsai/bonsai/bim/export_ifc.py index 7295c72e34..633d6292f9 100644 --- a/src/bonsai/bonsai/bim/export_ifc.py +++ b/src/bonsai/bonsai/bim/export_ifc.py @@ -24,21 +24,14 @@ import os import tempfile import zipfile from logging import Logger -from math import radians from typing import Union import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.util.element -import ifcopenshell.util.placement import ifcopenshell.util.unit -from mathutils import Vector -import bonsai.core.aggregate import bonsai.core.geometry -import bonsai.core.spatial -import bonsai.core.style import bonsai.tool as tool from bonsai.bim.ifc import IfcStore @@ -52,7 +45,6 @@ class IfcExporter: self.set_header() IfcStore.update_cache() self.sync_all_objects() - tool.Project.save_linked_models_to_ifc() extension = self.ifc_export_settings.output_file.split(".")[-1].lower() if extension == "ifczip": with tempfile.TemporaryDirectory() as unzipped_path: diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index 9267134988..ab4a0875ab 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -28,7 +28,6 @@ import bpy import ifcopenshell import ifcopenshell.ifcopenshell_wrapper as W import ifcopenshell.util.attribute -import ifcopenshell.util.element import ifcopenshell.util.unit from ifcopenshell.util.doc import ( get_attribute_doc, diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 6e04800dd4..7e8f40560e 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -32,7 +32,6 @@ import ifcopenshell.api.pset import ifcopenshell.geom import ifcopenshell.ifcopenshell_wrapper as W import ifcopenshell.util.element -import ifcopenshell.util.geolocation import ifcopenshell.util.placement import ifcopenshell.util.representation import ifcopenshell.util.shape @@ -85,7 +84,7 @@ class MaterialCreator: if element.is_a("IfcTypeProduct"): self.parse_element_type_material_styles(element) self.parsed_meshes.add(self.mesh.name) - if not self.ifc_import_settings.load_indexed_maps: + if self.ifc_import_settings.load_indexed_maps: self.load_texture_maps(shape_has_openings) self.assign_material_slots_to_faces() tool.Geometry.record_object_materials(obj) @@ -117,7 +116,6 @@ class MaterialCreator: for texture in texture_style.Textures or []: if coords := getattr(texture, "IsMappedBy", None): coords = coords[0] - # IfcTextureCoordinateGenerator handled in the style shader graph if coords.is_a("IfcIndexedTextureMap"): return coords # TODO: support IfcTextureMap @@ -135,6 +133,10 @@ class MaterialCreator: if shape_has_openings and coords.is_a("IfcIndexedTextureMap"): continue tool.Loader.load_indexed_map(coords, self.mesh) + elif tool.Style.get_texture_style(material): + # No explicit coordinate mapping (e.g. IFC2X3 has no IsMappedBy, + # and IFC4 COORD uses generated UVs). Bake XY→UV as fallback. + tool.Loader.load_generated_uv_map(self.mesh) def assign_material_slots_to_faces(self) -> None: if not self.mesh["ios_materials"]: @@ -892,48 +894,6 @@ class IfcImporter: obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, self.get_element_matrix(element)) ) - if element.is_a("IfcAnnotation") and getattr(element, "ObjectType", None) == "IMAGE": - image = None - if obj.data and obj.data.materials and obj.data.materials[0]: - material = obj.data.materials[0] - if material.use_nodes and material.node_tree: - for node in material.node_tree.nodes: - if node.type == "TEX_IMAGE" and node.image: - image = node.image - break - if image: - import bmesh - - bm = bmesh.new() - bm.from_mesh(obj.data) - if not bm.loops.layers.uv: - uv_layer = bm.loops.layers.uv.new() - else: - uv_layer = bm.loops.layers.uv.active - - if bm.verts: - min_x = min(v.co.x for v in bm.verts) - max_x = max(v.co.x for v in bm.verts) - min_y = min(v.co.y for v in bm.verts) - max_y = max(v.co.y for v in bm.verts) - - width = max_x - min_x - height = max_y - min_y - - for face in bm.faces: - for loop in face.loops: - vert = loop.vert - u = (vert.co.x - min_x) / width if width > 0 else 0.5 - v = (vert.co.y - min_y) / height if height > 0 else 0.5 - - u = max(0.0, min(1.0, u)) - v = max(0.0, min(1.0, v)) - - loop[uv_layer].uv = (u, v) - - bm.to_mesh(obj.data) - bm.free() - obj.data.update() return obj def load_existing_meshes(self) -> None: diff --git a/src/bonsai/bonsai/bim/module/aggregate/decorator.py b/src/bonsai/bonsai/bim/module/aggregate/decorator.py index 080f965bf8..eb389a58bd 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/decorator.py +++ b/src/bonsai/bonsai/bim/module/aggregate/decorator.py @@ -19,7 +19,6 @@ import blf import bpy import gpu -import ifcopenshell import ifcopenshell.util.element from bpy.types import SpaceView3D from bpy_extras import view3d_utils @@ -27,7 +26,6 @@ from gpu_extras.batch import batch_for_shader from mathutils import Vector import bonsai.tool as tool -from bonsai.bim.module.geometry.decorator import ItemDecorator def transparent_color(color, alpha=0.1): diff --git a/src/bonsai/bonsai/bim/module/aggregate/operator.py b/src/bonsai/bonsai/bim/module/aggregate/operator.py index e9f23afece..98bc612784 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/operator.py +++ b/src/bonsai/bonsai/bim/module/aggregate/operator.py @@ -19,8 +19,6 @@ from typing import TYPE_CHECKING import bpy -import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.group import ifcopenshell.api.pset import ifcopenshell.api.root diff --git a/src/bonsai/bonsai/bim/module/aggregate/prop.py b/src/bonsai/bonsai/bim/module/aggregate/prop.py index 241663882f..0595c200c5 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/prop.py +++ b/src/bonsai/bonsai/bim/module/aggregate/prop.py @@ -23,12 +23,7 @@ import ifcopenshell.util.element from bpy.props import ( BoolProperty, CollectionProperty, - EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, PointerProperty, - StringProperty, ) from bpy.types import PropertyGroup @@ -37,8 +32,6 @@ from bonsai.bim.module.aggregate.decorator import ( AggregateDecorator, AggregateModeDecorator, ) -from bonsai.bim.module.spatial.data import SpatialData -from bonsai.bim.prop import Attribute, StrProperty def can_aggregate(relating_obj: bpy.types.Object, related_obj: bpy.types.Object) -> bool: diff --git a/src/bonsai/bonsai/bim/module/aggregate/ui.py b/src/bonsai/bonsai/bim/module/aggregate/ui.py index d693843401..f59ead6308 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/ui.py +++ b/src/bonsai/bonsai/bim/module/aggregate/ui.py @@ -21,7 +21,6 @@ from bpy.types import Panel import bonsai.tool as tool from bonsai.bim.ifc import IfcStore from bonsai.bim.module.aggregate.data import AggregateData -from bonsai.bim.module.group.data import GroupsData, ObjectGroupsData class BIM_PT_aggregate(Panel): diff --git a/src/bonsai/bonsai/bim/module/alignment/operator.py b/src/bonsai/bonsai/bim/module/alignment/operator.py index 0705dcb1b9..42ad1d25de 100644 --- a/src/bonsai/bonsai/bim/module/alignment/operator.py +++ b/src/bonsai/bonsai/bim/module/alignment/operator.py @@ -18,24 +18,14 @@ # pyright: reportUnnecessaryTypeIgnoreComment=error -import calendar -import json -import os import time -from datetime import datetime import bpy import ifcopenshell.api.alignment import ifcopenshell.api.spatial import ifcopenshell.geom -import ifcopenshell.util.selector -import ifcopenshell.util.sequence -import isodate from bpy_extras.io_utils import ImportHelper -from dateutil import parser, relativedelta -import bonsai.bim.module.sequence.helper as helper -import bonsai.core.sequence as core import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/attribute/prop.py b/src/bonsai/bonsai/bim/module/attribute/prop.py index 98ffb00c5c..acff2424b7 100644 --- a/src/bonsai/bonsai/bim/module/attribute/prop.py +++ b/src/bonsai/bonsai/bim/module/attribute/prop.py @@ -23,16 +23,12 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, - StringProperty, ) from bpy.types import PropertyGroup import bonsai.tool as tool -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute class BIMAttributeProperties(PropertyGroup): diff --git a/src/bonsai/bonsai/bim/module/bcf/bcfstore.py b/src/bonsai/bonsai/bim/module/bcf/bcfstore.py index a1fec8aa6e..b1abc97c6e 100644 --- a/src/bonsai/bonsai/bim/module/bcf/bcfstore.py +++ b/src/bonsai/bonsai/bim/module/bcf/bcfstore.py @@ -19,9 +19,7 @@ import os from typing import Union -import bcf import bcf.bcfxml -import bcf.v2.bcfxml import bpy import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/bcf/operator.py b/src/bonsai/bonsai/bim/module/bcf/operator.py index b0a6f1f699..6edf257f05 100644 --- a/src/bonsai/bonsai/bim/module/bcf/operator.py +++ b/src/bonsai/bonsai/bim/module/bcf/operator.py @@ -16,22 +16,18 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import os import tempfile import uuid import webbrowser -from math import atan, cos, degrees, radians, sin, tan +from math import atan, degrees, radians, tan from pathlib import Path -import bcf import bcf.agnostic.topic import bcf.agnostic.visinfo -import bcf.bcfxml import bcf.v2.bcfxml import bcf.v2.model import bcf.v2.topic import bcf.v2.visinfo -import bcf.v3 import bcf.v3.bcfxml import bcf.v3.document import bcf.v3.model @@ -43,11 +39,10 @@ import ifcopenshell.util.geolocation import ifcopenshell.util.unit import numpy as np from bpy_extras.io_utils import ExportHelper, ImportHelper -from mathutils import Euler, Matrix, Vector, geometry +from mathutils import Matrix, Vector from xsdata.models.datatype import XmlDateTime import bonsai.bim.module.bcf.bcfstore as bcfstore -import bonsai.bim.module.bcf.prop as bcf_prop import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/bcf/prop.py b/src/bonsai/bonsai/bim/module/bcf/prop.py index b720a2ff74..3ac751387e 100644 --- a/src/bonsai/bonsai/bim/module/bcf/prop.py +++ b/src/bonsai/bonsai/bim/module/bcf/prop.py @@ -24,8 +24,6 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, PointerProperty, StringProperty, diff --git a/src/bonsai/bonsai/bim/module/bcf/ui.py b/src/bonsai/bonsai/bim/module/bcf/ui.py index e3478f63cf..881bef488b 100644 --- a/src/bonsai/bonsai/bim/module/bcf/ui.py +++ b/src/bonsai/bonsai/bim/module/bcf/ui.py @@ -18,7 +18,6 @@ from __future__ import annotations -import os from typing import TYPE_CHECKING import bpy diff --git a/src/bonsai/bonsai/bim/module/boundary/decorator.py b/src/bonsai/bonsai/bim/module/boundary/decorator.py index 1f5859824e..a2d134a135 100644 --- a/src/bonsai/bonsai/bim/module/boundary/decorator.py +++ b/src/bonsai/bonsai/bim/module/boundary/decorator.py @@ -20,7 +20,6 @@ import bmesh import gpu from bpy.types import SpaceView3D from gpu_extras.batch import batch_for_shader -from mathutils import Vector import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/boundary/operator.py b/src/bonsai/bonsai/bim/module/boundary/operator.py index 4b26a0a4d9..5720d6aae5 100644 --- a/src/bonsai/bonsai/bim/module/boundary/operator.py +++ b/src/bonsai/bonsai/bim/module/boundary/operator.py @@ -29,18 +29,15 @@ import ifcopenshell.api.root import ifcopenshell.geom import ifcopenshell.util.element import ifcopenshell.util.placement -import ifcopenshell.util.representation import ifcopenshell.util.shape import ifcopenshell.util.unit import mathutils -import numpy as np import shapely import shapely.ops from ifcopenshell.util.shape_builder import ShapeBuilder from mathutils import Matrix, Vector import bonsai.bim.import_ifc as import_ifc -import bonsai.core import bonsai.core.geometry import bonsai.tool as tool from bonsai.bim.ifc import IfcStore diff --git a/src/bonsai/bonsai/bim/module/boundary/prop.py b/src/bonsai/bonsai/bim/module/boundary/prop.py index cd7e0b9de3..2e9ab8c975 100644 --- a/src/bonsai/bonsai/bim/module/boundary/prop.py +++ b/src/bonsai/bonsai/bim/module/boundary/prop.py @@ -21,13 +21,7 @@ from typing import TYPE_CHECKING, Union import bpy from bpy.props import ( BoolProperty, - CollectionProperty, - EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, PointerProperty, - StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/boundary/ui.py b/src/bonsai/bonsai/bim/module/boundary/ui.py index 54694d752c..0ca21b9ce5 100644 --- a/src/bonsai/bonsai/bim/module/boundary/ui.py +++ b/src/bonsai/bonsai/bim/module/boundary/ui.py @@ -16,8 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import bpy -from bpy.types import Panel, UIList +from bpy.types import Panel import bonsai.tool as tool from bonsai.bim.module.boundary.data import SpaceBoundariesData diff --git a/src/bonsai/bonsai/bim/module/brick/data.py b/src/bonsai/bonsai/bim/module/brick/data.py index bae294d713..e414723594 100644 --- a/src/bonsai/bonsai/bim/module/brick/data.py +++ b/src/bonsai/bonsai/bim/module/brick/data.py @@ -63,8 +63,7 @@ class BrickschemaData: if namespace == "https://brickschema.org/schema/Brick": return [] results = [] - query = BrickStore.graph.query( - """ + query = BrickStore.graph.query(""" PREFIX brick: PREFIX rdfs: PREFIX rdf: @@ -81,10 +80,7 @@ class BrickschemaData: } } GROUP BY ?object - """.replace( - "{uri}", uri - ) - ) + """.replace("{uri}", uri)) for row in query: predicate_uri = row.get("predicate") predicate_name = predicate_uri.toPython().split("#")[-1] diff --git a/src/bonsai/bonsai/bim/module/brick/operator.py b/src/bonsai/bonsai/bim/module/brick/operator.py index cb0a5f675f..2f71e6b638 100644 --- a/src/bonsai/bonsai/bim/module/brick/operator.py +++ b/src/bonsai/bonsai/bim/module/brick/operator.py @@ -19,7 +19,6 @@ import os import bpy -import ifcopenshell.api from bpy_extras.io_utils import ExportHelper, ImportHelper import bonsai.bim.handler diff --git a/src/bonsai/bonsai/bim/module/brick/prop.py b/src/bonsai/bonsai/bim/module/brick/prop.py index 9fe6d78407..6201113a09 100644 --- a/src/bonsai/bonsai/bim/module/brick/prop.py +++ b/src/bonsai/bonsai/bim/module/brick/prop.py @@ -23,10 +23,7 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup @@ -34,7 +31,7 @@ from bpy.types import PropertyGroup import bonsai.core.brick as core import bonsai.tool.brick as tool from bonsai.bim.module.brick.data import BrickschemaData, BrickschemaReferencesData -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import StrProperty from bonsai.tool.brick import BrickStore diff --git a/src/bonsai/bonsai/bim/module/bsdd/data.py b/src/bonsai/bonsai/bim/module/bsdd/data.py index 6e916d3355..37d030d867 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/data.py +++ b/src/bonsai/bonsai/bim/module/bsdd/data.py @@ -16,13 +16,8 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import bpy -import ifcopenshell -import ifcopenshell.util.classification -import ifcopenshell.util.date import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore def refresh(): diff --git a/src/bonsai/bonsai/bim/module/bsdd/operator.py b/src/bonsai/bonsai/bim/module/bsdd/operator.py index d3f5533c1a..8b94e12fd9 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/operator.py +++ b/src/bonsai/bonsai/bim/module/bsdd/operator.py @@ -19,7 +19,6 @@ import textwrap from typing import Any import bpy -import bsdd import ifcopenshell.api.pset import ifcopenshell.util.element diff --git a/src/bonsai/bonsai/bim/module/bsdd/prop.py b/src/bonsai/bonsai/bim/module/bsdd/prop.py index 347cdab1b2..5103358b6e 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/prop.py +++ b/src/bonsai/bonsai/bim/module/bsdd/prop.py @@ -23,10 +23,7 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup @@ -34,7 +31,7 @@ from bpy.types import PropertyGroup import bonsai.tool as tool from bonsai.bim.module.bsdd.data import BSDDData from bonsai.bim.module.classification.data import ClassificationsData -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute def get_active_dictionary(self: "BIMBSDDProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS: diff --git a/src/bonsai/bonsai/bim/module/cad/operator.py b/src/bonsai/bonsai/bim/module/cad/operator.py index 857d757ca1..3b6ef8fc69 100644 --- a/src/bonsai/bonsai/bim/module/cad/operator.py +++ b/src/bonsai/bonsai/bim/module/cad/operator.py @@ -17,13 +17,11 @@ # along with Bonsai. If not, see . import math -from math import cos, pi, radians, sin, sqrt -from typing import Union +from math import pi, sqrt import bmesh import bpy import bpy_extras -import ifcopenshell.util.unit import mathutils from mathutils import Matrix, Vector diff --git a/src/bonsai/bonsai/bim/module/cad/prop.py b/src/bonsai/bonsai/bim/module/cad/prop.py index db737b51dc..7dab36df91 100644 --- a/src/bonsai/bonsai/bim/module/cad/prop.py +++ b/src/bonsai/bonsai/bim/module/cad/prop.py @@ -22,8 +22,6 @@ from typing import TYPE_CHECKING import bpy from bpy.types import PropertyGroup -from bonsai.bim.module.model.data import AuthoringData - class BIMCadProperties(PropertyGroup): resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1) diff --git a/src/bonsai/bonsai/bim/module/cad/workspace.py b/src/bonsai/bonsai/bim/module/cad/workspace.py index b384463ac2..20faa4dcc7 100644 --- a/src/bonsai/bonsai/bim/module/cad/workspace.py +++ b/src/bonsai/bonsai/bim/module/cad/workspace.py @@ -20,12 +20,10 @@ import os from functools import partial import bpy -import ifcopenshell.util.unit from bpy.types import WorkSpaceTool -import bonsai.bim.module.type.prop as type_prop import bonsai.tool as tool -from bonsai.bim.module.model.data import AuthoringData, RailingData, RoofData +from bonsai.bim.module.model.data import RailingData, RoofData def load_custom_icons(): diff --git a/src/bonsai/bonsai/bim/module/clash/data.py b/src/bonsai/bonsai/bim/module/clash/data.py index 4ecbb229a4..f5b87ede95 100644 --- a/src/bonsai/bonsai/bim/module/clash/data.py +++ b/src/bonsai/bonsai/bim/module/clash/data.py @@ -18,9 +18,6 @@ import json -import bpy -import ifcopenshell.util.element - import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/clash/decorator.py b/src/bonsai/bonsai/bim/module/clash/decorator.py index d1d0daf095..ab95c92d9f 100644 --- a/src/bonsai/bonsai/bim/module/clash/decorator.py +++ b/src/bonsai/bonsai/bim/module/clash/decorator.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . import blf -import bmesh import gpu from bpy.types import SpaceView3D from bpy_extras.view3d_utils import location_3d_to_region_2d diff --git a/src/bonsai/bonsai/bim/module/clash/operator.py b/src/bonsai/bonsai/bim/module/clash/operator.py index 688568270f..94866a4e97 100644 --- a/src/bonsai/bonsai/bim/module/clash/operator.py +++ b/src/bonsai/bonsai/bim/module/clash/operator.py @@ -18,16 +18,13 @@ import json import logging -import os import tempfile from math import radians from pathlib import Path from typing import TYPE_CHECKING -import bmesh import bpy import ifcopenshell -import numpy as np from bpy_extras.io_utils import ExportHelper, ImportHelper from mathutils import Matrix, Vector diff --git a/src/bonsai/bonsai/bim/module/clash/prop.py b/src/bonsai/bonsai/bim/module/clash/prop.py index 5f42a400e7..8bcd71632b 100644 --- a/src/bonsai/bonsai/bim/module/clash/prop.py +++ b/src/bonsai/bonsai/bim/module/clash/prop.py @@ -26,7 +26,6 @@ from bpy.props import ( FloatProperty, FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup @@ -34,7 +33,7 @@ from ifcopenshell.geom.main import CLASH_TYPE_ITEMS, ClashType from mathutils import Vector import bonsai.tool as tool -from bonsai.bim.prop import Attribute, BIMFilterGroup, StrProperty +from bonsai.bim.prop import BIMFilterGroup, StrProperty class ClashSource(PropertyGroup): diff --git a/src/bonsai/bonsai/bim/module/classification/operator.py b/src/bonsai/bonsai/bim/module/classification/operator.py index 7bf699cc31..77b77ba541 100644 --- a/src/bonsai/bonsai/bim/module/classification/operator.py +++ b/src/bonsai/bonsai/bim/module/classification/operator.py @@ -18,7 +18,6 @@ import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.classification import ifcopenshell.api.pset import ifcopenshell.util.classification diff --git a/src/bonsai/bonsai/bim/module/classification/prop.py b/src/bonsai/bonsai/bim/module/classification/prop.py index 454f861f07..44f174bd13 100644 --- a/src/bonsai/bonsai/bim/module/classification/prop.py +++ b/src/bonsai/bonsai/bim/module/classification/prop.py @@ -23,10 +23,7 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/classification/ui.py b/src/bonsai/bonsai/bim/module/classification/ui.py index 77310be49a..6840238927 100644 --- a/src/bonsai/bonsai/bim/module/classification/ui.py +++ b/src/bonsai/bonsai/bim/module/classification/ui.py @@ -25,7 +25,6 @@ import ifcopenshell.util.classification from bpy.types import Panel, UIList import bonsai.bim.helper -import bonsai.bim.module.classification.prop as classification_prop import bonsai.tool as tool from bonsai.bim.module.classification.data import ( ClassificationsData, diff --git a/src/bonsai/bonsai/bim/module/constraint/operator.py b/src/bonsai/bonsai/bim/module/constraint/operator.py index 976f4ff6a3..d438325f44 100644 --- a/src/bonsai/bonsai/bim/module/constraint/operator.py +++ b/src/bonsai/bonsai/bim/module/constraint/operator.py @@ -19,7 +19,6 @@ from typing import TYPE_CHECKING import bpy -import ifcopenshell.api import ifcopenshell.api.constraint import bonsai.bim.helper diff --git a/src/bonsai/bonsai/bim/module/constraint/prop.py b/src/bonsai/bonsai/bim/module/constraint/prop.py index c76d48e8a6..55ae9d1223 100644 --- a/src/bonsai/bonsai/bim/module/constraint/prop.py +++ b/src/bonsai/bonsai/bim/module/constraint/prop.py @@ -20,19 +20,13 @@ from typing import TYPE_CHECKING, Literal import bpy from bpy.props import ( - BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup -from ifcopenshell.util.doc import get_entity_doc -import bonsai.tool as tool from bonsai.bim.module.constraint.data import ConstraintsData from bonsai.bim.prop import Attribute diff --git a/src/bonsai/bonsai/bim/module/context/data.py b/src/bonsai/bonsai/bim/module/context/data.py index ade8061a4b..e2717a166a 100644 --- a/src/bonsai/bonsai/bim/module/context/data.py +++ b/src/bonsai/bonsai/bim/module/context/data.py @@ -18,7 +18,6 @@ from typing import Any -import bpy import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/context/prop.py b/src/bonsai/bonsai/bim/module/context/prop.py index ff73c53a5a..e8540746ff 100644 --- a/src/bonsai/bonsai/bim/module/context/prop.py +++ b/src/bonsai/bonsai/bim/module/context/prop.py @@ -20,19 +20,13 @@ from typing import TYPE_CHECKING import bpy from bpy.props import ( - BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, - StringProperty, ) from bpy.types import PropertyGroup -from bonsai.bim.module.context.data import ContextData -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute class BIMContextProperties(PropertyGroup): diff --git a/src/bonsai/bonsai/bim/module/cost/data.py b/src/bonsai/bonsai/bim/module/cost/data.py index aab9ea9231..32299b8e41 100644 --- a/src/bonsai/bonsai/bim/module/cost/data.py +++ b/src/bonsai/bonsai/bim/module/cost/data.py @@ -18,13 +18,11 @@ from typing import Any, Union -import bpy import ifcopenshell import ifcopenshell.util.cost import ifcopenshell.util.date import ifcopenshell.util.element import ifcopenshell.util.unit -from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/cost/operator.py b/src/bonsai/bonsai/bim/module/cost/operator.py index 48b672a2c2..b612100a1d 100644 --- a/src/bonsai/bonsai/bim/module/cost/operator.py +++ b/src/bonsai/bonsai/bim/module/cost/operator.py @@ -987,10 +987,10 @@ class ExportCostSchedulesToPDF(bpy.types.Operator, ExportHelper): @classmethod def poll(cls, context): try: - import typst + import typst # noqa: F401 return True - except: + except ModuleNotFoundError: cls.poll_message_set( "Typst not available.\nIt can be installed from Quality and\nControl -> Debug and using 'typst' with Pip Install.\n(Run Blender as Administrator)" ) diff --git a/src/bonsai/bonsai/bim/module/cost/prop.py b/src/bonsai/bonsai/bim/module/cost/prop.py index 121ca617e7..426d5c1e33 100644 --- a/src/bonsai/bonsai/bim/module/cost/prop.py +++ b/src/bonsai/bonsai/bim/module/cost/prop.py @@ -19,16 +19,13 @@ from typing import TYPE_CHECKING, Literal, Union import bpy -import ifcopenshell.api import ifcopenshell.api.cost from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/covering/workspace.py b/src/bonsai/bonsai/bim/module/covering/workspace.py index 68b2704166..728b4f6013 100644 --- a/src/bonsai/bonsai/bim/module/covering/workspace.py +++ b/src/bonsai/bonsai/bim/module/covering/workspace.py @@ -21,7 +21,6 @@ import os from functools import partial import bpy -import ifcopenshell from bpy.types import WorkSpaceTool import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/covetool/operator.py b/src/bonsai/bonsai/bim/module/covetool/operator.py index fa298ada9c..fb2b2f3086 100644 --- a/src/bonsai/bonsai/bim/module/covetool/operator.py +++ b/src/bonsai/bonsai/bim/module/covetool/operator.py @@ -20,7 +20,6 @@ import json from math import atan2, degrees import bpy -import ifcopenshell import ifcopenshell.util.element import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/csv/operator.py b/src/bonsai/bonsai/bim/module/csv/operator.py index d42e8d3852..3b1db40b6b 100644 --- a/src/bonsai/bonsai/bim/module/csv/operator.py +++ b/src/bonsai/bonsai/bim/module/csv/operator.py @@ -19,14 +19,10 @@ from __future__ import annotations import json -import logging -import os -import tempfile from collections import Counter from typing import TYPE_CHECKING import bpy -import ifccsv import ifcopenshell import ifcopenshell.util.selector from bpy_extras.io_utils import ExportHelper, ImportHelper diff --git a/src/bonsai/bonsai/bim/module/csv/prop.py b/src/bonsai/bonsai/bim/module/csv/prop.py index a698f89b76..8104a8d75b 100644 --- a/src/bonsai/bonsai/bim/module/csv/prop.py +++ b/src/bonsai/bonsai/bim/module/csv/prop.py @@ -23,15 +23,11 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup -from bonsai.bim.prop import BIMFilterGroup, StrProperty +from bonsai.bim.prop import BIMFilterGroup class CsvAttribute(PropertyGroup): diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index 6e30a3cc0f..9315386008 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -29,7 +29,6 @@ from typing import TYPE_CHECKING, Any, Literal, Union, assert_never, get_args import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.pset import ifcopenshell.geom import ifcopenshell.ifcopenshell_wrapper as W diff --git a/src/bonsai/bonsai/bim/module/debug/prop.py b/src/bonsai/bonsai/bim/module/debug/prop.py index e52ec5ff7d..9744159e20 100644 --- a/src/bonsai/bonsai/bim/module/debug/prop.py +++ b/src/bonsai/bonsai/bim/module/debug/prop.py @@ -20,13 +20,9 @@ from typing import TYPE_CHECKING, Literal, get_args import bpy from bpy.props import ( - BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/demo/prop.py b/src/bonsai/bonsai/bim/module/demo/prop.py index f206fe3156..6570c4cbc0 100644 --- a/src/bonsai/bonsai/bim/module/demo/prop.py +++ b/src/bonsai/bonsai/bim/module/demo/prop.py @@ -32,18 +32,10 @@ from typing import TYPE_CHECKING -import bpy - # Properties have many different data types. We won't use all of them in this # demo module, but this is a list for your reference. from bpy.props import ( BoolProperty, - CollectionProperty, - EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/diff/prop.py b/src/bonsai/bonsai/bim/module/diff/prop.py index c8f9d48c84..cfe5b147fe 100644 --- a/src/bonsai/bonsai/bim/module/diff/prop.py +++ b/src/bonsai/bonsai/bim/module/diff/prop.py @@ -23,16 +23,12 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup from bonsai.bim.module.diff.data import DiffData -from bonsai.bim.prop import BIMFilterGroup, StrProperty +from bonsai.bim.prop import BIMFilterGroup def update_diff_json_file(self: "DiffProperties", context: bpy.types.Context) -> None: diff --git a/src/bonsai/bonsai/bim/module/document/data.py b/src/bonsai/bonsai/bim/module/document/data.py index 97b4a1f206..f194e2f4e0 100644 --- a/src/bonsai/bonsai/bim/module/document/data.py +++ b/src/bonsai/bonsai/bim/module/document/data.py @@ -19,8 +19,6 @@ import os import bpy -import ifcopenshell -import ifcopenshell.util.schema from natsort import natsorted import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/document/operator.py b/src/bonsai/bonsai/bim/module/document/operator.py index a4e5cfc4ac..006ed27957 100644 --- a/src/bonsai/bonsai/bim/module/document/operator.py +++ b/src/bonsai/bonsai/bim/module/document/operator.py @@ -20,10 +20,9 @@ import json import bpy -import bonsai.bim.handler import bonsai.core.document as core import bonsai.tool as tool -from bonsai.bim.module.document.data import DocumentData, ObjectDocumentData +from bonsai.bim.module.document.data import ObjectDocumentData class LoadProjectDocuments(bpy.types.Operator): diff --git a/src/bonsai/bonsai/bim/module/document/prop.py b/src/bonsai/bonsai/bim/module/document/prop.py index b1eb41cc2e..c51f7b636c 100644 --- a/src/bonsai/bonsai/bim/module/document/prop.py +++ b/src/bonsai/bonsai/bim/module/document/prop.py @@ -5,17 +5,14 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup import bonsai.tool as tool from bonsai.bim.module.document.data import DocumentData, refresh -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute def update_document_name(self: "Document", context: bpy.types.Context) -> None: diff --git a/src/bonsai/bonsai/bim/module/document/ui.py b/src/bonsai/bonsai/bim/module/document/ui.py index eda5d34a1d..d618d6d966 100644 --- a/src/bonsai/bonsai/bim/module/document/ui.py +++ b/src/bonsai/bonsai/bim/module/document/ui.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import bpy from bpy.types import Panel, UIList import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/drawing/annotation.py b/src/bonsai/bonsai/bim/module/drawing/annotation.py index 18d914872a..d1e2dc4b2b 100644 --- a/src/bonsai/bonsai/bim/module/drawing/annotation.py +++ b/src/bonsai/bonsai/bim/module/drawing/annotation.py @@ -18,15 +18,12 @@ from __future__ import annotations -import math -import os -from pathlib import Path from typing import Optional import bmesh import bpy import ifcopenshell.util.element -from mathutils import Matrix, Vector +from mathutils import Vector import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/drawing/data.py b/src/bonsai/bonsai/bim/module/drawing/data.py index 1668d386d3..8373ed29bc 100644 --- a/src/bonsai/bonsai/bim/module/drawing/data.py +++ b/src/bonsai/bonsai/bim/module/drawing/data.py @@ -23,8 +23,6 @@ from typing import Any, Union import bpy import ifcopenshell.util.element -import ifcopenshell.util.representation -import ifcopenshell.util.selector import ifcopenshell.util.unit from natsort import natsorted diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index 705fabfcf9..f950f7a1bb 100644 --- a/src/bonsai/bonsai/bim/module/drawing/decoration.py +++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py @@ -23,7 +23,7 @@ from functools import cache from math import acos, atan, cos, degrees, pi, radians, sin from pathlib import Path from timeit import default_timer as timer -from typing import Optional, Union +from typing import Optional import blf import bmesh @@ -34,7 +34,6 @@ import ifcopenshell.util.element import ifcopenshell.util.representation import ifcopenshell.util.unit import numpy as np -import shapely from bpy.types import SpaceView3D from bpy_extras.view3d_utils import location_3d_to_region_2d from gpu_extras.batch import batch_for_shader diff --git a/src/bonsai/bonsai/bim/module/drawing/helper.py b/src/bonsai/bonsai/bim/module/drawing/helper.py index ee8ecf805d..7dce81359d 100644 --- a/src/bonsai/bonsai/bim/module/drawing/helper.py +++ b/src/bonsai/bonsai/bim/module/drawing/helper.py @@ -17,10 +17,8 @@ # along with Bonsai. If not, see . import math -from typing import Union import bpy -import ifcopenshell import ifcopenshell.util.element import ifcopenshell.util.unit import mathutils.geometry diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index d02e4c6c76..582c7b062b 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -18,9 +18,9 @@ import hashlib import json +import logging import multiprocessing import os -import re import shutil import subprocess import time @@ -41,7 +41,6 @@ from typing import ( import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.document import ifcopenshell.api.pset import ifcopenshell.api.style @@ -50,19 +49,18 @@ import ifcopenshell.ifcopenshell_wrapper import ifcopenshell.util.element import ifcopenshell.util.representation import ifcopenshell.util.selector +import ifcopenshell.util.shape_builder import ifcopenshell.util.unit import numpy as np import shapely -import shapely.ops from bpy_extras.image_utils import load_image from bpy_extras.io_utils import ImportHelper from lxml import etree -from mathutils import Color, Matrix, Vector +from mathutils import Color, Vector import bonsai.bim.export_ifc import bonsai.bim.handler -import bonsai.bim.helper -import bonsai.bim.module.drawing.annotation as annotation +import bonsai.bim.import_ifc import bonsai.bim.module.drawing.sheeter as sheeter import bonsai.bim.module.drawing.svgwriter as svgwriter import bonsai.core.drawing as core @@ -138,7 +136,7 @@ class AddAnnotationType(bpy.types.Operator, tool.Ifc.Operator): element.ApplicableOccurrence = f"IfcAnnotation/{object_type}" if props.create_representation_for_type and object_type == "IMAGE": - bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", use_existing_object_by_name=obj.name) + bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", existing_object_by_name=obj.name) class EnableAddAnnotationType(bpy.types.Operator): @@ -928,7 +926,7 @@ class CreateDrawing(bpy.types.Operator): props = tool.Project.get_project_props() for link in props.get_loaded_links_for_drawings(): - files[link.name] = self.get_linked_file(link) + files[link.filepath] = self.get_linked_file(link) target_view = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]["TargetView"] self.setup_serialiser(target_view) @@ -1374,7 +1372,7 @@ class CreateDrawing(bpy.types.Operator): return True def get_linked_file(self, link: "Link") -> ifcopenshell.file: - link_path = link.name + link_path = link.filepath ifc_file = IfcStore.session_files.get(link_path, None) if ifc_file is not None: return ifc_file @@ -1759,7 +1757,7 @@ class AddAnnotation(bpy.types.Operator, tool.Ifc.Operator): enable_editing=True, ) if props.object_type == "IMAGE": - bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", use_existing_object_by_name=obj.name) + bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", existing_object_by_name=obj.name) class AddSheet(bpy.types.Operator, tool.Ifc.Operator): @@ -2896,12 +2894,16 @@ class AddSchedule(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_options = {"REGISTER", "UNDO"} bl_description = "Add an .ods, .xls or .xlsx file as a schedule" + files: bpy.props.CollectionProperty(name="Files", type=bpy.types.OperatorFileListElement) + directory: bpy.props.StringProperty(subtype="DIR_PATH") filter_glob: bpy.props.StringProperty(default="*.ods;*.xls;*.xlsx", options={"HIDDEN"}) use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True) def _execute(self, context): - filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path) - core.add_document(tool.Ifc, tool.Drawing, "SCHEDULE", uri=filepath) + for filepath in tool.Blender.get_selected_files( + self.directory, self.files, use_relative_path=self.use_relative_path + ): + core.add_document(tool.Ifc, tool.Drawing, "SCHEDULE", uri=filepath) class RemoveSchedule(bpy.types.Operator, tool.Ifc.Operator): @@ -3090,23 +3092,16 @@ class AddReference(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_description = "Import a .svg file to the project as a reference" bl_options = {"REGISTER", "UNDO"} + files: bpy.props.CollectionProperty(name="Files", type=bpy.types.OperatorFileListElement) + directory: bpy.props.StringProperty(subtype="DIR_PATH") filter_glob: bpy.props.StringProperty(default="*.svg", options={"HIDDEN"}) use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True) filename_ext = ".svg" - files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement) - directory: bpy.props.StringProperty(subtype="DIR_PATH") - def _execute(self, context): - # Handle both single and multiple file selection - if self.files: - for file_elem in self.files: - filepath = os.path.join(self.directory, file_elem.name) - uri = tool.Ifc.get_uri(filepath, use_relative_path=self.use_relative_path) - core.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=uri) - else: - # Fallback for single file (backward compatibility) - filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path) + for filepath in tool.Blender.get_selected_files( + self.directory, self.files, use_relative_path=self.use_relative_path + ): core.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=filepath) @@ -3165,6 +3160,7 @@ class EditTextPopup(bpy.types.Operator): bpy.ops.bim.disable_editing_text() def execute(self, context): + # TODO: check for possible subtle undo bug here # can't use invoke() because this operator # will be run indirectly by hotkey # so we use execute() and track whether it's the first run of the operator @@ -3804,83 +3800,51 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True) filter_image: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"}) filter_folder: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"}) - - override_existing_image: bpy.props.BoolProperty( - name="Override Existing Image", - default=True, - description=( - "Override image if it was previously loaded to Blender. If disabled, will always create a new image" - ), - ) - use_existing_object_by_name: bpy.props.StringProperty( - name="Use Existing Object By Name", - description="Existing object name to add a style with reference image to. If not provided will create a new object.", - options={"SKIP_SAVE"}, - ) - x_length: bpy.props.FloatProperty( name="X Length", - description="Width of the reference image in project units", + description="Width of the reference image", default=1.0, min=0.001, soft_min=0.01, precision=3, + unit="LENGTH", ) y_length: bpy.props.FloatProperty( name="Y Length", - description="Height of the reference image in project units", + description="Height of the reference image", default=1.0, min=0.001, soft_min=0.01, precision=3, + unit="LENGTH", + ) + show_texture_solid_mode: bpy.props.BoolProperty( + name="Show Texture in Solid mode (slow)", + description="Show Texture in Solid mode (slow)", + default=False, ) - show_dimensions_dialog: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"}) - - def draw(self, context): - layout = self.layout - - if getattr(self, "show_dimensions_dialog", False): - if tool.Ifc.get(): - length_unit = ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "LENGTHUNIT") - if length_unit: - unit_name = ifcopenshell.util.unit.get_full_unit_name(length_unit).lower() - else: - unit_name = "project units" - layout.label(text=f"Set Reference Image Dimensions (in {unit_name}):") - else: - layout.label(text="Set Reference Image Dimensions (in project units):") - layout.separator() - layout.prop(self, "x_length") - layout.prop(self, "y_length") - else: - if Path(tool.Ifc.get_path()).is_file(): - layout.prop(self, "use_relative_path") - else: - self.use_relative_path = False - layout.label(text="Save the .ifc file first ") - layout.label(text="to use relative paths.") - layout.prop(self, "override_existing_image") - layout.prop(self, "use_existing_object_by_name") + @classmethod + def poll(cls, context): + if not tool.Ifc.get(): + cls.poll_message_set("No IFC project is loaded.") + return False + return True def invoke(self, context, event): - if not getattr(self, "show_dimensions_dialog", False): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - else: - return context.window_manager.invoke_props_dialog(self) + self._last_filepath = "" + return super().invoke(context, event) + + def check(self, context): + if not hasattr(self, "_last_filepath"): + self._last_filepath = "" + + if self.filepath and self.filepath != self._last_filepath: + self._last_filepath = self.filepath - def execute(self, context): - if not getattr(self, "show_dimensions_dialog", False): abs_path = Path(self.filepath).absolute().resolve() - if self.override_existing_image: - params = {"check_existing": True, "force_reload": True} - else: - params = {"check_existing": False} - - try: - image = load_image(abs_path.name, str(abs_path.parent), **params) - + if abs_path.exists() and abs_path.is_file(): + image = load_image(abs_path.name, str(abs_path.parent), check_existing=False) image_width_px = image.size[0] image_height_px = image.size[1] aspect_ratio = image_width_px / image_height_px @@ -3893,17 +3857,23 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): self.y_length = 1.0 bpy.data.images.remove(image) + return True - except Exception as e: - self.report({"ERROR"}, f"Failed to load image: {str(e)}") - return {"CANCELLED"} + return False - self.show_dimensions_dialog = True - return context.window_manager.invoke_props_dialog(self) - - return self._execute(context) + def draw(self, context): + layout = self.layout + if Path(tool.Ifc.get_path()).is_file(): + layout.prop(self, "use_relative_path") + else: + self.use_relative_path = False + layout.prop(self, "show_texture_solid_mode") + layout.prop(self, "x_length") + layout.prop(self, "y_length") def _execute(self, context): + project_props = tool.Project.get_project_props() + project_props.load_indexed_maps = self.show_texture_solid_mode space = tool.Blender.get_view3d_space() if space.shading.color_type != "TEXTURE": space.shading.color_type = "TEXTURE" @@ -3916,121 +3886,66 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): image_filepath = Path(tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)) ifc_file = tool.Ifc.get() - if self.override_existing_image: - params = {"check_existing": True, "force_reload": True} - else: - params = {"check_existing": False} + params = {"check_existing": False} image = load_image(abs_path.name, str(abs_path.parent), **params) - def bm_add_image_plane(mesh): - bm = tool.Blender.get_bmesh_for_mesh(mesh, clean=True) + mesh = bpy.data.meshes.new(image_filepath.stem) + obj = bpy.data.objects.new(image_filepath.stem, mesh) + element = tool.Drawing.run_root_assign_class( + obj=obj, ifc_class="IfcAnnotation", predefined_type="IMAGE", should_add_representation=False + ) - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) - plane_scale = Vector((self.x_length * unit_scale / 2.0, self.y_length * unit_scale / 2.0, 1.0)) - matrix = Matrix.LocRotScale(None, None, plane_scale) - bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=1, matrix=matrix, calc_uvs=False) + builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file) + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) + hx = self.x_length * 0.5 / unit_scale + hy = self.y_length * 0.5 / unit_scale + verts = [(-hx, -hy, 0.0), (hx, -hy, 0.0), (hx, hy, 0.0), (-hx, hy, 0.0)] + item = builder.mesh(verts, [[0, 1, 2, 3]]) - if not bm.loops.layers.uv: - uv_layer = bm.loops.layers.uv.new() - else: - uv_layer = bm.loops.layers.uv.active + ifc_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") + representation = builder.get_representation(ifc_context, [item]) + ifcopenshell.api.geometry.assign_representation(ifc_file, element, representation) - min_x = min(v.co.x for v in bm.verts) - max_x = max(v.co.x for v in bm.verts) - min_y = min(v.co.y for v in bm.verts) - max_y = max(v.co.y for v in bm.verts) - - width = max_x - min_x - height = max_y - min_y - - for face in bm.faces: - for loop in face.loops: - vert = loop.vert - u = (vert.co.x - min_x) / width if width > 0 else 0.5 - v = (vert.co.y - min_y) / height if height > 0 else 0.5 - - u = max(0.0, min(1.0, u)) - v = max(0.0, min(1.0, v)) - loop[uv_layer].uv = (u, v) - - tool.Blender.apply_bmesh(mesh, bm) - - if self.use_existing_object_by_name: - obj = bpy.data.objects[self.use_existing_object_by_name] - bm_add_image_plane(obj.data) - bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="") - else: - temp_mesh = bpy.data.meshes.new("temp_mesh") - bm_add_image_plane(temp_mesh) - obj = bpy.data.objects.new(image_filepath.stem, temp_mesh) - tool.Drawing.run_root_assign_class( - obj=obj, - ifc_class="IfcAnnotation", - predefined_type="IMAGE", - should_add_representation=True, - context=ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW"), - ifc_representation_class=None, - ) - tool.Blender.remove_data_block(temp_mesh) - - element = tool.Ifc.get_entity(obj) - if element and isinstance(obj.data, bpy.types.Mesh): - representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") - if representation and representation.Items: - item_id = representation.Items[0].id() - num_faces = len(obj.data.polygons) - obj.data["ios_item_ids"] = [item_id] * num_faces - tool.Blender.Attribute.fill_attribute(obj.data, "ios_item_ids", "FACE", "INT", [item_id] * num_faces) - - for item in representation.Items: - if item.is_a("IfcPolygonalFaceSet") and item.Coordinates: - new_coords = [] - for vertex in obj.data.vertices: - co = obj.matrix_world @ vertex.co - new_coords.append([co.x, co.y, co.z]) - item.Coordinates.CoordList = new_coords - - tool.Blender.set_active_object(obj) - - material = bpy.data.materials.new(name=image_filepath.stem) - obj.data.materials.append(None) # new slot - obj.material_slots[0].material = material - bpy.ops.bim.add_style() - - style = tool.Ifc.get_entity(material) - assert style - tool.Style.assign_style_to_object(style, obj) + style = ifcopenshell.api.style.add_style(tool.Ifc.get(), name=image_filepath.stem) + ifcopenshell.api.style.assign_representation_styles( + ifc_file, shape_representation=representation, styles=[style] + ) # TODO: IfcSurfaceStyleRendering is unnecessary here, added it only because # we don't support IfcSurfaceStyleWithTextures without Rendering yet shading_attributes = { - "SurfaceColour": { - "Red": 1.0, - "Green": 1.0, - "Blue": 1.0, - }, + "SurfaceColour": {"Red": 1.0, "Green": 1.0, "Blue": 1.0}, "Transparency": 0.0, "ReflectanceMethod": "NOTDEFINED", } ifcopenshell.api.style.add_surface_style( - tool.Ifc.get(), - style=style, - ifc_class="IfcSurfaceStyleRendering", - attributes=shading_attributes, + tool.Ifc.get(), style=style, ifc_class="IfcSurfaceStyleRendering", attributes=shading_attributes ) - texture = ifc_file.create_entity("IfcImageTexture", Mode="DIFFUSE", URLReference=image_filepath.as_posix()) - textures = [texture] - ifc_file.create_entity("IfcTextureCoordinateGenerator", Maps=textures, Mode="COORD") # UV map - ifcopenshell.api.style.add_surface_style( - ifc_file, - style=style, - ifc_class="IfcSurfaceStyleWithTextures", - attributes={"Textures": textures}, - ) - tool.Style.reload_material_from_ifc(material) - tool.Geometry.record_object_materials(obj) - return {"FINISHED"} + if tool.Ifc.get_schema() == "IFC2X3": + texture = ifc_file.create_entity( + "IfcImageTexture", + RepeatS=True, + RepeatT=True, + TextureType="TEXTURE", + UrlReference=image_filepath.as_posix(), + ) + else: + texture = ifc_file.create_entity("IfcImageTexture", Mode="DIFFUSE", URLReference=image_filepath.as_posix()) + ifc_file.create_entity("IfcTextureCoordinateGenerator", Maps=[texture], Mode="COORD") + + textures = [texture] + ifcopenshell.api.style.add_surface_style( + ifc_file, style=style, ifc_class="IfcSurfaceStyleWithTextures", attributes={"Textures": textures} + ) + + logger = logging.getLogger("ImportIFC") + ifc_import_settings = bonsai.bim.import_ifc.IfcImportSettings.factory(bpy.context, None, logger) + ifc_importer = bonsai.bim.import_ifc.IfcImporter(ifc_import_settings) + ifc_importer.file = tool.Ifc.get() + ifc_importer.create_style(style) + + bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation) class ConvertSVGToDXF(bpy.types.Operator): diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py index b323b8d41e..cc597a4802 100644 --- a/src/bonsai/bonsai/bim/module/drawing/prop.py +++ b/src/bonsai/bonsai/bim/module/drawing/prop.py @@ -18,14 +18,11 @@ import enum import json -import os from collections.abc import Callable -from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, Union, get_args +from typing import TYPE_CHECKING, Any, Literal, Union import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.pset import ifcopenshell.util.element from bpy.props import ( @@ -34,7 +31,6 @@ from bpy.props import ( CollectionProperty, EnumProperty, FloatProperty, - FloatVectorProperty, IntProperty, PointerProperty, StringProperty, @@ -42,19 +38,17 @@ from bpy.props import ( from bpy.types import PropertyGroup from mathutils import Matrix -import bonsai.bim.module.drawing.annotation as annotation import bonsai.bim.module.drawing.decoration as decoration import bonsai.core.drawing as core import bonsai.tool as tool from bonsai.bim.module.drawing.data import ( AnnotationData, - DecoratorData, DrawingsData, ElementValuesData, SheetsData, ) from bonsai.bim.module.drawing.data import refresh as refresh_drawing_data -from bonsai.bim.prop import Attribute, BIMFilterGroup, StrProperty +from bonsai.bim.prop import Attribute, BIMFilterGroup diagram_scales_enum = [] diff --git a/src/bonsai/bonsai/bim/module/drawing/scheduler.py b/src/bonsai/bonsai/bim/module/drawing/scheduler.py index 3b120e8d99..a1610020c4 100644 --- a/src/bonsai/bonsai/bim/module/drawing/scheduler.py +++ b/src/bonsai/bonsai/bim/module/drawing/scheduler.py @@ -22,7 +22,6 @@ import string from pathlib import Path from textwrap import wrap -import bpy import openpyxl import openpyxl.cell # Unnecessary, bug in typeshed. import openpyxl.utils # Unnecessary, bug in typeshed. @@ -33,7 +32,6 @@ from odf.table import Table, TableCell, TableColumn, TableRow from odf.text import P import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.drawing.svgwriter import SvgWriter DEBUG = False diff --git a/src/bonsai/bonsai/bim/module/drawing/shaders.py b/src/bonsai/bonsai/bim/module/drawing/shaders.py index 2efd6ea285..774002fea3 100644 --- a/src/bonsai/bonsai/bim/module/drawing/shaders.py +++ b/src/bonsai/bonsai/bim/module/drawing/shaders.py @@ -335,12 +335,9 @@ class BaseLinesShader(BaseShader): TYPE = "LINES" - DEF_GLSL = ( - BaseShader.DEF_GLSL - + """ + DEF_GLSL = BaseShader.DEF_GLSL + """ #define GAP_SIZE {gap_size} """ - ) GEOM_GLSL = """ layout(lines) in; @@ -401,13 +398,10 @@ class DotsGizmoShader(GizmoShader): TYPE = "POINTS" - DEF_GLSL = ( - BaseShader.DEF_GLSL - + """ + DEF_GLSL = BaseShader.DEF_GLSL + """ #define CIRCLE_SEGMENTS 12 #define CIRCLE_RADIUS 8 """ - ) GEOM_GLSL = """ layout(points) in; diff --git a/src/bonsai/bonsai/bim/module/drawing/sheeter.py b/src/bonsai/bonsai/bim/module/drawing/sheeter.py index 7d8ebb67f4..df57b6efb5 100644 --- a/src/bonsai/bonsai/bim/module/drawing/sheeter.py +++ b/src/bonsai/bonsai/bim/module/drawing/sheeter.py @@ -26,7 +26,6 @@ import xml.etree.ElementTree as ET from pathlib import Path from xml.dom import minidom -import bpy import ifcopenshell.util.geolocation import pystache from mathutils import Vector diff --git a/src/bonsai/bonsai/bim/module/drawing/svgwriter.py b/src/bonsai/bonsai/bim/module/drawing/svgwriter.py index ef64fdefc6..9b2910b9a0 100644 --- a/src/bonsai/bonsai/bim/module/drawing/svgwriter.py +++ b/src/bonsai/bonsai/bim/module/drawing/svgwriter.py @@ -18,7 +18,6 @@ import math import os -import re import shutil import xml.etree.ElementTree as ET from collections.abc import Callable, Sequence @@ -30,10 +29,8 @@ import bmesh import bpy import ifcopenshell import ifcopenshell.util.element -import ifcopenshell.util.representation import ifcopenshell.util.selector import ifcopenshell.util.unit -import mathutils import svgwrite import svgwrite.container import svgwrite.text @@ -1026,7 +1023,8 @@ class SvgWriter: for text_literal in text_literals: text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product or element) - + if newline_at: + text = helper.add_newline_between_words(text, newline_at) text_segments = parse_markdown_it(text) if len(text_segments) == 1 and text_segments[0]["url"] is None and not text_segments[0].get("break", False): diff --git a/src/bonsai/bonsai/bim/module/drawing/ui.py b/src/bonsai/bonsai/bim/module/drawing/ui.py index 556459b8b6..1b6a11cd75 100644 --- a/src/bonsai/bonsai/bim/module/drawing/ui.py +++ b/src/bonsai/bonsai/bim/module/drawing/ui.py @@ -30,7 +30,6 @@ from bonsai.bim.module.drawing.data import ( DocumentsData, DrawingsData, ElementFiltersData, - ElementValuesData, ProductAssignmentsData, SheetsData, ) @@ -89,7 +88,7 @@ class BIM_PT_camera(Panel): for link in links: row = panel.row(align=True) split = row.split(factor=0.9) - split.label(text=link.name, icon="FILE") + split.label(text=link.filepath, icon="FILE") split.prop(link, "include_in_drawings", text="") else: panel.label(text="No IFC projects linked and loaded.") diff --git a/src/bonsai/bonsai/bim/module/fm/data.py b/src/bonsai/bonsai/bim/module/fm/data.py index 80a686e364..6d486fd9ce 100644 --- a/src/bonsai/bonsai/bim/module/fm/data.py +++ b/src/bonsai/bonsai/bim/module/fm/data.py @@ -16,8 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import importlib -import os import ifcfm diff --git a/src/bonsai/bonsai/bim/module/fm/operator.py b/src/bonsai/bonsai/bim/module/fm/operator.py index 629251282a..43e9bb50e7 100644 --- a/src/bonsai/bonsai/bim/module/fm/operator.py +++ b/src/bonsai/bonsai/bim/module/fm/operator.py @@ -16,10 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import json -import logging import os -import tempfile import bpy import ifcfm diff --git a/src/bonsai/bonsai/bim/module/fm/prop.py b/src/bonsai/bonsai/bim/module/fm/prop.py index 2eb1ae4739..cd6b038bc7 100644 --- a/src/bonsai/bonsai/bim/module/fm/prop.py +++ b/src/bonsai/bonsai/bim/module/fm/prop.py @@ -23,11 +23,7 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, PointerProperty, - StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/geometry/__init__.py b/src/bonsai/bonsai/bim/module/geometry/__init__.py index bc64947436..111b67b5b0 100644 --- a/src/bonsai/bonsai/bim/module/geometry/__init__.py +++ b/src/bonsai/bonsai/bim/module/geometry/__init__.py @@ -16,8 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import math - import bpy import ifcopenshell.util.element from bpy.app.handlers import persistent diff --git a/src/bonsai/bonsai/bim/module/geometry/decorator.py b/src/bonsai/bonsai/bim/module/geometry/decorator.py index c5a82c1d59..589b18ec84 100644 --- a/src/bonsai/bonsai/bim/module/geometry/decorator.py +++ b/src/bonsai/bonsai/bim/module/geometry/decorator.py @@ -19,7 +19,6 @@ from collections.abc import Sequence import blf -import bmesh import bpy import gpu import ifcopenshell diff --git a/src/bonsai/bonsai/bim/module/geometry/helper.py b/src/bonsai/bonsai/bim/module/geometry/helper.py index 511669d60d..3b14003435 100644 --- a/src/bonsai/bonsai/bim/module/geometry/helper.py +++ b/src/bonsai/bonsai/bim/module/geometry/helper.py @@ -16,22 +16,17 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -from math import pi, pow +from math import pi from typing import Any, Optional, TypeVar, Union import bmesh import bpy import ifcopenshell -import ifcopenshell.util.shape import ifcopenshell.util.unit import mathutils -import numpy as np -import shapely from ifcopenshell.util.shape_builder import ShapeBuilder from mathutils import Matrix, Vector, geometry -import bonsai.tool as tool - T = TypeVar("T") diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index fbd694d219..f8389cb3af 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import re from collections.abc import Sequence from time import time from typing import ( @@ -32,11 +31,9 @@ from typing import ( import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.boundary import ifcopenshell.api.drawing import ifcopenshell.api.geometry -import ifcopenshell.api.grid import ifcopenshell.api.group import ifcopenshell.api.layer import ifcopenshell.api.material @@ -61,10 +58,8 @@ import bonsai.core.geometry as core import bonsai.core.nest import bonsai.core.root import bonsai.core.spatial -import bonsai.core.style import bonsai.tool as tool from bonsai.bim.ifc import IfcStore -from bonsai.bim.module.model.data import AuthoringData from bonsai.bim.module.model.decorator import ProfileDecorator if TYPE_CHECKING: @@ -902,6 +897,7 @@ class OverrideDelete(bpy.types.Operator): if not is_valid_data_block: continue + element = tool.Ifc.get_entity(obj) if element: if tool.Geometry.is_locked(element): @@ -912,6 +908,9 @@ class OverrideDelete(bpy.types.Operator): if ifcopenshell.util.element.get_pset(element, "BBIM_Array"): self.report({"INFO"}, "Elements that are part of an array cannot be deleted.") continue + if element.is_a("IfcDocumentReference"): + self.report({"INFO"}, "Linked models cannot be deleted.") + continue if element.is_a("IfcGridAxis"): # Deleting the last W axis is OK if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or ( @@ -1208,6 +1207,11 @@ class OverrideDuplicateMove(bpy.types.Operator): operator.report({"ERROR"}, f"Drawing '{obj.name}' not duplicated.") continue + if element.is_a("IfcDocumentReference"): + objects_to_remove.add(obj) + operator.report({"ERROR"}, f"Linked model '{obj.name}' not duplicated.") + continue + if tool.Geometry.is_locked(element): objects_to_remove.add(obj) operator.report({"ERROR"}, lock_error_message(obj.name)) diff --git a/src/bonsai/bonsai/bim/module/geometry/prop.py b/src/bonsai/bonsai/bim/module/geometry/prop.py index 5336eef1c0..549c3bd227 100644 --- a/src/bonsai/bonsai/bim/module/geometry/prop.py +++ b/src/bonsai/bonsai/bim/module/geometry/prop.py @@ -24,8 +24,6 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, PointerProperty, StringProperty, @@ -34,7 +32,6 @@ from bpy.types import PropertyGroup import bonsai.tool as tool from bonsai.bim.module.geometry.data import RepresentationsData, ViewportData -from bonsai.bim.prop import Attribute, ObjProperty, StrProperty def get_contexts(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: diff --git a/src/bonsai/bonsai/bim/module/georeference/data.py b/src/bonsai/bonsai/bim/module/georeference/data.py index cf86314f9a..bc9e5b9ac4 100644 --- a/src/bonsai/bonsai/bim/module/georeference/data.py +++ b/src/bonsai/bonsai/bim/module/georeference/data.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . -import bpy import ifcopenshell.util.element import ifcopenshell.util.geolocation import ifcopenshell.util.schema diff --git a/src/bonsai/bonsai/bim/module/georeference/decorator.py b/src/bonsai/bonsai/bim/module/georeference/decorator.py index 6cece45cc8..05bce0e20b 100644 --- a/src/bonsai/bonsai/bim/module/georeference/decorator.py +++ b/src/bonsai/bonsai/bim/module/georeference/decorator.py @@ -19,10 +19,7 @@ from math import radians import blf -import bmesh -import bpy import gpu -import ifcopenshell import ifcopenshell.util.geolocation from bpy.types import SpaceView3D from bpy_extras.view3d_utils import location_3d_to_region_2d diff --git a/src/bonsai/bonsai/bim/module/georeference/operator.py b/src/bonsai/bonsai/bim/module/georeference/operator.py index 6c82e6684c..93a4e0ef24 100644 --- a/src/bonsai/bonsai/bim/module/georeference/operator.py +++ b/src/bonsai/bonsai/bim/module/georeference/operator.py @@ -21,7 +21,6 @@ from bpy_extras.io_utils import ImportHelper import bonsai.core.georeference as core import bonsai.tool as tool -from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator class AddGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): @@ -51,7 +50,7 @@ class RemoveGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): bl_description = "Remove the georeferencing" def _execute(self, context): - core.remove_georeferencing(tool.Ifc) + core.remove_georeferencing(tool.Ifc, tool.Georeference) class EditGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/bonsai/bonsai/bim/module/georeference/prop.py b/src/bonsai/bonsai/bim/module/georeference/prop.py index 5a1a37f06f..41035398e9 100644 --- a/src/bonsai/bonsai/bim/module/georeference/prop.py +++ b/src/bonsai/bonsai/bim/module/georeference/prop.py @@ -23,11 +23,7 @@ import ifcopenshell.util.geolocation from bpy.props import ( BoolProperty, CollectionProperty, - EnumProperty, FloatProperty, - FloatVectorProperty, - IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup @@ -224,16 +220,12 @@ class BIMGeoreferenceProperties(PropertyGroup): x_axis_ordinate: StringProperty(name="X Axis Ordinate", update=update_grid_north_vector) x_axis_is_null: BoolProperty(name="X Axis Is Null") - # These are only for reference to capture data about a host model from a linked model - # If you relink a model from a new host origin, we can autodetect it in theory with this - host_model_origin: StringProperty(name="Host Model Origin") - host_model_origin_si: StringProperty(name="Host Model Origin SI") - host_model_project_north: StringProperty(name="Host Model Angle to Grid North") - # This is the ENH in project units and SI units of the Blender session's 0,0,0. # These are only for reference, using tool.Georeference.set_model_origin on # project load, project create, and when linking for the first time from an # empty Blender session. + model_is_georeferenced: BoolProperty(name="Model Is Georeferenced") + model_crs: StringProperty(name="Model CRS") model_origin: StringProperty(name="Model Origin") model_origin_si: StringProperty(name="Model Origin SI") model_project_north: StringProperty(name="Model Angle to Grid North") @@ -275,10 +267,6 @@ class BIMGeoreferenceProperties(PropertyGroup): x_axis_ordinate: str x_axis_is_null: bool - host_model_origin: str - host_model_origin_si: str - host_model_project_north: str - model_origin: str model_origin_si: str model_project_north: str diff --git a/src/bonsai/bonsai/bim/module/group/data.py b/src/bonsai/bonsai/bim/module/group/data.py index c7be2ff393..fd5eba102d 100644 --- a/src/bonsai/bonsai/bim/module/group/data.py +++ b/src/bonsai/bonsai/bim/module/group/data.py @@ -17,9 +17,6 @@ # along with Bonsai. If not, see . import bpy -import ifcopenshell -import ifcopenshell.util.cost -import ifcopenshell.util.element import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/group/operator.py b/src/bonsai/bonsai/bim/module/group/operator.py index 66872ed90a..adea9ee48c 100644 --- a/src/bonsai/bonsai/bim/module/group/operator.py +++ b/src/bonsai/bonsai/bim/module/group/operator.py @@ -16,10 +16,9 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -from typing import TYPE_CHECKING, Literal, get_args +from typing import TYPE_CHECKING, get_args import bpy -import ifcopenshell.api import ifcopenshell.api.group import ifcopenshell.util.element diff --git a/src/bonsai/bonsai/bim/module/group/prop.py b/src/bonsai/bonsai/bim/module/group/prop.py index 48bff62e35..56cf7aae07 100644 --- a/src/bonsai/bonsai/bim/module/group/prop.py +++ b/src/bonsai/bonsai/bim/module/group/prop.py @@ -22,18 +22,14 @@ import bpy from bpy.props import ( BoolProperty, CollectionProperty, - EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup import bonsai.tool as tool from bonsai.bim.module.pset.data import refresh as refresh_pset -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute def update_active_group_index(self, context): diff --git a/src/bonsai/bonsai/bim/module/ifcgit/data.py b/src/bonsai/bonsai/bim/module/ifcgit/data.py index 9cb9f2655d..92da517a1c 100644 --- a/src/bonsai/bonsai/bim/module/ifcgit/data.py +++ b/src/bonsai/bonsai/bim/module/ifcgit/data.py @@ -1,8 +1,6 @@ import os import shutil -import bpy - # import tool import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/layer/data.py b/src/bonsai/bonsai/bim/module/layer/data.py index 4550d3257e..1ec9db8b39 100644 --- a/src/bonsai/bonsai/bim/module/layer/data.py +++ b/src/bonsai/bonsai/bim/module/layer/data.py @@ -19,7 +19,6 @@ from typing import Any import bpy -import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/layer/operator.py b/src/bonsai/bonsai/bim/module/layer/operator.py index 8475bebd54..4ff2a4fe0e 100644 --- a/src/bonsai/bonsai/bim/module/layer/operator.py +++ b/src/bonsai/bonsai/bim/module/layer/operator.py @@ -19,7 +19,6 @@ from typing import TYPE_CHECKING import bpy -import ifcopenshell.api import ifcopenshell.api.layer import ifcopenshell.util.element diff --git a/src/bonsai/bonsai/bim/module/layer/prop.py b/src/bonsai/bonsai/bim/module/layer/prop.py index 26a6c3680d..f91681af76 100644 --- a/src/bonsai/bonsai/bim/module/layer/prop.py +++ b/src/bonsai/bonsai/bim/module/layer/prop.py @@ -23,16 +23,13 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup import bonsai.tool as tool -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute def update_layer_property(self: "Layer", context: bpy.types.Context, *, property: str) -> None: diff --git a/src/bonsai/bonsai/bim/module/library/operator.py b/src/bonsai/bonsai/bim/module/library/operator.py index 6c7abe8be5..058b474732 100644 --- a/src/bonsai/bonsai/bim/module/library/operator.py +++ b/src/bonsai/bonsai/bim/module/library/operator.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . import bpy -import ifcopenshell.api import bonsai.core.library as core import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/library/prop.py b/src/bonsai/bonsai/bim/module/library/prop.py index 69647c195f..d7a9b51e1f 100644 --- a/src/bonsai/bonsai/bim/module/library/prop.py +++ b/src/bonsai/bonsai/bim/module/library/prop.py @@ -20,20 +20,16 @@ from typing import TYPE_CHECKING, Literal import bpy from bpy.props import ( - BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup import bonsai.tool as tool from bonsai.bim.module.library.data import LibrariesData -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute def update_active_reference_index(self, context): diff --git a/src/bonsai/bonsai/bim/module/library/ui.py b/src/bonsai/bonsai/bim/module/library/ui.py index f76e6a3251..32780f2797 100644 --- a/src/bonsai/bonsai/bim/module/library/ui.py +++ b/src/bonsai/bonsai/bim/module/library/ui.py @@ -28,7 +28,7 @@ import bonsai.tool as tool from bonsai.bim.module.library.data import LibrariesData, LibraryReferencesData if TYPE_CHECKING: - from bonsai.bim.module.library.prop import BIMLibraryProperties, LibraryReference + from bonsai.bim.module.library.prop import LibraryReference class BIM_PT_libraries(Panel): diff --git a/src/bonsai/bonsai/bim/module/light/__init__.py b/src/bonsai/bonsai/bim/module/light/__init__.py index 82e1187d4e..520d2623c4 100644 --- a/src/bonsai/bonsai/bim/module/light/__init__.py +++ b/src/bonsai/bonsai/bim/module/light/__init__.py @@ -16,10 +16,8 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import importlib import importlib.util import stat -import traceback from pathlib import Path import bpy diff --git a/src/bonsai/bonsai/bim/module/light/data.py b/src/bonsai/bonsai/bim/module/light/data.py index d708b6170f..d14835bb27 100644 --- a/src/bonsai/bonsai/bim/module/light/data.py +++ b/src/bonsai/bonsai/bim/module/light/data.py @@ -16,9 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import bpy import ifcopenshell.util.geolocation -from mathutils import Vector import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/light/decorator.py b/src/bonsai/bonsai/bim/module/light/decorator.py index 9cfb9ae186..f72a941ab8 100644 --- a/src/bonsai/bonsai/bim/module/light/decorator.py +++ b/src/bonsai/bonsai/bim/module/light/decorator.py @@ -16,10 +16,8 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -from math import degrees, radians import blf -import bmesh import bpy import gpu from bpy.types import SpaceView3D diff --git a/src/bonsai/bonsai/bim/module/material/data.py b/src/bonsai/bonsai/bim/module/material/data.py index 36c93add4f..543a44824b 100644 --- a/src/bonsai/bonsai/bim/module/material/data.py +++ b/src/bonsai/bonsai/bim/module/material/data.py @@ -16,14 +16,12 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import os from typing import Any, Union import bpy import ifcopenshell import ifcopenshell.util.doc import ifcopenshell.util.element -import ifcopenshell.util.schema from natsort import natsorted import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index 81ef17e6e4..e0761bf416 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -20,7 +20,6 @@ import json from typing import TYPE_CHECKING, Any, Literal, Union import bpy -import ifcopenshell.api import ifcopenshell.api.material import ifcopenshell.api.profile import ifcopenshell.api.style diff --git a/src/bonsai/bonsai/bim/module/material/prop.py b/src/bonsai/bonsai/bim/module/material/prop.py index 27567ab27e..9c8454f05e 100644 --- a/src/bonsai/bonsai/bim/module/material/prop.py +++ b/src/bonsai/bonsai/bim/module/material/prop.py @@ -19,25 +19,21 @@ from typing import TYPE_CHECKING import bpy -import ifcopenshell from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup -from ifcopenshell.util.doc import get_entity_doc import bonsai.tool as tool from bonsai.bim.module.classification.data import MaterialClassificationsData from bonsai.bim.module.material.data import MaterialsData, ObjectMaterialData from bonsai.bim.module.profile.data import ProfileData -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute def get_profile_classes(self, context): diff --git a/src/bonsai/bonsai/bim/module/material/ui.py b/src/bonsai/bonsai/bim/module/material/ui.py index 2ac1908327..f743306767 100644 --- a/src/bonsai/bonsai/bim/module/material/ui.py +++ b/src/bonsai/bonsai/bim/module/material/ui.py @@ -22,7 +22,6 @@ from typing import TYPE_CHECKING, Any import bpy import ifcopenshell.util.element -import ifcopenshell.util.unit from bpy.types import Panel, UIList import bonsai.bim.helper diff --git a/src/bonsai/bonsai/bim/module/misc/operator.py b/src/bonsai/bonsai/bim/module/misc/operator.py index 33703331f8..02cc56834b 100644 --- a/src/bonsai/bonsai/bim/module/misc/operator.py +++ b/src/bonsai/bonsai/bim/module/misc/operator.py @@ -19,12 +19,11 @@ from typing import TYPE_CHECKING, Literal, assert_never, get_args import bpy -import ifcopenshell import ifcopenshell.util.geolocation import ifcopenshell.util.placement import ifcopenshell.util.unit import numpy as np -from mathutils import Euler, Matrix, Vector +from mathutils import Matrix import bonsai.core.geometry as core_geometry import bonsai.core.misc as core diff --git a/src/bonsai/bonsai/bim/module/misc/prop.py b/src/bonsai/bonsai/bim/module/misc/prop.py index 889e1f5099..f596f22fc6 100644 --- a/src/bonsai/bonsai/bim/module/misc/prop.py +++ b/src/bonsai/bonsai/bim/module/misc/prop.py @@ -16,21 +16,12 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import bpy from bpy.props import ( - BoolProperty, - CollectionProperty, - EnumProperty, - FloatProperty, FloatVectorProperty, IntProperty, - PointerProperty, - StringProperty, ) from bpy.types import PropertyGroup -from bonsai.bim.prop import Attribute, StrProperty - class BIMMiscProperties(PropertyGroup): total_storeys: IntProperty( diff --git a/src/bonsai/bonsai/bim/module/model/array.py b/src/bonsai/bonsai/bim/module/model/array.py index 5f045435ac..dd54bf0ab3 100644 --- a/src/bonsai/bonsai/bim/module/model/array.py +++ b/src/bonsai/bonsai/bim/module/model/array.py @@ -19,12 +19,10 @@ import json import bpy -import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.pset import ifcopenshell.util.element import ifcopenshell.util.unit -from mathutils import Matrix, Vector +from mathutils import Matrix import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/model/covering.py b/src/bonsai/bonsai/bim/module/model/covering.py index 468554b9be..dd3889a7cb 100644 --- a/src/bonsai/bonsai/bim/module/model/covering.py +++ b/src/bonsai/bonsai/bim/module/model/covering.py @@ -18,8 +18,6 @@ import bpy -import ifcopenshell -import ifcopenshell.util.element import bonsai.core.covering as core import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/model/data.py b/src/bonsai/bonsai/bim/module/model/data.py index 0b97e05e77..10553f1bed 100644 --- a/src/bonsai/bonsai/bim/module/model/data.py +++ b/src/bonsai/bonsai/bim/module/model/data.py @@ -25,7 +25,7 @@ import bpy import ifcopenshell import ifcopenshell.util.element import ifcopenshell.util.schema -from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc +from ifcopenshell.util.doc import get_entity_doc from natsort import natsorted import bonsai.tool as tool @@ -424,12 +424,12 @@ class SverchokData: return tool.Model.get_modeling_bbim_pset_data(bpy.context.active_object, "BBIM_Sverchok") @classmethod - def has_sverchok(cls): + def has_sverchok(cls) -> bool: try: - import sverchok + import sverchok # noqa: F401 return True - except: + except ModuleNotFoundError: return False diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index 328804d4c6..d4dc1a218d 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -19,9 +19,8 @@ from __future__ import annotations import math -from itertools import chain from math import cos, radians, sin, tan -from typing import Any, Literal, Union +from typing import Any, Literal import blf import bmesh @@ -466,13 +465,14 @@ class PolylineDecorator: self.addon_prefs = tool.Blender.get_addon_preferences() self.font_id = 0 - font_size = tool.Blender.scale_font_size(12) + font_size = tool.Blender.scale_font_size() + offset = tool.Blender.scale_font_size() * 1.5 + line_height = tool.Blender.scale_font_size() * 1.25 blf.size(self.font_id, font_size) blf.enable(self.font_id, blf.SHADOW) blf.shadow(self.font_id, 6, 0, 0, 0, 1) color = self.addon_prefs.decorations_colour color_highlight = self.addon_prefs.decorator_color_special - offset = 20 new_line = 0 for i, (key, field_name) in enumerate(texts.items()): formatted_value = None @@ -480,7 +480,7 @@ class PolylineDecorator: # Controls which options are displayed in the UI if key not in self.input_ui.input_options: continue - new_line += 20 + new_line += line_height if self.tool_state and key != self.tool_state.input_type: formatted_value = self.input_ui.get_formatted_value(key) else: @@ -515,7 +515,7 @@ class PolylineDecorator: self.addon_prefs = tool.Blender.get_addon_preferences() self.font_id = 1 self.shader = gpu.shader.from_builtin("UNIFORM_COLOR") - font_size = tool.Blender.scale_font_size(12) + font_size = tool.Blender.scale_font_size() blf.size(self.font_id, font_size) blf.enable(self.font_id, blf.SHADOW) blf.shadow(self.font_id, 6, 0, 0, 0, 1) @@ -1076,7 +1076,7 @@ class ProductDecorator: data["verts"] = [] # Verts - polyline_vertices = [] + polyline_vertices: list[Vector] = [] polyline_props = tool.Model.get_polyline_props() polyline_data = polyline_props.insertion_polyline polyline_points = polyline_data[0].polyline_points if polyline_data else [] @@ -1196,7 +1196,7 @@ class ProductDecorator: data = {} data["verts"] = [] # Verts - polyline_vertices = [] + polyline_vertices: list[Vector] = [] polyline_props = tool.Model.get_polyline_props() polyline_data = polyline_props.insertion_polyline polyline_points = polyline_data[0].polyline_points if polyline_data else [] @@ -1936,7 +1936,7 @@ class BoundingBoxDecorator: addon_prefs = tool.Blender.get_addon_preferences() font_id = 0 - font_size = tool.Blender.scale_font_size(12) + font_size = tool.Blender.scale_font_size() blf.size(font_id, font_size) blf.enable(font_id, blf.SHADOW) blf.shadow(font_id, 6, 0, 0, 0, 1) diff --git a/src/bonsai/bonsai/bim/module/model/door.py b/src/bonsai/bonsai/bim/module/model/door.py index dbdf5e8dcf..5a14cde101 100644 --- a/src/bonsai/bonsai/bim/module/model/door.py +++ b/src/bonsai/bonsai/bim/module/model/door.py @@ -17,21 +17,18 @@ # along with Bonsai. If not, see . -import collections import collections.abc import json -from typing import TYPE_CHECKING, get_args +from typing import TYPE_CHECKING import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.geometry import ifcopenshell.api.material import ifcopenshell.api.pset import ifcopenshell.util.element import ifcopenshell.util.representation -import ifcopenshell.util.schema import ifcopenshell.util.unit from mathutils import Matrix, Vector diff --git a/src/bonsai/bonsai/bim/module/model/grid.py b/src/bonsai/bonsai/bim/module/model/grid.py index df4f140ebc..8ceaa605dc 100644 --- a/src/bonsai/bonsai/bim/module/model/grid.py +++ b/src/bonsai/bonsai/bim/module/model/grid.py @@ -19,7 +19,6 @@ from typing import TYPE_CHECKING import bpy -import ifcopenshell.api import ifcopenshell.api.grid from bpy.props import FloatProperty, IntProperty from bpy.types import Operator diff --git a/src/bonsai/bonsai/bim/module/model/handler.py b/src/bonsai/bonsai/bim/module/model/handler.py index 47075cdcfa..bfa064cda7 100644 --- a/src/bonsai/bonsai/bim/module/model/handler.py +++ b/src/bonsai/bonsai/bim/module/model/handler.py @@ -16,12 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import bpy -import ifcopenshell import ifcopenshell.api from bpy.app.handlers import persistent -from bonsai.bim.module.model import opening, product, profile, slab, task, wall +from bonsai.bim.module.model import opening, product, profile, task @persistent diff --git a/src/bonsai/bonsai/bim/module/model/mep.py b/src/bonsai/bonsai/bim/module/model/mep.py index cdb2538d28..245e10a21b 100644 --- a/src/bonsai/bonsai/bim/module/model/mep.py +++ b/src/bonsai/bonsai/bim/module/model/mep.py @@ -16,18 +16,13 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import collections import collections.abc import json -import math import re from copy import copy -from math import asin, cos, degrees, pi, radians, sin, tan +from math import cos, degrees, pi, radians, sin, tan -import bmesh import bpy -import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.geometry import ifcopenshell.api.material import ifcopenshell.api.pset @@ -41,9 +36,7 @@ import numpy as np from ifcopenshell.util.shape_builder import ShapeBuilder from mathutils import Matrix, Vector -import bonsai.core.geometry import bonsai.core.root -import bonsai.core.type import bonsai.tool as tool from bonsai.bim.module.model.profile import DumbProfileJoiner from bonsai.tool.cad import VTX_PRECISION diff --git a/src/bonsai/bonsai/bim/module/model/opening.py b/src/bonsai/bonsai/bim/module/model/opening.py index b0806ec33d..c20325c2e7 100644 --- a/src/bonsai/bonsai/bim/module/model/opening.py +++ b/src/bonsai/bonsai/bim/module/model/opening.py @@ -16,37 +16,29 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import json -import logging -from collections import defaultdict from collections.abc import Sequence -from math import pi, radians -from typing import Any, Optional, Union, cast +from math import radians +from typing import Any, Optional, Union import bmesh import bpy import gpu import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.feature import ifcopenshell.api.geometry import ifcopenshell.api.root import ifcopenshell.geom import ifcopenshell.util.element -import ifcopenshell.util.placement import ifcopenshell.util.representation import ifcopenshell.util.shape import ifcopenshell.util.shape_builder import ifcopenshell.util.unit import numpy as np import shapely -from bpy.props import FloatProperty from bpy.types import Operator, SpaceView3D -from bpy_extras.object_utils import AddObjectHelper, object_data_add from gpu_extras.batch import batch_for_shader -from mathutils import Euler, Matrix, Vector +from mathutils import Matrix, Vector -import bonsai.bim.import_ifc as import_ifc import bonsai.core.geometry import bonsai.tool as tool from bonsai.bim.module.drawing.decoration import DecoratorData diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index de169796f6..e33fe2cf5d 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -18,28 +18,13 @@ from __future__ import annotations -import copy -import math -from typing import Any, Literal, Optional, Union +from typing import Literal, Union -import bmesh import bpy import ifcopenshell -import ifcopenshell.api -import ifcopenshell.geom -import ifcopenshell.util.element -import ifcopenshell.util.placement -import ifcopenshell.util.representation -import ifcopenshell.util.type import ifcopenshell.util.unit -import mathutils.geometry -from lark import Lark, Transformer from mathutils import Vector -import bonsai.core.geometry -import bonsai.core.model as core -import bonsai.core.root -import bonsai.core.type import bonsai.tool as tool from bonsai.bim.module.model.decorator import PolylineDecorator @@ -211,22 +196,21 @@ class PolylineOperator: context.workspace.status_text_set(draw_instructions) def handle_lock_axis(self, context: bpy.types.Context, event: bpy.types.Event) -> None: + angle_snap = tool.Snap.get_angle_snap_value(context) if event.value == "PRESS" and event.type == "A": self.tool_state.lock_axis = False if self.tool_state.lock_axis else True if self.tool_state.lock_axis: self.tool_state.snap_angle = self.input_ui.get_number_value("WORLD_ANGLE") - # Round to the closest 5 - self.tool_state.snap_angle = round(self.tool_state.snap_angle / 5) * 5 + self.tool_state.snap_angle = round(self.tool_state.snap_angle / angle_snap) * angle_snap if event.shift and event.type in {"WHEELUPMOUSE", "WHEELDOWNMOUSE"}: self.tool_state.lock_axis = True self.tool_state.snap_angle = self.input_ui.get_number_value("WORLD_ANGLE") - # Round to the closest 5 - self.tool_state.snap_angle = round(self.tool_state.snap_angle / 5) * 5 + self.tool_state.snap_angle = round(self.tool_state.snap_angle / angle_snap) * angle_snap if event.type in {"WHEELUPMOUSE"}: - self.tool_state.snap_angle += 5 + self.tool_state.snap_angle += angle_snap else: - self.tool_state.snap_angle -= 5 + self.tool_state.snap_angle -= angle_snap self.handle_mouse_move(context, event) detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state) self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps) diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index 012cba4889..d7c96bce1d 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -19,12 +19,11 @@ # pyright: reportUnnecessaryTypeIgnoreComment=error import json -from typing import TYPE_CHECKING, Any, Literal, Optional, Union, assert_never, get_args +from typing import TYPE_CHECKING, Any, Literal, get_args import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.geometry import ifcopenshell.api.system import ifcopenshell.util.element @@ -34,8 +33,6 @@ import ifcopenshell.util.shape_builder import ifcopenshell.util.system import ifcopenshell.util.type import ifcopenshell.util.unit -import mathutils -import numpy as np from bpy_extras.object_utils import AddObjectHelper from mathutils import Matrix, Vector diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index 572898c484..f759d40a94 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -17,13 +17,11 @@ # along with Bonsai. If not, see . import copy -from math import atan2, degrees, pi +from math import atan2, degrees, pi, radians from typing import Any, Literal, Optional, Union -import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.geometry import ifcopenshell.api.pset import ifcopenshell.api.type @@ -38,7 +36,6 @@ from mathutils import Matrix, Vector import bonsai.core.geometry import bonsai.core.material import bonsai.core.root -import bonsai.core.type import bonsai.tool as tool from bonsai.bim.ifc import IfcStore from bonsai.bim.module.model.decorator import ( @@ -195,8 +192,8 @@ class DumbProfileGenerator: if should_round: # Round to nearest 50mm (yes, metric for now) self.length = 0.05 * round(length / 0.05) - # Round to nearest 5 degrees - nearest_degree = (pi / 180) * 5 + angle_snap = tool.Snap.get_angle_snap_value(bpy.context) + nearest_degree = radians(angle_snap) self.rotation = nearest_degree * round(self.rotation / nearest_degree) self.location = coords[0] data["obj"] = self.create_profile() diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index ec79a49196..cb246ac7c5 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -22,7 +22,6 @@ from math import pi, radians from typing import TYPE_CHECKING, Any, Literal, Optional, Union, get_args import bpy -import ifcopenshell import ifcopenshell.util.element from bpy.types import NodeTree, PropertyGroup from mathutils import Vector diff --git a/src/bonsai/bonsai/bim/module/model/railing.py b/src/bonsai/bonsai/bim/module/model/railing.py index 624b1b64a7..7ca66d8dbc 100644 --- a/src/bonsai/bonsai/bim/module/model/railing.py +++ b/src/bonsai/bonsai/bim/module/model/railing.py @@ -23,7 +23,6 @@ from typing import Any import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.geometry import ifcopenshell.api.pset import ifcopenshell.util.representation diff --git a/src/bonsai/bonsai/bim/module/model/roof.py b/src/bonsai/bonsai/bim/module/model/roof.py index 4038faa993..e1f7903299 100644 --- a/src/bonsai/bonsai/bim/module/model/roof.py +++ b/src/bonsai/bonsai/bim/module/model/roof.py @@ -23,14 +23,12 @@ from typing import Any, Literal, Union import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.pset import ifcopenshell.util.representation import ifcopenshell.util.unit -import mathutils.geometry import shapely from bpypolyskel import bpypolyskel -from mathutils import Matrix, Quaternion, Vector +from mathutils import Quaternion, Vector import bonsai.core.root import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index 602c36df11..58a353ab28 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -17,13 +17,10 @@ # along with Bonsai. If not, see . import json -from math import acos, cos, degrees, pi, sin -from typing import Optional +from math import cos, pi -import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.geometry import ifcopenshell.api.material import ifcopenshell.api.pset @@ -37,7 +34,6 @@ from mathutils import Matrix, Vector import bonsai.core.geometry import bonsai.core.root -import bonsai.core.type import bonsai.tool as tool from bonsai.bim.ifc import IfcStore from bonsai.bim.module.model.decorator import ( diff --git a/src/bonsai/bonsai/bim/module/model/stair.py b/src/bonsai/bonsai/bim/module/model/stair.py index 5b52308542..87152c645b 100644 --- a/src/bonsai/bonsai/bim/module/model/stair.py +++ b/src/bonsai/bonsai/bim/module/model/stair.py @@ -23,7 +23,6 @@ import bpy import ifcopenshell import ifcopenshell.api.pset import ifcopenshell.util.element -import ifcopenshell.util.representation import ifcopenshell.util.unit from mathutils import Matrix, Vector @@ -38,7 +37,7 @@ from bonsai.tool.numeric_input import ( ) V_ = tool.Blender.V_ -from typing import TYPE_CHECKING, get_args +from typing import TYPE_CHECKING from bmesh.types import BMVert from bpy.props import IntProperty diff --git a/src/bonsai/bonsai/bim/module/model/sverchok_modifier.py b/src/bonsai/bonsai/bim/module/model/sverchok_modifier.py index 0a318b1fb1..08f29890c4 100644 --- a/src/bonsai/bonsai/bim/module/model/sverchok_modifier.py +++ b/src/bonsai/bonsai/bim/module/model/sverchok_modifier.py @@ -22,7 +22,6 @@ import zipfile import bmesh import bpy -import ifcopenshell import ifcopenshell.api.pset import ifcopenshell.util.element from bpy_extras.io_utils import ExportHelper, ImportHelper diff --git a/src/bonsai/bonsai/bim/module/model/task.py b/src/bonsai/bonsai/bim/module/model/task.py index 93a361b73c..a6fe2607a2 100644 --- a/src/bonsai/bonsai/bim/module/model/task.py +++ b/src/bonsai/bonsai/bim/module/model/task.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.pset import ifcopenshell.util.date diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index d8a297433b..512eaf3fdf 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -22,11 +22,10 @@ from collections.abc import Iterable from typing import TYPE_CHECKING, Any import bpy -from bpy.types import Menu, Panel +from bpy.types import Panel import bonsai.bim import bonsai.tool as tool -from bonsai.bim import module from bonsai.bim.helper import prop_with_search from bonsai.bim.module.model.data import ( ArrayData, diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index ba3d93586b..441566e3e6 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -20,12 +20,11 @@ import copy import math -from math import acos, atan2, cos, degrees, pi, sin -from typing import TYPE_CHECKING, Any, Literal, Optional, Union, assert_never, get_args +from math import atan2, cos, degrees, pi, sin +from typing import TYPE_CHECKING, Any, Literal, Union, get_args import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.feature import ifcopenshell.api.geometry import ifcopenshell.api.material @@ -45,11 +44,9 @@ from mathutils import Matrix, Vector import bonsai.core.geometry import bonsai.core.model as core import bonsai.core.root -import bonsai.core.type import bonsai.tool as tool from bonsai.bim.ifc import IfcStore from bonsai.bim.module.model.decorator import PolylineDecorator, ProductDecorator -from bonsai.bim.module.model.opening import FilledOpeningGenerator from bonsai.bim.module.model.polyline import PolylineOperator @@ -916,8 +913,8 @@ class DumbWallGenerator: if should_round: # Round to nearest 50mm (yes, metric for now) self.length = 0.05 * round(length / 0.05) - # Round to nearest 5 degrees - nearest_degree = (math.pi / 180) * 5 + angle_snap = tool.Snap.get_angle_snap_value(bpy.context) + nearest_degree = math.radians(angle_snap) self.rotation = nearest_degree * round(self.rotation / nearest_degree) self.location = coords[0] data["obj"] = self.create_wall() diff --git a/src/bonsai/bonsai/bim/module/model/window.py b/src/bonsai/bonsai/bim/module/model/window.py index ad473a110e..30e8d767b5 100644 --- a/src/bonsai/bonsai/bim/module/model/window.py +++ b/src/bonsai/bonsai/bim/module/model/window.py @@ -17,21 +17,18 @@ # along with Bonsai. If not, see . -import collections import collections.abc import json -from typing import TYPE_CHECKING, get_args +from typing import TYPE_CHECKING import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.geometry import ifcopenshell.api.material import ifcopenshell.api.pset import ifcopenshell.util.element import ifcopenshell.util.representation -import ifcopenshell.util.shape_builder import ifcopenshell.util.unit from bmesh.types import BMVert from ifcopenshell.api.geometry.add_window_representation import DEFAULT_PANEL_SCHEMAS diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index 7b7a91906d..5f7cf7699d 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -17,15 +17,13 @@ # along with Bonsai. If not, see . import os -import sys from functools import partial -from typing import Any, Optional, Union +from typing import Optional, Union import bpy import bpy.utils.previews from bpy.types import Menu, WorkSpaceTool -import bonsai.bim import bonsai.core.model as core import bonsai.tool as tool from bonsai.bim.helper import draw_attribute, prop_with_search diff --git a/src/bonsai/bonsai/bim/module/nest/decorator.py b/src/bonsai/bonsai/bim/module/nest/decorator.py index e9630ebc26..66608b0171 100644 --- a/src/bonsai/bonsai/bim/module/nest/decorator.py +++ b/src/bonsai/bonsai/bim/module/nest/decorator.py @@ -19,7 +19,6 @@ import blf import bpy import gpu -import ifcopenshell import ifcopenshell.util.element from bpy.types import SpaceView3D from bpy_extras import view3d_utils @@ -27,7 +26,6 @@ from gpu_extras.batch import batch_for_shader from mathutils import Vector import bonsai.tool as tool -from bonsai.bim.module.geometry.decorator import ItemDecorator def transparent_color(color, alpha=0.1): diff --git a/src/bonsai/bonsai/bim/module/nest/operator.py b/src/bonsai/bonsai/bim/module/nest/operator.py index 9d7eb2df67..6961fea0c8 100644 --- a/src/bonsai/bonsai/bim/module/nest/operator.py +++ b/src/bonsai/bonsai/bim/module/nest/operator.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . import bpy -import ifcopenshell import ifcopenshell.util.element import bonsai.core.nest as core diff --git a/src/bonsai/bonsai/bim/module/nest/prop.py b/src/bonsai/bonsai/bim/module/nest/prop.py index c36388398c..4c7533455a 100644 --- a/src/bonsai/bonsai/bim/module/nest/prop.py +++ b/src/bonsai/bonsai/bim/module/nest/prop.py @@ -22,19 +22,12 @@ import bpy from bpy.props import ( BoolProperty, CollectionProperty, - EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, PointerProperty, - StringProperty, ) from bpy.types import PropertyGroup import bonsai.tool as tool from bonsai.bim.module.nest.decorator import NestDecorator, NestModeDecorator -from bonsai.bim.module.spatial.data import SpatialData -from bonsai.bim.prop import Attribute, StrProperty def update_relating_object(self: "BIMObjectNestProperties", context: bpy.types.Context) -> None: diff --git a/src/bonsai/bonsai/bim/module/owner/prop.py b/src/bonsai/bonsai/bim/module/owner/prop.py index dfe2b95ecd..171712e799 100644 --- a/src/bonsai/bonsai/bim/module/owner/prop.py +++ b/src/bonsai/bonsai/bim/module/owner/prop.py @@ -20,14 +20,9 @@ from typing import TYPE_CHECKING, Literal import bpy from bpy.props import ( - BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, - StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/patch/operator.py b/src/bonsai/bonsai/bim/module/patch/operator.py index 57413214d6..99459b99e9 100644 --- a/src/bonsai/bonsai/bim/module/patch/operator.py +++ b/src/bonsai/bonsai/bim/module/patch/operator.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . import json -import os from pathlib import Path from typing import TYPE_CHECKING, cast diff --git a/src/bonsai/bonsai/bim/module/patch/prop.py b/src/bonsai/bonsai/bim/module/patch/prop.py index 9382b4e786..e14bb3b1ef 100644 --- a/src/bonsai/bonsai/bim/module/patch/prop.py +++ b/src/bonsai/bonsai/bim/module/patch/prop.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import importlib import importlib.util from pathlib import Path from typing import TYPE_CHECKING, Literal, Union @@ -27,15 +26,11 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute ifcpatchrecipes_enum: list[tuple[str, str, str]] = [] diff --git a/src/bonsai/bonsai/bim/module/profile/data.py b/src/bonsai/bonsai/bim/module/profile/data.py index 34ea4d1b7b..09f8eaf383 100644 --- a/src/bonsai/bonsai/bim/module/profile/data.py +++ b/src/bonsai/bonsai/bim/module/profile/data.py @@ -19,7 +19,6 @@ from typing import Any import bpy -import bpy.utils import bpy.utils.previews import ifcopenshell.util.doc diff --git a/src/bonsai/bonsai/bim/module/profile/operator.py b/src/bonsai/bonsai/bim/module/profile/operator.py index 871d8f3b86..42a37989a0 100644 --- a/src/bonsai/bonsai/bim/module/profile/operator.py +++ b/src/bonsai/bonsai/bim/module/profile/operator.py @@ -23,7 +23,6 @@ import ifcopenshell.util.element import bonsai.bim.helper import bonsai.bim.module.model.profile as model_profile -import bonsai.core.profile as core import bonsai.tool as tool from bonsai.bim.module.geometry.helper import Helper from bonsai.bim.module.model.decorator import ProfileDecorator diff --git a/src/bonsai/bonsai/bim/module/profile/prop.py b/src/bonsai/bonsai/bim/module/profile/prop.py index 61e2546193..3a4a71650e 100644 --- a/src/bonsai/bonsai/bim/module/profile/prop.py +++ b/src/bonsai/bonsai/bim/module/profile/prop.py @@ -19,15 +19,10 @@ from typing import TYPE_CHECKING, Union import bpy -import ifcopenshell -import ifcopenshell.util.attribute -import ifcopenshell.util.schema from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, PointerProperty, StringProperty, @@ -36,7 +31,7 @@ from bpy.types import PropertyGroup import bonsai.tool as tool from bonsai.bim.module.profile.data import ProfileData -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute def get_profile_classes(self: "BIMProfileProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: diff --git a/src/bonsai/bonsai/bim/module/project/__init__.py b/src/bonsai/bonsai/bim/module/project/__init__.py index 53ebaff14e..db642c18c2 100644 --- a/src/bonsai/bonsai/bim/module/project/__init__.py +++ b/src/bonsai/bonsai/bim/module/project/__init__.py @@ -31,27 +31,32 @@ classes = ( operator.BIM_OT_load_clipping_planes, operator.BIM_OT_save_clipping_planes, operator.ChangeLibraryElement, + operator.ClearMeasurement, operator.ClearRecentIFCProjects, operator.CreateClippingPlane, operator.CreateProject, operator.DisableCulling, operator.DisableEditingHeader, + operator.DisableEditingLink, operator.EditHeader, + operator.EditLink, operator.EditProjectLibrary, operator.EnableCulling, operator.EnableEditingHeader, + operator.EnableEditingLink, operator.ExportIFC, operator.FlipClippingPlane, + operator.HideQueriedLinkedElement, operator.IFCFileHandlerOperator, operator.ImageScalingTool, operator.LinkIfc, + operator.LoadBlendMetadataAndIFC, operator.LoadLink, operator.LoadLinkedProject, operator.LoadProject, operator.LoadProjectElements, - operator.MeasureTool, operator.MeasureFaceAreaTool, - operator.ClearMeasurement, + operator.MeasureTool, operator.NewProject, operator.QueryLinkedElement, operator.RefreshClippingPlanes, @@ -62,6 +67,7 @@ classes = ( operator.RewindLibrary, operator.SaveLibraryFile, operator.SelectLibraryFile, + operator.SelectLinkedModelElement, operator.SelectLinkHandle, operator.ToggleFilterCategories, operator.ToggleLinkSelectability, @@ -69,7 +75,6 @@ classes = ( operator.UnassignLibraryDeclaration, operator.UnlinkIfc, operator.UnloadLink, - operator.LoadBlendMetadataAndIFC, workspace.ExploreHotkey, prop.LibraryBreadcrumb, prop.LibraryElement, diff --git a/src/bonsai/bonsai/bim/module/project/data.py b/src/bonsai/bonsai/bim/module/project/data.py index e2e960b382..8ecf30eef5 100644 --- a/src/bonsai/bonsai/bim/module/project/data.py +++ b/src/bonsai/bonsai/bim/module/project/data.py @@ -16,12 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import os from collections import defaultdict from pathlib import Path from typing import Any, Union -import bpy import ifcopenshell.util.file import bonsai.tool as tool @@ -172,6 +170,6 @@ class ProjectLibraryData: class LinksData: - linked_data = {} + linked_data: dict[str, Any] = {} enable_culling = False is_loaded = False diff --git a/src/bonsai/bonsai/bim/module/project/decorator.py b/src/bonsai/bonsai/bim/module/project/decorator.py index 14035ca91c..72090a769b 100644 --- a/src/bonsai/bonsai/bim/module/project/decorator.py +++ b/src/bonsai/bonsai/bim/module/project/decorator.py @@ -16,15 +16,11 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -from typing import Union - -import blf import bmesh import bpy import gpu from bpy.app.handlers import persistent from bpy.types import SpaceView3D -from bpy_extras import view3d_utils from gpu_extras.batch import batch_for_shader from mathutils import Vector @@ -56,7 +52,7 @@ class ProjectDecorator: installed = None @classmethod - def install(cls, context): + def install(cls, context: bpy.types.Context) -> None: if cls.installed: cls.uninstall() handler = cls() @@ -101,26 +97,20 @@ class ProjectDecorator: # general shader self.shader = gpu.shader.from_builtin("UNIFORM_COLOR") - selected_vertices = [] - selected_edges = [] - selected_tris = [] - props = tool.Project.get_project_props() - try: - obj = props.queried_obj - selected_vertices = obj["selected_vertices"] - selected_edges = obj["selected_edges"] - selected_tris = obj["selected_tris"] - except: + obj = props.queried_obj + if obj is None: return + geom = tool.Project.Link.get_selected_geometry(obj) + selected_vertices = geom.selected_vertices - root_obj: Union[bpy.types.Object, None] = props.queried_obj_root + root_obj = props.queried_obj_root if root_obj and not (m := root_obj.matrix_world).is_identity: selected_vertices = [m @ Vector(v) for v in selected_vertices] - if selected_edges: - self.draw_batch("LINES", selected_vertices, selected_elements_color, selected_edges) - self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), selected_tris) + if geom.selected_edges: + self.draw_batch("LINES", selected_vertices, selected_elements_color, geom.selected_edges) + self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), geom.selected_tris) class ClippingPlaneDecorator: diff --git a/src/bonsai/bonsai/bim/module/project/gizmo.py b/src/bonsai/bonsai/bim/module/project/gizmo.py index d91668eaa8..fa8edb8c7e 100644 --- a/src/bonsai/bonsai/bim/module/project/gizmo.py +++ b/src/bonsai/bonsai/bim/module/project/gizmo.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . -import bpy from bpy.types import GizmoGroup from mathutils import Matrix diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index b6f63f13ca..a0768e0cf5 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -31,8 +31,8 @@ from typing import TYPE_CHECKING, Literal, Union, get_args import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.attribute +import ifcopenshell.api.document import ifcopenshell.api.nest import ifcopenshell.api.project import ifcopenshell.api.root @@ -49,12 +49,14 @@ import ifcopenshell.util.unit import numpy as np from bpy.app.handlers import persistent from bpy_extras.io_utils import ExportHelper, ImportHelper -from ifcopenshell.geom import ShapeElementType -from mathutils import Matrix, Vector +from bpy_extras.view3d_utils import ( + region_2d_to_origin_3d, + region_2d_to_vector_3d, +) +from mathutils import Vector import bonsai.bim.handler import bonsai.bim.helper -import bonsai.bim.schema import bonsai.core.project as core import bonsai.tool as tool from bonsai.bim import export_ifc, import_ifc @@ -1321,7 +1323,7 @@ class ToggleFilterCategories(bpy.types.Operator): return {"FINISHED"} -class LinkIfc(bpy.types.Operator, ImportHelper): +class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator): bl_idname = "bim.link_ifc" bl_label = "Link IFC" bl_options = {"REGISTER", "UNDO"} @@ -1360,134 +1362,131 @@ class LinkIfc(bpy.types.Operator, ImportHelper): row = self.layout.row() row.prop(pprops, "project_north") - def execute(self, context): + def _execute(self, context): start = time.time() files = [f.name for f in self.files] if self.files else [self.filepath] + + if not files or all(not f or not f.strip() for f in files): + self.report({"ERROR"}, "No file selected") + return {"CANCELLED"} + + existing_links = tool.Project.get_linked_models_documents() if tool.Ifc.get() else {} for filename in files: + if not filename or not filename.strip(): + continue filepath = Path(self.directory) / filename if bpy.data.filepath and filepath.samefile(bpy.data.filepath): self.report({"INFO"}, "Can't link the current .blend file") continue props = tool.Project.get_project_props() - new = props.links.add() filepath = tool.Ifc.get_uri(filepath, use_relative_path=self.use_relative_path) + + new = props.links.add() + if tool.Ifc.get(): + if not (document := existing_links.get(filepath)): + document = ifcopenshell.api.document.add_information(tool.Ifc.get()) + document.Name = Path(filepath).name + document.Scope = "LINKED_MODEL" + reference = ifcopenshell.api.document.add_reference(tool.Ifc.get(), information=document) + reference[1] = ",".join([str(o) for o in np.eye(4).flatten().tolist()]) + reference.Location = filepath.replace("\\", "/") + new.ifc_definition_id = reference.id() new.name = filepath - status = bpy.ops.bim.load_link(filepath=filepath, use_cache=self.use_cache) - if status == {"CANCELLED"}: - error_msg = ( - f'Error processing IFC file "{filepath}" ' - "was critical and blend file either wasn't saved or wasn't updated. " - "See logs above in system console for details." - ) - print(error_msg) - self.report({"ERROR"}, error_msg) - return {"FINISHED"} - print(f"Finished linking {len(files)} IFCs", time.time() - start) - return {"FINISHED"} + new.filepath = filepath + bpy.ops.bim.load_link(link_index=-1, use_cache=self.use_cache) -class UnlinkIfc(bpy.types.Operator): +class UnlinkIfc(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.unlink_ifc" bl_label = "Unlink IFC" bl_options = {"REGISTER", "UNDO"} bl_description = "Remove the selected file from the link list" - filepath: bpy.props.StringProperty() + link_index: bpy.props.IntProperty(name="Link Index") - def execute(self, context): - filepath = Path(self.filepath).as_posix() - bpy.ops.bim.unload_link(filepath=filepath) + def _execute(self, context): props = tool.Project.get_project_props() - index = props.links.find(filepath) - if index != -1: - props.links.remove(index) - return {"FINISHED"} + link = props.links[self.link_index] + bpy.ops.bim.unload_link(link_index=self.link_index) + if tool.Ifc.get(): + reference = tool.Ifc.get().by_id(link.ifc_definition_id) + document = tool.Document.get_reference_document(reference) + ifcopenshell.api.document.remove_reference(tool.Ifc.get(), reference) + if document and not tool.Document.get_document_references(document): + ifcopenshell.api.document.remove_information(tool.Ifc.get(), document) + props.links.remove(self.link_index) -class UnloadLink(bpy.types.Operator): +class UnloadLink(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.unload_link" bl_label = "Unload Link" bl_options = {"REGISTER", "UNDO"} bl_description = "Unload the selected linked file" - filepath: bpy.props.StringProperty() + link_index: bpy.props.IntProperty(name="Link Index") - def execute(self, context): - filepath = tool.Blender.ensure_blender_path_is_abs(Path(self.filepath)) - if filepath.suffix.lower() == ".ifc": - filepath = filepath.with_suffix(".ifc.cache.blend") - - for library in list(bpy.data.libraries): - if tool.Blender.ensure_blender_path_is_abs(Path(library.filepath)) == filepath: + def _execute(self, context): + link = tool.Project.get_project_props().links[self.link_index] + if obj := tool.Project.get_link_empty_handle(link): + collection = obj.instance_collection + library = collection.library + tool.Ifc.unlink(obj=obj) + bpy.data.objects.remove(obj) + if collection.users == 0: + bpy.data.collections.remove(collection) + if not len([c for c in bpy.data.collections if c.library == library]): bpy.data.libraries.remove(library) - - props = tool.Project.get_project_props() - links = props.links - link = links[self.filepath] - # Let's assume that user might delete it. - if empty_handle := link.empty_handle: - bpy.data.objects.remove(empty_handle) - - # following lines removes the library also when use_relative_path=True, otherwise it doesn't - libraries = bpy.data.libraries - for library in libraries: - if library.name == self.filepath + ".cache.blend": - bpy.data.libraries.remove(library) - link.is_loaded = False - - if not any([l.is_loaded for l in links]): - ProjectDecorator.uninstall() - # we make sure we don't draw queried object from the file that was just unlinked - elif queried_obj := props.queried_obj: - queried_filepath = Path(queried_obj["ifc_filepath"]) - if queried_filepath == filepath: - ProjectDecorator.uninstall() - - return {"FINISHED"} + ProjectDecorator.uninstall() -class LoadLink(bpy.types.Operator): +class LoadLink(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.load_link" bl_label = "Load Link" bl_options = {"REGISTER", "UNDO"} bl_description = "Load the selected file" - filepath: bpy.props.StringProperty(name="Link Filepath") - use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) - filepath_: Path + link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration] + use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) # pyright: ignore[reportRedeclaration] - def execute(self, context): - filepath = Path(tool.Ifc.resolve_uri(self.filepath)) + if TYPE_CHECKING: + link_index: int + use_cache: bool + + def _execute(self, context): + self.link = tool.Project.get_project_props().links[self.link_index] + filepath = Path(tool.Ifc.resolve_uri(self.link.filepath)) if not filepath.exists(): self.report({"ERROR"}, f"File does not exist: '{filepath}'") return {"CANCELLED"} self.filepath_ = filepath - if filepath.suffix.lower().endswith(".blend"): - self.link_blend(filepath) - elif filepath.suffix.lower().endswith(".ifc"): - status = self.link_ifc() - if status: - return status - return {"FINISHED"} + if filepath.suffix.lower().endswith(".ifc"): + return self.link_ifc() def link_blend(self, filepath: Path) -> None: with bpy.data.libraries.load(str(filepath), link=True) as (data_from, data_to): - data_to.scenes = data_from.scenes - link = tool.Project.get_project_props().links[self.filepath] - for scene in bpy.data.scenes: - if not scene.library or Path(scene.library.filepath) != filepath: + data_to.collections = [c for c in data_from.collections if "IfcProject" in c] + + # Find the linked collection + for collection in bpy.data.collections: + if not collection.library or Path(collection.library.filepath) != filepath: continue - for child in scene.collection.children: - if "IfcProject" not in child.name: - continue - empty = bpy.data.objects.new(child.name, None) - empty.instance_type = "COLLECTION" - empty.instance_collection = child - link.empty_handle = empty - bpy.context.scene.collection.objects.link(empty) - break + # Create unique empty instance for this link + empty_name = collection.name + empty = bpy.data.objects.new(empty_name, None) + empty.instance_type = "COLLECTION" + empty.instance_collection = collection + empty.matrix_world = tool.Project.calculate_link_matrix(self.link) + + tool.Project.set_link_empty_handle(self.link, empty) + assert bpy.context.scene + bpy.context.scene.collection.objects.link(empty) + self.link.is_loaded = True + if tool.Ifc.get(): # For non-IFC projects, locking has no meaning + tool.Geometry.lock_object(empty) + tool.Blender.select_and_activate_single_object(bpy.context, empty) break - link.is_loaded = True - tool.Blender.select_and_activate_single_object(bpy.context, empty) + else: + print(f"WARNING: No IfcProject collection found in {filepath}") + self.link.is_loaded = False def link_ifc(self) -> Union[set[str], None]: blend_filepath = self.filepath_.with_suffix(".ifc.cache.blend") @@ -1502,14 +1501,12 @@ class LoadLink(bpy.types.Operator): code = f""" import bpy +import sys def run(): import bonsai.tool as tool gprops = tool.Georeference.get_georeference_props() # Our model origin becomes their host model origin - gprops.host_model_origin = "{gprops.model_origin}" - gprops.host_model_origin_si = "{gprops.model_origin_si}" - gprops.host_model_project_north = "{gprops.model_project_north}" gprops.has_blender_offset = {gprops.has_blender_offset} gprops.blender_offset_x = "{gprops.blender_offset_x}" gprops.blender_offset_y = "{gprops.blender_offset_y}" @@ -1522,7 +1519,12 @@ def run(): pprops.false_origin = "{pprops.false_origin}" pprops.project_north = "{pprops.project_north}" # Use absolute path to be safe from cwd changes. - bpy.ops.bim.load_linked_project(filepath=r"{str(self.filepath_)}") + try: + bpy.ops.bim.load_linked_project(filepath=r"{str(self.filepath_)}") + except RuntimeError as e: + # Operator failed (returned CANCELLED with error report) + print(f"Failed to load linked project: {{e}}") + sys.exit(1) # Use str instead of as_posix to avoid issues with Windows shared paths. bpy.ops.wm.save_as_mainfile(filepath=r"{str(blend_filepath)}") @@ -1556,14 +1558,17 @@ except Exception as e: if not blend_filepath.exists() or blend_filepath.stat().st_mtime < t: return {"CANCELLED"} - self.set_model_origin_from_link() - + self.set_model_origin_from_link() + self.set_georeferencing_indicator() self.link_blend(blend_filepath) def set_model_origin_from_link(self) -> None: if tool.Ifc.get(): return # The current model's coordinates always take priority. + if len(tool.Project.get_project_props().links) > 1: + return # Only the first link sets the origin + json_filepath = self.filepath_.with_suffix(".ifc.cache.json") if not json_filepath.exists(): return @@ -1576,21 +1581,36 @@ except Exception as e: if (value := data.get(prop, None)) is not None: setattr(gprops, prop, value) + def set_georeferencing_indicator(self) -> None: + if not tool.Ifc.get(): + self.link.georeferenced = "NONE" + return + if not (crs_name := (ifcopenshell.util.geolocation.get_crs(tool.Ifc.get()) or {}).get("Name", "")): + self.link.georeferenced = "NONE" + return + reference = tool.Ifc.get().by_id(self.link.ifc_definition_id) + json_filepath = Path(reference.Location).with_suffix(".ifc.cache.json") + if not json_filepath.exists(): + self.link.georeferenced = "NONE" + return + with open(json_filepath, "r") as f: + data = json.load(f) + if not data["model_is_georeferenced"]: + self.link.georeferenced = "NONE" + else: + self.link.georeferenced = "FULL_COMPATIBLE" if crs_name == data["model_crs"] else "NOT_COMPATIBLE" + class ReloadLink(bpy.types.Operator): bl_idname = "bim.reload_link" bl_label = "Reload Link" bl_options = {"REGISTER", "UNDO"} bl_description = "Reload the selected file" - filepath: bpy.props.StringProperty() + link_index: bpy.props.IntProperty(name="Link Index") def execute(self, context): - is_abs = os.path.isabs(Path(self.filepath)) - use_relative_path = not is_abs - bpy.ops.bim.unlink_ifc(filepath=self.filepath) - filepath = tool.Ifc.resolve_uri(self.filepath) - status = bpy.ops.bim.link_ifc(filepath=filepath, use_cache=False, use_relative_path=use_relative_path) - return {"FINISHED"} + bpy.ops.bim.unload_link(link_index=self.link_index) + return bpy.ops.bim.load_link(link_index=self.link_index, use_cache=False) or {"FINISHED"} class ToggleLinkSelectability(bpy.types.Operator): @@ -1598,16 +1618,18 @@ class ToggleLinkSelectability(bpy.types.Operator): bl_label = "Toggle Link Selectability" bl_options = {"REGISTER", "UNDO"} bl_description = "Toggle selectability" - link: bpy.props.StringProperty(name="Linked IFC Filepath") + link_index: bpy.props.IntProperty(name="Link Index") def execute(self, context): props = tool.Project.get_project_props() - link = props.links[self.link] - self.library_filepath = tool.Blender.ensure_blender_path_is_abs(Path(self.link).with_suffix(".ifc.cache.blend")) + link = props.links[self.link_index] + self.library_filepath = tool.Blender.ensure_blender_path_is_abs( + Path(link.filepath).with_suffix(".ifc.cache.blend") + ) link.is_selectable = (is_selectable := not link.is_selectable) for collection in self.get_linked_collections(): collection.hide_select = not is_selectable - if handle := link.empty_handle: + if handle := tool.Project.get_link_empty_handle(link): handle.hide_select = not is_selectable return {"FINISHED"} @@ -1624,13 +1646,23 @@ class ToggleLinkVisibility(bpy.types.Operator): bl_label = "Toggle Link Visibility" bl_options = {"REGISTER", "UNDO"} bl_description = "Toggle visibility between SOLID and WIREFRAME" - link: bpy.props.StringProperty(name="Linked IFC Filepath") - mode: bpy.props.EnumProperty(name="Visibility Mode", items=((i, i, "") for i in ("WIREFRAME", "VISIBLE"))) + + link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration] + mode: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + name="Visibility Mode", + items=((i, i, "") for i in ("WIREFRAME", "VISIBLE")), + ) + + if TYPE_CHECKING: + link_index: int + mode: Literal["WIREFRAME", "VISIBLE"] def execute(self, context): props = tool.Project.get_project_props() - link = props.links[self.link] - self.library_filepath = tool.Blender.ensure_blender_path_is_abs(Path(self.link).with_suffix(".ifc.cache.blend")) + link = props.links[self.link_index] + self.library_filepath = tool.Blender.ensure_blender_path_is_abs( + Path(link.filepath).with_suffix(".ifc.cache.blend") + ) if self.mode == "WIREFRAME": self.toggle_wireframe(link) elif self.mode == "VISIBLE": @@ -1638,12 +1670,16 @@ class ToggleLinkVisibility(bpy.types.Operator): return {"FINISHED"} def toggle_wireframe(self, link: "Link") -> None: + linked_collections = self.get_linked_collections() + link.is_wireframe = not link.is_wireframe display_type = "WIRE" if link.is_wireframe else "TEXTURED" - for collection in self.get_linked_collections(): + for collection in linked_collections: objs = filter(lambda obj: "IfcOpeningElement" not in obj.name, collection.all_objects) for obj in objs: obj.display_type = display_type + if handle := tool.Project.get_link_empty_handle(link): + handle.display_type = display_type def toggle_visibility(self, link: "Link") -> None: linked_collections = self.get_linked_collections() @@ -1652,7 +1688,7 @@ class ToggleLinkVisibility(bpy.types.Operator): layer_collections = tool.Blender.get_layer_collections_mapping(linked_collections) for layer_collection in layer_collections.values(): layer_collection.exclude = is_hidden - if handle := link.empty_handle: + if handle := tool.Project.get_link_empty_handle(link): handle.hide_set(is_hidden) def get_linked_collections(self) -> list[bpy.types.Collection]: @@ -1663,17 +1699,101 @@ class ToggleLinkVisibility(bpy.types.Operator): ] +class EnableEditingLink(bpy.types.Operator): + bl_idname = "bim.enable_editing_link" + bl_label = "Enable Editing Link" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Enable editing link location" + + def execute(self, context): + link = tool.Project.get_project_props().active_link + assert link + link.is_editing = True + obj = tool.Project.get_link_empty_handle(link) + assert obj + tool.Geometry.unlock_object(obj) + return {"FINISHED"} + + +class DisableEditingLink(bpy.types.Operator): + bl_idname = "bim.disable_editing_link" + bl_label = "Disable Editing Link" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Disable editing link and restore to previously saved location" + + def execute(self, context): + link = tool.Project.get_project_props().active_link + assert link + link.is_editing = False + obj = tool.Project.get_link_empty_handle(link) + assert obj + obj.matrix_world = tool.Project.calculate_link_matrix(link) + tool.Geometry.lock_object(obj) + return {"FINISHED"} + + +class EditLink(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.edit_link" + bl_label = "Edit Link" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Disable editing link and restore to previously saved location" + + def _execute(self, context): + link = tool.Project.get_project_props().active_link + assert link + link.is_editing = False + obj = tool.Project.get_link_empty_handle(link) + assert obj + new_obj_matrix = obj.matrix_world + + filepath = Path(tool.Ifc.resolve_uri(link.filepath)) + with open(filepath.with_suffix(".ifc.cache.json"), "r") as f: + metadata = json.load(f) + + rot = ifcopenshell.util.shape_builder.np_rotation_matrix( + radians(-float(metadata["model_project_north"])), 4, "Z" + ) + global_matrix = rot @ np.eye(4) + global_matrix[:, 3][:3] = [float(o) for o in metadata["model_origin_si"].split(",")] + + gprops = tool.Georeference.get_georeference_props() + rot = ifcopenshell.util.shape_builder.np_rotation_matrix(radians(-float(gprops.model_project_north)), 4, "Z") + local_matrix = rot @ np.eye(4) + local_matrix[:, 3][:3] = [float(o) for o in gprops.model_origin_si.split(",")] + + # obj_matrix is typically calculated as: + # obj_matrix = np.linalg.inv(local_matrix) @ transformation @ global_matrix + identity_blender_matrix = np.linalg.inv(local_matrix) @ global_matrix + if np.allclose(np.array(new_obj_matrix), identity_blender_matrix, atol=1e-5): + link.has_transformation = False + transformation = ",".join(map(str, np.eye(4).reshape(-1))) + else: + transformed_global_matrix = local_matrix @ np.array(new_obj_matrix) + transformation = transformed_global_matrix @ np.linalg.inv(global_matrix) + link.has_transformation = True + transformation = ",".join(map(str, transformation.reshape(-1))) + + if tool.Ifc.get(): + reference = tool.Ifc.get().by_id(link.ifc_definition_id) + reference[1] = transformation + else: + link.transformation = transformation + + obj.matrix_world = tool.Project.calculate_link_matrix(link) + tool.Geometry.lock_object(obj) + + class SelectLinkHandle(bpy.types.Operator): bl_idname = "bim.select_link_handle" bl_label = "Select Link Handle" bl_options = {"REGISTER", "UNDO"} bl_description = "Select link empty object handle" - index: bpy.props.IntProperty(name="Link Index") + link_index: bpy.props.IntProperty(name="Link Index") def execute(self, context): props = tool.Project.get_project_props() - link = props.links[self.index] - handle = link.empty_handle + link = props.links[self.link_index] + handle = tool.Project.get_link_empty_handle(link) if not handle: self.report({"ERROR"}, "Link has no empty handle (probably it was deleted).") return {"CANCELLED"} @@ -1681,6 +1801,43 @@ class SelectLinkHandle(bpy.types.Operator): return {"FINISHED"} +class SelectLinkedModelElement(bpy.types.Operator): + bl_idname = "bim.select_linked_model_element" + bl_label = "Select Linked Model Element" + bl_options = {"REGISTER"} + bl_description = "Select an element in the currently selected linked model by providing GlobalId." + + guid: bpy.props.StringProperty(name="GlobalId") # pyright: ignore[reportRedeclaration] + + if TYPE_CHECKING: + guid: str + + def invoke(self, context, event): + assert context.window_manager + return context.window_manager.invoke_props_dialog(self) + + def execute(self, context) -> set["rna_enums.OperatorReturnItems"]: + guid = self.guid.strip() + if not guid: + self.report({"ERROR"}, "GlobalId is not provided.") + return {"CANCELLED"} + + props = tool.Project.get_project_props() + active_link = props.active_link + assert active_link is not None + assert active_link.is_loaded + + guid_obj = tool.Project.Link.get_obj_by_guid(active_link, guid) + if not guid_obj: + filepath = active_link.filepath + self.report({"INFO"}, f"Element with GlobalId '{guid}' not found in the linked model at '{filepath}'.") + return {"CANCELLED"} + + tool.Project.Link.select_linked_element(context, guid_obj, guid) + self.report({"INFO"}, f"Element with GlobalId '{guid}' is selected.") + return {"FINISHED"} + + class ExportIFC(bpy.types.Operator, ExportHelper): bl_idname = "bim.save_project" bl_label = "Save IFC" @@ -1866,7 +2023,14 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper): print("Processing", self.filepath) self.collection = bpy.data.collections.new("IfcProject/" + os.path.basename(self.filepath)) - self.file = ifcopenshell.open(self.filepath) + + try: + self.file = ifcopenshell.open(self.filepath) + except Exception as e: + self.report({"ERROR"}, f"Failed to open IFC file: {str(e)}") + bpy.data.collections.remove(self.collection) + return {"CANCELLED"} + tool.Ifc.set(self.file) print("Finished opening") @@ -1897,20 +2061,13 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper): if tool.Loader.settings.false_origin_mode == "MANUAL" and tool.Loader.settings.false_origin: tool.Loader.set_manual_blender_offset(self.file) elif tool.Loader.settings.false_origin_mode == "AUTOMATIC": - if host_model_origin_si := gprops.host_model_origin_si: - host_model_origin_si = [float(o) / self.unit_scale for o in host_model_origin_si.split(",")] - tool.Loader.settings.false_origin = host_model_origin_si - tool.Loader.settings.project_north = float(gprops.host_model_project_north) - tool.Loader.set_manual_blender_offset(self.file) - else: - tool.Loader.guess_false_origin(self.file) + tool.Loader.guess_false_origin(self.file) tool.Georeference.set_model_origin() self.json_filepath = self.filepath + ".cache.json" data = { - "host_model_origin": gprops.host_model_origin, - "host_model_origin_si": gprops.host_model_origin_si, - "host_model_project_north": gprops.host_model_project_north, + "model_is_georeferenced": gprops.model_is_georeferenced, + "model_crs": gprops.model_crs, "model_origin": gprops.model_origin, "model_origin_si": gprops.model_origin_si, "model_project_north": gprops.model_project_north, @@ -2162,21 +2319,16 @@ class QueryLinkedElement(bpy.types.Operator): @classmethod def poll(cls, context): + assert context.area return context.area.type == "VIEW_3D" - def execute(self, context): - import sqlite3 - - from bpy_extras.view3d_utils import ( - region_2d_to_origin_3d, - region_2d_to_vector_3d, - ) - + def execute(self, context) -> set["rna_enums.OperatorReturnItems"]: LinksData.linked_data = {} props = tool.Project.get_project_props() props.queried_obj = None - for area in bpy.context.screen.areas: + assert context.screen + for area in context.screen.areas: if area.type == "PROPERTIES": for region in area.regions: if region.type == "WINDOW": @@ -2184,128 +2336,89 @@ class QueryLinkedElement(bpy.types.Operator): elif area.type == "VIEW_3D": area.tag_redraw() + assert context.region and context.region_data region = context.region rv3d = context.region_data coord = (self.mouse_x, self.mouse_y) origin = region_2d_to_origin_3d(region, rv3d, coord) direction = region_2d_to_vector_3d(region, rv3d, coord) - hit, location, normal, face_index, obj, instance_matrix = self.ray_cast(context, origin, direction) + hit, location, normal, face_index, obj, instance_matrix = tool.Blender.ray_cast_scene( + context, origin, direction + ) if not hit: self.report({"INFO"}, "No object found.") return {"FINISHED"} - if "guids" not in obj: + if not tool.Project.Link.is_linked_element(obj): self.report({"INFO"}, "Object is not a linked IFC element.") return {"FINISHED"} - guid = None - guid_start_index = 0 - for i, guid_end_index in enumerate(obj["guid_ids"]): - if face_index < guid_end_index: - guid = obj["guids"][i] - props.queried_obj = obj - props.queried_obj_root = self.find_obj_root(obj, instance_matrix) - - selected_tris = [] - selected_edges = [] - vert_indices = set() - for polygon in obj.data.polygons[guid_start_index:guid_end_index]: - vert_indices.update(polygon.vertices) - vert_indices = list(vert_indices) - vert_map = {k: v for v, k in enumerate(vert_indices)} - selected_vertices = [tuple(obj.matrix_world @ obj.data.vertices[vi].co) for vi in vert_indices] - for polygon in obj.data.polygons[guid_start_index:guid_end_index]: - selected_tris.append(tuple(vert_map[v] for v in polygon.vertices)) - selected_edges.extend(tuple([vert_map[vi] for vi in e] for e in polygon.edge_keys)) - - obj["selected_vertices"] = selected_vertices - obj["selected_edges"] = selected_edges - obj["selected_tris"] = selected_tris - - break - guid_start_index = guid_end_index - - self.db = sqlite3.connect(obj["db"]) - self.c = self.db.cursor() - - self.c.execute(f"SELECT * FROM elements WHERE global_id = '{guid}' LIMIT 1") - element = self.c.fetchone() - - attributes = {} - for i, attr in enumerate(["GlobalId", "IFC Class", "Predefined Type", "Name", "Description"]): - if element[i + 1] is not None: - attributes[attr] = element[i + 1] - - self.c.execute("SELECT * FROM properties WHERE element_id = ?", (element[0],)) - rows = self.c.fetchall() - - properties = {} - for row in rows: - properties.setdefault(row[1], {})[row[2]] = row[3] - - self.c.execute("SELECT * FROM relationships WHERE from_id = ?", (element[0],)) - relationships = self.c.fetchall() - - relating_type_id = None - - for relationship in relationships: - if relationship[1] == "IfcRelDefinesByType": - relating_type_id = relationship[2] - - type_properties = {} - if relating_type_id is not None: - self.c.execute("SELECT * FROM properties WHERE element_id = ?", (relating_type_id,)) - rows = self.c.fetchall() - for row in rows: - type_properties.setdefault(row[1], {})[row[2]] = row[3] - - LinksData.linked_data = { - "attributes": attributes, - "properties": [(k, properties[k]) for k in sorted(properties.keys())], - "type_properties": [(k, type_properties[k]) for k in sorted(type_properties.keys())], - } - self.db.close() - - for area in bpy.context.screen.areas: - if area.type == "PROPERTIES": - for region in area.regions: - if region.type == "WINDOW": - region.tag_redraw() - elif area.type == "VIEW_3D": - area.tag_redraw() + guid = tool.Project.Link.get_guid_by_face_index(obj, face_index) + assert guid is not None + tool.Project.Link.select_linked_element(context, obj, guid) self.report({"INFO"}, f"Loaded data for {guid}") ProjectDecorator.install(bpy.context) return {"FINISHED"} - def ray_cast(self, context: bpy.types.Context, origin: Vector, direction: Vector): - depsgraph = context.evaluated_depsgraph_get() - result = context.scene.ray_cast(depsgraph, origin, direction) - return result - - def find_obj_root(self, obj: bpy.types.Object, matrix: Matrix) -> Union[bpy.types.Object, None]: - collections = set(obj.users_collection) - for o in bpy.data.objects: - if ( - o.type != "EMPTY" - or o.instance_type != "COLLECTION" - or o.instance_collection not in collections - or not np.allclose(matrix, o.matrix_world, atol=1e-4) - ): - continue - return o - def invoke(self, context, event): self.mouse_x = event.mouse_region_x self.mouse_y = event.mouse_region_y return self.execute(context) +class HideQueriedLinkedElement(bpy.types.Operator): + bl_idname = "bim.hide_queried_linked_element" + bl_label = "Hide Queried Linked Element" + bl_description = ( + "Hide geometry for currently queried linked element.\n\n" + "ALT+Click (or ALT+H in Explore Tool) to unhide all geometry for currently selected linked model.\n" + "(Not Yet Implemented) SHIFT+Click to hide everything but currently queried element." + ) + bl_options = {"REGISTER", "UNDO"} + + unhide_all: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration] + + if TYPE_CHECKING: + unhide_all: bool + + def invoke(self, context, event): + self.unhide_all = event.alt + return self.execute(context) + + def execute(self, context) -> set["rna_enums.OperatorReturnItems"]: + props = tool.Project.get_project_props() + + if self.unhide_all: + return self.run_unhide_all() + + obj = props.queried_obj + if not obj: + self.report({"INFO"}, "No object is queried to hide.") + return {"FINISHED"} + guid = props.queried_guid + tool.Project.Link.hide_linked_element(obj, guid) + tool.Project.Link.deselect_queried_linked_element() + + self.report({"INFO"}, "Queried object is now hidden.") + return {"FINISHED"} + + def run_unhide_all(self) -> set["rna_enums.OperatorReturnItems"]: + props = tool.Project.get_project_props() + link = props.active_link + if not link: + self.report({"INFO"}, "No linked model is currently selected.") + return {"FINISHED"} + tool.Project.Link.unhide_all_elements(link) + self.report({"INFO"}, "All linked model geometry is unhidden.") + return {"FINISHED"} + + class AppendInspectedLinkedElement(AppendLibraryElement): bl_idname = "bim.append_inspected_linked_element" bl_label = "Append Inspected Linked Element" bl_description = "Append inspected linked element" - bl_options = {"REGISTER"} + bl_options = {"REGISTER", "UNDO"} def _execute(self, context): from bonsai.bim.module.project.data import LinksData @@ -2321,6 +2434,7 @@ class AppendInspectedLinkedElement(AppendLibraryElement): return {"CANCELLED"} queried_obj = props.queried_obj + assert queried_obj ifc_file = tool.Ifc.get() linked_ifc_file: ifcopenshell.file @@ -2578,28 +2692,25 @@ class CreateClippingPlane(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - from bpy_extras.view3d_utils import ( - region_2d_to_origin_3d, - region_2d_to_vector_3d, - ) - # Clean up deleted planes props = tool.Project.get_project_props() if len(props.clipping_planes) > 5: self.report({"INFO"}, "Maximum of six clipping planes allowed.") return {"FINISHED"} + assert context.screen for area in context.screen.areas: if area.type == "VIEW_3D": area.tag_redraw() + assert context.region and context.region_data region = context.region rv3d = context.region_data if rv3d: # Called from a 3D viewport coord = (self.mouse_x, self.mouse_y) origin = region_2d_to_origin_3d(region, rv3d, coord) direction = region_2d_to_vector_3d(region, rv3d, coord) - hit, location, normal, face_index, obj, matrix = self.ray_cast(context, origin, direction) + hit, location, normal, face_index, obj, matrix = tool.Blender.ray_cast_scene(context, origin, direction) if not hit: self.report({"INFO"}, "No object found.") return {"FINISHED"} @@ -2634,11 +2745,6 @@ class CreateClippingPlane(bpy.types.Operator): bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT") return {"FINISHED"} - def ray_cast(self, context, origin, direction): - depsgraph = context.evaluated_depsgraph_get() - result = context.scene.ray_cast(depsgraph, origin, direction) - return result - def invoke(self, context, event): self.mouse_x = event.mouse_region_x self.mouse_y = event.mouse_region_y @@ -2737,7 +2843,7 @@ class IFCFileHandlerOperator(bpy.types.Operator): def clean_up_path(path: str) -> str: # In Blender 4.5.6 there was a bug producing unncesseary double slash prefix - # breaking the paths. Issue is not present in 5.0+ and presumably will be solved in 4.5.7 too. + # breaking the paths. Issue is not present in 5.0+ and is fixed in 4.5.7. # https://projects.blender.org/blender/blender/issues/153822 if bpy.app.version == (4, 5, 6): blender_prefix = "//" @@ -3141,29 +3247,9 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator): bmesh.ops.scale(bm, vec=(scale_factor, scale_factor, 1.0), verts=bm.verts) - if bm.loops.layers.uv: - uv_layer = bm.loops.layers.uv.active - - min_x = min(v.co.x for v in bm.verts) - max_x = max(v.co.x for v in bm.verts) - min_y = min(v.co.y for v in bm.verts) - max_y = max(v.co.y for v in bm.verts) - - width = max_x - min_x - height = max_y - min_y - - for face in bm.faces: - for loop in face.loops: - vert = loop.vert - u = (vert.co.x - min_x) / width if width > 0 else 0.5 - v = (vert.co.y - min_y) / height if height > 0 else 0.5 - - u = max(0.0, min(1.0, u)) - v = max(0.0, min(1.0, v)) - loop[uv_layer].uv = (u, v) - bm.to_mesh(mesh) bm.free() + tool.Loader.load_generated_uv_map(mesh) mesh.update() element = tool.Ifc.get_entity(self.target_object) diff --git a/src/bonsai/bonsai/bim/module/project/prop.py b/src/bonsai/bonsai/bim/module/project/prop.py index 08859af1f9..5524f7efa7 100644 --- a/src/bonsai/bonsai/bim/module/project/prop.py +++ b/src/bonsai/bonsai/bim/module/project/prop.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import os from collections.abc import Generator from typing import TYPE_CHECKING, Literal, Union, assert_never, get_args @@ -38,7 +37,7 @@ import bonsai.bim.helper import bonsai.tool as tool from bonsai.bim.ifc import IfcStore from bonsai.bim.module.project.data import ProjectData, ProjectLibraryData -from bonsai.bim.prop import Attribute, ObjProperty, StrProperty +from bonsai.bim.prop import Attribute, ObjProperty def get_export_schema(self: "BIMProjectProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: @@ -219,29 +218,62 @@ class FilterCategory(PropertyGroup): class Link(PropertyGroup): - name: StringProperty( - name="Name", + name: StringProperty(name="Name") + filepath: StringProperty( + name="Filepath", description="Filepath to linked .ifc file, stored in posix format (could be relative to .ifc file, not to .blend)", ) + transformation: StringProperty( + name="Transformation", + description="4x4 matrix transformation as a flattened comma separated list for the linked model", + default="", + ) + georeferenced: EnumProperty( + name="Georeferenced", + description="Georeferencing status: compatibility between host and linked model", + items=[ + ("NONE", "No Georef", "Linked model has no georeferencing"), + ("NOT_COMPATIBLE", "Not Compatible", "Has geo data but CRS differ from host"), + ("FULL_COMPATIBLE", "Full Compatible", "Both CRS name and vertical datum match host"), + ], + default="NONE", + ) + has_transformation: BoolProperty( + name="Has Transformation", + description="Whether there is a transformation from its global coordinates", + default=False, + ) is_loaded: BoolProperty(name="Is Loaded", default=False) + is_editing: BoolProperty(name="Is Editing", description="Whether the link is being transformed", default=False) is_selectable: BoolProperty(name="Is Selectable", default=True) is_wireframe: BoolProperty(name="Is Wireframe", default=False) is_hidden: BoolProperty(name="Is Hidden", default=False) include_in_drawings: BoolProperty(name="Include in Drawings", default=True, options=set()) empty_handle: PointerProperty( name="Empty Object Handle", - description="We use empty object handle to allow simple manipulations with a linked model (moving, scaling, rotating)", + description="Storage for empty handle. Used in non-IFC scenarios or temporarily during link creation", type=bpy.types.Object, ) + ifc_definition_id: IntProperty( + name="IFC Definition ID", + description="STEP ID of the IfcDocumentReference when linked to a parent IFC project. Zero when no parent IFC exists", + default=0, + ) if TYPE_CHECKING: name: str + filepath: str + transformation: str + georeferenced: Literal["NONE", "NOT_COMPATIBLE", "FULL_COMPATIBLE"] + has_transformation: bool is_loaded: bool + is_editing: bool is_selectable: bool is_wireframe: bool is_hidden: bool include_in_drawings: bool empty_handle: Union[bpy.types.Object, None] + ifc_definition_id: int class EditedObj(PropertyGroup): @@ -420,10 +452,15 @@ class BIMProjectProperties(PropertyGroup): ) queried_obj: bpy.props.PointerProperty(type=bpy.types.Object) queried_obj_root: bpy.props.PointerProperty(type=bpy.types.Object) + queried_guid: bpy.props.StringProperty() clipping_planes: bpy.props.CollectionProperty(type=ObjProperty) clipping_planes_active_index: bpy.props.IntProperty(min=0, default=0, max=5) edited_objs: bpy.props.CollectionProperty(type=EditedObj) + @property + def active_link(self) -> Union[Link, None]: + return tool.Blender.get_active_uilist_element(self.links, self.active_link_index) + @property def active_clipping_plane(self) -> ObjProperty | None: return tool.Blender.get_active_uilist_element(self.clipping_planes, self.clipping_planes_active_index) @@ -514,6 +551,7 @@ class BIMProjectProperties(PropertyGroup): should_save_metadata_for_this_file: bool queried_obj: Union[bpy.types.Object, None] queried_obj_root: Union[bpy.types.Object, None] + queried_guid: str clipping_planes: bpy.types.bpy_prop_collection_idprop[ObjProperty] clipping_planes_active_index: int edited_objs: bpy.types.bpy_prop_collection_idprop[EditedObj] diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index 79ed2c7d7a..d86dd3cf5b 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -22,7 +22,6 @@ import os from typing import TYPE_CHECKING import bpy -import ifcopenshell from bpy.types import Menu, Panel, UIList import bonsai.bim @@ -477,17 +476,28 @@ class BIM_PT_links(Panel): def draw(self, context): self.props = tool.Project.get_project_props() + row = self.layout.row(align=True) row.operator("bim.link_ifc") if self.props.links: - self.layout.template_list( - "BIM_UL_links", - "", - self.props, - "links", - self.props, - "active_link_index", - ) + if self.props.active_link: + row = self.layout.row(align=True) + row.alignment = "RIGHT" + index = self.props.active_link_index + if self.props.active_link.is_loaded: + if self.props.active_link.is_editing: + row.operator("bim.edit_link", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_link", text="", icon="CANCEL") + else: + row.operator("bim.enable_editing_link", text="", icon="GREASEPENCIL") + row.operator("bim.select_linked_model_element", icon="VIEWZOOM", text="") + row.operator("bim.select_link_handle", text="", icon="OBJECT_DATA").link_index = index + row.operator("bim.unload_link", text="", icon="UNLINKED").link_index = index + row.operator("bim.reload_link", text="", icon="FILE_REFRESH").link_index = index + else: + row.operator("bim.load_link", text="", icon="LINKED").link_index = index + row.operator("bim.unlink_ifc", text="", icon="X").link_index = index + self.layout.template_list("BIM_UL_links", "", self.props, "links", self.props, "active_link_index") if LinksData.enable_culling: row = self.layout.row(align=True) @@ -607,47 +617,30 @@ class BIM_UL_links(UIList): active_propname, index, ): - if item: - row = layout.row(align=True) - if item.is_loaded: - row.label(text=item.name) - op = row.operator( - "bim.toggle_link_selectability", - text="", - icon="RESTRICT_SELECT_OFF" if item.is_selectable else "RESTRICT_SELECT_ON", - emboss=False, - ) - op.link = item.name - op = row.operator( - "bim.toggle_link_visibility", - text="", - icon="CUBE" if item.is_wireframe else "MESH_CUBE", - emboss=False, - ) - op.link = item.name - op.mode = "WIREFRAME" - op = row.operator( - "bim.toggle_link_visibility", - text="", - icon="HIDE_ON" if item.is_hidden else "HIDE_OFF", - emboss=False, - ) - op.link = item.name - op.mode = "VISIBLE" - op = row.operator("bim.select_link_handle", text="", icon="OBJECT_DATA") - op.index = index - op = row.operator("bim.unload_link", text="", icon="UNLINKED") - op.filepath = item.name - op = row.operator("bim.reload_link", text="", icon="FILE_REFRESH") - op.filepath = item.name - else: - row.prop(item, "name", text="") - op = row.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER") - op.attribute_data_path = tool.Blender.get_full_data_path(item, "name") - op = row.operator("bim.load_link", text="", icon="LINKED") - op.filepath = item.name - op = row.operator("bim.unlink_ifc", text="", icon="X") - op.filepath = item.name + row = layout.row(align=True) + if item.is_loaded: + if item.georeferenced == "NONE": + row.label(text="", icon="QUESTION") + elif item.georeferenced == "NOT_COMPATIBLE": + row.label(text="", icon="ERROR") + elif item.georeferenced == "FULL_COMPATIBLE": + row.label(text="", icon="WORLD") + if item.has_transformation: + row.label(text="", icon="OBJECT_ORIGIN") + + row.label(text=item.filepath) + icon = "RESTRICT_SELECT_OFF" if item.is_selectable else "RESTRICT_SELECT_ON" + row.operator("bim.toggle_link_selectability", text="", icon=icon, emboss=False).link_index = index + icon = "CUBE" if item.is_wireframe else "MESH_CUBE" + op = row.operator("bim.toggle_link_visibility", text="", icon=icon, emboss=False) + op.link_index = index + op.mode = "WIREFRAME" + icon = "HIDE_ON" if item.is_hidden else "HIDE_OFF" + op = row.operator("bim.toggle_link_visibility", text="", icon=icon, emboss=False) + op.link_index = index + op.mode = "VISIBLE" + else: + row.label(text=item.filepath) class BIM_PT_purge(Panel): diff --git a/src/bonsai/bonsai/bim/module/project/workspace.py b/src/bonsai/bonsai/bim/module/project/workspace.py index a72981c625..5437d1270b 100644 --- a/src/bonsai/bonsai/bim/module/project/workspace.py +++ b/src/bonsai/bonsai/bim/module/project/workspace.py @@ -19,7 +19,6 @@ import os import bpy -from bpy.types import WorkSpaceTool import bonsai.tool as tool from bonsai.bim.module.project.data import LinksData @@ -41,9 +40,11 @@ class ExploreTool(bpy.types.WorkSpaceTool): ("bim.explore_hotkey", {"type": "C", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_C")]}), ("bim.explore_hotkey", {"type": "M", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_M")]}), ("bim.explore_hotkey", {"type": "S", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_S")]}), + ("bim.explore_hotkey", {"type": "H", "value": "PRESS"}, {"properties": [("hotkey", "H")]}), + ("bim.explore_hotkey", {"type": "H", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_H")]}), ) - def draw_settings(context, layout, ws_tool): + def draw_settings(context: bpy.types.Context, layout: bpy.types.UILayout, ws_tool) -> None: row = layout.row(align=True) row.label(text="Query Object", icon="MOUSE_RMB") row = layout.row(align=True) @@ -62,6 +63,9 @@ class ExploreTool(bpy.types.WorkSpaceTool): row.label(text="", icon="EVENT_ALT") row.label(text="Disable Culling" if LinksData.enable_culling else "Enable Culling", icon="EVENT_C") + row = layout.row(align=True) + row.operator("bim.hide_queried_linked_element", text="Hide Queried Element", icon="EVENT_H") + prop = tool.Project.get_measure_tool_settings() row = layout.row(align=True) row.label(text="", icon="EVENT_SHIFT") @@ -87,6 +91,7 @@ class ExploreHotkey(bpy.types.Operator): bl_idname = "bim.explore_hotkey" bl_label = "" bl_options = {"REGISTER", "UNDO", "INTERNAL"} + hotkey: bpy.props.StringProperty() description: bpy.props.StringProperty() @@ -138,3 +143,9 @@ class ExploreHotkey(bpy.types.Operator): return bpy.ops.bim.image_scaling_tool("INVOKE_DEFAULT") + + def hotkey_H(self) -> None: + bpy.ops.bim.hide_queried_linked_element() + + def hotkey_A_H(self) -> None: + bpy.ops.bim.hide_queried_linked_element(unhide_all=True) diff --git a/src/bonsai/bonsai/bim/module/pset/operator.py b/src/bonsai/bonsai/bim/module/pset/operator.py index 3257760531..d7755cf80a 100644 --- a/src/bonsai/bonsai/bim/module/pset/operator.py +++ b/src/bonsai/bonsai/bim/module/pset/operator.py @@ -32,7 +32,7 @@ import bonsai.tool as tool from bonsai.bim.ifc import IfcStore if TYPE_CHECKING: - from bonsai.bim.module.pset.prop import AddEditPropertyEntry, RenamePropertyEntry + from bonsai.bim.module.pset.prop import AddEditPropertyEntry class TogglePsetExpansion(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/bonsai/bonsai/bim/module/pset/prop.py b/src/bonsai/bonsai/bim/module/pset/prop.py index 4b16e15010..1777fa0f94 100644 --- a/src/bonsai/bonsai/bim/module/pset/prop.py +++ b/src/bonsai/bonsai/bim/module/pset/prop.py @@ -22,13 +22,11 @@ import bpy import ifcopenshell import ifcopenshell.util.attribute import ifcopenshell.util.doc -import ifcopenshell.util.element from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, FloatProperty, - FloatVectorProperty, IntProperty, PointerProperty, StringProperty, @@ -44,7 +42,7 @@ from bonsai.bim.module.pset.data import ( ObjectPsetsData, PsetsGeneralData, ) -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute psetnames = {} qtonames = {} diff --git a/src/bonsai/bonsai/bim/module/pset_template/data.py b/src/bonsai/bonsai/bim/module/pset_template/data.py index 1725b974f1..0d922fe2ae 100644 --- a/src/bonsai/bonsai/bim/module/pset_template/data.py +++ b/src/bonsai/bonsai/bim/module/pset_template/data.py @@ -16,11 +16,8 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import os -import pathlib from typing import Any -import bpy import ifcopenshell import ifcopenshell.util.attribute import ifcopenshell.util.doc diff --git a/src/bonsai/bonsai/bim/module/pset_template/operator.py b/src/bonsai/bonsai/bim/module/pset_template/operator.py index e956917df3..0ae20ce0ea 100644 --- a/src/bonsai/bonsai/bim/module/pset_template/operator.py +++ b/src/bonsai/bonsai/bim/module/pset_template/operator.py @@ -20,7 +20,6 @@ import os import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.pset_template import bonsai.bim.handler diff --git a/src/bonsai/bonsai/bim/module/pset_template/prop.py b/src/bonsai/bonsai/bim/module/pset_template/prop.py index 9ec31b364f..3b2d6c5d9e 100644 --- a/src/bonsai/bonsai/bim/module/pset_template/prop.py +++ b/src/bonsai/bonsai/bim/module/pset_template/prop.py @@ -16,18 +16,15 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import os from typing import TYPE_CHECKING import bpy -import ifcopenshell import ifcopenshell.util.attribute from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, FloatProperty, - FloatVectorProperty, IntProperty, PointerProperty, StringProperty, @@ -38,7 +35,6 @@ from ifcopenshell.util.doc import get_attribute_doc import bonsai.tool as tool from bonsai.bim.ifc import IfcStore from bonsai.bim.module.pset_template.data import PsetTemplatesData -from bonsai.bim.prop import Attribute, StrProperty def updatePsetTemplateFiles(self, context): diff --git a/src/bonsai/bonsai/bim/module/pset_template/ui.py b/src/bonsai/bonsai/bim/module/pset_template/ui.py index 6576e8f56c..92cd78e4ba 100644 --- a/src/bonsai/bonsai/bim/module/pset_template/ui.py +++ b/src/bonsai/bonsai/bim/module/pset_template/ui.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import bpy from bpy.types import Panel import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/qto/operator.py b/src/bonsai/bonsai/bim/module/qto/operator.py index 572c65e258..6aa10c9beb 100644 --- a/src/bonsai/bonsai/bim/module/qto/operator.py +++ b/src/bonsai/bonsai/bim/module/qto/operator.py @@ -18,7 +18,6 @@ import bpy import ifcopenshell -import ifcopenshell.api from ifcopenshell.util.profiler import Profiler import bonsai.core.qto as core diff --git a/src/bonsai/bonsai/bim/module/qto/prop.py b/src/bonsai/bonsai/bim/module/qto/prop.py index 9952285d30..7ffbfcdfa7 100644 --- a/src/bonsai/bonsai/bim/module/qto/prop.py +++ b/src/bonsai/bonsai/bim/module/qto/prop.py @@ -22,18 +22,12 @@ import bpy import ifc5d.qto from bpy.props import ( BoolProperty, - CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup import bonsai.tool as tool -from bonsai.bim.prop import Attribute, StrProperty CALCULATOR_FUNCTION_ENUM_ITEMS: list[Union[tuple[str, str, str], None]] = [] diff --git a/src/bonsai/bonsai/bim/module/resource/prop.py b/src/bonsai/bonsai/bim/module/resource/prop.py index 4c6828e56c..9f2c5ba85c 100644 --- a/src/bonsai/bonsai/bim/module/resource/prop.py +++ b/src/bonsai/bonsai/bim/module/resource/prop.py @@ -19,7 +19,6 @@ from typing import TYPE_CHECKING, Literal, get_args import bpy -import ifcopenshell.api import ifcopenshell.api.resource import ifcopenshell.util.resource from bpy.props import ( @@ -27,9 +26,7 @@ from bpy.props import ( CollectionProperty, EnumProperty, FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/root/data.py b/src/bonsai/bonsai/bim/module/root/data.py index 7adf7b5fb9..c1988c8b82 100644 --- a/src/bonsai/bonsai/bim/module/root/data.py +++ b/src/bonsai/bonsai/bim/module/root/data.py @@ -19,7 +19,6 @@ from collections import defaultdict from typing import Union -import bpy import ifcopenshell.util.attribute import ifcopenshell.util.element import ifcopenshell.util.schema diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py index ac766172bd..159f157d44 100644 --- a/src/bonsai/bonsai/bim/module/root/operator.py +++ b/src/bonsai/bonsai/bim/module/root/operator.py @@ -22,7 +22,6 @@ import bmesh import bpy import idprop import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.geometry import ifcopenshell.api.material import ifcopenshell.api.pset @@ -34,7 +33,6 @@ import ifcopenshell.util.type import ifcopenshell.util.unit from mathutils import Vector -import bonsai.bim.handler import bonsai.bim.module.root.prop as root_prop import bonsai.core.geometry import bonsai.core.root as core diff --git a/src/bonsai/bonsai/bim/module/root/prop.py b/src/bonsai/bonsai/bim/module/root/prop.py index 854eb6d0f0..35b7b9f403 100644 --- a/src/bonsai/bonsai/bim/module/root/prop.py +++ b/src/bonsai/bonsai/bim/module/root/prop.py @@ -19,17 +19,10 @@ from typing import TYPE_CHECKING, Union import bpy -import ifcopenshell import ifcopenshell.util.element -import ifcopenshell.util.schema import ifcopenshell.util.type from bpy.props import ( - BoolProperty, - CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, PointerProperty, StringProperty, ) diff --git a/src/bonsai/bonsai/bim/module/root/ui.py b/src/bonsai/bonsai/bim/module/root/ui.py index 72eadb4e39..f9eae350e4 100644 --- a/src/bonsai/bonsai/bim/module/root/ui.py +++ b/src/bonsai/bonsai/bim/module/root/ui.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import bpy from bpy.types import Panel import bonsai.bim.module.root.prop as root_prop diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index 83f401a122..b7ba62acdb 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -19,11 +19,10 @@ import bisect import json import traceback -from typing import TYPE_CHECKING, Literal, assert_never, get_args +from typing import TYPE_CHECKING, Any, Literal, assert_never, get_args import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.group import ifcopenshell.util.element import ifcopenshell.util.selector @@ -31,13 +30,10 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) -from bpy.types import Operator, PropertyGroup +from bpy.types import Operator from natsort import natsorted import bonsai.core.search as core @@ -695,9 +691,9 @@ class EditFilterQuery(Operator, tool.Ifc.Operator): filter_groups = tool.Search.get_filter_groups(module) if tool.Blender.get_addon_preferences().chain_filter_with_set_operations: - filter_structure = [] + filter_structure: list[list[dict[str, Any]]] = [] for filter_group in filter_groups: - group_data = [] + group_data: list[dict[str, Any]] = [] for ifc_filter in filter_group.filters: filter_data = { "type": ifc_filter.type, @@ -852,9 +848,9 @@ class SaveSearch(Operator, tool.Ifc.Operator): query = tool.Search.export_filter_query(filter_groups) results = tool.Search.execute_filter_groups(filter_groups) - filter_structure = [] + filter_structure: list[list[dict[str, Any]]] = [] for filter_group in filter_groups: - group_data = [] + group_data: list[dict[str, Any]] = [] for ifc_filter in filter_group.filters: filter_data = { "type": ifc_filter.type, diff --git a/src/bonsai/bonsai/bim/module/search/prop.py b/src/bonsai/bonsai/bim/module/search/prop.py index e3403b5abe..0f45112420 100644 --- a/src/bonsai/bonsai/bim/module/search/prop.py +++ b/src/bonsai/bonsai/bim/module/search/prop.py @@ -19,7 +19,6 @@ from typing import TYPE_CHECKING, Literal, get_args import bpy -import ifcopenshell.util.schema from bpy.props import ( BoolProperty, CollectionProperty, @@ -27,7 +26,6 @@ from bpy.props import ( FloatProperty, FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup @@ -40,8 +38,6 @@ from bonsai.bim.module.search.data import ( ) from bonsai.bim.prop import BIMFilterGroup, ObjProperty -from . import operator, prop, ui - def get_element_key(self: "BIMSearchProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not SelectSimilarData.is_loaded: diff --git a/src/bonsai/bonsai/bim/module/sequence/data.py b/src/bonsai/bonsai/bim/module/sequence/data.py index 7ea6050ca3..20027281eb 100644 --- a/src/bonsai/bonsai/bim/module/sequence/data.py +++ b/src/bonsai/bonsai/bim/module/sequence/data.py @@ -20,7 +20,6 @@ import json from typing import Any import bpy -import ifcopenshell import ifcopenshell.util.attribute import ifcopenshell.util.date from ifcopenshell.util.doc import get_predefined_type_doc diff --git a/src/bonsai/bonsai/bim/module/sequence/helper.py b/src/bonsai/bonsai/bim/module/sequence/helper.py index 50d2367144..9e16e38402 100644 --- a/src/bonsai/bonsai/bim/module/sequence/helper.py +++ b/src/bonsai/bonsai/bim/module/sequence/helper.py @@ -23,7 +23,6 @@ from typing import Any, Union import bpy import ifcopenshell.util.date -import isodate from dateutil import parser from bonsai.bim.prop import ISODuration diff --git a/src/bonsai/bonsai/bim/module/sequence/prop.py b/src/bonsai/bonsai/bim/module/sequence/prop.py index 6a5937ac45..abb1aa9f0c 100644 --- a/src/bonsai/bonsai/bim/module/sequence/prop.py +++ b/src/bonsai/bonsai/bim/module/sequence/prop.py @@ -16,14 +16,11 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -from typing import TYPE_CHECKING, Literal, Union, get_args +from typing import TYPE_CHECKING, Literal, get_args import bpy -import ifcopenshell.api import ifcopenshell.api.sequence -import ifcopenshell.util.attribute import ifcopenshell.util.date -import isodate from bpy.props import ( BoolProperty, CollectionProperty, @@ -31,7 +28,6 @@ from bpy.props import ( FloatProperty, FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/sequence/ui.py b/src/bonsai/bonsai/bim/module/sequence/ui.py index 0ecd772276..f758e89ef5 100644 --- a/src/bonsai/bonsai/bim/module/sequence/ui.py +++ b/src/bonsai/bonsai/bim/module/sequence/ui.py @@ -19,7 +19,6 @@ from typing import TYPE_CHECKING, Any, Optional import bpy -import ifcopenshell import isodate from bpy.types import Panel, UIList diff --git a/src/bonsai/bonsai/bim/module/spatial/decorator.py b/src/bonsai/bonsai/bim/module/spatial/decorator.py index 8d66d4e8a6..047a9ff555 100644 --- a/src/bonsai/bonsai/bim/module/spatial/decorator.py +++ b/src/bonsai/bonsai/bim/module/spatial/decorator.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . import blf -import bmesh import gpu from bpy.types import SpaceView3D from bpy_extras.view3d_utils import location_3d_to_region_2d diff --git a/src/bonsai/bonsai/bim/module/spatial/prop.py b/src/bonsai/bonsai/bim/module/spatial/prop.py index 3d3d2e74f4..b0d393b265 100644 --- a/src/bonsai/bonsai/bim/module/spatial/prop.py +++ b/src/bonsai/bonsai/bim/module/spatial/prop.py @@ -19,15 +19,11 @@ from typing import TYPE_CHECKING, Literal, Union import bpy -import ifcopenshell import ifcopenshell.api.attribute -import ifcopenshell.util.unit from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, PointerProperty, StringProperty, diff --git a/src/bonsai/bonsai/bim/module/structural/operator.py b/src/bonsai/bonsai/bim/module/structural/operator.py index edb3ab18ae..500dce4371 100644 --- a/src/bonsai/bonsai/bim/module/structural/operator.py +++ b/src/bonsai/bonsai/bim/module/structural/operator.py @@ -16,13 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import json from math import degrees -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Literal import bpy -import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.aggregate import ifcopenshell.api.group import ifcopenshell.api.structural diff --git a/src/bonsai/bonsai/bim/module/structural/prop.py b/src/bonsai/bonsai/bim/module/structural/prop.py index 24d6ead470..e1e658ce7a 100644 --- a/src/bonsai/bonsai/bim/module/structural/prop.py +++ b/src/bonsai/bonsai/bim/module/structural/prop.py @@ -25,21 +25,19 @@ from bpy.props import ( CollectionProperty, EnumProperty, FloatProperty, - FloatVectorProperty, IntProperty, PointerProperty, StringProperty, ) from bpy.types import PropertyGroup -import bonsai.tool as tool from bonsai.bim.module.structural.data import ( BoundaryConditionsData, LoadGroupDecorationData, StructuralLoadCasesData, StructuralLoadsData, ) -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute def get_load_groups_to_show(self: "BIMStructuralProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: diff --git a/src/bonsai/bonsai/bim/module/structural/shader.py b/src/bonsai/bonsai/bim/module/structural/shader.py index d6f92028d6..b9b5a5c7bc 100644 --- a/src/bonsai/bonsai/bim/module/structural/shader.py +++ b/src/bonsai/bonsai/bim/module/structural/shader.py @@ -58,15 +58,13 @@ class DecorationShader: "PLANAR LOAD", } if pattern not in valid_patterns: - raise ValueError( - """pattern must be one of: + raise ValueError("""pattern must be one of: PERPENDICULAR DISTRIBUTED FORCE PARALLEL DISTRIBUTED FORCE, DISTRIBUTED MOMENT, SINGLE FORCE, SINGLE MOMENT, - PLANAR LOAD""" - ) + PLANAR LOAD""") if "DISTRIBUTED" in pattern.upper(): shader = self.get_linear_shader(pattern) return shader diff --git a/src/bonsai/bonsai/bim/module/style/operator.py b/src/bonsai/bonsai/bim/module/style/operator.py index a26cce67f1..e7d1d05e0a 100644 --- a/src/bonsai/bonsai/bim/module/style/operator.py +++ b/src/bonsai/bonsai/bim/module/style/operator.py @@ -87,6 +87,7 @@ class RemoveStyle(bpy.types.Operator, tool.Ifc.Operator): core.remove_style(tool.Ifc, tool.Style, style=tool.Ifc.get().by_id(self.style), reload_styles_ui=True) +# TODO: remove completely class AddStyle(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_style" bl_label = "Add Style" diff --git a/src/bonsai/bonsai/bim/module/style/prop.py b/src/bonsai/bonsai/bim/module/style/prop.py index b1660dfd4c..5d14bbd248 100644 --- a/src/bonsai/bonsai/bim/module/style/prop.py +++ b/src/bonsai/bonsai/bim/module/style/prop.py @@ -24,7 +24,6 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, FloatVectorProperty, IntProperty, PointerProperty, diff --git a/src/bonsai/bonsai/bim/module/system/data.py b/src/bonsai/bonsai/bim/module/system/data.py index c5d8897cc8..65faca50d5 100644 --- a/src/bonsai/bonsai/bim/module/system/data.py +++ b/src/bonsai/bonsai/bim/module/system/data.py @@ -19,7 +19,6 @@ from typing import Any, Union import bpy -import ifcopenshell import ifcopenshell.util.schema import ifcopenshell.util.system import ifcopenshell.util.unit diff --git a/src/bonsai/bonsai/bim/module/system/decorator.py b/src/bonsai/bonsai/bim/module/system/decorator.py index f2448ba6e6..13ac7519f1 100644 --- a/src/bonsai/bonsai/bim/module/system/decorator.py +++ b/src/bonsai/bonsai/bim/module/system/decorator.py @@ -16,19 +16,15 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -from math import cos, radians, sin import bmesh import bpy import gpu -import ifcopenshell from bpy.app.handlers import persistent from bpy.types import SpaceView3D from gpu_extras.batch import batch_for_shader -from mathutils import Matrix, Vector import bonsai.tool as tool -from bonsai.bim.module.system.data import SystemDecorationData ERROR_ELEMENTS_COLOR = (1, 0.2, 0.322, 1) # RED UNSPECIAL_ELEMENT_COLOR = (0.2, 0.2, 0.2, 1) # GREY diff --git a/src/bonsai/bonsai/bim/module/system/operator.py b/src/bonsai/bonsai/bim/module/system/operator.py index 089bd9f24b..e1f6f5e9e4 100644 --- a/src/bonsai/bonsai/bim/module/system/operator.py +++ b/src/bonsai/bonsai/bim/module/system/operator.py @@ -19,7 +19,7 @@ from typing import TYPE_CHECKING import bpy -import ifcopenshell.api +import ifcopenshell.api.attribute import ifcopenshell.api.system import ifcopenshell.util.system @@ -445,6 +445,7 @@ class CycleFlowDirection(bpy.types.Operator, tool.Ifc.Operator): return "Cycle through flow directions: SOURCE → SINK → SOURCEANDSINK → NOTDEFINED → SOURCE..." def _execute(self, context): + ifc_file = tool.Ifc.get() port = tool.Ifc.get().by_id(self.port_id) if not port or not port.is_a("IfcDistributionPort"): return {"CANCELLED"} @@ -459,7 +460,7 @@ class CycleFlowDirection(bpy.types.Operator, tool.Ifc.Operator): } next_direction = flow_cycle_map.get(current_direction, "SOURCE") - tool.Ifc.run("attribute.edit_attributes", product=port, attributes={"FlowDirection": next_direction}) + ifcopenshell.api.attribute.edit_attributes(ifc_file, product=port, attributes={"FlowDirection": next_direction}) connected_port = tool.System.get_connected_port(port) if connected_port: @@ -470,8 +471,8 @@ class CycleFlowDirection(bpy.types.Operator, tool.Ifc.Operator): "NOTDEFINED": "NOTDEFINED", } connected_direction = connected_direction_map.get(next_direction, "NOTDEFINED") - tool.Ifc.run( - "attribute.edit_attributes", product=connected_port, attributes={"FlowDirection": connected_direction} + ifcopenshell.api.attribute.edit_attributes( + ifc_file, product=connected_port, attributes={"FlowDirection": connected_direction} ) PortData.is_loaded = False diff --git a/src/bonsai/bonsai/bim/module/system/prop.py b/src/bonsai/bonsai/bim/module/system/prop.py index a40f50a90e..a03b2a0cfb 100644 --- a/src/bonsai/bonsai/bim/module/system/prop.py +++ b/src/bonsai/bonsai/bim/module/system/prop.py @@ -23,10 +23,7 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup @@ -35,7 +32,7 @@ import bonsai.bim.handler import bonsai.bim.module.system.decorator as decorator import bonsai.tool as tool from bonsai.bim.module.system.data import SystemData -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute def get_system_class(self: "BIMSystemProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: diff --git a/src/bonsai/bonsai/bim/module/tester/data.py b/src/bonsai/bonsai/bim/module/tester/data.py index 1d1a1c4901..8535d879c6 100644 --- a/src/bonsai/bonsai/bim/module/tester/data.py +++ b/src/bonsai/bonsai/bim/module/tester/data.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import bpy import ifctester.reporter import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/tester/operator.py b/src/bonsai/bonsai/bim/module/tester/operator.py index 6cb7999184..c8485d3339 100644 --- a/src/bonsai/bonsai/bim/module/tester/operator.py +++ b/src/bonsai/bonsai/bim/module/tester/operator.py @@ -31,7 +31,6 @@ from typing import Union import bpy import ifcopenshell -import ifctester import ifctester.ids import ifctester.reporter import socketio diff --git a/src/bonsai/bonsai/bim/module/tester/prop.py b/src/bonsai/bonsai/bim/module/tester/prop.py index 4beb07c533..1bfa32c1eb 100644 --- a/src/bonsai/bonsai/bim/module/tester/prop.py +++ b/src/bonsai/bonsai/bim/module/tester/prop.py @@ -22,9 +22,6 @@ import bpy from bpy.props import ( BoolProperty, CollectionProperty, - EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, PointerProperty, StringProperty, @@ -32,7 +29,7 @@ from bpy.props import ( from bpy.types import PropertyGroup from bonsai.bim.module.tester.data import TesterData -from bonsai.bim.prop import MultipleFileSelect, StrProperty +from bonsai.bim.prop import MultipleFileSelect def update_active_specification_index(self: "IfcTesterProperties", context: bpy.types.Context) -> None: diff --git a/src/bonsai/bonsai/bim/module/type/operator.py b/src/bonsai/bonsai/bim/module/type/operator.py index 8d24989f12..4a6ce053fc 100644 --- a/src/bonsai/bonsai/bim/module/type/operator.py +++ b/src/bonsai/bonsai/bim/module/type/operator.py @@ -18,16 +18,11 @@ from typing import TYPE_CHECKING -import bmesh import bpy -import ifcopenshell.api import ifcopenshell.api.attribute import ifcopenshell.api.type import ifcopenshell.util.element import ifcopenshell.util.representation -import ifcopenshell.util.schema -import ifcopenshell.util.type -import ifcopenshell.util.unit import bonsai.bim.helper import bonsai.core.geometry diff --git a/src/bonsai/bonsai/bim/module/type/prop.py b/src/bonsai/bonsai/bim/module/type/prop.py index 13141a7947..d624fd59a0 100644 --- a/src/bonsai/bonsai/bim/module/type/prop.py +++ b/src/bonsai/bonsai/bim/module/type/prop.py @@ -20,16 +20,11 @@ from typing import TYPE_CHECKING, Union import bpy import ifcopenshell.util.element -import ifcopenshell.util.type from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, PointerProperty, - StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/unit/data.py b/src/bonsai/bonsai/bim/module/unit/data.py index fc51ef3710..6d83bb4e16 100644 --- a/src/bonsai/bonsai/bim/module/unit/data.py +++ b/src/bonsai/bonsai/bim/module/unit/data.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import ifcopenshell import ifcopenshell.util.attribute import ifcopenshell.util.schema import ifcopenshell.util.unit diff --git a/src/bonsai/bonsai/bim/module/unit/operator.py b/src/bonsai/bonsai/bim/module/unit/operator.py index da2c7edc1d..f0cee9a6be 100644 --- a/src/bonsai/bonsai/bim/module/unit/operator.py +++ b/src/bonsai/bonsai/bim/module/unit/operator.py @@ -19,8 +19,6 @@ from typing import TYPE_CHECKING import bpy -import ifcopenshell.api -import ifcopenshell.util.unit import bonsai.core.unit as core import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/unit/prop.py b/src/bonsai/bonsai/bim/module/unit/prop.py index 02fe7ac803..60639ac3a1 100644 --- a/src/bonsai/bonsai/bim/module/unit/prop.py +++ b/src/bonsai/bonsai/bim/module/unit/prop.py @@ -23,16 +23,13 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup from bonsai.bim.module.unit.data import UnitsData -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute def get_unit_classes(self: "BIMUnitProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: diff --git a/src/bonsai/bonsai/bim/module/void/data.py b/src/bonsai/bonsai/bim/module/void/data.py index 6d1fcc12a2..0253140651 100644 --- a/src/bonsai/bonsai/bim/module/void/data.py +++ b/src/bonsai/bonsai/bim/module/void/data.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -from collections.abc import Generator from typing import Any, Union import bpy diff --git a/src/bonsai/bonsai/bim/module/web/data.py b/src/bonsai/bonsai/bim/module/web/data.py index e74b7fa0b6..652d5f7169 100644 --- a/src/bonsai/bonsai/bim/module/web/data.py +++ b/src/bonsai/bonsai/bim/module/web/data.py @@ -18,8 +18,6 @@ import os -import bpy - import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/web/operator.py b/src/bonsai/bonsai/bim/module/web/operator.py index 8efd15494e..a2910b9805 100644 --- a/src/bonsai/bonsai/bim/module/web/operator.py +++ b/src/bonsai/bonsai/bim/module/web/operator.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import os import bpy diff --git a/src/bonsai/bonsai/bim/module/web/prop.py b/src/bonsai/bonsai/bim/module/web/prop.py index 1538454270..42bd272578 100644 --- a/src/bonsai/bonsai/bim/module/web/prop.py +++ b/src/bonsai/bonsai/bim/module/web/prop.py @@ -18,16 +18,9 @@ from typing import TYPE_CHECKING -import bpy from bpy.props import ( BoolProperty, - CollectionProperty, - EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, - StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 456d5ed5a0..f332c91401 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -268,63 +268,45 @@ class SaveBlendMetadataFile(bpy.types.Operator): import bpy # Ensure all styles are loaded before attempting to remove them -try: - bpy.ops.bim.load_styles() -except Exception: - pass +bpy.ops.bim.load_styles() # 1. Collect all IfcStyle material names ifcstyle_material_names = [] -try: - styles_props = getattr(bpy.context.scene, "BIMStylesProperties", None) - if styles_props is None and bpy.data.scenes: - styles_props = getattr(bpy.data.scenes[0], "BIMStylesProperties", None) - if styles_props: - for style in list(styles_props.styles): - material = getattr(style, "blender_material", None) - if material and material.name: - ifcstyle_material_names.append(material.name) -except Exception: - pass +styles_props = getattr(bpy.context.scene, "BIMStylesProperties", None) +if styles_props is None and bpy.data.scenes: + styles_props = getattr(bpy.data.scenes[0], "BIMStylesProperties", None) +if styles_props: + for style in list(styles_props.styles): + material = getattr(style, "blender_material", None) + if material and material.name: + ifcstyle_material_names.append(material.name) # 2. Purge IfcStore -try: - from bonsai.bim.ifc import IfcStore -except ImportError: - IfcStore = None - -if IfcStore: - try: - IfcStore.purge() - except Exception: - pass +from bonsai.bim.ifc import IfcStore +IfcStore.purge() # 3. Remove all collections named IfcProject* for collection in list(bpy.data.collections): if collection.name.startswith('IfcProject'): - try: - bpy.data.collections.remove(collection, do_unlink=True) - except Exception: - pass + bpy.data.collections.remove(collection, do_unlink=True) -# 4. Purge orphaned data blocks after removing IfcProject collections -try: - bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) -except Exception: - pass +# 4.1 Remove all collections from linked libraries (they will be recreated by bonsai) +for collection in list(bpy.data.collections): + if collection.library: + bpy.data.collections.remove(collection, do_unlink=True) -# 5. Remove all materials corresponding to the IfcStyles we collected -materials_removed = 0 -try: - for mat_name in ifcstyle_material_names: - if mat_name in bpy.data.materials: - try: - bpy.data.materials.remove(bpy.data.materials[mat_name], do_unlink=True) - materials_removed += 1 - except Exception: - pass -except Exception: - pass +# 4.2. Remove all empty objects that are collection instances for linked models +for obj in list(bpy.data.objects): + if obj.type == 'EMPTY' and obj.instance_type == 'COLLECTION' and obj.name.startswith('IfcProject/'): + bpy.data.objects.remove(obj, do_unlink=True) + +# 5. Purge orphaned data blocks after removing IfcProject collections +bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) + +# 6. Remove all materials corresponding to the IfcStyles we collected +for mat_name in ifcstyle_material_names: + if mat_name in bpy.data.materials: + bpy.data.materials.remove(bpy.data.materials[mat_name], do_unlink=True) bpy.ops.wm.save_as_mainfile(filepath=r'{blendmetadata_path}') """ diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index 4e93c1c690..61b916965e 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -21,8 +21,6 @@ import os from typing import TYPE_CHECKING, Any, Literal, Union, assert_never, get_args import bpy -import ifcopenshell -import ifcopenshell.util.pset import ifcopenshell.util.unit from bpy.props import ( BoolProperty, @@ -35,17 +33,9 @@ from bpy.props import ( StringProperty, ) from bpy.types import PropertyGroup -from ifcopenshell.util.doc import ( - get_attribute_doc, - get_entity_doc, - get_predefined_type_doc, - get_property_doc, - get_property_set_doc, -) import bonsai.bim import bonsai.bim.handler -import bonsai.bim.schema import bonsai.tool as tool cwd = os.path.dirname(os.path.realpath(__file__)) diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index d224761609..1d0e8742dd 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -24,10 +24,9 @@ from typing import TYPE_CHECKING, Literal, Optional import bpy import platformdirs -from bpy.props import BoolProperty, IntProperty, StringProperty +from bpy.props import BoolProperty, StringProperty from bpy.types import Panel from ifcopenshell.util.doc import ( - get_attribute_doc, get_entity_doc, get_property_set_doc, get_type_doc, @@ -38,7 +37,6 @@ from natsort import natsorted import bonsai.bim import bonsai.bim.helper import bonsai.tool as tool -from bonsai import get_debug_info from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDProperty from bonsai.bim.module.model.prop import ( BIMDoorProperties, @@ -57,8 +55,6 @@ from bonsai.bim.module.model.ui import ( from bonsai.bim.module.pset.prop import IfcProperty from bonsai.bim.prop import Attribute -from . import ifc - if TYPE_CHECKING: from bonsai.bim.module.project.prop import BIMProjectProperties from bonsai.bim.prop import ObjProperty @@ -467,12 +463,12 @@ class DocPreferences(bpy.types.PropertyGroup): classes_to_wireframe: StringProperty( default="IfcVirtualElement", name="Classes to Wireframe", - description="Upon import, these classes will display as wireframe.\nEx: IfcVirtualelement, IfcSpace", + description="Upon import, these classes will display as wireframe.\nEx: IfcVirtualElement, IfcSpace", ) classes_no_cut: StringProperty( default="IfcVirtualElement, IfcSpace", name="Classes that are not cut", - description="The cut decoractor will be turned off for these classes\nEx: IfcVirtualelement, IfcSpace", + description="The cut decorator will be turned off for these classes\nEx: IfcVirtualElement, IfcSpace", ) if TYPE_CHECKING: @@ -660,6 +656,11 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): bsdd_load_test_dictionaries: BoolProperty( name="Load Test Dictionaries", description="Load dictionaries that are for testing only", default=False ) + bsdd_baseurl: StringProperty( + name="bSDD API Base URL", + description="Base URL for data dictionary API requests, e.g. https://api.bsdd.buildingsmart.org/api/", + default="https://api.bsdd.buildingsmart.org/api/", + ) should_disable_undo_on_save: BoolProperty( name="Disable Undo When Saving (Faster saves, no undo for you!)", default=False ) @@ -743,6 +744,12 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): default=".ifc.metadata.blend", ) + decorator_font_scale: bpy.props.FloatProperty( + name="Decorator Font Scale", + description="Scale factor for decorator font size.", + default=1.0, + ) + if TYPE_CHECKING: svg2pdf_command: str svg2dxf_command: str @@ -767,6 +774,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): bsdd_load_preview_dictionaries: bool bsdd_load_inactive_dictionaries: bool bsdd_load_test_dictionaries: bool + bsdd_baseurl: str should_disable_undo_on_save: bool should_stream: bool should_always_cache: bool @@ -782,6 +790,8 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): mass_time_units_in_wizard: bool chain_filter_with_set_operations: bool save_metadata_blend_file: bool + metadata_blend_file_suffix: str + decorator_font_scale: float def draw(self, context: bpy.types.Context) -> None: layout = self.layout @@ -972,6 +982,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): layout.prop(self, "bsdd_load_preview_dictionaries") layout.prop(self, "bsdd_load_inactive_dictionaries") layout.prop(self, "bsdd_load_test_dictionaries") + layout.prop(self, "bsdd_baseurl") def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: layout.prop(self, "container_hide_show_isolate") @@ -1004,6 +1015,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): else: row = layout.row() row.operator("bim.manage_tab_visibility", icon="PREFERENCES") + layout.prop(self, "decorator_font_scale") # Scene panel groups diff --git a/src/bonsai/bonsai/core/attribute.py b/src/bonsai/bonsai/core/attribute.py index 2bc2340cd8..803cc353bf 100644 --- a/src/bonsai/bonsai/core/attribute.py +++ b/src/bonsai/bonsai/core/attribute.py @@ -21,8 +21,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Union if TYPE_CHECKING: - import bpy - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/brick.py b/src/bonsai/bonsai/core/brick.py index a21c6d0656..95b673f88b 100644 --- a/src/bonsai/bonsai/core/brick.py +++ b/src/bonsai/bonsai/core/brick.py @@ -18,10 +18,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Union if TYPE_CHECKING: - import bpy import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/bsdd.py b/src/bonsai/bonsai/core/bsdd.py index 1cbf610ea7..55268c131f 100644 --- a/src/bonsai/bonsai/core/bsdd.py +++ b/src/bonsai/bonsai/core/bsdd.py @@ -18,12 +18,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: - import bpy - import bsdd - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/context.py b/src/bonsai/bonsai/core/context.py index 49815881aa..7ea77fc2e8 100644 --- a/src/bonsai/bonsai/core/context.py +++ b/src/bonsai/bonsai/core/context.py @@ -21,7 +21,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: - import bpy import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/cost.py b/src/bonsai/bonsai/core/cost.py index 6cd2d916c3..66067cf36e 100644 --- a/src/bonsai/bonsai/core/cost.py +++ b/src/bonsai/bonsai/core/cost.py @@ -21,7 +21,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Literal, Optional, Union if TYPE_CHECKING: - import bpy import ifcopenshell import ifcopenshell.util.cost diff --git a/src/bonsai/bonsai/core/covering.py b/src/bonsai/bonsai/core/covering.py index c308c861fc..a8c7a73f25 100644 --- a/src/bonsai/bonsai/core/covering.py +++ b/src/bonsai/bonsai/core/covering.py @@ -18,11 +18,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: - import bpy - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/debug.py b/src/bonsai/bonsai/core/debug.py index b79bd4f0e7..3702a9cca3 100644 --- a/src/bonsai/bonsai/core/debug.py +++ b/src/bonsai/bonsai/core/debug.py @@ -19,11 +19,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: - import bpy - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/document.py b/src/bonsai/bonsai/core/document.py index e38fc5d1e1..b7fe4ef2e5 100644 --- a/src/bonsai/bonsai/core/document.py +++ b/src/bonsai/bonsai/core/document.py @@ -18,7 +18,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: import ifcopenshell diff --git a/src/bonsai/bonsai/core/georeference.py b/src/bonsai/bonsai/core/georeference.py index e74b5f577d..f594f41416 100644 --- a/src/bonsai/bonsai/core/georeference.py +++ b/src/bonsai/bonsai/core/georeference.py @@ -18,17 +18,16 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: - import bpy - import ifcopenshell import bonsai.tool as tool def add_georeferencing(georeference: type[tool.Georeference]) -> None: georeference.add_georeferencing() + georeference.set_model_origin() def enable_editing_georeferencing(georeference: type[tool.Georeference]) -> None: @@ -37,8 +36,9 @@ def enable_editing_georeferencing(georeference: type[tool.Georeference]) -> None georeference.enable_editing() -def remove_georeferencing(ifc: type[tool.Ifc]) -> None: +def remove_georeferencing(ifc: type[tool.Ifc], georeference: type[tool.Georeference]) -> None: ifc.run("georeference.remove_georeferencing") + georeference.set_model_origin() def disable_editing_georeferencing(georeference: type[tool.Georeference]) -> None: diff --git a/src/bonsai/bonsai/core/ifcgit.py b/src/bonsai/bonsai/core/ifcgit.py index 703013e266..fb23652ebc 100644 --- a/src/bonsai/bonsai/core/ifcgit.py +++ b/src/bonsai/bonsai/core/ifcgit.py @@ -19,12 +19,11 @@ from __future__ import annotations import platform -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: import bpy import git - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/library.py b/src/bonsai/bonsai/core/library.py index e6c58bf0f7..e006db797e 100644 --- a/src/bonsai/bonsai/core/library.py +++ b/src/bonsai/bonsai/core/library.py @@ -18,7 +18,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: import bpy diff --git a/src/bonsai/bonsai/core/misc.py b/src/bonsai/bonsai/core/misc.py index 297b81eac8..a5f0deb2f3 100644 --- a/src/bonsai/bonsai/core/misc.py +++ b/src/bonsai/bonsai/core/misc.py @@ -18,11 +18,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: import bpy - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/model.py b/src/bonsai/bonsai/core/model.py index 6edce4d100..7b17d5de5d 100644 --- a/src/bonsai/bonsai/core/model.py +++ b/src/bonsai/bonsai/core/model.py @@ -22,7 +22,6 @@ from typing import TYPE_CHECKING, Literal, Optional if TYPE_CHECKING: import bpy - import ifcopenshell from mathutils import Vector import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/nest.py b/src/bonsai/bonsai/core/nest.py index 4ab491666b..ffa594e731 100644 --- a/src/bonsai/bonsai/core/nest.py +++ b/src/bonsai/bonsai/core/nest.py @@ -18,7 +18,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Union if TYPE_CHECKING: import bpy diff --git a/src/bonsai/bonsai/core/owner.py b/src/bonsai/bonsai/core/owner.py index 90383e7885..aeecb93a64 100644 --- a/src/bonsai/bonsai/core/owner.py +++ b/src/bonsai/bonsai/core/owner.py @@ -18,10 +18,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Union if TYPE_CHECKING: - import bpy import ifcopenshell from ifcopenshell.api.owner.add_actor import ACTOR_TYPE from ifcopenshell.api.owner.add_address import ADDRESS_TYPE diff --git a/src/bonsai/bonsai/core/patch.py b/src/bonsai/bonsai/core/patch.py index 4acee34494..1ea0a71d4a 100644 --- a/src/bonsai/bonsai/core/patch.py +++ b/src/bonsai/bonsai/core/patch.py @@ -19,11 +19,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: - import bpy - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/profile.py b/src/bonsai/bonsai/core/profile.py index e102360062..77f64c4606 100644 --- a/src/bonsai/bonsai/core/profile.py +++ b/src/bonsai/bonsai/core/profile.py @@ -18,11 +18,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: - import bpy - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/project.py b/src/bonsai/bonsai/core/project.py index 18dbd3fab2..0e4b044b5e 100644 --- a/src/bonsai/bonsai/core/project.py +++ b/src/bonsai/bonsai/core/project.py @@ -21,8 +21,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: - import bpy - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/pset.py b/src/bonsai/bonsai/core/pset.py index 6af9f1e1ee..62517b7ab2 100644 --- a/src/bonsai/bonsai/core/pset.py +++ b/src/bonsai/bonsai/core/pset.py @@ -19,7 +19,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Optional, Union +from typing import TYPE_CHECKING, Any, Union if TYPE_CHECKING: import bpy diff --git a/src/bonsai/bonsai/core/resource.py b/src/bonsai/bonsai/core/resource.py index cab03b7ebc..63f2e02b90 100644 --- a/src/bonsai/bonsai/core/resource.py +++ b/src/bonsai/bonsai/core/resource.py @@ -24,7 +24,6 @@ from collections.abc import Iterable from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: - import bpy import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/search.py b/src/bonsai/bonsai/core/search.py index cc16fb56f6..fb4771d92b 100644 --- a/src/bonsai/bonsai/core/search.py +++ b/src/bonsai/bonsai/core/search.py @@ -21,7 +21,6 @@ from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: - import bpy import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/sequence.py b/src/bonsai/bonsai/core/sequence.py index d62b4325ad..4e1ead7d08 100644 --- a/src/bonsai/bonsai/core/sequence.py +++ b/src/bonsai/bonsai/core/sequence.py @@ -21,7 +21,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional, Union if TYPE_CHECKING: - import bpy import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/spatial.py b/src/bonsai/bonsai/core/spatial.py index c464f84766..5086a8a872 100644 --- a/src/bonsai/bonsai/core/spatial.py +++ b/src/bonsai/bonsai/core/spatial.py @@ -18,7 +18,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Union, assert_never +from typing import TYPE_CHECKING, Optional, Union if TYPE_CHECKING: import bpy diff --git a/src/bonsai/bonsai/core/structural.py b/src/bonsai/bonsai/core/structural.py index c3f787905a..fb5a28fe24 100644 --- a/src/bonsai/bonsai/core/structural.py +++ b/src/bonsai/bonsai/core/structural.py @@ -18,10 +18,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Union if TYPE_CHECKING: - import bpy import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/style.py b/src/bonsai/bonsai/core/style.py index 20fd98509c..04ee971f97 100644 --- a/src/bonsai/bonsai/core/style.py +++ b/src/bonsai/bonsai/core/style.py @@ -18,7 +18,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: import bpy diff --git a/src/bonsai/bonsai/core/system.py b/src/bonsai/bonsai/core/system.py index 418c472645..f0a3ad0380 100644 --- a/src/bonsai/bonsai/core/system.py +++ b/src/bonsai/bonsai/core/system.py @@ -18,10 +18,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Union if TYPE_CHECKING: - import bpy import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/type.py b/src/bonsai/bonsai/core/type.py index a203b9ff76..cf8f3effbe 100644 --- a/src/bonsai/bonsai/core/type.py +++ b/src/bonsai/bonsai/core/type.py @@ -18,12 +18,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional - -import bonsai.core.geometry +from typing import TYPE_CHECKING if TYPE_CHECKING: - import bpy import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/unit.py b/src/bonsai/bonsai/core/unit.py index 863821e6f5..c0ecc46fcb 100644 --- a/src/bonsai/bonsai/core/unit.py +++ b/src/bonsai/bonsai/core/unit.py @@ -19,10 +19,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: - import bpy import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/web.py b/src/bonsai/bonsai/core/web.py index b0d9222460..81b9e8a90c 100644 --- a/src/bonsai/bonsai/core/web.py +++ b/src/bonsai/bonsai/core/web.py @@ -18,11 +18,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING if TYPE_CHECKING: - import bpy - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/tool/__init__.py b/src/bonsai/bonsai/tool/__init__.py index 68fbf0e381..06e498e8be 100644 --- a/src/bonsai/bonsai/tool/__init__.py +++ b/src/bonsai/bonsai/tool/__init__.py @@ -15,6 +15,9 @@ # # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +# +# Ignore unused imports. +# ruff: noqa: F401 from bonsai.tool.aggregate import Aggregate from bonsai.tool.attribute import Attribute diff --git a/src/bonsai/bonsai/tool/attribute.py b/src/bonsai/bonsai/tool/attribute.py index 426283ac9e..ce5cdba030 100644 --- a/src/bonsai/bonsai/tool/attribute.py +++ b/src/bonsai/bonsai/tool/attribute.py @@ -21,11 +21,8 @@ from __future__ import annotations from typing import ( TYPE_CHECKING, Any, - Literal, - Optional, TypeVar, Union, - assert_never, ) import bpy @@ -37,7 +34,6 @@ import bonsai.tool as tool if TYPE_CHECKING: from bonsai.bim.module.attribute.prop import ( - BIMAttributeProperties, BIMExplorerProperties, ) diff --git a/src/bonsai/bonsai/tool/bcf.py b/src/bonsai/bonsai/tool/bcf.py index c98cfeb4e0..7be0f97a5b 100644 --- a/src/bonsai/bonsai/tool/bcf.py +++ b/src/bonsai/bonsai/tool/bcf.py @@ -33,7 +33,6 @@ import bcf.v3.topic import bpy import bonsai.core.tool -import bonsai.tool as tool if TYPE_CHECKING: from bonsai.bim.module.bcf.prop import BCFProperties diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index e499f35106..205e592a35 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -74,7 +74,7 @@ if TYPE_CHECKING: BIMSolarProperties, RadianceExporterProperties, ) - from bonsai.bim.prop import BIMObjectProperties, BIMProperties, BIMSnapProperties + from bonsai.bim.prop import BIMObjectProperties, BIMProperties T = TypeVar("T") @@ -1571,13 +1571,22 @@ class Blender(bonsai.core.tool.Blender): return getattr(scene, "sun_pos_properties", None) @classmethod - def scale_font_size(cls, size): + def scale_font_size(cls, size=None): default_dpi = 72 default_pixel_size = 1.0 + ui_style = bpy.context.preferences.ui_styles[0] + base_size = ui_style.widget.points if size is None else size + platform_scale = 0.5 if sys.platform == "darwin" else 1 + default_scale = default_dpi * default_pixel_size system = bpy.context.preferences.system system_scale = system.dpi * system.pixel_size - return (system_scale / default_scale) * size + return ( + (system_scale / default_scale) + * base_size + * platform_scale + * tool.Blender.get_addon_preferences().decorator_font_scale + ) @classmethod def apply_transform_as_local(cls, obj: bpy.types.Object) -> bool: @@ -2165,3 +2174,42 @@ class Blender(bonsai.core.tool.Blender): if cls.BLENDER_5: return np.array(mathutils_type) return np.array(mathutils_type, dtype=np.float32) + + @classmethod + def get_selected_files( + cls, directory: str, files: bpy.types.OperatorFileListElement, use_relative_path=False + ) -> list[Path]: + return [ + tool.Ifc.get_uri(Path(directory) / f.name, use_relative_path=use_relative_path) + for f in files + if (Path(directory) / f.name).is_file() + ] + + @classmethod + def ray_cast_scene( + cls, + context: bpy.types.Context, + origin: Vector, + direction: Vector, + ) -> tuple[bool, Vector, Vector, int, bpy.types.Object, Matrix]: + """ + + The returned matrix is just ``obj.matrix_world``. + The returned object is not evaluated by the current depsgraph, + e.g. if object is modified by the depsgraph (e.g. by modifiers) + object has to be evaluated first (`obj.evaluated_get(depsgraph)`). + """ + depsgraph = context.evaluated_depsgraph_get() + assert context.scene + result = context.scene.ray_cast( + depsgraph, + origin, + direction, + ) + return result + + @classmethod + def depsgraph_evaluate(cls, obj: bpy.types.Object) -> bpy.types.Object: + depsgraph = bpy.context.evaluated_depsgraph_get() + evaluated_obj = obj.evaluated_get(depsgraph) + return evaluated_obj diff --git a/src/bonsai/bonsai/tool/brick.py b/src/bonsai/bonsai/tool/brick.py index 42b80cc288..7ef2c30444 100644 --- a/src/bonsai/bonsai/tool/brick.py +++ b/src/bonsai/bonsai/tool/brick.py @@ -37,13 +37,11 @@ import bonsai.core.tool import bonsai.tool as tool try: - import urllib.parse import brickschema import brickschema.persistent from brickschema.namespaces import REF, A from rdflib import BNode, Literal, Namespace, URIRef - from rdflib.namespace import RDF except: # See #1860 print("Warning: brickschema not available.") @@ -166,17 +164,13 @@ class Brick(bonsai.core.tool.Brick): @classmethod def export_brick_attributes(cls, brick_uri: str) -> dict[str, Any]: - query = BrickStore.graph.query( - """ + query = BrickStore.graph.query(""" PREFIX rdfs: SELECT ?label { <{brick_uri}> rdfs:label ?label . } LIMIT 1 - """.replace( - "{brick_uri}", brick_uri - ) - ) + """.replace("{brick_uri}", brick_uri)) name = None for row in query: name = str(row.get("label")) @@ -220,18 +214,14 @@ class Brick(bonsai.core.tool.Brick): @classmethod def get_brickifc_project(cls) -> Union[str, None]: project = tool.Ifc.get().by_type("IfcProject")[0] - query = BrickStore.graph.query( - """ + query = BrickStore.graph.query(""" PREFIX ref: SELECT ?proj WHERE { ?proj a ref:ifcProject . ?proj ref:ifcProjectID "{project_globalid}" . } LIMIT 1 - """.replace( - "{project_globalid}", project.GlobalId - ) - ) + """.replace("{project_globalid}", project.GlobalId)) results = list(query) if results: return results[0][0].toPython() @@ -277,17 +267,13 @@ class Brick(bonsai.core.tool.Brick): @classmethod def get_item_class(cls, item: str) -> Union[str, None]: - query = BrickStore.graph.query( - """ + query = BrickStore.graph.query(""" PREFIX brick: SELECT ?class WHERE { <{item}> a ?class . } LIMIT 1 - """.replace( - "{item}", item - ) - ) + """.replace("{item}", item)) for row in query: return row.get("class").toPython().split("#")[-1] @@ -310,8 +296,7 @@ class Brick(bonsai.core.tool.Brick): @classmethod def import_brick_classes(cls, brick_class: str, split_screen: bool = False) -> None: - query = BrickStore.graph.query( - """ + query = BrickStore.graph.query(""" PREFIX brick: PREFIX rdfs: PREFIX rdf: @@ -324,10 +309,7 @@ class Brick(bonsai.core.tool.Brick): } GROUP BY ?group ORDER BY asc(?group) - """.replace( - "{brick_class}", brick_class - ) - ) + """.replace("{brick_class}", brick_class)) props = tool.Brick.get_brick_props() if split_screen: bricks = props.split_screen_bricks @@ -344,8 +326,7 @@ class Brick(bonsai.core.tool.Brick): @classmethod def import_brick_items(cls, brick_class: str, split_screen: bool = False) -> None: - query = BrickStore.graph.query( - """ + query = BrickStore.graph.query(""" PREFIX brick: PREFIX rdfs: PREFIX rdf: @@ -356,10 +337,7 @@ class Brick(bonsai.core.tool.Brick): } } ORDER BY asc(?item) - """.replace( - "{brick_class}", brick_class - ) - ) + """.replace("{brick_class}", brick_class)) props = tool.Brick.get_brick_props() if split_screen: bricks = props.split_screen_bricks @@ -509,8 +487,7 @@ class BrickStore: @classmethod def load_sub_roots(cls) -> None: - query = BrickStore.graph.query( - """ + query = BrickStore.graph.query(""" PREFIX brick: PREFIX rdfs: SELECT ?subRoot ?subClasses WHERE { @@ -527,8 +504,7 @@ class BrickStore: } FILTER(?subClasses > 3) } - """ - ) + """) for row in query: sub_root = row.get("subRoot").toPython().split("#")[-1] BrickStore.root_classes.append(sub_root) @@ -560,8 +536,7 @@ class BrickStore: @classmethod def load_entity_classes(cls) -> None: for root_class in BrickStore.root_classes: - query = BrickStore.graph.query( - """ + query = BrickStore.graph.query(""" PREFIX brick: PREFIX rdfs: PREFIX owl: @@ -571,25 +546,20 @@ class BrickStore: ?class owl:deprecated true . } } - """.replace( - "{root_class}", root_class - ) - ) + """.replace("{root_class}", root_class)) BrickStore.entity_classes[root_class] = [] for uri in sorted([x[0].toPython() for x in query]): BrickStore.entity_classes[root_class].append(uri) @classmethod def load_relationships(cls) -> None: - query = BrickStore.graph.query( - """ + query = BrickStore.graph.query(""" PREFIX brick: PREFIX rdfs: SELECT DISTINCT ?relation WHERE { ?relation rdfs:subPropertyOf brick:Relationship . } - """ - ) + """) for uri in sorted([x[0].toPython() for x in query]): BrickStore.relationships.append(uri) diff --git a/src/bonsai/bonsai/tool/bsdd.py b/src/bonsai/bonsai/tool/bsdd.py index dc42708867..47c948abcc 100644 --- a/src/bonsai/bonsai/tool/bsdd.py +++ b/src/bonsai/bonsai/tool/bsdd.py @@ -123,6 +123,10 @@ class Bsdd(bonsai.core.tool.Bsdd): @classmethod def get_dictionaries(cls) -> list[bsdd.DictionaryContractV1]: prefs = tool.Blender.get_addon_preferences() + baseurl = getattr(prefs, "bsdd_baseurl", "https://api.bsdd.buildingsmart.org/api/") + cls.client = bsdd.Client() + if hasattr(cls.client, "baseurl"): + cls.client.baseurl = baseurl response = cls.client.get_dictionary(include_test_dictionaries=prefs.bsdd_load_test_dictionaries) dicts = response.get("dictionaries") or [] statuses = ["Active"] diff --git a/src/bonsai/bonsai/tool/cad.py b/src/bonsai/bonsai/tool/cad.py index 4e3d8e1499..13678df15a 100644 --- a/src/bonsai/bonsai/tool/cad.py +++ b/src/bonsai/bonsai/tool/cad.py @@ -30,7 +30,6 @@ from __future__ import annotations -import itertools import math import sys from typing import TYPE_CHECKING, Union diff --git a/src/bonsai/bonsai/tool/clash.py b/src/bonsai/bonsai/tool/clash.py index 7c9961646c..fb64e82b49 100644 --- a/src/bonsai/bonsai/tool/clash.py +++ b/src/bonsai/bonsai/tool/clash.py @@ -19,12 +19,9 @@ from __future__ import annotations import json -import os -from contextlib import contextmanager -from typing import TYPE_CHECKING, Literal, Union, get_args +from typing import TYPE_CHECKING, Literal, Union import bpy -import ifcopenshell from ifcclash import ifcclash from ifcclash.ifcclash import ClashSource from mathutils import Vector diff --git a/src/bonsai/bonsai/tool/classification.py b/src/bonsai/bonsai/tool/classification.py index 7b2ebb073e..9264e5bee2 100644 --- a/src/bonsai/bonsai/tool/classification.py +++ b/src/bonsai/bonsai/tool/classification.py @@ -22,10 +22,8 @@ from typing import TYPE_CHECKING, Union, assert_never import bpy import ifcopenshell.api -import ifcopenshell.util.classification import bonsai.core.tool -import bonsai.tool as tool if TYPE_CHECKING: from bonsai.bim.module.classification.prop import ( diff --git a/src/bonsai/bonsai/tool/collector.py b/src/bonsai/bonsai/tool/collector.py index 74a55d7dc1..b0aa358629 100644 --- a/src/bonsai/bonsai/tool/collector.py +++ b/src/bonsai/bonsai/tool/collector.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -from typing import Union import bpy import ifcopenshell.util.element diff --git a/src/bonsai/bonsai/tool/covering.py b/src/bonsai/bonsai/tool/covering.py index 018a39eb46..28e2a80716 100644 --- a/src/bonsai/bonsai/tool/covering.py +++ b/src/bonsai/bonsai/tool/covering.py @@ -21,7 +21,6 @@ from __future__ import annotations from typing import TYPE_CHECKING import bpy -import ifcopenshell import ifcopenshell.util.element import bonsai.core.tool diff --git a/src/bonsai/bonsai/tool/document.py b/src/bonsai/bonsai/tool/document.py index 5d0aaf1a8e..910ca92cf4 100644 --- a/src/bonsai/bonsai/tool/document.py +++ b/src/bonsai/bonsai/tool/document.py @@ -251,11 +251,19 @@ class Document(bonsai.core.tool.Document): def get_document_references( cls, document: ifcopenshell.entity_instance ) -> tuple[ifcopenshell.entity_instance, ...]: + # TODO: migrate to util.document and replace all instances """Get IfcDocumentReference.ReferencedDocuments, compatible with IFC2X3.""" if document.file.schema == "IFC2X3": return document.DocumentReferences or () return document.HasDocumentReferences + @classmethod + def get_reference_document(cls, reference: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance | None: + # TODO: migrate to util.document and replace all instances + if reference.file.schema == "IFC2X3": + return (reference.ReferenceToDocument or (None))[0] + return reference.ReferencedDocument + @classmethod def clear_active_document(cls) -> None: props = cls.get_document_props() diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index a7238c75dc..b8ec26fd37 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -18,8 +18,6 @@ from __future__ import annotations -import collections -import collections.abc import json import logging import math @@ -857,9 +855,11 @@ class Drawing(bonsai.core.tool.Drawing): def edit_text_literals(cls, obj: bpy.types.Object, literal_attributes: dict) -> None: assert (element := tool.Ifc.get_entity(obj)) assert (rep := cls.get_annotation_representation(element)) - for literal in cls.get_text_literal(obj, return_list=True): + to_remove = [i for i in rep.Items if i.is_a("IfcTextLiteral")] + new_literals = [cls.add_literal(**a) for a in literal_attributes] + rep.Items = [i for i in rep.Items if not i.is_a("IfcTextLiteral")] + new_literals + for literal in to_remove: ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), literal) - rep.Items = [cls.add_literal(**a) for a in literal_attributes] @classmethod def add_literal(cls, **attributes: str) -> ifcopenshell.entity_instance: @@ -2710,16 +2710,14 @@ class Drawing(bonsai.core.tool.Drawing): return float(value) except: pass # Perhaps it's imperial? - l = lark.Lark( - """start: feet? "-"? inches? + l = lark.Lark("""start: feet? "-"? inches? feet: NUMBER? "-"? fraction? "'" inches: NUMBER? "-"? fraction? "\\"" fraction: NUMBER "/" NUMBER %import common.NUMBER %import common.WS %ignore WS // Disregard spaces in text - """ - ) + """) try: start = l.parse(value) diff --git a/src/bonsai/bonsai/tool/feature.py b/src/bonsai/bonsai/tool/feature.py index d58a8df55c..3a06cbf625 100644 --- a/src/bonsai/bonsai/tool/feature.py +++ b/src/bonsai/bonsai/tool/feature.py @@ -22,11 +22,9 @@ from collections.abc import Iterable from typing import TYPE_CHECKING import bpy -import ifcopenshell import ifcopenshell.api.feature import ifcopenshell.util.representation -import bonsai.bim.helper import bonsai.core.geometry import bonsai.core.tool import bonsai.tool as tool diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 13ec6695d2..e24152d642 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -39,7 +39,6 @@ from typing import ( import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.boundary import ifcopenshell.api.geometry import ifcopenshell.api.grid @@ -81,7 +80,7 @@ if TYPE_CHECKING: BIMGeometryProperties, BIMObjectGeometryProperties, ) - from bonsai.bim.prop import Attribute, BIMMeshProperties + from bonsai.bim.prop import BIMMeshProperties class Geometry(bonsai.core.tool.Geometry): diff --git a/src/bonsai/bonsai/tool/georeference.py b/src/bonsai/bonsai/tool/georeference.py index eefdd12cd9..0c47002a36 100644 --- a/src/bonsai/bonsai/tool/georeference.py +++ b/src/bonsai/bonsai/tool/georeference.py @@ -22,7 +22,6 @@ import json from typing import TYPE_CHECKING, Any, Literal, Union import bpy -import ifcopenshell import ifcopenshell.api.georeference import ifcopenshell.util.geolocation import ifcopenshell.util.placement @@ -315,6 +314,23 @@ class Georeference(bonsai.core.tool.Georeference): ) return coordinates + @classmethod + def global2local(cls, matrix, is_specified_in_map_units: bool) -> tuple[float, float, float]: + matrix = ifcopenshell.util.geolocation.auto_global2local( + tool.Ifc.get(), matrix, is_specified_in_map_units=is_specified_in_map_units + ) + props = cls.get_georeference_props() + if props.has_blender_offset: + matrix = ifcopenshell.util.geolocation.global2local( + matrix, + float(props.blender_offset_x), + float(props.blender_offset_y), + float(props.blender_offset_z), + float(props.blender_x_axis_abscissa), + float(props.blender_x_axis_ordinate), + ) + return matrix + @classmethod def import_plot(cls, filepath: str) -> None: import bmesh @@ -385,6 +401,9 @@ class Georeference(bonsai.core.tool.Georeference): unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) gprops = tool.Georeference.get_georeference_props() e, n, h = cls.xyz2enh((0, 0, 0), should_return_in_map_units=False) + crs = ifcopenshell.util.geolocation.get_crs(tool.Ifc.get()) or {} + gprops.model_is_georeferenced = bool(crs) + gprops.model_crs = crs.get("Name", "") or "" gprops.model_origin = f"{e},{n},{h}" gprops.model_origin_si = f"{e * unit_scale},{n * unit_scale},{h * unit_scale}" angle = ifcopenshell.util.geolocation.get_grid_north(tool.Ifc.get()) diff --git a/src/bonsai/bonsai/tool/group.py b/src/bonsai/bonsai/tool/group.py index f0f3664f6f..3e221b57aa 100644 --- a/src/bonsai/bonsai/tool/group.py +++ b/src/bonsai/bonsai/tool/group.py @@ -25,7 +25,6 @@ import bpy import ifcopenshell from natsort import natsorted -import bonsai.bim.helper import bonsai.core.tool import bonsai.tool as tool diff --git a/src/bonsai/bonsai/tool/layer.py b/src/bonsai/bonsai/tool/layer.py index 396cc13605..a49bf8bf05 100644 --- a/src/bonsai/bonsai/tool/layer.py +++ b/src/bonsai/bonsai/tool/layer.py @@ -23,7 +23,6 @@ from typing import TYPE_CHECKING import bpy import bonsai.core.tool -import bonsai.tool as tool if TYPE_CHECKING: from bonsai.bim.module.layer.prop import BIMLayerProperties diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index d81fec5fd2..94b15e73b7 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -179,7 +179,8 @@ class Loader(bonsai.core.tool.Loader): def surface_texture_to_dict(cls, surface_texture): if isinstance(surface_texture, dict): return surface_texture - mappings = surface_texture.IsMappedBy or [] + # IsMappedBy is an IFC4+ inverse attribute, not available in IFC2X3. + mappings = getattr(surface_texture, "IsMappedBy", None) or [] surface_texture = surface_texture.get_info() uv_mode = None if mappings: @@ -188,7 +189,7 @@ class Loader(bonsai.core.tool.Loader): uv_mode = "Generated" elif coordinates.is_a("IfcTextureCoordinateGenerator") and coordinates.Mode == "COORD-EYE": uv_mode = "Camera" - surface_texture["uv_mode"] = uv_mode or "UV" + surface_texture["uv_mode"] = uv_mode or "Generated" return surface_texture @classmethod @@ -286,6 +287,9 @@ class Loader(bonsai.core.tool.Loader): for texture in textures: mode = texture.get("Mode", None) + # IFC2X3 IfcImageTexture has no Mode attribute; default to DIFFUSE. + if mode is None and texture["type"] == "IfcImageTexture": + mode = "DIFFUSE" node = None image_url = None @@ -293,7 +297,8 @@ class Loader(bonsai.core.tool.Loader): def get_image() -> Union[bpy.types.Image, None]: # TODO: orphaned textures after shader recreated? if texture["type"] == "IfcImageTexture": - original_image_url = texture["URLReference"] + # IFC2X3 uses UrlReference, IFC4+ uses URLReference. + original_image_url = texture.get("URLReference") or texture.get("UrlReference", "") is_relative = not os.path.isabs(original_image_url) nonlocal image_url image_url = Path(original_image_url) @@ -539,6 +544,33 @@ class Loader(bonsai.core.tool.Loader): for colour in colours: cls.load_indexed_map(colour, mesh) + @classmethod + def load_generated_uv_map(cls, mesh: bpy.types.Mesh) -> None: + bm = bmesh.new() + bm.from_mesh(mesh) + uv_layer = bm.loops.layers.uv.active or bm.loops.layers.uv.new("UVMap") + + all_verts = [v.co for v in bm.verts] + if not all_verts: + bm.free() + return + + min_x = min(v.x for v in all_verts) + max_x = max(v.x for v in all_verts) + min_y = min(v.y for v in all_verts) + max_y = max(v.y for v in all_verts) + width = max_x - min_x + height = max_y - min_y + + for face in bm.faces: + for loop in face.loops: + u = (loop.vert.co.x - min_x) / width if width > 0 else 0.5 + v = (loop.vert.co.y - min_y) / height if height > 0 else 0.5 + loop[uv_layer].uv = (max(0.0, min(1.0, u)), max(0.0, min(1.0, v))) + + bm.to_mesh(mesh) + bm.free() + @classmethod def load_indexed_map(cls, index_map: ifcopenshell.entity_instance, mesh: bpy.types.Mesh) -> None: """Add data from index map as blender mesh attribute. diff --git a/src/bonsai/bonsai/tool/material.py b/src/bonsai/bonsai/tool/material.py index 6f862100e2..e09462a8c2 100644 --- a/src/bonsai/bonsai/tool/material.py +++ b/src/bonsai/bonsai/tool/material.py @@ -38,7 +38,6 @@ if TYPE_CHECKING: BIMMaterialProperties, BIMObjectMaterialProperties, ) - from bonsai.bim.module.material.prop import Material as MaterialItem class Material(bonsai.core.tool.Material): diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 148f4a6b75..23c0071b1b 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -18,7 +18,6 @@ from __future__ import annotations -import collections import collections.abc import json from collections.abc import Iterable, Sequence @@ -38,7 +37,6 @@ from typing import ( import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.geometry import ifcopenshell.api.grid import ifcopenshell.api.pset @@ -1244,18 +1242,7 @@ class Model(bonsai.core.tool.Model): height = 100 - is_horizontal = False - if element.is_a("IfcSlabType"): - is_horizontal = True - - parametric = ifcopenshell.util.element.get_psets(element).get("EPset_Parametric") - if parametric: - layer_set_direction = parametric.get("LayerSetDirection", None) - if layer_set_direction == "AXIS2": - is_horizontal = False - elif layer_set_direction == "AXIS3": - is_horizontal = True - + is_horizontal = cls.get_usage_type(element) == "LAYER3" if is_horizontal: width, height = height, width @@ -1266,7 +1253,7 @@ class Model(bonsai.core.tool.Model): del thicknesses[-1] for thickness in thicknesses: current_thickness += thickness - if element.is_a("IfcSlabType"): + if is_horizontal: y = (current_thickness / total_thickness) * height line = [x_offset, y_offset + y, x_offset + width, y_offset + y] else: @@ -2637,7 +2624,7 @@ class Model(bonsai.core.tool.Model): reference_obj: bpy.types.Object, objs: Iterable[bpy.types.Object], align_type: Literal["CENTER", "POSITIVE", "NEGATIVE"], - ): + ) -> None: if align_type == "CENTER": point = reference_obj.matrix_world @ (Vector(reference_obj.bound_box[0]) + (reference_obj.dimensions / 2)) elif align_type == "POSITIVE": @@ -2782,7 +2769,7 @@ class Model(bonsai.core.tool.Model): SvIfcStore.use_bonsai_file = False @classmethod - def create_bmesh_from_vertices(cls, vertices, is_closed=False): + def create_bmesh_from_vertices(cls, vertices: list[Vector], is_closed: bool = False) -> bmesh.types.BMesh: bm = bmesh.new() new_verts = [bm.verts.new(v) for v in vertices] diff --git a/src/bonsai/bonsai/tool/owner.py b/src/bonsai/bonsai/tool/owner.py index 9ed0bda219..9f8cf4ab7c 100644 --- a/src/bonsai/bonsai/tool/owner.py +++ b/src/bonsai/bonsai/tool/owner.py @@ -18,7 +18,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Literal, Union, assert_never +from typing import TYPE_CHECKING, Any, Literal, Union import bpy import ifcopenshell diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index b288260d7d..5a91fc79b8 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -18,12 +18,10 @@ import math from dataclasses import dataclass, field -from math import cos, degrees, radians, sin, tan +from math import radians from typing import Literal, Optional, Union -import bmesh import bpy -import ifcopenshell import ifcopenshell.util.unit from lark import Lark, Transformer from mathutils import Matrix, Vector @@ -191,7 +189,8 @@ class Polyline(bonsai.core.tool.Polyline): orientation_angle = 0 if input_ui: if should_round: - angle = 5 * round(angle / 5) if distance < angle_round_threshold else angle + angle_snap = tool.Snap.get_angle_snap_value(context) + angle = angle_snap * round(angle / angle_snap) if distance < angle_round_threshold else angle factor = tool.Snap.get_increment_snap_value(context) distance = factor * round(distance / factor) input_ui.set_value("X", mouse_vector.x) diff --git a/src/bonsai/bonsai/tool/profile.py b/src/bonsai/bonsai/tool/profile.py index 99d2164a32..4c7c2fc448 100644 --- a/src/bonsai/bonsai/tool/profile.py +++ b/src/bonsai/bonsai/tool/profile.py @@ -25,9 +25,6 @@ import ifcopenshell import ifcopenshell.api.profile import ifcopenshell.geom import ifcopenshell.ifcopenshell_wrapper as W -import ifcopenshell.util.element -import ifcopenshell.util.placement -import ifcopenshell.util.representation import ifcopenshell.util.shape import ifcopenshell.util.unit import numpy as np diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index d4df4c80de..d92bf19cf5 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -18,19 +18,24 @@ from __future__ import annotations +import json import os import shutil from collections import defaultdict +from math import radians from pathlib import Path -from typing import TYPE_CHECKING, NamedTuple, Optional, Union +from typing import TYPE_CHECKING, Any, NamedTuple, Optional import bpy import ifcopenshell import ifcopenshell.api.document import ifcopenshell.util.element import ifcopenshell.util.representation -import ifcopenshell.util.unit +import ifcopenshell.util.shape_builder +import numpy as np +import numpy.typing as npt from ifcopenshell.api.project.append_asset import APPENDABLE_ASSET_TYPES +from mathutils import Matrix import bonsai.bim.schema import bonsai.core.aggregate @@ -43,7 +48,11 @@ import bonsai.tool as tool from bonsai.bim.ifc import IfcStore if TYPE_CHECKING: - from bonsai.bim.module.project.prop import BIMProjectProperties, MeasureToolSettings + from bonsai.bim.module.project.prop import ( + BIMProjectProperties, + Link, + MeasureToolSettings, + ) HiearchyDict = dict[ifcopenshell.entity_instance, "HiearchyDict"] @@ -58,6 +67,47 @@ class Project(bonsai.core.tool.Project): assert (scene := bpy.context.scene) return scene.MeasureToolSettings # pyright: ignore[reportAttributeAccessIssue] + @classmethod + def get_link_empty_handle(cls, link: Link) -> bpy.types.Object | None: + if tool.Ifc.get(): + return tool.Ifc.get_object(tool.Ifc.get().by_id(link.ifc_definition_id)) + return link.empty_handle + + @classmethod + def set_link_empty_handle(cls, link: Link, empty: bpy.types.Object) -> None: + if tool.Ifc.get(): + tool.Ifc.link(tool.Ifc.get().by_id(link.ifc_definition_id), empty) + else: + link.empty_handle = empty + + @classmethod + def calculate_link_matrix(cls, link: Link) -> Matrix: + filepath = Path(tool.Ifc.resolve_uri(link.filepath)) + with open(filepath.with_suffix(".ifc.cache.json"), "r") as f: + metadata = json.load(f) + + rot = ifcopenshell.util.shape_builder.np_rotation_matrix( + radians(-float(metadata["model_project_north"])), 4, "Z" + ) + global_matrix = rot @ np.eye(4) + global_matrix[:, 3][:3] = [float(o) for o in metadata["model_origin_si"].split(",")] + + if tool.Ifc.get(): + transformation = tool.Ifc.get().by_id(link.ifc_definition_id)[1] # Identification + else: + transformation = link.transformation + + if transformation: + transformation = np.fromstring(transformation, sep=",", dtype=np.float64).reshape(4, 4) + if not np.allclose(transformation, np.eye(4)): + global_matrix = transformation @ global_matrix + + gprops = tool.Georeference.get_georeference_props() + rot = ifcopenshell.util.shape_builder.np_rotation_matrix(radians(-float(gprops.model_project_north)), 4, "Z") + local_matrix = rot @ np.eye(4) + local_matrix[:, 3][:3] = [float(o) for o in gprops.model_origin_si.split(",")] + return Matrix(np.linalg.inv(local_matrix) @ global_matrix) + @classmethod def append_all_types_from_template(cls, template: str) -> None: # TODO refactor @@ -249,68 +299,32 @@ class Project(bonsai.core.tool.Project): tool.Root.reload_grid_decorator() @classmethod - def get_linked_models_document(cls) -> Union[ifcopenshell.entity_instance, None]: - for document in tool.Ifc.get().by_type("IfcDocumentInformation"): - if document.Name == "BBIM_Linked_Models": - return document + def get_linked_models_documents(cls) -> dict[str, ifcopenshell.entity_instance]: + linked_docs = {} + for doc in tool.Ifc.get().by_type("IfcDocumentInformation"): + if doc.Scope == "LINKED_MODEL": + for reference in tool.Drawing.get_document_references(doc): + linked_docs[Path(reference.Location).as_posix()] = doc + break + return linked_docs @classmethod def load_linked_models_from_ifc(cls) -> None: links = tool.Project.get_project_props().links links.clear() - links_document = cls.get_linked_models_document() - if not links_document: - return - - references = tool.Document.get_document_references(links_document) - if not references: - return - - for reference in references: - link = links.add() - link.name = reference.Location - - @classmethod - def save_linked_models_to_ifc(cls) -> None: - ifc_file = tool.Ifc.get() - links = tool.Project.get_project_props().links - filepaths: set[Path] = set() - for link in links: - filepaths.add(Path(link.name)) - - links_document = cls.get_linked_models_document() - - if not filepaths and links_document is None: - return - - paths_to_add = filepaths.copy() - references_to_remove: list[ifcopenshell.entity_instance] = [] - if links_document: - references = tool.Document.get_document_references(links_document) - for reference in references: - # I guess got corrupted by the user. - if not (location := reference.Location): - references_to_remove.remove(reference) - continue - path = Path(location) - if path in paths_to_add: - paths_to_add.remove(path) - else: - references_to_remove.append(reference) - - if paths_to_add: - if links_document is None: - links_document = ifcopenshell.api.document.add_information(ifc_file) - links_document.Name = "BBIM_Linked_Models" - links_document.Description = "Bonsai internal document containing references to currently linked models" - - for path in paths_to_add: - reference = ifcopenshell.api.document.add_reference(ifc_file, links_document) - reference.Location = path.as_posix() - - if references_to_remove: - for reference in references_to_remove: - ifcopenshell.api.document.remove_reference(ifc_file, reference) + for doc in tool.Ifc.get().by_type("IfcDocumentInformation"): + if doc.Scope != "LINKED_MODEL": + continue + for reference in tool.Drawing.get_document_references(doc): + filepath = reference.Location + link = links.add() + link.name = filepath + link.filepath = filepath + link.ifc_definition_id = reference.id() + link.has_transformation = False + if reference[1]: + m = np.fromstring(reference[1], sep=",", dtype=np.float64).reshape(4, 4) + link.has_transformation = not np.allclose(m, np.eye(4)) @classmethod def get_project_library_elements( @@ -489,8 +503,8 @@ class Project(bonsai.core.tool.Project): ) @classmethod - def get_clipping_planes_normals(cls): - normals = [] + def get_clipping_planes_normals(cls) -> list[tuple[Vector, Vector]]: + normals: list[tuple[Vector, Vector]] = [] for clipping_plane in tool.Project.get_project_props().clipping_planes: plane = clipping_plane.obj if not plane or not plane.data: @@ -499,6 +513,7 @@ class Project(bonsai.core.tool.Project): if plane.mode == "EDIT": continue # A profile decorator or something else is used here. + assert isinstance(plane.data, bpy.types.Mesh) v1 = plane.matrix_world @ plane.data.vertices[0].co v2 = plane.matrix_world @ plane.data.vertices[1].co v3 = plane.matrix_world @ plane.data.vertices[2].co @@ -533,11 +548,11 @@ class Project(bonsai.core.tool.Project): if not ifc_file: raise Exception("No IFC file loaded") - doc = tool.Ifc.run("document.add_information", parent=None) + doc = ifcopenshell.api.document.add_information(ifc_file, parent=None) if ifc_file.schema == "IFC2X3": - tool.Ifc.run( - "document.edit_information", + ifcopenshell.api.document.edit_information( + ifc_file, information=doc, attributes={ "DocumentId": "BLEND_METADATA", @@ -548,8 +563,8 @@ class Project(bonsai.core.tool.Project): }, ) else: - tool.Ifc.run( - "document.edit_information", + ifcopenshell.api.document.edit_information( + ifc_file, information=doc, attributes={ "Identification": "BLEND_METADATA", @@ -569,13 +584,293 @@ class Project(bonsai.core.tool.Project): return ifc_file = tool.Ifc.get() - if not ifc_file: - return - - tool.Ifc.run("document.edit_information", information=doc, attributes={"Location": metadata_filename}) + ifcopenshell.api.document.edit_information( + ifc_file, information=doc, attributes={"Location": metadata_filename} + ) @classmethod def remove_metadata_document_information(cls) -> None: doc = cls.get_metadata_document_information() - if doc: - tool.Ifc.run("document.remove_information", information=doc) + if not doc: + return + + ifc_file = tool.Ifc.get() + ifcopenshell.api.document.remove_information(ifc_file, information=doc) + + class Link: + """Tools for working with linked models.""" + + @classmethod + def is_linked_element(cls, obj: bpy.types.Object) -> bool: + return "guids" in obj + + @classmethod + def get_obj_by_guid(cls, link: Link, guid: str) -> bpy.types.Object | None: + assert link.is_loaded + + handle = tool.Project.get_link_empty_handle(link) + assert handle + col = handle.instance_collection + assert col + + guid_obj = None + for obj in col.objects: + obj_guids: list[str] = obj["guids"] + if guid in obj_guids: + guid_obj = obj + break + + return guid_obj + + @classmethod + def get_linked_element_guid_ids(cls, obj: bpy.types.Object, *, skip_hidden: bool) -> npt.NDArray[np.int64]: + obj_guid_ids: npt.NDArray[np.int64] = np.array(obj["guid_ids"]) + + if not skip_hidden: + return obj_guid_ids + + # 'hidden_indices' is needed, because otherwise we can't make sense of 'guid_ids', + # since part of the geometry is hidden. + obj_hidden_indices: list[int] = list(obj.get("hidden_indices") or []) + + if not obj_hidden_indices: + return obj_guid_ids + + # Skip hidden geometry indices. + hidden_indices_mask = np.zeros(len(obj_guid_ids), dtype=bool) + hidden_indices_mask[obj_hidden_indices] = True + deltas = np.diff(obj_guid_ids, prepend=0) + deltas[~hidden_indices_mask] = 0 + obj_guid_ids -= np.cumsum(deltas) + return obj_guid_ids + + @classmethod + def get_guid_by_face_index(cls, obj: bpy.types.Object, face_index: int) -> str | None: + guids: list[str] = obj["guids"] + guid_ids = cls.get_linked_element_guid_ids(obj, skip_hidden=True) + for guid, guid_end_index in zip(guids, guid_ids): + if face_index < guid_end_index: + return guid + + @classmethod + def get_linked_element_geom_slice(cls, obj: bpy.types.Object, guid: str) -> slice[int, int]: + """ + Get slice for ``obj.data.polygons`` for the provided ``guid``. + """ + obj_guids: list[str] = obj["guids"] + + # Just to be safe. + obj_hidden_indices: list[int] = obj.get("hidden_indices") or [] + index = obj_guids.index(guid) + if index in obj_hidden_indices: + assert False, "Unexpected. Why would you need the geometry for the hidden element?" + obj_guid_ids = cls.get_linked_element_guid_ids(obj, skip_hidden=False) + guid_end_index = obj_guid_ids[index] + guid_start_index = index and obj_guid_ids[index - 1] + return slice(guid_start_index, guid_end_index) + + @classmethod + def hide_linked_element(cls, obj: bpy.types.Object, guid: str) -> None: + verts = tool.Project.Link.get_linked_element_verts(obj, guid) + + # `MeshPolygon.hide` works only in EDIT mode, + # so we use vertex groups + Mask modifier. + MODIFIER_VG_NAME = "BBIM_HIDE_LINKED_GEOMETRY" + + vertex_groups = obj.vertex_groups + vertex_group = vertex_groups.get(MODIFIER_VG_NAME) + if vertex_group is None: + vertex_group = vertex_groups.new(name=MODIFIER_VG_NAME) + + modifiers = obj.modifiers + modifier = modifiers.get(MODIFIER_VG_NAME) + if modifier is None: + modifier = modifiers.new(MODIFIER_VG_NAME, "MASK") + assert isinstance(modifier, bpy.types.MaskModifier) + modifier.vertex_group = MODIFIER_VG_NAME + modifier.invert_vertex_group = True + + vertex_group.add(verts, 1.0, "REPLACE") + + hidden_indices: list[int] = list(obj.get("hidden_indices") or []) + guid_ids: list[str] = obj["guids"] + index = guid_ids.index(guid) + hidden_indices.append(index) + obj["hidden_indices"] = hidden_indices + + @classmethod + def unhide_all_elements(cls, link: Link) -> None: + obj = tool.Project.get_link_empty_handle(link) + assert obj + col = obj.instance_collection + assert col + + for obj_ in col.objects: + if "hidden_indices" not in obj_: + continue + obj_.vertex_groups.clear() + obj_.modifiers.clear() + del obj_["hidden_indices"] + + @classmethod + def select_linked_element_geom(cls, obj: bpy.types.Object, guid: str) -> None: + slice_ = cls.get_linked_element_geom_slice(obj, guid) + + mesh = obj.data + assert isinstance(mesh, bpy.types.Mesh) + guid_polygons = mesh.polygons[slice_] + + selected_tris: list[tuple[int, ...]] = [] + selected_edges: list[tuple[int, ...]] = [] + + # Restart verts indices for our polygons. + guid_vertices_set: set[int] = set() + for polygon in guid_polygons: + guid_vertices_set.update(polygon.vertices) + vert_map = {k: v for v, k in enumerate(guid_vertices_set)} + + selected_vertices = [obj.matrix_world @ mesh.vertices[vi].co for vi in vert_map] + for polygon in guid_polygons: + selected_tris.append(tuple(vert_map[vi] for vi in polygon.vertices)) + selected_edges.extend(tuple([vert_map[vi] for vi in e]) for e in polygon.edge_keys) + + obj["selected_vertices"] = selected_vertices + obj["selected_edges"] = selected_edges + obj["selected_tris"] = selected_tris + + @classmethod + def get_linked_element_verts(cls, obj: bpy.types.Object, guid: str) -> set[int]: + slice_ = cls.get_linked_element_geom_slice(obj, guid) + + mesh = obj.data + assert isinstance(mesh, bpy.types.Mesh) + guid_polygons = mesh.polygons[slice_] + + guid_vertices_set: set[int] = set() + for polygon in guid_polygons: + guid_vertices_set.update(polygon.vertices) + return guid_vertices_set + + @classmethod + def select_linked_element( + cls, + context: bpy.types.Context, + obj: bpy.types.Object, + guid: str, + instance_matrix: Matrix | None = None, + ) -> None: + import sqlite3 + + from ifcpatch.recipes.ExtractPropertiesToSQLite import ( + ElementRow, + PropertyRow, + RelationshipRow, + ) + + from bonsai.bim.module.project.data import LinksData + from bonsai.bim.module.project.decorator import ProjectDecorator + + # Not sure if there's a difference between `instance_matrix` coming from `ray_cast` + # and usual `matrix_world`, maybe we can just get it from object always. + if instance_matrix is None: + instance_matrix = obj.matrix_world + + cls.deselect_queried_linked_element() + cls.set_queried_linked_element(obj, guid, instance_matrix) + cls.select_linked_element_geom(obj, guid) + db = sqlite3.connect(obj["db"]) + c = db.cursor() + + c.execute(f"SELECT * FROM elements WHERE global_id = '{guid}' LIMIT 1") + element = ElementRow(*c.fetchone()) + + attributes: dict[str, Any] = {} + for i, attr in enumerate(["GlobalId", "IFC Class", "Predefined Type", "Name", "Description"]): + if element[i + 1] is not None: + attributes[attr] = element[i + 1] + + c.execute("SELECT * FROM properties WHERE element_id = ?", (element[0],)) + rows = [PropertyRow(*row) for row in c.fetchall()] + + properties: defaultdict[str, dict[str, str]] = defaultdict(dict) + for row in rows: + properties[row.pset_name][row.name] = row.value + + c.execute("SELECT * FROM relationships WHERE from_id = ?", (element[0],)) + relationships = [RelationshipRow(*row) for row in c.fetchall()] + + relating_type_id = None + + for relationship in relationships: + if relationship[1] == "IfcRelDefinesByType": + relating_type_id = relationship[2] + + type_properties: defaultdict[str, dict[str, str]] = defaultdict(dict) + if relating_type_id is not None: + c.execute("SELECT * FROM properties WHERE element_id = ?", (relating_type_id,)) + rows = [PropertyRow(*row) for row in c.fetchall()] + for row in rows: + type_properties[row.pset_name][row.name] = row.value + + LinksData.linked_data = { + "attributes": attributes, + "properties": [(k, properties[k]) for k in sorted(properties.keys())], + "type_properties": [(k, type_properties[k]) for k in sorted(type_properties.keys())], + } + db.close() + + assert context.screen + for area in context.screen.areas: + if area.type == "PROPERTIES": + for region in area.regions: + if region.type == "WINDOW": + region.tag_redraw() + elif area.type == "VIEW_3D": + area.tag_redraw() + + ProjectDecorator.install(context) + + @classmethod + def set_queried_linked_element(cls, obj: bpy.types.Object, guid: str, instance_matrix: Matrix) -> None: + props = tool.Project.get_project_props() + props.queried_obj = obj + props.queried_obj_root = cls.find_obj_root(obj, instance_matrix) + props.queried_guid = guid + + @classmethod + def deselect_queried_linked_element(cls) -> None: + props = tool.Project.get_project_props() + obj = props.queried_obj + props.property_unset("queried_obj") + props.property_unset("queried_obj_root") + props.property_unset("queried_guid") + + if obj is not None: + for field in cls.SelectedGeometry._fields: + del obj[field] + + @classmethod + def find_obj_root(cls, obj: bpy.types.Object, matrix: Matrix) -> bpy.types.Object | None: + collections = set(obj.users_collection) + for o in bpy.data.objects: + if ( + o.type != "EMPTY" + or o.instance_type != "COLLECTION" + or o.instance_collection not in collections + or not np.allclose(matrix, o.matrix_world, atol=1e-4) + ): + continue + return o + + class SelectedGeometry(NamedTuple): + selected_vertices: list[tuple[float, float, float]] + selected_edges: list[tuple[int, int]] + selected_tris: list[tuple[int, int, int]] + + @classmethod + def get_selected_geometry(cls, obj: bpy.types.Object) -> SelectedGeometry: + return cls.SelectedGeometry( + obj["selected_vertices"], + obj["selected_edges"], + obj["selected_tris"], + ) diff --git a/src/bonsai/bonsai/tool/pset.py b/src/bonsai/bonsai/tool/pset.py index 8eabcd155d..2e2fc4383c 100644 --- a/src/bonsai/bonsai/tool/pset.py +++ b/src/bonsai/bonsai/tool/pset.py @@ -22,7 +22,6 @@ from typing import TYPE_CHECKING, Any, Literal, Union, assert_never import bpy import ifcopenshell -import ifcopenshell.api.pset import ifcopenshell.util.attribute import ifcopenshell.util.element diff --git a/src/bonsai/bonsai/tool/pset_template.py b/src/bonsai/bonsai/tool/pset_template.py index e4c1c3dbe1..fea5362d4e 100644 --- a/src/bonsai/bonsai/tool/pset_template.py +++ b/src/bonsai/bonsai/tool/pset_template.py @@ -24,8 +24,6 @@ from typing import TYPE_CHECKING, Literal, final import bpy import ifcopenshell import ifcopenshell.api.pset_template -import ifcopenshell.util.attribute -import ifcopenshell.util.element import bonsai.bim import bonsai.core.tool diff --git a/src/bonsai/bonsai/tool/qto.py b/src/bonsai/bonsai/tool/qto.py index 4fd271b283..ec14df6f4c 100644 --- a/src/bonsai/bonsai/tool/qto.py +++ b/src/bonsai/bonsai/tool/qto.py @@ -22,7 +22,6 @@ from typing import TYPE_CHECKING, Any, Literal, Optional, Union import bpy import ifcopenshell -import ifcopenshell.util.element import ifcopenshell.util.unit from mathutils import Vector diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index ee06066dd7..d98aaf26ab 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -16,14 +16,12 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import copy from typing import Union import bmesh import bpy import mathutils import numpy as np -from bpy_extras import view3d_utils from mathutils import Vector import bonsai.core.tool diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py index be7e350adc..02ddbe9745 100644 --- a/src/bonsai/bonsai/tool/root.py +++ b/src/bonsai/bonsai/tool/root.py @@ -22,7 +22,6 @@ from typing import TYPE_CHECKING, Any, Literal, Optional, Union import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.feature import ifcopenshell.api.geometry import ifcopenshell.api.root diff --git a/src/bonsai/bonsai/tool/search.py b/src/bonsai/bonsai/tool/search.py index ffd3a1aef3..356addee3a 100644 --- a/src/bonsai/bonsai/tool/search.py +++ b/src/bonsai/bonsai/tool/search.py @@ -20,7 +20,7 @@ from __future__ import annotations import json from itertools import cycle -from typing import TYPE_CHECKING, Literal, Union +from typing import TYPE_CHECKING, Any, Literal, Union import bpy import ifcopenshell.guid @@ -51,7 +51,9 @@ class Search(bonsai.core.tool.Search): @classmethod def import_filter_structure( - cls, filter_structure: list, filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup] + cls, + filter_structure: list[list[dict[str, Any]]], + filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup], ) -> None: filter_groups.clear() @@ -188,7 +190,9 @@ class Search(bonsai.core.tool.Search): return "" @classmethod - def execute_filter_groups(cls, filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]) -> set: + def execute_filter_groups( + cls, filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup] + ) -> set[ifcopenshell.entity_instance]: """ Execute filter groups with simplified chaining support. Within a single group chain, all filters chain sequentially with ADD/SUBTRACT/FILTER modes. @@ -196,10 +200,10 @@ class Search(bonsai.core.tool.Search): """ preferences = tool.Blender.get_addon_preferences() - all_group_results = [] + all_group_results: list[set[ifcopenshell.entity_instance]] = [] - for group_idx, filter_group in enumerate(filter_groups): - group_results = set() + for filter_group in filter_groups: + group_results: set[ifcopenshell.entity_instance] = set() for filter_index, ifc_filter in enumerate(filter_group.filters): if not ifc_filter.value: @@ -245,7 +249,7 @@ class Search(bonsai.core.tool.Search): if group_results: all_group_results.append(group_results) - final_results = set() + final_results: set[ifcopenshell.entity_instance] = set() for group_results in all_group_results: final_results.update(group_results) @@ -262,9 +266,9 @@ class Search(bonsai.core.tool.Search): """ filter_structure = data.get("filter_structure", []) - all_group_results = [] + all_group_results: list[set[ifcopenshell.entity_instance]] = [] for group_data in filter_structure: - group_results = set() + group_results: set[ifcopenshell.entity_instance] = set() for filter_data in group_data: filter_mode = filter_data.get("filter_mode", "ADD") @@ -332,7 +336,7 @@ class Search(bonsai.core.tool.Search): if group_results: all_group_results.append(group_results) - final_results = set() + final_results: set[ifcopenshell.entity_instance] = set() for group_results in all_group_results: final_results.update(group_results) diff --git a/src/bonsai/bonsai/tool/sequence.py b/src/bonsai/bonsai/tool/sequence.py index 8c5339d438..2afed8741a 100644 --- a/src/bonsai/bonsai/tool/sequence.py +++ b/src/bonsai/bonsai/tool/sequence.py @@ -18,11 +18,8 @@ from __future__ import annotations -import base64 import json -import os import re -import webbrowser from collections.abc import Iterable from datetime import datetime from datetime import time as datetime_time @@ -32,14 +29,12 @@ import bpy import ifcopenshell import ifcopenshell.api.group import ifcopenshell.api.sequence -import ifcopenshell.ifcopenshell_wrapper as W import ifcopenshell.util.date import ifcopenshell.util.element import ifcopenshell.util.selector import ifcopenshell.util.sequence import isodate import mathutils -import pystache from dateutil import parser from mathutils import Color diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 5c538dfc43..02f484d388 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -23,10 +23,7 @@ from typing import TYPE_CHECKING, Any, Union import bmesh import bpy -import ifcopenshell import ifcopenshell.util.unit -import mathutils -from lark import Lark, Transformer from mathutils import Matrix, Vector import bonsai.core.tool @@ -114,6 +111,15 @@ class Snap(bonsai.core.tool.Snap): return increment + @classmethod + def get_angle_snap_value(cls, context: bpy.types.Context) -> float: + """Get the angle snap increment from Blender's tool settings. + + :param context: Blender context + :return: Angle snap increment in degrees + """ + return math.degrees(context.scene.tool_settings.snap_angle_increment_3d) + @classmethod def get_snap_points_on_raycasted_face(cls, context, event, obj, face_index): matrix = obj.matrix_world.copy() diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index d38d895c22..b185b9ab59 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -49,7 +49,6 @@ import bonsai.core.root import bonsai.core.spatial import bonsai.core.tool import bonsai.core.type -import bonsai.core.unit import bonsai.tool as tool if TYPE_CHECKING: diff --git a/src/bonsai/bonsai/tool/style.py b/src/bonsai/bonsai/tool/style.py index 78cb19680e..8db3ed30fe 100644 --- a/src/bonsai/bonsai/tool/style.py +++ b/src/bonsai/bonsai/tool/style.py @@ -26,7 +26,6 @@ import ifcopenshell import ifcopenshell.api.style import ifcopenshell.util.element import ifcopenshell.util.representation -import numpy as np from mathutils import Color import bonsai.bim.helper diff --git a/src/bonsai/bonsai/tool/surveyor.py b/src/bonsai/bonsai/tool/surveyor.py index f59d9cd7da..42fe540c63 100644 --- a/src/bonsai/bonsai/tool/surveyor.py +++ b/src/bonsai/bonsai/tool/surveyor.py @@ -17,12 +17,10 @@ # along with Bonsai. If not, see . import bpy -import ifcopenshell.api import ifcopenshell.util.geolocation import ifcopenshell.util.unit import numpy as np import numpy.typing as npt -from mathutils import Matrix import bonsai.core.tool import bonsai.tool as tool diff --git a/src/bonsai/bonsai/tool/system.py b/src/bonsai/bonsai/tool/system.py index 8b949a36c9..926bceca91 100644 --- a/src/bonsai/bonsai/tool/system.py +++ b/src/bonsai/bonsai/tool/system.py @@ -28,7 +28,6 @@ import ifcopenshell.api.system import ifcopenshell.util.element import ifcopenshell.util.system from mathutils import Matrix, Vector -from natsort import natsorted import bonsai.bim.helper import bonsai.core.geometry diff --git a/src/bonsai/bonsai/tool/unit.py b/src/bonsai/bonsai/tool/unit.py index 1221f28be9..5825c6f382 100644 --- a/src/bonsai/bonsai/tool/unit.py +++ b/src/bonsai/bonsai/tool/unit.py @@ -20,7 +20,7 @@ from __future__ import annotations import json import math -from typing import TYPE_CHECKING, Any, Literal, Union, assert_never +from typing import TYPE_CHECKING, Any, Literal, Union import bpy import ifcopenshell diff --git a/src/bonsai/bonsai/tool/web.py b/src/bonsai/bonsai/tool/web.py index 35811b3819..9ebf211ce8 100644 --- a/src/bonsai/bonsai/tool/web.py +++ b/src/bonsai/bonsai/tool/web.py @@ -30,7 +30,6 @@ import threading import time import webbrowser from pathlib import Path -from time import sleep from typing import TYPE_CHECKING, Any, Optional, Union import bpy @@ -118,7 +117,6 @@ class Web(bonsai.core.tool.Web): :param port: The port number on which to start the WebSocket server. """ - import addon_utils global ws_process diff --git a/src/bonsai/docs/guides/development/installation.rst b/src/bonsai/docs/guides/development/installation.rst index 3446617ab6..7355d09ca1 100644 --- a/src/bonsai/docs/guides/development/installation.rst +++ b/src/bonsai/docs/guides/development/installation.rst @@ -10,6 +10,8 @@ There are different methods of installation, depending on your situation. recommended for developers who are actively coding. 4. :ref:`guides/development/installation:Packaged installation` is recommended for those who use a package manager. +5. :ref:`guides/development/installation:BonsaiPR (Bleeding Edge) Installation` merges all open, non-draft PRs automatically. + System requirements ------------------- @@ -231,6 +233,95 @@ the `Makefile `__ in the ``dist`` target. + + +BonsaiPR (Bleeding Edge) Installation +-------------------------------------- + +**BonsaiPR** is a community-maintained build that automatically merges open pull +requests (PRs) from the IfcOpenShell repository into a single installable add-on. +It is intended for power users and testers who want to try the latest community +contributions before they are officially reviewed and merged. + +Why BonsaiPR Exists +~~~~~~~~~~~~~~~~~~~~ + +Many excellent PRs are submitted by contributors, but core maintainers have +limited time for timely reviews. As a result, PRs often sit unmerged, +contributors lose momentum, and valuable work risks being forgotten. + +BonsaiPR addresses this by providing a ``bleeding_edge`` build that merges all +open, non-draft PRs automatically. Power users can install this build to test +multiple PRs together, helping catch issues earlier and reducing the load on core +developers. + +.. warning:: + + You must enable either **Bonsai** or **BonsaiPR**, but **not both at the + same time**. Enabling both can cause conflicts or unexpected behaviour. To + switch between them, disable the active one before enabling the other. + +How It Works +~~~~~~~~~~~~~ + +On a regular basis (and whenever a PR is opened or modified), an automated +system: + +1. Clones the IfcOpenShell repository and merges all open, non-draft PRs. +2. Builds the resulting add-on for all supported platforms. +3. Publishes the result as a release on the `BonsaiPR releases page + `__. +4. The list of branches is also published on `falken10vdl's IfcOpenShell Fork + `__. + +Each release includes a full report listing which PRs were merged successfully, +which were skipped (e.g. drafts), and which failed due to conflicts with other +PRs. + +Installing BonsaiPR with Automated Updates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. Open Blender and go to :menuselection:`Edit --> Preferences --> Add-ons`. + Disable **Bonsai** if it is currently enabled. + +2. Click on the **Get Extensions** tab in the left sidebar. + +3. In the top right, click the **Repositories** dropdown, then the **+ icon**, + and select **Add Remote Repository**. + +4. Enter the following URL:: + + https://raw.githubusercontent.com/falken10vdl/bonsaiPR/refs/heads/main/index.json + +5. Enable **Check for Updates on Startup**, then click **Create**. + +6. In the **Get Extensions** search bar, type ``bonsai`` and look for + **BonsaiPR**. Click **Install**. + +7. Go to :menuselection:`Edit --> Preferences --> Add-ons` and confirm that + **BonsaiPR** is enabled and **Bonsai** is disabled. + +8. Restart Blender. + +Blender will automatically check for updates to the BonsaiPR extension on +startup, so you will always have access to the latest bleeding edge build. + +Installing BonsaiPR Manually +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you prefer to install manually, download the appropriate ``.zip`` file for +your platform from the `BonsaiPR releases page +`__: + +- **Linux (x64)**: ``bonsaiPR_py311-0.8.4-alphaYYMMDDHHMM-linux-x64.zip`` +- **macOS Intel (x64)**: ``bonsaiPR_py311-0.8.4-alphaYYMMDDHHMM-macos-x64.zip`` +- **macOS Apple Silicon (ARM64)**: ``bonsaiPR_py311-0.8.4-alphaYYMMDDHHMM-macos-arm64.zip`` +- **Windows (x64)**: ``bonsaiPR_py311-0.8.4-alphaYYMMDDHHMM-windows-x64.zip`` + +Then go to :menuselection:`Edit --> Preferences --> Get Extensions --> "V" Icon +(top right) --> Install from Disk` and select the downloaded zip. + + Add-on compatibility -------------------- diff --git a/src/bonsai/pyproject.toml b/src/bonsai/pyproject.toml index 3ec2cb8748..4ee183cba0 100644 --- a/src/bonsai/pyproject.toml +++ b/src/bonsai/pyproject.toml @@ -38,3 +38,12 @@ exclude = ["test*"] [tool.ruff] extend = "../../pyproject.toml" +lint.extend-select = [ + "F401", # unused imports +] + +[tool.ruff.lint.isort] +known-first-party = [ + "test", + "bonsai", +] diff --git a/src/bonsai/scripts/bonsai_translations.py b/src/bonsai/scripts/bonsai_translations.py index 7a0fe86ea9..1597eaabcb 100644 --- a/src/bonsai/scripts/bonsai_translations.py +++ b/src/bonsai/scripts/bonsai_translations.py @@ -5,7 +5,6 @@ try: if not hasattr(bpy, "context"): raise ModuleNotFoundError import addon_utils - import bl_i18n_utils BPY_IS_LOADED = True except ModuleNotFoundError: diff --git a/src/bonsai/scripts/classifications/brick_classifiction.py b/src/bonsai/scripts/classifications/brick_classifiction.py index 9a9d42353e..17246940c7 100644 --- a/src/bonsai/scripts/classifications/brick_classifiction.py +++ b/src/bonsai/scripts/classifications/brick_classifiction.py @@ -22,8 +22,7 @@ class Generator: } ) - query = self.schema.query( - """ + query = self.schema.query(""" PREFIX brick: PREFIX rdfs: PREFIX skos: @@ -49,8 +48,7 @@ class Generator: } } GROUP BY ?entity - """ - ) + """) # create references dictionary references = {} @@ -76,17 +74,13 @@ class Generator: ) # get all parents of the entity - query = self.schema.query( - """ + query = self.schema.query(""" PREFIX brick: PREFIX rdfs: SELECT ?parent WHERE { brick:{entity} rdfs:subClassOf ?parent . } - """.replace( - "{entity}", location.split("#")[-1] - ) - ) + """.replace("{entity}", location.split("#")[-1])) # filter parents for the brick entity for row in query: parent = row.get("parent").toPython() diff --git a/src/bonsai/scripts/classifications/vbis.py b/src/bonsai/scripts/classifications/vbis.py index 7c4f43b967..71d0af5fe9 100644 --- a/src/bonsai/scripts/classifications/vbis.py +++ b/src/bonsai/scripts/classifications/vbis.py @@ -1,9 +1,6 @@ import csv -import json -import os # import pystache -import subprocess from pathlib import Path import ifcopenshell diff --git a/src/bonsai/scripts/gbxml.py b/src/bonsai/scripts/gbxml.py index 7280c69c2a..7bd8063a15 100644 --- a/src/bonsai/scripts/gbxml.py +++ b/src/bonsai/scripts/gbxml.py @@ -17,14 +17,11 @@ # along with Bonsai. If not, see . import math -import sys import uuid import bpy -import bspy # pyright: ignore[reportMissingImports] # sys.path.append('C:\Program Files\Python37\Lib\site-packages') -import lxml import lxml.etree from bspy import Gbxml # pyright: ignore[reportMissingImports] diff --git a/src/bonsai/scripts/generate_au_library.py b/src/bonsai/scripts/generate_au_library.py index 43f05b45da..91fe224255 100644 --- a/src/bonsai/scripts/generate_au_library.py +++ b/src/bonsai/scripts/generate_au_library.py @@ -20,7 +20,6 @@ # pylint: skip-file import bpy -import ifcopenshell import ifcopenshell.api import ifcopenshell.api.context import ifcopenshell.api.geometry diff --git a/src/bonsai/scripts/generate_entourage_library.py b/src/bonsai/scripts/generate_entourage_library.py index 7435186396..fe7a75856e 100644 --- a/src/bonsai/scripts/generate_entourage_library.py +++ b/src/bonsai/scripts/generate_entourage_library.py @@ -17,10 +17,8 @@ # along with Bonsai. If not, see . import os -from pathlib import Path import bpy -import ifcopenshell import ifcopenshell.api import ifcopenshell.api.context import ifcopenshell.api.geometry diff --git a/src/bonsai/scripts/generate_furniture_library.py b/src/bonsai/scripts/generate_furniture_library.py index 1c1572c854..7b9d411772 100644 --- a/src/bonsai/scripts/generate_furniture_library.py +++ b/src/bonsai/scripts/generate_furniture_library.py @@ -17,7 +17,7 @@ # along with Bonsai. If not, see . from itertools import chain -from math import cos, pi, tan +from math import pi, tan from pathlib import Path from typing import Optional, Union @@ -25,10 +25,8 @@ import ifcopenshell import ifcopenshell.api import ifcopenshell.api.context import ifcopenshell.api.geometry -import ifcopenshell.api.material import ifcopenshell.api.project import ifcopenshell.api.root -import ifcopenshell.api.style import ifcopenshell.api.unit import ifcopenshell.util.element import numpy as np diff --git a/src/bonsai/scripts/generate_landscape_library.py b/src/bonsai/scripts/generate_landscape_library.py index 34d2366fd5..4e6ad3eba1 100644 --- a/src/bonsai/scripts/generate_landscape_library.py +++ b/src/bonsai/scripts/generate_landscape_library.py @@ -20,13 +20,10 @@ import csv import os import random from collections import namedtuple -from itertools import chain -from math import cos, pi, sin, tan -from pathlib import Path +from math import cos, pi, sin from random import uniform import bpy -import ifcopenshell import ifcopenshell.api import ifcopenshell.api.context import ifcopenshell.api.geometry @@ -36,7 +33,7 @@ import ifcopenshell.api.style import ifcopenshell.api.unit import ifcopenshell.util.unit from ifcopenshell.util.shape_builder import ShapeBuilder -from mathutils import Matrix, Vector +from mathutils import Vector import bonsai.tool as tool diff --git a/src/bonsai/scripts/generate_site_library.py b/src/bonsai/scripts/generate_site_library.py index 0b0511e9da..d8439287ad 100644 --- a/src/bonsai/scripts/generate_site_library.py +++ b/src/bonsai/scripts/generate_site_library.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . import bpy -import ifcopenshell import ifcopenshell.api import ifcopenshell.api.context import ifcopenshell.api.geometry diff --git a/src/bonsai/scripts/generate_steel_profiles_library.py b/src/bonsai/scripts/generate_steel_profiles_library.py index c54818df4b..255a448127 100644 --- a/src/bonsai/scripts/generate_steel_profiles_library.py +++ b/src/bonsai/scripts/generate_steel_profiles_library.py @@ -19,11 +19,10 @@ # fmt: off # pylint: skip-file -from math import cos, pi +from math import pi from pathlib import Path import boltspy as bolts # pyright: ignore[reportMissingImports] -import ifcopenshell import ifcopenshell.api import ifcopenshell.api.material import ifcopenshell.api.project diff --git a/src/bonsai/scripts/geonodes_modifier_prototype.py b/src/bonsai/scripts/geonodes_modifier_prototype.py index 16370c1ca2..185200942e 100644 --- a/src/bonsai/scripts/geonodes_modifier_prototype.py +++ b/src/bonsai/scripts/geonodes_modifier_prototype.py @@ -21,7 +21,6 @@ import json import bmesh import bpy -import ifcopenshell import ifcopenshell.api.pset import ifcopenshell.util.element diff --git a/src/bonsai/scripts/get_all_qtos.py b/src/bonsai/scripts/get_all_qtos.py index d364115559..8d407120eb 100644 --- a/src/bonsai/scripts/get_all_qtos.py +++ b/src/bonsai/scripts/get_all_qtos.py @@ -7,7 +7,6 @@ from typing import Union import ifc5d import ifcopenshell.util.pset -import ifcopenshell.util.type def order_dict(dictionary): diff --git a/src/bonsai/scripts/obj2ifc-meshlab.py b/src/bonsai/scripts/obj2ifc-meshlab.py index c8292bb0f7..70849e11d9 100644 --- a/src/bonsai/scripts/obj2ifc-meshlab.py +++ b/src/bonsai/scripts/obj2ifc-meshlab.py @@ -21,8 +21,6 @@ import argparse from pathlib import Path -import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.aggregate import ifcopenshell.api.context import ifcopenshell.api.geometry diff --git a/src/bonsai/scripts/obj2ifc.py b/src/bonsai/scripts/obj2ifc.py index 70f3bf7b41..ec5459c4a3 100644 --- a/src/bonsai/scripts/obj2ifc.py +++ b/src/bonsai/scripts/obj2ifc.py @@ -21,8 +21,6 @@ import argparse from pathlib import Path -import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.aggregate import ifcopenshell.api.context import ifcopenshell.api.geometry diff --git a/src/bonsai/scripts/replace_drawing_path.py b/src/bonsai/scripts/replace_drawing_path.py index 500726082f..2dd4296fad 100644 --- a/src/bonsai/scripts/replace_drawing_path.py +++ b/src/bonsai/scripts/replace_drawing_path.py @@ -31,7 +31,6 @@ import os import sys from sys import platform -import ifcopenshell import ifcopenshell.api.pset import ifcopenshell.util.element diff --git a/src/bonsai/scripts/reregister_bonsai.py b/src/bonsai/scripts/reregister_bonsai.py index ba9ab2ba2a..4eb3764708 100644 --- a/src/bonsai/scripts/reregister_bonsai.py +++ b/src/bonsai/scripts/reregister_bonsai.py @@ -23,7 +23,6 @@ Use operators instead of `blender --command extension remove` to ensure disable and enable occur in the same Blender session. """ - import bpy import bonsai.tool as tool diff --git a/src/bonsai/scripts/setup_pytest.py b/src/bonsai/scripts/setup_pytest.py index 28dc7565f1..a4e56ca35c 100644 --- a/src/bonsai/scripts/setup_pytest.py +++ b/src/bonsai/scripts/setup_pytest.py @@ -45,10 +45,10 @@ for dep in dependencies: subprocess.check_call(command + [dep]) try: - import pygments - import pytest - import pytest_bdd - import pytest_blender + import pygments # noqa: F401 + import pytest # noqa: F401 + import pytest_bdd # noqa: F401 + import pytest_blender # noqa: F401 print("Test dependency installation was successful!") except Exception as e: diff --git a/src/bonsai/scripts/standalone_drawer.py b/src/bonsai/scripts/standalone_drawer.py index 3eed11b9c9..886887bf32 100644 --- a/src/bonsai/scripts/standalone_drawer.py +++ b/src/bonsai/scripts/standalone_drawer.py @@ -4,7 +4,6 @@ from typing import NamedTuple import ifcopenshell import ifcopenshell.geom -import ifcopenshell.ifcopenshell_wrapper as W import ifcopenshell.util.element # W.turn_on_detailed_logging() diff --git a/src/bonsai/scripts/waldo.py b/src/bonsai/scripts/waldo.py index 00243c5782..c58e1dc1ec 100644 --- a/src/bonsai/scripts/waldo.py +++ b/src/bonsai/scripts/waldo.py @@ -3,7 +3,6 @@ from itertools import cycle from math import radians -import ifcopenshell import ifcopenshell.api.aggregate import ifcopenshell.api.context import ifcopenshell.api.geometry diff --git a/src/bonsai/test/bim/bootstrap.py b/src/bonsai/test/bim/bootstrap.py index c799c74c00..587b8f5a2d 100644 --- a/src/bonsai/test/bim/bootstrap.py +++ b/src/bonsai/test/bim/bootstrap.py @@ -34,6 +34,7 @@ from bonsai.bim.ifc import IfcStore # Monkey-patch webbrowser opening since we want to test headlessly webbrowser.open = lambda x: True +tool.Drawing.open_with_user_command = lambda x, y: True variables = {"cwd": os.getcwd(), "ifc": "IfcStore.get_file()"} diff --git a/src/bonsai/test/bim/feature/drawing.feature b/src/bonsai/test/bim/feature/drawing.feature index 6a24168e4b..b7b78e748d 100644 --- a/src/bonsai/test/bim/feature/drawing.feature +++ b/src/bonsai/test/bim/feature/drawing.feature @@ -3,12 +3,6 @@ Feature: Drawing Scenario: Duplicate drawing 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 I save IFC project And I look at the "Drawings" panel And I click "IMPORT" @@ -315,3 +309,10 @@ Scenario: Create sheet - with a drawing added to it And I click "IMAGE_PLANE" When I click "OUTPUT" Then the file "{ifc_dir}/sheets/A01 - UNTITLED.svg" should contain "IfcWall" + +Scenario: Add reference image + Given an empty IFC project + And I save IFC project + When I press "bim.add_reference_image(filepath='{cwd}/test/files/image.jpg')" + Then the object "IfcAnnotation/image" exists + And the object "IfcAnnotation/image" dimensions are "1.0,0.565,0." diff --git a/src/bonsai/test/bim/feature/project.feature b/src/bonsai/test/bim/feature/project.feature index cb9aeec21e..f536a17220 100644 --- a/src/bonsai/test/bim/feature/project.feature +++ b/src/bonsai/test/bim/feature/project.feature @@ -677,31 +677,43 @@ Scenario: Load project elements - all georeferencing coordinate situations with And the object "IfcActuator/J" has a vertex at "10.366,3.813,-1" And the object "IfcActuator/J" has a vertex at "12.298,4.331,-1" -Scenario: Link IFC +Scenario: Link IFC - from an empty IFC project Given an empty IFC project - When I link IFC project from "{cwd}/test/files/basic.ifc" + When I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "True" And the collection "IfcProject/basic.ifc" exists And the object "Chunk" exists And the object "Chunk" is placed in the collection "IfcProject/basic.ifc" -Scenario: Link IFC - disabled false origin mode - Given an empty IFC project - # Not currently possible via UI - And I set "scene.BIMProjectProperties.distance_limit" to "5" - And I set "scene.BIMProjectProperties.false_origin_mode" to "DISABLED" - When I link IFC project from "{cwd}/test/files/geolocation.ifc" - Then the object "Chunk" exists - And the object "Chunk" has a vertex at "2,2,-1" - And the object "Chunk" has a vertex at "9,-1,-1" - And the object "Chunk" has a vertex at "17,4,-1" - Scenario: Link IFC - from an empty IFC project - automatic false origin mode (0,0,0 will be the false origin) Given an empty IFC project # Not currently possible via UI And I set "scene.BIMProjectProperties.distance_limit" to "5" And I set "scene.BIMProjectProperties.false_origin_mode" to "AUTOMATIC" - When I link IFC project from "{cwd}/test/files/geolocation.ifc" + When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)" + Then the object "Chunk" exists + And the object "Chunk" has a vertex at "2,2,-1" + And the object "Chunk" has a vertex at "9,-1,-1" + And the object "Chunk" has a vertex at "17,4,-1" + +Scenario: Link IFC - from an empty IFC project - manual false origin mode + Given an empty IFC project + # Not currently possible via UI + And I set "scene.BIMProjectProperties.distance_limit" to "5" + And I set "scene.BIMProjectProperties.false_origin_mode" to "MANUAL" + And I set "scene.BIMProjectProperties.false_origin" to "10000,0,0" + When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)" + Then the object "Chunk" exists + And the object "Chunk" has a vertex at "2,2,-1" + And the object "Chunk" has a vertex at "9,-1,-1" + And the object "Chunk" has a vertex at "17,4,-1" + +Scenario: Link IFC - from an empty IFC project - disabled false origin mode + Given an empty IFC project + # Not currently possible via UI + And I set "scene.BIMProjectProperties.distance_limit" to "5" + And I set "scene.BIMProjectProperties.false_origin_mode" to "DISABLED" + When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)" Then the object "Chunk" exists And the object "Chunk" has a vertex at "2,2,-1" And the object "Chunk" has a vertex at "9,-1,-1" @@ -712,31 +724,42 @@ Scenario: Link IFC - from an empty Blender session - automatic false origin mode # Not currently possible via UI And I set "scene.BIMProjectProperties.distance_limit" to "5" And I set "scene.BIMProjectProperties.false_origin_mode" to "AUTOMATIC" - When I link IFC project from "{cwd}/test/files/geolocation.ifc" + When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)" Then the object "Chunk" exists And the object "Chunk" has a vertex at "-11,-2,0" And the object "Chunk" has a vertex at "-4,-5,0" And the object "Chunk" has a vertex at "4,0,0" -Scenario: Link IFC - manual false origin mode +Scenario: Link IFC - from an empty Blender session - manual false origin mode Given an empty Blender session # Not currently possible via UI And I set "scene.BIMProjectProperties.distance_limit" to "5" And I set "scene.BIMProjectProperties.false_origin_mode" to "MANUAL" And I set "scene.BIMProjectProperties.false_origin" to "10000,0,0" - When I link IFC project from "{cwd}/test/files/geolocation.ifc" + When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)" Then the object "Chunk" exists And the object "Chunk" has a vertex at "-8,2,-1" And the object "Chunk" has a vertex at "-1,-1,-1" And the object "Chunk" has a vertex at "7,4,-1" -Scenario: Link IFC - automatic false origin mode - two different false origins and project norths - grid north is up because we start with geolocation.ifc +Scenario: Link IFC - from an empty Blender session - disabled false origin mode + Given an empty Blender session + # Not currently possible via UI + And I set "scene.BIMProjectProperties.distance_limit" to "5" + And I set "scene.BIMProjectProperties.false_origin_mode" to "DISABLED" + When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)" + Then the object "Chunk" exists + And the object "Chunk" has a vertex at "2,2,-1" + And the object "Chunk" has a vertex at "9,-1,-1" + And the object "Chunk" has a vertex at "17,4,-1" + +Scenario: Link IFC - from an empty Blender session - automatic false origin mode - two different false origins and project norths - grid north is up because we start with geolocation.ifc Given an empty Blender session # Not currently possible via UI And I set "scene.BIMProjectProperties.distance_limit" to "5" And I set "scene.BIMProjectProperties.false_origin_mode" to "AUTOMATIC" - When I link IFC project from "{cwd}/test/files/geolocation.ifc" - And I link IFC project from "{cwd}/test/files/geolocation-mapconversion-angle.ifc" + When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)" + And I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation-mapconversion-angle.ifc', use_cache=False)" Then the object "Col:IfcProject/geolocation.ifc:Chunk" exists And the object "Col:IfcProject/geolocation-mapconversion-angle.ifc:Chunk" exists And the object "Col:IfcProject/geolocation.ifc:Chunk" has a vertex at "-11,-2,0" @@ -746,13 +769,13 @@ Scenario: Link IFC - automatic false origin mode - two different false origins a And the object "Col:IfcProject/geolocation-mapconversion-angle.ifc:Chunk" has a vertex at "9.294,-9.366,0" And the object "Col:IfcProject/geolocation-mapconversion-angle.ifc:Chunk" has a vertex at "18.722,-9.036,0" -Scenario: Link IFC - automatic false origin mode - two different false origins and project norths - project north is up because we start with geolocation-mapconversion-angle.ifc +Scenario: Link IFC - from an empty Blender session - automatic false origin mode - two different false origins and project norths - project north is up because we start with geolocation-mapconversion-angle.ifc Given an empty Blender session # Not currently possible via UI And I set "scene.BIMProjectProperties.distance_limit" to "5" And I set "scene.BIMProjectProperties.false_origin_mode" to "AUTOMATIC" - When I link IFC project from "{cwd}/test/files/geolocation-mapconversion-angle.ifc" - And I link IFC project from "{cwd}/test/files/geolocation.ifc" + When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation-mapconversion-angle.ifc', use_cache=False)" + And I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)" Then the object "Col:IfcProject/geolocation.ifc:Chunk" exists And the object "Col:IfcProject/geolocation-mapconversion-angle.ifc:Chunk" exists And the object "Col:IfcProject/geolocation-mapconversion-angle.ifc:Chunk" has a vertex at "-11,-2,0" @@ -762,14 +785,14 @@ Scenario: Link IFC - automatic false origin mode - two different false origins a And the object "Col:IfcProject/geolocation.ifc:Chunk" has a vertex at "-17.696,-7.866,0" And the object "Col:IfcProject/geolocation.ifc:Chunk" has a vertex at "-13.268,0.464,0" -Scenario: Link IFC - automatic false origin mode - three identical false origins but different project and map units +Scenario: Link IFC - from an empty Blender session - automatic false origin mode - three identical false origins but different project and map units Given an empty Blender session # Not currently possible via UI And I set "scene.BIMProjectProperties.distance_limit" to "5" And I set "scene.BIMProjectProperties.false_origin_mode" to "AUTOMATIC" - When I link IFC project from "{cwd}/test/files/geolocation-unit1.ifc" - And I link IFC project from "{cwd}/test/files/geolocation-unit2.ifc" - And I link IFC project from "{cwd}/test/files/geolocation-unit3.ifc" + When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation-unit1.ifc', use_cache=False)" + And I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation-unit2.ifc', use_cache=False)" + And I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation-unit3.ifc', use_cache=False)" Then the object "Col:IfcProject/geolocation-unit1.ifc:Chunk" exists And the object "Col:IfcProject/geolocation-unit2.ifc:Chunk" exists And the object "Col:IfcProject/geolocation-unit3.ifc:Chunk" exists @@ -779,54 +802,54 @@ Scenario: Link IFC - automatic false origin mode - three identical false origins Scenario: Toggle link visibility - wireframe mode Given an empty IFC project - And I link IFC project from "{cwd}/test/files/basic.ifc" - When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='WIREFRAME')" + And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" + When I press "bim.toggle_link_visibility(link_index=0, mode='WIREFRAME')" Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_wireframe" is "True" And the object "Chunk" should display as "WIRE" - When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='WIREFRAME')" + When I press "bim.toggle_link_visibility(link_index=0, mode='WIREFRAME')" Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_wireframe" is "False" And the object "Chunk" should display as "TEXTURED" Scenario: Toggle link selectability Given an empty IFC project - And I link IFC project from "{cwd}/test/files/basic.ifc" - When I press "bim.toggle_link_selectability(link='{cwd}/test/files/basic.ifc')" + And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" + When I press "bim.toggle_link_selectability(link_index=0)" Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_selectable" is "False" And the collection "IfcProject/basic.ifc" is unselectable - When I press "bim.toggle_link_selectability(link='{cwd}/test/files/basic.ifc')" + When I press "bim.toggle_link_selectability(link_index=0)" Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_selectable" is "True" And the collection "IfcProject/basic.ifc" is selectable Scenario: Toggle link visibility - visible mode Given an empty IFC project - And I link IFC project from "{cwd}/test/files/basic.ifc" - When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='VISIBLE')" + And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" + When I press "bim.toggle_link_visibility(link_index=0, mode='VISIBLE')" Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_hidden" is "True" And the object "IfcProject/basic.ifc" is not visible - When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='VISIBLE')" + When I press "bim.toggle_link_visibility(link_index=0, mode='VISIBLE')" Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_hidden" is "False" And the object "IfcProject/basic.ifc" is visible Scenario: Unload link Given an empty Blender session - And I link IFC project from "{cwd}/test/files/basic.ifc" - When I press "bim.unload_link(filepath='{cwd}/test/files/basic.ifc')" + And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" + When I press "bim.unload_link(link_index=0)" Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "False" And the collection "IfcProject/basic.ifc" does not exist Scenario: Load link Given an empty Blender session - And I link IFC project from "{cwd}/test/files/basic.ifc" - And I press "bim.unload_link(filepath='{cwd}/test/files/basic.ifc')" - When I press "bim.load_link(filepath='{cwd}/test/files/basic.ifc')" + And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" + And I press "bim.unload_link(link_index=0)" + When I press "bim.load_link(link_index=0)" Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "True" And the object "IfcProject/basic.ifc" exists Scenario: Unlink IFC Given an empty Blender session - And I link IFC project from "{cwd}/test/files/basic.ifc" - And I press "bim.unload_link(filepath='{cwd}/test/files/basic.ifc')" - When I press "bim.unlink_ifc(filepath='{cwd}/test/files/basic.ifc')" + And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" + And I press "bim.unload_link(link_index=0)" + When I press "bim.unlink_ifc(link_index=0)" Then "scene.BIMProjectProperties.links.get('{cwd}/test/files/basic.ifc')" is "None" And "scene.collection.children.get('IfcProject/basic.ifc')" is "None" And the object "Chunk" does not exist diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 9df6bbf627..262dce65ed 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -70,10 +70,7 @@ 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.cwd() / "test/files/temp" -TEST_FILES_DIR = Path.cwd() / "test/files" - -CLEAN_LINKED_FILES_CACHE = False +TMP = Path(f"{variables['cwd']}/test/files/temp") EPSET_DRAWING = Path.cwd() / "bonsai/bim/data/pset/EPset_Drawing.ifc" EPSET_DRAWING_BYTES = EPSET_DRAWING.read_bytes() @@ -283,6 +280,8 @@ def create_ui_name_cache(): try: panel_type = getattr(bpy.types, bl_idname) if panel_type.bl_rna.base.name == "Panel": + if "_tab_" in panel_type.bl_idname: + continue # Tab panels are just groups and not relevant in testing ui_name_cache[panel_type.bl_label] = panel_type.bl_idname elif panel_type.bl_rna.base.name == "Operator": ui_name_cache[panel_type.bl_label] = bl_idname @@ -311,25 +310,6 @@ def vectors_are_equal(v1, v2): return all(is_x(v1[i], v2[i]) for i in range(len(v1))) -@pytest.fixture(scope="function", autouse=True) -def run_for_each_test() -> Generator[None]: - # Code before this runs before each test - yield - # Code after this runs after each test - - global CLEAN_LINKED_FILES_CACHE - if CLEAN_LINKED_FILES_CACHE: - for filepath in TEST_FILES_DIR.glob("*.ifc.cache.*"): - filepath.unlink() - CLEAN_LINKED_FILES_CACHE = False - - # pset_template tests are editing EPset_Drawing.ifc, so we need to restore it. - global RELOAD_EPSET_DRAWING - if RELOAD_EPSET_DRAWING: - EPSET_DRAWING.write_bytes(EPSET_DRAWING_BYTES) - RELOAD_EPSET_DRAWING = False - - @given("an untestable scenario") def an_untestable_scenario(): pass @@ -384,16 +364,6 @@ def saving_ifc_project() -> None: tool.Project.save_test_project() -@given(parsers.parse('I link IFC project from "{filepath}"')) -@when(parsers.parse('I link IFC project from "{filepath}"')) -@then(parsers.parse('I link IFC project from "{filepath}"')) -def i_link_ifc_project_from_filepath(filepath: str) -> None: - global CLEAN_LINKED_FILES_CACHE - filepath = replace_variables(filepath) - CLEAN_LINKED_FILES_CACHE = True - bpy.ops.bim.link_ifc(filepath=filepath, use_cache=False) - - @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 @@ -1587,10 +1557,19 @@ def the_object_name_has_a_vertex_at_location(name, location): is_pass = False target = Vector([float(co) for co in location.split(",")]) verts = [] - for v in obj.data.vertices: - verts.append(obj.matrix_world @ v.co) - if (verts[-1] - target).length < 0.001: - is_pass = True + depsgraph = bpy.context.evaluated_depsgraph_get() + obj_eval = obj.evaluated_get(depsgraph) + mesh = obj_eval.to_mesh(preserve_all_data_layers=False, depsgraph=depsgraph) + try: + for inst in depsgraph.object_instances: + if inst.object.original is obj: + mw = inst.matrix_world + for i, v in enumerate(mesh.vertices): + verts.append(mw @ v.co) + if (verts[-1] - target).length < 0.001: + is_pass = True + finally: + obj_eval.to_mesh_clear() assert is_pass, f"No verts found at {location}: {verts}" diff --git a/src/bonsai/test/pyproject.toml b/src/bonsai/test/pyproject.toml new file mode 100644 index 0000000000..7567fe8627 --- /dev/null +++ b/src/bonsai/test/pyproject.toml @@ -0,0 +1,5 @@ +[tool.ruff] +extend = "../pyproject.toml" +lint.ignore = [ + "F401", # unused imports +] diff --git a/src/bonsai/test/tool/test_blender.py b/src/bonsai/test/tool/test_blender.py index 43d04c7c19..cd155b5dee 100644 --- a/src/bonsai/test/tool/test_blender.py +++ b/src/bonsai/test/tool/test_blender.py @@ -16,6 +16,8 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +import tempfile +from pathlib import Path from typing import TYPE_CHECKING import bpy @@ -110,3 +112,35 @@ class TestBlenderErrorMessageExtraction(NewFile): assert error_reports == [] bpy.utils.unregister_class(OBJECT_OT_test_fail_operator) + + +class TestGetSelectedFiles(NewFile): + def test_get_a_single_file(self) -> None: + with tempfile.NamedTemporaryFile() as f: + file = type("", (object,), {"name": f.name})() + assert subject.get_selected_files(Path(f.name).parent, [file]) == [f.name] + + def test_get_multiple_files(self) -> None: + with tempfile.NamedTemporaryFile() as f: + with tempfile.NamedTemporaryFile() as g: + file = type("", (object,), {"name": f.name})() + file2 = type("", (object,), {"name": g.name})() + assert subject.get_selected_files(Path(f.name).parent, [file, file2]) == [f.name, g.name] + + def test_exclude_directories(self) -> None: + with tempfile.NamedTemporaryFile() as f: + with tempfile.TemporaryDirectory() as d: + file = type("", (object,), {"name": f.name})() + directory = type("", (object,), {"name": d})() + assert subject.get_selected_files(Path(f.name).parent, [file, directory]) == [f.name] + + def test_get_relative_paths(self) -> None: + with tempfile.TemporaryDirectory() as tmp_dir: + base_path = Path(tmp_dir) + with tempfile.NamedTemporaryFile(dir=tmp_dir, suffix=".ifc") as f: + tool.Ifc.set_path(str(f.name)) + with tempfile.NamedTemporaryFile(dir=tmp_dir) as g: + file = type("", (object,), {"name": g.name}) + assert subject.get_selected_files(Path(g.name).parent, [file], use_relative_path=True) == [ + Path(g.name).name + ] diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index f7beb94358..06091ff8c4 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -30,6 +30,8 @@ import ifcopenshell.guid import ifcopenshell.util.element import mathutils import numpy as np +import pytest +from ifcopenshell.util.shape_builder import ShapeBuilder from mathutils import Vector import bonsai.core.tool @@ -150,6 +152,34 @@ class TestDisableEditingSheets(NewFile): assert props.is_editing_sheets == False +class TestEditTextLiterals(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + obj = bpy.data.objects.new("Object", None) + element = ifc.createIfcAnnotation() + element.Representation = ifc.createIfcProductDefinitionShape() + context = ifc.createIfcGeometricRepresentationSubContext(ContextType="Plan", ContextIdentifier="Annotation") + item = ifc.createIfcTextLiteralWithExtent(Literal="Literal", Path="RIGHT", BoxAlignment="bottom-left") + builder = ShapeBuilder(tool.Ifc.get()) + polyline = builder.polyline([(0.0, 0.0, 0.0), (1.0, 0.0, 0.0)]) + representation = ifc.createIfcShapeRepresentation(ContextOfItems=context, Items=[item, polyline]) + element.Representation.Representations = [representation] + tool.Ifc.link(element, obj) + literal_attributes = [ + { + "Literal": "Foo", + "Path": "RIGHT", + "BoxAlignment": "bottom-left", + } + ] + subject.edit_text_literals(obj, literal_attributes) + assert len(ifc.by_type("IfcTextLiteralWithExtent")) == 1 + literal = ifc.by_type("IfcTextLiteralWithExtent")[0] + assert literal in representation.Items + assert literal.Literal == "Foo" + + class TestDisableEditingText(NewFile): def test_run(self): obj = bpy.data.objects.new("Object", None) @@ -904,7 +934,7 @@ class TestAddReferenceImage(NewFile): bpy.ops.bim.save_project(filepath=str(ifc_path), should_save_as=True) filepath = Path("test/files/image.jpg").absolute() - bpy.ops.bim.add_reference_image(filepath=str(filepath)) + bpy.ops.bim.add_reference_image(filepath=str(filepath), x_length=3.53982, y_length=2.0) obj = bpy.data.objects["IfcAnnotation/image"] assert obj is not None @@ -927,72 +957,4 @@ class TestAddReferenceImage(NewFile): assert texture_filepath == filepath uv_node = material_nodes["Texture Coordinate"] - assert len(uv_node.outputs["Generated"].links[:]) == 1 - - -class TestAddReference(NewFile): - def test_add_single_reference(self): - """Test adding a single reference file (backward compatibility)""" - bpy.ops.bim.create_project() - ifc_path = Path("test/files/temp/test.ifc").absolute() - bpy.ops.bim.save_project(filepath=str(ifc_path), should_save_as=True) - - # Create a temporary SVG file - svg_path = Path("test/files/temp/reference.svg").absolute() - svg_path.parent.mkdir(parents=True, exist_ok=True) - with open(svg_path, "w") as f: - f.write('') - - try: - # Add single reference - bpy.ops.bim.add_reference(filepath=str(svg_path)) - - # Verify reference was added - ifc = tool.Ifc.get() - references = [doc for doc in ifc.by_type("IfcDocumentInformation") if doc.Scope == "REFERENCE"] - assert len(references) == 1 - assert references[0].Name == "reference" - finally: - # Cleanup - if svg_path.exists(): - svg_path.unlink() - - def test_add_multiple_references(self): - """Test adding multiple reference files at once""" - bpy.ops.bim.create_project() - ifc_path = Path("test/files/temp/test.ifc").absolute() - bpy.ops.bim.save_project(filepath=str(ifc_path), should_save_as=True) - - # Create temporary SVG files - temp_dir = Path("test/files/temp").absolute() - temp_dir.mkdir(parents=True, exist_ok=True) - - svg_files = [] - for i in range(3): - svg_path = temp_dir / f"reference_{i}.svg" - with open(svg_path, "w") as f: - f.write('') - svg_files.append(svg_path) - - try: - # Test by directly calling core.add_document multiple times - # (simulating what the operator does with multiple files) - ifc = tool.Ifc.get() - for svg_file in svg_files: - uri = tool.Ifc.get_uri(str(svg_file), use_relative_path=True) - from bonsai.bim import core - - core.drawing.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=uri) - - # Verify all references were added - references = [doc for doc in ifc.by_type("IfcDocumentInformation") if doc.Scope == "REFERENCE"] - assert len(references) == 3 - - reference_names = {ref.Name for ref in references} - expected_names = {f"reference_{i}" for i in range(3)} - assert reference_names == expected_names - finally: - # Cleanup - for svg_file in svg_files: - if svg_file.exists(): - svg_file.unlink() + assert len(uv_node.outputs["UV"].links[:]) == 1 diff --git a/src/bonsai/test/tool/test_project.py b/src/bonsai/test/tool/test_project.py index 06c3f8ec25..c6d1c410de 100644 --- a/src/bonsai/test/tool/test_project.py +++ b/src/bonsai/test/tool/test_project.py @@ -17,8 +17,11 @@ # along with Bonsai. If not, see . import contextlib +import json import tempfile from pathlib import Path +from tempfile import NamedTemporaryFile +from typing import cast import bpy import ifcopenshell @@ -27,6 +30,7 @@ import ifcopenshell.api.document import ifcopenshell.api.root import ifcopenshell.api.unit import ifcpatch +import numpy as np from ifcpatch.recipes import Ifc2Sql import bonsai.core.tool @@ -263,7 +267,7 @@ class TestLoadLinkedModels(NewFile): props = tool.Project.get_project_props() ifcopenshell.api.root.create_entity(ifc, "IfcProject") document = ifcopenshell.api.document.add_information(ifc) - document.Name = "BBIM_Linked_Models" + document.Name = "X" tool.Ifc.set(ifc) subject.load_linked_models_from_ifc() assert len(props.links) == 0 @@ -273,86 +277,81 @@ class TestLoadLinkedModels(NewFile): props = tool.Project.get_project_props() ifcopenshell.api.root.create_entity(ifc, "IfcProject") document = ifcopenshell.api.document.add_information(ifc) - document.Name = "BBIM_Linked_Models" + document.Scope = "LINKED_MODEL" reference = ifcopenshell.api.document.add_reference(ifc, document) - linked_model_path = "test.ifc" - reference.Location = linked_model_path + reference.Location = "test.ifc" + reference.Identification = "" + reference2 = ifcopenshell.api.document.add_reference(ifc, document) + reference2.Location = "test2.ifc" + reference2.Identification = "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16" tool.Ifc.set(ifc) subject.load_linked_models_from_ifc() - assert len(props.links) == 1 - assert props.links[0].name == linked_model_path + assert len(props.links) == 2 + assert props.links[0].name == "test.ifc" + assert props.links[0].ifc_definition_id == reference.id() + assert props.links[0].has_transformation is False + assert props.links[1].name == "test2.ifc" + assert props.links[1].ifc_definition_id == reference2.id() + assert props.links[1].has_transformation is True -class TestSaveLinkedModelsToIfc(NewFile): - def test_save_linked_models_to_ifc_no_links(self): - ifc = ifcopenshell.file() - tool.Ifc.set(ifc) - subject.save_linked_models_to_ifc() - assert len(ifc.by_type("IfcDocumentInformation")) == 0 - assert len(ifc.by_type("IfcDocumentReference")) == 0 - - def test_save_linked_models_to_ifc_paths_to_add(self): - ifc = ifcopenshell.file() - ifcopenshell.api.root.create_entity(ifc, "IfcProject") +class TestCalculateLinkMatrix(NewFile): + def test_linking_a_model_without_an_offset_to_our_session_with_no_offset(self): props = tool.Project.get_project_props() - link = props.links.add() - linked_model_path = "test.ifc" - link.name = linked_model_path - tool.Ifc.set(ifc) - subject.save_linked_models_to_ifc() - assert len(documents := ifc.by_type("IfcDocumentInformation")) == 1 - assert documents[0].Name == "BBIM_Linked_Models" - assert len(references := ifc.by_type("IfcDocumentReference")) == 1 - assert references[0].Location == linked_model_path + gprops = tool.Georeference.get_georeference_props() + with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=True) as tmp: + link = props.links.add() + link.filepath = tmp.name.replace(".ifc.cache.json", ".ifc") + json.dump({"model_project_north": "0", "model_origin_si": "0,0,0"}, tmp) + tmp.flush() + gprops.model_project_north = "0" + gprops.model_origin_si = "0,0,0" + assert np.allclose(subject.calculate_link_matrix(link), np.eye(4)) - def test_save_linked_models_to_ifc_already_created_references(self): - ifc = ifcopenshell.file() - links = tool.Project.get_project_props().links - ifcopenshell.api.root.create_entity(ifc, "IfcProject") + def test_linking_an_offset_model_to_our_session_with_no_offset(self): + props = tool.Project.get_project_props() + gprops = tool.Georeference.get_georeference_props() + with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=True) as tmp: + link = props.links.add() + link.filepath = tmp.name.replace(".ifc.cache.json", ".ifc") + json.dump({"model_project_north": "0", "model_origin_si": "5,0,0"}, tmp) + tmp.flush() + gprops.model_project_north = "0" + gprops.model_origin_si = "0,0,0" + m = np.eye(4) + m[0][3] = 5 + assert np.allclose(subject.calculate_link_matrix(link), m) - document = ifcopenshell.api.document.add_information(ifc) - document.Name = "BBIM_Linked_Models" - document_id = document.id() - reference = ifcopenshell.api.document.add_reference(ifc, document) - linked_model_path = "test.ifc" - reference.Location = linked_model_path - reference_id = reference.id() + def test_linking_an_offset_model_to_our_session_with_offset(self): + props = tool.Project.get_project_props() + gprops = tool.Georeference.get_georeference_props() + with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=True) as tmp: + link = props.links.add() + link.filepath = tmp.name.replace(".ifc.cache.json", ".ifc") + json.dump({"model_project_north": "0", "model_origin_si": "5,0,0"}, tmp) + tmp.flush() + gprops.model_project_north = "0" + gprops.model_origin_si = "2,0,0" + m = np.eye(4) + m[0][3] = 3 + assert np.allclose(subject.calculate_link_matrix(link), m) - link = links.add() - linked_model_path = "test.ifc" - link.name = linked_model_path - tool.Ifc.set(ifc) - subject.save_linked_models_to_ifc() - - # Information and references to stay intact. - assert len(documents := ifc.by_type("IfcDocumentInformation")) == 1 - assert documents[0].id() == document_id - assert documents[0].Name == "BBIM_Linked_Models" - assert len(references := ifc.by_type("IfcDocumentReference")) == 1 - assert references[0].id() == reference_id - assert references[0].Location == linked_model_path - - def test_save_linked_models_to_ifc_references_to_remove(self): - ifc = ifcopenshell.file() - links = tool.Project.get_project_props().links - ifcopenshell.api.root.create_entity(ifc, "IfcProject") - - document = ifcopenshell.api.document.add_information(ifc) - document.Name = "BBIM_Linked_Models" - document_id = document.id() - reference = ifcopenshell.api.document.add_reference(ifc, document) - linked_model_path = "test.ifc" - reference.Location = linked_model_path - - tool.Ifc.set(ifc) - subject.save_linked_models_to_ifc() - links.clear() - - # Remove reference for removed link. - assert len(documents := ifc.by_type("IfcDocumentInformation")) == 1 - assert documents[0].id() == document_id - assert documents[0].Name == "BBIM_Linked_Models" - assert len(ifc.by_type("IfcDocumentReference")) == 0 + def test_linking_an_offset_model_to_our_session_with_offset_and_transformation(self): + props = tool.Project.get_project_props() + gprops = tool.Georeference.get_georeference_props() + with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=True) as tmp: + link = props.links.add() + link.filepath = tmp.name.replace(".ifc.cache.json", ".ifc") + transformation = np.eye(4) + transformation[0][3] = 4 + link.transformation = ",".join(map(str, transformation.reshape(-1))) + json.dump({"model_project_north": "0", "model_origin_si": "5,0,0"}, tmp) + tmp.flush() + gprops.model_project_north = "0" + gprops.model_origin_si = "2,0,0" + m = np.eye(4) + m[0][3] = 7 + assert np.allclose(subject.calculate_link_matrix(link), m) class TestLoadingIfcSqlite(NewFile): @@ -401,3 +400,50 @@ class TestLoadingIfcSqlite(NewFile): for element_name in elements_without_meshes: assert element_name in bpy.data.objects assert not bpy.data.objects[element_name].data + + +class TestGettingLinkedElementGeomSlice: + def __init__(self): + self.test_get_first_element() + self.test_get_middle_element() + self.test_skip_hidden_first_element() + self.test_skip_hidden_middle_element() + self.test_handle_hidden_non_first_element() + + TEST_OBJ = { + "guids": ["aaa", "bbb", "ccc"], + "guid_ids": [5, 10, 15], + } + + def test_get_first_element(self): + obj = TestGettingLinkedElementGeomSlice.TEST_OBJ + obj = cast(bpy.types.Object, obj) + slice_ = subject.Link.get_linked_element_geom_slice(obj, "aaa") + assert range(15)[slice_] == range(5) + + def test_get_middle_element(self): + obj = TestGettingLinkedElementGeomSlice.TEST_OBJ + obj = cast(bpy.types.Object, obj) + slice_ = subject.Link.get_linked_element_geom_slice(obj, "bbb") + assert range(15)[slice_] == range(5, 10) + + def test_skip_hidden_first_element(self): + obj = TestGettingLinkedElementGeomSlice.TEST_OBJ + obj = obj | {"hidden_indices": [0]} + obj = cast(bpy.types.Object, obj) + slice_ = subject.Link.get_linked_element_geom_slice(obj, "bbb") + assert range(15)[slice_] == range(5) + + def test_skip_hidden_middle_element(self): + obj = TestGettingLinkedElementGeomSlice.TEST_OBJ + obj = obj | {"hidden_indices": [1]} + obj = cast(bpy.types.Object, obj) + slice_ = subject.Link.get_linked_element_geom_slice(obj, "ccc") + assert range(15)[slice_] == range(5, 10) + + def test_handle_hidden_non_first_element(self): + obj = TestGettingLinkedElementGeomSlice.TEST_OBJ + obj = obj | {"hidden_indices": [1]} + obj = cast(bpy.types.Object, obj) + slice_ = subject.Link.get_linked_element_geom_slice(obj, "aaa") + assert range(15)[slice_] == range(5) diff --git a/src/examples/CMakeLists.txt b/src/examples/CMakeLists.txt index 79a2399821..a7c99043bd 100644 --- a/src/examples/CMakeLists.txt +++ b/src/examples/CMakeLists.txt @@ -36,11 +36,15 @@ else() endif() macro(build_example exe_name) - set (additional_targets ${ARGN}) + set(additional_targets ${ARGN}) add_executable(${exe_name} ${exe_name}.cpp) if(STANDALONE_PROJECT) - target_link_libraries(${exe_name} IfcOpenShell::IfcParse $<$:IfcOpenShell::${additional_targets}>) + target_link_libraries( + ${exe_name} + IfcOpenShell::IfcParse + $<$:IfcOpenShell::${additional_targets}> + ) else() target_include_directories(${exe_name} PRIVATE "${CMAKE_SOURCE_DIR}/../src") target_link_libraries(${exe_name} IfcParse ${additional_targets}) diff --git a/src/ifc2ca/_deprecated/scriptCodeAsterBonded.py b/src/ifc2ca/_deprecated/scriptCodeAsterBonded.py index 0b1184453b..e5bfd03aed 100644 --- a/src/ifc2ca/_deprecated/scriptCodeAsterBonded.py +++ b/src/ifc2ca/_deprecated/scriptCodeAsterBonded.py @@ -89,27 +89,22 @@ class COMMANDFILE: f.write("# Linear Static Analysis With Self-Weight\n") - f.write( - """ + f.write(""" # STEP: INITIALIZE STUDY DEBUT( PAR_LOT = 'NON' ) -""" - ) +""") - f.write( - """ + f.write(""" # STEP: READ MED FILE mesh = LIRE_MAILLAGE( FORMAT = 'MED', UNITE = 20 ) -""" - ) +""") - f.write( - """ + f.write(""" # STEP: DEFINE MODEL model = AFFE_MODELE( MAILLAGE = mesh, @@ -118,8 +113,7 @@ model = AFFE_MODELE( TOUT = 'OUI', PHENOMENE = 'MECANIQUE', MODELISATION = '3D' - ),""" - ) + ),""") if faceGroupNames: template = """ @@ -157,12 +151,10 @@ model = AFFE_MODELE( f.write(template.format(**context)) - f.write( - """ + f.write(""" ) )\n -""" - ) +""") f.write("# STEP: DEFINE MATERIALS") @@ -195,12 +187,10 @@ model = AFFE_MODELE( f.write(template.format(**context)) - f.write( - """ + f.write(""" material = AFFE_MATERIAU( MAILLAGE = mesh, - AFFE = (""" - ) + AFFE = (""") for i, material in enumerate(materials): template = """ @@ -227,20 +217,16 @@ material = AFFE_MATERIAU( f.write(template.format(**context)) - f.write( - """ + f.write(""" ) ) -""" - ) +""") - f.write( - """ + f.write(""" # STEP: DEFINE ELEMENTS element = AFFE_CARA_ELEM( MODELE = model, - POUTRE = (""" - ) + POUTRE = (""") for profile in profiles: if profile["profileShape"] == "rectangular" and profile["profileType"] == "AREA": @@ -296,11 +282,9 @@ element = AFFE_CARA_ELEM( f.write(template.format(**context)) - f.write( - """ + f.write(""" ), - COQUE = (""" - ) + COQUE = (""") for el in [el for el in elements if el["geometryType"] == "surface"]: @@ -319,15 +303,11 @@ element = AFFE_CARA_ELEM( f.write(template.format(**context)) - f.write( - """ - ),""" - ) + f.write(""" + ),""") - f.write( - """ - ORIENTATION = (""" - ) + f.write(""" + ORIENTATION = (""") for el in [el for el in elements if el["geometryType"] == "line"]: @@ -345,21 +325,16 @@ element = AFFE_CARA_ELEM( f.write(template.format(**context)) - f.write( - """ - ),""" - ) + f.write(""" + ),""") - f.write( - """ + f.write(""" )\n -""" - ) +""") f.write("# STEP: DEFINE SUPPORTS AND CONSTRAINTS") - f.write( - """ + f.write(""" liaisons = AFFE_CHAR_MECA( MODELE = model, DDL_IMPO = ( @@ -372,14 +347,11 @@ liaisons = AFFE_CHAR_MECA( DRY = 0.0, DRZ = 0.0 ) - ),""" - ) + ),""") if rigidLinkGroupNames: - f.write( - """ - LIAISON_SOLIDE = (""" - ) + f.write(""" + LIAISON_SOLIDE = (""") for groupName in rigidLinkGroupNames: template = """ @@ -391,16 +363,12 @@ liaisons = AFFE_CHAR_MECA( f.write(template.format(**context)) - f.write( - """ - ),""" - ) + f.write(""" + ),""") - f.write( - """ + f.write(""" ) -""" - ) +""") template = """ # STEP: DEFINE LOAD @@ -418,8 +386,7 @@ gravLoad = AFFE_CHAR_MECA( f.write(template.format(**context)) - f.write( - """ + f.write(""" # STEP: RUN ANALYSIS res_Bld = MECA_STATIQUE( MODELE = model, @@ -434,8 +401,7 @@ res_Bld = MECA_STATIQUE( ) ) ) -""" - ) +""") # f.write( # ''' @@ -515,8 +481,7 @@ res_Bld = MECA_STATIQUE( # ''' # ) # - f.write( - """ + f.write(""" # STEP: DEFORMED SHAPE EXTRACTION IMPR_RESU( FORMAT = 'MED', @@ -527,15 +492,12 @@ IMPR_RESU( NOM_CHAM_MED = ('Bld_DISP',), # 'Bld_REAC', 'Bld_FORC' ) ) -""" - ) +""") - f.write( - """ + f.write(""" # STEP: CONCLUDE STUDY FIN() -""" - ) +""") f.close() diff --git a/src/ifc2ca/_deprecated/scriptSalomeBonded.py b/src/ifc2ca/_deprecated/scriptSalomeBonded.py index fb2bd575f9..d3b6d506c8 100644 --- a/src/ifc2ca/_deprecated/scriptSalomeBonded.py +++ b/src/ifc2ca/_deprecated/scriptSalomeBonded.py @@ -53,16 +53,16 @@ class MODEL: """Function to define a Point from a polyline (list of 1 point)""" - (x, y, z) = pl + x, y, z = pl return self.geompy.MakeVertex(x, y, z) def makeLine(self, pl): """Function to define a Line from a polyline (list of 2 points)""" - (x, y, z) = pl[0] + x, y, z = pl[0] P1 = self.geompy.MakeVertex(x, y, z) - (x, y, z) = pl[1] + x, y, z = pl[1] P2 = self.geompy.MakeVertex(x, y, z) return self.geompy.MakeLineTwoPnt(P1, P2) diff --git a/src/ifcbimtester/bimtester/features/steps/aggregation/en.py b/src/ifcbimtester/bimtester/features/steps/aggregation/en.py index a2bbe621a1..f0c4af8f15 100644 --- a/src/ifcbimtester/bimtester/features/steps/aggregation/en.py +++ b/src/ifcbimtester/bimtester/features/steps/aggregation/en.py @@ -28,12 +28,8 @@ use_step_matcher("parse") @step('There must be exactly {number} "{ifc_class}" elements') def step_impl(context, number, ifc_class): num = len(IfcStore.file.by_type(ifc_class)) - assert num == int( - number - ), "Could not find {} elements of {}. \ - Found {} element(s).".format( - number, ifc_class, num - ) + assert num == int(number), "Could not find {} elements of {}. \ + Found {} element(s).".format(number, ifc_class, num) @given('a set of (key,value) called ("{key_name}","{value_name}")') @@ -95,13 +91,9 @@ def step_impl(context, attribute_name): @then('there must be exactly a number of "{ifc_class}" equals to the number of distinct value') def step_impl(context, ifc_class): try: - context.execute_steps( - """ + context.execute_steps(""" then There must be exactly {number} "{ifc_class}" elements - """.format( - ifc_class=ifc_class, number=context.model.get_count_distinct_values() - ) - ) + """.format(ifc_class=ifc_class, number=context.model.get_count_distinct_values())) except AssertionError as error: str_error = str(error) assert False, str_error[: str_error.find("Traceback")] diff --git a/src/ifcbimtester/examples/steps/aggregation.py b/src/ifcbimtester/examples/steps/aggregation.py index ccf46b5e0d..96e0375b43 100644 --- a/src/ifcbimtester/examples/steps/aggregation.py +++ b/src/ifcbimtester/examples/steps/aggregation.py @@ -51,13 +51,9 @@ def step_impl(context, path_file): @then("there must be exactly a number of {ifc_class} equals to the number of distinct row value") def step_impl(context, ifc_class): try: - context.execute_steps( - """ + context.execute_steps(""" then There must be exactly {number} {ifc_class} elements - """.format( - ifc_class=ifc_class, number=context.model.get_count_distinct_values() - ) - ) + """.format(ifc_class=ifc_class, number=context.model.get_count_distinct_values())) except AssertionError as error: str_error = str(error) assert False, str_error[: str_error.find("Traceback")] diff --git a/src/ifcgeom/Converter.cpp b/src/ifcgeom/Converter.cpp index 6b70ac7f51..a50a43cc66 100644 --- a/src/ifcgeom/Converter.cpp +++ b/src/ifcgeom/Converter.cpp @@ -12,11 +12,8 @@ ifcopenshell::geometry::Converter::Converter(std::unique_ptrsettings(); } -ifcopenshell::geometry::Converter::~Converter() -{ - if (mapping_ != nullptr) { - delete mapping_; - } +ifcopenshell::geometry::Converter::~Converter() { + delete mapping_; } namespace { diff --git a/src/ifcgeom/kernels/opencascade/loft.cpp b/src/ifcgeom/kernels/opencascade/loft.cpp index 60997d24ad..b055b7743b 100644 --- a/src/ifcgeom/kernels/opencascade/loft.cpp +++ b/src/ifcgeom/kernels/opencascade/loft.cpp @@ -82,49 +82,79 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re } if (non_polygonal) { - if (loft->children.size() == 2) { - BRep_Builder BB; - TopoDS_Shell comp; - BB.MakeShell(comp); + if (loft->children.size() < 2) { + Logger::Error("Not enough sections to loft"); + return false; + } + std::vector> sections; + sections.reserve(loft->children.size()); - TopoDS_Shape f0, f1; - if (!convert(std::static_pointer_cast(loft->children.front()), f0) || - !convert(std::static_pointer_cast(loft->children.back()), f1)) - { + TopoDS_Shape f0, f1; + + // Convert all children to vectors of wires + for (const auto& child : loft->children) { + TopoDS_Shape shape; + if (!convert(std::static_pointer_cast(child), shape)) { + return false; + } + if (shape.ShapeType() != TopAbs_FACE) { + return false; + } + // At least make sure to have outer wire consistent, but in reality + // this is probably not a concern given how to build up these faces + auto f = TopoDS::Face(shape); + + if (child == loft->children.front()) { + f0 = f; + } else if (child == loft->children.back()) { + f1 = f; + } + + auto outer = BRepTools::OuterWire(f); + sections.emplace_back(); + sections.back().push_back(outer); + for (TopoDS_Iterator it(f); it.More(); it.Next()) { + if (outer != it.Value()) { + sections.back().push_back(TopoDS::Wire(it.Value())); + } + } + } + + auto first_wire_count = sections.front().size(); + for (auto& section : sections) { + if (section.size() != first_wire_count) { + Logger::Error("Inconsistent number of wires in sections"); return false; } - if (f0.ShapeType() != TopAbs_FACE || f1.ShapeType() != TopAbs_FACE) { + } + + BRep_Builder BB; + TopoDS_Shell comp; + BB.MakeShell(comp); + + for (size_t i = 0; i < first_wire_count; ++i) { + // Rule=True uses linear interpolation. + // This is critical for preventing twists in roads/railings. + BRepOffsetAPI_ThruSections builder(false, true); + for (auto& ws : sections) { + builder.AddWire(ws[i]); + } + builder.Build(); + if (!builder.IsDone()) { return false; } - - TopExp_Explorer exp1(f0, TopAbs_WIRE); - TopExp_Explorer exp2(f1, TopAbs_WIRE); - for (; exp1.More() && exp2.More(); exp1.Next(), exp2.Next()) { - const auto& w1 = TopoDS::Wire(exp1.Current()); - const auto& w2 = TopoDS::Wire(exp2.Current()); - BRepOffsetAPI_ThruSections builder; - builder.AddWire(w1); - builder.AddWire(w2); - builder.Build(); - if (!builder.IsDone()) { - return false; - } - for (TopExp_Explorer exp(builder.Shape(), TopAbs_FACE); exp.More(); exp.Next()) { - BB.Add(comp, exp.Current()); - } + for (TopExp_Explorer exp(builder.Shape(), TopAbs_FACE); exp.More(); exp.Next()) { + BB.Add(comp, exp.Current()); } - - BB.Add(comp, f0.Reversed()); - BB.Add(comp, f1); - - result = BRepBuilderAPI_MakeSolid(comp).Solid(); - - return true; - } else { - Logger::Error("Lofting more than two sections is not supported"); - return false; } + + BB.Add(comp, f0.Reversed()); + BB.Add(comp, f1); + + result = BRepBuilderAPI_MakeSolid(comp).Solid(); + + return true; } TopTools_ListOfShape faces; @@ -135,13 +165,6 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re std::vector shps(loft->children.size()); std::vector>> all_tags; - - std::ostringstream oss; - loft->children[0]->print(oss); - loft->children[1]->print(oss); - auto s = oss.str(); - std::wcout << s.c_str() << std::endl; - // First convert all taxonomy items to TopoDS_Wire/Face for (auto it = loft->children.begin(); it < loft->children.end(); ++it) { auto i = std::distance(loft->children.begin(), it); @@ -237,6 +260,18 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re return true; */ + if (shps.size() < 2) { + Logger::Error("Not enough sections to loft"); + return false; + } + + if (shps[0].ShapeType() == TopAbs_FACE) { + // When processing a sectioned *surface* there are no + // begin and end caps that need to be added. + BB.Add(comp, shps.front().Reversed()); + BB.Add(comp, shps.back()); + } + // @todo this approach is // potentially incorrect as there is no guarantee that the wires for // subsequently placed profiles are traversed from an equivalent start vertex. @@ -262,18 +297,6 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re ws[0][i] = TopoDS::Wire(*fa[i]); } } - if (it->ShapeType() == TopAbs_FACE) { - // When processing a sectioned *surface* there are no - // begin and end caps that need to be added. - if (it == shps.begin()) { - // faces.Append(shps[0]); - BB.Add(comp, shps[0]); - } - if (jt == shps.end() - 1) { - // faces.Append(shps[1]); - BB.Add(comp, shps[1]); - } - } if (!all_tags.empty()) { // only open profiles have tags for now, so there is only one wire, no inner wires diff --git a/src/ifcgeom/mapping/CMakeLists.txt b/src/ifcgeom/mapping/CMakeLists.txt index 4e185b9fa9..6369b98b60 100644 --- a/src/ifcgeom/mapping/CMakeLists.txt +++ b/src/ifcgeom/mapping/CMakeLists.txt @@ -1,12 +1,21 @@ find_package(Eigen3 REQUIRED) +# When using `ENABLE_BUILD_OPTIMIZATIONS``/GL` compilation flags makes resulting mapping libs huge (e.g. 1.5GB each) +# and combining them together to a single IfcGeom won't be possible due to 4GB file size limit. +# So in this case we have to ensure each mapping is built as separate libs instead of .obj files to be linked together. +if(ENABLE_BUILD_OPTIMIZATIONS AND MSVC) + set(mapping_library_type "STATIC") +else() + set(mapping_library_type "OBJECT") +endif() + foreach(schema ${SCHEMA_VERSIONS}) file(GLOB IFCGEOM_I_FILES *.i) file(GLOB IFCGEOM_H_FILES *.h) file(GLOB IFCGEOM_CPP_FILES *.cpp) set(IFCGEOM_FILES ${IFCGEOM_CPP_FILES} ${IFCGEOM_H_FILES} ${IFCGEOM_I_FILES}) - add_library(geometry_mapping_ifc${schema} OBJECT ${IFCGEOM_FILES}) + add_library(geometry_mapping_ifc${schema} ${mapping_library_type} ${IFCGEOM_FILES}) set_target_properties( geometry_mapping_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}" diff --git a/src/ifcgeom/mapping/IfcObjectPlacement.cpp b/src/ifcgeom/mapping/IfcObjectPlacement.cpp index a0a37dd30f..66744d5571 100644 --- a/src/ifcgeom/mapping/IfcObjectPlacement.cpp +++ b/src/ifcgeom/mapping/IfcObjectPlacement.cpp @@ -37,7 +37,6 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcObjectPlacement* inst) { } auto self_places = placement->PlacesObject(); - inst->ReferencedByPlacements(); for (auto iter = self_places->begin(); iter != self_places->end(); ++iter) { if ((placement_rel_to_type_ && (*iter)->declaration().is(*placement_rel_to_type_)) || (placement_rel_to_instance_ && (*iter)->as() == placement_rel_to_instance_)) { @@ -47,12 +46,16 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcObjectPlacement* inst) { // Look for two levels deep, we want to know if we're at or *above* the // element we're ignoring, but we don't want to traverse the entire model. +#ifdef SCHEMA_IfcObjectPlacement_HAS_ReferencedByPlacements if (depth < 2) { auto refs = placement->ReferencedByPlacements(); for (auto& ref : *refs) { q.emplace_back(ref, depth + 1); } } +#else + Logger::Warning("Using --site-local-placement or --building-local-placement on IFC4.2 might have issues"); +#endif } } @@ -123,7 +126,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcObjectPlacement* inst) { if (fallback) { auto mapped_fallback = taxonomy::cast(map(fallback)); - if (mapped_fallback != result) { + if (!result->ccomponents().isApprox(mapped_fallback->ccomponents())) { Logger::Warning("Computed placement differs from fallback", inst); } } diff --git a/src/ifcgeom/mapping/IfcRevolvedAreaSolid.cpp b/src/ifcgeom/mapping/IfcRevolvedAreaSolid.cpp index 8b2fe01272..f6a9fc861c 100644 --- a/src/ifcgeom/mapping/IfcRevolvedAreaSolid.cpp +++ b/src/ifcgeom/mapping/IfcRevolvedAreaSolid.cpp @@ -43,11 +43,20 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcRevolvedAreaSolid* inst) { angle = ang; } + taxonomy::direction3::ptr axis; + if (inst->Axis()->Axis()) { + axis = taxonomy::cast(map(inst->Axis()->Axis())); + } else { + // IfcAxis1Placement.Axis is optional, and defaults to (0, 0, 1) if not provided. + axis = taxonomy::make(0, 0, 1); + } + + return taxonomy::make( matrix, taxonomy::cast(map(inst->SweptArea())), taxonomy::cast(map(inst->Axis()->Location())), - taxonomy::cast(map(inst->Axis()->Axis())), + axis, angle ); diff --git a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp index a2bc648f08..f2556092b4 100644 --- a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp +++ b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp @@ -61,33 +61,16 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in longitudes.push_back(*pbde->DistanceAlong()->as(true) * length_unit_); - // Corresponds to the profile X, Y directions (hopefully). Eigen::Vector3d po( - pbde->OffsetLateral().get_value_or(0.), - // @todo I don't understand whether vertical is an offset relative to the tangent plane or to the global XY plane - pbde->OffsetVertical().get_value_or(0.), - 0. - ); + pbde->OffsetLateral().get_value_or(0.), + // @todo I don't understand whether vertical is an offset relative to the tangent plane or to the global XY plane + pbde->OffsetVertical().get_value_or(0.), + 0.); - profile_offsets.push_back(po); + profile_offsets.push_back(po); - boost::optional rot; - if (csp->Axis() && csp->RefDirection()) { - rot = taxonomy::matrix4( - Eigen::Vector3d(0, 0, 0), - taxonomy::cast(map(csp->Axis()))->ccomponents(), - taxonomy::cast(map(csp->RefDirection()))->ccomponents()).ccomponents().block<3,3>(0,0); - } else if (csp->Axis()) { - rot = taxonomy::matrix4( - Eigen::Vector3d(0, 0, 0), - taxonomy::cast(map(csp->Axis()))->ccomponents()).ccomponents().block<3, 3>(0, 0); - } else if (csp->RefDirection()) { - rot = taxonomy::matrix4( - Eigen::Vector3d(0, 0, 0), - Eigen::Vector3d(0, 0, 1), - taxonomy::cast(map(csp->RefDirection()))->ccomponents() - ).ccomponents().block<3, 3>(0, 0); - } + auto axis2_placement_linear = taxonomy::cast(map(csp)); + boost::optional rot(axis2_placement_linear->ccomponents().block<3, 3>(0, 0)); profile_rotations.push_back(rot); } if (faces.size() != profile_offsets.size()) { diff --git a/src/ifcgeom/mapping/IfcSectionedSurface.cpp b/src/ifcgeom/mapping/IfcSectionedSurface.cpp index 49fde15759..efdeda5757 100644 --- a/src/ifcgeom/mapping/IfcSectionedSurface.cpp +++ b/src/ifcgeom/mapping/IfcSectionedSurface.cpp @@ -63,36 +63,18 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSurface* inst) { longitudes.push_back(*pbde->DistanceAlong()->as(true) * length_unit_); - // Corresponds to the profile X, Y directions (hopefully). - Eigen::Vector3d po( - pbde->OffsetLateral().get_value_or(0.), - // @todo I don't understand whether vertical is an offset relative to the tangent plane or to the global XY plane - pbde->OffsetVertical().get_value_or(0.), - 0. - ); + Eigen::Vector3d po( + pbde->OffsetLateral().get_value_or(0.), + // @todo I don't understand whether vertical is an offset relative to the tangent plane or to the global XY plane + pbde->OffsetVertical().get_value_or(0.), + 0.); - profile_offsets.push_back(po); + profile_offsets.push_back(po); - boost::optional rot; - if (csp->Axis() && csp->RefDirection()) { - rot = taxonomy::matrix4( - Eigen::Vector3d(0, 0, 0), - taxonomy::cast(map(csp->Axis()))->ccomponents(), - taxonomy::cast(map(csp->RefDirection()))->ccomponents()).ccomponents().block<3, 3>(0, 0); - } else if (csp->Axis()) { - rot = taxonomy::matrix4( - Eigen::Vector3d(0, 0, 0), - taxonomy::cast(map(csp->Axis()))->ccomponents()).ccomponents().block<3, 3>(0, 0); - } else if (csp->RefDirection()) { - rot = taxonomy::matrix4( - Eigen::Vector3d(0, 0, 0), - Eigen::Vector3d(0, 0, 1), - taxonomy::cast(map(csp->RefDirection()))->ccomponents()) - .ccomponents() - .block<3, 3>(0, 0); - } - profile_rotations.push_back(rot); - } + auto axis2_placement_linear = taxonomy::cast(map(csp)); + boost::optional rot(axis2_placement_linear->ccomponents().block<3, 3>(0, 0)); + profile_rotations.push_back(rot); + } #else return nullptr; #endif diff --git a/src/ifcgeom/mapping/IfcSurfaceOfRevolution.cpp b/src/ifcgeom/mapping/IfcSurfaceOfRevolution.cpp index f83c4ecf03..c5fe21b58b 100644 --- a/src/ifcgeom/mapping/IfcSurfaceOfRevolution.cpp +++ b/src/ifcgeom/mapping/IfcSurfaceOfRevolution.cpp @@ -31,11 +31,19 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSurfaceOfRevolution* inst) { matrix = taxonomy::cast(map(inst->Position())); } + taxonomy::direction3::ptr axis; + if (inst->AxisPosition()->Axis()) { + axis = taxonomy::cast(map(inst->AxisPosition()->Axis())); + } else { + // IfcAxis1Placement.Axis is optional, and defaults to (0, 0, 1) if not provided. + axis = taxonomy::make(0, 0, 1); + } + return taxonomy::make( matrix, taxonomy::cast(map(inst->SweptCurve())), taxonomy::cast(map(inst->AxisPosition()->Location())), - taxonomy::cast(map(inst->AxisPosition()->Axis())), + axis, boost::none ); } diff --git a/src/ifcgeom/mapping/IfcToroidalSurface.cpp b/src/ifcgeom/mapping/IfcToroidalSurface.cpp index ce17a37dbc..d73e90bb1b 100644 --- a/src/ifcgeom/mapping/IfcToroidalSurface.cpp +++ b/src/ifcgeom/mapping/IfcToroidalSurface.cpp @@ -24,16 +24,11 @@ using namespace ifcopenshell::geometry; #ifdef SCHEMA_HAS_IfcToroidalSurface taxonomy::ptr mapping::map_impl(const IfcSchema::IfcToroidalSurface* inst) { - return nullptr; - - /* - gp_Trsf trsf; - IfcGeom::Kernel::convert(inst->Position(), trsf); - - // IfcElementarySurface.Position has unit scale factor - face = BRepBuilderAPI_MakeFace(new Geom_ToroidalSurface(gp::XOY(), inst->MajorRadius() * length_unit_, inst->MinorRadius() * length_unit_), getValue(GV_PRECISION)).Face().Moved(trsf); - return true; - */ + auto c = taxonomy::make(); + c->radius1 = inst->MajorRadius() * length_unit_; + c->radius2 = inst->MinorRadius() * length_unit_; + c->matrix = taxonomy::cast(map(inst->Position())); + return c; } #endif diff --git a/src/ifcgeom/mapping/mapping.cpp b/src/ifcgeom/mapping/mapping.cpp index f98bfe232d..edfdd3a13b 100644 --- a/src/ifcgeom/mapping/mapping.cpp +++ b/src/ifcgeom/mapping/mapping.cpp @@ -691,6 +691,10 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSurfaceStyle* style) { } taxonomy::ptr mapping::map(const IfcBaseInterface* inst) { + if (inst == nullptr) { + Logger::Error("Warning nullptr passed to map() function"); + return nullptr; + } auto iden = inst->as()->identity(); if (use_caching_) { std::lock_guard guard(cache_guard_); diff --git a/src/ifcgeom/taxonomy.cpp b/src/ifcgeom/taxonomy.cpp index 7d509db8cb..2ce115dfd1 100644 --- a/src/ifcgeom/taxonomy.cpp +++ b/src/ifcgeom/taxonomy.cpp @@ -543,8 +543,8 @@ piecewise_function::const_ptr offset_function::get_offset() const { return offse ifcopenshell::geometry::taxonomy::collection::ptr ifcopenshell::geometry::flatten(const taxonomy::collection::ptr& deep) { auto flat = make(); ifcopenshell::geometry::visit(deep, [&flat](taxonomy::ptr i) { - flat->children.push_back(taxonomy::cast(clone(i))); - }); + flat->children.push_back(std::static_pointer_cast(clone(i))); + }); return flat; } diff --git a/src/ifcgeom/taxonomy.h b/src/ifcgeom/taxonomy.h index ab8a222aee..5d70d4490c 100644 --- a/src/ifcgeom/taxonomy.h +++ b/src/ifcgeom/taxonomy.h @@ -1622,25 +1622,19 @@ typedef item const* ptr; for (auto& i : deep->children) { // @todo Sad... now that we have templated collection members, // we can't generally use collection_base anymore as a cast target. - if (auto s = taxonomy::dcast(i)) { + if (auto s = std::dynamic_pointer_cast(i)) { visit(s, fn); - } - else if (auto s = taxonomy::dcast(i)) { + } else if (auto s = std::dynamic_pointer_cast(i)) { visit(s, fn); - } - else if (auto s = taxonomy::dcast(i)) { + } else if (auto s = std::dynamic_pointer_cast(i)) { visit(s, fn); - } - else if (auto s = taxonomy::dcast(i)) { + } else if (auto s = std::dynamic_pointer_cast(i)) { visit(s, fn); - } - else if (auto s = taxonomy::dcast(i)) { + } else if (auto s = std::dynamic_pointer_cast(i)) { visit(s, fn); - } - else if (auto s = taxonomy::dcast(i)) { + } else if (auto s = std::dynamic_pointer_cast(i)) { visit(s, fn); - } - else if (auto s = taxonomy::dcast(i)) { + } else if (auto s = std::dynamic_pointer_cast(i)) { visit(s, fn); } else { diff --git a/src/ifcopenshell-python/Makefile b/src/ifcopenshell-python/Makefile index 78370a46fb..90c503d145 100644 --- a/src/ifcopenshell-python/Makefile +++ b/src/ifcopenshell-python/Makefile @@ -68,14 +68,15 @@ ifeq ($(PLATFORM), win64) PLATFORMTAG:=win_amd64 endif -BINARY_VERSION:=0.8.4 -BUILD_COMMIT:=e8eb5e4 +BINARY_VERSION:=0.8.5 +BUILD_COMMIT:=1c5b825 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: @echo "You can provide one of the 4 platforms (linux64, macos64, macosm164, win64) using 'PLATFORM=xxx'." + @echo "And Python version using 'PYNUMBER=xx' (e.g. 'PYNUMBER=311')." @echo ${IOS_URL} @echo ${IFCCONVERT_URL} diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst index 2d0cc9e071..4e28e83ed4 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst @@ -254,7 +254,7 @@ nest formulas, for example ``concat(title("foo"), lower("Bar"))`` will produce "``imperial_length({{value}}, {{precision}}, {{input_unit}}, {{output_unit}}, {{suppress_zero_inches}})``", "``imperial_length(3.0, 4, ""foot"", ""foot"", true)`` OR ``imperial_length(3.0, 4, ""foot"", ""foot"", false)``", "``3'`` OR ``3' - 0""``", "The ``{{value}}`` may be specified either as ``foot`` or ``inch`` depending on ``{{input_unit}}``. The ``{{value}}`` is then rounded to the nearest ``1/{{precision}}`` inch, then formatted using fractional feet and inches if ``{{output_unit}}`` is set to ``foot``, or just inches if ``{{output_unit}}`` is set to ``inch``. When ``{{suppress_zero_inches}}`` is ``true`` (default), measurements with zero inches will omit the inch portion (e.g., ``3'`` instead of ``3' - 0""``)." "``sort({{values}})``", "``sort({{mats.Name}})``", "``Name1, Name2``", "Sorts a list of items." "``reverse({{values}})``", "``reverse({{mats.Name}})``", "``Name2, Name1``", "Reverses a list of items." - "``join({{separator}}, {{values}})``", "``join("-", {{mats.Name}})``", "``Name1-Name2``", "Joins a list of items with a custom separator. By default, all lists a rendered as comma separated." + "``join({{separator}}, {{values}})``", "``join(""-"", {{mats.Name}})``", "``Name1-Name2``", "Joins a list of items with a custom separator. By default, all lists a rendered as comma separated." "``{{value1}}[+-*/]{{value2}}``", "``{{z}}+3``", "``5``", "Does arithmetic. Typical operators such as +, -, \*, and / are allowed and can be mixed with other variables and formatting functions." When using queries in an IfcAnnotation tag surround with backticks. diff --git a/src/ifcopenshell-python/ifcopenshell/__init__.py b/src/ifcopenshell-python/ifcopenshell/__init__.py index fb38426aff..998eb6e5de 100644 --- a/src/ifcopenshell-python/ifcopenshell/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/__init__.py @@ -53,6 +53,7 @@ Example: for wall in walls: print(wall.Name) """ + from __future__ import annotations import os diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/_create_offset_curve_representation.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/_create_offset_curve_representation.py index a5675a4dc2..28a9f6ef49 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/_create_offset_curve_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/_create_offset_curve_representation.py @@ -36,7 +36,7 @@ def _create_offset_curve_representation( expected_type = "IfcAlignment" if not alignment.is_a(expected_type): raise TypeError(f"Expected {expected_type} but got {alignment.is_a()}") - + expected_type = "IfcPointByDistanceExpression" for offset in offsets: if not offset.is_a(expected_type): diff --git a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py index 0a5aee0e1c..0f09bd4991 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py @@ -40,7 +40,7 @@ APPENDABLE_ASSET = Literal[ "IfcProfileDef", "IfcPresentationStyle", ] -APPENDABLE_ASSET_TYPES = get_args(APPENDABLE_ASSET) +APPENDABLE_ASSET_TYPES: tuple[APPENDABLE_ASSET, ...] = get_args(APPENDABLE_ASSET) MATERIAL_SETS = ("IfcMaterialLayerSet", "IfcMaterialConstituentSet", "IfcMaterialProfileSet") diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py index 4446971f0d..f19be2360b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py +++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py @@ -63,14 +63,15 @@ def reference_structure( storey1 = ifcopenshell.api.root.create_entity(model, ifc_class="IfcBuildingStorey") storey2 = ifcopenshell.api.root.create_entity(model, ifc_class="IfcBuildingStorey") storey3 = ifcopenshell.api.root.create_entity(model, ifc_class="IfcBuildingStorey") + space = ifcopenshell.api.root.create_entity(model, ifc_class="IfcSpace") # The project contains a site (note that project aggregation is a special case in IFC) ifcopenshell.api.aggregate.assign_object(model, products=[site], relating_object=project) # The site has a building, the building has a storey, and the storey has a space ifcopenshell.api.aggregate.assign_object(model, products=[building], relating_object=site) - ifcopenshell.api.aggregate.assign_object(model, products=[storey], relating_object=building) - ifcopenshell.api.aggregate.assign_object(model, products=[space], relating_object=storey) + ifcopenshell.api.aggregate.assign_object(model, products=[storey1,storey2,storey3], relating_object=building) + ifcopenshell.api.aggregate.assign_object(model, products=[space], relating_object=storey1) # Create a column, this column spans 3 storeys column = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall") @@ -80,7 +81,11 @@ def reference_structure( # And referenced in the others ifcopenshell.api.spatial.reference_structure( - model, products=[column], relating_structure=[storey2, storey3] + model, products=[column], relating_structure=storey2 + ) + + ifcopenshell.api.spatial.reference_structure( + model, products=[column], relating_structure=storey3 ) """ diff --git a/src/ifcopenshell-python/ifcopenshell/express/bootstrap.py b/src/ifcopenshell-python/ifcopenshell/express/bootstrap.py index 9ff87d5ad0..ef3c3c3ef0 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/bootstrap.py +++ b/src/ifcopenshell-python/ifcopenshell/express/bootstrap.py @@ -163,7 +163,18 @@ statements = [] terminals = reduce(lambda x, y: x | y, (find_bytype(e, Terminal) for id, e in express)) keywords = list(filter(operator.attrgetter("is_keyword"), terminals)) negated_keywords = map(lambda s: "~%s" % s, keywords) -no_action = {"letter", "digit", "digits", "real_literal", "integer_literal", "string_literal", "simple_string_literal", "letter", "not_quote", "not_paren_star_quote_special"} +no_action = { + "letter", + "digit", + "digits", + "real_literal", + "integer_literal", + "string_literal", + "simple_string_literal", + "letter", + "not_quote", + "not_paren_star_quote_special", +} while True: emitted_in_loop = set() @@ -194,7 +205,9 @@ for id in to_emit: if id in to_combine: stmt = "Suppress%s" % stmt if id not in no_action and not isinstance(expr.contents, Keyword): - children = list(map(operator.attrgetter('contents'), reduce(lambda x, y: x | y, (find_bytype(e, Keyword) for e in [expr])))) + children = list( + map(operator.attrgetter("contents"), reduce(lambda x, y: x | y, (find_bytype(e, Keyword) for e in [expr]))) + ) has_duplicates = len(children) > len(set(children)) node_type = "ListNode" if ("ZeroOrMore" in stmt or has_duplicates) else "Node" action = ".setParseAction(%s)" % ( @@ -243,5 +256,4 @@ if __name__ == "__main__": mdl = importlib.import_module(output) mdl.Generator(m).emit() sys.stdout.write(m.schema.name) -""" % ("\n ".join(statements)) - ) +""" % ("\n ".join(statements))) diff --git a/src/ifcopenshell-python/ifcopenshell/express/cat.py b/src/ifcopenshell-python/ifcopenshell/express/cat.py index d6e7b2c880..373afeb1d5 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/cat.py +++ b/src/ifcopenshell-python/ifcopenshell/express/cat.py @@ -1,15 +1,16 @@ import sys, fileinput -if sys.platform == "win32" and not hasattr(sys.stdout, 'buffer'): +if sys.platform == "win32" and not hasattr(sys.stdout, "buffer"): import os, msvcrt + msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) files = sys.argv[1:] -if files[0] == '-o': - b = open(files[1], 'wb') +if files[0] == "-o": + b = open(files[1], "wb") files = files[2:] else: - b = getattr(sys.stdout, 'buffer', sys.stdout) + b = getattr(sys.stdout, "buffer", sys.stdout) -for line in fileinput.input(files=files, mode='rb'): +for line in fileinput.input(files=files, mode="rb"): b.write(line) diff --git a/src/ifcopenshell-python/ifcopenshell/express/codegen.py b/src/ifcopenshell-python/ifcopenshell/express/codegen.py index fe997091b1..dfcf5083c8 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/codegen.py +++ b/src/ifcopenshell-python/ifcopenshell/express/codegen.py @@ -26,7 +26,7 @@ def indent(n, s): else: strs = s splitted = itertools.chain.from_iterable(map(functools.partial(str.split, sep="\n"), map(str, strs))) - return "\n".join(" "*n + l for l in splitted) + return "\n".join(" " * n + l for l in splitted) class Base: diff --git a/src/ifcopenshell-python/ifcopenshell/express/header.py b/src/ifcopenshell-python/ifcopenshell/express/header.py index 4cc8c8ea84..6059354925 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/header.py +++ b/src/ifcopenshell-python/ifcopenshell/express/header.py @@ -28,6 +28,7 @@ from collections import defaultdict USE_VIRTUAL_INHERITANCE = True + class Header(codegen.Base): def __init__(self, mapping): declarations = [] diff --git a/src/ifcopenshell-python/ifcopenshell/express/implementation.py b/src/ifcopenshell-python/ifcopenshell/express/implementation.py index 248bbd6e86..bc4b080ad6 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/implementation.py +++ b/src/ifcopenshell-python/ifcopenshell/express/implementation.py @@ -69,7 +69,7 @@ class Implementation(codegen.Base): templates.enum_from_string_stmt % dict(context, **locals()) for value in enum.values ), ) - + if USE_VIRTUAL_INHERITANCE: for name, enum in mapping.schema.selects.items(): write( @@ -118,10 +118,7 @@ class Implementation(codegen.Base): null_check = "" if arg["is_optional"]: - attr_check = ( - "if(get_attribute_value(%d).isNull()) { return %%s; }" - % (arg["index"] - 1,) - ) + attr_check = "if(get_attribute_value(%d).isNull()) { return %%s; }" % (arg["index"] - 1,) if "boost::optional" in arg["full_type"]: null_check = attr_check % "boost::none" else: @@ -157,7 +154,7 @@ class Implementation(codegen.Base): return templates.set_attr_stmt_enum elif arg["is_templated_list"] and not (select or simple or express): return templates.set_attr_stmt_array - elif arg["full_type"].endswith('*'): + elif arg["full_type"].endswith("*"): return templates.set_attr_instance else: return templates.set_attr_stmt @@ -178,7 +175,9 @@ class Implementation(codegen.Base): "non_optional_type": arg["non_optional_type"].replace("::Value", ""), "star_if_optional": "*" if "boost::optional" in arg["full_type"] else "", "check_optional_set_begin": "if (v) {" if "boost::optional" in arg["full_type"] else "", - "check_optional_set_else": "} else {" if "boost::optional" in arg["full_type"] else "if constexpr (false)", + "check_optional_set_else": ( + "} else {" if "boost::optional" in arg["full_type"] else "if constexpr (false)" + ), "check_optional_set_end": "}" if "boost::optional" in arg["full_type"] else "", }, ) @@ -193,11 +192,15 @@ class Implementation(codegen.Base): tmpl = ( templates.constructor_stmt_array if arg["is_templated_list"] - else templates.constructor_stmt_enum - if arg["is_enum"] - else templates.constructor_stmt_instance - if arg["full_type"].endswith('*') - else templates.constructor_stmt + else ( + templates.constructor_stmt_enum + if arg["is_enum"] + else ( + templates.constructor_stmt_instance + if arg["full_type"].endswith("*") + else templates.constructor_stmt + ) + ) ) impl = tmpl % { "name": deref_name, @@ -321,7 +324,7 @@ class Implementation(codegen.Base): else templates.simpletype_impl_is_without_supertype ) - constructor = templates.constructor_single_initlist# if superclass else templates.constructor + constructor = templates.constructor_single_initlist # if superclass else templates.constructor simpletype_impl_cast = ( templates.simpletype_impl_cast_templated @@ -374,8 +377,21 @@ class Implementation(codegen.Base): ("IfcEntityInstanceData&& e",), "", ), - ("", "", constructor, "", ("%s v" % type_str,), ("set_attribute_value(0, v%s);" % ("->generalize()" if mapping.is_templated_list(type) else ""))) if mapping.simple_type_parent(class_name) is None else \ - ("v", "", constructor, "", ("%s v" % type_str,), ""), + ( + ( + "", + "", + constructor, + "", + ("%s v" % type_str,), + ( + "set_attribute_value(0, v%s);" + % ("->generalize()" if mapping.is_templated_list(type) else "") + ), + ) + if mapping.simple_type_parent(class_name) is None + else ("v", "", constructor, "", ("%s v" % type_str,), "") + ), ("", "", templates.cast_function, type_str, (), simpletype_impl_cast), ), ), diff --git a/src/ifcopenshell-python/ifcopenshell/express/mapping.py b/src/ifcopenshell-python/ifcopenshell/express/mapping.py index 7ebac2478b..609f346c2c 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/mapping.py +++ b/src/ifcopenshell-python/ifcopenshell/express/mapping.py @@ -25,6 +25,7 @@ import schema from header import USE_VIRTUAL_INHERITANCE + class Mapping: express_to_cpp_typemapping = { diff --git a/src/ifcopenshell-python/ifcopenshell/express/nodes.py b/src/ifcopenshell-python/ifcopenshell/express/nodes.py index 34d3ef747b..2b8b872c83 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/nodes.py +++ b/src/ifcopenshell-python/ifcopenshell/express/nodes.py @@ -23,6 +23,7 @@ import operator import collections import bootstrap + class Node: def __init__(self, s, loc, tokens, rule=None): self.rule = rule or (type(self).__name__) @@ -58,15 +59,15 @@ class ListNode: rules_as_list = set() for t in self.tokens: - r = getattr(t, 'rule', None) + r = getattr(t, "rule", None) if r: rules_as_list.add(r) self.dict_tokens[r].append(t) - + for r, t in tokens.asDict().items(): if r not in rules_as_list: self.dict_tokens[r].append(t) - + self.flat = sum([getattr(t, "flat", [t]) for t in self.tokens], []) def __repr__(self): @@ -74,7 +75,7 @@ class ListNode: def __iter__(self): return iter(self.tokens) - + # Somehow indexing messes up the pyparsing results, so instead of x[0] use list(x)[0] # def __getitem__(self, i): # return self.tokens[i] @@ -110,7 +111,7 @@ def format_clause(exp): return "".join(whitespace(term) for term in exp.flat) -class TypeDeclaration(Node): +class TypeDeclaration(Node): name = property(lambda self: self.type_id[0]) utype = property(lambda self: self.underlying_type.any().any()) type = property(lambda self: self.utype[0] if isinstance(self.utype, list) else self.utype) @@ -245,7 +246,8 @@ class NamedType(Node): def do_try(fn): try: return fn() - except: pass + except: + pass def get_rule_id(x): @@ -255,8 +257,14 @@ def get_rule_id(x): if matches: return matches[0] + rule_dependencies = { - k: list(map(operator.attrgetter('contents'), bootstrap.reduce(lambda x, y: x | y, (bootstrap.find_bytype(e, bootstrap.Keyword) for e in [v])))) \ + k: list( + map( + operator.attrgetter("contents"), + bootstrap.reduce(lambda x, y: x | y, (bootstrap.find_bytype(e, bootstrap.Keyword) for e in [v])), + ) + ) for k, v in bootstrap.express } @@ -264,16 +272,17 @@ all_rules = [k for k, e in bootstrap.express] rule_definitions = {k: v for k, v in bootstrap.express} + def to_tree(x, key=None): - + def prune(di): # translate class names back to grammar rules if nested actions are encountered di = {get_rule_id(k) or k: v for k, v in di.items()} - + def replace_synonyms(x): for y in x: yield y - if False: # y in di: + if False: # y in di: # production element from grammar is found in parsed data, # return that. @@ -292,19 +301,21 @@ def to_tree(x, key=None): yield S # Do this recursively yield from replace_synonyms([S]) - + # is this a concatenation with zero or more synonyms? then also processs that # @todo catches: # - simple_expression = term { add_like_op term } . # but should probably also work on # - a = b { b } # in which case the second Concat would be eliminated - elif isinstance(rule, bootstrap.Concat) and \ - len(rule.contents) == 2 and \ - is_synonym(rule.contents[0]) and \ - isinstance(rule.contents[1].contents, bootstrap.Repeated) and \ - isinstance(rule.contents[1].contents.contents[0], bootstrap.Concat) and \ - str(rule.contents[1].contents.contents[0].contents[1]) == str(rule.contents[0]): + elif ( + isinstance(rule, bootstrap.Concat) + and len(rule.contents) == 2 + and is_synonym(rule.contents[0]) + and isinstance(rule.contents[1].contents, bootstrap.Repeated) + and isinstance(rule.contents[1].contents.contents[0], bootstrap.Concat) + and str(rule.contents[1].contents.contents[0].contents[1]) == str(rule.contents[0]) + ): S = is_synonym(rule.contents[0]) yield S # Do this recursively @@ -315,13 +326,13 @@ def to_tree(x, key=None): if key == "aggregation_types": # hack hack hack apparently the parser can't distinguish these subrules += list(replace_synonyms(rule_dependencies["general_aggregation_types"])) - + if rule_dependencies[key] and not subrules: # sometimes an intermediate production rule is missing # from the pyparsing output, e.g from parameter to simple_expression # directly. Recover from this. subrules = sum(map(rule_dependencies.__getitem__, rule_dependencies[key]), []) - + if not isinstance(rule_definitions[key], bootstrap.Union): # Filter out terminals when not a union. E.g no # reason to retain TYPE, END_TYPE, but operators @@ -331,7 +342,7 @@ def to_tree(x, key=None): vs = list(di.values()) return {k: v for k, v in di.items() if k in subrules or (k == key and len(vs) == 1 and vs[0] not in all_rules)} - + def simplify(di): if isinstance(di, list): if set(map(type, di)) == {str} and set(map(len, di)) == {1}: @@ -343,11 +354,11 @@ def to_tree(x, key=None): return {k: simplify(v) for k, v in di.items()} else: return di - + if isinstance(x, ListNode): d = to_tree(x.dict_tokens, key=get_rule_id(x) or key) - if key == 'if_stmt': + if key == "if_stmt": # The definition of if statement if (roughy): # 'if' expr 'then' stmt+ 'else' stmt+ # this causes stmt to be joined under the same @@ -355,39 +366,41 @@ def to_tree(x, key=None): # `else_stmt` that collects the second group # of stmts. - statements = x.dict_tokens['stmt'] - + statements = x.dict_tokens["stmt"] + else_index = None if_nesting = 0 - for i, tk in enumerate(x.flat): - if tk == 'if': if_nesting += 1 - if tk == 'end_if': if_nesting -= 1 - if tk == 'else' and if_nesting == 1: + for i, tk in enumerate(x.flat): + if tk == "if": + if_nesting += 1 + if tk == "end_if": + if_nesting -= 1 + if tk == "else" and if_nesting == 1: else_index = i if else_index: indices = [] for s in statements: for i in range(max(indices, default=0), len(x.flat)): - if x.flat[i:i+len(s.flat)] == s.flat: + if x.flat[i : i + len(s.flat)] == s.flat: indices.append(i) break assert len(indices) == len(statements) before_else = [i < else_index for i in indices] - else_stmt = [st for b, st in zip(before_else, d['stmt']) if not b] - d['stmt'] = [st for b, st in zip(before_else, d['stmt']) if b] + else_stmt = [st for b, st in zip(before_else, d["stmt"]) if not b] + d["stmt"] = [st for b, st in zip(before_else, d["stmt"]) if b] if else_stmt: - d['else_stmt'] = else_stmt - - if key == 'formal_parameter': + d["else_stmt"] = else_stmt + + if key == "formal_parameter": # Not so pretty hack to fix the overwriting of simple_id-like # ast nodes. The full solution would probably to register parse # actions. And directly reassign. - pid = d['parameter_id'][0][0] - d['parameter_id'][0] = x.flat[:x.flat.index(pid)+1:2] + pid = d["parameter_id"][0][0] + d["parameter_id"][0] = x.flat[: x.flat.index(pid) + 1 : 2] if key is None: return {get_rule_id(x): d} @@ -400,7 +413,10 @@ def to_tree(x, key=None): elif isinstance(x, dict): # d = {k: to_tree(v, key=k) for k, v in x.items()} # not fully understood, but when finding specific node Types and production rules, prioritize the former - d = {get_rule_id(k) or k: to_tree(v, key=k) for k, v in sorted(x.items(), key=lambda p: get_rule_id(p[0]) is not None)} + d = { + get_rule_id(k) or k: to_tree(v, key=k) + for k, v in sorted(x.items(), key=lambda p: get_rule_id(p[0]) is not None) + } return simplify(prune(d)) elif isinstance(x, list): return [to_tree(v, key=key) for v in x] @@ -459,7 +475,8 @@ class SuperTypeExpression(Node): else: constraint = self.supertype_rule[0] return [ - list(list(s)[0])[0].simple_id for s in list(list(list(constraint.subtype_constraint[0].supertype_expression[0])[0])[0].one_of[0])[2::2] + list(list(s)[0])[0].simple_id + for s in list(list(list(constraint.subtype_constraint[0].supertype_expression[0])[0])[0].one_of[0])[2::2] ] sub_types = property(get_sub_types) @@ -576,10 +593,11 @@ class ProcedureDeclaration(ListNode): @property def name(self): return self.flat[1] - - + + class FunctionDeclaration(ProcedureDeclaration): pass - + + class RuleDeclaration(ProcedureDeclaration): pass diff --git a/src/ifcopenshell-python/ifcopenshell/express/rule_compiler.py b/src/ifcopenshell-python/ifcopenshell/express/rule_compiler.py index 41fc6764c1..a77936d853 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/rule_compiler.py +++ b/src/ifcopenshell-python/ifcopenshell/express/rule_compiler.py @@ -48,11 +48,7 @@ def to_graph(tree): # bootstrap.py that result in an intermediate list index node in to_tree() # Start with the intermediate nodes and filter out root (needs to have predecessors) - intermediate = [ - n - for n in g.nodes - if g.nodes[n].get("label") is None and list(g.predecessors(n)) - ] + intermediate = [n for n in g.nodes if g.nodes[n].get("label") is None and list(g.predecessors(n))] for n in intermediate: pr = list(g.predecessors(n)) @@ -82,8 +78,7 @@ def to_graph(tree): for n in g.nodes: if ( len(list(g.successors(n))) == 0 - and g.nodes[n].get("label") - not in ifcopenshell.express.express_parser.all_rules + and g.nodes[n].get("label") not in ifcopenshell.express.express_parser.all_rules ): g.nodes[n]["is_terminal"] = True @@ -105,8 +100,7 @@ def write_dot(fn, g): def format(di): Q = '"' inner = ",".join( - f"{k}={'' if v.startswith('<') else Q}{v}{'' if v.startswith('<') else Q}" - for k, v in di.items() + f"{k}={'' if v.startswith('<') else Q}{v}{'' if v.startswith('<') else Q}" for k, v in di.items() ) if inner: inner = f"[{inner}]" @@ -179,9 +173,7 @@ class context: def has_inverse(self, a): for r in self.rules: - if a in map( - lambda n: self.graph.nodes[n].get("label"), self.graph.predecessors(r) - ): + if a in map(lambda n: self.graph.nodes[n].get("label"), self.graph.predecessors(r)): return True return False @@ -190,10 +182,7 @@ class context: yield context(self.graph, [r]) def descendants(self): - return [ - b.rules[0][len(self.rules[0]) + 1 :] - for b in self.branches(allow_multiple=True) - ] + return [b.rules[0][len(self.rules[0]) + 1 :] for b in self.branches(allow_multiple=True)] def __repr__(self): try: @@ -206,14 +195,11 @@ class context: assert len(self.rules) == 1 nodes = itertools.chain( self.rules, - itertools.chain.from_iterable( - dict(nx.bfs_successors(self.graph, self.rules[0])).values() - ), + itertools.chain.from_iterable(dict(nx.bfs_successors(self.graph, self.rules[0])).values()), ) terminals_or_values = list( filter( - lambda n: self.graph.nodes[n].get("is_terminal") - or self.graph.nodes[n].get("value"), + lambda n: self.graph.nodes[n].get("is_terminal") or self.graph.nodes[n].get("value"), nodes, ) ) @@ -375,9 +361,7 @@ def calc_{class_name}_{str(derived_attr.attribute_decl.redeclared_attribute.qual """ if context.entity_body.derive_clause: - statements.extend( - map(format_derived, context.entity_body.derive_clause.branches()) - ) + statements.extend(map(format_derived, context.entity_body.derive_clause.branches())) return "\n\n".join(statements) @@ -420,10 +404,12 @@ def process_expression(context): exclude=[context.rel_op_extended], ) else: - if len(context.simple_expression.branches()) == 2 and str(context.rel_op_extended) == 'in': + if len(context.simple_expression.branches()) == 2 and str(context.rel_op_extended) == "in": # IfcBlobTexture try: - is_literal_str_list = set(map(type, ast.literal_eval(str(context.simple_expression.branches()[1])))) == {str} + is_literal_str_list = set( + map(type, ast.literal_eval(str(context.simple_expression.branches()[1]))) + ) == {str} except: is_literal_str_list = False if is_literal_str_list: @@ -567,9 +553,7 @@ def process_function_decl(context): str.lower, map( str, - context.function_head.formal_parameter.parameter_id.branches( - allow_multiple=True - ), + context.function_head.formal_parameter.parameter_id.branches(allow_multiple=True), ), ) return f"def {context.function_head.function_id}({', '.join(arguments)}):\n{indent(4, context.algorithm_head.local_decl)}\n{indent(4, context.stmt.branches())}" @@ -582,9 +566,7 @@ def process_query(context): def process_local_variable(context): if context.expression: expr = str(context.expression) - if ( - context.parameter_type.generalized_types.general_aggregation_types.general_set_type - ): + if context.parameter_type.generalized_types.general_aggregation_types.general_set_type: expr = re.sub(r"(\[[^\]]*\])", "express_set(\\1)", expr) return "%s = %s" % (str(context.variable_id).lower(), expr) @@ -623,9 +605,7 @@ def process_assignment(context): if m := re.match(r"^([^\[]+)\[([^\[]+)\]$", lhs): # @todo ugly regex hack aggr, index = m.groups() - return ( - f"temp = list({aggr})\ntemp[{index}] = {context.expression}\n{aggr} = temp" - ) + return f"temp = list({aggr})\ntemp[{index}] = {context.expression}\n{aggr} = temp" else: return "%s = %s" % (lhs, context.expression) @@ -643,11 +623,7 @@ def process_case_action(context): def process_case_statement(context): branches = context.branches( - exclude=[ - getattr(context, v) - for v in context.descendants() - if not v.startswith("case_action") - ] + exclude=[getattr(context, v) for v in context.descendants() if not v.startswith("case_action")] ) if context.stmt and context.stmt.branches(): branches += [f"else:\n{indent(4, context.stmt)}"] @@ -658,9 +634,7 @@ def process_aggregate_initializer(context): if context.element.repetition: return "([%s] * %s)" % (context.element.expression, context.element.repetition) else: - return "[%s]" % ",".join( - map(str, context.element.branches() if context.element else ()) - ) + return "[%s]" % ",".join(map(str, context.element.branches() if context.element else ())) def process_index(context): @@ -676,9 +650,7 @@ def process_index(context): codegen_rule("function_call", process_function_call) codegen_rule( "actual_parameter_list", - lambda context: ",".join( - map(str, context.expression.branches() if context.expression else []) - ), + lambda context: ",".join(map(str, context.expression.branches() if context.expression else [])), ) codegen_rule("entity_decl", functools.partial(process_type_decl, "entity")) codegen_rule("rule_decl", process_rule_decl) @@ -696,9 +668,7 @@ codegen_rule("simple_factor", simple_concat) codegen_rule("primary", simple_concat) codegen_rule("qualifier", simple_concat) codegen_rule("return_stmt", lambda context: "return %s" % context) -codegen_rule( - "compound_stmt", lambda context: "\n".join(map(str, context.stmt.branches())) -) +codegen_rule("compound_stmt", lambda context: "\n".join(map(str, context.stmt.branches()))) codegen_rule("if_stmt", process_if_stmt) codegen_rule("repeat_stmt", process_repeat_stmt) # codegen_rule("index", lambda context: '**express_index(%s)' % context) @@ -707,19 +677,14 @@ codegen_rule("index_qualifier", process_index) codegen_rule("group_qualifier", lambda context: empty()) codegen_rule("attribute_qualifier", lambda context: ".%s" % context) codegen_rule("rel_op", process_rel_op) -codegen_rule( - "built_in_constant", lambda context: "None" if str(context) == "?" else str(context) -) +codegen_rule("built_in_constant", lambda context: "None" if str(context) == "?" else str(context)) codegen_rule("assignment_stmt", process_assignment) codegen_rule("local_variable", process_local_variable) codegen_rule("local_decl", lambda context: "\n".join(map(str, context.branches()))) codegen_rule("general_ref/parameter_ref", make_lowercase) codegen_rule( "qualifiable_factor/attribute_ref", - make_lowercase_if( - lambda context: str(context) - not in set(map(str, schema.all_declarations.keys())) - ), + make_lowercase_if(lambda context: str(context) not in set(map(str, schema.all_declarations.keys()))), ) codegen_rule("case_action", process_case_action) codegen_rule("case_stmt", process_case_statement) @@ -824,17 +789,17 @@ if __name__ == "__main__": import subprocess schema = ifcopenshell.express.express_parser.parse(sys.argv[1]).schema - + try: ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema.name) except: # @nb note the difference here between: - # + # # - ifcopenshell.express.express_parser.parse # - ifcopenshell.express.parse.parse - # + # # First generates a pyparsing AST - # + # # Second populates a latebound schema # that can be registered in C++. builder = ifcopenshell.express.parse(sys.argv[1]) @@ -1056,13 +1021,13 @@ INDETERMINATE = indeterminate_type() if isinstance(v, str): nl = "\n" es = "\\n" - n[ - "label" - ] = f'<
{n.get("label")}
{v.replace("<", "<").replace(">", ">").replace(nl, "
")}
>' + n["label"] = ( + f'<
{n.get("label")}
{v.replace("<", "<").replace(">", ">").replace(nl, "
")}
>' + ) elif isinstance(v, empty): - n[ - "label" - ] = f'<
{n.get("label")}
---
>' + n["label"] = ( + f'<
{n.get("label")}
---
>' + ) fn = f"{nm}.dot" write_dot(fn, G) diff --git a/src/ifcopenshell-python/ifcopenshell/express/rule_executor.py b/src/ifcopenshell-python/ifcopenshell/express/rule_executor.py index 9201e480be..68240aeddd 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/rule_executor.py +++ b/src/ifcopenshell-python/ifcopenshell/express/rule_executor.py @@ -9,24 +9,28 @@ from codegen import indent def reverse_compile(s): - return re.sub(r'\bself\b', 'SELF', re.sub( - r"\s*\-\s*EXPRESS_ONE_BASED_INDEXING", - "", + return re.sub( + r"\bself\b", + "SELF", re.sub( - ", )?+.(, INDETERMINATE)\\"[::-1], - "]\\1[", + r"\s*\-\s*EXPRESS_ONE_BASED_INDEXING", + "", re.sub( - r", '(\w+)', INDETERMINATE\)", - ".\\1", - s.strip() - .replace("len(", "SIZEOF(") - .replace("assert ", "") - .replace(" is not False", "") - .replace("express_getattr(", "") - .replace("express_getitem(", ""), + ", )?+.(, INDETERMINATE)\\"[::-1], + "]\\1[", + re.sub( + r", '(\w+)', INDETERMINATE\)", + ".\\1", + s.strip() + .replace("len(", "SIZEOF(") + .replace("assert ", "") + .replace(" is not False", "") + .replace("express_getattr(", "") + .replace("express_getitem(", ""), + )[::-1], )[::-1], - )[::-1], - )) + ), + ) @dataclass @@ -68,9 +72,13 @@ def run(f: ifcopenshell.file, logger: Logger) -> None: if hasattr(logger, "set_instance"): # when using the json logger, we notify it of the relevant instance - pre_annotate_instance = lambda instance: logger.set_state('instance', instance) if hasattr(logger, 'set_state') else None + pre_annotate_instance = lambda instance: ( + logger.set_state("instance", instance) if hasattr(logger, "set_state") else None + ) post_annotate_instance = lambda instance: instance - pre_annotate_attribute = lambda attribute: logger.set_state('attribute', attribute) if hasattr(logger, 'set_state') else None + pre_annotate_attribute = lambda attribute: ( + logger.set_state("attribute", attribute) if hasattr(logger, "set_state") else None + ) post_annotate_attribute = lambda attribute: None else: # when using the normal text logger the instance is appended to the method @@ -90,13 +98,13 @@ def run(f: ifcopenshell.file, logger: Logger) -> None: import time import subprocess - current_dir_files = {fn.lower(): fn for fn in os.listdir('.')} - schema_name = str(f.schema_identifier).split(' ')[-1].lower() - schema_path = current_dir_files.get(schema_name + '.exp') - fn = schema_path[:-4] + '.py' + current_dir_files = {fn.lower(): fn for fn in os.listdir(".")} + schema_name = str(f.schema_identifier).split(" ")[-1].lower() + schema_path = current_dir_files.get(schema_name + ".exp") + fn = schema_path[:-4] + ".py" if not os.path.exists(fn): subprocess.run([sys.executable, "-m", "ifcopenshell.express.rule_compiler", schema_path, fn], check=True) - time.sleep(1.) + time.sleep(1.0) source = open(fn, "r").read() a = ast.parse(source) @@ -108,12 +116,14 @@ def run(f: ifcopenshell.file, logger: Logger) -> None: rules = list(filter(lambda x: hasattr(x, "SCOPE"), scope.values())) - if hasattr(logger, 'set_state'): - logger.set_state('type', 'global_rule') + if hasattr(logger, "set_state"): + logger.set_state("type", "global_rule") for R in [r for r in rules if r.SCOPE == "file"]: try: R()(f) + except RecursionError as e: + logger.info(str(e)) except Exception as e: ln = e.__traceback__.tb_next.tb_lineno pre_annotate_attribute(R.__name__) @@ -127,17 +137,15 @@ def run(f: ifcopenshell.file, logger: Logger) -> None: ) ) - if hasattr(logger, 'set_state'): - logger.set_state('type', 'simpletype_rule') + if hasattr(logger, "set_state"): + logger.set_state("type", "simpletype_rule") types = {} subtypes = collections.defaultdict(list) for d in S.declarations(): if isinstance(d, ifcopenshell.ifcopenshell_wrapper.type_declaration): types[d.name()] = d - if isinstance( - d.declared_type(), ifcopenshell.ifcopenshell_wrapper.named_type - ): + if isinstance(d.declared_type(), ifcopenshell.ifcopenshell_wrapper.named_type): subtypes[d.declared_type().declared_type().name()].append(d.name()) D = collections.defaultdict(list) @@ -170,6 +178,8 @@ def run(f: ifcopenshell.file, logger: Logger) -> None: for R in D[type_name(type)]: try: R()(fix_type(value)) + except RecursionError as e: + logger.info(str(e)) except Exception as e: ln = e.__traceback__.tb_next.tb_lineno pre_annotate_instance(instance) @@ -220,6 +230,8 @@ def run(f: ifcopenshell.file, logger: Logger) -> None: for inst in f: try: values = list(inst) + except RecursionError as e: + logger.info(str(e)) except Exception as e: if hasattr(logger, "set_state"): logger.error(str(e)) @@ -228,22 +240,22 @@ def run(f: ifcopenshell.file, logger: Logger) -> None: continue entity = S.declaration_by_name(inst.is_a()) attrs = entity.all_attributes() - for i, (attr, val, is_derived) in enumerate( - zip(attrs, values, entity.derived()) - ): + for i, (attr, val, is_derived) in enumerate(zip(attrs, values, entity.derived())): if is_derived: # @todo pass else: check(val, attr.type_of_attribute(), instance=inst) - if hasattr(logger, 'set_state'): - logger.set_state('type', 'entity_rule') + if hasattr(logger, "set_state"): + logger.set_state("type", "entity_rule") for R in [r for r in rules if r.SCOPE == "entity"]: for inst in f.by_type(R.TYPE_NAME): try: R()(inst) + except RecursionError as e: + logger.info(str(e)) except Exception as e: ln = e.__traceback__.tb_next.tb_lineno pre_annotate_instance(inst) diff --git a/src/ifcopenshell-python/ifcopenshell/express/schema.py b/src/ifcopenshell-python/ifcopenshell/express/schema.py index ebb050d1ae..315db817b6 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/schema.py +++ b/src/ifcopenshell-python/ifcopenshell/express/schema.py @@ -27,6 +27,7 @@ if tuple(map(int, platform.python_version_tuple())) < (2, 7): collections.OrderedDict = ordereddict.OrderedDict + # According to ISO 10303-11 7.1.2: Letters: "... The case of # letters is significant only within explicit string literals." class OrderedCaseInsensitiveDict_KeyObject(str): @@ -92,23 +93,21 @@ class Schema: sort = lambda d: OrderedCaseInsensitiveDict(sorted(d)) - declarations = [ - d.any()[0] - for d in schema_declarations - if d.rule == "declaration" - ] + [ - d - for d in schema_declarations - if d.rule == "RuleDeclaration" + declarations = [d.any()[0] for d in schema_declarations if d.rule == "declaration"] + [ + d for d in schema_declarations if d.rule == "RuleDeclaration" ] - + self.types = sort([(t.name, t) for t in declarations if isinstance(t, nodes.TypeDeclaration)]) self.entities = sort([(t.name, t) for t in declarations if isinstance(t, nodes.EntityDeclaration)]) self.rules = sort([(t.name, t) for t in declarations if isinstance(t, nodes.RuleDeclaration)]) self.functions = sort([(t.name, t) for t in declarations if isinstance(t, nodes.FunctionDeclaration)]) - self.keys = list(self.types.keys()) + list(self.entities.keys()) + list(self.rules.keys()) + list(self.functions.keys()) - self.all_declarations = {k: v for d in (self.types, self.entities, self.rules, self.functions) for k, v in d.items()} + self.keys = ( + list(self.types.keys()) + list(self.entities.keys()) + list(self.rules.keys()) + list(self.functions.keys()) + ) + self.all_declarations = { + k: v for d in (self.types, self.entities, self.rules, self.functions) for k, v in d.items() + } of_type = lambda *types: sort( [(a, b.type) for a, b in self.types.items() if any(isinstance(b.type, ty) for ty in types)] diff --git a/src/ifcopenshell-python/ifcopenshell/express/schema_class.py b/src/ifcopenshell-python/ifcopenshell/express/schema_class.py index 50a4e50c5d..d7595ad6cb 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/schema_class.py +++ b/src/ifcopenshell-python/ifcopenshell/express/schema_class.py @@ -123,6 +123,7 @@ class string_pool: def __init__(self, fn): self.di = {} self.fn = fn + def append(self, v): def _(): if i := self.di.get(v): @@ -131,7 +132,9 @@ class string_pool: i = len(self.di) self.di[v] = i return i + return self.fn(_()) + def __iter__(self): return iter(self.di.keys()) @@ -146,9 +149,9 @@ class EarlyBoundCodeWriter: "", '#include "../ifcparse/IfcSchema.h"', '#include "../ifcparse/%(schema_name_title)s.h"' % self.__dict__, - '#include ', + "#include ", "", - 'using namespace std::string_literals;', + "using namespace std::string_literals;", "using namespace IfcParse;", "", ] @@ -180,18 +183,18 @@ class EarlyBoundCodeWriter: self.statements.append("{factory_placeholder}") -# self.statements.append( -# """ -# #if defined(__clang__) -# __attribute__((optnone)) -# #elif defined(__GNUC__) || defined(__GNUG__) -# #pragma GCC push_options -# #pragma GCC optimize ("O0") -# #elif defined(_MSC_VER) -# #pragma optimize("", off) -# #endif -# """ -# ) + # self.statements.append( + # """ + # #if defined(__clang__) + # __attribute__((optnone)) + # #elif defined(__GNUC__) || defined(__GNUG__) + # #pragma GCC push_options + # #pragma GCC optimize ("O0") + # #elif defined(_MSC_VER) + # #pragma optimize("", off) + # #endif + # """ + # ) self.statements.append("IfcParse::schema_definition* %s_populate_schema() {" % self.schema_name.upper()) self.statements.append("{string_pool_placeholder}") @@ -200,7 +203,7 @@ class EarlyBoundCodeWriter: index_in_schema = self.names.index(name) ref = self.strings.append(name) self.statements.append( - ' %(schema_name)s_types[%(index_in_schema)d] = new type_declaration(%(ref)s, %(index_in_schema)d, %(declared_type)s);' + " %(schema_name)s_types[%(index_in_schema)d] = new type_declaration(%(ref)s, %(index_in_schema)d, %(declared_type)s);" % locals() ) @@ -210,7 +213,7 @@ class EarlyBoundCodeWriter: ref = self.strings.append(name) items = ",".join(self.strings.append(v) for v in enum.values) self.statements.append( - ' %(schema_name)s_types[%(index_in_schema)d] = new enumeration_type(%(ref)s, %(index_in_schema)d, {%(items)s});' + " %(schema_name)s_types[%(index_in_schema)d] = new enumeration_type(%(ref)s, %(index_in_schema)d, {%(items)s});" % locals() ) @@ -218,10 +221,14 @@ class EarlyBoundCodeWriter: schema_name = self.schema_name.upper() index_in_schema = self.names.index(name) ref = self.strings.append(name) - supertype = "0" if len(type.supertypes) == 0 else "%s_types[%d]" % (self.schema_name, self.names.index(type.supertypes[0])) + supertype = ( + "0" + if len(type.supertypes) == 0 + else "%s_types[%d]" % (self.schema_name, self.names.index(type.supertypes[0])) + ) is_abstract = "true" if type.abstract else "false" self.statements.append( - ' %(schema_name)s_types[%(index_in_schema)d] = new entity(%(ref)s, %(is_abstract)s, %(index_in_schema)d, (entity*) %(supertype)s);' + " %(schema_name)s_types[%(index_in_schema)d] = new entity(%(ref)s, %(is_abstract)s, %(index_in_schema)d, (entity*) %(supertype)s);" % locals() ) @@ -233,73 +240,89 @@ class EarlyBoundCodeWriter: map(lambda v: "%s_types[%d]" % (self.schema_name, self.names.index(v)), sorted(map(str, type.values))) ) self.statements.append( - ' %(schema_name)s_types[%(index_in_schema)d] = new select_type(%(ref)s, %(index_in_schema)d, {%(items)s});' + " %(schema_name)s_types[%(index_in_schema)d] = new select_type(%(ref)s, %(index_in_schema)d, {%(items)s});" % locals() ) def entity_attributes(self, name, attribute_definitions, is_derived): schema_name = self.schema_name.upper() index_in_schema = self.names.index(name) + def _(): index_in_schema = self.names.index(name) schema_name = self.schema_name for attr_name, decl_type, optional in attribute_definitions: attr_name_ref = self.strings.append(attr_name) optional_cpp = str(optional).lower() - yield 'new attribute(%(attr_name_ref)s, %(decl_type)s, %(optional_cpp)s)' % locals() + yield "new attribute(%(attr_name_ref)s, %(decl_type)s, %(optional_cpp)s)" % locals() + attributes = ",".join(_()) derived = ",".join(map(lambda b: str(b).lower(), is_derived)) - self.statements.append(" ((entity*)%(schema_name)s_types[%(index_in_schema)d])->set_attributes({%(attributes)s}, {%(derived)s});" % locals()) + self.statements.append( + " ((entity*)%(schema_name)s_types[%(index_in_schema)d])->set_attributes({%(attributes)s}, {%(derived)s});" + % locals() + ) def inverse_attributes(self, name, inv_attrs): schema_name = self.schema_name.upper() index_in_schema = self.names.index(name) + def _(): schema_name = self.schema_name index_in_schema = self.names.index(name) for attr_name, aggr_type, bound1, bound2, entity_ref, attribute_entity, attribute_entity_index in inv_attrs: attr_name_ref = self.strings.append(attr_name) opposite_index_in_schema = self.names.index(entity_ref) - opposite1 = '%(schema_name)s_types[%(opposite_index_in_schema)d]' % locals() + opposite1 = "%(schema_name)s_types[%(opposite_index_in_schema)d]" % locals() opposite_index_in_schema = self.names.index(attribute_entity) - opposite2 = '%(schema_name)s_types[%(opposite_index_in_schema)d]' % locals() - yield 'new inverse_attribute(%(attr_name_ref)s, inverse_attribute::%(aggr_type)s_type, %(bound1)d, %(bound2)d, ((entity*) %(opposite1)s), ((entity*) %(opposite2)s)->attributes()[%(attribute_entity_index)d])' % locals() + opposite2 = "%(schema_name)s_types[%(opposite_index_in_schema)d]" % locals() + yield "new inverse_attribute(%(attr_name_ref)s, inverse_attribute::%(aggr_type)s_type, %(bound1)d, %(bound2)d, ((entity*) %(opposite1)s), ((entity*) %(opposite2)s)->attributes()[%(attribute_entity_index)d])" % locals() + attributes = ",".join(_()) - self.statements.append(" ((entity*) %(schema_name)s_types[%(index_in_schema)d])->set_inverse_attributes({%(attributes)s});" % locals()) + self.statements.append( + " ((entity*) %(schema_name)s_types[%(index_in_schema)d])->set_inverse_attributes({%(attributes)s});" + % locals() + ) def entity_subtypes(self, name, tys): schema_name = self.schema_name.upper() index_in_schema = self.names.index(name) - subtypes = ",".join(map(lambda t: ("((entity*) %%(schema_name)s_types[%d])" % self.names.index(t)), tys)) % locals() - self.statements.append(" ((entity*) %(schema_name)s_types[%(index_in_schema)d])->set_subtypes({%(subtypes)s});" % locals()) + subtypes = ( + ",".join(map(lambda t: ("((entity*) %%(schema_name)s_types[%d])" % self.names.index(t)), tys)) % locals() + ) + self.statements.append( + " ((entity*) %(schema_name)s_types[%(index_in_schema)d])->set_subtypes({%(subtypes)s});" % locals() + ) def finalize(self, can_be_instantiated_set): schema_name = self.schema_name.upper() schema_name_title = self.schema_name.capitalize() + def _(): schema_name = self.schema_name.upper() schema_name_title = self.schema_name.capitalize() for type_name in self.names: index_in_schema = self.names.index(type_name) yield "%(schema_name)s_types[%(index_in_schema)d]" % locals() + declarations = ",".join(_()) schema_name_ref = self.strings.append(schema_name) self.statements.append( - ' return new schema_definition(%(schema_name_ref)s, {%(declarations)s}, new %(schema_name)s_instance_factory());' + " return new schema_definition(%(schema_name_ref)s, {%(declarations)s}, new %(schema_name)s_instance_factory());" % locals() ) - self.statements.append("}"); + self.statements.append("}") -# self.statements.append( -# """ -# #if defined(__clang__) -# #elif defined(__GNUC__) || defined(__GNUG__) -# #pragma GCC pop_options -# #elif defined(_MSC_VER) -# #pragma optimize("", on) -# #endif -# """ -# ) + # self.statements.append( + # """ + # #if defined(__clang__) + # #elif defined(__GNUC__) || defined(__GNUG__) + # #pragma GCC pop_options + # #elif defined(_MSC_VER) + # #pragma optimize("", on) + # #endif + # """ + # ) self.statements.extend( ( @@ -340,24 +363,18 @@ class EarlyBoundCodeWriter: ) ) - self.statements[self.statements.index("{factory_placeholder}")] = ( - """ + self.statements[self.statements.index("{factory_placeholder}")] = """ class %(schema_name)s_instance_factory : public IfcParse::instance_factory { virtual IfcUtil::IfcBaseClass* operator()(const IfcParse::declaration* decl, IfcEntityInstanceData&& data) const { %(instance_mapping)s } }; -""" - % locals() - ) +""" % locals() "" - self.statements[self.statements.index("{string_pool_placeholder}")] = ( - """ + self.statements[self.statements.index("{string_pool_placeholder}")] = """ const std::string strings[] = {%s}; -""" - % ",".join(map(lambda s: '"%s"s' % s, self.strings)) - ) +""" % ",".join(map(lambda s: '"%s"s' % s, self.strings)) def __str__(self): return "\n".join(self.statements) @@ -379,16 +396,19 @@ class SchemaClass(codegen.Base): def wrapper(*args, **kwargs): schema_name_upper = mapping.schema.name.upper() declared_type = fn(*args, **kwargs) - if 'simple_type' in declared_type: + if "simple_type" in declared_type: pass else: - match = re.search(r'\((\w+?_[\w+]+?_\w+?)\)', declared_type) + match = re.search(r"\((\w+?_[\w+]+?_\w+?)\)", declared_type) if match: old_decl = match.group(1) - name = old_decl.lower().replace(schema_name.lower() + '_', '').replace('_type', '') + name = old_decl.lower().replace(schema_name.lower() + "_", "").replace("_type", "") idx = [n.lower() for n in x.names].index(name) - declared_type = declared_type.replace(old_decl, '%(schema_name_upper)s_types[%(idx)d]' % locals()) + declared_type = declared_type.replace( + old_decl, "%(schema_name_upper)s_types[%(idx)d]" % locals() + ) return declared_type + return wrapper if code == EarlyBoundCodeWriter else fn @transform_to_indexed diff --git a/src/ifcopenshell-python/ifcopenshell/express/templates.py b/src/ifcopenshell-python/ifcopenshell/express/templates.py index e0398fd58d..564fe881a5 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/templates.py +++ b/src/ifcopenshell-python/ifcopenshell/express/templates.py @@ -217,7 +217,7 @@ const IfcParse::entity& %(schema_name)s::%(name)s::Class() { return *((IfcParse: %(schema_name)s::%(name)s::%(name)s(%(constructor_arguments)s) : %(superclass_num_attrs)s { %(constructor_implementation)s; populate_derived(); } """ -# data_ = e; +# data_ = e; # data_ = new IfcEntityInstanceData(%(schema_name_upper)s_types[%(index_in_schema)d]); optional_attribute_description = "/// Whether the optional attribute %s is defined for this %s" @@ -255,32 +255,16 @@ get_attr_stmt_nested_array = "%(null_check)s aggregate_of_aggregate_of_instance: get_inverse = "if (!file_) { return nullptr; } return file_->getInverse(id_, %(schema_name_upper)s_types[%(type_index)d], %(index)d)->as<%(type)s>();" -set_attr_stmt = ( - "%(check_optional_set_begin)sset_attribute_value(%(index)d, %(star_if_optional)sv);%(check_optional_set_else)sunset_attribute_value(%(index)d);%(check_optional_set_end)s" -) -set_attr_instance = ( - "%(check_optional_set_begin)sset_attribute_value(%(index)d, v->as());%(check_optional_set_else)sunset_attribute_value(%(index)d);%(check_optional_set_end)s" -) -set_attr_stmt_enum = "%(check_optional_set_begin)sset_attribute_value(%(index)d, EnumerationReference(&%(non_optional_type)s::Class(), (size_t) %(star_if_optional)sv));%(check_optional_set_else)sunset_attribute_value(%(index)d);%(check_optional_set_end)s" -set_attr_stmt_array = ( - "%(check_optional_set_begin)sset_attribute_value(%(index)d, (%(star_if_optional)sv)->generalize());%(check_optional_set_else)sunset_attribute_value(%(index)d);%(check_optional_set_end)s" -) +set_attr_stmt = "%(check_optional_set_begin)sset_attribute_value(%(index)d, %(star_if_optional)sv);%(check_optional_set_else)sunset_attribute_value(%(index)d);%(check_optional_set_end)s" +set_attr_instance = "%(check_optional_set_begin)sset_attribute_value(%(index)d, v->as());%(check_optional_set_else)sunset_attribute_value(%(index)d);%(check_optional_set_end)s" +set_attr_stmt_enum = "%(check_optional_set_begin)sset_attribute_value(%(index)d, EnumerationReference(&%(non_optional_type)s::Class(), (size_t) %(star_if_optional)sv));%(check_optional_set_else)sunset_attribute_value(%(index)d);%(check_optional_set_end)s" +set_attr_stmt_array = "%(check_optional_set_begin)sset_attribute_value(%(index)d, (%(star_if_optional)sv)->generalize());%(check_optional_set_else)sunset_attribute_value(%(index)d);%(check_optional_set_end)s" -constructor_stmt = ( - "set_attribute_value(%(index)d, (%(name)s));" -) -constructor_stmt_enum = ( - "set_attribute_value(%(index)d, (EnumerationReference(&%(type)s::Class(),(size_t)%(name)s)));" -) -constructor_stmt_array = ( - "set_attribute_value(%(index)d, (%(name)s)->generalize());" -) -constructor_stmt_derived = ( - "" -) -constructor_stmt_instance = ( - "set_attribute_value(%(index)d, %(name)s ? %(name)s->as() : (IfcUtil::IfcBaseClass*) nullptr);" -) +constructor_stmt = "set_attribute_value(%(index)d, (%(name)s));" +constructor_stmt_enum = "set_attribute_value(%(index)d, (EnumerationReference(&%(type)s::Class(),(size_t)%(name)s)));" +constructor_stmt_array = "set_attribute_value(%(index)d, (%(name)s)->generalize());" +constructor_stmt_derived = "" +constructor_stmt_instance = "set_attribute_value(%(index)d, %(name)s ? %(name)s->as() : (IfcUtil::IfcBaseClass*) nullptr);" constructor_stmt_optional = " if (%(name)s) {%(stmt)s }" diff --git a/src/ifcopenshell-python/ifcopenshell/geom/app.py b/src/ifcopenshell-python/ifcopenshell/geom/app.py index fa07f3f2b8..d6bab207f1 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/app.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/app.py @@ -145,8 +145,7 @@ class configuration: config.set( "snippets", "print all wall ids", - self.config_encode( - """ + self.config_encode(""" ########################################################################### # A simple script that iterates over all walls in the current model # # and prints their Globally unique IDs (GUIDS) to the console window # @@ -154,15 +153,13 @@ class configuration: for wall in model.by_type("IfcWall"): print ("wall with global id: "+str(wall.GlobalId)) -""".lstrip() - ), +""".lstrip()), ) config.set( "snippets", "print properties of current selection", - self.config_encode( - """ + self.config_encode(""" ########################################################################### # A simple script that iterates over all IfcPropertySets of the currently # # selected object and prints them to the console # @@ -180,8 +177,7 @@ if selection: for prop in relDefinesByProperties.RelatingPropertyDefinition.HasProperties: print ("{:<20} :{}".format(prop.Name,prop.NominalValue.wrappedValue)) print ("\\n") -""".lstrip() - ), +""".lstrip()), ) with open(conf_file, "w") as configfile: config.write(configfile) diff --git a/src/ifcopenshell-python/ifcopenshell/util/cost.py b/src/ifcopenshell-python/ifcopenshell/util/cost.py index 19172c4710..875594f1a5 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/cost.py +++ b/src/ifcopenshell-python/ifcopenshell/util/cost.py @@ -352,8 +352,7 @@ def get_cost_rate( class CostValueUnserialiser: def parse(self, formula: str): - l = lark.Lark( - """start: formula + l = lark.Lark("""start: formula formula: operand (operator operand)* operand: value | category "(" formula ")" value: NUMBER? @@ -390,8 +389,7 @@ class CostValueUnserialiser: NEWLINE: (CR? LF)+ %ignore WS // Disregard spaces in text - """ - ) + """) start = l.parse(formula) return self.get_formula(start.children[0]) diff --git a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py index fba8dfffaf..47c8886691 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py +++ b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py @@ -18,7 +18,7 @@ import math from decimal import ROUND_HALF_UP, Decimal -from typing import NamedTuple, Optional, Union +from typing import Any, NamedTuple, Optional, Union import numpy as np @@ -268,6 +268,15 @@ def get_helmert_transformation_parameters(ifc_file: ifcopenshell.file) -> Option return HelmertTransformation(e, n, h, xaa, xao, scale, factor_x, factor_y, factor_z) +def get_crs(ifc_file: ifcopenshell.file) -> dict[str, Any]: + """Get CRS information from an IFC file.""" + if ifc_file.schema == "IFC2X3": + return ifcopenshell.util.element.get_pset(ifc_file.by_type("IfcProject")[0], "ePSet_ProjectedCRS") + for context in ifc_file.by_type("IfcGeometricRepresentationContext", include_subtypes=False): + if operation := context.HasCoordinateOperation: + return operation[0].TargetCRS.get_info() + + def auto_z2e(ifc_file: ifcopenshell.file, z: float, should_return_in_map_units: bool = True) -> float: """Convert a Z coordinate to an elevation using model georeferencing data diff --git a/src/ifcopenshell-python/ifcopenshell/util/scripts/validate_stub.py b/src/ifcopenshell-python/ifcopenshell/util/scripts/validate_stub.py index 2ddf444704..5ba2466f29 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/scripts/validate_stub.py +++ b/src/ifcopenshell-python/ifcopenshell/util/scripts/validate_stub.py @@ -25,7 +25,6 @@ Things we do check: - class hierarchy """ - import ast import difflib from pathlib import Path diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index d3bef9758f..c6ee820ab8 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -39,8 +39,7 @@ import ifcopenshell.util.shape import ifcopenshell.util.system import ifcopenshell.util.unit -filter_elements_grammar = lark.Lark( - """start: filter_group +filter_elements_grammar = lark.Lark("""start: filter_group filter_group: facet_list ("+" facet_list)* facet_list: facet ("," facet)* @@ -111,11 +110,9 @@ filter_elements_grammar = lark.Lark( NEWLINE: (CR? LF)+ %ignore WS // Disregard spaces in text -""" -) +""") -get_element_grammar = lark.Lark( - """start: keys +get_element_grammar = lark.Lark("""start: keys keys: key ("." key)* key: quoted_string | regex_string | unquoted_string @@ -130,11 +127,9 @@ get_element_grammar = lark.Lark( WS: /[ \\t\\f\\r\\n]/+ %ignore WS // Disregard spaces in text - """ -) + """) -format_grammar = lark.Lark( - """start: expression +format_grammar = lark.Lark("""start: expression ?expression: add_sub ?add_sub: mul_div @@ -193,8 +188,7 @@ format_grammar = lark.Lark( NEWLINE: (CR? LF)+ %ignore WS // Disregard spaces in text -""" -) +""") class FormatTransformer(lark.Transformer): diff --git a/src/ifcopenshell-python/ifcopenshell/validate.py b/src/ifcopenshell-python/ifcopenshell/validate.py index 13f148d520..e1270afb57 100644 --- a/src/ifcopenshell-python/ifcopenshell/validate.py +++ b/src/ifcopenshell-python/ifcopenshell/validate.py @@ -793,6 +793,12 @@ if __name__ == "__main__": parser.add_argument("files", nargs="+", help="The IFC file to validate.") parser.add_argument("--rules", action="store_true", help="Run express rules.") parser.add_argument("--json", action="store_true", help="Output in JSON format.") + parser.add_argument( + "--recursion-limit", + type=int, + default=-1, + help="Override sys.getrecursionlimit to process express rules on deeply nested structures (e.g 10000)", + ) parser.add_argument( "--fields", action="store_true", @@ -804,6 +810,9 @@ if __name__ == "__main__": filenames: list[str] = args.files some_file_is_invalid = False + if args.recursion_limit > 0: + sys.setrecursionlimit(args.recursion_limit) + for fn in filenames: handler = None if args.json: diff --git a/src/ifcopenshell-python/scripts/dev_environment.py b/src/ifcopenshell-python/scripts/dev_environment.py index db65020048..8058a58b92 100644 --- a/src/ifcopenshell-python/scripts/dev_environment.py +++ b/src/ifcopenshell-python/scripts/dev_environment.py @@ -11,6 +11,7 @@ import site from pathlib import Path SITE = Path(site.getusersitepackages()) +SITE.mkdir(parents=True, exist_ok=True) REPO_PATH = Path(__file__).parent.parent.parent.parent REPO_PATH_SRC = REPO_PATH / "src" assert REPO_PATH_SRC.exists(), f"'{REPO_PATH_SRC}' doesn't exist." @@ -36,8 +37,8 @@ for package, repo_package_path in packages.items(): if package_path.exists(): # I guess it's a directory. shutil.rmtree(package_path) - package_path.symlink_to(repo_package_path, True) print(f"Symlinking {package_path} -> {repo_package_path}") + package_path.symlink_to(repo_package_path, True) PACKAGE_PATH = SITE / "ifcopenshell" diff --git a/src/ifcopenshell-python/test/typing_tests.py b/src/ifcopenshell-python/test/typing_tests.py index 5b2d789613..ccf007fa62 100644 --- a/src/ifcopenshell-python/test/typing_tests.py +++ b/src/ifcopenshell-python/test/typing_tests.py @@ -21,7 +21,6 @@ This file should produce no warnings from type checker (currently pyright). Those tests are not automatically checked and just there to make sure overloads are making sense. """ - from typing import Union from typing_extensions import assert_type diff --git a/src/ifcopenshell-python/test/util/test_pset.py b/src/ifcopenshell-python/test/util/test_pset.py index 3b44951ec4..bec1a10ba0 100644 --- a/src/ifcopenshell-python/test/util/test_pset.py +++ b/src/ifcopenshell-python/test/util/test_pset.py @@ -17,6 +17,7 @@ # along with IfcOpenShell. If not, see . """Run this test from src/ifcopenshell-python folder: pytest --durations=0 ifcopenshell/util/test_pset.py""" + from ifcopenshell.util import pset from ifcopenshell.util.pset import ApplicableEntity diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py index 3c94876a66..b8302e0f4a 100644 --- a/src/ifcopenshell-python/test/util/test_selector.py +++ b/src/ifcopenshell-python/test/util/test_selector.py @@ -54,7 +54,7 @@ class TestFormat(test.bootstrap.IFC4): def test_number_formatting(self): assert subject.format("round(123, 5)") == "125" assert subject.format('round("123", 5)') == "125" - assert subject.format('round(-123, 5)') == "-125" + assert subject.format("round(-123, 5)") == "-125" assert subject.format("int(123.123)") == "123" assert subject.format("int(123)") == "123" assert subject.format("number(123)") == "123" @@ -79,14 +79,14 @@ class TestFormat(test.bootstrap.IFC4): assert subject.format('imperial_length(3.0, 4, "foot", "foot", False)') == "3' - 0\"" def test_variable_formatting(self): - assert subject.format('{{undefined}}') is None - assert subject.format('upper({{undefined}})') == "NONE" - assert subject.format('int({{undefined}})') == "0" + assert subject.format("{{undefined}}") is None + assert subject.format("upper({{undefined}})") == "NONE" + assert subject.format("int({{undefined}})") == "0" element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") - assert subject.format('{{undefined}}', element) is None - assert subject.format('{{class}}', element) == "IfcWall" - assert subject.format('{{ class }}', element) == "IfcWall" - assert subject.format('upper({{ class }})', element) == "IFCWALL" + assert subject.format("{{undefined}}", element) is None + assert subject.format("{{class}}", element) == "IfcWall" + assert subject.format("{{ class }}", element) == "IfcWall" + assert subject.format("upper({{ class }})", element) == "IFCWALL" def test_list_formatting(self): element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") @@ -98,17 +98,17 @@ class TestFormat(test.bootstrap.IFC4): layer = ifcopenshell.api.material.add_layer(self.file, layer_set=material_set, material=material2) layer = ifcopenshell.api.material.add_layer(self.file, layer_set=material_set, material=material3) ifcopenshell.api.material.assign_material(self.file, products=[element], material=material_set) - assert subject.format('{{materials.Name}}', element) == "CON01, CON03, CON02" - assert subject.format('sort({{materials.Name}})', element) == "CON01, CON02, CON03" - assert subject.format('reverse({{materials.Name}})', element) == "CON02, CON03, CON01" + assert subject.format("{{materials.Name}}", element) == "CON01, CON03, CON02" + assert subject.format("sort({{materials.Name}})", element) == "CON01, CON02, CON03" + assert subject.format("reverse({{materials.Name}})", element) == "CON02, CON03, CON01" assert subject.format('join("-", {{materials.Name}})', element) == "CON01-CON03-CON02" def test_expressions(self): - assert subject.format('2+3') == "5" - assert subject.format('-2+3') == "1" - assert subject.format('2-3') == "-1" - assert subject.format('3*2') == "6" - assert subject.format('3/2') == "1.5" + assert subject.format("2+3") == "5" + assert subject.format("-2+3") == "1" + assert subject.format("2-3") == "-1" + assert subject.format("3*2") == "6" + assert subject.format("3/2") == "1.5" class TestGetElementValue(test.bootstrap.IFC4): diff --git a/src/ifcparse/IfcParse.cpp b/src/ifcparse/IfcParse.cpp index f0f880b7d6..95ab95c0db 100644 --- a/src/ifcparse/IfcParse.cpp +++ b/src/ifcparse/IfcParse.cpp @@ -746,7 +746,7 @@ namespace { static std::string format_double(const double& d) { std::ostringstream oss; oss.imbue(std::locale::classic()); - oss << std::setprecision(std::numeric_limits::digits10) << d; + oss << std::setprecision(std::numeric_limits::max_digits10) << d; const std::string str = oss.str(); oss.str(""); std::string::size_type e = str.find('e'); diff --git a/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py b/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py index 634a8fb796..c91467036f 100644 --- a/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py +++ b/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py @@ -17,21 +17,47 @@ # along with IfcPatch. If not, see . +import logging import tempfile +from typing import NamedTuple import ifcopenshell.util.element +import ifcpatch + try: - import sqlite3 + import sqlite3 # noqa: F401 except: print("No SQLite support") -class Patcher: +class ElementRow(NamedTuple): + element_id: int + guid: str + class_: str + predefined_type: str | None + name: str | None + description: str | None + + +class PropertyRow(NamedTuple): + element_id: int + pset_name: str + name: str + value: str + + +class RelationshipRow(NamedTuple): + element_id: int + rel_ifc_class: str + to_id: int + + +class Patcher(ifcpatch.BasePatcher): def __init__( self, - file, - logger, + file: ifcopenshell.file, + logger: logging.Logger | None = None, ): """Extracts properties and relationships from a IFC-SPF model to SQLite. @@ -45,18 +71,18 @@ class Patcher: result = ifcpatch.execute({"input": fn, "file": model, "recipe": "ExtractPropertiesToSQLite"}) ifcpatch.write(result, "output.sqlite") """ - self.file = file - self.logger = logger + super().__init__(file, logger) def patch(self): + import sqlite3 + tmp = tempfile.NamedTemporaryFile(delete=False) db_file = tmp.name self.db = sqlite3.connect(db_file) self.c = self.db.cursor() self.file_patched = db_file - self.c.execute( - """ + self.c.execute(""" CREATE TABLE IF NOT EXISTS elements ( id integer PRIMARY KEY NOT NULL UNIQUE, global_id text, @@ -65,51 +91,47 @@ class Patcher: name text, description text ); - """ - ) + """) self.c.execute("CREATE INDEX IF NOT EXISTS idx_global_id ON elements (global_id);") self.c.execute("CREATE INDEX IF NOT EXISTS idx_ifc_class ON elements (ifc_class);") self.c.execute("CREATE INDEX IF NOT EXISTS idx_predefined_type ON elements (predefined_type);") - self.c.execute( - """ + self.c.execute(""" CREATE TABLE IF NOT EXISTS relationships ( from_id integer NOT NULL, type text, to_id integer NOT NULL ); - """ - ) + """) self.c.execute("CREATE INDEX IF NOT EXISTS idx_from_id ON relationships (from_id);") - self.c.execute( - """ + self.c.execute(""" CREATE TABLE IF NOT EXISTS properties ( element_id integer NOT NULL, set_name text, name text, value text ); - """ - ) + """) self.c.execute("CREATE INDEX IF NOT EXISTS idx_element_id ON properties (element_id);") elements = self.file.by_type("IfcObjectDefinition") - rows = [] - properties = [] - relationships = [] + rows: list[ElementRow] = [] + properties: list[PropertyRow] = [] + relationships: list[RelationshipRow] = [] id_map = {e.id(): i for i, e in enumerate(elements)} + for i, element in enumerate(elements): rows.append( - [ + ElementRow( i, element[0], # IfcRoot.GlobalId element.is_a(), ifcopenshell.util.element.get_predefined_type(element), element[2], # IfcRoot.Name element[3], # IfcRoot.Description - ] + ) ) psets = ifcopenshell.util.element.get_psets(element, should_inherit=False) for pset_name, pset_data in psets.items(): @@ -120,49 +142,55 @@ class Patcher: value = "True" if value else "False" elif not isinstance(value, str): value = str(value) - properties.append([i, pset_name, prop_name, value]) + properties.append(PropertyRow(i, pset_name, prop_name, value)) material = ifcopenshell.util.element.get_material(element, should_skip_usage=True) if material: name = getattr(material, "Name", getattr(material, "LayerSetName", None)) or "Unnamed" - properties.append([i, "IFC Material", "Name", name]) - properties.append([i, "IFC Material", "Class", material.is_a()]) + properties.append(PropertyRow(i, "IFC Material", "Name", name)) + properties.append(PropertyRow(i, "IFC Material", "Class", material.is_a())) if material.is_a("IfcMaterial"): materials = [] elif material.is_a("IfcMaterialLayerSet"): for idx, item in enumerate(material.MaterialLayers or []): material = item.Material - properties.append([i, "IFC Material", f"Layer {idx + 1} Name", getattr(item, "Name", None)]) - properties.append([i, "IFC Material", f"Layer {idx + 1} Material", material.Name]) + properties.append( + PropertyRow(i, "IFC Material", f"Layer {idx + 1} Name", getattr(item, "Name", None)) + ) + properties.append(PropertyRow(i, "IFC Material", f"Layer {idx + 1} Material", material.Name)) if category := getattr(material, "Category", None): - properties.append([i, "IFC Material", f"Layer {idx + 1} Category", category]) + properties.append(PropertyRow(i, "IFC Material", f"Layer {idx + 1} Category", category)) elif material.is_a("IfcMaterialProfileSet"): for idx, item in enumerate(material.MaterialProfiles or []): material = item.Material - properties.append([i, "IFC Material", f"Profile {idx + 1} Name", item.Name]) - properties.append([i, "IFC Material", f"Profile {idx + 1} Material", material.Name]) + properties.append(PropertyRow(i, "IFC Material", f"Profile {idx + 1} Name", item.Name)) + properties.append(PropertyRow(i, "IFC Material", f"Profile {idx + 1} Material", material.Name)) if category := getattr(material, "Category", None): - properties.append([i, "IFC Material", f"Profile {idx + 1} Category", category]) + properties.append(PropertyRow(i, "IFC Material", f"Profile {idx + 1} Category", category)) elif material.is_a("IfcMaterialConstituentSet"): for idx, item in enumerate(material.MaterialConstituents or []): material = item.Material - properties.append([i, "IFC Material", f"Constituent {idx + 1} Name", item.Name]) - properties.append([i, "IFC Material", f"Constituent {idx + 1} Material", material.Name]) + properties.append(PropertyRow(i, "IFC Material", f"Constituent {idx + 1} Name", item.Name)) + properties.append( + PropertyRow(i, "IFC Material", f"Constituent {idx + 1} Material", material.Name) + ) if category := getattr(material, "Category", None): - properties.append([i, "IFC Material", f"Constituent {idx + 1} Category", category]) + properties.append( + PropertyRow(i, "IFC Material", f"Constituent {idx + 1} Category", category) + ) elif material.is_a("IfcMaterialList"): for idx, material in enumerate(material.Materials): - properties.append([i, "IFC Material", f"Material {idx + 1} Name", material.Name]) + properties.append(PropertyRow(i, "IFC Material", f"Material {idx + 1} Name", material.Name)) if category := getattr(material, "Category", None): - properties.append([i, "IFC Material", f"Material {idx + 1} Category", category]) + properties.append(PropertyRow(i, "IFC Material", f"Material {idx + 1} Category", category)) layers = ifcopenshell.util.element.get_layers(self.file, element) for idx, layer in enumerate(layers): - properties.append([i, "IFC Presentation Layer Assignment", f"Layer {idx + 1}", layer.Name]) + properties.append(PropertyRow(i, "IFC Presentation Layer Assignment", f"Layer {idx + 1}", layer.Name)) relating_type = ifcopenshell.util.element.get_type(element) if relating_type and relating_type != element: - relationships.append([i, "IfcRelDefinesByType", id_map[relating_type.id()]]) + relationships.append(RelationshipRow(i, "IfcRelDefinesByType", id_map[relating_type.id()])) self.c.executemany("INSERT INTO elements VALUES (?, ?, ?, ?, ?, ?);", rows) self.c.executemany("INSERT INTO properties VALUES (?, ?, ?, ?);", properties) diff --git a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py index f7e294898a..d1405e9a33 100644 --- a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py +++ b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py @@ -125,7 +125,6 @@ class Patcher(ifcpatch.BasePatcher): ) """ super().__init__(file, logger) - self.logger = logger self.sql_type: Literal["sqlite", "mysql"] = sql_type.lower() self.host = host self.username = username @@ -349,13 +348,11 @@ class Patcher(ifcpatch.BasePatcher): assert cursor is not None row = cursor.fetchone() elif self.sql_type == "mysql": - cursor = self.c.execute( - f""" + cursor = self.c.execute(f""" SELECT 1 FROM information_schema.tables WHERE table_schema = '{self.database}' AND table_name = 'id_map' LIMIT 1; - """ - ) + """) row = self.c.fetchone() else: assert_never(self.sql_type) diff --git a/src/ifcpatch/ifcpatch/recipes/MergeProjects.py b/src/ifcpatch/ifcpatch/recipes/MergeProjects.py index ffb8e2e65b..9d20164424 100644 --- a/src/ifcpatch/ifcpatch/recipes/MergeProjects.py +++ b/src/ifcpatch/ifcpatch/recipes/MergeProjects.py @@ -16,20 +16,28 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcPatch. If not, see . +from collections.abc import Sequence from logging import Logger from typing import Union +import ifcpatch import ifcopenshell import ifcopenshell.util.element import ifcopenshell.util.geolocation import ifcopenshell.util.unit import numpy as np +import ifcpatch from ifcpatch.recipes.SetFalseOrigin import Patcher as SetFalseOrigin -class Patcher: - def __init__(self, file: ifcopenshell.file, logger: Logger, filepaths: list[Union[str, ifcopenshell.file]]): +class Patcher(ifcpatch.BasePatcher): + def __init__( + self, + file: ifcopenshell.file, + logger: Logger | None = None, + filepaths: Sequence[Union[str, ifcopenshell.file]] = (), + ): """Merge two or more IFC models into one Note that other than combining the two (or more) IfcProject elements into @@ -50,8 +58,7 @@ class Patcher: ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "MergeProjects", "arguments": ["/path/to/model2.ifc"]}) """ - self.file = file - self.logger = logger + super().__init__(file, logger) self.filepaths = filepaths def patch(self): @@ -61,6 +68,8 @@ class Patcher: "replace it with a list of file/filepaths." ) self.filepaths = [self.filepaths] + if len(self.filepaths) == 0: + raise ValueError("At least one file/filepath must be provided to merge with the main model.") for filepath in self.filepaths: if isinstance(filepath, ifcopenshell.file): other = filepath diff --git a/src/ifctester/webapp/package-lock.json b/src/ifctester/webapp/package-lock.json index 647b9d534e..d94807f270 100644 --- a/src/ifctester/webapp/package-lock.json +++ b/src/ifctester/webapp/package-lock.json @@ -24,7 +24,7 @@ "clsx": "^2.1.1", "mode-watcher": "^1.1.0", "sass-embedded": "^1.89.0", - "svelte": "^5.28.1", + "svelte": "^5.53.6", "svelte-sonner": "^1.0.5", "tailwind-merge": "^3.3.0", "tailwind-variants": "^1.0.0", @@ -37,6 +37,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -543,6 +544,16 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -588,9 +599,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz", - "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -602,9 +613,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz", - "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -616,9 +627,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz", - "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -630,9 +641,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz", - "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -644,9 +655,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz", - "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -658,9 +669,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz", - "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -672,9 +683,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz", - "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -686,9 +697,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz", - "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -700,9 +711,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz", - "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -714,9 +725,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz", - "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -727,10 +738,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz", - "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], @@ -741,10 +752,38 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz", - "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -756,9 +795,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz", - "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -770,9 +809,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz", - "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -784,9 +823,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz", - "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -798,9 +837,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz", - "integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -812,9 +851,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz", - "integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -825,10 +864,38 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz", - "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -840,9 +907,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz", - "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -853,10 +920,24 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz", - "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -1270,9 +1351,15 @@ } }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, "node_modules/acorn": { @@ -1288,9 +1375,9 @@ } }, "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -1445,6 +1532,12 @@ "node": ">=8" } }, + "node_modules/devalue": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz", + "integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==", + "license": "MIT" + }, "node_modules/engine.io-client": { "version": "6.6.3", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", @@ -1546,9 +1639,9 @@ "license": "MIT" }, "node_modules/esrap": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.6.tgz", - "integrity": "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz", + "integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -2118,13 +2211,13 @@ } }, "node_modules/rollup": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz", - "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -2134,26 +2227,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.41.1", - "@rollup/rollup-android-arm64": "4.41.1", - "@rollup/rollup-darwin-arm64": "4.41.1", - "@rollup/rollup-darwin-x64": "4.41.1", - "@rollup/rollup-freebsd-arm64": "4.41.1", - "@rollup/rollup-freebsd-x64": "4.41.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", - "@rollup/rollup-linux-arm-musleabihf": "4.41.1", - "@rollup/rollup-linux-arm64-gnu": "4.41.1", - "@rollup/rollup-linux-arm64-musl": "4.41.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", - "@rollup/rollup-linux-riscv64-gnu": "4.41.1", - "@rollup/rollup-linux-riscv64-musl": "4.41.1", - "@rollup/rollup-linux-s390x-gnu": "4.41.1", - "@rollup/rollup-linux-x64-gnu": "4.41.1", - "@rollup/rollup-linux-x64-musl": "4.41.1", - "@rollup/rollup-win32-arm64-msvc": "4.41.1", - "@rollup/rollup-win32-ia32-msvc": "4.41.1", - "@rollup/rollup-win32-x64-msvc": "4.41.1", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, @@ -2677,21 +2775,23 @@ } }, "node_modules/svelte": { - "version": "5.33.10", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.10.tgz", - "integrity": "sha512-/yArPQIBoQS2p86LKnvJywOXkVHeEXnFgrDPSxkEfIAEkykopYuy2bF6UUqHG4IbZlJD6OurLxJT8Kn7kTk9WA==", + "version": "5.53.6", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.6.tgz", + "integrity": "sha512-lP5DGF3oDDI9fhHcSpaBiJEkFLuS16h92DhM1L5K1lFm0WjOmUh1i2sNkBBk8rkxJRpob0dBE75jRfUzGZUOGA==", "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", + "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", - "aria-query": "^5.3.1", + "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", + "devalue": "^5.6.3", "esm-env": "^1.2.1", - "esrap": "^1.4.6", + "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -2851,9 +2951,9 @@ } }, "node_modules/tar": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", - "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", + "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { diff --git a/src/ifctester/webapp/package.json b/src/ifctester/webapp/package.json index 4a8abd7897..bb7d58567e 100644 --- a/src/ifctester/webapp/package.json +++ b/src/ifctester/webapp/package.json @@ -18,7 +18,7 @@ "clsx": "^2.1.1", "mode-watcher": "^1.1.0", "sass-embedded": "^1.89.0", - "svelte": "^5.28.1", + "svelte": "^5.53.6", "svelte-sonner": "^1.0.5", "tailwind-merge": "^3.3.0", "tailwind-variants": "^1.0.0", diff --git a/src/opencdeserver/api/app/repository/bcf.py b/src/opencdeserver/api/app/repository/bcf.py index a65c43d115..3812334e41 100644 --- a/src/opencdeserver/api/app/repository/bcf.py +++ b/src/opencdeserver/api/app/repository/bcf.py @@ -694,8 +694,7 @@ class BCFDB(MyDB): snapshot_type = "" snapshot = False set_snapshot = "" - cypher_viewpoint = ( - """ + cypher_viewpoint = """ MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic) WHERE u.username = $username AND r1.createViewpoint = True @@ -713,9 +712,7 @@ class BCFDB(MyDB): v.spaces_visible = $spaces_visible, v.space_boundaries_visible = $space_boundaries_visible, v.openings_visible = $openings_visible - """ - % set_snapshot - ) + """ % set_snapshot if viewpoint.guid is None: viewpoint.guid = uuid4() if viewpoint.orthogonal_camera is None: @@ -1450,8 +1447,7 @@ class BCFDB(MyDB): else: document_url = "" document_reference.url = "" - cypher = ( - """ + cypher = """ MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic) WHERE u.username = $username AND r1.updateDocumentReferences = True @@ -1461,9 +1457,7 @@ class BCFDB(MyDB): SET r3.guid: $document_reference_id, %s d.description = $description - """ - % document_url - ) + """ % document_url result = tx.run( cypher, username=current_user.username, diff --git a/src/opencdeserver/api/app/repository/documents.py b/src/opencdeserver/api/app/repository/documents.py index 95c669a7bc..d03512c8e3 100644 --- a/src/opencdeserver/api/app/repository/documents.py +++ b/src/opencdeserver/api/app/repository/documents.py @@ -36,17 +36,14 @@ class DOCDB(MyDB): else: version_index_criteria = "AND d.version_index = $version_index" - cypher = ( - """ + cypher = """ MATCH (d:Document) WHERE d.document_id = $document_id %s RETURN d AS document ORDER by d.version_index DESC LIMIT 1 - """ - % version_index_criteria - ) + """ % version_index_criteria result = tx.run(cypher, document_id=document_id, version_index=version_index) @@ -891,8 +888,7 @@ class DOCDB(MyDB): else: version_index_criteria = "" - cypher = ( - """ + cypher = """ MATCH (u:User)-[r3:HAS_ACTIONS_ON]->(p:Project)-[r4:CONTAINS]->(d:Document) WHERE u.username = $username AND d.document_id = $document_id @@ -900,9 +896,7 @@ class DOCDB(MyDB): RETURN d AS document ORDER by d.version_index DESC LIMIT 1 - """ - % version_index_criteria - ) + """ % version_index_criteria result = tx.run( cypher, username=current_user.username, document_id=document_id, version_index=version_index @@ -927,8 +921,7 @@ class DOCDB(MyDB): else: version_index_criteria = "" - cypher = ( - """ + cypher = """ MATCH (u:User)-[r3:HAS_ACTIONS_ON]->(p:Project)-[r4:CONTAINS]->(d:Document) WHERE u.username = $username AND d.document_id = $document_id @@ -936,9 +929,7 @@ class DOCDB(MyDB): RETURN d AS document ORDER by d.version_index DESC LIMIT 1 - """ - % version_index_criteria - ) + """ % version_index_criteria result = tx.run( cypher, username=current_user.username, document_id=document_id, version_index=version_index diff --git a/src/serializers/schema_dependent/CMakeLists.txt b/src/serializers/schema_dependent/CMakeLists.txt index ef877342e9..73c1e5d3e7 100644 --- a/src/serializers/schema_dependent/CMakeLists.txt +++ b/src/serializers/schema_dependent/CMakeLists.txt @@ -9,9 +9,9 @@ foreach(schema ${SCHEMA_VERSIONS}) Serializers_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}" ) - target_link_libraries(Serializers_ifc${schema} ${HDF5_LIBRARIES} ${GLTF_LIBRARIES}) + target_link_libraries(Serializers_ifc${schema} IfcGeom ${HDF5_LIBRARIES} ${GLTF_LIBRARIES}) if(NOT WASM_BUILD) - target_link_libraries(Serializers_ifc${schema} IfcGeom ${OpenCASCADE_LIBRARIES}) + target_link_libraries(Serializers_ifc${schema} ${OpenCASCADE_LIBRARIES}) endif() endforeach() diff --git a/src/svgfill/CMakeLists.txt b/src/svgfill/CMakeLists.txt index f2f8c1a062..0d9764013a 100644 --- a/src/svgfill/CMakeLists.txt +++ b/src/svgfill/CMakeLists.txt @@ -36,7 +36,14 @@ message(STATUS "Boost include files found in ${Boost_INCLUDE_DIRS}") find_package(LibXml2 REQUIRED) find_package(CGAL REQUIRED) -include_directories(${Boost_INCLUDE_DIRS} ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/svgpp/include) +set(SVGPP_INCLUDE "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/svgpp/include") +if(NOT EXISTS ${SVGPP_INCLUDE}) + message( + FATAL_ERROR + "Missing svgpp include path, probably you forgot to initialize submodules in git repo. Missing path - '${SVGPP_INCLUDE}'." + ) +endif() +include_directories(${Boost_INCLUDE_DIRS} ${SVGPP_INCLUDE}) file(GLOB LIB_H_FILES src/*.h) file(GLOB LIB_CPP_FILES src/svgfill.cpp src/arrange_polygons.cpp) diff --git a/src/svgfill/src/arrange_polygons.cpp b/src/svgfill/src/arrange_polygons.cpp index 7bddc2e41a..4fa20c68dc 100644 --- a/src/svgfill/src/arrange_polygons.cpp +++ b/src/svgfill/src/arrange_polygons.cpp @@ -1,4 +1,4 @@ -// #define SVGFILL_DEBUG +#define SVGFILL_DEBUG // #define SVGFILL_MAIN #ifndef SVGFILL_MAIN @@ -38,6 +38,7 @@ typedef CGAL::Exact_predicates_exact_constructions_kernel K; typedef CGAL::Polygon_2 Polygon_2; typedef CGAL::Polygon_with_holes_2 Polygon_with_holes_2; typedef K::Point_2 Point_2; +typedef K::Vector_2 Vector_2; typedef K::Segment_2 Segment_2; typedef std::vector Polygon_list; typedef CGAL::Arr_segment_traits_2 Traits_2; @@ -164,31 +165,6 @@ boost::optional subtract_retain_largest(const T& lhs, const T& rhs) { return boost::none; } -// Function to write polygons as line segments in OBJ format -void write_polygon_to_obj(std::ofstream& ofs, size_t& vertex_index, bool as_line, const Polygon_2& polygon, const std::string& name) { - ofs << "o " << name << "\n"; // Object name - - // Write vertices - for (auto vit = polygon.vertices_begin(); vit != polygon.vertices_end(); ++vit) { - ofs << "v " << CGAL::to_double(vit->x()) << " " << CGAL::to_double(vit->y()) << " 0\n"; - } - - if (as_line) { - // Write line segments (edges) - for (size_t j = 0; j < polygon.size(); ++j) { - ofs << "l " << vertex_index + j << " " << vertex_index + (j + 1) % polygon.size() << "\n"; - } - } else { - ofs << "f"; - for (size_t j = 0; j < polygon.size(); ++j) { - ofs << " " << vertex_index + j; - } - ofs << "\n"; - } - - vertex_index += polygon.size(); -} - Polygon_2 circ_to_poly(typename Arrangement_2::Ccb_halfedge_const_circulator circ) { Polygon_2 poly; @@ -208,27 +184,6 @@ Polygon_with_holes_2 circ_to_poly(typename Arrangement_2::Ccb_halfedge_const_cir return poly; } -void write_polygon_to_svg(std::ostream& ofs, const Polygon_2& polygon) { - ofs << "x()) << "," << CGAL::to_double(vit->y()) << " "; - } - ofs << "\" style=\"fill:none;stroke-width:1\" />\n"; -} - -// Function to write a Polygon_with_holes_2 to an SVG file -void write_polygon_with_holes_to_svg(std::ostream& ofs, const Polygon_with_holes_2& polygon_with_holes) { - // Write the outer boundary (main polygon) - if (!polygon_with_holes.is_unbounded()) { - write_polygon_to_svg(ofs, polygon_with_holes.outer_boundary()); - } - - // Write the holes (if any) with a different color (e.g., red) - for (auto hit = polygon_with_holes.holes_begin(); hit != polygon_with_holes.holes_end(); ++hit) { - write_polygon_to_svg(ofs, *hit); - } -} - Polygon_2 fuse_with_offset(const std::vector& polygons, double polygon_offset_distance) { // Find the outer perimeter using offset - union - negative offset std::vector offset_polygons; @@ -285,78 +240,77 @@ Polygon_2 fuse_with_offset(const std::vector& polygons, double polygo return inner_offset.front(); } -void arrange_cgal_polygons(const std::vector& input_polygons_, std::vector& output_polygons, double polygon_offset_distance = -1.) { - static const double OVERLAP_RESOLUTION_DISTANCE = 1.e-2; - // even larger amount of inset so that outer perimeter is safely within all input polygons even when overlap resolution is applied - // no, `1.e-2 + 1.e-5` creates issues with the outer perimeter, are there other tolerances in play? - static const double OUTER_PERIMITER_ADDITIONAL_INSET_AMOUNT = 1.e-5; +double estimate_polygon_offset_distance(const std::vector& polygons) { + double total_edge_length = 0.; + size_t num_edges = 0; + for (auto& p : polygons) { + for (auto it = p.edges_begin(); it != p.edges_end(); ++it) { + total_edge_length += std::sqrt(CGAL::to_double(CGAL::squared_distance(it->start(), it->end()))); + num_edges += 1; + } + } + return total_edge_length / num_edges / 2; +} - if (polygon_offset_distance < 0.) { - double total_edge_length = 0.; - size_t num_edges = 0; - for (auto& p : input_polygons_) { - for (auto it = p.edges_begin(); it != p.edges_end(); ++it) { - total_edge_length += std::sqrt(CGAL::to_double(CGAL::squared_distance(it->start(), it->end()))); - num_edges += 1; +void clean_polygon(Polygon_2& poly) { + // Ensure counterclockwise orientation and remove duplicate last point if present also remove close points + if (!poly.is_counterclockwise_oriented()) { + poly.reverse_orientation(); + } + std::vector> ps(poly.begin(), poly.end()); + if (ps.front() == ps.back()) { + ps.pop_back(); + } + poly = Polygon_2(ps.begin(), ps.end()); + remove_close_points(poly); +} + +void smooth_polygon(double factor, Polygon_2& poly) { + auto ps = create_and_convert_offset_polygon(-factor, poly); + if (ps.size() == 1) { + auto r2 = ps.front(); + ps = create_and_convert_offset_polygon(+factor, r2); + if (ps.size() == 1) { + poly = ps.front(); + } + } +} + +template +void split_self_intersecting_polygon(const CGAL::Polygon_2& poly, OutIt output_it) { + if (poly.is_simple()) { + *output_it++ = poly; + return; + } + Arrangement_2 arr; + for (auto it = poly.edges_begin(); it != poly.edges_end(); ++it) { + CGAL::insert(arr, Segment_2(it->start(), it->end())); + } + for (auto it = arr.faces_begin(); it != arr.faces_end(); ++it) { + if (it->is_unbounded()) { + for (auto jt = it->inner_ccbs_begin(); jt != it->inner_ccbs_end(); ++jt) { + auto inner = circ_to_poly(*jt); + // reverse because it's an inner bound to the infinite outer facet + inner.reverse_orientation(); + *output_it++ = inner; } } - polygon_offset_distance = total_edge_length / num_edges / 2; } +} - auto input_polygons__ = input_polygons_; - decltype(input_polygons__) input_polygons; - - for (auto& i : input_polygons__) { - std::vector> ps(i.begin(), i.end()); - if (ps.front() == ps.back()) { - ps.pop_back(); - } - input_polygons.emplace_back(ps.begin(), ps.end()); - } - - for (auto& polygon : input_polygons) { - if (!polygon.is_counterclockwise_oriented()) { - polygon.reverse_orientation(); - } - } - - for (auto& polygon : input_polygons) { - remove_close_points(polygon); - } - -#ifdef SVGFILL_DEBUG - std::ofstream obj("obj.obj"); - size_t vi = 1; - - std::ofstream svg("svg.svg"); - svg << "\n"; - - for (auto it = input_polygons.begin(); it != input_polygons.end(); ++it) { - write_polygon_to_obj(obj, vi, true, *it, "input_poly_" + std::to_string(std::distance(input_polygons.begin(), it))); - write_polygon_to_svg(svg, *it); - } - - obj << std::flush; -#endif - +std::set> +find_overlaps(const std::vector& polygons) { typedef CGAL::Box_intersection_d::Box_with_handle_d Box; + std::vector boxes; + std::vector>> input_triangulated; - std::vector boxes; - for (auto it = input_polygons.begin(); it != input_polygons.end(); ++it) { + for (auto it = polygons.begin(); it != polygons.end(); ++it) { constexpr double offset = 1.e-3; auto b = it->bbox(); boxes.emplace_back( CGAL::Bbox_2(b.xmin() - offset, b.ymin() - offset, b.xmax() + offset, b.ymax() + offset), - std::distance(input_polygons.begin(), it) - ); - - if (!it->is_simple()) { -#ifdef SVGFILL_DEBUG - write_polygon_to_obj(obj, vi, true, *it, "self-intersecting"); -#endif - throw std::runtime_error("Self-intersecting input"); - } + std::distance(polygons.begin(), it)); CGAL::Polygon_triangulation_decomposition_2 decompositor; std::vector temp; @@ -378,10 +332,8 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v bool registered_overlap = false; for (auto& t2 : input_triangulated[b.handle()]) { if (CGAL::squared_distance(t1, t2) < (1.e-3 * 1.e-3)) { - overlaps.insert({ - (a.handle() < b.handle()) ? a.handle() : b.handle(), - (a.handle() < b.handle()) ? b.handle() : a.handle() - }); + overlaps.insert({(a.handle() < b.handle()) ? a.handle() : b.handle(), + (a.handle() < b.handle()) ? b.handle() : a.handle()}); registered_overlap = true; break; } @@ -393,297 +345,319 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v } }); - if (true) { - // solve overlaps by means of subtraction - // loop over overlaps and subtract the smaller polygon from the larger one + return overlaps; +} - std::set eliminated_polies; - std::map overlap_counts; - for (auto& p : overlaps) { - overlap_counts[p.first]++; - overlap_counts[p.second]++; +class DebugWriter { + public: + DebugWriter(bool enabled, const std::string& filename_prefix) + : enabled_(enabled) { + if (enabled_) { + obj.open(filename_prefix + ".obj"); + vi = 1; + svg.open(filename_prefix + ".svg"); + svg << "\n"; } - - for (const auto& edge : overlaps) { - // Skip eliminated - if (eliminated_polies.find(edge.first) != eliminated_polies.end() || - eliminated_polies.find(edge.second) != eliminated_polies.end()) { - continue; - } - - // Many overlaps indicate an aggregated polygon, skip them - /* - if (overlap_counts[edge.first] > 10 || overlap_counts[edge.second] > 10) { - if (overlap_counts[edge.first] > 10) { - eliminated_polies.insert(edge.first); - } - if (overlap_counts[edge.second] > 10) { - eliminated_polies.insert(edge.second); - } - continue; - } - */ - - // these are pointers now, because otherwise swap would not work? - auto* poly1 = &input_polygons[edge.first]; - auto* poly2 = &input_polygons[edge.second]; - - // Populate eliminated_polies with small polygons - // This can happen over time when modifications are made to the polygons to solve overlaps - bool skip = false; - if (poly1->area() < 1.e-2) { - eliminated_polies.insert(edge.first); - skip = true; - } - if (poly2->area() < 1.e-2) { - eliminated_polies.insert(edge.second); - skip = true; - } - // Small slivers are also just eliminated - if (!maybe_take_first_if_single_item(create_and_convert_offset_polygon(-1.e-1, *poly1))) { - eliminated_polies.insert(edge.first); - skip = true; - } - if (!maybe_take_first_if_single_item(create_and_convert_offset_polygon(-1.e-1, *poly2))) { - eliminated_polies.insert(edge.second); - skip = true; - } - if (skip) { - continue; - } - - // Skip polygons that have a very high intersection over union - // ratio, which indicates that they are very likely duplicates - if (CGAL::do_intersect(*poly1, *poly2)) { - std::vector result; - CGAL::intersection(*poly1, *poly2, std::back_inserter(result)); - typename K::FT intersection_area = 0; - for (auto& r : result) { - auto poly_area = r.outer_boundary().area(); - for (auto& h : r.holes()) { - poly_area -= h.area(); - } - intersection_area += poly_area; - } - CGAL::Polygon_with_holes_2 poly12; - CGAL::join(*poly1, *poly2, poly12); - typename K::FT union_area = poly12.outer_boundary().area(); - for (auto& h : poly12.holes()) { - union_area -= h.area(); - } - if (union_area > 0 && intersection_area / union_area > 0.99) { - // std::cerr << intersection_area / union_area << std::endl; - eliminated_polies.insert(edge.first); - continue; - } - } - - if (!(poly1->is_simple() && poly2->is_simple())) { - continue; - } - - { - std::vector result; - // std::cerr << poly1.area() << " " << poly2.area() << std::endl; - // std::cerr.flush(); - - boost::optional mp1, mp2, mp3, mp4; - bool swap = false; - - swap = poly1->area() <= poly2->area(); - if (swap) { - std::swap(poly1, poly2); - } - - bool success = false; - if ((mp1 = maybe_take_first_if_single_item(create_and_convert_offset_polygon(OVERLAP_RESOLUTION_DISTANCE, *poly2)))) { - if ((mp2 = subtract_retain_largest(*poly1, *mp1))) { - if ((mp3 = maybe_take_first_if_single_item(create_and_convert_offset_polygon(OVERLAP_RESOLUTION_DISTANCE * 2, *mp2)))) { - if ((mp4 = subtract_retain_largest(*poly2, *mp3))) { - *poly1 = *mp2; - *poly2 = *mp4; - success = true; - } - } - } - } - - /* - if (swap) { - // swap back to retain original ordering - // what's the point in swapping back here? - std::swap(poly1, poly2); - } - */ - - if (!success) { - eliminated_polies.insert(swap ? edge.first : edge.second); - continue; - } - } - } - - // iterate over the eliminated polygons and remove them from the input polygons - for (auto it = eliminated_polies.rbegin(); it != eliminated_polies.rend(); ++it) { - input_polygons.erase(input_polygons.begin() + *it); + } + ~DebugWriter() { + if (enabled_) { + svg << "\n"; + obj << std::flush; + obj.close(); + svg.close(); } } + void write_polygon(const Polygon_2& polygon, const std::string& name) { + if (enabled_) { + write_polygon_to_obj_(obj, vi, true, polygon, name); + write_polygon_to_svg_(svg, polygon, name); + obj << std::flush; + } + } + + void write_segment(const Point_2& p, const Point_2& q, const std::string& name) { + if (enabled_) { + if (last_segment_name_ != name) { + last_segment_name_ = name; + obj << "o " << name << "\n"; + } + obj << "v " << CGAL::to_double(p.x()) << " " << CGAL::to_double(p.y()) << " 0\n"; + obj << "v " << CGAL::to_double(q.x()) << " " << CGAL::to_double(q.y()) << " 0\n"; + obj << "l " << vi++; + obj << " " << vi++ << "\n"; + + svg << ""; + + obj << std::flush; + } + } + + void write_polygon(const Polygon_with_holes_2& polygon, const std::string& name) { + if (enabled_) { + write_polygon(polygon.outer_boundary(), name); + for (auto hit = polygon.holes_begin(); hit != polygon.holes_end(); ++hit) { + write_polygon(*hit, name); + } + } + } + + void write_polygons(const std::vector& polygons, const std::string& name) { + if (enabled_) { + size_t i = 0; + for (auto& polygon : polygons) { + write_polygon_to_obj_(obj, vi, true, polygon, name + "_" + std::to_string(i++)); + write_polygon_to_svg_(svg, polygon, name); + } + obj << std::flush; + } + } + + void write_polygons(const std::vector& polygons, const std::string& name) { + if (enabled_) { + size_t i = 0; + for (auto& polygon : polygons) { + write_polygon_to_obj_(obj, vi, true, polygon.outer_boundary(), name + "_" + std::to_string(i)); + write_polygon_to_svg_(svg, polygon.outer_boundary(), name); + for (auto hit = polygon.holes_begin(); hit != polygon.holes_end(); ++hit) { + write_polygon_to_obj_(obj, vi, true, *hit, name + "_" + std::to_string(i)); + write_polygon_to_svg_(svg, *hit, name); + } + } + obj << std::flush; + } + } + + private: + std::ofstream obj; + size_t vi; + std::ofstream svg; + bool enabled_; + std::string last_segment_name_; + + void write_polygon_to_svg_(std::ostream& ofs, const Polygon_2& polygon, const std::string& class_name = "") { + ofs << "x()) << "," << -CGAL::to_double(vit->y()) << " "; + } + ofs << "\"/>\n"; + } + + void write_polygon_to_obj_(std::ofstream& ofs, size_t& vertex_index, bool as_line, const Polygon_2& polygon, const std::string& name) { + ofs << "o " << name << "\n"; // Object name + + // Write vertices + for (auto vit = polygon.vertices_begin(); vit != polygon.vertices_end(); ++vit) { + ofs << "v " << CGAL::to_double(vit->x()) << " " << CGAL::to_double(vit->y()) << " 0\n"; + } + + if (as_line) { + // Write line segments (edges) + for (size_t j = 0; j < polygon.size(); ++j) { + ofs << "l " << vertex_index + j << " " << vertex_index + (j + 1) % polygon.size() << "\n"; + } + } else { + ofs << "f"; + for (size_t j = 0; j < polygon.size(); ++j) { + ofs << " " << vertex_index + j; + } + ofs << "\n"; + } + + vertex_index += polygon.size(); + } +}; + +void eliminate_overlaps(double OVERLAP_RESOLUTION_DISTANCE, std::vector& polygons) { + // solve overlaps by means of subtraction + // loop over overlaps and subtract the smaller polygon from the larger one + + std::set eliminated_polies; + /* - if constexpr (false) { - // solve overlap by means of union into components - std::vector> adj(input_polygons.size()); - for (const auto& edge : overlaps) { - adj[edge.first].push_back(edge.second); - adj[edge.second].push_back(edge.first); - } - - std::vector visited(input_polygons.size(), false); - std::vector> connected_components; - - for (size_t v = 0; v < input_polygons.size(); ++v) { - if (!visited[v]) { - connected_components.emplace_back(); - - std::stack stack; - stack.push(v); - visited[v] = true; - - while (!stack.empty()) { - size_t u = stack.top(); - stack.pop(); - connected_components.back().push_back(u); - - for (size_t neighbor : adj[u]) { - if (!visited[neighbor]) { - visited[neighbor] = true; - stack.push(neighbor); - } - } - } - } - } - - std::vector fused_polies; - - for (auto& comp : connected_components) { - std::vector comp_polies; - if (comp.size() == 1) { - fused_polies.push_back(input_polygons[comp.front()]); - } else { - for (auto& c : comp) { - comp_polies.push_back(input_polygons[c]); - } - fused_polies.push_back(fuse_with_offset(comp_polies, 1.e-2)); - } - } - -#ifdef SVGFILL_DEBUG - for (auto it = fused_polies.begin(); it != fused_polies.end(); ++it) { - write_polygon_to_obj(obj, vi, true, *it, "fused_poly_" + std::to_string(std::distance(fused_polies.begin(), it))); - write_polygon_to_svg(svg, *it); - } -#endif - - input_polygons = fused_polies; + std::map overlap_counts; + for (auto& p : overlaps) { + overlap_counts[p.first]++; + overlap_counts[p.second]++; } */ - { - // [NB Nov 6] we cannot do this anymore because it could revert the spacing between input polygons - // that touch in the corner. - // Now that overlaps/touches at corners are handled more locally only a small indent is produced - // which would be undone by means of an inset+offset. - // - // [NB Nov 10] this is actually still necessary though, but we apply a much smaller distance now - // to keep the overlap eliminations in tact - // - // Inset-offset to remove tiny details that may cause enourmous spikes in offsets - for (auto& r : input_polygons) { - auto ps = create_and_convert_offset_polygon(-polygon_offset_distance / 10000., r); - if (ps.size() == 1) { - auto r2 = ps.front(); - ps = create_and_convert_offset_polygon(+polygon_offset_distance / 10000., r2); - if (ps.size() == 1) { - r = ps.front(); + auto overlaps = find_overlaps(polygons); + + for (const auto& edge : overlaps) { + // Skip eliminated + if (eliminated_polies.find(edge.first) != eliminated_polies.end() || + eliminated_polies.find(edge.second) != eliminated_polies.end()) { + continue; + } + + // Many overlaps indicate an aggregated polygon, skip them + /* + if (overlap_counts[edge.first] > 10 || overlap_counts[edge.second] > 10) { + if (overlap_counts[edge.first] > 10) { + eliminated_polies.insert(edge.first); + } + if (overlap_counts[edge.second] > 10) { + eliminated_polies.insert(edge.second); + } + continue; + } + */ + + // these are pointers now, because otherwise swap would not work? + auto* poly1 = &polygons[edge.first]; + auto* poly2 = &polygons[edge.second]; + + // @todo this is applied during overlap processing, maybe better after the boolean operation, + // because they can be come small or narrow when overlaps are resolved + + // Populate eliminated_polies with small polygons + // This can happen over time when modifications are made to the polygons to solve overlaps + bool skip = false; + if (poly1->area() < 1.e-2) { + eliminated_polies.insert(edge.first); + skip = true; + } + if (poly2->area() < 1.e-2) { + eliminated_polies.insert(edge.second); + skip = true; + } + // Small slivers are also just eliminated + if (!maybe_take_first_if_single_item(create_and_convert_offset_polygon(-1.e-1, *poly1))) { + eliminated_polies.insert(edge.first); + skip = true; + } + if (!maybe_take_first_if_single_item(create_and_convert_offset_polygon(-1.e-1, *poly2))) { + eliminated_polies.insert(edge.second); + skip = true; + } + if (skip) { + continue; + } + + // Skip polygons that have a very high intersection over union + // ratio, which indicates that they are very likely duplicates + if (CGAL::do_intersect(*poly1, *poly2)) { + std::vector result; + CGAL::intersection(*poly1, *poly2, std::back_inserter(result)); + typename K::FT intersection_area = 0; + for (auto& r : result) { + auto poly_area = r.outer_boundary().area(); + for (auto& h : r.holes()) { + poly_area -= h.area(); } + intersection_area += poly_area; + } + CGAL::Polygon_with_holes_2 poly12; + CGAL::join(*poly1, *poly2, poly12); + typename K::FT union_area = poly12.outer_boundary().area(); + for (auto& h : poly12.holes()) { + union_area -= h.area(); + } + if (union_area > 0 && intersection_area / union_area > 0.99) { + // std::cerr << intersection_area / union_area << std::endl; + eliminated_polies.insert(edge.first); + continue; + } + } + + if (!(poly1->is_simple() && poly2->is_simple())) { + continue; + } + + { + std::vector result; + + boost::optional mp1, mp2, mp3, mp4; + bool swap = false; + + swap = poly1->area() <= poly2->area(); + if (swap) { + std::swap(poly1, poly2); + } + + bool success = false; + if ((mp1 = maybe_take_first_if_single_item(create_and_convert_offset_polygon(OVERLAP_RESOLUTION_DISTANCE, *poly2)))) { + if ((mp2 = subtract_retain_largest(*poly1, *mp1))) { + if ((mp3 = maybe_take_first_if_single_item(create_and_convert_offset_polygon(OVERLAP_RESOLUTION_DISTANCE * 2, *mp2)))) { + if ((mp4 = subtract_retain_largest(*poly2, *mp3))) { + *poly1 = *mp2; + *poly2 = *mp4; + success = true; + } + } + } + } + + if (!success) { + eliminated_polies.insert(swap ? edge.first : edge.second); + continue; } } } -#ifdef SVGFILL_DEBUG - for (auto it = input_polygons.begin(); it != input_polygons.end(); ++it) { - write_polygon_to_obj(obj, vi, true, *it, "processed_input_poly_" + std::to_string(std::distance(input_polygons.begin(), it))); - write_polygon_to_svg(svg, *it); + // iterate over the eliminated polygons and remove them from the input polygons + for (auto it = eliminated_polies.rbegin(); it != eliminated_polies.rend(); ++it) { + polygons.erase(polygons.begin() + *it); } -#endif +} - // Unfortunately CGAL does not seem to have a ready to use aabb primitive for segments in 2D, - // so we have to use 3D segments and aabb tree for 2D polygons. - std::list> all_segs; - std::unordered_map*, decltype(input_polygons.begin())> seg_to_poly; +class SegmentLookup { + public: + typedef std::vector::const_iterator PolygonIt; - for (auto it = input_polygons.begin(); it != input_polygons.end(); ++it) { - for (auto eit = it->edges_begin(); eit != it->edges_end(); ++eit) { - CGAL::Segment_3 seg3d( - CGAL::Point_3(eit->source().x(), eit->source().y(), 0), - CGAL::Point_3(eit->target().x(), eit->target().y(), 0) - ); - all_segs.push_back(seg3d); - seg_to_poly[&all_segs.back()] = it; - } - } - - using TreeTraits = CGAL::AABB_traits>::iterator>>; - using Tree = CGAL::AABB_tree; - - Tree tree(all_segs.begin(), all_segs.end()); - tree.accelerate_distance_queries(); - - auto input_polygon_boundary = - [&](const Point_2& p, double tol = 1e-5) -> decltype(input_polygons.begin()) + SegmentLookup(const std::vector& polygons) + : polygons_ref_(polygons) { + // Unfortunately CGAL does not seem to have a ready to use aabb primitive for segments in 2D, + // so we have to use 3D segments and aabb tree for 2D polygons. + for (auto it = polygons.begin(); it != polygons.end(); ++it) { + for (auto eit = it->edges_begin(); eit != it->edges_end(); ++eit) { + CGAL::Segment_3 seg3d( + CGAL::Point_3(eit->source().x(), eit->source().y(), 0), + CGAL::Point_3(eit->target().x(), eit->target().y(), 0)); + all_segs.push_back(seg3d); + seg_to_poly[&all_segs.back()] = it; + } + } + tree_ = Tree(all_segs.begin(), all_segs.end()); + tree_.accelerate_distance_queries(); + } + + // This part is the most computationally expensive. Caching effectively halves the lookup time here, since every vertex on the subdivided corridor mesh has on average two outgoing edges. + PolygonIt input_polygon_boundary(const Point_2& p, double tol = 1e-5) { + auto it = input_polygon_boundary_cache_.find(p); + if (it != input_polygon_boundary_cache_.end()) { + return it->second; + } + // Find closest point & corresponding segment - auto closest = tree.closest_point_and_primitive(CGAL::Point_3(p.x(), p.y(), 0)); + auto closest = tree_.closest_point_and_primitive(CGAL::Point_3(p.x(), p.y(), 0)); const auto& closest_pt = closest.first; auto seg_ptr = &*closest.second; double d = CGAL::to_double(CGAL::squared_distance(p, Point_2(closest_pt.x(), closest_pt.y()))); + + PolygonIt res; if (d < (tol * tol)) { - return seg_to_poly.find(seg_ptr)->second; + res = seg_to_poly.find(seg_ptr)->second; + } else { + res = polygons_ref_.end(); } - return input_polygons.end(); + + input_polygon_boundary_cache_[p] = res; + return res; }; - /* - auto input_polygon_boundary = [&input_polygons](const CGAL::Point_2& p, double tol = 1.e-5) { - // unfortunately some imprecision slept into the code so we can't - // so we can't just use has_on_boundary() anymore - double D = std::numeric_limits::infinity(); - for (auto it = input_polygons.begin(); it != input_polygons.end(); ++it) { - for (auto jt = it->edges_begin(); jt != it->edges_end(); ++jt) { - const auto& seg = *jt; - auto d = std::sqrt(CGAL::to_double(CGAL::squared_distance(seg, p))); - if (d < D) { - D = d; - } - if (d < tol) { - return it; - } - } - } - return input_polygons.end(); - }; - */ - - auto close_input_point = [&input_polygons](const CGAL::Point_2& P) { + std::pair> close_input_point(const CGAL::Point_2& P) const { + // @todo use tree CGAL::Point_2 closest; double closest_distance = std::numeric_limits::infinity(); - auto input_it = input_polygons.end(); + auto input_it = polygons_ref_.end(); // unfortunately some imprecision slept into the code so we can't // so we can't just use has_on_boundary() anymore - for (auto it = input_polygons.begin(); it != input_polygons.end(); ++it) { + for (auto it = polygons_ref_.begin(); it != polygons_ref_.end(); ++it) { for (auto& p : *it) { auto d = std::sqrt(CGAL::to_double(CGAL::squared_distance(P, p))); if (d < closest_distance) { @@ -697,14 +671,16 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v return std::make_pair(input_it, closest); }; - auto project_input_point = [&input_polygons](const CGAL::Point_2& P) { + std::pair> project_input_point(const CGAL::Point_2& P) const { + // @todo use tree + CGAL::Point_2 closest; typename K::FT closest_sq_distance = std::numeric_limits::infinity(); - auto input_it = input_polygons.end(); + auto input_it = polygons_ref_.end(); // unfortunately some imprecision slept into the code so we can't // so we can't just use has_on_boundary() anymore - for (auto it = input_polygons.begin(); it != input_polygons.end(); ++it) { + for (auto it = polygons_ref_.begin(); it != polygons_ref_.end(); ++it) { for (auto jt = it->edges_begin(); jt != it->edges_end(); ++jt) { auto Pp = jt->supporting_line().projection(P); auto d = CGAL::squared_distance(Pp, P); @@ -719,224 +695,96 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v return std::make_pair(input_it, closest); }; - // Find the outer perimeter using offset - union - negative offset - std::vector offset_polygons; - for (auto& r : input_polygons) { - auto R = r; - if (!R.is_counterclockwise_oriented()) { - R.reverse_orientation(); - } + std::vector> n_closest_input_segments(const Segment_2& e, size_t n = 2) const { + auto mid = CGAL::ORIGIN + ((e.source() - CGAL::ORIGIN) + (e.target() - CGAL::ORIGIN)) / 2; - // Overlap removal can also result in close points causing problems when converted into non-exact nt - remove_close_points(R); + std::vector>::iterator> cands; + cands.reserve(64); - auto ps = create_and_convert_offset_polygon(polygon_offset_distance, R); - for (auto& p : ps) { - if (!p.is_simple()) { - /*{ - std::cerr << "input ["; - bool first = true; - for (auto& pp : r) { - if (!first) { - std::cerr << ","; - } - first = false; - std::cerr << "(" << pp.x() << "," << pp.y() << ")"; - } - std::cerr << "]" << std::endl; - } + auto mid3 = CGAL::Point_3(mid.x(), mid.y(), 0); - { - std::cerr << "["; - bool first = true; - for (auto& pp : p) { - if (!first) { - std::cerr << ","; - } - first = false; - std::cerr << "(" << pp.x() << "," << pp.y() << ")"; - } - std::cerr << "]" << std::endl; - }*/ - - throw std::runtime_error("Complex polygon originated from offset"); - } - } - offset_polygons.insert(offset_polygons.end(), ps.begin(), ps.end()); - } - -#ifdef SVGFILL_DEBUG - for (auto it = offset_polygons.begin(); it != offset_polygons.end(); ++it) { - write_polygon_to_obj(obj, vi, true, *it, "offset_poly_" + std::to_string(std::distance(offset_polygons.begin(), it))); - write_polygon_to_svg(svg, *it); - } -#endif - - // Perform Boolean union on the offset polygons - std::vector unioned_polygons; - CGAL::join(offset_polygons.begin(), offset_polygons.end(), std::back_inserter(unioned_polygons)); - - if (unioned_polygons.size() > 1) { - // @todo this is currently one of the major limitations in the code that still can be eliminated - // by grouping the input polygons by their perimiter polygon in unioned_polygons - std::sort(unioned_polygons.begin(), unioned_polygons.end(), [](auto& p, auto& q) { return p.outer_boundary().area() > q.outer_boundary().area(); }); - } - -#ifdef SVGFILL_DEBUG - write_polygon_to_obj(obj, vi, true, unioned_polygons.front().outer_boundary(), "offset_poly_joined"); - write_polygon_to_svg(svg, unioned_polygons.front().outer_boundary()); - -#endif - - Polygon_2 fused_removed_close_points; - { - std::vector> ps; - auto& p = unioned_polygons.front().outer_boundary(); - ps.reserve(p.size()); - auto I = p.begin(); - auto J = I + 1; - for (;; ++J) { - bool last = false; - if (J == p.end()) { - J = p.begin(); - last = true; - } - // if (CGAL::squared_distance(*I, *J) > (polygon_offset_distance * polygon_offset_distance)) { - if (CGAL::squared_distance(*I, *J) > (1.e-4 * 1.e-4)) { - ps.push_back(*J); - I = J; - } - if (last) { + for (int i = -1; i <= 3; ++i) { + cands.clear(); + double r = std::pow(10.0, i); + auto midbb = mid3.bbox(); + CGAL::Bbox_3 box(midbb.xmin() - r, midbb.ymin() - r, -1.0, midbb.xmax() + r, midbb.ymax() + r, +1.0); + tree_.all_intersected_primitives(box, std::back_inserter(cands)); + if (cands.size() >= n) { break; } } - fused_removed_close_points = Polygon_2(ps.begin(), ps.end()); + + if (cands.empty()) { + return {}; + } + + std::vector>> scored; + scored.reserve(cands.size()); + for (auto it : cands) { + const auto& s3 = *it; + Segment_2 s2(Point_2(s3.source().x(), s3.source().y()), Point_2(s3.target().x(), s3.target().y())); + scored.emplace_back(CGAL::squared_distance(mid, s2), s2); + } + + std::sort(scored.begin(), scored.end(), [](auto& a, auto& b) { return a.first < b.first; }); + if (scored.size() > n) { + scored.resize(n); + } + + std::vector> out; + out.reserve(scored.size()); + for (auto& p : scored) { + out.push_back(p.second); + } + return out; } - // Apply negative offset to get the outer perimeter polygon - auto inner_offset = create_and_convert_offset_polygon( - // Because polygon_offset is inexact, make sure our inset distance is slightly larger - // std::nexttoward(-polygon_offset_distance, -std::numeric_limits::infinity()), +private: + using TreeTraits = CGAL::AABB_traits>::iterator>>; + using Tree = CGAL::AABB_tree; - // 1.e-8 even was too little and still resulted in slivers of triangle around the perimeter - -polygon_offset_distance - OUTER_PERIMITER_ADDITIONAL_INSET_AMOUNT, - fused_removed_close_points); + const std::vector& polygons_ref_; + std::list> all_segs; + std::unordered_map*, PolygonIt> seg_to_poly; + Tree tree_; -#ifdef SVGFILL_DEBUG - write_polygon_to_obj(obj, vi, true, inner_offset.front(), "joined_inset"); - write_polygon_to_svg(svg, inner_offset.front()); -#endif + std::map::const_iterator> input_polygon_boundary_cache_; +}; - /* - // there is non-insignificant chance that around the outer boundary, vertices are located in - // between of the input polyhedra, but intermediate vertices result in triangles that will no longer - // span between the two spaces with two edges and therefore cause the topological centre line - // to no run up to the center. Eliminate all vertices that are not on the polyhedral boundary of polygon. - - // this theory proved to be false. once we have topological end points in our graph that are - // connected to input polyhedra to form closed cells, we move those topological end points to - // the average of the input polyhedra corner points, thus effectively also moving them outwards. - { - for (auto& i : inner_offset) { - std::vector> ps; - for (auto& p : i) { - if (input_polygon_boundary(p, 1.e-3) != input_polygons.end()) { - ps.push_back(p); - } - } - i = Polygon_2(ps.begin(), ps.end()); +Polygon_2 subdivide_polygon(double max_distance, const Polygon_2 & p) { + std::vector points; + for (auto it = p.edges_begin(); it != p.edges_end(); ++it) { + const auto& seg = *it; + auto num_splits = (int)std::ceil(std::sqrt(CGAL::to_double(seg.squared_length())) / max_distance) - 1; + points.push_back(seg.source()); + for (auto i = 0; i < num_splits; ++i) { + auto d = (seg.target() - seg.source()) / (num_splits + 1) * (i + 1); + points.push_back(seg.source() + d); } } + return Polygon_2(points.begin(), points.end()); +}; -#ifdef SVGFILL_DEBUG - write_polygon_to_obj(obj, vi, true, inner_offset.front(), "joined_inset_cleaned"); - write_polygon_to_svg(svg, inner_offset.front()); -#endif - */ - - // Subtract original polygons from outer perimeter - std::vector difference_result, difference_result_subdivided; - for (auto& i : inner_offset) { - std::vector working_copy; - working_copy.emplace_back(i); - - for (auto& r : input_polygons) { - std::vector temp_working_copy; - for (auto& wc : working_copy) { - CGAL::difference(wc, r, std::back_inserter(temp_working_copy)); - } - working_copy = temp_working_copy; - } - difference_result.insert(difference_result.end(), working_copy.begin(), working_copy.end()); +Polygon_with_holes_2 subdivide_polygon(double max_distance, const Polygon_with_holes_2& pwh) { + Polygon_2 outer = subdivide_polygon(max_distance, pwh.outer_boundary()); + std::vector holes; + for (auto hit = pwh.holes_begin(); hit != pwh.holes_end(); ++hit) { + holes.push_back(subdivide_polygon(max_distance, *hit)); } + return Polygon_with_holes_2(outer, holes.begin(), holes.end()); +}; - // subdivide difference_result to have better behave triangulation - - { - const double max_distance = polygon_offset_distance / 8.; - auto subdivide_polygon = [max_distance](const Polygon_2& p) { - std::vector points; - for (auto it = p.edges_begin(); it != p.edges_end(); ++it) { - const auto& seg = *it; - auto num_splits = (int)std::ceil(std::sqrt(CGAL::to_double(seg.squared_length())) / max_distance) - 1; - points.push_back(seg.source()); - for (auto i = 0; i < num_splits; ++i) { - auto d = (seg.target() - seg.source()) / (num_splits + 1) * (i + 1); - points.push_back(seg.source() + d); - } - } - return Polygon_2(points.begin(), points.end()); - }; - - for (auto& pwh : difference_result) { - // Subdivide outer boundary - Polygon_2 outer = subdivide_polygon(pwh.outer_boundary()); - // Subdivide holes - std::vector holes; - for (auto hit = pwh.holes_begin(); hit != pwh.holes_end(); ++hit) { - holes.push_back(subdivide_polygon(*hit)); - } - // Construct new Polygon_with_holes_2 - difference_result_subdivided.push_back(Polygon_with_holes_2(outer, holes.begin(), holes.end())); - } - } - -#ifdef SVGFILL_DEBUG - for (auto it = difference_result_subdivided.begin(); it != difference_result_subdivided.end(); ++it) { - auto i = std::distance(difference_result_subdivided.begin(), it); - write_polygon_to_obj(obj, vi, true, it->outer_boundary(), "difference_result_subdivided_" + std::to_string(i)); - write_polygon_to_svg(svg, it->outer_boundary()); - for (auto& p : it->holes()) { - write_polygon_to_obj(obj, vi, true, p, "difference_result_subdivided_" + std::to_string(i)); - write_polygon_to_svg(svg, p); - } - } -#endif - - std::list> triangular_polygons; - - for (auto& pwh : difference_result_subdivided) { - CGAL::Polygon_triangulation_decomposition_2 decompositor; - decompositor(pwh, std::back_inserter(triangular_polygons)); - } - - triangular_polygons.erase(std::remove_if(triangular_polygons.begin(), triangular_polygons.end(), [](const CGAL::Polygon_2& p) { - return CGAL::to_double(p.area()) < 1.e-8; - }), triangular_polygons.end()); - -#ifdef SVGFILL_DEBUG - for (auto it = triangular_polygons.begin(); it != triangular_polygons.end(); ++it) { - write_polygon_to_obj(obj, vi, false, *it, "tri_" + std::to_string(std::distance(triangular_polygons.begin(), it))); - write_polygon_to_svg(svg, *it); - } -#endif - +std::tuple< + std::map>, + std::map>, + std::map, std::vector*>>> +build_line_graph(const std::vector& input_polygons, SegmentLookup& segment_lookup, const std::vector& triangular_polygons) { // Build maps of triangle -> edge and edge -> triangle in order to do traversal on the 'corridor mesh' - std::map, std::vector*>> segment_to_facet; - std::map, std::vector*>> segment_to_input_facet; + std::map, std::vector*>> segment_to_facet; + std::map, std::vector*>> segment_to_input_facet; std::map, Point_2> segment_to_midpoint; std::map> midpoint_to_segment; - std::map*, std::vector>> facet_to_segment; + std::map*, std::vector>> facet_to_segment; for (auto& tri : triangular_polygons) { for (size_t i = 0; i < 3; ++i) { @@ -950,26 +798,14 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v } } - // This part is the most computationally expensive. Caching effectively halves the lookup time here, since every vertex has two outgoing edges. - std::map input_polygon_boundary_cache; - auto cached_input_polygon_boundary = [&](const Point_2& p, double tol = 1e-5) -> decltype(input_polygons.begin()) - { - auto it = input_polygon_boundary_cache.find(p); - if (it == input_polygon_boundary_cache.end()) { - auto index = input_polygon_boundary(p, tol); - input_polygon_boundary_cache[p] = index; - return index; - } else { - return it->second; - } - }; + // @todo The smarter thing to do probably after creating the corridor mesh, register segments wrt to originating input polygon(s) and maintain that mapping when subdividing // Register midpoints on the edges within the 'corridor mesh' that span multiple input polygons for (auto& p : segment_to_facet) { auto center = CGAL::ORIGIN + (((p.first.first - CGAL::ORIGIN) + (p.first.second - CGAL::ORIGIN)) / 2); - auto p1index = cached_input_polygon_boundary(p.first.first); - auto p2index = cached_input_polygon_boundary(p.first.second); + auto p1index = segment_lookup.input_polygon_boundary(p.first.first); + auto p2index = segment_lookup.input_polygon_boundary(p.first.second); segment_to_input_facet[p.first].push_back(&*p1index); segment_to_input_facet[p.first].push_back(&*p2index); @@ -978,17 +814,8 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v segment_to_midpoint[p.first] = center; midpoint_to_segment[center] = p.first; } - - if (p1index != input_polygons.end() && p2index != input_polygons.end() && p1index != p2index) { - segment_to_midpoint[p.first] = center; - midpoint_to_segment[center] = p.first; - } } -#ifdef SVGFILL_DEBUG - obj << "o network_1\n"; -#endif - // Observe corridor mesh topology to join edge midpoints into a network std::map> line_graph; for (auto& p : segment_to_midpoint) { @@ -1000,20 +827,15 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v decltype(segment_to_midpoint)::const_iterator it; if ((it = segment_to_midpoint.find(r)) != segment_to_midpoint.end()) { line_graph[p.second].push_back(it->second); - -#ifdef SVGFILL_DEBUG - obj << "v " << CGAL::to_double(p.second.x()) << " " << CGAL::to_double(p.second.y()) << " 0\n"; - obj << "v " << CGAL::to_double(it->second.x()) << " " << CGAL::to_double(it->second.y()) << " 0\n"; - obj << "l " << vi++; - obj << " " << vi++ << "\n"; - - svg << "second.x()) << "\" y2=\"" << CGAL::to_double(it->second.y()) << "\" />"; -#endif } } } } + return {line_graph, midpoint_to_segment, segment_to_input_facet}; +} + +std::set> find_triangles(const std::map>& line_graph) { // Find triangles in this network often occuring at junctions in the corridor mesh std::set> triangles; std::function&)> find_triangles_recursive; @@ -1024,7 +846,7 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v const std::vector& neighbors_current = line_graph.at(path.back()); if (std::find(neighbors_current.begin(), neighbors_current.end(), path.front()) != neighbors_current.end()) { // We found a triangle, add it to the set - Triangle triangle = { path[0], path[1], path[2] }; + Triangle triangle = {path[0], path[1], path[2]}; std::sort(triangle.begin(), triangle.end()); triangles.insert(triangle); } @@ -1037,29 +859,28 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v if (std::find(path.begin(), path.end(), neighbor) == path.end()) { path.push_back(neighbor); find_triangles_recursive(path); - path.pop_back(); // Backtrack + path.pop_back(); // Backtrack } } }; for (auto& p : line_graph) { - std::vector ps = { p.first }; + std::vector ps = {p.first}; find_triangles_recursive(ps); } - // For every triangle found in the network we eliminate one edge to break the cycle - // The edge we eliminate is the edge with the greatest angle with any of it's neighbours + return triangles; +} - // non exact time, we need sqrt +std::set> eliminate_triangles(const std::map>& line_graph) { + auto triangles = find_triangles(line_graph); + + // @todo this currently uses a simple cartesian kernel for performance for support of sqrt, but + // this should be possible to rewrite as ratios/slopes in the exact kernel as well using SK = CGAL::Simple_cartesian; CGAL::Cartesian_converter C{}; -#ifdef SVGFILL_DEBUG - obj << "o eliminated\n"; -#endif - std::set> eliminated_segments; - for (auto& t : triangles) { Triangle st; std::transform(t.begin(), t.end(), st.begin(), C); @@ -1075,7 +896,7 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v double max_abs_dot = 0.; { - auto& ni = line_graph[t[i]]; + auto& ni = line_graph.find(t[i])->second; for (auto& n : ni) { if (std::find(t.begin(), t.end(), n) == t.end()) { // not contained in triangle @@ -1092,7 +913,7 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v } { - auto& nj = line_graph[t[j]]; + auto& nj = line_graph.find(t[j])->second; for (auto& n : nj) { if (std::find(t.begin(), t.end(), n) == t.end()) { // not contained in triangle @@ -1106,7 +927,6 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v } } } - } if (max_abs_dot < global_min_abs_dot) { @@ -1119,176 +939,195 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v auto i = global_min_abs_dot_index; auto j = (i + 2) % 3; - eliminated_segments.insert({ t[i], t[j] }); - eliminated_segments.insert({ t[j], t[i] }); - -#ifdef SVGFILL_DEBUG - obj << "v " << st[j].x() << " " << st[j].y() << " 0\n"; - obj << "v " << st[i].x() << " " << st[i].y() << " 0\n"; - obj << "l " << vi++; - obj << " " << vi++ << "\n"; - - svg << ""; -#endif + eliminated_segments.insert({t[i], t[j]}); + eliminated_segments.insert({t[j], t[i]}); } - } - Graph2D G2(line_graph); - for (auto& e : eliminated_segments) { - G2.remove_edge(e.first, e.second); + return eliminated_segments; +} + +bool is_parallel_2degree_node(Graph2D::vertex_const_iterator vit) { + auto it = vit->second.begin(); + auto& P = *it++; + auto& Q = *it++; + auto e1 = P - vit->first; + auto e2 = vit->first - Q; + if (e1.squared_length() == 0 || e2.squared_length() == 0) { + // @todo why does this happen? + return false; } + e1 /= std::sqrt(CGAL::to_double(e1.squared_length())); + e2 /= std::sqrt(CGAL::to_double(e2.squared_length())); + return std::abs(CGAL::to_double(e1 * e2)) > (1. - 1.e-5); +}; - auto G = G2.weld_vertices(); -#ifdef SVGFILL_DEBUG - obj << "o network_2\n"; - for (auto it = G.edges_begin(); it != G.edges_end(); ++it) { - obj << "v " << CGAL::to_double(it->first.x()) << " " << CGAL::to_double(it->first.y()) << " 0\n"; - obj << "v " << CGAL::to_double(it->second.x()) << " " << CGAL::to_double(it->second.y()) << " 0\n"; - obj << "l " << vi++; - obj << " " << vi++ << "\n"; - } - obj << std::flush; -#endif - - auto is_parallel_2degree_node = [](decltype(G)::vertex_const_iterator vit) { - auto it = vit->second.begin(); - auto& P = *it++; - auto& Q = *it++; - auto e1 = P - vit->first; - auto e2 = vit->first - Q; - if (e1.squared_length() == 0 || e2.squared_length() == 0) { - // @todo why does this happen? - return false; - } - e1 /= std::sqrt(CGAL::to_double(e1.squared_length())); - e2 /= std::sqrt(CGAL::to_double(e2.squared_length())); - return std::abs(CGAL::to_double(e1 * e2)) > (1. - 1.e-5); - }; - - { - // Remove colinear vertices - size_t n_vertices_removed = 0; - for (auto vit = G.vertices_begin(); vit != G.vertices_end();) { - if (vit->second.size() == 2) { - if (is_parallel_2degree_node(vit)) { - vit = G.eliminate_vertex(vit); - ++n_vertices_removed; - } else { - ++vit; - } +void eliminate_colinear_vertices(Graph2D& G) { + size_t n_vertices_removed = 0; + for (auto vit = G.vertices_begin(); vit != G.vertices_end();) { + if (vit->second.size() == 2) { + if (is_parallel_2degree_node(vit)) { + vit = G.eliminate_vertex(vit); + ++n_vertices_removed; } else { ++vit; } + } else { + ++vit; + } + } +} + +struct Ccw_radial_sort { + Point_2 c; + explicit Ccw_radial_sort(const Point_2& center) : c(center) {} + + bool operator()(const Point_2& a, const Point_2& b) const { + const Vector_2 va = a - c; + const Vector_2 vb = b - c; + + // Only left-turn is not sufficient because we should not wrap around, + // but rather start from e.g positive x-axis and then sort CCW. + // Therefore top-half plane always comes before bottom-half plane. + const bool ua = va.y() == 0 ? va.x() > 0 : va.y() > 0; + const bool ub = vb.y() == 0 ? vb.x() > 0 : vb.y() > 0; + if (ua != ub) { + return ua; + } + + if (CGAL::collinear(c, a, b)) { + // Nearer first so that original polygon edges are likely retained + // (not sure if it matters). + return va.squared_length() < vb.squared_length(); + } + + // This is a less functor, so we return true if c,a,b is a left turn, which means that a is CCW before b + return CGAL::left_turn(c, a, b); + } +}; + +void build_radial_neighbour_map(const std::vector& polygons, double radius, std::map>& neighbour_map) { + for (auto& poly : polygons) { + for (auto it = poly.edges_begin(); it != poly.edges_end(); ++it) { + auto source = it->source(); + auto target = it->target(); + neighbour_map[source].push_back(target); + neighbour_map[target].push_back(source); } - // std::cout << "Eliminated " << n_vertices_removed << " vertices" << std::endl; } - // Ortho edge slide - { - std::list> edges_to_remove, edges_to_insert; + // Box_intersection_d package to find close vertices and connect them as well + typedef CGAL::Box_intersection_d::Box_with_handle_d Box; + std::vector boxes; + for (auto& poly : polygons) { + for (auto it = poly.vertices_begin(); it != poly.vertices_end(); ++it) { + const auto pb = it->bbox(); + boxes.emplace_back( + CGAL::Bbox_2(pb.xmin() - radius, pb.ymin() - radius, pb.xmax() + radius, pb.ymax() + radius), + *it); + } + } + CGAL::box_self_intersection_d(boxes.begin(), boxes.end(), [&](const Box& a, const Box& b) { + if ((a.handle() - b.handle()).squared_length() <= (radius * radius)) { + neighbour_map[a.handle()].push_back(b.handle()); + neighbour_map[b.handle()].push_back(a.handle()); + } + }); - for (auto vit = G.vertices_begin(); vit != G.vertices_end(); ++vit) { - auto& selected = vit->first; + // radial sort + for (auto& p : neighbour_map) { + auto& nb = p.second; + std::sort(nb.begin(), nb.end(), Ccw_radial_sort(p.first)); + } +} - if (vit->second.size() >= 3) { - for (auto vjt = vit->second.begin(); vjt != vit->second.end(); ++vjt) { - auto& neighbour = *vjt; - bool processed_neighbour = false; +void edge_slide(Graph2D& G) { + std::list> edges_to_remove, edges_to_insert; - if (G.find(neighbour)->second.size() == 2 && !is_parallel_2degree_node(G.find(neighbour))) { - auto vkt = G.find(neighbour)->second.begin(); - if (selected == *vkt) { - vkt++; - } - auto& other = *vkt; + for (auto vit = G.vertices_begin(); vit != G.vertices_end(); ++vit) { + auto& selected = vit->first; - if ((other - neighbour).squared_length() < (neighbour - selected).squared_length()) { - continue; - } + if (vit->second.size() >= 3) { + for (auto vjt = vit->second.begin(); vjt != vit->second.end(); ++vjt) { + auto& neighbour = *vjt; + bool processed_neighbour = false; - auto incoming = CGAL::Ray_2(other, neighbour - other); - boost::optional> closest_neighbouring_segment; - boost::optional> closest_intersection_point; - K::FT sq_distance_along_ray = std::numeric_limits::infinity(); + if (G.find(neighbour)->second.size() == 2 && !is_parallel_2degree_node(G.find(neighbour))) { + auto vkt = G.find(neighbour)->second.begin(); + if (selected == *vkt) { + vkt++; + } + auto& other = *vkt; - for (auto vlt = vit->second.begin(); vlt != vit->second.end(); ++vlt) { - auto& other_neighbour = *vlt; - if (vlt != vjt) { - CGAL::Segment_2 neighbouring_segment(selected, other_neighbour); - auto x = CGAL::intersection(incoming, neighbouring_segment); - if (x) { - if (auto* xp = variant_get>(&*x)) { - auto dist = ((*xp) - other).squared_length(); - if (dist < sq_distance_along_ray) { - closest_neighbouring_segment = neighbouring_segment; - closest_intersection_point = *xp; - sq_distance_along_ray = dist; - } + if ((other - neighbour).squared_length() < (neighbour - selected).squared_length()) { + continue; + } + + auto incoming = CGAL::Ray_2(other, neighbour - other); + boost::optional> closest_neighbouring_segment; + boost::optional> closest_intersection_point; + K::FT sq_distance_along_ray = std::numeric_limits::infinity(); + + for (auto vlt = vit->second.begin(); vlt != vit->second.end(); ++vlt) { + auto& other_neighbour = *vlt; + if (vlt != vjt) { + CGAL::Segment_2 neighbouring_segment(selected, other_neighbour); + auto x = CGAL::intersection(incoming, neighbouring_segment); + if (x) { + if (auto* xp = variant_get>(&*x)) { + auto dist = ((*xp) - other).squared_length(); + if (dist < sq_distance_along_ray) { + closest_neighbouring_segment = neighbouring_segment; + closest_intersection_point = *xp; + sq_distance_along_ray = dist; } } } } - - if (closest_intersection_point && closest_neighbouring_segment) { - edges_to_remove.push_back(*closest_neighbouring_segment); - edges_to_remove.push_back({ neighbour, selected }); - edges_to_insert.push_back({ closest_neighbouring_segment->source(), *closest_intersection_point }); - edges_to_insert.push_back({ closest_neighbouring_segment->target(), *closest_intersection_point }); - edges_to_insert.push_back({ neighbour, *closest_intersection_point }); - - processed_neighbour = true; - } } - if (processed_neighbour) { - // Only one neigbour is processed because otherwise we obtain intersections - break; + + if (closest_intersection_point && closest_neighbouring_segment) { + edges_to_remove.push_back(*closest_neighbouring_segment); + edges_to_remove.push_back({neighbour, selected}); + edges_to_insert.push_back({closest_neighbouring_segment->source(), *closest_intersection_point}); + edges_to_insert.push_back({closest_neighbouring_segment->target(), *closest_intersection_point}); + edges_to_insert.push_back({neighbour, *closest_intersection_point}); + + processed_neighbour = true; } } + if (processed_neighbour) { + // Only one neigbour is processed because otherwise we obtain intersections + break; + } } } - - for (auto& s : edges_to_remove) { - G.remove_edge(s.source(), s.target()); - } - - - for (auto& s : edges_to_insert) { - G.insert(s.source(), s.target()); - } - -#ifdef SVGFILL_DEBUG - obj << "o network_3\n"; - for (auto it = G.edges_begin(); it != G.edges_end(); ++it) { - obj << "v " << CGAL::to_double(it->first.x()) << " " << CGAL::to_double(it->first.y()) << " 0\n"; - obj << "v " << CGAL::to_double(it->second.x()) << " " << CGAL::to_double(it->second.y()) << " 0\n"; - obj << "l " << vi++; - obj << " " << vi++ << "\n"; - } -#endif } - // Now plot the edges on an arrangement in order to find planar cycles - // and merge the corridor-halves with their neighbouring input polygon - - Arrangement_2 arr; - - for (auto it = G.edges_begin(); it != G.edges_end(); ++it) { - if (it->first == it->second) { - continue; - } - CGAL::insert(arr, Segment_2(it->first, it->second)); + for (auto& s : edges_to_remove) { + G.remove_edge(s.source(), s.target()); } - std::list> move_ops; - std::list> edge_ops; + for (auto& s : edges_to_insert) { + G.insert(s.source(), s.target()); + } +} + +std::list> extend_end_vertices_based_on_input( + const Graph2D& G, + const std::map>& midpoint_to_segment, + const std::map, std::vector*>>& segment_to_input_facet, + const Polygon_list& inner_offset, + const SegmentLookup& segment_lookup +){ + std::list> constructed_segments; for (auto it = G.vertices_begin(); it != G.vertices_end(); ++it) { if (it->second.size() == 1) { auto& M = it->first; - decltype(midpoint_to_segment)::mapped_type* q = nullptr; + const std::pair* q = nullptr; if (midpoint_to_segment.find(M) == midpoint_to_segment.end()) { typename K::FT min_sq_distance = std::numeric_limits::infinity(); @@ -1299,7 +1138,7 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v } } } else { - q = &midpoint_to_segment[M]; + q = &midpoint_to_segment.find(M)->second; } if (q == nullptr) { @@ -1309,9 +1148,10 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v bool handled_as_graph_path = false; // distance from unioned - shoot ray? - if (segment_to_input_facet[*q].size() == 2) { + if (segment_to_input_facet.find(*q)->second.size() == 2) { for (auto& bnd : inner_offset) { // if point M is contained in bnd interior: + // if (!bnd.has_on_unbounded_side(M)) { if (bnd.has_on_bounded_side(M)) { auto& incoming = *it->second.begin(); // create ray incoming -> M @@ -1336,13 +1176,16 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v } if (closest_intersection_point) { + constructed_segments.push_front({M, *closest_intersection_point}); + break; +#if 0 Graph2D GGG(bnd); GGG.refine(*GGG.query(*closest_intersection_point, 0.01), *closest_intersection_point); - std::array>, 2> input_points = { { {}, {} } }; + std::array>, 2> input_points = {{{}, {}}}; size_t i = 0; - for (auto& fac : segment_to_input_facet[*q]) { + for (auto& fac : segment_to_input_facet.find(*q)->second) { for (auto it = fac->vertices_begin(); it != fac->vertices_end(); ++it) { auto seg = GGG.query(*it, 0.01); if (seg) { @@ -1361,28 +1204,32 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v if (!a1.empty() && !a2.empty()) { if (M != *closest_intersection_point) { - edge_ops.push_front({ M, *closest_intersection_point }); + constructed_segments.push_front({M, *closest_intersection_point}); } for (auto it = a1.begin(); it != a1.end() && std::next(it) != a1.end(); ++it) { - edge_ops.push_front({ *it, *(std::next(it)) }); + constructed_segments.push_front({*it, *(std::next(it))}); } for (auto it = a2.begin(); it != a2.end() && std::next(it) != a2.end(); ++it) { - edge_ops.push_front({ *it, *(std::next(it)) }); + constructed_segments.push_front({*it, *(std::next(it))}); } handled_as_graph_path = true; break; } +#endif + } else { + std::cerr << "Warning: no intersection found when extending end vertex, this will likely result in invalid topology" << std::endl; } } } } +#if 0 if (!handled_as_graph_path) { // else we choose to map point to the midpoint of the found two close points. - auto pq = close_input_point(q->first); - auto pr = close_input_point(q->second); + auto pq = segment_lookup.close_input_point(q->first); + auto pr = segment_lookup.close_input_point(q->second); auto Q = pq.second; auto R = pr.second; @@ -1392,16 +1239,16 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v // where Q and R are co-located, because the point R' is further away // in that case M + M-Q should gives is x that we then project onto the // input boundary - // - // - // ┌───────┐ - // │ │ - // │ │ - // │ │ - // └───────o <--Q,R - // - // ────────o <--M - // + // + // + // ┌───────┐ + // │ │ + // │ │ + // │ │ + // └───────o <--Q,R + // + // ────────o <--M + // // ┌───────x───────────────o <---R' // │ │ // │ │ @@ -1410,93 +1257,23 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v // └───────────────────────┘ // @todo is this projection actually necessary or is it already 'exact enough'? - R = project_input_point(M + (M - Q)).second; + R = segment_lookup.project_input_point(M + (M - Q)).second; } auto avg = CGAL::ORIGIN + ((Q - CGAL::ORIGIN) + (R - CGAL::ORIGIN)) / 2; - move_ops.push_front({ M, avg }); - edge_ops.push_front({ avg, Q }); - edge_ops.push_front({ avg, R }); - + constructed_segments.push_front({M, avg}); + constructed_segments.push_front({avg, Q}); + constructed_segments.push_front({avg, R}); } - } - } - -#ifdef SVGFILL_DEBUG - obj << "o network_4\n"; #endif - - // note that we actually don't move but draw an edge - for (auto& pq : move_ops) { - if (pq.first == pq.second) { - continue; - } - CGAL::insert(arr, Segment_2(pq.first, pq.second)); - -#ifdef SVGFILL_DEBUG - obj << "v " << CGAL::to_double(pq.first.x()) << " " << CGAL::to_double(pq.first.y()) << " 0\n"; - obj << "v " << CGAL::to_double(pq.second.x()) << " " << CGAL::to_double(pq.second.y()) << " 0\n"; - obj << "l " << vi++; - obj << " " << vi++ << "\n"; -#endif - } - - - for (auto& pq : edge_ops) { - if (pq.first == pq.second) { - continue; - } - CGAL::insert(arr, Segment_2(pq.first, pq.second)); - -#ifdef SVGFILL_DEBUG - obj << "v " << CGAL::to_double(pq.first.x()) << " " << CGAL::to_double(pq.first.y()) << " 0\n"; - obj << "v " << CGAL::to_double(pq.second.x()) << " " << CGAL::to_double(pq.second.y()) << " 0\n"; - obj << "l " << vi++; - obj << " " << vi++ << "\n"; -#endif - } - - // Plot input polygons - for (auto& poly : input_polygons) { - for (size_t i = 0; i != poly.size(); ++i) { - auto j = (i + 1) % poly.size(); - if (poly.vertex(i) == poly.vertex(j)) { - continue; - } - CGAL::insert(arr, Segment_2(poly.vertex(i), poly.vertex(j))); } } + return constructed_segments; +} -#ifdef SVGFILL_DEBUG - { - obj << "o arrangement_1\n"; - for (auto it = arr.edges_begin(); it != arr.edges_end(); ++it) { - auto& p = it->source()->point(); - auto& q = it->target()->point(); - obj << "v " << CGAL::to_double(p.x()) << " " << CGAL::to_double(p.y()) << " 0\n"; - obj << "v " << CGAL::to_double(q.x()) << " " << CGAL::to_double(q.y()) << " 0\n"; - obj << "l " << vi++; - obj << " " << vi++ << "\n"; - } - } -#endif - - /* { - // debug, add outer bounds so that we can plot the face for any remaining edges - auto poly = unioned_polygons.front().outer_boundary(); - for (size_t i = 0; i != poly.size(); ++i) { - auto j = (i + 1) % poly.size(); - CGAL::insert(arr, Segment_2(poly.vertex(i), poly.vertex(j))); - } - } */ - - // Now loop over the arrangement faces, when a face coincides with a point on the - // corridor network we know it needs to be joined with an input polygon. In that - // case the edges need to be eliminated that correspond to original geometry. - - size_t face_id = 0; +void fuse_corridor_halves_with_input(Arrangement_2& arr, Graph2D& G, SegmentLookup& segment_lookup, const Polygon_list& input_polygons, DebugWriter& debug_output) { std::set edges_to_remove; for (auto it = arr.faces_begin(); it != arr.faces_end(); ++it) { @@ -1535,7 +1312,7 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v auto& p = curr->source()->point(); auto& q = curr->target()->point(); auto center = CGAL::ORIGIN + (((p - CGAL::ORIGIN) + (q - CGAL::ORIGIN)) / 2); - auto p1index = input_polygon_boundary(center); + auto p1index = segment_lookup.input_polygon_boundary(center); const bool on_orig_bound = p1index != input_polygons.end(); if (on_orig_bound) { if (edges_to_remove.find(curr->twin()) != edges_to_remove.end()) { @@ -1553,7 +1330,7 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v auto& p = curr->source()->point(); auto& q = curr->target()->point(); auto center = CGAL::ORIGIN + (((p - CGAL::ORIGIN) + (q - CGAL::ORIGIN)) / 2); - auto p1index = input_polygon_boundary(center); + auto p1index = segment_lookup.input_polygon_boundary(center); const bool on_orig_bound = p1index != input_polygons.end(); if (on_orig_bound) { if (edges_to_remove.find(curr->twin()) != edges_to_remove.end()) { @@ -1569,126 +1346,724 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v } } } - -#ifdef SVGFILL_DEBUG - write_polygon_to_svg(svg, circ_to_poly(it->outer_ccb())); - - obj << "o " << "face_"; - if (is_corridor) { - obj << "corri_"; - } - obj << face_id++ << "\n"; - - std::ostringstream oss; - - { - auto vv = vi; - auto curr = it->outer_ccb(); - do { - auto& p = curr->source()->point(); - obj << "v " << CGAL::to_double(p.x()) << " " << CGAL::to_double(p.y()) << " 0\n"; - oss << "l " << vi++; - ++curr; - if (curr == it->outer_ccb()) { - oss << " " << vv << "\n"; - } else { - oss << " " << vi << "\n"; - } - } while (curr != it->outer_ccb()); - } - - for (auto jt = it->inner_ccbs_begin(); jt != it->inner_ccbs_end(); ++jt) { - auto vv = vi; - auto curr = *jt; - do { - auto& p = curr->source()->point(); - obj << "v " << CGAL::to_double(p.x()) << " " << CGAL::to_double(p.y()) << " 0\n"; - oss << "l " << vi++; - ++curr; - if (curr == *jt) { - oss << " " << vv << "\n"; - } else { - oss << " " << vi << "\n"; - } - } while (curr != *jt); - } - - obj << oss.str(); -#endif } size_t remove_id = 0; for (auto& e : edges_to_remove) { -#ifdef SVGFILL_DEBUG - obj << "o " << "remove_" << remove_id++ << "\n"; - { - auto& p = e->source()->point(); - obj << "v " << CGAL::to_double(p.x()) << " " << CGAL::to_double(p.y()) << " 0\n"; - } - { - auto& p = e->target()->point(); - obj << "v " << CGAL::to_double(p.x()) << " " << CGAL::to_double(p.y()) << " 0\n"; - } - obj << "l " << vi++; - obj << " " << vi++ << std::endl; -#endif + debug_output.write_segment(e->source()->point(), e->target()->point(), "arr_remove_edge_" + std::to_string(remove_id++)); CGAL::remove_edge(arr, e); } +} + +#include + +class Segment_2_less { + public: + bool operator()(const Segment_2& a, const Segment_2& b) const { + if (a.source() != b.source()) { + return a.source() < b.source(); + } + return a.target() < b.target(); + } +}; + +void clean_noisy_paths(Arrangement_2& arr, SegmentLookup& segment_lookup) { + using SK = CGAL::Simple_cartesian; + CGAL::Cartesian_converter C{}; + + auto other = [](const Segment_2& e, const Point_2& v) { + return (e.source() == v) ? e.target() : e.source(); + }; + + std::set edges; + for (auto he = arr.edges_begin(); he != arr.edges_end(); ++he) { + auto a = he->source()->point(); + auto b = he->target()->point(); + if (a < b) { + edges.insert({a, b}); + } else { + edges.insert({b, a}); + } + } + + auto edge_badness = [&](const Segment_2& e) -> double { + auto closest = segment_lookup.n_closest_input_segments(e, 2); + if (closest.size() != 2) { + throw std::runtime_error("Unable to locate two nearby edges"); + } + + auto get_dir = [&](const Segment_2& s) { + auto a = C(s.source()); + auto b = C(s.target()); + SK::Vector_2 v = b - a; + double l = std::sqrt(v.squared_length()); + if (l <= 1e-12) { + return std::make_pair(SK::Vector_2(0, 0), 0.); + } + return std::make_pair(v / l, l); + }; + + auto [own_dir, own_length] = get_dir(e); + + auto angle = [&](const SK::Vector_2& ov) { + double d = std::abs(own_dir * ov); + if (d > 1.0) { + d = 1.0; + } + return std::acos(d); + }; + + double best = std::numeric_limits::infinity(); + for (auto& s : closest) { + auto [dv, dl] = get_dir(s); + best = std::min(best, angle(dv)); + } + return (best + 0.1) / own_length; + }; + + std::map badnesses; + for (auto& e : edges) { + badnesses[e] = edge_badness(e); + } + + double thr; + { + std::vector tmp; + tmp.reserve(badnesses.size()); + for (auto& p : badnesses) { + tmp.push_back(p.second); + } + std::nth_element(tmp.begin(), tmp.begin() + tmp.size() / 2, tmp.end()); + double med = tmp[tmp.size() / 2]; + thr = 10.0 * med; + } + + std::set bad_edges; + for (auto& p : badnesses) { + if (p.second > thr) { + bad_edges.insert(p.first); + } + } + + std::map> topo, bad_topo; + + for (auto& e : edges) { + topo[e.source()].push_back(e); + topo[e.target()].push_back(e); + } + for (auto& e : bad_edges) { + bad_topo[e.source()].push_back(e); + bad_topo[e.target()].push_back(e); + } + + std::set break_vertices; + for (auto& p : bad_topo) { + const auto& v = p.first; + if (!(topo[v].size() == 2 && bad_topo[v].size() == 2)) { + break_vertices.insert(v); + } + } + + std::set seen; + std::vector> bad_paths; + + for (auto& s : break_vertices) { + for (auto& e0 : bad_topo[s]) { + if (seen.count(e0)) { + continue; + } + + Point_2 v = s; + std::vector path; + path.push_back(s); + + auto e = e0; + while (true) { + seen.insert(e); + v = other(e, v); + path.push_back(v); + + if (break_vertices.count(v)) { + break; + } + + auto& inc = bad_topo[v]; + std::vector nxt; + nxt.reserve(2); + for (auto& ee : inc) { + if (ee != e && !seen.count(ee)) { + nxt.push_back(ee); + } + } + if (nxt.size() != 1) { + break; + } + e = nxt[0]; + } + + if (path.size() > 1) { + bad_paths.push_back(std::move(path)); + } + } + } + + std::set> to_remove; + std::vector> to_insert; + + auto dirs_from = [&](const Point_2& v, const std::set& path_edges) { + std::vector ds; + for (auto& ee : topo[v]) { + if (path_edges.count(ee) || bad_edges.count(ee)) { + continue; + } + Point_2 u = other(ee, v); + ds.push_back(v - u); + } + return ds; + }; + + auto collapse_path = [&](const std::vector& path) -> std::optional { + std::set path_edges; + for (size_t i = 0; i + 1 < path.size(); ++i) { + auto* a = &path[i]; + auto* b = &path[i + 1]; + if (*a < *b) { + path_edges.insert({*a, *b}); + } else { + path_edges.insert({*b, *a}); + } + } + + const auto& a0 = path.front(); + const auto& b0 = path.back(); + + auto das = dirs_from(a0, path_edges); + auto dbs = dirs_from(b0, path_edges); + if (das.empty() || dbs.empty()) { + return std::nullopt; + } + + K::FT best_ke = std::numeric_limits::infinity(); + std::optional best_x; + + for (auto& da : das) { + for (auto& db : dbs) { + CGAL::Ray_2 r1(a0, da); + CGAL::Ray_2 r2(b0, db); + + auto x = CGAL::intersection(r1, r2); + if (x) { + if (auto* xp = variant_get>(&*x)) { + // @todo does it matter that this is squared? + auto ke = CGAL::squared_distance(*xp, a0) + CGAL::squared_distance(*xp, b0); + if (ke < best_ke) { + best_ke = ke; + best_x = *xp; + } + } + } + } + } + + return best_x; + }; + + for (auto& path : bad_paths) { + auto x = collapse_path(path); + if (!x) { + // std::cerr << "Unable to collapse path, skipping" << std::endl; + continue; + } + + double orig_length = 0.; + for (size_t i = 0; i < path.size() - 1; ++i) { + auto& a = path[i]; + auto& b = path[i + 1]; + orig_length += std::sqrt(CGAL::to_double(CGAL::squared_distance(a, b))); + } + + double new_length = std::sqrt(CGAL::to_double((path.front() - *x).squared_length())) + std::sqrt(CGAL::to_double((path.back() - *x).squared_length())); + + if (new_length > orig_length * 2 || orig_length > new_length * 2) { + // std::cerr << "Collapsing path would increase length too much, skipping" << std::endl; + continue; + } + + std::cerr << "new_length: " << new_length << " orig_length: " << orig_length << std::endl; + + for (size_t i = 0; i < path.size(); ++i) { + auto& v = path[i]; + if (CGAL::squared_distance(v, *x) < 1.e-5) { + std::cerr << "Collapsing path would create near-duplicate vert to previous path, skipping" << std::endl; + continue; + } + } + + for (size_t i = 0; i < path.size() - 1; ++i) { + auto& a = path[i]; + auto& b = path[i + 1]; + if (a < b) { + to_remove.insert({a, b}); + } else { + to_remove.insert({b, a}); + } + } + auto s = path.front(); + auto t = path.back(); + if (s != *x) { + to_insert.push_back({s, *x}); + } + if (t != *x) { + to_insert.push_back({t, *x}); + } + } + + /* + using Walk_pl = CGAL::Arr_walk_along_line_point_location; + Walk_pl walk_pl(arr); + + for (auto& e : to_remove) { + // debug_output.write_segment(e->source()->point(), e->target()->point(), "arr_bad_remove"); + auto res = walk_pl.locate(e.first); + if (auto* v = boost::get(&res)) { + Arrangement_2::Halfedge_around_vertex_circulator first, curr; + first = curr = (*v)->incident_halfedges(); + size_t i = 0; + std::array pts; + std::array hes; + do { + Arrangement_2::Vertex_const_handle u = curr->source(); + hes[i] = curr; + pts[i++] = u->point(); + } while (++curr != first); + + + if ((*v)->point() != e.first) { + std::cerr << "Warning: unable to locate vertex for edge removal, skipping" << std::endl; + continue; + } + } else { + std::cerr << "Warning: unable to locate vertex for edge removal, skipping" << std::endl; + continue; + } + } + */ + + for (auto& e : to_remove) { + bool removed = false; + for (auto he = arr.edges_begin(); he != arr.edges_end(); ++he) { + auto a = he->source()->point(); + auto b = he->target()->point(); + if ((a == e.first && b == e.second) || (a == e.second && b == e.first)) { + CGAL::remove_edge(arr, he); + removed = true; + break; + } + } + if (!removed) { + std::cerr << "Warning: unable to locate edge for removal, skipping" << std::endl; + } + } + + for (auto& pq : to_insert) { + if (pq.first == pq.second) { + continue; + } + CGAL::insert(arr, Segment_2(pq.first, pq.second)); + // debug_output.write_segment(pq.first, pq.second, "arr_bad_insert"); + } + +} + +void remove_colinear_vertices(Arrangement_2& arr) { + std::set to_remove; + std::set> to_add; + while (true) { + bool removed_this_round = false; + for (auto it = arr.vertices_begin(); it != arr.vertices_end(); ++it) { + if (it->degree() == 2) { + Arrangement_2::Halfedge_around_vertex_circulator first, curr; + first = curr = it->incident_halfedges(); + size_t i = 0; + std::array pts; + std::array hes; + do { + Arrangement_2::Vertex_const_handle u = curr->source(); + hes[i] = curr; + pts[i++] = u->point(); + } while (++curr != first); + + using SK = CGAL::Simple_cartesian; + CGAL::Cartesian_converter C{}; + + auto a = C(pts[0]); + auto b = C(it->point()); + auto c = C(pts[1]); + + SK::Vector_2 ab = b - a; + SK::Vector_2 ac = c - a; + ab /= std::sqrt(ab.squared_length()); + ac /= std::sqrt(ac.squared_length()); + + double d = ab * ac; + if (std::acos(d) < 1e-12) { + CGAL::remove_edge(arr, hes[0]); + CGAL::remove_edge(arr, hes[1]); + CGAL::insert(arr, Segment_2(pts[0], pts[1])); + removed_this_round = true; + break; + }; + } + } + if (!removed_this_round) { + break; + } + } +} + +class timer { + public: + class entry { + public: + entry(std::map::const_iterator start_it) + : start_it(start_it) {} + void stop() { + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration(end - start_it->second).count(); + std::cerr << "Timing for " << start_it->first << ": " << duration << " ms" << std::endl; + } + + private: + std::map::const_iterator start_it; + }; + + entry start(const std::string& name) { + return entry(timings_.insert({name, std::chrono::high_resolution_clock::now()}).first); + } + + private: + std::map< + std::string, + std::chrono::high_resolution_clock::time_point> + timings_; +}; + +void arrange_cgal_polygons(const std::vector& input_polygons_, std::vector& output_polygons, double polygon_offset_distance = -1.) { + static const double OVERLAP_RESOLUTION_DISTANCE = 1.e-1; + // even larger amount of inset so that outer perimeter is safely within all input polygons even when overlap resolution is applied + // no, `1.e-2 + 1.e-5` creates issues with the outer perimeter, are there other tolerances in play? + static const double OUTER_PERIMITER_ADDITIONAL_INSET_AMOUNT = 1.e-5; + +#ifdef SVGFILL_DEBUG + auto t = std::time(nullptr); + auto tm = *std::localtime(&t); + + std::ostringstream oss; + oss << std::put_time(&tm, "arrangement_%Y%m%d%H%M%S"); + auto now = oss.str(); + DebugWriter debug_output(true, now); +#else + DebugWriter debug_output(false, ""); +#endif + + timer timer; + + auto t0 = timer.start("input"); + + debug_output.write_polygons(input_polygons_, "input"); + + if (polygon_offset_distance < 0.) { + polygon_offset_distance = estimate_polygon_offset_distance(input_polygons_); + } + + // Create copy to make mutable for cleaning + auto input_polygons = input_polygons_; + + for (auto& polygon : input_polygons) { + clean_polygon(polygon); + } + + { + decltype(input_polygons) split_polygons; + for (auto& poly : input_polygons) { + split_self_intersecting_polygon(poly, std::back_inserter(split_polygons)); + } + std::swap(input_polygons, split_polygons); + } + + t0.stop(); + t0 = timer.start("overlap elimination"); + + eliminate_overlaps(OVERLAP_RESOLUTION_DISTANCE, input_polygons); + + t0.stop(); + + // [NB Nov 6] we cannot do this anymore because it could revert the spacing between input polygons + // that touch in the corner. + // Now that overlaps/touches at corners are handled more locally only a small indent is produced + // which would be undone by means of an inset+offset. + // + // [NB Nov 10] this is actually still necessary though, but we apply a much smaller distance now + // to keep the overlap eliminations in tact + // + // Inset-offset to remove tiny details that may cause enourmous spikes in offsets + for (auto& r : input_polygons) { + smooth_polygon(polygon_offset_distance / 1000., r); + } + + debug_output.write_polygons(input_polygons, "processed_input"); + +#if 1 + t0 = timer.start("outer perimeter"); + + // Find the outer perimeter using offset - union - negative offset + std::vector offset_polygons; + for (auto& r : input_polygons) { + auto R = r; + if (!R.is_counterclockwise_oriented()) { + R.reverse_orientation(); + } + + // Overlap removal can also result in close points causing problems when converted into non-exact nt + remove_close_points(R); + + auto ps = create_and_convert_offset_polygon(polygon_offset_distance, R); + for (auto& p : ps) { + if (!p.is_simple()) { + throw std::runtime_error("Complex polygon originated from offset"); + } + } + offset_polygons.insert(offset_polygons.end(), ps.begin(), ps.end()); + } + + debug_output.write_polygons(offset_polygons, "offset_input"); + + // Perform Boolean union on the offset polygons + std::vector unioned_polygons; + CGAL::join(offset_polygons.begin(), offset_polygons.end(), std::back_inserter(unioned_polygons)); + + if (unioned_polygons.size() > 1) { + // @todo this is currently one of the major limitations in the code that still can be eliminated + // by grouping the input polygons by their perimiter polygon in unioned_polygons + std::sort(unioned_polygons.begin(), unioned_polygons.end(), [](auto& p, auto& q) { return p.outer_boundary().area() > q.outer_boundary().area(); }); + } + + debug_output.write_polygon(unioned_polygons.front().outer_boundary(), "offset_joined"); + + Polygon_2 fused_removed_close_points = unioned_polygons.front().outer_boundary(); + remove_close_points(fused_removed_close_points, 1.e-4); + + // Apply negative offset to get the outer perimeter polygon + auto outer_perimiter = create_and_convert_offset_polygon( + // Because polygon_offset is inexact, make sure our inset distance is slightly larger + // std::nexttoward(-polygon_offset_distance, -std::numeric_limits::infinity()), + + // 1.e-8 even was too little and still resulted in slivers of triangle around the perimeter + -polygon_offset_distance - OUTER_PERIMITER_ADDITIONAL_INSET_AMOUNT, + fused_removed_close_points); + + debug_output.write_polygons(outer_perimiter, "outer_perimiter"); +#else + std::map> neighbour_map; + build_radial_neighbour_map(input_polygons, polygon_offset_distance, neighbour_map); + + auto start_vertex = neighbour_map.rbegin()->first; + auto next_vertex = neighbour_map.rbegin()->second.front(); + + std::vector cycle = {start_vertex, next_vertex}; + while (cycle.back() != cycle.front()) { + const auto& incoming_from = *(cycle.rbegin() + 1); + const auto& nb = neighbour_map[cycle.back()]; + auto it = std::find(nb.begin(), nb.end(), incoming_from); + // cycle it -1 around nb + if (it == nb.begin()) { + it == nb.end() - 1; + } else { + --it; + } + cycle.push_back(*it); + } + std::vector outer_perimiter; + outer_perimiter.emplace_back(cycle.begin(), cycle.end()); +#endif + + t0.stop(); + t0 = timer.start("corridor creation"); + + // Subtract original polygons from outer perimeter + std::vector difference_result, difference_result_subdivided; + for (auto& i : outer_perimiter) { + std::vector working_copy; + working_copy.emplace_back(i); + + for (auto& r : input_polygons) { + std::vector temp_working_copy; + for (auto& wc : working_copy) { + CGAL::difference(wc, r, std::back_inserter(temp_working_copy)); + } + working_copy = temp_working_copy; + } + difference_result.insert(difference_result.end(), working_copy.begin(), working_copy.end()); + } + + t0.stop(); + t0 = timer.start("corridor triangulation"); + + // subdivide difference_result to have better more detailed triangulation and therefore less-pronounced artefacts in midpoint network + + for (auto& pwh : difference_result) { + difference_result_subdivided.push_back(subdivide_polygon(polygon_offset_distance / 8., pwh)); + // difference_result_subdivided.push_back(subdivide_polygon(polygon_offset_distance / 64., pwh)); + } + + debug_output.write_polygons(difference_result_subdivided, "corridor_subdivided"); + + std::vector> triangular_polygons; + for (auto& pwh : difference_result_subdivided) { + CGAL::Polygon_triangulation_decomposition_2 decompositor; + decompositor(pwh, std::back_inserter(triangular_polygons)); + } + + t0.stop(); + + /* + * // @todo decide whether this is smart or not + * // Would this not hurt topology too much? + triangular_polygons.erase(std::remove_if(triangular_polygons.begin(), triangular_polygons.end(), [](const CGAL::Polygon_2& p) { + return CGAL::to_double(p.area()) < 1.e-8; + }), triangular_polygons.end()); + */ + + t0 = timer.start("center line"); + + debug_output.write_polygons(triangular_polygons, "triangulated_corridor"); + + SegmentLookup segment_lookup(input_polygons); + + auto [line_graph, midpoint_to_segment, segment_to_input_facet] = build_line_graph(input_polygons, segment_lookup, triangular_polygons); + for (auto& p : line_graph) { + for (auto& q : p.second) { + debug_output.write_segment(p.first, q, "network_1"); + } + } + + t0.stop(); + + t0 = timer.start("center line cleaning"); + + auto triangles = find_triangles(line_graph); + + // For every triangle found in the network we eliminate one edge to break the cycle + // The edge we eliminate is the edge with the greatest angle with any of it's neighbours + auto eliminated_segments = eliminate_triangles(line_graph); + + Graph2D G2(line_graph); + for (auto& e : eliminated_segments) { + debug_output.write_segment(e.first, e.second, "eliminated"); + G2.remove_edge(e.first, e.second); + } + + auto G = G2.weld_vertices(); + + for (auto it = G.edges_begin(); it != G.edges_end(); ++it) { + debug_output.write_segment(it->first, it->second, "network_2"); + } + + eliminate_colinear_vertices(G); + + edge_slide(G); + + for (auto it = G.edges_begin(); it != G.edges_end(); ++it) { + debug_output.write_segment(it->first, it->second, "network_3"); + } + + t0.stop(); + + t0 = timer.start("topology"); + + auto segments = extend_end_vertices_based_on_input(G, midpoint_to_segment, segment_to_input_facet, outer_perimiter, segment_lookup); + + // Now plot the edges on an arrangement in order to find planar cycles + // and merge the corridor-halves with their neighbouring input polygon + Arrangement_2 arr; + G.to_arrangement(arr); + + for (auto& pq : segments) { + if (pq.first == pq.second) { + continue; + } + CGAL::insert(arr, Segment_2(pq.first, pq.second)); + + debug_output.write_segment(pq.first, pq.second, "extended_segments"); + } + +#if 0 + // Write input polygons to arrangement_2 + // We no longer do this because we add the outer perimiter now, subdivided by the corridor network which is extended and intersected with the outer perimiter + for (auto& poly : input_polygons) { + for (size_t i = 0; i != poly.size(); ++i) { + auto j = (i + 1) % poly.size(); + if (poly.vertex(i) == poly.vertex(j)) { + continue; + } + CGAL::insert(arr, Segment_2(poly.vertex(i), poly.vertex(j))); + } + } +#else + // Write outer perimeter to arrangement_2 + for (auto& p : outer_perimiter) { + for (auto it = p.edges_begin(); it != p.edges_end(); ++it) { + auto source = it->source(); + auto target = it->target(); + if (source == target) { + continue; + } + CGAL::insert(arr, Segment_2(source, target)); + } + } +#endif + + // Just for the automatic numbering, create a full vector + std::vector temp; + for (auto it = arr.faces_begin(); it != arr.faces_end(); ++it) { + if (it->is_unbounded()) { + continue; + } + temp.push_back(circ_to_poly(it->outer_ccb())); + } + debug_output.write_polygons(temp, "arr_faces"); + + + /* { + // debug, add outer bounds so that we can plot the face for any remaining edges + auto poly = unioned_polygons.front().outer_boundary(); + for (size_t i = 0; i != poly.size(); ++i) { + auto j = (i + 1) % poly.size(); + CGAL::insert(arr, Segment_2(poly.vertex(i), poly.vertex(j))); + } + } */ + + // Now loop over the arrangement faces, when a face coincides with a point on the + // corridor network we know it needs to be joined with an input polygon. In that + // case the edges need to be eliminated that correspond to original geometry. + +#if 0 + fuse_corridor_halves_with_input(arr, G, segment_lookup, input_polygons, debug_output); +#else + remove_colinear_vertices(arr); + clean_noisy_paths(arr, segment_lookup); +#endif + + t0.stop(); for (auto it = arr.faces_begin(); it != arr.faces_end(); ++it) { if (it->is_unbounded()) { continue; } - output_polygons.push_back(circ_to_poly(it->outer_ccb())); - -#ifdef SVGFILL_DEBUG - write_polygon_to_svg(svg, circ_to_poly(it->outer_ccb())); - - obj << "o " << "merged_face_"; - obj << face_id++ << "\n"; - - std::ostringstream oss; - - { - auto vv = vi; - auto curr = it->outer_ccb(); - do { - auto& p = curr->source()->point(); - obj << "v " << CGAL::to_double(p.x()) << " " << CGAL::to_double(p.y()) << " 0\n"; - oss << "l " << vi++; - ++curr; - if (curr == it->outer_ccb()) { - oss << " " << vv << "\n"; - } else { - oss << " " << vi << "\n"; - } - } while (curr != it->outer_ccb()); - } - - for (auto jt = it->inner_ccbs_begin(); jt != it->inner_ccbs_end(); ++jt) { - auto vv = vi; - auto curr = *jt; - do { - auto& p = curr->source()->point(); - obj << "v " << CGAL::to_double(p.x()) << " " << CGAL::to_double(p.y()) << " 0\n"; - oss << "l " << vi++; - ++curr; - if (curr == *jt) { - oss << " " << vv << "\n"; - } else { - oss << " " << vi << "\n"; - } - } while (curr != *jt); - } - - obj << oss.str(); -#endif } -#ifdef SVGFILL_DEBUG - svg << "\n"; -#endif + debug_output.write_polygons(output_polygons, "arr_faces_merged"); } #ifndef SVGFILL_MAIN diff --git a/src/svgfill/src/graph_2d.h b/src/svgfill/src/graph_2d.h index aaaa059b4e..da2b5014ec 100644 --- a/src/svgfill/src/graph_2d.h +++ b/src/svgfill/src/graph_2d.h @@ -2,8 +2,10 @@ #define GRAPH_2D_H #ifdef SVGFILL_DEBUG +#if 0 #include #endif +#endif template class Graph2D { @@ -334,6 +336,16 @@ public: return Graph2D(input_adjacency_list); } + template + void to_arrangement(T& arr) { + for (auto it = edges_begin(); it != edges_end(); ++it) { + if (it->first == it->second) { + continue; + } + CGAL::insert(arr, CGAL::Segment_2(it->first, it->second)); + } + } + void assert_symmetric() { #ifdef SVGFILL_DEBUG #if 0 diff --git a/win/build-all-win.py b/win/build-all-win.py index 087f869ac4..e1bd5cccdf 100644 --- a/win/build-all-win.py +++ b/win/build-all-win.py @@ -6,6 +6,7 @@ but also archives them to '~/outputs'. import os import subprocess +import zipfile from pathlib import Path from zipfile import ZipFile @@ -27,7 +28,23 @@ ZIP_TEMPLATE = f"{{package_name}}-v{VERSION}-{SHA}-win64.zip" def run(command: list[str]) -> None: print("Running:", command) - subprocess.check_call(command) # nosec B603 + subprocess.check_call(command) + + +def set_env(var_name: str, value: str) -> tuple[str, str | None]: + """ + :return: Tuple of ``(var_name, old_value)`` to be passed to ``restore_env``. + """ + old_value = os.getenv(var_name) + os.environ[var_name] = value + return var_name, old_value + + +def restore_env(var_name: str, old_value: str | None) -> None: + if old_value is None: + del os.environ[var_name] + else: + os.environ[var_name] = old_value def build() -> None: @@ -40,16 +57,16 @@ def build() -> None: text=True, input="y\n", ) + OLD_ADD_COMMIT_SHA = set_env("ADD_COMMIT_SHA", "ON") run( [ str(REPO_WIN / "run-cmake.bat"), "vs2022-x64", "-DENABLE_BUILD_OPTIMIZATIONS=ON", "-DGLTF_SUPPORT=ON", - "-DADD_COMMIT_SHA=ON", - "-DVERSION_OVERRIDE=ON", ] ) + restore_env(*OLD_ADD_COMMIT_SHA) run([str(REPO_WIN / "install-ifcopenshell.bat"), "vs2022-x64", "Release"]) @@ -61,7 +78,7 @@ def archive_executables() -> None: if file.suffix.lower() != ".exe": continue zip_name = ZIP_TEMPLATE.format(package_name=file.stem) - with ZipFile(OUTPUT_DIR / zip_name, "w") as zipf: + with ZipFile(OUTPUT_DIR / zip_name, "w", compression=zipfile.ZIP_DEFLATED) as zipf: zipf.write(file, arcname=file.name) print(f"{file} -> {zip_name}") @@ -76,7 +93,7 @@ def archive_python_package(python_version: str, python_path: Path) -> None: file.unlink() zip_name = ZIP_TEMPLATE.format(package_name=f"ifcopenshell-python-{python_version_major_minor}") - with ZipFile(OUTPUT_DIR / zip_name, "w") as zipf: + with ZipFile(OUTPUT_DIR / zip_name, "w", compression=zipfile.ZIP_DEFLATED) as zipf: for file in package_path.rglob("*"): arcname = file.relative_to(site_packages) zipf.write(file, arcname=arcname) diff --git a/win/build-deps.cmd b/win/build-deps.cmd index c81541104f..811f381f19 100644 --- a/win/build-deps.cmd +++ b/win/build-deps.cmd @@ -249,8 +249,9 @@ IF NOT %ERRORLEVEL%==0 GOTO :Error :proj -IF EXIST "%INSTALL_DIR%\proj-9.2.1" ( - echo Found existing "%INSTALL_DIR%\proj-9.2.1", skipping +set PROJ_VERSION=9.4.1 +IF EXIST "%INSTALL_DIR%\proj-%PROJ_VERSION%" ( + echo Found existing "%INSTALL_DIR%\proj-%PROJ_VERSION%", skipping goto :mpir ) @@ -269,13 +270,13 @@ copy sqlite3.h %INSTALL_DIR%\sqlite3\include popd set DEPENDENCY_NAME=proj -set DEPENDENCY_DIR=%DEPS_DIR%\proj-9.2.1 -call :DownloadFile https://download.osgeo.org/proj/proj-9.2.1.zip "%DEPS_DIR%" proj-9.2.1.zip +set DEPENDENCY_DIR=%DEPS_DIR%\proj-%PROJ_VERSION% +call :DownloadFile https://download.osgeo.org/proj/proj-%PROJ_VERSION%.zip "%DEPS_DIR%" proj-%PROJ_VERSION%.zip IF NOT %ERRORLEVEL%==0 GOTO :Error -call :ExtractArchive proj-9.2.1.zip "%DEPS_DIR%" "%DEPS_DIR%\proj-9.2.1" +call :ExtractArchive proj-%PROJ_VERSION%.zip "%DEPS_DIR%" "%DEPS_DIR%\proj-%PROJ_VERSION%" IF NOT %ERRORLEVEL%==0 GOTO :Error pushd "%DEPENDENCY_DIR%" -call :RunCMake -DCMAKE_INSTALL_PREFIX="%INSTALL_DIR%\proj-9.2.1" ^ +call :RunCMake -DCMAKE_INSTALL_PREFIX="%INSTALL_DIR%\proj-%PROJ_VERSION%" ^ -DSQLITE3_INCLUDE_DIR=%INSTALL_DIR%\sqlite3\include ^ -DSQLITE3_LIBRARY=%INSTALL_DIR%\sqlite3\lib\sqlite3.lib ^ -DENABLE_TIFF=Off -DENABLE_CURL=Off -DBUILD_PROJSYNC=Off ^ @@ -297,8 +298,9 @@ IF EXIST "%INSTALL_DIR%\mpir" ( ) set DEPENDENCY_NAME=mpir +:: `mpfr` depends on relative path `..\mpir\config.h`, so dependency name should match exactly. set DEPENDENCY_DIR=%DEPS_DIR%\mpir -call :GitCloneAndCheckoutRevision https://github.com/BrianGladman/mpir.git "%DEPENDENCY_DIR%" +call :GitCloneAndCheckoutRevision https://github.com/Andrej730/mpir-vs2026.git "%DEPENDENCY_DIR%" IF NOT %ERRORLEVEL%==0 GOTO :Error pushd "%DEPENDENCY_DIR%" git reset --hard @@ -412,6 +414,11 @@ if exist "%DEPS_DIR%\boost-%BOOST_VERSION%". ( ren %DEPS_DIR%\boost-%BOOST_VERSION% boost_%BOOST_VER% ) +:: As boost 1.90.0 it still includes b2 that doesn't support vc145 (not to mention older boost versions). +:: So to support vc145 we download b2 separately (only if we do use vc145). +call :check_boost_vc145_compatibility "%VC_VER%" "%DEPS_DIR%" "%DEPENDENCY_DIR%" +if NOT %ERRORLEVEL%==0 GOTO :Error + :: Build Boost build script if not exist "%DEPENDENCY_DIR%\project-config.jam". ( cd "%DEPS_DIR%" @@ -462,8 +469,10 @@ IF NOT %ERRORLEVEL%==0 git apply --reject --whitespace=fix "%~dp0patches\OpenCOL :: uncomment to following line in order to delete the CMakeCache.txt always if experiencing problems. REM IF EXIST "%DEPENDENCY_DIR%\%BUILD_DIR%\CMakeCache.txt". del "%DEPENDENCY_DIR%\%BUILD_DIR%\CMakeCache.txt" :: NOTE Enforce that the embedded LibXml2 and PCRE are used as there might be problems with arbitrary versions of the libraries. +:: OpenCOLLADA is ancient at this point and allows cmake 2.6+, which results in error in cmake 4, so we override minimum cmake version. call :RunCMake -DCMAKE_INSTALL_PREFIX="%INSTALL_DIR%\%DEPENDENCY_INSTALL_NAME%" -DUSE_STATIC_MSVC_RUNTIME=0 -DCMAKE_DEBUG_POSTFIX=d ^ - -DLIBXML2_LIBRARIES="" -DLIBXML2_INCLUDE_DIR="" -DPCRE_INCLUDE_DIR="" -DPCRE_LIBRARIES="" + -DLIBXML2_LIBRARIES="" -DLIBXML2_INCLUDE_DIR="" -DPCRE_INCLUDE_DIR="" -DPCRE_LIBRARIES="" ^ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 IF NOT %ERRORLEVEL%==0 GOTO :Error REM IF NOT EXIST "%DEPS_DIR%\OpenCOLLADA\%BUILD_DIR%\lib\%DEBUG_OR_RELEASE%\OpenCOLLADASaxFrameworkLoader.lib". call :BuildCMakeProject "%DEPENDENCY_DIR%\%BUILD_DIR%" %DEBUG_OR_RELEASE% @@ -509,9 +518,18 @@ cd "%DEPENDENCY_DIR%" :: TODO: remove CMAKE_DEBUG_POSTFIX setting later. :: Temporarily explicitly set `CMAKE_DEBUG_POSTFIX` to empty to override it's perviously being set to `d`. :: OCCT don't need it, since it's layout is separating debug and release build by different folders. +:: +:: OCCT 7.8.1 we're using is becoming old and it was targeting cmake 3.1+. +::To make it buildable on cmake 4, we override policy version, but it may have some quirks in the future and we may consider version bump. call :RunCMake -DINSTALL_DIR="%DEPENDENCY_INSTALL_DIR%" -DBUILD_LIBRARY_TYPE="Static" -DCMAKE_DEBUG_POSTFIX="" ^ - -DBUILD_MODULE_Draw=0 -DUSE_FREETYPE=OFF ^ - -DBUILD_USE_PCH=ON + -DBUILD_MODULE_Draw=0 ^ + -DBUILD_RELEASE_DISABLE_EXCEPTIONS=OFF ^ + -DUSE_XLIB=OFF ^ + -DUSE_FREETYPE=OFF ^ + -DUSE_OPENGL=OFF ^ + -DUSE_GLES2=OFF ^ + -DBUILD_USE_PCH=ON ^ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 if not %ERRORLEVEL%==0 goto :Error :: whole program optimization avoids Visual C++ hanging when compiling 32-bit release OCCT up to version 7.4.0 @@ -572,7 +590,7 @@ IF NOT %ERRORLEVEL%==0 GOTO :Error :SWIG set DEPENDENCY_NAME=SWIG -set SWIG_VERSION=4.1.0 +set SWIG_VERSION=4.2.1 set DEPENDENCY_DIR=%DEPS_DIR%\swig-%SWIG_VERSION% set DEPENDENCY_INSTALL_DIR=%INSTALL_DIR%\swig-%SWIG_VERSION% echo SWIG_INSTALL_DIR=%DEPENDENCY_INSTALL_DIR%>>"%~dp0\%BUILD_DEPS_CACHE_PATH%" @@ -917,6 +935,15 @@ exit /b 0 IF NOT %ERRORLEVEL%==0 GOTO :Error exit /b 0 +:: Params: +:: - %1 - VC_VER +:: - %2 - DEPS_DIR +:: - %3 - BOOST_ROOT +:check_boost_vc145_compatibility +%PWSH_TOOLS% check_boost_vc145_compatibility "%1" "%2" "%3" +IF NOT %ERRORLEVEL%==0 GOTO :Error +exit /b 0 + :: PrintUsage - Prints usage information :PrintUsage call "%~dp0\utils\cecho.cmd" 0 10 "Requirements for a successful execution:" diff --git a/win/patches/V7_8_1.patch b/win/patches/V7_8_1.patch index 842a52bad6..9f185521c2 100644 --- a/win/patches/V7_8_1.patch +++ b/win/patches/V7_8_1.patch @@ -84,3 +84,16 @@ index c9399159f1..0aa55392f9 100644 endif() if (BUILD_SHARED_LIBS AND NOT "${BUILD_SHARED_LIBRARY_NAME_POSTFIX}" STREQUAL "") +diff --git a/adm/cmake/cotire.cmake b/adm/cmake/cotire.cmake +index acdca71a9f..6c6e29b374 100644 +--- a/adm/cmake/cotire.cmake ++++ b/adm/cmake/cotire.cmake +@@ -37,7 +37,7 @@ set(__COTIRE_INCLUDED TRUE) + if (NOT CMAKE_SCRIPT_MODE_FILE) + cmake_policy(PUSH) + endif() +-cmake_minimum_required(VERSION 2.8.12) ++cmake_minimum_required(VERSION 3.5) + if (NOT CMAKE_SCRIPT_MODE_FILE) + cmake_policy(POP) + endif() diff --git a/win/run-cmake.bat b/win/run-cmake.bat index 397887eb6c..2a49a65042 100755 --- a/win/run-cmake.bat +++ b/win/run-cmake.bat @@ -20,6 +20,10 @@ :: Example usage: :: run-cmake.bat vs2022-x64 :: run-cmake.bat vs2022-x64 -DGLTF_SUPPORT=ON -DHDF5_SUPPORT=OFF +:: +:: Used environment variables: +:: - `ADD_COMMIT_SHA` - if defined then `ADD_COMMIT_SHA` and `VERSION_OVERRIDE` cmake args will be set to `ON`. +:: - `USE_NINJA` - if defined then the Ninja generator will be used instead of the Visual Studio. @if not defined ECHO_ON ( echo off ) @@ -105,7 +109,13 @@ set PYTHON_LIBRARY=%PYTHONHOME%\libs\python%PY_VER_MAJOR_MINOR%.lib :: we can remove it later. if not defined SWIG_INSTALL_DIR set SWIG_INSTALL_DIR=%INSTALL_DIR%\swigwin set JSON_INCLUDE_DIR=%INSTALL_DIR%\json -if not defined ADD_COMMIT_SHA set ADD_COMMIT_SHA=Off +if defined ADD_COMMIT_SHA ( + set ADD_COMMIT_SHA=ON + set VERSION_OVERRIDE=ON +) else ( + set ADD_COMMIT_SHA=OFF + set VERSION_OVERRIDE=OFF +) set CGAL_INSTALL_DIR=%INSTALL_DIR%\cgal set GMP_INSTALL_DIR=%INSTALL_DIR%\mpir @@ -180,19 +190,16 @@ if defined USE_NINJA ( ) IF NOT "%VS_TOOLSET_HOST%"=="" ( - cmake.exe %CMAKELISTS_DIR% -G %GENERATOR% %ARCH_OPTION% -T %VS_TOOLSET_HOST% ^ - -DCMAKE_INSTALL_PREFIX="%CMAKE_INSTALL_PREFIX%" ^ - -DWITH_ROCKSDB=On -DWITH_ZSTD=On ^ - -DCMAKE_PREFIX_PATH="%CMAKE_PREFIX_PATH%" ^ - -DADD_COMMIT_SHA=%ADD_COMMIT_SHA% %ARGUMENTS% -) ELSE ( - cmake.exe %CMAKELISTS_DIR% -G %GENERATOR% %ARCH_OPTION% ^ - -DCMAKE_INSTALL_PREFIX="%CMAKE_INSTALL_PREFIX%" ^ - -DWITH_ROCKSDB=On -DWITH_ZSTD=On ^ - -DCMAKE_PREFIX_PATH="%CMAKE_PREFIX_PATH%" ^ - -DADD_COMMIT_SHA=%ADD_COMMIT_SHA% %ARGUMENTS% + set VS_TOOLSET_OPTION=-T %VS_TOOLSET_HOST% ) +cmake.exe %CMAKELISTS_DIR% -G %GENERATOR% %ARCH_OPTION% %VS_TOOLSET_OPTION% ^ + -DCMAKE_INSTALL_PREFIX="%CMAKE_INSTALL_PREFIX%" ^ + -DWITH_ROCKSDB=On -DWITH_ZSTD=On ^ + -DCMAKE_PREFIX_PATH="%CMAKE_PREFIX_PATH%" ^ + -DADD_COMMIT_SHA=%ADD_COMMIT_SHA% -DVERSION_OVERRIDE=%VERSION_OVERRIDE% ^ + %ARGUMENTS% + IF NOT %ERRORLEVEL%==0 GOTO :Error echo. diff --git a/win/utils/tools.ps1 b/win/utils/tools.ps1 index c143581789..583e4cfb3b 100644 --- a/win/utils/tools.ps1 +++ b/win/utils/tools.ps1 @@ -2,6 +2,8 @@ Set-PSDebug -Trace 0 Set-StrictMode -Version 3 $ErrorActionPreference = "Stop" +$cecho = "$PSScriptRoot\cecho.cmd" + # Create marker file to indicate whether Release or Debug build was installed. function mark { @@ -18,7 +20,7 @@ function mark { if (Test-Path -Path $marker_filepath) { return } - cecho.cmd 0 13 "Marking installation in '$installation_dir' with '$ENV:MARKER_FILE'." + . $cecho 0 13 "Marking installation in '$installation_dir' with '$ENV:MARKER_FILE'." New-Item -Path $marker_filepath -ItemType File | Out-Null } @@ -76,7 +78,7 @@ function mark_based_on_artifacts { if (Test-Path -Path $marker_filepath) { return } - cecho.cmd 0 13 "Found artifact '$artifact' for dependency '$dependency_name' $env:BUILD_CFG." + . $cecho 0 13 "Found artifact '$artifact' for dependency '$dependency_name' $env:BUILD_CFG." & mark $installation_dir } @@ -138,12 +140,11 @@ function extract_file { [string]$dir_after_extraction ) if (Test-Path -Path "$dir_after_extraction") { - cecho.cmd 0 13 "$dependency_name already extracted into '$dir_after_extraction'. Skipping." - exit 0 + . $cecho 0 13 "$dependency_name already extracted into '$dir_after_extraction'. Skipping." + return } - cecho.cmd 0 13 "Extracting $dependency_name into '$destination_dir' from '$filename'." + . $cecho 0 13 "Extracting $dependency_name into '$destination_dir' from '$filename'." 7za x "$filename" -o"$destination_dir" - exit 0 } @@ -161,13 +162,12 @@ function download_file { mkdir "$destination_dir" -Force | Out-Null pushd "$destination_dir" if (Test-Path -Path "$filename") { - cecho.cmd 0 13 "$dependency_name already downloaded. Skipping." - exit 0 + . $cecho 0 13 "$dependency_name already downloaded. Skipping." + return } - cecho.cmd 0 13 "Downloading $dependency_name into '$destination_dir'" + . $cecho 0 13 "Downloading $dependency_name into '$destination_dir'" Invoke-WebRequest $url -OutFile $filename - exit 0 } @@ -185,21 +185,20 @@ function git_clone_and_checkout_revision { [string]$revision ) if (Test-Path -Path "$dest_dir") { - cecho.cmd 0 13 "Cloning $dependency_name is already cloned." - exit 0 + . $cecho 0 13 "Cloning $dependency_name is already cloned." + return } - cecho.cmd 0 13 "Cloning $dependency_name into '$dest_dir'." + . $cecho 0 13 "Cloning $dependency_name into '$dest_dir'." pushd "$env:DEPS_DIR" git clone $git_url $dest_dir popd pushd "$dest_dir" git fetch - cecho.cmd 0 13 "Checking out $dependency_name revision $revision." + . $cecho 0 13 "Checking out $dependency_name revision $revision." git reset --hard git checkout $revision popd - exit 0 } function install_cmake_project { @@ -212,20 +211,61 @@ function install_cmake_project { [string]$configuration ) pushd "$build_dir" - cecho.cmd 0 13 "Installing $dependency_name ($configuration). Please be patient, this may take a while." + . $cecho 0 13 "Installing $dependency_name ($configuration). Please be patient, this may take a while." $command = "cmake --install . --config $configuration" - cecho.cmd 0 13 "$command" + . $cecho 0 13 "$command" Invoke-Expression $command popd - exit 0 } +function check_boost_vc145_compatibility { + param( + [Parameter(Mandatory = $true)] + [string]$VC_VER, + [Parameter(Mandatory = $true)] + [string]$DEPS_DIR, + [Parameter(Mandatory = $true)] + [string]$BOOST_ROOT + ) + + $boost_build_path = "$BOOST_ROOT/tools/build" + + if ($VC_VER -ne "14.5") { + . $cecho 0 13 "VC_VER is not 14.5, no need to install updated b2." + return + } + + $res = Select-String -Path "$boost_build_path/src/engine/build.bat" -Pattern 'vc143, vc145' -Quiet; + if ($res) { + . $cecho 0 13 "vc145 already supported, no need to install updated b2." + return + } + + $b2_version = "5.4.2" + $b2_stem = "b2-$b2_version" + $b2_path = "$DEPS_DIR\$b2_stem" + $b2_filename = "$b2_stem.zip" + + & download_file "b2" "https://github.com/bfgroup/b2/releases/download/$b2_version/$b2_filename" "$DEPS_DIR" "$b2_filename" + & extract_file "b2" "$b2_filename" "$DEPS_DIR" "$b2_path" + + . $cecho 0 13 "Installing b2 with vc145 support..." + Remove-Item -Recurse -Path "$boost_build_path" + Copy-Item -Path "$b2_path" -Destination "$boost_build_path" -Recurse + . $cecho 0 13 "b2 with vc145 support installed." +} + function main { & setup_build_cfg # Dispatch command. $command = $Args[0] - $command_args = $Args[1..($args.Count - 1)] + if ($args.Count -gt 1) { + $command_args = $Args[1..($args.Count - 1)] + } + else { + $command_args = @() + } & $command @command_args } diff --git a/win/vs-cfg.cmd b/win/vs-cfg.cmd index 794291792b..d89f3fa88b 100644 --- a/win/vs-cfg.cmd +++ b/win/vs-cfg.cmd @@ -35,6 +35,11 @@ :: "vs2019-x86-v141_xp" => cmake -G "Visual Studio 16 2019" -A Win32 -T v141_xp :: :: NOTE: The delayed environment variable expansion needs to be enabled before calling this. +:: +:: Output variables: +:: - VC_VER - e.g. "14.5" +:: - VS_VER - e.g. "2026" +:: - BOOST_BOOTSTRAP_VER - e.g. "vc145" @if not defined ECHO_ON ( echo off ) @@ -46,7 +51,8 @@ set GENERATORS[2]="Visual Studio 14 2015" set GENERATORS[3]="Visual Studio 15 2017" set GENERATORS[4]="Visual Studio 16 2019" set GENERATORS[5]="Visual Studio 17 2022" -set LAST_GENERATOR_IDX=5 +set GENERATORS[6]="Visual Studio 18 2026" +set LAST_GENERATOR_IDX=6 :: Is generator shorthand used? set GEN_SHORTHAND=!GENERATOR:vs=! @@ -104,6 +110,9 @@ IF "!GENERATOR!"=="" IF NOT "%VisualStudioVersion%"=="" ( GOTO :GeneratorValid ) ) + call utils\cecho.cmd 0 12 ^ + "Generator is not provided and VisualStudioVersion='%VisualStudioVersion%' is not supported - cannot proceed." + exit /b 1 ) :: Check that the used CMake version supports the chosen generator @@ -157,6 +166,7 @@ IF %VS_VER%==2015 ( set "VC_VER=14.0" ) IF %VS_VER%==2017 ( set "VC_VER=14.1" ) IF %VS_VER%==2019 ( set "VC_VER=14.2" ) IF %VS_VER%==2022 ( set "VC_VER=14.3" ) +IF %VS_VER%==2026 ( set "VC_VER=14.5" ) :: determine the argument for Boost bootstrap set BOOST_BOOTSTRAP_VER=vc%VC_VER%