Compare commits

...

245 Commits

Author SHA1 Message Date
Ryan Schultz 99c49736fa small tweak 2025-03-02 10:31:46 -06:00
Bruno Postle c1efdbb8ea black . 2025-03-02 11:37:52 +00:00
Bruno Postle e240ae2669 Fix bpy.ops.bim.select_by_material() for layersets
This operator assumes all material definitions have a Name attribute,
mostly they do except that Material Layer Sets have a LayerSetName
attribute for reasons.
2025-03-02 11:24:07 +00:00
Ryan Schultz 945099b817 Set IfcMaterialLayerSetUsage's DirectionSense/OffsetFromReferenceLine/ReferenceExtent on all selected objects 2025-03-01 18:04:41 -06:00
Bruno Perdigão f4b0fe6560 Polyline tool - Fix issue where Y and Z axis lock were inverted when in YZ plane. 2025-02-28 13:52:20 -03:00
Bruno Perdigão efc6842a9d Improve snapping for empty objects. 2025-02-28 13:44:00 -03:00
Bruno Perdigão 43931e951e Fix another issue (follows 2609a8293c) with walls created with obtuse x_angle. 2025-02-28 13:19:51 -03:00
Bruno Perdigão 134a480b03 Fix slab preview decorator when using obtuse x_angle. 2025-02-28 13:02:02 -03:00
Bruno Perdigão 0eb32ed7b7 Refactor ChangeExtrusionXAngle to support obtuse x_angle for slabs.
To support obtuse x_angle a refactoring had to be made, which helped
improve the general code for slab addition. This is challenging because
there are a few features that interact with each other to create slabs,
like `depth`, `direction_sense`, `offset` and `x_angle`. In addition,
these interactions can happen in different parts of the code. This
commit addresses `ChangeExtrusionXAngle` in `model/wall.py`.
See e8e88c25fdcef55826443fe6b000aec1baa6f25a for more information.
2025-02-28 13:02:02 -03:00
Bruno Perdigão 1855ac421c Refactor slab addition to support obtuse x_angle. See #5938
To support obtuse x_angle a refactoring had to be made, which helped
improve the general code for slab addition.
This is challenging because there are a few features that interact
with each other to create slabs, like `depth`, `direction_sense`, `offset`
and `x_angle`. In addition, these interactions can happen in different parts
of the code. This refactor improves the coherence between those different parts.
Files changed:
- `api/geometry/add_slab_representation.py`
- `model/slab.py` - inside the function `change_thickness()`
File to be changed in a following commit:
- `model/wall.py` - inside the operator `ChangeExtrusionXAngle` - To-do
2025-02-28 13:02:02 -03:00
Bruno Perdigão 7a69d6f20e Fix issue when updating wall x_angle with obtuse angle. See #5938 2025-02-28 13:02:02 -03:00
Bruno Perdigão 2609a8293c Fix issue where walls are created with obtuse x_angle. See #5938 2025-02-28 13:02:02 -03:00
Bruno Perdigão e68c47a897 Remove code related to 0104a2cc9f 2025-02-28 13:02:02 -03:00
Andrej730 06fa2b05d6 Fix #6245 after 17642ca 2025-02-28 18:31:14 +05:00
Andrej730 db52e70121 Fix possibility of adding new invalid surface styles
1) Add default values for new IfcSurfaceStyleLighting so they won't appear invalid.
2) Temporarily disable starting surface style with a texture style since it requires additional texture UI to be exposed or some default texture to be assigned to keep it valid.
2025-02-28 18:31:14 +05:00
Andrej730 c81fe97cc2 Fix bim.change_cardinal_point breaking if one of objects had no material 2025-02-28 18:31:13 +05:00
Andrej730 b9f4503206 Paramteric doors/windows - detect previously assigned materials when editing is enabled
Example - https://imgur.com/a/FEbSBaX
2025-02-28 18:31:13 +05:00
Andrej730 bd4dc501a2 Pamateric doors/windows - add material select shortcut to UI
Example - https://i.imgur.com/UfJTjGz.png
2025-02-28 18:31:13 +05:00
Andrej730 8d1255f0bc Fix toggling wireframe for odd case when there are no objects linked
Noticed investigating #6242
2025-02-28 18:31:13 +05:00
Andrej730 cc7abe6a8d Fix error applying blender offset to read-only matrix (e.g. during linking)
Example:
```
  File "\bonsai\bim\module\project\operator.py", line 1815, in execute
    self.process_occurrence(shape)
  File "\bonsai\bim\module\project\operator.py", line 1952, in process_occurrence
    obj.matrix_world = tool.Loader.apply_blender_offset_to_matrix_world(obj, mat)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\bonsai\tool\loader.py", line 952, in apply_blender_offset_to_matrix_world
    matrix[0][3] = offset_xyz[0]
    ~~~~~~~~~^^^
    ValueError: assignment destination is read-only
```

Noticed investigating #6242
2025-02-28 18:31:12 +05:00
Andrej730 edfcc3f74b ifc2sql - change default database name to match default sql_type
Co-Authored-By: bsmithuk <68438529+bsmithuk@users.noreply.github.com>
2025-02-28 18:31:12 +05:00
Andrej730 b1c87b1eb4 Set tools hotkeys name to empty string to hide it from tooltip #6238 2025-02-28 18:31:12 +05:00
Andrej730 d37bda6871 cad operators - add descriptions #6238 2025-02-28 18:31:12 +05:00
Andrej730 bb0d156bbf cad operators - use Blender units for props 2025-02-28 18:31:12 +05:00
Andrej730 b89ffcb6d0 SplitByBuildingStorey - ensure user won't pass a file by accident 2025-02-28 18:31:12 +05:00
Andrej730 323967b9fd Fix ifc2sql test after 9055f14bb9 2025-02-28 18:31:11 +05:00
Andrej730 4dd10b16e6 SplitByBuildingStorey - Bonsai to recognize filepath argument after cac3d7f 2025-02-28 18:31:11 +05:00
Andrej730 15a0d43180 fix for glob pattern in 62d53ba62d 2025-02-28 18:31:11 +05:00
Andrej730 5a1f7000ec ifcpatch - add simple test for docs generator 2025-02-28 18:31:11 +05:00
Andrej730 277aaca6c5 typing 2025-02-28 18:31:10 +05:00
Andrej730 9187f2d2b5 Bonsai patcher - small fix for cases when Blender restarts 2025-02-28 18:31:10 +05:00
Andrej730 62d53ba62d Bonsai - recognize ifc2sql database argument as filepath #6227 2025-02-27 18:35:43 +05:00
Andrej730 9055f14bb9 Ifc2Sql - hide IFC output argument as there's already database argument #6227
Also handle directory paths more gracefully.
2025-02-27 18:35:43 +05:00
Andrej730 0bd608b6cb Bonsai - ifcpatch description to include arguments descriptions
Also fix a bug with broken patch descriptions if some argument had more than one line for the description.

Example - https://i.imgur.com/1syL4gJ.png
Notcied working on #6227
2025-02-27 18:35:43 +05:00
Andrej730 497542d7a0 ifc2sql - small optimizations 2025-02-27 18:35:43 +05:00
Andrej730 26b2a7d584 typing 2025-02-27 18:35:43 +05:00
Andrej730 2d0bd9782a Reuse tool.Blender.add_layout_hotkey_operator
Now all tools are using a general method and it should be easier to maintain the consistency between the tools.

Fixes #6238.

ping @trhyder just in case
2025-02-26 17:55:15 +05:00
Andrej730 746a61a199 Add tool.Blender.KEY_MODIFIERS instead of storing it at workspaces 2025-02-26 17:55:15 +05:00
Andrej730 24df52e2f7 Do not purge orphaned IfcDocumentReferences #6117 2025-02-26 17:55:14 +05:00
Andrej730 257f0b11f5 Fix typo 2025-02-26 17:55:13 +05:00
Andrej730 b4f2039367 Fix bim.purge_unused_openings poll
It was returning true regardless whether tool.Geometry.has_openings returned True or not.
2025-02-26 17:55:13 +05:00
Dion Moult cfb7d026d1 IfcPatch recipe for Revit 2025 TINs import workaround
To understand what's going on, please read the docstring. Then please
take as much alcohol required to erase that memory.
2025-02-26 18:13:29 +11:00
Dion Moult 34acd6de35 See #1227. Implement ATPATH wall connections (going both ways). 2025-02-26 18:13:29 +11:00
Dion Moult 0f3de599a6 Don't break if no area unit defined 2025-02-26 18:13:28 +11:00
Bruno Perdigão 0104a2cc9f Remove polyline wall size limit restriction. See #6162 2025-02-25 18:06:42 -03:00
Bruno Perdigão 81f4b08b6d Fix issue where long lines where not being detected by the snapping system. 2025-02-25 18:06:42 -03:00
Bruno Perdigão 10776b7de8 Remove debug print statement 2025-02-25 18:06:42 -03:00
Bruno Perdigão b0efb648f2 Make snapping system zoom-dependent. See #6162
Refactor the snapping system to better organize the weighting and ordering
of snap distances. This change enhances the "stickiness" of snapping points,
allowing for prioritized control over different types of snapping points.
2025-02-25 18:06:42 -03:00
Andrej730 a20a8a68a6 Smal UI fix - display "Types" instead of "Typess" 2025-02-25 18:32:47 +05:00
Andrej730 f8ec8402fe Remove test for deprecator operator (c85eb79) 2025-02-25 18:28:48 +05:00
Andrej730 aaf008f007 Fix adding transom to door #6230 (d23cfcb) 2025-02-25 18:28:48 +05:00
Andrej730 f63d6b80c5 quantitifaction - not to add empty qto if nothing was quantified
It already works that way for ifcopenshell quantitification, making it work the same for Blender quantifications.

Noticed in #6220
2025-02-25 18:28:48 +05:00
Andrej730 2f6ae1745f typing 2025-02-25 18:28:48 +05:00
Andrej730 5f4098e5d4 Update type_elements_filtered on changing ifc class
Needed after 1d815cdb67
2025-02-25 18:28:47 +05:00
Andrej730 b49cc6c5e9 AuthoringData.data["relating_type_data"]
Simplify lots of data keys to just one dictionary relating_type_data, so it will be easier to maintain it and update if needed.
2025-02-25 18:28:47 +05:00
Andrej730 dcc681ac74 Revert a428169 to avoid constant reload of cached data
The code was recalculating type_elements and relating_type_id every time Blender trying to check enum items.
Couldn't replicate issue when type duplicated, perhaps it's resolved some other way already.
2025-02-25 18:28:47 +05:00
Thomas Krijnen 240c78c12a Update IfcHierarchyHelper.h 2025-02-25 14:18:10 +01:00
Andrej730 6de409031e Fix #6222
Mentioned in #6223
2025-02-25 14:21:01 +05:00
Bruno Perdigão ea274588b0 See #6214. Fix how extrusion existing angle is calculated.
The issue happen when the layer has a negative direction sense.
2025-02-24 20:46:37 -03:00
Bruno Perdigão e46e1512fa Fix #6160 where axis snapping in polyline tool were not working consistently 2025-02-24 20:46:26 -03:00
Thomas Krijnen 70182d943d Update build_osx.yml 2025-02-24 17:11:01 +01:00
Andrej730 0bd4451ce9 Fix issue not cleaning up the mesh after 76277c35d2 2025-02-24 19:31:36 +05:00
Andrej730 899ada0019 Fix purge unused types test after 3bb4243 2025-02-24 18:52:35 +05:00
Andrej730 f6ae711f6c Fix feature tests failing due to AuthoringData changes
relating_type_id now relies on type_elements which relies on ifc_class_current and they wasn't updated when ifc_class was changed.

Typical failing test looked like "TypeError: bpy_struct: item.attr = val: expected a string enum, not int".
2025-02-24 18:52:35 +05:00
Andrej730 1d815cdb67 Fix issue reloading AuthoringData from workspace at every draw call
If data was already loaded for BIMTool ("all") this ` elif ifc_element_type == "all" and AuthoringData.data["ifc_element_type"] is not None:` would always fail and this `AuthoringData.data["ifc_element_type"] != ifc_element_type` would always result to True, constantly recalculating data on every draw call.

This may uncover some issues that were hidden by the constant update (e.g. the bug fixed in the next commit).

Simplified it so now ifc_element_type is almost always referring to either ifc class or None. It's still using "all" in the BIMTool itself but then it's converted to None when passed to draw methods.
2025-02-24 18:52:35 +05:00
Andrej730 ce6339869d Fix some feature tests 2025-02-24 18:52:35 +05:00
Andrej730 5eb9cb2ee4 len(get_inverse) -> get_total_inverses 2025-02-24 18:52:35 +05:00
Andrej730 6eda4e389c typing 2025-02-24 18:52:34 +05:00
Andrej730 0783f8082e black . 2025-02-24 18:52:34 +05:00
Thomas Krijnen 3854afb34d Update build-all.py; --force when fetch tags 2025-02-24 14:01:48 +01:00
Thomas Krijnen 0b2321f478 Add template keyword 2025-02-24 13:33:53 +01:00
Thomas Krijnen 5510fb1e3c Add constexpr 2025-02-24 13:30:45 +01:00
Thomas Krijnen 7331f7361f Workaround explicit in-class specialization to if constexpr #6206 2025-02-24 13:30:24 +01:00
Andrej730 e06684e274 Comment out outdated code #6200 2025-02-24 16:46:32 +05:00
Andrej730 57d6e9b544 Fix error reassigning class for structural items 2025-02-24 15:15:54 +05:00
Andrej730 5fa0aa03e4 util.schema.is_a - use c++ implementation 2025-02-24 15:15:54 +05:00
Andrej730 1d2ac9426d Fix error during class reassignment after 43cc7a0 2025-02-24 15:15:54 +05:00
Andrej730 e79a85a4fd bim.edit_container_attributes - remove deprecated operator
Currently you can edit container name directly from spatial manager, so this operator is no longer needed (and it also was missing from UI either way).
2025-02-24 15:15:54 +05:00
Andrej730 076a6c152d fix typo 2025-02-24 15:15:54 +05:00
Andrej730 76277c35d2 Fix for 365393aa07
ahh, forgot that representation is already removed at this point
2025-02-24 15:01:00 +05:00
Thomas Krijnen a81b4a7dbb Create build_all.yml 2025-02-24 10:30:49 +01:00
Andrej730 ee8a3927b4 Fix displaying some attr types names after e8cb9e7 #6216 2025-02-24 14:20:49 +05:00
Andrej730 365393aa07 Fix #6209 after 67bac44 2025-02-24 14:01:24 +05:00
Thomas Krijnen 9289e1d0cc Try catch to keep the people happy #6176 2025-02-23 15:50:31 +01:00
Thomas Krijnen c30b5cbc08 Add std::array typemap #6137 #6191 #6196 2025-02-23 15:22:36 +01:00
Thomas Krijnen 9736345d79 Test case #6173 2025-02-23 14:46:31 +01:00
Thomas Krijnen 5599e52184 Option to retain original edges in cgal for non-boolean non-face-with-voids inputs #6173 2025-02-23 14:46:10 +01:00
Thomas Krijnen ce26af5a7e Use ProfileType in mapping #6175 2025-02-23 11:01:17 +01:00
Dion Moult 973c9e2ec9 See #1227. Implement axis updating for new wall join code. 2025-02-23 18:59:17 +11:00
Dion Moult 29a890662e See #6201. Another typing regression fix. 2025-02-23 18:30:58 +11:00
Dion Moult b42c3ba8c2 See #1227. Wall layer test script now supports atstart/atend combo test cases and materials. 2025-02-23 17:28:14 +11:00
Dion Moult a7509dbe29 See #1227. Experimental support for 3D layerset slicing. 2025-02-23 17:27:32 +11:00
Dion Moult 23158b1b7e Fix #6201. Regression in creating drawings due to typing in 67bac441ad 2025-02-23 12:02:50 +11:00
Richard Brice 8c16f394c1 Updates alignment api
Fixes mapping of vertical alignment parabola business logic to geometry. Adds creation function of h and v alignment. Adds stationing referent at start of alignment. Fixes utility functions
2025-02-22 16:16:59 -08:00
Richard Brice 12f8a1f769 Adds utility to stringize station values 2025-02-22 16:15:00 -08:00
Richard Brice 7ffd2742d6 Fixes bug mapping geometry for IfcSectionedSolidHorizontal that was introduced in 26ba761 2025-02-22 16:11:10 -08:00
Richard Brice 6429fe34ed Implements IfcHierarchyHelper.addRelatedObject for IfcRelDefinesByType 2025-02-22 16:11:06 -08:00
Jonas Frei d38f56e82d fix: bug in initialization of gltf serialiser fixed 2025-02-22 15:31:45 +01:00
Dion Moult c549dea943 See #1227. Test script for priority-aware wall layer joins. 2025-02-22 20:01:17 +11:00
Andrej730 894c05e138 Fix old typo in 686f4a0 2025-02-21 18:45:23 +05:00
Andrej730 92c2fc0b7f Fix errors regarding added PredefinedType to IfcAnnotations in IFC4X3 #6200
In IFC4X3 IfcAnnotation now has PredefinedType and checking ObjectType isn't enough.
2025-02-21 18:45:23 +05:00
Andrej730 ee2fa739bb remove_deep2 - fix traversing same element twice
(I hope I don't miss anything, no tests seem to fail and performance is increased significantly) but because `subelement_queue` initiated with the `traverse` all `element`'s subelements will be traversed inside `while` loop twice - once as a part of initial queue and another time when `element` is traversed inside the loop and all those elements added to the queue again.
Now we just initiate the `queue` with the `element` and it will be traversed inside the loop like any other element.
2025-02-21 18:45:12 +05:00
Andrej730 f7efbe5924 remove_representation - optimizations
1) `do_not_delete` performs best when it's set
2) also_consider when `element` related elements go first, so there will be no need to traverse all other elements to see if they cover `element`'s inverses.
2025-02-21 18:45:11 +05:00
Andrej730 6e20ee9fea remove_deep2 - also_considered_inverses optimization
1) replaced walk with traverse(max_levels=1)
2) early return if there total_inverses == 0
3) early return if also_considered_inverses is enough to cover total_inverses
2025-02-21 18:45:11 +05:00
Andrej730 e6d3396630 typing 2025-02-21 18:45:11 +05:00
Andrej730 cf1647a451 Fix bugs in 17642ca4e9 2025-02-21 17:31:33 +05:00
Andrej730 69535c53e0 Fix typo in 4bb32b252d #6199
ahh
2025-02-21 17:31:33 +05:00
Bruno Perdigão fed8c21625 Polyline tool - Improves the preview fix for #6083 by avoiding overlapping edges. 2025-02-20 22:52:42 -03:00
Bruno Perdigão 0f92c2d26b Fix #6161
Error - cannot access local variable 'snap_point'
2025-02-20 22:21:44 -03:00
Bruno Perdigão c11060b46a Fix #6185 issue when trying to create a slab with only two points. 2025-02-20 21:13:39 -03:00
Bruno Perdigão 9f0d408e79 See #6190. Polyline tool support for XZ and YZ planes for profiles. 2025-02-20 18:46:23 -03:00
Bruno Perdigão d37baa4e0c Fix calculation for polyline tool when working with X, Y and Z. 2025-02-20 18:22:10 -03:00
Bruno Perdigão ae4f125371 Fix auto-angle snapping in polyline tools.
When locked to a plane, the mouse will be loosely locked to and angle
that is divided by 15 degrees. This was not working properly for "XZ"
and "YZ" planes.
2025-02-20 18:22:10 -03:00
Andrej730 25d8fff5fd remove_representation - small optimizations
check schema version instead of accessing attribute directly (hasattr under the hood is just doing getattr and checking whether it returns AttributeError), so one less IFC access

Similar thing with element.Item to access IFC just once.
2025-02-20 19:12:24 +05:00
Andrej730 331f491d50 typing 2025-02-20 19:12:24 +05:00
Dion Moult b19ece18c6 Fix minor regression in linking models due to new tool call. 2025-02-20 23:21:59 +11:00
Andrej730 abddc739fe Fix UI for 4bb32b252d 2025-02-20 17:19:31 +05:00
Andrej730 174f909137 Fix #6192 2025-02-20 17:01:12 +05:00
Andrej730 a826115803 Project Library UI - allow expanding libraries hierarchy even if there are no assets yet 2025-02-20 14:33:00 +05:00
Andrej730 79abbad796 Project Library UI - fix library asset count not considering assets from sublibraries
As a result if all elements were assigned to sublibrary, it was showing that library itself has 0 assets and wasn't allowing to expand to see the sublibraries.
2025-02-20 14:33:00 +05:00
Andrej730 04a20b5c80 small fix for 4bb32b252d 2025-02-20 14:33:00 +05:00
Dion Moult f4d101788d Fix insane bug when replacing element that looped through every single element. 2025-02-20 19:18:35 +11:00
Andrej730 4bb32b252d IFC Delete - add logs to system console
In some cases deletion can take ages, so there should be at least some way for user to find out how it's going.
2025-02-19 18:31:30 +05:00
Andrej730 8b92b28dfc should_clean_mesh - add description 2025-02-19 18:31:09 +05:00
Andrej730 43cc7a0304 Replace direct access to IfcStore with tool.Ifc 2025-02-19 18:31:07 +05:00
Andrej730 67bac441ad typing 2025-02-19 18:31:06 +05:00
Andrej730 6c511f6b1b Project library UI - fix missing elements added to parent libraries
Previously if there was library A and it had sublibrary B and then some element were directly assigned to library A, UI still wouldn't show this element and would show only elements assigned to the B, the last library in the hierarchy
2025-02-19 17:53:59 +05:00
Dion Moult deb0a46acb Fix #6186. Don't hide filter UI if there are no results. 2025-02-19 17:52:34 +11:00
Dion Moult ee0599e263 Fix #6136. Allow setting of custom tmp dir, useful for packaged envs like flatpak 2025-02-19 17:44:48 +11:00
Dion Moult a8efd53d6b Fix #6182. For convenience, precalculate scale value for projected CRS map unit map conversion 2025-02-19 16:41:20 +11:00
Dion Moult 02f0c92a41 Fix bug where updating the null value on an enum property would lead to infinite recursion 2025-02-19 16:39:24 +11:00
Bruno Perdigão f11e09c7d8 See #6121. Area measurement tool now uses IFC area unit. 2025-02-18 19:37:22 -03:00
Richard Brice bb1e00e474 Fixes problems mapping vertical alignment segments business logic to geometry 2025-02-18 12:43:11 -08:00
Andrej730 4efb79e472 fix missing get_spline_points in add_representation 2025-02-18 18:50:35 +05:00
Andrej730 527e0f3dbf set_element_value - fix error for Python 3.9
types.EllipsisType was added only in 3.10
2025-02-18 18:50:35 +05:00
Andrej730 8ea3142451 Fix errors removing layers/constituents after eeac0a2 2025-02-18 18:50:35 +05:00
Andrej730 a68cb74699 ifcopenshell.alignment - fix error in Python 3.9
match statement was added only in Python 3.10
2025-02-18 18:50:35 +05:00
Andrej730 17642ca4e9 typing 2025-02-18 18:50:34 +05:00
Andrej730 83a351b1c7 black . 2025-02-18 18:50:33 +05:00
Dion Moult 37baa7a57b Fix #5845. Allow non GRAPH_VIEW reference representations. Apparently it's a thing in the docs.
If anybody can explain to me how Reference is meant to be used (the
example in the docs is really ambiguous) please do :)
2025-02-18 20:45:05 +11:00
Dion Moult 7ca2c043b5 Fix #6179. Allow searching for materials in parametric object UI. 2025-02-18 20:37:50 +11:00
Dion Moult 1e3ddce5c2 Fix #6174. Accommodate invalid index maps, and also implement an early return for single colours.
A single colour index map is a waste but some IFCs have it apparently.
2025-02-18 18:39:22 +11:00
Dion Moult c79020aeab See #5888. Debugging undos is hard, so include the operator name in the event log. 2025-02-18 16:31:36 +11:00
Dion Moult 645298e9d6 See #5888. Fix crash due to improper modal undo handling. It now considers both FINISH and CANCELLED states. 2025-02-18 16:30:27 +11:00
Dion Moult ae76d3253d See #5888. Fix bug where loading type thumbnails made undo history not synced with Blender.
If a tool.Ifc.Operator updates a prop, the prop update function will
only call _after_ the operator finishes (and therefore adds an undo step
to the undo stack). If the prop update function then calls another
tool.Ifc.Operator, that will result in another "top-level" operator
call. This second operator _won't_ get added to Blender's undo history,
yet the Bonsai history / IfcOpenShell history will have another undo
step added. Yikes!

TL;DR don't call tool.Ifc.Operator from a prop update function.
2025-02-18 16:27:53 +11:00
Bruno Perdigão 693f7c5397 Fix #6119 2025-02-17 18:03:25 -03:00
Bruno Perdigão a8960957c0 Fix #6083 2025-02-17 17:09:13 -03:00
Bruno Perdigão c609aea1de Black . 2025-02-17 16:46:58 -03:00
Bruno Perdigão 247bff4075 Polyline tool initial support for custom transformations.
Still early development. Only works for XY plane.
https://imgur.com/a/qW8gMFW
2025-02-17 16:46:58 -03:00
Bruno Perdigão 5a3a032fe6 Snap - modify intersection plane origin to be related to the view location. 2025-02-17 16:46:58 -03:00
Andrej730 5d8c2f2ec2 deprecate test-safe #5192 2025-02-17 17:58:45 +05:00
Andrej730 93ee92c401 Profiles, Materials UI - indicate in UI currently edited element 2025-02-17 17:58:45 +05:00
Andrej730 8c2d71b306 Rename library references from UI
Example - https://imgur.com/a/Ez99d1N
2025-02-17 17:58:45 +05:00
Andrej730 8f1446bfa4 Library UI - rename assets and libraries from UI
Example - https://imgur.com/a/0kcQrb2
2025-02-17 17:58:45 +05:00
Andrej730 32608ff194 py -m ifcopenshell.validate to use argparse
So command would fail if no arguments provided and there would hints on how to use it.
2025-02-17 17:58:45 +05:00
Andrej730 b0fbea60ae typing 2025-02-17 17:58:44 +05:00
Dion Moult 8f261be338 See #5839. More accurately consider the relating product definition when checking for shape aspects. 2025-02-16 17:08:55 +11:00
Dion Moult 42e184c936 Fix #5839. Appending an asset now also considers shape aspects. 2025-02-16 17:08:34 +11:00
Dion Moult e0a93626a5 Fix #6038. Standardardise passing around as_posix() paths 2025-02-16 16:17:18 +11:00
Dion Moult aaa4be1a94 Fix #5691. UI tweak to show less colour, more text for usability. 2025-02-16 15:54:28 +11:00
Dion Moult 9f453d54d8 Piles should also default to vertical, just like columns 2025-02-16 15:43:03 +11:00
Dion Moult b226a79ee8 See #5888. Fix bug where polyline tools weren't part of the undo system. 2025-02-16 11:48:51 +11:00
Dion Moult 774a770c48 Purge usage of mathutils in grid API functions
The only thing left is add_representation which is Blender specific
anyway and is slowly being refactored out.
2025-02-16 08:20:59 +11:00
Dion Moult aff90e89f7 Don't burn CPU cycles for updating parametric windows 2025-02-16 07:47:03 +11:00
Dion Moult aad4dad9a7 Migrate documentation for IfcTester and make optional deps mandatory in PyPI
I don't like this, but PyPI makes it really, really hard to "discover"
optional deps and what they're used for. So it's just easier to make it
a mandatory dependency and advanced users can always strip it out. This
stops user reports about "it doesn't work out of the box I only did pip
install ifctester"
2025-02-16 07:46:22 +11:00
Dion Moult 6ac6f13018 Fix #5824. Prioritise 3D over 2D. Only then consider subcontexts.
It's unexpected in our 3D default viewing world to have 2D first over
3D.
2025-02-15 19:03:52 +11:00
Dion Moult d23cfcb2cf Fix #2981. Standard case windows can now have different materials and implement shape aspects. 2025-02-15 18:31:25 +11:00
Dion Moult 71668fbaf1 Only switch representation once when creating a door
Previously it would switch representation (and sync changes) every
single time a new representation was created (in the
replace_obj_ifc_representation function). I think this is misplaced
responsibility. The function should change the representation, but not
be responsible for switching.
2025-02-15 18:29:13 +11:00
Dion Moult 81b7f3dc30 New API function to set shape aspect constituents 2025-02-15 18:26:13 +11:00
Dion Moult a0fa861b92 Shape builder can now created triangulated face sets 2025-02-15 16:10:12 +11:00
Dion Moult a63c8e1d70 Don't burn CPU cycles recalculating the door every draw call in parametric door interface 2025-02-15 16:09:26 +11:00
Andrej730 3e1d1bf7fa Library UI - tree view
Example of tree view with libraries hierarchy - https://imgur.com/a/zPuaoPv
Example of library assignment in non-tree view - https://imgur.com/a/9kZFblj
2025-02-14 17:51:47 +05:00
Andrej730 20e1fc2618 Fix bug in get_container not passing specified ifc_class to the parent #6155 2025-02-14 17:49:51 +05:00
Andrej730 1014487d82 black . 2025-02-14 17:49:47 +05:00
Dion Moult 3832077d0f Fix recent regression where util.shape.get_vertices returns a read-only array, so you can't edit in place (e.g. for offsets) 2025-02-14 13:06:09 +11:00
Bruno Perdigão 768bf17b05 Small fix in snap. 2025-02-13 18:26:44 -03:00
Bruno Perdigão fb4ad5290f Fix mix_snap_and_axis() to ignore intersection too close from polyline.
Follows 5fe87bf5aa
2025-02-13 17:20:10 -03:00
Bruno Perdigão f20bdd0ab6 Fix issue where detected snap vector where being changed by other function.
It was probably a reference issue. Copying the vectors solved the issue.
2025-02-13 16:49:37 -03:00
Bruno Perdigão 5fe87bf5aa Modify mix_snap_and_axis() to select the snapping point closest to the last point. 2025-02-13 14:51:40 -03:00
Bruno Perdigão c23a20df84 Small fix 2025-02-13 14:51:40 -03:00
Bruno Perdigão b87cd33b12 Polyline tool a snapping system small refactor. 2025-02-13 14:51:40 -03:00
Bruno Perdigão 8acd5321b0 Fix polyline tool angle lock when using XZ and YZ plane 2025-02-13 14:51:39 -03:00
Bruno Perdigão 1165ef3f33 More refactor of the snapping system, after 7e239ac511e991fd8ccf32add878024d921db38b 2025-02-13 14:51:39 -03:00
Bruno Perdigão 2b7108a1d8 Refactor snapping points data structure. 2025-02-13 14:51:39 -03:00
Bruno Perdigão 33d9e050ec Fix #6123 2025-02-13 14:51:39 -03:00
Andrej730 b88fd964bc bim.refresh_library - add missing undo flag
It's unsafe in Blender to leave operators that edit blend data without undo flag.
2025-02-13 18:55:44 +05:00
Andrej730 0b32c89c26 use_smooth - use correct buffer length
apparently it wasn't creating any issues but still creating larger buffer than necessary
2025-02-13 18:55:44 +05:00
Andrej730 9db1783062 more numpy arrays to match expected buffer type 2025-02-13 18:55:44 +05:00
Dion Moult 2d54a48f8e Add option for door glazing material 2025-02-13 19:18:17 +11:00
Dion Moult 39a30f2577 Redesign profile edit interface to be consistent like all other list+edit button interfaces
I kept on clicking the wrong thing
2025-02-13 19:18:02 +11:00
Dion Moult 1e946fd98e Fix three small but significant typos in previous fixes.
BTW @andrej730 that optimisation is awesome. Here's a timeit:

import bpy
import numpy as np
import timeit

obj = bpy.context.active_object
verts = obj.data.vertices

def foreach_get_method():
    coords = np.empty(len(verts) * 3, dtype=np.float32)
    obj.data.vertices.foreach_get("co", coords)
    coords = coords.reshape(-1, 3)
    coords = coords.astype("d")
    coords /= 10
    return coords

def list_comprehension_method():
    coords = [v.co / 10 for v in verts]
    return coords

# Number of times each function will be executed
num_runs = 10000

t_listcomp = timeit.timeit(list_comprehension_method, number=num_runs)
t_foreach = timeit.timeit(foreach_get_method, number=num_runs)

