Compare commits

...

256 Commits

Author SHA1 Message Date
Andrej730 91a54be2c9 linter github workflow - use uv for acquiring black 2025-06-18 13:52:38 +05:00
Andrej730 02532fd021 linter github workflow - use correct minimum required versions
Previously it was using Python 3.12 for everything, therefore syntax errors from Python 3.9 (e.g. `match` statement in ifcopenshell-python) went under the radar.
2025-06-18 13:52:38 +05:00
Andrej730 21f5363c74 Bonsai - cache get_user and get_application for a speed-up 2025-06-17 18:44:32 +05:00
Andrej730 71d469a271 Bonsai - use main version part to avoid creating too many IfcApplications 2025-06-17 18:41:20 +05:00
Andrej730 96bdda305e Bonsai - purge unused IfcApplications and IfcOrganizations 2025-06-17 18:41:20 +05:00
Andrej730 1063238479 Bonsai - merge identical IfcOrganizations, IfcApplications
Added an utility to merge organizations and application that have exactly the same data. Noticed working with different files that sometimes they have these data duplicated - I guess reasons vary (revit exporter, our bug - c58b84d), but it's nice to be able to clean up redundant data.
Though still need to add some UI to change IfcApplications, so user could also get rid of them in case if they have small differences.

Feature location - https://files.catbox.moe/pwymx1.png

@theoryshaw in file from #6784 there were 2481 identical organizations and 1234 identical applications 😅
2025-06-17 18:41:20 +05:00
Andrej730 d1337cff02 Refresh applications UI on changes (bd1871bd4) 2025-06-17 18:41:19 +05:00
Andrej730 f26f2780e9 owner.ui - check props explicitly instead of relying on ifc data update 2025-06-17 18:41:19 +05:00
Andrej730 2e3c733c0d IfcParseWrapper - exclude instances_by_reference
As it does exactly the same as get_inverse()
2025-06-17 18:41:19 +05:00
Andrej730 ff09cd0a56 IfcParseWrapper - instance_by_guid -> by_guid
It seems they have exactly the same signatures and it makes sense to just rename instance_by_guid to by_guid, instead of implementing it separately.
2025-06-17 18:41:19 +05:00
Andrej730 df91f475c1 entity_instance constructor - skip setting None value 2025-06-17 18:41:19 +05:00
Andrej730 a733f36576 CMake - set C++17 as the minimum required version (34676a567)
Currently IfcOpenShell doesn't compile for C++14 (it stumbles upon `invoke_result_t` in XmlSerializer.cpp) and I'm not sure if it's needed.
2025-06-17 18:41:18 +05:00
Andrej730 9282ccabb3 typing 2025-06-17 18:41:18 +05:00
Thomas Krijnen 404c39873d Update WASM example app 2025-06-17 11:50:02 +02:00
Thomas Krijnen 34a1bc6f04 WASM build changes #6794 2025-06-16 21:18:14 +02:00
Andrej730 15112211c1 add_application not to add deprecated IfcTelecomAddress in ifc4x3 #6806 2025-06-16 19:57:00 +05:00
Andrej730 e3bbec6394 entity_instance constructor and destructor micro optimizations 2025-06-16 19:55:25 +05:00
Andrej730 bd1871bd44 Basic IfcApplications UI
Just so you can view IfcApplications in the project from UI

Example - https://files.catbox.moe/15a72n.png
2025-06-16 19:55:25 +05:00
Andrej730 a75f6a5804 typing 2025-06-16 19:55:25 +05:00
Andrej730 a12bb343ca Fix RUF015 (next() instead of list comprehensions[0])
https://docs.astral.sh/ruff/rules/-iterable-allocation-for-first-element/
2025-06-16 19:55:25 +05:00
Thomas Krijnen b486b32c8d WASM build changes 2025-06-14 18:05:29 +02:00
Thomas Krijnen 5e7893dbb3 Update build-all.py 2025-06-14 15:38:28 +02:00
Thomas Krijnen c8d74a560c Nix build retry downloads 2025-06-13 18:15:23 +02:00
Andrej730 9b8357c5ec entity_instance.__hash__ - reuse id() value for optimization 2025-06-13 19:33:30 +05:00
Andrej730 67b539aba1 remove_representation - optimization
In some cases remove_representation got 2x times faster (when there were a lot representation items that are not IfcTessellatedFaceSets).

getattr with attribute that might not be present on the element could be much slower then checking it's class explicitly.
2025-06-13 19:33:29 +05:00
Andrej730 176c5cc0fb typing 2025-06-13 19:33:29 +05:00
Thomas Krijnen 152c94a7d6 Update cgal for compilation on clang 19 https://github.com/CGAL/cgal/pull/8330 2025-06-13 16:06:08 +02:00
Thomas Krijnen 4b92d7d076 cgal: Dont proceed when not valid after triangulation #6735 2025-06-13 13:33:42 +02:00
Cristian Ritter d10c176bb6 re-adding u32string if ASCII instead of use only Boost.Locale 2025-06-13 10:39:59 +02:00
Bruno Postle 3503d41b3f Minor fix for the CMake package_source target 2025-06-13 09:11:09 +01:00
Esteban DUGUEPEROUX 34676a567e #6803: Update IfcOffsetCurveByDistance to be compitable on Windows with MSVC and C++20
In addition add option to be able to build IfcOpenShell against other C++ standard than C++17
2025-06-13 09:43:13 +02:00
Thomas Krijnen f5a46ffb7c pyodide: try 0.28.0a3 2025-06-12 20:48:52 +02:00
Cristian Ritter 94452b4a7a change deprecated wstring_convert to Boost.Locale and fix #6779 issue 2025-06-12 08:52:38 +02:00
Thomas Krijnen 2c0b1691ae -lexceptions.js #6794 2025-06-11 16:45:06 +02:00
Andrej 60c71c145b IfcFile::inverse_attr_record - add a note 2025-06-11 19:39:46 +05:00
Andrej d0f4dd00c7 getInverse -> get_inverses_by_declaration
Similar to getTotalInverses rename, to prevent confusing with get_inverse.
2025-06-11 19:39:46 +05:00
Andrej eee94f53a4 get_inverse, get_inverse_indices, get_total_inverses - prevent possible crashes
When passing entity without an id (e.g. IfcBaseType)
2025-06-11 19:39:46 +05:00
Andrej 6bb028696d getTotalInverses -> get_total_inverses_by_id
Rename method to avoid confusion with get_total_inverses
2025-06-11 19:39:46 +05:00
Andrej 3499c50d7d entity_instance.__getattr__ - small optimization for inverse attrs
Stop iterating early and don't build a list to make it a bit more optimized.

Though I'm unsure if anyone is really using `unpack_non_aggregate_inverses`
2025-06-11 19:39:46 +05:00
Andrej cd952d7e54 typing 2025-06-11 19:38:24 +05:00
Thomas Krijnen 2741acf4fe Fix compilation 2025-06-11 10:39:02 +02:00
Thomas Krijnen 69b6c41791 Use u32string throughout in character decoder 2025-06-10 20:33:21 +02:00
myoualid d1306d7a27 pyodide fatal error example #6794 2025-06-10 19:31:09 +01:00
Andrej 2bb373c1b2 bim.remove_representation - show operator time if it took more than 10 sec #6784 2025-06-10 18:54:37 +05:00
Andrej b4bc7ec371 remove_deep2 optimization for cases when the same entity is referenced by multiple other in the subgraph #6784 2025-06-10 18:54:37 +05:00
Andrej 1fecd77121 Fix a typo 2025-06-10 18:54:37 +05:00
Andrej c9b41a1499 Warning for joining non-mesh-like representation items #6793 2025-06-10 18:54:36 +05:00
Andrej b1ae213349 Fix bim.unassign_structural_load_case (redundant argument) 2025-06-10 18:54:36 +05:00
Andrej 5d632eae3d opencdeserver - fix bug with DocumentVersions initialization 2025-06-10 18:54:36 +05:00
Andrej 1608677d40 ifcopenshell.api.run to use explicit kwargs instead of unpacking 2025-06-10 18:54:35 +05:00
Andrej a37d46ca3d typing 2025-06-10 18:54:33 +05:00
Thomas Krijnen 862119834f Submodule 2025-06-09 21:37:13 +02:00
Thomas Krijnen 4d7453f9a4 Increase SVG precision to prevent self-intersections on detailed polygons 2025-06-09 21:31:55 +02:00
Thomas Krijnen a4f06f69cc Update pyodide to 0.27.7 #6794 2025-06-09 20:36:37 +02:00
Andrej 24f693e0bb ExpandedGroups - encapsulate in BIMGroupProperties 2025-06-09 18:03:25 +05:00
Andrej 32088fb95a Bonsai - fix cjio dependency (3d77fc7) 2025-06-09 18:03:22 +05:00
Andrej 812baa1c04 ifctester pyproject.toml - document flask dependency 2025-06-09 18:03:22 +05:00
Andrej 6862523c75 pyproject black - select all src folders by default, exclude explicitly 2025-06-09 18:03:22 +05:00
Andrej 8b4683aef1 replace api.run with static methods 2025-06-09 18:03:19 +05:00
Andrej 02d359d0e6 Fix Ruff UP035 (also deprecated annotation symbols) 2025-06-09 18:03:19 +05:00
Andrej 7d4176d105 Fix Ruff UP006 (deprecated annotation symbols) 2025-06-09 18:03:18 +05:00
Andrej 6eb45917fe fix typo 2025-06-09 18:03:15 +05:00
Andrej 3f4656600e typing 2025-06-09 18:03:15 +05:00
Andrej 7034c0b18a Fix typo (5ba82d7) 2025-06-09 18:03:14 +05:00
Andrej 97ece7c20b black . 2025-06-09 18:03:14 +05:00
Massimo Fabbro 5ba82d7764 See #5155. Update doc accordingly 2025-06-08 10:29:28 +02:00
falken10 8e8b44a744 Resource should not increment quantity in cost schedule 2025-06-08 10:23:00 +02:00
Massimo Fabbro 8b9927b77b See #6570. Better variable name 2025-06-08 10:22:12 +02:00
Andrej 09f6991565 cost items ui - update current index on item deletion
To make it less awkward to remove multiple items starting from the last.

Before - https://files.catbox.moe/wce56c.mp4
After - https://files.catbox.moe/wv1jhs.mp4
2025-06-06 19:06:47 +05:00
Andrej 14e8fad841 bim.contract / expand cost items - remove unused prop 2025-06-06 19:06:47 +05:00
Andrej c76785e02c tool.cost - refactor expand/contract rates methods
So they will be more similar
2025-06-06 19:06:46 +05:00
Andrej 7808dd535d Fix contracted cost item removal logic
It was removing checking for cost item index, though container is storing cost item ids.
2025-06-06 19:06:46 +05:00
Andrej 72cf77690e Prevent removing rel when is still valid after IfcCostItem removal #6769 2025-06-06 19:06:46 +05:00
Andrej bf4a3f074f typing 2025-06-06 19:06:46 +05:00
Andrej a374a7e3ff bonsai pyproject.toml - document dev dependencies 2025-06-06 13:11:59 +05:00
Andrej b6267e4ad1 Fix issue with occurrences when reassigning IfcTypeProduct class #6781 (977d814) 2025-06-06 13:11:59 +05:00
Andrej f21b34ffc1 reassign_class - safe check to prevent errors like #6781 2025-06-06 13:11:58 +05:00
Andrej 6de16bf658 bim.reassign_class - add simple test to cover cases like #6781 2025-06-06 13:11:58 +05:00
Andrej 9f17b9fd86 reassign_class - calculate occurrence_class just once for the entire loop 2025-06-06 13:11:58 +05:00
Andrej 781df9e3d8 Fix incorrect glob separator (8c9ccfd) 2025-06-06 11:57:46 +05:00
Andrej 8c9ccfdaae Fix #6760 2025-06-06 11:22:05 +05:00
Andrej 3892ac56a4 Skip 26b21dd test if non-standard schema is not available (e.g. in github ci) 2025-06-05 19:05:28 +05:00
Andrej ba6674299c ifocpenshell-python makefile - shortcut to get current build urls 2025-06-05 19:05:28 +05:00
Andrej fc0624692f test_sql - use Ifc2Sql patcher for readability 2025-06-05 19:05:28 +05:00
Andrej d6b5c73d4a ifcpatch - fix doc-string (b07b0e4be) 2025-06-05 19:05:28 +05:00
Andrej f622d2f961 ifc2sql - consider .ifcsqlite suffix 2025-06-05 19:05:27 +05:00
Andrej 3a66a1c754 geom settings - add new settings from 439f9c1 2025-06-05 19:05:27 +05:00
Andrej d0741cd495 Fix error using X hotkey on empty collection #6696 2025-06-05 19:05:27 +05:00
Andrej 0d39f8df28 Safer BlendData access
To avoid issues like #6771 when we by accident find an ID data-block from linked .blend file.
2025-06-05 19:05:27 +05:00
Andrej 87265d052b typing 2025-06-05 19:05:27 +05:00
Thomas Krijnen 5af7c9a32d Clarify Compound of Solids behaviour in create_solid_from_faces() #6780 2025-06-05 10:22:28 +02:00
Andrej 1ce9a4c68e ifc2sql - fix mysql errors inserting values into shape table 2025-06-04 19:10:52 +05:00
Andrej 930d58cf69 ifc2sql - fix issue inserting mysql json attributes
It seems mysql doesn't have automatic conversion from other types to json, so we stringify it for compatibility.
2025-06-04 19:10:51 +05:00
Andrej 926785c5ec ifc2sql - fix should_get_inverses for mysql 2025-06-04 19:10:51 +05:00
Andrej 274e19dcb9 ifc2sql - warn user if ifc database already exists 2025-06-04 19:10:51 +05:00
Andrej 7f0f93f726 ifc2sql - close cursor too
Important on Windows if someone is using Patcher explicitly and `self.c` is not garbage collected and file stays locked and cannot be moved.
2025-06-04 19:10:51 +05:00
Andrej b07b0e4bea ifc2sql - fix regression with missing resulting database filepath (9055f14) #6758 2025-06-04 19:10:50 +05:00
Andrej a0e4bdcbef ifcpatch - fix ifc2x3 bootstrap (c58b84d)
After c58b84d append_asset requires user to be a valid IFC entity.
2025-06-04 19:04:32 +05:00
Andrej 2da183bfa6 bpy._typing -> bpy.stub_internal
https://github.com/nutti/fake-bpy-module/issues/362
2025-06-04 19:04:31 +05:00
Andrej adf8a1f4e0 ci - run all ifcopenshell packages tests regardless of errors in some 2025-06-04 19:04:31 +05:00
Andrej e1ec422bb1 common.mk - explicitly specify sh as default shell 2025-06-04 19:04:31 +05:00
Andrej c51e36dbca ifcpatch - a static way to use ifcpatch 2025-06-04 19:04:31 +05:00
Andrej b393c120f1 ifcpatch pyproject - list optional dependencies 2025-06-04 19:04:31 +05:00
Andrej 3c7e938d90 IfcGeomWrapper - add missing property for edges_item_ids 2025-06-04 19:04:31 +05:00
Andrej a614fac71a typing 2025-06-04 19:04:31 +05:00
Andrej 5a211c67eb black . 2025-06-04 19:04:30 +05:00
Andrej 6ea53d54ba ruff to ignore src/ifcconvert/cityjson submodule 2025-06-04 11:02:08 +05:00
Dion Moult b9b5aefd5e Fix bSDD to properly use names and codes 2025-06-04 12:11:34 +10:00
Ryan Schultz e1c1b828ad Have save/cancel filter on top as well--helps with long filters. 2025-06-03 07:29:19 -05:00
Thomas Krijnen 3180004a28 WKT serializer: adapt section height to geometry height and emit height as geo:length 2025-06-02 13:30:06 +02:00
Thomas Krijnen 2ea2dc303c OSX action: try with pip's awscli 2025-06-02 11:28:19 +02:00
Thomas Krijnen 0aae57620f Temporary Python 3.9 compatibility 2025-06-02 08:42:47 +02:00
Thomas Krijnen 5c681d729c vc path 2025-06-01 13:55:53 +02:00
Thomas Krijnen 802ab8c296 vs2022 2025-06-01 13:39:00 +02:00
Thomas Krijnen 2ebe7046d3 windows-2022 2025-06-01 10:00:33 +02:00
Thomas Krijnen 3562d89f01 Win S3 upload: try without --recursive but loop in ps 2025-06-01 09:53:17 +02:00
Thomas Krijnen 1155b52375 Black 2025-06-01 09:23:10 +02:00
Thomas Krijnen 2d67440085 Draw.py don't assert just only take outer boundary in arrangement 2025-06-01 09:22:40 +02:00
Thomas Krijnen cc3f2aa28d Expose mirror-y in draw.py 2025-06-01 09:21:59 +02:00
Massimo Fabbro 390349aa7a See #6570. Better cost item rate identification
Now show cost item rate shows the Identification and not the ID for better understanding
2025-05-31 15:58:42 +02:00
Massimo Fabbro eade620581 Add calculating functions for Qto_CourseBaseQuantities for IfcCourse elements 2025-05-31 14:57:46 +02:00
Thomas Krijnen 88ac0a0f6c Wrong preprocessor statement #6765 2025-05-31 14:14:43 +02:00
Bruno Perdigão a531bd8c1f Fix. Uninstall Bonsai Decorators if they are not selected at start 2025-05-30 16:20:42 -03:00
Andrej f44769c138 ifc2sql - add example for should_expand 2025-05-30 18:13:18 +05:00
Andrej a6e3cc667c ifc2sql - add missing should_get_inverses description 2025-05-30 18:13:17 +05:00
Andrej 59b17fc593 ifcopenshell_wrapper.pyi 2025-05-30 18:13:17 +05:00
Andrej fb1b5bea51 ifcopenshell.file - use IFC4X3 instead of IFC4X3_ADD2 for simplicity 2025-05-30 18:13:11 +05:00
Andrej e8aa508405 Add 'advanced' optional ifcopenshell dependencies
To list all other optional dependencies.
2025-05-30 18:13:11 +05:00
Andrej 363083104b Add 'tabulate' to optional ifcopenshell dev dependencies
Used in test_rules and test_wall_opening.
2025-05-30 18:13:11 +05:00
Andrej 2798bf3b68 ifcopenshell.api.geometry to fail more gracefully with fake-bpy
If user has fake-bpy-module for type hints, it will fail with `ImportError: cannot import name 'Vector' from 'mathutils' (unknown location)` instead of `ModuleNotFoundError`.
2025-05-30 18:13:11 +05:00
Andrej e41c4bf7ae Separate mathutils tests 2025-05-30 18:13:11 +05:00
Andrej 3bdce4fbc3 Make zip strict argument optional to support Python 3.9 2025-05-30 18:13:11 +05:00
Andrej ad7473ff1f Make dataclasses args optional to support Python 3.9 #6725 2025-05-30 18:13:10 +05:00
Andrej 3d77fc7c7d ifccityjson - more specific depdendency
API in cjio 0.10 changed (`load` method was moved elsewhere) and for now we just specify that we require older cjio version.
2025-05-30 18:13:10 +05:00
Andrej 92a222cda7 typing 2025-05-30 18:13:10 +05:00
Andrej cc60794293 edit style for rep item - fallback in case user has no styles #6764
Or if style was removed in the process of editing. And it seems now UI will never appear broken that way.

Previously - https://files.catbox.moe/exhkln.png
Currently - https://files.catbox.moe/r36be8.png
2025-05-30 17:01:11 +05:00
Kristoffer Andersen c7b4033f6f Update conda_build_config.yaml
try to fix conda daily
2025-05-29 21:18:19 +02:00
Andrej 1ab288de0b ifc5d - fix <py3.11 compatibility issue (6499ac4) 2025-05-29 19:14:43 +05:00
Andrej 48f62c0b5b ifcopenshell_wrapper.pyi - update from the latest build
I guess previously I used some of my local builds, so some symbols were missing.
2025-05-29 19:14:43 +05:00
Andrej 5fc549453c validate_stub - add wrappers constants 2025-05-29 19:14:43 +05:00
Andrej bd5f5a30b3 validate_stub - also skip object 2025-05-29 19:14:43 +05:00
Andrej baf3edca13 ifcopenshell.validate - fix Python 3.9 support (25eecde) 2025-05-29 19:14:42 +05:00
Andrej c23e973575 Fix moved cost.remove_cost_item_value (b272210) 2025-05-29 19:14:42 +05:00
Andrej a5801e439f edit_attributes - fix Python 3.9 support (39fd9ce) 2025-05-29 19:14:42 +05:00
Andrej 4e65c33bae Bonsai make - use ifcjson master instead of fork
https://github.com/IFCJSON-Team/IFC2JSON_python/pull/3 got merged
2025-05-29 19:14:42 +05:00
Andrej c3652a56ee dev_environment.py #6754 2025-05-29 19:14:42 +05:00
Andrej e1b3b68a8b gitignore jquery.min.js (320a4cf) 2025-05-29 19:14:42 +05:00
Andrej dbce4c5d98 bcf - add missing requests dependency 2025-05-29 19:14:42 +05:00
Andrej 0663c5130a typing 2025-05-29 19:14:42 +05:00
Andrej ea7ced738a Introduce Ruff github CI with some basic linter rules
And also to help catching syntax errors on older Python versions.
2025-05-28 18:24:13 +05:00
Andrej 7cde9629f8 Fix Ruff UP034 (extraneous-parentheses)
https://docs.astral.sh/ruff/rules/extraneous-parentheses/
2025-05-28 18:24:13 +05:00
Andrej 5f5ba72919 Fix Ruff UP003 (taking type of primitive)
https://docs.astral.sh/ruff/rules/UP003
2025-05-28 18:24:13 +05:00
Andrej feebc701fa Fix Ruff UP008 (super-call-with-parameters)
https://docs.astral.sh/ruff/rules/super-call-with-parameters/
2025-05-28 18:24:13 +05:00
Andrej 5ab14f1e4e Fix Ruff UP022 (replace-stdout-stderr)
https://docs.astral.sh/ruff/rules/replace-stdout-stderr/
2025-05-28 18:24:12 +05:00
Andrej f289b55766 Fix Ruff UP010 (unnecessary-future-import)
https://docs.astral.sh/ruff/rules/unnecessary-future-import/
2025-05-28 18:24:12 +05:00
Andrej 3976b77ee1 Fix Ruff UP033 (lru-cache-with-maxsize-none)
https://docs.astral.sh/ruff/rules/lru-cache-with-maxsize-none/
2025-05-28 18:24:12 +05:00
Andrej 7259dd625f Fix Ruff UP013 (convert-typed-dict-functional-to-class)
https://docs.astral.sh/ruff/rules/convert-typed-dict-functional-to-class/
2025-05-28 18:24:12 +05:00
Andrej 03f5ee653b Fix Ruff UP017 (datetime-timezone-utc)
https://docs.astral.sh/ruff/rules/datetime-timezone-utc/
2025-05-28 18:24:12 +05:00
Andrej 62a0c258b2 Fix Ruff UP012 (unnecessary-encode-utf8)
https://docs.astral.sh/ruff/rules/unnecessary-encode-utf8/
2025-05-28 18:24:11 +05:00
Andrej c838139fdc Fix Ruff UP024 (os-error-alias)
https://docs.astral.sh/ruff/rules/os-error-alias/
2025-05-28 18:24:11 +05:00
Andrej a42a95adcb Fix Ruff UP037 (quoted-annotation)
https://docs.astral.sh/ruff/rules/quoted-annotation/
2025-05-28 18:24:11 +05:00
Andrej 3b9d6ed145 Fix Ruff UP009 (utf8-encoding-declaration)
https://docs.astral.sh/ruff/rules/utf8-encoding-declaration/
2025-05-28 18:24:11 +05:00
Andrej 3c4105cbc5 Fix Ruff UP011 (lru-cache-without-parameters)
https://docs.astral.sh/ruff/rules/lru-cache-without-parameters/
2025-05-28 18:24:11 +05:00
Andrej 302423bfdb Fix Ruff UP004 (useless-object-inheritance)
https://docs.astral.sh/ruff/rules/useless-object-inheritance/
2025-05-28 18:24:11 +05:00
Andrej 6dbf86382f Fix Ruff UP018 (native-literals)
https://docs.astral.sh/ruff/rules/native-literals/
2025-05-28 18:24:11 +05:00
Andrej 3b50993b21 ifc2ca, bcf, opencdeserver - add Python 3.9 support
Was breaking due to `|` operator used in annotations instead of `Union`
2025-05-28 18:24:11 +05:00
Andrej a8634a561a ifcopenshell.util.mvd_info - fix breaking Python 3.9 support (cbe8fc1)
due to `|` operator used in annotations instead of `Union`
2025-05-28 18:24:10 +05:00
Andrej a16b8fa376 Fill ifcopenshell_wrapper.pyi with symbols #6684
As described in #6744 partial stub wrapper is completely replacing the type information from the wrapper, so having partial stub is making things worse than they were before.
This commit is adding all available symbols from the wrapper to the stub and allows to gradually include type information to the stub.
It also adds a safe net to ensure stub and wrapper won't go out of sync.
2025-05-28 18:24:10 +05:00
Andrej a25698734a Address PR feedback from #6719 2025-05-28 18:24:10 +05:00
Andrej 1c1bf56893 typing 2025-05-28 18:24:10 +05:00
Andrej ceeb7ca355 black . 2025-05-28 18:24:10 +05:00
Martin15135215 6b77147d45 Create ifcopenshell_wrapper.pyi
stub file for swig-generated ifcopenshell_wrapper python file
2025-05-28 12:29:10 +00:00
Dion Moult a929ab0c4f Big bSDD overhaul for properties and classifications. See description.
* bSDD now supports a couple more args when querying the API.
 * Fetching properties via the classification UI has been removed
 * The bSDD dictionary selector has been merged into the classification
source dropdown
 * The bSDD dictionary selector merged into the property set name
dropdown
 * You can now search for properties in the bSDD directly when adding
properties
 * You can browse properties via groups or classifications (based on
assigned classifications)
 * You can browse properties via keyword (with some limitations due to
the API)
 * You can selectively choose which properties you then want to add.
Only basic support right now.
2025-05-28 20:51:08 +10:00
Iagoba Apellaniz e9eb5f892a Update installation.rst to point to the latest release 2025-05-28 07:25:26 +00:00
Dion Moult 72ef055946 Add tests for boundary 2025-05-27 10:38:38 +10:00
Ryan Schultz 32e4a06fe4 fix: #6749: use shift + click to remove that selection from the selection set.
changed bim.select_similar.calculate_sum to `ctrl + click` instead, since `shift + click` is a UI pattern used in vanilla blender when selecting/deselecting things.
Also refactored into helper methods.
2025-05-26 18:18:18 -05:00
Massimo Fabbro 02af46c99f Fix #6700. Cleanup 2025-05-26 17:56:13 +02:00
Massimo Fabbro 085fc8ca1c Fix #6700
It seems that with new shapely release the buffer behavior has changed so now it seems to work again
2025-05-26 17:56:13 +02:00
Dion Moult 93d36501a3 Fix #3994. Regression where space boundaries would not be created due to shape var being garbage cleaned. 2025-05-26 23:04:51 +10:00
Dion Moult a74f1c6e08 Fix #6513. Bug where OBJ not found when adding element.
This was actually fixed earlier already due to restructuring of
structural element authoring (i.e. the representation_template items now
always has OBJ unless it is a structural item)
2025-05-26 16:45:35 +10:00
Dion Moult 8766cad37e bSDD searches now work on type classes as well as predefined types 2025-05-26 16:34:31 +10:00
Sayanjyoti Das 320a4cf4fa Update installation scripts on documentation 2025-05-26 13:57:52 +10:00
Sayanjyoti Das 77ed64a900 Bundle jQuery at build time instead of fetching from a CDN 2025-05-26 13:57:52 +10:00
Sayanjyoti Das b17c6dde85 fix jquery CDN links 2025-05-26 13:57:52 +10:00
falken10vdl b0b7bbf95f Update data.py to get data sorted
Then later is easier to read in lists
2025-05-26 13:56:03 +10:00
Dion Moult 873fe6a87f Shift down grouping below spatial 2025-05-26 13:55:38 +10:00
talukderpushpak2000 31dc3fe002 Initial Commit 2025-05-26 13:53:58 +10:00
Martin15135215 697b034c22 Add annotation for file.header 2025-05-26 13:52:29 +10:00
Dion Moult 810e8df6fe Fix #6640. Fix invalid association of annotations to grids.
There was already some half written code with this intent it seems, so I
just finished it up.
2025-05-26 13:49:06 +10:00
Dion Moult d6cc1ff9e9 Fix #2999. You can now edit topology representation items. 2025-05-26 12:41:55 +10:00
Dion Moult 242d70c311 Fix set update in loop bug when trying to filter elements by spatial container 2025-05-26 10:47:05 +10:00
Massimo Fabbro 327899ca62 See #6570. Small fix for previuos commit 2025-05-25 08:00:54 +02:00
Massimo Fabbro 482512085c See #6570. Small fix 2025-05-25 08:00:54 +02:00
Massimo Fabbro 12774607f3 See #6570. Now it is possible to assign a cost rate from a non schedule of rate cost schedule 2025-05-25 08:00:54 +02:00
Massimo Fabbro 242d0ffb3f See #6570.Minor fix 2025-05-25 08:00:54 +02:00
Massimo Fabbro 2bef2a0219 See #6570. More consistent cost rate assignment
Now when a cost rate is assigned to a cost item, it creates also a relation between them, making the relation more consistent. It is also shown in the UI
2025-05-25 08:00:54 +02:00
Ryan Schultz 8cb871a6a2 Better convention: Using poll() method for b452348748
Thank you @Andrej730.
per https://github.com/IfcOpenShell/IfcOpenShell/commit/b452348748629a6e6dad6df24e21bfad2c203043#r157689504
2025-05-24 07:53:45 -05:00
Andrej730 fb5358e54e clash ui - add basic header for the found clashes
Example - https://i.imgur.com/JjKIUZX.png

