Compare commits

..

576 Commits

Author SHA1 Message Date
Andrej730 05a3004ef0 test 2025-04-01 19:06:44 +05:00
Andrej730 fe5afaf172 black . 2025-04-01 18:42:47 +05:00
falken10vdl 0039f376ca Update operator.py so argv parameters in Windows are correctly parsed
Currently teh parameters that have spaces are not properly parsed when invoking os.execv.

Ej:
We start blender with some parameters and then invoke bim.restart_blender:
C:\Users\falke>c:\Users\falke\Documents\blender-4.2.8-windows-x64\blender.exe --debug --python-expr "import bpy; bpy.ops.bim.load_project(filepath='C:/Users/falke/Documents/bonsaiDevel/Testing.ifc', should_start_fresh_session=True, use_detailed_tooltip=True)"
TBBmalloc: skip allocation functions replacement in ucrtbase.dll: unknown prologue for function _msize
Switching to fully guarded memory allocator.
Blender 4.2.8 LTS
Build: 2025-03-18 01:34:37 Windows Release
argv[0] = c:\Users\falke\Documents\blender-4.2.8-windows-x64\blender.exe
argv[1] = --debug
argv[2] = --python-expr
argv[3] = import bpy; bpy.ops.bim.load_project(filepath='C:/Users/falke/Documents/bonsaiDevel/Testing.ifc', should_start_fresh_session=True, use_detailed_tooltip=True)
Starting import process :: 0.00
Loading file :: 0.00
Calculate unit scale :: 0.00
Process context filter :: 0.01
Calculate model offset :: 0.00
Set units :: 0.00
Create project :: 0.00
Process element filter :: 0.00
Create styles :: 0.00
Parsing native elements :: 0.00
Done creating geometry
Create native elements :: 0.00
Done creating geometry
Create elements :: 0.03
Create annotations :: 0.00
Create positioning elements :: 0.00
0 / 3 elements processed ...
Create spatial elements :: 0.00
Create structural items :: 0.00
Create element types :: 0.00
Place objects in collections :: 0.00
Setup arrays :: 0.00
Load linked models :: 0.00
Add project to scene :: 0.00
Setting default context :: 0.01
Import finished in 0.06 seconds
No MySQL support
Writing userprefs: "C:\Users\falke\AppData\Roaming\Blender Foundation\Blender\4.2\config\userpref.blend" ok
Info: Preferences saved
TBBmalloc: skip allocation functions replacement in ucrtbase.dll: unknown prologue for function _msize
Switching to fully guarded memory allocator.
Blender 4.2.8 LTS
Build: 2025-03-18 01:34:37 Windows Release
argv[0] = c:\Users\falke\Documents\blender-4.2.8-windows-x64\blender.exe
argv[1] = --debug
argv[2] = --python-expr
argv[3] = import
argv[4] = bpy;
argv[5] = bpy.ops.bim.load_project(filepath='C:/Users/falke/Documents/bonsaiDevel/Testing.ifc',
argv[6] = should_start_fresh_session=True,
argv[7] = use_detailed_tooltip=True)

C:\Users\falke>  File "<string>", line 1
    import
          ^
SyntaxError: invalid syntax
Error: Cannot read file "C:\Users\falke\bpy;": No such file or directory
Error: argument has no '.blend' file extension, not using as new file, exiting! C:\Users\falke\bpy;

Blender quit

By adding the proposed list2cmdline based solution, the commandline is properly formated before invoking os.execv

Thanks!
2025-04-01 18:09:25 +11:00
Dion Moult c307871d37 Fix #6472. Typo in IfcTester test cases. 2025-04-01 17:56:51 +11:00
Andrej730 31c9f60bca Fix errors creating objects if default container is hidden #6456 2025-04-01 11:32:14 +05:00
Dion Moult 93e7d92076 Fix bug where removing a context (used by representations) left invalid representations 2025-04-01 17:28:22 +11:00
Dion Moult 16f09655b8 Bump version numbers in dev scripts 2025-04-01 14:21:34 +11:00
Dion Moult b6115a6d71 Fix bug where adding objects relying on missing contexts would fail 2025-04-01 14:20:56 +11:00
Richard Brice f0b5e93737 Fixes problem evaluating gradient and segmented reference curves with start locations offset from start of base curve. 2025-03-31 16:18:05 -07:00
Andrej730 9895302c9d bim.add_boundary - add description/error reports
Example description - https://i.imgur.com/jn3y2KU.png

fyi @steverugi
2025-03-31 19:10:21 +05:00
Andrej730 8f5f346fa4 boundary/create_mesh - small optimizations 2025-03-31 19:10:21 +05:00
Andrej730 6adf5d957a Fix error adding a space boundary (a7509db) 2025-03-31 19:10:21 +05:00
Andrej730 e6be79f2ad assign/unassign type operators - add descriptions 2025-03-31 19:10:20 +05:00
Andrej730 e3228513c0 typing 2025-03-31 19:10:20 +05:00
Andrej730 547506d626 black . 2025-03-31 19:10:20 +05:00
Andrej730 3fe29fa842 Prevent user from trying to assign material style if not style is selected
Possibly explains #6462
2025-03-31 14:07:41 +05:00
Andrej730 e18e2caea8 Bonsai - exclude selected non-ifcobjects from type assignment/unsassignment #6454 2025-03-31 14:07:41 +05:00
Andrej730 1cac12dc53 Use fallback elevation value if default container is missing #6470
I guess it's always theoretically possible that user end up without a default container.
2025-03-31 13:49:18 +05:00
Andrej730 8d530eba68 Fallback to the aggregate if default container is removed #6470 2025-03-31 13:49:18 +05:00
Andrej730 4593e5aea6 Fix ExportHelper not optional filename_ext #6463 (4594f59) 2025-03-31 12:51:37 +05:00
Andrej730 cba6161043 Fix #6467 (d35a2dc) 2025-03-31 12:05:15 +05:00
Richard Brice 42c49af4e3 Implements business to geometry mapping for alignment cant.
Also fixes problems that were created by this work in other files
2025-03-29 15:30:04 -07:00
Andrej730 7bdbfee041 Fix bim.export_cost_schedules #6463 (4594f59) 2025-03-29 23:36:37 +05:00
Thomas Krijnen f37bda2d7c Add hybrid-cgal-simple-opencascade to CLI help 2025-03-28 20:43:17 +01:00
Andrej730 0bf065c4bd Fix issue editing item layer when it had a layer previously (13a182c) 2025-03-28 17:59:08 +05:00
Andrej730 c58b84ddb5 append_asset - fix duplicated applications/organizations/pao
Noticed when starting new project with demo library it had 29 duplicated IfcApplications...
2025-03-28 17:59:07 +05:00
Andrej730 81b5fc30c7 template_file - add a note 2025-03-28 17:59:07 +05:00
Andrej730 fd423f21e2 ifcopenshell.validate - validate IfcApplications 2025-03-28 17:59:07 +05:00
Andrej730 4afed8f2f3 typing 2025-03-28 17:59:07 +05:00
Thomas Krijnen 9254601924 Underscores in geom setting to argparse namespace 2025-03-28 12:11:27 +01:00
Thomas Krijnen e9460a2c9a Actually Eigen::Quaterniond arg order is *w* xyz 2025-03-28 12:10:03 +01:00
Andrej730 f08f9ad310 export_mesh_to_tessellation to consider empty material slots #6453 2025-03-28 12:01:09 +05:00
Andrej730 69ddf52ff0 Add ifc2x3/ifc4/ifc4x3 demo templates as symlinks to repo
Example - https://i.imgur.com/d4udENs.png
2025-03-28 10:29:31 +05:00
Andrej730 2fd02e281d Add IFC2X3, IFC4X3 demo libraries 2025-03-27 19:31:04 +05:00
Andrej730 fdb283b7af generate_demo_library - sync guid on regeneration 2025-03-27 19:31:04 +05:00
Andrej730 013d7f6659 generate_demo_library - typing and small refactor 2025-03-27 19:31:03 +05:00
Andrej730 5b33c6018a spatial decomposition ui - use poll_message_set instead of .enabled
To give user more descriptive message about what's wrong.
2025-03-27 19:31:03 +05:00
Andrej730 50324a566f black . 2025-03-27 19:31:03 +05:00
Andrej730 64b8842055 typing 2025-03-27 19:29:45 +05:00
Massimo Fabbro 05be0f69bd Fix #6305
Fix wrong calculation with blender engine for IfcFooting
2025-03-26 22:30:56 +01:00
Andrej730 3f823573cf Fix broken scroll bar in some UI lists #6439
Just found that all scroll bars for UIList kept in sync using `list_id` - it's that second argument for `UILayout.template_list` that I forgot exist, since I've never seen set it to anything besides the empty string.

So in #6439 we were reusing same UIList for multiple collection props and since one of the props was empty, it was constantly resetting scroll bar position.

Was about to report it as a bug to Blender but stumbled upon similar issue https://projects.blender.org/blender/blender/issues/124364

So the solution is make sure each UIList has unique `list_id`.

Fyi @Moult @Gorgious56
2025-03-26 18:49:22 +05:00
Andrej730 a3c5c0ce43 Fix typo in 64f51d7 2025-03-26 18:49:21 +05:00
Andrej730 2e61532919 bim.unassign_material - add description 2025-03-26 18:49:21 +05:00
Andrej730 c3107e37c0 material.remove_material_set to remove usages
To prevent them from being invalidated.

Noticed this as a possible issue investigating https://community.osarch.org/discussion/2820/error-message-in-bonsai-bim
2025-03-26 18:49:21 +05:00
Andrej730 c85f9f02ba Check material override explicitly, not just by name
It was inconsistent when it was comparing usage with unnamed layer set and comparing usage with named layer set.
2025-03-26 18:49:21 +05:00
Andrej730 f888a32fa4 reuse get_material_name 2025-03-26 18:49:21 +05:00
Andrej730 d35a2dcf5b typing 2025-03-26 18:49:21 +05:00
Arjo Nagelhout 244320dd49 add virtual destructor to AbstractKernel base class
this fixes a SIGTRAP when destructing the geometry iterator, as that calls delete kernel_.
2025-03-25 20:11:39 +01:00
Andrej730 0d7439c7a5 ifcopenshell.validate to catch invalid entities used in entities attributes #6409
Example

```
For instance:
    # 1 =IFCCARTESIANPOINT((5.,5.,nan.));
Entity with name 'nan.' not found in schema 'IFC4'
```
2025-03-25 18:15:37 +05:00
Thomas Krijnen bd92821b2e Add file offset to instance reference errors so that they are picked up by ifcopenshell.validate 2025-03-25 11:44:46 +01:00
Thomas Krijnen 3744b81ccd Only accept finite floats #6409 2025-03-25 10:15:24 +01:00
Andrej730 9311f50206 Fix for 4594f596c2
Traceback:
```
Error: Python: Traceback (most recent call last):
  File "\bonsai\bim\module\project\operator.py", line 127, in execute
    self._execute(context)
  File "\bonsai\bim\module\project\operator.py", line 143, in _execute
    core.create_project(
  File "\bonsai\core\project.py", line 108, in create_project
    project.append_all_types_from_template(template)
  File "\bonsai\tool\project.py", line 55, in append_all_types_from_template
    bpy.ops.bim.select_library_file(filepath=filepath.__str__())
  File "\Blender\4.4\scripts\modules\bpy\ops.py", line 109, in __call__
    ret = _op_call(self.idname_py(), kw)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: Converting py args to operator properties:: keyword "filepath" unrecognized
```
2025-03-25 12:00:50 +05:00
Thomas Krijnen eec8c88e6f Unique ptrs #6417 2025-03-24 14:09:00 +01:00
shmoodyyy dd53f9b494 style: keep tab indenting consistency 2025-03-24 13:59:58 +01:00
shmoodyyy c6e3ac4155 fix: memory leak from missing deletes of raw heap allocations; big issue within create_shape() python lib 2025-03-24 13:59:58 +01:00
Andrej730 4594f596c2 Reuse ImportHelper/ExportHelper everywhere possible for consistency 2025-03-24 17:36:27 +05:00
Andrej730 4f091af935 bim.export_ifccsv - clarify description and title 2025-03-24 17:36:27 +05:00
Andrej730 2f34f997dd Fix api.alignment missing __all__ 2025-03-24 17:36:27 +05:00
Andrej730 1600e00295 Fix clash smart groups selection (6660ee3)
It was using prop `guid` which doesn't exist , so `by_guid(global_id.guid)` would always fail with some exception.
2025-03-24 17:36:26 +05:00
Andrej730 7a55e837f7 typing 2025-03-24 17:36:26 +05:00
Andrej730 595ee3fa47 Provide descriptions to all operators that use fileselect_add #6420
I guess all it takes a persistent Blender crash to fill up operators descriptions 😅
2025-03-24 11:36:40 +05:00
Andrej730 c84f8e55a6 Fix typo 2025-03-24 11:36:40 +05:00
Andrej730 b63d9090a1 black . 2025-03-24 11:36:40 +05:00
Dion Moult 58c7976a5e See #1227. Fix regression where new wall system didn't preserve existing dimensions when regenerating geometry for an object that previously didn't have a layerset 2025-03-23 23:55:15 +11:00
Dion Moult 384a74734e Fix regression in split along edge
Aren't tests great
2025-03-23 23:36:19 +11:00
Dion Moult b269a806eb Improve description for dev setup
Be more dogmatic in the setup to prevent confusion.
2025-03-23 23:31:04 +11:00
Bruno Postle f43e666583 git colourisation fixes
Fix bug where uncommitted changes were not fully colourised in the same
way as diffs. Also try and catch more changes, eg. highlight if a
Products Type has changed.
2025-03-23 12:07:41 +00:00
Dion Moult 26b21ddcb3 Fix #6414. Allow extracting elements from non-standard IFC schemas. 2025-03-23 22:16:42 +11:00
Dion Moult f1138d1aac Fix regression in point cloud loading in refactoring the new item mode. 2025-03-23 21:55:57 +11:00
Dion Moult 51eef295c8 Fix models with crazy contexts (which kills the iterator which is context based) 2025-03-23 21:12:46 +11:00
Massimo Fabbro 212da8b6d6 fix #6271
Fix wrong length calculation with Blender Engine for IfcDoor and IfcWindows
2025-03-23 07:40:04 +01:00
Thomas Krijnen f7163b8db8 schema -> schema_identifier #6414 2025-03-22 19:27:50 +01:00
Ryan Schultz 9ee947b730 a better implementation for #3731 - to round to the nearest foot. 2025-03-22 09:53:15 -05:00
Ryan Schultz 328fb8bc0e fix #3731 - round to the nearest foot 2025-03-22 09:11:41 -05:00
Bruno Postle 75fe548bb0 Remove zombie method reintroduced in 5830573
edit_container_attributes() was deleted in e79a85a, but somehow got
dragged back in
2025-03-22 10:17:41 +00:00
Bruno Postle 5830573cda Create transform orientation slots (#6215)
Setting the default container switches to an appropriate orientation
slot if the orientation of this container isn't global. eg. if a building is
rotated, selecting to work in a storey will set the orientation to the
building orientation. Closes #6128
2025-03-22 09:01:54 +00:00
Bruno Postle 775106d0a0 Place drawings on sheets
When adding a drawing to a sheet, place to the right of the last drawing
if it fits on the sheet - otherwise start a new row below all existing
drawings.

This means that new drawings are no longer just piled up on top of each
other at the top-left, but it does mean that new rows of drawings are
added below the title box when the sheet is full.
2025-03-21 23:37:30 +00:00
sebjf 25b7003d2d Issue 6385 - Delete IfcGeom::Elements as soon as possible (#6411)
* ISSUE #6385 Delete iterator elements as soon as they are moved past

* ISSUE #6385 added loop to destructor to clean up initialised elements not yet disposed of by the iterator
2025-03-21 18:52:40 +01:00
Ryan Schultz c80c1e9e15 fix #5709 - ensure REFLECTED_PLAN_VIEW camera has (-1,-1,-1) scale. 2025-03-21 10:34:33 -05:00
Andrej730 b5c062415d ifcsverchok.helper - add debug get_selected_nodes method 2025-03-21 18:39:38 +05:00
Andrej730 a98a7ac856 ifcsverchok - add some sockets descriptions, specify types 2025-03-21 18:39:38 +05:00
Andrej730 ee2b836b84 ifcsverchok - add/update node descriptions 2025-03-21 18:39:38 +05:00
Andrej730 e075c32a5a typing 2025-03-21 18:39:38 +05:00
Andrej730 a567af370b ifcsverchok - fix missing socket tooltips
apparently `tooltip` property doesn't exist but `description` does
2025-03-21 18:39:38 +05:00
Andrej730 544b61b95a remove debug print 2025-03-21 18:39:37 +05:00
Thomas Krijnen 45c45d6862 ifcopenshell.geom.serializers.guess_from_extension() 2025-03-21 14:35:47 +01:00
Thomas Krijnen 6880e31a66 Add ColladaSerializer to python bindings 2025-03-21 14:35:23 +01:00
Thomas Krijnen e327932f4e Fixes to HierarchyHelper #6365 2025-03-21 13:48:59 +01:00
Andrej730 f2283e5895 Fix Blender 4.4 errors starting some operators #6399 #6396
Ping @brunoperdigao just in case.
2025-03-21 12:02:53 +05:00
Dion Moult ea84f829af The experimental cutting tool now supports a bisect mode for batch cuts 2025-03-21 16:46:42 +11:00
Dion Moult 3231752fef Minor fix to clipping planes if there is no camera 2025-03-21 16:46:08 +11:00
Dion Moult 963ba5fc30 See #6404. See #1227. Create axis context if it does not exist for walls. 2025-03-21 16:45:29 +11:00
Dion Moult 8e1e0aec79 Fix #6404. See #1227. Fix IFC2X3 wall creation with new wall engine. 2025-03-21 16:30:56 +11:00
Thomas Krijnen 9c4ec34de3 Some form of circle handling in loop_to_function_item_upgrade_impl() #6381 2025-03-20 20:57:47 +01:00
Thomas Krijnen 4c2844292e Apply skew in infra lofts #6386 2025-03-20 16:14:12 +01:00
Sebastian Friston 35fe79dfb2 ISSUE #6328 fix in dispatch_token to avoid missing unknown in logicals 2025-03-20 14:03:07 +01:00
Thomas Krijnen 58a5f881e6 Retrieve length unit info from underlying mapping in iterator #6355 2025-03-20 13:57:30 +01:00
Andrej730 4d5a3ec633 ifcsverchok - ifc.write_file_panel to filter .ifc files 2025-03-20 16:28:49 +05:00
Andrej730 1feb00678c node.sv_ifc_create_file_refresh - fix LB Out bl_label 2025-03-20 16:28:48 +05:00
Andrej730 f10ebe0166 ifcsverchok - remove debug print 2025-03-20 16:28:48 +05:00
Andrej730 5f8fc1ba8c node.sv_ifc_tooltip - hide title 2025-03-20 16:28:48 +05:00
Andrej730 f8f36cf2f0 typing 2025-03-20 16:28:48 +05:00
Thomas Krijnen 2a2bfea870 Read settings from argparser 2025-03-20 11:47:14 +01:00
Thomas Krijnen 8bdf8369b5 pyodide demo: install shapely 2025-03-20 10:49:41 +01:00
Thomas Krijnen 2bc230919e Update publish-pyodide-demo-app.yml 2025-03-20 10:44:08 +01:00
Thomas Krijnen 53fd1284b5 Update wheels #6402 2025-03-20 10:42:27 +01:00
Andrej730 3c777a0b85 Blender 4.4 - fix broken UI lists #6396
Example error:
```python
RuntimeError: could not create instance of BIM_UL_containers_manager to call callback function 'filter_items'
2025-03-19:16:13:14,001 ERROR   [log.py:69] Uncaught exception
RuntimeError: could not create instance of BIM_UL_containers_manager to call callback function 'draw_item'
```
2025-03-19 16:52:50 +05:00
Thomas Krijnen fa3c57b016 Implement readable setting type retrieval 2025-03-19 11:42:43 +01:00
Thomas Krijnen e658b00768 Reinstate old bespoke model-offset/rotation parsing in IfcConvert #6290 2025-03-19 11:42:11 +01:00
Thomas Krijnen 26b55216ca Normalize model-rotation quaternion 2025-03-19 11:40:54 +01:00
Andrej730 d912012f76 Bonsai makefile - fill last_commit_date
Also .isoformat(), so string would match exactly with the one git command provides.
2025-03-19 14:15:21 +05:00
Andrej730 d868d8ceac Fix missing default value for last_commit_date (37ec2bf) #6389
Which is needed when Bonsai is not using git
2025-03-19 14:14:36 +05:00
Andrej730 8def5ec30b Use explicit bl_ui_utils.layout import to fix #6389
Also related - https://github.com/nutti/fake-bpy-module/issues/352
2025-03-19 14:14:36 +05:00
Andrej730 ad03a0c8d9 debugging docs - add section about debugging addon 2025-03-18 17:43:38 +05:00
Richard Brice 09a43f1339 get distance along alignment from station
New alignment api function to get the horizontal distance along an alignment from a station value
2025-03-17 09:16:35 -07:00
Dion Moult c176b9d607 See #1227. Fix bug where unjoin walls didn't recheck the position. 2025-03-18 01:14:06 +11:00
Dion Moult 6cf5cc002e Fix #6318. Fix warnings when decorator cannot convert 3D to 2D screen space location. 2025-03-18 01:06:44 +11:00
Andrej730 0abe6329d6 Quantification to consider Pset_ProfileMechanical.MassPerLength #6344 2025-03-17 18:31:58 +05:00
Andrej730 5109168533 quantification fallback - small clarification 2025-03-17 18:31:57 +05:00
Andrej730 0b4deee73f typing 2025-03-17 18:31:57 +05:00
Andrej730 1f43f7f807 black . 2025-03-17 18:31:57 +05:00
Jesse Roodhorst 73a83c6d4e Issue #5130 fix + sheet all expand and contract (#6379) 2025-03-17 18:28:03 +05:00
Dion Moult 9c5b1a04ff Fix #6372. See #1227. More reliable layer slicing. Always slice in in 2D, not 3D, where possible. 2025-03-18 00:14:56 +11:00
Dion Moult e67753eb4f See #1227. Clear cache when activating drawings. 2025-03-17 23:05:14 +11:00
Andrej730 778a839e5f get_shape_aspects to consider element's type #6374
See diagram in https://github.com/IfcOpenShell/IfcOpenShell/issues/5839#issuecomment-2661296683 when IfcShapeAspect assigned to IfcRepresentationMap instead of being assigned to IfcProductDefinitionShape directly.
2025-03-17 16:38:12 +05:00
Andrej730 102de32737 Fix UI errors in case of uppercase unit names (7af6727)
IfcConversionBasedUnit's Name is case insenstive and it was producing UI errors.
Noticed working with file from Revit Ryan attached in #6374

Traceback:
Traceback (most recent call last):
  File "\bonsai\bim\module\pset\ui.py", line 456, in poll
    ObjectMaterialData.load()
  File "\bonsai\bim\module\material\data.py", line 167, in load
    cls.data["total_thickness"] = cls.total_thickness()
                                  ^^^^^^^^^^^^^^^^^^^^^
  File "\bonsai\bim\module\material\data.py", line 331, in total_thickness
    return format_distance(thickness, precision=precision, suppress_zero_inches=True, in_unit_length=True)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\bonsai\bim\module\drawing\helper.py", line 157, in format_distance
    unit_length = unit_length_mapping[unit_length]
                  ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
KeyError: 'FOOT'
2025-03-17 16:38:12 +05:00
Sayan J. Das cf3a554b15 Fix #5809: Add safety check during file.write (#6292)
Co-authored-by: theseyan <sayanjyotidasworkmail@gmai.com>
2025-03-17 16:17:42 +05:00
Andrej730 a8ff6ecf14 Fix object materials UI #6375 (ba37754) 2025-03-17 15:11:16 +05:00
Andrej730 55be5cc289 get_length, getget_object_main_axis - simplify
1) get_object_main_axis - replace branching with just max between dimensions. There is a small caveat that previously for `x,y,z=1,2,2` it would return 'x' but now it returns 'y', but I'm not sure if 'x' was really intended here.
2) get_length - current implementation does the same as get_linear_length

