Compare commits

...

476 Commits

Author SHA1 Message Date
Andrej730 ecee3781e4 Fix more missing Blender descriptions leading to crashes #6420 2025-04-15 19:43:38 +05:00
Andrej730 52f949b2dc assign_class - avoid empty geometries and assigning shape reprs to structural items 2025-04-15 19:43:38 +05:00
Andrej730 cb7e04f61a assign_class - replace object with empty if no representation was added
Same goal as ffffcc576e - prevent user confusion when they reload IFC file and see the representation is gone.
2025-04-15 19:43:38 +05:00
Andrej730 b59e5b076d get_pset, get_psets - don't fail on ifc2x3 profile psets and partially handle ifc2x3 material psets #6538 2025-04-14 18:59:47 +05:00
Andrej730 1516237537 Allow Undefined topology representations for IfcStructuralPointConnection #6459
Because apparently it's not invalid IFC.
IfcStructuralPointConnection documentation says that they should have a topology representation with a IfcVertexPoint but it doesn't restict this representation from having Undefined type if it does have a IfcVertexPoint as it's item.
2025-04-14 17:49:17 +05:00
Andrej730 53cd5f834e add_mesh_representation - reuse shape builder + numpy input support 2025-04-14 16:17:59 +05:00
Andrej730 6dc6fddd0e add_mesh_representation - expand documentation and typing #6541 2025-04-14 16:17:59 +05:00
Dion Moult 52c8cc0a72 Fix #6140. You can configure the force snap setting.
I can't figure out a more elegant way of doing this. I.e. I don't know
how to detect whether or not the user has explicitly set their own snap
default.
2025-04-13 17:57:08 +10:00
Dion Moult 463473b959 Fix #6359. No longer sync materials at export sync we have a proper material / style manager now not Blender materials. 2025-04-13 16:09:36 +10:00
Dion Moult fd0c578995 No longer sync edited objects since we don't defer representation updates anymore due to item mode. 2025-04-13 16:06:15 +10:00
Dion Moult 74fbe94ed4 Update feature_request.yml 2025-04-13 12:10:28 +10:00
Dion Moult 38002e1ee2 Update and rename feature_request.md to feature_request.yml 2025-04-13 12:08:28 +10:00
Dion Moult 58b070d22d Update and rename bug_report.md to bug_report.yml 2025-04-13 12:06:46 +10:00
Dion Moult e2b119bf9b Update feature_request.md 2025-04-13 11:42:53 +10:00
Dion Moult 8cf3807c2c Update feature_request.md 2025-04-13 11:42:11 +10:00
Dion Moult ad3d0fecb2 Describe other channels in issue template 2025-04-13 11:38:33 +10:00
Dion Moult fcfe6f97c3 Update bug_report.md 2025-04-13 11:32:30 +10:00
Dion Moult c0190b5065 Fix #6452. Fix #6478. Disable camera clipping due to unpredictable Blender behaviour (reported upstream).
https://projects.blender.org/blender/blender/issues/137384
2025-04-13 11:20:57 +10:00
Andrej730 e596ac6b1b dev_environment.bat - ensure symlinks are loaded #6473 2025-04-11 19:55:27 +05:00
Andrej730 d9ed6a4950 Fix bim test after 1e3f1e7dfc 2025-04-11 19:55:26 +05:00
Andrej730 a49ccfcfc8 test_append_asset - fix skipped test
It wasn't appending library_material so test would never fail
2025-04-11 19:55:26 +05:00
Andrej730 7974d8a0d0 project.append_asset - test assume_asset_uniqueness_by_name=False 2025-04-11 19:55:26 +05:00
Andrej730 f00e77ef6c bim.validate_ifc_assets Bonsai operator #6243
There's now operator to check that all profiles, styles, materials in the current project have a unique name. Which is important since it's used when we append assets to ensure there are no duplicated assets.

Operator location - https://i.imgur.com/bvXjZ8Y.png

Example output:
```
IfcMaterial name 'KnownUnknown' is used by multiple assets:
- # 260=IfcMaterial('KnownUnknown',$,$)
- # 1220=IfcMaterial('KnownUnknown',$,$)

Found issues validating IfcProfileDef assets.
IfcProfileDef name 'DEMO-C' is used by multiple assets:
- # 266=IfcCShapeProfileDef(.AREA.,'DEMO-C',$,0.2,0.1,0.0015,0.03,0.005)
- # 261=IfcIShapeProfileDef(.AREA.,'DEMO-C',$,0.1,0.2,0.005,0.01,0.005,$,$)

Found issues validating IfcPresentationStyle assets.
IfcPresentationStyle name 'Frame' is used by multiple assets:
- # 363=IfcSurfaceStyle('Frame',.BOTH.,(# 362))
- # 1221=IfcSurfaceStyle('Frame',.BOTH.,(# 1222))
```
2025-04-11 19:55:26 +05:00
Andrej730 fc37cca1d0 ExtractElements - expose assume_asset_uniqueness_by_name option #6243
I'd much rather not expose this option to ifcpatch but I guess models are different and there should be a way to extract elements even if assets name uniqueness is not ensured (since it's not IFC requirement anyway). But it may cause duplicated assets.
2025-04-11 19:55:26 +05:00
Andrej730 53da00a5b7 append_asset - fix possible errors with assume_asset_uniqueness_by_name=False after f43e33b
If `assume_asset_uniqueness_by_name` is `False`, then we don't really need `SafeRemovalContext`.
2025-04-11 19:55:25 +05:00
Andrej730 b5bc53bdf6 Renaming style from UI not to add .xxx Blender suffix to style name
Set style name explicitly, this way what user does in UI is what they get in IFC.
Previously confusingly if you set style name to "Glass" and "Glass" already existed, you would get "Glass.001" style internally but "Glass" name in UI.
2025-04-11 19:55:25 +05:00
Andrej730 bc0fbf744a bim.search - set found object as active
As sometimes it's hard of find a selected object in outliner if it's not active.
2025-04-11 19:55:25 +05:00
Andrej730 53b26a9b66 typing 2025-04-11 19:55:25 +05:00
Thomas Krijnen 858e73c9d9 IfcPropertySetDefinitionSet in xml serializer #6330 2025-04-11 15:53:27 +02:00
Thomas Krijnen 952400bd58 Support defined types with instance references in parser (IfcPropertySetDefinitionSet; #6330) 2025-04-11 15:52:40 +02:00
Andrej730 ffffcc576e assign_class - reload representation after assignment
Just to make sure user will set what was actually saved to IFC right away. To prevent confusion when they see it only after model was reloaded completely.
2025-04-10 19:16:04 +05:00
Andrej730 d8102d848d Fix bug with the first object rename after class assignment
The problem was that during class assignment, it was setting the object name and `is_renaming` flag but only then linking to IFC. Since object wasn't linked to IFC, name callback wasn't triggered and `is_renaming` wasn't reset and first rename wouldn't work.
Now it's just not setting the flag if object is not linked to IFC, as there's no callback then and don't need to protect from it.
2025-04-10 17:44:06 +05:00
Andrej730 9407faf1f9 vs-cfg.md - support cmake 4.0.0 2025-04-10 17:44:06 +05:00
Andrej730 1e3f1e7dfc Show assign/reference container buttons if container has no elements #6514 2025-04-10 10:54:46 +05:00
Andrej730 8aad28c730 bim.assign_class - skip ifc objects to prevent errors 2025-04-09 18:26:55 +05:00
Andrej730 81943ec9e5 bim.assign_class - apply modifiers
Noticed working on #6500
2025-04-09 18:26:55 +05:00
Andrej730 5b15de50b6 bim.assign_class - use blender operators in a more safe way
Use temp_override or modify selection not to modify some object by accident, because this operator can also be called not from UI but by providing `object` argument.
2025-04-09 18:26:54 +05:00
Andrej730 4d50dce16d bim.assign_class - use get_selected_objects 2025-04-09 18:26:54 +05:00
Andrej730 69b5d0940f bim.assign_class - flip normals if matrix is negative #6500
object.transform_apply doesn't preserve normals (https://projects.blender.org/blender/blender/issues/102266)
2025-04-09 18:26:54 +05:00
Andrej730 273d6c1039 Experimental geo nodes integration 2025-04-09 18:26:54 +05:00
Andrej730 24ef03c96e bim.assign_container - skip aggregated elements #6514
New description - https://i.imgur.com/UeSPtEe.png
2025-04-09 18:26:54 +05:00
Andrej730 cc4553d3c9 bim.aggregate_assign_object - clarification 2025-04-09 18:26:54 +05:00
Andrej730 cf1f74e260 typing 2025-04-09 18:26:53 +05:00
Andrej730 3052c6e578 black . 2025-04-09 18:26:53 +05:00
Bruno Perdigão 0eb70b7fc3 See #6397 - Fix extrusion profile editing issue.
Profile transformations now correctly use the perpendicular value of the offset.
2025-04-08 20:07:46 -03:00
Bruno Perdigão eb863bb2aa Snap - Filter out 2D bounding boxes that are too far from the camera view. 2025-04-08 18:11:01 -03:00
Andrej730 345a6e72d9 Make referencing in structure more discoverable #6514
Didn't wanted to disrupt existing operators, but we probably should deprecate them.
2025-04-08 15:54:41 +05:00
Andrej730 8b775ea30f bim.assign_container - small clarification 2025-04-08 15:54:41 +05:00
Thomas Krijnen b712f30ad3 Don't assign material as style when it doesn't have any styles #6314 2025-04-07 21:25:33 +02:00
Andrej730 0bbe1aad5b Fix UI errors in Blender 4.2 (missing MOUSE_MMB_SCROLL icon)
Noticed #6515
2025-04-07 16:20:22 +05:00
Andrej730 29de264b43 ifc5d - remove Qto_BodyGeometryValidation from ifc4x3 base qtos (6c04669)
As it's not part of base quantities and it would be unexpected for user if some additional qto would start calculating for every IfcProduct.
2025-04-07 16:20:22 +05:00
Andrej730 f4714d2012 ifc5d - avoid overriding results from previous queries after f63d6b8 2025-04-07 16:20:21 +05:00
Andrej730 8bfdabab80 ifc5d IfcOpenShell.calculate - warn on missing formulas 2025-04-07 16:20:21 +05:00
Andrej730 50c1220730 typing 2025-04-07 16:20:21 +05:00
Bruno Perdigão c280550e30 Measure Tool - fix issue where the area highlight was incorrectly displayed in polyline mode. 2025-04-05 11:55:05 -03:00
Bruno Perdigão 2bf303686c Snap - Improve how viewport zoom affects snapping. 2025-04-05 11:45:56 -03:00
Bruno Perdigão bb91416c4e Snap - Fix creation of 2D bounding boxes
Previously, objects with 2D bounding boxes outside the view were still being added. This issue is now resolved.
Additionally, implement simple AABB detection to check if the bounding box is within the viewport.
2025-04-04 20:19:25 -03:00
Bruno Perdigão 47e8b978af Snap - filter visible objects before creating the 2d bouding box 2025-04-04 18:38:07 -03:00
Andrej730 50eae4ae19 Fix typo, expose Extend To Underside description 2025-04-04 19:04:03 +05:00
Andrej730 979183b64b Make adding openings more discoverable
1) It's back as Shift-O hotkey in BIM tool - https://i.imgur.com/XfmfqNY.png
2) It's always visible in Voids UI - https://i.imgur.com/rO6nOSw.png
2025-04-04 19:04:03 +05:00
Andrej730 dc2f788326 model/wall.py - poll messages 2025-04-04 19:04:03 +05:00
Andrej730 7d12880eb9 BIM Tool - add descriptions to "Align" section buttons 2025-04-04 19:03:08 +05:00
Andrej730 6676c7f62e typing 2025-04-04 19:03:08 +05:00
Thomas Krijnen c0e8c3d1b5 Update ci-ifcopenshell-docker.yml - action versions 2025-04-04 14:11:41 +02:00
Andrej730 e9245efd37 bim.browse_external_style - cache enum items #6496 #5673 2025-04-03 19:37:51 +05:00
Andrej730 de287395d2 Bump Blender build in tests to 4.4 2025-04-03 19:18:42 +05:00
Andrej730 938d72ebe3 Fix UI issue with props missing (65c99c5) 2025-04-03 18:06:49 +05:00
Andrej730 605c86afc5 Fix void bim tests after bim.add_potential_opening was removed 2025-04-03 18:06:48 +05:00
Andrej730 68cf8ee247 BIM tests - replace some class assignment with PanelSpy version 2025-04-03 18:06:48 +05:00
Andrej730 a452d1d361 Fix failing bim tests after 5b33c60 2025-04-03 18:06:48 +05:00
Andrej730 71067e4a14 Fix style test in Blender 4.4 2025-04-03 18:06:48 +05:00
Andrej730 7bacdbf8dd typing 2025-04-03 17:45:49 +05:00
Thomas Krijnen 54c1dfdb77 Update ci-ifcopenshell-docker.yml 2025-04-03 09:29:49 +02:00
Thomas Krijnen 834123ad14 Update release.yml - add workflow_dispatch 2025-04-03 09:27:32 +02:00
Andrej730 3cbab975d5 Fix tests after ea84f82 2025-04-02 19:29:43 +05:00
Andrej730 0315542237 geometry.remove_representation_item - add element argument
Use explicit element argument instead of props.representation_obj as representation_obj may not be there (e.g. removing rep items without item mode) or some other object might be in item mode and then booleans won't be unmarked.

Found the issue because of the failing test 🥳
2025-04-02 19:29:43 +05:00
Andrej730 156cca5932 representation_obj - add a note 2025-04-02 19:29:43 +05:00
Andrej730 4672fc341f Fix representation items / booleans UI prioritizing item mode object over active obj
It led to confusion when object A would be in Item Mode and you open properties for object B and try to show rep items for B and UI would be unresponsive.
2025-04-02 19:29:43 +05:00
Andrej730 f69b02cad9 bim.override_object_join - a bit more readable error message
Use name instead of bpy_struct repr, add a line split.
2025-04-02 19:29:42 +05:00
Andrej730 40af3f0495 Fix bug adding brick namespace in ed0b1c5
brick graph has bool evaluation too
2025-04-02 19:29:42 +05:00
Andrej730 0dd42c1fb2 fix core system tests after 5b9ef23 2025-04-02 19:29:42 +05:00
Andrej730 0990dfd372 biim.load_link - error if file does not exist
Noticed investigating #6403
2025-04-02 12:42:02 +05:00
Andrej730 9316e0bff0 Skip loading linked models in the drawing if they're unloaded #6403
As it's probably unintuitive (user can't see the linked model in viewport but we still load it) and user then have no option to keep model in Links but keep it unloaded.

Also producing traceback below if linked file is missing.
```
Error: Python: Traceback (most recent call last):
  File "\bonsai\bim\module\drawing\operator.py", line 299, in execute
    linework_svg = self.generate_linework(context)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\bonsai\bim\module\drawing\operator.py", line 828, in generate_linework
    IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\ifcopenshell\__init__.py", line 162, in open
    raise FileNotFoundError(f"File does not exist: '{path}'.")
FileNotFoundError: File does not exist: 'D:\Dropbox\GitLab\OD_Library\BIM\OD_Revit_Template - 2020 - IFC4 - Reference View.ifc'.
```
2025-04-02 12:42:02 +05:00
Andrej730 bc709f06f7 Fix Blender 4.4 crashing on Mac (decorators issues) #6461 2025-04-01 19:16:16 +05:00
Andrej730 cc031a23cb black . 2025-04-01 19:16:16 +05:00
Andrej730 614b71e0c6 gitignore compiled ifcopenshell_wrapper files on mac/linux #4686 2025-04-01 19:10:05 +05:00
Andrej730 66329f07cb dev_environment.sh - validate input paths 2025-04-01 19:10:05 +05:00
falken10vdl 0039f376ca Update operator.py so argv parameters in Windows are correctly parsed
Currently teh parameters that have spaces are not properly parsed when invoking os.execv.

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

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

Blender quit

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

```
Traceback (most recent call last):
  File "/home/runner/work/IfcOpenShell/IfcOpenShell/src/bonsai/scripts/bonsai_translations.py", line 379, in <module>
    update_translations_from_po(po_directory=Path(args.input), translations_module=Path(args.output))
  File "/home/runner/work/IfcOpenShell/IfcOpenShell/src/bonsai/scripts/bonsai_translations.py", line 203, in update_translations_from_po
    if (msgstr := msg.translations[lang]) in (None, ""):
                  ~~~~~~~~~~~~~~~~^^^^^^
KeyError: 'de_DE'
```
2025-03-07 20:48:44 +05:00
Andrej730 197281eb5a bonsai_translations - use utf-8 for opening files
Critical on Windows as it's not using utf-8 by default
2025-03-07 20:48:44 +05:00
Andrej730 7b370e6142 Fix operators missing execute after a663216 2025-03-07 17:13:00 +05:00
Andrej730 71a7d0c086 Fix missing IfcDistributionCircuit and IfcElectricalCircuit #6202
When checking if element is assignable to the system.
2025-03-07 17:12:59 +05:00
Andrej730 b86e735530 Fix breaking Systems UI with IfcElectricalCircuits 2025-03-07 17:12:59 +05:00
Andrej730 46104f5bf5 Update Profiles UI profile editing arbitrary profile #6284 2025-03-07 17:12:59 +05:00
Andrej730 d68068a84b bim.load_profiles - try not to check all inverses for optimization 2025-03-07 17:12:59 +05:00
Andrej730 fc7dd3b234 Preserve ProfileName editing arbitrary profile #6284 2025-03-07 17:12:59 +05:00
Andrej730 85fc97eb84 Profiles UI - add safety check for missing profiles #6284
Example - https://imgur.com/a/bVdV5oB
2025-03-07 17:12:59 +05:00
Andrej730 9076deddbc Consider ifc2x3 in a358cff1b7 #6255
In ifc2x3 position is not optional, so we should reset it's coordinates instead.
2025-03-07 17:12:59 +05:00
Andrej730 eb0d3c38b2 ifcopenshell.util - make ifc_file argument optional if it can be deduced from ifc element 2025-03-07 17:12:59 +05:00
Andrej730 590288496e typing 2025-03-07 17:12:58 +05:00
Andrej730 269e137ea2 black . 2025-03-07 17:12:58 +05:00
Dion Moult ed722c6f13 See #1227. Refactor out test waldo script into API function. 2025-03-07 19:05:24 +11:00
Dion Moult 2b9e3d2a2e See #1227. Enable layerset slicing. 2025-03-07 19:03:45 +11:00
Dion Moult 7dee888ecf See #1227. New util function in shape builder to do an X axis intersection. 2025-03-07 19:00:18 +11:00
Dion Moult a67927ae67 Automatically merge excessive styles when loading models
Came across this problem with some models from 12D
2025-03-07 17:02:05 +11:00
Dion Moult 1271e4b7e3 New IfcPatch recipe to merge identical styles 2025-03-07 17:01:14 +11:00
Andrej730 a44d0e713a Bump Blender build and fix dead url 2025-03-06 19:11:46 +05:00
Andrej730 a358cff1b7 Fix extrusion position update on OffsetFromReferenceLine changed #6255
Also fixed an issue when previously there was an offset and now it was set to 0 but slab did not reset it's position.
2025-03-06 18:43:54 +05:00
Andrej730 188ca3e402 Fix bim.profiles_ui_select button in profiles set ui
It was using IfcMaterialProfile id instead of IfcProfileDef id, so profile was never selected
Example - https://imgur.com/a/Sf1ghPO
2025-03-06 18:43:54 +05:00
Andrej730 b5260d3005 copy_class to prevent duplicating profiles #6261 2025-03-06 18:43:54 +05:00
Andrej730 79940641b2 typing 2025-03-06 18:43:54 +05:00
Andrej730 7458040949 Fix ifcpatch unit conversion for lists of ifc entities #6280 2025-03-06 16:14:02 +05:00
Andrej730 14eb543662 TestConvertFileLengthUnits - add attributes conversion tests 2025-03-06 16:14:02 +05:00
Andrej730 e786687882 ConvertFileLengthUnits - unify tests, add ifc4x3 tests 2025-03-06 16:14:02 +05:00
Andrej730 37e131193b edit_georeferencing - add note on MapUnit data type 2025-03-06 16:14:02 +05:00
Andrej730 dfb00f27bb Fix georeferencing docs url 2025-03-06 16:14:02 +05:00
Dion Moult 53dea65d8b See #1227. Implement joins for sloped walls. 2025-03-06 19:38:46 +11:00
Andrej730 2c16839de7 Fix parametric stairs/railings/roofs not updating ifc link after representation update #6264
Related to 3107fd9
2025-03-05 18:53:57 +05:00
Andrej730 c4f2d556f1 Uncomment use of get_edges_representation_item_ids
as build was updated awhile ago
2025-03-05 18:53:57 +05:00
Andrej730 74900193ee merge_identical_objects to use quicker version of get_info 2025-03-05 18:53:57 +05:00
Andrej730 2987d38a31 append_asset - check unique styles based on their name #6269 2025-03-05 18:53:56 +05:00
Andrej730 08b50bb9ea Library UI - recognize already appended styles
Example - https://i.imgur.com/XCXHY4C.png
Noticed investigating #6269
2025-03-05 18:53:56 +05:00
Andrej730 1257eb0872 append_asset - allow using same name for different ifcpresentationstyle types
Since they are not used interchangeably typically.
2025-03-05 18:53:56 +05:00
Andrej730 1c8ef176b1 typing 2025-03-05 18:53:56 +05:00
Andrej730 e1e51cbf26 black . 2025-03-05 18:53:56 +05:00
Andrej730 4c97cc8a41 import_ifc - not to break completely on facing breaking mesh #6270
create_mesh returns None only in case if it meets some exception and it prints logs in that case, but atleast some breaking mesh won't be in the way of users trying to open some model
2025-03-05 18:53:09 +05:00
Dion Moult 7c6e124fe9 See #1227. Basic implementation of sloped walls (joins not yet considered) 2025-03-05 22:10:33 +11:00
Andrej730 3fc312752b Show an error for unsupport curve types #6259 2025-03-04 18:10:42 +05:00
Andrej730 d0cb5456b3 Fix importing IfcPolylines always assuming they're closed
Resolved issue during import in #6259 but doesn't completely resolves the issue.
2025-03-04 18:10:42 +05:00
Andrej730 86072079de draw_image_for_ifc_profile - optimizations using numpy 2025-03-04 18:10:42 +05:00
Andrej730 9440096106 Fix #6260 after 2f6ae17 2025-03-04 18:10:42 +05:00
Andrej730 649a913bbc add_profile_representation - add test 2025-03-04 18:10:42 +05:00
Andrej730 53a3afaded typing 2025-03-04 18:10:41 +05:00
Andrej730 33d3984990 add_profile_representation - reuse util.shape methods 2025-03-04 18:10:41 +05:00
Andrej730 5f86c6d1c2 shape_builder.create_axis2_placement_3d_from_matrix - small optimization
create_axis2_placement_3d now is fine with numpy arrays
2025-03-03 13:54:57 +05:00
Andrej730 10d5378ee3 TessellateElements - small optimization
tolist() supports conversion of multidimensional arrays too
2025-03-03 13:54:57 +05:00
Andrej730 7f27785927 docs fixes
1) material.add_profile_set doesn't exist
2) usecase.file is not defined, use `model` instead`
3) Specify material for material.assign_material
2025-03-03 13:54:57 +05:00
Andrej730 fed063a0c6 edit_profile_usage - add test 2025-03-03 13:54:56 +05:00
Andrej730 855062e36b Quick to select profile from BIM Tool in Profile UI
Example - https://imgur.com/a/tJdQUGq
2025-03-03 13:54:56 +05:00
Andrej730 7b74b6c68f Simplify edit_profile_usage with util.shape 2025-03-03 13:54:56 +05:00
Andrej730 a01278aa8d ifcopenshell.util.shape to return Python floats if return is just a scalar 2025-03-03 13:54:56 +05:00
Ryan Schultz 99c49736fa small tweak 2025-03-02 10:31:46 -06:00
Bruno Postle c1efdbb8ea black . 2025-03-02 11:37:52 +00:00
Bruno Postle e240ae2669 Fix bpy.ops.bim.select_by_material() for layersets
This operator assumes all material definitions have a Name attribute,
mostly they do except that Material Layer Sets have a LayerSetName
attribute for reasons.
2025-03-02 11:24:07 +00:00
Ryan Schultz 945099b817 Set IfcMaterialLayerSetUsage's DirectionSense/OffsetFromReferenceLine/ReferenceExtent on all selected objects 2025-03-01 18:04:41 -06:00
Bruno Perdigão f4b0fe6560 Polyline tool - Fix issue where Y and Z axis lock were inverted when in YZ plane. 2025-02-28 13:52:20 -03:00
Bruno Perdigão efc6842a9d Improve snapping for empty objects. 2025-02-28 13:44:00 -03:00
Bruno Perdigão 43931e951e Fix another issue (follows 2609a8293c) with walls created with obtuse x_angle. 2025-02-28 13:19:51 -03:00
Bruno Perdigão 134a480b03 Fix slab preview decorator when using obtuse x_angle. 2025-02-28 13:02:02 -03:00
Bruno Perdigão 0eb32ed7b7 Refactor ChangeExtrusionXAngle to support obtuse x_angle for slabs.
To support obtuse x_angle a refactoring had to be made, which helped
improve the general code for slab addition. This is challenging because
there are a few features that interact with each other to create slabs,
like `depth`, `direction_sense`, `offset` and `x_angle`. In addition,
these interactions can happen in different parts of the code. This
commit addresses `ChangeExtrusionXAngle` in `model/wall.py`.
See e8e88c25fdcef55826443fe6b000aec1baa6f25a for more information.
2025-02-28 13:02:02 -03:00
Bruno Perdigão 1855ac421c Refactor slab addition to support obtuse x_angle. See #5938
To support obtuse x_angle a refactoring had to be made, which helped
improve the general code for slab addition.
This is challenging because there are a few features that interact
with each other to create slabs, like `depth`, `direction_sense`, `offset`
and `x_angle`. In addition, these interactions can happen in different parts
of the code. This refactor improves the coherence between those different parts.
Files changed:
- `api/geometry/add_slab_representation.py`
- `model/slab.py` - inside the function `change_thickness()`
File to be changed in a following commit:
- `model/wall.py` - inside the operator `ChangeExtrusionXAngle` - To-do
2025-02-28 13:02:02 -03:00
Bruno Perdigão 7a69d6f20e Fix issue when updating wall x_angle with obtuse angle. See #5938 2025-02-28 13:02:02 -03:00
Bruno Perdigão 2609a8293c Fix issue where walls are created with obtuse x_angle. See #5938 2025-02-28 13:02:02 -03:00
Bruno Perdigão e68c47a897 Remove code related to 0104a2cc9f 2025-02-28 13:02:02 -03:00
Andrej730 06fa2b05d6 Fix #6245 after 17642ca 2025-02-28 18:31:14 +05:00
Andrej730 db52e70121 Fix possibility of adding new invalid surface styles
1) Add default values for new IfcSurfaceStyleLighting so they won't appear invalid.
2) Temporarily disable starting surface style with a texture style since it requires additional texture UI to be exposed or some default texture to be assigned to keep it valid.
2025-02-28 18:31:14 +05:00
Andrej730 c81fe97cc2 Fix bim.change_cardinal_point breaking if one of objects had no material 2025-02-28 18:31:13 +05:00
Andrej730 b9f4503206 Paramteric doors/windows - detect previously assigned materials when editing is enabled
Example - https://imgur.com/a/FEbSBaX
2025-02-28 18:31:13 +05:00
Andrej730 bd4dc501a2 Pamateric doors/windows - add material select shortcut to UI
Example - https://i.imgur.com/UfJTjGz.png
2025-02-28 18:31:13 +05:00
Andrej730 8d1255f0bc Fix toggling wireframe for odd case when there are no objects linked
Noticed investigating #6242
2025-02-28 18:31:13 +05:00
Andrej730 cc7abe6a8d Fix error applying blender offset to read-only matrix (e.g. during linking)
Example:
```
  File "\bonsai\bim\module\project\operator.py", line 1815, in execute
    self.process_occurrence(shape)
  File "\bonsai\bim\module\project\operator.py", line 1952, in process_occurrence
    obj.matrix_world = tool.Loader.apply_blender_offset_to_matrix_world(obj, mat)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\bonsai\tool\loader.py", line 952, in apply_blender_offset_to_matrix_world
    matrix[0][3] = offset_xyz[0]
    ~~~~~~~~~^^^
    ValueError: assignment destination is read-only
