Compare commits

...

84 Commits

Author SHA1 Message Date
Bruno Postle 07b53f38b4 regenerate_wall_representation: document BBIM_Boolean preservation requirement 2026-03-23 22:32:49 +00:00
Bruno Postle e80fd56d3f Doc clarification for api.geometry.add_wall_representation clippings normal 2026-03-23 22:32:27 +00:00
Bruno Postle 0f0ced593b Doc clarification for api.feature.remove_feature
Generated with the assistance of an AI coding tool.
2026-03-23 22:32:09 +00:00
Bruno Postle 05bd7df68f Doc clarification for api.geometry.edit_object_placement
Generated with the assistance of an AI coding tool.
2026-03-23 22:32:08 +00:00
Bruno Postle ee4681dc4c Doc clarification for api.sequence.assign_process
Generated with the assistance of an AI coding tool.
2026-03-23 22:32:08 +00:00
Sayan J. Das f679c63a18 Merge pull request #7808 from theseyan/ifctester-improvements-rebased
IfcTester webapp improvements
2026-03-23 15:48:34 +05:30
Thomas Krijnen e6cc0e7813 Initialize ncount_total #7834 2026-03-23 10:54:38 +01:00
Dion Moult cf2acfc649 Add covering feature tests for ceiling and cursor variants
Add tests for all four covering generation operators: flooring/ceiling
from walls and flooring/ceiling from cursor. Previously only flooring
from walls was tested.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 23:25:04 +11:00
Dion Moult 60ebb99fda Fix covering geometry not persisting to IFC, same root cause as #7055
The covering tool used bmesh as an intermediate and relied on
type.assign_type post-listeners (removed in 44a52863a) to generate
the IfcExtrudedAreaSolid body. With those listeners gone, coverings
had no body representation and assign_swept_area_outer_curve crashed.

Build covering representations from scratch using ShapeBuilder, reading
the extrusion depth from the type's IfcMaterialLayerSet. Also replace
bpy.ops.bim.assign_class with bonsai.core.root.assign_class using
should_add_representation=False, consistent with the space fix.