print("foreach method: {:.6f} seconds over {} runs".format(t_foreach,
num_runs))
print("List comprehension method: {:.6f} seconds over {}
runs".format(t_listcomp, num_runs))

# foreach method: 0.088656 seconds over 10000 runs
# List comprehension method: 0.644446 seconds over 10000 runs
2025-02-13 13:07:29 +11:00
Richard Brice 79046e2970 Compute horizontal project of vertical spiral curve 2025-02-12 16:47:51 -08:00
Dion Moult f6c6cc9a1b Fix #6131. Fix bug where you could potentially create an invalid composite profile with no profiles. 2025-02-13 07:35:58 +11:00
Dion Moult af03953ef7 Apparently you couldn't edit composite profile defs. Now you can. 2025-02-13 07:35:58 +11:00
Thomas Krijnen cdcbc2ad3b Fix 2d boolean check for periodic curves #6147 2025-02-12 16:08:32 +01:00
Andrej730 ba2456ad36 Move common code to create_mesh_from_shape 2025-02-12 18:39:27 +05:00
Andrej730 299b5bcf6b Optimize loading meshes using numpy arrays
1) avoid calling geometry.xxx multiple times as each time it creates a new copy
2) use numpy arrays with correct data type, so Blender could perform buffer copy significantly reducing overhead
2025-02-12 18:39:27 +05:00
Andrej730 cb95539cb0 reuse get_cartesian_point_offset 2025-02-12 18:39:27 +05:00
Andrej730 7b78ebc0f3 Fix possible float precision issues similar to f6087613f0 2025-02-12 18:39:27 +05:00
Andrej730 f39e6ccb54 typing 2025-02-12 18:39:27 +05:00
Dion Moult 3e1710c6fc Fix bug where duplicating a curve (which would result in a new curve) didn't go in the same collection nor was linked to the right item due to a typo 2025-02-12 23:53:54 +11:00
Thomas Krijnen fac5104bd8 Take into account edge orientation also for polygonal edges #6113 2025-02-12 10:57:30 +01:00
Thomas Krijnen 51c4059771 20.04 is closing down 2025-02-12 10:31:14 +01:00
Dion Moult f24de9d384 Fix bug where you couldn't add a meshlike object in a boolean hierarchy 2025-02-12 15:36:57 +11:00
Dion Moult 1e08a71c5a Fix bug where disabling item mode didn't also quit from the boolean editing mode
These two modes should be tied together.
2025-02-12 15:31:56 +11:00
Dion Moult 24b4be3ea1 Fix bug where item positions weren't synced before changing numeric attributes 2025-02-12 08:40:21 +11:00
Dion Moult d2420c2e45 Revert "Hotfix for PyPI < python 3.11". Release made.
This reverts commit d093d80698.
2025-02-12 08:03:36 +11:00
Dion Moult d093d80698 Hotfix for PyPI < python 3.11 2025-02-12 07:53:20 +11:00
Bruno Perdigão aef972a518 Enable snapping of overlapped objects when viewport is in x-ray mode. 2025-02-11 12:31:24 -03:00
Bruno Perdigão 06c6fa5dff Fix #6064
Slabs offset are now measured perpendicularly.
2025-02-11 10:33:29 -03:00
Andrej730 f6087613f0 add_representation - use numpy arrays for calculations that include model offset 2025-02-11 18:14:45 +05:00
Andrej730 45e36590eb Add paste buffer to outliner context menu #6139 2025-02-11 18:14:45 +05:00
Andrej730 7f37acfd38 Fix typo 2025-02-11 18:14:44 +05:00
Andrej730 de2329c352 bim.override_paste_buffer - add description 2025-02-11 18:14:44 +05:00
Andrej730 09abf0c7ab bim.override_paste_buffer - fix issue when ctrl-v might unlink currently selected ifc elements 2025-02-11 18:14:44 +05:00
Andrej730 77c2e626b3 Add info message on paste
Original Blender `view3d.pastebuffer` had an info missing and it wasn't displayed in UI since it's coming from suboperator. New info message also mentions that objects were unlinked from IFC
2025-02-11 18:14:44 +05:00
Andrej730 9ea6a1fc75 typing 2025-02-11 18:14:44 +05:00
Andrej730 5b7964f04c black . 2025-02-11 18:14:44 +05:00
Andrej730 57d7d83ebb Fix Python <3.10 support #6141 2025-02-11 16:50:07 +05:00
tim 2f4c3de92d New icons
add_from_closed_loop and add_from_perimeter
2025-02-11 21:32:03 +11:00
tim 136270b566 IFC icon in Scene dropdown
custom white mode support for IFC icon in scene drop down menu
2025-02-11 21:31:15 +11:00
tim 557a5f03d2 Icon mode for Open-Recent-IFC-Project
Added UIData class with function for storing icon color mode. Open Recent IFC Project icons now supports blender dark, blender light, and custom "white" themes
2025-02-11 21:31:15 +11:00
tim 4e1ff8ea6a Icon mode for IFC Modes
Added UIData class with function for storing icon color mode. IFC Mode dropdown now supports blender dark, blender light, and custom "white" themes
2025-02-11 21:31:15 +11:00
tim 08680d8726 Geographic Element Tool Icon
My tree was missing
2025-02-11 21:31:15 +11:00
Dion Moult 53cf1ee0a1 Fix UI bug where changing the profile name didn't result in the UI being refreshed 2025-02-11 20:03:50 +11:00
Dion Moult ddf7d1b0b9 Change default behaviour of remove_representation not to remove named profiles
I guess this is a Bonsaism but it makes sense as named profiles are
significant, and it isn't the first time materials and profiles have
been treated as "rooted" in a sense. It's also annoying to keep on
having my profile library drop off when I'm just switching profiles.
2025-02-11 20:01:19 +11:00
Dion Moult eeac0a2e30 Fix bug where removing material set items weren't consistent in how they purged materials and profiles
Previously remove_profile would purge unused materials and profiles, but
remove_layer and remove_constituent wouldn't. This was as subtle
inconsitency. Now everything by default consistently retains materials
and profiles, and has options to change this default.
2025-02-11 20:00:17 +11:00
Dion Moult 1f83b4f7a0 Fix bug where non-columns would still behave like columns if they were profiled which is unintuitive
On second thoughts only columns should go up, everything else should go
wherever you click to. Not really a bug, just an unexpected default.
2025-02-11 18:54:45 +11:00
Dion Moult ada8cc90ef Very minor docs rst syntax fix 2025-02-11 18:53:32 +11:00
Andrej730 21562d7e7d Add distinguishable name for arrays constraints 2025-02-11 12:06:40 +05:00
Andrej730 77f9dbf4f0 small fix for 4c0a7e1ca9
mentioned in #6129
2025-02-10 19:11:13 +05:00
Andrej730 875dd37579 Save Blender preferences on Blender Restart #6095 2025-02-10 17:41:28 +05:00
Andrej730 e1c49ad317 Fix error adding transom to parametric door #6129 2025-02-10 16:47:14 +05:00
Andrej730 8e9d4be55d Improve error logs for failing profiles #6131 2025-02-10 16:47:14 +05:00
Andrej730 65d65972ac Fix occurrence name function prop missing after ce8bdff 2025-02-10 16:47:14 +05:00
Andrej730 b2d66bbb89 Fix bug where selecting element / element type without representation wasn't updating bim tools active type 2025-02-10 16:47:13 +05:00
Andrej730 d8a250f153 Update Annotation Tool types when selecting elements #6134
Similar to other BIM Tools

