Compare commits

..

151 Commits

Author SHA1 Message Date
Andrej730 f14b4c467d Revert "Bump IOS" #6909
This reverts commit a4dd1c3463.
2025-07-16 16:36:16 +05:00
Dion Moult 4a9cf433d1 Fix bug in IfcTester thanks to Sayan where parsing XML text using ET would expand namespaces and therefore break namespaces during decode 2025-07-16 18:14:45 +10:00
Andrej730 866362c6ed More readable error creating a drawing with underlay but no active style #4870 2025-07-16 12:31:10 +05:00
Andrej730 0dc17a2141 Revert optional pyradiance 41e6478 #6633
Since issue is resolved upstream and there's no need to keep import optional.
2025-07-16 11:22:45 +05:00
Andrej730 d55e7faf2a Fix ifc4x3 breaking import of csvs that have enum values after c9847ca #6708 2025-07-15 18:01:57 +05:00
Andrej730 52762f828c Save all properties from Preferences UI as actual preferences
Previously some of them were saved only for current .blend file, moving them to preferences will make it more consistent with usual Blender UX. If needed we'll be able to add some way to fine-grain them later.

There's also a temporary patch that's going to migrate old .blend-props to new preferences-props to make process less disruptive.
2025-07-15 17:47:38 +05:00
Andrej730 1d0868eb18 bim.attribute_search_values - enable by default if possible
Since release just happened, it's more safe to try out propagating attribute search feature everywhere.
2025-07-15 17:47:37 +05:00
Andrej730 9461c6369e ci-bonsai-daily - use Blender 4.5 build 2025-07-15 17:47:37 +05:00
Andrej730 0f3b570800 Fix get_segment_length bug with non-parametrized profiles (f9c4b6a)
`settings` wasn't defined, so `create_shape` was always resulting in an error. Also, `create_shape` is returning `Triangulation` in this kind of cases, so there's no `.geometry`.

Also removed IfcCircleProfileDef if-check since it's covered by IfcParameterizedProfileDef.
2025-07-15 17:47:37 +05:00
Andrej730 30cbfad871 Fix code typo in 40f92d1 2025-07-15 17:47:37 +05:00
Andrej730 262739f37a owner.remove_role - fix error handling resource level rel (fe1e307)
It's probably was just an overlook in the original commit - it was referring to `settings["organisation"]`, though `"organization"` was never passed, leading to an error. And probably by accident remove_role tests were using the same tests as remove_organization, so the issue went under the radar for a while.
2025-07-15 17:47:37 +05:00
Andrej730 03fa850ce2 Fix old bug in FixArchiCADToRevitDoorSwings (1d74306)
It was using variable `f` that was never defined
2025-07-15 17:47:36 +05:00
Andrej730 8d2c0d0785 Fix old error setting up bcf camera in some cases (0d6cacd)
`cam_height` and `cam_width` were used in `setup_camera()` but never defined.
2025-07-15 17:47:36 +05:00
Andrej730 742a9df311 Fix error starting import_ifc profiling (in 515bfd1) 2025-07-15 17:47:36 +05:00
Andrej730 c95a0e8ed5 Fix error in generating error message in 8e9d4be
`profile` variable was never defined in this context
2025-07-15 17:47:36 +05:00
Andrej730 a28478c193 Fix bim.align_product operator (after 2db6b86)
It was using `core` in it's code though it was never defined.
2025-07-15 17:47:36 +05:00
Andrej730 b521901c90 Fix error message when filtering more than 50 selected objects (after c4ffc06)
name variable was undefined leading to an error
2025-07-15 17:47:35 +05:00
Andrej730 20f8737fb3 generate_furniture_library - remove unused methods
We have them in demo library either way
2025-07-15 17:47:35 +05:00
Andrej730 f18c93c9ee typing 2025-07-15 17:47:35 +05:00
Richard Brice a461281b1e Fixes problem moving alignment representation from parent to child alignment. Forgot to assign the ObjectPlacement 2025-07-14 11:09:02 -07:00
Andrej730 2d3d747d49 Move props from 'Other' preferences tab to preferences #6733
To prevent users confusion since it's a general Blender UX to expect here general preferences, not per-blend file settings.
2025-07-14 18:53:11 +05:00
Andrej730 d165cd2692 Update material classification UI on changing selected materials #6902 2025-07-14 18:53:11 +05:00
Andrej730 3f0e30245b Fix TestSelectSimilarContainer after 553003e 2025-07-14 18:53:11 +05:00
Andrej730 c111aafd3a Fix TestAddContext after 2698ba6 2025-07-14 18:53:11 +05:00
Andrej730 465bd23d6f import_attributes2 -> import_attributes
import_attributes were removed in 76f0b57ff
2025-07-14 18:53:10 +05:00
Andrej730 c7d3e00dca typing 2025-07-14 18:53:10 +05:00
Andrej730 72af88a4b3 black . 2025-07-14 18:53:10 +05:00
Richard Brice 73b76ba666 Adds IfcPolyline representation for alignment. Also some general cleanup 2025-07-13 16:34:52 -07:00
Dion Moult e93a66be49 Update VERSION 2025-07-13 20:25:14 +10:00
Dion Moult a4dd1c3463 Bump IOS 2025-07-13 19:55:21 +10:00
Andrej730 6d8e513f97 Temporary hack for path tags without 'd' attr #6871 #6876 2025-07-11 18:24:00 +05:00
Andrej730 baeaa073df dimensionality - add description 2025-07-11 18:24:00 +05:00
Andrej730 f07391181d context-ids, context-types, context-identifiers - add descriptions
Hope I don't miss anything, but couldn't find any references to ContextTypes, ContextIdentifiers and they seem to have no effect.
2025-07-11 18:23:59 +05:00
Andrej730 722f71b3b3 ifcgeom/ConversionSettings.h - fix duplicated ContextIds derived type
I guess it only affected how `defineOption` worked. Noticed the issue when was looking through `ifcconvert --help` and `context-ids` were duplicated 3 times.
2025-07-11 18:23:59 +05:00
Andrej730 76e4ec0069 bim.select_uri_attribute - simplify data_path eval 2025-07-11 18:23:59 +05:00
Andrej730 df18124bec vs-cfg.cmd - update error message
To make it a bit more easier to understand what to do in cases like below (the solution is to provide generator explicitly: `run-cmake.bat vs2022-x64 -DADD_COMMIT_SHA=ON -DVERSION_OVERRIDE=ON`).

```
run-cmake.bat -DADD_COMMIT_SHA=ON -DVERSION_OVERRIDE=ON

vs-cfg.cmd: The used CMake version does not support generator `-DADD_COMMIT_SHA` - cannot proceed.
```
2025-07-11 18:23:59 +05:00
Andrej730 370488cd27 docs conf.py - update year 2025-07-11 18:23:59 +05:00
Andrej730 4bbc463c8f ifcopenshell-python\docs\requirements.txt 2025-07-11 18:23:59 +05:00
Andrej730 4a11a117ad Fix bim.activate_model hiding all elements #6893
It was an overlook with using by accident bim.disable_status_filters instead of bim.enable_status_filters in bim.activate_status_filters, but it was also kind of unexpectedly introduced in 7858cb1.

But thinking about it, bim.activate_model shouldn't activate Statuses UI either way, since it might bring their filters they had before but explicitly disabled.
2025-07-11 18:23:58 +05:00
Andrej730 19be8107b3 Fix Ruff UP037 (quoted-annotation) 2025-07-11 18:23:58 +05:00
Andrej730 e1976d5bfd Fix Ruff UP013 (convert-typed-dict-functional-to-class)
Apparently `type` is okay and doesn't break things in even in Python 3.12
2025-07-11 18:23:58 +05:00
Andrej730 0fcc47508a Structural UI - refactor common code to tool, fix bugs
Also revert previous change in UI - `draw_attributes` doesn't cover it and there indeed should be special handling.
2025-07-10 19:42:31 +05:00
Andrej730 76f0b57ff1 Migrate to import_attributes2 2025-07-10 19:42:30 +05:00
Andrej730 1d8ee406af Fix alignment.__all__ missing create_segment_representations 2025-07-10 19:42:30 +05:00
Andrej730 05cb06a28e typing 2025-07-10 19:42:30 +05:00
Andrej730 8a6eb1b60f Update IfcApplication attributes export after 5866994 2025-07-10 19:42:30 +05:00
Andrej730 37d4ee97e7 alignment - fix missing imports 2025-07-10 19:42:29 +05:00
Andrej730 ef5d1448da Fix is_null not updating when adding new attribute subitems #6869 2025-07-10 19:42:29 +05:00
Massimo Fabbro eed42b8fac Fix #6711. Forgot to change the space elevation 2025-07-09 22:27:31 +02:00
Thomas Krijnen 1f0ac8fdb9 submodule 2025-07-09 20:45:39 +02:00
Andrej730 1b1a1f143a bim.attribute_search_values - enable it in more places
Since this feature can be generally useful, let's try it out to see the cases when it should be disabled.

