Compare commits

..

405 Commits

Author SHA1 Message Date
Andrej730 e8f18b5b6d ci-bonsai-daily - include submodules for a test 2025-05-21 13:00:46 +05:00
Andrej730 718254fe5e Fix regression parsing iso duration with floats (d1b1b08) #6715 2025-05-21 12:16:07 +05:00
Andrej730 41227b89c8 Fix subtle bug with operators precedence in eabdb5d
`+` has a higher precedence than `or`, so it resulted in the error below

```
Traceback (most recent call last):
  File "\bonsai\bim\module\spatial\prop.py", line 86, in update_active_container_index
    tool.Spatial.load_contained_elements()
  File "\bonsai\tool\spatial.py", line 312, in load_contained_elements
    cls.load_contained_elements_by_type(container)
  File "\bonsai\tool\spatial.py", line 321, in load_contained_elements_by_type
    results = cls.get_container_elements_grouped_by_type(container)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\bonsai\tool\spatial.py", line 294, in get_container_elements_grouped_by_type
    element_type.is_a() + "/" + element_type.Name or "Unnamed"
    ~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~
TypeError: can only concatenate str (not "NoneType") to str
```
2025-05-20 19:06:25 +05:00
Andrej730 2547ee9ab5 bonsai.crash.txt
There seems to be cases when Blender might crash too unexpectedly.
E.g. #6686 - during viewport render system is running out of resources and crashing without leaving blender.crash.txt, leaving everyone clueless. Which is arguably a bug in Blender (both crash and lack of any crash report), but at least we'll have a fallback for this case.

What bonsai.crash.txt does - it's created in system temp folder with the current Python traceback, just before some dangerous operation. If operation didn't crashed Blender, then file will be unlinked. If crash did occurred, it might be the only clue for users and devs on what actually happened.
2025-05-20 19:06:25 +05:00
Andrej730 d22f1bcd71 'viewport' render type - add a note 2025-05-20 19:06:24 +05:00
Andrej730 0c2158089d disable update_has_underlay if update_props is disabled
Otherwise it was trying to load drawing styles too early, when drawing was not yet active. And then they were reloaded again during ActivateDrawingBase. Occurred after c2860e6, before this commit issue was kind of hidden.
2025-05-20 19:06:02 +05:00
Thomas Krijnen ccafc203b5 submodule 2025-05-20 10:05:21 +02:00
Andrej730 0efc2d25f1 Fix for assign_class and object with modifiers in Blender <4.4 #6701 2025-05-20 12:24:35 +05:00
Andrej730 ac50f002f0 Fix confused blender and pset props in 9586869 2025-05-19 19:28:55 +05:00
Andrej730 66f4c957ea black . 2025-05-19 19:28:55 +05:00
Dion Moult 67016d6f90 See #2999. Reimplement basic creation of structural items. 2025-05-19 17:36:12 +10:00
Thomas Krijnen 439f9c14ce OcctNoCleanTriangulation CacheShapes PermissiveShapeReuse setting; Cgal conditional copy during triangulation; occt cheaper check for triangulation existance #6712 2025-05-18 22:14:22 +02:00
Dion Moult 404bf13a18 Update colour by property test to reflect new behaviour of only showing geometry not empties
ALL (feature) TESTS PASS YESS
2025-05-18 19:23:34 +10:00
Dion Moult 3a37bcdcaa Update test cases to reflect new drawing activation UI 2025-05-18 19:09:29 +10:00
Dion Moult 992ad8e24a See #4699. Continue refactoring logic from drawing decorator into DecoratorData. 2025-05-18 15:32:55 +10:00
Dion Moult 1de2515fcd More bSDD tests to cover all current functionality 2025-05-18 12:05:33 +10:00
Thomas Krijnen 4bc88eac2f ifdefs around geometry handling for 4.3 rc schemas #6710 2025-05-17 16:01:28 +02:00
Dion Moult 22f10e5549 Fix #6669. bSDD now supports test dictionaries, refactor domain to dictionary, support zero length keyword searches, multi-dict support, and basic tests.
Feature tests can now have more sophisticated per UIList item spies.
2025-05-17 21:28:12 +10:00
Andrej730 8e34c1d7df Fix error using bim.updating_representation for object with material set
Possibly occurred after 4c255cc, but haven't investigated deeper.

@BIMvoice
The error you were reporting about Split Along Edge operator was caused by this issue, should be fixed now too.
See https://jmp.sh/hgJEHsgH

Example error traceback:
```
Error: Python: Traceback (most recent call last):
  File "C\bonsai\bim\ifc.py", line 487, in execute_ifc_operator
    result = getattr(operator, "_execute")(context)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C\bonsai\bim\module\geometry\operator.py", line 541, in _execute
    self.update_obj_mesh_representation(context, obj)
  File "C\bonsai\bim\module\geometry\operator.py", line 666, in update_obj_mesh_representation
    tool.Ifc.link(new_representation, data)
  File "C\bonsai\tool\ifc.py", line 230, in link
    IfcStore.link_element(element, obj)
  File "C\bonsai\bim\ifc.py", line 258, in link_element
    tool.Geometry.get_mesh_props(obj).ifc_definition_id = element.id()
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C\bonsai\tool\geometry.py", line 92, in get_mesh_props
    return mesh.BIMMeshProperties
           ^^^^^^^^^^^^^^^^^^^^^^
ReferenceError: StructRNA of type Mesh has been removed
```
2025-05-16 19:43:42 +05:00
Andrej730 4369baa226 bim.split_along_edge - improve descriptions, add error handling 2025-05-16 19:43:42 +05:00
Andrej730 7941d71feb SvgWriter - make required arguments more explicit 2025-05-16 19:43:42 +05:00
Andrej730 68d967c2e5 pyproject.toml - enableTypeIgnoreComments=false 2025-05-16 19:43:41 +05:00
Andrej730 4ea9a598c7 operator.drawing - remove unnecessary ifc operators 2025-05-16 19:43:41 +05:00
Andrej730 06d1c81954 black . 2025-05-16 19:43:41 +05:00
Andrej730 aa5f08880b typing 2025-05-16 19:43:41 +05:00
Ryan Schultz 9e6320d545 A preference to list those classes that are not affected by the 'cut decorator'. 2025-05-15 21:45:53 -05:00
Bruno Perdigão f39d3d4b9c Fix polyline plane method decorator to show properly.
The `plane_method` where not being cleared after canceling or confirming
the polyline operator.
2025-05-15 21:28:31 -03:00
Bruno Perdigão 5c72cf35bd See #6208. Change wall merge tolerance for more consistent behavior. 2025-05-15 19:08:04 -03:00
Bruno Perdigão b9a435e1c3 Fix polyline tool calculation when dealing with 180 degrees angles. 2025-05-15 17:41:32 -03:00
Bruno Perdigão 92f2559ce0 See #6536. Recalculate profile only for selected objects. 2025-05-15 17:41:32 -03:00
Bruno Perdigão acf9713943 Fix aggregate decorator after #6537. 2025-05-15 17:41:32 -03:00
Andrej730 a77125ff12 bim.add_drawing - document camera placement behaviour in description
See https://i.imgur.com/VfOjGHk.png
2025-05-15 20:01:02 +05:00
Andrej730 c8841bcb6a Add a test for be65774 2025-05-15 20:01:01 +05:00
Andrej730 b20997dea8 Fix tests missing assert statement - add assert and fix them 2025-05-15 19:45:11 +05:00
Andrej730 292aca6685 Remove unnecessary IFC operators 2025-05-15 19:45:11 +05:00
Andrej730 3a824ec969 Allow adding arrays to openings and spatial elements 0b8fb41 0e37528 2025-05-15 19:45:11 +05:00
Andrej730 bea9f42f87 Annotator.add_text - remove ancient dead code
It was deprecated a while ago in 43869c3, when we stopped using curves for text annotations. bim.resize_text was also using curve annotations and now also was unused
2025-05-15 19:45:11 +05:00
Andrej730 e4a50b32ce typing 2025-05-15 19:40:44 +05:00
Andrej730 76611024b6 Remove drawing styles attributes prop (a50c633cf) 2025-05-15 10:30:35 +05:00
Andrej730 92ee82374c EPset_Drawing.Metadata - add description
See https://i.imgur.com/0fR9fRj.png
2025-05-14 19:40:46 +05:00
Andrej730 af81aa1fae Reapply 06f1477 to fix not appearing IFC Description button
Not that skipping non-string properties was a bad idea, but inserting it right at the beginning was - non-string IFC properties were missing IFC descriptions because of it. Moved it past `Attribute` check to avoid this issue.

For anyone hearing about this feature for the first time - https://imgur.com/a/dNRao3R

I assume this was also the issue that was encountered in #5892
2025-05-14 19:40:45 +05:00
Andrej730 16f9f91561 util.doc - statically type json structures 2025-05-14 19:40:45 +05:00
Andrej730 3a52a848ae draw_custom_context_menu - small refactor to improve readibility 2025-05-14 19:40:45 +05:00
Andrej730 a50c633cf4 Remove drawing styles attributes
It's been awhile since they were unused (2c301d7) and now they're replaced with EPset_Drawing.Metadata
2025-05-14 19:40:45 +05:00
Andrej730 6251750133 common method for getting active drawing style 2025-05-14 19:40:45 +05:00
Andrej730 acef6379e8 drawing styles - add descriptions to render types, remove NONE type
NONE type removed as it was unused and basically meant the same as 'VIEWPORT'.

Example - https://i.imgur.com/7r6CJdg.png
2025-05-14 19:40:45 +05:00
Andrej730 c2860e6e25 drawing.operator not to use DrawingsData 2025-05-14 19:40:44 +05:00
Andrej730 b8e5cd740a Use common method for getting active drawing entity 2025-05-14 19:40:44 +05:00
Andrej730 9188dc60ab typing 2025-05-14 19:40:44 +05:00
Thomas Krijnen 8ce56db0c3 Align preprocessor flags #6706 2025-05-14 13:09:17 +02:00
Andrej730 b91b1401f4 util.get_aggregate - make it more readable (5597095) 2025-05-13 17:44:42 +05:00
Andrej730 0e37528915 Skip IfcOpeningElements arrays during base import, setup constraint on opening load #6688 2025-05-13 17:23:46 +05:00
Andrej730 9991c4aa56 IfcImporter.create_generic_elements - prevent argument mutation
as it may have unexpected consequences
2025-05-13 17:23:46 +05:00
Andrej730 0ef3983035 bim.show_opening - simplify code 2025-05-13 17:23:46 +05:00
Andrej730 0f6e6a48f4 bim.show_openings - expand description 2025-05-13 17:23:45 +05:00
Andrej730 3d225beafd Add notes on Tekla accommodations (7c3870595) 2025-05-13 16:59:12 +05:00
Andrej730 6a91bbb720 typing 2025-05-13 16:59:12 +05:00
Hannes 1f23e4ba18 Fixed typo in __init__.py
See https://github.com/IfcOpenShell/IfcOpenShell/issues/6625
2025-05-13 13:18:24 +02:00
Andrej730 2ebcffc7d4 Geometry changes will be lost - show warning only for IFC objects
Previously it would also occur for vanilla Blender objects not connected to IFC.
2025-05-13 13:26:54 +05:00
Andrej730 3d4321f713 Prevent class assignment in non-object mode #6697
Example - https://i.imgur.com/KQYQLQZ.png
2025-05-13 13:26:54 +05:00
Andrej730 f8799305e1 black . 2025-05-13 13:26:54 +05:00
Dion Moult 7c38705958 Accommodate invalid missing mapped representations and items coming from Tekla (2023) 2025-05-13 13:09:37 +10:00
Bruno Perdigão f6fc0cfa0f See #6537. Fix object display type when exiting aggregate mode. 2025-05-12 22:01:40 -03:00
Richard Brice 686e9c946b Adds object placement to stationing referent. 2025-05-12 16:14:53 -07:00
sboddy 3085072751 Improve drawing activation as per #6653 (#6659)
Drawings are now activated in a bit different way - not by the camera button located above the drawings list, but by using the camera indicator next to each drawing. Which allows do change active drawing very quickly. All activation options (shift+, alt+) are also available from this indicator.

Now - https://imgur.com/a/DmnWkul

Before - https://imgur.com/a/zjPDkPa
2025-05-12 19:46:34 +05:00
Andrej730 0328be4093 Drawing styles - remove deprecated properties / property values
Deprecated properties:
- scene.render.engine + BLENDER_EEVEE
- scene.render.simplify_shadows
- scene.render.simplify_shadows_render
- space.overlay.backwire_opacity
- space.overlay.show_edges
- space.overlay.show_occlude_wire
- scene.display.shading.wireframe_color_type + MATERIAL
2025-05-12 19:10:04 +05:00
Andrej730 6218d3902e bim.activate_drawing_style - show warnings if some property failed to be set
Otherwise errors were passing silently (just console errors) and remained unfixed. 'PYTEST_VERSION' is needed to be even more strict when we're running pytests, which allows us to catch outdated Blender properties in time.