Update handler.py
2025-02-10 16:47:13 +05:00
Andrej730 4c0a7e1ca9 typing 2025-02-10 16:47:13 +05:00
Dion Moult 11c6b55f7c Revise the modern COBie 2.4 implementation to also allow PointOfContact/WarrantyPeriod
Legacy COBie 2.4 allowed this but not sure why it dropped off.
2025-02-10 17:26:51 +11:00
Dion Moult 1d2214efd9 Fix IfcOpenShell-Python on Python 3.10
Elipsis is not allowed for generic typing
2025-02-10 17:26:03 +11:00
Dion Moult cb027c80f9 Fix dev install script - brick schema directory changed 2025-02-10 10:50:25 +11:00
Dion Moult 44c7c6d14e Bump to 0.8.2 2025-02-09 16:45:44 +11:00
Dion Moult 3e7f244100 Drop Windows 32bit support for this release as it isn't built. We can restore it in the future if needed. 2025-02-09 14:59:15 +11:00
Dion Moult da1c92c42b Bump IOS
Previous one didn't have the full matrix, so we can't release with it
2025-02-09 14:53:32 +11:00
Dion Moult 17682498da Minor UI cleanup for reorganised visibility buttons 2025-02-09 14:53:09 +11:00
455 changed files with 10178 additions and 6067 deletions
+21
View File
@@ -0,0 +1,21 @@
name: Dispatch Build IfcOpenShell
on:
workflow_dispatch:
jobs:
trigger-workflows:
runs-on: ubuntu-latest
strategy:
matrix:
workflow:
- 'Build IfcOpenShell Linux'
- 'Build IfcOpenShell Linux ARM'
- 'Build IfcOpenShell OSX'
- 'Build IfcOpenShell WASM / Pyodide'
- 'Build IfcOpenShell Windows'
steps:
- name: Trigger binary build workflows
uses: benc-uk/workflow-dispatch@v1
with:
workflow: ${{ matrix.workflow }}
+1 -3
View File
@@ -128,10 +128,8 @@ jobs:
#
# To force the link and overwrite all conflicting files:
# brew link --overwrite python@3.13
brew link --overwrite python@3.12
brew link --overwrite python@3.13
# https://github.com/rust-lang/rustup/pull/3989/files
brew install --overwrite awscli
brew install --overwrite awscli | true
- name: Upload .zip archives to S3
run: |
+1 -1
View File
@@ -5,7 +5,7 @@ on:
jobs:
build_ifcopenshell:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Checkout Repository
+1 -1
View File
@@ -5,7 +5,7 @@ on:
jobs:
build_ifcopenshell:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
container: rockylinux:8
steps:
-4
View File
@@ -24,10 +24,6 @@ jobs:
name: "Windows 64bit",
short_name: win64,
}
- {
name: "Windows 32bit",
short_name: win32,
}
- {
name: "Linux 64bit",
short_name: linux64
@@ -25,10 +25,6 @@ jobs:
name: "Windows 64bit",
short_name: win64,
}
- {
name: "Windows 32bit",
short_name: win32,
}
- {
name: "Linux 64bit",
short_name: linux64
-1
View File
@@ -48,7 +48,6 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely
pip install mathutils
pip install src/bcf --no-deps
pip install https://github.com/Andrej730/aud/archive/refs/heads/master-reduced-size.zip
+1 -1
View File
@@ -1 +1 @@
0.8.1
0.8.2
+1 -1
View File
@@ -350,7 +350,7 @@ def git_clone_or_pull_repository(clone_url, target_dir, revision=None):
run([git, "clone", "--recursive", clone_url, target_dir])
else:
logger.info(f"directory '{target_dir}' already cloned. Pulling latest changes.")
run([git, "-C", target_dir, "fetch", "--all", "--tags"])
run([git, "-C", target_dir, "fetch", "--all", "--tags", "--force"])
# detect whether we are on a branch and pull
if run([git, "rev-parse", "--abbrev-ref", "HEAD"], cwd=target_dir) != "HEAD":
+1 -1
View File
@@ -85,7 +85,7 @@ BLENDER_PLATFORM:=windows-x64
endif
# Current build commit hash.
OLD:=463289e
OLD:=c49ca69
.PHONY: bump
bump:
ifndef NEW
+1 -3
View File
@@ -115,10 +115,8 @@ classes = [
operator.ReloadIfcFile,
operator.RemoveIfcFile,
operator.RevertClippingPlaneCut,
operator.SelectDataDir,
operator.SelectCacheDir,
operator.SelectDir,
operator.SelectIfcFile,
operator.SelectSchemaDir,
operator.SelectURIAttribute,
operator.SetTab,
operator.SwitchTab,
Binary file not shown.

After

Width:  |  Height:  |  Size: 420 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

+3 -2
View File
@@ -132,11 +132,12 @@ class IfcExporter:
bpy.ops.bim.update_representation(obj=obj.name)
def has_changed_materials(self, obj: bpy.types.Object) -> bool:
checksum = obj.data.BIMMeshProperties.material_checksum
mprops = tool.Geometry.get_mesh_props(obj.data)
checksum = mprops.material_checksum
return checksum != tool.Geometry.get_material_checksum(obj)
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
element = self.file.by_id(tool.Blender.get_object_bim_props(obj).ifc_definition_id)
if tool.Geometry.is_scaled(obj):
bpy.ops.bim.update_representation(obj=obj.name)
# update_representation might not apply scale if the object has openings
+55 -36
View File
@@ -29,7 +29,6 @@ from bpy.app.handlers import persistent
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.owner.prop import get_user_person, get_user_organisation
from bonsai.bim.module.model.data import AuthoringData
from bonsai.bim.module.model.workspace import LIST_OF_TOOLS, TOOLS_TO_CLASSES_MAP
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
@@ -61,18 +60,19 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
if props.is_renaming:
props.is_renmaing = False
return
IfcStore.get_file().by_id(ifc_definition_id).Name = obj.name
tool.Ifc.get().by_id(ifc_definition_id).Name = obj.name
refresh_ui_data()
return
if not obj.BIMObjectProperties.ifc_definition_id:
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
return
if obj.BIMObjectProperties.is_renaming:
obj.BIMObjectProperties.is_renaming = False
if props.is_renaming:
props.is_renaming = False
return
element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
element = tool.Ifc.get().by_id(props.ifc_definition_id)
if "/" in obj.name:
object_name = obj.name
element_name = obj.name.split("/", 1)[1]
@@ -88,8 +88,8 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
if not element.is_a("IfcRoot"):
return
element.Name = element_name
if obj.BIMObjectProperties.collection:
obj.BIMObjectProperties.collection.name = object_name
if props.collection:
props.collection.name = object_name
refresh_ui_data()
@@ -108,29 +108,44 @@ def update_bim_tool_props():
return
mode = bpy.context.mode
current_tool = bpy.context.workspace.tools.from_space_view3d_mode(mode)
if not current_tool or current_tool.idname not in LIST_OF_TOOLS:
if not current_tool or current_tool.idname not in tool.Blender.get_list_of_tools():
return
element = tool.Ifc.get_entity(obj)
if not element:
return
props = tool.Model.get_model_props()
aprops = tool.Drawing.get_annotation_props()
TOOLS_TO_CLASSES_MAP = tool.Blender.get_tools_to_classes_map()
is_annotation_tool = current_tool.idname == "bim.annotation_tool"
if element.is_a("IfcTypeProduct") or element.is_a("IfcProduct"):
element_type = ifcopenshell.util.element.get_type(element)
if element_type:
is_bim_tool = current_tool.idname == "bim.bim_tool"
if is_annotation_tool and (object_type := tool.Drawing.get_annotation_type_object_type(element_type)):
aprops.object_type = object_type
aprops.relating_type_id = str(element_type.id())
return
if is_bim_tool:
props.ifc_class = element_type.is_a()
if is_bim_tool or TOOLS_TO_CLASSES_MAP.get(current_tool.idname) == element_type.is_a():
props.relating_type_id = str(element_type.id())
if is_annotation_tool:
return
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
return
props = bpy.context.scene.BIMModelProperties
if element.is_a("IfcElementType") or element.is_a("IfcElement"):
element_type = ifcopenshell.util.element.get_type(element)
if element_type:
is_bim_tool = current_tool.idname == "bim.bim_tool"
if is_bim_tool:
props.ifc_class = element_type.is_a()
if is_bim_tool or TOOLS_TO_CLASSES_MAP.get(current_tool.idname) == element_type.is_a():
props.relating_type_id = str(element_type.id())
extrusion = tool.Model.get_extrusion(representation)
if not extrusion:
return
def get_x_angle(extrusion):
def get_x_angle(extrusion: ifcopenshell.entity_instance) -> float:
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
return x_angle
@@ -207,35 +222,38 @@ def refresh_ui_data():
if isinstance(tool.Ifc.get(), ifcopenshell.sqlite):
tool.Ifc.get().clear_cache()
bpy.context.scene.DocProperties.should_draw_decorations = bpy.context.scene.DocProperties.should_draw_decorations
props = tool.Drawing.get_document_props()
props.should_draw_decorations = props.should_draw_decorations
if bpy.context.scene.WebProperties.is_connected:
tool.Web.send_webui_data()
@persistent
def loadIfcStore(scene):
def loadIfcStore(scene: bpy.types.Scene) -> None:
IfcStore.purge()
refresh_ui_data()
if not IfcStore.get_file():
if not tool.Ifc.get():
return
IfcStore.get_schema()
tool.Ifc.schema()
IfcStore.relink_all_objects()
@persistent
def undo_post(scene):
if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction:
IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction
IfcStore.undo(until_key=bpy.context.scene.BIMProperties.last_transaction)
def undo_post(scene: bpy.types.Scene) -> None:
props = tool.Blender.get_bim_props()
if IfcStore.last_transaction != props.last_transaction:
IfcStore.last_transaction = props.last_transaction
IfcStore.undo(until_key=props.last_transaction)
refresh_ui_data()
tool.Ifc.rebuild_element_maps()
@persistent
def redo_post(scene):
if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction:
IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction
IfcStore.redo(until_key=bpy.context.scene.BIMProperties.last_transaction)
def redo_post(scene: bpy.types.Scene) -> None:
props = tool.Blender.get_bim_props()
if IfcStore.last_transaction != props.last_transaction:
IfcStore.last_transaction = props.last_transaction
IfcStore.redo(until_key=props.last_transaction)
refresh_ui_data()
tool.Ifc.rebuild_element_maps()
@@ -268,10 +286,10 @@ def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None
return pao
def viewport_shading_changed_callback(area):
def viewport_shading_changed_callback(area: bpy.types.Area) -> None:
shading = area.spaces.active.shading.type
if shading == "RENDERED":
bpy.context.scene.BIMStylesProperties.active_style_type = "External"
tool.Style.get_style_props().active_style_type = "Internal"
def subscribe_to_viewport_shading_changes():
@@ -326,13 +344,14 @@ def load_post(scene):
tool.Blender.setup_tabs()
if tool.Ifc.get() and bpy.data.is_saved:
bpy.context.scene.BIMProperties.has_blend_warning = True
props = tool.Blender.get_bim_props()
props.has_blend_warning = True
# Bonsai overlays
georeference_props = bpy.context.scene.BIMGeoreferenceProperties
georeference_props = tool.Georeference.get_georeference_props()
aggregate_props = bpy.context.scene.BIMAggregateProperties
nest_props = bpy.context.scene.BIMNestProperties
model_props = bpy.context.scene.BIMModelProperties
model_props = tool.Model.get_model_props()
if georeference_props.should_visualise:
GeoreferenceDecorator.install(bpy.context)
if aggregate_props.aggregate_decorator:
+15 -5
View File
@@ -27,12 +27,12 @@ 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 bonsai.bim.ifc import IfcStore
from typing import Optional, Callable, Any, Union, Iterable, TYPE_CHECKING
if TYPE_CHECKING:
import bonsai.bim.prop
from bonsai.bim.prop import Attribute
from bonsai.bim.module.search.prop import BIMFilterGroup
# ImportCallback return values:
# - None - property should be imported by default workflow
@@ -119,7 +119,8 @@ def import_attributes(
data: dict[str, Any],
callback: Optional[ImportCallback] = None,
) -> None:
for attribute in IfcStore.get_schema().declaration_by_name(ifc_class).all_attributes():
schema = tool.Ifc.schema()
for attribute in schema.declaration_by_name(ifc_class).all_attributes():
import_attribute(attribute, props, data, callback=callback)
@@ -292,7 +293,10 @@ def prop_with_search(
should_click_ok: bool = False,
original_operator_path: Optional[str] = None,
**kwargs: Any,
):
) -> bpy.types.UILayout:
"""
:return: Added row.
"""
# kwargs are layout.prop arguments (text, icon, etc.)
row = layout.row(align=True)
row.prop(data, prop_name, **kwargs)
@@ -306,6 +310,7 @@ def prop_with_search(
op.original_operator_path = original_operator_path or ""
except TypeError: # Prop is not iterable
pass
return row
def get_enum_items(
@@ -372,11 +377,16 @@ def convert_property_group_from_si(property_group: bpy.types.PropertyGroup, skip
setattr(property_group, prop_name, prop_value)
def draw_filter(layout: bpy.types.UILayout, filter_groups, data, module: str) -> None:
def draw_filter(
layout: bpy.types.UILayout,
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup],
data,
module: str,
) -> None:
if not data.is_loaded:
data.load()
sprops = bpy.context.scene.BIMSearchProperties
sprops = tool.Search.get_search_props()
if tool.Ifc.get():
row = layout.row(align=True)
+50 -20
View File
@@ -105,7 +105,8 @@ class IfcStore:
@staticmethod
def get_file():
if IfcStore.file is None:
IfcStore.path = cast(str, bpy.context.scene.BIMProperties.ifc_file)
props = tool.Blender.get_bim_props()
IfcStore.path = props.ifc_file
# 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))
@@ -119,10 +120,11 @@ class IfcStore:
@staticmethod
def get_cache():
if IfcStore.cache is None and IfcStore.path:
props = tool.Blender.get_bim_props()
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(bpy.context.scene.BIMProperties.cache_dir, exist_ok=True)
IfcStore.cache_path = os.path.join(bpy.context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5")
os.makedirs(props.cache_dir, exist_ok=True)
IfcStore.cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5")
cache_path = Path(IfcStore.cache_path)
cache_settings = ifcopenshell.geom.settings()
serializer_settings = ifcopenshell.geom.serializer_settings()
@@ -162,7 +164,8 @@ class IfcStore:
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()
new_cache_path = os.path.join(bpy.context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5")
props = tool.Blender.get_bim_props()
new_cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5")
IfcStore.cache = None
try:
shutil.move(IfcStore.cache_path, new_cache_path)
@@ -178,6 +181,7 @@ class IfcStore:
if not os.path.isfile(path):
return
extension = path.split(".")[-1]
props = tool.Project.get_project_props()
if extension.lower() == "ifczip":
with tempfile.TemporaryDirectory() as unzipped_path:
with zipfile.ZipFile(path, "r") as zip_ref:
@@ -187,7 +191,7 @@ class IfcStore:
return
elif extension.lower() == "ifcxml":
IfcStore.file = ifcopenshell.file(ifcopenshell.ifcopenshell_wrapper.parse_ifcxml(path))
elif bpy.context.scene.BIMProjectProperties.should_stream:
elif props.should_stream:
IfcStore.file = ifcopenshell.open(path, should_stream=True)
else:
IfcStore.file = ifcopenshell.open(path)
@@ -195,9 +199,8 @@ class IfcStore:
@staticmethod
def get_schema() -> ifcopenshell.ifcopenshell_wrapper.schema_definition:
if IfcStore.file is None:
IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(
bpy.context.scene.BIMProjectProperties.export_schema
)
props = tool.Project.get_project_props()
IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(props.export_schema)
elif IfcStore.schema is None:
IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(IfcStore.file.schema_identifier)
return IfcStore.schema
@@ -247,7 +250,7 @@ class IfcStore:
# refactor this class and deprecate usage of IfcStore in favour of
# tools.
if not isinstance(obj, (bpy.types.Object, bpy.types.Material)):
obj.BIMMeshProperties.ifc_definition_id = element.id()
tool.Geometry.get_mesh_props(obj).ifc_definition_id = element.id()
return
existing_obj = IfcStore.id_map.get(element.id(), None)
@@ -264,7 +267,8 @@ class IfcStore:
if element.is_a("IfcSurfaceStyle"):
obj.BIMStyleProperties.ifc_definition_id = element.id()
else:
obj.BIMObjectProperties.ifc_definition_id = element.id()
props = tool.Blender.get_object_bim_props(obj)
props.ifc_definition_id = element.id()
tool.Ifc.setup_listeners(obj)
@@ -404,13 +408,21 @@ class IfcStore:
if isinstance(obj, bpy.types.Material):
obj.BIMStyleProperties.ifc_definition_id = 0
else: # bpy.types.Object
obj.BIMObjectProperties.ifc_definition_id = 0
props = tool.Blender.get_object_bim_props(obj)
props.ifc_definition_id = 0
@staticmethod
def execute_ifc_operator(operator: tool.Ifc.Operator, context: bpy.types.Context, is_invoke=False) -> set[str]:
def execute_ifc_operator(
operator: tool.Ifc.Operator,
context: bpy.types.Context,
event=None,
method: Literal["EXECUTE", "INVOKE", "MODAL"] = "EXECUTE",
) -> set[str]:
bonsai.last_actions.append({"type": "operator", "name": operator.bl_idname})
bpy.context.scene.BIMProperties.is_dirty = True
is_top_level_operator = not bool(IfcStore.current_transaction)
props = tool.Blender.get_bim_props()
props.is_dirty = True
# Modals don't nest, and Blender handles the loop that continuously calls modal()
is_top_level_operator = not bool(IfcStore.current_transaction) or (method == "MODAL")
if is_top_level_operator:
IfcStore.begin_transaction(operator)
@@ -436,10 +448,12 @@ class IfcStore:
bonsai.bim.handler.refresh_ui_data()
try:
if is_invoke:
result = getattr(operator, "_invoke")(context, None)
else:
if method == "EXECUTE":
result = getattr(operator, "_execute")(context)
elif method == "INVOKE":
result = getattr(operator, "_invoke")(context, event)
elif method == "MODAL":
result = getattr(operator, "_modal")(context, event)
except:
bonsai.last_error = traceback.format_exc()
# Try to ensure undo will work since Blender undo does work in case of errors.
@@ -451,12 +465,27 @@ class IfcStore:
end_top_level_operator()
raise
end_top_level_operator()
if method == "MODAL":
if result == {"FINISHED"}:
end_top_level_operator()
elif result == {"CANCELLED"}:
# Please read the docs: https://docs.blender.org/api/current/bpy.types.Operator.html
# > "when an operator returns {'CANCELLED'}, no undo step will be created".
# This means that if your modal edits IFC data, then the user
# cancels it, Blender's undo history will not be in sync with
# Bonsai / IfcOpenShell's undo history. Instead of hoping for
# Bonsai devs to remember to handle the "cancel" state (i.e.
# detect escape keypress) and return {"FINISHED"}, we instead
# always enforce an undo step.
bpy.ops.ed.undo_push(message=f"Cancel {operator.bl_idname}")
end_top_level_operator()
else:
end_top_level_operator()
return result
@staticmethod
def begin_transaction(operator: tool.Ifc.Operator) -> None:
IfcStore.current_transaction = str(uuid.uuid4())
IfcStore.current_transaction = str(uuid.uuid4()) + operator.__class__.__name__
operator.transaction_key = IfcStore.current_transaction
@staticmethod
@@ -470,7 +499,8 @@ class IfcStore:
) -> None:
key = getattr(operator, "transaction_key", None)
data = getattr(operator, "transaction_data", None)
bpy.context.scene.BIMProperties.last_transaction = key
props = tool.Blender.get_bim_props()
props.last_transaction = key
IfcStore.last_transaction = key
rollback = rollback or getattr(operator, "rollback", lambda data: True)
commit = commit or getattr(operator, "commit", lambda data: True)
+48 -53
View File
@@ -24,6 +24,7 @@ import logging
import traceback
import mathutils
import numpy as np
import numpy.typing as npt
import multiprocessing
import ifcopenshell
import ifcopenshell.geom
@@ -36,7 +37,8 @@ import ifcopenshell.util.shape
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore, IFC_CONNECTED_TYPE
from bonsai.tool.loader import OBJECT_DATA_TYPE
from typing import Dict, Union, Optional, Any
from typing import Dict, Union, Optional, Any, Literal
from ifcopenshell.util.shape import MatrixType
class MaterialCreator:
@@ -68,6 +70,9 @@ class MaterialCreator:
if isinstance(mesh, bpy.types.Curve):
return
if mesh.get("has_layer_styles", None) == True:
return
self.mesh = mesh
self.obj = obj
if element.is_a("IfcTypeProduct"):
@@ -86,10 +91,11 @@ class MaterialCreator:
def parse_element_type_material_styles(self, element: ifcopenshell.entity_instance) -> None:
if self.mesh["ios_materials"]:
return # Already has materials assign to the representation itself
return # Already has materials assign to the representation itself
# Otherwise, we need to check for material styles on the element, since
# create_shape on types only works on representations.
context = tool.Ifc.get().by_id(self.mesh.BIMMeshProperties.ifc_definition_id).ContextOfItems
mprops = tool.Geometry.get_mesh_props(self.mesh)
context = tool.Ifc.get().by_id(mprops.ifc_definition_id).ContextOfItems
for material in ifcopenshell.util.element.get_materials(element):
if style := ifcopenshell.util.representation.get_material_style(material, context):
self.mesh["ios_materials"] = (style.id(),)
@@ -167,7 +173,7 @@ class MaterialCreator:
if -1 in self.mesh["ios_material_ids"]:
material_to_slot[-1] = get_empty_slot_index()
material_index = [material_to_slot[mat_id] for mat_id in self.mesh["ios_material_ids"]]
material_index = np.array([material_to_slot[mat_id] for mat_id in self.mesh["ios_material_ids"]], dtype="I")
self.mesh.polygons.foreach_set("material_index", material_index)
def resolve_all_stylable_representation_items(
@@ -217,7 +223,7 @@ class IfcImporter:
self.progress = 0
self.material_creator = MaterialCreator(ifc_import_settings, self)
classes_to_wireframe_str = bpy.context.scene.DocProperties.classes_to_wireframe
classes_to_wireframe_str = tool.Drawing.get_document_props().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:
@@ -433,7 +439,7 @@ class IfcImporter:
return False
def calculate_model_offset(self) -> None:
props = bpy.context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if self.ifc_import_settings.false_origin_mode == "MANUAL":
tool.Loader.set_manual_blender_offset(self.file)
elif self.ifc_import_settings.false_origin_mode == "AUTOMATIC":
@@ -474,14 +480,16 @@ class IfcImporter:
if grid.WAxes:
self.create_grid_axes(grid.WAxes, grid_obj, grid_placement)
def create_grid_axes(self, axes, grid_obj, grid_placement):
def create_grid_axes(
self, axes: list[ifcopenshell.entity_instance], grid_obj: bpy.types.Object, grid_placement: MatrixType
) -> None:
for axis in axes:
shape = tool.Loader.create_generic_shape(axis.AxisCurve)
mesh = self.create_mesh(axis, shape)
obj = bpy.data.objects.new(tool.Loader.get_name(axis), mesh)
obj.show_in_front = True
self.link_element(axis, obj)
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, grid_placement.copy()))
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, grid_placement))
def create_element_types(self):
for element_type in self.element_types:
@@ -588,6 +596,7 @@ class IfcImporter:
obj.hide_select = True
def create_generic_sqlite_elements(self, elements: set[ifcopenshell.entity_instance]) -> None:
assert isinstance(self.file, ifcopenshell.sql.sqlite)
self.geometry_cache = self.file.get_geometry([e.id() for e in elements])
for geometry_id, geometry in self.geometry_cache["geometry"].items():
mesh_name = tool.Loader.get_mesh_name_from_shape(type("Geometry", (), {"id": geometry_id}))
@@ -597,21 +606,9 @@ class IfcImporter:
mesh["has_cartesian_point_offset"] = False
if geometry["faces"]:
num_vertices = len(verts) // 3
total_faces = len(geometry["faces"])
loop_start = range(0, total_faces, 3)
num_loops = total_faces // 3
loop_total = [3] * num_loops
num_vertex_indices = len(geometry["faces"])
mesh.vertices.add(num_vertices)
mesh.vertices.foreach_set("co", verts)
mesh.loops.add(num_vertex_indices)
mesh.loops.foreach_set("vertex_index", geometry["faces"])
mesh.polygons.add(num_loops)
mesh.polygons.foreach_set("loop_start", loop_start)
mesh.polygons.foreach_set("loop_total", loop_total)
mesh.update()
mesh = tool.Loader.create_mesh_from_shape(
mesh=mesh, faces=geometry["faces"].reshape(-1, 3), verts=verts.reshape(-1, 3)
)
else:
e = geometry["edges"]
v = verts
@@ -815,7 +812,7 @@ class IfcImporter:
if shape:
# We use numpy here because Blender mathutils.Matrix is not accurate enough
mat = np.array(shape.transformation.matrix).reshape((4, 4), order="F")
mat = ifcopenshell.util.shape.get_shape_matrix(shape)
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, mat))
assert mesh # Type checker.
if not materials_updated:
@@ -888,17 +885,19 @@ class IfcImporter:
def load_file(self):
self.ifc_import_settings.logger.info("loading file %s", self.ifc_import_settings.input_file)
if not bpy.context.scene.BIMProperties.ifc_file:
bpy.context.scene.BIMProperties.ifc_file = self.ifc_import_settings.input_file
self.file = IfcStore.get_file()
props = tool.Blender.get_bim_props()
if not props.ifc_file:
props.ifc_file = self.ifc_import_settings.input_file
self.file = tool.Ifc.get()
def calculate_unit_scale(self):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
tool.Loader.set_unit_scale(self.unit_scale)
def set_units(self):
def set_units(self) -> None:
if not (assignment := self.file.by_type("IfcProject")[0].UnitsInContext):
return # Geometry is optional in IFC
props = tool.Blender.get_bim_props()
for unit in assignment.Units:
if unit.is_a("IfcNamedUnit") and unit.UnitType == "LENGTHUNIT":
if unit.is_a("IfcSIUnit"):
@@ -918,26 +917,27 @@ class IfcImporter:
elif unit.is_a("IfcNamedUnit") and unit.UnitType == "AREAUNIT":
name = unit.Name if unit.is_a("IfcSIUnit") else unit.Name.lower()
try:
bpy.context.scene.BIMProperties.area_unit = "{}{}".format(
props.area_unit = "{}{}".format(
unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name
)
except: # Probably an invalid unit.
bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE"
props.area_unit = "SQUARE_METRE"
elif unit.is_a("IfcNamedUnit") and unit.UnitType == "VOLUMEUNIT":
name = unit.Name if unit.is_a("IfcSIUnit") else unit.Name.lower()
try:
bpy.context.scene.BIMProperties.volume_unit = "{}{}".format(
props.volume_unit = "{}{}".format(
unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name
)
except: # Probably an invalid unit.
bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE"
props.volume_unit = "CUBIC_METRE"
def create_project(self):
project = self.file.by_type("IfcProject")[0]
self.project = {"ifc": project}
obj = tool.Ifc.get_object(project)
if obj:
self.project["blender"] = obj.BIMObjectProperties.collection
props = tool.Blender.get_object_bim_props(obj)
self.project["blender"] = props.collection
self.has_existing_project = True
return
self.project["blender"] = bpy.data.collections.new(
@@ -947,7 +947,8 @@ class IfcImporter:
obj.hide_select = True
self.project["blender"].objects.link(obj)
self.project["blender"].BIMCollectionProperties.obj = obj
obj.BIMObjectProperties.collection = self.collections[project.GlobalId] = self.project["blender"]
props = tool.Blender.get_object_bim_props(obj)
props.collection = self.collections[project.GlobalId] = self.project["blender"]
def create_styles(self) -> None:
for style in self.file.by_type("IfcSurfaceStyle"):
@@ -974,10 +975,10 @@ class IfcImporter:
tool.Collector.assign(obj, should_clean_users_collection=False)
def is_curve_annotation(self, element: ifcopenshell.entity_instance) -> bool:
object_type = element.ObjectType
object_type = ifcopenshell.util.element.get_predefined_type(element)
return (
object_type in tool.Drawing.ANNOTATION_TYPES_DATA
and tool.Drawing.ANNOTATION_TYPES_DATA[object_type][3] == "curve"
and tool.Drawing.ANNOTATION_TYPES_DATA[object_type].data_type == "curve"
)
def get_drawing_group(self, element):
@@ -1030,7 +1031,7 @@ class IfcImporter:
self,
element: ifcopenshell.entity_instance,
shape: Union[ifcopenshell.geom.ShapeElementType, ifcopenshell.geom.ShapeType],
cartesian_point_offset=None,
cartesian_point_offset: Union[npt.NDArray[np.float64], Literal[False]] = None,
) -> bpy.types.Mesh:
try:
if hasattr(shape, "geometry"):
@@ -1046,40 +1047,33 @@ class IfcImporter:
old_mesh.name = mesh_name + ".old"
mesh = bpy.data.meshes.new(mesh_name)
verts = ifcopenshell.util.shape.get_vertices(geometry)
if cartesian_point_offset is False:
verts = geometry.verts
mesh["has_cartesian_point_offset"] = False
elif cartesian_point_offset is not None:
verts_array = np.array(geometry.verts)
offset = np.array([-cartesian_point_offset[0], -cartesian_point_offset[1], -cartesian_point_offset[2]])
offset_verts = verts_array + np.tile(offset, len(verts_array) // 3)
verts = offset_verts.tolist()
verts = verts - cartesian_point_offset
mesh["has_cartesian_point_offset"] = True
mesh["cartesian_point_offset"] = (
f"{cartesian_point_offset[0]},{cartesian_point_offset[1]},{cartesian_point_offset[2]}"
)
elif geometry.verts and tool.Loader.is_point_far_away(
(geometry.verts[0], geometry.verts[1], geometry.verts[2]), is_meters=True
):
elif verts.size and tool.Loader.is_point_far_away(verts[0], is_meters=True):
# Shift geometry close to the origin based off that first vert it found
verts_array = np.array(geometry.verts)
offset = np.array([-geometry.verts[0], -geometry.verts[1], -geometry.verts[2]])
offset_verts = verts_array + np.tile(offset, len(verts_array) // 3)
verts = offset_verts.tolist()
offset = verts[0]
verts = verts - offset
mesh["has_cartesian_point_offset"] = True
mesh["cartesian_point_offset"] = f"{geometry.verts[0]},{geometry.verts[1]},{geometry.verts[2]}"
mesh["cartesian_point_offset"] = f"{offset[0]},{offset[1]},{offset[2]}"
else:
verts = geometry.verts
mesh["has_cartesian_point_offset"] = False
return tool.Loader.convert_geometry_to_mesh(
mesh = tool.Loader.convert_geometry_to_mesh(
geometry,
mesh,
verts=verts,
load_indexed_maps=self.ifc_import_settings.load_indexed_maps,
)
return tool.Loader.slice_layerset_mesh(element, mesh)
except:
self.ifc_import_settings.logger.error("Could not create mesh for %s", element)
import traceback
@@ -1087,9 +1081,10 @@ class IfcImporter:
print(traceback.format_exc())
def set_default_context(self):
rprops = tool.Root.get_root_props()
for subcontext in self.file.by_type("IfcGeometricRepresentationSubContext"):
if subcontext.ContextIdentifier == "Body":
bpy.context.scene.BIMRootProperties.contexts = str(subcontext.id())
rprops.contexts = str(subcontext.id())
break
def link_element(self, element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None:
@@ -23,7 +23,6 @@ import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.core.aggregate as core
import bonsai.core.spatial
from bonsai.bim.ifc import IfcStore
class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
+9 -9
View File
@@ -34,12 +34,12 @@ class BIM_PT_aggregate(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
props = context.active_object.BIMObjectProperties
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
return False
if not IfcStore.get_element(props.ifc_definition_id):
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
return False
if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
return False
@@ -66,9 +66,9 @@ class BIM_PT_aggregate(Panel):
col.enabled = False
op = col.operator("bim.aggregate_assign_object", icon="CHECKMARK")
if props.relating_object:
op.relating_object = props.relating_object.BIMObjectProperties.ifc_definition_id
op.relating_object = tool.Blender.get_object_bim_props(props.relating_object).ifc_definition_id
elif props.related_object:
op.related_object = props.related_object.BIMObjectProperties.ifc_definition_id
op.related_object = tool.Blender.get_object_bim_props(props.related_object).ifc_definition_id
row.operator("bim.disable_editing_aggregate", icon="CANCEL", text="")
return
else:
@@ -115,14 +115,14 @@ class BIM_PT_linked_aggregate(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
props = context.active_object.BIMObjectProperties
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
return False
if not IfcStore.get_element(props.ifc_definition_id):
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
return False
if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
return False
return True
@@ -26,7 +26,6 @@ import bonsai.bim.helper
import bonsai.tool as tool
import bonsai.core.attribute as core
import bonsai.core.spatial
from bonsai.bim.ifc import IfcStore
def get_objs_for_operation(operator_properties, context):
@@ -117,7 +116,7 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
obj = tool.Blender.get_active_object(is_selected=False)
if not (element := tool.Ifc.get_entity(obj)):
return
@@ -17,7 +17,6 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
from bonsai.bim.ifc import IfcStore
from bonsai.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup
from bpy.props import (
@@ -18,7 +18,6 @@
import bonsai.bim.helper
from bpy.types import Panel
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.attribute.data import AttributesData
import bonsai.tool as tool
@@ -113,7 +113,7 @@ class AuginCreateNewModel(bpy.types.Operator):
context.scene.collection.objects.link(cam_obj)
context.scene.camera = cam_obj
tmpdir = tempfile.mkdtemp()
tmpdir = tempfile.mkdtemp(dir=tool.Blender.get_addon_preferences().tmp_dir or None)
thumb_path = os.path.join(tmpdir, "thumb.png")
context.scene.render.image_settings.file_format = "PNG"
context.scene.render.filepath = thumb_path
@@ -122,15 +122,16 @@ class AuginCreateNewModel(bpy.types.Operator):
context.scene.render.image_settings.file_format = old_file_format
context.scene.render.filepath = old_filepath
client.upload_file(context.scene.BIMProperties.ifc_file, result["s3_bucket"], result["model_path"])
bim_props = tool.Blender.get_bim_props()
client.upload_file(bim_props.ifc_file, result["s3_bucket"], result["model_path"])
client.upload_file(thumb_path, result["s3_bucket"], result["thumb_path"])
# Notify done
url = "https://server.auge.pro.br/API/v3/augin_rest.php/files_uploaded"
payload = {
"user_token": props.token,
"ifc_filesize": os.path.getsize(context.scene.BIMProperties.ifc_file),
"model_filesize": os.path.getsize(context.scene.BIMProperties.ifc_file),
"ifc_filesize": os.path.getsize(bim_props.ifc_file),
"model_filesize": os.path.getsize(bim_props.ifc_file),
"thumb_filesize": os.path.getsize(thumb_path),
"model_upload_path": result["model_path"],
"thumb_upload_path": result["thumb_path"],
+3 -1
View File
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy.types
import bonsai.tool as tool
class BIM_PT_augin(bpy.types.Panel):
@@ -47,7 +48,8 @@ class BIM_PT_augin(bpy.types.Panel):
row = layout.row()
row.label(text="Logged in as " + props.username)
if not context.scene.BIMProperties.ifc_file:
bim_props = tool.Blender.get_bim_props()
if not bim_props.ifc_file:
row = layout.row()
row.label(text="No IFC Found")
return
+4 -2
View File
@@ -21,6 +21,7 @@ import bpy
import bcf
import bcf.bcfxml
import bcf.v2.bcfxml
import bonsai.tool as tool
from typing import Union
@@ -30,7 +31,8 @@ class BcfStore:
@classmethod
def get_bcfxml(cls) -> Union[bcf.bcfxml.BcfXml, None]:
if not cls.bcfxml:
bcf_filepath = bpy.context.scene.BCFProperties.bcf_file
props = tool.Bcf.get_bcf_props()
bcf_filepath = props.bcf_file
if not os.path.isabs(bcf_filepath):
bcf_filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), bcf_filepath))
if bcf_filepath:
@@ -46,7 +48,7 @@ class BcfStore:
@classmethod
def set(cls, bcfxml: Union[bcf.bcfxml.BcfXml, None], filepath: str) -> None:
cls.bcfxml = bcfxml
props = bpy.context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
props.bcf_file = filepath
# Set bcf_version prop on load.
+75 -57
View File
@@ -43,7 +43,6 @@ import bonsai.tool as tool
import bonsai.bim.module.bcf.prop as bcf_prop
import bonsai.bim.module.bcf.bcfstore as bcfstore
from pathlib import Path
from bonsai.bim.ifc import IfcStore
from math import radians, degrees, atan, tan, cos, sin
from mathutils import Vector, Matrix, Euler, geometry
from xsdata.models.datatype import XmlDateTime
@@ -55,7 +54,7 @@ class NewBcfProject(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
bcf_v2 = props.bcf_version == "2"
bcf_class = bcf.v2.bcfxml.BcfXml if bcf_v2 else bcf.v3.bcfxml.BcfXml
bcfxml = bcf_class.create_new("New Project")
@@ -103,7 +102,8 @@ class LoadBcfProject(bpy.types.Operator):
assert bcfxml.project
if bcfxml.project.name is None:
bcfxml.project.name = nameless
context.scene.BCFProperties.name = bcfxml.project.name
props = tool.Bcf.get_bcf_props()
props.name = bcfxml.project.name
bpy.ops.bim.load_bcf_topics()
self.report({"INFO"}, f"BCF Project '{Path(self.filepath).name}' is loaded.")
return {"FINISHED"}
@@ -131,7 +131,7 @@ class LoadBcfTopics(bpy.types.Operator):
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
props.topics.clear()
# workaround, one non standard topic would break reading entire bcf
# ignored these topics ATM
@@ -163,7 +163,8 @@ class LoadBcfTopic(bpy.types.Operator):
assert bcfxml
topic = bcfxml.topics[self.topic_guid]
bcfxml.get_header(self.topic_guid)
new = context.scene.BCFProperties.topics[self.topic_index]
props = tool.Bcf.get_bcf_props()
new = props.topics[self.topic_index]
data_map = {
"name": topic.guid,
"title": topic.topic.title,
@@ -244,7 +245,8 @@ class LoadBcfComments(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
blender_topic = context.scene.BCFProperties.topics.get(self.topic_guid)
props = tool.Bcf.get_bcf_props()
blender_topic = props.topics.get(self.topic_guid)
blender_topic.comments.clear()
for comment in bcfxml.topics[self.topic_guid].comments:
new = blender_topic.comments.add()
@@ -274,7 +276,9 @@ class EditBcfProjectName(bpy.types.Operator):
# Bonsai creates default project on load.
assert bcfxml.project
bcfxml.project.name = context.scene.BCFProperties.name
props = tool.Bcf.get_bcf_props()
bcfxml.project.name = props.name
return {"FINISHED"}
@@ -284,7 +288,7 @@ class EditBcfTopicName(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
@@ -300,7 +304,7 @@ class EditBcfTopic(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
@@ -371,13 +375,15 @@ class AddBcfTopic(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.scene.BCFProperties.author
props = tool.Bcf.get_bcf_props()
return props.author
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
bcfxml.add_topic("New Topic", "", context.scene.BCFProperties.author)
props = tool.Bcf.get_bcf_props()
bcfxml.add_topic("New Topic", "", props.author)
bpy.ops.bim.load_bcf_topics()
return {"FINISHED"}
@@ -389,7 +395,7 @@ class AddBcfBimSnippet(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
props_are_filled = all(
(getattr(props, attr) for attr in ("bim_snippet_reference", "bim_snippet_schema", "bim_snippet_type"))
)
@@ -403,7 +409,7 @@ class AddBcfBimSnippet(bpy.types.Operator):
assert bcfxml
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
is_external = "://" in props.bim_snippet_reference
@@ -435,7 +441,7 @@ class AddBcfRelatedTopic(bpy.types.Operator):
assert bcfxml
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
related_topics = tool.Bcf.get_topic_related_topics(topic)
@@ -462,14 +468,15 @@ class AddBcfHeaderFile(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.scene.BCFProperties.file_reference
props = tool.Bcf.get_bcf_props()
return props.file_reference
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
@@ -519,9 +526,10 @@ class ViewBcfTopic(bpy.types.Operator):
topic_guid: bpy.props.StringProperty()
def execute(self, context):
for index, topic in enumerate(context.scene.BCFProperties.topics):
props = tool.Bcf.get_bcf_props()
for index, topic in enumerate(props.topics):
if topic.name.lower() == self.topic_guid.lower():
context.scene.BCFProperties.active_topic_index = index
props.active_topic_index = index
break
return {"FINISHED"}
@@ -546,7 +554,7 @@ class AddBcfViewpoint(bpy.types.Operator):
blender_camera = context.scene.camera
assert blender_camera
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
@@ -675,7 +683,7 @@ class RemoveBcfViewpoint(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
if not bcfxml:
return False
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
topic = props.active_topic
if not topic:
return False
@@ -690,7 +698,7 @@ class RemoveBcfViewpoint(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
del topic.viewpoints[blender_topic.viewpoints]
@@ -715,7 +723,7 @@ class RemoveBcfFile(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
header_files = tool.Bcf.get_topic_header_files(topic)
@@ -733,13 +741,14 @@ class RemoveBcfTopic(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.scene.BCFProperties.topics
props = tool.Bcf.get_bcf_props()
return props.topics
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
del bcfxml.topics[props.active_topic.name]
bpy.ops.bim.load_bcf_topics()
return {"FINISHED"}
@@ -752,13 +761,14 @@ class AddBcfReferenceLink(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.scene.BCFProperties.reference_link
props = tool.Bcf.get_bcf_props()
return bool(props.reference_link)
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
reference_links = tool.Bcf.get_topic_reference_links(topic)
@@ -776,14 +786,15 @@ class AddBcfDocumentReference(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.scene.BCFProperties.document_reference
props = tool.Bcf.get_bcf_props()
return bool(props.document_reference)
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
@@ -855,13 +866,14 @@ class AddBcfLabel(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.scene.BCFProperties.label
props = tool.Bcf.get_bcf_props()
return bool(props.label)
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
new = blender_topic.labels.add()
@@ -883,7 +895,7 @@ class EditBcfReferenceLinks(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
reference_links = [r.name for r in blender_topic.reference_links]
@@ -900,7 +912,7 @@ class EditBcfLabels(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
labels = [l.name for l in blender_topic.labels]
@@ -918,7 +930,7 @@ class RemoveBcfReferenceLink(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
reference_links = tool.Bcf.get_topic_reference_links(topic)
@@ -938,7 +950,7 @@ class RemoveBcfLabel(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
labels = tool.Bcf.get_topic_labels(topic)
@@ -957,7 +969,7 @@ class RemoveBcfBimSnippet(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
tool.Bcf.set_topic_bim_snippet(topic, None)
@@ -978,7 +990,7 @@ class RemoveBcfDocumentReference(bpy.types.Operator):
assert bcfxml
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
@@ -1043,7 +1055,7 @@ class RemoveBcfRelatedTopic(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
related_topics = tool.Bcf.get_topic_related_topics(topic)
@@ -1063,7 +1075,7 @@ class RemoveBcfComment(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
comments = topic.comments
@@ -1084,7 +1096,7 @@ class EditBcfComment(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
blender_comment = blender_topic.comments.get(self.comment_guid)
topic = bcfxml.topics[blender_topic.name]
@@ -1092,7 +1104,7 @@ class EditBcfComment(bpy.types.Operator):
if comment.guid == self.comment_guid:
comment.comment = blender_comment.comment
comment.modified_date = XmlDateTime.now()
comment.modified_author = context.scene.BCFProperties.author
comment.modified_author = props.author
bpy.ops.bim.load_bcf_comments(topic_guid=topic.guid)
return {"FINISHED"}
@@ -1105,7 +1117,7 @@ class AddBcfComment(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
if not props.comment:
cls.poll_message_set("No comment to add.")
return False
@@ -1128,7 +1140,7 @@ class AddBcfComment(bpy.types.Operator):
assert bcfxml
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
comments = topic.comments
@@ -1136,7 +1148,7 @@ class AddBcfComment(bpy.types.Operator):
if bcf_v2:
comment = bcf.v2.model.Comment(
date=XmlDateTime.now(),
author=context.scene.BCFProperties.author,
author=props.author,
comment=props.comment,
guid=str(uuid.uuid4()),
)
@@ -1149,7 +1161,7 @@ class AddBcfComment(bpy.types.Operator):
else:
comment = bcf.v3.model.Comment(
date=XmlDateTime.now(),
author=context.scene.BCFProperties.author,
author=props.author,
comment=props.comment,
guid=str(uuid.uuid4()),
)
@@ -1179,7 +1191,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
if blender_topic is None:
cls.poll_message_set("No topic is active.")
@@ -1193,11 +1205,11 @@ class ActivateBcfViewpoint(bpy.types.Operator):
return True
def execute(self, context):
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
topic = bcfxml.topics[blender_topic.name]
if self.viewpoint_guid:
@@ -1303,7 +1315,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
[0, 0, 0, 1],
)
)
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if props.has_blender_offset:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
matrix = ifcopenshell.util.geolocation.global2local(
@@ -1342,7 +1354,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
objs: list[bpy.types.Object] = []
for global_id in exception_global_ids:
obj = IfcStore.get_element(global_id)
obj = tool.Ifc.get_object_by_identifier(global_id)
if obj and context.view_layer.objects.get(obj.name):
assert isinstance(obj, bpy.types.Object)
objs.append(obj)
@@ -1401,7 +1413,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
return
bpy.ops.object.select_all(action="DESELECT")
for global_id in selected_global_ids:
obj = IfcStore.get_element(global_id)
obj = tool.Ifc.get_object_by_identifier(global_id)
if obj:
obj.select_set(True)
obj.hide_set(False)
@@ -1414,7 +1426,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
for acomponent in acoloring.component:
global_id_colours.setdefault(acomponent.ifc_guid, acoloring.color)
for global_id, color in global_id_colours.items():
obj = IfcStore.get_element(global_id)
obj = tool.Ifc.get_object_by_identifier(global_id)
if obj:
obj.color = self.hex_to_rgb(color)
@@ -1517,7 +1529,8 @@ class OpenBcfReferenceLink(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
webbrowser.open(context.scene.BCFProperties.topic_links[self.index].name)
props = tool.Bcf.get_bcf_props()
webbrowser.open(props.topic_links[self.index].name)
return {"FINISHED"}
@@ -1530,7 +1543,8 @@ class SelectBcfHeaderFile(bpy.types.Operator):
def execute(self, context):
if self.filepath:
context.scene.BCFProperties.file_reference = self.filepath
props = tool.Bcf.get_bcf_props()
props.file_reference = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
@@ -1546,7 +1560,8 @@ class SelectBcfBimSnippetReference(bpy.types.Operator):
def execute(self, context):
if self.filepath:
context.scene.BCFProperties.bim_snippet_reference = self.filepath
props = tool.Bcf.get_bcf_props()
props.bim_snippet_reference = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
@@ -1562,7 +1577,8 @@ class SelectBcfDocumentReference(bpy.types.Operator):
def execute(self, context):
if self.filepath:
context.scene.BCFProperties.document_reference = self.filepath
props = tool.Bcf.get_bcf_props()
props.document_reference = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
@@ -1585,7 +1601,8 @@ class LoadBcfHeaderIfcFile(bpy.types.Operator):
assert bcfxml
bcf_path = tool.Bcf.get_path()
topic = bcfxml.topics[context.scene.BCFProperties.active_topic.name]
props = tool.Bcf.get_bcf_props()
topic = bcfxml.topics[props.active_topic.name]
entity = tool.Bcf.get_topic_header_files(topic)[self.index]
ifc_path = bcf.agnostic.topic.extract_file(topic, entity)
bpy.ops.bim.load_project(filepath=ifc_path)
@@ -1605,7 +1622,8 @@ class ExtractBcfFile(bpy.types.Operator):
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
topic = bcfxml.topics[context.scene.BCFProperties.active_topic.name]
props = tool.Bcf.get_bcf_props()
topic = bcfxml.topics[props.active_topic.name]
if self.entity_type == "HEADER_FILE":
entity = tool.Bcf.get_topic_header_files(topic)[self.index]
+75 -4
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 . import bcfstore
from bonsai.bim.prop import StrProperty
from bpy.types import PropertyGroup
@@ -34,6 +35,7 @@ from functools import partial
from typing import Literal
from typing_extensions import assert_never
from bcf.agnostic.extensions import get_extensions_attributes
from typing import TYPE_CHECKING, Union
bcfviewpoints_enum = None
@@ -95,7 +97,7 @@ def getBcfViewpoints(self, context, force_update=False):
global bcfviewpoints_enum
if bcfviewpoints_enum is None or force_update: # Retrieving Viewpoints is slow. Make sure we only do when needed
bcfviewpoints_enum = []
props = context.scene.BCFProperties
props = tool.Bcf.get_bcf_props()
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
topic = props.active_topic
@@ -110,6 +112,12 @@ class BcfBimSnippet(PropertyGroup):
type: StringProperty(name="Type")
is_external: BoolProperty(name="Is External")
if TYPE_CHECKING:
schema: str
reference: str
type: str
is_external: bool
class BcfDocumentReference(PropertyGroup):
reference: StringProperty(name="Reference")
@@ -117,6 +125,12 @@ class BcfDocumentReference(PropertyGroup):
guid: StringProperty(name="GUID")
is_external: BoolProperty(name="Is External")
if TYPE_CHECKING:
reference: str
description: str
guid: str
is_external: bool
class BcfComment(PropertyGroup):
name: StringProperty(name="GUID")
@@ -128,6 +142,16 @@ class BcfComment(PropertyGroup):
modified_author: StringProperty(name="Modified Author")
is_editable: BoolProperty(name="Is Editable", default=False, update=updateBcfCommentIsEditable)
if TYPE_CHECKING:
name: str
date: str
author: str
comment: str
viewpoint: str
modified_date: str
modified_author: str
is_editable: bool
def get_extensions_items(
self: "BCFProperties", context: bpy.types.Context, edit_text: str, extensions_attr: str
@@ -183,6 +207,30 @@ class BcfTopic(PropertyGroup):
comments: CollectionProperty(name="Comments", type=BcfComment)
is_editable: BoolProperty(name="Edit Topic Attributes", default=False, update=updateBcfTopicIsEditable)
if TYPE_CHECKING:
name: str
title: str
type: str
status: str
priority: str
stage: str
creation_date: str
creation_author: str
modified_date: str
modified_author: str
assigned_to: str
due_date: str
description: str
viewpoints: str
files: bpy.types.bpy_prop_collection_idprop[StrProperty]
reference_links: bpy.types.bpy_prop_collection_idprop[BcfReferenceLink]
labels: bpy.types.bpy_prop_collection_idprop[BcfLabel]
bim_snippet: BcfBimSnippet
document_references: bpy.types.bpy_prop_collection_idprop[BcfDocumentReference]
related_topics: bpy.types.bpy_prop_collection_idprop[StrProperty]
comments: bpy.types.bpy_prop_collection_idprop[BcfComment]
is_editable: bool
def get_related_topics(self: "BCFProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
global RELATED_TOPICS_ENUM_ITEMS
@@ -235,7 +283,30 @@ class BCFProperties(PropertyGroup):
comment: StringProperty(default="", name="Comment")
has_related_viewpoint: BoolProperty(name="Has Related Viewpoint", default=False)
def clear_input_fields(self):
if TYPE_CHECKING:
bcf_file: str
bcf_version: str
comment_text_width: int
name: str
author: str
topics: bpy.types.bpy_prop_collection_idprop[BcfTopic]
active_topic_index: int
file_reference: str
file_ifc_project: str
file_ifc_spatial_structure_element: str
reference_link: str
label: str
bim_snippet_reference: str
bim_snippet_type: str
bim_snippet_schema: str
document_reference: str
document_reference_description: str
document_description: str
related_topic: str
comment: str
has_related_viewpoint: bool
def clear_input_fields(self) -> None:
self.file_reference = ""
self.file_ifc_project = ""
self.file_ifc_spatial_structure_element = ""
@@ -250,7 +321,7 @@ class BCFProperties(PropertyGroup):
self.has_related_viewpoint = False
@property
def active_topic(self):
def active_topic(self) -> Union[BcfTopic, None]:
if len(self.topics) == 0:
return None
if self.active_topic_index < 0:
@@ -259,5 +330,5 @@ class BCFProperties(PropertyGroup):
self.active_topic_index = len(self.topics) - 1
return self.topics[self.active_topic_index]
def refresh_topic(self, context):
def refresh_topic(self, context: bpy.types.Context) -> None:
refreshBcfTopic(self, context)
+6 -10
View File
@@ -37,8 +37,7 @@ class BIM_PT_bcf(Panel):
layout.use_property_split = True
layout.use_property_decorate = False
scene = context.scene
props = scene.BCFProperties
props = tool.Bcf.get_bcf_props()
if not bcfstore.BcfStore.get_bcfxml():
row = layout.row(align=True)
@@ -63,18 +62,17 @@ class BIM_PT_bcf(Panel):
row = layout.row()
row.prop(props, "author")
props = context.scene.BCFProperties
row = layout.row()
row.template_list("BIM_UL_topics", "", props, "topics", props, "active_topic_index")
col = row.column(align=True)
col.operator("bim.add_bcf_topic", icon="ADD", text="")
col.operator("bim.remove_bcf_topic", icon="REMOVE", text="")
if props.active_topic_index < len(props.topics):
topic = props.active_topic
topic = props.active_topic
if topic is not None:
is_editable = topic.is_editable
col.prop(topic, "is_editable", icon="CHECKMARK" if topic.is_editable else "GREASEPENCIL", icon_only=True)
topic = props.active_topic
row = layout.row()
row.enabled = is_editable
row.prop(topic, "description", text="")
@@ -131,8 +129,7 @@ class BIM_PT_bcf_metadata(Panel):
layout.use_property_split = True
layout.use_property_decorate = False
scene = context.scene
props = scene.BCFProperties
props = tool.Bcf.get_bcf_props()
bcfxml = bcfstore.BcfStore.get_bcfxml()
if not bcfxml or props.active_topic_index >= len(props.topics):
@@ -296,8 +293,7 @@ class BIM_PT_bcf_comments(Panel):
layout.use_property_split = True
layout.use_property_decorate = False
scene = context.scene
props = scene.BCFProperties
props = tool.Bcf.get_bcf_props()
if props.active_topic_index >= len(props.topics):
layout.label(text="No BCF project is loaded")
@@ -25,6 +25,7 @@ import mathutils
import numpy as np
import multiprocessing
import ifcopenshell.api
import ifcopenshell.api.boundary
import ifcopenshell.geom
import ifcopenshell.util.unit
import ifcopenshell.util.shape
@@ -113,7 +114,7 @@ class Loader:
bm.edges.new((verts[-1], verts[0]))
bm.to_mesh(mesh)
bm.free()
mesh.BIMMeshProperties.ifc_definition_id = surface.id()
tool.Ifc.link(surface, mesh)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
matrix = mathutils.Matrix(
ifcopenshell.util.placement.get_axis2placement(surface.BasisSurface.Position).tolist()
@@ -315,7 +316,8 @@ class ColourByRelatedBuildingElement(bpy.types.Operator):
def _execute(self, context):
for obj in context.visible_objects:
if not obj.BIMObjectProperties.ifc_definition_id:
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
continue
element = tool.Ifc.get_entity(obj)
if not element.is_a("IfcRelSpaceBoundary"):
@@ -416,7 +418,7 @@ class UpdateBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
tool.Boundary.move_origin_to_space_origin(context.active_object)
settings = tool.Boundary.get_assign_connection_geometry_settings(context.active_object)
ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings)
ifcopenshell.api.boundary.assign_connection_geometry(tool.Ifc.get(), **settings)
return {"FINISHED"}
@@ -660,6 +662,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
obj.select_set(True)
def auto_generate_boundaries(self, space, space_obj):
props = tool.Model.get_model_props()
# Identify all potential building elements
# TODO: don't select everything, use AABB culling in Blender
building_elements = (
@@ -775,9 +778,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
# we cheat by using the exterior boundary to mean "gross".
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
parent_boundary = tool.Ifc.run(
"root.create_entity", ifc_class=bpy.context.scene.BIMModelProperties.boundary_class
)
parent_boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
if building_element.is_a("IfcVirtualElement"):
parent_boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
else:
@@ -854,9 +855,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
if opening_polygon.intersection(exterior_boundary_polygon).area == 0:
continue
boundary = tool.Ifc.run(
"root.create_entity", ifc_class=bpy.context.scene.BIMModelProperties.boundary_class
)
boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
boundary.RelatingSpace = space
boundary.RelatedBuildingElement = filling or opening
boundary.ConnectionGeometry = self.create_connection_geometry_from_polygon(
@@ -877,6 +876,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
if not relating_space or not related_building_element:
return
props = tool.Model.get_model_props()
# Find which face on space should be bounded to related building element
# TODO: Handle round wall where multiple faces need to be bound to the same related building element
bm = bmesh.new()
@@ -924,7 +924,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
):
return
parent_boundary = tool.Ifc.run("root.create_entity", ifc_class=context.scene.BIMModelProperties.boundary_class)
parent_boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
parent_boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
# Set to EXTERNAL by default and turn later to internal if there is a corresponding boundary relating to an
# internal space
@@ -959,7 +959,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
continue
connection_geometry = self.create_connection_geometry_from_polygon(opening_polygon, target_face_matrix)
boundary = tool.Ifc.run("root.create_entity", ifc_class=context.scene.BIMModelProperties.boundary_class)
boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
boundary.RelatingSpace = relating_space
boundary.RelatedBuildingElement = filling
boundary.ConnectionGeometry = connection_geometry
+12 -11
View File
@@ -18,7 +18,6 @@
import bpy
from bpy.types import Panel, UIList
from bonsai.bim.ifc import IfcStore
import bonsai.tool as tool
from bonsai.bim.module.boundary.data import SpaceBoundariesData
@@ -34,7 +33,7 @@ class BIM_PT_SceneBoundaries(Panel):
@classmethod
def poll(cls, context):
return IfcStore.get_file()
return tool.Ifc.get()
def draw(self, context):
row = self.layout.row(align=True)
@@ -53,20 +52,20 @@ class BIM_PT_Boundary(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
props = context.active_object.BIMObjectProperties
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
return False
if not IfcStore.get_element(props.ifc_definition_id):
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
return False
entity = IfcStore.get_file().by_id(props.ifc_definition_id)
entity = tool.Ifc.get().by_id(props.ifc_definition_id)
return entity.is_a("IfcRelSpaceBoundary")
def draw(self, context):
obj = context.active_object
assert obj
props = obj.BIMObjectProperties
props = tool.Blender.get_object_bim_props(obj)
ifc_file = tool.Ifc.get()
boundary = ifc_file.by_id(props.ifc_definition_id)
self.bprops = tool.Boundary.get_object_boundary_props(obj)
@@ -129,12 +128,12 @@ class BIM_PT_SpaceBoundaries(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
props = context.active_object.BIMObjectProperties
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
return False
if not IfcStore.get_element(props.ifc_definition_id):
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
return False
element = tool.Ifc.get_entity(context.active_object)
for ifc_class in ("IfcSpace", "IfcExternalSpatialElement"):
@@ -146,7 +145,9 @@ class BIM_PT_SpaceBoundaries(Panel):
if not SpaceBoundariesData.is_loaded:
SpaceBoundariesData.load()
self.props = context.active_object.BIMObjectProperties
obj = context.active_object
assert obj
self.props = tool.Blender.get_object_bim_props(obj)
self.ifc_file = tool.Ifc.get()
row = self.layout.row()
row.operator("bim.load_space_boundaries")
-1
View File
@@ -18,7 +18,6 @@
import bonsai.tool as tool
from bpy.types import Panel, UIList
from bonsai.bim.ifc import IfcStore
class BIM_PT_bsdd(Panel):
+51 -47
View File
@@ -128,9 +128,10 @@ class CadMitre(bpy.types.Operator):
class CadFillet(bpy.types.Operator):
bl_idname = "bim.cad_fillet"
bl_label = "CAD Fillet"
bl_description = "Add fillet to the 2 selected edges."
bl_options = {"REGISTER", "UNDO"}
resolution: bpy.props.IntProperty(name="Arc Resolution", min=0, default=1)
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE")
@classmethod
def poll(cls, context):
@@ -143,18 +144,15 @@ class CadFillet(bpy.types.Operator):
layout.prop(self, prop)
def execute(self, context):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.radius = self.radius * si_conversion
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.mode_set(mode="EDIT")
obj = bpy.context.active_object
cursor = obj.matrix_world.inverted() @ bpy.context.scene.cursor.location
mesh = obj.data
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
bm = bmesh.from_edit_mesh(mesh)
selected_edges = [e for e in bm.edges if e.select]
if len(selected_edges) != 2:
self.report({"ERROR"}, "Exactly 2 edges should be selected.")
return {"CANCELLED"}
# Assume the user has selected two edges sharing a vert, but merge verts
@@ -219,6 +217,7 @@ class CadFillet(bpy.types.Operator):
class CadArcFrom2Points(bpy.types.Operator):
bl_idname = "bim.cad_arc_from_2_points"
bl_label = "CAD Arc from 2 Points"
bl_description = "Add an arc to the active mesh based on 2 selected vertices."
bl_options = {"REGISTER", "UNDO"}
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
should_flip: bpy.props.BoolProperty(name="Flip", description="Flip arc", default=False)
@@ -235,6 +234,7 @@ class CadArcFrom2Points(bpy.types.Operator):
if bpy.context.mode != "EDIT_MESH":
return {"CANCELLED"}
obj = bpy.context.active_object
assert obj
region = bpy.context.region
region_3d = bpy.context.area.spaces.active.region_3d
cursor = bpy.context.scene.cursor.location
@@ -242,10 +242,12 @@ class CadArcFrom2Points(bpy.types.Operator):
if not center:
return {"CANCELLED"}
mesh = obj.data
assert isinstance(mesh, bpy.types.Mesh)
mw = obj.matrix_world
bm = bmesh.from_edit_mesh(mesh)
selected_verts = [v for v in bm.verts if v.select]
if len(selected_verts) != 2:
self.report({"ERROR"}, "Exactly 2 vertices should be selected.")
return {"CANCELLED"}
v1 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[0].co)
v2 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[1].co)
@@ -342,11 +344,12 @@ class CadArcFrom3Points(bpy.types.Operator):
class CadOffset(bpy.types.Operator):
bl_idname = "bim.cad_offset"
bl_label = "CAD Offset"
bl_description = "Copy selected mesh geometry at provided offset. Mesh copied based on the current viewport angle."
bl_options = {"REGISTER", "UNDO"}
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE")
@classmethod
def poll(self, context):
def poll(cls, context):
return context.mode == "EDIT_MESH"
def draw(self, context):
@@ -369,9 +372,6 @@ class CadOffset(bpy.types.Operator):
# effect of converting an unclosed loop into a closed loop which is also
# not what users expect.
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.distance = self.distance * si_conversion
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.mode_set(mode="EDIT")
@@ -543,7 +543,8 @@ class CadOffset(bpy.types.Operator):
class AddIfcCircle(bpy.types.Operator):
bl_idname = "bim.add_ifccircle"
bl_label = "Add IfcCircle"
radius: bpy.props.FloatProperty(name="Radius", default=0.5)
bl_description = "Add IfcCircle to the currently active mesh."
radius: bpy.props.FloatProperty(name="Radius", default=0.5, subtype="DISTANCE")
@classmethod
def poll(cls, context):
@@ -551,18 +552,19 @@ class AddIfcCircle(bpy.types.Operator):
return bool(obj) and obj.type == "MESH"
def execute(self, context):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.radius = self.radius * si_conversion
obj = context.active_object
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
self.obj = obj
self.mesh = mesh
if self.has_selected_existing_circle(context):
self.change_radius(context)
else:
self.create_circle(context)
return {"FINISHED"}
def has_selected_existing_circle(self, context):
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
def has_selected_existing_circle(self, context: bpy.types.Context) -> bool:
obj = self.obj
bm = bmesh.from_edit_mesh(self, mesh)
verts = [v for v in bm.verts if v.select and not v.hide]
if len(verts) != 2:
return False
@@ -575,32 +577,32 @@ class AddIfcCircle(bpy.types.Operator):
for group in groups:
if group in verts[0][deform_layer] and group in verts[1][deform_layer]:
return True
return False
def change_radius(self, context):
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
def change_radius(self, context: bpy.types.Object) -> None:
bm = bmesh.from_edit_mesh(self.mesh)
verts = [v for v in bm.verts if v.select and not v.hide]
center = verts[0].co.lerp(verts[1].co, 0.5)
verts[0].co = center + ((verts[0].co - center).normalized() * self.radius)
verts[1].co = center + ((verts[1].co - center).normalized() * self.radius)
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(obj.data)
bmesh.update_edit_mesh(self.mesh)
def create_circle(self, context):
obj = context.active_object
def create_circle(self, context: bpy.types.Context) -> None:
obj = self.obj
bpy.ops.object.mode_set(mode="OBJECT")
# The last group may be the result of a prior run of this operator.
# I tried looping through all groups but Blender group indices seem to behave unpredictably.
if len(obj.vertex_groups):
last_group = obj.vertex_groups[-1]
verts_in_group = [v for v in obj.data.vertices if last_group.index in [vg.group for vg in v.groups]]
verts_in_group = [v for v in self.mesh.vertices if last_group.index in [vg.group for vg in v.groups]]
if "IFCCIRCLE" in last_group.name and len(verts_in_group) != 2:
obj.vertex_groups.remove(last_group)
group = obj.vertex_groups.new(name="IFCCIRCLE")
bpy.ops.object.mode_set(mode="EDIT")
bm = bmesh.from_edit_mesh(obj.data)
bm = bmesh.from_edit_mesh(self.mesh)
bm.verts.layers.deform.verify()
deform_layer = bm.verts.layers.deform.active
@@ -617,7 +619,7 @@ class AddIfcCircle(bpy.types.Operator):
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(obj.data)
bmesh.update_edit_mesh(self.mesh)
class AddIfcArcIndexFillet(bpy.types.Operator):
@@ -638,18 +640,19 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
layout.prop(self, prop)
def execute(self, context):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.radius = self.radius * si_conversion
obj = context.active_object
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
self.obj = obj
self.mesh = mesh
if self.has_selected_existing_arc(context):
self.change_radius(context)
else:
self.create_arc(context)
return self.create_arc(context)
return {"FINISHED"}
def has_selected_existing_arc(self, context: bpy.types.Context) -> bool:
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
obj = self.obj
bm = bmesh.from_edit_mesh(self.mesh)
verts = [v for v in bm.verts if v.select and not v.hide]
if len(verts) != 3:
return False
@@ -668,13 +671,14 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
return False
def change_radius(self, context: bpy.types.Context) -> None:
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
obj = self.obj
bm = bmesh.from_edit_mesh(self.mesh)
edges = [e for e in bm.edges if e.select and not e.hide]
mid = list(set(edges[0].verts) & set(edges[1].verts))[0]
v1 = edges[0].other_vert(mid)
v2 = edges[1].other_vert(mid)
assert v1 and v2
center = tool.Cad.get_center_of_arc([v1.co, mid.co, v2.co])
if len(v1.link_edges) != 2 or len(v2.link_edges) != 2:
@@ -682,6 +686,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
v3 = v1.link_edges[1].other_vert(v1) if mid in v1.link_edges[0].verts else v1.link_edges[0].other_vert(v1)
v4 = v2.link_edges[1].other_vert(v2) if mid in v2.link_edges[0].verts else v2.link_edges[0].other_vert(v2)
assert v3 and v4
dir1 = (v3.co - v1.co).normalized()
dir2 = (v4.co - v2.co).normalized()
@@ -702,11 +707,10 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(obj.data)
def create_arc(self, context):
obj = bpy.context.active_object
bmesh.update_edit_mesh(self.mesh)
def create_arc(self, context: bpy.types.Context) -> set[str]:
obj = self.obj
bpy.ops.object.mode_set(mode="OBJECT")
# The last group may be the result of a prior run of this operator.
# I tried looping through all groups but Blender group indices seem to behave unpredictably.
@@ -718,7 +722,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
group = obj.vertex_groups.new(name="IFCARCINDEX")
bpy.ops.object.mode_set(mode="EDIT")
bm = bmesh.from_edit_mesh(obj.data)
bm = bmesh.from_edit_mesh(self.mesh)
bm.verts.layers.deform.verify()
deform_layer = bm.verts.layers.deform.active
@@ -737,6 +741,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
shared_vert = list(set(selected_edges[0].verts) & set(selected_edges[1].verts))[0]
v1 = selected_edges[0].other_vert(shared_vert)
v2 = selected_edges[1].other_vert(shared_vert)
assert v1 and v2
dir1 = (v1.co - shared_vert.co).normalized()
dir2 = (v2.co - shared_vert.co).normalized()
edge_angle = dir1.angle(dir2)
@@ -770,7 +775,8 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(obj.data)
bmesh.update_edit_mesh(self.mesh)
return {"FINISHED"}
class AlignViewToProfile(bpy.types.Operator):
@@ -806,8 +812,9 @@ class AlignViewToProfile(bpy.types.Operator):
class AddRectangle(bpy.types.Operator):
bl_idname = "bim.add_rectangle"
bl_label = "Add Rectangle"
x: bpy.props.FloatProperty(name="X", default=1)
y: bpy.props.FloatProperty(name="Y", default=1)
bl_description = "Add rectangle shape to the active mesh."
x: bpy.props.FloatProperty(name="X", default=1, subtype="DISTANCE")
y: bpy.props.FloatProperty(name="Y", default=1, subtype="DISTANCE")
@classmethod
def poll(cls, context):
@@ -815,11 +822,8 @@ class AddRectangle(bpy.types.Operator):
return bool(obj) and obj.type == "MESH"
def execute(self, context):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.x = self.x * si_conversion
self.y = self.y * si_conversion
obj = context.active_object
assert obj and isinstance(obj.data, bpy.types.Mesh)
bm = bmesh.from_edit_mesh(obj.data)
cursor = obj.matrix_world.inverted() @ context.scene.cursor.location
new_verts = [
+9
View File
@@ -20,6 +20,7 @@ import bpy
from bonsai.bim.module.model.data import AuthoringData
from bpy.types import PropertyGroup
from math import pi
from typing import TYPE_CHECKING
class BIMCadProperties(PropertyGroup):
@@ -31,3 +32,11 @@ class BIMCadProperties(PropertyGroup):
gable_roof_edge_angle: bpy.props.FloatProperty(
name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE"
)
if TYPE_CHECKING:
resolution: int
radius: float
distance: float
x: float
y: float
gable_roof_edge_angle: float
+54 -59
View File
@@ -23,7 +23,7 @@ import bonsai.bim.module.type.prop as type_prop
import ifcopenshell.util.unit
from bpy.types import WorkSpaceTool
from bonsai.bim.module.model.data import AuthoringData, RailingData, RoofData
from typing import Union
from functools import partial
def load_custom_icons():
@@ -69,10 +69,12 @@ class CadTool(WorkSpaceTool):
("bim.cad_hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
)
def draw_settings(context, layout, workspace_tool):
def draw_settings(
context: bpy.types.Context, layout: bpy.types.UILayout, workspace_tool: bpy.types.WorkSpaceTool
) -> None:
ui_context = str(context.region.type)
obj = context.active_object
if not obj or not obj.data:
if not obj or not (data := obj.data):
return
is_profile = tool.Geometry.is_profile_object_active()
if is_profile:
@@ -112,17 +114,20 @@ class CadTool(WorkSpaceTool):
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Rectangle", "S_R", "Rectangle", ui_context)
add_layout_hotkey_operator(row, "Rectangle", "S_R", bpy.ops.bim.add_rectangle.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Circle", "S_C", "Circle", ui_context)
add_layout_hotkey_operator(row, "Circle", "S_C", bpy.ops.bim.add_ifccircle.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.set_arc_index.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Reset Vertex", "S_X", bpy.ops.bim.reset_vertex.__doc__, ui_context)
elif hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.subshape_type == "AXIS":
elif (
isinstance(data, tool.Geometry.TYPES_WITH_MESH_PROPERTIES)
and tool.Geometry.get_mesh_props(data).subshape_type == "AXIS"
):
add_header_apply_button(
layout, "Edit Axis", "bim.edit_extrusion_axis", "bim.disable_editing_extrusion_axis", ui_context
)
@@ -133,15 +138,15 @@ class CadTool(WorkSpaceTool):
row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", "Fillet", ui_context)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.cad_fillet.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
else:
if (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["pset_data"]
and context.active_object.BIMRailingProperties.is_editing_path
and tool.Model.get_railing_props(obj).is_editing_path
):
add_header_apply_button(
layout,
@@ -154,7 +159,7 @@ class CadTool(WorkSpaceTool):
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
and context.active_object.BIMRoofProperties.is_editing_path
and tool.Model.get_roof_props(obj).is_editing_path
):
add_header_apply_button(
layout, "Edit Roof Path", "bim.finish_editing_roof_path", "bim.cancel_editing_roof_path", ui_context
@@ -171,17 +176,17 @@ class CadTool(WorkSpaceTool):
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "2-Point Arc", "S_C", "2-Point Arc", ui_context)
add_layout_hotkey_operator(row, "2-Point Arc", "S_C", bpy.ops.bim.cad_arc_from_2_points.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.cad_arc_from_3_points.__doc__, ui_context)
class CadHotkey(bpy.types.Operator):
bl_idname = "bim.cad_hotkey"
bl_label = "CAD Hotkey"
bl_options = {"REGISTER", "UNDO"}
bl_label = ""
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
hotkey: bpy.props.StringProperty()
description: bpy.props.StringProperty()
@@ -190,12 +195,14 @@ class CadHotkey(bpy.types.Operator):
return operator.description or ""
def execute(self, context):
self.props = context.scene.BIMCadProperties
self.props = tool.Cad.get_cad_props()
getattr(self, f"hotkey_{self.hotkey}")()
return {"FINISHED"}
def draw(self, context):
props = context.scene.BIMCadProperties
props = tool.Cad.get_cad_props()
obj = context.active_object
if self.hotkey == "S_C":
if tool.Geometry.is_profile_object_active():
row = self.layout.row()
@@ -221,7 +228,7 @@ class CadHotkey(bpy.types.Operator):
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
and bpy.context.active_object.BIMRoofProperties.is_editing_path
and tool.Model.get_roof_props(obj).is_editing_path
):
self.layout.row().prop(props, "gable_roof_edge_angle")
@@ -231,9 +238,8 @@ class CadHotkey(bpy.types.Operator):
row.prop(props, "resolution")
def hotkey_S_C(self):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if tool.Geometry.is_profile_object_active():
bpy.ops.bim.add_ifccircle(radius=self.props.radius / si_conversion)
bpy.ops.bim.add_ifccircle(radius=self.props.radius)
else:
bpy.ops.bim.cad_arc_from_2_points()
@@ -241,36 +247,49 @@ class CadHotkey(bpy.types.Operator):
bpy.ops.bim.cad_trim_extend()
def hotkey_S_F(self):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if tool.Geometry.is_profile_object_active():
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius / si_conversion)
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius)
else:
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius / si_conversion)
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius)
def hotkey_S_O(self):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bpy.ops.bim.cad_offset(distance=self.props.distance / si_conversion)
bpy.ops.bim.cad_offset(distance=self.props.distance)
def hotkey_S_Q(self):
element = tool.Ifc.get_entity(bpy.context.active_object)
if bpy.context.active_object.data.BIMMeshProperties.subshape_type == "PROFILE":
obj = bpy.context.active_object
if not obj:
return
if not tool.Geometry.has_mesh_properties(data := obj.data):
return
element = tool.Ifc.get_entity(obj)
if not element:
return
mprops = tool.Geometry.get_mesh_props(data)
if mprops.subshape_type == "PROFILE":
if element.is_a("IfcProfileDef"):
bpy.ops.bim.edit_arbitrary_profile()
elif element.is_a("IfcRelSpaceBoundary"):
bpy.ops.bim.edit_boundary_geometry()
else:
bpy.ops.bim.edit_extrusion_profile()
elif bpy.context.active_object.data.BIMMeshProperties.subshape_type == "AXIS":
elif mprops.subshape_type == "AXIS":
bpy.ops.bim.edit_extrusion_axis()
def hotkey_S_R(self):
obj = bpy.context.active_object
if not obj:
return
if tool.Geometry.is_profile_object_active():
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bpy.ops.bim.add_rectangle(x=self.props.x / si_conversion, y=self.props.y / si_conversion)
bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y)
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
and bpy.context.active_object.BIMRoofProperties.is_editing_path
and tool.Model.get_roof_props(obj).is_editing_path
):
bpy.ops.bim.set_gable_roof_edge_angle(angle=self.props.gable_roof_edge_angle)
@@ -288,7 +307,9 @@ class CadHotkey(bpy.types.Operator):
bpy.ops.bim.reset_vertex()
def add_header_apply_button(layout, text, apply_operator, cancel_operator, ui_context=""):
def add_header_apply_button(
layout: bpy.types.UILayout, text: str, apply_operator: str, cancel_operator: str, ui_context: str = ""
) -> None:
custom_icon = custom_icon_previews.get(text.upper().replace(" ", "_"), custom_icon_previews["IFC"]).icon_id
row = layout.row(align=True)
row.label(text=f"{text} Mode", icon_value=custom_icon)
@@ -307,34 +328,8 @@ def add_header_apply_button(layout, text, apply_operator, cancel_operator, ui_co
row.label(text="Tools")
def add_layout_hotkey_operator(
layout: bpy.types.UILayout, text: str, hotkey: str, description: Union[str, None], ui_context: str = ""
) -> bpy.types.OperatorProperties:
parts = hotkey.split("_")
modifier, key = parts
op_text = "" if ui_context == "TOOL_HEADER" else text
custom_icon = custom_icon_previews.get(text.upper().replace(" ", "_"), custom_icon_previews["IFC"]).icon_id
modifier_icon, modifier_str = MODIFIERS.get(modifier, ("NONE", ""))
row = layout if ui_context == "TOOL_HEADER" else layout.row(align=True)
op = row.operator("bim.cad_hotkey", text=op_text, icon_value=custom_icon)
if ui_context != "TOOL_HEADER" and len(parts) == 2:
layout = layout.row(align=True) # Create a new line for hotkey display
layout.label(text="", icon=modifier_icon)
layout.label(text="", icon=f"EVENT_{key}")
hotkey_description = f"Hotkey: {modifier_str} {key}"
description = "\n\n".join(filter(None, [hotkey_description if description else ""]))
op.hotkey = hotkey
op.description = description or hotkey_description
return op
add_layout_hotkey_operator = partial(tool.Blender.add_layout_hotkey_operator, tool_name="cad", module_name=__name__)
custom_icon_previews = None
display_mode = None
MODIFIERS = {
"S": ("EVENT_SHIFT", ""),
}
@@ -300,7 +300,7 @@ class SelectIfcClashResults(bpy.types.Operator):
def execute(self, context):
# TODO refactor into new clash results system
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
with open(self.filepath) as f:
clash_sets = json.load(f)
@@ -316,13 +316,15 @@ class SelectIfcClashResults(bpy.types.Operator):
global_ids.extend([clash["a_global_id"], clash["b_global_id"]])
for obj in context.visible_objects:
if not obj.BIMObjectProperties.ifc_definition_id:
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
continue
ifc_file = ""
for scene in obj.users_scene:
if scene.BIMProperties.ifc_file:
ifc_file = scene.BIMProperties.ifc_file
bim_props = tool.Blender.get_bim_props(scene)
if bim_props.ifc_file:
ifc_file = bim_props.ifc_file
if scene.library:
break
@@ -334,7 +336,7 @@ class SelectIfcClashResults(bpy.types.Operator):
element_file = self.file
try:
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
element = element_file.by_id(props.ifc_definition_id)
except:
continue
@@ -478,7 +480,7 @@ class SelectSmartGroup(bpy.types.Operator):
@classmethod
def poll(cls, context):
return IfcStore.get_file() and context.visible_objects and context.scene.BIMClashProperties.active_smart_group
return tool.Ifc.get() and context.visible_objects and context.scene.BIMClashProperties.active_smart_group
def execute(self, context):
selected_smart_group = context.scene.BIMClashProperties.active_smart_group
@@ -118,7 +118,7 @@ class MaterialClassificationsData(ReferencesData):
def references(cls):
results = []
props = bpy.context.scene.BIMMaterialProperties
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.ifc_definition_id:
@@ -201,7 +201,8 @@ class EnableEditingClassification(bpy.types.Operator):
def execute(self, context):
def callback(name, prop, data):
if name == "ReferenceTokens":
new = bpy.context.scene.BIMGeoreferenceProperties.projected_crs.add()
geo_props = tool.Georeference.get_georeference_props()
new = geo_props.projected_crs.add()
new.name = name
new.data_type = "string"
new.is_null = data[name] is None
@@ -239,7 +240,7 @@ class RemoveClassification(bpy.types.Operator, tool.Ifc.Operator):
classification: bpy.props.IntProperty()
def _execute(self, context):
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"classification.remove_classification",
tool.Ifc.get(),
@@ -21,7 +21,6 @@ import bonsai.bim.helper
import bonsai.tool as tool
import bonsai.bim.module.classification.prop as classification_prop
from bpy.types import Panel, UIList
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.classification.data import (
ClassificationsData,
ClassificationReferencesData,
@@ -124,7 +123,7 @@ class ReferenceUI:
self.sprops = context.scene.BIMClassificationProperties
self.bprops = context.scene.BIMBSDDProperties
self.props = context.scene.BIMClassificationReferenceProperties
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
self.draw_add_ui(context)
@@ -312,7 +311,7 @@ class BIM_PT_material_classifications(Panel, ReferenceUI):
def poll(cls, context):
if not tool.Ifc.get():
return False
props = context.scene.BIMMaterialProperties
props = tool.Material.get_material_props()
if props.is_editing and (material := props.active_material) and material.ifc_definition_id:
return True
return False
@@ -23,7 +23,6 @@ import ifcopenshell.api.constraint
import ifcopenshell.util.attribute
import bonsai.bim.helper
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
class LoadObjectives(bpy.types.Operator):
@@ -84,7 +83,7 @@ class AddObjective(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
result = ifcopenshell.api.run("constraint.add_objective", IfcStore.get_file())
result = ifcopenshell.api.run("constraint.add_objective", tool.Ifc.get())
bpy.ops.bim.load_objectives()
bpy.ops.bim.enable_editing_constraint(constraint=result.id())
return {"FINISHED"}
@@ -116,7 +115,7 @@ class RemoveConstraint(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMConstraintProperties
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"constraint.remove_constraint", self.file, **{"constraint": self.file.by_id(self.constraint)}
)
@@ -162,8 +161,12 @@ class AssignConstraint(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.file = tool.Ifc.get()
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)]
objs = [bpy.data.objects[self.obj]] if self.obj else context.selected_objects
products = [
self.file.by_id(obj_id)
for obj in objs
if (obj_id := tool.Blender.get_object_bim_props(obj).ifc_definition_id)
]
if products:
ifcopenshell.api.run(
"constraint.assign_constraint",
@@ -185,8 +188,12 @@ class UnassignConstraint(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.file = tool.Ifc.get()
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)]
objs = [bpy.data.objects[self.obj]] if self.obj else context.selected_objects
products = [
self.file.by_id(obj_id)
for obj in objs
if (obj_id := tool.Blender.get_object_bim_props(obj).ifc_definition_id)
]
if products:
ifcopenshell.api.run(
"constraint.unassign_constraint",
@@ -16,8 +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/>.
import bonsai.tool as tool
from bpy.types import Panel, UIList
from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import draw_attributes
from bonsai.bim.module.constraint.data import ConstraintsData, ObjectConstraintsData
@@ -33,7 +33,7 @@ class BIM_PT_constraints(Panel):
@classmethod
def poll(cls, context):
return IfcStore.get_file()
return tool.Ifc.get()
def draw(self, context):
if not ConstraintsData.is_loaded:
@@ -79,21 +79,21 @@ class BIM_PT_object_constraints(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
return False
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
props = tool.Blender.get_object_bim_props(obj)
return bool(tool.Ifc.get_object_by_identifier(props.ifc_definition_id))
def draw(self, context):
if not ObjectConstraintsData.is_loaded:
ObjectConstraintsData.load()
obj = context.active_object
self.oprops = obj.BIMObjectProperties
assert obj
self.oprops = tool.Blender.get_object_bim_props(obj)
self.sprops = context.scene.BIMConstraintProperties
self.props = obj.BIMObjectConstraintProperties
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
self.draw_add_ui()
@@ -740,15 +740,14 @@ class LoadProductCostItems(bpy.types.Operator):
@classmethod
def poll(cls, context):
if not tool.Ifc.get() or not (obj := context.active_object) or not (obj.BIMObjectProperties.ifc_definition_id):
if not tool.Ifc.get() or not (obj := context.active_object) or not (tool.Blender.get_ifc_definition_id(obj)):
cls.poll_message_set("No IFC object is active.")
return False
return True
def execute(self, context):
core.load_product_cost_items(
tool.Cost, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id)
)
obj = context.active_object
core.load_product_cost_items(tool.Cost, product=tool.Ifc.get_entity(obj))
return {"FINISHED"}
+2 -3
View File
@@ -19,7 +19,6 @@
import bpy
import ifcopenshell.api
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.classification.data import CostClassificationsData
from bonsai.bim.module.cost.data import CostSchedulesData, CostItemRatesData, CostItemQuantitiesData
from bonsai.bim.prop import StrProperty, Attribute
@@ -83,7 +82,7 @@ def update_cost_item_identification(self, context):
props = context.scene.BIMCostProperties
if not props.is_cost_update_enabled or self.identification == "XXX":
return
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"cost.edit_cost_item",
self.file,
@@ -98,7 +97,7 @@ def update_cost_item_name(self, context):
props = context.scene.BIMCostProperties
if not props.is_cost_update_enabled or self.name == "Unnamed":
return
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"cost.edit_cost_item",
self.file,
+1 -2
View File
@@ -21,7 +21,6 @@ import bonsai.bim.helper
import bonsai.bim.module.cost.prop as CostProp
import bonsai.tool as tool
from bpy.types import Panel, UIList
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.cost.data import CostSchedulesData
from typing import Any
@@ -37,7 +36,7 @@ class BIM_PT_cost_schedules(Panel):
@classmethod
def poll(cls, context):
file = IfcStore.get_file()
file = tool.Ifc.get()
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
def draw(self, context):
@@ -18,9 +18,13 @@
import bpy
from bpy.types import PropertyGroup
from typing import TYPE_CHECKING
class BIMCoveringProperties(PropertyGroup):
ceiling_height: bpy.props.FloatProperty(
name="ceiling_height", default=2.7, subtype="DISTANCE", description="Ceiling height"
)
if TYPE_CHECKING:
ceiling_height: float
@@ -24,6 +24,7 @@ import bonsai.tool as tool
from bonsai.bim.helper import prop_with_search
from bonsai.bim.module.model.data import AuthoringData
from bpy.types import WorkSpaceTool
from functools import partial
class CoveringTool(WorkSpaceTool):
@@ -45,17 +46,15 @@ class CoveringTool(WorkSpaceTool):
CoveringToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
def add_layout_hotkey(layout: bpy.types.UILayout, text: str, hotkey: str, description: str) -> None:
args = ("covering", layout, text, hotkey, description)
tool.Blender.add_layout_hotkey_operator(*args)
add_layout_hotkey = partial(tool.Blender.add_layout_hotkey_operator, tool_name="covering", module_name=__name__)
class CoveringToolUI:
@classmethod
def draw(cls, context, layout, ifc_element_type=None):
cls.layout = layout
cls.props = context.scene.BIMModelProperties
cls.covering_props = context.scene.BIMCoveringProperties
cls.props = tool.Model.get_model_props()
cls.covering_props = tool.Covering.get_covering_props()
row = cls.layout.row(align=True)
if not tool.Ifc.get():
@@ -136,8 +135,8 @@ class CoveringToolUI:
if AuthoringData.data["ifc_classes"]:
if cls.props.ifc_class:
box = cls.layout.box()
if AuthoringData.data["type_thumbnail"]:
box.template_icon(icon_value=AuthoringData.data["type_thumbnail"], scale=5)
if thumbnail := AuthoringData.data["relating_type_data"].get("thumbnail"):
box.template_icon(icon_value=thumbnail, scale=5)
else:
op = box.operator("bim.load_type_thumbnails", text="Load Thumbnails", icon="FILE_REFRESH")
op.ifc_class = cls.props.ifc_class
@@ -145,8 +144,8 @@ class CoveringToolUI:
class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.covering_hotkey"
bl_label = "Hotkey"
bl_options = {"REGISTER", "UNDO"}
bl_label = ""
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
hotkey: bpy.props.StringProperty()
description: bpy.props.StringProperty()
@@ -159,12 +158,11 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
return operator.description or ""
def _execute(self, context):
# self.props = context.scene.BIMCoveringProperties
self.props = tool.Covering.get_covering_props()
getattr(self, f"hotkey_{self.hotkey}")()
def invoke(self, context, event):
# https://blender.stackexchange.com/questions/276035/how-do-i-make-operators-remember-their-property-values-when-called-from-a-hotkey
# self.props = context.scene.BIMSpatialProperties
return self.execute(context)
def draw(self, context):
@@ -175,14 +173,14 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(active_obj)
container = tool.Root.get_default_container()
if AuthoringData.data["predefined_type"] == "FLOORING":
if AuthoringData.data["relating_type_data"].get("predefined_type") == "FLOORING":
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
bpy.ops.bim.add_instance_flooring_coverings_from_walls()
elif container:
bpy.ops.bim.add_instance_flooring_covering_from_cursor()
else:
bpy.ops.bim.add_constr_type_instance()
elif AuthoringData.data["predefined_type"] == "CEILING":
elif AuthoringData.data["relating_type_data"].get("predefined_type") == "CEILING":
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
bpy.ops.bim.add_instance_ceiling_coverings_from_walls()
elif container:
@@ -20,8 +20,8 @@ import bpy
import json
import ifcopenshell
import ifcopenshell.util.element
import bonsai.tool as tool
from math import degrees, atan2
from bonsai.bim.ifc import IfcStore
from .api import Api
@@ -92,7 +92,7 @@ class RunAnalysis(bpy.types.Operator):
bl_label = "Run Analysis"
def execute(self, context):
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
self.inputs = {
"floors": [],
"walls": [],
@@ -181,12 +181,12 @@ class RunAnalysis(bpy.types.Operator):
if modifier.type == "TRIANGULATE":
return True
def get_covetool_category(self, obj):
def get_covetool_category(self, obj: bpy.types.Object):
if not hasattr(obj, "data") or not isinstance(obj.data, bpy.types.Mesh):
return
if not obj.BIMObjectProperties.ifc_definition_id:
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
return
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
element = self.file.by_id(ifc_id)
ifc_class = element.is_a()
if "IfcSlab" in ifc_class:
return "floors"
+2 -3
View File
@@ -27,7 +27,6 @@ import ifcopenshell
import ifcopenshell.util.selector
import bonsai.tool as tool
import bonsai.bim.module.drawing.scheduler as scheduler
from bonsai.bim.ifc import IfcStore
from bonsai.bim.handler import refresh_ui_data
from typing import TYPE_CHECKING
from collections import Counter
@@ -212,7 +211,7 @@ class ExportIfcCsv(bpy.types.Operator):
props = context.scene.CsvProperties
self.filepath = bpy.path.ensure_ext(self.filepath, f".{props.format}")
if props.should_load_from_memory:
ifc_file = IfcStore.get_file()
ifc_file = tool.Ifc.get()
else:
ifc_file = ifcopenshell.open(props.csv_ifc_file)
results = ifcopenshell.util.selector.filter_elements(
@@ -314,7 +313,7 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator):
props = context.scene.CsvProperties
if props.should_load_from_memory:
ifc_file = IfcStore.get_file()
ifc_file = tool.Ifc.get()
else:
ifc_file = ifcopenshell.open(props.csv_ifc_file)
ifc_csv = ifccsv.IfcCsv()
+3 -3
View File
@@ -17,8 +17,8 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bonsai.bim.helper
import bonsai.tool as tool
from bpy.types import Panel
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.search.data import SearchData
@@ -37,7 +37,7 @@ class BIM_PT_ifccsv(Panel):
scene = context.scene
props = scene.CsvProperties
if IfcStore.get_file():
if tool.Ifc.get():
row = layout.row(align=True)
row.prop(props, "should_load_from_memory")
row.operator("bim.import_csv_attributes", icon="IMPORT", text="")
@@ -49,7 +49,7 @@ class BIM_PT_ifccsv(Panel):
row.operator("bim.export_csv_attributes", icon="EXPORT", text="")
row.prop(props, "should_show_settings", icon="PREFERENCES", text="")
if not IfcStore.get_file() or not props.should_load_from_memory:
if not tool.Ifc.get() or not props.should_load_from_memory:
row = layout.row(align=True)
row.prop(props, "csv_ifc_file")
row.operator("bim.select_csv_ifc_file", icon="FILE_FOLDER", text="")
+60 -34
View File
@@ -78,10 +78,10 @@ class PrintIfcFile(bpy.types.Operator):
@classmethod
def poll(cls, context):
return IfcStore.get_file()
return tool.Ifc.get()
def execute(self, context):
print(IfcStore.get_file().wrapped_data.to_string())
print(tool.Ifc.get().wrapped_data.to_string())
return {"FINISHED"}
@@ -101,13 +101,14 @@ class ConvertToBlender(bpy.types.Operator):
if tool.Geometry.has_mesh_properties(data):
if data.library:
continue
data.BIMMeshProperties.ifc_definition_id = 0
tool.Geometry.get_mesh_props(data).ifc_definition_id = 0
for material in bpy.data.materials:
if material.library:
continue
tool.Ifc.unlink(obj=material)
context.scene.BIMProperties.ifc_file = ""
context.scene.BIMDebugProperties.attributes.clear()
bim_props = tool.Blender.get_bim_props()
bim_props.ifc_file = ""
tool.Debug.get_debug_props().attributes.clear()
IfcStore.purge()
bonsai.bim.handler.refresh_ui_data()
return {"FINISHED"}
@@ -160,7 +161,8 @@ class ProfileImportIFC(bpy.types.Operator):
if not tool.Ifc.get():
cls.poll_message_set("No IFC file loaded.")
return False
if not context.scene.BIMProperties.ifc_file:
bim_props = tool.Blender.get_bim_props()
if not bim_props.ifc_file:
cls.poll_message_set("Current IFC file is not saved.")
return False
return True
@@ -184,10 +186,10 @@ class CreateAllShapes(bpy.types.Operator):
@classmethod
def poll(cls, context):
return IfcStore.get_file()
return tool.Ifc.get()
def execute(self, context):
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
elements = self.file.by_type("IfcElement") + self.file.by_type("IfcSpace")
total = len(elements)
@@ -256,7 +258,7 @@ class CreateShapeFromStepId(bpy.types.Operator):
logger = logging.getLogger("ImportIFC")
self.ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger)
self.file = tool.Ifc.get()
element = self.file.by_id(self.step_id or int(context.scene.BIMDebugProperties.step_id))
element = self.file.by_id(self.step_id or int(tool.Debug.get_debug_props().step_id))
settings = ifcopenshell.geom.settings()
settings.set("keep-bounding-boxes", True)
if self.should_include_curves:
@@ -309,7 +311,7 @@ class RewindInspector(bpy.types.Operator):
bl_description = "Rewind the Inspector to the previously inspected element"
def execute(self, context):
props = context.scene.BIMDebugProperties
props = tool.Debug.get_debug_props()
total_breadcrumbs = len(props.step_id_breadcrumb)
if total_breadcrumbs < 2:
return {"FINISHED"}
@@ -328,13 +330,13 @@ class InspectFromStepId(bpy.types.Operator):
@classmethod
def poll(cls, context):
return IfcStore.get_file()
return tool.Ifc.get()
def execute(self, context):
self.file = IfcStore.get_file()
debug_props = context.scene.BIMDebugProperties
self.file = tool.Ifc.get()
debug_props = tool.Debug.get_debug_props()
debug_props.active_step_id = self.step_id
crumb = context.scene.BIMDebugProperties.step_id_breadcrumb.add()
crumb = debug_props.step_id_breadcrumb.add()
crumb.name = str(self.step_id)
element = self.file.by_id(self.step_id)
debug_props.attributes.clear()
@@ -380,12 +382,12 @@ class InspectFromObject(bpy.types.Operator):
def get_active_object_ifc_definition(cls, context: bpy.types.Context) -> Union[int, None]:
obj = context.active_object
assert obj
if ifc_id := obj.BIMObjectProperties.ifc_definition_id:
if ifc_id := tool.Blender.get_ifc_definition_id(obj):
return ifc_id
if (
(data := obj.data)
and tool.Geometry.has_mesh_properties(data)
and (ifc_id := data.BIMMeshProperties.ifc_definition_id)
and (ifc_id := tool.Geometry.get_mesh_props(data).ifc_definition_id)
):
return ifc_id
@@ -422,7 +424,7 @@ class PrintObjectPlacement(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
placement = ifcopenshell.util.placement.get_local_placement(IfcStore.get_file().by_id(self.step_id))
placement = ifcopenshell.util.placement.get_local_placement(tool.Ifc.get().by_id(self.step_id))
if self.create_empty_object:
bpy.ops.object.empty_add(type="ARROWS")
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
@@ -438,7 +440,8 @@ class ParseExpress(bpy.types.Operator):
bl_label = "Parse Express"
def execute(self, context):
core.parse_express(tool.Debug, context.scene.BIMDebugProperties.express_file)
props = tool.Debug.get_debug_props()
core.parse_express(tool.Debug, props.express_file)
bonsai.bim.handler.refresh_ui_data()
return {"FINISHED"}
@@ -452,8 +455,9 @@ class SelectExpressFile(bpy.types.Operator):
filter_glob: bpy.props.StringProperty(default="*.exp", options={"HIDDEN"})
def execute(self, context):
props = tool.Debug.get_debug_props()
if os.path.exists(self.filepath) and "exp" in os.path.splitext(self.filepath)[1]:
context.scene.BIMDebugProperties.express_file = self.filepath
props.express_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
@@ -497,7 +501,7 @@ class PrintUnusedElementStats(bpy.types.Operator):
ignore_styled_items: bpy.props.BoolProperty(name="Ignore Styled Items", default=True)
def execute(self, context):
props = context.scene.BIMDebugProperties
props = tool.Debug.get_debug_props()
# ignore some classes that could have zero 0 inverse references by their nature
ignore_classes = []
if self.ignore_contexts:
@@ -509,6 +513,7 @@ class PrintUnusedElementStats(bpy.types.Operator):
if self.ignore_styled_items:
ignore_classes += ["IfcStyledItem"]
ignore_classes += [
"IfcDocumentReference", # Document references for sheet elements (drawings, schedules, etc).
"IfcIndexedColourMap", # Only referenced by inverse attributes.
"IfcIndexedTextureMap", # Only referenced by inverse attributes.
]
@@ -523,7 +528,7 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Purge Unused Elements By Class"
bl_description = (
"Will find all elements of class that have no inverse references and will remove them, use very carefully.\n"
"If IFC class is provided in neighbour field, will purge only elemnts of the provided class. Otherwise will purge all white-listed elements.\n"
"If IFC class is provided in neighbour field, will purge only elements of the provided class. Otherwise will purge all white-listed elements.\n"
"ALT+CLICK to provide a path where to save the IFC file with the removed elements (note changes will be applied to the current IFC too)"
)
bl_options = {"REGISTER", "UNDO"}
@@ -543,7 +548,7 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
return True
def _execute(self, context):
props = context.scene.BIMDebugProperties
props = tool.Debug.get_debug_props()
if props.ifc_class_purge:
purged_elements = core.purge_unused_elements(tool.Ifc, tool.Debug, props.ifc_class_purge)
self.report({"INFO"}, f"{purged_elements} unused elements found and removed.")
@@ -609,18 +614,30 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
"IfcUnitAssignment",
"IfcVirtualGridIntersection",
]
whitelist_exceptions = {
# Document references for sheet elements (drawings, schedules, etc).
"IfcExternalReference": ("IfcDocumentReference",),
}
total_purged = 0
schema = tool.Ifc.schema()
while True:
total_batches = 0
print("*" * 100)
total_batch_purged = 0
for ifc_class in whitelisted_classes:
total_class_purged = 0
# Ensure class is present in the schema.
try:
elements = tool.Ifc.get().by_type(ifc_class)
except:
continue # Probably not in this schema?
schema.declaration_by_name(ifc_class)
except RuntimeError:
continue
elements = tool.Ifc.get().by_type(ifc_class)
if ifc_class in whitelist_exceptions:
elements = [e for e in elements if not any(e.is_a(c) for c in whitelist_exceptions[ifc_class])]
to_purge = set()
for element in elements:
try:
@@ -687,15 +704,17 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
if purged == 0:
return
scene = context.scene
if object_type == "PROFILE":
if scene.BIMProfileProperties.is_editing:
props = tool.Profile.get_profile_props()
if props.is_editing:
bpy.ops.bim.load_profiles()
elif object_type == "STYLE":
if scene.BIMStylesProperties.is_editing:
props = tool.Style.get_style_props()
if props.is_editing:
bpy.ops.bim.load_styles()
elif object_type == "MATERIAL":
if scene.BIMMaterialProperties.is_editing:
props = tool.Material.get_material_props()
if props.is_editing:
bpy.ops.bim.load_materials()
@@ -735,15 +754,17 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
if merged == 0:
return
scene = context.scene
if object_type == "PROFILE":
if scene.BIMProfileProperties.is_editing:
props = tool.Profile.get_profile_props()
if props.is_editing:
bpy.ops.bim.load_profiles()
elif object_type == "STYLE":
if scene.BIMStylesProperties.is_editing:
props = tool.Style.get_style_props()
if props.is_editing:
bpy.ops.bim.load_styles()
elif object_type == "MATERIAL":
if scene.BIMMaterialProperties.is_editing:
props = tool.Material.get_material_props()
if props.is_editing:
bpy.ops.bim.load_materials()
@@ -799,7 +820,7 @@ class DebugActiveDrawing(bpy.types.Operator):
)
def execute(self, context: bpy.types.Context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
drawing_item = props.drawings[props.active_drawing_index]
drawing = tool.Ifc.get().by_id(drawing_item.ifc_definition_id)
@@ -934,6 +955,11 @@ class RestartBlender(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
# Save preferences manually since we're restarting Blender using .execv
# and it doens't have a chance to save them on exit.
if context.preferences.use_preferences_save:
bpy.ops.wm.save_userpref()
ms_store_app_id = tool.Blender.get_microsoft_store_app_id()
if not ms_store_app_id:
path = bpy.app.binary_path
+19 -6
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
from bonsai.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup
from bpy.props import (
@@ -28,6 +29,9 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal, get_args
DisplayType = Literal["BOUNDS", "WIRE", "SOLID", "TEXTURED"]
class BIMDebugProperties(PropertyGroup):
@@ -41,14 +45,23 @@ class BIMDebugProperties(PropertyGroup):
inverse_references: CollectionProperty(name="Inverse References", type=Attribute)
express_file: StringProperty(name="Express File")
display_type: EnumProperty(
items=[
("BOUNDS", "Bounds", ""),
("WIRE", "Wire", ""),
("SOLID", "Solid", ""),
("TEXTURED", "Textured", ""),
],
items=[(display_type, display_type.capitalize(), "") for display_type in get_args(DisplayType)],
name="Display Type",
default="BOUNDS",
)
ifc_class_purge: StringProperty(name="Unused Elements IFC Class", default="")
package_name: StringProperty(name="Package Name", default="")
if TYPE_CHECKING:
step_id: int
number_of_polygons: int
percentile_of_polygons: int
active_step_id: int
step_id_breadcrumb: bpy.types.bpy_prop_collection_idprop[StrProperty]
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
inverse_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
inverse_references: bpy.types.bpy_prop_collection_idprop[Attribute]
express_file: str
display_type: str
ifc_class_purge: str
package_name: str
+11 -11
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 bpy.types import Panel
@@ -32,10 +33,11 @@ class BIM_PT_debug(Panel):
def draw(self, context):
layout = self.layout
props = context.scene.BIMDebugProperties
props = tool.Debug.get_debug_props()
bim_props = tool.Blender.get_bim_props()
row = self.layout.row(align=True)
row.prop(context.scene.BIMProperties, "ifc_file", text="")
row.prop(bim_props, "ifc_file", text="")
row.operator("bim.validate_ifc_file", icon="CHECKMARK", text="")
row.operator("bim.select_ifc_file", icon="FILE_FOLDER", text="")
@@ -87,27 +89,25 @@ class BIM_PT_debug(Panel):
row.prop(props, "step_id", text="")
row = layout.split(factor=0.7, align=True)
row.operator("bim.select_high_polygon_meshes").threshold = context.scene.BIMDebugProperties.number_of_polygons
row.operator("bim.select_high_polygon_meshes").threshold = props.number_of_polygons
row.prop(props, "number_of_polygons", text="")
row = layout.split(factor=0.7, align=True)
row.operator("bim.select_highest_polygon_meshes").percentile = (
context.scene.BIMDebugProperties.percentile_of_polygons
)
row.operator("bim.select_highest_polygon_meshes").percentile = props.percentile_of_polygons
row.prop(props, "percentile_of_polygons", text="")
row = layout.split(factor=0.5, align=True)
row.prop(props, "display_type", text="")
row.operator("bim.override_display_type").display = context.scene.BIMDebugProperties.display_type
row.operator("bim.override_display_type").display = props.display_type
layout.operator("bim.purge_unused_representations")
row = layout.row(align=True)
row.prop(context.scene.BIMDebugProperties, "ifc_class_purge", text="")
row.prop(props, "ifc_class_purge", text="")
row.operator("bim.purge_unused_elements_by_class", text="Purge Orphaned", icon="TRASH")
row.operator("bim.print_unused_elements_stats", text="", icon="INFO")
if context.active_object and context.active_object.data:
mprops = context.active_object.data.BIMMeshProperties
if context.active_object and (data := context.active_object.data):
mprops = tool.Geometry.get_mesh_props(data)
row = layout.row()
row.operator("bim.get_representation_ifc_parameters")
for index, ifc_parameter in enumerate(mprops.ifc_parameters):
@@ -123,7 +123,7 @@ class BIM_PT_debug(Panel):
row.operator("bim.rewind_inspector", icon="FRAME_PREV", text="")
row.prop(props, "active_step_id", text="")
row = layout.row(align=True)
row.operator("bim.inspect_from_step_id").step_id = context.scene.BIMDebugProperties.active_step_id
row.operator("bim.inspect_from_step_id").step_id = props.active_step_id
row.operator("bim.inspect_from_object")
if props.attributes:
+10 -8
View File
@@ -65,13 +65,14 @@ class VisualiseDiff(bpy.types.Operator):
obj.color = (0.0, 0.0, 0.7, 1.0)
continue
if not obj.BIMObjectProperties.ifc_definition_id:
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
continue
ifc_file = ""
for scene in obj.users_scene:
if scene.BIMProperties.ifc_file:
ifc_file = scene.BIMProperties.ifc_file
bim_props = tool.Blender.get_bim_props(scene)
if bim_props.ifc_file:
ifc_file = bim_props.ifc_file
if scene.library:
break
@@ -83,7 +84,7 @@ class VisualiseDiff(bpy.types.Operator):
element_file = ifc_file
try:
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
element = element_file.by_id(ifc_id)
except:
continue
global_id = getattr(element, "GlobalId", None)
@@ -252,13 +253,14 @@ class SelectDiffObjects(bpy.types.Operator):
obj.select_set(True)
continue
if not obj.BIMObjectProperties.ifc_definition_id:
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
continue
ifc_file = ""
for scene in obj.users_scene:
if scene.BIMProperties.ifc_file:
ifc_file = scene.BIMProperties.ifc_file
bim_props = tool.Blender.get_bim_props(scene)
if bim_props.ifc_file:
ifc_file = bim_props.ifc_file
if scene.library:
break
@@ -270,7 +272,7 @@ class SelectDiffObjects(bpy.types.Operator):
element_file = ifc_file
try:
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
element = element_file.by_id(ifc_id)
except:
continue
global_id = getattr(element, "GlobalId", None)
-1
View File
@@ -17,7 +17,6 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from bpy.types import Panel
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.diff.data import DiffData
import bonsai.bim.helper
import bonsai.tool as tool
@@ -52,7 +52,7 @@ class DocumentData:
@classmethod
def parent_document(cls):
props = bpy.context.scene.BIMDocumentProperties
props = tool.Document.get_document_props()
if len(props.breadcrumbs):
parent = tool.Ifc.get().by_id(int(props.breadcrumbs[-1].name))
if tool.Ifc.get_schema() == "IFC2X3":
@@ -24,7 +24,6 @@ import ifcopenshell.util.element
import bonsai.bim.handler
import bonsai.tool as tool
import bonsai.core.document as core
from bonsai.bim.ifc import IfcStore
class LoadProjectDocuments(bpy.types.Operator):
@@ -116,7 +115,7 @@ class EditDocument(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.scene.BIMDocumentProperties
props = tool.Document.get_document_props()
core.edit_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(props.active_document_id))
@@ -30,6 +30,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING
def update_document_name(self: "Document", context: bpy.types.Context) -> None:
@@ -57,6 +58,11 @@ class Document(PropertyGroup):
)
ifc_definition_id: IntProperty(name="IFC Definition ID")
if TYPE_CHECKING:
identification: str
is_information: bool
ifc_definition_id: int
class BIMDocumentProperties(PropertyGroup):
document_attributes: CollectionProperty(name="Document Attributes", type=Attribute)
@@ -65,3 +71,11 @@ class BIMDocumentProperties(PropertyGroup):
breadcrumbs: CollectionProperty(name="Breadcrumbs", type=StrProperty)
active_document_index: IntProperty(name="Active Document Index")
is_editing: BoolProperty(name="Is Editing", default=False)
if TYPE_CHECKING:
document_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
active_document_id: int
documents: bpy.types.bpy_prop_collection_idprop[Document]
breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
active_document_index: int
is_editing: bool
+11 -9
View File
@@ -16,8 +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/>.
import bonsai.tool as tool
from bpy.types import Panel, UIList
from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import draw_attributes
from bonsai.bim.module.document.data import DocumentData, ObjectDocumentData
@@ -33,13 +33,13 @@ class BIM_PT_documents(Panel):
@classmethod
def poll(cls, context):
return IfcStore.get_file()
return tool.Ifc.get()
def draw(self, context):
if not DocumentData.is_loaded:
DocumentData.load()
self.props = context.scene.BIMDocumentProperties
self.props = tool.Document.get_document_props()
row = self.layout.row(align=True)
row.label(text="{} Documents Found".format(DocumentData.data["total_information"]), icon="FILE")
@@ -90,20 +90,22 @@ class BIM_PT_object_documents(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
return False
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
if not tool.Ifc.get_object_by_identifier(ifc_id):
return False
return True
def draw(self, context):
if not ObjectDocumentData.is_loaded:
ObjectDocumentData.load()
obj = context.active_object
self.oprops = obj.BIMObjectProperties
self.props = context.scene.BIMDocumentProperties
self.file = IfcStore.get_file()
self.oprops = tool.Blender.get_object_bim_props(obj)
self.props = tool.Document.get_document_props()
self.file = tool.Ifc.get()
self.draw_add_ui()
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import os
import bpy
import math
@@ -37,7 +38,7 @@ class Annotator:
if related_element is None:
location, _, _, _ = Annotator.get_placeholder_coords(bpy.context)
else:
obj.data.BIMAssignedProductProperties.related_element = related_element
curve.BIMAssignedProductProperties.related_element = related_element
location = related_element.location
obj.location = location
obj.hide_render = True
@@ -47,9 +48,10 @@ class Annotator:
tool.Blender.get_data_dir_path(Path("fonts") / "OpenGost Type B TT.ttf").__str__()
)
font.name = "OpenGost Type B TT"
obj.data.font = font
obj.data.BIMTextProperties.font_size = "2.5"
collection = bpy.context.scene.camera.BIMObjectProperties.collection
curve.font = font
props = tool.Drawing.get_text_props(obj)
props.font_size = "2.5"
collection = tool.Blender.get_object_bim_props(bpy.context.scene.camera).collection
collection.objects.link(obj)
Annotator.resize_text(obj)
return obj
@@ -66,7 +68,8 @@ class Annotator:
return
# This is a magic number for OpenGost
font_size = 1.6 / 1000
font_size *= float(text_obj.data.BIMTextProperties.font_size)
props = tool.Drawing.get_text_props(text_obj)
font_size *= float(props.font_size)
font_size /= tool.Drawing.get_scale_ratio(tool.Drawing.get_diagram_scale(camera)["Scale"])
@@ -123,12 +126,14 @@ class Annotator:
return obj
@staticmethod
def get_annotation_obj(drawing: ifcopenshell.entity_instance, object_type: str, data_type: str) -> bpy.types.Object:
def get_annotation_obj(
drawing: ifcopenshell.entity_instance, object_type: str, data_type: tool.Drawing.ANNOTATION_DATA_TYPE
) -> bpy.types.Object:
camera = tool.Ifc.get_object(drawing)
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
matrix_world = tool.Drawing.get_camera_matrix(camera)
matrix_world.translation = co1
collection = camera.BIMObjectProperties.collection
collection = tool.Blender.get_object_bim_props(camera).collection
if object_type == "TEXT":
obj = bpy.data.objects.new(object_type, None)
@@ -145,11 +150,17 @@ class Annotator:
collection.objects.link(obj)
return obj
if object_type != "ANGLE":
for obj in collection.objects:
element = tool.Ifc.get_entity(obj)
if element and element.ObjectType == object_type and obj.type == object_type.upper():
return obj
# TODO: remove as outdated?
# Is reusing the same objects preventing the creation of new annotations.
# if object_type != "ANGLE":
# for obj in collection.objects:
# element = tool.Ifc.get_entity(obj)
# if (
# element
# and ifcopenshell.util.element.get_predefined_type(element) == object_type
# and obj.type == data_type.upper()
# ):
# return obj
if data_type == "mesh":
data = bpy.data.meshes.new(object_type)
+15 -8
View File
@@ -88,7 +88,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:
titleblocks_dir = bpy.context.scene.DocProperties.titleblocks_dir
props = tool.Drawing.get_document_props()
titleblocks_dir = props.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")])
@@ -120,23 +121,25 @@ class DrawingsData:
@classmethod
def location_hint(cls):
if bpy.context.scene.DocProperties.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
props = tool.Drawing.get_document_props()
if props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
results = [("0", "Origin", "")]
results.extend(
[(str(s.id()), s.Name or "Unnamed", "") for s in tool.Ifc.get().by_type("IfcBuildingStorey")]
)
return results
elif bpy.context.scene.DocProperties.target_view in ["MODEL_VIEW"]:
elif props.target_view in ["MODEL_VIEW"]:
return [(h.upper(), h, "") for h in ["Orthographic", "Perspective"]]
return [(h.upper(), h, "") for h in ["North", "South", "East", "West"]]
@classmethod
def active_drawing_pset_data(cls):
ifc_file = tool.Ifc.get()
drawing_id = bpy.context.scene.DocProperties.active_drawing_id
props = tool.Drawing.get_document_props()
drawing_id = props.active_drawing_id
if drawing_id == 0:
return {}
drawing = ifc_file.by_id(bpy.context.scene.DocProperties.active_drawing_id)
drawing = ifc_file.by_id(drawing_id)
return ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing")
@@ -303,7 +306,7 @@ class DecoratorData:
if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
return None
props = obj.BIMTextProperties
props = tool.Drawing.get_text_props(obj)
# getting font size
pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {}
# use `regular` as default
@@ -361,7 +364,11 @@ class DecoratorData:
element = tool.Ifc.get_entity(obj)
supported_object_types = ("DIMENSION", "DIAMETER", "SECTION_LEVEL", "PLAN_LEVEL", "RADIUS")
if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in supported_object_types:
if (
not element
or not element.is_a("IfcAnnotation")
or ifcopenshell.util.element.get_predefined_type(element) not in supported_object_types
):
return None
dimension_style = "arrow"
@@ -399,7 +406,7 @@ class AnnotationData:
@classmethod
def load(cls):
cls.is_loaded = True
cls.props = bpy.context.scene.BIMAnnotationProperties
cls.props = tool.Drawing.get_annotation_props()
cls.data["relating_type_id"] = cls.relating_type_id()
cls.data["relating_types"] = cls.relating_types()
@@ -423,7 +423,8 @@ class BaseDecorator:
# font_size = 16 <-- this is a good default
# TODO: need to synchronize it better with svg
magic_font_scale = bpy.context.scene.DocProperties.magic_font_scale
props = tool.Drawing.get_document_props()
magic_font_scale = props.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]
@@ -589,7 +590,7 @@ class BaseDecorator:
text_dir = self.get_annotation_direction(context, obj)
pos = location_3d_to_region_2d(region, region3d, text_world_position)
props = obj.BIMTextProperties
props = tool.Drawing.get_text_props(obj)
text_data = DecoratorData.get_ifc_text_data(obj)
if props.is_editing:
text_data = text_data | props.get_text_edited_data()
@@ -976,6 +977,7 @@ class FallDecorator(BaseDecorator):
# same function as in svgwriter.py
def get_label_text():
element = tool.Ifc.get_entity(obj)
assert element
B, A = [v.co.xyz for v in spline_points[:2]]
rise = abs(A.z - B.z)
O = A.copy()
@@ -988,13 +990,14 @@ class FallDecorator(BaseDecorator):
angle = 90
# ues SLOPE_ANGLE as default
if element.ObjectType in ("FALL", "SLOPE_ANGLE"):
object_type = ifcopenshell.util.element.get_predefined_type(element)
if object_type in ("FALL", "SLOPE_ANGLE"):
return f"{angle}°"
elif element.ObjectType == "SLOPE_FRACTION":
elif object_type == "SLOPE_FRACTION":
if angle == 90:
return "-"
return f"{self.format_value(context, rise)} / {self.format_value(context, run)}"
elif element.ObjectType == "SLOPE_PERCENT":
elif object_type == "SLOPE_PERCENT":
if angle == 90:
return "-"
return f"{round(angle_tg * 100)} %"
@@ -2022,7 +2025,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"]
if drawing_font := bpy.context.scene.DocProperties.drawing_font:
props = tool.Drawing.get_document_props()
if drawing_font := props.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__())
@@ -2045,7 +2049,7 @@ class DecorationsHandler:
if not element.is_a("IfcAnnotation"):
continue
object_type: Union[str, None] = element.ObjectType
object_type: Union[str, None] = ifcopenshell.util.element.get_predefined_type(element)
if object_type == "DRAWING":
continue
+23 -15
View File
@@ -19,6 +19,7 @@
import bpy
import blf
import gpu
import bonsai.tool as tool
from bpy import types
from mathutils import Vector
from mathutils import geometry
@@ -478,21 +479,25 @@ class ExtrusionWidget(types.GizmoGroup):
bl_options = {"3D", "PERSISTENT", "SHOW_MODAL_ALL"}
@classmethod
def poll(cls, ctx):
obj = ctx.object
def poll(cls, context):
obj = context.active_object
return (
obj
and obj.type == "MESH"
and obj.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth") is not None
and (data := obj.data)
and isinstance(data, bpy.types.Mesh)
and tool.Geometry.get_mesh_props(data).ifc_parameters.get("IfcExtrudedAreaSolid/Depth") is not None
)
def setup(self, ctx):
target = ctx.object
prop = target.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth")
def setup(self, context: bpy.types.Context) -> None:
target = context.object
assert target
mesh = target.data
assert isinstance(mesh, bpy.types.Mesh)
prop = tool.Geometry.get_mesh_props(mesh).ifc_parameters.get("IfcExtrudedAreaSolid/Depth")
basis = target.matrix_world.normalized()
theme = ctx.preferences.themes[0].user_interface
scale_value = self.get_scale_value(ctx.scene.unit_settings.system, ctx.scene.unit_settings.length_unit)
theme = context.preferences.themes[0].user_interface
scale_value = self.get_scale_value(context.scene.unit_settings.system, context.scene.unit_settings.length_unit)
# setup handle
gz = self.handle = self.gizmos.new("BIM_GT_uglydot_3d")
@@ -521,23 +526,26 @@ class ExtrusionWidget(types.GizmoGroup):
# gz.use_draw_modal = True
# gz.target_set_prop('value', target.demo, 'depth')
def refresh(self, ctx):
def refresh(self, context: bpy.types.Context) -> None:
"""updating gizmos"""
target = ctx.object
target = context.active_object
basis = target.matrix_world.normalized()
self.handle.matrix_basis = basis
self.guides.matrix_basis = basis
def update(self, ctx):
def update(self, context: bpy.types.Context) -> None:
"""updating object"""
bpy.ops.bim.update_parametric_representation()
target = ctx.object
prop = target.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth")
target = context.active_object
assert target
mesh = target.data
assert isinstance(mesh, bpy.types.Mesh)
prop = tool.Geometry.get_mesh_props(mesh).ifc_parameters.get("IfcExtrudedAreaSolid/Depth")
self.handle.target_set_prop("offset", prop, "value")
self.guides.target_set_prop("depth", prop, "value")
@staticmethod
def get_scale_value(system, length_unit):
def get_scale_value(system: str, length_unit: str) -> float:
scale_value = 1
if system == "METRIC":
if length_unit == "KILOMETERS":
@@ -24,7 +24,8 @@ from bpy.app.handlers import persistent
@persistent
def load_post(*args):
if bpy.context.scene.DocProperties.should_draw_decorations:
props = tool.Drawing.get_document_props()
if props.should_draw_decorations:
decoration.DecorationsHandler.install(bpy.context)
else:
decoration.DecorationsHandler.uninstall()
@@ -35,9 +36,11 @@ def depsgraph_update_pre_handler(scene):
set_active_camera_resolution(scene)
def set_active_camera_resolution(scene):
if not scene.camera or "/" not in scene.camera.name or not scene.DocProperties.drawings:
def set_active_camera_resolution(scene: bpy.types.Scene) -> None:
props = tool.Drawing.get_document_props()
if not scene.camera or "/" not in scene.camera.name or not props.drawings:
return
assert isinstance(scene.camera.data, bpy.types.Camera)
props = scene.camera.data.BIMCameraProperties
ortho_scale = max((props.width, props.height))
aspect_ratio = props.width / props.height
@@ -60,5 +63,3 @@ def set_active_camera_resolution(scene):
scene.render.resolution_x = scene.camera.data.BIMCameraProperties.raster_x = int(raster_x)
scene.render.resolution_y = scene.camera.data.BIMCameraProperties.raster_y = int(raster_y)
current_drawing = scene.DocProperties.drawings[scene.DocProperties.current_drawing_index]
+51 -6
View File
@@ -19,8 +19,10 @@
import bpy
import math
import mathutils.geometry
import ifcopenshell
import bonsai.tool as tool
from mathutils import Vector
from typing import Union
# Code taken and updated from https://blenderartists.org/t/detecting-intersection-of-bounding-boxes/457520/2
@@ -135,6 +137,10 @@ def format_distance(
scaleFactor = bpy.context.scene.unit_settings.scale_length
unit_system = bpy.context.scene.unit_settings.system
unit_length = bpy.context.scene.unit_settings.length_unit
if area_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT"):
area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol(area_unit)
else:
area_unit_symbol = ""
value *= scaleFactor
@@ -225,7 +231,24 @@ def format_distance(
if add_inches or frac:
tx_dist += '"'
else:
tx_dist = str("%1.3f" % (value * toInches / inPerFoot)) + " sq. ft."
fmt = "%1.3f"
sq_feet = round(value * toInches / inPerFoot, 4)
tx_dist = ""
if area_unit_symbol == " ft2":
fmt += area_unit_symbol
tx_dist = fmt % sq_feet
if area_unit_symbol == " in2":
sq_inch = sq_feet * 144
fmt += area_unit_symbol
tx_dist = fmt % sq_inch
if area_unit_symbol == " yd2":
sq_yard = sq_feet / 9
fmt += area_unit_symbol
tx_dist = fmt % sq_yard
if area_unit_symbol == " mi2":
sq_mile = sq_feet / 27878400
fmt += area_unit_symbol
tx_dist = fmt % sq_mile
# METRIC FORMATTING
elif unit_system == "METRIC":
@@ -287,19 +310,41 @@ def format_distance(
d_mm = value * (1000)
tx_dist = fmt % d_mm
if isArea:
tx_dist += s_code
if area_unit_symbol == " m2":
if decimal_places is None:
fmt = "%1.3f"
if hide_units is False:
fmt += area_unit_symbol
tx_dist = fmt % value
if area_unit_symbol == " cm2":
if decimal_places is None:
fmt = "%1.1f"
if hide_units is False:
fmt += area_unit_symbol
d_cm = value * (10000)
tx_dist = fmt % d_cm
if area_unit_symbol == " mm2":
if decimal_places is None:
fmt = "%1.0f"
if hide_units is False:
fmt += area_unit_symbol
d_cm = value * (1000000)
tx_dist = fmt % d_cm
else:
tx_dist = fmt % value
return tx_dist
def get_active_drawing(scene):
def get_active_drawing(
scene: bpy.types.Scene,
) -> Union[tuple[bpy.types.Collection, bpy.types.Camera], tuple[None, None]]:
"""Get active drawing collection and camera"""
props = scene.DocProperties
props = tool.Drawing.get_document_props()
try:
camera = tool.Ifc.get_object(tool.Ifc.get().by_id(props.active_drawing_id))
return camera.BIMObjectProperties.collection, camera
return tool.Blender.get_object_bim_props(camera).collection, camera
except:
return None, None
@@ -416,7 +461,7 @@ def elevate_segment(bounds, segm):
return [Vector((x, ymin, zmin)), Vector((x, ymax, zmin))]
def add_newline_between_words(text, newline_at):
def add_newline_between_words(text: str, newline_at: int) -> str:
result = []
start = 0
+121 -88
View File
@@ -84,7 +84,7 @@ class AddAnnotationType(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.scene.BIMAnnotationProperties
props = tool.Drawing.get_annotation_props()
object_type = props.object_type
has_representation = props.create_representation_for_type
drawing = tool.Ifc.get_entity(bpy.context.scene.camera)
@@ -115,7 +115,7 @@ class EnableAddAnnotationType(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
bpy.context.scene.BIMAnnotationProperties.is_adding_type = True
tool.Drawing.get_annotation_props().is_adding_type = True
class DisableAddAnnotationType(bpy.types.Operator, tool.Ifc.Operator):
@@ -124,7 +124,7 @@ class DisableAddAnnotationType(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
bpy.context.scene.BIMAnnotationProperties.is_adding_type = False
tool.Drawing.get_annotation_props().is_adding_type = False
class AddDrawing(bpy.types.Operator, tool.Ifc.Operator):
@@ -134,19 +134,19 @@ class AddDrawing(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Add a drawing view to the IFC project"
def _execute(self, context):
self.props = context.scene.DocProperties
hint = self.props.location_hint
if self.props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
props = tool.Drawing.get_document_props()
hint = props.location_hint
if props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
hint = int(hint)
core.add_drawing(
tool.Ifc,
tool.Collector,
tool.Drawing,
target_view=self.props.target_view,
target_view=props.target_view,
location_hint=hint,
)
try:
drawing = tool.Ifc.get().by_id(self.props.active_drawing_id)
drawing = tool.Ifc.get().by_id(props.active_drawing_id)
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
except:
pass
@@ -162,7 +162,7 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -176,7 +176,7 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
row.prop(self, "should_duplicate_annotations")
def _execute(self, context):
self.props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
core.duplicate_drawing(
tool.Ifc,
tool.Drawing,
@@ -184,7 +184,7 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
should_duplicate_annotations=self.should_duplicate_annotations,
)
try:
drawing = tool.Ifc.get().by_id(self.props.active_drawing_id)
drawing = tool.Ifc.get().by_id(props.active_drawing_id)
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
except:
pass
@@ -244,9 +244,9 @@ class CreateDrawing(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
active_drawing_id = context.scene.camera.BIMObjectProperties.ifc_definition_id
active_drawing_id = tool.Blender.get_ifc_definition_id(context.scene.camera)
if self.print_all:
original_drawing_id = active_drawing_id
drawings_to_print = [d.ifc_definition_id for d in self.props.drawings if d.is_selected and d.is_drawing]
@@ -261,7 +261,7 @@ class CreateDrawing(bpy.types.Operator):
self.camera = context.scene.camera
self.camera_element = tool.Ifc.get_entity(self.camera)
self.camera_document = tool.Drawing.get_drawing_document(self.camera_element)
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
with profile("Drawing generation process"):
with profile("Initialize drawing generation process"):
@@ -385,13 +385,13 @@ class CreateDrawing(bpy.types.Operator):
obj.hide_render = obj.name not in visible_object_names
context.scene.render.filepath = str(Path(svg_path).with_suffix(".png"))
drawing_style = context.scene.DocProperties.drawing_styles[self.cprops.active_drawing_style_index]
drawing_style = self.props.drawing_styles[self.cprops.active_drawing_style_index]
if drawing_style.render_type == "DEFAULT":
bpy.ops.render.render(write_still=True)
else:
previous_visibility = {}
for obj in self.camera.BIMObjectProperties.collection.objects:
for obj in tool.Blender.get_object_bim_props(self.camera).collection.objects:
if bpy.context.view_layer.objects.get(obj.name):
previous_visibility[obj.name] = obj.hide_get()
obj.hide_set(True)
@@ -712,9 +712,11 @@ class CreateDrawing(bpy.types.Operator):
}
cached_linework -= edited_guids
files = {context.scene.BIMProperties.ifc_file: tool.Ifc.get()}
bim_props = tool.Blender.get_bim_props()
files = {bim_props.ifc_file: tool.Ifc.get()}
for link in context.scene.BIMProjectProperties.links:
props = tool.Project.get_project_props()
for link in props.links:
if link.name not in IfcStore.session_files:
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
files[link.name] = IfcStore.session_files[link.name]
@@ -729,7 +731,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(context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5")
ifc_cache_path = os.path.join(bim_props.cache_dir, f"{ifc_hash}.h5")
self.serialiser.setFile(ifc)
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element, ifc_file=ifc)
@@ -1141,7 +1143,8 @@ class CreateDrawing(bpy.types.Operator):
try:
return tool.Ifc.get().by_guid(guid)
except:
for link in bpy.context.scene.BIMProjectProperties.links:
props = tool.Project.get_project_props()
for link in props.links:
if link.name not in IfcStore.session_files:
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
try:
@@ -1367,7 +1370,11 @@ class CreateDrawing(bpy.types.Operator):
elements = list(elements | filtered_drawing_annotations)
annotations = sorted(
elements, key=lambda a: (tool.Drawing.get_annotation_z_index(a), 1 if a.ObjectType == "TEXT" else 0)
elements,
key=lambda a: (
tool.Drawing.get_annotation_z_index(a),
1 if ifcopenshell.util.element.get_predefined_type(a) == "TEXT" else 0,
),
)
precision = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "MetricPrecision")
@@ -1433,7 +1440,7 @@ class AddAnnotation(bpy.types.Operator, tool.Ifc.Operator):
return operator.description or ""
def _execute(self, context):
props = bpy.context.scene.BIMAnnotationProperties
props = tool.Drawing.get_annotation_props()
if not (drawing := tool.Ifc.get_entity(context.scene.camera)):
self.report({"WARNING"}, "Not a BIM camera")
return
@@ -1458,7 +1465,8 @@ class AddSheet(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Add a sheet to the project"
def _execute(self, context):
core.add_sheet(tool.Ifc, tool.Drawing, titleblock=context.scene.DocProperties.titleblock)
props = tool.Drawing.get_document_props()
core.add_sheet(tool.Ifc, tool.Drawing, titleblock=props.titleblock)
class DuplicateSheet(bpy.types.Operator, tool.Ifc.Operator):
@@ -1474,7 +1482,7 @@ class DuplicateSheet(bpy.types.Operator, tool.Ifc.Operator):
cls.poll_message_set("Not implemented yet.")
return False
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -1483,7 +1491,7 @@ class DuplicateSheet(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
pass
"""
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
core.duplicate_sheet(
tool.Ifc,
tool.Drawing,
@@ -1508,7 +1516,7 @@ class OpenLayout(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
self.props = context.scene.DocProperties
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_builder = sheeter.SheetBuilder()
sheet_builder.update_sheet_drawing_sizes(sheet)
@@ -1530,7 +1538,8 @@ class SelectAllSheets(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
for sheet in context.scene.DocProperties.sheets:
props = tool.Drawing.get_document_props()
for sheet in props.sheets:
if sheet.is_selected != self.select_all:
sheet.is_selected = self.select_all
return {"FINISHED"}
@@ -1550,7 +1559,7 @@ class OpenSheet(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_sheet_item(is_sheet=True):
cls.poll_message_set("No sheet selected.")
return False
@@ -1564,7 +1573,7 @@ class OpenSheet(bpy.types.Operator, tool.Ifc.Operator):
return self.execute(context)
def execute(self, context):
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
svg2pdf_command = tool.Blender.get_addon_preferences().svg2pdf_command
if self.open_all:
@@ -1612,9 +1621,10 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
# Won't be visible in UI anyway.
if not props.sheets or not context.scene.BIMProperties.data_dir:
bim_props = tool.Blender.get_bim_props()
if not props.sheets or not bim_props.data_dir:
return False
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
@@ -1622,8 +1632,8 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
return True
def _execute(self, context):
props = context.scene.DocProperties
active_drawing = tool.Drawing.get_active_drawing_item()
props = tool.Drawing.get_document_props()
active_drawing = props.drawings[props.active_drawing_index]
assert active_drawing
active_sheet = tool.Drawing.get_active_sheet(context)
@@ -1680,7 +1690,7 @@ class RemoveDrawingFromSheet(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
active_item = tool.Drawing.get_active_sheet_item()
if active_item is None:
return False
@@ -1714,11 +1724,12 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_sheet_item(is_sheet=True):
cls.poll_message_set("No sheet selected.")
return False
return props.sheets and context.scene.BIMProperties.data_dir
bim_props = tool.Blender.get_bim_props()
return props.sheets and bim_props.data_dir
def invoke(self, context, event):
# opening all sheets on shift+click
@@ -1731,7 +1742,7 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
scene = context.scene
props = scene.DocProperties
props = tool.Drawing.get_document_props()
svg2pdf_command = tool.Blender.get_addon_preferences().svg2pdf_command
svg2dxf_command = tool.Blender.get_addon_preferences().svg2dxf_command
@@ -1838,7 +1849,8 @@ class SelectAllDrawings(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
for drawing in context.scene.DocProperties.drawings:
props = tool.Drawing.get_document_props()
for drawing in props.drawings:
if drawing.is_selected != self.select_all:
drawing.is_selected = self.select_all
return {"FINISHED"}
@@ -1857,7 +1869,7 @@ class OpenDrawing(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -1871,7 +1883,7 @@ class OpenDrawing(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
if self.open_all:
drawings = [
tool.Ifc.get().by_id(d.ifc_definition_id) for d in self.props.drawings if d.is_drawing and d.is_selected
@@ -1907,7 +1919,7 @@ class ActivateModel(bpy.types.Operator):
bl_description = "Activate the model view, hide all annotations"
def execute(self, context):
dprops = bpy.context.scene.DocProperties
dprops = tool.Drawing.get_document_props()
dprops.active_drawing_id = 0
CutDecorator.uninstall()
@@ -1971,11 +1983,12 @@ class ActivateDrawingBase:
return self.execute(context)
def execute(self, context):
if bpy.context.scene.DocProperties.is_editing_drawings == False:
props = tool.Drawing.get_document_props()
if props.is_editing_drawings == False:
bpy.ops.bim.load_drawings()
drawing = tool.Ifc.get().by_id(self.drawing)
dprops = bpy.context.scene.DocProperties
dprops = tool.Drawing.get_document_props()
if self.use_quick_preview:
tool.Blender.activate_camera(tool.Drawing.import_temporary_drawing_camera(drawing))
@@ -2027,7 +2040,7 @@ class ActivateDrawing(bpy.types.Operator, ActivateDrawingBase):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -2050,7 +2063,7 @@ class ActivateDrawingFromSheet(bpy.types.Operator, ActivateDrawingBase):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_sheet_item(reference_type="DRAWING"):
cls.poll_message_set("No drawing selected.")
return False
@@ -2066,7 +2079,8 @@ class SelectDocIfcFile(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
context.scene.DocProperties.ifc_files[self.index].name = self.filepath
props = tool.Drawing.get_document_props()
props.ifc_files[self.index].name = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
@@ -2081,7 +2095,7 @@ class ResizeText(bpy.types.Operator):
# TODO: check undo redo
def execute(self, context):
for obj in context.scene.camera.BIMObjectProperties.collection.objects:
for obj in tool.Blender.get_object_bim_props(context.scene.camera).collection.objects:
if isinstance(obj.data, bpy.types.TextCurve):
annotation.Annotator.resize_text(obj)
return {"FINISHED"}
@@ -2098,7 +2112,7 @@ class RemoveDrawing(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -2112,12 +2126,10 @@ class RemoveDrawing(bpy.types.Operator, tool.Ifc.Operator):
return self.execute(context)
def _execute(self, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if self.remove_all:
drawings = [
tool.Ifc.get().by_id(d.ifc_definition_id)
for d in context.scene.DocProperties.drawings
if d.is_drawing and d.is_selected
tool.Ifc.get().by_id(d.ifc_definition_id) for d in props.drawings if d.is_drawing and d.is_selected
]
else:
if not self.drawing:
@@ -2187,7 +2199,8 @@ class ReloadDrawingStyles(bpy.types.Operator):
with open(json_path, "r") as fi:
shading_styles_json = json.load(fi)
drawing_styles = context.scene.DocProperties.drawing_styles
props = tool.Drawing.get_document_props()
drawing_styles = props.drawing_styles
drawing_styles.clear()
styles = [style for style in shading_styles_json]
for style_name in styles:
@@ -2212,7 +2225,8 @@ class AddDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
drawing_styles = context.scene.DocProperties.drawing_styles
props = tool.Drawing.get_document_props()
drawing_styles = props.drawing_styles
new = drawing_styles.add()
# drawing style is saved to ifc on rename
new.name = tool.Blender.ensure_unique_name("New Drawing Style", drawing_styles)
@@ -2227,7 +2241,8 @@ class RemoveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
context.scene.DocProperties.drawing_styles.remove(self.index)
props = tool.Drawing.get_document_props()
props.drawing_styles.remove(self.index)
context.scene.camera.data.BIMCameraProperties.active_drawing_style_index = max(self.index - 1, 0)
bpy.ops.bim.save_drawing_styles_data()
return {"FINISHED"}
@@ -2283,7 +2298,8 @@ class SaveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
index = int(self.index)
else:
index = context.scene.camera.data.BIMCameraProperties.active_drawing_style_index
scene.DocProperties.drawing_styles[index].raster_style = json.dumps(style)
props = tool.Drawing.get_document_props()
props.drawing_styles[index].raster_style = json.dumps(style)
bpy.ops.bim.save_drawing_styles_data()
return {"FINISHED"}
@@ -2309,7 +2325,8 @@ class SaveDrawingStylesData(bpy.types.Operator, tool.Ifc.Operator):
if not DrawingsData.is_loaded:
DrawingsData.load()
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
drawing_styles = context.scene.DocProperties.drawing_styles
props = tool.Drawing.get_document_props()
drawing_styles = props.drawing_styles
rel_path = drawing_pset_data["ShadingStyles"]
current_style = drawing_pset_data.get("CurrentShadingStyle", None)
@@ -2338,7 +2355,7 @@ class SaveDrawingStylesData(bpy.types.Operator, tool.Ifc.Operator):
new_style_name = None
ifc_file = tool.Ifc.get()
drawing = ifc_file.by_id(context.scene.DocProperties.active_drawing_id)
drawing = ifc_file.by_id(props.active_drawing_id)
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
ifcopenshell.api.run(
"pset.edit_pset", ifc_file, pset=pset, properties={"CurrentShadingStyle": new_style_name}
@@ -2357,17 +2374,18 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
scene = context.scene
ifc_file = tool.Ifc.get()
active_drawing_style_index = scene.camera.data.BIMCameraProperties.active_drawing_style_index
props = tool.Drawing.get_document_props()
if active_drawing_style_index >= len(scene.DocProperties.drawing_styles):
if active_drawing_style_index >= len(props.drawing_styles):
self.report({"ERROR"}, "Could not find active drawing style")
return {"CANCELLED"}
self.drawing_style = scene.DocProperties.drawing_styles[active_drawing_style_index]
self.drawing_style = props.drawing_styles[active_drawing_style_index]
self.set_raster_style(context)
self.set_query(context)
drawing = ifc_file.by_id(scene.DocProperties.active_drawing_id)
drawing = ifc_file.by_id(props.active_drawing_id)
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
ifcopenshell.api.run(
"pset.edit_pset", ifc_file, pset=pset, properties={"CurrentShadingStyle": self.drawing_style.name}
@@ -2392,7 +2410,8 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
def set_query(self, context: bpy.types.Context) -> None:
self.include_global_ids = []
self.exclude_global_ids = []
for ifc_file in context.scene.DocProperties.ifc_files:
props = tool.Drawing.get_document_props()
for ifc_file in props.ifc_files:
try:
ifc = ifcopenshell.open(ifc_file.name)
except:
@@ -2507,14 +2526,15 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not props.schedules:
cls.poll_message_set("No schedule selected.")
return False
return props.schedules and props.sheets and context.scene.BIMProperties.data_dir
bim_props = tool.Blender.get_bim_props()
return props.schedules and props.sheets and bim_props.data_dir
def _execute(self, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
active_schedule = props.schedules[props.active_schedule_index]
active_sheet = tool.Drawing.get_active_sheet(context)
schedule = tool.Ifc.get().by_id(active_schedule.ifc_definition_id)
@@ -2573,14 +2593,15 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not props.references:
cls.poll_message_set("No reference selected.")
return False
return props.references and props.sheets and context.scene.BIMProperties.data_dir
bim_props = tool.Blender.get_bim_props()
return props.references and props.sheets and bim_props.data_dir
def _execute(self, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
active_reference = props.references[props.active_reference_index]
active_sheet = tool.Drawing.get_active_sheet(context)
extref = tool.Ifc.get().by_id(active_reference.ifc_definition_id)
@@ -2682,7 +2703,8 @@ class AddDrawingStyleAttribute(bpy.types.Operator):
def execute(self, context):
props = context.scene.camera.data.BIMCameraProperties
context.scene.DocProperties.drawing_styles[props.active_drawing_style_index].attributes.add()
dprops = tool.Drawing.get_document_props()
dprops.drawing_styles[props.active_drawing_style_index].attributes.add()
return {"FINISHED"}
@@ -2695,7 +2717,8 @@ class RemoveDrawingStyleAttribute(bpy.types.Operator):
def execute(self, context):
props = context.scene.camera.data.BIMCameraProperties
context.scene.DocProperties.drawing_styles[props.active_drawing_style_index].attributes.remove(self.index)
dprops = tool.Drawing.get_document_props()
dprops.drawing_styles[props.active_drawing_style_index].attributes.remove(self.index)
return {"FINISHED"}
@@ -2725,7 +2748,9 @@ class EditTextPopup(bpy.types.Operator):
# need to keep them in sync or move to some common function
# NOTE: that `popup_active_attribute` is used here when it's not used in `BIM_PT_text.draw()`
props = context.active_object.BIMTextProperties
obj = context.active_object
assert obj
props = tool.Drawing.get_text_props(obj)
row = self.layout.row(align=True)
row.operator("bim.add_text_literal", icon="ADD", text="Add Literal")
@@ -2826,9 +2851,11 @@ class AddTextLiteral(bpy.types.Operator):
def execute(self, context):
obj = context.active_object
assert obj
# similar to `tool.Drawing.import_text_attributes`
literal_props = obj.BIMTextProperties.literals.add()
props = tool.Drawing.get_text_props(obj)
literal_props = props.literals.add()
literal_attributes = literal_props.attributes
literal_attr_values = {
"Literal": "Literal",
@@ -2865,7 +2892,9 @@ class RemoveTextLiteral(bpy.types.Operator):
def execute(self, context):
obj = context.active_object
obj.BIMTextProperties.literals.remove(self.literal_prop_id)
assert obj
props = tool.Drawing.get_text_props(obj)
props.literals.remove(self.literal_prop_id)
return {"FINISHED"}
@@ -2880,7 +2909,9 @@ class OrderTextLiteralUp(bpy.types.Operator):
def execute(self, context):
obj = context.active_object
obj.BIMTextProperties.literals.move(self.literal_prop_id, self.literal_prop_id - 1)
assert obj
props = tool.Drawing.get_text_props(obj)
props.literals.move(self.literal_prop_id, self.literal_prop_id - 1)
return {"FINISHED"}
@@ -2895,7 +2926,9 @@ class OrderTextLiteralDown(bpy.types.Operator):
def execute(self, context):
obj = context.active_object
obj.BIMTextProperties.literals.move(self.literal_prop_id, self.literal_prop_id + 1)
assert obj
props = tool.Drawing.get_text_props(obj)
props.literals.move(self.literal_prop_id, self.literal_prop_id + 1)
return {"FINISHED"}
@@ -2969,7 +3002,7 @@ class LoadSheets(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
core.load_sheets(tool.Drawing)
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
sheets_not_found = []
for sheet_prop in props.sheets:
if not sheet_prop.is_sheet:
@@ -2999,7 +3032,7 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
document_type: Literal["SHEET", "TITLEBLOCK", "EMBEDDED"]
def invoke(self, context, event):
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)
if sheet.is_a("IfcDocumentInformation"):
self.document_type = "SHEET"
@@ -3013,6 +3046,7 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
props = tool.Drawing.get_document_props()
if self.document_type == "SHEET":
row = self.layout.row()
row.prop(self, "identification", text="Identification")
@@ -3020,13 +3054,13 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
row.prop(self, "name", text="Name")
elif self.document_type == "TITLEBLOCK":
row = self.layout.row()
row.prop(context.scene.DocProperties, "titleblock", text="Titleblock")
row.prop(props, "titleblock", text="Titleblock")
elif self.document_type == "EMBEDDED":
row = self.layout.row()
row.prop(self, "identification", text="Identification")
def _execute(self, context):
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)
if self.document_type == "SHEET":
core.rename_sheet(tool.Ifc, tool.Drawing, sheet=sheet, identification=self.identification, name=self.name)
@@ -3124,7 +3158,7 @@ class ExpandTargetView(bpy.types.Operator):
target_view: bpy.props.StringProperty()
def execute(self, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
for drawing in [d for d in props.drawings if d.target_view == self.target_view]:
drawing.is_expanded = True
core.load_drawings(tool.Drawing)
@@ -3140,7 +3174,7 @@ class ContractTargetView(bpy.types.Operator):
target_view: bpy.props.StringProperty()
def execute(self, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
for drawing in [d for d in props.drawings if d.target_view == self.target_view]:
drawing.is_expanded = False
core.load_drawings(tool.Drawing)
@@ -3156,7 +3190,7 @@ class ExpandSheet(bpy.types.Operator):
sheet: bpy.props.IntProperty()
def execute(self, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
for sheet in [s for s in props.sheets if s.ifc_definition_id == self.sheet]:
sheet.is_expanded = True
core.load_sheets(tool.Drawing)
@@ -3172,7 +3206,7 @@ class ContractSheet(bpy.types.Operator):
sheet: bpy.props.IntProperty()
def execute(self, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
for sheet in [s for s in props.sheets if s.ifc_definition_id == self.sheet]:
sheet.is_expanded = False
core.load_sheets(tool.Drawing)
@@ -3363,7 +3397,7 @@ class ConvertSVGToDXF(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -3377,14 +3411,13 @@ class ConvertSVGToDXF(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
props = tool.Drawing.get_document_props()
if self.convert_all:
drawings = [
tool.Ifc.get().by_id(d.ifc_definition_id)
for d in context.scene.DocProperties.drawings
if d.is_drawing and d.is_selected
tool.Ifc.get().by_id(d.ifc_definition_id) for d in props.drawings if d.is_drawing and d.is_selected
]
else:
drawings = [tool.Ifc.get().by_id(context.scene.DocProperties.drawings.get(self.view).ifc_definition_id)]
drawings = [tool.Ifc.get().by_id(props.drawings.get(self.view).ifc_definition_id)]
drawing_uris: list[Path] = []
drawings_not_found: list[str] = []
+21 -8
View File
@@ -44,7 +44,7 @@ from bpy.props import (
CollectionProperty,
BoolVectorProperty,
)
from typing import TYPE_CHECKING, Literal
from typing import TYPE_CHECKING, Literal, Any
diagram_scales_enum = []
@@ -76,7 +76,7 @@ def update_diagram_scale(self, context):
try:
element = (
tool.Ifc.get()
.by_id(self.id_data.BIMMeshProperties.ifc_definition_id)
.by_id(tool.Geometry.get_mesh_props(self.id_data).ifc_definition_id)
.OfProductRepresentation[0]
.ShapeOfProduct[0]
)
@@ -93,7 +93,7 @@ def update_diagram_scale(self, context):
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties=diagram_scale)
def update_is_nts(self, context):
def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
if not self.update_props:
return
if not context.scene.camera or context.scene.camera.data != self.id_data:
@@ -104,7 +104,7 @@ def update_is_nts(self, context):
try:
element = (
tool.Ifc.get()
.by_id(self.id_data.BIMMeshProperties.ifc_definition_id)
.by_id(tool.Geometry.get_mesh_props(self.id_data).ifc_definition_id)
.OfProductRepresentation[0]
.ShapeOfProduct[0]
)
@@ -192,8 +192,8 @@ def get_drawing_style_name(self: "DrawingStyle"):
def set_drawing_style_name(self: "DrawingStyle", new_value: str) -> None:
"""ensure the name is unique"""
scene = bpy.context.scene
drawing_styles = [s.name for s in scene.DocProperties.drawing_styles if s.name != self.name]
props = tool.Drawing.get_document_props()
drawing_styles = [s.name for s in props.drawing_styles if s.name != self.name]
new_value = tool.Blender.ensure_unique_name(new_value, drawing_styles)
old_value = self.name
self["name"] = new_value
@@ -266,7 +266,7 @@ def update_titleblocks(self, context):
def update_should_draw_decorations(self, context: bpy.types.Context) -> None:
if self.should_draw_decorations:
# TODO: design a proper text variable templating renderer
collection = context.scene.camera.BIMObjectProperties.collection
collection = tool.Blender.get_object_bim_props(context.scene.camera).collection
for obj in collection.objects:
element = tool.Ifc.get_entity(obj)
if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
@@ -613,7 +613,13 @@ class BIMTextProperties(PropertyGroup):
)
newline_at: IntProperty(name="Newline At")
def get_text_edited_data(self):
if TYPE_CHECKING:
is_editing: bool
literals: bpy.types.bpy_prop_collection_idprop[LiteralProps]
font_size: str
newline_at: int
def get_text_edited_data(self) -> dict[str, Any]:
"""should be called only if `is_editing`
otherwise should use `DecoratorData.get_ifc_text_data(obj)` instead
because this data could be out of date
@@ -676,3 +682,10 @@ class BIMAnnotationProperties(PropertyGroup):
)
is_adding_type: bpy.props.BoolProperty(default=False)
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
if TYPE_CHECKING:
object_type: str
relating_type_id: str
create_representation_for_type: bool
is_adding_type: bool
type_name: str
@@ -56,7 +56,7 @@ def a1_to_rc(cell):
class Scheduler:
def schedule(self, infile, outfile):
def schedule(self, infile: str, outfile: str) -> None:
self.svg = svgwrite.Drawing(
outfile,
debug=False,
@@ -71,11 +71,12 @@ class Scheduler:
elif infile.endswith("xlsx"):
self.schedule_xlsx(infile, outfile)
def parse_css(self, infile):
def parse_css(self, infile: str) -> None:
props = tool.Drawing.get_document_props()
stylesheet_path = os.path.splitext(infile)[0] + ".css"
if not os.path.exists(stylesheet_path):
stylesheet_rel_path = getattr(bpy.context.scene.DocProperties, "schedules_stylesheet_path")
ifc_file_path = os.path.dirname(IfcStore.path)
stylesheet_rel_path = props.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):
stylesheet_path = tool.Blender.get_data_dir_path(Path("assets") / "schedule.css")
@@ -91,7 +92,7 @@ class Scheduler:
self.svg.defs.add(self.svg.style(css))
def schedule_xlsx(self, infile, outfile):
def schedule_xlsx(self, infile: str, outfile: str) -> None:
workbook = openpyxl.open(infile, data_only=True)
sheet = workbook.active
@@ -236,7 +237,7 @@ class Scheduler:
self.svg["viewBox"] = "0 0 {} {}".format(total_width, total_height)
self.svg.save(pretty=True)
def schedule_ods(self, infile, outfile):
def schedule_ods(self, infile: str, outfile: str) -> None:
doc = load_ods(infile)
# useful for debugging ods
@@ -495,11 +496,11 @@ class Scheduler:
self.svg["viewBox"] = "0 0 {} {}".format(total_width, total_height)
self.svg.save(pretty=True)
def get_style(self, style_name, styles):
def get_style(self, style_name: str, styles: dict) -> dict:
style = styles[style_name] if style_name else {}
return style
def get_box_alignment(self, style):
def get_box_alignment(self, style: dict) -> str:
if style and "vertical-align" in style and style["vertical-align"] != "automatic":
vertical_align = style["vertical-align"]
else:
@@ -181,35 +181,35 @@ class SvgWriter:
self.decimal_places = decimal_places
for element in annotations:
obj = tool.Ifc.get_object(element)
if not obj or element.ObjectType == "DRAWING":
if not obj or (object_type := ifcopenshell.util.element.get_predefined_type(element)) == "DRAWING":
continue
elif element.ObjectType == "GRID":
elif object_type == "GRID":
self.draw_grid_annotation(obj)
elif element.ObjectType == "TEXT_LEADER":
elif object_type == "TEXT_LEADER":
self.draw_leader_annotation(obj)
elif element.ObjectType == "STAIR_ARROW":
elif object_type == "STAIR_ARROW":
self.draw_stair_annotation(obj)
elif element.ObjectType == "DIMENSION":
elif object_type == "DIMENSION":
self.draw_dimension_annotations(obj)
elif element.ObjectType == "ANGLE":
elif object_type == "ANGLE":
self.draw_angle_annotations(obj)
elif element.ObjectType == "RADIUS":
elif object_type == "RADIUS":
self.draw_radius_annotations(obj)
elif element.ObjectType == "DIAMETER":
elif object_type == "DIAMETER":
self.draw_diameter_annotations(obj)
elif element.ObjectType == "ELEVATION":
elif object_type == "ELEVATION":
self.draw_elevation_annotation(obj)
elif element.ObjectType == "SECTION":
elif object_type == "SECTION":
self.draw_section_annotation(obj)
elif element.ObjectType == "BREAKLINE":
elif object_type == "BREAKLINE":
self.draw_break_annotations(obj)
elif element.ObjectType == "PLAN_LEVEL":
elif object_type == "PLAN_LEVEL":
self.draw_plan_level_annotation(obj)
elif element.ObjectType == "SECTION_LEVEL":
elif object_type == "SECTION_LEVEL":
self.draw_section_level_annotation(obj)
elif element.ObjectType == "TEXT":
elif object_type == "TEXT":
self.draw_text_annotation(obj, obj.location)
elif element.ObjectType in ("FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
elif object_type in ("FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
self.draw_fall_annotations(obj)
else:
self.draw_misc_annotation(obj)
@@ -1207,13 +1207,14 @@ class SvgWriter:
angle = 90
# ues SLOPE_ANGLE as default
if element.ObjectType in ("FALL", "SLOPE_ANGLE"):
object_type = ifcopenshell.util.element.get_predefined_type(element)
if object_type in ("FALL", "SLOPE_ANGLE"):
return f"{angle}°"
elif element.ObjectType == "SLOPE_FRACTION":
elif object_type == "SLOPE_FRACTION":
if angle == 90:
return "-"
return f"{helper.format_distance(rise, precision=self.precision, decimal_places=self.decimal_places)} / {helper.format_distance(run, precision=self.precision, decimal_places=self.decimal_places)}"
elif element.ObjectType == "SLOPE_PERCENT":
elif object_type == "SLOPE_PERCENT":
if angle == 90:
return "-"
return f"{round(angle_tg * 100)} %"
+9 -8
View File
@@ -49,7 +49,7 @@ class BIM_PT_camera(Panel):
return
self.layout.use_property_split = True
dprops = context.scene.DocProperties
dprops = tool.Drawing.get_document_props()
props = context.scene.camera.data.BIMCameraProperties
col = self.layout.column(align=True)
@@ -161,7 +161,7 @@ class BIM_PT_drawing_underlay(Panel):
layout.use_property_split = True
camera = context.scene.camera
assert camera
dprops = context.scene.DocProperties
dprops = tool.Drawing.get_document_props()
props = camera.data.BIMCameraProperties
drawing_index_is_valid = props.active_drawing_style_index < len(dprops.drawing_styles)
@@ -229,7 +229,7 @@ class BIM_PT_drawings(Panel):
draw_project_not_saved_ui(self)
return
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
if not self.props.is_editing_drawings:
row = self.layout.row(align=True)
@@ -302,7 +302,7 @@ class BIM_PT_schedules(Panel):
draw_project_not_saved_ui(self)
return
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
if not self.props.is_editing_schedules:
row = self.layout.row(align=True)
@@ -352,7 +352,7 @@ class BIM_PT_references(Panel):
draw_project_not_saved_ui(self)
return
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
if not self.props.is_editing_references:
row = self.layout.row(align=True)
@@ -394,7 +394,7 @@ class BIM_PT_sheets(Panel):
draw_project_not_saved_ui(self)
return
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
if not self.props.is_editing_sheets:
row = self.layout.row(align=True)
@@ -520,7 +520,8 @@ class BIM_PT_text(Panel):
def draw(self, context):
obj = context.active_object
props = obj.BIMTextProperties
assert obj
props = tool.Drawing.get_text_props(obj)
if props.is_editing:
# shares most of the code with EditTextPopup.draw()
@@ -600,7 +601,7 @@ class BIM_UL_drawinglist(bpy.types.UIList):
selected_icon = "CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT"
row.prop(item, "is_selected", text="", icon=selected_icon, emboss=False)
row.prop(item, "name", text="", emboss=False)
self.props = context.scene.DocProperties
self.props = tool.Drawing.get_document_props()
if (
self.props.drawings
and self.props.active_drawing_id
@@ -19,6 +19,7 @@
import os
import bpy
import ifcopenshell.util.element
import bonsai.core.type
import bonsai.core.drawing as core
import bonsai.tool as tool
@@ -26,7 +27,7 @@ import ifcopenshell.util.representation
from bonsai.bim.module.drawing.data import DecoratorData, AnnotationData
from bonsai.bim.helper import prop_with_search
from bpy.types import WorkSpaceTool
from typing import Union
from functools import partial
class LaunchAnnotationTypeManager(bpy.types.Operator):
@@ -45,7 +46,7 @@ class LaunchAnnotationTypeManager(bpy.types.Operator):
if not AnnotationData.is_loaded:
AnnotationData.load()
props = context.scene.BIMAnnotationProperties
props = tool.Drawing.get_annotation_props()
columns = self.layout.column_flow(columns=3)
row = columns.row()
@@ -134,28 +135,14 @@ class AnnotationTool(WorkSpaceTool):
AnnotationToolUI.draw(context, layout)
def add_layout_hotkey_operator(
layout: bpy.types.UILayout, text: str, hotkey: str, description: Union[str, None]
) -> tuple[bpy.types.OperatorProperties, bpy.types.UILayout]:
modifiers = {
"A": "EVENT_ALT",
"S": "EVENT_SHIFT",
}
modifier, key = hotkey.split("_")
row = layout.row(align=True)
row.label(text="", icon=modifiers[modifier])
row.label(text="", icon=f"EVENT_{key}")
op = row.operator("bim.annotation_hotkey", text=text)
op.hotkey = hotkey
op.description = description
return op, row
add_layout_hotkey_operator = partial(
tool.Blender.add_layout_hotkey_operator, tool_name="annotation", module_name=__name__
)
# TODO: move to operator
def create_annotation_occurrence(context):
props = context.scene.BIMAnnotationProperties
props = tool.Drawing.get_annotation_props()
relating_type = tool.Ifc.get().by_id(int(props.relating_type_id))
object_type = props.object_type
@@ -197,10 +184,12 @@ def create_annotation_occurrence(context):
class AnnotationToolUI:
layout: bpy.types.UILayout
@classmethod
def draw(cls, context, layout):
cls.layout = layout
cls.props = context.scene.BIMAnnotationProperties
cls.props = tool.Drawing.get_annotation_props()
row = cls.layout.row(align=True)
if not tool.Ifc.get():
@@ -224,7 +213,8 @@ class AnnotationToolUI:
@classmethod
def draw_create_object_interface(cls):
row = cls.layout.row(align=True)
row.prop(bpy.context.scene.DocProperties, "should_draw_decorations", text="Viewport Annotations")
props = tool.Drawing.get_document_props()
row.prop(props, "should_draw_decorations", text="Viewport Annotations")
@classmethod
def draw_edit_object_interface(cls, context):
@@ -261,8 +251,8 @@ class AnnotationToolUI:
class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.annotation_hotkey"
bl_label = "Hotkey"
bl_options = {"REGISTER", "UNDO"}
bl_label = ""
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
hotkey: bpy.props.StringProperty()
description: bpy.props.StringProperty()
@@ -279,19 +269,19 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
self.report({"ERROR"}, "No drawing active for annotation hotkeys.")
return {"CANCELLED"}
self.props = context.scene.BIMAnnotationProperties
self.props = tool.Drawing.get_annotation_props()
getattr(self, f"hotkey_{self.hotkey}")()
def invoke(self, context, event):
# https://blender.stackexchange.com/questions/276035/how-do-i-make-operators-remember-their-property-values-when-called-from-a-hotkey
self.props = context.scene.BIMAnnotationProperties
self.props = tool.Drawing.get_annotation_props()
return self.execute(context)
def draw(self, context):
pass
def hotkey_S_T(self):
props = bpy.context.scene.BIMAnnotationProperties
props = tool.Drawing.get_annotation_props()
object_type = props.object_type
if object_type not in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP:
@@ -330,7 +320,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if not element or not element.is_a("IfcAnnotation"):
continue
annotation_type = element.ObjectType
annotation_type = ifcopenshell.util.element.get_predefined_type(element)
if annotation_type not in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP:
self.report({"ERROR"}, f"Annotation type {annotation_type} is not supported for readjustment.")
continue
+2 -3
View File
@@ -18,7 +18,6 @@
import bonsai.tool as tool
from bpy.types import Panel
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.fm.data import FMData
@@ -40,11 +39,11 @@ class BIM_PT_fm(Panel):
scene = context.scene
props = scene.BIMFMProperties
if IfcStore.get_file():
if tool.Ifc.get():
row = layout.row()
row.prop(props, "should_load_from_memory")
if not IfcStore.get_file() or not props.should_load_from_memory:
if not tool.Ifc.get() or not props.should_load_from_memory:
row = layout.row()
props.ifc_files.layout_file_select(row, "*.ifc;*.ifczip;*.ifcxml", "IFC File(s)")
@@ -98,12 +98,14 @@ addon_keymaps = []
@persistent
def block_scale(scene):
def block_scale(scene: bpy.types.Scene) -> None:
import bonsai.tool as tool
if obj := (getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active):
if isinstance(obj, bpy.types.Object) and obj.BIMObjectProperties.ifc_definition_id:
if isinstance(obj, bpy.types.Object) and tool.Blender.get_ifc_definition_id(obj):
if obj.scale != (1, 1, 1):
obj.scale = (1, 1, 1)
elif isinstance(obj, bpy.types.Mesh) and obj.BIMMeshProperties.ifc_definition_id:
elif isinstance(obj, bpy.types.Mesh) and tool.Geometry.get_mesh_props(obj).ifc_definition_id:
if obj.scale != (1, 1, 1):
obj.scale = (1, 1, 1)
+12 -10
View File
@@ -53,15 +53,16 @@ class ViewportData:
obj = bpy.context.active_object
element = tool.Ifc.get_entity(obj)
modes = [obj_mode]
if bpy.context.scene.BIMGeometryProperties.representation_obj:
modes: list[tuple[str, str, str, str, int]] = [obj_mode]
gprops = tool.Geometry.get_geometry_props()
if gprops.representation_obj:
modes.append(item_mode)
if not obj:
return modes
if obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs:
pprops = tool.Project.get_project_props()
if obj in pprops.clipping_planes_objs:
pass
elif element:
if tool.Geometry.is_locked(element):
@@ -107,8 +108,9 @@ class RepresentationsData:
element = tool.Ifc.get_entity(obj)
active_representation_id = None
if obj.data and hasattr(obj.data, "BIMMeshProperties"):
active_representation_id = obj.data.BIMMeshProperties.ifc_definition_id
active_representation = tool.Geometry.get_active_representation(obj)
if active_representation:
active_representation_id = active_representation.id()
for representation in tool.Geometry.get_representations_iter(element):
representation_type = representation.RepresentationType
@@ -158,9 +160,9 @@ class RepresentationsData:
if not obj.data:
return []
element = tool.Ifc.get_entity(obj)
if not (active_representation_id := obj.data.BIMMeshProperties.ifc_definition_id):
base_representation = tool.Geometry.get_active_representation(obj)
if not base_representation:
return [] # Maybe in profile editing mode
base_representation = tool.Ifc.get().by_id(active_representation_id)
# shape aspects matching context of the active representation
matching_shape_aspects = []
@@ -390,7 +392,7 @@ class PlacementData:
def load(cls):
cls.data = {"has_placement": cls.has_placement()}
props = bpy.context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
obj = bpy.context.active_object
if obj and props.has_blender_offset:
xyz = cls.original_xyz(obj)
@@ -413,7 +415,7 @@ class PlacementData:
@classmethod
def original_xyz(cls, obj):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
props = bpy.context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
xyz = ifcopenshell.util.geolocation.xyz2enh(
obj.matrix_world[0][3],
obj.matrix_world[1][3],
@@ -46,12 +46,14 @@ class ItemDecorator:
obj_is_boolean: dict[str, list[ifcopenshell.entity_instance]] = {}
objs: dict[str, dict[str, list]] = {}
obj_matrix: dict[str, Matrix] = {}
for item_obj in context.scene.BIMGeometryProperties.item_objs:
props = tool.Geometry.get_geometry_props()
for item_obj in props.item_objs:
if obj := item_obj.obj:
obj: bpy.types.Object
objs[obj.name] = cls.get_obj_data(obj)
obj_is_selected[obj.name] = obj.select_get()
item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
item = tool.Geometry.get_active_representation(obj)
assert item
obj_is_boolean[obj.name] = [i for i in tool.Ifc.get().get_inverse(item) if i.is_a("IfcBooleanResult")]
obj_matrix[obj.name] = obj.matrix_world.copy()
@@ -143,7 +145,8 @@ class ItemDecorator:
color = selected_elements_color
blf.color(font_id, *color)
for item in context.scene.BIMGeometryProperties.item_objs:
props = tool.Geometry.get_geometry_props()
for item in props.item_objs:
if (obj := item.obj) and obj.hide_get() == False:
if obj.select_get():
centroid = obj.matrix_world @ Vector(obj.bound_box[0]).lerp(Vector(obj.bound_box[6]), 0.5)
File diff suppressed because it is too large Load Diff
@@ -32,7 +32,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import Optional, TYPE_CHECKING, Union
from typing import Optional, TYPE_CHECKING, Union, Literal
def get_contexts(self, context):
@@ -270,6 +270,9 @@ class BIMObjectGeometryProperties(PropertyGroup):
representation_item_layer: str
GeometryMode = Literal["OBJECT", "ITEM", "EDIT"]
class BIMGeometryProperties(PropertyGroup):
# Revit workaround
should_use_presentation_style_assignment: BoolProperty(name="Force Presentation Style Assignment", default=False)
@@ -308,7 +311,7 @@ class BIMGeometryProperties(PropertyGroup):
should_force_faceted_brep: bool
should_force_triangulation: bool
is_changing_mode: bool
mode: str
mode: GeometryMode
representation_obj: Union[bpy.types.Object, None]
item_objs: bpy.types.bpy_prop_collection_idprop[RepresentationItemObject]
representation_from_object: Union[bpy.types.Object, None]
+53 -27
View File
@@ -20,7 +20,6 @@ import bpy
import bonsai.bim
import bonsai.tool as tool
from bpy.types import Panel, Menu, UIList
from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import prop_with_search
from bonsai.bim.module.geometry.data import (
RepresentationsData,
@@ -32,13 +31,31 @@ from bonsai.bim.module.geometry.data import (
from bonsai.bim.module.layer.data import LayersData
class UIData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {"menu_icon_color_mode": cls.icon_color_mode("user_interface.wcol_menu.text")}
cls.is_loaded = True
@classmethod
def icon_color_mode(cls, color_path):
return tool.Blender.detect_icon_color_mode(color_path)
def mode_menu(self, context):
if not tool.Ifc.get():
return
if not UIData.is_loaded:
UIData.load()
ifc_icon = f"{UIData.data['menu_icon_color_mode']}_ifc"
row = self.layout.row(align=True)
if context.scene.BIMGeometryProperties.mode == "EDIT":
props = tool.Geometry.get_geometry_props()
if props.mode == "EDIT":
row.operator("bim.override_mode_set_object", icon="CANCEL", text="Discard Changes").should_save = False
row.prop(context.scene.BIMGeometryProperties, "mode", text="", icon_value=bonsai.bim.icons["IFC"].icon_id)
row.prop(props, "mode", text="", icon_value=bonsai.bim.icons[ifc_icon].icon_id)
def object_menu(self, context):
@@ -105,6 +122,7 @@ class BIM_MT_object_set_origin(Menu):
def outliner_menu(self, context):
self.layout.separator()
self.layout.operator("bim.override_outliner_delete", icon="X")
self.layout.operator("bim.override_paste_buffer", icon="PLUGIN")
class BIM_PT_representations(Panel):
@@ -206,7 +224,9 @@ class BIM_PT_representation_items(Panel):
if not RepresentationItemsData.is_loaded:
RepresentationItemsData.load()
obj = context.scene.BIMGeometryProperties.representation_obj or tool.Blender.get_active_object()
props = tool.Geometry.get_geometry_props()
obj = props.representation_obj or tool.Blender.get_active_object()
assert obj
props = tool.Geometry.get_object_geometry_props(obj)
row = self.layout.row(align=True)
@@ -314,18 +334,17 @@ class BIM_PT_connections(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
if not tool.Ifc.get_object_by_identifier(tool.Blender.get_ifc_definition_id(obj)):
return False
return IfcStore.get_file()
return tool.Ifc.get()
def draw(self, context):
if not ConnectionsData.is_loaded:
ConnectionsData.load()
layout = self.layout
props = context.active_object.BIMObjectProperties
if not ConnectionsData.data["connections"] and not ConnectionsData.data["is_connection_realization"]:
layout.label(text="No connections found")
@@ -391,15 +410,18 @@ class BIM_PT_mesh(Panel):
@classmethod
def poll(cls, context):
return (
context.active_object is not None
and context.active_object.type == "MESH"
and hasattr(context.active_object.data, "BIMMeshProperties")
and context.active_object.data.BIMMeshProperties.ifc_definition_id
(obj := context.active_object) is not None
and (mesh := obj.data)
and isinstance(mesh, bpy.types.Mesh)
and tool.Geometry.get_mesh_props(mesh).ifc_definition_id
)
def draw(self, context):
if not context.active_object.data:
return
obj = context.active_object
assert obj
mesh = obj.data
assert isinstance(mesh, bpy.types.Mesh)
row = self.layout.row()
row.label(text="Advanced Users Only", icon="ERROR")
@@ -407,7 +429,7 @@ class BIM_PT_mesh(Panel):
row = layout.row()
text = "Manually Save Representation"
if tool.Ifc.is_edited(context.active_object):
if tool.Ifc.is_edited(obj):
text += "*"
row.operator("bim.update_representation", text=text)
@@ -434,8 +456,8 @@ class BIM_PT_mesh(Panel):
op = row.operator("bim.update_representation", text="Convert To Arbitrary Extrusion With Voids")
op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids"
if context.active_object and context.active_object.data:
mprops = context.active_object.data.BIMMeshProperties
if True:
mprops = tool.Geometry.get_mesh_props(mesh)
row = layout.row()
row.operator("bim.get_representation_ifc_parameters")
for index, ifc_parameter in enumerate(mprops.ifc_parameters):
@@ -457,12 +479,16 @@ class BIM_PT_placement(Panel):
@classmethod
def poll(cls, context):
return context.active_object and context.active_object.BIMObjectProperties.ifc_definition_id
return (obj := context.active_object) and tool.Blender.get_ifc_definition_id(obj)
def draw(self, context):
if not PlacementData.is_loaded:
PlacementData.load()
obj = context.active_object
assert obj
props = tool.Blender.get_object_bim_props(obj)
if not PlacementData.data["has_placement"]:
row = self.layout.row()
row.label(text="No Object Placement Found")
@@ -473,12 +499,12 @@ class BIM_PT_placement(Panel):
row = self.layout.row()
row.prop(context.active_object, "rotation_euler", text="Rotation")
if context.active_object.BIMObjectProperties.blender_offset_type != "NONE":
if props.blender_offset_type != "NONE":
row = self.layout.row(align=True)
row.label(text="Blender Offset", icon="TRACKING_REFINE_FORWARDS")
row.label(text=context.active_object.BIMObjectProperties.blender_offset_type)
row.label(text=props.blender_offset_type)
if context.active_object.BIMObjectProperties.blender_offset_type != "NOT_APPLICABLE":
if props.blender_offset_type != "NOT_APPLICABLE":
row = self.layout.row(align=True)
row.label(text=PlacementData.data["original_x"], icon="EMPTY_AXIS")
row.label(text=PlacementData.data["original_y"])
@@ -551,14 +577,14 @@ class BIM_PT_workarounds(Panel):
@classmethod
def poll(cls, context):
return (
context.active_object is not None
and context.active_object.type == "MESH"
and hasattr(context.active_object.data, "BIMMeshProperties")
and context.active_object.data.BIMMeshProperties.ifc_definition_id
(obj := context.active_object) is not None
and (mesh := obj.data)
and isinstance(mesh, bpy.types.Mesh)
and tool.Geometry.get_mesh_props(mesh).ifc_definition_id
)
def draw(self, context):
props = context.scene.BIMGeometryProperties
props = tool.Geometry.get_geometry_props()
row = self.layout.row()
row.prop(props, "should_force_faceted_brep")
row = self.layout.row()
@@ -568,7 +594,7 @@ class BIM_PT_workarounds(Panel):
class BIM_UL_representation_items(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, icon, active_data, active_propname):
if item:
icon = "MATERIAL" if item.surface_style else "MESH_UVSPHERE"
row = layout.row(align=True)
@@ -167,7 +167,7 @@ class GeoreferenceData:
result["rotation"] = str(round(ifcopenshell.util.geolocation.yaxis2angle(*wcs[:, 1][:2]), 3))
result["x"], result["y"], result["z"] = wcs[:, 3][:3]
props = bpy.context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if props.has_blender_offset:
blender_xyz = ifcopenshell.util.geolocation.enh2xyz(
result["x"],
@@ -193,7 +193,7 @@ class GeoreferenceData:
@classmethod
def local_origin(cls):
props = bpy.context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if not props.has_blender_offset:
return
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
@@ -21,6 +21,7 @@ import blf
import gpu
import bmesh
import ifcopenshell
import ifcopenshell.util.geolocation
import bonsai.tool as tool
from math import radians
from bpy.types import SpaceView3D
@@ -53,7 +54,8 @@ class GeoreferenceDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
self.scale = bpy.context.scene.BIMGeoreferenceProperties.visualization_scale
props = tool.Georeference.get_georeference_props()
self.scale = props.visualization_scale
content_pos = [v * self.scale for v in content_pos]
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
@@ -64,7 +66,7 @@ class GeoreferenceDecorator:
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if not props.model_origin: # If this is empty, no georeferencing data has been loaded.
return
@@ -177,7 +179,7 @@ class GeoreferenceDecorator:
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if not props.model_origin: # If this is empty, no georeferencing data has been loaded.
return
@@ -350,7 +352,7 @@ class GeoreferenceDecorator:
self.gn_angle = float(GeoreferenceData.data["map_derived_angle"] or 0)
self.tn_angle = float(GeoreferenceData.data["true_derived_angle"] or 0)
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if props.has_blender_offset:
blender_angle = ifcopenshell.util.geolocation.xaxis2angle(
float(props.blender_x_axis_abscissa), float(props.blender_x_axis_ordinate)
@@ -33,15 +33,18 @@ from bpy.props import (
)
from bonsai.bim.module.georeference.data import GeoreferenceData
from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator
from typing import TYPE_CHECKING
def get_coordinate_operation_class(self, context):
def get_coordinate_operation_class(
self: "BIMGeoreferenceProperties", context: bpy.types.Context
) -> list[tuple[str, str, str]]:
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
return GeoreferenceData.data["coordinate_operation_class"]
def update_true_north_angle(self, context):
def update_true_north_angle(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
if self.is_changing_angle:
return
self.is_changing_angle = True
@@ -54,7 +57,7 @@ def update_true_north_angle(self, context):
self.is_changing_angle = False
def update_true_north_vector(self, context):
def update_true_north_vector(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
if self.is_changing_angle:
return
self.is_changing_angle = True
@@ -67,7 +70,7 @@ def update_true_north_vector(self, context):
self.is_changing_angle = False
def update_grid_north_angle(self, context):
def update_grid_north_angle(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
if self.is_changing_angle:
return
self.is_changing_angle = True
@@ -81,7 +84,7 @@ def update_grid_north_angle(self, context):
self.is_changing_angle = False
def update_grid_north_vector(self, context):
def update_grid_north_vector(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
if self.is_changing_angle:
return
self.is_changing_angle = True
@@ -95,15 +98,15 @@ def update_grid_north_vector(self, context):
self.is_changing_angle = False
def update_should_visualise(self, context):
def update_should_visualise(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
if self.should_visualise:
GeoreferenceDecorator.install(bpy.context)
else:
GeoreferenceDecorator.uninstall()
def update_blender_coordinates(self, context):
props = bpy.context.scene.BIMGeoreferenceProperties
def update_blender_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
props = self
if props.is_updating_coordinates:
return
props.is_updating_coordinates = True
@@ -123,8 +126,8 @@ def update_blender_coordinates(self, context):
props.is_updating_coordinates = False
def update_local_coordinates(self, context):
props = bpy.context.scene.BIMGeoreferenceProperties
def update_local_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
props = self
if props.is_updating_coordinates:
return
props.is_updating_coordinates = True
@@ -147,8 +150,8 @@ def update_local_coordinates(self, context):
props.is_updating_coordinates = False
def update_map_coordinates(self, context):
props = bpy.context.scene.BIMGeoreferenceProperties
def update_map_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
props = self
if props.is_updating_coordinates:
return
props.is_updating_coordinates = True
@@ -248,3 +251,45 @@ class BIMGeoreferenceProperties(PropertyGroup):
wcs_y: StringProperty(name="WCS Y", default="0")
wcs_z: StringProperty(name="WCS Z", default="0")
wcs_rotation: StringProperty(name="WCS Rotation", default="0")
if TYPE_CHECKING:
coordinate_operation_class: str
is_changing_angle: bool
is_editing: bool
is_editing_wcs: bool
is_editing_true_north: bool
coordinate_operation: bpy.types.bpy_prop_collection_idprop[Attribute]
projected_crs: bpy.types.bpy_prop_collection_idprop[Attribute]
is_updating_coordinates: bool
blender_coordinates: str
local_coordinates: str
map_coordinates: str
should_visualise: bool
visualization_scale: float
grid_north_angle: str
x_axis_abscissa: str
x_axis_ordinate: str
x_axis_is_null: bool
host_model_origin: str
host_model_origin_si: str
host_model_project_north: str
model_origin: str
model_origin_si: str
model_project_north: str
has_blender_offset: bool
blender_offset_x: str
blender_offset_y: str
blender_offset_z: str
blender_x_axis_abscissa: str
blender_x_axis_ordinate: str
true_north_angle: str
true_north_abscissa: str
true_north_ordinate: str
wcs_x: str
wcs_y: str
wcs_z: str
wcs_rotation: str
@@ -32,7 +32,7 @@ class BIM_PT_gis(Panel):
bl_parent_id = "BIM_PT_tab_geometry"
def draw_header(self, context):
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
row = self.layout.row(align=True)
icon = "HIDE_OFF" if props.should_visualise else "HIDE_ON"
row.label(text="") # empty text occupies the left of the row
@@ -43,7 +43,7 @@ class BIM_PT_gis(Panel):
def draw(self, context):
self.layout.use_property_split = True
self.layout.use_property_decorate = False
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
@@ -54,7 +54,7 @@ class BIM_PT_gis(Panel):
self.draw_ui(context)
def draw_editable_ui(self, context):
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
row = self.layout.row(align=True)
row.label(text="Projected CRS", icon="WORLD")
row.operator("bim.edit_georeferencing", icon="CHECKMARK", text="")
@@ -87,7 +87,7 @@ class BIM_PT_gis(Panel):
draw_attribute(attribute, self.layout.row())
def draw_ui(self, context):
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if tool.Ifc.get_schema() == "IFC2X3":
row = self.layout.row()
@@ -149,7 +149,7 @@ class BIM_PT_gis_true_north(Panel):
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
self.props = context.scene.BIMGeoreferenceProperties
self.props = tool.Georeference.get_georeference_props()
if self.props.is_editing_true_north:
self.draw_editable_ui(context)
@@ -200,7 +200,7 @@ class BIM_PT_gis_blender(Panel):
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if props.has_blender_offset:
row = self.layout.row()
@@ -233,7 +233,7 @@ class BIM_PT_gis_wcs(Panel):
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if props.is_editing_wcs:
self.draw_editable_ui(context)
@@ -263,7 +263,7 @@ class BIM_PT_gis_wcs(Panel):
row.operator("bim.enable_editing_wcs", icon="GREASEPENCIL", text="")
def draw_editable_ui(self, context):
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
row = self.layout.row(align=True)
row.label(text="World Coordinate System", icon="EMPTY_ARROWS")
@@ -293,7 +293,7 @@ class BIM_PT_gis_calculator(Panel):
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
props = context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if props.has_blender_offset:
row = self.layout.row(align=True)
@@ -22,7 +22,6 @@ import ifcopenshell.api.group
import ifcopenshell.util.attribute
import bonsai.bim.helper
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
import json
@@ -129,8 +128,7 @@ class RemoveGroup(bpy.types.Operator, tool.Ifc.Operator):
group: bpy.props.IntProperty()
def _execute(self, context):
props = context.scene.BIMGroupProperties
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run("group.remove_group", self.file, **{"group": self.file.by_id(self.group)})
bpy.ops.bim.load_groups()
return {"FINISHED"}
+5 -4
View File
@@ -17,8 +17,8 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
from bpy.types import Panel, UIList
from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import draw_attributes
from bonsai.bim.module.group.data import GroupsData, ObjectGroupsData
@@ -34,7 +34,7 @@ class BIM_PT_groups(Panel):
@classmethod
def poll(cls, context):
return IfcStore.get_file()
return tool.Ifc.get()
def draw(self, context):
if not GroupsData.is_loaded:
@@ -91,9 +91,10 @@ class BIM_PT_object_groups(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
return IfcStore.get_file() and context.active_object.BIMObjectProperties.ifc_definition_id
props = tool.Blender.get_object_bim_props(obj)
return tool.Ifc.get() and props.ifc_definition_id
def draw(self, context):
if not ObjectGroupsData.is_loaded:
@@ -374,16 +374,17 @@ class ObjectLog(bpy.types.Operator):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
cls.poll_message_set("No Active Object")
elif not context.active_object.BIMObjectProperties.ifc_definition_id:
elif not tool.Blender.get_ifc_definition_id(obj):
cls.poll_message_set("Active Object doesn't have an IFC definition")
else:
return True
def execute(self, context):
step_id = context.active_object.BIMObjectProperties.ifc_definition_id
obj = context.active_object
assert obj
step_id = tool.Blender.get_ifc_definition_id(obj)
core.entity_log(tool.IfcGit, tool.Ifc, step_id, self)
return {"FINISHED"}
+8 -14
View File
@@ -24,7 +24,6 @@ import ifcopenshell.util.element
import ifcopenshell.util.attribute
import bonsai.bim.helper
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
class LoadLayers(bpy.types.Operator):
@@ -33,7 +32,7 @@ class LoadLayers(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
props = context.scene.BIMLayerProperties
props.layers.clear()
for layer in tool.Ifc.get().by_type("IfcPresentationLayerAssignment"):
@@ -126,7 +125,7 @@ class RemovePresentationLayer(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMLayerProperties
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run("layer.remove_layer", self.file, **{"layer": self.file.by_id(self.layer)})
bpy.ops.bim.load_layers()
return {"FINISHED"}
@@ -142,12 +141,12 @@ class AssignPresentationLayer(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
item = bpy.data.meshes.get(self.item) if self.item else context.active_object.data
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"layer.assign_layer",
self.file,
**{
"items": [self.file.by_id(item.BIMMeshProperties.ifc_definition_id)],
"items": [self.file.by_id(tool.Geometry.get_mesh_props(item).ifc_definition_id)],
"layer": self.file.by_id(self.layer),
},
)
@@ -164,15 +163,10 @@ class UnassignPresentationLayer(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
item = bpy.data.meshes.get(self.item) if self.item else context.active_object.data
self.file = IfcStore.get_file()
ifcopenshell.api.run(
"layer.unassign_layer",
self.file,
**{
"items": [self.file.by_id(item.BIMMeshProperties.ifc_definition_id)],
"layer": self.file.by_id(self.layer),
},
)
ifc_file = tool.Ifc.get()
representation = tool.Geometry.get_data_representation(item)
assert representation
ifcopenshell.api.layer.unassign_layer(ifc_file, items=[representation], layer=ifc_file.by_id(self.layer))
return {"FINISHED"}
+2 -3
View File
@@ -17,8 +17,8 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
from bpy.types import Panel, UIList, Mesh
from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import draw_attributes
from bonsai.bim.module.layer.data import LayersData
@@ -34,7 +34,7 @@ class BIM_PT_layers(Panel):
@classmethod
def poll(cls, context):
return IfcStore.get_file()
return tool.Ifc.get()
def draw(self, context):
if not LayersData.is_loaded:
@@ -77,7 +77,6 @@ class BIM_UL_layers(UIList):
row.label(text=item.name)
if context.active_object and isinstance(context.active_object.data, Mesh):
mprops = context.active_object.data.BIMMeshProperties
if item.ifc_definition_id in LayersData.data["active_layers"]:
op = row.operator("bim.unassign_presentation_layer", text="", icon="KEYFRAME_HLT", emboss=False)
op.layer = item.ifc_definition_id
+3 -2
View File
@@ -47,7 +47,8 @@ class LibrariesData:
@classmethod
def library_attributes(cls):
library_id = bpy.context.scene.BIMLibraryProperties.active_library_id
props = tool.Library.get_library_props()
library_id = props.active_library_id
if not library_id:
return []
results = []
@@ -63,7 +64,7 @@ class LibrariesData:
@classmethod
def reference_attributes(cls):
props = bpy.context.scene.BIMLibraryProperties
props = tool.Library.get_library_props()
try:
reference_id = props.references[props.active_reference_index].ifc_definition_id
except:
+26 -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 bonsai.bim.module.library.data import LibrariesData
from bpy.types import PropertyGroup
@@ -30,16 +31,31 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal
def update_active_reference_index(self, context):
LibrariesData.is_loaded = False
def update_library_element_name(self: "LibraryReference", context: bpy.types.Context):
ifc_file = tool.Ifc.get()
element = ifc_file.by_id(self.ifc_definition_id)
previous_name = element.Name
if self.name == previous_name:
return
element.Name = self.name
LibrariesData.is_loaded = False
class LibraryReference(PropertyGroup):
name: StringProperty(name="Name")
name: StringProperty(name="Name", update=update_library_element_name)
ifc_definition_id: IntProperty(name="IFC Definition ID")
if TYPE_CHECKING:
name: str
ifc_definition_id: int
class BIMLibraryProperties(PropertyGroup):
editing_mode: EnumProperty(
@@ -58,3 +74,12 @@ class BIMLibraryProperties(PropertyGroup):
active_reference_id: IntProperty(name="Active Reference Id")
references: CollectionProperty(type=LibraryReference, name="References")
active_reference_index: IntProperty(name="Active Reference Index", update=update_active_reference_index)
if TYPE_CHECKING:
editing_mode: Literal["NONE", "LIBRARY", "REFERENCES", "REFERENCE"]
library_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
active_library_id: int
reference_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
active_reference_id: int
references: bpy.types.bpy_prop_collection_idprop[LibraryReference]
active_reference_index: int
+16 -6
View File
@@ -16,10 +16,16 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import bonsai.bim.helper
import bonsai.tool as tool
from bpy.types import Panel, UIList
from bonsai.bim.module.library.data import LibrariesData, LibraryReferencesData
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.library.prop import BIMLibraryProperties, LibraryReference
class BIM_PT_libraries(Panel):
@@ -38,7 +44,7 @@ class BIM_PT_libraries(Panel):
def draw(self, context):
if not LibrariesData.is_loaded:
LibrariesData.load()
self.props = context.scene.BIMLibraryProperties
self.props = tool.Library.get_library_props()
if self.props.editing_mode == "LIBRARY":
self.draw_editable_library_ui()
@@ -110,7 +116,7 @@ class BIM_PT_library_references(Panel):
def draw(self, context):
if not LibraryReferencesData.is_loaded:
LibraryReferencesData.load()
self.props = context.scene.BIMLibraryProperties
self.props = tool.Library.get_library_props()
if self.props.editing_mode == "REFERENCES":
self.layout.template_list(
@@ -129,10 +135,12 @@ class BIM_PT_library_references(Panel):
class BIM_UL_library_references(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: LibraryReference, icon, active_data, active_propname
):
if item:
row = layout.row(align=True)
row.label(text=item.name)
row.prop(item, "name", text="", emboss=False)
op = row.operator("bim.enable_editing_library_reference", text="", icon="GREASEPENCIL")
op.reference = item.ifc_definition_id
op = row.operator("bim.remove_library_reference", text="", icon="X")
@@ -140,9 +148,11 @@ class BIM_UL_library_references(UIList):
class BIM_UL_object_library_references(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: LibraryReference, icon, active_data, active_propname
):
if item:
row = layout.row(align=True)
row.label(text=item.name)
row.prop(item, "name", text="", emboss=False)
op = row.operator("bim.assign_library_reference", text="", icon="ADD")
op.reference = item.ifc_definition_id
@@ -48,7 +48,8 @@ class MaterialsData:
@classmethod
def total_materials(cls):
return len(tool.Ifc.get().by_type(bpy.context.scene.BIMMaterialProperties.material_type))
props = tool.Material.get_material_props()
return len(tool.Ifc.get().by_type(props.material_type))
@classmethod
def material_types(cls):
@@ -101,7 +102,7 @@ class MaterialsData:
@classmethod
def material_styles_data(cls) -> dict[int, list[dict[str, Any]]]:
props = bpy.context.scene.BIMMaterialProperties
props = tool.Material.get_material_props()
material_styles_data: dict[int, list[dict[str, Any]]] = {}
for material_item in props.materials:
@@ -281,8 +282,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()
if unit_system == "IMPERIAL":
precision = bpy.context.scene.DocProperties.imperial_precision
precision = props.imperial_precision
else:
precision = None
formatted_thickness = format_distance(
@@ -19,6 +19,7 @@
import bpy
import json
import ifcopenshell.api
import ifcopenshell.api.material
import ifcopenshell.util.element
import ifcopenshell.util.attribute
import ifcopenshell.util.representation
@@ -28,7 +29,6 @@ import bonsai.tool as tool
import bonsai.core.style
import bonsai.core.material as core
import bonsai.bim.module.model.profile as model_profile
from bonsai.bim.ifc import IfcStore
from typing import Any, Union, TYPE_CHECKING
from bonsai.bim.module.model import wall, slab
@@ -40,7 +40,8 @@ class LoadMaterials(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
core.load_materials(tool.Material, context.scene.BIMMaterialProperties.material_type)
props = tool.Material.get_material_props()
core.load_materials(tool.Material, props.material_type)
return {"FINISHED"}
@@ -66,7 +67,10 @@ class SelectByMaterial(bpy.types.Operator):
core.select_by_material(tool.Material, tool.Spatial, material=material)
# copy selection query to clipboard
material_name = material.Name
if material.is_a("IfcMaterialLayerSet"):
material_name = material.LayerSetName
else:
material_name = material.Name
result = f'material="{material_name}"'
bpy.context.window_manager.clipboard = result
self.report({"INFO"}, f"({result}) was copied to the clipboard.")
@@ -116,7 +120,7 @@ class AssignParameterizedProfile(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
profile = ifcopenshell.api.run(
"profile.add_parameterized_profile",
self.file,
@@ -290,7 +294,7 @@ class AddConstituent(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"material.add_constituent",
self.file,
@@ -314,7 +318,7 @@ class RemoveConstituent(bpy.types.Operator, tool.Ifc.Operator):
if len(material_set.MaterialConstituents) == 1:
self.report({"ERROR"}, "At least one constituent must exist")
return {"CANCELLED"}
ifcopenshell.api.run("material.remove_constituent", tool.Ifc.get(), constituent=constituent)
ifcopenshell.api.material.remove_constituent(tool.Ifc.get(), constituent=constituent)
class AddProfile(bpy.types.Operator, tool.Ifc.Operator):
@@ -326,13 +330,14 @@ class AddProfile(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
props = tool.Material.get_material_props()
ifcopenshell.api.run(
"material.add_profile",
self.file,
profile_set=self.file.by_id(self.profile_set),
material=self.file.by_id(int(obj.BIMObjectMaterialProperties.material)),
profile=self.file.by_id(int(context.scene.BIMMaterialProperties.profiles)),
profile=self.file.by_id(int(props.profiles)),
)
@@ -349,7 +354,7 @@ class RemoveProfile(bpy.types.Operator, tool.Ifc.Operator):
if len(material_set.MaterialProfiles) == 1:
self.report({"ERROR"}, "At least one profile must exist")
return {"CANCELLED"}
ifcopenshell.api.run("material.remove_profile", tool.Ifc.get(), profile=profile)
ifcopenshell.api.material.remove_profile(tool.Ifc.get(), profile=profile)
class AddLayer(bpy.types.Operator, tool.Ifc.Operator):
@@ -384,7 +389,7 @@ class ReorderMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
material_set = self.file.by_id(self.material_set)
ifcopenshell.api.run(
"material.reorder_set_item",
@@ -410,7 +415,7 @@ class RemoveLayer(bpy.types.Operator, tool.Ifc.Operator):
if len(material_set.MaterialLayers) == 1:
self.report({"ERROR"}, "At least one layer must exist")
return {"CANCELLED"}
ifcopenshell.api.run("material.remove_layer", tool.Ifc.get(), layer=layer)
ifcopenshell.api.material.remove_layer(tool.Ifc.get(), layer=layer)
for material_set in material_sets:
slab.DumbSlabPlaner().regenerate_from_layer_set(material_set)
wall.DumbWallPlaner().regenerate_from_layer_set(material_set)
@@ -440,7 +445,7 @@ class AddListItem(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"material.add_list_item",
self.file,
@@ -460,7 +465,7 @@ class RemoveListItem(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"material.remove_list_item",
self.file,
@@ -554,7 +559,7 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator):
material_set_usage: bpy.props.IntProperty()
def _execute(self, context):
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
active_obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
props = active_obj.BIMObjectMaterialProperties
element = tool.Ifc.get_entity(active_obj)
@@ -574,32 +579,40 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator):
material_set = self.file.by_id(self.material_set)
attributes = bonsai.bim.helper.export_attributes(props.material_set_attributes)
ifcopenshell.api.run(
"material.edit_assigned_material",
ifcopenshell.api.material.edit_assigned_material(
self.file,
**{"element": material_set, "attributes": attributes},
element=material_set,
attributes=attributes,
)
if self.material_set_usage:
material_set_usage = self.file.by_id(self.material_set_usage)
attributes = bonsai.bim.helper.export_attributes(props.material_set_usage_attributes)
if material_set_usage.is_a("IfcMaterialLayerSetUsage"):
ifcopenshell.api.run(
"material.edit_layer_usage",
ifcopenshell.api.material.edit_layer_usage(
self.file,
**{"usage": material_set_usage, "attributes": attributes},
)
slab.DumbSlabPlaner().regenerate_from_layer_set(material_set_usage.ForLayerSet)
wall.DumbWallPlaner().regenerate_from_layer_set(material_set_usage.ForLayerSet)
elif material_set_usage.is_a("IfcMaterialProfileSetUsage"):
if attributes.get("CardinalPoint", None):
attributes["CardinalPoint"] = int(attributes["CardinalPoint"])
ifcopenshell.api.run(
"material.edit_profile_usage",
self.file,
**{"usage": material_set_usage, "attributes": attributes},
usage=material_set_usage,
attributes=attributes,
)
layer_sets_to_regenerate = set()
for obj in objects:
obj_element = tool.Ifc.get_entity(obj)
obj_material_usage = ifcopenshell.util.element.get_material(obj_element)
if obj_material_usage and obj_material_usage.is_a("IfcMaterialLayerSetUsage"):
obj_material_usage.OffsetFromReferenceLine = material.OffsetFromReferenceLine
obj_material_usage.DirectionSense = material.DirectionSense
obj_material_usage.ReferenceExtent = material.ReferenceExtent
layer_sets_to_regenerate.add(obj_material_usage.ForLayerSet)
for layer_set in layer_sets_to_regenerate:
wall.DumbWallPlaner().regenerate_from_layer_set(layer_set)
slab.DumbSlabPlaner().regenerate_from_layer_set(layer_set)
bpy.ops.bim.disable_editing_assigned_material(obj=active_obj.name)
@@ -660,9 +673,9 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
material_set_item: bpy.props.IntProperty()
def execute(self, context):
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.mprops = context.scene.BIMMaterialProperties
self.mprops = tool.Material.get_material_props()
self.props = obj.BIMObjectMaterialProperties
self.props.active_material_set_item_id = self.material_set_item
@@ -704,9 +717,9 @@ class EditMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
props = obj.BIMObjectMaterialProperties
mprops = context.scene.BIMMaterialProperties
mprops = tool.Material.get_material_props()
element = tool.Ifc.get_entity(obj)
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
@@ -763,7 +776,7 @@ class ExpandMaterialCategory(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
props = context.scene.BIMMaterialProperties
props = tool.Material.get_material_props()
for index, category in (
(i, c)
for i, c in enumerate(props.materials)
@@ -792,7 +805,7 @@ class ContractMaterialCategory(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
props = context.scene.BIMMaterialProperties
props = tool.Material.get_material_props()
for index, category in (
(i, c)
for i, c in enumerate(props.materials)
@@ -812,7 +825,7 @@ class EnableEditingMaterialStyle(bpy.types.Operator):
material: bpy.props.IntProperty()
def execute(self, context):
props = bpy.context.scene.BIMMaterialProperties
props = tool.Material.get_material_props()
props.active_material_id = self.material
props.editing_material_type = "STYLE"
@@ -843,7 +856,7 @@ class EditMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMMaterialProperties
props = tool.Material.get_material_props()
ifc_file = tool.Ifc.get()
material = ifc_file.by_id(props.active_material_id)
style = ifc_file.by_id(int(props.styles))
@@ -867,7 +880,7 @@ class UnassignMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
context: bpy.props.IntProperty()
def _execute(self, context):
props = bpy.context.scene.BIMMaterialProperties
props = tool.Material.get_material_props()
material = tool.Ifc.get().by_id(props.materials[props.active_material_index].ifc_definition_id)
style = tool.Ifc.get().by_id(self.style)
context = tool.Ifc.get().by_id(self.context)
@@ -888,7 +901,7 @@ class SelectMaterialInMaterialsUI(bpy.types.Operator):
material_id: int
def execute(self, context):
props = bpy.context.scene.BIMMaterialProperties
props = tool.Material.get_material_props()
ifc_file = tool.Ifc.get()
material_id = self.material_id
material = ifc_file.by_id(material_id)
@@ -22,7 +22,6 @@ from ifcopenshell.util.doc import get_entity_doc
import bonsai.tool as tool
from bonsai.bim.module.material.data import MaterialsData, ObjectMaterialData
from bonsai.bim.module.profile.data import ProfileData
from bonsai.bim.ifc import IfcStore
from bonsai.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup
from bpy.props import (
@@ -121,7 +120,7 @@ def set_material_name(self: "Material", new_category_name: str) -> None:
material.Category = new_category_name
# Reload UI elements if necessary.
props: "BIMMaterialProperties" = self.id_data.BIMMaterialProperties
props = tool.Material.get_material_props()
new_category_name_already_in_use = bool(
next((m for m in props.materials if m.is_category and m.name == new_category_name), None)
)
+27 -17
View File
@@ -21,7 +21,6 @@ import bonsai.bim.helper
import bonsai.tool as tool
import bpy
from bpy.types import Panel, UIList
from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import draw_attributes
from bonsai.bim.helper import prop_with_search
from bonsai.bim.module.material.data import MaterialsData, ObjectMaterialData
@@ -29,7 +28,7 @@ from bonsai.bim.module.drawing.helper import format_distance
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.material.prop import Material
from bonsai.bim.module.material.prop import Material, BIMMaterialProperties
class BIM_PT_materials(Panel):
@@ -43,13 +42,13 @@ class BIM_PT_materials(Panel):
@classmethod
def poll(cls, context):
return IfcStore.get_file()
return tool.Ifc.get()
def draw(self, context):
if not MaterialsData.is_loaded:
MaterialsData.load()
self.props = context.scene.BIMMaterialProperties
self.props = tool.Material.get_material_props()
material = tool.Material.get_active_material_item()
material_id = material.ifc_definition_id if material else None
@@ -138,14 +137,14 @@ class BIM_PT_object_material(Panel):
def poll(cls, context):
if not tool.Blender.is_tab(context, "GEOMETRY"):
return False
if not context.active_object:
if not (obj := context.active_object):
return False
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
ifc_id = tool.Blender.get_ifc_definition_id(obj)
if not ifc_id:
return False
if not IfcStore.get_element(props.ifc_definition_id):
if not tool.Ifc.get_object_by_identifier(ifc_id):
return False
if not hasattr(IfcStore.get_file().by_id(props.ifc_definition_id), "HasAssociations"):
if not hasattr(tool.Ifc.get().by_id(ifc_id), "HasAssociations"):
return False
return True
@@ -153,10 +152,12 @@ class BIM_PT_object_material(Panel):
if not ObjectMaterialData.is_loaded:
ObjectMaterialData.load()
self.file = IfcStore.get_file()
self.oprops = context.active_object.BIMObjectProperties
self.props = context.active_object.BIMObjectMaterialProperties
self.mprops = context.scene.BIMMaterialProperties
obj = context.active_object
assert obj
self.file = tool.Ifc.get()
self.oprops = tool.Blender.get_object_bim_props(obj)
self.props = obj.BIMObjectMaterialProperties
self.mprops = tool.Material.get_material_props()
if not ObjectMaterialData.data["materials"]:
row = self.layout.row(align=True)
@@ -351,9 +352,10 @@ class BIM_PT_object_material(Panel):
if ObjectMaterialData.data["total_thickness"]:
total_thickness = ObjectMaterialData.data["total_thickness"]
unit_system = bpy.context.scene.unit_settings.system
props = tool.Drawing.get_document_props()
if unit_system == "IMPERIAL":
precision = bpy.context.scene.DocProperties.imperial_precision
precision = props.imperial_precision
else:
precision = None
formatted_thickness = format_distance(
@@ -401,10 +403,16 @@ class BIM_PT_object_material(Panel):
class BIM_UL_materials(UIList):
def draw_item(
self, context, layout: bpy.types.UILayout, data, item: Material, icon, active_data, active_propname
self,
context,
layout: bpy.types.UILayout,
data: BIMMaterialProperties,
item: Material,
icon,
active_data,
active_propname,
) -> None:
mprops = context.scene.BIMMaterialProperties
material_type = mprops.material_type
material_type = data.material_type
if item:
row = layout.row(align=True)
@@ -425,6 +433,8 @@ class BIM_UL_materials(UIList):
row.prop(item, "name", text="", emboss=False)
else:
row.label(text="", icon="BLANK1")
if item.ifc_definition_id == data.active_material_id:
row.label(text="", icon="GREASEPENCIL")
if material_type == "IfcMaterialList":
row.label(text=item.name, icon="MATERIAL")
else:
@@ -257,7 +257,7 @@ class DrawSystemArrows(bpy.types.Operator, tool.Ifc.Operator):
sources = []
for obj in bpy.context.selected_objects:
if not obj.BIMObjectProperties.ifc_definition_id:
if not tool.Blender.get_ifc_definition_id(obj):
continue
element = tool.Ifc.get_entity(obj)
@@ -299,7 +299,7 @@ class DrawSystemArrows(bpy.types.Operator, tool.Ifc.Operator):
tool.Blender.select_and_activate_single_object(context, curve)
def get_absolute_matrix(self, matrix):
props = bpy.context.scene.BIMGeoreferenceProperties
props = tool.Georeference.get_georeference_props()
if props.has_blender_offset:
matrix = np.array(
ifcopenshell.util.geolocation.global2local(
+13 -7
View File
@@ -72,7 +72,9 @@ class DisableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
context.active_object.BIMArrayProperties.is_editing = -1
obj = context.active_object
assert obj
tool.Model.get_array_props(obj).is_editing = -1
return {"FINISHED"}
@@ -84,8 +86,9 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = context.active_object
assert obj
element = tool.Ifc.get_entity(obj)
props = obj.BIMArrayProperties
props = tool.Model.get_array_props(obj)
relating_obj = props.relating_array_object
@@ -119,7 +122,7 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMArrayProperties
props = tool.Model.get_array_props(obj)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
@@ -182,7 +185,7 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMArrayProperties
props = tool.Model.get_array_props(obj)
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
data = json.loads(pset["Data"])
@@ -302,7 +305,8 @@ class Input3DCursorXArray(bpy.types.Operator):
def execute(self, context):
obj = context.active_object
props = obj.BIMArrayProperties
assert obj
props = tool.Model.get_array_props(obj)
cursor = context.scene.cursor
if props.use_local_space:
props.x = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.translation).x
@@ -318,7 +322,8 @@ class Input3DCursorYArray(bpy.types.Operator):
def execute(self, context):
obj = context.active_object
props = obj.BIMArrayProperties
assert obj
props = tool.Model.get_array_props(obj)
cursor = context.scene.cursor
if props.use_local_space:
props.y = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.translation).y
@@ -334,7 +339,8 @@ class Input3DCursorZArray(bpy.types.Operator):
def execute(self, context):
obj = context.active_object
props = obj.BIMArrayProperties
assert obj
props = tool.Model.get_array_props(obj)
cursor = context.scene.cursor
if props.use_local_space:
props.z = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.translation).z
+62 -74
View File
@@ -26,7 +26,7 @@ from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc
import bonsai.tool as tool
from math import degrees
from natsort import natsorted
from typing import Union
from typing import Union, Optional, Any
def refresh():
@@ -43,31 +43,25 @@ def refresh():
class AuthoringData:
data = {}
type_thumbnails = {}
type_thumbnails: dict[int, int] = {}
types_per_page = 9
ifc_element_type = None
is_loaded = False
@classmethod
def load(cls, ifc_element_type=None):
def load(cls, ifc_element_type: Optional[str] = None):
cls.is_loaded = True
cls.props = bpy.context.scene.BIMModelProperties
if ifc_element_type:
cls.ifc_element_type = None if ifc_element_type == "all" else ifc_element_type
cls.props = tool.Model.get_model_props()
cls.data["default_container"] = cls.default_container()
cls.data["ifc_element_type"] = cls.ifc_element_type
cls.data["ifc_element_type"] = ifc_element_type
cls.data["ifc_classes"] = cls.ifc_classes()
cls.data["ifc_class_current"] = cls.ifc_class_current()
# Make sure .ifc_classes() was run before next lines
cls.data["type_elements"] = cls.type_elements()
cls.data["type_elements_filtered"] = cls.type_elements_filtered()
# After .type_elements().
cls.data["relating_type_id"] = cls.relating_type_id()
# Make sure .relating_type_id() was run before next lines
cls.data["relating_type_id_current"] = cls.relating_type_id_current()
cls.data["relating_type_name"] = cls.relating_type_name()
cls.data["relating_type_description"] = cls.relating_type_description()
cls.data["relating_type_material_usage"] = cls.relating_type_material_usage()
cls.data["predefined_type"] = cls.predefined_type()
cls.data["relating_type_data"] = cls.relating_type_data()
# Make sure .type_elements_filtered() was run before next lines
cls.data["total_types"] = cls.total_types()
cls.data["total_pages"] = cls.total_pages() # Only after .total_types()
@@ -75,7 +69,6 @@ class AuthoringData:
cls.data["prev_page"] = cls.prev_page()
cls.data["paginated_relating_types"] = cls.paginated_relating_types()
cls.data["type_thumbnail"] = cls.type_thumbnail() # Only after .relating_type_id_current()
cls.data["materials"] = cls.materials()
cls.data["is_voidable_element"] = cls.is_voidable_element()
cls.data["has_visible_openings"] = cls.has_visible_openings()
@@ -97,7 +90,7 @@ class AuthoringData:
@classmethod
def default_container(cls) -> str | None:
props = bpy.context.scene.BIMSpatialDecompositionProperties
props = tool.Spatial.get_spatial_props()
if props.default_container:
try:
return tool.Ifc.get().by_id(props.default_container).Name
@@ -112,10 +105,6 @@ class AuthoringData:
version = tool.Ifc.get_schema()
return [(c, c, get_entity_doc(version, c).get("description", "")) for c in sorted(names)]
@classmethod
def type_thumbnail(cls):
return cls.type_thumbnails.get(int(cls.data["relating_type_id_current"] or 0), 0)
@classmethod
def materials(cls):
results = [("0", "None", "No material")]
@@ -153,7 +142,7 @@ class AuthoringData:
def type_elements_filtered(cls):
search_query = cls.props.search_name.lower()
def filter_element(element):
def filter_element(element: ifcopenshell.entity_instance) -> bool:
if search_query in (element.Name or "Unnamed").lower():
return True
if search_query in (element.Description or "").lower():
@@ -173,21 +162,24 @@ class AuthoringData:
elements = cls.data["type_elements_filtered"]
elements = elements[(cls.props.type_page - 1) * cls.types_per_page : cls.props.type_page * cls.types_per_page]
for element in elements:
predefined_type = ifcopenshell.util.element.get_predefined_type(element)
if predefined_type == "NOTDEFINED":
predefined_type = None
results.append(
{
"id": element.id(),
"ifc_class": element.is_a(),
"name": element.Name or "Unnamed",
"description": element.Description or "No Description",
"predefined_type": predefined_type,
"icon_id": cls.type_thumbnails.get(element.id(), None) or 0,
}
)
results.append(cls.get_type_data(element))
return results
@classmethod
def get_type_data(cls, element: ifcopenshell.entity_instance) -> dict[str, Any]:
predefined_type = ifcopenshell.util.element.get_predefined_type(element)
if predefined_type == "NOTDEFINED":
predefined_type = None
data = {
"id": element.id(),
"ifc_class": element.is_a(),
"name": element.Name or "Unnamed",
"description": element.Description or "No Description",
"predefined_type": predefined_type,
"icon_id": cls.type_thumbnails.get(element.id(), 0),
}
return data
@classmethod
def is_voidable_element(cls):
if active_object := tool.Blender.get_active_object():
@@ -316,38 +308,16 @@ class AuthoringData:
return [(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in elements]
@classmethod
def relating_type_id_current(cls):
def relating_type_data(cls) -> dict[str, Any]:
relating_type_id = tool.Blender.get_enum_safe(cls.props, "relating_type_id")
relating_type_id_data = cls.data["relating_type_id"]
if not relating_type_id and relating_type_id_data:
relating_type_id = relating_type_id_data[0][0]
return relating_type_id
@classmethod
def relating_type_name(cls):
if relating_type_id := cls.data["relating_type_id_current"]:
return tool.Ifc.get().by_id(int(relating_type_id)).Name or "Unnamed"
@classmethod
def relating_type_description(cls):
if relating_type_id := cls.data["relating_type_id_current"]:
return tool.Ifc.get().by_id(int(relating_type_id)).Description or "No description"
@classmethod
def relating_type_material_usage(cls):
if relating_type_id := cls.data["relating_type_id_current"]:
return tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id)))
@classmethod
def predefined_type(cls):
relating_type_id = tool.Blender.get_enum_safe(cls.props, "relating_type_id")
if relating_type_id is None:
return
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
if not hasattr(relating_type, "PredefinedType"):
return
predefined_type = relating_type.PredefinedType
return predefined_type
if not relating_type_id_data:
return {}
relating_type_id = relating_type_id_data[0][0]
ifc_file = tool.Ifc.get()
relating_type = ifc_file.by_id(int(relating_type_id))
return cls.get_type_data(relating_type)
@classmethod
def selected_material_usages(cls):
@@ -415,7 +385,9 @@ class StairData:
@classmethod
def general_params(cls):
props = bpy.context.active_object.BIMStairProperties
obj = bpy.context.active_object
assert obj
props = tool.Model.get_stair_props(obj)
data = cls.data["pset_data"]["data_dict"]
general_params = {}
general_props = props.get_props_kwargs(stair_type=data["stair_type"])
@@ -481,7 +453,9 @@ class WindowData:
@classmethod
def general_params(cls):
props = bpy.context.active_object.BIMWindowProperties
obj = bpy.context.active_object
assert obj
props = tool.Model.get_window_props(obj)
data = cls.data["pset_data"]["data_dict"]
general_params = {}
general_props = props.get_general_kwargs()
@@ -492,7 +466,9 @@ class WindowData:
@classmethod
def lining_params(cls):
props = bpy.context.active_object.BIMWindowProperties
obj = bpy.context.active_object
assert obj
props = tool.Model.get_window_props(obj)
data = cls.data["pset_data"]["data_dict"]
lining_data = data["lining_properties"]
lining_params = {}
@@ -504,7 +480,9 @@ class WindowData:
@classmethod
def panel_params(cls):
props = bpy.context.active_object.BIMWindowProperties
obj = bpy.context.active_object
assert obj
props = tool.Model.get_window_props(obj)
panel_data = cls.data["pset_data"]["data_dict"]["panel_properties"]
panel_params = {}
panel_props = props.get_panel_kwargs()
@@ -535,7 +513,9 @@ class DoorData:
@classmethod
def general_params(cls):
props = bpy.context.active_object.BIMDoorProperties
obj = bpy.context.active_object
assert obj
props = tool.Model.get_door_props(obj)
data = cls.data["pset_data"]["data_dict"]
general_params = {}
general_props = props.get_general_kwargs()
@@ -546,7 +526,9 @@ class DoorData:
@classmethod
def lining_params(cls):
props = bpy.context.active_object.BIMDoorProperties
obj = bpy.context.active_object
assert obj
props = tool.Model.get_door_props(obj)
data = cls.data["pset_data"]["data_dict"]
lining_data = data["lining_properties"]
lining_params = {}
@@ -558,7 +540,9 @@ class DoorData:
@classmethod
def panel_params(cls):
props = bpy.context.active_object.BIMDoorProperties
obj = bpy.context.active_object
assert obj
props = tool.Model.get_door_props(obj)
data = cls.data["pset_data"]["data_dict"]
panel_data = cls.data["pset_data"]["data_dict"]["panel_properties"]
panel_params = {}
@@ -589,7 +573,9 @@ class RailingData:
@classmethod
def general_params(cls):
props = bpy.context.active_object.BIMRailingProperties
obj = bpy.context.active_object
assert obj
props = tool.Model.get_railing_props(obj)
data = cls.data["pset_data"]["data_dict"]
general_params = {}
general_props = props.get_general_kwargs(railing_type=data["railing_type"])
@@ -623,7 +609,9 @@ class RoofData:
@classmethod
def general_params(cls):
props = bpy.context.active_object.BIMRoofProperties
obj = bpy.context.active_object
assert obj
props = tool.Model.get_roof_props(obj)
data = cls.data["pset_data"]["data_dict"]
general_params = {}
general_props = props.get_general_kwargs(generation_method=data["generation_method"])
@@ -656,19 +644,19 @@ class ItemData:
@classmethod
def representation_identifier(cls):
props = bpy.context.scene.BIMGeometryProperties
props = tool.Geometry.get_geometry_props()
rep = tool.Geometry.get_active_representation(props.representation_obj)
return rep.RepresentationIdentifier
@classmethod
def representation_type(cls):
props = bpy.context.scene.BIMGeometryProperties
props = tool.Geometry.get_geometry_props()
rep = tool.Geometry.get_active_representation(props.representation_obj)
return rep.RepresentationType
@classmethod
def representation_usage(cls):
props = bpy.context.scene.BIMGeometryProperties
props = tool.Geometry.get_geometry_props()
return tool.Model.get_usage_type(tool.Ifc.get_entity(props.representation_obj))
@classmethod
@@ -695,6 +695,8 @@ class PolylineDecorator:
self.draw_batch("LINES", mouse_point + projection_point, decorator_color_unselected, edges)
if axis1 and axis2:
axis1 = [tuple(tool.Polyline.use_transform_orientations(Vector(v))) for v in axis1]
axis2 = [tuple(tool.Polyline.use_transform_orientations(Vector(v))) for v in axis2]
self.line_shader.uniform_float("lineWidth", 1.5)
self.draw_batch("LINES", axis1, highlight_color(axis_color1), [(0, 1)])
self.draw_batch("LINES", axis2, highlight_color(axis_color2), [(0, 1)])
@@ -829,7 +831,7 @@ class ProductDecorator:
polyline_points = polyline_data[0].polyline_points if polyline_data else []
self.relating_type = None
props = context.scene.BIMModelProperties
props = tool.Model.get_model_props()
relating_type_id = props.relating_type_id
if relating_type_id:
self.relating_type = tool.Ifc.get().by_id(int(relating_type_id))
+39 -80
View File
@@ -41,7 +41,7 @@ V_ = tool.Blender.V_
def update_door_modifier_representation(obj: bpy.types.Object) -> None:
props = obj.BIMDoorProperties
props = tool.Model.get_door_props(obj)
element = tool.Ifc.get_entity(obj)
ifc_file = tool.Ifc.get()
sliding_door = "SLIDING" in props.door_type
@@ -73,7 +73,7 @@ def update_door_modifier_representation(obj: bpy.types.Object) -> None:
},
}
previously_active_context = tool.Geometry.get_active_representation_context(obj)
active_context = tool.Geometry.get_active_representation_context(obj)
# ELEVATION_VIEW representation
profile = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Profile", "ELEVATION_VIEW")
@@ -91,47 +91,22 @@ def update_door_modifier_representation(obj: bpy.types.Object) -> None:
representation_data["part_of_product"] = ifcopenshell.util.representation.get_part_of_product(element, body)
model_representation = ifcopenshell.api.run("geometry.add_door_representation", ifc_file, **representation_data)
representation_data["part_of_product"] = None
if fallback_material := (int(props.lining_material) or int(props.panel_material)):
lining_material = tool.Ifc.get().by_id(int(props.lining_material) or fallback_material)
panel_material = tool.Ifc.get().by_id(int(props.panel_material) or fallback_material)
should_create_new_material_set = False
if material := ifcopenshell.util.element.get_material(element):
if (
material.is_a("IfcMaterialConstituent")
and len(names := [c.Name for c in material.MaterialConstituents]) == 2
and set(names) == {"Lining", "Framing"}
):
should_create_new_material_set = False
else:
should_create_new_material_set = True
ifcopenshell.api.material.unassign_material(ifc_file, products=[element])
if not material.is_a("IfcMaterial") and not ifc_file.get_total_inverses(material):
ifcopenshell.api.material.remove_material_set(ifc_file, material=material)
else:
should_create_new_material_set = True
if should_create_new_material_set:
material_set = ifcopenshell.api.material.add_material_set(ifc_file, set_type="IfcMaterialConstituentSet")
ifcopenshell.api.material.add_constituent(
ifc_file, constituent_set=material_set, material=lining_material, name="Lining"
)
ifcopenshell.api.material.add_constituent(
ifc_file, constituent_set=material_set, material=panel_material, name="Framing"
)
ifcopenshell.api.material.assign_material(ifc_file, products=[element], material=material_set)
styles = {
"Lining": ifcopenshell.util.representation.get_material_style(lining_material, body),
"Framing": ifcopenshell.util.representation.get_material_style(panel_material, body),
}
for item in model_representation.Items:
if aspect := ifcopenshell.util.representation.get_item_shape_aspect(model_representation, item):
if style := styles.get(aspect.Name, None):
ifcopenshell.api.style.assign_item_style(ifc_file, item=item, style=style)
tool.Model.replace_object_ifc_representation(body, obj, model_representation)
if fallback_material := (int(props.lining_material) or int(props.framing_material) or int(props.glazing_material)):
ifcopenshell.api.material.set_shape_aspect_constituents(
ifc_file,
element=element,
context=body,
materials={
"Lining": tool.Ifc.get().by_id(int(props.lining_material) or fallback_material),
"Framing": tool.Ifc.get().by_id(int(props.framing_material) or fallback_material),
"Glazing": tool.Ifc.get().by_id(int(props.glazing_material) or fallback_material),
},
)
elif material := ifcopenshell.util.element.get_material(element):
ifcopenshell.api.material.unassign_material(ifc_file, products=[element])
if not material.is_a("IfcMaterial") and not ifc_file.get_total_inverses(material):
ifcopenshell.api.material.remove_material_set(ifc_file, material=material)
tool.Model.replace_object_ifc_representation(body, obj, model_representation)
# Body/PLAN_VIEW representation
plan_body = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "Body", "PLAN_VIEW")
@@ -163,38 +138,15 @@ def update_door_modifier_representation(obj: bpy.types.Object) -> None:
)
tool.Model.replace_object_ifc_representation(plan_annotation, obj, plan_representation)
# adding switch representation at the end instead of changing order of representations
# to prevent #2744
if tool.Geometry.get_active_representation_context(obj) != previously_active_context:
previously_active_representation = ifcopenshell.util.representation.get_representation(
element,
previously_active_context.ContextType,
previously_active_context.ContextIdentifier,
previously_active_context.TargetView,
)
if not previously_active_representation:
# we assume there is no representation because it was
# Plan/Annotation/PLAN_VIEW
previously_active_context = ifcopenshell.util.representation.get_context(
ifc_file, "Plan", "Body", "PLAN_VIEW"
)
previously_active_representation = ifcopenshell.util.representation.get_representation(
element,
previously_active_context.ContextType,
previously_active_context.ContextIdentifier,
previously_active_context.TargetView,
)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=previously_active_representation,
should_reload=True,
is_global=True,
should_sync_changes_first=True,
)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=ifcopenshell.util.representation.get_representation(element, active_context),
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
# type attributes
if tool.Ifc.get_schema() != "IFC2X3":
@@ -338,7 +290,8 @@ def create_bm_door_lining(
def update_door_modifier_bmesh(context: bpy.types.Context) -> None:
obj = context.active_object
props = obj.BIMDoorProperties
assert obj
props = tool.Model.get_door_props(obj)
overall_width = props.overall_width
overall_height = props.overall_height
@@ -497,7 +450,6 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None:
side_lining_thickness,
transom_thickness,
]
window_lining_thickness.append(transom_thickness)
window_lining_size = V_(overall_width, lining_depth, window_lining_height)
window_position = V_(0, 0, overall_height - window_lining_height)
frame_size = V_(door_opening_width, frame_depth, frame_height)
@@ -572,9 +524,10 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Add a Parametric Door to the Selected IFC Door Elements"
bl_options = {"REGISTER", "UNDO"}
def add_door_on_object(self, obj):
def add_door_on_object(self, obj: bpy.types.Object) -> None:
element = tool.Ifc.get_entity(obj)
props = obj.BIMDoorProperties
assert element
props = tool.Model.get_door_props(obj)
door_data = props.get_general_kwargs(convert_to_project_units=True)
lining_props = props.get_lining_kwargs(convert_to_project_units=True)
@@ -608,11 +561,12 @@ class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Cancel Editing Door on Selected Objects"
bl_options = {"REGISTER", "UNDO"}
def cancel_editing_door_on_object(self, obj):
def cancel_editing_door_on_object(self, obj: bpy.types.Object) -> None:
element = tool.Ifc.get_entity(obj)
assert element
if not tool.Blender.Modifier.is_door(element):
return
props = obj.BIMDoorProperties
props = tool.Model.get_door_props(obj)
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Door", "Data"))
data.update(data.pop("lining_properties"))
data.update(data.pop("panel_properties"))
@@ -646,9 +600,10 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
def finish_editing_door_on_object(self, obj):
element = tool.Ifc.get_entity(obj)
assert element
if not tool.Blender.Modifier.is_door(element):
return
props = obj.BIMDoorProperties
props = tool.Model.get_door_props(obj)
door_data = props.get_general_kwargs(convert_to_project_units=True)
lining_props = props.get_lining_kwargs(convert_to_project_units=True)
@@ -678,12 +633,14 @@ class EnableEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
def edit_door_on_obj(self, obj):
element = tool.Ifc.get_entity(obj)
assert element
if not tool.Blender.Modifier.is_door(element):
return
props = obj.BIMDoorProperties
props = tool.Model.get_door_props(obj)
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Door", "Data"))
data.update(data.pop("lining_properties"))
data.update(data.pop("panel_properties"))
data.update(tool.Model.get_constituents_props_data(element))
# required since we could load pset from .ifc and BIMDoorProperties won't be set
props.set_props_kwargs_from_ifc_data(data)
@@ -702,9 +659,11 @@ class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
def remove_door_on_object(self, obj):
element = tool.Ifc.get_entity(obj)
assert element
if not tool.Blender.Modifier.is_door(element):
return
obj.BIMDoorProperties.is_editing = False
props = tool.Model.get_door_props(obj)
props.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)

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