Refactored shared coordinate-conversion and extrusion-building logic
into get_2d_vertices_from_polygon and set_extrusion_representation_from_polygon,
used by both space and covering code paths. Removed all bmesh-dependent
dead code from the spatial tool.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 23:18:59 +11:00
Dion Moult ab96add772 Gitignore all test cache files 2026-03-22 22:26:23 +11:00
Dion Moult d8de623086 Fix space regen not saving geometry to IFC (#7055)
Space regeneration was only updating the Blender mesh and marking the
object as edited, but the IFC representation was never synced on save.
Replace the bmesh-based approach with ShapeBuilder to write geometry
directly to IFC as an IfcExtrudedAreaSolid, then reload via
switch_representation. This applies to both new space creation and
existing space regeneration.

Also changes assign_ifcspace_class_to_obj to call
bonsai.core.root.assign_class directly with
should_add_representation=False instead of bpy.ops.bim.assign_class.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 22:14:02 +11:00
dependabot[bot] a3f2e061fb Bump hendrikmuhs/ccache-action from 1.2.20 to 1.2.21
Bumps [hendrikmuhs/ccache-action](https://github.com/hendrikmuhs/ccache-action) from 1.2.20 to 1.2.21.
- [Release notes](https://github.com/hendrikmuhs/ccache-action/releases)
- [Commits](https://github.com/hendrikmuhs/ccache-action/compare/v1.2.20...v1.2.21)

---
updated-dependencies:
- dependency-name: hendrikmuhs/ccache-action
  dependency-version: 1.2.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-22 21:31:54 +11:00
dependabot[bot] f51a4673db Bump ruff from 0.15.6 to 0.15.7
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.6 to 0.15.7.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.6...0.15.7)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-22 21:31:42 +11:00
Dion Moult 75b8d4f218 Remove spatial containment and aggregation when nesting
The nest assign_object API now removes existing spatial containment and
aggregate relationships before creating the nest, matching the behavior
documented in its docstring and consistent with aggregate.assign_object.

Fix #7248

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 15:28:47 +11:00
Dion Moult b8136d4762 Prevent cyclic references when assigning nesting or aggregation
Walk up the full hierarchy via get_parent() in can_nest() and
can_aggregate() to reject assignments that would create a cycle.
Also reject self-assignment.

Fix #7248

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 15:28:47 +11:00
Dion Moult ecde429d36 Fix crash after undo of assign_class on macOS (#7419)
After assigning an IFC class and undoing, msgbus subscriptions registered
with the old Python object wrapper survived (PERSISTENT flag) but could
not be cleared because: (1) rollback_link_element looked up objects by
their post-link name which no longer exists after undo, and (2) the
per-object clear_by_owner calls in rebuild_element_maps used new Python
wrappers that didn't match the old subscription owners.

Fix by using a dedicated stable object (object_subscription_owner) as
the msgbus owner for all per-object subscriptions, allowing
rebuild_element_maps to clear all stale subscriptions in one call
regardless of Python wrapper identity changes during undo/redo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 14:42:55 +11:00
Dion Moult 1771b34449 Fix error when entering edit mode on camera objects
Fixes #7313.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 14:08:12 +11:00
Dion Moult 41469acbc8 Fix walrus operator precedence in MaterialCreator
The `is not ...` was being captured by the walrus assignment due to
missing parentheses, causing the condition to always evaluate incorrectly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 13:58:17 +11:00
Ryan Schultz 547b22199f Without 'Material.Name' layers merge. (#7700) 2026-03-21 18:02:02 -05:00
Dion Moult 94c15213f6 Guard against emptying IfcShapeRepresentation Items
remove_representation_item now returns early if removing the item would
leave Items empty. edit_text_literals returns early on empty attributes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 20:10:11 +11:00
Dion Moult fca258fb07 Fix add_boolean removing second operands from unrelated representations
add_boolean was removing second operands from ALL IfcShapeRepresentations
that referenced them, which could corrupt unrelated shapes and leave
representations with empty Items (bug #7803).

The API no longer modifies Items — callers manage this explicitly.
validate_type and Bonsai's AddBoolean operator now handle their own
item removal scoped to the correct representation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 20:10:11 +11:00
Dion Moult bcfad8d96d Migrate remove_deep to remove_deep2 across API modules
remove_deep is deprecated and can silently delete elements still in use.
remove_deep2 requires zero inverses before removal, making it safer.
Also fixes a double-removal bug in remove_grid_axis and prevents
removing the last prop template from a pset template.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 20:10:11 +11:00
Parag Debnath c026dd3b6e IsVentilated now defaults to False (#7819)
* IsVantillated now defaults to false

* IsVentilated now defaults to False

---------

Co-authored-by: Parag Debnath <paragforwork@gmail.com>
2026-03-20 23:33:40 +11:00
Dion Moult d0f20371bd Add feature to get parent of a particular IFC class 2026-03-20 23:10:00 +11:00
Dion Moult 7b6e82a9cc Fix stair calculated params test to set custom_tread_lock=False
Tests using custom first/last tread runs were not setting
custom_tread_lock=False, so the custom values were silently ignored
since 8f7cf76d9 introduced the lock gate in the calculation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 23:09:15 +11:00
Andrej730 286c69429d gitignore bonsai external_dependencies 2026-03-20 15:49:08 +05:00
dependabot[bot] 9c22dc6013 Bump socket.io-parser from 4.2.4 to 4.2.6 in /src/ifctester/webapp
Bumps [socket.io-parser](https://github.com/socketio/socket.io) from 4.2.4 to 4.2.6.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/socket.io-parser@4.2.4...socket.io-parser@4.2.6)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-version: 4.2.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 11:47:01 +01:00
Andrej730 0acbd5ffad black . 2026-03-20 15:45:24 +05:00
Andrej730 77f0c43314 ids_doc_generator - fix invalid escape sequence SyntaxWarning
SyntaxWarning: invalid escape sequence '\/' at line 312.
`\/` in a plain string is treated as `/` by accident; replaced with raw string r"..." to be explicit.
2026-03-20 15:43:13 +05:00
Andrej730 c1a9708504 ci.yml - build ifctester docs
to ensure script doesn't break
2026-03-20 15:43:13 +05:00
Andrej730 bcf6f6197d ifctester - move build-ids-docs target to ifctester Makefile
Also added a note why it lives in test folder and added it's output to gitignore.
2026-03-20 15:43:13 +05:00
Andrej730 1778656bd5 ids_doc_generator - fix Property args missed (ed8eb75)
TypeError: Property.__init__() got an unexpected keyword argument 'name'
2026-03-20 15:43:12 +05:00
Andrej730 54f450129c ids_doc_generator - fix failed_entities removed (bd92c043)
AttributeError: 'Attribute' object has no attribute 'failed_entities'
2026-03-20 15:43:12 +05:00
Andrej730 9fad1569c7 ids_doc_generator - fix error due to stale cache (f40281e97)
AssertionError: bool(facet(inst)) is expected
2026-03-20 15:43:12 +05:00
Andrej730 722c374fa6 ids_doc_generator - handle invalid entities coming from a test (1ed770d)
Exception: About to emit invalid example data: IfcMaterial.Name not optional
2026-03-20 15:43:12 +05:00
Andrej730 91b6c3e256 bcf v3 tests - fix wrong args, add dead code TODOs 2026-03-20 15:43:12 +05:00
Andrej730 ea3f71b4e0 rename test files to test_* prefix for pytest discovery and fix missing add_pset name arg 2026-03-20 15:43:12 +05:00
Andrej730 1a8b17e235 ifcfm cobie24 - remove unused ifc_file param from get_unit_name 2026-03-20 15:43:12 +05:00
Andrej730 2bad861122 ifcopenshell_wrapper.pyi - support varargs and kwargs in constructors 2026-03-20 15:43:11 +05:00
Andrej730 6c1fb3b01a Remove stale mass_time_units_in_wizard references (5c31ae4c3) 2026-03-20 15:36:18 +05:00
Andrej730 f5be64af6c Remove redundant __init__ from BaseLinesShader 2026-03-20 15:36:18 +05:00
Andrej730 b043dd4d04 Fix unknown-argument error in BaseLinesShader.__init__ 2026-03-20 15:36:18 +05:00
Andrej730 ffd2466321 Fix missing prop name in bim.mep_add_bend 2026-03-20 15:36:17 +05:00
Andrej730 e9241fd812 Fix error in bim.fit_flow_segments 2026-03-20 15:36:17 +05:00
Andrej730 48d45451ac Remove dead code join_walls_TZ, join_T, join_Z superseded in acdc40fb4 2026-03-20 15:36:17 +05:00
Andrej730 3b7cf6e865 Fix error displaying bsdd description after API update (ed81a0a4b) 2026-03-20 15:36:17 +05:00
Andrej730 6038373ee5 ifcopenshell_wrapper.pyi - sync default values, validate_stub - suggest default values 2026-03-20 15:36:16 +05:00
Andrej730 3d7de87b46 ifcopenshell_wrapper.pyi - add temp MakeVolume stub 2026-03-20 15:36:16 +05:00
Andrej730 cb113ae8da ifcopenshell_wrapper.pyi - support stubs for constructors 2026-03-20 15:36:15 +05:00
Andrej730 26280d24fe Add ty to check for missing symbols and other simple errors 2026-03-20 15:36:14 +05:00
Andrej730 30551cb288 typing 2026-03-20 15:36:14 +05:00
Andrej730 3bf0edeca2 Fix subtle walrus operator bug in align_walls using e before assignment 2026-03-20 15:34:57 +05:00
Andrej730 3590b08e68 search/operator - remove unnecessary Ifc Operators 2026-03-20 15:34:57 +05:00
Ryan Schultz fd902d88fb Update selector_syntax.rst with query examples
Clarified usage of queries in IfcAnnotation tags with examples.
2026-03-18 18:25:03 -05:00
Sayan Jyoti Das aa5f5120e0 delete ifcopenshell wheel 2026-03-18 14:37:24 +05:30
Sayan Jyoti Das 81986bcbfb ifcopenshell wasm wheel should be dynamically fetched, not included in git 2026-03-18 14:35:55 +05:30
Andrej730 ec6c268cdb Fix type assign_type core test (44a52863a) 2026-03-18 13:15:39 +05:00
Andrej730 64003fd5ef Fix drawing update_drawing_name core test (19534e225) 2026-03-18 13:15:38 +05:00
Andrej730 58d07bace4 Fix drawing edit_text core test and tool interface (5e9f97a0c) 2026-03-18 13:15:38 +05:00
Andrej730 3b718bc58d Fix georeference core tests (b246998f6) 2026-03-18 13:15:38 +05:00
tsomanna_QCOM 18c035ea77 Fix Windows ARM64 Python Bindings Issue 2026-03-18 08:44:34 +01:00
Andrej730 f2e2e324b1 Fixing stubs
- `function_item`, `tags` added in df7318973
- MakeVolume added in c385b93, ignore as all other conversion settings
- moved `SeparateZUpNode` ignore to the other geom serializer settings
2026-03-18 12:25:14 +05:00
Andrej730 069dbbd8c2 bonsai docs - add maintenance page 2026-03-18 12:25:14 +05:00
Andrej730 3385872e8b ci-black-formatting - use variables for min Python versions 2026-03-18 12:25:14 +05:00
Andrej730 f0b27a0910 ifcopenshell-python Makefile - simplify pyversion check, similar to 6409f41 2026-03-18 12:25:13 +05:00
Andrej730 888158570a Remove Python 3.9 references 2026-03-18 11:20:22 +05:00
Andrej730 c03156b5cd control.assign_control - remove deprecated related_object argument support 2026-03-18 11:20:22 +05:00
Andrej730 05bf2b82d2 system.disconnect_port - fix missing flow direction reset (bbda8d2) 2026-03-18 11:03:33 +05:00
Andrej730 7bdc1b6a75 cache_dependencies - skip ifcopenshell dir when packing 2026-03-18 11:01:38 +05:00
Sayan Jyoti Das 03de69814a fixes and cleanups from old branch 2026-03-18 11:22:41 +05:30
Sayan Jyoti Das 4dc6a0f2bc update ifcopenshell wheel to ifcopenshell-0.8.5+a51b2c5 2026-03-18 11:18:50 +05:30
Andrej730 c33509364c Fix error generating ifcpatch recipes docs for Bonsai tooltips
Mentioned in https://github.com/IfcOpenShell/IfcOpenShell/issues/7667#issuecomment-4076645173

Traceback:
```
Traceback (most recent call last):
  File "\bonsai\bim\module\patch\prop.py", line 55, in get_ifcpatch_recipes
    docs = ifcpatch.extract_docs(f, "Patcher", "__init__", ("src", "file", "logger", "args"))
  File "\ifcpatch\__init__.py", line 168, in extract_docs
    spec.loader.exec_module(submodule)
    ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
  File "<frozen importlib._bootstrap_external>", line 1027, in exec_module
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "\ifcpatch/recipes/FixRevit2025TINs.py", line 31, in <module>
    class Patcher:
    ...<509 lines>...
            return co / self.unit_scale
  File "\ifcpatch/recipes/FixRevit2025TINs.py", line 168, in Patcher
    def create_edges(self, obj: bpy.types.Object) -> None:
                                ^^^
NameError: name 'bpy' is not defined
File "\bonsai\bim\module\patch\prop.py", line 43, in get_ifcpatch_recipes
```
2026-03-18 10:04:36 +05:00
Sayan Jyoti Das 47f058341b local ifctester wheel build 2026-03-18 10:29:09 +05:30
Thomas Krijnen a51b2c587c Revert "Simplifies IfxAxis2PlacementLinear, assumes default Axis = (0,0,1)"
This reverts commit cf1552e79e.
2026-03-17 20:49:48 +01:00
Sayan Jyoti Das 845a13ba83 some fixes and lint cleanups 2026-03-17 21:16:53 +05:30
Sayan Jyoti Das 530841967e Merge branch 'v0.8.0' into ifctester-improvements 2026-03-17 19:49:09 +05:30
Andrej730 c36e7badae Remove use of deprecated os.popen 2026-03-17 18:14:22 +05:00
Andrej730 515fe8d2ef Remove use of deprecated tempfile.mktemp 2026-03-17 18:14:22 +05:00
Andrej730 b8d3d1d105 pyproject.toml - add ty command to check for deprecated methods 2026-03-17 18:14:22 +05:00
Sayan Jyoti Das e95da857d6 convert codebase to typescript + introduce biome lint 2026-03-16 21:55:10 +05:30
Sayan Jyoti Das d587d1ac11 build step for pyodide 2026-03-16 21:46:46 +05:30
Sayan Jyoti Das 6117417b89 update webapp + bonsai integration 2026-03-16 15:54:52 +05:30
Thomas Krijnen 0398584c69 empty 2026-03-16 15:45:41 +05:30
Thomas Krijnen 0c69f85d5e Empty 2026-03-16 15:45:40 +05:30
228 changed files with 3789 additions and 1559 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ jobs:
python ../nix/cache_dependencies.py unpack
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.20
uses: hendrikmuhs/ccache-action@v1.2.21
with:
key: mac-${{ matrix.arch }}
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
python ../IfcOpenShell/nix/cache_dependencies.py unpack
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.20
uses: hendrikmuhs/ccache-action@v1.2.21
with:
key: ubuntu-22.04-${{ runner.arch }}
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
python3 ../nix/cache_dependencies.py unpack
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.20
uses: hendrikmuhs/ccache-action@v1.2.21
with:
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
python3 ../nix/cache_dependencies.py unpack
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.20
uses: hendrikmuhs/ccache-action@v1.2.21
with:
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
+1 -1
View File
@@ -52,7 +52,7 @@ jobs:
}
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.20
uses: hendrikmuhs/ccache-action@v1.2.21
with:
key: win-${{ matrix.arch }}
# Windows ccache needs ~1GB
+7 -4
View File
@@ -7,6 +7,9 @@ on:
jobs:
lint-formatting:
runs-on: ubuntu-latest
env:
MIN_IOS_PY_VERSION: "3.10"
MIN_BLENDER_PY_VERSION: "3.11"
steps:
- name: Action - checkout repository
uses: actions/checkout@v6
@@ -14,12 +17,12 @@ jobs:
- name: Action - install python
uses: actions/setup-python@v6
with:
python-version: "3.10"
python-version: ${{ env.MIN_IOS_PY_VERSION }}
- name: Action - install python
uses: actions/setup-python@v6
with:
python-version: "3.11"
python-version: ${{ env.MIN_BLENDER_PY_VERSION }}
- name: Install dependencies
run: |
@@ -35,8 +38,8 @@ jobs:
ERROR=0
# Using 2 Python versions - one minimum required for IfcOpenShell
# and other that's used by Blender currently.
python3.10 -W error -m compileall -q src/ifcopenshell-python || ERROR=1
python3.11 -W error -m compileall -q src/bonsai || ERROR=1
python${{ env.MIN_IOS_PY_VERSION }} -W error -m compileall -q src/ifcopenshell-python || ERROR=1
python${{ env.MIN_BLENDER_PY_VERSION }} -W error -m compileall -q src/bonsai || ERROR=1
exit $ERROR
continue-on-error: true
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
-
name: ccache
uses: hendrikmuhs/ccache-action@v1.2.20
uses: hendrikmuhs/ccache-action@v1.2.21
-
name: Build ifcopenshell
@@ -24,7 +24,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pyver: [py39, py310, py311, py312, py313, py314]
pyver: [py310, py311, py312, py313, py314]
config:
- {
name: "Windows 64bit",
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pyver: [py39, py310, py311, py312, py313, py314]
pyver: [py310, py311, py312, py313, py314]
config:
- {
name: "Windows 64bit",
+2 -1
View File
@@ -79,7 +79,7 @@ jobs:
libhdf5-dev libcgal-dev libeigen3-dev
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.20
uses: hendrikmuhs/ccache-action@v1.2.21
with:
key: ubuntu-22.04-${{ runner.arch }}
@@ -254,6 +254,7 @@ jobs:
cd ../ifcpatch && make test || ERROR=1
pip install -e ../ifctester --no-deps
cd ../ifctester && make test || ERROR=1
make build-ids-docs || ERROR=1
# Run mathutils related tests at the end to ensure no other code is relying on mathutils.
cd ../ifcopenshell-python
pip install mathutils
+10 -3
View File
@@ -5,6 +5,8 @@
/_installed-vs*-x*/
/build/
/src/examples/build/
# ifctester docs output
/src/ifctester/test/build/
# output directories
/cmake/out/
@@ -12,6 +14,7 @@
/src/ifcmax/out/
/src/ifcwrap/out/
/src/qtviewer/out/
/src/ifctester/webapp/public/pyodide/
/win/BuildDepsCache*.txt
@@ -80,10 +83,14 @@ src/ifcopenshell-python/test/build
# bonsai i18n
src/bonsai/bonsai/translations.py
# bonsai test temp files
# bonsai external dependencies (cloned for just ty checks)
src/bonsai/external_dependencies/
# bonsai test temp/cache files
src/bonsai/test/files/temp
src/bonsai/test/files/basic.ifc.cache.blend
src/bonsai/test/files/basic.ifc.cache.sqlite
src/bonsai/test/files/*.cache.blend
src/bonsai/test/files/*.cache.json
src/bonsai/test/files/*.cache.sqlite
# bonsai data
src/bonsai/bonsai/bim/data/build/
+19 -10
View File
@@ -13,6 +13,7 @@ import hashlib
import os
import pathlib
import re
import subprocess
from typing import NoReturn
from urllib import request
@@ -20,7 +21,7 @@ from github import Github
def get_repo_tag_names() -> list[str]:
git_return = os.popen("git tag -l").read()
git_return = subprocess.check_output("git tag -l", text=True)
tag_names = [tag_name for tag_name in git_return.split("\n") if tag_name]
print(f"{len(tag_names)} tag_names found in repo")
return tag_names
@@ -78,6 +79,10 @@ def get_release_zip(tag: str) -> tuple[str, str]:
raise Exception(f"Couldn't find the release matching '{python_version}' and '{TARGET_OS}' in tag '{tag}'.")
def run(command: str) -> None:
subprocess.check_output(command)
start = datetime.datetime.now()
URL_CHOCO_PACKAGE = "https://community.chocolatey.org/packages/blender"
@@ -97,7 +102,7 @@ should_release = False
target_release_tag = ""
TARGET_OS = "windows-x64"
git_status = os.popen("git status").read()
git_status = subprocess.check_output("git status", text=True)
print(git_status)
for tag_name in get_repo_tag_names():
@@ -147,7 +152,7 @@ blenderbim_build_version = target_release_tag.replace("blenderbim-", "")
# url_blenderbim_py3x_win_zip
release_zip_file_name, url_blenderbim_py3x_win_zip = get_release_zip(target_release_tag)
os.popen(f"wget {url_blenderbim_py3x_win_zip} --no-verbose").read()
subprocess.check_call(f"wget {url_blenderbim_py3x_win_zip} --no-verbose")
# sha256sum_blenderbim_py310_win_zip
sha256sum_blenderbim_py3x_win_zip = get_file_sha256_hash(release_zip_file_name)
@@ -201,13 +206,13 @@ print("[INFO] inserting dynamic chocolatey package parameters successful")
print("\n_____ build choco.exe with mono")
choco_version = "1.1.0"
os.popen(f"wget https://github.com/chocolatey/choco/archive/refs/tags/{choco_version}.tar.gz --quiet").read()
os.popen(f"tar -xzf {choco_version}.tar.gz").read()
run(f"wget https://github.com/chocolatey/choco/archive/refs/tags/{choco_version}.tar.gz --quiet")
run(f"tar -xzf {choco_version}.tar.gz")
print("choco tar unpack successful")
os.chdir("choco-1.1.0")
os.popen("./build.sh").read()
run("./build.sh")
os.popen("cp -r build_output/chocolatey /opt/chocolatey").read()
run("cp -r build_output/chocolatey /opt/chocolatey")
os.chdir(BLENDERBIM_DIR)
if pathlib.Path("/opt/chocolatey/choco.exe").exists():
@@ -215,11 +220,15 @@ if pathlib.Path("/opt/chocolatey/choco.exe").exists():
print("\n_____ build choco pack")
os.popen("mono /opt/chocolatey/choco.exe pack --allow-unofficial").read()
os.popen('mono /opt/chocolatey/choco.exe setapikey --key="{choco_token}" --source="https://push.chocolatey.org/" --allow-unofficial').read()
run("mono /opt/chocolatey/choco.exe pack --allow-unofficial")
run(
'mono /opt/chocolatey/choco.exe setapikey --key="{choco_token}" --source="https://push.chocolatey.org/" --allow-unofficial'
)
print("\n_____ build choco push")
os.popen('mono /opt/chocolatey/choco.exe push --source="https://push.chocolatey.org/" --key="$CHOCO_TOKEN" --allow-unofficial --verbose').read()
run(
'mono /opt/chocolatey/choco.exe push --source="https://push.chocolatey.org/" --key="$CHOCO_TOKEN" --allow-unofficial --verbose'
)
print(f"choco push of version: {target_release_tag} successful!")
print(f"it took: {datetime.datetime.now() - start}")
+3
View File
@@ -41,6 +41,9 @@ def pack_dependencies(install_dir: Path) -> None:
if not dependency_path.is_dir():
continue
dependency_name = dependency_path.name
# Skip ifcopenshell - it's a build output, not a dependency to reuse across builds.
if dependency_name == "ifcopenshell":
continue
tar_path = install_dir / f"{CACHE_PREFIX}{dependency_name}.tar.gz"
if tar_path.exists():
print(f"Skipping existing cache: '{tar_path}'")
+175 -1
View File
@@ -3,7 +3,7 @@ name = "IfcOpenShell"
version = "0.0.0"
dependencies = [
"black==26.3.1",
"ruff==0.15.6",
"ruff==0.15.7",
"poethepoet",
"gersemi==0.26.1",
]
@@ -78,6 +78,139 @@ ignore = [
"UP032", # Replace .format with f-string
]
[tool.ty.rules]
all = "ignore"
# Structural rules (no deep type inference needed, easier to adapt).
abstract-method-in-final-class = "error"
ambiguous-protocol-member = "error"
byte-string-type-annotation = "error"
conflicting-declarations = "error"
conflicting-metaclass = "error"
cyclic-class-definition = "error"
cyclic-type-alias-definition = "error"
dataclass-field-order = "error"
duplicate-base = "error"
duplicate-kw-only = "error"
empty-body = "error"
escape-character-in-forward-annotation = "error"
final-on-non-method = "error"
final-without-value = "error"
fstring-type-annotation = "error"
ignore-comment-unknown-rule = "error"
implicit-concatenated-string-type-annotation = "error"
inconsistent-mro = "error"
ineffective-final = "error"
instance-layout-conflict = "error"
invalid-dataclass = "error"
invalid-dataclass-override = "error"
invalid-enum-member-annotation = "error"
invalid-explicit-override = "error"
invalid-frozen-dataclass-subclass = "error"
invalid-generic-class = "error"
invalid-generic-enum = "error"
invalid-ignore-comment = "error"
invalid-legacy-positional-parameter = "error"
invalid-legacy-type-variable = "error"
invalid-named-tuple = "error"
invalid-newtype = "error"
invalid-overload = "error"
invalid-paramspec = "error"
invalid-protocol = "error"
invalid-syntax-in-forward-annotation = "error"
invalid-total-ordering = "error"
invalid-type-alias-type = "error"
invalid-type-checking-constant = "error"
invalid-type-guard-definition = "error"
invalid-type-variable-bound = "error"
invalid-type-variable-constraints = "error"
invalid-typed-dict-header = "error"
invalid-typed-dict-statement = "error"
override-of-final-method = "error"
override-of-final-variable = "error"
possibly-missing-import = "error"
possibly-missing-submodule = "error"
# Has false positives due to ty walrus operator bug.
# possibly-unresolved-reference = "error"
raw-string-type-annotation = "error"
redundant-final-classvar = "error"
shadowed-type-variable = "error"
subclass-of-final-class = "error"
super-call-in-named-tuple-method = "error"
unavailable-implicit-super-arguments = "error"
unbound-type-variable = "error"
undefined-reveal = "error"
unresolved-global = "error"
unresolved-import = "error"
unresolved-reference = "error"
unused-ignore-comment = "error"
unused-type-ignore-comment = "error"
useless-overload-body = "error"
# Non-structural rules:
deprecated = "error"
zero-stepsize-in-slice = "error"
possibly-missing-implicit-call = "error"
unused-awaitable = "error"
# Function argument rules:
# Conflicts with `ifcopenshell.api.geometry.add_representation` type of callables we have, confusing them with a module.
# call-non-callable = "error"
conflicting-argument-forms = "error"
# Too many false positives.
# invalid-argument-type = "error"
missing-argument = "error"
parameter-already-assigned = "error"
positional-only-parameter-as-kwarg = "error"
too-many-positional-arguments = "error"
unknown-argument = "error"
# Has a lot of warnings due to current ty walrus operator issues.
# index-out-of-bounds = "error"
# unresolved-attribute = "error"
[tool.ty.environment]
extra-paths = [
"src/bonsai/external_dependencies",
"src/bcf",
"src/bsdd",
"src/bonsai",
"src/ifc4d",
"src/ifc5d",
"src/ifccityjson",
"src/ifcclash",
"src/ifccsv",
"src/ifcdiff",
"src/ifcfm",
"src/ifcopenshell-python",
"src/ifcpatch",
"src/ifctester",
]
[tool.ty.src]
exclude = [
# External dependencies cloned for type checking only.
"src/bonsai/external_dependencies",
# Submodules.
"src/ifcopenshell-python/ifcopenshell/express",
"src/ifcopenshell-python/ifcopenshell/mvd",
"src/ifcopenshell-python/ifcopenshell/simple_spf",
"src/svgfill/3rdparty",
# Has special dependencies.
"src/ifcopenshell-python/ifcopenshell/geom/app.py",
"src/ifcopenshell-python/ifcopenshell/geom/code_editor_pane.py",
"src/ifcopenshell-python/ifcopenshell/util/doc.py",
"src/ifcopenshell-python/ifcopenshell/util/generate_pset_templates.py",
"src/ifcopenshell-python/ifcopenshell/util/ifc4x3dev_scrape_data_for_docs.py",
# Too esoteric.
"src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py",
"src/ifc2ca/templates",
# Too dev.
"src/bcf/setup.py",
"src/bsdd/yml_to_classes.py",
# Deprecated.
"src/ifc2ca/_deprecated",
]
[tool.poe.tasks]
ruff-main = "ruff check --extend-exclude nix/build-all.py"
@@ -87,6 +220,47 @@ ruff.sequence = ["ruff-main", "ruff-old"]
black = "black ."
ty.sequence = ["ty-bonsai", "ty-ios"]
ty.help = "Run ty type checker. Requires ty-venv to be set up first."
ty-bonsai = "ty check src/bonsai --python=src/bonsai/.venv"
ty-venv.sequence = ["ty-venv-bonsai", "ty-venv-ios"]
ty-venv-bonsai.sequence = [
{cmd = "uv venv src/bonsai/.venv --python=3.11 --allow-existing"},
{cmd = "uv pip install -r src/bonsai/type-check-requirements.txt --python=src/bonsai/.venv"},
]
ty-venv-ios.sequence = [
{cmd = "uv venv src/ifcopenshell-python/.venv --python=3.10 --allow-existing"},
{cmd = "uv pip install -r src/ifcopenshell-python/type-check-requirements.txt --python=src/ifcopenshell-python/.venv"},
]
format.sequence = ["black", "ruff-main", "ruff-old"]
cmake-format = "gersemi . --in-place"
[tool.poe.tasks.ty-ios]
# --ignore unresolved-reference: walrus operator false positives in ty.
cmd = """
ty check
src/bcf
src/bsdd
src/ifc2ca
src/ifc4d
src/ifc5d
src/ifccityjson
src/ifcclash
src/ifccsv
src/ifcdiff
src/ifcfm
src/ifcopenshell-python
src/ifcpatch
src/ifctester
--python=src/ifcopenshell-python/.venv
--ignore unresolved-reference
"""
[tool.poe.tasks.bonsai-deps]
help = "Clone or update Bonsai external dependencies."
cmd = "python src/bonsai/scripts/bonsai_deps.py"
+3 -3
View File
@@ -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]
self.server.auth_state = query.get("state", [""])[0]
self.send_response(200)
self.send_header("Content-type", "text/plain")
self.end_headers()
@@ -255,7 +255,7 @@ class BcfClient:
project_id: str = "",
topics: str = "",
query_string: Optional[str] = None,
) -> list[Any]:
) -> None:
# return self.get(
# f"/projects/{project_id}/topics",
# {
+14 -10
View File
@@ -173,16 +173,17 @@ def assert_viewpoints(viewpoints):
assert viewpoint.snapshot is not None
# TODO: dead code - ported from v2 but buildingSMART/BCF-XML has no v3 MaximumInformation.bcf equivalent
def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
expected_vp = mdl.VisualizationInfo(
components=mdl.Components(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=False,
),
selection=expected_selection,
visibility=mdl.ComponentVisibility(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=False,
),
exceptions=expected_exception,
default_visibility=False,
),
@@ -193,6 +194,7 @@ def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, e
camera_direction=mdl.Direction(x=0.6745243072509766, y=-0.6599355936050415, z=-0.33091068267822266),
camera_up_vector=mdl.Direction(x=0.2271970510482788, y=-0.24091780185699463, z=0.9435783624649048),
field_of_view=60,
aspect_ratio=1.0,
),
guid="21dd4807-e9af-439e-a980-04d913a6b1ce",
)
@@ -200,16 +202,17 @@ def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, e
assert viewpoint.snapshot is not None
# TODO: dead code - ported from v2 but buildingSMART/BCF-XML has no v3 MaximumInformation.bcf equivalent
def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
expected_vp = mdl.VisualizationInfo(
components=mdl.Components(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=True,
),
selection=expected_selection,
visibility=mdl.ComponentVisibility(
view_setup_hints=mdl.ViewSetupHints(
spaces_visible=False,
space_boundaries_visible=False,
openings_visible=True,
),
exceptions=expected_exception,
default_visibility=True,
),
@@ -220,6 +223,7 @@ def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, ex
camera_direction=mdl.Direction(x=0.7232745289802551, y=0.5967116951942444, z=-0.3475759029388428),
camera_up_vector=mdl.Direction(x=0.27662187814712524, y=0.21082592010498047, z=0.937567412853241),
field_of_view=60,
aspect_ratio=1.0,
),
guid="81daa431-bf01-4a49-80a2-1ab07c177717",
)
+8 -5
View File
@@ -45,16 +45,19 @@ from bonsai.bim.module.nest.decorator import NestDecorator
cwd = os.path.dirname(os.path.realpath(__file__))
global_subscription_owner = object()
# Separate owner for per-object msgbus subscriptions (name, active_material_index).
# Using a dedicated owner allows clearing all per-object subscriptions at once
# during undo/redo without affecting other global subscriptions.
object_subscription_owner = object()
def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -> None:
try:
obj.name
except:
# The object is invalid but somehow still has a callback. Clear all
# msgbus subscriptions to prevent useless further triggers.
bpy.msgbus.clear_by_owner(obj)
return # In case the object RNA is gone during an undo / redo operation
# The object is invalid but somehow still has a callback.
# This can occur during undo/redo when the Python wrapper is stale.
return
# Blender names are up to 63 UTF-8 bytes
if len(bytes(obj.name, "utf-8")) >= 63:
return
@@ -189,7 +192,7 @@ def subscribe_to(obj: bpy.types.ID, data_path: str, callback: Callable[[bpy.type
return
bpy.msgbus.subscribe_rna(
key=subscribe_to,
owner=obj,
owner=object_subscription_owner,
args=(
obj,
data_path,
+4 -9
View File
@@ -316,11 +316,8 @@ class IfcStore:
del IfcStore.id_map[data["id"]]
if "guid" in data:
del IfcStore.guid_map[data["guid"]]
obj = IfcStore.get_object_by_name(data["obj"])
if obj is None:
# obj was just created during this step and didn't existed before.
return
bpy.msgbus.clear_by_owner(obj)
# Note: msgbus subscriptions are cleared globally during
# rebuild_element_maps which runs after every undo/redo.
@staticmethod
def commit_link_element(data: OperationData) -> None:
@@ -367,10 +364,8 @@ class IfcStore:
del IfcStore.id_map[data["id"]]
if "guid" in data:
del IfcStore.guid_map[data["guid"]]
obj = IfcStore.get_object_by_name(data["obj"])
# obj might be removed after unlink.
if not obj:
bpy.msgbus.clear_by_owner(obj)
# Note: msgbus subscriptions are cleared globally during
# rebuild_element_maps which runs after every undo/redo.
@staticmethod
def unlink_element(
+2 -2
View File
@@ -64,8 +64,8 @@ class MaterialCreator:
mesh: Union[OBJECT_DATA_TYPE, None],
shape_has_openings: bool,
) -> None:
if ((rep := getattr(element, "Representation", ...) is not ...) and not rep) or (
(rep := getattr(element, "RepresentationMaps", ...) is not ...) and not rep
if (((rep := getattr(element, "Representation", ...)) is not ... and not rep) or
((rep := getattr(element, "RepresentationMaps", ...)) is not ... and not rep)
):
return
@@ -101,7 +101,7 @@ class AggregateDecorator:
cls.is_installed = False
def dotted_line_shader(self):
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty:ignore[too-many-positional-arguments]
vert_out.smooth("FLOAT", "v_ArcLength")
shader_info = gpu.types.GPUShaderCreateInfo()
+1 -1
View File
@@ -230,7 +230,7 @@ class BcfTopic(PropertyGroup):
def get_related_topics(self: "BCFProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
global RELATED_TOPICS_ENUM_ITEMS
global RELATED_TOPICS_ENUM_ITEMS # ty: ignore[unresolved-global]
props = self
active_topic = props.active_topic
active_related_topics = active_topic.related_topics.keys()
+4 -4
View File
@@ -46,26 +46,26 @@ def get_libraries(self, context):
def get_namespaces(self, context):
global NAMESPACES_ENUM_ITEMS
global NAMESPACES_ENUM_ITEMS # ty: ignore[unresolved-global]
NAMESPACES_ENUM_ITEMS = [(uri, f"{alias}: {uri}", "") for alias, uri in BrickStore.namespaces]
return NAMESPACES_ENUM_ITEMS
def get_brick_entity_classes(self, context):
global ENTITY_CLASSES_ENUM_ITEMS
global ENTITY_CLASSES_ENUM_ITEMS # ty: ignore[unresolved-global]
entity = self.brick_entity_create_type
ENTITY_CLASSES_ENUM_ITEMS = [(uri, uri.split("#")[-1], "") for uri in BrickStore.entity_classes[entity]]
return ENTITY_CLASSES_ENUM_ITEMS
def get_brick_roots(self, context):
global BRICK_ROOTS_ENUM_ITEMS
global BRICK_ROOTS_ENUM_ITEMS # ty: ignore[unresolved-global]
BRICK_ROOTS_ENUM_ITEMS = [(root, root, "") for root in BrickStore.root_classes]
return BRICK_ROOTS_ENUM_ITEMS
def get_brick_relations(self, context):
global BRICK_RELATIONS_ENUM_ITEMS
global BRICK_RELATIONS_ENUM_ITEMS # ty: ignore[unresolved-global]
BRICK_RELATIONS_ENUM_ITEMS = [(uri, uri.split("#")[-1], "") for uri in BrickStore.relationships]
for relation in BrickschemaData.data["active_relations"]:
if relation["predicate_name"] == "label":
@@ -1285,7 +1285,7 @@ class SnapManager:
continue
coords = np.empty(vertex_count * 3, dtype=np.float32)
mesh.vertices.foreach_get("co", coords) # type: ignore[arg-type]
mesh.vertices.foreach_get("co", coords)
coords = coords.reshape(-1, 3)
matrix = np.array(obj_eval.matrix_world, dtype=np.float32)
@@ -456,7 +456,8 @@ def format_distance(
tx_dist = fmt % d_cm
else:
tx_dist = fmt % value
assert f"Unexpected unit_system - '{unit_system}'."
# tx_dist = fmt % value
return tx_dist
@@ -1426,6 +1426,7 @@ class CreateDrawing(bpy.types.Operator):
"/Pset_.*Common/.Status",
"EPset_Status.Status",
"EPset_Status.UserDefinedStatus",
"Material.Name",
]
group = root.find("{http://www.w3.org/2000/svg}g")
@@ -366,9 +366,6 @@ class BaseLinesShader(BaseShader):
}
"""
def __init__(self, gap_size=16):
super().__init__(gap_size=gap_size)
def glenable(self):
super().glenable()
@@ -1066,7 +1066,7 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
cls.poll_message_set("Only available from Outliner.")
return False
def execute(self, context):
def execute(self, context): # ty:ignore[override-of-final-method]
if len(getattr(context, "selected_ids", [])) == 0:
return {"FINISHED"}
@@ -2289,7 +2289,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
elif obj in pprops.clipping_planes_objs:
self.report({"ERROR"}, "Clipping planes cannot be edited")
elif element:
if not obj.data:
if not obj.data or obj.type not in ("MESH", "CURVE"):
self.report({"INFO"}, "No geometry to edit")
elif tool.Geometry.is_locked(element):
self.report({"ERROR"}, lock_error_message(obj.name))
@@ -139,7 +139,9 @@ def update_local_coordinates(self: "BIMGeoreferenceProperties", context: bpy.typ
tool.Georeference.set_coordinates(
"blender",
ifcopenshell.util.geolocation.enh2xyz(
*local_coordinates,
local_coordinates[0],
local_coordinates[1],
local_coordinates[2],
float(props.blender_offset_x),
float(props.blender_offset_y),
float(props.blender_offset_z),
@@ -162,7 +164,9 @@ def update_map_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types
tool.Georeference.set_coordinates(
"blender",
ifcopenshell.util.geolocation.enh2xyz(
*local_coordinates,
local_coordinates[0],
local_coordinates[1],
local_coordinates[2],
float(props.blender_offset_x),
float(props.blender_offset_y),
float(props.blender_offset_z),
+1 -1
View File
@@ -27,7 +27,7 @@ from bonsai.bim.prop import StrProperty
class BIMCityJsonProperties(PropertyGroup):
def get_lods(self, context):
global LODS_ENUM_ITEMS
global LODS_ENUM_ITEMS # ty: ignore[unresolved-global]
LODS_ENUM_ITEMS = [(item.name, "LOD" + item.name, "Level of Detail " + item.name) for item in self.lods]
return LODS_ENUM_ITEMS
+1 -1
View File
@@ -320,7 +320,7 @@ class RadianceExporterProperties(PropertyGroup):
)
def get_subcategories(self, context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
global SUBCATEGORIES_ENUM_ITEMS
global SUBCATEGORIES_ENUM_ITEMS # ty: ignore[unresolved-global]
if self.category in spectraldb:
SUBCATEGORIES_ENUM_ITEMS = [(k, k, "") for k in spectraldb[self.category].keys()]
else:
@@ -717,7 +717,11 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
self.props.material_set_item_material = str(material_set_item.Material.id())
self.props.material_set_item_attributes.clear()
bonsai.bim.helper.import_attributes(material_set_item, self.props.material_set_item_attributes)
bonsai.bim.helper.import_attributes(
material_set_item,
self.props.material_set_item_attributes,
callback=self.import_attributes_callback,
)
if material_set_item.is_a("IfcMaterialProfile"):
if material_set_item.Profile and material_set_item.Profile.ProfileName:
@@ -725,6 +729,29 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
return {"FINISHED"}
def import_attributes_callback(
self, name: str, prop: Union["Attribute", None], data: dict[str, Any]
) -> None | Literal[True]:
if data["type"] != "IfcMaterialLayer" or name != "IsVentilated" or not prop:
return None
# Keep null semantics unchanged on export, but avoid an empty UI selection.
prop.data_type = "enum"
prop.special_type = "LOGICAL"
prop.enum_items = json.dumps(("TRUE", "FALSE", "UNKNOWN"))
value = data[name]
if value == "UNKNOWN":
prop.enum_value = "UNKNOWN"
elif value is None:
# Keep visible default as FALSE, but preserve null semantics on save.
prop.enum_value = "FALSE"
prop.is_null = True
else:
prop.enum_value = "TRUE" if value else "FALSE"
return True
class DisableEditingMaterialSetItem(bpy.types.Operator):
bl_idname = "bim.disable_editing_material_set_item"
+2 -2
View File
@@ -227,7 +227,7 @@ class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator):
is_parallel21 = tool.Cad.is_x(angle21, (0, 180), tolerance=0.001)
is_parallel23 = tool.Cad.is_x(angle23, (0, 180), tolerance=0.001)
if not all(is_parallel12, is_parallel13, is_parallel21, is_parallel23):
if not all([is_parallel12, is_parallel13, is_parallel21, is_parallel23]):
fitting_type = "WYE"
if not fitting_type:
@@ -903,7 +903,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
start_segment_id: bpy.props.IntProperty(name="Start Segment Element ID", default=0)
end_segment_id: bpy.props.IntProperty(name="End Segment Element ID", default=0)
radius: bpy.props.FloatProperty(
"Bend Inner Radius", description="Bend inner radius in SI units", default=0.2, subtype="DISTANCE", min=0
name="Bend Inner Radius", description="Bend inner radius in SI units", default=0.2, subtype="DISTANCE", min=0
)
def _execute(self, context):
@@ -540,6 +540,16 @@ class AddBoolean(Operator, tool.Ifc.Operator):
booleans = ifcopenshell.api.geometry.add_boolean(tool.Ifc.get(), first_item, second_items, props.operator)
rep_obj = tool.Geometry.get_geometry_props().representation_obj
if booleans:
# Users typically select two top-level items and expect the
# operand to be absorbed into the boolean, not remain as a
# standalone item alongside it.
representation = tool.Geometry.get_active_representation(rep_obj)
representation = ifcopenshell.util.representation.resolve_representation(representation)
second_items_set = set(second_items)
new_items = [i for i in representation.Items if i not in second_items_set]
if new_items:
representation.Items = new_items
rep_element = tool.Ifc.get_entity(rep_obj)
tool.Model.mark_manual_booleans(rep_element, booleans)
tool.Geometry.reload_representation(rep_obj)
@@ -694,10 +694,14 @@ def generate_box(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[
new_settings = settings.copy()
new_settings["context"] = box_context
new_box = ifcopenshell.api.geometry.add_representation(ifc_file, should_run_listeners=False, **new_settings)
new_box = ifcopenshell.api.geometry.add_representation(
ifc_file,
should_run_listeners=False, # ty:ignore[unknown-argument]
**new_settings,
)
ifcopenshell.api.geometry.assign_representation(
ifc_file,
should_run_listeners=False,
should_run_listeners=False, # ty:ignore[unknown-argument]
product=product,
representation=new_box,
)
+40 -16
View File
@@ -18,7 +18,7 @@
import copy
from math import atan2, degrees, pi, radians
from typing import Any, Literal, Optional, Union
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
import bpy
import ifcopenshell
@@ -49,7 +49,7 @@ ProfileFrom2PointsReturn = Union[dict[str, Any], None]
class DumbProfileGenerator:
def __init__(self, relating_type):
def __init__(self, relating_type: ifcopenshell.entity_instance):
self.relating_type = relating_type
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
@@ -201,7 +201,7 @@ class DumbProfileGenerator:
class DumbProfileRegenerator:
def regenerate_from_profile_def(self, profile):
def regenerate_from_profile_def(self, profile: ifcopenshell.entity_instance) -> None:
self.file = tool.Ifc.get()
objs = []
if not profile:
@@ -221,7 +221,7 @@ class DumbProfileRegenerator:
for element in self.get_element_types_using_profile(profile):
tool.Model.mark_thumbnail_for_update(element)
def regenerate_from_profile(self, usecase_path, ifc_file, settings):
def regenerate_from_profile(self, usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
self.file = ifc_file
objs = []
profile = settings["profile"].Profile
@@ -233,7 +233,7 @@ class DumbProfileRegenerator:
objs.append(obj)
DumbProfileRecalculator().recalculate(objs)
def get_elements_using_profile(self, profile):
def get_elements_using_profile(self, profile: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
results = []
profile_sets = [
mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile")
@@ -252,7 +252,9 @@ class DumbProfileRegenerator:
results.extend(rel.RelatedObjects)
return results
def get_element_types_using_profile(self, profile):
def get_element_types_using_profile(
self, profile: ifcopenshell.entity_instance
) -> list[ifcopenshell.entity_instance]:
results = []
profile_sets = [
mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile")
@@ -269,12 +271,18 @@ class ExtendProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.extend_profile"
bl_label = "Extend Profile"
bl_options = {"REGISTER", "UNDO"}
join_type: bpy.props.StringProperty()
join_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
items=[("-", "Unjoin", ""), ("L", "L", ""), ("V", "V", ""), ("T", "T", "")],
default="-",
)
if TYPE_CHECKING:
join_type: Literal["-", "L", "V", "T"]
def _execute(self, context):
selected_objs = context.selected_objects
joiner = DumbProfileJoiner()
if not self.join_type:
if self.join_type == "-":
for obj in selected_objs:
joiner.unjoin(obj)
return {"FINISHED"}
@@ -626,11 +634,15 @@ class DumbProfileJoiner:
if connection1 == "ATEND":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
self.body[1] = intersect
else:
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
max_dim = self.get_max_bound_box_dimension(profile1)
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
@@ -673,11 +685,15 @@ class DumbProfileJoiner:
elif connection1 == "ATSTART":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
self.body[0] = intersect
else:
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
max_dim = self.get_max_bound_box_dimension(profile1)
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
@@ -721,7 +737,9 @@ class DumbProfileJoiner:
if connection1 == "ATEND":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
self.body[1] = intersect
else:
plane = self.get_profile_plane(
@@ -729,7 +747,9 @@ class DumbProfileJoiner:
furthest_plane if is_relating else closest_plane,
z_inwards=False if is_relating else True,
)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
max_dim = self.get_max_bound_box_dimension(profile1)
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
self.clippings.append(
@@ -742,7 +762,9 @@ class DumbProfileJoiner:
elif connection1 == "ATSTART":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
self.body[0] = intersect
else:
plane = self.get_profile_plane(
@@ -750,7 +772,9 @@ class DumbProfileJoiner:
furthest_plane if is_relating else closest_plane,
z_inwards=False if is_relating else True,
)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
intersect = mathutils.geometry.intersect_line_plane(
axis1[0], axis1[1], plane.translation, plane.col[2].to_3d()
)
max_dim = self.get_max_bound_box_dimension(profile1)
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
self.clippings.append(
+2 -2
View File
@@ -31,11 +31,11 @@ def calculate_quantities(usecase_path, ifc_file: ifcopenshell.file, settings):
return
task = next(e for e in ifc_file.get_inverse(element) if e.is_a("IfcTask"))
qto = ifcopenshell.api.pset.add_qto(
ifc_file, should_run_listeners=False, product=task, name="Qto_TaskBaseQuantities"
ifc_file, should_run_listeners=False, product=task, name="Qto_TaskBaseQuantities" # ty:ignore[unknown-argument]
)
ifcopenshell.api.pset.edit_qto(
ifc_file,
should_run_listeners=False,
should_run_listeners=False, # ty:ignore[unknown-argument]
qto=qto,
properties={
"StandardWork": ifcopenshell.util.date.ifc2datetime(element.ScheduleDuration).days,
@@ -1268,27 +1268,6 @@ class DumbWallJoiner:
bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=wall2)
return wall2
def join_Z(self, wall1, slab2):
element1 = tool.Ifc.get_entity(wall1)
element2 = tool.Ifc.get_entity(slab2)
for rel in element1.ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements" and rel.Description == "TOP":
ifcopenshell.api.geometry.disconnect_element(
tool.Ifc.get(),
relating_element=rel.RelatingElement,
related_element=element1,
)
ifcopenshell.api.geometry.connect_element(
tool.Ifc.get(),
relating_element=element2,
related_element=element1,
description="TOP",
)
tool.Model.recreate_wall(element1, wall1)
def set_axis(self, wall, p1, p2):
axis = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW")
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
@@ -1333,29 +1312,6 @@ class DumbWallJoiner:
self.set_axis(element1, p1, p2)
tool.Model.recreate_wall(element1, wall1)
def join_T(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None:
element1 = tool.Ifc.get_entity(wall1)
element2 = tool.Ifc.get_entity(wall2)
axis1 = tool.Model.get_wall_axis(wall1)
axis2 = tool.Model.get_wall_axis(wall2)
intersect = tool.Cad.intersect_edges(axis1["reference"], axis2["reference"])
if intersect:
intersect, _ = intersect
else:
return
connection = "ATEND" if tool.Cad.edge_percent(intersect, axis1["reference"]) > 0.5 else "ATSTART"
ifcopenshell.api.geometry.connect_path(
tool.Ifc.get(),
related_element=element1,
relating_element=element2,
relating_connection="ATPATH",
related_connection=connection,
description="BUTT",
)
tool.Model.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
def connect(self, obj1: bpy.types.Object, obj2: bpy.types.Object) -> None:
wall1 = tool.Ifc.get_entity(obj1)
wall2 = tool.Ifc.get_entity(obj2)
@@ -101,7 +101,7 @@ class NestDecorator:
cls.is_installed = False
def dotted_line_shader(self):
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty:ignore[too-many-positional-arguments]
vert_out.smooth("FLOAT", "v_ArcLength")
shader_info = gpu.types.GPUShaderCreateInfo()
@@ -321,7 +321,7 @@ def get_gross_perimeter(o: bpy.types.Object) -> float:
return gross_perimeter
def get_space_net_perimeter(obj: bpy.types.Object) -> float:
def get_space_net_perimeter(obj: bpy.types.Object) -> None:
pass
@@ -619,7 +619,7 @@ class SelectFilterElements(bpy.types.Operator):
return {"FINISHED"}
class ApplyFilterFromText(Operator, tool.Ifc.Operator):
class ApplyFilterFromText(Operator):
bl_idname = "bim.apply_filter_from_text"
bl_label = "Apply Filter Configuration"
bl_description = "Apply the JSON filter configuration from the current text block"
@@ -1440,7 +1440,7 @@ class ShowAllElements(Operator):
return {"FINISHED"}
class SelectSimilar(Operator, tool.Ifc.Operator):
class SelectSimilar(Operator):
bl_idname = "bim.select_similar"
bl_label = "Select Similar"
bl_options = {"REGISTER", "UNDO"}
@@ -28,6 +28,7 @@ import ifcopenshell.util.representation
import ifcopenshell.util.unit
import ifcopenshell.util.unit as ifcunit
import numpy as np
import numpy.typing as npt
from mathutils import Vector
import bonsai.tool as tool
@@ -478,7 +479,7 @@ class ShaderInfo:
"""get the args to the point shader"""
location = np.array(location)
indices = []
direction_dict = {
direction_dict: dict[str, tuple[npt.NDArray, ...]] = {
"fx": (np.array((1, 0, 0)), np.array((0, 1, 0)), np.array((0, 0, 1))),
"fy": (np.array((0, 1, 0)), np.array((1, 0, 0)), np.array((0, 0, 1))),
"fz": (np.array((0, 0, 1)), np.array((0, 1, 0)), np.array((1, 0, 0))),
@@ -83,7 +83,7 @@ class DecorationShader:
PARALLEL DISTRIBUTED FORCE,
DISTRIBUTED MOMENT,
"""
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty:ignore[too-many-positional-arguments]
vert_out.smooth("VEC3", "forces")
vert_out.smooth("VEC3", "co")
@@ -203,7 +203,7 @@ class DecorationShader:
"""param: pattern: type of pattern
SINGLE FORCE,
SINGLE MOMENT"""
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty: ignore[too-many-positional-arguments]
vert_out.smooth("VEC3", "co")
shader_info = gpu.types.GPUShaderCreateInfo()
@@ -253,7 +253,7 @@ class DecorationShader:
def get_planar_shader(self) -> gpu.types.GPUShader:
"""shader for planar loads"""
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty: ignore[too-many-positional-arguments]
vert_out.smooth("VEC3", "co")
shader_info = gpu.types.GPUShaderCreateInfo()
-2
View File
@@ -787,7 +787,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
doc: DocPreferences
default_parameters: DefaultParameters
container_hide_show_isolate: bool
mass_time_units_in_wizard: bool
chain_filter_with_set_operations: bool
save_metadata_blend_file: bool
metadata_blend_file_suffix: str
@@ -986,7 +985,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "container_hide_show_isolate")
layout.prop(self, "mass_time_units_in_wizard")
row = layout.row(align=True)
row.prop(self, "chain_filter_with_set_operations")
row.operator("bim.open_uri", text="", icon="HELP").uri = "https://community.osarch.org/discussion/3270"
+11 -61
View File
@@ -51,21 +51,11 @@ def add_instance_flooring_covering_from_cursor(
if isinstance(space_polygon, str):
return
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, polygon_is_si=True)
name = "Covering"
mesh = spatial.get_named_mesh_from_bmesh(name=name, bmesh=bm)
obj = spatial.get_named_obj_from_mesh(name, mesh)
obj = spatial.create_object("Covering")
spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj)
spatial.translate_obj_to_z_location(obj, z)
points = spatial.get_2d_vertices_from_obj(obj)
points = spatial.get_scaled_2d_vertices(points)
spatial.assign_type_to_obj(obj)
spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
body = spatial.get_body_representation(obj)
spatial.regen_obj_representation(obj, body)
spatial.set_covering_representation_from_polygon(obj, space_polygon, polygon_is_si=True)
def add_instance_ceiling_covering_from_cursor(
@@ -95,21 +85,11 @@ def add_instance_ceiling_covering_from_cursor(
if isinstance(space_polygon, str):
return
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, polygon_is_si=True)
name = "Covering"
mesh = spatial.get_named_mesh_from_bmesh(name=name, bmesh=bm)
obj = spatial.get_named_obj_from_mesh(name, mesh)
obj = spatial.create_object("Covering")
spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj)
spatial.translate_obj_to_z_location(obj, z + ceiling_height)
points = spatial.get_2d_vertices_from_obj(obj)
points = spatial.get_scaled_2d_vertices(points)
spatial.assign_type_to_obj(obj)
spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
body = spatial.get_body_representation(obj)
spatial.regen_obj_representation(obj, body)
spatial.set_covering_representation_from_polygon(obj, space_polygon, polygon_is_si=True)
def regen_selected_covering_object(root: type[tool.Root], spatial: type[tool.Spatial]) -> None:
@@ -127,19 +107,7 @@ def regen_selected_covering_object(root: type[tool.Root], spatial: type[tool.Spa
if isinstance(space_polygon, str):
return
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, polygon_is_si=True)
name = "Aux"
mesh = spatial.get_named_mesh_from_bmesh(name=name, bmesh=bm)
mesh = spatial.get_transformed_mesh_from_local_to_global(mesh)
obj = spatial.get_named_obj_from_mesh(name, mesh)
points = spatial.get_2d_vertices_from_obj(obj)
points = spatial.get_scaled_2d_vertices(points)
spatial.assign_swept_area_outer_curve_from_2d_vertices(active_obj, vertices=points)
body = spatial.get_body_representation(active_obj)
spatial.regen_obj_representation(active_obj, body)
spatial.set_covering_representation_from_polygon(active_obj, space_polygon, polygon_is_si=True)
# TODO CHECK IF IT IS POSSIBLE TO CREATE ONLY ONE CORE FUNCTION FOR _FROM_WALLS
@@ -151,22 +119,13 @@ def add_instance_flooring_coverings_from_walls(root: type[tool.Root], spatial: t
union = spatial.get_union_shape_from_selected_objects()
for i, linear_ring in enumerate(union.interiors):
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
bm = spatial.get_bmesh_from_polygon(poly, h=0, polygon_is_si=False)
name = "Covering" + str(i)
obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm)
spatial.set_obj_origin_to_bboxcenter(obj)
obj = spatial.create_object(name)
spatial.set_obj_origin_to_polygon_center(obj, poly, polygon_is_si=False)
spatial.translate_obj_to_z_location(obj, z)
points = spatial.get_2d_vertices_from_obj(obj)
points = spatial.get_scaled_2d_vertices(points)
spatial.assign_type_to_obj(obj)
spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
body = spatial.get_body_representation(obj)
spatial.regen_obj_representation(obj, body)
spatial.set_covering_representation_from_polygon(obj, poly, polygon_is_si=False)
def add_instance_ceiling_coverings_from_walls(
@@ -179,22 +138,13 @@ def add_instance_ceiling_coverings_from_walls(
union = spatial.get_union_shape_from_selected_objects()
for i, linear_ring in enumerate(union.interiors):
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
bm = spatial.get_bmesh_from_polygon(poly, h=0, polygon_is_si=False)
name = "Covering" + str(i)
obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm)
spatial.set_obj_origin_to_bboxcenter(obj)
obj = spatial.create_object(name)
spatial.set_obj_origin_to_polygon_center(obj, poly, polygon_is_si=False)
spatial.translate_obj_to_z_location(obj, z)
points = spatial.get_2d_vertices_from_obj(obj)
points = spatial.get_scaled_2d_vertices(points)
spatial.assign_type_to_obj(obj)
spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
body = spatial.get_body_representation(obj)
spatial.regen_obj_representation(obj, body)
spatial.set_covering_representation_from_polygon(obj, poly, polygon_is_si=False)
class NoDefaultContainer(Exception):
+1 -4
View File
@@ -432,10 +432,7 @@ def update_drawing_name(
if drawing_tool.get_name(drawing) != name:
ifc.run("attribute.edit_attributes", product=drawing, attributes={"Name": name})
# Update the camera object name
camera = ifc.get_object(drawing)
if camera and camera.name != name:
camera.name = name
drawing_tool.set_camera_name(drawing, name)
group = drawing_tool.get_drawing_group(drawing)
if drawing_tool.get_name(group) != name:
+1 -43
View File
@@ -109,7 +109,7 @@ def align_walls(
align_type: AlignType,
):
reference_obj = blender.get_active_object(is_selected=True)
if not (e := ifc.get_entity(reference_obj) or not model.get_usage_type(e) == "LAYER2"):
if not reference_obj or not (e := ifc.get_entity(reference_obj)) or not model.get_usage_type(e) == "LAYER2":
reference_obj = None
objs = [
o
@@ -159,48 +159,6 @@ def extend_wall_to_slab(
model.reload_body_representation(wall_objs)
def join_walls_TZ(
ifc: type[tool.Ifc],
blender: type[tool.Blender],
geometry: type[tool.Geometry],
joiner: DumbWallJoiner,
model: type[tool.Model],
) -> None:
selected_objs = [
o
for o in blender.get_selected_objects()
if (e := ifc.get_entity(o)) and model.get_usage_type(e) in ("LAYER2", "LAYER3")
]
if len(selected_objs) < 2:
raise RequireAtLeastTwoLayeredElements(
"Two or more vertically or horizontally layered elements must be selected to connect their paths together"
)
for obj in selected_objs:
geometry.clear_scale(obj)
elements = [ifc.get_entity(o) for o in blender.get_selected_objects()]
layer2_elements = []
layer3_elements = []
for element in elements:
usage = model.get_usage_type(element)
if usage == "LAYER2":
layer2_elements.append(element)
elif usage == "LAYER3":
layer3_elements.append(element)
if layer3_elements:
target = ifc.get_object(layer3_elements[0])
for element in layer2_elements:
joiner.join_Z(ifc.get_object(element), target)
else:
if not (active_obj := blender.get_active_object()):
active_obj = selected_objs[0]
for obj in selected_objs:
if obj == active_obj:
continue
joiner.join_T(obj, active_obj)
class RequireTwoWallsError(Exception):
pass
+8 -12
View File
@@ -219,13 +219,8 @@ def generate_space(
else:
assert space_polygon
bm = spatial.get_bmesh_from_polygon(space_polygon, h=h, polygon_is_si=True)
mesh = spatial.get_named_mesh_from_bmesh(name="Space", bmesh=bm)
if element and element.is_a("IfcSpace"):
mesh = spatial.get_transformed_mesh_from_local_to_global(mesh)
spatial.edit_active_space_obj_from_mesh(mesh)
spatial.set_space_representation_from_polygon(active_obj, element, space_polygon, h, polygon_is_si=True)
spatial.translate_obj_to_z_location(active_obj, z)
else:
if relating_type:
@@ -233,12 +228,13 @@ def generate_space(
else:
name = "Space"
obj = spatial.get_named_obj_from_mesh(name, mesh)
obj = spatial.create_object(name)
spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj)
spatial.translate_obj_to_z_location(obj, z)
spatial.assign_ifcspace_class_to_obj(obj)
element = ifc.get_entity(obj)
spatial.set_space_representation_from_polygon(obj, element, space_polygon, h, polygon_is_si=True)
if relating_type:
spatial.assign_relating_type_to_element(ifc, type, element, relating_type)
@@ -257,16 +253,16 @@ def generate_spaces_from_walls(
for i, linear_ring in enumerate(union.interiors):
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
bm = spatial.get_bmesh_from_polygon(poly, h, polygon_is_si=False)
name = "Space" + str(i)
obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm)
spatial.set_obj_origin_to_bboxcenter_and_zero_elevation(obj)
obj = spatial.create_object(name)
spatial.set_obj_origin_to_polygon_center(obj, poly, polygon_is_si=False)
spatial.translate_obj_to_z_location(obj, z)
spatial.assign_ifcspace_class_to_obj(obj)
element = ifc.get_entity(obj)
spatial.set_space_representation_from_polygon(obj, element, poly, h, polygon_is_si=False)
def toggle_space_visibility(ifc: type[tool.Ifc], spatial: type[tool.Spatial]) -> None:
model = ifc.get()
+15 -13
View File
@@ -349,7 +349,10 @@ class Drawing:
def enable_editing_text(cls, obj): pass
def ensure_unique_drawing_name(cls, name): pass
def ensure_unique_identification(cls, identification): pass
def export_font_size(cls, obj): pass
def export_symbol(cls, obj): pass
def export_text_literal_attributes(cls, obj): pass
def export_wrap_length(cls, obj): pass
def generate_drawing_matrix(cls, target_view, location_hint): pass
def generate_drawing_name(cls, target_view, location_hint): pass
def generate_reference_attributes(cls, reference, **attributes): pass
@@ -402,6 +405,7 @@ class Drawing:
def run_root_assign_class(cls, obj=None, ifc_class=None, predefined_type=None, should_add_representation=True, context=None, ifc_representation_class=None): pass
def run_type_assign_type(cls, element=None, relating_type=None): pass
def select_assigned_product(cls, drawing): pass
def set_camera_name(cls, drawing, name): pass
def set_drawing_collection_name(cls, drawing, collection): pass
def set_name(cls, element, name): pass
def setup_annotation_object(cls, obj, object_type): pass
@@ -620,7 +624,10 @@ class Model:
def import_rectangle(cls, obj, position, profile): pass
def load_openings(cls, openings): pass
def purge_scene_openings(cls): pass
def recalculate_walls(cls, objs): pass
def regenerate_array(cls, parent, data): pass
def regenerate_profile(cls, obj): pass
def regenerate_slab(cls, obj): pass
def reload_body_representation(cls, obj_or_objects): pass
def replace_object_ifc_representation(cls, ifc_file, ifc_context, obj, new_representation): pass
@@ -992,14 +999,12 @@ class Spatial:
def get_purged_inner_holes_poly(cls, union_geom, min_area): pass
def get_poly_valid_interior_list(cls, poly, min_area, interiors_list): pass
def get_buffered_poly_from_linear_ring(cls, linear_ring): pass
def get_bmesh_from_polygon(cls, poly, h, polygon_is_si=False): pass
def get_named_obj_from_bmesh(cls, name, bmesh): pass
def get_named_obj_from_mesh(cls, name, mesh): pass
def get_named_mesh_from_bmesh(cls, name, bmesh): pass
def get_transformed_mesh_from_local_to_global(cls, mesh): pass
def edit_active_space_obj_from_mesh(cls, mesh): pass
def set_obj_origin_to_bboxcenter(cls, obj): pass
def set_obj_origin_to_bboxcenter_and_zero_elevation(cls, obj): pass
def get_2d_vertices_from_polygon(cls, poly, obj, polygon_is_si=True): pass
def set_extrusion_representation_from_polygon(cls, obj, element, poly, depth_ifc, polygon_is_si=True): pass
def set_space_representation_from_polygon(cls, obj, element, poly, h, polygon_is_si=True): pass
def set_covering_representation_from_polygon(cls, obj, poly, polygon_is_si=True): pass
def create_object(cls, name): pass
def set_obj_origin_to_polygon_center(cls, obj, poly, polygon_is_si=True): pass
def set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj): pass
def get_selected_objects(cls): pass
def get_active_obj(cls): pass
@@ -1007,14 +1012,9 @@ class Spatial:
def get_active_obj_height(cls): pass
def get_relating_type_id(cls): pass
def translate_obj_to_z_location(cls, obj, z): pass
def get_2d_vertices_from_obj(cls, obj): pass
def get_scaled_2d_vertices(cls, points): pass
def assign_swept_area_outer_curve_from_2d_vertices(cls, obj, vertices): pass
def get_body_representation(cls, obj): pass
def assign_ifcspace_class_to_obj(cls, obj): pass
def assign_type_to_obj(cls, obj): pass
def assign_relating_type_to_element(cls, ifc, type, element, relating_type): pass
def regen_obj_representation(cls, obj, body): pass
def toggle_spaces_visibility_wired_and_textured(cls, spaces): pass
def toggle_hide_spaces(cls, spaces): pass
def set_default_container(cls, container): pass
@@ -1119,6 +1119,8 @@ class Type:
def get_representation_context(cls, representation): pass
def get_type_occurrences(cls, element_type): pass
def has_material_usage(cls, element): pass
def record_material_usage_attributes(cls, element): pass
def restore_material_usage_attributes(cls, element, usage_attributes): pass
def run_geometry_add_representation(cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None): pass
def run_geometry_switch_representation(cls, obj=None, representation=None): pass
+28 -9
View File
@@ -49,21 +49,40 @@ class Aggregate(bonsai.core.tool.Aggregate):
related_object = tool.Ifc.get_entity(related_obj)
if not relating_object or not related_object:
return False
if relating_object == related_object:
return False
is_compatible_class = False
if (relating_object.is_a("IfcElement") or relating_object.is_a("IfcElementType")) and related_object.is_a(
"IfcElement"
):
return True
if tool.Ifc.get_schema() == "IFC2X3":
is_compatible_class = True
elif tool.Ifc.get_schema() == "IFC2X3":
if relating_object.is_a("IfcSpatialStructureElement") and related_object.is_a("IfcSpatialStructureElement"):
return True
if relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialStructureElement"):
return True
is_compatible_class = True
elif relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialStructureElement"):
is_compatible_class = True
else:
if relating_object.is_a("IfcSpatialElement") and related_object.is_a("IfcSpatialElement"):
return True
if relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialElement"):
return True
return False
is_compatible_class = True
elif relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialElement"):
is_compatible_class = True
if not is_compatible_class:
return False
# Prevent cyclic references: walk up the full hierarchy from the
# proposed parent and reject if we encounter the proposed child.
ancestor = ifcopenshell.util.element.get_parent(relating_object)
seen = {relating_object}
while ancestor:
if ancestor == related_object:
return False
if ancestor in seen:
break
seen.add(ancestor)
ancestor = ifcopenshell.util.element.get_parent(ancestor)
return True
@classmethod
def has_physical_body_representation(cls, element: ifcopenshell.entity_instance) -> bool:
+6 -3
View File
@@ -32,6 +32,8 @@ import bonsai.core.tool
import bonsai.tool as tool
if TYPE_CHECKING:
from bsdd.bsdd import ClassContractV1, ClassPropertyContractV1, PropertyContractV5
from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDDictionary
@@ -39,8 +41,8 @@ class Bsdd(bonsai.core.tool.Bsdd):
default_identifier_url = "https://identifier.buildingsmart.org"
default_api_url = "https://api.bsdd.buildingsmart.org/api/"
client = bsdd.Client()
bsdd_classes: dict[str, dict] = {}
bsdd_properties: dict[str, dict] = {}
bsdd_classes: dict[str, ClassContractV1] = {}
bsdd_properties: dict[str, ClassPropertyContractV1 | PropertyContractV5] = {}
@classmethod
def identifier_url(cls) -> str:
@@ -267,7 +269,8 @@ class Bsdd(bonsai.core.tool.Bsdd):
@classmethod
def get_bsdd_property(cls, uri: str) -> dict:
if not (bsdd_property := cls.bsdd_properties.get(uri, {})):
bsdd_property = cls.client.get_property(uri, include_classes=True)
# Cache miss occurs for keyword search mode, for classes cache is prepopulated.
bsdd_property = cls.client.get_property(uri)
cls.bsdd_properties[uri] = bsdd_property
return bsdd_property
+1 -1
View File
@@ -154,7 +154,7 @@ class Cost(bonsai.core.tool.Cost):
device = aud.Device()
# chaching.mp3 is by Lucish_ CC-BY-3.0 https://freesound.org/people/Lucish_/sounds/554841/
filepath = tool.Blender.get_data_dir_path("chaching.mp3").__str__()
sound = aud.Sound(filepath)
sound = aud.Sound(filepath) # ty:ignore[too-many-positional-arguments]
device.play(sound)
@classmethod
+8
View File
@@ -857,6 +857,8 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def edit_text_literals(cls, obj: bpy.types.Object, literal_attributes: dict) -> None:
if not literal_attributes:
return
assert (element := tool.Ifc.get_entity(obj))
assert (rep := cls.get_annotation_representation(element))
to_remove = [i for i in rep.Items if i.is_a("IfcTextLiteral")]
@@ -1288,6 +1290,12 @@ class Drawing(bonsai.core.tool.Drawing):
def get_representation(cls, element, context):
return ifcopenshell.util.representation.get_representation(element, context)
@classmethod
def set_camera_name(cls, drawing: ifcopenshell.entity_instance, name: str) -> None:
camera = tool.Ifc.get_object(drawing)
if camera and camera.name != name:
camera.name = name
@classmethod
def set_drawing_collection_name(
cls, drawing: ifcopenshell.entity_instance, collection: bpy.types.Collection
+4 -3
View File
@@ -1407,8 +1407,6 @@ class Geometry(bonsai.core.tool.Geometry):
:param representation_item: item to remove.
:param element: item's element. Is used to unmark manual booleans.
"""
# NOTE: we assume it's not the last representation item
# otherwise we probably would need to remove representation too
# NOTE: a lot of shared code with `geometry.remove_representation`
ifc_file = tool.Ifc.get()
shape_aspects: list[ifcopenshell.entity_instance] = []
@@ -1467,7 +1465,10 @@ class Geometry(bonsai.core.tool.Geometry):
cls.remove_representation_items_from_shape_aspect([representation_item], shape_aspect)
if representation:
representation.Items = tuple(set(representation.Items) - {representation_item})
new_items = tuple(set(representation.Items) - {representation_item})
if not new_items:
return
representation.Items = new_items
also_consider = list(consider_inverses)
ifcopenshell.util.element.remove_deep2(ifc_file, representation_item, also_consider=also_consider)
+6 -4
View File
@@ -197,12 +197,16 @@ class Ifc(bonsai.core.tool.Ifc):
if not cls.get():
return
# Clear all per-object msgbus subscriptions at once using the dedicated
# owner. After undo/redo, per-object Python wrappers have new
# identities so clearing by individual obj would miss stale
# subscriptions registered with the old wrappers.
bpy.msgbus.clear_by_owner(bonsai.bim.handler.object_subscription_owner)
for obj in bpy.data.objects:
if obj.library:
continue
bpy.msgbus.clear_by_owner(obj)
element = cls.get_entity(obj)
if not element:
continue
@@ -217,8 +221,6 @@ class Ifc(bonsai.core.tool.Ifc):
if obj.library:
continue
bpy.msgbus.clear_by_owner(obj)
style = cls.get_entity(obj)
if not style:
continue
+1 -1
View File
@@ -284,7 +284,7 @@ class IfcGit:
if re.match("^Ifc", obj.name):
bpy.data.objects.remove(obj, do_unlink=True)
bpy.data.orphans_purge(do_recursive=True)
bpy.data.orphans_purge(do_recursive=True) # ty:ignore[unknown-argument]
settings = import_ifc.IfcImportSettings.factory(bpy.context, path_ifc, logging.getLogger("ImportIFC"))
settings.should_setup_viewport_camera = False
+17 -3
View File
@@ -45,9 +45,23 @@ class Nest(bonsai.core.tool.Nest):
related_object = tool.Ifc.get_entity(related_obj)
if not relating_object or not related_object:
return False
if relating_object.is_a("IfcElement") and related_object.is_a("IfcElement"):
return True
return False
if relating_object == related_object:
return False
is_compatible_class = relating_object.is_a("IfcElement") and related_object.is_a("IfcElement")
if not is_compatible_class:
return False
# Prevent cyclic references: walk up the full hierarchy from the
# proposed parent and reject if we encounter the proposed child.
ancestor = ifcopenshell.util.element.get_parent(relating_object)
seen = {relating_object}
while ancestor:
if ancestor == related_object:
return False
if ancestor in seen:
break
seen.add(ancestor)
ancestor = ifcopenshell.util.element.get_parent(ancestor)
return True
@classmethod
def disable_editing(cls, obj: bpy.types.Object) -> None:
+124 -151
View File
@@ -21,13 +21,13 @@ from __future__ import annotations
import json
from collections import defaultdict
from collections.abc import Generator, Iterable
from math import pi
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
import bmesh
import bpy
import ifcopenshell
import ifcopenshell.api.attribute
import ifcopenshell.api.geometry
import ifcopenshell.api.type
import ifcopenshell.geom
import ifcopenshell.util.classification
@@ -991,114 +991,107 @@ class Spatial(bonsai.core.tool.Spatial):
return poly
@classmethod
def get_bmesh_from_polygon(cls, poly: Polygon, h: float, polygon_is_si: bool = False) -> bmesh.types.BMesh:
"""
:param h: Height, in meters.
:param polygon_is_si: Should be True if `poly` is defined in meters.
"""
mat = Matrix()
bm = bmesh.new()
bm.verts.index_update()
bm.edges.index_update()
mat_invert = mat.inverted()
si_conversion = 1.0 if polygon_is_si else ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
new_verts = [
# bm.verts.new(mat_invert @ (Vector([v[0], v[1], 0]) * si_conversion)) for v in poly.exterior.coords[0:-1]
bm.verts.new(mat_invert @ (Vector([v[0], v[1], 0]) * si_conversion))
for v in shapely.get_exterior_ring(poly).coords[0:-1]
]
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
bm.verts.index_update()
bm.edges.index_update()
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
bmesh.ops.triangle_fill(bm, edges=bm.edges)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges)
if h != 0:
extrusion = bmesh.ops.extrude_face_region(bm, geom=bm.faces)
extruded_verts = [g for g in extrusion["geom"] if isinstance(g, bmesh.types.BMVert)]
bmesh.ops.translate(bm, vec=[0.0, 0.0, h], verts=extruded_verts)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
return bm
@classmethod
def get_named_obj_from_bmesh(cls, name: str, bmesh: bmesh.types.BMesh) -> bpy.types.Object:
mesh = cls.get_named_mesh_from_bmesh(name, bmesh)
obj = cls.get_named_obj_from_mesh(name, mesh)
return obj
@classmethod
def get_named_obj_from_mesh(cls, name: str, mesh: bpy.types.Mesh) -> bpy.types.Object:
def create_object(cls, name: str) -> bpy.types.Object:
mesh = bpy.data.meshes.new(name=name)
obj = bpy.data.objects.new(name, mesh)
return obj
@classmethod
def get_named_mesh_from_bmesh(cls, name: str, bmesh: bmesh.types.BMesh) -> bpy.types.Mesh:
mesh = bpy.data.meshes.new(name=name)
bmesh.to_mesh(mesh)
bmesh.free()
return mesh
def set_obj_origin_to_polygon_center(
cls, obj: bpy.types.Object, poly: Polygon, polygon_is_si: bool = True
) -> None:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
centroid = poly.centroid
if polygon_is_si:
obj.location = Vector((centroid.x, centroid.y, 0))
else:
obj.location = Vector((centroid.x * unit_scale, centroid.y * unit_scale, 0))
@classmethod
def get_transformed_mesh_from_local_to_global(cls, mesh: bpy.types.Mesh) -> bpy.types.Mesh:
active_obj = cls.get_active_obj()
mat = active_obj.matrix_world
mesh.transform(mat.inverted())
mesh.update()
return mesh
def get_2d_vertices_from_polygon(
cls,
poly: Polygon,
obj: bpy.types.Object,
polygon_is_si: bool = True,
) -> list[list[float]]:
"""Convert a world-space shapely polygon to 2D vertices in obj's local space, in IFC file units.
:param poly: The polygon in world space.
:param obj: The Blender object whose local space is used.
:param polygon_is_si: True if polygon coords are in SI, False if in IFC file units.
:return: List of [x, y] coordinates (not closed).
"""
ifc_file = tool.Ifc.get()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
bpy.context.view_layer.update()
mat_inv = obj.matrix_world.inverted()
coords_2d = []
for v in shapely.get_exterior_ring(poly).coords[:-1]:
world_si = Vector((v[0], v[1], 0))
if not polygon_is_si:
world_si = world_si * unit_scale
local_si = mat_inv @ world_si
coords_2d.append([local_si.x / unit_scale, local_si.y / unit_scale])
return coords_2d
@classmethod
def edit_active_space_obj_from_mesh(cls, mesh: bpy.types.Mesh) -> None:
active_obj = bpy.context.active_object
old_mesh = active_obj.data
old_mesh_name = old_mesh.name
assert active_obj and isinstance(old_mesh, bpy.types.Mesh)
tool.Geometry.get_mesh_props(mesh).ifc_definition_id = tool.Geometry.get_mesh_props(old_mesh).ifc_definition_id
tool.Geometry.change_object_data(active_obj, mesh, is_global=True)
tool.Ifc.edit(active_obj)
tool.Blender.remove_data_block(old_mesh)
# Rename after old mesh is removed to avoid .001 suffix.
mesh.name = old_mesh_name
def set_extrusion_representation_from_polygon(
cls,
obj: bpy.types.Object,
element: ifcopenshell.entity_instance,
poly: Polygon,
depth_ifc: float,
polygon_is_si: bool = True,
) -> None:
"""Create or replace the IFC body representation from a polygon extrusion.
:param obj: The Blender object.
:param element: The IFC product entity.
:param poly: The polygon in world space.
:param depth_ifc: The extrusion depth in IFC file units.
:param polygon_is_si: True if polygon coords are in SI, False if in IFC file units.
"""
ifc_file = tool.Ifc.get()
builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
coords_2d = cls.get_2d_vertices_from_polygon(poly, obj, polygon_is_si)
curve = builder.polyline(coords_2d, closed=True)
item = builder.extrude(curve, magnitude=depth_ifc)
old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if old_body:
context = old_body.ContextOfItems
ifcopenshell.api.geometry.unassign_representation(ifc_file, product=element, representation=old_body)
ifcopenshell.api.geometry.remove_representation(ifc_file, representation=old_body)
else:
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
new_body = builder.get_representation(context, item)
ifcopenshell.api.geometry.assign_representation(ifc_file, product=element, representation=new_body)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=new_body,
)
@classmethod
def set_obj_origin_to_bboxcenter(cls, obj: bpy.types.Object) -> None:
mat = obj.matrix_world
inverted = mat.inverted()
local_bbox_center = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector())
global_bbox_center = mat @ local_bbox_center
def set_space_representation_from_polygon(
cls,
obj: bpy.types.Object,
element: ifcopenshell.entity_instance,
poly: Polygon,
h: float,
polygon_is_si: bool = True,
) -> None:
"""Create or replace the IFC body representation of a space from a polygon.
oldLoc = obj.location
newLoc = global_bbox_center
diff = newLoc - oldLoc
for vert in obj.data.vertices:
aux_vector = mat @ vert.co
aux_vector = aux_vector - diff
vert.co = inverted @ aux_vector
obj.location = newLoc
@classmethod
def set_obj_origin_to_bboxcenter_and_zero_elevation(cls, obj: bpy.types.Object) -> None:
mat = obj.matrix_world
inverted = mat.inverted()
local_bbox_center = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector())
global_bbox_center = mat @ local_bbox_center
global_obj_origin = global_bbox_center
global_obj_origin.z = 0
oldLoc = obj.location
newLoc = global_obj_origin
diff = newLoc - oldLoc
for vert in obj.data.vertices:
aux_vector = mat @ vert.co
aux_vector = aux_vector - diff
vert.co = inverted @ aux_vector
obj.location = newLoc
:param h: The height in SI (meters).
"""
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
cls.set_extrusion_representation_from_polygon(obj, element, poly, h / unit_scale, polygon_is_si)
@classmethod
def set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj: bpy.types.Object) -> None:
@@ -1145,65 +1138,53 @@ class Spatial(bonsai.core.tool.Spatial):
if z != 0:
obj.location = obj.location + Vector((0, 0, z))
@classmethod
def get_2d_vertices_from_obj(cls, obj: bpy.types.Object) -> list[tuple]:
points = []
vectors = [v.co for v in obj.data.vertices.values()]
for vector in vectors:
points.append(vector.xy)
points.append(vectors[0].xy)
return points
@classmethod
def get_scaled_2d_vertices(cls, points: list[Vector]) -> list[tuple[float, float]]:
model = tool.Ifc.get()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(model)
_points = []
for p in points:
_p = list(p)
_p[0] /= unit_scale
_p[1] /= unit_scale
_points.append(_p)
return _points
@classmethod
def assign_swept_area_outer_curve_from_2d_vertices(cls, obj: bpy.types.Object, vertices: list[Vector]) -> None:
body = cls.get_body_representation(obj)
model = tool.Ifc.get()
extrusion = tool.Model.get_extrusion(body)
area = extrusion.SweptArea
old_area = area.OuterCurve
builder = ifcopenshell.util.shape_builder.ShapeBuilder(model)
outer_curve = builder.polyline(vertices, closed=True)
area.OuterCurve = outer_curve
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_area)
@classmethod
def get_body_representation(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
element = tool.Ifc.get_entity(obj)
return ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
@classmethod
def assign_ifcspace_class_to_obj(cls, obj: bpy.types.Object) -> None:
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace")
bonsai.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class="IfcSpace",
should_add_representation=False,
)
@classmethod
def assign_type_to_obj(cls, obj: bpy.types.Object) -> None:
# TODO this code looks in the wrong spot and suspicious
props = tool.Model.get_model_props()
ifc_file = tool.Ifc.get()
relating_type_id = props.relating_type_id
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
relating_type = ifc_file.by_id(int(relating_type_id))
ifc_class = relating_type.is_a()
instance_class = ifcopenshell.util.type.get_applicable_entities(ifc_class, ifc_file.schema)[0]
bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class)
bonsai.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class=instance_class,
should_add_representation=False,
)
element = tool.Ifc.get_entity(obj)
assert element
ifcopenshell.api.type.assign_type(ifc_file, related_objects=[element], relating_type=relating_type)
@classmethod
def set_covering_representation_from_polygon(
cls,
obj: bpy.types.Object,
poly: Polygon,
polygon_is_si: bool = True,
) -> None:
"""Create the covering body representation from a polygon, extruded by the type's material layer thickness."""
element = tool.Ifc.get_entity(obj)
relating_type = ifcopenshell.util.element.get_type(element)
material = ifcopenshell.util.element.get_material(relating_type, should_skip_usage=True)
depth = 0.0
if material and material.is_a("IfcMaterialLayerSet"):
depth = sum(layer.LayerThickness for layer in material.MaterialLayers)
cls.set_extrusion_representation_from_polygon(obj, element, poly, depth, polygon_is_si)
@classmethod
def assign_relating_type_to_element(
cls,
@@ -1214,14 +1195,6 @@ class Spatial(bonsai.core.tool.Spatial):
) -> None:
bonsai.core.type.assign_type(ifc, tool.Model, type, element=element, type=relating_type)
@classmethod
def regen_obj_representation(cls, obj: bpy.types.Object, body: ifcopenshell.entity_instance) -> None:
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=body,
)
@classmethod
def set_space_visibility(cls, is_visible: bool) -> None:
@@ -19,4 +19,5 @@ This chapter covers how you can help contribute to Bonsai.
undo_system
writing_docs
debugging
maintenance
ide/index
@@ -0,0 +1,65 @@
Maintenance
===========
This page documents what needs to be updated in various maintenance scenarios.
Python Version Added or Removed
--------------------------------
When adding or removing a supported Python version, update the following:
.. list-table::
:header-rows: 1
* - File
- What to update
* - ``.github/workflows/ci-black-formatting.yaml``
- ``MIN_IOS_PY_VERSION``
* - ``.github/workflows/ci-ifcopenshell-python-pypi.yml``
- ``pyver`` matrix
* - ``.github/workflows/ci-ifcopenshell-python.yml``
- ``pyver`` matrix
* - ``nix/build-all.py``
- ``PYTHON_VERSIONS`` list
* - ``src/bsdd/pyproject.toml``
- ``requires-python``
* - ``src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst``
- add or remove the row in the ZIP packages table
* - ``src/ifcopenshell-python/Makefile``
- ``SUPPORTED_PYVERSIONS``
* - ``src/ifcopenshell-python/pyproject.toml``
- ``requires-python``
* - ``src/ifcopenshell-python/test/test_package.py``
- ``SUPPORTED_PY_VERSIONS`` tuple
* - ``win/build-all-win.py``
- ``PYTHON_VERSIONS`` list
Blender Version Updated
-----------------------
When a new Blender version is released and supported:
.. list-table::
:header-rows: 1
* - File
- What to update
* - ``.github/workflows/ci-bonsai-daily.yml``
- Blender download URL
Blender's Bundled Python Version Updated
-----------------------------------------
When Blender ships with a new Python version:
.. list-table::
:header-rows: 1
* - File
- What to update
* - ``.github/workflows/ci-black-formatting.yaml``
- ``MIN_BLENDER_PY_VERSION``
* - ``src/bonsai/Makefile``
- ``SUPPORTED_PYVERSIONS``
* - ``src/bonsai/scripts/dev_environment.py``
- ``PYTHON_VERSION`` mapping (Blender version, bundled Python version)
+23
View File
@@ -0,0 +1,23 @@
"""Clone or update Bonsai external dependencies.
Must be run from the repository root.
"""
import subprocess
from pathlib import Path
DEPS = [
("https://projects.blender.org/pioverfour/sun_position.git", "sun_position"),
("https://github.com/kevancress/MeasureIt_ARCH", "MeasureIt_ARCH"),
("https://github.com/nortikin/sverchok.git", "sverchok"),
]
base = Path("src/bonsai/external_dependencies")
base.mkdir(parents=True, exist_ok=True)
for url, name in DEPS:
path = base / name
if not path.exists():
subprocess.check_call(["git", "clone", url, str(path)])
else:
subprocess.check_call(["git", "-C", str(path), "pull", "--rebase"])
+2 -2
View File
@@ -273,10 +273,10 @@ if BPY_IS_LOADED:
f"Couldn't find locale path in the source directory, creating dummy directory: {source_locale_path}.",
)
from ui_translate.settings import ( # pyright: ignore[reportMissingImports]
from ui_translate.settings import ( # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
settings as ui_translate_settings,
)
from ui_translate.update_ui import ( # pyright: ignore[reportMissingImports]
from ui_translate.update_ui import ( # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
UI_OT_i18n_updatetranslation_init_settings,
)
+3 -1
View File
@@ -23,7 +23,9 @@ import bpy
# sys.path.append('C:\Program Files\Python37\Lib\site-packages')
import lxml.etree
from bspy import Gbxml # pyright: ignore[reportMissingImports]
from bspy import ( # ty: ignore[unresolved-import]
Gbxml, # pyright: ignore[reportMissingImports]
)
class GbxmlExporter:
@@ -22,7 +22,7 @@
from math import pi
from pathlib import Path
import boltspy as bolts # pyright: ignore[reportMissingImports]
import boltspy as bolts # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
import ifcopenshell.api
import ifcopenshell.api.material
import ifcopenshell.api.project
+1 -1
View File
@@ -31,7 +31,7 @@ import ifcopenshell.api.spatial
import ifcopenshell.api.unit
import ifcopenshell.guid
import numpy as np
import pymeshlab # pyright: ignore[reportMissingImports]
import pymeshlab # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
class Obj2Ifc:
+1 -1
View File
@@ -31,7 +31,7 @@ import ifcopenshell.api.spatial
import ifcopenshell.api.unit
import ifcopenshell.guid
import numpy as np
import pywavefront # pyright: ignore[reportMissingImports]
import pywavefront # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
class Obj2Ifc:
+108 -3
View File
@@ -2,7 +2,7 @@
Feature: Covering
Covers covering tool.
Scenario: Execute generate flooring coverings from walls
Scenario: Add flooring from walls
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
@@ -24,9 +24,9 @@ Scenario: Execute generate flooring coverings from walls
And the cursor is at "0,2.0,0"
And I set "scene.BIMModelProperties.length" to "1.9"
And I press "bim.add_occurrence"
# add_instance_flooring_coverings_from_walls is expecting FLOORING predefined type.
# Set COV30 predefined type to FLOORING.
And the object "IfcCoveringType/COV30" is selected
And I look at the "Attributes" panel
And I look at the "Object Attributes" panel
And I click "Edit"
And I set the "PredefinedType" property to "FLOORING"
And I click "Save Attributes"
@@ -42,3 +42,108 @@ Scenario: Execute generate flooring coverings from walls
Then the object "IfcCovering/Covering0" exists
And the object "IfcCovering/Covering0" is at "1.8,1.05,0.0"
And the object "IfcCovering/Covering0" dimensions are "3.4,1.9,0.03"
Scenario: Add ceiling from walls
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_occurrence"
And the object "IfcWall/Wall" is selected
And I press "bim.change_layer_length(length=3.6)"
And the cursor is at "3.6,0.1,3"
And I set "scene.BIMModelProperties.length" to "2.0"
And I press "bim.add_occurrence"
And the cursor is at "3.5,2.1,3"
And I set "scene.BIMModelProperties.length" to "3.5"
And I press "bim.add_occurrence"
And the cursor is at "0,2.0,0"
And I set "scene.BIMModelProperties.length" to "1.9"
And I press "bim.add_occurrence"
# Set COV30 predefined type to CEILING.
And the object "IfcCoveringType/COV30" is selected
And I look at the "Object Attributes" panel
And I click "Edit"
And I set the "PredefinedType" property to "CEILING"
And I click "Save Attributes"
# Run the operator with ceiling height = 2.7 (default).
When the object "IfcWall/Wall" is selected
And additionally the object "IfcWall/Wall.001" is selected
And additionally the object "IfcWall/Wall.002" is selected
And additionally the object "IfcWall/Wall.003" is selected
And I set "scene.BIMModelProperties.ifc_class" to "IfcCoveringType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcCoveringType') if e.Name == 'COV30'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_instance_ceiling_coverings_from_walls"
Then the object "IfcCovering/Covering0" exists
And the object "IfcCovering/Covering0" is at "1.8,1.05,2.7"
And the object "IfcCovering/Covering0" dimensions are "3.4,1.9,0.03"
Scenario: Add flooring from cursor
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_occurrence"
And the cursor is at "1.1,0,0"
And I press "bim.add_occurrence"
And the object "IfcWall/Wall.001" is selected
And I press "bim.hotkey(hotkey='S_R')"
And the cursor is at "0,.9,0"
And I press "bim.add_occurrence"
And the cursor is at "-1,0,0"
And I press "bim.add_occurrence"
And the object "IfcWall/Wall.003" is selected
And I press "bim.hotkey(hotkey='S_R')"
And the object "IfcWall/Wall.003" is moved to "0,0,0"
# Set COV30 predefined type to FLOORING.
And the object "IfcCoveringType/COV30" is selected
And I look at the "Object Attributes" panel
And I click "Edit"
And I set the "PredefinedType" property to "FLOORING"
And I click "Save Attributes"
# Generate covering from cursor inside the room.
When the cursor is at "0.5,0.5,0"
And I deselect all objects
And I set "scene.BIMModelProperties.ifc_class" to "IfcCoveringType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcCoveringType') if e.Name == 'COV30'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_instance_flooring_covering_from_cursor"
Then the object "IfcCovering/Covering" exists
And the object "IfcCovering/Covering" dimensions are "1,0.8,0.03"
Scenario: Add ceiling from cursor
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_occurrence"
And the cursor is at "1.1,0,0"
And I press "bim.add_occurrence"
And the object "IfcWall/Wall.001" is selected
And I press "bim.hotkey(hotkey='S_R')"
And the cursor is at "0,.9,0"
And I press "bim.add_occurrence"
And the cursor is at "-1,0,0"
And I press "bim.add_occurrence"
And the object "IfcWall/Wall.003" is selected
And I press "bim.hotkey(hotkey='S_R')"
And the object "IfcWall/Wall.003" is moved to "0,0,0"
# Set COV30 predefined type to CEILING.
And the object "IfcCoveringType/COV30" is selected
And I look at the "Object Attributes" panel
And I click "Edit"
And I set the "PredefinedType" property to "CEILING"
And I click "Save Attributes"
# Generate covering from cursor inside the room.
When the cursor is at "0.5,0.5,0"
And I deselect all objects
And I set "scene.BIMModelProperties.ifc_class" to "IfcCoveringType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcCoveringType') if e.Name == 'COV30'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_instance_ceiling_covering_from_cursor"
Then the object "IfcCovering/Covering" exists
And the object "IfcCovering/Covering" dimensions are "1,0.8,0.03"
@@ -422,6 +422,24 @@ Scenario: Enable editing material set item
When I press "bim.enable_editing_material_set_item(material_set_item={material_profile})"
Then nothing happens
Scenario: Edit layer item defaults null IsVentilated to FALSE in UI
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 press "bim.add_material()"
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
And the variable "layer" is "{ifc}.by_type('IfcMaterialLayer')[0].id()"
And I press "bim.enable_editing_material_set_item(material_set_item={layer})"
When I evaluate expression "attrs = bpy.context.active_object.BIMObjectMaterialProperties.material_set_item_attributes; is_vent = next(a for a in attrs if a.name == 'IsVentilated'); assert is_vent.enum_value == 'FALSE'; assert is_vent.is_null is True"
And I press "bim.edit_material_set_item(material_set_item={layer})"
Then I evaluate expression "assert {ifc}.by_id({layer}).IsVentilated is None"
Scenario: Add material set layer
Given an empty IFC project
And I add a cube
+1 -1
View File
@@ -242,7 +242,7 @@ class TemplateListItemSpy(PanelSpy):
self.spied_props: list[dict[str, Any]] = []
self.spied_operators: list[dict[str, Any]] = []
if len(signature(blender_panel.draw_item).parameters) == 8:
blender_panel.draw_item(
blender_panel.draw_item( # ty:ignore[missing-argument]
self,
bpy.context,
self,
+10 -3
View File
@@ -35,9 +35,14 @@ class TestDisableEditingText:
class TestEditText:
def test_run(self, drawing):
drawing.synchronise_ifc_and_text_attributes("obj").should_be_called()
drawing.update_text_size_pset("obj").should_be_called()
drawing.update_text_annotation_properties("obj").should_be_called()
drawing.export_text_literal_attributes("obj").should_be_called().will_return("literal_attributes")
drawing.export_font_size("obj").should_be_called().will_return("font_size")
drawing.edit_text_font_size("obj", "font_size").should_be_called()
drawing.export_wrap_length("obj").should_be_called().will_return("wrap_length")
drawing.edit_text_wrap_length("obj", "wrap_length").should_be_called()
drawing.export_symbol("obj").should_be_called().will_return("symbol")
drawing.edit_text_symbol("obj", "symbol").should_be_called()
drawing.edit_text_literals("obj", "literal_attributes").should_be_called()
drawing.disable_editing_text("obj").should_be_called()
subject.edit_text(drawing, obj="obj")
@@ -466,6 +471,7 @@ class TestRemoveDrawing:
class TestUpdateDrawingName:
def test_do_not_update_if_name_unchanged(self, ifc, drawing):
drawing.get_name("drawing").should_be_called().will_return("name")
drawing.set_camera_name("drawing", "name").should_be_called()
drawing.get_drawing_group("drawing").should_be_called().will_return("group")
drawing.get_name("group").should_be_called().will_return("name")
drawing.get_drawing_collection("drawing").should_be_called().will_return("collection")
@@ -482,6 +488,7 @@ class TestUpdateDrawingName:
def test_run(self, ifc, drawing):
drawing.get_name("drawing").should_be_called().will_return("oldname")
ifc.run("attribute.edit_attributes", product="drawing", attributes={"Name": "name"}).should_be_called()
drawing.set_camera_name("drawing", "name").should_be_called()
drawing.get_drawing_group("drawing").should_be_called().will_return("group")
drawing.get_name("group").should_be_called().will_return("oldname")
ifc.run("attribute.edit_attributes", product="group", attributes={"Name": "name"}).should_be_called()
+4 -2
View File
@@ -23,6 +23,7 @@ from test.core.bootstrap import georeference, ifc
class TestAddGeoreferencing:
def test_run(self, georeference):
georeference.add_georeferencing().should_be_called()
georeference.set_model_origin().should_be_called()
subject.add_georeferencing(georeference)
@@ -35,9 +36,10 @@ class TestEnableEditingGeoreferencing:
class TestRemoveGeoreferencing:
def test_run(self, ifc):
def test_run(self, ifc, georeference):
ifc.run("georeference.remove_georeferencing").should_be_called()
subject.remove_georeferencing(ifc)
georeference.set_model_origin().should_be_called()
subject.remove_georeferencing(ifc, georeference)
class TestDisableEditingGeoreferencing:
+5 -3
View File
@@ -22,8 +22,9 @@ from test.core.bootstrap import geometry, ifc, model, type
class TestAssignType:
def test_assigning_and_switching_to_an_existing_type_data(self, ifc, model, type):
type.record_material_usage_attributes("element").should_be_called().will_return(None)
ifc.run("type.assign_type", related_objects=["element"], relating_type="type").should_be_called()
type.has_material_usage("element").should_be_called().will_return(False)
model.get_usage_type("type").should_be_called(2).will_return(None)
ifc.get_object("type").should_be_called().will_return("type_obj")
type.get_object_data("type_obj").should_be_called().will_return("type_obj_data")
type.change_object_data("obj", "type_obj_data", is_global=False).should_be_called()
@@ -31,9 +32,10 @@ class TestAssignType:
type.disable_editing("obj").should_be_called()
subject.assign_type(ifc, model, type, element="element", type="type")
def test_assigning_and_not_changing_data_if_the_type_has_no_data(self, ifc, type):
def test_assigning_and_not_changing_data_if_the_type_has_no_data(self, ifc, model, type):
type.record_material_usage_attributes("element").should_be_called().will_return(None)
ifc.run("type.assign_type", related_objects=["element"], relating_type="type").should_be_called()
type.has_material_usage("element").should_be_called().will_return(False)
model.get_usage_type("type").should_be_called(2).will_return(None)
ifc.get_object("type").should_be_called().will_return("type_obj")
type.get_object_data("type_obj").should_be_called().will_return(None)
ifc.get_object("element").should_be_called().will_return("obj")
+37
View File
@@ -18,6 +18,7 @@
import bpy
import ifcopenshell
import ifcopenshell.api.aggregate
import ifcopenshell.api.context
import ifcopenshell.api.geometry
import ifcopenshell.api.root
@@ -99,6 +100,42 @@ class TestCanAggregate(NewFile):
subelement_obj = bpy.data.objects.new("Object", None)
assert subject.can_aggregate(element_obj, subelement_obj) is False
def test_element_cannot_aggregate_to_itself(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
element = ifc.createIfcElementAssembly()
element_obj = bpy.data.objects.new("Object", None)
tool.Ifc.link(element, element_obj)
assert subject.can_aggregate(element_obj, element_obj) is False
def test_cyclic_aggregation_is_prevented(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
assembly_a = ifc.createIfcElementAssembly()
assembly_a_obj = bpy.data.objects.new("AssemblyA", None)
tool.Ifc.link(assembly_a, assembly_a_obj)
beam = ifc.createIfcBeam()
beam_obj = bpy.data.objects.new("Beam", None)
tool.Ifc.link(beam, beam_obj)
ifcopenshell.api.aggregate.assign_object(ifc, products=[beam], relating_object=assembly_a)
assert subject.can_aggregate(beam_obj, assembly_a_obj) is False
def test_deep_cyclic_aggregation_is_prevented(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
assembly_a = ifc.createIfcElementAssembly()
assembly_a_obj = bpy.data.objects.new("AssemblyA", None)
tool.Ifc.link(assembly_a, assembly_a_obj)
assembly_b = ifc.createIfcElementAssembly()
assembly_b_obj = bpy.data.objects.new("AssemblyB", None)
tool.Ifc.link(assembly_b, assembly_b_obj)
beam = ifc.createIfcBeam()
beam_obj = bpy.data.objects.new("Beam", None)
tool.Ifc.link(beam, beam_obj)
ifcopenshell.api.aggregate.assign_object(ifc, products=[assembly_b], relating_object=assembly_a)
ifcopenshell.api.aggregate.assign_object(ifc, products=[beam], relating_object=assembly_b)
assert subject.can_aggregate(beam_obj, assembly_a_obj) is False
class TestHasPhysicalBodyRepresentation(NewFile):
def test_run(self):
+4
View File
@@ -176,6 +176,7 @@ class TestStairCalculatedParams(NewFile):
pset_data = pset_data_base.copy()
calculated_data = calculated_data_base.copy()
pset_data["custom_first_last_tread_run"] = (0.1, 0.4)
pset_data["custom_tread_lock"] = False
calculated_data["Length"] += -0.2 + 0.1
self.compare_data(pset_data, calculated_data)
@@ -183,6 +184,7 @@ class TestStairCalculatedParams(NewFile):
pset_data = pset_data_base.copy()
calculated_data = calculated_data_base.copy()
pset_data["custom_first_last_tread_run"] = (0.0, None)
pset_data["custom_tread_lock"] = False
calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each
self.compare_data(pset_data, calculated_data)
@@ -190,6 +192,7 @@ class TestStairCalculatedParams(NewFile):
pset_data = pset_data_base.copy()
calculated_data = calculated_data_base.copy()
pset_data["custom_first_last_tread_run"] = (None, 0.0)
pset_data["custom_tread_lock"] = False
calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each
self.compare_data(pset_data, calculated_data)
@@ -197,6 +200,7 @@ class TestStairCalculatedParams(NewFile):
pset_data = pset_data_base.copy()
calculated_data = calculated_data_base.copy()
pset_data["custom_first_last_tread_run"] = (0.0, 0.0)
pset_data["custom_tread_lock"] = False
calculated_data["Length"] = 0.6 # Only 2 middle treads at 0.3 each
self.compare_data(pset_data, calculated_data)
+37
View File
@@ -19,6 +19,7 @@
import bpy
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.nest
import ifcopenshell.api.spatial
import bonsai.core.tool
@@ -51,6 +52,42 @@ class TestCanNest(NewFile):
subelement_obj = bpy.data.objects.new("Object", None)
assert subject.can_nest(element_obj, subelement_obj) is False
def test_element_cannot_nest_to_itself(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
element = ifc.createIfcWall()
element_obj = bpy.data.objects.new("Object", None)
tool.Ifc.link(element, element_obj)
assert subject.can_nest(element_obj, element_obj) is False
def test_cyclic_nesting_is_prevented(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
wall_a = ifc.createIfcWall()
wall_a_obj = bpy.data.objects.new("WallA", None)
tool.Ifc.link(wall_a, wall_a_obj)
wall_b = ifc.createIfcWall()
wall_b_obj = bpy.data.objects.new("WallB", None)
tool.Ifc.link(wall_b, wall_b_obj)
ifcopenshell.api.nest.assign_object(ifc, related_objects=[wall_b], relating_object=wall_a)
assert subject.can_nest(wall_b_obj, wall_a_obj) is False
def test_deep_cyclic_nesting_is_prevented(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
wall_a = ifc.createIfcWall()
wall_a_obj = bpy.data.objects.new("WallA", None)
tool.Ifc.link(wall_a, wall_a_obj)
wall_b = ifc.createIfcWall()
wall_b_obj = bpy.data.objects.new("WallB", None)
tool.Ifc.link(wall_b, wall_b_obj)
wall_c = ifc.createIfcWall()
wall_c_obj = bpy.data.objects.new("WallC", None)
tool.Ifc.link(wall_c, wall_c_obj)
ifcopenshell.api.nest.assign_object(ifc, related_objects=[wall_b], relating_object=wall_a)
ifcopenshell.api.nest.assign_object(ifc, related_objects=[wall_c], relating_object=wall_b)
assert subject.can_nest(wall_c_obj, wall_a_obj) is False
class TestDisableEditing(NewFile):
def test_run(self):
+1 -1
View File
@@ -365,7 +365,7 @@ class TestLoadingIfcSqlite(NewFile):
sql_type="SQLite",
)
patcher.patch()
tmp_file = Path(tempfile.mktemp(suffix=".ifcsqlite"))
tmp_file = Path(tempfile.mkstemp(suffix=".ifcsqlite")[1])
ifcpatch.write(patcher.get_output(), tmp_file)
elements_with_meshes = [
+41
View File
@@ -0,0 +1,41 @@
aiohttp
beautifulsoup4
boto3
botocore
brickschema
cjio >=0.8, <0.10
debugpy
ezdxf
fake-bpy-module-latest
git+https://github.com/prochitecture/bpypolyskel
git+https://github.com/Andrej730/IFC2JSON_python.git@pyproject_toml
gitpython
isodate
lark
lxml
lxml-stubs
markdown-it-py
natsort
numpy
odfpy
openpyxl
pandas
pillow
platformdirs
pygments
pyradiance
pystache
pytest
pytest_bdd
pytest_blender
python-dateutil
python-socketio
pytz
rdflib
requests
shapely
svgwrite
typing-extensions
typst
tzfpy
xsdata
+1 -1
View File
@@ -7,7 +7,7 @@ from typing import Literal, Optional
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, model_validator
from .type_hints import *
from type_hints import *
def _lower_first(s: str) -> str:
+1
View File
@@ -28,6 +28,7 @@ dist:
mkdir -p dist
cp -r $(PACKAGE_NAME) build/
cp pyproject.toml build/
if [ -f README.md ]; then cp README.md build/; fi
ifeq ($(IS_STABLE), TRUE)
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml
ifdef IS_MODULE
+1 -1
View File
@@ -955,7 +955,7 @@ def get_unit_type_name(ifc_file: ifcopenshell.file, unit_type: str) -> Union[str
return val(unit.Currency)
def get_unit_name(ifc_file: ifcopenshell.entity_instance, unit: ifcopenshell.entity_instance) -> Union[str, None]:
def get_unit_name(unit: ifcopenshell.entity_instance) -> Union[str, None]:
if unit.is_a("IfcNamedUnit"):
return val(unit.Name)
+1 -1
View File
@@ -953,7 +953,7 @@ def get_unit_type_name(ifc_file: ifcopenshell.file, unit_type: str) -> Union[str
return val(unit.Currency)
def get_unit_name(ifc_file: ifcopenshell.entity_instance, unit: ifcopenshell.entity_instance) -> Union[str, None]:
def get_unit_name(unit: ifcopenshell.entity_instance) -> Union[str, None]:
if unit.is_a("IfcNamedUnit"):
return val(unit.Name)
@@ -29,8 +29,10 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcAxis2PlacementLinear* inst)
Logger::Error(std::runtime_error("Location must be IfcPointByDistanceExpression for IfcAxis2PlacementLinear"));
}
Eigen::Vector3d o, axis(0, 0, 1), refDirection;
taxonomy::matrix4::ptr m = taxonomy::cast<taxonomy::matrix4>(map(inst->Location()));
Eigen::Vector3d o = m->components().col(3).head<3>();
o = m->components().col(3).head<3>();
// From 8.9.3.4 IfcAxis2PlacementLinear there are 4 cases that need to be considered
// 1) Axis is given but not RefDirection
@@ -38,12 +40,43 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcAxis2PlacementLinear* inst)
// 3) Neither Axis or RefDirection are provided
// 4) Both Axis and RefDirection are provided
Eigen::Vector3d z = inst->Axis() ? *taxonomy::cast<taxonomy::direction3>(map(inst->Axis()))->components_ : Eigen::Vector3d(0,0,1); // Axis is (0,0,1) when omitted
Eigen::Vector3d rd = inst->RefDirection() ? *taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()))->components_ : m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
Eigen::Vector3d y = z.cross(rd);
Eigen::Vector3d x = y.cross(z);
const bool hasAxis = inst->Axis() != nullptr;
const bool hasRef = inst->RefDirection() != nullptr;
return taxonomy::make<taxonomy::matrix4>(o, z, x);
/*
if (hasAxis != hasRef) {
Logger::Warning("Axis and RefDirection should be specified together", inst);
}
*/
if (hasAxis && !hasRef) {
taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
axis = *a->components_;
refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
// refDirection is not necessarily orthogonal to axis.
// axis.cross(refDirection) gives y. y.cross(axis) gives x=refDirection
refDirection = axis.cross(refDirection).cross(axis);
} else if (!hasAxis && hasRef) {
taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()));
refDirection = *r->components_;
Eigen::Vector3d up(0, 0, 1);
axis = refDirection.cross(up.cross(refDirection));
} else if (!hasAxis && !hasRef) {
refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
Eigen::Vector3d up(0, 0, 1);
axis = refDirection.cross(up.cross(refDirection));
} else {
taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
axis = *a->components_;
taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()));
refDirection = *r->components_;
refDirection = axis.cross(refDirection).cross(axis); // refDirection needs to be orthogonal to axis
}
// axis and refDirection need to be orthogonal
return taxonomy::make<taxonomy::matrix4>(o, axis, refDirection);
}
#endif
+7 -25
View File
@@ -27,29 +27,15 @@ SED:=sed -i '' -e
endif
endif
# TODO: we should simplify this at some point...
ifeq ($(PYVERSION), py39)
PYNUMBER:=39
endif
ifeq ($(PYVERSION), py310)
PYNUMBER:=310
endif
ifeq ($(PYVERSION), py311)
PYNUMBER:=311
endif
ifeq ($(PYVERSION), py312)
PYNUMBER:=312
endif
ifeq ($(PYVERSION), py313)
PYNUMBER:=313
endif
ifeq ($(PYVERSION), py314)
PYNUMBER:=314
endif
ifndef PYNUMBER
$(error Unsupported PYVERSION '$(PYVERSION)')
SUPPORTED_PYVERSIONS := py310 py311 py312 py313 py314
ifeq ($(filter $(PYVERSION),$(SUPPORTED_PYVERSIONS)),)
$(error Unsupported PYVERSION=$(PYVERSION). Must be one of $(SUPPORTED_PYVERSIONS))
endif
PYMINOR:=$(subst py3,,$(PYVERSION))
PYNUMBER:=3$(PYMINOR)
# We actually do support glibc 2.28-2.30 (see #5636)
# but those are old and there's no demand for it.
ifeq ($(PLATFORM), linux64)
@@ -93,10 +79,6 @@ test-parallel:
test-mathutils:
pytest -p no:pytest-blender test/util/test_shape_builder.py
.PHONY: build-ids-docs
build-ids-docs:
mkdir -p test/build
cd test && python ids_doc_generator.py
.PHONY: qa
qa:
@@ -257,7 +257,8 @@ nest formulas, for example ``concat(title("foo"), lower("Bar"))`` will produce
"``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.
Examples:
````number({{Qto_WallBaseQuantities.Width}}, ",",".")```` or
````round({{Qto_BuildingElementProxyQuantities.NetVolume}},.1)````
When using queries in an IfcAnnotation tag surround with backticks. Examples:
- ````number({{Qto_WallBaseQuantities.Width}}, ",",".")````
- ````round({{Qto_BuildingElementProxyQuantities.NetVolume}},.1)````
- ````join(", OVER ", reverse({{material.item.Material.Name}}))````
@@ -111,8 +111,8 @@ __all__ = [
]
try:
from .stream import stream, stream_entity
from .stream import stream as _stream
from .stream import stream, stream_entity # ty: ignore[possibly-missing-import]
from .stream import stream as _stream # ty: ignore[possibly-missing-import]
except:
pass
@@ -199,11 +199,13 @@ def open(
for ty in bypass_types:
f.bypass_type(ty)
if mmap:
f.initialize(str(path.absolute()), mmap=mmap)
# mmap parameter is only available for builds with USE_MMAP, not used in our main builds
f.initialize(str(path.absolute()), mmap=mmap) # type: ignore[unknown-argument]
else:
f.initialize(str(path.absolute()))
elif mmap:
f = ifcopenshell_wrapper.open(str(path.absolute()), mmap=mmap)
# mmap parameter is only available for builds with USE_MMAP, not used in our main builds
f = ifcopenshell_wrapper.open(str(path.absolute()), mmap=mmap) # type: ignore[unknown-argument]
else:
f = ifcopenshell_wrapper.open(str(path.absolute()))
return file(f)
@@ -42,7 +42,6 @@ import importlib
import inspect
import json
from collections.abc import Callable
from functools import partial
from typing import TYPE_CHECKING, Any, Optional
import numpy
@@ -90,11 +89,7 @@ def renamed_arguments_deprecation(
# "group.add_group": partial(
# renamed_arguments_deprecation, arguments_remapped={"Name": "name", "Description": "description"}
# ),
ARGUMENTS_DEPRECATION: dict[str, Callable[[str, dict[str, Any]], tuple[str, dict[str, Any]]]] = {
"control.assign_control": partial(
batching_argument_deprecation, prev_argument="related_object", new_argument="related_objects"
),
}
ARGUMENTS_DEPRECATION: dict[str, Callable[[str, dict[str, Any]], tuple[str, dict[str, Any]]]] = {}
CACHED_USECASE_CLASSES: dict[str, Callable] = {}
@@ -51,8 +51,10 @@ def remove_context(file: ifcopenshell.file, context: ifcopenshell.entity_instanc
new = context.ParentContext
for inverse in file.get_inverse(context):
if inverse.is_a("IfcCoordinateOperation"):
# Trick to make sure the coordinate operation is not referenced
# by a context so we can delete it safely
inverse.SourceCRS = inverse.TargetCRS
ifcopenshell.util.element.remove_deep(file, inverse)
ifcopenshell.util.element.remove_deep2(file, inverse)
else:
ifcopenshell.util.element.replace_attribute(inverse, context, new)
file.remove(context)
@@ -59,6 +59,6 @@ def edit_cost_value(
value["ValueComponent"],
)
value = file.create_entity("IfcMeasureWithUnit", value_component, value["UnitComponent"])
if old_unit_basis and file.get_total_inverses(old_unit_basis) == 0:
ifcopenshell.util.element.remove_deep(file, old_unit_basis)
if old_unit_basis:
ifcopenshell.util.element.remove_deep2(file, old_unit_basis)
setattr(cost_value, name, value)
@@ -22,11 +22,13 @@ import ifcopenshell.util.element
def remove_feature(file: ifcopenshell.file, feature: ifcopenshell.entity_instance) -> None:
"""Remove a feature
"""Permanently delete a feature element and its void or projection relationship.
Fillings are retained as orphans. Featured elements remain. Features
cannot exist by themselves, so not only is the relationship removed, the
feature is also removed.
The feature entity (e.g. IfcOpeningElement) is removed from the model
along with its IfcRelVoidsElement or IfcRelProjectsElement relationship.
The host element (wall, slab, etc.) is unaffected. Any fillings (windows,
doors) that occupied the opening become orphaned and must be separately
deleted via root.remove_product.
:param feature: The IfcFeatureElement to remove.
@@ -31,17 +31,6 @@ def add_boolean(
) -> list[ifcopenshell.entity_instance]:
"""Adds a boolean operation to two or more representation items
If an IfcBooleanOperand is part of the top level items in an
IfcShapeRepresentation, it will be removed from that level whilst being
added to the IfcBooleanResult. This is because it is generally intuitive
that an item is either participating in a boolean operation, or being an
item in its own right, but not both.
However, if an IfcBooleanOperand is part of another boolean operation
already, it will not be removed from the existing operation. A new
operation will be created, and therefore it will participate in two
operations.
This function protects against recursive booleans.
After a boolean operation is made, since the items of
@@ -101,9 +90,6 @@ def add_boolean(
booleans = []
for second_item in second_items:
for inverse in file.get_inverse(second_item):
if inverse.is_a("IfcShapeRepresentation"):
inverse.Items = list(set(inverse.Items) - {second_item})
if first.is_a("IfcTesselatedFaceSet"):
first.Closed = True # For now, trust the user to do the right thing.
if second_item.is_a("IfcTesselatedFaceSet"):
@@ -20,11 +20,11 @@ from __future__ import annotations
import math
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
import bmesh # pyright: ignore[reportMissingImports]
import bpy # pyright: ignore[reportMissingImports]
import bmesh # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
import bpy # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
import numpy as np
import numpy.typing as npt
from mathutils import Matrix, Vector # pyright: ignore[reportMissingImports]
from mathutils import Matrix, Vector # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
import ifcopenshell.util.shape_builder
import ifcopenshell.util.unit
@@ -47,7 +47,9 @@ def add_wall_representation(
:param thickness: The thickness of the wall in meters.
:param x_angle: The slope angle along the wall's X-axis, in radians.
:param clippings: List of clipping definitions. Clippings can be `Clipping` objects
or dictionaries of arguments for `Clipping.parse`.
or dictionaries of arguments for `Clipping.parse`. Each clipping has a
``normal`` that points toward the removed material (the discarded side),
not toward the kept material; see :func:`clip_solid` for details.
:param booleans: List of any existing IfcBooleanResults.
:return: IfcShapeRepresentation.
"""
@@ -52,10 +52,11 @@ def edit_object_placement(
:param is_si: If True, the matrix is given in SI units. If false, in
project units.
:param should_transform_children: A child element is a nested element,
opening, filling, etc. If true, child elements will move along with the
parent. If false, child elements will stay where they are. Because most
placements in IFC are relative, this means that if a child moves, we
actually don't change their placement.
opening, filling, etc. If True, child elements move along with the
parent; pass True when moving an assembly (roof, furniture group, etc.)
and you want all children to follow. If False (default), child elements
keep their current world positions; their local placements are rewritten
to compensate for the parent move.
:return: The new or updated IfcLocalPlacement entity
"""
usecase = Usecase()
@@ -69,6 +69,12 @@ def regenerate_wall_representation(
additional extrusions are generated for each connection that boolean
difference the base extrusion.
Clippings applied via :func:`geometry.clip_solid` or
:func:`geometry.clip_solid_bounded` are preserved only if the ``element``
parameter was passed when creating them, which registers the result in the
``BBIM_Boolean`` property set. Clippings created without that parameter
are silently discarded during regeneration.
This will also update the axis line representation (e.g. trim the axis line
to any connections).
@@ -83,6 +83,7 @@ def validate_type(
if remaining_items:
ifcopenshell.api.geometry.add_boolean(file, preferred_item, remaining_items, "UNION")
representation.Items = [i for i in representation.Items if i not in remaining_items]
representation.RepresentationType = ifcopenshell.util.representation.guess_type(representation.Items)
if representation.RepresentationType == "CSG":
@@ -42,7 +42,5 @@ def remove_grid_axis(file: ifcopenshell.file, axis: ifcopenshell.entity_instance
ifcopenshell.api.grid.remove_grid_axis(model, axis=axis_2)
"""
axis_curve = axis.AxisCurve
if file.get_total_inverses(axis_curve) == 1:
ifcopenshell.util.element.remove_deep(file, axis_curve)
file.remove(axis_curve)
file.remove(axis)
ifcopenshell.util.element.remove_deep2(file, axis_curve)
@@ -19,7 +19,9 @@
from typing import Union
import ifcopenshell
import ifcopenshell.api.aggregate
import ifcopenshell.api.owner
import ifcopenshell.api.spatial
import ifcopenshell.guid
import ifcopenshell.util.element
@@ -137,7 +139,10 @@ def assign_object(
if not objects_to_change:
return is_nested_by
# NOTE: An object can both be nested and assigned to a container or an aggregate.
# Can be either only nested, aggregated, or contained at the same time.
possibly_contained = [o for o in objects_without_nests if hasattr(o, "ContainedInStructure")]
ifcopenshell.api.spatial.unassign_container(file, products=possibly_contained)
ifcopenshell.api.aggregate.unassign_object(file, products=objects_without_nests)
# unassign elements from previous nests
for nests in previous_nests_rels:
@@ -431,7 +431,7 @@ class Usecase:
)
ifcopenshell.api.type.assign_type(
self.file,
should_run_listeners=False,
should_run_listeners=False, # ty:ignore[unknown-argument]
related_objects=[element],
relating_type=new_type,
should_map_representations=False,
@@ -22,9 +22,9 @@ import ifcopenshell.util.element
def remove_prop_template(file: ifcopenshell.file, prop_template: ifcopenshell.entity_instance) -> None:
"""Removes a property template
Note that a property set template should always have at least one
property template to be valid, so take care when removing property
templates.
Note that a property set template should always have at least one property
template to be valid. So a property set template will not be removed if it
is the only template ina a property ste template.
:param prop_template: The IfcSimplePropertyTemplate to remove.
:return: None
@@ -43,10 +43,8 @@ def remove_prop_template(file: ifcopenshell.file, prop_template: ifcopenshell.en
ifcopenshell.api.pset_template.remove_prop_template(model, prop_template=prop2)
"""
for inverse in file.get_inverse(prop_template):
if len(inverse.HasPropertyTemplates) == 1:
inverse.HasPropertyTemplates = []
else:
if len(inverse.HasPropertyTemplates) > 1:
has_property_templates = list(inverse.HasPropertyTemplates)
has_property_templates.remove(prop_template)
inverse.HasPropertyTemplates = has_property_templates
ifcopenshell.util.element.remove_deep(file, prop_template)
ifcopenshell.util.element.remove_deep2(file, prop_template)
@@ -38,4 +38,4 @@ def remove_pset_template(file: ifcopenshell.file, pset_template: ifcopenshell.en
# Let's remove the template.
ifcopenshell.api.pset_template.remove_pset_template(model, pset_template=template)
"""
ifcopenshell.util.element.remove_deep(file, pset_template)
ifcopenshell.util.element.remove_deep2(file, pset_template)
@@ -79,5 +79,5 @@ def add_resource_quantity(
old_quantity = resource.BaseQuantity
resource.BaseQuantity = quantity
if old_quantity:
ifcopenshell.util.element.remove_deep(file, old_quantity)
ifcopenshell.util.element.remove_deep2(file, old_quantity)
return quantity

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