Example - https://i.imgur.com/K7Trkxf.png
2025-05-12 19:10:04 +05:00
Andrej730 be1836fa48 Mark thumbnails as outdated instead of updating them right away
As user may never open them in type manager
2025-05-12 19:08:46 +05:00
Andrej730 22b5db8a1e ci-black-formatting - print diff 2025-05-12 19:08:46 +05:00
Andrej730 4268e1376c Address PR review changes #6660 2025-05-12 19:08:46 +05:00
Andrej730 e2c1a54e0a typing 2025-05-12 19:08:46 +05:00
falken10vdl d1be660469 Update type manager preview on updating paramteric door/window type (#6656) 2025-05-12 12:41:02 +05:00
Kristoffer Andersen 3ae5227b57 Update ci-ifcopenshell-conda-daily.yml
remove env var
2025-05-11 19:59:05 +02:00
Kristoffer Andersen cdc994ca76 Update ci-ifcopenshell-conda-daily.yml
move rattler build output dir outside of build dir
2025-05-11 19:58:27 +02:00
Kristoffer Andersen 0ad4087f0a Update ci-ifcopenshell-conda-daily.yml
remove dotnet to save space on ubuntu runner
2025-05-11 19:28:12 +02:00
Kristoffer Andersen 4b7ca648c6 Update ci-ifcopenshell-conda-daily.yml
try to get even more space on ubuntu ci runner
2025-05-11 19:23:28 +02:00
Kristoffer Andersen 17bd6e6b0c Update ci-ifcopenshell-conda-daily.yml
fix missing comma
2025-05-11 19:12:34 +02:00
Kristoffer Andersen 7b80803cbe separate osx-x86 and osx-arm64 for the conda daily builds (#6694)
* separate osx-x86 and osx-arm64 for the conda daily builds

* remove running this in PR. No need.
2025-05-11 18:59:52 +02:00
Kristoffer Andersen eaf67d83f9 Update ci-ifcopenshell-conda-daily.yml
strip daily conda build version date string
2025-05-11 16:04:21 +02:00
Kristoffer Andersen 41f754e15b Update ci-ifcopenshell-conda-daily.yml
Update conda ci
2025-05-11 15:53:29 +02:00
Kristoffer Andersen b7cdb86327 use rattler-build to speed up daily conda builds and fix deps (#6692)
* use rattler-build to speed up daily conda builds and add typing_extensions

* minor update
2025-05-11 09:37:02 +02:00
falken10vdl 809a20dc99 system info copied when clicking in Bonsai Version (#6660) 2025-05-09 16:09:37 -05:00
Andrej730 f5e41d18c6 drawing.operator - reuse blender.get_view3d_space 2025-05-09 19:26:28 +05:00
Andrej730 60a8c45ad7 Fix IFC operators issues for drawing styles operations (38cf88d) 2025-05-09 19:26:28 +05:00
Andrej730 73f98bfe4b Remove 2 years old fallback for missing ShadingStyles prop in drawing's pset (38cf88d) 2025-05-09 19:26:28 +05:00
Andrej730 958686958b drawing.prop - reuse generic pset update callback 2025-05-09 19:26:28 +05:00
Andrej730 e0b61c5794 Fix drawing styles update on underlay toggle after 9329390 2025-05-09 19:26:28 +05:00
Andrej730 5aa5552759 Fix drawing test after be657748f 2025-05-09 19:26:27 +05:00
Andrej730 82a19cee7b generate_drawing_matrix - simplify logic 2025-05-09 19:26:27 +05:00
Andrej730 43d4adbd9d Fix tests after specifying types more precisely in def9352ad 2025-05-09 19:26:27 +05:00
Andrej730 1d3c71985f typing 2025-05-09 19:26:27 +05:00
Andrej730 381fca0812 black . 2025-05-09 19:26:27 +05:00
Dion Moult 0f37eca333 Fix bug in IFC4X3 when convering properties to quantities 2025-05-09 15:24:30 +10:00
Dion Moult 6bf9b8b0c9 Fix bug where appending assets could accidentally create duplicate contexts 2025-05-09 15:24:17 +10:00
Bruno Perdigão 9361cfee28 See #6536. Update cardinal point when editing IfcMaterialProfileSetUsage. 2025-05-08 22:06:48 -03:00
Bruno Perdigão 1c205d494a See #6332. Fix issue where imperial precision were being used by metric formatting. 2025-05-08 19:15:51 -03:00
Ryan Schultz 4d1d224a34 fix #6683 - When placing Level (Plan) annotation, have it take on the 'z' dimension of the 3d cursor, verses the front plane of the drawing camera 2025-05-08 17:04:34 -05:00
Andrej730 4976af5e66 drawing.create_camera - remove setting location line that had no effect
Apparently it had no effect from the beginning in 074fe0d and lost any use in 6a8292d, when we started to base position on current cursor's location.
2025-05-08 19:23:24 +05:00
Andrej730 be657748f3 Align new Model drawings to viewport by default
Previously it was creating not very useful top-down camera at `(0,0,0)`, now it should be more intuitive.

Example - https://imgur.com/a/GHrvCSJ
2025-05-08 19:23:24 +05:00
Andrej730 def9352ade typing 2025-05-08 19:23:23 +05:00
Josef Wienerroither a20002ecca Update links and version info ifcmax.rst 2025-05-08 14:42:50 +02:00
Andrej730 b7a0f230ad Fix confused target_view callbacks (1619ae854)
Fix using the same function name twice leading to DocProperties using camera callback too.
2025-05-08 15:50:00 +05:00
Andrej730 76529a06bb Fix resetting perspective camera on drawing activation (1619ae854) 2025-05-08 15:41:34 +05:00
Andrej730 dcdae67a2d Fix typo in 1619ae854 2025-05-08 15:41:34 +05:00
Andrej730 5a7a368169 ifcopenshell.file.Transaction - add static typing 2025-05-08 15:41:34 +05:00
Thomas Krijnen ec13294430 python39 compatibility 2025-05-08 08:37:41 +02:00
Andrej730 653b88ba18 Bonsai - make undo errors visible to user
In theory there are possible undo errors, when some step wasn't registered in undo system and undo then won't be able to undo change from IFC. The problem is, those errors could be very dangerous but they occur in background, undetected by users, they're only visible as console traceback.

After this commit we'll try to detect them and set `last_error`, which will show usual Bonsai error panel in Properties.

@theoryshaw just in case, as you will probably find this kind of error 😁

Example traceback user might see in system console (or in their error report from "Copy Error Message").
```
last_error: Undo transaction operations:
- {'action': 'create', 'value': {'id': 1560, 'type': 'IfcGrid', 'GlobalId': '05Y8D_Ja12HfiEbmc$D3WN', 'OwnerHistory': None, 'Name': None, 'Description': None, 'ObjectType': None, 'ObjectPlacement': None, 'Representation': None, 'UAxes': None, 'VAxes': None, 'WAxes': None, 'PredefinedType': None}}
- {'action': 'edit', 'id': 1560, 'index': 2, 'old': None, 'new': 'Grid'}
- {'action': 'edit', 'id': 1221, 'index': 4, 'old': ({'id': 1297}, {'id': 1247}, {'id': 1220}, {'id': 1272}), 'new': ({'id': 1272}, {'id': 1247}, {'id': 1220}, {'id': 1560}, {'id': 1297})}
- {'action': 'create', 'value': {'id': 1561, 'type': 'IfcCartesianPoint', 'Coordinates': (0.0, 0.0, 0.0)}}
- {'action': 'create', 'value': {'id': 1562, 'type': 'IfcDirection', 'DirectionRatios': (0.0, 0.0, 1.0)}}
- {'action': 'create', 'value': {'id': 1563, 'type': 'IfcDirection', 'DirectionRatios': (1.0, 0.0, 0.0)}}
- {'action': 'create', 'value': {'id': 1564, 'type': 'IfcAxis2Placement3D', 'Location': {'id': 1561}, 'Axis': {'id': 1562}, 'RefDirection': {'id': 1563}}}
- {'action': 'create', 'value': {'id': 1565, 'type': 'IfcLocalPlacement', 'PlacementRelTo': None, 'RelativePlacement': {'id': 1564}}}
- {'action': 'edit', 'id': 1565, 'index': 0, 'old': None, 'new': {'id': 66}}
- {'action': 'edit', 'id': 1560, 'index': 5, 'old': None, 'new': {'id': 1565}}
- {'action': 'create', 'value': {'id': 1566, 'type': 'IfcGridAxis', 'AxisTag': 'A', 'AxisCurve': None, 'SameSense': True}}
- {'action': 'edit', 'id': 1560, 'index': 7, 'old': None, 'new': ({'id': 1566},)}
- {'action': 'create', 'value': {'id': 1567, 'type': 'IfcCartesianPoint', 'Coordinates': (-2.0, 0.0)}}
- {'action': 'create', 'value': {'id': 1568, 'type': 'IfcCartesianPoint', 'Coordinates': (22.0, 0.0)}}
- {'action': 'create', 'value': {'id': 1569, 'type': 'IfcPolyline', 'Points': ({'id': 1567}, {'id': 1568})}}
- {'action': 'edit', 'id': 1566, 'index': 1, 'old': None, 'new': {'id': 1569}}
- {'action': 'create', 'value': {'id': 1570, 'type': 'IfcGridAxis', 'AxisTag': 'B', 'AxisCurve': None, 'SameSense': True}}
- {'action': 'edit', 'id': 1560, 'index': 7, 'old': ({'id': 1566},), 'new': ({'id': 1566}, {'id': 1570})}
- {'action': 'create', 'value': {'id': 1571, 'type': 'IfcCartesianPoint', 'Coordinates': (-2.0, 10.0)}}
- {'action': 'create', 'value': {'id': 1572, 'type': 'IfcCartesianPoint', 'Coordinates': (22.0, 10.0)}}
- {'action': 'create', 'value': {'id': 1573, 'type': 'IfcPolyline', 'Points': ({'id': 1571}, {'id': 1572})}}
- {'action': 'edit', 'id': 1570, 'index': 1, 'old': None, 'new': {'id': 1573}}
- {'action': 'create', 'value': {'id': 1574, 'type': 'IfcGridAxis', 'AxisTag': 'C', 'AxisCurve': None, 'SameSense': True}}
- {'action': 'edit', 'id': 1560, 'index': 7, 'old': ({'id': 1566}, {'id': 1570}), 'new': ({'id': 1566}, {'id': 1570}, {'id': 1574})}
- {'action': 'create', 'value': {'id': 1575, 'type': 'IfcCartesianPoint', 'Coordinates': (-2.0, 20.0)}}
- {'action': 'create', 'value': {'id': 1576, 'type': 'IfcCartesianPoint', 'Coordinates': (22.0, 20.0)}}
- {'action': 'create', 'value': {'id': 1577, 'type': 'IfcPolyline', 'Points': ({'id': 1575}, {'id': 1576})}}
- {'action': 'edit', 'id': 1574, 'index': 1, 'old': None, 'new': {'id': 1577}}
- {'action': 'create', 'value': {'id': 1578, 'type': 'IfcGridAxis', 'AxisTag': '01', 'AxisCurve': None, 'SameSense': True}}
- {'action': 'edit', 'id': 1560, 'index': 8, 'old': None, 'new': ({'id': 1578},)}
- {'action': 'create', 'value': {'id': 1579, 'type': 'IfcCartesianPoint', 'Coordinates': (0.0, -2.0)}}
- {'action': 'create', 'value': {'id': 1580, 'type': 'IfcCartesianPoint', 'Coordinates': (0.0, 22.0)}}
- {'action': 'create', 'value': {'id': 1581, 'type': 'IfcPolyline', 'Points': ({'id': 1579}, {'id': 1580})}}
- {'action': 'edit', 'id': 1578, 'index': 1, 'old': None, 'new': {'id': 1581}}
- {'action': 'create', 'value': {'id': 1582, 'type': 'IfcGridAxis', 'AxisTag': '02', 'AxisCurve': None, 'SameSense': True}}
- {'action': 'edit', 'id': 1560, 'index': 8, 'old': ({'id': 1578},), 'new': ({'id': 1578}, {'id': 1582})}
- {'action': 'create', 'value': {'id': 1583, 'type': 'IfcCartesianPoint', 'Coordinates': (10.0, -2.0)}}
- {'action': 'create', 'value': {'id': 1584, 'type': 'IfcCartesianPoint', 'Coordinates': (10.0, 22.0)}}
- {'action': 'create', 'value': {'id': 1585, 'type': 'IfcPolyline', 'Points': ({'id': 1583}, {'id': 1584})}}
- {'action': 'edit', 'id': 1582, 'index': 1, 'old': None, 'new': {'id': 1585}}
- {'action': 'create', 'value': {'id': 1586, 'type': 'IfcGridAxis', 'AxisTag': '03', 'AxisCurve': None, 'SameSense': True}}
- {'action': 'edit', 'id': 1560, 'index': 8, 'old': ({'id': 1578}, {'id': 1582}), 'new': ({'id': 1578}, {'id': 1582}, {'id': 1586})}
- {'action': 'create', 'value': {'id': 1587, 'type': 'IfcCartesianPoint', 'Coordinates': (20.0, -2.0)}}
- {'action': 'create', 'value': {'id': 1588, 'type': 'IfcCartesianPoint', 'Coordinates': (20.0, 22.0)}}
- {'action': 'create', 'value': {'id': 1589, 'type': 'IfcPolyline', 'Points': ({'id': 1587}, {'id': 1588})}}
- {'action': 'edit', 'id': 1586, 'index': 1, 'old': None, 'new': {'id': 1589}}
Traceback (most recent call last):
  File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\ifcopenshell\file.py", line 352, in undo
    transaction.rollback()
  File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\ifcopenshell\file.py", line 167, in rollback
    element = self.file.by_id(operation["id"])
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\ifcopenshell\file.py", line 510, in by_id
    return self[id]
           ~~~~^^^^
  File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\ifcopenshell\file.py", line 497, in __getitem__
    return entity_instance(self.wrapped_data.by_id(key), self)
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\ifcopenshell\ifcopenshell_wrapper.py", line 8884, in by_id
    return _ifcopenshell_wrapper.file_by_id(self, id)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: Instance #1565 not found

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\bonsai\bim\ifc.py", line 426, in callback
    tool.Ifc.get().undo()
  File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\ifcopenshell\file.py", line 354, in undo
    raise UndoSystemError("Error during transaction undo.", transaction) from e
ifcopenshell.file.UndoSystemError: Error during transaction undo.
```
2025-05-07 18:54:57 +05:00
Andrej730 f8489779f6 Fix missing IFC undo for all activate drawing operations
Very dangerous bug that could create a situation when .blend will go out of sync with IFC leading to unpredictable issues.
Example issue:
1. Create a drawing and activate it.
2. Create a grid.
3. Activate the drawing again.
4. Undoing just drawing activation here would work fine - annotations would be still floating in IFC without objects, but that would be fixed on the next drawing activation

To make it more dangerous undo grid creation. Then you'll get the traceback similar to the one below. The most dangerous part of it, that user won't see an error, it will just pop up in the system console silently.
```
Traceback (most recent call last):
  File "\bonsai\bim\handler.py", line 244, in undo_post
    IfcStore.undo(until_key=props.last_transaction)
  File "\bonsai\bim\ifc.py", line 533, in undo
    transaction["rollback"](transaction["data"])
  File "\bonsai\bim\ifc.py", line 449, in <lambda>
    operator, rollback=lambda d: tool.Ifc.get().undo(), commit=lambda d: tool.Ifc.get().redo()
                                 ^^^^^^^^^^^^^^^^^^^^^
  File "\ifcopenshell\file.py", line 345, in undo
    transaction.rollback()
  File "\ifcopenshell\file.py", line 161, in rollback
    element = self.file.by_id(operation["id"])
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\ifcopenshell\file.py", line 498, in by_id
    return self[id]
           ~~~~^^^^
  File "\ifcopenshell\file.py", line 485, in __getitem__
    return entity_instance(self.wrapped_data.by_id(key), self)
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\ifcopenshell\ifcopenshell_wrapper.py", line 8884, in by_id
    return _ifcopenshell_wrapper.file_by_id(self, id)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: Instance 1565 not found
```
2025-05-07 18:54:57 +05:00
Andrej730 64df82a76b ifcopenshell.file - save last failed transaction for introspection 2025-05-07 18:54:57 +05:00
Andrej730 af4fbd6750 Document features available for perspective camera in it's description
See https://i.imgur.com/8ZPJ8ag.png
2025-05-07 18:54:56 +05:00
Andrej730 1619ae8543 Expose camera type to UI and automatically update IFC camera type on drawing activation
Noticed fixing #6670 that though example project in this issue had camera named "PERSPECTIVE" but it was saved to IFC as orthogonal and there is no straightforward way to change it, besides changing Blender camera props and updating representation.

Example - https://imgur.com/a/GQ9A3EK

It's kind of exposed to UI now, but there's still bunch of issues with it to address.
2025-05-07 18:54:56 +05:00
Andrej730 41eadf735e add_representation - more readable error on using panoramic camera 2025-05-07 18:54:56 +05:00
Andrej730 172775d4ed Fix constant camera representation update because of the float garbage values
Similar issue to 82f25f5

Basically matrix is a little bit different every time you activate drawing even if user made no changes to camera position. My guess is basically all float values stored by Blender somewhere deep as float32 but when you access matrix world, Blender converts them to Python floats (which are float64) and some garbage values introduced along the way creating this noise.
2025-05-07 18:54:56 +05:00
Andrej730 d539cad1c2 Fix mistake in fd30a6b #6670 2025-05-07 18:54:56 +05:00
Andrej730 94cf56e080 typing 2025-05-07 18:54:56 +05:00
Geert Hesselink cf253824ca update latest simple_spf submodue 2025-05-07 15:48:01 +02:00
Andrej730 9c73f17303 add pytest-bdd to bonsai daily cli 2025-05-06 20:08:09 +05:00
Andrej730 a6f523039b bonsai pytest.ini - specify plugins explicitly
To make it possible to run the tests with PYTEST_DISABLE_PLUGIN_AUTOLOAD set, which is very useful if you don't want to start Blender anytime you run any tests on your system.
2025-05-06 20:07:44 +05:00
Andrej730 c2f9bfeb8d Fix bim search tests after 0b22333 2025-05-06 20:07:44 +05:00
Andrej730 3ab1c2e0f9 outliner.delete - make errors from attempt to delete locked object less scary
Traceback errors give users an impression that some broke and maybe project now is in some invalid state and they should ctrl-z immediately, error reports are much more friendly.

Before - https://i.imgur.com/N4fUU1M.png
After - https://i.imgur.com/6On1Be0.png
2025-05-06 19:03:05 +05:00
Andrej730 0ed964daa2 object.delete, outliner.delete - move to common method is_batch logic 2025-05-06 19:03:04 +05:00
Andrej730 03f619a77c aggregate decorator indicator - make it more intuitive
Example - https://imgur.com/a/RoFQKa1
2025-05-06 19:03:04 +05:00
Andrej730 e5ad35e2c8 bim.add_aggregate - use more common way to get all selected objects 2025-05-06 19:03:04 +05:00
Andrej730 297f15737d bim.add_aggregate - remove unused 'obj' property (c51aa91) 2025-05-06 19:03:03 +05:00
Andrej730 374b95efdf bim.add_aggregate - update description 2025-05-06 19:03:03 +05:00
Andrej730 744ed522b6 Fix error duplicating just an IfcElementAssembly (d2b05ea)
Example error traceback:
```
Traceback (most recent call last):
  File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\bonsai\bim\module\geometry\operator.py", line 1044, in execute
    return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\bonsai\bim\module\geometry\operator.py", line 1055, in execute_duplicate_operator
    IfcStore.execute_ifc_operator(operator, context)
  File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\bonsai\bim\ifc.py", line 458, in execute_ifc_operator
    result = getattr(operator, "_execute")(context)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\bonsai\bim\module\geometry\operator.py", line 1047, in _execute
    return OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\bonsai\bim\module\geometry\operator.py", line 1085, in execute_ifc_duplicate_operator
    old_to_new, new_active_obj = tool.Geometry.duplicate_ifc_objects(
                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\bonsai\tool\geometry.py", line 2131, in duplicate_ifc_objects
    tool.Root.recreate_aggregate(old_to_new)
  File "C:\Users\Andrej\AppData\Roaming\Blender Foundation\Blender\4.4\extensions\.local\lib\python3.11\site-packages\bonsai\tool\root.py", line 397, in recreate_aggregate
    tool.Blender.select_and_activate_single_object(bpy.context, tool.Ifc.get_object(new_aggregate[0]))
                                                                                    ~~~~~~~~~~~~~^^^
TypeError: 'NoneType' object is not subscriptable
```
2025-05-06 19:03:03 +05:00
Andrej730 f4bc3fffa3 bim.override_move - rename for clairty and add description 2025-05-06 19:03:02 +05:00
Andrej730 06a2cecb51 typing 2025-05-06 15:15:03 +05:00
Andrej730 b0f2785e99 black . 2025-05-06 11:11:07 +05:00
Thomas Krijnen c6c852c7f6 NB: fix get_cartesiantransformationoperator3d() 2025-05-05 21:47:57 +02:00
Geert Hesselink 5d920d5e31 header_entity type -> attr check 2025-05-05 20:54:19 +02:00
Geert Hesselink 2b522b71cb hasattr to isinstance() 2025-05-05 20:54:19 +02:00
Geert Hesselink d00c3735ec Update src/ifcopenshell-python/ifcopenshell/validate.py
Co-authored-by: Thomas Krijnen <t.krijnen@gmail.com>
2025-05-05 20:54:19 +02:00
Geert Hesselink bbdc9c45dd use repr(), compatible f.header 2025-05-05 20:54:19 +02:00
Geert Hesselink 5eb9cdf40d handle header entity string reversal 2025-05-05 20:54:19 +02:00
Geert Hesselink 689289bb60 validate ifcopenshell.simple_spf.file 2025-05-05 20:54:19 +02:00
Geert Hesselink d401e4c948 set set/get_description, add view_definitions & tests 2025-05-05 16:18:09 +02:00
Geert Hesselink cbe8fc1ebd mvd info to module, add testing, setattr & getattr 2025-05-05 16:18:09 +02:00
Geert Hesselink 9f11568aff Allow for dots in mvd value 2025-05-05 16:18:09 +02:00
Geert Hesselink 968e193f99 abort string when None, more explicit error handling 2025-05-05 16:18:09 +02:00
Geert Hesselink 2d06a79842 handle emptry description field 2025-05-05 16:18:09 +02:00
Geert Hesselink 808a16ba4a parsed mvd description in ifcopenshell.file 2025-05-05 16:18:09 +02:00
Andrej730 a67a6034a5 Use mesh.separate for vanilla Blender objects #6658 (0b22333)
mesh.separate works completely different in EDIT mode, previously we only supported it from object mode
2025-05-05 18:45:09 +05:00
Andrej730 8fe1a63339 Bonsai docs - add page to list hotkeys
See https://docs-unstable.bonsaibim.org/reference/hotkeys.html
2025-05-05 18:45:08 +05:00
Andrej730 1ce78f3c65 Override Shift-X and Shift-Del with IFC Delete
To prevent users from creating weird state of IFC model by using non-ifc `object.delete` triggered by those hotkeys by default.
2025-05-05 18:45:08 +05:00
Andrej730 57b35c68e4 IFC delete - just reuse object.delete if no IFC file active
So it will be possible for this operator to completely override object.delete
2025-05-05 18:45:08 +05:00
Andrej730 ff57b91691 typing 2025-05-05 18:45:08 +05:00
Andrej730 6e15219fad black . 2025-05-05 18:34:47 +05:00
Andrej730 979964f211 Merge branch 'falken10vdl-patch-2' into v0.8.0 2025-05-05 18:33:15 +05:00
falken10vdl 4ce2f81479 Skip non MESH and CURVE object listing elements for coloring #6657
So non MESH or CURVE objects are not listed in the Colour by property.

Now all unrelated empty objects are excluded from the coloring section as they were just taking space, see - https://imgur.com/sPA8I5V
2025-05-05 18:31:19 +05:00
Thomas Krijnen dbdde52435 validate.py Add test files 2025-05-05 14:21:19 +02:00
Thomas Krijnen 84568f34f0 validate.py be more specific in distinguishing type decl and entity refs 2025-05-05 14:20:47 +02:00
Geert Hesselink fe9c548ae0 Valid template header default (#6610)
* Valid template header default

* consistent version in tempalte file

* schema-dependent mvd
2025-05-05 13:56:40 +02:00
Andrej730 1714516a81 Fix bim.override_move issue with aggregates #6648 (c9fc543) 2025-05-05 14:59:31 +05:00
Andrej730 c4c3a2056b overridden delete operators - add descriptions 2025-05-02 19:58:24 +05:00
Andrej730 fad35b14f2 Fix dangerous bug with bim.override_outliner_delete
Essentialy outliner.delete should do the same as object.delete, but in our case the main implementation was in object.delete while outliner.delete was missing very important parts - e.g. handling arrays.

So if user could have remove part of an array without errors, leaving pset data invalid. See #6590, #6547.
Found this by trying to debug issues above.

Now outliner.delete will call object.delete explicitly, so the two stay connected.

I also had an idea that may be it somehow can produce #6632/#6619, but couldn't find a way to reproduce it. So it's still uncertain.
2025-05-02 19:58:24 +05:00
Andrej730 2382f8d7f8 color by property - add operators descriptions 2025-05-02 19:58:23 +05:00
Andrej730 9ae7a68c43 Display actual Bonsai version for live repo (reuse last commit date) 2025-05-02 19:58:23 +05:00
Andrej730 7a5e5fe602 Reuse get_last_commit_hash
Noticed that hash wasn't updating from live git repo because of this.
Also update commit data only when running the package, not extension.
2025-05-02 19:58:23 +05:00
Andrej730 839b2c9c1f typing 2025-05-02 19:58:23 +05:00
Andrej730 4aaaf6e8f5 Fix ifc2x3 errors editing IfcSurfaceStyleShading #6624 2025-05-02 12:32:49 +05:00
Andrej730 1b38b03532 new operator - bim.select_entity_by_guid
I find myself often having element guid copied (either from other viewer or from text editor or from elsewhere) and to find it in scene I needed to create a new search group, add a filter, paste guid and then search for it. So decided to add a shortcut for this that just takes guid from the clipboard and runs the search for it.

Location - https://i.imgur.com/3c5rm1N.png
2025-05-01 19:32:53 +05:00
Andrej730 cbf0fa88eb Reenable black formatting workflow
Since 3b1e893 black formatter workflow basically had no use since it wasn't reporting any issues (no one checks workflow logs unless workflow failed) and wasn't reliable for catching syntax errors either (black formatter catches syntax errors only when they get in the way of it's formatting the code, but now we delegate to python itself to catch syntax errors, see 406962287c).

And we also have some docs on how to apply black formatter - https://docs.bonsaibim.org/guides/development/code_style.html
2025-05-01 19:32:52 +05:00
Andrej730 6961ca8ce7 Fix bug creating a drawing if linked model schema doesn't match the main model schema
Co-Authored-By: Thomas Krijnen <1096535+aothms@users.noreply.github.com>
2025-05-01 19:32:52 +05:00
Andrej730 d758d4aef5 Windows - fix opening paths with special characters #6630 2025-05-01 19:32:52 +05:00
Andrej730 84240c870c More consistent Windows Bonsai .ifc file association
Rely on `winreg` instead of running powershell scripts with cmd commands + .reg file + weird workflow with the need to retype cmd command. Also use HKCU instead of HKCR because it doesn't require admin rights.

If someone hearing about this feature for the first time, here's a short demo - https://imgur.com/a/dfG44pY (icon appears too but after explorer restart).
2025-05-01 19:32:52 +05:00
Andrej730 4483d8e958 Fix typo 2025-05-01 19:32:52 +05:00
Thomas Krijnen 70440050db Bounds checking in variant array 2025-04-30 20:43:43 +02:00
Thomas Krijnen bc7527a73b Defensiveness against empty faces 2025-04-30 20:43:27 +02:00
Thomas Krijnen c8ad1ba0ee Fix faceset helper on advanced brep oriented edges #6604 2025-04-30 20:43:13 +02:00
Andrej730 c7a29d5604 Fix invalid IfcGrid cartesian points (774a770)
They were saved as 4d coordinates, not 2d.
2025-04-30 19:36:22 +05:00
Andrej730 814c16d3ee IfcGrid - add placement by default to avoid invalid IFC 2025-04-30 19:36:22 +05:00
Andrej730 3bedbfb82c Operator to run git diff on current ifc file
Short demo - https://imgur.com/a/TvnztNy
2025-04-30 19:36:22 +05:00
Andrej730 07f42b9126 typing 2025-04-30 19:36:21 +05:00
Andrej730 5b1da583d9 black . 2025-04-30 19:36:21 +05:00
Thomas Krijnen c624162f09 partial_success_is_success #6589 2025-04-30 14:53:53 +02:00
FabioPiccinini bc221cfa99 Use tool.Blender.get_view3d_area() to allow the use of separate windows (#6617) 2025-04-30 17:29:05 +05:00
c4rlosdias 5d02afa47e clear list of entities failed when execute Tester 2025-04-30 17:27:06 +05:00
Andrej730 406962287c ci-black-formatting - check syntax errors explicitly 2025-04-30 15:22:09 +05:00
Andrej730 241f7679b0 create_axis_curve - fix typo 2025-04-30 14:24:04 +05:00
Thomas Krijnen 0001071185 partial_success_is_success #6589 2025-04-29 22:03:18 +02:00
Thomas Krijnen 8d476c792d todo notes #6589 2025-04-29 21:43:49 +02:00
Thomas Krijnen 97c7aefad7 One more self-intersection check in cgal kernel #6589 2025-04-29 21:39:08 +02:00
Andrej730 34cafc761a Use actual text position instead of local pos rendering drawings text decorations
E.g. it's important if text is part of parametric array and it's local position may be constant, but it's constrainet by parent's position.
2025-04-29 19:26:43 +05:00
Andrej730 3fbde8db28 mesh.add_grid - display props in Blender project units
Example - https://i.imgur.com/koLiH7U.png
2025-04-29 19:26:42 +05:00
Andrej730 0b8fb41ac4 bim.add_array - guard from unexpected element types
So we can gradually expand it with the things we do know to support.
2025-04-29 19:26:18 +05:00
Andrej730 dc46afc227 Setup array constraints for annotations that use paramatric arrays
Previously you was able to add parametric array to annotations but constraint would be missing on file reload.
2025-04-29 19:26:18 +05:00
Andrej730 b8ee1d4d8d import_ifc - skip setup_arrays for annotations #6636 2025-04-29 19:26:18 +05:00
Andrej730 ecf461e1e0 typing 2025-04-29 19:26:17 +05:00
Andrej730 1856eeddeb Fix missed cases of loading unloaded linked files in 9316e0b (#6403) 2025-04-29 15:20:45 +05:00
Andrej730 afb2b9fdea Fix issues printing drawing with projects linked by relative path
The fix in f9bf889 wasn't related to this problem and was targetting another bug.
2025-04-29 15:20:45 +05:00
Andrej730 af2cb2896a Revert f9bf889 #6605
As this fix is no longer needed and covered by more general resolve_uri refactor
2025-04-29 15:20:45 +05:00
Andrej730 e792cf8fa0 get_uri, resolve_uri - refactor #6605
1) Use pathlib for consistency
2) Ensure consistent output - always use posix format and resolve paths (but keep the symlinks)
2025-04-29 15:20:45 +05:00
Thomas Krijnen 7b71e2ce2a Don't set xstep.cascade.unit as it appears to result in scaling during export #6623 2025-04-29 10:53:01 +02:00
Thomas Krijnen ce72337d3a Mark settings as internal #6623 2025-04-29 10:52:27 +02:00
Bruno Postle 0e9ca541c4 Linux escape double-quotes in filenames #6630
Special chars ('#{)$ were already escaped, but " wasn't
2025-04-28 21:34:05 +01:00
Andrej730 784576b249 IFC Join - support joining non-ifc Blender objects into representation items
Short demo - https://imgur.com/a/o9MJiIr
2025-04-28 19:14:21 +05:00
Andrej730 5e26e55a8c edit_meshlike_item - fix missing item object name update 2025-04-28 19:14:21 +05:00
Andrej730 1b110c4a84 reuse ifc.link for meshes 2025-04-28 19:14:20 +05:00
Andrej730 b94629133c geometry.name_item_object 2025-04-28 19:14:20 +05:00
Andrej730 55e270c135 IFC Join - fix error joining representation items (6ce4b5d)
It was trying to reaccess already removed ifc_definition_id leading to an error. Now we update id with a new item.
2025-04-28 19:14:20 +05:00
Andrej730 308d628b1e IFC Join - prevent joining rep items into non items
Because it was removing representation item object leaving Bonsai in confusing state, which may lead to errors later
2025-04-28 19:14:20 +05:00
Andrej730 5e16588eb7 IFC Join - prevent possible ghost ifc objects after join
Skip ifc objects of different type as they will be ignored by object.join either way and removing them produced ghost ifc objects.
2025-04-28 19:14:20 +05:00
Andrej730 e919a0643f IFC Join - add description 2025-04-28 19:14:19 +05:00
Andrej730 9835ef977f IFC Join - add poll check similar to object.join 2025-04-28 19:14:19 +05:00
Andrej730 10f84537de typing 2025-04-28 19:14:19 +05:00
Massimo Fabbro f9a0081b33 Fix bug in reload link ifc where in Windows gives an error if relative path is used 2025-04-28 06:22:13 +02:00
Massimo Fabbro 11462c388a Purge unecessary code 2025-04-28 06:22:13 +02:00
Ryan Schultz 4bcd9639f4 whoops 2025-04-27 16:55:45 -05:00
Ryan Schultz b856c40fe7 small tweak to previous commit 2025-04-27 16:54:34 -05:00
Ryan Schultz dd1c529544 Fix #6601 - If you duplicate something it makes the duplicate the active object.
also bulk deselect all objects_to_remove to speed things up a little.
2025-04-27 13:12:07 -05:00
Andrej730 c9fbec4d46 Revert "Bump IOS build in Bonsai" #6589
This reverts commit 50e7728f95.
2025-04-25 19:47:17 +05:00
Andrej730 c9437329ef IFC Mesh Separate - fix bug in 52af302bef separating multiple elements
Forgot to clear selection after processing each object, it could have led to incorrect results since mesh.separate is applied to the selection later.
2025-04-25 19:47:17 +05:00
Andrej730 a09e971674 IFC Mesh Separate - fix not updated items decorator after some item was separated
Noticed fixing bug from #6285
2025-04-25 19:47:17 +05:00
Andrej730 8ac04e32e5 bim.import_representation_items - add description 2025-04-25 19:47:16 +05:00
Andrej730 0634dc83d4 IFC Mesh Separate - fix bug when separating an item wouldn't change separated item
Mentioned by Ryan in #6285
2025-04-25 19:47:16 +05:00
Andrej730 5dc4b18d3f IFC Mesh Separate - reload representation just once if multiple items got separated 2025-04-25 19:47:16 +05:00
Andrej730 f84bca8017 IFC Mesh Separate - fix bug when new separated mesh wasn't relinked to a new ifc item 2025-04-25 19:47:16 +05:00
Andrej730 31a6c91c4b core.duplicate_drawing - remove debug print statements and unused code 2025-04-25 19:47:16 +05:00
Andrej730 30242d859e Fix core test for drawing duplication (0b1c049) 2025-04-25 19:47:16 +05:00
Andrej730 330799c799 core tests - very readable error for unpredicted return values
Previously:
E       TypeError: Object of type Prophecy is not JSON serializable

Now:
E           TypeError: Failed to serialize call: '{'name': 'duplicate_ifc_objects', 'args': ([<Prophecy for 'bonsai.core.tool.Ifc'>],), 'kwargs': {}}'.
E           Possibly due to unpredicted return value for some call.
E           See the list of the recent calls without return values:
E           - {'name': 'get_object', 'args': ('annotation',), 'kwargs': {}}
E           - {'name': 'run', 'args': ('group.assign_group',), 'kwargs': {'group': 'new_group', 'products': ['new_drawing']}}
E           - {'name': 'run', 'args': ('group.edit_group',), 'kwargs': {'group': 'new_group', 'attributes': {'Name': 'unique_name', 'ObjectType': 'DRAWING'}}}
E           - {'name': 'run', 'args': ('group.unassign_group',), 'kwargs': {'group': 'group', 'products': ['new_drawing']}}
2025-04-25 19:47:15 +05:00
Andrej730 e2c2234199 core tests - more readable errors
E.g. it shows now:
AttributeError: Interface 'bonsai.core.tool.Drawing' has no attribute 'weird_method'.

Instead of:
Prophecy <class 'abc.Drawing'> has no attribute weird_method
2025-04-25 19:47:15 +05:00
Andrej730 34efa9f459 typing 2025-04-25 19:47:15 +05:00
Andrej730 261de9b2be entity_instance.__getattr__ - document some attributes conversion to Python 2025-04-25 19:47:15 +05:00
Andrej730 52af302bef IFC Mesh Separate - add to object mode menu too for discoverability + support multiple selected elements
Short demo - https://imgur.com/a/PIuJ9Rq
2025-04-24 19:54:36 +05:00
Andrej730 529c74f6a9 create_camera - small optimization with util.shape 2025-04-24 19:54:36 +05:00
Andrej730 fd30a6b5eb create_camera - small optimization with early return 2025-04-24 19:54:35 +05:00
Andrej730 82f25f50cc Fix resaving camera to IFC on every drawing activation (32899eb)
`camera_props.update_representation` basically always returned True leading to `bim.update_representation` call on camera on every drawing activation. It was always True because `create_camera` wasn't setting `representation` and some other related camera props.

Changes:
- create_camera now setting representation
- ortho_scale, rather_x and raster_y now explicitly is tied to width, height and drawing scale props, not on depsgraph update. Depsgraph update wasn't allowing to set these props in time for create_camera to record representation correctly
- depsgraph update now only in charge of ortho_scale (just to be sure user won't touch it by hand) and scene.render resolution
2025-04-24 19:54:35 +05:00
Andrej730 d0dc31588d typing 2025-04-24 19:54:35 +05:00
Andrej730 68f71c715e Fix invalid cprops after camera data is recreated
Mentioned in  #5676, discoverd after 476604202a.

`bim.update_representation` removes previous `camera.data` and adds a new one making cprops invalid possibly leading to either invalid prop values or crashes (https://projects.blender.org/blender/blender/issues/137889).
2025-04-23 19:22:31 +05:00
Andrej730 6913060f0c bim.expand_target_view - remove trailing space from description 2025-04-23 19:22:31 +05:00
Andrej730 4427c73816 bim.override_mesh_separate - handle possible mode mismatch more gracefully
Other options LOOSE_PARTS and MATERIAL do support object mode.
2025-04-22 20:05:32 +05:00
Andrej730 d188155fac bim.override_mesh_separate - fix bug with skip_save
Previously you would use bim.override_mesh_separate from UI with any of the available `type` options, then running operator separately would leading to running this operator from F3 to always choose this options (possibly leading to errors in objects mode)
2025-04-22 20:05:31 +05:00
Andrej730 598dfd75e7 bim.override_origin_set - use operator_enum for menu 2025-04-22 20:05:31 +05:00
Andrej730 5c63cd7981 bim.override_mesh_separate - use operator_enum, hide Blender Separate
as it's now integrated in IFC separate and using Blender Separate for ifc elements is not safe
2025-04-22 20:05:31 +05:00
Andrej730 0b22333b69 bim.override_mesh_separate - integrate mesh.separate
mesh.separate will be used for all other selected non-ifc objects.

Included some code to support multple objects but there are still a few issue with it, I'll look into soon.
2025-04-22 20:05:31 +05:00
Andrej730 69481c2d63 bim.override_mesh_separate - fix bug separating elements (7bc4b51)
`obj` variable was used but never passed to `separate_element`

```
Error: Python: Traceback (most recent call last):
  File "\bonsai\bim\ifc.py", line 458, in execute_ifc_operator
    result = getattr(operator, "_execute")(context)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\bonsai\bim\module\geometry\operator.py", line 105, in _execute
    self.separate_element(element)
  File "\bonsai\bim\module\geometry\operator.py", line 175, in separate_element
    new_objs = [obj]
                ^^^
NameError: name 'obj' is not defined
```
2025-04-22 20:05:31 +05:00
Andrej730 671c887201 bim.override_mesh_separate - invoke menu if called without argument
Mimicking default mesh.separate behaviour.

@Gorgious56 I don't know if you've seen this before, but you might be interested - I've found a way to figure if operator wasn't called without any arguments.
2025-04-22 20:05:30 +05:00
Andrej730 0a6fbeccb0 bim.override_mesh_separate - use original Blender enum and description 2025-04-22 20:05:30 +05:00
Andrej730 4b3ca11c12 typing 2025-04-22 20:05:30 +05:00
Andrej730 f9bf8897bc Fix error linking ifc files with relative filepaths going upwards
Mentioned in https://github.com/IfcOpenShell/IfcOpenShell/pull/6540#issuecomment-2820938852
2025-04-22 16:37:05 +05:00
Andrej730 932939099a Add target_view prop to camera
This property was already set in 50b41f5 but Blender property was missing. Though nowit's not very useful as it just writes value to pset.
2025-04-22 16:37:05 +05:00
Andrej730 8d31d93531 typing 2025-04-22 16:37:05 +05:00
Andrej730 5c4f157624 readable_ifc_duration - add doc-string and note 2025-04-21 19:00:25 +05:00
Andrej730 f87c694432 Fix issue with custom diagram scale (1d5891f)
It was referring to non-existent prop "custom_diagram_scale"
2025-04-21 19:00:25 +05:00
Andrej730 38eedffd80 tool.Ifc - add some doc-strings 2025-04-21 19:00:25 +05:00
Andrej730 305439d27e Link.name - fix description
After f8828d3 we store linked filepath in .ifc project, so we don't support path relative to .blend anymore.
2025-04-21 19:00:24 +05:00
Andrej730 908bf02098 ifc5d - strip " Cost" suffix when importing csv
" Cost" is a suffix is added on export, so it makes to get rid of it on import.
2025-04-21 19:00:24 +05:00
Andrej730 c6176d364f ifc5d - use filename for imported schedule name
So it will be in sync with how export works.

Removed non_sor_fields is unrelated - forgot to remove before.
2025-04-21 19:00:24 +05:00
Andrej730 068814c095 bim.assign_document - add to documents UI 2025-04-21 19:00:24 +05:00
Andrej730 476604202a typing 2025-04-21 19:00:24 +05:00
Andrej730 558bda1aea black . 2025-04-21 19:00:23 +05:00
Dion Moult 426f1b91f2 See #2783. Don't show 0 durations for clarity. 2025-04-21 22:59:14 +10:00
Dion Moult d1b1b08174 See #2783. Duration parsing no longer uses isodate to remain faithful to formatting. 2025-04-21 22:39:23 +10:00
Thomas Krijnen c1513190d4 Debug build_win.yml 2025-04-21 13:17:14 +02:00
Andrej730 50e7728f95 Bump IOS build in Bonsai
New build resolves #6529 #6576
2025-04-21 13:57:25 +05:00
Andrej730 4e00a8d596 bonsai Makefile - specify shell explicitly
To avoid Windows trying to use cmd
2025-04-21 13:57:25 +05:00
Massimo Fabbro 266cc32336 Add calculating functions for earthworks 2025-04-21 10:48:00 +02:00
Andrej730 f8ad05cb54 Use isodate to parse iso durations #2783
Related #6585

Co-Authored-By: sboddy <12862430+sboddy@users.noreply.github.com>
2025-04-21 12:45:40 +05:00
Andrej730 311f9b96e7 black . 2025-04-21 12:45:40 +05:00
Dion Moult 56bfe80b3b Fix #6501. Fix #6104. Fix #6189. You can now change wall offset convention after drawing walls.
People kept on getting confused with the align tool thinking it changed
the baseline to reference line offset whereas it actually just aligned
the object bodies. This tool now does exactly that.

Also start refactoring the "DumbWall" classes into the tools so we can
test them properly.
2025-04-21 17:17:09 +10:00
Dion Moult 2db6b860b2 See #6501. Fix bug where wall alignment didn't strictly check for poll requirements prior to executing operator. 2025-04-21 15:38:26 +10:00
Ryan Schultz 5af2c3914a When \n stopped the world. fix previous commit.
```
os: Windows
os_version: 10.0.19045
python_version: 3.11.7
architecture: ('64bit', 'WindowsPE')
machine: AMD64
processor: Intel64 Family 6 Model 158 Stepping 10, GenuineIntel
blender_version: 4.2.1 LTS
bonsai_version: 0.0.0
bonsai_commit_hash: f8ebc2d
bonsai_commit_date: 2025-04-20T10:30:03-05:00
last_actions:
last_error: Traceback (most recent call last):
  File "C:\Users\Ryan Schultz\AppData\Roaming\Blender Foundation\Blender\4.2\extensions\.local\lib\python3.11\site-packages\bonsai\bim\module\project\operator.py", line 1003, in finish_loading_project
    bpy.ops.bim.load_project_elements()
  File "c:\Program Files\Blender Foundation\Blender 4.2\4.2\scripts\modules\bpy\ops.py", line 109, in __call__
    ret = _op_call(self.idname_py(), kw)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: Error: Python: Traceback (most recent call last):
  File "C:\Users\Ryan Schultz\AppData\Roaming\Blender Foundation\Blender\4.2\extensions\.local\lib\python3.11\site-packages\bonsai\bim\module\project\operator.py", line 1063, in execute
    bonsai.bim.schema.reload(self.file.schema_identifier)
  File "C:\Users\Ryan Schultz\AppData\Roaming\Blender Foundation\Blender\4.2\extensions\.local\lib\python3.11\site-packages\bonsai\bim\schema.py", line 67, in reload
    ifc = IfcSchema(schema_identifier)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Ryan Schultz\AppData\Roaming\Blender Foundation\Blender\4.2\extensions\.local\lib\python3.11\site-packages\bonsai\bim\schema.py", line 43, in __init__
    self.load_pset_templates()
  File "C:\Users\Ryan Schultz\AppData\Roaming\Blender Foundation\Blender\4.2\extensions\.local\lib\python3.11\site-packages\bonsai\bim\schema.py", line 57, in load_pset_templates
    self.psetqto.templates.append(ifcopenshell.open(path))
                                  ^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Ryan Schultz\AppData\Roaming\Blender Foundation\Blender\4.2\extensions\.local\lib\python3.11\site-packages\ifcopenshell\__init__.py", line 183, in open
    return file(f)
           ^^^^^^^
  File "C:\Users\Ryan Schultz\AppData\Roaming\Blender Foundation\Blender\4.2\extensions\.local\lib\python3.11\site-packages\ifcopenshell\file.py", line 295, in __init__
    raise exc(msg)
ifcopenshell.Error: Syntax error during parse, check logs
Location: c:\Program Files\Blender Foundation\Blender 4.2\4.2\scripts\modules\bpy\ops.py:109

ifc: projection lines.ifc
schema: IFC4
preprocessor_version: IfcOpenShell 0.0.0
originating_system: Bonsai 0.8.3-alpha250417-f3228c3
```
2025-04-20 11:20:05 -05:00
Ryan Schultz f8ebc2d156 Property in EPset_Drawing for SVG classes that 'rise to the top'. per: https://community.osarch.org/discussion/comment/22689/#Comment_22689 2025-04-20 10:30:03 -05:00
Thomas Krijnen f3228c31f8 Debug build_win.yml 2025-04-20 08:42:03 +02:00
Dion Moult 86d481f4f8 Revert "Fix #4130. Skip invalid topology."
This reverts commit bb598d848a.
2025-04-20 15:07:45 +10:00
Ryan Schultz 05f79c844a For cut lines through terrain. I still plan to make these a preference, however. 2025-04-19 12:46:52 -05:00
Thomas Krijnen b87a7b3d0a cgal kernel - conditionally reorient extrusion basis #6529 2025-04-19 18:50:51 +02:00
Massimo Fabbro e54acd83f8 See #5149
Now it is possible to assign a resource also to a cost item in a schedule of rates
2025-04-19 16:40:47 +02:00
Massimo Fabbro f8e52c8a12 See #6570. Add a button to refresh an imported csv file
Now the imported csv filepath is stored so it is possible to refresh the cost schedule without the need of reimporting it
2025-04-19 16:40:24 +02:00
Dion Moult 0608f92bc4 See #4699. Potentially fix crashes by only loading IFC data once and caching in decorator. 2025-04-19 21:19:18 +10:00
Dion Moult df18108406 Add tests for sheet creation and drawing / sheet generation 2025-04-19 17:36:21 +10:00
Thomas Krijnen b64dcfd4b8 Fix compilation on schemas prior to ifc4 #6549 2025-04-19 09:21:22 +02:00
Dion Moult d1493b4234 Fix #5571. No longer create "RASTER" document references. Process all images at sheet creation time.
I think this was overengineered and a bad decision. RASTER is a
technical detail, not a semantic label (in contrast with whether it's a
title or a drawing).
2025-04-19 16:53:14 +10:00
Dion Moult fb34af9162 Fix #6575. Regression in moving aggregates. 2025-04-19 15:30:46 +10:00
Dion Moult bb598d848a Fix #4130. Skip invalid topology. 2025-04-19 15:25:13 +10:00
Dion Moult 502cf6f439 Standalone script to generate drawings for debugging 2025-04-19 14:59:54 +10:00
Dion Moult 9875fce579 Revert "See #6574. Bump IOS."
This reverts commit 90198115b6.
2025-04-19 14:58:07 +10:00
Dion Moult 90198115b6 See #6574. Bump IOS. 2025-04-19 14:29:20 +10:00
Dion Moult 4896a69981 Add test cases for drawing duplication and shapely fill mode. 2025-04-19 13:42:36 +10:00
Dion Moult 3390f58c88 Fix failing tests 2025-04-19 08:19:25 +10:00
Andrej730 917d28a7b1 ifc5d cost schedule export - guess unit from quantity type 2025-04-18 19:25:46 +05:00
Andrej730 bebaa4d1bd ifc5d cost import - fix unexpected columns assigned as values
Mentioned in #5492 - id, index and lots of others were assigned as values at import.
2025-04-18 19:25:46 +05:00
Andrej730 93a4f4980a Revert ddd7dff75e
I guess after all it's not very reliable idea since it's possible to have a cost schedule without quantities (even non-SoR) and it's giving users false assumptions that they don't need to worry about "Is Schedule of Rates" flag in some cases.
2025-04-18 19:25:46 +05:00
Andrej730 1e30eae1d7 ifc5d - continue refactor 2025-04-18 19:25:46 +05:00
Andrej730 ff90396bcf typing 2025-04-18 19:25:46 +05:00
Dion Moult a030b84fc5 Typo 2025-04-18 19:50:03 +10:00
Dion Moult 0b1c049e81 Fix #5137. Fix bug where duplicated drawings didn't duplicate properly. Duplicating drawings now shares code with regular object duplication.
Previously code wasn't shared and duplication was reimplemented in an
incomplete way. Now it's shared which means that it's also more
sophisticated (i.e. does all the linked aggregate magic).

Also the duplicate code seems super complicated especially with arrays
and linked aggregates. Moving it into a tool makes it more reusable and
we can test it more granularly in the future.
2025-04-18 19:47:54 +10:00
Dion Moult 74390c0529 Fix bug where duplication outside IFC projects didn't use native Blender operators.
Historically we overrode the operator, so we had to reinvent native
functionality. Now we only override the hotkey, so we can reuse native
functionality. This also means that things we failed to reinvent like
duplicating unlinked animation data now works properly. This bit me when
I was doing animation work and Bonsai's duplicate messed things up.
2025-04-18 18:28:15 +10:00
Thomas Krijnen 21d39d8a23 Use exact topology points at parametric range bounds to guarantee welding #4487 2025-04-18 10:08:57 +02:00
Thomas Krijnen ad0f62f88f Don't terminate on invalid placement location #4883 2025-04-18 09:17:19 +02:00
Andrej730 85d8fa49ab ifc5d - add examples for other formats 2025-04-17 19:07:20 +05:00
Andrej730 c5f4b53555 bim.export_cost_schedules - fix bug with overridden prop value
E.g. if you would call this operator for 1 specific cost schedule, Blender would remember it's value and would keep on using it even if operator is going to be called outside this schedule
2025-04-17 18:55:42 +05:00
Andrej730 016cfa94d2 export_cost_schedules - fix confusing filename
Mentioned in #6545

Override filename to be always empty, so user won't have an expectation that csvs will be saved as a single file (currently they're always saved to a directory).
2025-04-17 18:55:41 +05:00
Andrej730 007af80234 ifc5d readme - add notes for possible missing columns 2025-04-17 18:55:41 +05:00
Andrej730 576a206d5b Fix typo 2025-04-17 18:55:41 +05:00
Andrej730 4cf6411a61 ifc5d.csv2ifc - add tests for sample files 2025-04-17 18:55:41 +05:00
Andrej730 37a0b3f6a9 Csv2Ifc - move arguments to init for a simpler setup 2025-04-17 18:55:41 +05:00
Andrej730 209caec49a csv2ifc import - rename required column from hierarchy to index #5492
To prevent confusion with 'Hierarchy' column that's added on the export and make current and export compatible.
Indices now always start from 1 for consistency.
2025-04-17 18:55:41 +05:00
Andrej730 bebb29f486 ifc5d csv export - rename Description column to Name #5492
Because actually it is a name and this is the column name that csv2ifc expects.
2025-04-17 18:55:40 +05:00
Andrej730 deb6e3779e ifc5d - refactor 2025-04-17 18:55:40 +05:00
Andrej730 a6c37771fc typing 2025-04-17 18:55:40 +05:00
Andrej730 95f754d2df black . 2025-04-17 18:55:40 +05:00
Thomas Krijnen 030dc6460a Prevent double free in iterator with output=native 2025-04-17 15:26:18 +02:00
Thomas Krijnen 360e08f268 SVG matrix #5676 2025-04-17 15:25:35 +02:00
Sebastian Friston dc84d85f7b ISSUE #6340 check if material is null before trying to get id 2025-04-17 14:33:07 +02:00
Dion Moult e16d8907f3 See #5676. Shapely fill calculation on drawings is 100x faster due to Blender raycasting instead of tree.select_ray. 2025-04-17 22:31:23 +10:00
Dion Moult 52cada180b Fix #4107. Bug where shapely used incorrect raycast coordinates causing fills to not show.
This has been around for 2 years, either it was never noticed or
something changed?
2025-04-17 19:38:31 +10:00
Gorgious56 1999216ea5 Use utilities to fetch collection hierarchy
I don't think this offers a noticeable performance gain but feels more idiomatic since it is available with the API.
2025-04-17 09:10:28 +02:00
Dion Moult 5a6476a57f Fix #3673. Bug where a split wall broke flipped doors.
Instead of regenerating fills upon splitting walls, placements are
preserved exactly.
2025-04-17 16:28:05 +10:00
Bruno Perdigão c45e3b0099 See #6537. Fix how OverrideMove works when dealing with multiple levels of aggregates. 2025-04-16 20:46:39 -03:00
Bruno Perdigão c9fc543e91 See #6537. Improve how aggregate mode behaves when dealing with multiple levels of aggregates. 2025-04-16 20:23:18 -03:00
Andrej730 9994fec571 ifc5d - fix bug exporting multiple csvs with the same name 2025-04-16 19:24:16 +05:00
Andrej730 b0aeabf2f6 ifc5d .csv cost schedule - save as unicode
Previously it was saved using Windows 1252 and importing it would fail with an error like so:
```
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x92 in position 650: invalid start byte
```
2025-04-16 19:24:16 +05:00
Andrej730 ddd7dff75e ifc5d csv2ifc - autodetect schedule of rates by the header structure 2025-04-16 19:24:16 +05:00
Andrej730 7acd13cb82 ifc5d readme - mention format examples 2025-04-16 19:24:15 +05:00
Andrej730 6499ac41cf ifc5d typing and refactor
no functional changes
2025-04-16 19:24:15 +05:00
Dion Moult e8b74245ce Update VERSION 2025-04-16 16:27:16 +10:00
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
331 changed files with 10488 additions and 4072 deletions
-30
View File
@@ -1,30 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Error Description and Steps to Reproduce**
<!--
Describe what problem occurred and what you expected to happen instead.
1. To reproduce this, open file '...'
2. Click on '....'
3. See error
-->
**Attachments**
<!--
If applicable, add screenshots to help explain your problem. Please also drag-drop any files necessary to show the error (rename the file extension from .ifc to .txt to upload). Private files can be uploaded to https://ifcopenshell.org/upload.html - only viewed by core developers and will be deleted afterwards.
-->
**Debug information**
<!--
If this is in Bonsai, paste the output from the Copy Debug Information option in Bonsai. It can be found under Quality and Coordination -> Quality Control -> Debug. If this is a general software issue, if relevant include details about IfcOpenShell version, operating system, Python version, etc.
-->
+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?
-15
View File
@@ -1,15 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Feature Description**
<!--
Describe a feature you'd like us to add. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable.
-->
@@ -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."
+6 -1
View File
@@ -118,5 +118,10 @@ jobs:
aws-region: us-east-1
- name: Upload .zip Archives to S3
env:
AWS_DEBUG: 1
AWS_RETRY_MODE: standard
AWS_MAX_ATTEMPTS: 3
run: |
aws s3 cp $env:USERPROFILE\output s3://ifcopenshell-builds/ --recursive
dir "$env:USERPROFILE\output"
aws s3 cp "$env:USERPROFILE\output" s3://ifcopenshell-builds/ --recursive --debug
+23 -44
View File
@@ -19,55 +19,34 @@ jobs:
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Step 1 - install dependencies
shell: bash
- name: Install dependencies
run: |
python3 -m pip install --upgrade pip
python3 -m pip install 'black>=24.10.0'
# NOTE: This would suffice, however it is less informative in terms of the 3 possible outcomes
# - name: QA Step - check linting
# shell: bash
# id: linting
# run: |
# python3 -m black .
# QA STEP
- name: QA Step - check linting
shell: bash
id: linting
# black doesn't catch all syntax errors, so we check them explicitly.
- name: Check syntax errors
id: syntax-errors
run: |
python3 -m black --check . \
&& exit 0 \
|| (echo "exit_code=$?" >> "$GITHUB_OUTPUT" && exit 1);
ERROR=0
python3 -W error -m compileall -q src/ifcopenshell-python || ERROR=1
python3 -W error -m compileall -q src/bonsai || ERROR=1
exit $ERROR
continue-on-error: true
# OUTCOME 1 of QA STEP
- name: QA Step - no linting errors
if: steps.linting.outcome == 'success'
shell: bash
run: |-
echo "::notice::QA step linting succeeded"
exit 0;
- name: Black formatter
id: linting
run: |
python3 -m black --diff --check .
continue-on-error: true
# OUTCOME 2i of QA STEP
- name: QA Step - unprettified code with no syntax errors
if: steps.linting.outputs.exit_code == 1
shell: bash
run: |-
echo "::group::QA step succeeded with warnings"
echo "::warning::one or more files contains unformatted code but no syntax errors";
echo "::notice::please run the linter before pushing!";
echo "::endgroup::"
exit 0;
# OUTCOME 2ii of QA STEP
- name: QA Step - code contains syntax errors
if: steps.linting.outputs.exit_code == 123
shell: bash
run: |-
echo "::group::QA step failed"
echo "::error::one or more files contains syntax errors";
echo "::notice::please run the linter and fix syntax errors before pushing!";
echo "::endgroup::"
exit 1;
- name: Final check
run: |
ERROR=0
if [ "${{ steps.syntax-errors.outcome }}" != "success" ]; then
echo "::error::Syntax errors check failed, see 'syntax-errors' step for the details." && ERROR=1
fi
if [ "${{ steps.linting.outcome }}" != "success" ]; then
echo "::error::Black formatting check failed, see 'linting' step for the details." && ERROR=1
fi
exit $ERROR
+4 -1
View File
@@ -55,6 +55,8 @@ jobs:
}
steps:
- uses: actions/checkout@v2
with:
submodules: recursive
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
@@ -104,7 +106,7 @@ jobs:
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
# Download Blender.
wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.3/blender-4.3.2-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.
@@ -175,6 +177,7 @@ jobs:
cd IfcOpenShell/src/bonsai
pip install pytest-blender
pip install pytest-bdd
blender --background --python scripts/setup_pytest.py
blender --python-expr "import bonsai; print(bonsai.bbim_semver); import ifcopenshell; print(ifcopenshell.version)" --background
make test
@@ -35,11 +35,11 @@ jobs:
- name: Version + date str
id: verdate
run: echo "verdate='${{ steps.version.outputs.version }}alpha${{ steps.date.outputs.date }}'" >> $GITHUB_OUTPUT
run: echo "verdate=${{ steps.version.outputs.version }}alpha${{ steps.date.outputs.date }}" >> $GITHUB_OUTPUT
test:
name: ${{ matrix.platform.distver }}-${{ matrix.pyver.name }}-${{ matrix.variant }}
name: ${{ matrix.platform.distver }}-${{ matrix.pyver.name }}
needs: activate
runs-on: ${{ matrix.platform.distver }}
defaults:
@@ -48,57 +48,67 @@ jobs:
strategy:
fail-fast: false
matrix:
variant: [novtk, all]
pyver: [
# { name: py311, distver: '3.11'},
{ name: py312, distver: '3.12'}
]
platform: [
{ name: win, distver: windows-latest, pkg_dir: 'win-64' },
{ name: linux, distver: ubuntu-latest, pkg_dir: 'linux-64' },
{ name: macOS, distver: macos-latest, pkg_dir: 'osx-64' }
{ name: macOS-arm, distver: macos-latest, pkg_dir: 'osx-arm64' },
{ name: macOS-x86, distver: macos-13, pkg_dir: 'osx-64' }
]
steps:
- name: Set up swap space
- name: Set Swap Space
if: runner.os == 'Linux'
uses: pierotofy/set-swap-space@v1.0
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- name: set ARTIFACTS ENV vars
shell: bash
run: |
pwd
if [[ "$RUNNER_OS" == "Windows" ]]; then
echo "ARTIFACTS_DIR=D:/a/artifacts" >> $GITHUB_ENV
elif [[ "$RUNNER_OS" == "macOS" ]]; then
echo "ARTIFACTS_DIR=/Users/runner/work/artifacts" >> $GITHUB_ENV
elif [[ "$RUNNER_OS" == "Linux" ]]; then
echo "ARTIFACTS_DIR=/home/runner/work/artifacts" >> $GITHUB_ENV
fi
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Download and extract MacOSX SDK
if: ${{ runner.os == 'macOS' }}
if: ${{ matrix.platform.name == 'macOS-x86' }}
run: |
curl -L https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX10.13.sdk.tar.xz | tar -xvJf - -C /Users/runner/work/
- uses: mamba-org/setup-micromamba@v1 # https://github.com/mamba-org/setup-micromamba
- uses: mamba-org/setup-micromamba@v2 # https://github.com/mamba-org/setup-micromamba
with:
environment-name: test-env
create-args: >-
python=3.11
python=3.12
anaconda-client
boa
rattler-build
- name: create conda package dist dir
run: |
mkdir -p ${{ github.workspace }}/dist
mkdir -p ${{ env.ARTIFACTS_DIR }}
- name: build & test ifcopenshell
run: |
conda mambabuild . --python ${{ matrix.pyver.distver }} --no-remove-work-dir --output-folder '${{ github.workspace }}/dist' --variants='{variant: ${{ matrix.variant }}}'
working-directory: ./conda
rattler-build build -r conda/recipe.yaml --output-dir '${{ env.ARTIFACTS_DIR }}'
env:
VERSION_OVERRIDE: ${{ needs.activate.outputs.verdate }}
- name: upload to anaconda
if: ${{ matrix.platform.name == 'win' }}
run: |
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell '${{ github.workspace }}/dist/${{ matrix.platform.pkg_dir }}/*.tar.bz2'
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell '${{ env.ARTIFACTS_DIR }}/${{ matrix.platform.pkg_dir }}/*.conda'
- name: upload to anaconda
if: ${{ matrix.platform.name != 'win' }}
run: |
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell ${{ github.workspace }}/dist/${{ matrix.platform.pkg_dir }}/*.tar.bz2
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell ${{ env.ARTIFACTS_DIR }}/${{ matrix.platform.pkg_dir }}/*.conda
+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
+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
+1 -1
View File
@@ -1 +1 @@
0.8.2
0.8.3
+4
View File
@@ -49,3 +49,7 @@ if errorlevel 1 exit 1
ninja install -j 1
if errorlevel 1 exit 1
python %RECIPE_DIR%/update_version_init.py %PKG_VERSION% %SP_DIR%/ifcopenshell/__init__.py
if errorlevel 1 exit 1
+3 -1
View File
@@ -46,4 +46,6 @@ cmake ${CMAKE_ARGS} -G Ninja \
ninja
ninja install -j 1
ninja install -j 1
python "${RECIPE_DIR}/update_version_init.py" "${PKG_VERSION}" "${SP_DIR}/ifcopenshell/__init__.py"
+13 -16
View File
@@ -1,7 +1,3 @@
variant:
- novtk
- all
occt:
- 7.8.1
@@ -24,12 +20,13 @@ cxx_compiler_version:
- '12' # [linux]
- '16' # [osx]
c_stdlib_version:
- '2.12' # [linux]
- '10.13' # [osx]
- 2.17 # [linux]
- 10.13 # [osx and x86_64]
- 11.0 # [osx and arm64]
hdf5:
- 1.14.3
- 1.14.6
libboost_devel:
- '1.84'
- '1.86'
libxml2:
- '2'
mpfr:
@@ -47,12 +44,12 @@ target_platform:
- win-64 # [win]
- linux-64 # [linux]
- osx-64 # [osx]
macos_machine:
- x86_64-apple-darwin13.4.0
MACOSX_DEPLOYMENT_TARGET:
- '10.13'
MACOSX_SDK_VERSION:
- '10.13'
macos_machine: # [osx]
- x86_64-apple-darwin13.4.0 # [osx and x86_64]
- arm64-apple-darwin20.0.0 # [osx and arm64]
MACOSX_DEPLOYMENT_TARGET: # [osx]
- 11.0 # [osx and arm64]
- 10.13 # [osx and x86_64]
CONDA_BUILD_SYSROOT:
- "/Users/runner/work/MacOSX10.13.sdk" # [osx]
CONDA_BUILD_SYSROOT: # [osx]
- "/Users/runner/work/MacOSX10.13.sdk" # [osx and x86_64]
+48 -33
View File
@@ -1,64 +1,78 @@
{% set version = environ.get('VERSION_OVERRIDE', '0.8.0') %}
{% set build = 0 %}
# Higher number -> Always prioritize "novtk" variant over "all" variant
{% set build = build + 200 %} # [variant == "novtk"]
{% set build = build + 100 %} # [variant == "all"]
context:
version: ${{ env.get("VERSION_OVERRIDE", default="0.8.2.1") }}
build: 1
package:
name: ifcopenshell
version: {{ version }}
version: ${{ version }}
source:
path: ../
- path: ..
build:
number: {{ build }}
skip: true # [py<39]
binary_relocation: false # [osx]
string: py{{ CONDA_PY }}_{{ variant }}_h{{ PKG_HASH }}_{{ build }}
run_exports:
- {{ pin_subpackage('ifcopenshell', max_pin='x.x.x') }} *{{ variant }}*
number: ${{ build }}
dynamic_linking:
binary_relocation: ${{ true if osx }}
requirements:
build:
- cmake
- if: build_platform != target_platform
then:
- python
- cross-python_${{ target_platform }}
- cmake <4
- ninja
- swig >=4.1.1
- {{ stdlib("c") }}
- {{ compiler('c') }}
- {{ compiler('cxx') }}
- ${{ stdlib("c") }}
- ${{ compiler('c') }}
- ${{ compiler('cxx') }}
host:
- python
- libboost-devel
- occt *=*{{ variant }}*
- occt
- libxml2
- cgal-cpp
- hdf5
- eigen
- mpfr
- nlohmann_json
- gmp # [unix]
- mpir # [win]
- gmp
- zlib
run:
- python
- {{ pin_compatible('occt', max_pin='x.x.x') }} *{{ variant }}*
- {{ pin_compatible('cgal-cpp', max_pin='x.x') }}
test:
imports:
- ifcopenshell
- shapely
- typing_extensions
- ${{ pin_compatible('occt', upper_bound='x.x.x') }}
- ${{ pin_compatible('cgal-cpp', upper_bound='x.x') }}
run_exports:
- ${{ pin_subpackage('ifcopenshell', upper_bound='x.x.x') }}
tests:
- python:
imports:
- ifcopenshell
pip_check: false
- script:
- python -c "import ifcopenshell; assert ifcopenshell.version == '${{ version }}', 'print(ifcopenshell.version)'"
requirements:
run:
- occt * *novtk* # Ensure that even though compiled against OCCT with VTK, it can still run with OCCT without VTK
- pytest
- python-dateutil
- xmlschema
- xsdata
- lxml
- isodate
- lark
- networkx
- tabulate
- shapely
about:
home: https://ifcopenshell.org
license: LGPL-3.0-or-later
license_file: COPYING
summary: 'IfcOpenShell is a library to support the IFC file format'
summary: IfcOpenShell is a library to support the IFC file format
description: |
IfcOpenShell
============
@@ -293,8 +307,9 @@ about:
[MSYS2]: https://msys2.github.io/ "MSYS2"
[win/readme.md]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/win/readme.md "win/readme.md"
[nix/build-all.py]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/nix/build-all.py "nix/build-all.py"
doc_url: https://ifcopenshell.org/
dev_url: https://github.com/IfcOpenShell/IfcOpenShell
homepage: https://ifcopenshell.org
repository: https://github.com/IfcOpenShell/IfcOpenShell
documentation: https://ifcopenshell.org/
extra:
recipe-maintainers:
+32
View File
@@ -0,0 +1,32 @@
import re
import argparse
from pathlib import Path
def update_version(file_path: str, version: str) -> None:
"""Update the version string in the given __init__.py file."""
file_path = Path(file_path)
# Read the file and replace the version
file_contents = file_path.read_text(encoding="utf-8")
new_contents = re.sub(r'version = "0\.0\.0"', f'version = "{version}"', file_contents)
# Write the updated contents back to the file
file_path.write_text(new_contents, encoding="utf-8")
print(f"Updated version in {file_path} to {version}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Update the version string in a Python file.")
parser.add_argument(
"version",
type=str,
help="The version string to replace '0.0.0' with (e.g., '1.2.3')."
)
parser.add_argument(
"file",
type=str,
help="The path to the __init__.py file where the version will be updated."
)
args = parser.parse_args()
update_version(args.file, args.version)
+3
View File
@@ -35,3 +35,6 @@ extend-exclude = '''
[tool.pyright]
reportInvalidTypeForm = false
disableBytesTypePromotions = true
reportUnnecessaryTypeIgnoreComment = true
# Just to add a quick insert of `# pyright: ignore[xxx]` comments in Pylance.
enableTypeIgnoreComments = false
+1 -1
View File
@@ -20,7 +20,7 @@ def get_extensions_attributes(extensions: Extensions) -> dict[str, AttributeData
- subattribute name"""
possible_attributes = {}
for field in fields(type(extensions)):
field_type = field.type.__args__[0] # type: ignore [reportAttributeAccessIssue]
field_type = field.type.__args__[0] # pyright: ignore [reportAttributeAccessIssue]
subfield = next(iter(fields(field_type)))
xsd_name = subfield.metadata["name"]
possible_attributes[field.name] = AttributeData(field_type, subfield.name, xsd_name)
+1
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
SHELL := sh
PYTHON:=python3.11
PIP:=pip3.11
PATCH:=patch
+25 -10
View File
@@ -28,6 +28,10 @@ IN_BLENDER = sys.modules.get("bpy", None)
if IN_BLENDER:
import bpy
# This file is executed twice - first as a bonsai-extension
# and then as a bonsai-package.
IN_PACKAGE = __package__ == "bonsai"
import re
import platform
import traceback
@@ -218,17 +222,21 @@ if IN_BLENDER:
info["binary_python_version"] = version
return info
try:
import git
def update_commit_data() -> None:
try:
import git
# We can't just use __file__ as bonsai/__init__.py is typically not symlinked
# as Blender have errors symlinking main addon package file.
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
global last_commit_hash
global last_commit_date
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
if IN_PACKAGE:
update_commit_data()
try:
import ifcopenshell.api
@@ -244,6 +252,12 @@ if IN_BLENDER:
ifcopenshell.api.add_pre_listener("*", "action_logger", log_api)
def purge_cache():
"""Purge cache left from previous session (e.g. after reload or update)."""
import bonsai.tool as tool
tool.Blender.get_bonsai_version.cache_clear()
def register():
if platform.system() == "Windows":
clean_up_dlls_safe_links()
@@ -261,6 +275,7 @@ if IN_BLENDER:
bonsai.REINSTALLED_BBIM_VERSION = current_version
bonsai.bim.register()
purge_cache()
def unregister():
if platform.system() == "Windows":
+8
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
@@ -100,6 +101,7 @@ classes = [
operator.BIM_OT_delete_object,
operator.BIM_OT_remove_section_plane,
operator.BIM_OT_select_entity,
operator.BIM_OT_select_entity_by_guid,
operator.BIM_OT_select_object,
operator.BIM_OT_show_description,
operator.BIM_OT_multiple_file_selector,
@@ -121,6 +123,7 @@ classes = [
operator.SelectURIAttribute,
operator.SetTab,
operator.SwitchTab,
operator.ShowSystemInfo,
prop.StrProperty,
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
prop.ObjProperty,
@@ -221,7 +224,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)
@@ -47,8 +47,6 @@
"scene.render.simplify_gpencil_shader_fx": true,
"scene.render.simplify_gpencil_tint": true,
"scene.render.simplify_gpencil_view_fill": true,
"scene.render.simplify_shadows": 1.0,
"scene.render.simplify_shadows_render": 1.0,
"scene.render.simplify_subdivision": 6,
"scene.render.simplify_subdivision_render": 6,
"scene.render.simplify_volumes": 1.0,
@@ -147,7 +145,7 @@
"scene.display.shading.use_scene_world_render": false,
"scene.display.shading.use_studiolight_view_rotation": true,
"scene.display.shading.use_world_space_lighting": false,
"scene.display.shading.wireframe_color_type": "MATERIAL",
"scene.display.shading.wireframe_color_type": "THEME",
"scene.display.shading.xray_alpha": 0.5,
"scene.display.shading.xray_alpha_wireframe": 0.0,
"scene.display.light_direction": [
@@ -162,7 +160,6 @@
"scene.display.shadow_focus": 0.0,
"scene.display.shadow_shift": 0.10000000149011612,
"scene.display.viewport_aa": "FXAA",
"space.overlay.backwire_opacity": 0.5,
"space.overlay.bone_wire_alpha": 1.0,
"space.overlay.display_handle": "SELECTED",
"space.overlay.fade_inactive_alpha": 0.4000000059604645,
@@ -189,7 +186,6 @@
"space.overlay.show_edge_crease": true,
"space.overlay.show_edge_seams": true,
"space.overlay.show_edge_sharp": true,
"space.overlay.show_edges": false,
"space.overlay.show_extra_edge_angle": false,
"space.overlay.show_extra_edge_length": false,
"space.overlay.show_extra_face_angle": false,
@@ -208,7 +204,6 @@
"space.overlay.show_motion_paths": true,
"space.overlay.show_object_origins": false,
"space.overlay.show_object_origins_all": false,
"space.overlay.show_occlude_wire": false,
"space.overlay.show_onion_skins": false,
"space.overlay.show_ortho_grid": true,
"space.overlay.show_outline_selected": true,
@@ -298,8 +293,6 @@
"scene.render.simplify_gpencil_shader_fx": true,
"scene.render.simplify_gpencil_tint": true,
"scene.render.simplify_gpencil_view_fill": true,
"scene.render.simplify_shadows": 1.0,
"scene.render.simplify_shadows_render": 1.0,
"scene.render.simplify_subdivision": 6,
"scene.render.simplify_subdivision_render": 6,
"scene.render.simplify_volumes": 1.0,
@@ -398,7 +391,7 @@
"scene.display.shading.use_scene_world_render": false,
"scene.display.shading.use_studiolight_view_rotation": true,
"scene.display.shading.use_world_space_lighting": false,
"scene.display.shading.wireframe_color_type": "MATERIAL",
"scene.display.shading.wireframe_color_type": "THEME",
"scene.display.shading.xray_alpha": 0.5,
"scene.display.shading.xray_alpha_wireframe": 0.0,
"scene.display.light_direction": [
@@ -413,7 +406,6 @@
"scene.display.shadow_focus": 0.0,
"scene.display.shadow_shift": 0.10000000149011612,
"scene.display.viewport_aa": "FXAA",
"space.overlay.backwire_opacity": 0.5,
"space.overlay.bone_wire_alpha": 1.0,
"space.overlay.display_handle": "SELECTED",
"space.overlay.fade_inactive_alpha": 0.4000000059604645,
@@ -440,7 +432,6 @@
"space.overlay.show_edge_crease": true,
"space.overlay.show_edge_seams": true,
"space.overlay.show_edge_sharp": true,
"space.overlay.show_edges": false,
"space.overlay.show_extra_edge_angle": false,
"space.overlay.show_extra_edge_length": false,
"space.overlay.show_extra_face_angle": false,
@@ -459,7 +450,6 @@
"space.overlay.show_motion_paths": true,
"space.overlay.show_object_origins": false,
"space.overlay.show_object_origins_all": false,
"space.overlay.show_occlude_wire": false,
"space.overlay.show_onion_skins": false,
"space.overlay.show_ortho_grid": true,
"space.overlay.show_outline_selected": true,
@@ -520,7 +510,7 @@
"scene.render.border_min_x": 0.0,
"scene.render.border_min_y": 0.0,
"scene.render.dither_intensity": 1.0,
"scene.render.engine": "BLENDER_EEVEE",
"scene.render.engine": "BLENDER_EEVEE_NEXT",
"scene.render.film_transparent": false,
"scene.render.filter_size": 1.5,
"scene.render.fps": 24,
@@ -549,8 +539,6 @@
"scene.render.simplify_gpencil_shader_fx": true,
"scene.render.simplify_gpencil_tint": true,
"scene.render.simplify_gpencil_view_fill": true,
"scene.render.simplify_shadows": 1.0,
"scene.render.simplify_shadows_render": 1.0,
"scene.render.simplify_subdivision": 6,
"scene.render.simplify_subdivision_render": 6,
"scene.render.simplify_volumes": 1.0,
@@ -649,7 +637,7 @@
"scene.display.shading.use_scene_world_render": false,
"scene.display.shading.use_studiolight_view_rotation": true,
"scene.display.shading.use_world_space_lighting": false,
"scene.display.shading.wireframe_color_type": "MATERIAL",
"scene.display.shading.wireframe_color_type": "THEME",
"scene.display.shading.xray_alpha": 0.5,
"scene.display.shading.xray_alpha_wireframe": 0.0,
"scene.display.light_direction": [
@@ -664,7 +652,6 @@
"scene.display.shadow_focus": 0.0,
"scene.display.shadow_shift": 0.10000000149011612,
"scene.display.viewport_aa": "FXAA",
"space.overlay.backwire_opacity": 0.5,
"space.overlay.bone_wire_alpha": 1.0,
"space.overlay.display_handle": "SELECTED",
"space.overlay.fade_inactive_alpha": 0.4000000059604645,
@@ -691,7 +678,6 @@
"space.overlay.show_edge_crease": true,
"space.overlay.show_edge_seams": true,
"space.overlay.show_edge_sharp": true,
"space.overlay.show_edges": false,
"space.overlay.show_extra_edge_angle": false,
"space.overlay.show_extra_edge_length": false,
"space.overlay.show_extra_face_angle": false,
@@ -710,7 +696,6 @@
"space.overlay.show_motion_paths": true,
"space.overlay.show_object_origins": true,
"space.overlay.show_object_origins_all": false,
"space.overlay.show_occlude_wire": false,
"space.overlay.show_onion_skins": false,
"space.overlay.show_ortho_grid": true,
"space.overlay.show_outline_selected": true,
@@ -1,11 +1,11 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((),'2;1');
FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',(),(),'EPset_Drawing','EPset_Drawing',$);
FILE_DESCRIPTION($,'2;1');
FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',$,$,'EPset_Drawing','EPset_Drawing',$);
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22,#23,#24,#25,#26,#27));
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#23,#22,#27,#24,#19,#12,#26,#9,#8,#7,#6,#4,#18,#11,#5,#20,#25,#14,#10,#17,#28,#16,#3,#21,#13,#15,#2));
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
@@ -14,7 +14,7 @@ DATA;
#7=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#8=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','Comma separated list of selector expressions to evaluate for each drawing elementand add results to their ''class'' attribute.\X2\000A\X0\E.g. ''Name, id'' would add to ''class'' value similar to ''Name-Wall id-1220''.\X2\000A\X0\Then it can be used to applied css styles based on the resulting class.\X2\000A\X0\If attribute is not present on the element, then it won''t be added to it''s ''class''.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#13=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
@@ -32,5 +32,6 @@ DATA;
#25=IFCSIMPLEPROPERTYTEMPLATE('3LHwCrOcb6Y8ozfJZ7Ay$c',$,'LineworkMode','Method to use for line work',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#26=IFCSIMPLEPROPERTYTEMPLATE('2iwERDOW55Pf4hCbuFRe1Q',$,'FillMode','Method to fill areas seen in projection',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#27=IFCSIMPLEPROPERTYTEMPLATE('1YF$qLzBzF19Io8aB2N8cE',$,'CutMode','Method for cutting geometry',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#28=IFCSIMPLEPROPERTYTEMPLATE('1YSnFzurrEyRNtoLdmmddP',$,'BringToFront','The objects with these SVG classes will render in front of all other objects.Ex: IfcBeam, IfcColumn',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
ENDSEC;
END-ISO-10303-21;
-30
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,41 +100,12 @@ 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(tool.Blender.get_object_bim_props(obj).ifc_definition_id)
if tool.Geometry.is_scaled(obj):
+3 -3
View File
@@ -347,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)
@@ -361,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
+1
View File
@@ -392,6 +392,7 @@ def draw_filter(
row = layout.row(align=True)
row.label(text=f"{len(data.data['saved_searches'])} Saved Searches")
row.operator("bim.select_entity_by_guid", text="", icon="CON_OBJECTSOLVER")
if data.data["saved_searches"]:
row.operator("bim.load_search", text="", icon="IMPORT").module = module
row.operator("bim.save_search", text="", icon="EXPORT").module = module
+31 -2
View File
@@ -31,9 +31,10 @@ import ifcopenshell.ifcopenshell_wrapper
import bonsai
import bonsai.bim.handler
import bonsai.tool as tool
from ifcopenshell.file import UndoSystemError
from pathlib import Path
from bonsai.tool.brick import BrickStore
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired, Literal
IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
@@ -417,6 +418,32 @@ class IfcStore:
props = tool.Blender.get_object_bim_props(obj)
props.ifc_definition_id = 0
@staticmethod
def get_ifc_file_undo_callback(callback_type: Literal["UNDO", "REDO"]):
def callback(_) -> None:
try:
if callback_type == "UNDO":
tool.Ifc.get().undo()
else:
tool.Ifc.get().redo()
except Exception as e:
# Persistent callbacks errors are not visible from UI and we set `last_error`
# to make it visible.
error_msg = ""
# In theory it should always be UndoSystemError, but just to be safe.
if isinstance(e, UndoSystemError):
error_msg += "Undo transaction operations:\n"
transaction = e.transaction
for operation in transaction.operations:
error_msg += f"- {str(operation)}\n"
# Show it in system console too, not just in last message.
print(error_msg)
error_msg += traceback.format_exc()
bonsai.last_error = error_msg
raise
return callback
@staticmethod
def execute_ifc_operator(
operator: tool.Ifc.Operator,
@@ -446,7 +473,9 @@ class IfcStore:
if tool.Ifc.get():
tool.Ifc.get().end_transaction()
IfcStore.add_transaction_operation(
operator, rollback=lambda d: tool.Ifc.get().undo(), commit=lambda d: tool.Ifc.get().redo()
operator,
rollback=IfcStore.get_ifc_file_undo_callback("UNDO"),
commit=IfcStore.get_ifc_file_undo_callback("REDO"),
)
if BrickStore.graph is not None: # `if BrickStore.graph` by itself takes ages.
BrickStore.end_transaction()
+60 -21
View File
@@ -38,7 +38,7 @@ import ifcopenshell.util.shape
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore, IFC_CONNECTED_TYPE
from bonsai.tool.loader import OBJECT_DATA_TYPE
from typing import Dict, Union, Optional, Any, Literal
from typing import Dict, Union, Optional, Any, Literal, Iterable
from ifcopenshell.util.shape import MatrixType
@@ -198,11 +198,16 @@ class MaterialCreator:
class IfcImporter:
file: ifcopenshell.file
"""Either provided by user as an attribute or will be loaded from ``input_file`` during ``execute()``."""
elements: set[ifcopenshell.entity_instance]
"""Set of IfcElements to import. Excluding ``gross_elements`` and ```native_elements``."""
def __init__(self, ifc_import_settings: IfcImportSettings):
self.ifc_import_settings = ifc_import_settings
tool.Loader.set_settings(ifc_import_settings)
self.diff = None
self.file: ifcopenshell.file = None
self.project = None
self.has_existing_project = False
# element guids to blender collections mapping
@@ -300,6 +305,7 @@ class IfcImporter:
bpy.context.window_manager.progress_end()
def process_context_filter(self) -> None:
"""Setup contexts. Necessary for importing elements representations."""
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.")
@@ -319,16 +325,15 @@ class IfcImporter:
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
def process_element_filter(self) -> None:
elements: list[ifcopenshell.entity_instance]
if self.ifc_import_settings.has_filter:
self.elements = self.ifc_import_settings.elements
if isinstance(self.elements, set):
self.elements = list(self.elements)
elements = list(self.ifc_import_settings.elements)
# TODO: enable filtering for annotations
else:
if self.file.schema in ("IFC2X3", "IFC4"):
self.elements = self.file.by_type("IfcElement") + self.file.by_type("IfcProxy")
elements = self.file.by_type("IfcElement") + self.file.by_type("IfcProxy")
else:
self.elements = self.file.by_type("IfcElement")
elements = self.file.by_type("IfcElement")
drawing_groups = [g for g in self.file.by_type("IfcGroup") if g.ObjectType == "DRAWING"]
drawing_annotations = set()
@@ -338,16 +343,16 @@ class IfcImporter:
self.annotations = set([a for a in self.file.by_type("IfcAnnotation")])
self.annotations -= drawing_annotations
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")]
elements = [e for e in elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")]
if self.ifc_import_settings.element_limit_mode == "UNLIMITED":
self.elements = set(self.elements)
self.elements = set(elements)
else:
offset = self.ifc_import_settings.element_offset
offset_limit = offset + self.ifc_import_settings.element_limit
self.elements = set(self.elements[offset:offset_limit])
self.elements = set(elements[offset:offset_limit])
if self.ifc_import_settings.has_filter or self.ifc_import_settings.element_limit_mode != "UNLIMITED":
self.element_types = set([ifcopenshell.util.element.get_type(e) for e in self.elements])
self.element_types = {t for e in self.elements if (t := ifcopenshell.util.element.get_type(e))}
else:
self.element_types = set(self.file.by_type("IfcTypeProduct"))
@@ -385,6 +390,7 @@ class IfcImporter:
return results
def parse_native_elements(self) -> None:
# TODO: move to `process_element_filter` to incapsulate all `self.elements` logic.
if not self.ifc_import_settings.should_load_geometry:
return
if not self.file.by_type("IfcSweptDiskSolid"):
@@ -422,7 +428,8 @@ class IfcImporter:
rep = representation
while True:
if len(rep.Items) == 1 and rep.Items[0].is_a("IfcMappedItem"):
# Tekla 2023 has missing items, though it's invalid IFC.
if rep.Items and len(rep.Items) == 1 and rep.Items[0].is_a("IfcMappedItem"):
rep_matrix = ifcopenshell.util.placement.get_mappeditem_transformation(rep.Items[0])
if not np.allclose(rep_matrix, np.eye(4)):
matrix = rep_matrix @ matrix
@@ -589,6 +596,7 @@ class IfcImporter:
return self.create_generic_sqlite_elements(elements)
if self.ifc_import_settings.should_load_geometry:
elements = elements.copy() # Prevent argument mutation.
context_settings = (
tool.Loader.settings.gross_context_settings if is_gross else tool.Loader.settings.context_settings
)
@@ -717,9 +725,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
@@ -1124,7 +1138,22 @@ class IfcImporter:
bpy.ops.view3d.view_selected()
bpy.ops.object.select_all(action="DESELECT")
def setup_arrays(self):
def setup_arrays(
self,
*,
annotations_to_import: Optional[set[ifcopenshell.entity_instance]] = None,
openings_to_import: Optional[set[ifcopenshell.entity_instance]] = None,
) -> None:
# During base import some elements are skipped.
is_base_import = True
elements_to_import: set[ifcopenshell.entity_instance] = set()
if annotations_to_import is not None:
is_base_import = False
elements_to_import = annotations_to_import
elif openings_to_import is not None:
is_base_import = False
elements_to_import = openings_to_import
for pset in self.file.by_type("IfcPropertySet"):
if pset.Name != "BBIM_Array":
continue
@@ -1132,16 +1161,24 @@ class IfcImporter:
continue
data = json.loads(data)
for rel in pset.DefinesOccurrence:
element: ifcopenshell.entity_instance
for element in rel.RelatedObjects:
if is_base_import:
if element.is_a("IfcAnnotation") or element.is_a("IfcOpeningElement"):
continue
else:
if element not in elements_to_import:
continue
for i in range(len(data)):
tool.Blender.Modifier.Array.set_children_lock_state(element, i, True)
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
class IfcImportSettings:
input_file: Union[str, None] = None
logger: Union[logging.Logger, None] = None
def __init__(self):
self.logger: logging.Logger = None
self.input_file = None
self.diff_file = None
self.geometry_library = "opencascade"
self.should_use_cpu_multiprocessing = True
@@ -1162,17 +1199,19 @@ class IfcImportSettings:
self.element_limit_mode = "UNLIMITED"
self.element_offset = 0
self.element_limit = 30000
self.has_filter = None
self.has_filter = False
self.should_filter_spatial_elements = True
self.should_setup_viewport_camera = True
self.contexts: list[ifcopenshell.entity_instance] = []
self.context_settings: list[ifcopenshell.geom.main.settings] = []
self.gross_context_settings: list[ifcopenshell.geom.main.settings] = []
self.elements: set[ifcopenshell.entity_instance] = set()
self.elements: Iterable[ifcopenshell.entity_instance] = set()
self.load_indexed_maps = False
@staticmethod
def factory(context=None, input_file=None, logger=None):
def factory(
context=None, input_file: Optional[str] = None, logger: Optional[logging.Logger] = None
) -> IfcImportSettings:
scene_diff = tool.Blender.get_diff_props()
props = tool.Project.get_project_props()
settings = IfcImportSettings()
@@ -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
@@ -162,8 +163,7 @@ class AggregateDecorator:
batch.draw(shader)
def draw_aggregate(self, context):
if context.scene.BIMAggregateProperties.in_aggregate_mode:
return
props = tool.Aggregate.get_aggregate_props()
self.addon_prefs = tool.Blender.get_addon_preferences()
decorator_color_special = self.addon_prefs.decorator_color_special
decorator_color_selected = self.addon_prefs.decorator_color_selected
@@ -192,7 +192,13 @@ class AggregateDecorator:
aggregates.append(obj)
continue
aggregate = ifcopenshell.util.element.get_aggregate(element)
aggregates_list = tool.Aggregate.get_aggregates_recursively(element)
if props.in_aggregate_mode and props.editing_aggregate:
index = aggregates_list.index(tool.Ifc.get_entity(props.editing_aggregate))
if index > 0:
aggregate = aggregates_list[index - 1]
else:
aggregate = aggregates_list[-1]
if aggregate:
aggregates.append(tool.Ifc.get_object(aggregate))
@@ -210,9 +216,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:
return
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate))
parts = tool.Aggregate.get_parts_recursively(tool.Ifc.get_entity(aggregate))
parts_objs = [tool.Ifc.get_object(p) for p in parts]
indices, edges = create_bounding_box(parts_objs)
@@ -261,7 +265,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:
@@ -292,7 +296,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
@@ -331,7 +335,7 @@ class AggregateModeDecorator:
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)])
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate_obj))
parts = tool.Aggregate.get_parts_recursively(tool.Ifc.get_entity(aggregate_obj))
color = self.addon_prefs.decorator_color_selected
parts_objs = [tool.Ifc.get_object(p) for p in parts]
@@ -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
@@ -132,12 +140,11 @@ class BIM_OT_disable_editing_aggregate(bpy.types.Operator):
class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
"""Add aggregate to IFC element"""
"""Add aggregate to selected IFC elements."""
bl_idname = "bim.add_aggregate"
bl_label = "Add Aggregate"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
ifc_class: bpy.props.StringProperty(name="IFC Class", default="IfcElementAssembly")
aggregate_name: bpy.props.StringProperty(name="Name", default="Default_Name")
@@ -157,7 +164,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
return
aggregate = self.create_aggregate(context, ifc_class, self.aggregate_name)
for obj in context.selected_objects:
for obj in tool.Blender.get_selected_objects():
element = tool.Ifc.get_entity(obj)
if not element:
continue
@@ -182,7 +189,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)
@@ -378,7 +385,7 @@ class BIM_OT_disable_aggregate_mode(bpy.types.Operator):
def execute(self, context):
bpy.ops.object.select_all(action="DESELECT")
bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate)
bonsai.core.aggregate.exit_aggregate_mode(tool.Aggregate)
return {"FINISHED"}
@@ -388,7 +395,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,16 +99,27 @@ 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):
in_aggregate_mode: BoolProperty(name="In Edit Mode", update=update_aggregate_mode_decorator)
editing_aggregate: PointerProperty(name="Editing Aggregate", type=bpy.types.Object)
previous_editing_aggregate: PointerProperty(name="Editing Aggregate", type=bpy.types.Object)
editing_objects: CollectionProperty(type=Objects)
not_editing_objects: CollectionProperty(type=Objects)
aggregate_decorator: BoolProperty(
@@ -115,3 +127,16 @@ class BIMAggregateProperties(PropertyGroup):
default=False,
update=update_aggregate_decorator,
)
previous_state: BoolProperty(
name="True if it was previously in aggregate mode",
default=False,
)
if TYPE_CHECKING:
in_aggregate_mode: bool
editing_aggregate: Union[bpy.types.Object, None]
previous_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
previous_state: bool
+7 -7
View File
@@ -49,11 +49,14 @@ 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()
icon = "HIDE_OFF" if props.aggregate_decorator else "HIDE_ON"
row.prop(props, "aggregate_decorator", icon=icon, 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()
@@ -76,10 +79,8 @@ class BIM_PT_aggregate(Panel):
if AggregateData.data["has_relating_object"]:
row.label(text=AggregateData.data["relating_object_label"], icon="TRIA_UP")
op = row.operator("bim.select_aggregate", icon="OBJECT_DATA", text="")
op.obj = context.active_object.name
op.select_parts = False
op = row.operator("bim.select_aggregate", icon="RESTRICT_SELECT_OFF", text="")
op.obj = context.active_object.name
op.select_parts = True
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
row.operator("bim.add_aggregate", icon="ADD", text="")
@@ -131,9 +132,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:
@@ -45,6 +45,7 @@ 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"})
@@ -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)
@@ -1283,6 +1283,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
cam_aspect: float,
context: bpy.types.Context,
) -> None:
assert isinstance(obj.data, bpy.types.Camera)
if viewpoint.visualization_info.orthogonal_camera:
camera = viewpoint.visualization_info.orthogonal_camera
obj.data.type = "ORTHO"
@@ -1400,6 +1401,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
self.hide_spaces(context)
def hide_spaces(self, context: bpy.types.Context) -> None:
assert context.area
old = context.area.type
context.area.type = "VIEW_3D"
bpy.ops.object.select_all(action="DESELECT")
@@ -21,14 +21,13 @@ from . import ui, prop, operator
classes = (
operator.GetBSDDClassificationProperties,
operator.LoadBSDDDomains,
operator.LoadBSDDDictionaries,
operator.SearchBSDDClass,
operator.SetActiveBSDDDictionary,
prop.BSDDDomain,
prop.BSDDDictionary,
prop.BSDDClassification,
prop.BSDDPset,
prop.BIMBSDDProperties,
ui.BIM_UL_bsdd_domains,
ui.BIM_UL_bsdd_dictionaries,
ui.BIM_UL_bsdd_classifications,
ui.BIM_PT_bsdd,
)
+45
View File
@@ -0,0 +1,45 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2022 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
import ifcopenshell
import ifcopenshell.util.date
import ifcopenshell.util.classification
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
def refresh():
BSDDData.is_loaded = False
class BSDDData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data["active_dictionary"] = cls.active_dictionary()
@classmethod
def active_dictionary(cls):
props = tool.Bsdd.get_bsdd_props()
results = [("ALL", "All Dictionaries", "All active dictionaries")]
results.extend([(d.uri, d.name, f"{d.status} - {d.version}") for d in props.dictionaries if d.is_active])
return results
+5 -25
View File
@@ -22,25 +22,13 @@ import bonsai.tool as tool
from bonsai.core import bsdd as core
class LoadBSDDDomains(bpy.types.Operator):
bl_idname = "bim.load_bsdd_domains"
class LoadBSDDDictionaries(bpy.types.Operator):
bl_idname = "bim.load_bsdd_dictionaries"
bl_label = "Load bSDD Dictionaries"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
core.load_bsdd(bsdd.Client(), tool.Bsdd)
return {"FINISHED"}
class SetActiveBSDDDictionary(bpy.types.Operator):
bl_idname = "bim.set_active_bsdd_domain"
bl_label = "Set bSDD Dictionary as active"
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty()
uri: bpy.props.StringProperty()
def execute(self, context):
core.set_active_bsdd_dictionary(self.name, self.uri, tool.Bsdd)
core.load_bsdd(tool.Bsdd)
return {"FINISHED"}
@@ -50,17 +38,9 @@ class SearchBSDDClass(bpy.types.Operator):
bl_description = "Search for bSDD classes by the provided keyword"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
# Requirement by buildingSMART bSDD api.
if len(context.scene.BIMBSDDProperties.keyword) < 3:
cls.poll_message_set("Search query has to be at least 3 characters long.")
return False
return True
def execute(self, context):
keyword = context.scene.BIMBSDDProperties.keyword
classes_found = core.search_class(keyword, bsdd.Client(), tool.Bsdd)
classes_found = core.search_class(keyword, tool.Bsdd)
self.report({"INFO"}, f"{classes_found} bSDD classes found for '{keyword}'.")
return {"FINISHED"}
@@ -72,7 +52,7 @@ class GetBSDDClassificationProperties(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
pset_data = core.get_class_properties(bsdd.Client(), tool.Bsdd)
pset_data = core.get_class_properties(tool.Bsdd)
psets_found = len(pset_data)
props_found = sum(len(pset) for pset in pset_data.values())
self.report({"INFO"}, f"{psets_found} psets found ({props_found} props) for the active classification.")
+30 -9
View File
@@ -18,6 +18,7 @@
import bpy
from bpy.types import PropertyGroup
from bonsai.bim.module.bsdd.data import BSDDData
from bonsai.bim.prop import Attribute, StrProperty
from bpy.props import (
PointerProperty,
@@ -31,21 +32,34 @@ from bpy.props import (
)
class BSDDDomain(PropertyGroup):
def get_active_dictionary(self, context):
if not BSDDData.is_loaded:
BSDDData.load()
return BSDDData.data["active_dictionary"]
def update_is_active(self: "BSDDDictionary", context: bpy.types.Context) -> None:
BSDDData.data["active_dictionary"] = BSDDData.active_dictionary()
class BSDDDictionary(PropertyGroup):
name: StringProperty(name="Name")
uri: StringProperty(name="URI")
default_language_code: StringProperty(name="Language")
organization_name_owner: StringProperty(name="Organization")
status: StringProperty(name="Status")
version: StringProperty(name="Version")
is_active: BoolProperty(
name="Is Active", description="Enable to search with this dictionary", default=False, update=update_is_active
)
class BSDDClassification(PropertyGroup):
name: StringProperty(name="Name")
reference_code: StringProperty(name="Reference Code")
uri: StringProperty(name="Namespace URI")
domain_name: StringProperty(name="Domain Name")
domain_namespace_uri: StringProperty(name="Domain Namespace URI")
dictionary_name: StringProperty(name="Dictionary Name")
dictionary_namespace_uri: StringProperty(name="Dictionary Namespace URI")
class BSDDPset(PropertyGroup):
@@ -54,10 +68,11 @@ class BSDDPset(PropertyGroup):
class BIMBSDDProperties(PropertyGroup):
active_domain: StringProperty(name="Active Domain")
active_dictionary: StringProperty(name="Active Dictionary")
active_dictionary: EnumProperty(items=get_active_dictionary, name="Active Dictionary")
active_uri: StringProperty(name="Active URI")
domains: CollectionProperty(name="Domains", type=BSDDDomain)
active_domain_index: IntProperty(name="Active Domain Index")
dictionaries: CollectionProperty(name="Dictionaries", type=BSDDDictionary)
active_dictionary_index: IntProperty(name="Active Dictionary Index")
classifications: CollectionProperty(name="Classifications", type=BSDDClassification)
active_classification_index: IntProperty(name="Active Classification Index")
keyword: StringProperty(name="Keyword", description="Query for bsdd classes search, case and accent insensitive")
@@ -69,9 +84,15 @@ class BIMBSDDProperties(PropertyGroup):
use_only_ifc_properties: BoolProperty(
name="Only IFC Properties",
description="Whether to display and assign only properties from IFC dictionary",
default=True,
default=False,
)
load_preview_domains: BoolProperty(
name="Load Preview Domains", description="Whether it should load preview and inactive domains", default=False
load_preview_dictionaries: BoolProperty(
name="Load Preview Dictionaries", description="Load dictionaries marked as Preview status", default=False
)
load_inactive_dictionaries: BoolProperty(
name="Load Inactive Dictionaries", description="Load dictionaries marked as Inactive status", default=False
)
load_test_dictionaries: BoolProperty(
name="Load Test Dictionaries", description="Load dictionaries that are for testing only", default=False
)
classification_psets: CollectionProperty(name="Classification Psets", type=BSDDPset)
+26 -32
View File
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bonsai.tool as tool
from bonsai.bim.module.bsdd.data import BSDDData
from bpy.types import Panel, UIList
@@ -30,65 +31,58 @@ class BIM_PT_bsdd(Panel):
bl_parent_id = "BIM_PT_tab_project_setup"
def draw(self, context):
if not BSDDData.is_loaded:
BSDDData.load()
props = context.scene.BIMBSDDProperties
layout = self.layout
row = self.layout.row(align=True)
row.prop(props, "load_preview_domains")
if len(props.domains):
row.operator("bim.load_bsdd_domains", text="", icon="FILE_REFRESH")
if props.active_domain:
if len(props.dictionaries):
row = self.layout.row()
row.label(text="Active: " + props.active_domain, icon="URL")
else:
row = self.layout.row()
row.label(text="No Active bSDD Domain", icon="ERROR")
row.operator("bim.load_bsdd_dictionaries", icon="FILE_REFRESH")
if len(props.domains):
if len(props.dictionaries):
self.layout.template_list(
"BIM_UL_bsdd_domains",
"BIM_UL_bsdd_dictionaries",
"",
props,
"domains",
"dictionaries",
props,
"active_domain_index",
"active_dictionary_index",
)
if 0 <= props.active_domain_index < len(props.domains):
selected_domain = props.domains[props.active_domain_index]
if 0 <= props.active_dictionary_index < len(props.dictionaries):
selected_dictionary = props.dictionaries[props.active_dictionary_index]
else:
selected_domain = None
selected_dictionary = None
if selected_domain:
layout.label(text="Selected domain:")
if selected_dictionary:
layout.label(text="Selected dictionary:")
box = layout.box()
row = box.row(align=True)
row.label(text="Language")
row.label(text=selected_domain.default_language_code)
row.label(text=selected_dictionary.default_language_code)
row = box.row(align=True)
row.label(text="Version")
row.label(text=selected_domain.version)
box.operator("bim.open_uri", text="Open bSDD In Browser", icon="URL").uri = selected_domain.uri
row.label(text=selected_dictionary.version)
box.operator("bim.open_uri", text="Open bSDD In Browser", icon="URL").uri = selected_dictionary.uri
else:
row = self.layout.row()
row.operator("bim.load_bsdd_domains")
row.operator("bim.load_bsdd_dictionaries")
class BIM_UL_bsdd_domains(UIList):
class BIM_UL_bsdd_dictionaries(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
props = context.scene.BIMBSDDProperties
row = layout.row(align=True)
if item.status != "Active":
row.label(text=f"{item.name} ({item.organization_name_owner}) - {item.status}", icon="ERROR")
row.label(
text=f"{item.name} ({item.organization_name_owner}) v{item.version} - {item.status}", icon="ERROR"
)
else:
row.label(text=f"{item.name} ({item.organization_name_owner})")
if item.uri == props.active_uri:
row.label(text="", icon="URL")
else:
op = row.operator("bim.set_active_bsdd_domain", text="", icon="RESTRICT_SELECT_OFF")
op.name = item.name
op.uri = item.uri
row.label(text=f"{item.name} ({item.organization_name_owner}) v{item.version}")
row.prop(
item, "is_active", icon="CHECKBOX_HLT" if item.is_active else "CHECKBOX_DEHLT", text="", emboss=False
)
class BIM_UL_bsdd_classifications(UIList):
@@ -116,28 +116,28 @@ class AddClassificationFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMBSDDProperties
domain = next((d for d in props.domains if d.name == props.active_domain and d.uri == props.active_uri), None)
# Maybe user loaded preview domains, set it as active
# and then reloaded them without preview domains.
if not domain:
self.report(
{"INFO"},
f"Couldn't find domain '{props.active_domain}' ({props.active_uri}). Try to reload bSDD dictionaries.",
if props.active_dictionary == "ALL":
dictionaries = [d.uri for d in props.dictionaries if d.is_active]
else:
dictionaries = [props.active_dictionary]
for uri in dictionaries:
if not (dictionary := tool.Bsdd.get_dictionary(uri)):
continue
has_classification = False
for element in tool.Ifc.get().by_type("IfcClassification"):
if element.Name == dictionary["name"] or (
tool.Classification.get_location(element) == dictionary["uri"]
):
self.report({"INFO"}, f"Classification '{dictionary['name']}' is already added to the project.")
has_classification = True
if has_classification:
continue
classification = ifcopenshell.api.run(
"classification.add_classification", tool.Ifc.get(), classification=dictionary["name"]
)
return
for element in tool.Ifc.get().by_type("IfcClassification"):
if element.Name == props.active_domain or (tool.Classification.get_location(element) == domain.uri):
self.report({"INFO"}, f"Classification '{props.active_domain}' is already added to the project.")
return
classification = ifcopenshell.api.run(
"classification.add_classification", tool.Ifc.get(), classification=props.active_domain
)
classification.Source = domain.organization_name_owner
classification.Edition = domain.version
tool.Classification.set_location(classification, domain.uri)
classification.Source = dictionary["organizationNameOwner"]
classification.Edition = dictionary["version"]
tool.Classification.set_location(classification, dictionary["uri"])
class EnableAddingManualClassification(bpy.types.Operator):
@@ -420,17 +420,17 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
classification = None
for element in tool.Ifc.get().by_type("IfcClassification"):
if element.Name == bsdd_classification.domain_name or (
tool.Classification.get_location(element) == bsdd_classification.domain_namespace_uri
if element.Name == bsdd_classification.dictionary_name or (
tool.Classification.get_location(element) == bsdd_classification.dictionary_namespace_uri
):
classification = element
break
if not classification:
classification = ifcopenshell.api.run(
"classification.add_classification", tool.Ifc.get(), classification=bsdd_classification.domain_name
"classification.add_classification", tool.Ifc.get(), classification=bsdd_classification.dictionary_name
)
tool.Classification.set_location(classification, bsdd_classification.domain_namespace_uri)
tool.Classification.set_location(classification, bsdd_classification.dictionary_namespace_uri)
for obj in objects:
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
@@ -78,13 +78,8 @@ class BIM_PT_classifications(Panel):
def draw_add_bsdd_ui(self, context):
self.bprops = context.scene.BIMBSDDProperties
if not self.bprops.active_domain:
row = self.layout.row()
row.label(text="No Active bSDD Domain", icon="ERROR")
return
row = self.layout.row()
row.label(text="Active: " + self.bprops.active_domain, icon="URL")
row.prop(self.bprops, "active_dictionary", text="")
row = self.layout.row()
row.operator("bim.add_classification_from_bsdd", icon="ADD")
@@ -163,13 +158,8 @@ class ReferenceUI:
row.operator("bim.enable_adding_manual_classification_reference", text="Add Reference", icon="ADD")
def draw_add_bsdd_ui(self, context):
if not self.bprops.active_domain:
row = self.layout.row()
row.label(text="No Active bSDD Domain", icon="ERROR")
return
row = self.layout.row()
row.label(text="Active: " + self.bprops.active_domain, icon="URL")
row.prop(self.bprops, "active_dictionary", text="")
row = self.layout.row(align=True)
row.prop(self.bprops, "keyword", text="")
@@ -30,6 +30,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING
class BIMContextProperties(PropertyGroup):
@@ -66,3 +67,10 @@ class BIMContextProperties(PropertyGroup):
)
active_context_id: IntProperty(name="Active Context Id")
context_attributes: CollectionProperty(name="Context Attributes", type=Attribute)
if TYPE_CHECKING:
contexts: str
subcontexts: str
target_views: str
active_context_id: int
context_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
+3 -2
View File
@@ -33,13 +33,14 @@ class BIM_PT_context(bpy.types.Panel):
@classmethod
def poll(cls, context):
return tool.Ifc.get()
return bool(tool.Ifc.get())
def draw(self, context):
if not ContextData.is_loaded:
ContextData.load()
props = context.scene.BIMContextProperties
assert self.layout
props = tool.Context.get_context_props()
row = self.layout.row(align=True)
row.prop(props, "contexts", text="")
@@ -61,6 +61,7 @@ classes = (
operator.ExportCostSchedules,
operator.HighlightProductCostItem,
operator.ImportCostScheduleCsv,
operator.RefreshCostScheduleCsv,
operator.LoadCostItemElementQuantities,
operator.LoadCostItemQuantities,
operator.LoadCostItemResourceQuantities,
@@ -82,6 +83,7 @@ classes = (
prop.CostItem,
prop.CostItemQuantity,
prop.CostItemType,
prop.CostItemsMapping,
prop.ScheduleColumn,
prop.BIMCostProperties,
ui.BIM_PT_cost_schedules,
+42 -26
View File
@@ -24,7 +24,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.unit
import bonsai.tool as tool
from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc
from typing import Any
from typing import Any, Union
def refresh():
@@ -33,13 +33,19 @@ def refresh():
CostItemQuantitiesData.is_loaded = False
CostItem = dict[str, Any]
CostQuantity = dict[str, Any]
CostSchedule = dict[str, Any]
Currency = dict[str, Any]
class CostSchedulesData:
data = {}
is_loaded = False
_cost_values: dict[int, dict[str, Any]]
@classmethod
def load(cls):
def load(cls) -> None:
cls.data = {
"predefined_types": cls.get_cost_schedule_types(),
"total_cost_schedules": cls.total_cost_schedules(),
@@ -54,18 +60,18 @@ class CostSchedulesData:
cls.is_loaded = True
@classmethod
def currency(cls):
def currency(cls) -> Union[Currency, None]:
unit = tool.Unit.get_project_currency_unit()
if unit:
return {"id": unit.id(), "name": unit.Currency}
@classmethod
def total_cost_schedules(cls):
def total_cost_schedules(cls) -> int:
return len(tool.Ifc.get().by_type("IfcCostSchedule"))
@classmethod
def schedules(cls):
results = []
def schedules(cls) -> list[CostSchedule]:
results: list[CostSchedule] = []
props = tool.Cost.get_cost_props()
if props.active_cost_schedule_id:
schedule = tool.Ifc.get().by_id(props.active_cost_schedule_id)
@@ -88,19 +94,19 @@ class CostSchedulesData:
return results
@classmethod
def is_editing_rates(cls):
def is_editing_rates(cls) -> bool:
props = tool.Cost.get_cost_props()
ifc_id = props.active_cost_schedule_id
if not ifc_id:
return
return False
return tool.Ifc.get().by_id(ifc_id).PredefinedType == "SCHEDULEOFRATES"
@classmethod
def cost_items(cls):
def cost_items(cls) -> dict[int, CostItem]:
cls._cost_values = {}
results = {}
results: dict[int, CostItem] = {}
for cost_item in tool.Ifc.get().by_type("IfcCostItem"):
data = {}
data: CostItem = {}
cls._load_cost_item_quantities(cost_item, data)
cls._load_cost_values(cost_item, data)
cls._load_nesting_index(cost_item, data)
@@ -108,13 +114,13 @@ class CostSchedulesData:
return results
@classmethod
def _load_nesting_index(cls, cost_item, data):
def _load_nesting_index(cls, cost_item: ifcopenshell.entity_instance, data: CostItem) -> None:
data["NestingIndex"] = None
for rel in cost_item.Nests or []:
data["NestingIndex"] = rel.RelatedObjects.index(cost_item)
@classmethod
def _load_cost_values(cls, root_element, data):
def _load_cost_values(cls, root_element: ifcopenshell.entity_instance, data: CostItem) -> None:
# data["CostValues"] = []
data["CategoryValues"] = {}
data["UnitBasisValueComponent"] = None
@@ -123,6 +129,7 @@ class CostSchedulesData:
data["TotalCost"] = 0.0
has_unit_basis = False
is_sum = False
values: list[ifcopenshell.entity_instance]
if root_element.is_a("IfcCostItem"):
values = root_element.CostValues
elif root_element.is_a("IfcConstructionResource"):
@@ -150,7 +157,7 @@ class CostSchedulesData:
data["TotalAppliedValue"] = None
@classmethod
def _load_cost_item_quantities(cls, cost_item, data):
def _load_cost_item_quantities(cls, cost_item: ifcopenshell.entity_instance, data: CostItem) -> None:
# parametric_quantities = []
# for rel in cost_item.Controls:
# for related_object in rel.RelatedObjects or []:
@@ -158,8 +165,9 @@ class CostSchedulesData:
# parametric_quantities.extend(quantities)
data["TotalCostQuantity"] = ifcopenshell.util.cost.get_total_quantity(cost_item)
data["UnitSymbol"] = "-"
if cost_item.CostQuantities:
quantity = cost_item.CostQuantities[0]
quantities: list[ifcopenshell.entity_instance] = cost_item.CostQuantities
if quantities:
quantity = quantities[0]
data["QuantityType"] = quantity.is_a()
unit = ifcopenshell.util.unit.get_property_unit(quantity, tool.Ifc.get())
if unit:
@@ -213,23 +221,31 @@ class CostSchedulesData:
) -> list[int]:
if not element.is_a("IfcObject"):
return []
cost_quantities = cost_item.CostQuantities
cost_quantities: list[ifcopenshell.entity_instance] = cost_item.CostQuantities
if not cost_quantities:
return []
results = []
results: list[int] = []
relationship: ifcopenshell.entity_instance
for relationship in element.IsDefinedBy:
if not relationship.is_a("IfcRelDefinesByProperties"):
continue
qto = relationship.RelatingPropertyDefinition
qto: ifcopenshell.entity_instance = relationship.RelatingPropertyDefinition
if not qto.is_a("IfcElementQuantity"):
continue
prop: ifcopenshell.entity_instance
for prop in qto.Quantities:
if prop in cost_quantities:
results.append(prop.id())
return results
@classmethod
def _load_cost_value(cls, root_element, root_element_data, cost_value):
def _load_cost_value(
cls,
root_element: ifcopenshell.entity_instance,
root_element_data: CostItem,
cost_value: ifcopenshell.entity_instance,
) -> None:
value_data = cost_value.get_info()
del value_data["AppliedValue"]
if value_data["UnitBasis"]:
@@ -256,8 +272,8 @@ class CostSchedulesData:
cls._load_cost_value(root_element, root_element_data, component)
@classmethod
def cost_quantities(cls):
results = []
def cost_quantities(cls) -> list[CostQuantity]:
results: list[CostQuantity] = []
props = tool.Cost.get_cost_props()
ifc_id = props.active_cost_item_id
if not ifc_id:
@@ -267,7 +283,7 @@ class CostSchedulesData:
return results
@classmethod
def cost_values(cls):
def cost_values(cls) -> list[dict[str, str]]:
props = tool.Cost.get_cost_props()
ifc_id = props.active_cost_item_id
if not ifc_id:
@@ -275,14 +291,14 @@ class CostSchedulesData:
return ifcopenshell.util.cost.get_cost_values(tool.Ifc.get().by_id(ifc_id))
@classmethod
def quantity_types(cls):
def quantity_types(cls) -> list[tuple[str, str, str]]:
return [
(t.name(), t.name(), "")
for t in tool.Ifc.schema().declaration_by_name("IfcPhysicalSimpleQuantity").subtypes()
]
@classmethod
def get_cost_schedule_types(cls):
def get_cost_schedule_types(cls) -> list[tuple[str, str, str]]:
types = ifcopenshell.util.cost.get_cost_schedule_types(tool.Ifc.get())
return [(t["name"], t["name"], t["description"]) for t in types]
@@ -299,7 +315,7 @@ class CostItemRatesData:
cls.is_loaded = True
@classmethod
def schedule_of_rates(cls):
def schedule_of_rates(cls) -> list[tuple[str, str, str]]:
return [
(str(s.id()), s.Name or "Unnamed", "")
for s in tool.Ifc.get().by_type("IfcCostSchedule")
+29 -9
View File
@@ -24,6 +24,7 @@ import bonsai.tool as tool
from bpy_extras.io_utils import ImportHelper, ExportHelper
import bonsai.tool as tool
import bonsai.core.cost as core
from pathlib import Path
from typing import get_args, TYPE_CHECKING, Literal
@@ -267,7 +268,7 @@ class AssignCostItemQuantity(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Assign Cost Item Quantity"
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
related_object_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
related_object_type: bpy.props.EnumProperty( # pyright: ignore [reportRedeclaration]
items=[(i, i, "") for i in get_args(tool.Cost.RELATED_OBJECT_TYPE)],
)
prop_name: bpy.props.StringProperty()
@@ -536,6 +537,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"})
@@ -550,7 +552,19 @@ class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator)
return True
def _execute(self, context):
core.import_cost_schedule_csv(tool.Cost, self.filepath, self.is_schedule_of_rates)
cost_schedule = core.import_cost_schedule_csv(tool.Cost, self.filepath, self.is_schedule_of_rates)
core.add_csv_filepath(tool.Cost, self.filepath, self.is_schedule_of_rates, cost_schedule)
return {"FINISHED"}
class RefreshCostScheduleCsv(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.refresh_cost_schedule_csv"
bl_label = "Refresh Cost Schedule CSV"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def _execute(self, context):
core.refresh_cost_schedule_csv(tool.Ifc, tool.Cost)
return {"FINISHED"}
@@ -686,8 +700,9 @@ class ExportCostSchedules(bpy.types.Operator, ExportHelper):
bl_idname = "bim.export_cost_schedules"
bl_label = "Export Cost Schedule"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Export a cost schedule to a CSV, XSLX OR ODS file"
cost_schedule: bpy.props.IntProperty()
bl_description = "Export current/all cost schedules as CSV, XSLX or ODS files to the provided directory."
cost_schedule: bpy.props.IntProperty(options={"SKIP_SAVE"})
format: bpy.props.EnumProperty(name="Format", items=(("CSV", "CSV", ""), ("XLSX", "XLSX", ""), ("ODS", "ODS", "")))
directory: bpy.props.StringProperty(subtype="FILE_PATH")
filter_folder: bpy.props.BoolProperty(
@@ -696,7 +711,15 @@ class ExportCostSchedules(bpy.types.Operator, ExportHelper):
)
if TYPE_CHECKING:
cost_schedule: int
format: Literal["CSV", "XLSX", "ODS"]
directory: str
def check(self, context):
if self.filepath != self.directory:
self.filepath = self.directory
return True
return False
@property
def filename_ext(self) -> str:
@@ -705,15 +728,12 @@ class ExportCostSchedules(bpy.types.Operator, ExportHelper):
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(
tool.Cost, filepath=self.directory, format=self.format, cost_schedule=cost_schedule
tool.Cost, dirpath=self.directory, format=self.format, cost_schedule=cost_schedule
)
if isinstance(r, str):
self.report({"ERROR"}, r)
return {"FINISHED"}
def invoke(self, context, event):
return ExportHelper.invoke(self, context, event)
def draw(self, context):
self.layout.label(text="Choose a format")
self.layout.prop(self, "format")
@@ -725,7 +745,7 @@ class ClearCostItemAssignments(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Clear Cost Item Product Assignments"
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
related_object_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
related_object_type: bpy.props.EnumProperty( # pyright: ignore [reportRedeclaration]
items=[(i, i, "") for i in get_args(tool.Cost.RELATED_OBJECT_TYPE)],
)
+13 -2
View File
@@ -90,7 +90,7 @@ def update_cost_item_identification(self: "CostItem", context: bpy.types.Context
**{"cost_item": self.file.by_id(self.ifc_definition_id), "attributes": {"Identification": self.identification}},
)
if props.active_cost_item_id == self.ifc_definition_id:
attribute = props.cost_item_attributes.get("Identification")
attribute = props.cost_item_attributes["Identification"]
attribute.string_value = self.identification
@@ -105,7 +105,7 @@ def update_cost_item_name(self: "CostItem", context: bpy.types.Context) -> None:
**{"cost_item": self.file.by_id(self.ifc_definition_id), "attributes": {"Name": self.name}},
)
if props.active_cost_item_id == self.ifc_definition_id:
attribute = props.cost_item_attributes.get("Name")
attribute = props.cost_item_attributes["Name"]
attribute.string_value = self.name
@@ -167,6 +167,15 @@ class CostItemQuantity(PropertyGroup):
total_cost_quantity: float
class CostItemsMapping(PropertyGroup):
cost_item_id: IntProperty(name="cost_item_id")
csv_filepath: StringProperty(name="filepath")
if TYPE_CHECKING:
cost_item_id: int
csv_filepath: str
class CostItemType(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
@@ -272,6 +281,7 @@ class BIMCostProperties(PropertyGroup):
custom_currency: StringProperty(
name="Custom Currency", default="USD", description="Custom Currency in ISO 4217 format"
)
cost_schedule_files: CollectionProperty(name="Cost Schedule Files", type=CostItemsMapping)
if TYPE_CHECKING:
cost_schedule_predefined_types: str
@@ -326,3 +336,4 @@ class BIMCostProperties(PropertyGroup):
show_cost_item_operators: bool
currency: str
custom_currency: str
cost_schedule_files: bpy.types.bpy_prop_collection_idprop[CostItemsMapping]
+42 -1
View File
@@ -71,6 +71,26 @@ class BIM_PT_cost_schedules(Panel):
text="Currently editing: {}[{}]".format(cost_schedule["name"], cost_schedule["predefined_type"]),
icon="LINENUMBERS_ON",
)
row0 = self.layout.row(align=True)
col = row0.column()
col.label(text="Linked CSV:")
row_1 = col.row(align=True)
if self.props.active_cost_schedule_id in [item.cost_item_id for item in self.props.cost_schedule_files]:
file = next(
(
item.csv_filepath
for item in self.props.cost_schedule_files
if item.cost_item_id == self.props.active_cost_schedule_id
),
None,
)
row_1.label(text=file)
row_1.operator("bim.refresh_cost_schedule_csv", icon="FILE_REFRESH", text="")
else:
row_1.label(text="No CSV file found")
row_1.operator("bim.import_cost_schedule_csv", icon="IMPORT", text="")
grid = self.layout.grid_flow(columns=2, even_columns=True)
col = grid.column()
row1 = col.row(align=True)
@@ -364,8 +384,29 @@ class BIM_PT_cost_item_types(Panel):
# TODO
col = grid.column()
has_quantity_names = CostProp.get_resource_quantity_names(self, context)
row2 = col.row(align=True)
row2.label(text="Resources")
# row2.label(text="Resources")
total_cost_item_resources = len(self.props.cost_item_resources)
row2.label(text="Resources({})".format(total_cost_item_resources))
op = row2.operator("bim.calculate_cost_item_resource_value", text="", icon="DISC")
op.cost_item = cost_item.ifc_definition_id
rtprops = context.scene.BIMResourceTreeProperties
rprops = context.scene.BIMResourceProperties
if rtprops.resources and rprops.active_resource_index < len(rtprops.resources):
if has_quantity_names:
op = row2.operator("bim.assign_cost_item_quantity", text="", icon="PROPERTIES")
op.related_object_type = "RESOURCE"
op.cost_item = cost_item.ifc_definition_id
op.prop_name = self.props.resource_quantity_names
op = row2.operator("bim.assign_cost_item_quantity", text="", icon="ADD")
op.related_object_type = "RESOURCE"
op.cost_item = cost_item.ifc_definition_id
op.prop_name = ""
row2 = col.row()
row2.template_list(
@@ -44,6 +44,7 @@ classes = (
operator.SelectHighPolygonMeshes,
operator.SelectHighestPolygonMeshes,
operator.ToggleDetailedIOSLogs,
operator.ValidateIfcAssets,
operator.ValidateIfcFile,
prop.BIMDebugProperties,
ui.BIM_PT_debug,
@@ -37,6 +37,7 @@ 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
@@ -152,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"
+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")
@@ -132,13 +132,14 @@ class RemoveDocument(bpy.types.Operator, tool.Ifc.Operator):
class AssignDocument(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_document"
bl_label = "Assign Document"
bl_description = "Assign active document to the selected objects."
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
document: bpy.props.IntProperty()
def _execute(self, context):
document = tool.Ifc.get().by_id(self.document)
objs = [bpy.data.objects.get(self.obj)] if self.obj else tool.Blender.get_selected_objects()
objs = [bpy.data.objects[self.obj]] if self.obj else tool.Blender.get_selected_objects()
for obj in objs:
element = tool.Ifc.get_entity(obj)
if element:
@@ -30,7 +30,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Union
def update_document_name(self: "Document", context: bpy.types.Context) -> None:
@@ -79,3 +79,7 @@ class BIMDocumentProperties(PropertyGroup):
breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
active_document_index: int
is_editing: bool
@property
def active_document(self) -> Union[Document, None]:
return tool.Blender.get_active_uilist_element(self.documents, self.active_document_index)
+5 -2
View File
@@ -61,14 +61,17 @@ class BIM_PT_documents(Panel):
if self.props.breadcrumbs:
row.operator("bim.add_document_reference", text="", icon="FILE_HIDDEN")
active_document = self.props.active_document
if self.props.active_document_id:
row.operator("bim.edit_document", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_document", text="", icon="CANCEL")
elif self.props.documents and self.props.active_document_index < len(self.props.documents):
ifc_definition_id = self.props.documents[self.props.active_document_index].ifc_definition_id
elif active_document:
ifc_definition_id = active_document.ifc_definition_id
row.operator("bim.select_document_objects", text="", icon="RESTRICT_SELECT_OFF").document = (
ifc_definition_id
)
row.operator("bim.assign_document", text="", icon="BRUSH_DATA").document = ifc_definition_id
row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL").document = ifc_definition_id
row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id
@@ -28,7 +28,6 @@ classes = (
operator.AddAnnotationType,
operator.AddDrawing,
operator.AddDrawingStyle,
operator.AddDrawingStyleAttribute,
operator.AddDrawingToSheet,
operator.AddReference,
operator.AddReferenceImage,
@@ -80,12 +79,10 @@ classes = (
operator.RemoveDrawing,
operator.RemoveDrawingFromSheet,
operator.RemoveDrawingStyle,
operator.RemoveDrawingStyleAttribute,
operator.RemoveReference,
operator.RemoveSchedule,
operator.RemoveSheet,
operator.RemoveTextLiteral,
operator.ResizeText,
operator.SaveDrawingStyle,
operator.SaveDrawingStylesData,
operator.SelectAllDrawings,
@@ -29,52 +29,6 @@ from typing import Optional
class Annotator:
@staticmethod
def add_text(related_element: Optional[ifcopenshell.entity_instance] = None) -> bpy.types.Object:
curve = bpy.data.curves.new(type="FONT", name="Text")
curve.body = "TEXT"
obj = bpy.data.objects.new("Text", curve)
obj.matrix_world = bpy.context.scene.camera.matrix_world
if related_element is None:
location, _, _, _ = Annotator.get_placeholder_coords(bpy.context)
else:
curve.BIMAssignedProductProperties.related_element = related_element
location = related_element.location
obj.location = location
obj.hide_render = True
font = bpy.data.fonts.get("OpenGost TypeB TT")
if not font:
font = bpy.data.fonts.load(
tool.Blender.get_data_dir_path(Path("fonts") / "OpenGost Type B TT.ttf").__str__()
)
font.name = "OpenGost Type B TT"
curve.font = font
props = tool.Drawing.get_text_props(obj)
props.font_size = "2.5"
collection = tool.Blender.get_object_bim_props(bpy.context.scene.camera).collection
collection.objects.link(obj)
Annotator.resize_text(obj)
return obj
@staticmethod
def resize_text(text_obj: bpy.types.Object) -> None:
camera = None
group = tool.Drawing.get_drawing_group(tool.Ifc.get_entity(text_obj))
for element in tool.Drawing.get_drawing_elements(group):
if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
camera = tool.Ifc.get_object(element)
break
if not camera:
return
# This is a magic number for OpenGost
font_size = 1.6 / 1000
props = tool.Drawing.get_text_props(text_obj)
font_size *= float(props.font_size)
font_size /= tool.Drawing.get_scale_ratio(tool.Drawing.get_diagram_scale(camera)["Scale"])
text_obj.data.size = font_size
@staticmethod
def add_line_to_annotation(
obj: bpy.types.Object, co1: Optional[Vector] = None, co2: Optional[Vector] = None
@@ -129,8 +83,14 @@ class Annotator:
def get_annotation_obj(
drawing: ifcopenshell.entity_instance, object_type: str, data_type: tool.Drawing.ANNOTATION_DATA_TYPE
) -> bpy.types.Object:
assert bpy.context.scene
camera = tool.Ifc.get_object(drawing)
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
assert isinstance(camera, bpy.types.Object)
# those annotations you want to obey the depth of the 3d cursor
if object_type == "PLAN_LEVEL":
co1 = bpy.context.scene.cursor.location.copy()
else:
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
matrix_world = tool.Drawing.get_camera_matrix(camera)
matrix_world.translation = co1
collection = tool.Blender.get_object_bim_props(camera).collection
+95 -39
View File
@@ -34,9 +34,7 @@ def refresh():
DrawingsData.is_loaded = False
ElementFiltersData.is_loaded = False
AnnotationData.is_loaded = False
DecoratorData.data = {}
DecoratorData.cut_cache = {}
DecoratorData.layerset_cache = {}
DecoratorData.is_loaded = False
class ProductAssignmentsData:
@@ -120,7 +118,7 @@ class DrawingsData:
return len([e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"])
@classmethod
def location_hint(cls):
def location_hint(cls) -> list[tuple[tool.Drawing.LocationHintType, str, str]]:
props = tool.Drawing.get_document_props()
if props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
results = [("0", "Origin", "")]
@@ -134,12 +132,10 @@ class DrawingsData:
@classmethod
def active_drawing_pset_data(cls):
ifc_file = tool.Ifc.get()
props = tool.Drawing.get_document_props()
drawing_id = props.active_drawing_id
if drawing_id == 0:
drawing = props.get_active_drawing()
if drawing is None:
return {}
drawing = ifc_file.by_id(drawing_id)
return ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing")
@@ -239,6 +235,41 @@ class DecoratorData:
slice_cache = {}
fill_cache = {}
@classmethod
def load(cls, handler):
cls.is_loaded = True
cls.cut_cache = {}
cls.layerset_cache = {}
text = {}
dimension = {}
fall = {}
symbol = {}
for obj in bpy.context.visible_objects:
if not (element := tool.Ifc.get_entity(obj)):
continue
if tool.Drawing.is_annotation_object_type(element, ("TEXT", "TEXT_LEADER")):
text[obj.name] = cls.get_text_data(obj)
if text[obj.name]["Symbol"]:
symbol[obj.name] = cls.get_symbol_data(obj)
elif tool.Drawing.is_annotation_object_type(
element, ("DIMENSION", "DIAMETER", "SECTION_LEVEL", "PLAN_LEVEL", "RADIUS")
):
dimension[obj.name] = cls.get_dimension_data(obj)
elif tool.Drawing.is_annotation_object_type(
element, ("FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT")
):
fall[obj.name] = cls.get_fall_data(obj)
elif tool.Drawing.is_annotation_object_type(element, ("SYMBOL",)):
symbol[obj.name] = cls.get_symbol_data(obj)
cls.data = {
"text": text,
"dimension": dimension,
"fall": fall,
"symbol": symbol,
"object_decorators": cls.object_decorators(handler),
}
@classmethod
def get_batting_thickness(cls, obj):
"""used by IfcAnnotations with ObjectType = "BATTING" """
@@ -296,17 +327,10 @@ class DecoratorData:
return display_data
@classmethod
def get_ifc_text_data(cls, obj: bpy.types.Object) -> dict[str, Any]:
def get_text_data(cls, obj: bpy.types.Object) -> dict:
"""used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER"\n
returns font size in mm for current ifc text object"""
result = cls.data.get(obj.name, None)
if result is not None:
return result
element = tool.Ifc.get_entity(obj)
if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
return None
props = tool.Drawing.get_text_props(obj)
# getting font size
pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {}
@@ -344,14 +368,7 @@ class DecoratorData:
literals_data.append(literal_data)
text_data = {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at}
cls.data[obj.name] = text_data
return text_data
@classmethod
def get_symbol(cls, obj: bpy.types.Object) -> Union[str, None]:
"""used by IfcAnnotations with ObjectType MULTI_SYMBOL"""
return tool.Drawing.get_annotation_symbol(tool.Ifc.get_entity(obj))
return {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at}
@classmethod
def get_dimension_data(cls, obj):
@@ -359,19 +376,7 @@ class DecoratorData:
DIMENSION / DIAMETER / SECTION_LEVEL / PLAN_LEVEL / RADIUS
"""
result = cls.data.get(obj.name, None)
if result is not None:
return result
element = tool.Ifc.get_entity(obj)
supported_object_types = ("DIMENSION", "DIAMETER", "SECTION_LEVEL", "PLAN_LEVEL", "RADIUS")
if (
not element
or not element.is_a("IfcAnnotation")
or ifcopenshell.util.element.get_predefined_type(element) not in supported_object_types
):
return None
dimension_style = "arrow"
fill_bg = False
classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
@@ -390,7 +395,7 @@ class DecoratorData:
custom_unit_list = pset_data.get("CustomUnit", None) or ""
custom_unit = custom_unit_list[0] if custom_unit_list else ""
dimension_data = {
return {
"dimension_style": dimension_style,
"show_description_only": show_description_only,
"suppress_zero_inches": suppress_zero_inches,
@@ -399,8 +404,59 @@ class DecoratorData:
"fill_bg": fill_bg,
"custom_unit": custom_unit,
}
cls.data[obj.name] = dimension_data
return dimension_data
@classmethod
def get_fall_data(cls, obj):
object_type = None
if element := tool.Ifc.get_entity(obj):
object_type = ifcopenshell.util.element.get_predefined_type(element)
return {"object_type": object_type}
@classmethod
def get_symbol_data(cls, obj):
return tool.Drawing.get_annotation_symbol(tool.Ifc.get_entity(obj))
@classmethod
def object_decorators(cls, handler):
import bonsai.bim.module.drawing.decoration
if not bonsai.bim.module.drawing.decoration.DecorationsHandler.installed:
return []
props = tool.Drawing.get_document_props()
if (drawing := props.get_active_drawing()) is None:
return []
camera = tool.Ifc.get_object(drawing)
assert isinstance(camera, bpy.types.Object)
collection = tool.Blender.get_object_bim_props(camera).collection
assert collection
results = []
viewport = tool.Blender.get_view3d_space()
for obj in collection.all_objects:
if not obj.visible_get(viewport=viewport):
continue
element = tool.Ifc.get_entity(obj)
if not element:
continue
if not element.is_a("IfcAnnotation"):
continue
object_type: Union[str, None] = ifcopenshell.util.element.get_predefined_type(element)
if object_type == "DRAWING":
continue
if dec := handler.decorators.get(object_type, None):
results.append((obj, dec))
elif isinstance(obj.data, bpy.types.Mesh):
if object_type == "LINEWORK" and "dashed" in str(
ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
).split(" "):
results.append((obj, handler.decorators["HIDDEN_LINE"]))
else:
results.append((obj, handler.decorators["MISC"]))
return results
class AnnotationData:
@@ -562,8 +562,7 @@ class BaseDecorator:
MiscDecorator.decorate(self, context, obj)
return
symbol = DecoratorData.get_symbol(obj)
if not symbol:
if not (symbol := DecoratorData.data["symbol"].get(obj.name, None)):
return
for vert in mesh.vertices:
@@ -572,8 +571,7 @@ class BaseDecorator:
return
# EMPTY objects
symbol = DecoratorData.get_symbol(obj)
if not symbol:
if not (symbol := DecoratorData.data["symbol"].get(obj.name, None)):
return
rotation = -Vector((1, 0)).angle_signed(annotation_dir)
@@ -590,7 +588,7 @@ class BaseDecorator:
"""if `text_world_position` is not provided, the object's location will be used"""
if not text_world_position:
text_world_position = obj.location
text_world_position = obj.matrix_world.translation
region = context.region
region3d = context.region_data
@@ -599,7 +597,7 @@ class BaseDecorator:
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)
text_data = DecoratorData.data["text"].get(obj.name, None)
if props.is_editing:
text_data = text_data | props.get_text_edited_data()
literals_data = text_data["Literals"]
@@ -656,7 +654,10 @@ class DimensionDecorator(BaseDecorator):
viewportDrawingScale = self.get_viewport_drawing_scale(context)
# setup geometry parameters
dimension_style = DecoratorData.get_dimension_data(obj)["dimension_style"]
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
if not dimension_data:
return
dimension_style = dimension_data["dimension_style"]
if dimension_style == "oblique":
size = viewportDrawingScale * 10 # OLBIQUE_SYMBOL_SIZE
angle = radians(45)
@@ -718,7 +719,9 @@ class DimensionDecorator(BaseDecorator):
element = tool.Ifc.get_entity(obj)
description = element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
if not dimension_data:
return
show_description_only = dimension_data["show_description_only"]
text_prefix = dimension_data["text_prefix"]
text_suffix = dimension_data["text_suffix"]
@@ -952,7 +955,9 @@ class RadiusDecorator(BaseDecorator):
return
element = tool.Ifc.get_entity(obj)
description = element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
if not dimension_data:
return
viewportDrawingScale = self.get_viewport_drawing_scale(context)
text_offset = 20 * viewportDrawingScale
@@ -991,8 +996,6 @@ class FallDecorator(BaseDecorator):
# generate label text
# same function as in svgwriter.py
def get_label_text():
element = tool.Ifc.get_entity(obj)
assert element
B, A = [v.co.xyz for v in spline_points[:2]]
rise = abs(A.z - B.z)
O = A.copy()
@@ -1004,8 +1007,8 @@ class FallDecorator(BaseDecorator):
else:
angle = 90
# ues SLOPE_ANGLE as default
object_type = ifcopenshell.util.element.get_predefined_type(element)
# uses SLOPE_ANGLE as default
object_type = DecoratorData.data["fall"].get(obj, {}).get("object_type", None)
if object_type in ("FALL", "SLOPE_ANGLE"):
return f"{angle}°"
elif object_type == "SLOPE_FRACTION":
@@ -1016,6 +1019,7 @@ class FallDecorator(BaseDecorator):
if angle == 90:
return "-"
return f"{round(angle_tg * 100)} %"
return "NO DATA"
if spline_points:
text = get_label_text()
@@ -1179,7 +1183,9 @@ class PlanLevelDecorator(BaseDecorator):
element = tool.Ifc.get_entity(obj)
description = element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
if not dimension_data:
return
for verts in splines:
p0, p1 = [location_3d_to_region_2d(region, region3d, v) for v in verts[:2]]
@@ -1262,7 +1268,9 @@ class SectionLevelDecorator(BaseDecorator):
storey = tool.Drawing.get_annotation_element(element)
tag = storey.Name if storey else element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
if not dimension_data:
return
for verts in splines:
@@ -1639,9 +1647,15 @@ class CutDecorator:
all_vertex_i_offset = 0
selected_vertex_i_offset = 0
classes_no_cut_str = tool.Drawing.get_document_props().classes_no_cut
classes_no_cut = [word.strip() for word in classes_no_cut_str.split(",")]
for obj in [o for o in bpy.context.visible_objects if o.type == "MESH"]:
if not (element := tool.Ifc.get_entity(obj)):
continue
# Skip the elements in the following classes
if element.is_a() in classes_no_cut:
continue
self.decorate(context, obj, element)
verts, edges = DecoratorData.cut_cache[element.id()]
@@ -1721,11 +1735,11 @@ class CutDecorator:
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)
else:
if tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
DecoratorData.cut_cache[element.id()] = (verts, edges)
else:
DecoratorData.cut_cache[element.id()] = (False, False)
def recalculate_fill(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
element_id = element.id()
@@ -1904,6 +1918,7 @@ class DecorationsHandler:
]
installed = None
handler = None
@classmethod
def install(cls, context):
@@ -1913,6 +1928,8 @@ class DecorationsHandler:
# NOTE: we USE POST_PIXEL here so that we can use both POLYLINE_UNIFORM_COLOR
# and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL")
if not DecoratorData.is_loaded:
DecoratorData.load(handler)
@classmethod
def uninstall(cls):
@@ -1935,47 +1952,9 @@ class DecorationsHandler:
for decorator in self.decorators.values():
decorator.font_id = font_id
def get_objects_and_decorators(self, collection):
# TODO: do it in data instead of the handler for performance?
results = []
viewport = bpy.context.space_data
for obj in collection.all_objects:
if not obj.visible_get(viewport=viewport):
continue
element = tool.Ifc.get_entity(obj)
if not element:
continue
if not element.is_a("IfcAnnotation"):
continue
object_type: Union[str, None] = ifcopenshell.util.element.get_predefined_type(element)
if object_type == "DRAWING":
continue
if dec := self.decorators.get(object_type, None):
results.append((obj, dec))
elif isinstance(obj.data, bpy.types.Mesh):
if object_type == "LINEWORK" and "dashed" in str(
ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
).split(" "):
results.append((obj, self.decorators["HIDDEN_LINE"]))
else:
results.append((obj, self.decorators["MISC"]))
return results
def __call__(self, context):
collection, _ = helper.get_active_drawing(context.scene)
if collection is None:
return
if not DrawingsData.is_loaded:
DrawingsData.load()
object_decorators = self.get_objects_and_decorators(collection)
for obj, decorator in object_decorators:
for obj, decorator in DecoratorData.data["object_decorators"]:
decorator.decorate(context, obj)
+17 -23
View File
@@ -37,29 +37,23 @@ def depsgraph_update_pre_handler(scene):
def set_active_camera_resolution(scene: bpy.types.Scene) -> None:
"""Sync scene render resolution with the active drawing
and prevent user from manually changing ``ortho_scale`` on IFC camera."""
props = tool.Drawing.get_document_props()
if not scene.camera or "/" not in scene.camera.name or not props.drawings:
camera_obj = scene.camera
if not camera_obj or "/" not in camera_obj.name or not props.drawings:
return
assert isinstance(scene.camera.data, bpy.types.Camera)
props = scene.camera.data.BIMCameraProperties
ortho_scale = max((props.width, props.height))
aspect_ratio = props.width / props.height
if (scene.camera.data.ortho_scale != ortho_scale) or (
scene.render.resolution_x / scene.render.resolution_y != aspect_ratio
assert isinstance((camera := camera_obj.data), bpy.types.Camera)
props = tool.Drawing.get_camera_props(camera)
if camera.type != props.camera_type:
camera.type = props.camera_type
ortho_scale, aspect_ratio = props.get_scale_and_aspect_ratio()
scene_render = scene.render
if (camera.ortho_scale != ortho_scale) or not tool.Cad.is_x(
scene_render.resolution_x / scene_render.resolution_y, aspect_ratio
):
scene.camera.data.ortho_scale = ortho_scale
diagram_scale = tool.Drawing.get_diagram_scale(scene.camera)
scale_ratio = tool.Drawing.get_scale_ratio(diagram_scale["Scale"])
if props.width > props.height:
aspect_ratio = props.height / props.width
raster_x = ortho_scale * scale_ratio * props.dpi / 0.0254
raster_y = ortho_scale * aspect_ratio * scale_ratio * props.dpi / 0.0254
else:
aspect_ratio = props.width / props.height
raster_x = ortho_scale * aspect_ratio * scale_ratio * props.dpi / 0.0254
raster_y = ortho_scale * scale_ratio * props.dpi / 0.0254
scene.render.resolution_x = scene.camera.data.BIMCameraProperties.raster_x = int(raster_x)
scene.render.resolution_y = scene.camera.data.BIMCameraProperties.raster_y = int(raster_y)
raster_x, raster_y = props.update_camera_resolution()
scene_render.resolution_x = raster_x
scene_render.resolution_y = raster_y
+10 -21
View File
@@ -305,7 +305,7 @@ def format_distance(
if unit_length == "MILLIMETERS":
value = value / 1000
if precision:
if precision and isinstance(precision, float):
value = precision * round(float(value) / precision)
if decimal_places is not None:
@@ -392,18 +392,6 @@ def format_distance(
return tx_dist
def get_active_drawing(
scene: bpy.types.Scene,
) -> Union[tuple[bpy.types.Collection, bpy.types.Camera], tuple[None, None]]:
"""Get active drawing collection and camera"""
props = tool.Drawing.get_document_props()
try:
camera = tool.Ifc.get_object(tool.Ifc.get().by_id(props.active_drawing_id))
return tool.Blender.get_object_bim_props(camera).collection, camera
except:
return None, None
def get_project_collection(scene):
"""Get main project collection"""
@@ -413,13 +401,15 @@ def get_project_collection(scene):
return colls[0]
def parse_diagram_scale(camera):
def parse_diagram_scale(camera: bpy.types.Camera) -> float:
"""Returns numeric value of scale"""
if camera.BIMCameraProperties.diagram_scale == "CUSTOM":
_, fraction = camera.BIMCameraProperties.custom_diagram_scale.split("|")
props = tool.Drawing.get_camera_props(camera)
if props.diagram_scale == "CUSTOM":
numerator = props.custom_scale_numerator
denominator = props.custom_scale_denominator
else:
_, fraction = camera.BIMCameraProperties.diagram_scale.split("|")
numerator, denominator = fraction.split("/")
_, fraction = props.diagram_scale.split("|")
numerator, denominator = fraction.split("/")
return float(numerator) / float(denominator)
@@ -431,12 +421,11 @@ def ortho_view_frame(
Similar to `bpy.types.Camera.view_frame`
:arg camera: camera of drawing
:type camera: bpy.types.Camera + BIMCameraProperties
:arg margin: margins, in scene units
:type margin: float
:return: (xmin, xmax, ymin, ymax, zmin, zmax) in local camera coordinates
"""
aspect = camera.BIMCameraProperties.raster_y / camera.BIMCameraProperties.raster_x
props = tool.Drawing.get_camera_props(camera)
aspect = props.raster_y / props.raster_x
size = camera.ortho_scale
hwidth = size * 0.5
hheight = size * 0.5 * aspect
File diff suppressed because it is too large Load Diff
+240 -66
View File
@@ -22,11 +22,13 @@ import json
import enum
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.pset
import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.core.drawing as core
import bonsai.bim.module.drawing.annotation as annotation
import bonsai.bim.module.drawing.decoration as decoration
from mathutils import Matrix
from bonsai.bim.prop import BIMFilterGroup
from bonsai.bim.module.drawing.data import DrawingsData, DecoratorData, SheetsData, AnnotationData
from bonsai.bim.module.drawing.data import refresh as refresh_drawing_data
@@ -44,7 +46,7 @@ from bpy.props import (
CollectionProperty,
BoolVectorProperty,
)
from typing import TYPE_CHECKING, Literal, Any
from typing import TYPE_CHECKING, Literal, Any, Callable, get_args, Union
diagram_scales_enum = []
@@ -55,23 +57,23 @@ def purge():
diagram_scales_enum = []
def update_target_view(self, context):
def update_target_view_doc(self: "DocProperties", context: bpy.types.Context) -> None:
DrawingsData.data["location_hint"] = DrawingsData.location_hint()
def get_location_hint(self, context):
def get_location_hint(self: "DocProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
if not DrawingsData.is_loaded:
DrawingsData.load()
return DrawingsData.data["location_hint"]
def update_diagram_scale(self, context):
def update_diagram_scale(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
if not self.update_props:
return
if not context.scene.camera or context.scene.camera.data != self.id_data:
assert context.scene
if not (camera := context.scene.camera) or camera.data != self.id_data:
return
element = tool.Ifc.get_entity(context.scene.camera)
if not element:
if not (element := tool.Ifc.get_entity(camera)):
return
try:
element = (
@@ -89,13 +91,15 @@ def update_diagram_scale(self, context):
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
else:
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="EPset_Drawing")
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties=diagram_scale)
pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=element, name="EPset_Drawing")
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties=diagram_scale)
self.update_camera_resolution()
def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
if not self.update_props:
return
assert context.scene
if not context.scene.camera or context.scene.camera.data != self.id_data:
return
element = tool.Ifc.get_entity(context.scene.camera)
@@ -118,8 +122,9 @@ def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> No
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"IsNTS": self.is_nts})
def get_diagram_scales(self, context):
def get_diagram_scales(self: "BIMCameraProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
global diagram_scales_enum
assert context.scene
if (
len(diagram_scales_enum) < 1
or (context.scene.unit_settings.system == "IMPERIAL" and len(diagram_scales_enum) == 13)
@@ -200,44 +205,39 @@ def set_drawing_style_name(self: "DrawingStyle", new_value: str) -> None:
bpy.ops.bim.save_drawing_styles_data(rename_style=True, rename_style_from=old_value, rename_style_to=new_value)
def update_document_name(self, context):
def update_document_name(self: "Document", context: bpy.types.Context) -> None:
document = tool.Ifc.get().by_id(self.ifc_definition_id)
core.update_document_name(tool.Ifc, tool.Drawing, document=document, name=self.name)
def update_has_underlay(self, context):
def update_has_underlay(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
if not self.update_props:
return
update_layer(self, context, "HasUnderlay", self.has_underlay)
assert context.scene
# making sure that camera is active
if self.has_underlay and (context.scene.camera and context.scene.camera.data == self.id_data):
bpy.ops.bim.reload_drawing_styles()
bpy.ops.bim.activate_drawing_style()
def update_has_linework(self, context):
update_layer(self, context, "HasLinework", self.has_linework)
def get_update_layer_callback(
camera_prop_name: str, pset_prop_name: str
) -> Callable[["BIMCameraProperties", bpy.types.Context], None]:
def update_layer_callback(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
update_layer(self, context, pset_prop_name, getattr(self, camera_prop_name))
return update_layer_callback
def update_has_annotation(self, context):
update_layer(self, context, "HasAnnotation", self.has_annotation)
def update_target_view_camera(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
if self.update_props and self.target_view != "MODEL_VIEW":
self.camera_type = "ORTHO"
update_layer(self, context, "TargetView", self.target_view)
def update_dpi(self, context):
update_layer(self, context, "DPI", self.dpi)
def update_linework_mode(self, context):
update_layer(self, context, "LineworkMode", self.linework_mode)
def update_fill_mode(self, context):
update_layer(self, context, "FillMode", self.fill_mode)
def update_cut_mode(self, context):
update_layer(self, context, "CutMode", self.cut_mode)
def update_layer(self, context, name, value):
def update_layer(self: "BIMCameraProperties", context: bpy.types.Context, name: str, value: Any) -> None:
assert context.scene
if not self.update_props:
return
if not context.scene.camera or context.scene.camera.data != self.id_data:
@@ -287,10 +287,24 @@ class Variable(PropertyGroup):
prop_key: StringProperty(name="Property Key")
TargetView = Literal["PLAN_VIEW", "ELEVATION_VIEW", "SECTION_VIEW", "REFLECTED_PLAN_VIEW", "MODEL_VIEW"]
TARGET_VIEW_ITEMS: list[tuple[TargetView, str, str]] = [
("PLAN_VIEW", "Plan", ""),
("ELEVATION_VIEW", "Elevation", ""),
("SECTION_VIEW", "Section", ""),
("REFLECTED_PLAN_VIEW", "RCP", ""),
("MODEL_VIEW", "Model", ""),
]
class Drawing(PropertyGroup):
ifc_definition_id: IntProperty(name="IFC Definition ID")
name: StringProperty(name="Name", update=update_drawing_name)
target_view: StringProperty(name="Target View")
target_view: EnumProperty(
name="Target View",
default="PLAN_VIEW",
items=TARGET_VIEW_ITEMS,
)
is_selected: BoolProperty(name="Is Selected", default=True)
is_drawing: BoolProperty(name="Is Drawing", default=False)
is_expanded: BoolProperty(name="Is Expanded", default=True)
@@ -298,7 +312,7 @@ class Drawing(PropertyGroup):
if TYPE_CHECKING:
ifc_definition_id: int
name: str
target_view: str
target_view: TargetView
is_selected: bool
is_drawing: bool
is_expanded: bool
@@ -334,21 +348,34 @@ class Sheet(PropertyGroup):
is_expanded: bool
RenderType = Literal["DEFAULT", "VIEWPORT"]
RENDER_TYPE_ENUM_ITEMS: dict[RenderType, tuple[str, str]] = {
"DEFAULT": ("Default", "Use default Blender render."),
"VIEWPORT": ("Viewport", "Render active viewport (using 'Rendered' viewport shading type)."),
}
class DrawingStyle(PropertyGroup):
name: StringProperty(name="Name", get=get_drawing_style_name, set=set_drawing_style_name)
raster_style: StringProperty(name="Raster Style", default="{}")
raster_style: StringProperty(
name="Raster Style",
description="JSON string for drawing style settings.",
default="{}",
)
render_type: EnumProperty(
items=[
("NONE", "None", ""),
("DEFAULT", "Default", ""),
("VIEWPORT", "Viewport", ""),
],
items=[(k, *v) for k, v in RENDER_TYPE_ENUM_ITEMS.items()],
name="Render Type",
default="VIEWPORT",
)
include_query: StringProperty(name="Include Query")
exclude_query: StringProperty(name="Exclude Query")
attributes: CollectionProperty(name="Attributes", type=StrProperty)
if TYPE_CHECKING:
name: str
raster_style: str
render_type: RenderType
include_query: str
exclude_query: str
class RasterStyleProperty(enum.Enum):
@@ -375,16 +402,10 @@ class DocProperties(PropertyGroup):
is_editing_schedules: BoolProperty(name="Is Editing Schedules", default=False)
is_editing_references: BoolProperty(name="Is Editing References", default=False)
target_view: EnumProperty(
items=[
("PLAN_VIEW", "Plan", ""),
("ELEVATION_VIEW", "Elevation", ""),
("SECTION_VIEW", "Section", ""),
("REFLECTED_PLAN_VIEW", "RCP", ""),
("MODEL_VIEW", "Model", ""),
],
items=TARGET_VIEW_ITEMS,
name="Target View",
default="PLAN_VIEW",
update=update_target_view,
update=update_target_view_doc,
)
location_hint: EnumProperty(items=get_location_hint, name="Location Hint")
drawings: CollectionProperty(name="Drawings", type=Drawing)
@@ -430,6 +451,11 @@ class DocProperties(PropertyGroup):
name="Classes to Wireframe",
description="Upon import, these classes will display as wireframe.\nEx: IfcVirtualelement, IfcSpace",
)
classes_no_cut: StringProperty(
default="IfcVirtualElement, IfcSpace",
name="Classes that are not cut",
description="The cut decoractor will be turned off for these classes\nEx: IfcVirtualelement, IfcSpace",
)
if TYPE_CHECKING:
should_use_underlay_cache: bool
@@ -439,7 +465,7 @@ class DocProperties(PropertyGroup):
is_editing_schedules: bool
is_editing_references: bool
target_view: Literal["PLAN_VIEW", "ELEVATION_VIEW", "SECTION_VIEW", "REFLECTED_PLAN_VIEW", "MODEL_VIEW"]
location_hint: str
location_hint: tool.Drawing.LocationHintType
drawings: bpy.types.bpy_prop_collection_idprop[Drawing]
active_drawing_id: int
active_drawing_index: int
@@ -471,6 +497,29 @@ class DocProperties(PropertyGroup):
imperial_precision: str
tolerance: float
classes_to_wireframe: str
classes_no_cut: str
def get_active_drawing(self) -> Union[ifcopenshell.entity_instance, None]:
drawing_id = self.active_drawing_id
if drawing_id == 0:
return None
return tool.Ifc.get().by_id(drawing_id)
def update_width_height(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
self.update_camera_resolution()
def update_camera_type(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
assert isinstance(camera := self.id_data, bpy.types.Camera)
camera.type = self.camera_type
CameraType = Literal["PERSP", "ORTHO"]
CAMERA_TYPE_ENUM_ITEMS: dict[CameraType, tuple[str, str]] = {
"ORTHO": ("Orthographic", "Most common camera for the drawings, supporting all of the features."),
"PERSP": ("Perspective", "The only avilable features for perspective camera: freestyle linework, underlay."),
}
class BIMCameraProperties(PropertyGroup):
@@ -481,7 +530,7 @@ class BIMCameraProperties(PropertyGroup):
],
default="OPENCASCADE",
name="Linework Mode",
update=update_linework_mode,
update=get_update_layer_callback("linework_mode", "LineworkMode"),
)
fill_mode: EnumProperty(
items=[
@@ -491,7 +540,7 @@ class BIMCameraProperties(PropertyGroup):
],
default="NONE",
name="Fill Mode",
update=update_fill_mode,
update=get_update_layer_callback("fill_mode", "FillMode"),
)
cut_mode: EnumProperty(
items=[
@@ -500,11 +549,32 @@ class BIMCameraProperties(PropertyGroup):
],
default="BISECT",
name="Cut Mode",
update=update_cut_mode,
update=get_update_layer_callback("cut_mode", "CutMode"),
)
has_underlay: BoolProperty(name="Underlay", default=False, update=update_has_underlay)
has_linework: BoolProperty(name="Linework", default=True, update=update_has_linework)
has_annotation: BoolProperty(name="Annotation", default=True, update=update_has_annotation)
# EPset_Drawing.
has_underlay: BoolProperty(
name="Underlay",
default=False,
update=update_has_underlay,
)
has_linework: BoolProperty(
name="Linework",
default=True,
update=get_update_layer_callback("has_linework", "HasLinework"),
)
has_annotation: BoolProperty(
name="Annotation",
default=True,
update=get_update_layer_callback("has_annotation", "HasAnnotation"),
)
target_view: EnumProperty(
name="Target View",
default="PLAN_VIEW",
items=TARGET_VIEW_ITEMS,
update=update_target_view_camera,
)
representation: StringProperty(name="Representation")
view_name: StringProperty(name="View Name")
diagram_scale: EnumProperty(items=get_diagram_scales, name="Drawing Scale", update=update_diagram_scale)
@@ -512,26 +582,85 @@ class BIMCameraProperties(PropertyGroup):
custom_scale_denominator: bpy.props.StringProperty(default="100", update=update_diagram_scale)
raster_x: IntProperty(name="Raster X", default=1000)
raster_y: IntProperty(name="Raster Y", default=1000)
dpi: IntProperty(name="DPI", default=75, update=update_dpi)
width: FloatProperty(name="Width", default=50, subtype="DISTANCE")
height: FloatProperty(name="Height", default=50, subtype="DISTANCE")
dpi: IntProperty(name="DPI", default=75, update=get_update_layer_callback("dpi", "DPI"))
width: FloatProperty(name="Width", default=50, subtype="DISTANCE", update=update_width_height)
height: FloatProperty(name="Height", default=50, subtype="DISTANCE", update=update_width_height)
# Bonsai property is needed to prevent user from using unsupported panoramic camera.
camera_type: EnumProperty(
name="Camera Type",
default="ORTHO",
items=[(k, *v) for k, v in CAMERA_TYPE_ENUM_ITEMS.items()],
update=update_camera_type,
)
is_nts: BoolProperty(name="Is NTS", update=update_is_nts)
active_drawing_style_index: IntProperty(name="Active Drawing Style Index")
filter_mode: StringProperty(name="Filter Mode", default="NONE")
include_filter_groups: CollectionProperty(type=BIMFilterGroup, name="Include Filter")
exclude_filter_groups: CollectionProperty(type=BIMFilterGroup, name="Exclude Filter")
update_props: BoolProperty(name="Enable Props Auto Update", default=True)
update_props: BoolProperty(
name="Enable Props Auto Update",
description="Update related EPset_Drawing pset on any change in camera properties.",
default=True,
)
if TYPE_CHECKING:
linework_mode: Literal["OPENCASCADE", "FREESTYLE"]
fill_mode: Literal["NONE", "SHAPELY", "SVGFILL"]
cut_mode: Literal["BISECT", "OPENCASCADE"]
has_underlay: bool
has_linework: bool
has_annotation: bool
target_view: TargetView
representation: str
view_name: str
diagram_scale: str
custom_scale_numerator: str
custom_scale_denominator: str
raster_x: int
raster_y: int
dpi: int
width: float
height: float
camera_type: CameraType
is_nts: bool
active_drawing_style_index: int
filter_mode: str
include_filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
exclude_filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
update_props: bool
def get_active_drawing_style(self) -> Union[DrawingStyle, None]:
dprops = tool.Drawing.get_document_props()
return tool.Blender.get_active_uilist_element(dprops.drawing_styles, self.active_drawing_style_index)
# For now, this JSON dump are all the parameters that determine a camera's "Block representation"
# By checking this, you will know whether or not the camera IFC representation needs to be refreshed
def update_representation(self, obj):
def update_representation(self, matrix_world: Matrix) -> bool:
"""Update ``representation`` based on current camera properties and the provided world matrix.
:return: ``True`` if ``representation`` was updated and
representation should also be updated in IFC.
"""
# Matrix is used instead of Object so this works before the Object exists,
# allowing all camera initialization to stay encapsulated in `create_camera`.
camera = self.id_data
assert isinstance(camera, bpy.types.Camera)
# Rounding is necessary to avoid float garbage differences
# forcing unnecessary representation update.
def round_(f: float) -> float:
return round(f, 6)
representation = json.dumps(
{
"matrix": [list(x) for x in obj.matrix_world],
"type": self.camera_type,
"matrix": [[round_(v) for v in row] for row in matrix_world],
"raster_x": self.raster_x,
"raster_y": self.raster_y,
"ortho_scale": obj.data.ortho_scale,
"clip_end": obj.data.clip_end,
"ortho_scale": round_(camera.ortho_scale),
"clip_end": round_(camera.clip_end),
}
)
if self.representation != representation:
@@ -539,6 +668,41 @@ class BIMCameraProperties(PropertyGroup):
return True
return False
def update_camera_resolution(self) -> tuple[int, int]:
"""Update ``camera.ortho_scale``, ``raster_x`` and ``raster_y``
based on current ``width`` and ``height`` and diagram scale props.
:return: tuple[resolution_x, resolution_y]
"""
assert isinstance(camera := self.id_data, bpy.types.Camera)
ortho_scale, aspect_ratio = self.get_scale_and_aspect_ratio()
aspect_ratio = self.width / self.height
camera.ortho_scale = ortho_scale
diagram_scale = tool.Drawing.get_diagram_scale(camera)
scale_ratio = tool.Drawing.get_scale_ratio(diagram_scale["Scale"])
if self.width > self.height:
aspect_ratio = self.height / self.width
raster_x = ortho_scale * scale_ratio * self.dpi / 0.0254
raster_y = ortho_scale * aspect_ratio * scale_ratio * self.dpi / 0.0254
else:
aspect_ratio = self.width / self.height
raster_x = ortho_scale * aspect_ratio * scale_ratio * self.dpi / 0.0254
raster_y = ortho_scale * scale_ratio * self.dpi / 0.0254
raster_x, raster_y = int(raster_x), int(raster_y)
self.raster_x, self.raster_y = raster_x, raster_y
return raster_x, raster_y
def get_scale_and_aspect_ratio(self) -> tuple[float, float]:
"""
:return: A tuple of calculated ortho scale and aspect ratio values.
"""
ortho_scale = max(self.width, self.height)
aspect_ratio = self.width / self.height
return ortho_scale, aspect_ratio
DEFAULT_BOX_ALIGNMENT = [False] * 6 + [True] + [False] * 2
BOX_ALIGNMENT_POSITIONS = [
@@ -596,6 +760,12 @@ class LiteralProps(PropertyGroup):
}
return text_data
if TYPE_CHECKING:
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
value: str
box_alignment: str
ifc_definition_id: int
class BIMTextProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing", default=False)
@@ -621,7 +791,7 @@ class BIMTextProperties(PropertyGroup):
def get_text_edited_data(self) -> dict[str, Any]:
"""should be called only if `is_editing`
otherwise should use `DecoratorData.get_ifc_text_data(obj)` instead
otherwise should use `DecoratorData.get_text_data(obj)` instead
because this data could be out of date
"""
literals_data = []
@@ -646,6 +816,10 @@ class BIMAssignedProductProperties(PropertyGroup):
is_editing_product: BoolProperty(name="Is Editing Product", default=False)
relating_product: PointerProperty(name="Relating Product", type=bpy.types.Object, poll=relating_product_poll)
if TYPE_CHECKING:
is_editing_product: bool
relating_product: Union[bpy.types.Object, None]
annotation_classes = [
(x, *tool.Drawing.ANNOTATION_TYPES_DATA[x][:3], i) for i, x in enumerate(tool.Drawing.ANNOTATION_TYPES_DATA)
@@ -90,7 +90,6 @@ class SheetBuilder:
layout_dir = os.path.dirname(layout_path)
drawing_path = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_reference(drawing))
underlay_path = os.path.splitext(drawing_path)[0] + "-underlay.png"
if not os.path.exists(layout_path) or not os.path.exists(drawing_path):
raise FileNotFoundError
@@ -116,16 +115,6 @@ class SheetBuilder:
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(x)
background.attrib["y"] = str(y)
background.attrib["width"] = str(view_width)
background.attrib["height"] = str(view_height)
# add foreground
if os.path.isfile(drawing_path):
foreground = ET.SubElement(view, "image")
@@ -406,14 +395,11 @@ class SheetBuilder:
images = view.findall("{http://www.w3.org/2000/svg}image")
background = None
foreground = None
view_title = None
for image in images:
if image.attrib["data-type"] == "background":
background = image
elif image.attrib["data-type"] == "foreground":
if image.attrib["data-type"] == "foreground":
foreground = image
elif image.attrib["data-type"] == "view-title":
view_title = image
@@ -423,12 +409,6 @@ class SheetBuilder:
svg = self.ensure_drawing_unique_styles(svg, drawing_id)
view.append(svg)
if background is not None:
background_path = os.path.join(self.layout_dir, self.get_href(background))
raster_path = os.path.join(self.sheets_dir, os.path.basename(background_path))
shutil.copy(background_path, raster_path)
self.references["RASTER"].append(raster_path)
if view_title is not None:
foreground_path = self.get_href(foreground)
data = reference.get_info()
@@ -521,8 +501,14 @@ class SheetBuilder:
self.scale = embedded.attrib.get("data-scale")
images = embedded.findall("{http://www.w3.org/2000/svg}image")
for image in images:
new_href = ntpath.basename(image.attrib.get("{http://www.w3.org/1999/xlink}href"))
image.attrib["{http://www.w3.org/1999/xlink}href"] = new_href
old_href = Path(image.attrib.get("{http://www.w3.org/1999/xlink}href"))
if not os.path.isabs(old_href):
template_dir = Path(os.path.join(self.layout_dir, svg_path)).resolve().parent
old_href = Path(os.path.join(template_dir, old_href))
old_href = old_href.absolute().resolve().as_posix()
new_href = Path(os.path.join(self.sheets_dir, Path(old_href).name)).absolute().resolve().as_posix()
shutil.copy(old_href, new_href)
image.attrib["{http://www.w3.org/1999/xlink}href"] = Path(old_href).name
for child in embedded:
if "namedview" in child.tag:
continue
@@ -22,6 +22,7 @@ import bpy
import math
import bmesh
import shutil
from bpy.types import SplineBezierPoints, SplinePoints
import mathutils
import xml.etree.ElementTree as ET
import svgwrite
@@ -37,7 +38,7 @@ from bonsai.bim.module.drawing.data import DrawingsData
from bonsai.bim.module.drawing.data import DecoratorData
from math import pi, ceil, atan, degrees, acos
from mathutils import geometry, Vector
from typing import Optional, Self
from typing import Optional, Self, Union, Sequence, Callable
from pathlib import Path
@@ -59,14 +60,29 @@ class External(svgwrite.container.Group):
class SvgWriter:
def __init__(self):
metadata: list[str]
resource_paths: dict[tool.Drawing.ResourceType, Union[str, None]]
def __init__(
self,
*,
camera_width: float,
camera_height: float,
camera: bpy.types.Object,
camera_projection: Vector,
scale: float = 1 / 100, # 1:100
human_scale: str = "NTS",
):
self.data_dir = None
self.human_scale = "NTS"
self.human_scale = human_scale
self.metadata = []
self.scale = 1 / 100 # 1:100
self.camera_width = None
self.camera_height = None
self.scale = scale
self.camera = camera
self.camera_width = camera_width
self.camera_height = camera_height
self.camera_projection = camera_projection
self.resource_paths = {}
self.calculate_scale()
def create_blank_svg(self, output_path: str) -> Self:
self.calculate_scale()
@@ -90,7 +106,7 @@ class SvgWriter:
def setup_drawing_resource_paths(self, element: ifcopenshell.entity_instance) -> None:
pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing")
for resource in ("Stylesheet", "Markers", "Symbols", "Patterns", "ShadingStyles"):
for resource in tool.Drawing.RESOURCE_TYPES:
resource_path = pset.get(resource)
if not resource_path:
self.resource_paths[resource] = None
@@ -176,12 +192,20 @@ class SvgWriter:
for child in root:
self.svg.defs.add(External(child))
def draw_annotations(self, annotations, precision, decimal_places):
def draw_annotations(
self,
annotations: list[ifcopenshell.entity_instance],
precision: Union[float, None],
decimal_places: Union[int, None],
) -> Self:
self.precision = precision
self.decimal_places = decimal_places
for element in annotations:
obj = tool.Ifc.get_object(element)
if not obj or (object_type := ifcopenshell.util.element.get_predefined_type(element)) == "DRAWING":
if (
not isinstance(obj, bpy.types.Object)
or (object_type := ifcopenshell.util.element.get_predefined_type(element)) == "DRAWING"
):
continue
elif object_type == "GRID":
self.draw_grid_annotation(obj)
@@ -256,11 +280,12 @@ class SvgWriter:
get_text, tag, dimension_data, text_position=text_position, angle=angle, class_str="SECTIONLEVEL"
)
def draw_stair_annotation(self, obj):
def draw_stair_annotation(self, obj: bpy.types.Object) -> None:
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
matrix_world = obj.matrix_world
classes = self.get_attribute_classes(obj)
assert isinstance(obj.data, bpy.types.Curve)
for spline in obj.data.splines:
points = self.get_spline_points(spline)
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
@@ -284,7 +309,7 @@ class SvgWriter:
)
)
def draw_grid_annotation(self, obj):
def draw_grid_annotation(self, obj: bpy.types.Object) -> None:
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
matrix_world = obj.matrix_world
@@ -358,6 +383,7 @@ class SvgWriter:
def get_attribute_classes(self, obj: bpy.types.Object) -> list[str]:
element = tool.Ifc.get_entity(obj)
assert element
global_id = "GlobalId-{}".format(element.GlobalId)
predefined_type = "PredefinedType-" + tool.Drawing.canonicalise_class_name(
str(ifcopenshell.util.element.get_predefined_type(element))
@@ -374,7 +400,7 @@ class SvgWriter:
)
return classes
def draw_line_annotation(self, obj):
def draw_line_annotation(self, obj: bpy.types.Object) -> None:
# TODO: properly scope these offsets
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
@@ -397,7 +423,7 @@ class SvgWriter:
elif isinstance(obj.data, bpy.types.Mesh):
self.draw_edge_annotation(obj, classes)
def draw_edge_annotation(self, obj, classes):
def draw_edge_annotation(self, obj: bpy.types.Object, classes: list[str]) -> None:
predefined_type = classes[2].split("-", 1)[1]
x_offset = self.raw_width / 2
@@ -415,7 +441,7 @@ class SvgWriter:
self.svg.line(start=start * self.svg_scale, end=end * self.svg_scale, class_=" ".join(classes))
)
def draw_batting_annotation():
def draw_batting_annotation() -> None:
v0_global = matrix_world @ obj.data.vertices[0].co.xyz
v1_global = matrix_world @ obj.data.vertices[1].co.xyz
v0 = self.project_point_onto_camera(v0_global)
@@ -481,41 +507,42 @@ class SvgWriter:
)
self.svg.add(self.svg.polyline(points=points, class_=" ".join(classes), style=polyline_style))
def draw_revision_cloud_annotation():
def draw_revision_cloud_annotation() -> None:
segment_width = 15.0
base_height = 1
width = 5
def get_svg_half_circle(height, width):
def get_svg_half_circle(height: float, width: float) -> str:
cp0 = f"0,-{height}"
cp1 = f"{width},-{height}"
end_point = f"{width},0"
circle = f"c{cp0} {cp1} {end_point}"
return circle
def get_revision_pattern(base_offset):
def get_revision_pattern(base_offset: Vector) -> str:
pattern = f"m{base_offset.x},{base_offset.y}"
pattern += " " + get_svg_half_circle(2 * base_height, width)
pattern += " " + get_svg_half_circle(2.5 * base_height, width)
pattern += " " + get_svg_half_circle(1.5 * base_height, width)
return pattern
def get_scale(size, direction):
def get_scale(size: float, direction: Vector) -> str:
original_edge = direction * size
current_svg_segments = ceil(size / segment_width) * segment_width * direction
scale = [1 if original_edge[i] == 0 else original_edge[i] / current_svg_segments[i] for i in range(2)]
return "scale(%f, %f)" % (scale[0], scale[1])
def poly_to_edges(poly):
edges = []
def poly_to_edges(poly: Sequence[int]) -> list[tuple[int, int]]:
edges: list[tuple[int, int]] = []
n_verts = len(poly)
lats_index = n_verts - 1
last_index = n_verts - 1
for i in range(len(poly)):
edge = [poly[i], (poly[i + 1]) if i != lats_index else poly[0]]
edge = (poly[i], (poly[i + 1]) if i != last_index else poly[0])
edges.append(edge)
return edges
element = tool.Ifc.get_entity(obj)
assert element
safe_offset_x = 2.0
marker_width = segment_width + safe_offset_x * 2
market_height = 15.0
@@ -573,7 +600,7 @@ class SvgWriter:
)
self.svg.add(polyline)
def draw_section_annotation():
def draw_section_annotation() -> None:
display_data = DecoratorData.get_section_markers_display_data(obj)
connect_markers = display_data["connect_markers"]
@@ -624,7 +651,7 @@ class SvgWriter:
for edge in obj.data.edges:
draw_simple_edge_annotation(*edge.vertices[:])
def draw_leader_annotation(self, obj):
def draw_leader_annotation(self, obj: bpy.types.Object) -> None:
self.draw_line_annotation(obj)
spline = obj.data.splines[0]
spline_points = spline.bezier_points if spline.bezier_points else spline.points
@@ -634,7 +661,7 @@ class SvgWriter:
position = Vector((0, 0, 0))
self.draw_text_annotation(obj, position)
def draw_section_annotation(self, obj):
def draw_section_annotation(self, obj: bpy.types.Object) -> None:
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
@@ -688,7 +715,7 @@ class SvgWriter:
)
)
def draw_elevation_annotation(self, obj):
def draw_elevation_annotation(self, obj: bpy.types.Object) -> None:
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
symbol_position = self.project_point_onto_camera(obj.location)
@@ -927,7 +954,7 @@ class SvgWriter:
symbol_transform = self.get_symbol_transform(svg_position_str)
self.draw_symbol(svg_id, symbol_transform)
def draw_break_annotations(self, obj):
def draw_break_annotations(self, obj: bpy.types.Object) -> None:
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
@@ -950,18 +977,20 @@ class SvgWriter:
d = "M{}".format(d[1:])
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
def draw_plan_level_annotation(self, obj):
def draw_plan_level_annotation(self, obj: bpy.types.Object) -> None:
offset = Vector([self.raw_width, self.raw_height]) / 2
matrix_world = obj.matrix_world
classes = self.get_attribute_classes(obj)
element = tool.Ifc.get_entity(obj)
assert element
description = element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
suppress_zero_inches = dimension_data["suppress_zero_inches"]
base_offset_y = 1.0
assert isinstance(obj.data, bpy.types.Curve)
for spline in obj.data.splines:
points = self.get_spline_points(spline)
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
@@ -1000,7 +1029,8 @@ class SvgWriter:
box_alignment=box_alignment,
)
def draw_angle_annotations(self, obj):
def draw_angle_annotations(self, obj: bpy.types.Object) -> None:
assert isinstance(obj.data, bpy.types.Curve)
points = obj.data.splines[0].points
# [1, 2, 3, 4, 5] -> [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
points_chunked = [points[i : i + 3] for i in range(len(points) - 2)]
@@ -1009,18 +1039,18 @@ class SvgWriter:
points_chunk = [obj.matrix_world @ p.co.xyz for p in points_chunk]
self.draw_svg_3point_arc(obj, points_chunk)
def draw_svg_3point_arc(self, obj, angle_points):
def draw_svg_3point_arc(self, obj: bpy.types.Object, angle_points: list[Vector]) -> None:
"""`angle_points` are expected to be already in world space"""
# This implementation uses an SVG arc, which means that it can only draw
# arcs that are orthogonal to the view (e.g. not arcs in 3D).
# Gosh this is bad code :(
def position_on_svg(p):
def position_on_svg(p: Vector) -> Vector:
p = self.project_point_onto_camera(p)
offset = Vector([self.raw_width / 2, self.raw_height / 2])
return (offset + p.xy * Vector((1, -1))) * self.svg_scale
def get_angle_value():
def get_angle_value() -> float:
"""points should be in world space"""
# calculate arc angle, need to make sure we do it in world space
v0, v1, v2 = angle_points
@@ -1068,7 +1098,7 @@ class SvgWriter:
# When rewriting to use polylines, we should use this normal instead.
# center = tool.Cad.get_center_of_arc([p.to_3d() for p in arc_points], None).to_2d()
# normal = mathutils.geometry.normal([arc_end_pts[0], arc_end_pts[1], center])
# normal = Vector(self.camera_projection)
# normal = self.camera_projection
# dir1 = (arc_end_pts[0] - center).normalized()
# arc_mid_dir = (arc_end_pts[1] - center).normalized()
@@ -1104,10 +1134,11 @@ class SvgWriter:
classes = self.get_attribute_classes(obj)
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
def draw_radius_annotations(self, obj):
def draw_radius_annotations(self, obj: bpy.types.Object) -> None:
offset = Vector([self.raw_width, self.raw_height]) / 2
classes = self.get_attribute_classes(obj)
element = tool.Ifc.get_entity(obj)
assert element
tag = element.Description
matrix_world = obj.matrix_world
@@ -1141,7 +1172,7 @@ class SvgWriter:
get_text, tag, dimension_data, text_position=text_position, class_str="RADIUS", box_alignment="center"
)
def draw_dimension_text(self, get_text, tag, dimension_data, **create_text_kwargs):
def draw_dimension_text(self, get_text: Callable[[], str], tag, dimension_data, **create_text_kwargs) -> None:
prefix = dimension_data["text_prefix"]
suffix = dimension_data["text_suffix"]
show_description_only = dimension_data["show_description_only"]
@@ -1166,12 +1197,13 @@ class SvgWriter:
for text in text_tags:
self.svg.add(text)
def draw_fall_annotations(self, obj):
def draw_fall_annotations(self, obj: bpy.types.Object) -> None:
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
classes = self.get_attribute_classes(obj)
element = tool.Ifc.get_entity(obj)
matrix_world = obj.matrix_world
assert isinstance(obj.data, bpy.types.Curve)
for spline in obj.data.splines:
points = self.get_spline_points(spline)
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
@@ -1236,13 +1268,14 @@ class SvgWriter:
text_style = SvgWriter.get_box_alignment_parameters("center")
self.svg.add(self.svg.text(tag, insert=tuple(text_position), class_="RADIUS", **text_style))
def draw_diameter_annotations(self, obj):
def draw_diameter_annotations(self, obj: bpy.types.Object) -> None:
classes = self.get_attribute_classes(obj)
matrix_world = obj.matrix_world
element = tool.Ifc.get_entity(obj)
dimension_text = element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
assert isinstance(obj.data, bpy.types.Curve)
for spline in obj.data.splines:
points = self.get_spline_points(spline)
for i, p in enumerate(points):
@@ -1264,13 +1297,14 @@ class SvgWriter:
custom_unit=dimension_data["custom_unit"],
)
def draw_dimension_annotations(self, obj):
def draw_dimension_annotations(self, obj: bpy.types.Object) -> None:
classes = self.get_attribute_classes(obj)
matrix_world = obj.matrix_world
element = tool.Ifc.get_entity(obj)
dimension_text = element.Description
dimension_data = DecoratorData.get_dimension_data(obj)
assert isinstance(obj.data, bpy.types.Curve)
for spline in obj.data.splines:
points = self.get_spline_points(spline)
for i in range(len(points) - 1):
@@ -1289,7 +1323,7 @@ class SvgWriter:
custom_unit=dimension_data["custom_unit"],
)
def draw_measureit_arch_dimension_annotations(self):
def draw_measureit_arch_dimension_annotations(self) -> None:
try:
import MeasureIt_ARCH.measureit_arch_external_utils
@@ -1303,9 +1337,9 @@ class SvgWriter:
def draw_dimension_annotation(
self,
v0_global,
v1_global,
classes,
v0_global: Vector,
v1_global: Vector,
classes: list[str],
dimension_text=None,
text_format=lambda x: x,
show_description_only=False,
@@ -1314,7 +1348,7 @@ class SvgWriter:
text_suffix="",
fill_bg=False,
custom_unit=None,
):
) -> None:
offset = Vector([self.raw_width, self.raw_height]) / 2
v0 = self.project_point_onto_camera(v0_global)
v1 = self.project_point_onto_camera(v1_global)
@@ -1441,12 +1475,12 @@ class SvgWriter:
# TODO is this needlessly complex?
return self.camera.matrix_world.inverted() @ geometry.intersect_line_plane(
point.xyz,
point.xyz - Vector(self.camera_projection),
point.xyz - self.camera_projection,
self.camera.location,
Vector(self.camera_projection),
self.camera_projection,
)
def get_spline_points(self, spline):
def get_spline_points(self, spline: bpy.types.Spline) -> Union[SplineBezierPoints, SplinePoints]:
return spline.bezier_points if spline.bezier_points else spline.points
def draw_polygon(self, polygon, position):
+52 -29
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import bonsai.bim.helper
import bonsai.tool as tool
@@ -28,6 +29,10 @@ from bonsai.bim.module.drawing.data import (
ElementFiltersData,
DecoratorData,
)
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.drawing.prop import DocProperties, Drawing
class BIM_PT_camera(Panel):
@@ -39,18 +44,21 @@ class BIM_PT_camera(Panel):
bl_parent_id = "BIM_PT_tab_drawings"
def draw(self, context):
if not (context.scene.camera and hasattr(context.scene.camera.data, "BIMCameraProperties")):
assert context.scene and self.layout
camera = context.scene.camera
if not camera:
row = self.layout.row()
row.label(text="No Active Drawing", icon="ERROR")
return
if "/" not in context.scene.camera.name:
if not tool.Ifc.get_entity(camera):
self.layout.label(text="This is not a BIM camera.")
return
assert isinstance(camera_data := camera.data, bpy.types.Camera)
props = tool.Drawing.get_camera_props(camera)
self.layout.use_property_split = True
dprops = tool.Drawing.get_document_props()
props = context.scene.camera.data.BIMCameraProperties
col = self.layout.column(align=True)
row = col.row(align=True)
@@ -63,6 +71,13 @@ class BIM_PT_camera(Panel):
row.prop(props, "has_annotation", icon="MOD_EDGESPLIT")
row.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
row = self.layout.row(align=True)
row.prop(props, "target_view")
if props.target_view == "MODEL_VIEW":
row = self.layout.row()
row.prop(props, "camera_type")
row = self.layout.row()
row.prop(props, "linework_mode")
if props.linework_mode == "OPENCASCADE":
@@ -77,7 +92,7 @@ class BIM_PT_camera(Panel):
row.prop(props, "height")
row = self.layout.row()
row.prop(context.scene.camera.data, "clip_end", text="Depth")
row.prop(camera_data, "clip_end", text="Depth")
row = self.layout.row(align=True)
row.prop(props, "diagram_scale", text="Scale")
@@ -110,7 +125,8 @@ class BIM_PT_element_filters(Panel):
if not ElementFiltersData.is_loaded:
ElementFiltersData.load()
props = context.scene.camera.data.BIMCameraProperties
assert context.scene and (camera := context.scene.camera)
props = tool.Drawing.get_camera_props(camera)
if props.filter_mode == "INCLUDE":
bonsai.bim.helper.draw_filter(
@@ -157,13 +173,13 @@ class BIM_PT_drawing_underlay(Panel):
return bool((camera := context.scene.camera) and tool.Ifc.get_entity(camera))
def draw(self, context):
assert self.layout
layout = self.layout
layout.use_property_split = True
camera = context.scene.camera
assert camera
assert context.scene and (camera := context.scene.camera)
dprops = tool.Drawing.get_document_props()
props = camera.data.BIMCameraProperties
drawing_index_is_valid = props.active_drawing_style_index < len(dprops.drawing_styles)
props = tool.Drawing.get_camera_props(camera)
drawing_style = props.get_active_drawing_style()
if not DrawingsData.is_loaded:
DrawingsData.load()
@@ -177,7 +193,7 @@ class BIM_PT_drawing_underlay(Panel):
row.label(text="Current Shading Style:")
row.label(text=current_shading_style)
row.operator("bim.add_drawing_style", icon="ADD", text="")
if drawing_index_is_valid:
if drawing_style:
row.operator("bim.remove_drawing_style", icon="X", text="").index = props.active_drawing_style_index
row.operator("bim.reload_drawing_styles", icon="FILE_REFRESH", text="")
@@ -192,9 +208,8 @@ class BIM_PT_drawing_underlay(Panel):
"active_drawing_style_index",
)
if not drawing_index_is_valid:
if drawing_style is None:
return
drawing_style = dprops.drawing_styles[props.active_drawing_style_index]
row = layout.row(align=True)
row.prop(drawing_style, "name")
@@ -206,14 +221,6 @@ class BIM_PT_drawing_underlay(Panel):
row = layout.row(align=True)
row.prop(drawing_style, "exclude_query")
row = layout.row()
row.operator("bim.add_drawing_style_attribute")
for index, attribute in enumerate(drawing_style.attributes):
row = layout.row(align=True)
row.prop(attribute, "name", text="")
row.operator("bim.remove_drawing_style_attribute", icon="X", text="").index = index
row = layout.row(align=True)
row.operator("bim.save_drawing_style")
row.operator("bim.activate_drawing_style")
@@ -266,9 +273,6 @@ class BIM_PT_drawings(Panel):
row3 = row.row(align=True)
row3.alignment = "RIGHT"
row3.operator("bim.activate_drawing", icon="OUTLINER_OB_CAMERA", text="").drawing = (
active_drawing.ifc_definition_id
)
row3.operator("bim.activate_model", icon="VIEW3D", text="")
row3.separator(factor=0.5, type="SPACE")
@@ -490,17 +494,19 @@ class BIM_PT_product_assignments(Panel):
@classmethod
def poll(cls, context):
if not tool.Ifc.get() or not context.active_object:
return
return False
element = tool.Ifc.get_entity(context.active_object)
if not element:
return
return False
return element.is_a("IfcAnnotation")
def draw(self, context):
if not ProductAssignmentsData.is_loaded:
ProductAssignmentsData.load()
props = context.active_object.BIMAssignedProductProperties
assert self.layout
assert (obj := context.active_object)
props = tool.Drawing.get_object_assigned_product_props(obj)
if props.is_editing_product:
row = self.layout.row(align=True)
@@ -586,7 +592,7 @@ class BIM_PT_text(Panel):
col.label(text=f' {literal_props.attributes["BoxAlignment"].string_value}')
else:
text_data = DecoratorData.get_ifc_text_data(obj)
text_data = DecoratorData.get_text_data(obj)
row = self.layout.row()
row.operator("bim.enable_editing_text", icon="GREASEPENCIL")
@@ -607,7 +613,16 @@ class BIM_PT_text(Panel):
class BIM_UL_drawinglist(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: DocProperties,
item: Drawing,
icon,
active_data,
active_propname,
):
if not item:
layout.label(text="", translate=False)
return
@@ -617,14 +632,22 @@ class BIM_UL_drawinglist(bpy.types.UIList):
row.label(text="", icon="BLANK1")
selected_icon = "CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT"
row.prop(item, "is_selected", text="", icon=selected_icon, emboss=False)
row.separator(factor=0.5, type="SPACE")
row.prop(item, "name", text="", emboss=False)
row.separator(factor=0.25, type="SPACE")
self.props = tool.Drawing.get_document_props()
if (
self.props.drawings
and self.props.active_drawing_id
and item.ifc_definition_id == self.props.active_drawing_id
):
row.label(text="", icon="OUTLINER_OB_CAMERA")
row.operator("bim.activate_drawing", text="", icon="VIEW_CAMERA", emboss=True, depress=True).drawing = (
item.ifc_definition_id
)
else:
row.operator("bim.activate_drawing", text="", icon="VIEW_CAMERA_UNSELECTED", emboss=False).drawing = (
item.ifc_definition_id
)
else:
if item.target_view == "PLAN_VIEW":
icon = "UV_FACESEL"
@@ -218,7 +218,7 @@ class AnnotationToolUI:
@classmethod
def draw_edit_object_interface(cls, context):
if DecoratorData.get_ifc_text_data(bpy.context.active_object):
if DecoratorData.get_text_data(bpy.context.active_object):
add_layout_hotkey_operator(cls.layout, "Edit Text", "S_E", "")
@classmethod
@@ -311,7 +311,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if not bpy.context.active_object:
return
if DecoratorData.get_ifc_text_data(bpy.context.active_object):
if DecoratorData.get_text_data(bpy.context.active_object):
bpy.ops.bim.edit_text_popup()
def hotkey_S_G(self):
@@ -56,7 +56,7 @@ classes = (
operator.OverrideMeshSeparate,
operator.OverrideModeSetEdit,
operator.OverrideModeSetObject,
operator.OverrideMove,
operator.OverrideMoveSelect,
operator.OverrideMoveMacro,
operator.OverrideOriginSet,
operator.OverrideOutlinerDelete,
@@ -95,7 +95,7 @@ classes = (
)
addon_keymaps = []
addon_keymaps: list[tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
@persistent
@@ -124,9 +124,9 @@ 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("BIM_OT_override_move_select")
operator.DuplicateMoveLinkedAggregateMacro.define("TRANSFORM_OT_translate")
operator.OverrideMoveMacro.define("BIM_OT_override_move")
operator.OverrideMoveMacro.define("BIM_OT_override_move_select")
operator.OverrideMoveMacro.define("TRANSFORM_OT_translate")
bpy.types.Object.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMObjectGeometryProperties)
@@ -156,11 +156,19 @@ def register():
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_mode_set_edit", "TAB", "PRESS")
addon_keymaps.append((km, kmi))
# Deletion.
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS")
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS", shift=True)
kmi.properties.use_global = True
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_object_delete", "DEL", "PRESS")
kmi.properties.confirm = False
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_object_delete", "DEL", "PRESS", shift=True)
kmi.properties.confirm = False
kmi.properties.use_global = True
addon_keymaps.append((km, kmi))
km = wm.keyconfigs.addon.keymaps.new(name="Mesh", space_type="EMPTY")
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
+35 -22
View File
@@ -23,6 +23,7 @@ import ifcopenshell.util.placement
import ifcopenshell.util.schema
import ifcopenshell.util.unit
import bonsai.tool as tool
from typing import Any, Union
from mathutils import Vector
@@ -45,13 +46,12 @@ class ViewportData:
cls.data = {"mode": cls.mode()}
@classmethod
def mode(cls):
def mode(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
obj_mode = ("OBJECT", "IFC Object Mode", "View and move the placements of objects", "OBJECT_DATAMODE", 0)
item_mode = ("ITEM", "IFC Item Mode", "View individual representation items", "MESH_DATA", 1)
edit_mode = ("EDIT", "IFC Edit Mode", "Edit representation items", "EDITMODE_HLT", 2)
obj = bpy.context.active_object
element = tool.Ifc.get_entity(obj)
modes: list[tuple[str, str, str, str, int]] = [obj_mode]
gprops = tool.Geometry.get_geometry_props()
@@ -61,13 +61,14 @@ class ViewportData:
if not obj:
return modes
element = tool.Ifc.get_entity(obj)
pprops = tool.Project.get_project_props()
if obj in pprops.clipping_planes_objs:
pass
elif element:
if tool.Geometry.is_locked(element):
pass
elif obj.data and tool.Geometry.is_profile_based(obj.data):
elif obj.data and tool.Geometry.has_mesh_properties(obj.data) and tool.Geometry.is_profile_based(obj.data):
modes.append(edit_mode)
elif element.is_a("IfcRelSpaceBoundary"):
modes.append(edit_mode)
@@ -88,7 +89,7 @@ class ViewportData:
class RepresentationsData:
data = {}
data: dict[str, Any] = {}
is_loaded = False
@classmethod
@@ -102,10 +103,12 @@ class RepresentationsData:
cls.is_loaded = True
@classmethod
def representations(cls):
results = []
def representations(cls) -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
obj = tool.Geometry.get_active_or_representation_obj()
assert obj
element = tool.Ifc.get_entity(obj)
assert element
active_representation_id = None
active_representation = tool.Geometry.get_active_representation(obj)
@@ -132,8 +135,8 @@ class RepresentationsData:
return results
@classmethod
def contexts(cls):
results = []
def contexts(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
results: list[tuple[str, str, str]] = []
for element in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
results.append((str(element.id()), element.ContextType or "Unnamed", ""))
for element in tool.Ifc.get().by_type("IfcGeometricRepresentationSubContext", include_subtypes=False):
@@ -219,9 +222,12 @@ class ConnectionsData:
cls.is_loaded = True
@classmethod
def connections(cls):
results = []
element = tool.Ifc.get_entity(bpy.context.active_object)
def connections(cls) -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
obj = bpy.context.active_object
assert obj
element = tool.Ifc.get_entity(obj)
assert element
connected_to = getattr(element, "ConnectedTo", [])
connected_from = getattr(element, "ConnectedFrom", [])
@@ -285,13 +291,16 @@ class ConnectionsData:
return results
@classmethod
def is_connection_realization(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
def is_connection_realization(cls) -> Union[list[dict[str, Any]], None]:
obj = bpy.context.active_object
assert obj
element = tool.Ifc.get_entity(obj)
assert element
connections = getattr(element, "IsConnectionRealization", None)
if not connections:
return
results = []
results: list[dict[str, Any]] = []
for rel in connections:
data = {
"realizing_elements_connection_type": rel.ConnectionType,
@@ -322,16 +331,19 @@ class DerivedCoordinatesData:
cls.is_loaded = True
@classmethod
def load_z_values(cls):
def load_z_values(cls) -> None:
cls.z_values = [
(bpy.context.active_object.matrix_world @ Vector(co))[2] for co in bpy.context.active_object.bound_box
]
@classmethod
def load_collection(cls):
def load_collection(cls) -> None:
cls.collection = None
cls.collection_z = 0
element = tool.Ifc.get_entity(bpy.context.active_object)
obj = bpy.context.active_object
if not obj:
return
element = tool.Ifc.get_entity(obj)
if not element:
return
parent = ifcopenshell.util.element.get_aggregate(element)
@@ -392,7 +404,7 @@ class PlacementData:
@classmethod
def load(cls):
cls.data = {"has_placement": cls.has_placement()}
cls.data: dict[str, Any] = {"has_placement": cls.has_placement()}
props = tool.Georeference.get_georeference_props()
obj = bpy.context.active_object
@@ -415,13 +427,14 @@ class PlacementData:
return False
@classmethod
def original_xyz(cls, obj):
def original_xyz(cls, obj: bpy.types.Object) -> list[float]:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
props = tool.Georeference.get_georeference_props()
translation = obj.matrix_world.translation
xyz = ifcopenshell.util.geolocation.xyz2enh(
obj.matrix_world[0][3],
obj.matrix_world[1][3],
obj.matrix_world[2][3],
translation[0],
translation[1],
translation[2],
float(props.blender_offset_x) * unit_scale,
float(props.blender_offset_y) * unit_scale,
float(props.blender_offset_z) * unit_scale,
File diff suppressed because it is too large Load Diff
+30 -10
View File
@@ -35,13 +35,13 @@ from bpy.props import (
from typing import Optional, TYPE_CHECKING, Union, Literal
def get_contexts(self, context):
def get_contexts(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
if not RepresentationsData.is_loaded:
RepresentationsData.load()
return RepresentationsData.data["contexts"]
def update_mode(self, context):
def update_mode(self: "BIMGeometryProperties", context: bpy.types.Context) -> None:
if self.is_changing_mode:
return
if context.mode.startswith("EDIT"):
@@ -62,7 +62,7 @@ def update_mode(self, context):
bpy.ops.bim.override_mode_set_edit("INVOKE_DEFAULT")
def update_representation_obj(self, context):
def update_representation_obj(self: "BIMGeometryProperties", context: bpy.types.Context) -> None:
for item_obj in self.item_objs:
if item_obj.obj:
data = item_obj.obj.data
@@ -76,13 +76,13 @@ def update_representation_obj(self, context):
self.is_changing_mode = False
def get_mode(self, context):
def get_mode(self: "BIMGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str, str, int]]:
if not ViewportData.is_loaded:
ViewportData.load()
return ViewportData.data["mode"]
def get_styles(self, context):
def get_styles(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
# postponed import to avoid circular import
from bonsai.bim.module.material.data import MaterialsData
@@ -91,13 +91,13 @@ def get_styles(self, context):
return MaterialsData.data["styles"]
def get_shape_aspects(self, context):
def get_shape_aspects(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
if not RepresentationsData.is_loaded:
RepresentationsData.load()
return RepresentationsData.data["shape_aspects"]
def get_material_constituents(self, context, edit_text):
def get_material_constituents(self: "ShapeAspect", context: bpy.types.Context, edit_text: str) -> list[str]:
from bonsai.bim.module.material.data import ObjectMaterialData
if not ObjectMaterialData.is_loaded:
@@ -105,7 +105,7 @@ def get_material_constituents(self, context, edit_text):
return ObjectMaterialData.data["active_material_constituents"]
def get_layers(self, context):
def get_layers(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
from bonsai.bim.module.layer.data import LayersData
if not LayersData.is_loaded:
@@ -113,7 +113,7 @@ def get_layers(self, context):
return LayersData.data["layers_enum"]
def get_layers_no_active(self, context):
def get_layers_no_active(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
from bonsai.bim.module.layer.data import LayersData
if not LayersData.is_loaded:
@@ -162,6 +162,11 @@ class RepresentationItemObject(PropertyGroup):
obj: PointerProperty(type=bpy.types.Object)
ifc_definition_id: IntProperty()
if TYPE_CHECKING:
name: str
obj: Union[bpy.types.Object, None]
ifc_definition_id: int
class ShapeAspect(PropertyGroup):
name: StringProperty(
@@ -283,7 +288,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)
@@ -297,6 +309,14 @@ class BIMGeometryProperties(PropertyGroup):
blender_item.name = name
return blender_item
def remove_item_object_by_entity(self, item: ifcopenshell.entity_instance) -> None:
ifc_id = item.id()
for i, item_obj in enumerate(self.item_objs):
if item_obj.ifc_definition_id == ifc_id:
self.item_objs.remove(i)
return
assert False
def is_object_valid_for_representation_copy(self, obj: bpy.types.Object) -> bool:
return bool(obj != bpy.context.active_object and obj.data)
+11 -27
View File
@@ -64,6 +64,7 @@ def object_menu(self, context):
self.layout.operator("bim.override_object_delete", icon="PLUGIN")
self.layout.operator("bim.override_paste_buffer", icon="PLUGIN")
self.layout.menu("BIM_MT_object_set_origin", icon="PLUGIN")
self.layout.menu("BIM_MT_separate", icon="PLUGIN")
def edit_mesh_menu(self, context):
@@ -76,25 +77,20 @@ class BIM_MT_separate(Menu):
bl_label = "IFC Separate"
def draw(self, context):
self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC Selection").type = "SELECTED"
self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC By Material").type = "MATERIAL"
self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC By Loose Parts").type = "LOOSE"
assert self.layout
self.layout.label(text="IFC Separate", icon_value=bonsai.bim.icons["IFC"].icon_id)
self.layout.operator_enum("bim.override_mesh_separate", "type")
# TODO: remove as it's the same as BIM_MT_separate?
class BIM_MT_hotkey_separate(Menu):
bl_idname = "BIM_MT_hotkey_separate"
bl_label = "Separate"
def draw(self, context):
assert self.layout
self.layout.label(text="IFC Separate", icon_value=bonsai.bim.icons["IFC"].icon_id)
self.layout.operator("bim.override_mesh_separate", text="Selection").type = "SELECTED"
self.layout.operator("bim.override_mesh_separate", text="By Material").type = "MATERIAL"
self.layout.operator("bim.override_mesh_separate", text="By Loose Parts").type = "LOOSE"
self.layout.separator()
self.layout.label(text="Blender Separate", icon="BLENDER")
self.layout.operator("mesh.separate", text="Selection").type = "SELECTED"
self.layout.operator("mesh.separate", text="By Material").type = "MATERIAL"
self.layout.operator("mesh.separate", text="By Loose Parts").type = "LOOSE"
self.layout.operator_enum("bim.override_mesh_separate", "type")
class BIM_MT_object_set_origin(Menu):
@@ -102,21 +98,9 @@ class BIM_MT_object_set_origin(Menu):
bl_label = "IFC Set Origin"
def draw(self, context):
self.layout.operator("bim.override_origin_set", icon="PLUGIN", text="IFC Geometry to Origin").origin_type = (
"GEOMETRY_ORIGIN"
)
self.layout.operator("bim.override_origin_set", icon="PLUGIN", text="IFC Origin to Geometry").origin_type = (
"ORIGIN_GEOMETRY"
)
self.layout.operator("bim.override_origin_set", icon="PLUGIN", text="IFC Origin to 3D Cursor").origin_type = (
"ORIGIN_CURSOR"
)
self.layout.operator(
"bim.override_origin_set", icon="PLUGIN", text="IFC Origin to Center of Mass (Surface)"
).origin_type = "ORIGIN_CENTER_OF_MASS"
self.layout.operator(
"bim.override_origin_set", icon="PLUGIN", text="IFC Origin to Center of Mass (Volume)"
).origin_type = "ORIGIN_CENTER_OF_VOLUME"
assert self.layout
self.layout.label(text="IFC Set Origin", icon_value=bonsai.bim.icons["IFC"].icon_id)
self.layout.operator_enum("bim.override_origin_set", property="origin_type")
def outliner_menu(self, context):
@@ -225,7 +209,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)
@@ -112,7 +112,9 @@ def update_blender_coordinates(self: "BIMGeoreferenceProperties", context: bpy.t
props.is_updating_coordinates = True
blender_coordinates = tool.Georeference.get_coordinates("blender")
local_coordinates = ifcopenshell.util.geolocation.xyz2enh(
*blender_coordinates,
blender_coordinates[0],
blender_coordinates[1],
blender_coordinates[2],
float(props.blender_offset_x),
float(props.blender_offset_y),
float(props.blender_offset_z),
@@ -38,6 +38,7 @@ classes = (
operator.RefreshGit,
operator.SwitchRevision,
operator.InstallGit,
operator.RunGitDiff,
prop.IfcGitTag,
prop.IfcGitListItem,
prop.IfcGitProperties,
@@ -409,3 +409,25 @@ class InstallGit(bpy.types.Operator):
core.install_git(tool.IfcGit, self)
refresh()
return {"FINISHED"}
class RunGitDiff(bpy.types.Operator):
"""Run `git diff` for the current version of IFC file and the last saved one."""
bl_label = "Git Diff"
bl_idname = "ifcgit.git_diff"
bl_options = set()
@classmethod
def poll(cls, context):
if not tool.Ifc.get():
cls.poll_message_set("No IFC file loaded.")
return False
if not tool.Ifc.get_path():
cls.poll_message_set("Current IFC file was never saved.")
return False
return True
def execute(self, context):
core.run_git_diff(tool.IfcGit, self)
return {"FINISHED"}
@@ -283,3 +283,5 @@ class IFCGIT_PT_revision_inspector(bpy.types.Panel):
"ifcgit.object_log",
icon="TEXT",
)
row = layout.row()
row.operator("ifcgit.git_diff")
@@ -392,6 +392,7 @@ ground_glow source ground
scene.add_surface(scene_path)
scene.add_source(sky_file_path)
print("Setting up view...")
assert isinstance(camera.data, bpy.types.Camera)
if camera.data.type == "PERSP":
# Perspective camera
camera_fov = camera.data.angle
@@ -547,9 +548,11 @@ class ViewFromSun(bpy.types.Operator):
def execute(self, context):
if not (camera := bpy.data.objects.get("SunPathCamera")):
camera = bpy.data.objects.new("SunPathCamera", bpy.data.cameras.new("SunPathCamera"))
assert isinstance(camera.data, bpy.types.Camera)
assert context.scene
camera.data.type = "ORTHO"
camera.data.ortho_scale = 100 # The default of 6m is too small
bpy.context.scene.collection.objects.link(camera)
context.scene.collection.objects.link(camera)
tool.Blender.activate_camera(camera)
props = context.scene.BIMSolarProperties
props.hour = props.hour # Just to refresh camera position
@@ -394,7 +394,7 @@ class ObjectMaterialData:
return [(m, m, ifcopenshell.util.doc.get_entity_doc(version, m).get("description", "")) for m in material_types]
@classmethod
def active_material_constituents(cls):
def active_material_constituents(cls) -> list[str]:
material = cls.material
if not cls.material or not material.is_a("IfcMaterialConstituentSet"):
return []
@@ -625,6 +625,15 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator):
wall.DumbWallPlaner().regenerate_from_layer_set(layer_set)
slab.DumbSlabPlaner().regenerate_from_layer_set(layer_set)
if material_set_usage.is_a("IfcMaterialProfileSetUsage"):
attributes["CardinalPoint"] = int(attributes["CardinalPoint"])
ifcopenshell.api.material.edit_profile_usage(
self.file,
usage=material_set_usage,
attributes=attributes,
)
model_profile.DumbProfileRecalculator().recalculate(objects)
bpy.ops.bim.disable_editing_assigned_material(obj=active_obj.name)
+34 -5
View File
@@ -27,6 +27,8 @@ import bonsai.core.misc as core
import bonsai.core.geometry as core_geometry
import bonsai.core.root
from mathutils import Vector, Matrix, Euler
from typing import TYPE_CHECKING, Literal, get_args
from typing_extensions import assert_never
class SetOverrideColour(bpy.types.Operator):
@@ -117,23 +119,45 @@ class ResizeToStorey(bpy.types.Operator, tool.Ifc.Operator):
core.resize_to_storey(tool.Misc, tool.Ifc, obj=obj, total_storeys=self.total_storeys)
SplitAlongEdgeMode = Literal["BOOLEAN", "BISECT"]
class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.split_along_edge"
bl_label = "Split Along Edge"
bl_description = (
"Active object is considered to be a cutting object."
"Split selected objects by the face of the cutter object.\n"
"Active object is considered to be a cutting object. "
"It can be a simple Blender object (e.g. a plane), not connected to IFC. "
"Will unassign element from a type if type has a representation."
)
bl_options = {"REGISTER", "UNDO"}
mode: bpy.props.StringProperty(default="BOOLEAN")
mode: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
default="BOOLEAN",
items=tuple((i, i, "") for i in get_args(SplitAlongEdgeMode)),
)
if TYPE_CHECKING:
mode: SplitAlongEdgeMode
@classmethod
def poll(cls, context):
return context.selected_objects
if not (obj := context.active_object) or obj.type != "MESH":
cls.poll_message_set("No active mesh object is selected.")
return False
if not context.selected_objects:
cls.poll_message_set("No objects selected")
return False
return True
def _execute(self, context):
cutter = context.active_object
objs = [o for o in context.selected_objects if o != cutter]
assert cutter
objs = [o for o in context.selected_objects if o != cutter and o.type == "MESH"]
if not objs:
self.report({"ERROR"}, "No other mesh objects selected besides the cutter object.")
return {"CANCELLED"}
if not tool.Ifc.get():
if self.mode == "BOOLEAN":
@@ -142,9 +166,11 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
tool.Misc.bisect_objects_with_cutter(objs, cutter)
for obj in objs:
bpy.data.objects.remove(obj)
else:
assert_never(self.mode)
return
objs_to_cut = []
objs_to_cut: list[bpy.types.Object] = []
# Splitting only works on meshes
for obj in objs:
# You cannot split meshes if the representation is mapped.
@@ -183,6 +209,8 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
new_objs = tool.Misc.boolean_objects_with_cutter(objs_to_cut, cutter)
elif self.mode == "BISECT":
new_objs = tool.Misc.bisect_objects_with_cutter(objs_to_cut, cutter)
else:
assert_never(self.mode)
for obj in new_objs:
bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=obj)
@@ -191,6 +219,7 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.update_representation(obj=obj.name)
representation = tool.Geometry.get_active_representation(obj)
assert representation
core_geometry.switch_representation(
tool.Ifc,
tool.Geometry,
@@ -40,6 +40,7 @@ from . import (
railing,
roof,
mep,
external,
)
from typing import NamedTuple
@@ -76,6 +77,7 @@ classes = (
wall.ExtendWallsToWall,
wall.FlipWall,
wall.MergeWall,
wall.OffsetWalls,
wall.RecalculateWall,
wall.SplitWall,
wall.UnjoinWalls,
@@ -138,6 +140,7 @@ classes = (
prop.BIMRoofProperties,
prop.BIMPolylineProperties,
prop.BIMProductPreviewProperties,
prop.BIMExternalParametricGeometryProperties,
ui.BIM_PT_array,
ui.BIM_PT_stair,
ui.BIM_PT_sverchok,
@@ -147,6 +150,7 @@ classes = (
ui.BIM_PT_roof,
ui.BIM_MT_type_manager_menu,
ui.BIM_MT_type_menu,
ui.BIM_PT_external_parametric_geometry,
ui.LaunchTypeMenu,
ui.LaunchTypeManager,
grid.BIM_OT_add_object,
@@ -198,6 +202,7 @@ classes = (
mep.MEPAddObstruction,
mep.MEPAddTransition,
mep.MEPAddBend,
external.ApplyExternalParametricGeometry,
)
addon_keymaps = []
@@ -255,6 +260,9 @@ def register():
bpy.types.Object.BIMDoorProperties = bpy.props.PointerProperty(type=prop.BIMDoorProperties)
bpy.types.Object.BIMRailingProperties = bpy.props.PointerProperty(type=prop.BIMRailingProperties)
bpy.types.Object.BIMRoofProperties = bpy.props.PointerProperty(type=prop.BIMRoofProperties)
bpy.types.Object.BIMExternalParametricGeometryProperties = bpy.props.PointerProperty(
type=prop.BIMExternalParametricGeometryProperties
)
bpy.types.VIEW3D_MT_add.prepend(ui.add_menu)
bpy.app.handlers.load_post.append(handler.load_post)
@@ -277,6 +285,7 @@ def unregister():
del bpy.types.Object.BIMDoorProperties
del bpy.types.Object.BIMRailingProperties
del bpy.types.Object.BIMRoofProperties
del bpy.types.Object.BIMExternalParametricGeometryProperties
bpy.app.handlers.load_post.remove(handler.load_post)
bpy.types.VIEW3D_MT_add.remove(ui.add_menu)
+22 -7
View File
@@ -20,6 +20,7 @@ import bpy
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.pset
import ifcopenshell.util.element
import ifcopenshell.util.unit
import bonsai.tool as tool
@@ -33,8 +34,23 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
assert (obj := context.active_object)
assert (element := tool.Ifc.get_entity(obj))
ifc_file = tool.Ifc.get()
allowed_types = (
"IfcElement",
"IfcAnnotation",
"IfcOpeningElement",
"IfcSpatialElement",
)
if not any(element.is_a(c) for c in allowed_types):
self.report(
{"ERROR"},
f"Adding array to element of type '{element.is_a()}' is not supported. Supported types: {','.join(allowed_types)}.",
)
return {"CANCELLED"}
array = {
"children": [],
@@ -54,14 +70,13 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator):
data.append(array)
pset = tool.Ifc.get().by_id(pset["id"])
else:
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Array")
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="BBIM_Array")
data = [array]
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
ifcopenshell.api.pset.edit_pset(
ifc_file,
pset=pset,
properties={"Parent": element.GlobalId, "Data": tool.Ifc.get().createIfcText(json.dumps(data))},
properties={"Parent": element.GlobalId, "Data": ifc_file.create_entity("IfcText", json.dumps(data))},
)
return {"FINISHED"}
+21 -11
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import blf
import bpy
@@ -34,6 +35,7 @@ from gpu_extras.presets import draw_circle_2d
from typing import Union
from bonsai.bim.module.drawing.helper import format_distance
from itertools import chain
from typing import Union, Any
def transparent_color(color, alpha=0.1):
@@ -348,21 +350,28 @@ class PolylineDecorator:
cls.is_installed = False
@classmethod
def update(cls, event, tool_state, input_ui, snapping_point):
def update(
cls,
event: bpy.types.Event,
tool_state: tool.Polyline.ToolState,
input_ui: tool.Polyline.PolylineUI,
snapping_point: Vector,
) -> None:
cls.event = event
cls.tool_state = tool_state
cls.input_ui = input_ui
# TODO: unused?
@classmethod
def set_input_ui(cls, input_ui):
def set_input_ui(cls, input_ui: tool.Polyline.PolylineUI) -> None:
cls.input_ui = input_ui
@classmethod
def set_angle_axis_line(cls, start, end):
def set_angle_axis_line(cls, start: Vector, end: Vector) -> None:
cls.axis_start = start
cls.axis_end = end
def calculate_measurement_x_y_and_z(self, context):
def calculate_measurement_x_y_and_z(self, context: bpy.types.Context) -> None:
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
@@ -385,7 +394,7 @@ class PolylineDecorator:
return (x_axis, y_axis, z_axis), (x_middle, y_middle, z_middle)
@classmethod
def calculate_polygon(self, points):
def calculate_polygon(cls, points: list[Vector]) -> dict[str, Any]:
bm = bmesh.new()
new_verts = [bm.verts.new(v) for v in points]
@@ -414,7 +423,7 @@ class PolylineDecorator:
shader.uniform_float("color", color)
batch.draw(shader)
def draw_input_ui(self, context):
def draw_input_ui(self, context: bpy.types.Context) -> None:
texts = {
"D": "Distance: ",
"A": "Angle: ",
@@ -424,6 +433,7 @@ class PolylineDecorator:
"AREA": "Area:",
}
try:
assert self.event
mouse_pos = self.event.mouse_region_x, self.event.mouse_region_y
except:
mouse_pos = (None, None)
@@ -538,7 +548,7 @@ class PolylineDecorator:
polyline_verts = [Vector((p.x, p.y, p.z)) for p in polyline_points]
# Area
if measure_type == "AREA" and polyline_data.area:
if measure_type == "POLY_AREA" and polyline_data.area:
if len(polyline_verts) < 3:
return
center = sum(polyline_verts, Vector()) / len(polyline_verts) # Center between all polyline points
@@ -556,7 +566,7 @@ class PolylineDecorator:
blf.draw(self.font_id, text)
# Length
if measure_type in {"POLYLINE", "AREA"}:
if measure_type in {"POLYLINE", "POLY_AREA"}:
if len(polyline_verts) < 3:
return
total_length_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, polyline_verts[-1])
@@ -726,8 +736,8 @@ class PolylineDecorator:
polyline_points = polyline_data.polyline_points
else:
polyline_points = []
polyline_verts = []
polyline_edges = []
polyline_verts: list[Vector] = []
polyline_edges: list[list[int]] = []
for point_prop in polyline_points:
point = Vector((point_prop.x, point_prop.y, point_prop.z))
polyline_verts.append(point)
@@ -765,7 +775,7 @@ class PolylineDecorator:
# Area highlight
if polyline_data:
area = polyline_data.area.split(" ")[0]
if area:
if polyline_data.measurement_type == "POLY_AREA" and area:
if float(area) > 0:
tris = self.calculate_polygon(polyline_verts)["tris"]
self.draw_batch("TRIS", polyline_verts, transparent_color(decorator_color_special), tris)
@@ -616,6 +616,9 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
props.is_editing = False
update_door_modifier_representation(obj)
element_type = ifcopenshell.util.element.get_type(element)
if element_type:
tool.Model.mark_thumbnail_for_update(element_type)
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
door_data = tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))
@@ -0,0 +1,40 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021, 2022 Dion Moult <dion@thinkmoult.com>, @Andrej730
#
# 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
import bmesh
import ifcopenshell
import bonsai.tool as tool
class ApplyExternalParametricGeometry(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.apply_external_parametric_geometry"
bl_label = "Apply External Parametric Geometry"
bl_description = "Apply external parametric geometry to the active object."
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
assert obj
assert (active_representation := tool.Geometry.get_active_representation(obj))
ifc_context = active_representation.ContextOfItems
tool.Model.add_representation(obj, ifc_context)
props = tool.Model.get_epg_props(obj)
props.is_editing = False
return {"FINISHED"}
+21 -9
View File
@@ -18,14 +18,18 @@
import bpy
import ifcopenshell.api
import ifcopenshell.api.grid
import bonsai.tool as tool
import bonsai.core.geometry
import bonsai.core.root
from bpy.types import Operator
from bpy.props import FloatProperty, IntProperty
from mathutils import Vector
from typing import TYPE_CHECKING
def add_object(self, context):
def add_object(self: "BIM_OT_add_object", context: bpy.types.Context) -> None:
ifc_file = tool.Ifc.get()
obj = bpy.data.objects.new("Grid", None)
obj.name = "Grid"
@@ -33,6 +37,11 @@ def add_object(self, context):
tool.Ifc, tool.Collector, tool.Root, obj=obj, ifc_class="IfcGrid", should_add_representation=False
)
grid = tool.Ifc.get_entity(obj)
assert grid
# Requirement in IFC4+.
if tool.Ifc.get_schema() != "IFC2X3":
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj)
for i in range(0, self.total_u):
verts = [
@@ -46,9 +55,7 @@ def add_object(self, context):
tag = chr(ord("A") + i)
obj = bpy.data.objects.new(f"IfcGridAxis/{tag}", mesh)
result = ifcopenshell.api.run(
"grid.create_grid_axis", tool.Ifc.get(), axis_tag=tag, uvw_axes="UAxes", grid=grid
)
result = ifcopenshell.api.grid.create_grid_axis(ifc_file, axis_tag=tag, uvw_axes="UAxes", grid=grid)
tool.Ifc.link(result, obj)
tool.Model.create_axis_curve(obj, result)
tool.Collector.assign(obj)
@@ -65,9 +72,7 @@ def add_object(self, context):
tag = str(i + 1).zfill(2)
obj = bpy.data.objects.new(f"IfcGridAxis/{tag}", mesh)
result = ifcopenshell.api.run(
"grid.create_grid_axis", tool.Ifc.get(), axis_tag=tag, uvw_axes="VAxes", grid=grid
)
result = ifcopenshell.api.grid.create_grid_axis(ifc_file, axis_tag=tag, uvw_axes="VAxes", grid=grid)
tool.Ifc.link(result, obj)
tool.Model.create_axis_curve(obj, result)
tool.Collector.assign(obj)
@@ -78,13 +83,20 @@ def add_object(self, context):
class BIM_OT_add_object(Operator, tool.Ifc.Operator):
bl_idname = "mesh.add_grid"
bl_label = "Grid"
bl_description = "Add IfcGrid."
bl_options = {"REGISTER", "UNDO"}
u_spacing: FloatProperty(name="U Spacing", default=10)
u_spacing: FloatProperty(name="U Spacing", default=10, subtype="DISTANCE")
total_u: IntProperty(name="Number of U Grids", default=3)
v_spacing: FloatProperty(name="V Spacing", default=10)
v_spacing: FloatProperty(name="V Spacing", default=10, subtype="DISTANCE")
total_v: IntProperty(name="Number of V Grids", default=3)
if TYPE_CHECKING:
u_spacing: float
total_u: int
v_spacing: float
total_v: int
@classmethod
def poll(cls, context):
return tool.Ifc.get() and context.mode == "OBJECT"
@@ -36,12 +36,6 @@ def load_post(*args):
"sequence.edit_task_time", "Bonsai.Task.CalculateQuantities", task.calculate_quantities
)
ifcopenshell.api.add_post_listener(
"material.edit_profile_usage",
"Bonsai.Product.RegenerateProfileUsage",
product.regenerate_profile_usage,
)
ifcopenshell.api.add_post_listener(
"type.assign_type", "Bonsai.DumbWall.RegenerateFromType", wall.DumbWallPlaner().regenerate_from_type
)
+12 -20
View File
@@ -484,32 +484,21 @@ class AddBoolean(Operator, tool.Ifc.Operator):
class ShowOpenings(Operator, tool.Ifc.Operator):
bl_idname = "bim.show_openings"
bl_label = "Show"
bl_description = "Show Openings"
bl_label = "Show Openings"
bl_description = "Show openings for selected objects"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
objs = list(context.selected_objects)
# If several parts of an aggregation are selected, the aggregate openings may be queried more than once
objects_element_map = set()
while objs:
obj = objs.pop(0)
for obj in tool.Blender.get_selected_objects():
element = tool.Ifc.get_entity(obj)
if element:
if getattr(element, "Decomposes", None):
# Select aggregate recursively
if element.Decomposes and (aggregate := element.Decomposes[0].RelatingObject):
aggregate_obj = tool.Ifc.get_object(aggregate)
objs.append(aggregate_obj)
objects_element_map.add((obj, element))
for obj, element in objects_element_map:
if element is None:
continue
self.show_object_openings(obj, element)
DecorationsHandler.install(bpy.context)
bpy.ops.bim.update_openings_focus()
return {"FINISHED"}
def show_object_openings(self, obj, element):
def show_object_openings(self, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
openings_elements = [rel.RelatedOpeningElement for rel in tool.Geometry.get_openings(element)]
if not openings_elements:
return
@@ -531,15 +520,18 @@ class UpdateOpeningsFocus(Operator):
preferences = tool.Blender.get_addon_preferences()
if preferences.opening_focus_opacity == 100:
return {"FINISHED"}
openings = set()
building_objects = set()
openings: set[bpy.types.Object] = set()
building_objects: set[bpy.types.Object] = set()
props = tool.Model.get_model_props()
for opening in props.openings:
if opening.obj:
openings.add(opening.obj)
opening_element = tool.Ifc.get_entity(opening.obj)
assert opening_element
building_element = opening_element.VoidsElements[0].RelatingBuildingElement
building_objects.add(tool.Ifc.get_object(building_element))
building_obj = tool.Ifc.get_object(building_element)
assert isinstance(building_obj, bpy.types.Object)
building_objects.add(building_obj)
for obj in context.scene.objects:
obj.color = [
+24 -3
View File
@@ -550,6 +550,8 @@ class PolylineOperator:
# TODO Fill doc strings
""" """
objs_2d_bbox: list[tuple[bpy.types.Object, list[float]]]
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return context.space_data.type == "VIEW_3D"
@@ -597,6 +599,7 @@ class PolylineOperator:
"Increment Angle": {"icons": True, "keys": ["EVENT_SHIFT", "MOUSE_MMB_SCROLL"]},
"Modify Snap Point": {"icons": True, "keys": ["EVENT_M"]},
"Close Polyline": {"icons": True, "keys": ["EVENT_C"]},
"Offset": {"icons": True, "keys": ["EVENT_O"]},
"Remove Point": {"icons": True, "keys": ["EVENT_BACKSPACE"]},
}
@@ -640,12 +643,22 @@ class PolylineOperator:
tool.Blender.update_viewport()
def choose_plane(self, event: bpy.types.Event, x: bool = True, y: bool = True, z: bool = True) -> None:
def get_plane_origin():
polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
if len(polyline_points) > 0:
reference_point = polyline_points[-1]
else:
reference_point = Vector((0.0, 0.0, 0.0))
self.tool_state.plane_origin = Vector((reference_point.x, reference_point.y, reference_point.z))
if x:
if event.shift and event.value == "PRESS" and event.type == "X":
self.tool_state.use_default_container = False
self.tool_state.plane_method = "YZ" if self.tool_state.plane_method != "YZ" else None
self.tool_state.axis_method = None
tool.Blender.update_viewport()
get_plane_origin()
if y:
if event.shift and event.value == "PRESS" and event.type == "Y":
@@ -653,6 +666,7 @@ class PolylineOperator:
self.tool_state.plane_method = "XZ" if self.tool_state.plane_method != "XZ" else None
self.tool_state.axis_method = None
tool.Blender.update_viewport()
get_plane_origin()
if z:
if event.shift and event.value == "PRESS" and event.type == "Z":
@@ -660,6 +674,7 @@ class PolylineOperator:
self.tool_state.plane_method = "XY" if self.tool_state.plane_method != "XY" else None
self.tool_state.axis_method = None
tool.Blender.update_viewport()
get_plane_origin()
def handle_instructions(
self, context: bpy.types.Context, custom_instructions: dict = {}, custom_info: str = "", overwrite: bool = False
@@ -680,7 +695,10 @@ class PolylineOperator:
for action, settings in instructions.items():
if settings["icons"]:
for key in settings["keys"]:
self.layout.label(text="", icon=key)
if bpy.app.version < (4, 3, 0) and key == "MOUSE_MMB_SCROLL":
self.layout.label(text="MMB")
else:
self.layout.label(text="", icon=key)
self.layout.label(text=action)
else:
key = settings["keys"][0]
@@ -874,6 +892,7 @@ class PolylineOperator:
else:
if event.value == "RELEASE" and event.type in {"ESC"}:
self.tool_state.axis_method = None
self.tool_state.plane_method = None
context.workspace.status_text_set(text=None)
PolylineDecorator.uninstall()
tool.Polyline.clear_polyline()
@@ -899,7 +918,8 @@ class PolylineOperator:
if self.mousemove_count == 2:
self.objs_2d_bbox = []
for obj in self.visible_objs:
self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj))
if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj):
self.objs_2d_bbox.append(bbox_2d)
if self.mousemove_count > 3:
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state)
@@ -988,7 +1008,8 @@ class PolylineOperator:
self.tool_state.mode = "Mouse"
self.visible_objs = tool.Raycast.get_visible_objects(context)
for obj in self.visible_objs:
self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj))
if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj):
self.objs_2d_bbox.append(bbox_2d)
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state)
self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps)
tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state)
+6 -44
View File
@@ -569,7 +569,7 @@ class AlignProduct(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
AlignType = Literal["CENTERLINE", "POSITIVE", "NEGATIVE"]
align_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
align_type: bpy.props.EnumProperty( # pyright: ignore [reportRedeclaration]
items=[(i, i, "") for i in get_args(AlignType)]
)
@@ -577,49 +577,12 @@ class AlignProduct(bpy.types.Operator):
align_type: AlignType
def execute(self, context):
selected_objs = context.selected_objects
if len(selected_objs) < 2 or not context.active_object:
self.report({"ERROR"}, "Please select atleast 2 objects.")
return {"FINISHED"}
if self.align_type == "CENTERLINE":
point = context.active_object.matrix_world @ (
Vector(context.active_object.bound_box[0]) + (context.active_object.dimensions / 2)
)
elif self.align_type == "POSITIVE":
point = context.active_object.matrix_world @ Vector(context.active_object.bound_box[6])
elif self.align_type == "NEGATIVE":
point = context.active_object.matrix_world @ Vector(context.active_object.bound_box[0])
else:
assert_never(self.align_type)
active_x_axis = context.active_object.matrix_world.to_quaternion() @ Vector((1, 0, 0))
active_y_axis = context.active_object.matrix_world.to_quaternion() @ Vector((0, 1, 0))
active_z_axis = context.active_object.matrix_world.to_quaternion() @ Vector((0, 0, 1))
x_distances = self.get_axis_distances(point, active_x_axis, context)
y_distances = self.get_axis_distances(point, active_y_axis, context)
if abs(sum(x_distances)) < abs(sum(y_distances)):
for i, obj in enumerate(selected_objs):
obj.matrix_world = Matrix.Translation(active_x_axis * -x_distances[i]) @ obj.matrix_world
else:
for i, obj in enumerate(selected_objs):
obj.matrix_world = Matrix.Translation(active_y_axis * -y_distances[i]) @ obj.matrix_world
try:
core.align_objects(tool.Blender, tool.Model, self.align_type)
except core.RequireAtLeastTwoElements as e:
self.report({"ERROR"}, str(e))
return {"FINISHED"}
def get_axis_distances(self, point: Vector, axis: Vector, context: bpy.types.Context) -> list[float]:
results = []
for obj in context.selected_objects:
if self.align_type == "CENTERLINE":
obj_point = obj.matrix_world @ (Vector(obj.bound_box[0]) + (obj.dimensions / 2))
elif self.align_type == "POSITIVE":
obj_point = obj.matrix_world @ Vector(obj.bound_box[6])
elif self.align_type == "NEGATIVE":
obj_point = obj.matrix_world @ Vector(obj.bound_box[0])
else:
assert_never(self.align_type)
results.append(mathutils.geometry.distance_point_to_plane(obj_point, point, axis))
return results
class LoadTypeThumbnails(bpy.types.Operator):
bl_idname = "bim.load_type_thumbnails"
@@ -645,8 +608,7 @@ class LoadTypeThumbnails(bpy.types.Operator):
while queue:
# if bpy.app.is_job_running("RENDER_PREVIEW") does not seem to reflect asset preview generation
element = queue.pop()
if tool.Model.update_thumbnail_for_element(element):
queue.append(element)
tool.Model.mark_thumbnail_for_update(element)
return {"FINISHED"}
@@ -36,7 +36,6 @@ import bonsai.core.root
from bonsai.bim.ifc import IfcStore
from math import pi, degrees, atan2
from mathutils import Vector, Matrix
from bonsai.bim.module.model.wall import DumbWallRecalculator
from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator
from bonsai.bim.module.model.polyline import PolylineOperator
from typing import Union, Any, Optional
@@ -213,7 +212,7 @@ class DumbProfileRegenerator:
# update related thumbnails
for element in self.get_element_types_using_profile(profile):
tool.Model.update_thumbnail_for_element(element, refresh=True)
tool.Model.mark_thumbnail_for_update(element)
def regenerate_from_profile(self, usecase_path, ifc_file, settings):
self.file = ifc_file
@@ -969,7 +968,7 @@ class Rotate90(bpy.types.Operator, tool.Ifc.Operator):
obj.matrix_world @= rotate_matrix
bpy.context.view_layer.update()
DumbProfileRecalculator().recalculate(profile_objs)
DumbWallRecalculator().recalculate(layer2_objs)
tool.Model.recalculate_walls(layer2_objs)
return {"FINISHED"}
@@ -1197,6 +1196,7 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato
):
self.create_profiles_from_polyline(context)
context.workspace.status_text_set(text=None)
self.tool_state.plane_method = None
ProductDecorator.uninstall()
PolylineDecorator.uninstall()
tool.Polyline.clear_polyline()
+42 -2
View File
@@ -221,13 +221,13 @@ class BIMModelProperties(PropertyGroup):
items=[("EXTERIOR", "Exterior", ""), ("CENTER", "Center", ""), ("INTERIOR", "Interior", "")],
name="Vertical Layer Offset Type",
default="EXTERIOR",
description="It's a convention that affects the offset to reference line",
description="Offset convention to reference line",
)
offset_type_horizontal: bpy.props.EnumProperty(
items=[("TOP", "Top", ""), ("CENTER", "Center", ""), ("BOTTOM", "Bottom", "")],
name="Horizontal Layer Offset Type",
default="TOP",
description="It's a convention that affects the offset to reference line",
description="Offset convention to reference line",
)
offset: bpy.props.FloatProperty(name="Offset", default=0.0, description="Material usage offset from reference line")
show_wall_axis: bpy.props.BoolProperty(
@@ -1142,3 +1142,43 @@ class BIMProductPreviewProperties(PropertyGroup):
verts: bpy.props.CollectionProperty(type=ProductPreviewItem)
edges: bpy.props.CollectionProperty(type=ProductPreviewItem)
tris: bpy.props.CollectionProperty(type=ProductPreviewItem)
def update_is_editing(self: "BIMExternalParametricGeometryProperties", context: bpy.types.Context) -> None:
if self.is_editing:
return
tool.Model.clean_up_parametric_geometry(self.id_data)
del self["is_editing"]
del self["geo_nodes"]
def update_geo_nodes(self: "BIMExternalParametricGeometryProperties", context: bpy.types.Context) -> None:
if self.geo_nodes:
tool.Model.setup_parametric_geometry(self.id_data)
return
modifier = tool.Model.get_epg_modifier(self.id_data)
assert modifier
modifier.show_viewport = False
del self["geo_nodes"]
class BIMExternalParametricGeometryProperties(bpy.types.PropertyGroup):
is_editing: bpy.props.BoolProperty(
name="Is Editing Paramteric Geometry",
description="Toggle editing parametric geometry.",
default=False,
update=update_is_editing,
)
geo_nodes: bpy.props.PointerProperty(
name="Geometry Nodes",
description="Geometry nodes tree to use as a source for representation.",
type=bpy.types.GeometryNodeTree,
update=update_geo_nodes,
poll=lambda self, node_tree: not node_tree.name.startswith("BBIM_EPG"),
)
if TYPE_CHECKING:
is_editing: bool
geo_nodes: Union[bpy.types.GeometryNodeTree, None]
+7 -6
View File
@@ -36,7 +36,6 @@ from math import cos, pi
from mathutils import Vector, Matrix
from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator
from bonsai.bim.module.model.polyline import PolylineOperator
from bonsai.bim.module.model.wall import DumbWallRecalculator
from typing import Optional
@@ -677,8 +676,9 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
# Restore the position to before it was changed by the offset and x_angle
rot_matrix = Matrix.Rotation(existing_x_angle, 4, "X")
offset = Vector((0.0, 0.0, -layer_params["offset"]))
rot_offset = offset @ rot_matrix
perpendicular_offset = layer_params["offset"] * abs(1 / cos(existing_x_angle))
offset_vector = Vector((0.0, 0.0, -perpendicular_offset))
rot_offset = offset_vector @ rot_matrix
tranlation_matrix = Matrix.Translation(rot_offset)
position = position @ tranlation_matrix
@@ -725,8 +725,9 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
# Restore the position to after it was changed by the offset and x_angle
rot_matrix = Matrix.Rotation(existing_x_angle, 4, "X")
offset = Vector((0.0, 0.0, -layer_params["offset"]))
rot_offset = offset @ rot_matrix
perpendicular_offset = layer_params["offset"] * abs(1 / cos(existing_x_angle))
offset_vector = Vector((0.0, 0.0, -perpendicular_offset))
rot_offset = offset_vector @ rot_matrix
tranlation_matrix = Matrix.Translation(rot_offset)
position = position @ tranlation_matrix
@@ -1033,5 +1034,5 @@ class RecalculateSlab(bpy.types.Operator, tool.Ifc.Operator):
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement.is_a("IfcWall"):
walls.append(tool.Ifc.get_object(rel.RelatedElement))
DumbWallRecalculator().recalculate(walls)
tool.Model.recalculate_walls(walls)
return {"FINISHED"}
+43
View File
@@ -701,6 +701,49 @@ class BIM_PT_roof(bpy.types.Panel):
row.operator("bim.add_roof", icon="ADD", text="")
class BIM_PT_external_parametric_geometry(bpy.types.Panel):
bl_label = "External Parametric Geometry"
bl_idname = "BIM_PT_external_parametric_geometry"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_options = {"DEFAULT_CLOSED"}
bl_parent_id = "BIM_PT_tab_parametric_geometry"
@classmethod
def poll(cls, context):
return (obj := context.active_object) and obj.type == "MESH"
def draw(self, context):
obj = context.active_object
assert obj
layout = self.layout
assert layout
props = tool.Model.get_epg_props(obj)
row = layout.row(align=True)
row.alignment = "RIGHT"
if not props.is_editing:
row.prop(props, "is_editing", icon="GREASEPENCIL", text="")
return
row.operator("bim.apply_external_parametric_geometry", icon="CHECKMARK", text="")
row.prop(props, "is_editing", icon="CANCEL", text="")
row = layout.row(align=True)
row.prop_search(props, "geo_nodes", bpy.data, "node_groups")
if props.geo_nodes:
assert (modifier := tool.Model.get_epg_modifier(obj))
inputs = tool.Model.get_parametric_geometry_inputs(modifier)
# NOTE: users won't be able to see inputs descriptions.
# If we add group node inputs as modifiers inputs, descriptions will be visible.
# But then we need to ensure inputs are up to date
# (e.g. probably just by adding a refresh button).
for input in inputs:
row = layout.row(align=True)
row.prop(input, "default_value", text=input.name)
def add_menu(self: bpy.types.Menu, context: bpy.types.Context) -> None:
self.layout.operator_context = "INVOKE_REGION_WIN"
self.layout.operator("bim.add_element", icon_value=bonsai.bim.icons["IFC"].icon_id, text="IFC Element")
+153 -142
View File
@@ -24,10 +24,12 @@ import math
import numpy as np
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.geometry
import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.shape_builder
import ifcopenshell.util.type
import mathutils.geometry
import bonsai.core.type
@@ -52,7 +54,10 @@ class UnjoinWalls(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
return context.selected_objects
if not tool.Model.has_selected_ifc_objects():
cls.poll_message_set("No IFC objects selected.")
return False
return True
def _execute(self, context):
core.unjoin_walls(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model)
@@ -66,7 +71,7 @@ class ExtendWallsToUnderside(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
slab = None
walls = []
walls: list[bpy.types.Object] = []
if (obj := tool.Blender.get_active_object(is_selected=True)) and (element := tool.Ifc.get_entity(obj)):
slab = obj
for obj in tool.Blender.get_selected_objects(include_active=False):
@@ -112,8 +117,8 @@ class ExtendWallsToWall(bpy.types.Operator, tool.Ifc.Operator):
ifcopenshell.api.geometry.connect_wall(
tool.Ifc.get(), wall1=element, wall2=target_element, is_atpath=True
)
joiner.recreate_wall(element, obj)
joiner.recreate_wall(target_element, target_obj)
tool.Model.recreate_wall(element, obj)
tool.Model.recreate_wall(target_element, target_obj)
else:
self.report({"ERROR"}, "Please select at least one LAYER2 element and one active LAYER2 element")
@@ -124,36 +129,22 @@ class AlignWall(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = """ Align the selected walls to the active wall:
'Ext.': align to the EXTERIOR face
'C/L': align to wall CENTERLINE
'C/L': align to wall CENTER
'Int.': align to the INTERIOR face"""
AlignType = Literal["CENTERLINE", "EXTERIOR", "INTERIOR"]
align_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
AlignType = Literal["CENTER", "EXTERIOR", "INTERIOR"]
align_type: bpy.props.EnumProperty( # pyright: ignore [reportRedeclaration]
items=((i, i, "") for i in get_args(AlignType))
)
if TYPE_CHECKING:
align_type: AlignType
@classmethod
def poll(cls, context):
selected_valid_objects = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
return context.active_object and len(selected_valid_objects) > 1
def execute(self, context):
selected_objects = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
for obj in selected_objects:
if obj == context.active_object:
continue
aligner = DumbWallAligner(obj, context.active_object)
if self.align_type == "CENTERLINE":
aligner.align_centerline()
elif self.align_type == "EXTERIOR":
aligner.align_first_layer()
elif self.align_type == "INTERIOR":
aligner.align_last_layer()
else:
assert_never(self.align_type)
try:
core.align_walls(tool.Ifc, tool.Blender, tool.Model, DumbWallAligner(), self.align_type)
except core.RequireAtLeastTwoLayeredElements as e:
self.report({"ERROR"}, str(e))
return {"FINISHED"}
@@ -165,10 +156,13 @@ class FlipWall(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
return context.selected_objects
if not tool.Model.has_selected_ifc_objects():
cls.poll_message_set("No IFC objects selected.")
return False
return True
def _execute(self, context):
selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
selected_objs = tool.Model.get_selected_mesh_objects()
joiner = DumbWallJoiner()
for obj in selected_objs:
joiner.flip(obj)
@@ -185,10 +179,13 @@ class SplitWall(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
return context.selected_objects
if not tool.Model.has_selected_ifc_objects():
cls.poll_message_set("No IFC objects selected.")
return False
return True
def _execute(self, context):
selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
selected_objs = tool.Model.get_selected_mesh_objects()
for obj in selected_objs:
DumbWallJoiner().split(obj, context.scene.cursor.location)
return {"FINISHED"}
@@ -202,12 +199,23 @@ class MergeWall(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
return context.selected_objects
if not context.active_object:
cls.poll_message_set("No active object selected.")
return False
elif not tool.Model.has_selected_ifc_objects():
cls.poll_message_set("No mesh IFC objects selected.")
return False
mesh_objects = [o for o in tool.Model.get_selected_ifc_objects() if o.type == "MESH"]
if len(mesh_objects) != 2:
cls.poll_message_set("Please select exactly two mesh IFC objects.")
return False
return True
def _execute(self, context):
selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
if len(selected_objs) == 2:
DumbWallJoiner().merge([o for o in selected_objs if o != context.active_object][0], context.active_object)
active_obj = context.active_object
assert active_obj
selected_objs = tool.Model.get_selected_mesh_objects()
DumbWallJoiner().merge(next(o for o in selected_objs if o != active_obj), active_obj)
return {"FINISHED"}
@@ -218,82 +226,94 @@ class RecalculateWall(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
return context.selected_objects
if not tool.Model.has_selected_mesh_ifc_objects():
cls.poll_message_set("No mesh IFC objects selected.")
return False
return True
def _execute(self, context):
DumbWallRecalculator().recalculate(context.selected_objects)
objects = tool.Model.get_selected_mesh_ifc_objects()
tool.Model.recalculate_walls(objects)
return {"FINISHED"}
class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.change_extrusion_depth"
bl_label = "Update"
bl_description = "Update Height"
bl_description = "Update height for the selected objects."
bl_options = {"REGISTER", "UNDO"}
depth: bpy.props.FloatProperty()
@classmethod
def poll(cls, context):
return context.selected_objects
if not tool.Model.has_selected_mesh_ifc_objects():
cls.poll_message_set("No mesh IFC objects selected.")
return False
return True
def _execute(self, context):
layer2_objs = []
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
for obj in context.selected_objects:
layer2_objs: list[bpy.types.Object] = []
ifc_file = tool.Ifc.get()
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
selected_objs = tool.Model.get_selected_mesh_ifc_objects()
for obj in selected_objs:
element = tool.Ifc.get_entity(obj)
if not element:
return
assert element
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
return
continue
extrusion = tool.Model.get_extrusion(representation)
if not extrusion:
return
continue
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
extrusion.Depth = self.depth / si_conversion * (1 / cos(x_angle))
if tool.Model.get_usage_type(element) == "LAYER2":
for rel in element.ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements":
ifcopenshell.api.run(
"geometry.disconnect_element",
tool.Ifc.get(),
ifcopenshell.api.geometry.disconnect_element(
ifc_file,
relating_element=rel.RelatingElement,
related_element=element,
)
layer2_objs.append(obj)
if layer2_objs:
DumbWallRecalculator().recalculate(layer2_objs)
tool.Model.recalculate_walls(layer2_objs)
return {"FINISHED"}
class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.change_extrusion_x_angle"
bl_label = "Update"
bl_description = "Update Angle"
bl_description = "Update angle for the selected objects."
bl_options = {"REGISTER", "UNDO"}
x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE")
@classmethod
def poll(cls, context):
return context.selected_objects
if not tool.Model.has_selected_mesh_ifc_objects():
cls.poll_message_set("No mesh IFC objects selected.")
return False
return True
def _execute(self, context):
layer2_objs = []
other_objs = []
layer2_objs: list[bpy.types.Object] = []
x_angle = 0 if tool.Cad.is_x(self.x_angle, 0, tolerance=0.001) else self.x_angle
x_angle = 0 if tool.Cad.is_x(self.x_angle, pi, tolerance=0.001) else self.x_angle
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
for obj in context.selected_objects:
selected_objs = tool.Model.get_selected_mesh_ifc_objects()
for obj in selected_objs:
element = tool.Ifc.get_entity(obj)
if not element:
return
assert element
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
return
continue
extrusion = tool.Model.get_extrusion(representation)
if not extrusion:
return
continue
existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle
@@ -373,26 +393,47 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
obj.rotation_euler.z = current_z_rot
if layer2_objs:
DumbWallRecalculator().recalculate(layer2_objs)
tool.Model.recalculate_walls(layer2_objs)
return {"FINISHED"}
class ChangeLayerLength(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.change_layer_length"
bl_label = "Update"
bl_description = "Update Length"
bl_description = "Update length for the selected objects."
bl_options = {"REGISTER", "UNDO"}
length: bpy.props.FloatProperty()
@classmethod
def poll(cls, context):
return context.selected_objects
if not tool.Model.has_selected_mesh_ifc_objects():
cls.poll_message_set("No mesh IFC objects selected.")
return False
return True
def _execute(self, context):
joiner = DumbWallJoiner()
for obj in context.selected_objects:
selected_objs = tool.Model.get_selected_mesh_ifc_objects()
for obj in selected_objs:
joiner.set_length(obj, self.length)
return {"FINISHED"}
class OffsetWalls(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.offset_walls"
bl_label = "Offset Walls"
bl_description = "Offset selected objects from their reference line."
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
if not tool.Model.has_selected_mesh_ifc_objects():
cls.poll_message_set("No mesh IFC objects selected.")
return False
return True
def _execute(self, context):
props = tool.Model.get_model_props()
core.offset_walls(tool.Ifc, tool.Blender, tool.Model, props.offset_type_vertical)
class AddWallsFromSlab(bpy.types.Operator, tool.Ifc.Operator):
@@ -463,12 +504,8 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
material_set_usage = model.by_id(material.id())
# if material.is_a("IfcMaterialLayerSetUsage"):
attributes = {"OffsetFromReferenceLine": offset, "DirectionSense": direction_sense}
ifcopenshell.api.run(
"material.edit_layer_usage",
model,
**{"usage": material_set_usage, "attributes": attributes},
)
DumbWallRecalculator().recalculate([wall["obj"]])
ifcopenshell.api.run("material.edit_layer_usage", model, usage=material_set_usage, attributes=attributes)
tool.Model.recalculate_walls([wall["obj"]])
if walls:
if is_polyline_closed:
@@ -534,6 +571,7 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
):
self.create_walls_from_polyline(context)
context.workspace.status_text_set(text=None)
self.tool_state.plane_method = None
ProductDecorator.uninstall()
PolylineDecorator.uninstall()
tool.Polyline.clear_polyline()
@@ -569,11 +607,11 @@ class DumbWallAligner:
# An alignment shifts the origin of all walls to the closest point on the
# local X axis of the reference wall. In addition, the Z rotation is copied.
# Z translations are ignored for alignment.
def __init__(self, wall: bpy.types.Object, reference_wall: bpy.types.Object):
self.wall = wall
def set_reference_wall(self, reference_wall: bpy.types.Object):
self.reference_wall = reference_wall
def align_centerline(self) -> None:
def align_centerline(self, wall: bpy.types.Object) -> None:
self.wall = wall
self.align_rotation()
l_start = Vector(self.reference_wall.bound_box[0]).lerp(Vector(self.reference_wall.bound_box[3]), 0.5)
@@ -591,7 +629,8 @@ class DumbWallAligner:
new_origin = point - offset
self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy
def align_last_layer(self) -> None:
def align_last_layer(self, wall: bpy.types.Object) -> None:
self.wall = wall
self.align_rotation()
if self.is_rotation_flipped():
@@ -614,7 +653,8 @@ class DumbWallAligner:
new_origin = point - offset
self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy
def align_first_layer(self) -> None:
def align_first_layer(self, wall: bpy.types.Object) -> None:
self.wall = wall
self.align_rotation()
if self.is_rotation_flipped():
@@ -656,30 +696,6 @@ class DumbWallAligner:
return round(degrees(angle) % 360) == 180
class DumbWallRecalculator:
def recalculate(self, walls: list[bpy.types.Object]) -> None:
queue: set[tuple[ifcopenshell.entity_instance, bpy.types.Object]] = set()
for wall in walls:
element = tool.Ifc.get_entity(wall)
if tool.Ifc.is_moved(wall):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall)
queue.add((element, wall))
for rel in getattr(element, "ConnectedTo", []):
obj = tool.Ifc.get_object(rel.RelatedElement)
if tool.Ifc.is_moved(obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
queue.add((rel.RelatedElement, obj))
for rel in getattr(element, "ConnectedFrom", []):
obj = tool.Ifc.get_object(rel.RelatingElement)
if tool.Ifc.is_moved(obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
queue.add((rel.RelatingElement, obj))
joiner = DumbWallJoiner()
for element, wall in queue:
if tool.Model.get_usage_type(element) == "LAYER2" and wall:
joiner.recreate_wall(element, wall)
class DumbWallGenerator:
def __init__(self, relating_type):
self.relating_type = relating_type
@@ -911,7 +927,7 @@ class DumbWallPlaner:
else:
for rel in inverse.AssociatedTo:
walls.extend([tool.Ifc.get_object(e) for e in rel.RelatedObjects])
DumbWallRecalculator().recalculate([w for w in set(walls) if w])
tool.Model.recalculate_walls([w for w in set(walls) if w])
def regenerate_from_type(self, usecase_path, ifc_file, settings):
relating_type = settings["relating_type"]
@@ -942,7 +958,7 @@ class DumbWallPlaner:
if layer_set_direction:
material.LayerSetDirection = layer_set_direction
if material.LayerSetDirection == "AXIS2":
DumbWallRecalculator().recalculate([obj])
tool.Model.recalculate_walls([obj])
class DumbWallJoiner:
@@ -962,7 +978,7 @@ class DumbWallJoiner:
axis1 = tool.Model.get_wall_axis(wall1)
axis = copy.deepcopy(axis1["reference"])
body = copy.deepcopy(axis1["reference"])
self.recreate_wall(element1, wall1, axis, body)
tool.Model.recreate_wall(element1, wall1)
def split(self, wall1: bpy.types.Object, target: Vector) -> None:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
@@ -975,7 +991,6 @@ class DumbWallJoiner:
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1)
axis1 = tool.Model.get_wall_axis(wall1)
axis2 = copy.deepcopy(axis1)
intersect, cut_percentage = mathutils.geometry.intersect_point_line(target.to_2d(), *axis1["reference"])
if cut_percentage < 0 or cut_percentage > 1 or tool.Cad.is_x(cut_percentage, (0, 1)):
return
@@ -1036,20 +1051,36 @@ class DumbWallJoiner:
# During the duplication process, filled voids are not copied. So we
# only need to check fillings on the original element1.
for opening in [r.RelatedOpeningElement for r in element1.HasOpenings if r.RelatedOpeningElement.HasFillings]:
filling_obj = tool.Ifc.get_object(opening.HasFillings[0].RelatedBuildingElement)
rel = opening.HasFillings[0]
filling = rel.RelatedBuildingElement
filling_obj = tool.Ifc.get_object(filling)
filling_location = filling_obj.matrix_world.translation
_, filling_position = mathutils.geometry.intersect_point_line(filling_location.to_2d(), *axis1["reference"])
if filling_position > cut_percentage:
# The filling should be moved from element1 to element2.
FilledOpeningGenerator().generate(filling_obj, wall2, target=filling_obj.matrix_world.translation)
new_opening = ifcopenshell.api.root.copy_class(tool.Ifc.get(), product=opening)
new_opening.VoidsElements[0].RelatingBuildingElement = element2
if new_opening.ObjectPlacement and new_opening.ObjectPlacement.is_a("IfcLocalPlacement"):
if element2.ObjectPlacement:
new_opening.ObjectPlacement.PlacementRelTo = element2.ObjectPlacement
# For now, we do copy opening representations
if opening.Representation:
new_opening.Representation = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(), opening.Representation, exclude=["IfcGeometricRepresentationContext"]
)
rel.RelatedBuildingElement = element2
# Remove the old opening
ifcopenshell.api.run("feature.remove_feature", tool.Ifc.get(), feature=opening)
p1, p2 = ifcopenshell.util.representation.get_reference_line(element1)
p3 = (wall1.matrix_world.inverted() @ intersect.to_3d()).to_2d() / unit_scale
self.set_axis(element1, p1, p3)
self.set_axis(element2, p3, p2)
self.recreate_wall(element1, wall1)
self.recreate_wall(element2, wall2)
tool.Model.recreate_wall(element1, wall1)
tool.Model.recreate_wall(element2, wall2)
def flip(self, wall1: bpy.types.Object) -> None:
if tool.Ifc.is_moved(wall1):
@@ -1076,9 +1107,9 @@ class DumbWallJoiner:
ifcopenshell.api.geometry.edit_object_placement(
tool.Ifc.get(), product=element1, matrix=matrix, is_si=False, should_transform_children=False
)
self.recreate_wall(element1, wall1)
tool.Model.recreate_wall(element1, wall1)
def merge(self, wall1, wall2):
def merge(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None:
if tool.Ifc.is_moved(wall1):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1)
if tool.Ifc.is_moved(wall2):
@@ -1086,6 +1117,7 @@ class DumbWallJoiner:
element1 = tool.Ifc.get_entity(wall1)
element2 = tool.Ifc.get_entity(wall2)
assert element1 and element2
p1, p2 = ifcopenshell.util.representation.get_reference_line(element1)
p3, p4 = ifcopenshell.util.representation.get_reference_line(element2)
@@ -1096,7 +1128,7 @@ class DumbWallJoiner:
p3 = (matrix1i @ matrix2 @ np.concatenate((p3, (0, 1))))[:2]
p4 = (matrix1i @ matrix2 @ np.concatenate((p4, (0, 1))))[:2]
if not np.isclose(p1[1], p4[1]) or not np.isclose(p3[1], p4[1]):
if not np.isclose(p1[1], p4[1], atol=1e-02) or not np.isclose(p3[1], p4[1], atol=1e-02):
return
x_ordinates = tuple(co[0] for co in (p1, p2, p3, p4))
@@ -1128,7 +1160,7 @@ class DumbWallJoiner:
related_connection=rel.RelatedConnectionType,
)
self.recreate_wall(element1, wall1)
tool.Model.recreate_wall(element1, wall1)
tool.Geometry.delete_ifc_object(wall2)
@@ -1161,7 +1193,7 @@ class DumbWallJoiner:
description="TOP",
)
self.recreate_wall(element1, wall1)
tool.Model.recreate_wall(element1, wall1)
def set_axis(self, wall, p1, p2):
axis = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW")
@@ -1190,24 +1222,23 @@ class DumbWallJoiner:
self.set_axis(element1, p1, intersect)
else:
self.set_axis(element1, intersect, p2)
self.recreate_wall(element1, wall1)
tool.Model.recreate_wall(element1, wall1)
def set_length(self, wall1, si_length):
def set_length(self, wall1: bpy.types.Object, si_length: float) -> None:
element1 = tool.Ifc.get_entity(wall1)
if not element1:
return
assert element1
if tool.Ifc.is_moved(wall1):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1)
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type="ATEND")
ifcopenshell.api.geometry.disconnect_path(tool.Ifc.get(), element=element1, connection_type="ATEND")
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
p1, p2 = ifcopenshell.util.representation.get_reference_line(element1)
p2[0] = p1[0] + si_length / unit_scale
self.set_axis(element1, p1, p2)
self.recreate_wall(element1, wall1)
tool.Model.recreate_wall(element1, wall1)
def join_T(self, wall1, wall2):
def join_T(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None:
element1 = tool.Ifc.get_entity(wall1)
element2 = tool.Ifc.get_entity(wall2)
axis1 = tool.Model.get_wall_axis(wall1)
@@ -1219,8 +1250,7 @@ class DumbWallJoiner:
return
connection = "ATEND" if tool.Cad.edge_percent(intersect, axis1["reference"]) > 0.5 else "ATSTART"
ifcopenshell.api.run(
"geometry.connect_path",
ifcopenshell.api.geometry.connect_path(
tool.Ifc.get(),
related_element=element1,
relating_element=element2,
@@ -1229,9 +1259,9 @@ class DumbWallJoiner:
description="BUTT",
)
self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
tool.Model.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
def connect(self, obj1, obj2):
def connect(self, obj1: bpy.types.Object, obj2: bpy.types.Object) -> None:
wall1 = tool.Ifc.get_entity(obj1)
wall2 = tool.Ifc.get_entity(obj2)
if tool.Ifc.is_moved(obj1):
@@ -1239,27 +1269,8 @@ class DumbWallJoiner:
if tool.Ifc.is_moved(obj2):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj2)
ifcopenshell.api.geometry.connect_wall(tool.Ifc.get(), wall1=wall1, wall2=wall2)
self.recreate_wall(wall1, obj1)
self.recreate_wall(wall2, obj2)
def recreate_wall(self, element: ifcopenshell.entity_instance, obj: bpy.types.Object, axis=None, body=None) -> None:
rep = ifcopenshell.api.geometry.regenerate_wall_representation(tool.Ifc.get(), element)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=rep,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
tool.Geometry.record_object_materials(obj)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
matrix = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
matrix[:, 3] *= unit_scale
obj.matrix_world = tool.Loader.apply_blender_offset_to_matrix_world(obj, matrix)
tool.Geometry.record_object_position(obj)
tool.Model.recreate_wall(wall1, obj1)
tool.Model.recreate_wall(wall2, obj2)
def create_matrix(self, p, x, y, z):
return Matrix([x, y, z, p]).to_4x4().transposed()
@@ -529,6 +529,9 @@ class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
props.is_editing = False
update_window_modifier_representation(context)
element_type = ifcopenshell.util.element.get_type(element)
if element_type:
tool.Model.mark_thumbnail_for_update(element_type)
pset = tool.Pset.get_element_pset(element, "BBIM_Window")
window_data = tool.Ifc.get().createIfcText(json.dumps(window_data, default=list))

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