Additionally to Materials UI, it's now enabled in object attributes, materials, profiles, styles and explorer ui.
2025-07-09 19:02:27 +05:00
Andrej730 9649c23a3b bim.attribute_search_values - more clear description 2025-07-09 19:02:27 +05:00
Andrej730 0ad5ace35f bim.attribute_search_values - support not just scene props 2025-07-09 19:02:27 +05:00
Andrej730 f7fdb8cc81 bim.attribute_search_values - rearrange props for readability 2025-07-09 19:02:27 +05:00
Andrej730 41e64781d8 Skip pyradiance import if it fails #6633 2025-07-09 19:02:27 +05:00
Andrej730 6ea7ee84a6 Remove prop.GlobalId
It seems it was unused since 6f55cd5
2025-07-09 19:02:27 +05:00
Andrej730 e7b86ae5b0 bim.add_group - use dynamic description to clarify when it's adding subgroups
It was kind of confusing since the icon and everything else was the same.
2025-07-09 19:02:27 +05:00
Andrej730 f6035609c9 Groups UI - use natsort 2025-07-09 19:02:27 +05:00
Andrej730 76feeaccc9 typing 2025-07-09 19:02:26 +05:00
Andrej730 af02a6b3a6 Fix UP037 (quoted-annotation) 2025-07-09 19:02:26 +05:00
Andrej730 832e68cb6f Fix Ruff UP034 (extraneous-parentheses) 2025-07-09 19:02:26 +05:00
Andrej730 a8268e8323 Replace deprecated typing.Sequence (40f92d15c) 2025-07-09 19:02:26 +05:00
Andrej730 5a4ad69bf7 black . 2025-07-09 19:02:26 +05:00
Richard Brice 40f92d15c3 Alignment api update (#6817)
* Some basic COGO survey points functions

* Update alignment api

Includes defining alignment segment by segment, automatic geometry definitions, and automatric stationing and referents

* Fixes bonsai import alignment from csv

* Adds DMS angle conversion functions to COGO api

* Updated per @civilx64 review comments

* Fixes problem with segment representations

* Fixes problem with segment transition codes

* Allows for compound vertical and horizontal curves

* Implements callbacks for referent naming

* Renames angle_from_bearing to bearing2dd for consistency with ifcopenshell.util.geolocation.dms2dd. Removes angle_from_dms because it duplicates dms2dd

* Documents register_referent_name_callback

* Fixes all sorts of problems with Cant/SegRefCurve implementation

* refactor referent unit tests to use a fixture for test setup

* lint with black

---------

Co-authored-by: Scott Lecher <civilx64@gmail.com>
2025-07-08 10:09:14 -07:00
Andrej730 ad8d02bfed Groups UI - support renaming groups directly from UI
Example - https://files.catbox.moe/u4b918.mp4
2025-07-08 21:20:59 +05:00
Andrej730 5866994b84 Intergrate explorer UI to support editing some props like #6869
Example - https://files.catbox.moe/ujhlwv.mp4
2025-07-08 21:20:59 +05:00
Andrej730 2ec2ac0e5c Attributes UI to support attributes with string subitems
Example - https://files.catbox.moe/oew1ue.mp4
2025-07-08 21:20:58 +05:00
Andrej730 c7555652d7 Bulk Rename Pset Props - show how many props were affected 2025-07-08 21:20:58 +05:00
Andrej730 5a7ce6d68b bim.bulk_remove_psets - show how many psets were removed
Also removed exception handling to catch that bug sneaking in, if it ever returns.
2025-07-08 21:20:58 +05:00
Andrej730 ad8913a979 Move bulk properties to GlobalPsetProperties 2025-07-08 21:20:58 +05:00
Andrej730 188b6a03a8 Attribute.is_uri - move to Attribute.special_type 2025-07-08 21:20:57 +05:00
Andrej730 7f3860b025 util.attribute - add tests 2025-07-08 21:20:57 +05:00
Andrej730 d08756d1b4 typing 2025-07-08 21:20:57 +05:00
Andrej730 525ef2b77b Explorer UI - add is_loaded toggle 2025-07-07 19:11:39 +05:00
Andrej730 fca2bf7aa8 Search filters - add example to criteria descriptions
To give users some default value to get familiar with the tool.
Example - https://files.catbox.moe/d1zh5n.png
2025-07-07 19:11:39 +05:00
Andrej730 ff82262490 Reuse get_active_sheet_item 2025-07-07 19:11:38 +05:00
Andrej730 c82351476a SheetBuilder - move references to local scope
Previously it was set as sheet builder attribute, but was never used as such.
2025-07-07 19:11:38 +05:00
Andrej730 d4f32802aa Show warnings if some sheets related files are missing
Previously when you would load/open/create sheets and some files were missing you would get traceback errors.

Also removed warnings for missing sheets svgs since they can go missing if someone is opening ifc project and want to regenerate sheets. We'll be able to return them in the future, if needed, but they will need some refinement.
2025-07-07 19:11:38 +05:00
Andrej730 d7668c3e3f drawing.get_sheet_identification 2025-07-07 19:11:38 +05:00
Andrej730 1445f91289 Fix very old bug with IfcPersonAndOrganization enum not loading (6ce8a36) 2025-07-07 19:11:37 +05:00
Andrej730 44e27f706a Owner UI - further decouple data.py from Blender entities 2025-07-07 19:11:37 +05:00
Andrej730 416ebde333 Fix names lists are not updated immediately
Similar to addresses problem in #6869
2025-07-07 19:11:37 +05:00
Andrej730 40dcd393c6 Fix addresses not updating immediately in IfcPerson UI
Found by @sboddy in #6869

Caused by trying to remove Blender entities from owner/data.py
2025-07-07 19:11:37 +05:00
Andrej730 4fda916c2f Fix duplicated assignments 2025-07-07 19:11:37 +05:00
Andrej730 acb9953ce3 dev_environment.py - add comments and small changes 2025-07-07 19:11:36 +05:00
Andrej730 4154356288 bim.operator - document resetting screen behaviour 2025-07-07 19:11:36 +05:00
Andrej730 54488bddbb bim.attribute_search_values - use enumproperty for data_type 2025-07-07 19:11:36 +05:00
Andrej730 8f21866248 typing 2025-07-07 19:11:36 +05:00
Thomas Krijnen f3b63aae76 Use should_use_presentation_style_assignment 2025-07-06 18:39:21 +02:00
Thomas Krijnen 3f05bab4e5 Shape builder: allow translate() on tesselated face sets 2025-07-06 18:35:19 +02:00
Thomas Krijnen 8534333092 Update doc comment 2025-07-06 18:34:50 +02:00
Ryan Schultz 5f2b4c96a6 fixes #6874 2025-07-06 10:26:12 -05:00
Massimo Fabbro 00836ec983 Fix #6736. Better use of non-SI units in creating spaces and coverings 2025-07-05 13:59:04 +02:00
Andrej730 fb78f75541 Basic IFC Entities explorer UI
A generic UI to allow editing simple entities like #6869 without creating an entire edit attributes UI inside edit attributes UI.
Still work in progress and should be connected to attributes UI.

Example - https://www.imgchest.com/p/agyv9lpq978
2025-07-04 19:48:12 +05:00
Andrej730 5948282ed0 bim.add_address - display it in more readable way
Before - https://cdn.imgchest.com/files/4gdcxp5j9l4.png
After - https://cdn.imgchest.com/files/yrgcnzgj5n4.png
2025-07-04 19:34:12 +05:00
Andrej730 47b81a6f89 bim.save_search - prefer only SEARCH IfcGroups by default #6857
Previously it was asking for a name to use to save search and users might type anything they find convenient at the moment, but it was possible that this name was used already by something important (for example drawing's IfcGroup) and would change it's description and assign objects there unexpectedly.

Now it will target IfcGroups with SEARCH ObjectType by default and only if user confirms, it will target all IfcGroups available (to keep the search a powerful tool for advanced users that allows them to populate any IfcGroup they want with items).

There's also a temporary patch in place  - when you load/save search Bonsai will try to find old search IfcGroups with ObjectType = None and update their ObjectType, this needed to make sure transition works seamlessly for the simple cases.
2025-07-04 19:34:12 +05:00
Andrej730 8ae08cb90e bim.save_search - not to fail silently 2025-07-04 19:34:11 +05:00
Andrej730 7796856119 typing 2025-07-04 19:34:11 +05:00
falken10vdl f75045f0c5 See #6570. Now it is possible to remove the csv cost file link.
This is a followup to that issue. In this particular case, a remove csv link button is added next to the refresh csv link button.
2025-07-04 12:35:17 +02:00
Andrej730 e52af2baad Iterator.validate_iterator_state to avoid crashes using next() and get()
Just to avoid accidental crashes using .get() / .next() when using them from Python - in cases when iterator was initialized but there were no elements or if iterator got exhausted.
2025-07-03 18:49:41 +05:00
Andrej730 da4ae4c2f2 Fix typo in 2a199cf 2025-07-03 18:48:30 +05:00
Andrej730 28043a8e5d ifc2sql - skip creating geometry if iterator failed to initialize 2025-07-03 18:48:30 +05:00
Andrej730 0e8fb44301 Remove unused Iterator.task_result_index_ (c78b289) 2025-07-03 18:48:08 +05:00
Andrej730 b97dca5d47 typing 2025-07-03 18:48:08 +05:00
Andrej730 207db459a7 Iterator.h - remove redundant typename (eeaf04c) 2025-07-03 18:48:07 +05:00
Andrej730 026eebf39b pyproject.toml - remove enableTypeIgnoreComments
As Pylance now allows inserting pyright ignore comments without it 🎉

https://github.com/microsoft/pylance-release/issues/7220
2025-07-03 18:48:07 +05:00
carlopav 5f5c0b41d5 Basic infrastructure for IfcCostSchedule PDF export with typst (#6860) 2025-07-03 13:18:03 +00:00
falken10 3055dfa1a6 clean up from developer's feedback + black formatting 2025-07-03 05:51:59 +02:00
falken10 b59bdb585b Refactor cost documents following developrs advice 2025-07-03 05:51:59 +02:00
falken10 ccd97c66ae refactor and black formatting 2025-07-03 05:51:59 +02:00
falken10 e6747b258c Remove CostSchedulesMapping and added csv_filepaths to data.py 2025-07-03 05:51:59 +02:00
falken10 6aa6803490 Added relative CSV file paths 2025-07-03 05:51:59 +02:00
falken10 72fbe3a6d9 Refactor cost schedule CSV filepath to use document references 2025-07-03 05:51:59 +02:00
Andrej730 68a602573b ifcopenshell.geom.iterator - more descriptive error in __init__ #6865 2025-07-02 18:14:34 +05:00
Andrej730 1e06dfcb76 Fix console errors when project is not loaded 2025-07-02 18:14:34 +05:00
Andrej730 3a0a912022 Use property_unset instead of in and del 2025-07-02 18:14:34 +05:00
Andrej730 b16fb7d917 bim.pip_install - add dynamic description 2025-07-02 18:14:34 +05:00
Andrej730 94d2d32d18 typing 2025-07-02 18:14:34 +05:00
falken10vdl e743db27f4 Draw attributes with search (#6849)
See example in #4325
2025-07-02 11:11:29 +05:00
Thomas Krijnen bb329affb8 settings: DeferProcessingFirstElement, MaxOffset, MaxOffsetDeviation, ApplyOffset; taxonomy: centroid funcs; iterator get_tasks + items() funcs; geom.map_shape() 2025-07-01 21:10:24 +02:00
Bruno Perdigão 6c4e4e5f91 Partial solution for #6775.
The issue happens when a wall has a "ATPATH" connection and has a
negative direction sense. It mixes the profile points in an incorrect
order. This commit seems to fix it, but maybe there is a better solution.
2025-06-30 19:50:41 -03:00
falken10 a31aa6053e black formatting 2025-06-30 18:25:41 -03:00
falken10 b90f146a68 update gizmo based on selected items 2025-06-30 18:25:41 -03:00
falken10 f67dca8de0 Enhance label rendering in BoundingBoxDecorator to prevent text overlaps 2025-06-30 18:25:41 -03:00
falken10 a686d70908 Updated based on feedback from @brunoperdigao 2025-06-30 18:25:41 -03:00
falken10vdl 38fe7fea65 Update workspace.py cosmetic remove nl from previous edits 2025-06-30 18:25:41 -03:00
falken10 b26890f74b black formating 2025-06-30 18:25:41 -03:00
falken10 2c16247943 moved to viewport overlays 2025-06-30 18:25:41 -03:00
falken10 53fc7bce0c clenaup 2025-06-30 18:25:41 -03:00
falken10 f5db905223 Separate decorator from operator to follow other modules in model / project 2025-06-30 18:25:41 -03:00
falken10 3557b11e64 same style as polyline texts 2025-06-30 18:25:41 -03:00
falken10 c420402334 black . formating 2025-06-30 18:25:41 -03:00
falken10 589c562cba refactor everyting within the class MeasureXYZDimensionsTool 2025-06-30 18:25:41 -03:00
falken10 d3f2711fe4 Moved everything to explore tool 2025-06-30 18:25:41 -03:00
falken10 8b4305360e updated to cope for multiple selection and better looking using trihedrons 2025-06-30 18:25:41 -03:00
falken10 1213bbdd73 set exact same color for bounding box edges as gizom axis 2025-06-30 18:25:41 -03:00
falken10 c3380daa19 better layout with colored bounding box 2025-06-30 18:25:41 -03:00
falken10 7aa87907d7 Add local orientation operator and UI toggle for colored dimensions 2025-06-30 18:25:41 -03:00
Andrej730 8cfd6b1754 Unify doc-strings (remove type/rtype)
As this information is already provided in type hints and we don't need to maintain it in two places.
2025-06-30 18:55:14 +05:00
Andrej730 fc10915d2b ifcopenshell.validate - remove simple_spf dependency (689289b)
Moved it to `TYPE_CHECKING` since it's currently used just for typing.

Was breaking validation in Bonsai since it doesn't provide all submodules with IfcOpenShell https://github.com/IfcOpenShell/IfcOpenShell/issues/6723#issuecomment-3016223786

Also added a simple test so github workflow would catch a simple error like this.
2025-06-30 18:08:34 +05:00
Andrej730 c23c21cb88 black . 2025-06-30 18:08:29 +05:00
Geert Hesselink 260bc80a4b update simple_spf submodule 2025-06-30 10:11:40 +02:00
Thomas Krijnen d7cf803fdf Don't use anonymous namespace for geometry_conversion_result 2025-06-29 20:04:44 +02:00
Thomas Krijnen 4488044a68 yum install xz 2025-06-29 19:58:37 +02:00
skemaikin 21a4db7823 serialize roughnessFactor
add roughnessFactor for gltf serialization
2025-06-29 14:24:52 +02:00
Ryan Schultz d300454118 Option to select only immediate children in Spatial Decomposition panel 2025-06-28 15:00:23 -05:00
Ryan Schultz 553003e664 Closes #6748: An option, when running bim.select_similar_container() to only select those objects in the immediate container 2025-06-28 13:16:51 -05:00
Ryan Schultz 524561a398 similar to #6748: option to select only immediate children, not recursively 2025-06-28 12:07:22 -05:00
Ryan Schultz 16f82683c8 closes #6843 - made merge by distance a little more tolerable 2025-06-28 10:07:54 -05:00
Ryan Schultz 4dbec59b9c Fix #6840: Add search to profile selection, when creating a new extrusion type 2025-06-28 09:17:40 -05:00
falken10vdl dcf69c46e5 Update PLAN_LEVEL annotation (#6844)
* Update PLAN_LEVEL annotation

* Length of line segment based on scale

---------

Co-authored-by: Ryan Schultz <ryan@openingdesign.com>
2025-06-27 09:48:12 -05:00
298 changed files with 7903 additions and 3186 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
findutils
findutils xz
git config --global --add safe.directory '*'
- name: Install aws cli
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
findutils
findutils xz
git config --global --add safe.directory '*'
- name: Install aws cli
+1 -1
View File
@@ -104,7 +104,7 @@ jobs:
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
# Download Blender.
wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.4/blender-4.4.0-linux-x64.tar.xz
wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.5/blender-4.5.0-linux-x64.tar.xz
tar -xf blender.tar.xz
# Setup Blender.
+1 -1
View File
@@ -1 +1 @@
0.8.3
0.8.4
-2
View File
@@ -16,8 +16,6 @@ extend-exclude = '''
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.
+4 -1
View File
@@ -105,6 +105,8 @@ classes = [
operator.BIM_OT_select_object,
operator.BIM_OT_show_description,
operator.BIM_OT_multiple_file_selector,
operator.BIM_OT_attribute_add_subitem,
operator.BIM_OT_attribute_remove_subitem,
operator.ClippingPlaneCutWithCappings,
operator.CloseBlendWarning,
operator.CloseError,
@@ -125,6 +127,7 @@ classes = [
operator.ShowSystemInfo,
prop.StrProperty,
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
operator.BIM_OT_attribute_search_values,
prop.ObjProperty,
prop.MultipleFileSelect,
prop.Attribute,
@@ -133,7 +136,6 @@ classes = [
prop.BIMProperties,
prop.IfcParameter,
prop.PsetQto,
prop.GlobalId,
prop.BIMObjectProperties,
prop.BIMCollectionProperties,
prop.BIMMeshProperties,
@@ -144,6 +146,7 @@ classes = [
ui.BIM_UL_clipping_plane,
ui.BIM_UL_generic,
ui.BIM_UL_topics,
ui.DocPreferences,
ui.BIM_ADDON_preferences,
# Tabs panel
ui.BIM_PT_tabs,
+5 -1
View File
@@ -30,7 +30,7 @@ from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.model.data import AuthoringData
from bonsai.bim.module.aggregate.decorator import AggregateDecorator
from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator
from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator
from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator, BoundingBoxDecorator
from bonsai.bim.module.nest.decorator import NestDecorator
from mathutils import Vector
from math import cos
@@ -409,9 +409,13 @@ def load_post(scene):
WallAxisDecorator.install(bpy.context)
if model_props.show_slab_direction:
SlabDirectionDecorator.install(bpy.context)
if model_props.show_bounding_box:
BoundingBoxDecorator.install(bpy.context)
if preferences.should_use_snap and (scene := bpy.context.scene):
# Snapping is off by default in Blender, but in BIM, it's more useful to be on
scene.tool_settings.use_snap = True
# Match default Bonsai snaps
scene.tool_settings.snap_elements_base = {"EDGE", "EDGE_PERPENDICULAR", "VERTEX", "EDGE_MIDPOINT", "FACE"}
tool.Blender.sync_old_preferences()
+82 -27
View File
@@ -27,6 +27,7 @@ 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 types import EllipsisType
from typing import Optional, Any, Union, TYPE_CHECKING
from collections.abc import Callable, Iterable
@@ -39,6 +40,9 @@ if TYPE_CHECKING:
# - None - property should be imported by default workflow
# - True - setting value for imported attribute should be skipped
# - False - property should be skipped entirely from import
# Second argument is optional,
# because ImportCallback might be called for attributes that are not created by default
# (e.g. IFC entity attributes).
ImportCallback = Callable[[str, Optional[bonsai.bim.prop.Attribute], dict[str, Any]], Union[bool, None]]
# ExportCallback return values:
# - True - property should be skipped entirely from export
@@ -52,30 +56,61 @@ def draw_attributes(
copy_operator: Optional[str] = None,
popup_active_attribute: Optional[bonsai.bim.prop.Attribute] = None,
callback: Optional[Callable[[bonsai.bim.prop.Attribute, bpy.types.UILayout], None]] = None,
*,
enable_search: Union[bool, EllipsisType] = ...,
) -> None:
"""Draw editable UI for prop.Attributes.
You can set attribute active in popup with `active_attribute`
meaning you will be able to type into attribute's field without having to click
on it first
:param enable_search: Add search button to string, integer, and float attributes
Possible values:
- ``...`` (default value) -
add search if possible. If it's not possible, there will be no warnings or errors.
- ``True`` - always add search, if it's not possible it will result in errors.
- ``False`` - never add search.
"""
for attribute in props:
row = layout.row(align=True)
if attribute == popup_active_attribute:
row.activate_init = True
draw_attribute(attribute, row, copy_operator)
draw_attribute(attribute, row, copy_operator, enable_search=enable_search)
if callback:
callback(attribute, row)
def draw_attribute(
attribute: bonsai.bim.prop.Attribute, layout: bpy.types.UILayout, copy_operator: Optional[str] = None
attribute: bonsai.bim.prop.Attribute,
layout: bpy.types.UILayout,
copy_operator: Optional[str] = None,
enable_search: Union[bool, EllipsisType] = ...,
) -> None:
value_name = attribute.get_value_name(display_only=True)
if value_name == "enum_value":
prop_with_search(layout, attribute, "enum_value", text=attribute.name)
elif value_name == "filepath_value":
attribute.filepath_value.layout_file_select(layout, filter_glob=attribute.filter_glob, text=attribute.name)
elif value_name == "subitems_values":
col = layout.column()
layout = col.row(align=True)
layout.label(text=f"{attribute.name}:")
data_path = tool.Blender.get_full_data_path(attribute, value_name)
for i, item in enumerate(attribute.subitems_values, 1):
row = col.row(align=True)
row.alignment = "EXPAND"
row.prop(item, "name", text=f"# {i}")
op = row.operator("bim.attribute_remove_subitem", text="", icon="X")
op.data_path = data_path
op.index = i - 1
op = layout.operator("bim.attribute_add_subitem", icon="ADD", text="")
op.data_path = data_path
elif attribute.name in ("ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"):
props = tool.Sequence.get_work_schedule_props()
for item in props.durations_attributes:
@@ -96,17 +131,34 @@ def draw_attribute(
text=attribute.display_name,
)
if attribute.is_uri:
if attribute.special_type == "URI":
op = layout.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER")
op.data_path = attribute.path_from_id("string_value")
op.attribute_data_path = tool.Blender.get_full_data_path(attribute)
elif attribute.special_type in ("DATE", "DATETIME"):
op = layout.operator("bim.datepicker", text="", icon="TIME")
op.target_prop = attribute.path_from_id("string_value")
op.include_time = attribute.special_type == "DATETIME"
if attribute.data_type in ("string", "integer", "float") and (
enable_search is True or (enable_search is ... and attribute.ifc_class)
):
op = layout.operator("bim.attribute_search_values", text="", icon="VIEWZOOM")
op.attribute_name = attribute.name
op.attribute_ifc_class = attribute.ifc_class
op.data_path = tool.Blender.get_full_data_path(attribute, value_name)
op.data_type = attribute.data_type
if attribute.is_optional:
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
if attribute.use_explorer_ui:
op = layout.operator("bim.explorer_show_ui_popup", text="", icon="ZOOM_SELECTED")
op.ifc_class = attribute.ifc_class
op.attribute_name = attribute.name
op.data_path = tool.Blender.get_full_data_path(attribute, value_name)
if ifc_id := attribute.get_value():
op.preselect_ifc_id = int(ifc_id)
if attribute.name == "GlobalId":
layout.operator("bim.generate_global_id", icon="FILE_REFRESH", text="")
elif copy_operator:
@@ -115,23 +167,14 @@ def draw_attribute(
def import_attributes(
ifc_class: str,
props: bpy.types.bpy_prop_collection_idprop[Attribute],
data: dict[str, Any],
callback: Optional[ImportCallback] = None,
) -> None:
schema = tool.Ifc.schema()
assert (entity := schema.declaration_by_name(ifc_class).as_entity())
for attribute in entity.all_attributes():
import_attribute(attribute, props, data, callback=callback)
# A more elegant attribute importer signature, intended to supersede import_attributes
def import_attributes2(
element: Union[str, ifcopenshell.entity_instance],
props: bpy.types.bpy_prop_collection_idprop[Attribute],
callback: Optional[ImportCallback] = None,
) -> None:
"""
:param element: Entity or IFC class string.
"""
info: dict[str, Any]
if isinstance(element, str):
assert (entity := tool.Ifc.schema().declaration_by_name(element).as_entity())
attributes = entity.all_attributes()
@@ -153,15 +196,19 @@ def import_attribute(
) -> None:
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
# Complex data types (aggregates and entities) are handled only by callback.
if data_type == ("list", "string"):
data_type = "list[string]"
if isinstance(data_type, tuple) or data_type == "entity":
callback(attribute.name(), None, data) if callback else None
return
new: bonsai.bim.prop.Attribute = props.add()
new = props.add()
new.name = attribute.name()
new.is_null = data[attribute.name()] is None
new.is_optional = attribute.optional()
new.data_type = data_type if isinstance(data_type, str) else ""
new.ifc_class = data["type"]
is_handled_by_callback = callback(attribute.name(), new, data) if callback else None
data_type = new.data_type # Allow callback to override data type.
@@ -171,8 +218,9 @@ def import_attribute(
props.remove(len(props) - 1)
elif data_type == "string":
new.string_value = "" if new.is_null else str(data[attribute.name()]).replace("\n", "\\n")
if attribute.type_of_attribute().declared_type().name() == "IfcURIReference":
new.is_uri = True
attribute_type = attribute.type_of_attribute()
if attribute_type._is("IfcURIReference"):
new.special_type = "URI"
elif attribute.type_of_attribute()._is("IfcDate"):
new.special_type = "DATE"
elif attribute.type_of_attribute()._is("IfcDateTime"):
@@ -206,6 +254,12 @@ def import_attribute(
if enum_value is not None:
new.enum_value = enum_value
elif data_type == "list[string]":
value: Union[list[str], None] = data[attribute.name()]
if value:
for item in value:
new.subitems_values.add().name = str(item).replace("\n", "\\n")
add_attribute_description(new, data)
add_attribute_min_max(attribute, new)
@@ -273,13 +327,14 @@ def export_attributes(
return attributes
def process_exported_entity_attribute(attributes: dict[str, Any], attribute_name: str) -> None:
entity_id = attributes[attribute_name]
if entity_id is None:
# Maybe it was removed by now and enum is invalid.
del attributes[attribute_name]
else:
attributes[attribute_name] = tool.Ifc.get().by_id(int(entity_id))
def process_exported_entity_attribute(attributes: dict[str, Any], attribute_names: list[str]) -> None:
for attribute_name in attribute_names:
entity_id = attributes[attribute_name]
if entity_id is None:
# Maybe it was removed by now and enum is invalid.
del attributes[attribute_name]
else:
attributes[attribute_name] = tool.Ifc.get().by_id(int(entity_id))
ENUM_ITEMS_DATA = Union[bpy.types.PropertyGroup, bpy.types.ID, bpy.types.Operator, bpy.types.OperatorProperties]
+12 -12
View File
@@ -105,7 +105,7 @@ class IfcStore:
IfcStore.session_files = {}
@staticmethod
def get_file():
def get_file() -> ifcopenshell.file | None:
if IfcStore.file is None:
props = tool.Blender.get_bim_props()
IfcStore.set_path(props.ifc_file)
@@ -117,20 +117,20 @@ class IfcStore:
return IfcStore.file
@staticmethod
def set_path(value):
def set_path(value: str) -> None:
IfcStore.path = value
# Interpret relative paths as relative to .blend file.
if IfcStore.path and not os.path.isabs(IfcStore.path):
IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path))
@staticmethod
def get_cache():
def get_cache() -> ifcopenshell.geom.serializers.hdf5 | None:
if IfcStore.cache is None and IfcStore.path:
props = tool.Blender.get_bim_props()
prefs = tool.Blender.get_addon_preferences()
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
os.makedirs(props.cache_dir, exist_ok=True)
IfcStore.cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5")
os.makedirs(prefs.cache_dir, exist_ok=True)
IfcStore.cache_path = os.path.join(prefs.cache_dir, f"{ifc_hash}.h5")
cache_path = Path(IfcStore.cache_path)
cache_settings = ifcopenshell.geom.settings()
serializer_settings = ifcopenshell.geom.serializer_settings()
@@ -163,15 +163,15 @@ class IfcStore:
return IfcStore.cache
@staticmethod
def update_cache():
def update_cache() -> None:
if not IfcStore.cache:
return
assert IfcStore.cache_path
assert IfcStore.file
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
props = tool.Blender.get_bim_props()
new_cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5")
prefs = tool.Blender.get_addon_preferences()
new_cache_path = os.path.join(prefs.cache_dir, f"{ifc_hash}.h5")
IfcStore.cache = None
try:
shutil.move(IfcStore.cache_path, new_cache_path)
@@ -183,11 +183,11 @@ class IfcStore:
IfcStore.get_cache()
@staticmethod
def load_file(path) -> None:
def load_file(path: str) -> None:
if not os.path.isfile(path):
return
extension = path.split(".")[-1]
props = tool.Project.get_project_props()
prefs = tool.Blender.get_addon_preferences()
if extension.lower() == "ifczip":
with tempfile.TemporaryDirectory() as unzipped_path:
with zipfile.ZipFile(path, "r") as zip_ref:
@@ -197,7 +197,7 @@ class IfcStore:
return
elif extension.lower() == "ifcxml":
IfcStore.file = ifcopenshell.file(ifcopenshell.ifcopenshell_wrapper.parse_ifcxml(path))
elif props.should_stream:
elif prefs.should_stream:
IfcStore.file = ifcopenshell.open(path, should_stream=True)
else:
IfcStore.file = ifcopenshell.open(path)
+3 -2
View File
@@ -231,7 +231,8 @@ class IfcImporter:
self.progress = 0
self.material_creator = MaterialCreator(ifc_import_settings, self)
classes_to_wireframe_str = tool.Drawing.get_document_props().classes_to_wireframe
prefs = tool.Blender.get_addon_preferences()
classes_to_wireframe_str = prefs.doc.classes_to_wireframe
self.classes_to_wireframe_list = [word.strip() for word in classes_to_wireframe_str.split(",")]
def profile_code(self, message: str) -> None:
@@ -662,7 +663,7 @@ class IfcImporter:
settings: Optional[ifcopenshell.geom.main.settings] = None,
) -> set[ifcopenshell.entity_instance]:
checkpoint = time.time()
results = set()
results: set[ifcopenshell.entity_instance] = set()
if not products:
return results
@@ -225,16 +225,25 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
bl_idname = "bim.select_aggregate"
bl_label = "Select Aggregate"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
select_parts: bpy.props.BoolProperty(default=False)
one_level_deep: bpy.props.BoolProperty(
name="One Level Deep", description="Select only immediate children, not recursively", default=False
)
@classmethod
def description(cls, context, properties):
if properties.select_parts:
return "Select Aggregate and Parts"
return "Select Aggregate and Parts.\n\nCtrl+click to select only one level deep"
else:
return "Select Aggregate"
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.ctrl:
self.one_level_deep = True
return self.execute(context)
def execute(self, context):
all_parts = []
for obj in context.selected_objects:
@@ -250,14 +259,27 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
obj.select_set(False)
if self.select_parts:
all_objs = []
selected_parts = []
for part in all_parts:
if part.IsDecomposedBy:
for subpart in part.IsDecomposedBy[0].RelatedObjects:
all_parts.append(subpart)
all_objs.append(part)
for rel in part.IsDecomposedBy:
for subpart in rel.RelatedObjects:
selected_parts.append(subpart)
for element in all_objs:
# If not limited to one level, traverse deeper
if not self.one_level_deep:
def add_descendants(elem):
if elem.IsDecomposedBy:
for rel in elem.IsDecomposedBy:
for deeper in rel.RelatedObjects:
selected_parts.append(deeper)
add_descendants(deeper)
add_descendants(subpart)
for element in set(selected_parts + all_parts):
obj = tool.Ifc.get_object(element)
if obj:
obj.select_set(True)
@@ -265,9 +287,11 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
else:
for aggregate_element in all_parts:
aggregate_obj = tool.Ifc.get_object(aggregate_element)
aggregate_obj.select_set(True)
bpy.context.view_layer.objects.active = aggregate_obj
if aggregate_obj:
aggregate_obj.select_set(True)
bpy.context.view_layer.objects.active = aggregate_obj
self.one_level_deep = False # <-- forcibly reset
return {"FINISHED"}
@@ -21,7 +21,6 @@
import os
import ifcopenshell.api.alignment
import ifcopenshell.api.alignment.add_stationing_to_alignment
import bpy
import json
@@ -62,9 +61,7 @@ class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
def _execute(self, context):
self.file = tool.Ifc.get()
start = time.time()
alignment = ifcopenshell.api.alignment.create_alignment_from_csv(self.file, self.filepath)
ifcopenshell.api.alignment.create_geometric_representation(self.file, alignment)
ifcopenshell.api.alignment.add_stationing_to_alignment(self.file, alignment=alignment, start_station=0.0)
alignment = ifcopenshell.api.alignment.create_from_csv(self.file, self.filepath)
# IFC 4.1.5.1 alignments cannot be contained in spatial structures, but can be referenced into them
sites = self.file.by_type("IfcSite")
@@ -25,13 +25,23 @@ classes = (
operator.EditAttributes,
operator.GenerateGlobalId,
operator.CopyAttributeToSelection,
operator.ExplorerAddEntity,
operator.ExplorerEnableEditingEntity,
operator.ExplorerDisableEditingEntity,
operator.ExplorerEditEntity,
operator.ExplorerShowUIPopup,
prop.BIMAttributeProperties,
prop.ExplorerEntity,
prop.BIMExplorerProperties,
ui.BIM_PT_object_attributes,
ui.BIM_PT_explorer,
ui.BIM_UL_explorer,
)
def register():
bpy.types.Object.BIMAttributeProperties = bpy.props.PointerProperty(type=prop.BIMAttributeProperties)
bpy.types.Scene.BIMExplorerProperties = bpy.props.PointerProperty(type=prop.BIMExplorerProperties)
def unregister():
@@ -26,7 +26,11 @@ import bonsai.bim.helper
import bonsai.tool as tool
import bonsai.core.attribute as core
import bonsai.core.spatial
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Union, Literal
if TYPE_CHECKING:
from bonsai.bim.prop import Attribute
import bpy.stub_internal.rna_enums as rna_enums
def get_objs_for_operation(
@@ -75,7 +79,9 @@ class EnableEditingAttributes(bpy.types.Operator, AttributesOperator):
None,
)
def callback(name, prop, data):
lookup_attrs = tool.Attribute.does_ifc_class_support_explorer_lookup(element.is_a())
def callback(name: str, prop: Union["Attribute", None], data: dict[str, Any]) -> None | Literal[True]:
if name in ("RefLatitude", "RefLongitude"):
new = props.attributes.add()
new.name = name
@@ -89,8 +95,19 @@ class EnableEditingAttributes(bpy.types.Operator, AttributesOperator):
if name in ("PredefinedType", "ObjectType") and has_inherited_predefined_type:
props.attributes.remove(len(props.attributes) - 1)
return True
if lookup_attrs and (name in lookup_attrs):
new = props.attributes.add()
new.name = name
new.ifc_class = data["type"]
new.data_type = "enum"
new.is_optional = True
new.enum_items_dynamic = lookup_attrs[name]
new.use_explorer_ui = True
value: Union[ifcopenshell.entity_instance, None] = data[name]
if value is not None:
new.enum_value = str(value.id())
bonsai.bim.helper.import_attributes2(element, props.attributes, callback=callback)
bonsai.bim.helper.import_attributes(element, props.attributes, callback=callback)
props.is_editing_attributes = True
def execute(self, context):
@@ -107,7 +124,8 @@ class DisableEditingAttributes(bpy.types.Operator, AttributesOperator):
def disable_editing_attributes_on_obj(self, obj: bpy.types.Object) -> None:
props = tool.Blender.get_object_attribute_props(obj)
props.is_editing_attributes = False
props.attributes.clear()
props.property_unset("is_editing_attributes")
def execute(self, context):
for obj in get_objs_for_operation(self, context):
@@ -127,7 +145,7 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
if not obj or not (element := tool.Ifc.get_entity(obj)):
return
def callback(attributes, prop):
def callback(attributes: dict[str, Any], prop: "Attribute") -> None | Literal[True]:
if prop.name in ("RefLatitude", "RefLongitude"):
if not prop.is_null:
try:
@@ -138,6 +156,9 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Blender.get_object_attribute_props(obj)
attributes = bonsai.bim.helper.export_attributes(props.attributes, callback=callback)
lookup_attrs = tool.Attribute.does_ifc_class_support_explorer_lookup(element.is_a())
if lookup_attrs:
bonsai.bim.helper.process_exported_entity_attribute(attributes, list(lookup_attrs))
ifcopenshell.api.attribute.edit_attributes(self.file, product=element, attributes=attributes)
tool.Root.set_object_name(obj, element)
@@ -204,3 +225,139 @@ class CopyAttributeToSelection(bpy.types.Operator, tool.Ifc.Operator):
tool.Ifc, tool.Blender, tool.Root, tool.Spatial, name=self.name, value=value
)
self.report({"INFO"}, f"Attribute was successfully copied to {total} elements.")
class ExplorerAddEntity(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.explorer_add_entity"
bl_label = "Add Entity"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context) -> None:
props = tool.Attribute.get_explorer_props()
ifc_file = tool.Ifc.get()
entity = ifc_file.create_entity(props.ifc_class)
tool.Attribute.refresh_uilist_entities()
tool.Attribute.enable_editing_entity(entity)
tool.Attribute.import_entity_attributes(entity)
class ExplorerEnableEditingEntity(bpy.types.Operator):
bl_idname = "bim.explorer_enable_editing_entity"
bl_label = "Enable Editing Entity"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
props = tool.Attribute.get_explorer_props()
ifc_file = tool.Ifc.get()
assert (active_entity := props.active_entity)
entity = ifc_file.by_id(active_entity.ifc_definition_id)
tool.Attribute.disable_editing_entity()
tool.Attribute.enable_editing_entity(entity)
tool.Attribute.import_entity_attributes(entity)
return {"FINISHED"}
class ExplorerDisableEditingEntity(bpy.types.Operator):
bl_idname = "bim.explorer_disable_editing_entity"
bl_label = "Disable Editing Entity"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
tool.Attribute.disable_editing_entity()
return {"FINISHED"}
class ExplorerEditEntity(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.explorer_edit_entity"
bl_label = "Edit Entity"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context) -> None:
ifc_file = tool.Ifc.get()
props = tool.Attribute.get_explorer_props()
entity = ifc_file.by_id(props.editing_entity_id)
attrs = tool.Attribute.export_entity_attributes()
for attr, value in attrs.items():
setattr(entity, attr, value)
tool.Attribute.refresh_uilist_entities()
tool.Attribute.disable_editing_entity()
class ExplorerShowUIPopup(bpy.types.Operator):
bl_idname = "bim.explorer_show_ui_popup"
bl_label = "Show Explorer UI"
bl_description = "Show Explorer UI to select element as attribute value or edit it."
bl_options = {"REGISTER", "UNDO"}
ifc_class: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
"""Element IFC class."""
attribute_name: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
"""IFC class attribute name."""
data_path: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
"""Full data path"""
preselect_ifc_id: bpy.props.IntProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
"""IFC id to preselect in the popup."""
if TYPE_CHECKING:
ifc_class: str
attribute_name: str
data_path: str
preselect_ifc_id: int
def invoke(self, context, event) -> "set[rna_enums.OperatorReturnItems]":
assert context.window_manager
assert self.ifc_class and self.attribute_name and self.data_path
props = tool.Attribute.get_explorer_props()
props.is_loaded = True
props.ifc_class = self.get_attribute_type()
if self.preselect_ifc_id:
props.active_entity_index = next(
i for i, e in enumerate(props.entities) if e.ifc_definition_id == self.preselect_ifc_id
)
return context.window_manager.invoke_props_dialog(self, width=400)
def get_attribute_type(self) -> str:
schema = tool.Ifc.schema()
entity = schema.declaration_by_name(self.ifc_class).as_entity()
assert entity
i = entity.attribute_index(self.attribute_name)
attr = entity.all_attributes()[i]
named_type = attr.type_of_attribute().as_named_type()
assert named_type
declared = named_type.declared_type()
return declared.name()
def draw(self, context) -> None:
from bonsai.bim.module.attribute.ui import BIM_PT_explorer
BIM_PT_explorer.draw(self, context, is_popup=True)
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
props = tool.Attribute.get_explorer_props()
active_entity = props.active_entity
if active_entity is None:
self.report({"WARNING"}, "No entity selected.")
return {"FINISHED"}
# Apply pending changes for convenience.
if props.editing_entity_id:
if props.editing_entity_id == active_entity.ifc_definition_id:
bpy.ops.bim.explorer_edit_entity()
else:
bpy.ops.bim.explorer_disable_editing_entity()
# Very important to do it after changes applied, otherwise enum might update
# and index will be pointing to a different element.
exec(f"{self.data_path} = '{active_entity.ifc_definition_id}'")
return {"FINISHED"}
def cancel(self, context: bpy.types.Context) -> None:
props = tool.Attribute.get_explorer_props()
if props.editing_entity_id:
bpy.ops.bim.explorer_disable_editing_entity()
+61 -1
View File
@@ -17,6 +17,7 @@
# 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 bpy.types import PropertyGroup
from bpy.props import (
@@ -29,7 +30,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Union
class BIMAttributeProperties(PropertyGroup):
@@ -39,3 +40,62 @@ class BIMAttributeProperties(PropertyGroup):
if TYPE_CHECKING:
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
is_editing_attributes: bool
class ExplorerEntity(PropertyGroup):
ifc_definition_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
ifc_definition_id: int
class BIMExplorerProperties(PropertyGroup):
def update_is_loaded(self, context: object) -> None:
if self.is_loaded:
# Trigger refresh.
self.ifc_class = self.ifc_class
else:
self.property_unset("is_loaded")
self.property_unset("ifc_class")
self.entities.clear()
self.property_unset("active_entity_index")
self.property_unset("editing_entity_id")
self.entity_attributes.clear()
is_loaded: BoolProperty( # pyright: ignore[reportRedeclaration]
name="Toggle Explorer UI",
update=update_is_loaded,
)
def get_ifc_class(self, context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
# TODO: Add more entities.
classes = [
"IfcPostalAddress",
"IfcTelecomAddress",
]
return [(c, c, "") for c in classes]
def update_ifc_class(self, context: object) -> None:
tool.Attribute.refresh_uilist_entities()
ifc_class: EnumProperty( # pyright: ignore[reportRedeclaration]
name="IFC Class To Search",
items=get_ifc_class,
update=update_ifc_class,
)
entities: CollectionProperty(type=ExplorerEntity) # pyright: ignore[reportRedeclaration]
active_entity_index: IntProperty() # pyright: ignore[reportRedeclaration]
editing_entity_id: IntProperty() # pyright: ignore[reportRedeclaration]
entity_attributes: CollectionProperty(type=Attribute) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
is_loaded: bool
ifc_class: str
entities: bpy.types.bpy_prop_collection_idprop[ExplorerEntity]
active_entity_index: int
editing_entity_id: int
entity_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
@property
def active_entity(self) -> Union[ExplorerEntity, None]:
return tool.Blender.get_active_uilist_element(self.entities, self.active_entity_index)
+78 -1
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 bonsai.bim.helper
import bpy.types
from bpy.types import Panel
from bonsai.bim.module.attribute.data import AttributesData
import bonsai.tool as tool
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.attribute.prop import BIMExplorerProperties, ExplorerEntity
def draw_ui(context: bpy.types.Context, layout: bpy.types.UILayout, attributes) -> None:
@@ -33,7 +38,9 @@ def draw_ui(context: bpy.types.Context, layout: bpy.types.UILayout, attributes)
row.operator("bim.edit_attributes", icon="CHECKMARK", text="Save Attributes")
row.operator("bim.disable_editing_attributes", icon="CANCEL", text="")
bonsai.bim.helper.draw_attributes(props.attributes, layout, copy_operator="bim.copy_attribute_to_selection")
bonsai.bim.helper.draw_attributes(
props.attributes, layout, copy_operator="bim.copy_attribute_to_selection", enable_search=True
)
else:
row = layout.row()
op = row.operator("bim.enable_editing_attributes", icon="GREASEPENCIL", text="Edit")
@@ -66,3 +73,73 @@ class BIM_PT_object_attributes(Panel):
if not AttributesData.is_loaded:
AttributesData.load()
draw_ui(context, self.layout, AttributesData.data["attributes"])
class BIM_PT_explorer(Panel):
bl_label = "Explorer"
bl_idname = "BIM_PT_explorer"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_project_setup"
def draw(self, context, *, is_popup=False):
assert (layout := self.layout)
props = tool.Attribute.get_explorer_props()
if is_popup:
layout.label(text=props.ifc_class)
else:
if not props.is_loaded:
row = layout.row(align=True)
row.label(text="Explorer UI is not Loaded.")
row.prop(props, "is_loaded", text="", icon="IMPORT")
return
row = layout.row(align=True)
row.prop(props, "ifc_class", text="")
row.prop(props, "is_loaded", text="", icon="CANCEL")
active_entity = props.active_entity
row = layout.row(align=True)
row.label(text=f"{len(props.entities)} entities found")
row.operator("bim.explorer_add_entity", text="", icon="ADD")
if active_entity:
if active_entity.ifc_definition_id == props.editing_entity_id:
row.operator("bim.explorer_disable_editing_entity", icon="CANCEL", text="")
else:
row.operator("bim.explorer_enable_editing_entity", icon="GREASEPENCIL", text="")
# TODO: 'Remove' button?
layout.template_list("BIM_UL_explorer", "", props, "entities", props, "active_entity_index")
if props.editing_entity_id:
box = self.layout.box()
# In popup we accept edits automatically for the convenience.
# Othrewise it seems very unintuitive, when you need to click confirmation twice.
if not is_popup:
row = box.row(align=True)
row.operator("bim.explorer_edit_entity", icon="CHECKMARK")
row.operator("bim.explorer_disable_editing_entity", icon="CANCEL", text="")
bonsai.bim.helper.draw_attributes(props.entity_attributes, box, enable_search=True)
class BIM_UL_explorer(bpy.types.UIList):
def draw_item(
self,
context: bpy.types.Context,
layout: bpy.types.UILayout,
data: BIMExplorerProperties,
item: ExplorerEntity,
icon,
active_data,
active_propname,
) -> None:
row = layout.row(align=True)
if item.ifc_definition_id == data.editing_entity_id:
row.label(text=item.name, icon="GREASEPENCIL")
else:
row.label(text=item.name)
+6 -1
View File
@@ -1208,9 +1208,11 @@ class ActivateBcfViewpoint(bpy.types.Operator):
self.file = tool.Ifc.get()
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
assert context.scene
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
assert blender_topic
topic = bcfxml.topics[blender_topic.name]
if self.viewpoint_guid:
viewpoint_guid = self.viewpoint_guid
@@ -1234,6 +1236,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
cam_height = context.scene.render.resolution_y
cam_aspect = cam_width / cam_height
assert isinstance(obj.data, bpy.types.Camera)
obj.data.background_images.clear()
if viewpoint.snapshot:
obj.data.show_background_images = True
@@ -1273,7 +1276,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
if tool.Bcf.get_viewpoint_bitmaps(viewpoint):
self.create_bitmaps(bcfxml, viewpoint, topic)
self.setup_camera(viewpoint, obj, cam_aspect, context)
self.setup_camera(viewpoint, obj, cam_aspect, context, cam_height, cam_width)
return {"FINISHED"}
def setup_camera(
@@ -1282,6 +1285,8 @@ class ActivateBcfViewpoint(bpy.types.Operator):
obj: bpy.types.Object,
cam_aspect: float,
context: bpy.types.Context,
cam_height: float,
cam_width: float,
) -> None:
assert isinstance(obj.data, bpy.types.Camera)
if viewpoint.visualization_info.orthogonal_camera:
+1 -13
View File
@@ -35,7 +35,7 @@ from bpy.props import (
from typing import Union, TYPE_CHECKING, Literal
def get_active_dictionary(self, context):
def get_active_dictionary(self: "BIMBSDDProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
if not BSDDData.is_loaded:
BSDDData.load()
return BSDDData.data["active_dictionary"]
@@ -158,15 +158,6 @@ class BIMBSDDProperties(PropertyGroup):
description="Whether to display and assign only properties from IFC dictionary",
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)
if TYPE_CHECKING:
@@ -186,9 +177,6 @@ class BIMBSDDProperties(PropertyGroup):
keyword: str
should_filter_ifc_class: bool
use_only_ifc_properties: bool
load_preview_dictionaries: bool
load_inactive_dictionaries: bool
load_test_dictionaries: bool
classification_psets: bpy.types.bpy_prop_collection_idprop[BSDDPset]
@property
@@ -33,7 +33,7 @@ def refresh():
class ClassificationsData:
data = {}
data: dict[str, Any] = {}
is_loaded = False
@classmethod
@@ -94,7 +94,7 @@ class ReferencesData:
class ClassificationReferencesData(ReferencesData):
data = {}
data: dict[str, Any] = {}
is_loaded = False
@classmethod
@@ -118,7 +118,7 @@ class ClassificationReferencesData(ReferencesData):
class MaterialClassificationsData(ReferencesData):
data = {}
data: dict[str, Any] = {}
is_loaded = False
@classmethod
@@ -134,8 +134,7 @@ class MaterialClassificationsData(ReferencesData):
results = []
props = tool.Material.get_material_props()
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index]
if material := props.active_material:
if material.ifc_definition_id:
element = tool.Ifc.get().by_id(material.ifc_definition_id)
for reference in ifcopenshell.util.classification.get_references(element):
@@ -146,7 +145,7 @@ class MaterialClassificationsData(ReferencesData):
class CostClassificationsData(ReferencesData):
data = {}
data: dict[str, Any] = {}
is_loaded = False
@classmethod
@@ -146,7 +146,7 @@ class EnableAddingManualClassification(bpy.types.Operator):
props.is_adding = True
props.active_classification_id = 0
props.classification_attributes.clear()
bonsai.bim.helper.import_attributes2("IfcClassification", props.classification_attributes)
bonsai.bim.helper.import_attributes("IfcClassification", props.classification_attributes)
return {"FINISHED"}
@@ -170,7 +170,7 @@ class EnableAddingManualClassificationReference(bpy.types.Operator):
props = tool.Classification.get_classification_reference_props()
props.is_adding = True
props.reference_attributes.clear()
bonsai.bim.helper.import_attributes2("IfcClassificationReference", props.reference_attributes)
bonsai.bim.helper.import_attributes("IfcClassificationReference", props.reference_attributes)
return {"FINISHED"}
@@ -205,7 +205,7 @@ class EnableEditingClassification(bpy.types.Operator):
props = tool.Classification.get_classification_props()
props.classification_attributes.clear()
bonsai.bim.helper.import_attributes2(
bonsai.bim.helper.import_attributes(
tool.Ifc.get().by_id(self.classification), props.classification_attributes, callback
)
props.active_classification_id = self.classification
@@ -274,7 +274,7 @@ class EnableEditingClassificationReference(bpy.types.Operator):
def execute(self, context):
props = tool.Classification.get_classification_reference_props()
props.reference_attributes.clear()
bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.reference), props.reference_attributes)
bonsai.bim.helper.import_attributes(tool.Ifc.get().by_id(self.reference), props.reference_attributes)
props.active_reference_id = self.reference
return {"FINISHED"}
@@ -28,7 +28,7 @@ from bonsai.bim.module.classification.data import (
MaterialClassificationsData,
CostClassificationsData,
)
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Union
if TYPE_CHECKING:
from bonsai.bim.module.classification.prop import BIMClassificationProperties, ClassificationReference
@@ -113,9 +113,15 @@ class BIM_PT_classifications(Panel):
class ReferenceUI:
layout: bpy.types.UILayout
data: type[
Union[
ClassificationReferencesData,
MaterialClassificationsData,
CostClassificationsData,
]
]
def draw_ui(self, context):
obj = context.active_object
def draw_ui(self, context) -> None:
self.sprops = tool.Classification.get_classification_props()
self.bprops = tool.Bsdd.get_bsdd_props()
self.props = tool.Classification.get_classification_reference_props()
@@ -133,7 +139,7 @@ class ReferenceUI:
else:
self.draw_reference_ui(reference)
def draw_add_ui(self, context):
def draw_add_ui(self, context) -> None:
row = self.layout.row(align=True)
row.label(text="Source", icon="OUTLINER")
row.prop(self.sprops, "classification_source", text="")
@@ -145,7 +151,7 @@ class ReferenceUI:
else:
self.draw_add_bsdd_ui(context)
def draw_add_manual_ui(self, context):
def draw_add_manual_ui(self, context) -> None:
row = self.layout.row()
row.prop(self.props, "classifications", text="")
if self.props.is_adding:
@@ -158,7 +164,7 @@ class ReferenceUI:
row = self.layout.row()
row.operator("bim.enable_adding_manual_classification_reference", text="Add Reference", icon="ADD")
def draw_add_bsdd_ui(self, context):
def draw_add_bsdd_ui(self, context) -> None:
row = self.layout.row(align=True)
row.prop(self.bprops, "keyword", text="")
row.prop(self.bprops, "should_filter_ifc_class", text="", icon="FILTER")
@@ -185,7 +191,7 @@ class ReferenceUI:
op.obj = self.obj
op.obj_type = self.obj_type
def draw_add_file_ui(self, context):
def draw_add_file_ui(self, context) -> None:
if not self.data.data["active_classification_library"]:
row = self.layout.row(align=True)
row.label(text="No Active Classification Library", icon="ERROR")
@@ -219,13 +225,13 @@ class ReferenceUI:
"active_library_reference_index",
)
def draw_editable_ui(self):
def draw_editable_ui(self) -> None:
row = self.layout.row(align=True)
row.operator("bim.edit_classification_reference", text="Save changes", icon="CHECKMARK")
row.operator("bim.disable_editing_classification_reference", text="", icon="CANCEL")
bonsai.bim.helper.draw_attributes(self.props.reference_attributes, self.layout)
def draw_reference_ui(self, reference):
def draw_reference_ui(self, reference: dict[str, Any]) -> None:
row = self.layout.row(align=True)
if self.file.schema == "IFC2X3":
name = reference["ItemReference"] or "No Identification"
@@ -81,7 +81,7 @@ class EnableEditingConstraint(bpy.types.Operator):
def execute(self, context):
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)
bonsai.bim.helper.import_attributes(tool.Ifc.get().by_id(self.constraint), props.constraint_attributes)
props.active_constraint_id = self.constraint
return {"FINISHED"}
@@ -59,9 +59,11 @@ classes = (
operator.ExpandCostItemRate,
operator.ExpandCostItems,
operator.ExportCostSchedules,
operator.ExportCostSchedulesToPDF,
operator.HighlightProductCostItem,
operator.ImportCostScheduleCsv,
operator.RefreshCostScheduleCsv,
operator.RemoveCostScheduleCsvLink,
operator.LoadCostItemElementQuantities,
operator.LoadCostItemQuantities,
operator.ShowAssignedCostRate,
@@ -84,7 +86,6 @@ classes = (
prop.CostItem,
prop.CostItemQuantity,
prop.CostItemType,
prop.CostSchedulesMapping,
prop.ScheduleColumn,
prop.BIMCostProperties,
ui.BIM_PT_cost_schedules,
+21
View File
@@ -56,9 +56,29 @@ class CostSchedulesData:
"cost_values": cls.cost_values(),
"quantity_types": cls.quantity_types(),
"currency": cls.currency(),
"csv_filepaths": cls.get_csv_filepaths(),
}
cls.is_loaded = True
@classmethod
def get_csv_filepaths(cls) -> dict[int, str]:
"""Get CSV filepaths for cost schedules from document references."""
filepaths = {}
cost_docs_document = tool.Cost.get_or_create_cost_documents()
if cost_docs_document:
references = tool.Document.get_document_references(cost_docs_document)
for reference in references:
if reference.Description and "Cost Schedule ID:" in reference.Description:
schedule_id_str = reference.Description.split("Cost Schedule ID:")[1].strip()
try:
schedule_id = int(schedule_id_str)
filepaths[schedule_id] = reference.Location
except ValueError:
pass
return filepaths
@classmethod
def currency(cls) -> Union[Currency, None]:
unit = tool.Unit.get_project_currency_unit()
@@ -171,6 +191,7 @@ class CostSchedulesData:
# parametric_quantities.extend(quantities)
data["TotalCostQuantity"] = ifcopenshell.util.cost.get_total_quantity(cost_item)
data["UnitSymbol"] = "-"
data["QuantityType"] = None
quantities: list[ifcopenshell.entity_instance] = cost_item.CostQuantities
if quantities:
quantity = quantities[0]
+147 -5
View File
@@ -25,6 +25,8 @@ 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
@@ -545,11 +547,16 @@ class SelectCostScheduleProducts(bpy.types.Operator):
class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
bl_idname = "bim.import_cost_schedule_csv"
bl_label = "Import Cost Schedule CSV"
bl_description = "Import cost schdule from the provided .csv file."
bl_description = "Import cost schedule from the provided .csv file."
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
is_schedule_of_rates: bpy.props.BoolProperty(name="Is Schedule Of Rates", default=False)
use_relative_path: bpy.props.BoolProperty(
name="Use Relative Path",
description="Store the CSV filepath relative to the currently opened IFC file",
default=False,
)
@classmethod
def poll(cls, context):
@@ -560,22 +567,82 @@ class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator)
return True
def _execute(self, context):
cost_schedule = core.import_cost_schedule_csv(tool.Cost, self.filepath, self.is_schedule_of_rates)
core.add_csv_filepath(tool.Cost, self.filepath, self.is_schedule_of_rates, cost_schedule)
return {"FINISHED"}
store_path = self.filepath
if self.use_relative_path:
store_path = tool.Ifc.get_uri(self.filepath, use_relative_path=True)
resolved_path = Path(tool.Ifc.resolve_uri(self.filepath))
if not resolved_path.exists():
self.report({"ERROR"}, f"File does not exist: '{store_path}' (resolved to '{resolved_path}')")
return {"CANCELLED"}
cost_schedule = core.import_cost_schedule_csv(tool.Cost, str(resolved_path), self.is_schedule_of_rates)
if cost_schedule:
core.add_csv_filepath(tool.Cost, store_path, self.is_schedule_of_rates, cost_schedule)
return {"FINISHED"}
return {"CANCELLED"}
def draw(self, context):
row = self.layout.row()
row.prop(self, "is_schedule_of_rates")
row = self.layout.row()
row.prop(self, "use_relative_path")
class RefreshCostScheduleCsv(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.refresh_cost_schedule_csv"
bl_label = "Refresh Cost Schedule CSV"
bl_description = "Refresh cost schedule data from the associated CSV file"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
props = tool.Cost.get_cost_props()
if not props.active_cost_schedule_id:
cls.poll_message_set("No active cost schedule")
return False
file_path = tool.Cost.get_cost_schedule_csv_filepath(props.active_cost_schedule_id)
if not file_path:
cls.poll_message_set("No CSV file associated with this cost schedule")
return False
return True
def _execute(self, context):
core.refresh_cost_schedule_csv(tool.Ifc, tool.Cost)
core.refresh_cost_schedule_csv(tool.Cost)
return {"FINISHED"}
class RemoveCostScheduleCsvLink(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_cost_schedule_csv_link"
bl_label = "Remove CSV Link"
bl_description = "Remove the link to the CSV file and delete the related document reference"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
props = tool.Cost.get_cost_props()
if not props.active_cost_schedule_id:
cls.poll_message_set("No active cost schedule")
return False
file_path = tool.Cost.get_cost_schedule_csv_filepath(props.active_cost_schedule_id)
if not file_path:
cls.poll_message_set("No CSV file associated with this cost schedule")
return False
return True
def _execute(self, context):
core.remove_cost_schedule_csv_link(tool.Cost)
return {"FINISHED"}
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
class AddCostColumn(bpy.types.Operator):
bl_idname = "bim.add_cost_column"
bl_label = "Add Cost Column"
@@ -785,6 +852,81 @@ class ExportCostSchedules(bpy.types.Operator, ExportHelper):
self.layout.label(text="Select a directory.")
class ExportCostSchedulesToPDF(bpy.types.Operator, ExportHelper):
bl_idname = "bim.export_cost_schedules_to_pdf"
bl_label = "Export Cost Schedule to PDF"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Print chosen cost schedule to pdf."
filename_ext = ".pdf"
filter_glob: bpy.props.StringProperty(default="*.pdf", options={"HIDDEN"}, maxlen=255)
cost_schedules_items = []
def get_cost_schedules_enum_items(self, context):
return ExportCostSchedulesToPDF.cost_schedules_items
cost_schedules_enum: bpy.props.EnumProperty(
name="",
description="Choose IfcCostSchedule to print",
items=get_cost_schedules_enum_items,
)
should_print_summary: bpy.props.BoolProperty(
name="Should print summary",
description="Print summary at the end of the document",
default=True,
)
def draw(self, context):
layout = self.layout
box = layout.box()
box.label(text="Select Ifc Cost Schedule:")
box.prop(self, "cost_schedules_enum", text="")
layout.separator()
box = layout.box()
box.label(text="Export properties:")
box.prop(self, "should_print_summary")
@classmethod
def poll(cls, context):
try:
import typst
return True
except:
cls.poll_message_set(
"Typst not available.\nIt can be installed from Quality and\nControl -> Debug and using 'typst' with Pip Install.\n(Run Blender as Administrator)"
)
return False
def invoke(self, context, event):
ExportCostSchedulesToPDF.cost_schedules_items.clear()
file = tool.Ifc.get()
schedules = file.by_type("IfcCostSchedule")
for schedule in schedules:
ExportCostSchedulesToPDF.cost_schedules_items.append(
(
str(schedule.id()),
"{} ({})".format(
schedule.Name if schedule.Name is not None else "Unnamed",
schedule.PredefinedType if schedule.PredefinedType is not None else "UNTYPED",
),
"",
)
)
return ExportHelper.invoke(self, context, event)
def execute(self, context):
file = tool.Ifc.get()
self.props = tool.Cost.get_cost_props()
cost_schedule = file.by_id(int(self.cost_schedules_enum))
options = {"should_print_summary": self.should_print_summary}
core.export_cost_schedules_to_pdf(
tool.Cost, filepath=self.filepath, cost_schedule=cost_schedule, options=options
)
return {"FINISHED"}
class ClearCostItemAssignments(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.clear_cost_item_assignments"
bl_label = "Clear Cost Item Product Assignments"
-11
View File
@@ -175,15 +175,6 @@ class CostItemQuantity(PropertyGroup):
total_cost_quantity: float
class CostSchedulesMapping(PropertyGroup):
cost_schedule_id: IntProperty(name="cost_schedule_id")
csv_filepath: StringProperty(name="filepath")
if TYPE_CHECKING:
cost_schedule_id: int
csv_filepath: str
class CostItemType(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
@@ -289,7 +280,6 @@ 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=CostSchedulesMapping)
if TYPE_CHECKING:
cost_schedule_predefined_types: str
@@ -344,7 +334,6 @@ class BIMCostProperties(PropertyGroup):
show_cost_item_operators: bool
currency: str
custom_currency: str
cost_schedule_files: bpy.types.bpy_prop_collection_idprop[CostSchedulesMapping]
@property
def active_cost_item(self) -> Union[CostItem, None]:
+9 -11
View File
@@ -53,6 +53,7 @@ class BIM_PT_cost_schedules(Panel):
if CostSchedulesData.data["total_cost_schedules"]:
row.alignment = "RIGHT"
row.operator("bim.export_cost_schedules", icon="EXPORT", text="Export All Schedules")
row.operator("bim.export_cost_schedules_to_pdf", icon="OUTPUT", text="")
row = self.layout.row(align=True)
row.label(text=f"{CostSchedulesData.data['total_cost_schedules']} Cost Schedules Found", icon="TEXT")
else:
@@ -76,17 +77,14 @@ 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_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_schedule_id == self.props.active_cost_schedule_id
),
None,
)
row_1.label(text=file)
csv_filepaths = CostSchedulesData.data["csv_filepaths"]
file_path = csv_filepaths.get(self.props.active_cost_schedule_id)
if file_path:
row_1.label(text=file_path)
row_1.operator("bim.refresh_cost_schedule_csv", icon="FILE_REFRESH", text="")
row_1.operator("bim.remove_cost_schedule_csv_link", icon="X", text="")
else:
row_1.label(text="No CSV file found")
row_1.operator("bim.import_cost_schedule_csv", icon="IMPORT", text="")
@@ -226,7 +224,7 @@ class BIM_PT_cost_schedules(Panel):
quantities = CostSchedulesData.data["cost_quantities"]
row = self.layout.row(align=True)
# In IFC, all quantities of IfcCostTime should have 1 type.
if quantities:
if quantities and cost_item.get("QuantityType"):
quantity_class = cost_item["QuantityType"]
row.label(text=quantity_class)
else:
@@ -239,7 +239,7 @@ class ProfileImportIFC(bpy.types.Operator):
import cProfile
import pstats
profile_file = Path(profile_filename)
profile_file = Path(self.profile_filename)
cProfile.run("import bpy; bpy.ops.bim.load_project_elements()", str(profile_file))
p = pstats.Stats(str(profile_file))
p.sort_stats("cumulative").print_stats(50)
@@ -824,6 +824,10 @@ class PipInstall(bpy.types.Operator):
bl_description = "Installs a package from PyPI"
name: bpy.props.StringProperty()
@classmethod
def description(cls, context, properties):
return f"Installs a package from PyPI: '{properties.name}'."
def execute(self, context):
blender_path = Path(bpy.app.binary_path).parent
target = next(
+2 -2
View File
@@ -86,8 +86,8 @@ class SheetsData:
project = tool.Ifc.get().by_type("IfcProject")[0]
titleblocks_dir = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir")
if not titleblocks_dir:
props = tool.Drawing.get_document_props()
titleblocks_dir = props.titleblocks_dir
prefs = tool.Blender.get_addon_preferences()
titleblocks_dir = prefs.doc.titleblocks_dir
titleblocks_dir = tool.Ifc.resolve_uri(titleblocks_dir)
if os.path.exists(titleblocks_dir):
files.extend([str(f.stem) for f in Path(titleblocks_dir).glob("*.svg")])
@@ -426,8 +426,8 @@ class BaseDecorator:
# font_size = 16 <-- this is a good default
# TODO: need to synchronize it better with svg
props = tool.Drawing.get_document_props()
magic_font_scale = props.magic_font_scale
prefs = tool.Blender.get_addon_preferences()
magic_font_scale = prefs.doc.magic_font_scale
font_size_px = int(magic_font_scale * mm_to_px) * font_size_mm / 2.5
pos = pos - line_no * font_size_px * rotation_matrix[1]
@@ -1661,7 +1661,7 @@ 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_str = self.addon_prefs.doc.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"]:
@@ -1959,8 +1959,8 @@ class DecorationsHandler:
for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
self.decorators[object_type] = self.decorators["FALL"]
self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"]
props = tool.Drawing.get_document_props()
if drawing_font := props.drawing_font:
prefs = tool.Blender.get_addon_preferences()
if drawing_font := prefs.doc.drawing_font:
drawing_font_path = tool.Blender.get_data_dir_path(Path("fonts") / drawing_font)
if drawing_font_path.is_file():
font_id = blf.load(drawing_font_path.__str__())
@@ -334,7 +334,15 @@ class CreateDrawing(bpy.types.Operator):
annotation_svg = None
with profile("Generate underlay"):
underlay_svg = self.generate_underlay(context)
if ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasUnderlay"):
drawing_style = self.cprops.get_active_drawing_style()
if not drawing_style:
self.report(
{"ERROR"},
f"Failed to create drawing '{self.drawing.Name}' - drawing has underlay but there's no active drawing underlay style.",
)
return {"FINISHED"}
underlay_svg = self.generate_underlay(context)
with profile("Generate linework"):
if tool.Drawing.is_camera_orthographic():
@@ -874,6 +882,7 @@ class CreateDrawing(bpy.types.Operator):
cached_linework -= edited_guids
bim_props = tool.Blender.get_bim_props()
prefs = tool.Blender.get_addon_preferences()
files = {bim_props.ifc_file: tool.Ifc.get()}
props = tool.Project.get_project_props()
@@ -890,7 +899,7 @@ class CreateDrawing(bpy.types.Operator):
# Don't use draw.main() just whilst we're prototyping and experimenting
# TODO: hash paths are never used
ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest()
ifc_cache_path = os.path.join(bim_props.cache_dir, f"{ifc_hash}.h5")
ifc_cache_path = os.path.join(prefs.cache_dir, f"{ifc_hash}.h5")
self.serialiser.setFile(ifc)
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element, ifc_file=ifc)
@@ -1470,6 +1479,9 @@ class CreateDrawing(bpy.types.Operator):
classes.add(el.attrib["{http://www.ifcopenshell.org/ns}guid"])
is_closed_polygon = False
for path in el.findall("{http://www.w3.org/2000/svg}path"):
# Temporary hack.
if "d" not in path.attrib:
continue
for subpath in path.attrib["d"].split("M")[1:]:
subpath_co = "M" + subpath.strip(" Z")
# Round due to inaccuracies from Blender meshes and bisection
@@ -1726,7 +1738,9 @@ class OpenLayout(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.props = tool.Drawing.get_document_props()
sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)
sheet_item = tool.Drawing.get_active_sheet_item()
assert sheet_item
sheet = tool.Ifc.get().by_id(sheet_item.ifc_definition_id)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.update_sheet_drawing_sizes(sheet)
core.open_layout(tool.Drawing, sheet=sheet)
@@ -1790,14 +1804,20 @@ class OpenSheet(bpy.types.Operator):
tool.Ifc.get().by_id(s.ifc_definition_id) for s in self.props.sheets if s.is_sheet and s.is_selected
]
else:
sheets = [tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)]
sheet_item = tool.Drawing.get_active_sheet_item()
assert sheet_item
sheets = [tool.Ifc.get().by_id(sheet_item.ifc_definition_id)]
sheet_uris = []
sheets_not_found = []
sheet_uris: list[str] = []
sheets_not_found: list[str] = []
warnings: list[tool.Drawing.SheetWarningType] = []
for sheet in sheets:
if not sheet.is_a("IfcDocumentInformation"):
continue
warnings.extend(sheets_warnings := tool.Drawing.validate_sheet_files(sheet))
if sheets_warnings:
continue
sheet_builder = sheeter.SheetBuilder()
references = sheet_builder.build(sheet)
sheet_uri = references["SHEET"]
@@ -1807,6 +1827,11 @@ class OpenSheet(bpy.types.Operator):
if not os.path.exists(sheet_uri):
sheets_not_found.append(sheet.Name)
if warnings:
self.report({"ERROR"}, f"There were errors opening sheets. See system console for the details.")
print("-" * 10)
print("\n".join(str(w) for w in warnings))
if sheets_not_found:
msg = "Some sheets .svg/.pdf files were not found, need to create them first: \n{}.".format(
"\n".join(sheets_not_found)
@@ -1832,8 +1857,8 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
def poll(cls, context):
props = tool.Drawing.get_document_props()
# Won't be visible in UI anyway.
bim_props = tool.Blender.get_bim_props()
if not props.sheets or not bim_props.data_dir:
prefs = tool.Blender.get_addon_preferences()
if not props.sheets or not prefs.data_dir:
return False
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
@@ -1846,7 +1871,7 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
assert active_drawing
ifc_file = tool.Ifc.get()
active_sheet = tool.Drawing.get_active_sheet(context)
active_sheet = tool.Drawing.get_active_sheet()
drawing = tool.Ifc.get().by_id(active_drawing.ifc_definition_id)
drawing_reference = tool.Drawing.get_drawing_document(drawing)
@@ -1938,8 +1963,8 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
if not tool.Drawing.get_active_sheet_item(is_sheet=True):
cls.poll_message_set("No sheet selected.")
return False
bim_props = tool.Blender.get_bim_props()
return props.sheets and bim_props.data_dir
prefs = tool.Blender.get_addon_preferences()
return props.sheets and prefs.data_dir
def invoke(self, context, event):
# opening all sheets on shift+click
@@ -1960,9 +1985,18 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
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]
else:
sheets = [tool.Ifc.get().by_id(props.sheets[props.active_sheet_index].ifc_definition_id)]
sheet_item = tool.Drawing.get_active_sheet_item()
assert sheet_item
sheets = [tool.Ifc.get().by_id(sheet_item.ifc_definition_id)]
warnings: list[tool.Drawing.SheetWarningType] = []
n_sheets_created = 0
for sheet in sheets:
warnings.extend(sheet_warnings := tool.Drawing.validate_sheet_files(sheet))
if sheet_warnings:
continue
# Update any drawing boundary changes
sheet_builder = sheeter.SheetBuilder()
sheet_builder.update_sheet_drawing_sizes(sheet)
@@ -2023,8 +2057,16 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
tool.Drawing.open_with_user_command(tool.Blender.get_addon_preferences().pdf_command, pdf)
else:
tool.Drawing.open_with_user_command(tool.Blender.get_addon_preferences().svg_command, svg)
n_sheets_created += 1
if not self.open_viewer:
self.report({"INFO"}, f"{len(sheets)} sheets created...")
self.report({"INFO"}, f"{n_sheets_created} sheets created...")
if warnings:
self.report({"ERROR"}, f"There were errors creating sheets. See system console for the details.")
print("-" * 10)
print("\n".join(str(w) for w in warnings))
class SelectAllDrawings(bpy.types.Operator):
@@ -2109,7 +2151,10 @@ class ActivateModel(bpy.types.Operator):
bl_idname = "bim.activate_model"
bl_label = "Activate Model"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Activate the model view, hide all annotations"
bl_description = (
"Activate the model view.\n\n"
"Show all objects (and apply status filters if they were enabled before) and hide all annotations."
)
def execute(self, context):
dprops = tool.Drawing.get_document_props()
@@ -2139,7 +2184,7 @@ class ActivateModel(bpy.types.Operator):
if not bpy.app.background:
with context.temp_override(**tool.Blender.get_viewport_context()):
bpy.ops.object.hide_view_clear()
bpy.ops.bim.activate_status_filters()
bpy.ops.bim.activate_status_filters(only_if_enabled=True)
for obj in context.visible_objects:
element = tool.Ifc.get_entity(obj)
@@ -2275,7 +2320,6 @@ class RemoveDrawing(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -2706,13 +2750,13 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
if not props.schedules:
cls.poll_message_set("No schedule selected.")
return False
bim_props = tool.Blender.get_bim_props()
return props.schedules and props.sheets and bim_props.data_dir
prefs = tool.Blender.get_addon_preferences()
return props.schedules and props.sheets and prefs.data_dir
def _execute(self, context):
props = tool.Drawing.get_document_props()
active_schedule = props.schedules[props.active_schedule_index]
active_sheet = tool.Drawing.get_active_sheet(context)
active_sheet = tool.Drawing.get_active_sheet()
ifc_file = tool.Ifc.get()
schedule = tool.Ifc.get().by_id(active_schedule.ifc_definition_id)
if tool.Ifc.get_schema() == "IFC2X3":
@@ -2780,7 +2824,7 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = tool.Drawing.get_document_props()
active_reference = props.references[props.active_reference_index]
active_sheet = tool.Drawing.get_active_sheet(context)
active_sheet = tool.Drawing.get_active_sheet()
ifc_file = tool.Ifc.get()
extref = tool.Ifc.get().by_id(active_reference.ifc_definition_id)
if tool.Ifc.get_schema() == "IFC2X3":
@@ -3010,7 +3054,7 @@ class AddTextLiteral(bpy.types.Operator):
"Path": "RIGHT",
"BoxAlignment": "bottom_left",
}
# emulates `bonsai.bim.helper.import_attributes2(ifc_literal, literal_props.attributes)`
# emulates `bonsai.bim.helper.import_attributes(ifc_literal, literal_props.attributes)`
for attr_name in literal_attr_values:
attr = literal_attributes.add()
attr.name = attr_name
@@ -3163,7 +3207,7 @@ class LoadSheets(bpy.types.Operator, tool.Ifc.Operator):
core.load_sheets(tool.Drawing)
props = tool.Drawing.get_document_props()
sheets_not_found = []
warnings: list[tool.Drawing.SheetWarningType] = []
for sheet_prop in props.sheets:
if not sheet_prop.is_sheet:
continue
@@ -3174,12 +3218,14 @@ class LoadSheets(bpy.types.Operator, tool.Ifc.Operator):
filepath = Path(document_uri)
if not filepath.is_file():
sheet_name = f"{sheet_prop.identification} - {sheet_prop.name}"
sheets_not_found.append(f'"{sheet_name}" - {document_uri}')
core.regenerate_sheet(tool.Drawing, sheet)
res = core.regenerate_sheet(tool.Drawing, sheet)
if res:
warnings.extend(res)
if sheets_not_found:
self.report({"ERROR"}, "Some sheets svg files are missing:\n" + "\n".join(sheets_not_found))
if warnings:
self.report({"WARNING"}, f"There were warnings loading sheets. See system console for the details.")
print("-" * 10)
print("\n".join(str(w) for w in warnings))
class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
@@ -3194,8 +3240,9 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
def invoke(self, context, event):
assert context.window_manager
self.props = tool.Drawing.get_document_props()
sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)
sheet_item = tool.Drawing.get_active_sheet_item()
assert sheet_item
sheet = tool.Ifc.get().by_id(sheet_item.ifc_definition_id)
if sheet.is_a("IfcDocumentInformation"):
self.document_type = "SHEET"
self.name = sheet.Name
@@ -3223,15 +3270,17 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
row.prop(self, "identification", text="Identification")
def _execute(self, context):
self.props = tool.Drawing.get_document_props()
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)
sheet_item = tool.Drawing.get_active_sheet_item()
assert sheet_item
sheet = tool.Ifc.get().by_id(sheet_item.ifc_definition_id)
if self.document_type == "SHEET":
core.rename_sheet(tool.Ifc, tool.Drawing, sheet=sheet, identification=self.identification, name=self.name)
elif self.document_type == "EMBEDDED":
core.rename_reference(tool.Ifc, tool.Drawing, reference=sheet, identification=self.identification)
elif self.document_type == "TITLEBLOCK":
titleblock = self.props.titleblock
titleblock = props.titleblock
reference = sheet
sheet = tool.Drawing.get_reference_document(reference)
assert sheet
@@ -423,39 +423,6 @@ class DocProperties(PropertyGroup):
active_sheet_index: IntProperty(name="Active Sheet Index")
drawing_styles: CollectionProperty(name="Drawing Styles", type=DrawingStyle)
should_draw_decorations: BoolProperty(name="Should Draw Decorations", update=update_should_draw_decorations)
sheets_dir: StringProperty(default=os.path.join("sheets") + os.path.sep, name="Default Sheets Directory")
layouts_dir: StringProperty(default=os.path.join("layouts") + os.path.sep, name="Default Layouts Directory")
titleblocks_dir: StringProperty(
default=os.path.join("layouts", "titleblocks") + os.path.sep, name="Default Titleblocks Directory"
)
drawings_dir: StringProperty(default=os.path.join("drawings") + os.path.sep, name="Default Drawings Directory")
stylesheet_path: StringProperty(
default=os.path.join("drawings", "assets", "default.css"), name="Default Stylesheet"
)
schedules_stylesheet_path: StringProperty(
default=os.path.join("drawings", "assets", "schedule.css"), name="Default Stylesheet for Schedules"
)
markers_path: StringProperty(default=os.path.join("drawings", "assets", "markers.svg"), name="Default Markers")
symbols_path: StringProperty(default=os.path.join("drawings", "assets", "symbols.svg"), name="Default Symbols")
patterns_path: StringProperty(default=os.path.join("drawings", "assets", "patterns.svg"), name="Default Patterns")
shadingstyles_path: StringProperty(
default=os.path.join("drawings", "assets", "shading_styles.json"), name="Default Shading Styles"
)
shadingstyle_default: StringProperty(default="Blender Default", name="Default Shading Style")
drawing_font: StringProperty(default="OpenGost Type B TT.ttf", name="Drawing Font")
magic_font_scale: bpy.props.FloatProperty(default=0.004118616, name="Font Scale Factor")
imperial_precision: StringProperty(default="1/32", name="Imperial Precision")
tolerance: bpy.props.FloatProperty(default=0.00001, name="A tolerance used when selecting objects")
classes_to_wireframe: StringProperty(
default="IfcVirtualElement",
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
@@ -480,23 +447,6 @@ class DocProperties(PropertyGroup):
active_sheet_index: int
drawing_styles: bpy.types.bpy_prop_collection_idprop[DrawingStyle]
should_draw_decorations: bool
sheets_dir: str
layouts_dir: str
titleblocks_dir: str
drawings_dir: str
stylesheet_path: str
schedules_stylesheet_path: str
markers_path: str
symbols_path: str
patterns_path: str
shadingstyles_path: str
shadingstyle_default: str
drawing_font: str
magic_font_scale: float
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
@@ -74,10 +74,11 @@ class Scheduler:
self.schedule_xlsx(infile, outfile)
def parse_css(self, infile: str) -> None:
props = tool.Drawing.get_document_props()
prefs = tool.Blender.get_addon_preferences()
stylesheet_path = os.path.splitext(infile)[0] + ".css"
if not os.path.exists(stylesheet_path):
stylesheet_rel_path = props.schedules_stylesheet_path
stylesheet_rel_path = prefs.doc.schedules_stylesheet_path
ifc_file_path = os.path.dirname(tool.Ifc.get_path())
stylesheet_path = ifc_file_path + "\\" + stylesheet_rel_path
if not os.path.exists(stylesheet_path):
@@ -285,9 +285,7 @@ class SheetBuilder:
title.attrib["width"] = str(self.convert_to_mm(title_root.attrib["width"]))
title.attrib["height"] = str(self.convert_to_mm(title_root.attrib["height"]))
def build(self, sheet: ifcopenshell.entity_instance) -> dict:
self.references = {"SHEET": None, "RASTER": []}
def build(self, sheet: ifcopenshell.entity_instance) -> dict[str, str]:
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
assert layout_path
self.layout_dir = os.path.dirname(layout_path)
@@ -313,9 +311,7 @@ class SheetBuilder:
with open(sheet_path, "wb") as output:
tree.write(output)
self.references["SHEET"] = sheet_path
return self.references
return {"SHEET": sheet_path}
def build_titleblock(self, root: ET.Element, sheet: ifcopenshell.entity_instance) -> None:
titleblock = root.findall(f'{SVG}g[@data-type="titleblock"]')[0]
+4 -3
View File
@@ -102,7 +102,7 @@ class BIM_PT_camera(Panel):
box = self.layout.box()
box.label(
text=f"Resulting image size is {render.resolution_x} x {render.resolution_y} ({round(megapixels, 2)} MP).",
icon="WARNING_LARGE",
icon="ERROR",
)
box.label(
text=(f"Which is more than {MEGAPIXELS_WARNING_THRESHOLD} megapixels and will require a lot of VRAM.")
@@ -419,6 +419,7 @@ class BIM_PT_sheets(Panel):
draw_project_not_saved_ui(self)
return
assert self.layout
self.props = tool.Drawing.get_document_props()
if not self.props.is_editing_sheets:
@@ -432,8 +433,8 @@ class BIM_PT_sheets(Panel):
row.operator("bim.add_sheet", text="", icon="ADD")
row.operator("bim.disable_editing_sheets", text="", icon="CANCEL")
if self.props.sheets and self.props.active_sheet_index < len(self.props.sheets):
active_sheet = self.props.sheets[self.props.active_sheet_index]
active_sheet = tool.Drawing.get_active_sheet_item()
if active_sheet:
row = self.layout.row(align=True)
row2 = row.row(align=True)
if active_sheet.is_sheet:
@@ -16,6 +16,8 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
# pyright: reportAttributeAccessIssue=false
import bpy
from . import ui, prop, operator
+10 -9
View File
@@ -36,24 +36,25 @@ class ExecuteIfcFM(bpy.types.Operator, ExportHelper):
@property
def filename_ext(self) -> str:
props = bpy.context.scene.BIMFMProperties
props = tool.Blender.get_fm_props()
return f".{props.format}"
def draw(self, context):
assert self.layout
layout = self.layout
props = context.scene.BIMFMProperties
props = tool.Blender.get_fm_props()
layout.prop(props, "format")
@classmethod
def poll(cls, context):
props = context.scene.BIMFMProperties
props = tool.Blender.get_fm_props()
if not props.should_load_from_memory and not props.ifc_files.single_file:
cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.")
return False
return True
def execute(self, context):
props = context.scene.BIMFMProperties
props = tool.Blender.get_fm_props()
ifc_file = tool.Ifc.get()
filepaths = []
if ifc_file and props.should_load_from_memory:
@@ -95,7 +96,7 @@ class SelectFMSpreadsheetFiles(bpy.types.Operator, ImportHelper):
files: bpy.props.CollectionProperty(name="File Path", type=bpy.types.OperatorFileListElement)
def execute(self, context):
props = context.scene.BIMFMProperties
props = tool.Blender.get_fm_props()
props.spreadsheet_files.clear()
dirname = os.path.dirname(self.filepath)
for f in self.files:
@@ -112,24 +113,24 @@ class ExecuteIfcFMFederate(bpy.types.Operator, ExportHelper):
@property
def filename_ext(self) -> str:
props = bpy.context.scene.BIMFMProperties
props = tool.Blender.get_fm_props()
return f".{props.format}"
def draw(self, context):
layout = self.layout
props = context.scene.BIMFMProperties
props = tool.Blender.get_fm_props()
layout.prop(props, "format")
@classmethod
def poll(cls, context):
props = context.scene.BIMFMProperties
props = tool.Blender.get_fm_props()
if not props.spreadsheet_files:
cls.poll_message_set("No spreadsheet files selected.")
return False
return True
def execute(self, context):
props = context.scene.BIMFMProperties
props = tool.Blender.get_fm_props()
parser = ifcfm.Parser(preset=props.engine)
parser.federate([f.name for f in props.spreadsheet_files])
writer = ifcfm.Writer(parser)
+10 -1
View File
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
from bonsai.bim.module.fm.data import FMData
from bonsai.bim.prop import MultipleFileSelect, StrProperty
from bpy.types import PropertyGroup
@@ -30,9 +31,10 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal
def get_engine(self, context):
def get_engine(self: "BIMFMProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
if not FMData.is_loaded:
FMData.load()
return FMData.data["engine"]
@@ -57,3 +59,10 @@ class BIMFMProperties(PropertyGroup):
default="ods",
options=set(),
)
if TYPE_CHECKING:
ifc_files: MultipleFileSelect
spreadsheet_files: bpy.types.bpy_prop_collection_idprop[StrProperty]
should_load_from_memory: bool
engine: str
format: Literal["csv", "xlsx", "ods"]
+2 -2
View File
@@ -33,11 +33,11 @@ class BIM_PT_fm(Panel):
if not FMData.is_loaded:
FMData.load()
assert self.layout
layout = self.layout
layout.use_property_split = True
scene = context.scene
props = scene.BIMFMProperties
props = tool.Blender.get_fm_props()
if tool.Ifc.get():
row = layout.row()
@@ -193,9 +193,8 @@ def update_is_adding_representation_layer(self: "BIMObjectGeometryProperties", c
return
return
if "representation_layer" in self:
del self["representation_layer"]
del self["is_adding_representation_layer"]
self.property_unset("representation_layer")
self.property_unset("is_adding_representation_layer")
def update_is_editing_item_layer(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> None:
@@ -216,9 +215,8 @@ def update_is_editing_item_layer(self: "BIMObjectGeometryProperties", context: b
self.representation_item_layer = str(layer.id())
return
if "representation_item_layer" in self:
del self["representation_item_layer"]
del self["is_editing_item_layer"]
self.property_unset("representation_item_layer")
self.property_unset("is_editing_item_layer")
class BIMObjectGeometryProperties(PropertyGroup):
+12 -4
View File
@@ -525,14 +525,22 @@ class BIM_PT_derived_coordinates(Panel):
text += "*"
row.operator("bim.edit_object_placement", text=text, icon="EXPORT")
row = self.layout.row()
row = self.layout.row(align=True)
row.label(text="XYZ Dimensions")
row = self.layout.row(align=True)
row.enabled = False
row.prop(context.active_object, "dimensions", text="X", index=0, slider=True)
row.prop(context.active_object, "dimensions", text="Y", index=1, slider=True)
row.prop(context.active_object, "dimensions", text="Z", index=2, slider=True)
area_3d = next((area for area in context.screen.areas if area.type == "VIEW_3D"), None)
space_3d = next((space for space in area_3d.spaces if space.type == "VIEW_3D"), None)
if bpy.context.scene.BIMModelProperties.show_bounding_box:
for axis, icon, idx in [("X", "STRIP_COLOR_01", 0), ("Y", "STRIP_COLOR_04", 1), ("Z", "STRIP_COLOR_05", 2)]:
row.label(text="", icon=icon)
row.prop(context.active_object, "dimensions", text=axis, index=idx)
else:
row.prop(context.active_object, "dimensions", text="X", index=0)
row.prop(context.active_object, "dimensions", text="Y", index=1)
row.prop(context.active_object, "dimensions", text="Z", index=2)
row = self.layout.row(align=True)
row.label(text="Min Global Z")
+16 -5
View File
@@ -23,6 +23,7 @@ import ifcopenshell.util.element
import bonsai.bim.helper
import bonsai.tool as tool
import json
from natsort import natsorted
class LoadGroups(bpy.types.Operator, tool.Ifc.Operator):
@@ -30,6 +31,8 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Load Groups"
bl_options = {"REGISTER", "UNDO"}
expanded_groups: list[int]
def _execute(self, context):
self.props = tool.Blender.get_group_props()
self.expanded_groups = json.loads(self.props.expanded_groups_json)
@@ -38,7 +41,7 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator):
groups = [
group for group in tool.Ifc.get().by_type("IfcGroup", include_subtypes=False) if not group.HasAssignments
]
sorted_groups = sorted(groups, key=lambda group: group.Name or "Unnamed")
sorted_groups = natsorted(groups, key=lambda group: group.Name or "Unnamed")
for group in sorted_groups:
self.load_group(group)
@@ -47,21 +50,22 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.disable_editing_group()
return {"FINISHED"}
def load_group(self, group, tree_depth=0):
def load_group(self, group: ifcopenshell.entity_instance, tree_depth: int = 0) -> None:
new = self.props.groups.add()
new.ifc_definition_id = group.id()
new.name = group.Name or "Unnamed"
new["name"] = group.Name or "Unnamed"
new.tree_depth = tree_depth
new.has_children = False
new.is_expanded = group.id() in self.expanded_groups
related_groups: list[ifcopenshell.entity_instance]
related_groups = [
related_object
for rel in group.IsGroupedBy or []
for related_object in rel.RelatedObjects
if related_object.is_a("IfcGroup")
]
sorted_related_groups = sorted(related_groups, key=lambda group: group.Name or "Unnamed")
sorted_related_groups = natsorted(related_groups, key=lambda group: group.Name or "Unnamed")
for related_group in sorted_related_groups:
new.has_children = True
@@ -80,6 +84,7 @@ class ToggleGroup(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = tool.Blender.get_group_props()
expanded_groups: set[int]
expanded_groups = set(json.loads(props.expanded_groups_json))
if self.option == "Expand":
expanded_groups.add(self.ifc_definition_id)
@@ -108,6 +113,12 @@ class AddGroup(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
group: bpy.props.IntProperty()
@classmethod
def description(cls, context, properties) -> str:
if properties.group:
return "Add new subgroup to the active IfcGroup."
return "Add new IfcGroup."
def _execute(self, context):
result = ifcopenshell.api.group.add_group(tool.Ifc.get())
if self.group:
@@ -154,7 +165,7 @@ class EnableEditingGroup(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
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)
bonsai.bim.helper.import_attributes(tool.Ifc.get().by_id(self.group), props.group_attributes)
props.active_group_id = self.group
return {"FINISHED"}
+12 -1
View File
@@ -38,8 +38,18 @@ def update_active_group_index(self, context):
refresh_pset()
def update_name(self: "Group", context: object) -> None:
group = tool.Ifc.get_entity_by_id(self.ifc_definition_id)
# Theoretically group can be removed outside Group UI.
if not group:
return
if group.Name == self.name:
return
group.Name = self.name
class Group(PropertyGroup):
name: StringProperty(name="Name")
name: StringProperty(name="Name", update=update_name)
ifc_definition_id: IntProperty(name="IFC Definition ID")
is_expanded: BoolProperty(name="Is Expanded", default=False)
has_children: BoolProperty(name="Has Children", default=False)
@@ -60,6 +70,7 @@ class BIMGroupProperties(PropertyGroup):
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="[]")
"""JSON serialized list[group_id]."""
if TYPE_CHECKING:
group_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
+2 -2
View File
@@ -129,7 +129,7 @@ class BIM_UL_groups(UIList):
active_data,
active_propname,
index: int,
):
) -> None:
if item:
row = layout.row(align=True)
for i in range(0, item.tree_depth):
@@ -141,4 +141,4 @@ class BIM_UL_groups(UIList):
op.ifc_definition_id = item.ifc_definition_id
op.index = index
op.option = "Collapse" if item.is_expanded else "Expand"
row.label(text=item.name)
row.prop(item, "name", text="", emboss=False)
@@ -82,7 +82,7 @@ class EnableEditingLayer(bpy.types.Operator):
def execute(self, context):
props = tool.Layer.get_layer_props()
props.layer_attributes.clear()
bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.layer), props.layer_attributes)
bonsai.bim.helper.import_attributes(tool.Ifc.get().by_id(self.layer), props.layer_attributes)
props.active_layer_id = self.layer
return {"FINISHED"}
@@ -24,6 +24,7 @@ from pathlib import Path
import importlib
import importlib.util
import traceback
import pyradiance
@@ -58,12 +59,14 @@ classes = (
def register():
bpy.types.Scene.BIMRadianceExporeterProperies = bpy.props.PointerProperty(type=prop.RadianceExporterProperties)
bpy.types.Scene.BIMSolarProperties = bpy.props.PointerProperty(type=prop.BIMSolarProperties)
pyradiance_path = Path(get_pyradiance_path())
bin_path = pyradiance_path / "bin"
if bin_path.exists():
for file in bin_path.iterdir():
if file.is_file():
file.chmod(file.stat().st_mode | stat.S_IEXEC)
if pyradiance:
pyradiance_path = Path(get_pyradiance_path())
bin_path = pyradiance_path / "bin"
if bin_path.exists():
for file in bin_path.iterdir():
if file.is_file():
file.chmod(file.stat().st_mode | stat.S_IEXEC)
def unregister():
+1 -10
View File
@@ -18,12 +18,7 @@
import os
try:
import pyradiance as pr
except ImportError:
print("PyRadiance is not available. Rendering functionality will be disabled.")
pr = None
import pyradiance as pr
from datetime import datetime
import bpy
import bonsai.tool as tool
@@ -138,10 +133,6 @@ class RadianceRender(bpy.types.Operator):
bl_description = "Renders the scene using Radiance"
def execute(self, context):
if pr is None:
self.report({"ERROR"}, "PyRadiance is not available. Cannot perform rendering.")
return {"CANCELLED"}
print("Starting Radiance rendering process...")
props = tool.Blender.get_radiance_exporter_props()
resolution_x, resolution_y = props.radiance_resolution_x, props.radiance_resolution_y
@@ -293,9 +293,9 @@ class ObjectMaterialData:
if item.is_a("IfcMaterialLayer"):
total_thickness = item.LayerThickness
unit_system = bpy.context.scene.unit_settings.system
props = tool.Drawing.get_document_props()
prefs = tool.Blender.get_addon_preferences()
if unit_system == "IMPERIAL":
precision = props.imperial_precision
precision = prefs.doc.imperial_precision
else:
precision = None
formatted_thickness = format_distance(
@@ -330,11 +330,12 @@ class ObjectMaterialData:
elif cls.material.is_a("IfcMaterialLayerSet"):
layers = cls.material.MaterialLayers
thickness = sum([l.LayerThickness for l in layers or []])
props = tool.Drawing.get_document_props()
prefs = tool.Blender.get_addon_preferences()
assert bpy.context.scene
unit_system = bpy.context.scene.unit_settings.system
precision = None
if unit_system == "IMPERIAL":
precision = props.imperial_precision
precision = prefs.doc.imperial_precision
return format_distance(thickness, precision=precision, suppress_zero_inches=True, in_unit_length=True)
@classmethod
@@ -28,9 +28,12 @@ import bonsai.bim.helper
import bonsai.tool as tool
import bonsai.core.material as core
import bonsai.bim.module.model.profile as model_profile
from typing import Any, Union, TYPE_CHECKING
from typing import Any, Union, TYPE_CHECKING, Literal
from bonsai.bim.module.model import wall, slab
if TYPE_CHECKING:
from bonsai.bim.prop import Attribute
class LoadMaterials(bpy.types.Operator):
bl_idname = "bim.load_materials"
@@ -500,15 +503,17 @@ class EnableEditingAssignedMaterial(bpy.types.Operator):
props.material_set_attributes.clear()
if "Usage" in material.is_a():
bonsai.bim.helper.import_attributes2(
material, props.material_set_usage_attributes, callback=self.import_attributes
bonsai.bim.helper.import_attributes(
material, props.material_set_usage_attributes, callback=self.import_attributes_callback
)
bonsai.bim.helper.import_attributes2(material[0], props.material_set_attributes)
bonsai.bim.helper.import_attributes(material[0], props.material_set_attributes)
else:
bonsai.bim.helper.import_attributes2(material, props.material_set_attributes)
bonsai.bim.helper.import_attributes(material, props.material_set_attributes)
return {"FINISHED"}
def import_attributes(self, name, prop, data):
def import_attributes_callback(
self, name: str, prop: Union["Attribute", None], data: dict[str, Any]
) -> None | Literal[True]:
if name == "CardinalPoint":
# TODO: complain to buildingSMART
cardinal_point_map = {
@@ -532,6 +537,7 @@ class EnableEditingAssignedMaterial(bpy.types.Operator):
18: "right in line with the shear centre",
19: "top in line with the shear centre",
}
assert prop
prop.data_type = "enum"
prop.enum_items = json.dumps(cardinal_point_map)
if data[name]:
@@ -646,7 +652,7 @@ class EnableEditingMaterialSetItemProfile(bpy.types.Operator):
self.props.active_material_set_item_id = self.material_set_item
self.props.material_set_item_profile_attributes.clear()
profile = tool.Ifc.get().by_id(self.material_set_item).Profile
bonsai.bim.helper.import_attributes2(profile, self.props.material_set_item_profile_attributes)
bonsai.bim.helper.import_attributes(profile, self.props.material_set_item_profile_attributes)
return {"FINISHED"}
@@ -707,7 +713,7 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
self.props.material_set_item_material = str(material_set_item.Material.id())
self.props.material_set_item_attributes.clear()
bonsai.bim.helper.import_attributes2(material_set_item, self.props.material_set_item_attributes)
bonsai.bim.helper.import_attributes(material_set_item, self.props.material_set_item_attributes)
if material_set_item.is_a("IfcMaterialProfile"):
if material_set_item.Profile and material_set_item.Profile.ProfileName:
@@ -882,6 +888,9 @@ class EditMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
if not tool.Ifc.get():
cls.poll_message_set("No IFC project loaded")
return False
props = tool.Material.get_material_props()
active_style = tool.Blender.get_enum_safe(props, "styles")
if not active_style:
+10 -4
View File
@@ -20,6 +20,7 @@ import ifcopenshell
import bpy
from ifcopenshell.util.doc import get_entity_doc
import bonsai.tool as tool
from bonsai.bim.module.classification.data import MaterialClassificationsData
from bonsai.bim.module.material.data import MaterialsData, ObjectMaterialData
from bonsai.bim.module.profile.data import ProfileData
from bonsai.bim.prop import StrProperty, Attribute
@@ -157,10 +158,16 @@ class Material(PropertyGroup):
class BIMMaterialProperties(PropertyGroup):
def update_active_material_index(self, context: bpy.types.Context) -> None:
if not MaterialClassificationsData.is_loaded:
return
MaterialClassificationsData.data["references"] = MaterialClassificationsData.references()
is_editing: BoolProperty(name="Is Editing", default=False)
material_type: EnumProperty(items=get_material_types, update=update_material_type, name="Material Type")
materials: CollectionProperty(name="Materials", type=Material)
active_material_index: IntProperty(name="Active Material Index")
active_material_index: IntProperty(name="Active Material Index", update=update_active_material_index)
profiles: EnumProperty(items=get_profiles, name="Profiles")
active_material_id: IntProperty(name="Active Material ID")
material_attributes: CollectionProperty(name="Material Attributes", type=Attribute)
@@ -169,9 +176,8 @@ class BIMMaterialProperties(PropertyGroup):
contexts: EnumProperty(items=get_contexts, name="Contexts")
@property
def active_material(self):
if 0 <= self.active_material_index < len(self.materials):
return self.materials[self.active_material_index]
def active_material(self) -> Material | None:
return tool.Blender.get_active_uilist_element(self.materials, self.active_material_index)
if TYPE_CHECKING:
is_editing: bool
+2 -2
View File
@@ -48,7 +48,7 @@ class BIM_PT_materials(Panel):
MaterialsData.load()
self.props = tool.Material.get_material_props()
material = tool.Material.get_active_material_item()
material = self.props.active_material
material_id = material.ifc_definition_id if material else None
row = self.layout.row(align=True)
@@ -110,7 +110,7 @@ class BIM_PT_materials(Panel):
return
ifc_definition_id = self.props.active_material_id
if self.props.editing_material_type == "ATTRIBUTES":
bonsai.bim.helper.draw_attributes(self.props.material_attributes, self.layout)
bonsai.bim.helper.draw_attributes(self.props.material_attributes, self.layout, enable_search=True)
row = self.layout.row(align=True)
row.operator("bim.edit_material", text="Save Material", icon="CHECKMARK").material = ifc_definition_id
row.operator("bim.disable_editing_material", text="", icon="CANCEL")
+273 -1
View File
@@ -21,11 +21,11 @@ import bpy
import blf
import bpy
import gpu
import gpu_extras
import bmesh
import ifcopenshell
import bonsai.tool as tool
import math
import mathutils
from math import sin, cos, radians
from bpy.types import SpaceView3D
from bpy_extras import view3d_utils
@@ -36,6 +36,7 @@ from typing import Union
from bonsai.bim.module.drawing.helper import format_distance
from itertools import chain
from typing import Union, Any
from bpy_extras.view3d_utils import location_3d_to_region_2d
def transparent_color(color, alpha=0.1):
@@ -1123,3 +1124,274 @@ class FaceAreaDecorator:
self.draw_batch("POINTS", data["verts"], decorator_color)
self.draw_batch("LINES", data["verts"], decorator_color, data["edges"])
self.draw_batch("TRIS", data["verts"], transparent_color(decorator_color, alpha=0.5), data["tris"])
class BoundingBoxDecorator:
is_installed = False
handlers = []
def __init__(self):
context = bpy.context
theme = context.preferences.themes.items()[0][1]
self.decorator_color_x_axis = (*theme.user_interface.axis_x, 1)
self.decorator_color_y_axis = (*theme.user_interface.axis_y, 1)
self.decorator_color_z_axis = (*theme.user_interface.axis_z, 1)
self.decorator_color_wire = (*theme.view_3d.bone_solid, 1)
self.decorator_color_special = tool.Blender.get_addon_preferences().decorator_color_special
@classmethod
def install(cls, context):
if cls.is_installed:
cls.uninstall()
handler = cls()
cls.handlers.append(
bpy.types.SpaceView3D.draw_handler_add(
handler.draw_bounding_box_wire_cube, (context,), "WINDOW", "POST_VIEW"
)
)
cls.handlers.append(
bpy.types.SpaceView3D.draw_handler_add(handler.draw_dimension_text, (context,), "WINDOW", "POST_PIXEL")
)
cls.is_installed = True
@classmethod
def uninstall(cls):
for handler in cls.handlers:
try:
bpy.types.SpaceView3D.draw_handler_remove(handler, "WINDOW")
except Exception:
pass
cls.handlers.clear()
cls.is_installed = False
@staticmethod
def get_combined_bounding_box_corners(objects):
if not objects:
return None, None, None
if len(objects) == 1:
bpy.context.scene.transform_orientation_slots[1].type = "LOCAL"
obj = objects[0]
corners = [obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box]
else:
bpy.context.scene.transform_orientation_slots[1].type = "GLOBAL"
all_corners = []
for obj in objects:
if hasattr(obj, "bound_box"):
all_corners.extend([obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box])
min_corner = mathutils.Vector(
(min(v.x for v in all_corners), min(v.y for v in all_corners), min(v.z for v in all_corners))
)
max_corner = mathutils.Vector(
(max(v.x for v in all_corners), max(v.y for v in all_corners), max(v.z for v in all_corners))
)
corners = [
mathutils.Vector((min_corner.x, min_corner.y, min_corner.z)),
mathutils.Vector((min_corner.x, min_corner.y, max_corner.z)),
mathutils.Vector((min_corner.x, max_corner.y, max_corner.z)),
mathutils.Vector((min_corner.x, max_corner.y, min_corner.z)),
mathutils.Vector((max_corner.x, min_corner.y, min_corner.z)),
mathutils.Vector((max_corner.x, min_corner.y, max_corner.z)),
mathutils.Vector((max_corner.x, max_corner.y, max_corner.z)),
mathutils.Vector((max_corner.x, max_corner.y, min_corner.z)),
]
edges = [
(0, 1, "Z"),
(1, 2, "Y"),
(2, 3, "Z"),
(3, 0, "Y"),
(4, 5, "Z"),
(5, 6, "Y"),
(6, 7, "Z"),
(7, 4, "Y"),
(0, 4, "X"),
(1, 5, "X"),
(2, 6, "X"),
(3, 7, "X"),
]
axis_colors = {
"X": (0.956, 0.282, 0.322, 1),
"Y": (0.565, 0.812, 0.125, 1),
"Z": (0.196, 0.529, 0.929, 1),
}
return corners, edges, axis_colors
@staticmethod
def find_closest_trihedron(corners, edges, region, rv3d):
min_y = float("inf")
best_origin = None
for idx, corner in enumerate(corners):
screen_co = location_3d_to_region_2d(region, rv3d, corner)
if screen_co is not None and screen_co.y < min_y:
min_y = screen_co.y
best_origin = idx
trihedron = [
{"X": (0, 4), "Y": (0, 3), "Z": (0, 1)},
{"X": (1, 5), "Y": (1, 2), "Z": (1, 0)},
{"X": (2, 6), "Y": (2, 1), "Z": (2, 3)},
{"X": (3, 7), "Y": (3, 0), "Z": (3, 2)},
{"X": (4, 0), "Y": (4, 7), "Z": (4, 5)},
{"X": (5, 1), "Y": (5, 6), "Z": (5, 4)},
{"X": (6, 2), "Y": (6, 5), "Z": (6, 7)},
{"X": (7, 3), "Y": (7, 4), "Z": (7, 6)},
]
return trihedron[best_origin]
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def draw_text_background(self, context, coords_dim, text_dim):
padding = 5
theme = context.preferences.themes.items()[0][1]
color = (*theme.user_interface.wcol_menu_back.inner[:3], 0.5) # unwrap color values and adds alpha
top_left = (coords_dim[0] - padding, coords_dim[1] + text_dim[1] + padding)
bottom_left = (coords_dim[0] - padding, coords_dim[1] - padding)
top_right = (coords_dim[0] + text_dim[0] + padding, coords_dim[1] + text_dim[1] + padding)
bottom_right = (coords_dim[0] + text_dim[0] + padding, coords_dim[1] - padding)
verts = [top_left, bottom_left, top_right, bottom_right]
gpu.state.blend_set("ALPHA")
self.draw_batch("TRIS", verts, color, [(0, 1, 2), (1, 2, 3)])
def draw_bounding_box_wire_cube(self, context):
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind()
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
selected_objects = [obj for obj in bpy.context.selected_objects if hasattr(obj, "bound_box")]
if not selected_objects:
return
corners, edges, axis_colors = self.get_combined_bounding_box_corners(selected_objects)
region = bpy.context.region
rv3d = bpy.context.region_data
gpu.state.line_width_set(2.0)
for i1, i2, axis in edges:
self.draw_batch("LINES", [corners[i1], corners[i2]], self.decorator_color_wire)
closest_indices = self.find_closest_trihedron(corners, edges, region, rv3d)
for axis in "XYZ":
pair = closest_indices[axis]
if pair is not None:
i1, i2 = pair
color = getattr(self, f"decorator_color_{axis.lower()}_axis")
self.draw_batch("LINES", [corners[i1], corners[i2]], color)
def draw_dimension_text(self, context):
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind()
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
selected_objects = [obj for obj in bpy.context.selected_objects if hasattr(obj, "bound_box")]
if not selected_objects:
return
corners, edges, axis_colors = self.get_combined_bounding_box_corners(selected_objects)
if not corners:
return
dims = mathutils.Vector(
(
(corners[4] - corners[0]).length,
(corners[3] - corners[0]).length,
(corners[1] - corners[0]).length,
)
)
region = bpy.context.region
rv3d = bpy.context.region_data
addon_prefs = tool.Blender.get_addon_preferences()
font_id = 0
font_size = tool.Blender.scale_font_size(12)
blf.size(font_id, font_size)
blf.enable(font_id, blf.SHADOW)
blf.shadow(font_id, 6, 0, 0, 0, 1)
color = addon_prefs.decorations_colour
blf.color(font_id, *color)
closest_indices = self.find_closest_trihedron(corners, edges, region, rv3d)
screen_coords = {}
for axis_idx, axis in enumerate("XYZ"):
pair = closest_indices[axis]
if pair is not None:
i1, i2 = pair
offset_factor = 0.6 - (axis_idx * 0.1)
center = corners[i1].lerp(corners[i2], offset_factor)
value = getattr(dims, axis.lower())
screen_co = location_3d_to_region_2d(region, rv3d, center)
if screen_co is not None:
screen_coords[axis] = (screen_co, value)
self.adjust_overlapping_labels(screen_coords)
for axis, (screen_co, value) in screen_coords.items():
blf.position(font_id, screen_co.x, screen_co.y, 0)
blf.color(font_id, 1, 1, 1, 1)
value_str = f"D{axis.lower()}: " + format_distance(value, hide_units=False)
text_length = blf.dimensions(font_id, value_str)
self.draw_text_background(context, screen_co, text_length)
blf.draw(font_id, value_str)
blf.disable(font_id, blf.SHADOW)
def adjust_overlapping_labels(self, screen_coords):
font_id = 0
text_dimensions = {}
for axis, (screen_co, value) in screen_coords.items():
value_str = f"D{axis.lower()}: " + format_distance(value, hide_units=False)
text_dimensions[axis] = blf.dimensions(font_id, value_str)
min_spacing = 5
axes = list(screen_coords.keys())
for i in range(len(axes)):
for j in range(i + 1, len(axes)):
axis1, axis2 = axes[i], axes[j]
co1, _ = screen_coords[axis1]
co2, _ = screen_coords[axis2]
dim1 = text_dimensions[axis1]
dim2 = text_dimensions[axis2]
bounds1 = {
"left": co1.x - min_spacing,
"right": co1.x + dim1[0] + min_spacing,
"top": co1.y + dim1[1] + min_spacing,
"bottom": co1.y - min_spacing,
}
bounds2 = {
"left": co2.x - min_spacing,
"right": co2.x + dim2[0] + min_spacing,
"top": co2.y + dim2[1] + min_spacing,
"bottom": co2.y - min_spacing,
}
if (
bounds1["left"] < bounds2["right"]
and bounds1["right"] > bounds2["left"]
and bounds1["bottom"] < bounds2["top"]
and bounds1["top"] > bounds2["bottom"]
):
x_overlap = min(bounds1["right"], bounds2["right"]) - max(bounds1["left"], bounds2["left"])
y_overlap = min(bounds1["top"], bounds2["top"]) - max(bounds1["bottom"], bounds2["bottom"])
if x_overlap < y_overlap:
if co1.x < co2.x:
co1.x -= x_overlap / 2 + min_spacing
co2.x += x_overlap / 2 + min_spacing
else:
co1.x += x_overlap / 2 + min_spacing
co2.x -= x_overlap / 2 + min_spacing
else:
if co1.y < co2.y:
co1.y -= y_overlap / 2 + min_spacing
co2.y += y_overlap / 2 + min_spacing
else:
co1.y += y_overlap / 2 + min_spacing
co2.y -= y_overlap / 2 + min_spacing
@@ -371,7 +371,9 @@ def get_vertical_profile_preview_data(
return data
def get_horizontal_profile_preview_data(context, relating_type):
def get_horizontal_profile_preview_data(
context: bpy.types.Context, relating_type: ifcopenshell.entity_instance
) -> dict[str, Any]:
material = ifcopenshell.util.element.get_material(relating_type)
try:
profile_curve = material.MaterialProfiles[0].Profile
@@ -385,7 +387,7 @@ def get_horizontal_profile_preview_data(context, relating_type):
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
if len(polyline_points) < 2:
return
return {}
for point in polyline_points:
polyline_verts.append(Vector((point.x, point.y, point.z)))
polyline_edges = [(i, i + 1) for i in range(len(polyline_verts) - 1)]
@@ -397,7 +399,7 @@ def get_horizontal_profile_preview_data(context, relating_type):
verts = shape.verts
if not verts:
raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile}'.")
raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile_curve}'.")
edges = shape.edges
@@ -433,7 +435,7 @@ def get_horizontal_profile_preview_data(context, relating_type):
case "9":
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
data = {}
data: dict[str, Any] = {}
data["verts"] = []
data["edges"] = []
data["tris"] = []
@@ -38,6 +38,7 @@ import bonsai.core.aggregate
import bonsai.core.type
import bonsai.core.root
import bonsai.core.geometry
import bonsai.core.model as core
import bonsai.core.spatial
from . import wall, slab, profile, mep
from bonsai.bim.ifc import IfcStore
@@ -564,6 +565,7 @@ class SetActiveType(bpy.types.Operator, tool.Ifc.Operator):
props.relating_type_id = str(self.relating_type)
# TODO: not exposed to UI.
class AlignProduct(bpy.types.Operator):
bl_idname = "bim.align_product"
bl_label = "Align Product"
+39 -11
View File
@@ -25,7 +25,7 @@ from bonsai.bim.prop import ObjProperty
from bonsai.bim.module.model.data import AuthoringData
from bpy.types import PropertyGroup, NodeTree
from math import pi, radians
from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator
from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator, BoundingBoxDecorator
from bonsai.bim.module.model.door import update_door_modifier_bmesh
from bonsai.bim.module.model.window import update_window_modifier_bmesh
from bonsai.bim.module.drawing.decoration import CutDecorator
@@ -114,6 +114,39 @@ def update_slab_direction_decorator(self: "BIMModelProperties", context: bpy.typ
SlabDirectionDecorator.uninstall()
def update_measure_xyz(self: "BIMModelProperties", context: bpy.types.Context) -> None:
area_3d = next((area for area in context.screen.areas if area.type == "VIEW_3D"), None)
space_3d = next((space for space in area_3d.spaces if space.type == "VIEW_3D"), None) if area_3d else None
scene = context.scene
if self.show_bounding_box:
BoundingBoxDecorator.install(context)
# Save previous state if not already saved
if not self.prev_transform_orientation_slot_type:
self.prev_transform_orientation_slot_type = scene.transform_orientation_slots[1].type
if not self.prev_show_gizmo_object_translate and space_3d:
self.prev_show_gizmo_object_translate = space_3d.show_gizmo_object_translate
# Set new state
if space_3d:
if len(context.selected_objects) == 1:
scene.transform_orientation_slots[1].type = "LOCAL"
else:
scene.transform_orientation_slots[1].type = "GLOBAL"
space_3d.show_gizmo_object_translate = True
else:
BoundingBoxDecorator.uninstall()
# Restore previous state
if space_3d:
prev_orientation = self.prev_transform_orientation_slot_type
if prev_orientation:
scene.transform_orientation_slots[1].type = prev_orientation
self.prev_transform_orientation_slot_type = ""
prev_gizmo = self.prev_show_gizmo_object_translate
if prev_gizmo is not None:
space_3d.show_gizmo_object_translate = prev_gizmo
self.prev_show_gizmo_object_translate = False
def update_cut_decorator(self: "BIMModelProperties", context: bpy.types.Context) -> None:
if self.show_cut_decorator:
CutDecorator.install(bpy.context)
@@ -158,14 +191,6 @@ class BIMModelProperties(PropertyGroup):
menu_relating_type_id: bpy.props.IntProperty()
icon_id: bpy.props.IntProperty()
updating: bpy.props.BoolProperty(default=False)
occurrence_name_style: bpy.props.EnumProperty(
items=[("CLASS", "By Class", ""), ("TYPE", "By Type", ""), ("CUSTOM", "Custom", "")],
name="Occurrence Name Style",
)
occurrence_name_function: bpy.props.StringProperty(
name="Occurrence Name Function",
description="Code that will be evaluated to generate occurrence name for CUSTOM occurrence name style",
)
getter_enum = {"ifc_class": get_ifc_class, "relating_type": get_relating_type_id}
extrusion_depth: bpy.props.FloatProperty(name="Extrusion Depth", min=0.001, default=42.0, subtype="DISTANCE")
cardinal_point: bpy.props.EnumProperty(
@@ -248,6 +273,11 @@ class BIMModelProperties(PropertyGroup):
default=False,
update=update_slab_direction_decorator,
)
prev_transform_orientation_slot_type: bpy.props.StringProperty(name="Previous Gizmo Orientation Type")
prev_show_gizmo_object_translate: bpy.props.BoolProperty(name="Previous Gizmo Translate")
show_bounding_box: bpy.props.BoolProperty(name="Measure XYZ Dimensions", default=False, update=update_measure_xyz)
show_cut_decorator: bpy.props.BoolProperty(
name="Show Cut Decorator",
default=True,
@@ -267,8 +297,6 @@ class BIMModelProperties(PropertyGroup):
menu_relating_type_id: int
icon_id: int
updating: bool
occurrence_name_style: Literal["CLASS", "TYPE", "CUSTOM"]
occurrence_name_function: str
extrusion_depth: float
cardinal_point: Literal[
"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19"
+17 -45
View File
@@ -60,30 +60,15 @@ class RolesAddressesData:
return results
@classmethod
def get_address_list_attributes(cls, address: ifcopenshell.entity_instance) -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
props = tool.Owner.get_owner_props()
def get_address_list_attributes(cls, address: ifcopenshell.entity_instance) -> list[str]:
if address.is_a("IfcPostalAddress"):
names = ["AddressLines"]
elif address.is_a("IfcTelecomAddress"):
names = ["TelephoneNumbers", "FacsimileNumbers", "ElectronicMailAddresses", "MessagingIDs"]
else:
assert False, f"Unexpected entity: {address}"
for name in names:
if name == "AddressLines":
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.address_lines)]
elif name == "TelephoneNumbers":
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.telephone_numbers)]
elif name == "FacsimileNumbers":
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.facsimile_numbers)]
elif name == "ElectronicMailAddresses":
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.electronic_mail_addresses)]
elif name == "MessagingIDs":
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.messaging_ids)]
else:
assert False, f"Unexpected name: {name}"
results.append({"name": name, "items": items})
return results
return names
class PeopleData(RolesAddressesData):
@@ -106,7 +91,6 @@ class PeopleData(RolesAddressesData):
"name": cls.get_person_name(person),
"roles_label": ", ".join([r["label"] for r in roles]),
"is_engaged": bool(person.EngagedIn),
"list_attributes": cls.get_person_list_attributes(person),
"roles": roles,
"addresses": cls.get_addresses(person),
}
@@ -125,23 +109,6 @@ class PeopleData(RolesAddressesData):
name += f" ({full_name})"
return name
@classmethod
def get_person_list_attributes(cls, person: ifcopenshell.entity_instance) -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
props = tool.Owner.get_owner_props()
name: tool.Owner.PersonAttributeType
for name in ("MiddleNames", "PrefixTitles", "SuffixTitles"):
if name == "MiddleNames":
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.middle_names)]
elif name == "PrefixTitles":
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.prefix_titles)]
elif name == "SuffixTitles":
items = [{"id": id, "prop": prop} for id, prop in enumerate(props.suffix_titles)]
else:
assert False, name
results.append({"name": name, "items": items})
return results
class OrganisationsData(RolesAddressesData):
data = {}
@@ -177,30 +144,28 @@ class OwnerData:
@classmethod
def load(cls):
cls.data = {
"user_person": cls.get_user_person(),
"user_organisation": cls.get_user_organisation(),
"users": cls.get_users(),
"user_person": cls.user_person(),
"user_organisation": cls.user_organisation(),
"users": cls.users(),
}
cls.is_loaded = True
@classmethod
def get_user_person(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
def user_person(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
return [(str(p.id()), p[0] or "Unnamed", "") for p in tool.Ifc.get().by_type("IfcPerson")]
@classmethod
def get_user_organisation(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
def user_organisation(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
return [(str(p.id()), p[0] or "Unnamed", "") for p in tool.Ifc.get().by_type("IfcOrganization")]
@classmethod
def get_users(cls) -> list[dict[str, Any]]:
props = tool.Owner.get_owner_props()
def users(cls) -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
for user in tool.Ifc.get().by_type("IfcPersonAndOrganization"):
results.append(
{
"id": user.id(),
"label": "{} ({})".format(user.ThePerson[0] or "Unnamed", user.TheOrganization[0] or "Unnamed"),
"is_active": props.active_user_id == user.id(),
}
)
return results
@@ -223,7 +188,14 @@ class ActorData:
props = tool.Owner.get_owner_props()
if not (ifc_class := props.actor_type):
ifc_class = cls.actor_type()[0][0]
return [(str(p.id()), p[0] or "Unnamed", "") for p in tool.Ifc.get().by_type(ifc_class)]
def get_name(entity: ifcopenshell.entity_instance) -> str:
if entity.is_a() == "IfcPersonAndOrganization":
return f"{get_name(entity.ThePerson)}/{get_name(entity.TheOrganization)}"
# 0 IfcPerson/IfcOrganization Id(IFC2X3)/Identification
return entity[0] or "Unnamed"
return [(str(p.id()), get_name(p), "") for p in tool.Ifc.get().by_type(ifc_class)]
@classmethod
def actors(cls) -> list[dict[str, Any]]:
+22 -20
View File
@@ -54,12 +54,11 @@ def draw_addresses(box: bpy.types.UILayout, parent: dict[str, Any]) -> None:
props = tool.Owner.get_owner_props()
row = box.row(align=True)
row.label(text="Addresses")
op = row.operator("bim.add_address", icon="LINK_BLEND", text="")
assert (wm := bpy.context.window_manager)
row.prop(wm.operator_properties_last(bpy.ops.bim.add_address.idname()), "ifc_class", text="")
op = row.operator("bim.add_address", icon="ADD", text="")
op.parent = parent["id"]
op.ifc_class = "IfcTelecomAddress"
op = row.operator("bim.add_address", icon="APPEND_BLEND", text="")
op.parent = parent["id"]
op.ifc_class = "IfcPostalAddress"
for address in parent["addresses"]:
if props.active_address_id == address["id"]:
@@ -67,18 +66,20 @@ def draw_addresses(box: bpy.types.UILayout, parent: dict[str, Any]) -> None:
row.operator("bim.edit_address", icon="CHECKMARK")
row.operator("bim.disable_editing_address", icon="CANCEL", text="")
bonsai.bim.helper.draw_attributes(props.address_attributes, box)
for attribute in address["list_attributes"]:
attributes: list[str] = address["list_attributes"]
for attribute_name in attributes:
row = box.row(align=True)
row.label(text=attribute["name"])
row.label(text=attribute_name)
op = row.operator("bim.add_address_attribute", icon="ADD", text="")
op.name = attribute["name"]
op.name = attribute_name
for item in attribute["items"]:
collection = tool.Owner.get_address_collection(attribute_name)
for i, item in enumerate(collection):
row = box.row(align=True)
row.prop(item["prop"], "name", text="")
row.prop(item, "name", text="")
op = row.operator("bim.remove_address_attribute", icon="REMOVE", text="")
op.name = attribute["name"]
op.id = item["id"]
op.name = attribute_name
op.id = i
else:
row = box.row(align=True)
row.label(text=address["label"])
@@ -124,18 +125,19 @@ class BIM_PT_people(bpy.types.Panel):
row.operator("bim.disable_editing_person", icon="CANCEL", text="")
bonsai.bim.helper.draw_attributes(props.person_attributes, box)
for attribute in person["list_attributes"]:
for attribute_name in tool.Owner.PERSON_ATTRIBUTE_TYPES:
row = box.row(align=True)
row.label(text=attribute["name"])
row.label(text=attribute_name)
op = row.operator("bim.add_person_attribute", icon="ADD", text="")
op.name = attribute["name"]
op.name = attribute_name
for item in attribute["items"]:
collection = tool.Owner.get_names_collection(attribute_name)
for i, item in enumerate(collection):
row = box.row(align=True)
row.prop(item["prop"], "name", text="")
row.prop(item, "name", text="")
op = row.operator("bim.remove_person_attribute", icon="REMOVE", text="")
op.name = attribute["name"]
op.id = item["id"]
op.name = attribute_name
op.id = i
draw_roles(box, person)
draw_addresses(box, person)
@@ -241,7 +243,7 @@ class BIM_PT_owner(bpy.types.Panel):
for user in OwnerData.data["users"]:
row = self.layout.row(align=True)
if user["is_active"]:
if props.active_user_id == user["id"]:
row.label(text=user["label"], icon="USER")
row.operator("bim.clear_user", icon="KEYFRAME", text="").user = user["id"]
else:
@@ -118,7 +118,7 @@ class EnableEditingProfile(bpy.types.Operator):
def execute(self, context):
props = tool.Profile.get_profile_props()
props.profile_attributes.clear()
bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.profile), props.profile_attributes)
bonsai.bim.helper.import_attributes(tool.Ifc.get().by_id(self.profile), props.profile_attributes)
props.active_profile_id = self.profile
return {"FINISHED"}
+2 -1
View File
@@ -48,6 +48,7 @@ class BIM_PT_profiles(Panel):
self.props = tool.Profile.get_profile_props()
active_profile = None
assert self.layout
if self.props.is_editing and (active_profile := tool.Profile.get_active_profile_ui()):
preview_collection = ProfileData.preview_collection
box = self.layout.box()
@@ -139,7 +140,7 @@ class BIM_PT_profiles(Panel):
self.draw_editable_ui(context)
def draw_editable_ui(self, context):
bonsai.bim.helper.draw_attributes(self.props.profile_attributes, self.layout)
bonsai.bim.helper.draw_attributes(self.props.profile_attributes, self.layout, enable_search=True)
class BIM_UL_profiles(UIList):
@@ -1594,14 +1594,15 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
def execute(self, context):
project_props = tool.Project.get_project_props()
prefs = tool.Blender.get_addon_preferences()
project_props.use_relative_project_path = self.use_relative_path
if project_props.should_disable_undo_on_save:
if prefs.should_disable_undo_on_save:
old_history_size = tool.Ifc.get().history_size
old_undo_steps = context.preferences.edit.undo_steps
tool.Ifc.get().history_size = 0
context.preferences.edit.undo_steps = 0
IfcStore.execute_ifc_operator(self, context)
if project_props.should_disable_undo_on_save:
if prefs.should_disable_undo_on_save:
tool.Ifc.get().history_size = old_history_size
context.preferences.edit.undo_steps = old_undo_steps
return {"FINISHED"}
+1 -7
View File
@@ -96,7 +96,7 @@ def is_editing_project_library_update(self: "BIMProjectProperties", context: bpy
self.editing_project_library_id = int(self.selected_project_library)
project_library = library_file.by_id(int(self.selected_project_library))
self.project_library_attributes.clear()
bonsai.bim.helper.import_attributes2(project_library, self.project_library_attributes)
bonsai.bim.helper.import_attributes(project_library, self.project_library_attributes)
self.parent_library = str(tool.Project.get_parent_library(project_library).id())
ProjectLibraryData.load() # Show edit icon in enum.
return
@@ -300,7 +300,6 @@ class BIMProjectProperties(PropertyGroup):
)
should_use_cpu_multiprocessing: BoolProperty(name="CPU Multiprocessing", default=True)
should_merge_materials_by_colour: BoolProperty(name="Merge Materials by Colour", default=False)
should_stream: BoolProperty(name="Stream Data From IFC-SPF (Only for advanced users)", default=False)
should_load_geometry: BoolProperty(name="Load Geometry", default=True)
should_clean_mesh: BoolProperty(
name="Clean Meshes",
@@ -362,9 +361,6 @@ class BIMProjectProperties(PropertyGroup):
description="Load indexed maps (UV and color maps)",
default=True,
)
should_disable_undo_on_save: BoolProperty(
name="Disable Undo When Saving (Faster saves, no undo for you!)", default=False
)
links: CollectionProperty(name="Links", type=Link)
active_link_index: IntProperty(name="Active Link Index")
export_schema: EnumProperty(items=get_export_schema, name="IFC Schema", update=update_export_schema)
@@ -458,7 +454,6 @@ class BIMProjectProperties(PropertyGroup):
geometry_library: Literal["opencascade", "cgal", "cgal-simple", "hybrid-cgal-simple-opencascade"]
should_use_cpu_multiprocessing: bool
should_merge_materials_by_colour: bool
should_stream: bool
should_load_geometry: bool
should_clean_mesh: bool
should_cache: bool
@@ -474,7 +469,6 @@ class BIMProjectProperties(PropertyGroup):
element_offset: int
element_limit: int
load_indexed_maps: bool
should_disable_undo_on_save: bool
links: bpy.types.bpy_prop_collection_idprop[Link]
active_link_index: int
export_schema: str
+5 -11
View File
@@ -33,16 +33,16 @@ classes = (
operator.UnsharePset,
operator.BIM_OT_add_property_to_edit,
operator.BIM_OT_remove_property_to_edit,
operator.BIM_OT_clear_list,
operator.BIM_OT_rename_parameters,
operator.BIM_OT_bulk_edit_clear_list,
operator.BIM_OT_pset_bulk_rename_parameters,
operator.BIM_OT_add_edit_custom_property,
operator.BIM_OT_bulk_remove_psets,
prop.IfcPropertyEnumeratedValue,
prop.IfcProperty,
prop.PsetProperties,
prop.RenameProperties,
prop.AddEditProperties,
prop.DeletePsets,
prop.RenamePropertyEntry,
prop.AddEditPropertyEntry,
prop.DeletePsetEntry,
prop.GlobalPsetProperties,
ui.BIM_PT_object_psets,
ui.BIM_PT_object_qtos,
@@ -71,9 +71,6 @@ def register():
bpy.types.Scene.GroupPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.ProfilePsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.WorkSchedulePsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.RenameProperties = bpy.props.CollectionProperty(type=prop.RenameProperties)
bpy.types.Scene.AddEditProperties = bpy.props.CollectionProperty(type=prop.AddEditProperties)
bpy.types.Scene.DeletePsets = bpy.props.CollectionProperty(type=prop.DeletePsets)
bpy.types.Scene.GlobalPsetProperties = bpy.props.PointerProperty(type=prop.GlobalPsetProperties)
@@ -86,7 +83,4 @@ def unregister():
del bpy.types.Scene.GroupPsetProperties
del bpy.types.Scene.ProfilePsetProperties
del bpy.types.Scene.WorkSchedulePsetProperties
del bpy.types.Scene.RenameProperties
del bpy.types.Scene.AddEditProperties
del bpy.types.Scene.DeletePsets
del bpy.types.Scene.GlobalPsetProperties
+4 -5
View File
@@ -26,7 +26,7 @@ import bonsai.bim.schema
# TODO: Should this cache belong here? Dunno. Maybe.
is_expanded = {}
is_expanded: dict[int, bool] = {}
def refresh():
@@ -176,8 +176,8 @@ class MaterialPsetsData(Data):
def load(cls):
ifc_definition_id = None
props = tool.Material.get_material_props()
if props.materials and props.active_material_index < len(props.materials):
ifc_definition_id = props.materials[props.active_material_index].ifc_definition_id
if material := props.active_material:
ifc_definition_id = material.ifc_definition_id
cls.data = {
"ifc_definition_id": ifc_definition_id,
@@ -189,8 +189,7 @@ class MaterialPsetsData(Data):
@classmethod
def pset_name(cls):
props = tool.Material.get_material_props()
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index]
if material := props.active_material:
if material.ifc_definition_id:
material = tool.Ifc.get().by_id(material.ifc_definition_id)
category = getattr(material, "Category", None) or None
+113 -50
View File
@@ -18,6 +18,7 @@
import bpy
import json
from bpy.types import Context, OperatorProperties
import ifcopenshell.api
import ifcopenshell.api.pset
import ifcopenshell.util.attribute
@@ -29,6 +30,11 @@ import bonsai.tool as tool
import bonsai.core.pset as core
import bonsai.bim.module.pset.data
from bonsai.bim.ifc import IfcStore
from collections import defaultdict
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.pset.prop import RenamePropertyEntry, AddEditPropertyEntry
class TogglePsetExpansion(bpy.types.Operator, tool.Ifc.Operator):
@@ -263,124 +269,179 @@ class CopyPropertyToSelection(bpy.types.Operator, tool.Ifc.Operator):
class BIM_OT_add_property_to_edit(bpy.types.Operator):
bl_label = "Add Edit Rule"
bl_label = "Add Property to Edit"
bl_idname = "bim.add_property_to_edit"
bl_options = {"REGISTER", "UNDO"}
option: bpy.props.StringProperty()
index: bpy.props.IntProperty(default=-1)
option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES],
)
index: bpy.props.IntProperty(default=-1) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
option: tool.Pset.BulkOperationType
index: int
@classmethod
def description(cls, context: bpy.types.Context, properties: bpy.types.OperatorProperties) -> str:
return f"Add property entry to for bulk operation '{properties.option}'."
def execute(self, context):
if self.index == -1:
getattr(context.scene, self.option).add()
tool.Pset.get_bulk_operation_collection(self.option).add()
else:
getattr(context.scene, self.option)[self.index].enum_values.add()
assert self.option == "ADD_EDIT"
props = tool.Pset.get_global_pset_props()
props.psets_to_add_edit[self.index].enum_values.add()
return {"FINISHED"}
class BIM_OT_remove_property_to_edit(bpy.types.Operator):
bl_label = "Remove Property to Be Renamed"
bl_label = "Remove Property from Editing"
bl_idname = "bim.remove_property_to_edit"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty()
index2: bpy.props.IntProperty(default=-1)
option: bpy.props.StringProperty()
index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
index2: bpy.props.IntProperty(default=-1) # pyright: ignore[reportRedeclaration]
option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES],
)
if TYPE_CHECKING:
index: int
index2: int
option: tool.Pset.BulkOperationType
@classmethod
def description(cls, context: bpy.types.Context, properties: bpy.types.OperatorProperties) -> str:
return f"Remove property entry from bulk operation '{properties.option}'."
def execute(self, context):
if self.index2 == -1:
getattr(context.scene, self.option).remove(self.index)
tool.Pset.get_bulk_operation_collection(self.option).remove(self.index)
else:
getattr(context.scene, self.option)[self.index].enum_values.remove(self.index2)
assert self.option == "ADD_EDIT"
props = tool.Pset.get_global_pset_props()
props.psets_to_add_edit[self.index].enum_values.remove(self.index2)
return {"FINISHED"}
class BIM_OT_clear_list(bpy.types.Operator):
class BIM_OT_bulk_edit_clear_list(bpy.types.Operator):
bl_label = "Clear List of Properties"
bl_idname = "bim.clear_list"
bl_idname = "bim.pset_bulk_edit_clear_list"
bl_options = {"REGISTER", "UNDO"}
option: bpy.props.StringProperty()
option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES],
)
if TYPE_CHECKING:
option: tool.Pset.BulkOperationType
def execute(self, context):
getattr(context.scene, self.option).clear()
tool.Pset.get_bulk_operation_collection(self.option).clear()
return {"FINISHED"}
class BIM_OT_rename_parameters(bpy.types.Operator, tool.Ifc.Operator):
class BIM_OT_pset_bulk_rename_parameters(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Rename Parameters"
bl_idname = "bim.rename_parameters"
bl_idname = "bim.pset_bulk_rename_parameters"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Rename parameters that are subclasses of IfcElement"
bl_description = "Rename pset properties for all IfcElements"
def _execute(self, context):
props_to_map = context.scene.RenameProperties
props_to_map = tool.Pset.get_global_pset_props().psets_to_rename
ifc_file = tool.Ifc.get()
all_ifc_elements = ifc_file.by_type("IfcElement")
properties_map: defaultdict[str, dict[str, str]] = defaultdict(dict)
"""pset_name -> {old_name -> new_name}"""
for p in props_to_map:
properties_map[p.name][p.existing_property_name] = p.new_property_name
props_renamed = 0
for ifc_element in all_ifc_elements:
for definition in ifc_element.IsDefinedBy:
if definition.is_a("IfcRelDefinesByProperties"):
prop_set = definition.RelatingPropertyDefinition
self.rename_property(prop_set, props_to_map, ifc_element)
props_renamed += self.rename_property(prop_set, properties_map)
self.report({"INFO"}, "Finished applying changes")
self.report({"INFO"}, f"Finished applying changes, {props_renamed} properties renamed.")
return {"FINISHED"}
def rename_property(self, property_set, properties_to_map, ifc_element):
def rename_property(
self,
property_set: ifcopenshell.entity_instance,
pset_remap: dict[str, dict[str, str]],
) -> int:
props_renamed = 0
pset_name = property_set.Name
if pset_name not in pset_remap:
return props_renamed
properties_remap = pset_remap[pset_name]
property_container: tuple[ifcopenshell.entity_instance, ...]
if property_set.is_a() == "IfcPropertySet":
property_container = property_set.HasProperties
elif property_set.is_a() == "IfcElementQuantity":
property_container = property_set.Quantities
else:
assert False
for obj_prop in property_container:
for prop2map in properties_to_map:
if prop2map.pset_name != property_set.Name:
continue
if prop2map.existing_property_name == obj_prop.Name:
obj_prop.Name = prop2map.new_property_name
prop_name = obj_prop.Name
if prop_name not in properties_remap:
continue
obj_prop.Name = properties_remap[prop_name]
props_renamed += 1
return props_renamed
class BIM_OT_add_edit_custom_property(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Add or Edit a Custom Property"
bl_idname = "bim.add_edit_custom_property"
bl_description = "Edit pset properties for selected objects."
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty()
def _execute(self, context):
self.file = tool.Ifc.get()
props = context.scene.AddEditProperties
entries = tool.Pset.get_global_pset_props().psets_to_add_edit
elements_changed = 0
for obj in tool.Blender.get_selected_objects():
ifc_element = tool.Ifc.get_entity(obj)
if not ifc_element:
continue
for prop in props:
for prop in entries:
value = getattr(prop, prop.get_value_name())
primary_measure_type = prop.primary_measure_type
if prop.template_type == "IfcPropertyEnumeratedValue":
value_ifc_entity = self.generate_enum_entity(prop)
elif prop.template_type == "IfcPropertySingleValue":
value_ifc_entity = getattr(self.file, f"create{primary_measure_type}")(value)
value_ifc_entity = self.file.create_entity(primary_measure_type, value)
else:
assert False
new_pset = ifcopenshell.api.pset.add_pset(self.file, product=ifc_element, name=prop.pset_name)
ifcopenshell.api.pset.edit_pset(
self.file, pset=new_pset, properties={prop.property_name: value_ifc_entity}
)
self.report({"INFO"}, "Finished applying changes")
ifcopenshell.api.pset.edit_pset(self.file, pset=new_pset, properties={prop.name: value_ifc_entity})
if entries:
elements_changed += 1
self.report({"INFO"}, f"Finished applying changes, {elements_changed} elements changed.")
return {"FINISHED"}
def generate_enum_entity(self, prop):
def generate_enum_entity(self, prop: "AddEditPropertyEntry") -> ifcopenshell.entity_instance:
prop_type = prop.get_value_name()
prop_enum = self.file.create_entity(
"IFCPROPERTYENUMERATION",
Name=prop.property_name,
Name=prop.name,
EnumerationValues=tuple(
self.file.create_entity(prop.primary_measure_type, ev[prop_type]) for ev in prop.enum_values
),
)
prop_enum_value = self.file.create_entity(
"IFCPROPERTYENUMERATEDVALUE",
Name=prop.property_name,
Name=prop.name,
EnumerationValues=tuple(
self.file.create_entity(prop.primary_measure_type, ev[prop_type])
for ev in prop.enum_values
@@ -400,7 +461,10 @@ class BIM_OT_bulk_remove_psets(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.file = tool.Ifc.get()
props = context.scene.DeletePsets
props = tool.Pset.get_global_pset_props()
pset_names_to_delete = {p.name for p in props.psets_to_delete}
psets_removed = 0
for obj in tool.Blender.get_selected_objects():
ifc_element = tool.Ifc.get_entity(obj)
@@ -408,19 +472,18 @@ class BIM_OT_bulk_remove_psets(bpy.types.Operator, tool.Ifc.Operator):
continue
psets = ifcopenshell.util.element.get_psets(ifc_element)
for prop in props:
pset = prop.pset_name
if pset in psets:
try:
ifcopenshell.api.pset.remove_pset(
self.file,
product=ifc_element,
pset=self.file.by_id(psets[pset]["id"]),
)
except KeyError:
pass # Sometimes the pset id is not found, I'm not sure why this happens though. - vulevukusej
for pset_name, pset_data in psets.items():
if pset_name not in pset_names_to_delete:
continue
self.report({"INFO"}, "Finished applying changes")
ifcopenshell.api.pset.remove_pset(
self.file,
product=ifc_element,
pset=self.file.by_id(pset_data["id"]),
)
psets_removed += 1
self.report({"INFO"}, f"Finished applying changes, {psets_removed} psets removed.")
return {"FINISHED"}
+62 -23
View File
@@ -103,19 +103,19 @@ def get_pset_name(self: "PsetProperties", context: bpy.types.Context) -> tool.Bl
return items
def get_object_pset_name(self, context):
def get_object_pset_name(self: "PsetProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
if not ObjectPsetsData.is_loaded:
ObjectPsetsData.load()
return ObjectPsetsData.data["pset_name"]
def get_material_pset_names(self, context):
def get_material_pset_names(self: "PsetProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
if not MaterialPsetsData.is_loaded:
MaterialPsetsData.load()
return MaterialPsetsData.data["pset_name"]
def get_material_set_pset_names(self, context):
def get_material_set_pset_names(self: "PsetProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
global psetnames
if not ObjectMaterialData.is_loaded:
ObjectMaterialData.load()
@@ -128,12 +128,14 @@ def get_material_set_pset_names(self, context):
return psetnames[ifc_class]
def get_material_set_item_pset_names(self, context) -> list[tuple[str, str, str]]:
def get_material_set_item_pset_names(
self: "PsetProperties", context: bpy.types.Context
) -> tool.Blender.BLENDER_ENUM_ITEMS:
global psetnames
obj = context.active_object
assert obj
omprops = tool.Material.get_object_material_props(obj)
if not omprops.active_material_set_item_id:
if not (ifc_definition_id := omprops.active_material_set_item_id):
return []
ifc_class = tool.Ifc.get().by_id(ifc_definition_id).is_a()
if ifc_class not in psetnames:
@@ -142,7 +144,7 @@ def get_material_set_item_pset_names(self, context) -> list[tuple[str, str, str]
return psetnames[ifc_class]
def get_task_qto_names(self, context):
def get_task_qto_names(self: "PsetProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
global qtonames
ifc_class = "IfcTask"
if ifc_class not in qtonames:
@@ -151,7 +153,7 @@ def get_task_qto_names(self, context):
return qtonames[ifc_class]
def get_resource_pset_names(self, context):
def get_resource_pset_names(self: "PsetProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
global psetnames
rprops = context.scene.BIMResourceProperties
rtprops = context.scene.BIMResourceTreeProperties
@@ -162,7 +164,7 @@ def get_resource_pset_names(self, context):
return psetnames[ifc_class]
def get_resource_qto_names(self, context):
def get_resource_qto_names(self: "PsetProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
global qtonames
rprops = context.scene.BIMResourceProperties
rtprops = context.scene.BIMResourceTreeProperties
@@ -173,7 +175,7 @@ def get_resource_qto_names(self, context):
return qtonames[ifc_class]
def get_group_pset_names(self, context):
def get_group_pset_names(self: "PsetProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
global psetnames
ifc_class = "IfcGroup"
if ifc_class not in psetnames:
@@ -182,7 +184,7 @@ def get_group_pset_names(self, context):
return psetnames[ifc_class]
def get_group_qto_names(self, context):
def get_group_qto_names(self: "PsetProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
global qtonames
ifc_class = "IfcGroup"
if ifc_class not in qtonames:
@@ -191,7 +193,7 @@ def get_group_qto_names(self, context):
return qtonames[ifc_class]
def get_profile_pset_names(self, context):
def get_profile_pset_names(self: "PsetProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
global psetnames
pprops = tool.Profile.get_profile_props()
ifc_class = tool.Ifc.get().by_id(pprops.profiles[pprops.active_profile_index].ifc_definition_id).is_a()
@@ -201,7 +203,7 @@ def get_profile_pset_names(self, context):
return psetnames[ifc_class]
def get_work_schedule_pset_names(self, context):
def get_work_schedule_pset_names(self: "PsetProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
global psetnames
ifc_class = "IfcWorkSchedule"
if ifc_class not in psetnames:
@@ -211,7 +213,7 @@ def get_work_schedule_pset_names(self, context):
# TODO: unsafe?
def get_qto_name(self, context):
def get_qto_name(self: "PsetProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
pset_type = repr(self)
prop_type = pset_type.split(".")[-1]
if "bpy.data.objects" in pset_type:
@@ -223,24 +225,26 @@ def get_qto_name(self, context):
results = get_resource_qto_names(self, context)
elif prop_type == "GroupPsetProperties":
results = get_group_qto_names(self, context)
return [("BBIM_CUSTOM", "Custom Qto", "Create a quantity set without using a template."), None] + results
else:
assert False
return [("BBIM_CUSTOM", "Custom Qto", "Create a quantity set without using a template."), None] + list(results)
def get_object_qto_name(self, context):
def get_object_qto_name(self: "PsetProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
if not ObjectPsetsData.is_loaded:
ObjectPsetsData.load()
return ObjectPsetsData.data["qto_name"]
# TODO: unsafe?
def get_template_type(self, context):
def get_template_type(self: "AddEditPropertyEntry", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
version = tool.Ifc.get_schema()
for t in ("IfcPropertySingleValue", "IfcPropertyEnumeratedValue"):
yield (t, t, ifcopenshell.util.doc.get_entity_doc(version, t).get("description", ""))
# TODO: unsafe?
def get_primary_measure_type(self, context):
def get_primary_measure_type(self: "AddEditPropertyEntry", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
if not AddEditCustomPropertiesData.is_loaded:
AddEditCustomPropertiesData.load()
return AddEditCustomPropertiesData.data["primary_measure_type"]
@@ -249,6 +253,9 @@ def get_primary_measure_type(self, context):
class IfcPropertyEnumeratedValue(PropertyGroup):
enumerated_values: CollectionProperty(type=Attribute)
if TYPE_CHECKING:
enumerated_values: bpy.types.bpy_prop_collection_idprop[Attribute]
IfcPropertyValueType = Literal["IfcPropertySingleValue", "IfcPropertyEnumeratedValue"]
@@ -292,15 +299,20 @@ class PsetProperties(PropertyGroup):
prop_value: str
class RenameProperties(PropertyGroup):
pset_name: StringProperty(name="Pset")
class RenamePropertyEntry(PropertyGroup):
name: StringProperty(name="Pset")
existing_property_name: StringProperty(name="Existing Property Name")
new_property_name: StringProperty(name="New Property Name")
if TYPE_CHECKING:
name: str
existing_property_name: str
new_property_name: str
class AddEditProperties(PropertyGroup):
class AddEditPropertyEntry(PropertyGroup):
pset_name: StringProperty(name="Pset")
property_name: StringProperty(name="Property")
name: StringProperty(name="Property")
string_value: StringProperty(name="Value")
bool_value: BoolProperty(name="Value")
int_value: IntProperty(name="Value")
@@ -309,6 +321,17 @@ class AddEditProperties(PropertyGroup):
template_type: EnumProperty(items=get_template_type, name="Template Type")
enum_values: CollectionProperty(name="Enum Values", type=Attribute)
if TYPE_CHECKING:
pset_name: str
name: str
string_value: str
bool_value: bool
int_value: int
float_value: float
primary_measure_type: str
template_type: str
enum_values: bpy.types.bpy_prop_collection_idprop[Attribute]
def get_value_name(self) -> Union[Literal["string_value", "bool_value", "int_value", "float_value"], None]:
schema = tool.Ifc.schema()
ifc_data_type = schema.declaration_by_name(self.primary_measure_type)
@@ -323,10 +346,26 @@ class AddEditProperties(PropertyGroup):
return "float_value"
class DeletePsets(PropertyGroup):
pset_name: StringProperty(name="Pset")
# This class is needed just to make tooltip more descriptive.
class DeletePsetEntry(PropertyGroup):
name: StringProperty(name="Pset to Remove")
if TYPE_CHECKING:
name: str
class GlobalPsetProperties(PropertyGroup):
pset_filter: StringProperty(name="Pset Filter", options={"TEXTEDIT_UPDATE"})
qto_filter: StringProperty(name="Qto Filter", options={"TEXTEDIT_UPDATE"})
# Bulk operations.
psets_to_delete: CollectionProperty(type=DeletePsetEntry) # pyright: ignore[reportRedeclaration]
psets_to_rename: CollectionProperty(type=RenamePropertyEntry) # pyright: ignore[reportRedeclaration]
psets_to_add_edit: CollectionProperty(type=AddEditPropertyEntry) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
pset_filter: str
qto_filter: str
psets_to_delete: bpy.types.bpy_prop_collection_idprop[DeletePsetEntry]
psets_to_rename: bpy.types.bpy_prop_collection_idprop[RenamePropertyEntry]
psets_to_add_edit: bpy.types.bpy_prop_collection_idprop[AddEditPropertyEntry]
+52 -43
View File
@@ -60,9 +60,9 @@ def draw_single_property(prop: IfcProperty, layout: bpy.types.UILayout, copy_ope
value_name,
text=prop.metadata.display_name,
)
if prop.metadata.is_uri:
if prop.metadata.special_type == "URI":
op = layout.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER")
op.data_path = prop.metadata.path_from_id("string_value")
op.attribute_data_path = tool.Blender.get_full_data_path(prop.metadata)
if prop.metadata.is_optional:
layout.prop(prop.metadata, "is_null", icon="RADIOBUT_OFF" if prop.metadata.is_null else "RADIOBUT_ON", text="")
if copy_operator:
@@ -474,16 +474,17 @@ class BIM_PT_material_psets(Panel):
return False
def draw(self, context):
assert self.layout
props = tool.Material.get_material_props()
if props.materials and props.active_material_index < len(props.materials):
ifc_definition_id = props.materials[props.active_material_index].ifc_definition_id
if material := props.active_material:
ifc_definition_id = material.ifc_definition_id
if not MaterialPsetsData.is_loaded:
MaterialPsetsData.load()
elif ifc_definition_id != MaterialPsetsData.data["ifc_definition_id"]:
MaterialPsetsData.load()
props = context.scene.MaterialPsetProperties
props = tool.Pset.get_pset_props("", "Material")
row = self.layout.row(align=True)
prop_with_search(row, props, "pset_name", text="")
op = row.operator("bim.add_pset", icon="ADD", text="")
@@ -523,6 +524,7 @@ class BIM_PT_material_set_item_psets(Panel):
if not MaterialSetItemPsetsData.is_loaded:
MaterialSetItemPsetsData.load()
assert self.layout
obj = context.active_object
assert obj
omprops = tool.Material.get_object_material_props(obj)
@@ -530,7 +532,7 @@ class BIM_PT_material_set_item_psets(Panel):
self.layout.label(text="No Material Set Item Edited.")
return
props = obj.MaterialSetItemPsetProperties
props = tool.Pset.get_pset_props(obj.name, "MaterialSetItem")
row = self.layout.row(align=True)
prop_with_search(row, props, "pset_name", text="")
op = row.operator("bim.add_pset", icon="ADD", text="")
@@ -569,7 +571,8 @@ class BIM_PT_task_qtos(Panel):
if not TaskQtosData.is_loaded:
TaskQtosData.load()
props = context.scene.TaskPsetProperties
assert self.layout
props = tool.Pset.get_pset_props("", "Task")
row = self.layout.row(align=True)
row.prop(props, "qto_name", text="")
op = row.operator("bim.add_qto", icon="ADD", text="")
@@ -603,7 +606,8 @@ class BIM_PT_resource_qtos(Panel):
if not ResourceQtosData.is_loaded:
ResourceQtosData.load()
props = context.scene.ResourcePsetProperties
assert self.layout
props = tool.Pset.get_pset_props("", "Resource")
row = self.layout.row(align=True)
row.prop(props, "qto_name", text="")
op = row.operator("bim.add_qto", icon="ADD", text="")
@@ -637,7 +641,8 @@ class BIM_PT_resource_psets(Panel):
if not ResourcePsetsData.is_loaded:
ResourcePsetsData.load()
props = context.scene.ResourcePsetProperties
assert self.layout
props = tool.Pset.get_pset_props("", "Resource")
row = self.layout.row(align=True)
prop_with_search(row, props, "pset_name", text="")
op = row.operator("bim.add_pset", icon="ADD", text="")
@@ -671,7 +676,8 @@ class BIM_PT_group_qtos(Panel):
if not GroupQtosData.is_loaded:
GroupQtosData.load()
props = context.scene.GroupPsetProperties
assert self.layout
props = tool.Pset.get_pset_props("", "Group")
row = self.layout.row(align=True)
row.prop(props, "qto_name", text="")
op = row.operator("bim.add_qto", icon="ADD", text="")
@@ -705,7 +711,8 @@ class BIM_PT_group_psets(Panel):
if not GroupPsetData.is_loaded:
GroupPsetData.load()
props = context.scene.GroupPsetProperties
assert self.layout
props = tool.Pset.get_pset_props("", "Group")
row = self.layout.row(align=True)
prop_with_search(row, props, "pset_name", text="")
op = row.operator("bim.add_pset", icon="ADD", text="")
@@ -749,7 +756,8 @@ class BIM_PT_profile_psets(Panel):
):
ProfilePsetsData.load()
props = context.scene.ProfilePsetProperties
assert self.layout
props = tool.Pset.get_pset_props("", "Profile")
row = self.layout.row(align=True)
prop_with_search(row, props, "pset_name", text="")
op = row.operator("bim.add_pset", icon="ADD", text="")
@@ -781,7 +789,8 @@ class BIM_PT_work_schedule_psets(Panel):
if not WorkSchedulePsetsData.is_loaded:
WorkSchedulePsetsData.load()
props = context.scene.WorkSchedulePsetProperties
assert self.layout
props = tool.Pset.get_pset_props("", "WorkSchedule")
row = self.layout.row(align=True)
prop_with_search(row, props, "pset_name", text="")
op = row.operator("bim.add_pset", icon="ADD", text="")
@@ -815,28 +824,28 @@ class BIM_PT_rename_parameters(Panel):
bl_options = {"DEFAULT_CLOSED"}
def draw(self, context):
assert self.layout
layout = self.layout
props = context.scene.RenameProperties
entries = tool.Pset.get_global_pset_props().psets_to_rename
row = layout.row()
op = row.operator("bim.add_property_to_edit", icon="ADD")
op.option = "RenameProperties"
op.option = "RENAME"
if props:
for index, prop in enumerate(props):
if entries:
for index, prop in enumerate(entries):
row = layout.row(align=True)
prop_with_search(row, prop, "pset_name", text="")
row.prop(prop, "name", text="")
row.prop(prop, "existing_property_name", text="")
row.prop(prop, "new_property_name", text="")
op = row.operator("bim.remove_property_to_edit", icon="X", text="")
op.index = index
op.option = "RenameProperties"
op.option = "RENAME"
if props:
row = layout.row(align=True)
row.operator("bim.rename_parameters", icon="CHECKMARK")
clear = row.operator("bim.clear_list", icon="CANCEL", text="")
clear.option = "RenameProperties"
row.operator("bim.pset_bulk_rename_parameters", icon="CHECKMARK")
clear = row.operator("bim.pset_bulk_edit_clear_list", icon="CANCEL", text="")
clear.option = "RENAME"
class BIM_PT_add_edit_custom_properties(Panel):
@@ -849,30 +858,31 @@ class BIM_PT_add_edit_custom_properties(Panel):
bl_options = {"DEFAULT_CLOSED"}
def draw(self, context):
assert self.layout
layout = self.layout
props = context.scene.AddEditProperties
entries = tool.Pset.get_global_pset_props().psets_to_add_edit
row = layout.row()
op = row.operator("bim.add_property_to_edit", icon="ADD")
op.option = "AddEditProperties"
op.option = "ADD_EDIT"
op.index = -1
if props:
for index, prop in enumerate(props):
if entries:
for index, prop in enumerate(entries):
row = layout.row(align=True)
prop_with_search(row, prop, "pset_name", text="")
row.prop(prop, "property_name", text="")
row.prop(prop, "name", text="")
if prop.template_type == "IfcPropertySingleValue":
row.prop(prop, prop.get_value_name(), text="")
prop_with_search(row, prop, "primary_measure_type", text="")
row.prop(prop, "template_type", text="")
op = row.operator("bim.remove_property_to_edit", icon="X", text="")
op.index = index
op.option = "AddEditProperties"
op.option = "ADD_EDIT"
if prop.template_type == "IfcPropertyEnumeratedValue":
op = row.operator("bim.add_property_to_edit", icon="ADD", text="Add Enum")
op.option = "AddEditProperties"
op.option = "ADD_EDIT"
op.index = index
for index2, prop2 in enumerate(prop.enum_values):
row = layout.row()
@@ -883,13 +893,12 @@ class BIM_PT_add_edit_custom_properties(Panel):
op = row.operator("bim.remove_property_to_edit", icon="X", text="")
op.index = index
op.index2 = index2
op.option = "AddEditProperties"
op.option = "ADD_EDIT"
if props:
row = layout.row(align=True)
op = row.operator("bim.add_edit_custom_property", icon="CHECKMARK", text="Apply Changes")
clear = row.operator("bim.clear_list", icon="CANCEL", text="")
clear.option = "AddEditProperties"
clear = row.operator("bim.pset_bulk_edit_clear_list", icon="CANCEL", text="")
clear.option = "ADD_EDIT"
class BIM_PT_delete_psets(Panel):
@@ -902,23 +911,23 @@ class BIM_PT_delete_psets(Panel):
bl_options = {"DEFAULT_CLOSED"}
def draw(self, context):
assert self.layout
layout = self.layout
props = context.scene.DeletePsets
entries = tool.Pset.get_global_pset_props().psets_to_delete
row = layout.row()
op = row.operator("bim.add_property_to_edit", icon="ADD")
op.option = "DeletePsets"
op.option = "DELETE"
if props:
for index, prop in enumerate(props):
if entries:
for index, prop in enumerate(entries):
row = layout.row(align=True)
prop_with_search(row, prop, "pset_name", text="")
row.prop(prop, "name", text="")
op = row.operator("bim.remove_property_to_edit", icon="X", text="")
op.index = index
op.option = "DeletePsets"
op.option = "DELETE"
if props:
row = layout.row(align=True)
op = row.operator("bim.bulk_remove_psets", icon="CHECKMARK", text="Apply Changes")
clear = row.operator("bim.clear_list", icon="CANCEL", text="")
clear.option = "DeletePsets"
clear = row.operator("bim.pset_bulk_edit_clear_list", icon="CANCEL", text="")
clear.option = "DELETE"
@@ -779,6 +779,6 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
row.prop(props, "representation_obj", text="Object")
elif props.representation_template == "PROFILESET":
row = self.layout.row()
row.prop(props, "profile", text="Profile")
prop_with_search(self.layout, props, "profile", text="Profile", should_click_ok=True)
if props.representation_template != "EMPTY":
prop_with_search(self.layout, props, "contexts", should_click_ok=True)
+5 -3
View File
@@ -20,6 +20,7 @@ import bpy
import json
import ifcopenshell.util.element
import bonsai.tool as tool
from natsort import natsorted
def refresh():
@@ -39,11 +40,11 @@ class SearchData:
cls.data["saved_searches"] = cls.saved_searches()
@classmethod
def saved_searches(cls):
def saved_searches(cls) -> list[tuple[str, str, str]]:
if not tool.Ifc.get():
return []
groups = tool.Ifc.get().by_type("IfcGroup")
results = []
results: list[ifcopenshell.entity_instance] = []
for group in groups:
try:
data = json.loads(group.Description)
@@ -51,7 +52,8 @@ class SearchData:
results.append(group)
except:
pass
return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")]
enum_items = [(str(g.id()), g.Name or "Unnamed", "") for g in results]
return natsorted(enum_items, key=lambda x: x[1])
class ColourByPropertyData:
@@ -19,6 +19,7 @@
import bpy
import json
import bisect
import traceback
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.group
@@ -105,6 +106,8 @@ class SelectFilterElements(bpy.types.Operator):
query = tool.Search.get_query_for_selected_elements()
filter_groups[self.group_index].filters[self.index].value = query
if query.startswith("bpy.data.texts['"):
text: bpy.types.Text = eval(query)
name = text.name
self.report({"INFO"}, f'List of Global Ids was saved to the text file "{name}" in the current .blend file')
return {"FINISHED"}
@@ -178,10 +181,49 @@ class Search(Operator):
class SaveSearch(Operator, tool.Ifc.Operator):
bl_idname = "bim.save_search"
bl_label = "Save Search"
bl_description = "Save search filter to an IFC group"
bl_description = (
"Save search filter to an IFC group.\n\n"
"Search query will be saved to group description, query elements will be assigned to the group."
)
bl_options = {"REGISTER", "UNDO"}
name: StringProperty(name="Name")
module: StringProperty()
name_search_items: list[str] = []
def get_name_search_items(self, context: object, text: str) -> list[str]:
# Extra item so it will be easy to select current text.
return [text] + SaveSearch.name_search_items
name: StringProperty( # pyright: ignore[reportRedeclaration]
name="Name",
search=get_name_search_items,
search_options={"SORT"},
)
module: StringProperty() # pyright: ignore[reportRedeclaration]
def update_use_all_ifcgroups(self, context: object = None) -> None:
ifc_file = tool.Ifc.get()
groups = {
g.Name or "Unnamed"
for g in ifc_file.by_type("IfcGroup")
if self.use_all_ifcgroups or g.ObjectType == "SEARCH"
}
self.name_search_items[:] = natsorted(groups)
use_all_ifcgroups: BoolProperty( # pyright: ignore[reportRedeclaration]
name="Use Any IfcGroup",
description=(
"By default we're targeting only IfcGroups with SEARCH ObjectType "
"to prevent breaking internal IfcGroups (e.g. IfcGroups used for Bonsai drawings).\n\n"
"Enabling this option allows saving search to any IfcGroup matched by the provided name.\n"
"Use with caution."
),
update=update_use_all_ifcgroups,
)
if TYPE_CHECKING:
name: str
module: str
use_all_ifcgroups: bool
def _execute(self, context):
if not self.name:
@@ -193,23 +235,37 @@ class SaveSearch(Operator, tool.Ifc.Operator):
query = tool.Search.export_filter_query(filter_groups)
results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query)
except:
print(traceback.format_exc())
self.report({"ERROR"}, "Error occurred trying save search.")
return
description = json.dumps({"type": "BBIM_Search", "query": query})
group = [g for g in tool.Ifc.get().by_type("IfcGroup") if g.Name == self.name]
ifc_file = tool.Ifc.get()
group = next(
(
g
for g in ifc_file.by_type("IfcGroup")
if g.Name == self.name and (self.use_all_ifcgroups or g.ObjectType == "SEARCH")
),
None,
)
if group:
group = group[0]
group.Description = description
else:
group = ifcopenshell.api.group.add_group(tool.Ifc.get(), name=self.name, description=description)
group.ObjectType = "SEARCH"
if results:
ifcopenshell.api.group.assign_group(tool.Ifc.get(), products=list(results), group=group)
def draw(self, context):
row = self.layout.row()
row.prop(self, "name")
assert (layout := self.layout)
layout.prop(self, "name")
layout.prop(self, "use_all_ifcgroups")
def invoke(self, context, event):
assert context.window_manager
tool.Search.patch_search_ifcgroups()
self.update_use_all_ifcgroups()
return context.window_manager.invoke_props_dialog(self)
@@ -227,11 +283,13 @@ class LoadSearch(Operator, tool.Ifc.Operator):
tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups)
def draw(self, context):
assert self.layout
props = tool.Search.get_search_props()
row = self.layout.row()
row.prop(props, "saved_searches", text="")
def invoke(self, context, event):
tool.Search.patch_search_ifcgroups()
return context.window_manager.invoke_props_dialog(self)
@@ -757,8 +815,8 @@ class SelectSimilar(Operator, tool.Ifc.Operator):
def execute(self, context):
self.calculated_sum = 0 # reset if run before
key = "predefined_type" if self.key == "PredefinedType" else self.key
dprops = tool.Drawing.get_document_props()
tolerance = dprops.tolerance
prefs = tool.Blender.get_addon_preferences()
tolerance = prefs.doc.tolerance
formatted_tolerance = f"{tolerance:.{max(0, -int(f'{tolerance:.1e}'.split('e')[-1])) if tolerance < 1 else 1}f}"
if self.calculate_sum:
+79 -12
View File
@@ -33,7 +33,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Literal
def get_element_key(self: "BIMSearchProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
@@ -142,18 +142,85 @@ class BIMSearchProperties(PropertyGroup):
filter_query: StringProperty(name="Filter Query")
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
facet: EnumProperty(
name="Facet",
items=[
("entity", "Class", "", "FILE_3D", 0),
("attribute", "Attribute", "", "COPY_ID", 1),
("property", "Property", "", "PROPERTIES", 2),
("material", "Material", "", "MATERIAL", 3),
("classification", "Classification", "", "OUTLINER", 4),
("location", "Location", "", "PACKAGE", 5),
("type", "Type", "", "FILE_VOLUME", 6),
("group", "Group", "", "OUTLINER_COLLECTION", 7),
("parent", "Parent", "", "FILE_PARENT", 8),
("query", "Query", "", "POINTCLOUD_DATA", 9),
("instance", "GlobalId", "", "GRIP", 10),
("entity", "Class", "Search by IFC class.\n\nExample: 'IfcWall'.", "FILE_3D", 0),
(
"attribute",
"Attribute",
"Search by IFC class attribute value.\n\nExample values: 'Name', 'Cube'.",
"COPY_ID",
1,
),
(
"property",
"Property",
"Search by Pset property value.\n\n"
"Example values: 'Pset_WallCommon', 'FireRating', 'equal to', '2HR'.",
"PROPERTIES",
2,
),
(
"material",
"Material",
"Search by material name.\n\n"
"Example: 'concrete'\n"
"(to select all elements with material named 'concrete').",
"MATERIAL",
3,
),
(
"classification",
"Classification",
"Search by classification references.\n\n"
"Example: 'MyReference'\n"
"(to select all elements that have classification reference with Id 'MyReference').",
"OUTLINER",
4,
),
(
"location",
"Location",
"Search by spatial element.\n\n"
"Example: 'My Storey'\n"
"(to select all elements contained in 'My Storey' spatial element).",
"PACKAGE",
5,
),
(
"type",
"Type",
"Search by element type.\n\n"
"Example: 'BaseType'\n(to select all elements that have element type named 'BaseType').",
"FILE_VOLUME",
6,
),
(
"group",
"Group",
"Search by IfcGroup name.\n\n"
"Example: 'MyGroup'\n(to select all elements that are assigned to IfcGroup named 'MyGroup').",
"OUTLINER_COLLECTION",
7,
),
(
"parent",
"Parent",
"Search by parent element.\n\n"
"Example: 'My Building'\n"
"(to select all children elements of 'My Building').",
"FILE_PARENT",
8,
),
(
"query",
"Query",
"Search elements by special queries.\n\n"
"Example values: 'types.count', '0' (to select all elements that have 0 occurrences).",
"POINTCLOUD_DATA",
9,
),
("instance", "GlobalId", "Search entity by guid.\n\nExample: '2W83_qKWvEGgeo5v66dTxG'.", "GRIP", 10),
],
)
saved_searches: EnumProperty(items=get_saved_searches, name="Saved Searches")
@@ -16,6 +16,8 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
# pyright: reportAttributeAccessIssue=false
import bpy
from . import ui, prop, operator
@@ -367,9 +367,10 @@ class WorkPlansData:
@classmethod
def active_work_plan_schedules(cls):
results = []
if not bpy.context.scene.BIMWorkPlanProperties.active_work_plan_id:
props = tool.Sequence.get_work_plan_props()
if not props.active_work_plan_id:
return []
for rel in tool.Ifc.get().by_id(bpy.context.scene.BIMWorkPlanProperties.active_work_plan_id).IsDecomposedBy:
for rel in tool.Ifc.get().by_id(props.active_work_plan_id).IsDecomposedBy:
for work_schedule in rel.RelatedObjects:
results.append({"id": work_schedule.id(), "name": work_schedule.Name or "Unnamed"})
return results
@@ -94,12 +94,24 @@ class ActivateStatusFilters(bpy.types.Operator):
bl_description = "Filter and display objects based on currently selected IFC statuses"
bl_options = {"REGISTER", "UNDO"}
only_if_enabled: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
name="Only If Filters are Enabled",
description="Activate status filters only in case if they were enabled from the UI before.",
default=False,
)
if TYPE_CHECKING:
only_if_enabled: bool
def execute(self, context):
props = tool.Sequence.get_status_props()
if not props.is_enabled:
# In case if operator was added to Quick Favorites.
bpy.ops.bim.disable_status_fitlers()
if not self.only_if_enabled:
# Allow users to use the same operator to refresh filters,
# even if they were not enabled before.
# Typically would occur when operator is added to Quick Favorites.
bpy.ops.bim.enable_status_filters()
return {"FINISHED"}
visible_statuses = {s.name for s in props.statuses if s.is_visible}
@@ -140,10 +152,11 @@ class EditWorkPlan(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Edit Work Plan"
def _execute(self, context):
props = tool.Sequence.get_work_plan_props()
core.edit_work_plan(
tool.Ifc,
tool.Sequence,
work_plan=tool.Ifc.get().by_id(context.scene.BIMWorkPlanProperties.active_work_plan_id),
work_plan=tool.Ifc.get().by_id(props.active_work_plan_id),
)
+12 -8
View File
@@ -88,7 +88,8 @@ class BIM_PT_work_plans(Panel):
def draw(self, context):
if not WorkPlansData.is_loaded:
WorkPlansData.load()
self.props = context.scene.BIMWorkPlanProperties
assert self.layout
self.props = tool.Sequence.get_work_plan_props()
row = self.layout.row()
if WorkPlansData.data["total_work_plans"]:
@@ -99,7 +100,7 @@ class BIM_PT_work_plans(Panel):
for work_plan in WorkPlansData.data["work_plans"]:
self.draw_work_plan_ui(work_plan)
def draw_work_plan_ui(self, work_plan):
def draw_work_plan_ui(self, work_plan: dict[str, Any]) -> None:
row = self.layout.row(align=True)
row.label(text=work_plan["name"], icon="TEXT")
if self.props.active_work_plan_id == work_plan["id"]:
@@ -121,10 +122,10 @@ class BIM_PT_work_plans(Panel):
elif self.props.editing_type == "SCHEDULES":
self.draw_work_schedule_ui()
def draw_editable_ui(self):
def draw_editable_ui(self) -> None:
draw_attributes(self.props.work_plan_attributes, self.layout)
def draw_work_schedule_ui(self):
def draw_work_schedule_ui(self) -> None:
if WorkPlansData.data["has_work_schedules"]:
row = self.layout.row(align=True)
row.prop(self.props, "work_schedules", text="")
@@ -177,7 +178,8 @@ class BIM_PT_work_schedules(Panel):
for work_schedule_id, work_schedule in SequenceData.data["work_schedules"].items():
self.draw_work_schedule_ui(work_schedule_id, work_schedule)
def draw_work_schedule_ui(self, work_schedule_id, work_schedule):
def draw_work_schedule_ui(self, work_schedule_id: int, work_schedule: dict[str, Any]) -> None:
assert self.layout
if work_schedule["PredefinedType"] == "BASELINE":
self.draw_readonly_work_schedule_ui(work_schedule_id)
else:
@@ -250,7 +252,7 @@ class BIM_PT_work_schedules(Panel):
self.draw_column_ui()
self.draw_editable_task_ui(work_schedule_id)
def draw_task_operators(self):
def draw_task_operators(self) -> None:
row = self.layout.row(align=True)
row.alignment = "RIGHT"
ifc_definition_id = None
@@ -285,9 +287,10 @@ class BIM_PT_work_schedules(Panel):
row.operator("bim.duplicate_task", text="Copy", icon="DUPLICATE").task = ifc_definition_id
row.operator("bim.remove_task", text="Delete", icon="X").task = ifc_definition_id
def draw_column_ui(self):
def draw_column_ui(self) -> None:
if not self.props.should_show_column_ui:
return
assert self.layout
row = self.layout.row()
row.operator("bim.setup_default_task_columns", text="Setup Default Columns", icon="ANCHOR_BOTTOM")
row.alignment = "RIGHT"
@@ -318,7 +321,8 @@ class BIM_PT_work_schedules(Panel):
def draw_editable_work_schedule_ui(self):
draw_attributes(self.props.work_schedule_attributes, self.layout)
def draw_editable_task_ui(self, work_schedule_id):
def draw_editable_task_ui(self, work_schedule_id: int) -> None:
assert self.layout
row = self.layout.row(align=True)
row.label(text="Task Tools")
row = self.layout.row(align=True)
@@ -305,10 +305,24 @@ class SelectContainer(bpy.types.Operator):
class SelectSimilarContainer(bpy.types.Operator):
bl_idname = "bim.select_similar_container"
bl_label = "Select Similar Container"
bl_description = "Recurvisevly selects all objects in the container.\n\nCtrl+click to select only one level deep"
bl_options = {"REGISTER", "UNDO"}
is_recursive: bpy.props.BoolProperty(default=True)
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.ctrl:
self.is_recursive = False
return self.execute(context)
def execute(self, context):
core.select_similar_container(tool.Ifc, tool.Spatial, obj=context.active_object)
core.select_similar_container(
tool.Ifc,
tool.Spatial,
obj=context.active_object,
is_recursive=self.is_recursive,
)
self.is_recursive = True # <-- forcibly reset
return {"FINISHED"}
@@ -431,18 +445,36 @@ class SelectDecomposedElements(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
should_filter: bpy.props.BoolProperty(name="Should Filter", default=True, options={"SKIP_SAVE"})
container: bpy.props.IntProperty()
is_recursive: bpy.props.BoolProperty(default=True, options={"SKIP_SAVE"})
@classmethod
def description(cls, context, operator):
return "Select the active item" + "\nALT+CLICK to select all listed elements"
return (
"Select the active item"
+ "\nALT+CLICK to select all listed elements.\nCTRL + CLICK to select only one level deep"
)
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.alt:
self.should_filter = False
if event.type == "LEFTMOUSE":
if event.alt:
self.should_filter = False
if event.ctrl:
self.is_recursive = False
return self.execute(context)
def execute(self, context):
tool.Spatial.select_products(tool.Spatial.get_filtered_elements(self.should_filter))
tool.Spatial.select_products(tool.Spatial.get_filtered_elements(self.should_filter, self.is_recursive))
# Make selected active element in list, the active object
props = tool.Spatial.get_spatial_props()
active_element = props.active_element
if active_element and active_element.type == "OCCURRENCE":
ifc_file = tool.Ifc.get()
ifc_entity = ifc_file.by_id(active_element.ifc_definition_id)
obj = tool.Ifc.get_object(ifc_entity)
if obj:
context.view_layer.objects.active = obj
obj.select_set(True)
return {"FINISHED"}
@@ -23,13 +23,13 @@ import ifcopenshell.api
import ifcopenshell.api.aggregate
import ifcopenshell.api.group
import ifcopenshell.api.structural
import ifcopenshell.util.attribute
import bonsai.bim.helper
import bonsai.core.structural as core
import bonsai.tool as tool
from math import degrees
from mathutils import Vector, Matrix
from bonsai.bim.module.structural.decorator import LoadsDecorator
from typing import Literal, Any, TYPE_CHECKING
class ShowLoads(bpy.types.Operator):
@@ -40,6 +40,7 @@ class ShowLoads(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def modal(self, context, event):
assert context.screen
if event.type == "F5":
LoadsDecorator.update()
for area in context.screen.areas:
@@ -54,6 +55,7 @@ class ShowLoads(bpy.types.Operator):
return {"PASS_THROUGH"}
def invoke(self, context, event):
assert context.window and context.window_manager and context.screen
collection = bpy.data.collections.get("IfcStructuralItem")
if collection is None:
self.report({"ERROR"}, "No IfcStructuralItems found.")
@@ -87,6 +89,7 @@ class AddStructuralMemberConnection(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Structural.get_object_structural_props(obj)
file = tool.Ifc.get()
related_structural_connection = file.by_id(oprops.ifc_definition_id)
assert props.relating_structural_member
relating_structural_member = tool.Ifc.get_entity(props.relating_structural_member)
assert relating_structural_member
if not relating_structural_member.is_a("IfcStructuralMember"):
@@ -171,39 +174,17 @@ class EnableEditingStructuralBoundaryCondition(bpy.types.Operator):
bl_idname = "bim.enable_editing_structural_boundary_condition"
bl_label = "Enable Editing Structural Boundary Condition"
bl_options = {"REGISTER", "UNDO"}
boundary_condition: bpy.props.IntProperty()
boundary_condition: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
boundary_condition: int
def execute(self, context):
obj = context.active_object
assert obj
props = tool.Structural.get_object_structural_props(obj)
props.boundary_condition_attributes.clear()
condition = tool.Ifc.get().by_id(self.boundary_condition)
schema = tool.Ifc.schema()
for attribute in schema.declaration_by_name(condition.is_a()).all_attributes():
value = getattr(condition, attribute.name(), None)
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
new = props.boundary_condition_attributes.add()
new.name = attribute.name()
new.is_null = value is None
new.is_optional = attribute.optional()
if isinstance(data_type, tuple) and data_type[0] == "select":
enum_items = [s.name() for s in ifcopenshell.util.attribute.get_select_items(attribute)]
new.enum_items = json.dumps(enum_items)
if isinstance(value, bool):
new.bool_value = False if new.is_null else value
new.data_type = "bool"
new.enum_value = "IfcBoolean"
elif isinstance(value, float):
new.float_value = 0.0 if new.is_null else value
new.data_type = "float"
new.enum_value = next(i for i in enum_items if i != "IfcBoolean")
elif data_type == "string":
new.string_value = "" if new.is_null else value
new.data_type = "string"
tool.Structural.import_boundary_condition_attributes(condition, props)
props.active_boundary_condition = self.boundary_condition
return {"FINISHED"}
@@ -212,7 +193,10 @@ class EditStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_structural_boundary_condition"
bl_label = "Edit Structural Boundary Condition"
bl_options = {"REGISTER", "UNDO"}
connection: bpy.props.IntProperty()
connection: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
connection: int
def _execute(self, context):
obj = context.active_object
@@ -223,18 +207,7 @@ class EditStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator):
connection = file.by_id(self.connection)
condition = connection.AppliedCondition
attributes = {}
for attribute in props.boundary_condition_attributes:
if attribute.is_null:
attributes[attribute.name] = {"value": None, "type": "null"}
elif attribute.data_type == "string":
attributes[attribute.name] = {"value": attribute.string_value, "type": "string"}
elif attribute.enum_value == "IfcBoolean":
attributes[attribute.name] = {"value": attribute.bool_value, "type": attribute.enum_value}
else:
attributes[attribute.name] = {"value": attribute.float_value, "type": attribute.enum_value}
ifcopenshell.api.structural.edit_structural_boundary_condition(file, condition=condition, attributes=attributes)
tool.Structural.export_and_apply_boundary_condition_attributes(condition, props)
bpy.ops.bim.disable_editing_structural_boundary_condition()
return {"FINISHED"}
@@ -608,12 +581,14 @@ class EnableEditingStructuralLoadCase(bpy.types.Operator):
self.props.active_load_case_id = self.load_case
self.props.load_case_editing_type = "ATTRIBUTES"
self.props.load_case_attributes.clear()
bonsai.bim.helper.import_attributes2(
tool.Ifc.get().by_id(self.load_case), self.props.load_case_attributes, callback=self.import_attributes
bonsai.bim.helper.import_attributes(
tool.Ifc.get().by_id(self.load_case),
self.props.load_case_attributes,
callback=self.import_attributes_callback,
)
return {"FINISHED"}
def import_attributes(self, name, prop, data):
def import_attributes_callback(self, name: str, prop: object, data: object) -> None | Literal[False]:
if name in ["SelfWeightCoefficients"]:
return False
@@ -807,7 +782,7 @@ class EnableEditingStructuralLoad(bpy.types.Operator):
def execute(self, context):
props = tool.Structural.get_structural_props()
props.structural_load_attributes.clear()
bonsai.bim.helper.import_attributes2(
bonsai.bim.helper.import_attributes(
tool.Ifc.get().by_id(self.structural_load), props.structural_load_attributes
)
props.active_structural_load_id = self.structural_load
@@ -949,36 +924,15 @@ class EnableEditingBoundaryCondition(bpy.types.Operator):
bl_idname = "bim.enable_editing_boundary_condition"
bl_label = "Enable Editing Boundary Condition"
bl_options = {"REGISTER", "UNDO"}
boundary_condition: bpy.props.IntProperty()
boundary_condition: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
boundary_condition: int
def execute(self, context):
props = tool.Structural.get_structural_props()
props.boundary_condition_attributes.clear()
boundary_condition = tool.Ifc.get().by_id(self.boundary_condition)
# bonsai.bim.helper.import_attributes(data["type"], props.boundary_condition_attributes, data)
schema = tool.Ifc.schema()
for attribute in schema.declaration_by_name(boundary_condition.is_a()).all_attributes():
value = getattr(boundary_condition, attribute.name(), None)
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
new = props.boundary_condition_attributes.add()
new.name = attribute.name()
new.is_null = value is None
new.is_optional = attribute.optional()
if isinstance(data_type, tuple) and data_type[0] == "select":
enum_items = [s.name() for s in ifcopenshell.util.attribute.get_select_items(attribute)]
new.enum_items = json.dumps(enum_items)
if isinstance(value, bool):
new.bool_value = False if new.is_null else value
new.data_type = "bool"
new.enum_value = "IfcBoolean"
elif isinstance(value, float):
new.float_value = 0.0 if new.is_null else value
new.data_type = "float"
new.enum_value = next(i for i in enum_items if i != "IfcBoolean")
elif data_type == "string":
new.string_value = "" if new.is_null else value
new.data_type = "string"
props = tool.Structural.get_structural_props()
tool.Structural.import_boundary_condition_attributes(boundary_condition, props)
props.active_boundary_condition_id = self.boundary_condition
return {"FINISHED"}
@@ -1017,22 +971,8 @@ class EditBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = tool.Structural.get_structural_props()
self.file = tool.Ifc.get()
# attributes = bonsai.bim.helper.export_attributes(props.boundary_condition_attributes)
attributes = {}
for attribute in props.boundary_condition_attributes:
if attribute.is_null:
attributes[attribute.name] = {"value": None, "type": "null"}
elif attribute.data_type == "string":
attributes[attribute.name] = {"value": attribute.string_value, "type": "string"}
elif attribute.enum_value == "IfcBoolean":
attributes[attribute.name] = {"value": attribute.bool_value, "type": attribute.enum_value}
else:
attributes[attribute.name] = {"value": attribute.float_value, "type": attribute.enum_value}
ifcopenshell.api.structural.edit_structural_boundary_condition(
self.file,
condition=self.file.by_id(props.active_boundary_condition_id),
attributes=attributes,
)
ifc_file = tool.Ifc.get()
condition = ifc_file.by_id(props.active_boundary_condition_id)
tool.Structural.export_and_apply_boundary_condition_attributes(condition, props)
bpy.ops.bim.load_boundary_conditions()
return {"FINISHED"}
+17 -4
View File
@@ -79,7 +79,20 @@ def draw_boundary_condition_ui(
def draw_boundary_condition_editable_ui(
layout: bpy.types.UILayout, props: Union[BIMStructuralProperties, BIMObjectStructuralProperties]
) -> None:
draw_attributes(props.boundary_condition_attributes, layout)
# Reimplement `draw_attributes` as we need to support multiple types for the same attribute.
for attribute in props.boundary_condition_attributes:
if attribute.data_type == "string":
row = layout.row(align=True)
bonsai.bim.helper.draw_attribute(attribute, row, enable_search=True)
else:
row = layout.row(align=True)
row.prop(attribute, "enum_value", text=attribute["name"])
if attribute.enum_value == "IfcBoolean":
row.prop(attribute, "bool_value", text="")
else:
row.prop(attribute, "float_value", text="")
if attribute.is_optional:
row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
def draw_boundary_condition_read_only_ui(layout: bpy.types.UILayout, boundary_condition: dict[str, Any]) -> None:
@@ -605,6 +618,7 @@ class BIM_PT_boundary_conditions(Panel):
BoundaryConditionsData.load()
self.props = tool.Structural.get_structural_props()
assert self.layout
row = self.layout.row(align=True)
row.label(
@@ -636,9 +650,8 @@ class BIM_PT_boundary_conditions(Panel):
"active_boundary_condition_index",
)
if self.props.active_boundary_condition_id:
draw_boundary_condition_editable_ui(self.layout, self.props)
# bonsai.bim.helper.draw_attributes(self.props.boundary_condition_attributes, self.layout)
if self.props.active_boundary_condition_id:
draw_boundary_condition_editable_ui(self.layout, self.props)
class BIM_UL_boundary_conditions(UIList):
@@ -25,6 +25,7 @@ import ifcopenshell.api
import ifcopenshell.api.style
import ifcopenshell.util.representation
import ifcopenshell.util.unit
from bonsai.bim.module.style.prop import ColourRgb
from bpy_extras.io_utils import ImportHelper
from pathlib import Path
from mathutils import Vector
@@ -739,9 +740,10 @@ class EnableEditingSurfaceStyle(bpy.types.Operator):
callback = None
if self.ifc_class == "IfcSurfaceStyleLighting":
def callback(attribute_name, _, data):
def callback(attribute_name: str, _: object, data: dict[str, Any]) -> None:
assert attributes
color = attributes.add()
assert isinstance(color, ColourRgb)
color.name = attribute_name
color_value = data[attribute_name]
if color_value is None:
@@ -751,7 +753,7 @@ class EnableEditingSurfaceStyle(bpy.types.Operator):
if attributes is not None:
attributes.clear()
bonsai.bim.helper.import_attributes2(surface_style or self.ifc_class, attributes, callback)
bonsai.bim.helper.import_attributes(surface_style or self.ifc_class, attributes, callback)
material = tool.Ifc.get_object(style)
msprops = tool.Style.get_material_style_props(material)
+1 -1
View File
@@ -78,6 +78,7 @@ class Style(PropertyGroup):
)
if TYPE_CHECKING:
name: str
ifc_definition_id: int
total_elements: int
style_classes: bpy.types.bpy_prop_collection_idprop[StrProperty]
@@ -167,7 +168,6 @@ class ColourRgb(PropertyGroup):
}
# to fit blender.bim.helper.draw_attribute
is_uri = False
is_optional = False
special_type = ""
+4 -3
View File
@@ -46,6 +46,7 @@ class BIM_PT_styles(Panel):
StylesData.load()
self.props = tool.Style.get_style_props()
assert self.layout
if not self.props.is_editing:
row = self.layout.row(align=True)
@@ -134,7 +135,7 @@ class BIM_PT_styles(Panel):
# display style elements props during edit
if self.props.is_editing_style:
if self.props.is_editing_class == "IfcSurfaceStyle":
bonsai.bim.helper.draw_attributes(self.props.attributes, self.layout)
bonsai.bim.helper.draw_attributes(self.props.attributes, self.layout, enable_search=True)
edit_label = "Save Attributes"
elif self.props.is_editing_class == "IfcSurfaceStyleShading":
self.draw_surface_style_shading()
@@ -245,10 +246,10 @@ class BIM_PT_styles(Panel):
op = row.operator("bim.browse_external_style", icon="APPEND_BLEND", text="Append From Blend File")
style = self.props.active_style
op.active_surface_style_id = style.ifc_definition_id
bonsai.bim.helper.draw_attributes(self.props.external_style_attributes, self.layout)
bonsai.bim.helper.draw_attributes(self.props.external_style_attributes, self.layout, enable_search=True)
def draw_refraction_surface_style(self):
bonsai.bim.helper.draw_attributes(self.props.refraction_style_attributes, self.layout)
bonsai.bim.helper.draw_attributes(self.props.refraction_style_attributes, self.layout, enable_search=True)
row = self.layout.row(align=True)
def draw_lighting_surface_style(self):
@@ -422,7 +422,7 @@ class EnableEditingZone(bpy.types.Operator):
def execute(self, context):
props = tool.System.get_zone_props()
props.attributes.clear()
bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.zone), props.attributes)
bonsai.bim.helper.import_attributes(tool.Ifc.get().by_id(self.zone), props.attributes)
props.is_editing = self.zone
return {"FINISHED"}
@@ -57,13 +57,13 @@ class AssignType(bpy.types.Operator, tool.Ifc.Operator):
related_objects = [bpy.data.objects[self.related_object]]
else:
related_objects = tool.Blender.get_selected_objects()
model_props = tool.Model.get_model_props()
prefs = tool.Blender.get_addon_preferences()
for obj in related_objects:
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcObject"):
continue
core.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
if model_props.occurrence_name_style == "TYPE":
if prefs.occurrence_name_style == "TYPE":
obj.name = tool.Model.generate_occurrence_name(relating_type, element.is_a())
+191 -34
View File
@@ -42,11 +42,13 @@ from mathutils import Vector, Euler
from math import radians
from pathlib import Path
from collections import namedtuple
from typing import Union, TYPE_CHECKING
from typing import Union, TYPE_CHECKING, Literal, get_args
from collections.abc import Iterable
from natsort import natsorted
if TYPE_CHECKING:
from bonsai.bim.prop import MultipleFileSelect
from bonsai.bim.prop import MultipleFileSelect, Attribute
from bpy.stub_internal import rna_enums
class SetTab(bpy.types.Operator):
@@ -154,27 +156,20 @@ class SelectURIAttribute(bpy.types.Operator, ImportHelper):
bl_label = "Select URI Attribute"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select a local file"
data_path: bpy.props.StringProperty(name="Data Path")
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
attribute_data_path: bpy.props.StringProperty(name="Data Path") # pyright: ignore[reportRedeclaration]
"""Full data path to Attribute."""
use_relative_path: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
name="Use Relative Path",
default=False,
)
if TYPE_CHECKING:
attribute_data_path: str
use_relative_path: bool
def execute(self, context):
# data_path contains the latter half of the path to the string_value property
# I have no idea how to find out the former half, so let's just use brute force.
data_path = self.data_path.replace(".string_value", "")
attribute = None
try:
attribute = eval(f"bpy.context.scene.{data_path}")
except:
try:
attribute = eval(f"bpy.context.active_object.{data_path}")
except:
try:
attribute = eval(f"bpy.context.active_object.active_material.{data_path}")
except:
# Do you know a better way?
pass
if attribute:
attribute.string_value = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)
attribute: Attribute = eval(self.attribute_data_path)
attribute.string_value = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)
return {"FINISHED"}
@@ -240,19 +235,12 @@ class SelectDir(bpy.types.Operator, ImportHelper):
bl_description = "Open a file browser to choose the directory"
data_path: bpy.props.StringProperty(name="Data Path")
if TYPE_CHECKING:
data_path: str
def execute(self, context):
crumbs = self.data_path.split(".")
if crumbs[0] == "preferences":
crumbs.pop(0)
data = tool.Blender.get_addon_preferences()
else:
data = context
while crumbs:
crumb = crumbs.pop(0)
if crumbs:
data = getattr(data, crumb)
else:
setattr(data, crumb, os.path.dirname(self.filepath))
data, attr = tool.Blender.resolve_data_path_to_data_attr(self.data_path)
setattr(data, attr, os.path.dirname(self.filepath))
return {"FINISHED"}
def invoke(self, context, event):
@@ -907,7 +895,7 @@ class FetchObjectPassport(bpy.types.Operator):
context.active_object.data = bpy.data.meshes[reference.name]
def update_enum_property_search_prop(self, context):
def update_enum_property_search_prop(self: "BIM_OT_enum_property_search", context: bpy.types.Context) -> None:
for i, prop in enumerate(self.collection_names):
if prop.name == self.dummy_name:
setattr(context.data, self.prop_name, self.collection_identifiers[i].name)
@@ -916,6 +904,7 @@ def update_enum_property_search_prop(self, context):
self.first_launch = False
else:
if not self.should_click_ok:
# This closes popup immediately, avoiding the need to click "OK".
context.window.screen = context.window.screen
if predefined_type:
try:
@@ -930,6 +919,7 @@ class BIM_OT_enum_property_search(bpy.types.Operator):
bl_label = "Search"
bl_description = "Search For Property"
bl_options = {"REGISTER", "UNDO"}
first_launch: bpy.props.BoolProperty(default=True, options={"SKIP_SAVE"})
dummy_name: bpy.props.StringProperty(name="Property", update=update_enum_property_search_prop)
collection_names: bpy.props.CollectionProperty(type=StrProperty)
@@ -1299,3 +1289,170 @@ class ShowSystemInfo(bpy.types.Operator):
col.separator()
col.label(text="(The information has been copied to the clipboard.)")
def update_attribute_search_value(self: "BIM_OT_attribute_search_values", context: bpy.types.Context) -> None:
should_click_ok = False
attr_name, attribute_obj = BIM_OT_attribute_search_values.resolve_data_path(self.data_path)
value = self.search_value
if self.data_type == "integer":
value = int(value)
elif self.data_type == "float":
value = float(value)
setattr(attribute_obj, attr_name, value)
if self.first_launch:
self.first_launch = False
else:
if not should_click_ok:
# This closes popup immediately, avoiding the need to click "OK".
context.window.screen = context.window.screen
AttributeSearchDataType = Literal["string", "integer", "float"]
class BIM_OT_attribute_search_values(bpy.types.Operator):
"""Search for attribute values. This implementation is based on bim.enum_property_search"""
bl_idname = "bim.attribute_search_values"
bl_label = "Search Attribute Values"
bl_description = "Search for attribute values used in the elements of the same IFC class."
bl_options = {"REGISTER", "UNDO"}
# Required properties.
attribute_name: bpy.props.StringProperty(name="Attribute Name")
attribute_ifc_class: bpy.props.StringProperty(name="Attribute IFC Class")
data_path: bpy.props.StringProperty(name="Data Path")
data_type: bpy.props.EnumProperty(
name="Data Type",
items=[(i, i, "") for i in get_args(AttributeSearchDataType)],
)
# Internal properties.
first_launch: bpy.props.BoolProperty(default=True, options={"SKIP_SAVE"})
search_value: bpy.props.StringProperty(
name="Search",
description="Search for attribute values",
update=update_attribute_search_value,
default="",
options={"SKIP_SAVE"},
)
collection_values: bpy.props.CollectionProperty(type=StrProperty, options={"SKIP_SAVE"})
if TYPE_CHECKING:
first_launch: bool
attribute_name: str
attribute_ifc_class: str
data_path: str
data_type: AttributeSearchDataType
search_value: str
collection_values: bpy.types.bpy_prop_collection_idprop[StrProperty]
@staticmethod
def resolve_data_path(data_path: str) -> tuple[str, "Attribute"]:
"""Resolve the data path of an object's attribute to get the attribute name and the object."""
attribute_obj, _, attr_name = data_path.rpartition(".")
attribute_obj = eval(attribute_obj)
return attr_name, attribute_obj
def invoke(self, context, event):
required_props = (self.attribute_name, self.attribute_ifc_class, self.data_path)
assert all(required_props), required_props
attr_name, attribute_obj = self.resolve_data_path(self.data_path)
self.search_value = str(getattr(attribute_obj, attr_name, ""))
unique_values = self.get_unique_attribute_values()
string_values = natsorted(unique_values)
for value in string_values:
self.collection_values.add().name = value
assert context.window_manager
return context.window_manager.invoke_props_dialog(self)
def get_unique_attribute_values(self) -> list[str]:
ifc_file = tool.Ifc.get()
unique_values: set[str] = set()
ifc_class = self.attribute_ifc_class
elements = ifc_file.by_type(ifc_class, include_subtypes=True)
for element in elements:
# We check just direct entity attributes and simply check if the attribute exists
if hasattr(element, self.attribute_name):
value = getattr(element, self.attribute_name)
if value is not None:
unique_values.add(str(value))
return list(unique_values)
def draw(self, context) -> None:
assert self.layout
row = self.layout.row()
row.label(text=f"Select {self.attribute_name} value:")
row = self.layout.row()
row.prop_search(
self,
"search_value",
self,
"collection_values",
text="",
results_are_suggestions=True,
)
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
return {"FINISHED"}
class BIM_OT_attribute_add_subitem(bpy.types.Operator):
bl_idname = "bim.attribute_add_subitem"
bl_label = "Add Subitem"
bl_description = "Add subitem to the current attribute"
bl_options = {"REGISTER", "UNDO"}
data_path: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
"""Full data path."""
if TYPE_CHECKING:
data_path: str
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
col: bpy.types.bpy_prop_collection_idprop[StrProperty]
col = eval(self.data_path)
attr: Attribute = col.data
if attr.is_optional and not col:
attr.is_null = False
col.add()
return {"FINISHED"}
class BIM_OT_attribute_remove_subitem(bpy.types.Operator):
bl_idname = "bim.attribute_remove_subitem"
bl_label = "Add Subitem"
bl_description = "Add subitem to the current attribute"
bl_options = {"REGISTER", "UNDO"}
data_path: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
"""Full data path."""
index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
data_path: str
index: int
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
col: bpy.types.bpy_prop_collection_idprop[StrProperty]
col = eval(self.data_path)
col.remove(self.index)
attr: Attribute = col.data
if attr.is_optional and not col:
attr.is_null = True
return {"FINISHED"}
+27 -41
View File
@@ -19,7 +19,6 @@
import os
import bpy
import json
import platformdirs
import ifcopenshell
import ifcopenshell.util.pset
import ifcopenshell.util.unit
@@ -128,20 +127,6 @@ def update_schema_dir(self: "BIMProperties", context: bpy.types.Context) -> None
bonsai.bim.schema.ifc.schema_dir = bim_props.schema_dir
def update_data_dir(self: "BIMProperties", context: bpy.types.Context) -> None:
import bonsai.bim.schema
bim_props = tool.Blender.get_bim_props()
bonsai.bim.schema.ifc.data_dir = bim_props.data_dir
def update_cache_dir(self: "BIMProperties", context: bpy.types.Context) -> None:
import bonsai.bim.schema
bim_props = tool.Blender.get_bim_props()
bonsai.bim.schema.ifc.cache_dir = bim_props.cache_dir
def update_section_color(self: "BIMProperties", context: bpy.types.Context) -> None:
section_node_group = bpy.data.node_groups.get("Section Override")
if section_node_group is None:
@@ -234,7 +219,9 @@ def update_attribute_value(self: "Attribute", context: bpy.types.Context) -> Non
def update_is_null(self: "Attribute", context: bpy.types.Context) -> None:
if self.is_null:
if self.data_type != "enum" and self.get_value() != (default := self.get_value_default()):
if self.data_type == "list[string]":
self.subitems_values.clear()
elif self.data_type != "enum" and self.get_value() != (default := self.get_value_default()):
self.set_value(default)
if self.is_null is not True:
self.is_null = True
@@ -286,8 +273,8 @@ def get_display_name(self: "Attribute") -> str:
return f"{name}, {unit_symbol}"
AttributeDataType = Literal["string", "integer", "float", "boolean", "enum", "file"]
AttributeSpecialType = Literal["", "DATE", "DATETIME", "LENGTH", "AREA", "VOLUME", "FORCE", "LOGICAL"]
AttributeDataType = Literal["string", "integer", "float", "boolean", "enum", "file", "list[string]"]
AttributeSpecialType = Literal["", "DATE", "DATETIME", "LENGTH", "AREA", "VOLUME", "FORCE", "LOGICAL", "URI"]
class Attribute(PropertyGroup):
@@ -300,6 +287,8 @@ class Attribute(PropertyGroup):
name="Data Type",
items=[(i, i, "") for i in get_args(AttributeDataType)],
)
# Value containers.
string_value: StringProperty(name="Value", update=update_attribute_value, description=tooltip)
bool_value: BoolProperty(name="Value", update=update_attribute_value, description=tooltip)
int_value: IntProperty(
@@ -330,14 +319,17 @@ class Attribute(PropertyGroup):
filepath_value: PointerProperty(type=MultipleFileSelect)
filter_glob: StringProperty()
is_null: BoolProperty(name="Is Null", update=update_is_null)
is_optional: BoolProperty(name="Is Optional")
is_uri: BoolProperty(name="Is Uri", default=False)
is_selected: BoolProperty(name="Is Selected", default=False)
subitems_values: CollectionProperty(type=StrProperty) # pyright: ignore[reportRedeclaration]
# Attribute parameters.
is_optional: BoolProperty(name="Is Optional")
value_min: FloatProperty(description="This is used to validate int_value and float_value")
value_min_constraint: BoolProperty(default=False, description="True if the numerical value has a lower bound")
value_max: FloatProperty(description="This is used to validate int_value and float_value")
value_max_constraint: BoolProperty(default=False, description="True if the numerical value has an upper bound")
special_type: StringProperty(name="Special Value Type", default="")
use_explorer_ui: BoolProperty() # pyright: ignore[reportRedeclaration]
metadata: StringProperty(name="Metadata", description="For storing some additional information about the attribute")
update: StringProperty(name="Update", description="Custom update function to be executed")
@@ -360,23 +352,28 @@ class Attribute(PropertyGroup):
filepath_value: MultipleFileSelect
filter_glob: str
is_null: bool
is_optional: bool
is_uri: bool
is_selected: bool
subitems_values: bpy.types.bpy_prop_collection_idprop[StrProperty]
is_optional: bool
value_min: float
value_min_constraint: bool
value_max: float
value_max_constraint: bool
use_explorer_ui: bool
metadata: str
update: str
def get_value(self) -> Union[str, float, int, bool, None]:
def get_value(self) -> Union[str, float, int, bool, list[str], None]:
if self.is_optional and self.is_null:
return None
if self.data_type == "string":
return self.string_value.replace("\\n", "\n")
if self.data_type == "file":
return [f.name for f in self.filepath_value.file_list]
elif self.data_type == "list[string]":
return [s.name for s in self.subitems_values]
value_name = self.get_value_name()
if value_name == "enum_value":
value = tool.Blender.get_enum_safe(self, "enum_value")
@@ -387,7 +384,7 @@ class Attribute(PropertyGroup):
value = value == "TRUE"
return value
def get_value_default(self) -> Union[str, float, int, bool]:
def get_value_default(self) -> Union[str, float, int, bool, list[str]]:
data_type = self.data_type
if data_type == "string":
return ""
@@ -401,10 +398,12 @@ class Attribute(PropertyGroup):
return "0"
elif data_type == "file":
return ""
elif data_type == "list[string]":
return []
else:
assert_never(data_type)
def get_value_name(self, display_only: bool = False) -> str:
def get_value_name(self, display_only: bool = False):
"""Get name of the value attribute.
:param display_only: Should be `True` if the value won't be accessed directly
@@ -425,6 +424,8 @@ class Attribute(PropertyGroup):
return "enum_value"
elif data_type == "file":
return "filepath_value"
elif data_type == "list[string]":
return "subitems_values"
else:
assert_never(data_type)
@@ -505,16 +506,7 @@ class BIMProperties(PropertyGroup):
schema_dir: StringProperty(
default=os.path.join(cwd, "schema") + os.path.sep, name="Schema Directory", update=update_schema_dir
)
data_dir: StringProperty(
default=(platformdirs.user_data_path("bonsai", roaming=True, ensure_exists=True) / "data").__str__(),
name="Data Directory",
update=update_data_dir,
)
cache_dir: StringProperty(
default=platformdirs.user_cache_dir("bonsai"), name="Cache Directory", update=update_cache_dir
)
has_blend_warning: BoolProperty(name="Has Blend Warning", default=False)
pset_dir: StringProperty(default=os.path.join("psets") + os.path.sep, name="Default Psets Directory")
ifc_file: StringProperty(name="IFC File")
last_transaction: StringProperty(name="Last Transaction")
should_section_selected_objects: BoolProperty(name="Section Selected Objects", default=False)
@@ -570,8 +562,6 @@ class BIMProperties(PropertyGroup):
if TYPE_CHECKING:
is_dirty: bool
schema_dir: str
data_dir: str
cache_dir: str
has_blend_warning: bool
pset_dir: str
ifc_file: str
@@ -605,16 +595,12 @@ class PsetQto(PropertyGroup):
is_editable: BoolProperty(name="Is Editable")
if TYPE_CHECKING:
name: str
properties: bpy.types.bpy_prop_collection_idprop[Attribute]
is_expanded: bool
is_editable: bool
class GlobalId(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
class BIMCollectionProperties(PropertyGroup):
obj: PointerProperty(type=bpy.types.Object)
+4
View File
@@ -24,6 +24,10 @@ import bpy_restrict_state
class IfcSchema:
data_dir: str
cache_dir: str
schema_dir: str
def __init__(self, schema_identifier="IFC4"):
if schema_identifier not in ("IFC2X3", "IFC4", "IFC4X3_ADD2"):
schema_identifier = "IFC4"
+190 -21
View File
@@ -19,6 +19,7 @@
import os
import bpy
import platform
import platformdirs
import bonsai.bim.helper
from pathlib import Path
from bpy.types import Panel
@@ -35,11 +36,13 @@ import bonsai.bim
import bonsai.tool as tool
from ifcopenshell.util.file import IfcHeaderExtractor
from bonsai.bim.prop import Attribute
from typing import Optional, TYPE_CHECKING
from typing import Optional, TYPE_CHECKING, Literal
from natsort import natsorted
class IFCFileSelector:
layout: bpy.types.UILayout
# Avoid overriding blender prop annotations at runtime.
if TYPE_CHECKING:
filepath: str
@@ -76,6 +79,7 @@ class IFCFileSelector:
assert isinstance(context.space_data, bpy.types.SpaceFileBrowser)
# Access filepath & Directory https://blender.stackexchange.com/a/207665
params = context.space_data.params
assert params
# Decode byte string https://stackoverflow.com/a/47737082/
directory = Path(params.directory.decode("utf-8"))
filepath = os.path.join(directory, params.filename)
@@ -122,6 +126,7 @@ class BIM_PT_section_plane(Panel):
bl_parent_id = "BIM_PT_tab_sandbox"
def draw(self, context):
assert self.layout
layout = self.layout
layout.use_property_split = True
props = tool.Blender.get_bim_props()
@@ -145,6 +150,7 @@ class BIM_PT_section_with_cappings(Panel):
bl_parent_id = "BIM_PT_tab_sandbox"
def draw(self, context):
assert self.layout
layout = self.layout
wm = context.window_manager
row = layout.row(align=True)
@@ -189,7 +195,16 @@ class BIM_UL_clipping_plane(bpy.types.UIList):
class BIM_UL_generic(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,
item: bpy.types.PropertyGroup,
icon,
active_data,
active_propname,
) -> None:
if item:
layout.prop(item, "name", text="", emboss=False)
else:
@@ -205,6 +220,98 @@ class BIM_UL_topics(bpy.types.UIList):
layout.label(text="", translate=False)
class DocPreferences(bpy.types.PropertyGroup):
sheets_dir: StringProperty(
default=os.path.join("sheets") + os.path.sep,
name="Default Sheets Directory",
)
layouts_dir: StringProperty(
default=os.path.join("layouts") + os.path.sep,
name="Default Layouts Directory",
)
titleblocks_dir: StringProperty(
default=os.path.join("layouts", "titleblocks") + os.path.sep,
name="Default Titleblocks Directory",
)
drawings_dir: StringProperty(
default=os.path.join("drawings") + os.path.sep,
name="Default Drawings Directory",
)
stylesheet_path: StringProperty(
default=os.path.join("drawings", "assets", "default.css"),
name="Default Stylesheet",
)
schedules_stylesheet_path: StringProperty(
default=os.path.join("drawings", "assets", "schedule.css"),
name="Default Stylesheet for Schedules",
)
markers_path: StringProperty(
default=os.path.join("drawings", "assets", "markers.svg"),
name="Default Markers",
)
symbols_path: StringProperty(
default=os.path.join("drawings", "assets", "symbols.svg"),
name="Default Symbols",
)
patterns_path: StringProperty(
default=os.path.join("drawings", "assets", "patterns.svg"),
name="Default Patterns",
)
shadingstyles_path: StringProperty(
default=os.path.join("drawings", "assets", "shading_styles.json"),
name="Default Shading Styles",
)
shadingstyle_default: StringProperty(
default="Blender Default",
name="Default Shading Style",
)
drawing_font: StringProperty(
default="OpenGost Type B TT.ttf",
name="Drawing Font",
)
magic_font_scale: bpy.props.FloatProperty(
default=0.004118616,
name="Font Scale Factor",
)
imperial_precision: StringProperty(
default="1/32",
name="Imperial Precision",
)
tolerance: bpy.props.FloatProperty(
default=0.00001,
name="A tolerance used when selecting objects",
)
classes_to_wireframe: StringProperty(
default="IfcVirtualElement",
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:
sheets_dir: str
layouts_dir: str
titleblocks_dir: str
drawings_dir: str
stylesheet_path: str
schedules_stylesheet_path: str
markers_path: str
symbols_path: str
patterns_path: str
shadingstyles_path: str
shadingstyle_default: str
drawing_font: str
magic_font_scale: float
imperial_precision: str
tolerance: float
classes_to_wireframe: str
classes_no_cut: str
class BIM_ADDON_preferences(bpy.types.AddonPreferences):
bl_idname = tool.Blender.get_blender_addon_package_name()
svg2pdf_command: StringProperty(
@@ -320,6 +427,53 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
description="When modifying openings, other elements of the model will display with some transparency.\n0 is fully transparent and 100 is fully opaque",
)
bsdd_load_preview_dictionaries: BoolProperty(
name="Load Preview Dictionaries", description="Load dictionaries marked as Preview status", default=False
)
bsdd_load_inactive_dictionaries: BoolProperty(
name="Load Inactive Dictionaries", description="Load dictionaries marked as Inactive status", default=False
)
bsdd_load_test_dictionaries: BoolProperty(
name="Load Test Dictionaries", description="Load dictionaries that are for testing only", default=False
)
should_disable_undo_on_save: BoolProperty(
name="Disable Undo When Saving (Faster saves, no undo for you!)", default=False
)
should_stream: BoolProperty(name="Stream Data From IFC-SPF (Only for advanced users)", default=False)
occurrence_name_style: bpy.props.EnumProperty(
items=[("CLASS", "By Class", ""), ("TYPE", "By Type", ""), ("CUSTOM", "Custom", "")],
name="Occurrence Name Style",
)
occurrence_name_function: bpy.props.StringProperty(
name="Occurrence Name Function",
description="Code that will be evaluated to generate occurrence name for CUSTOM occurrence name style",
)
def update_data_dir(self, context: bpy.types.Context) -> None:
import bonsai.bim.schema
bonsai.bim.schema.ifc.data_dir = self.data_dir
def update_cache_dir(self, context: bpy.types.Context) -> None:
import bonsai.bim.schema
bonsai.bim.schema.ifc.cache_dir = self.cache_dir
data_dir: StringProperty(
default=(platformdirs.user_data_path("bonsai", roaming=True, ensure_exists=True) / "data").__str__(),
name="Data Directory",
update=update_data_dir,
)
cache_dir: StringProperty(
default=platformdirs.user_cache_dir("bonsai"), name="Cache Directory", update=update_cache_dir
)
pset_dir: StringProperty(
default=os.path.join("psets") + os.path.sep,
name="Default Psets Directory",
)
doc: bpy.props.PointerProperty(type=DocPreferences)
if TYPE_CHECKING:
svg2pdf_command: str
svg2dxf_command: str
@@ -341,6 +495,17 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
decorator_color_error: tuple[float, float, float, float]
decorator_color_background: tuple[float, float, float, float]
opening_focus_opacity: int
bsdd_load_preview_dictionaries: bool
bsdd_load_inactive_dictionaries: bool
bsdd_load_test_dictionaries: bool
should_disable_undo_on_save: bool
should_stream: bool
occurrence_name_style: Literal["CLASS", "TYPE", "CUSTOM"]
occurrence_name_function: str
data_dir: str
cache_dir: str
pset_dir: str
doc: DocPreferences
def draw(self, context: bpy.types.Context) -> None:
layout = self.layout
@@ -381,29 +546,26 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
layout.prop(self, "spatial_elements_unselectable")
def draw_model_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
props = tool.Model.get_model_props()
layout.prop(props, "occurrence_name_style")
if props.occurrence_name_style == "CUSTOM":
layout.prop(props, "occurrence_name_function")
layout.prop(self, "occurrence_name_style")
if self.occurrence_name_style == "CUSTOM":
layout.prop(self, "occurrence_name_function")
def draw_directories(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
props = tool.Blender.get_bim_props()
row = layout.row(align=True)
row.prop(props, "data_dir")
row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "scene.BIMProperties.data_dir"
row.prop(self, "data_dir")
row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "preferences.data_dir"
row = layout.row(align=True)
row.prop(props, "cache_dir")
row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "scene.BIMProperties.cache_dir"
row.prop(self, "cache_dir")
row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "preferences.cache_dir"
row = layout.row(align=True)
row.prop(self, "tmp_dir")
row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "preferences.tmp_dir"
def draw_drawing_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
props = tool.Blender.get_bim_props()
layout.prop(props, "pset_dir")
dprops = tool.Drawing.get_document_props()
layout.prop(self, "pset_dir")
dprops = self.doc
layout.prop(dprops, "sheets_dir")
layout.prop(dprops, "layouts_dir")
layout.prop(dprops, "titleblocks_dir")
@@ -433,14 +595,12 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
def draw_other_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "opening_focus_opacity")
props = tool.Project.get_project_props()
layout.prop(props, "should_disable_undo_on_save")
layout.prop(props, "should_stream")
bprops = tool.Bsdd.get_bsdd_props()
layout.prop(self, "should_disable_undo_on_save")
layout.prop(self, "should_stream")
layout.label(text="bSDD:")
layout.prop(bprops, "load_preview_dictionaries")
layout.prop(bprops, "load_inactive_dictionaries")
layout.prop(bprops, "load_test_dictionaries")
layout.prop(self, "bsdd_load_preview_dictionaries")
layout.prop(self, "bsdd_load_inactive_dictionaries")
layout.prop(self, "bsdd_load_test_dictionaries")
# Scene panel groups
@@ -1186,6 +1346,13 @@ class EnumData:
organizations = tool.Ifc.get().by_type("IfcOrganization")
return natsorted(((str(e.id()), e.Name, "") for e in organizations), key=lambda x: x[1])
@classmethod
def postal_addresses(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
addresses = tool.Ifc.get().by_type("IfcPostalAddress")
items = ((str(e.id()), (e.Description or "Undescribed"), "") for e in addresses)
items = natsorted(items, key=lambda x: x[1])
return items
class UIData:
data = {}
@@ -1334,6 +1501,8 @@ class BIM_PT_decorators_overlay(Panel):
row = col.row(align=True)
row.prop(model_props, "show_slab_direction", text="Slab Direction")
row = col.row(align=True)
row.prop(model_props, "show_bounding_box", text="Bounding Box Dimensions")
row = col.row(align=True)
row.prop(model_props, "show_cut_decorator", text="Cut Decorator")
row.prop(model_props, "show_cut_decorator_fill", text="Fill Cut Decorator")
-2
View File
@@ -30,7 +30,6 @@ def add_context(
context_type: Optional[str] = None,
context_identifier: Optional[str] = None,
target_view: Optional[str] = None,
target_scale: Optional[float] = None,
parent: Optional[str] = None,
) -> ifcopenshell.entity_instance:
return ifc.run(
@@ -38,7 +37,6 @@ def add_context(
context_type=context_type,
context_identifier=context_identifier,
target_view=target_view,
target_scale=target_scale,
parent=parent,
)
+17 -3
View File
@@ -340,9 +340,10 @@ def select_cost_schedule_products(
def import_cost_schedule_csv(
cost: type[tool.Cost], file_path: str, is_schedule_of_rates: bool
cost: type[tool.Cost], resolved_path: str, is_schedule_of_rates: bool
) -> ifcopenshell.entity_instance:
cost_schedule = cost.import_cost_schedule_csv(file_path, is_schedule_of_rates)
cost_schedule = cost.import_cost_schedule_csv(resolved_path, is_schedule_of_rates)
return cost_schedule
@@ -354,12 +355,18 @@ def remove_csv_filepath(cost: type[tool.Cost], cost_schedule) -> None:
cost.remove_csv_filepath(cost_schedule)
def refresh_cost_schedule_csv(ifc: type[tool.Ifc], cost: type[tool.Cost]) -> None:
def refresh_cost_schedule_csv(cost: type[tool.Cost]) -> Optional[str]:
cost.delete_all_cost_items()
cost.refresh_cost_schedule_csv()
cost.load_cost_schedule_tree()
def remove_cost_schedule_csv_link(cost: type[tool.Cost]) -> None:
cost_schedule = cost.get_active_cost_schedule()
if cost_schedule:
cost.remove_csv_filepath(cost_schedule)
def add_cost_column(cost: type[tool.Cost], name: str) -> None:
cost.add_cost_column(name)
@@ -396,6 +403,13 @@ def export_cost_schedules(
return cost.export_cost_schedules(dirpath, format, cost_schedule)
def export_cost_schedules_to_pdf(
cost: type[tool.Cost], filepath: str, cost_schedule: ifcopenshell.entity_instance, options: dict
):
cost.play_sound()
return cost.export_cost_schedules_to_pdf(filepath, cost_schedule, options)
def clear_cost_item_assignments(
ifc: type[tool.Ifc],
cost: type[tool.Cost],
+5 -6
View File
@@ -51,7 +51,7 @@ def add_instance_flooring_covering_from_cursor(
if isinstance(space_polygon, str):
return
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0)
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, polygon_is_si=True)
name = "Covering"
mesh = spatial.get_named_mesh_from_bmesh(name=name, bmesh=bm)
@@ -65,7 +65,6 @@ def add_instance_flooring_covering_from_cursor(
spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
body = spatial.get_body_representation(obj)
print(obj, body)
spatial.regen_obj_representation(obj, body)
@@ -96,7 +95,7 @@ def add_instance_ceiling_covering_from_cursor(
if isinstance(space_polygon, str):
return
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0)
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, polygon_is_si=True)
name = "Covering"
mesh = spatial.get_named_mesh_from_bmesh(name=name, bmesh=bm)
@@ -128,7 +127,7 @@ def regen_selected_covering_object(root: type[tool.Root], spatial: type[tool.Spa
if isinstance(space_polygon, str):
return
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0)
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, polygon_is_si=True)
name = "Aux"
mesh = spatial.get_named_mesh_from_bmesh(name=name, bmesh=bm)
@@ -152,7 +151,7 @@ def add_instance_flooring_coverings_from_walls(root: type[tool.Root], spatial: t
union = spatial.get_union_shape_from_selected_objects()
for i, linear_ring in enumerate(union.interiors):
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
bm = spatial.get_bmesh_from_polygon(poly, h=0)
bm = spatial.get_bmesh_from_polygon(poly, h=0, polygon_is_si=False)
name = "Covering" + str(i)
obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm)
@@ -180,7 +179,7 @@ def add_instance_ceiling_coverings_from_walls(
union = spatial.get_union_shape_from_selected_objects()
for i, linear_ring in enumerate(union.interiors):
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
bm = spatial.get_bmesh_from_polygon(poly, h=0)
bm = spatial.get_bmesh_from_polygon(poly, h=0, polygon_is_si=False)
name = "Covering" + str(i)
obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm)
+11 -2
View File
@@ -18,7 +18,7 @@
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING, Optional, Union
from typing import TYPE_CHECKING, Optional, Union, Literal
if TYPE_CHECKING:
import bpy
@@ -108,13 +108,22 @@ def add_sheet(ifc: type[tool.Ifc], drawing: type[tool.Drawing], titleblock: ifco
drawing.import_sheets()
def regenerate_sheet(drawing: type[tool.Drawing], sheet: ifcopenshell.entity_instance) -> None:
def regenerate_sheet(
drawing: type[tool.Drawing], sheet: ifcopenshell.entity_instance
) -> Union[list[tool.Drawing.SheetWarningType], None]:
warnings = drawing.validate_sheet_files(sheet)
if warnings:
return warnings
titleblock_uri = drawing.get_document_uri(sheet, "TITLEBLOCK")
assert titleblock_uri
drawing.create_svg_sheet(sheet, drawing.sanitise_filename(Path(titleblock_uri).stem))
try:
drawing.add_drawings(sheet)
except FileNotFoundError:
path_layout = drawing.get_document_uri(sheet, "LAYOUT")
assert path_layout
if drawing.does_file_exist(path_layout):
drawing.delete_file(path_layout)
+9 -3
View File
@@ -110,10 +110,15 @@ def select_container(
spatial.set_active_object(ifc.get_object(container), selection_mode=selection_mode)
def select_similar_container(ifc: type[tool.Ifc], spatial: type[tool.Spatial], obj: bpy.types.Object) -> None:
def select_similar_container(
ifc: type[tool.Ifc],
spatial: type[tool.Spatial],
obj: bpy.types.Object,
is_recursive: bool = True,
) -> None:
element = ifc.get_entity(obj)
if element:
spatial.select_products(spatial.get_decomposed_elements(spatial.get_container(element)))
spatial.select_products(spatial.get_decomposed_elements(spatial.get_container(element), is_recursive))
def select_product(spatial: type[tool.Spatial], product: ifcopenshell.entity_instance) -> None:
@@ -212,6 +217,7 @@ def generate_space(
if element and element.is_a("IfcSpace"):
mesh = spatial.get_transformed_mesh_from_local_to_global(mesh)
spatial.edit_active_space_obj_from_mesh(mesh)
spatial.translate_obj_to_z_location(active_obj, z)
else:
if relating_type:
name = model.generate_occurrence_name(relating_type, "IfcSpace")
@@ -242,7 +248,7 @@ def generate_spaces_from_walls(
for i, linear_ring in enumerate(union.interiors):
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
bm = spatial.get_bmesh_from_polygon(poly, h)
bm = spatial.get_bmesh_from_polygon(poly, h, polygon_is_si=False)
name = "Space" + str(i)
+7 -2
View File
@@ -75,6 +75,11 @@ class Aggregate:
def get_relating_object(cls, related_element): pass
@interface
class Attribute:
pass
@interface
class Bcf:
pass
@@ -223,6 +228,7 @@ class Cost:
def expand_cost_item(cls, cost_item_id): pass
def expand_cost_items(cls): pass
def export_cost_schedules(cls, filepath, format, cost_schedule): pass
def export_cost_schedules_to_pdf(cls, filepath, cost_schedule, options): pass
def format_unit(cls, unit): pass
def get_active_cost_item(cls): pass
def get_active_cost_schedule(cls): pass
@@ -537,7 +543,6 @@ class Material:
def ensure_material_assigned(cls, elements, material_type, material): pass
def ensure_material_unassigned(cls, elements): pass
def ensure_new_material_set_is_valid(cls, material): pass
def get_active_material_item(cls): pass
def get_active_material_type(cls): pass
def get_default_material(cls): pass
def get_elements_by_material(cls, material): pass
@@ -927,7 +932,7 @@ class Spatial:
def filter_products(cls, products, action): pass
def get_active_container(cls): pass
def get_container(cls, element): pass
def get_decomposed_elements(cls, container): pass
def get_decomposed_elements(cls, container, recursive): pass
def get_object_matrix(cls, obj): pass
def get_relative_object_matrix(cls, target_obj, relative_to_obj): pass
def get_selected_product_types(cls): pass
+1
View File
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from bonsai.tool.aggregate import Aggregate
from bonsai.tool.attribute import Attribute
from bonsai.tool.bcf import Bcf
from bonsai.tool.blender import Blender
from bonsai.tool.boundary import Boundary
+93
View File
@@ -0,0 +1,93 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2021 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/>.
from __future__ import annotations
import bpy
import bonsai.core.tool
import bonsai.bim.helper as helper
import bonsai.tool as tool
import ifcopenshell
from typing import (
Any,
Optional,
Union,
Literal,
TypeVar,
TYPE_CHECKING,
assert_never,
)
if TYPE_CHECKING:
from bonsai.bim.module.attribute.prop import BIMAttributeProperties, BIMExplorerProperties
T = TypeVar("T")
class Attribute(bonsai.core.tool.Attribute):
@classmethod
def get_explorer_props(cls) -> BIMExplorerProperties:
assert (scene := bpy.context.scene)
return scene.BIMExplorerProperties # pyright: ignore[reportAttributeAccessIssue]
@classmethod
def refresh_uilist_entities(cls) -> None:
props = cls.get_explorer_props()
props.entities.clear()
ifc_file = tool.Ifc.get()
for element in ifc_file.by_type(props.ifc_class):
entity = props.entities.add()
entity.name = str(element)
entity.ifc_definition_id = element.id()
@classmethod
def enable_editing_entity(cls, entity: ifcopenshell.entity_instance) -> None:
props = cls.get_explorer_props()
props.editing_entity_id = entity.id()
@classmethod
def disable_editing_entity(cls) -> None:
props = cls.get_explorer_props()
props.entity_attributes.clear()
props.property_unset("editing_entity_id")
@classmethod
def import_entity_attributes(cls, entity: ifcopenshell.entity_instance) -> None:
props = cls.get_explorer_props()
helper.import_attributes(entity, props.entity_attributes)
@classmethod
def export_entity_attributes(cls) -> dict[str, Any]:
props = cls.get_explorer_props()
attributes = helper.export_attributes(props.entity_attributes)
return attributes
EXPLORER_UI_LOOKUP_PROPS: dict[str, dict[str, str]] = {
"IfcSite": {"SiteAddress": "postal_addresses"},
"IfcBuilding": {"BuildingAddress": "postal_addresses"},
}
"""``ifc_class -> {attribute_name -> enum_items_dynamic}``"""
@classmethod
def does_ifc_class_support_explorer_lookup(cls, ifc_class: str) -> Union[dict[str, str], None]:
"""
:return: ``{attribute_name -> enum_items_dynamic}``
if IFC class does support explorer UI lookup, otherwise returns ``None``.
"""
if not (lookup_attrs := cls.EXPLORER_UI_LOOKUP_PROPS.get(ifc_class)):
return
return lookup_attrs
+135 -14
View File
@@ -61,6 +61,7 @@ if TYPE_CHECKING:
from bonsai.bim.module.constraint.prop import BIMConstraintProperties, BIMObjectConstraintProperties
from bonsai.bim.module.csv.prop import CsvProperties
from bonsai.bim.module.diff.prop import DiffProperties
from bonsai.bim.module.fm.prop import BIMFMProperties
from bonsai.bim.module.group.prop import BIMGroupProperties
from bonsai.bim.module.light.prop import BIMSolarProperties, RadianceExporterProperties
@@ -785,7 +786,6 @@ class Blender(bonsai.core.tool.Blender):
to False, which will make datablock deletion significantly faster
by avoiding unnecessary Blender data checks.
:return: None
:rtype: None
"""
collection_name = repr(data_block).split(".", 2)[-1].split("[", 1)[0]
getattr(bpy.data, collection_name).remove(
@@ -799,7 +799,6 @@ class Blender(bonsai.core.tool.Blender):
:param data_blocks: iterable of data blocks to remove
:param remove_unused_data: set to True to purge data that would be orphaned by the operation
:return: None
:rtype: None
"""
data_blocks = list(data_blocks)
if remove_unused_data:
@@ -1535,6 +1534,67 @@ class Blender(bonsai.core.tool.Blender):
obj.matrix_world = matrix
return True
@classmethod
def get_full_data_path(cls, bpy_struct: bpy.types.bpy_struct, path: str = "") -> str:
"""Get full data path to Blender entity or it's attributes.
:param bpy_struct: Blender entity.
:param path: Additional path to add to entity.
:return: Path in a format
``bpy.data.scenes['Scene'].BIMExplorerProperties.entity_attributes[4].enum_value``
"""
if path:
bpy_prop: bpy.types.bpy_prop # pyright: ignore[reportAttributeAccessIssue]
bpy_prop = bpy_struct.path_resolve(path, False)
return repr(bpy_prop)
return repr(bpy_struct)
@classmethod
def resolve_data_path_to_data_attr(cls, data_path: str) -> tuple[bpy.types.bpy_struct, str]:
"""
:param data_path: Non-full data path to attribute.
Examples:
- `preferences.prop_group.string_prop` (`preferences` would mean addon preferences)
- `scene.string_prop` (`scene` can be any member of `Context`)
:return: Resolved tuple of Blender Struct and property name.
Examples:
- `(preferences.prop_group, "string_prop)`
- `(scene, "string_prop)`
"""
# Get data to modify.
base_path, _, data_path_ = data_path.partition(".")
if base_path == "preferences":
data = tool.Blender.get_addon_preferences()
data_path = data_path_
else:
data = bpy.context
# Get property group if available.
base_path, _, attr = data_path.rpartition(".")
if base_path:
data = data.path_resolve(base_path)
return data, attr
@classmethod
@contextlib.contextmanager
def preserve_prop_value(cls, bpy_object: bpy.types.bpy_struct, prop_name: str):
if bpy_object.is_property_set(prop_name):
prop_value = getattr(bpy_object, prop_name)
else:
prop_value = ...
try:
yield
except:
raise
finally:
if prop_value is ...:
bpy_object.property_unset(prop_name)
return
setattr(bpy_object, prop_name, prop_value)
@classmethod
def set_prop_from_path(cls, bpy_object: bpy.types.bpy_struct, prop_path: str, value: Any) -> None:
"""Set `data_block` property value using path from `path_from_id`."""
@@ -1603,7 +1663,7 @@ class Blender(bonsai.core.tool.Blender):
@classmethod
def get_user_data_dir(cls) -> Path:
props = cls.get_bim_props()
props = cls.get_addon_preferences()
return Path(props.data_dir)
@classmethod
@@ -1665,51 +1725,56 @@ class Blender(bonsai.core.tool.Blender):
@classmethod
def get_object_constraint_props(cls, obj: bpy.types.Object) -> BIMObjectConstraintProperties:
return obj.BIMObjectConstraintProperties
return obj.BIMObjectConstraintProperties # pyright: ignore[reportAttributeAccessIssue]
@classmethod
def get_constraint_props(cls) -> BIMConstraintProperties:
assert (scene := bpy.context.scene)
return scene.BIMConstraintProperties
return scene.BIMConstraintProperties # pyright: ignore[reportAttributeAccessIssue]
@classmethod
def get_csv_props(cls) -> CsvProperties:
assert (scene := bpy.context.scene)
return scene.CsvProperties
return scene.CsvProperties # pyright: ignore[reportAttributeAccessIssue]
@classmethod
def get_diff_props(cls) -> DiffProperties:
assert (scene := bpy.context.scene)
return scene.DiffProperties
return scene.DiffProperties # pyright: ignore[reportAttributeAccessIssue]
@classmethod
def get_group_props(cls) -> BIMGroupProperties:
assert (scene := bpy.context.scene)
return scene.BIMGroupProperties
return scene.BIMGroupProperties # pyright: ignore[reportAttributeAccessIssue]
@classmethod
def get_bim_props(cls, scene: Optional[bpy.types.Scene] = None) -> BIMProperties:
if scene is None:
scene = bpy.context.scene
return scene.BIMProperties
assert (scene := bpy.context.scene)
return scene.BIMProperties # pyright: ignore[reportAttributeAccessIssue]
@classmethod
def get_object_bim_props(cls, obj: bpy.types.Object) -> BIMObjectProperties:
return obj.BIMObjectProperties
return obj.BIMObjectProperties # pyright: ignore[reportAttributeAccessIssue]
@classmethod
def get_object_attribute_props(cls, obj: bpy.types.Object) -> BIMAttributeProperties:
return obj.BIMAttributeProperties
return obj.BIMAttributeProperties # pyright: ignore[reportAttributeAccessIssue]
@classmethod
def get_solar_props(cls) -> BIMSolarProperties:
assert (scene := bpy.context.scene)
return scene.BIMSolarProperties
return scene.BIMSolarProperties # pyright: ignore[reportAttributeAccessIssue]
@classmethod
def get_radiance_exporter_props(cls) -> RadianceExporterProperties:
assert (scene := bpy.context.scene)
return scene.BIMRadianceExporeterProperies
return scene.BIMRadianceExporeterProperies # pyright: ignore[reportAttributeAccessIssue]
@classmethod
def get_fm_props(cls) -> BIMFMProperties:
assert (scene := bpy.context.scene)
return scene.BIMFMProperties # pyright: ignore[reportAttributeAccessIssue]
@classmethod
def get_ifc_definition_id(cls, obj: IFC_CONNECTED_TYPE) -> int:
@@ -1845,3 +1910,59 @@ class Blender(bonsai.core.tool.Blender):
# Remove file if crash didn't happened.
path.unlink()
@classmethod
def sync_old_preferences(cls) -> None:
# Added on 25.07.15.
# TODO: deprecate later.
settings_remap = {
"scene.BIMBSDDProperties.load_preview_dictionaries": "preferences.bsdd_load_preview_dictionaries",
"scene.BIMBSDDProperties.load_inactive_dictionaries": "preferences.bsdd_load_inactive_dictionaries",
"scene.BIMBSDDProperties.load_test_dictionaries": "preferences.bsdd_load_test_dictionaries",
"scene.BIMProjectProperties.should_disable_undo_on_save": "preferences.should_disable_undo_on_save",
"scene.BIMProjectProperties.should_stream": "preferences.should_stream",
"scene.BIMModelProperties.occurrence_name_style": "preferences.occurrence_name_style",
"scene.BIMModelProperties.occurrence_name_function": "preferences.occurrence_name_function",
"scene.BIMProperties.pset_dir": "preferences.pset_dir",
"scene.BIMProperties.data_dir": "preferences.data_dir",
"scene.BIMProperties.cache_dir": "preferences.cache_dir",
"scene.DocProperties.sheets_dir": "preferences.doc.sheets_dir",
"scene.DocProperties.layouts_dir": "preferences.doc.layouts_dir",
"scene.DocProperties.titleblocks_dir": "preferences.doc.titleblocks_dir",
"scene.DocProperties.drawings_dir": "preferences.doc.drawings_dir",
"scene.DocProperties.stylesheet_path": "preferences.doc.stylesheet_path",
"scene.DocProperties.schedules_stylesheet_path": "preferences.doc.schedules_stylesheet_path",
"scene.DocProperties.markers_path": "preferences.doc.markers_path",
"scene.DocProperties.symbols_path": "preferences.doc.symbols_path",
"scene.DocProperties.patterns_path": "preferences.doc.patterns_path",
"scene.DocProperties.shadingstyles_path": "preferences.doc.shadingstyles_path",
"scene.DocProperties.shadingstyle_default": "preferences.doc.shadingstyle_default",
"scene.DocProperties.drawing_font": "preferences.doc.drawing_font",
"scene.DocProperties.magic_font_scale": "preferences.doc.magic_font_scale",
"scene.DocProperties.imperial_precision": "preferences.doc.imperial_precision",
"scene.DocProperties.tolerance": "preferences.doc.tolerance",
"scene.DocProperties.classes_to_wireframe": "preferences.doc.classes_to_wireframe",
"scene.DocProperties.classes_no_cut": "preferences.doc.classes_no_cut",
}
props_updated = False
for old_path, path in settings_remap.items():
data, attr = cls.resolve_data_path_to_data_attr(path)
# User already overridden the value.
if data.is_property_set(attr):
continue
data_old, attr_old = cls.resolve_data_path_to_data_attr(old_path)
# User was only using default value previously.
if attr_old not in data_old:
continue
old_value = data_old[attr_old]
print(f"Updating {path} based on previous value from {old_path} - '{old_value}'.")
setattr(data, attr, old_value)
props_updated = True
# Doesn't seem to save on exit if edited from Python API, so we do it manually.
assert bpy.context.preferences
if props_updated and bpy.context.preferences.use_preferences_save:
bpy.ops.wm.save_userpref()
+8 -6
View File
@@ -107,20 +107,22 @@ class Bsdd(bonsai.core.tool.Bsdd):
@classmethod
def get_dictionary(cls, uri: str) -> bsdd.DictionaryContractV1:
props = cls.get_bsdd_props()
response = cls.client.get_dictionary(dictionary_uri=uri, include_test_dictionaries=props.load_test_dictionaries)
prefs = tool.Blender.get_addon_preferences()
response = cls.client.get_dictionary(
dictionary_uri=uri, include_test_dictionaries=prefs.bsdd_load_test_dictionaries
)
if dicts := response.get("dictionaries"):
return dicts[0]
@classmethod
def get_dictionaries(cls) -> list[bsdd.DictionaryContractV1]:
props = cls.get_bsdd_props()
response = cls.client.get_dictionary(include_test_dictionaries=props.load_test_dictionaries)
prefs = tool.Blender.get_addon_preferences()
response = cls.client.get_dictionary(include_test_dictionaries=prefs.bsdd_load_test_dictionaries)
dicts = response.get("dictionaries") or []
statuses = ["Active"]
if props.load_preview_dictionaries:
if prefs.bsdd_load_preview_dictionaries:
statuses.append("Preview")
if props.load_inactive_dictionaries:
if prefs.bsdd_load_inactive_dictionaries:
statuses.append("Inactive")
return list(filter(lambda d: d["status"] in statuses, dicts))
+1 -2
View File
@@ -119,8 +119,7 @@ class Clash(bonsai.core.tool.Clash):
if not clash_set:
return
clash_set.clashes.clear()
if "clashes_loaded" in clash_set:
del clash_set["clashes_loaded"]
clash_set.property_unset("clashes_loaded")
@classmethod
def load_clash_sets(cls, fn: str) -> None:
+6 -4
View File
@@ -46,7 +46,7 @@ class Context(bonsai.core.tool.Context):
props.context_attributes.clear()
context = cls.get_context()
def callback(name: str, prop, data) -> Union[bool, None]:
def callback(name: str, prop: Union[Attribute, None], data: dict[str, Any]) -> Union[bool, None]:
if context.is_a("IfcGeometricRepresentationSubContext"):
if name == "Precision":
props.context_attributes.remove(props.context_attributes.find("Precision"))
@@ -57,8 +57,9 @@ class Context(bonsai.core.tool.Context):
elif name == "TargetScale":
props.context_attributes.remove(props.context_attributes.find("TargetScale"))
scale_denominator = None
if data.get(name) is not None and data.get(name) != 0:
scale_denominator = 1.0 / data.get(name)
value = data.get(name)
if value not in (None, 0):
scale_denominator = 1.0 / value
new_prop = props.context_attributes.add()
new_prop.name = "ScaleDenominator"
new_prop.data_type = "float"
@@ -68,9 +69,10 @@ class Context(bonsai.core.tool.Context):
else: # IfcGeometricRepresentationContext
# Import precision as a string because Blender has problem displaying 1e-7 and smaller numbers in UI.
if name == "Precision":
assert prop
prop.data_type = "string"
bonsai.bim.helper.import_attributes(context.is_a(), props.context_attributes, context.get_info(), callback)
bonsai.bim.helper.import_attributes(context, props.context_attributes, callback)
@classmethod
def clear_context(cls) -> None:
+107 -33
View File
@@ -22,6 +22,7 @@ import bonsai.core.tool
import bonsai.tool as tool
import ifcopenshell.api
import ifcopenshell.api.cost
import ifcopenshell.api.document
import ifcopenshell.api.nest
import ifcopenshell.util.element
import ifcopenshell.util.date
@@ -34,6 +35,7 @@ from typing import Optional, Any, Union, Literal, TYPE_CHECKING, assert_never
from collections.abc import Generator
if TYPE_CHECKING:
from bonsai.bim.prop import Attribute
from bonsai.bim.module.cost.prop import BIMCostProperties, CostItemQuantity
@@ -128,7 +130,7 @@ class Cost(bonsai.core.tool.Cost):
props = cls.get_cost_props()
props.cost_schedule_attributes.clear()
bonsai.bim.helper.import_attributes2(cost_schedule, props.cost_schedule_attributes, callback=special_import)
bonsai.bim.helper.import_attributes(cost_schedule, props.cost_schedule_attributes, callback=special_import)
@classmethod
def enable_editing_cost_items(cls, cost_schedule: ifcopenshell.entity_instance) -> None:
@@ -234,7 +236,7 @@ class Cost(bonsai.core.tool.Cost):
def load_cost_item_attributes(cls, cost_item: ifcopenshell.entity_instance) -> None:
props = cls.get_cost_props()
props.cost_item_attributes.clear()
bonsai.bim.helper.import_attributes2(cost_item, props.cost_item_attributes)
bonsai.bim.helper.import_attributes(cost_item, props.cost_item_attributes)
@classmethod
def disable_editing_cost_item(cls) -> None:
@@ -379,7 +381,7 @@ class Cost(bonsai.core.tool.Cost):
def load_cost_item_quantity_attributes(cls, physical_quantity: ifcopenshell.entity_instance) -> None:
props = cls.get_cost_props()
props.quantity_attributes.clear()
bonsai.bim.helper.import_attributes2(physical_quantity, props.quantity_attributes)
bonsai.bim.helper.import_attributes(physical_quantity, props.quantity_attributes)
@classmethod
def enable_editing_cost_item_values(cls, cost_item: ifcopenshell.entity_instance) -> None:
@@ -415,7 +417,13 @@ class Cost(bonsai.core.tool.Cost):
@classmethod
def load_cost_item_value_attributes(cls, cost_value: ifcopenshell.entity_instance) -> None:
def import_attributes(name, prop, data, cost_value, is_rates, props_collection):
props = cls.get_cost_props()
props.cost_value_attributes.clear()
props_collection = props.cost_value_attributes
# is_rates = cls.is_active_schedule_of_rates()
is_rates = True # so it is possible to assign a cost item rate that it not only from a Schedule of Rate
def import_attributes_callback(name: str, prop: Union[Attribute, None], data) -> None | Literal[True]:
if name == "AppliedValue":
# TODO: for now, only support simple IfcValues (which are effectively IfcMonetaryMeasure)
prop = props_collection.add()
@@ -452,14 +460,9 @@ class Cost(bonsai.core.tool.Cost):
break
return True
props = cls.get_cost_props()
props.cost_value_attributes.clear()
# is_rates = cls.is_active_schedule_of_rates()
is_rates = True # so it is possible to assign a cost item rate that it not only from a Schedule of Rate
callback = lambda name, prop, data: import_attributes(
name, prop, data, cost_value, is_rates, props.cost_value_attributes
bonsai.bim.helper.import_attributes(
cost_value, props.cost_value_attributes, callback=import_attributes_callback
)
bonsai.bim.helper.import_attributes2(cost_value, props.cost_value_attributes, callback=callback)
@classmethod
def calculate_applied_value(
@@ -584,11 +587,33 @@ class Cost(bonsai.core.tool.Cost):
import time
start = time.time()
csv2ifc = Csv2Ifc(file_path, tool.Ifc.get(), is_schedule_of_rates=is_schedule_of_rates)
resolved_path = tool.Ifc.resolve_uri(file_path)
csv2ifc = Csv2Ifc(resolved_path, tool.Ifc.get(), is_schedule_of_rates=is_schedule_of_rates)
csv2ifc.execute()
print("Import finished in {:.2f} seconds".format(time.time() - start))
return csv2ifc.cost_schedule
@classmethod
def get_or_create_cost_documents(cls) -> ifcopenshell.entity_instance:
ifc_file = tool.Ifc.get()
cost_docs_document = next(
(
document
for document in ifc_file.by_type("IfcDocumentInformation")
if document.Name == "BBIM_Cost_Documents"
),
None,
)
if not cost_docs_document:
cost_docs_document = ifcopenshell.api.document.add_information(ifc_file)
cost_docs_document.Name = "BBIM_Cost_Documents"
cost_docs_document.Description = "Bonsai internal document containing references to cost CSV files"
return cost_docs_document
@classmethod
def add_csv_filepath(
cls,
@@ -596,33 +621,41 @@ class Cost(bonsai.core.tool.Cost):
is_schedule_of_rates: bool = False,
cost_schedule: ifcopenshell.entity_instance = None,
) -> None:
if not file_path:
if not file_path or not cost_schedule:
return
props = cls.get_cost_props()
if not props.active_cost_schedule_id in [item.cost_schedule_id for item in props.cost_schedule_files]:
item = props.cost_schedule_files.add()
item.cost_schedule_id = cost_schedule.id()
item.csv_filepath = file_path
ifc_file = tool.Ifc.get()
cost_docs_document = cls.get_or_create_cost_documents()
reference = ifcopenshell.api.document.add_reference(ifc_file, cost_docs_document)
reference.Location = file_path
reference.Description = f"Cost Schedule ID: {cost_schedule.id()}"
if is_schedule_of_rates:
reference.Identification = "SCHEDULE_OF_RATES"
else:
return
reference.Identification = "COST_SCHEDULE"
@classmethod
def remove_csv_filepath(cls, cost_schedule: ifcopenshell.entity_instance = None) -> None:
if not cost_schedule:
return
props = cls.get_cost_props()
cost_schedule_id = cost_schedule.id()
if cost_schedule_id in [item.cost_schedule_id for item in props.cost_schedule_files]:
for i, item in enumerate(props.cost_schedule_files):
if item.cost_schedule_id == cost_schedule_id:
props.cost_schedule_files.remove(i)
print(f"Cost schedule id={cost_schedule_id} csv filepath correctly removed")
return
else:
ifc_file = tool.Ifc.get()
cost_docs_document = cls.get_or_create_cost_documents()
if not cost_docs_document:
return
cost_schedule_id = cost_schedule.id()
references = tool.Document.get_document_references(cost_docs_document)
for reference in references:
if reference.Description and f"Cost Schedule ID: {cost_schedule_id}" in reference.Description:
ifcopenshell.api.document.remove_reference(ifc_file, reference)
return
@classmethod
def delete_all_cost_items(cls):
cost_schedule = tool.Cost.get_active_cost_schedule()
@@ -632,25 +665,59 @@ class Cost(bonsai.core.tool.Cost):
ifcopenshell.api.cost.remove_cost_item(tool.Ifc.get(), cost_item=item)
tool.Cost.clean_up_cost_item_tree(cost_item_id)
@classmethod
def is_schedule_of_rates_csv(cls, cost_schedule_id: int) -> bool:
"""Check if a cost schedule is a schedule of rates based on document references."""
cost_docs_document = cls.get_or_create_cost_documents()
if not cost_docs_document:
return False
references = tool.Document.get_document_references(cost_docs_document)
for reference in references:
if reference.Description and f"Cost Schedule ID: {cost_schedule_id}" in reference.Description:
return reference.Identification == "SCHEDULE_OF_RATES"
return False
@classmethod
def get_cost_schedule_csv_filepath(cls, cost_schedule_id: int) -> Optional[str]:
cost_docs_document = cls.get_or_create_cost_documents()
if not cost_docs_document:
return None
references = tool.Document.get_document_references(cost_docs_document)
for reference in references:
if reference.Description and f"Cost Schedule ID: {cost_schedule_id}" in reference.Description:
return reference.Location
return None
@classmethod
def refresh_cost_schedule_csv(cls):
"""Refresh cost schedule from CSV file stored in document references."""
from ifc5d.csv2ifc import Csv2Ifc
props = cls.get_cost_props()
cost_schedule_id = props.active_cost_schedule_id
file_path = next(
(item.csv_filepath for item in props.cost_schedule_files if item.cost_schedule_id == cost_schedule_id), None
)
file_path = cls.get_cost_schedule_csv_filepath(cost_schedule_id)
if not file_path:
return
resolved_path = tool.Ifc.resolve_uri(file_path)
cost_schedule = tool.Ifc.get_entity_by_id(cost_schedule_id)
is_schedule_of_rates = cls.is_schedule_of_rates_csv(cost_schedule_id)
csv2ifc = Csv2Ifc()
csv2ifc.csv = file_path
csv2ifc.csv = resolved_path
csv2ifc.file = tool.Ifc.get()
csv2ifc.cost_schedule = cost_schedule
csv2ifc.is_schedule_of_rates = False
csv2ifc.is_schedule_of_rates = is_schedule_of_rates
csv2ifc.refresh()
print("Csv file correctly refreshed")
@@ -778,6 +845,13 @@ class Cost(bonsai.core.tool.Cost):
except:
return "Could not open file location"
@classmethod
def export_cost_schedules_to_pdf(cls, filepath: str, cost_schedule: ifcopenshell.entity_instance, options: dict):
from ifc5d.ifc5Dspreadsheet import Ifc5DPdfWriter
writer = Ifc5DPdfWriter(file=tool.Ifc.get(), output=filepath, cost_schedule=cost_schedule, options=options)
writer.write()
@classmethod
def get_units(cls) -> dict[int, str]:
units = {}
+5 -2
View File
@@ -19,6 +19,7 @@
from __future__ import annotations
import os
import json
import bmesh
import bpy
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.api.material
@@ -56,8 +57,8 @@ class Debug(bonsai.core.tool.Debug):
@classmethod
def purge_hdf5_cache(cls) -> None:
props = tool.Blender.get_bim_props()
cache_dir = props.cache_dir
prefs = tool.Blender.get_addon_preferences()
cache_dir = prefs.cache_dir
filelist = [f for f in os.listdir(cache_dir) if f.endswith(".h5")]
for f in filelist:
try:
@@ -70,6 +71,7 @@ class Debug(bonsai.core.tool.Debug):
mesh = bpy.data.meshes.new("Debug")
bm.to_mesh(mesh)
obj = bpy.data.objects.new(name, mesh)
assert bpy.context.scene
bpy.context.scene.collection.objects.link(obj)
return obj
@@ -80,6 +82,7 @@ class Debug(bonsai.core.tool.Debug):
mesh = bpy.data.meshes.new("Debug")
mesh.from_pydata(verts, edges, [])
obj = bpy.data.objects.new(name, mesh)
assert bpy.context.scene
bpy.context.scene.collection.objects.link(obj)
return obj

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