Ping @maxfb87 just in case.
2025-03-17 14:48:22 +05:00
Andrej730 9af00d4e72 typing 2025-03-17 14:27:52 +05:00
Andrej730 d8592a2a95 get_rectangular_perimeter to use get_x again #6364 (4260313)
Just restoring original behaviour before 4260313 when get_rectangular_perimeter prioritized x dimension.
2025-03-17 14:07:11 +05:00
Andrej730 16ba10f957 Fix UI error when selected object has no material #6371 (ba37754) 2025-03-17 12:02:34 +05:00
Andrej730 0d1298aac1 Fix #6367
ahh, no idea where I picked up this string during refactoring
2025-03-17 11:57:27 +05:00
Andrej730 210257de8e black . 2025-03-17 11:57:26 +05:00
Ryan Schultz 37ec2bf0e4 add bonsai_commit_date to error report 2025-03-16 20:04:04 -05:00
Sayanjyoti Das 6e7c8ad640 Fix #6362 2025-03-16 23:32:42 +11:00
Dion Moult bfc4490c97 See #1227. Implement wall layers in SVG drawings. 2025-03-16 21:43:59 +11:00
Sayanjyoti Das d50e8060ac better comments 2025-03-16 17:48:44 +11:00
Sayanjyoti Das a21939dc0d fix error 2025-03-16 17:48:44 +11:00
Sayanjyoti Das 8845440edd fix #6360 2025-03-16 17:47:44 +11:00
Dion Moult ba37754068 Fix #6310. See #1227. Fix regression with layer ordering. 2025-03-16 14:17:37 +11:00
Dion Moult ed48afc633 Only clip on visible drawing cameras. 2025-03-16 11:53:35 +11:00
Dion Moult 7262a50184 See #1227. Merging doubles gives more reliable results in bisection fills. 2025-03-16 11:40:30 +11:00
Bruno Perdigão 67b0ab6685 Fix #6366. Improve extrusion x_angle and direction handling.
This solution brings new questions to #5938.
Currently, we lack a reliable way to calculate the existing x_angle only based solely on the extrusion direction. For example, a 30 degree angled extrusion with positive direction has the same extrusion direction as a -150 degree angled extrusion with negative direction. The difference lies in the object's rotation.
This means that things can get messy if the user changes the object x angle somehow. We may need to explore alternative approaches.
2025-03-15 21:00:57 -03:00
Dion Moult 2941a4ad1a See #1227. Recalculate normals and limited dissolve when layerset slicing and generating underside clipping geom. 2025-03-16 10:56:18 +11:00
Dion Moult b2e2f765ca See #1227. Clip based on active camera for legibility of drawings. Needs polish. 2025-03-16 00:23:47 +11:00
Dion Moult 2270185f8c See #1227. Drawing decorator now does material-aware fills for everything, and also basic caching. 2025-03-15 22:56:01 +11:00
Dion Moult faef2df2a6 See #1227. Basic uncached implementation of visualising wall layers in drawings 2025-03-15 20:56:56 +11:00
Dion Moult bef7ff33f7 Prevent errors where decorator is still running and there is no camera 2025-03-15 20:54:15 +11:00
Bruno Perdigão 615ca24825 Fix #6363. Improve how Curves are handled by the snapping system. 2025-03-14 21:43:26 -03:00
Bruno Perdigão 7e42b10159 Snap - Add face normal intersection to ´mix_snap_and_axis´ function. 2025-03-14 21:33:35 -03:00
Richard Brice 04e07db0a3 Import alignment from csv (#6234)
* Start of the official alignment API

* Import alignment into bonsai model using CSV file
2025-03-14 07:31:50 -07:00
Andrej730 eed47c8c87 Display internal ID in calculator functions description
Example - https://i.imgur.com/3C3TBCu.png
2025-03-14 18:35:50 +05:00
Andrej730 9ba3b6a61f add cgal-original-edges option to settings (5599e52) 2025-03-14 18:19:08 +05:00
Andrej730 6cfd15e819 Fix mistake in 64f51d7 2025-03-14 18:19:07 +05:00
Andrej730 e55523ce3e bim.show_loads - add undo for safety
In Blender it's unsafe to leave operators without UNDO options if they edit any ID data-block.
2025-03-14 18:19:07 +05:00
Andrej730 f36830d6dc bim.show_loads - handle absence of structural items 2025-03-14 18:19:07 +05:00
Andrej730 2f87029434 Use schema_identifier to get IFC4X3 schema more precisely 2025-03-14 18:19:07 +05:00
Andrej730 9f78d9881e ifcopenshell.util.shape - use refs to functions in docs 2025-03-14 18:19:06 +05:00
Andrej730 1d447e6e23 ifc5d.qto.IfcOpenshell - skip None values
Just for consistency, in all other cases besides get_segment_length we were only preserving floats anyway
2025-03-14 18:19:06 +05:00
Andrej730 4c53c2ab1f ifc5d.qto.IfcOpenShell - get_weight function
Added support for it in .json for the same elements that support it in IFC4QtoBaseQuantitiesBlender
2025-03-14 18:19:06 +05:00
Andrej730 86ebc968e5 ifcopenshell.util.element.get_element_mass_density
Move it from Blender qto calculator as this method can be generally useful
2025-03-14 18:19:06 +05:00
Andrej730 1de67ffd9c ifc5d.qto - add x,y,z functions just for consistency with ifcopenshell calculator 2025-03-14 18:19:06 +05:00
Andrej730 1408beeac5 ifc5d.qto.Blender - functions descriptions 2025-03-14 18:19:05 +05:00
Andrej730 791a2e40f1 qto - add note on gross and net functions 2025-03-14 18:19:05 +05:00
Andrej730 3f6a9a55cd bim.calculate_side_formwork_area - expose to ui 2025-03-14 18:19:05 +05:00
Andrej730 ec702e2ec1 Simple quantity calculator - add operators descriptions 2025-03-14 18:19:05 +05:00
Andrej730 4f6dbbe72b bim.calculate_single_quantity - ifcopenshell calculator to appear first 2025-03-14 18:19:05 +05:00
Andrej730 8ca344259f bim.perform_quantity_take_off - support fallback calculator in ifc4x3 too 2025-03-14 18:19:04 +05:00
Andrej730 e038f968f2 bim.calculate_single_quantity - add info message 2025-03-14 18:19:04 +05:00
Andrej730 0e8810c1f5 typing 2025-03-14 18:19:02 +05:00
Andrej730 10ecac33b8 black . 2025-03-14 18:19:02 +05:00
Dion Moult 069b630b16 Minor fix where you'd randomly get incorrect ports connected based on the first birdsong of spring 2025-03-14 14:41:23 +11:00
Bruno Perdigão 8f6cd18288 Add instruction to MeasureFaceArea tool 2025-03-13 21:32:13 -03:00
Bruno Perdigão 7b7108b938 MeasureFaceAreaTool - Fix issue where face with index zero where not being selected 2025-03-13 17:02:40 -03:00
Bruno Perdigão ebd719783c black . 2025-03-13 16:56:22 -03:00
Bruno Perdigão 46a31386da Add "Face Area" option to measure tool. See #6163. 2025-03-13 16:55:43 -03:00
Bruno Perdigão c80c7c6d36 Refactor - move mouse_offset from snap.py to raycast.py 2025-03-13 16:55:43 -03:00
Bruno Perdigão ed7cf248ec typing 2025-03-13 16:55:43 -03:00
Bruno Perdigão b40f630783 Refactor - move raycast functions from snap.py to raycast.py 2025-03-13 16:55:37 -03:00
Bruno Perdigão f99015e123 Polyline tool - move "remove_last_polyline_point" to "handle_inserting_polyline". 2025-03-13 16:54:35 -03:00
Bruno Perdigão be76d4cb44 Refactor - Update input options display in Polyline tool UI 2025-03-13 16:54:35 -03:00
Bruno Perdigão 6949f9ceae Refactor - Move filtering logic for snap objects into a separate function 2025-03-13 16:54:35 -03:00
Andrej730 4daf4693f3 selector syntax - note on PredefinedType 2025-03-13 18:43:19 +05:00
Andrej730 edd60a73ca generate_pset_templates - fix running after ifc4x3 release
The error was:
```
Traceback (most recent call last):
  File "\IfcOpenShell\src\ifcopenshell-python\ifcopenshell\util\generate_pset_templates.py", line 416, in <module>
    templates_generator.parse_ifc4x3_data()
  File "\IfcOpenShell\src\ifcopenshell-python\ifcopenshell\util\generate_pset_templates.py", line 94, in parse_ifc4x3_data
    self.parse_psets_data("IFC4X3", pset_data_glob, "IFC4X3 Property Set Templates", str(IFC4x3_OUTPUT_PATH))
  File "\IfcOpenShell\src\ifcopenshell-python\ifcopenshell\util\generate_pset_templates.py", line 129, in parse_psets_data
    schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\ifcopenshell\ifcopenshell_wrapper.py", line 9037, in schema_by_name
    return _ifcopenshell_wrapper.schema_by_name(arg1)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: No schema named IFC4X3
```
2025-03-13 18:43:19 +05:00
Andrej730 6c04669543 ifc5d - add ifc4x3 base quantities #6325 2025-03-13 18:43:19 +05:00
Andrej730 4eae112087 not real ifc4x3 quantities, just adding ifc4 quantities for diff 2025-03-13 18:43:18 +05:00
Andrej730 5be076cb96 Reset operator context in 63aada25dd for clarity 2025-03-13 18:43:18 +05:00
Andrej730 ef67d866b6 black . 2025-03-13 18:43:18 +05:00
Dion Moult 609f468ce1 Update failing tests 2025-03-13 22:07:00 +11:00
Dion Moult 3852e3c63f Add tests for new light rendered shadows feature 2025-03-13 22:06:21 +11:00
Dion Moult 5cb9e1c68d Minor tweak to UI to make the shadow UI more elegant and reflect IFC style types 2025-03-13 11:09:42 +11:00
Dion Moult 52fbe09dd4 Fix syntax error otherwise nothing loads
Also PortData is loaded in the authoring tool and there might not be any
active object (e.g. fresh session).
2025-03-13 11:06:56 +11:00
falken10 4cff2f1fc1 Add traverse_transparent property to control sun light traversal through transparent objects 2025-03-13 10:36:43 +11:00
falken10 6517cafd98 Update add_prop_template.py so the properties are sorted
This allows to keep the pset properties sorted for easy access
2025-03-13 10:35:40 +11:00
falken10 5114576457 Update profile.py to add MEPGenerator().setup_ports when the profile has only one segment (no DumbProfileJoiner called)
When adding a single duct in MEP no ports where attached.
The function create_profiles_from_polyline does not call DumbProfileJoiner (which eventually calls MEPGenerator().setup_ports).
I have added a check so if there is only one segment, the MEPGenerator().setup_ports is called explictily
2025-03-13 10:35:24 +11:00
falken10 9bafe07c36 Update system.py so two ortogonal arrows (in x and y local) are used for MEP systems decorations
Currently the flow direction decorations run in the local Y. This means that if one is creating ducts/pipes in the xy plane, the decorations are drawn in planes perpendicular to that one and than means that the arrows are not visible from a top/bottom view.
By adding another set of arrows perpendicular the arrows are always visible no matter what projection is taken
2025-03-13 10:31:27 +11:00
falken10 6b85e9ec85 Update operator.py
This will sort the groups by name so it is easy to find them.
2025-03-13 10:31:06 +11:00
Dion Moult d89957faab Fix regression in 227f31574 which prevented quantifications
The key in the dictionary is not a class name, it's a query string to
allow for complex quantification rules.
2025-03-13 09:18:07 +11:00
Ryan Schultz ba8753d55e Updated docs regarding material selector syntax. 2025-03-12 16:27:48 -05:00
Bruno Perdigão 5df9c45795 Improve weight in snapping for "Plane", "Axis" and "Face" type. 2025-03-12 17:16:47 -03:00
Bruno Perdigão 5cc0aa71c7 Fix issues with snapping empties and faceless objects in x-ray mode. 2025-03-12 17:16:47 -03:00
Bruno Perdigão b06ddbcad4 Remove duplicate line 2025-03-12 17:16:47 -03:00
Ryan Schultz 18431dec31 added material.item.Material.Name.0 to selector syntax 2025-03-12 14:23:43 -05:00
Bruno Perdigão 7e9985758f Remove debug print statements. 2025-03-12 11:10:14 -03:00
Andrej730 ece91a94db util.schema.reassign_class to return early if class already assigned 2025-03-12 18:49:26 +05:00
Andrej730 dccfb634d9 Ports UI - cache instead of accesing IFC on draw calls 2025-03-12 18:49:26 +05:00
Andrej730 ffc6c6a7ad Zones UI - rearrange button to match the Systems UI layout 2025-03-12 18:49:26 +05:00
Andrej730 dbf4a2a5ed Fix reassign_class on IFC4X3, use ValueError
Use ValueError to make it more specific
2025-03-12 18:49:22 +05:00
Andrej730 f6640d3904 typing 2025-03-12 18:49:22 +05:00
Andrej730 63aada25dd Deprecate LaunchAddElement, LaunchRenameType
TIL that it's possible to set invoke context from draw by changing operator_context property in UILayout 😅
2025-03-12 17:08:41 +05:00
Dion Moult 6596f5bc4b Fix regressions in MEP joins after switching over to numpy from mathutils 2025-03-12 23:05:34 +11:00
Dion Moult acdc40fb41 See #1227. Reimplement extending walls to another wall 2025-03-12 23:05:34 +11:00
Andrej730 9be0a60c0a github issue templates - use comment blocks for instructions
As users sometimes tend not to remove the intsruction messages...
2025-03-12 16:29:13 +05:00
Andrej730 e9a3d06352 Document copy and paste workarounds in docs #6327 2025-03-12 16:13:53 +05:00
Andrej730 dfd30fbcaa black . 2025-03-12 16:13:52 +05:00
Dion Moult a941eb3f90 Parametric roof geometry is now whitelisted for IfcSlab.ROOF and IfcCovering.ROOFING too. 2025-03-12 18:47:50 +11:00
Dion Moult 226736eb41 Fix #3357. Remove Shift-E for parametric objects (superseded by tab) and you can now extend to anything not just slabs.
This Shift-E still needs more polish as we figure out the best hotkeys
and what's most natural to users when they select different combinations
of objects, so expect the conditionals that govern when Shift-E does
things to still change in the future.
2025-03-12 18:41:18 +11:00
Dion Moult f5a1c4aae3 See #1227. Reimplement extend walls to slab (or anything with negative Z faces)
There are a few differences to the previous implementation:

 1. It is no longer recalculated on wall regeneration. The new wall
regeneration is strict to the spec on the rules of rel connects path,
and so this being a "userdefined" connection we only calculate it
explicitly when the user invokes the operator.
 2. It uses meshes instead of clipping planes, so you can clip to
strange shapes or gable roofs or whatever. Nice.
2025-03-12 18:05:57 +11:00
Bruno Perdigão a7a4198fc1 Polyline tool - add "dm" and "cm" to the input validation. 2025-03-11 17:10:52 -03:00
Bruno Perdigão 6e0e19d690 See #6130. Fix issue where aggregate mode where not being disabled.
The issue was caused by deleting an object outside the aggregate while in
aggregate mode. The selection for these objects are now disabled to prevent
this issue
2025-03-11 16:58:08 -03:00
Bruno Perdigão 72bbf244f2 Fix issue #6221 where crtl+shift+D were not moving the whole copied aggregate. 2025-03-11 16:33:40 -03:00
Bruno Perdigão 41c1c6e8fa Fix #6291.
When changing slab `x_angle` the rotation matrix now takes into account object's world matrix.
2025-03-11 16:11:47 -03:00
Bruno Perdigão a32735ac6a Small fix after 7af672712f 2025-03-11 14:04:34 -03:00
Bruno Perdigão 031540e982 Add #5496. Objects from linked ifc files can now be snapped. 2025-03-11 12:37:47 -03:00
Bruno Perdigão db1d8d938f Allow dimensions to use custom units from BBIM_Dimension Pset.
- Implementation of @theoryshaw PR #5922.
 - Note: `IfcPropertyEnumeration` allows multiple choices, but this is not
 applicable for this use case. Users must manually deselect other options;
 otherwise, it defaults to the first selected option.
2025-03-11 11:56:16 -03:00
Bruno Perdigão aa478ab359 Fix area measure tool to display first dimension correctly. 2025-03-11 11:56:16 -03:00
Bruno Perdigão 19c103c627 Fix issue where "C" to close polyline where not updating measurement dimension. 2025-03-11 11:56:16 -03:00
Bruno Perdigão 7af672712f See #6164. Allow measure tool to work without IFC Project. 2025-03-11 11:56:08 -03:00
Andrej730 6bd8636c92 Revert "Bump IOS"
This reverts commit c1bc36ab86.
2025-03-11 18:46:38 +05:00
Andrej730 a9f7d4ab3b get_applicable_types in ifc2x3 to prioritize IfcBuildingElementProxyType
for occurrence classes without special type.

E.g. previously:
you select IfcSlab and change it to IfcRoof - since there is not IfcRoofType in IFC2X3 it would figure the matching product type is IfcBeamType and change this slab's type element to IfcBeamType. IfcBuildingElementProxyType seems more generic and fitting.
2025-03-11 18:37:24 +05:00
Andrej730 977d814d39 bim.reassign_class - fix issue in ifc2x3 when selected product class would be ignored
E.g. you select IfcSlab and change it to IfcRoof - since there is not IfcRoofType in IFC2X3 it would figure the matching product type is IfcBeamType (which is confusing too but that's another subject) and change this slab's type object to IfcBeamType which consequently change IfcSlab to IfcBeam instead of IfcRoof that was selected originally.

Noticed investigating #5918
2025-03-11 18:37:23 +05:00
Andrej730 5b95bb37e6 Systems UI - cache active system data 2025-03-11 18:37:23 +05:00
Andrej730 85b9906e20 Zones UI - add buttons for unassigning zone from the active objects 2025-03-11 18:37:23 +05:00
Andrej730 64f51d733e Fixed version of f3f7939c8b + get_element_zones 2025-03-11 18:37:23 +05:00
Andrej730 ed0b1c5009 typing 2025-03-11 18:37:22 +05:00
Dion Moult c1bc36ab86 Bump IOS 2025-03-11 21:59:53 +11:00
Dion Moult 9cfcaeb587 Fix bug and add boolean tests where if you added a boolean that fully clipped something you couldn't edit it due to lack of item ids. 2025-03-11 21:14:11 +11:00
Dion Moult d9e48467cd Fix bug where removing a boolean didn't unmark it as a manual boolean. 2025-03-11 21:13:04 +11:00
Dion Moult f08dba6483 Improve debug logs when running BDD tests
I've been spoilt by tools like Behat and so this make it much nicer
because it tells you exactly the feature/scenario/step where it failed.
2025-03-11 18:40:00 +11:00
Dion Moult 1e97d2c5fa See #6307. Prevent user from manually changing the ifc file path.
You can still hover over and copy paste if you want.
2025-03-11 18:38:45 +11:00
Dion Moult bf54132805 Fix #6307. Warning: critical bug where saving a file broke undo state.
Previously, changing props.ifc_file had an update hook to reload
information about the IFC model. But this isn't always correct because
there are two situations:

 1. The ifc_file path changed because you saved the file for the first
time or saved as, and this is merely recording a new saved path of the
existing file object.
 2. The ifc_file path changed because the user manually changed it or
selected a new file. This could reference an entirely new file object.
This is dangerous because we can't trust anything anymore, including our
undo history.

So the new default situation is that there is no magic hook. If you
change props.ifc_file, that's all it changes ... just a path stored in
Blender with not much significance.

If the user runs select_ifc_file to explicitly relink the file, it now
explicitly purges in that situation and clears the undo history.

Basically now behaviour is explicit, not using magic hooks.
2025-03-11 18:37:53 +11:00
Dion Moult 570513b60b Revert "util.get_element_systems to consider all kinds of ifcsystems"
This reverts commit f3f7939c8b.
2025-03-11 09:13:48 +11:00
Bruno Postle e1ba98c5f8 fix loading ifc2x3 project libraries 2025-03-10 21:07:18 +00:00
Andrej730 8ed9c32404 Rename systems and zones from UIList
example - https://imgur.com/a/8yTzmy3
2025-03-10 18:52:39 +05:00
Andrej730 5b9ef23bdd bim.assign_system/unassign_system - add info messages
Also check whether element is assignable prior to running system.assign_system to avoid scary exceptions.
2025-03-10 18:52:39 +05:00
Andrej730 f3f7939c8b util.get_element_systems to consider all kinds of ifcsystems 2025-03-10 18:52:39 +05:00
Andrej730 469b4f3502 Systems UI - move buttons to the header to unclutter UI 2025-03-10 18:52:39 +05:00
Andrej730 c4b57eb903 typing 2025-03-10 18:52:39 +05:00
Andrej730 3886c79a2c black . 2025-03-10 18:52:39 +05:00
Massimo Fabbro 566fe32ab2 Fix wrong wall legth qto calculation 2025-03-10 23:19:12 +11:00
Dion Moult 7fb6adc025 Fix #6296. See #1227. More normal fixes in layer set slicing. 2025-03-10 23:15:24 +11:00
Dion Moult c03cf4ce58 Fix #6303. I messed up the commit in 1259607a 2025-03-10 23:06:16 +11:00
Dion Moult 41188e26e1 See #1227. Don't touch manual booleans when regenerating wall body. 2025-03-10 22:54:42 +11:00
Dion Moult 823f27cd6a Add tests for new element add feature 2025-03-10 22:53:55 +11:00
Massimo Fabbro 4260313a99 Fix #6271
Fix the wrong length calculation for beams and columns
2025-03-10 20:09:10 +11:00
Dion Moult 1259607a9f See #1227. Fix slicing normal calculation for AXIS3 elements. 2025-03-10 18:10:05 +11:00
Dion Moult 9f1ffe6381 Minor fix where you shouldn't be able to add an element without an IFC project 2025-03-10 18:08:43 +11:00
Dion Moult ba1b6b5b10 Fix regression where type thumbnails don't refresh properly from ae76d3253
The fix in ae76d3253 removed the refresh_ui_data call, so we need to
handle it a bit better. I also heavily simplified the thumbnail loading.
2025-03-10 18:08:30 +11:00
Dion Moult 2318132ea2 Minor refactor add_constr_type_instance is now add_occurrence 2025-03-10 08:23:56 +11:00
Massimo Fabbro b6b553e726 Fix #6271
Fix a bug about wrong net values qto calculation with IfcOpenshell
2025-03-09 22:48:08 +11:00
Dion Moult 076f39e2d6 I think bSDD should use the Name for the property name. See 115a48f
It seems correct according to
https://github.com/buildingSMART/bSDD/blob/master/Documentation/bSDD%20JSON%20import%20model.md
2025-03-09 21:35:20 +11:00
Dion Moult abfe8ec315 Fix failing IOS tests 2025-03-09 20:47:02 +11:00
Dion Moult 9d7fe179cf See #1227. Minor fix of incorrect layerset slicing of negative sense slabs. 2025-03-09 19:29:35 +11:00
Dion Moult 8ffb5c56e4 See #1227. Fix inconsistent layer UI based on direction sense and minor UI cleanup.
The UI here looks suspicious in that logic is occuring in the draw call
instead of the cached data object.
2025-03-09 19:28:47 +11:00
Dion Moult 4109b19c3a See #1227. Slightly more defensive to make sure ATPATH is between START and END 2025-03-09 18:31:49 +11:00
theseyan 7503530741 Fix #4807 2025-03-09 18:02:59 +11:00
Dion Moult e78127d54f See #1227. Don't move any child at all when regenerating wall body. 2025-03-09 18:00:13 +11:00
Dion Moult 5fa4f2e79b See #1227. Reimplement merge walls with new wall body generation code. 2025-03-09 17:32:19 +11:00
Dion Moult e5027e0be8 See #1227. Implement updating of object position. 2025-03-09 08:52:05 +11:00
Dion Moult af2dd1d7c6 See #1227. Start to integrate into Bonsai. Basic testing of join/unjoin/extend/split/flip done. 2025-03-09 08:25:32 +11:00
Massimo Fabbro 5d9528d791 Fix #6168. Now reload link button works
It is fixed also an inconsistency problem when use relative path is True
2025-03-08 11:47:43 +01:00
Dion Moult 47d11daa15 See #1227. Small clean up of old sketch-wall based code which we no longer support. 2025-03-08 11:26:27 +11:00
Dion Moult 01ec750814 See #1227. New connect wall API function since this can be done generically.
This will supersede the butt/mitre join functions we used to have.
2025-03-08 11:25:07 +11:00
Dion Moult 8c9a657a8c See #1227. Check layer set direction when slicing. 2025-03-08 11:13:26 +11:00
Bruno Perdigão 48acfad8bb Fix #6213. Adjust wall clipping from slabs to account for slab offset and direction sense. 2025-03-07 15:49:32 -03:00
Bruno Perdigão 01b63d87d0 Fix issue with updating slab offset with negative direction sense. 2025-03-07 15:42:09 -03:00
Bruno Perdigão 36744d391e Fix #6236. Product preview properties were not being cleared when data is empty. 2025-03-07 14:14:28 -03:00
Andrej730 f0e5430daa bonsai_translations - allow .po files going out of sync
We had Chinese translation for Bonsai and some stub file for German and translation.py generation was failing with the error below. Chinese translation is more recent so it has some keys that German file hadn't and some keys were gone in the more recent version. In theory we should make all .po file in sync but let's skip it for now to keep things going.

```
Traceback (most recent call last):
  File "/home/runner/work/IfcOpenShell/IfcOpenShell/src/bonsai/scripts/bonsai_translations.py", line 379, in <module>
    update_translations_from_po(po_directory=Path(args.input), translations_module=Path(args.output))
  File "/home/runner/work/IfcOpenShell/IfcOpenShell/src/bonsai/scripts/bonsai_translations.py", line 203, in update_translations_from_po
    if (msgstr := msg.translations[lang]) in (None, ""):
                  ~~~~~~~~~~~~~~~~^^^^^^
KeyError: 'de_DE'
```
2025-03-07 20:48:44 +05:00
Andrej730 197281eb5a bonsai_translations - use utf-8 for opening files
Critical on Windows as it's not using utf-8 by default
2025-03-07 20:48:44 +05:00
Andrej730 7b370e6142 Fix operators missing execute after a663216 2025-03-07 17:13:00 +05:00
Andrej730 71a7d0c086 Fix missing IfcDistributionCircuit and IfcElectricalCircuit #6202
When checking if element is assignable to the system.
2025-03-07 17:12:59 +05:00
Andrej730 b86e735530 Fix breaking Systems UI with IfcElectricalCircuits 2025-03-07 17:12:59 +05:00
Andrej730 46104f5bf5 Update Profiles UI profile editing arbitrary profile #6284 2025-03-07 17:12:59 +05:00
Andrej730 d68068a84b bim.load_profiles - try not to check all inverses for optimization 2025-03-07 17:12:59 +05:00
Andrej730 fc7dd3b234 Preserve ProfileName editing arbitrary profile #6284 2025-03-07 17:12:59 +05:00
Andrej730 85fc97eb84 Profiles UI - add safety check for missing profiles #6284
Example - https://imgur.com/a/bVdV5oB
2025-03-07 17:12:59 +05:00
Andrej730 9076deddbc Consider ifc2x3 in a358cff1b7 #6255
In ifc2x3 position is not optional, so we should reset it's coordinates instead.
2025-03-07 17:12:59 +05:00
Andrej730 eb0d3c38b2 ifcopenshell.util - make ifc_file argument optional if it can be deduced from ifc element 2025-03-07 17:12:59 +05:00
Andrej730 590288496e typing 2025-03-07 17:12:58 +05:00
Andrej730 269e137ea2 black . 2025-03-07 17:12:58 +05:00
Dion Moult ed722c6f13 See #1227. Refactor out test waldo script into API function. 2025-03-07 19:05:24 +11:00
Dion Moult 2b9e3d2a2e See #1227. Enable layerset slicing. 2025-03-07 19:03:45 +11:00
Dion Moult 7dee888ecf See #1227. New util function in shape builder to do an X axis intersection. 2025-03-07 19:00:18 +11:00
Dion Moult a67927ae67 Automatically merge excessive styles when loading models
Came across this problem with some models from 12D
2025-03-07 17:02:05 +11:00
Dion Moult 1271e4b7e3 New IfcPatch recipe to merge identical styles 2025-03-07 17:01:14 +11:00
Andrej730 a44d0e713a Bump Blender build and fix dead url 2025-03-06 19:11:46 +05:00
Andrej730 a358cff1b7 Fix extrusion position update on OffsetFromReferenceLine changed #6255
Also fixed an issue when previously there was an offset and now it was set to 0 but slab did not reset it's position.
2025-03-06 18:43:54 +05:00
Andrej730 188ca3e402 Fix bim.profiles_ui_select button in profiles set ui
It was using IfcMaterialProfile id instead of IfcProfileDef id, so profile was never selected
Example - https://imgur.com/a/Sf1ghPO
2025-03-06 18:43:54 +05:00
Andrej730 b5260d3005 copy_class to prevent duplicating profiles #6261 2025-03-06 18:43:54 +05:00
Andrej730 79940641b2 typing 2025-03-06 18:43:54 +05:00
Andrej730 7458040949 Fix ifcpatch unit conversion for lists of ifc entities #6280 2025-03-06 16:14:02 +05:00
Andrej730 14eb543662 TestConvertFileLengthUnits - add attributes conversion tests 2025-03-06 16:14:02 +05:00
Andrej730 e786687882 ConvertFileLengthUnits - unify tests, add ifc4x3 tests 2025-03-06 16:14:02 +05:00
Andrej730 37e131193b edit_georeferencing - add note on MapUnit data type 2025-03-06 16:14:02 +05:00
Andrej730 dfb00f27bb Fix georeferencing docs url 2025-03-06 16:14:02 +05:00
Dion Moult 53dea65d8b See #1227. Implement joins for sloped walls. 2025-03-06 19:38:46 +11:00
Andrej730 2c16839de7 Fix parametric stairs/railings/roofs not updating ifc link after representation update #6264
Related to 3107fd9
2025-03-05 18:53:57 +05:00
Andrej730 c4f2d556f1 Uncomment use of get_edges_representation_item_ids
as build was updated awhile ago
2025-03-05 18:53:57 +05:00
Andrej730 74900193ee merge_identical_objects to use quicker version of get_info 2025-03-05 18:53:57 +05:00
Andrej730 2987d38a31 append_asset - check unique styles based on their name #6269 2025-03-05 18:53:56 +05:00
Andrej730 08b50bb9ea Library UI - recognize already appended styles
Example - https://i.imgur.com/XCXHY4C.png
Noticed investigating #6269
2025-03-05 18:53:56 +05:00
Andrej730 1257eb0872 append_asset - allow using same name for different ifcpresentationstyle types
Since they are not used interchangeably typically.
2025-03-05 18:53:56 +05:00
Andrej730 1c8ef176b1 typing 2025-03-05 18:53:56 +05:00
Andrej730 e1e51cbf26 black . 2025-03-05 18:53:56 +05:00
Andrej730 4c97cc8a41 import_ifc - not to break completely on facing breaking mesh #6270
create_mesh returns None only in case if it meets some exception and it prints logs in that case, but atleast some breaking mesh won't be in the way of users trying to open some model
2025-03-05 18:53:09 +05:00
Dion Moult 7c6e124fe9 See #1227. Basic implementation of sloped walls (joins not yet considered) 2025-03-05 22:10:33 +11:00
Andrej730 3fc312752b Show an error for unsupport curve types #6259 2025-03-04 18:10:42 +05:00
Andrej730 d0cb5456b3 Fix importing IfcPolylines always assuming they're closed
Resolved issue during import in #6259 but doesn't completely resolves the issue.
2025-03-04 18:10:42 +05:00
Andrej730 86072079de draw_image_for_ifc_profile - optimizations using numpy 2025-03-04 18:10:42 +05:00
Andrej730 9440096106 Fix #6260 after 2f6ae17 2025-03-04 18:10:42 +05:00
Andrej730 649a913bbc add_profile_representation - add test 2025-03-04 18:10:42 +05:00
Andrej730 53a3afaded typing 2025-03-04 18:10:41 +05:00
Andrej730 33d3984990 add_profile_representation - reuse util.shape methods 2025-03-04 18:10:41 +05:00
Andrej730 5f86c6d1c2 shape_builder.create_axis2_placement_3d_from_matrix - small optimization
create_axis2_placement_3d now is fine with numpy arrays
2025-03-03 13:54:57 +05:00
Andrej730 10d5378ee3 TessellateElements - small optimization
tolist() supports conversion of multidimensional arrays too
2025-03-03 13:54:57 +05:00
Andrej730 7f27785927 docs fixes
1) material.add_profile_set doesn't exist
2) usecase.file is not defined, use `model` instead`
3) Specify material for material.assign_material
2025-03-03 13:54:57 +05:00
Andrej730 fed063a0c6 edit_profile_usage - add test 2025-03-03 13:54:56 +05:00
Andrej730 855062e36b Quick to select profile from BIM Tool in Profile UI
Example - https://imgur.com/a/tJdQUGq
2025-03-03 13:54:56 +05:00
Andrej730 7b74b6c68f Simplify edit_profile_usage with util.shape 2025-03-03 13:54:56 +05:00
Andrej730 a01278aa8d ifcopenshell.util.shape to return Python floats if return is just a scalar 2025-03-03 13:54:56 +05:00
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
782 changed files with 34386 additions and 13222 deletions
+11 -1
View File
@@ -7,14 +7,24 @@ assignees: ''
---
Replace this text describing what problem occurred and what you expected to happen instead.
**Error Description and Steps to Reproduce**
<!--
Describe what problem occurred and what you expected to happen instead.
1. To reproduce this, open file '...'
2. Click on '....'
3. See error
-->
**Attachments**
<!--
If applicable, add screenshots to help explain your problem. Please also drag-drop any files necessary to show the error (rename the file extension from .ifc to .txt to upload). Private files can be uploaded to https://ifcopenshell.org/upload.html - only viewed by core developers and will be deleted afterwards.
-->
**Debug information**
<!--
If this is in Bonsai, paste the output from the Copy Debug Information option in Bonsai. It can be found under Quality and Coordination -> Quality Control -> Debug. If this is a general software issue, if relevant include details about IfcOpenShell version, operating system, Python version, etc.
-->
+6 -1
View File
@@ -7,4 +7,9 @@ assignees: ''
---
Replace this text and describe a feature you'd like us to add. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable.
**Feature Description**
<!--
Describe a feature you'd like us to add. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable.
-->
+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:
+1 -1
View File
@@ -104,7 +104,7 @@ jobs:
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
# Download Blender.
wget -q -O blender.tar.xz https://ftp.nluug.nl/pub/graphics/blender/release/Blender4.3/blender-4.3.0-linux-x64.tar.xz
wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.3/blender-4.3.2-linux-x64.tar.xz
tar -xf blender.tar.xz
# Setup Blender.
-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
@@ -9,6 +9,8 @@ on:
paths:
- 'src/pyodide/**'
- '.github/workflows/publish-pyodide-demo-app.yml'
branches:
- v0.8.0
jobs:
activate:
+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":
+3 -1
View File
@@ -46,6 +46,7 @@ VERSION_MINOR:=$(shell cat '../../VERSION' | cut -d '.' -f 2)
VERSION_PATCH:=$(shell cat '../../VERSION' | cut -d '.' -f 3)
VERSION_DATE:=$(shell date '+%y%m%d')
LAST_COMMIT_HASH:=$(shell git rev-parse HEAD)
LAST_COMMIT_DATE:=$(shell git show -s --format=%cI)
PYVERSION:=py310
PYPI_IMP:=cp
@@ -85,7 +86,7 @@ BLENDER_PLATFORM:=windows-x64
endif
# Current build commit hash.
OLD:=463289e
OLD:=c49ca69
.PHONY: bump
bump:
ifndef NEW
@@ -254,6 +255,7 @@ ifeq ($(IS_STABLE), TRUE)
else
$(SED) "s/0.0.0/$(VERSION)-alpha$(VERSION_DATE)/" build/bonsai/blender_manifest.toml
$(SED) "s/8888888/$(LAST_COMMIT_HASH)/" build/bonsai/__init__.py
$(SED) "s/9999999/$(LAST_COMMIT_DATE)/" build/bonsai/__init__.py
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/pyproject.toml
endif
+9
View File
@@ -40,6 +40,7 @@ from typing import Union, Any, Generator
last_commit_hash = "8888888"
last_commit_date = "9999999"
def get_last_commit_hash() -> Union[str, None]:
@@ -51,6 +52,12 @@ def get_last_commit_hash() -> Union[str, None]:
return last_commit_hash[:7]
def get_last_commit_date() -> Union[str, None]:
if last_commit_date == str(9_999999):
return None
return last_commit_date
# Accessed from bonsai extension:
bbim_semver: dict[str, Any] = {}
@@ -96,6 +103,7 @@ def get_debug_info():
"blender_version": bpy.app.version_string,
"bonsai_version": bbim_version,
"bonsai_commit_hash": get_last_commit_hash(),
"bonsai_commit_date": get_last_commit_date(),
"last_actions": last_actions,
"last_error": last_error,
}
@@ -218,6 +226,7 @@ if IN_BLENDER:
path = Path(__file__).resolve().parent
repo = git.Repo(str(path), search_parent_directories=True)
last_commit_hash = repo.head.object.hexsha
last_commit_date = repo.head.object.committed_datetime.isoformat()
except:
pass
+2 -3
View File
@@ -83,6 +83,7 @@ modules = {
"covering": None,
"web": None,
"light": None,
"alignment": None,
# Uncomment this line to enable loading of the demo module. Happy hacking!
# The name "demo" must correlate to a folder name in `bim/module/`.
# "demo": None,
@@ -115,10 +116,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

File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1');
FILE_NAME('/dev/null','2024-03-03T16:03:58+11:00',(),(),'IfcOpenShell v0.7.0-eaa2aa05a','IfcOpenShell v0.7.0-eaa2aa05a','Nobody');
FILE_NAME('/dev/null','2025-03-27T17:30:27+05:00',(''),(''),'IfcOpenShell 0.0.0','IfcOpenShell 0.0.0','Nobody');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
@@ -29,7 +29,7 @@ DATA;
#22=IFCMATERIALLAYERSET((#24),$,$);
#23=IFCRELASSOCIATESMATERIAL('0khP_Smdj12f2JrOTRbRKj',$,$,$,(#21),#22);
#24=IFCMATERIALLAYER(#20,0.05,$,$,$,$,$);
#25=IFCRELDECLARES('31JriXdev6BP2sC9lcxdC1',$,$,$,#2,(#26,#48,#72,#59,#92,#30,#96,#63,#21,#54,#87,#210,#153,#34,#67,#38,#82,#77,#42));
#25=IFCRELDECLARES('31JriXdev6BP2sC9lcxdC1',$,$,$,#2,(#59,#92,#38,#82,#30,#96,#77,#63,#42,#54,#87,#153,#34,#21,#67,#210,#26,#48,#72));
#26=IFCWALLTYPE('3R5onkTtX1IxdCMPoThRaw',$,'WAL100',$,$,$,$,$,$,.NOTDEFINED.);
#27=IFCMATERIALLAYERSET((#29),$,$);
#28=IFCRELASSOCIATESMATERIAL('0$mXRPCT16nP6Q70XFNvZv',$,$,$,(#26),#27);
File diff suppressed because one or more lines are too long
@@ -28,11 +28,13 @@ DATA;
#21=IFCSIMPLEPROPERTYTEMPLATE('1UDakJ5_f7kBhggNSW4$h5',$,'SymbolsPath','Default symbols SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#22=IFCSIMPLEPROPERTYTEMPLATE('0d53LEtgLDQxnv__NfgH7i',$,'PatternsPath','Default patterns SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#23=IFCSIMPLEPROPERTYTEMPLATE('26qFNMv7nCHgU6Jd7Anga5',$,'ShadingStylesPath','Default shading styles',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcTypeProduct',(#25,#26,#27,#28));
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcAnnotation/RADIUS,IfcAnnotation/DIAMETER,IfcTypeProduct',(#25,#26,#27,#28,#30));
#25=IFCSIMPLEPROPERTYTEMPLATE('1rL2AbQsXD8RbpoWH5pYOV',$,'ShowDescriptionOnly','Hide the measurement values and show only annotation description',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#26=IFCSIMPLEPROPERTYTEMPLATE('0SVyOfB0rC2xNfdRYf3XvY',$,'SuppressZeroInches','Suppress 0 inch values in dimension annotation text (for example: 12'' - 0" -> 12'')',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#27=IFCSIMPLEPROPERTYTEMPLATE('2bUmj458PBqPAtUoI3MXsb',$,'TextPrefix','Text to add before annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#28=IFCSIMPLEPROPERTYTEMPLATE('0bnzttUb9BPuN597uNTXOE',$,'TextSuffix','Text to add after annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#29=IFCSIMPLEPROPERTYTEMPLATE('2pJmUDpB50VBdCOib1zcJJ',$,'Newline_At','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#30=IFCSIMPLEPROPERTYTEMPLATE('2TJn72t_v2cvBUG916Dpev',$,'CustomUnit','Dimension''s custom unit',.P_ENUMERATEDVALUE.,'IfcText',$,#31,$,$,$,.READWRITE.);
#31=IFCPROPERTYENUMERATION('CustomUnit',(IFCTEXT('Feet and Inches - Fractional'),IFCTEXT('Feet - Decimal'),IFCTEXT('Inches - Fractional'),IFCTEXT('Inches - Decimal'),IFCTEXT('Meters'),IFCTEXT('Decimeters'),IFCTEXT('Centimeters'),IFCTEXT('Millimeters')),$);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1 @@
../../libraries/IFC2X3 Demo Library.ifc
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
../../libraries/IFC4 Demo Library.ifc
@@ -0,0 +1 @@
../../libraries/IFC4X3 Demo Library.ifc
+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
+58 -41
View File
@@ -24,18 +24,15 @@ import ifcopenshell.util.unit
import ifcopenshell.api.owner.settings
import bonsai.bim
import bonsai.tool as tool
import bonsai.core.owner as core_owner
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
from bonsai.bim.module.nest.decorator import NestDecorator
from mathutils import Vector
from math import cos, degrees
from math import cos
from typing import Union, Callable
@@ -56,23 +53,24 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
return
if isinstance(obj, bpy.types.Material):
props = obj.BIMStyleProperties
props = tool.Style.get_material_style_props(obj)
if ifc_definition_id := props.ifc_definition_id:
if props.is_renaming:
props.is_renmaing = False
props.is_renaming = 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 +86,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 +106,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 +220,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 +284,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 = "External"
def subscribe_to_viewport_shading_changes():
@@ -326,13 +342,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:
+17 -7
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
@@ -76,8 +76,8 @@ def draw_attribute(
elif value_name == "filepath_value":
attribute.filepath_value.layout_file_select(layout, filter_glob=attribute.filter_glob, text=attribute.name)
elif attribute.name in ("ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"):
propis = bpy.context.scene.BIMWorkScheduleProperties
for item in propis.durations_attributes:
props = tool.Sequence.get_work_schedule_props()
for item in props.durations_attributes:
if item.name == attribute.name:
duration_props = item
layout.label(text=attribute.name)
@@ -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)
+62 -26
View File
@@ -33,7 +33,7 @@ import bonsai.bim.handler
import bonsai.tool as tool
from pathlib import Path
from bonsai.tool.brick import BrickStore
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired, cast
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired
IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
@@ -105,10 +105,8 @@ class IfcStore:
@staticmethod
def get_file():
if IfcStore.file is None:
IfcStore.path = cast(str, bpy.context.scene.BIMProperties.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))
props = tool.Blender.get_bim_props()
IfcStore.set_path(props.ifc_file)
if IfcStore.path:
try:
IfcStore.load_file(IfcStore.path)
@@ -116,13 +114,21 @@ class IfcStore:
print(f"Failed to load file {IfcStore.path}. Error details: {e}")
return IfcStore.file
@staticmethod
def set_path(value):
IfcStore.path = value
# Interpret relative paths as relative to .blend file.
if IfcStore.path and not os.path.isabs(IfcStore.path):
IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path))
@staticmethod
def get_cache():
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 +168,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 +185,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 +195,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 +203,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 +254,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)
@@ -262,9 +269,11 @@ class IfcStore:
IfcStore.guid_map[global_id] = obj
if element.is_a("IfcSurfaceStyle"):
obj.BIMStyleProperties.ifc_definition_id = element.id()
props = tool.Style.get_material_style_props(obj)
props.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)
@@ -402,15 +411,24 @@ class IfcStore:
@staticmethod
def purge_blender_ifc_data(obj: IFC_CONNECTED_TYPE) -> None:
if isinstance(obj, bpy.types.Material):
obj.BIMStyleProperties.ifc_definition_id = 0
props = tool.Style.get_material_style_props(obj)
props.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 +454,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 +471,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 +505,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)
+89 -70
View File
@@ -20,10 +20,12 @@ from __future__ import annotations
import bpy
import time
import json
import ifcpatch
import logging
import traceback
import mathutils
import numpy as np
import numpy.typing as npt
import multiprocessing
import ifcopenshell
import ifcopenshell.geom
@@ -36,7 +38,8 @@ import ifcopenshell.util.shape
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore, IFC_CONNECTED_TYPE
from bonsai.tool.loader import OBJECT_DATA_TYPE
from typing import Dict, Union, Optional, Any
from typing import Dict, Union, Optional, Any, Literal
from ifcopenshell.util.shape import MatrixType
class MaterialCreator:
@@ -68,6 +71,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"):
@@ -81,15 +87,16 @@ class MaterialCreator:
def load_existing_materials(self) -> None:
for material in bpy.data.materials:
if ifc_definition_id := material.BIMStyleProperties.ifc_definition_id:
if ifc_definition_id := tool.Blender.get_ifc_definition_id(material):
self.styles[ifc_definition_id] = material
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 +174,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 +224,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:
@@ -293,6 +300,20 @@ class IfcImporter:
bpy.context.window_manager.progress_end()
def process_context_filter(self) -> None:
contexts = self.file.by_type("IfcGeometricRepresentationContext")
if len(contexts) > 100: # Probably something strange happening. Encountered from Revizto.
print("Warning! Excessive contexts were found and merged where applicable.")
uniques = {}
i = 0
for element in contexts:
data = "-".join([str(a) for a in element])
if unique := uniques.get(data, None):
ifcopenshell.util.element.replace_element(element, unique)
self.file.remove(element)
i += 1
else:
uniques[data] = element
print(f"Replaced {i} IfcGeometricRepresentationContext")
tool.Loader.settings.contexts = ifcopenshell.util.representation.get_prioritised_contexts(self.file)
tool.Loader.settings.context_settings = tool.Loader.create_settings()
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
@@ -433,7 +454,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 +495,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:
@@ -506,8 +529,9 @@ class IfcImporter:
pass
elif shape:
mesh = self.create_mesh(element, shape)
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
if mesh is not None:
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
else:
self.ifc_import_settings.logger.error("Failed to generate shape for %s", element)
break
@@ -588,6 +612,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 +622,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
@@ -748,12 +761,9 @@ class IfcImporter:
def create_pointclouds(self, products: set[ifcopenshell.entity_instance]) -> set[ifcopenshell.entity_instance]:
result = set()
for product in products:
representation = self.get_pointcloud_representation(product)
if representation is not None:
pointcloud = self.create_pointcloud(product, representation)
if pointcloud is not None:
if representation := self.get_pointcloud_representation(product):
if pointcloud := self.create_pointcloud(product, representation):
result.add(pointcloud)
return result
def create_pointcloud(
@@ -801,8 +811,9 @@ class IfcImporter:
materials_updated = bool(mesh)
if mesh is None:
mesh = self.create_mesh(element, shape)
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
if mesh is not None:
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
else:
mesh = None
@@ -813,11 +824,10 @@ class IfcImporter:
if element.is_a(ifcclass):
obj.display_type = "WIRE"
if shape:
if shape and mesh:
# 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:
self.material_creator.create(element, obj, mesh, tool.Geometry.does_shape_has_openings(shape))
elif mesh: # When does this occur?
@@ -888,17 +898,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 +930,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,10 +960,16 @@ 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"):
styles = self.file.by_type("IfcSurfaceStyle")
if len(styles) > self.ifc_import_settings.style_limit: # Probably something strange happening
print("Warning! Excessive styles were found and merged where applicable.")
ifcpatch.execute({"file": self.file, "recipe": "MergeStyles", "arguments": []})
styles = self.file.by_type("IfcSurfaceStyle")
for style in styles:
self.create_style(style)
def create_style(self, style: ifcopenshell.entity_instance) -> None:
@@ -963,10 +982,11 @@ class IfcImporter:
self.material_creator.styles[style.id()] = blender_material
style_elements = tool.Style.get_style_elements(blender_material)
props = tool.Style.get_material_style_props(blender_material)
if tool.Style.has_blender_external_style(style_elements):
blender_material.BIMStyleProperties.active_style_type = "External"
props.active_style_type = "External"
else:
blender_material.BIMStyleProperties.active_style_type = "Shading"
props.active_style_type = "Shading"
def place_objects_in_collections(self) -> None:
for ifc_definition_id, obj in self.added_data.items():
@@ -974,10 +994,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,8 +1050,8 @@ class IfcImporter:
self,
element: ifcopenshell.entity_instance,
shape: Union[ifcopenshell.geom.ShapeElementType, ifcopenshell.geom.ShapeType],
cartesian_point_offset=None,
) -> bpy.types.Mesh:
cartesian_point_offset: Union[npt.NDArray[np.float64], Literal[False]] = None,
) -> Union[bpy.types.Mesh, None]:
try:
if hasattr(shape, "geometry"):
# shape is ShapeElementType
@@ -1046,40 +1066,36 @@ 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,
)
# E.g. `create_mesh` is also used for IfcRelSpaceBoundary.
if element.is_a("IfcObjectDefinition"):
return tool.Loader.slice_layerset_mesh(element, mesh)
return mesh
except:
self.ifc_import_settings.logger.error("Could not create mesh for %s", element)
import traceback
@@ -1087,9 +1103,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:
@@ -1135,6 +1152,7 @@ class IfcImportSettings:
self.deflection_tolerance = 0.001
self.angular_tolerance = 0.5
self.void_limit = 30
self.style_limit = 300
# Locations greater than 1km are not considered "small sites" according to the georeferencing guide
# Users can configure this if they have to handle larger sites but beware of surveying precision
self.distance_limit = 1000
@@ -1155,7 +1173,7 @@ class IfcImportSettings:
@staticmethod
def factory(context=None, input_file=None, logger=None):
scene_diff = bpy.context.scene.DiffProperties
scene_diff = tool.Blender.get_diff_props()
props = tool.Project.get_project_props()
settings = IfcImportSettings()
settings.input_file = input_file
@@ -1172,6 +1190,7 @@ class IfcImportSettings:
settings.deflection_tolerance = props.deflection_tolerance
settings.angular_tolerance = props.angular_tolerance
settings.void_limit = props.void_limit
settings.style_limit = props.style_limit
settings.distance_limit = props.distance_limit
settings.false_origin_mode = props.false_origin_mode
try:
@@ -131,6 +131,9 @@ class AggregateDecorator:
return shader
def draw_custom_batch(self, coords, color):
indices = None
if not tool.Blender.validate_shader_batch_data(coords, indices):
return
shader = self.dotted_line_shader()
arc_lengths = [0]
@@ -141,6 +144,7 @@ class AggregateDecorator:
shader,
"LINE_STRIP",
{"position": coords, "arcLength": arc_lengths},
indices=indices,
)
matrix = bpy.context.region_data.perspective_matrix
@@ -150,6 +154,8 @@ class AggregateDecorator:
batch.draw(shader)
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
@@ -243,6 +249,8 @@ class AggregateModeDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
@@ -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):
@@ -102,6 +102,7 @@ class BIMObjectAggregateProperties(PropertyGroup):
class Objects(bpy.types.PropertyGroup):
obj: PointerProperty(type=bpy.types.Object)
previous_display_type: bpy.props.StringProperty(default="TEXTURED")
previous_hide_select: bpy.props.BoolProperty(default=False)
class BIMAggregateProperties(PropertyGroup):
+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
@@ -0,0 +1,36 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
# from . import ui, prop, operator
from . import operator
classes = (operator.ImportAlignmentCSV,)
def menu_func_import(self, context):
self.layout.operator(operator.ImportAlignmentCSV.bl_idname, text="Alignment (.csv)")
def register():
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
def unregister():
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
@@ -0,0 +1,113 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>, 2022 Yassine Oualid <yassine@sigmadimensions.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
# pyright: reportUnnecessaryTypeIgnoreComment=error
import os
import ifcopenshell.api.alignment
import ifcopenshell.api.alignment.add_stationing_to_alignment
import bpy
import json
import time
import calendar
import isodate
import bonsai.core.sequence as core
import bonsai.tool as tool
import bonsai.bim.module.sequence.helper as helper
import ifcopenshell.api.spatial
import ifcopenshell.geom
import ifcopenshell.util.sequence
import ifcopenshell.util.selector
from datetime import datetime
from dateutil import parser, relativedelta
from bpy_extras.io_utils import ImportHelper
from typing import get_args, TYPE_CHECKING
from typing_extensions import assert_never
class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.import_alignment_csv"
bl_label = "Import Alignment CSV"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
@classmethod
def poll(cls, context):
ifc_file = tool.Ifc.get()
if ifc_file is None:
cls.poll_message_set("No IFC file is loaded.")
return False
elif ifc_file.schema != "IFC4X3":
cls.poll_message_set("Schema must be IFC4x3.")
return False
return True
def _execute(self, context):
self.file = tool.Ifc.get()
start = time.time()
alignment = ifcopenshell.api.alignment.create_alignment_from_csv(self.file, self.filepath)
ifcopenshell.api.alignment.create_geometric_representation(self.file, alignment)
ifcopenshell.api.alignment.add_stationing_to_alignment(self.file, alignment=alignment, start_station=0.0)
# IFC 4.1.5.1 alignments cannot be contained in spatial structures, but can be referenced into them
sites = self.file.by_type("IfcSite")
for site in sites:
ifcopenshell.api.spatial.reference_structure(self.file, products=[alignment], relating_structure=site)
# process the generated IfcReferent for the alignment
for rel in alignment.IsNestedBy:
for referent in rel.RelatedObjects:
if referent.is_a("IfcReferent"):
referent_obj = bpy.data.objects.new(tool.Loader.get_name(referent), None)
tool.Geometry.link(referent, referent_obj)
tool.Collector.assign(referent_obj, should_clean_users_collection=False)
# an alignment can be an aggregation of multiple child alignments (ie. multiple verticals for a single horizontal)
# get all the alignment curves
curves = []
for rel in alignment.IsDecomposedBy:
for agg in rel.RelatedObjects:
if agg.is_a("IfcAlignment"):
curves.append(ifcopenshell.api.alignment.get_curve(agg)) # 3D curve
# if there aren't any curves from aggregation, then there is only a single vertical or no vertical
if len(curves) == 0:
curves.append(ifcopenshell.api.alignment.get_curve(alignment))
settings = ifcopenshell.geom.settings()
for curve in curves:
shape = ifcopenshell.geom.create_shape(settings, curve)
# create a new Blender mesh
mesh_name = tool.Loader.get_mesh_name_from_shape(shape)
mesh = bpy.data.meshes.new(mesh_name)
m = tool.Loader.convert_geometry_to_mesh(shape, mesh)
# create a new Blender object
alignment_obj = bpy.data.objects.new(tool.Loader.get_name(alignment), m)
# link the blender object to with the alignment element
tool.Geometry.link(alignment, alignment_obj)
# assign the object to the blender collections
tool.Collector.assign(alignment_obj, should_clean_users_collection=False)
self.report({"INFO"}, "Imported in %s seconds" % (time.time() - start))
@@ -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.
+90 -83
View File
@@ -42,8 +42,8 @@ import ifcopenshell.util.unit
import bonsai.tool as tool
import bonsai.bim.module.bcf.prop as bcf_prop
import bonsai.bim.module.bcf.bcfstore as bcfstore
from bpy_extras.io_utils import ImportHelper, ExportHelper
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 +55,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")
@@ -64,12 +64,14 @@ class NewBcfProject(bpy.types.Operator):
return {"FINISHED"}
class LoadBcfProject(bpy.types.Operator):
class LoadBcfProject(bpy.types.Operator, ImportHelper):
bl_idname = "bim.load_bcf_project"
bl_label = "Load BCF Project"
bl_description = "Load the BCF file."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"})
filename_ext = ".bcf"
def execute(self, context):
# Operator is also used when new project is created by not yet saved.
@@ -103,15 +105,12 @@ 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"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class UnloadBcfProject(bpy.types.Operator):
bl_idname = "bim.unload_bcf_project"
@@ -131,7 +130,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 +162,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 +244,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 +275,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 +287,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 +303,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
@@ -337,13 +340,15 @@ class EditBcfTopic(bpy.types.Operator):
return {"FINISHED"}
class SaveBcfProject(bpy.types.Operator):
class SaveBcfProject(bpy.types.Operator, ExportHelper):
bl_idname = "bim.save_bcf_project"
bl_label = "Save BCF Project"
bl_description = "Save active BCF project by the provided filepath."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"})
save_current_bcf: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
filename_ext = ".bcf"
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
@@ -360,8 +365,7 @@ class SaveBcfProject(bpy.types.Operator):
self.filepath = str(path)
return self.execute(context)
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
return ExportHelper.invoke(self, context, event)
class AddBcfTopic(bpy.types.Operator):
@@ -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,58 +1529,51 @@ 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"}
class SelectBcfHeaderFile(bpy.types.Operator):
class SelectBcfHeaderFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_bcf_header_file"
bl_label = "Select BCF Header File"
bl_description = "Select filepath for BCF header reference."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"})
filename_ext = ".ifc"
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):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectBcfBimSnippetReference(bpy.types.Operator):
class SelectBcfBimSnippetReference(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_bcf_bim_snippet_reference"
bl_label = "Select BCF BIM Snippet Reference"
bl_description = "Select filepath for BCF snippet reference."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
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):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectBcfDocumentReference(bpy.types.Operator):
class SelectBcfDocumentReference(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_bcf_document_reference"
bl_label = "Select BCF Document Reference"
bl_description = "Select filepath for BCF document reference."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
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):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class LoadBcfHeaderIfcFile(bpy.types.Operator):
bl_idname = "bim.load_bcf_header_ifc_file"
@@ -1585,7 +1590,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 +1611,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")
@@ -43,6 +43,8 @@ class BoundaryDecorator:
cls.installed = None
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
+126 -73
View File
@@ -25,6 +25,8 @@ import mathutils
import numpy as np
import multiprocessing
import ifcopenshell.api
import ifcopenshell.api.boundary
import ifcopenshell.api.root
import ifcopenshell.geom
import ifcopenshell.util.unit
import ifcopenshell.util.shape
@@ -40,6 +42,7 @@ from bonsai.bim.module.model.decorator import ProfileDecorator
from bonsai.bim.module.boundary.decorator import BoundaryDecorator
import bonsai.core
import bonsai.core.geometry
from typing import Union, Optional
def disable_editing_boundary_geometry(context):
@@ -57,7 +60,7 @@ def disable_editing_boundary_geometry(context):
class Loader:
def __init__(self, operator=None):
def __init__(self, operator: Optional[bpy.types.Operator] = None):
self.operator = operator
self.ifc_file = None
self.logger = None
@@ -66,17 +69,20 @@ class Loader:
self.fallback_settings = self.load_fallback_settings()
self.load_importer()
def create_mesh(self, boundary):
def create_mesh(self, boundary: ifcopenshell.entity_instance) -> Union[bpy.types.Mesh, None]:
# ConnectionGeometry is optional in IFC schema for some reasons.
if not boundary.ConnectionGeometry:
return None
assert self.ifc_importer
assert self.operator
surface = boundary.ConnectionGeometry.SurfaceOnRelatingElement
# Workaround for invalid geometry provided by Revit. See https://github.com/Autodesk/revit-ifc/issues/270
if surface.is_a("IfcCurveBoundedPlane") and not getattr(surface, "InnerBoundaries", None):
surface.InnerBoundaries = ()
try:
shape = ifcopenshell.geom.create_shape(self.settings, surface)
mesh = self.ifc_importer.create_mesh(None, shape)
mesh = self.ifc_importer.create_mesh(boundary, shape)
assert mesh
tool.Loader.link_mesh(shape, mesh)
except RuntimeError:
# Fallback solution for invalid geometry provided by Revit. (InnerBoundaries cuting OuterBoundary)
@@ -96,7 +102,8 @@ class Loader:
return None
mesh = bpy.data.meshes.new(str(surface.id()))
bm = bmesh.new()
verts = [bm.verts.new(shape.verts[i : i + 3]) for i in range(0, len(shape.verts), 3)]
coords = ifcopenshell.util.shape.get_vertices(shape)
verts = [bm.verts.new(coord) for coord in coords]
bm.faces.new(verts)
for inner_boundary in surface.InnerBoundaries:
try:
@@ -107,13 +114,15 @@ class Loader:
f"Skipping an inner boundary for IfcRelSpaceBoundary with ID {boundary.id()}. Geometry might be invalid",
)
return None
verts = [bm.verts.new(shape.verts[i : i + 3]) for i in range(0, len(shape.verts), 3)]
coords = ifcopenshell.util.shape.get_vertices(shape)
verts = [bm.verts.new(coord) for coord in coords]
bm.faces.new(verts)
for i in range(len(verts) - 1):
bm.edges.new(verts[i : i + 2])
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()
@@ -130,7 +139,7 @@ class Loader:
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
return settings
def load_importer(self):
def load_importer(self) -> None:
self.ifc_file = tool.Ifc.get()
self.logger = logging.getLogger("ImportIFC")
ifc_import_settings = import_ifc.IfcImportSettings.factory(bpy.context, IfcStore.path, self.logger)
@@ -279,7 +288,7 @@ class SelectProjectBoundaries(bpy.types.Operator):
return {"FINISHED"}
def get_colour(ifc_boundary):
def get_colour(ifc_boundary: ifcopenshell.entity_instance) -> tuple[float, float, float, float]:
"""Return a color depending on IfcClass given"""
product_colors = {
"IfcWall": (0.7, 0.3, 0, 1),
@@ -315,7 +324,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 +426,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"}
@@ -582,47 +592,70 @@ class DecorateBoundaries(bpy.types.Operator, tool.Ifc.Operator):
class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_boundary"
bl_label = "Add Boundary"
bl_description = (
"There are two options for this operator:\n"
"- just 1 IfcSpace is selected - generate a boundary based on "
"potential nearby (0.1m) boundary elements.\n"
"- 1 IfcSpace and 1 IfcElement are selected - generate a boundary "
"for IfcSpace and use selected IfcElement as boundary's building element."
)
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
relating_space = None
related_building_element = None
relating_space_obj = None
related_building_element_obj = None
parent_boundaries = []
parent_boundaries: list[ifcopenshell.entity_instance] = []
objs = context.selected_objects
objs = tool.Blender.get_selected_objects()
ifc_objects = {element: obj for obj in objs if (element := tool.Ifc.get_entity(obj))}
if len(ifc_objects) not in (1, 2):
self.report({"ERROR"}, "Please select one or two IFC elements.")
return {"CANCELLED"}
if not any((element := tool.Ifc.get_entity(obj)) and element.is_a("IfcSpace") for obj in objs):
self.report({"INFO"}, "No IfcSpace elements selected.")
return {"FINISHED"}
if not any(element.is_a("IfcSpace") for element in ifc_objects):
self.report({"ERROR"}, "One of the selected objects must be an IfcSpace.")
return {"CANCELLED"}
if len(objs) == 2:
# The user may select two objects, a space and its related building element
for obj in objs:
element = tool.Ifc.get_entity(obj)
if not element:
continue
if element.is_a("IfcSpace"):
relating_space = element
relating_space_obj = obj
else:
related_building_element = element
related_building_element_obj = obj
# Validate input.
space = next((e for e in ifc_objects.keys() if e.is_a("IfcSpace")), None)
if not space:
self.report({"ERROR"}, "One of the selected objects must be an IfcSpace.")
return {"CANCELLED"}
element = next((e for e in ifc_objects.keys() if e.is_a("IfcElement")), None)
if not element:
self.report({"ERROR"}, "One of the selected objects must be an IfcElement.")
return {"CANCELLED"}
parent_boundary = self.create_element_boundary(
context, relating_space, relating_space_obj, related_building_element, related_building_element_obj
context, space, ifc_objects[space], element, ifc_objects[element]
)
if parent_boundary:
parent_boundaries.append(parent_boundary)
if not parent_boundary:
self.report({"ERROR"}, "Failed to create a boundary.")
return {"FINISHED"}
parent_boundaries.append(parent_boundary)
elif len(objs) == 1:
space = next(iter(ifc_objects))
if not space.is_a("IfcSpace"):
self.report({"ERROR"}, "1 element selected but it's not an IfcSpace - please, select IfcSpace..")
return {"CANCELLED"}
# New prototype, old code not yet removed. Still testing.
space = tool.Ifc.get_entity(objs[0])
if space.is_a("IfcSpace"):
self.auto_generate_boundaries(space, objs[0])
elif len(objs) == 1:
res = self.auto_generate_boundaries(space, ifc_objects[space])
if isinstance(res, str):
self.report({"ERROR"}, res)
return {"FINISHED"}
parent_boundaries.extend(res)
elif len(objs) == 1 and False:
# Optionally the user may select just the space, and the building element shall be auto-detected
# TODO : refactor to be able to generate all boundaries for selected space automatically or with an option
relating_space = None
related_building_element = None
relating_space_obj = None
related_building_element_obj = None
def msg(self, context):
self.layout.label(text="Please set an active container to detect space boundaries from.")
@@ -659,7 +692,17 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
obj = tool.Ifc.get_object(parent_boundary)
obj.select_set(True)
def auto_generate_boundaries(self, space, space_obj):
def auto_generate_boundaries(
self, space: ifcopenshell.entity_instance, space_obj: bpy.types.Object
) -> Union[str, list[ifcopenshell.entity_instance]]:
"""
:return: list of created boundaries or a string with error description.
"""
ifc_file = tool.Ifc.get()
props = tool.Model.get_model_props()
boundaries: list[ifcopenshell.entity_instance] = []
assert isinstance(space_obj.data, bpy.types.Mesh)
# Identify all potential building elements
# TODO: don't select everything, use AABB culling in Blender
building_elements = (
@@ -679,7 +722,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
shapes = {}
settings = ifcopenshell.geom.settings()
settings.set("disable-opening-subtractions", True)
iterator = ifcopenshell.geom.iterator(settings, tool.Ifc.get(), multiprocessing.cpu_count(), include=include)
iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=include)
if iterator.initialize():
while True:
tree.add_element(iterator.get_native())
@@ -692,12 +735,12 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
building_elements = [e for e in tree.select(space, extend=0.1) if e != space]
if not building_elements:
return
return "No building elements found to create boundaries."
# Create a dissolved bmesh for the space
space_bm = bmesh.new()
space_bm.from_mesh(space_obj.data)
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts, edges=space_bm.edges)
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts[:], edges=space_bm.edges[:])
# Create dissolved bmeshes for all boundary elements
building_element_bms = {}
@@ -775,9 +818,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 = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
if building_element.is_a("IfcVirtualElement"):
parent_boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
else:
@@ -809,6 +850,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
exterior_boundary_polygon, space_face_matrix
)
self.set_boundary_name(parent_boundary)
boundaries.append(parent_boundary)
for rel in getattr(building_element, "HasOpenings", []):
opening = rel.RelatedOpeningElement
@@ -854,9 +896,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 = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
boundary.RelatingSpace = space
boundary.RelatedBuildingElement = filling or opening
boundary.ConnectionGeometry = self.create_connection_geometry_from_polygon(
@@ -870,28 +910,39 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
if boundary.is_a() != "IfcRelSpaceBoundary":
boundary.ParentBoundary = parent_boundary
self.set_boundary_name(boundary)
boundaries.append(boundary)
return boundaries
def create_element_boundary(
self, context, relating_space, relating_space_obj, related_building_element, related_building_element_obj
):
if not relating_space or not related_building_element:
return
self,
context: bpy.types.Context,
space: ifcopenshell.entity_instance,
space_obj: bpy.types.Object,
building_element: ifcopenshell.entity_instance,
building_element_obj: bpy.types.Object,
) -> Union[ifcopenshell.entity_instance, None]:
"""Create element boundary for IfcSpace and IfcElement as building element.
:return: IfcRelSpaceBoundary or ``None`` if couldn't find a face on ``space_obj``
that would be close enough to ``related_building_element_obj``.
"""
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()
bm.from_mesh(relating_space_obj.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi * 2 / 360, verts=bm.verts, edges=bm.edges)
space_bm = bmesh.new()
assert isinstance(space_obj.data, bpy.types.Mesh)
space_bm.from_mesh(space_obj.data)
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts[:], edges=space_bm.edges[:])
target_distance = inf
target_face = None
for face in bm.faces:
centroid = relating_space_obj.matrix_world @ face.calc_center_median()
raycast = related_building_element_obj.closest_point_on_mesh(
related_building_element_obj.matrix_world.inverted() @ centroid, distance=1
)
to_building_obj_space = building_element_obj.matrix_world.inverted()
for face in space_bm.faces:
centroid = space_obj.matrix_world @ face.calc_center_median()
raycast = building_element_obj.closest_point_on_mesh(to_building_obj_space @ centroid, distance=1)
if raycast[0]:
distance = (related_building_element_obj.matrix_world @ raycast[1] - centroid).length
distance = (building_element_obj.matrix_world @ raycast[1] - centroid).length
if distance < target_distance:
target_face = face
target_distance = distance
@@ -906,9 +957,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
target_face_polygon = shapely.Polygon([tuple((target_face_matrix_i @ v).xy) for v in target_face_verts])
related_building_element_polygon = self.get_flattened_polygon(
related_building_element, relating_space_obj, target_face_matrix_i
)
related_building_element_polygon = self.get_flattened_polygon(building_element, space_obj, target_face_matrix_i)
gross_boundary_polygon = target_face_polygon.intersection(related_building_element_polygon)
@@ -924,7 +973,8 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
):
return
parent_boundary = tool.Ifc.run("root.create_entity", ifc_class=context.scene.BIMModelProperties.boundary_class)
ifc_file = tool.Ifc.get()
parent_boundary = ifcopenshell.api.root.create_entity(ifc_file, 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
@@ -937,14 +987,14 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
# space.
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
for rel in getattr(related_building_element, "HasOpenings", []):
for rel in getattr(building_element, "HasOpenings", []):
opening = rel.RelatedOpeningElement
if not opening.HasFillings:
continue
# TODO: HasFilling is a zero to many relationship. How to handle many ?
filling = opening.HasFillings[0].RelatedBuildingElement
opening_polygon = self.get_flattened_polygon(opening, relating_space_obj, target_face_matrix_i)
opening_polygon = self.get_flattened_polygon(opening, space_obj, target_face_matrix_i)
# An inner boundary is supposed to overlap its parent boundary according to IFC4 documentation
# so we extend our exterior boundary with all opening which has a filling
@@ -959,11 +1009,11 @@ 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.RelatingSpace = relating_space
boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
boundary.RelatingSpace = space
boundary.RelatedBuildingElement = filling
boundary.ConnectionGeometry = connection_geometry
if related_building_element.is_a("IfcVirtualElement"):
if building_element.is_a("IfcVirtualElement"):
boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
else:
boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
@@ -975,20 +1025,23 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
connection_geometry = self.create_connection_geometry_from_polygon(
exterior_boundary_polygon, target_face_matrix
)
parent_boundary.RelatingSpace = relating_space
parent_boundary.RelatedBuildingElement = related_building_element
parent_boundary.RelatingSpace = space
parent_boundary.RelatedBuildingElement = building_element
parent_boundary.ConnectionGeometry = connection_geometry
self.set_boundary_name(parent_boundary)
return parent_boundary
def get_face_matrix(self, p1, p2, p3):
def get_face_matrix(self, p1: Vector, p2: Vector, p3: Vector) -> Matrix:
edge1 = p2 - p1
edge2 = p3 - p1
normal = edge1.cross(edge2)
assert isinstance(normal, Vector)
z_axis = normal.normalized()
x_axis = p2 - p1
x_axis.normalize()
y_axis = z_axis.cross(x_axis)
assert isinstance(y_axis, Vector)
mat = Matrix()
mat.col[0][:3] = x_axis
@@ -1058,7 +1111,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
return surface
def set_boundary_name(self, boundary):
def set_boundary_name(self, boundary: ifcopenshell.entity_instance):
"""
By convention 1stLevel and 2ndLevel boundary have specific name and description
See https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRelSpaceBoundary.htm
+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 -1
View File
@@ -52,7 +52,7 @@ class BrickschemaData:
def active_relations(cls):
if BrickStore.graph is None:
return []
props = bpy.context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
try:
brick = props.bricks[props.active_brick_index]
except:
+21 -21
View File
@@ -22,29 +22,26 @@ import ifcopenshell.api
import bonsai.tool as tool
import bonsai.core.brick as core
import bonsai.bim.handler
from bpy_extras.io_utils import ImportHelper, ExportHelper
from bonsai.bim.ifc import IfcStore
from bonsai.tool.brick import BrickStore
class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator):
class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.load_brick_project"
bl_label = "Load Brickschema Project"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Load in a Brick project from a file"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"})
filename_ext = ".ttl"
def _execute(self, context):
if os.path.exists(self.filepath) and "ttl" in os.path.splitext(self.filepath)[1].lower():
root = context.scene.BIMBrickProperties.brick_list_root
root = tool.Brick.get_brick_props().brick_list_root
core.load_brick_project(tool.Brick, filepath=self.filepath, brick_root=root)
else:
self.report({"ERROR"}, f"Failed to load {self.filepath}")
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ViewBrickClass(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.view_brick_class"
@@ -111,10 +108,10 @@ class AssignBrickReference(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Assign the selected Ifc entity to the selected Brick entity"
def _execute(self, context):
if not context.active_object:
if not (obj := context.active_object) or not (element := tool.Ifc.get_entity(obj)):
self.report({"ERROR"}, f"No Ifc selected")
return
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
try:
props.bricks[props.active_brick_index]
except:
@@ -123,7 +120,7 @@ class AssignBrickReference(bpy.types.Operator, tool.Ifc.Operator):
core.assign_brick_reference(
tool.Ifc,
tool.Brick,
element=tool.Ifc.get_entity(context.active_object),
element=element,
library=tool.Ifc.get().by_id(int(props.libraries)),
brick_uri=props.bricks[props.active_brick_index].uri,
)
@@ -136,7 +133,7 @@ class AddBrick(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Create the Brick entity"
def _execute(self, context):
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
core.add_brick(
tool.Ifc,
tool.Brick,
@@ -155,7 +152,7 @@ class AddBrickRelation(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Create the Brick relationship"
def _execute(self, context):
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
brick = props.bricks[props.active_brick_index]
if props.new_brick_relation_type == "http://www.w3.org/2000/01/rdf-schema#label":
object = props.new_brick_relation_object
@@ -173,7 +170,7 @@ class ConvertIfcToBrick(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Convert Ifc entities and relations to Brick entities and relations"
def _execute(self, context):
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
library = None
if props.libraries:
library = tool.Ifc.get().by_id(int(props.libraries))
@@ -201,7 +198,8 @@ class NewBrickFile(bpy.types.Operator):
return {"FINISHED"}
def _execute(self, context):
root = context.scene.BIMBrickProperties.brick_list_root
props = tool.Brick.get_brick_props()
root = props.brick_list_root
core.new_brick_file(tool.Brick, brick_root=root)
def rollback(self, data):
@@ -230,7 +228,7 @@ class RemoveBrick(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Delete this entity"
def _execute(self, context):
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
core.remove_brick(
tool.Ifc,
tool.Brick,
@@ -239,18 +237,20 @@ class RemoveBrick(bpy.types.Operator, tool.Ifc.Operator):
)
class SerializeBrick(bpy.types.Operator):
class SerializeBrick(bpy.types.Operator, ExportHelper):
bl_idname = "bim.serialize_brick"
bl_label = "Serialize Brick"
# Prevents crash on Blender 4.4.0.
bl_description = "Save active Brick project by the provided filepath."
filename_ext = ".ttl"
filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"})
def invoke(self, context, event):
if self.should_save_as or not BrickStore.path:
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
return ExportHelper.invoke(self, context, event)
else:
return self.execute(context)
@@ -273,7 +273,7 @@ class AddBrickNamespace(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Bind a new namespace to the Brick project"
def _execute(self, context):
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
alias = props.new_brick_namespace_alias
uri = props.new_brick_namespace_uri
core.add_brick_namespace(tool.Brick, alias=alias, uri=uri)
@@ -288,6 +288,6 @@ class RemoveBrickRelation(bpy.types.Operator, tool.Ifc.Operator):
object: bpy.props.StringProperty(name="Object")
def _execute(self, context):
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
brick = props.bricks[props.active_brick_index]
core.remove_brick_relation(tool.Brick, brick_uri=brick.uri, predicate=self.predicate, object=self.object)
+35 -4
View File
@@ -33,6 +33,7 @@ from bpy.props import (
import bonsai.core.brick as core
import bonsai.tool.brick as tool
from bonsai.tool.brick import BrickStore
from typing import TYPE_CHECKING
def update_active_brick_index(self, context):
@@ -74,13 +75,13 @@ def get_brick_relations(self, context):
return BRICK_RELATIONS_ENUM_ITEMS
def update_view(self, context):
root = context.scene.BIMBrickProperties.brick_list_root
def update_view(self: "BIMBrickProperties", context: bpy.types.Context) -> None:
root = self.brick_list_root
core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=False)
def split_screen_update_view(self, context):
root = context.scene.BIMBrickProperties.split_screen_brick_list_root
def split_screen_update_view(self: "BIMBrickProperties", context: bpy.types.Context) -> None:
root = self.split_screen_brick_list_root
core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=True)
@@ -90,6 +91,11 @@ class Brick(PropertyGroup):
uri: StringProperty(name="URI")
total_items: IntProperty(name="Total Items")
if TYPE_CHECKING:
label: str
uri: str
total_items: int
class BIMBrickProperties(PropertyGroup):
active_brick_class: StringProperty(name="Active Brick Class")
@@ -124,3 +130,28 @@ class BIMBrickProperties(PropertyGroup):
split_screen_brick_list_root: EnumProperty(
name="Split Screen Brick List Root", items=get_brick_roots, update=split_screen_update_view
)
if TYPE_CHECKING:
active_brick_class: str
brick_breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
bricks: bpy.types.bpy_prop_collection_idprop[Brick]
active_brick_index: int
libraries: str
set_list_root_toggled: bool
brick_list_root: str
namespace: str
new_brick_namespace_alias: str
new_brick_namespace_uri: str
new_brick_label: str
brick_entity_create_type: str
brick_entity_class: str
brick_create_relations_toggled: bool
brick_edit_relations_toggled: bool
new_brick_relation_type: str
new_brick_relation_object: str
split_screen_toggled: bool
split_screen_bricks: bpy.types.bpy_prop_collection_idprop[Brick]
split_screen_active_brick_index: int
split_screen_active_brick_class: str
split_screen_brick_breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
split_screen_brick_list_root: str
+19 -7
View File
@@ -45,7 +45,7 @@ class BIM_PT_brickschema_project_info(Panel):
bl_parent_id = "BIM_PT_brickschema"
def draw(self, context):
self.props = context.scene.BIMBrickProperties
self.props = tool.Brick.get_brick_props()
if not BrickschemaData.data["is_loaded"]:
row = self.layout.row(align=True)
@@ -87,7 +87,7 @@ class BIM_PT_brickschema_namespaces(Panel):
return BrickStore.graph != None
def draw(self, context):
self.props = context.scene.BIMBrickProperties
self.props = tool.Brick.get_brick_props()
row = self.layout.row(align=True)
row.label(text="Active Namespace:")
@@ -122,7 +122,7 @@ class BIM_PT_brickschema_create_entity(Panel):
return BrickStore.graph != None
def draw(self, context):
self.props = context.scene.BIMBrickProperties
self.props = tool.Brick.get_brick_props()
# TO DO: hide this if selected entity already has a reference, or something similar
row = self.layout.row(align=True)
@@ -156,7 +156,7 @@ class BIM_PT_brickschema_viewport(Panel):
return BrickStore.graph != None
def draw(self, context):
self.props = context.scene.BIMBrickProperties
self.props = tool.Brick.get_brick_props()
row = self.layout.row(align=True)
row.column().alignment = "RIGHT"
@@ -178,7 +178,14 @@ class BIM_PT_brickschema_viewport(Panel):
row = grid_left.row()
BIM_UL_bricks.split_screen = False
row.template_list("BIM_UL_bricks", "", self.props, "bricks", self.props, "active_brick_index")
row.template_list(
BIM_UL_bricks.__name__,
BIM_UL_bricks.__name__ + "_bricks",
self.props,
"bricks",
self.props,
"active_brick_index",
)
if self.props.split_screen_toggled:
grid_right = grid.column(align=True)
@@ -195,7 +202,12 @@ class BIM_PT_brickschema_viewport(Panel):
row = grid_right.row()
BIM_UL_bricks.split_screen = True
row.template_list(
"BIM_UL_bricks", "", self.props, "split_screen_bricks", self.props, "split_screen_active_brick_index"
BIM_UL_bricks.__name__,
BIM_UL_bricks.__name__ + "_split_screen_bricks",
self.props,
"split_screen_bricks",
self.props,
"split_screen_active_brick_index",
)
if BrickschemaData.data["active_relations"]:
@@ -292,7 +304,7 @@ class BIM_PT_ifc_brickschema_references(Panel):
def draw(self, context):
if not BrickschemaReferencesData.is_loaded:
BrickschemaReferencesData.load()
self.props = context.scene.BIMBrickProperties
self.props = tool.Brick.get_brick_props()
if not BrickschemaReferencesData.data["is_loaded"]:
row = self.layout.row()
-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", ""),
}
@@ -49,6 +49,8 @@ class ClashDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
@@ -60,8 +62,9 @@ class ClashDecorator:
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
text = context.scene.BIMClashProperties.active_clash_text
p = context.scene.BIMClashProperties.p1.lerp(context.scene.BIMClashProperties.p2, 0.5)
props = tool.Clash.get_clash_props()
text = props.active_clash_text
p = props.p1.lerp(props.p2, 0.5)
font_id = 0
blf.size(font_id, 12)
@@ -92,7 +95,8 @@ class ClashDecorator:
# general shader
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
selected_vertices = [context.scene.BIMClashProperties.p1, context.scene.BIMClashProperties.p2]
props = tool.Clash.get_clash_props()
selected_vertices = [props.p1, props.p2]
selected_edges = []
if selected_vertices[0] != selected_vertices[1]:
selected_edges = [[0, 1]]
+77 -76
View File
@@ -24,26 +24,20 @@ import logging
import numpy as np
import ifcopenshell
import bonsai.tool as tool
from bpy_extras.io_utils import ExportHelper, ImportHelper
from math import radians
from mathutils import Matrix, Vector
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.clash.decorator import ClashDecorator
class ExportClashSets(bpy.types.Operator):
class ExportClashSets(bpy.types.Operator, ExportHelper):
bl_idname = "bim.export_clash_sets"
bl_label = "Export Clash Sets"
bl_description = "Export clash sets to a selected file"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
clash_sets = tool.Clash.export_clash_sets()
@@ -52,26 +46,24 @@ class ExportClashSets(bpy.types.Operator):
return {"FINISHED"}
class ImportClashSets(bpy.types.Operator):
class ImportClashSets(bpy.types.Operator, ImportHelper):
bl_idname = "bim.import_clash_sets"
bl_label = "Import Clash Sets"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Import clash sets from a selected file"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
return ImportHelper.invoke(self, context, event)
def execute(self, context):
tool.Clash.load_clash_sets(self.filepath)
context.scene.BIMClashProperties.clash_sets.clear()
props = tool.Clash.get_clash_props()
props.clash_sets.clear()
for clash_set in tool.Clash.get_clash_sets():
new = context.scene.BIMClashProperties.clash_sets.add()
new = props.clash_sets.add()
new.name = clash_set["name"]
new.mode = clash_set["mode"]
if new.mode == "intersection":
@@ -106,7 +98,8 @@ class AddClashSet(bpy.types.Operator):
bl_description = "Add a clash set"
def execute(self, context):
new = context.scene.BIMClashProperties.clash_sets.add()
props = tool.Clash.get_clash_props()
new = props.clash_sets.add()
new.name = "New Clash Set"
return {"FINISHED"}
@@ -119,7 +112,8 @@ class RemoveClashSet(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
context.scene.BIMClashProperties.clash_sets.remove(self.index)
props = tool.Clash.get_clash_props()
props.clash_sets.remove(self.index)
return {"FINISHED"}
@@ -131,7 +125,8 @@ class AddClashSource(bpy.types.Operator):
group: bpy.props.StringProperty()
def execute(self, context):
clash_set = context.scene.BIMClashProperties.active_clash_set
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
source = getattr(clash_set, self.group).add()
return {"FINISHED"}
@@ -145,78 +140,74 @@ class RemoveClashSource(bpy.types.Operator):
group: bpy.props.StringProperty()
def execute(self, context):
clash_set = context.scene.BIMClashProperties.active_clash_set
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
getattr(clash_set, self.group).remove(self.index)
return {"FINISHED"}
class SelectClashSource(bpy.types.Operator):
class SelectClashSource(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_clash_source"
bl_label = "Select Clash Source"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select an IFC file to add as a clash source"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
index: bpy.props.IntProperty()
group: bpy.props.StringProperty()
filename_ext = ".ifc"
def execute(self, context):
clash_set = context.scene.BIMClashProperties.active_clash_set
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
getattr(clash_set, self.group)[self.index].name = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectClashResults(bpy.types.Operator):
class SelectClashResults(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_clash_results"
bl_label = "Select Clash Results"
bl_description = "Select filepath for clash results."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
context.scene.BIMClashProperties.clash_results_path = self.filepath
props = tool.Clash.get_clash_props()
props.clash_results_path = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectSmartGroupedClashesPath(bpy.types.Operator):
class SelectSmartGroupedClashesPath(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_smart_grouped_clashes_path"
bl_label = "Select Smart-Grouped Clashes Path"
bl_description = "Select filepath for smart-grouped clashes."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
context.scene.BIMClashProperties.smart_grouped_clashes_path = self.filepath
props = tool.Clash.get_clash_props()
props.smart_grouped_clashes_path = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ExecuteIfcClash(bpy.types.Operator):
class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
bl_idname = "bim.execute_ifc_clash"
bl_label = "Execute IFC Clash"
bl_description = "Execute clash detection and save the information to a .bcf or .json file"
filter_glob: bpy.props.StringProperty(default="*.bcf;*.json", options={"HIDDEN"})
format: bpy.props.EnumProperty(name="Format", items=[(i, i, "") for i in ("bcf", "json")])
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
@property
def filename_ext(self) -> str:
return f".{self.format.lower()}"
def invoke(self, context, event):
if self.filepath:
return self.execute(context)
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
return ExportHelper.invoke(self, context, event)
def execute(self, context):
from ifcclash import ifcclash
self.props = context.scene.BIMClashProperties
self.props = tool.Clash.get_clash_props()
_, extension = os.path.splitext(self.filepath)
if extension != ".bcf":
@@ -284,27 +275,27 @@ class ExecuteIfcClash(bpy.types.Operator):
return {"FINISHED"}
class SelectIfcClashResults(bpy.types.Operator):
class SelectIfcClashResults(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_ifc_clash_results"
bl_label = "Select IFC Clash Results"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select the clashing IFC geometry stored in a file"
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
return ImportHelper.invoke(self, context, event)
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)
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
clash_props = tool.Clash.get_clash_props()
assert clash_props.active_clash_set
clash_set_name = clash_props.active_clash_set.name
global_ids = []
for clash_set in clash_sets:
if clash_set["name"] != clash_set_name:
@@ -316,13 +307,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 +327,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
@@ -354,7 +347,7 @@ class SelectClash(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
self.props = context.scene.BIMClashProperties
self.props = tool.Clash.get_clash_props()
clash_set = tool.Clash.get_clash_set(self.props.active_clash_set.name)
active_clash = self.props.active_clash
clash = tool.Clash.get_clash(clash_set, active_clash.a_global_id, active_clash.b_global_id)
@@ -388,13 +381,15 @@ class SmartClashGroup(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.scene.BIMClashProperties.clash_results_path
props = tool.Clash.get_clash_props()
return bool(props.clash_results_path)
def execute(self, context):
from ifcclash import ifcclash
settings = ifcclash.ClashSettings()
self.filepath = bpy.path.ensure_ext(context.scene.BIMClashProperties.clash_results_path, ".json")
props = tool.Clash.get_clash_props()
self.filepath = bpy.path.ensure_ext(props.clash_results_path, ".json")
settings.output = self.filepath
settings.logger = logging.getLogger("Clash")
settings.logger.setLevel(logging.DEBUG)
@@ -404,19 +399,18 @@ class SmartClashGroup(bpy.types.Operator):
clash_sets = json.load(f)
# execute the smart grouping
save_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json")
smart_grouped_clashes = ifc_clasher.smart_group_clashes(
clash_sets, context.scene.BIMClashProperties.smart_clash_grouping_max_distance
)
save_path = bpy.path.ensure_ext(props.smart_grouped_clashes_path, ".json")
smart_grouped_clashes = ifc_clasher.smart_group_clashes(clash_sets, props.smart_clash_grouping_max_distance)
# save smart_groups to json
with open(save_path, "w") as f:
f.write(json.dumps(smart_grouped_clashes))
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
assert props.active_clash_set
clash_set_name = props.active_clash_set.name
# Reset the list of smart_clash_groups for the UI
context.scene.BIMClashProperties.smart_clash_groups.clear()
props.smart_clash_groups.clear()
for clash_set, smart_groups in smart_grouped_clashes.items():
# Only select the clashes that correspond to the actively selected IFC Clash Set
@@ -424,7 +418,7 @@ class SmartClashGroup(bpy.types.Operator):
continue
else:
for smart_group, global_id_pairs in smart_groups[0].items():
new_group = context.scene.BIMClashProperties.smart_clash_groups.add()
new_group = props.smart_clash_groups.add()
new_group.number = f"{smart_group}"
for pair in global_id_pairs:
@@ -442,18 +436,21 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.scene.BIMClashProperties.active_clash_set
props = tool.Clash.get_clash_props()
return bool(props.active_clash_set)
def execute(self, context):
smart_groups_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json")
props = tool.Clash.get_clash_props()
smart_groups_path = bpy.path.ensure_ext(props.smart_grouped_clashes_path, ".json")
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
assert props.active_clash_set
clash_set_name = props.active_clash_set.name
with open(smart_groups_path) as f:
smart_grouped_clashes = json.load(f)
# Reset the list of smart_clash_groups for the UI
context.scene.BIMClashProperties.smart_clash_groups.clear()
props.smart_clash_groups.clear()
for clash_set, smart_groups in smart_grouped_clashes.items():
# Only select the clashes that correspond to the actively selected IFC Clash Set
@@ -461,12 +458,12 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator):
continue
else:
for smart_group, global_id_pairs in smart_groups[0].items():
new_group = context.scene.BIMClashProperties.smart_clash_groups.add()
new_group = props.smart_clash_groups.add()
new_group.number = f"{smart_group}"
for pair in global_id_pairs:
for guid in pair:
new_global_id = new_group.global_ids.add()
new_global_id.guid = guid
new_global_id.name = guid
return {"FINISHED"}
@@ -478,15 +475,19 @@ 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
props = tool.Clash.get_clash_props()
return tool.Ifc.get() and context.visible_objects and props.active_smart_group
def execute(self, context):
selected_smart_group = context.scene.BIMClashProperties.active_smart_group
products = []
ifc_file = tool.Ifc.get()
props = tool.Clash.get_clash_props()
selected_smart_group = props.active_smart_group
assert selected_smart_group
products: list[ifcopenshell.entity_instance] = []
for global_id in selected_smart_group.global_ids:
try:
products.append(tool.Ifc.get().by_guid(global_id.guid))
except:
products.append(ifc_file.by_guid(global_id.name))
except RuntimeError:
continue
tool.Spatial.select_products(products, unhide=True)
context_override = tool.Blender.get_viewport_context()
+54 -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 bonsai.bim.prop import StrProperty, Attribute, BIMFilterGroup
from bpy.types import PropertyGroup
from bpy.props import (
@@ -29,6 +30,8 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from mathutils import Vector
from typing import TYPE_CHECKING, Literal, Union
class ClashSource(PropertyGroup):
@@ -43,6 +46,10 @@ class ClashSource(PropertyGroup):
name="Mode",
)
if TYPE_CHECKING:
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
mode: Literal["a", "i", "e"]
class Clash(PropertyGroup):
a_global_id: StringProperty(name="A")
@@ -51,9 +58,15 @@ class Clash(PropertyGroup):
b_name: StringProperty(name="B Name")
status: BoolProperty(name="Status", default=False)
if TYPE_CHECKING:
a_global_id: str
b_global_id: str
a_name: str
b_name: str
status: bool
class ClashSet(PropertyGroup):
name: StringProperty(name="Name")
mode: EnumProperty(
items=[
(
@@ -76,11 +89,25 @@ class ClashSet(PropertyGroup):
b: CollectionProperty(name="Group B", type=ClashSource)
clashes: CollectionProperty(name="Clashes", type=Clash)
if TYPE_CHECKING:
mode: Literal["intersection", "collision", "clearance"]
tolerance: float
clearance: float
allow_touching: bool
check_all: bool
a: bpy.types.bpy_prop_collection_idprop[ClashSource]
b: bpy.types.bpy_prop_collection_idprop[ClashSource]
clashes: bpy.types.bpy_prop_collection_idprop[Clash]
class SmartClashGroup(PropertyGroup):
number: StringProperty(name="Number")
global_ids: CollectionProperty(name="GlobalIDs", type=StrProperty)
if TYPE_CHECKING:
number: str
global_ids: bpy.types.bpy_prop_collection_idprop[StrProperty]
class BIMClashProperties(PropertyGroup):
blender_clash_set_a: CollectionProperty(name="Blender Clash Set A", type=StrProperty)
@@ -107,17 +134,33 @@ class BIMClashProperties(PropertyGroup):
subtype="FILE_PATH",
)
@property
def active_clash_set(self):
if self.active_clash_set_index < len(self.clash_sets):
return self.clash_sets[self.active_clash_set_index]
if TYPE_CHECKING:
blender_clash_set_a: bpy.types.bpy_prop_collection_idprop[StrProperty]
blender_clash_set_b: bpy.types.bpy_prop_collection_idprop[StrProperty]
clash_sets: bpy.types.bpy_prop_collection_idprop[ClashSet]
should_create_clash_snapshots: bool
clash_results_path: str
smart_grouped_clashes_path: str
active_clash_set_index: int
active_clash_index: int
smart_clash_groups: bpy.types.bpy_prop_collection_idprop[SmartClashGroup]
active_smart_group_index: int
smart_clash_grouping_max_distance: int
p1: Vector
p2: Vector
active_clash_text: str
export_path: str
@property
def active_smart_group(self):
if self.active_smart_group_index < len(self.smart_clash_groups):
return self.smart_clash_groups[self.active_smart_group_index]
def active_clash_set(self) -> Union[ClashSet, None]:
return tool.Blender.get_active_uilist_element(self.clash_sets, self.active_clash_set_index)
@property
def active_clash(self):
if self.active_clash_index < len(self.active_clash_set.clashes):
return self.active_clash_set.clashes[self.active_clash_index]
def active_smart_group(self) -> Union[SmartClashGroup, None]:
return tool.Blender.get_active_uilist_element(self.smart_clash_groups, self.active_smart_group_index)
@property
def active_clash(self) -> Union[Clash, None]:
if not (clash_set := self.active_clash_set):
return None
return tool.Blender.get_active_uilist_element(clash_set.clashes, self.active_clash_index)
+3 -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
import bonsai.bim.helper
from bpy.types import Panel
from bonsai.bim.module.clash.data import ClashData
@@ -36,9 +37,7 @@ class BIM_PT_ifcclash(Panel):
ClashData.load()
layout = self.layout
scene = context.scene
props = scene.BIMClashProperties
props = tool.Clash.get_clash_props()
row = layout.row(align=True)
row.operator("bim.add_clash_set")
@@ -157,7 +156,7 @@ class BIM_PT_smart_clash_manager(Panel):
def draw(self, context):
layout = self.layout
props = context.scene.BIMClashProperties
props = tool.Clash.get_clash_props()
row = layout.row()
layout.label(text="Select clash results to 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:
@@ -145,11 +145,8 @@ class CostClassificationsData(ReferencesData):
@classmethod
def references(cls):
results = []
element = tool.Ifc.get().by_id(
bpy.context.scene.BIMCostProperties.cost_items[
bpy.context.scene.BIMCostProperties.active_cost_item_index
].ifc_definition_id
)
props = tool.Cost.get_cost_props()
element = tool.Ifc.get().by_id(props.cost_items[props.active_cost_item_index].ifc_definition_id)
if element:
for reference in ifcopenshell.util.classification.get_references(element):
data = reference.get_info()
@@ -25,19 +25,16 @@ import ifcopenshell.util.classification
import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.bim.helper
from bpy_extras.io_utils import ImportHelper
from bonsai.bim.ifc import IfcStore
class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator):
class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.load_classification_library"
bl_label = "Load Classification Library"
bl_description = "Load classification library from the provided filepath."
filename_ext = ".ifc"
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def _execute(self, context):
IfcStore.classification_file = ifcopenshell.open(self.filepath)
@@ -201,7 +198,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 +237,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(),
@@ -459,6 +457,7 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
for prop in blender_properties:
properties[prop.name] = prop.get_value()
# TODO: is this still the correct approach?
if classification_pset.name == "undefined_set":
if "ObjectType" in properties:
if hasattr(element, "ObjectType"):
@@ -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
@@ -339,7 +338,8 @@ class BIM_PT_cost_classifications(Panel, ReferenceUI):
def poll(cls, context):
if not tool.Ifc.get():
return False
return bool(context.scene.BIMCostProperties.cost_items)
props = tool.Cost.get_cost_props()
return bool(props.cost_items)
def draw(self, context):
if not CostClassificationsData.is_loaded:
@@ -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()
+11 -6
View File
@@ -66,8 +66,9 @@ class CostSchedulesData:
@classmethod
def schedules(cls):
results = []
if bpy.context.scene.BIMCostProperties.active_cost_schedule_id:
schedule = tool.Ifc.get().by_id(bpy.context.scene.BIMCostProperties.active_cost_schedule_id)
props = tool.Cost.get_cost_props()
if props.active_cost_schedule_id:
schedule = tool.Ifc.get().by_id(props.active_cost_schedule_id)
results.append(
{
"id": schedule.id(),
@@ -88,7 +89,8 @@ class CostSchedulesData:
@classmethod
def is_editing_rates(cls):
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_schedule_id
props = tool.Cost.get_cost_props()
ifc_id = props.active_cost_schedule_id
if not ifc_id:
return
return tool.Ifc.get().by_id(ifc_id).PredefinedType == "SCHEDULEOFRATES"
@@ -256,7 +258,8 @@ class CostSchedulesData:
@classmethod
def cost_quantities(cls):
results = []
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_item_id
props = tool.Cost.get_cost_props()
ifc_id = props.active_cost_item_id
if not ifc_id:
return results
for quantity in tool.Ifc.get().by_id(ifc_id).CostQuantities or []:
@@ -265,7 +268,8 @@ class CostSchedulesData:
@classmethod
def cost_values(cls):
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_item_id
props = tool.Cost.get_cost_props()
ifc_id = props.active_cost_item_id
if not ifc_id:
return []
return ifcopenshell.util.cost.get_cost_values(tool.Ifc.get().by_id(ifc_id))
@@ -324,7 +328,8 @@ class CostItemQuantitiesData:
@classmethod
def process_quantity_names(cls):
active_task_index = bpy.context.scene.BIMWorkScheduleProperties.active_task_index
props = tool.Sequence.get_work_schedule_props()
active_task_index = props.active_task_index
tprops = tool.Sequence.get_task_tree_props()
total_tasks = len(tprops.tasks)
if not total_tasks or active_task_index >= total_tasks:
+23 -15
View File
@@ -21,10 +21,10 @@
import bpy
import ifcopenshell.api
import bonsai.tool as tool
from bpy_extras.io_utils import ImportHelper
from bpy_extras.io_utils import ImportHelper, ExportHelper
import bonsai.tool as tool
import bonsai.core.cost as core
from typing import get_args, TYPE_CHECKING
from typing import get_args, TYPE_CHECKING, Literal
class AddCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
@@ -36,16 +36,18 @@ class AddCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
object_type: bpy.props.StringProperty()
def _execute(self, context):
predefined_type = context.scene.BIMCostProperties.cost_schedule_predefined_types
props = tool.Cost.get_cost_props()
predefined_type = props.cost_schedule_predefined_types
if predefined_type == "USERDEFINED":
predefined_type = self.object_type
core.add_cost_schedule(tool.Ifc, name=self.name, predefined_type=predefined_type)
def draw(self, context):
layout = self.layout
props = tool.Cost.get_cost_props()
layout.prop(self, "name", text="Name")
layout.prop(context.scene.BIMCostProperties, "cost_schedule_predefined_types", text="Type")
if context.scene.BIMCostProperties.cost_schedule_predefined_types == "USERDEFINED":
layout.prop(props, "cost_schedule_predefined_types", text="Type")
if props.cost_schedule_predefined_types == "USERDEFINED":
layout.prop(self, "object_type", text="Object type")
def invoke(self, context, event):
@@ -58,10 +60,11 @@ class EditCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = tool.Cost.get_cost_props()
core.edit_cost_schedule(
tool.Ifc,
tool.Cost,
cost_schedule=tool.Ifc.get().by_id(context.scene.BIMCostProperties.active_cost_schedule_id),
cost_schedule=tool.Ifc.get().by_id(props.active_cost_schedule_id),
)
@@ -559,7 +562,8 @@ class AddCostColumn(bpy.types.Operator):
@classmethod
def poll(cls, context):
if not context.scene.BIMCostProperties.cost_column:
props = tool.Cost.get_cost_props()
if not props.cost_column:
cls.poll_message_set("Cost column name is empty")
return False
return True
@@ -678,7 +682,7 @@ class CalculateCostItemResourceValue(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
class ExportCostSchedules(bpy.types.Operator):
class ExportCostSchedules(bpy.types.Operator, ExportHelper):
bl_idname = "bim.export_cost_schedules"
bl_label = "Export Cost Schedule"
bl_options = {"REGISTER", "UNDO"}
@@ -691,6 +695,13 @@ class ExportCostSchedules(bpy.types.Operator):
default=True,
)
if TYPE_CHECKING:
format: Literal["CSV", "XLSX", "ODS"]
@property
def filename_ext(self) -> str:
return f".{self.format.lower()}"
def execute(self, context):
cost_schedule = tool.Ifc.get().by_id(self.cost_schedule) if self.cost_schedule else None
r = core.export_cost_schedules(
@@ -701,9 +712,7 @@ class ExportCostSchedules(bpy.types.Operator):
return {"FINISHED"}
def invoke(self, context, event):
wm = context.window_manager
wm.fileselect_add(self)
return {"RUNNING_MODAL"}
return ExportHelper.invoke(self, context, event)
def draw(self, context):
self.layout.label(text="Choose a format")
@@ -740,15 +749,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"}
+83 -7
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
@@ -34,6 +33,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal
def get_schedule_of_rates(self, context):
@@ -79,11 +79,11 @@ def update_active_cost_item_index(self, context):
CostClassificationsData.load()
def update_cost_item_identification(self, context):
props = context.scene.BIMCostProperties
def update_cost_item_identification(self: "CostItem", context: bpy.types.Context):
props = tool.Cost.get_cost_props()
if not props.is_cost_update_enabled or self.identification == "XXX":
return
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"cost.edit_cost_item",
self.file,
@@ -94,11 +94,11 @@ def update_cost_item_identification(self, context):
attribute.string_value = self.identification
def update_cost_item_name(self, context):
props = context.scene.BIMCostProperties
def update_cost_item_name(self: "CostItem", context: bpy.types.Context) -> None:
props = tool.Cost.get_cost_props()
if not props.is_cost_update_enabled or self.name == "Unnamed":
return
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"cost.edit_cost_item",
self.file,
@@ -143,6 +143,14 @@ class CostItem(PropertyGroup):
is_expanded: BoolProperty(name="Is Expanded")
level_index: IntProperty(name="Level Index")
if TYPE_CHECKING:
name: str
identification: str
ifc_definition_id: int
has_children: bool
is_expanded: bool
level_index: int
class CostItemQuantity(PropertyGroup):
name: StringProperty(name="Name")
@@ -151,11 +159,22 @@ class CostItemQuantity(PropertyGroup):
unit_symbol: StringProperty(name="Unit Symbol")
total_cost_quantity: FloatProperty(name="Total Quantity")
if TYPE_CHECKING:
name: str
ifc_definition_id: int
total_quantity: float
unit_symbol: str
total_cost_quantity: float
class CostItemType(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
if TYPE_CHECKING:
name: str
ifc_definition_id: int
def update_cost_item_parent(self, context):
cost_item = tool.Cost.get_highlighted_cost_item()
@@ -177,6 +196,9 @@ def update_active_cost_item_resources(self, context):
class ScheduleColumn(PropertyGroup):
schedule_id: IntProperty()
if TYPE_CHECKING:
schedule_id: int
class BIMCostProperties(PropertyGroup):
cost_schedule_predefined_types: EnumProperty(
@@ -250,3 +272,57 @@ class BIMCostProperties(PropertyGroup):
custom_currency: StringProperty(
name="Custom Currency", default="USD", description="Custom Currency in ISO 4217 format"
)
if TYPE_CHECKING:
cost_schedule_predefined_types: str
is_cost_update_enabled: bool
cost_schedule_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
is_editing: str
active_cost_schedule_id: int
cost_items: bpy.types.bpy_prop_collection_idprop[CostItem]
active_cost_item_id: int
cost_item_editing_type: str
active_cost_item_index: int
cost_item_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
contracted_cost_items: str
quantity_types: str
product_quantity_names: str
process_quantity_names: str
resource_quantity_names: str
active_cost_item_quantity_id: int
quantity_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
cost_types: Literal["FIXED", "SUM", "CATEGORY"]
cost_category: str
fixed_cost_value: float
active_cost_value_id: int
cost_value_editing_type: str
cost_value_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
cost_value_formula: str
cost_column: str
should_show_column_ui: bool
should_show_currency_ui: bool
columns: bpy.types.bpy_prop_collection_idprop[StrProperty]
columns_storage: bpy.types.bpy_prop_collection_idprop[ScheduleColumn]
active_column_index: int
cost_item_products: bpy.types.bpy_prop_collection_idprop[CostItemQuantity]
active_cost_item_product_index: int
cost_item_processes: bpy.types.bpy_prop_collection_idprop[CostItemQuantity]
active_cost_item_process_index: int
cost_item_resources: bpy.types.bpy_prop_collection_idprop[CostItemQuantity]
active_cost_item_resource_index: int
cost_item_type_products: bpy.types.bpy_prop_collection_idprop[CostItemType]
active_cost_item_type_product_index: int
schedule_of_rates: str
cost_item_rates: bpy.types.bpy_prop_collection_idprop[CostItem]
active_cost_item_rate_index: int
contracted_cost_item_rates: str
product_cost_items: bpy.types.bpy_prop_collection_idprop[CostItemQuantity]
active_product_cost_item_index: int
enable_reorder: bool
show_nested_elements: bool
show_nested_tasks: bool
show_nested_resources: bool
change_cost_item_parent: bool
show_cost_item_operators: bool
currency: str
custom_currency: str
+46 -37
View File
@@ -16,14 +16,17 @@
# 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.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
from typing import Any, TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.cost.prop import BIMCostProperties, CostItemQuantity
class BIM_PT_cost_schedules(Panel):
@@ -37,14 +40,14 @@ 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):
if not CostSchedulesData.is_loaded:
CostSchedulesData.load()
self.props = context.scene.BIMCostProperties
self.props = tool.Cost.get_cost_props()
if not self.props.active_cost_schedule_id:
row = self.layout.row(align=True)
if CostSchedulesData.data["total_cost_schedules"]:
@@ -300,7 +303,7 @@ class BIM_PT_cost_item_types(Panel):
@classmethod
def poll(cls, context):
props = context.scene.BIMCostProperties
props = tool.Cost.get_cost_props()
total_cost_items = len(props.cost_items)
if not props.active_cost_schedule_id:
return False
@@ -313,7 +316,7 @@ class BIM_PT_cost_item_types(Panel):
return False
def draw(self, context):
self.props = context.scene.BIMCostProperties
self.props = tool.Cost.get_cost_props()
cost_item = self.props.cost_items[self.props.active_cost_item_index]
grid = self.layout.grid_flow(columns=3, even_columns=True)
@@ -349,8 +352,8 @@ class BIM_PT_cost_item_types(Panel):
row2 = col.row()
row2.template_list(
"BIM_UL_cost_item_quantities",
"",
BIM_UL_cost_item_quantities.__name__,
BIM_UL_cost_item_quantities.__name__ + "_cost_item_processes",
self.props,
"cost_item_processes",
self.props,
@@ -366,8 +369,8 @@ class BIM_PT_cost_item_types(Panel):
row2 = col.row()
row2.template_list(
"BIM_UL_cost_item_quantities",
"",
BIM_UL_cost_item_quantities.__name__,
BIM_UL_cost_item_quantities.__name__ + "_cost_item_resources",
self.props,
"cost_item_resources",
self.props,
@@ -386,7 +389,7 @@ class BIM_PT_cost_item_quantities(Panel):
@classmethod
def poll(cls, context):
props = context.scene.BIMCostProperties
props = tool.Cost.get_cost_props()
total_cost_items = len(props.cost_items)
if not props.active_cost_schedule_id:
return False
@@ -399,7 +402,7 @@ class BIM_PT_cost_item_quantities(Panel):
return False
def draw(self, context):
self.props = context.scene.BIMCostProperties
self.props = tool.Cost.get_cost_props()
cost_item = self.props.cost_items[self.props.active_cost_item_index]
@@ -436,8 +439,8 @@ class BIM_PT_cost_item_quantities(Panel):
row2.prop(self.props, "show_nested_elements", text="Show nested")
row2 = col.row()
row2.template_list(
"BIM_UL_cost_item_quantities",
"",
BIM_UL_cost_item_quantities.__name__,
BIM_UL_cost_item_quantities.__name__ + "_cost_item_products",
self.props,
"cost_item_products",
self.props,
@@ -463,7 +466,7 @@ class BIM_PT_cost_item_quantities(Panel):
row2.label(text="Tasks ({})".format(total_cost_item_processes))
tprops = tool.Sequence.get_task_tree_props()
wprops = context.scene.BIMWorkScheduleProperties
wprops = tool.Sequence.get_work_schedule_props()
if tprops.tasks and wprops.active_task_index < len(tprops.tasks):
if has_quantity_names:
op = row2.operator("bim.assign_cost_item_quantity", text="", icon="PROPERTIES")
@@ -481,8 +484,8 @@ class BIM_PT_cost_item_quantities(Panel):
row2 = col.row()
row2.template_list(
"BIM_UL_cost_item_quantities",
"",
BIM_UL_cost_item_quantities.__name__,
BIM_UL_cost_item_quantities.__name__ + "_cost_item_processes",
self.props,
"cost_item_processes",
self.props,
@@ -529,8 +532,8 @@ class BIM_PT_cost_item_quantities(Panel):
row2 = col.row()
row2.template_list(
"BIM_UL_cost_item_quantities",
"",
BIM_UL_cost_item_quantities.__name__,
BIM_UL_cost_item_quantities.__name__ + "_cost_item_resources",
self.props,
"cost_item_resources",
self.props,
@@ -558,7 +561,7 @@ class BIM_PT_cost_item_rates(Panel):
@classmethod
def poll(cls, context):
props = context.scene.BIMCostProperties
props = tool.Cost.get_cost_props()
total_cost_items = len(props.cost_items)
if not props.active_cost_schedule_id:
return False
@@ -571,7 +574,7 @@ class BIM_PT_cost_item_rates(Panel):
return False
def draw(self, context):
self.props = context.scene.BIMCostProperties
self.props = tool.Cost.get_cost_props()
row = self.layout.row(align=True)
row.prop(self.props, "schedule_of_rates", text="")
if self.props.active_cost_item_rate_index < len(self.props.cost_item_rates):
@@ -607,13 +610,14 @@ class BIM_UL_cost_items_trait:
split2.label(text="Quantity")
split2.label(text="Value")
for column in bpy.context.scene.BIMCostProperties.columns:
props = tool.Cost.get_cost_props()
for column in props.columns:
split2.label(text=column.name)
split2.label(text="Total Cost")
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
self.props = context.scene.BIMCostProperties
self.props = tool.Cost.get_cost_props()
cost_item = CostSchedulesData.data["cost_items"][item.ifc_definition_id]
row = layout.row(align=True)
@@ -709,18 +713,16 @@ class BIM_UL_cost_items_trait:
class BIM_UL_cost_items(BIM_UL_cost_items_trait, UIList):
def __init__(self):
self.contract_operator = "bim.contract_cost_item"
self.expand_operator = "bim.expand_cost_item"
contract_operator = "bim.contract_cost_item"
expand_operator = "bim.expand_cost_item"
class BIM_UL_cost_item_rates(BIM_UL_cost_items_trait, UIList):
# A schedule of rates UIList is identical to a regular cost items UIList but
# we want a separate UIList instance so that you can browse both lists
# independently in Blender. So we use a trait.
def __init__(self):
self.contract_operator = "bim.contract_cost_item_rate"
self.expand_operator = "bim.expand_cost_item_rate"
contract_operator = "bim.contract_cost_item_rate"
expand_operator = "bim.expand_cost_item_rate"
def draw_quantity_column(self, layout, cost_item):
self.draw_uom_column(layout, cost_item)
@@ -731,7 +733,6 @@ class BIM_UL_cost_item_rates(BIM_UL_cost_items_trait, UIList):
class BIM_UL_cost_columns(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
props = context.scene.BIMCostProperties
if item:
row = layout.row(align=True)
row.prop(item, "name", emboss=False, text="")
@@ -740,7 +741,7 @@ class BIM_UL_cost_columns(UIList):
class BIM_UL_cost_item_types(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
props = context.scene.BIMCostProperties
props = tool.Cost.get_cost_props()
cost_item = props.cost_items[props.active_cost_item_index]
if item:
@@ -752,9 +753,17 @@ class BIM_UL_cost_item_types(UIList):
class BIM_UL_cost_item_quantities(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
props = context.scene.BIMCostProperties
cost_item = props.cost_items[props.active_cost_item_index]
def draw_item(
self,
context: bpy.types.Context,
layout: bpy.types.UILayout,
data: BIMCostProperties,
item: CostItemQuantity,
icon,
active_data,
active_propname,
):
cost_item = data.cost_items[data.active_cost_item_index]
if item:
row = layout.row(align=True)
op = row.operator("bim.select_product", text="", icon="RESTRICT_SELECT_OFF")
@@ -791,15 +800,15 @@ class BIM_PT_Costing_Tools(Panel):
bl_parent_id = "BIM_PT_tab_cost"
def draw(self, context):
self.props = context.scene.BIMCostProperties
props = tool.Cost.get_cost_props()
row = self.layout.row()
row.operator("bim.load_product_cost_items", icon="FILE_REFRESH")
row = self.layout.row()
row.template_list(
"BIM_UL_product_cost_items",
"",
self.props,
props,
"product_cost_items",
self.props,
props,
"active_product_cost_item_index",
)
@@ -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():
@@ -88,7 +87,7 @@ class CoveringToolUI:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
row.operator("bim.add_constr_type_instance", text="Add")
row.operator("bim.add_occurrence", text="Add")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
@@ -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,22 +173,22 @@ 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":
bpy.ops.bim.add_occurrence()
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:
bpy.ops.bim.add_instance_ceiling_covering_from_cursor()
else:
bpy.ops.bim.add_constr_type_instance()
bpy.ops.bim.add_occurrence()
else:
bpy.ops.bim.add_constr_type_instance()
bpy.ops.bim.add_occurrence()
def hotkey_S_G(self):
active_obj = bpy.context.active_object
@@ -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"
+42 -46
View File
@@ -27,7 +27,7 @@ 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 bpy_extras.io_utils import ExportHelper, ImportHelper
from bonsai.bim.handler import refresh_ui_data
from typing import TYPE_CHECKING
from collections import Counter
@@ -43,7 +43,8 @@ class AddCsvAttribute(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
attribute = context.scene.CsvProperties.csv_attributes.add()
props = tool.Blender.get_csv_props()
props.csv_attributes.add()
return {"FINISHED"}
@@ -54,7 +55,8 @@ class RemoveCsvAttribute(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
context.scene.CsvProperties.csv_attributes.remove(self.index)
props = tool.Blender.get_csv_props()
props.csv_attributes.remove(self.index)
return {"FINISHED"}
@@ -65,7 +67,8 @@ class RemoveAllCsvAttributes(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
context.scene.CsvProperties.csv_attributes.clear()
props = tool.Blender.get_csv_props()
props.csv_attributes.clear()
return {"FINISHED"}
@@ -77,8 +80,9 @@ class ReorderCsvAttribute(bpy.types.Operator):
new_index: bpy.props.IntProperty()
def execute(self, context):
old = context.scene.CsvProperties.csv_attributes[self.old_index]
new = context.scene.CsvProperties.csv_attributes[self.new_index]
props = tool.Blender.get_csv_props()
old = props.csv_attributes[self.old_index]
new = props.csv_attributes[self.new_index]
props = ["name", "header", "sort", "group", "varies_value", "summary", "formatting"]
for prop in props:
value = getattr(new, prop)
@@ -87,16 +91,16 @@ class ReorderCsvAttribute(bpy.types.Operator):
return {"FINISHED"}
class ImportCsvAttributes(bpy.types.Operator):
class ImportCsvAttributes(bpy.types.Operator, ImportHelper):
bl_idname = "bim.import_csv_attributes"
bl_label = "Load CSV Settings"
bl_description = "Import a json template for CSV export"
bl_options = {"REGISTER", "UNDO"}
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filename_ext = ".json"
def execute(self, context):
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
data = json.load(open(self.filepath))
tool.Search.import_filter_query(data["query"], props.filter_groups)
@@ -122,22 +126,17 @@ class ImportCsvAttributes(bpy.types.Operator):
setattr(new, prop, attribute[prop])
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ExportCsvAttributes(bpy.types.Operator):
class ExportCsvAttributes(bpy.types.Operator, ExportHelper):
bl_idname = "bim.export_csv_attributes"
bl_label = "Save CSV Settings"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Save a json template for CSV export"
filename_ext = ".json"
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
settings = {}
for prop in [
@@ -176,43 +175,45 @@ class ExportCsvAttributes(bpy.types.Operator):
return {"FINISHED"}
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ExportIfcCsv(bpy.types.Operator):
class ExportIfcCsv(bpy.types.Operator, ExportHelper):
bl_idname = "bim.export_ifccsv"
bl_label = "Export IFC"
bl_description = "Export IFC data as a spreadsheet."
filename_ext = ".csv"
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
@classmethod
def poll(cls, context):
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
if not props.should_load_from_memory and not props.csv_ifc_file:
cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.")
return False
return True
@classmethod
def description(cls, context, properties):
props = tool.Blender.get_csv_props()
if props.format == "web":
return "Open Web UI for spreadsheet data export."
return f"Export IFC data as a spreadsheet by the provided filepath in '{props.format}' format ."
def invoke(self, context, event):
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
if props.format == "web":
return self.execute(context)
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
self.filter_glob = f"*.{props.format}"
self.filename_ext = f".{props.format}"
return ExportHelper.invoke(self, context, event)
def execute(self, context):
import ifccsv
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
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(
@@ -288,16 +289,16 @@ class ExportIfcCsv(bpy.types.Operator):
]
class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator):
class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.import_ifccsv"
bl_label = "Import to IFC"
bl_description = "Import IFC data from a spreadsheet."
bl_options = {"REGISTER", "UNDO"}
filter_glob: bpy.props.StringProperty(default="*.csv;*.ods;*.xlsx", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
@classmethod
def poll(cls, context):
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
if not props.should_load_from_memory and not props.csv_ifc_file:
cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.")
return False
@@ -305,16 +306,14 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator):
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".csv")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
return ImportHelper.invoke(self, context, event)
def _execute(self, context):
import ifccsv
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
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()
@@ -338,18 +337,15 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
class SelectCsvIfcFile(bpy.types.Operator):
class SelectCsvIfcFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_csv_ifc_file"
bl_label = "Select CSV IFC File"
bl_description = "Select IFC file for spreadsheet import/export."
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".ifc"
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
context.scene.CsvProperties.csv_ifc_file = self.filepath
props = tool.Blender.get_csv_props()
props.csv_ifc_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
+33
View File
@@ -29,6 +29,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal
class CsvAttribute(PropertyGroup):
@@ -59,6 +60,15 @@ class CsvAttribute(PropertyGroup):
)
formatting: StringProperty(default="{{value}}", name="Formatting")
if TYPE_CHECKING:
name: str
header: str
sort: Literal["NONE", "ASC", "DESC"]
group: Literal["NONE", "GROUP", "CONCAT", "VARIES", "SUM", "AVERAGE", "MIN", "MAX"]
varies_value: str
summary: Literal["NONE", "SUM", "AVERAGE", "MIN", "MAX"]
formatting: str
class CsvProperties(PropertyGroup):
csv_ifc_file: StringProperty(default="", name="IFC File")
@@ -104,3 +114,26 @@ class CsvProperties(PropertyGroup):
name="Load from Memory",
description="Use IFC file currently loaded in Bonsai",
)
if TYPE_CHECKING:
csv_ifc_file: str
ifc_selector: str
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
csv_attributes: bpy.types.bpy_prop_collection_idprop[CsvAttribute]
should_generate_svg: bool
should_preserve_existing: bool
include_global_id: bool
null_value: str
empty_value: str
true_value: str
false_value: str
concat_value: str
csv_delimiter: Literal["NONE", "ASC", "DESC"]
format: Literal["csv", "xlsx", "ods", "web"]
csv_custom_delimiter: str
should_show_settings: bool
should_show_sort: bool
should_show_group: bool
should_show_summary: bool
should_show_formatting: bool
should_load_from_memory: bool
+8 -7
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
@@ -33,11 +33,9 @@ class BIM_PT_ifccsv(Panel):
def draw(self, context):
layout = self.layout
props = tool.Blender.get_csv_props()
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 +47,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="")
@@ -126,5 +124,8 @@ class BIM_PT_ifccsv(Panel):
row.operator("bim.remove_csv_attribute", icon="X", text="").index = index
row = layout.row(align=True)
row.operator("bim.export_ifccsv", icon="EXPORT", text="Export IFC to " + props.format.upper())
if props.format == "web":
row.operator("bim.export_ifccsv", icon="EXPORT", text="Open Web UI")
else:
row.operator("bim.export_ifccsv", icon="EXPORT", text="Export IFC to " + props.format.upper())
row.operator("bim.import_ifccsv", icon="IMPORT")
+72 -43
View File
@@ -23,6 +23,7 @@ import time
import random
import logging
import subprocess
import platform
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.geom
@@ -36,6 +37,7 @@ import bonsai.core.profile
import bonsai.core.type
import bonsai.bim.handler
import bonsai.bim.import_ifc as import_ifc
from bpy_extras.io_utils import ImportHelper, ExportHelper
from pathlib import Path
from bonsai import get_debug_info, format_debug_info
from bonsai.bim.ifc import IfcStore
@@ -78,10 +80,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 +103,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 +163,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 +188,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 +260,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 +313,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 +332,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 +384,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 +426,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,28 +442,25 @@ 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"}
class SelectExpressFile(bpy.types.Operator):
class SelectExpressFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_express_file"
bl_label = "Select Express File"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select an IFC EXPRESS definition"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
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):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class PurgeHdf5Cache(bpy.types.Operator):
bl_idname = "bim.purge_hdf5_cache"
@@ -497,7 +498,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 +510,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.
]
@@ -518,21 +520,22 @@ class PrintUnusedElementStats(bpy.types.Operator):
return {"FINISHED"}
class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator, ExportHelper):
bl_idname = "bim.purge_unused_elements_by_class"
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"}
filename_ext = ".ifc"
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.alt:
context.window_manager.fileselect_add(self)
return ExportHelper.invoke(self, context, event)
return self.execute(context)
@classmethod
@@ -543,7 +546,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 +612,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 +702,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 +752,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 +818,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,10 +953,20 @@ 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
os.execv(path, sys.argv)
if platform.system() == "Windows":
args = sys.argv[1:]
command_line = subprocess.list2cmdline([path] + args)
os.execv(path, [command_line])
else:
os.execv(path, sys.argv)
else:
# Microsoft apps do not allow launching blender.exe directly
# since Blender folder is kind of private.
+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:
+1 -1
View File
@@ -43,7 +43,7 @@ class DiffData:
@classmethod
def diff_json(cls):
props = bpy.context.scene.DiffProperties
props = tool.Blender.get_diff_props()
if not props.diff_json_file:
cls.diff = None
return
+33 -40
View File
@@ -24,24 +24,23 @@ import ifcopenshell
import bonsai.bim.handler
import bonsai.bim.import_ifc
import bonsai.tool as tool
from bpy_extras.io_utils import ImportHelper, ExportHelper
from bonsai.bim.ifc import IfcStore
class SelectDiffJsonFile(bpy.types.Operator):
class SelectDiffJsonFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_diff_json_file"
bl_label = "Select Diff JSON File"
bl_description = "Select filepath for IFC diff results."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
filename_ext = ".json"
def execute(self, context):
context.scene.DiffProperties.diff_json_file = self.filepath
props = tool.Blender.get_diff_props()
props.diff_json_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class VisualiseDiff(bpy.types.Operator):
bl_idname = "bim.visualise_diff"
@@ -50,7 +49,8 @@ class VisualiseDiff(bpy.types.Operator):
def execute(self, context):
ifc_file = tool.Ifc.get()
with open(context.scene.DiffProperties.diff_json_file, "r") as file:
props = tool.Blender.get_diff_props()
with open(props.diff_json_file, "r") as file:
diff = json.load(file)
for obj in context.visible_objects:
obj.color = (1.0, 1.0, 1.0, 1.0)
@@ -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)
@@ -100,55 +101,45 @@ class VisualiseDiff(bpy.types.Operator):
return {"FINISHED"}
class SelectDiffOldFile(bpy.types.Operator):
class SelectDiffOldFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_diff_old_file"
bl_label = "Select Diff Old File"
bl_description = "Select filepath for an old IFC file to compare."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
filename_ext = ".ifc"
def execute(self, context):
context.scene.DiffProperties.old_file = self.filepath
props = tool.Blender.get_diff_props()
props.old_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectDiffNewFile(bpy.types.Operator):
class SelectDiffNewFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_diff_new_file"
bl_label = "Select Diff New File"
bl_description = "Select filepath for a new IFC file to compare."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
filename_ext = ".ifc"
def execute(self, context):
context.scene.DiffProperties.new_file = self.filepath
props = tool.Blender.get_diff_props()
props.new_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ExecuteIfcDiff(bpy.types.Operator):
class ExecuteIfcDiff(bpy.types.Operator, ExportHelper):
bl_idname = "bim.execute_ifc_diff"
bl_label = "Execute IFC Diff"
bl_description = "Compare two IFC files and save a json diff report by the provided filepath."
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
import ifcdiff
self.props = context.scene.DiffProperties
self.props = tool.Blender.get_diff_props()
if tool.Ifc.get():
if self.props.active_file == "NONE":
@@ -237,7 +228,8 @@ class SelectDiffObjects(bpy.types.Operator):
def execute(self, context):
ifc_file = tool.Ifc.get()
with open(context.scene.DiffProperties.diff_json_file, "r") as file:
props = tool.Blender.get_diff_props()
with open(props.diff_json_file, "r") as file:
diff = json.load(file)
for obj in context.visible_objects:
obj.select_set(False)
@@ -252,13 +244,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 +263,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)
+18 -2
View File
@@ -30,18 +30,25 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal, get_args
def update_diff_json_file(self, context):
def update_diff_json_file(self: "DiffProperties", context: bpy.types.Context) -> None:
DiffData.data["diff_json"] = DiffData.diff_json()
RelationshipType = Literal["type", "property", "container", "aggregate", "classification"]
class Relationships(PropertyGroup):
relationship: EnumProperty(
name="Relationship",
items=[(r, r.capitalize(), r) for r in ["type", "property", "container", "aggregate", "classification"]],
items=[(r, r.capitalize(), r) for r in get_args(RelationshipType)],
)
if TYPE_CHECKING:
relationship: RelationshipType
class DiffProperties(PropertyGroup):
diff_json_file: StringProperty(default="", name="JSON Output", update=update_diff_json_file)
@@ -59,3 +66,12 @@ class DiffProperties(PropertyGroup):
name="Active File",
default="NEW",
)
if TYPE_CHECKING:
diff_json_file: str
old_file: str
new_file: str
diff_relationships: bpy.types.bpy_prop_collection_idprop[Relationships]
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
should_load_changed_elements: bool
active_file: Literal["NONE", "OLD", "NEW"]
+1 -4
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
@@ -38,9 +37,7 @@ class BIM_PT_diff(Panel):
layout = self.layout
layout.use_property_split = True
scene = context.scene
props = scene.DiffProperties
props = tool.Blender.get_diff_props()
layout.label(text="IFC Diff Setup:")
@@ -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)
+20 -9
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")
@@ -233,7 +236,8 @@ class DecoratorData:
# stores 1 type of data per object
data = {}
cut_cache = {}
layerset_cache = {}
slice_cache = {}
fill_cache = {}
@classmethod
def get_batting_thickness(cls, obj):
@@ -303,7 +307,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 +365,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"
@@ -379,6 +387,8 @@ class DecoratorData:
suppress_zero_inches = pset_data.get("SuppressZeroInches", False)
text_prefix = pset_data.get("TextPrefix", None) or ""
text_suffix = pset_data.get("TextSuffix", None) or ""
custom_unit_list = pset_data.get("CustomUnit", None) or ""
custom_unit = custom_unit_list[0] if custom_unit_list else ""
dimension_data = {
"dimension_style": dimension_style,
@@ -387,6 +397,7 @@ class DecoratorData:
"text_prefix": text_prefix,
"text_suffix": text_suffix,
"fill_bg": fill_bg,
"custom_unit": custom_unit,
}
cls.data[obj.name] = dimension_data
return dimension_data
@@ -399,7 +410,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()
+226 -320
View File
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import os
import gpu
import bpy
import blf
@@ -29,7 +28,7 @@ import ifcopenshell.util.unit
import bonsai.tool as tool
import bonsai.bim.module.drawing.helper as helper
from pathlib import Path
from math import pi, sin, cos, tan, acos, atan, degrees, radians, ceil
from math import pi, sin, cos, acos, atan, degrees, radians
from bpy.types import SpaceView3D
from mathutils import Vector, Matrix
from bpy_extras.view3d_utils import location_3d_to_region_2d
@@ -319,6 +318,8 @@ class BaseDecorator:
color: tuple[float, float, float, float],
indices: Optional[list[tuple[int, int]]] = None,
) -> None:
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.base_shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
@@ -423,7 +424,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]
@@ -494,14 +496,20 @@ class BaseDecorator:
self.draw_label(context, text=text, line_no=line_number_start, multiline=True, **draw_label_kwargs)
@lru_cache(maxsize=None)
def format_value(self, context, value):
def format_value(self, context, value, custom_unit=None):
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
precision = drawing_pset_data.get("MetricPrecision", None)
if not precision:
precision = drawing_pset_data.get("ImperialPrecision", None)
decimal_places = drawing_pset_data.get("DecimalPlaces", None)
return format_distance(value, precision=precision, decimal_places=decimal_places)
return format_distance(
value,
precision=precision,
decimal_places=decimal_places,
suppress_zero_inches=True,
custom_unit=custom_unit,
)
def draw_asterisk(self, context: bpy.types.Context, pos: Vector, rotation: float = 0.0, scale: float = 1.0) -> None:
"""`pos` is a world space position\n
@@ -588,8 +596,9 @@ class BaseDecorator:
region3d = context.region_data
text_dir = self.get_annotation_direction(context, obj)
pos = location_3d_to_region_2d(region, region3d, text_world_position)
props = obj.BIMTextProperties
if not (pos := location_3d_to_region_2d(region, region3d, text_world_position)):
return
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()
@@ -721,6 +730,8 @@ class DimensionDecorator(BaseDecorator):
v1 = Vector(vertices[i1])
p0 = location_3d_to_region_2d(region, region3d, v0)
p1 = location_3d_to_region_2d(region, region3d, v1)
if not p0 or not p1:
return
text_dir = p1 - p0
if text_dir.length < 1:
continue
@@ -736,7 +747,7 @@ class DimensionDecorator(BaseDecorator):
if not show_description_only:
length = (v1 - v0).length
text = self.format_value(context, length)
text = self.format_value(context, length, custom_unit=dimension_data["custom_unit"])
if isinstance(self, DiameterDecorator):
text = "D" + text
text = text_prefix + text + text_suffix
@@ -871,6 +882,8 @@ class AngleDecorator(BaseDecorator):
# calculate angle position
p0, p1, p2 = [location_3d_to_region_2d(region, region3d, p) for p in vertices[i0 : i1 + 2]]
if not p0 or not p1 or not p2:
continue
edge0 = p0 - p1
edge1 = p2 - p1
radius = min(edge0.length_squared, edge1.length_squared) ** 0.5
@@ -935,6 +948,8 @@ class RadiusDecorator(BaseDecorator):
spline_points = self.get_spline_points(obj)
p0, p1 = [location_3d_to_region_2d(region, region3d, p) for p in self.get_spline_points(obj)]
if not p0 or not p1:
return
element = tool.Ifc.get_entity(obj)
description = element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
@@ -945,7 +960,7 @@ class RadiusDecorator(BaseDecorator):
def get_text():
length = (spline_points[-1] - spline_points[-2]).length
return "R" + self.format_value(context, length)
return "R" + self.format_value(context, length, custom_unit=dimension_data["custom_unit"])
self.draw_dimension_text(
context, get_text, description, dimension_data, pos=pos, text_dir=Vector((1, 0)), box_alignment="center"
@@ -967,7 +982,8 @@ class FallDecorator(BaseDecorator):
region = context.region
region3d = context.region_data
dir = Vector((1, 0))
pos = location_3d_to_region_2d(region, region3d, self.get_spline_end(obj))
if not (pos := location_3d_to_region_2d(region, region3d, self.get_spline_end(obj))):
return
spline = obj.data.splines[0]
spline_points = spline.bezier_points if spline.bezier_points else spline.points
@@ -976,6 +992,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 +1005,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)} %"
@@ -1156,7 +1174,6 @@ class PlanLevelDecorator(BaseDecorator):
self.draw_labels(context, obj, self.get_splines(obj))
def draw_labels(self, context, obj, splines):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
region = context.region
region3d = context.region_data
@@ -1166,6 +1183,8 @@ class PlanLevelDecorator(BaseDecorator):
for verts in splines:
p0, p1 = [location_3d_to_region_2d(region, region3d, v) for v in verts[:2]]
if not p0 or not p1:
continue
text_dir = p1 - p0
if text_dir.length < 1:
continue
@@ -1432,8 +1451,10 @@ class GridDecorator(BaseDecorator):
p1 = location_3d_to_region_2d(region, region3d, v1)
dir = Vector((1, 0))
text = obj.name.split("/")[1].split(".")[0]
self.draw_label(context, text, p0, dir, vcenter=True, gap=0)
self.draw_label(context, text, p1, dir, vcenter=True, gap=0)
if p0:
self.draw_label(context, text, p0, dir, vcenter=True, gap=0)
if p1:
self.draw_label(context, text, p1, dir, vcenter=True, gap=0)
class ElevationDecorator(BaseDecorator):
@@ -1586,6 +1607,9 @@ class CutDecorator:
@classmethod
def install(cls, context):
DecoratorData.cut_cache.clear()
DecoratorData.slice_cache.clear()
DecoratorData.fill_cache.clear()
if cls.installed:
cls.uninstall()
handler = cls()
@@ -1600,38 +1624,50 @@ class CutDecorator:
cls.installed = None
def __call__(self, context):
if not context.scene.camera:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
selected_elements_color = self.addon_prefs.decorator_color_selected
self.fallback_colour = (0.3, 0.3, 0.3, 1)
all_vertices = []
all_edges = []
selected_vertices = []
selected_edges = []
layer_vertices = []
layer_edges = []
fills = {}
all_vertex_i_offset = 0
selected_vertex_i_offset = 0
layer_vertex_i_offset = 0
for obj in [o for o in bpy.context.visible_objects if o.type == "MESH"]:
verts, edges = self.decorate(context, obj)
if not verts:
if not (element := tool.Ifc.get_entity(obj)):
continue
self.decorate(context, obj, element)
obj_layer_verts, obj_layer_edges = self.slice_layersets(context, obj, verts, edges)
if obj_layer_verts:
layer_vertices.extend(obj_layer_verts)
layer_edges.extend([[vi + layer_vertex_i_offset for vi in e] for e in obj_layer_edges])
layer_vertex_i_offset += len(obj_layer_verts)
verts, edges = DecoratorData.cut_cache[element.id()]
if verts:
if obj.select_get():
selected_vertices.extend(verts)
selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges])
selected_vertex_i_offset += len(verts)
else:
all_vertices.extend(verts)
all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges])
all_vertex_i_offset += len(verts)
if obj.select_get():
selected_vertices.extend(verts)
selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges])
selected_vertex_i_offset += len(verts)
else:
all_vertices.extend(verts)
all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges])
all_vertex_i_offset += len(verts)
verts, edges = DecoratorData.slice_cache.get(element.id(), (None, None))
if verts:
if obj.select_get():
selected_vertices.extend(verts)
selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges])
selected_vertex_i_offset += len(verts)
else:
all_vertices.extend(verts)
all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges])
all_vertex_i_offset += len(verts)
for colour, element_fills in DecoratorData.fill_cache[element.id()].items():
fills.setdefault(colour, []).append(element_fills)
gpu.state.point_size_set(1)
gpu.state.blend_set("ALPHA")
@@ -1649,9 +1685,10 @@ class CutDecorator:
black = (0, 0, 0, 1)
if layer_vertices:
self.draw_batch("LINES", layer_vertices, black, layer_edges)
self.draw_batch("POINTS", layer_vertices, black)
for colour, element_fills in fills.items():
for verts_tris in element_fills:
for verts, tris in verts_tris:
self.draw_batch("TRIS", verts, colour, tris)
gpu.state.point_size_set(2)
self.line_shader.uniform_float("lineWidth", 3.0)
@@ -1664,315 +1701,183 @@ class CutDecorator:
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def decorate(self, context, obj):
element = tool.Ifc.get_entity(obj)
if not element:
return None, None
def decorate(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
has_cut_cache = element.id() in DecoratorData.cut_cache
has_fill_cache = element.id() in DecoratorData.fill_cache
# Currently selected objects shall not be cached as they may be being moved / edited.
# If the camera is selected, we also disable the cache as the user may be moving the camera.
if obj.select_get() or context.scene.camera.select_get():
verts, edges = None, None
else:
verts, edges = DecoratorData.cut_cache.get(element.id(), (None, None))
# Currently selected objects must be recalculated as they may be being moved / edited.
# If the camera is selected, we also recalculate as the user may be moving the camera.
if verts is False:
return None, None
elif verts:
return verts, edges
if not has_cut_cache or obj.select_get() or context.scene.camera.select_get():
self.recalculate_cut(context, obj, element)
if not has_fill_cache or obj.select_get() or context.scene.camera.select_get():
self.recalculate_fill(context, obj, element)
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
DecoratorData.cut_cache[element.id()] = (False, False)
return None, None
if verts is None:
else:
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
DecoratorData.cut_cache[element.id()] = (verts, edges)
return verts, edges
def slice_layersets(self, context, obj, cut_verts, cut_edges):
element = tool.Ifc.get_entity(obj)
# Currently selected objects shall not be cached as they may be being moved / edited.
# If the camera is selected, we also disable the cache as the user may be moving the camera.
if obj.select_get() or context.scene.camera.select_get():
verts, edges = None, None
else:
verts, edges = DecoratorData.layerset_cache.get(element.id(), (None, None))
if verts is False:
return None, None
elif verts is not None:
return verts, edges
def recalculate_fill(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
element_id = element.id()
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
DecoratorData.layerset_cache[element.id()] = (False, False)
return None, None
DecoratorData.fill_cache[element_id] = {}
return
if tool.Model.get_usage_type(element) != "LAYER2":
DecoratorData.layerset_cache[element.id()] = (False, False)
return None, None
DecoratorData.fill_cache[element_id] = {}
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
layers = self.get_layer_data(element)
mesh = obj.data
bm_original = bmesh.new()
bm_original.from_mesh(mesh)
if not layers:
DecoratorData.layerset_cache[element.id()] = (False, False)
return None, None
minx = min([co[0] for co in obj.bound_box])
maxx = max([co[0] for co in obj.bound_box])
min_edge = [Vector((minx, layers["offset"])), Vector((maxx, layers["offset"]))]
max_edge = [
Vector((minx, layers["offset"] + layers["thickness"])),
Vector((maxx, layers["offset"] + layers["thickness"])),
]
centerline = [
Vector((minx, layers["offset"] + layers["thickness"] / 2)),
Vector((maxx, layers["offset"] + layers["thickness"] / 2)),
]
connections = self.get_connections(element, obj, centerline, min_edge, max_edge)
start_point = None
end_point = None
if connections["ATSTART"]:
boundary_edge = min_edge if connections["ATSTART"]["angle"] > 0 else max_edge
rel_dir = connections["ATSTART"]["centerline"][1] - connections["ATSTART"]["centerline"][0]
offset, _ = tool.Cad.intersect_edges(boundary_edge, [centerline[0], centerline[0] + rel_dir])
offset_x = (offset - centerline[0]).length + abs(offset.x)
intersect, _ = tool.Cad.intersect_edges(boundary_edge, connections["ATSTART"]["centerline"])
if tool.Cad.is_point_on_edge(intersect, boundary_edge):
centerline[0] = centerline[0] + Vector((offset_x, 0))
start_point = centerline[0] + rel_dir
if connections["ATEND"]:
boundary_edge = min_edge if connections["ATEND"]["angle"] > 0 else max_edge
rel_dir = connections["ATEND"]["centerline"][1] - connections["ATEND"]["centerline"][0]
offset, _ = tool.Cad.intersect_edges(boundary_edge, [centerline[1], centerline[1] + rel_dir])
offset_x = (offset - centerline[1]).length + abs((maxx - abs(offset.x)))
intersect, _ = tool.Cad.intersect_edges(boundary_edge, connections["ATEND"]["centerline"])
if tool.Cad.is_point_on_edge(intersect, boundary_edge):
centerline[1] = centerline[1] - Vector((offset_x, 0))
end_point = centerline[1] + rel_dir
min_segments = self.get_segments(connections["MINPATH"], centerline, start_point, end_point)
max_segments = self.get_segments(connections["MAXPATH"], centerline, start_point, end_point)
final_segments = []
for segment in min_segments:
segment_line = shapely.LineString([co for co in segment])
for distance in layers["min_layers"]:
distance *= -1
layer_line = shapely.offset_curve(segment_line, distance, join_style=shapely.BufferJoinStyle.mitre)
final_segments.append(list(layer_line.coords))
for segment in max_segments:
segment_line = shapely.LineString([co for co in segment])
for distance in layers["max_layers"]:
layer_line = shapely.offset_curve(segment_line, distance, join_style=shapely.BufferJoinStyle.mitre)
final_segments.append(list(layer_line.coords))
# Extrude and bisect layers
bm = bmesh.new()
verts = []
edges = []
offset = 0
for segment in final_segments:
if not segment:
# Why does this occur?
continue
if isinstance(segment[0], tuple):
segment = [Vector(co) for co in segment]
verts.extend([bm.verts.new(co.to_3d()) for co in segment])
[bm.edges.new((verts[i + offset], verts[i + 1 + offset])) for i in range(0, len(segment) - 1)]
offset += len(segment)
extrusion_dir = (0, 0, 3)
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=bm.edges[:])
bmesh.ops.translate(
bm, vec=extrusion_dir, verts=[v for v in extruded_geom["geom"] if isinstance(v, bmesh.types.BMVert)]
)
verts, edges = self.bisect_mesh(obj, bm, context.scene.camera)
layer_linestrings = [shapely.LineString((verts[e[0]], verts[e[1]])) for e in edges]
clipped_linestrings = []
polygons = shapely.polygonize([shapely.LineString((cut_verts[e[0]], cut_verts[e[1]])) for e in cut_edges])
for polygon in polygons.geoms:
clipped_linestrings.extend([polygon.intersection(ls) for ls in layer_linestrings])
verts = []
edges = []
offset = 0
for linestring in clipped_linestrings:
try:
linestring = list(linestring.coords)
except:
print("Failed ... ", linestring)
continue
verts.extend(linestring)
edges.extend([(i + offset, i + 1 + offset) for i in range(0, len(linestring) - 1)])
offset += len(linestring)
DecoratorData.layerset_cache[element.id()] = (verts, edges)
return verts, edges
def bisect_mesh(self, obj, bm, camera):
camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world
# Slice our mesh into a 2D drawing cut (2D is always easier)
camera_matrix = obj.matrix_world.inverted() @ context.scene.camera.matrix_world
global_offset = context.scene.camera.matrix_world.col[2].xyz * -context.scene.camera.data.clip_start
plane_co = camera_matrix.translation
plane_no = camera_matrix.col[2].xyz
geom = bm_original.verts[:] + bm_original.edges[:] + bm_original.faces[:]
bmesh.ops.bisect_plane(
bm_original,
geom=geom,
dist=0.0001,
plane_co=plane_co,
plane_no=plane_no,
clear_outer=True,
clear_inner=True,
)
bmesh.ops.remove_doubles(bm_original, verts=bm_original.verts, dist=0.000001)
bmesh.ops.triangle_fill(bm_original, use_dissolve=True, edges=bm_original.edges)
global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start
if not (material := ifcopenshell.util.element.get_material(element)):
verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera)
DecoratorData.fill_cache[element_id].setdefault(self.fallback_colour, []).append((verts, tris))
return
# Run the bisect operation
geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
results = bmesh.ops.bisect_plane(bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no)
if material.is_a() not in ("IfcMaterialLayerSet", "IfcMaterialLayerSetUsage"):
# Constituents, lists, and item styles not supported yet
material = ifcopenshell.util.element.get_materials(element)[0]
verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera)
colour = self.get_material_colour(material)
DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris))
return
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if material.is_a("IfcMaterialLayerSetUsage"):
usage = material
layer_set = material.ForLayerSet
offset = usage.OffsetFromReferenceLine * self.unit_scale
sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1
elif material.is_a("IfcMaterialLayerSet"):
usage = None
layer_set = material
offset = 0
sense_factor = 1
if len(layer_set.MaterialLayers) == 1:
material = layer_set.MaterialLayers[0].Material
verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera)
colour = self.get_material_colour(material)
DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris))
return
bm = bm_original.copy()
prev_co = None
if not usage:
sense_factor = 1 # Assume the extrusion vector points in the direction sense
no = tool.Drawing.get_extrusion_vector(element).normalized()
co = Vector((0.0, 0.0, offset))
elif usage.LayerSetDirection == "AXIS2":
co = Vector((0.0, offset, 0.0))
no = tool.Drawing.get_extrusion_vector(element).normalized()
no = no.cross(Vector([1.0, 0.0, 0.0]))
elif usage.LayerSetDirection == "AXIS3":
co = Vector((0.0, 0.0, offset))
no = tool.Drawing.get_extrusion_vector(element).normalized()
no = Vector([0.0, 0.0, 1.0])
elif usage.LayerSetDirection == "AXIS1":
co = Vector((0.0, 0.0, offset))
no = tool.Drawing.get_extrusion_vector(element).normalized()
no = Vector([1.0, 0.0, 0.0])
no *= sense_factor
last_i = len(layer_set.MaterialLayers) - 1
vert_map = {}
verts = []
edges = []
j = 0
for i, layer in enumerate(layer_set.MaterialLayers):
prev_co = co.copy()
co += no * layer.LayerThickness * self.unit_scale
if i != last_i:
bisect = bmesh.ops.bisect_plane(
bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no
)
for geom in bisect["geom_cut"]:
if isinstance(geom, bmesh.types.BMVert):
verts.append(tuple((obj.matrix_world @ geom.co) + global_offset))
vert_map[geom.index] = j
j += 1
else:
# It seems as though edges always appear after verts
edges.append([vert_map[v.index] for v in geom.verts])
colour = self.get_material_colour(layer.Material)
bm_fill = bm_original.copy()
if i != last_i:
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
bmesh.ops.bisect_plane(bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True)
if i != 0:
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
bmesh.ops.bisect_plane(bm_fill, geom=geom, dist=0.0001, plane_co=prev_co, plane_no=no, clear_inner=True)
DecoratorData.fill_cache[element_id].setdefault(colour, []).append(
self.get_bmesh_tris(obj, bm_fill, context.scene.camera)
)
DecoratorData.slice_cache[element.id()] = (verts, edges)
bm_original.free()
def get_material_colour(self, material: ifcopenshell.entity_instance) -> tuple[float, float, float, float]:
body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
style = ifcopenshell.util.representation.get_material_style(material, body)
if not style:
return self.fallback_colour
if not (styles := [s for s in style.Styles if s.is_a("IfcSurfaceStyleShading")]):
return self.fallback_colour
colour = styles[0].SurfaceColour
return (colour.Red, colour.Green, colour.Blue, 1)
def get_bmesh_tris(self, obj, bm, camera):
global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start
triangulate = bmesh.ops.triangulate(bm, faces=bm.faces)
vert_map = {}
verts = []
tris = []
i = 0
for geom in results["geom_cut"]:
if isinstance(geom, bmesh.types.BMVert):
verts.append(tuple((obj.matrix_world @ geom.co) + global_offset))
vert_map[geom.index] = i
i += 1
else:
# It seems as though edges always appear after verts
edges.append([vert_map[v.index] for v in geom.verts])
bm.free()
return verts, edges
def get_layer_data(self, element):
usage = ifcopenshell.util.element.get_material(element)
offset = usage.OffsetFromReferenceLine * self.unit_scale
layer_set = usage.ForLayerSet
if len(layer_set.MaterialLayers) == 1:
return # No use slicing if there's only one layer
total_thickness = layer_set.TotalThickness
half_thickness = total_thickness / 2
min_layers = []
max_layers = []
current_thickness = 0
for layer in layer_set.MaterialLayers:
current_thickness += layer.LayerThickness
if current_thickness / total_thickness < 0.5:
min_layers.append((half_thickness - current_thickness) * self.unit_scale)
else:
max_layers.append((current_thickness - half_thickness) * self.unit_scale)
min_layers.reverse()
max_layers.pop()
if usage.DirectionSense == "NEGATIVE":
total_thickness *= -1
offset *= -1
min_layers = [l * -1 for l in min_layers]
max_layers = [l * -1 for l in max_layers]
return {
"offset": offset,
"min_layers": min_layers,
"max_layers": max_layers,
"thickness": total_thickness * self.unit_scale,
}
def get_segments(self, connections, centerline, start_point, end_point):
segments = []
total_connections = len(connections)
if total_connections:
for i in range(0, total_connections + 1):
if i == 0:
connection = connections[i]
segments.append([centerline[0], connection["intersection"], connection["out_point"]])
elif i == total_connections:
segments.append([segments[-1][-1], segments[-1][-2], centerline[1]])
for face in triangulate["faces"]:
tri = []
for vert in face.verts:
if index := vert_map.get(vert.index, None):
tri.append(index)
else:
connection = connections[i]
segments.append(
[segments[-1][-1], segments[-1][-2], connection["intersection"], connection["out_point"]]
)
else:
segments.append([centerline[0], centerline[1]])
if start_point:
segments[0].insert(0, start_point)
if end_point:
segments[-1].append(end_point)
return segments
def get_connections(self, wall, obj, centerline, min_edge, max_edge):
connections = {"ATEND": None, "ATSTART": None, "ATPATH": [], "MINPATH": [], "MAXPATH": []}
for rel in wall.ConnectedTo:
# How do you join to a non layered element? Not sure.
if tool.Model.get_usage_type(rel.RelatedElement) != "LAYER2":
continue
if rel.RelatingConnectionType == "ATPATH":
metadata = self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
if not metadata:
continue
connections["ATPATH"].append(metadata)
else:
metadata = self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
if not metadata:
continue
connections[rel.RelatingConnectionType] = metadata
for rel in wall.ConnectedFrom:
if tool.Model.get_usage_type(rel.RelatingElement) != "LAYER2":
continue
# We only consider ATPATH since in this situation, we have the
# priority. The non-priority wall never has any layers that need to
# "turn a corner".
if rel.RelatedConnectionType == "ATPATH":
metadata = self.get_connection_metadata(obj, rel.RelatingElement, centerline, min_edge, max_edge)
if not metadata:
continue
connections["ATPATH"].append(metadata)
connections["ATPATH"] = sorted(connections["ATPATH"], key=lambda c: c["intersection"].x)
for connection in connections["ATPATH"]:
if connection["angle"] > 0:
connections["MINPATH"].append(connection)
else:
connections["MAXPATH"].append(connection)
return connections
def get_connection_metadata(self, obj, rel_element, centerline, min_edge, max_edge):
rel_obj = tool.Ifc.get_object(rel_element)
layers = self.get_layer_data(rel_element)
if not layers:
return
minx = min([co[0] for co in rel_obj.bound_box])
maxx = max([co[0] for co in rel_obj.bound_box])
rel_centerline = [
Vector((minx, layers["offset"] + layers["thickness"] / 2)),
Vector((maxx, layers["offset"] + layers["thickness"] / 2)),
]
rel_centerline = [obj.matrix_world.inverted() @ rel_obj.matrix_world @ v.to_3d() for v in rel_centerline]
rel_centerline = [v.to_2d() for v in rel_centerline]
intersection = tool.Cad.intersect_edges(centerline, rel_centerline)
if intersection:
intersection, _ = intersection
else:
return
closest_centerline_point = tool.Cad.closest_vector(intersection, tuple(rel_centerline))
if closest_centerline_point == rel_centerline[1]:
rel_centerline = [rel_centerline[1], rel_centerline[0]]
angle = tool.Cad.angle_edges(centerline, rel_centerline, signed=True)
# A little extreme, but maybe it's OK?
out_point = tool.Cad.furthest_vector(intersection, tuple(rel_centerline))
return {
"element": rel_element,
"obj": rel_obj,
"centerline": rel_centerline,
"intersection": intersection,
"angle": angle,
"out_point": out_point,
}
verts.append(tuple((obj.matrix_world @ vert.co) + global_offset))
vert_map[vert.index] = i
tri.append(i)
i += 1
tris.append(tri)
return verts, tris
class DecorationsHandler:
@@ -2022,7 +1927,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 +1951,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
+25 -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
@@ -413,6 +414,8 @@ class ExtrusionGuidesGizmo(CustomGizmo, types.Gizmo):
shader_wrapper = ExtrusionGuidesShader()
verts = [Vector((0, 0, 0)), Vector((0, 0, 1))]
verts, edges = shader_wrapper.process_geometry(verts)
if not tool.Blender.validate_shader_batch_data(verts, edges):
verts, edges = [], []
self.custom_shape = shader_wrapper, shader_wrapper.batch(
pos=verts,
indices=edges,
@@ -478,21 +481,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 +528,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]
+113 -13
View File
@@ -19,8 +19,11 @@
import bpy
import math
import mathutils.geometry
import ifcopenshell
import ifcopenshell.util.unit
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
@@ -128,15 +131,53 @@ def format_distance(
decimal_places=None,
suppress_zero_inches=False,
in_unit_length=False,
custom_unit=None,
):
s_code = "\u00b2" # Superscript two THIS IS LEGACY (but being kept for when Area Measurements are re-implimented)
# Get Scene Unit Settings
scaleFactor = bpy.context.scene.unit_settings.scale_length
# Get Blender Scene Unit Settings
unit_scale = bpy.context.scene.unit_settings.scale_length
unit_system = bpy.context.scene.unit_settings.system
unit_length = bpy.context.scene.unit_settings.length_unit
area_unit_symbol = " m2" if unit_system == "METRIC" else " ft2"
value *= scaleFactor
# Get IFC Unit Settings
if tool.Ifc.get():
unit_scale = 1
if length_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "LENGTHUNIT"):
unit_system = "METRIC" if length_unit.Name == "METRE" else "IMPERIAL"
unit_length = length_unit.Name.upper()
if hasattr(length_unit, "Prefix") and length_unit.Prefix:
unit_length = length_unit.Prefix + length_unit.Name
unit_length_mapping = {
"FOOT": "FEET",
"INCH": "INCHES",
"METRE": "METERS",
"DECIMETRE": "DECIMETERS",
"CENTIMETRE": "CENTIMETERS",
"MILLIMETRE": "MILLIMETERS",
}
unit_length = unit_length_mapping[unit_length]
# For now we only format area in IFC Units
if area_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT"):
area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol(area_unit)
unit_fraction = True if unit_system == "IMPERIAL" else False
# Custom Unit Settings
if custom_unit:
unit_mapping = {
"Feet and Inches - Fractional": ("IMPERIAL", "FEET", True),
"Feet - Decimal": ("IMPERIAL", "FEET", False),
"Inches - Fractional": ("IMPERIAL", "INCHES", True),
"Inches - Decimal": ("IMPERIAL", "INCHES", False),
"Meters": ("METRIC", "METERS", False),
"Decimeters": ("METRIC", "DECIMETERS", False),
"Centimeters": ("METRIC", "CENTIMETERS", False),
"Millimeters": ("METRIC", "MILLIMETERS", False),
}
if custom_unit in unit_mapping:
unit_system, unit_length, unit_fraction = unit_mapping[custom_unit]
value *= unit_scale
# Imperial Formatting
if unit_system == "IMPERIAL":
@@ -161,10 +202,14 @@ def format_distance(
base = int(precision)
decInches = value * toInches
decFeet = decInches / 12
# Separate ft and inches
# Unless Inches are the specified Length Unit
if unit_length != "INCHES":
# Unless Inches are the specified Length Unit or unit_fraction is False
if unit_length == "FEET" and not unit_fraction:
feet = round(decInches / inPerFoot, 3) # keep decimal
decInches = 0
elif unit_length != "INCHES":
feet = int(decInches / inPerFoot) # remove decimal
decInches -= feet * inPerFoot
else:
@@ -199,6 +244,10 @@ def format_distance(
feet += 1
inches = 0
# Check whether decimal or fractional
if not unit_fraction:
inches = round(decInches, 3)
frac = None
if not isArea:
add_inches = bool(inches) or not suppress_zero_inches or (inches == 0 and frac)
tx_dist = ""
@@ -224,12 +273,33 @@ def format_distance(
tx_dist += str(frac) + "/" + str(base)
if add_inches or frac:
tx_dist += '"'
if precision == "12" and unit_system == "IMPERIAL":
tx_dist = str(round(decFeet)) + "'"
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":
if in_unit_length:
if unit_length == "DECIMETERS":
value = value / 10
if unit_length == "CENTIMETERS":
value = value / 100
if unit_length == "MILLIMETERS":
@@ -248,6 +318,14 @@ def format_distance(
if hide_units is False:
fmt += " m"
tx_dist = fmt % value
# Decimeters
elif unit_length == "DECIMETERS":
if decimal_places is None:
fmt = "%1.1f"
if hide_units is False:
fmt += " dm"
d_dm = value * (10)
tx_dist = fmt % d_dm
# Centimeters
elif unit_length == "CENTIMETERS":
if decimal_places is None:
@@ -287,19 +365,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 +516,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
File diff suppressed because it is too large Load Diff
+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:
+48 -12
View File
@@ -32,6 +32,7 @@ from mathutils import Vector
import re
VIEW_TITLE_OFFSET_Y = 5
DRAWING_PADDING = 10
DEFAULT_POSITION = Vector((30, 30))
SVG = "{http://www.w3.org/2000/svg}"
XLINK = "{http://www.w3.org/1999/xlink}"
@@ -113,13 +114,15 @@ class SheetBuilder:
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
x, y = self.next_drawing_location(layout_root, view_width)
# add background
if os.path.isfile(underlay_path):
background = ET.SubElement(view, "image")
background.attrib["data-type"] = "background"
background.attrib["xlink:href"] = os.path.relpath(underlay_path, layout_dir)
background.attrib["x"] = str(DEFAULT_POSITION.x)
background.attrib["y"] = str(DEFAULT_POSITION.y)
background.attrib["x"] = str(x)
background.attrib["y"] = str(y)
background.attrib["width"] = str(view_width)
background.attrib["height"] = str(view_height)
@@ -128,16 +131,49 @@ class SheetBuilder:
foreground = ET.SubElement(view, "image")
foreground.attrib["data-type"] = "foreground"
foreground.attrib["xlink:href"] = os.path.relpath(drawing_path, layout_dir)
foreground.attrib["x"] = str(DEFAULT_POSITION.x)
foreground.attrib["y"] = str(DEFAULT_POSITION.y)
foreground.attrib["x"] = str(x)
foreground.attrib["y"] = str(y)
foreground.attrib["width"] = str(view_width)
foreground.attrib["height"] = str(view_height)
self.add_view_title(
DEFAULT_POSITION.x, view_height + DEFAULT_POSITION.y + VIEW_TITLE_OFFSET_Y, view, layout_dir
)
self.add_view_title(x, view_height + y + VIEW_TITLE_OFFSET_Y, view, layout_dir)
layout_tree.write(layout_path)
def next_drawing_location(self, layout_root: ET.Element, next_width: float) -> list:
titleblocks = layout_root.findall(f'{SVG}g[@data-type="titleblock"]')
drawings = layout_root.findall(f'{SVG}g[@data-type="drawing"]')
# how wide is the title block frame
try:
titleblock_width = self.convert_to_mm(titleblocks[0][0].attrib.get("width"))
except (IndexError, AttributeError):
titleblock_width = 840.0
# where does the last drawing finish
try:
last = drawings[-1][0]
last_width = self.convert_to_mm(last.attrib.get("width"))
last_x = self.convert_to_mm(last.attrib.get("x"))
last_y = self.convert_to_mm(last.attrib.get("y"))
except (IndexError, AttributeError):
return [DEFAULT_POSITION.x, DEFAULT_POSITION.y]
# check if the new drawing fits in the current row
if last_x + last_width + DRAWING_PADDING + next_width + DEFAULT_POSITION.x < titleblock_width:
return [last_x + last_width + DRAWING_PADDING, last_y]
# start a new row, find the y
for drawing in drawings:
for image in drawing:
try:
image_y = self.convert_to_mm(image.attrib.get("y"))
image_height = self.convert_to_mm(image.attrib.get("height"))
except AttributeError:
return [DEFAULT_POSITION.x, DEFAULT_POSITION.y]
if image_y + image_height + DRAWING_PADDING > last_y:
last_y = image_y + image_height + DRAWING_PADDING
return [DEFAULT_POSITION.x, last_y]
def update_sheet_drawing_sizes(self, sheet: ifcopenshell.entity_instance) -> None:
ET.register_namespace("", "http://www.w3.org/2000/svg")
@@ -221,6 +257,8 @@ class SheetBuilder:
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
x, y = self.next_drawing_location(layout_root, view_width)
view = ET.SubElement(layout_root, "g")
view.attrib["data-id"] = str(reference.id())
view.attrib["data-type"] = document.Scope.lower()
@@ -229,14 +267,12 @@ class SheetBuilder:
foreground = ET.SubElement(view, "image")
foreground.attrib["data-type"] = "content"
foreground.attrib["xlink:href"] = os.path.relpath(view_path, layout_dir)
foreground.attrib["x"] = str(DEFAULT_POSITION.x)
foreground.attrib["y"] = str(DEFAULT_POSITION.y)
foreground.attrib["x"] = str(x)
foreground.attrib["y"] = str(y)
foreground.attrib["width"] = str(view_width)
foreground.attrib["height"] = str(view_height)
self.add_view_title(
DEFAULT_POSITION.x, view_height + DEFAULT_POSITION.y + VIEW_TITLE_OFFSET_Y, view, layout_dir
)
self.add_view_title(x, view_height + y + VIEW_TITLE_OFFSET_Y, view, layout_dir)
layout_tree.write(layout_path)
def add_view_title(self, x: float, y: float, parent: ET.Element, layout_dir: str) -> None:
@@ -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)
@@ -1128,7 +1128,12 @@ class SvgWriter:
def get_text():
radius = (points[-1].co - points[-2].co).length
radius = helper.format_distance(radius, precision=self.precision, decimal_places=self.decimal_places)
radius = helper.format_distance(
radius,
precision=self.precision,
decimal_places=self.decimal_places,
custom_unit=dimension_data["custom_unit"],
)
text = f"R{radius}"
return text
@@ -1207,13 +1212,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)} %"
@@ -1255,6 +1261,7 @@ class SvgWriter:
text_prefix=dimension_data["text_prefix"],
text_suffix=dimension_data["text_suffix"],
fill_bg=dimension_data["fill_bg"],
custom_unit=dimension_data["custom_unit"],
)
def draw_dimension_annotations(self, obj):
@@ -1279,6 +1286,7 @@ class SvgWriter:
text_prefix=dimension_data["text_prefix"],
text_suffix=dimension_data["text_suffix"],
fill_bg=dimension_data["fill_bg"],
custom_unit=dimension_data["custom_unit"],
)
def draw_measureit_arch_dimension_annotations(self):
@@ -1305,6 +1313,7 @@ class SvgWriter:
text_prefix="",
text_suffix="",
fill_bg=False,
custom_unit=None,
):
offset = Vector([self.raw_width, self.raw_height]) / 2
v0 = self.project_point_onto_camera(v0_global)
@@ -1338,6 +1347,7 @@ class SvgWriter:
precision=self.precision,
decimal_places=self.decimal_places,
suppress_zero_inches=suppress_zero_inches,
custom_unit=custom_unit,
)
text = text_prefix + str(dimension) + text_suffix
else:

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