```

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

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

Fixes #6238.

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

Noticed in #6220
2025-02-25 18:28:48 +05:00
Andrej730 2f6ae1745f typing 2025-02-25 18:28:48 +05:00
Andrej730 5f4098e5d4 Update type_elements_filtered on changing ifc class
Needed after 1d815cdb67
2025-02-25 18:28:47 +05:00
Andrej730 b49cc6c5e9 AuthoringData.data["relating_type_data"]
Simplify lots of data keys to just one dictionary relating_type_data, so it will be easier to maintain it and update if needed.
2025-02-25 18:28:47 +05:00
Andrej730 dcc681ac74 Revert a428169 to avoid constant reload of cached data
The code was recalculating type_elements and relating_type_id every time Blender trying to check enum items.
Couldn't replicate issue when type duplicated, perhaps it's resolved some other way already.
2025-02-25 18:28:47 +05:00
Thomas Krijnen 240c78c12a Update IfcHierarchyHelper.h 2025-02-25 14:18:10 +01:00
Andrej730 6de409031e Fix #6222
Mentioned in #6223
2025-02-25 14:21:01 +05:00
Bruno Perdigão ea274588b0 See #6214. Fix how extrusion existing angle is calculated.
The issue happen when the layer has a negative direction sense.
2025-02-24 20:46:37 -03:00
Bruno Perdigão e46e1512fa Fix #6160 where axis snapping in polyline tool were not working consistently 2025-02-24 20:46:26 -03:00
593 changed files with 29888 additions and 10128 deletions
-20
View File
@@ -1,20 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
Replace this text describing what problem occurred and what you expected to happen instead.
1. To reproduce this, open file '...'
2. Click on '....'
3. See error
**Attachments**
If applicable, add screenshots to help explain your problem. Please also drag-drop any files necessary to show the error (rename the file extension from .ifc to .txt to upload). Private files can be uploaded to https://ifcopenshell.org/upload.html - only viewed by core developers and will be deleted afterwards.
**Debug information**
If this is in Bonsai, paste the output from the Copy Debug Information option in Bonsai. It can be found under Quality and Coordination -> Quality Control -> Debug. If this is a general software issue, if relevant include details about IfcOpenShell version, operating system, Python version, etc.
+23
View File
@@ -0,0 +1,23 @@
name: Bug Report
description: Crashes, error messages, and broken features
type: bug
body:
- type: textarea
attributes:
label: Bug Description
placeholder: |
Describe what problem occurred and what you expected to happen instead.
1. To reproduce this, open file '...'
2. Click on '....'
3. See error
- type: textarea
attributes:
label: Attachments
description: "Private files can be uploaded to https://ifcopenshell.org/upload.html - only viewed by core developers and will be deleted afterwards. Please submit your report first then upload private files afterwards."
placeholder: "If applicable, add screenshots to help explain your problem. Please also drag-drop any files necessary to show the error (rename the file extension from .ifc to .txt to upload)."
- type: textarea
attributes:
label: Debug and Error Output
description: "If this is in Bonsai, paste the output from the Copy Debug Information option in Bonsai. It can be found under Quality and Coordination -> Quality Control -> Debug. If this is a general software issue, if relevant include details about IfcOpenShell version, operating system, Python version, etc."
render: yes
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Community Forums
url: https://community.osarch.org/
about: Have a question? Want to discuss an idea? Try the OSArch forums instead.
- name: Live Chat
url: https://osarch.org/chat
about: Have a really confusing issue? Want real-time support?
-10
View File
@@ -1,10 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
Replace this text and describe a feature you'd like us to add. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable.
@@ -0,0 +1,8 @@
name: Feature Request
description: Suggest a new feature or improvement
type: Feature
body:
- type: textarea
attributes:
label: Feature Description
placeholder: "Describe a feature you'd like us to add, or a change to the user interface, design, or workflow for usability. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable."
+1 -1
View File
@@ -104,7 +104,7 @@ jobs:
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
# Download Blender.
wget -q -O blender.tar.xz https://ftp.nluug.nl/pub/graphics/blender/release/Blender4.3/blender-4.3.0-linux-x64.tar.xz
wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.4/blender-4.4.0-linux-x64.tar.xz
tar -xf blender.tar.xz
# Setup Blender.
+6 -6
View File
@@ -9,7 +9,7 @@ on:
jobs:
activate:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
if: |
github.repository == 'IfcOpenShell/IfcOpenShell' &&
!startsWith(github.event.head_commit.message, 'Release ') &&
@@ -18,7 +18,7 @@ jobs:
- run: echo ok go
build:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
needs: activate
steps:
- uses: actions/checkout@v2
@@ -35,7 +35,7 @@ jobs:
-
name: ccache
uses: hendrikmuhs/ccache-action@v1
uses: hendrikmuhs/ccache-action@v1.2
-
name: Build ifcopenshell
@@ -75,7 +75,7 @@ jobs:
make package
working-directory: build
- name: Upload
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
# Artifact name
name: ifcos-artifacts
@@ -83,7 +83,7 @@ jobs:
path: build/assets/Ifc*
deliver:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
needs: build
name: Docker Build, Tag, Push
@@ -118,6 +118,6 @@ jobs:
repository: aecgeeks/ifcopenshell
# Since the dispatch is set to `tag`, `github.ref_name` should evaluate to the pushed tag
# On a workflow dispatch, `ref_name` will take on the value from the dispatch payload
tags: aecgeeks/ifcopenshell:${{ github.ref_name }}${{ github.ref_name == github.event.repository.default_branch && ',aecgeeks/ifcopenshell:latest' }}
tags: aecgeeks/ifcopenshell:${{ github.ref_name }}${{ github.ref_name == github.event.repository.default_branch && ',aecgeeks/ifcopenshell:22.04' }}
file: ./Dockerfile
push: true
@@ -9,6 +9,8 @@ on:
paths:
- 'src/pyodide/**'
- '.github/workflows/publish-pyodide-demo-app.yml'
branches:
- v0.8.0
jobs:
activate:
+1
View File
@@ -4,6 +4,7 @@ on:
push:
tags:
- 'v[0-9].[0-9].[0-9]*'
workflow_dispatch:
jobs:
activate:
+1 -1
View File
@@ -91,7 +91,7 @@ src/bonsai/drawings
src/bonsai/layouts
# ifcopenshell swig and compiled files
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper.so
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper*.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
# apple
+2
View File
@@ -46,6 +46,7 @@ VERSION_MINOR:=$(shell cat '../../VERSION' | cut -d '.' -f 2)
VERSION_PATCH:=$(shell cat '../../VERSION' | cut -d '.' -f 3)
VERSION_DATE:=$(shell date '+%y%m%d')
LAST_COMMIT_HASH:=$(shell git rev-parse HEAD)
LAST_COMMIT_DATE:=$(shell git show -s --format=%cI)
PYVERSION:=py310
PYPI_IMP:=cp
@@ -254,6 +255,7 @@ ifeq ($(IS_STABLE), TRUE)
else
$(SED) "s/0.0.0/$(VERSION)-alpha$(VERSION_DATE)/" build/bonsai/blender_manifest.toml
$(SED) "s/8888888/$(LAST_COMMIT_HASH)/" build/bonsai/__init__.py
$(SED) "s/9999999/$(LAST_COMMIT_DATE)/" build/bonsai/__init__.py
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/pyproject.toml
endif
+9
View File
@@ -40,6 +40,7 @@ from typing import Union, Any, Generator
last_commit_hash = "8888888"
last_commit_date = "9999999"
def get_last_commit_hash() -> Union[str, None]:
@@ -51,6 +52,12 @@ def get_last_commit_hash() -> Union[str, None]:
return last_commit_hash[:7]
def get_last_commit_date() -> Union[str, None]:
if last_commit_date == str(9_999999):
return None
return last_commit_date
# Accessed from bonsai extension:
bbim_semver: dict[str, Any] = {}
@@ -96,6 +103,7 @@ def get_debug_info():
"blender_version": bpy.app.version_string,
"bonsai_version": bbim_version,
"bonsai_commit_hash": get_last_commit_hash(),
"bonsai_commit_date": get_last_commit_date(),
"last_actions": last_actions,
"last_error": last_error,
}
@@ -218,6 +226,7 @@ if IN_BLENDER:
path = Path(__file__).resolve().parent
repo = git.Repo(str(path), search_parent_directories=True)
last_commit_hash = repo.head.object.hexsha
last_commit_date = repo.head.object.committed_datetime.isoformat()
except:
pass
+7
View File
@@ -20,6 +20,7 @@ import os
import bpy
import bpy.utils.previews
import importlib
from bpy_extras.io_utils import ImportHelper, ExportHelper
from . import handler, ui, prop, operator
from typing import Callable, Union
@@ -83,6 +84,7 @@ modules = {
"covering": None,
"web": None,
"light": None,
"alignment": None,
# Uncomment this line to enable loading of the demo module. Happy hacking!
# The name "demo" must correlate to a folder name in `bim/module/`.
# "demo": None,
@@ -220,7 +222,12 @@ def on_register(scene):
def register():
for cls in classes:
# Prevent crashes in Blender 4.4.0, see #6420.
if issubclass(cls, (ImportHelper, ExportHelper)):
assert getattr(cls, "bl_description", "") or cls.__doc__, cls
bpy.utils.register_class(cls)
bpy.app.handlers.depsgraph_update_post.append(on_register)
bpy.app.handlers.undo_post.append(handler.undo_post)
bpy.app.handlers.redo_post.append(handler.redo_post)
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1');
FILE_NAME('/dev/null','2024-03-03T16:03:58+11:00',(),(),'IfcOpenShell v0.7.0-eaa2aa05a','IfcOpenShell v0.7.0-eaa2aa05a','Nobody');
FILE_NAME('/dev/null','2025-03-27T17:30:27+05:00',(''),(''),'IfcOpenShell 0.0.0','IfcOpenShell 0.0.0','Nobody');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
@@ -29,7 +29,7 @@ DATA;
#22=IFCMATERIALLAYERSET((#24),$,$);
#23=IFCRELASSOCIATESMATERIAL('0khP_Smdj12f2JrOTRbRKj',$,$,$,(#21),#22);
#24=IFCMATERIALLAYER(#20,0.05,$,$,$,$,$);
#25=IFCRELDECLARES('31JriXdev6BP2sC9lcxdC1',$,$,$,#2,(#26,#48,#72,#59,#92,#30,#96,#63,#21,#54,#87,#210,#153,#34,#67,#38,#82,#77,#42));
#25=IFCRELDECLARES('31JriXdev6BP2sC9lcxdC1',$,$,$,#2,(#59,#92,#38,#82,#30,#96,#77,#63,#42,#54,#87,#153,#34,#21,#67,#210,#26,#48,#72));
#26=IFCWALLTYPE('3R5onkTtX1IxdCMPoThRaw',$,'WAL100',$,$,$,$,$,$,.NOTDEFINED.);
#27=IFCMATERIALLAYERSET((#29),$,$);
#28=IFCRELASSOCIATESMATERIAL('0$mXRPCT16nP6Q70XFNvZv',$,$,$,(#26),#27);
File diff suppressed because one or more lines are too long
@@ -28,11 +28,13 @@ DATA;
#21=IFCSIMPLEPROPERTYTEMPLATE('1UDakJ5_f7kBhggNSW4$h5',$,'SymbolsPath','Default symbols SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#22=IFCSIMPLEPROPERTYTEMPLATE('0d53LEtgLDQxnv__NfgH7i',$,'PatternsPath','Default patterns SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#23=IFCSIMPLEPROPERTYTEMPLATE('26qFNMv7nCHgU6Jd7Anga5',$,'ShadingStylesPath','Default shading styles',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcTypeProduct',(#25,#26,#27,#28));
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcAnnotation/RADIUS,IfcAnnotation/DIAMETER,IfcTypeProduct',(#25,#26,#27,#28,#30));
#25=IFCSIMPLEPROPERTYTEMPLATE('1rL2AbQsXD8RbpoWH5pYOV',$,'ShowDescriptionOnly','Hide the measurement values and show only annotation description',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#26=IFCSIMPLEPROPERTYTEMPLATE('0SVyOfB0rC2xNfdRYf3XvY',$,'SuppressZeroInches','Suppress 0 inch values in dimension annotation text (for example: 12'' - 0" -> 12'')',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#27=IFCSIMPLEPROPERTYTEMPLATE('2bUmj458PBqPAtUoI3MXsb',$,'TextPrefix','Text to add before annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#28=IFCSIMPLEPROPERTYTEMPLATE('0bnzttUb9BPuN597uNTXOE',$,'TextSuffix','Text to add after annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#29=IFCSIMPLEPROPERTYTEMPLATE('2pJmUDpB50VBdCOib1zcJJ',$,'Newline_At','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#30=IFCSIMPLEPROPERTYTEMPLATE('2TJn72t_v2cvBUG916Dpev',$,'CustomUnit','Dimension''s custom unit',.P_ENUMERATEDVALUE.,'IfcText',$,#31,$,$,$,.READWRITE.);
#31=IFCPROPERTYENUMERATION('CustomUnit',(IFCTEXT('Feet and Inches - Fractional'),IFCTEXT('Feet - Decimal'),IFCTEXT('Inches - Fractional'),IFCTEXT('Inches - Decimal'),IFCTEXT('Meters'),IFCTEXT('Decimeters'),IFCTEXT('Centimeters'),IFCTEXT('Millimeters')),$);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1 @@
../../libraries/IFC2X3 Demo Library.ifc
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
../../libraries/IFC4 Demo Library.ifc
@@ -0,0 +1 @@
../../libraries/IFC4X3 Demo Library.ifc
+1 -31
View File
@@ -47,7 +47,6 @@ class IfcExporter:
self.set_header()
IfcStore.update_cache()
self.sync_all_objects()
self.sync_edited_objects()
tool.Project.save_linked_models_to_ifc()
extension = self.ifc_export_settings.output_file.split(".")[-1].lower()
if extension == "ifczip":
@@ -101,43 +100,14 @@ class IfcExporter:
result = self.sync_object_placement(obj)
if result:
results.append(result)
result = self.sync_object_material(obj)
# TODO: sync_object_material always returns None
# so it's never really appended
if result:
results.append(result)
except ReferenceError:
pass # The object is likely deleted
return results
def sync_edited_objects(self) -> list[ifcopenshell.entity_instance]:
results: list[ifcopenshell.entity_instance] = []
for obj in IfcStore.edited_objs.copy():
if not obj:
continue
if not tool.Blender.is_valid_data_block(obj):
continue
element = tool.Ifc.get_entity(obj)
if element:
results.append(element)
bpy.ops.bim.update_representation(obj=obj.name)
IfcStore.edited_objs.clear()
return results
def sync_object_material(self, obj: bpy.types.Object) -> None:
if not obj.data or not isinstance(obj.data, bpy.types.Mesh):
return
if not self.has_changed_materials(obj):
return
bpy.ops.bim.update_representation(obj=obj.name)
def has_changed_materials(self, obj: bpy.types.Object) -> bool:
mprops = tool.Geometry.get_mesh_props(obj.data)
checksum = mprops.material_checksum
return checksum != tool.Geometry.get_material_checksum(obj)
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
element = self.file.by_id(tool.Blender.get_object_bim_props(obj).ifc_definition_id)
if tool.Geometry.is_scaled(obj):
bpy.ops.bim.update_representation(obj=obj.name)
# update_representation might not apply scale if the object has openings
+14 -15
View File
@@ -24,17 +24,15 @@ import ifcopenshell.util.unit
import ifcopenshell.api.owner.settings
import bonsai.bim
import bonsai.tool as tool
import bonsai.core.owner as core_owner
from bpy.app.handlers import persistent
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.owner.prop import get_user_person, get_user_organisation
from bonsai.bim.module.model.data import AuthoringData
from bonsai.bim.module.aggregate.decorator import AggregateDecorator
from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator
from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator
from bonsai.bim.module.nest.decorator import NestDecorator
from mathutils import Vector
from math import cos, degrees
from math import cos
from typing import Union, Callable
@@ -55,23 +53,24 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
return
if isinstance(obj, bpy.types.Material):
props = obj.BIMStyleProperties
props = tool.Style.get_material_style_props(obj)
if ifc_definition_id := props.ifc_definition_id:
if props.is_renaming:
props.is_renmaing = False
props.is_renaming = False
return
tool.Ifc.get().by_id(ifc_definition_id).Name = obj.name
refresh_ui_data()
return
if not obj.BIMObjectProperties.ifc_definition_id:
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
return
if obj.BIMObjectProperties.is_renaming:
obj.BIMObjectProperties.is_renaming = False
if props.is_renaming:
props.is_renaming = False
return
element = tool.Ifc.get().by_id(obj.BIMObjectProperties.ifc_definition_id)
element = tool.Ifc.get().by_id(props.ifc_definition_id)
if "/" in obj.name:
object_name = obj.name
element_name = obj.name.split("/", 1)[1]
@@ -87,8 +86,8 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
if not element.is_a("IfcRoot"):
return
element.Name = element_name
if obj.BIMObjectProperties.collection:
obj.BIMObjectProperties.collection.name = object_name
if props.collection:
props.collection.name = object_name
refresh_ui_data()
@@ -288,7 +287,7 @@ def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None
def viewport_shading_changed_callback(area: bpy.types.Area) -> None:
shading = area.spaces.active.shading.type
if shading == "RENDERED":
bpy.context.scene.BIMStylesProperties.active_style_type = "External"
tool.Style.get_style_props().active_style_type = "External"
def subscribe_to_viewport_shading_changes():
@@ -348,8 +347,8 @@ def load_post(scene):
# Bonsai overlays
georeference_props = tool.Georeference.get_georeference_props()
aggregate_props = bpy.context.scene.BIMAggregateProperties
nest_props = bpy.context.scene.BIMNestProperties
aggregate_props = tool.Aggregate.get_aggregate_props()
nest_props = tool.Nest.get_nest_props()
model_props = tool.Model.get_model_props()
if georeference_props.should_visualise:
GeoreferenceDecorator.install(bpy.context)
@@ -362,7 +361,7 @@ def load_post(scene):
if model_props.show_slab_direction:
SlabDirectionDecorator.install(bpy.context)
if scene := bpy.context.scene:
if preferences.should_use_snap and (scene := bpy.context.scene):
# Snapping is off by default in Blender, but in BIM, it's more useful to be on
scene.tool_settings.use_snap = True
# Match default Bonsai snaps
+7 -3
View File
@@ -76,8 +76,8 @@ def draw_attribute(
elif value_name == "filepath_value":
attribute.filepath_value.layout_file_select(layout, filter_glob=attribute.filter_glob, text=attribute.name)
elif attribute.name in ("ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"):
propis = bpy.context.scene.BIMWorkScheduleProperties
for item in propis.durations_attributes:
props = tool.Sequence.get_work_schedule_props()
for item in props.durations_attributes:
if item.name == attribute.name:
duration_props = item
layout.label(text=attribute.name)
@@ -293,7 +293,10 @@ def prop_with_search(
should_click_ok: bool = False,
original_operator_path: Optional[str] = None,
**kwargs: Any,
):
) -> bpy.types.UILayout:
"""
:return: Added row.
"""
# kwargs are layout.prop arguments (text, icon, etc.)
row = layout.row(align=True)
row.prop(data, prop_name, **kwargs)
@@ -307,6 +310,7 @@ def prop_with_search(
op.original_operator_path = original_operator_path or ""
except TypeError: # Prop is not iterable
pass
return row
def get_enum_items(
+17 -9
View File
@@ -33,7 +33,7 @@ import bonsai.bim.handler
import bonsai.tool as tool
from pathlib import Path
from bonsai.tool.brick import BrickStore
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired, cast
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired
IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
@@ -106,10 +106,7 @@ class IfcStore:
def get_file():
if IfcStore.file is None:
props = tool.Blender.get_bim_props()
IfcStore.path = props.ifc_file
# Interpret relative paths as relative to .blend file.
if IfcStore.path and not os.path.isabs(IfcStore.path):
IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path))
IfcStore.set_path(props.ifc_file)
if IfcStore.path:
try:
IfcStore.load_file(IfcStore.path)
@@ -117,6 +114,13 @@ class IfcStore:
print(f"Failed to load file {IfcStore.path}. Error details: {e}")
return IfcStore.file
@staticmethod
def set_path(value):
IfcStore.path = value
# Interpret relative paths as relative to .blend file.
if IfcStore.path and not os.path.isabs(IfcStore.path):
IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path))
@staticmethod
def get_cache():
if IfcStore.cache is None and IfcStore.path:
@@ -265,9 +269,11 @@ class IfcStore:
IfcStore.guid_map[global_id] = obj
if element.is_a("IfcSurfaceStyle"):
obj.BIMStyleProperties.ifc_definition_id = element.id()
props = tool.Style.get_material_style_props(obj)
props.ifc_definition_id = element.id()
else:
obj.BIMObjectProperties.ifc_definition_id = element.id()
props = tool.Blender.get_object_bim_props(obj)
props.ifc_definition_id = element.id()
tool.Ifc.setup_listeners(obj)
@@ -405,9 +411,11 @@ class IfcStore:
@staticmethod
def purge_blender_ifc_data(obj: IFC_CONNECTED_TYPE) -> None:
if isinstance(obj, bpy.types.Material):
obj.BIMStyleProperties.ifc_definition_id = 0
props = tool.Style.get_material_style_props(obj)
props.ifc_definition_id = 0
else: # bpy.types.Object
obj.BIMObjectProperties.ifc_definition_id = 0
props = tool.Blender.get_object_bim_props(obj)
props.ifc_definition_id = 0
@staticmethod
def execute_ifc_operator(
+61 -26
View File
@@ -20,6 +20,7 @@ from __future__ import annotations
import bpy
import time
import json
import ifcpatch
import logging
import traceback
import mathutils
@@ -38,6 +39,7 @@ import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore, IFC_CONNECTED_TYPE
from bonsai.tool.loader import OBJECT_DATA_TYPE
from typing import Dict, Union, Optional, Any, Literal
from ifcopenshell.util.shape import MatrixType
class MaterialCreator:
@@ -85,7 +87,7 @@ class MaterialCreator:
def load_existing_materials(self) -> None:
for material in bpy.data.materials:
if ifc_definition_id := material.BIMStyleProperties.ifc_definition_id:
if ifc_definition_id := tool.Blender.get_ifc_definition_id(material):
self.styles[ifc_definition_id] = material
def parse_element_type_material_styles(self, element: ifcopenshell.entity_instance) -> None:
@@ -298,6 +300,20 @@ class IfcImporter:
bpy.context.window_manager.progress_end()
def process_context_filter(self) -> None:
contexts = self.file.by_type("IfcGeometricRepresentationContext")
if len(contexts) > 100: # Probably something strange happening. Encountered from Revizto.
print("Warning! Excessive contexts were found and merged where applicable.")
uniques = {}
i = 0
for element in contexts:
data = "-".join([str(a) for a in element])
if unique := uniques.get(data, None):
ifcopenshell.util.element.replace_element(element, unique)
self.file.remove(element)
i += 1
else:
uniques[data] = element
print(f"Replaced {i} IfcGeometricRepresentationContext")
tool.Loader.settings.contexts = ifcopenshell.util.representation.get_prioritised_contexts(self.file)
tool.Loader.settings.context_settings = tool.Loader.create_settings()
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
@@ -479,14 +495,16 @@ class IfcImporter:
if grid.WAxes:
self.create_grid_axes(grid.WAxes, grid_obj, grid_placement)
def create_grid_axes(self, axes, grid_obj, grid_placement):
def create_grid_axes(
self, axes: list[ifcopenshell.entity_instance], grid_obj: bpy.types.Object, grid_placement: MatrixType
) -> None:
for axis in axes:
shape = tool.Loader.create_generic_shape(axis.AxisCurve)
mesh = self.create_mesh(axis, shape)
obj = bpy.data.objects.new(tool.Loader.get_name(axis), mesh)
obj.show_in_front = True
self.link_element(axis, obj)
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, grid_placement.copy()))
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, grid_placement))
def create_element_types(self):
for element_type in self.element_types:
@@ -511,8 +529,9 @@ class IfcImporter:
pass
elif shape:
mesh = self.create_mesh(element, shape)
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
if mesh is not None:
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
else:
self.ifc_import_settings.logger.error("Failed to generate shape for %s", element)
break
@@ -698,9 +717,15 @@ class IfcImporter:
def create_structural_point_connections(self):
for product in self.file.by_type("IfcStructuralPointConnection"):
# TODO: make this based off ifcopenshell. See #1409
representation: ifcopenshell.entity_instance = next(
rep for rep in product.Representation.Representations if rep.RepresentationType == "Vertex"
)
representation = tool.Structural.get_vertex_representation(product)
if not representation:
print(
"WARNING. Skipping invalid IfcStructuralPointConnection - "
f"element has no valid representation:\n{product}."
)
continue
mesh = tool.Loader.create_structural_point_connection_mesh(representation)
if mesh is None:
continue
@@ -742,12 +767,9 @@ class IfcImporter:
def create_pointclouds(self, products: set[ifcopenshell.entity_instance]) -> set[ifcopenshell.entity_instance]:
result = set()
for product in products:
representation = self.get_pointcloud_representation(product)
if representation is not None:
pointcloud = self.create_pointcloud(product, representation)
if pointcloud is not None:
if representation := self.get_pointcloud_representation(product):
if pointcloud := self.create_pointcloud(product, representation):
result.add(pointcloud)
return result
def create_pointcloud(
@@ -795,8 +817,9 @@ class IfcImporter:
materials_updated = bool(mesh)
if mesh is None:
mesh = self.create_mesh(element, shape)
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
if mesh is not None:
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
else:
mesh = None
@@ -807,11 +830,10 @@ class IfcImporter:
if element.is_a(ifcclass):
obj.display_type = "WIRE"
if shape:
if shape and mesh:
# We use numpy here because Blender mathutils.Matrix is not accurate enough
mat = np.array(shape.transformation.matrix).reshape((4, 4), order="F")
mat = ifcopenshell.util.shape.get_shape_matrix(shape)
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, mat))
assert mesh # Type checker.
if not materials_updated:
self.material_creator.create(element, obj, mesh, tool.Geometry.does_shape_has_openings(shape))
elif mesh: # When does this occur?
@@ -933,7 +955,8 @@ class IfcImporter:
self.project = {"ifc": project}
obj = tool.Ifc.get_object(project)
if obj:
self.project["blender"] = obj.BIMObjectProperties.collection
props = tool.Blender.get_object_bim_props(obj)
self.project["blender"] = props.collection
self.has_existing_project = True
return
self.project["blender"] = bpy.data.collections.new(
@@ -943,10 +966,16 @@ class IfcImporter:
obj.hide_select = True
self.project["blender"].objects.link(obj)
self.project["blender"].BIMCollectionProperties.obj = obj
obj.BIMObjectProperties.collection = self.collections[project.GlobalId] = self.project["blender"]
props = tool.Blender.get_object_bim_props(obj)
props.collection = self.collections[project.GlobalId] = self.project["blender"]
def create_styles(self) -> None:
for style in self.file.by_type("IfcSurfaceStyle"):
styles = self.file.by_type("IfcSurfaceStyle")
if len(styles) > self.ifc_import_settings.style_limit: # Probably something strange happening
print("Warning! Excessive styles were found and merged where applicable.")
ifcpatch.execute({"file": self.file, "recipe": "MergeStyles", "arguments": []})
styles = self.file.by_type("IfcSurfaceStyle")
for style in styles:
self.create_style(style)
def create_style(self, style: ifcopenshell.entity_instance) -> None:
@@ -959,10 +988,11 @@ class IfcImporter:
self.material_creator.styles[style.id()] = blender_material
style_elements = tool.Style.get_style_elements(blender_material)
props = tool.Style.get_material_style_props(blender_material)
if tool.Style.has_blender_external_style(style_elements):
blender_material.BIMStyleProperties.active_style_type = "External"
props.active_style_type = "External"
else:
blender_material.BIMStyleProperties.active_style_type = "Shading"
props.active_style_type = "Shading"
def place_objects_in_collections(self) -> None:
for ifc_definition_id, obj in self.added_data.items():
@@ -1027,7 +1057,7 @@ class IfcImporter:
element: ifcopenshell.entity_instance,
shape: Union[ifcopenshell.geom.ShapeElementType, ifcopenshell.geom.ShapeType],
cartesian_point_offset: Union[npt.NDArray[np.float64], Literal[False]] = None,
) -> bpy.types.Mesh:
) -> Union[bpy.types.Mesh, None]:
try:
if hasattr(shape, "geometry"):
# shape is ShapeElementType
@@ -1068,7 +1098,10 @@ class IfcImporter:
verts=verts,
load_indexed_maps=self.ifc_import_settings.load_indexed_maps,
)
return tool.Loader.slice_layerset_mesh(element, mesh)
# E.g. `create_mesh` is also used for IfcRelSpaceBoundary.
if element.is_a("IfcObjectDefinition"):
return tool.Loader.slice_layerset_mesh(element, mesh)
return mesh
except:
self.ifc_import_settings.logger.error("Could not create mesh for %s", element)
import traceback
@@ -1125,6 +1158,7 @@ class IfcImportSettings:
self.deflection_tolerance = 0.001
self.angular_tolerance = 0.5
self.void_limit = 30
self.style_limit = 300
# Locations greater than 1km are not considered "small sites" according to the georeferencing guide
# Users can configure this if they have to handle larger sites but beware of surveying precision
self.distance_limit = 1000
@@ -1145,7 +1179,7 @@ class IfcImportSettings:
@staticmethod
def factory(context=None, input_file=None, logger=None):
scene_diff = bpy.context.scene.DiffProperties
scene_diff = tool.Blender.get_diff_props()
props = tool.Project.get_project_props()
settings = IfcImportSettings()
settings.input_file = input_file
@@ -1162,6 +1196,7 @@ class IfcImportSettings:
settings.deflection_tolerance = props.deflection_tolerance
settings.angular_tolerance = props.angular_tolerance
settings.void_limit = props.void_limit
settings.style_limit = props.style_limit
settings.distance_limit = props.distance_limit
settings.false_origin_mode = props.false_origin_mode
try:
@@ -20,6 +20,7 @@ import blf
import bpy
import gpu
import ifcopenshell
import ifcopenshell.util.element
import bonsai.tool as tool
from bpy.types import SpaceView3D
from bpy_extras import view3d_utils
@@ -131,6 +132,9 @@ class AggregateDecorator:
return shader
def draw_custom_batch(self, coords, color):
indices = None
if not tool.Blender.validate_shader_batch_data(coords, indices):
return
shader = self.dotted_line_shader()
arc_lengths = [0]
@@ -141,6 +145,7 @@ class AggregateDecorator:
shader,
"LINE_STRIP",
{"position": coords, "arcLength": arc_lengths},
indices=indices,
)
matrix = bpy.context.region_data.perspective_matrix
@@ -150,13 +155,16 @@ class AggregateDecorator:
batch.draw(shader)
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def draw_aggregate(self, context):
if context.scene.BIMAggregateProperties.in_aggregate_mode:
props = tool.Aggregate.get_aggregate_props()
if props.in_aggregate_mode:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
decorator_color_special = self.addon_prefs.decorator_color_special
@@ -204,7 +212,7 @@ class AggregateDecorator:
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
if context.scene.BIMAggregateProperties.in_aggregate_mode:
if props.in_aggregate_mode:
return
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate))
parts_objs = [tool.Ifc.get_object(p) for p in parts]
@@ -243,6 +251,8 @@ class AggregateModeDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
@@ -253,7 +263,7 @@ class AggregateModeDecorator:
return
region = context.region
rv3d = region.data
props = context.scene.BIMAggregateProperties
props = tool.Aggregate.get_aggregate_props()
aggregate_obj = props.editing_aggregate
if not aggregate_obj:
@@ -284,7 +294,7 @@ class AggregateModeDecorator:
def draw_aggregate_empty(self, context):
if context.mode == "EDIT_MESH":
return
props = context.scene.BIMAggregateProperties
props = tool.Aggregate.get_aggregate_props()
aggregate_obj = props.editing_aggregate
if not aggregate_obj:
return
@@ -23,6 +23,7 @@ import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.core.aggregate as core
import bonsai.core.spatial
from typing import TYPE_CHECKING
class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
@@ -30,23 +31,30 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Assign Object To Aggregation"
bl_description = (
"Assign object as an aggregate to the selected IFC elements.\n\n"
"If called from Bonsai UI, then either 'Relating Whole' or 'Related Part' must be provided.\n"
"If called from Object Aggregates UI, then either 'Relating Whole' or 'Related Part' must be provided.\n"
"If called directly, active object will be considered a relating whole"
)
bl_options = {"REGISTER", "UNDO"}
relating_object: bpy.props.IntProperty()
related_object: bpy.props.IntProperty()
if TYPE_CHECKING:
relating_object: int
related_object: int
def _execute(self, context):
relating_obj = None
if self.relating_object:
relating_obj = tool.Ifc.get_object(tool.Ifc.get().by_id(self.relating_object))
assert relating_obj
elif self.related_object:
aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get().by_id(self.related_object))
if aggregate:
relating_obj = tool.Ifc.get_object(aggregate)
elif context.active_object:
assert relating_obj
else:
relating_obj = context.active_object
if not relating_obj:
self.report({"ERROR"}, "No relating object is provided.")
return
@@ -66,7 +74,7 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
relating_obj=relating_obj,
related_obj=obj,
)
props = context.scene.BIMAggregateProperties
props = tool.Aggregate.get_aggregate_props()
if relating_obj == props.editing_aggregate and props.in_aggregate_mode:
new_editing_obj = props.editing_objects.add()
new_editing_obj.obj = obj
@@ -182,7 +190,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
)
core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj)
def create_aggregate(self, context, ifc_class, aggregate_name):
def create_aggregate(self, context: bpy.types.Context, ifc_class: str, aggregate_name: str) -> bpy.types.Object:
aggregate = bpy.data.objects.new(aggregate_name, None)
aggregate.location = context.scene.cursor.location
bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class=ifc_class)
@@ -388,7 +396,7 @@ class BIM_OT_toggle_aggregate_mode_local_view(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BIMAggregateProperties
props = tool.Aggregate.get_aggregate_props()
objs = [o.obj for o in props.editing_objects]
if props.in_aggregate_mode:
if context.space_data.local_view:
@@ -33,6 +33,7 @@ from bpy.props import (
CollectionProperty,
)
from bonsai.bim.module.aggregate.decorator import AggregateDecorator, AggregateModeDecorator
from typing import TYPE_CHECKING, Union
def can_aggregate(relating_obj: bpy.types.Object, related_obj: bpy.types.Object) -> bool:
@@ -98,10 +99,21 @@ class BIMObjectAggregateProperties(PropertyGroup):
poll=poll_related_object,
)
if TYPE_CHECKING:
is_editing: bool
relating_object: Union[bpy.types.Object, None]
related_object: Union[bpy.types.Object, None]
class Objects(bpy.types.PropertyGroup):
obj: PointerProperty(type=bpy.types.Object)
previous_display_type: bpy.props.StringProperty(default="TEXTURED")
previous_hide_select: bpy.props.BoolProperty(default=False)
if TYPE_CHECKING:
obj: Union[bpy.types.Object, None]
previous_display_type: str
previous_hide_select: bool
class BIMAggregateProperties(PropertyGroup):
@@ -114,3 +126,10 @@ class BIMAggregateProperties(PropertyGroup):
default=False,
update=update_aggregate_decorator,
)
if TYPE_CHECKING:
in_aggregate_mode: bool
editing_aggregate: Union[bpy.types.Object, None]
editing_objects: bpy.types.bpy_prop_collection_idprop[Objects]
not_editing_objects: bpy.types.bpy_prop_collection_idprop[Objects]
aggregate_decorator: bool
+12 -11
View File
@@ -34,9 +34,9 @@ class BIM_PT_aggregate(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
props = context.active_object.BIMObjectProperties
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
return False
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
@@ -49,11 +49,13 @@ class BIM_PT_aggregate(Panel):
layout = self.layout
row = layout.row()
row.label(text="Aggregate Decorator")
row.prop(context.scene.BIMAggregateProperties, "aggregate_decorator", icon="HIDE_OFF", text="")
props = tool.Aggregate.get_aggregate_props()
row.prop(props, "aggregate_decorator", icon="HIDE_OFF", text="")
if not AggregateData.is_loaded:
AggregateData.load()
props = context.active_object.BIMObjectAggregateProperties
assert (obj := context.active_object)
props = tool.Aggregate.get_object_aggregate_props(obj)
if props.is_editing:
row = layout.row()
@@ -66,9 +68,9 @@ class BIM_PT_aggregate(Panel):
col.enabled = False
op = col.operator("bim.aggregate_assign_object", icon="CHECKMARK")
if props.relating_object:
op.relating_object = props.relating_object.BIMObjectProperties.ifc_definition_id
op.relating_object = tool.Blender.get_object_bim_props(props.relating_object).ifc_definition_id
elif props.related_object:
op.related_object = props.related_object.BIMObjectProperties.ifc_definition_id
op.related_object = tool.Blender.get_object_bim_props(props.related_object).ifc_definition_id
row.operator("bim.disable_editing_aggregate", icon="CANCEL", text="")
return
else:
@@ -115,9 +117,9 @@ class BIM_PT_linked_aggregate(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
props = context.active_object.BIMObjectProperties
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
return False
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
@@ -131,9 +133,8 @@ class BIM_PT_linked_aggregate(Panel):
if not AggregateData.is_loaded:
AggregateData.load()
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMObjectAggregateProperties
assert (obj := context.active_object)
assert (element := tool.Ifc.get_entity(obj))
row = layout.row(align=True)
if element.Decomposes:
@@ -0,0 +1,36 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
# from . import ui, prop, operator
from . import operator
classes = (operator.ImportAlignmentCSV,)
def menu_func_import(self, context):
self.layout.operator(operator.ImportAlignmentCSV.bl_idname, text="Alignment (.csv)")
def register():
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
def unregister():
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
@@ -0,0 +1,114 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>, 2022 Yassine Oualid <yassine@sigmadimensions.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
# pyright: reportUnnecessaryTypeIgnoreComment=error
import os
import ifcopenshell.api.alignment
import ifcopenshell.api.alignment.add_stationing_to_alignment
import bpy
import json
import time
import calendar
import isodate
import bonsai.core.sequence as core
import bonsai.tool as tool
import bonsai.bim.module.sequence.helper as helper
import ifcopenshell.api.spatial
import ifcopenshell.geom
import ifcopenshell.util.sequence
import ifcopenshell.util.selector
from datetime import datetime
from dateutil import parser, relativedelta
from bpy_extras.io_utils import ImportHelper
from typing import get_args, TYPE_CHECKING
from typing_extensions import assert_never
class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.import_alignment_csv"
bl_label = "Import Alignment CSV"
bl_description = " Import alignment from the provided .csv file."
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
@classmethod
def poll(cls, context):
ifc_file = tool.Ifc.get()
if ifc_file is None:
cls.poll_message_set("No IFC file is loaded.")
return False
elif ifc_file.schema != "IFC4X3":
cls.poll_message_set("Schema must be IFC4x3.")
return False
return True
def _execute(self, context):
self.file = tool.Ifc.get()
start = time.time()
alignment = ifcopenshell.api.alignment.create_alignment_from_csv(self.file, self.filepath)
ifcopenshell.api.alignment.create_geometric_representation(self.file, alignment)
ifcopenshell.api.alignment.add_stationing_to_alignment(self.file, alignment=alignment, start_station=0.0)
# IFC 4.1.5.1 alignments cannot be contained in spatial structures, but can be referenced into them
sites = self.file.by_type("IfcSite")
for site in sites:
ifcopenshell.api.spatial.reference_structure(self.file, products=[alignment], relating_structure=site)
# process the generated IfcReferent for the alignment
for rel in alignment.IsNestedBy:
for referent in rel.RelatedObjects:
if referent.is_a("IfcReferent"):
referent_obj = bpy.data.objects.new(tool.Loader.get_name(referent), None)
tool.Geometry.link(referent, referent_obj)
tool.Collector.assign(referent_obj, should_clean_users_collection=False)
# an alignment can be an aggregation of multiple child alignments (ie. multiple verticals for a single horizontal)
# get all the alignment curves
curves = []
for rel in alignment.IsDecomposedBy:
for agg in rel.RelatedObjects:
if agg.is_a("IfcAlignment"):
curves.append(ifcopenshell.api.alignment.get_curve(agg)) # 3D curve
# if there aren't any curves from aggregation, then there is only a single vertical or no vertical
if len(curves) == 0:
curves.append(ifcopenshell.api.alignment.get_curve(alignment))
settings = ifcopenshell.geom.settings()
for curve in curves:
shape = ifcopenshell.geom.create_shape(settings, curve)
# create a new Blender mesh
mesh_name = tool.Loader.get_mesh_name_from_shape(shape)
mesh = bpy.data.meshes.new(mesh_name)
m = tool.Loader.convert_geometry_to_mesh(shape, mesh)
# create a new Blender object
alignment_obj = bpy.data.objects.new(tool.Loader.get_name(alignment), m)
# link the blender object to with the alignment element
tool.Geometry.link(alignment, alignment_obj)
# assign the object to the blender collections
tool.Collector.assign(alignment_obj, should_clean_users_collection=False)
self.report({"INFO"}, "Imported in %s seconds" % (time.time() - start))
@@ -26,30 +26,42 @@ import bonsai.bim.helper
import bonsai.tool as tool
import bonsai.core.attribute as core
import bonsai.core.spatial
from typing import TYPE_CHECKING
def get_objs_for_operation(operator_properties, context):
def get_objs_for_operation(
operator_properties: "AttributesOperator", context: bpy.types.Context
) -> list[bpy.types.Object]:
if operator_properties.obj:
return [bpy.data.objects[operator_properties.obj]]
if operator_properties.mass_operation:
return context.selected_objects[:]
return [context.active_object]
obj = context.active_object
assert obj
return [obj]
class EnableEditingAttributes(bpy.types.Operator):
bl_idname = "bim.enable_editing_attributes"
bl_label = "Enable Editing Attributes"
bl_description = "ALT + Left Click to enable editing attributes on all selected objects"
bl_options = {"REGISTER", "UNDO"}
class AttributesOperator:
obj: bpy.props.StringProperty(options={"SKIP_SAVE"})
mass_operation: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
if TYPE_CHECKING:
obj: str
mass_operation: bool
def invoke(self, context, event):
self.mass_operation = event.alt
return self.execute(context)
def enable_editing_attribute_on_obj(self, obj):
props = obj.BIMAttributeProperties
class EnableEditingAttributes(bpy.types.Operator, AttributesOperator):
bl_idname = "bim.enable_editing_attributes"
bl_label = "Enable Editing Attributes"
bl_description = "ALT + Left Click to enable editing attributes on all selected objects"
bl_options = {"REGISTER", "UNDO"}
def enable_editing_attribute_on_obj(self, obj: bpy.types.Object) -> None:
props = tool.Blender.get_object_attribute_props(obj)
props.attributes.clear()
element = tool.Ifc.get_entity(obj)
@@ -87,20 +99,14 @@ class EnableEditingAttributes(bpy.types.Operator):
return {"FINISHED"}
class DisableEditingAttributes(bpy.types.Operator):
class DisableEditingAttributes(bpy.types.Operator, AttributesOperator):
bl_idname = "bim.disable_editing_attributes"
bl_label = "Disable Editing Attributes"
bl_description = "ALT + Left Click to disable editing attributes on all selected objects"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty(options={"SKIP_SAVE"})
mass_operation: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
self.mass_operation = event.alt
return self.execute(context)
def disable_editing_attributes_on_obj(self, obj):
props = obj.BIMAttributeProperties
def disable_editing_attributes_on_obj(self, obj: bpy.types.Object) -> None:
props = tool.Blender.get_object_attribute_props(obj)
props.is_editing_attributes = False
def execute(self, context):
@@ -118,7 +124,7 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.file = tool.Ifc.get()
obj = tool.Blender.get_active_object(is_selected=False)
if not (element := tool.Ifc.get_entity(obj)):
if not obj or not (element := tool.Ifc.get_entity(obj)):
return
def callback(attributes, prop):
@@ -130,7 +136,7 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
attributes[prop.name] = None
return True
props = obj.BIMAttributeProperties
props = tool.Blender.get_object_attribute_props(obj)
attributes = bonsai.bim.helper.export_attributes(props.attributes, callback=callback)
ifcopenshell.api.attribute.edit_attributes(self.file, product=element, attributes=attributes)
@@ -165,13 +171,12 @@ class GenerateGlobalId(bpy.types.Operator, tool.Ifc.Operator):
element.GlobalId = ifcopenshell.guid.new()
obj = context.active_object
if not obj or not obj.BIMAttributeProperties.is_editing_attributes:
if not obj or not (props := tool.Blender.get_object_attribute_props(obj)).is_editing_attributes:
return {"FINISHED"}
props = obj.BIMAttributeProperties
element = tool.Ifc.get_entity(obj)
if not element.is_a("IfcRoot"):
if not element or not element.is_a("IfcRoot"):
return {"FINISHED"}
if self.use_selected and obj in context.selected_objects:
@@ -191,7 +196,10 @@ class CopyAttributeToSelection(bpy.types.Operator, tool.Ifc.Operator):
name: bpy.props.StringProperty()
def _execute(self, context):
value = tool.Blender.get_active_object().BIMAttributeProperties.attributes.get(self.name).get_value()
obj = tool.Blender.get_active_object()
assert obj
props = tool.Blender.get_object_attribute_props(obj)
value = props.attributes[self.name].get_value()
total = core.copy_attribute_to_selection(
tool.Ifc, tool.Blender, tool.Root, tool.Spatial, name=self.name, value=value
)
@@ -29,8 +29,13 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING
class BIMAttributeProperties(PropertyGroup):
attributes: CollectionProperty(name="Attributes", type=Attribute)
is_editing_attributes: BoolProperty(name="Is Editing Attributes")
if TYPE_CHECKING:
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
is_editing_attributes: bool
+4 -2
View File
@@ -17,14 +17,16 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bonsai.bim.helper
import bpy.types
from bpy.types import Panel
from bonsai.bim.module.attribute.data import AttributesData
import bonsai.tool as tool
def draw_ui(context, layout, attributes):
def draw_ui(context: bpy.types.Context, layout: bpy.types.UILayout, attributes) -> None:
obj = context.active_object
props = obj.BIMAttributeProperties
assert obj
props = tool.Blender.get_object_attribute_props(obj)
if props.is_editing_attributes:
row = layout.row(align=True)
+15 -26
View File
@@ -42,6 +42,7 @@ import ifcopenshell.util.unit
import bonsai.tool as tool
import bonsai.bim.module.bcf.prop as bcf_prop
import bonsai.bim.module.bcf.bcfstore as bcfstore
from bpy_extras.io_utils import ImportHelper, ExportHelper
from pathlib import Path
from math import radians, degrees, atan, tan, cos, sin
from mathutils import Vector, Matrix, Euler, geometry
@@ -63,12 +64,14 @@ class NewBcfProject(bpy.types.Operator):
return {"FINISHED"}
class LoadBcfProject(bpy.types.Operator):
class LoadBcfProject(bpy.types.Operator, ImportHelper):
bl_idname = "bim.load_bcf_project"
bl_label = "Load BCF Project"
bl_description = "Load the BCF file."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"})
filename_ext = ".bcf"
def execute(self, context):
# Operator is also used when new project is created by not yet saved.
@@ -108,10 +111,6 @@ class LoadBcfProject(bpy.types.Operator):
self.report({"INFO"}, f"BCF Project '{Path(self.filepath).name}' is loaded.")
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class UnloadBcfProject(bpy.types.Operator):
bl_idname = "bim.unload_bcf_project"
@@ -341,13 +340,15 @@ class EditBcfTopic(bpy.types.Operator):
return {"FINISHED"}
class SaveBcfProject(bpy.types.Operator):
class SaveBcfProject(bpy.types.Operator, ExportHelper):
bl_idname = "bim.save_bcf_project"
bl_label = "Save BCF Project"
bl_description = "Save active BCF project by the provided filepath."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"})
save_current_bcf: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
filename_ext = ".bcf"
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
@@ -364,8 +365,7 @@ class SaveBcfProject(bpy.types.Operator):
self.filepath = str(path)
return self.execute(context)
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
return ExportHelper.invoke(self, context, event)
class AddBcfTopic(bpy.types.Operator):
@@ -1534,12 +1534,13 @@ class OpenBcfReferenceLink(bpy.types.Operator):
return {"FINISHED"}
class SelectBcfHeaderFile(bpy.types.Operator):
class SelectBcfHeaderFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_bcf_header_file"
bl_label = "Select BCF Header File"
bl_description = "Select filepath for BCF header reference."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"})
filename_ext = ".ifc"
def execute(self, context):
if self.filepath:
@@ -1547,16 +1548,12 @@ class SelectBcfHeaderFile(bpy.types.Operator):
props.file_reference = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectBcfBimSnippetReference(bpy.types.Operator):
class SelectBcfBimSnippetReference(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_bcf_bim_snippet_reference"
bl_label = "Select BCF BIM Snippet Reference"
bl_description = "Select filepath for BCF snippet reference."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
if self.filepath:
@@ -1564,16 +1561,12 @@ class SelectBcfBimSnippetReference(bpy.types.Operator):
props.bim_snippet_reference = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectBcfDocumentReference(bpy.types.Operator):
class SelectBcfDocumentReference(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_bcf_document_reference"
bl_label = "Select BCF Document Reference"
bl_description = "Select filepath for BCF document reference."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
if self.filepath:
@@ -1581,10 +1574,6 @@ class SelectBcfDocumentReference(bpy.types.Operator):
props.document_reference = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class LoadBcfHeaderIfcFile(bpy.types.Operator):
bl_idname = "bim.load_bcf_header_ifc_file"
@@ -43,6 +43,8 @@ class BoundaryDecorator:
cls.installed = None
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
+120 -66
View File
@@ -26,6 +26,7 @@ import numpy as np
import multiprocessing
import ifcopenshell.api
import ifcopenshell.api.boundary
import ifcopenshell.api.root
import ifcopenshell.geom
import ifcopenshell.util.unit
import ifcopenshell.util.shape
@@ -41,6 +42,7 @@ from bonsai.bim.module.model.decorator import ProfileDecorator
from bonsai.bim.module.boundary.decorator import BoundaryDecorator
import bonsai.core
import bonsai.core.geometry
from typing import Union, Optional
def disable_editing_boundary_geometry(context):
@@ -58,7 +60,7 @@ def disable_editing_boundary_geometry(context):
class Loader:
def __init__(self, operator=None):
def __init__(self, operator: Optional[bpy.types.Operator] = None):
self.operator = operator
self.ifc_file = None
self.logger = None
@@ -67,17 +69,20 @@ class Loader:
self.fallback_settings = self.load_fallback_settings()
self.load_importer()
def create_mesh(self, boundary):
def create_mesh(self, boundary: ifcopenshell.entity_instance) -> Union[bpy.types.Mesh, None]:
# ConnectionGeometry is optional in IFC schema for some reasons.
if not boundary.ConnectionGeometry:
return None
assert self.ifc_importer
assert self.operator
surface = boundary.ConnectionGeometry.SurfaceOnRelatingElement
# Workaround for invalid geometry provided by Revit. See https://github.com/Autodesk/revit-ifc/issues/270
if surface.is_a("IfcCurveBoundedPlane") and not getattr(surface, "InnerBoundaries", None):
surface.InnerBoundaries = ()
try:
shape = ifcopenshell.geom.create_shape(self.settings, surface)
mesh = self.ifc_importer.create_mesh(None, shape)
mesh = self.ifc_importer.create_mesh(boundary, shape)
assert mesh
tool.Loader.link_mesh(shape, mesh)
except RuntimeError:
# Fallback solution for invalid geometry provided by Revit. (InnerBoundaries cuting OuterBoundary)
@@ -97,7 +102,8 @@ class Loader:
return None
mesh = bpy.data.meshes.new(str(surface.id()))
bm = bmesh.new()
verts = [bm.verts.new(shape.verts[i : i + 3]) for i in range(0, len(shape.verts), 3)]
coords = ifcopenshell.util.shape.get_vertices(shape)
verts = [bm.verts.new(coord) for coord in coords]
bm.faces.new(verts)
for inner_boundary in surface.InnerBoundaries:
try:
@@ -108,7 +114,9 @@ class Loader:
f"Skipping an inner boundary for IfcRelSpaceBoundary with ID {boundary.id()}. Geometry might be invalid",
)
return None
verts = [bm.verts.new(shape.verts[i : i + 3]) for i in range(0, len(shape.verts), 3)]
coords = ifcopenshell.util.shape.get_vertices(shape)
verts = [bm.verts.new(coord) for coord in coords]
bm.faces.new(verts)
for i in range(len(verts) - 1):
bm.edges.new(verts[i : i + 2])
bm.edges.new((verts[-1], verts[0]))
@@ -131,7 +139,7 @@ class Loader:
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
return settings
def load_importer(self):
def load_importer(self) -> None:
self.ifc_file = tool.Ifc.get()
self.logger = logging.getLogger("ImportIFC")
ifc_import_settings = import_ifc.IfcImportSettings.factory(bpy.context, IfcStore.path, self.logger)
@@ -280,7 +288,7 @@ class SelectProjectBoundaries(bpy.types.Operator):
return {"FINISHED"}
def get_colour(ifc_boundary):
def get_colour(ifc_boundary: ifcopenshell.entity_instance) -> tuple[float, float, float, float]:
"""Return a color depending on IfcClass given"""
product_colors = {
"IfcWall": (0.7, 0.3, 0, 1),
@@ -316,7 +324,8 @@ class ColourByRelatedBuildingElement(bpy.types.Operator):
def _execute(self, context):
for obj in context.visible_objects:
if not obj.BIMObjectProperties.ifc_definition_id:
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
continue
element = tool.Ifc.get_entity(obj)
if not element.is_a("IfcRelSpaceBoundary"):
@@ -583,47 +592,70 @@ class DecorateBoundaries(bpy.types.Operator, tool.Ifc.Operator):
class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_boundary"
bl_label = "Add Boundary"
bl_description = (
"There are two options for this operator:\n"
"- just 1 IfcSpace is selected - generate a boundary based on "
"potential nearby (0.1m) boundary elements.\n"
"- 1 IfcSpace and 1 IfcElement are selected - generate a boundary "
"for IfcSpace and use selected IfcElement as boundary's building element."
)
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
relating_space = None
related_building_element = None
relating_space_obj = None
related_building_element_obj = None
parent_boundaries = []
parent_boundaries: list[ifcopenshell.entity_instance] = []
objs = context.selected_objects
objs = tool.Blender.get_selected_objects()
ifc_objects = {element: obj for obj in objs if (element := tool.Ifc.get_entity(obj))}
if len(ifc_objects) not in (1, 2):
self.report({"ERROR"}, "Please select one or two IFC elements.")
return {"CANCELLED"}
if not any((element := tool.Ifc.get_entity(obj)) and element.is_a("IfcSpace") for obj in objs):
self.report({"INFO"}, "No IfcSpace elements selected.")
return {"FINISHED"}
if not any(element.is_a("IfcSpace") for element in ifc_objects):
self.report({"ERROR"}, "One of the selected objects must be an IfcSpace.")
return {"CANCELLED"}
if len(objs) == 2:
# The user may select two objects, a space and its related building element
for obj in objs:
element = tool.Ifc.get_entity(obj)
if not element:
continue
if element.is_a("IfcSpace"):
relating_space = element
relating_space_obj = obj
else:
related_building_element = element
related_building_element_obj = obj
# Validate input.
space = next((e for e in ifc_objects.keys() if e.is_a("IfcSpace")), None)
if not space:
self.report({"ERROR"}, "One of the selected objects must be an IfcSpace.")
return {"CANCELLED"}
element = next((e for e in ifc_objects.keys() if e.is_a("IfcElement")), None)
if not element:
self.report({"ERROR"}, "One of the selected objects must be an IfcElement.")
return {"CANCELLED"}
parent_boundary = self.create_element_boundary(
context, relating_space, relating_space_obj, related_building_element, related_building_element_obj
context, space, ifc_objects[space], element, ifc_objects[element]
)
if parent_boundary:
parent_boundaries.append(parent_boundary)
if not parent_boundary:
self.report({"ERROR"}, "Failed to create a boundary.")
return {"FINISHED"}
parent_boundaries.append(parent_boundary)
elif len(objs) == 1:
space = next(iter(ifc_objects))
if not space.is_a("IfcSpace"):
self.report({"ERROR"}, "1 element selected but it's not an IfcSpace - please, select IfcSpace..")
return {"CANCELLED"}
# New prototype, old code not yet removed. Still testing.
space = tool.Ifc.get_entity(objs[0])
if space.is_a("IfcSpace"):
self.auto_generate_boundaries(space, objs[0])
elif len(objs) == 1:
res = self.auto_generate_boundaries(space, ifc_objects[space])
if isinstance(res, str):
self.report({"ERROR"}, res)
return {"FINISHED"}
parent_boundaries.extend(res)
elif len(objs) == 1 and False:
# Optionally the user may select just the space, and the building element shall be auto-detected
# TODO : refactor to be able to generate all boundaries for selected space automatically or with an option
relating_space = None
related_building_element = None
relating_space_obj = None
related_building_element_obj = None
def msg(self, context):
self.layout.label(text="Please set an active container to detect space boundaries from.")
@@ -660,8 +692,17 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
obj = tool.Ifc.get_object(parent_boundary)
obj.select_set(True)
def auto_generate_boundaries(self, space, space_obj):
def auto_generate_boundaries(
self, space: ifcopenshell.entity_instance, space_obj: bpy.types.Object
) -> Union[str, list[ifcopenshell.entity_instance]]:
"""
:return: list of created boundaries or a string with error description.
"""
ifc_file = tool.Ifc.get()
props = tool.Model.get_model_props()
boundaries: list[ifcopenshell.entity_instance] = []
assert isinstance(space_obj.data, bpy.types.Mesh)
# Identify all potential building elements
# TODO: don't select everything, use AABB culling in Blender
building_elements = (
@@ -681,7 +722,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
shapes = {}
settings = ifcopenshell.geom.settings()
settings.set("disable-opening-subtractions", True)
iterator = ifcopenshell.geom.iterator(settings, tool.Ifc.get(), multiprocessing.cpu_count(), include=include)
iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=include)
if iterator.initialize():
while True:
tree.add_element(iterator.get_native())
@@ -694,12 +735,12 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
building_elements = [e for e in tree.select(space, extend=0.1) if e != space]
if not building_elements:
return
return "No building elements found to create boundaries."
# Create a dissolved bmesh for the space
space_bm = bmesh.new()
space_bm.from_mesh(space_obj.data)
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts, edges=space_bm.edges)
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts[:], edges=space_bm.edges[:])
# Create dissolved bmeshes for all boundary elements
building_element_bms = {}
@@ -777,7 +818,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
# we cheat by using the exterior boundary to mean "gross".
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
parent_boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
parent_boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
if building_element.is_a("IfcVirtualElement"):
parent_boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
else:
@@ -809,6 +850,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
exterior_boundary_polygon, space_face_matrix
)
self.set_boundary_name(parent_boundary)
boundaries.append(parent_boundary)
for rel in getattr(building_element, "HasOpenings", []):
opening = rel.RelatedOpeningElement
@@ -854,7 +896,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
if opening_polygon.intersection(exterior_boundary_polygon).area == 0:
continue
boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
boundary.RelatingSpace = space
boundary.RelatedBuildingElement = filling or opening
boundary.ConnectionGeometry = self.create_connection_geometry_from_polygon(
@@ -868,29 +910,39 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
if boundary.is_a() != "IfcRelSpaceBoundary":
boundary.ParentBoundary = parent_boundary
self.set_boundary_name(boundary)
boundaries.append(boundary)
return boundaries
def create_element_boundary(
self, context, relating_space, relating_space_obj, related_building_element, related_building_element_obj
):
if not relating_space or not related_building_element:
return
self,
context: bpy.types.Context,
space: ifcopenshell.entity_instance,
space_obj: bpy.types.Object,
building_element: ifcopenshell.entity_instance,
building_element_obj: bpy.types.Object,
) -> Union[ifcopenshell.entity_instance, None]:
"""Create element boundary for IfcSpace and IfcElement as building element.
:return: IfcRelSpaceBoundary or ``None`` if couldn't find a face on ``space_obj``
that would be close enough to ``related_building_element_obj``.
"""
props = tool.Model.get_model_props()
# Find which face on space should be bounded to related building element
# TODO: Handle round wall where multiple faces need to be bound to the same related building element
bm = bmesh.new()
bm.from_mesh(relating_space_obj.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi * 2 / 360, verts=bm.verts, edges=bm.edges)
space_bm = bmesh.new()
assert isinstance(space_obj.data, bpy.types.Mesh)
space_bm.from_mesh(space_obj.data)
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts[:], edges=space_bm.edges[:])
target_distance = inf
target_face = None
for face in bm.faces:
centroid = relating_space_obj.matrix_world @ face.calc_center_median()
raycast = related_building_element_obj.closest_point_on_mesh(
related_building_element_obj.matrix_world.inverted() @ centroid, distance=1
)
to_building_obj_space = building_element_obj.matrix_world.inverted()
for face in space_bm.faces:
centroid = space_obj.matrix_world @ face.calc_center_median()
raycast = building_element_obj.closest_point_on_mesh(to_building_obj_space @ centroid, distance=1)
if raycast[0]:
distance = (related_building_element_obj.matrix_world @ raycast[1] - centroid).length
distance = (building_element_obj.matrix_world @ raycast[1] - centroid).length
if distance < target_distance:
target_face = face
target_distance = distance
@@ -905,9 +957,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
target_face_polygon = shapely.Polygon([tuple((target_face_matrix_i @ v).xy) for v in target_face_verts])
related_building_element_polygon = self.get_flattened_polygon(
related_building_element, relating_space_obj, target_face_matrix_i
)
related_building_element_polygon = self.get_flattened_polygon(building_element, space_obj, target_face_matrix_i)
gross_boundary_polygon = target_face_polygon.intersection(related_building_element_polygon)
@@ -923,7 +973,8 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
):
return
parent_boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
ifc_file = tool.Ifc.get()
parent_boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
parent_boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
# Set to EXTERNAL by default and turn later to internal if there is a corresponding boundary relating to an
# internal space
@@ -936,14 +987,14 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
# space.
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
for rel in getattr(related_building_element, "HasOpenings", []):
for rel in getattr(building_element, "HasOpenings", []):
opening = rel.RelatedOpeningElement
if not opening.HasFillings:
continue
# TODO: HasFilling is a zero to many relationship. How to handle many ?
filling = opening.HasFillings[0].RelatedBuildingElement
opening_polygon = self.get_flattened_polygon(opening, relating_space_obj, target_face_matrix_i)
opening_polygon = self.get_flattened_polygon(opening, space_obj, target_face_matrix_i)
# An inner boundary is supposed to overlap its parent boundary according to IFC4 documentation
# so we extend our exterior boundary with all opening which has a filling
@@ -959,10 +1010,10 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
connection_geometry = self.create_connection_geometry_from_polygon(opening_polygon, target_face_matrix)
boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
boundary.RelatingSpace = relating_space
boundary.RelatingSpace = space
boundary.RelatedBuildingElement = filling
boundary.ConnectionGeometry = connection_geometry
if related_building_element.is_a("IfcVirtualElement"):
if building_element.is_a("IfcVirtualElement"):
boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
else:
boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
@@ -974,20 +1025,23 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
connection_geometry = self.create_connection_geometry_from_polygon(
exterior_boundary_polygon, target_face_matrix
)
parent_boundary.RelatingSpace = relating_space
parent_boundary.RelatedBuildingElement = related_building_element
parent_boundary.RelatingSpace = space
parent_boundary.RelatedBuildingElement = building_element
parent_boundary.ConnectionGeometry = connection_geometry
self.set_boundary_name(parent_boundary)
return parent_boundary
def get_face_matrix(self, p1, p2, p3):
def get_face_matrix(self, p1: Vector, p2: Vector, p3: Vector) -> Matrix:
edge1 = p2 - p1
edge2 = p3 - p1
normal = edge1.cross(edge2)
assert isinstance(normal, Vector)
z_axis = normal.normalized()
x_axis = p2 - p1
x_axis.normalize()
y_axis = z_axis.cross(x_axis)
assert isinstance(y_axis, Vector)
mat = Matrix()
mat.col[0][:3] = x_axis
@@ -1057,7 +1111,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
return surface
def set_boundary_name(self, boundary):
def set_boundary_name(self, boundary: ifcopenshell.entity_instance):
"""
By convention 1stLevel and 2ndLevel boundary have specific name and description
See https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRelSpaceBoundary.htm
+8 -6
View File
@@ -52,9 +52,9 @@ class BIM_PT_Boundary(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
props = context.active_object.BIMObjectProperties
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
return False
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
@@ -65,7 +65,7 @@ class BIM_PT_Boundary(Panel):
def draw(self, context):
obj = context.active_object
assert obj
props = obj.BIMObjectProperties
props = tool.Blender.get_object_bim_props(obj)
ifc_file = tool.Ifc.get()
boundary = ifc_file.by_id(props.ifc_definition_id)
self.bprops = tool.Boundary.get_object_boundary_props(obj)
@@ -128,9 +128,9 @@ class BIM_PT_SpaceBoundaries(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
props = context.active_object.BIMObjectProperties
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
return False
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
@@ -145,7 +145,9 @@ class BIM_PT_SpaceBoundaries(Panel):
if not SpaceBoundariesData.is_loaded:
SpaceBoundariesData.load()
self.props = context.active_object.BIMObjectProperties
obj = context.active_object
assert obj
self.props = tool.Blender.get_object_bim_props(obj)
self.ifc_file = tool.Ifc.get()
row = self.layout.row()
row.operator("bim.load_space_boundaries")
+1 -1
View File
@@ -52,7 +52,7 @@ class BrickschemaData:
def active_relations(cls):
if BrickStore.graph is None:
return []
props = bpy.context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
try:
brick = props.bricks[props.active_brick_index]
except:
+21 -21
View File
@@ -22,29 +22,26 @@ import ifcopenshell.api
import bonsai.tool as tool
import bonsai.core.brick as core
import bonsai.bim.handler
from bpy_extras.io_utils import ImportHelper, ExportHelper
from bonsai.bim.ifc import IfcStore
from bonsai.tool.brick import BrickStore
class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator):
class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.load_brick_project"
bl_label = "Load Brickschema Project"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Load in a Brick project from a file"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"})
filename_ext = ".ttl"
def _execute(self, context):
if os.path.exists(self.filepath) and "ttl" in os.path.splitext(self.filepath)[1].lower():
root = context.scene.BIMBrickProperties.brick_list_root
root = tool.Brick.get_brick_props().brick_list_root
core.load_brick_project(tool.Brick, filepath=self.filepath, brick_root=root)
else:
self.report({"ERROR"}, f"Failed to load {self.filepath}")
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ViewBrickClass(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.view_brick_class"
@@ -111,10 +108,10 @@ class AssignBrickReference(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Assign the selected Ifc entity to the selected Brick entity"
def _execute(self, context):
if not context.active_object:
if not (obj := context.active_object) or not (element := tool.Ifc.get_entity(obj)):
self.report({"ERROR"}, f"No Ifc selected")
return
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
try:
props.bricks[props.active_brick_index]
except:
@@ -123,7 +120,7 @@ class AssignBrickReference(bpy.types.Operator, tool.Ifc.Operator):
core.assign_brick_reference(
tool.Ifc,
tool.Brick,
element=tool.Ifc.get_entity(context.active_object),
element=element,
library=tool.Ifc.get().by_id(int(props.libraries)),
brick_uri=props.bricks[props.active_brick_index].uri,
)
@@ -136,7 +133,7 @@ class AddBrick(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Create the Brick entity"
def _execute(self, context):
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
core.add_brick(
tool.Ifc,
tool.Brick,
@@ -155,7 +152,7 @@ class AddBrickRelation(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Create the Brick relationship"
def _execute(self, context):
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
brick = props.bricks[props.active_brick_index]
if props.new_brick_relation_type == "http://www.w3.org/2000/01/rdf-schema#label":
object = props.new_brick_relation_object
@@ -173,7 +170,7 @@ class ConvertIfcToBrick(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Convert Ifc entities and relations to Brick entities and relations"
def _execute(self, context):
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
library = None
if props.libraries:
library = tool.Ifc.get().by_id(int(props.libraries))
@@ -201,7 +198,8 @@ class NewBrickFile(bpy.types.Operator):
return {"FINISHED"}
def _execute(self, context):
root = context.scene.BIMBrickProperties.brick_list_root
props = tool.Brick.get_brick_props()
root = props.brick_list_root
core.new_brick_file(tool.Brick, brick_root=root)
def rollback(self, data):
@@ -230,7 +228,7 @@ class RemoveBrick(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Delete this entity"
def _execute(self, context):
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
core.remove_brick(
tool.Ifc,
tool.Brick,
@@ -239,18 +237,20 @@ class RemoveBrick(bpy.types.Operator, tool.Ifc.Operator):
)
class SerializeBrick(bpy.types.Operator):
class SerializeBrick(bpy.types.Operator, ExportHelper):
bl_idname = "bim.serialize_brick"
bl_label = "Serialize Brick"
# Prevents crash on Blender 4.4.0.
bl_description = "Save active Brick project by the provided filepath."
filename_ext = ".ttl"
filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"})
def invoke(self, context, event):
if self.should_save_as or not BrickStore.path:
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
return ExportHelper.invoke(self, context, event)
else:
return self.execute(context)
@@ -273,7 +273,7 @@ class AddBrickNamespace(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Bind a new namespace to the Brick project"
def _execute(self, context):
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
alias = props.new_brick_namespace_alias
uri = props.new_brick_namespace_uri
core.add_brick_namespace(tool.Brick, alias=alias, uri=uri)
@@ -288,6 +288,6 @@ class RemoveBrickRelation(bpy.types.Operator, tool.Ifc.Operator):
object: bpy.props.StringProperty(name="Object")
def _execute(self, context):
props = context.scene.BIMBrickProperties
props = tool.Brick.get_brick_props()
brick = props.bricks[props.active_brick_index]
core.remove_brick_relation(tool.Brick, brick_uri=brick.uri, predicate=self.predicate, object=self.object)
+35 -4
View File
@@ -33,6 +33,7 @@ from bpy.props import (
import bonsai.core.brick as core
import bonsai.tool.brick as tool
from bonsai.tool.brick import BrickStore
from typing import TYPE_CHECKING
def update_active_brick_index(self, context):
@@ -74,13 +75,13 @@ def get_brick_relations(self, context):
return BRICK_RELATIONS_ENUM_ITEMS
def update_view(self, context):
root = context.scene.BIMBrickProperties.brick_list_root
def update_view(self: "BIMBrickProperties", context: bpy.types.Context) -> None:
root = self.brick_list_root
core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=False)
def split_screen_update_view(self, context):
root = context.scene.BIMBrickProperties.split_screen_brick_list_root
def split_screen_update_view(self: "BIMBrickProperties", context: bpy.types.Context) -> None:
root = self.split_screen_brick_list_root
core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=True)
@@ -90,6 +91,11 @@ class Brick(PropertyGroup):
uri: StringProperty(name="URI")
total_items: IntProperty(name="Total Items")
if TYPE_CHECKING:
label: str
uri: str
total_items: int
class BIMBrickProperties(PropertyGroup):
active_brick_class: StringProperty(name="Active Brick Class")
@@ -124,3 +130,28 @@ class BIMBrickProperties(PropertyGroup):
split_screen_brick_list_root: EnumProperty(
name="Split Screen Brick List Root", items=get_brick_roots, update=split_screen_update_view
)
if TYPE_CHECKING:
active_brick_class: str
brick_breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
bricks: bpy.types.bpy_prop_collection_idprop[Brick]
active_brick_index: int
libraries: str
set_list_root_toggled: bool
brick_list_root: str
namespace: str
new_brick_namespace_alias: str
new_brick_namespace_uri: str
new_brick_label: str
brick_entity_create_type: str
brick_entity_class: str
brick_create_relations_toggled: bool
brick_edit_relations_toggled: bool
new_brick_relation_type: str
new_brick_relation_object: str
split_screen_toggled: bool
split_screen_bricks: bpy.types.bpy_prop_collection_idprop[Brick]
split_screen_active_brick_index: int
split_screen_active_brick_class: str
split_screen_brick_breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
split_screen_brick_list_root: str
+19 -7
View File
@@ -45,7 +45,7 @@ class BIM_PT_brickschema_project_info(Panel):
bl_parent_id = "BIM_PT_brickschema"
def draw(self, context):
self.props = context.scene.BIMBrickProperties
self.props = tool.Brick.get_brick_props()
if not BrickschemaData.data["is_loaded"]:
row = self.layout.row(align=True)
@@ -87,7 +87,7 @@ class BIM_PT_brickschema_namespaces(Panel):
return BrickStore.graph != None
def draw(self, context):
self.props = context.scene.BIMBrickProperties
self.props = tool.Brick.get_brick_props()
row = self.layout.row(align=True)
row.label(text="Active Namespace:")
@@ -122,7 +122,7 @@ class BIM_PT_brickschema_create_entity(Panel):
return BrickStore.graph != None
def draw(self, context):
self.props = context.scene.BIMBrickProperties
self.props = tool.Brick.get_brick_props()
# TO DO: hide this if selected entity already has a reference, or something similar
row = self.layout.row(align=True)
@@ -156,7 +156,7 @@ class BIM_PT_brickschema_viewport(Panel):
return BrickStore.graph != None
def draw(self, context):
self.props = context.scene.BIMBrickProperties
self.props = tool.Brick.get_brick_props()
row = self.layout.row(align=True)
row.column().alignment = "RIGHT"
@@ -178,7 +178,14 @@ class BIM_PT_brickschema_viewport(Panel):
row = grid_left.row()
BIM_UL_bricks.split_screen = False
row.template_list("BIM_UL_bricks", "", self.props, "bricks", self.props, "active_brick_index")
row.template_list(
BIM_UL_bricks.__name__,
BIM_UL_bricks.__name__ + "_bricks",
self.props,
"bricks",
self.props,
"active_brick_index",
)
if self.props.split_screen_toggled:
grid_right = grid.column(align=True)
@@ -195,7 +202,12 @@ class BIM_PT_brickschema_viewport(Panel):
row = grid_right.row()
BIM_UL_bricks.split_screen = True
row.template_list(
"BIM_UL_bricks", "", self.props, "split_screen_bricks", self.props, "split_screen_active_brick_index"
BIM_UL_bricks.__name__,
BIM_UL_bricks.__name__ + "_split_screen_bricks",
self.props,
"split_screen_bricks",
self.props,
"split_screen_active_brick_index",
)
if BrickschemaData.data["active_relations"]:
@@ -292,7 +304,7 @@ class BIM_PT_ifc_brickschema_references(Panel):
def draw(self, context):
if not BrickschemaReferencesData.is_loaded:
BrickschemaReferencesData.load()
self.props = context.scene.BIMBrickProperties
self.props = tool.Brick.get_brick_props()
if not BrickschemaReferencesData.data["is_loaded"]:
row = self.layout.row()
+51 -47
View File
@@ -128,9 +128,10 @@ class CadMitre(bpy.types.Operator):
class CadFillet(bpy.types.Operator):
bl_idname = "bim.cad_fillet"
bl_label = "CAD Fillet"
bl_description = "Add fillet to the 2 selected edges."
bl_options = {"REGISTER", "UNDO"}
resolution: bpy.props.IntProperty(name="Arc Resolution", min=0, default=1)
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE")
@classmethod
def poll(cls, context):
@@ -143,18 +144,15 @@ class CadFillet(bpy.types.Operator):
layout.prop(self, prop)
def execute(self, context):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.radius = self.radius * si_conversion
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.mode_set(mode="EDIT")
obj = bpy.context.active_object
cursor = obj.matrix_world.inverted() @ bpy.context.scene.cursor.location
mesh = obj.data
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
bm = bmesh.from_edit_mesh(mesh)
selected_edges = [e for e in bm.edges if e.select]
if len(selected_edges) != 2:
self.report({"ERROR"}, "Exactly 2 edges should be selected.")
return {"CANCELLED"}
# Assume the user has selected two edges sharing a vert, but merge verts
@@ -219,6 +217,7 @@ class CadFillet(bpy.types.Operator):
class CadArcFrom2Points(bpy.types.Operator):
bl_idname = "bim.cad_arc_from_2_points"
bl_label = "CAD Arc from 2 Points"
bl_description = "Add an arc to the active mesh based on 2 selected vertices."
bl_options = {"REGISTER", "UNDO"}
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
should_flip: bpy.props.BoolProperty(name="Flip", description="Flip arc", default=False)
@@ -235,6 +234,7 @@ class CadArcFrom2Points(bpy.types.Operator):
if bpy.context.mode != "EDIT_MESH":
return {"CANCELLED"}
obj = bpy.context.active_object
assert obj
region = bpy.context.region
region_3d = bpy.context.area.spaces.active.region_3d
cursor = bpy.context.scene.cursor.location
@@ -242,10 +242,12 @@ class CadArcFrom2Points(bpy.types.Operator):
if not center:
return {"CANCELLED"}
mesh = obj.data
assert isinstance(mesh, bpy.types.Mesh)
mw = obj.matrix_world
bm = bmesh.from_edit_mesh(mesh)
selected_verts = [v for v in bm.verts if v.select]
if len(selected_verts) != 2:
self.report({"ERROR"}, "Exactly 2 vertices should be selected.")
return {"CANCELLED"}
v1 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[0].co)
v2 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[1].co)
@@ -342,11 +344,12 @@ class CadArcFrom3Points(bpy.types.Operator):
class CadOffset(bpy.types.Operator):
bl_idname = "bim.cad_offset"
bl_label = "CAD Offset"
bl_description = "Copy selected mesh geometry at provided offset. Mesh copied based on the current viewport angle."
bl_options = {"REGISTER", "UNDO"}
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE")
@classmethod
def poll(self, context):
def poll(cls, context):
return context.mode == "EDIT_MESH"
def draw(self, context):
@@ -369,9 +372,6 @@ class CadOffset(bpy.types.Operator):
# effect of converting an unclosed loop into a closed loop which is also
# not what users expect.
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.distance = self.distance * si_conversion
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.mode_set(mode="EDIT")
@@ -543,7 +543,8 @@ class CadOffset(bpy.types.Operator):
class AddIfcCircle(bpy.types.Operator):
bl_idname = "bim.add_ifccircle"
bl_label = "Add IfcCircle"
radius: bpy.props.FloatProperty(name="Radius", default=0.5)
bl_description = "Add IfcCircle to the currently active mesh."
radius: bpy.props.FloatProperty(name="Radius", default=0.5, subtype="DISTANCE")
@classmethod
def poll(cls, context):
@@ -551,18 +552,19 @@ class AddIfcCircle(bpy.types.Operator):
return bool(obj) and obj.type == "MESH"
def execute(self, context):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.radius = self.radius * si_conversion
obj = context.active_object
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
self.obj = obj
self.mesh = mesh
if self.has_selected_existing_circle(context):
self.change_radius(context)
else:
self.create_circle(context)
return {"FINISHED"}
def has_selected_existing_circle(self, context):
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
def has_selected_existing_circle(self, context: bpy.types.Context) -> bool:
obj = self.obj
bm = bmesh.from_edit_mesh(self.mesh)
verts = [v for v in bm.verts if v.select and not v.hide]
if len(verts) != 2:
return False
@@ -575,32 +577,32 @@ class AddIfcCircle(bpy.types.Operator):
for group in groups:
if group in verts[0][deform_layer] and group in verts[1][deform_layer]:
return True
return False
def change_radius(self, context):
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
def change_radius(self, context: bpy.types.Object) -> None:
bm = bmesh.from_edit_mesh(self.mesh)
verts = [v for v in bm.verts if v.select and not v.hide]
center = verts[0].co.lerp(verts[1].co, 0.5)
verts[0].co = center + ((verts[0].co - center).normalized() * self.radius)
verts[1].co = center + ((verts[1].co - center).normalized() * self.radius)
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(obj.data)
bmesh.update_edit_mesh(self.mesh)
def create_circle(self, context):
obj = context.active_object
def create_circle(self, context: bpy.types.Context) -> None:
obj = self.obj
bpy.ops.object.mode_set(mode="OBJECT")
# The last group may be the result of a prior run of this operator.
# I tried looping through all groups but Blender group indices seem to behave unpredictably.
if len(obj.vertex_groups):
last_group = obj.vertex_groups[-1]
verts_in_group = [v for v in obj.data.vertices if last_group.index in [vg.group for vg in v.groups]]
verts_in_group = [v for v in self.mesh.vertices if last_group.index in [vg.group for vg in v.groups]]
if "IFCCIRCLE" in last_group.name and len(verts_in_group) != 2:
obj.vertex_groups.remove(last_group)
group = obj.vertex_groups.new(name="IFCCIRCLE")
bpy.ops.object.mode_set(mode="EDIT")
bm = bmesh.from_edit_mesh(obj.data)
bm = bmesh.from_edit_mesh(self.mesh)
bm.verts.layers.deform.verify()
deform_layer = bm.verts.layers.deform.active
@@ -617,7 +619,7 @@ class AddIfcCircle(bpy.types.Operator):
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(obj.data)
bmesh.update_edit_mesh(self.mesh)
class AddIfcArcIndexFillet(bpy.types.Operator):
@@ -638,18 +640,19 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
layout.prop(self, prop)
def execute(self, context):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.radius = self.radius * si_conversion
obj = context.active_object
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
self.obj = obj
self.mesh = mesh
if self.has_selected_existing_arc(context):
self.change_radius(context)
else:
self.create_arc(context)
return self.create_arc(context)
return {"FINISHED"}
def has_selected_existing_arc(self, context: bpy.types.Context) -> bool:
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
obj = self.obj
bm = bmesh.from_edit_mesh(self.mesh)
verts = [v for v in bm.verts if v.select and not v.hide]
if len(verts) != 3:
return False
@@ -668,13 +671,14 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
return False
def change_radius(self, context: bpy.types.Context) -> None:
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
obj = self.obj
bm = bmesh.from_edit_mesh(self.mesh)
edges = [e for e in bm.edges if e.select and not e.hide]
mid = list(set(edges[0].verts) & set(edges[1].verts))[0]
v1 = edges[0].other_vert(mid)
v2 = edges[1].other_vert(mid)
assert v1 and v2
center = tool.Cad.get_center_of_arc([v1.co, mid.co, v2.co])
if len(v1.link_edges) != 2 or len(v2.link_edges) != 2:
@@ -682,6 +686,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
v3 = v1.link_edges[1].other_vert(v1) if mid in v1.link_edges[0].verts else v1.link_edges[0].other_vert(v1)
v4 = v2.link_edges[1].other_vert(v2) if mid in v2.link_edges[0].verts else v2.link_edges[0].other_vert(v2)
assert v3 and v4
dir1 = (v3.co - v1.co).normalized()
dir2 = (v4.co - v2.co).normalized()
@@ -702,11 +707,10 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(obj.data)
def create_arc(self, context):
obj = bpy.context.active_object
bmesh.update_edit_mesh(self.mesh)
def create_arc(self, context: bpy.types.Context) -> set[str]:
obj = self.obj
bpy.ops.object.mode_set(mode="OBJECT")
# The last group may be the result of a prior run of this operator.
# I tried looping through all groups but Blender group indices seem to behave unpredictably.
@@ -718,7 +722,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
group = obj.vertex_groups.new(name="IFCARCINDEX")
bpy.ops.object.mode_set(mode="EDIT")
bm = bmesh.from_edit_mesh(obj.data)
bm = bmesh.from_edit_mesh(self.mesh)
bm.verts.layers.deform.verify()
deform_layer = bm.verts.layers.deform.active
@@ -737,6 +741,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
shared_vert = list(set(selected_edges[0].verts) & set(selected_edges[1].verts))[0]
v1 = selected_edges[0].other_vert(shared_vert)
v2 = selected_edges[1].other_vert(shared_vert)
assert v1 and v2
dir1 = (v1.co - shared_vert.co).normalized()
dir2 = (v2.co - shared_vert.co).normalized()
edge_angle = dir1.angle(dir2)
@@ -770,7 +775,8 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
bm.verts.index_update()
bm.edges.index_update()
bmesh.update_edit_mesh(obj.data)
bmesh.update_edit_mesh(self.mesh)
return {"FINISHED"}
class AlignViewToProfile(bpy.types.Operator):
@@ -806,8 +812,9 @@ class AlignViewToProfile(bpy.types.Operator):
class AddRectangle(bpy.types.Operator):
bl_idname = "bim.add_rectangle"
bl_label = "Add Rectangle"
x: bpy.props.FloatProperty(name="X", default=1)
y: bpy.props.FloatProperty(name="Y", default=1)
bl_description = "Add rectangle shape to the active mesh."
x: bpy.props.FloatProperty(name="X", default=1, subtype="DISTANCE")
y: bpy.props.FloatProperty(name="Y", default=1, subtype="DISTANCE")
@classmethod
def poll(cls, context):
@@ -815,11 +822,8 @@ class AddRectangle(bpy.types.Operator):
return bool(obj) and obj.type == "MESH"
def execute(self, context):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.x = self.x * si_conversion
self.y = self.y * si_conversion
obj = context.active_object
assert obj and isinstance(obj.data, bpy.types.Mesh)
bm = bmesh.from_edit_mesh(obj.data)
cursor = obj.matrix_world.inverted() @ context.scene.cursor.location
new_verts = [
+9
View File
@@ -20,6 +20,7 @@ import bpy
from bonsai.bim.module.model.data import AuthoringData
from bpy.types import PropertyGroup
from math import pi
from typing import TYPE_CHECKING
class BIMCadProperties(PropertyGroup):
@@ -31,3 +32,11 @@ class BIMCadProperties(PropertyGroup):
gable_roof_edge_angle: bpy.props.FloatProperty(
name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE"
)
if TYPE_CHECKING:
resolution: int
radius: float
distance: float
x: float
y: float
gable_roof_edge_angle: float
+31 -53
View File
@@ -23,7 +23,7 @@ import bonsai.bim.module.type.prop as type_prop
import ifcopenshell.util.unit
from bpy.types import WorkSpaceTool
from bonsai.bim.module.model.data import AuthoringData, RailingData, RoofData
from typing import Union
from functools import partial
def load_custom_icons():
@@ -114,11 +114,11 @@ class CadTool(WorkSpaceTool):
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Rectangle", "S_R", "Rectangle", ui_context)
add_layout_hotkey_operator(row, "Rectangle", "S_R", bpy.ops.bim.add_rectangle.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Circle", "S_C", "Circle", ui_context)
add_layout_hotkey_operator(row, "Circle", "S_C", bpy.ops.bim.add_ifccircle.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.set_arc_index.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
@@ -138,15 +138,15 @@ class CadTool(WorkSpaceTool):
row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context
)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", "Fillet", ui_context)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.cad_fillet.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
else:
if (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["pset_data"]
and obj.BIMRailingProperties.is_editing_path
and tool.Model.get_railing_props(obj).is_editing_path
):
add_header_apply_button(
layout,
@@ -159,7 +159,7 @@ class CadTool(WorkSpaceTool):
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
and obj.BIMRoofProperties.is_editing_path
and tool.Model.get_roof_props(obj).is_editing_path
):
add_header_apply_button(
layout, "Edit Roof Path", "bim.finish_editing_roof_path", "bim.cancel_editing_roof_path", ui_context
@@ -176,17 +176,17 @@ class CadTool(WorkSpaceTool):
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "2-Point Arc", "S_C", "2-Point Arc", ui_context)
add_layout_hotkey_operator(row, "2-Point Arc", "S_C", bpy.ops.bim.cad_arc_from_2_points.__doc__, ui_context)
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.cad_arc_from_3_points.__doc__, ui_context)
class CadHotkey(bpy.types.Operator):
bl_idname = "bim.cad_hotkey"
bl_label = "CAD Hotkey"
bl_options = {"REGISTER", "UNDO"}
bl_label = ""
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
hotkey: bpy.props.StringProperty()
description: bpy.props.StringProperty()
@@ -195,12 +195,14 @@ class CadHotkey(bpy.types.Operator):
return operator.description or ""
def execute(self, context):
self.props = context.scene.BIMCadProperties
self.props = tool.Cad.get_cad_props()
getattr(self, f"hotkey_{self.hotkey}")()
return {"FINISHED"}
def draw(self, context):
props = context.scene.BIMCadProperties
props = tool.Cad.get_cad_props()
obj = context.active_object
if self.hotkey == "S_C":
if tool.Geometry.is_profile_object_active():
row = self.layout.row()
@@ -226,7 +228,7 @@ class CadHotkey(bpy.types.Operator):
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
and bpy.context.active_object.BIMRoofProperties.is_editing_path
and tool.Model.get_roof_props(obj).is_editing_path
):
self.layout.row().prop(props, "gable_roof_edge_angle")
@@ -236,9 +238,8 @@ class CadHotkey(bpy.types.Operator):
row.prop(props, "resolution")
def hotkey_S_C(self):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if tool.Geometry.is_profile_object_active():
bpy.ops.bim.add_ifccircle(radius=self.props.radius / si_conversion)
bpy.ops.bim.add_ifccircle(radius=self.props.radius)
else:
bpy.ops.bim.cad_arc_from_2_points()
@@ -246,15 +247,13 @@ class CadHotkey(bpy.types.Operator):
bpy.ops.bim.cad_trim_extend()
def hotkey_S_F(self):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if tool.Geometry.is_profile_object_active():
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius / si_conversion)
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius)
else:
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius / si_conversion)
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius)
def hotkey_S_O(self):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bpy.ops.bim.cad_offset(distance=self.props.distance / si_conversion)
bpy.ops.bim.cad_offset(distance=self.props.distance)
def hotkey_S_Q(self):
obj = bpy.context.active_object
@@ -281,13 +280,16 @@ class CadHotkey(bpy.types.Operator):
bpy.ops.bim.edit_extrusion_axis()
def hotkey_S_R(self):
obj = bpy.context.active_object
if not obj:
return
if tool.Geometry.is_profile_object_active():
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bpy.ops.bim.add_rectangle(x=self.props.x / si_conversion, y=self.props.y / si_conversion)
bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y)
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
and bpy.context.active_object.BIMRoofProperties.is_editing_path
and tool.Model.get_roof_props(obj).is_editing_path
):
bpy.ops.bim.set_gable_roof_edge_angle(angle=self.props.gable_roof_edge_angle)
@@ -305,7 +307,9 @@ class CadHotkey(bpy.types.Operator):
bpy.ops.bim.reset_vertex()
def add_header_apply_button(layout, text, apply_operator, cancel_operator, ui_context=""):
def add_header_apply_button(
layout: bpy.types.UILayout, text: str, apply_operator: str, cancel_operator: str, ui_context: str = ""
) -> None:
custom_icon = custom_icon_previews.get(text.upper().replace(" ", "_"), custom_icon_previews["IFC"]).icon_id
row = layout.row(align=True)
row.label(text=f"{text} Mode", icon_value=custom_icon)
@@ -324,34 +328,8 @@ def add_header_apply_button(layout, text, apply_operator, cancel_operator, ui_co
row.label(text="Tools")
def add_layout_hotkey_operator(
layout: bpy.types.UILayout, text: str, hotkey: str, description: Union[str, None], ui_context: str = ""
) -> bpy.types.OperatorProperties:
parts = hotkey.split("_")
modifier, key = parts
op_text = "" if ui_context == "TOOL_HEADER" else text
custom_icon = custom_icon_previews.get(text.upper().replace(" ", "_"), custom_icon_previews["IFC"]).icon_id
modifier_icon, modifier_str = MODIFIERS.get(modifier, ("NONE", ""))
row = layout if ui_context == "TOOL_HEADER" else layout.row(align=True)
op = row.operator("bim.cad_hotkey", text=op_text, icon_value=custom_icon)
if ui_context != "TOOL_HEADER" and len(parts) == 2:
layout = layout.row(align=True) # Create a new line for hotkey display
layout.label(text="", icon=modifier_icon)
layout.label(text="", icon=f"EVENT_{key}")
hotkey_description = f"Hotkey: {modifier_str} {key}"
description = "\n\n".join(filter(None, [hotkey_description if description else ""]))
op.hotkey = hotkey
op.description = description or hotkey_description
return op
add_layout_hotkey_operator = partial(tool.Blender.add_layout_hotkey_operator, tool_name="cad", module_name=__name__)
custom_icon_previews = None
display_mode = None
MODIFIERS = {
"S": ("EVENT_SHIFT", ""),
}
@@ -49,6 +49,8 @@ class ClashDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
@@ -60,8 +62,9 @@ class ClashDecorator:
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
text = context.scene.BIMClashProperties.active_clash_text
p = context.scene.BIMClashProperties.p1.lerp(context.scene.BIMClashProperties.p2, 0.5)
props = tool.Clash.get_clash_props()
text = props.active_clash_text
p = props.p1.lerp(props.p2, 0.5)
font_id = 0
blf.size(font_id, 12)
@@ -92,7 +95,8 @@ class ClashDecorator:
# general shader
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
selected_vertices = [context.scene.BIMClashProperties.p1, context.scene.BIMClashProperties.p2]
props = tool.Clash.get_clash_props()
selected_vertices = [props.p1, props.p2]
selected_edges = []
if selected_vertices[0] != selected_vertices[1]:
selected_edges = [[0, 1]]
+73 -73
View File
@@ -24,26 +24,20 @@ import logging
import numpy as np
import ifcopenshell
import bonsai.tool as tool
from bpy_extras.io_utils import ExportHelper, ImportHelper
from math import radians
from mathutils import Matrix, Vector
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.clash.decorator import ClashDecorator
class ExportClashSets(bpy.types.Operator):
class ExportClashSets(bpy.types.Operator, ExportHelper):
bl_idname = "bim.export_clash_sets"
bl_label = "Export Clash Sets"
bl_description = "Export clash sets to a selected file"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
clash_sets = tool.Clash.export_clash_sets()
@@ -52,26 +46,24 @@ class ExportClashSets(bpy.types.Operator):
return {"FINISHED"}
class ImportClashSets(bpy.types.Operator):
class ImportClashSets(bpy.types.Operator, ImportHelper):
bl_idname = "bim.import_clash_sets"
bl_label = "Import Clash Sets"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Import clash sets from a selected file"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
return ImportHelper.invoke(self, context, event)
def execute(self, context):
tool.Clash.load_clash_sets(self.filepath)
context.scene.BIMClashProperties.clash_sets.clear()
props = tool.Clash.get_clash_props()
props.clash_sets.clear()
for clash_set in tool.Clash.get_clash_sets():
new = context.scene.BIMClashProperties.clash_sets.add()
new = props.clash_sets.add()
new.name = clash_set["name"]
new.mode = clash_set["mode"]
if new.mode == "intersection":
@@ -106,7 +98,8 @@ class AddClashSet(bpy.types.Operator):
bl_description = "Add a clash set"
def execute(self, context):
new = context.scene.BIMClashProperties.clash_sets.add()
props = tool.Clash.get_clash_props()
new = props.clash_sets.add()
new.name = "New Clash Set"
return {"FINISHED"}
@@ -119,7 +112,8 @@ class RemoveClashSet(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
context.scene.BIMClashProperties.clash_sets.remove(self.index)
props = tool.Clash.get_clash_props()
props.clash_sets.remove(self.index)
return {"FINISHED"}
@@ -131,7 +125,8 @@ class AddClashSource(bpy.types.Operator):
group: bpy.props.StringProperty()
def execute(self, context):
clash_set = context.scene.BIMClashProperties.active_clash_set
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
source = getattr(clash_set, self.group).add()
return {"FINISHED"}
@@ -145,78 +140,74 @@ class RemoveClashSource(bpy.types.Operator):
group: bpy.props.StringProperty()
def execute(self, context):
clash_set = context.scene.BIMClashProperties.active_clash_set
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
getattr(clash_set, self.group).remove(self.index)
return {"FINISHED"}
class SelectClashSource(bpy.types.Operator):
class SelectClashSource(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_clash_source"
bl_label = "Select Clash Source"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select an IFC file to add as a clash source"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
index: bpy.props.IntProperty()
group: bpy.props.StringProperty()
filename_ext = ".ifc"
def execute(self, context):
clash_set = context.scene.BIMClashProperties.active_clash_set
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
getattr(clash_set, self.group)[self.index].name = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectClashResults(bpy.types.Operator):
class SelectClashResults(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_clash_results"
bl_label = "Select Clash Results"
bl_description = "Select filepath for clash results."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
context.scene.BIMClashProperties.clash_results_path = self.filepath
props = tool.Clash.get_clash_props()
props.clash_results_path = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectSmartGroupedClashesPath(bpy.types.Operator):
class SelectSmartGroupedClashesPath(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_smart_grouped_clashes_path"
bl_label = "Select Smart-Grouped Clashes Path"
bl_description = "Select filepath for smart-grouped clashes."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
context.scene.BIMClashProperties.smart_grouped_clashes_path = self.filepath
props = tool.Clash.get_clash_props()
props.smart_grouped_clashes_path = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ExecuteIfcClash(bpy.types.Operator):
class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
bl_idname = "bim.execute_ifc_clash"
bl_label = "Execute IFC Clash"
bl_description = "Execute clash detection and save the information to a .bcf or .json file"
filter_glob: bpy.props.StringProperty(default="*.bcf;*.json", options={"HIDDEN"})
format: bpy.props.EnumProperty(name="Format", items=[(i, i, "") for i in ("bcf", "json")])
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
@property
def filename_ext(self) -> str:
return f".{self.format.lower()}"
def invoke(self, context, event):
if self.filepath:
return self.execute(context)
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
return ExportHelper.invoke(self, context, event)
def execute(self, context):
from ifcclash import ifcclash
self.props = context.scene.BIMClashProperties
self.props = tool.Clash.get_clash_props()
_, extension = os.path.splitext(self.filepath)
if extension != ".bcf":
@@ -284,19 +275,17 @@ class ExecuteIfcClash(bpy.types.Operator):
return {"FINISHED"}
class SelectIfcClashResults(bpy.types.Operator):
class SelectIfcClashResults(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_ifc_clash_results"
bl_label = "Select IFC Clash Results"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select the clashing IFC geometry stored in a file"
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
return ImportHelper.invoke(self, context, event)
def execute(self, context):
# TODO refactor into new clash results system
@@ -304,7 +293,9 @@ class SelectIfcClashResults(bpy.types.Operator):
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
with open(self.filepath) as f:
clash_sets = json.load(f)
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
clash_props = tool.Clash.get_clash_props()
assert clash_props.active_clash_set
clash_set_name = clash_props.active_clash_set.name
global_ids = []
for clash_set in clash_sets:
if clash_set["name"] != clash_set_name:
@@ -316,7 +307,8 @@ class SelectIfcClashResults(bpy.types.Operator):
global_ids.extend([clash["a_global_id"], clash["b_global_id"]])
for obj in context.visible_objects:
if not obj.BIMObjectProperties.ifc_definition_id:
props = tool.Blender.get_object_bim_props(obj)
if not props.ifc_definition_id:
continue
ifc_file = ""
@@ -335,7 +327,7 @@ class SelectIfcClashResults(bpy.types.Operator):
element_file = self.file
try:
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
element = element_file.by_id(props.ifc_definition_id)
except:
continue
@@ -355,7 +347,7 @@ class SelectClash(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
self.props = context.scene.BIMClashProperties
self.props = tool.Clash.get_clash_props()
clash_set = tool.Clash.get_clash_set(self.props.active_clash_set.name)
active_clash = self.props.active_clash
clash = tool.Clash.get_clash(clash_set, active_clash.a_global_id, active_clash.b_global_id)
@@ -389,13 +381,15 @@ class SmartClashGroup(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.scene.BIMClashProperties.clash_results_path
props = tool.Clash.get_clash_props()
return bool(props.clash_results_path)
def execute(self, context):
from ifcclash import ifcclash
settings = ifcclash.ClashSettings()
self.filepath = bpy.path.ensure_ext(context.scene.BIMClashProperties.clash_results_path, ".json")
props = tool.Clash.get_clash_props()
self.filepath = bpy.path.ensure_ext(props.clash_results_path, ".json")
settings.output = self.filepath
settings.logger = logging.getLogger("Clash")
settings.logger.setLevel(logging.DEBUG)
@@ -405,19 +399,18 @@ class SmartClashGroup(bpy.types.Operator):
clash_sets = json.load(f)
# execute the smart grouping
save_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json")
smart_grouped_clashes = ifc_clasher.smart_group_clashes(
clash_sets, context.scene.BIMClashProperties.smart_clash_grouping_max_distance
)
save_path = bpy.path.ensure_ext(props.smart_grouped_clashes_path, ".json")
smart_grouped_clashes = ifc_clasher.smart_group_clashes(clash_sets, props.smart_clash_grouping_max_distance)
# save smart_groups to json
with open(save_path, "w") as f:
f.write(json.dumps(smart_grouped_clashes))
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
assert props.active_clash_set
clash_set_name = props.active_clash_set.name
# Reset the list of smart_clash_groups for the UI
context.scene.BIMClashProperties.smart_clash_groups.clear()
props.smart_clash_groups.clear()
for clash_set, smart_groups in smart_grouped_clashes.items():
# Only select the clashes that correspond to the actively selected IFC Clash Set
@@ -425,7 +418,7 @@ class SmartClashGroup(bpy.types.Operator):
continue
else:
for smart_group, global_id_pairs in smart_groups[0].items():
new_group = context.scene.BIMClashProperties.smart_clash_groups.add()
new_group = props.smart_clash_groups.add()
new_group.number = f"{smart_group}"
for pair in global_id_pairs:
@@ -443,18 +436,21 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.scene.BIMClashProperties.active_clash_set
props = tool.Clash.get_clash_props()
return bool(props.active_clash_set)
def execute(self, context):
smart_groups_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json")
props = tool.Clash.get_clash_props()
smart_groups_path = bpy.path.ensure_ext(props.smart_grouped_clashes_path, ".json")
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
assert props.active_clash_set
clash_set_name = props.active_clash_set.name
with open(smart_groups_path) as f:
smart_grouped_clashes = json.load(f)
# Reset the list of smart_clash_groups for the UI
context.scene.BIMClashProperties.smart_clash_groups.clear()
props.smart_clash_groups.clear()
for clash_set, smart_groups in smart_grouped_clashes.items():
# Only select the clashes that correspond to the actively selected IFC Clash Set
@@ -462,12 +458,12 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator):
continue
else:
for smart_group, global_id_pairs in smart_groups[0].items():
new_group = context.scene.BIMClashProperties.smart_clash_groups.add()
new_group = props.smart_clash_groups.add()
new_group.number = f"{smart_group}"
for pair in global_id_pairs:
for guid in pair:
new_global_id = new_group.global_ids.add()
new_global_id.guid = guid
new_global_id.name = guid
return {"FINISHED"}
@@ -479,15 +475,19 @@ class SelectSmartGroup(bpy.types.Operator):
@classmethod
def poll(cls, context):
return tool.Ifc.get() and context.visible_objects and context.scene.BIMClashProperties.active_smart_group
props = tool.Clash.get_clash_props()
return tool.Ifc.get() and context.visible_objects and props.active_smart_group
def execute(self, context):
selected_smart_group = context.scene.BIMClashProperties.active_smart_group
products = []
ifc_file = tool.Ifc.get()
props = tool.Clash.get_clash_props()
selected_smart_group = props.active_smart_group
assert selected_smart_group
products: list[ifcopenshell.entity_instance] = []
for global_id in selected_smart_group.global_ids:
try:
products.append(tool.Ifc.get().by_guid(global_id.guid))
except:
products.append(ifc_file.by_guid(global_id.name))
except RuntimeError:
continue
tool.Spatial.select_products(products, unhide=True)
context_override = tool.Blender.get_viewport_context()
+54 -11
View File
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
from bonsai.bim.prop import StrProperty, Attribute, BIMFilterGroup
from bpy.types import PropertyGroup
from bpy.props import (
@@ -29,6 +30,8 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from mathutils import Vector
from typing import TYPE_CHECKING, Literal, Union
class ClashSource(PropertyGroup):
@@ -43,6 +46,10 @@ class ClashSource(PropertyGroup):
name="Mode",
)
if TYPE_CHECKING:
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
mode: Literal["a", "i", "e"]
class Clash(PropertyGroup):
a_global_id: StringProperty(name="A")
@@ -51,9 +58,15 @@ class Clash(PropertyGroup):
b_name: StringProperty(name="B Name")
status: BoolProperty(name="Status", default=False)
if TYPE_CHECKING:
a_global_id: str
b_global_id: str
a_name: str
b_name: str
status: bool
class ClashSet(PropertyGroup):
name: StringProperty(name="Name")
mode: EnumProperty(
items=[
(
@@ -76,11 +89,25 @@ class ClashSet(PropertyGroup):
b: CollectionProperty(name="Group B", type=ClashSource)
clashes: CollectionProperty(name="Clashes", type=Clash)
if TYPE_CHECKING:
mode: Literal["intersection", "collision", "clearance"]
tolerance: float
clearance: float
allow_touching: bool
check_all: bool
a: bpy.types.bpy_prop_collection_idprop[ClashSource]
b: bpy.types.bpy_prop_collection_idprop[ClashSource]
clashes: bpy.types.bpy_prop_collection_idprop[Clash]
class SmartClashGroup(PropertyGroup):
number: StringProperty(name="Number")
global_ids: CollectionProperty(name="GlobalIDs", type=StrProperty)
if TYPE_CHECKING:
number: str
global_ids: bpy.types.bpy_prop_collection_idprop[StrProperty]
class BIMClashProperties(PropertyGroup):
blender_clash_set_a: CollectionProperty(name="Blender Clash Set A", type=StrProperty)
@@ -107,17 +134,33 @@ class BIMClashProperties(PropertyGroup):
subtype="FILE_PATH",
)
@property
def active_clash_set(self):
if self.active_clash_set_index < len(self.clash_sets):
return self.clash_sets[self.active_clash_set_index]
if TYPE_CHECKING:
blender_clash_set_a: bpy.types.bpy_prop_collection_idprop[StrProperty]
blender_clash_set_b: bpy.types.bpy_prop_collection_idprop[StrProperty]
clash_sets: bpy.types.bpy_prop_collection_idprop[ClashSet]
should_create_clash_snapshots: bool
clash_results_path: str
smart_grouped_clashes_path: str
active_clash_set_index: int
active_clash_index: int
smart_clash_groups: bpy.types.bpy_prop_collection_idprop[SmartClashGroup]
active_smart_group_index: int
smart_clash_grouping_max_distance: int
p1: Vector
p2: Vector
active_clash_text: str
export_path: str
@property
def active_smart_group(self):
if self.active_smart_group_index < len(self.smart_clash_groups):
return self.smart_clash_groups[self.active_smart_group_index]
def active_clash_set(self) -> Union[ClashSet, None]:
return tool.Blender.get_active_uilist_element(self.clash_sets, self.active_clash_set_index)
@property
def active_clash(self):
if self.active_clash_index < len(self.active_clash_set.clashes):
return self.active_clash_set.clashes[self.active_clash_index]
def active_smart_group(self) -> Union[SmartClashGroup, None]:
return tool.Blender.get_active_uilist_element(self.smart_clash_groups, self.active_smart_group_index)
@property
def active_clash(self) -> Union[Clash, None]:
if not (clash_set := self.active_clash_set):
return None
return tool.Blender.get_active_uilist_element(clash_set.clashes, self.active_clash_index)
+3 -4
View File
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
import bonsai.bim.helper
from bpy.types import Panel
from bonsai.bim.module.clash.data import ClashData
@@ -36,9 +37,7 @@ class BIM_PT_ifcclash(Panel):
ClashData.load()
layout = self.layout
scene = context.scene
props = scene.BIMClashProperties
props = tool.Clash.get_clash_props()
row = layout.row(align=True)
row.operator("bim.add_clash_set")
@@ -157,7 +156,7 @@ class BIM_PT_smart_clash_manager(Panel):
def draw(self, context):
layout = self.layout
props = context.scene.BIMClashProperties
props = tool.Clash.get_clash_props()
row = layout.row()
layout.label(text="Select clash results to group:")
@@ -145,11 +145,8 @@ class CostClassificationsData(ReferencesData):
@classmethod
def references(cls):
results = []
element = tool.Ifc.get().by_id(
bpy.context.scene.BIMCostProperties.cost_items[
bpy.context.scene.BIMCostProperties.active_cost_item_index
].ifc_definition_id
)
props = tool.Cost.get_cost_props()
element = tool.Ifc.get().by_id(props.cost_items[props.active_cost_item_index].ifc_definition_id)
if element:
for reference in ifcopenshell.util.classification.get_references(element):
data = reference.get_info()
@@ -25,19 +25,16 @@ import ifcopenshell.util.classification
import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.bim.helper
from bpy_extras.io_utils import ImportHelper
from bonsai.bim.ifc import IfcStore
class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator):
class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.load_classification_library"
bl_label = "Load Classification Library"
bl_description = "Load classification library from the provided filepath."
filename_ext = ".ifc"
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def _execute(self, context):
IfcStore.classification_file = ifcopenshell.open(self.filepath)
@@ -460,6 +457,7 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
for prop in blender_properties:
properties[prop.name] = prop.get_value()
# TODO: is this still the correct approach?
if classification_pset.name == "undefined_set":
if "ObjectType" in properties:
if hasattr(element, "ObjectType"):
@@ -338,7 +338,8 @@ class BIM_PT_cost_classifications(Panel, ReferenceUI):
def poll(cls, context):
if not tool.Ifc.get():
return False
return bool(context.scene.BIMCostProperties.cost_items)
props = tool.Cost.get_cost_props()
return bool(props.cost_items)
def draw(self, context):
if not CostClassificationsData.is_loaded:
@@ -161,8 +161,12 @@ class AssignConstraint(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.file = tool.Ifc.get()
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)]
objs = [bpy.data.objects[self.obj]] if self.obj else context.selected_objects
products = [
self.file.by_id(obj_id)
for obj in objs
if (obj_id := tool.Blender.get_object_bim_props(obj).ifc_definition_id)
]
if products:
ifcopenshell.api.run(
"constraint.assign_constraint",
@@ -184,8 +188,12 @@ class UnassignConstraint(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.file = tool.Ifc.get()
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)]
objs = [bpy.data.objects[self.obj]] if self.obj else context.selected_objects
products = [
self.file.by_id(obj_id)
for obj in objs
if (obj_id := tool.Blender.get_object_bim_props(obj).ifc_definition_id)
]
if products:
ifcopenshell.api.run(
"constraint.unassign_constraint",
@@ -79,18 +79,18 @@ class BIM_PT_object_constraints(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id):
return False
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
props = tool.Blender.get_object_bim_props(obj)
return bool(tool.Ifc.get_object_by_identifier(props.ifc_definition_id))
def draw(self, context):
if not ObjectConstraintsData.is_loaded:
ObjectConstraintsData.load()
obj = context.active_object
self.oprops = obj.BIMObjectProperties
assert obj
self.oprops = tool.Blender.get_object_bim_props(obj)
self.sprops = context.scene.BIMConstraintProperties
self.props = obj.BIMObjectConstraintProperties
self.file = tool.Ifc.get()
+11 -6
View File
@@ -66,8 +66,9 @@ class CostSchedulesData:
@classmethod
def schedules(cls):
results = []
if bpy.context.scene.BIMCostProperties.active_cost_schedule_id:
schedule = tool.Ifc.get().by_id(bpy.context.scene.BIMCostProperties.active_cost_schedule_id)
props = tool.Cost.get_cost_props()
if props.active_cost_schedule_id:
schedule = tool.Ifc.get().by_id(props.active_cost_schedule_id)
results.append(
{
"id": schedule.id(),
@@ -88,7 +89,8 @@ class CostSchedulesData:
@classmethod
def is_editing_rates(cls):
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_schedule_id
props = tool.Cost.get_cost_props()
ifc_id = props.active_cost_schedule_id
if not ifc_id:
return
return tool.Ifc.get().by_id(ifc_id).PredefinedType == "SCHEDULEOFRATES"
@@ -256,7 +258,8 @@ class CostSchedulesData:
@classmethod
def cost_quantities(cls):
results = []
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_item_id
props = tool.Cost.get_cost_props()
ifc_id = props.active_cost_item_id
if not ifc_id:
return results
for quantity in tool.Ifc.get().by_id(ifc_id).CostQuantities or []:
@@ -265,7 +268,8 @@ class CostSchedulesData:
@classmethod
def cost_values(cls):
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_item_id
props = tool.Cost.get_cost_props()
ifc_id = props.active_cost_item_id
if not ifc_id:
return []
return ifcopenshell.util.cost.get_cost_values(tool.Ifc.get().by_id(ifc_id))
@@ -324,7 +328,8 @@ class CostItemQuantitiesData:
@classmethod
def process_quantity_names(cls):
active_task_index = bpy.context.scene.BIMWorkScheduleProperties.active_task_index
props = tool.Sequence.get_work_schedule_props()
active_task_index = props.active_task_index
tprops = tool.Sequence.get_task_tree_props()
total_tasks = len(tprops.tasks)
if not total_tasks or active_task_index >= total_tasks:
+24 -15
View File
@@ -21,10 +21,10 @@
import bpy
import ifcopenshell.api
import bonsai.tool as tool
from bpy_extras.io_utils import ImportHelper
from bpy_extras.io_utils import ImportHelper, ExportHelper
import bonsai.tool as tool
import bonsai.core.cost as core
from typing import get_args, TYPE_CHECKING
from typing import get_args, TYPE_CHECKING, Literal
class AddCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
@@ -36,16 +36,18 @@ class AddCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
object_type: bpy.props.StringProperty()
def _execute(self, context):
predefined_type = context.scene.BIMCostProperties.cost_schedule_predefined_types
props = tool.Cost.get_cost_props()
predefined_type = props.cost_schedule_predefined_types
if predefined_type == "USERDEFINED":
predefined_type = self.object_type
core.add_cost_schedule(tool.Ifc, name=self.name, predefined_type=predefined_type)
def draw(self, context):
layout = self.layout
props = tool.Cost.get_cost_props()
layout.prop(self, "name", text="Name")
layout.prop(context.scene.BIMCostProperties, "cost_schedule_predefined_types", text="Type")
if context.scene.BIMCostProperties.cost_schedule_predefined_types == "USERDEFINED":
layout.prop(props, "cost_schedule_predefined_types", text="Type")
if props.cost_schedule_predefined_types == "USERDEFINED":
layout.prop(self, "object_type", text="Object type")
def invoke(self, context, event):
@@ -58,10 +60,11 @@ class EditCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = tool.Cost.get_cost_props()
core.edit_cost_schedule(
tool.Ifc,
tool.Cost,
cost_schedule=tool.Ifc.get().by_id(context.scene.BIMCostProperties.active_cost_schedule_id),
cost_schedule=tool.Ifc.get().by_id(props.active_cost_schedule_id),
)
@@ -533,6 +536,7 @@ class SelectCostScheduleProducts(bpy.types.Operator):
class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
bl_idname = "bim.import_cost_schedule_csv"
bl_label = "Import Cost Schedule CSV"
bl_description = "Import cost schdule from the provided .csv file."
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
@@ -559,7 +563,8 @@ class AddCostColumn(bpy.types.Operator):
@classmethod
def poll(cls, context):
if not context.scene.BIMCostProperties.cost_column:
props = tool.Cost.get_cost_props()
if not props.cost_column:
cls.poll_message_set("Cost column name is empty")
return False
return True
@@ -678,7 +683,7 @@ class CalculateCostItemResourceValue(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
class ExportCostSchedules(bpy.types.Operator):
class ExportCostSchedules(bpy.types.Operator, ExportHelper):
bl_idname = "bim.export_cost_schedules"
bl_label = "Export Cost Schedule"
bl_options = {"REGISTER", "UNDO"}
@@ -691,6 +696,13 @@ class ExportCostSchedules(bpy.types.Operator):
default=True,
)
if TYPE_CHECKING:
format: Literal["CSV", "XLSX", "ODS"]
@property
def filename_ext(self) -> str:
return f".{self.format.lower()}"
def execute(self, context):
cost_schedule = tool.Ifc.get().by_id(self.cost_schedule) if self.cost_schedule else None
r = core.export_cost_schedules(
@@ -701,9 +713,7 @@ class ExportCostSchedules(bpy.types.Operator):
return {"FINISHED"}
def invoke(self, context, event):
wm = context.window_manager
wm.fileselect_add(self)
return {"RUNNING_MODAL"}
return ExportHelper.invoke(self, context, event)
def draw(self, context):
self.layout.label(text="Choose a format")
@@ -740,15 +750,14 @@ class LoadProductCostItems(bpy.types.Operator):
@classmethod
def poll(cls, context):
if not tool.Ifc.get() or not (obj := context.active_object) or not (obj.BIMObjectProperties.ifc_definition_id):
if not tool.Ifc.get() or not (obj := context.active_object) or not (tool.Blender.get_ifc_definition_id(obj)):
cls.poll_message_set("No IFC object is active.")
return False
return True
def execute(self, context):
core.load_product_cost_items(
tool.Cost, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id)
)
obj = context.active_object
core.load_product_cost_items(tool.Cost, product=tool.Ifc.get_entity(obj))
return {"FINISHED"}
+81 -4
View File
@@ -33,6 +33,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal
def get_schedule_of_rates(self, context):
@@ -78,8 +79,8 @@ def update_active_cost_item_index(self, context):
CostClassificationsData.load()
def update_cost_item_identification(self, context):
props = context.scene.BIMCostProperties
def update_cost_item_identification(self: "CostItem", context: bpy.types.Context):
props = tool.Cost.get_cost_props()
if not props.is_cost_update_enabled or self.identification == "XXX":
return
self.file = tool.Ifc.get()
@@ -93,8 +94,8 @@ def update_cost_item_identification(self, context):
attribute.string_value = self.identification
def update_cost_item_name(self, context):
props = context.scene.BIMCostProperties
def update_cost_item_name(self: "CostItem", context: bpy.types.Context) -> None:
props = tool.Cost.get_cost_props()
if not props.is_cost_update_enabled or self.name == "Unnamed":
return
self.file = tool.Ifc.get()
@@ -142,6 +143,14 @@ class CostItem(PropertyGroup):
is_expanded: BoolProperty(name="Is Expanded")
level_index: IntProperty(name="Level Index")
if TYPE_CHECKING:
name: str
identification: str
ifc_definition_id: int
has_children: bool
is_expanded: bool
level_index: int
class CostItemQuantity(PropertyGroup):
name: StringProperty(name="Name")
@@ -150,11 +159,22 @@ class CostItemQuantity(PropertyGroup):
unit_symbol: StringProperty(name="Unit Symbol")
total_cost_quantity: FloatProperty(name="Total Quantity")
if TYPE_CHECKING:
name: str
ifc_definition_id: int
total_quantity: float
unit_symbol: str
total_cost_quantity: float
class CostItemType(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
if TYPE_CHECKING:
name: str
ifc_definition_id: int
def update_cost_item_parent(self, context):
cost_item = tool.Cost.get_highlighted_cost_item()
@@ -176,6 +196,9 @@ def update_active_cost_item_resources(self, context):
class ScheduleColumn(PropertyGroup):
schedule_id: IntProperty()
if TYPE_CHECKING:
schedule_id: int
class BIMCostProperties(PropertyGroup):
cost_schedule_predefined_types: EnumProperty(
@@ -249,3 +272,57 @@ class BIMCostProperties(PropertyGroup):
custom_currency: StringProperty(
name="Custom Currency", default="USD", description="Custom Currency in ISO 4217 format"
)
if TYPE_CHECKING:
cost_schedule_predefined_types: str
is_cost_update_enabled: bool
cost_schedule_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
is_editing: str
active_cost_schedule_id: int
cost_items: bpy.types.bpy_prop_collection_idprop[CostItem]
active_cost_item_id: int
cost_item_editing_type: str
active_cost_item_index: int
cost_item_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
contracted_cost_items: str
quantity_types: str
product_quantity_names: str
process_quantity_names: str
resource_quantity_names: str
active_cost_item_quantity_id: int
quantity_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
cost_types: Literal["FIXED", "SUM", "CATEGORY"]
cost_category: str
fixed_cost_value: float
active_cost_value_id: int
cost_value_editing_type: str
cost_value_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
cost_value_formula: str
cost_column: str
should_show_column_ui: bool
should_show_currency_ui: bool
columns: bpy.types.bpy_prop_collection_idprop[StrProperty]
columns_storage: bpy.types.bpy_prop_collection_idprop[ScheduleColumn]
active_column_index: int
cost_item_products: bpy.types.bpy_prop_collection_idprop[CostItemQuantity]
active_cost_item_product_index: int
cost_item_processes: bpy.types.bpy_prop_collection_idprop[CostItemQuantity]
active_cost_item_process_index: int
cost_item_resources: bpy.types.bpy_prop_collection_idprop[CostItemQuantity]
active_cost_item_resource_index: int
cost_item_type_products: bpy.types.bpy_prop_collection_idprop[CostItemType]
active_cost_item_type_product_index: int
schedule_of_rates: str
cost_item_rates: bpy.types.bpy_prop_collection_idprop[CostItem]
active_cost_item_rate_index: int
contracted_cost_item_rates: str
product_cost_items: bpy.types.bpy_prop_collection_idprop[CostItemQuantity]
active_product_cost_item_index: int
enable_reorder: bool
show_nested_elements: bool
show_nested_tasks: bool
show_nested_resources: bool
change_cost_item_parent: bool
show_cost_item_operators: bool
currency: str
custom_currency: str
+45 -35
View File
@@ -16,13 +16,17 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import bonsai.bim.helper
import bonsai.bim.module.cost.prop as CostProp
import bonsai.tool as tool
from bpy.types import Panel, UIList
from bonsai.bim.module.cost.data import CostSchedulesData
from typing import Any
from typing import Any, TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.cost.prop import BIMCostProperties, CostItemQuantity
class BIM_PT_cost_schedules(Panel):
@@ -43,7 +47,7 @@ class BIM_PT_cost_schedules(Panel):
if not CostSchedulesData.is_loaded:
CostSchedulesData.load()
self.props = context.scene.BIMCostProperties
self.props = tool.Cost.get_cost_props()
if not self.props.active_cost_schedule_id:
row = self.layout.row(align=True)
if CostSchedulesData.data["total_cost_schedules"]:
@@ -299,7 +303,7 @@ class BIM_PT_cost_item_types(Panel):
@classmethod
def poll(cls, context):
props = context.scene.BIMCostProperties
props = tool.Cost.get_cost_props()
total_cost_items = len(props.cost_items)
if not props.active_cost_schedule_id:
return False
@@ -312,7 +316,7 @@ class BIM_PT_cost_item_types(Panel):
return False
def draw(self, context):
self.props = context.scene.BIMCostProperties
self.props = tool.Cost.get_cost_props()
cost_item = self.props.cost_items[self.props.active_cost_item_index]
grid = self.layout.grid_flow(columns=3, even_columns=True)
@@ -348,8 +352,8 @@ class BIM_PT_cost_item_types(Panel):
row2 = col.row()
row2.template_list(
"BIM_UL_cost_item_quantities",
"",
BIM_UL_cost_item_quantities.__name__,
BIM_UL_cost_item_quantities.__name__ + "_cost_item_processes",
self.props,
"cost_item_processes",
self.props,
@@ -365,8 +369,8 @@ class BIM_PT_cost_item_types(Panel):
row2 = col.row()
row2.template_list(
"BIM_UL_cost_item_quantities",
"",
BIM_UL_cost_item_quantities.__name__,
BIM_UL_cost_item_quantities.__name__ + "_cost_item_resources",
self.props,
"cost_item_resources",
self.props,
@@ -385,7 +389,7 @@ class BIM_PT_cost_item_quantities(Panel):
@classmethod
def poll(cls, context):
props = context.scene.BIMCostProperties
props = tool.Cost.get_cost_props()
total_cost_items = len(props.cost_items)
if not props.active_cost_schedule_id:
return False
@@ -398,7 +402,7 @@ class BIM_PT_cost_item_quantities(Panel):
return False
def draw(self, context):
self.props = context.scene.BIMCostProperties
self.props = tool.Cost.get_cost_props()
cost_item = self.props.cost_items[self.props.active_cost_item_index]
@@ -435,8 +439,8 @@ class BIM_PT_cost_item_quantities(Panel):
row2.prop(self.props, "show_nested_elements", text="Show nested")
row2 = col.row()
row2.template_list(
"BIM_UL_cost_item_quantities",
"",
BIM_UL_cost_item_quantities.__name__,
BIM_UL_cost_item_quantities.__name__ + "_cost_item_products",
self.props,
"cost_item_products",
self.props,
@@ -462,7 +466,7 @@ class BIM_PT_cost_item_quantities(Panel):
row2.label(text="Tasks ({})".format(total_cost_item_processes))
tprops = tool.Sequence.get_task_tree_props()
wprops = context.scene.BIMWorkScheduleProperties
wprops = tool.Sequence.get_work_schedule_props()
if tprops.tasks and wprops.active_task_index < len(tprops.tasks):
if has_quantity_names:
op = row2.operator("bim.assign_cost_item_quantity", text="", icon="PROPERTIES")
@@ -480,8 +484,8 @@ class BIM_PT_cost_item_quantities(Panel):
row2 = col.row()
row2.template_list(
"BIM_UL_cost_item_quantities",
"",
BIM_UL_cost_item_quantities.__name__,
BIM_UL_cost_item_quantities.__name__ + "_cost_item_processes",
self.props,
"cost_item_processes",
self.props,
@@ -528,8 +532,8 @@ class BIM_PT_cost_item_quantities(Panel):
row2 = col.row()
row2.template_list(
"BIM_UL_cost_item_quantities",
"",
BIM_UL_cost_item_quantities.__name__,
BIM_UL_cost_item_quantities.__name__ + "_cost_item_resources",
self.props,
"cost_item_resources",
self.props,
@@ -557,7 +561,7 @@ class BIM_PT_cost_item_rates(Panel):
@classmethod
def poll(cls, context):
props = context.scene.BIMCostProperties
props = tool.Cost.get_cost_props()
total_cost_items = len(props.cost_items)
if not props.active_cost_schedule_id:
return False
@@ -570,7 +574,7 @@ class BIM_PT_cost_item_rates(Panel):
return False
def draw(self, context):
self.props = context.scene.BIMCostProperties
self.props = tool.Cost.get_cost_props()
row = self.layout.row(align=True)
row.prop(self.props, "schedule_of_rates", text="")
if self.props.active_cost_item_rate_index < len(self.props.cost_item_rates):
@@ -606,13 +610,14 @@ class BIM_UL_cost_items_trait:
split2.label(text="Quantity")
split2.label(text="Value")
for column in bpy.context.scene.BIMCostProperties.columns:
props = tool.Cost.get_cost_props()
for column in props.columns:
split2.label(text=column.name)
split2.label(text="Total Cost")
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
self.props = context.scene.BIMCostProperties
self.props = tool.Cost.get_cost_props()
cost_item = CostSchedulesData.data["cost_items"][item.ifc_definition_id]
row = layout.row(align=True)
@@ -708,18 +713,16 @@ class BIM_UL_cost_items_trait:
class BIM_UL_cost_items(BIM_UL_cost_items_trait, UIList):
def __init__(self):
self.contract_operator = "bim.contract_cost_item"
self.expand_operator = "bim.expand_cost_item"
contract_operator = "bim.contract_cost_item"
expand_operator = "bim.expand_cost_item"
class BIM_UL_cost_item_rates(BIM_UL_cost_items_trait, UIList):
# A schedule of rates UIList is identical to a regular cost items UIList but
# we want a separate UIList instance so that you can browse both lists
# independently in Blender. So we use a trait.
def __init__(self):
self.contract_operator = "bim.contract_cost_item_rate"
self.expand_operator = "bim.expand_cost_item_rate"
contract_operator = "bim.contract_cost_item_rate"
expand_operator = "bim.expand_cost_item_rate"
def draw_quantity_column(self, layout, cost_item):
self.draw_uom_column(layout, cost_item)
@@ -730,7 +733,6 @@ class BIM_UL_cost_item_rates(BIM_UL_cost_items_trait, UIList):
class BIM_UL_cost_columns(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
props = context.scene.BIMCostProperties
if item:
row = layout.row(align=True)
row.prop(item, "name", emboss=False, text="")
@@ -739,7 +741,7 @@ class BIM_UL_cost_columns(UIList):
class BIM_UL_cost_item_types(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
props = context.scene.BIMCostProperties
props = tool.Cost.get_cost_props()
cost_item = props.cost_items[props.active_cost_item_index]
if item:
@@ -751,9 +753,17 @@ class BIM_UL_cost_item_types(UIList):
class BIM_UL_cost_item_quantities(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
props = context.scene.BIMCostProperties
cost_item = props.cost_items[props.active_cost_item_index]
def draw_item(
self,
context: bpy.types.Context,
layout: bpy.types.UILayout,
data: BIMCostProperties,
item: CostItemQuantity,
icon,
active_data,
active_propname,
):
cost_item = data.cost_items[data.active_cost_item_index]
if item:
row = layout.row(align=True)
op = row.operator("bim.select_product", text="", icon="RESTRICT_SELECT_OFF")
@@ -790,15 +800,15 @@ class BIM_PT_Costing_Tools(Panel):
bl_parent_id = "BIM_PT_tab_cost"
def draw(self, context):
self.props = context.scene.BIMCostProperties
props = tool.Cost.get_cost_props()
row = self.layout.row()
row.operator("bim.load_product_cost_items", icon="FILE_REFRESH")
row = self.layout.row()
row.template_list(
"BIM_UL_product_cost_items",
"",
self.props,
props,
"product_cost_items",
self.props,
props,
"active_product_cost_item_index",
)
@@ -18,9 +18,13 @@
import bpy
from bpy.types import PropertyGroup
from typing import TYPE_CHECKING
class BIMCoveringProperties(PropertyGroup):
ceiling_height: bpy.props.FloatProperty(
name="ceiling_height", default=2.7, subtype="DISTANCE", description="Ceiling height"
)
if TYPE_CHECKING:
ceiling_height: float
@@ -24,6 +24,7 @@ import bonsai.tool as tool
from bonsai.bim.helper import prop_with_search
from bonsai.bim.module.model.data import AuthoringData
from bpy.types import WorkSpaceTool
from functools import partial
class CoveringTool(WorkSpaceTool):
@@ -45,9 +46,7 @@ class CoveringTool(WorkSpaceTool):
CoveringToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
def add_layout_hotkey(layout: bpy.types.UILayout, text: str, hotkey: str, description: str) -> None:
args = ("covering", layout, text, hotkey, description)
tool.Blender.add_layout_hotkey_operator(*args)
add_layout_hotkey = partial(tool.Blender.add_layout_hotkey_operator, tool_name="covering", module_name=__name__)
class CoveringToolUI:
@@ -55,7 +54,7 @@ class CoveringToolUI:
def draw(cls, context, layout, ifc_element_type=None):
cls.layout = layout
cls.props = tool.Model.get_model_props()
cls.covering_props = context.scene.BIMCoveringProperties
cls.covering_props = tool.Covering.get_covering_props()
row = cls.layout.row(align=True)
if not tool.Ifc.get():
@@ -88,7 +87,7 @@ class CoveringToolUI:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
row.operator("bim.add_constr_type_instance", text="Add")
row.operator("bim.add_occurrence", text="Add")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
@@ -136,8 +135,8 @@ class CoveringToolUI:
if AuthoringData.data["ifc_classes"]:
if cls.props.ifc_class:
box = cls.layout.box()
if AuthoringData.data["type_thumbnail"]:
box.template_icon(icon_value=AuthoringData.data["type_thumbnail"], scale=5)
if thumbnail := AuthoringData.data["relating_type_data"].get("thumbnail"):
box.template_icon(icon_value=thumbnail, scale=5)
else:
op = box.operator("bim.load_type_thumbnails", text="Load Thumbnails", icon="FILE_REFRESH")
op.ifc_class = cls.props.ifc_class
@@ -145,8 +144,8 @@ class CoveringToolUI:
class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.covering_hotkey"
bl_label = "Hotkey"
bl_options = {"REGISTER", "UNDO"}
bl_label = ""
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
hotkey: bpy.props.StringProperty()
description: bpy.props.StringProperty()
@@ -159,12 +158,11 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
return operator.description or ""
def _execute(self, context):
# self.props = context.scene.BIMCoveringProperties
self.props = tool.Covering.get_covering_props()
getattr(self, f"hotkey_{self.hotkey}")()
def invoke(self, context, event):
# https://blender.stackexchange.com/questions/276035/how-do-i-make-operators-remember-their-property-values-when-called-from-a-hotkey
# self.props = context.scene.BIMSpatialProperties
return self.execute(context)
def draw(self, context):
@@ -175,22 +173,22 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(active_obj)
container = tool.Root.get_default_container()
if AuthoringData.data["predefined_type"] == "FLOORING":
if AuthoringData.data["relating_type_data"].get("predefined_type") == "FLOORING":
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
bpy.ops.bim.add_instance_flooring_coverings_from_walls()
elif container:
bpy.ops.bim.add_instance_flooring_covering_from_cursor()
else:
bpy.ops.bim.add_constr_type_instance()
elif AuthoringData.data["predefined_type"] == "CEILING":
bpy.ops.bim.add_occurrence()
elif AuthoringData.data["relating_type_data"].get("predefined_type") == "CEILING":
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
bpy.ops.bim.add_instance_ceiling_coverings_from_walls()
elif container:
bpy.ops.bim.add_instance_ceiling_covering_from_cursor()
else:
bpy.ops.bim.add_constr_type_instance()
bpy.ops.bim.add_occurrence()
else:
bpy.ops.bim.add_constr_type_instance()
bpy.ops.bim.add_occurrence()
def hotkey_S_G(self):
active_obj = bpy.context.active_object
@@ -181,12 +181,12 @@ class RunAnalysis(bpy.types.Operator):
if modifier.type == "TRIANGULATE":
return True
def get_covetool_category(self, obj):
def get_covetool_category(self, obj: bpy.types.Object):
if not hasattr(obj, "data") or not isinstance(obj.data, bpy.types.Mesh):
return
if not obj.BIMObjectProperties.ifc_definition_id:
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
return
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
element = self.file.by_id(ifc_id)
ifc_class = element.is_a()
if "IfcSlab" in ifc_class:
return "floors"
+40 -43
View File
@@ -27,6 +27,7 @@ import ifcopenshell
import ifcopenshell.util.selector
import bonsai.tool as tool
import bonsai.bim.module.drawing.scheduler as scheduler
from bpy_extras.io_utils import ExportHelper, ImportHelper
from bonsai.bim.handler import refresh_ui_data
from typing import TYPE_CHECKING
from collections import Counter
@@ -42,7 +43,8 @@ class AddCsvAttribute(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
attribute = context.scene.CsvProperties.csv_attributes.add()
props = tool.Blender.get_csv_props()
props.csv_attributes.add()
return {"FINISHED"}
@@ -53,7 +55,8 @@ class RemoveCsvAttribute(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
context.scene.CsvProperties.csv_attributes.remove(self.index)
props = tool.Blender.get_csv_props()
props.csv_attributes.remove(self.index)
return {"FINISHED"}
@@ -64,7 +67,8 @@ class RemoveAllCsvAttributes(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
context.scene.CsvProperties.csv_attributes.clear()
props = tool.Blender.get_csv_props()
props.csv_attributes.clear()
return {"FINISHED"}
@@ -76,8 +80,9 @@ class ReorderCsvAttribute(bpy.types.Operator):
new_index: bpy.props.IntProperty()
def execute(self, context):
old = context.scene.CsvProperties.csv_attributes[self.old_index]
new = context.scene.CsvProperties.csv_attributes[self.new_index]
props = tool.Blender.get_csv_props()
old = props.csv_attributes[self.old_index]
new = props.csv_attributes[self.new_index]
props = ["name", "header", "sort", "group", "varies_value", "summary", "formatting"]
for prop in props:
value = getattr(new, prop)
@@ -86,16 +91,16 @@ class ReorderCsvAttribute(bpy.types.Operator):
return {"FINISHED"}
class ImportCsvAttributes(bpy.types.Operator):
class ImportCsvAttributes(bpy.types.Operator, ImportHelper):
bl_idname = "bim.import_csv_attributes"
bl_label = "Load CSV Settings"
bl_description = "Import a json template for CSV export"
bl_options = {"REGISTER", "UNDO"}
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filename_ext = ".json"
def execute(self, context):
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
data = json.load(open(self.filepath))
tool.Search.import_filter_query(data["query"], props.filter_groups)
@@ -121,22 +126,17 @@ class ImportCsvAttributes(bpy.types.Operator):
setattr(new, prop, attribute[prop])
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ExportCsvAttributes(bpy.types.Operator):
class ExportCsvAttributes(bpy.types.Operator, ExportHelper):
bl_idname = "bim.export_csv_attributes"
bl_label = "Save CSV Settings"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Save a json template for CSV export"
filename_ext = ".json"
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
settings = {}
for prop in [
@@ -175,40 +175,42 @@ class ExportCsvAttributes(bpy.types.Operator):
return {"FINISHED"}
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ExportIfcCsv(bpy.types.Operator):
class ExportIfcCsv(bpy.types.Operator, ExportHelper):
bl_idname = "bim.export_ifccsv"
bl_label = "Export IFC"
bl_description = "Export IFC data as a spreadsheet."
filename_ext = ".csv"
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
@classmethod
def poll(cls, context):
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
if not props.should_load_from_memory and not props.csv_ifc_file:
cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.")
return False
return True
@classmethod
def description(cls, context, properties):
props = tool.Blender.get_csv_props()
if props.format == "web":
return "Open Web UI for spreadsheet data export."
return f"Export IFC data as a spreadsheet by the provided filepath in '{props.format}' format ."
def invoke(self, context, event):
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
if props.format == "web":
return self.execute(context)
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
self.filter_glob = f"*.{props.format}"
self.filename_ext = f".{props.format}"
return ExportHelper.invoke(self, context, event)
def execute(self, context):
import ifccsv
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
self.filepath = bpy.path.ensure_ext(self.filepath, f".{props.format}")
if props.should_load_from_memory:
ifc_file = tool.Ifc.get()
@@ -287,16 +289,16 @@ class ExportIfcCsv(bpy.types.Operator):
]
class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator):
class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.import_ifccsv"
bl_label = "Import to IFC"
bl_description = "Import IFC data from a spreadsheet."
bl_options = {"REGISTER", "UNDO"}
filter_glob: bpy.props.StringProperty(default="*.csv;*.ods;*.xlsx", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
@classmethod
def poll(cls, context):
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
if not props.should_load_from_memory and not props.csv_ifc_file:
cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.")
return False
@@ -304,14 +306,12 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator):
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".csv")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
return ImportHelper.invoke(self, context, event)
def _execute(self, context):
import ifccsv
props = context.scene.CsvProperties
props = tool.Blender.get_csv_props()
if props.should_load_from_memory:
ifc_file = tool.Ifc.get()
else:
@@ -337,18 +337,15 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
class SelectCsvIfcFile(bpy.types.Operator):
class SelectCsvIfcFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_csv_ifc_file"
bl_label = "Select CSV IFC File"
bl_description = "Select IFC file for spreadsheet import/export."
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".ifc"
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
context.scene.CsvProperties.csv_ifc_file = self.filepath
props = tool.Blender.get_csv_props()
props.csv_ifc_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
+33
View File
@@ -29,6 +29,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal
class CsvAttribute(PropertyGroup):
@@ -59,6 +60,15 @@ class CsvAttribute(PropertyGroup):
)
formatting: StringProperty(default="{{value}}", name="Formatting")
if TYPE_CHECKING:
name: str
header: str
sort: Literal["NONE", "ASC", "DESC"]
group: Literal["NONE", "GROUP", "CONCAT", "VARIES", "SUM", "AVERAGE", "MIN", "MAX"]
varies_value: str
summary: Literal["NONE", "SUM", "AVERAGE", "MIN", "MAX"]
formatting: str
class CsvProperties(PropertyGroup):
csv_ifc_file: StringProperty(default="", name="IFC File")
@@ -104,3 +114,26 @@ class CsvProperties(PropertyGroup):
name="Load from Memory",
description="Use IFC file currently loaded in Bonsai",
)
if TYPE_CHECKING:
csv_ifc_file: str
ifc_selector: str
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
csv_attributes: bpy.types.bpy_prop_collection_idprop[CsvAttribute]
should_generate_svg: bool
should_preserve_existing: bool
include_global_id: bool
null_value: str
empty_value: str
true_value: str
false_value: str
concat_value: str
csv_delimiter: Literal["NONE", "ASC", "DESC"]
format: Literal["csv", "xlsx", "ods", "web"]
csv_custom_delimiter: str
should_show_settings: bool
should_show_sort: bool
should_show_group: bool
should_show_summary: bool
should_show_formatting: bool
should_load_from_memory: bool
+5 -4
View File
@@ -33,9 +33,7 @@ class BIM_PT_ifccsv(Panel):
def draw(self, context):
layout = self.layout
scene = context.scene
props = scene.CsvProperties
props = tool.Blender.get_csv_props()
if tool.Ifc.get():
row = layout.row(align=True)
@@ -126,5 +124,8 @@ class BIM_PT_ifccsv(Panel):
row.operator("bim.remove_csv_attribute", icon="X", text="").index = index
row = layout.row(align=True)
row.operator("bim.export_ifccsv", icon="EXPORT", text="Export IFC to " + props.format.upper())
if props.format == "web":
row.operator("bim.export_ifccsv", icon="EXPORT", text="Open Web UI")
else:
row.operator("bim.export_ifccsv", icon="EXPORT", text="Export IFC to " + props.format.upper())
row.operator("bim.import_ifccsv", icon="IMPORT")
@@ -44,6 +44,7 @@ classes = (
operator.SelectHighPolygonMeshes,
operator.SelectHighestPolygonMeshes,
operator.ToggleDetailedIOSLogs,
operator.ValidateIfcAssets,
operator.ValidateIfcFile,
prop.BIMDebugProperties,
ui.BIM_PT_debug,
+96 -18
View File
@@ -23,6 +23,7 @@ import time
import random
import logging
import subprocess
import platform
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.geom
@@ -36,6 +37,8 @@ import bonsai.core.profile
import bonsai.core.type
import bonsai.bim.handler
import bonsai.bim.import_ifc as import_ifc
from collections import defaultdict
from bpy_extras.io_utils import ImportHelper, ExportHelper
from pathlib import Path
from bonsai import get_debug_info, format_debug_info
from bonsai.bim.ifc import IfcStore
@@ -150,6 +153,67 @@ class ValidateIfcFile(bpy.types.Operator):
return {"FINISHED"}
class ValidateIfcAssets(bpy.types.Operator):
bl_idname = "bim.validate_ifc_assets"
bl_label = "Validate IFC Assets"
bl_description = (
"Run Bonsai validation for IFC assets.\n\n"
"There's an internal Bonsai convention to treat some IFC assets "
"as unique based on their name (e.g. profiles, materials, styles).\n"
"Though it's not required by IFC, it is a generally good practice "
"to keep asset names unique and it also helps with various issues.\n"
"If it's not conformed, it could lead to duplicated assets or "
"the opposite - different assets of the same name treated as one."
)
bl_options = set()
@classmethod
def poll(cls, context):
if not tool.Ifc.get():
cls.poll_message_set("IFC file is not loaded.")
return False
return True
def execute(self, context):
ifc_file = tool.Ifc.get()
ifc_classes = {
"IfcMaterial": "Name",
"IfcProfileDef": "ProfileName",
"IfcPresentationStyle": "Name",
}
issues_found = False
unique_assets: defaultdict[str, list[ifcopenshell.entity_instance]]
for ifc_class, name_attr in ifc_classes.items():
unique_assets = defaultdict(list)
for asset in ifc_file.by_type(ifc_class):
asset_name: Union[str, None] = getattr(asset, name_attr)
if asset_name is None:
continue
unique_assets[asset_name].append(asset)
msg = ""
for asset_name, assets in unique_assets.items():
if len(assets) == 1:
continue
msg += f"{ifc_class} name '{asset_name}' is used by multiple assets:\n"
for asset in assets:
msg += f"- {asset}\n"
if msg:
issues_found = True
msg = f"Found issues validating {ifc_class} assets.\n" + msg
print(msg)
if issues_found:
self.report({"INFO"}, "Check asset validation results in the system console.")
else:
self.report({"INFO"}, "No asset validation issues found.")
return {"FINISHED"}
class ProfileImportIFC(bpy.types.Operator):
profile_filename = "blender.prof"
bl_idname = "bim.profile_import_ifc"
@@ -382,7 +446,7 @@ class InspectFromObject(bpy.types.Operator):
def get_active_object_ifc_definition(cls, context: bpy.types.Context) -> Union[int, None]:
obj = context.active_object
assert obj
if ifc_id := obj.BIMObjectProperties.ifc_definition_id:
if ifc_id := tool.Blender.get_ifc_definition_id(obj):
return ifc_id
if (
(data := obj.data)
@@ -446,12 +510,11 @@ class ParseExpress(bpy.types.Operator):
return {"FINISHED"}
class SelectExpressFile(bpy.types.Operator):
class SelectExpressFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_express_file"
bl_label = "Select Express File"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select an IFC EXPRESS definition"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.exp", options={"HIDDEN"})
def execute(self, context):
@@ -460,10 +523,6 @@ class SelectExpressFile(bpy.types.Operator):
props.express_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class PurgeHdf5Cache(bpy.types.Operator):
bl_idname = "bim.purge_hdf5_cache"
@@ -513,6 +572,7 @@ class PrintUnusedElementStats(bpy.types.Operator):
if self.ignore_styled_items:
ignore_classes += ["IfcStyledItem"]
ignore_classes += [
"IfcDocumentReference", # Document references for sheet elements (drawings, schedules, etc).
"IfcIndexedColourMap", # Only referenced by inverse attributes.
"IfcIndexedTextureMap", # Only referenced by inverse attributes.
]
@@ -522,21 +582,22 @@ class PrintUnusedElementStats(bpy.types.Operator):
return {"FINISHED"}
class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator, ExportHelper):
bl_idname = "bim.purge_unused_elements_by_class"
bl_label = "Purge Unused Elements By Class"
bl_description = (
"Will find all elements of class that have no inverse references and will remove them, use very carefully.\n"
"If IFC class is provided in neighbour field, will purge only elemnts of the provided class. Otherwise will purge all white-listed elements.\n"
"If IFC class is provided in neighbour field, will purge only elements of the provided class. Otherwise will purge all white-listed elements.\n"
"ALT+CLICK to provide a path where to save the IFC file with the removed elements (note changes will be applied to the current IFC too)"
)
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".ifc"
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.alt:
context.window_manager.fileselect_add(self)
return ExportHelper.invoke(self, context, event)
return self.execute(context)
@classmethod
@@ -613,18 +674,30 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
"IfcUnitAssignment",
"IfcVirtualGridIntersection",
]
whitelist_exceptions = {
# Document references for sheet elements (drawings, schedules, etc).
"IfcExternalReference": ("IfcDocumentReference",),
}
total_purged = 0
schema = tool.Ifc.schema()
while True:
total_batches = 0
print("*" * 100)
total_batch_purged = 0
for ifc_class in whitelisted_classes:
total_class_purged = 0
# Ensure class is present in the schema.
try:
elements = tool.Ifc.get().by_type(ifc_class)
except:
continue # Probably not in this schema?
schema.declaration_by_name(ifc_class)
except RuntimeError:
continue
elements = tool.Ifc.get().by_type(ifc_class)
if ifc_class in whitelist_exceptions:
elements = [e for e in elements if not any(e.is_a(c) for c in whitelist_exceptions[ifc_class])]
to_purge = set()
for element in elements:
try:
@@ -691,13 +764,13 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
if purged == 0:
return
scene = context.scene
if object_type == "PROFILE":
props = tool.Profile.get_profile_props()
if props.is_editing:
bpy.ops.bim.load_profiles()
elif object_type == "STYLE":
if scene.BIMStylesProperties.is_editing:
props = tool.Style.get_style_props()
if props.is_editing:
bpy.ops.bim.load_styles()
elif object_type == "MATERIAL":
props = tool.Material.get_material_props()
@@ -741,13 +814,13 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
if merged == 0:
return
scene = context.scene
if object_type == "PROFILE":
props = tool.Profile.get_profile_props()
if props.is_editing:
bpy.ops.bim.load_profiles()
elif object_type == "STYLE":
if scene.BIMStylesProperties.is_editing:
props = tool.Style.get_style_props()
if props.is_editing:
bpy.ops.bim.load_styles()
elif object_type == "MATERIAL":
props = tool.Material.get_material_props()
@@ -950,7 +1023,12 @@ class RestartBlender(bpy.types.Operator):
ms_store_app_id = tool.Blender.get_microsoft_store_app_id()
if not ms_store_app_id:
path = bpy.app.binary_path
os.execv(path, sys.argv)
if platform.system() == "Windows":
args = sys.argv[1:]
command_line = subprocess.list2cmdline([path] + args)
os.execv(path, [command_line])
else:
os.execv(path, sys.argv)
else:
# Microsoft apps do not allow launching blender.exe directly
# since Blender folder is kind of private.
+2
View File
@@ -50,6 +50,8 @@ class BIM_PT_debug(Panel):
row.prop(props, "package_name", text="")
row.operator("bim.pip_install", icon="EVENT_PAGEDOWN").name = props.package_name
layout.operator("bim.validate_ifc_assets", icon="CHECKMARK")
row = layout.row()
row.operator("bim.reload_ifc_file", text="Incrementally Reload Changes")
+1 -1
View File
@@ -43,7 +43,7 @@ class DiffData:
@classmethod
def diff_json(cls):
props = bpy.context.scene.DiffProperties
props = tool.Blender.get_diff_props()
if not props.diff_json_file:
cls.diff = None
return
+27 -36
View File
@@ -24,24 +24,23 @@ import ifcopenshell
import bonsai.bim.handler
import bonsai.bim.import_ifc
import bonsai.tool as tool
from bpy_extras.io_utils import ImportHelper, ExportHelper
from bonsai.bim.ifc import IfcStore
class SelectDiffJsonFile(bpy.types.Operator):
class SelectDiffJsonFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_diff_json_file"
bl_label = "Select Diff JSON File"
bl_description = "Select filepath for IFC diff results."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
filename_ext = ".json"
def execute(self, context):
context.scene.DiffProperties.diff_json_file = self.filepath
props = tool.Blender.get_diff_props()
props.diff_json_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class VisualiseDiff(bpy.types.Operator):
bl_idname = "bim.visualise_diff"
@@ -50,7 +49,8 @@ class VisualiseDiff(bpy.types.Operator):
def execute(self, context):
ifc_file = tool.Ifc.get()
with open(context.scene.DiffProperties.diff_json_file, "r") as file:
props = tool.Blender.get_diff_props()
with open(props.diff_json_file, "r") as file:
diff = json.load(file)
for obj in context.visible_objects:
obj.color = (1.0, 1.0, 1.0, 1.0)
@@ -65,7 +65,7 @@ class VisualiseDiff(bpy.types.Operator):
obj.color = (0.0, 0.0, 0.7, 1.0)
continue
if not obj.BIMObjectProperties.ifc_definition_id:
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
continue
ifc_file = ""
@@ -84,7 +84,7 @@ class VisualiseDiff(bpy.types.Operator):
element_file = ifc_file
try:
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
element = element_file.by_id(ifc_id)
except:
continue
global_id = getattr(element, "GlobalId", None)
@@ -101,55 +101,45 @@ class VisualiseDiff(bpy.types.Operator):
return {"FINISHED"}
class SelectDiffOldFile(bpy.types.Operator):
class SelectDiffOldFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_diff_old_file"
bl_label = "Select Diff Old File"
bl_description = "Select filepath for an old IFC file to compare."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
filename_ext = ".ifc"
def execute(self, context):
context.scene.DiffProperties.old_file = self.filepath
props = tool.Blender.get_diff_props()
props.old_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class SelectDiffNewFile(bpy.types.Operator):
class SelectDiffNewFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_diff_new_file"
bl_label = "Select Diff New File"
bl_description = "Select filepath for a new IFC file to compare."
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
filename_ext = ".ifc"
def execute(self, context):
context.scene.DiffProperties.new_file = self.filepath
props = tool.Blender.get_diff_props()
props.new_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ExecuteIfcDiff(bpy.types.Operator):
class ExecuteIfcDiff(bpy.types.Operator, ExportHelper):
bl_idname = "bim.execute_ifc_diff"
bl_label = "Execute IFC Diff"
bl_description = "Compare two IFC files and save a json diff report by the provided filepath."
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
import ifcdiff
self.props = context.scene.DiffProperties
self.props = tool.Blender.get_diff_props()
if tool.Ifc.get():
if self.props.active_file == "NONE":
@@ -238,7 +228,8 @@ class SelectDiffObjects(bpy.types.Operator):
def execute(self, context):
ifc_file = tool.Ifc.get()
with open(context.scene.DiffProperties.diff_json_file, "r") as file:
props = tool.Blender.get_diff_props()
with open(props.diff_json_file, "r") as file:
diff = json.load(file)
for obj in context.visible_objects:
obj.select_set(False)
@@ -253,7 +244,7 @@ class SelectDiffObjects(bpy.types.Operator):
obj.select_set(True)
continue
if not obj.BIMObjectProperties.ifc_definition_id:
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
continue
ifc_file = ""
@@ -272,7 +263,7 @@ class SelectDiffObjects(bpy.types.Operator):
element_file = ifc_file
try:
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
element = element_file.by_id(ifc_id)
except:
continue
global_id = getattr(element, "GlobalId", None)
+18 -2
View File
@@ -30,18 +30,25 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal, get_args
def update_diff_json_file(self, context):
def update_diff_json_file(self: "DiffProperties", context: bpy.types.Context) -> None:
DiffData.data["diff_json"] = DiffData.diff_json()
RelationshipType = Literal["type", "property", "container", "aggregate", "classification"]
class Relationships(PropertyGroup):
relationship: EnumProperty(
name="Relationship",
items=[(r, r.capitalize(), r) for r in ["type", "property", "container", "aggregate", "classification"]],
items=[(r, r.capitalize(), r) for r in get_args(RelationshipType)],
)
if TYPE_CHECKING:
relationship: RelationshipType
class DiffProperties(PropertyGroup):
diff_json_file: StringProperty(default="", name="JSON Output", update=update_diff_json_file)
@@ -59,3 +66,12 @@ class DiffProperties(PropertyGroup):
name="Active File",
default="NEW",
)
if TYPE_CHECKING:
diff_json_file: str
old_file: str
new_file: str
diff_relationships: bpy.types.bpy_prop_collection_idprop[Relationships]
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
should_load_changed_elements: bool
active_file: Literal["NONE", "OLD", "NEW"]
+1 -3
View File
@@ -37,9 +37,7 @@ class BIM_PT_diff(Panel):
layout = self.layout
layout.use_property_split = True
scene = context.scene
props = scene.DiffProperties
props = tool.Blender.get_diff_props()
layout.label(text="IFC Diff Setup:")
+6 -4
View File
@@ -90,18 +90,20 @@ class BIM_PT_object_documents(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id):
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
return False
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
if not tool.Ifc.get_object_by_identifier(ifc_id):
return False
return True
def draw(self, context):
if not ObjectDocumentData.is_loaded:
ObjectDocumentData.load()
obj = context.active_object
self.oprops = obj.BIMObjectProperties
self.oprops = tool.Blender.get_object_bim_props(obj)
self.props = tool.Document.get_document_props()
self.file = tool.Ifc.get()
@@ -51,7 +51,7 @@ class Annotator:
curve.font = font
props = tool.Drawing.get_text_props(obj)
props.font_size = "2.5"
collection = bpy.context.scene.camera.BIMObjectProperties.collection
collection = tool.Blender.get_object_bim_props(bpy.context.scene.camera).collection
collection.objects.link(obj)
Annotator.resize_text(obj)
return obj
@@ -133,7 +133,7 @@ class Annotator:
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
matrix_world = tool.Drawing.get_camera_matrix(camera)
matrix_world.translation = co1
collection = camera.BIMObjectProperties.collection
collection = tool.Blender.get_object_bim_props(camera).collection
if object_type == "TEXT":
obj = bpy.data.objects.new(object_type, None)
+5 -1
View File
@@ -236,7 +236,8 @@ class DecoratorData:
# stores 1 type of data per object
data = {}
cut_cache = {}
layerset_cache = {}
slice_cache = {}
fill_cache = {}
@classmethod
def get_batting_thickness(cls, obj):
@@ -386,6 +387,8 @@ class DecoratorData:
suppress_zero_inches = pset_data.get("SuppressZeroInches", False)
text_prefix = pset_data.get("TextPrefix", None) or ""
text_suffix = pset_data.get("TextSuffix", None) or ""
custom_unit_list = pset_data.get("CustomUnit", None) or ""
custom_unit = custom_unit_list[0] if custom_unit_list else ""
dimension_data = {
"dimension_style": dimension_style,
@@ -394,6 +397,7 @@ class DecoratorData:
"text_prefix": text_prefix,
"text_suffix": text_suffix,
"fill_bg": fill_bg,
"custom_unit": custom_unit,
}
cls.data[obj.name] = dimension_data
return dimension_data
+215 -313
View File
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import os
import gpu
import bpy
import blf
@@ -29,7 +28,7 @@ import ifcopenshell.util.unit
import bonsai.tool as tool
import bonsai.bim.module.drawing.helper as helper
from pathlib import Path
from math import pi, sin, cos, tan, acos, atan, degrees, radians, ceil
from math import pi, sin, cos, acos, atan, degrees, radians
from bpy.types import SpaceView3D
from mathutils import Vector, Matrix
from bpy_extras.view3d_utils import location_3d_to_region_2d
@@ -319,6 +318,8 @@ class BaseDecorator:
color: tuple[float, float, float, float],
indices: Optional[list[tuple[int, int]]] = None,
) -> None:
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.base_shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
@@ -495,14 +496,20 @@ class BaseDecorator:
self.draw_label(context, text=text, line_no=line_number_start, multiline=True, **draw_label_kwargs)
@lru_cache(maxsize=None)
def format_value(self, context, value):
def format_value(self, context, value, custom_unit=None):
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
precision = drawing_pset_data.get("MetricPrecision", None)
if not precision:
precision = drawing_pset_data.get("ImperialPrecision", None)
decimal_places = drawing_pset_data.get("DecimalPlaces", None)
return format_distance(value, precision=precision, decimal_places=decimal_places)
return format_distance(
value,
precision=precision,
decimal_places=decimal_places,
suppress_zero_inches=True,
custom_unit=custom_unit,
)
def draw_asterisk(self, context: bpy.types.Context, pos: Vector, rotation: float = 0.0, scale: float = 1.0) -> None:
"""`pos` is a world space position\n
@@ -589,7 +596,8 @@ class BaseDecorator:
region3d = context.region_data
text_dir = self.get_annotation_direction(context, obj)
pos = location_3d_to_region_2d(region, region3d, text_world_position)
if not (pos := location_3d_to_region_2d(region, region3d, text_world_position)):
return
props = tool.Drawing.get_text_props(obj)
text_data = DecoratorData.get_ifc_text_data(obj)
if props.is_editing:
@@ -722,6 +730,8 @@ class DimensionDecorator(BaseDecorator):
v1 = Vector(vertices[i1])
p0 = location_3d_to_region_2d(region, region3d, v0)
p1 = location_3d_to_region_2d(region, region3d, v1)
if not p0 or not p1:
return
text_dir = p1 - p0
if text_dir.length < 1:
continue
@@ -737,7 +747,7 @@ class DimensionDecorator(BaseDecorator):
if not show_description_only:
length = (v1 - v0).length
text = self.format_value(context, length)
text = self.format_value(context, length, custom_unit=dimension_data["custom_unit"])
if isinstance(self, DiameterDecorator):
text = "D" + text
text = text_prefix + text + text_suffix
@@ -872,6 +882,8 @@ class AngleDecorator(BaseDecorator):
# calculate angle position
p0, p1, p2 = [location_3d_to_region_2d(region, region3d, p) for p in vertices[i0 : i1 + 2]]
if not p0 or not p1 or not p2:
continue
edge0 = p0 - p1
edge1 = p2 - p1
radius = min(edge0.length_squared, edge1.length_squared) ** 0.5
@@ -936,6 +948,8 @@ class RadiusDecorator(BaseDecorator):
spline_points = self.get_spline_points(obj)
p0, p1 = [location_3d_to_region_2d(region, region3d, p) for p in self.get_spline_points(obj)]
if not p0 or not p1:
return
element = tool.Ifc.get_entity(obj)
description = element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
@@ -946,7 +960,7 @@ class RadiusDecorator(BaseDecorator):
def get_text():
length = (spline_points[-1] - spline_points[-2]).length
return "R" + self.format_value(context, length)
return "R" + self.format_value(context, length, custom_unit=dimension_data["custom_unit"])
self.draw_dimension_text(
context, get_text, description, dimension_data, pos=pos, text_dir=Vector((1, 0)), box_alignment="center"
@@ -968,7 +982,8 @@ class FallDecorator(BaseDecorator):
region = context.region
region3d = context.region_data
dir = Vector((1, 0))
pos = location_3d_to_region_2d(region, region3d, self.get_spline_end(obj))
if not (pos := location_3d_to_region_2d(region, region3d, self.get_spline_end(obj))):
return
spline = obj.data.splines[0]
spline_points = spline.bezier_points if spline.bezier_points else spline.points
@@ -1159,7 +1174,6 @@ class PlanLevelDecorator(BaseDecorator):
self.draw_labels(context, obj, self.get_splines(obj))
def draw_labels(self, context, obj, splines):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
region = context.region
region3d = context.region_data
@@ -1169,6 +1183,8 @@ class PlanLevelDecorator(BaseDecorator):
for verts in splines:
p0, p1 = [location_3d_to_region_2d(region, region3d, v) for v in verts[:2]]
if not p0 or not p1:
continue
text_dir = p1 - p0
if text_dir.length < 1:
continue
@@ -1435,8 +1451,10 @@ class GridDecorator(BaseDecorator):
p1 = location_3d_to_region_2d(region, region3d, v1)
dir = Vector((1, 0))
text = obj.name.split("/")[1].split(".")[0]
self.draw_label(context, text, p0, dir, vcenter=True, gap=0)
self.draw_label(context, text, p1, dir, vcenter=True, gap=0)
if p0:
self.draw_label(context, text, p0, dir, vcenter=True, gap=0)
if p1:
self.draw_label(context, text, p1, dir, vcenter=True, gap=0)
class ElevationDecorator(BaseDecorator):
@@ -1589,6 +1607,9 @@ class CutDecorator:
@classmethod
def install(cls, context):
DecoratorData.cut_cache.clear()
DecoratorData.slice_cache.clear()
DecoratorData.fill_cache.clear()
if cls.installed:
cls.uninstall()
handler = cls()
@@ -1603,38 +1624,50 @@ class CutDecorator:
cls.installed = None
def __call__(self, context):
if not context.scene.camera:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
selected_elements_color = self.addon_prefs.decorator_color_selected
self.fallback_colour = (0.3, 0.3, 0.3, 1)
all_vertices = []
all_edges = []
selected_vertices = []
selected_edges = []
layer_vertices = []
layer_edges = []
fills = {}
all_vertex_i_offset = 0
selected_vertex_i_offset = 0
layer_vertex_i_offset = 0
for obj in [o for o in bpy.context.visible_objects if o.type == "MESH"]:
verts, edges = self.decorate(context, obj)
if not verts:
if not (element := tool.Ifc.get_entity(obj)):
continue
self.decorate(context, obj, element)
obj_layer_verts, obj_layer_edges = self.slice_layersets(context, obj, verts, edges)
if obj_layer_verts:
layer_vertices.extend(obj_layer_verts)
layer_edges.extend([[vi + layer_vertex_i_offset for vi in e] for e in obj_layer_edges])
layer_vertex_i_offset += len(obj_layer_verts)
verts, edges = DecoratorData.cut_cache[element.id()]
if verts:
if obj.select_get():
selected_vertices.extend(verts)
selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges])
selected_vertex_i_offset += len(verts)
else:
all_vertices.extend(verts)
all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges])
all_vertex_i_offset += len(verts)
if obj.select_get():
selected_vertices.extend(verts)
selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges])
selected_vertex_i_offset += len(verts)
else:
all_vertices.extend(verts)
all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges])
all_vertex_i_offset += len(verts)
verts, edges = DecoratorData.slice_cache.get(element.id(), (None, None))
if verts:
if obj.select_get():
selected_vertices.extend(verts)
selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges])
selected_vertex_i_offset += len(verts)
else:
all_vertices.extend(verts)
all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges])
all_vertex_i_offset += len(verts)
for colour, element_fills in DecoratorData.fill_cache[element.id()].items():
fills.setdefault(colour, []).append(element_fills)
gpu.state.point_size_set(1)
gpu.state.blend_set("ALPHA")
@@ -1652,9 +1685,10 @@ class CutDecorator:
black = (0, 0, 0, 1)
if layer_vertices:
self.draw_batch("LINES", layer_vertices, black, layer_edges)
self.draw_batch("POINTS", layer_vertices, black)
for colour, element_fills in fills.items():
for verts_tris in element_fills:
for verts, tris in verts_tris:
self.draw_batch("TRIS", verts, colour, tris)
gpu.state.point_size_set(2)
self.line_shader.uniform_float("lineWidth", 3.0)
@@ -1667,315 +1701,183 @@ class CutDecorator:
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def decorate(self, context, obj):
element = tool.Ifc.get_entity(obj)
if not element:
return None, None
def decorate(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
has_cut_cache = element.id() in DecoratorData.cut_cache
has_fill_cache = element.id() in DecoratorData.fill_cache
# Currently selected objects shall not be cached as they may be being moved / edited.
# If the camera is selected, we also disable the cache as the user may be moving the camera.
if obj.select_get() or context.scene.camera.select_get():
verts, edges = None, None
else:
verts, edges = DecoratorData.cut_cache.get(element.id(), (None, None))
# Currently selected objects must be recalculated as they may be being moved / edited.
# If the camera is selected, we also recalculate as the user may be moving the camera.
if verts is False:
return None, None
elif verts:
return verts, edges
if not has_cut_cache or obj.select_get() or context.scene.camera.select_get():
self.recalculate_cut(context, obj, element)
if not has_fill_cache or obj.select_get() or context.scene.camera.select_get():
self.recalculate_fill(context, obj, element)
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
DecoratorData.cut_cache[element.id()] = (False, False)
return None, None
if verts is None:
else:
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
DecoratorData.cut_cache[element.id()] = (verts, edges)
return verts, edges
def slice_layersets(self, context, obj, cut_verts, cut_edges):
element = tool.Ifc.get_entity(obj)
# Currently selected objects shall not be cached as they may be being moved / edited.
# If the camera is selected, we also disable the cache as the user may be moving the camera.
if obj.select_get() or context.scene.camera.select_get():
verts, edges = None, None
else:
verts, edges = DecoratorData.layerset_cache.get(element.id(), (None, None))
if verts is False:
return None, None
elif verts is not None:
return verts, edges
def recalculate_fill(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
element_id = element.id()
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
DecoratorData.layerset_cache[element.id()] = (False, False)
return None, None
DecoratorData.fill_cache[element_id] = {}
return
if tool.Model.get_usage_type(element) != "LAYER2":
DecoratorData.layerset_cache[element.id()] = (False, False)
return None, None
DecoratorData.fill_cache[element_id] = {}
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
layers = self.get_layer_data(element)
mesh = obj.data
bm_original = bmesh.new()
bm_original.from_mesh(mesh)
if not layers:
DecoratorData.layerset_cache[element.id()] = (False, False)
return None, None
minx = min([co[0] for co in obj.bound_box])
maxx = max([co[0] for co in obj.bound_box])
min_edge = [Vector((minx, layers["offset"])), Vector((maxx, layers["offset"]))]
max_edge = [
Vector((minx, layers["offset"] + layers["thickness"])),
Vector((maxx, layers["offset"] + layers["thickness"])),
]
centerline = [
Vector((minx, layers["offset"] + layers["thickness"] / 2)),
Vector((maxx, layers["offset"] + layers["thickness"] / 2)),
]
connections = self.get_connections(element, obj, centerline, min_edge, max_edge)
start_point = None
end_point = None
if connections["ATSTART"]:
boundary_edge = min_edge if connections["ATSTART"]["angle"] > 0 else max_edge
rel_dir = connections["ATSTART"]["centerline"][1] - connections["ATSTART"]["centerline"][0]
offset, _ = tool.Cad.intersect_edges(boundary_edge, [centerline[0], centerline[0] + rel_dir])
offset_x = (offset - centerline[0]).length + abs(offset.x)
intersect, _ = tool.Cad.intersect_edges(boundary_edge, connections["ATSTART"]["centerline"])
if tool.Cad.is_point_on_edge(intersect, boundary_edge):
centerline[0] = centerline[0] + Vector((offset_x, 0))
start_point = centerline[0] + rel_dir
if connections["ATEND"]:
boundary_edge = min_edge if connections["ATEND"]["angle"] > 0 else max_edge
rel_dir = connections["ATEND"]["centerline"][1] - connections["ATEND"]["centerline"][0]
offset, _ = tool.Cad.intersect_edges(boundary_edge, [centerline[1], centerline[1] + rel_dir])
offset_x = (offset - centerline[1]).length + abs((maxx - abs(offset.x)))
intersect, _ = tool.Cad.intersect_edges(boundary_edge, connections["ATEND"]["centerline"])
if tool.Cad.is_point_on_edge(intersect, boundary_edge):
centerline[1] = centerline[1] - Vector((offset_x, 0))
end_point = centerline[1] + rel_dir
min_segments = self.get_segments(connections["MINPATH"], centerline, start_point, end_point)
max_segments = self.get_segments(connections["MAXPATH"], centerline, start_point, end_point)
final_segments = []
for segment in min_segments:
segment_line = shapely.LineString([co for co in segment])
for distance in layers["min_layers"]:
distance *= -1
layer_line = shapely.offset_curve(segment_line, distance, join_style=shapely.BufferJoinStyle.mitre)
final_segments.append(list(layer_line.coords))
for segment in max_segments:
segment_line = shapely.LineString([co for co in segment])
for distance in layers["max_layers"]:
layer_line = shapely.offset_curve(segment_line, distance, join_style=shapely.BufferJoinStyle.mitre)
final_segments.append(list(layer_line.coords))
# Extrude and bisect layers
bm = bmesh.new()
verts = []
edges = []
offset = 0
for segment in final_segments:
if not segment:
# Why does this occur?
continue
if isinstance(segment[0], tuple):
segment = [Vector(co) for co in segment]
verts.extend([bm.verts.new(co.to_3d()) for co in segment])
[bm.edges.new((verts[i + offset], verts[i + 1 + offset])) for i in range(0, len(segment) - 1)]
offset += len(segment)
extrusion_dir = (0, 0, 3)
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=bm.edges[:])
bmesh.ops.translate(
bm, vec=extrusion_dir, verts=[v for v in extruded_geom["geom"] if isinstance(v, bmesh.types.BMVert)]
)
verts, edges = self.bisect_mesh(obj, bm, context.scene.camera)
layer_linestrings = [shapely.LineString((verts[e[0]], verts[e[1]])) for e in edges]
clipped_linestrings = []
polygons = shapely.polygonize([shapely.LineString((cut_verts[e[0]], cut_verts[e[1]])) for e in cut_edges])
for polygon in polygons.geoms:
clipped_linestrings.extend([polygon.intersection(ls) for ls in layer_linestrings])
verts = []
edges = []
offset = 0
for linestring in clipped_linestrings:
try:
linestring = list(linestring.coords)
except:
print("Failed ... ", linestring)
continue
verts.extend(linestring)
edges.extend([(i + offset, i + 1 + offset) for i in range(0, len(linestring) - 1)])
offset += len(linestring)
DecoratorData.layerset_cache[element.id()] = (verts, edges)
return verts, edges
def bisect_mesh(self, obj, bm, camera):
camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world
# Slice our mesh into a 2D drawing cut (2D is always easier)
camera_matrix = obj.matrix_world.inverted() @ context.scene.camera.matrix_world
global_offset = context.scene.camera.matrix_world.col[2].xyz * -context.scene.camera.data.clip_start
plane_co = camera_matrix.translation
plane_no = camera_matrix.col[2].xyz
geom = bm_original.verts[:] + bm_original.edges[:] + bm_original.faces[:]
bmesh.ops.bisect_plane(
bm_original,
geom=geom,
dist=0.0001,
plane_co=plane_co,
plane_no=plane_no,
clear_outer=True,
clear_inner=True,
)
bmesh.ops.remove_doubles(bm_original, verts=bm_original.verts, dist=0.000001)
bmesh.ops.triangle_fill(bm_original, use_dissolve=True, edges=bm_original.edges)
global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start
if not (material := ifcopenshell.util.element.get_material(element)):
verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera)
DecoratorData.fill_cache[element_id].setdefault(self.fallback_colour, []).append((verts, tris))
return
# Run the bisect operation
geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
results = bmesh.ops.bisect_plane(bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no)
if material.is_a() not in ("IfcMaterialLayerSet", "IfcMaterialLayerSetUsage"):
# Constituents, lists, and item styles not supported yet
material = ifcopenshell.util.element.get_materials(element)[0]
verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera)
colour = self.get_material_colour(material)
DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris))
return
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if material.is_a("IfcMaterialLayerSetUsage"):
usage = material
layer_set = material.ForLayerSet
offset = usage.OffsetFromReferenceLine * self.unit_scale
sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1
elif material.is_a("IfcMaterialLayerSet"):
usage = None
layer_set = material
offset = 0
sense_factor = 1
if len(layer_set.MaterialLayers) == 1:
material = layer_set.MaterialLayers[0].Material
verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera)
colour = self.get_material_colour(material)
DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris))
return
bm = bm_original.copy()
prev_co = None
if not usage:
sense_factor = 1 # Assume the extrusion vector points in the direction sense
no = tool.Drawing.get_extrusion_vector(element).normalized()
co = Vector((0.0, 0.0, offset))
elif usage.LayerSetDirection == "AXIS2":
co = Vector((0.0, offset, 0.0))
no = tool.Drawing.get_extrusion_vector(element).normalized()
no = no.cross(Vector([1.0, 0.0, 0.0]))
elif usage.LayerSetDirection == "AXIS3":
co = Vector((0.0, 0.0, offset))
no = tool.Drawing.get_extrusion_vector(element).normalized()
no = Vector([0.0, 0.0, 1.0])
elif usage.LayerSetDirection == "AXIS1":
co = Vector((0.0, 0.0, offset))
no = tool.Drawing.get_extrusion_vector(element).normalized()
no = Vector([1.0, 0.0, 0.0])
no *= sense_factor
last_i = len(layer_set.MaterialLayers) - 1
vert_map = {}
verts = []
edges = []
j = 0
for i, layer in enumerate(layer_set.MaterialLayers):
prev_co = co.copy()
co += no * layer.LayerThickness * self.unit_scale
if i != last_i:
bisect = bmesh.ops.bisect_plane(
bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no
)
for geom in bisect["geom_cut"]:
if isinstance(geom, bmesh.types.BMVert):
verts.append(tuple((obj.matrix_world @ geom.co) + global_offset))
vert_map[geom.index] = j
j += 1
else:
# It seems as though edges always appear after verts
edges.append([vert_map[v.index] for v in geom.verts])
colour = self.get_material_colour(layer.Material)
bm_fill = bm_original.copy()
if i != last_i:
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
bmesh.ops.bisect_plane(bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True)
if i != 0:
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
bmesh.ops.bisect_plane(bm_fill, geom=geom, dist=0.0001, plane_co=prev_co, plane_no=no, clear_inner=True)
DecoratorData.fill_cache[element_id].setdefault(colour, []).append(
self.get_bmesh_tris(obj, bm_fill, context.scene.camera)
)
DecoratorData.slice_cache[element.id()] = (verts, edges)
bm_original.free()
def get_material_colour(self, material: ifcopenshell.entity_instance) -> tuple[float, float, float, float]:
body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
style = ifcopenshell.util.representation.get_material_style(material, body)
if not style:
return self.fallback_colour
if not (styles := [s for s in style.Styles if s.is_a("IfcSurfaceStyleShading")]):
return self.fallback_colour
colour = styles[0].SurfaceColour
return (colour.Red, colour.Green, colour.Blue, 1)
def get_bmesh_tris(self, obj, bm, camera):
global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start
triangulate = bmesh.ops.triangulate(bm, faces=bm.faces)
vert_map = {}
verts = []
tris = []
i = 0
for geom in results["geom_cut"]:
if isinstance(geom, bmesh.types.BMVert):
verts.append(tuple((obj.matrix_world @ geom.co) + global_offset))
vert_map[geom.index] = i
i += 1
else:
# It seems as though edges always appear after verts
edges.append([vert_map[v.index] for v in geom.verts])
bm.free()
return verts, edges
def get_layer_data(self, element):
usage = ifcopenshell.util.element.get_material(element)
offset = usage.OffsetFromReferenceLine * self.unit_scale
layer_set = usage.ForLayerSet
if len(layer_set.MaterialLayers) == 1:
return # No use slicing if there's only one layer
total_thickness = layer_set.TotalThickness
half_thickness = total_thickness / 2
min_layers = []
max_layers = []
current_thickness = 0
for layer in layer_set.MaterialLayers:
current_thickness += layer.LayerThickness
if current_thickness / total_thickness < 0.5:
min_layers.append((half_thickness - current_thickness) * self.unit_scale)
else:
max_layers.append((current_thickness - half_thickness) * self.unit_scale)
min_layers.reverse()
max_layers.pop()
if usage.DirectionSense == "NEGATIVE":
total_thickness *= -1
offset *= -1
min_layers = [l * -1 for l in min_layers]
max_layers = [l * -1 for l in max_layers]
return {
"offset": offset,
"min_layers": min_layers,
"max_layers": max_layers,
"thickness": total_thickness * self.unit_scale,
}
def get_segments(self, connections, centerline, start_point, end_point):
segments = []
total_connections = len(connections)
if total_connections:
for i in range(0, total_connections + 1):
if i == 0:
connection = connections[i]
segments.append([centerline[0], connection["intersection"], connection["out_point"]])
elif i == total_connections:
segments.append([segments[-1][-1], segments[-1][-2], centerline[1]])
for face in triangulate["faces"]:
tri = []
for vert in face.verts:
if index := vert_map.get(vert.index, None):
tri.append(index)
else:
connection = connections[i]
segments.append(
[segments[-1][-1], segments[-1][-2], connection["intersection"], connection["out_point"]]
)
else:
segments.append([centerline[0], centerline[1]])
if start_point:
segments[0].insert(0, start_point)
if end_point:
segments[-1].append(end_point)
return segments
def get_connections(self, wall, obj, centerline, min_edge, max_edge):
connections = {"ATEND": None, "ATSTART": None, "ATPATH": [], "MINPATH": [], "MAXPATH": []}
for rel in wall.ConnectedTo:
# How do you join to a non layered element? Not sure.
if tool.Model.get_usage_type(rel.RelatedElement) != "LAYER2":
continue
if rel.RelatingConnectionType == "ATPATH":
metadata = self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
if not metadata:
continue
connections["ATPATH"].append(metadata)
else:
metadata = self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
if not metadata:
continue
connections[rel.RelatingConnectionType] = metadata
for rel in wall.ConnectedFrom:
if tool.Model.get_usage_type(rel.RelatingElement) != "LAYER2":
continue
# We only consider ATPATH since in this situation, we have the
# priority. The non-priority wall never has any layers that need to
# "turn a corner".
if rel.RelatedConnectionType == "ATPATH":
metadata = self.get_connection_metadata(obj, rel.RelatingElement, centerline, min_edge, max_edge)
if not metadata:
continue
connections["ATPATH"].append(metadata)
connections["ATPATH"] = sorted(connections["ATPATH"], key=lambda c: c["intersection"].x)
for connection in connections["ATPATH"]:
if connection["angle"] > 0:
connections["MINPATH"].append(connection)
else:
connections["MAXPATH"].append(connection)
return connections
def get_connection_metadata(self, obj, rel_element, centerline, min_edge, max_edge):
rel_obj = tool.Ifc.get_object(rel_element)
layers = self.get_layer_data(rel_element)
if not layers:
return
minx = min([co[0] for co in rel_obj.bound_box])
maxx = max([co[0] for co in rel_obj.bound_box])
rel_centerline = [
Vector((minx, layers["offset"] + layers["thickness"] / 2)),
Vector((maxx, layers["offset"] + layers["thickness"] / 2)),
]
rel_centerline = [obj.matrix_world.inverted() @ rel_obj.matrix_world @ v.to_3d() for v in rel_centerline]
rel_centerline = [v.to_2d() for v in rel_centerline]
intersection = tool.Cad.intersect_edges(centerline, rel_centerline)
if intersection:
intersection, _ = intersection
else:
return
closest_centerline_point = tool.Cad.closest_vector(intersection, tuple(rel_centerline))
if closest_centerline_point == rel_centerline[1]:
rel_centerline = [rel_centerline[1], rel_centerline[0]]
angle = tool.Cad.angle_edges(centerline, rel_centerline, signed=True)
# A little extreme, but maybe it's OK?
out_point = tool.Cad.furthest_vector(intersection, tuple(rel_centerline))
return {
"element": rel_element,
"obj": rel_obj,
"centerline": rel_centerline,
"intersection": intersection,
"angle": angle,
"out_point": out_point,
}
verts.append(tuple((obj.matrix_world @ vert.co) + global_offset))
vert_map[vert.index] = i
tri.append(i)
i += 1
tris.append(tri)
return verts, tris
class DecorationsHandler:
@@ -414,6 +414,8 @@ class ExtrusionGuidesGizmo(CustomGizmo, types.Gizmo):
shader_wrapper = ExtrusionGuidesShader()
verts = [Vector((0, 0, 0)), Vector((0, 0, 1))]
verts, edges = shader_wrapper.process_geometry(verts)
if not tool.Blender.validate_shader_batch_data(verts, edges):
verts, edges = [], []
self.custom_shape = shader_wrapper, shader_wrapper.batch(
pos=verts,
indices=edges,
+67 -11
View File
@@ -20,6 +20,7 @@ import bpy
import math
import mathutils.geometry
import ifcopenshell
import ifcopenshell.util.unit
import bonsai.tool as tool
from mathutils import Vector
from typing import Union
@@ -130,18 +131,53 @@ def format_distance(
decimal_places=None,
suppress_zero_inches=False,
in_unit_length=False,
custom_unit=None,
):
s_code = "\u00b2" # Superscript two THIS IS LEGACY (but being kept for when Area Measurements are re-implimented)
# Get Scene Unit Settings
scaleFactor = bpy.context.scene.unit_settings.scale_length
# Get Blender Scene Unit Settings
unit_scale = bpy.context.scene.unit_settings.scale_length
unit_system = bpy.context.scene.unit_settings.system
unit_length = bpy.context.scene.unit_settings.length_unit
area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol(
ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT")
)
area_unit_symbol = " m2" if unit_system == "METRIC" else " ft2"
value *= scaleFactor
# Get IFC Unit Settings
if tool.Ifc.get():
unit_scale = 1
if length_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "LENGTHUNIT"):
unit_system = "METRIC" if length_unit.Name == "METRE" else "IMPERIAL"
unit_length = length_unit.Name.upper()
if hasattr(length_unit, "Prefix") and length_unit.Prefix:
unit_length = length_unit.Prefix + length_unit.Name
unit_length_mapping = {
"FOOT": "FEET",
"INCH": "INCHES",
"METRE": "METERS",
"DECIMETRE": "DECIMETERS",
"CENTIMETRE": "CENTIMETERS",
"MILLIMETRE": "MILLIMETERS",
}
unit_length = unit_length_mapping[unit_length]
# For now we only format area in IFC Units
if area_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT"):
area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol(area_unit)
unit_fraction = True if unit_system == "IMPERIAL" else False
# Custom Unit Settings
if custom_unit:
unit_mapping = {
"Feet and Inches - Fractional": ("IMPERIAL", "FEET", True),
"Feet - Decimal": ("IMPERIAL", "FEET", False),
"Inches - Fractional": ("IMPERIAL", "INCHES", True),
"Inches - Decimal": ("IMPERIAL", "INCHES", False),
"Meters": ("METRIC", "METERS", False),
"Decimeters": ("METRIC", "DECIMETERS", False),
"Centimeters": ("METRIC", "CENTIMETERS", False),
"Millimeters": ("METRIC", "MILLIMETERS", False),
}
if custom_unit in unit_mapping:
unit_system, unit_length, unit_fraction = unit_mapping[custom_unit]
value *= unit_scale
# Imperial Formatting
if unit_system == "IMPERIAL":
@@ -166,10 +202,14 @@ def format_distance(
base = int(precision)
decInches = value * toInches
decFeet = decInches / 12
# Separate ft and inches
# Unless Inches are the specified Length Unit
if unit_length != "INCHES":
# Unless Inches are the specified Length Unit or unit_fraction is False
if unit_length == "FEET" and not unit_fraction:
feet = round(decInches / inPerFoot, 3) # keep decimal
decInches = 0
elif unit_length != "INCHES":
feet = int(decInches / inPerFoot) # remove decimal
decInches -= feet * inPerFoot
else:
@@ -204,6 +244,10 @@ def format_distance(
feet += 1
inches = 0
# Check whether decimal or fractional
if not unit_fraction:
inches = round(decInches, 3)
frac = None
if not isArea:
add_inches = bool(inches) or not suppress_zero_inches or (inches == 0 and frac)
tx_dist = ""
@@ -229,6 +273,8 @@ def format_distance(
tx_dist += str(frac) + "/" + str(base)
if add_inches or frac:
tx_dist += '"'
if precision == "12" and unit_system == "IMPERIAL":
tx_dist = str(round(decFeet)) + "'"
else:
fmt = "%1.3f"
sq_feet = round(value * toInches / inPerFoot, 4)
@@ -252,6 +298,8 @@ def format_distance(
# METRIC FORMATTING
elif unit_system == "METRIC":
if in_unit_length:
if unit_length == "DECIMETERS":
value = value / 10
if unit_length == "CENTIMETERS":
value = value / 100
if unit_length == "MILLIMETERS":
@@ -270,6 +318,14 @@ def format_distance(
if hide_units is False:
fmt += " m"
tx_dist = fmt % value
# Decimeters
elif unit_length == "DECIMETERS":
if decimal_places is None:
fmt = "%1.1f"
if hide_units is False:
fmt += " dm"
d_dm = value * (10)
tx_dist = fmt % d_dm
# Centimeters
elif unit_length == "CENTIMETERS":
if decimal_places is None:
@@ -343,7 +399,7 @@ def get_active_drawing(
props = tool.Drawing.get_document_props()
try:
camera = tool.Ifc.get_object(tool.Ifc.get().by_id(props.active_drawing_id))
return camera.BIMObjectProperties.collection, camera
return tool.Blender.get_object_bim_props(camera).collection, camera
except:
return None, None
+212 -50
View File
@@ -43,6 +43,7 @@ import bonsai.bim.module.drawing.svgwriter as svgwriter
import bonsai.bim.module.drawing.annotation as annotation
import bonsai.bim.module.drawing.sheeter as sheeter
import bonsai.bim.export_ifc
from bpy_extras.io_utils import ImportHelper
from bonsai.bim.module.drawing.decoration import CutDecorator
from bonsai.bim.module.drawing.data import DecoratorData, DrawingsData
from typing import NamedTuple, List, Union, Optional, Literal
@@ -246,7 +247,7 @@ class CreateDrawing(bpy.types.Operator):
def execute(self, context):
self.props = tool.Drawing.get_document_props()
active_drawing_id = context.scene.camera.BIMObjectProperties.ifc_definition_id
active_drawing_id = tool.Blender.get_ifc_definition_id(context.scene.camera)
if self.print_all:
original_drawing_id = active_drawing_id
drawings_to_print = [d.ifc_definition_id for d in self.props.drawings if d.is_selected and d.is_drawing]
@@ -391,7 +392,7 @@ class CreateDrawing(bpy.types.Operator):
bpy.ops.render.render(write_still=True)
else:
previous_visibility = {}
for obj in self.camera.BIMObjectProperties.collection.objects:
for obj in tool.Blender.get_object_bim_props(self.camera).collection.objects:
if bpy.context.view_layer.objects.get(obj.name):
previous_visibility[obj.name] = obj.hide_get()
obj.hide_set(True)
@@ -562,6 +563,112 @@ class CreateDrawing(bpy.types.Operator):
path.attrib["d"] = d
group.append(g)
def generate_wall_layers(self, context: bpy.types.Context, root):
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
if "projection" in el.get("class", "").split():
continue
element = self.get_element_by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
if not (obj := tool.Ifc.get_object(element)):
continue
if not (material := ifcopenshell.util.element.get_material(element)):
continue
if material.is_a() not in ("IfcMaterialLayerSet", "IfcMaterialLayerSetUsage"):
continue
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if material.is_a("IfcMaterialLayerSetUsage"):
usage = material
layer_set = material.ForLayerSet
offset = usage.OffsetFromReferenceLine * self.unit_scale
sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1
elif material.is_a("IfcMaterialLayerSet"):
usage = None
layer_set = material
offset = 0
sense_factor = 1
camera_matrix_i = context.scene.camera.matrix_world.inverted()
group = root.find("{http://www.w3.org/2000/svg}g")
raw_width, raw_height = self.get_camera_dimensions()
x_offset = raw_width / 2
y_offset = raw_height / 2
svg_scale = self.scale * 1000 # IFC is in meters, SVG is in mm
mesh = obj.data
bm = bmesh.new()
bm.from_mesh(mesh)
# Slice our mesh into a 2D drawing cut (2D is always easier)
camera_matrix = obj.matrix_world.inverted() @ context.scene.camera.matrix_world
plane_co = camera_matrix.translation
plane_no = camera_matrix.col[2].xyz
geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
bmesh.ops.bisect_plane(
bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no, clear_outer=True, clear_inner=True
)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.000001)
bmesh.ops.triangle_fill(bm, use_dissolve=True, edges=bm.edges)
prev_co = None
if not usage:
sense_factor = 1 # Assume the extrusion vector points in the direction sense
no = tool.Drawing.get_extrusion_vector(element).normalized()
co = Vector((0.0, 0.0, offset))
elif usage.LayerSetDirection == "AXIS2":
co = Vector((0.0, offset, 0.0))
no = tool.Drawing.get_extrusion_vector(element).normalized()
no = no.cross(Vector([1.0, 0.0, 0.0]))
elif usage.LayerSetDirection == "AXIS3":
co = Vector((0.0, 0.0, offset))
no = tool.Drawing.get_extrusion_vector(element).normalized()
no = Vector([0.0, 0.0, 1.0])
elif usage.LayerSetDirection == "AXIS1":
co = Vector((0.0, 0.0, offset))
no = tool.Drawing.get_extrusion_vector(element).normalized()
no = Vector([1.0, 0.0, 0.0])
no *= sense_factor
last_i = len(layer_set.MaterialLayers) - 1
for i, layer in enumerate(layer_set.MaterialLayers):
prev_co = co.copy()
co += no * layer.LayerThickness * self.unit_scale
bm_fill = bm.copy()
if i != last_i:
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
bmesh.ops.bisect_plane(bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True)
if i != 0:
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
bmesh.ops.bisect_plane(
bm_fill, geom=geom, dist=0.0001, plane_co=prev_co, plane_no=no, clear_inner=True
)
bm_fill.verts.ensure_lookup_table()
bm_fill.edges.ensure_lookup_table()
verts = [tuple(obj.matrix_world @ v.co) for v in bm_fill.verts]
edges = [[v.index for v in e.verts] for e in bm_fill.edges]
g = etree.SubElement(root, "{http://www.w3.org/2000/svg}g")
g.attrib["{http://www.ifcopenshell.org/ns}guid"] = element.GlobalId
g.attrib["{http://www.ifcopenshell.org/ns}name"] = element.Name or ""
g.attrib["{http://www.ifcopenshell.org/ns}layer-id"] = str(layer.id())
lines = []
for edge in edges:
start = [o for o in (camera_matrix_i @ Vector(verts[edge[0]])).xy]
end = [o for o in (camera_matrix_i @ Vector(verts[edge[1]])).xy]
coords = [start, end]
d = " ".join(
["L{},{}".format((x_offset + p[0]) * svg_scale, (y_offset - p[1]) * svg_scale) for p in coords]
)
d = "M{}".format(d[1:])
path = etree.SubElement(g, "{http://www.w3.org/2000/svg}path")
path.attrib["d"] = d
group.append(g)
bm.free()
def generate_freestyle_linework(self, context: bpy.types.Context) -> str | None:
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"):
return
@@ -692,7 +799,6 @@ class CreateDrawing(bpy.types.Operator):
exporter = bonsai.bim.export_ifc.IfcExporter(None)
exporter.file = tool.Ifc.get()
invalidated_elements = exporter.sync_all_objects()
invalidated_elements += exporter.sync_edited_objects()
invalidated_guids = [e.GlobalId for e in invalidated_elements if hasattr(e, "GlobalId")]
if cache := IfcStore.get_cache():
[cache.remove(guid) for guid in invalidated_guids]
@@ -717,6 +823,8 @@ class CreateDrawing(bpy.types.Operator):
props = tool.Project.get_project_props()
for link in props.links:
if not link.is_loaded:
continue
if link.name not in IfcStore.session_files:
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
files[link.name] = IfcStore.session_files[link.name]
@@ -769,10 +877,12 @@ class CreateDrawing(bpy.types.Operator):
if self.camera.data.BIMCameraProperties.cut_mode == "BISECT":
self.remove_cut_linework(root)
self.generate_bisect_linework(context, root)
self.generate_wall_layers(context, root)
self.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root)
elif self.camera.data.BIMCameraProperties.cut_mode == "OPENCASCADE":
self.move_projection_to_bottom(root)
self.generate_wall_layers(context, root)
self.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root)
@@ -1101,7 +1211,7 @@ class CreateDrawing(bpy.types.Operator):
# the style of the face when running tree.select_ray()
# tree.enable_face_styles(True)
def get_svg_classes(self, element):
def get_svg_classes(self, element, layer=None):
classes = [element.is_a()]
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
material_name = ""
@@ -1113,7 +1223,13 @@ class CreateDrawing(bpy.types.Operator):
material_name = tool.Drawing.canonicalise_class_name(material_name)
classes.append(f"material-{material_name}")
else:
classes.append(f"material-null")
classes.append("material-null")
if layer:
classes.append(layer.is_a())
material_name = layer.Material.Name or "null"
material_name = tool.Drawing.canonicalise_class_name(material_name)
classes.append(f"layer-material-{material_name}")
for key in self.metadata:
value = ifcopenshell.util.selector.get_element_value(element, key)
@@ -1152,6 +1268,23 @@ class CreateDrawing(bpy.types.Operator):
except:
continue
def get_element_by_id(self, step_id):
try:
step_id = int(step_id)
except:
return
try:
return tool.Ifc.get().by_id(step_id)
except:
props = tool.Project.get_project_props()
for link in props.links:
if link.name not in IfcStore.session_files:
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
try:
return IfcStore.session_files[link.name].by_id(step_id)
except:
continue
def remove_cut_linework(self, root):
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
if "projection" not in el.get("class", "").split():
@@ -1163,15 +1296,15 @@ class CreateDrawing(bpy.types.Operator):
join_criteria = join_criteria.split(",")
else:
# Drawing convention states that same objects classes with the same material are merged when cut.
join_criteria = ["class", "material.Name", "/Pset_.*Common/.Status", "EPset_Status.Status"]
join_criteria = ["class", "material.Name", "/Pset_.*Common/.Status", "EPset_Status.Status", "Material.Name"]
group = root.find("{http://www.w3.org/2000/svg}g")
joined_paths = {}
self.is_manifold_cache = {}
ifc = tool.Ifc.get()
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
element = self.get_element_by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
layer = self.get_element_by_id(el.get("{http://www.ifcopenshell.org/ns}layer-id"))
if "projection" in el.get("class", "").split():
classes = self.get_svg_classes(element)
@@ -1179,7 +1312,7 @@ class CreateDrawing(bpy.types.Operator):
el.set("class", " ".join(classes))
continue
else:
classes = self.get_svg_classes(element)
classes = self.get_svg_classes(element, layer)
classes.append("cut")
el.set("class", " ".join(classes))
@@ -1188,7 +1321,12 @@ class CreateDrawing(bpy.types.Operator):
if not obj: # This is a linked model object. For now, do nothing.
continue
if not self.is_manifold(obj):
if (material := ifcopenshell.util.element.get_material(element)) and material.is_a() in (
"IfcMaterialLayerSet",
"IfcMaterialLayerSetUsage",
):
pass # These are always manifold
elif not self.is_manifold(obj):
continue
# An element group will contain a bunch of paths representing the
@@ -1273,6 +1411,15 @@ class CreateDrawing(bpy.types.Operator):
else:
keys.append(key)
if layer:
for query in join_criteria:
key = ifcopenshell.util.selector.get_element_value(layer, query)
print("got layer key", query, key)
if isinstance(key, (list, tuple)):
keys.extend(key)
else:
keys.append(key)
hash_keys = hash(tuple(keys))
if el.findall("{http://www.w3.org/2000/svg}path"):
@@ -1289,7 +1436,8 @@ class CreateDrawing(bpy.types.Operator):
for path in el.findall("{http://www.w3.org/2000/svg}path"):
for subpath in path.attrib["d"].split("M")[1:]:
subpath_co = "M" + subpath.strip(" Z")
coords = [[float(o) for o in co[1:].split(",")] for co in subpath_co.split()]
# Round due to inaccuracies from Blender meshes and bisection
coords = [[round(float(o), 3) for o in co[1:].split(",")] for co in subpath_co.split()]
if subpath.strip().lower().endswith("z"):
coords.append(coords[0])
if len(coords) > 2 and coords[0] == coords[-1]:
@@ -2020,6 +2168,8 @@ class ActivateDrawingBase:
camera_props = camera.data.BIMCameraProperties
if camera_props.update_representation(camera):
bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="")
# See 6452 and 6478.
# bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
return {"FINISHED"}
@@ -2040,7 +2190,6 @@ class ActivateDrawing(bpy.types.Operator, ActivateDrawingBase):
@classmethod
def poll(cls, context):
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -2070,12 +2219,14 @@ class ActivateDrawingFromSheet(bpy.types.Operator, ActivateDrawingBase):
return True
class SelectDocIfcFile(bpy.types.Operator):
# TODO: not exposed to the UI.
class SelectDocIfcFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_doc_ifc_file"
bl_label = "Select Documentation IFC File"
bl_description = "Selection .ifc file for documentation."
bl_options = {"REGISTER", "UNDO"}
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filename_ext = ".ifc"
index: bpy.props.IntProperty()
def execute(self, context):
@@ -2083,10 +2234,6 @@ class SelectDocIfcFile(bpy.types.Operator):
props.ifc_files[self.index].name = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ResizeText(bpy.types.Operator):
bl_idname = "bim.resize_text"
@@ -2095,7 +2242,7 @@ class ResizeText(bpy.types.Operator):
# TODO: check undo redo
def execute(self, context):
for obj in context.scene.camera.BIMObjectProperties.collection.objects:
for obj in tool.Blender.get_object_bim_props(context.scene.camera).collection.objects:
if isinstance(obj.data, bpy.types.TextCurve):
annotation.Annotator.resize_text(obj)
return {"FINISHED"}
@@ -2465,13 +2612,12 @@ class RemoveSheet(bpy.types.Operator, tool.Ifc.Operator):
core.remove_sheet(tool.Ifc, tool.Drawing, sheet=tool.Ifc.get().by_id(self.sheet))
class AddSchedule(bpy.types.Operator, tool.Ifc.Operator):
class AddSchedule(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.add_schedule"
bl_label = "Add Schedule"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add an .ods, .xls or .xlsx file as a schedule"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ods;*.xls;*.xlsx", options={"HIDDEN"})
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
@@ -2479,10 +2625,6 @@ class AddSchedule(bpy.types.Operator, tool.Ifc.Operator):
filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)
core.add_document(tool.Ifc, tool.Drawing, "SCHEDULE", uri=filepath)
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class RemoveSchedule(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_schedule"
@@ -2652,24 +2794,20 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
tool.Drawing.import_sheets()
class AddReference(bpy.types.Operator, tool.Ifc.Operator):
class AddReference(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.add_reference"
bl_label = "Add Reference"
bl_description = "Import a .svg file to the project as a reference"
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.svg", options={"HIDDEN"})
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
filename_ext = ".svg"
def _execute(self, context):
filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)
core.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=filepath)
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class RemoveReference(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_reference"
@@ -3152,14 +3290,22 @@ class DisableEditingDrawings(bpy.types.Operator, tool.Ifc.Operator):
class ExpandTargetView(bpy.types.Operator):
bl_idname = "bim.expand_target_view"
bl_label = "Expand Target View"
bl_description = "Show views in this category"
bl_description = "\nSHIFT+CLICK to expand all view categories "
bl_options = {"REGISTER", "UNDO"}
target_view: bpy.props.StringProperty()
expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# Expanding all categories on shift+click.
# Make sure to use SKIP_SAVE on property, otherwise it might get stuck (copied from #4771).
if event.type == "LEFTMOUSE" and event.shift:
self.expand_all = True
return self.execute(context)
def execute(self, context):
props = tool.Drawing.get_document_props()
for drawing in [d for d in props.drawings if d.target_view == self.target_view]:
for drawing in [d for d in props.drawings if self.expand_all or d.target_view == self.target_view]:
drawing.is_expanded = True
core.load_drawings(tool.Drawing)
return {"FINISHED"}
@@ -3168,14 +3314,22 @@ class ExpandTargetView(bpy.types.Operator):
class ContractTargetView(bpy.types.Operator):
bl_idname = "bim.contract_target_view"
bl_label = "Contract Target View"
bl_description = "Hide views in this category"
bl_description = "\n\nSHIFT+CLICK to hide all view categories"
bl_options = {"REGISTER", "UNDO"}
target_view: bpy.props.StringProperty()
contract_all: bpy.props.BoolProperty(name="Contract All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# Contracting all categories on shift+click.
# Make sure to use SKIP_SAVE on property, otherwise it might get stuck (copied from #4771).
if event.type == "LEFTMOUSE" and event.shift:
self.contract_all = True
return self.execute(context)
def execute(self, context):
props = tool.Drawing.get_document_props()
for drawing in [d for d in props.drawings if d.target_view == self.target_view]:
for drawing in [d for d in props.drawings if self.contract_all or d.target_view == self.target_view]:
drawing.is_expanded = False
core.load_drawings(tool.Drawing)
return {"FINISHED"}
@@ -3184,14 +3338,20 @@ class ContractTargetView(bpy.types.Operator):
class ExpandSheet(bpy.types.Operator):
bl_idname = "bim.expand_sheet"
bl_label = "Expand Sheet"
bl_description = "Show views, schedules, references etc\nplaced on this sheet"
bl_description = "Show views, schedules, references etc\nplaced on this sheet.\n\nShift+click to expand all sheets."
bl_options = {"REGISTER", "UNDO"}
sheet: bpy.props.IntProperty()
expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.shift:
self.expand_all = True
return self.execute(context)
def execute(self, context):
props = tool.Drawing.get_document_props()
for sheet in [s for s in props.sheets if s.ifc_definition_id == self.sheet]:
for sheet in [s for s in props.sheets if self.expand_all or s.ifc_definition_id == self.sheet]:
sheet.is_expanded = True
core.load_sheets(tool.Drawing)
return {"FINISHED"}
@@ -3200,14 +3360,22 @@ class ExpandSheet(bpy.types.Operator):
class ContractSheet(bpy.types.Operator):
bl_idname = "bim.contract_sheet"
bl_label = "Contract Sheet"
bl_description = "Hide views, schedules, references etc\nplaced on this sheet"
bl_description = (
"Hide views, schedules, references etc\nplaced on this sheet.\n\nShift+click to contract all sheets."
)
bl_options = {"REGISTER", "UNDO"}
sheet: bpy.props.IntProperty()
expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.shift:
self.expand_all = True
return self.execute(context)
def execute(self, context):
props = tool.Drawing.get_document_props()
for sheet in [s for s in props.sheets if s.ifc_definition_id == self.sheet]:
for sheet in [s for s in props.sheets if self.expand_all or s.ifc_definition_id == self.sheet]:
sheet.is_expanded = False
core.load_sheets(tool.Drawing)
return {"FINISHED"}
@@ -3268,7 +3436,7 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.activate_drawing(drawing=element.id(), should_view_from_camera=False)
class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator):
class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.add_reference_image"
bl_label = "Add Reference Image"
bl_description = "Add or import reference image to the IFC project"
@@ -3276,9 +3444,6 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
filepath: bpy.props.StringProperty(
name="File Path", description="Filepath used to import from", maxlen=1024, default="", subtype="FILE_PATH"
)
filter_image: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"})
filter_folder: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"})
@@ -3306,10 +3471,6 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator):
layout.prop(self, "override_existing_image")
layout.prop(self, "use_existing_object_by_name")
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def _execute(self, context):
abs_path = Path(self.filepath).absolute().resolve()
image_filepath = Path(tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path))
@@ -3353,7 +3514,8 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator):
obj.material_slots[0].material = material
bpy.ops.bim.add_style()
style = ifc_file.by_id(material.BIMStyleProperties.ifc_definition_id)
style = tool.Ifc.get_entity(material)
assert style
tool.Style.assign_style_to_object(style, obj)
# TODO: IfcSurfaceStyleRendering is unnecessary here, added it only because
+1 -1
View File
@@ -266,7 +266,7 @@ def update_titleblocks(self, context):
def update_should_draw_decorations(self, context: bpy.types.Context) -> None:
if self.should_draw_decorations:
# TODO: design a proper text variable templating renderer
collection = context.scene.camera.BIMObjectProperties.collection
collection = tool.Blender.get_object_bim_props(context.scene.camera).collection
for obj in collection.objects:
element = tool.Ifc.get_entity(obj)
if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
+48 -12
View File
@@ -32,6 +32,7 @@ from mathutils import Vector
import re
VIEW_TITLE_OFFSET_Y = 5
DRAWING_PADDING = 10
DEFAULT_POSITION = Vector((30, 30))
SVG = "{http://www.w3.org/2000/svg}"
XLINK = "{http://www.w3.org/1999/xlink}"
@@ -113,13 +114,15 @@ class SheetBuilder:
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
x, y = self.next_drawing_location(layout_root, view_width)
# add background
if os.path.isfile(underlay_path):
background = ET.SubElement(view, "image")
background.attrib["data-type"] = "background"
background.attrib["xlink:href"] = os.path.relpath(underlay_path, layout_dir)
background.attrib["x"] = str(DEFAULT_POSITION.x)
background.attrib["y"] = str(DEFAULT_POSITION.y)
background.attrib["x"] = str(x)
background.attrib["y"] = str(y)
background.attrib["width"] = str(view_width)
background.attrib["height"] = str(view_height)
@@ -128,16 +131,49 @@ class SheetBuilder:
foreground = ET.SubElement(view, "image")
foreground.attrib["data-type"] = "foreground"
foreground.attrib["xlink:href"] = os.path.relpath(drawing_path, layout_dir)
foreground.attrib["x"] = str(DEFAULT_POSITION.x)
foreground.attrib["y"] = str(DEFAULT_POSITION.y)
foreground.attrib["x"] = str(x)
foreground.attrib["y"] = str(y)
foreground.attrib["width"] = str(view_width)
foreground.attrib["height"] = str(view_height)
self.add_view_title(
DEFAULT_POSITION.x, view_height + DEFAULT_POSITION.y + VIEW_TITLE_OFFSET_Y, view, layout_dir
)
self.add_view_title(x, view_height + y + VIEW_TITLE_OFFSET_Y, view, layout_dir)
layout_tree.write(layout_path)
def next_drawing_location(self, layout_root: ET.Element, next_width: float) -> list:
titleblocks = layout_root.findall(f'{SVG}g[@data-type="titleblock"]')
drawings = layout_root.findall(f'{SVG}g[@data-type="drawing"]')
# how wide is the title block frame
try:
titleblock_width = self.convert_to_mm(titleblocks[0][0].attrib.get("width"))
except (IndexError, AttributeError):
titleblock_width = 840.0
# where does the last drawing finish
try:
last = drawings[-1][0]
last_width = self.convert_to_mm(last.attrib.get("width"))
last_x = self.convert_to_mm(last.attrib.get("x"))
last_y = self.convert_to_mm(last.attrib.get("y"))
except (IndexError, AttributeError):
return [DEFAULT_POSITION.x, DEFAULT_POSITION.y]
# check if the new drawing fits in the current row
if last_x + last_width + DRAWING_PADDING + next_width + DEFAULT_POSITION.x < titleblock_width:
return [last_x + last_width + DRAWING_PADDING, last_y]
# start a new row, find the y
for drawing in drawings:
for image in drawing:
try:
image_y = self.convert_to_mm(image.attrib.get("y"))
image_height = self.convert_to_mm(image.attrib.get("height"))
except AttributeError:
return [DEFAULT_POSITION.x, DEFAULT_POSITION.y]
if image_y + image_height + DRAWING_PADDING > last_y:
last_y = image_y + image_height + DRAWING_PADDING
return [DEFAULT_POSITION.x, last_y]
def update_sheet_drawing_sizes(self, sheet: ifcopenshell.entity_instance) -> None:
ET.register_namespace("", "http://www.w3.org/2000/svg")
@@ -221,6 +257,8 @@ class SheetBuilder:
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
x, y = self.next_drawing_location(layout_root, view_width)
view = ET.SubElement(layout_root, "g")
view.attrib["data-id"] = str(reference.id())
view.attrib["data-type"] = document.Scope.lower()
@@ -229,14 +267,12 @@ class SheetBuilder:
foreground = ET.SubElement(view, "image")
foreground.attrib["data-type"] = "content"
foreground.attrib["xlink:href"] = os.path.relpath(view_path, layout_dir)
foreground.attrib["x"] = str(DEFAULT_POSITION.x)
foreground.attrib["y"] = str(DEFAULT_POSITION.y)
foreground.attrib["x"] = str(x)
foreground.attrib["y"] = str(y)
foreground.attrib["width"] = str(view_width)
foreground.attrib["height"] = str(view_height)
self.add_view_title(
DEFAULT_POSITION.x, view_height + DEFAULT_POSITION.y + VIEW_TITLE_OFFSET_Y, view, layout_dir
)
self.add_view_title(x, view_height + y + VIEW_TITLE_OFFSET_Y, view, layout_dir)
layout_tree.write(layout_path)
def add_view_title(self, x: float, y: float, parent: ET.Element, layout_dir: str) -> None:
@@ -1128,7 +1128,12 @@ class SvgWriter:
def get_text():
radius = (points[-1].co - points[-2].co).length
radius = helper.format_distance(radius, precision=self.precision, decimal_places=self.decimal_places)
radius = helper.format_distance(
radius,
precision=self.precision,
decimal_places=self.decimal_places,
custom_unit=dimension_data["custom_unit"],
)
text = f"R{radius}"
return text
@@ -1256,6 +1261,7 @@ class SvgWriter:
text_prefix=dimension_data["text_prefix"],
text_suffix=dimension_data["text_suffix"],
fill_bg=dimension_data["fill_bg"],
custom_unit=dimension_data["custom_unit"],
)
def draw_dimension_annotations(self, obj):
@@ -1280,6 +1286,7 @@ class SvgWriter:
text_prefix=dimension_data["text_prefix"],
text_suffix=dimension_data["text_suffix"],
fill_bg=dimension_data["fill_bg"],
custom_unit=dimension_data["custom_unit"],
)
def draw_measureit_arch_dimension_annotations(self):
@@ -1306,6 +1313,7 @@ class SvgWriter:
text_prefix="",
text_suffix="",
fill_bg=False,
custom_unit=None,
):
offset = Vector([self.raw_width, self.raw_height]) / 2
v0 = self.project_point_onto_camera(v0_global)
@@ -1339,6 +1347,7 @@ class SvgWriter:
precision=self.precision,
decimal_places=self.decimal_places,
suppress_zero_inches=suppress_zero_inches,
custom_unit=custom_unit,
)
text = text_prefix + str(dimension) + text_suffix
else:
+20 -3
View File
@@ -183,7 +183,14 @@ class BIM_PT_drawing_underlay(Panel):
if not dprops.drawing_styles:
return
layout.template_list("BIM_UL_generic", "", dprops, "drawing_styles", props, "active_drawing_style_index")
layout.template_list(
"BIM_UL_generic",
"BIM_UL_generic_drawing_styles",
dprops,
"drawing_styles",
props,
"active_drawing_style_index",
)
if not drawing_index_is_valid:
return
@@ -326,7 +333,12 @@ class BIM_PT_schedules(Panel):
row.operator("bim.remove_schedule", icon="X", text="").schedule = active_schedule.ifc_definition_id
self.layout.template_list(
"BIM_UL_generic", "", self.props, "schedules", self.props, "active_schedule_index"
"BIM_UL_generic",
"BIM_UL_generic_schedules",
self.props,
"schedules",
self.props,
"active_schedule_index",
)
@@ -373,7 +385,12 @@ class BIM_PT_references(Panel):
row.operator("bim.remove_reference", icon="X", text="").reference = active_reference.ifc_definition_id
self.layout.template_list(
"BIM_UL_generic", "", self.props, "references", self.props, "active_reference_index"
"BIM_UL_generic",
"BIM_UL_generic_references",
self.props,
"references",
self.props,
"active_reference_index",
)
@@ -27,7 +27,7 @@ import ifcopenshell.util.representation
from bonsai.bim.module.drawing.data import DecoratorData, AnnotationData
from bonsai.bim.helper import prop_with_search
from bpy.types import WorkSpaceTool
from typing import Union
from functools import partial
class LaunchAnnotationTypeManager(bpy.types.Operator):
@@ -135,23 +135,9 @@ class AnnotationTool(WorkSpaceTool):
AnnotationToolUI.draw(context, layout)
def add_layout_hotkey_operator(
layout: bpy.types.UILayout, text: str, hotkey: str, description: Union[str, None]
) -> tuple[bpy.types.OperatorProperties, bpy.types.UILayout]:
modifiers = {
"A": "EVENT_ALT",
"S": "EVENT_SHIFT",
}
modifier, key = hotkey.split("_")
row = layout.row(align=True)
row.label(text="", icon=modifiers[modifier])
row.label(text="", icon=f"EVENT_{key}")
op = row.operator("bim.annotation_hotkey", text=text)
op.hotkey = hotkey
op.description = description
return op, row
add_layout_hotkey_operator = partial(
tool.Blender.add_layout_hotkey_operator, tool_name="annotation", module_name=__name__
)
# TODO: move to operator
@@ -265,8 +251,8 @@ class AnnotationToolUI:
class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.annotation_hotkey"
bl_label = "Hotkey"
bl_options = {"REGISTER", "UNDO"}
bl_label = ""
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
hotkey: bpy.props.StringProperty()
description: bpy.props.StringProperty()
+28 -25
View File
@@ -24,14 +24,25 @@ import logging
import tempfile
import ifcopenshell
import bonsai.tool as tool
from bpy_extras.io_utils import ExportHelper, ImportHelper
class ExecuteIfcFM(bpy.types.Operator):
class ExecuteIfcFM(bpy.types.Operator, ExportHelper):
bl_idname = "bim.execute_ifcfm"
bl_label = "Execute IfcFM"
file_format: bpy.props.StringProperty()
bl_description = "Export IfcFM data as a spreadsheet."
filter_glob: bpy.props.StringProperty(default="*.csv;*.ods;*.xlsx", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
@property
def filename_ext(self) -> str:
props = bpy.context.scene.BIMFMProperties
return f".{props.format}"
def draw(self, context):
layout = self.layout
props = context.scene.BIMFMProperties
layout.prop(props, "format")
@classmethod
def poll(cls, context):
@@ -41,13 +52,6 @@ class ExecuteIfcFM(bpy.types.Operator):
return False
return True
def invoke(self, context, event):
props = context.scene.BIMFMProperties
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
props = context.scene.BIMFMProperties
ifc_file = tool.Ifc.get()
@@ -82,12 +86,12 @@ class ExecuteIfcFM(bpy.types.Operator):
return {"FINISHED"}
class SelectFMSpreadsheetFiles(bpy.types.Operator):
class SelectFMSpreadsheetFiles(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_fm_spreadsheet_files"
bl_label = "Select FM Spreadsheet Files"
bl_description = "Select FM spreadsheets to merge."
bl_options = {"REGISTER", "UNDO"}
filter_glob: bpy.props.StringProperty(default="*.ods;*.xlsx", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
files: bpy.props.CollectionProperty(name="File Path", type=bpy.types.OperatorFileListElement)
def execute(self, context):
@@ -99,16 +103,22 @@ class SelectFMSpreadsheetFiles(bpy.types.Operator):
new.name = os.path.join(dirname, f.name)
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ExecuteIfcFMFederate(bpy.types.Operator):
class ExecuteIfcFMFederate(bpy.types.Operator, ExportHelper):
bl_idname = "bim.execute_ifcfm_federate"
bl_label = "Merge IfcFM SpreadSheets"
bl_description = "Merge added IfcFM spreadsheets."
filter_glob: bpy.props.StringProperty(default="*.ods;*.xlsx", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
@property
def filename_ext(self) -> str:
props = bpy.context.scene.BIMFMProperties
return f".{props.format}"
def draw(self, context):
layout = self.layout
props = context.scene.BIMFMProperties
layout.prop(props, "format")
@classmethod
def poll(cls, context):
@@ -118,13 +128,6 @@ class ExecuteIfcFMFederate(bpy.types.Operator):
return False
return True
def invoke(self, context, event):
props = context.scene.BIMFMProperties
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
props = context.scene.BIMFMProperties
parser = ifcfm.Parser(preset=props.engine)
@@ -19,6 +19,7 @@
import bpy
from . import ui, prop, operator
from bpy.app.handlers import persistent
import ifcopenshell.util.element
classes = (
operator.AddCurvelikeItem,
@@ -102,9 +103,14 @@ def block_scale(scene: bpy.types.Scene) -> None:
import bonsai.tool as tool
if obj := (getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active):
if isinstance(obj, bpy.types.Object) and obj.BIMObjectProperties.ifc_definition_id:
if obj.scale != (1, 1, 1):
obj.scale = (1, 1, 1)
if isinstance(obj, bpy.types.Object) and tool.Blender.get_ifc_definition_id(obj):
if obj.type == "CAMERA":
camera = tool.Ifc.get_entity(obj)
if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW":
obj.scale = (-1, -1, -1)
else:
if obj.scale != (1, 1, 1):
obj.scale = (1, 1, 1)
elif isinstance(obj, bpy.types.Mesh) and tool.Geometry.get_mesh_props(obj).ifc_definition_id:
if obj.scale != (1, 1, 1):
obj.scale = (1, 1, 1)
@@ -118,6 +124,7 @@ def register():
operator.OverrideDuplicateMoveLinkedMacro.define("BIM_OT_override_object_duplicate_move_linked")
operator.OverrideDuplicateMoveLinkedMacro.define("TRANSFORM_OT_translate")
operator.DuplicateMoveLinkedAggregateMacro.define("BIM_OT_object_duplicate_move_linked_aggregate")
operator.DuplicateMoveLinkedAggregateMacro.define("BIM_OT_override_move")
operator.DuplicateMoveLinkedAggregateMacro.define("TRANSFORM_OT_translate")
operator.OverrideMoveMacro.define("BIM_OT_override_move")
operator.OverrideMoveMacro.define("TRANSFORM_OT_translate")
@@ -157,15 +157,17 @@ class RepresentationsData:
if not cls.data["representations"]:
return []
obj = tool.Geometry.get_active_or_representation_obj()
assert obj
if not obj.data:
return []
element = tool.Ifc.get_entity(obj)
assert element
base_representation = tool.Geometry.get_active_representation(obj)
if not base_representation:
return [] # Maybe in profile editing mode
# shape aspects matching context of the active representation
matching_shape_aspects = []
matching_shape_aspects: list[ifcopenshell.entity_instance] = []
for shape_aspect in ifcopenshell.util.element.get_shape_aspects(element):
matching_representation = tool.Geometry.get_shape_aspect_representation(shape_aspect, base_representation)
if matching_representation:
@@ -128,6 +128,8 @@ class ItemDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
@@ -464,7 +464,8 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
assert tool.Geometry.is_data_supported_for_adding_representation(data)
mprops = tool.Geometry.get_mesh_props(data)
product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
product = tool.Ifc.get_entity(obj)
assert product
material = ifcopenshell.util.element.get_material(product, should_skip_usage=True)
# NOTE: Currently iterator doesn't detect whether opening is actually affected the representation
@@ -903,7 +904,7 @@ class OverrideOutlinerDelete(bpy.types.Operator):
if element := tool.Ifc.get_entity(obj):
if tool.Geometry.is_locked(element):
self.report({"ERROR"}, lock_error_message(obj.name))
if collection := obj.BIMObjectProperties.collection:
if collection := tool.Blender.get_object_bim_props(obj).collection:
collections_to_delete.discard(collection)
continue
tool.Geometry.delete_ifc_object(obj)
@@ -1067,7 +1068,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
continue
# clear object's collection so it will be able to have it's own
new_obj.BIMObjectProperties.collection = None
tool.Blender.get_object_bim_props(new_obj).collection = None
# copy the actual class
new = bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
@@ -1774,8 +1775,8 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
obj.select_set(False)
self.report(
{"ERROR"},
f"IFC join failed - object '{obj}' has a different representation type "
f"({obj_rep.RepresentationType}) than target '{self.target}' ({representation_type}).",
f"IFC join failed - object '{obj.name}' has a different representation type "
f"({obj_rep.RepresentationType})\nthan target '{self.target.name}' ({representation_type}).",
)
return
@@ -1928,7 +1929,7 @@ class OverrideEscape(bpy.types.Operator):
tool.Geometry.disable_item_mode()
elif tool.Model.get_model_props().openings:
bpy.ops.bim.hide_all_openings()
elif context.scene.BIMAggregateProperties.in_aggregate_mode:
elif tool.Aggregate.get_aggregate_props().in_aggregate_mode:
bpy.ops.bim.disable_aggregate_mode()
elif active_object := context.active_object:
if tool.Blender.Modifier.try_canceling_editing_modifier_parameters_or_path(active_object):
@@ -1952,24 +1953,26 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
selected_objs = context.selected_objects # Purposely exclude active object
aprops = tool.Aggregate.get_aggregate_props()
nprops = tool.Nest.get_nest_props()
if self.has_aggregates(selected_objs):
if not context.scene.BIMAggregateProperties.in_aggregate_mode:
if not aprops.in_aggregate_mode:
bonsai.core.aggregate.enable_aggregate_mode(tool.Aggregate, context.active_object)
return {"FINISHED"}
if self.has_nests(selected_objs):
if not context.scene.BIMNestProperties.in_nest_mode:
if not nprops.in_nest_mode:
bonsai.core.nest.enable_nest_mode(tool.Nest, context.active_object)
return {"FINISHED"}
if len(selected_objs) == 1 and context.active_object == selected_objs[0]:
self.handle_single_object(context, context.active_object)
elif len(selected_objs) == 0:
if context.scene.BIMAggregateProperties.in_aggregate_mode:
if aprops.in_aggregate_mode:
bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate)
return {"FINISHED"}
if context.scene.BIMNestProperties.in_nest_mode:
if nprops.in_nest_mode:
bonsai.core.nest.disable_nest_mode(tool.Nest)
return {"FINISHED"}
tool.Geometry.disable_item_mode()
@@ -1980,9 +1983,10 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
props = tool.Geometry.get_geometry_props()
pprops = tool.Project.get_project_props()
aprops = tool.Aggregate.get_aggregate_props()
if obj == props.representation_obj:
self.report({"ERROR"}, f"Element '{obj.name}' is in item mode and cannot be edited directly")
elif obj in [o.obj for o in context.scene.BIMAggregateProperties.not_editing_objects]:
elif obj in [o.obj for o in aprops.not_editing_objects]:
obj.select_set(False)
self.report(
{"ERROR"}, f"Element '{obj.name}' does not belong to this aggregate and cannot be edited directly"
@@ -2098,7 +2102,8 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
continue
aggregate = ifcopenshell.util.element.get_aggregate(element)
parts = ifcopenshell.util.element.get_parts(element)
if (aggregate or parts) and not bpy.context.scene.BIMAggregateProperties.in_aggregate_mode:
props = tool.Aggregate.get_aggregate_props()
if (aggregate or parts) and not props.in_aggregate_mode:
return True
else:
return False
@@ -2110,7 +2115,8 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
continue
nest = ifcopenshell.util.element.get_nest(element)
components = ifcopenshell.util.element.get_components(element)
if (nest or components) and not bpy.context.scene.BIMNestProperties.in_nest_mode:
props = tool.Nest.get_nest_props()
if (nest or components) and not props.in_nest_mode:
return True
else:
return False
@@ -2256,8 +2262,10 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
return
old_profile = item.SweptArea
profile.ProfileName = old_profile.ProfileName
for inverse in tool.Ifc.get().get_inverse(old_profile):
ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile)
tool.Profile.replace_profile_in_profiles_ui(old_profile.id(), profile.id())
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_profile)
tool.Geometry.reload_representation(props.representation_obj)
@@ -2506,11 +2514,12 @@ class RemoveRepresentationItem(bpy.types.Operator, tool.Ifc.Operator):
return True
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
assert (obj := tool.Geometry.get_active_or_representation_obj())
assert (element := tool.Ifc.get_entity(obj))
ifc_file = tool.Ifc.get()
representation_item = ifc_file.by_id(self.representation_item_id)
tool.Geometry.remove_representation_item(representation_item)
tool.Geometry.remove_representation_item(representation_item, element)
tool.Geometry.reload_representation(obj)
# reload items ui
@@ -2885,6 +2894,15 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
else:
assert False, "Unexpected mesh type."
if not item_ids:
# It is possible that the user has created a shape that
# IfcOpenShell cannot render (i.e. boolean clipped everything), but
# we still want to edit items. I'm not sure the best way to handle
# this, but for now perhaps we can detect when there are no
# item_ids at all.
representation = tool.Ifc.get_entity(data)
item_ids = [i["item"].id() for i in ifcopenshell.util.representation.resolve_items(representation)]
queue = list(set(item_ids))
processed_ids = set()
boolean_ids = set()
@@ -3257,7 +3275,7 @@ class OverrideMove(bpy.types.Operator):
self.new_active_obj = obj
# Get aggregates
props = context.scene.BIMAggregateProperties
props = tool.Aggregate.get_aggregate_props()
not_editing_objs = [o.obj for o in props.not_editing_objects]
aggregates_to_move = []
for obj in context.selected_objects:
@@ -3292,7 +3310,7 @@ class OverrideMove(bpy.types.Operator):
return {"FINISHED"}
# Get nests
props = context.scene.BIMNestProperties
props = tool.Nest.get_nest_props()
not_editing_objs = [o.obj for o in props.not_editing_objects]
nests_to_move = []
for obj in context.selected_objects:
+12 -5
View File
@@ -121,7 +121,7 @@ def get_layers_no_active(self, context):
return LayersData.data["layers_enum_no_active"]
def update_shape_aspect(self, context):
def update_shape_aspect(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> None:
shape_aspect_id = self.representation_item_shape_aspect
attrs = self.shape_aspect_attrs
@@ -209,6 +209,7 @@ def update_is_editing_item_layer(self: "BIMObjectGeometryProperties", context: b
item = ifc_file.by_id(active_ui_item.ifc_definition_id)
if layer := next(iter(item.LayerAssignment), None):
self.representation_item_layer = str(layer.id())
return
if "representation_item_layer" in self:
del self["representation_item_layer"]
@@ -250,9 +251,8 @@ class BIMObjectGeometryProperties(PropertyGroup):
representation_item_layer: EnumProperty(items=get_layers, name="Representation Item's Layer")
@property
def active_item(self):
if 0 <= self.active_item_index < len(self.items):
return self.items[self.active_item_index]
def active_item(self) -> Union[RepresentationItem, None]:
return tool.Blender.get_active_uilist_element(self.items, self.active_item_index)
if TYPE_CHECKING:
contexts: str
@@ -283,7 +283,14 @@ class BIMGeometryProperties(PropertyGroup):
is_changing_mode: BoolProperty(name="Is Changing Mode", default=False)
mode: EnumProperty(items=get_mode, name="IFC Interaction Mode", update=update_mode)
representation_obj: PointerProperty(
name="Representation Object", type=bpy.types.Object, update=update_representation_obj
name="Representation Object",
description=(
"Only used for Item Mode. When element is in Item Mode, new objects are imported "
"for each element's representation item, original element's object is hidden and "
"representation_obj pointing to it. None if no object in Item Mode."
),
type=bpy.types.Object,
update=update_representation_obj,
)
item_objs: CollectionProperty(name="Item Objects", type=RepresentationItemObject)
+11 -8
View File
@@ -225,7 +225,7 @@ class BIM_PT_representation_items(Panel):
RepresentationItemsData.load()
props = tool.Geometry.get_geometry_props()
obj = props.representation_obj or tool.Blender.get_active_object()
obj = tool.Geometry.get_active_or_representation_obj()
assert obj
props = tool.Geometry.get_object_geometry_props(obj)
@@ -334,9 +334,9 @@ class BIM_PT_connections(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id):
if not tool.Ifc.get_object_by_identifier(tool.Blender.get_ifc_definition_id(obj)):
return False
return tool.Ifc.get()
@@ -345,7 +345,6 @@ class BIM_PT_connections(Panel):
ConnectionsData.load()
layout = self.layout
props = context.active_object.BIMObjectProperties
if not ConnectionsData.data["connections"] and not ConnectionsData.data["is_connection_realization"]:
layout.label(text="No connections found")
@@ -480,12 +479,16 @@ class BIM_PT_placement(Panel):
@classmethod
def poll(cls, context):
return (obj := context.active_object) and obj.BIMObjectProperties.ifc_definition_id
return (obj := context.active_object) and tool.Blender.get_ifc_definition_id(obj)
def draw(self, context):
if not PlacementData.is_loaded:
PlacementData.load()
obj = context.active_object
assert obj
props = tool.Blender.get_object_bim_props(obj)
if not PlacementData.data["has_placement"]:
row = self.layout.row()
row.label(text="No Object Placement Found")
@@ -496,12 +499,12 @@ class BIM_PT_placement(Panel):
row = self.layout.row()
row.prop(context.active_object, "rotation_euler", text="Rotation")
if context.active_object.BIMObjectProperties.blender_offset_type != "NONE":
if props.blender_offset_type != "NONE":
row = self.layout.row(align=True)
row.label(text="Blender Offset", icon="TRACKING_REFINE_FORWARDS")
row.label(text=context.active_object.BIMObjectProperties.blender_offset_type)
row.label(text=props.blender_offset_type)
if context.active_object.BIMObjectProperties.blender_offset_type != "NOT_APPLICABLE":
if props.blender_offset_type != "NOT_APPLICABLE":
row = self.layout.row(align=True)
row.label(text=PlacementData.data["original_x"], icon="EMPTY_AXIS")
row.label(text=PlacementData.data["original_y"])
@@ -54,6 +54,8 @@ class GeoreferenceDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
props = tool.Georeference.get_georeference_props()
self.scale = props.visualization_scale
content_pos = [v * self.scale for v in content_pos]
@@ -20,6 +20,7 @@ import bpy
import bonsai.tool as tool
import bonsai.core.georeference as core
from bpy_extras.io_utils import ImportHelper
from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator
@@ -87,22 +88,18 @@ class GetCursorLocation(bpy.types.Operator, tool.Ifc.Operator):
core.get_cursor_location(tool.Georeference)
class ImportPlot(bpy.types.Operator, tool.Ifc.Operator):
class ImportPlot(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.import_plot"
bl_label = "Import Plot"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Import plot"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
bl_description = "Import plot from a csv file."
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
filename_ext = ".csv"
def execute(self, context):
def _execute(self, context):
core.import_plot(tool.Georeference, filepath=self.filepath)
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class EnableEditingWCS(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_wcs"
+20 -11
View File
@@ -35,9 +35,13 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator):
self.expanded_groups = json.loads(context.scene.ExpandedGroups.json_string)
self.props.groups.clear()
for group in tool.Ifc.get().by_type("IfcGroup", include_subtypes=False):
if not group.HasAssignments:
self.load_group(group)
groups = [
group for group in tool.Ifc.get().by_type("IfcGroup", include_subtypes=False) if not group.HasAssignments
]
sorted_groups = sorted(groups, key=lambda group: group.Name or "Unnamed")
for group in sorted_groups:
self.load_group(group)
self.props.is_editing = True
bpy.ops.bim.disable_editing_group()
@@ -51,14 +55,19 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator):
new.has_children = False
new.is_expanded = group.id() in self.expanded_groups
for rel in group.IsGroupedBy or []:
for related_object in rel.RelatedObjects:
if not related_object.is_a("IfcGroup"):
continue
new.has_children = True
if not new.is_expanded:
return
self.load_group(related_object, tree_depth=tree_depth + 1)
related_groups = [
related_object
for rel in group.IsGroupedBy or []
for related_object in rel.RelatedObjects
if related_object.is_a("IfcGroup")
]
sorted_related_groups = sorted(related_groups, key=lambda group: group.Name or "Unnamed")
for related_group in sorted_related_groups:
new.has_children = True
if not new.is_expanded:
return
self.load_group(related_group, tree_depth=tree_depth + 1)
class ToggleGroup(bpy.types.Operator, tool.Ifc.Operator):
+3 -2
View File
@@ -91,9 +91,10 @@ class BIM_PT_object_groups(Panel):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
return False
return tool.Ifc.get() and context.active_object.BIMObjectProperties.ifc_definition_id
props = tool.Blender.get_object_bim_props(obj)
return tool.Ifc.get() and props.ifc_definition_id
def draw(self, context):
if not ObjectGroupsData.is_loaded:
@@ -374,16 +374,17 @@ class ObjectLog(bpy.types.Operator):
@classmethod
def poll(cls, context):
if not context.active_object:
if not (obj := context.active_object):
cls.poll_message_set("No Active Object")
elif not context.active_object.BIMObjectProperties.ifc_definition_id:
elif not tool.Blender.get_ifc_definition_id(obj):
cls.poll_message_set("Active Object doesn't have an IFC definition")
else:
return True
def execute(self, context):
step_id = context.active_object.BIMObjectProperties.ifc_definition_id
obj = context.active_object
assert obj
step_id = tool.Blender.get_ifc_definition_id(obj)
core.entity_log(tool.IfcGit, tool.Ifc, step_id, self)
return {"FINISHED"}
@@ -52,6 +52,8 @@ class SolarDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
@@ -638,6 +638,7 @@ class RADIANCE_OT_export_material_mappings(bpy.types.Operator, ExportHelper):
bl_description = "Export material mappings to a JSON file"
filename_ext = ".json"
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def execute(self, context):
props = context.scene.radiance_exporter_properties
+37 -7
View File
@@ -71,8 +71,8 @@ def update_sun_path_size(self, context):
update_sun_path(self)
def update_display_shadows(self, context):
if self.display_shadows:
def update_shadow_mode(self, context):
if self.shadow_mode == "SHADING":
update_sun_path(self)
context.scene.render.engine = "BLENDER_WORKBENCH"
context.scene.display.shading.light = "FLAT"
@@ -82,6 +82,20 @@ def update_display_shadows(self, context):
context.scene.view_settings.view_transform = "Standard" # Preserve shading colours
space = tool.Blender.get_view3d_space()
space.shading.type = "RENDERED"
elif self.shadow_mode == "RENDERING":
if context.scene.sun_pos_properties.sun_object is None:
bpy.ops.object.light_add(type="SUN", radius=1, align="WORLD", location=(0, 0, 0), scale=(1, 1, 1))
bpy.ops.object.move_to_collection(collection_index=0)
context.scene.sun_pos_properties.sun_object = bpy.context.active_object
update_sun_path(self)
context.scene.render.engine = "BLENDER_EEVEE_NEXT"
context.scene.display.shading.light = "FLAT"
context.scene.display.shading.show_shadows = True
context.scene.display.shading.show_object_outline = True
context.scene.display.shadow_focus = 1.0
context.scene.view_settings.view_transform = "Standard" # Preserve shading colours
space = tool.Blender.get_view3d_space()
space.shading.type = "RENDERED"
else:
space = tool.Blender.get_view3d_space()
space.shading.type = "SOLID"
@@ -394,11 +408,27 @@ class BIMSolarProperties(PropertyGroup):
azimuth: FloatProperty(name="Azimuth")
elevation: FloatProperty(name="Elevation")
UTC_zone: FloatProperty(name="UTC Zone")
display_shadows: BoolProperty(
name="Display Shadows",
default=False,
description="Enables a visual style to display shadows easily",
update=update_display_shadows,
shadow_mode: bpy.props.EnumProperty(
items=(
("NONE", "No Shadows", "No shadows"),
(
"SHADING",
"Shaded",
"Fast shadows sufficient for external shadow analysis based on shading styles",
"SHADING_SOLID",
1,
),
(
"RENDERING",
"Rendered",
"Raycast (Eevee) shadows considering transparency based on rendering styles",
"SHADING_RENDERED",
2,
),
),
name="Shadow Mode",
description="How to display shadows in the scene",
update=update_shadow_mode,
)
display_sun_path: BoolProperty(
name="Display Sun Path",
+8 -2
View File
@@ -236,8 +236,14 @@ class BIM_PT_solar(bpy.types.Panel):
row.operator("bim.move_sun_path_to_3d_cursor")
row = self.layout.row(align=True)
row.prop(props, "display_shadows", icon="SHADING_RENDERED")
row.prop(context.scene.display.shading, "shadow_intensity", text="Shadow Intensity")
row.prop(props, "shadow_mode", icon="SHADING_RENDERED", expand=True)
if props.shadow_mode == "SHADING":
row = self.layout.row()
row.prop(context.scene.display.shading, "shadow_intensity", text="Shadow Intensity")
elif props.shadow_mode == "RENDERING":
row = self.layout.row()
row.prop(context.scene.sun_pos_properties.sun_object.data, "energy", text="Sun Intensity")
row = self.layout.row(align=True)
row.operator("bim.view_from_sun", icon="LIGHT_HEMI")

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