Also show indices, so it will be easier to communicate a specific clash to someone.
2025-05-23 18:45:07 +05:00
Andrej730 deae4e659b clash ui - expose clash type to UI
Example - https://imgur.com/a/Wdyg3pn
2025-05-23 18:45:07 +05:00
Andrej730 29bd89324a ifcopenshell.geom.tree.get_clash_type 2025-05-23 18:42:55 +05:00
Andrej730 087608a5f5 clash ui - show clearance, tolerance using blender units for readibility 2025-05-23 18:42:55 +05:00
Andrej730 bc5a291321 ifcclash - remove 'objects' dict item
It seems to be part of legacy code that's no longer used since 18c38b3
2025-05-23 18:42:55 +05:00
Andrej730 88befea174 typing 2025-05-23 18:42:55 +05:00
Andrej730 441e2ddbd6 black . 2025-05-23 18:42:55 +05:00
Dion Moult 09e07c80c5 Implement recursive contract / expand of the spatial tree 2025-05-23 17:35:15 +10:00
Orion Sehn e3ddccdb11 updating documentation to make it clear to run the setup script on windows with blender closed. 2025-05-23 11:38:25 +05:00
Orion Sehn 621f6e7a31 fix to default directory on dev_environment.bat assuming you weren't using the daily build 2025-05-23 11:38:25 +05:00
Bruno Perdigão 0052570fdb See #6166. First implementation of polyline extend walls. 2025-05-22 23:47:11 -03:00
Bruno Perdigão 55e2171cac Remove print statement 2025-05-22 23:16:48 -03:00
Bruno Perdigão 1f8967d5b8 See #6240. Fix input handling for "close polyline". 2025-05-22 23:16:25 -03:00
falken10vdl 0410cec21b Added Quickstart guides for Linux and Windows IDEs using VSCode (#6494)
* Added Quickstart guides for Linux and Windows IDEs using VSCode

* added some tips to Linux QS

* Added point 15

* extra picture for step 15

* Added windows part

* updated windows vscode launch settings

* added info about branch rebase for PRs

* updated dev_environment to cope for some errors
2025-05-22 09:46:41 -05:00
Andrej730 08ab48887a clash - allow running a quick clash without saving results to some file explicitly
It's available on alt+click.
Example - https://imgur.com/a/wctjHnq
2025-05-22 18:09:28 +05:00
Andrej730 97bb587772 Bump bonsai build #6620
Besides the pending issues also resolves #6589 and error in #6196
2025-05-22 17:08:46 +05:00
Ryan Schultz b452348748 Select all instances that share the same attribute value as any of the selected objects 2025-05-21 22:58:34 -05:00
Andrej730 60c18628c5 clash ui - display "clashes are not loaded" to avoid confusion with "no clashes found"
Example - https://imgur.com/a/5a6cuEG
2025-05-21 18:43:03 +05:00
Andrej730 5bb769448d bim.execute_ifc_clash - show results in Bonsai UI for bcf file too
Previously results would show up only if you'd save .json, creating confusion since UI is showing "0 Clashes Found" though there were clashes.
2025-05-21 18:43:03 +05:00
Andrej730 ca890d2f9b clash ui - make filters expandable for readibility
Example - https://imgur.com/a/ZeVeF3u
2025-05-21 18:43:03 +05:00
Andrej730 99782a2813 bim.select_clash_source - hide internal props from user
to avoid confusion
2025-05-21 18:43:02 +05:00
Andrej730 d48406006f clash ui - move file selection button closer to filepath field
To make the type of the field more apparent.
2025-05-21 18:43:02 +05:00
Andrej730 0405b5c61f clash ui - deduplicate ui code 2025-05-21 18:43:02 +05:00
Andrej730 c9eea76e57 bim.execute_ifc_clash - more user-friendly error when one of the filepaths do not exist 2025-05-21 18:43:02 +05:00
Andrej730 cc73143ac1 clash - typing fixes and some refactor 2025-05-21 18:43:02 +05:00
Andrej730 84b0abfb60 ci-ifcopenshell-python - include submodules #6723 2025-05-21 18:43:02 +05:00
Andrej730 13ea2ba8d2 ifcopenshell-python makefile - set sh as the main shell explicitly 2025-05-21 18:43:02 +05:00
Andrej730 d148ab4f31 Fix error switching to custom title block #6685 2025-05-21 18:43:02 +05:00
Andrej730 c7a030cc2d typing 2025-05-21 18:43:01 +05:00
Andrej730 718254fe5e Fix regression parsing iso duration with floats (d1b1b08) #6715 2025-05-21 12:16:07 +05:00
Andrej730 41227b89c8 Fix subtle bug with operators precedence in eabdb5d
`+` has a higher precedence than `or`, so it resulted in the error below

```
Traceback (most recent call last):
  File "\bonsai\bim\module\spatial\prop.py", line 86, in update_active_container_index
    tool.Spatial.load_contained_elements()
  File "\bonsai\tool\spatial.py", line 312, in load_contained_elements
    cls.load_contained_elements_by_type(container)
  File "\bonsai\tool\spatial.py", line 321, in load_contained_elements_by_type
    results = cls.get_container_elements_grouped_by_type(container)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\bonsai\tool\spatial.py", line 294, in get_container_elements_grouped_by_type
    element_type.is_a() + "/" + element_type.Name or "Unnamed"
    ~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~
TypeError: can only concatenate str (not "NoneType") to str
```
2025-05-20 19:06:25 +05:00
Andrej730 2547ee9ab5 bonsai.crash.txt
There seems to be cases when Blender might crash too unexpectedly.
E.g. #6686 - during viewport render system is running out of resources and crashing without leaving blender.crash.txt, leaving everyone clueless. Which is arguably a bug in Blender (both crash and lack of any crash report), but at least we'll have a fallback for this case.

What bonsai.crash.txt does - it's created in system temp folder with the current Python traceback, just before some dangerous operation. If operation didn't crashed Blender, then file will be unlinked. If crash did occurred, it might be the only clue for users and devs on what actually happened.
2025-05-20 19:06:25 +05:00
Andrej730 d22f1bcd71 'viewport' render type - add a note 2025-05-20 19:06:24 +05:00
Andrej730 0c2158089d disable update_has_underlay if update_props is disabled
Otherwise it was trying to load drawing styles too early, when drawing was not yet active. And then they were reloaded again during ActivateDrawingBase. Occurred after c2860e6, before this commit issue was kind of hidden.
2025-05-20 19:06:02 +05:00
Thomas Krijnen ccafc203b5 submodule 2025-05-20 10:05:21 +02:00
Andrej730 0efc2d25f1 Fix for assign_class and object with modifiers in Blender <4.4 #6701 2025-05-20 12:24:35 +05:00
Andrej730 ac50f002f0 Fix confused blender and pset props in 9586869 2025-05-19 19:28:55 +05:00
Andrej730 66f4c957ea black . 2025-05-19 19:28:55 +05:00
Dion Moult 67016d6f90 See #2999. Reimplement basic creation of structural items. 2025-05-19 17:36:12 +10:00
Thomas Krijnen 439f9c14ce OcctNoCleanTriangulation CacheShapes PermissiveShapeReuse setting; Cgal conditional copy during triangulation; occt cheaper check for triangulation existance #6712 2025-05-18 22:14:22 +02:00
Dion Moult 404bf13a18 Update colour by property test to reflect new behaviour of only showing geometry not empties
ALL (feature) TESTS PASS YESS
2025-05-18 19:23:34 +10:00
Dion Moult 3a37bcdcaa Update test cases to reflect new drawing activation UI 2025-05-18 19:09:29 +10:00
Dion Moult 992ad8e24a See #4699. Continue refactoring logic from drawing decorator into DecoratorData. 2025-05-18 15:32:55 +10:00
Dion Moult 1de2515fcd More bSDD tests to cover all current functionality 2025-05-18 12:05:33 +10:00
Thomas Krijnen 4bc88eac2f ifdefs around geometry handling for 4.3 rc schemas #6710 2025-05-17 16:01:28 +02:00
Dion Moult 22f10e5549 Fix #6669. bSDD now supports test dictionaries, refactor domain to dictionary, support zero length keyword searches, multi-dict support, and basic tests.
Feature tests can now have more sophisticated per UIList item spies.
2025-05-17 21:28:12 +10:00
Andrej730 8e34c1d7df Fix error using bim.updating_representation for object with material set
Possibly occurred after 4c255cc, but haven't investigated deeper.

@BIMvoice
The error you were reporting about Split Along Edge operator was caused by this issue, should be fixed now too.
See https://jmp.sh/hgJEHsgH

Example error traceback:
```
Error: Python: Traceback (most recent call last):
  File "C\bonsai\bim\ifc.py", line 487, in execute_ifc_operator
    result = getattr(operator, "_execute")(context)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C\bonsai\bim\module\geometry\operator.py", line 541, in _execute
    self.update_obj_mesh_representation(context, obj)
  File "C\bonsai\bim\module\geometry\operator.py", line 666, in update_obj_mesh_representation
    tool.Ifc.link(new_representation, data)
  File "C\bonsai\tool\ifc.py", line 230, in link
    IfcStore.link_element(element, obj)
  File "C\bonsai\bim\ifc.py", line 258, in link_element
    tool.Geometry.get_mesh_props(obj).ifc_definition_id = element.id()
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C\bonsai\tool\geometry.py", line 92, in get_mesh_props
    return mesh.BIMMeshProperties
           ^^^^^^^^^^^^^^^^^^^^^^
ReferenceError: StructRNA of type Mesh has been removed
```
2025-05-16 19:43:42 +05:00
Andrej730 4369baa226 bim.split_along_edge - improve descriptions, add error handling 2025-05-16 19:43:42 +05:00
Andrej730 7941d71feb SvgWriter - make required arguments more explicit 2025-05-16 19:43:42 +05:00
Andrej730 68d967c2e5 pyproject.toml - enableTypeIgnoreComments=false 2025-05-16 19:43:41 +05:00
Andrej730 4ea9a598c7 operator.drawing - remove unnecessary ifc operators 2025-05-16 19:43:41 +05:00
Andrej730 06d1c81954 black . 2025-05-16 19:43:41 +05:00
Andrej730 aa5f08880b typing 2025-05-16 19:43:41 +05:00
Ryan Schultz 9e6320d545 A preference to list those classes that are not affected by the 'cut decorator'. 2025-05-15 21:45:53 -05:00
Bruno Perdigão f39d3d4b9c Fix polyline plane method decorator to show properly.
The `plane_method` where not being cleared after canceling or confirming
the polyline operator.
2025-05-15 21:28:31 -03:00
Bruno Perdigão 5c72cf35bd See #6208. Change wall merge tolerance for more consistent behavior. 2025-05-15 19:08:04 -03:00
Bruno Perdigão b9a435e1c3 Fix polyline tool calculation when dealing with 180 degrees angles. 2025-05-15 17:41:32 -03:00
Bruno Perdigão 92f2559ce0 See #6536. Recalculate profile only for selected objects. 2025-05-15 17:41:32 -03:00
Bruno Perdigão acf9713943 Fix aggregate decorator after #6537. 2025-05-15 17:41:32 -03:00
625 changed files with 11453 additions and 3890 deletions
+5 -15
View File
@@ -44,7 +44,11 @@ jobs:
brew update
brew install git bison autoconf automake libffi cmake findutils
echo "$(brew --prefix findutils)/libexec/gnubin" >> $GITHUB_PATH
- name: Install aws cli
run: |
python -m pip install awscli
- name: Unpack Dependencies
run: |
install_root=$(find ./build -maxdepth 4 -name install)
@@ -117,20 +121,6 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Overwrite aws cli
run: |
# Error: The `brew link` step did not complete successfully
# The formula built, but is not symlinked into /usr/local
# Could not symlink bin/idle3
# Target /usr/local/bin/idle3
# already exists. You may want to remove it:
# rm '/usr/local/bin/idle3'
#
# To force the link and overwrite all conflicting files:
# brew link --overwrite python@3.13
# https://github.com/rust-lang/rustup/pull/3989/files
brew install --overwrite awscli | true
- name: Upload .zip archives to S3
run: |
aws s3 cp ~/output s3://ifcopenshell-builds/ --recursive
+5 -5
View File
@@ -19,7 +19,7 @@ jobs:
with:
submodules: recursive
repository: pyodide/pyodide
ref: '0.26.4'
ref: '0.28.0a3'
token: ${{ secrets.BUILD_REPO_TOKEN }}
path: pyodide
@@ -29,18 +29,18 @@ jobs:
sed -i s/0.8.0/$VERSION/g IfcOpenShell/pyodide/meta.yaml
sed -i s/0.8.0/$VERSION/g IfcOpenShell/pyodide/setup.py
echo '#!/usr/bin/bash' > script.sh
echo 'cd pyodide' > script.sh
echo 'make && pip install ./pyodide-build' >> script.sh
echo 'cd pyodide' >> script.sh
echo 'make' >> script.sh
echo 'cd ..' >> script.sh
echo 'mkdir -p packages/ifcopenshell' >> script.sh
echo 'cp IfcOpenShell/pyodide/meta.yaml packages/ifcopenshell' >> script.sh
echo 'PYODIDE_ROOT=/src/pyodide \' >> script.sh
echo 'PATH=/src/pyodide/emsdk/emsdk:/src/pyodide/emsdk/emsdk/node/20.18.0_64bit/bin:/src/pyodide/emsdk/emsdk/upstream/emscripten:$PATH \' >> script.sh
echo 'PATH=/src/pyodide/emsdk/emsdk:/src/pyodide/emsdk/emsdk/node/22.16.0_64bit/bin:/src/pyodide/emsdk/emsdk/upstream/emscripten:$PATH \' >> script.sh
echo 'pyodide build-recipes ifcopenshell --install' >> script.sh
chmod +x script.sh
sed -i s/--tty// pyodide/run_docker
pyodide/run_docker ./script.sh
mv dist/ifcopenshell-$VERSION-py3-none-any.whl dist/ifcopenshell-$VERSION+${GITHUB_SHA:0:7}-cp312-cp312-emscripten_3_1_58_wasm32.whl
mv dist/ifcopenshell-$VERSION-py3-none-any.whl dist/ifcopenshell-$VERSION+${GITHUB_SHA:0:7}-cp313-cp313-emscripten_4_0_9_wasm32.whl
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
+14 -11
View File
@@ -5,7 +5,7 @@ on:
jobs:
build_ifcopenshell:
runs-on: windows-2019
runs-on: windows-2022
strategy:
fail-fast: false
matrix:
@@ -21,7 +21,7 @@ jobs:
uses: actions/checkout@v3
with:
repository: IfcOpenShell/build-outputs
path: _deps-vs2019-x64-installed
path: _deps-vs2022-x64-installed
ref: windows-${{ matrix.arch }}
lfs: true
token: ${{ secrets.BUILD_REPO_TOKEN }}
@@ -40,7 +40,7 @@ jobs:
- name: Unpack Dependencies
run: |
cd _deps-vs2019-x64-installed
cd _deps-vs2022-x64-installed
Get-ChildItem -Path . -Filter 'cache-*.zip' | ForEach-Object {
7z x $_.FullName
}
@@ -53,17 +53,17 @@ jobs:
for /f "tokens=1,2,3 delims=." %%a in ("%PYTHON_VERSION%") do (
set PY_VER_MAJOR_MINOR=%%a%%b
)
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
SET IFCOS_INSTALL_PYTHON=FALSE
cd win
echo y | call build-deps.cmd vs2019-x64 Release
echo y | call build-deps.cmd vs2022-x64 Release
SET PYTHONHOME=C:\Python\${{ matrix.python }}
call run-cmake.bat vs2019-x64 -DENABLE_BUILD_OPTIMIZATIONS=On -DGLTF_SUPPORT=ON -DADD_COMMIT_SHA=ON -DVERSION_OVERRIDE=ON
call install-ifcopenshell.bat vs2019-x64 Release
call run-cmake.bat vs2022-x64 -DENABLE_BUILD_OPTIMIZATIONS=On -DGLTF_SUPPORT=ON -DADD_COMMIT_SHA=ON -DVERSION_OVERRIDE=ON
call install-ifcopenshell.bat vs2022-x64 Release
- name: Pack Dependencies
run: |
cd _deps-vs2019-x64-installed
cd _deps-vs2022-x64-installed
Get-ChildItem -Path . -Directory | ForEach-Object {
$cacheFile = "cache-$($_.Name).zip"
echo $cacheFile
@@ -74,7 +74,7 @@ jobs:
- name: Commit and Push Changes to Build Repository
run: |
cd _deps-vs2019-x64-installed
cd _deps-vs2022-x64-installed
git config user.name "IfcOpenBot"
git config user.email "ifcopenbot@ifcopenshell.org"
git add *.zip
@@ -90,7 +90,7 @@ jobs:
if ("${{ matrix.python }}" -eq "3.9.11") {
# only for the first python version the executables are assembled for upload
cd _installed-vs2019-x64/bin
cd _installed-vs2022-x64/bin
Get-ChildItem -Path . | ForEach-Object {
echo $_
$exe = $_.Name
@@ -124,4 +124,7 @@ jobs:
AWS_MAX_ATTEMPTS: 3
run: |
dir "$env:USERPROFILE\output"
aws s3 cp "$env:USERPROFILE\output" s3://ifcopenshell-builds/ --recursive --debug
foreach ($zip in Get-ChildItem -Path "$env:USERPROFILE\output" -Filter *.zip) {
aws s3 cp "$($zip.FullName)" s3://ifcopenshell-builds/ --debug
Start-Sleep -Seconds 5
}
+24 -12
View File
@@ -4,9 +4,6 @@ on:
push:
pull_request:
env:
PYTHON_VERSION: "3.12"
jobs:
lint-formatting:
runs-on: ubuntu-latest
@@ -17,27 +14,39 @@ jobs:
- name: Action - install python
uses: actions/setup-python@v5.3.0
with:
python-version: "${{ env.PYTHON_VERSION }}"
python-version: "3.9"
- name: Action - install python
uses: actions/setup-python@v5.3.0
with:
python-version: "3.11"
- name: Install dependencies
run: |
python3 -m pip install --upgrade pip
python3 -m pip install 'black>=24.10.0'
curl -LsSf https://astral.sh/uv/install.sh | sh
uv tool install ruff
uv tool install black
# black doesn't catch all syntax errors, so we check them explicitly.
- name: Check syntax errors
id: syntax-errors
run: |
ERROR=0
python3 -W error -m compileall -q src/ifcopenshell-python || ERROR=1
python3 -W error -m compileall -q src/bonsai || ERROR=1
python3.9 -W error -m compileall -q src/ifcopenshell-python || ERROR=1
python3.11 -W error -m compileall -q src/bonsai || ERROR=1
exit $ERROR
continue-on-error: true
- name: Black formatter
id: linting
id: black
run: |
python3 -m black --diff --check .
uvx black --diff --check .
continue-on-error: true
- name: Ruff check
id: ruff
run: |
uvx ruff check
continue-on-error: true
- name: Final check
@@ -46,7 +55,10 @@ jobs:
if [ "${{ steps.syntax-errors.outcome }}" != "success" ]; then
echo "::error::Syntax errors check failed, see 'syntax-errors' step for the details." && ERROR=1
fi
if [ "${{ steps.linting.outcome }}" != "success" ]; then
echo "::error::Black formatting check failed, see 'linting' step for the details." && ERROR=1
if [ "${{ steps.black.outcome }}" != "success" ]; then
echo "::error::Black formatting check failed, see 'black' step for the details." && ERROR=1
fi
if [ "${{ steps.ruff.outcome }}" != "success" ]; then
echo "::error::Ruff check failed, see 'ruff' step for the details." && ERROR=1
fi
exit $ERROR
@@ -44,6 +44,7 @@ jobs:
cmake \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_STANDARD=17 \
-DCMAKE_INSTALL_PREFIX=$PWD/install/ \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=/usr \
@@ -44,6 +44,8 @@ jobs:
}
steps:
- uses: actions/checkout@v2
with:
submodules: recursive
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
@@ -39,6 +39,8 @@ jobs:
}
steps:
- uses: actions/checkout@v2
with:
submodules: recursive
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
+16 -6
View File
@@ -82,6 +82,7 @@ jobs:
cmake \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_STANDARD=17 \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=/usr \
-DCMAKE_SYSTEM_PREFIX_PATH=/usr \
@@ -127,12 +128,21 @@ jobs:
cd ../src/ifcopenshell-python
mv ifcopenshell ifcopenshell-local # Force testing on installed module
pip install -e ../ifcpatch --no-deps # Needed for sql.py tests.
make test
cd ../bcf && make test
ERROR=0
make test || ERROR=1
cd ../bcf && make test || ERROR=1
pip install requests
cd ../bsdd && make test
cd ../bsdd && make test || ERROR=1
pip install deepdiff
cd ../ifcdiff && make test
cd ../ifcpatch && make test
cd ../ifcdiff && make test || ERROR=1
cd ../ifcpatch && make test || ERROR=1
pip install -e ../ifctester --no-deps
cd ../ifctester && make test
cd ../ifctester && make test || ERROR=1
# Run mathutils related tests at the end to ensure no other code is relying on mathutils.
cd ../ifcopenshell-python
pip install mathutils
make test-mathutils || ERROR=1
if [ $ERROR -ne 0 ]; then
echo "One or more tests failed";
exit 1;
fi
+1
View File
@@ -46,6 +46,7 @@ jobs:
mkdir build && cd build
cmake \
-DCMAKE_INSTALL_PREFIX=$PWD/install/ \
-DCMAKE_CXX_STANDARD=17 \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=/usr \
-DCMAKE_SYSTEM_PREFIX_PATH=/usr \
+2 -1
View File
@@ -86,6 +86,7 @@ src/bonsai/bonsai/bim/data/build/
src/bonsai/bonsai/bim/data/gantt/index.html
src/bonsai/bonsai/bim/data/gantt/jsgantt.js
src/bonsai/bonsai/bim/data/gantt/jsgantt.css
src/bonsai/bonsai/bim/data/webui/static/js/jquery.min.js
src/bonsai/drawings
src/bonsai/layouts
@@ -106,4 +107,4 @@ src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
src/bonsai/bonsai/bim/schema/Brick.ttl
bonsaiDecoratorForLoads.code-workspace
dev_environment.bat
+8 -1
View File
@@ -18,7 +18,12 @@
################################################################################
cmake_minimum_required(VERSION 3.21)
set(CMAKE_CXX_STANDARD 17)
if (NOT DEFINED CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 17)
endif()
if (CMAKE_CXX_STANDARD LESS 17)
message(FATAL_ERROR "C++17 or newer is required.")
endif()
set(CMAKE_CXX_STANDARD_REQUIRED ON) # not necessary, but encouraged
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
@@ -1289,6 +1294,8 @@ endif()
list(APPEND CPACK_SOURCE_IGNORE_FILES
"/\\\\.git"
"/build/"
"/.pytest_cache/"
"/__pycache__/"
)
set(CPACK_SOURCE_INSTALLED_DIRECTORIES "${CMAKE_SOURCE_DIR}/..;/")
set(CPACK_PACKAGE_NAME "${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}")
+1
View File
@@ -9,6 +9,7 @@ set LIBXML2="%LIBRARY_PREFIX%/lib/libxml2.lib"
cmake -G "Ninja" ^
-D SCHEMA_VERSIONS="2x3;4;4x1;4x3_add2" ^
-D CMAKE_BUILD_TYPE:STRING=Release ^
-D CMAKE_CXX_STANDARD=17 ^
-D CMAKE_INSTALL_PREFIX:FILEPATH="%LIBRARY_PREFIX%" ^
-D CMAKE_PREFIX_PATH:FILEPATH="%LIBRARY_PREFIX%" ^
-D CMAKE_SYSTEM_PREFIX_PATH:FILEPATH="%LIBRARY_PREFIX%" ^
+1
View File
@@ -16,6 +16,7 @@ cmake ${CMAKE_ARGS} -G Ninja \
-DSCHEMA_VERSIONS="2x3;4;4x1;4x3_add2" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=$PREFIX \
-DCMAKE_CXX_STANDARD=17 \
${CMAKE_PLATFORM_FLAGS[@]} \
-DCMAKE_PREFIX_PATH=$PREFIX \
-DCMAKE_SYSTEM_PREFIX_PATH=$PREFIX \
+5 -2
View File
@@ -1,3 +1,6 @@
python:
- 3.12
occt:
- 7.8.1
@@ -28,7 +31,7 @@ hdf5:
libboost_devel:
- '1.86'
libxml2:
- '2'
- 2.13
mpfr:
- '4'
gmp:
@@ -52,4 +55,4 @@ MACOSX_DEPLOYMENT_TARGET: # [osx]
- 10.13 # [osx and x86_64]
CONDA_BUILD_SYSROOT: # [osx]
- "/Users/runner/work/MacOSX10.13.sdk" # [osx and x86_64]
- "/Users/runner/work/MacOSX10.13.sdk" # [osx and x86_64]
+40 -13
View File
@@ -69,6 +69,7 @@ import sysconfig
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
import time
from urllib.request import urlretrieve
@@ -88,14 +89,14 @@ OCE_VERSION = "0.18.3"
OCCT_VERSION = "7.8.1"
BOOST_VERSION = "1.86.0"
PCRE_VERSION = "8.41"
LIBXML2_VERSION = "2.9.11"
LIBXML2_VERSION = "2.13.8"
SWIG_VERSION = "4.0.2"
OPENCOLLADA_VERSION = "v1.6.68"
HDF5_VERSION = "1.12.1"
GMP_VERSION = "6.2.1"
MPFR_VERSION = "3.1.6" # latest is 4.1.0
CGAL_VERSION = "5.3"
CGAL_VERSION = "5.6.x-branch"
USD_VERSION = "23.05"
TBB_VERSION = "2021.9.0"
@@ -323,7 +324,7 @@ def run_autoconf(arg1, configure_args, cwd):
if "wasm" in flags:
wasm.append("emconfigure")
run([*wasm, "/bin/sh", "../configure"] + configure_args + [f"--prefix={prefix}"], cwd=cwd)
run([*wasm, "/bin/sh", "../configure", *(["--host=wasm32"] if "wasm" in flags and not any(s.startswith('--host') for s in configure_args) else []), *configure_args, f"--prefix={prefix}"], cwd=cwd)
def run_cmake(arg1, cmake_args, cmake_dir=None, cwd=None):
@@ -336,7 +337,7 @@ def run_cmake(arg1, cmake_args, cmake_dir=None, cwd=None):
if "wasm" in flags:
wasm.append("emcmake")
run([*wasm, "cmake", P, *cmake_args, f"-DCMAKE_BUILD_TYPE={BUILD_CFG}", f"-DBUILD_SHARED_LIBS={OFF_ON[not BUILD_STATIC]}"], cwd=cwd)
run([*wasm, "cmake", P, *cmake_args, f"-DCMAKE_BUILD_TYPE={BUILD_CFG}", f"-DBUILD_SHARED_LIBS={OFF_ON[not BUILD_STATIC]}", f"-DCMAKE_CXX_FLAGS='{os.environ['CXXFLAGS']}'", f"-DCMAKE_C_FLAGS='{os.environ['CFLAGS']}'", f"-DCMAKE_SHARED_LINKER_FLAGS={os.environ['LDFLAGS']}"], cwd=cwd)
def git_clone_or_pull_repository(clone_url, target_dir, revision=None):
@@ -362,7 +363,7 @@ def git_clone_or_pull_repository(clone_url, target_dir, revision=None):
run([git, "checkout", revision], cwd=target_dir)
def build_dependency(name, mode, build_tool_args, download_url, download_name, download_tool=download_tool_default, revision=None, patch=None, additional_files={}, no_append_name=False, **kwargs):
def build_dependency(name, mode, build_tool_args, download_url, download_name, download_tool=download_tool_default, revision=None, patch=None, shell=None, pre_compile_subs=[], additional_files={}, no_append_name=False, **kwargs):
"""Handles building of dependencies with different tools (which are
distinguished with the `mode` argument. `build_tool_args` is expected to be
a list which is necessary in order to not mess up quoting of compiler and
@@ -385,7 +386,14 @@ def build_dependency(name, mode, build_tool_args, download_url, download_name, d
download_path = os.path.join(build_dir, download_name)
if not os.path.exists(download_path):
urlretrieve(url, os.path.join(build_dir, download_path))
for _ in range(3):
try:
urlretrieve(url, os.path.join(build_dir, download_path))
break
except ConnectionError as e:
print(e, "... retrying...")
time.sleep(30.)
continue
else:
logger.info(f"Download '{download_path}' already exists, assuming it's an undamaged download and that it has been extracted if possible, skipping")
elif download_tool == download_tool_git:
@@ -404,6 +412,8 @@ def build_dependency(name, mode, build_tool_args, download_url, download_name, d
compr = "gz"
elif download_name.endswith(".tar.bz2"):
compr = "bz2"
elif download_name.endswith(".tar.xz"):
compr = "xz"
else:
raise RuntimeError("fix source for new download type")
download_tarfile = tarfile.open(name=download_tarfile_path, mode=f"r:{compr}")
@@ -430,6 +440,9 @@ def build_dependency(name, mode, build_tool_args, download_url, download_name, d
except Exception as e:
# Assert that the patch has already been applied
run(["patch", "-p1", "--batch", "--reverse", "--dry-run", "-i", patch_abs], cwd=extract_dir)
if shell is not None:
sp.run(shell, shell=True, check=True, cwd=extract_dir)
if mode == "ctest":
run(["ctest", "-S", "HDF5config.cmake,BUILD_GENERATOR=Unix", "-C", BUILD_CFG, "-V", "-O", "hdf5.log"], cwd=extract_dir)
@@ -451,6 +464,12 @@ def build_dependency(name, mode, build_tool_args, download_url, download_name, d
run_cmake(name, build_tool_args, cwd=extract_build_dir)
else:
raise ValueError()
for fn, before, after in pre_compile_subs:
with open(os.path.join(extract_dir, fn), 'r') as f:
s = f.read()
s = s.replace(before, after)
with open(os.path.join(extract_dir, fn), 'w') as f:
f.write(s)
logger.info(f"\rBuilding {name}... ")
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "VERBOSE=1"], cwd=extract_build_dir)
logger.info(f"\rInstalling {name}... ")
@@ -478,9 +497,6 @@ ADDITIONAL_ARGS = []
if platform.system() == "Darwin":
ADDITIONAL_ARGS = [f"-mmacosx-version-min={TOOLSET}"] + ADDITIONAL_ARGS
if "wasm" in flags:
ADDITIONAL_ARGS.extend(("-sWASM_BIGINT", "-fwasm-exceptions"))
# If the linker supports GC sections, set it up to reduce binary file size
# -fPIC is required for the shared libraries to work
@@ -491,7 +507,11 @@ CFLAGS = os.environ.get("CFLAGS", "")
LDFLAGS = os.environ.get("LDFLAGS", "")
ADDITIONAL_ARGS_STR = " ".join(ADDITIONAL_ARGS)
if "wasm" not in flags and sp.call([bash, "-c", "ld --gc-sections 2>&1 | grep -- --gc-sections &> /dev/null"]) != 0:
if "wasm" in flags:
CFLAGS_MINIMAL = CXXFLAGS_MINIMAL = CFLAGS = CXXFLAGS = os.environ['SIDE_MODULE_CFLAGS']
LDFLAGS = os.environ['SIDE_MODULE_LDFLAGS']
elif sp.call([bash, "-c", "ld --gc-sections 2>&1 | grep -- --gc-sections &> /dev/null"]) != 0:
CXXFLAGS_MINIMAL = f"{CXXFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
CFLAGS_MINIMAL = f"{CFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
if BUILD_STATIC:
@@ -517,6 +537,7 @@ if "lto" in flags:
locals()[f] += f" -flto={IFCOS_NUM_BUILD_PROCS}"
os.environ["CXXFLAGS"] = CXXFLAGS
os.environ["CPPFLAGS"] = CXXFLAGS
os.environ["CFLAGS"] = CFLAGS
os.environ["LDFLAGS"] = LDFLAGS
@@ -653,8 +674,8 @@ if "libxml2" in targets:
"--without-iconv",
"--without-lzma"
],
download_url="ftp://xmlsoft.org/libxml2/",
download_name=f"libxml2-{LIBXML2_VERSION}.tar.gz"
download_url=f"https://download.gnome.org/sources/libxml2/{'.'.join(LIBXML2_VERSION.split('.')[0:2])}/",
download_name=f"libxml2-{LIBXML2_VERSION}.tar.xz"
)
if "OpenCOLLADA" in targets:
@@ -687,9 +708,11 @@ if "OpenCOLLADA" in targets:
if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flags:
# Python should not be built with -fvisibility=hidden, from experience that introduces segfaults
OLD_CPP_FLAGS = os.environ["CPPFLAGS"]
OLD_CXX_FLAGS = os.environ["CXXFLAGS"]
OLD_C_FLAGS = os.environ["CFLAGS"]
os.environ["CXXFLAGS"] = CXXFLAGS_MINIMAL
os.environ["CPPFLAGS"] = CXXFLAGS_MINIMAL
os.environ["CFLAGS"] = CFLAGS_MINIMAL
# On OSX a dynamic python library is built or it would not be compatible
@@ -719,6 +742,7 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
):
raise e
os.environ["CPPFLAGS"] = OLD_CPP_FLAGS
os.environ["CXXFLAGS"] = OLD_CXX_FLAGS
os.environ["CFLAGS"] = OLD_C_FLAGS
@@ -764,6 +788,7 @@ if "cgal" in targets:
name=f"gmp-{GMP_VERSION}",
mode="autoconf",
build_tool_args=[ENABLE_FLAG, DISABLE_FLAG, "--with-pic", *gmp_args],
pre_compile_subs=([("build/config.h", "HAVE_OBSTACK_VPRINTF 1", "HAVE_OBSTACK_VPRINTF 0")] if "wasm" in flags else []),
download_url="https://ftp.gnu.org/gnu/gmp/",
download_name=f"gmp-{GMP_VERSION}.tar.bz2"
)
@@ -792,7 +817,7 @@ if "cgal" in targets:
download_url="https://github.com/CGAL/cgal.git",
download_name="cgal",
download_tool=download_tool_git,
revision=f"v{CGAL_VERSION}"
revision=CGAL_VERSION
)
if "usd" in targets:
@@ -842,6 +867,7 @@ os.makedirs(executables_dir, exist_ok=True)
cmake_args = [
"-DCMAKE_CXX_STANDARD=" "17",
"-DUSE_MMAP=" "OFF",
"-DBUILD_EXAMPLES=" "OFF",
"-DBUILD_SHARED_LIBS=" +OFF_ON[not BUILD_STATIC],
@@ -947,6 +973,7 @@ if "IfcOpenShell-Python" in targets:
if "wasm" in flags:
ADDITIONAL_ARGS = f"-Wl,-undefined,suppress -sSIDE_MODULE=2 -sEXPORTED_FUNCTIONS=_PyInit__ifcopenshell_wrapper"
os.environ["CPPFLAGS"] = f"{CXXFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["CXXFLAGS"] = f"{CXXFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["CFLAGS"] = f"{CFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["LDFLAGS"] = f"{LDFLAGS} {ADDITIONAL_ARGS}"
+1 -1
View File
@@ -7,7 +7,7 @@ source:
build:
script: |
BUILD_CFG=Release python nix/build-all.py --without-hdf5 --without-opencollada --without-swig --without-pcre -v --wasm --py312 IfcOpenShell-Python
BUILD_CFG=Release python nix/build-all.py --without-hdf5 --without-opencollada --without-swig --without-pcre -v --wasm --py313 IfcOpenShell-Python
mv package/ifcopenshell .
cp pyodide/setup.py .
+49 -24
View File
@@ -1,38 +1,63 @@
[tool.black]
line-length = 120
include = '''
src/(
bcf
|bcfserver
|bonsai
|bsdd
|foundationserver
|ifc2ca
|ifc4d
|ifc5d
|ifcbimtester
|ifcblender
|ifccityjson
|ifcclash
|ifccsv
|ifcdiff
|ifcfm
|ifcpatch
|ifctester
|ifcopenshell-python
|ifcsverchok
|opencdeserver
)/.*.py$
'''
include = "src/.*.pyi?$"
extend-exclude = '''
src/ifcopenshell-python/ifcopenshell/express/*
|src/ifcopenshell-python/ifcopenshell/mvd/*
|src/ifcopenshell-python/ifcopenshell/simple_spf/*
|src/ifc2ca/templates/*
|src/ifcconvert/cityjson/*
|src/svgfill
|src/exterior-shell-extractor
|src/pyodide
'''
[tool.pyright]
reportInvalidTypeForm = false
disableBytesTypePromotions = true
reportUnnecessaryTypeIgnoreComment = true
# Just to add a quick insert of `# pyright: ignore[xxx]` comments in Pylance.
enableTypeIgnoreComments = false
# Define here general ruff settings,
# then they will be inherited projects .toml files.
# This allows using assuming different Python version for different projects.
[tool.ruff]
exclude = [
# Submodules.
"src/ifcconvert/cityjson",
"src/ifcopenshell-python/ifcopenshell/express",
"src/ifcopenshell-python/ifcopenshell/mvd",
"src/ifcopenshell-python/ifcopenshell/simple_spf",
"src/svgfill",
#
# Unformatted.
"src/exterior-shell-extractor",
# Incompatible with linter.
"src/ifc2ca/templates",
]
[tool.ruff.lint]
select = [
# Default Ruff rules.
# "E4", # imports
# "E7", # statements
"E9", # io errors
# "F", # pyflakes
#
"FA", # future annotations
"UP", # pyupgrade
"RUF015", # next() > list_comprehension[0]
]
ignore = [
"FA100", # Conflicts with Blender using annotations for props definitions.
# Maybe will enable later:
"UP007", # Optional to X | Y
"UP015", # Unnecessary mode argument
"UP028", # yield for -> yield from
"UP030", # implicit references for positional format fields
"UP031", # Replace % with .format
"UP032", # Replace .format with f-string
# Deprecated Ruff rules.
"UP038", # Use X | Y in isinstance
]
+1 -1
View File
@@ -20,7 +20,7 @@ def get_extensions_attributes(extensions: Extensions) -> dict[str, AttributeData
- subattribute name"""
possible_attributes = {}
for field in fields(type(extensions)):
field_type = field.type.__args__[0] # type: ignore [reportAttributeAccessIssue]
field_type = field.type.__args__[0] # pyright: ignore [reportAttributeAccessIssue]
subfield = next(iter(fields(field_type)))
xsd_name = subfield.metadata["name"]
possible_attributes[field.name] = AttributeData(field_type, subfield.name, xsd_name)
+2 -1
View File
@@ -6,6 +6,7 @@ Copyright (c) 2017-2020 Anthon van der Neut, Ruamel bvba
original idea from https://stackoverflow.com/a/19722365/1307905
"""
from __future__ import annotations
import zipfile
from io import BytesIO
from os import PathLike
@@ -47,7 +48,7 @@ class InMemoryZipFile:
def data(self) -> bytes:
return self.in_memory_data.getvalue()
def __enter__(self) -> "InMemoryZipFile":
def __enter__(self) -> InMemoryZipFile:
return self
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
+6 -5
View File
@@ -1,5 +1,6 @@
"""BCF XML V2 handler."""
from __future__ import annotations
import uuid
import warnings
import zipfile
@@ -31,7 +32,7 @@ class BcfXml:
self._extension_schema: Optional[bytes] = None
self._zip_file = self._load_zip_file()
def __enter__(self) -> "BcfXml":
def __enter__(self) -> BcfXml:
return self
def __exit__(self, *args: Any) -> None:
@@ -99,7 +100,7 @@ class BcfXml:
extensions = mdl_extensions.Extensions()
xs = "{http://www.w3.org/2001/XMLSchema}"
root = etree.parse(io.BytesIO((self.extension_schema)))
root = etree.parse(io.BytesIO(self.extension_schema))
attrs = bcf.agnostic.extensions.get_extensions_attributes(extensions)
xsd_to_attrs = {v.subattr_xsd_name: k for k, v in attrs.items()}
@@ -148,7 +149,7 @@ class BcfXml:
return topics
@classmethod
def load(cls, filename: Path, xml_handler: Optional[AbstractXmlParserSerializer] = None) -> Optional["BcfXml"]:
def load(cls, filename: Path, xml_handler: Optional[AbstractXmlParserSerializer] = None) -> Optional[BcfXml]:
"""
Create a BcfXml object from a file.
@@ -172,7 +173,7 @@ class BcfXml:
cls,
project_name: Optional[str] = None,
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> "BcfXml":
) -> BcfXml:
"""
Create a new BcfXml object.
@@ -261,7 +262,7 @@ class BcfXml:
)
# region Deprecated methods
def new_project(self) -> "BcfXml":
def new_project(self) -> BcfXml:
"""Deprecated method."""
warnings.warn("new_project is deprecated, use create_new instead.", DeprecationWarning)
return self.create_new()
+20 -16
View File
@@ -14,16 +14,20 @@
# Currently extensions support for v2 is only read-only.
import sys
from dataclasses import dataclass, field, fields
from typing import List, NamedTuple, Optional
from typing import Optional
@dataclass(slots=True, kw_only=True)
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(**DATACLASS_KWARGS)
class ExtensionsPriorities:
class Meta:
global_type = False
priority: List[str] = field(
priority: list[str] = field(
default_factory=list,
metadata={
"name": "Priority",
@@ -35,12 +39,12 @@ class ExtensionsPriorities:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ExtensionsSnippetTypes:
class Meta:
global_type = False
snippet_type: List[str] = field(
snippet_type: list[str] = field(
default_factory=list,
metadata={
"name": "SnippetType",
@@ -52,12 +56,12 @@ class ExtensionsSnippetTypes:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ExtensionsStages:
class Meta:
global_type = False
stage: List[str] = field(
stage: list[str] = field(
default_factory=list,
metadata={
"name": "Stage",
@@ -69,12 +73,12 @@ class ExtensionsStages:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ExtensionsTopicLabels:
class Meta:
global_type = False
topic_label: List[str] = field(
topic_label: list[str] = field(
default_factory=list,
metadata={
"name": "TopicLabel",
@@ -86,12 +90,12 @@ class ExtensionsTopicLabels:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ExtensionsTopicStatuses:
class Meta:
global_type = False
topic_status: List[str] = field(
topic_status: list[str] = field(
default_factory=list,
metadata={
"name": "TopicStatus",
@@ -103,12 +107,12 @@ class ExtensionsTopicStatuses:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ExtensionsTopicTypes:
class Meta:
global_type = False
topic_type: List[str] = field(
topic_type: list[str] = field(
default_factory=list,
metadata={
"name": "TopicType",
@@ -120,12 +124,12 @@ class ExtensionsTopicTypes:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ExtensionsUsers:
class Meta:
global_type = False
user: List[str] = field(
user: list[str] = field(
default_factory=list,
metadata={
"name": "UserIdType",
@@ -137,7 +141,7 @@ class ExtensionsUsers:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Extensions:
topic_types: Optional[ExtensionsTopicTypes] = field(
default=None,
+21 -18
View File
@@ -1,10 +1,13 @@
import sys
from dataclasses import dataclass, field
from typing import List, Optional
from typing import Optional
from xsdata.models.datatype import XmlDateTime
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class BimSnippet:
reference: str = field(
metadata={
@@ -38,7 +41,7 @@ class BimSnippet:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class CommentViewpoint:
class Meta:
global_type = False
@@ -53,7 +56,7 @@ class CommentViewpoint:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class HeaderFile:
class Meta:
global_type = False
@@ -109,7 +112,7 @@ class HeaderFile:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class TopicDocumentReference:
class Meta:
global_type = False
@@ -147,7 +150,7 @@ class TopicDocumentReference:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class TopicRelatedTopic:
class Meta:
global_type = False
@@ -162,7 +165,7 @@ class TopicRelatedTopic:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ViewPoint:
viewpoint: Optional[str] = field(
default=None,
@@ -198,7 +201,7 @@ class ViewPoint:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Comment:
date: XmlDateTime = field(
metadata={
@@ -258,9 +261,9 @@ class Comment:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Header:
file: List[HeaderFile] = field(
file: list[HeaderFile] = field(
default_factory=list,
metadata={
"name": "File",
@@ -271,9 +274,9 @@ class Header:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Topic:
reference_link: List[str] = field(
reference_link: list[str] = field(
default_factory=list,
metadata={
"name": "ReferenceLink",
@@ -305,7 +308,7 @@ class Topic:
"namespace": "",
},
)
labels: List[str] = field(
labels: list[str] = field(
default_factory=list,
metadata={
"name": "Labels",
@@ -385,7 +388,7 @@ class Topic:
"namespace": "",
},
)
document_reference: List[TopicDocumentReference] = field(
document_reference: list[TopicDocumentReference] = field(
default_factory=list,
metadata={
"name": "DocumentReference",
@@ -393,7 +396,7 @@ class Topic:
"namespace": "",
},
)
related_topic: List[TopicRelatedTopic] = field(
related_topic: list[TopicRelatedTopic] = field(
default_factory=list,
metadata={
"name": "RelatedTopic",
@@ -425,7 +428,7 @@ class Topic:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Markup:
header: Optional[Header] = field(
default=None,
@@ -443,7 +446,7 @@ class Markup:
"required": True,
}
)
comment: List[Comment] = field(
comment: list[Comment] = field(
default_factory=list,
metadata={
"name": "Comment",
@@ -451,7 +454,7 @@ class Markup:
"namespace": "",
},
)
viewpoints: List[ViewPoint] = field(
viewpoints: list[ViewPoint] = field(
default_factory=list,
metadata={
"name": "Viewpoints",
+5 -2
View File
@@ -1,8 +1,11 @@
import sys
from dataclasses import dataclass, field
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Project:
name: Optional[str] = field(
default=None,
@@ -21,7 +24,7 @@ class Project:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ProjectExtension:
project: Optional[Project] = field(
default=None,
+4 -1
View File
@@ -1,8 +1,11 @@
import sys
from dataclasses import dataclass, field
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Version:
detailed_version: Optional[str] = field(
default=None,
+29 -26
View File
@@ -1,6 +1,9 @@
import sys
from dataclasses import dataclass, field
from enum import Enum
from typing import List, Optional
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
class BitmapFormat(Enum):
@@ -8,7 +11,7 @@ class BitmapFormat(Enum):
JPG = "JPG"
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Component:
originating_system: Optional[str] = field(
default=None,
@@ -35,7 +38,7 @@ class Component:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Direction:
x: float = field(
metadata={
@@ -60,7 +63,7 @@ class Direction:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Point:
x: float = field(
metadata={
@@ -85,7 +88,7 @@ class Point:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ViewSetupHints:
spaces_visible: Optional[bool] = field(
default=None,
@@ -110,7 +113,7 @@ class ViewSetupHints:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ClippingPlane:
location: Point = field(
metadata={
@@ -128,12 +131,12 @@ class ClippingPlane:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ComponentColoringColor:
class Meta:
global_type = False
component: List[Component] = field(
component: list[Component] = field(
default_factory=list,
metadata={
"name": "Component",
@@ -151,9 +154,9 @@ class ComponentColoringColor:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ComponentSelection:
component: List[Component] = field(
component: list[Component] = field(
default_factory=list,
metadata={
"name": "Component",
@@ -163,12 +166,12 @@ class ComponentSelection:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ComponentVisibilityExceptions:
class Meta:
global_type = False
component: List[Component] = field(
component: list[Component] = field(
default_factory=list,
metadata={
"name": "Component",
@@ -178,7 +181,7 @@ class ComponentVisibilityExceptions:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Line:
start_point: Point = field(
metadata={
@@ -196,7 +199,7 @@ class Line:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class OrthogonalCamera:
"""
Attributes
@@ -236,7 +239,7 @@ class OrthogonalCamera:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class PerspectiveCamera:
"""
Attributes
@@ -281,7 +284,7 @@ class PerspectiveCamera:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class VisualizationInfoBitmap:
class Meta:
global_type = False
@@ -330,9 +333,9 @@ class VisualizationInfoBitmap:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ComponentColoring:
color: List[ComponentColoringColor] = field(
color: list[ComponentColoringColor] = field(
default_factory=list,
metadata={
"name": "Color",
@@ -342,7 +345,7 @@ class ComponentColoring:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ComponentVisibility:
exceptions: Optional[ComponentVisibilityExceptions] = field(
default=None,
@@ -360,12 +363,12 @@ class ComponentVisibility:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class VisualizationInfoClippingPlanes:
class Meta:
global_type = False
clipping_plane: List[ClippingPlane] = field(
clipping_plane: list[ClippingPlane] = field(
default_factory=list,
metadata={
"name": "ClippingPlane",
@@ -374,12 +377,12 @@ class VisualizationInfoClippingPlanes:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class VisualizationInfoLines:
class Meta:
global_type = False
line: List[Line] = field(
line: list[Line] = field(
default_factory=list,
metadata={
"name": "Line",
@@ -389,7 +392,7 @@ class VisualizationInfoLines:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Components:
view_setup_hints: Optional[ViewSetupHints] = field(
default=None,
@@ -421,7 +424,7 @@ class Components:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class VisualizationInfo:
"""
VisualizationInfo documentation.
@@ -462,7 +465,7 @@ class VisualizationInfo:
"type": "Element",
},
)
bitmap: List[VisualizationInfoBitmap] = field(
bitmap: list[VisualizationInfoBitmap] = field(
default_factory=list,
metadata={
"name": "Bitmap",
+2 -1
View File
@@ -1,5 +1,6 @@
"""BCF XML V2 Topic handler."""
from __future__ import annotations
import datetime
import tempfile
import uuid
@@ -152,7 +153,7 @@ class TopicHandler:
topic_type: str = "",
topic_status: str = "",
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> "TopicHandler":
) -> TopicHandler:
"""
Create a new BCF topic.
+2 -1
View File
@@ -1,6 +1,7 @@
import uuid
import zipfile
from typing import Any, Iterable, Optional, Literal, Union
from typing import Any, Optional, Literal, Union
from collections.abc import Iterable
import numpy as np
from ifcopenshell import entity_instance
+21 -21
View File
@@ -21,11 +21,11 @@ import http.server
import os
import tempfile
import time
import urllib
import urllib.parse
import uuid
import webbrowser
from re import A
from typing import Any, Optional, Tuple
from typing import Any, Optional
import requests
@@ -40,7 +40,7 @@ class OAuthReceiver(http.server.BaseHTTPRequestHandler):
self.send_response(200)
self.send_header("Content-type", "text/plain")
self.end_headers()
self.wfile.write("You have now authenticated :) You may now close this browser window.".encode("utf-8"))
self.wfile.write(b"You have now authenticated :) You may now close this browser window.")
class FoundationClient:
@@ -174,7 +174,7 @@ class BcfClient:
except requests.exceptions.HTTPError as e:
print(f"message: {response.reason}' '{response.status_code}' '{ e }")
def post(self, endpoint: str, data: Any = None, params: Any = None) -> Tuple[int, str]:
def post(self, endpoint: str, data: Any = None, params: Any = None) -> tuple[int, str]:
headers = {
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
"Content-type": "application/json",
@@ -192,7 +192,7 @@ class BcfClient:
print(f"message: {response.reason}' '{response.status_code}, {errh}")
return response.status_code, response.reason
def put(self, endpoint: str, data: Any = None, params: Any = None) -> Tuple[int, str]:
def put(self, endpoint: str, data: Any = None, params: Any = None) -> tuple[int, str]:
headers = {
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
"Content-type": "application/json",
@@ -210,7 +210,7 @@ class BcfClient:
print(f"message: {response.reason}' '{response.status_code}, {errh}")
return response.status_code, response.reason
def delete(self, endpoint: str, params: Any = None) -> Tuple[int, str]:
def delete(self, endpoint: str, params: Any = None) -> tuple[int, str]:
headers = {
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
"Content-type": "application/json",
@@ -237,7 +237,7 @@ class BcfClient:
},
)
def update_project(self, project_id: str = "", data: Any = None) -> Tuple[int, str]:
def update_project(self, project_id: str = "", data: Any = None) -> tuple[int, str]:
url = f"{self.baseurl}/projects/{project_id}"
headers = {"Authorization": f"Bearer {self.foundation_client.get_access_token()}"}
resp = requests.put(url, headers=headers, data=data)
@@ -276,16 +276,16 @@ class BcfClient:
},
)
def create_topic(self, project_id: str = "", data: Any = None) -> Tuple[int, str]:
def create_topic(self, project_id: str = "", data: Any = None) -> tuple[int, str]:
return self.post(f"/projects/{project_id}/topics", data=data)
def update_topic(self, project_id: str = "", topic_id: str = "", data: Any = None) -> Tuple[int, str]:
def update_topic(self, project_id: str = "", topic_id: str = "", data: Any = None) -> tuple[int, str]:
return self.put(f"/projects/{project_id}/topics/{topic_id}", data=data)
def delete_topic(self, project_id: str = "", topic_id: str = "") -> Tuple[int, str]:
def delete_topic(self, project_id: str = "", topic_id: str = "") -> tuple[int, str]:
return self.delete(f"/projects/{project_id}/topics/{topic_id}")
def get_snippet(self, project_id: str = "", topic_id: str = "") -> Tuple[int, str]:
def get_snippet(self, project_id: str = "", topic_id: str = "") -> tuple[int, str]:
headers = {
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
"Content-type": "application/octet-stream",
@@ -332,7 +332,7 @@ class BcfClient:
topic_id: str = "",
data: Any = None,
params: Any = None,
) -> Tuple[int, str]:
) -> tuple[int, str]:
return self.put(
f"/projects/{project_id}/topics/{topic_id}/files",
data=data,
@@ -347,7 +347,7 @@ class BcfClient:
topic_id: str = "",
data: Any = None,
params: Any = None,
) -> Tuple[int, str]:
) -> tuple[int, str]:
return self.post(
f"/projects/{project_id}/topics/{topic_id}/comments",
data=data,
@@ -363,7 +363,7 @@ class BcfClient:
},
)
def delete_comment(self, project_id: str = "", topic_id: str = "", comment_id: str = "") -> Tuple[int, str]:
def delete_comment(self, project_id: str = "", topic_id: str = "", comment_id: str = "") -> tuple[int, str]:
return self.delete(f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}")
def update_comment(
@@ -372,7 +372,7 @@ class BcfClient:
topic_id: str = "",
comment_id: str = "",
data: Any = None,
) -> Tuple[int, str]:
) -> tuple[int, str]:
return self.put(
f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}",
data=data,
@@ -387,7 +387,7 @@ class BcfClient:
},
)
def create_viewpoints(self, project_id: str = "", topic_id: str = "", data: Any = None) -> Tuple[int, str]:
def create_viewpoints(self, project_id: str = "", topic_id: str = "", data: Any = None) -> tuple[int, str]:
return self.post(
f"/projects/{project_id}/topics/{topic_id}/viewpoints",
data=data,
@@ -408,7 +408,7 @@ class BcfClient:
project_id: str = "",
topic_id: str = "",
viewpoint_id: str = "",
) -> Tuple[int, str]:
) -> tuple[int, str]:
return self.delete(
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}",
)
@@ -478,7 +478,7 @@ class BcfClient:
project_id: str = "",
topic_id: str = "",
data: Any = None,
) -> Tuple[int, str]:
) -> tuple[int, str]:
return self.put(
f"/projects/{project_id}/topics/{topic_id}/related_topics",
data=data,
@@ -498,7 +498,7 @@ class BcfClient:
project_id: str = "",
topic_id: str = "",
data: Any = None,
) -> Tuple[int, str]:
) -> tuple[int, str]:
return self.post(
f"/projects/{project_id}/topics/{topic_id}/document_references",
data=data,
@@ -510,7 +510,7 @@ class BcfClient:
topic_id: str = "",
document_reference_id: str = "",
data: Any = None,
) -> Tuple[int, str]:
) -> tuple[int, str]:
return self.put(
f"/projects/{project_id}/topics/{topic_id}/document_references/{document_reference_id}",
data=data,
@@ -543,7 +543,7 @@ class BcfClient:
return response.status_code
def get_document(self, project_id: str = "", topic_id: str = "", document_id: str = "") -> Tuple[int, str]:
def get_document(self, project_id: str = "", topic_id: str = "", document_id: str = "") -> tuple[int, str]:
headers = {
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
"Content-type": "application/octet-stream",
+5 -4
View File
@@ -1,5 +1,6 @@
"""BCF XML V3 handlers."""
from __future__ import annotations
import uuid
import warnings
import zipfile
@@ -30,7 +31,7 @@ class BcfXml:
self._documents: Optional[DocumentsHandler] = None
self._zip_file = self._load_zip_file()
def __enter__(self) -> "BcfXml":
def __enter__(self) -> BcfXml:
return self
def __exit__(self, *args: Any) -> None:
@@ -116,7 +117,7 @@ class BcfXml:
return self._documents
@classmethod
def load(cls, filename: Path, xml_handler: Optional[AbstractXmlParserSerializer] = None) -> Optional["BcfXml"]:
def load(cls, filename: Path, xml_handler: Optional[AbstractXmlParserSerializer] = None) -> Optional[BcfXml]:
"""
Create a BcfXml object from a file.
@@ -141,7 +142,7 @@ class BcfXml:
project_name: Optional[str] = None,
extensions: Optional[mdl.Extensions] = None,
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> "BcfXml":
) -> BcfXml:
"""
Create a new BcfXml object.
@@ -239,7 +240,7 @@ class BcfXml:
)
# region Deprecated methods
def new_project(self) -> "BcfXml":
def new_project(self) -> BcfXml:
"""Deprecated method."""
warnings.warn("new_project is deprecated, use create_new instead.", DeprecationWarning)
return self.create_new()
+8 -5
View File
@@ -1,8 +1,11 @@
import sys
from dataclasses import dataclass, field
from typing import List, Optional
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Document:
filename: str = field(
metadata={
@@ -34,12 +37,12 @@ class Document:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class DocumentInfoDocuments:
class Meta:
global_type = False
document: List[Document] = field(
document: list[Document] = field(
default_factory=list,
metadata={
"name": "Document",
@@ -49,7 +52,7 @@ class DocumentInfoDocuments:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class DocumentInfo:
documents: Optional[DocumentInfoDocuments] = field(
default=None,
+19 -16
View File
@@ -1,13 +1,16 @@
import sys
from dataclasses import dataclass, field
from typing import List, Optional
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ExtensionsPriorities:
class Meta:
global_type = False
priority: List[str] = field(
priority: list[str] = field(
default_factory=list,
metadata={
"name": "Priority",
@@ -19,12 +22,12 @@ class ExtensionsPriorities:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ExtensionsSnippetTypes:
class Meta:
global_type = False
snippet_type: List[str] = field(
snippet_type: list[str] = field(
default_factory=list,
metadata={
"name": "SnippetType",
@@ -36,12 +39,12 @@ class ExtensionsSnippetTypes:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ExtensionsStages:
class Meta:
global_type = False
stage: List[str] = field(
stage: list[str] = field(
default_factory=list,
metadata={
"name": "Stage",
@@ -53,12 +56,12 @@ class ExtensionsStages:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ExtensionsTopicLabels:
class Meta:
global_type = False
topic_label: List[str] = field(
topic_label: list[str] = field(
default_factory=list,
metadata={
"name": "TopicLabel",
@@ -70,12 +73,12 @@ class ExtensionsTopicLabels:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ExtensionsTopicStatuses:
class Meta:
global_type = False
topic_status: List[str] = field(
topic_status: list[str] = field(
default_factory=list,
metadata={
"name": "TopicStatus",
@@ -87,12 +90,12 @@ class ExtensionsTopicStatuses:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ExtensionsTopicTypes:
class Meta:
global_type = False
topic_type: List[str] = field(
topic_type: list[str] = field(
default_factory=list,
metadata={
"name": "TopicType",
@@ -104,12 +107,12 @@ class ExtensionsTopicTypes:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ExtensionsUsers:
class Meta:
global_type = False
user: List[str] = field(
user: list[str] = field(
default_factory=list,
metadata={
"name": "User",
@@ -121,7 +124,7 @@ class ExtensionsUsers:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Extensions:
topic_types: Optional[ExtensionsTopicTypes] = field(
default=None,
+28 -25
View File
@@ -1,10 +1,13 @@
import sys
from dataclasses import dataclass, field
from typing import List, Optional
from typing import Optional
from xsdata.models.datatype import XmlDateTime
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class BimSnippet:
reference: str = field(
metadata={
@@ -44,7 +47,7 @@ class BimSnippet:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class CommentViewpoint:
class Meta:
global_type = False
@@ -59,7 +62,7 @@ class CommentViewpoint:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class DocumentReference:
document_guid: Optional[str] = field(
default=None,
@@ -100,7 +103,7 @@ class DocumentReference:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class File:
filename: Optional[str] = field(
default=None,
@@ -157,12 +160,12 @@ class File:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class TopicLabels:
class Meta:
global_type = False
label: List[str] = field(
label: list[str] = field(
default_factory=list,
metadata={
"name": "Label",
@@ -174,12 +177,12 @@ class TopicLabels:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class TopicReferenceLinks:
class Meta:
global_type = False
reference_link: List[str] = field(
reference_link: list[str] = field(
default_factory=list,
metadata={
"name": "ReferenceLink",
@@ -191,7 +194,7 @@ class TopicReferenceLinks:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class TopicRelatedTopicsRelatedTopic:
class Meta:
global_type = False
@@ -206,7 +209,7 @@ class TopicRelatedTopicsRelatedTopic:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ViewPoint:
viewpoint: Optional[str] = field(
default=None,
@@ -246,7 +249,7 @@ class ViewPoint:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Comment:
date: XmlDateTime = field(
metadata={
@@ -312,12 +315,12 @@ class Comment:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class HeaderFiles:
class Meta:
global_type = False
file: List[File] = field(
file: list[File] = field(
default_factory=list,
metadata={
"name": "File",
@@ -327,12 +330,12 @@ class HeaderFiles:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class TopicDocumentReferences:
class Meta:
global_type = False
document_reference: List[DocumentReference] = field(
document_reference: list[DocumentReference] = field(
default_factory=list,
metadata={
"name": "DocumentReference",
@@ -342,12 +345,12 @@ class TopicDocumentReferences:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class TopicRelatedTopics:
class Meta:
global_type = False
related_topic: List[TopicRelatedTopicsRelatedTopic] = field(
related_topic: list[TopicRelatedTopicsRelatedTopic] = field(
default_factory=list,
metadata={
"name": "RelatedTopic",
@@ -357,12 +360,12 @@ class TopicRelatedTopics:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class TopicViewpoints:
class Meta:
global_type = False
view_point: List[ViewPoint] = field(
view_point: list[ViewPoint] = field(
default_factory=list,
metadata={
"name": "ViewPoint",
@@ -372,7 +375,7 @@ class TopicViewpoints:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Header:
files: Optional[HeaderFiles] = field(
default=None,
@@ -384,12 +387,12 @@ class Header:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class TopicComments:
class Meta:
global_type = False
comment: List[Comment] = field(
comment: list[Comment] = field(
default_factory=list,
metadata={
"name": "Comment",
@@ -399,7 +402,7 @@ class TopicComments:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Topic:
reference_links: Optional[TopicReferenceLinks] = field(
default=None,
@@ -596,7 +599,7 @@ class Topic:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Markup:
header: Optional[Header] = field(
default=None,
+5 -2
View File
@@ -1,8 +1,11 @@
import sys
from dataclasses import dataclass, field
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Project:
name: Optional[str] = field(
default=None,
@@ -25,7 +28,7 @@ class Project:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ProjectInfo:
project: Project = field(
metadata={
+4 -1
View File
@@ -1,7 +1,10 @@
import sys
from dataclasses import dataclass, field
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Version:
version_id: str = field(
metadata={
+32 -28
View File
@@ -1,6 +1,10 @@
import sys
from dataclasses import dataclass, field
from enum import Enum
from typing import List, Optional
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
class BitmapFormat(Enum):
@@ -8,7 +12,7 @@ class BitmapFormat(Enum):
JPG = "jpg"
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Component:
originating_system: Optional[str] = field(
default=None,
@@ -39,7 +43,7 @@ class Component:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Direction:
x: float = field(
metadata={
@@ -64,7 +68,7 @@ class Direction:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Point:
x: float = field(
metadata={
@@ -89,7 +93,7 @@ class Point:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ViewSetupHints:
spaces_visible: bool = field(
default=False,
@@ -114,7 +118,7 @@ class ViewSetupHints:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Bitmap:
format: BitmapFormat = field(
metadata={
@@ -162,7 +166,7 @@ class Bitmap:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ClippingPlane:
location: Point = field(
metadata={
@@ -180,12 +184,12 @@ class ClippingPlane:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ComponentColoringColorComponents:
class Meta:
global_type = False
component: List[Component] = field(
component: list[Component] = field(
default_factory=list,
metadata={
"name": "Component",
@@ -195,9 +199,9 @@ class ComponentColoringColorComponents:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ComponentSelection:
component: List[Component] = field(
component: list[Component] = field(
default_factory=list,
metadata={
"name": "Component",
@@ -206,12 +210,12 @@ class ComponentSelection:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ComponentVisibilityExceptions:
class Meta:
global_type = False
component: List[Component] = field(
component: list[Component] = field(
default_factory=list,
metadata={
"name": "Component",
@@ -220,7 +224,7 @@ class ComponentVisibilityExceptions:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Line:
start_point: Point = field(
metadata={
@@ -238,7 +242,7 @@ class Line:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class OrthogonalCamera:
"""
Attributes
@@ -288,7 +292,7 @@ class OrthogonalCamera:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class PerspectiveCamera:
"""
Attributes
@@ -344,7 +348,7 @@ class PerspectiveCamera:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ComponentColoringColor:
class Meta:
global_type = False
@@ -366,7 +370,7 @@ class ComponentColoringColor:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ComponentVisibility:
view_setup_hints: Optional[ViewSetupHints] = field(
default=None,
@@ -391,12 +395,12 @@ class ComponentVisibility:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class VisualizationInfoBitmaps:
class Meta:
global_type = False
bitmap: List[Bitmap] = field(
bitmap: list[Bitmap] = field(
default_factory=list,
metadata={
"name": "Bitmap",
@@ -405,12 +409,12 @@ class VisualizationInfoBitmaps:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class VisualizationInfoClippingPlanes:
class Meta:
global_type = False
clipping_plane: List[ClippingPlane] = field(
clipping_plane: list[ClippingPlane] = field(
default_factory=list,
metadata={
"name": "ClippingPlane",
@@ -419,12 +423,12 @@ class VisualizationInfoClippingPlanes:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class VisualizationInfoLines:
class Meta:
global_type = False
line: List[Line] = field(
line: list[Line] = field(
default_factory=list,
metadata={
"name": "Line",
@@ -433,9 +437,9 @@ class VisualizationInfoLines:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class ComponentColoring:
color: List[ComponentColoringColor] = field(
color: list[ComponentColoringColor] = field(
default_factory=list,
metadata={
"name": "Color",
@@ -444,7 +448,7 @@ class ComponentColoring:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class Components:
selection: Optional[ComponentSelection] = field(
default=None,
@@ -469,7 +473,7 @@ class Components:
)
@dataclass(slots=True, kw_only=True)
@dataclass(**DATACLASS_KWARGS)
class VisualizationInfo:
"""
VisualizationInfo documentation.
+4 -3
View File
@@ -1,5 +1,6 @@
"""BCF XML V3 Topic handler."""
from __future__ import annotations
import datetime
import uuid
import zipfile
@@ -91,12 +92,12 @@ class TopicHandler:
self._bim_snippet = value
@property
def viewpoints(self) -> dict[str, "VisualizationInfoHandler"]:
def viewpoints(self) -> dict[str, VisualizationInfoHandler]:
if self._viewpoints is None:
self._viewpoints = self._load_viewpoints()
return self._viewpoints
def _load_viewpoints(self) -> dict[str, "VisualizationInfoHandler"]:
def _load_viewpoints(self) -> dict[str, VisualizationInfoHandler]:
if self._topic_dir and self.topic.viewpoints and (viewpoints := self.topic.viewpoints.view_point):
return VisualizationInfoHandler.from_topic_viewpoints(self._topic_dir, viewpoints)
return {}
@@ -139,7 +140,7 @@ class TopicHandler:
topic_type: str = "",
topic_status: str = "",
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> "TopicHandler":
) -> TopicHandler:
"""
Create a new BCF topic.
+2 -1
View File
@@ -1,6 +1,7 @@
import uuid
import zipfile
from typing import Any, Iterable, Optional, Literal, Union
from typing import Any, Optional, Literal, Union
from collections.abc import Iterable
import numpy as np
from ifcopenshell import entity_instance
+3 -3
View File
@@ -1,6 +1,6 @@
"""XML Parser and Serializer factories."""
from typing import Optional, Protocol, Type, TypeVar
from typing import Optional, Protocol, TypeVar
from xsdata.formats.dataclass.context import XmlContext
from xsdata.formats.dataclass.parsers import XmlParser
@@ -29,7 +29,7 @@ T = TypeVar("T")
class AbstractXmlParserSerializer(Protocol):
"""XML Parser and serializer wrapper."""
def parse(self, xml: bytes, clazz: Type[T]) -> T:
def parse(self, xml: bytes, clazz: type[T]) -> T:
"""
Parse an XML file to an object.
@@ -61,7 +61,7 @@ class XmlParserSerializer:
self.parser = build_xml_parser(self.context)
self.serializer = build_serializer(self.context)
def parse(self, xml: bytes, clazz: Type[T]) -> T:
def parse(self, xml: bytes, clazz: type[T]) -> T:
"""
Parse an XML file to an object.
+1
View File
@@ -16,6 +16,7 @@ dependencies = [
"xsdata>=24.4",
"numpy",
"ifcopenshell",
"requests",
]
version = "0.0.0"
classifiers = [
+6 -4
View File
@@ -87,7 +87,7 @@ BLENDER_PLATFORM:=windows-x64
endif
# Current build commit hash.
OLD:=c49ca69
OLD:=ec13294
.PHONY: bump
bump:
ifndef NEW
@@ -138,7 +138,7 @@ endif
# odfpy doesn't come with its own wheel, so whee'l (get it?) create it ourselves!
cd build && . env/$(VENV_ACTIVATE) && $(PIP) wheel odfpy --wheel-dir=./wheels
# Required by IFCCityJSON
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download cjio --dest=./wheels
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download "cjio >=0.8, <0.10" --dest=./wheels
# Required in general for sorting all sorts of stuff in a nice way
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download natsort --dest=./wheels
# Provides express rule validation for ifcopenshell.validate
@@ -193,13 +193,15 @@ else
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pyradiance $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
endif
# jQuery is required for web UI functionality
cd build/bonsai/bim/data/webui/static/js/ && wget https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js
# Provides jsgantt-improved supports for web-based construction sequencing gantt charts
cd build/bonsai/bim/data/gantt/ && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.js
cd build/bonsai/bim/data/gantt/ && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.css
# Provides IFCJSON functionality
# TODO: replace with main repo if https://github.com/IFCJSON-Team/IFC2JSON_python/pull/3 is merged.
cd build && wget -O ifc2json.zip https://github.com/Moult/IFC2JSON_python/archive/refs/heads/feature-ios-v0.8.0.zip
cd build && wget -O ifc2json.zip https://github.com/IFCJSON-Team/IFC2JSON_python/archive/refs/heads/master.zip
cd build && unzip ifc2json.zip && rm ifc2json.zip
# IFCJSON doesn't have pyproject.toml, so we use python command.
cd build && . env/$(VENV_ACTIVATE) && cd IFC2JSON_python-*/file_converters && \
+2 -1
View File
@@ -40,7 +40,8 @@ import uuid
import shutil
from collections import deque
from pathlib import Path
from typing import Union, Any, Generator
from typing import Union, Any
from collections.abc import Generator
last_commit_hash = "8888888"
+3 -2
View File
@@ -22,7 +22,8 @@ import bpy.utils.previews
import importlib
from bpy_extras.io_utils import ImportHelper, ExportHelper
from . import handler, ui, prop, operator
from typing import Callable, Union
from typing import Union
from collections.abc import Callable
try:
from bonsai.translations import translations_dict
@@ -152,10 +153,10 @@ classes = [
ui.BIM_PT_tab_new_project_wizard,
ui.BIM_PT_tab_project_info,
ui.BIM_PT_tab_spatial,
ui.BIM_PT_tab_grouping_and_filtering,
ui.BIM_PT_tab_project_setup,
ui.BIM_PT_tab_geometry,
ui.BIM_PT_tab_stakeholders,
ui.BIM_PT_tab_grouping_and_filtering,
# Object information
ui.BIM_PT_tab_object_metadata,
ui.BIM_PT_tab_misc,
@@ -21,7 +21,7 @@
/>
<script
type="text/javascript"
src="https://code.jquery.com/jquery-3.6.0.min.js"
src="/static/js/jquery.min.js"
></script>
<script
type="text/javascript"
@@ -19,7 +19,7 @@
/>
<script
type="text/javascript"
src="https://code.jquery.com/jquery-3.6.0.min.js"
src="/static/js/jquery.min.js"
></script>
<script
type="text/javascript"
@@ -24,7 +24,7 @@
/>
<script
type="text/javascript"
src="https://code.jquery.com/jquery-3.6.0.min.js"
src="/static/js/jquery.min.js"
></script>
<script
type="text/javascript"
@@ -17,7 +17,7 @@
/>
<script
type="text/javascript"
src="https://code.jquery.com/jquery-3.6.0.min.js"
src="/static/js/jquery.min.js"
></script>
<script
type="text/javascript"
@@ -21,7 +21,7 @@
/>
<script
type="text/javascript"
src="https://code.jquery.com/jquery-3.6.0.min.js"
src="/static/js/jquery.min.js"
></script>
<script
type="text/javascript"
+1 -5
View File
@@ -76,11 +76,7 @@ class IfcExporter:
def set_header(self):
self.file.wrapped_data.header.file_name.name = os.path.basename(self.ifc_export_settings.output_file)
self.file.wrapped_data.header.file_name.time_stamp = (
datetime.datetime.utcnow()
.replace(tzinfo=datetime.timezone.utc)
.astimezone()
.replace(microsecond=0)
.isoformat()
datetime.datetime.utcnow().replace(tzinfo=datetime.UTC).astimezone().replace(microsecond=0).isoformat()
)
self.file.wrapped_data.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version)
self.file.wrapped_data.header.file_name.originating_system = "{} {}".format(
+54 -8
View File
@@ -24,6 +24,7 @@ import ifcopenshell.util.unit
import ifcopenshell.api.owner.settings
import bonsai.bim
import bonsai.tool as tool
import weakref
from bpy.app.handlers import persistent
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.model.data import AuthoringData
@@ -33,7 +34,8 @@ from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDe
from bonsai.bim.module.nest.decorator import NestDecorator
from mathutils import Vector
from math import cos
from typing import Union, Callable
from typing import Union
from collections.abc import Callable
cwd = os.path.dirname(os.path.realpath(__file__))
@@ -256,31 +258,70 @@ def redo_post(scene: bpy.types.Scene) -> None:
tool.Ifc.rebuild_element_maps()
# Cache is important as those entities will be retrieved very often,
# for every IfcOwnerHistory creation or update.
class SettingsCache:
APPLICATION_ID: Union[int, None] = None
USER_ID: Union[int, None] = None
_file: Union[weakref.ReferenceType[ifcopenshell.file], None] = None
@classmethod
def get_file(cls) -> Union[ifcopenshell.file, None]:
if cls._file is None:
return None
return cls._file()
@classmethod
def set_file(cls, file: ifcopenshell.file) -> None:
cls._file = weakref.ref(file)
def get_application(ifc: ifcopenshell.file) -> ifcopenshell.entity_instance:
# TODO: cache this for even faster application retrieval. It honestly makes a difference on long scripts.
version = tool.Blender.get_bonsai_version()
if SettingsCache.get_file() is ifc and SettingsCache.APPLICATION_ID is not None:
try:
app = ifc.by_id(SettingsCache.APPLICATION_ID)
return app
except RuntimeError:
pass
# Use only main part from the version to avoid flooding advanced users projects with IfcApplications.
version = tool.Blender.get_bonsai_version().split("-")[0]
for element in ifc.by_type("IfcApplication"):
if element.ApplicationIdentifier == "Bonsai" and element.Version == version:
return element
return ifcopenshell.api.run(
"owner.add_application",
application = ifcopenshell.api.owner.add_application(
ifc,
version=version,
application_full_name="Bonsai",
application_identifier="Bonsai",
)
SettingsCache.APPLICATION_ID = application.id()
SettingsCache.set_file(ifc)
return application
def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None]:
# TODO: cache this for even faster application retrieval. It honestly makes a difference on long scripts.
if SettingsCache.get_file() is ifc and SettingsCache.USER_ID is not None:
try:
user = ifc.by_id(SettingsCache.USER_ID)
return user
except RuntimeError:
pass
if pao := next(iter(ifc.by_type("IfcPersonAndOrganization")), None):
SettingsCache.USER_ID = pao.id()
SettingsCache.set_file(ifc)
return pao
elif ifc.schema == "IFC2X3":
if (person := next(iter(ifc.by_type("IfcPerson")), None)) is None:
person = tool.Ifc.run("owner.add_person")
person = ifcopenshell.api.owner.add_person(ifc)
if (organization := next(iter(ifc.by_type("IfcOrganization")), None)) is None:
organization = tool.Ifc.run("owner.add_organisation")
pao = tool.Ifc.run("owner.add_person_and_organisation", person=person, organisation=organization)
organization = ifcopenshell.api.owner.add_organisation(ifc)
pao = ifcopenshell.api.owner.add_person_and_organisation(ifc, person=person, organisation=organization)
SettingsCache.USER_ID = pao.id()
SettingsCache.set_file(ifc)
return pao
@@ -350,6 +391,11 @@ def load_post(scene):
aggregate_props = tool.Aggregate.get_aggregate_props()
nest_props = tool.Nest.get_nest_props()
model_props = tool.Model.get_model_props()
GeoreferenceDecorator.uninstall()
AggregateDecorator.uninstall()
NestDecorator.uninstall()
WallAxisDecorator.uninstall()
SlabDirectionDecorator.uninstall()
if georeference_props.should_visualise:
GeoreferenceDecorator.install(bpy.context)
if aggregate_props.aggregate_decorator:
+31 -7
View File
@@ -27,7 +27,8 @@ import ifcopenshell.util.element
import ifcopenshell.util.unit
from ifcopenshell.util.doc import get_attribute_doc, get_predefined_type_doc, get_property_doc
import bonsai.tool as tool
from typing import Optional, Callable, Any, Union, Iterable, TYPE_CHECKING
from typing import Optional, Any, Union, TYPE_CHECKING
from collections.abc import Callable, Iterable
if TYPE_CHECKING:
import bonsai.bim.prop
@@ -120,7 +121,8 @@ def import_attributes(
callback: Optional[ImportCallback] = None,
) -> None:
schema = tool.Ifc.schema()
for attribute in schema.declaration_by_name(ifc_class).all_attributes():
assert (entity := schema.declaration_by_name(ifc_class).as_entity())
for attribute in entity.all_attributes():
import_attribute(attribute, props, data, callback=callback)
@@ -131,11 +133,13 @@ def import_attributes2(
callback: Optional[ImportCallback] = None,
) -> None:
if isinstance(element, str):
attributes = tool.Ifc.schema().declaration_by_name(element).as_entity().all_attributes()
assert (entity := tool.Ifc.schema().declaration_by_name(element).as_entity())
attributes = entity.all_attributes()
info = {a.name(): None for a in attributes}
info["type"] = element
else:
attributes = element.wrapped_data.declaration().as_entity().all_attributes()
assert (entity := element.wrapped_data.declaration().as_entity())
attributes = entity.all_attributes()
info = element.get_info()
for attribute in attributes:
import_attribute(attribute, props, info, callback=callback)
@@ -351,8 +355,23 @@ def get_enum_items(
return items
def draw_expandable_panel(layout, context, label: str, ui_func, default_closed: bool = True):
header, panel = layout.panel(label, default_closed=default_closed)
def draw_expandable_panel(
layout: bpy.types.UILayout,
context: bpy.types.Context,
label: str,
ui_func: Callable[[bpy.types.UILayout, bpy.types.Context], None],
default_closed: bool = True,
*,
panel_id: str = "",
) -> None:
"""
:param panel_id: Optional unique identifier for the panel.
By default is matching ``label``, but if more than one panel with the same name is used,
then ``panel_id`` can be provided explicitly to ensure panels can be expanded/collapsed separately.
"""
if not panel_id:
panel_id = label
header, panel = layout.panel(panel_id, default_closed=default_closed)
header.label(text=label)
if panel:
ui_func(panel, context)
@@ -396,7 +415,12 @@ def draw_filter(
if data.data["saved_searches"]:
row.operator("bim.load_search", text="", icon="IMPORT").module = module
row.operator("bim.save_search", text="", icon="EXPORT").module = module
if module != "search":
if module == "drawing_include":
row.operator("bim.edit_element_filter", icon="CHECKMARK", text="").filter_mode = "INCLUDE"
if module == "drawing_exclude":
row.operator("bim.edit_element_filter", icon="CHECKMARK", text="").filter_mode = "EXCLUDE"
row.operator("bim.enable_editing_element_filter", icon="CANCEL", text="").filter_mode = "NONE"
row = layout.row(align=True)
row.operator("bim.add_filter_group", text="Add Search Group", icon="ADD").module = module
row.operator("bim.edit_filter_query", text="", icon="FILTER").module = module
+3 -2
View File
@@ -34,7 +34,8 @@ import bonsai.tool as tool
from ifcopenshell.file import UndoSystemError
from pathlib import Path
from bonsai.tool.brick import BrickStore
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired, Literal
from typing import Union, Optional, TypedDict, NotRequired, Literal
from collections.abc import Callable
IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
@@ -69,7 +70,7 @@ class IfcStore:
cache_path: Optional[str] = None
id_map: dict[int, IFC_CONNECTED_TYPE] = {}
guid_map: dict[str, IFC_CONNECTED_TYPE] = {}
edited_objs: Set[bpy.types.Object] = set()
edited_objs: set[bpy.types.Object] = set()
pset_template_path: str = ""
pset_template_file: Optional[ifcopenshell.file] = None
classification_path: str = ""
+7 -7
View File
@@ -38,7 +38,8 @@ import ifcopenshell.util.shape
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore, IFC_CONNECTED_TYPE
from bonsai.tool.loader import OBJECT_DATA_TYPE
from typing import Dict, Union, Optional, Any, Literal, Iterable
from typing import Union, Optional, Any, Literal
from collections.abc import Iterable
from ifcopenshell.util.shape import MatrixType
@@ -47,7 +48,7 @@ class MaterialCreator:
obj: bpy.types.Object
def __init__(self, ifc_import_settings: IfcImportSettings, ifc_importer: IfcImporter):
self.styles: Dict[int, bpy.types.Material] = {}
self.styles: dict[int, bpy.types.Material] = {}
self.parsed_meshes: set[str] = set()
self.ifc_import_settings = ifc_import_settings
self.ifc_importer = ifc_importer
@@ -300,7 +301,6 @@ class IfcImporter:
tool.Spatial.run_spatial_import_spatial_decomposition()
if default_container := tool.Spatial.guess_default_container():
tool.Spatial.set_default_container(default_container)
tool.Loader.setup_active_bsdd_classification()
self.update_progress(100)
bpy.context.window_manager.progress_end()
@@ -857,7 +857,7 @@ class IfcImporter:
return obj
def load_existing_meshes(self) -> None:
self.meshes.update({m.name: m for m in bpy.data.meshes})
self.meshes.update({m.name: m for m in bpy.data.meshes if m.library is None})
def merge_materials_by_colour(self):
cleaned_materials = {}
@@ -1034,7 +1034,7 @@ class IfcImporter:
element: ifcopenshell.entity_instance,
shape: Union[ifcopenshell.geom.ShapeElementType, ifcopenshell.geom.ShapeType],
) -> bpy.types.Curve:
if hasattr(shape, "geometry"):
if isinstance(shape, ifcopenshell.geom.ShapeElementType):
geometry = shape.geometry
else:
geometry = shape
@@ -1067,7 +1067,7 @@ class IfcImporter:
cartesian_point_offset: Union[npt.NDArray[np.float64], Literal[False]] = None,
) -> Union[bpy.types.Mesh, None]:
try:
if hasattr(shape, "geometry"):
if isinstance(shape, ifcopenshell.geom.ShapeElementType):
# shape is ShapeElementType
geometry = shape.geometry
else:
@@ -1076,7 +1076,7 @@ class IfcImporter:
# Mesh may already exists (e.g. during representation reimport)
# and we assign some suffix to it to prevent Blender from adding '.001' suffix to the new mesh.
mesh_name = tool.Loader.get_mesh_name_from_shape(geometry)
if old_mesh := bpy.data.meshes.get(mesh_name):
if old_mesh := bpy.data.meshes.get((mesh_name, None)):
old_mesh.name = mesh_name + ".old"
mesh = bpy.data.meshes.new(mesh_name)
@@ -164,8 +164,6 @@ class AggregateDecorator:
def draw_aggregate(self, context):
props = tool.Aggregate.get_aggregate_props()
if props.in_aggregate_mode:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
decorator_color_special = self.addon_prefs.decorator_color_special
decorator_color_selected = self.addon_prefs.decorator_color_selected
@@ -194,7 +192,13 @@ class AggregateDecorator:
aggregates.append(obj)
continue
aggregate = ifcopenshell.util.element.get_aggregate(element)
aggregates_list = tool.Aggregate.get_aggregates_recursively(element)
if props.in_aggregate_mode and props.editing_aggregate:
index = aggregates_list.index(tool.Ifc.get_entity(props.editing_aggregate))
if index > 0:
aggregate = aggregates_list[index - 1]
else:
aggregate = aggregates_list[-1]
if aggregate:
aggregates.append(tool.Ifc.get_object(aggregate))
@@ -212,9 +216,7 @@ class AggregateDecorator:
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
if props.in_aggregate_mode:
return
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate))
parts = tool.Aggregate.get_parts_recursively(tool.Ifc.get_entity(aggregate))
parts_objs = [tool.Ifc.get_object(p) for p in parts]
indices, edges = create_bounding_box(parts_objs)
@@ -333,7 +335,7 @@ class AggregateModeDecorator:
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate_obj))
parts = tool.Aggregate.get_parts_recursively(tool.Ifc.get_entity(aggregate_obj))
color = self.addon_prefs.decorator_color_selected
parts_objs = [tool.Ifc.get_object(p) for p in parts]
@@ -19,6 +19,7 @@
import bpy
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.pset
import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.core.aggregate as core
@@ -112,7 +113,7 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Linked_Aggregate")
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
class BIM_OT_enable_editing_aggregate(bpy.types.Operator):
@@ -312,7 +313,7 @@ class BIM_OT_break_link_to_other_aggregates(bpy.types.Operator, tool.Ifc.Operato
for part in parts:
pset = ifcopenshell.util.element.get_pset(part, "BBIM_Linked_Aggregate")
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=part, pset=pset)
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=part, pset=pset)
linked_aggregate_group = next(
r.RelatingGroup
@@ -385,7 +386,7 @@ class BIM_OT_disable_aggregate_mode(bpy.types.Operator):
def execute(self, context):
bpy.ops.object.select_all(action="DESELECT")
bonsai.core.aggregate.exit_aggregate_mode(tool.Aggregate)
core.exit_aggregate_mode(tool.Aggregate)
return {"FINISHED"}
@@ -38,8 +38,6 @@ import ifcopenshell.util.selector
from datetime import datetime
from dateutil import parser, relativedelta
from bpy_extras.io_utils import ImportHelper
from typing import get_args, TYPE_CHECKING
from typing_extensions import assert_never
class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
+1 -1
View File
@@ -397,7 +397,7 @@ class AddBcfBimSnippet(bpy.types.Operator):
def poll(cls, context):
props = tool.Bcf.get_bcf_props()
props_are_filled = all(
(getattr(props, attr) for attr in ("bim_snippet_reference", "bim_snippet_schema", "bim_snippet_type"))
getattr(props, attr) for attr in ("bim_snippet_reference", "bim_snippet_schema", "bim_snippet_type")
)
if not props_are_filled:
cls.poll_message_set("Some BIM snippet fields are empty.")
-3
View File
@@ -31,9 +31,6 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from functools import partial
from typing import Literal
from typing_extensions import assert_never
from bcf.agnostic.extensions import get_extensions_attributes
from typing import TYPE_CHECKING, Union
@@ -404,12 +404,13 @@ class EditBoundaryAttributes(bpy.types.Operator, tool.Ifc.Operator):
assert obj
bprops = tool.Boundary.get_object_boundary_props(obj)
boundary = tool.Ifc.get_entity(obj)
assert boundary
attributes = dict()
for ifc_attribute, blender_property in EDITABLE_ATTRIBUTES.items():
obj = getattr(bprops, blender_property, None)
entity = tool.Ifc.get_entity(obj)
attributes[blender_property] = entity
ifcopenshell.api.run("boundary.edit_attributes", tool.Ifc.get(), entity=boundary, **attributes)
ifcopenshell.api.boundary.edit_attributes(tool.Ifc.get(), entity=boundary, **attributes)
bpy.ops.bim.disable_editing_boundary()
return {"FINISHED"}
@@ -605,7 +606,12 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
parent_boundaries: list[ifcopenshell.entity_instance] = []
objs = tool.Blender.get_selected_objects()
ifc_objects = {element: obj for obj in objs if (element := tool.Ifc.get_entity(obj))}
ifc_objects = {
element: obj
for obj in objs
if (element := tool.Ifc.get_entity(obj)) and isinstance(obj.data, bpy.types.Mesh) and obj.data.vertices
}
if len(ifc_objects) not in (1, 2):
self.report({"ERROR"}, "Please select one or two IFC elements.")
return {"CANCELLED"}
@@ -647,46 +653,6 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
parent_boundaries.extend(res)
elif len(objs) == 1 and False:
# Optionally the user may select just the space, and the building element shall be auto-detected
# TODO : refactor to be able to generate all boundaries for selected space automatically or with an option
relating_space = None
related_building_element = None
relating_space_obj = None
related_building_element_obj = None
def msg(self, context):
self.layout.label(text="Please set an active container to detect space boundaries from.")
element = tool.Ifc.get_entity(objs[0])
if element.is_a("IfcSpace"):
relating_space = element
relating_space_obj = objs[0]
if not (container := tool.Root.get_default_container()):
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
for subelement in ifcopenshell.util.element.get_decomposition(container):
if not (
subelement.is_a("IfcWall") or subelement.is_a("IfcSlab") or subelement.is_a("IfcVirtualElement")
):
continue
obj = tool.Ifc.get_object(subelement)
if obj:
related_building_element = subelement
related_building_element_obj = obj
parent_boundary = self.create_element_boundary(
context,
relating_space,
relating_space_obj,
related_building_element,
related_building_element_obj,
)
if parent_boundary:
parent_boundaries.append(parent_boundary)
bpy.ops.bim.show_boundaries()
for parent_boundary in parent_boundaries:
obj = tool.Ifc.get_object(parent_boundary)
@@ -711,6 +677,14 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
+ tool.Ifc.get().by_type("IfcVirtualElement")
)
for building_element in building_elements:
if obj := tool.Ifc.get_object(building_element):
if tool.Ifc.is_moved(obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
if tool.Ifc.is_moved(space_obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=space_obj)
# Don't generate boundaries of building elements that we've already got bounaries for.
for boundary in space.BoundedBy:
if boundary.RelatedBuildingElement in building_elements:
@@ -727,7 +701,10 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
while True:
tree.add_element(iterator.get_native())
shape = iterator.get()
shapes[shape.id] = shape
shapes[shape.id] = {
"verts": ifcopenshell.util.shape.get_vertices(shape.geometry),
"faces": ifcopenshell.util.shape.get_faces(shape.geometry),
}
if not iterator.next():
break
@@ -748,13 +725,11 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
bm = bmesh.new()
shape = shapes[building_element.id()]
verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
for vert in verts:
for vert in shape["verts"]:
bm.verts.new(Vector(vert))
bm.verts.ensure_lookup_table()
faces = ifcopenshell.util.shape.get_faces(shape.geometry)
for face in faces:
for face in shape["faces"]:
bm.faces.new([bm.verts[i] for i in face])
bm.verts.ensure_lookup_table()
bm.faces.ensure_lookup_table()
+10 -6
View File
@@ -20,16 +20,20 @@ import bpy
from . import ui, prop, operator
classes = (
operator.GetBSDDClassificationProperties,
operator.LoadBSDDDomains,
operator.SearchBSDDClass,
operator.SetActiveBSDDDictionary,
prop.BSDDDomain,
operator.AddBSDDProperties,
operator.ImportBSDDClasses,
operator.LoadBSDDDictionaries,
operator.SearchBSDDClassifications,
operator.SearchBSDDProperties,
prop.BSDDDictionary,
prop.BSDDClassification,
prop.BSDDProperty,
prop.BSDDPset,
prop.BIMBSDDProperties,
ui.BIM_UL_bsdd_domains,
ui.BIM_UL_bsdd_classifications,
ui.BIM_UL_bsdd_dictionaries,
ui.BIM_UL_bsdd_classes,
ui.BIM_UL_bsdd_properties,
ui.BIM_PT_bsdd,
)
+45
View File
@@ -0,0 +1,45 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifcopenshell
import ifcopenshell.util.date
import ifcopenshell.util.classification
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
def refresh():
BSDDData.is_loaded = False
class BSDDData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data["active_dictionary"] = cls.active_dictionary()
@classmethod
def active_dictionary(cls):
props = tool.Bsdd.get_bsdd_props()
results = [("ALL", "All Dictionaries", "All active dictionaries")]
results.extend([(d.uri, d.name, f"{d.status} - {d.version}") for d in props.dictionaries if d.is_active])
return results
+62 -35
View File
@@ -19,61 +19,88 @@
import bpy
import bsdd
import bonsai.tool as tool
import ifcopenshell.api.pset
import ifcopenshell.util.element
from bonsai.core import bsdd as core
class LoadBSDDDomains(bpy.types.Operator):
bl_idname = "bim.load_bsdd_domains"
class LoadBSDDDictionaries(bpy.types.Operator):
bl_idname = "bim.load_bsdd_dictionaries"
bl_label = "Load bSDD Dictionaries"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
core.load_bsdd(bsdd.Client(), tool.Bsdd)
core.load_bsdd(tool.Bsdd)
return {"FINISHED"}
class SetActiveBSDDDictionary(bpy.types.Operator):
bl_idname = "bim.set_active_bsdd_domain"
bl_label = "Set bSDD Dictionary as active"
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty()
uri: bpy.props.StringProperty()
def execute(self, context):
core.set_active_bsdd_dictionary(self.name, self.uri, tool.Bsdd)
return {"FINISHED"}
class SearchBSDDClass(bpy.types.Operator):
class SearchBSDDClassifications(bpy.types.Operator):
bl_idname = "bim.search_bsdd_classifications"
bl_label = "Search bSDD Class"
bl_description = "Search for bSDD classes by the provided keyword"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
# Requirement by buildingSMART bSDD api.
if len(context.scene.BIMBSDDProperties.keyword) < 3:
cls.poll_message_set("Search query has to be at least 3 characters long.")
return False
return True
def execute(self, context):
keyword = context.scene.BIMBSDDProperties.keyword
classes_found = core.search_class(keyword, bsdd.Client(), tool.Bsdd)
self.report({"INFO"}, f"{classes_found} bSDD classes found for '{keyword}'.")
total = core.search_bsdd_class(tool.Bsdd, context.scene.BIMBSDDProperties.keyword)
self.report({"INFO"}, f"{total} bSDD classes found.")
return {"FINISHED"}
class GetBSDDClassificationProperties(bpy.types.Operator):
bl_idname = "bim.get_bsdd_classification_properties"
bl_label = "Search bSDD Class Properties"
bl_description = "Search for bSDD class properties for the currently selected class"
class ImportBSDDClasses(bpy.types.Operator):
bl_idname = "bim.import_bsdd_classes"
bl_label = "Import bSDD Classes"
bl_description = "Load bSDD classes that apply to the current element"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def execute(self, context):
pset_data = core.get_class_properties(bsdd.Client(), tool.Bsdd)
psets_found = len(pset_data)
props_found = sum(len(pset) for pset in pset_data.values())
self.report({"INFO"}, f"{psets_found} psets found ({props_found} props) for the active classification.")
props = tool.Bsdd.get_bsdd_props()
core.import_bsdd_classes(tool.Bsdd, self.obj, self.obj_type)
self.report({"INFO"}, f"{len(props.classes)} bSDD classes found.")
return {"FINISHED"}
class SearchBSDDProperties(bpy.types.Operator):
bl_idname = "bim.search_bsdd_properties"
bl_label = "Search bSDD Properties"
bl_description = "Search for bSDD properties that apply to the current element"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def execute(self, context):
props = tool.Bsdd.get_bsdd_props()
core.search_bsdd_properties(tool.Bsdd, context.scene.BIMBSDDProperties.keyword, self.obj, self.obj_type)
self.report({"INFO"}, f"{len(props.properties)} bSDD properties found.")
return {"FINISHED"}
class AddBSDDProperties(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_bsdd_properties"
bl_label = "Add bSDD Properties"
bl_description = "Add selected bSDD properties"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def _execute(self, context):
self.file = tool.Ifc.get()
bprops = tool.Bsdd.get_bsdd_props()
psets = {}
for selected_property in bprops.selected_properties:
psets.setdefault(selected_property.metadata, {})[selected_property.name] = selected_property.get_value()
props = tool.Pset.get_pset_props(self.obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
element = tool.Ifc.get().by_id(ifc_definition_id)
properties = {}
current_psets = ifcopenshell.util.element.get_psets(element, verbose=True)
for pset_name, properties in psets.items():
if pset := current_psets.get(pset_name, None):
pset = self.file.by_id(pset["id"])
else:
pset = ifcopenshell.api.pset.add_pset(self.file, product=element, name=pset_name)
ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties=properties)
+69 -10
View File
@@ -17,7 +17,9 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
from bpy.types import PropertyGroup
from bonsai.bim.module.bsdd.data import BSDDData
from bonsai.bim.prop import Attribute, StrProperty
from bpy.props import (
PointerProperty,
@@ -29,23 +31,56 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import Union
class BSDDDomain(PropertyGroup):
def get_active_dictionary(self, context):
if not BSDDData.is_loaded:
BSDDData.load()
return BSDDData.data["active_dictionary"]
def update_is_active(self: "BSDDDictionary", context: bpy.types.Context) -> None:
BSDDData.data["active_dictionary"] = BSDDData.active_dictionary()
def update_is_selected(self: "BSDDProperty", context: bpy.types.Context) -> None:
tool.Bsdd.import_selected_properties()
def update_active_class_index(self: "BIMBSDDProperties", context: bpy.types.Context) -> None:
tool.Bsdd.import_class_properties()
BSDDData.data["active_dictionary"] = BSDDData.active_dictionary()
class BSDDDictionary(PropertyGroup):
name: StringProperty(name="Name")
uri: StringProperty(name="URI")
default_language_code: StringProperty(name="Language")
organization_name_owner: StringProperty(name="Organization")
status: StringProperty(name="Status")
version: StringProperty(name="Version")
is_active: BoolProperty(
name="Is Active", description="Enable to search with this dictionary", default=False, update=update_is_active
)
class BSDDClassification(PropertyGroup):
name: StringProperty(name="Name")
reference_code: StringProperty(name="Reference Code")
uri: StringProperty(name="Namespace URI")
domain_name: StringProperty(name="Domain Name")
domain_namespace_uri: StringProperty(name="Domain Namespace URI")
uri: StringProperty(name="URI")
dictionary_name: StringProperty(name="Dictionary Name")
dictionary_namespace_uri: StringProperty(name="Dictionary Namespace URI")
class BSDDProperty(PropertyGroup):
name: StringProperty(name="Name")
code: StringProperty(name="Code")
uri: StringProperty(name="URI")
pset: StringProperty(name="Pset")
is_selected: BoolProperty(
name="Is Selected", description="Select to add or edit this property", default=False, update=update_is_selected
)
class BSDDPset(PropertyGroup):
@@ -54,12 +89,26 @@ class BSDDPset(PropertyGroup):
class BIMBSDDProperties(PropertyGroup):
active_domain: StringProperty(name="Active Domain")
active_dictionary: StringProperty(name="Active Dictionary")
active_dictionary: EnumProperty(items=get_active_dictionary, name="Active Dictionary")
active_uri: StringProperty(name="Active URI")
domains: CollectionProperty(name="Domains", type=BSDDDomain)
active_domain_index: IntProperty(name="Active Domain Index")
dictionaries: CollectionProperty(name="Dictionaries", type=BSDDDictionary)
active_dictionary_index: IntProperty(name="Active Dictionary Index")
classifications: CollectionProperty(name="Classifications", type=BSDDClassification)
active_classification_index: IntProperty(name="Active Classification Index")
property_filter_mode: EnumProperty(
name="Property Filter Mode",
items=[
("CLASS", "By Class", "Browse properties by class or group"),
("KEYWORD", "By Keyword", "Search properties directly using a keyword"),
],
default="CLASS",
)
classes: CollectionProperty(name="Classes", type=BSDDClassification)
active_class_index: IntProperty(name="Active Class Index", update=update_active_class_index)
properties: CollectionProperty(name="Properties", type=BSDDProperty)
active_property_index: IntProperty(name="Active Property Index")
selected_properties: CollectionProperty(name="Selected Properties", type=Attribute)
keyword: StringProperty(name="Keyword", description="Query for bsdd classes search, case and accent insensitive")
should_filter_ifc_class: BoolProperty(
name="Filter Active IFC Class",
@@ -69,9 +118,19 @@ class BIMBSDDProperties(PropertyGroup):
use_only_ifc_properties: BoolProperty(
name="Only IFC Properties",
description="Whether to display and assign only properties from IFC dictionary",
default=True,
default=False,
)
load_preview_domains: BoolProperty(
name="Load Preview Domains", description="Whether it should load preview and inactive domains", default=False
load_preview_dictionaries: BoolProperty(
name="Load Preview Dictionaries", description="Load dictionaries marked as Preview status", default=False
)
load_inactive_dictionaries: BoolProperty(
name="Load Inactive Dictionaries", description="Load dictionaries marked as Inactive status", default=False
)
load_test_dictionaries: BoolProperty(
name="Load Test Dictionaries", description="Load dictionaries that are for testing only", default=False
)
classification_psets: CollectionProperty(name="Classification Psets", type=BSDDPset)
@property
def active_class(self) -> Union[BSDDClassification, None]:
return tool.Blender.get_active_uilist_element(self.classes, self.active_class_index)
+54 -32
View File
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bonsai.tool as tool
from bonsai.bim.module.bsdd.data import BSDDData
from bpy.types import Panel, UIList
@@ -30,65 +31,58 @@ class BIM_PT_bsdd(Panel):
bl_parent_id = "BIM_PT_tab_project_setup"
def draw(self, context):
if not BSDDData.is_loaded:
BSDDData.load()
props = context.scene.BIMBSDDProperties
layout = self.layout
row = self.layout.row(align=True)
row.prop(props, "load_preview_domains")
if len(props.domains):
row.operator("bim.load_bsdd_domains", text="", icon="FILE_REFRESH")
if props.active_domain:
if len(props.dictionaries):
row = self.layout.row()
row.label(text="Active: " + props.active_domain, icon="URL")
else:
row = self.layout.row()
row.label(text="No Active bSDD Domain", icon="ERROR")
row.operator("bim.load_bsdd_dictionaries", icon="FILE_REFRESH")
if len(props.domains):
if len(props.dictionaries):
self.layout.template_list(
"BIM_UL_bsdd_domains",
"BIM_UL_bsdd_dictionaries",
"",
props,
"domains",
"dictionaries",
props,
"active_domain_index",
"active_dictionary_index",
)
if 0 <= props.active_domain_index < len(props.domains):
selected_domain = props.domains[props.active_domain_index]
if 0 <= props.active_dictionary_index < len(props.dictionaries):
selected_dictionary = props.dictionaries[props.active_dictionary_index]
else:
selected_domain = None
selected_dictionary = None
if selected_domain:
layout.label(text="Selected domain:")
if selected_dictionary:
layout.label(text="Selected Dictionary:")
box = layout.box()
row = box.row(align=True)
row.label(text="Language")
row.label(text=selected_domain.default_language_code)
row.label(text=selected_dictionary.default_language_code)
row = box.row(align=True)
row.label(text="Version")
row.label(text=selected_domain.version)
box.operator("bim.open_uri", text="Open bSDD In Browser", icon="URL").uri = selected_domain.uri
row.label(text=selected_dictionary.version)
box.operator("bim.open_uri", text="Open bSDD In Browser", icon="URL").uri = selected_dictionary.uri
else:
row = self.layout.row()
row.operator("bim.load_bsdd_domains")
row.operator("bim.load_bsdd_dictionaries")
class BIM_UL_bsdd_domains(UIList):
class BIM_UL_bsdd_dictionaries(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
props = context.scene.BIMBSDDProperties
row = layout.row(align=True)
if item.status != "Active":
row.label(text=f"{item.name} ({item.organization_name_owner}) - {item.status}", icon="ERROR")
row.label(
text=f"{item.name} ({item.organization_name_owner}) v{item.version} - {item.status}", icon="ERROR"
)
else:
row.label(text=f"{item.name} ({item.organization_name_owner})")
if item.uri == props.active_uri:
row.label(text="", icon="URL")
else:
op = row.operator("bim.set_active_bsdd_domain", text="", icon="RESTRICT_SELECT_OFF")
op.name = item.name
op.uri = item.uri
row.label(text=f"{item.name} ({item.organization_name_owner}) v{item.version}")
row.prop(
item, "is_active", icon="CHECKBOX_HLT" if item.is_active else "CHECKBOX_DEHLT", text="", emboss=False
)
class BIM_UL_bsdd_classifications(UIList):
@@ -98,3 +92,31 @@ class BIM_UL_bsdd_classifications(UIList):
row.label(text=item.reference_code)
row.label(text=item.name)
row.operator("bim.open_uri", text="", icon="URL").uri = item.uri
class BIM_UL_bsdd_classes(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
row.label(text=item.name)
row.operator("bim.open_uri", text="", icon="URL").uri = item.uri
class BIM_UL_bsdd_properties(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
name = item.name
if name != item.code:
name += f" ({item.code})"
row.label(text=name)
if item.pset:
row.label(text=item.pset)
row.operator("bim.open_uri", text="", icon="URL").uri = item.uri
row.prop(
item,
"is_selected",
icon="CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT",
text="",
emboss=False,
)
+4 -4
View File
@@ -177,7 +177,7 @@ class CadFillet(bpy.types.Operator):
selected_edges.append(cut_edges[1])
# Calculate the distance to slide each edge to make space for the fillet arc
shared_vert = list(set(selected_edges[0].verts) & set(selected_edges[1].verts))[0]
shared_vert = next(iter(set(selected_edges[0].verts) & set(selected_edges[1].verts)))
v1 = selected_edges[0].other_vert(shared_vert)
v2 = selected_edges[1].other_vert(shared_vert)
dir1 = (v1.co - shared_vert.co).normalized()
@@ -205,7 +205,7 @@ class CadFillet(bpy.types.Operator):
# If faces are involved, then the chamfer edge protects existing faces from the newly created fillet.
# If no faces are involved, we delete the chamfer edge.
if is_wire:
chamfer_edge = [e for e in bm.edges if shared_vert in e.verts and v2 in e.verts][0]
chamfer_edge = next(e for e in bm.edges if shared_vert in e.verts and v2 in e.verts)
bm.edges.remove(chamfer_edge)
bm.verts.index_update()
@@ -675,7 +675,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
bm = bmesh.from_edit_mesh(self.mesh)
edges = [e for e in bm.edges if e.select and not e.hide]
mid = list(set(edges[0].verts) & set(edges[1].verts))[0]
mid = next(iter(set(edges[0].verts) & set(edges[1].verts)))
v1 = edges[0].other_vert(mid)
v2 = edges[1].other_vert(mid)
assert v1 and v2
@@ -738,7 +738,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
bmesh.ops.remove_doubles(bm, verts=all_verts, dist=1e-4)
# Calculate the distance to slide each edge to make space for the fillet arc
shared_vert = list(set(selected_edges[0].verts) & set(selected_edges[1].verts))[0]
shared_vert = next(iter(set(selected_edges[0].verts) & set(selected_edges[1].verts)))
v1 = selected_edges[0].other_vert(shared_vert)
v2 = selected_edges[1].other_vert(shared_vert)
assert v1 and v2
+96 -26
View File
@@ -19,16 +19,19 @@
import os
import bpy
import json
import tempfile
import bmesh
import logging
import numpy as np
import ifcopenshell
import bonsai.tool as tool
from pathlib import Path
from bpy_extras.io_utils import ExportHelper, ImportHelper
from math import radians
from mathutils import Matrix, Vector
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.clash.decorator import ClashDecorator
from typing import TYPE_CHECKING
class ExportClashSets(bpy.types.Operator, ExportHelper):
@@ -127,7 +130,8 @@ class AddClashSource(bpy.types.Operator):
def execute(self, context):
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
source = getattr(clash_set, self.group).add()
assert clash_set
clash_set.get_clash_sources_group(self.group).add()
return {"FINISHED"}
@@ -142,7 +146,8 @@ class RemoveClashSource(bpy.types.Operator):
def execute(self, context):
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
getattr(clash_set, self.group).remove(self.index)
assert clash_set
clash_set.get_clash_sources_group(self.group).remove(self.index)
return {"FINISHED"}
@@ -152,14 +157,16 @@ class SelectClashSource(bpy.types.Operator, ImportHelper):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select an IFC file to add as a clash source"
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
index: bpy.props.IntProperty()
group: bpy.props.StringProperty()
index: bpy.props.IntProperty(options={"HIDDEN"})
group: bpy.props.StringProperty(options={"HIDDEN"})
filename_ext = ".ifc"
def execute(self, context):
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
getattr(clash_set, self.group)[self.index].name = self.filepath
assert clash_set
clash_source = clash_set.get_clash_sources_group(self.group)[self.index]
clash_source.name = self.filepath
return {"FINISHED"}
@@ -190,16 +197,39 @@ class SelectSmartGroupedClashesPath(bpy.types.Operator, ImportHelper):
class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
bl_idname = "bim.execute_ifc_clash"
bl_label = "Execute IFC Clash"
bl_description = "Execute clash detection and save the information to a .bcf or .json file"
filter_glob: bpy.props.StringProperty(default="*.bcf;*.json", options={"HIDDEN"})
format: bpy.props.EnumProperty(name="Format", items=[(i, i, "") for i in ("bcf", "json")])
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
bl_description = (
"Execute clash detection and save the information to a .bcf or .json file.\n\n"
"ALT+click to run a quick clash without selecting a file to save."
)
filter_glob: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
default="*.bcf;*.json", options={"HIDDEN"}
)
format: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
name="Format", items=[(i, i, "") for i in ("bcf", "json")]
)
filepath: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
subtype="FILE_PATH", options={"SKIP_SAVE"}
)
quick_clash: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
options={"SKIP_SAVE"},
)
if TYPE_CHECKING:
filter_glob: str
format: str
filepath: str
quick_clash: bool
@property
def filename_ext(self) -> str:
return f".{self.format.lower()}"
def invoke(self, context, event):
if event.alt:
self.quick_clash = True
return self.execute(context)
if self.filepath:
return self.execute(context)
return ExportHelper.invoke(self, context, event)
@@ -209,14 +239,32 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
self.props = tool.Clash.get_clash_props()
_, extension = os.path.splitext(self.filepath)
if extension != ".bcf":
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
# TODO Temporarily until BCF support comes back
if extension != ".json":
self.filepath = bpy.path.ensure_ext(self.filepath, ".bcf")
for clash_set in self.props.clash_sets:
for clash_sources in clash_set.get_clash_sources().values():
for clash_source in clash_sources:
if not Path(clash_source.name).is_file():
self.report(
{"ERROR"},
f"One of the provided clash source filepaths do not exist: '{clash_source.name}'.",
)
return {"CANCELLED"}
temp_file = None
if self.quick_clash:
temp_file = tempfile.NamedTemporaryFile(suffix=".json", delete=False, mode="w")
temp_file.close()
extension = ".json"
self.filepath = temp_file.name
else:
extension = Path(self.filepath).suffix.lower()
if extension != ".bcf":
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
# TODO Temporarily until BCF support comes back
if extension != ".json":
self.filepath = bpy.path.ensure_ext(self.filepath, ".bcf")
assert extension in (".bcf", ".json")
self.props.export_path = self.filepath
self.props.export_path = self.filepath
settings = ifcclash.ClashSettings()
settings.output = self.filepath
settings.logger = logging.getLogger("Clash")
@@ -225,11 +273,14 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
if self.props.should_create_clash_snapshots:
def get_viewpoint_snapshot(viewpoint):
def get_viewpoint_snapshot(viewpoint) -> tuple[str, bytes]:
assert context.scene
camera = bpy.data.objects.get("IFC Clash Camera")
if not camera:
camera = bpy.data.objects.new("IFC Clash Camera", bpy.data.cameras.new("IFC Clash Camera"))
context.scene.collection.objects.link(camera)
assert isinstance(camera.data, bpy.types.Camera)
bcf_camera = viewpoint.visualization_info.perspective_camera
p = bcf_camera.camera_view_point
@@ -238,6 +289,7 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
y = bcf_camera.camera_up_vector
y = Vector([y.x, y.y, y.z])
x = y.cross(z)
assert isinstance(x, Vector)
mat = Matrix(
[
@@ -251,9 +303,9 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
camera.matrix_world = mat
context.scene.camera = camera
camera.data.angle = radians(60)
area = next(area for area in context.screen.areas if area.type == "VIEW_3D")
area.spaces[0].region_3d.view_perspective = "CAMERA"
area.spaces[0].shading.show_xray = True
assert (space := tool.Blender.get_view3d_space()) and space.region_3d
space.region_3d.view_perspective = "CAMERA"
space.shading.show_xray = True
context.scene.render.resolution_x = 480
context.scene.render.resolution_y = 270
context.scene.render.image_settings.file_format = "PNG"
@@ -268,10 +320,26 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
clasher.clash()
clasher.export()
if extension == ".json":
# Load clash results to UI.
if extension == ".bcf":
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
try:
tmp.close()
settings.output = tmp.name
clasher.export()
tool.Clash.load_clash_sets(tmp.name)
finally:
Path(tmp.name).unlink()
else:
tool.Clash.load_clash_sets(self.filepath)
tool.Clash.import_active_clashes()
self.report({"INFO"}, "Finished IFC clash.")
tool.Clash.import_active_clashes()
if self.quick_clash:
assert temp_file is not None
Path(temp_file.name).unlink()
self.report({"INFO"}, "IFC Clash completed and results are loaded.")
else:
self.report({"INFO"}, f"IFC Clash results are saved to '{Path(self.filepath).name}'.")
return {"FINISHED"}
@@ -348,14 +416,16 @@ class SelectClash(bpy.types.Operator):
def execute(self, context):
self.props = tool.Clash.get_clash_props()
clash_set = tool.Clash.get_clash_set(self.props.active_clash_set.name)
active_clash = self.props.active_clash
assert (active_clash := self.props.active_clash)
assert (active_clash_set := self.props.active_clash_set)
clash_set = tool.Clash.get_clash_set(active_clash_set.name)
assert clash_set
clash = tool.Clash.get_clash(clash_set, active_clash.a_global_id, active_clash.b_global_id)
if not clash:
return {"FINISHED"}
products = []
products: list[ifcopenshell.entity_instance] = []
for global_id in (clash["a_global_id"], clash["b_global_id"]):
try:
+42 -6
View File
@@ -30,14 +30,18 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from ifcopenshell.geom.main import ClashType, CLASH_TYPE_ITEMS
from mathutils import Vector
from typing import TYPE_CHECKING, Literal, Union
class ClashSource(PropertyGroup):
name: StringProperty(name="File")
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
mode: EnumProperty(
name: StringProperty( # pyright: ignore[reportRedeclaration]
name="File",
description="Absolute filepath to existing .ifc file to use as a clash source.",
)
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups") # pyright: ignore[reportRedeclaration]
mode: EnumProperty( # pyright: ignore[reportRedeclaration]
items=[
("a", "All Elements", "All elements will be used for clashing"),
("i", "Include", "Only the selected elements are included for clashing"),
@@ -47,6 +51,7 @@ class ClashSource(PropertyGroup):
)
if TYPE_CHECKING:
name: str
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
mode: Literal["a", "i", "e"]
@@ -56,16 +61,31 @@ class Clash(PropertyGroup):
b_global_id: StringProperty(name="B")
a_name: StringProperty(name="A Name")
b_name: StringProperty(name="B Name")
status: BoolProperty(name="Status", default=False)
clash_type: EnumProperty( # pyright: ignore[reportRedeclaration]
name="Clash Type",
items=tuple((i, i, "") for i in CLASH_TYPE_ITEMS),
)
status: BoolProperty(
name="Status",
description="Clash status, not stored anywhere - currently just displayed in UI for convenience.",
default=False,
)
if TYPE_CHECKING:
a_global_id: str
b_global_id: str
a_name: str
b_name: str
clash_type: ClashType
status: bool
def clashes_loaded_update(self: "ClashSet", context: bpy.types.Context) -> None:
if self.clashes_loaded:
return
tool.Clash.clear_active_clash_set_results()
class ClashSet(PropertyGroup):
mode: EnumProperty(
items=[
@@ -81,13 +101,18 @@ class ClashSet(PropertyGroup):
],
name="Mode",
)
tolerance: FloatProperty(name="Tolerance", default=0.002)
clearance: FloatProperty(name="Clearance", default=0.01)
tolerance: FloatProperty(name="Tolerance", default=0.002, subtype="DISTANCE")
clearance: FloatProperty(name="Clearance", default=0.01, subtype="DISTANCE")
allow_touching: BoolProperty(name="Allow Touching", default=False)
check_all: BoolProperty(name="Check All", default=False)
a: CollectionProperty(name="Group A", type=ClashSource)
b: CollectionProperty(name="Group B", type=ClashSource)
clashes: CollectionProperty(name="Clashes", type=Clash)
clashes_loaded: BoolProperty(
name="Clash Results Are Loaded",
description="Click to unload clash results for the clash set.",
update=clashes_loaded_update,
)
if TYPE_CHECKING:
mode: Literal["intersection", "collision", "clearance"]
@@ -98,6 +123,17 @@ class ClashSet(PropertyGroup):
a: bpy.types.bpy_prop_collection_idprop[ClashSource]
b: bpy.types.bpy_prop_collection_idprop[ClashSource]
clashes: bpy.types.bpy_prop_collection_idprop[Clash]
clashes_loaded: bool
def get_clash_sources_group(
self, group: tool.Clash.ClashSourceGroup
) -> "bpy.types.bpy_prop_collection_idprop[ClashSource]":
return getattr(self, group)
def get_clash_sources(
self,
) -> "dict[tool.Clash.ClashSourceGroup, bpy.types.bpy_prop_collection_idprop[ClashSource]]":
return {g: self.get_clash_sources_group(g) for g in tool.Clash.CLASH_SOURCE_GROUP_LITERALS}
class SmartClashGroup(PropertyGroup):
+117 -44
View File
@@ -16,11 +16,16 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import bonsai.tool as tool
import bonsai.bim.helper
from bpy.types import Panel
from bonsai.bim.module.clash.data import ClashData
from typing import TYPE_CHECKING, assert_never
if TYPE_CHECKING:
from bonsai.bim.module.clash.prop import BIMClashProperties, ClashSet, SmartClashGroup, Clash
class BIM_PT_ifcclash(Panel):
@@ -36,6 +41,7 @@ class BIM_PT_ifcclash(Panel):
if not ClashData.is_loaded:
ClashData.load()
assert self.layout
layout = self.layout
props = tool.Clash.get_clash_props()
@@ -74,46 +80,62 @@ class BIM_PT_ifcclash(Panel):
row.prop(clash_set, "clearance")
row = layout.row()
row.prop(clash_set, "check_all")
else:
assert_never(clash_set.mode)
row = layout.row(align=True)
row.label(text="Group A:", icon="OUTLINER_OB_POINTCLOUD")
row.operator("bim.add_clash_source", icon="ADD", text="").group = "a"
for index, source in enumerate(clash_set.a):
def draw_clash_set_group(group: tool.Clash.ClashSourceGroup) -> None:
row = layout.row(align=True)
row.prop(source, "name", text="")
row.prop(source, "mode", text="")
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = "a"
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = "a"
row.label(text=f"Group {group.upper()}:", icon="OUTLINER_OB_POINTCLOUD")
row.operator("bim.add_clash_source", icon="ADD", text="").group = group
if source.mode != "a":
bonsai.bim.helper.draw_filter(
layout, source.filter_groups, ClashData, f"clash_{props.active_clash_set_index}_a_{index}"
)
sources = clash_set.get_clash_sources_group(group)
if not sources:
return
layout_ = layout.box()
for index, source in enumerate(sources):
row = layout_.row(align=True)
row.column().label(text="", icon="POINTCLOUD_POINT")
row = layout.row(align=True)
row.label(text="Group B:", icon="OUTLINER_OB_POINTCLOUD")
row.operator("bim.add_clash_source", icon="ADD", text="").group = "b"
# Draw user attention to empty filepaths.
col = row.column()
col.alert = not bool(source.name)
col.prop(source, "name", text="", placeholder="source.ifc")
for index, source in enumerate(clash_set.b):
row = layout.row(align=True)
row.prop(source, "name", text="")
row.prop(source, "mode", text="")
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = "b"
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = "b"
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = group
# Make sure file selection sticks to "name" field.
row.label(text="", icon="BLANK1")
if source.mode != "a":
bonsai.bim.helper.draw_filter(
layout, source.filter_groups, ClashData, f"clash_{props.active_clash_set_index}_b_{index}"
)
row.prop(source, "mode", text="")
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = group
if source.mode != "a":
def draw_filter(layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
bonsai.bim.helper.draw_filter(
layout,
source.filter_groups,
ClashData,
f"clash_{props.active_clash_set_index}_{group}_{index}",
)
bonsai.bim.helper.draw_expandable_panel(
layout_,
context,
"Filter",
draw_filter,
default_closed=True,
panel_id=f"filter_{group}_{index}",
)
layout.separator()
draw_clash_set_group("a")
layout.separator()
draw_clash_set_group("b")
layout.separator()
row = layout.row()
row.prop(props, "should_create_clash_snapshots")
@@ -125,11 +147,26 @@ class BIM_PT_ifcclash(Panel):
op.filepath = props.export_path
row = layout.row()
row.label(text=f"{len(clash_set.clashes)} Clashes Found", icon="PIVOT_CURSOR")
if clash_set.clashes_loaded:
row.column().label(text=f"{len(clash_set.clashes)} Clashes Found", icon="PIVOT_CURSOR")
layout.template_list("BIM_UL_clashes", "", props.active_clash_set, "clashes", props, "active_clash_index")
row = layout.row()
row.operator("bim.select_clash")
col = row.column()
col.alignment = "RIGHT"
col.prop(clash_set, "clashes_loaded", text="", icon="TRASH", invert_checkbox=True)
split = layout.split(factor=0.07, align=True)
split.label(text="#")
row = split.row(align=True)
row.label(text="Group A Element")
row.label(text="Group B Element")
row.label(text="Type")
layout.template_list("BIM_UL_clashes", "", props.active_clash_set, "clashes", props, "active_clash_index")
row = layout.row()
row.operator("bim.select_clash")
else:
row.label(text="Clashes Are Not Loaded", icon="PIVOT_CURSOR")
class BIM_PT_clash_manager(Panel):
@@ -155,6 +192,7 @@ class BIM_PT_smart_clash_manager(Panel):
bl_parent_id = "BIM_PT_clash_manager"
def draw(self, context):
assert self.layout
layout = self.layout
props = tool.Clash.get_clash_props()
@@ -188,8 +226,16 @@ class BIM_PT_smart_clash_manager(Panel):
class BIM_UL_clash_sets(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
ob = data
def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: BIMClashProperties,
item: ClashSet,
icon,
active_data,
active_propname,
) -> None:
if item:
layout.prop(item, "name", text="", emboss=False)
else:
@@ -197,8 +243,16 @@ class BIM_UL_clash_sets(bpy.types.UIList):
class BIM_UL_smart_groups(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
ob = data
def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: BIMClashProperties,
item: SmartClashGroup,
icon,
active_data,
active_propname,
) -> None:
if item:
layout.label(text=str(item.number), translate=False, icon="NONE", icon_value=0)
else:
@@ -206,11 +260,30 @@ class BIM_UL_smart_groups(bpy.types.UIList):
class BIM_UL_clashes(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: BIMClashProperties,
item: Clash,
icon,
active_data,
active_propname,
index,
fit_flag,
) -> None:
if item:
row = layout.row(align=True)
split = layout.split(factor=0.05, align=True)
split.label(text=str(index + 1))
row = split.row(align=False)
row.label(text=str(item.a_name), translate=False, icon="NONE", icon_value=0)
row.label(text=str(item.b_name), translate=False, icon="NONE", icon_value=0)
col = row.column()
col.enabled = False
col.prop(item, "clash_type", text="")
row.prop(item, "status", text="")
else:
layout.label(text="", translate=False)
@@ -21,6 +21,7 @@ import ifcopenshell
import ifcopenshell.util.date
import ifcopenshell.util.classification
import bonsai.tool as tool
from typing import Any
from bonsai.bim.ifc import IfcStore
@@ -41,6 +42,7 @@ class ClassificationsData:
cls.data["has_classification_file"] = cls.has_classification_file()
cls.data["classifications"] = cls.classifications()
cls.data["available_classifications"] = cls.available_classifications()
cls.data["classification_source"] = cls.classification_source()
@classmethod
def has_classification_file(cls):
@@ -62,6 +64,19 @@ class ClassificationsData:
return []
return [(str(e.id()), e.Name, "") for e in IfcStore.classification_file.by_type("IfcClassification")]
@classmethod
def classification_source(cls):
items = [
("FILE", "IFC File", ""),
("MANUAL", "Manual Entry", ""),
]
bprops = tool.Bsdd.get_bsdd_props()
dictionaries = [(d.uri, f"bSDD: {d.name}", "") for d in bprops.dictionaries if d.is_active]
if dictionaries:
items.append(("BSDD", "All Active bSDDs", ""))
items.extend(dictionaries)
return items
class ReferencesData:
@classmethod
@@ -143,10 +158,13 @@ class CostClassificationsData(ReferencesData):
cls.data["object_type"] = "Cost"
@classmethod
def references(cls):
results = []
def references(cls) -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
props = tool.Cost.get_cost_props()
element = tool.Ifc.get().by_id(props.cost_items[props.active_cost_item_index].ifc_definition_id)
active_cost_item = props.active_cost_item
if not active_cost_item:
return results
element = tool.Ifc.get().by_id(active_cost_item.ifc_definition_id)
if element:
for reference in ifcopenshell.util.classification.get_references(element):
data = reference.get_info()
@@ -20,6 +20,7 @@ import bpy
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.pset
import ifcopenshell.api.classification
import ifcopenshell.util.classification
import ifcopenshell.util.element
@@ -47,8 +48,7 @@ class AddClassification(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMClassificationProperties
ifcopenshell.api.run(
"classification.add_classification",
ifcopenshell.api.classification.add_classification(
tool.Ifc.get(),
classification=IfcStore.classification_file.by_id(int(props.available_classifications)),
)
@@ -62,11 +62,9 @@ class AddManualClassification(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMClassificationProperties
attributes = bonsai.bim.helper.export_attributes(props.classification_attributes)
classification = ifcopenshell.api.run(
"classification.add_classification", tool.Ifc.get(), classification="Unnamed"
)
ifcopenshell.api.run(
"classification.edit_classification", tool.Ifc.get(), classification=classification, attributes=attributes
classification = ifcopenshell.api.classification.add_classification(tool.Ifc.get(), classification="Unnamed")
ifcopenshell.api.classification.edit_classification(
tool.Ifc.get(), classification=classification, attributes=attributes
)
props.is_adding = False
@@ -95,17 +93,14 @@ class AddManualClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
]
if products:
classification = tool.Ifc.get().by_id(int(props.classifications))
reference = ifcopenshell.api.run(
"classification.add_reference",
reference = ifcopenshell.api.classification.add_reference(
tool.Ifc.get(),
products=products,
classification=classification,
identification="X",
name="Unnamed",
)
ifcopenshell.api.run(
"classification.edit_reference", tool.Ifc.get(), reference=reference, attributes=attributes
)
ifcopenshell.api.classification.edit_reference(tool.Ifc.get(), reference=reference, attributes=attributes)
props.is_adding = False
@@ -115,29 +110,30 @@ class AddClassificationFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.scene.BIMBSDDProperties
domain = next((d for d in props.domains if d.name == props.active_domain and d.uri == props.active_uri), None)
# Maybe user loaded preview domains, set it as active
# and then reloaded them without preview domains.
if not domain:
self.report(
{"INFO"},
f"Couldn't find domain '{props.active_domain}' ({props.active_uri}). Try to reload bSDD dictionaries.",
cprops = context.scene.BIMClassificationProperties
bprops = tool.Bsdd.get_bsdd_props()
if cprops.classification_source == "BSDD":
dictionaries = [d.uri for d in bprops.dictionaries if d.is_active]
else:
dictionaries = [cprops.classification_source]
for uri in dictionaries:
if not (dictionary := tool.Bsdd.get_dictionary(uri)):
continue
has_classification = False
for element in tool.Ifc.get().by_type("IfcClassification"):
if element.Name == dictionary["name"] or (
tool.Classification.get_location(element) == dictionary["uri"]
):
self.report({"INFO"}, f"Classification '{dictionary['name']}' is already added to the project.")
has_classification = True
if has_classification:
continue
classification = ifcopenshell.api.classification.add_classification(
tool.Ifc.get(), classification=dictionary["name"]
)
return
for element in tool.Ifc.get().by_type("IfcClassification"):
if element.Name == props.active_domain or (tool.Classification.get_location(element) == domain.uri):
self.report({"INFO"}, f"Classification '{props.active_domain}' is already added to the project.")
return
classification = ifcopenshell.api.run(
"classification.add_classification", tool.Ifc.get(), classification=props.active_domain
)
classification.Source = domain.organization_name_owner
classification.Edition = domain.version
tool.Classification.set_location(classification, domain.uri)
classification.Source = dictionary["organizationNameOwner"]
classification.Edition = dictionary["version"]
tool.Classification.set_location(classification, dictionary["uri"])
class EnableAddingManualClassification(bpy.types.Operator):
@@ -238,8 +234,7 @@ class RemoveClassification(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"classification.remove_classification",
ifcopenshell.api.classification.remove_classification(
tool.Ifc.get(),
classification=self.file.by_id(self.classification),
)
@@ -328,8 +323,7 @@ class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
if elements_by_references:
for reference, products in elements_by_references.items():
ifcopenshell.api.run(
"classification.remove_reference",
ifcopenshell.api.classification.remove_reference(
tool.Ifc.get(),
reference=reference,
products=products,
@@ -384,8 +378,7 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
if (ifc_definition_id := tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context))
]
if products:
ifcopenshell.api.run(
"classification.add_reference",
ifcopenshell.api.classification.add_reference(
tool.Ifc.get(),
reference=IfcStore.classification_file.by_id(self.reference),
products=products,
@@ -420,25 +413,24 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
classification = None
for element in tool.Ifc.get().by_type("IfcClassification"):
if element.Name == bsdd_classification.domain_name or (
tool.Classification.get_location(element) == bsdd_classification.domain_namespace_uri
if element.Name == bsdd_classification.dictionary_name or (
tool.Classification.get_location(element) == bsdd_classification.dictionary_namespace_uri
):
classification = element
break
if not classification:
classification = ifcopenshell.api.run(
"classification.add_classification", tool.Ifc.get(), classification=bsdd_classification.domain_name
classification = ifcopenshell.api.classification.add_classification(
tool.Ifc.get(), classification=bsdd_classification.dictionary_name
)
tool.Classification.set_location(classification, bsdd_classification.domain_namespace_uri)
tool.Classification.set_location(classification, bsdd_classification.dictionary_namespace_uri)
for obj in objects:
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
if not ifc_definition_id:
continue
element = tool.Ifc.get().by_id(ifc_definition_id)
reference = ifcopenshell.api.run(
"classification.add_reference",
reference = ifcopenshell.api.classification.add_reference(
tool.Ifc.get(),
products=[element],
classification=classification,
@@ -476,18 +468,16 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
elif is_pset:
pset = ifcopenshell.api.run(
"pset.add_pset", tool.Ifc.get(), product=element, name=classification_pset.name
pset = ifcopenshell.api.pset.add_pset(
tool.Ifc.get(), product=element, name=classification_pset.name
)
else:
pset = ifcopenshell.api.run(
"pset.add_qto", tool.Ifc.get(), product=element, name=classification_pset.name
)
pset = ifcopenshell.api.pset.add_qto(tool.Ifc.get(), product=element, name=classification_pset.name)
if is_pset:
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties=properties)
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties=properties)
else:
ifcopenshell.api.run("pset.edit_qto", tool.Ifc.get(), qto=pset, properties=properties)
ifcopenshell.api.pset.edit_qto(tool.Ifc.get(), qto=pset, properties=properties)
class ChangeClassificationLevel(bpy.types.Operator):
@@ -44,6 +44,12 @@ def get_classifications(self, context):
return ClassificationReferencesData.data["classifications"]
def get_classification_source(self, context):
if not ClassificationsData.is_loaded:
ClassificationsData.load()
return ClassificationsData.data["classification_source"]
class ClassificationReference(PropertyGroup):
name: StringProperty(name="Name")
identification: StringProperty(name="Identification")
@@ -54,15 +60,7 @@ class ClassificationReference(PropertyGroup):
class BIMClassificationProperties(PropertyGroup):
is_adding: BoolProperty(name="Is Adding", default=False)
classification_source: EnumProperty(
items=[
("FILE", "IFC File", ""),
("BSDD", "buildingSMART Data Dictionary", ""),
("MANUAL", "Manual Entry", ""),
],
name="Classification Source",
default="FILE",
)
classification_source: EnumProperty(items=get_classification_source, name="Classification Source")
available_classifications: EnumProperty(items=get_available_classifications, name="Available Classifications")
classification_attributes: CollectionProperty(name="Classification Attributes", type=Attribute)
active_classification_id: IntProperty(name="Active Classification Id")
@@ -54,10 +54,10 @@ class BIM_PT_classifications(Panel):
if self.props.classification_source == "FILE":
self.draw_add_file_ui(context)
elif self.props.classification_source == "BSDD":
self.draw_add_bsdd_ui(context)
elif self.props.classification_source == "MANUAL":
self.draw_add_manual_ui(context)
else:
self.draw_add_bsdd_ui(context)
for classification in ClassificationsData.data["classifications"]:
if self.props.active_classification_id == classification["id"]:
@@ -76,16 +76,6 @@ class BIM_PT_classifications(Panel):
row.operator("bim.enable_adding_manual_classification", text="Add Classification", icon="ADD")
def draw_add_bsdd_ui(self, context):
self.bprops = context.scene.BIMBSDDProperties
if not self.bprops.active_domain:
row = self.layout.row()
row.label(text="No Active bSDD Domain", icon="ERROR")
return
row = self.layout.row()
row.label(text="Active: " + self.bprops.active_domain, icon="URL")
row = self.layout.row()
row.operator("bim.add_classification_from_bsdd", icon="ADD")
@@ -144,10 +134,10 @@ class ReferenceUI:
if self.sprops.classification_source == "FILE":
self.draw_add_file_ui(context)
elif self.sprops.classification_source == "BSDD":
self.draw_add_bsdd_ui(context)
elif self.sprops.classification_source == "MANUAL":
self.draw_add_manual_ui(context)
else:
self.draw_add_bsdd_ui(context)
def draw_add_manual_ui(self, context):
row = self.layout.row()
@@ -163,22 +153,11 @@ class ReferenceUI:
row.operator("bim.enable_adding_manual_classification_reference", text="Add Reference", icon="ADD")
def draw_add_bsdd_ui(self, context):
if not self.bprops.active_domain:
row = self.layout.row()
row.label(text="No Active bSDD Domain", icon="ERROR")
return
row = self.layout.row()
row.label(text="Active: " + self.bprops.active_domain, icon="URL")
row = self.layout.row(align=True)
row.prop(self.bprops, "keyword", text="")
row.prop(self.bprops, "should_filter_ifc_class", text="", icon="FILTER")
row.operator("bim.search_bsdd_classifications", text="", icon="VIEWZOOM")
row = self.layout.row()
row.prop(self.bprops, "should_filter_ifc_class")
row.prop(self.bprops, "use_only_ifc_properties")
if len(self.bprops.classifications):
self.layout.template_list(
"BIM_UL_bsdd_classifications",
@@ -199,21 +178,6 @@ class ReferenceUI:
)
op.obj = self.obj
op.obj_type = self.obj_type
row.operator("bim.get_bsdd_classification_properties", text="", icon="COPY_ID")
if len(self.bprops.classification_psets):
use_only_ifc_properties = self.bprops.use_only_ifc_properties
for pset in self.bprops.classification_psets:
properties = pset.properties
if use_only_ifc_properties:
properties = [p for p in properties if p.metadata == "IFC"]
if not properties:
continue
box = self.layout.box()
row = box.row()
row.label(text=pset.name, icon="COPY_ID")
bonsai.bim.helper.draw_attributes(properties, box)
def draw_add_file_ui(self, context):
if not self.data.data["active_classification_library"]:
@@ -17,12 +17,27 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import json
import ifcopenshell.api
import ifcopenshell.api.constraint
import ifcopenshell.util.attribute
import bonsai.bim.helper
import bonsai.tool as tool
from typing import TYPE_CHECKING
def get_active_object(context: bpy.types.Context, obj_name: str) -> bpy.types.Object:
if obj_name:
obj = bpy.data.objects[obj_name]
else:
assert (obj := context.active_object)
return obj
def get_selected_objects(context: bpy.types.Context, obj_name: str) -> list[bpy.types.Object]:
if obj_name:
objs = [bpy.data.objects[obj_name]]
else:
objs = context.selected_objects
return objs
class LoadObjectives(bpy.types.Operator):
@@ -31,7 +46,7 @@ class LoadObjectives(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BIMConstraintProperties
props = tool.Blender.get_constraint_props()
props.constraints.clear()
for constraint in tool.Ifc.get().by_type("IfcObjective"):
new = props.constraints.add()
@@ -48,7 +63,8 @@ class DisableConstraintEditingUI(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
context.scene.BIMConstraintProperties.is_editing = ""
props = tool.Blender.get_constraint_props()
props.is_editing = ""
bpy.ops.bim.disable_editing_constraint()
return {"FINISHED"}
@@ -59,8 +75,11 @@ class EnableEditingConstraint(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
constraint: bpy.props.IntProperty()
if TYPE_CHECKING:
constraint: int
def execute(self, context):
props = context.scene.BIMConstraintProperties
props = tool.Blender.get_constraint_props()
props.constraint_attributes.clear()
bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.constraint), props.constraint_attributes)
props.active_constraint_id = self.constraint
@@ -73,7 +92,8 @@ class DisableEditingConstraint(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
context.scene.BIMConstraintProperties.active_constraint_id = 0
props = tool.Blender.get_constraint_props()
props.active_constraint_id = 0
return {"FINISHED"}
@@ -83,7 +103,7 @@ class AddObjective(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
result = ifcopenshell.api.run("constraint.add_objective", tool.Ifc.get())
result = ifcopenshell.api.constraint.add_objective(tool.Ifc.get())
bpy.ops.bim.load_objectives()
bpy.ops.bim.enable_editing_constraint(constraint=result.id())
return {"FINISHED"}
@@ -95,7 +115,7 @@ class EditObjective(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.scene.BIMConstraintProperties
props = tool.Blender.get_constraint_props()
attributes = bonsai.bim.helper.export_attributes(props.constraint_attributes)
ifc_file = tool.Ifc.get()
ifcopenshell.api.constraint.edit_objective(
@@ -113,12 +133,13 @@ class RemoveConstraint(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
constraint: bpy.props.IntProperty()
if TYPE_CHECKING:
constraint: int
def _execute(self, context):
props = context.scene.BIMConstraintProperties
props = tool.Blender.get_constraint_props()
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"constraint.remove_constraint", self.file, **{"constraint": self.file.by_id(self.constraint)}
)
ifcopenshell.api.constraint.remove_constraint(self.file, constraint=self.file.by_id(self.constraint))
if props.is_editing == "IfcObjective":
bpy.ops.bim.load_objectives()
return {"FINISHED"}
@@ -130,9 +151,12 @@ class EnableAssigningConstraint(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
if TYPE_CHECKING:
obj: str
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
props = obj.BIMObjectConstraintProperties
obj = get_active_object(context, self.obj)
props = tool.Blender.get_object_constraint_props(obj)
if props.available_constraint_types == "IfcObjective":
bpy.ops.bim.load_objectives()
props.is_adding = props.available_constraint_types
@@ -145,9 +169,12 @@ class DisableAssigningConstraint(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
if TYPE_CHECKING:
obj: str
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
props = obj.BIMObjectConstraintProperties
obj = get_active_object(context, self.obj)
props = tool.Blender.get_object_constraint_props(obj)
props.is_adding = ""
return {"FINISHED"}
@@ -159,8 +186,13 @@ class AssignConstraint(bpy.types.Operator, tool.Ifc.Operator):
obj: bpy.props.StringProperty()
constraint: bpy.props.IntProperty()
if TYPE_CHECKING:
obj: str
constraint: int
def _execute(self, context):
self.file = tool.Ifc.get()
objs = get_selected_objects(context, self.obj)
objs = [bpy.data.objects[self.obj]] if self.obj else context.selected_objects
products = [
self.file.by_id(obj_id)
@@ -168,13 +200,10 @@ class AssignConstraint(bpy.types.Operator, tool.Ifc.Operator):
if (obj_id := tool.Blender.get_object_bim_props(obj).ifc_definition_id)
]
if products:
ifcopenshell.api.run(
"constraint.assign_constraint",
ifcopenshell.api.constraint.assign_constraint(
self.file,
**{
"products": products,
"constraint": self.file.by_id(self.constraint),
},
products=products,
constraint=self.file.by_id(self.constraint),
)
return {"FINISHED"}
@@ -186,21 +215,22 @@ class UnassignConstraint(bpy.types.Operator, tool.Ifc.Operator):
obj: bpy.props.StringProperty()
constraint: bpy.props.IntProperty()
if TYPE_CHECKING:
obj: str
constraint: int
def _execute(self, context):
self.file = tool.Ifc.get()
objs = [bpy.data.objects[self.obj]] if self.obj else context.selected_objects
objs = get_selected_objects(context, self.obj)
products = [
self.file.by_id(obj_id)
for obj in objs
if (obj_id := tool.Blender.get_object_bim_props(obj).ifc_definition_id)
]
if products:
ifcopenshell.api.run(
"constraint.unassign_constraint",
ifcopenshell.api.constraint.unassign_constraint(
self.file,
**{
"products": products,
"constraint": self.file.by_id(self.constraint),
},
products=products,
constraint=self.file.by_id(self.constraint),
)
return {"FINISHED"}
@@ -32,6 +32,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal
def get_available_constraint_types(self, context):
@@ -44,6 +45,10 @@ class Constraint(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
if TYPE_CHECKING:
name: str
ifc_definition_id: int
class BIMConstraintProperties(PropertyGroup):
constraint_attributes: CollectionProperty(name="Constraint Attributes", type=Attribute)
@@ -52,7 +57,18 @@ class BIMConstraintProperties(PropertyGroup):
active_constraint_index: IntProperty(name="Active Constraint Index")
is_editing: StringProperty(name="Is Editing")
if TYPE_CHECKING:
constraint_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
active_constraint_id: int
constraints: bpy.types.bpy_prop_collection_idprop[Constraint]
active_constraint_index: int
is_editing: str
class BIMObjectConstraintProperties(PropertyGroup):
is_adding: StringProperty(name="Is Adding")
available_constraint_types: EnumProperty(items=get_available_constraint_types, name="Available Constraint Types")
if TYPE_CHECKING:
is_adding: str
available_constraint_types: Literal["IfcObjective"]
+31 -8
View File
@@ -16,10 +16,15 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bonsai.tool as tool
from bpy.types import Panel, UIList
from bonsai.bim.helper import draw_attributes
from bonsai.bim.module.constraint.data import ConstraintsData, ObjectConstraintsData
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.constraint.prop import BIMConstraintProperties, BIMObjectConstraintProperties, Constraint
class BIM_PT_constraints(Panel):
@@ -39,7 +44,7 @@ class BIM_PT_constraints(Panel):
if not ConstraintsData.is_loaded:
ConstraintsData.load()
self.props = context.scene.BIMConstraintProperties
self.props = tool.Blender.get_constraint_props()
if not self.props.is_editing or self.props.is_editing == "IfcObjective":
row = self.layout.row(align=True)
@@ -91,8 +96,8 @@ class BIM_PT_object_constraints(Panel):
obj = context.active_object
assert obj
self.oprops = tool.Blender.get_object_bim_props(obj)
self.sprops = context.scene.BIMConstraintProperties
self.props = obj.BIMObjectConstraintProperties
self.sprops = tool.Blender.get_constraint_props()
self.props = tool.Blender.get_object_constraint_props(obj)
self.file = tool.Ifc.get()
self.draw_add_ui()
@@ -131,15 +136,24 @@ class BIM_PT_object_constraints(Panel):
class BIM_UL_constraints(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
def draw_item(
self,
context,
layout,
data: BIMConstraintProperties,
item: Constraint,
icon,
active_data,
active_propname,
):
if item:
row = layout.row(align=True)
row.label(text=item.name)
if context.scene.BIMConstraintProperties.active_constraint_id == item.ifc_definition_id:
if context.scene.BIMConstraintProperties.is_editing == "IfcObjective":
if data.active_constraint_id == item.ifc_definition_id:
if data.is_editing == "IfcObjective":
row.operator("bim.edit_objective", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_constraint", text="", icon="CANCEL")
elif context.scene.BIMConstraintProperties.active_constraint_id:
elif data.active_constraint_id:
row.operator("bim.remove_constraint", text="", icon="X").constraint = item.ifc_definition_id
else:
op = row.operator("bim.enable_editing_constraint", text="", icon="GREASEPENCIL")
@@ -148,7 +162,16 @@ class BIM_UL_constraints(UIList):
class BIM_UL_object_constraints(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
def draw_item(
self,
context,
layout,
data: BIMObjectConstraintProperties,
item: Constraint,
icon,
active_data,
active_propname,
):
if item:
row = layout.row(align=True)
row.label(text=item.name)
@@ -64,6 +64,7 @@ classes = (
operator.RefreshCostScheduleCsv,
operator.LoadCostItemElementQuantities,
operator.LoadCostItemQuantities,
operator.ShowAssignedCostRate,
operator.LoadCostItemResourceQuantities,
operator.LoadCostItemTaskQuantities,
operator.LoadCostItemTypes,
@@ -83,7 +84,7 @@ classes = (
prop.CostItem,
prop.CostItemQuantity,
prop.CostItemType,
prop.CostItemsMapping,
prop.CostSchedulesMapping,
prop.ScheduleColumn,
prop.BIMCostProperties,
ui.BIM_PT_cost_schedules,
+14 -1
View File
@@ -110,6 +110,7 @@ class CostSchedulesData:
cls._load_cost_item_quantities(cost_item, data)
cls._load_cost_values(cost_item, data)
cls._load_nesting_index(cost_item, data)
cls._load_assigned_cost_rate(cost_item, data)
results[cost_item.id()] = data
return results
@@ -154,7 +155,12 @@ class CostSchedulesData:
else:
data["TotalCost"] = data["TotalAppliedValue"] * cost_quantity
if is_sum:
data["TotalAppliedValue"] = None
pass # If it is None it doesn't allow me to assign a cost rate composed by sum
# data["TotalAppliedValue"] = None
@classmethod
def _load_assigned_cost_rate(cls, cost_item: ifcopenshell.entity_instance, data: CostItem) -> None:
data["AssignedCostRate"] = tool.Cost.get_assigned_rate_cost_item(cost_item)
@classmethod
def _load_cost_item_quantities(cls, cost_item: ifcopenshell.entity_instance, data: CostItem) -> None:
@@ -248,6 +254,8 @@ class CostSchedulesData:
) -> None:
value_data = cost_value.get_info()
del value_data["AppliedValue"]
if tool.Cost.get_assigned_rate_cost_item(root_element):
root_element = tool.Cost.get_assigned_rate_cost_item(root_element)
if value_data["UnitBasis"]:
data = cost_value.UnitBasis.get_info()
data["ValueComponent"] = data["ValueComponent"].wrappedValue
@@ -311,6 +319,7 @@ class CostItemRatesData:
def load(cls):
cls.data = {
"schedule_of_rates": cls.schedule_of_rates(),
"cost_schedules": cls.cost_schedules(),
}
cls.is_loaded = True
@@ -322,6 +331,10 @@ class CostItemRatesData:
if s.PredefinedType == "SCHEDULEOFRATES"
]
@classmethod
def cost_schedules(cls) -> list[tuple[str, str, str]]:
return [(str(s.id()), s.Name or "Unnamed", "") for s in tool.Ifc.get().by_type("IfcCostSchedule")]
class CostItemQuantitiesData:
data = {}
+57 -15
View File
@@ -19,12 +19,12 @@
# pyright: reportUnnecessaryTypeIgnoreComment=error
import bpy
import ifcopenshell.api
import textwrap
import ifcopenshell.api.nest
import bonsai.tool as tool
from bpy_extras.io_utils import ImportHelper, ExportHelper
import bonsai.tool as tool
import bonsai.core.cost as core
from pathlib import Path
from typing import get_args, TYPE_CHECKING, Literal
@@ -45,6 +45,7 @@ class AddCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
def draw(self, context):
layout = self.layout
assert layout
props = tool.Cost.get_cost_props()
layout.prop(self, "name", text="Name")
layout.prop(props, "cost_schedule_predefined_types", text="Type")
@@ -148,8 +149,11 @@ class ExpandCostItem(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Expand this cost item"
cost_item: bpy.props.IntProperty()
if TYPE_CHECKING:
cost_item: int
def _execute(self, context):
core.expand_cost_item(tool.Cost, cost_item=tool.Ifc.get().by_id(self.cost_item))
core.expand_cost_item(tool.Cost, cost_item_id=self.cost_item)
class ExpandCostItems(bpy.types.Operator, tool.Ifc.Operator):
@@ -157,7 +161,6 @@ class ExpandCostItems(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Expand Cost Items"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Expand all cost items"
cost_items: bpy.props.StringProperty()
def _execute(self, context):
core.expand_cost_items(tool.Cost)
@@ -170,8 +173,11 @@ class ContractCostItem(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Contract a cost item"
cost_item: bpy.props.IntProperty()
if TYPE_CHECKING:
cost_item: int
def _execute(self, context):
core.contract_cost_item(tool.Cost, cost_item=tool.Ifc.get().by_id(self.cost_item))
core.contract_cost_item(tool.Cost, cost_item_id=self.cost_item)
class ContractCostItems(bpy.types.Operator, tool.Ifc.Operator):
@@ -179,7 +185,6 @@ class ContractCostItems(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Contract Cost Item"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Collapse cost item tree"
cost_item: bpy.props.IntProperty()
def _execute(self, context):
core.contract_cost_items(tool.Cost)
@@ -191,6 +196,9 @@ class RemoveCostItem(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
if TYPE_CHECKING:
cost_item: int
def _execute(self, context):
core.remove_cost_item(tool.Ifc, tool.Cost, cost_item_id=self.cost_item)
@@ -268,7 +276,7 @@ class AssignCostItemQuantity(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Assign Cost Item Quantity"
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
related_object_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
related_object_type: bpy.props.EnumProperty( # pyright: ignore [reportRedeclaration]
items=[(i, i, "") for i in get_args(tool.Cost.RELATED_OBJECT_TYPE)],
)
prop_name: bpy.props.StringProperty()
@@ -608,6 +616,34 @@ class LoadCostItemQuantities(bpy.types.Operator):
return {"FINISHED"}
class ShowAssignedCostRate(bpy.types.Operator):
bl_idname = "bim.show_assigned_cost_rate"
bl_label = "Info about the assigned cost item rate"
bl_options = {"REGISTER"}
assigned_rate_identification: bpy.props.StringProperty()
assigned_rate_name: bpy.props.StringProperty()
assigned_rate_description: bpy.props.StringProperty()
assigned_rate_total_value: bpy.props.FloatProperty()
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self, width=450)
def execute(self, context):
# core.load_cost_item_quantities(tool.Cost) IS IT NECESSARY?
return {"FINISHED"}
def draw(self, context):
layout = self.layout
wrapper = textwrap.TextWrapper(width=80)
layout.label(text=f"ID: {self.assigned_rate_identification}")
layout.label(text=f"Name: {self.assigned_rate_name}")
layout.label(text="Description:")
for line in wrapper.wrap(str(self.assigned_rate_description)):
layout.label(text=line)
layout.label(text=f"Value: {self.assigned_rate_total_value}")
class LoadCostItemElementQuantities(bpy.types.Operator):
bl_idname = "bim.load_cost_item_element_quantities"
bl_label = "Load Cost Item Element Quantities"
@@ -658,7 +694,10 @@ class AssignCostValue(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
core.assign_cost_value(
tool.Ifc, cost_item=tool.Ifc.get().by_id(self.cost_item), cost_rate=tool.Ifc.get().by_id(self.cost_rate)
tool.Ifc,
tool.Cost,
cost_item=tool.Ifc.get().by_id(self.cost_item),
cost_rate=tool.Ifc.get().by_id(self.cost_rate),
)
@@ -668,6 +707,9 @@ class ExpandCostItemRate(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
if TYPE_CHECKING:
cost_item: int
def _execute(self, context):
core.expand_cost_item_rate(tool.Cost, self.cost_item)
return {"FINISHED"}
@@ -679,6 +721,9 @@ class ContractCostItemRate(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
if TYPE_CHECKING:
cost_item: int
def _execute(self, context):
core.contract_cost_item_rate(tool.Cost, self.cost_item)
return {"FINISHED"}
@@ -745,7 +790,7 @@ class ClearCostItemAssignments(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Clear Cost Item Product Assignments"
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
related_object_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
related_object_type: bpy.props.EnumProperty( # pyright: ignore [reportRedeclaration]
items=[(i, i, "") for i in get_args(tool.Cost.RELATED_OBJECT_TYPE)],
)
@@ -801,13 +846,10 @@ class ReorderCostItem(bpy.types.Operator, tool.Ifc.Operator):
cost_item: bpy.props.IntProperty()
def _execute(self, context):
ifcopenshell.api.run(
"nest.reorder_nesting",
ifcopenshell.api.nest.reorder_nesting(
tool.Ifc.get(),
**{
"item": tool.Ifc.get().by_id(self.cost_item),
"new_index": self.new_index,
},
item=tool.Ifc.get().by_id(self.cost_item),
new_index=self.new_index,
)
tool.Cost.load_cost_schedule_tree()
+36 -24
View File
@@ -18,6 +18,7 @@
import bpy
import ifcopenshell.api
import ifcopenshell.api.cost
import bonsai.tool as tool
from bonsai.bim.module.classification.data import CostClassificationsData
from bonsai.bim.module.cost.data import CostSchedulesData, CostItemRatesData, CostItemQuantitiesData
@@ -33,44 +34,51 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal
from typing import TYPE_CHECKING, Literal, Union
def get_schedule_of_rates(self, context):
def get_schedule_of_rates(self: "BIMCostProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
if not CostItemRatesData.is_loaded:
CostItemRatesData.load()
return CostItemRatesData.data["schedule_of_rates"]
# return CostItemRatesData.data["schedule_of_rates"]
return CostItemRatesData.data["cost_schedules"]
def update_schedule_of_rates(self, context):
def update_schedule_of_rates(self: "BIMCostProperties", context: bpy.types.Context) -> None:
tool.Cost.load_schedule_of_rates_tree(schedule_of_rates=tool.Ifc.get().by_id(int(self.schedule_of_rates)))
def get_quantity_types(self, context):
def get_quantity_types(self: "BIMCostProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
if not CostSchedulesData.is_loaded:
CostSchedulesData.load()
return CostSchedulesData.data["quantity_types"]
def get_product_quantity_names(self, context):
def get_product_quantity_names(
self: "BIMCostProperties", context: bpy.types.Context
) -> tool.Blender.BLENDER_ENUM_ITEMS:
if not CostItemQuantitiesData.is_loaded:
CostItemQuantitiesData.load()
return CostItemQuantitiesData.data["product_quantity_names"]
def get_process_quantity_names(self, context):
def get_process_quantity_names(
self: "BIMCostProperties", context: bpy.types.Context
) -> tool.Blender.BLENDER_ENUM_ITEMS:
if not CostItemQuantitiesData.is_loaded:
CostItemQuantitiesData.load()
return CostItemQuantitiesData.data["process_quantity_names"]
def get_resource_quantity_names(self, context):
def get_resource_quantity_names(
self: "BIMCostProperties", context: bpy.types.Context
) -> tool.Blender.BLENDER_ENUM_ITEMS:
if not CostItemQuantitiesData.is_loaded:
CostItemQuantitiesData.load()
return CostItemQuantitiesData.data["resource_quantity_names"]
def update_active_cost_item_index(self, context):
def update_active_cost_item_index(self: "BIMCostProperties", context: bpy.types.Context) -> None:
schedule = tool.Ifc.get().by_id(self.active_cost_schedule_id)
if schedule.PredefinedType == "SCHEDULEOFRATES":
tool.Cost.load_cost_item_types()
@@ -83,11 +91,11 @@ def update_cost_item_identification(self: "CostItem", context: bpy.types.Context
props = tool.Cost.get_cost_props()
if not props.is_cost_update_enabled or self.identification == "XXX":
return
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"cost.edit_cost_item",
self.file,
**{"cost_item": self.file.by_id(self.ifc_definition_id), "attributes": {"Identification": self.identification}},
ifc_file = tool.Ifc.get()
ifcopenshell.api.cost.edit_cost_item(
ifc_file,
cost_item=ifc_file.by_id(self.ifc_definition_id),
attributes={"Identification": self.identification},
)
if props.active_cost_item_id == self.ifc_definition_id:
attribute = props.cost_item_attributes["Identification"]
@@ -98,11 +106,11 @@ def update_cost_item_name(self: "CostItem", context: bpy.types.Context) -> None:
props = tool.Cost.get_cost_props()
if not props.is_cost_update_enabled or self.name == "Unnamed":
return
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"cost.edit_cost_item",
self.file,
**{"cost_item": self.file.by_id(self.ifc_definition_id), "attributes": {"Name": self.name}},
ifc_file = tool.Ifc.get()
ifcopenshell.api.cost.edit_cost_item(
ifc_file,
cost_item=ifc_file.by_id(self.ifc_definition_id),
attributes={"Name": self.name},
)
if props.active_cost_item_id == self.ifc_definition_id:
attribute = props.cost_item_attributes["Name"]
@@ -167,12 +175,12 @@ class CostItemQuantity(PropertyGroup):
total_cost_quantity: float
class CostItemsMapping(PropertyGroup):
cost_item_id: IntProperty(name="cost_item_id")
class CostSchedulesMapping(PropertyGroup):
cost_schedule_id: IntProperty(name="cost_schedule_id")
csv_filepath: StringProperty(name="filepath")
if TYPE_CHECKING:
cost_item_id: int
cost_schedule_id: int
csv_filepath: str
@@ -281,7 +289,7 @@ class BIMCostProperties(PropertyGroup):
custom_currency: StringProperty(
name="Custom Currency", default="USD", description="Custom Currency in ISO 4217 format"
)
cost_schedule_files: CollectionProperty(name="Cost Schedule Files", type=CostItemsMapping)
cost_schedule_files: CollectionProperty(name="Cost Schedule Files", type=CostSchedulesMapping)
if TYPE_CHECKING:
cost_schedule_predefined_types: str
@@ -336,4 +344,8 @@ class BIMCostProperties(PropertyGroup):
show_cost_item_operators: bool
currency: str
custom_currency: str
cost_schedule_files: bpy.types.bpy_prop_collection_idprop[CostItemsMapping]
cost_schedule_files: bpy.types.bpy_prop_collection_idprop[CostSchedulesMapping]
@property
def active_cost_item(self) -> Union[CostItem, None]:
return tool.Blender.get_active_uilist_element(self.cost_items, self.active_cost_item_index)
+23 -4
View File
@@ -76,12 +76,12 @@ class BIM_PT_cost_schedules(Panel):
col = row0.column()
col.label(text="Linked CSV:")
row_1 = col.row(align=True)
if self.props.active_cost_schedule_id in [item.cost_item_id for item in self.props.cost_schedule_files]:
if self.props.active_cost_schedule_id in [item.cost_schedule_id for item in self.props.cost_schedule_files]:
file = next(
(
item.csv_filepath
for item in self.props.cost_schedule_files
if item.cost_item_id == self.props.active_cost_schedule_id
if item.cost_schedule_id == self.props.active_cost_schedule_id
),
None,
)
@@ -608,8 +608,8 @@ class BIM_PT_cost_item_rates(Panel):
return False
if not CostSchedulesData.is_loaded:
return False
if CostSchedulesData.data["is_editing_rates"]:
return False
# if CostSchedulesData.data["is_editing_rates"]:
# return False
if total_cost_items > 0 and props.active_cost_item_index < total_cost_items:
return True
return False
@@ -655,6 +655,8 @@ class BIM_UL_cost_items_trait:
for column in props.columns:
split2.label(text=column.name)
split2.label(text="Total Cost")
split2.alignment = "LEFT"
split2.label(text="Rate")
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
@@ -685,6 +687,8 @@ class BIM_UL_cost_items_trait:
if self.props.change_cost_item_parent:
self.draw_parent_operator(row, item.ifc_definition_id)
self.draw_assigned_rate_column(split2, cost_item)
# TODO: reimplement "bim.copy_cost_item_values" somewhere with better UX
def draw_parent_operator(self, row, cost_item_id):
@@ -724,6 +728,21 @@ class BIM_UL_cost_items_trait:
else:
layout.label(text="-")
def draw_assigned_rate_column(self, layout, cost_item):
row = layout.row()
row.alignment = "LEFT"
if cost_item["AssignedCostRate"] is not None:
op = row.operator("bim.show_assigned_cost_rate", text="", emboss=False, icon="ZOOM_IN")
identification = cost_item["AssignedCostRate"].Identification
op.assigned_rate_identification = identification if identification is not None else "XXX"
op.assigned_rate_name = cost_item["AssignedCostRate"].Name or ""
op.assigned_rate_description = cost_item["AssignedCostRate"].Description or ""
op.assigned_rate_total_value = CostSchedulesData.data["cost_items"][cost_item["AssignedCostRate"].id()][
"TotalAppliedValue"
]
else:
row.label(text="-")
def draw_value_column(self, layout, cost_item):
if cost_item["TotalAppliedValue"]:
text = "{0:,.2f}".format(cost_item["TotalAppliedValue"]).replace(",", " ")
@@ -312,6 +312,7 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
import ifccsv
props = tool.Blender.get_csv_props()
ifc_file: ifcopenshell.file
if props.should_load_from_memory:
ifc_file = tool.Ifc.get()
else:
+37 -23
View File
@@ -42,7 +42,10 @@ from bpy_extras.io_utils import ImportHelper, ExportHelper
from pathlib import Path
from bonsai import get_debug_info, format_debug_info
from bonsai.bim.ifc import IfcStore
from typing import get_args, Union
from typing import get_args, Union, Any, TYPE_CHECKING, Literal, get_args, assert_never
if TYPE_CHECKING:
from bonsai.bim.prop import Attribute
class CopyDebugInformation(bpy.types.Operator):
@@ -70,6 +73,7 @@ class CopyDebugInformation(bpy.types.Operator):
print(text_with_backticks)
print("-" * 80)
assert context.window_manager
context.window_manager.clipboard = text_with_backticks
return {"FINISHED"}
@@ -347,8 +351,9 @@ class SelectHighPolygonMeshes(bpy.types.Operator):
threshold: bpy.props.IntProperty()
def execute(self, context):
assert context.view_layer
for obj in context.view_layer.objects:
if obj.type == "MESH" and len(obj.data.polygons) > self.threshold:
if isinstance(obj.data, bpy.types.Mesh) and len(obj.data.polygons) > self.threshold:
obj.select_set(True)
return {"FINISHED"}
@@ -361,6 +366,7 @@ class SelectHighestPolygonMeshes(bpy.types.Operator):
percentile: bpy.props.IntProperty()
def execute(self, context):
assert context.view_layer
objects = [obj for obj in context.view_layer.objects if obj.type == "MESH"]
if objects:
percentile = len(max(objects, key=lambda o: len(o.data.polygons)).data.polygons) * self.percentile / 100
@@ -423,7 +429,7 @@ class InspectFromStepId(bpy.types.Operator):
new.int_value = inverse.id()
return {"FINISHED"}
def add_attribute(self, prop, key, value):
def add_attribute(self, prop: "bpy.types.bpy_prop_collection_idprop[Attribute]", key: str, value: Any) -> None:
if isinstance(value, tuple) and len(value) < 10:
for i, item in enumerate(value):
self.add_attribute(prop, key + f"[{i}]", item)
@@ -459,8 +465,10 @@ class InspectFromObject(bpy.types.Operator):
def poll(cls, context):
if not context.active_object:
cls.poll_message_set("No Active Object")
return False
elif not cls.get_active_object_ifc_definition(context):
cls.poll_message_set("Active Object doesn't have an IFC definition")
return False
else:
return True
@@ -492,6 +500,7 @@ class PrintObjectPlacement(bpy.types.Operator):
if self.create_empty_object:
bpy.ops.object.empty_add(type="ARROWS")
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
assert context.active_object
context.active_object.matrix_world = placement.transpose()
context.active_object.matrix_world.translation *= si_conversion
context.active_object.empty_display_size = self.arrow_size
@@ -730,21 +739,22 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator, ExportHe
tool.Ifc.get().write(self.filepath)
PurgeObjectType = Literal["TYPE", "PROFILE", "STYLE", "MATERIAL", "ORGANIZATION", "APPLICATION"]
class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.purge_unused_objects"
bl_label = "Purge Unused Objects"
bl_options = {"REGISTER", "UNDO"}
object_type: bpy.props.EnumProperty(
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
name="Object Type",
items=(
("TYPE", "Type", ""),
("PROFILE", "Profile", ""),
("STYLE", "Style", ""),
("MATERIAL", "Material", ""),
),
items=((s, s.capitalize(), "") for s in get_args(PurgeObjectType)),
)
if TYPE_CHECKING:
object_type: PurgeObjectType
def _execute(self, context):
object_type = self.object_type
if object_type == "TYPE":
@@ -755,9 +765,10 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
purged = tool.Style.purge_unused_styles()
elif object_type == "MATERIAL":
purged = tool.Material.purge_unused_materials()
elif object_type in ("APPLICATION", "ORGANIZATION"):
purged = core.purge_unused_elements(tool.Ifc, tool.Debug, "IfcApplication")
else:
self.report({"ERROR"}, f"Invalid object type {object_type}.")
return {"CANCELLED"}
assert_never(object_type)
self.report({"INFO"}, f"{purged} unused {object_type.lower()}s were purged.")
@@ -776,6 +787,10 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Material.get_material_props()
if props.is_editing:
bpy.ops.bim.load_materials()
elif object_type == "ORGANIZATION":
props = tool.Owner.get_owner_props()
if tool.Ifc.get_entity_by_id(props.active_organisation_id) is None:
props.active_organisation_id = 0
class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
@@ -784,23 +799,21 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "For materials currently only IfcMaterials are supported"
bl_options = {"REGISTER", "UNDO"}
object_type: bpy.props.EnumProperty(
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
name="Object Type",
items=(
("TYPE", "Type", ""),
("PROFILE", "Profile", ""),
("STYLE", "Style", ""),
("MATERIAL", "Material", ""),
),
items=((s, s.capitalize(), "") for s in get_args(PurgeObjectType)),
)
if TYPE_CHECKING:
object_type: PurgeObjectType
def _execute(self, context):
object_type: str = self.object_type
if object_type in ("STYLE", "MATERIAL"):
merged_data = tool.Debug.merge_identical_objects(object_type)
else:
self.report({"ERROR"}, f"Invalid object type {object_type}.")
if object_type in ("PROFILE", "TYPE"):
self.report({"ERROR"}, f"Unsupported object type {object_type}.")
return {"CANCELLED"}
merged_data = tool.Debug.merge_identical_objects(object_type)
plural_object_type = f"{object_type.lower()}s"
if merged_data:
for element_type, element_names in merged_data.items():
@@ -1017,6 +1030,7 @@ class RestartBlender(bpy.types.Operator):
def execute(self, context):
# Save preferences manually since we're restarting Blender using .execv
# and it doens't have a chance to save them on exit.
assert context.preferences
if context.preferences.use_preferences_save:
bpy.ops.wm.save_userpref()
+1
View File
@@ -31,6 +31,7 @@ class BIM_PT_debug(Panel):
bl_parent_id = "BIM_PT_tab_quality_control"
def draw(self, context):
assert self.layout
layout = self.layout
props = tool.Debug.get_debug_props()
+74 -10
View File
@@ -236,23 +236,39 @@ class DecoratorData:
fill_cache = {}
@classmethod
def load(cls):
def load(cls, handler):
cls.is_loaded = True
cls.cut_cache = {}
cls.layerset_cache = {}
text = {}
dimension = {}
fall = {}
symbol = {}
for obj in bpy.context.visible_objects:
if not (element := tool.Ifc.get_entity(obj)):
continue
if tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
text[obj.name] = cls.get_ifc_text_data(obj)
if tool.Drawing.is_annotation_object_type(element, ("TEXT", "TEXT_LEADER")):
text[obj.name] = cls.get_text_data(obj)
if text[obj.name]["Symbol"]:
symbol[obj.name] = cls.get_symbol_data(obj)
elif tool.Drawing.is_annotation_object_type(
element, ("DIMENSION", "DIAMETER", "SECTION_LEVEL", "PLAN_LEVEL", "RADIUS")
):
dimension[obj.name] = cls.get_dimension_data(obj)
cls.data = {"text": text, "dimension": dimension}
elif tool.Drawing.is_annotation_object_type(
element, ("FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT")
):
fall[obj.name] = cls.get_fall_data(obj)
elif tool.Drawing.is_annotation_object_type(element, ("SYMBOL",)):
symbol[obj.name] = cls.get_symbol_data(obj)
cls.data = {
"text": text,
"dimension": dimension,
"fall": fall,
"symbol": symbol,
"object_decorators": cls.object_decorators(handler),
}
@classmethod
def get_batting_thickness(cls, obj):
@@ -311,7 +327,7 @@ class DecoratorData:
return display_data
@classmethod
def get_ifc_text_data(cls, obj: bpy.types.Object) -> dict:
def get_text_data(cls, obj: bpy.types.Object) -> dict:
"""used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER"\n
returns font size in mm for current ifc text object"""
element = tool.Ifc.get_entity(obj)
@@ -354,11 +370,6 @@ class DecoratorData:
return {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at}
@classmethod
def get_symbol(cls, obj: bpy.types.Object) -> Union[str, None]:
"""used by IfcAnnotations with ObjectType MULTI_SYMBOL"""
return tool.Drawing.get_annotation_symbol(tool.Ifc.get_entity(obj))
@classmethod
def get_dimension_data(cls, obj):
"""used by Ifc Annotations with ObjectType:
@@ -394,6 +405,59 @@ class DecoratorData:
"custom_unit": custom_unit,
}
@classmethod
def get_fall_data(cls, obj):
object_type = None
if element := tool.Ifc.get_entity(obj):
object_type = ifcopenshell.util.element.get_predefined_type(element)
return {"object_type": object_type}
@classmethod
def get_symbol_data(cls, obj):
return tool.Drawing.get_annotation_symbol(tool.Ifc.get_entity(obj))
@classmethod
def object_decorators(cls, handler):
import bonsai.bim.module.drawing.decoration
if not bonsai.bim.module.drawing.decoration.DecorationsHandler.installed:
return []
props = tool.Drawing.get_document_props()
if (drawing := props.get_active_drawing()) is None:
return []
camera = tool.Ifc.get_object(drawing)
assert isinstance(camera, bpy.types.Object)
collection = tool.Blender.get_object_bim_props(camera).collection
assert collection
results = []
viewport = tool.Blender.get_view3d_space()
for obj in collection.all_objects:
if not obj.visible_get(viewport=viewport):
continue
element = tool.Ifc.get_entity(obj)
if not element:
continue
if not element.is_a("IfcAnnotation"):
continue
object_type: Union[str, None] = ifcopenshell.util.element.get_predefined_type(element)
if object_type == "DRAWING":
continue
if dec := handler.decorators.get(object_type, None):
results.append((obj, dec))
elif isinstance(obj.data, bpy.types.Mesh):
if object_type == "LINEWORK" and "dashed" in str(
ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
).split(" "):
results.append((obj, handler.decorators["HIDDEN_LINE"]))
else:
results.append((obj, handler.decorators["MISC"]))
return results
class AnnotationData:
data = {}
@@ -24,6 +24,7 @@ import bmesh
import shapely
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.unit
import bonsai.tool as tool
import bonsai.bim.module.drawing.helper as helper
@@ -37,8 +38,9 @@ from bonsai.bim.module.drawing.data import DecoratorData, DrawingsData
from bonsai.bim.module.drawing.shaders import add_verts_sequence, add_offsets
from bonsai.bim.module.drawing.helper import format_distance
from timeit import default_timer as timer
from functools import lru_cache
from typing import Optional, Iterator, Type, Union
from functools import cache
from typing import Optional, Union
from collections.abc import Iterator
UNSPECIAL_ELEMENT_COLOR = (0.2, 0.2, 0.2, 1) # GREY
@@ -495,7 +497,7 @@ class BaseDecorator:
self.draw_label(context, text=text, line_no=line_number_start, multiline=True, **draw_label_kwargs)
@lru_cache(maxsize=None)
@cache
def format_value(self, context, value, custom_unit=None):
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
precision = drawing_pset_data.get("MetricPrecision", None)
@@ -562,8 +564,7 @@ class BaseDecorator:
MiscDecorator.decorate(self, context, obj)
return
symbol = DecoratorData.get_symbol(obj)
if not symbol:
if not (symbol := DecoratorData.data["symbol"].get(obj.name, None)):
return
for vert in mesh.vertices:
@@ -572,8 +573,7 @@ class BaseDecorator:
return
# EMPTY objects
symbol = DecoratorData.get_symbol(obj)
if not symbol:
if not (symbol := DecoratorData.data["symbol"].get(obj.name, None)):
return
rotation = -Vector((1, 0)).angle_signed(annotation_dir)
@@ -998,8 +998,6 @@ class FallDecorator(BaseDecorator):
# generate label text
# same function as in svgwriter.py
def get_label_text():
element = tool.Ifc.get_entity(obj)
assert element
B, A = [v.co.xyz for v in spline_points[:2]]
rise = abs(A.z - B.z)
O = A.copy()
@@ -1011,8 +1009,8 @@ class FallDecorator(BaseDecorator):
else:
angle = 90
# ues SLOPE_ANGLE as default
object_type = ifcopenshell.util.element.get_predefined_type(element)
# uses SLOPE_ANGLE as default
object_type = DecoratorData.data["fall"].get(obj, {}).get("object_type", None)
if object_type in ("FALL", "SLOPE_ANGLE"):
return f"{angle}°"
elif object_type == "SLOPE_FRACTION":
@@ -1023,6 +1021,7 @@ class FallDecorator(BaseDecorator):
if angle == 90:
return "-"
return f"{round(angle_tg * 100)} %"
return "NO DATA"
if spline_points:
text = get_label_text()
@@ -1650,9 +1649,15 @@ class CutDecorator:
all_vertex_i_offset = 0
selected_vertex_i_offset = 0
classes_no_cut_str = tool.Drawing.get_document_props().classes_no_cut
classes_no_cut = [word.strip() for word in classes_no_cut_str.split(",")]
for obj in [o for o in bpy.context.visible_objects if o.type == "MESH"]:
if not (element := tool.Ifc.get_entity(obj)):
continue
# Skip the elements in the following classes
if element.is_a() in classes_no_cut:
continue
self.decorate(context, obj, element)
verts, edges = DecoratorData.cut_cache[element.id()]
@@ -1892,7 +1897,7 @@ class CutDecorator:
class DecorationsHandler:
decorators_classes: list[Type[BaseDecorator]] = [
decorators_classes: list[type[BaseDecorator]] = [
DimensionDecorator,
AngleDecorator,
GridDecorator,
@@ -1915,17 +1920,18 @@ class DecorationsHandler:
]
installed = None
handler = None
@classmethod
def install(cls, context):
if cls.installed:
cls.uninstall()
if not DecoratorData.is_loaded:
DecoratorData.load()
handler = cls()
# NOTE: we USE POST_PIXEL here so that we can use both POLYLINE_UNIFORM_COLOR
# and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL")
if not DecoratorData.is_loaded:
DecoratorData.load(handler)
@classmethod
def uninstall(cls):
@@ -1948,53 +1954,9 @@ class DecorationsHandler:
for decorator in self.decorators.values():
decorator.font_id = font_id
def get_objects_and_decorators(self, collection):
# TODO: do it in data instead of the handler for performance?
results = []
viewport = bpy.context.space_data
for obj in collection.all_objects:
if not obj.visible_get(viewport=viewport):
continue
element = tool.Ifc.get_entity(obj)
if not element:
continue
if not element.is_a("IfcAnnotation"):
continue
object_type: Union[str, None] = ifcopenshell.util.element.get_predefined_type(element)
if object_type == "DRAWING":
continue
if dec := self.decorators.get(object_type, None):
results.append((obj, dec))
elif isinstance(obj.data, bpy.types.Mesh):
if object_type == "LINEWORK" and "dashed" in str(
ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
).split(" "):
results.append((obj, self.decorators["HIDDEN_LINE"]))
else:
results.append((obj, self.decorators["MISC"]))
return results
def __call__(self, context):
props = tool.Drawing.get_document_props()
drawing = props.get_active_drawing()
if drawing is None:
return
camera = tool.Ifc.get_object(drawing)
assert isinstance(camera, bpy.types.Object)
collection = tool.Blender.get_object_bim_props(camera).collection
assert collection
if not DrawingsData.is_loaded:
DrawingsData.load()
object_decorators = self.get_objects_and_decorators(collection)
for obj, decorator in object_decorators:
for obj, decorator in DecoratorData.data["object_decorators"]:
decorator.decorate(context, obj)
@@ -24,18 +24,21 @@ import bmesh
import shutil
import hashlib
import shapely
import shapely.ops
import subprocess
import numpy as np
import multiprocessing
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.document
import ifcopenshell.api.pset
import ifcopenshell.api.style
import ifcopenshell.ifcopenshell_wrapper
import ifcopenshell.geom
import ifcopenshell.util.selector
import ifcopenshell.util.representation
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.selector
import ifcopenshell.util.unit
import bonsai.bim.helper
import bonsai.bim.handler
import bonsai.tool as tool
@@ -48,7 +51,7 @@ import bonsai.bim.export_ifc
from bpy_extras.io_utils import ImportHelper
from bonsai.bim.module.drawing.decoration import CutDecorator
from bonsai.bim.module.drawing.data import DecoratorData
from typing import NamedTuple, List, Union, Optional, Literal, TYPE_CHECKING, Any, TypedDict
from typing import NamedTuple, Union, Optional, Literal, TYPE_CHECKING, Any, TypedDict
from lxml import etree
from math import radians
from mathutils import Vector, Color, Matrix
@@ -61,7 +64,7 @@ from bpy_extras.image_utils import load_image
if TYPE_CHECKING:
from bonsai.bim.module.drawing.prop import RenderType
from bonsai.bim.module.project.prop import Link
from bpy._typing import rna_enums
from bpy.stub_internal import rna_enums
cwd = os.path.dirname(os.path.realpath(__file__))
@@ -82,8 +85,8 @@ class profile:
class LineworkContexts(NamedTuple):
body: List[List[int]]
annotation: List[List[int]]
body: list[list[int]]
annotation: list[list[int]]
class AddAnnotationType(bpy.types.Operator, tool.Ifc.Operator):
@@ -117,22 +120,24 @@ class AddAnnotationType(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", use_existing_object_by_name=obj.name)
class EnableAddAnnotationType(bpy.types.Operator, tool.Ifc.Operator):
class EnableAddAnnotationType(bpy.types.Operator):
bl_idname = "bim.enable_add_annotation_type"
bl_label = "Enable Add Annotation Type"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
tool.Drawing.get_annotation_props().is_adding_type = True
return {"FINISHED"}
class DisableAddAnnotationType(bpy.types.Operator, tool.Ifc.Operator):
class DisableAddAnnotationType(bpy.types.Operator):
bl_idname = "bim.disable_add_annotation_type"
bl_label = "Disable Add Annotation Type"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
tool.Drawing.get_annotation_props().is_adding_type = False
return {"FINISHED"}
class AddDrawing(bpy.types.Operator, tool.Ifc.Operator):
@@ -183,9 +188,11 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
return True
def invoke(self, context, event):
assert context.window_manager
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
assert self.layout
row = self.layout
row.prop(self, "should_duplicate_annotations")
@@ -225,14 +232,27 @@ class CreateDrawing(bpy.types.Operator):
+ "Add the CTRL modifier to optionally open drawings to view them as\n"
+ "they are created"
)
print_all: bpy.props.BoolProperty(name="Print All", default=False, options={"SKIP_SAVE"})
open_viewer: bpy.props.BoolProperty(name="Open in Viewer", default=False, options={"SKIP_SAVE"})
sync: bpy.props.BoolProperty(
print_all: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
name="Print All",
default=False,
options={"SKIP_SAVE"},
)
open_viewer: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
name="Open in Viewer",
default=False,
options={"SKIP_SAVE"},
)
sync: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
name="Sync Before Creating Drawing",
description="Could save some time if you're sure IFC and current Blender session are already in sync",
default=True,
)
if TYPE_CHECKING:
print_all: bool
open_viewer: bool
sync: bool
drawing_name: str
is_manifold_cache: dict[str, bool]
@@ -268,6 +288,7 @@ class CreateDrawing(bpy.types.Operator):
assert context.scene and context.scene.camera
active_drawing_id = tool.Blender.get_ifc_definition_id(context.scene.camera)
original_drawing_id = None
if self.print_all:
original_drawing_id = active_drawing_id
drawings_to_print = [d.ifc_definition_id for d in self.props.drawings if d.is_selected and d.is_drawing]
@@ -297,16 +318,15 @@ class CreateDrawing(bpy.types.Operator):
# Reassign props as data is recreated during the update.
self.cprops = tool.Drawing.get_camera_props(self.camera)
self.svg_writer = svgwriter.SvgWriter()
self.svg_writer.human_scale = self.human_scale
self.svg_writer.scale = self.scale
self.svg_writer.camera = self.camera
self.svg_writer.camera_width, self.svg_writer.camera_height = self.get_camera_dimensions()
self.svg_writer.camera_projection = tuple(
self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
camera_dims = self.get_camera_dimensions()
self.svg_writer = svgwriter.SvgWriter(
camera_width=camera_dims[0],
camera_height=camera_dims[1],
camera=self.camera,
camera_projection=(self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))),
scale=self.scale,
human_scale=self.human_scale,
)
self.svg_writer.calculate_scale()
self.svg_writer.setup_drawing_resource_paths(self.camera_element)
underlay_svg = None
@@ -338,11 +358,14 @@ class CreateDrawing(bpy.types.Operator):
if not self.open_viewer:
self.report({"INFO"}, f"{len(drawings_to_print)} drawings created...")
if self.print_all:
assert original_drawing_id is not None
bpy.ops.bim.activate_drawing(drawing=original_drawing_id, should_view_from_camera=False)
return {"FINISHED"}
def get_camera_dimensions(self):
def get_camera_dimensions(self) -> tuple[float, float]:
assert bpy.context.scene
render = bpy.context.scene.render
assert isinstance(self.camera.data, bpy.types.Camera)
if self.is_landscape(render):
width = self.camera.data.ortho_scale
height = width / render.resolution_x * render.resolution_y
@@ -442,7 +465,10 @@ class CreateDrawing(bpy.types.Operator):
previous_format = context.scene.render.image_settings.file_format
space.shading.type = "RENDERED"
context.scene.render.image_settings.file_format = "PNG"
bpy.ops.render.opengl(write_still=True)
with tool.Blender.bonsai_crash_txt("render.opengl"):
bpy.ops.render.opengl(write_still=True)
space.shading.type = previous_shading
context.scene.render.image_settings.file_format = previous_format
@@ -1036,7 +1062,7 @@ class CreateDrawing(bpy.types.Operator):
# file 2 only has the groups we are interested in.
# in fact in the approach, it's only a single group
g2 = list(yield_groups(svg2))[0]
g2 = next(yield_groups(svg2))
# Loop over the cell paths
for pi, p in enumerate(g2.getElementsByTagName("path")):
@@ -1818,6 +1844,7 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Drawing.get_document_props()
active_drawing = props.drawings[props.active_drawing_index]
assert active_drawing
ifc_file = tool.Ifc.get()
active_sheet = tool.Drawing.get_active_sheet(context)
drawing = tool.Ifc.get().by_id(active_drawing.ifc_definition_id)
@@ -1841,7 +1868,7 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
self.report({"ERROR"}, "The drawing must be generated before adding to a sheet.")
return
reference = tool.Ifc.run("document.add_reference", information=sheet)
reference = ifcopenshell.api.document.add_reference(ifc_file, information=sheet)
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
@@ -1857,7 +1884,7 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
Location=drawing_reference.Location,
Description="DRAWING",
)
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
ifcopenshell.api.document.edit_reference(ifc_file, reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.add_drawing(reference, drawing, sheet)
@@ -1928,6 +1955,7 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Drawing.get_document_props()
svg2pdf_command = tool.Blender.get_addon_preferences().svg2pdf_command
svg2dxf_command = tool.Blender.get_addon_preferences().svg2dxf_command
ifc_file = tool.Ifc.get()
if self.create_all:
sheets = [tool.Ifc.get().by_id(s.ifc_definition_id) for s in props.sheets if s.is_sheet and s.is_selected]
@@ -1966,9 +1994,9 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
has_sheet_reference = True
if not has_sheet_reference:
reference = tool.Ifc.run("document.add_reference", information=sheet)
tool.Ifc.run(
"document.edit_reference",
reference = ifcopenshell.api.document.add_reference(ifc_file, information=sheet)
ifcopenshell.api.document.edit_reference(
ifc_file,
reference=reference,
attributes=tool.Drawing.generate_reference_attributes(
reference, Location=tool.Ifc.get_uri(svg, use_relative_path=True), Description="SHEET"
@@ -2150,6 +2178,7 @@ class ActivateDrawingBase(tool.Ifc.Operator):
return self.execute(context)
def _execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
assert context.scene
props = tool.Drawing.get_document_props()
if props.is_editing_drawings == False:
bpy.ops.bim.load_drawings()
@@ -2161,12 +2190,14 @@ class ActivateDrawingBase(tool.Ifc.Operator):
tool.Blender.activate_camera(tool.Drawing.import_temporary_drawing_camera(drawing))
return {"FINISHED"}
viewport_position = None
if not self.should_view_from_camera:
viewport_position = tool.Blender.get_viewport_position()
core.activate_drawing_view(tool.Ifc, tool.Blender, tool.Drawing, drawing=drawing)
if not self.should_view_from_camera:
assert viewport_position
tool.Blender.set_viewport_position(viewport_position)
dprops.active_drawing_id = self.drawing
@@ -2182,6 +2213,7 @@ class ActivateDrawingBase(tool.Ifc.Operator):
# Save drawing bounds to the .ifc file
camera = context.scene.camera
assert camera
camera_props = tool.Drawing.get_camera_props(camera)
if camera_props.update_representation(camera.matrix_world):
bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="")
@@ -2732,6 +2764,7 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Drawing.get_document_props()
active_schedule = props.schedules[props.active_schedule_index]
active_sheet = tool.Drawing.get_active_sheet(context)
ifc_file = tool.Ifc.get()
schedule = tool.Ifc.get().by_id(active_schedule.ifc_definition_id)
if tool.Ifc.get_schema() == "IFC2X3":
schedule_location = tool.Drawing.get_path_with_ext(schedule.DocumentReferences[0].Location, "svg")
@@ -2756,7 +2789,7 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
self.report({"ERROR"}, "The schedule must be generated before adding to a sheet.")
return
reference = tool.Ifc.run("document.add_reference", information=sheet)
reference = ifcopenshell.api.document.add_reference(ifc_file, information=sheet)
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
@@ -2772,7 +2805,7 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
Location=schedule_location,
Description="SCHEDULE",
)
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
ifcopenshell.api.document.edit_reference(ifc_file, reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.add_document(reference, schedule, sheet)
@@ -2799,6 +2832,7 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Drawing.get_document_props()
active_reference = props.references[props.active_reference_index]
active_sheet = tool.Drawing.get_active_sheet(context)
ifc_file = tool.Ifc.get()
extref = tool.Ifc.get().by_id(active_reference.ifc_definition_id)
if tool.Ifc.get_schema() == "IFC2X3":
extref_location = tool.Drawing.get_path_with_ext(extref.DocumentReferences[0].Location, "svg")
@@ -2823,7 +2857,7 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
self.report({"ERROR"}, f"Cannot find reference svg by path {extref_location}.")
return
reference = tool.Ifc.run("document.add_reference", information=sheet)
reference = ifcopenshell.api.document.add_reference(ifc_file, information=sheet)
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
@@ -2839,7 +2873,7 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
Location=extref_location,
Description="REFERENCE",
)
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
ifcopenshell.api.document.edit_reference(ifc_file, reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.add_document(reference, extref, sheet)
@@ -2912,6 +2946,7 @@ class EditTextPopup(bpy.types.Operator):
# need to keep them in sync or move to some common function
# NOTE: that `popup_active_attribute` is used here when it's not used in `BIM_PT_text.draw()`
assert self.layout
obj = context.active_object
assert obj
props = tool.Drawing.get_text_props(obj)
@@ -3186,6 +3221,7 @@ class LoadSheets(bpy.types.Operator, tool.Ifc.Operator):
sheet = tool.Ifc.get().by_id(sheet_prop.ifc_definition_id)
document_uri = tool.Drawing.get_document_uri(sheet)
assert document_uri is not None
filepath = Path(document_uri)
if not filepath.is_file():
@@ -3223,6 +3259,7 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
assert self.layout
props = tool.Drawing.get_document_props()
if self.document_type == "SHEET":
row = self.layout.row()
@@ -3238,6 +3275,7 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.props = tool.Drawing.get_document_props()
ifc_file = tool.Ifc.get()
sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)
if self.document_type == "SHEET":
core.rename_sheet(tool.Ifc, tool.Drawing, sheet=sheet, identification=self.identification, name=self.name)
@@ -3247,8 +3285,9 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
titleblock = self.props.titleblock
reference = sheet
sheet = tool.Drawing.get_reference_document(reference)
tool.Ifc.run(
"document.edit_reference",
assert sheet
ifcopenshell.api.document.edit_reference(
ifc_file,
reference=reference,
attributes={"Location": tool.Drawing.get_default_titleblock_path(titleblock)},
)
@@ -3433,12 +3472,16 @@ class SelectAssignedProduct(bpy.types.Operator, tool.Ifc.Operator):
class EnableEditingElementFilter(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_element_filter"
bl_label = "Enable Editing Element Filter"
bl_description = "Enable editing options for the include or exclude filter"
bl_label = "Element Filter Mode"
bl_options = {"REGISTER", "UNDO"}
filter_mode: bpy.props.StringProperty()
@classmethod
def description(cls, context, properties):
if properties.filter_mode == "NONE":
return "Cancel filter"
return "Enable editing options for the include or exclude filter"
def _execute(self, context):
assert context.scene
obj = context.scene.camera
@@ -3460,6 +3503,7 @@ class EnableEditingElementFilter(bpy.types.Operator, tool.Ifc.Operator):
class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_element_filter"
bl_label = "Edit Element Filter"
bl_description = "Saves the filter"
bl_options = {"REGISTER", "UNDO"}
filter_mode: bpy.props.StringProperty()
@@ -3584,8 +3628,8 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
texture = ifc_file.create_entity("IfcImageTexture", Mode="DIFFUSE", URLReference=image_filepath.as_posix())
textures = [texture]
ifc_file.create_entity("IfcTextureCoordinateGenerator", Maps=textures, Mode="COORD") # UV map
tool.Ifc.run(
"style.add_surface_style",
ifcopenshell.api.style.add_surface_style(
ifc_file,
style=style,
ifc_class="IfcSurfaceStyleWithTextures",
attributes={"Textures": textures},
+20 -11
View File
@@ -46,7 +46,8 @@ from bpy.props import (
CollectionProperty,
BoolVectorProperty,
)
from typing import TYPE_CHECKING, Literal, Any, Callable, get_args, Union
from typing import TYPE_CHECKING, Literal, Any, get_args, Union
from collections.abc import Callable
diagram_scales_enum = []
@@ -118,8 +119,8 @@ def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> No
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
else:
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="EPset_Drawing")
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"IsNTS": self.is_nts})
pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=element, name="EPset_Drawing")
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"IsNTS": self.is_nts})
def get_diagram_scales(self: "BIMCameraProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
@@ -211,6 +212,8 @@ def update_document_name(self: "Document", context: bpy.types.Context) -> None:
def update_has_underlay(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
if not self.update_props:
return
update_layer(self, context, "HasUnderlay", self.has_underlay)
assert context.scene
# making sure that camera is active
@@ -247,8 +250,8 @@ def update_layer(self: "BIMCameraProperties", context: bpy.types.Context, name:
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
else:
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="EPset_Drawing")
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={name: value})
pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=element, name="EPset_Drawing")
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={name: value})
def get_titleblocks(self, context):
@@ -349,7 +352,7 @@ class Sheet(PropertyGroup):
RenderType = Literal["DEFAULT", "VIEWPORT"]
RENDER_TYPE_ENUM_ITEMS: dict[RenderType, tuple[str, str]] = {
"DEFAULT": ("Default", "Use default Blender render."),
"VIEWPORT": ("Viewport", "Render active viewport."),
"VIEWPORT": ("Viewport", "Render active viewport (using 'Rendered' viewport shading type)."),
}
@@ -449,6 +452,11 @@ class DocProperties(PropertyGroup):
name="Classes to Wireframe",
description="Upon import, these classes will display as wireframe.\nEx: IfcVirtualelement, IfcSpace",
)
classes_no_cut: StringProperty(
default="IfcVirtualElement, IfcSpace",
name="Classes that are not cut",
description="The cut decoractor will be turned off for these classes\nEx: IfcVirtualelement, IfcSpace",
)
if TYPE_CHECKING:
should_use_underlay_cache: bool
@@ -490,6 +498,7 @@ class DocProperties(PropertyGroup):
imperial_precision: str
tolerance: float
classes_to_wireframe: str
classes_no_cut: str
def get_active_drawing(self) -> Union[ifcopenshell.entity_instance, None]:
drawing_id = self.active_drawing_id
@@ -522,7 +531,7 @@ class BIMCameraProperties(PropertyGroup):
],
default="OPENCASCADE",
name="Linework Mode",
update=get_update_layer_callback("LineworkMode", "linework_mode"),
update=get_update_layer_callback("linework_mode", "LineworkMode"),
)
fill_mode: EnumProperty(
items=[
@@ -532,7 +541,7 @@ class BIMCameraProperties(PropertyGroup):
],
default="NONE",
name="Fill Mode",
update=get_update_layer_callback("FillMode", "fill_mode"),
update=get_update_layer_callback("fill_mode", "FillMode"),
)
cut_mode: EnumProperty(
items=[
@@ -541,7 +550,7 @@ class BIMCameraProperties(PropertyGroup):
],
default="BISECT",
name="Cut Mode",
update=get_update_layer_callback("CutMode", "cut_mode"),
update=get_update_layer_callback("cut_mode", "CutMode"),
)
# EPset_Drawing.
@@ -574,7 +583,7 @@ class BIMCameraProperties(PropertyGroup):
custom_scale_denominator: bpy.props.StringProperty(default="100", update=update_diagram_scale)
raster_x: IntProperty(name="Raster X", default=1000)
raster_y: IntProperty(name="Raster Y", default=1000)
dpi: IntProperty(name="DPI", default=75, update=get_update_layer_callback("DPI", "dpi"))
dpi: IntProperty(name="DPI", default=75, update=get_update_layer_callback("dpi", "DPI"))
width: FloatProperty(name="Width", default=50, subtype="DISTANCE", update=update_width_height)
height: FloatProperty(name="Height", default=50, subtype="DISTANCE", update=update_width_height)
# Bonsai property is needed to prevent user from using unsupported panoramic camera.
@@ -783,7 +792,7 @@ class BIMTextProperties(PropertyGroup):
def get_text_edited_data(self) -> dict[str, Any]:
"""should be called only if `is_editing`
otherwise should use `DecoratorData.get_ifc_text_data(obj)` instead
otherwise should use `DecoratorData.get_text_data(obj)` instead
because this data could be out of date
"""
literals_data = []
@@ -22,6 +22,8 @@ import bpy
import string
import svgwrite
import openpyxl
import openpyxl.cell # Unnecessary, bug in typeshed.
import openpyxl.utils # Unnecessary, bug in typeshed.
import bonsai.tool as tool
from bonsai.bim.module.drawing.svgwriter import SvgWriter
@@ -521,6 +521,7 @@ class SheetBuilder:
)
titleblock_path = tool.Ifc.resolve_uri(tool.Drawing.get_default_titleblock_path(titleblock_name))
sheet_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
assert sheet_path is not None
sheet_dir = os.path.dirname(sheet_path)
os.makedirs(sheet_dir, exist_ok=True)
@@ -531,9 +532,9 @@ class SheetBuilder:
ET.register_namespace("", "http://www.w3.org/2000/svg")
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
view_root = ET.parse(ootb_titleblock_path).getroot()
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
view_root = ET.parse(titleblock_path).getroot()
view_width = self.convert_to_mm(view_root.attrib["width"])
view_height = self.convert_to_mm(view_root.attrib["height"])
sheet_tree = ET.parse(sheet_path)
root = sheet_tree.getroot()
@@ -26,6 +26,7 @@ from bpy.types import SplineBezierPoints, SplinePoints
import mathutils
import xml.etree.ElementTree as ET
import svgwrite
import svgwrite.container
import svgwrite.text
import ifcopenshell
import ifcopenshell.util.element
@@ -38,7 +39,8 @@ from bonsai.bim.module.drawing.data import DrawingsData
from bonsai.bim.module.drawing.data import DecoratorData
from math import pi, ceil, atan, degrees, acos
from mathutils import geometry, Vector
from typing import Optional, Self, Union, Sequence, Callable
from typing import Optional, Self, Union
from collections.abc import Callable, Sequence
from pathlib import Path
@@ -53,7 +55,7 @@ class External(svgwrite.container.Group):
if elem.tag.startswith(ns):
elem.tag = elem.tag[nsl:]
super(External, self).__init__(**extra)
super().__init__(**extra)
def get_xml(self):
return self.xml
@@ -61,15 +63,28 @@ class External(svgwrite.container.Group):
class SvgWriter:
metadata: list[str]
resource_paths: dict[tool.Drawing.ResourceType, Union[str, None]]
def __init__(self):
def __init__(
self,
*,
camera_width: float,
camera_height: float,
camera: bpy.types.Object,
camera_projection: Vector,
scale: float = 1 / 100, # 1:100
human_scale: str = "NTS",
):
self.data_dir = None
self.human_scale = "NTS"
self.human_scale = human_scale
self.metadata = []
self.scale = 1 / 100 # 1:100
self.camera_width = None
self.camera_height = None
self.scale = scale
self.camera = camera
self.camera_width = camera_width
self.camera_height = camera_height
self.camera_projection = camera_projection
self.resource_paths = {}
self.calculate_scale()
def create_blank_svg(self, output_path: str) -> Self:
self.calculate_scale()
@@ -93,7 +108,7 @@ class SvgWriter:
def setup_drawing_resource_paths(self, element: ifcopenshell.entity_instance) -> None:
pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing")
for resource in ("Stylesheet", "Markers", "Symbols", "Patterns", "ShadingStyles"):
for resource in tool.Drawing.RESOURCE_TYPES:
resource_path = pset.get(resource)
if not resource_path:
self.resource_paths[resource] = None
@@ -1085,7 +1100,7 @@ class SvgWriter:
# When rewriting to use polylines, we should use this normal instead.
# center = tool.Cad.get_center_of_arc([p.to_3d() for p in arc_points], None).to_2d()
# normal = mathutils.geometry.normal([arc_end_pts[0], arc_end_pts[1], center])
# normal = Vector(self.camera_projection)
# normal = self.camera_projection
# dir1 = (arc_end_pts[0] - center).normalized()
# arc_mid_dir = (arc_end_pts[1] - center).normalized()
@@ -1462,9 +1477,9 @@ class SvgWriter:
# TODO is this needlessly complex?
return self.camera.matrix_world.inverted() @ geometry.intersect_line_plane(
point.xyz,
point.xyz - Vector(self.camera_projection),
point.xyz - self.camera_projection,
self.camera.location,
Vector(self.camera_projection),
self.camera_projection,
)
def get_spline_points(self, spline: bpy.types.Spline) -> Union[SplineBezierPoints, SplinePoints]:
+1 -1
View File
@@ -592,7 +592,7 @@ class BIM_PT_text(Panel):
col.label(text=f' {literal_props.attributes["BoxAlignment"].string_value}')
else:
text_data = DecoratorData.get_ifc_text_data(obj)
text_data = DecoratorData.get_text_data(obj)
row = self.layout.row()
row.operator("bim.enable_editing_text", icon="GREASEPENCIL")
@@ -20,6 +20,7 @@
import os
import bpy
import ifcopenshell.util.element
import bonsai.core.geometry
import bonsai.core.type
import bonsai.core.drawing as core
import bonsai.tool as tool
@@ -218,7 +219,7 @@ class AnnotationToolUI:
@classmethod
def draw_edit_object_interface(cls, context):
if DecoratorData.get_ifc_text_data(bpy.context.active_object):
if DecoratorData.get_text_data(bpy.context.active_object):
add_layout_hotkey_operator(cls.layout, "Edit Text", "S_E", "")
@classmethod
@@ -311,7 +312,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if not bpy.context.active_object:
return
if DecoratorData.get_ifc_text_data(bpy.context.active_object):
if DecoratorData.get_text_data(bpy.context.active_object):
bpy.ops.bim.edit_text_popup()
def hotkey_S_G(self):
@@ -27,7 +27,7 @@ from bpy.types import SpaceView3D
from mathutils import Vector, Matrix
from gpu_extras.batch import batch_for_shader
from bpy_extras.view3d_utils import location_3d_to_region_2d
from typing import Sequence
from collections.abc import Sequence
class ItemDecorator:
@@ -22,7 +22,11 @@ import bmesh
import numpy as np
import numpy.typing as npt
import ifcopenshell
import ifcopenshell.api.group
import ifcopenshell.api.pset
import ifcopenshell.api.geometry
import ifcopenshell.api.layer
import ifcopenshell.api.material
import ifcopenshell.api.root
import ifcopenshell.api.style
import ifcopenshell.util.element
@@ -50,7 +54,7 @@ from typing import Any, Union, Literal, get_args, TYPE_CHECKING, assert_never, N
from bonsai.bim.module.model.decorator import ProfileDecorator
if TYPE_CHECKING:
from bpy._typing import rna_enums
from bpy.stub_internal import rna_enums
class EditObjectPlacement(bpy.types.Operator, tool.Ifc.Operator):
@@ -470,13 +474,21 @@ class RemoveRepresentation(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
representation_id: bpy.props.IntProperty()
if TYPE_CHECKING:
representation_id: int
def _execute(self, context):
start_time = time()
assert context.active_object
core.remove_representation(
tool.Ifc,
tool.Geometry,
obj=context.active_object,
representation=tool.Ifc.get().by_id(self.representation_id),
)
operator_time = time() - start_time
if operator_time > 10:
self.report({"INFO"}, f"{self.bl_label} was finished in {operator_time:.2f} seconds.")
class PurgeUnusedRepresentations(bpy.types.Operator, tool.Ifc.Operator):
@@ -583,10 +595,11 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
# We are explicitly casting to a tessellation, so remove all parametric materials.
element_type = ifcopenshell.util.element.get_type(product)
if element_type: # Some invalid IFCs use material sets without a type.
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), products=[element_type])
ifcopenshell.api.material.unassign_material(self.file, products=[element_type])
tool.Material.ensure_material_unassigned([element_type])
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), products=[product])
ifcopenshell.api.material.unassign_material(self.file, products=[product])
tool.Material.ensure_material_unassigned([product])
data = obj.data # Required after ensure_material_unassigned
else:
# These objects are parametrically based on an axis and should not be modified as a mesh
if extrusion := tool.Model.get_extrusion(old_representation):
@@ -603,6 +616,7 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
# added this as a fallback for easier transition some annotation types to 3d
# if they were create before as 2d
element = tool.Ifc.get_entity(obj)
assert element
if tool.Drawing.is_annotation_object_type(element, ("FALL", "SECTION_LEVEL", "PLAN_LEVEL")):
context_of_items = tool.Drawing.get_annotation_context("MODEL_VIEW")
@@ -626,7 +640,7 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
representation_data["text_literal"] = tool.Geometry.get_text_literal(old_representation)
# TODO: replace with core.add_representation?
new_representation = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data)
new_representation = ifcopenshell.api.geometry.add_representation(self.file, **representation_data)
if new_representation is None:
self.report({"ERROR"}, "Error creating representation for Blender object.")
return {"CANCELLED"}
@@ -720,7 +734,7 @@ class GetRepresentationIfcParameters(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = context.active_object
assert obj and tool.Geometry.has_mesh_properties((data := obj.data))
assert obj and tool.Geometry.has_mesh_properties(data := obj.data)
core.get_representation_ifc_parameters(tool.Geometry, obj=obj)
parameters = tool.Geometry.get_mesh_props(data).ifc_parameters
self.report({"INFO"}, f"{len(parameters)} parameters found.")
@@ -750,10 +764,10 @@ class CopyRepresentation(bpy.types.Operator, tool.Ifc.Operator):
bm.to_mesh(obj.data)
old_rep = tool.Geometry.get_representation_by_context(element, geometric_context)
if old_rep:
ifcopenshell.api.run(
"geometry.unassign_representation", tool.Ifc.get(), product=element, representation=old_rep
ifcopenshell.api.geometry.unassign_representation(
tool.Ifc.get(), product=element, representation=old_rep
)
ifcopenshell.api.run("geometry.remove_representation", tool.Ifc.get(), representation=old_rep)
ifcopenshell.api.geometry.remove_representation(tool.Ifc.get(), representation=old_rep)
core.add_representation(
tool.Ifc,
tool.Geometry,
@@ -996,14 +1010,16 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
selected_ids_data = self.get_selected_ids_data(context)
try:
with context.temp_override(selected_objects=list(selected_ids_data.objects)):
bpy.ops.bim.override_object_delete(is_batch=self.is_batch)
except RuntimeError as e:
error_reports = tool.Blender.extract_error_reports(e)
if not error_reports:
raise
tool.Blender.report_operator_errors(self, error_reports)
if selected_ids_data.objects:
try:
with context.temp_override(selected_objects=list(selected_ids_data.objects)):
bpy.ops.bim.override_object_delete(is_batch=self.is_batch)
except RuntimeError as e:
error_reports = tool.Blender.extract_error_reports(e)
if not error_reports:
raise
tool.Blender.report_operator_errors(self, error_reports)
for collection in selected_ids_data.collections:
# Removing an aggregate object would also remove it's collection
@@ -1177,10 +1193,9 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
if not pset:
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name)
pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=part, name=self.pset_name)
ifcopenshell.api.run(
"pset.edit_pset",
ifcopenshell.api.pset.edit_pset(
tool.Ifc.get(),
pset=pset,
properties={"Index": index},
@@ -1202,18 +1217,18 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
if self.group_name in product_groups_name:
return
linked_aggregate_group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), name=self.group_name)
ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), products=[element], group=linked_aggregate_group)
linked_aggregate_group = ifcopenshell.api.group.add_group(tool.Ifc.get(), name=self.group_name)
ifcopenshell.api.group.assign_group(tool.Ifc.get(), products=[element], group=linked_aggregate_group)
def custom_incremental_naming_for_element_assembly(old_to_new):
for new in old_to_new.values():
if new[0].is_a("IfcElementAssembly"):
group_elements = [
group_elements: list[ifcopenshell.entity_instance] = next(
r.RelatedObjects
for r in getattr(new[0], "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
][0]
)
number = len(group_elements) - 1
number = f"{number:02d}"
@@ -1241,12 +1256,9 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
for old, new in old_to_new.items():
pset = ifcopenshell.util.element.get_pset(old, "BBIM_Linked_Aggregate")
if pset:
new_pset = ifcopenshell.api.run(
"pset.add_pset", tool.Ifc.get(), product=new[0], name=self.pset_name
)
new_pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=new[0], name=self.pset_name)
ifcopenshell.api.run(
"pset.edit_pset",
ifcopenshell.api.pset.edit_pset(
tool.Ifc.get(),
pset=new_pset,
properties={"Index": pset["Index"]},
@@ -1353,12 +1365,12 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry.delete_ifc_object(tool.Ifc.get_object(element))
def get_original_names(element: ifcopenshell.entity_instance) -> dict[int, dict[int, str]]:
group = [
group = next(
r.RelatingGroup
for r in getattr(element, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if self.group_name in r.RelatingGroup.Name
][0].id()
).id()
original_names[group] = {}
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
@@ -1494,7 +1506,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
for group in linked_aggregate_groups:
elements = tool.Drawing.get_group_elements(tool.Ifc.get().by_id(group))
if len(linked_aggregate_groups) > 1:
base_instance = [e for e in elements if e in selected_parents][0]
base_instance = next(e for e in elements if e in selected_parents)
instances_to_refresh = elements
elif (len(linked_aggregate_groups) == 1) and (len(selected_parents) > 1):
@@ -1558,8 +1570,12 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
bl_description = (
blender_op.description
+ ".\nAlso makes sure changes are in sync with IFC."
+ "\nIf IFC object is joined into non-IFC object, it will be removed from IFC."
+ "\nJoining representation items only supported for joining with other representation items of the same representation."
+ "\n\nJoining IFC object into another IFC object/Blender object will "
+ "only merge their current representations and will remove joined object from IFC. "
+ "Representation items are ignored for this case."
+ "\n\nJoining representation items only supported for joining with other representation items "
+ "of the same representation (only mesh-like items supported currently) "
+ "or with non-IFC Blender objects."
)
bl_options = {"REGISTER", "UNDO"}
@@ -1642,6 +1658,11 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry.reload_representation(rep_obj)
context.view_layer.update()
tool.Root.reload_item_decorator()
else:
self.report(
{"WARNING"},
f"Unsupported type of item to join: {item}. Currently only mesh-like items are supported.",
)
def join_ifc_obj(self, context: bpy.types.Context) -> None:
ifc_file = tool.Ifc.get()
@@ -1687,7 +1708,7 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
if obj == self.target:
continue
if obj.type != self.target_type:
obj.select_set((False))
obj.select_set(False)
continue
element = tool.Ifc.get_entity(obj)
@@ -1748,7 +1769,7 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
copied_item = ifcopenshell.util.element.copy_deep(
ifc_file, item, exclude=("IfcCartesianPointList",)
)
new_points = processed_point_lists.get((points := item.Points))
new_points = processed_point_lists.get(points := item.Points)
if new_points is None:
new_points = ifcopenshell.util.element.copy_deep(ifc_file, points)
dim = item.Dim
@@ -2186,7 +2207,9 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
item = tool.Geometry.get_active_representation(obj)
assert item
if tool.Geometry.is_meshlike_item(item):
if tool.Geometry.is_geometric_data(obj.data) and obj.data.polygons:
if tool.Geometry.is_geometric_data(obj.data) and (
item.is_a("IfcVertex") or item.is_a("IfcEdge") or obj.data.polygons
):
tool.Geometry.edit_meshlike_item(obj)
else:
tool.Geometry.import_item(obj)
@@ -2229,8 +2252,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
curves.append(profile.OuterCurve)
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
curves.extend(profile.InnerCurves)
new_footprint = ifcopenshell.api.run(
"geometry.add_footprint_representation",
new_footprint = ifcopenshell.api.geometry.add_footprint_representation(
tool.Ifc.get(),
context=footprint_context,
curves=curves,
@@ -2245,8 +2267,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
tool.Ifc, tool.Geometry, obj=obj, representation=old_footprint
)
else:
ifcopenshell.api.run(
"geometry.assign_representation",
ifcopenshell.api.geometry.assign_representation(
tool.Ifc.get(),
product=element,
representation=new_footprint,
@@ -2359,7 +2380,7 @@ class EnableEditingRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
item.tags += ","
item.tags += tag
if tool.Geometry.has_mesh_properties((data := obj.data)):
if tool.Geometry.has_mesh_properties(data := obj.data):
representation = tool.Geometry.get_data_representation(data)
assert representation
@@ -2552,7 +2573,8 @@ class EnableEditingRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator
ifc_file = tool.Ifc.get()
# set dropdown to currently active style
representation_item_id = props.active_item.ifc_definition_id
assert (active_item := props.active_item)
representation_item_id = active_item.ifc_definition_id
representation_item = ifc_file.by_id(representation_item_id)
style = tool.Style.get_representation_item_style(representation_item)
if style:
@@ -2571,8 +2593,14 @@ class EditRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator):
props.is_editing_item_style = False
ifc_file = tool.Ifc.get()
surface_style = ifc_file.by_id(int(props.representation_item_style))
representation_item_id = props.active_item.ifc_definition_id
surface_style_id = tool.Blender.get_enum_safe(props, "representation_item_style")
if surface_style_id in (None, "-"):
surface_style = None
else:
surface_style = ifc_file.by_id(int(props.representation_item_style))
assert (active_item := props.active_item)
representation_item_id = active_item.ifc_definition_id
representation_item = ifc_file.by_id(representation_item_id)
tool.Style.assign_style_to_representation_item(representation_item, surface_style)
@@ -32,7 +32,6 @@ classes = (
operator.SetGroupVisibility,
operator.ToggleGroup,
operator.UnassignGroup,
prop.ExpandedGroups,
prop.Group,
prop.BIMGroupProperties,
ui.BIM_PT_groups,
@@ -43,9 +42,7 @@ classes = (
def register():
bpy.types.Scene.BIMGroupProperties = bpy.props.PointerProperty(type=prop.BIMGroupProperties)
bpy.types.Scene.ExpandedGroups = bpy.props.PointerProperty(type=prop.ExpandedGroups)
def unregister():
del bpy.types.Scene.BIMGroupProperties
del bpy.types.Scene.ExpandedGroups
+19 -16
View File
@@ -19,7 +19,7 @@
import bpy
import ifcopenshell.api
import ifcopenshell.api.group
import ifcopenshell.util.attribute
import ifcopenshell.util.element
import bonsai.bim.helper
import bonsai.tool as tool
import json
@@ -31,8 +31,8 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
self.props = context.scene.BIMGroupProperties
self.expanded_groups = json.loads(context.scene.ExpandedGroups.json_string)
self.props = tool.Blender.get_group_props()
self.expanded_groups = json.loads(self.props.expanded_groups_json)
self.props.groups.clear()
groups = [
@@ -79,12 +79,13 @@ class ToggleGroup(bpy.types.Operator, tool.Ifc.Operator):
option: bpy.props.StringProperty(name="Expand or Collapse")
def _execute(self, context):
expanded_groups = set(json.loads(context.scene.ExpandedGroups.json_string))
props = tool.Blender.get_group_props()
expanded_groups = set(json.loads(props.expanded_groups_json))
if self.option == "Expand":
expanded_groups.add(self.ifc_definition_id)
elif self.ifc_definition_id in expanded_groups:
expanded_groups.remove(self.ifc_definition_id)
context.scene.ExpandedGroups.json_string = json.dumps(list(expanded_groups))
props.expanded_groups_json = json.dumps(list(expanded_groups))
bpy.ops.bim.load_groups()
return {"FINISHED"}
@@ -95,8 +96,9 @@ class DisableGroupEditingUI(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
context.scene.BIMGroupProperties.is_editing = False
context.scene.BIMGroupProperties.active_group_id = 0
props = tool.Blender.get_group_props()
props.is_editing = False
props.active_group_id = 0
return {"FINISHED"}
@@ -107,10 +109,10 @@ class AddGroup(bpy.types.Operator, tool.Ifc.Operator):
group: bpy.props.IntProperty()
def _execute(self, context):
result = ifcopenshell.api.run("group.add_group", tool.Ifc.get())
result = ifcopenshell.api.group.add_group(tool.Ifc.get())
if self.group:
ifcopenshell.api.run(
"group.assign_group", tool.Ifc.get(), products=[result], group=tool.Ifc.get().by_id(self.group)
ifcopenshell.api.group.assign_group(
tool.Ifc.get(), products=[result], group=tool.Ifc.get().by_id(self.group)
)
bpy.ops.bim.load_groups()
return {"FINISHED"}
@@ -122,7 +124,7 @@ class EditGroup(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.scene.BIMGroupProperties
props = tool.Blender.get_group_props()
attributes = bonsai.bim.helper.export_attributes(props.group_attributes)
ifc_file = tool.Ifc.get()
ifcopenshell.api.group.edit_group(ifc_file, group=ifc_file.by_id(props.active_group_id), attributes=attributes)
@@ -138,7 +140,7 @@ class RemoveGroup(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.file = tool.Ifc.get()
ifcopenshell.api.run("group.remove_group", self.file, **{"group": self.file.by_id(self.group)})
ifcopenshell.api.group.remove_group(self.file, group=self.file.by_id(self.group))
bpy.ops.bim.load_groups()
return {"FINISHED"}
@@ -150,7 +152,7 @@ class EnableEditingGroup(bpy.types.Operator, tool.Ifc.Operator):
group: bpy.props.IntProperty()
def _execute(self, context):
props = context.scene.BIMGroupProperties
props = tool.Blender.get_group_props()
props.group_attributes.clear()
bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.group), props.group_attributes)
props.active_group_id = self.group
@@ -163,7 +165,8 @@ class DisableEditingGroup(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
context.scene.BIMGroupProperties.active_group_id = 0
props = tool.Blender.get_group_props()
props.active_group_id = 0
return {"FINISHED"}
@@ -183,9 +186,9 @@ class AssignGroup(bpy.types.Operator, tool.Ifc.Operator):
if not self.is_assigning:
return bpy.ops.bim.unassign_group(group=self.group)
products = [
tool.Ifc.get_entity(o)
element
for o in tool.Blender.get_selected_objects(include_active=False)
if tool.Ifc.get_entity(o)
if (element := tool.Ifc.get_entity(o))
]
ifcopenshell.api.group.assign_group(tool.Ifc.get(), products=products, group=tool.Ifc.get().by_id(self.group))
+20 -8
View File
@@ -17,10 +17,10 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
from bonsai.bim.prop import StrProperty, Attribute
from bonsai.bim.module.pset.data import refresh as refresh_pset
from bpy.types import PropertyGroup
import json
from bpy.props import (
PointerProperty,
StringProperty,
@@ -31,16 +31,13 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Union
def update_active_group_index(self, context):
refresh_pset()
class ExpandedGroups(StrProperty):
json_string: StringProperty(name="JSON String", default="[]")
class Group(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
@@ -48,6 +45,13 @@ class Group(PropertyGroup):
has_children: BoolProperty(name="Has Children", default=False)
tree_depth: IntProperty(name="Tree Depth")
if TYPE_CHECKING:
name: str
ifc_definition_id: int
is_expanded: bool
has_children: bool
tree_depth: int
class BIMGroupProperties(PropertyGroup):
group_attributes: CollectionProperty(name="Group Attributes", type=Attribute)
@@ -55,8 +59,16 @@ class BIMGroupProperties(PropertyGroup):
groups: CollectionProperty(name="Groups", type=Group)
active_group_index: IntProperty(name="Active Group Index", update=update_active_group_index)
active_group_id: IntProperty(name="Active Group Id")
expanded_groups_json: StringProperty(name="JSON String", default="[]")
if TYPE_CHECKING:
group_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
is_editing: bool
groups: bpy.types.bpy_prop_collection_idprop[Group]
active_group_index: int
active_group_id: int
expanded_groups_json: str
@property
def active_group(self):
if self.active_group_index < len(self.groups):
return self.groups[self.active_group_index]
def active_group(self) -> Union[Group, None]:
return tool.Blender.get_active_uilist_element(self.groups, self.active_group_index)
+19 -3
View File
@@ -16,11 +16,16 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import bonsai.tool as tool
from bpy.types import Panel, UIList
from bonsai.bim.helper import draw_attributes
from bonsai.bim.module.group.data import GroupsData, ObjectGroupsData
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.group.prop import BIMGroupProperties, Group
class BIM_PT_groups(Panel):
@@ -39,7 +44,7 @@ class BIM_PT_groups(Panel):
def draw(self, context):
if not GroupsData.is_loaded:
GroupsData.load()
self.props = context.scene.BIMGroupProperties
self.props = tool.Blender.get_group_props()
row = self.layout.row(align=True)
row.label(text=f"{GroupsData.data['total_groups']} Groups Found", icon="OUTLINER")
@@ -99,7 +104,8 @@ class BIM_PT_object_groups(Panel):
def draw(self, context):
if not ObjectGroupsData.is_loaded:
ObjectGroupsData.load()
self.props = context.scene.BIMGroupProperties
assert self.layout
self.props = tool.Blender.get_group_props()
for group in ObjectGroupsData.data["groups"]:
row = self.layout.row(align=True)
@@ -113,7 +119,17 @@ class BIM_PT_object_groups(Panel):
class BIM_UL_groups(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
def draw_item(
self,
context: bpy.types.Context,
layout: bpy.types.UILayout,
data: BIMGroupProperties,
item: Group,
icon,
active_data,
active_propname,
index: int,
):
if item:
row = layout.row(align=True)
for i in range(0, item.tree_depth):
+2 -2
View File
@@ -153,7 +153,7 @@ class IfcGitData:
@classmethod
def commit(cls):
props = bpy.context.scene.IfcGitProperties
props = tool.IfcGit.get_ifcgit_props()
if cls.repo() and len(props.ifcgit_commits) > 0:
item = props.ifcgit_commits[props.commit_index]
try:
@@ -163,7 +163,7 @@ class IfcGitData:
@classmethod
def current_revision(cls):
props = bpy.context.scene.IfcGitProperties
props = tool.IfcGit.get_ifcgit_props()
if cls.repo() and cls.repo().head.is_valid() and len(props.ifcgit_commits) > 0:
return tool.IfcGitRepo.repo.commit()
+10 -12
View File
@@ -68,7 +68,7 @@ class CloneRepo(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = context.scene.IfcGitProperties
props = tool.IfcGit.get_ifcgit_props()
if (
props.remote_url
and props.local_folder
@@ -80,7 +80,7 @@ class CloneRepo(bpy.types.Operator):
def execute(self, context):
props = context.scene.IfcGitProperties
props = tool.IfcGit.get_ifcgit_props()
core.clone_repo(tool.IfcGit, props.remote_url, props.local_folder, self)
props.remote_url = ""
refresh()
@@ -112,7 +112,7 @@ class CommitChanges(bpy.types.Operator):
@classmethod
def poll(cls, context):
IfcGitData.make_sure_is_loaded()
props = context.scene.IfcGitProperties
props = tool.IfcGit.get_ifcgit_props()
repo = IfcGitData.data["repo"]
if props.commit_message == "":
return False
@@ -148,7 +148,7 @@ class AddTag(bpy.types.Operator):
@classmethod
def poll(cls, context):
IfcGitData.make_sure_is_loaded()
props = context.scene.IfcGitProperties
props = tool.IfcGit.get_ifcgit_props()
if props.new_tag_name == "":
return False
repo = IfcGitData.data["repo"]
@@ -218,7 +218,7 @@ class DisplayRevision(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = context.scene.IfcGitProperties
props = tool.IfcGit.get_ifcgit_props()
if props.ifcgit_commits:
return True
@@ -253,7 +253,7 @@ class SwitchRevision(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = context.scene.IfcGitProperties
props = tool.IfcGit.get_ifcgit_props()
if props.ifcgit_commits:
return True
@@ -274,7 +274,7 @@ class Merge(bpy.types.Operator):
@classmethod
def poll(cls, context):
IfcGitData.make_sure_is_loaded()
props = context.scene.IfcGitProperties
props = tool.IfcGit.get_ifcgit_props()
if IfcGitData.data["ifcmerge_exe"] and props.ifcgit_commits and not IfcGitData.data["is_detached"]:
return True
return False
@@ -296,8 +296,7 @@ class Push(bpy.types.Operator):
bl_options = {"REGISTER"}
def execute(self, context):
props = context.scene.IfcGitProperties
props = tool.IfcGit.get_ifcgit_props()
repo = IfcGitData.data["repo"]
core.push(tool.IfcGit, repo, props.select_remote, self)
return {"FINISHED"}
@@ -311,8 +310,7 @@ class Fetch(bpy.types.Operator):
bl_options = {"REGISTER"}
def execute(self, context):
props = context.scene.IfcGitProperties
props = tool.IfcGit.get_ifcgit_props()
repo = IfcGitData.data["repo"]
remote = repo.remotes[props.select_remote]
remote.fetch()
@@ -329,7 +327,7 @@ class AddRemote(bpy.types.Operator):
@classmethod
def poll(cls, context):
IfcGitData.make_sure_is_loaded()
props = context.scene.IfcGitProperties
props = tool.IfcGit.get_ifcgit_props()
repo = IfcGitData.data["repo"]
if (
not repo
+32 -9
View File
@@ -1,4 +1,5 @@
import bpy
import bonsai.tool as tool
from bpy.types import PropertyGroup
from bpy.props import (
StringProperty,
@@ -8,11 +9,10 @@ from bpy.props import (
EnumProperty,
)
from bonsai.bim.module.ifcgit.data import IfcGitData
from typing import TYPE_CHECKING, Literal
def git_branches(self, context):
"""branches enum"""
def git_branches(self: "IfcGitProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
# NOTE "Python must keep a reference to the strings returned by
# the callback or Blender will misbehave or even crash"
IfcGitData.data["branch_names"] = sorted([branch.name for branch in IfcGitData.data["repo"].heads])
@@ -29,9 +29,7 @@ def git_branches(self, context):
return [(myname, myname, myname) for myname in IfcGitData.data["branch_names"]]
def git_remotes(self, context):
"""remotes enum"""
def git_remotes(self: "IfcGitProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
IfcGitData.data["remote_names"] = sorted([remote.name for remote in IfcGitData.data["remotes"]])
if "origin" in IfcGitData.data["remote_names"]:
@@ -41,12 +39,11 @@ def git_remotes(self, context):
return [(myname, myname, myname) for myname in IfcGitData.data["remote_names"]]
def update_revlist(self, context):
def update_revlist(self: "IfcGitProperties", context: bpy.types.Context) -> None:
"""wrapper to trigger update of the revision list"""
bpy.ops.ifcgit.refresh()
props = context.scene.IfcGitProperties
props.commit_index = 0
self.commit_index = 0
class IfcGitTag(PropertyGroup):
@@ -61,6 +58,10 @@ class IfcGitTag(PropertyGroup):
default="",
)
if TYPE_CHECKING:
name: str
message: str
class IfcGitListItem(PropertyGroup):
"""Group of properties representing an item in the list."""
@@ -89,6 +90,14 @@ class IfcGitListItem(PropertyGroup):
)
tags: CollectionProperty(type=IfcGitTag, name="List of revision tags")
if TYPE_CHECKING:
hexsha: str
relevant: bool
author_name: str
author_email: str
message: str
tags: bpy.types.bpy_prop_collection_idprop[IfcGitTag]
class IfcGitProperties(PropertyGroup):
@@ -140,3 +149,17 @@ class IfcGitProperties(PropertyGroup):
],
update=update_revlist,
)
if TYPE_CHECKING:
ifcgit_commits: bpy.types.bpy_prop_collection_idprop[IfcGitListItem]
commit_index: int
commit_message: str
new_branch_name: str
new_tag_name: str
new_tag_message: str
remote_name: str
remote_url: str
local_folder: str
display_branch: str
select_remote: str
ifcgit_filter: Literal["all", "tagged", "relevant"]

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