Compare commits

...

253 Commits

Author SHA1 Message Date
Dion Moult 90fce18042 New icon tabbed interface so you don't need to click twice to change tab. 2023-07-15 13:22:13 +10:00
Dion Moult 0a840188bf DANGER! Potentially fix a bunch of undo bugs by completely rebuilding undo id_map tracking with a much simpler solution.
In the past, we used undo_pre and redo_pre and a bunch of complex commit_link and rollback_link tracking to figure out what IDs and GUIDs were added / removed during transactions and how to handle invalidated objects in the id/guid maps.

I really don't know why I made it that complex.

Now we simply treat all data as bad and rebuild the either map. This means slower undos / redos for large projects, but in theory the maps will never invalidate.

This either fixes a lot of things or breaks a lot of things :) Watch out!
2023-07-15 00:05:34 +10:00
Dion Moult a2e7cb8158 Fix undo bugs in the type module where some operators were not part of the undo system for some reason. 2023-07-15 00:00:10 +10:00
Dion Moult 2034fa203b Fix #3275. You can now rename types from the type manager. 2023-07-14 23:58:33 +10:00
Andrej730 572a8c812e root.remove_product fix 2023-07-14 18:22:31 +05:00
Andrej730 af4b5e1fe6 oops, forgot to clean up the test name 2023-07-14 18:02:51 +05:00
Andrej730 9c383e0c1a Fixed plan level annotation rotation #3440 2023-07-14 17:42:45 +05:00
Andrej730 a72231cb91 StairData refactor, solves #3416 2023-07-14 16:17:14 +05:00
Andrej730 846dbc341d root.copy_class to support copying ports in ifc2x3 2023-07-14 15:48:41 +05:00
Andrej730 ed95516b9d Clean up type objects on ifc import 2023-07-14 15:04:15 +05:00
Andrej730 b6ddc0d55e avoid IfcSpatialElement errors in ifc2x3
IfcSpatialElement was added only in ifc4
2023-07-14 14:46:58 +05:00
Andrej730 be5950bcb7 clean up after running test_wall_opening.py 2023-07-14 14:46:58 +05:00
Andrej730 6810bbff2f remove_product to remove IfcRelConnectsElements 2023-07-14 14:46:58 +05:00
Andrej730 2fa7cbaabf Fixing confusing UI #3357
It was showing "Edit Profile" when you have slab and wall selected when it will actually extend the wall to the slab.
2023-07-14 14:46:58 +05:00
Dion Moult 69fa12423d See #3421. Fix drawing bug where IfcOpenShell might return unclosed paths even though the shape is closed and therefore break cut polygon fills. 2023-07-14 17:32:45 +10:00
Dion Moult 8f609ff383 See #3421. Fix bug where some projection fills wouldn't polygonize correctly in shapely due to numerical tolerance. 2023-07-14 17:29:30 +10:00
Dion Moult 924bf2a3ad Fix #3442. Update sheet list if drawing name changes. 2023-07-14 13:54:24 +10:00
Dion Moult 2be8f4a139 Move object data out of object tab and into new object dropdown. 2023-07-14 13:41:02 +10:00
Dion Moult 8411feedf4 You can now open projects from the Ctrl-N shortcut. 2023-07-14 13:40:09 +10:00
Gorgious56 56e6c8e0e1 Improve slightly the boolean interface in mesh properties editor 2023-07-13 14:52:00 +02:00
Dion Moult 818226d1e2 Fix #3414. The project wizard now doesn't load a fresh session by default. Note that File > Open still enforces a fresh session. 2023-07-13 22:34:13 +10:00
Andrej730 6336241431 Cleaning up connections on delete_ifc
Noticed an error if you connect wall and slab, then remove the slab and try to regenerate the wall - error occured because connection to that slab wasn't removed.

The error was:
Error: Python: Traceback (most recent call last):
  File "\3.6\scripts\addons\blenderbim\bim\module\model\workspace.py", line 381, in invoke
    return self.execute(context)
  File "\3.6\scripts\addons\blenderbim\tool\ifc.py", line 136, in execute
    IfcStore.execute_ifc_operator(self, context)
  File "\3.6\scripts\addons\blenderbim\bim\ifc.py", line 421, in execute_ifc_operator
    result = getattr(operator, "_execute")(context)
  File "\3.6\scripts\addons\blenderbim\bim\module\model\workspace.py", line 373, in _execute
    getattr(self, f"hotkey_{self.hotkey}")()
  File "\3.6\scripts\addons\blenderbim\bim\module\model\workspace.py", line 465, in hotkey_S_E
    bpy.ops.bim.join_wall(join_type="T")
  File "C:\software\Steam\steamapps\common\Blender\3.6\scripts\modules\bpy\ops.py", line 113, in __call__
    ret = _op_call(self.idname_py(), None, kw)
RuntimeError: Error: Python: Traceback (most recent call last):
  File "\3.6\scripts\addons\blenderbim\tool\ifc.py", line 136, in execute
    IfcStore.execute_ifc_operator(self, context)
  File "\3.6\scripts\addons\blenderbim\bim\ifc.py", line 421, in execute_ifc_operator
    result = getattr(operator, "_execute")(context)
  File "\3.6\scripts\addons\blenderbim\bim\module\model\wall.py", line 76, in _execute
    joiner.join_E(context.active_object, context.scene.cursor.location)
  File "\3.6\scripts\addons\blenderbim\bim\module\model\wall.py", line 1038, in join_E
    self.recreate_wall(element1, wall1, axis, body)
  File "\3.6\scripts\addons\blenderbim\bim\module\model\wall.py", line 1145, in recreate_wall
    height = self.clip(obj, tool.Ifc.get_object(rel.RelatingElement))
  File "\3.6\scripts\addons\blenderbim\tool\ifc.py", line 89, in get_object
    return IfcStore.get_element(element.id())
AttributeError: 'NoneType' object has no attribute 'id'
2023-07-13 17:32:25 +05:00
Dion Moult 7720166c31 Fix #3393. Bug where duplicating a typed text didn't also copy its literal. 2023-07-13 22:22:00 +10:00
Andrej730 3b05799956 Delete edit text from BIM Tool since it's already in Annotation Tool 2023-07-13 15:54:28 +05:00
Andrej730 f3924249e9 handle null diffuse color loading ifc styles
The error was:
Traceback (most recent call last):
  File "\3.6\scripts\addons\blenderbim\bim\module\style\prop.py", line 99, in update_shading_style
    tool.Style.set_surface_style_props(blender_material)
  File "\3.6\scripts\addons\blenderbim\tool\style.py", line 174, in set_surface_style_props
    style_data["DiffuseColour"] = style_data["DiffuseColour"][1]
TypeError: 'NoneType' object is not subscriptable
2023-07-13 14:54:36 +05:00
Gorgious56 7f9e8a207d Enable drawing generation buttons only when an actual drawing is selected 2023-07-13 11:38:11 +02:00
Andrej730 38f7c044a3 Fixed error on canceling editing railing/roof
Since we do not update representation for railing / roofs and just mark them wtih `tool.Ifc.edit(obj)` their previous representation may still be incorrect. So we restore the previous representation just using bmesh update method.

For example if create a new roof, enter editing roof properties mode and then cancel editing, then it would result in "Failed to process a shape" because roof still had just empty Body/PointCloud representation.
2023-07-13 14:32:09 +05:00
Andrej730 7896d5b6e5 Fixing errors on loading ifc after ef8f82501
The error was:
RuntimeError: Operator bpy.ops.object.editmode_toggle.poll() failed, context is incorrect
Location: C:\software\Steam\steamapps\common\Blender\3.6\scripts\modules\bpy\ops.py:113
Traceback (most recent call last):
  File "\3.6\scripts\addons\blenderbim\bim\module\project\operator.py", line 594, in execute
    bpy.ops.bim.load_project_elements()
  File "C:\software\Steam\steamapps\common\Blender\3.6\scripts\modules\bpy\ops.py", line 113, in __call__
    ret = _op_call(self.idname_py(), None, kw)
RuntimeError: Error: Python: Traceback (most recent call last):
  File "\3.6\scripts\addons\blenderbim\bim\module\project\operator.py", line 653, in execute
    ifc_importer.execute()
  File "\3.6\scripts\addons\blenderbim\bim\import_ifc.py", line 265, in execute
    self.clean_mesh()
  File "\3.6\scripts\addons\blenderbim\bim\import_ifc.py", line 1347, in clean_mesh
    bpy.ops.object.editmode_toggle(context_override)
  File "C:\software\Steam\steamapps\common\Blender\3.6\scripts\modules\bpy\ops.py", line 111, in __call__
    ret = _op_call(self.idname_py(), C_dict, kw, C_exec, C_undo)
RuntimeError: Operator bpy.ops.object.editmode_toggle.poll() failed, context is incorrect
Location: C:\software\Steam\steamapps\common\Blender\3.6\scripts\modules\bpy\ops.py:113
2023-07-13 13:45:50 +05:00
Andrej730 96d812eaba Fixed console errors on checking modeling data 2023-07-13 13:03:18 +05:00
Dion Moult 0390a65590 Fix #3300. Bug where it was impossible to delete normal Blender objects in the outliner when an IFC existed.
Also port over batch deletion to the outliner override.
2023-07-13 16:26:06 +10:00
Andrej730 ad4ab1567f model data parameters renamed to pset_data for consitency 2023-07-13 11:08:52 +05:00
Andrej730 d633733d18 DoorData use refactor 2023-07-13 10:58:03 +05:00
Andrej730 cd1a813630 WindowData use refactor 2023-07-13 10:36:09 +05:00
Andrej730 c9e6da9b6c change .is_editing type for ifc modifiers to boolean 2023-07-13 09:53:11 +05:00
Andrej730 cada2c36e3 RoofData use refactor 2023-07-13 09:48:33 +05:00
Dion Moult 33304eb1a5 Group drawings by target view in the drawing list 2023-07-13 14:13:28 +10:00
Bruno Perdigão 4e72ef8b20 Select all objects in array modifier #3331 2023-07-13 13:21:49 +10:00
Dion Moult b9d8e86748 Fix #3411. Filtering sheets now doesn't break sheet hierarchy and also lets you search for sheet identification, name, and names of drawings / titleblocks / things on sheets. 2023-07-13 11:53:08 +10:00
Dion Moult 3adfc63b65 Fix #3432. Bug where null applicable occurrence of a type product caused the annotation tool to fail. 2023-07-13 11:27:01 +10:00
Andrej730 91e7d49060 makefile fix 2023-07-12 18:34:48 +05:00
Andrej730 4ad1a2be73 minor change for RailingData 2023-07-12 17:46:56 +05:00
Andrej730 ef8f825012 bim.load_project to refresh session on execute not invoke
if you accidentally opened file dialog or just changed your mind it shouldn't remove all objects from the current blender session
2023-07-12 17:10:30 +05:00
Andrej730 56adfc70b5 RailingData use refactor, solves #3416 2023-07-12 17:01:51 +05:00
smr02 bc909a26fa ifcsverchok delete api_WIP 2023-07-12 21:18:20 +10:00
Andrej730 dd0a015ec3 display current commit in status bar
it works for users who symlinked their blenderbim/bim folders to git repository or installed blenderbim from release created from makefile
https://i.imgur.com/mEBH9I7.png
2023-07-12 12:34:24 +05:00
Gorgious56 3813c26db7 Fix error when trying to edit an object that doesn't support edit mode with the TAB keypress
Also run black on the rest of the file
2023-07-12 09:28:21 +02:00
Dion Moult 95c06c7f82 Fix #3420. Fix missing sheet references on drawings and incorrect direction of section references. 2023-07-12 17:17:54 +10:00
Andrej730 db910d4024 more consistency in values for plan-, section level and diameter 2023-07-12 11:47:19 +05:00
Andrej730 669dbd2e8d radius annotation - support for prefix/suffix/fill-bg
and more consistency between viewport and svg
2023-07-12 11:47:18 +05:00
Andrej730 becbfccae9 PLAN/SECTION_LEVEL consistency between viewport and svg
before - https://i.imgur.com/bwYbozz.png
after - https://i.imgur.com/7BlmQIg.png

NOTE that if you're using markers.svg that was created before this update you won't see the changes for the section-/plan-level markers.
2023-07-12 11:47:18 +05:00
Andrej730 2f02d401f2 fill-bg for diameter, plan- and section-level annotations 2023-07-12 11:47:18 +05:00
Dion Moult ec2ca4bcf4 Fix #3417. Apply 50mm offset to camera to ensure text isn't "behind" the camera. This 50mm convention is used in quite a few places, we should standardise it. 2023-07-12 12:37:53 +10:00
Dion Moult 89df937f89 Fix #3425. Fix invalid IFC data where grid axes should be 2D not 3D. 2023-07-12 11:41:58 +10:00
Thomas Krijnen 7567971a32 Update entity_instance.py 2023-07-11 21:41:06 +02:00
Thomas Krijnen 74160731c9 Update rule_executor.py 2023-07-11 21:40:05 +02:00
Andrej730 5d7fefa042 Prevent resetting selected tab on blender restart
Not sure if there is any caveats to that
2023-07-11 18:23:26 +05:00
Dion Moult db4c61a472 Fix #3407. Override Ctrl-S to the export IFC which checks for Blender projects.
1. No IFC --> Saves Blend file
 2. IFC, unsaved Blend --> Saves IFC
 3. IFC, saved Blend --> Saves IFC and Blend
2023-07-11 23:08:05 +10:00
Andrej730 298568d3ea PLAN_LEVEL annotations support for prefix, suffix #3399
Viewport - https://i.imgur.com/XRa3oJO.png
SVG - https://i.imgur.com/C8PjoiO.png
2023-07-11 17:04:58 +05:00
Dion Moult 4da7b355bf Fix #3414. Only clear default scene, not custom scene when loading / creating IFCs. 2023-07-11 21:45:34 +10:00
Andrej730 2a15743f8b SECTION_LEVEL annotations support for prefix, suffix #3399
also added support for "show only description" for section_level
2023-07-11 16:42:07 +05:00
Andrej730 e54cc11ca6 Fixed #3400 introduced in dda16c8 2023-07-11 14:54:51 +05:00
Dion Moult 24fdfc6d82 Resolve merge conflict 2023-07-11 18:15:18 +10:00
Dion Moult 8689164769 Fix registration bug that created hundreds of workspaces due to continuous triggering of the depsgraph_update.
Use an is_registering check to only run the on_register once.

Rename setDefaultProperties to load_post to better reflect what handler it is.

Unregister our custom scene panels upon addon activation.

Move aprops getting to tool.Blender because it is a bit complex. Also, aprops may not be setup until load_post is called, so the len() check protects against that.
2023-07-11 18:08:02 +10:00
Gorgious56 86dd79daa6 Keep active object when unlinking an IFC element
Unlinking an element recreates a new blender object so the active object would be lost, jumping the properties to the scene panel which was slightly annoying
2023-07-11 09:33:06 +02:00
Andrej730 a231cc31e8 fixed properties search after 949a2bb and fixed console errors on search
1) When you search not from the SCENE properties blender creates some kind of phantom area to check search results. As this area is not present in `context.screen.areas` it was resulting in the error below.

2) After 949a2bb search in scene properties was limited to currently selected tab. Given the amount of tools we have in properties and native blender approach for searching across all available sections in properties, it seems reasonable to make search work across all tabs.

Console error was:
The error was
Python: Traceback (most recent call last):
  File "\3.6\scripts\addons\blenderbim\bim\ui.py", line 281, in draw
    aprops = context.screen.BIMAreaProperties[context.screen.areas[:].index(context.area)]
ValueError: bpy.data.screens['Layout.001']...Area is not in list
2023-07-11 11:00:20 +05:00
Dion Moult e412d27398 Integrate brickschema undo system into BlenderBIM Add-on undo system wrapper 2023-07-11 11:16:49 +10:00
Andrej730 60388f1a24 Fixed #3412
After 949a2bb printing happens not fron N-panel (which is part of viewport) but from scene properties - so current context doesn't have region_3d anymore.

PS The same issue was there all along if you'd try to print the drawing from Camera Properties tab.
2023-07-10 17:31:11 +05:00
Andrej730 d13d1493e3 allow to call select/color by attribute by providing arguments
instead of always checking scene.BIMSearchProperties. This allows to add different setups of selections to quick favorites
2023-07-10 17:17:12 +05:00
Dion Moult 556489d686 Implement Ctrl-Tab in properties to conveniently toggle between two tabs. 2023-07-10 22:04:10 +10:00
Dion Moult 657e77117c Move built-in scene panels to a dedicated "Blender Properties" tab instead of unregistering permanently. 2023-07-10 22:04:10 +10:00
Andrej730 7d97f5e375 Library for US AISC15 Steel Profiles #3203 2023-07-10 16:36:07 +05:00
Gorgious56 7583b56738 Trying to edit a railing modifier path while modifying the parameters is now prevented and user is informed with a tooltip 2023-07-10 13:00:43 +02:00
Gorgious56 073a00b2ca Fix IfcRailing modifier support spacing <= 0 would throw an error, and deactivate support spacing field if manual support is selected 2023-07-10 12:47:46 +02:00
Thomas Krijnen 8baa0945e9 2dc4bbf8cd Apply same logic for derived attributes 2023-07-10 10:26:37 +02:00
Dion Moult 717c28b7c9 Override Ctrl-N new file menu to make it easier to start new projects and new IFC icon. 2023-07-10 17:16:32 +10:00
Andrej730 f4a4e906ab Using EPset_Drawing data instead of camera props on drawing print #3396
Previously if you change `HasUnderlay` in pset it caused errors because camera properties `has_underlay` wasn't updated.
Still some work need to be done for updating keep camera properties in sync with pset - probably we can should call something like `tool.Drawing.import_drawing()` on EPset_Drawing edit
2023-07-10 11:09:48 +05:00
Dion Moult 052ff71c51 See #3411. Sheets are now sorted alphabetically by default. 2023-07-10 14:13:44 +10:00
Dion Moult 3dfd36877e See #3016. Scene shading styles are now stored as part of the drawing style. 2023-07-10 14:13:44 +10:00
Dion Moult a9f2237d32 Update splash image to be centered 2023-07-10 13:15:49 +10:00
Dion Moult 62c93f0462 Fix #3410. Minor regression when moving UI to properties panel. 2023-07-10 11:19:15 +10:00
Dion Moult 60ea504267 Add support for Point representations too, not just point cloud. 2023-07-10 10:23:33 +10:00
Dion Moult 1e3b45c0b8 Fix bug where all grouped annotations were excluded when opening models, whereas we only want drawing annotations to be excluded. 2023-07-10 10:23:33 +10:00
Massimo Fabbro 757277cedf Fix bug where drawing decimal places couldn't be 0. Now it's possible to define 0 as value 2023-07-09 18:52:03 +02:00
Dion Moult 14fced7aba Forgot to commit updated IfcTester test suite 2023-07-09 22:30:14 +10:00
Dion Moult 7a3b299148 Fix critical undo sync bug related to invoking instead of executing. See notes.
Currently, the undo system works by wrapping the execute operator. What was not considered was that execute() isn't the only entry point to operators. Operators can be invoked. If an invoked operator calls a bpy.ops.bim.something in its invoke() function, then bpy.ops.bim.something will start and finish its own transaction, and then when the invoke finishes and finally runs execute(), another transaction will be started and finished. This means that we get at least one extra transaction logged (one per any call to bpy.ops.bim.foo in invoke()).

This mismatch of number of transactions and undo stack means that when the user presses ctrl-z, the undo doesn't actually occur. This leads to a sync bug.

To be safe, all IFC invokes must be wrapped. This will take some time to review so expect another commit soon.
2023-07-09 22:29:49 +10:00
Dion Moult 45cf6a8ace Drawing panels now moved to the drawings and documents "tab" 2023-07-09 18:30:40 +10:00
Dion Moult 9b1693e112 Update IfcTester to comply with latest modifications to IDS XSD. 2023-07-09 17:02:12 +10:00
Dion Moult cc59929cfd Visual clean up of annotation tool to only activate when a drawing is active and have a type manager similar to the BIM tool. 2023-07-09 16:01:16 +10:00
Dion Moult 43fd012a4c Update tool icons to all use the same underlying "cube" shape and add bracing for structural icon and background walls for spatial icon. 2023-07-08 22:56:23 +10:00
Bruno Perdigão f96f663dc3 recreate decompositions in Refresh operator 2023-07-08 22:12:24 +10:00
Bruno Perdigão 486bafc814 changed keymap 2023-07-08 22:12:24 +10:00
Bruno Perdigão 20a31be5bf clear BIMObjectPropertis.collection after duplication 2023-07-08 22:12:24 +10:00
Bruno Perdigão 98dad0cbbe initial commit 2023-07-08 22:12:24 +10:00
Dion Moult b0ce2e5a00 Fix #3390. Type products should not be set in the model interface. 2023-07-08 22:09:50 +10:00
Thomas Krijnen 3aea0915d0 #3189 proposal (#3190) 2023-07-08 20:07:28 +10:00
Bruno Perdigão 0ee5e3887b removed total_items from the operator 2023-07-08 20:06:18 +10:00
Bruno Perdigão 1afcec2005 implementation of #3332 2023-07-08 20:06:18 +10:00
Dion Moult 656b7cb06e Merge pull request #3405 from brunoperdigao/pset_select_same_value
Bug fix in #3392
2023-07-08 20:05:22 +10:00
Dion Moult 19d3f98b6f Merge branch 'v0.7.0' into pset_select_same_value 2023-07-08 20:05:13 +10:00
Dion Moult 73a736f52a Fix #3402. Activate drawing no longer affects the visibility of non-IFC objects. 2023-07-08 19:55:45 +10:00
Dion Moult 6adc48ade4 Fix #3403. Templates must be specific to the version you are creating. 2023-07-08 19:30:11 +10:00
Dion Moult 949a2bb347 Start mocking up our own property "tabs" with the intention to use this new tab system instead of Blender property tabs
Blender property tabs are hardcoded and we cannot customise them. IFC data only "somewhat" maps to Blender property tabs, and some things fail to map causing user confusion. For example material and styles are very confusing. Also scene and object properties are crazy overpopulated (see how many times IFCArchitect bulk collapses things). And this also causes things to be spread out and lost (e.g. some random panels in viewport N menu). This should give us a lot more control to organise panels exactly how we want them.
2023-07-08 17:03:06 +10:00
Dion Moult 8cb63720eb Workspace inspired by IFCArchitect's layout is now setup by default and irrelevant Blender scene panels are hidden. You can disable this behaviour for advanced users, or use your own workspace layout but at least now there is a better "BIM-optimised" layout.
The layout uses three columns with a focus on a larger outliner (important as BIM models have lots of objects and complex hierarchies!) and properties, and removes the mostly irrelevant timeline for beginners.
2023-07-08 16:26:21 +10:00
Dion Moult 52ac1090e9 You can now create, open, and save projects from the file menu, with simple project presets. Also the scene is cleared so no more manual cube / light / camera deletion. Also poll prior to saving. No more import / export menu to confuse users.
https://imgur.com/a/EIt2H5b
2023-07-08 14:39:39 +10:00
Dion Moult 4f6180baf8 Delete old annotation panel in favour of annotation tool 2023-07-08 10:58:53 +10:00
Bruno Perdigão 743339598e minor fix 2023-07-07 15:27:48 -03:00
Bruno Perdigão 2e423f26ff bugfix and alt+click functionality 2023-07-07 15:19:35 -03:00
luzpaz b3b45893cc Fix various typos in src/ifcgeom* subdirs 2023-07-07 18:44:02 +02:00
Dion Moult a65747dd2c Minor doc improvements and SQL/stream code cleanup 2023-07-07 18:12:52 +10:00
Dion Moult 4286ad6dcc Fix #3394. Fix bug where railing and roof type names were hardcoded. Also improve UI of create type form so you can actually read the fields. 2023-07-07 17:57:09 +10:00
Dion Moult 0b823c772e Fix #3213. Internally, we should always use Unix path separators. 2023-07-07 17:20:07 +10:00
Dion Moult 9944024457 The user can now toggle between rotate before / after translating in OffsetObjectPlacements 2023-07-07 15:57:42 +10:00
Dion Moult 3cdad90755 Fix #3016. Drawing styles that set the shading style will change shading style upon activating the style. 2023-07-07 15:17:53 +10:00
Dion Moult 71846d683b Fix #3121. Merge equivalent contexts when merging projects in MergeProject Ifcpatch recipe. 2023-07-07 13:04:44 +10:00
Dion Moult 7be0fd6137 Fix failing tests since Github test runner only has IFC4X3_ADD1 not TC1 compiled in. 2023-07-07 12:33:38 +10:00
Bruno Perdigão 1eef746f60 implements #3268 2023-07-07 12:15:05 +10:00
Trashman247 1696ca5e08 Remove binding namespace new project (already in schema) 2023-07-06 16:47:53 -07:00
Trashman247 c011458ea2 remove import BrickStore in props 2023-07-06 16:47:53 -07:00
Trashman247 abf809f9dd Add brick operator accepts label parameter & add toggle for namespace editing 2023-07-06 16:47:53 -07:00
Trashman247 9257816e62 Add filter to listed namespaces (need add more aliases) 2023-07-06 16:47:53 -07:00
Trashman247 d47125b28f Double space core functions and clear brick browser on project close 2023-07-06 16:47:53 -07:00
Trashman247 b7453f584e Separate serialize into "save" and "save as" with file selection 2023-07-06 16:47:53 -07:00
Bruno Perdigão 6a7cf6843a implements #3268 2023-07-06 15:10:53 -03:00
Sigma Dimensions 9c4fa666c9 Georeferencing utility to convert an angle in degrees to X Axis Abscissa and Ordinate 2023-07-06 16:02:03 +01:00
Dion Moult a970120fac See #3126 and 5b4010c. Fix failing tests, and allow schema_by_name to follow the same schema_version distinction as file. 2023-07-06 22:40:31 +10:00
Andrej730 72907cd3f0 w-type US steel profiles #3203 2023-07-06 17:09:03 +05:00
Dion Moult ae7c9f35a6 See #3126. Forgot to commit tests related to explicit schema versions. 2023-07-06 20:56:33 +10:00
Thomas Krijnen 0b111084b0 Update win32 download links installation.rst 2023-07-06 11:30:09 +02:00
Thomas Krijnen 2ee0dcaf64 #3359 return early in case of no-prefiltering 2023-07-06 11:21:17 +02:00
Andrej730 70eabead92 fill-bg support for dimensions #3387
Example - https://i.imgur.com/lvJwtDI.png
works the same way as for text by adding "fill-bg" to EPset_Annotation.Classes
2023-07-06 13:52:51 +05:00
Dion Moult 5a30d9fb9b Fix #3383. Disallow any extrusion angles >=90 which are invalid. 2023-07-06 17:34:22 +10:00
Dion Moult 3126db342d Test all shapes in the debug panel now also attempts 2D checks to prevent false positives. 2023-07-06 17:24:51 +10:00
Dion Moult c0db03438f Revert "More descriptive RuntimeError: Failed to process shape #3383"
This reverts commit 1542217117.
2023-07-06 17:02:57 +10:00
Andrej730 1542217117 More descriptive RuntimeError: Failed to process shape #3383
Example of the new error - https://i.imgur.com/0FMQw5V.png
2023-07-06 10:29:17 +05:00
Dion Moult 5b4010c67d Fix #3126. Implement mapping from IFC4X3 to IFC4X3_ADD1 and explicit version identifiers in ifcopenshell.file 2023-07-06 14:22:36 +10:00
Dion Moult c18d96f0e3 Fix #3386. 2023-07-06 13:46:09 +10:00
Dion Moult f9173700a6 Fix #3349. Allow inserting a filling into an uncontained building element. 2023-07-06 13:40:29 +10:00
Bruno Perdigão 7100a7fa8e fix #3063 2023-07-06 12:39:20 +10:00
Dion Moult 0078d691d3 Fix #3382. Minor regression in wall join refactor from d64c135 2023-07-06 11:45:12 +10:00
Sigma Dimensions fd626e37a0 fix #3385 - renaming grids with "F2" 2023-07-05 17:41:57 +01:00
Andrej730 e62d7ad9ef Moving editing roof and railing paths hotkey from Shift+E/Q to Tab 2023-07-05 18:40:35 +05:00
Andrej730 80eebc03d3 Fix #3263 2023-07-05 18:04:14 +05:00
Andrej730 62fea990d9 railing modifiers - fixing normals for frameless_panel #3277 2023-07-05 17:47:16 +05:00
Dion Moult 7ab01e6ac0 See #3359. Bump IfcOpenShell and enable HLR prefiltering optimisation for drawing generation 2023-07-05 22:18:53 +10:00
Andrej730 74a025b81c type selection error #3318
if you switch ifc_class then relating_type_id can become invalid and we wasn't taking it into account

Also fixed bunch of warning like `pyrna_enum_to_py: current value '17' matches no enum in 'BIMModelProperties', '', 'relating_type_id'`` when some ifc_class has N related types and then you switch to other ifc_class that has less than N related types.
2023-07-05 16:52:23 +05:00
Andrej730 ca8ed87f2a Revert "enable activating object type when trying to select type from an object ( whilst the type is hidden from view layer)"
This reverts commit 4bc987924d since there was some unintentional changes.

Now when you use select_type in object properties - ifc class and related type gets selected in the BIM Tool  - https://i.imgur.com/drIHYDz.png

#3318
2023-07-05 16:52:23 +05:00
Andrej730 9fbcb3900f ShapeBuilder.polyline - skip creating segments if they are not needed 2023-07-05 13:02:17 +05:00
Dion Moult 1533283822 Fix #3356. Include version info in status bar for debugging and new copy debug info button to help bugreporting. 2023-07-05 17:39:43 +10:00
Dion Moult 6beca6382f Fix #3256. Support file profiles made out of multiple representation items. 2023-07-05 14:38:48 +10:00
Dion Moult a1caef5b4f See #3334. Fix some get_pset implementation errors in 64516a1 2023-07-05 10:37:13 +10:00
OrionSehn 7da96b4951 Proposed fix to #3352 2023-07-05 09:56:42 +10:00
Andrej730 66c275f617 Sync ifc class in BIM Tool and Type Manager
It's seems it would be better for ux if they will be connected
2023-07-04 17:17:57 +05:00
Andrej730 52a8fcd86e add_representation - detect non consequtive curves and handle them
Basically if current mesh curve is non consequtive we handle it in similar way to b03c4da7b - convert it to curves using Blender and then save them to ifc.

The advantage is that it creates less curves in ifc because mesh method wasn't able to detect separate curves correctly if vertex order was messed up (usually it is) and it would create separate curve for each curve segment.

I've also replaced figuring vertex duplicates with `bmesh.ops.remove_doubles` manually with `bmesh.ops.remove_doubles` and it's now much faster. In some cases because of the manual calculations it was taking too long - in #3233 Ryan mentiones that it freezes blender, it wasn't really freezing Blender but to finish calculations it would take 10 minutes.
2023-07-04 16:43:42 +05:00
Dion Moult 3ba7ecdb95 See #3376. Bump updated build of IfcSverchok. 2023-07-04 20:17:01 +10:00
Dion Moult 36f7625cee Merge AWS Lambda installation documentation into official docs 2023-07-04 20:10:57 +10:00
Dion Moult ee82714423 Whoops 2023-07-04 19:47:49 +10:00
Dion Moult 281147367e Close #3377. Implement xyz coordinates for COBie object placement. 2023-07-04 19:45:11 +10:00
Dion Moult 0beea4255a Write IfcOpenShell documentation for geometry trees. 2023-07-04 18:04:15 +10:00
Andrej730 b03c4da7bf experimental support for spline curves representation #3233
there is still no full support but as a workaround I've added blender converting curves to meshes and back to curves so we could preserve the actual shapes

before - https://i.imgur.com/9F542Iy.png
after - https://i.imgur.com/t58RlkF.png
2023-07-04 10:57:22 +05:00
Dion Moult c0402687b5 See #3334. Optimise pset getting and add simple "broadphase" filtering for all facets for faster validation. 2023-07-04 15:50:44 +10:00
Dion Moult c6fa7161b6 Fix bug where IfcTester would pass derived attribute facets.
Now that IfcOpenShell supports derived attributes, we need to explicitly make sure the attribute facet only considers forward attributes.
2023-07-04 15:50:44 +10:00
Dion Moult 64516a173a See #3334. Fix bug where get_pset didn't handle property inheritance correctly. 2023-07-04 15:50:44 +10:00
c4rlosdias f507e3e8bd Add extra formats to output file 2023-07-04 12:42:59 +10:00
Andrej730 2f15d73b30 Prevent crash on attempt to link the current .blend file 2023-07-03 17:23:49 +05:00
Andrej730 53cc6f0bd6 Storing external styles paths as posix for cross platform compatibility
Also it was wrongly referring to .blend file instead of .ifc to generate relative paths.
2023-07-03 17:09:01 +05:00
Andrej730 7680652c28 disable default vebose mode for getting shader attribute to prevent spam 2023-07-03 16:56:47 +05:00
Andrej730 e8b1d00e6d adding default ifc objects added from shift-a menu to the current collection 2023-07-03 16:22:20 +05:00
Andrej730 6ec7e48423 Editiing arbitrary profile without enabling editing profile
It was a bit confusing UX since when you finish editing arbitrary profile path the data is already saved in ifc but you were still in editing profile properties
2023-07-03 15:54:51 +05:00
Andrej730 ccf09b962a Update type manager thumbnails on editing profiles
Example - https://imgur.com/a/IcV8IIR
2023-07-03 14:34:20 +05:00
Andrej730 485d41ae09 disable chaching sound running the bim tests
to save someone's ears (probably mine 😁) from running 10 chaching sounds per second
2023-07-03 12:43:26 +05:00
Dion Moult b67c5a0828 Fix #3366. Fix crash in IfcDiff if you are comparing a model with no geometry. 2023-07-03 17:28:38 +10:00
Andrej730 7b92057227 Fixed errors displaying IFC Connections for IfcRelConnectsElements
IfcRelConnectsElements type of connection wasn't handled properly resulting in errors.
Now you should be able to see connections for connected slabs, walls etc - https://i.imgur.com/LpFBKBC.png

Traceback:
Traceback (most recent call last):
  File "\blenderbim\bim\module\geometry\ui.py", line 102, in draw
    ConnectionsData.load()
  File "\blenderbim\bim\module\geometry\data.py", line 100, in load
    cls.data = {"connections": cls.connections()}
  File "\blenderbim\bim\module\geometry\data.py", line 134, in connections
    relating_element_connection_type = rel.RelatingConnectionType
  File "\blenderbim\libs\site\packages\ifcopenshell\entity_instance.py", line 171, in __getattr__
    raise AttributeError(
AttributeError: entity instance of type 'IFC4.IfcRelConnectsElements' has no attribute 'RelatingConnectionType'
2023-07-03 11:53:03 +05:00
Dion Moult a980ce1bfa Fix #3355. Reimplement graphical clash snapshots in BCF from IfcClash in the BlenderBIM Add-on. 2023-07-03 16:50:40 +10:00
Dion Moult 51b7f24a00 You can now unload BCF projects to load another one. 2023-07-03 16:50:40 +10:00
Dion Moult 136e1c3b3f Merge pull request #3364 from IfcOpenShell/interface-to-interact-with-Brickschema
Interface to interact with brickschema PR #1
2023-07-03 09:55:09 +10:00
Carlos Dias c094517a10 Update operator.py
missing output file format in arguments
2023-07-03 09:42:17 +10:00
Andrej730 b61a11d5b7 reworked detecting neighbour walls when placing new ones
hopefully it will be a bit more natural now - please give feeback

before - https://imgur.com/a/VYQt5ua
now - https://imgur.com/a/5LRAmrd
2023-07-02 19:59:18 +05:00
Andrej730 012da8fcb9 More responsive UI for IFC search 2023-07-02 15:52:24 +05:00
Andrej730 d64c1358d7 a bit of bim.join_wall refactor 2023-07-02 11:04:00 +05:00
Andrej730 0252701f7a Error editing roof profile #3357
it was assuming it's a slab and was giving errors trying to start editing slab profile

File "C:\Users\user\AppData\Roaming\Blender Foundation\Blender\3.6\scripts\addons\blenderbim\bim\module\model\slab.py", line 618, in _execute
if extrusion.Position:
AttributeError: 'NoneType' object has no attribute 'Position'
2023-07-02 10:44:01 +05:00
Bruno Postle 6fed9bf795 BlenderBIM Git documentation minor corrections 2023-07-01 14:35:32 +01:00
Bruno Postle b088a763d8 BlenderBIM document Git merge asymmetry (#3096) 2023-07-01 09:53:26 +01:00
Dion Moult 104fe56612 Whoops 2023-07-01 15:41:40 +10:00
Dion Moult 9e0760b8d2 Fix hppfcl segfault by bumping hppfcl and its dependencies. Remove support for py37. 2023-07-01 15:37:21 +10:00
Andrej730 f26986645a Added warning for a86456392 2023-06-30 18:19:23 +05:00
Andrej730 a86456392b improved relative path handling for textures #3365
now we're going to store all texture paths in .ifc as posix to keep them working if .ifc moved from one platform to another

previously it was saving all paths as absolute
2023-06-30 18:05:26 +05:00
Andrej730 61de0ad64c Calculating angle based on world position instead of camera pos #3362 2023-06-30 15:02:39 +05:00
Andrej730 243eee9ebf Added support for OCCLUSION textures for 3aff29dec 2023-06-30 14:44:06 +05:00
Andrej730 f23ac034c7 Shape builder - detect AdvancedSweptSolid
To avoid validation errors on creating wall mounted railings

    #143=IfcShapeRepresentation(#15,'Body','SweptSolid',(#116,#126,#129,#139,#142))
Rule IfcShapeRepresentation.CorrectItemsForType:
    IfcShapeRepresentationTypes(self.RepresentationType, self.Items)
Violated by:
    False
     +  where False = IfcShapeRepresentationTypes('SweptSolid', (#116=IfcSweptDiskSolid(#115,10.,$,$,$), #126=IfcExtrudedAreaSolid(#120,#124,#125,20.), #129=IfcSweptDiskSolid(#128,10.,$,$,$), #139=IfcExtrudedAreaSolid(#133,#137,#138,20.), #142=IfcSweptDiskSolid(#141,25.000000372529,$,$,$)))
     +    where 'SweptSolid' = #143=IfcShapeRepresentation(#15,'Body','SweptSolid',(#116,#126,#129,#139,#142)).RepresentationType
     +    and   (#116=IfcSweptDiskSolid(#115,10.,$,$,$), #126=IfcExtrudedAreaSolid(#120,#124,#125,20.), #129=IfcSweptDiskSolid(#128,10.,$,$,$), #139=IfcExtrudedAreaSolid(#133,#137,#138,20.), #142=IfcSweptDiskSolid(#141,25.000000372529,$,$,$)) = #143=IfcShapeRepresentation(#15,'Body','SweptSolid',(#116,#126,#129,#139,#142)).Items
2023-06-30 14:19:07 +05:00
Andrej730 363ede9801 set default position on creating window and door
to avoid validation errors like IfcProduct.PlacementForShapeRepresentation
2023-06-30 14:16:39 +05:00
Andrej730 18d26df24e Fixing ifc git poll errors if .ifc file is not yet saved
Errors:
Traceback (most recent call last):
  File "\Blender\3.6\scripts\addons\blenderbim\bim\module\ifcgit\operator.py", line 20, in poll
    if not os.path.isfile(path_ifc):
  File "\Blender\3.6\python\lib\genericpath.py", line 30, in isfile
    st = os.stat(path)
TypeError: stat: path should be string, bytes, os.PathLike or integer, not NoneType
Traceback (most recent call last):
  File "\Blender\3.6\scripts\addons\blenderbim\bim\module\ifcgit\operator.py", line 48, in poll
    if not os.path.isfile(path_ifc):
  File "\Blender\3.6\python\lib\genericpath.py", line 30, in isfile
    st = os.stat(path)
TypeError: stat: path should be string, bytes, os.PathLike or integer, not NoneType
2023-06-30 13:42:01 +05:00
Andrej730 0df62d5746 updated furniture library after f5031035c and a385938 2023-06-30 13:30:09 +05:00
Andrej730 f5031035c7 Shape builder - avoid duplicated indices in polylines
For example
IfcLineIndex((1,2,3,4,1))
instead of
IfcLineIndex((1,2,2,3,3,4,4,1))
2023-06-30 13:27:00 +05:00
Thomas Krijnen fc50bdd3a0 Fix openbot build 2023-06-30 09:32:10 +02:00
Kristoffer Andersen 3117d5b2e7 Update build.sh
Use only 1 cpu to reduce memory usage for conda builds
2023-06-30 09:27:15 +02:00
Riley Wong e6dc1d1acd Merge branch 'v0.7.0' into interface-to-interact-with-Brickschema 2023-06-30 00:05:20 -07:00
Trashman247 bd8afabce3 Resolve .git-ignore file conflict 2023-06-30 00:03:46 -07:00
Trashman247 01da8498f2 Move Brick.ttl ignore to main git-ignore file 2023-06-29 23:56:20 -07:00
Trashman247 a34b8d06ec Silence known rdflib_sqlalchemy TypeError warning 2023-06-29 23:42:00 -07:00
Massimo Fabbro 5764cfcbb9 Add regen button also under space tool 2023-06-30 14:34:21 +10:00
Dion Moult 1ad2465ad4 See #3283. Minor fix where IfcContext is abstract, so IfcProjectLibrary should be checked instead when loading filtered models. 2023-06-30 13:58:18 +10:00
Dion Moult f28e28abc0 Fix failing build 2023-06-30 11:59:17 +10:00
Dion Moult 97e321dd12 Bundle qhull for hppfcl on Linux too 2023-06-30 11:47:44 +10:00
Thomas Krijnen fc4b36e759 Fix openbot build 2023-06-29 22:04:18 +02:00
Thomas Krijnen 832cdce934 Fix openbot build 2023-06-29 22:03:29 +02:00
Andrej730 e45974df62 Support for door/window/stair modifiers in ifc2x3 2023-06-29 18:38:06 +05:00
Andrej730 bba118ed19 ShapeBuilder to use IfcPolyline of IfcIndexedPolyCurve to support ifc2x3
needed it to add suport for window/door modifiers in ifc2x3 in the next commit
2023-06-29 18:38:06 +05:00
Andrej730 709590401c fixing errors running tests if no tool selected in blender startup file
Error was: AttributeError: 'NoneType' object has no attribute 'idname'
2023-06-29 17:19:40 +05:00
Dion Moult ab74d1456b Bundle qhull for ifcclash on macosm1 2023-06-29 22:10:39 +10:00
Thomas Krijnen 4457da9042 Help swig not to trip over old occt handle definitions 2023-06-29 13:29:49 +02:00
Thomas Krijnen d501118eb4 Add --prefilter flag to draw.py 2023-06-29 13:29:49 +02:00
Thomas Krijnen 0a03d20133 SVG prefiltering elements based on large faces 2023-06-29 13:29:49 +02:00
Andrej730 f3f56c0891 simplified dev installation script for mac/linux 2023-06-29 16:13:37 +05:00
Andrej730 a689b31279 Fixed crash on Mac m1 on removing unused types (similar to 4d0be1972) 2023-06-29 15:34:52 +05:00
Andrej730 4d0be19720 Fix crash on Mac M1 on removing cost item
Crash happened because we were addressing `.id()` of already deleted `cost_item`.
Surprisingly for me on windows addressing `cost_item` was causing crash too but `cost_item.id()` worked fine.
2023-06-29 14:56:06 +05:00
Andrej730 fb7591caef avoid errors unregistering tools from tests 2023-06-29 13:43:08 +05:00
Dion Moult 8efaf2fbcb Attempt to bundle hppfcl for macosm1 2023-06-29 18:13:52 +10:00
Andrej730 d41068e132 Console warnings if drawing's assets not found #3325 2023-06-29 10:15:02 +05:00
Andrej730 4e65f143da unregistering spatial and structural tools on disabling addon 2023-06-28 16:54:43 +05:00
Andrej730 40656297a6 Fix for f88835b #3271
Now we do not rely on small offset to keep using inner curves to create linings, if some part of the lining is not present (which happens on mullions and transoms) we'll create U or L shape extrusion instead of rectangle with inner curve.

Long story short - this should keep ifc for windows valid.
2023-06-28 15:57:29 +05:00
Andrej730 f88835b87b Corrected L shapes in window lining after e89a9cd #3271
From this - https://i.imgur.com/hQuCcR3.png
To this - https://i.imgur.com/qZ2Lyfo.png

It's a bit hacky since we still use really small offsets to keep "The outer curve shall enclose all inner curves." rule for IfcArbitraryProfileDefWithVoids valid.
2023-06-28 15:56:38 +05:00
Andrej730 8f18db64cf Disable default stream mode for now to avoid errors #3354 2023-06-28 12:38:00 +05:00
Andrej730 fdd473234a Poll message for bim.file_unassociate/bim.file_associate 2023-06-28 10:45:53 +05:00
Andrej730 569f56f713 Fixed error on saving styles without textures after 3aff29dec 2023-06-28 10:39:57 +05:00
Andrej730 61607f9de2 Added IFC override operators to outliner/object context menus #3345
Outliner context menu - https://i.imgur.com/TkJKbLh.png
Object context menu - https://i.imgur.com/JXOEdYH.png
2023-06-28 10:32:33 +05:00
Dion Moult 40b34f6a33 Docs now point to the popular Search IFC Class website. 2023-06-28 13:30:36 +10:00
Dion Moult 89b0674b08 You can now exclude classes from streaming in ifcopenshell.stream 2023-06-28 12:41:50 +10:00
Trashman247 37e4f98857 Implement fully undo/redo with checks
While these actions are short in the backend--taking about 0.01 seconds to run--the Blender UI lags a lot leading to 2-3 second pauses because of the "refresh_brick_viewer" function being slow. This should be investigated.
2023-06-27 13:21:44 -07:00
Andrej730 e89a9cd01c adjust window offset on transoms/mullions for symmetry #3271
Before -  https://i.imgur.com/DbEzJ9H.png
After - https://i.imgur.com/0CERh86.png

Added it for modifier preview and ifc 2d and 3d repr but still need to work some things in 3d representation (L shape lining is not always detected correctly).
2023-06-27 19:30:49 +05:00
Dion Moult 20b63f530a New option to enable streaming IFC data into BBIM.
To test, go into add-on preferences and enable "Should Stream". Then load an IFC-SPF in advanced mode, and disable "Load Geometry" before loading (the geometry iterator doesn't work with streaming).
2023-06-27 21:07:37 +10:00
Dion Moult 183230b7f5 Naive implementation of file stream and Lark based IFC-SPF reader to handle huge IFCs. 2023-06-27 21:03:02 +10:00
Andrej730 7e2b40176c Fixing tests after 2dc9dc200 2023-06-27 15:29:05 +05:00
Andrej730 3aff29dec1 Support for import IfcSurfaceStyleWithTextures
Added UI for adding textures in BBIM shader graph generator

https://imgur.com/a/BI5pOZn
2023-06-27 14:30:30 +05:00
Trashman247 2ce32c6521 Change namespace selector to only show alias 2023-06-26 23:17:11 -07:00
Trashman247 9d84172f15 Add changeset versioning to Brick add/remove 2023-06-26 23:16:52 -07:00
Trashman247 8341931a62 Reformat commenting, remove prints, and remove BrickStore.project 2023-06-26 16:01:26 -07:00
Trashman247 81a4ff711c Implement fully serialize operator
- Turns out, you need the keyword "graph=" in graph_at() to actually select a graph of that name from the collection, otherwise it just returns the entire collection, so I changed that, which correctly isolates the project from the collection for serialization now.
- With this same change, I opted turn BrickStore.VersionedGraphCollection simply into BrickStore.graph and create a new BrickStore.get_project() to return the isolated graph. - This meant I should remove the reload_graph() function because I was actually just loading the entire collection into it still, and its functionality breaks when it isn't the entire collection
2023-06-26 13:59:28 -07:00
Dion Moult 2da86c170f See #3347. Hotfix latest IfcOpenShell SQLite features when making builds. 2023-06-26 22:14:59 +10:00
Dion Moult 41a176cb6d The BlenderBIM Add-on now supports loading IFCSQLite files! 2023-06-26 17:21:01 +10:00
Dion Moult 10e96239a1 You can now toggle loading geometry in advanced mode when loading a project 2023-06-26 17:13:34 +10:00
Dion Moult 897dfb1e33 ifcopenshell.sqlite wrapper now handles JSON fields by default, which is significantly more performant
f attributes holding entity lists are expanded in SQL, this means that you end up with significantly more rows. Also, you cannot have the ifc_id marked as a UNIQUE field / benefit from unique primary key index. So leaving the fields as JSON is actually significantly more efficient (almost 10X faster to load a model into BBIM).
2023-06-26 17:08:50 +10:00
Dion Moult c1d972f7bf Ifc2Sql patch now can store inverses for efficient queries. 2023-06-26 17:08:50 +10:00
Trashman247 d9196882dc Implement early testing for serialize operator (not functional)
All the right code seems to be in place, but it seems the package won't go through with the serialize function because of read/write permissions (ERRNO 13)
2023-06-25 23:56:15 -07:00
Trashman247 580f8f6218 Change graph naming to all caps 2023-06-25 23:21:27 -07:00
Trashman247 e177f5f051 Add Brick.ttl to .gitignore 2023-06-25 23:20:56 -07:00
Andrej730 1a5c501af0 Fixed typo in e3a7f7627 2023-06-26 10:48:50 +05:00
Andrej730 12c10f98a3 Fixing bug after 28cc168
Was causing some tests to fail
2023-06-26 10:33:07 +05:00
Andrej730 e3a7f76276 Avoid ui errors after 2dc9dc200 2023-06-24 10:19:35 +05:00
Trashman247 046ce2b8e7 Update BrickStore.purge()
I wanted to just have a BrickStore.clear() which would so this:

        BrickStore.VersionedGraphCollection = None
        BrickStore.graph = None
        BrickStore.path = None

(aka not also set BrickStore.schema = None, since it should theoretically just load in the same path anyway) but for some reason Blender crashes when clearing a project and loading one again this way.
2023-06-23 16:55:34 -07:00
Andrej730 6dab4501ad Unhide in render for not excluded objects #3338
Objects were hidden in render but never unhidden if they were no more in drawing's .Exlcude
2023-06-23 18:19:33 +05:00
Andrej730 2dc9dc2000 Generating IFC compatible shader graphs
Added new panel. in shader graph N-panel that allows you to create fully GLTF and IFC compatible shader graphs by just tweaking parameters from the panel. Note that it's still saved to IFC only when you hit "Save Current Style", before that it's still just stored in blender.

https://imgur.com/a/vmkoLPH

The premise is that if you want to have non-external style you'd be able to create it entirely from this panel - so there will no need to guess or to remember which parameters from the graph are supported by IFC and which are not.

It's still work in progress, still need to polish it, add support for more reflectance methods and add suport for textures (need to be able to import/export it with .ifc and set them from this panel).
2023-06-23 18:04:32 +05:00
Trashman247 bcf5f07718 Rework NewBrick and LoadBrick in test_brick.py to fit with new VGC format
- VGC means VersionedGraphCollection.
- BrickStore.schema is now a filepath, so make that the case in the testing also.
2023-06-19 21:59:17 -07:00
Trashman247 a3572f3372 Implement VersionedGraphCollection to Brick module
- Reworked `load_brick_file` and `new_brick_file` to fit under the VersionedGraphCollection implementation. (Other methods may have broken).
- Added `undo_brick` and `redo_brick`.
- Kept BrickStore.graph the same as to generally still work with the rest of the code. This was done by parsing the VersionedGraphCollection with the new `reload_brick_graph` method.
2023-06-19 21:54:28 -07:00
195 changed files with 56103 additions and 20975 deletions
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pyver: [py37, py39, py310]
pyver: [py39, py310]
config:
- {
name: "Windows Build",
+4 -1
View File
@@ -94,4 +94,7 @@ src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
# apple
.DS_Store
.DS_Store
# Brickschema
src/blenderbim/blenderbim/bim/schema/Brick.ttl
+2 -1
View File
@@ -1,8 +1,9 @@
IfcOpenShell
============
<p align="center">
<img src="https://github.com/IfcOpenShell/IfcOpenShell/assets/79010126/9e540403-c79f-446a-88c6-7c1ebc6ba864">
<img src="https://github.com/IfcOpenShell/IfcOpenShell/assets/88302/34901387-e2dd-4a0c-8e38-9ffc32a66cde">
</p>
IfcOpenShell is an open source ([LGPL]) software library for working with Industry Foundation Classes ([IFC]). Complete
-48
View File
@@ -1,48 +0,0 @@
Dockerized AWS Lambda Function with Python and IfcOpenShell
===========================================================
This guide provides a Dockerfile and sample code to help you run an AWS Lambda function written in Python and utilizing IfcOpenShell library.
Prerequisites
-------------
Make sure you have Docker installed on your machine. You can download Docker from the official website: [Docker Instalation](https://docs.docker.com/engine/install/)
Getting Started
---------------
1. **Clone this repository to your local machine:**
2. **Customize the Lambda function code:**
- Replace the sample Lambda function code in the `example_handler` directory with your own code.
- Update the import path in the Dockerfile's `CMD` instruction to match your Lambda function's handler function.
3. **Install the required Python packages:**
- Edit the `requirements.txt` file and add any additional dependencies required by your Lambda function.
4. **Build the Docker image:**
```shell
$ docker build -t lambda-ifcopenshell .
```
5. **Run the Docker container:**
```shell
$ docker run lambda-ifcopenshell
```
6. **Test lambda locally**
Follow the steps in this [guide](https://docs.aws.amazon.com/lambda/latest/dg/images-test.html)
7. **Deploy to lambda**
This is beyond the scope of this example. Please refer to AWS documentation. Some tools that could be useful are - AWS CloudFormaton, AWS CDK, pulumi or terraform
---------
> Note: This is a superficial guide, in order to make this work for your usecase you will need a good understanding of other AWS services like SQS, S3, API Gateway etc.
For more information on building lambda containers refer to this [guide](https://docs.aws.amazon.com/lambda/latest/dg/images-create.html).
+1 -1
View File
@@ -41,4 +41,4 @@ cmake -G Ninja \
ninja
ninja install -j 2
ninja install -j 1
+2
View File
@@ -275,6 +275,7 @@ class TopicHandler:
"""
new_viewpoint = VisualizationInfoHandler.create_new(element, self._xml_handler)
self.add_visinfo_handler(new_viewpoint)
return new_viewpoint
def add_viewpoint_from_point_and_guids(self, position: NDArray[np.float_], *guids: str) -> None:
"""Add a viewpoint pointing at an XYZ point in space
@@ -287,6 +288,7 @@ class TopicHandler:
position, *guids, xml_handler=self._xml_handler
)
self.add_visinfo_handler(vi_handler)
return vi_handler
def add_visinfo_handler(self, new_viewpoint: VisualizationInfoHandler) -> None:
self.viewpoints[new_viewpoint.guid + ".bcfv"] = new_viewpoint
+104 -74
View File
@@ -36,12 +36,9 @@ endif
endif
VERSION:=`date '+%y%m%d'`
PYVERSION:=py37
LAST_COMMIT_HASH:=$(shell git rev-parse HEAD)
PYVERSION:=py310
ifeq ($(PYVERSION), py37)
PYLIBDIR:=python3.7
PYNUMBER:=37
endif
ifeq ($(PYVERSION), py39)
PYLIBDIR:=python3.9
PYNUMBER:=39
@@ -52,123 +49,100 @@ PYNUMBER:=310
endif
ifeq ($(PLATFORM), linux)
ifeq ($(PYVERSION), py37)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/linux-64/hpp-fcl-1.7.5-py37h5f1835d_0.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/linux-64/eigenpy-2.6.5-py37h95e2c48_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py37h0379df6_3.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/30/c0/d0526314971fc661b083ab135747dc68446a3022686da8c16d25fcf6ef07/lxml-4.6.3-cp37-cp37m-manylinux2014_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/1d/a4/931d0780f31f3ea8c4f9ef6464a2825137c5241e6707a5fb03bef760a7eb/shapely-2.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/ed/d5/c2e84e1e36ab8ebea033921d5886a056c77e18bab5ab1051fcc22de2e8a2/Pillow-9.2.0-cp37-cp37m-manylinux_2_28_x86_64.whl
endif
ifeq ($(PYVERSION), py39)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/linux-64/hpp-fcl-1.7.5-py39hbcdfc36_0.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/linux-64/eigenpy-2.6.5-py39h5aed9d1_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py39h5472131_3.tar.bz2
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.3.4/download/linux-64/hpp-fcl-2.3.4-py39h40a70d0_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.1.0/download/linux-64/eigenpy-3.1.0-py39hdfdd6bb_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/linux-64/boost-1.78.0-py39h7c9e3ff_4.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/linux-64/qhull-2020.2-h4bd325d_2.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/19/d9/a69c6aff5673554df48120565a14a50eaa41d29ae03b02faa0b023666318/lxml-4.6.3-cp39-cp39-manylinux2014_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/2d/f2/8ec281d357e8bb7d08dc8d727f0e4c8ef3dae7d3fa75c69c8e452bb82d50/shapely-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/01/61/3ff85fb4bb596ce3d223c8fcf93c8df5c12bc8899dfb4fb3cb1c5b20dd5f/Pillow-9.2.0-cp39-cp39-manylinux_2_28_x86_64.whl
endif
ifeq ($(PYVERSION), py310)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/linux-64/hpp-fcl-1.8.0-py310hdaf7e41_1.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.11/download/linux-64/eigenpy-2.6.11-py310hf3e5c9c_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py310h7c3ba0c_5.tar.bz2
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.3.4/download/linux-64/hpp-fcl-2.3.4-py310h995690b_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.1.0/download/linux-64/eigenpy-3.1.0-py310hf02b7e0_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/linux-64/boost-1.78.0-py310hc4a4660_4.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/linux-64/qhull-2020.2-h4bd325d_2.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/25/1e/19b46d8e8881fe0df2e20945d51919eeb1817836d62a90efa8506530e45c/lxml-4.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/a8/a5/403728b5614b28083f6424dfdefec5fcf58068495fb03bb08532671c642f/shapely-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/f6/51/320986ebd6d46a0e95c2240468ced73153b691ce07617078bcdf30c609ec/Pillow-9.2.0-cp310-cp310-manylinux_2_28_x86_64.whl
endif
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/linux-64/assimp-5.0.1-hedfc422_6.tar.bz2
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/linux-64/octomap-1.9.7-h4bd325d_0.tar.bz2
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.8/download/linux-64/octomap-1.9.8-h924138e_0.tar.bz2
BOOSTCPP_URL:=https://anaconda.org/conda-forge/boost-cpp/1.78.0/download/linux-64/boost-cpp-1.78.0-h6582d0a_3.conda
ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/linux-64/zlib-1.2.11-h516909a_1010.tar.bz2
endif
ifeq ($(PLATFORM), macos)
ifeq ($(PYVERSION), py37)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-64/hpp-fcl-1.7.5-py37h2d7f23a_0.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/osx-64/eigenpy-2.6.5-py37h0695097_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py37hd79e0ac_3.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/1e/3e/f0abc15d5dac50939bccc589aae336d5ead4c72e7ad1039a2e0f3630ea92/lxml-4.6.3-cp37-cp37m-macosx_10_9_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/e6/7d/4923f27c340339e1c896c77cafc8ed672c8d381a025bbab6c6ddcba27e8f/shapely-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/88/49/c26fc3b5b0e82bdc9d8751d6b939da29327b0d98f7c3b95a575cbfed2743/Pillow-9.2.0-cp37-cp37m-macosx_10_10_x86_64.whl
endif
ifeq ($(PYVERSION), py39)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-64/hpp-fcl-1.7.5-py39h1e32b98_0.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/osx-64/eigenpy-2.6.5-py39h5405915_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py39ha641261_3.tar.bz2
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.3.4/download/osx-64/hpp-fcl-2.3.4-py39hd85b194_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.1.0/download/osx-64/eigenpy-3.1.0-py39hc4d6e28_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-64/boost-1.78.0-py39h953a6b8_4.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/osx-64/qhull-2020.2-h940c156_2.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/b8/74/a71f7ad72e8db54ce899efab84507b801660750cbbfa6a39e6717557d36a/lxml-4.6.3-cp39-cp39-macosx_10_9_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/36/a4/7e542a209f862f967d7cb8e939eff155f4294a27d17e16441fb8bdd51a2c/shapely-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/88/7a/ddfe28b485b623361457d4783007c1f9ba83a87f93e7fec32f64793efb6c/Pillow-9.2.0-cp39-cp39-macosx_10_10_x86_64.whl
endif
ifeq ($(PYVERSION), py310)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/osx-64/hpp-fcl-1.8.0-py310h651ac30_1.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.11/download/osx-64/eigenpy-2.6.11-py310hc03097c_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py310h509978a_5.tar.bz2
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.3.4/download/osx-64/hpp-fcl-2.3.4-py310h1db6f5f_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.1.0/download/osx-64/eigenpy-3.1.0-py310h43da829_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-64/boost-1.78.0-py310h3e792ce_4.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/osx-64/qhull-2020.2-h940c156_2.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/a1/44/17b7dac7a18807d30e2fe10c3328c152808f5464565e230bfd0e77f178c6/lxml-4.8.0-cp310-cp310-macosx_10_15_x86_64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/1f/2a/dc3353c2431cf53e8d04bb8fba27e584410ca3435c9c85f76d71bf0c0e80/shapely-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/d8/60/b13c00d403f34110e96c1b5c0afa73ce461efe3fe960c3a7e3e7fe190d82/Pillow-9.2.0-cp310-cp310-macosx_10_10_x86_64.whl
endif
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/osx-64/assimp-5.0.1-h1224e73_6.tar.bz2
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/osx-64/octomap-1.9.7-h940c156_0.tar.bz2
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.8/download/osx-64/octomap-1.9.8-hb8565cd_0.tar.bz2
BOOSTCPP_URL:=https://anaconda.org/conda-forge/boost-cpp/1.78.0/download/osx-64/boost-cpp-1.78.0-hf5ba120_3.conda
ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/osx-64/zlib-1.2.11-h7795811_1010.tar.bz2
endif
ifeq ($(PLATFORM), macosm1)
ifeq ($(PYVERSION), py37)
# Warning: py37 not supported on Apple M1
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-64/hpp-fcl-1.7.5-py37h2d7f23a_0.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/osx-64/eigenpy-2.6.5-py37h0695097_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py37hd79e0ac_3.tar.bz2
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-64/lxml-4.9.1-py37h994c40b_0.tar.bz2
SHAPELY_URL:=https://files.pythonhosted.org/packages/e6/7d/4923f27c340339e1c896c77cafc8ed672c8d381a025bbab6c6ddcba27e8f/shapely-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/aa/bc/21097cd891dd2fa02f2b3d767e02e883e026482e59d29975d1bc30024aa3/Pillow-9.2.0-cp39-cp39-macosx_11_0_arm64.whl
endif
ifeq ($(PYVERSION), py39)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-arm64/hpp-fcl-1.7.5-py39ha69f3c1_2.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.7/download/osx-arm64/eigenpy-2.6.7-py39ha69f3c1_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py39h2550fe3_0.tar.bz2
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.3.4/download/osx-arm64/hpp-fcl-2.3.4-py39hc34188a_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.1.0/download/osx-arm64/eigenpy-3.1.0-py39h13cfc01_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py39h99de9ae_4.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/osx-arm64/qhull-2020.2-hc021e02_2.tar.bz2
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py39h9eb174b_0.tar.bz2
SHAPELY_URL:=https://files.pythonhosted.org/packages/ea/aa/45fbd031edf3149cb767d8b9f9db45d5faf0324d743c6b8fb0298cc022d0/shapely-2.0.1-cp39-cp39-macosx_11_0_arm64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/aa/bc/21097cd891dd2fa02f2b3d767e02e883e026482e59d29975d1bc30024aa3/Pillow-9.2.0-cp39-cp39-macosx_11_0_arm64.whl
endif
ifeq ($(PYVERSION), py310)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/osx-arm64/hpp-fcl-1.8.0-py310h5699539_2.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.9/download/osx-arm64/eigenpy-2.6.9-py310habfc766_1.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py310h4566fd1_0.tar.bz2
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.3.4/download/osx-arm64/hpp-fcl-2.3.4-py310h46fc4cd_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.1.0/download/osx-arm64/eigenpy-3.1.0-py310ha2643af_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py310h629746b_4.tar.bz2
QHULL_URL:=https://anaconda.org/conda-forge/qhull/2020.2/download/osx-arm64/qhull-2020.2-hc021e02_2.tar.bz2
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py310h02f21da_0.tar.bz2
SHAPELY_URL:=https://files.pythonhosted.org/packages/ec/41/d59208743e737184e1b403e95a937aebb022b8459e99efbcd5208fc8be46/shapely-2.0.1-cp310-cp310-macosx_11_0_arm64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/0c/5f/117b653cad585f3aedfe0de996c292e67d4b020ed77f652e5a6c8c24f908/Pillow-9.2.0-cp310-cp310-macosx_11_0_arm64.whl
endif
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/osx-arm64/assimp-5.0.1-h0f81e16_7.tar.bz2
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/osx-arm64/octomap-1.9.7-hc021e02_0.tar.bz2
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.8/download/osx-arm64/octomap-1.9.8-hffc8910_0.tar.bz2
BOOSTCPP_URL:=https://anaconda.org/conda-forge/boost-cpp/1.78.0/download/osx-arm64/boost-cpp-1.78.0-h9ed8d21_3.conda
ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/osx-arm64/zlib-1.2.11-h90dfc92_1014.tar.bz2
endif
ifeq ($(PLATFORM), win)
ifeq ($(PYVERSION), py37)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/win-64/hpp-fcl-1.7.5-py37h839d6b1_0.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/win-64/eigenpy-2.6.5-py37h2c32e34_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/win-64/boost-1.74.0-py37h3b38789_3.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/9e/5e/171ee9d40a600f565fe691ec5bf7596247ec62cfb2edc00c91afe8ea837b/lxml-4.6.3-cp37-cp37m-win_amd64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/e2/87/b8b8d8d57b429b01aa56b7d723075c09f33c988b8091bb6f790c83436909/shapely-2.0.1-cp37-cp37m-win_amd64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/69/f5/9e802159d78b2eaf26bf1f8b94648605993f5ca7247ac8870f065063fc40/Pillow-9.2.0-cp37-cp37m-win_amd64.whl
endif
ifeq ($(PYVERSION), py39)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/win-64/hpp-fcl-1.7.5-py39h2e7c763_0.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/win-64/eigenpy-2.6.5-py39h3ce40e6_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/win-64/boost-1.74.0-py39hefe7e4c_3.tar.bz2
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.3.4/download/win-64/hpp-fcl-2.3.4-py39h39b25cb_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.1.0/download/win-64/eigenpy-3.1.0-py39hb6c915b_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/win-64/boost-1.78.0-py39hea4d8d1_4.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/72/d4/426ecb8849c47c3e370c87aa0ac05d85768df917ffea27fcd6686a5e6495/lxml-4.6.3-cp39-cp39-win_amd64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/a7/ae/eab645c4075093584b7a65ab71cb8ff4563a015bd935c9b55dba14b2c1eb/shapely-2.0.1-cp39-cp39-win_amd64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/19/3f/b4d4bcf05dbcbe07f2e9613a8f4180c297395e73a91d8ad22c32c6624f8c/Pillow-9.2.0-cp39-cp39-win_amd64.whl
endif
ifeq ($(PYVERSION), py310)
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/win-64/hpp-fcl-1.8.0-py310hc5a3c62_1.tar.bz2
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.11/download/win-64/eigenpy-2.6.11-py310hbd43d28_0.tar.bz2
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/win-64/boost-1.74.0-py310hc781a3c_5.tar.bz2
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/2.3.4/download/win-64/hpp-fcl-2.3.4-py310heb59267_0.conda
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/3.1.0/download/win-64/eigenpy-3.1.0-py310hd146abe_0.conda
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/win-64/boost-1.78.0-py310h220cb41_4.tar.bz2
LXML_URL:=https://files.pythonhosted.org/packages/f6/71/65c80a4caa1617a4c6e8fe1500cebb179db96232e2f623bfe6a1f4294e39/lxml-4.8.0-cp310-cp310-win_amd64.whl
SHAPELY_URL:=https://files.pythonhosted.org/packages/81/8a/7ac076a86b2632f1872284c5e60ed5f2fc26094875a85b35d9fa17b52504/shapely-2.0.1-cp310-cp310-win_amd64.whl
PILLOW_URL:=https://files.pythonhosted.org/packages/02/55/67a3c17b9e7d972ed8c246f104da99ca4f3ea42fba566697e479011b84b6/Pillow-9.2.0-cp310-cp310-win_amd64.whl
endif
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/win-64/assimp-5.0.1-hc2aa0de_6.tar.bz2
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/win-64/octomap-1.9.7-h5362a0b_0.tar.bz2
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.8/download/win-64/octomap-1.9.8-h91493d7_0.tar.bz2
BOOSTCPP_URL:=https://anaconda.org/conda-forge/boost-cpp/1.78.0/download/win-64/boost-cpp-1.78.0-h9f4b32c_3.conda
ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/win-64/zlib-1.2.11-h62dcd97_1010.tar.bz2
endif
@@ -190,7 +164,7 @@ endif
cp -r blenderbim/* dist/blenderbim/
# Provides IfcOpenShell Python functionality
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-8f41ae0-$(PLATFORM)64.zip
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-fc50bdd-$(PLATFORM)64.zip
cd dist/working && unzip ifcopenshell-python*
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
@@ -206,9 +180,7 @@ endif
# IfcOpenBot sometimes lags behind, so we hotfix the Python utilities
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/util/* dist/blenderbim/libs/site/packages/ifcopenshell/util/
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/api/* dist/blenderbim/libs/site/packages/ifcopenshell/api/
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/entity_instance.py dist/blenderbim/libs/site/packages/ifcopenshell/
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/file.py dist/blenderbim/libs/site/packages/ifcopenshell/
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/validate.py dist/blenderbim/libs/site/packages/ifcopenshell/
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/*.py dist/blenderbim/libs/site/packages/ifcopenshell/
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/express/rule_executor.py dist/blenderbim/libs/site/packages/ifcopenshell/express/
# Provides bcf functionality
cp -r dist/working/IfcOpenShell-0.7.0/src/bcf/src/bcf dist/blenderbim/libs/site/packages/
@@ -416,7 +388,8 @@ endif
# Required by IFCClash
mkdir dist/working
cd dist/working && wget $(HPPFCL_URL)
cd dist/working && tar -xf hpp-fcl*
cd dist/working && unzip hpp-fcl*
cd dist/working && tar -I zstd -xvf pkg*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/$(PYLIBDIR)/site-packages/hppfcl dist/blenderbim/libs/site/packages/
cp -r dist/working/lib/*.so* dist/blenderbim/libs/
@@ -425,6 +398,10 @@ ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/$(PYLIBDIR)/site-packages/hppfcl dist/blenderbim/libs/site/packages/
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/$(PYLIBDIR)/site-packages/hppfcl dist/blenderbim/libs/site/packages/
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Lib/site-packages/hppfcl dist/blenderbim/libs/site/packages/
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
@@ -441,21 +418,45 @@ endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
rm -rf dist/working
# Required by hpp-fcl
# Required by hpp-fcl except on Windows
ifneq ($(PLATFORM), win)
mkdir dist/working
cd dist/working && wget $(EIGENPY_URL)
cd dist/working && tar -xf eigenpy*
cd dist/working && wget $(QHULL_URL)
cd dist/working && tar -xf qhull*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/*.so* dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
rm -rf dist/working
endif
# Required by hpp-fcl
mkdir dist/working
cd dist/working && wget $(EIGENPY_URL)
cd dist/working && unzip eigenpy*
cd dist/working && tar -I zstd -xvf pkg*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/*.so* dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
@@ -471,6 +472,9 @@ endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
# Uh, do nothing, apparently? No DLLs are shipped.
endif
@@ -486,11 +490,33 @@ endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
rm -rf dist/working
# Required by hpp-fcl
mkdir dist/working
cd dist/working && wget $(BOOSTCPP_URL)
cd dist/working && unzip boost-cpp*
cd dist/working && tar -I zstd -xvf pkg*
ifeq ($(PLATFORM), linux)
cp -r dist/working/lib/libboost_serialization.so* dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/libboost_serialization.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/libboost_serialization.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/boost_serialization.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
rm -rf dist/working
# Required by hpp-fcl
mkdir dist/working
cd dist/working && wget $(ZLIB_URL)
@@ -501,6 +527,9 @@ endif
ifeq ($(PLATFORM), macos)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), macosm1)
cp -r dist/working/lib/*.dylib dist/blenderbim/libs/
endif
ifeq ($(PLATFORM), win)
cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/
endif
@@ -647,6 +676,7 @@ endif
rm -rf dist/blenderbim/libs/site/packages/numpy.libs
cd dist/blenderbim && $(SED) "s/999999/$(VERSION)/" __init__.py
cd dist/blenderbim/bim && $(SED) "s/8888888/$(LAST_COMMIT_HASH)/" __init__.py
cd dist && zip -r blenderbim-$(VERSION)-$(PYVERSION)-$(PLATFORM).zip ./*
rm -rf dist/blenderbim
+76 -9
View File
@@ -16,11 +16,16 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
from pathlib import Path
import bpy
import bpy.utils.previews
import blenderbim
import importlib
from . import handler, ui, prop, operator, helper
cwd = os.path.dirname(os.path.realpath(__file__))
modules = {
"project": None,
"search": None,
@@ -89,6 +94,7 @@ classes = [
operator.ConfigureVisibility,
operator.OpenUpstream,
operator.OpenUri,
operator.SwitchTab,
operator.ReloadIfcFile,
operator.RemoveIfcFile,
operator.SelectDataDir,
@@ -106,6 +112,7 @@ classes = [
prop.ObjProperty,
prop.Attribute,
prop.ModuleVisibility,
prop.BIMAreaProperties,
prop.BIMProperties,
prop.IfcParameter,
prop.PsetQto,
@@ -119,6 +126,7 @@ classes = [
ui.BIM_UL_topics,
ui.BIM_ADDON_preferences,
# Scene panel groups
ui.BIM_PT_root,
ui.BIM_PT_project_info,
ui.BIM_PT_project_setup,
ui.BIM_PT_collaboration,
@@ -140,25 +148,34 @@ classes = [
for mod in modules.values():
classes.extend(mod.classes)
addon_keymaps = []
icons = None
is_registering = False
last_commit_hash = "8888888"
def on_register(scene):
handler.setDefaultProperties(scene)
global is_registering
if is_registering:
return
is_registering = True
handler.load_post(scene)
if not bpy.app.background:
bpy.app.handlers.depsgraph_update_post.remove(on_register)
is_registering = False
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.app.handlers.depsgraph_update_post.append(on_register)
bpy.app.handlers.undo_pre.append(handler.undo_pre)
bpy.app.handlers.undo_post.append(handler.undo_post)
bpy.app.handlers.redo_pre.append(handler.redo_pre)
bpy.app.handlers.redo_post.append(handler.redo_post)
bpy.app.handlers.load_post.append(handler.setDefaultProperties)
bpy.app.handlers.load_post.append(handler.load_post)
bpy.app.handlers.load_post.append(handler.loadIfcStore)
bpy.app.handlers.save_post.append(handler.ensureIfcExported)
bpy.types.Scene.BIMProperties = bpy.props.PointerProperty(type=prop.BIMProperties)
bpy.types.Screen.BIMAreaProperties = bpy.props.CollectionProperty(type=prop.BIMAreaProperties)
bpy.types.Collection.BIMCollectionProperties = bpy.props.PointerProperty(type=prop.BIMCollectionProperties)
bpy.types.Object.BIMObjectProperties = bpy.props.PointerProperty(type=prop.BIMObjectProperties)
bpy.types.Material.BIMObjectProperties = bpy.props.PointerProperty(type=prop.BIMObjectProperties)
@@ -167,24 +184,53 @@ def register():
bpy.types.Curve.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
bpy.types.Camera.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
bpy.types.PointLight.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
bpy.types.SCENE_PT_unit.append(ui.ifc_units)
if hasattr(bpy.types, "UI_MT_button_context_menu"):
bpy.types.UI_MT_button_context_menu.append(ui.draw_custom_context_menu)
bpy.types.STATUSBAR_HT_header.append(ui.draw_statusbar)
for mod in modules.values():
mod.register()
wm = bpy.context.window_manager
if wm.keyconfigs.addon:
km = wm.keyconfigs.addon.keymaps.new(name='Window', space_type='EMPTY')
kmi = km.keymap_items.new('bim.switch_tab', 'TAB', 'PRESS', ctrl=True)
addon_keymaps.append((km, kmi))
global icons
icons_dir = os.path.join(cwd, "data", "icons")
icon_preview = bpy.utils.previews.new()
for filename in os.listdir(icons_dir):
if filename.endswith(".png"):
icon_name = os.path.splitext(filename)[0]
icon_path = os.path.join(icons_dir, filename)
icon_preview.load(icon_name, icon_path, "IMAGE")
icons = icon_preview
global last_commit_hash
try:
import git
path = Path(__file__).resolve().parent
repo = git.Repo(str(path), search_parent_directories=True)
last_commit_hash = repo.head.object.hexsha
except:
pass
def unregister():
global icons
bpy.utils.previews.remove(icons)
for cls in reversed(classes):
if getattr(cls, "is_registered", None) is None:
bpy.utils.unregister_class(cls)
elif cls.is_registered:
bpy.utils.unregister_class(cls)
bpy.app.handlers.load_post.remove(handler.setDefaultProperties)
bpy.app.handlers.load_post.remove(handler.load_post)
bpy.app.handlers.load_post.remove(handler.loadIfcStore)
bpy.app.handlers.save_post.remove(handler.ensureIfcExported)
del bpy.types.Scene.BIMProperties
del bpy.types.Collection.BIMCollectionProperties
del bpy.types.Object.BIMObjectProperties
@@ -194,9 +240,30 @@ def unregister():
del bpy.types.Curve.BIMMeshProperties
del bpy.types.Camera.BIMMeshProperties
del bpy.types.PointLight.BIMMeshProperties
bpy.types.SCENE_PT_unit.remove(ui.ifc_units)
if hasattr(bpy.types, "UI_MT_button_context_menu"):
bpy.types.UI_MT_button_context_menu.remove(ui.draw_custom_context_menu)
bpy.types.STATUSBAR_HT_header.remove(ui.draw_statusbar)
for mod in reversed(list(modules.values())):
mod.unregister()
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
if kc:
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
for panel in [
"SCENE_PT_scene",
"SCENE_PT_unit",
"SCENE_PT_physics",
"SCENE_PT_rigid_body_world",
"SCENE_PT_audio",
"SCENE_PT_keying_sets",
"SCENE_PT_custom_props",
]:
try:
bpy.utils.unregister_class(getattr(handler, f"Override_{panel}"))
except:
pass
@@ -75,7 +75,7 @@
<marker id="stair-marker-end" markerHeight="42" markerWidth="21" orient="auto" refX="21" refY="21">
<path d="M 0 0 L 21 21 L 0 42" class="annotation stair" style="stroke-width:2; marker-start: none; marker-end: none;" />
</marker>
<marker id="plan-level-marker" markerHeight="30" markerWidth="30" refX="15" refY="15">
<marker id="plan-level-marker" markerHeight="30" markerWidth="30" refX="15" refY="15" orient="auto">
<g>
<path d="M 15 0 L 15 30" class="annotation" style="stroke-width:1;" />
<path d="M 0 15 L 30 15" class="annotation" style="stroke-width:1;" />
@@ -84,7 +84,7 @@
<path d="M 15 15 L 22.5 15 A 7.5 7.5 0 0 1 15 22.5 Z" fill="black"/>
</g>
</marker>
<marker id="section-level-marker" markerHeight="11" markerWidth="90" refX="20" refY="10">
<marker id="section-level-marker" markerHeight="11" markerWidth="90" refX="20" refY="10" orient="auto-start-reverse">
<g>
<path d="M 0 1 L 20 1 L 10 11 Z" fill="black" />
<path d="M 0 1 L 90 1" class="annotation" style="stroke-width:1;" />

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

@@ -389,7 +389,7 @@
"scene.display.shading.studiolight_background_blur": 0.0,
"scene.display.shading.studiolight_intensity": 0.0,
"scene.display.shading.studiolight_rotate_z": 0.0,
"scene.display.shading.type": "SOLID",
"scene.display.shading.type": "RENDERED",
"scene.display.shading.use_compositor": "DISABLED",
"scene.display.shading.use_dof": false,
"scene.display.shading.use_scene_lights": false,
@@ -640,7 +640,7 @@
"scene.display.shading.studiolight_background_blur": 0.0,
"scene.display.shading.studiolight_intensity": 0.0,
"scene.display.shading.studiolight_rotate_z": 0.0,
"scene.display.shading.type": "SOLID",
"scene.display.shading.type": "RENDERED",
"scene.display.shading.use_compositor": "DISABLED",
"scene.display.shading.use_dof": false,
"scene.display.shading.use_scene_lights": false,
@@ -752,4 +752,4 @@
"space.overlay.xray_alpha_bone": 0.0
}
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
Name,foo
TransomThickness,500
1 Name foo
2 TransomThickness 500
Binary file not shown.
+119 -24
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
import bpy
import json
import addon_utils
@@ -30,6 +31,7 @@ from mathutils import Vector
from math import cos, degrees
cwd = os.path.dirname(os.path.realpath(__file__))
global_subscription_owner = object()
@@ -52,10 +54,12 @@ def mode_callback(obj, data):
def name_callback(obj, data):
# TODO Do we still need this, now that we are monitoring the undo redo objects?
try:
obj.name
except:
# The object is invalid but somehow still has a callback. Clear all
# msgbus subscriptions to prevent useless further triggers.
bpy.msgbus.clear_by_owner(obj)
return # In case the object RNA is gone during an undo / redo operation
# Blender names are up to 63 UTF-8 bytes
if len(bytes(obj.name, "utf-8")) >= 63:
@@ -72,6 +76,9 @@ def name_callback(obj, data):
if not obj.BIMObjectProperties.ifc_definition_id or "/" not in obj.name:
return
element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
if element.is_a("IfcGridAxis"):
element.AxisTag = obj.name.split("/")[1]
refresh_ui_data()
if not element.is_a("IfcRoot"):
return
if obj.BIMObjectProperties.collection:
@@ -98,8 +105,8 @@ def update_bim_tool_props():
if not obj:
return
mode = bpy.context.mode
current_tool = bpy.context.workspace.tools.from_space_view3d_mode(mode).idname
if current_tool != "bim.bim_tool":
current_tool = bpy.context.workspace.tools.from_space_view3d_mode(mode)
if not current_tool or current_tool.idname != "bim.bim_tool":
return
element = tool.Ifc.get_entity(obj)
if not element:
@@ -168,6 +175,9 @@ def refresh_ui_data():
except AttributeError:
pass
if isinstance(tool.Ifc.get(), ifcopenshell.sqlite):
tool.Ifc.get().clear_cache()
def purge_module_data():
from blenderbim.bim import modules
@@ -195,24 +205,13 @@ def loadIfcStore(scene):
IfcStore.relink_all_objects()
@persistent
def undo_pre(scene):
IfcStore.track_undo_redo_stack_object_map()
@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()
purge_module_data()
IfcStore.track_undo_redo_stack_selected_objects()
IfcStore.reload_undo_redo_stack_objects()
@persistent
def redo_pre(scene):
IfcStore.track_undo_redo_stack_object_map()
tool.Ifc.rebuild_element_maps()
@persistent
@@ -221,14 +220,7 @@ def redo_post(scene):
IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction
IfcStore.redo()
purge_module_data()
IfcStore.track_undo_redo_stack_selected_objects()
IfcStore.reload_undo_redo_stack_objects()
@persistent
def ensureIfcExported(scene):
if IfcStore.get_file() and not bpy.context.scene.BIMProperties.ifc_file:
bpy.ops.export_ifc.bim("INVOKE_DEFAULT")
tool.Ifc.rebuild_element_maps()
def get_application(ifc):
@@ -265,8 +257,78 @@ def viewport_shading_changed_callback(area):
bpy.context.scene.BIMStylesProperties.active_style_type = "External"
if getattr(bpy.types, "SCENE_PT_scene"):
class Override_SCENE_PT_scene(bpy.types.SCENE_PT_scene):
bl_idname = "SCENE_PT_scene_override"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "BLENDER")
if getattr(bpy.types, "SCENE_PT_unit"):
class Override_SCENE_PT_unit(bpy.types.SCENE_PT_unit):
bl_idname = "SCENE_PT_unit_override"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "BLENDER")
if getattr(bpy.types, "SCENE_PT_physics"):
class Override_SCENE_PT_physics(bpy.types.SCENE_PT_physics):
bl_idname = "SCENE_PT_physics_override"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "BLENDER")
if getattr(bpy.types, "SCENE_PT_rigid_body_world"):
class Override_SCENE_PT_rigid_body_world(bpy.types.SCENE_PT_rigid_body_world):
bl_idname = "SCENE_PT_rigid_body_world_override"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "BLENDER")
if getattr(bpy.types, "SCENE_PT_audio"):
class Override_SCENE_PT_audio(bpy.types.SCENE_PT_audio):
bl_idname = "SCENE_PT_audio_override"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "BLENDER")
if getattr(bpy.types, "SCENE_PT_keying_sets"):
class Override_SCENE_PT_keying_sets(bpy.types.SCENE_PT_keying_sets):
bl_idname = "SCENE_PT_keying_sets_override"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "BLENDER")
if getattr(bpy.types, "SCENE_PT_custom_props"):
class Override_SCENE_PT_custom_props(bpy.types.SCENE_PT_custom_props):
bl_idname = "SCENE_PT_custom_props_override"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "BLENDER")
@persistent
def setDefaultProperties(scene):
def load_post(scene):
global global_subscription_owner
active_object_key = bpy.types.LayerObjects, "active"
bpy.msgbus.subscribe_rna(
@@ -290,3 +352,36 @@ def setDefaultProperties(scene):
ifcopenshell.api.owner.settings.get_user = lambda ifc: core_owner.get_user(tool.Owner)
ifcopenshell.api.owner.settings.get_application = get_application
AuthoringData.type_thumbnails = {}
if bpy.context.preferences.addons["blenderbim"].preferences.should_setup_workspace:
if "BIM" in bpy.data.workspaces:
bpy.context.window.workspace = bpy.data.workspaces["BIM"]
else:
bpy.ops.workspace.append_activate(idname="BIM", filepath=os.path.join(cwd, "data", "workspace.blend"))
# To improve usability for new users, we hijack the scene properties
# tab. We override default scene properties panels with our own poll
# to hide them unless the user has chosen to view Blender properties.
for panel in [
"SCENE_PT_scene",
"SCENE_PT_unit",
"SCENE_PT_physics",
"SCENE_PT_rigid_body_world",
"SCENE_PT_audio",
"SCENE_PT_keying_sets",
"SCENE_PT_custom_props",
]:
if getattr(bpy.types, panel, None):
try:
bpy.utils.register_class(globals()[f"Override_{panel}"])
bpy.utils.unregister_class(getattr(bpy.types, panel))
except:
pass
# https://blender.stackexchange.com/questions/140644/how-can-make-the-state-of-a-boolean-property-relative-to-the-3d-view-area
for screen in bpy.data.screens:
if len(screen.BIMAreaProperties) == 20:
continue
screen.BIMAreaProperties.clear()
for i in range(20): # 20 is an arbitrary value of split areas
screen.BIMAreaProperties.add()
+5
View File
@@ -273,6 +273,9 @@ def convert_property_group_from_si(property_group, skip_props=()):
setattr(property_group, prop_name, prop_value)
# TODO this should move into ifcopenshell.util
class IfcHeaderExtractor:
def __init__(self, filepath: str):
self.filepath = filepath
@@ -284,6 +287,8 @@ class IfcHeaderExtractor:
return self.extract_ifc_spf(ifc_file)
elif extension.lower() == "ifczip":
return self.extract_ifc_zip()
elif extension.lower() == "ifcsqlite":
return {} # TODO
def extract_ifc_spf(self, ifc_file):
# https://www.steptools.com/stds/step/IS_final_p21e3.html#clause-8
+27 -107
View File
@@ -26,6 +26,7 @@ import ifcopenshell
import blenderbim.bim.handler
import blenderbim.tool as tool
from pathlib import Path
from blenderbim.tool.brick import BrickStore
class IfcStore:
@@ -43,9 +44,6 @@ class IfcStore:
classification_file = None
library_path = ""
library_file = None
element_listeners = set()
undo_redo_stack_objects = set()
undo_redo_stack_object_names = {}
current_transaction = ""
last_transaction = ""
history = []
@@ -120,6 +118,8 @@ class IfcStore:
@staticmethod
def load_file(path):
if not os.path.isfile(path):
return
extension = path.split(".")[-1]
if extension.lower() == "ifczip":
with tempfile.TemporaryDirectory() as unzipped_path:
@@ -130,7 +130,9 @@ class IfcStore:
return
elif extension.lower() == "ifcxml":
IfcStore.file = ifcopenshell.file(ifcopenshell.ifcopenshell_wrapper.parse_ifcxml(path))
elif extension.lower() == "ifc":
elif bpy.context.scene.BIMProjectProperties.should_stream:
IfcStore.file = ifcopenshell.open(path, should_stream=True)
else:
IfcStore.file = ifcopenshell.open(path)
@staticmethod
@@ -156,96 +158,6 @@ class IfcStore:
return
return obj
@staticmethod
def add_element_listener(callback):
IfcStore.element_listeners.add(callback)
@staticmethod
def track_undo_redo_stack_object_map():
"""Keeps track of currently mapped object names, typically during undo and redo
When any Blender object is stored outside a Blender PointerProperty, such as
in a regular Python list, there is the likely probability that the object
will be invalidated when undo or redo occurs. Object invalidation seems to
occur whenever an object is affected during an operation.
For example, if an operator deletes a modifier on o1, then o1 will be invalidated.
"""
for key, value in IfcStore.id_map.items():
try:
IfcStore.undo_redo_stack_object_names[key] = value.name
except:
continue
@staticmethod
def track_undo_redo_stack_selected_objects():
"""Keeps track of selected object names, typically during undo and redo
When any Blender object is stored outside a Blender PointerProperty, such as
in a regular Python list, there is the likely probability that the object
will be invalidated when undo or redo occurs. Object invalidation seems to
occur for selected objects either pre/post undo/redo event, including
selected objects for consecutive undo/redos, and all children. This is
important because selected objects are often deleted from the scene.
So if I first select o1, then o2, then o3, then press undo, o3 will be
invalidated. If instead I press undo twice, o3 and o2 will be invalidated.
"""
if bpy.context.active_object:
objects = set([o.name for o in bpy.context.selected_objects + [bpy.context.active_object]])
objects.update([o.name for o in bpy.context.active_object.children])
else:
objects = set([o.name for o in bpy.context.selected_objects])
for obj in bpy.context.selected_objects:
objects.update([o.name for o in obj.children])
IfcStore.undo_redo_stack_objects |= objects
@staticmethod
def reload_undo_redo_stack_objects():
"""Reloads any invalidated objects after undo or redo
After an undo or redo operation, objects may have been invalidated in
our id_map and guid_map. Invalidated objects are typically those that
have been manipulated or deleted. This checks the cache of mapped and
selected objects prior to the operation and ensures that if the object
is invalidated, they are reloaded based on the object name that was
tracked prior to the undo / redo.
"""
file = IfcStore.get_file()
if not file:
return
# First, reload objects that were selected or active
for name in IfcStore.undo_redo_stack_objects:
obj = bpy.data.objects.get(name)
if not obj:
continue
if not obj.BIMObjectProperties.ifc_definition_id:
continue
element = file.by_id(obj.BIMObjectProperties.ifc_definition_id)
data = {"id": element.id(), "obj": obj.name}
if hasattr(element, "GlobalId"):
data["guid"] = element.GlobalId
IfcStore.commit_link_element(data)
# Scan for any straggling invalidated objects which were indirectly affected and reload them too.
for key, value in IfcStore.id_map.items():
try:
value.name
except:
# TODO not so sure about this obj_name check
obj_name = IfcStore.undo_redo_stack_object_names.get(key, None)
if not obj_name:
continue
obj = bpy.data.objects.get(obj_name)
if not obj or not obj.BIMObjectProperties.ifc_definition_id:
continue
element = file.by_id(obj.BIMObjectProperties.ifc_definition_id)
data = {"id": element.id(), "obj": obj.name}
if hasattr(element, "GlobalId"):
data["guid"] = element.GlobalId
IfcStore.commit_link_element(data)
@staticmethod
def relink_all_objects():
if not IfcStore.get_file():
@@ -304,9 +216,6 @@ class IfcStore:
blenderbim.bim.handler.subscribe_to(obj, "mode", blenderbim.bim.handler.mode_callback)
blenderbim.bim.handler.subscribe_to(obj, "active_material_index", blenderbim.bim.handler.active_material_index_callback)
for listener in IfcStore.element_listeners:
listener(element, obj)
if IfcStore.history:
data = {"id": element.id(), "guid": getattr(element, "GlobalId", None), "obj": obj.name}
IfcStore.history[-1]["operations"].append(
@@ -327,10 +236,12 @@ class IfcStore:
IfcStore.id_map[data["id"]] = obj
if "guid" in data:
IfcStore.guid_map[data["guid"]] = obj
blenderbim.bim.handler.subscribe_to(obj, "mode", blenderbim.bim.handler.mode_callback)
blenderbim.bim.handler.subscribe_to(obj, "name", blenderbim.bim.handler.name_callback)
if isinstance(obj, bpy.types.Material):
blenderbim.bim.handler.subscribe_to(obj, "diffuse_color", blenderbim.bim.handler.color_callback)
elif isinstance(obj, bpy.types.Object):
blenderbim.bim.handler.subscribe_to(obj, "mode", blenderbim.bim.handler.mode_callback)
blenderbim.bim.handler.subscribe_to(obj, "active_material_index", blenderbim.bim.handler.active_material_index_callback)
# TODO Listeners are not re-registered. Does this cause nasty problems to debug later on?
# TODO We're handling id_map and guid_map, but what about edited_objs? This might cause big problems.
@@ -396,24 +307,33 @@ class IfcStore:
)
@staticmethod
def execute_ifc_operator(operator, context):
def execute_ifc_operator(operator, context, is_invoke=False):
is_top_level_operator = not bool(IfcStore.current_transaction)
if is_top_level_operator:
IfcStore.begin_transaction(operator)
IfcStore.get_file().begin_transaction()
if tool.Ifc.get():
tool.Ifc.get().begin_transaction()
if BrickStore.graph:
BrickStore.begin_transaction()
# This empty transaction ensures that each operator has at least one transaction
IfcStore.add_transaction_operation(operator, rollback=lambda data: True, commit=lambda data: True)
else:
operator.transaction_key = IfcStore.current_transaction
result = getattr(operator, "_execute")(context)
if is_invoke:
result = getattr(operator, "_invoke")(context, None)
else:
result = getattr(operator, "_execute")(context)
if is_top_level_operator:
IfcStore.get_file().end_transaction()
IfcStore.add_transaction_operation(
operator, rollback=lambda d: IfcStore.get_file().undo(), commit=lambda d: IfcStore.get_file().redo()
)
if tool.Ifc.get():
tool.Ifc.get().end_transaction()
IfcStore.add_transaction_operation(
operator, rollback=lambda d: tool.Ifc.get().undo(), commit=lambda d: tool.Ifc.get().redo()
)
if BrickStore.graph:
BrickStore.end_transaction()
IfcStore.end_transaction(operator)
blenderbim.bim.handler.refresh_ui_data()
@@ -421,8 +341,6 @@ class IfcStore:
@staticmethod
def begin_transaction(operator):
IfcStore.undo_redo_stack_objects = set()
IfcStore.undo_redo_stack_object_names = {}
IfcStore.current_transaction = str(uuid.uuid4())
operator.transaction_key = IfcStore.current_transaction
@@ -449,6 +367,7 @@ class IfcStore:
@staticmethod
def undo():
BrickStore.undo()
if not IfcStore.history:
return
event = IfcStore.history.pop()
@@ -458,6 +377,7 @@ class IfcStore:
@staticmethod
def redo():
BrickStore.redo()
if not IfcStore.future:
return
event = IfcStore.future.pop()
+116 -34
View File
@@ -327,13 +327,19 @@ class IfcImporter:
if isinstance(self.elements, set):
self.elements = list(self.elements)
# TODO: enable filtering for annotations
self.annotations = set([a for a in self.file.by_type("IfcAnnotation") if not a.HasAssignments])
else:
if self.file.schema in ("IFC2X3", "IFC4"):
self.elements = self.file.by_type("IfcElement") + self.file.by_type("IfcProxy")
else:
self.elements = self.file.by_type("IfcElement")
self.annotations = set([a for a in self.file.by_type("IfcAnnotation") if not a.HasAssignments])
drawing_groups = [g for g in self.file.by_type("IfcGroup") if g.ObjectType == "DRAWING"]
drawing_annotations = set()
for drawing_group in drawing_groups:
for rel in drawing_group.IsGroupedBy:
drawing_annotations.update(rel.RelatedObjects)
self.annotations = set([a for a in self.file.by_type("IfcAnnotation")])
self.annotations -= drawing_annotations
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement")]
if self.ifc_import_settings.is_coordinating:
@@ -362,11 +368,13 @@ class IfcImporter:
while True:
results.add(spatial_element)
spatial_element = ifcopenshell.util.element.get_aggregate(spatial_element)
if not spatial_element or spatial_element.is_a() in ("IfcContext", "IfcProject"):
if not spatial_element or spatial_element.is_a() in ("IfcProject", "IfcProjectLibrary"):
break
return results
def parse_native_elements(self):
if not self.ifc_import_settings.should_load_geometry:
return
for element in self.elements:
if self.is_native(element):
self.native_elements.add(element)
@@ -578,6 +586,8 @@ class IfcImporter:
return result
def create_grids(self):
if not self.ifc_import_settings.should_load_geometry:
return
grids = self.file.by_type("IfcGrid")
for grid in grids:
shape = None
@@ -623,34 +633,37 @@ class IfcImporter:
def create_element_type(self, element):
self.ifc_import_settings.logger.info("Creating object %s", element)
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
representation = ifcopenshell.util.representation.get_representation(element, "Plan", "Annotation")
if not representation:
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Annotation")
mesh = None
if representation:
mesh_name = "{}/{}".format(representation.ContextOfItems.id(), representation.id())
mesh = self.meshes.get(mesh_name)
if mesh is None:
shape = None
try:
shape = ifcopenshell.geom.create_shape(self.settings, representation)
except:
if self.ifc_import_settings.should_load_geometry:
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
representation = ifcopenshell.util.representation.get_representation(element, "Plan", "Annotation")
if not representation:
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Annotation")
if representation:
mesh_name = "{}/{}".format(representation.ContextOfItems.id(), representation.id())
mesh = self.meshes.get(mesh_name)
if mesh is None:
shape = None
try:
shape = ifcopenshell.geom.create_shape(self.settings_2d, representation)
shape = ifcopenshell.geom.create_shape(self.settings, representation)
except:
self.ifc_import_settings.logger.error("Failed to generate shape for %s", element)
if shape:
mesh = self.create_mesh(element, shape)
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
try:
shape = ifcopenshell.geom.create_shape(self.settings_2d, representation)
except:
self.ifc_import_settings.logger.error("Failed to generate shape for %s", element)
if shape:
mesh = self.create_mesh(element, shape)
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
obj = bpy.data.objects.new(tool.Loader.get_name(element), mesh)
self.link_element(element, obj)
self.material_creator.create(element, obj, mesh)
self.type_products[element.GlobalId] = obj
def create_native_elements(self):
if not self.ifc_import_settings.should_load_geometry:
return
progress = 0
checkpoint = time.time()
total = len(self.native_elements)
@@ -692,21 +705,76 @@ class IfcImporter:
self.create_generic_elements(self.elements)
def create_generic_elements(self, elements):
if isinstance(self.file, ifcopenshell.sqlite):
return self.create_generic_sqlite_elements(elements)
# Based on my experience in viewing BIM models, representations are prioritised as follows:
# 1. 3D Body, 2. 2D Body, 3. 2D Plans / annotations, 4. Point clouds, 5. No representation
# If an element has a representation that doesn't follow 1, 2, 3, or 4, it will not show by default.
# The user can load them later if they want to view them.
products = self.create_products(elements)
elements -= products
products = self.create_products(elements, settings=self.settings_curve)
elements -= products
products = self.create_products(elements, settings=self.settings_2d)
elements -= products
products = self.create_pointclouds(elements)
elements -= products
for element in elements:
if self.ifc_import_settings.should_load_geometry:
products = self.create_products(elements)
elements -= products
products = self.create_products(elements, settings=self.settings_curve)
elements -= products
products = self.create_products(elements, settings=self.settings_2d)
elements -= products
products = self.create_pointclouds(elements)
elements -= products
total = len(elements)
for i, element in enumerate(elements):
if i % 250 == 0:
print("{} / {} elements processed ...".format(i, total))
self.create_product(element)
def create_generic_sqlite_elements(self, elements):
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(type("Geometry", (), {"id": geometry_id}))
mesh = bpy.data.meshes.new(mesh_name)
verts = geometry["verts"]
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()
else:
e = geometry["edges"]
v = verts
vertices = [[v[i], v[i + 1], v[i + 2]] for i in range(0, len(v), 3)]
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
mesh.from_pydata(vertices, edges, [])
mesh["ios_materials"] = geometry["materials"]
mesh["ios_material_ids"] = geometry["material_ids"]
self.meshes[mesh_name] = mesh
total = len(elements)
for i, element in enumerate(elements):
if i % 250 == 0:
print("{} / {} elements processed ...".format(i, total))
mesh = None
geometry_id = self.geometry_cache["shapes"][element.id()]["geometry"]
if geometry_id:
mesh_name = tool.Loader.get_mesh_name(type("Geometry", (), {"id": geometry_id}))
mesh = self.meshes.get(mesh_name)
self.create_product(element, mesh=mesh)
def create_products(self, products, settings=None):
if settings is None:
settings = self.settings
@@ -824,7 +892,7 @@ class IfcImporter:
return None
for representation in representations:
if representation.RepresentationType == "PointCloud":
if representation.RepresentationType in ("PointCloud", "Point"):
return representation
elif self.file.schema == "IFC2X3" and representation.RepresentationType == "GeometricSet":
@@ -1274,12 +1342,21 @@ class IfcImporter:
last_obj = obj
if not last_obj:
return
# temporarily unhide types collection to make sure all objects will be cleaned
project_collection = bpy.context.view_layer.layer_collection.children[self.project["blender"].name]
types_collection = project_collection.children[self.type_collection.name]
types_collection.hide_viewport = False
bpy.context.view_layer.objects.active = last_obj
context_override = {}
bpy.ops.object.editmode_toggle(context_override)
bpy.ops.mesh.tris_convert_to_quads(context_override)
bpy.ops.mesh.normals_make_consistent(context_override)
bpy.ops.object.editmode_toggle(context_override)
types_collection.hide_viewport = True
bpy.context.view_layer.objects.active = last_obj
IfcStore.edited_objs.clear()
def load_file(self):
@@ -1562,7 +1639,10 @@ class IfcImporter:
return rel.RelatingGroup
def get_element_matrix(self, element):
result = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
if isinstance(element, ifcopenshell.sqlite_entity):
result = self.geometry_cache["shapes"][element.id()]["matrix"]
else:
result = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
result[0][3] *= self.unit_scale
result[1][3] *= self.unit_scale
result[2][3] *= self.unit_scale
@@ -1719,7 +1799,7 @@ class IfcImporter:
mesh.polygons.add(num_loops)
mesh.polygons.foreach_set("loop_start", loop_start)
mesh.polygons.foreach_set("loop_total", loop_total)
mesh.polygons.foreach_set("use_smooth", [0]*total_faces)
mesh.polygons.foreach_set("use_smooth", [0] * total_faces)
mesh.update()
else:
e = geometry.edges
@@ -1796,6 +1876,7 @@ class IfcImportSettings:
self.should_use_cpu_multiprocessing = True
self.merge_mode = None
self.should_merge_materials_by_colour = False
self.should_load_geometry = True
self.should_use_native_meshes = False
self.should_clean_mesh = True
self.should_cache = True
@@ -1825,6 +1906,7 @@ class IfcImportSettings:
settings.should_use_cpu_multiprocessing = props.should_use_cpu_multiprocessing
settings.merge_mode = props.merge_mode
settings.should_merge_materials_by_colour = props.should_merge_materials_by_colour
settings.should_load_geometry = props.should_load_geometry
settings.should_use_native_meshes = props.should_use_native_meshes
settings.should_clean_mesh = props.should_clean_mesh
settings.should_cache = props.should_cache
@@ -57,6 +57,7 @@ classes = (
operator.SelectBcfBimSnippetReference,
operator.SelectBcfDocumentReference,
operator.SelectBcfHeaderFile,
operator.UnloadBcfProject,
operator.ViewBcfTopic,
prop.BcfReferenceLink,
prop.BcfLabel,
@@ -58,7 +58,6 @@ class LoadBcfProject(bpy.types.Operator):
filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"})
def execute(self, context):
context.scene.BCFProperties.is_loaded = False
if self.filepath:
bcfstore.BcfStore.bcfxml = bcf.bcfxml.load(self.filepath)
bcfxml = bcfstore.BcfStore.get_bcfxml()
@@ -72,6 +71,17 @@ class LoadBcfProject(bpy.types.Operator):
return {"RUNNING_MODAL"}
class UnloadBcfProject(bpy.types.Operator):
bl_idname = "bim.unload_bcf_project"
bl_label = "Unload BCF Project"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
bcfstore.BcfStore.set(None)
context.scene.BCFProperties.is_loaded = False
return {"FINISHED"}
class LoadBcfTopics(bpy.types.Operator):
bl_idname = "bim.load_bcf_topics"
bl_label = "Load BCF Topics"
@@ -39,14 +39,16 @@ class BIM_PT_bcf(Panel):
scene = context.scene
props = scene.BCFProperties
row = layout.row(align=True)
row.operator("bim.new_bcf_project", text="New Project")
row.operator("bim.load_bcf_project", text="Load Project")
if not props.is_loaded:
row = layout.row(align=True)
row.operator("bim.new_bcf_project", text="New Project")
row.operator("bim.load_bcf_project", text="Load Project")
return
row.operator("bim.save_bcf_project", text="Save Project")
row = layout.row(align=True)
row.operator("bim.save_bcf_project", icon="EXPORT", text="Save Project")
row.operator("bim.unload_bcf_project", text="", icon="CANCEL")
row = layout.row()
row.prop(props, "name")
@@ -33,6 +33,8 @@ classes = (
operator.RewindBrickClass,
operator.ViewBrickClass,
operator.ViewBrickItem,
operator.SerializeBrick,
operator.AddBrickNamespace,
prop.Brick,
prop.BIMBrickProperties,
ui.BIM_PT_brickschema,
@@ -109,8 +109,10 @@ class BrickschemaData:
if BrickStore.graph is None:
return []
results = []
filter = ["brick", "owl", "w3", "xml"]
for alias, uri in BrickStore.graph.namespaces():
results.append((uri, f"{alias}: {uri}", ""))
if alias not in filter:
results.append((uri, f"{alias}: {uri}", ""))
return results
@classmethod
@@ -22,11 +22,12 @@ import blenderbim.tool as tool
import blenderbim.core.brick as core
import blenderbim.bim.handler
from blenderbim.bim.ifc import IfcStore
from blenderbim.tool.brick import BrickStore
class Operator:
def execute(self, context):
self._execute(context)
IfcStore.execute_ifc_operator(self, context)
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
@@ -126,6 +127,7 @@ class AddBrick(bpy.types.Operator, Operator):
namespace=props.namespace,
brick_class=props.brick_equipment_class,
library=library,
label=props.new_brick_label,
)
@@ -158,14 +160,36 @@ class ConvertIfcToBrick(bpy.types.Operator, Operator):
core.convert_ifc_to_brick(tool.Brick, namespace=props.namespace, library=library)
class NewBrickFile(bpy.types.Operator, Operator):
class NewBrickFile(bpy.types.Operator):
bl_idname = "bim.new_brick_file"
bl_label = "New Brick File"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
IfcStore.begin_transaction(self)
IfcStore.add_transaction_operation(self, rollback=self.rollback, commit=lambda data: True)
self._execute(context)
self.transaction_data = {
"schema": BrickStore.schema,
"path": BrickStore.path,
"graph": BrickStore.graph,
}
IfcStore.add_transaction_operation(self, rollback=lambda data: True, commit=self.commit)
IfcStore.end_transaction(self)
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
def _execute(self, context):
core.new_brick_file(tool.Brick)
def rollback(self, data):
BrickStore.purge()
def commit(self, data):
BrickStore.schema = data["schema"]
BrickStore.path = data["path"]
BrickStore.graph = data["graph"]
class RefreshBrickViewer(bpy.types.Operator, Operator):
bl_idname = "bim.refresh_brick_viewer"
@@ -189,3 +213,42 @@ class RemoveBrick(bpy.types.Operator, Operator):
library=tool.Ifc.get().by_id(int(props.libraries)) if props.libraries else None,
brick_uri=props.bricks[props.active_brick_index].uri,
)
class SerializeBrick(bpy.types.Operator):
bl_idname = "bim.serialize_brick"
bl_label = "Serialize Brick"
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"}
else:
return self.execute(context)
def execute(self, context):
if self.should_save_as or not BrickStore.path:
BrickStore.path = self.filepath
core.serialize_brick(tool.Brick)
return {"FINISHED"}
@classmethod
def description(cls, context, properties):
if properties.should_save_as:
return "Save Brick project to a selected file"
return "Save the Brick project"
class AddBrickNamespace(bpy.types.Operator, Operator):
bl_idname = "bim.add_brick_namespace"
bl_label = "Add Brick Namespace"
def _execute(self, context):
props = context.scene.BIMBrickProperties
alias = props.new_brick_namespace_alias
uri = props.new_brick_namespace_uri
core.add_namespace(tool.Brick, alias=alias, uri=uri)
@@ -69,3 +69,7 @@ class BIMBrickProperties(PropertyGroup):
libraries: EnumProperty(name="Libraries", items=get_libraries)
namespace: EnumProperty(name="Namespace", items=get_namespaces)
brick_equipment_class: EnumProperty(name="Brick Equipment Class", items=get_brick_equipment_classes)
brick_settings_toggled: BoolProperty(name="Brick Settings Toggled", default=False)
new_brick_label: StringProperty(name="New Brick Label")
new_brick_namespace_alias: StringProperty(name="New Brick Namespace Alias")
new_brick_namespace_uri: StringProperty(name="New Brick Namespace URI")
@@ -20,6 +20,7 @@ import blenderbim.tool as tool
from bpy.types import Panel, UIList
from blenderbim.bim.helper import prop_with_search
from blenderbim.bim.module.brick.data import BrickschemaData, BrickschemaReferencesData
from blenderbim.tool.brick import BrickStore
class BIM_PT_brickschema(Panel):
@@ -30,6 +31,10 @@ class BIM_PT_brickschema(Panel):
bl_region_type = "WINDOW"
bl_context = "scene"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "OTHER")
def draw(self, context):
if not BrickschemaData.is_loaded:
BrickschemaData.load()
@@ -41,15 +46,39 @@ class BIM_PT_brickschema(Panel):
row.operator("bim.load_brick_project", text="Load Project")
return
if BrickStore.path:
row = self.layout.row(align=True)
row.label(text=BrickStore.path, icon="FILEBROWSER")
row = self.layout.row(align=True)
if len(self.props.brick_breadcrumbs):
row.operator("bim.rewind_brick_class", text="", icon="FRAME_PREV")
row.label(text=self.props.active_brick_class)
row.prop(data=self.props, property="brick_settings_toggled", text="", icon="PREFERENCES")
row.operator("bim.refresh_brick_viewer", text="", icon="FILE_REFRESH")
row.operator("bim.close_brick_project", text="", icon="CANCEL")
if self.props.brick_settings_toggled:
box = self.layout.box()
row = box.row(align=True)
row.label(text="Active Namespace:")
row = box.row(align=True)
prop_with_search(row, self.props, "namespace", text="")
row = box.row(align=True)
row.label(text="Bind New Namespace:")
row = box.row(align=True)
row.prop(data=self.props, property="new_brick_namespace_alias", text="")
col = row.column()
col.alignment = "CENTER"
col.scale_x = 1.1
col.label(text=":")
row.prop(data=self.props, property="new_brick_namespace_uri", text="")
row.operator("bim.add_brick_namespace", text="", icon="ADD")
row = self.layout.row(align=True)
prop_with_search(row, self.props, "namespace", text="")
row.label(text="Create Entity:")
row = self.layout.row(align=True)
row.prop(data=self.props, property="new_brick_label", text="")
prop_with_search(row, self.props, "brick_equipment_class", text="")
row.operator("bim.add_brick", text="", icon="ADD")
@@ -58,6 +87,12 @@ class BIM_PT_brickschema(Panel):
row.operator("bim.add_brick_feed", text="", icon="PLUGIN")
row.operator("bim.remove_brick", text="", icon="X")
row = self.layout.row(align=True)
op = row.operator("bim.serialize_brick", icon="EXPORT", text="Save")
op.should_save_as = False
op = row.operator("bim.serialize_brick", icon="FILE_TICK", text="Save As")
op.should_save_as = True
self.layout.template_list("BIM_UL_bricks", "", self.props, "bricks", self.props, "active_brick_index")
for attribute in BrickschemaData.data["attributes"]:
@@ -137,24 +137,22 @@ class CadTool(WorkSpaceTool):
else:
if (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and RailingData.data["pset_data"]
and context.active_object.BIMRailingProperties.is_editing_path
):
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Q")
row.operator("bim.cad_hotkey", text="Apply Railing Path").hotkey = "S_Q"
row.label(text="", icon=f"EVENT_TAB")
row.operator("bim.finish_editing_railing_path")
row.operator("bim.cancel_editing_railing_path", icon="CANCEL", text="")
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["parameters"]
and RoofData.data["pset_data"]
and context.active_object.BIMRoofProperties.is_editing_path
):
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Q")
row.operator("bim.cad_hotkey", text="Apply Roof Path").hotkey = "S_Q"
row.label(text="", icon=f"EVENT_TAB")
row.operator("bim.finish_editing_roof_path")
row.operator("bim.cancel_editing_roof_path", icon="CANCEL", text="")
row = layout.row(align=True)
@@ -229,7 +227,7 @@ class CadHotkey(bpy.types.Operator):
row.prop(props, "y")
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["parameters"]
and RoofData.data["pset_data"]
and bpy.context.active_object.BIMRoofProperties.is_editing_path
):
self.layout.row().prop(props, "gable_roof_edge_angle")
@@ -270,26 +268,12 @@ class CadHotkey(bpy.types.Operator):
elif bpy.context.active_object.data.BIMMeshProperties.subshape_type == "AXIS":
bpy.ops.bim.edit_extrusion_axis()
elif (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and bpy.context.active_object.BIMRailingProperties.is_editing_path
):
bpy.ops.bim.finish_editing_railing_path()
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["parameters"]
and bpy.context.active_object.BIMRoofProperties.is_editing_path
):
bpy.ops.bim.finish_editing_roof_path()
def hotkey_S_R(self):
if self.is_profile():
bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y)
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["parameters"]
and RoofData.data["pset_data"]
and bpy.context.active_object.BIMRoofProperties.is_editing_path
):
bpy.ops.bim.set_gable_roof_edge_angle(
@@ -23,7 +23,7 @@ import bmesh
import logging
import numpy as np
import ifcopenshell
from mathutils import Matrix
from mathutils import Matrix, Vector
from math import radians
from blenderbim.bim.ifc import IfcStore
@@ -222,6 +222,7 @@ class ExecuteIfcClash(bpy.types.Operator):
_, extension = os.path.splitext(self.filepath)
if extension != ".json":
self.filepath = bpy.path.ensure_ext(self.filepath, ".bcf")
settings = ifcclash.ClashSettings()
settings.output = self.filepath
settings.logger = logging.getLogger("Clash")
@@ -230,12 +231,28 @@ class ExecuteIfcClash(bpy.types.Operator):
if context.scene.BIMClashProperties.should_create_clash_snapshots:
def get_viewpoint_snapshot(viewpoint, mat):
def get_viewpoint_snapshot(viewpoint):
camera = bpy.data.objects.get("IFC Clash Camera")
if not camera:
camera = bpy.data.objects.new("IFC Clash Camera", bpy.data.cameras.new("IFC Clash Camera"))
context.scene.collection.objects.link(camera)
camera.matrix_world = Matrix(mat)
bcf_camera = viewpoint.visualization_info.perspective_camera
p = bcf_camera.camera_view_point
z = bcf_camera.camera_direction
z = Vector([z.x, z.y, z.z]) * -1
y = bcf_camera.camera_up_vector
y = Vector([y.x, y.y, y.z])
x = y.cross(z)
mat = Matrix([
[x[0], y[0], z[0], p.x],
[x[1], y[1], z[1], p.y],
[x[2], y[2], z[2], p.z],
[0, 0, 0, 0],
])
camera.matrix_world = mat
context.scene.camera = camera
camera.data.angle = radians(60)
area = next(area for area in context.screen.areas if area.type == "VIEW_3D")
@@ -246,7 +263,8 @@ class ExecuteIfcClash(bpy.types.Operator):
context.scene.render.image_settings.file_format = "PNG"
context.scene.render.filepath = os.path.join(context.scene.BIMProperties.data_dir, "snapshot.png")
bpy.ops.render.opengl(write_still=True)
return context.scene.render.filepath
with open(context.scene.render.filepath, "rb") as f:
return ("snapshot.png", f.read())
clasher.get_viewpoint_snapshot = get_viewpoint_snapshot
@@ -163,7 +163,7 @@ class RemoveCostItem(bpy.types.Operator, tool.Ifc.Operator):
cost_item: bpy.props.IntProperty()
def _execute(self, context):
core.remove_cost_item(tool.Ifc, tool.Cost, cost_item=tool.Ifc.get().by_id(self.cost_item))
core.remove_cost_item(tool.Ifc, tool.Cost, cost_item_id=self.cost_item)
class EnableEditingCostItem(bpy.types.Operator, tool.Ifc.Operator):
@@ -128,12 +128,14 @@ class ExportCsvAttributes(bpy.types.Operator):
class ExportIfcCsv(bpy.types.Operator):
bl_idname = "bim.export_ifccsv"
bl_label = "Export IFC to CSV"
filename_ext = ".csv"
bl_label = "Export IFC"
#filename_ext = ".csv"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".csv")
props = context.scene.CsvProperties
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
@@ -142,7 +144,7 @@ class ExportIfcCsv(bpy.types.Operator):
import ifccsv
props = context.scene.CsvProperties
self.filepath = bpy.path.ensure_ext(self.filepath, ".csv")
self.filepath = bpy.path.ensure_ext(self.filepath, f".{props.format}")
if props.should_load_from_memory:
ifc_file = IfcStore.get_file()
else:
@@ -152,7 +154,7 @@ class ExportIfcCsv(bpy.types.Operator):
ifc_csv = ifccsv.IfcCsv()
attributes = [a.name for a in props.csv_attributes]
sep = props.csv_custom_delimiter if props.csv_delimiter == "CUSTOM" else props.csv_delimiter
ifc_csv.export(ifc_file, results, attributes, output=self.filepath, format=args.format, delimiter=sep)
ifc_csv.export(ifc_file, results, attributes, output=self.filepath, format=props.format, delimiter=sep)
return {"FINISHED"}
@@ -45,5 +45,14 @@ class CsvProperties(PropertyGroup):
name="IFC CSV Delimiter",
default=",",
)
format: EnumProperty(
items=[
("csv", "csv", ""),
("xlsx", "xlsx", ""),
("ods", "ods", ""),
],
name="Output format",
default="csv",
)
csv_custom_delimiter: StringProperty(default="", name="Custom Delimiter")
should_load_from_memory: BoolProperty(default=False, name="Load from Memory")
@@ -60,17 +60,21 @@ class BIM_PT_ifccsv(Panel):
row = layout.row(align=True)
row.prop(attribute, "name", text="")
row.operator("bim.remove_csv_attribute", icon="X", text="").index = index
row = layout.row(align=True)
row.prop(props, "csv_delimiter")
row.prop(props, "format")
if props.csv_delimiter == "CUSTOM":
if props.format == 'csv':
row = layout.row(align=True)
row.prop(props, "csv_custom_delimiter")
row.prop(props, "csv_delimiter")
if props.csv_delimiter == "CUSTOM":
row = layout.row(align=True)
row.prop(props, "csv_custom_delimiter")
row = layout.row()
split = row.split(factor=0.5)
c = split.column()
c.operator("bim.export_ifccsv", icon="EXPORT")
c.operator("bim.export_ifccsv", icon="EXPORT", text="Export IFC to " + props.format.upper())
c = split.column()
c.operator("bim.import_ifccsv", icon="IMPORT")
@@ -20,6 +20,7 @@ import bpy
from . import ui, prop, operator
classes = (
operator.CopyDebugInformation,
operator.CreateAllShapes,
operator.CreateShapeFromStepId,
operator.InspectFromObject,
@@ -20,6 +20,9 @@ import os
import bpy
import time
import logging
import platform
import subprocess
import addon_utils
import ifcopenshell
import ifcopenshell.util.placement
import ifcopenshell.util.representation
@@ -31,6 +34,58 @@ import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
class CopyDebugInformation(bpy.types.Operator):
bl_idname = "bim.copy_debug_information"
bl_label = "Copy Debug Information"
bl_description = "Copies debugging information to your clipboard for use in bugreports"
def execute(self, context):
version = ".".join(
[
str(x)
for x in [
addon.bl_info.get("version", (-1, -1, -1))
for addon in addon_utils.modules()
if addon.bl_info["name"] == "BlenderBIM"
][0]
]
)
info = {
"os": platform.system(),
"os_version": platform.version(),
"python_version": platform.python_version(),
"architecture": platform.architecture(),
"machine": platform.machine(),
"processor": platform.processor(),
"blender_version": bpy.app.version_string,
"blenderbim_version": version,
"ifc": False,
}
if tool.Ifc.get():
info.update(
{
"ifc": os.path.basename(tool.Ifc.get_path()) if os.path.isfile(tool.Ifc.get_path()) else "Unsaved",
"schema": tool.Ifc.get().schema,
"preprocessor_version": tool.Ifc.get().wrapped_data.header.file_name.preprocessor_version,
"originating_system": tool.Ifc.get().wrapped_data.header.file_name.originating_system,
}
)
# Format it in a readable way
text = "\n".join(f"{k}: {v}" for k, v in info.items())
print(text)
if platform.system() == "Windows":
command = "echo | set /p nul=" + text.strip()
elif platform.system() == "Darwin": # for MacOS
command = 'printf "' + text.strip().replace("\n", "\\n").replace('"', "") + '" | pbcopy'
else: # Linux
command = 'printf "' + text.strip().replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard'
subprocess.run(command, shell=True, check=True)
return {"FINISHED"}
class PrintIfcFile(bpy.types.Operator):
bl_idname = "bim.print_ifc_file"
bl_label = "Print IFC File"
@@ -73,7 +128,7 @@ class ConvertToBlender(bpy.types.Operator):
def execute(self, context):
for o in bpy.data.objects:
if o.type in {'MESH', 'EMPTY'}:
if o.type in {"MESH", "EMPTY"}:
o.BIMObjectProperties.ifc_definition_id = 0
if o.data:
o.data.BIMMeshProperties.ifc_definition_id = 0
@@ -135,6 +190,8 @@ class CreateAllShapes(bpy.types.Operator):
total = len(elements)
settings = ifcopenshell.geom.settings()
settings_2d = ifcopenshell.geom.settings()
settings_2d.set(settings_2d.INCLUDE_CURVES, True)
failures = []
excludes = () # For the developer to debug with
for i, element in enumerate(elements):
@@ -142,8 +199,16 @@ class CreateAllShapes(bpy.types.Operator):
continue
print(f"{i}/{total}:", element)
start = time.time()
shape = None
try:
shape = ifcopenshell.geom.create_shape(settings, element)
except:
try:
shape = ifcopenshell.geom.create_shape(settings_2d, element)
except:
failures.append(element)
print("***** FAILURE *****")
if shape:
print(
"Success",
time.time() - start,
@@ -151,9 +216,6 @@ class CreateAllShapes(bpy.types.Operator):
len(shape.geometry.edges),
len(shape.geometry.faces),
)
except:
failures.append(element)
print("***** FAILURE *****")
print(f"Failures: {len(failures)}")
for failure in failures:
print(failure)
@@ -48,6 +48,9 @@ class BIM_PT_debug(Panel):
row = layout.row()
row.operator("bim.print_ifc_file")
row = layout.row()
row.operator("bim.copy_debug_information")
row = layout.row()
row.operator("bim.purge_hdf5_cache")
@@ -24,22 +24,27 @@ classes = (
operator.ActivateDrawingStyle,
operator.ActivateModel,
operator.AddAnnotation,
operator.AddAnnotationType,
operator.AddDrawing,
operator.AddDrawingStyle,
operator.AddDrawingStyleAttribute,
operator.AddDrawingToSheet,
operator.AddReference,
operator.AddReferenceToSheet,
operator.AddSchedule,
operator.AddScheduleToSheet,
operator.AddReferenceToSheet,
operator.AddSheet,
operator.AddTextLiteral,
operator.BuildSchedule,
operator.CleanWireframes,
operator.ContractSheet,
operator.ContractTargetView,
operator.CreateDrawing,
operator.CreateSheets,
operator.DisableAddAnnotationType,
operator.DisableEditingAssignedProduct,
operator.DisableEditingDrawings,
operator.DisableEditingReferences,
operator.DisableEditingSchedules,
operator.DisableEditingSheets,
operator.DisableEditingText,
@@ -48,34 +53,34 @@ classes = (
operator.EditSheet,
operator.EditText,
operator.EditTextPopup,
operator.EnableAddAnnotationType,
operator.EnableEditingAssignedProduct,
operator.EnableEditingText,
operator.ExpandSheet,
operator.ExpandTargetView,
operator.LoadDrawings,
operator.LoadReferences,
operator.LoadSchedules,
operator.LoadSheets,
operator.OpenDrawing,
operator.OpenReference,
operator.OpenSchedule,
operator.OpenSheet,
operator.ReloadDrawingStyles,
operator.RemoveDrawing,
operator.RemoveDrawingFromSheet,
operator.RemoveDrawingStyle,
operator.RemoveDrawingStyleAttribute,
operator.RemoveReference,
operator.RemoveSchedule,
operator.ReloadDrawingStyles,
operator.RemoveSheet,
operator.RemoveTextLiteral,
operator.SelectAllDrawings,
operator.ResizeText,
operator.SaveDrawingStyle,
operator.SaveDrawingStylesData,
operator.SelectAllDrawings,
operator.SelectAssignedProduct,
operator.SelectDocIfcFile,
operator.LoadReferences,
operator.DisableEditingReferences,
operator.AddReference,
operator.RemoveReference,
operator.OpenReference,
prop.Variable,
prop.Drawing,
prop.Document,
@@ -89,7 +94,6 @@ classes = (
prop.BIMAnnotationProperties,
ui.BIM_PT_camera,
ui.BIM_PT_drawing_underlay,
ui.BIM_PT_annotation_utilities,
ui.BIM_PT_sheets,
ui.BIM_PT_drawings,
ui.BIM_PT_schedules,
@@ -103,6 +107,7 @@ classes = (
gizmos.DimensionLabelGizmo,
gizmos.ExtrusionGuidesGizmo,
gizmos.ExtrusionWidget,
workspace.LaunchAnnotationTypeManager,
workspace.Hotkey,
)
@@ -287,28 +287,36 @@ class DecoratorData:
cls.data[obj.name] = text_data
return text_data
# used by Ifc Annotations with ObjectType = "DIMENSION" / "DIAMETER"
@classmethod
def get_dimension_data(cls, obj):
"""used by Ifc Annotations with ObjectType:
DIMENSION / DIAMETER / SECTION_LEVEL / PLAN_LEVEL / RADIUS
"""
result = cls.data.get(obj.name, None)
if result is not None:
return result
element = tool.Ifc.get_entity(obj)
supported_object_types = ("DIMENSION", "DIAMETER")
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:
return None
dimension_style = "arrow"
fill_bg = False
classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
if classes and "oblique" in classes.lower().split():
dimension_style = "oblique"
if classes:
classes_split = classes.lower().split()
if "oblique" in classes_split:
dimension_style = "oblique"
elif "fill-bg" in classes_split:
fill_bg = True
pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Dimension") or {}
show_description_only = pset_data.get("ShowDescriptionOnly", False)
suppress_zero_inches = pset_data.get("SuppressZeroInches", False)
text_prefix = pset_data.get("TextPrefix", "")
text_suffix = pset_data.get("TextSuffix", "")
text_prefix = pset_data.get("TextPrefix", None) or ""
text_suffix = pset_data.get("TextSuffix", None) or ""
dimension_data = {
"dimension_style": dimension_style,
@@ -316,6 +324,7 @@ class DecoratorData:
"suppress_zero_inches": suppress_zero_inches,
"text_prefix": text_prefix,
"text_suffix": text_suffix,
"fill_bg": fill_bg,
}
cls.data[obj.name] = dimension_data
return dimension_data
@@ -329,18 +338,31 @@ class AnnotationData:
def load(cls):
cls.is_loaded = True
cls.props = bpy.context.scene.BIMAnnotationProperties
cls.data["relating_types"] = cls.get_relating_types()
cls.data["relating_type_id"] = cls.relating_type_id()
cls.data["relating_types"] = cls.relating_types()
@classmethod
def get_relating_types(cls):
def relating_type_id(cls):
object_type = cls.props.object_type
relating_types = []
for relating_type in tool.Ifc.get().by_type("IfcTypeProduct"):
if tool.Drawing.is_annotation_object_type(relating_type, object_type):
relating_types.append(relating_type)
enum_items = [(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in relating_types]
results = [("0", "Untyped", "")]
results.extend([(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in relating_types])
return results
# item to create anootations without relating types
enum_items.insert(0, ("0", "-", ""))
return enum_items
@classmethod
def relating_types(cls):
object_type = cls.props.object_type
relating_types = []
for relating_type in tool.Ifc.get().by_type("IfcTypeProduct"):
if tool.Drawing.is_annotation_object_type(relating_type, object_type):
relating_types.append({
"id": relating_type.id(),
"name": relating_type.Name or "Unnamed",
"description": relating_type.Description or "No Description",
})
return sorted(relating_types, key=lambda x: x["name"])
@@ -363,7 +363,7 @@ class BaseDecorator:
"""
if multiline:
line_no = 0
line_no = line_no
lines = text.split("\\n")
lines = lines if multiline_to_bottom else lines[::-1]
for line in lines:
@@ -454,6 +454,24 @@ class BaseDecorator:
blf.draw(font_id, text)
blf.disable(font_id, blf.ROTATION)
def draw_dimension_text(self, context, get_text, description, dimension_data, **draw_label_kwargs):
prefix = dimension_data["text_prefix"]
suffix = dimension_data["text_suffix"]
show_description_only = dimension_data["show_description_only"]
line_number_start = 0
if not show_description_only:
text = get_text()
full_prefix = ((description + "\\n") if description else "") + prefix
text = full_prefix + text + suffix
line_number_start -= full_prefix.count("\\n")
else:
if not description:
return
text = description
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):
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
@@ -637,39 +655,35 @@ class DimensionDecorator(BaseDecorator):
perpendicular = Vector((-text_dir.y, text_dir.x)).normalized()
text_offset = perpendicular * text_offset_value
common_label_attrs = {
"context": context,
"multiline": True,
"text_dir": text_dir,
}
base_pos = p0 + text_dir * 0.5
if not show_description_only:
length = (v1 - v0).length
text = self.format_value(context, length)
if isinstance(self, DiameterDecorator):
text = "D" + text
text = text_prefix + text + text_suffix
else:
if not description:
continue
text = description
self.draw_label(
context,
text,
p0 + text_dir * 0.5 + text_offset,
text_dir,
box_alignment="bottom-middle",
multiline=True,
multiline_to_bottom=False,
)
if description:
self.draw_label(
context,
description,
p0 + text_dir * 0.5 - text_offset,
text_dir,
box_alignment="top-middle",
multiline=True,
)
self.draw_label(
text=text,
pos=base_pos + text_offset,
box_alignment="bottom-middle",
multiline_to_bottom=False,
**common_label_attrs,
)
elif show_description_only and description:
if not show_description_only and description:
self.draw_label(
context,
description,
p0 + text_dir * 0.5 + text_offset,
text_dir,
box_alignment="bottom-middle",
multiline=True,
multiline_to_bottom=False,
text=description, pos=base_pos - text_offset, box_alignment="top-middle", **common_label_attrs
)
@@ -772,19 +786,23 @@ class AngleDecorator(BaseDecorator):
v0 = Vector(vertices[i0])
v1 = Vector(vertices[i1])
v2 = Vector(vertices[i1 + 1])
p0 = location_3d_to_region_2d(region, region3d, v0)
p1 = location_3d_to_region_2d(region, region3d, v1)
p2 = location_3d_to_region_2d(region, region3d, v2)
edge0 = p0 - p1
edge1 = p2 - p1
# calculate angle value
edge0_ws = v0 - v1
edge1_ws = v2 - v1
try:
cos_a = edge0.dot(edge1) / (edge0.length * edge1.length)
cos_a = edge0_ws.dot(edge1_ws) / (edge0_ws.length * edge1_ws.length)
except ZeroDivisionError:
continue
circle_angle_rad = acos(cos_a)
circle_angle = circle_angle_rad / pi * 180
# calculate angle position
p0 = location_3d_to_region_2d(region, region3d, v0)
p1 = location_3d_to_region_2d(region, region3d, v1)
p2 = location_3d_to_region_2d(region, region3d, v2)
edge0 = p0 - p1
edge1 = p2 - p1
base_edge = edge0 if ccw(p0, p1, p2) else edge1
text_offset = (Matrix.Rotation(-circle_angle_rad / 2, 2) @ base_edge).normalized() * ANGLE_LABEL_OFFSET
label_position = p1 + text_offset
@@ -824,12 +842,15 @@ class RadiusDecorator(BaseDecorator):
objecttype = "RADIUS"
def get_spline_end(self, obj):
def get_spline_points(self, obj):
spline = obj.data.splines[0]
spline_points = spline.bezier_points if spline.bezier_points else spline.points
if not spline_points:
return Vector((0, 0, 0))
return obj.matrix_world @ spline_points[0].co
spline_points = [Vector([0, 0, 0])] * 2
elif len(spline_points) == 1:
spline_points = [spline_points[0]] * 2
return [(obj.matrix_world @ p.co) for p in spline_points[:2]]
def decorate(self, context, obj):
self.draw_arrow(context, obj)
@@ -838,15 +859,24 @@ class RadiusDecorator(BaseDecorator):
def draw_labels(self, context, obj):
region = context.region
region3d = context.region_data
dir = Vector((1, 0))
pos = location_3d_to_region_2d(region, region3d, self.get_spline_end(obj))
spline_points = self.get_spline_points(obj)
spline = obj.data.splines[0]
spline_points = spline.bezier_points if spline.bezier_points else spline.points
if spline_points:
length = (spline_points[-1].co - spline_points[-2].co).length
text = self.format_value(context, length)
self.draw_label(context, text, pos, dir, gap=0, center=False, vcenter=False)
p0, p1 = [location_3d_to_region_2d(region, region3d, p) for p in self.get_spline_points(obj)]
element = tool.Ifc.get_entity(obj)
description = element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
viewportDrawingScale = self.get_viewport_drawing_scale(context)
text_offset = 20 * viewportDrawingScale
pos = p0 - (p1 - p0).normalized() * text_offset
def get_text():
length = (spline_points[-1] - spline_points[-2]).length
return "R" + self.format_value(context, length)
self.draw_dimension_text(
context, get_text, description, dimension_data, pos=pos, text_dir=Vector((1, 0)), box_alignment="center"
)
class FallDecorator(BaseDecorator):
@@ -1056,23 +1086,40 @@ class PlanLevelDecorator(BaseDecorator):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
region = context.region
region3d = context.region_data
element = tool.Ifc.get_entity(obj)
description = element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
for verts in splines:
p0, p1 = [location_3d_to_region_2d(region, region3d, v) for v in verts[:2]]
dir = p1 - p0
if dir.length < 1:
text_dir = p1 - p0
if text_dir.length < 1:
continue
if dir.x > 0:
if text_dir.x > 0:
box_alignment = "bottom-left"
else:
box_alignment = "bottom-right"
dir *= -1
text_dir *= -1
z = verts[0].z / unit_scale
z = ifcopenshell.util.geolocation.auto_z2e(tool.Ifc.get(), z)
z *= unit_scale
text = "RL " + self.format_value(context, z)
self.draw_label(context, text, p0, dir, gap=8, center=False, box_alignment=box_alignment)
def get_text():
z = verts[0].z
rl = self.format_value(context, z)
text = "{}{}".format("" if z < 0 else "+", rl)
return text
self.draw_dimension_text(
context,
get_text,
description,
dimension_data,
box_alignment=box_alignment,
pos=p0,
text_dir=text_dir,
gap=8,
center=False,
)
class SectionLevelDecorator(BaseDecorator):
@@ -1119,31 +1166,29 @@ class SectionLevelDecorator(BaseDecorator):
self.draw_labels(context, obj, self.get_splines(obj), text_position.to_2d(), text_dir.to_2d())
def draw_labels(self, context, obj, splines, text_position, text_dir):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
element = tool.Ifc.get_entity(obj)
storey = tool.Drawing.get_annotation_element(element)
tag = storey.Name if storey else element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
for verts in splines:
z = verts[0].z / unit_scale
z = ifcopenshell.util.geolocation.auto_z2e(tool.Ifc.get(), z)
z *= unit_scale
text_lines = ["RL " + self.format_value(context, z)]
if tag:
text_lines.append(tag)
def get_text():
z = verts[0].z
rl = self.format_value(context, z)
text = "RL {}{}".format("" if z < 0 else "+", rl)
return text
for line_i, line in zip((0, -1), text_lines):
self.draw_label(
context,
line,
text_position,
text_dir,
center=False,
vcenter=False,
line_no=line_i,
)
self.draw_dimension_text(
context,
get_text,
tag,
dimension_data,
box_alignment="bottom-left",
pos=text_position,
text_dir=text_dir,
)
break # support only 1 label
@@ -208,19 +208,19 @@ def format_distance(
if precision:
value = precision * round(float(value) / precision)
if decimal_places:
if decimal_places is not None:
fmt = "%1." + str(decimal_places) + "f"
# Meters
if unit_length == "METERS":
if not decimal_places:
if decimal_places is None:
fmt = "%1.3f"
if hide_units is False:
fmt += " m"
tx_dist = fmt % value
# Centimeters
elif unit_length == "CENTIMETERS":
if not decimal_places:
if decimal_places is None:
fmt = "%1.1f"
if hide_units is False:
fmt += " cm"
@@ -228,7 +228,7 @@ def format_distance(
tx_dist = fmt % d_cm
# Millimeters
elif unit_length == "MILLIMETERS":
if not decimal_places:
if decimal_places is None:
fmt = "%1.0f"
if hide_units is False:
fmt += " mm"
@@ -237,20 +237,20 @@ def format_distance(
# Otherwise Use Adaptive Units
else:
if round(value, 2) >= 1.0 and not decimal_places:
if round(value, 2) >= 1.0 and decimal_places is None:
fmt = "%1.3f"
if hide_units is False:
fmt += " m"
tx_dist = fmt % value
else:
if round(value, 2) >= 0.01 and not decimal_places:
if round(value, 2) >= 0.01 and decimal_places is None:
fmt = "%1.1f"
if hide_units is False:
fmt += " cm"
d_cm = value * (100)
tx_dist = fmt % d_cm
else:
if not decimal_places:
if decimal_places is None:
fmt = "%1.0f"
if hide_units is False:
fmt += " mm"
@@ -23,6 +23,7 @@ import json
import time
import bmesh
import shutil
import hashlib
import shapely
import subprocess
import webbrowser
@@ -32,6 +33,7 @@ import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.util.selector
import ifcopenshell.util.representation
import ifcopenshell.util.element
import blenderbim.bim.schema
import blenderbim.tool as tool
import blenderbim.core.drawing as core
@@ -84,6 +86,52 @@ class Operator:
return {"FINISHED"}
class AddAnnotationType(bpy.types.Operator, Operator):
bl_idname = "bim.add_annotation_type"
bl_label = "Add Annotation Type"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.scene.BIMAnnotationProperties
object_type = props.object_type
has_representation = props.create_representation_for_type
drawing = tool.Ifc.get_entity(bpy.context.scene.camera)
if props.create_representation_for_type:
obj = tool.Drawing.create_annotation_object(drawing, object_type)
else:
obj = bpy.data.objects.new(object_type, None)
obj.name = props.type_name
element = tool.Drawing.run_root_assign_class(
obj=obj,
ifc_class="IfcTypeProduct",
predefined_type=object_type,
should_add_representation=has_representation,
context=ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Annotation", "MODEL_VIEW"),
ifc_representation_class=tool.Drawing.get_ifc_representation_class(object_type),
)
element.ApplicableOccurrence = f"IfcAnnotation/{object_type}"
class EnableAddAnnotationType(bpy.types.Operator, Operator):
bl_idname = "bim.enable_add_annotation_type"
bl_label = "Enable Add Annotation Type"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
bpy.context.scene.BIMAnnotationProperties.is_adding_type = True
class DisableAddAnnotationType(bpy.types.Operator, Operator):
bl_idname = "bim.disable_add_annotation_type"
bl_label = "Disable Add Annotation Type"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
bpy.context.scene.BIMAnnotationProperties.is_adding_type = False
class AddDrawing(bpy.types.Operator, Operator):
bl_idname = "bim.add_drawing"
bl_label = "Add Drawing"
@@ -184,7 +232,8 @@ class CreateDrawing(bpy.types.Operator):
with profile("Drawing generation process"):
with profile("Initialize drawing generation process"):
self.cprops = self.camera.data.BIMCameraProperties
self.drawing_name = self.file.by_id(drawing_id).Name
self.drawing = self.file.by_id(drawing_id)
self.drawing_name = self.drawing.Name
self.metadata = tool.Drawing.get_drawing_metadata(self.camera_element)
self.get_scale(context)
if self.cprops.update_representation(self.camera):
@@ -275,7 +324,7 @@ class CreateDrawing(bpy.types.Operator):
return svg_path
def generate_underlay(self, context):
if not self.cprops.has_underlay:
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasUnderlay"):
return
svg_path = self.get_svg_path(cache_type="underlay")
context.scene.render.filepath = svg_path[0:-4] + ".png"
@@ -317,7 +366,7 @@ class CreateDrawing(bpy.types.Operator):
return svg_path
def generate_linework(self, context):
if not self.cprops.has_linework:
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"):
return
svg_path = self.get_svg_path(cache_type="linework")
if os.path.isfile(svg_path) and self.props.should_use_linework_cache:
@@ -346,10 +395,6 @@ class CreateDrawing(bpy.types.Operator):
}
cached_linework -= edited_guids
# This is a work in progress. See #1153 and #1564.
import hashlib
import ifcopenshell.draw
files = {context.scene.BIMProperties.ifc_file: tool.Ifc.get()}
for ifc_path, ifc in files.items():
@@ -520,7 +565,7 @@ class CreateDrawing(bpy.types.Operator):
for projection in projections:
boundary_lines = []
for path in projection.findall("./{http://www.w3.org/2000/svg}path"):
start, end = [co[1:].split(",") for co in path.attrib["d"].split()]
start, end = [[round(float(o), 1) for o in co[1:].split(",")] for co in path.attrib["d"].split()]
boundary_lines.append(shapely.LineString([start, end]))
unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines))
closed_polygons = shapely.polygonize(unioned_boundaries.geoms)
@@ -793,6 +838,7 @@ class CreateDrawing(bpy.types.Operator):
self.serialiser.setElevationRefGuid(self.camera_element.GlobalId)
self.serialiser.setScale(self.scale)
self.serialiser.setSubtractionSettings(ifcopenshell.ifcopenshell_wrapper.ALWAYS)
self.serialiser.setUsePrefiltering(True) # See #3359
# tree = ifcopenshell.geom.tree()
# This instructs the tree to explode BReps into faces and return
# the style of the face when running tree.select_ray()
@@ -846,6 +892,42 @@ class CreateDrawing(bpy.types.Operator):
classes.append("cut")
el.set("class", " ".join(classes))
# An element group will contain a bunch of paths representing the
# cut of that element. However IfcOpenShell may not correctly
# create closed paths. We post-process all paths with shapely to
# ensure things that should be closed (i.e.
# shapely.polygonize_full) are, and things which aren't are left
# alone (e.g. dangles, cuts, invalids). See #3421.
line_strings = []
old_paths = []
for path in el.findall("{http://www.w3.org/2000/svg}path"):
for subpath in path.attrib["d"].split("M")[1:]:
subpath = "M" + subpath.strip()
coords = [[round(float(o), 1) for o in co[1:].split(",")] for co in subpath.split()]
line_strings.append(shapely.LineString(coords))
old_paths.append(path)
unioned_line_strings = shapely.union_all(shapely.GeometryCollection(line_strings))
if hasattr(unioned_line_strings, "geoms"):
results = shapely.polygonize_full(unioned_line_strings.geoms)
else:
results = []
# If we succeeded in generating new path geometry, remove all the
# old paths and add new ones.
if results:
for path in old_paths:
path.getparent().remove(path)
for result in results:
for geom in result.geoms:
path = etree.SubElement(el, "path")
if isinstance(geom, shapely.Polygon):
d = "M" + " L".join([",".join([str(o) for o in co]) for co in geom.exterior.coords[0:-1]]) + " Z"
for interior in geom.interiors:
d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z"
elif isinstance(geom, shapely.LineString):
d = "M" + " L".join([",".join([str(o) for o in co]) for co in geom.coords]) + " Z"
path.attrib["d"] = d
# Architectural convention only merges these objects. E.g. pipe segments and fittings shouldn't merge.
if not element.is_a("IfcWall") and not element.is_a("IfcSlab"):
continue
@@ -921,7 +1003,7 @@ class CreateDrawing(bpy.types.Operator):
group.insert(0, projection)
def generate_annotation(self, context):
if not self.cprops.has_annotation:
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasAnnotation"):
return
svg_path = self.get_svg_path(cache_type="annotation")
if os.path.isfile(svg_path) and self.props.should_use_annotation_cache:
@@ -1264,7 +1346,25 @@ class ActivateModel(bpy.types.Operator):
CutDecorator.uninstall()
if not bpy.app.background:
bpy.ops.object.hide_view_clear()
view3d_context = dict()
for window in bpy.context.window_manager.windows:
screen = window.screen
for area in screen.areas:
if area.type == "VIEW_3D":
view3d_context["window"] = window
view3d_context["screen"] = screen
view3d_context["area"] = area
for region in area.regions:
if region.type == "WINDOW":
view3d_context["region"] = region
break
break
if view3d_context:
break
if view3d_context:
bpy.ops.object.hide_view_clear(view3d_context)
subcontext = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
@@ -1632,10 +1732,14 @@ class ActivateDrawingStyle(bpy.types.Operator, Operator):
space = self.get_view_3d(context) # Do not remove. It is used in exec later
style = json.loads(self.drawing_style.raster_style)
for path, value in style.items():
if isinstance(value, str):
exec(f"{path} = '{value}'")
else:
exec(f"{path} = {value}")
try:
if isinstance(value, str):
exec(f"{path} = '{value}'")
else:
exec(f"{path} = {value}")
except:
# Differences in Blender versions mean result in failures here
print(f"Failed to set shading style {path} to {value}")
def set_query(self, context):
self.include_global_ids = []
@@ -2273,6 +2377,34 @@ class DisableEditingDrawings(bpy.types.Operator, Operator):
core.disable_editing_drawings(tool.Drawing)
class ExpandTargetView(bpy.types.Operator):
bl_idname = "bim.expand_target_view"
bl_label = "Expand Target View"
bl_options = {"REGISTER", "UNDO"}
target_view: bpy.props.StringProperty()
def execute(self, context):
props = context.scene.DocProperties
for drawing in [d for d in props.drawings if d.target_view == self.target_view]:
drawing.is_expanded = True
core.load_drawings(tool.Drawing)
return {"FINISHED"}
class ContractTargetView(bpy.types.Operator):
bl_idname = "bim.contract_target_view"
bl_label = "Contract Target View"
bl_options = {"REGISTER", "UNDO"}
target_view: bpy.props.StringProperty()
def execute(self, context):
props = context.scene.DocProperties
for drawing in [d for d in props.drawings if d.target_view == self.target_view]:
drawing.is_expanded = False
core.load_drawings(tool.Drawing)
return {"FINISHED"}
class ExpandSheet(bpy.types.Operator):
bl_idname = "bim.expand_sheet"
bl_label = "Expand Sheet"
@@ -165,8 +165,9 @@ def get_diagram_scales(self, context):
def update_drawing_name(self, context):
drawing = tool.Ifc.get().by_id(self.ifc_definition_id)
core.update_drawing_name(tool.Ifc, tool.Drawing, drawing=drawing, name=self.name)
if self.ifc_definition_id:
drawing = tool.Ifc.get().by_id(self.ifc_definition_id)
core.update_drawing_name(tool.Ifc, tool.Drawing, drawing=drawing, name=self.name)
def get_drawing_style_name(self):
@@ -253,6 +254,8 @@ class Drawing(PropertyGroup):
name: StringProperty(name="Name", update=update_drawing_name)
target_view: StringProperty(name="Target View")
is_selected: BoolProperty(name="Is Selected", default=True)
is_drawing: BoolProperty(name="Is Drawing", default=False)
is_expanded: BoolProperty(name="Is Expanded", default=True)
class Document(PropertyGroup):
@@ -297,6 +300,7 @@ class RasterStyleProperty(enum.Enum):
SHADING = "scene.display.shading"
DISPLAY = "scene.display"
OVERLAY = "space.overlay"
SPACE_SHADING = "space.shading"
RASTER_STYLE_PROPERTIES_EXCLUDE = (
@@ -499,7 +503,7 @@ ANNOTATION_TYPES_DATA = {
"PLAN_LEVEL": ("Level (Plan)", "", "SORTBYEXT", "curve"),
"SECTION_LEVEL": ("Level (Section)", "", "TRIA_DOWN", "curve"),
"BREAKLINE": ("Breakline", "", "FCURVE", "mesh"),
"LINEWORK": ("Line", "", "MESH_MONKEY", "mesh"),
"LINEWORK": ("Line", "", "SNAP_MIDPOINT", "mesh"),
"BATTING": ("Batting", "Add batting annotation.\nThickness could be changed through Thickness property of BBIM_Batting property set", "FORCE_FORCE", "mesh"),
"REVISION_CLOUD":("Revision Cloud", "Add revision cloud", "VOLUME_DATA", "mesh"),
"FILL_AREA": ("Fill Area", "", "NODE_TEXTURE", "mesh"),
@@ -510,13 +514,10 @@ ANNOTATION_TYPES_DATA = {
annotation_classes = [(x, *ANNOTATION_TYPES_DATA[x][:3], i) for i, x in enumerate(ANNOTATION_TYPES_DATA)]
def get_annotation_data_prop(prop_name):
def function(self, context):
if not AnnotationData.is_loaded:
AnnotationData.load()
return AnnotationData.data[prop_name]
return function
def get_relating_type_id(self, context):
if not AnnotationData.is_loaded:
AnnotationData.load()
return AnnotationData.data["relating_type_id"]
def update_annotation_object_type(self, context):
@@ -533,12 +534,12 @@ class BIMAnnotationProperties(PropertyGroup):
object_type: bpy.props.EnumProperty(
name="Annotation Object Type", items=annotation_classes, default="TEXT", update=update_annotation_object_type
)
relating_type_id: bpy.props.EnumProperty(
name="Relating Annotation Type", items=get_annotation_data_prop("relating_types")
)
relating_type_id: bpy.props.EnumProperty(name="Relating Annotation Type", items=get_relating_type_id)
create_representation_for_type: bpy.props.BoolProperty(
name="Create Representation For Type",
default=False,
description='Whether "Add type" should define a representation for the type \n'
"or allow occurences to have their own",
)
is_adding_type: bpy.props.BoolProperty(default=False)
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
@@ -310,10 +310,13 @@ class SheetBuilder:
brackets_level -= 1
text += l
# replace urls url(#marker) with url(#prefix-marker)
# since url(#marker.prefix) doesn't seem to work
text = re.sub(r"url\(#([^\)]+)\)", rf"url(#{prefix}-\1)", text)
style.text = text
def replace_urls(text):
"""replace urls `url(#marker)` with `url(#prefix-marker)`
since `url(#marker.prefix)` doesn't seem to work
"""
return re.sub(r"url\(#([^\)]+)\)", rf"url(#{prefix}-\1)", text)
style.text = replace_urls(text)
for svg_element in svg.findall(f".//*"):
if svg_element.tag in (f"{SVG}style", f"{SVG}svg"):
@@ -325,6 +328,9 @@ class SheetBuilder:
# add class "prefix" to all classes
if "class" in attrib:
attrib["class"] += f" {prefix}"
if "filter" in attrib:
# example use "#fill-background" filter
attrib["filter"] = replace_urls(attrib["filter"])
if svg_element.tag == f"{SVG}use":
href_attrib = f"{XLINK}href"
if href_attrib in attrib:
@@ -100,6 +100,9 @@ class SvgWriter:
if not os.path.exists(resource_path):
resource_basename = os.path.basename(resource_path)
ootb_resource = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", resource_basename)
print(
f"WARNING. Couldn't find {resource} for the drawing by the path: {resource_path}. Default BBIM resource will be copied from {ootb_resource}"
)
if os.path.exists(ootb_resource):
shutil.copy(ootb_resource, resource_path)
self.resource_paths[resource] = resource_path
@@ -121,23 +124,35 @@ class SvgWriter:
self.height = self.raw_height * self.svg_scale
def add_stylesheet(self):
if not self.resource_paths["Stylesheet"] or not os.path.exists(self.resource_paths["Stylesheet"]):
path = self.resource_paths["Stylesheet"]
if not path:
return
with open(self.resource_paths["Stylesheet"], "r") as stylesheet:
if not os.path.exists(path):
print(f"WARNING. Couldn't find stylesheet for the drawing by the path: {path}")
return
with open(path, "r") as stylesheet:
self.svg.defs.add(self.svg.style(stylesheet.read()))
def add_markers(self):
if not self.resource_paths["Markers"] or not os.path.exists(self.resource_paths["Markers"]):
path = self.resource_paths["Markers"]
if not path:
return
tree = ET.parse(self.resource_paths["Markers"])
if not os.path.exists(path):
print(f"WARNING. Couldn't find markers for the drawing by the path: {path}")
return
tree = ET.parse(path)
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
def add_symbols(self):
if not self.resource_paths["Symbols"] or not os.path.exists(self.resource_paths["Symbols"]):
path = self.resource_paths["Symbols"]
if not path:
return
tree = ET.parse(self.resource_paths["Symbols"])
if not os.path.exists(path):
print(f"WARNING. Couldn't find symbols for the drawing by the path: {path}")
return
tree = ET.parse(path)
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
@@ -148,9 +163,15 @@ class SvgWriter:
return External(xml_symbol) if xml_symbol else None
def add_patterns(self):
if not self.resource_paths["Patterns"] or not os.path.exists(self.resource_paths["Patterns"]):
path = self.resource_paths["Patterns"]
if not path:
return
tree = ET.parse(self.resource_paths["Patterns"])
if not os.path.exists(path):
print(f"WARNING. Couldn't find patterns for the drawing by the path: {path}")
return
if not path or not os.path.exists(path):
return
tree = ET.parse(path)
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
@@ -200,51 +221,42 @@ class SvgWriter:
return self
def draw_section_level_annotation(self, obj):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
offset = Vector([self.raw_width, self.raw_height]) / 2
matrix_world = obj.matrix_world
classes = self.get_attribute_classes(obj)
element = tool.Ifc.get_entity(obj)
storey = tool.Drawing.get_annotation_element(element)
tag = storey.Name if storey else element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
suppress_zero_inches = dimension_data["suppress_zero_inches"]
base_offset_y = 3.5
for spline in obj.data.splines:
points = self.get_spline_points(spline)
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
d = " ".join(
[
"L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
for p in projected_points
]
)
projected_points_svg = [(offset + p.xy * Vector((1, -1))) * self.svg_scale for p in projected_points]
d = " ".join(["L {} {}".format(*p) for p in projected_points_svg])
d = "M{}".format(d[1:])
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
text_position = Vector(
(
(x_offset + projected_points[0].x) * self.svg_scale,
((y_offset - projected_points[0].y) * self.svg_scale) - 3.5,
)
)
text_position = projected_points_svg[0] - Vector((0, base_offset_y))
vector = projected_points_svg[0] - projected_points_svg[1]
angle = math.degrees(vector.angle_signed(Vector((1, 0))))
# TODO: allow metric to be configurable
rl_value = (matrix_world @ points[0].co.xyz).z
if bpy.context.scene.unit_settings.system == "IMPERIAL":
rl = helper.format_distance(rl_value, precision=self.precision, decimal_places=self.decimal_places)
else:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
rl = rl_value / unit_scale
rl = ifcopenshell.util.geolocation.auto_z2e(tool.Ifc.get(), rl)
rl *= unit_scale
rl = "{:.3f}m".format(rl)
text_style = SvgWriter.get_box_alignment_parameters("bottom-left")
self.svg.add(
self.svg.text(
"RL {}{}".format("" if rl_value < 0 else "+", rl),
insert=tuple(text_position),
class_="SECTIONLEVEL",
**text_style,
def get_text():
z = (matrix_world @ points[0].co.xyz).z
rl = helper.format_distance(
z,
precision=self.precision,
decimal_places=self.decimal_places,
suppress_zero_inches=suppress_zero_inches,
)
text = "RL {}{}".format("" if z < 0 else "+", rl)
return text
self.draw_dimension_text(
get_text, tag, dimension_data, text_position=text_position, angle=angle, class_str="SECTIONLEVEL"
)
if tag:
self.svg.add(self.svg.text(tag, insert=(text_position[0], text_position[1] - 5), **text_style))
def draw_stair_annotation(self, obj):
x_offset = self.raw_width / 2
@@ -705,15 +717,19 @@ class SvgWriter:
drawing = tool.Drawing.get_annotation_element(element)
reference = tool.Drawing.get_drawing_reference(drawing)
if reference:
sheet = tool.Drawing.get_reference_document(reference)
if sheet:
if tool.Ifc.get_schema() == "IFC2X3":
reference_id = reference.ItemReference or "-"
sheet_id = sheet.DocumentId or "-"
else:
reference_id = reference.Identification or "-"
sheet_id = sheet.Identification or "-"
return (reference_id, sheet_id)
for sheet_reference in tool.Ifc.get().by_type("IfcDocumentReference"):
if sheet_reference.Description != "DRAWING" or sheet_reference.Location != reference.Location:
continue
sheet = tool.Drawing.get_reference_document(sheet_reference)
if sheet:
if tool.Ifc.get_schema() == "IFC2X3":
reference_id = sheet_reference.ItemReference or "-"
sheet_id = sheet.DocumentId or "-"
else:
reference_id = sheet_reference.Identification or "-"
sheet_id = sheet.Identification or "-"
return (reference_id, sheet_id)
break
return ("-", "-")
@staticmethod
@@ -807,45 +823,19 @@ class SvgWriter:
line_number = 0
for text_literal in text_literals:
# after pretty indentation some redundant spaces can occur in svg tags
# this is why we apply "font-size: 0;" to the text tag to remove those spaces
# and add clases to the tspan tags
# ref: https://github.com/IfcOpenShell/IfcOpenShell/issues/2833#issuecomment-1471584960
text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product)
attribs = {
"transform": text_transform,
"style": "font-size: 0;",
}
def add_text_tag(add_fill_bg):
nonlocal line_number
text_tag = self.svg.text(
"",
**(attribs | {"filter": "url(#fill-background)"}) if add_fill_bg else attribs,
**SvgWriter.get_box_alignment_parameters(text_literal.BoxAlignment),
)
self.svg.add(text_tag)
text_lines = text.replace("\\n", "\n").split("\n")
for text_line in text_lines:
# position has to be inserted at tspan to avoid x offset between tspans
# note that tspan doesn't support using `transform` attribute
# so we use (0,0) position because tspan is already offseted by text transform
tspan = self.svg.tspan(text_line, class_=classes_str, insert=(0, 0))
# doing it here and not in tspan constructor because constructor adds unnecessary spaces
tspan.update({"dy": f"{line_number}em"})
text_tag.add(tspan)
line_number += 1
if add_fill_bg:
# return line_number back to the original value
line_number -= len(text_lines)
if "fill-bg" in classes:
add_text_tag(True)
add_text_tag(False)
text_tags = self.create_text_tag(
text,
text_position_svg,
angle,
text_literal.BoxAlignment,
classes_str,
fill_bg="fill-bg" in classes,
line_number_start=line_number,
)
for tag in text_tags:
self.svg.add(tag)
line_number += len(tag.elements)
def draw_break_annotations(self, obj):
x_offset = self.raw_width / 2
@@ -871,53 +861,60 @@ class SvgWriter:
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
def draw_plan_level_annotation(self, obj):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
offset = Vector([self.raw_width, self.raw_height]) / 2
matrix_world = obj.matrix_world
classes = self.get_attribute_classes(obj)
element = tool.Ifc.get_entity(obj)
description = element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
suppress_zero_inches = dimension_data["suppress_zero_inches"]
base_offset_y = 1.0
for spline in obj.data.splines:
points = self.get_spline_points(spline)
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
d = " ".join(
[
"L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
for p in projected_points
]
)
projected_points_svg = [(offset + p.xy * Vector((1, -1))) * self.svg_scale for p in projected_points]
d = " ".join(["L {} {}".format(*p) for p in projected_points_svg])
d = "M{}".format(d[1:])
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
text_position = Vector(
(
(x_offset + projected_points[0].x) * self.svg_scale,
((y_offset - projected_points[0].y) * self.svg_scale) - 1.0,
)
)
# TODO: allow metric to be configurable
rl_value = (matrix_world @ points[0].co).z
if bpy.context.scene.unit_settings.system == "IMPERIAL":
rl = helper.format_distance(rl_value, precision=self.precision, decimal_places=self.decimal_places)
text_position = projected_points_svg[0] - Vector((0, base_offset_y))
text_dir = projected_points_svg[1] - projected_points_svg[0]
if text_dir.x < 0:
box_alignment = "bottom-right"
text_dir *= -1
else:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
rl = rl_value / unit_scale
rl = ifcopenshell.util.geolocation.auto_z2e(tool.Ifc.get(), rl)
rl *= unit_scale
rl = "{:.3f}m".format(rl)
box_alignment = "bottom-left"
angle = math.degrees(text_dir.angle_signed(Vector((1, 0))))
box_alignment = "bottom-left" if projected_points[0].x <= projected_points[-1].x else "bottom-right"
text_style = SvgWriter.get_box_alignment_parameters(box_alignment)
self.svg.add(
self.svg.text(
"{}{}".format("" if rl_value < 0 else "+", rl),
insert=tuple(text_position),
class_="PLANLEVEL",
**text_style,
# TODO: allow metric to be configurable
def get_text():
z = (matrix_world @ points[0].co.xyz).z
rl = helper.format_distance(
z,
precision=self.precision,
decimal_places=self.decimal_places,
suppress_zero_inches=suppress_zero_inches,
)
text = "{}{}".format("" if z < 0 else "+", rl)
return text
self.draw_dimension_text(
get_text,
description,
dimension_data,
text_position=text_position,
angle=angle,
class_str="PLANLEVEL",
box_alignment=box_alignment,
)
def draw_angle_annotations(self, obj):
points = obj.data.splines[0].points
region = bpy.context.region
region_3d = bpy.context.area.spaces.active.region_3d
area = tool.Blender.get_viewport_context()["area"]
region_3d = area.spaces.active.region_3d
points_chunked = [points[i : i + 3] for i in range(len(points) - 2)]
for points_chunk in points_chunked:
@@ -1051,48 +1048,61 @@ class SvgWriter:
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
def draw_radius_annotations(self, obj):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
offset = Vector([self.raw_width, self.raw_height]) / 2
classes = self.get_attribute_classes(obj)
element = tool.Ifc.get_entity(obj)
tag = element.Description
matrix_world = obj.matrix_world
dimension_data = DecoratorData.get_dimension_data(obj)
for spline in obj.data.splines:
points = self.get_spline_points(spline)
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
d = " ".join(
[
"L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
for p in projected_points
]
)
projected_points_svg = [(offset + p.xy * Vector((1, -1))) * self.svg_scale for p in projected_points]
d = " ".join(["L {} {}".format(*p) for p in projected_points_svg])
d = "M{}".format(d[1:])
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
p0 = projected_points_svg[0]
p1 = projected_points_svg[1]
p0 = Vector(
(
(x_offset + projected_points[0].x) * self.svg_scale,
(y_offset - projected_points[0].y) * self.svg_scale,
)
)
p1 = Vector(
(
(x_offset + projected_points[1].x) * self.svg_scale,
(y_offset - projected_points[1].y) * self.svg_scale,
)
)
text_offset = (p0 - p1).xy.normalized() * 5
text_position = projected_points[0]
text_position = Vector(
((x_offset + text_position.x) * self.svg_scale, (y_offset - text_position.y) * self.svg_scale)
)
text_position += text_offset
text_offset = (p0 - p1).normalized() * 5
text_position = p0 + text_offset
text_style = SvgWriter.get_box_alignment_parameters("center")
radius = (points[-1].co - points[-2].co).length
radius = helper.format_distance(radius, precision=self.precision, decimal_places=self.decimal_places)
tag = element.Description or f"R{radius}"
def get_text():
radius = (points[-1].co - points[-2].co).length
radius = helper.format_distance(radius, precision=self.precision, decimal_places=self.decimal_places)
text = f"R{radius}"
return text
self.svg.add(self.svg.text(tag, insert=tuple(text_position), class_="RADIUS", **text_style))
self.draw_dimension_text(
get_text, tag, dimension_data, text_position=text_position, class_str="RADIUS", box_alignment="center"
)
def draw_dimension_text(self, get_text, tag, dimension_data, **create_text_kwargs):
prefix = dimension_data["text_prefix"]
suffix = dimension_data["text_suffix"]
show_description_only = dimension_data["show_description_only"]
fill_bg = dimension_data["fill_bg"]
text_tags = []
line_number_start = 0
if not show_description_only:
text = get_text()
full_prefix = ((tag + "\\n") if tag else "") + prefix
text = full_prefix + text + suffix
line_number_start -= full_prefix.count("\\n")
else:
if not tag:
return
text = tag
text_tags += self.create_text_tag(
text, line_number_start=line_number_start, fill_bg=fill_bg, **create_text_kwargs
)
for text in text_tags:
self.svg.add(text)
def draw_fall_annotations(self, obj):
x_offset = self.raw_width / 2
@@ -1187,6 +1197,7 @@ class SvgWriter:
suppress_zero_inches=dimension_data["suppress_zero_inches"],
text_prefix=dimension_data["text_prefix"],
text_suffix=dimension_data["text_suffix"],
fill_bg=dimension_data["fill_bg"],
)
def draw_dimension_annotations(self, obj):
@@ -1210,6 +1221,7 @@ class SvgWriter:
suppress_zero_inches=dimension_data["suppress_zero_inches"],
text_prefix=dimension_data["text_prefix"],
text_suffix=dimension_data["text_suffix"],
fill_bg=dimension_data["fill_bg"],
)
def draw_measureit_arch_dimension_annotations(self):
@@ -1235,6 +1247,7 @@ class SvgWriter:
suppress_zero_inches=False,
text_prefix="",
text_suffix="",
fill_bg=False,
):
offset = Vector([self.raw_width, self.raw_height]) / 2
v0 = self.project_point_onto_camera(v0_global)
@@ -1244,13 +1257,6 @@ class SvgWriter:
mid = ((end - start) / 2) + start
vector = end - start
perpendicular = Vector((vector.y, -vector.x)).normalized()
dimension = (v1_global - v0_global).length
dimension = helper.format_distance(
dimension,
precision=self.precision,
decimal_places=self.decimal_places,
suppress_zero_inches=suppress_zero_inches,
)
sheet_dimension = (end - start).length
# if annotation can't fit offset text to the right of marker
@@ -1260,81 +1266,109 @@ class SvgWriter:
line = self.svg.line(start=start, end=end, class_=" ".join(classes))
self.svg.add(line)
if not show_description_only:
text = f"{text_prefix}{str(dimension)}{text_suffix}"
text_tag = self.create_text_tag(
text,
text_position + perpendicular,
angle,
"bottom-middle",
"DIMENSION",
text_format=text_format,
multiline=True,
multiline_to_bottom=False,
)
self.svg.add(text_tag)
if dimension_text:
text_tag = self.create_text_tag(
dimension_text,
text_position - perpendicular,
angle,
"top-middle",
"DIMENSION",
text_format=text_format,
multiline=True,
multiline_to_bottom=True,
)
self.svg.add(text_tag)
text_tags = []
text_tag_kwargs = {
"angle": angle,
"class_str": "DIMENSION",
"text_format": text_format,
"fill_bg": fill_bg,
}
elif show_description_only and dimension_text:
text_tag = self.create_text_tag(
dimension_text,
text_position + perpendicular,
angle,
"bottom-middle",
"DIMENSION",
text_format=text_format,
multiline=True,
multiline_to_bottom=False,
if not show_description_only:
dimension = (v1_global - v0_global).length
dimension = helper.format_distance(
dimension,
precision=self.precision,
decimal_places=self.decimal_places,
suppress_zero_inches=suppress_zero_inches,
)
self.svg.add(text_tag)
text = text_prefix + str(dimension) + text_suffix
else:
if not dimension_text:
return
text = dimension_text
text_tags += self.create_text_tag(
text,
text_position + perpendicular,
box_alignment="bottom-middle",
multiline_to_bottom=False,
**text_tag_kwargs,
)
if not show_description_only and dimension_text:
text_tags += self.create_text_tag(
dimension_text,
text_position - perpendicular,
box_alignment="top-middle",
multiline_to_bottom=True,
**text_tag_kwargs,
)
for tag in text_tags:
self.svg.add(tag)
def create_text_tag(
self,
text,
text_position,
angle,
box_alignment,
class_str,
angle=0.0,
box_alignment="bottom-left",
class_str="",
text_format=lambda x: x,
multiline=False,
multiline_to_bottom=False,
multiline=True,
multiline_to_bottom=True,
fill_bg=False,
line_number_start=0,
_draw_fill_bg=False,
):
"""returns list of created text tags"""
text_tags = []
if fill_bg:
method_kwargs = locals() | {"_draw_fill_bg": True, "fill_bg": False}
del method_kwargs["self"]
del method_kwargs["text_tags"]
text_tags += self.create_text_tag(**method_kwargs)
base_text_attrs = SvgWriter.get_box_alignment_parameters(box_alignment)
base_text_attrs = base_text_attrs | ({"filter": "url(#fill-background)"} if _draw_fill_bg else {})
if not multiline:
text_kwargs = {"transform": "rotate({} {} {})".format(angle, text_position.x, text_position.y)}
return self.svg.text(
transform_kwargs = {"transform": "rotate({} {} {})".format(angle, text_position.x, text_position.y)}
text_tag = self.svg.text(
text_format(text),
insert=text_position,
class_=class_str,
**(text_kwargs | SvgWriter.get_box_alignment_parameters(box_alignment)),
**(transform_kwargs | base_text_attrs),
)
text_tags.append(text_tag)
return text_tags
text_position_svg_str = ", ".join(map(str, text_position))
text_transform = f"translate({text_position_svg_str}) rotate({angle})"
# after pretty indentation some redundant spaces can occur in svg tags
# this is why we apply "font-size: 0;" to the text tag to remove those spaces
# and add clases to the tspan tags
# ref: https://github.com/IfcOpenShell/IfcOpenShell/issues/2833#issuecomment-1471584960
text_kwargs = {
"transform": text_transform,
"style": "font-size: 0;",
}
text_tag = self.svg.text("", **text_kwargs, **SvgWriter.get_box_alignment_parameters(box_alignment))
text_tag = self.svg.text("", **text_kwargs, **base_text_attrs)
text_tags.append(text_tag)
text_lines = text.replace("\\n", "\n").split("\n")
text_lines = text_lines if multiline_to_bottom else text_lines[::-1]
for line_number, text_line in enumerate(text_lines):
for line_number, text_line in enumerate(text_lines, line_number_start):
# position has to be inserted at tspan to avoid x offset between tspans
# note that tspan doesn't support using `transform` attribute
# so we use (0,0) position because tspan is already offseted by text transform
tspan = self.svg.tspan(text_format(text_line), class_=class_str, insert=(0, 0))
# doing it here and not in tspan constructor because constructor adds unnecessary spaces
tspan.update({"dy": f"{line_number if multiline_to_bottom else -line_number}em"})
text_tag.add(tspan)
return text_tag
return text_tags
def project_point_onto_camera(self, point):
# TODO is this needlessly complex?
@@ -161,13 +161,13 @@ class BIM_PT_drawing_underlay(Panel):
class BIM_PT_drawings(Panel):
bl_label = "Drawings"
bl_idname = "BIM_PT_drawings"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "BIM Documentation"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
@classmethod
def poll(cls, context):
return tool.Ifc.get()
return tool.Blender.is_tab(context, "DRAWINGS") and tool.Ifc.get()
def draw(self, context):
if not DrawingsData.is_loaded:
@@ -199,18 +199,31 @@ class BIM_PT_drawings(Panel):
col.alignment = "LEFT"
row2 = col.row(align=True)
row2.operator("bim.remove_drawing", icon="X", text="").drawing = active_drawing.ifc_definition_id
row2.operator(
"bim.duplicate_drawing", icon="COPYDOWN", text=""
).drawing = active_drawing.ifc_definition_id
col = row.column()
col.alignment = "RIGHT"
op = row.operator("bim.select_all_drawings", icon="SELECT_SUBTRACT", text="")
op = row.operator("bim.open_drawing", icon="URL", text="")
open_drawing_button = row.row(align=True)
op = open_drawing_button.operator("bim.open_drawing", icon="URL", text="")
op.view = active_drawing.name
open_drawing_button.enabled = active_drawing.ifc_definition_id > 0
row.operator("bim.activate_model", icon="VIEW3D", text="")
op = row.operator("bim.activate_drawing", icon="OUTLINER_OB_CAMERA", text="")
drawing_button = row.row(align=True)
op = drawing_button.operator("bim.activate_drawing", icon="OUTLINER_OB_CAMERA", text="")
op.drawing = active_drawing.ifc_definition_id
row.operator("bim.create_drawing", text="", icon="OUTPUT")
drawing_button.enabled = active_drawing.ifc_definition_id > 0
create_drawing_button = row.row(align=True)
create_drawing_button.operator("bim.create_drawing", text="", icon="OUTPUT")
create_drawing_button.enabled = active_drawing.ifc_definition_id > 0
self.layout.template_list(
"BIM_UL_drawinglist", "", self.props, "drawings", self.props, "active_drawing_index"
)
@@ -229,13 +242,13 @@ class BIM_PT_drawings(Panel):
class BIM_PT_schedules(Panel):
bl_label = "Schedules"
bl_idname = "BIM_PT_schedules"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "BIM Documentation"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
@classmethod
def poll(cls, context):
return tool.Ifc.get()
return tool.Blender.is_tab(context, "DRAWINGS") and tool.Ifc.get()
def draw(self, context):
if not DocumentsData.is_loaded:
@@ -276,21 +289,18 @@ class BIM_PT_schedules(Panel):
def draw_project_not_saved_ui(self):
row = self.layout.row()
row.label(text="Project Not Yet Saved", icon="ERROR")
row = self.layout.row()
op = row.operator("export_ifc.bim", icon="EXPORT", text="Save Project")
op.should_save_as = False
class BIM_PT_references(Panel):
bl_label = "References"
bl_idname = "BIM_PT_references"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "BIM Documentation"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
@classmethod
def poll(cls, context):
return tool.Ifc.get()
return tool.Blender.is_tab(context, "DRAWINGS") and tool.Ifc.get()
def draw(self, context):
if not DocumentsData.is_loaded:
@@ -328,13 +338,13 @@ class BIM_PT_references(Panel):
class BIM_PT_sheets(Panel):
bl_label = "Sheets"
bl_idname = "BIM_PT_sheets"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "BIM Documentation"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
@classmethod
def poll(cls, context):
return tool.Ifc.get()
return tool.Blender.is_tab(context, "DRAWINGS") and tool.Ifc.get()
def draw(self, context):
if not SheetsData.is_loaded:
@@ -489,95 +499,22 @@ class BIM_PT_text(Panel):
row.label(text=literal_data[attribute])
class BIM_PT_annotation_utilities(Panel):
bl_idname = "BIM_PT_annotation_utilities"
bl_label = "Annotation"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "BIM Documentation"
def draw(self, context):
layout = self.layout
self.props = context.scene.DocProperties
row = layout.row(align=True)
op = row.operator("bim.add_annotation", text="Dimension", icon="FIXED_SIZE")
op.object_type = "DIMENSION"
op.data_type = tool.Drawing.get_annotation_data_type(op.object_type)
op = row.operator("bim.add_annotation", text="Angle", icon="DRIVER_ROTATIONAL_DIFFERENCE")
op.object_type = "ANGLE"
op.data_type = tool.Drawing.get_annotation_data_type(op.object_type)
row = layout.row(align=True)
op = row.operator("bim.add_annotation", text="Radius", icon="FORWARD")
op.object_type = "RADIUS"
op.data_type = tool.Drawing.get_annotation_data_type(op.object_type)
op = row.operator("bim.add_annotation", text="Diameter", icon="ARROW_LEFTRIGHT")
op.object_type = "DIAMETER"
op.data_type = tool.Drawing.get_annotation_data_type(op.object_type)
row = layout.row(align=True)
op = row.operator("bim.add_annotation", text="Text", icon="SMALL_CAPS")
op.object_type = "TEXT"
op.data_type = tool.Drawing.get_annotation_data_type(op.object_type)
op = row.operator("bim.add_annotation", text="Leader", icon="TRACKING_BACKWARDS")
op.object_type = "TEXT_LEADER"
op.data_type = tool.Drawing.get_annotation_data_type(op.object_type)
row = layout.row(align=True)
op = row.operator("bim.add_annotation", text="Stair Arrow", icon="SCREEN_BACK")
op.object_type = "STAIR_ARROW"
op.data_type = tool.Drawing.get_annotation_data_type(op.object_type)
op = row.operator("bim.add_annotation", text="Hidden", icon="CON_TRACKTO")
op.object_type = "HIDDEN_LINE"
op.data_type = tool.Drawing.get_annotation_data_type(op.object_type)
row = layout.row(align=True)
op = row.operator("bim.add_annotation", text="Level (Plan)", icon="SORTBYEXT")
op.object_type = "PLAN_LEVEL"
op.data_type = tool.Drawing.get_annotation_data_type(op.object_type)
op = row.operator("bim.add_annotation", text="Level (Section)", icon="TRIA_DOWN")
op.object_type = "SECTION_LEVEL"
op.data_type = tool.Drawing.get_annotation_data_type(op.object_type)
row = layout.row(align=True)
op = row.operator("bim.add_annotation", text="Breakline", icon="FCURVE")
op.object_type = "BREAKLINE"
op.data_type = tool.Drawing.get_annotation_data_type(op.object_type)
op = row.operator("bim.add_annotation", text="Line", icon="MESH_MONKEY")
op.object_type = "LINEWORK"
op.data_type = tool.Drawing.get_annotation_data_type(op.object_type)
row = layout.row(align=True)
op = row.operator("bim.add_annotation", text="Batting", icon="FORCE_FORCE")
op.object_type = "BATTING"
op.data_type = tool.Drawing.get_annotation_data_type(op.object_type)
op.description = "Add batting annotation.\nThickness could be changed through Thickness property of BBIM_Batting property set"
op = row.operator("bim.add_annotation", text="Fill Area", icon="NODE_TEXTURE")
op.object_type = "FILL_AREA"
row = layout.row(align=True)
op = row.operator("bim.add_annotation", text="Fall", icon="SORT_ASC")
op.object_type = "FALL"
op.data_type = tool.Drawing.get_annotation_data_type(op.object_type)
op = row.operator("bim.add_annotation", text="Revision", icon="VOLUME_DATA")
op.object_type = "REVISION_CLOUD"
op.data_type = tool.Drawing.get_annotation_data_type(op.object_type)
row = layout.row(align=True)
row.prop(self.props, "should_draw_decorations", text="Viewport Annotations")
row.enabled = context.scene.camera is not None
class BIM_UL_drawinglist(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
if not item:
layout.label(text="", translate=False)
return
row = layout.row(align=True)
if item.is_drawing:
row.label(text="", icon="BLANK1")
selected_icon = "CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT"
row.prop(item, "is_selected", text="", icon=selected_icon)
icon = "UV_FACESEL"
if item.target_view == "ELEVATION_VIEW":
row.prop(item, "is_selected", text="", icon=selected_icon, emboss=False)
row.prop(item, "name", text="", emboss=False)
else:
if item.target_view == "PLAN_VIEW":
icon = "UV_FACESEL"
elif item.target_view == "ELEVATION_VIEW":
icon = "UV_VERTEXSEL"
elif item.target_view == "SECTION_VIEW":
icon = "UV_EDGESEL"
@@ -585,9 +522,17 @@ class BIM_UL_drawinglist(bpy.types.UIList):
icon = "XRAY"
elif item.target_view == "MODEL_VIEW":
icon = "SNAP_VOLUME"
else:
icon = "CLIPUV_HLT"
if item.is_expanded:
row.operator(
"bim.contract_target_view", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN"
).target_view = item.target_view
else:
row.operator(
"bim.expand_target_view", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT"
).target_view = item.target_view
row.prop(item, "name", text="", icon=icon, emboss=False)
else:
layout.label(text="", translate=False)
class BIM_UL_sheets(bpy.types.UIList):
@@ -626,3 +571,31 @@ class BIM_UL_sheets(bpy.types.UIList):
else:
name = item.name or "Unnamed"
row.label(text=name)
def draw_filter(self, context, layout):
# We only need filtering, not reordering for sheets.
row = layout.row(align=True)
row.prop(self, "filter_name", text="")
row.prop(self, "use_filter_invert", text="", icon="ARROW_LEFTRIGHT")
def filter_items(self, context, data, propname):
flt_flags = []
flt_neworder = []
if self.filter_name:
filter_name = self.filter_name.lower()
active_sheet = None
for sheet in data.sheets:
if sheet.is_sheet:
active_sheet = sheet
active_sheet_index = len(flt_flags)
if filter_name in sheet.name.lower() or filter_name in sheet.identification.lower():
flt_flags.append(self.bitflag_filter_item)
if not sheet.is_sheet:
flt_flags[active_sheet_index] = self.bitflag_filter_item
else:
flt_flags.append(0)
if not flt_flags:
return flt_flags, flt_neworder
return flt_flags, flt_neworder
@@ -23,14 +23,14 @@ import blenderbim.tool as tool
from blenderbim.bim.helper import prop_with_search
from bpy.types import WorkSpaceTool
# from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData
from blenderbim.bim.module.drawing.prop import ANNOTATION_TYPES_DATA
from blenderbim.bim.module.drawing.data import DecoratorData, AnnotationData
from blenderbim.bim.ifc import IfcStore
import blenderbim.bim.handler
# declaring it here to avoid circular import problems
# TODO: Fix circular import.
# Declaring it here to avoid circular import problems
class Operator:
def execute(self, context):
IfcStore.execute_ifc_operator(self, context)
@@ -38,6 +38,75 @@ class Operator:
return {"FINISHED"}
class LaunchAnnotationTypeManager(bpy.types.Operator):
bl_idname = "bim.launch_annotation_type_manager"
bl_label = "Launch Annotation Type Manager"
bl_options = {"REGISTER"}
bl_description = "Manage annotation types and templates"
def execute(self, context):
return {"FINISHED"}
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=550)
def draw(self, context):
if not AnnotationData.is_loaded:
AnnotationData.load()
props = context.scene.BIMAnnotationProperties
columns = self.layout.column_flow(columns=3)
row = columns.row()
row.alignment = "LEFT"
row.label(text=f"{len(AnnotationData.data['relating_types'])} Types", icon="FILE_VOLUME")
row = columns.row(align=True)
row.alignment = "CENTER"
# In case you want something here in the future
row = columns.row(align=True)
row.alignment = "RIGHT"
# In case you want something here in the future
if props.is_adding_type:
row = self.layout.row()
box = row.box()
row = box.row()
row.prop(props, "type_name")
row = box.row()
row.prop(props, "create_representation_for_type", text="Geometric Type")
row = box.row(align=True)
row.operator("bim.add_annotation_type", icon="CHECKMARK", text="Save New Type")
row.operator("bim.disable_add_annotation_type", icon="CANCEL", text="")
else:
row = self.layout.row()
row.operator("bim.enable_add_annotation_type", icon="ADD", text="Create New Type")
flow = self.layout.grid_flow(row_major=True, columns=3, even_columns=True, even_rows=True, align=True)
for relating_type in AnnotationData.data["relating_types"]:
outer_col = flow.column()
box = outer_col.box()
row = box.row()
row.alignment = "CENTER"
row.label(text=relating_type["name"], icon="FILE_3D")
row = box.row()
row.alignment = "CENTER"
row.label(text=relating_type["description"])
row = box.row(align=True)
op = row.operator("bim.select_type", icon="OBJECT_DATA")
op.relating_type = relating_type["id"]
op = row.operator("bim.duplicate_type", icon="DUPLICATE", text="")
op.element = relating_type["id"]
op = row.operator("bim.remove_type", icon="X", text="")
op.element = relating_type["id"]
class AnnotationTool(WorkSpaceTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
@@ -89,42 +158,8 @@ def add_layout_hotkey_operator(layout, text, hotkey, description):
return op, row
# TODO: move to operator
def create_annotation_type(context):
# just empty to store parameters
props = context.scene.BIMAnnotationProperties
object_type = props.object_type
create_representation = props.create_representation_for_type
drawing = tool.Ifc.get_entity(bpy.context.scene.camera)
if props.create_representation_for_type:
obj = tool.Drawing.create_annotation_object(drawing, object_type)
else:
obj = bpy.data.objects.new(object_type, None)
obj.name = f"{object_type}_TYPE"
obj.location = context.scene.cursor.location
tool.Drawing.ensure_annotation_in_drawing_plane(obj)
drawing = tool.Ifc.get_entity(context.scene.camera)
ifc_context = tool.Drawing.get_annotation_context(tool.Drawing.get_drawing_target_view(drawing), object_type)
element = tool.Drawing.run_root_assign_class(
obj=obj,
ifc_class="IfcTypeProduct",
predefined_type=object_type,
should_add_representation=create_representation,
context=ifc_context,
ifc_representation_class=tool.Drawing.get_ifc_representation_class(object_type),
)
element.ApplicableOccurrence = f"IfcAnnotation/{object_type}"
tool.Blender.select_and_activate_single_object(context, obj)
# TODO: move to operator
def create_annotation_occurence(context):
# object_type = context.scene.BIMAnnotationProperties.object_type
props = context.scene.BIMAnnotationProperties
relating_type = tool.Ifc.get().by_id(int(props.relating_type_id))
object_type = props.object_type
@@ -171,6 +206,11 @@ class AnnotationToolUI:
row.label(text="No IFC Project", icon="ERROR")
return
drawing = tool.Ifc.get_entity(context.scene.camera)
if not drawing:
row.label(text="No Active Drawing", icon="ERROR")
return
if not AnnotationData.is_loaded:
AnnotationData.load()
@@ -183,9 +223,7 @@ class AnnotationToolUI:
@classmethod
def draw_create_object_interface(cls):
row = cls.layout.row(align=True)
op, row = add_layout_hotkey_operator(cls.layout, "Add Type", "S_C", "Create a new annotation type")
selected_icon = "CHECKBOX_HLT" if cls.props.create_representation_for_type else "CHECKBOX_DEHLT"
row.prop(cls.props, "create_representation_for_type", text="", icon=selected_icon)
row.prop(bpy.context.scene.DocProperties, "should_draw_decorations", text="Viewport Annotations")
@classmethod
def draw_edit_object_interface(cls, context):
@@ -204,10 +242,9 @@ class AnnotationToolUI:
row = cls.layout.row(align=True)
row.label(text="", icon="FILE_3D")
prop_with_search(row, cls.props, "relating_type_id", text="")
row.operator("bim.launch_annotation_type_manager", icon="LIGHTPROBE_GRID", text="")
create_type_occurence = cls.props.relating_type_id != "0"
label = "Add Type Occurence" if create_type_occurence else "Add Annotation"
add_layout_hotkey_operator(cls.layout, label, "S_A", "Create a new annotation")
add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation")
if object_type in ("TEXT", "STAIR_ARROW"):
add_layout_hotkey_operator(
@@ -281,6 +318,3 @@ class Hotkey(bpy.types.Operator, Operator):
related_object = tool.Ifc.get_object(related_product)
tool.Drawing.setup_annotation_object(obj, element.ObjectType, related_object)
def hotkey_S_C(self):
create_annotation_type(bpy.context)
@@ -29,6 +29,9 @@ classes = (
operator.OverrideDuplicateMoveLinked,
operator.OverrideDuplicateMoveLinkedMacro,
operator.OverrideDuplicateMoveMacro,
operator.OverrideDuplicateMoveAggregate,
operator.OverrideDuplicateMoveAggregateMacro,
operator.RefreshAggregate,
operator.OverrideJoin,
operator.OverrideModeSetEdit,
operator.OverrideModeSetObject,
@@ -58,38 +61,58 @@ def register():
operator.OverrideDuplicateMoveMacro.define("TRANSFORM_OT_translate")
operator.OverrideDuplicateMoveLinkedMacro.define("BIM_OT_override_object_duplicate_move_linked")
operator.OverrideDuplicateMoveLinkedMacro.define("TRANSFORM_OT_translate")
operator.OverrideDuplicateMoveAggregateMacro.define("BIM_OT_override_object_duplicate_move_aggregate")
operator.OverrideDuplicateMoveAggregateMacro.define("TRANSFORM_OT_translate")
bpy.types.Object.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMObjectGeometryProperties)
bpy.types.Scene.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMGeometryProperties)
bpy.types.OBJECT_PT_transform.append(ui.BIM_PT_transform)
bpy.types.VIEW3D_MT_object.append(ui.object_menu)
bpy.types.OUTLINER_MT_object.append(ui.outliner_menu)
bpy.types.VIEW3D_MT_object_context_menu.append(ui.object_menu)
wm = bpy.context.window_manager
if wm.keyconfigs.addon:
km = wm.keyconfigs.addon.keymaps.new(name="Object Mode", space_type="EMPTY")
kmi = km.keymap_items.new("bim.override_object_join", "J", "PRESS", ctrl=True)
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_object_duplicate_move_macro", "D", "PRESS", shift=True)
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_object_duplicate_move_linked_macro", "D", "PRESS", alt=True)
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_object_duplicate_move_aggregate_macro", "D", "PRESS", ctrl=True, shift=True)
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_mode_set_edit", "TAB", "PRESS")
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS")
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_object_delete", "DEL", "PRESS")
kmi.properties.confirm = False
addon_keymaps.append((km, kmi))
km = wm.keyconfigs.addon.keymaps.new(name="Mesh", space_type="EMPTY")
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
addon_keymaps.append((km, kmi))
km = wm.keyconfigs.addon.keymaps.new(name="Curve", space_type="EMPTY")
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
addon_keymaps.append((km, kmi))
km = wm.keyconfigs.addon.keymaps.new(name="Outliner", space_type="OUTLINER")
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_outliner_delete", "X", "PRESS")
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_outliner_delete", "DEL", "PRESS")
addon_keymaps.append((km, kmi))
def unregister():
bpy.types.VIEW3D_MT_object.remove(ui.object_menu)
bpy.types.OBJECT_PT_transform.remove(ui.BIM_PT_transform)
bpy.types.OUTLINER_MT_object.remove(ui.outliner_menu)
bpy.types.VIEW3D_MT_object_context_menu.remove(ui.outliner_menu)
del bpy.types.Scene.BIMGeometryProperties
del bpy.types.Object.BIMGeometryProperties
wm = bpy.context.window_manager
@@ -111,10 +111,13 @@ class ConnectionsData:
for rel in connected_to:
if element.is_a("IfcDistributionPort"):
related_element = rel.RelatedPort
related_element_connection_type = ""
else:
related_element = rel.RelatedElement
if element.is_a("IfcRelConnectsPathElements"):
related_element_connection_type = rel.RelatedConnectionType
else:
related_element_connection_type = ""
results.append(
{
@@ -128,10 +131,13 @@ class ConnectionsData:
for rel in connected_from:
if element.is_a("IfcDistributionPort"):
relating_element = rel.RelatingPort
relating_element_connection_type = ""
else:
relating_element = rel.RelatingElement
if element.is_a("IfcRelConnectsPathElements"):
relating_element_connection_type = rel.RelatingConnectionType
else:
relating_element_connection_type = ""
results.append(
{
@@ -20,18 +20,20 @@ import bpy
import bmesh
import logging
import numpy as np
import json
import ifcopenshell
import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.api
import blenderbim.core.geometry as core
import blenderbim.core.aggregate
import blenderbim.core.style
import blenderbim.core.root
import blenderbim.core.drawing
import blenderbim.tool as tool
import blenderbim.bim.handler
from mathutils import Vector
from mathutils import Vector, Matrix
from blenderbim.bim import import_ifc
from blenderbim.bim.ifc import IfcStore
@@ -415,7 +417,11 @@ class OverrideDelete(bpy.types.Operator):
if self.is_batch:
ifcopenshell.util.element.batch_remove_deep2(tool.Ifc.get())
for obj in context.selected_objects:
if tool.Ifc.get_entity(obj):
element = tool.Ifc.get_entity(obj)
if element:
if ifcopenshell.util.element.get_pset(element, "BBIM_Array"):
self.report({"INFO"}, "Elements that are part of an array cannot be deleted.")
return {"FINISHED"}
tool.Geometry.delete_ifc_object(obj)
else:
bpy.data.objects.remove(obj)
@@ -445,6 +451,7 @@ class OverrideOutlinerDelete(bpy.types.Operator):
bl_label = "IFC Delete"
bl_options = {"REGISTER", "UNDO"}
hierarchy: bpy.props.BoolProperty(default=False)
is_batch: bpy.props.BoolProperty(name="Is Batch", default=False)
@classmethod
def poll(cls, context):
@@ -477,7 +484,26 @@ class OverrideOutlinerDelete(bpy.types.Operator):
bpy.data.collections.remove(collection)
return {"FINISHED"}
def invoke(self, context, event):
if tool.Ifc.get():
total_elements = len(tool.Ifc.get().wrapped_data.entity_names())
total_polygons = sum([len(o.data.polygons) for o in context.selected_objects if o.type == "MESH"])
# These numbers are a bit arbitrary, but basically batching is only
# really necessary on large models and large geometry removals.
self.is_batch = total_elements > 500000 and total_polygons > 2000
if self.is_batch:
return context.window_manager.invoke_props_dialog(self)
return self.execute(context)
def draw(self, context):
row = self.layout.row()
row.label(text="Warning: Faster deletion will use more memory.", icon="ERROR")
row = self.layout.row()
row.prop(self, "is_batch", text="Enable Faster Deletion")
def _execute(self, context):
if self.is_batch:
ifcopenshell.util.element.batch_remove_deep2(tool.Ifc.get())
objects_to_delete = set()
collections_to_delete = set()
for item in context.selected_ids:
@@ -490,8 +516,20 @@ class OverrideOutlinerDelete(bpy.types.Operator):
elif item.bl_rna.identifier == "Object":
objects_to_delete.add(bpy.data.objects.get(item.name))
for obj in objects_to_delete:
# This is the only difference
tool.Geometry.delete_ifc_object(obj)
if tool.Ifc.get_entity(obj):
tool.Geometry.delete_ifc_object(obj)
else:
bpy.data.objects.remove(obj)
for collection in collections_to_delete:
bpy.data.collections.remove(collection)
if self.is_batch:
old_file = tool.Ifc.get()
old_file.end_transaction()
new_file = ifcopenshell.util.element.unbatch_remove_deep2(tool.Ifc.get())
new_file.begin_transaction()
tool.Ifc.set(new_file)
self.transaction_data = {"old_file": old_file, "new_file": new_file}
IfcStore.add_transaction_operation(self)
return {"FINISHED"}
def get_collection_objects_and_children(self, collection):
@@ -506,6 +544,14 @@ class OverrideOutlinerDelete(bpy.types.Operator):
children = children.union(collection.children)
return {"objects": objects, "children": children}
def rollback(self, data):
tool.Ifc.set(data["old_file"])
data["old_file"].undo()
def commit(self, data):
data["old_file"].redo()
tool.Ifc.set(data["new_file"])
class OverrideDuplicateMoveMacro(bpy.types.Macro):
bl_idname = "bim.override_object_duplicate_move_macro"
@@ -643,6 +689,443 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
return {"FINISHED"}
class OverrideDuplicateMoveAggregateMacro(bpy.types.Macro):
bl_idname = "bim.override_object_duplicate_move_aggregate_macro"
bl_label = "IFC Duplicate Objects Aggregate"
bl_options = {"REGISTER", "UNDO"}
class OverrideDuplicateMoveAggregate(bpy.types.Operator):
bl_idname = "bim.override_object_duplicate_move_aggregate"
bl_label = "IFC Duplicate Objects Aggregate"
bl_options = {"REGISTER", "UNDO"}
is_interactive: bpy.props.BoolProperty(name="Is Interactive", default=True)
@classmethod
def poll(cls, context):
return len(context.selected_objects) > 0
def execute(self, context):
# Deep magick from the dawn of time
if IfcStore.get_file():
IfcStore.execute_ifc_operator(self, context)
if self.new_active_obj:
context.view_layer.objects.active = self.new_active_obj
return {"FINISHED"}
new_active_obj = None
for obj in context.selected_objects:
new_obj = obj.copy()
if obj.data:
new_obj.data = obj.data.copy()
if obj == context.active_object:
new_active_obj = new_obj
for collection in obj.users_collection:
collection.objects.link(new_obj)
obj.select_set(False)
new_obj.select_set(True)
if new_active_obj:
context.view_layer.objects.active = new_active_obj
return {"FINISHED"}
def _execute(self, context):
self.new_active_obj = None
old_to_new = {}
### Adding the assembly data
def add_assembly_data(element, parent, data_to_add):
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Aggregate_Data")
data = [data_to_add]
if pset:
if parent == None:
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=tool.Ifc.get().by_id(pset["id"]),
properties={"Data": json.dumps(data)},
)
else:
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=tool.Ifc.get().by_id(pset["id"]),
properties={"Parent": parent.GlobalId, "Data": json.dumps(data)},
)
else:
pset = ifcopenshell.api.run(
"pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Aggregate_Data"
)
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Parent": parent.GlobalId, "Data": json.dumps(data)},
)
def add_child_to_assembly_data(new_entity):
pset = ifcopenshell.util.element.get_pset(new_entity, "BBIM_Aggregate_Data")
parent_element = tool.Ifc.get().by_guid(pset["Parent"])
parent_pset = ifcopenshell.util.element.get_pset(parent_element, "BBIM_Aggregate_Data")
data = json.loads(parent_pset["Data"])
data[0]["children"].append(new_entity.GlobalId)
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=tool.Ifc.get().by_id(parent_pset["id"]),
properties={"Data": json.dumps(data)},
)
def create_data_structure(entity, level=-1):
level += 1
data_children = {
"children": [],
"instance_of": [],
}
data_parent = {
"children": [],
"instance_of": [entity.GlobalId],
}
if not entity.is_a("IfcElementAssembly"):
return
else:
if level == 0:
add_assembly_data(entity, entity, data_parent)
else:
add_assembly_data(entity, None, data_parent)
parts = ifcopenshell.util.element.get_parts(entity)
for part in parts:
if part.is_a("IfcElementAssembly"):
add_assembly_data(part, entity, data_children)
add_child_to_assembly_data(part)
create_data_structure(part, level)
continue
add_assembly_data(part, entity, data_children)
add_child_to_assembly_data(part)
return
def recreate_data_structure(entity, level=-1):
level += 1
pset = ifcopenshell.util.element.get_pset(entity, "BBIM_Aggregate_Data")
pset_data = json.loads(pset["Data"])[0]
instance_of = pset_data["instance_of"]
data_children = {
"children": [],
"instance_of": [],
}
# Keeps instance ID through all copies
data_parent = {
"children": [],
"instance_of": instance_of,
}
if not entity.is_a("IfcElementAssembly"):
return
else:
if level == 0:
add_assembly_data(entity, entity, data_parent)
else:
add_assembly_data(entity, None, data_parent)
parts = ifcopenshell.util.element.get_parts(entity)
for part in parts:
if part.is_a("IfcElementAssembly"):
add_child_to_assembly_data(part)
recreate_data_structure(part, level)
continue
add_assembly_data(part, entity, data_children)
add_child_to_assembly_data(part)
return
def duplicate_all(obj, level=-1, new_parent=None, parents=[]):
level += 1
entity = tool.Ifc.get_entity(obj)
if level == 0:
new_parent = duplicate_objects(obj)
parents.append(new_parent)
else:
pair = []
pair.append(new_parent)
new_parent = duplicate_objects(obj)
pair.append(new_parent)
parents.append(pair)
if not entity.is_a("IfcElementAssembly"):
return
else:
parts = ifcopenshell.util.element.get_parts(entity)
# Ensures that we are duplication all the IfcElementAssembly
# before duplicating the nested objects
for part in parts:
if part.is_a("IfcElementAssembly"):
pass
else:
part_obj = tool.Ifc.get_object(part)
new_part = duplicate_objects(part_obj)
blenderbim.core.aggregate.assign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=tool.Ifc.get_object(new_parent),
related_obj=tool.Ifc.get_object(new_part),
)
for part in parts:
if part.is_a("IfcElementAssembly"):
part_obj = tool.Ifc.get_object(part)
# Recursion Call
duplicate_all(part_obj, level, new_parent, parents)
if level == 0:
for p in parents[1:]:
blenderbim.core.aggregate.assign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=tool.Ifc.get_object(p[0]),
related_obj=tool.Ifc.get_object(p[1]),
)
return new_parent
return
def duplicate_objects(obj, is_root=False):
new_obj = obj.copy()
if obj.data:
new_obj.data = obj.data.copy()
if obj == context.active_object:
self.new_active_obj = new_obj
for collection in obj.users_collection:
collection.objects.link(new_obj)
obj.select_set(False)
new_obj.select_set(True)
# This is needed to make sure the new object gets unlink from
# the old object assembly collection
new_obj.BIMObjectProperties.collection = None
# Copy the actual class
new_entity = blenderbim.core.root.copy_class(
tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj
)
if new_entity:
# Checks if the object belongs to an Ifc Array
array_pset = ifcopenshell.util.element.get_pset(new_entity, "BBIM_Array")
if array_pset:
array_pset = tool.Ifc.get().by_id(array_pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new_entity, pset=array_pset)
blenderbim.core.aggregate.unassign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=tool.Ifc.get_object(selected_root_entity),
related_obj=tool.Ifc.get_object(new_entity),
)
old_to_new[tool.Ifc.get_entity(obj)] = [new_entity]
return new_entity
### Check if only one element and it's assembly
if len(context.selected_objects) != 1:
return {"FINISHED"}
selected_obj = context.selected_objects[0]
selected_root_entity = tool.Ifc.get_entity(selected_obj)
if not selected_root_entity.is_a("IfcElementAssembly"):
return {"FINISHED"}
pset = ifcopenshell.util.element.get_pset(selected_root_entity, "BBIM_Aggregate_Data")
if not pset:
create_data_structure(selected_root_entity)
pset = ifcopenshell.util.element.get_pset(selected_root_entity, "BBIM_Aggregate_Data")
selected_root_parent = selected_root_entity
new_root_entity = duplicate_all(selected_obj)
recreate_data_structure(new_root_entity)
old_objs = []
for old, new in old_to_new.items():
old_objs.append(tool.Ifc.get_object(old))
relationships = tool.Root.get_decomposition_relationships(old_objs)
tool.Root.recreate_decompositions(relationships, old_to_new)
blenderbim.bim.handler.purge_module_data()
return {"FINISHED"}
class RefreshAggregate(bpy.types.Operator):
bl_idname = "bim.refresh_aggregate"
bl_label = "IFC Refresh Aggregate"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return len(context.selected_objects) > 0
def execute(self, context):
# Deep magick from the dawn of time
if IfcStore.get_file():
IfcStore.execute_ifc_operator(self, context)
return {"FINISHED"}
return {"FINISHED"}
def _execute(self, context):
self.new_active_obj = None
old_to_new = {}
def remove_objects(entity, level=-1, parents=[]):
level += 1
if level == 0:
parents = [entity]
parts = ifcopenshell.util.element.get_parts(entity)
for part in parts:
if part.is_a("IfcElementAssembly"):
parents.append(part)
remove_objects(part, level, parents)
continue
else:
part_obj = tool.Ifc.get_object(part)
if part_obj:
tool.Geometry.delete_ifc_object(part_obj)
return parents
def duplicate_children(entity):
pset = ifcopenshell.util.element.get_pset(entity, "BBIM_Aggregate_Data")
pset_data = json.loads(pset["Data"])[0]
instance_of = pset_data["instance_of"][0]
instance_entity = tool.Ifc.get().by_guid(instance_of)
if not instance_entity.is_a("IfcElementAssembly"):
return
else:
parts = ifcopenshell.util.element.get_parts(instance_entity)
for part in parts:
if part.is_a("IfcElementAssembly"):
pass
else:
part_obj = tool.Ifc.get_object(part)
new_part = duplicate_objects(part_obj)
blenderbim.core.aggregate.assign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=tool.Ifc.get_object(entity),
related_obj=tool.Ifc.get_object(new_part),
)
def duplicate_objects(obj, is_root=False):
new_obj = obj.copy()
if obj.data:
new_obj.data = obj.data.copy()
if obj == context.active_object:
self.new_active_obj = new_obj
for collection in obj.users_collection:
collection.objects.link(new_obj)
obj.select_set(False)
new_obj.select_set(True)
# Copy the actual class
new_entity = blenderbim.core.root.copy_class(
tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj
)
if new_entity:
# Checks if the object belongs to an Ifc Array
array_pset = ifcopenshell.util.element.get_pset(new_entity, "BBIM_Array")
if array_pset:
array_pset = tool.Ifc.get().by_id(array_pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new_entity, pset=array_pset)
blenderbim.core.aggregate.unassign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=obj,
related_obj=tool.Ifc.get_object(new_entity),
)
old_to_new[tool.Ifc.get_entity(obj)] = [new_entity]
return new_entity
if len(context.selected_objects) != 1:
return {"FINISHED"}
selected_root_obj = context.selected_objects[0]
selected_root_entity = tool.Ifc.get_entity(selected_root_obj)
if not selected_root_entity.is_a("IfcElementAssembly"):
return {"FINISHED"}
pset = ifcopenshell.util.element.get_pset(selected_root_entity, "BBIM_Aggregate_Data")
if not pset:
return {"FINISHED"}
pset_data = json.loads(pset["Data"])[0]
instance_of = pset_data["instance_of"][0]
original_root_entity = tool.Ifc.get().by_guid(instance_of)
if original_root_entity == selected_root_entity:
return {"FINISHED"}
parents = remove_objects(selected_root_entity)
original_root_object = tool.Ifc.get_object(original_root_entity)
selected_matrix = selected_root_obj.matrix_world
original_matrix = original_root_object.matrix_world
for parent in parents:
duplicate_children(parent)
old_objs = []
for old, new in old_to_new.items():
old_objs.append(tool.Ifc.get_object(old))
new_obj = tool.Ifc.get_object(new[0])
matrix_diff = new_obj.matrix_world @ original_matrix
new_matrix = selected_matrix @ matrix_diff
new_obj.matrix_world = new_matrix
relationships = tool.Root.get_decomposition_relationships(old_objs)
tool.Root.recreate_decompositions(relationships, old_to_new)
blenderbim.bim.handler.purge_module_data()
return {"FINISHED"}
class OverrideJoin(bpy.types.Operator, Operator):
bl_idname = "bim.override_object_join"
bl_label = "IFC Join"
@@ -803,6 +1286,7 @@ class OverrideModeSetEdit(bpy.types.Operator):
):
obj.select_set(False)
continue
if usage_type == "PROFILE":
if len(context.selected_objects) == 1:
bpy.ops.bim.hotkey(hotkey="A_E", description="")
@@ -811,12 +1295,33 @@ class OverrideModeSetEdit(bpy.types.Operator):
self.report({"INFO"}, "Only a single profile-based representation can be edited at a time.")
obj.select_set(False)
continue
# TODO: refactor repetitive code
if tool.Pset.get_element_pset(element, "BBIM_Roof"):
if len(context.selected_objects) == 1:
bpy.ops.bim.enable_editing_roof_path()
return {"FINISHED"}
else:
self.report({"INFO"}, "Only a single profile-based representation can be edited at a time.")
obj.select_set(False)
continue
if tool.Pset.get_element_pset(element, "BBIM_Railing"):
if len(context.selected_objects) == 1:
if obj.BIMRailingProperties.is_editing == 1:
self.report({"INFO"}, "Can't edit path while the modifier parameters are being modified")
return {"FINISHED"}
bpy.ops.bim.enable_editing_railing_path()
return {"FINISHED"}
else:
self.report({"INFO"}, "Only a single profile-based representation can be edited at a time.")
obj.select_set(False)
continue
if (
tool.Geometry.is_profile_based(obj.data)
or usage_type == "LAYER3"
or tool.Geometry.is_swept_profile(representation)
or tool.Pset.get_element_pset(element, "BBIM_Roof")
or tool.Pset.get_element_pset(element, "BBIM_Railing")
):
if len(context.selected_objects) == 1:
bpy.ops.bim.hotkey(hotkey="S_E", description="")
@@ -853,17 +1358,19 @@ class OverrideModeSetEdit(bpy.types.Operator):
# The active object is non-mesh-like. Set a valid object (or None) as active
context.view_layer.objects.active = context.selected_objects[0] if context.selected_objects else None
if context.active_object:
bpy.ops.object.mode_set(mode="EDIT", toggle=True)
else:
# restore the selection if nothing worked
for obj in objs:
obj.select_set(True)
context.view_layer.objects.active = active_obj
return tool.Blender.toggle_edit_mode(context)
# Restore the selection if nothing worked
for obj in objs:
obj.select_set(True)
context.view_layer.objects.active = active_obj
return {"FINISHED"}
def invoke(self, context, event):
return IfcStore.execute_ifc_operator(self, context, is_invoke=True)
def _invoke(self, context, event):
if not tool.Ifc.get():
return bpy.ops.object.mode_set(mode="EDIT", toggle=True)
return tool.Blender.toggle_edit_mode(context)
return self.execute(context)
@@ -911,6 +1418,9 @@ class OverrideModeSetObject(bpy.types.Operator):
row.label(text="No Geometry Found: Object will revert to previous state.")
def invoke(self, context, event):
return IfcStore.execute_ifc_operator(self, context, is_invoke=True)
def _invoke(self, context, event):
self.is_valid = True
self.should_save = True
@@ -944,9 +1454,9 @@ class OverrideModeSetObject(bpy.types.Operator):
if tool.Ifc.get_object(profile): # We are editing an arbitrary profile
bpy.ops.bim.edit_arbitrary_profile()
elif tool.Pset.get_element_pset(element, "BBIM_Railing"):
bpy.ops.bim.cad_hotkey(hotkey="S_Q")
bpy.ops.bim.finish_editing_railing_path()
elif tool.Pset.get_element_pset(element, "BBIM_Roof"):
bpy.ops.bim.cad_hotkey(hotkey="S_Q")
bpy.ops.bim.finish_editing_roof_path()
elif tool.Model.get_usage_type(element) == "PROFILE":
bpy.ops.bim.edit_extrusion_axis()
else:
@@ -24,10 +24,14 @@ from blenderbim.bim.module.geometry.data import RepresentationsData, Connections
def object_menu(self, context):
self.layout.separator()
self.layout.operator("bim.override_object_duplicate_move", icon="PLUGIN")
self.layout.operator("bim.override_object_delete", icon="PLUGIN")
self.layout.operator("bim.override_paste_buffer", icon="PLUGIN")
def outliner_menu(self, context):
self.layout.separator()
self.layout.operator("bim.override_outliner_delete", icon='X')
class BIM_PT_representations(Panel):
bl_label = "IFC Representations"
@@ -33,6 +33,7 @@ classes = (
operator.ConvertGlobalToLocal,
operator.GetCursorLocation,
operator.SetCursorLocation,
operator.ConvertAngleToCoordinates,
prop.BIMGeoreferenceProperties,
ui.BIM_PT_gis,
ui.BIM_PT_gis_utilities,
@@ -171,3 +171,19 @@ class ConvertGlobalToLocal(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
core.convert_global_to_local(tool.Georeference)
class ConvertAngleToCoordinates(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.convert_angle_to_coord"
bl_label = "Convert Angle To Y Axis"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Convert angle to Y axis"
type: bpy.props.StringProperty()
@classmethod
def poll(cls, context):
file = tool.Ifc.get()
props = context.scene.BIMGeoreferenceProperties
return file and (props.angle_degree_input_x or props.angle_degree_input_y)
def _execute(self, context):
core.convert_angle_to_coord(tool.Georeference, type=self.type)
@@ -37,6 +37,12 @@ class BIMGeoreferenceProperties(PropertyGroup):
projected_crs: CollectionProperty(name="Projected CRS", type=Attribute)
coordinate_input: StringProperty(name="Coordinate Input", description='Formatted "x,y,z" (without quotes)')
coordinate_output: StringProperty(name="Coordinate Output", description='Formatted "x,y,z" (without quotes)')
angle_degree_input_x: FloatProperty(name="Angle Degree Input", description="Angle (in degrees) rel to Easting")
angle_degree_input_y: FloatProperty(name="Angle Degree Input", description="Angle (in degrees) rel to +Y")
x_axis_abscissa_output: StringProperty(name="X Axis Abscissa Ordinate Output", description="X axis abscissa and ordinate", )
x_axis_ordinate_output: StringProperty(name="X Axis Abscissa Ordinate Output", description="X axis abscissa and ordinate", )
y_axis_abscissa_output: StringProperty(name="Y Axis Abscissa Ordinate Output", description="Y axis abscissa and ordinate", )
y_axis_ordinate_output: StringProperty(name="Y Axis Abscissa Ordinate Output", description="Y axis abscissa and ordinate", )
has_blender_offset: BoolProperty(name="Has Blender Offset")
blender_eastings: StringProperty(name="Blender Eastings", default="0")
blender_northings: StringProperty(name="Blender Northings", default="0")
@@ -167,3 +167,21 @@ class BIM_PT_gis_utilities(Panel):
row = self.layout.row(align=True)
row.operator("bim.convert_local_to_global", text="Local to Global")
row.operator("bim.convert_global_to_local", text="Global to Local")
row = self.layout.row(align=True)
row.label(text="Orientation Calculator", icon="TRACKING_REFINE_FORWARDS")
row = self.layout.row(align=True)
row.prop(props, "angle_degree_input_x", text="Angle (°) rel to Easting")
row.operator("bim.convert_angle_to_coord", text="", icon="FILE_REFRESH").type = "rel_x"
row = self.layout.row(align=True)
row.prop(props, "x_axis_abscissa_output", text="XAxis Abscissa")
row = self.layout.row(align=True)
row.prop(props, "x_axis_ordinate_output", text="XAxis Ordinate")
row = self.layout.row(align=True)
row.prop(props, "angle_degree_input_y", text="Angle (°) rel to to +Y")
row.operator("bim.convert_angle_to_coord", text="", icon="FILE_REFRESH").type = "rel_y"
row = self.layout.row(align=True)
row.prop(props, "y_axis_abscissa_output", text="North Abscissa")
row = self.layout.row(align=True)
row.prop(props, "y_axis_ordinate_output", text="North Ordinate")
@@ -17,7 +17,7 @@ class CreateRepo(bpy.types.Operator):
def poll(cls, context):
IfcGitData.make_sure_is_loaded()
path_ifc = IfcGitData.data["path_ifc"]
if not os.path.isfile(path_ifc):
if not path_ifc or not os.path.isfile(path_ifc):
return False
if IfcGitData.data["repo"]:
# repo already exists
@@ -45,7 +45,7 @@ class AddFileToRepo(bpy.types.Operator):
def poll(cls, context):
IfcGitData.make_sure_is_loaded()
path_ifc = IfcGitData.data["path_ifc"]
if not os.path.isfile(path_ifc):
if not path_ifc or not os.path.isfile(path_ifc):
return False
if not IfcGitData.data["repo"]:
# repo doesn't exist
@@ -47,6 +47,7 @@ classes = (
array.EnableEditingArray,
array.RemoveArray,
array.SelectArrayParent,
array.SelectAllArrayObjects,
array.Input3DCursorXArray,
array.Input3DCursorYArray,
array.Input3DCursorZArray,
@@ -54,6 +55,8 @@ classes = (
product.AddEmptyType,
product.AlignProduct,
product.ChangeTypePage,
product.DisableAddType,
product.EnableAddType,
product.LoadTypeThumbnails,
product.MirrorElements,
workspace.Hotkey,
@@ -142,6 +142,7 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Remove Array"
bl_options = {"REGISTER", "UNDO"}
item: bpy.props.IntProperty()
keep_objs: bpy.props.BoolProperty(name="Keep Objects", default=False)
def _execute(self, context):
obj = context.active_object
@@ -151,6 +152,10 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
data = json.loads(pset["Data"])
data[self.item]["count"] = 1
if (self.keep_objs) & (self.item < (len(data) - 1)):
self.report({"INFO"}, "Keeping the objects is only allowed when you are removing the last Array of the object")
return {"FINISHED"}
props.is_editing = -1
@@ -159,7 +164,7 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
except:
return {"FINISHED"}
tool.Model.regenerate_array(parent, data)
tool.Model.regenerate_array(parent, data, self.keep_objs)
pset = tool.Ifc.get().by_id(pset["id"])
if len(data) == 1:
@@ -171,6 +176,13 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
def draw(self, context):
row = self.layout.row()
row.prop(self, "keep_objs")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
class SelectArrayParent(bpy.types.Operator):
bl_idname = "bim.select_array_parent"
@@ -189,6 +201,32 @@ class SelectArrayParent(bpy.types.Operator):
obj.select_set(True)
return {"FINISHED"}
class SelectAllArrayObjects(bpy.types.Operator):
bl_idname = "bim.select_all_array_objects"
bl_label = "Select All Array Objects"
bl_options = {"REGISTER", "UNDO"}
parent: bpy.props.StringProperty()
def execute(self, context):
try:
element = tool.Ifc.get().by_guid(self.parent)
except:
return {"FINISHED"}
obj = tool.Ifc.get_object(element)
obj.select_set(True)
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
data = json.loads(pset["Data"])
for i in range(len(data)):
for child in data[i]["children"]:
element = tool.Ifc.get().by_guid(child)
obj = tool.Ifc.get_object(element)
if obj:
context.view_layer.objects.active = obj
obj.select_set(True)
return {"FINISHED"}
class Input3DCursorXArray(bpy.types.Operator):
bl_idname = "bim.input_cursor_x_array"
@@ -19,11 +19,11 @@
import bpy
import math
import json
import functools
import ifcopenshell
import ifcopenshell.util.element
from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc
import blenderbim.tool as tool
from math import degrees
def refresh():
@@ -48,15 +48,18 @@ class AuthoringData:
cls.is_loaded = True
cls.props = bpy.context.scene.BIMModelProperties
cls.data["ifc_classes"] = cls.ifc_classes()
cls.data["relating_type_id"] = cls.relating_type_id()
cls.data["relating_type_id"] = cls.relating_type_id() # only after .ifc_classes()
cls.data["type_class"] = cls.type_class()
# only after .type_class()
cls.data["type_predefined_type"] = cls.type_predefined_type()
cls.data["total_types"] = cls.total_types()
cls.data["total_pages"] = cls.total_pages()
cls.data["next_page"] = cls.next_page()
cls.data["prev_page"] = cls.prev_page()
cls.data["paginated_relating_types"] = cls.paginated_relating_types()
cls.data["type_thumbnail"] = cls.type_thumbnail()
cls.data["type_thumbnail"] = cls.type_thumbnail() # only after .relating_type_id()
cls.data["is_voidable_element"] = cls.is_voidable_element()
cls.data["has_visible_openings"] = cls.has_visible_openings()
cls.data["has_visible_boundaries"] = cls.has_visible_boundaries()
@@ -64,6 +67,7 @@ class AuthoringData:
cls.data["active_material_usage"] = cls.active_material_usage()
cls.data["active_representation_type"] = cls.active_representation_type()
cls.data["boundary_class"] = cls.boundary_class()
cls.data["selected_material_usages"] = cls.selected_material_usages()
@classmethod
def boundary_class(cls):
@@ -112,6 +116,8 @@ class AuthoringData:
def type_thumbnail(cls):
if not cls.data["relating_type_id"]:
return 0
if not tool.Blender.enum_property_has_valid_index(cls.props, "relating_type_id", cls.data["relating_type_id"]):
return 0
element = tool.Ifc.get().by_id(int(cls.props.relating_type_id))
return cls.type_thumbnails.get(element.id(), None) or 0
@@ -230,12 +236,26 @@ class AuthoringData:
if ifc_class:
elements = sorted(tool.Ifc.get().by_type(ifc_class), key=lambda s: s.Name or "Unnamed")
results.extend(elements)
return [
(str(e.id()), e.Name or "Unnamed", e.Description or "")
for e in results
]
return [(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in results]
return []
@classmethod
def selected_material_usages(cls):
selected_usages = {}
for obj in bpy.context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
usage = tool.Model.get_usage_type(element)
if not usage:
representation = tool.Geometry.get_active_representation(obj)
if representation and representation.RepresentationType == "SweptSolid":
usage = "SWEPTSOLID"
else:
continue
selected_usages.setdefault(usage, []).append(obj)
return selected_usages
class ArrayData:
data = {}
@@ -270,17 +290,26 @@ class StairData:
@classmethod
def load(cls):
cls.is_loaded = True
cls.data = {"parameters": cls.parameters()}
cls.data = {}
cls.data["pset_data"] = cls.pset_data()
if not cls.data["pset_data"]:
return
cls.data["general_params"] = cls.general_params()
@classmethod
def parameters(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if element:
psets = ifcopenshell.util.element.get_psets(element)
parameters = psets.get("BBIM_Stair", None)
if parameters:
parameters["data_dict"] = json.loads(parameters.get("Data", "[]") or "[]")
return parameters
def pset_data(cls):
return tool.Model.get_modeling_bbim_pset_data(bpy.context.active_object, "BBIM_Stair")
@classmethod
def general_params(cls):
props = bpy.context.active_object.BIMStairProperties
data = cls.data["pset_data"]["data_dict"]
general_params = {}
general_props = props.get_props_kwargs(stair_type=data["stair_type"])
for prop_name in general_props:
prop_readable_name, prop_value = get_prop_from_data(props, data, prop_name)
general_params[prop_readable_name] = prop_value
return general_params
class SverchokData:
@@ -290,17 +319,12 @@ class SverchokData:
@classmethod
def load(cls):
cls.is_loaded = True
cls.data = {"parameters": cls.parameters(), "has_sverchok": cls.has_sverchok()}
cls.data = {"pset_data": cls.pset_data(), "has_sverchok": cls.has_sverchok()}
# NOTE: never used
@classmethod
def parameters(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if element:
psets = ifcopenshell.util.element.get_psets(element)
parameters = psets.get("BBIM_Sverchok", None)
if parameters:
parameters["data_dict"] = json.loads(parameters.get("Data", "[]") or "[]")
return parameters
def pset_data(cls):
return tool.Model.get_modeling_bbim_pset_data(bpy.context.active_object, "BBIM_Sverchok")
@classmethod
def has_sverchok(cls):
@@ -312,6 +336,13 @@ class SverchokData:
return False
def get_prop_from_data(props, data, prop_name):
prop_value = data[prop_name]
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
prop_readable_name = props.bl_rna.properties[prop_name].name
return prop_readable_name, prop_value
class WindowData:
data = {}
is_loaded = False
@@ -319,17 +350,51 @@ class WindowData:
@classmethod
def load(cls):
cls.is_loaded = True
cls.data = {"parameters": cls.parameters()}
cls.data = {}
cls.data["pset_data"] = cls.pset_data()
if not cls.data["pset_data"]:
return
cls.data["general_params"] = cls.general_params()
cls.data["lining_params"] = cls.lining_params()
cls.data["panel_params"] = cls.panel_params()
@classmethod
def parameters(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if element:
psets = ifcopenshell.util.element.get_psets(element)
parameters = psets.get("BBIM_Window", None)
if parameters:
parameters["data_dict"] = json.loads(parameters.get("Data", "[]") or "[]")
return parameters
def pset_data(cls):
return tool.Model.get_modeling_bbim_pset_data(bpy.context.active_object, "BBIM_Window")
@classmethod
def general_params(cls):
props = bpy.context.active_object.BIMWindowProperties
data = cls.data["pset_data"]["data_dict"]
general_params = {}
general_props = props.get_general_kwargs()
for prop_name in general_props:
prop_readable_name, prop_value = get_prop_from_data(props, data, prop_name)
general_params[prop_readable_name] = prop_value
return general_params
@classmethod
def lining_params(cls):
props = bpy.context.active_object.BIMWindowProperties
data = cls.data["pset_data"]["data_dict"]
lining_data = data["lining_properties"]
lining_params = {}
lining_props = props.get_lining_kwargs(window_type=data["window_type"])
for prop_name in lining_props:
prop_readable_name, prop_value = get_prop_from_data(props, lining_data, prop_name)
lining_params[prop_readable_name] = prop_value
return lining_params
@classmethod
def panel_params(cls):
props = bpy.context.active_object.BIMWindowProperties
panel_data = cls.data["pset_data"]["data_dict"]["panel_properties"]
panel_params = {}
panel_props = props.get_panel_kwargs()
for prop_name in panel_props:
prop_readable_name, prop_value = get_prop_from_data(props, panel_data, prop_name)
panel_params[prop_readable_name] = prop_value
return panel_params
class DoorData:
@@ -339,17 +404,52 @@ class DoorData:
@classmethod
def load(cls):
cls.is_loaded = True
cls.data = {"parameters": cls.parameters()}
cls.data = {}
cls.data["pset_data"] = cls.pset_data()
if not cls.data["pset_data"]:
return
cls.data["general_params"] = cls.general_params()
cls.data["lining_params"] = cls.lining_params()
cls.data["panel_params"] = cls.panel_params()
@classmethod
def parameters(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if element:
psets = ifcopenshell.util.element.get_psets(element)
parameters = psets.get("BBIM_Door", None)
if parameters:
parameters["data_dict"] = json.loads(parameters.get("Data", "[]") or "[]")
return parameters
def pset_data(cls):
return tool.Model.get_modeling_bbim_pset_data(bpy.context.active_object, "BBIM_Door")
@classmethod
def general_params(cls):
props = bpy.context.active_object.BIMDoorProperties
data = cls.data["pset_data"]["data_dict"]
general_params = {}
general_props = props.get_general_kwargs()
for prop_name in general_props:
prop_readable_name, prop_value = get_prop_from_data(props, data, prop_name)
general_params[prop_readable_name] = prop_value
return general_params
@classmethod
def lining_params(cls):
props = bpy.context.active_object.BIMDoorProperties
data = cls.data["pset_data"]["data_dict"]
lining_data = data["lining_properties"]
lining_params = {}
lining_props = props.get_lining_kwargs(door_type=data["door_type"], lining_data=lining_data)
for prop_name in lining_props:
prop_readable_name, prop_value = get_prop_from_data(props, lining_data, prop_name)
lining_params[prop_readable_name] = prop_value
return lining_params
@classmethod
def panel_params(cls):
props = bpy.context.active_object.BIMDoorProperties
data = cls.data["pset_data"]["data_dict"]
panel_data = cls.data["pset_data"]["data_dict"]["panel_properties"]
panel_params = {}
panel_props = props.get_panel_kwargs(lining_data=data["lining_properties"])
for prop_name in panel_props:
prop_readable_name, prop_value = get_prop_from_data(props, panel_data, prop_name)
panel_params[prop_readable_name] = prop_value
return panel_params
class RailingData:
@@ -359,17 +459,31 @@ class RailingData:
@classmethod
def load(cls):
cls.is_loaded = True
cls.data = {"parameters": cls.parameters()}
cls.data = {}
cls.data["pset_data"] = cls.pset_data()
if not cls.data["pset_data"]:
return
cls.data["general_params"] = cls.general_params()
cls.data["path_data"] = cls.path_data()
@classmethod
def parameters(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if element:
psets = ifcopenshell.util.element.get_psets(element)
parameters = psets.get("BBIM_Railing", None)
if parameters:
parameters["data_dict"] = json.loads(parameters.get("Data", "[]") or "[]")
return parameters
def pset_data(cls):
return tool.Model.get_modeling_bbim_pset_data(bpy.context.active_object, "BBIM_Railing")
@classmethod
def general_params(cls):
props = bpy.context.active_object.BIMRailingProperties
data = cls.data["pset_data"]["data_dict"]
general_params = {}
general_props = props.get_general_kwargs(railing_type=data["railing_type"])
for prop_name in general_props:
prop_readable_name, prop_value = get_prop_from_data(props, data, prop_name)
general_params[prop_readable_name] = prop_value
return general_params
@classmethod
def path_data(cls):
return cls.data["pset_data"]["data_dict"]["path_data"]
class RoofData:
@@ -379,14 +493,27 @@ class RoofData:
@classmethod
def load(cls):
cls.is_loaded = True
cls.data = {"parameters": cls.parameters()}
cls.data = {}
cls.data["pset_data"] = cls.pset_data()
if not cls.data["pset_data"]:
return
cls.data["general_params"] = cls.general_params()
@classmethod
def parameters(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if element:
psets = ifcopenshell.util.element.get_psets(element)
parameters = psets.get("BBIM_Roof", None)
if parameters:
parameters["data_dict"] = json.loads(parameters.get("Data", "[]") or "[]")
return parameters
def pset_data(cls):
return tool.Model.get_modeling_bbim_pset_data(bpy.context.active_object, "BBIM_Roof")
@classmethod
def general_params(cls):
props = bpy.context.active_object.BIMRoofProperties
data = cls.data["pset_data"]["data_dict"]
general_params = {}
general_props = props.get_general_kwargs(generation_method=data["generation_method"])
for prop_name in general_props:
prop_readable_name, prop_value = get_prop_from_data(props, data, prop_name)
if prop_name in ("angle", "rafter_edge_angle"):
prop_value = round(degrees(prop_value), 2)
general_params[prop_readable_name] = prop_value
return general_params
@@ -128,7 +128,8 @@ def update_door_modifier_representation(context):
)
# type attributes
element.OperationType = props.door_type
if tool.Ifc.get_schema() != "IFC2X3":
element.OperationType = props.door_type
# occurences attributes
occurences = tool.Ifc.get_all_element_occurences(element)
@@ -483,10 +484,15 @@ class BIM_OT_add_door(bpy.types.Operator, tool.Ifc.Operator):
mesh = bpy.data.meshes.new("IfcDoor")
obj = bpy.data.objects.new("IfcDoor", mesh)
obj.location = spawn_location
collection = context.view_layer.active_layer_collection.collection
collection.objects.link(obj)
element = blenderbim.core.root.assign_class(
tool.Ifc, tool.Collector, tool.Root, obj=obj, ifc_class="IfcDoor", should_add_representation=False
)
element.PredefinedType = "DOOR"
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
if tool.Ifc.get_schema() != "IFC2X3":
element.PredefinedType = "DOOR"
bpy.ops.object.select_all(action="DESELECT")
bpy.context.view_layer.objects.active = None
@@ -507,8 +513,8 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
props = obj.BIMDoorProperties
if element.is_a() not in ("IfcDoor", "IfcDoorType"):
self.report({"ERROR"}, "Object has to be IfcDoor/IfcDoorType type to add a door.")
if element.is_a() not in ("IfcDoor", "IfcDoorType", "IfcDoorStyle"):
self.report({"ERROR"}, "Object has to be IfcDoor/IfcDoorType/IfcDoorStyle type to add a door.")
return {"CANCELLED"}
door_data = props.get_general_kwargs(convert_to_project_units=True)
@@ -559,7 +565,7 @@ class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
should_sync_changes_first=False,
)
props.is_editing = -1
props.is_editing = False
return {"FINISHED"}
@@ -580,7 +586,7 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
door_data["lining_properties"] = lining_props
door_data["panel_properties"] = panel_props
props.is_editing = -1
props.is_editing = False
update_door_modifier_representation(context)
@@ -605,7 +611,7 @@ class EnableEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
# required since we could load pset from .ifc and BIMDoorProperties won't be set
props.set_props_kwargs_from_ifc_data(data)
props.is_editing = 1
props.is_editing = True
return {"FINISHED"}
@@ -618,7 +624,7 @@ class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object
props = obj.BIMDoorProperties
element = tool.Ifc.get_entity(obj)
obj.BIMDoorProperties.is_editing = -1
obj.BIMDoorProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
@@ -19,9 +19,12 @@
import bpy
import gpu
import bmesh
import shapely
import logging
import numpy as np
import ifcopenshell
import ifcopenshell.util.shape
import ifcopenshell.util.element
import ifcopenshell.util.shape_builder
import ifcopenshell.util.representation
import blenderbim.tool as tool
@@ -215,9 +218,8 @@ class FilledOpeningGenerator:
def generate_opening_from_filling(self, filling, filling_obj, voided_obj):
thickness = voided_obj.dimensions[1] + 0.1 + 0.1
ifc_file = tool.Ifc.get()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
shape_builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
shape_builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
profile = None
filling_type = ifcopenshell.util.element.get_type(filling)
@@ -226,19 +228,40 @@ class FilledOpeningGenerator:
filling_type, "Model", "Profile", "ELEVATION_VIEW"
)
filling_obj = tool.Ifc.get_object(filling_type)
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
if profile:
curve_3d = ifcopenshell.util.representation.resolve_representation(profile).Items[0]
profile = ifcopenshell.util.representation.resolve_representation(profile)
def get_curve_2d_from_3d(curve_3d):
ifc_segments = [shape_builder.deep_copy(s) for s in curve_3d.Segments]
ifc_points = ifc_file.createIfcCartesianPointList2D([Vector(p).xz for p in curve_3d.Points.CoordList])
ifc_curve = ifc_file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
return ifc_curve
def get_curve_2d_from_3d(profile):
if len(profile.Items) == 1:
curve_3d = profile.Items[0]
if tool.Ifc.get_schema() == "IFC2X3":
coords = [Vector(p).xz for p in shape_builder.get_polyline_coords(curve_3d)]
return shape_builder.polyline(coords, closed=True)
# using different algorithm to keep arc segments possible in the future
ifc_segments = [shape_builder.deep_copy(s) for s in curve_3d.Segments]
ifc_points = tool.Ifc.get().createIfcCartesianPointList2D(
[Vector(p).xz for p in curve_3d.Points.CoordList]
)
return tool.Ifc.get().createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
settings = ifcopenshell.geom.settings()
settings.set(settings.INCLUDE_CURVES, True)
geometry = ifcopenshell.geom.create_shape(settings, profile)
verts = ifcopenshell.util.shape.get_vertices(geometry)
# [0, 2] represents X and Z ordinates
verts = [(np.around(v[[0, 2]], decimals=3) / unit_scale).tolist() for v in verts]
edges = ifcopenshell.util.shape.get_edges(geometry)
boundary_lines = [shapely.LineString([verts[v] for v in e]) for e in edges]
unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines))
closed_polygons = shapely.polygonize(boundary_lines)
polygon = max(closed_polygons.geoms, key=lambda polygon: polygon.area)
return shape_builder.polyline(list(polygon.exterior.coords))
extrusion = shape_builder.extrude(
get_curve_2d_from_3d(curve_3d),
get_curve_2d_from_3d(profile),
magnitude=thickness / unit_scale,
position=Vector([0.0, -0.1 / unit_scale, 0.0]),
position_x_axis=Vector((1, 0, 0)),
@@ -475,10 +498,12 @@ class AddBoolean(Operator, tool.Ifc.Operator):
bl_label = "Add Boolean"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return len(context.selected_objects) == 2
def _execute(self, context):
props = context.scene.BIMModelProperties
if len(context.selected_objects) != 2:
return {"FINISHED"}
obj1, obj2 = context.selected_objects
element1 = tool.Ifc.get_entity(obj1)
element2 = tool.Ifc.get_entity(obj2)
@@ -523,14 +548,18 @@ class ShowBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
bl_label = "Show Booleans"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
obj = context.active_object
return (
obj.data
and hasattr(obj.data, "BIMMeshProperties")
and obj.data.BIMMeshProperties.ifc_definition_id
)
def _execute(self, context):
obj = context.active_object
if (
not obj.data
or not hasattr(obj.data, "BIMMeshProperties")
or not obj.data.BIMMeshProperties.ifc_definition_id
):
return {"FINISHED"}
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
booleans = []
@@ -37,6 +37,24 @@ from . import prop
import json
class EnableAddType(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_add_type"
bl_label = "Enable Add Type"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
bpy.context.scene.BIMModelProperties.is_adding_type = True
class DisableAddType(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_add_type"
bl_label = "Disable Add Type"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
bpy.context.scene.BIMModelProperties.is_adding_type = False
class AddEmptyType(bpy.types.Operator, AddObjectHelper):
bl_idname = "bim.add_empty_type"
bl_label = "Add Empty Type"
@@ -154,10 +172,11 @@ class AddConstrTypeInstance(bpy.types.Operator):
)
else:
parent = ifcopenshell.util.element.get_container(building_element)
parent_obj = tool.Ifc.get_object(parent)
blenderbim.core.spatial.assign_container(
tool.Ifc, tool.Collector, tool.Spatial, structure_obj=parent_obj, element_obj=obj
)
if parent:
parent_obj = tool.Ifc.get_object(parent)
blenderbim.core.spatial.assign_container(
tool.Ifc, tool.Collector, tool.Spatial, structure_obj=parent_obj, element_obj=obj
)
# set occurences properties for the types defined with modifiers
if instance_class in ["IfcWindow", "IfcDoor"]:
@@ -292,8 +311,6 @@ class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator):
offset: bpy.props.IntProperty()
def _execute(self, context):
from PIL import Image, ImageDraw
if bpy.app.background:
return
@@ -310,106 +327,11 @@ class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator):
offset = 0
queue = queue[offset : offset + 9]
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
while queue:
# if bpy.app.is_job_running("RENDER_PREVIEW") does not seem to reflect asset preview generation
element = queue.pop()
obj = tool.Ifc.get_object(element)
if not obj:
continue # Nothing to process
elif AuthoringData.type_thumbnails.get(element.id(), None):
continue # Already processed
elif obj.preview and obj.preview.icon_id:
AuthoringData.type_thumbnails[element.id()] = obj.preview.icon_id
continue
if obj.data:
obj.asset_generate_preview()
while not obj.preview:
pass
else:
size = 128
img = Image.new("RGBA", (size, size))
draw = ImageDraw.Draw(img)
material = ifcopenshell.util.element.get_material(element)
if material and material.is_a("IfcMaterialProfileSet"):
profile = material.MaterialProfiles[0].Profile
tool.Profile.draw_image_for_ifc_profile(draw, profile, size)
elif material and material.is_a("IfcMaterialLayerSet"):
thicknesses = [l.LayerThickness for l in material.MaterialLayers]
total_thickness = sum(thicknesses)
si_total_thickness = total_thickness * unit_scale
if si_total_thickness <= 0.051:
width = 10
elif si_total_thickness <= 0.11:
width = 20
elif si_total_thickness <= 0.21:
width = 30
elif si_total_thickness <= 0.31:
width = 40
else:
width = 50
height = 100
is_horizontal = False
if element.is_a("IfcSlabType"):
is_horizontal = True
parametric = ifcopenshell.util.element.get_psets(element).get("EPset_Parametric")
if parametric:
layer_set_direction = parametric.get("LayerSetDirection", None)
if layer_set_direction == "AXIS2":
is_horizontal = False
elif layer_set_direction == "AXIS3":
is_horizontal = True
if is_horizontal:
width, height = height, width
x_offset = (size / 2) - (width / 2)
y_offset = (size / 2) - (height / 2)
draw.rectangle([x_offset, y_offset, width + x_offset, height + y_offset], outline="white", width=5)
current_thickness = 0
del thicknesses[-1]
for thickness in thicknesses:
current_thickness += thickness
if element.is_a("IfcSlabType"):
y = (current_thickness / total_thickness) * height
line = [x_offset, y_offset + y, x_offset + width, y_offset + y]
else:
x = (current_thickness / total_thickness) * width
line = [x_offset + x, y_offset, x_offset + x, y_offset + height]
draw.line(line, fill="white", width=2)
elif False:
# TODO: things like parametric duct segments
pass
elif not element.RepresentationMaps:
# Empties are represented by a generic thumbnail
width = height = 100
x_offset = (size / 2) - (width / 2)
y_offset = (size / 2) - (height / 2)
draw.line([x_offset, y_offset, width + x_offset, height + y_offset], fill="white", width=2)
draw.line([x_offset, y_offset + height, width + x_offset, y_offset], fill="white", width=2)
draw.rectangle([x_offset, y_offset, width + x_offset, height + y_offset], outline="white", width=5)
else:
draw.line([0, 0, size, size], fill="red", width=2)
draw.line([0, size, size, 0], fill="red", width=2)
pixels = [item for sublist in img.getdata() for item in sublist]
obj.asset_generate_preview()
while not obj.preview:
pass
obj.preview.image_size = size, size
obj.preview.image_pixels_float = pixels
queue.append(element)
if tool.Model.update_thumbnail_for_element(element):
queue.append(element)
return {"FINISHED"}
@@ -472,7 +394,6 @@ class MirrorElements(bpy.types.Operator, tool.Ifc.Operator):
obj.matrix_world = newmat
def generate_box(usecase_path, ifc_file, settings):
box_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Box", "MODEL_VIEW")
if not box_context:
@@ -145,12 +145,21 @@ class DumbProfileRegenerator:
objs = []
if not profile:
return
element_types = set()
for element in self.get_elements_using_profile(profile):
obj = tool.Ifc.get_object(element)
if obj:
objs.append(obj)
if element.is_a("IfcElementType"):
element_types.add(element)
DumbProfileRecalculator().recalculate(objs)
# update related thumbnails
for element in self.get_element_types_using_profile(profile):
tool.Model.update_thumbnail_for_element(element, refresh=True)
def regenerate_from_profile(self, usecase_path, ifc_file, settings):
self.file = ifc_file
objs = []
@@ -165,9 +174,10 @@ class DumbProfileRegenerator:
def get_elements_using_profile(self, profile):
results = []
for profile_set in [
profile_sets = [
mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile")
]:
]
for profile_set in profile_sets:
for inverse in self.file.get_inverse(profile_set):
if not inverse.is_a("IfcMaterialProfileSetUsage"):
continue
@@ -181,6 +191,18 @@ class DumbProfileRegenerator:
results.extend(rel.RelatedObjects)
return results
def get_element_types_using_profile(self, profile):
results = []
profile_sets = [
mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile")
]
for profile_set in profile_sets:
for inverse in self.file.get_inverse(profile_set):
if not inverse.is_a("IfcRelAssociatesMaterial"):
continue
results.extend(inverse.RelatedObjects)
return results
def regenerate_from_type(self, usecase_path, ifc_file, settings):
obj = tool.Ifc.get_object(settings["related_object"])
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
@@ -59,6 +59,8 @@ def update_ifc_class(self, context):
bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class)
AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id()
AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail()
if not tool.Blender.enum_property_has_valid_index(self, "relating_type_id", AuthoringData.data["relating_type_id"]):
self["relating_type_id"] = 0
def update_type_class(self, context):
@@ -69,6 +71,10 @@ def update_type_class(self, context):
AuthoringData.data["paginated_relating_types"] = AuthoringData.paginated_relating_types()
AuthoringData.data["type_predefined_type"] = AuthoringData.type_predefined_type()
type_class = self.type_class
if (type_class, type_class, "") in get_ifc_class(self, context):
self.ifc_class = type_class
def update_relating_type_id(self, context):
AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id()
@@ -86,6 +92,7 @@ class BIMModelProperties(PropertyGroup):
)
icon_id: bpy.props.IntProperty()
updating: bpy.props.BoolProperty(default=False)
is_adding_type: bpy.props.BoolProperty(default=False)
occurrence_name_style: bpy.props.EnumProperty(
items=[("CLASS", "By Class", ""), ("TYPE", "By Type", ""), ("CUSTOM", "Custom", "")],
name="Occurrence Name Style",
@@ -126,7 +133,7 @@ class BIMModelProperties(PropertyGroup):
z: bpy.props.FloatProperty(name="Z", default=0.5)
rl1: bpy.props.FloatProperty(name="RL", default=1) # Used for things like walls, doors, flooring, skirting, etc
rl2: bpy.props.FloatProperty(name="RL", default=1) # Used for things like windows, other hosted furniture
x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE")
x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE", min=0, max=pi / 180 * 89)
type_page: bpy.props.IntProperty(name="Type Page", default=1, update=update_type_page)
type_template: bpy.props.EnumProperty(
items=(
@@ -183,7 +190,7 @@ class BIMStairProperties(PropertyGroup):
("GENERIC", "Generic", ""),
)
is_editing: bpy.props.IntProperty(default=-1)
is_editing: bpy.props.BoolProperty(default=False)
width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01, subtype="DISTANCE")
height: bpy.props.FloatProperty(name="Height", default=1.0, soft_min=0.01, subtype="DISTANCE")
number_of_treads: bpy.props.IntProperty(name="Number of treads", default=6, soft_min=1)
@@ -194,16 +201,18 @@ class BIMStairProperties(PropertyGroup):
has_top_nib: bpy.props.BoolProperty(name="Has top nib", default=True)
stair_type: bpy.props.EnumProperty(name="Stair type", items=stair_types, default="CONCRETE")
def get_props_kwargs(self, convert_to_project_units=False):
def get_props_kwargs(self, convert_to_project_units=False, stair_type=None):
if not stair_type:
stair_type = self.stair_type
stair_kwargs = {
"stair_type": self.stair_type,
"stair_type": stair_type,
"width": self.width,
"height": self.height,
"number_of_treads": self.number_of_treads,
"tread_run": self.tread_run,
}
if self.stair_type == "CONCRETE":
if stair_type == "CONCRETE":
concrete_props = {
"base_slab_depth": self.base_slab_depth,
"top_slab_depth": self.top_slab_depth,
@@ -212,13 +221,13 @@ class BIMStairProperties(PropertyGroup):
}
stair_kwargs.update(concrete_props)
elif self.stair_type == "WOOD/STEEL":
elif stair_type == "WOOD/STEEL":
wood_steel_props = {
"tread_depth": self.tread_depth,
}
stair_kwargs.update(wood_steel_props)
elif self.stair_type == "GENERIC":
elif stair_type == "GENERIC":
pass
if not convert_to_project_units:
@@ -273,7 +282,7 @@ class BIMWindowProperties(PropertyGroup):
}
# fmt: on
is_editing: bpy.props.IntProperty(default=-1)
is_editing: bpy.props.BoolProperty(default=False)
window_type: bpy.props.EnumProperty(
name="Window Type", items=window_types, default="SINGLE_PANEL", update=window_type_prop_update
)
@@ -333,7 +342,9 @@ class BIMWindowProperties(PropertyGroup):
return kwargs
return tool.Model.convert_data_to_project_units(kwargs, ["window_type"])
def get_lining_kwargs(self, convert_to_project_units=False):
def get_lining_kwargs(self, window_type=None, convert_to_project_units=False):
if not window_type:
window_type = self.window_type
kwargs = {
"lining_depth": self.lining_depth,
"lining_thickness": self.lining_thickness,
@@ -342,7 +353,7 @@ class BIMWindowProperties(PropertyGroup):
"lining_to_panel_offset_y": self.lining_to_panel_offset_y,
}
if self.window_type in (
if window_type in (
"DOUBLE_PANEL_VERTICAL",
"TRIPLE_PANEL_BOTTOM",
"TRIPLE_PANEL_TOP",
@@ -353,7 +364,7 @@ class BIMWindowProperties(PropertyGroup):
kwargs["mullion_thickness"] = self.mullion_thickness
kwargs["first_mullion_offset"] = self.first_mullion_offset
if self.window_type in (
if window_type in (
"DOUBLE_PANEL_HORIZONTAL",
"TRIPLE_PANEL_BOTTOM",
"TRIPLE_PANEL_TOP",
@@ -364,10 +375,10 @@ class BIMWindowProperties(PropertyGroup):
kwargs["transom_thickness"] = self.transom_thickness
kwargs["first_transom_offset"] = self.first_transom_offset
if self.window_type in ("TRIPLE_PANEL_VERTICAL",):
if window_type in ("TRIPLE_PANEL_VERTICAL",):
kwargs["second_mullion_offset"] = self.second_mullion_offset
if self.window_type in ("TRIPLE_PANEL_HORIZONTAL",):
if window_type in ("TRIPLE_PANEL_HORIZONTAL",):
kwargs["second_transom_offset"] = self.second_transom_offset
if not convert_to_project_units:
@@ -402,7 +413,7 @@ class BIMDoorProperties(PropertyGroup):
("DOUBLE_DOOR_SLIDING", "DOUBLE_DOOR_SLIDING", ""),
)
is_editing: bpy.props.IntProperty(default=-1)
is_editing: bpy.props.BoolProperty(default=False)
door_type: bpy.props.EnumProperty(name="Door Operation Type", items=door_types, default="SINGLE_SWING_LEFT")
overall_height: bpy.props.FloatProperty(name="Overall Height", default=2.0, subtype="DISTANCE")
overall_width: bpy.props.FloatProperty(name="Overall Width", default=0.9, subtype="DISTANCE")
@@ -415,39 +426,43 @@ class BIMDoorProperties(PropertyGroup):
description="Offset from the outer side of the wall (by Y-axis). "
"If present then adding casing is not possible.\n"
"`0.025 mm` is good as default value",
default=0.0,
subtype="DISTANCE"
default=0.0,
subtype="DISTANCE",
)
lining_to_panel_offset_x: bpy.props.FloatProperty(
name="Lining to Panel Offset X", default=0.025, subtype="DISTANCE"
)
lining_to_panel_offset_y: bpy.props.FloatProperty(
name="Lining to Panel Offset Y", default=0.025, subtype="DISTANCE"
)
lining_to_panel_offset_x: bpy.props.FloatProperty(name="Lining to Panel Offset X", default=0.025, subtype="DISTANCE")
lining_to_panel_offset_y: bpy.props.FloatProperty(name="Lining to Panel Offset Y", default=0.025, subtype="DISTANCE")
transom_thickness: bpy.props.FloatProperty(
name="Transom Thickness",
description="Set values > 0 to add a transom.\n" "`0.050 mm` is good as default value",
default=0.000,
subtype="DISTANCE"
subtype="DISTANCE",
)
transom_offset: bpy.props.FloatProperty(
name="Transom Offset",
description="Distance from the bottom door opening to the beginning of the transom (unlike windows)",
default=1.525,
subtype="DISTANCE"
subtype="DISTANCE",
)
casing_thickness: bpy.props.FloatProperty(
name="Casing Thickness", description="Set values > 0 and LiningOffset = 0 to add a casing.", default=0.075,
subtype="DISTANCE"
name="Casing Thickness",
description="Set values > 0 and LiningOffset = 0 to add a casing.",
default=0.075,
subtype="DISTANCE",
)
casing_depth: bpy.props.FloatProperty(name="Casing Depth", default=0.005, subtype="DISTANCE")
threshold_thickness: bpy.props.FloatProperty(
name="Threshold Thickness", description="Set values > 0 to add a threshold.", default=0.025,
subtype="DISTANCE"
name="Threshold Thickness", description="Set values > 0 to add a threshold.", default=0.025, subtype="DISTANCE"
)
threshold_depth: bpy.props.FloatProperty(name="Threshold Depth", default=0.1, subtype="DISTANCE")
threshold_offset: bpy.props.FloatProperty(
name="Threshold Offset", description="`0.025 mm` is good as default value", default=0.000,
subtype="DISTANCE"
name="Threshold Offset", description="`0.025 mm` is good as default value", default=0.000, subtype="DISTANCE"
)
# panel properties
@@ -472,28 +487,35 @@ class BIMDoorProperties(PropertyGroup):
return kwargs
return tool.Model.convert_data_to_project_units(kwargs, ["door_type"])
def get_lining_kwargs(self, convert_to_project_units=False):
def get_lining_kwargs(self, convert_to_project_units=False, door_type=None, lining_data=None):
if not door_type:
door_type = self.door_type
transom_thickness = lining_data["transom_thickness"] if lining_data else self.transom_thickness
lining_offset = lining_data["lining_offset"] if lining_data else self.lining_offset
threshold_thickness = lining_data["threshold_thickness"] if lining_data else self.threshold_thickness
kwargs = {
"lining_depth": self.lining_depth,
"lining_thickness": self.lining_thickness,
"lining_offset": self.lining_offset,
"lining_offset": lining_offset,
}
if "SLIDING" not in self.door_type:
if "SLIDING" not in door_type:
kwargs["lining_to_panel_offset_x"] = self.lining_to_panel_offset_x
kwargs["lining_to_panel_offset_y"] = self.lining_to_panel_offset_y
kwargs["transom_thickness"] = self.transom_thickness
if self.transom_thickness:
kwargs["transom_thickness"] = transom_thickness
if transom_thickness:
kwargs["transom_offset"] = self.transom_offset
if not self.lining_offset:
if not lining_offset:
kwargs["casing_thickness"] = self.casing_thickness
if self.casing_thickness:
kwargs["casing_depth"] = self.casing_depth
kwargs["threshold_thickness"] = self.threshold_thickness
if self.threshold_thickness:
kwargs["threshold_thickness"] = threshold_thickness
if threshold_thickness:
kwargs["threshold_depth"] = self.threshold_depth
kwargs["threshold_offset"] = self.threshold_offset
@@ -501,16 +523,17 @@ class BIMDoorProperties(PropertyGroup):
return kwargs
return tool.Model.convert_data_to_project_units(kwargs)
def get_panel_kwargs(self, convert_to_project_units=False):
def get_panel_kwargs(self, convert_to_project_units=False, lining_data=None):
transom_thickness = lining_data["transom_thickness"] if lining_data else self.transom_thickness
kwargs = {"panel_depth": self.panel_depth, "panel_width_ratio": self.panel_width_ratio}
if self.transom_thickness:
if transom_thickness:
kwargs["frame_thickness"] = self.frame_thickness
kwargs["frame_depth"] = self.frame_depth
if not convert_to_project_units:
return kwargs
return tool.Model.convert_data_to_project_units(kwargs, ("panel_width_ratio", ))
return tool.Model.convert_data_to_project_units(kwargs, ("panel_width_ratio",))
def set_props_kwargs_from_ifc_data(self, kwargs):
kwargs = tool.Model.convert_data_to_si_units(kwargs, self.non_si_units_props)
@@ -540,7 +563,7 @@ class BIMRailingProperties(PropertyGroup):
("NONE", "NONE", ""),
)
is_editing: bpy.props.IntProperty(default=-1)
is_editing: bpy.props.BoolProperty(default=False)
is_editing_path: bpy.props.BoolProperty(default=False)
railing_type: bpy.props.EnumProperty(name="Railing Type", items=railing_types, default="FRAMELESS_PANEL")
@@ -556,26 +579,36 @@ class BIMRailingProperties(PropertyGroup):
"If disabled, supports are added automatically based on the support spacing",
)
support_spacing: bpy.props.FloatProperty(
name="Support Spacing", default=1.0, description="Distance between supports if automatic supports are used"
name="Support Spacing",
default=1.0,
min=0.01,
description="Distance between supports if automatic supports are used",
subtype="DISTANCE",
)
railing_diameter: bpy.props.FloatProperty(name="Railing Diameter", default=0.050)
railing_diameter: bpy.props.FloatProperty(name="Railing Diameter", default=0.050, subtype="DISTANCE")
clear_width: bpy.props.FloatProperty(
name="Clear Width", default=0.040, description="Clear width between the railing and the wall"
name="Clear Width",
default=0.040,
description="Clear width between the railing and the wall",
subtype="DISTANCE",
)
terminal_type: bpy.props.EnumProperty(name="Terminal Type", items=cap_types, default="180")
def get_general_kwargs(self, convert_to_project_units=False):
def get_general_kwargs(self, railing_type=None, convert_to_project_units=False):
if railing_type is None:
railing_type = self.railing_type
base_kwargs = {
"railing_type": self.railing_type,
"railing_type": railing_type,
"height": self.height,
}
additional_kwargs = {}
if self.railing_type == "FRAMELESS_PANEL":
if railing_type == "FRAMELESS_PANEL":
additional_kwargs = {
"thickness": self.thickness,
"spacing": self.spacing,
}
elif self.railing_type == "WALL_MOUNTED_HANDRAIL":
elif railing_type == "WALL_MOUNTED_HANDRAIL":
additional_kwargs = {
"railing_diameter": self.railing_diameter,
"clear_width": self.clear_width,
@@ -596,6 +629,7 @@ class BIMRailingProperties(PropertyGroup):
for prop_name in kwargs:
setattr(self, prop_name, kwargs[prop_name])
class BIMRoofProperties(PropertyGroup):
non_si_units_props = (
"is_editing",
@@ -611,7 +645,7 @@ class BIMRoofProperties(PropertyGroup):
("ANGLE", "ANGLE", ""),
)
is_editing: bpy.props.IntProperty(default=-1)
is_editing: bpy.props.BoolProperty(default=False)
is_editing_path: bpy.props.BoolProperty(default=False)
roof_type: bpy.props.EnumProperty(name="Roof Type", items=roof_types, default="HIP/GABLE ROOF")
@@ -625,14 +659,16 @@ class BIMRoofProperties(PropertyGroup):
roof_thickness: bpy.props.FloatProperty(name="Roof Thickness", default=0.1, subtype="DISTANCE")
rafter_edge_angle: bpy.props.FloatProperty(name="Rafter Edge Angle", min=0, max=pi, default=pi / 2, subtype="ANGLE")
def get_general_kwargs(self, convert_to_project_units=False):
def get_general_kwargs(self, generation_method=None, convert_to_project_units=False):
if generation_method is None:
generation_method = self.generation_method
kwargs = {
"roof_type": self.roof_type,
"generation_method": self.generation_method,
"generation_method": generation_method,
"roof_thickness": self.roof_thickness,
"rafter_edge_angle": self.rafter_edge_angle,
}
if self.generation_method == "HEIGHT":
if generation_method == "HEIGHT":
kwargs["height"] = self.height
else:
kwargs["angle"] = self.angle
@@ -23,14 +23,12 @@ import ifcopenshell
from ifcopenshell.util.shape_builder import V
import blenderbim
import blenderbim.tool as tool
from blenderbim.bim.helper import convert_property_group_from_si
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.door import bm_sort_out_geom
from blenderbim.bim.module.model.data import RailingData, refresh
from blenderbim.bim.module.model.decorator import ProfileDecorator
from mathutils import Vector, Matrix
from pprint import pprint
from mathutils import Vector
import json
# reference:
@@ -38,7 +36,6 @@ import json
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRailingType.htm
def bm_split_edge_at_offset(edge, offset):
v0, v1 = edge.verts
@@ -83,7 +80,9 @@ def update_railing_modifier_ifc_data(context):
if props.railing_type == "WALL_MOUNTED_HANDRAIL":
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
railing_path = [Vector(v) for v in RailingData.data["parameters"]["data_dict"]["path_data"]["verts"]]
pset_data = tool.Model.get_modeling_bbim_pset_data(bpy.context.active_object, "BBIM_Railing")
path_data = pset_data["data_dict"]["path_data"]
railing_path = [Vector(v) for v in path_data["verts"]]
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
representation_data = {
@@ -118,12 +117,16 @@ def update_bbim_railing_pset(element, railing_data):
def update_railing_modifier_bmesh(context):
"""before using should make sure that Data contains up-to-date information.
If BBIM Pset just changed should call refresh() before updating bmesh
"""
obj = context.object
props = obj.BIMRailingProperties
# NOTE: using Data since bmesh update will hapen very often
if not RailingData.is_loaded:
RailingData.load()
path_data = RailingData.data["parameters"]["data_dict"]["path_data"]
path_data = RailingData.data["path_data"]
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
# need to make sure we support edit mode
@@ -196,10 +199,11 @@ def update_railing_modifier_bmesh(context):
verts_to_dissolve.append(other_vert)
bmesh.ops.dissolve_edges(bm, edges=edges_to_dissolve)
bmesh.ops.dissolve_verts(bm, verts=verts_to_dissolve)
# to remove unnecessary verts in 0 spacing case
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
tool.Blender.apply_bmesh(obj.data, bm)
generate_frameless_panel_railing()
@@ -241,9 +245,15 @@ def get_path_data(obj):
other_edge = lambda edges, edge: next(e for e in edges if e != edge)
while len(link_edges := v.link_edges) != 1:
prev_v = v
link_edges = v.link_edges
edge = other_edge(link_edges, edge)
v = edge.other_vert(v)
v = edge.other_vert(prev_v)
# skip path verts if they just go vertical to avoid errors
if (v.co.xy - prev_v.co.xy).length <= 0.0001:
continue
points.append(v.co)
segments.append((i - 1, i))
i += 1
@@ -273,6 +283,9 @@ class BIM_OT_add_railing(bpy.types.Operator, tool.Ifc.Operator):
mesh = bpy.data.meshes.new("IfcRailing")
obj = bpy.data.objects.new("IfcRailing", mesh)
obj.location = spawn_location
collection = context.view_layer.active_layer_collection.collection
collection.objects.link(obj)
body_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
blenderbim.core.root.assign_class(
tool.Ifc,
@@ -307,7 +320,6 @@ class AddRailing(bpy.types.Operator, tool.Ifc.Operator):
self.report({"ERROR"}, "Object has to be IfcRailing/IfcRailingType type to add a railing.")
return {"CANCELLED"}
railing_data = props.get_general_kwargs(convert_to_project_units=True)
path_data = get_path_data(obj)
if not path_data:
@@ -343,7 +355,7 @@ class EnableEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
# required since we could load pset from .ifc and BIMRailingProperties won't be set
props.set_props_kwargs_from_ifc_data(data)
props.is_editing = 1
props.is_editing = True
return {"FINISHED"}
@@ -357,21 +369,12 @@ class CancelEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Railing", "Data"))
props = obj.BIMRailingProperties
# restore previous settings since editing was canceled
props.set_props_kwargs_from_ifc_data(data)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
props.is_editing = -1
update_railing_modifier_bmesh(context)
props.is_editing = False
return {"FINISHED"}
@@ -385,13 +388,12 @@ class FinishEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
props = obj.BIMRailingProperties
if not RailingData.is_loaded:
RailingData.load()
path_data = RailingData.data["parameters"]["data_dict"]["path_data"]
pset_data = tool.Model.get_modeling_bbim_pset_data(bpy.context.active_object, "BBIM_Railing")
path_data = pset_data["data_dict"]["path_data"]
railing_data = props.get_general_kwargs(convert_to_project_units=True)
railing_data["path_data"] = path_data
props.is_editing = -1
props.is_editing = False
update_bbim_railing_pset(element, railing_data)
update_railing_modifier_ifc_data(context)
@@ -409,9 +411,8 @@ class FlipRailingPathOrder(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
props = obj.BIMRailingProperties
if not RailingData.is_loaded:
RailingData.load()
path_data = RailingData.data["parameters"]["data_dict"]["path_data"]
pset_data = tool.Model.get_modeling_bbim_pset_data(bpy.context.active_object, "BBIM_Railing")
path_data = pset_data["data_dict"]["path_data"]
# flip the vertex order and edges
path_data["verts"] = path_data["verts"][::-1]
@@ -487,7 +488,9 @@ class FinishEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator):
props.is_editing_path = False
update_bbim_railing_pset(element, railing_data)
refresh() # RailingData has to be updated before run update_railing_modifier_bmesh
# RailingData has to be updated before run update_railing_modifier_bmesh
# since we know that BBIM_Railing could have changed
refresh()
update_railing_modifier_bmesh(context)
if bpy.context.object.mode == "EDIT":
bpy.ops.object.mode_set(mode="OBJECT")
@@ -502,9 +505,8 @@ class RemoveRailing(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = context.active_object
props = obj.BIMRailingProperties
element = tool.Ifc.get_entity(obj)
obj.BIMRailingProperties.is_editing = -1
obj.BIMRailingProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Railing")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
@@ -383,20 +383,6 @@ def bm_get_indices(sequence):
return [i.index for i in sequence]
def roof_is_gabled():
if not RoofData.is_loaded:
RoofData.load()
path_data = RoofData.parameters()["data_dict"]["path_data"]
angle_layer = path_data.get("gable_roof_angles", None)
if not angle_layer:
return False
for edge_angle in angle_layer:
if float_is_zero(edge_angle - pi / 2):
return True
return False
def update_roof_modifier_ifc_data(context):
"""should be called after new geometry settled
since it's going to update ifc representation
@@ -405,6 +391,17 @@ def update_roof_modifier_ifc_data(context):
props = obj.BIMRoofProperties
element = tool.Ifc.get_entity(obj)
def roof_is_gabled():
pset_data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Roof")
path_data = pset_data["data_dict"]["path_data"]
angle_layer = path_data.get("gable_roof_angles", None)
if not angle_layer:
return False
for edge_angle in angle_layer:
if float_is_zero(edge_angle - pi / 2):
return True
return False
# type attributes
if props.roof_type == "HIP/GABLE ROOF":
element.PredefinedType = "GABLE_ROOF" if roof_is_gabled() else "HIP_ROOF"
@@ -424,12 +421,16 @@ def update_bbim_roof_pset(element, roof_data):
def update_roof_modifier_bmesh(context):
"""before using should make sure that Data contains up-to-date information.
If BBIM Pset just changed should call refresh() before updating bmesh
"""
obj = context.object
props = obj.BIMRoofProperties
# NOTE: using Data since bmesh update will hapen very often
if not RoofData.is_loaded:
RoofData.load()
path_data = RoofData.data["parameters"]["data_dict"]["path_data"]
path_data = RoofData.data["pset_data"]["data_dict"]["path_data"]
angle_layer_data = path_data.get("gable_roof_angles", None)
separate_verts_data = path_data.get("gable_roof_separate_verts", None)
@@ -514,6 +515,9 @@ class BIM_OT_add_roof(bpy.types.Operator, tool.Ifc.Operator):
mesh = bpy.data.meshes.new("IfcRoof")
obj = bpy.data.objects.new("IfcRoof", mesh)
obj.location = spawn_location
collection = context.view_layer.active_layer_collection.collection
collection.objects.link(obj)
body_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
blenderbim.core.root.assign_class(
tool.Ifc,
@@ -593,7 +597,7 @@ class EnableEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Roof", "Data"))
# required since we could load pset from .ifc and BIMRoofProperties won't be set
props.set_props_kwargs_from_ifc_data(data)
props.is_editing = 1
props.is_editing = True
return {"FINISHED"}
@@ -607,21 +611,12 @@ class CancelEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Roof", "Data"))
props = obj.BIMRoofProperties
# restore previous settings since editing was canceled
props.set_props_kwargs_from_ifc_data(data)
update_roof_modifier_bmesh(context)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
props.is_editing = -1
props.is_editing = False
return {"FINISHED"}
@@ -635,13 +630,12 @@ class FinishEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
props = obj.BIMRoofProperties
if not RoofData.is_loaded:
RoofData.load()
path_data = RoofData.data["parameters"]["data_dict"]["path_data"]
pset_data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Roof")
path_data = pset_data["data_dict"]["path_data"]
roof_data = props.get_general_kwargs(convert_to_project_units=True)
roof_data["path_data"] = path_data
props.is_editing = -1
props.is_editing = False
update_bbim_roof_pset(element, roof_data)
update_roof_modifier_ifc_data(context)
@@ -766,9 +760,8 @@ class RemoveRoof(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = context.active_object
props = obj.BIMRoofProperties
element = tool.Ifc.get_entity(obj)
obj.BIMRoofProperties.is_editing = -1
obj.BIMRoofProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Roof")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
@@ -250,7 +250,8 @@ def update_ifc_stair_props(obj):
props = obj.BIMStairProperties
ifc_file = tool.Ifc.get()
element.PredefinedType = "STRAIGHT"
if tool.Ifc.get_schema() != "IFC2X3":
element.PredefinedType = "STRAIGHT"
number_of_risers = props.number_of_treads + 1
# update IfcStairFlight properties (seems already deprecated but keep it for now)
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcStairFlight.htm
@@ -260,7 +261,11 @@ def update_ifc_stair_props(obj):
tread_length = props.tread_depth / si_conversion
if element.is_a("IfcStairFlight"):
element.NumberOfRisers = number_of_risers
if tool.Ifc.get_schema() == "IFC2X3":
element.NumberOfRiser = number_of_risers
else:
element.NumberOfRisers = number_of_risers
element.NumberOfTreads = props.number_of_treads
element.RiserHeight = riser_height
element.TreadLength = tread_length
@@ -320,6 +325,9 @@ class BIM_OT_add_clever_stair(bpy.types.Operator, tool.Ifc.Operator):
mesh = bpy.data.meshes.new("IfcStairFlight")
obj = bpy.data.objects.new("StairFlight", mesh)
obj.location = spawn_location
collection = context.view_layer.active_layer_collection.collection
collection.objects.link(obj)
body_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
element = blenderbim.core.root.assign_class(
tool.Ifc,
@@ -330,7 +338,8 @@ class BIM_OT_add_clever_stair(bpy.types.Operator, tool.Ifc.Operator):
should_add_representation=True,
context=body_context,
)
element.PredefinedType = "STRAIGHT"
if tool.Ifc.get_schema() != "IFC2X3":
element.PredefinedType = "STRAIGHT"
bpy.ops.object.select_all(action="DESELECT")
bpy.context.view_layer.objects.active = None
@@ -385,7 +394,7 @@ class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
props.set_props_kwargs_from_ifc_data(data)
update_stair_modifier(context)
props.is_editing = -1
props.is_editing = False
return {"FINISHED"}
@@ -401,7 +410,7 @@ class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator):
props = obj.BIMStairProperties
data = props.get_props_kwargs(convert_to_project_units=True)
props.is_editing = -1
props.is_editing = False
update_stair_modifier(context)
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
@@ -425,7 +434,7 @@ class EnableEditingStair(bpy.types.Operator, tool.Ifc.Operator):
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Stair", "Data"))
# required since we could load pset from .ifc and BIMStairProperties won't be set
props.set_props_kwargs_from_ifc_data(data)
props.is_editing = 1
props.is_editing = True
return {"FINISHED"}
@@ -438,7 +447,7 @@ class RemoveStair(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object
props = obj.BIMStairProperties
element = tool.Ifc.get_entity(obj)
obj.BIMStairProperties.is_editing = -1
obj.BIMStairProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
@@ -18,7 +18,7 @@
import bpy
import blenderbim.tool as tool
from bpy.types import Panel, Operator, Menu
from bpy.types import Panel, Menu
from blenderbim.bim.module.model.data import (
AuthoringData,
ArrayData,
@@ -36,13 +36,12 @@ from blenderbim.bim.module.model.door import update_door_modifier_bmesh
from blenderbim.bim.module.model.railing import update_railing_modifier_bmesh
from blenderbim.bim.module.model.roof import update_roof_modifier_bmesh
from blenderbim.bim.helper import prop_with_search
from math import degrees
class LaunchTypeManager(bpy.types.Operator):
bl_idname = "bim.launch_type_manager"
bl_label = "Launch Type Manager"
bl_options = {"REGISTER"}
bl_options = {"REGISTER", "UNDO"}
bl_description = "Display all available Construction Types to add new instances"
def execute(self, context):
@@ -70,10 +69,7 @@ class LaunchTypeManager(bpy.types.Operator):
row = columns.row(align=True)
row.alignment = "CENTER"
row.prop(props, "type_predefined_type", text="")
row.prop(props, "type_template", text="")
row.prop(props, "type_name", text="")
row.operator("bim.add_type", icon="ADD", text="")
# In case you want something here in the future
row = columns.row(align=True)
row.alignment = "RIGHT"
@@ -86,6 +82,22 @@ class LaunchTypeManager(bpy.types.Operator):
op = row.operator("bim.change_type_page", icon="TRIA_RIGHT", text="")
op.page = AuthoringData.data["next_page"]
if props.is_adding_type:
row = self.layout.row()
box = row.box()
row = box.row()
row.prop(props, "type_predefined_type")
row = box.row()
row.prop(props, "type_template")
row = box.row()
row.prop(props, "type_name")
row = box.row(align=True)
row.operator("bim.add_type", icon="CHECKMARK", text="Save New Type")
row.operator("bim.disable_add_type", icon="CANCEL", text="")
else:
row = self.layout.row()
row.operator("bim.enable_add_type", icon="ADD", text="Create New Type")
flow = self.layout.grid_flow(row_major=True, columns=3, even_columns=True, even_rows=True, align=True)
for relating_type in AuthoringData.data["paginated_relating_types"]:
@@ -115,6 +127,8 @@ class LaunchTypeManager(bpy.types.Operator):
op.ifc_class = relating_type["ifc_class"]
op.relating_type_id = relating_type["id"]
op = row.operator("bim.rename_type", icon="GREASEPENCIL", text="")
op.element = relating_type["id"]
op = row.operator("bim.select_type", icon="OBJECT_DATA", text="")
op.relating_type = relating_type["id"]
op = row.operator("bim.duplicate_type", icon="DUPLICATE", text="")
@@ -176,6 +190,9 @@ class BIM_PT_array(bpy.types.Panel):
row.label(text=ArrayData.data["parameters"]["parent_name"], icon="CON_CHILDOF")
op = row.operator("bim.select_array_parent", icon="OBJECT_DATA", text="")
op.parent = ArrayData.data["parameters"]["Parent"]
op = row.operator("bim.select_all_array_objects", icon="RESTRICT_SELECT_OFF", text="")
op.parent = ArrayData.data["parameters"]["Parent"]
if ArrayData.data["parameters"]["data_dict"]:
row.operator("bim.add_array", icon="ADD", text="")
@@ -237,12 +254,12 @@ class BIM_PT_stair(bpy.types.Panel):
props = context.active_object.BIMStairProperties
if StairData.data["parameters"]:
if StairData.data["pset_data"]:
row = self.layout.row(align=True)
row.label(text="Stair parameters", icon="IPO_CONSTANT")
stair_data = StairData.data["parameters"]["data_dict"]
if props.is_editing != -1:
stair_data = StairData.data["pset_data"]["data_dict"]
if props.is_editing:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_stair", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
@@ -254,11 +271,9 @@ class BIM_PT_stair(bpy.types.Panel):
row.operator("bim.enable_editing_stair", icon="GREASEPENCIL", text="")
row.operator("bim.remove_stair", icon="X", text="")
row = self.layout.row(align=True)
for prop in props.get_props_kwargs():
prop_value = stair_data[prop]
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
for prop_name, prop_value in StairData.data["general_params"].items():
row = self.layout.row(align=True)
row.label(text=f"{props.bl_rna.properties[prop].name}")
row.label(text=prop_name)
row.label(text=str(prop_value))
# calculated properties
@@ -335,14 +350,12 @@ class BIM_PT_window(bpy.types.Panel):
props = context.active_object.BIMWindowProperties
if WindowData.data["parameters"]:
if WindowData.data["pset_data"]:
row = self.layout.row(align=True)
row.label(text="Window parameters", icon="OUTLINER_OB_LATTICE")
window_data = WindowData.data["parameters"]["data_dict"]
number_of_panels, panels_data = props.window_types_panels[props.window_type]
if props.is_editing != -1:
if props.is_editing:
number_of_panels, panels_data = props.window_types_panels[props.window_type]
row = self.layout.row(align=True)
row.operator("bim.finish_editing_window", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_window", icon="CANCEL", text="")
@@ -383,25 +396,22 @@ class BIM_PT_window(bpy.types.Panel):
row.operator("bim.remove_window", icon="X", text="")
box = self.layout.box()
general_props = props.get_general_kwargs()
for prop in general_props:
prop_value = window_data[prop]
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
general_params = WindowData.data["general_params"]
window_type_prop = props.bl_rna.properties["window_type"].name
number_of_panels, panels_data = props.window_types_panels[general_params[window_type_prop]]
for prop_name, prop_value in general_params.items():
row = box.row(align=True)
row.label(text=f"{props.bl_rna.properties[prop].name}")
row.label(text=prop_name)
row.label(text=str(prop_value))
lining_props = props.get_lining_kwargs()
self.layout.label(text="Lining properties")
lining_box = self.layout.box()
for prop in lining_props:
prop_value = window_data["lining_properties"][prop]
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
row = lining_box.row(align=True)
row.label(text=f"{props.bl_rna.properties[prop].name}")
box = self.layout.box()
for prop_name, prop_value in WindowData.data["lining_params"].items():
row = box.row(align=True)
row.label(text=prop_name)
row.label(text=str(prop_value))
panel_props = props.get_panel_kwargs()
panel_props = WindowData.data["panel_params"]
self.layout.label(text="Panel properties")
panel_box = self.layout.box()
@@ -415,14 +425,12 @@ class BIM_PT_window(bpy.types.Panel):
r.label(text=f"#{panel_i}")
r = cols[panel_i + 1].row()
# TODO: align property values more evenly
for prop in panel_props:
cols[0].row().label(text=f"{props.bl_rna.properties[prop].name}")
for prop_name in panel_props:
cols[0].row().label(text=prop_name)
for panel_i in range(number_of_panels):
r = cols[panel_i + 1].row()
r.alignment = "CENTER"
prop_value = window_data["panel_properties"][prop][panel_i]
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
prop_value = panel_props[prop_name][panel_i]
r.label(text=str(prop_value))
r = cols[panel_i + 1].row()
@@ -451,13 +459,11 @@ class BIM_PT_door(bpy.types.Panel):
props = context.active_object.BIMDoorProperties
if DoorData.data["parameters"]:
if DoorData.data["pset_data"]:
row = self.layout.row(align=True)
row.label(text="Door parameters", icon="OUTLINER_OB_LATTICE")
door_data = DoorData.data["parameters"]["data_dict"]
if props.is_editing != -1:
if props.is_editing:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_door", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_door", icon="CANCEL", text="")
@@ -483,32 +489,23 @@ class BIM_PT_door(bpy.types.Panel):
row.operator("bim.remove_door", icon="X", text="")
box = self.layout.box()
general_props = props.get_general_kwargs()
for prop in general_props:
prop_value = door_data[prop]
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
for prop_name, prop_value in DoorData.data["general_params"].items():
row = box.row(align=True)
row.label(text=f"{props.bl_rna.properties[prop].name}")
row.label(text=prop_name)
row.label(text=str(prop_value))
lining_props = props.get_lining_kwargs()
self.layout.label(text="Lining properties")
lining_box = self.layout.box()
for prop in lining_props:
prop_value = door_data["lining_properties"][prop]
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
for prop_name, prop_value in DoorData.data["lining_params"].items():
row = lining_box.row(align=True)
row.label(text=f"{props.bl_rna.properties[prop].name}")
row.label(text=prop_name)
row.label(text=str(prop_value))
panel_props = props.get_panel_kwargs()
self.layout.label(text="Panel properties")
panel_box = self.layout.box()
for prop in panel_props:
prop_value = door_data["panel_properties"][prop]
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
for prop_name, prop_value in DoorData.data["panel_params"].items():
row = panel_box.row(align=True)
row.label(text=f"{props.bl_rna.properties[prop].name}")
row.label(text=prop_name)
row.label(text=str(prop_value))
else:
row = self.layout.row()
@@ -535,19 +532,22 @@ class BIM_PT_railing(bpy.types.Panel):
props = context.active_object.BIMRailingProperties
if RailingData.data["parameters"]:
if RailingData.data["pset_data"]:
row = self.layout.row(align=True)
row.label(text="Railing parameters", icon="OUTLINER_OB_LATTICE")
railing_data = RailingData.data["parameters"]["data_dict"]
if props.is_editing != -1:
if props.is_editing:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_railing", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_railing", icon="CANCEL", text="")
general_props = props.get_general_kwargs()
for prop in general_props:
if prop == "support_spacing" and props.use_manual_supports:
row = self.layout.row()
row.prop(props, prop)
row.active = False
continue
self.layout.prop(props, prop)
update_railing_modifier_bmesh(context)
@@ -559,18 +559,15 @@ class BIM_PT_railing(bpy.types.Panel):
else:
row.operator("bim.enable_editing_railing", icon="GREASEPENCIL", text="")
row.operator("bim.enable_editing_railing_path", icon="ANIM", text="")
# TODO: good for preview but probably should move to .is_editing == -1
# TODO: good for preview but probably should move to .is_editing == True
# since it's writing to ifc
row.operator("bim.flip_railing_path_order", icon="ARROW_LEFTRIGHT", text="")
row.operator("bim.remove_railing", icon="X", text="")
box = self.layout.box()
general_props = props.get_general_kwargs()
for prop in general_props:
prop_value = railing_data[prop]
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
for prop_name, prop_value in RailingData.data["general_params"].items():
row = box.row(align=True)
row.label(text=f"{props.bl_rna.properties[prop].name}")
row.label(text=prop_name)
row.label(text=str(prop_value))
else:
row = self.layout.row()
@@ -597,13 +594,11 @@ class BIM_PT_roof(bpy.types.Panel):
props = context.active_object.BIMRoofProperties
if RoofData.data["parameters"]:
if RoofData.data["pset_data"]:
row = self.layout.row(align=True)
row.label(text="Roof parameters", icon="OUTLINER_OB_LATTICE")
roof_data = RoofData.data["parameters"]["data_dict"]
if props.is_editing != -1:
if props.is_editing:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_roof", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_roof", icon="CANCEL", text="")
@@ -624,14 +619,9 @@ class BIM_PT_roof(bpy.types.Panel):
row.operator("bim.remove_roof", icon="X", text="")
box = self.layout.box()
general_props = props.get_general_kwargs()
for prop in general_props:
prop_value = roof_data[prop]
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
for prop_name, prop_value in RoofData.data["general_params"].items():
row = box.row(align=True)
row.label(text=f"{props.bl_rna.properties[prop].name}")
if prop in ("angle", "rafter_edge_angle"):
prop_value = round(degrees(prop_value), 2)
row.label(text=prop_name)
row.label(text=str(prop_value))
else:
row = self.layout.row()
@@ -60,20 +60,33 @@ class JoinWall(bpy.types.Operator, tool.Ifc.Operator):
for obj in selected_objs:
joiner.unjoin(obj)
return {"FINISHED"}
if not context.active_object:
return {"FINISHED"}
if not context.active_object or not context.active_object.BIMObjectProperties.ifc_definition_id:
self.report({"ERROR"}, f"No active object selected")
return {"CANCELLED"}
for obj in selected_objs:
tool.Geometry.clear_scale(obj)
if not selected_objs:
self.report({"ERROR"}, f"No IFC objects selected")
return {"CANCELLED"}
if len(selected_objs) == 1:
joiner.join_E(context.active_object, context.scene.cursor.location)
return {"FINISHED"}
if len(selected_objs) == 2:
if self.join_type in ("L", "V"):
if len(selected_objs) != 2:
self.report({"ERROR"}, f"It requires 2 selected objects to do join of type {self.join_type}")
return {"CANCELLED"}
another_selected_object = next(o for o in selected_objs if o != context.active_object)
if self.join_type == "L":
joiner.join_L([o for o in selected_objs if o != context.active_object][0], context.active_object)
joiner.join_L(another_selected_object, context.active_object)
elif self.join_type == "V":
joiner.join_V([o for o in selected_objs if o != context.active_object][0], context.active_object)
if len(selected_objs) < 2:
joiner.join_V(another_selected_object, context.active_object)
return {"FINISHED"}
if self.join_type == "T":
elements = [tool.Ifc.get_entity(o) for o in context.selected_objects]
layer2_elements = []
@@ -522,6 +535,7 @@ class DumbWallGenerator:
return (point1 - point2).length < 0.1
def derive_from_cursor(self):
RAYCAST_PRECISION = 0.01
self.location = bpy.context.scene.cursor.location
if self.collection:
for sibling_obj in self.collection.objects:
@@ -529,24 +543,41 @@ class DumbWallGenerator:
continue
if "IfcWall" not in sibling_obj.name:
continue
local_location = sibling_obj.matrix_world.inverted() @ self.location
inv_obj_matrix = sibling_obj.matrix_world.inverted()
local_location = inv_obj_matrix @ self.location
try:
raycast = sibling_obj.closest_point_on_mesh(local_location, distance=0.01)
raycast = sibling_obj.closest_point_on_mesh(local_location, distance=RAYCAST_PRECISION)
except:
# If the mesh has no faces
raycast = [None]
if not raycast[0]:
continue
for face in sibling_obj.data.polygons:
if (
abs(face.normal.y) >= 0.75
and abs(mathutils.geometry.distance_point_to_plane(local_location, face.center, face.normal))
< 0.01
):
# Rotate the wall in the direction of the face normal
normal = (sibling_obj.matrix_world.to_quaternion() @ face.normal).normalized()
self.rotation = math.atan2(normal[1], normal[0])
break
normal = (sibling_obj.matrix_world.to_quaternion() @ face.normal).normalized()
face_center = sibling_obj.matrix_world @ face.center
if normal.z != 0 or abs(mathutils.geometry.distance_point_to_plane(self.location, face_center, normal)) > 0.01:
continue
rotation = math.atan2(normal[1], normal[0])
rotated_y_axis = Matrix.Rotation(-rotation, 4, "Z")[1].xyz
# since wall thickness goes by local Y+ axis
# we find best position for the next wall
# by finding the face of another wall that will be very close to the some test point.
# test point is calculated by applying to cursor position some little offset along the face
#
# a bit different offset to be safe on raycast
test_pos = self.location + rotated_y_axis * RAYCAST_PRECISION * 1.1
test_pos_local = inv_obj_matrix @ test_pos
raycast = sibling_obj.closest_point_on_mesh(test_pos_local, distance=RAYCAST_PRECISION)
if not raycast[0]:
continue
self.rotation = rotation
break
if self.rotation != 0:
break
return self.create_wall()
def create_wall(self):
@@ -1106,7 +1137,7 @@ class DumbWallJoiner:
previous_matrix = obj.matrix_world.copy()
previous_origin = previous_matrix.col[3].to_2d()
obj.matrix_world[0][3], obj.matrix_world[1][3] = self.body[0]
obj.matrix_world.col[3].xy = self.body[0]
bpy.context.view_layer.update()
for rel in element.ConnectedFrom:
@@ -1401,6 +1432,7 @@ class DumbWallJoiner:
return True
def clip(self, wall1, slab2):
"""returns height of the clipped wall, adds clipping plane to `clippings`"""
element1 = tool.Ifc.get_entity(wall1)
element2 = tool.Ifc.get_entity(slab2)
@@ -1410,19 +1442,15 @@ class DumbWallJoiner:
bases = [axis1["base"][0].to_3d(), axis1["base"][1].to_3d(), axis1["side"][0].to_3d(), axis1["side"][1].to_3d()]
extrusion = self.get_extrusion_data(tool.Ifc.get().by_id(wall1.data.BIMMeshProperties.ifc_definition_id))
d = wall1.matrix_world.to_quaternion() @ extrusion["direction"]
wall_dir = wall1.matrix_world.to_quaternion() @ extrusion["direction"]
slab_pt = slab2.matrix_world @ Vector((0, 0, 0))
slab_dir = slab2.matrix_world.to_quaternion() @ Vector((0, 0, -1))
tops = [mathutils.geometry.intersect_line_plane(b, b + d, slab_pt, slab_dir) for b in bases]
i_bottom = None
i_top = None
for i, co in enumerate(tops):
if i_top is None or co[2] > i_top[2]:
i_top = co
i_bottom = bases[i]
tops = [mathutils.geometry.intersect_line_plane(b, b + wall_dir, slab_pt, slab_dir) for b in bases]
top_index = max(range(4), key=lambda i: tops[i].z)
i_top = tops[top_index]
i_bottom = bases[top_index]
quaternion = slab2.matrix_world.to_quaternion()
x_axis = quaternion @ Vector((1, 0, 0))
@@ -167,7 +167,8 @@ def update_window_modifier_representation(context):
)
# type attributes
element.PartitioningType = props.window_type
if tool.Ifc.get_schema() != "IFC2X3":
element.PartitioningType = props.window_type
# occurences attributes
occurences = tool.Ifc.get_all_element_occurences(element)
@@ -262,14 +263,19 @@ def create_bm_window(
frame_thickness,
glass_thickness,
position: Vector,
x_offsets: list = None,
):
"""`lining_thickness` expected to be defined as a list,
"""`lining_thickness` and `x_offsets` are expected to be defined as a list,
similarly to `create_bm_window_frame` `thickness` argument"""
if x_offsets is None:
x_offsets = [lining_to_panel_offset_x] * 4
# window lining
window_lining_verts = create_bm_window_frame(bm, lining_size, lining_thickness)
# window frame
frame_position = V(lining_to_panel_offset_x, lining_to_panel_offset_y_full, lining_to_panel_offset_x)
frame_position = V(x_offsets[0], lining_to_panel_offset_y_full, x_offsets[3])
frame_verts = create_bm_window_frame(bm, frame_size, frame_thickness, frame_position)
# window glass
@@ -319,21 +325,35 @@ def update_window_modifier_bmesh(context):
unique_cols = len(set(panel_row))
for column_i, panel_i in enumerate(panel_row):
# detect mullion
has_mullion = unique_cols > 1
first_column = column_i == 0
last_column = column_i == unique_cols - 1
left_to_mullion = has_mullion and not last_column
right_to_mullion = has_mullion and not first_column
# detect transom
has_transom = unique_rows_in_col[column_i] > 1
first_row = row_i == 0
last_row = row_i == unique_rows_in_col[column_i] - 1
top_to_transom = has_transom and not first_row
bottom_to_transom = has_transom and not last_row
# calculate current panel dimensions
if unique_cols > 1:
if column_i == 0:
if has_mullion:
if first_column:
panel_width = first_mullion_offset
elif column_i == unique_cols - 1:
elif last_column:
panel_width = overall_width - accumulated_width
else:
panel_width = second_mullion_offset - accumulated_width
else:
panel_width = overall_width
if unique_rows_in_col[column_i] > 1:
if row_i == 0:
if has_transom:
if first_row:
panel_height = first_transom_offset
elif row_i == unique_rows_in_col[column_i] - 1:
elif last_row:
panel_height = overall_height - accumulated_height[column_i]
else:
panel_height = second_transom_offset - accumulated_height[column_i]
@@ -347,7 +367,7 @@ def update_window_modifier_bmesh(context):
frame_depth = props.frame_depth[panel_i]
frame_thickness = props.frame_thickness[panel_i]
lining_to_panel_offset_y_full = (lining_depth - frame_depth) + lining_to_panel_offset_y
# add window
window_lining_size = V(
panel_width,
@@ -355,25 +375,33 @@ def update_window_modifier_bmesh(context):
panel_height,
)
# calculate lining thickness
# calculate lining thickness and frame size / offset
# taking into account mullions and transoms
window_lining_thickness = [lining_thickness] * 4
# mullion thickness
if unique_cols > 1:
if column_i != 0:
window_lining_thickness[0] = mullion_thickness # left column
if column_i != unique_cols - 1:
window_lining_thickness[2] = mullion_thickness # right column
# transom thickness
if unique_rows_in_col[column_i] > 1:
if row_i != 0:
window_lining_thickness[3] = transom_thickness # bottom row
if row_i != unique_rows_in_col[column_i] - 1:
window_lining_thickness[1] = transom_thickness # top row
# fmt: off
window_lining_thickness = [
mullion_thickness if right_to_mullion else lining_thickness,
transom_thickness if bottom_to_transom else lining_thickness,
mullion_thickness if left_to_mullion else lining_thickness,
transom_thickness if top_to_transom else lining_thickness,
]
# x offsets can differ if there are mullions or transoms because we're trying to maintain symmetry
base_frame_clear = lining_to_panel_offset_x + frame_thickness - lining_thickness
current_offset_x = base_frame_clear - frame_thickness + mullion_thickness
current_offset_z = base_frame_clear - frame_thickness + transom_thickness
# fmt: off
x_offsets = [
current_offset_x if right_to_mullion else lining_to_panel_offset_x, # LEFT
current_offset_z if bottom_to_transom else lining_to_panel_offset_x, # TOP
current_offset_x if left_to_mullion else lining_to_panel_offset_x, # RIGHT
current_offset_z if top_to_transom else lining_to_panel_offset_x, # BOTTOM
]
# fmt: on
frame_size = window_lining_size.copy()
frame_size.y = frame_depth
frame_size = frame_size - V(lining_to_panel_offset_x * 2, 0, lining_to_panel_offset_x * 2)
frame_size.x -= x_offsets[0] + x_offsets[2]
frame_size.z -= x_offsets[1] + x_offsets[3]
window_position = V(accumulated_width, 0, accumulated_height[column_i])
lining_verts, panel_verts, glass_verts = create_bm_window(
@@ -381,11 +409,12 @@ def update_window_modifier_bmesh(context):
window_lining_size,
window_lining_thickness,
lining_to_panel_offset_x,
(lining_depth - frame_depth) + lining_to_panel_offset_y,
lining_to_panel_offset_y_full,
frame_size,
frame_thickness,
glass_thickness,
window_position,
x_offsets,
)
built_panels.append(panel_i)
@@ -424,10 +453,15 @@ class BIM_OT_add_window(bpy.types.Operator, tool.Ifc.Operator):
mesh = bpy.data.meshes.new("IfcWindow")
obj = bpy.data.objects.new("IfcWindow", mesh)
obj.location = spawn_location
collection = context.view_layer.active_layer_collection.collection
collection.objects.link(obj)
element = blenderbim.core.root.assign_class(
tool.Ifc, tool.Collector, tool.Root, obj=obj, ifc_class="IfcWindow", should_add_representation=False
)
element.PredefinedType = "WINDOW"
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
if tool.Ifc.get_schema() != "IFC2X3":
element.PredefinedType = "WINDOW"
bpy.ops.object.select_all(action="DESELECT")
bpy.context.view_layer.objects.active = None
@@ -448,8 +482,8 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
props = obj.BIMWindowProperties
if element.is_a() not in ("IfcWindow", "IfcWindowType"):
self.report({"ERROR"}, "Object has to be IfcWindow/IfcWindowType type to add a window.")
if element.is_a() not in ("IfcWindow", "IfcWindowType", "IfcWindowStyle"):
self.report({"ERROR"}, "Object has to be IfcWindow/IfcWindowType/IfcWindowStyle type to add a window.")
return {"CANCELLED"}
window_data = props.get_general_kwargs(convert_to_project_units=True)
@@ -497,7 +531,7 @@ class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
should_sync_changes_first=False,
)
props.is_editing = -1
props.is_editing = False
return {"FINISHED"}
@@ -518,7 +552,7 @@ class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
window_data["lining_properties"] = lining_props
window_data["panel_properties"] = panel_props
props.is_editing = -1
props.is_editing = False
update_window_modifier_representation(context)
@@ -544,7 +578,7 @@ class EnableEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
# required since we could load pset from .ifc and BIMWindowProperties won't be set
props.set_props_kwargs_from_ifc_data(data)
props.is_editing = 1
props.is_editing = True
return {"FINISHED"}
@@ -557,7 +591,7 @@ class RemoveWindow(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object
props = obj.BIMWindowProperties
element = tool.Ifc.get_entity(obj)
obj.BIMWindowProperties.is_editing = -1
obj.BIMWindowProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Window")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
@@ -176,7 +176,11 @@ class BimToolUI:
row.operator("bim.join_wall", icon="X", text="").join_type = ""
elif AuthoringData.data["active_material_usage"] == "LAYER3":
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
if len(context.selected_objects) == 1:
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
elif "LAYER2" in AuthoringData.data["selected_material_usages"]:
add_layout_hotkey_operator(cls.layout, "Extend Wall To Slab", "S_E", "")
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="x_angle", text="X Angle")
@@ -207,12 +211,14 @@ class BimToolUI:
elif (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and RailingData.data["pset_data"]
and not context.active_object.BIMRailingProperties.is_editing_path
):
# NOTE: should be above "active_representation_type" = "SweptSolid" check
# because it could be a SweptSolid too
add_layout_hotkey_operator(cls.layout, "Edit Railing Path", "S_E", "")
row = cls.layout.row(align=True)
row.label(text="", icon=f"EVENT_TAB")
row.operator("bim.enable_editing_railing_path", text="Edit Railing Path")
elif AuthoringData.data["active_representation_type"] == "SweptSolid":
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
@@ -246,13 +252,12 @@ class BimToolUI:
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["parameters"]
and RoofData.data["pset_data"]
and not context.active_object.BIMRoofProperties.is_editing_path
):
add_layout_hotkey_operator(cls.layout, "Edit Roof Path", "S_E", "")
elif DecoratorData.get_ifc_text_data(bpy.context.object):
add_layout_hotkey_operator(cls.layout, "Edit Text", "S_E", "")
row = cls.layout.row(align=True)
row.label(text="", icon=f"EVENT_TAB")
row.operator("bim.enable_editing_roof_path", text="Edit Roof Path")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
@@ -423,12 +428,21 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
# and it might conflict with one of the conditions below
if (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and RailingData.data["pset_data"]
and not bpy.context.active_object.BIMRailingProperties.is_editing_path
):
bpy.ops.bim.enable_editing_railing_path()
return
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
and not bpy.context.active_object.BIMRoofProperties.is_editing_path
):
# undo the unselection done above because roof has no usage type
bpy.ops.bim.enable_editing_roof_path()
return
selected_usages = {}
for obj in bpy.context.selected_objects:
element = tool.Ifc.get_entity(obj)
@@ -447,7 +461,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if len(bpy.context.selected_objects) == 1:
if self.active_material_usage == "LAYER3":
# Edit LAYER2 profile
# Edit LAYER3 profile
if bpy.context.active_object and bpy.context.active_object.mode == "OBJECT":
bpy.ops.bim.enable_editing_extrusion_profile()
elif self.active_material_usage == "LAYER2":
@@ -459,6 +473,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
else:
# Edit SWEPTSOLID profile (assuming single profile for now)
bpy.ops.bim.enable_editing_extrusion_profile()
elif self.active_material_usage == "LAYER2" and selected_usages.get("PROFILE", []):
# Extend PROFILEs to LAYER2
[o.select_set(False) for o in selected_usages.get("LAYER3", [])]
@@ -483,19 +498,6 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
[o.select_set(False) for o in selected_usages.get("LAYER2", [])]
bpy.ops.bim.extend_profile(join_type="T")
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["parameters"]
and not bpy.context.active_object.BIMRoofProperties.is_editing_path
):
# undo the unselection done above because roof has no usage type
bpy.context.object.select_set(True)
bpy.ops.bim.enable_editing_roof_path()
elif DecoratorData.get_ifc_text_data(bpy.context.object):
bpy.context.object.select_set(True)
bpy.ops.bim.edit_text_popup()
def hotkey_S_F(self):
if not bpy.context.selected_objects:
return
@@ -131,14 +131,15 @@ class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
for obj in context.selected_objects:
obj.select_set(False)
props = context.scene.BIMProfileProperties
profile = tool.Ifc.get().by_id(props.active_profile_id)
active_profile = props.profiles[props.active_profile_index]
profile_id = active_profile.ifc_definition_id
props.active_arbitrary_profile_id = profile_id
profile = tool.Ifc.get().by_id(profile_id)
obj = tool.Model.import_profile(profile)
tool.Ifc.link(profile, obj)
bpy.context.scene.collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj
tool.Blender.select_and_activate_single_object(context, obj)
bpy.ops.object.mode_set(mode="EDIT")
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_arbitrary_profile(context))
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
@@ -153,6 +154,8 @@ def disable_editing_arbitrary_profile(context):
bpy.data.objects.remove(obj)
bpy.data.meshes.remove(profile_mesh)
props = context.scene.BIMProfileProperties
props.active_arbitrary_profile_id = 0
# need to update profile manager ui
# if this was called from decorator
refresh()
@@ -174,7 +177,7 @@ class EditArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMProfileProperties
old_profile = tool.Ifc.get().by_id(props.active_profile_id)
old_profile = tool.Ifc.get().by_id(props.active_arbitrary_profile_id)
obj = context.active_object
@@ -194,6 +197,7 @@ class EditArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.object.mode_set(mode="EDIT")
return
prev_profile_id = profile.id()
profile_mesh = obj.data
bpy.data.objects.remove(obj)
bpy.data.meshes.remove(profile_mesh)
@@ -204,6 +208,8 @@ class EditArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile)
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_profile)
bpy.ops.bim.load_profiles()
props.active_profile_id = profile.id()
if props.active_profile_id == prev_profile_id:
props.active_profile_id = profile.id()
props.active_arbitrary_profile_id = 0
model_profile.DumbProfileRegenerator().regenerate_from_profile_def(profile)
@@ -54,6 +54,7 @@ class BIMProfileProperties(PropertyGroup):
profiles: CollectionProperty(name="Profiles", type=Profile)
active_profile_index: IntProperty(name="Active Profile Index")
active_profile_id: IntProperty(name="Active Profile Id")
active_arbitrary_profile_id: IntProperty(name="Active Arbitrary Profile Id")
profile_attributes: CollectionProperty(name="Profile Attributes", type=Attribute)
profile_classes: EnumProperty(items=get_profile_classes, name="Profile Classes")
@@ -42,10 +42,12 @@ class BIM_PT_profiles(Panel):
ProfileData.load()
self.props = context.scene.BIMProfileProperties
active_profile = None
if self.props.is_editing and self.props.profiles and self.props.active_profile_index < len(self.props.profiles):
preview_collection = ProfileData.preview_collection
box = self.layout.box()
profile_id = self.props.profiles[self.props.active_profile_index].ifc_definition_id
active_profile = self.props.profiles[self.props.active_profile_index]
profile_id = active_profile.ifc_definition_id
profile_id_str = str(profile_id)
if profile_id_str in preview_collection:
preview_image = preview_collection[profile_id_str]
@@ -77,19 +79,19 @@ class BIM_PT_profiles(Panel):
self.props,
"active_profile_index",
)
if active_profile and active_profile.ifc_class == "IfcArbitraryClosedProfileDef":
if self.props.active_arbitrary_profile_id:
row = self.layout.row(align=True)
row.operator("bim.edit_arbitrary_profile", text="Save Arbitrary Profile", icon="CHECKMARK")
row.operator("bim.disable_editing_arbitrary_profile", text="", icon="CANCEL")
else:
row = self.layout.row()
row.operator("bim.enable_editing_arbitrary_profile", text="Edit Arbitrary Profile", icon="GREASEPENCIL")
if self.props.active_profile_id:
self.draw_editable_ui(context)
def draw_editable_ui(self, context):
if ProfileData.data["is_arbitrary_profile"]:
if ProfileData.data["is_editing_arbitrary_profile"]:
row = self.layout.row(align=True)
row.operator("bim.edit_arbitrary_profile", text="Save Profile", icon="CHECKMARK")
row.operator("bim.disable_editing_arbitrary_profile", text="", icon="CANCEL")
else:
row = self.layout.row()
row.operator("bim.enable_editing_arbitrary_profile", text="Edit Profile", icon="GREASEPENCIL")
blenderbim.bim.helper.draw_attributes(self.props.profile_attributes, self.layout)
@@ -20,6 +20,7 @@ import bpy
from . import ui, prop, operator
classes = (
operator.AppendEntireLibrary,
operator.AppendLibraryElement,
operator.AssignLibraryDeclaration,
operator.ChangeLibraryElement,
@@ -33,10 +34,10 @@ classes = (
operator.LoadLink,
operator.LoadProject,
operator.LoadProjectElements,
operator.NewProject,
operator.RefreshLibrary,
operator.RewindLibrary,
operator.SaveLibraryFile,
operator.AppendEntireLibrary,
operator.SelectLibraryFile,
operator.ToggleFilterCategories,
operator.ToggleLinkVisibility,
@@ -48,6 +49,8 @@ classes = (
prop.FilterCategory,
prop.Link,
prop.BIMProjectProperties,
ui.BIM_MT_new_project,
ui.BIM_MT_project,
ui.BIM_PT_project,
ui.BIM_PT_project_library,
ui.BIM_PT_links,
@@ -57,22 +60,34 @@ classes = (
)
def menu_func_export(self, context):
op = self.layout.operator(operator.ExportIFC.bl_idname, text="Industry Foundation Classes (.ifc/.ifczip/.ifcjson)")
op.should_save_as = True
def menu_func_import(self, context):
self.layout.operator(operator.ImportIFC.bl_idname, text="Industry Foundation Classes (.ifc/.ifczip/.ifcxml)")
addon_keymaps = []
def register():
bpy.types.Scene.BIMProjectProperties = bpy.props.PointerProperty(type=prop.BIMProjectProperties)
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
bpy.types.TOPBAR_MT_file.prepend(ui.file_menu)
wm = bpy.context.window_manager
if wm.keyconfigs.addon:
km = wm.keyconfigs.addon.keymaps.get("Window")
if not km:
km = wm.keyconfigs.addon.keymaps.new("Window")
kmi = km.keymap_items.new("wm.call_menu", "N", "PRESS", ctrl=True)
kmi.properties.name = "BIM_MT_new_project"
addon_keymaps.append((km, kmi))
km = wm.keyconfigs.addon.keymaps.new(name="Window", space_type="EMPTY")
kmi = km.keymap_items.new("export_ifc.bim", "S", "PRESS", ctrl=True)
kmi.properties.should_save_as = False
addon_keymaps.append((km, kmi))
def unregister():
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
bpy.types.TOPBAR_MT_file.remove(ui.file_menu)
del bpy.types.Scene.BIMProjectProperties
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
if kc:
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
@@ -36,6 +36,51 @@ from blenderbim.bim.ui import IFCFileSelector
from blenderbim.bim import import_ifc
from blenderbim.bim import export_ifc
from pathlib import Path
from bpy.app.handlers import persistent
class NewProject(bpy.types.Operator):
bl_idname = "bim.new_project"
bl_label = "New Project"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Start a new IFC project in a fresh session"
preset: bpy.props.StringProperty()
def execute(self, context):
bpy.ops.wm.read_homefile()
if self.preset == "metric_m":
bpy.context.scene.BIMProjectProperties.export_schema = "IFC4"
bpy.context.scene.unit_settings.system = "METRIC"
bpy.context.scene.unit_settings.length_unit = "METERS"
bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE"
bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE"
bpy.context.scene.BIMProjectProperties.template_file = "0"
elif self.preset == "metric_mm":
bpy.context.scene.BIMProjectProperties.export_schema = "IFC4"
bpy.context.scene.unit_settings.system = "METRIC"
bpy.context.scene.unit_settings.length_unit = "MILLIMETERS"
bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE"
bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE"
bpy.context.scene.BIMProjectProperties.template_file = "0"
elif self.preset == "imperial_ft":
bpy.context.scene.BIMProjectProperties.export_schema = "IFC4"
bpy.context.scene.unit_settings.system = "IMPERIAL"
bpy.context.scene.unit_settings.length_unit = "FEET"
bpy.context.scene.BIMProperties.area_unit = "square foot"
bpy.context.scene.BIMProperties.volume_unit = "cubic foot"
bpy.context.scene.BIMProjectProperties.template_file = "0"
elif self.preset == "demo":
bpy.context.scene.BIMProjectProperties.export_schema = "IFC4"
bpy.context.scene.unit_settings.system = "METRIC"
bpy.context.scene.unit_settings.length_unit = "MILLIMETERS"
bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE"
bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE"
bpy.context.scene.BIMProjectProperties.template_file = "IFC4 Demo Library.ifc"
if self.preset != "wizard":
bpy.ops.bim.create_project()
return {"FINISHED"}
class CreateProject(bpy.types.Operator):
@@ -57,6 +102,9 @@ class CreateProject(bpy.types.Operator):
props = context.scene.BIMProjectProperties
template = None if props.template_file == "0" else props.template_file
blenderbim.bim.schema.reload(props.export_schema)
if tool.Blender.is_default_scene():
for obj in bpy.data.objects:
bpy.data.objects.remove(obj)
core.create_project(tool.Ifc, tool.Project, schema=props.export_schema, template=template)
def rollback(self, data):
@@ -525,13 +573,37 @@ class LoadProject(bpy.types.Operator, IFCFileSelector):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Load an existing IFC project"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcsqlite", options={"HIDDEN"})
is_advanced: bpy.props.BoolProperty(name="Enable Advanced Mode", default=False)
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
should_start_fresh_session: bpy.props.BoolProperty(name="Should Start Fresh Session", default=True)
def execute(self, context):
@persistent
def load_handler(*args):
bpy.app.handlers.load_post.remove(load_handler)
self.finish_loading_project(context)
if self.should_start_fresh_session:
# WARNING: wm.read_homefile clears context which could lead to some
# operators to fail:
# https://blender.stackexchange.com/a/282558/135166
# So we continue using the load_post handler thats triggered when
# context is already restored
bpy.app.handlers.load_post.append(load_handler)
bpy.ops.wm.read_homefile()
return {"FINISHED"}
else:
return self.finish_loading_project(context)
def finish_loading_project(self, context):
if not self.is_existing_ifc_file():
return {"FINISHED"}
if tool.Blender.is_default_scene():
for obj in bpy.data.objects:
bpy.data.objects.remove(obj)
context.scene.BIMProperties.ifc_file = self.get_filepath()
context.scene.BIMProjectProperties.is_loading = True
context.scene.BIMProjectProperties.total_elements = len(tool.Ifc.get().by_type("IfcElement"))
@@ -687,6 +759,9 @@ class LinkIfc(bpy.types.Operator):
files = [f.name for f in self.files] if self.files else [self.filepath]
for filename in files:
filepath = os.path.join(self.directory, filename)
if bpy.data.filepath and Path(filepath).samefile(bpy.data.filepath):
self.report({"INFO"}, "Can't link the current .blend file")
continue
new = context.scene.BIMProjectProperties.links.add()
if self.use_relative_path:
filepath = os.path.relpath(filepath, bpy.path.abspath("//"))
@@ -814,7 +889,7 @@ class ToggleLinkVisibility(bpy.types.Operator):
class ExportIFC(bpy.types.Operator):
bl_idname = "export_ifc.bim"
bl_label = "Export IFC"
bl_label = "Save IFC"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".ifc"
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"})
@@ -825,21 +900,23 @@ class ExportIFC(bpy.types.Operator):
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
save_as_invoked: bpy.props.BoolProperty(name="Save As Dialog Was Invoked", default=False, options={"HIDDEN"})
@classmethod
def poll(cls, context):
return tool.Ifc.get()
def draw(self, context):
layout = self.layout
layout.prop(self, "json_version")
layout.prop(self, "json_compact")
if bpy.data.is_saved:
layout.prop(self, "use_relative_path")
else:
layout.label(text="Save the .blend file first ")
layout.label(text="to use relative paths for .ifc.")
def invoke(self, context, event):
self.save_as_invoked = False
if not IfcStore.get_file():
self.report({"ERROR"}, "No IFC project is available for export - create or import a project first.")
if not tool.Ifc.get():
bpy.ops.wm.save_mainfile("INVOKE_DEFAULT")
return {"FINISHED"}
self.save_as_invoked = False
if context.scene.BIMProperties.ifc_file and not self.should_save_as:
self.filepath = context.scene.BIMProperties.ifc_file
if not os.path.isabs(self.filepath):
@@ -910,7 +987,10 @@ class ExportIFC(bpy.types.Operator):
if bpy.data.is_saved and bpy.data.is_dirty and bpy.data.filepath:
bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath)
blenderbim.bim.handler.purge_module_data()
return {"FINISHED"}
self.report({"INFO"}, f'IFC Project "{os.path.basename(output_file)}" Saved')
if bpy.data.is_saved:
bpy.ops.wm.save_mainfile("INVOKE_DEFAULT")
@classmethod
def description(cls, context, properties):
@@ -148,6 +148,8 @@ class BIMProjectProperties(PropertyGroup):
default="NONE",
)
should_merge_materials_by_colour: BoolProperty(name="Merge Materials by Colour", default=False)
should_stream: BoolProperty(name="Stream Data From IFC-SPF (Only for advanced users)", default=False)
should_load_geometry: BoolProperty(name="Load Geometry", default=True)
should_use_native_meshes: BoolProperty(name="Native Meshes", default=False)
should_clean_mesh: BoolProperty(name="Clean Meshes", default=True)
should_cache: BoolProperty(name="Cache", default=False)
@@ -17,12 +17,64 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
import blenderbim.bim
from blenderbim.bim.helper import prop_with_search
from bpy.types import Panel, UIList
from bpy.types import Panel, Menu, UIList
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.project.data import ProjectData
class BIM_MT_project(Menu):
bl_idname = "BIM_MT_project"
bl_label = "New IFC Project"
def draw(self, context):
self.layout.operator("bim.new_project", text="New Metric (m) Project").preset = "metric_m"
self.layout.operator("bim.new_project", text="New Metric (mm) Project").preset = "metric_mm"
self.layout.operator("bim.new_project", text="New Imperial (ft) Project").preset = "imperial_ft"
self.layout.operator("bim.new_project", text="New Demo Project").preset = "demo"
self.layout.operator("bim.new_project", text="New Project Wizard").preset = "wizard"
class BIM_MT_new_project(Menu):
bl_idname = "BIM_MT_new_project"
bl_label = "New Project"
def draw(self, context):
self.layout.operator_context = "INVOKE_DEFAULT"
op = self.layout.operator("bim.load_project", text="Open IFC Project", icon="FILEBROWSER")
op.should_start_fresh_session = True
# Do we need to set it back to exec default?
# self.layout.operator_context = "EXEC_DEFAULT"
self.layout.separator()
self.layout.label(text="New IFC Project", icon_value=blenderbim.bim.icons["IFC"].icon_id)
self.layout.operator("bim.new_project", text="Metric (m) Project").preset = "metric_m"
self.layout.operator("bim.new_project", text="Metric (mm) Project").preset = "metric_mm"
self.layout.operator("bim.new_project", text="Imperial (ft) Project").preset = "imperial_ft"
self.layout.operator("bim.new_project", text="Demo Project").preset = "demo"
self.layout.operator("bim.new_project", text="Project Wizard").preset = "wizard"
self.layout.separator()
self.layout.label(text="New Blender Project", icon="BLENDER")
# In theory we can dynamically grab the list from list(bpy.utils.app_template_paths())
self.layout.operator("wm.read_homefile", text="General Project").app_template = ""
self.layout.operator("wm.read_homefile", text="2D Animation Project").app_template = "2D_Animation"
self.layout.operator("wm.read_homefile", text="Sculpting Project").app_template = "Sculpting"
self.layout.operator("wm.read_homefile", text="VFX Project").app_template = "VFX"
self.layout.operator("wm.read_homefile", text="Video Editing Project").app_template = "Video_Editing"
def file_menu(self, context):
self.layout.menu("BIM_MT_project", icon="COLLECTION_NEW")
op = self.layout.operator("bim.load_project", text="Open IFC Project", icon="FILEBROWSER")
op.should_start_fresh_session = True
self.layout.separator()
op = self.layout.operator("export_ifc.bim", icon="FILE_TICK", text="Save IFC Project")
op.should_save_as = False
op = self.layout.operator("export_ifc.bim", text="Save IFC Project As...")
op.should_save_as = True
self.layout.separator()
class BIM_PT_project(Panel):
bl_label = "IFC Project"
bl_idname = "BIM_PT_project"
@@ -77,6 +129,8 @@ class BIM_PT_project(Panel):
row = self.layout.row()
row.prop(pprops, "should_cache")
row = self.layout.row()
row.prop(pprops, "should_load_geometry")
row = self.layout.row()
row.prop(pprops, "should_use_native_meshes")
row = self.layout.row()
row.prop(pprops, "should_merge_materials_by_colour")
@@ -156,13 +210,10 @@ class BIM_PT_project(Panel):
row.prop(props, "ifc_file", text="")
row.operator("bim.reload_ifc_file", icon="FILE_REFRESH", text="")
row.operator("bim.select_ifc_file", icon="FILE_FOLDER", text="")
row = self.layout.row(align=True)
op = row.operator("export_ifc.bim", icon="EXPORT", text="Save Project")
op.should_save_as = False
op = row.operator("export_ifc.bim", icon="FILE_TICK", text="Save As")
op.should_save_as = True
row.operator("bim.unload_project", text="", icon="X")
row.operator("bim.unload_project", text="", icon="CANCEL")
else:
row = self.layout.row()
row.label(text="File Not Saved", icon="ERROR")
def draw_create_project_ui(self, context):
props = context.scene.BIMProperties
@@ -180,7 +231,7 @@ class BIM_PT_project(Panel):
row = self.layout.row(align=True)
row.operator("bim.create_project")
row.operator("bim.load_project")
row.operator("bim.load_project").should_start_fresh_session = False
class BIM_PT_project_library(Panel):
@@ -30,6 +30,7 @@ classes = (
operator.CalculateQuantity,
operator.RemovePset,
operator.TogglePsetExpansion,
operator.SelectSimilarPsetValue,
operator.BIM_OT_add_property_to_edit,
operator.BIM_OT_remove_property_to_edit,
operator.BIM_OT_clear_list,
@@ -320,6 +320,52 @@ class EditPset(bpy.types.Operator, Operator):
tool.Blender.update_viewport()
class SelectSimilarPsetValue(bpy.types.Operator):
"""
Selects objects with the same property value.
"""
bl_idname = "bim.select_similar_pset_value"
bl_label = "Select Similar Pset Value"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Selects objects with the same property value.\n\n" + "ALT+CLICK to select from all pset common"
pset_name: bpy.props.StringProperty()
prop_name: bpy.props.StringProperty()
value: bpy.props.StringProperty()
all_pset_common: bpy.props.BoolProperty(name = "All Pset Common", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
if event.type == "LEFTMOUSE" and event.alt:
self.all_pset_common = True
return self.execute(context)
def execute(self, context):
for element in tool.Ifc.get().by_type("IfcElement"):
try:
obj = tool.Ifc.get_object(element)
psets = ifcopenshell.util.element.get_psets(element)
except:
continue
if self.all_pset_common & self.pset_name.endswith("Common"):
for key in psets.keys():
if key.endswith("Common"):
try:
pset = psets[key]
if str(pset[self.prop_name]) == self.value:
obj.select_set(True)
except:
continue
else:
try:
pset = psets[self.pset_name]
if str(pset[self.prop_name]) == self.value:
obj.select_set(True)
except:
continue
return {"FINISHED"}
class RemovePset(bpy.types.Operator, Operator):
bl_idname = "bim.remove_pset"
bl_label = "Remove Pset"
@@ -135,6 +135,10 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type):
row.scale_y = 0.8
row.label(text=prop["Name"])
row.label(text=str(prop["NominalValue"]))
op = row.operator("bim.select_similar_pset_value", text="", icon="RESTRICT_SELECT_OFF")
op.pset_name = pset["Name"]
op.prop_name = prop["Name"]
op.value = str(prop["NominalValue"])
if not has_props_displayed:
row = box.row()
row.scale_y = 0.8
@@ -159,7 +159,7 @@ class IfcClassData:
@classmethod
def name(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
element = tool.Ifc.get_entity(bpy.context.view_layer.objects.active)
if not element:
return
name = element.is_a()
@@ -170,13 +170,13 @@ class IfcClassData:
@classmethod
def ifc_class(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
element = tool.Ifc.get_entity(bpy.context.view_layer.objects.active)
if element:
return element.is_a()
@classmethod
def can_reassign_class(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
element = tool.Ifc.get_entity(bpy.context.view_layer.objects.active)
if element:
if element.is_a("IfcOpeningElement") or element.is_a("IfcOpeningStandardCase"):
return False
@@ -168,6 +168,7 @@ class UnlinkObject(bpy.types.Operator):
else:
objects = context.selected_objects
for obj in objects:
was_active_object = obj == context.active_object
object_name = obj.name
element = tool.Ifc.get_entity(obj)
if element:
@@ -189,6 +190,8 @@ class UnlinkObject(bpy.types.Operator):
blenderbim.core.material.unlink_material(tool.Ifc, obj=material_slot.material)
if "Ifc" in object_name and "/" in object_name:
obj.name = object_name.split("/", 1)[1]
if was_active_object:
tool.Blender.set_active_object(obj)
return {"FINISHED"}
def draw(self, context):
@@ -79,12 +79,23 @@ class SelectGlobalId(Operator):
def execute(self, context):
ifc_file = tool.Ifc.get()
props = context.scene.BIMSearchProperties
global_id = self.global_id or props.global_id
entity = ifc_file.by_guid(global_id)
global_id = self.global_id.strip()
if not global_id:
self.report({"ERROR"}, "Set Global ID for search.")
return {"CANCELLED"}
try:
entity = ifc_file.by_guid(global_id)
except RuntimeError:
self.report({"ERROR"}, f"No IFC entity found with guid '{global_id}'.")
return {"CANCELLED"}
obj = tool.Ifc.get_object(entity)
if not obj:
self.report({"ERROR"}, "No object found")
self.report({"ERROR"}, f"No Blender object found with guid '{global_id}'.")
return {"CANCELLED"}
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
return {"FINISHED"}
@@ -99,13 +110,29 @@ class SelectIfcClass(Operator):
ifc_class: StringProperty()
def execute(self, context):
ifc_class = self.ifc_class
if not ifc_class:
self.report({"ERROR"}, "Set IFC Class for search.")
return {"CANCELLED"}
sch = tool.Ifc.schema()
try:
sch.declaration_by_name(ifc_class)
except RuntimeError:
self.report({"ERROR"}, f"IFC Class '{ifc_class}' is not found in schema {sch.name()}.")
return {"CANCELLED"}
self.file = IfcStore.get_file()
n_objects = 0
for obj in context.visible_objects:
if not obj.BIMObjectProperties.ifc_definition_id or obj.is_library_indirect:
continue
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
if does_keyword_exist(self.ifc_class, element.is_a(), context):
if does_keyword_exist(ifc_class, element.is_a(), context):
obj.select_set(True)
n_objects += 1
self.report({"INFO"}, f"{n_objects} objects selected.")
return {"FINISHED"}
@@ -116,11 +143,19 @@ class SelectAttribute(Operator):
bl_label = "Select Attribute"
bl_options = {"REGISTER", "UNDO"}
attribute_name: bpy.props.StringProperty(default="")
attribute_value: bpy.props.StringProperty(default="")
def execute(self, context):
attribute_name = self.attribute_name or context.scene.BIMSearchProperties.search_attribute_name
attribute_value = self.attribute_value
if not attribute_name:
self.report({"ERROR"}, "Set Attribute Name for search.")
return {"CANCELLED"}
self.file = IfcStore.get_file()
props = context.scene.BIMSearchProperties
pattern = props.search_attribute_value
attribute_name = props.search_attribute_name
n_objects = 0
for obj in context.visible_objects:
if not obj.BIMObjectProperties.ifc_definition_id:
continue
@@ -130,8 +165,10 @@ class SelectAttribute(Operator):
value = next((v for k, v in data.items() if k.lower() == attribute_name.lower()), None)
else:
value = getattr(element, attribute_name, None)
if does_keyword_exist(pattern, value, context):
if does_keyword_exist(attribute_value, value, context):
obj.select_set(True)
n_objects += 1
self.report({"INFO"}, f"{n_objects} objects selected.")
return {"FINISHED"}
@@ -142,12 +179,25 @@ class SelectPset(Operator):
bl_label = "Select Pset"
bl_options = {"REGISTER", "UNDO"}
pset_name: bpy.props.StringProperty(default="")
prop_name: bpy.props.StringProperty(default="")
pset_value: bpy.props.StringProperty(default="")
def execute(self, context):
search_pset_name = self.pset_name or context.scene.BIMSearchProperties.search_pset_name
search_prop_name = self.prop_name or context.scene.BIMSearchProperties.search_prop_name
pattern = self.pset_value
if not search_pset_name:
self.report({"ERROR"}, "Set PSet name for search.")
return {"CANCELLED"}
if not search_prop_name:
self.report({"ERROR"}, "Set Property name for search.")
return {"CANCELLED"}
self.file = IfcStore.get_file()
props = context.scene.BIMSearchProperties
search_pset_name = props.search_pset_name
search_prop_name = props.search_prop_name
pattern = props.search_pset_value
n_objects = 0
for obj in context.visible_objects:
if not obj.BIMObjectProperties.ifc_definition_id:
continue
@@ -166,6 +216,8 @@ class SelectPset(Operator):
value = props.get(search_prop_name, None)
if does_keyword_exist(pattern, value, context):
obj.select_set(True)
n_objects += 1
self.report({"INFO"}, f"{n_objects} objects selected.")
return {"FINISHED"}
@@ -176,6 +228,8 @@ class ColourByAttribute(Operator):
bl_label = "Colour by Attribute"
bl_options = {"REGISTER", "UNDO"}
attribute_name: bpy.props.StringProperty(default="")
def execute(self, context):
IfcStore.begin_transaction(self)
self.store_state(context)
@@ -185,10 +239,15 @@ class ColourByAttribute(Operator):
return result
def _execute(self, context):
attribute_name = self.attribute_name or context.scene.BIMSearchProperties.search_attribute_name
if not attribute_name:
self.report({"ERROR"}, "Set Attribute Name for search.")
return {"CANCELLED"}
self.file = IfcStore.get_file()
colours = cycle(colour_list)
values = {}
attribute_name = context.scene.BIMSearchProperties.search_attribute_name
for obj in context.visible_objects:
if not obj.BIMObjectProperties.ifc_definition_id:
continue
@@ -227,6 +286,9 @@ class ColourByPset(Operator):
bl_label = "Colour by Pset"
bl_options = {"REGISTER", "UNDO"}
pset_name: bpy.props.StringProperty(default="")
prop_name: bpy.props.StringProperty(default="")
def execute(self, context):
IfcStore.begin_transaction(self)
self.store_state(context)
@@ -236,11 +298,19 @@ class ColourByPset(Operator):
return result
def _execute(self, context):
search_pset_name = self.pset_name or context.scene.BIMSearchProperties.search_pset_name
search_prop_name = self.prop_name or context.scene.BIMSearchProperties.search_prop_name
if not search_pset_name:
self.report({"ERROR"}, "Set PSet name for search.")
return {"CANCELLED"}
if not search_prop_name:
self.report({"ERROR"}, "Set Property name for search.")
return {"CANCELLED"}
self.file = IfcStore.get_file()
colours = cycle(colour_list)
values = {}
search_pset_name = context.scene.BIMSearchProperties.search_pset_name
search_prop_name = context.scene.BIMSearchProperties.search_prop_name
for obj in context.visible_objects:
if not obj.BIMObjectProperties.ifc_definition_id:
continue
@@ -364,6 +434,13 @@ class ActivateIfcClassFilter(Operator):
bl_idname = "bim.activate_ifc_class_filter"
bl_label = "Filter by Class"
@classmethod
def poll(cls, context):
if not context.selected_objects:
cls.poll_message_set("Select objects to filter.")
return False
return True
def invoke(self, context, event):
props = bpy.context.scene.BIMSearchProperties
props.filter_classes.clear()
@@ -410,6 +487,13 @@ class ActivateIfcBuildingStoreyFilter(Operator):
bl_idname = "bim.activate_ifc_building_storey_filter"
bl_label = "Filter by Building Storey"
@classmethod
def poll(cls, context):
if not context.selected_objects:
cls.poll_message_set("Select objects to filter.")
return False
return True
def invoke(self, context, event):
props = bpy.context.scene.BIMSearchProperties
props.filter_building_storeys.clear()
@@ -475,14 +559,18 @@ class FilterModelElements(Operator):
def execute(self, context):
selection_props = context.scene.IfcSelectorProperties
selection = selection_props.selector_query_syntax if selection_props.manual_override else self.add_groups(selection_props)
selection = (
selection_props.selector_query_syntax
if selection_props.manual_override
else self.add_groups(selection_props)
)
selection_props.selector_query_syntax = selection
r = core.search(tool.Search, tool.Spatial, query=selection, action=self.option)
if isinstance(r, str):
self.report({"WARNING"}, r)
return {"FINISHED"}
def add_groups(self, selector:str) -> str:
def add_groups(self, selector: str) -> str:
selection = ""
for group_index, group in enumerate(selector.groups):
if group_index != 0:
@@ -517,7 +605,6 @@ class FilterModelElements(Operator):
def add_filters(self, selection: str, query: str) -> str:
for f_index, f in enumerate(query.filters):
if f_index != 0:
selection += " & " if f.and_or == "and" else " | "
selection += f".{query.active_option}"
@@ -535,12 +622,13 @@ class FilterModelElements(Operator):
selection += f'{f.active_option.split(": ")[1]}.{f.active_sub_option.split(": ")[1]} {"!" if f.negation else ""}{f.comparison} {value}'
elif f.selector == "Attribute":
# we're using the prop_search functionality in blender which returns the index of the option. Sometimes the user can override this and enter a value that doesn't exist in the list. In this case there is no index and we need to handle it. @vulevukusej
pattern = re.compile(r'^[0-9]+:')
pattern = re.compile(r"^[0-9]+:")
match = pattern.search(f.active_option)
selection += f'{f.active_option.split(": ")[1] if match else f.active_option} {"!" if f.negation else ""}{f.comparison} {value}'
selection += "]"
return selection
# This needs to be moved into ui code, I know ;) - vulevukusej
class IfcSelector(Operator):
"""Select elements in model with IFC Selector"""
@@ -53,15 +53,23 @@ class BIM_PT_search(Panel):
row = self.layout.row(align=True)
row.prop(props, "search_attribute_name", text="", icon="PROPERTIES")
row.prop(props, "search_attribute_value", text="")
row.operator("bim.select_attribute", text="", icon="VIEWZOOM")
row.operator("bim.colour_by_attribute", text="", icon="BRUSH_DATA")
op = row.operator("bim.select_attribute", text="", icon="VIEWZOOM")
op.attribute_name = props.search_attribute_name
op.attribute_value = props.search_attribute_value
op = row.operator("bim.colour_by_attribute", text="", icon="BRUSH_DATA")
op.attribute_name = props.search_attribute_name
row = self.layout.row(align=True)
row.prop(props, "search_pset_name", text="", icon="COPY_ID")
row.prop(props, "search_prop_name", text="")
row.prop(props, "search_pset_value", text="")
row.operator("bim.select_pset", text="", icon="VIEWZOOM")
row.operator("bim.colour_by_pset", text="", icon="BRUSH_DATA")
op = row.operator("bim.select_pset", text="", icon="VIEWZOOM")
op.pset_name = props.search_pset_name
op.prop_name = props.search_prop_name
op.pset_value = props.search_pset_value
op = row.operator("bim.colour_by_pset", text="", icon="BRUSH_DATA")
op.pset_name = props.search_pset_name
op.prop_name = props.search_prop_name
row = self.layout.row(align=True)
row.operator("bim.activate_ifc_class_filter", icon="FILTER")
@@ -60,6 +60,8 @@ def register():
def unregister():
if not bpy.app.background:
bpy.utils.unregister_tool(workspace.SpatialTool)
del bpy.types.Scene.BIMSpatialProperties
del bpy.types.Object.BIMObjectSpatialProperties
del bpy.types.Scene.BIMSpatialManagerProperties
@@ -43,7 +43,11 @@ class SpatialData:
@classmethod
def containers(cls):
results = {}
for container in tool.Ifc.get().by_type("IfcSpatialElement"):
if tool.Ifc.get_schema() == "IFC2X3":
spatial_elements = tool.Ifc.get().by_type("IfcSpatialStructureElement")
else:
spatial_elements = tool.Ifc.get().by_type("IfcSpatialElement")
for container in spatial_elements:
results[container.id()] = {
"type": container.is_a(),
"id": container.id(),
@@ -49,6 +49,7 @@ class SpatialTool(WorkSpaceTool):
("bim.spatial_hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
("bim.spatial_hotkey", {"type": "B", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_B")]}),
("bim.spatial_hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}),
("bim.spatial_hotkey", {"type": "G", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_G")]}),
)
def draw_settings(context, layout, ws_tool):
@@ -106,6 +107,14 @@ class SpatialToolUI:
@classmethod
def draw_selected_object_interface(cls, context):
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
if element and bpy.context.selected_objects and element.is_a("IfcSpace"):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
row.operator("bim.generate_space", text="Regen")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_B")
@@ -167,3 +176,6 @@ class Hotkey(bpy.types.Operator, Operator):
def hotkey_S_T(self):
bpy.ops.bim.toggle_space_visibility()
def hotkey_S_G(self):
bpy.ops.bim.generate_space()
@@ -100,5 +100,7 @@ def register():
def unregister():
if not bpy.app.background:
bpy.utils.unregister_tool(workspace.StructuralTool)
del bpy.types.Scene.BIMStructuralProperties
del bpy.types.Object.BIMStructuralProperties
@@ -37,6 +37,7 @@ classes = (
operator.UnlinkStyle,
operator.UpdateStyleColours,
operator.UpdateStyleTextures,
operator.ClearTextureMapPath,
prop.Style,
prop.BIMStylesProperties,
prop.BIMStyleProperties,
@@ -45,6 +46,7 @@ classes = (
ui.BIM_PT_style_attributes,
ui.BIM_PT_external_style_attributes,
ui.BIM_UL_styles,
ui.BIM_PT_STYLE_GRAPH,
)
@@ -30,11 +30,26 @@ import os
class UpdateStyleColours(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.update_style_colours"
bl_label = "Save Current Shading Style"
bl_description = "Save current style values to IfcSurfaceStyleShading"
bl_description = (
"Save current style values to IfcSurfaceStyleShading.\n\n" + "ALT+CLICK to see saved values details"
)
bl_options = {"REGISTER", "UNDO"}
verbose: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# verobse print to console on alt+click
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
if event.type == "LEFTMOUSE" and event.alt:
self.verbose = True
return self.execute(context)
def _execute(self, context):
core.update_style_colours(tool.Ifc, tool.Style, obj=context.active_object.active_material)
mat = context.active_object.active_material
core.update_style_colours(tool.Ifc, tool.Style, obj=mat, verbose=self.verbose)
if self.verbose:
self.report({"INFO"}, "Check the system console to see saved style properties")
tool.Style.set_surface_style_props(mat)
class UpdateStyleTextures(bpy.types.Operator, tool.Ifc.Operator):
@@ -259,9 +274,10 @@ class BrowseExternalStyle(bpy.types.Operator):
external_style = StyleAttributesData.data["style_elements"].get("IfcExternallyDefinedSurfaceStyle", None)
if self.use_relative_path:
filepath = os.path.relpath(self.filepath, bpy.path.abspath("//"))
filepath = os.path.relpath(self.filepath, tool.Ifc.get_path())
else:
filepath = self.filepath
filepath = Path(filepath).as_posix()
attributes = {
"Location": filepath,
@@ -377,3 +393,25 @@ class SelectByStyle(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
core.select_by_style(tool.Style, tool.Spatial, style=tool.Ifc.get().by_id(self.style))
class ClearTextureMapPath(bpy.types.Operator):
bl_idname = "bim.clear_texture_map_path"
bl_label = "Clear Texture Map Path"
bl_options = {"REGISTER", "UNDO"}
texture_map_prop: bpy.props.StringProperty(default="")
@classmethod
def poll(cls, context):
poll = getattr(context, "material", None)
if not poll:
cls.poll_message_set("Select a material")
return poll
def execute(self, context):
if not self.texture_map_prop:
self.report({"ERROR"}, "Provide a texture map")
return {"CANCELLED"}
props = context.material.BIMStyleProperties
setattr(props, self.texture_map_prop, "")
return {"FINISHED"}
@@ -96,9 +96,43 @@ def update_shading_style(self, context):
if rendering_style and texture_style:
tool.Loader.create_surface_style_with_textures(blender_material, rendering_style, texture_style)
tool.Style.set_surface_style_props(blender_material)
tool.Style.record_shading(blender_material)
# TODO: support more more methods
REFLECTANCE_METHODS = [
("PHYSICAL", "PHYSICAL", ""),
("FLAT", "FLAT", ""),
# ("METAL", "METAL", ""),
# ("MATT", "MATT", ""),
# ("GLASS", "GLASS", ""),
# ("NOTDEFINED", "NOTDEFINED", ""),
]
def update_shader_graph(self, context):
if not self.update_graph:
return
material = self.id_data
style_data = tool.Style.get_surface_style_from_props(material)
textures_data = tool.Style.get_texture_style_from_props(material)
tool.Loader.create_surface_style_rendering(material, style_data)
tool.Loader.create_surface_style_with_textures(material, style_data, textures_data)
def update_graph_get(self):
return self.get("update_graph", True)
def update_graph_set(self, value):
self["update_graph"] = value
if value:
material = self.id_data
tool.Style.set_surface_style_props(material)
class BIMStyleProperties(PropertyGroup):
attributes: CollectionProperty(name="Attributes", type=Attribute)
is_editing: BoolProperty(name="Is Editing")
@@ -113,3 +147,79 @@ class BIMStyleProperties(PropertyGroup):
default="Shading",
update=update_shading_style,
)
# GLTF style properties
update_graph: BoolProperty(
name="Update Shade Graph on Prop Change",
description="Update shader graph in real time\nas you update style properties",
default=True,
get=update_graph_get,
set=update_graph_set,
)
reflectance_method: EnumProperty(
name="Reflectance Method",
description="Reflectance method to use for the material",
items=REFLECTANCE_METHODS,
default="PHYSICAL",
update=update_shader_graph,
)
surface_color: bpy.props.FloatVectorProperty(
name="Surface Color",
subtype="COLOR",
default=(1, 1, 1, 1),
min=0.0,
max=1.0,
size=4,
update=update_shader_graph,
)
diffuse_color: bpy.props.FloatVectorProperty(
name="Diffuse Color",
subtype="COLOR",
default=(1, 1, 1, 1),
min=0.0,
max=1.0,
size=4,
update=update_shader_graph,
)
transparency: bpy.props.FloatProperty(
name="Transparency", default=0.0, min=0.0, max=1.0, update=update_shader_graph
)
roughness: bpy.props.FloatProperty(name="Roughness", default=0.0, min=0.0, max=1.0, update=update_shader_graph)
metallic: bpy.props.FloatProperty(name="Metallic", default=0.0, min=0.0, max=1.0, update=update_shader_graph)
normal_path: bpy.props.StringProperty(
name="NormalMap",
maxlen=1024,
default="",
subtype="FILE_PATH",
update=update_shader_graph,
)
emissive_path: bpy.props.StringProperty(
name="Emissive",
maxlen=1024,
default="",
subtype="FILE_PATH",
update=update_shader_graph,
)
metallic_roughness_path: bpy.props.StringProperty(
name="Metallic/Roughness",
maxlen=1024,
default="",
subtype="FILE_PATH",
update=update_shader_graph,
description="Green Channel = Roughness,\nBlue Channel = Metallic",
)
diffuse_path: bpy.props.StringProperty(
name="Diffuse",
maxlen=1024,
default="",
subtype="FILE_PATH",
update=update_shader_graph,
)
occlusion_path: bpy.props.StringProperty(
name="Occlusion",
description="Note that occlusion isn't actually used in Blender shader, we're just storing the data",
maxlen=1024,
default="",
subtype="FILE_PATH",
update=update_shader_graph,
)
@@ -16,11 +16,13 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import blenderbim.bim.helper
from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.style.data import StylesData, StyleAttributesData
from bl_ui.properties_material import MaterialButtonsPanel
from blenderbim.tool.style import TEXTURE_MAPS_BY_METHODS, STYLE_TEXTURE_PROPS_MAP
class BIM_PT_styles(Panel):
@@ -65,6 +67,24 @@ class BIM_PT_styles(Panel):
self.layout.template_list("BIM_UL_styles", "", self.props, "styles", self.props, "active_style_index")
def draw_style_ui(self, context):
mat = context.material
props = mat.BIMMaterialProperties
style_props = mat.BIMStyleProperties
row = self.layout.row(align=True)
if not props.ifc_style_id:
row.operator("bim.add_style", icon="ADD")
return
row.prop(style_props, "active_style_type", icon="SHADING_RENDERED", text="")
row.operator("bim.update_current_style", icon="FILE_REFRESH", text="")
row = self.layout.row(align=True)
row.operator("bim.update_style_colours", icon="GREASEPENCIL")
row.operator("bim.update_style_textures", icon="TEXTURE", text="")
row.operator("bim.unlink_style", icon="UNLINKED", text="")
row.operator("bim.remove_style", icon="X", text="").style = props.ifc_style_id
class BIM_PT_style(MaterialButtonsPanel, Panel):
bl_label = "IFC Style"
bl_idname = "BIM_PT_style"
@@ -81,22 +101,11 @@ class BIM_PT_style(MaterialButtonsPanel, Panel):
)
def draw(self, context):
props = context.active_object.active_material.BIMMaterialProperties
style_props = context.active_object.active_material.BIMStyleProperties
mat = context.material
row = self.layout.row(align=True)
props = mat.BIMMaterialProperties
draw_style_ui(self, context)
if not props.ifc_style_id:
row.operator("bim.add_style", icon="ADD")
return
row.prop(style_props, "active_style_type", icon="SHADING_RENDERED", text="")
row.operator("bim.update_current_style", icon="FILE_REFRESH", text="")
row = self.layout.row(align=True)
row.operator("bim.update_style_colours", icon="GREASEPENCIL")
row.operator("bim.update_style_textures", icon="TEXTURE", text="")
row.operator("bim.unlink_style", icon="UNLINKED", text="")
row.operator("bim.remove_style", icon="X", text="").style = props.ifc_style_id
row = self.layout.row(align=True)
row.prop(mat, "diffuse_color", text="Viewport Color" if mat.use_nodes else "Render Color")
@@ -212,3 +221,66 @@ class BIM_UL_styles(UIList):
row2 = row.row()
row2.alignment = "RIGHT"
row2.label(text=str(item.total_elements))
class BIM_PT_STYLE_GRAPH(Panel):
bl_idname = "BIM_PT_style_graph"
bl_space_type = "NODE_EDITOR"
bl_label = "IFC Style Graph Settings"
bl_region_type = "UI"
bl_category = "BBIM"
@classmethod
def poll(cls, context):
return getattr(context, "material", None)
def draw(self, context):
layout = self.layout
props = context.active_object.active_material.BIMStyleProperties
draw_style_ui(self, context)
layout.separator()
box = layout.box()
box.label(text="Creating shader from this panel")
box.label(text="ensures that shader is")
box.label(text="GLTF compatible")
box.label(text="and therefore will be ")
box.label(text="stored in IFC safely.")
layout.separator()
layout.prop(props, "update_graph", text="Graph Auto Update")
layout.label(text="Reflectance Method:")
layout.prop(props, "reflectance_method", text="")
layout.prop(props, "surface_color")
layout.prop(props, "transparency")
if not (props.reflectance_method == "PHYSICAL" and props.diffuse_path) and not (
props.reflectance_method == "FLAT" and props.emissive_path
):
prop_name = "Emissive Color" if props.reflectance_method == "FLAT" else "Diffuse Color"
layout.prop(props, "diffuse_color", text=prop_name)
if props.reflectance_method == "PHYSICAL" and not props.metallic_roughness_path:
layout.prop(props, "metallic")
if props.reflectance_method not in ("PHYSICAL", "FLAT", "NOTDEFINED") or (
props.reflectance_method == "PHYSICAL" and not props.metallic_roughness_path
):
layout.prop(props, "roughness")
def add_texture_path(path_name):
row = layout.row(align=True)
row.prop(props, path_name)
op = row.operator("bim.clear_texture_map_path", text="", icon="X")
op.texture_map_prop = path_name
layout.separator()
texture_maps = TEXTURE_MAPS_BY_METHODS.get(props.reflectance_method, [])
if not texture_maps:
return
if not bpy.data.filepath:
layout.label(text="Save .blend file to keep relative paths", icon="ERROR")
layout.label(text="Texture Maps:")
for texture_type in texture_maps:
add_texture_path(STYLE_TEXTURE_PROPS_MAP[texture_type])
@@ -27,6 +27,7 @@ classes = (
operator.EnableEditingType,
operator.PurgeUnusedTypes,
operator.RemoveType,
operator.RenameType,
operator.SelectSimilarType,
operator.SelectType,
operator.SelectTypeObjects,
@@ -141,10 +141,16 @@ class SelectType(bpy.types.Operator):
def execute(self, context):
element = tool.Ifc.get().by_id(self.relating_type)
tool.Spatial.select_products([element])
obj = tool.Ifc.get_object(element)
if obj:
context.view_layer.objects.active = obj
if obj in context.selectable_objects:
tool.Blender.select_and_activate_single_object(context, obj)
else:
self.report({"INFO"}, "Type object can't be selected : It may be hidden or in an excluded collection.")
# IfcTypeProducts are only used for annotations and not part of the model interface.
if element.is_a() != "IfcTypeProduct":
context.scene.BIMModelProperties.ifc_class = element.is_a()
context.scene.BIMModelProperties.relating_type_id = str(self.relating_type)
return {"FINISHED"}
@@ -194,7 +200,7 @@ class SelectTypeObjects(bpy.types.Operator):
class AddType(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_type"
bl_label = "Add Type"
bl_options = {"REGISTER"}
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.scene.BIMModelProperties
@@ -205,6 +211,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
ifc_file = tool.Ifc.get()
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
if not body:
props.type_class = props.type_class
return {"FINISHED"}
if template == "MESH":
@@ -329,8 +336,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
tool.Collector,
tool.Root,
obj=obj,
predefined_type=predefined_type,
ifc_class="IfcWindowType",
predefined_type=predefined_type if tool.Ifc.get_schema() != "IFC2X3" else None,
ifc_class="IfcWindowType" if tool.Ifc.get_schema() != "IFC2X3" else "IfcWindowStyle",
should_add_representation=False,
)
bpy.ops.object.select_all(action="DESELECT")
@@ -347,8 +354,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
tool.Collector,
tool.Root,
obj=obj,
predefined_type=predefined_type,
ifc_class="IfcDoorType",
predefined_type=predefined_type if tool.Ifc.get_schema() != "IFC2X3" else None,
ifc_class="IfcDoorType" if tool.Ifc.get_schema() != "IFC2X3" else "IfcDoorStyle",
should_add_representation=False,
)
bpy.ops.object.select_all(action="DESELECT")
@@ -378,7 +385,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
elif template == "RAILING":
mesh = bpy.data.meshes.new("IfcRailing")
obj = bpy.data.objects.new("TYPEX", mesh)
obj = bpy.data.objects.new(name, mesh)
element = blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
@@ -397,7 +404,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
elif template == "ROOF":
mesh = bpy.data.meshes.new("IfcRoof")
obj = bpy.data.objects.new("TYPEX", mesh)
obj = bpy.data.objects.new(name, mesh)
element = blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
@@ -415,6 +422,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.add_roof()
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class)
props.type_class = props.type_class
return {"FINISHED"}
def add_default_material(self):
@@ -428,7 +436,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
class RemoveType(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_type"
bl_label = "Remove Type"
bl_options = {"REGISTER"}
bl_options = {"REGISTER", "UNDO"}
element: bpy.props.IntProperty()
def _execute(self, context):
@@ -438,13 +446,35 @@ class RemoveType(bpy.types.Operator, tool.Ifc.Operator):
if obj:
tool.Ifc.unlink(obj=obj)
bpy.data.objects.remove(obj)
return {"FINISHED"}
class RenameType(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.rename_type"
bl_label = "Rename Type"
bl_options = {"REGISTER", "UNDO"}
element: bpy.props.IntProperty()
name: bpy.props.StringProperty(name="Name")
def _execute(self, context):
element = tool.Ifc.get().by_id(self.element)
obj = tool.Ifc.get_object(element)
element.Name = self.name
if obj:
tool.Root.set_object_name(obj, element)
def invoke(self, context, event):
element = tool.Ifc.get().by_id(self.element)
self.name = element.Name or "Unnamed"
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
self.layout.prop(self, "name")
class DuplicateType(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.duplicate_type"
bl_label = "Duplicate Type"
bl_options = {"REGISTER"}
bl_options = {"REGISTER", "UNDO"}
element: bpy.props.IntProperty()
def _execute(self, context):
@@ -470,7 +500,7 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator):
class PurgeUnusedTypes(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.purge_unused_types"
bl_label = "Purge Unused Types"
bl_options = {"REGISTER"}
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.purge_unused_types(tool.Ifc, tool.Type)
@@ -109,8 +109,10 @@ class BIM_PT_booleans(Panel):
if context.active_object.data.BIMMeshProperties.ifc_definition_id:
row = layout.row(align=True)
row.label(text=f"{BooleansData.data['total_booleans']} Booleans Found")
row.operator("bim.add_boolean", text="", icon="ADD")
row.operator("bim.show_booleans", text="", icon="HIDE_OFF")
row.operator("bim.add_boolean", text="Apply Boolean", icon="ADD")
show_boolean_button = row.row(align=True)
show_boolean_button.operator("bim.show_booleans", text="", icon="HIDE_OFF")
show_boolean_button.enabled = BooleansData.data['total_booleans'] > 0
row.operator("bim.hide_booleans", text="", icon="HIDE_ON")
elif context.active_object.data.BIMMeshProperties.ifc_boolean_id:
row = layout.row()

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