Compare commits

...

92 Commits

Author SHA1 Message Date
Andrej730 8b302f8d21 Make BIM Tool alignment buttons clickable and some hotkeys code cleanup 2023-04-10 11:22:17 +05:00
Dion Moult dfeebd6b6b Fix #2960. 2023-04-10 09:01:34 +10:00
Dion Moult 641253c4c6 Document list now shows description and locations instead of names as names are inherited. 2023-04-09 23:03:10 +10:00
Dion Moult 6ce49b7130 See #2516. References to titleblocks, layouts, and published sheets are now explicitly stored. Titleblock paths are now configurable. Generated filenames are sanitised. 2023-04-09 23:03:10 +10:00
Andrej730 804ad0cece Prevent flickering for SectionLevel and Grid decorators 2023-04-09 18:03:08 +05:00
Dion Moult 6c60d14c05 Fix #2959. Oversight when disabling mesh edit mode. Edit grid axes should be allowed. 2023-04-09 12:52:10 +10:00
Dion Moult def2d2bcc9 See #2516. WARNING: breaking changes! Restructure drawing system to use relative paths based on IfcDocumentReference Locations. 2023-04-08 21:59:53 +10:00
Andrej730 9bca705dc3 Solved producing orphan objects data on drawing deletion 2023-04-08 11:37:35 +05:00
Andrej730 2dcc271dff Updating sheet name from UI #2950 2023-04-07 19:06:41 +05:00
Thomas Krijnen 33415fa1af #2685 Fix AttributeError 2023-04-07 16:02:11 +02:00
Andrej730 10315e0e29 annotation and gizmo shaders rework for smoothed lines without bgl #2897
Reworked annotation and gizmo shaders to work without `bgl` and support smooth lines.

Moved LIB_GLSL and DEF_GLSL to shaders module since it's basically the same code and this way it'll be easier to keep track of it.
2023-04-07 17:36:58 +05:00
Dion Moult f321cc81c8 Minor improvement to add error message when mirror elements called without a mirror axis. 2023-04-07 20:07:38 +10:00
Bruno Perdigão 68b685f580 minor fix, sets a minimum to 'count' 2023-04-07 19:58:33 +10:00
Bruno Perdigão c87c79b937 Fix #2937. IFC Array now have the option to sync children 2023-04-07 19:58:33 +10:00
Sigma Dimensions de3dfcdc43 BBIM 4D Features to:
- Display nested task resources/task inputs of the active task
- Show list of tasks related to the current object selection
- Highlight, in the WorkSchedule panel, tasks  related to the current object selection
2023-04-07 04:33:56 +02:00
Kristoffer Andersen 657ddc0fa1 conda daily pin occt version 2023-04-06 21:38:42 +02:00
Dion Moult 1da01489f1 Fix #2935. Duplicating objects clear any parametric array relationships. 2023-04-06 21:18:36 +10:00
Massimo Fabbro 64a27614ff Fix bug #2825 2023-04-06 21:12:18 +10:00
Dion Moult 6c5acfc8bb Fix #2945. Selector should expand enumerated list properties so that you can match any of them. 2023-04-06 19:21:22 +10:00
Andrej730 884ee5b587 Warning when user loading sheets and some svgs are missing
Added this warning because missing svgs can lead to errors later on.
2023-04-06 13:51:12 +05:00
Andrej730 c2320a3c1e ChangeSheetTitleBlock operator
Added operator to change title block of currently selected sheet, it's located right after "Add sheet" button.
The way it works - you select the sheet, select the desired titleblock from dropdown menu and it overrides current titleblock from the sheet with the new one.
2023-04-06 13:51:07 +05:00
Bruno Perdigão ab23030051 ifc array modifier: get cursor position in relation to the object local axes 2023-04-06 10:45:47 +10:00
Bruno Perdigão 699c692f6c ifc array modifier: adds the option to get the dimensions from the 3d cursor 2023-04-06 10:45:47 +10:00
Bruno Perdigão 0c85e26950 ifc array modifier: bugfix, prevent divider from being zero. 2023-04-06 10:45:35 +10:00
Bruno Perdigão 2cd7e737cd ifc array modifier: adds the option to choose between "total" and "increment" as dimension input 2023-04-06 10:45:35 +10:00
Gorgious56 a1adbe2b34 You can now change the section line decorator width in the UI 2023-04-06 09:37:01 +10:00
Gorgious56 a144c2b7ea Add new shader to display the section as a thick line on the mesh 2023-04-06 09:37:01 +10:00
Gorgious56 201c056ffa Add new decorator utility to the Compare node group 2023-04-06 09:37:01 +10:00
Andrej730 2e95168610 Title case naming for operators 2023-04-05 17:49:58 +05:00
Andrej730 b18c05cbb9 Open all drawings at once with shift+click (similar to print all)
Also added filepath check to make sure all drawings were printed first.
2023-04-05 17:47:27 +05:00
Andrej730 102a4c6110 Title case naming for operators 2023-04-05 17:47:27 +05:00
Dion Moult b6c0ef3d19 Fix #2936. Unassigning a type now also unmaps representations. 2023-04-05 22:33:11 +10:00
Andrej730 ea3065dbc2 Sync viewport visibility based on EPset_Drawing/Include Exclude #2613
It's not real time, it's triggered on using "Activate view" button in "Drawings" section.
2023-04-05 16:34:43 +05:00
Gorgious56 534c3ad329 Fix temporary section cutaway socket inputs/outputs not initializing properly
Related to #2547
2023-04-05 13:30:35 +02:00
Dion Moult 90a7d807a1 Fix #2938. Unlinked objects should no longer synchronise edits after they've changed. 2023-04-05 21:07:44 +10:00
Dion Moult 4baafb5e2f See #2938. Fix issue where unlinking didn't consider other users of the object's data or material. 2023-04-05 20:58:24 +10:00
Andrej730 f911765cdb Support for SECTION_LEVEL annotations description in viewport
Example - https://i.imgur.com/ATF9Fp6.png
(similar to svg)
2023-04-05 14:44:27 +05:00
Andrej730 d0a36c79d5 Small fix 2023-04-05 13:10:41 +05:00
Andrej730 ffea5320a0 Background for Ifc Text annotations in SVG
Added option to fill background of Ifc Text annotation with white color.

To make it work need to add "fill-bg" class to EPset_Annotation - https://i.imgur.com/ig2EhkQ.png.

Result - https://i.imgur.com/mq4hzAM.png
2023-04-05 12:38:36 +05:00
Andrej730 0b54e41a40 Small editing assigned product UI fix
Previously checkmark didn't worked at all if the existing and new product were the same.
2023-04-04 19:02:07 +05:00
Andrej730 26c67f770a Sliding doors - more consistent 2d repr between door types #2919 2023-04-04 18:53:59 +05:00
Andrej730 f6623db0e6 Sliding doors - moved annotation arrow to Annotation/PLAN_VIEW #2919 2023-04-04 18:38:44 +05:00
Dion Moult d7844ee8e8 Fix #2114. Duplicate overrides are now defined as macros which means that repeating last function now works. 2023-04-04 22:01:45 +10:00
Andrej730 1c8970bbf2 removed redundant parts from generate_steel_profiles_library.py
removed never used methods and contexts
2023-04-04 14:23:21 +05:00
Dion Moult eb70849a30 Fix #2898. Explicitly check for changed active material in material UI panels. 2023-04-04 17:04:32 +10:00
Andrej730 77ce3c9067 Fixed ifc validation errors for generated openings #2925
Now ELEVATION_VIEW 3d curves converted to 2d to be extruded later on to create opening representation.
2023-04-04 11:18:29 +05:00
Dion Moult 56cd226a27 Deprecate mode subscriptiosn of profiles and walls 2023-04-04 13:59:10 +10:00
Dion Moult a1acdc0977 See #2881. Allow indices in the selector instead of relying on external evaluations. 2023-04-04 09:58:23 +10:00
Dion Moult 1a624c957b See #2916. Minor fix. 2023-04-04 09:43:58 +10:00
Andrej730 1c9eeb2aca Revert "Fixed ifc validation errors for generated openings #2925"
This reverts commit 66a53b486a.
2023-04-03 23:04:27 +05:00
Gorgious56 45f06eeea7 Interface now displays a hint when trying to edit a non-mesh-like object
& Fix case where the active object would go into edit mode even if not mesh-like when multiple objects were selected
2023-04-03 17:55:44 +02:00
Andrej730 66a53b486a Fixed ifc validation errors for generated openings #2925 2023-04-03 19:53:43 +05:00
Andrej730 fc45f5b48f Fixed ifc validation errors for furniture library #2925
Fixed bunch of validation errors with furniture library:

- IfcSpaceType PredefinedType wasn't optional
`<attribute PredefinedType: <enumeration IfcSpaceTypeEnum: (EXTERNAL, GFA, INTERNAL, NOTDEFINED, PARKING, SPACE, USERDEFINED)>> Not optional`

- some object had 0.0 depth extrusion
```
Rule IfcPositiveLengthMeasure_WR1:
    (self > 0.)
Violated by:
    (0.0 > 0.0)
```

- some orphan representations violating:
```
Rule IfcShapeModel_WR11:
    ((sizeof(self.OfProductRepresentation) == 1) ^ (sizeof(self.RepresentationMap) == 1) ^ (sizeof(ofshapeaspect) == 1))
Violated by:
    ((0 == 1 ^ 0 == 1) ^ 0 == 1)
     +  where 0 = sizeof(())
     +    where () = #9091=IfcShapeRepresentation(#15,'Body','SweptSolid',(#9090)).OfProductRepresentation
     +  and   0 = sizeof(())
     +    where () = #9091=IfcShapeRepresentation(#15,'Body','SweptSolid',(#9090)).RepresentationMap
     +  and   0 = sizeof(())
```
2023-04-03 19:53:43 +05:00
Andrej730 c7105a22e6 Fixed validation error for ELEVATION_VIEW curves #2925
Error occured because ShapeBuilder was assigning "Curve2D" representation type for elevation view curves when the correct type is "Curve3D".

It use to occur for both doors and windows created with ifc modifier.

```
Validation error text:
2023-04-03:18:21:28,879 ERROR   [rule_executor.py:154] On instance:
    #135=IfcShapeRepresentation(#21,'Profile','Curve2D',(#134))
Rule IfcShapeRepresentation_CorrectItemsForType:
    (IfcShapeRepresentationTypes(self.RepresentationType,self.Items))
Violated by:
    False
     +  where False = IfcShapeRepresentationTypes('Curve2D', (#134=IfcIndexedPolyCurve(#133,(IfcLineIndex((1,2)),IfcLineIndex((2,3)),IfcLineIndex((3,4)),IfcLineIndex((4,1))),$),))
     +    where 'Curve2D' = #135=IfcShapeRepresentation(#21,'Profile','Curve2D',(#134)).RepresentationType
     +    and   (#134=IfcIndexedPolyCurve(#133,(IfcLineIndex((1,2)),IfcLineIndex((2,3)),IfcLineIndex((3,4)),IfcLineIndex((4,1))),$),) = #135=IfcShapeRepresentation(#21,'Profile','Curve2D',(#134)).Items
```
2023-04-03 19:53:43 +05:00
Andrej730 e1fc7cad44 Fixed all unit.add_si_unit uses after fd87747
Now they were causing `TypeError: Usecase.__init__() got an unexpected keyword argument 'name'`
2023-04-03 19:53:42 +05:00
Andrej730 afeeaeb984 Fixed some validation errors for doors and windows #2925
Fixed the ones caused by using 3d curves as inner/outer curves in profiles.
2023-04-03 19:53:42 +05:00
Andrej730 2bc60b87f2 Fixed issue with tool.Blender.apply_bmesh 2023-04-03 19:53:42 +05:00
Dion Moult 4ad9ecb7db FixRevitTINs patch recipe now also purges non up-facing polygons as Revit doesn't handle them very well. 2023-04-03 21:28:34 +10:00
Dion Moult 3ec7b82b85 Fix #2930. Do not require entity applicability in IfcTester. 2023-04-03 21:28:00 +10:00
Dion Moult 48666def6b Fix #2910. Bug where style colours were not synced prior to duplication. 2023-04-03 20:16:43 +10:00
Chun 5732921fb6 a simplifier workflow and a more consistent coding style 2023-04-03 19:50:53 +10:00
Chun b86c11e1dc Unify variable 2023-04-03 19:50:53 +10:00
Chun 1bae4b219a Update functions of get_connected_to and get_connected_from compatible with IFC4 and IFC2X3 2023-04-03 19:50:53 +10:00
Chun 70ff73f805 Add IFC4 support for functions get_connected_to and get_connected_from in util.system 2023-04-03 19:50:53 +10:00
Chun a9636c5038 Add IFC4 support to get_connected_to and get_connected_from functions 2023-04-03 19:50:53 +10:00
Dion Moult a406b9f7ef Fix #2928. 2023-04-03 18:12:32 +10:00
Dion Moult f291345111 Fix #2924. IfcElements default to a null predefined type so they can inherit where possible. 2023-04-03 13:54:25 +10:00
Dion Moult c947ccb8be Fix #2926. Connections removed when rotating 90 deg to prevent unintuitive behaviour. Layer2 are also now recalculated. 2023-04-03 13:43:42 +10:00
Dion Moult 28c6b0d4bb Link to IfcArchitect tutorial series for quickstart video tutorials 2023-04-03 12:35:10 +10:00
Dion Moult 19317a9cc2 See #2881. Add support for evaluated commands in smart text literals 2023-04-02 21:25:06 +10:00
Dion Moult d67da6425e Fix #2927. Continuing to make mesh editing stricter. 2023-04-02 15:02:43 +10:00
Dion Moult 901f41358f Shortlist which IFC geometries may be treated as a meshlike object. 2023-04-02 13:45:05 +10:00
Dion Moult 061922aea2 Minor fix 2023-04-01 22:19:29 +11:00
Massimo Fabbro 5f7b6a5498 fix qto ui bug where objects with no assigned base qto doesn't allow to show the qto panel 2023-04-01 21:59:53 +11:00
Dion Moult 2ef9d5eed9 Fix #2922. You can now edit extrusion profiles of swept solids. 2023-04-01 21:27:20 +11:00
Dion Moult 9fba57d5db Disable edit mode on non mesh-like representations as well to prevent risk of invalid geometry. 2023-04-01 21:25:45 +11:00
Dion Moult e9074e3b3f Fix bug where generated openings invalidly used 3 dimensions instead of 2 2023-04-01 18:14:54 +11:00
Andrej730 f053ba714f Fixed bug in tool.Blender.get_bmesh_for_mesh
It caused errors because it was expecting mesh object to be selected - when it's not necessary that active_object is the one that mesh belongs to and sometimes no objects can be selected at all.
2023-04-01 12:12:34 +05:00
Andrej730 b6598f09a3 Fixed error with switching from EDIT mode for annotation meshes
Error occured when you were editing annotation mesh and tried to switched from EDIT mode

Error: Python: Traceback (most recent call last):
  File "\blenderbim\bim\module\geometry\operator.py", line 838, in invoke
    elif element.HasOpenings:
  File "\blenderbim\libs\site\packages\ifcopenshell\entity_instance.py", line 153, in __getattr__
    raise AttributeError(
AttributeError: entity instance of type 'IFC4.IfcAnnotation' has no attribute 'HasOpenings'
2023-04-01 12:00:07 +05:00
Andrej730 b9d7b95304 Revert part of 3a3d96be0 #2897
Reverted part of the bgl to gpu transition (to not scare away people with annotations without antialiasing🙂) until I work out the complete solution.
2023-03-31 19:56:26 +05:00
Andrej730 3db540f786 Fixed another typo related to #2889... 2023-03-31 19:50:22 +05:00
Andrej730 071b99c6c6 Fixed crash in Blender 3.5 on creating fill area annotation
Crash was caused by some change in Blender Python API in 3.5.
The code below resulted in `ValueError: bpy_struct: item.attr = val: MeshPolygon.vertices: array length cannot be changed to 4 (expected 0)`

```
obj.data.polygons.add(1)
p1 = obj.data.polygons[-1]
p1.vertices = (v1.index, v2.index, v3.index, v4.index)
```
2023-03-31 17:52:07 +05:00
Andrej730 3a3d96be0a bgl to gpu: profiles and openings decorations #2897
Still need to fix shaders.py and decoration.py
2023-03-31 16:38:24 +05:00
Andrej730 a03fbab4d5 Added description to some annotation operators 2023-03-31 11:06:21 +05:00
Andrej730 cc3b119288 Automatically adjusted STAIR_ARROWs now stop at the top thread
Previously they were ending at the end of the stair object and problem was that the end of the object could be at the slab underneath the stair.

Example - https://i.imgur.com/Y43v0GM.png
Before - https://i.imgur.com/Ze9DoK2.png
2023-03-31 10:59:39 +05:00
Andrej730 1a750d1b85 STAIR_ARROW annotations attached to IfcStairFlight object now readjusted after changes in IFC Stair modifier
Later probably will also need some other way to readjust annotations - so they can be readjusted for custom stairs, not created with modifier.
2023-03-31 10:59:39 +05:00
Andrej730 5b3ae9b9fd Clever STAIR_ARROW annotation
Now adding STAIR_ARROW annotation is a bit more clever - if you have IfcStairFlight selected when adding this type of annotation it will automatically:
1) define stair as it's parent;
2) add the stair as it's product;
3) place itself along the stair (it's assumed that stair direction is X+)

Example - https://i.imgur.com/Ze9DoK2.png
2023-03-30 18:41:55 +05:00
Andrej730 81471d80cd Fixed bug causing errors in IFC4X3 projects
It was causing an error below on opening BIM Tool and probably in some other places.

```
Traceback (most recent call last):
  File "C:\software\Steam\steamapps\common\Blender\3.5\scripts\startup\bl_ui\space_view3d.py", line 35, in draw
    self.draw_tool_settings(context)
  File "C:\software\Steam\steamapps\common\Blender\3.5\scripts\startup\bl_ui\space_view3d.py", line 48, in draw_tool_settings
    tool = ToolSelectPanelHelper.draw_active_tool_header(
  File "C:\software\Steam\steamapps\common\Blender\3.5\scripts\startup\bl_ui\space_toolsystem_common.py", line 809, in draw_active_tool_header
    draw_settings(context, layout, tool)
  File "\Blender\3.5\scripts\addons\blenderbim\bim\module\model\workspace.py", line 67, in draw_settings
    BimToolUI.draw(context, layout)
  File "\Blender\3.5\scripts\addons\blenderbim\bim\module\model\workspace.py", line 89, in draw
    AuthoringData.load()
  File "\Blender\3.5\scripts\addons\blenderbim\bim\module\model\data.py", line 52, in load
    cls.load_ifc_classes()
  File "\Blender\3.5\scripts\addons\blenderbim\bim\module\model\data.py", line 109, in load_ifc_classes
    cls.data["ifc_classes"] = cls.ifc_classes()
  File "\Blender\3.5\scripts\addons\blenderbim\bim\module\model\data.py", line 196, in ifc_classes
    + tool.Ifc.get().by_type("IfcDoorStyle")
  File "\Blender\3.5\scripts\addons\blenderbim\libs\site\packages\ifcopenshell\file.py", line 364, in by_type
    return [entity_instance(e, self) for e in self.wrapped_data.by_type(type)]
  File "\Blender\3.5\scripts\addons\blenderbim\libs\site\packages\ifcopenshell\ifcopenshell_wrapper.py", line 4499, in by_type
    return _ifcopenshell_wrapper.file_by_type(self, *args)
RuntimeError: Entity with name 'IfcDoorStyle' not found in schema 'IFC4X3'
Traceback (most recent call last):
  File "C:\software\Steam\steamapps\common\Blender\3.5\scripts\startup\bl_ui\space_view3d.py", line 35, in draw
    self.draw_tool_settings(context)
  File "C:\software\Steam\steamapps\common\Blender\3.5\scripts\startup\bl_ui\space_view3d.py", line 48, in draw_tool_settings
    tool = ToolSelectPanelHelper.draw_active_tool_header(
  File "C:\software\Steam\steamapps\common\Blender\3.5\scripts\startup\bl_ui\space_toolsystem_common.py", line 809, in draw_active_tool_header
    draw_settings(context, layout, tool)
  File "\Blender\3.5\scripts\addons\blenderbim\bim\module\model\workspace.py", line 67, in draw_settings
    BimToolUI.draw(context, layout)
  File "\Blender\3.5\scripts\addons\blenderbim\bim\module\model\workspace.py", line 92, in draw
    cls.draw_header_interface()
  File "\Blender\3.5\scripts\addons\blenderbim\bim\module\model\workspace.py", line 351, in draw_header_interface
    cls.draw_type_selection_interface()
  File "\Blender\3.5\scripts\addons\blenderbim\bim\module\model\workspace.py", line 367, in draw_type_selection_interface
    if AuthoringData.data["ifc_classes"]:
KeyError: 'ifc_classes'
```
2023-03-30 17:12:00 +05:00
Andrej730 eb14db4deb Fixed typo in ifcopenshell.util.type 2023-03-30 16:42:25 +05:00
Andrej730 50d989363a Added arrow to 2d repr of sliding doors (#2919)
Example - https://i.imgur.com/eJnhDXU.png
2023-03-30 16:36:55 +05:00
Dion Moult e52a99bd71 Load all type products by default (in preparation for IfcAnnotationType support) 2023-03-30 21:26:16 +11:00
Andrej730 a7a7c86657 IFC Door Modifier - sliding doors
Added support for sliding doors in IFC Door Modifier.

The main differences are 2d representation (https://i.imgur.com/VyEH569.png) and the door panel being placed before the lining, not after
2023-03-30 12:41:34 +05:00
110 changed files with 27244 additions and 26577 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ requirements:
run:
- python
- boost-cpp
- occt
- {{ pin_compatible('occt', max_pin='x.x.x') }}
- libxml2
- cgal-cpp
- hdf5
+16 -1
View File
@@ -17,6 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import blenderbim
import importlib
from . import handler, ui, prop, operator, helper
@@ -171,7 +172,21 @@ def register():
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
if ( #these panels haven't the cls.is_registered attribute
cls == blenderbim.bim.module.drawing.gizmos.ExtrusionWidget or
cls == blenderbim.bim.module.drawing.gizmos.ExtrusionGuidesGizmo or
cls == blenderbim.bim.module.drawing.gizmos.DimensionLabelGizmo or
cls == blenderbim.bim.module.drawing.gizmos.DotGizmo or
cls == blenderbim.bim.module.drawing.gizmos.UglyDotGizmo
):
if bpy.data.scenes['Scene'].BIMProperties.module_visibility['drawing'].is_visible is True:
bpy.utils.unregister_class(cls)
continue
else:
continue
if cls.is_registered is not False:
bpy.utils.unregister_class(cls)
bpy.app.handlers.load_post.remove(handler.setDefaultProperties)
bpy.app.handlers.load_post.remove(handler.loadIfcStore)
bpy.app.handlers.save_post.remove(handler.ensureIfcExported)
@@ -96,4 +96,12 @@
<path d="M 7.5 10 L 10 0 L 15 10" class="annotation" style="stroke-width:1; fill: white;" />
</g>
</marker>
<filter x="0" y="0" width="1" height="1" id="fill-background">
<feFlood flood-color="white" result="bg" />
<feMerge>
<feMergeNode in="bg"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</svg>

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -5
View File
@@ -126,7 +126,7 @@ class IfcExporter:
continue
try:
if isinstance(obj, bpy.types.Material):
if self.has_changed_shading(obj):
if tool.Ifc.has_changed_shading(obj):
blenderbim.core.style.update_style_colours(tool.Ifc, tool.Style, obj=obj)
else:
element = tool.Ifc.get_entity(obj)
@@ -149,10 +149,6 @@ class IfcExporter:
checksum = obj.data.BIMMeshProperties.material_checksum
return checksum != str([s.id() for s in tool.Geometry.get_styles(obj) if s])
def has_changed_shading(self, obj):
checksum = obj.BIMMaterialProperties.shading_checksum
return checksum != repr(np.array(obj.diffuse_color).tobytes())
def sync_object_placement(self, obj):
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
if not tool.Ifc.is_moved(obj):
+4 -4
View File
@@ -105,16 +105,16 @@ def active_material_index_callback(obj, data):
refresh_ui_data()
def subscribe_to(object, data_path, callback):
def subscribe_to(obj, data_path, callback):
try:
subscribe_to = object.path_resolve(data_path, False)
subscribe_to = obj.path_resolve(data_path, False)
except:
return
bpy.msgbus.subscribe_rna(
key=subscribe_to,
owner=object,
owner=obj,
args=(
object,
obj,
data_path,
),
notify=callback,
+2 -1
View File
@@ -24,6 +24,7 @@ import zipfile
import tempfile
import ifcopenshell
import blenderbim.bim.handler
import blenderbim.tool as tool
from pathlib import Path
@@ -371,7 +372,7 @@ class IfcStore:
def unlink_element(element=None, obj=None):
if element is None:
try:
element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
element = tool.Ifc.get_entity(obj)
except:
pass
+1 -17
View File
@@ -335,23 +335,7 @@ class IfcImporter:
if self.ifc_import_settings.has_filter or offset or offset_limit < len(self.elements):
self.element_types = set([ifcopenshell.util.element.get_type(e) for e in self.elements])
else:
if self.file.schema == "IFC2X3":
self.element_types = set(
self.file.by_type("IfcElementType")
+ self.file.by_type("IfcDoorStyle")
+ self.file.by_type("IfcWindowStyle")
)
elif self.file.schema == "IFC4":
self.element_types = set(
self.file.by_type("IfcElementType")
+ self.file.by_type("IfcDoorStyle")
+ self.file.by_type("IfcWindowStyle")
+ self.file.by_type("IfcSpatialElementType")
)
else:
self.element_types = set(
self.file.by_type("IfcElementType") + self.file.by_type("IfcSpatialElementType")
)
self.element_types = set(self.file.by_type("IfcTypeProduct"))
if self.ifc_import_settings.has_filter and self.ifc_import_settings.should_filter_spatial_elements:
self.spatial_elements = self.get_spatial_elements_filtered_by_elements(self.elements)
@@ -59,9 +59,13 @@ class MaterialAttributesData:
@classmethod
def load(cls):
cls.data = {"attributes": cls.attributes()}
cls.data = {"ifc_definition_id": cls.ifc_definition_id(), "attributes": cls.attributes()}
cls.is_loaded = True
@classmethod
def ifc_definition_id(cls):
return bpy.context.active_object.active_material.BIMObjectProperties.ifc_definition_id
@classmethod
def attributes(cls):
results = []
@@ -94,4 +94,10 @@ class BIM_PT_material_attributes(Panel):
def draw(self, context):
if not MaterialAttributesData.is_loaded:
MaterialAttributesData.load()
elif (
context.active_object.active_material.BIMObjectProperties.ifc_definition_id
!= MaterialAttributesData.data["ifc_definition_id"]
):
MaterialAttributesData.load()
draw_ui(context, self.layout, "Material", MaterialAttributesData.data["attributes"])
@@ -119,7 +119,7 @@ class Loader:
class LoadProjectSpaceBoundaries(bpy.types.Operator):
bl_idname = "bim.load_project_space_boundaries"
bl_label = "Load all project space boundaries"
bl_label = "Load All Project Space Boundaries"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -131,7 +131,7 @@ class LoadProjectSpaceBoundaries(bpy.types.Operator):
class LoadBoundary(bpy.types.Operator):
bl_idname = "bim.load_boundary"
bl_label = "Load boundary"
bl_label = "Load Boundary"
bl_options = {"REGISTER", "UNDO"}
boundary_id: bpy.props.IntProperty()
@@ -147,7 +147,7 @@ class LoadBoundary(bpy.types.Operator):
class LoadSpaceBoundaries(bpy.types.Operator):
bl_idname = "bim.load_space_boundaries"
bl_label = "Load selected space boundaries"
bl_label = "Load Selected Space Boundaries"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -168,7 +168,7 @@ def get_element_boundaries(element):
class SelectRelatedElementBoundaries(bpy.types.Operator):
bl_idname = "bim.select_related_element_boundaries"
bl_label = "Select related element space boundaries"
bl_label = "Select Related Element Space Boundaries"
bl_options = {"REGISTER", "UNDO"}
related_element: bpy.props.IntProperty()
@@ -185,7 +185,7 @@ class SelectRelatedElementBoundaries(bpy.types.Operator):
class SelectRelatedElementTypeBoundaries(bpy.types.Operator):
bl_idname = "bim.select_related_element_type_boundaries"
bl_label = "Select related element type space boundaries"
bl_label = "Select Related Element Type Space Boundaries"
bl_options = {"REGISTER", "UNDO"}
related_element: bpy.props.IntProperty()
@@ -208,7 +208,7 @@ class SelectRelatedElementTypeBoundaries(bpy.types.Operator):
class SelectSpaceBoundaries(bpy.types.Operator):
bl_idname = "bim.select_space_boundaries"
bl_label = "Select all space boundaries"
bl_label = "Select All Space Boundaries"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -224,7 +224,7 @@ class SelectSpaceBoundaries(bpy.types.Operator):
class SelectProjectBoundaries(bpy.types.Operator):
bl_idname = "bim.select_project_space_boundaries"
bl_label = "Select all project space boundaries"
bl_label = "Select All Project Space Boundaries"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -260,7 +260,7 @@ def get_colour(ifc_boundary):
class ColourByRelatedBuildingElement(bpy.types.Operator):
bl_idname = "bim.colour_by_related_building_element"
bl_label = "Apply colour based on related building elements"
bl_label = "Apply Colour Based on Related Building Elements"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -308,7 +308,7 @@ EDITABLE_ATTRIBUTES = {
class EnableEditingBoundary(bpy.types.Operator):
bl_idname = "bim.enable_editing_boundary"
bl_label = "Edit boundary relations"
bl_label = "Edit Boundary Relations"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -327,7 +327,7 @@ class EnableEditingBoundary(bpy.types.Operator):
class DisableEditingBoundary(bpy.types.Operator):
bl_idname = "bim.disable_editing_boundary"
bl_label = "Disable editing boundary relations"
bl_label = "Disable Editing Boundary Relations"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -340,7 +340,7 @@ class DisableEditingBoundary(bpy.types.Operator):
class EditBoundaryAttributes(bpy.types.Operator):
bl_idname = "bim.edit_boundary_attributes"
bl_label = "Disable editing boundary relations"
bl_label = "Disable Editing Boundary Relations"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -361,7 +361,7 @@ class EditBoundaryAttributes(bpy.types.Operator):
class UpdateBoundaryGeometry(bpy.types.Operator):
bl_idname = "bim.update_boundary_geometry"
bl_label = "Update boundary geometry"
bl_label = "Update Boundary Geometry"
bl_description = """
Update boundary connection geometry from mesh.
Mesh must lie on a single plane. It should look like a face or a face with holes.
@@ -156,7 +156,7 @@ class CadTool(WorkSpaceTool):
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_R")
row.operator("bim.hotkey", text="Set gable roof angle").hotkey = "S_R"
row.operator("bim.cad_hotkey", text="Set Gable Roof Angle").hotkey = "S_R"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
@@ -149,7 +149,7 @@ class ContractCostItem(bpy.types.Operator, tool.Ifc.Operator):
class RemoveCostItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_cost_item"
bl_label = "Remove Cost item"
bl_label = "Remove Cost Item"
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
@@ -55,6 +55,7 @@ classes = (
operator.LoadSheets,
operator.OpenSchedule,
operator.OpenSheet,
operator.ChangeSheetTitleBlock,
operator.OpenView,
operator.RemoveDrawing,
operator.RemoveDrawingFromSheet,
@@ -92,51 +92,17 @@ class Annotator:
@staticmethod
def add_plane_to_annotation(obj):
co1, co2, co3, co4 = Annotator.get_placeholder_coords()
co1 = obj.matrix_world.inverted() @ co1 # bot left
co2 = obj.matrix_world.inverted() @ co2 # top left
co3 = obj.matrix_world.inverted() @ co3 # bot right
co4 = obj.matrix_world.inverted() @ co4 # top right
# default order = bot left, top left, bot right, top right
# therefore we redefine the order
face_verts = [0, 2, 3, 1]
obj.data.vertices.add(4)
v1 = obj.data.vertices[-4]
v2 = obj.data.vertices[-3]
v3 = obj.data.vertices[-2]
v4 = obj.data.vertices[-1]
v1.co = co4
v2.co = co2
v3.co = co1
v4.co = co3
verts_world_space = Annotator.get_placeholder_coords()
verts_local = [obj.matrix_world.inverted() @ v for v in verts_world_space]
bm = tool.Blender.get_bmesh_for_mesh(obj.data, clean=True)
new_verts = [bm.verts.new(v) for v in verts_local]
obj.data.edges.add(4)
e1 = obj.data.edges[-4]
e2 = obj.data.edges[-3]
e3 = obj.data.edges[-2]
e4 = obj.data.edges[-1]
e1.vertices = (v1.index, v2.index)
e2.vertices = (v2.index, v3.index)
e3.vertices = (v3.index, v4.index)
e4.vertices = (v4.index, v1.index)
obj.data.loops.add(4)
l1 = obj.data.loops[-4]
l2 = obj.data.loops[-3]
l3 = obj.data.loops[-2]
l4 = obj.data.loops[-1]
l1.vertex_index = v1.index
l1.edge_index = e1.index
l2.vertex_index = v2.index
l2.edge_index = e2.index
l3.vertex_index = v3.index
l3.edge_index = e3.index
l4.vertex_index = v4.index
l4.edge_index = e4.index
obj.data.polygons.add(1)
p1 = obj.data.polygons[-1]
p1.vertices = (v1.index, v2.index, v3.index, v4.index)
p1.loop_start = l1.index
p1.loop_total = 4
bm.faces.new([new_verts[i] for i in face_verts])
tool.Blender.apply_bmesh(obj.data, bm, obj)
return obj
@staticmethod
File diff suppressed because it is too large Load Diff
@@ -18,10 +18,9 @@
import bpy
import blf
import math
import gpu, bgl
import gpu
from bpy import types
from mathutils import Vector, Matrix
from mathutils import Vector
from mathutils import geometry
from bpy_extras import view3d_utils
from blenderbim.bim.module.drawing.shaders import DotsGizmoShader, ExtrusionGuidesShader, BaseLinesShader
@@ -273,7 +272,7 @@ class CustomGizmo:
gpu.matrix.multiply_matrix(self.matrix_world)
batch.draw()
bgl.glDisable(bgl.GL_BLEND)
gpu.state.blend_set("NONE")
class OffsetHandle:
@@ -500,7 +499,7 @@ class ExtrusionWidget(types.GizmoGroup):
gz = self.guides = self.gizmos.new("BIM_GT_extrusion_guides")
gz.matrix_basis = basis
gz.color = gz.color_highlight = tuple(theme.gizmo_secondary)
gz.alpha = gz.alpha_highlight = 0.5
gz.alpha = gz.alpha_highlight = 0.75
gz.use_draw_modal = True
gz.target_set_prop("depth", prop, "value")
gz.scale_value = scale_value
@@ -46,6 +46,7 @@ from mathutils import Vector
from timeit import default_timer as timer
from blenderbim.bim.module.drawing.prop import RasterStyleProperty, Literal
from blenderbim.bim.ifc import IfcStore
from pathlib import Path
cwd = os.path.dirname(os.path.realpath(__file__))
@@ -147,7 +148,7 @@ class CreateDrawing(bpy.types.Operator):
"Creates/refreshes a .svg drawing based on currently active camera.\n\n"
+ "SHIFT+CLICK to print all annotations"
)
print_all: bpy.props.BoolProperty(name="Print all", default=False)
print_all: bpy.props.BoolProperty(name="Print All", default=False)
@classmethod
def poll(cls, context):
@@ -178,6 +179,7 @@ class CreateDrawing(bpy.types.Operator):
self.camera = context.scene.camera
self.camera_element = tool.Ifc.get_entity(self.camera)
self.camera_document = tool.Drawing.get_drawing_document(self.camera_element)
self.file = IfcStore.get_file()
with profile("Drawing generation process"):
@@ -197,14 +199,8 @@ class CreateDrawing(bpy.types.Operator):
self.svg_writer.camera_projection = tuple(
self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
)
pset = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]
related_paths = {
"Stylesheet": pset.get("Stylesheet"),
"Markers": pset.get("Markers"),
"Symbols": pset.get("Symbols"),
"Patterns": pset.get("Patterns"),
}
self.svg_writer.define_related_paths(**related_paths)
self.svg_writer.setup_drawing_resource_paths(self.camera_element)
with profile("Generate underlay"):
underlay_svg = self.generate_underlay(context)
@@ -236,7 +232,7 @@ class CreateDrawing(bpy.types.Operator):
def combine_svgs(self, context, underlay, linework, annotation):
# Hacky :)
svg_path = os.path.join(context.scene.BIMProperties.data_dir, "diagrams", self.drawing_name + ".svg")
svg_path = self.get_svg_path()
with open(svg_path, "w") as outfile:
self.svg_writer.create_blank_svg(svg_path).define_boilerplate()
boilerplate = self.svg_writer.svg.tostring()
@@ -281,7 +277,7 @@ class CreateDrawing(bpy.types.Operator):
def generate_underlay(self, context):
if not self.cprops.has_underlay:
return
svg_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", self.drawing_name + "-underlay.svg")
svg_path = self.get_svg_path(cache_type="underlay")
context.scene.render.filepath = svg_path[0:-4] + ".png"
drawing_style = context.scene.DocProperties.drawing_styles[self.cprops.active_drawing_style_index]
@@ -321,7 +317,7 @@ class CreateDrawing(bpy.types.Operator):
def generate_linework(self, context):
if not self.cprops.has_linework:
return
svg_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", self.drawing_name + "-linework.svg")
svg_path = self.get_svg_path(cache_type="linework")
if os.path.isfile(svg_path) and self.props.should_use_linework_cache:
return svg_path
@@ -604,7 +600,7 @@ class CreateDrawing(bpy.types.Operator):
def generate_annotation(self, context):
if not self.cprops.has_annotation:
return
svg_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", self.drawing_name + "-annotation.svg")
svg_path = self.get_svg_path(cache_type="annotation")
if os.path.isfile(svg_path) and self.props.should_use_annotation_cache:
return svg_path
@@ -661,6 +657,17 @@ class CreateDrawing(bpy.types.Operator):
return element.LayerSetName
return "mat-" + str(element.id())
def get_svg_path(self, cache_type=None):
drawing_path = tool.Drawing.get_document_uri(self.camera_document)
drawings_dir = os.path.dirname(drawing_path)
if cache_type:
drawings_dir = os.path.join(drawings_dir, "cache")
os.makedirs(drawings_dir, exist_ok=True)
return os.path.join(drawings_dir, f"{self.drawing_name}-{cache_type}.svg")
os.makedirs(drawings_dir, exist_ok=True)
return drawing_path
class AddAnnotation(bpy.types.Operator, Operator):
bl_idname = "bim.add_annotation"
@@ -668,16 +675,23 @@ class AddAnnotation(bpy.types.Operator, Operator):
bl_options = {"REGISTER", "UNDO"}
object_type: bpy.props.StringProperty()
data_type: bpy.props.StringProperty()
description: bpy.props.StringProperty()
@classmethod
def poll(cls, context):
return IfcStore.get_file() and context.scene.camera
@classmethod
def description(cls, context, operator):
return operator.description or ""
def _execute(self, context):
drawing = tool.Ifc.get_entity(context.scene.camera)
if not drawing:
self.report({"WARNING"}, "Not a BIM camera")
return
# TODO: support adding multiple annotations if there are multiple selected objects
# that can be used as annotations references
r = core.add_annotation(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing, object_type=self.object_type)
if isinstance(r, str):
self.report({"WARNING"}, r)
@@ -703,7 +717,7 @@ class OpenSheet(bpy.types.Operator, Operator):
core.open_sheet(tool.Drawing, sheet=sheet)
class AddDrawingToSheet(bpy.types.Operator):
class AddDrawingToSheet(bpy.types.Operator, Operator):
bl_idname = "bim.add_drawing_to_sheet"
bl_label = "Add Drawing To Sheet"
bl_options = {"REGISTER", "UNDO"}
@@ -713,11 +727,13 @@ class AddDrawingToSheet(bpy.types.Operator):
props = context.scene.DocProperties
return props.drawings and props.sheets and context.scene.BIMProperties.data_dir
def execute(self, context):
def _execute(self, context):
props = context.scene.DocProperties
active_drawing = props.drawings[props.active_drawing_index]
active_sheet = props.sheets[props.active_sheet_index]
drawing = tool.Ifc.get().by_id(active_drawing.ifc_definition_id)
drawing_reference = tool.Drawing.get_drawing_document(drawing)
sheet = tool.Ifc.get().by_id(active_sheet.ifc_definition_id)
if not sheet.is_a("IfcDocumentInformation"):
return {"FINISHED"}
@@ -729,13 +745,7 @@ class AddDrawingToSheet(bpy.types.Operator):
has_drawing = False
for reference in references:
if tool.Ifc.get_schema() == "IFC2X3":
element = [r for r in tool.Ifc.by_type("IfcRelAssociatesDocument") if r.RelatingDocument == reference][
0
].RelatedObjects[0]
else:
element = reference.DocumentRefForObjects[0].RelatedObjects[0]
if element == drawing:
if reference.Location == drawing_reference.Location:
has_drawing = True
break
@@ -744,11 +754,17 @@ class AddDrawingToSheet(bpy.types.Operator):
reference = tool.Ifc.run("document.add_reference", information=sheet)
if tool.Ifc.get_schema() == "IFC2X3":
attributes = {"ItemReference": str(len(sheet.DocumentReferences or []))}
references = sheet.DocumentReferences
id_attr = "ItemReference"
else:
attributes = {"Identification": str(len(sheet.HasDocumentReferences or []))}
references = sheet.HasDocumentReferences
id_attr = "Identification"
attributes = {
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "SCHEDULE")]) + 1),
"Location": drawing_reference.Location,
"Description": "DRAWING",
}
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
tool.Ifc.run("document.assign_document", product=drawing, document=reference)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
sheet_builder.add_drawing(reference, drawing, sheet)
@@ -802,15 +818,28 @@ class CreateSheets(bpy.types.Operator):
name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = scene.BIMProperties.data_dir
sheet_builder.build(sheet)
# These variables will be made available to the evaluated commands
svg = sheet_builder.build(sheet)
basename = os.path.basename(svg)
path = os.path.dirname(svg)
pdf = os.path.splitext(svg)[0] + ".pdf"
eps = os.path.splitext(svg)[0] + ".eps"
dxf = os.path.splitext(svg)[0] + ".dxf"
references = getattr(sheet, "HasDocumentReferences", getattr(sheet, "DocumentReferences", []))
if not [r for r in references if r.Description == "SHEET"]:
reference = tool.Ifc.run("document.add_reference", information=sheet)
tool.Ifc.run(
"document.edit_reference",
reference=reference,
attributes={"Location": tool.Ifc.get_relative_uri(svg), "Description": "SHEET"},
)
svg2pdf_command = context.preferences.addons["blenderbim"].preferences.svg2pdf_command
svg2dxf_command = context.preferences.addons["blenderbim"].preferences.svg2dxf_command
if svg2pdf_command:
path = os.path.join(scene.BIMProperties.data_dir, "build", name)
svg = os.path.join(path, name + ".svg")
pdf = os.path.join(path, name + ".pdf")
# With great power comes great responsibility. Example:
# [['inkscape', svg, '-o', pdf]]
commands = eval(svg2pdf_command)
@@ -818,11 +847,6 @@ class CreateSheets(bpy.types.Operator):
subprocess.run(command)
if svg2dxf_command:
path = os.path.join(scene.BIMProperties.data_dir, "build", name)
svg = os.path.join(path, name + ".svg")
eps = os.path.join(path, name + ".eps")
dxf = os.path.join(path, name + ".dxf")
base = os.path.join(path, name)
# With great power comes great responsibility. Example:
# [['inkscape', svg, '-o', eps], ['pstoedit', '-dt', '-f', 'dxf:-polyaslines -mm', eps, dxf, '-psarg', '-dNOSAFER']]
commands = eval(svg2dxf_command)
@@ -832,10 +856,32 @@ class CreateSheets(bpy.types.Operator):
if svg2pdf_command:
open_with_user_command(context.preferences.addons["blenderbim"].preferences.pdf_command, pdf)
else:
open_with_user_command(
context.preferences.addons["blenderbim"].preferences.svg_command,
os.path.join(scene.BIMProperties.data_dir, "build", name, name + ".svg"),
)
open_with_user_command(context.preferences.addons["blenderbim"].preferences.svg_command, svg)
return {"FINISHED"}
class ChangeSheetTitleBlock(bpy.types.Operator):
bl_idname = "bim.change_sheet_title_block"
bl_label = "Change Sheet Title Block"
bl_description = "Change the title block of the active sheet"
bl_options = {"REGISTER"}
def execute(self, context):
scene = context.scene
props = scene.DocProperties
if not len(props.sheets):
return {"CANCELLED"}
active_sheet = props.sheets[props.active_sheet_index]
sheet = tool.Ifc.get().by_id(active_sheet.ifc_definition_id)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = scene.BIMProperties.data_dir
titleblock = scene.DocProperties.titleblock
sheet_builder.change_titleblock(sheet, titleblock)
self.report({"INFO"}, 'Title block changed for sheet "{}" to {}'.format(sheet.Name, titleblock))
return {"FINISHED"}
@@ -843,12 +889,47 @@ class OpenView(bpy.types.Operator):
bl_idname = "bim.open_view"
bl_label = "Open View"
view: bpy.props.StringProperty()
bl_description = (
"Opens a .svg drawing based on currently active camera with default system viewer\n"
+ 'or using "svg_command" from the BlenderBIM preferences (if provided).\n\n'
+ "SHIFT+CLICK to open all drawings"
)
open_all: bpy.props.BoolProperty(name="Open All", default=False)
def invoke(self, context, event):
# opening all annotations on shift+click
if event.type == "LEFTMOUSE" and event.shift:
self.open_all = True
else:
# can't rely on default value since `self.open_all = True`
# will set the value to `True` for all future calls
self.open_all = False
return self.execute(context)
def execute(self, context):
open_with_user_command(
context.preferences.addons["blenderbim"].preferences.svg_command,
os.path.join(context.scene.BIMProperties.data_dir, "diagrams", self.view + ".svg"),
)
if self.open_all:
drawings = [tool.Ifc.get().by_id(d.ifc_definition_id) for d in context.scene.DocProperties.drawings]
else:
drawings = [tool.Ifc.get().by_id(context.scene.DocProperties.drawings.get(self.view).ifc_definition_id)]
drawing_uris = []
drawings_not_found = []
for drawing in drawings:
drawing_uri = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_document(drawing))
drawing_uris.append(drawing_uri)
if not os.path.exists(drawing_uri):
drawings_not_found.append(drawing.Name)
if drawings_not_found:
msg = "Some drawings .svg files were not found, need to print them first: \n{}.".format(
"\n".join(drawings_not_found)
)
self.report({"ERROR"}, msg)
return {"CANCELLED"}
for drawing_uri in drawing_uris:
open_with_user_command(context.preferences.addons["blenderbim"].preferences.svg_command, drawing_uri)
return {"FINISHED"}
@@ -1072,12 +1153,15 @@ class AddSchedule(bpy.types.Operator, Operator):
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ods;*.xls;*.xlsx", options={"HIDDEN"})
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
def _execute(self, context):
filepath = self.filepath
if self.use_relative_path:
filepath = os.path.relpath(filepath, bpy.path.abspath("//"))
ifc_path = tool.Ifc.get_path()
if os.path.isfile(ifc_path):
ifc_path = os.path.dirname(ifc_path)
filepath = os.path.relpath(filepath, ifc_path)
core.add_schedule(
tool.Ifc,
tool.Drawing,
@@ -1118,25 +1202,29 @@ class BuildSchedule(bpy.types.Operator, Operator):
core.build_schedule(tool.Drawing, schedule=tool.Ifc.get().by_id(self.schedule))
class AddScheduleToSheet(bpy.types.Operator):
class AddScheduleToSheet(bpy.types.Operator, Operator):
bl_idname = "bim.add_schedule_to_sheet"
bl_label = "Add Schedule To Sheet"
bl_options = {"REGISTER", "UNDO"}
# TODO: check undo redo
@classmethod
def poll(cls, context):
props = context.scene.DocProperties
return props.schedules and props.sheets and context.scene.BIMProperties.data_dir
def execute(self, context):
def _execute(self, context):
props = context.scene.DocProperties
active_schedule = props.schedules[props.active_schedule_index]
active_sheet = props.sheets[props.active_sheet_index]
schedule = tool.Ifc.get().by_id(active_schedule.ifc_definition_id)
if tool.Ifc.get_schema() == "IFC2X3":
schedule_location = tool.Drawing.get_path_with_ext(schedule.DocumentReferences[0].Location, "svg")
else:
schedule_location = tool.Drawing.get_path_with_ext(schedule.HasDocumentReferences[0].Location, "svg")
sheet = tool.Ifc.get().by_id(active_sheet.ifc_definition_id)
if not sheet.is_a("IfcDocumentInformation"):
return {"FINISHED"}
return
if tool.Ifc.get_schema() == "IFC2X3":
references = sheet.DocumentReferences or []
@@ -1145,19 +1233,25 @@ class AddScheduleToSheet(bpy.types.Operator):
has_schedule = False
for reference in references:
if reference.Location == tool.Drawing.get_schedule_location(schedule):
if reference.Location == schedule_location:
has_schedule = True
break
if has_schedule:
return {"FINISHED"}
return
reference = tool.Ifc.run("document.add_reference", information=sheet)
if tool.Ifc.get_schema() == "IFC2X3":
attributes = {"ItemReference": str(len(sheet.DocumentReferences or []))}
references = sheet.DocumentReferences
id_attr = "ItemReference"
else:
attributes = {"Identification": str(len(sheet.HasDocumentReferences or []))}
attributes["Location"] = tool.Drawing.get_schedule_location(schedule)
references = sheet.HasDocumentReferences
id_attr = "Identification"
attributes = {
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "SCHEDULE")]) + 1),
"Location": schedule_location,
"Description": "SCHEDULE",
}
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
@@ -1165,7 +1259,6 @@ class AddScheduleToSheet(bpy.types.Operator):
sheet_builder.add_schedule(reference, schedule, sheet)
tool.Drawing.import_sheets()
return {"FINISHED"}
class AddDrawingStyleAttribute(bpy.types.Operator):
@@ -1219,7 +1312,7 @@ class EditTextPopup(bpy.types.Operator):
props = context.active_object.BIMTextProperties
row = self.layout.row(align=True)
row.operator("bim.add_text_literal", icon="ADD", text="Add literal")
row.operator("bim.add_text_literal", icon="ADD", text="Add Literal")
row = self.layout.row(align=True)
row.prop(props, "font_size")
@@ -1302,7 +1395,7 @@ class DisableEditingText(bpy.types.Operator, Operator):
class AddTextLiteral(bpy.types.Operator):
bl_idname = "bim.add_text_literal"
bl_label = "Add text literal"
bl_label = "Add Text Literal"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -1337,7 +1430,7 @@ class AddTextLiteral(bpy.types.Operator):
class RemoveTextLiteral(bpy.types.Operator):
bl_idname = "bim.remove_text_literal"
bl_label = "Remove text literal"
bl_label = "Remove Text Literal"
bl_options = {"REGISTER", "UNDO"}
literal_prop_id: bpy.props.IntProperty()
@@ -1387,6 +1480,23 @@ class LoadSheets(bpy.types.Operator, Operator):
def _execute(self, context):
core.load_sheets(tool.Drawing)
props = context.scene.DocProperties
sheets_not_found = []
for sheet_prop in props.sheets:
if not sheet_prop.is_sheet:
continue
sheet = tool.Ifc.get().by_id(sheet_prop.ifc_definition_id)
document_uri = tool.Drawing.get_document_uri(sheet)
filepath = Path(document_uri)
if not filepath.is_file():
sheet_name = f"{sheet_prop.identification} - {sheet_prop.name}"
sheets_not_found.append(f'"{sheet_name}" - {document_uri}')
if sheets_not_found:
self.report({"ERROR"}, "Some sheets svg files are missing:\n" + "\n".join(sheets_not_found))
class DisableEditingSheets(bpy.types.Operator, Operator):
bl_idname = "bim.disable_editing_sheets"
@@ -250,11 +250,18 @@ class Schedule(PropertyGroup):
class Sheet(PropertyGroup):
def set_name(self, new):
old = self.get("name")
path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets")
if old and os.path.isfile(os.path.join(path, old + ".svg")):
os.rename(os.path.join(path, old + ".svg"), os.path.join(path, new + ".svg"))
if new == old:
return
sheet = tool.Ifc.get().by_id(self.ifc_definition_id)
old_path = Path(tool.Drawing.get_document_uri(sheet))
core.update_sheet_name(tool.Ifc, tool.Drawing, sheet=sheet, name=new)
self["name"] = new
new_path = Path(tool.Drawing.get_document_uri(sheet))
if old and old_path.is_file():
old_path.rename(new_path)
def get_name(self):
return self.get("name")
@@ -347,6 +354,14 @@ class DocProperties(PropertyGroup):
decorations_colour: FloatVectorProperty(
name="Decorations Colour", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
)
docs_dir: StringProperty(default=os.path.join("docs") + os.path.sep, name="Default Docs Directory")
sheets_dir: StringProperty(default=os.path.join("sheets") + os.path.sep, name="Default Sheets Directory")
titleblocks_dir: StringProperty(default=os.path.join("sheets", "titleblocks") + os.path.sep, name="Default Titleblocks Directory")
drawings_dir: StringProperty(default=os.path.join("drawings") + os.path.sep, name="Default Drawings Directory")
stylesheet_path: StringProperty(default=os.path.join("drawings", "assets", "default.css"), name="Default Stylesheet")
markers_path: StringProperty(default=os.path.join("drawings", "assets", "markers.svg"), name="Default Markers")
symbols_path: StringProperty(default=os.path.join("drawings", "assets", "symbols.svg"), name="Default Symbols")
patterns_path: StringProperty(default=os.path.join("drawings", "assets", "patterns.svg"), name="Default Patterns")
class BIMCameraProperties(PropertyGroup):
@@ -16,10 +16,177 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bgl
from mathutils import Matrix
from gpu.types import GPUShader
from gpu_extras.batch import batch_for_shader
import gpu
BASE_DEF_GLSL = """
#define PI 3.141592653589793
#define MAX_POINTS 64
#define CIRCLE_SEGS 12
#define SMOOTH_WIDTH 1.0
#define lineSmooth true
"""
# since `bgl` is deprecated, creating smoothing lines became tricky
# Notes for creating shaders with smoothed lines:
# in geom shader - use triangle_strip, DEFAULT_SETUP, do_edge_verts or do_vertex to emit vertices
# in frag shader - use lineWidth uniform, smoothline flaot in, smoothing shader code from base shader
# mind the vertex limit since emitting vertices for smoothed lines produces twice as much vertices
BASE_LIB_GLSL = """
// TODO: redefine as macor instead
uniform vec2 winsize;
uniform float lineWidth;
#define half_winsize (winsize / 2)
// convert camera to window
#define C2W(v) vec4(v.x * half_winsize.x / v.w, v.y * half_winsize.y / v.w, v.z / v.w, 1)
// convert window to camera
#define W2C(v) vec4(v.x * v.w / half_winsize.x, v.y * v.w / half_winsize.y, v.z * v.w, 1)
void emitSegment(vec4 p0, vec4 p1) {
gl_Position = p0;
EmitVertex();
gl_Position = p1;
EmitVertex();
EndPrimitive();
}
void emitTriangle(vec4 p1, vec4 p2, vec4 p3) {
gl_Position = p1;
EmitVertex();
gl_Position = p2;
EmitVertex();
gl_Position = p3;
EmitVertex();
EndPrimitive();
}
#define matCLIP2WIN() vec4(winsize.x/2, winsize.y/2, 1, 1)
#define CLIP2WIN(v) (clip2win * (v) / (v).w)
#define matWIN2CLIP() vec4(2/winsize.x, 2/winsize.y, 1, 1)
#define WIN2CLIP(v) (win2clip * (v) * (v).w)
#define DEFAULT_SETUP() vec4 clip2win = matCLIP2WIN(), win2clip = matWIN2CLIP(); vec2 EDGE_DIR
out float smoothline;
void arrow_head(in vec4 dir, in float size, in float angle, out vec4 head[3]) {
vec4 nose = dir * size;
float c = cos(angle), s = sin(angle);
head[0] = nose;
head[1] = vec4(mat2(c, -s, +s, c) * nose.xy, 0, 0);
head[2] = vec4(mat2(c, +s, -s, c) * nose.xy, 0, 0);
}
void circle_head(in float size, out vec4 head[CIRCLE_SEGS]) {
float angle_d = PI * 2 / CIRCLE_SEGS;
for(int i = 0; i<CIRCLE_SEGS; i++) {
float angle = angle_d * i;
head[i] = vec4(cos(angle), sin(angle), 0, 0) * size;
}
}
bool check_counterclockwise(in vec4 A, in vec4 B, in vec4 C) {
return (C.y-A.y) * (B.x-A.x) > (B.y-A.y) * (C.x-A.x);
}
void angle_circle_head(
in vec4 circle_start, in float circle_angle,
in bool counterclockwise,
out vec4 head[CIRCLE_SEGS+1], out float angle_segs) {
// 1 added to CIRCLE_SEGS because we're number of vertices
// for n segments is n+1
float angle_d;
angle_d = PI * 2 / CIRCLE_SEGS; // 30d
// need to bottom clamp it to 1, otherwise it causes Blender crash at extruding the curve
angle_segs = max(1, ceil(circle_angle / angle_d));
angle_d = circle_angle / angle_segs;
for(int i = 0; i < (angle_segs + 1); i++) {
float angle = angle_d * i;
if (counterclockwise) {
head[i] = vec4(
circle_start.x * cos(-angle) + circle_start.y * sin(-angle),
circle_start.x * -sin(-angle) + circle_start.y * cos(-angle),
0, 0
);
} else {
head[i] = vec4(
circle_start.x * cos(angle) + circle_start.y * sin(angle),
circle_start.x * -sin(angle) + circle_start.y * cos(angle),
0, 0
);
}
}
}
void cross_head(in vec4 dir, in float size, out vec4 head[3]) {
vec4 nose = dir * size;
float c = cos(PI/2), s = sin(PI/2);
head[0] = nose;
head[1] = vec4(mat2(c, -s, +s, c) * nose.xy, 0, 0);
head[2] = vec4(mat2(c, +s, -s, c) * nose.xy, 0, 0);
}
// do_vertex_util is custom EmitVertex method to support line smoothing
// `e` - edge xy direction in win/clip space (directions are invariant in both spaces)
#define do_vertex(pos, e) (do_vertex_util(pos, vec2(-(e).y, (e).x) / winsize.xy))
#define do_vertex_win(pos, e) ( do_vertex( WIN2CLIP( pos ), e ) )
// if vertex is shared by two segments of the line still need to emit it twice
// to avoid smoothing artifacts
// don't forget to initialize `vec2 EDGE_DIR` for macro to work
// `pos0` / `pos1` - vertex position in clip space
#define do_edge_verts(pos0, pos1) ( EDGE_DIR = normalize( ( (pos1) - (pos0) ).xy ), do_vertex( pos0, EDGE_DIR ), do_vertex( pos1, EDGE_DIR) )
#define do_edge_verts_win(pos0, pos1) ( do_edge_verts( WIN2CLIP(pos0), WIN2CLIP(pos1) ) )
void do_vertex_util(vec4 pos, vec2 ofs)
{
float final_line_width = lineWidth + SMOOTH_WIDTH * float(lineSmooth);
ofs *= final_line_width;
smoothline = final_line_width * 0.5;
gl_Position = pos;
gl_Position.xy += ofs * pos.w;
EmitVertex();
smoothline = -final_line_width * 0.5;
gl_Position = pos;
gl_Position.xy -= ofs * pos.w;
EmitVertex();
}
// geometry utils
void triangle_head(in vec4 side, in vec4 dir, in float length, in float width, in float radius, out vec4 head[5]) {
vec4 nose = side * length;
vec4 ear = dir * width;
head[0] = side * -radius;
head[1] = nose * -.5;
head[2] = vec4(0) + ear;
head[3] = nose * .5;
head[4] = side * radius;
}
void do_triangle_head(vec4 pos_w, vec4 head[5]) {
DEFAULT_SETUP();
do_edge_verts_win(pos_w + head[0], pos_w + head[1]);
do_edge_verts_win(pos_w + head[1], pos_w + head[2]);
do_edge_verts_win(pos_w + head[2], pos_w + head[3]);
do_edge_verts_win(pos_w + head[3], pos_w + head[4]);
}
void do_circle_head(vec4 pos_w, vec4 head[CIRCLE_SEGS]) {
DEFAULT_SETUP();
for(int i=0; i<CIRCLE_SEGS-1; i++) {
do_edge_verts_win(pos_w + head[i], pos_w + head[i+1]);
EmitVertex();
}
do_edge_verts_win( pos_w + head[CIRCLE_SEGS-1], pos_w + head[0] );
}
"""
class BaseShader:
@@ -28,7 +195,7 @@ class BaseShader:
The Geometry shader works in clipping coords (aftre projecting before division and window scaling).
To make window-scale geometry, vectors should be calculated in window space and than back-projected to clipping spae.
Provide `winSize` uniform vector with halfsize of window, and use W2C and C2W macros.
Provide `half_winsize` uniform vector with halfsize of window, and use W2C and C2W macros.
Replace glsl code in derived classes.
Beware of unused attributes and uniforms: glsl compiler will optimize them out and blender fail with an exception.
@@ -43,9 +210,7 @@ class BaseShader:
TYPE = None # should be LINES|POINTS|etc
DEF_GLSL = """
#define PI 3.141592653589793
"""
DEF_GLSL = BASE_DEF_GLSL
VERT_GLSL = """
uniform mat4 viewMatrix;
@@ -63,46 +228,45 @@ class BaseShader:
"""
# prepended to geom_glsl
LIB_GLSL = """
uniform vec2 winSize;
// convert camera to window
#define C2W(v) vec4(v.x * winSize.x / v.w, v.y * winSize.y / v.w, v.z / v.w, 1)
// convert window to camera
#define W2C(v) vec4(v.x * v.w / winSize.x, v.y * v.w / winSize.y, v.z * v.w, 1)
void emitSegment(vec4 p0, vec4 p1) {
gl_Position = p0;
EmitVertex();
gl_Position = p1;
EmitVertex();
EndPrimitive();
}
void emitTriangle(vec4 p1, vec4 p2, vec4 p3) {
gl_Position = p1;
EmitVertex();
gl_Position = p2;
EmitVertex();
gl_Position = p3;
EmitVertex();
EndPrimitive();
}
"""
LIB_GLSL = BASE_LIB_GLSL
GEOM_GLSL = """
uniform float viewportDrawingScale;
layout(lines) in;
layout(triangle_strip, max_vertices = 4) out;
void main() {
// default setup for macro to work
vec4 clip2win = matCLIP2WIN();
vec4 win2clip = matWIN2CLIP();
vec2 EDGE_DIR;
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
do_edge_verts(p0, p1);
EndPrimitive();
}
"""
FRAG_GLSL = """
uniform vec4 color;
uniform float lineWidth;
in float smoothline;
out vec4 fragColor;
void main() {
vec2 co = gl_FragCoord.xy;
fragColor = color;
if (lineSmooth) {
fragColor.a *= clamp((lineWidth + SMOOTH_WIDTH) * 0.5 - abs(smoothline), 0.0, 1.0);
}
}
"""
def __init__(self):
# NB: libcode arg doesn't work
# TODO: rename to .shader
self.prog = GPUShader(
vertexcode=self.VERT_GLSL,
fragcode=self.FRAG_GLSL,
@@ -120,27 +284,29 @@ class BaseShader:
self.prog.bind()
def glenable(self):
bgl.glEnable(bgl.GL_BLEND)
bgl.glBlendFunc(bgl.GL_SRC_ALPHA, bgl.GL_ONE_MINUS_SRC_ALPHA)
bgl.glBlendEquation(bgl.GL_FUNC_ADD)
# bgl.glEnable(bgl.GL_DEPTH_TEST)
# bgl.glDepthFunc(bgl.GL_LEQUAL)
# bgl.glDepthMask(True)
gpu.state.blend_set("ALPHA")
gpu.state.depth_test_set("LESS_EQUAL")
def uniform_region(self, ctx):
region = ctx.region
region3d = ctx.region_data
try:
self.prog.uniform_float("viewMatrix", region3d.perspective_matrix)
except ValueError: # unused uniform
pass
try:
self.prog.uniform_float("winSize", (region.width / 2, region.height / 2))
except ValueError: # unused uniform
pass
uniform_floats = {
"viewMatrix": region3d.perspective_matrix,
"winsize": (region.width, region.height),
"lineWidth": 2.5,
}
for name, value in uniform_floats.items():
try:
self.prog.uniform_float(name, value)
# TODO: shouldn't just try'n'catch them
# because they may indicate errors in code
except ValueError: # unused uniform
pass
# TODO: add smoothing if this shades is going to be used
class BaseLinesShader(BaseShader):
"""Draws line segments with gaps around vertices at endpoints"""
@@ -185,7 +351,6 @@ class BaseLinesShader(BaseShader):
def glenable(self):
super().glenable()
bgl.glEnable(bgl.GL_LINE_SMOOTH)
class GizmoShader(BaseShader):
@@ -207,6 +372,7 @@ class GizmoShader(BaseShader):
"""
# TODO: add smoothing if this shader is going to be used
class DotsGizmoShader(GizmoShader):
"""Draws circles of radius 1 around points"""
@@ -274,24 +440,26 @@ class ExtrusionGuidesShader(GizmoShader):
uniform mat4 ModelViewProjectionMatrix;
layout(lines) in;
layout(line_strip, max_vertices=10) out;
layout(triangle_strip, max_vertices=MAX_POINTS) out;
void main() {
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
vec4 p0w = C2W(p0), p1w = C2W(p1);
// default setup for macro to work
vec2 EDGE_DIR;
emitSegment(p0, p1);
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
do_edge_verts(p0, p1);
EndPrimitive();
vec4 bx = ModelViewProjectionMatrix[0] * CROSS_SIZE;
vec4 by = ModelViewProjectionMatrix[1] * CROSS_SIZE;
emitSegment(p0 - bx, p0 + bx);
emitSegment(p0 - by, p0 + by);
emitSegment(p1 - bx, p1 + bx);
emitSegment(p1 - by, p1 + by);
do_edge_verts(p0 - bx, p0 + bx);
EndPrimitive();
do_edge_verts(p0 - by, p0 + by);
EndPrimitive();
do_edge_verts(p1 - bx, p1 + bx);
EndPrimitive();
do_edge_verts(p1 - by, p1 + by);
EndPrimitive();
}
"""
def glenable(self):
super().glenable()
bgl.glEnable(bgl.GL_LINE_SMOOTH)
@@ -17,14 +17,15 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
import bpy
import uuid
import shutil
import ntpath
import pystache
import urllib.parse
import xml.etree.ElementTree as ET
import blenderbim.tool as tool
import ifcopenshell.util.geolocation
from shutil import copy
from xml.dom import minidom
@@ -34,21 +35,24 @@ class SheetBuilder:
self.scale = "NTS"
def create(self, sheet_path, titleblock_name):
sheet_dir = os.path.dirname(sheet_path)
root = ET.Element("svg")
root.attrib["xmlns"] = "http://www.w3.org/2000/svg"
root.attrib["xmlns:xlink"] = "http://www.w3.org/1999/xlink"
root.attrib["id"] = "root"
root.attrib["version"] = "1.1"
view_root = ET.parse(
os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
).getroot()
titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
view_root = ET.parse(titleblock_path).getroot()
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
view = ET.SubElement(root, "g")
view.attrib["data-type"] = "titleblock"
titleblock = ET.SubElement(view, "image")
titleblock.attrib["xlink:href"] = "../templates/titleblocks/" + titleblock_name + ".svg"
titleblock.attrib["xlink:href"] = os.path.relpath(
tool.Drawing.get_default_titleblock_path(titleblock_name), sheet_dir
)
titleblock.attrib["x"] = "0"
titleblock.attrib["y"] = "0"
titleblock.attrib["width"] = str(view_width)
@@ -58,17 +62,23 @@ class SheetBuilder:
root.attrib["height"] = "{}mm".format(view_height)
root.attrib["viewBox"] = "0 0 {} {}".format(view_width, view_height)
os.makedirs(sheet_dir, exist_ok=True)
os.makedirs(os.path.join(sheet_dir, "titleblocks"), exist_ok=True)
sheet_titleblock_path = os.path.join(sheet_dir, "titleblocks", titleblock_name + ".svg")
if not os.path.exists(sheet_titleblock_path):
shutil.copy(titleblock_path, sheet_titleblock_path)
with open(sheet_path, "w") as f:
f.write(minidom.parseString(ET.tostring(root)).toprettyxml(indent=" "))
def add_drawing(self, reference, drawing, sheet):
filename = drawing.Name
sheet_name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
sheet_dir = os.path.join(self.data_dir, "sheets")
drawing_dir = os.path.join(self.data_dir, "diagrams")
sheet_path = os.path.join(sheet_dir, sheet_name + ".svg")
drawing_path = os.path.join(drawing_dir, filename + ".svg")
underlay_path = os.path.join(drawing_dir, filename + "-underlay.png")
sheet_path = tool.Drawing.get_document_uri(sheet)
sheet_name = os.path.splitext(os.path.basename(sheet_path))[0]
sheet_dir = os.path.dirname(sheet_path)
drawing_path = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_reference(drawing))
drawing_path = tool.Ifc.resolve_uri(drawing_path)
underlay_path = os.path.splitext(drawing_path)[0] + "-underlay.png"
if not os.path.isfile(sheet_path):
raise FileNotFoundError
@@ -110,7 +120,7 @@ class SheetBuilder:
foreground.attrib["width"] = str(view_width)
foreground.attrib["height"] = str(view_height)
self.add_view_title(30, view_height + 35, view)
self.add_view_title(30, view_height + 35, view, sheet_dir)
sheet_tree.write(sheet_path)
def remove_drawing(self, reference, sheet):
@@ -128,11 +138,12 @@ class SheetBuilder:
sheet_tree.write(sheet_path)
def add_schedule(self, reference, schedule, sheet):
view_path = tool.Drawing.get_document_uri(schedule)
view_path = tool.Drawing.get_path_with_ext(tool.Drawing.get_document_uri(schedule), "svg")
if not os.path.exists(view_path):
tool.Drawing.create_svg_schedule(schedule)
schedule_name = os.path.splitext(os.path.basename(view_path))[0]
sheet_path = tool.Drawing.get_document_uri(sheet)
sheet_dir = os.path.dirname(sheet_path)
ET.register_namespace("", "http://www.w3.org/2000/svg")
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
@@ -152,31 +163,40 @@ class SheetBuilder:
foreground = ET.SubElement(view, "image")
foreground.attrib["data-type"] = "table"
foreground.attrib["xlink:href"] = "../schedules/{}.svg".format(schedule_name)
foreground.attrib["xlink:href"] = os.path.relpath(view_path, sheet_dir)
foreground.attrib["x"] = "30"
foreground.attrib["y"] = "30"
foreground.attrib["width"] = str(view_width)
foreground.attrib["height"] = str(view_height)
self.add_view_title(30, view_height + 35, view)
self.add_view_title(30, view_height + 35, view, sheet_dir)
sheet_tree.write(sheet_path)
def add_view_title(self, x, y, parent):
title_tree = ET.parse(os.path.join(self.data_dir, "templates", "view-title.svg"))
def add_view_title(self, x, y, parent, sheet_dir):
title_path = os.path.join(sheet_dir, "assets", "view-title.svg")
os.makedirs(os.path.dirname(title_path), exist_ok=True)
if not os.path.exists(title_path):
ootb_title = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", "view-title.svg")
shutil.copy(ootb_title, title_path)
title_tree = ET.parse(title_path)
title_root = title_tree.getroot()
title = ET.SubElement(parent, "image")
title.attrib["data-type"] = "view-title"
title.attrib["xlink:href"] = "../templates/view-title.svg"
title.attrib["xlink:href"] = os.path.relpath(title_path, sheet_dir)
title.attrib["x"] = str(x)
title.attrib["y"] = str(y)
title.attrib["width"] = str(self.convert_to_mm(title_root.attrib.get("width")))
title.attrib["height"] = str(self.convert_to_mm(title_root.attrib.get("height")))
def build(self, sheet):
sheet_name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
os.makedirs(os.path.join(self.data_dir, "build", sheet_name), exist_ok=True)
sheet_path = tool.Drawing.get_document_uri(sheet)
sheet_name = os.path.splitext(os.path.basename(sheet_path))[0]
self.sheet_dir = os.path.dirname(sheet_path)
sheet_path = os.path.join(self.data_dir, "sheets", f"{sheet_name}.svg")
docs_dir = tool.Ifc.resolve_uri(os.path.join(bpy.context.scene.DocProperties.docs_dir, sheet_name))
os.makedirs(docs_dir, exist_ok=True)
ET.register_namespace("", "http://www.w3.org/2000/svg")
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
@@ -191,9 +211,13 @@ class SheetBuilder:
self.build_drawings(root, sheet)
self.build_schedules(root)
with open(os.path.join(self.data_dir, "build", sheet_name, f"{sheet_name}.svg"), "wb") as output:
output_filename = os.path.join(docs_dir, f"{sheet_name}.svg")
with open(output_filename, "wb") as output:
tree.write(output)
return output_filename
def build_titleblock(self, root, sheet):
titleblock = root.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
image = titleblock.findall("{http://www.w3.org/2000/svg}image")[0]
@@ -233,7 +257,7 @@ class SheetBuilder:
if background is not None:
background_path = os.path.join(self.data_dir, "sheets", self.get_href(background))
copy(background_path, os.path.join(self.data_dir, "build", sheet_name))
shutil.copy(background_path, os.path.join(self.data_dir, "build", sheet_name))
if view_title is not None:
foreground_path = self.get_href(foreground)
@@ -298,7 +322,7 @@ class SheetBuilder:
self.defs.append(clip_path)
svg_path = self.get_href(image)
with open(os.path.join(self.data_dir, "sheets", svg_path), "r") as template:
with open(os.path.join(self.sheet_dir, svg_path), "r") as template:
embedded = ET.fromstring(pystache.render(template.read(), data))
# viewBox should not be nested
embedded.attrib["viewBox"] = ""
@@ -314,6 +338,33 @@ class SheetBuilder:
group.append(child)
return group
def change_titleblock(self, sheet, titleblock_name):
ET.register_namespace("", "http://www.w3.org/2000/svg")
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
titleblock_svg_path = os.path.join(self.data_dir, "templates", "titleblocks", f"{titleblock_name}.svg")
view_root = ET.parse(titleblock_svg_path).getroot()
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
sheet_dir = os.path.join(self.data_dir, "sheets")
sheet_name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
sheet_path = os.path.join(sheet_dir, sheet_name + ".svg")
sheet_tree = ET.parse(sheet_path)
root = sheet_tree.getroot()
titleblock = sheet_tree.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
image = titleblock.findall("{http://www.w3.org/2000/svg}image[@{http://www.w3.org/1999/xlink}href]")[0]
image.attrib["{http://www.w3.org/1999/xlink}href"] = f"../templates/titleblocks/{titleblock_name}.svg"
image.attrib["width"] = str(view_width)
image.attrib["height"] = str(view_height)
root.attrib["width"] = "{}mm".format(view_width)
root.attrib["height"] = "{}mm".format(view_height)
root.attrib["viewBox"] = "0 0 {} {}".format(view_width, view_height)
sheet_tree.write(sheet_path)
def convert_to_mm(self, value):
# CSS is what defines these possibilities
# https://www.w3.org/TR/SVG/refs.html#ref-css-values-3
@@ -21,6 +21,7 @@ import re
import bpy
import math
import bmesh
import shutil
import pystache
import mathutils
import xml.etree.ElementTree as ET
@@ -65,10 +66,9 @@ class SvgWriter:
self.scale = 1 / 100 # 1:100
self.camera_width = None
self.camera_height = None
self.related_paths = None
self.resource_paths = {}
def create_blank_svg(self, output_path):
self.define_related_paths() # making sure all paths are defined
self.calculate_scale()
self.svg = svgwrite.Drawing(
output_path,
@@ -94,26 +94,20 @@ class SvgWriter:
)
return self
def define_related_paths(self, **related_paths):
if not self.related_paths:
self.related_paths = {}
if not related_paths:
related_paths = {
"Stylesheet": os.path.join(self.data_dir, "styles", f"default.css"),
"Markers": os.path.join(self.data_dir, "templates", "markers.svg"),
"Symbols": os.path.join(self.data_dir, "templates", "symbols.svg"),
"Patterns": os.path.join(self.data_dir, "templates", "patterns.svg"),
}
for path_name in list(related_paths.keys()):
if path_name in self.related_paths:
del related_paths[path_name]
for path_name in related_paths:
uri = related_paths[path_name]
custom_path = tool.Ifc.resolve_uri(uri)
if custom_path:
self.related_paths[path_name] = custom_path
def setup_drawing_resource_paths(self, element):
pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing")
for resource in ("Stylesheet", "Markers", "Symbols", "Patterns"):
resource_path = pset.get(resource)
if not resource_path:
self.resource_paths[resource] = None
continue
os.makedirs(os.path.dirname(resource_path), exist_ok=True)
if not os.path.exists(resource_path):
resource_basename = os.path.basename(resource_path)
ootb_resource = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", resource_basename)
if os.path.exists(ootb_resource):
shutil.copy(ootb_resource, resource_path)
self.resource_paths[resource] = tool.Ifc.resolve_uri(resource_path)
def define_boilerplate(self):
self.add_stylesheet()
@@ -132,28 +126,36 @@ class SvgWriter:
self.height = self.raw_height * self.svg_scale
def add_stylesheet(self):
with open(self.related_paths["Stylesheet"], "r") as stylesheet:
if not self.resource_paths["Stylesheet"] or not os.path.exists(self.resource_paths["Stylesheet"]):
return
with open(self.resource_paths["Stylesheet"], "r") as stylesheet:
self.svg.defs.add(self.svg.style(stylesheet.read()))
def add_markers(self):
tree = ET.parse(self.related_paths["Markers"])
if not self.resource_paths["Markers"] or not os.path.exists(self.resource_paths["Markers"]):
return
tree = ET.parse(self.resource_paths["Markers"])
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
def add_symbols(self):
tree = ET.parse(self.related_paths["Symbols"])
if not self.resource_paths["Symbols"] or not os.path.exists(self.resource_paths["Symbols"]):
return
tree = ET.parse(self.resource_paths["Symbols"])
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
def find_xml_symbol_by_id(self, id):
tree = ET.parse(self.related_paths["Symbols"])
tree = ET.parse(self.resource_paths["Symbols"])
xml_symbol = tree.find(f'.//*[@id="{id}"]')
return External(xml_symbol) if xml_symbol else None
def add_patterns(self):
tree = ET.parse(self.related_paths["Patterns"])
if not self.resource_paths["Patterns"] or not os.path.exists(self.resource_paths["Patterns"]):
return
tree = ET.parse(self.resource_paths["Patterns"])
root = tree.getroot()
for child in root:
self.svg.defs.add(External(child))
@@ -662,7 +664,8 @@ class SvgWriter:
)
transform = "rotate({}, {}, {})".format(angle, *text_position_svg)
classes_str = " ".join(self.get_attribute_classes(text_obj))
classes = self.get_attribute_classes(text_obj)
classes_str = " ".join(classes)
symbol = tool.Drawing.get_annotation_symbol(element)
template_text_fields = []
@@ -731,12 +734,24 @@ class SvgWriter:
)
self.svg.add(text_tag)
for line_number, text_line in enumerate(text.replace("\\n", "\n").split("\n")):
text_lines = text.replace("\\n", "\n").split("\n")
# adding fill background tspan
# tspans and text doesn't support setting up "background-color"
# so we add a filter, we do it in separate span because otherwise it will produce blurry image
# adding just 1 tspan seems to work
if "fill-bg" in classes:
tspan = self.svg.tspan(
text_lines[0], class_=classes_str, insert=text_position_svg, filter="url(#fill-background)"
)
text_tag.add(tspan)
for line_number, text_line in enumerate(text_lines):
# position has to be inserted at tspan to avoid x offset between tspans
t_span = self.svg.tspan(text_line, class_=classes_str, insert=text_position_svg)
tspan = self.svg.tspan(text_line, class_=classes_str, insert=text_position_svg)
# doing it here and not in tspan constructor because constructor adds unnecessary spaces
t_span.update({"dy": f"{line_number}em"})
text_tag.add(t_span)
tspan.update({"dy": f"{line_number}em"})
text_tag.add(tspan)
def draw_break_annotations(self, obj):
x_offset = self.raw_width / 2
@@ -274,6 +274,7 @@ class BIM_PT_sheets(Panel):
row = self.layout.row(align=True)
row.prop(self.props, "titleblock", text="")
row.operator("bim.add_sheet", text="", icon="ADD")
row.operator("bim.change_sheet_title_block", text="", icon="MODIFIER_DATA")
row.operator("bim.disable_editing_sheets", text="", icon="CANCEL")
if self.props.sheets and self.props.active_sheet_index < len(self.props.sheets):
@@ -446,6 +447,7 @@ class BIM_PT_annotation_utilities(Panel):
op = row.operator("bim.add_annotation", text="Stair Arrow", icon="SCREEN_BACK")
op.object_type = "STAIR_ARROW"
op.data_type = "curve"
op.description = "Add stair arrow annotation.\nIf you have IfcStairFlight object selected, it will be used as a reference for the annotation"
op = row.operator("bim.add_annotation", text="Hidden", icon="CON_TRACKTO")
op.object_type = "HIDDEN_LINE"
op.data_type = "mesh"
@@ -470,6 +472,7 @@ class BIM_PT_annotation_utilities(Panel):
op = row.operator("bim.add_annotation", text="Batting", icon="FORCE_FORCE")
op.object_type = "BATTING"
op.data_type = "mesh"
op.description = "Add batting annotation.\nThickness could be changed through Thickness property of BBIM_Batting property set"
op = row.operator("bim.add_annotation", text="Fill Area", icon="NODE_TEXTURE")
op.object_type = "FILL_AREA"
@@ -503,26 +506,32 @@ class BIM_UL_drawinglist(bpy.types.UIList):
class BIM_UL_sheets(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
if item.is_sheet:
if item.is_expanded:
row.operator(
"bim.contract_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN"
).sheet = item.ifc_definition_id
else:
row.operator(
"bim.expand_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT"
).sheet = item.ifc_definition_id
else:
row.label(text="", icon="BLANK1")
if item.reference_type == "DRAWING":
row.label(text="", icon="IMAGE_DATA")
elif item.reference_type == "SCHEDULE":
row.label(text="", icon="LONGDISPLAY")
name = "{} - {}".format(item.identification or "X", item.name or "Unnamed")
row.label(text=name)
else:
if not item:
layout.label(text="", translate=False)
return
row = layout.row()
if item.is_sheet:
if item.is_expanded:
row.operator(
"bim.contract_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN"
).sheet = item.ifc_definition_id
else:
row.operator(
"bim.expand_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT"
).sheet = item.ifc_definition_id
name = "{} - {}".format(item.identification or "X", item.name or "Unnamed")
row.prop(item, "name", text=item.identification or "X", emboss=False)
else:
row.label(text="", icon="BLANK1")
if item.reference_type == "DRAWING":
row.label(text="", icon="IMAGE_DATA")
elif item.reference_type == "SCHEDULE":
row.label(text="", icon="LONGDISPLAY")
elif item.reference_type == "TITLEBLOCK":
row.label(text="", icon="MENU_PANEL")
if item.identification:
name = f"{item.identification} - {item.name or 'Unnamed'}"
else:
name = item.name or "Unnamed"
row.label(text=name)
@@ -27,6 +27,8 @@ classes = (
operator.OverrideDelete,
operator.OverrideDuplicateMove,
operator.OverrideDuplicateMoveLinked,
operator.OverrideDuplicateMoveLinkedMacro,
operator.OverrideDuplicateMoveMacro,
operator.OverrideJoin,
operator.OverrideModeSetEdit,
operator.OverrideModeSetObject,
@@ -52,6 +54,11 @@ addon_keymaps = []
def register():
operator.OverrideDuplicateMoveMacro.define("BIM_OT_override_object_duplicate_move")
operator.OverrideDuplicateMoveMacro.define("TRANSFORM_OT_translate")
operator.OverrideDuplicateMoveLinkedMacro.define("BIM_OT_override_object_duplicate_move_linked")
operator.OverrideDuplicateMoveLinkedMacro.define("TRANSFORM_OT_translate")
bpy.types.Object.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMObjectGeometryProperties)
bpy.types.Scene.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMGeometryProperties)
bpy.types.OBJECT_PT_transform.append(ui.BIM_PT_transform)
@@ -60,9 +67,8 @@ def register():
if wm.keyconfigs.addon:
km = wm.keyconfigs.addon.keymaps.new(name="Object Mode", space_type="EMPTY")
kmi = km.keymap_items.new("bim.override_object_join", "J", "PRESS", ctrl=True)
kmi = km.keymap_items.new("bim.override_object_duplicate_move", "D", "PRESS", shift=True)
kmi.properties.is_interactive = True
kmi = km.keymap_items.new("bim.override_object_duplicate_move_linked", "D", "PRESS", alt=True)
kmi = km.keymap_items.new("bim.override_object_duplicate_move_macro", "D", "PRESS", shift=True)
kmi = km.keymap_items.new("bim.override_object_duplicate_move_linked_macro", "D", "PRESS", alt=True)
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
kmi = km.keymap_items.new("bim.override_mode_set_edit", "TAB", "PRESS")
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS")
@@ -498,6 +498,12 @@ class OverrideOutlinerDelete(bpy.types.Operator, OverrideDeleteTrait):
return {"objects": objects, "children": children}
class OverrideDuplicateMoveMacro(bpy.types.Macro):
bl_idname = "bim.override_object_duplicate_move_macro"
bl_label = "IFC Duplicate Objects"
bl_options = {"REGISTER", "UNDO"}
class OverrideDuplicateMove(bpy.types.Operator):
bl_idname = "bim.override_object_duplicate_move"
bl_label = "IFC Duplicate Objects"
@@ -529,8 +535,6 @@ class OverrideDuplicateMove(bpy.types.Operator):
new_obj.select_set(True)
if new_active_obj:
context.view_layer.objects.active = new_active_obj
if self.is_interactive:
bpy.ops.transform.translate("INVOKE_DEFAULT")
return {"FINISHED"}
def _execute(self, context):
@@ -551,17 +555,26 @@ class OverrideDuplicateMove(bpy.types.Operator):
# Copy the actual class
new = blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
if new:
array_pset = ifcopenshell.util.element.get_pset(new, "BBIM_Array")
if array_pset:
array_pset = tool.Ifc.get().by_id(array_pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new, pset=array_pset)
old_to_new[tool.Ifc.get_entity(obj)] = [new]
# Recreate decompositions
tool.Root.recreate_decompositions(relationships, old_to_new)
if self.is_interactive:
bpy.ops.transform.translate("INVOKE_DEFAULT")
blenderbim.bim.handler.purge_module_data()
class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro):
bl_idname = "bim.override_object_duplicate_move_linked_macro"
bl_label = "IFC Duplicate Linked"
bl_options = {"REGISTER", "UNDO"}
class OverrideDuplicateMoveLinked(bpy.types.Operator):
bl_idname = "bim.override_object_duplicate_move_linked"
bl_label = "IFC Duplicate Linked"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
@@ -586,7 +599,6 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
new_obj.select_set(True)
if new_active_obj:
context.view_layer.objects.active = new_active_obj
bpy.ops.transform.translate("INVOKE_DEFAULT")
return {"FINISHED"}
def _execute(self, context):
@@ -596,6 +608,8 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
old_to_new = {}
for obj in context.selected_objects:
new_obj = obj.copy()
if obj.data:
new_obj.data = obj.data.copy()
if obj == context.active_object:
self.new_active_obj = new_obj
for collection in obj.users_collection:
@@ -605,10 +619,13 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
# Copy the actual class
new = blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
if new:
array_pset = ifcopenshell.util.element.get_pset(new, "BBIM_Array")
if array_pset:
array_pset = tool.Ifc.get().by_id(array_pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new, pset=array_pset)
old_to_new[tool.Ifc.get_entity(obj)] = new
# Recreate decompositions
tool.Root.recreate_decompositions(relationships, old_to_new)
bpy.ops.transform.translate("INVOKE_DEFAULT")
blenderbim.bim.handler.purge_module_data()
return {"FINISHED"}
@@ -616,6 +633,12 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
class OverrideJoin(bpy.types.Operator, Operator):
bl_idname = "bim.override_object_join"
bl_label = "IFC Join"
bl_options = {"REGISTER", "UNDO"}
def invoke(self, context, event):
if not tool.Ifc.get():
return bpy.ops.object.join()
return self.execute(context)
def _execute(self, context):
if not tool.Ifc.get():
@@ -703,6 +726,7 @@ class OverrideJoin(bpy.types.Operator, Operator):
class OverridePasteBuffer(bpy.types.Operator):
bl_idname = "bim.override_paste_buffer"
bl_label = "IFC Paste BIM Objects"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
bpy.ops.view3d.pastebuffer()
@@ -724,7 +748,6 @@ class OverrideModeSetEdit(bpy.types.Operator):
objs = context.selected_objects or [context.active_object]
if context.active_object:
context.active_object.select_set(True)
edited_objs = []
for obj in objs:
if not obj:
continue
@@ -742,26 +765,38 @@ class OverrideModeSetEdit(bpy.types.Operator):
obj.select_set(False)
continue
if obj.data.BIMMeshProperties.ifc_definition_id:
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
if representation.RepresentationType in ("Tessellation", "Brep"):
if element.HasOpenings:
# Mesh elements with openings must disable openings
# so that you can edit the original topology.
core.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=False,
)
obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data)
if not context.selected_objects:
return {"FINISHED"}
bpy.ops.object.mode_set(mode="EDIT", toggle=True)
representation = tool.Geometry.get_active_representation(obj)
if not representation:
continue
if tool.Geometry.is_meshlike(representation):
if getattr(element, "HasOpenings", None):
# Mesh elements with openings must disable openings
# so that you can edit the original topology.
core.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=False,
)
obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data)
else:
obj.select_set(False)
continue
if not context.selected_objects or len(context.selected_objects) != len(objs):
# We are trying to edit at least one non-mesh-like object : Display a hint to the user
self.report({"INFO"}, "Only mesh-compatible representations may be edited in edit mode.")
if context.active_object not in context.selected_objects:
# The active object is non-mesh-like. Set a valid object (or None) as active
context.view_layer.objects.active = context.selected_objects[0] if context.selected_objects else None
if context.active_object:
bpy.ops.object.mode_set(mode="EDIT", toggle=True)
return {"FINISHED"}
def invoke(self, context, event):
@@ -825,14 +860,13 @@ class OverrideModeSetObject(bpy.types.Operator):
continue
element = tool.Ifc.get_entity(obj)
if not element:
if not element or element.is_a("IfcAnnotation"):
continue
if obj.data.BIMMeshProperties.ifc_definition_id:
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
if representation.RepresentationType in (
"Tessellation",
"Brep",
if tool.Geometry.is_meshlike(
representation
) and obj.data.BIMMeshProperties.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data):
self.edited_objs.append(obj)
elif element.HasOpenings:
@@ -49,7 +49,7 @@ class BIM_OT_cityjson2ifc(Operator):
class BIM_OT_find_cityjson_lod(Operator):
bl_idname = "bim.find_cityjson_lod"
bl_label = "Find LODs in CityJSON file"
bl_label = "Find LODs in CityJSON File"
bl_context = "scene"
def execute(self, context):
@@ -24,6 +24,7 @@ import ifcopenshell.util.attribute
import ifcopenshell.util.representation
import blenderbim.bim.helper
import blenderbim.tool as tool
import blenderbim.core.style
import blenderbim.core.material as core
import blenderbim.bim.module.model.profile as model_profile
from blenderbim.bim.module.material.prop import purge as material_prop_purge
@@ -666,8 +667,12 @@ class CopyMaterial(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
blender_material = context.active_object.active_material
material = tool.Ifc.get_entity(blender_material)
if tool.Ifc.has_changed_shading(blender_material):
blenderbim.core.style.update_style_colours(tool.Ifc, tool.Style, obj=blender_material)
copied_material = ifcopenshell.api.run("material.copy_material", tool.Ifc.get(), material=material)
copied_blender_material = bpy.data.materials.new(blender_material.name)
copied_blender_material = blender_material.copy()
copied_style = self.get_style(copied_material)
tool.Ifc.link(copied_material, copied_blender_material)
if copied_style:
@@ -47,6 +47,9 @@ classes = (
array.EnableEditingArray,
array.RemoveArray,
array.SelectArrayParent,
array.Input3DCursorXArray,
array.Input3DCursorYArray,
array.Input3DCursorZArray,
product.AddConstrTypeInstance,
product.AddEmptyType,
product.AlignProduct,
@@ -22,7 +22,7 @@ import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
import blenderbim.tool as tool
from mathutils import Vector
from mathutils import Vector, Matrix
class AddArray(bpy.types.Operator, tool.Ifc.Operator):
@@ -34,7 +34,16 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
array = {"children": [], "count": 1, "x": 0.0, "y": 0.0, "z": 0.0, "use_local_space": True}
array = {
"children": [],
"count": 1,
"x": 0.0,
"y": 0.0,
"z": 0.0,
"use_local_space": True,
"sync_children": False,
"dimension_input_type": "Increment",
}
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
@@ -81,6 +90,8 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
props.y = data["y"]
props.z = data["z"]
props.use_local_space = data.get("use_local_space", False)
props.sync_children = data.get("sync_children", False)
props.dimension_input_type = data.get("dimension_input_type", "Increment")
props.is_editing = self.item
return {"FINISHED"}
@@ -105,6 +116,8 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator):
"y": props.y,
"z": props.z,
"use_local_space": props.use_local_space,
"sync_children": props.sync_children,
"dimension_input_type": props.dimension_input_type,
}
props.is_editing = -1
@@ -173,3 +186,48 @@ class SelectArrayParent(bpy.types.Operator):
context.view_layer.objects.active = obj
obj.select_set(True)
return {"FINISHED"}
class Input3DCursorXArray(bpy.types.Operator):
bl_idname = "bim.input_cursor_x_array"
bl_label = "Get 3d Cursor X Input for Array"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
props = obj.BIMArrayProperties
cursor = context.scene.cursor
if props.use_local_space:
props.x = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.col[3]).x
else:
props.x = cursor.location.x - obj.location.x
return {"FINISHED"}
class Input3DCursorYArray(bpy.types.Operator):
bl_idname = "bim.input_cursor_y_array"
bl_label = "Get 3d Cursor Y Input for Array"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
props = obj.BIMArrayProperties
cursor = context.scene.cursor
if props.use_local_space:
props.y = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.col[3]).y
else:
props.y = cursor.location.y - obj.location.y
return {"FINISHED"}
class Input3DCursorZArray(bpy.types.Operator):
bl_idname = "bim.input_cursor_z_array"
bl_label = "Get 3d Cursor Z Input for Array"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
props = obj.BIMArrayProperties
cursor = context.scene.cursor
if props.use_local_space:
props.z = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.col[3]).z
else:
props.z = cursor.location.z - obj.location.z
return {"FINISHED"}
@@ -64,6 +64,7 @@ class AuthoringData:
cls.data["has_visible_openings"] = cls.has_visible_openings()
cls.data["active_class"] = cls.active_class()
cls.data["active_material_usage"] = cls.active_material_usage()
cls.data["active_representation_type"] = cls.active_representation_type()
@classmethod
def type_class(cls):
@@ -187,16 +188,24 @@ class AuthoringData:
if element:
return tool.Model.get_usage_type(element)
@classmethod
def active_representation_type(cls):
if bpy.context.active_object:
representation = tool.Geometry.get_active_representation(bpy.context.active_object)
if representation:
return representation.RepresentationType
@classmethod
def ifc_classes(cls):
results = []
classes = {
e.is_a()
for e in tool.Ifc.get().by_type("IfcElementType")
+ tool.Ifc.get().by_type("IfcDoorStyle")
+ tool.Ifc.get().by_type("IfcWindowStyle")
+ tool.Ifc.get().by_type("IfcSpaceType")
e.is_a() for e in (tool.Ifc.get().by_type("IfcElementType") + tool.Ifc.get().by_type("IfcSpaceType"))
}
if tool.Ifc.get_schema() in ("IFC2X3", "IFC4"):
classes.update(
{e.is_a() for e in (tool.Ifc.get().by_type("IfcDoorStyle") + tool.Ifc.get().by_type("IfcWindowStyle"))}
)
results.extend([(c, c, "") for c in sorted(classes)])
return results
@@ -17,18 +17,16 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import gpu
import bgl
import bmesh
import blenderbim.tool as tool
from math import pi, degrees, sin, cos, radians
from math import sin, cos, radians
from bpy.types import SpaceView3D
from mathutils import Vector, Matrix
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
from gpu_extras.batch import batch_for_shader
white = (1, 1, 1, 1)
lightgrey = (0.7, 0.7, 0.7, 1)
white_t = (1, 1, 1, 0.5)
green = (0.545, 0.863, 0, 1)
red = (1, 0.2, 0.322, 1)
blue = (0.157, 0.565, 1, 1)
@@ -71,13 +69,11 @@ class ProfileDecorator:
pass
cls.installed = None
def create_batch(self, shader_type, content_pos, color, indices=None, bind=True):
batch = batch_for_shader(self.shader, shader_type, {"pos": content_pos}, indices=indices)
# TODO: what's bind is for?
if bind:
self.shader.bind()
self.shader.uniform_float("color", color)
batch.draw(self.shader)
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def draw_faces(self, bm, vertices_coords):
"""mutates original bm (triangulates it)
@@ -87,7 +83,7 @@ class ProfileDecorator:
bmesh.ops.triangulate(traingulated_bm, faces=traingulated_bm.faces)
face_indices = [[v.index for v in f.verts] for f in traingulated_bm.faces]
self.create_batch("TRIS", vertices_coords, faces_color, face_indices)
self.draw_batch("TRIS", vertices_coords, faces_color, face_indices)
def __call__(self, context, get_custom_bmesh=None, draw_faces=False, exit_edit_mode_callback=None):
obj = context.active_object
@@ -103,20 +99,8 @@ class ProfileDecorator:
else:
bm = bmesh.from_edit_mesh(obj.data)
def gl_init(use_bgl=False):
# TODO: remove as deprecated?
if use_bgl:
bgl.glLineWidth(2)
bgl.glPointSize(6)
bgl.glEnable(bgl.GL_BLEND)
bgl.glEnable(bgl.GL_LINE_SMOOTH)
else:
gpu.state.line_width_set(2)
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
bgl.glEnable(bgl.GL_LINE_SMOOTH)
gl_init(True)
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
### Actually drawing
all_vertices = []
@@ -202,22 +186,31 @@ class ProfileDecorator:
unselected_edges.append(edge_indices)
### Actually drawing
# 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
self.line_shader.bind()
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
self.shader.bind()
# Draw faces
if draw_faces:
self.draw_faces(bm, all_vertices)
self.create_batch("LINES", all_vertices, lightgrey, unselected_edges)
self.create_batch("LINES", all_vertices, green, selected_edges)
self.create_batch("LINES", all_vertices, grey, arc_edges)
self.create_batch("LINES", all_vertices, preview_edges_color, preview_edges)
self.create_batch("LINES", all_vertices, blue, roof_angle_edges)
self.draw_batch("LINES", all_vertices, white_t, unselected_edges)
self.draw_batch("LINES", all_vertices, green, selected_edges)
self.draw_batch("LINES", all_vertices, grey, arc_edges)
self.draw_batch("LINES", all_vertices, preview_edges_color, preview_edges)
self.draw_batch("LINES", all_vertices, blue, roof_angle_edges)
self.create_batch("POINTS", unselected_vertices, lightgrey)
self.create_batch("POINTS", error_vertices, red)
self.create_batch("POINTS", special_vertices, blue)
self.create_batch("POINTS", selected_vertices, green)
self.draw_batch("POINTS", unselected_vertices, white_t)
self.draw_batch("POINTS", error_vertices, red)
self.draw_batch("POINTS", special_vertices, blue)
self.draw_batch("POINTS", selected_vertices, green)
# Draw arcs
arc_centroids = []
@@ -242,9 +235,9 @@ class ProfileDecorator:
arc_centroids.append(tuple(centroid))
arc_segments.append(tool.Cad.create_arc_segments(pts=points, num_verts=17, make_edges=True))
self.create_batch("POINTS", arc_centroids, grey, bind=False)
self.draw_batch("POINTS", arc_centroids, grey)
for verts, edges in arc_segments:
self.create_batch("LINES", verts, blue, edges, bind=False)
self.draw_batch("LINES", verts, blue, edges)
# Draw circles
circle_centroids = []
@@ -263,9 +256,9 @@ class ProfileDecorator:
segments = [[list(matrix @ Vector(v)) for v in segments[0]], segments[1]]
circle_segments.append(segments)
self.create_batch("POINTS", circle_centroids, grey, bind=False)
self.draw_batch("POINTS", circle_centroids, grey)
for verts, edges in circle_segments:
self.create_batch("LINES", verts, blue, edges, bind=False)
self.draw_batch("LINES", verts, blue, edges)
def create_matrix(self, p, x, y, z):
return Matrix([x, y, z, p]).to_4x4().transposed()
@@ -43,6 +43,7 @@ def update_door_modifier_representation(context):
props = obj.BIMDoorProperties
element = tool.Ifc.get_entity(obj)
ifc_file = tool.Ifc.get()
sliding_door = "SLIDING" in props.door_type
representation_data = {
"operation_type": props.door_type,
@@ -87,13 +88,37 @@ def update_door_modifier_representation(context):
model_representation = ifcopenshell.api.run("geometry.add_door_representation", ifc_file, **representation_data)
tool.Model.replace_object_ifc_representation(body, obj, model_representation)
# PLAN_VIEW representation
plan = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "Body", "PLAN_VIEW")
if plan:
representation_data["context"] = plan
# Body/PLAN_VIEW representation
plan_body = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "Body", "PLAN_VIEW")
if plan_body:
representation_data["context"] = plan_body
plan_representation = ifcopenshell.api.run("geometry.add_door_representation", ifc_file, **representation_data)
tool.Model.replace_object_ifc_representation(plan, obj, plan_representation)
tool.Model.replace_object_ifc_representation(plan_body, obj, plan_representation)
# Annotation/PLAN_VIEW representation
plan_annotation = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "Annotation", "PLAN_VIEW")
if plan_annotation:
if not sliding_door:
# only sliding doors have Annotation/PLAN_VIEW
# for other types we just check for old representation and remove it if it's there
old_representation = ifcopenshell.util.representation.get_representation(
element, "Plan", "Annotation", "PLAN_VIEW"
)
if old_representation:
core.remove_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=old_representation,
)
else:
representation_data["context"] = plan_annotation
plan_representation = ifcopenshell.api.run(
"geometry.add_door_representation", ifc_file, **representation_data
)
tool.Model.replace_object_ifc_representation(plan_annotation, obj, plan_representation)
if plan_body or plan_annotation:
# adding switch representation at the end instead of changing order of representations
# to prevent #2744
core.switch_representation(
@@ -263,13 +288,17 @@ def update_door_modifier_bmesh(context):
door_type = props.door_type
double_swing_door = "DOUBLE_SWING" in door_type
double_door = "DOUBLE_DOOR" in door_type
sliding_door = "SLIDING" in door_type
# lining params
lining_depth = props.lining_depth * si_conversion
lining_thickness_default = props.lining_thickness * si_conversion
lining_offset = props.lining_offset * si_conversion
lining_to_panel_offset_x = props.lining_to_panel_offset_x * si_conversion
lining_to_panel_offset_y = props.lining_to_panel_offset_y * si_conversion
lining_to_panel_offset_x = (
props.lining_to_panel_offset_x * si_conversion if not sliding_door else lining_thickness_default
)
panel_depth = props.panel_depth * si_conversion
lining_to_panel_offset_y = props.lining_to_panel_offset_y * si_conversion if not sliding_door else -panel_depth
transom_thickness = props.transom_thickness * si_conversion / 2
transfom_offset = props.transom_offset * si_conversion
@@ -278,10 +307,10 @@ def update_door_modifier_bmesh(context):
window_lining_height = overall_height - transfom_offset - transom_thickness
side_lining_thickness = lining_thickness_default
panel_lining_overlap_x = max(lining_thickness_default - lining_to_panel_offset_x, 0)
panel_lining_overlap_x = max(lining_thickness_default - lining_to_panel_offset_x, 0) if not sliding_door else 0
top_lining_thickness = transom_thickness or lining_thickness_default
panel_top_lining_overlap_x = max(top_lining_thickness - lining_to_panel_offset_x, 0)
panel_top_lining_overlap_x = max(top_lining_thickness - lining_to_panel_offset_x, 0) if not sliding_door else 0
door_opening_width = overall_width - lining_to_panel_offset_x * 2
if double_swing_door:
side_lining_thickness = side_lining_thickness - panel_lining_overlap_x
@@ -296,7 +325,6 @@ def update_door_modifier_bmesh(context):
casing_depth = props.casing_depth * si_conversion
# panel params
panel_depth = props.panel_depth * si_conversion
panel_width = door_opening_width * props.panel_width_ratio
frame_depth = props.frame_depth * si_conversion
frame_thickness = props.frame_thickness * si_conversion
@@ -389,7 +417,7 @@ def update_door_modifier_bmesh(context):
panel_position = V(lining_to_panel_offset_x, lining_to_panel_offset_y, threshold_thickness)
if double_door:
# TODO: keep a little space between doors for readibility?
# keeping a little space between doors for readibility
double_door_offset = 0.001 * si_conversion
panel_size.x = panel_size.x / 2 - double_door_offset
door_verts.extend(create_bm_door_panel(panel_size, panel_position, "LEFT"))
@@ -516,7 +544,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_door"
bl_label = "Cancel editing Door"
bl_label = "Cancel Editing Door"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -545,7 +573,7 @@ class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_door"
bl_label = "Finish editing door"
bl_label = "Finish Editing Door"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -60,9 +60,6 @@ def load_post(*args):
product.regenerate_profile_usage,
)
IfcStore.add_element_listener(wall.element_listener)
IfcStore.add_element_listener(profile.element_listener)
ifcopenshell.api.add_post_listener(
"geometry.add_representation", "BlenderBIM.DumbWall.CalculateQuantities", wall.calculate_quantities
)
@@ -18,7 +18,6 @@
import bpy
import gpu
import bgl
import bmesh
import logging
import numpy as np
@@ -36,7 +35,6 @@ from bpy.types import Operator
from bpy.types import SpaceView3D
from bpy.props import FloatProperty
from bpy_extras.object_utils import AddObjectHelper, object_data_add
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
from gpu_extras.batch import batch_for_shader
@@ -217,8 +215,9 @@ class FilledOpeningGenerator:
def generate_opening_from_filling(self, filling, filling_obj, voided_obj):
thickness = voided_obj.dimensions[1] + 0.1 + 0.1
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
shape_builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
ifc_file = tool.Ifc.get()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
shape_builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
profile = None
filling_type = ifcopenshell.util.element.get_type(filling)
@@ -227,47 +226,59 @@ class FilledOpeningGenerator:
filling_type, "Model", "Profile", "ELEVATION_VIEW"
)
filling_obj = tool.Ifc.get_object(filling_type)
context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
if profile:
curve_3d = ifcopenshell.util.representation.resolve_representation(profile).Items[0]
def get_curve_2d_from_3d(curve_3d):
ifc_segments = [shape_builder.deep_copy(s) for s in curve_3d.Segments]
ifc_points = ifc_file.createIfcCartesianPointList2D([Vector(p).xz for p in curve_3d.Points.CoordList])
ifc_curve = ifc_file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
return ifc_curve
extrusion = shape_builder.extrude(
ifcopenshell.util.representation.resolve_representation(profile).Items[0],
get_curve_2d_from_3d(curve_3d),
magnitude=thickness / unit_scale,
position=Vector([0.0, -0.1 / unit_scale, 0.0]),
extrusion_vector=Vector([0.0, 1.0, 0.0]),
position_x_axis=Vector((1, 0, 0)),
position_z_axis=Vector((0, -1, 0)),
extrusion_vector=Vector((0, 0, -1)),
)
return shape_builder.get_representation(context, [extrusion])
x, y, z = filling_obj.dimensions
opening_position = Vector([0.0, -0.1 / unit_scale, 0.0])
opening_size = Vector([x, 0, z]) / unit_scale
opening_size = Vector([x, z]) / unit_scale
# Windows and doors can have a casing that overlaps the wall
# but shouldn't affect the size of the opening.
# So we shouldn't use object dimensions in that case. More: #2784
# Just keeping it for windows and doors for now to be safe
x_redefined, z_redefined = False, False
has_width_attribute, has_height_attribute = False, False
if filling.is_a() in ["IfcWindow", "IfcDoor"]:
if filling.OverallWidth:
opening_size.x = filling.OverallWidth
x_redefined = True
has_width_attribute = True
if filling.OverallHeight:
opening_size.z = filling.OverallHeight
z_redefined = True
opening_size.y = filling.OverallHeight
has_height_attribute = True
# making sure if min_x or min_z != 0 to shift the opening accordingly
# to prevent something like #2784
if not x_redefined:
if not has_width_attribute:
opening_position.x = min(v[0] for v in filling_obj.bound_box)
if not z_redefined:
if not has_height_attribute:
opening_position.z = min(v[2] for v in filling_obj.bound_box)
extrusion = shape_builder.extrude(
shape_builder.rectangle(size=opening_size),
magnitude=thickness / unit_scale,
position=opening_position,
extrusion_vector=Vector([0.0, 1.0, 0.0]),
position_z_axis=Vector((0.0, -1.0, 0.0)),
position_x_axis=Vector((1.0, 0.0, 0.0)),
extrusion_vector=Vector((0.0, 0.0, -1.0)),
)
return shape_builder.get_representation(context, [extrusion])
@@ -709,13 +720,12 @@ class EditOpenings(Operator, tool.Ifc.Operator):
if opening_obj:
if tool.Ifc.is_edited(opening_obj):
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
building_objs.add(obj)
building_objs.update(self.get_all_building_objects_of_similar_openings(opening))
elif tool.Ifc.is_moved(opening_obj):
blenderbim.core.geometry.edit_object_placement(
tool.Ifc, tool.Geometry, tool.Surveyor, obj=opening_obj
)
building_objs.add(obj)
building_objs.add(obj)
building_objs.update(self.get_all_building_objects_of_similar_openings(opening))
tool.Ifc.unlink(element=opening, obj=opening_obj)
bpy.data.objects.remove(opening_obj)
@@ -758,7 +768,7 @@ class EditOpenings(Operator, tool.Ifc.Operator):
results.add(obj)
return results
# TODO: merge with ProfileDecorator?
class DecorationsHandler:
installed = None
@@ -777,11 +787,15 @@ class DecorationsHandler:
pass
cls.installed = None
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def __call__(self, context):
bgl.glLineWidth(2)
bgl.glPointSize(6)
bgl.glEnable(bgl.GL_BLEND)
bgl.glEnable(bgl.GL_LINE_SMOOTH)
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
for opening in context.scene.BIMModelProperties.openings:
obj = opening.obj
@@ -790,7 +804,7 @@ class DecorationsHandler:
continue
white = (1, 1, 1, 1)
white_t = (1, 1, 1, 0.1)
white_t = (1, 1, 1, 0.5)
green = (0.545, 0.863, 0, 1)
red = (1, 0.2, 0.322, 1)
red_t = (1, 0.2, 0.322, 0.1)
@@ -798,6 +812,13 @@ class DecorationsHandler:
blue_t = (0.157, 0.565, 1, 0.1)
grey = (0.2, 0.2, 0.2, 1)
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
self.line_shader.bind() # required to be able to change uniforms of the shader
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
verts = []
@@ -829,41 +850,21 @@ class DecorationsHandler:
else:
unselected_edges.append(edge_indices)
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=unselected_edges)
self.shader.bind()
self.shader.uniform_float("color", white)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=selected_edges)
self.shader.uniform_float("color", green)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": unselected_vertices})
self.shader.uniform_float("color", white)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": selected_vertices})
self.shader.uniform_float("color", green)
batch.draw(self.shader)
self.draw_batch("LINES", verts, white_t, unselected_edges)
self.draw_batch("LINES", verts, green, selected_edges)
self.draw_batch("POINTS", unselected_vertices, white)
self.draw_batch("POINTS", selected_vertices, green)
else:
bm = bmesh.new()
bm.from_mesh(obj.data)
verts = [tuple(obj.matrix_world @ v.co) for v in bm.verts]
edges = [tuple([v.index for v in e.verts]) for e in bm.edges]
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
self.shader.bind()
self.shader.uniform_float("color", green if obj in context.selected_objects else blue)
batch.draw(self.shader)
self.draw_batch("LINES", verts, green if obj in context.selected_objects else blue, edges)
obj.data.calc_loop_triangles()
tris = [tuple(t.vertices) for t in obj.data.loop_triangles]
batch = batch_for_shader(self.shader, "TRIS", {"pos": verts}, indices=tris)
self.shader.bind()
self.shader.uniform_float("color", blue_t)
batch.draw(self.shader)
self.draw_batch("TRIS", verts, blue_t, tris)
if "HalfSpaceSolid" in obj.name:
# Arrow shape
@@ -876,10 +877,7 @@ class DecorationsHandler:
tuple(obj.matrix_world @ Vector((0, -0.05, 0.45))),
]
edges = [(0, 1), (1, 2), (1, 3), (1, 4), (1, 5)]
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
self.shader.bind()
self.shader.uniform_float("color", green if obj in context.selected_objects else blue)
batch.draw(self.shader)
self.draw_batch("LINES", verts, green if obj in context.selected_objects else blue, edges)
if obj.mode != "EDIT":
bm.free()
@@ -402,7 +402,7 @@ class MirrorElements(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.mirror_elements"
bl_label = "Mirror Elements"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Faux-mirrors the selected object by an active empty along a mirror plane."
bl_description = "Faux-mirrors the selected objects by an active empty along a mirror plane"
@classmethod
def poll(cls, context):
@@ -429,6 +429,13 @@ class MirrorElements(bpy.types.Operator, tool.Ifc.Operator):
mirror.select_set(False)
if not context.selected_objects:
self.report(
{"INFO"},
"At least two objects must be selected: an object to be mirrored, and a mirror axis as the active object.",
)
return {"FINISHED"}
bpy.ops.bim.override_object_duplicate_move(is_interactive=False)
for obj in context.selected_objects:
@@ -31,64 +31,10 @@ import blenderbim.core.geometry
from math import pi, degrees, inf
from mathutils import Vector, Matrix, Quaternion
from blenderbim.bim.module.geometry.helper import Helper
from blenderbim.bim.module.model.wall import DumbWallRecalculator
from blenderbim.bim.module.model.decorator import ProfileDecorator
def element_listener(element, obj):
blenderbim.bim.handler.subscribe_to(obj, "mode", mode_callback)
def mode_callback(obj, data):
for obj in set(bpy.context.selected_objects + [bpy.context.active_object]):
if (
not obj.data
or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve))
or not obj.BIMObjectProperties.ifc_definition_id
or not bpy.context.scene.BIMProjectProperties.is_authoring
):
return
product = tool.Ifc.get().by_id(obj.BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbProfile":
return
if obj.mode == "EDIT":
tool.Ifc.edit(obj)
bm = bmesh.from_edit_mesh(obj.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.update_edit_mesh(obj.data)
else:
material_usage = ifcopenshell.util.element.get_material(product)
x, y = obj.dimensions[0:2]
if not material_usage.CardinalPoint:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, y, 0)) / 2))
elif material_usage.CardinalPoint == 1:
new_origin = obj.matrix_world @ Vector(obj.bound_box[4])
elif material_usage.CardinalPoint == 2:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, 0, 0)) / 2))
elif material_usage.CardinalPoint == 3:
new_origin = obj.matrix_world @ Vector(obj.bound_box[0])
elif material_usage.CardinalPoint == 4:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[4]) + (Vector((0, y, 0)) / 2))
elif material_usage.CardinalPoint == 5:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, y, 0)) / 2))
elif material_usage.CardinalPoint == 6:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((0, y, 0)) / 2))
elif material_usage.CardinalPoint == 7:
new_origin = obj.matrix_world @ Vector(obj.bound_box[7])
elif material_usage.CardinalPoint == 8:
new_origin = obj.matrix_world @ (Vector(obj.bound_box[3]) + (Vector((x, 0, 0)) / 2))
elif material_usage.CardinalPoint == 9:
new_origin = obj.matrix_world @ Vector(obj.bound_box[3])
if (obj.matrix_world.translation - new_origin).length < 0.001:
return
obj.data.transform(
Matrix.Translation(
(obj.matrix_world.inverted().to_quaternion() @ (obj.matrix_world.translation - new_origin))
)
)
obj.matrix_world.translation = new_origin
class DumbProfileGenerator:
def __init__(self, relating_type):
self.relating_type = relating_type
@@ -892,15 +838,24 @@ class Rotate90(bpy.types.Operator, tool.Ifc.Operator):
return context.selected_objects
def _execute(self, context):
objs = []
profile_objs = []
layer2_objs = []
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
usage = tool.Model.get_usage_type(element)
if usage == "PROFILE":
profile_objs.append(obj)
elif usage == "LAYER2":
layer2_objs.append(obj)
if element.ConnectedTo or element.ConnectedFrom:
objs.append(obj)
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATSTART")
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATEND")
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATPATH")
rotate_matrix = Matrix.Rotation(pi / 2, 4, self.axis)
obj.matrix_world @= rotate_matrix
bpy.context.view_layer.update()
DumbProfileRecalculator().recalculate(objs)
DumbProfileRecalculator().recalculate(profile_objs)
DumbWallRecalculator().recalculate(layer2_objs)
return {"FINISHED"}
@@ -252,7 +252,7 @@ class BIMArrayProperties(PropertyGroup):
is_editing: bpy.props.IntProperty(
default=-1, description="Currently edited array index. -1 if not in array editing mode."
)
count: bpy.props.IntProperty(name="Count", default=0)
count: bpy.props.IntProperty(name="Count", default=0, min=0)
x: bpy.props.FloatProperty(name="X", default=0)
y: bpy.props.FloatProperty(name="Y", default=0)
z: bpy.props.FloatProperty(name="Z", default=0)
@@ -261,6 +261,19 @@ class BIMArrayProperties(PropertyGroup):
description="Use local space for array items offset instead of world space",
default=True,
)
dimension_input_type: bpy.props.EnumProperty(
items=(
("Increment", "Increment", ""),
("Total", "Total", ""),
),
name="Type of input dimension",
default="Increment",
)
sync_children: bpy.props.BoolProperty(
name="Sync Children",
description="Keep children objects in sync with parent object",
default = False,
)
class BIMStairProperties(PropertyGroup):
@@ -455,7 +468,9 @@ class BIMDoorProperties(PropertyGroup):
("DOUBLE_SWING_LEFT", "DOUBLE_SWING_LEFT", ""),
("DOUBLE_SWING_RIGHT", "DOUBLE_SWING_RIGHT", ""),
("DOUBLE_DOOR_SINGLE_SWING", "DOUBLE_DOOR_SINGLE_SWING", ""),
("DOUBLE_DOOR_DOUBLE_SWING", "DOUBLE_DOOR_DOUBLE_SWING", ""),
("SLIDING_TO_LEFT", "SLIDING_TO_LEFT", ""),
("SLIDING_TO_RIGHT", "SLIDING_TO_RIGHT", ""),
("DOUBLE_DOOR_SLIDING", "DOUBLE_DOOR_SLIDING", ""),
)
door_added_previously: bpy.props.BoolProperty(default=False)
@@ -525,10 +540,12 @@ class BIMDoorProperties(PropertyGroup):
"lining_depth": self.lining_depth,
"lining_thickness": self.lining_thickness,
"lining_offset": self.lining_offset,
"lining_to_panel_offset_x": self.lining_to_panel_offset_x,
"lining_to_panel_offset_y": self.lining_to_panel_offset_y,
}
if "SLIDING" not in self.door_type:
kwargs["lining_to_panel_offset_x"] = self.lining_to_panel_offset_x
kwargs["lining_to_panel_offset_y"] = self.lining_to_panel_offset_y
kwargs["transom_thickness"] = self.transom_thickness
if self.transom_thickness:
kwargs["transom_offset"] = self.transom_offset
@@ -299,7 +299,7 @@ class EnableEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
class CancelEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_railing"
bl_label = "Cancel editing Railing"
bl_label = "Cancel Editing Railing"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -328,7 +328,7 @@ class CancelEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
class FinishEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_railing"
bl_label = "Finish editing railing"
bl_label = "Finish Editing Railing"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -469,7 +469,7 @@ class EnableEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
class CancelEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_roof"
bl_label = "Cancel editing Roof"
bl_label = "Cancel Editing Roof"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -498,7 +498,7 @@ class CancelEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
class FinishEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_roof"
bl_label = "Finish editing roof"
bl_label = "Finish Editing Roof"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -633,7 +633,7 @@ class RemoveRoof(bpy.types.Operator, tool.Ifc.Operator):
class SetGableRoofEdgeAngle(bpy.types.Operator):
bl_idname = "bim.set_gable_roof_edge_angle"
bl_label = "Set gable roof edge angle"
bl_label = "Set Gable Roof Edge Angle"
bl_options = {"REGISTER", "UNDO"}
angle: bpy.props.FloatProperty(name="Angle", default=90)
@@ -604,6 +604,7 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
body = ifcopenshell.util.representation.resolve_representation(body)
extrusion = tool.Model.get_extrusion(body)
if extrusion.Position:
@@ -636,8 +637,9 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
extrusion = tool.Model.get_extrusion(representation)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
body = ifcopenshell.util.representation.resolve_representation(body)
extrusion = tool.Model.get_extrusion(body)
if extrusion.Position:
position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist())
position[0][3] *= self.unit_scale
@@ -670,13 +672,17 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
bpy.data.meshes.remove(profile_mesh)
# Only certain classes should have a footprint
if element.is_a() not in ("IfcSlab", "IfcRamp"):
return {"FINISHED"}
footprint_context = ifcopenshell.util.representation.get_context(
tool.Ifc.get(), "Plan", "FootPrint", "SKETCH_VIEW"
)
@@ -287,6 +287,22 @@ def update_ifc_stair_props(obj):
)
tool.Ifc.edit(obj)
# update related annotation objects
def get_elements_from_product(product):
elements = []
for rel in product.ReferencedBy:
if not rel.is_a("IfcRelAssignsToProduct"):
continue
elements.extend(rel.RelatedObjects)
return elements
stair_obj = obj
for rel_element in get_elements_from_product(element):
if not rel_element.is_a("IfcAnnotation") or rel_element.ObjectType != "STAIR_ARROW":
continue
if annotation_obj := tool.Ifc.get_object(rel_element):
tool.Drawing.setup_annotation_object(annotation_obj, "STAIR_ARROW", stair_obj)
class BIM_OT_add_clever_stair(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "mesh.add_clever_stair"
@@ -368,7 +384,7 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_stair"
bl_label = "Cancel editing Stair"
bl_label = "Cancel Editing Stair"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -388,7 +404,7 @@ class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_stair"
bl_label = "Finish editing stair"
bl_label = "Finish Editing Stair"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -182,7 +182,7 @@ class UpdateDataFromSverchok(bpy.types.Operator, tool.Ifc.Operator):
# removed the part that was relying on node graph to be opened at execution
class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.import_sverchok_graph"
bl_label = "Import sverchok graph"
bl_label = "Import Sverchok Graph"
bl_options = {"REGISTER"}
filepath: bpy.props.StringProperty(
@@ -224,7 +224,7 @@ class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator):
# removed the part that was relying on node graph to be opened at execution
class ExportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.export_sverchok_graph"
bl_label = "Export sverchok graph"
bl_label = "Export Sverchok Graph"
bl_options = {"REGISTER"}
filepath: bpy.props.StringProperty(
@@ -224,11 +224,20 @@ class BIM_PT_array(bpy.types.Panel):
row.operator("bim.edit_array", icon="CHECKMARK", text="").item = i
row.operator("bim.disable_editing_array", icon="CANCEL", text="")
row = box.row(align=True)
row.prop(props, "use_local_space")
row.prop(props, "dimension_input_type")
row = box.row(align=True)
row.prop(props, "use_local_space")
row.prop(props, "sync_children")
col = box.column()
row = col.row(align=True)
row.prop(props, "x")
row.operator("bim.input_cursor_x_array", icon="CURSOR", text="")
row = col.row(align=True)
row.prop(props, "y")
row.operator("bim.input_cursor_y_array", icon="CURSOR", text="")
row = col.row(align=True)
row.prop(props, "z")
row.operator("bim.input_cursor_z_array", icon="CURSOR", text="")
else:
row = box.row(align=True)
@@ -236,6 +245,8 @@ class BIM_PT_array(bpy.types.Panel):
row.operator("bim.enable_editing_array", icon="GREASEPENCIL", text="").item = i
row.operator("bim.remove_array", icon="X", text="").item = i
row = box.row(align=True)
row.label(text=f"Dimension input type: {array.get('dimension_input_type', 'Increment')}")
row = box.row(align=True)
row.label(text=f"Use Local Space: {array.get('use_local_space', False)}")
row = box.row(align=True)
row.label(text=f"X: {array['x']}")
@@ -273,7 +284,7 @@ class BIM_PT_stair(bpy.types.Panel):
stair_data = StairData.data["parameters"]["data_dict"]
if props.is_editing != -1:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_stair", icon="CHECKMARK", text="Finish editing")
row.operator("bim.finish_editing_stair", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
row = self.layout.row(align=True)
for prop_name in props.get_props_kwargs():
@@ -373,7 +384,7 @@ class BIM_PT_window(bpy.types.Panel):
if props.is_editing != -1:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_window", icon="CHECKMARK", text="Finish editing")
row.operator("bim.finish_editing_window", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_window", icon="CANCEL", text="")
general_props = props.get_general_kwargs()
@@ -488,7 +499,7 @@ class BIM_PT_door(bpy.types.Panel):
if props.is_editing != -1:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_door", icon="CHECKMARK", text="Finish editing")
row.operator("bim.finish_editing_door", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_door", icon="CANCEL", text="")
general_props = props.get_general_kwargs()
@@ -572,7 +583,7 @@ class BIM_PT_railing(bpy.types.Panel):
if props.is_editing != -1:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_railing", icon="CHECKMARK", text="Finish editing")
row.operator("bim.finish_editing_railing", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_railing", icon="CANCEL", text="")
general_props = props.get_general_kwargs()
@@ -631,7 +642,7 @@ class BIM_PT_roof(bpy.types.Panel):
if props.is_editing != -1:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_roof", icon="CHECKMARK", text="Finish editing")
row.operator("bim.finish_editing_roof", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_roof", icon="CANCEL", text="")
general_props = props.get_general_kwargs()
@@ -37,38 +37,6 @@ from mathutils import Vector, Matrix
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
def element_listener(element, obj):
blenderbim.bim.handler.subscribe_to(obj, "mode", mode_callback)
def mode_callback(obj, data):
for obj in set(bpy.context.selected_objects + [bpy.context.active_object]):
if (
not obj.data
or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve))
or not obj.BIMObjectProperties.ifc_definition_id
or not bpy.context.scene.BIMProjectProperties.is_authoring
):
return
product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbLayer2":
return
if obj.mode == "EDIT":
tool.Ifc.edit(obj)
bm = bmesh.from_edit_mesh(obj.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.update_edit_mesh(obj.data)
else:
new_origin = obj.matrix_world @ Vector(obj.bound_box[0])
obj.data.transform(
Matrix.Translation(
(obj.matrix_world.inverted().to_quaternion() @ (obj.matrix_world.translation - new_origin))
)
)
obj.matrix_world.translation = new_origin
class JoinWall(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.join_wall"
bl_label = "Join Wall"
@@ -69,10 +69,12 @@ def update_simple_openings(element, opening_width, opening_height):
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
extrusion = shape_builder.extrude(
shape_builder.rectangle(size=Vector([opening_width, 0.0, opening_height])),
shape_builder.rectangle(size=Vector([opening_width, 0.0, opening_height]).xz),
magnitude=thickness / unit_scale,
position=Vector([0.0, -0.1 / unit_scale, 0.0]),
extrusion_vector=Vector([0.0, 1.0, 0.0]),
position_x_axis=V(1, 0, 0),
position_z_axis=V(0, -1, 0),
extrusion_vector=V(0, 0, -1),
)
new_representation = shape_builder.get_representation(context, extrusion)
@@ -480,7 +482,7 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_window"
bl_label = "Cancel editing Window"
bl_label = "Cancel Editing Window"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -508,7 +510,7 @@ class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_window"
bl_label = "Finish editing window"
bl_label = "Finish Editing Window"
bl_options = {"REGISTER"}
def _execute(self, context):
@@ -68,7 +68,17 @@ class BimTool(WorkSpaceTool):
def add_layout_hotkey_operator(layout, text, hotkey, description):
op = layout.operator("bim.hotkey", text=text)
modifiers = {
"A": "EVENT_ALT",
"S": "EVENT_SHIFT",
}
modifier, key = hotkey.split("_")
row = layout.row(align=True)
row.label(text="", icon=modifiers[modifier])
row.label(text="", icon=f"EVENT_{key}")
op = row.operator("bim.hotkey", text=text)
op.hotkey = hotkey
op.description = description
return op
@@ -132,10 +142,7 @@ class BimToolUI:
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="rl2", text="RL")
elif cls.props.ifc_class in ("IfcSpaceType"):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
add_layout_hotkey_operator(row, "Generate", "S_G", bpy.ops.bim.generate_space.__doc__)
add_layout_hotkey_operator(cls.layout, "Generate", "S_G", bpy.ops.bim.generate_space.__doc__)
@classmethod
def draw_edit_object_interface(cls, context):
@@ -155,47 +162,19 @@ class BimToolUI:
op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
op.x_angle = cls.props.x_angle
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Extend").hotkey = "S_E"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_T")
row.operator("bim.hotkey", text="Butt").hotkey = "S_T"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Y")
row.operator("bim.hotkey", text="Mitre").hotkey = "S_Y"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_M")
add_layout_hotkey_operator(row, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__)
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_F")
add_layout_hotkey_operator(row, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__)
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_K")
add_layout_hotkey_operator(row, "Split", "S_K", bpy.ops.bim.split_wall.__doc__)
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_R")
row.operator("bim.hotkey", text="Rotate 90").hotkey = "S_R"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.recalculate_wall.__doc__)
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
add_layout_hotkey_operator(cls.layout, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__)
add_layout_hotkey_operator(cls.layout, "Split", "S_K", bpy.ops.bim.split_wall.__doc__)
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_wall.__doc__)
row.operator("bim.join_wall", icon="X", text="").join_type = ""
elif AuthoringData.data["active_material_usage"] == "LAYER3":
# unnecessary check because BIM Tool is not available in EDIT mode?
if context.active_object.mode == "OBJECT":
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Edit Profile").hotkey = "S_E"
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="x_angle", text="X Angle")
@@ -215,32 +194,17 @@ class BimToolUI:
op = row.operator("bim.change_profile_depth", icon="FILE_REFRESH", text="")
op.depth = cls.props.extrusion_depth
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Extend").hotkey = "S_E"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_ALT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Edit Axis").hotkey = "A_E"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_T")
row.operator("bim.hotkey", text="Butt").hotkey = "S_T"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Y")
row.operator("bim.hotkey", text="Mitre").hotkey = "S_Y"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_R")
row.operator("bim.hotkey", text="Rotate 90").hotkey = "S_R"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__)
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "")
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__)
row.operator("bim.extend_profile", icon="X", text="").join_type = ""
elif AuthoringData.data["active_representation_type"] == "SweptSolid":
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
elif AuthoringData.data["active_class"] in (
"IfcWindow",
"IfcWindowStandardCase",
@@ -254,20 +218,11 @@ class BimToolUI:
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="rl1", text="RL")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.recalculate_fill.__doc__)
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_F")
row.operator("bim.hotkey", text="Flip").hotkey = "S_F"
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_fill.__doc__)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", "")
elif AuthoringData.data["active_class"] in ("IfcSpace",):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__)
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__)
elif AuthoringData.data["active_class"] in (
"IfcCableCarrierSegmentType",
@@ -275,39 +230,29 @@ class BimToolUI:
"IfcDuctSegmentType",
"IfcPipeSegmentType",
):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Extend", icon="EVENT_E")
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
elif (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and not context.active_object.BIMRailingProperties.is_editing_path
):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Edit Railing Path").hotkey = "S_E"
add_layout_hotkey_operator(cls.layout, "Edit Railing Path", "S_E", "")
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["parameters"]
and not context.active_object.BIMRoofProperties.is_editing_path
):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Edit Roof Path").hotkey = "S_E"
add_layout_hotkey_operator(cls.layout, "Edit Roof Path", "S_E", "")
elif DecoratorData.get_ifc_text_data(bpy.context.object):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Edit text").hotkey = "S_E"
add_layout_hotkey_operator(cls.layout, "Edit Text", "S_E", "")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_O")
if len(context.selected_objects) == 2:
row.operator("bim.add_opening", text="Apply Void")
else:
@@ -320,27 +265,14 @@ class BimToolUI:
else:
row.operator("bim.show_openings", icon="HIDE_OFF", text="")
row = cls.layout.row(align=True)
row.label(text="Align")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Align Exterior", icon="EVENT_X")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Align Centerline", icon="EVENT_C")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Align Interior", icon="EVENT_V")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Mirror", icon="EVENT_M")
cls.layout.row(align=True).label(text="Align")
add_layout_hotkey_operator(cls.layout, "Align Exterior", "S_X", "")
add_layout_hotkey_operator(cls.layout, "Align Centerline", "S_C", "")
add_layout_hotkey_operator(cls.layout, "Align Interior", "S_V", "")
add_layout_hotkey_operator(cls.layout, "Mirror", "S_M", bpy.ops.bim.mirror_elements.__doc__)
row = cls.layout.row(align=True)
row.label(text="Mode")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_ALT")
row.label(text="", icon="EVENT_O")
add_layout_hotkey_operator(row, "Void", "A_O", "Show / edit openings")
cls.layout.row(align=True).label(text="Mode")
add_layout_hotkey_operator(cls.layout, "Void", "A_O", "Show / edit openings")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_ALT")
row.label(text="", icon="EVENT_D")
@@ -483,8 +415,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
continue
usage = tool.Model.get_usage_type(element)
if not usage:
obj.select_set(False)
continue
representation = tool.Geometry.get_active_representation(obj)
if representation and representation.RepresentationType == "SweptSolid":
usage = "SWEPTSOLID"
else:
obj.select_set(False)
continue
selected_usages.setdefault(usage, []).append(obj)
if len(bpy.context.selected_objects) == 1:
@@ -498,7 +434,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
elif self.active_material_usage == "PROFILE":
# Extend PROFILE to cursor
bpy.ops.bim.extend_profile(join_type="T")
else:
# Edit SWEPTSOLID profile (assuming single profile for now)
bpy.ops.bim.enable_editing_extrusion_profile()
elif self.active_material_usage == "LAYER2" and selected_usages.get("PROFILE", []):
# Extend PROFILEs to LAYER2
[o.select_set(False) for o in selected_usages.get("LAYER3", [])]
@@ -573,7 +511,13 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if self.active_material_usage == "LAYER2":
bpy.ops.bim.merge_wall()
else:
bpy.ops.bim.mirror_elements()
if len(bpy.context.selected_objects) == 1:
self.report(
{"INFO"},
"At least two objects must be selected: an object to be mirrored, and a mirror axis as the active object.",
)
else:
bpy.ops.bim.mirror_elements()
def hotkey_S_R(self):
if not bpy.context.selected_objects:
@@ -102,7 +102,7 @@ class ExecuteIfcPatch(bpy.types.Operator):
class UpdateIfcPatchArguments(bpy.types.Operator):
bl_idname = "bim.update_ifc_patch_arguments"
bl_label = "Update IFC Patch arguments"
bl_label = "Update IFC Patch Arguments"
recipe: bpy.props.StringProperty()
def execute(self, context):
@@ -96,7 +96,10 @@ class MaterialPsetsData(Data):
@classmethod
def load(cls):
cls.data = {"psets": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object.active_material))}
cls.data = {
"ifc_definition_id": bpy.context.active_object.active_material.BIMObjectProperties.ifc_definition_id,
"psets": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object.active_material)),
}
cls.is_loaded = True
@@ -298,6 +298,7 @@ class EditPset(bpy.types.Operator, Operator):
bpy.ops.bim.load_cost_item_quantities()
bpy.ops.bim.disable_pset_editing(obj=self.obj, obj_type=self.obj_type)
tool.Blender.update_viewport()
class RemovePset(bpy.types.Operator, Operator):
@@ -494,7 +495,7 @@ class BIM_OT_add_property_to_edit(bpy.types.Operator):
class BIM_OT_remove_property_to_edit(bpy.types.Operator):
bl_label = "Remove property to be renamed"
bl_label = "Remove Property to Be Renamed"
bl_idname = "bim.remove_property_to_edit"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty()
@@ -510,7 +511,7 @@ class BIM_OT_remove_property_to_edit(bpy.types.Operator):
class BIM_OT_clear_list(bpy.types.Operator):
bl_label = "Clear list of properties"
bl_label = "Clear List of Properties"
bl_idname = "bim.clear_list"
bl_options = {"REGISTER", "UNDO"}
option: bpy.props.StringProperty()
@@ -558,7 +559,7 @@ class BIM_OT_rename_parameters(bpy.types.Operator):
class BIM_OT_add_edit_custom_property(bpy.types.Operator):
bl_label = "Add or edit a custom property"
bl_label = "Add or Edit a Custom Property"
bl_idname = "bim.add_edit_custom_property"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty()
@@ -615,7 +616,7 @@ class BIM_OT_add_edit_custom_property(bpy.types.Operator):
class BIM_OT_bulk_remove_psets(bpy.types.Operator):
bl_label = "Bulk remove psets from selected objects"
bl_label = "Bulk Remove Psets from Selected Objects"
bl_idname = "bim.bulk_remove_psets"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Bulk remove psets from selected objects"
@@ -268,6 +268,11 @@ class BIM_PT_material_psets(Panel):
def draw(self, context):
if not MaterialPsetsData.is_loaded:
MaterialPsetsData.load()
elif (
context.active_object.active_material.BIMObjectProperties.ifc_definition_id
!= MaterialPsetsData.data["ifc_definition_id"]
):
MaterialPsetsData.load()
props = context.active_object.active_material.PsetProperties
row = self.layout.row(align=True)
@@ -161,7 +161,7 @@ class EnableEditingPropTemplate(bpy.types.Operator):
class DeletePropEnum(bpy.types.Operator):
bl_idname = "bim.delete_prop_enum"
bl_label = "delete property enumeration"
bl_label = "Delete Property Enumeration"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty()
@@ -173,7 +173,7 @@ class DeletePropEnum(bpy.types.Operator):
class AddPropEnum(bpy.types.Operator):
bl_idname = "bim.add_prop_enum"
bl_label = "add property enumeration"
bl_label = "Add Property Enumeration"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty()
@@ -186,7 +186,7 @@ class AssignBaseQto(bpy.types.Operator, tool.Ifc.Operator):
class CalculateAllQuantities(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.calculate_all_quantities"
bl_label = "Calculate all quantities"
bl_label = "Calculate All Quantities"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Calculate all possible quantities and assign them to selected object"
@@ -47,7 +47,7 @@ class LoadResourceProperties(bpy.types.Operator):
class AddResource(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_resource"
bl_label = "Add resource"
bl_label = "Add Resource"
bl_options = {"REGISTER", "UNDO"}
ifc_class: bpy.props.StringProperty()
parent_resource: bpy.props.IntProperty()
@@ -168,9 +168,14 @@ class UnlinkObject(bpy.types.Operator):
objects = context.selected_objects
for obj in objects:
if obj.BIMObjectProperties.ifc_definition_id:
if obj in IfcStore.edited_objs:
IfcStore.edited_objs.remove(obj)
IfcStore.unlink_element(obj=obj)
if obj.data:
obj.data = obj.data.copy()
for material_slot in obj.material_slots:
if material_slot.material:
material_slot.material = material_slot.material.copy()
blenderbim.core.style.unlink_style(tool.Ifc, tool.Style, obj=material_slot.material)
blenderbim.core.material.unlink_material(tool.Ifc, obj=material_slot.material)
if "Ifc" in obj.name and "/" in obj.name:
@@ -20,7 +20,6 @@ import bpy
from . import ui, prop, operator
classes = (
operator.HighlightProductRelatedTask,
operator.ExpandAllTasks,
operator.ContractAllTasks,
operator.AddSummaryTask,
@@ -113,6 +112,8 @@ classes = (
operator.VisualiseWorkScheduleDateRange,
operator.LoadTaskAnimationColors,
operator.DisableEditingTaskAnimationColors,
operator.LoadProductTasks,
operator.HighlightTask,
prop.WorkPlan,
prop.BIMWorkPlanProperties,
prop.Task,
@@ -140,6 +141,8 @@ classes = (
ui.BIM_PT_4D_Tools,
ui.BIM_PT_Task_Bar_Creator,
ui.BIM_UL_animation_colors,
ui.BIM_UL_product_input_tasks,
ui.BIM_UL_product_output_tasks,
)
@@ -53,7 +53,7 @@ def parse_duration_as_blender_props(dt, simplify=True):
seconds = getattr(dt, "seconds", 0)
hours, seconds = divmod(seconds, 3600)
minutes, seconds = divmod(seconds, 60)
days = getattr(dt, "days",0)
days = getattr(dt, "days", 0)
months = int(getattr(dt, "months", 0))
years = int(getattr(dt, "years", 0))
return {
@@ -454,11 +454,8 @@ class UnassignProcess(bpy.types.Operator):
)
else:
core.unassign_input_products(
tool.Ifc,
tool.Sequence,
tool.Spatial,
task=tool.Ifc.get().by_id(self.task)
)
tool.Ifc, tool.Sequence, tool.Spatial, task=tool.Ifc.get().by_id(self.task)
)
elif self.related_object_type == "CONTROL":
pass # TODO
return {"FINISHED"}
@@ -606,7 +603,7 @@ class ImportP6XER(bpy.types.Operator, ImportHelper):
class ImportPP(bpy.types.Operator, ImportHelper):
bl_idname = "import_pp.bim"
bl_label = "Import Powerproject pp"
bl_label = "Import Powerproject .pp"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".pp"
filter_glob: bpy.props.StringProperty(default="*.pp", options={"HIDDEN"})
@@ -1006,6 +1003,7 @@ class VisualiseWorkScheduleDate(bpy.types.Operator):
core.visualise_work_schedule_date(tool.Sequence, work_schedule=tool.Ifc.get().by_id(self.work_schedule))
return {"FINISHED"}
class GuessDateRange(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.guess_date_range"
bl_label = "Guess Work Schedule Date Range"
@@ -1024,7 +1022,10 @@ class VisualiseWorkScheduleDateRange(bpy.types.Operator):
@classmethod
def poll(cls, context):
has_start, has_finish = bpy.context.scene.BIMWorkScheduleProperties.visualisation_start, bpy.context.scene.BIMWorkScheduleProperties.visualisation_finish
has_start, has_finish = (
bpy.context.scene.BIMWorkScheduleProperties.visualisation_start,
bpy.context.scene.BIMWorkScheduleProperties.visualisation_finish,
)
return bool(has_start and has_finish) and not "-" in (has_start, has_finish)
def execute(self, context):
@@ -1106,7 +1107,7 @@ class BlenderBIM_DatePicker(bpy.types.Operator):
class BlenderBIM_DatePickerSetDate(bpy.types.Operator):
bl_label = "set date"
bl_label = "Set Date"
bl_idname = "bim.datepicker_setdate"
bl_options = {"REGISTER", "UNDO"}
selected_date: bpy.props.StringProperty()
@@ -1117,7 +1118,7 @@ class BlenderBIM_DatePickerSetDate(bpy.types.Operator):
class BlenderBIM_RedrawDatePicker(bpy.types.Operator):
bl_label = "redraw datepicker window"
bl_label = "Redraw Datepicker Window"
bl_idname = "bim.redraw_datepicker"
bl_options = {"REGISTER", "UNDO"}
action: bpy.props.StringProperty()
@@ -1230,20 +1231,9 @@ class CalculateTaskDuration(bpy.types.Operator, tool.Ifc.Operator):
core.calculate_task_duration(tool.Ifc, tool.Sequence, task=tool.Ifc.get().by_id(self.task))
class HighlightProductRelatedTask(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.highlight_product_related_task"
bl_label = "Highlights the related task"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Finds the related Task"
product_type: bpy.props.StringProperty()
def _execute(self, context):
core.highlight_product_related_task(tool.Sequence, tool.Spatial, product_type=self.product_type)
class ExpandAllTasks(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.expand_all_tasks"
bl_label = "Expands all tasks"
bl_label = "Expands All Tasks"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Finds the related Task"
product_type: bpy.props.StringProperty()
@@ -1254,7 +1244,7 @@ class ExpandAllTasks(bpy.types.Operator, tool.Ifc.Operator):
class ContractAllTasks(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.contract_all_tasks"
bl_label = "Expands all tasks"
bl_label = "Expands All Tasks"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Finds the related Task"
product_type: bpy.props.StringProperty()
@@ -1293,6 +1283,7 @@ class DisableEditingTaskAnimationColors(bpy.types.Operator):
core.disable_editing_task_animation_colors(tool.Sequence)
return {"FINISHED"}
class CopyTask(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.duplicate_task"
bl_label = "Copy Task"
@@ -1300,4 +1291,39 @@ class CopyTask(bpy.types.Operator, tool.Ifc.Operator):
task: bpy.props.IntProperty()
def _execute(self, context):
core.duplicate_task(tool.Ifc, tool.Sequence, task=tool.Ifc.get().by_id(self.task))
core.duplicate_task(tool.Ifc, tool.Sequence, task=tool.Ifc.get().by_id(self.task))
class LoadProductTasks(bpy.types.Operator):
bl_idname = "bim.load_product_tasks"
bl_label = "Load Product Tasks"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
if not tool.Ifc.get():
return False
if not context.active_object:
return False
if not context.active_object.BIMObjectProperties.ifc_definition_id:
return False
return True
def execute(self, context):
core.load_product_tasks(
tool.Sequence, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id)
)
return {"FINISHED"}
class HighlightTask(bpy.types.Operator):
bl_idname = "bim.highlight_task"
bl_label = "Highlight Task"
bl_options = {"REGISTER", "UNDO"}
task: bpy.props.IntProperty()
def execute(self, context):
r = core.highlight_task(tool.Sequence, task=tool.Ifc.get().by_id(self.task))
if isinstance(r, str):
self.report({"WARNING"}, r)
return {"FINISHED"}
@@ -96,6 +96,14 @@ def update_active_task_outputs(self, context):
bpy.ops.bim.load_task_outputs()
def update_active_task_resources(self, context):
bpy.ops.bim.load_task_resources()
def update_active_task_inputs(self, context):
bpy.ops.bim.load_task_inputs()
def updateTaskName(self, context):
props = context.scene.BIMWorkScheduleProperties
if not props.is_task_update_enabled or self.name == "Unnamed":
@@ -389,9 +397,13 @@ class BIMWorkScheduleProperties(PropertyGroup):
active_task_input_index: IntProperty(name="Active Task Input Index")
task_outputs: CollectionProperty(name="Task Outputs", type=TaskProduct)
active_task_output_index: IntProperty(name="Active Task Output Index")
is_nested_task_outputs: BoolProperty(
name="Is Nested Task Outputs", default=False, update=update_active_task_outputs
)
show_nested_outputs: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_outputs)
show_nested_resources: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_resources)
show_nested_inputs: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_inputs)
product_input_tasks: CollectionProperty(name="Product Task Inputs", type=TaskProduct)
product_output_tasks: CollectionProperty(name="Product Task Outputs", type=TaskProduct)
active_product_output_task_index: IntProperty(name="Active Product Output Task Index")
active_product_input_task_index: IntProperty(name="Active Product Input Task Index")
class BIMTaskTreeProperties(PropertyGroup):
@@ -433,6 +433,8 @@ class BIM_PT_task_icom(Panel):
op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="")
op.task = task.ifc_definition_id
row2 = col.row()
row2.prop(self.props, "show_nested_inputs", text="Show Nested")
row2 = col.row()
row2.template_list("BIM_UL_task_inputs", "", self.props, "task_inputs", self.props, "active_task_input_index")
@@ -456,6 +458,9 @@ class BIM_PT_task_icom(Panel):
op.related_object_type = "RESOURCE"
op.resource = self.props.task_resources[self.props.active_task_resource_index].ifc_definition_id
row2 = col.row()
row2.prop(self.props, "show_nested_resources", text="Show Nested")
row2 = col.row()
row2.template_list(
"BIM_UL_task_resources", "", self.props, "task_resources", self.props, "active_task_resource_index"
@@ -481,7 +486,7 @@ class BIM_PT_task_icom(Panel):
op = row2.operator("bim.select_task_related_products", icon="RESTRICT_SELECT_OFF", text="Select")
op.task = task.ifc_definition_id
row2 = col.row()
row2.prop(self.props, "is_nested_task_outputs", text="Show Nested")
row2.prop(self.props, "show_nested_outputs", text="Show Nested")
row2 = col.row()
row2.template_list(
"BIM_UL_task_outputs", "", self.props, "task_outputs", self.props, "active_task_output_index"
@@ -534,6 +539,26 @@ class BIM_UL_task_outputs(UIList):
row.prop(item, "name", emboss=False, text="")
class BIM_UL_product_input_tasks(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
op = row.operator("bim.highlight_task", text="", icon="STYLUS_PRESSURE")
op.task = item.ifc_definition_id
row.split(factor=0.8)
row.label(text=item.name)
class BIM_UL_product_output_tasks(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
op = row.operator("bim.highlight_task", text="", icon="STYLUS_PRESSURE")
op.task = item.ifc_definition_id
row.split(factor=0.8)
row.label(text=item.name)
class BIM_UL_tasks(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
@@ -798,14 +823,32 @@ class BIM_PT_4D_Tools(Panel):
bl_category = "4D/5D Toolkit"
def draw(self, context):
self.props = context.scene.BIMWorkScheduleProperties
row = self.layout.row()
row.operator(
"bim.highlight_product_related_task", text="Find Input-related Task ", icon="STYLUS_PRESSURE"
).product_type = "Input"
row = self.layout.row()
row.operator(
"bim.highlight_product_related_task", text="Go to Output-related Task ", icon="STYLUS_PRESSURE"
).product_type = "Output"
row.operator("bim.load_product_tasks", text="Load Tasks", icon="FILE_REFRESH")
grid = self.layout.grid_flow(columns=2, even_columns=True)
col1 = grid.column()
col1.label(text="Product Input Tasks")
col1.template_list(
"BIM_UL_product_input_tasks",
"",
self.props,
"product_input_tasks",
self.props,
"active_product_input_task_index",
)
col2 = grid.column()
col2.label(text="Product Output Tasks")
col2.template_list(
"BIM_UL_product_output_tasks",
"",
self.props,
"product_output_tasks",
self.props,
"active_product_output_task_index",
)
class BIM_PT_Task_Bar_Creator(Panel):
@@ -34,6 +34,7 @@ class StylesData:
@classmethod
def load(cls):
cls.data = {"style_types": cls.style_types(), "total_styles": cls.total_styles()}
cls.is_loaded = True
@classmethod
def style_types(cls):
@@ -55,7 +56,12 @@ class StyleAttributesData:
@classmethod
def load(cls):
cls.data = {"attributes": cls.get_attributes()}
cls.data = {"ifc_style_id": cls.ifc_style_id(), "attributes": cls.get_attributes()}
cls.is_loaded = True
@classmethod
def ifc_style_id(cls):
return bpy.context.active_object.active_material.BIMMaterialProperties.ifc_style_id
@classmethod
def get_attributes(cls):
@@ -23,8 +23,6 @@ from blenderbim.bim.module.style.data import StylesData, StyleAttributesData
from bl_ui.properties_material import MaterialButtonsPanel
class BIM_PT_styles(Panel):
bl_label = "IFC Styles"
bl_idname = "BIM_PT_styles"
@@ -93,7 +91,7 @@ class BIM_PT_style(MaterialButtonsPanel, Panel):
row.operator("bim.remove_style", icon="X", text="").style = props.ifc_style_id
row = self.layout.row(align=True)
row.prop(mat, "diffuse_color", text="Color")
else:
row.operator("bim.add_style", icon="ADD")
@@ -117,6 +115,11 @@ class BIM_PT_style_attributes(Panel):
def draw(self, context):
if not StyleAttributesData.is_loaded:
StyleAttributesData.load()
elif (
context.active_object.active_material.BIMMaterialProperties.ifc_style_id
!= StyleAttributesData.data["ifc_style_id"]
):
StyleAttributesData.load()
obj = context.active_object.active_material
mprops = obj.BIMMaterialProperties
@@ -64,19 +64,49 @@ class UnassignType(bpy.types.Operator):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
def exclude_callback(attribute):
return attribute.is_a("IfcProfileDef") and attribute.ProfileName
self.file = IfcStore.get_file()
related_objects = (
objs = (
[bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects
)
for related_object in related_objects:
oprops = related_object.BIMObjectProperties
ifcopenshell.api.run(
"type.unassign_type",
self.file,
**{
"related_object": self.file.by_id(oprops.ifc_definition_id),
},
)
for obj in objs:
element = tool.Ifc.get_entity(obj)
if not element or element.is_a("IfcElementType"):
continue
ifcopenshell.api.run("type.unassign_type", self.file, related_object=element)
active_representation = tool.Geometry.get_active_representation(obj)
active_context = active_representation.ContextOfItems
new_active_representation = None
if element.Representation:
representations = []
for representation in element.Representation.Representations:
resolved_representation = ifcopenshell.util.representation.resolve_representation(representation)
if representation == resolved_representation:
representations.append(representation)
else:
# We must unmap representations.
copied_representation = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(), resolved_representation, exclude=["IfcGeometricRepresentationContext"], exclude_callback=exclude_callback
)
representations.append(copied_representation)
if representation.ContextOfItems == active_context:
new_active_representation = copied_representation
element.Representation.Representations = representations
if new_active_representation:
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=new_active_representation,
should_reload=False,
is_global=False,
should_sync_changes_first=False,
)
return {"FINISHED"}
@@ -20,6 +20,7 @@ import blenderbim.tool as tool
import blenderbim.bim.module.type.prop as type_prop
from bpy.types import Panel
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import prop_with_search
from blenderbim.bim.module.type.data import TypeData
@@ -69,7 +70,7 @@ class BIM_PT_type(Panel):
row.prop(props, "relating_type_class", text="")
if type_prop.get_relating_type(None, context):
row.prop(props, "relating_type", text="")
prop_with_search(row, props, "relating_type", text="")
row.operator("bim.assign_type", icon="CHECKMARK", text="")
else:
row.label(text="No Types Found")
+51 -27
View File
@@ -101,6 +101,7 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ReloadSelectedIfcFile(bpy.types.Operator, IFCFileSelector):
bl_idname = "bim.reload_selected_ifc_file"
bl_label = "Reload selected IFC File"
@@ -114,6 +115,7 @@ class ReloadSelectedIfcFile(bpy.types.Operator, IFCFileSelector):
context.scene.BIMProperties.ifc_file = context.scene.BIMProperties.ifc_file
return {"FINISHED"}
class SelectDataDir(bpy.types.Operator):
bl_idname = "bim.select_data_dir"
bl_label = "Select Data Directory"
@@ -211,6 +213,11 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
def create_section_compare_node(self):
group = bpy.data.node_groups.new("Section Compare", type="ShaderNodeTree")
group.inputs.new("NodeSocketFloat", "Value")
group.inputs["Value"].default_value = 1.0 # Mandatory multiplier for the last node group
group.inputs.new("NodeSocketVector", "Vector")
group.outputs.new("NodeSocketFloat", "Value")
group.outputs.new("NodeSocketFloat", "Line Decorator")
group_input = group.nodes.new(type="NodeGroupInput")
group_input.location = 0, 50
@@ -222,6 +229,12 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
greater.inputs[1].default_value = 0
greater.location = 400, 0
compare = group.nodes.new(type="ShaderNodeMath")
compare.operation = "COMPARE"
compare.inputs[1].default_value = 0
compare.inputs[2].default_value = 0.04
compare.location = 400, -200
multiply = group.nodes.new(type="ShaderNodeMath")
multiply.operation = "MULTIPLY"
multiply.inputs[0].default_value = 1
@@ -230,57 +243,68 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
group_output = group.nodes.new(type="NodeGroupOutput")
group_output.location = 800, 0
group.links.new(group_input.outputs[""], multiply.inputs[0])
group.links.new(group_input.outputs[""], separate_xyz.inputs[0])
group.links.new(group_input.outputs["Value"], multiply.inputs[0])
group.links.new(group_input.outputs["Vector"], separate_xyz.inputs[0])
group.links.new(separate_xyz.outputs[2], greater.inputs[0])
group.links.new(greater.outputs[0], multiply.inputs[1])
group.links.new(multiply.outputs[0], group_output.inputs[""])
group.links.new(multiply.outputs[0], group_output.inputs["Value"])
group.links.new(separate_xyz.outputs[2], compare.inputs[0])
group.links.new(compare.outputs[0], group_output.inputs["Line Decorator"])
def create_section_override_node(self, obj, context):
group = bpy.data.node_groups.new("Section Override", type="ShaderNodeTree")
group.inputs.new("NodeSocketShader", "Shader")
group.outputs.new("NodeSocketShader", "Shader")
links = group.links
nodes = group.nodes
group_input = nodes.new(type="NodeGroupInput")
group_output = nodes.new(type="NodeGroupOutput")
group_output.location = 600, 250
group_output.location = 800, 250
backfacing_mix = nodes.new(type="ShaderNodeMixShader")
backfacing_mix.location = group_output.location - Vector((400, 350))
mix_decorator = group.nodes.new(type="ShaderNodeMixShader")
mix_decorator.name = "Line Decorator Mix"
mix_decorator.location = group_output.location - Vector((200, 0))
mix_section = group.nodes.new(type="ShaderNodeMixShader")
mix_section.name = "Section Mix"
mix_section.inputs[0].default_value = 1 # Directly pass input shader when there is no cutaway
mix_section.location = mix_decorator.location - Vector((200, 200))
transparent = nodes.new(type="ShaderNodeBsdfTransparent")
transparent.location = mix_section.location - Vector((200, 100))
mix_backfacing = nodes.new(type="ShaderNodeMixShader")
mix_backfacing.location = mix_section.location - Vector((200, 0))
group_input.location = mix_backfacing.location - Vector((200, 50))
backfacing = nodes.new(type="ShaderNodeNewGeometry")
backfacing.location = backfacing_mix.location + Vector((-200, 200))
group_input.location = backfacing_mix.location - Vector((200, 50))
backfacing.location = mix_backfacing.location + Vector((-200, 200))
emission = nodes.new(type="ShaderNodeEmission")
emission.inputs[0].default_value = list(context.scene.BIMProperties.section_plane_colour) + [1]
emission.location = backfacing_mix.location - Vector((200, 150))
transparent = nodes.new(type="ShaderNodeBsdfTransparent")
transparent.location = group_output.location - Vector((400, 100))
section_mix = group.nodes.new(type="ShaderNodeMixShader")
section_mix.name = "Section Mix"
section_mix.inputs[0].default_value = 1 # Directly pass input shader when there is no cutaway
section_mix.location = group_output.location - Vector((200, 0))
emission.location = mix_backfacing.location - Vector((200, 150))
cut_obj = nodes.new(type="ShaderNodeTexCoord")
cut_obj.object = obj
cut_obj.location = group_output.location - Vector((800, 150))
cut_obj.location = backfacing.location - Vector((200, 200))
section_compare = nodes.new(type="ShaderNodeGroup")
section_compare.node_tree = bpy.data.node_groups.get("Section Compare")
section_compare.name = "Last Section Compare"
section_compare.location = group_output.location - Vector((600, 0))
section_compare.location = backfacing.location + Vector((0, 200))
links.new(cut_obj.outputs["Object"], section_compare.inputs[1])
links.new(backfacing.outputs["Backfacing"], backfacing_mix.inputs[0])
links.new(group_input.outputs[""], backfacing_mix.inputs[1])
links.new(emission.outputs["Emission"], backfacing_mix.inputs[2])
links.new(section_compare.outputs[0], section_mix.inputs[0])
links.new(transparent.outputs["BSDF"], section_mix.inputs[1])
links.new(backfacing_mix.outputs["Shader"], section_mix.inputs[2])
links.new(section_mix.outputs["Shader"], group_output.inputs[""])
links.new(backfacing.outputs["Backfacing"], mix_backfacing.inputs[0])
links.new(group_input.outputs["Shader"], mix_backfacing.inputs[1])
links.new(emission.outputs["Emission"], mix_backfacing.inputs[2])
links.new(section_compare.outputs["Value"], mix_section.inputs[0])
links.new(transparent.outputs[0], mix_section.inputs[1])
links.new(mix_backfacing.outputs["Shader"], mix_section.inputs[2])
links.new(section_compare.outputs["Line Decorator"], mix_decorator.inputs[0])
links.new(mix_section.outputs["Shader"], mix_decorator.inputs[1])
links.new(mix_decorator.outputs["Shader"], group_output.inputs["Shader"])
def append_obj_to_section_override_node(self, obj):
group = bpy.data.node_groups.get("Section Override")
@@ -695,7 +719,7 @@ class BIM_OT_enum_property_search(bpy.types.Operator):
class EditBlenderCollection(bpy.types.Operator):
bl_idname = "bim.edit_blender_collection"
bl_label = "Add or Remove blender collection item"
bl_label = "Add or Remove Blender Collection Item"
bl_options = {"REGISTER", "UNDO"}
option: bpy.props.StringProperty(description="add or remove item from collection")
collection: bpy.props.StringProperty(description="collection to be edited")
+20 -1
View File
@@ -173,6 +173,18 @@ def update_section_color(self, context):
pass
def update_section_line_decorator(self, context):
compare_node_group = bpy.data.node_groups.get("Section Compare")
if compare_node_group is None:
return
for node in compare_node_group.nodes:
if not hasattr(node, "operation"):
continue
if node.operation == "COMPARE":
node.inputs[2].default_value = self.section_line_decorator_width
break
class StrProperty(PropertyGroup):
pass
@@ -313,13 +325,20 @@ class BIMProperties(PropertyGroup):
last_transaction: StringProperty(name="Last Transaction")
should_section_selected_objects: BoolProperty(name="Section Selected Objects", default=False)
section_plane_colour: FloatVectorProperty(
name="Temporary Section Cutaway Colour",
name="Cutaway Colour",
subtype="COLOR",
default=(1, 0, 0),
min=0.0,
max=1.0,
update=update_section_color,
)
section_line_decorator_width: FloatProperty(
name="Line Decorator Width",
default=0.04,
min=0.0,
soft_max=1.0,
update=update_section_line_decorator,
)
area_unit: EnumProperty(
default="SQUARE_METRE",
items=[
+20 -5
View File
@@ -78,11 +78,9 @@ class BIM_PT_section_plane(Panel):
layout.use_property_split = True
props = context.scene.BIMProperties
row = layout.row()
row.prop(props, "should_section_selected_objects")
row = layout.row()
row.prop(props, "section_plane_colour")
layout.prop(props, "should_section_selected_objects")
layout.prop(props, "section_plane_colour")
layout.prop(props, "section_line_decorator_width")
row = layout.row(align=True)
row.operator("bim.add_section_plane")
@@ -184,6 +182,23 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
row.prop(context.scene.BIMProperties, "data_dir")
row.operator("bim.select_data_dir", icon="FILE_FOLDER", text="")
row = self.layout.row(align=True)
row.prop(context.scene.DocProperties, "docs_dir")
row = self.layout.row(align=True)
row.prop(context.scene.DocProperties, "sheets_dir")
row = self.layout.row(align=True)
row.prop(context.scene.DocProperties, "titleblocks_dir")
row = self.layout.row(align=True)
row.prop(context.scene.DocProperties, "drawings_dir")
row = self.layout.row(align=True)
row.prop(context.scene.DocProperties, "stylesheet_path")
row = self.layout.row(align=True)
row.prop(context.scene.DocProperties, "markers_path")
row = self.layout.row(align=True)
row.prop(context.scene.DocProperties, "symbols_path")
row = self.layout.row(align=True)
row.prop(context.scene.DocProperties, "patterns_path")
row = layout.row()
row.operator("bim.configure_visibility")
+51 -18
View File
@@ -45,13 +45,13 @@ def disable_editing_assigned_product(drawing, obj=None):
def edit_assigned_product(ifc, drawing, obj=None, product=None):
element = ifc.get_entity(obj)
existing_product = drawing.get_assigned_product(element)
if existing_product == product:
return
if existing_product:
ifc.run("drawing.unassign_product", relating_product=existing_product, related_object=element)
if product:
ifc.run("drawing.assign_product", relating_product=product, related_object=element)
drawing.update_text_value(obj)
if existing_product != product:
if existing_product:
ifc.run("drawing.unassign_product", relating_product=existing_product, related_object=element)
if product:
ifc.run("drawing.assign_product", relating_product=product, related_object=element)
drawing.update_text_value(obj)
drawing.disable_editing_assigned_product(obj)
@@ -66,19 +66,31 @@ def disable_editing_sheets(drawing):
def add_sheet(ifc, drawing, titleblock=None):
sheet = ifc.run("document.add_information")
layout = ifc.run("document.add_reference", information=sheet)
titleblock_reference = ifc.run("document.add_reference", information=sheet)
identification = drawing.generate_sheet_identification()
identification = drawing.ensure_unique_identification(identification)
attributes = {"Identification": identification, "Name": "UNTITLED", "Scope": "DOCUMENTATION"}
if ifc.get_schema() == "IFC2X3":
attributes["DocumentId"] = attributes["Identification"]
del attributes["Identification"]
attributes = {"DocumentId": identification, "Name": "UNTITLED", "Scope": "SHEET"}
else:
attributes = {"Identification": identification, "Name": "UNTITLED", "Scope": "SHEET"}
ifc.run("document.edit_information", information=sheet, attributes=attributes)
ifc.run(
"document.edit_reference",
reference=layout,
attributes={"Location": drawing.get_default_sheet_path(identification, "UNTITLED"), "Description": "LAYOUT"},
)
ifc.run(
"document.edit_reference",
reference=titleblock_reference,
attributes={"Location": drawing.get_default_titleblock_path(titleblock), "Description": "TITLEBLOCK"},
)
drawing.create_svg_sheet(sheet, titleblock)
drawing.import_sheets()
def open_sheet(drawing, sheet=None):
drawing.open_svg(drawing.get_document_uri(sheet))
drawing.open_svg(drawing.get_document_uri(sheet, "LAYOUT"))
def remove_sheet(ifc, drawing, sheet=None):
@@ -86,6 +98,11 @@ def remove_sheet(ifc, drawing, sheet=None):
drawing.import_sheets()
def update_sheet_name(ifc, drawing, sheet=None, name=None):
if drawing.get_name(sheet) != name:
ifc.run("document.edit_information", information=sheet, attributes={"Name": name})
def load_schedules(drawing):
drawing.import_schedules()
drawing.enable_editing_schedules()
@@ -98,13 +115,13 @@ def disable_editing_schedules(drawing):
def add_schedule(ifc, drawing, uri=None):
schedule = ifc.run("document.add_information")
reference = ifc.run("document.add_reference", information=schedule)
name = drawing.get_path_filename(uri)
if ifc.get_schema() == "IFC2X3":
attributes = {"DocumentId": "X", "Name": "UNTITLED", "Scope": "SCHEDULE"}
ifc.run("document.edit_information", information=schedule, attributes=attributes)
ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri})
attributes = {"DocumentId": "X", "Name": name, "Scope": "SCHEDULE"}
else:
attributes = {"Identification": "X", "Name": "UNTITLED", "Scope": "SCHEDULE", "Location": uri}
ifc.run("document.edit_information", information=schedule, attributes=attributes)
attributes = {"Identification": "X", "Name": name, "Scope": "SCHEDULE"}
ifc.run("document.edit_information", information=schedule, attributes=attributes)
ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri})
drawing.import_schedules()
@@ -114,7 +131,7 @@ def remove_schedule(ifc, drawing, schedule=None):
def open_schedule(drawing, schedule=None):
drawing.open_spreadsheet(drawing.get_schedule_location(schedule))
drawing.open_spreadsheet(drawing.get_document_uri(schedule))
def update_schedule_name(ifc, drawing, schedule=None, name=None):
@@ -159,8 +176,22 @@ def add_drawing(ifc, collector, drawing, target_view=None, location_hint=None):
"HasLinework": True,
"HasAnnotation": True,
"GlobalReferencing": True,
"Stylesheet": drawing.get_default_drawing_resource_path("Stylesheet"),
"Markers": drawing.get_default_drawing_resource_path("Markers"),
"Symbols": drawing.get_default_drawing_resource_path("Symbols"),
"Patterns": drawing.get_default_drawing_resource_path("Patterns"),
},
)
information = ifc.run("document.add_information")
uri = drawing.get_default_drawing_path(drawing_name)
reference = ifc.run("document.add_reference", information=information)
if ifc.get_schema() == "IFC2X3":
attributes = {"DocumentId": "X", "Name": drawing_name, "Scope": "DRAWING"}
else:
attributes = {"Identification": "X", "Name": drawing_name, "Scope": "DRAWING"}
ifc.run("document.edit_information", information=information, attributes=attributes)
ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri})
ifc.run("document.assign_document", product=element, document=reference)
drawing.import_drawings()
@@ -199,6 +230,7 @@ def remove_drawing(ifc, drawing_tool, drawing=None):
if reference_obj:
drawing_tool.delete_object(reference_obj)
ifc.run("root.remove_product", product=reference)
ifc.run("document.remove_information", information=drawing_tool.get_drawing_document(drawing))
ifc.run("root.remove_product", product=drawing)
drawing_tool.import_drawings()
@@ -232,12 +264,13 @@ def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None)
)
ifc.run("group.assign_group", group=drawing_tool.get_drawing_group(drawing), products=[element])
collector.assign(obj)
drawing_tool.setup_annotation_object(obj, object_type)
drawing_tool.enable_editing(obj)
def build_schedule(drawing, schedule=None):
drawing.create_svg_schedule(schedule)
drawing.open_svg(drawing.get_document_uri(schedule))
drawing.open_svg(drawing.get_path_with_ext(drawing.get_document_uri(schedule), "svg"))
def sync_references(ifc, collector, drawing_tool, drawing=None):
@@ -454,6 +454,13 @@ def calculate_task_duration(ifc, sequence, task=None):
sequence.load_task_properties()
def highlight_task(sequence, task=None):
work_schedule = sequence.get_work_schedule(task)
is_work_schedule_active = sequence.is_work_schedule_active(work_schedule)
if is_work_schedule_active:
sequence.highlight_task(task)
def highlight_product_related_task(sequence, spatial, product_type=None):
products = spatial.get_selected_products()
if products:
@@ -514,3 +521,6 @@ def visualise_work_schedule_date(sequence, work_schedule=None):
def generate_gantt_chart(sequence, work_schedule):
json = sequence.create_tasks_json(work_schedule)
sequence.generate_gantt_browser_chart(json)
def load_product_tasks(sequence, product=None):
sequence.load_product_tasks(product)
+21 -11
View File
@@ -71,16 +71,21 @@ class Aggregate:
@interface
class Blender: pass
def create_ifc_object(cls, ifc_class, name, data): pass
def get_name(cls, ifc_class, name): pass
def get_obj_ifc_definition_id(cls, obj, obj_type): pass
def get_selected_objects(cls): pass
def set_active_object(cls, obj): pass
def apply_bmesh(cls, mesh, bm): pass
def get_bmesh_for_mesh(cls, mesh, clean=False): pass
def get_viewport_context(cls): pass
def update_viewport(cls): pass
class Blender:
def set_active_object(cls, obj): pass
def get_name(cls, ifc_class, name): pass
def get_selected_objects(cls): pass
def create_ifc_object(cls, ifc_class: str, name: str = None, data=None): pass
def get_obj_ifc_definition_id(cls, obj=None, obj_type=None): pass
def is_ifc_object(cls, obj): pass
def is_ifc_class_active(cls, ifc_class): pass
def get_viewport_context(cls): pass
def update_viewport(cls): pass
def get_default_selection_keypmap(cls): pass
def get_object_bounding_box(cls, obj): pass
def apply_bmesh(cls, mesh, bm, obj=None): pass
def get_bmesh_for_mesh(cls, mesh, clean=False): pass
def bmesh_join(cls, bm_a, bm_b, callback=None): pass
@interface
@@ -223,6 +228,7 @@ class Drawing:
def activate_view(cls, camera): pass
def copy_representation(cls, source, dest): pass
def create_annotation_object(cls, drawing, object_type): pass
def setup_annotation_object(cls, obj, object_type): pass
def create_camera(cls, name, matrix): pass
def create_svg_schedule(cls, schedule): pass
def create_svg_sheet(cls, document, titleblock): pass
@@ -249,15 +255,19 @@ class Drawing:
def get_annotation_context(cls, target_view): pass
def get_assigned_product(cls, element): pass
def get_body_context(cls): pass
def get_default_drawing_path(cls, name): pass
def get_default_drawing_resource_path(cls, resource): pass
def get_default_sheet_path(cls, identification, name): pass
def get_document_uri(cls, document): pass
def get_drawing_collection(cls, drawing): pass
def get_drawing_document(cls, drawing): pass
def get_drawing_group(cls, drawing): pass
def get_path_filename(cls, uri): pass
def get_drawing_references(cls, drawing): pass
def get_drawing_target_view(cls, drawing): pass
def get_group_elements(cls, group): pass
def get_ifc_representation_class(cls, object_type): pass
def get_name(cls, element): pass
def get_schedule_location(cls, schedule): pass
def get_text_literal(cls, obj): pass
def remove_literal_from_annotation(cls, obj, literal): pass
def synchronise_ifc_and_text_attributes(cls, obj): pass
+30 -4
View File
@@ -160,12 +160,32 @@ class Blender:
)
return keymap
@classmethod
def get_object_bounding_box(cls, obj):
"""Returns dict with local min and max x, y, z values for the object.
Careful with using this method for objects in EDIT mode because
it requires all EDIT mode changes to be applied.
"""
bound_box = obj.bound_box
bbox_dict = {
"min_x": bound_box[0][0],
"max_x": bound_box[6][0],
"min_y": bound_box[0][1],
"max_y": bound_box[6][1],
"min_z": bound_box[0][2],
"max_z": bound_box[6][2],
}
return bbox_dict
## BMESH UTILS ##
@classmethod
def apply_bmesh(cls, mesh, bm):
def apply_bmesh(cls, mesh, bm, obj=None):
"""`obj` argument is not optional if you plan to update mesh in EDIT mode
and it's possible that that mesh object won't be currenly active."""
import bmesh
if bpy.context.object and bpy.context.object.mode == "EDIT":
if mesh.is_editmode:
# better to be safe because otherwise mesh won't be updated
# and you won't get any errors
if not bm.is_wrapped or hash(bmesh.from_edit_mesh(mesh)) != hash(bm):
@@ -174,7 +194,13 @@ class Blender:
"For applying bmesh in edit mode bmesh should be acquired with `bmesh.from_edit_mesh(me)`."
)
bmesh.update_edit_mesh(mesh)
bpy.context.object.update_from_editmode()
if not obj:
if not bpy.context.object and bpy.context.object.data != mesh:
raise Exception(
"Error applying bmesh in EDIT object - object is "
"not provided and can't be acquired from the context. "
)
obj.update_from_editmode()
else:
bm.to_mesh(mesh)
# only freeing bmesh if object is in OBJECT mode
@@ -190,7 +216,7 @@ class Blender:
def get_bmesh_for_mesh(cls, mesh, clean=False):
import bmesh
if bpy.context.object.mode == "EDIT":
if mesh.is_editmode:
bm = bmesh.from_edit_mesh(mesh)
if clean:
bm.clear()
+4 -2
View File
@@ -92,14 +92,16 @@ class Document(blenderbim.core.tool.Document):
for element in document.DocumentReferences or []:
new = props.documents.add()
new.ifc_definition_id = element.id()
new.name = element.Name or "Unnamed"
name = " - ".join([x for x in [element.Description, element.Location] if x])
new.name = name or "Unnamed"
new.identification = element.ItemReference or "*"
new.is_information = False
else:
for element in document.HasDocumentReferences:
new = props.documents.add()
new.ifc_definition_id = element.id()
new.name = element.Name or "Unnamed"
name = " - ".join([x for x in [element.Description, element.Location] if x])
new.name = name or "Unnamed"
new.identification = element.Identification or "*"
new.is_information = False
+134 -30
View File
@@ -38,6 +38,8 @@ import blenderbim.bim.module.drawing.helper as helper
from blenderbim.bim.module.drawing.data import FONT_SIZES, DecoratorData
from blenderbim.bim.module.drawing.prop import get_diagram_scales, BOX_ALIGNMENT_POSITIONS
from mathutils import Vector
class Drawing(blenderbim.core.tool.Drawing):
@classmethod
@@ -74,8 +76,40 @@ class Drawing(blenderbim.core.tool.Drawing):
obj = annotation.Annotator.add_line_to_annotation(obj, co2, co1)
elif object_type != "TEXT":
obj = annotation.Annotator.add_line_to_annotation(obj)
return obj
# TODO: add a way for users to run this method to readjust the annotations
# based on non-modifier stairs
@classmethod
def setup_annotation_object(cls, obj, object_type, related_object=None):
"""Finish object's adjustments after both object and entity are created"""
if not related_object:
related_object = bpy.context.active_object
related_entity = tool.Ifc.get_entity(related_object)
if object_type == "STAIR_ARROW":
if related_entity and related_entity.is_a("IfcStairFlight"):
stair, stair_entity = related_object, related_entity
arrow = obj
arrow_element = tool.Ifc.get_entity(arrow)
arrow.parent = stair
# place the arrow
# NOTE: may not work correctly in EDIT mode
bbox = tool.Blender.get_object_bounding_box(stair)
float_is_zero = lambda f: 0.0001 >= f >= -0.0001
arrow.location = Vector((bbox["min_x"], (bbox["max_y"] - bbox["min_y"]) / 2, bbox["max_z"]))
last_step_x = max(v.co.x for v in stair.data.vertices if float_is_zero(v.co.z - bbox["max_z"]))
arrow.data.splines[0].points[0].co = Vector((0, 0, 0, 1))
arrow.data.splines[0].points[1].co = Vector((last_step_x, 0, 0, 1))
if not cls.get_assigned_product(arrow_element):
tool.Ifc.run("drawing.assign_product", relating_product=stair_entity, related_object=arrow_element)
pass
@classmethod
def create_camera(cls, name, matrix):
camera = bpy.data.objects.new(name, bpy.data.cameras.new(name))
@@ -94,13 +128,17 @@ class Drawing(blenderbim.core.tool.Drawing):
@classmethod
def create_svg_schedule(cls, schedule):
schedule_creator = scheduler.Scheduler()
schedule_creator.schedule(cls.get_schedule_location(schedule), cls.get_document_uri(schedule))
schedule_creator.schedule(
cls.get_document_uri(schedule), cls.get_path_with_ext(cls.get_document_uri(schedule), "svg")
)
@classmethod
def create_svg_sheet(cls, document, titleblock):
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = bpy.context.scene.BIMProperties.data_dir
sheet_builder.create(cls.get_document_uri(document), titleblock)
uri = cls.get_document_uri(document)
sheet_builder.create(uri, titleblock)
return uri
@classmethod
def delete_collection(cls, collection):
@@ -112,7 +150,20 @@ class Drawing(blenderbim.core.tool.Drawing):
tool.Ifc.delete(element)
obj = tool.Ifc.get_object(element)
if obj:
obj_data = obj.data
bpy.data.objects.remove(obj)
if obj_data.users == 0: # in case we have drawing element types
cls.remove_object_data(obj_data)
@classmethod
def remove_object_data(cls, data):
"""also removes all related objects"""
if isinstance(data, bpy.types.Camera):
bpy.data.cameras.remove(data)
elif isinstance(data, bpy.types.Mesh):
bpy.data.meshes.remove(data)
elif isinstance(data, bpy.types.Curve):
bpy.data.curves.remove(C.object.data)
@classmethod
def delete_object(cls, obj):
@@ -204,18 +255,33 @@ class Drawing(blenderbim.core.tool.Drawing):
return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
@classmethod
def get_document_uri(cls, document):
if hasattr(document, "Identification"):
name = document.Identification or "X"
else:
name = document.DocumentId or "X"
name += " - " + (document.Name or "Unnamed")
if not hasattr(document, "Scope"):
return
if document.Scope == "DOCUMENTATION":
return os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets", name + ".svg")
elif document.Scope == "SCHEDULE":
return os.path.join(bpy.context.scene.BIMProperties.data_dir, "schedules", name + ".svg")
def get_document_uri(cls, document, description=None):
if getattr(document, "Location", None):
if os.path.isabs(document.Location):
return document.Location
ifc_path = tool.Ifc.get_path()
if os.path.isfile(ifc_path):
ifc_path = os.path.dirname(ifc_path)
return os.path.abspath(os.path.join(ifc_path, document.Location))
if document.is_a("IfcDocumentInformation"):
if tool.Ifc.get_schema() == "IFC2X3":
references = document.DocumentReferences
else:
references = document.HasDocumentReferences
for reference in references:
if description and reference.Description != description:
continue
location = cls.get_document_uri(reference)
if location:
return location
@classmethod
def get_path_filename(cls, path):
return os.path.splitext(os.path.basename(path))[0]
@classmethod
def get_path_with_ext(cls, path, ext):
return os.path.splitext(path)[0] + f".{ext}"
@classmethod
def get_drawing_collection(cls, drawing):
@@ -229,6 +295,12 @@ class Drawing(blenderbim.core.tool.Drawing):
if rel.is_a("IfcRelAssignsToGroup"):
return rel.RelatingGroup
@classmethod
def get_drawing_document(cls, drawing):
for rel in drawing.HasAssociations:
if rel.is_a("IfcRelAssociatesDocument"):
return rel.RelatingDocument
@classmethod
def get_drawing_references(cls, drawing):
results = set()
@@ -258,12 +330,6 @@ class Drawing(blenderbim.core.tool.Drawing):
def get_name(cls, element):
return element.Name
@classmethod
def get_schedule_location(cls, schedule):
if tool.Ifc.get_schema() == "IFC2X3":
return schedule.DocumentReferences[0].Location
return schedule.Location
@classmethod
def generate_drawing_matrix(cls, target_view, location_hint):
x = 0 if location_hint == 0 else bpy.context.scene.cursor.matrix[0][3]
@@ -501,7 +567,7 @@ class Drawing(blenderbim.core.tool.Drawing):
props = bpy.context.scene.DocProperties
expanded_sheets = {s.ifc_definition_id for s in props.sheets if s.is_expanded}
props.sheets.clear()
sheets = [d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "DOCUMENTATION"]
sheets = [d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "SHEET"]
for sheet in sheets:
new = props.sheets.add()
new.ifc_definition_id = sheet.id()
@@ -517,22 +583,19 @@ class Drawing(blenderbim.core.tool.Drawing):
continue
for reference in cls.get_document_references(sheet):
if reference.Description == "LAYOUT":
continue # The layout itself is an internal detail and should not be visible to users
new = props.sheets.add()
new.ifc_definition_id = reference.id()
new.is_sheet = False
if tool.Ifc.get_schema() == "IFC2X3":
new.identification = reference.ItemReference or "X"
new.identification = reference.ItemReference or ""
else:
new.identification = reference.Identification or "X"
new.identification = reference.Identification or ""
element = cls.get_reference_element(reference)
if element:
new.name = element.Name
new.reference_type = "DRAWING"
else:
new.name = cls.get_reference_document(reference).Name or "Unnamed"
new.reference_type = "SCHEDULE"
new.name = os.path.basename(reference.Location)
new.reference_type = reference.Description
@classmethod
def import_text_attributes(cls, obj):
@@ -674,6 +737,28 @@ class Drawing(blenderbim.core.tool.Drawing):
return location_hint + " " + target_view.split("_")[0]
return target_view
@classmethod
def get_default_sheet_path(cls, identification, name):
return os.path.join(
bpy.context.scene.DocProperties.sheets_dir, cls.sanitise_filename(f"{identification} - {name}.svg")
)
@classmethod
def get_default_titleblock_path(cls, name):
return os.path.join(bpy.context.scene.DocProperties.titleblocks_dir, cls.sanitise_filename(f"{name}.svg"))
@classmethod
def get_default_drawing_path(cls, name):
return os.path.join(bpy.context.scene.DocProperties.drawings_dir, cls.sanitise_filename(f"{name}.svg"))
@classmethod
def sanitise_filename(cls, name):
return "".join(x for x in name if (x.isalnum() or x in "._- "))
@classmethod
def get_default_drawing_resource_path(cls, resource):
return getattr(bpy.context.scene.DocProperties, f"{resource.lower()}_path") or None
@classmethod
def get_potential_reference_elements(cls, drawing):
elements = []
@@ -1041,11 +1126,20 @@ class Drawing(blenderbim.core.tool.Drawing):
def replace_text_literal_variables(cls, text, product):
if not product:
return text
for command in re.findall("``.*?``", text):
original_command = command
for variable in re.findall("{{.*?}}", command):
value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2])
command = command.replace(variable, repr(value))
text = text.replace(original_command, str(eval(command[2:-2])))
for variable in re.findall("{{.*?}}", text):
value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2])
if isinstance(value, (list, tuple)):
value = ", ".join(str(v) for v in value)
text = text.replace(variable, str(value))
return text
@classmethod
@@ -1197,3 +1291,13 @@ class Drawing(blenderbim.core.tool.Drawing):
project_collection.children["Views"].children[camera.users_collection[0].name].hide_viewport = False
bpy.data.collections.get(camera.users_collection[0].name).hide_render = False
tool.Spatial.set_active_object(camera)
# sync viewport objects visibility with selectors from EPset_Drawing/Include and /Exclude
drawing_elements = cls.get_drawing_elements(tool.Ifc.get_entity(camera))
for element in tool.Ifc.get().by_type("IfcElement"):
if element.is_a() in ("IfcOpeningElement",):
continue
obj = tool.Ifc.get_object(element)
obj.hide_viewport = element not in drawing_elements
obj.hide_render = element not in drawing_elements
+53 -13
View File
@@ -77,6 +77,11 @@ class Geometry(blenderbim.core.tool.Geometry):
if obj.data:
return obj.data.copy()
@classmethod
def get_active_representation(cls, obj):
if obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.ifc_definition_id:
return tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
@classmethod
def get_cartesian_point_coordinate_offset(cls, obj):
props = bpy.context.scene.BIMGeoreferenceProperties
@@ -124,21 +129,32 @@ class Geometry(blenderbim.core.tool.Geometry):
@classmethod
def get_mesh_checksum(cls, mesh):
# Get mesh data
vertices = mesh.vertices[:]
edges = mesh.edges[:]
faces = mesh.polygons[:]
data_bytes = b""
if isinstance(mesh, bpy.types.Mesh):
vertices = mesh.vertices[:]
edges = mesh.edges[:]
faces = mesh.polygons[:]
# Convert mesh data to bytes
data_bytes = b''
for v in vertices:
data_bytes += struct.pack('3f', *v.co)
for e in edges:
data_bytes += struct.pack('2i', *e.vertices)
for f in faces:
data_bytes += struct.pack('%di' % len(f.vertices), *f.vertices)
# Convert mesh data to bytes
for v in vertices:
data_bytes += struct.pack("3f", *v.co)
for e in edges:
data_bytes += struct.pack("2i", *e.vertices)
for f in faces:
data_bytes += struct.pack("%di" % len(f.vertices), *f.vertices)
elif isinstance(mesh, bpy.types.Curve):
splines = mesh.splines[:]
for spline in splines:
if spline.type == "BEZIER":
for bezier_point in spline.bezier_points:
data_bytes += struct.pack("3f", *bezier_point.co)
data_bytes += struct.pack("3f", *bezier_point.handle_left)
data_bytes += struct.pack("3f", *bezier_point.handle_right)
else:
for point in spline.points:
data_bytes += struct.pack("4f", *point.co)
# Generate hash of mesh data
hasher = hashlib.sha1()
hasher.update(data_bytes)
return hasher.hexdigest()
@@ -256,6 +272,30 @@ class Geometry(blenderbim.core.tool.Geometry):
def is_mapped_representation(cls, representation):
return representation.RepresentationType == "MappedRepresentation"
@classmethod
def is_meshlike(cls, representation):
if representation.RepresentationType in (
"AdvancedBrep",
"Annotation2D",
"BoundingBox",
"Brep",
"Curve",
"Curve2D",
"Curve3D",
"FillArea",
"GeometricCurveSet",
"GeometricSet",
"Point",
"PointCloud",
"Surface",
"Surface2D",
"Surface3D",
"SurfaceModel",
"Tessellation",
):
return True
return False
@classmethod
def is_type_product(cls, element):
return element.is_a("IfcTypeProduct")
+20 -1
View File
@@ -46,6 +46,11 @@ class Ifc(blenderbim.core.tool.Ifc):
if IfcStore.get_file():
return IfcStore.get_file().schema
@classmethod
def has_changed_shading(cls, obj):
checksum = obj.BIMMaterialProperties.shading_checksum
return checksum != repr(np.array(obj.diffuse_color).tobytes())
@classmethod
def is_deleted(cls, element):
return element.id() in IfcStore.deleted_ids
@@ -101,7 +106,21 @@ class Ifc(blenderbim.core.tool.Ifc):
@classmethod
def resolve_uri(cls, uri):
return uri if not uri or os.path.isabs(uri) else os.path.join(os.path.dirname(cls.get_path()), uri)
if os.path.isabs(uri):
return uri
ifc_path = cls.get_path()
if os.path.isfile(ifc_path):
ifc_path = os.path.dirname(ifc_path)
return uri if not uri or os.path.isabs(uri) else os.path.join(ifc_path, uri)
@classmethod
def get_relative_uri(cls, uri):
if not os.path.isabs(uri):
return uri
ifc_path = cls.get_path()
if os.path.isfile(ifc_path):
ifc_path = os.path.dirname(ifc_path)
return os.path.relpath(uri, ifc_path)
@classmethod
def unlink(cls, element=None, obj=None):
+13 -1
View File
@@ -414,12 +414,24 @@ class Model(blenderbim.core.tool.Model):
obj_stack = [parent]
for array in data:
if array["sync_children"]:
removed_children = set(array["children"])
for removed_child in removed_children:
element = tool.Ifc.get().by_guid(removed_child)
obj = tool.Ifc.get_object(element)
if obj:
bpy.data.objects.remove(obj)
child_i = 0
existing_children = set(array["children"])
total_existing_children = len(array["children"])
children_elements = []
children_objs = []
base_offset = Vector([array["x"], array["y"], array["z"]]) * unit_scale
if array['dimension_input_type'] == "Total":
divider = 1 if ((array["count"] - 1) == 0) else (array["count"] - 1)
base_offset = Vector([array["x"] / divider, array["y"] / divider, array["z"] / divider]) * unit_scale
else:
base_offset = Vector([array["x"], array["y"], array["z"]]) * unit_scale
for i in range(array["count"]):
if i == 0:
continue
+2 -1
View File
@@ -157,10 +157,11 @@ class Qto(blenderbim.core.tool.Qto):
print(f"RELATING COST QUANTITY NAME: {relating_cost_item["quantity_name"]}")
...
"""
quantities = cls.get_base_qto(product).Quantities
model = tool.Ifc.get()
cost_items = model.by_type("IfcCostItem")
result = []
base_qto = cls.get_base_qto(product)
quantities = base_qto.Quantities if base_qto else []
for cost_item in cost_items:
cost_item_quantities = cost_item.CostQuantities if cost_item.CostQuantities is not None else []
+29 -3
View File
@@ -422,7 +422,18 @@ class Sequence(blenderbim.core.tool.Sequence):
@classmethod
def get_task_inputs(cls, task):
return ifcopenshell.util.sequence.get_task_inputs(task)
is_deep = bpy.context.scene.BIMWorkScheduleProperties.show_nested_inputs
return ifcopenshell.util.sequence.get_task_inputs(task, is_deep)
@classmethod
def get_task_outputs(cls, task):
is_deep = bpy.context.scene.BIMWorkScheduleProperties.show_nested_outputs
return ifcopenshell.util.sequence.get_task_outputs(task, is_deep)
@classmethod
def get_task_resources(cls, task):
is_deep = bpy.context.scene.BIMWorkScheduleProperties.show_nested_resources
return ifcopenshell.util.sequence.get_task_resource(task, is_deep)
@classmethod
def load_task_inputs(cls, inputs):
@@ -459,7 +470,7 @@ class Sequence(blenderbim.core.tool.Sequence):
@classmethod
def get_task_outputs(cls, task):
is_deep = bpy.context.scene.BIMWorkScheduleProperties.is_nested_task_outputs
is_deep = bpy.context.scene.BIMWorkScheduleProperties.show_nested_outputs
return ifcopenshell.util.sequence.get_task_outputs(task, is_deep)
@classmethod
@@ -777,7 +788,7 @@ class Sequence(blenderbim.core.tool.Sequence):
displayed_tasks = [item.ifc_definition_id for item in task_props.tasks]
if not task.id() in displayed_tasks:
expand_ancestors(task)
task_index = [item.ifc_definition_id for item in task_props.tasks].index(task.id()) or 0
task_index = displayed_tasks.index(task.id()) or 0
bpy.context.scene.BIMWorkScheduleProperties.active_task_index = task_index
@classmethod
@@ -1500,3 +1511,18 @@ class Sequence(blenderbim.core.tool.Sequence):
f.write(pystache.render(t.read(), {"json_data": json.dumps(task_json)}))
webbrowser.open("file://" + os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.html"))
@classmethod
def load_product_tasks(cls, product):
props = bpy.context.scene.BIMWorkScheduleProperties
props.is_task_update_enabled = False
props.product_input_tasks.clear()
props.product_output_tasks.clear()
task_inputs, task_ouputs = ifcopenshell.util.sequence.get_tasks_for_product(product)
for task in task_inputs or []:
new = props.product_input_tasks.add()
new.name = task.Name or "Unnamed"
new.ifc_definition_id = task.id()
for task in task_ouputs or []:
new = props.product_output_tasks.add()
new.name = task.Name or "Unnamed"
new.ifc_definition_id = task.id()
+11
View File
@@ -3,6 +3,17 @@ Let's get started
The BlenderBIM Add-on lets you analyse, create, and modify OpenBIM with Blender.
Quickstart video tutorial
-------------------------
Learn how to model a small building and create simple architectural plans, sections, 2D details, and sheet layouts in this short tutorial series.
`View all tutorial videos <https://www.youtube.com/playlist?list=PLMDcOjMJYxUPHHvEHqAsOuBdSPsp6or32>`__
.. raw:: html
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/videoseries?list=PLMDcOjMJYxUPHHvEHqAsOuBdSPsp6or32" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
.. note::
This documentation is incomplete. Would you like to help write more? `Get in touch! <https://osarch.org/chat/>`__
@@ -42,7 +42,7 @@ class LibraryGenerator:
ifcopenshell.api.run(
"project.assign_declaration", self.file, definition=self.library, relating_context=self.project
)
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", name="METRE", prefix="MILLI")
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", prefix="MILLI")
ifcopenshell.api.run("unit.assign_unit", self.file, units=[unit])
model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model")
@@ -56,7 +56,6 @@ def get_simple_2dcurve_data(coords, fillets=[], fillet_radius=[], closed=True, i
for co_i, co in enumerate(coords, 0):
current_point = len(points)
if co_i in fillets:
r = fillets[co_i]
rsb = r * cos(pi / 4) # radius shift big
rss = r - rsb # radius shift small
@@ -230,7 +229,7 @@ class LibraryGenerator:
ifcopenshell.api.run(
"project.assign_declaration", self.file, definition=self.library, relating_context=self.project
)
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", name="METRE", prefix="MILLI")
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", prefix="MILLI")
ifcopenshell.api.run("unit.assign_unit", self.file, units=[unit])
model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model")
@@ -386,23 +385,40 @@ class LibraryGenerator:
mirror_axis = V(0, 1)
output = []
seat = builder.rectangle(size=V(width, depth))
chair_back = builder.rectangle(size=V(width, thickness))
items_to_adjust = []
items_3d = []
chair_back = None
items_to_adjust.append(seat := builder.rectangle(size=V(width, depth)))
leg = builder.rectangle(size=V(thickness, thickness))
legs = [leg] + builder.mirror(
leg, mirror_axes=[V(1, 0), V(0, 1), V(1, 1)], mirror_point=V(width / 2, depth / 2), create_copy=True
)
builder.translate([seat, chair_back] + legs, shift_to_center)
builder.mirror([seat, chair_back] + legs, mirror_axis)
seat = builder.extrude(
builder.profile(seat), thickness, position=V(0, 0, seat_level), extrusion_vector=V(0, 0, -1)
)
chair_back = builder.extrude(
builder.profile(chair_back), height - seat_level, position=V(0, 0, height), extrusion_vector=V(0, 0, -1)
)
legs = [builder.extrude(leg, seat_level - thickness) for leg in legs]
items_to_adjust.extend(legs)
items_3d = [seat, chair_back] + legs
if height > seat_level:
items_to_adjust.append(chair_back := builder.rectangle(size=V(width, thickness)))
builder.translate(items_to_adjust, shift_to_center)
builder.mirror(items_to_adjust, mirror_axis)
items_3d.append(
builder.extrude(
builder.profile(seat), thickness, position=V(0, 0, seat_level), extrusion_vector=V(0, 0, -1)
)
)
if chair_back:
items_3d.append(
builder.extrude(
builder.profile(chair_back),
height - seat_level,
position=V(0, 0, height),
extrusion_vector=V(0, 0, -1),
)
)
items_3d.extend([builder.extrude(leg, seat_level - thickness) for leg in legs])
if return_representations:
representation_3d = builder.get_representation(
context=self.representations["model_body"], items=items_3d
@@ -1286,8 +1302,6 @@ class LibraryGenerator:
shift_to_center = V(-width / 2, 0)
mirror_axis = V(0, 1)
representation_3d, representation_2d = create_box_objects(width, depth, height, return_representations=True)
back_wall_depth = depth * 0.1
back_wall_border_mask_height = height * 0.1
back_wall_border_mask_depth = depth * 0.1
@@ -1955,12 +1969,17 @@ class LibraryGenerator:
representation_3d, representation_2d = create_box_objects(
overall_width, overall_depth, overall_height, return_representations=True
)
self.create_explicit_type("IfcSpaceType", parking_lot_name, representation_3d, representation_2d)
self.create_explicit_type(
"IfcSpaceType", parking_lot_name, representation_3d, representation_2d, PredefinedType="PARKING"
)
self.file.write(output_filename)
def create_explicit_type(self, ifc_class, name, representation_3d, representation_2d):
def create_explicit_type(self, ifc_class, name, representation_3d, representation_2d, **params):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name)
for param, value in params.items():
setattr(element, param, value)
ifcopenshell.api.run(
"geometry.assign_representation", self.file, product=element, representation=representation_3d
)
@@ -152,30 +152,9 @@ class LibraryGenerator:
ifcopenshell.api.run(
"project.assign_declaration", self.file, definition=self.library, relating_context=self.project
)
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", name="METRE", prefix="MILLI")
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", prefix="MILLI")
ifcopenshell.api.run("unit.assign_unit", self.file, units=[unit])
model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model")
plan = ifcopenshell.api.run("context.add_context", self.file, context_type="Plan")
self.representations = {
"body": ifcopenshell.api.run(
"context.add_context",
self.file,
context_type="Model",
context_identifier="Body",
target_view="MODEL_VIEW",
parent=model,
),
"annotation": ifcopenshell.api.run(
"context.add_context",
self.file,
context_type="Plan",
context_identifier="Annotation",
target_view="PLAN_VIEW",
parent=plan,
),
}
self.material = ifcopenshell.api.run("material.add_material", self.file, name="Unknown")
# parameters could be optional (example: welded i-beams don't have FilletRadius)
@@ -245,29 +224,6 @@ class LibraryGenerator:
self.file.write(output_filename)
def create_layer_set_type(self, name, data):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=data["type"], name=name)
element.Description = data["Description"]
rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialLayerSet")
layer_set = rel.RelatingMaterial
for layer_data in data["Layers"]:
layer = ifcopenshell.api.run(
"material.add_layer", self.file, layer_set=layer_set, material=self.materials[layer_data[1]]["ifc"]
)
layer.Name = layer_data[0]
layer.LayerThickness = layer_data[2]
ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
return element
def create_layer_type(self, ifc_class, name, thickness):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name)
rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialLayerSet")
layer_set = rel.RelatingMaterial
layer = ifcopenshell.api.run("material.add_layer", self.file, layer_set=layer_set, material=self.materials["TBD"]["ifc"])
layer.LayerThickness = thickness
ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
return element
def create_profile_type(self, ifc_class, name, profile):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name)
rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialProfileSet")
@@ -279,38 +235,6 @@ class LibraryGenerator:
ifcopenshell.api.run("material.assign_profile", self.file, material_profile=material_profile, profile=profile)
ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
def create_type(self, ifc_class, name, representations):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name)
for rep_name, obj_name in representations.items():
obj = bpy.data.objects.get(obj_name)
representation = ifcopenshell.api.run(
"geometry.add_representation",
self.file,
context=self.representations[rep_name],
blender_object=obj,
geometry=obj.data,
total_items=max(1, len(obj.material_slots)),
)
styles = []
for slot in obj.material_slots:
style = ifcopenshell.api.run("style.add_style", self.file, name=slot.material.name)
ifcopenshell.api.run(
"style.add_surface_style",
self.file,
style=style,
ifc_class="IfcSurfaceStyleRendering",
attributes=tool.Style.get_surface_rendering_attributes(slot.material),
)
styles.append(style)
if styles:
ifcopenshell.api.run(
"style.assign_representation_styles", self.file, shape_representation=representation, styles=styles
)
ifcopenshell.api.run(
"geometry.assign_representation", self.file, product=element, representation=representation
)
ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
if __name__ == "__main__":
path = Path(__file__).parents[1] / "blenderbim/bim/data/libraries"
@@ -4,6 +4,7 @@ from mathutils import Vector
V = lambda *x: Vector([float(i) for i in x])
# TODO: move examples to more suitable place
def simple_uses():
ifc_file = ifcopenshell.file()
@@ -91,7 +92,7 @@ def mirror_placement_test():
"root.create_entity", ifc_file, ifc_class="IfcProjectLibrary", name=f"Non-structural assets library"
)
ifcopenshell.api.run("project.assign_declaration", ifc_file, definition=library, relating_context=project)
unit = ifcopenshell.api.run("unit.add_si_unit", ifc_file, unit_type="LENGTHUNIT", name="METRE", prefix="MILLI")
unit = ifcopenshell.api.run("unit.add_si_unit", ifc_file, unit_type="LENGTHUNIT", prefix="MILLI")
ifcopenshell.api.run("unit.assign_unit", ifc_file, units=[unit])
model = ifcopenshell.api.run("context.add_context", ifc_file, context_type="Model")
plan = ifcopenshell.api.run("context.add_context", ifc_file, context_type="Plan")
@@ -165,7 +166,7 @@ def curve_between_two_points_test():
"root.create_entity", ifc_file, ifc_class="IfcProjectLibrary", name=f"Non-structural assets library"
)
ifcopenshell.api.run("project.assign_declaration", ifc_file, definition=library, relating_context=project)
unit = ifcopenshell.api.run("unit.add_si_unit", ifc_file, unit_type="LENGTHUNIT", name="METRE", prefix="MILLI")
unit = ifcopenshell.api.run("unit.add_si_unit", ifc_file, unit_type="LENGTHUNIT", prefix="MILLI")
ifcopenshell.api.run("unit.assign_unit", ifc_file, units=[unit])
model = ifcopenshell.api.run("context.add_context", ifc_file, context_type="Model")
plan = ifcopenshell.api.run("context.add_context", ifc_file, context_type="Plan")
+25 -3
View File
@@ -87,10 +87,11 @@ class TestAddSheet:
drawing.generate_sheet_identification().should_be_called().will_return("identification")
drawing.ensure_unique_identification("identification").should_be_called().will_return("u_identification")
ifc.get_schema().should_be_called().will_return("IFC4")
drawing.get_default_sheet_path("u_identification", "UNTITLED").should_be_called().will_return("uri")
ifc.run(
"document.edit_information",
information="sheet",
attributes={"Identification": "u_identification", "Name": "UNTITLED", "Scope": "DOCUMENTATION"},
attributes={"Identification": "u_identification", "Name": "UNTITLED", "Scope": "DOCUMENTATION", "Location": "uri"},
).should_be_called()
drawing.create_svg_sheet("sheet", "titleblock").should_be_called()
drawing.import_sheets().should_be_called()
@@ -141,18 +142,21 @@ class TestDisableEditingSchedules:
class TestAddSchedule:
def test_run(self, ifc, drawing):
ifc.run("document.add_information").should_be_called().will_return("schedule")
drawing.get_path_filename("uri").should_be_called().will_return("UNTITLED")
ifc.run("document.add_reference", information="schedule").should_be_called().will_return("reference")
ifc.get_schema().should_be_called().will_return("IFC4")
ifc.run(
"document.edit_information",
information="schedule",
attributes={"Identification": "X", "Name": "UNTITLED", "Scope": "SCHEDULE", "Location": "uri"},
attributes={"Identification": "X", "Name": "UNTITLED", "Scope": "SCHEDULE"},
).should_be_called()
ifc.run("document.edit_reference", reference="reference", attributes={"Location": "uri"}).should_be_called()
drawing.import_schedules().should_be_called()
subject.add_schedule(ifc, drawing, uri="uri")
def test_using_a_document_id_in_ifc2x3(self, ifc, drawing):
ifc.run("document.add_information").should_be_called().will_return("schedule")
drawing.get_path_filename("uri").should_be_called().will_return("UNTITLED")
ifc.run("document.add_reference", information="schedule").should_be_called().will_return("reference")
ifc.get_schema().should_be_called().will_return("IFC2X3")
ifc.run(
@@ -174,7 +178,7 @@ class TestRemoveSchedule:
class TestOpenSchedule:
def test_run(self, drawing):
drawing.get_schedule_location("schedule").should_be_called().will_return("uri")
drawing.get_document_uri("schedule").should_be_called().will_return("uri")
drawing.open_spreadsheet("uri").should_be_called()
subject.open_schedule(drawing, schedule="schedule")
@@ -225,6 +229,10 @@ class TestAddDrawing:
ifc.run("group.assign_group", group="group", products=["element"]).should_be_called()
collector.assign("obj").should_be_called()
ifc.run("pset.add_pset", product="element", name="EPset_Drawing").should_be_called().will_return("pset")
drawing.get_default_drawing_resource_path("Stylesheet").should_be_called().will_return("stylesheet.css")
drawing.get_default_drawing_resource_path("Markers").should_be_called().will_return("markers.svg")
drawing.get_default_drawing_resource_path("Symbols").should_be_called().will_return("symbols.svg")
drawing.get_default_drawing_resource_path("Patterns").should_be_called().will_return("patterns.svg")
ifc.run(
"pset.edit_pset",
pset="pset",
@@ -236,8 +244,19 @@ class TestAddDrawing:
"HasLinework": True,
"HasAnnotation": True,
"GlobalReferencing": True,
"Stylesheet": "stylesheet.css",
"Markers": "markers.svg",
"Symbols": "symbols.svg",
"Patterns": "patterns.svg",
},
).should_be_called()
drawing.get_default_drawing_path("name").should_be_called().will_return("uri")
ifc.run("document.add_information").should_be_called().will_return("information")
ifc.run("document.add_reference", information="information").should_be_called().will_return("reference")
ifc.get_schema().should_be_called().will_return("IFC4")
ifc.run("document.edit_information", information="information", attributes={"Identification": "X", "Name": "name", "Scope": "DRAWING"}).should_be_called()
ifc.run("document.edit_reference", reference="reference", attributes={"Location": "uri"}).should_be_called()
ifc.run("document.assign_document", product="element", document="reference").should_be_called()
drawing.import_drawings().should_be_called()
subject.add_drawing(ifc, collector, drawing, target_view="target_view", location_hint="location_hint")
@@ -277,6 +296,8 @@ class TestRemoveDrawing:
ifc.get_object("reference").should_be_called().will_return("reference_obj")
drawing.delete_object("reference_obj").should_be_called()
ifc.run("root.remove_product", product="reference").should_be_called()
drawing.get_drawing_document("drawing").should_be_called().will_return("information")
ifc.run("document.remove_information", information="information").should_be_called()
ifc.run("root.remove_product", product="drawing").should_be_called()
drawing.import_drawings().should_be_called()
subject.remove_drawing(ifc, drawing, drawing="drawing")
@@ -321,6 +342,7 @@ class TestAddAnnotation:
drawing.get_drawing_group("drawing").should_be_called().will_return("group")
ifc.run("group.assign_group", group="group", products=["element"]).should_be_called()
collector.assign("obj").should_be_called()
drawing.setup_annotation_object("obj", "object_type").should_be_called()
drawing.enable_editing("obj").should_be_called()
subject.add_annotation(ifc, collector, drawing, drawing="drawing", object_type="object_type")
+33 -36
View File
@@ -61,9 +61,15 @@ class TestCreateCamera(NewFile):
class TestCreateSvgSheet(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="DOCUMENTATION")
subject.create_svg_sheet(document, "A1")
assert os.path.isfile(os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets", "X - FOOBAR.svg"))
document = ifc.createIfcDocumentInformation(
Identification="X",
Name="FOOBAR",
Scope="DOCUMENTATION",
Location=os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", "X - FOOBAR.svg"),
)
uri = subject.create_svg_sheet(document, "A1")
assert uri.endswith(".svg")
assert os.path.isfile(uri)
class TestDeleteCollection(NewFile):
@@ -204,11 +210,13 @@ class TestEnsureUniqueIdentification(NewFile):
class TestExportTextLiteralAttributes(NewFile):
def test_run(self):
TestImportTextAttributes().test_run()
assert subject.export_text_literal_attributes(bpy.data.objects.get("Object")) == [{
"Literal": "Literal",
"Path": "RIGHT",
"BoxAlignment": "bottom-left",
}]
assert subject.export_text_literal_attributes(bpy.data.objects.get("Object")) == [
{
"Literal": "Literal",
"Path": "RIGHT",
"BoxAlignment": "bottom-left",
}
]
class TestGetAnnotationContext(NewFile):
@@ -236,23 +244,29 @@ class TestGetBodyContext(NewFile):
class TestGetDocumentUri(NewFile):
def test_get_sheet_uri(self):
def test_run(self):
ifc = ifcopenshell.file()
document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="DOCUMENTATION")
result = subject.get_document_uri(document)
assert result == os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets", "X - FOOBAR.svg")
document = ifc.createIfcDocumentInformation(
Identification="X", Name="FOOBAR", Scope="SHEET", Location="Location"
)
assert subject.get_document_uri(document) == os.path.abspath(os.path.join(tool.Ifc.get_path(), "Location"))
def test_get_schedule_uri(self):
def test_get_indirect_locations(self):
ifc = ifcopenshell.file()
document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="SCHEDULE")
result = subject.get_document_uri(document)
assert result == os.path.join(bpy.context.scene.BIMProperties.data_dir, "schedules", "X - FOOBAR.svg")
document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="SHEET")
reference = ifc.createIfcDocumentReference(Location="Location", ReferencedDocument=document)
assert subject.get_document_uri(document) == os.path.abspath(os.path.join(tool.Ifc.get_path(), "Location"))
assert subject.get_document_uri(reference) == os.path.abspath(os.path.join(tool.Ifc.get_path(), "Location"))
def test_run_ifc2x3(self):
ifc = ifcopenshell.file(schema="IFC2X3")
document = ifc.createIfcDocumentInformation(DocumentId="X", Name="FOOBAR", Scope="DOCUMENTATION")
result = subject.get_document_uri(document)
assert result == os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets", "X - FOOBAR.svg")
tool.Ifc.set(ifc)
reference = ifc.createIfcDocumentReference(Location="Location")
document = ifc.createIfcDocumentInformation(
DocumentId="X", Name="FOOBAR", Scope="SHEET", DocumentReferences=[reference]
)
assert subject.get_document_uri(document) == os.path.abspath(os.path.join(tool.Ifc.get_path(), "Location"))
assert subject.get_document_uri(reference) == os.path.abspath(os.path.join(tool.Ifc.get_path(), "Location"))
class TestGetDrawingCollection(NewFile):
@@ -311,23 +325,6 @@ class TestGetName(NewFile):
assert subject.get_name(ifc.createIfcWall(Name="Foobar")) == "Foobar"
class TestGetScheduleLocation(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
schedule = ifcopenshell.api.run("document.add_information", ifc)
schedule.Location = "uri"
reference = ifcopenshell.api.run("document.add_reference", ifc, information=schedule)
subject.get_schedule_location(schedule) == "uri"
def test_run_ifc2x3(self):
ifc = ifcopenshell.file(schema="IFC2X3")
tool.Ifc.set(ifc)
reference = ifc.createIfcDocumentReference(Location="uri")
schedule = ifc.createIfcDocumentInformation(DocumentReferences=[reference])
subject.get_schedule_location(schedule) == "uri"
class TestGenerateDrawingMatrix(NewFile):
def test_returning_the_origin_as_a_fallback(self):
assert subject.generate_drawing_matrix("PLAN_VIEW", None) == mathutils.Matrix()
+1 -1
View File
@@ -138,7 +138,7 @@ class Csv2Ifc:
if unit:
return unit
else:
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type=unit_type, name=ifcopenshell.util.unit.si_type_names.get(unit_type, None))
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type=unit_type)
# unit = self.file.createIfcContextDependentUnit(
# self.file.createIfcDimensionalExponents(0, 0, 0, 0, 0, 0, 0), "USERDEFINED", symbol
# )
@@ -320,7 +320,7 @@ def import_ifc(filename, use_names, process_relations, blender_booleans):
class ImportIFC(bpy.types.Operator, ImportHelper):
bl_idname = "import_scene.ifc"
bl_label = "Import .ifc file"
bl_label = "Import .ifc File"
filename_ext = ".ifc"
filter_glob: StringProperty(default="*.ifc", options={"HIDDEN"})
@@ -20,6 +20,7 @@ import ifcopenshell.util.unit
from ifcopenshell.util.shape_builder import ShapeBuilder, V
from ifcopenshell.api.geometry.add_window_representation import create_ifc_window
from mathutils import Vector
from math import cos, radians
import collections
@@ -31,6 +32,9 @@ SUPPORTED_DOOR_TYPES = (
"DOUBLE_SWING_LEFT",
"DOUBLE_DOOR_SINGLE_SWING",
"DOUBLE_DOOR_DOUBLE_SWING",
"SLIDING_TO_LEFT",
"SLIDING_TO_RIGHT",
"DOUBLE_DOOR_SLIDING",
)
@@ -57,8 +61,15 @@ def create_ifc_door_lining(
V(th_side, 0.0, 0.0),
]
points = [p.xz for p in points]
door_lining = builder.polyline(points, closed=True)
door_lining = builder.extrude(door_lining, size.y, extrusion_vector=V(0, 1, 0))
door_lining = builder.extrude(
door_lining,
size.y,
position_x_axis=V(1, 0, 0),
position_z_axis=V(0, -1, 0),
extrusion_vector=V(0, 0, -1),
)
builder.translate(door_lining, position)
return door_lining
@@ -146,6 +157,7 @@ class Usecase:
door_type = self.settings["operation_type"]
double_swing_door = "DOUBLE_SWING" in door_type
double_door = "DOUBLE_DOOR" in door_type
sliding_door = "SLIDING" in door_type
if door_type not in SUPPORTED_DOOR_TYPES:
raise NotImplementedError(f'Door type "{door_type}" is not currently supported.')
@@ -162,8 +174,11 @@ class Usecase:
lining_depth = lining_props["LiningDepth"]
lining_thickness_default = lining_props["LiningThickness"]
lining_offset = lining_props["LiningOffset"]
lining_to_panel_offset_x = lining_props["LiningToPanelOffsetX"]
lining_to_panel_offset_y_full = lining_props["LiningToPanelOffsetY"]
lining_to_panel_offset_x = (
lining_props["LiningToPanelOffsetX"] if not sliding_door else lining_thickness_default
)
panel_depth = panel_props["PanelDepth"]
lining_to_panel_offset_y_full = lining_props["LiningToPanelOffsetY"] if not sliding_door else -panel_depth
transom_thickness = lining_props["TransomThickness"] / 2
transfom_offset = lining_props["TransomOffset"]
@@ -172,10 +187,10 @@ class Usecase:
window_lining_height = overall_height - transfom_offset - transom_thickness
side_lining_thickness = lining_thickness_default
panel_lining_overlap_x = max(lining_thickness_default - lining_to_panel_offset_x, 0)
panel_lining_overlap_x = max(lining_thickness_default - lining_to_panel_offset_x, 0) if not sliding_door else 0
top_lining_thickness = transom_thickness or lining_thickness_default
panel_top_lining_overlap_x = max(top_lining_thickness - lining_to_panel_offset_x, 0)
panel_top_lining_overlap_x = max(top_lining_thickness - lining_to_panel_offset_x, 0) if not sliding_door else 0
door_opening_width = overall_width - lining_to_panel_offset_x * 2
if double_swing_door:
side_lining_thickness = side_lining_thickness - panel_lining_overlap_x
@@ -190,7 +205,6 @@ class Usecase:
casing_depth = lining_props["CasingDepth"]
# panel params
panel_depth = panel_props["PanelDepth"]
panel_width = door_opening_width * panel_props["PanelWidth"]
frame_depth = panel_props["FrameDepth"]
frame_thickness = panel_props["FrameThickness"]
@@ -201,6 +215,7 @@ class Usecase:
handle_size = self.convert_si_to_unit(V(120, 40, 20) * 0.001)
handle_offset = self.convert_si_to_unit(V(60, 0, 1000) * 0.001) # to the handle center
handle_center_offset = V(handle_size.y / 2, 0, handle_size.z) / 2
slider_arrow_symbol_size = self.convert_si_to_unit(30 * 0.001)
if transfom_offset:
panel_height = transfom_offset + transom_thickness - lining_to_panel_offset_x - threshold_thickness
@@ -221,8 +236,44 @@ class Usecase:
# create 2d representation
if self.settings["context"].TargetView == "PLAN_VIEW":
items_2d = []
door_items = []
panel_size = V(panel_width, panel_depth)
if not sliding_door:
panel_position = V(lining_to_panel_offset_x, lining_depth)
else:
panel_position = V(lining_to_panel_offset_x, -panel_size.y)
if self.settings["context"].ContextIdentifier == "Annotation":
# only sliding door has annotation representation
if not sliding_door:
return None
# arrow symbol
arrow_symbol = []
arrow_offset = slider_arrow_symbol_size / cos(radians(15))
arrow_symbol.append(
builder.polyline(
points=(V(0.35 * panel_size.x, 0), V(0.65 * panel_size.x, 0)),
)
)
arrow_symbol.append(
builder.polyline(
points=(
V(slider_arrow_symbol_size, arrow_offset),
V(0, 0),
V(slider_arrow_symbol_size, -arrow_offset),
),
position_offset=V(0.35 * panel_size.x, 0),
)
)
builder.translate(arrow_symbol, panel_position + V(0, -arrow_offset * 1.5))
items_2d.extend(arrow_symbol)
representation_2d = builder.get_representation(self.settings["context"], items_2d, "Curve2D")
return representation_2d
door_items = []
# create lining
if l_shape_check([side_lining_thickness]):
lining_points = [
@@ -243,7 +294,10 @@ class Usecase:
)
# TODO: make second swing lines dashed
def create_ifc_door_panel_2d(panel_size, panel_position, door_swing_type):
def create_ifc_door_panel_2d(panel_size, panel_position, door_swing_type, sliding=False):
if sliding:
return create_ifc_door_sliding_panel_2d(panel_size, panel_position, door_swing_type)
door_items = []
panel_size = panel_size.yx
# create semi-semi-circle
@@ -274,13 +328,18 @@ class Usecase:
builder.mirror(door_items, mirror_axes=V(1, 0), mirror_point=mirror_point)
return door_items
door_items = []
panel_size = V(panel_width, panel_depth)
panel_position = V(lining_to_panel_offset_x, lining_depth)
def create_ifc_door_sliding_panel_2d(panel_size, panel_position, door_swing_type):
door = builder.rectangle(panel_size, position=panel_position - V(panel_size.x * 0.5, 0))
if door_swing_type == "RIGHT":
mirror_point = panel_position + V(panel_size.x / 2, 0)
builder.mirror(door, mirror_axes=V(1, 0), mirror_point=mirror_point)
return [door]
door_items = []
if double_door:
panel_size.x = panel_size.x / 2
door_items.extend(create_ifc_door_panel_2d(panel_size, panel_position, "LEFT"))
door_items.extend(create_ifc_door_panel_2d(panel_size, panel_position, "LEFT", sliding_door))
mirror_point = panel_position + V(door_opening_width / 2, 0)
door_items.extend(
@@ -288,7 +347,7 @@ class Usecase:
)
else:
door_swing_type = "LEFT" if door_type.endswith("LEFT") else "RIGHT"
door_items.extend(create_ifc_door_panel_2d(panel_size, panel_position, door_swing_type))
door_items.extend(create_ifc_door_panel_2d(panel_size, panel_position, door_swing_type, sliding_door))
items_2d.extend(door_items)
builder.translate(items_2d, V(0, lining_offset))
@@ -383,7 +442,7 @@ class Usecase:
panel_position = V(lining_to_panel_offset_x, lining_to_panel_offset_y_full, threshold_thickness)
if double_door:
# TODO: keep a little space between doors for readibility?
# keeping a little space between doors for readibility
double_door_offset = self.convert_si_to_unit(0.001)
panel_size.x = panel_size.x / 2 - double_door_offset
door_items.extend(create_ifc_door_panel(panel_size, panel_position, "LEFT"))
@@ -30,15 +30,15 @@ import collections
# - order of rows is from top of the window to bottom
DEFAULT_PANEL_SCHEMAS = {
"SINGLE_PANEL": [[0]],
"DOUBLE_PANEL_HORIZONTAL": [[0], [1]],
"DOUBLE_PANEL_VERTICAL": [[0, 1]],
"TRIPLE_PANEL_BOTTOM": [[0, 1], [2, 2]],
"TRIPLE_PANEL_TOP": [[0, 0], [1, 2]],
"TRIPLE_PANEL_LEFT": [[0, 1], [0, 2]],
"TRIPLE_PANEL_RIGHT": [[0, 1], [2, 1]],
"TRIPLE_PANEL_HORIZONTAL": [[0], [1], [2]],
"TRIPLE_PANEL_VERTICAL": [[0, 1, 2]],
"SINGLE_PANEL": [[0]],
"DOUBLE_PANEL_HORIZONTAL": [[0], [1]],
"DOUBLE_PANEL_VERTICAL": [[0, 1]],
"TRIPLE_PANEL_BOTTOM": [[0, 1], [2, 2]],
"TRIPLE_PANEL_TOP": [[0, 0], [1, 2]],
"TRIPLE_PANEL_LEFT": [[0, 1], [0, 2]],
"TRIPLE_PANEL_RIGHT": [[0, 1], [2, 1]],
"TRIPLE_PANEL_HORIZONTAL": [[0], [1], [2]],
"TRIPLE_PANEL_VERTICAL": [[0, 1, 2]],
}
@@ -54,13 +54,20 @@ def create_ifc_window_frame_simple(builder, size: Vector, thickness: list, posit
th_left, th_up, th_right, th_bottom = thickness
panel_rect = builder.rectangle(size=size * V(1, 0, 1))
panel_rect = builder.rectangle(size=size.xz)
inner_rect_size = size - V(th_left + th_right, 0, th_bottom + th_up)
inner_rect = builder.rectangle(size=inner_rect_size * V(1, 0, 1), position=V(th_left, 0, th_bottom))
inner_rect = builder.rectangle(size=inner_rect_size.xz, position=V(th_left, th_bottom))
panel_profile = builder.profile(panel_rect, inner_curves=inner_rect)
panel_extruded = builder.extrude(panel_profile, size.y, extrusion_vector=V(0, 1, 0), position=position)
panel_extruded = builder.extrude(
panel_profile,
size.y,
position_x_axis=V(1, 0, 0),
position_z_axis=V(0, -1, 0),
extrusion_vector=V(0, 0, -1),
position=position,
)
return panel_extruded
@@ -133,7 +140,9 @@ def create_ifc_window(
glass = builder.extrude(
glass_rect,
glass_thickness,
extrusion_vector=V(0, 1, 0),
position_x_axis=V(1, 0, 0),
position_z_axis=V(0, -1, 0),
extrusion_vector=V(0, 0, -1),
position=glass_position,
)
@@ -100,7 +100,7 @@ class Usecase:
return element
def handle_2x3_defaults(self, element):
if element.is_a("IfcElement") or element.is_a("IfcElementType"):
if element.is_a("IfcElementType"):
if hasattr(element, "PredefinedType") and not element.PredefinedType:
element.PredefinedType = "NOTDEFINED"
@@ -117,7 +117,7 @@ class Usecase:
element.Sizeable = False
def handle_4_defaults(self, element):
if element.is_a("IfcElement") or element.is_a("IfcElementType"):
if element.is_a("IfcElementType"):
if hasattr(element, "PredefinedType") and not element.PredefinedType:
element.PredefinedType = "NOTDEFINED"
@@ -293,19 +293,25 @@ class Selector:
results = []
for prop_name, prop_value in value.items():
if re.match(key, prop_name):
results.append(prop_value)
if isinstance(prop_value, (list, tuple)):
results.extend(prop_value)
else:
results.append(prop_value)
value = results
else:
value = value.get(key, None)
elif isinstance(value, (list, tuple)): # If we use regex
results = []
for v in value:
subvalue = cls.get_element_value(v, [key], is_regex=is_regex)
if isinstance(subvalue, list):
results.extend(subvalue)
else:
results.append(subvalue)
value = results
if key.isnumeric():
value = value[int(key)]
else:
results = []
for v in value:
subvalue = cls.get_element_value(v, [key], is_regex=is_regex)
if isinstance(subvalue, list):
results.extend(subvalue)
else:
results.append(subvalue)
value = results
return value
@classmethod
@@ -326,26 +326,45 @@ def get_task_outputs(task, is_deep=False):
if not is_deep:
return get_direct_task_outputs(task)
else:
nested_tasks = get_all_nested_tasks(task)
return [
output
for nested_task in nested_tasks
for nested_task in get_all_nested_tasks(task)
for output in get_direct_task_outputs(nested_task)
]
def get_task_inputs(task, is_deep=False):
if not is_deep:
return [object for rel in task.OperatesOn if rel.is_a("IfcRelAssignsToProcess") for object in rel.RelatedObjects if object.is_a("IfcProduct")]
else:
return [
output
for nested_task in get_all_nested_tasks(task)
for output in [object for rel in nested_task.OperatesOn if rel.is_a("IfcRelAssignsToProcess") for object in rel.RelatedObjects if object.is_a("IfcProduct")]
]
def get_task_inputs(task):
inputs = []
for rel in task.OperatesOn:
for object in rel.RelatedObjects:
if object.is_a("IfcProduct"):
inputs.append(object)
return inputs
def get_task_resources(task, is_deep=False):
if not is_deep:
return [object for rel in task.OperatesOn if rel.is_a("IfcRelAssignsToProcess") for object in rel.RelatedObjects if object.is_a("IfcResource")]
else:
return [
output
for nested_task in get_all_nested_tasks(task)
for output in [object for rel in nested_task.OperatesOn if rel.is_a("IfcRelAssignsToProcess") for object in rel.RelatedObjects if object.is_a("IfcResource")]
]
def has_task_outputs(task):
return len(get_task_outputs(task)) > 0
def has_task_inputs(task):
return len(get_task_inputs(task)) > 0
def get_tasks_for_product(product):
inputs = []
outputs = []
for assignment in product.HasAssignments:
if assignment.is_a("IfcRelAssignsToProcess") and assignment.RelatingProcess.is_a("IfcTask"):
inputs.append(assignment.RelatingProcess)
for reference in product.ReferencedBy:
if reference.is_a("IfcRelAssignsToProduct") and reference.RelatedObjects[0].is_a("IfcTask"):
outputs.extend([obj for obj in reference.RelatedObjects if obj.is_a("IfcTask")])
return inputs, outputs
@@ -55,7 +55,6 @@ class ShapeBuilder:
return ifc_curve
def get_rectangle_coords(self, size: Vector = Vector((1.0, 1.0)).freeze(), position: Vector = None):
dimensions = len(size)
if not position:
@@ -184,9 +183,30 @@ class ShapeBuilder:
# because of that you can't create bool edges of outer_curve this way
# < returns IfcArbitraryClosedProfileDef or IfcArbitraryProfileDefWithVoids
if outer_curve.Dim != 2:
# TODO: replace with exception
print(
f"WARNING. Outer curve for IfcArbitraryClosedProfileDef/IfcIfcArbitraryProfileDefWithVoid should be 2D to be valid, currently it has {outer_curve.Dim} dimensions.\n"
"Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcArbitraryClosedProfileDef.htm#8.15.3.1.4-Formal-propositions"
)
import traceback
traceback.print_stack()
if inner_curves:
if not isinstance(inner_curves, collections.abc.Iterable):
inner_curves = [inner_curves]
# TODO: replace with exception
if any(curve.Dim != 2 for curve in inner_curves):
print(
"WARNING. InnerCurve for IfcIfcArbitraryProfileDefWithVoid sould be 2D to be valid, "
"currently on one of the inner curves is using different amount of dimensions.\n"
"Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcArbitraryClosedProfileDef.htm#8.15.3.1.4-Formal-propositions"
)
import traceback
traceback.print_stack()
profile = self.file.createIfcArbitraryProfileDefWithVoids(
ProfileName=name, ProfileType=profile_type, OuterCurve=outer_curve, InnerCurves=inner_curves
@@ -241,7 +261,6 @@ class ShapeBuilder:
def rotate_2d_point(
self, point_2d: Vector, angle=90, pivot_point: Vector = Vector((0.0, 0.0)).freeze(), counter_clockwise=False
):
# > angle - in degrees
# < rotated Vector
@@ -407,9 +426,9 @@ class ShapeBuilder:
if hasattr(c.SweptArea, "InnerCurves"):
for inner_curve in c.SweptArea.InnerCurves:
self.translate(inner_curve, base_position)
self.translate(inner_curve, base_position.to_2d())
self.mirror(inner_curve, mirror_axes, mirror_point, placement_matrix=placement_matrix)
self.translate(inner_curve, -new_position)
self.translate(inner_curve, -new_position.to_2d())
# extrusion converted to world space
base_extruded_direction = Vector(c.ExtrudedDirection.DirectionRatios)
@@ -471,6 +490,12 @@ class ShapeBuilder:
# > position_y_axis - optional, could be used to calculate Z-axis based on Y-axis
# < IfcExtrudedAreaSolid
if not magnitude:
raise Exception(
"Extrusion magnitude must be greater than 0 to be valid.\n"
"Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcPositiveLengthMeasure.htm#8.11.2.71.3-Formal-representation"
)
if profile_or_curve.is_a() not in ("IfcArbitraryClosedProfileDef", "IfcArbitraryProfileDefWithVoids"):
profile_or_curve = self.profile(profile_or_curve)
@@ -488,15 +513,24 @@ class ShapeBuilder:
)
return extruded_area
def get_representation(self, context, items):
def get_representation(self, context, items, representation_type=None):
# > items - could be a list or single curve/IfcExtrudedAreaSolid
# < IfcShapeRepresentation
if not isinstance(items, collections.abc.Iterable):
items = [items]
if not representation_type:
if items[0].is_a("IfcExtrudedAreaSolid"):
representation_type = "SweptSolid"
elif items[0].is_a("IfcCurve") and items[0].Dim == 3:
representation_type = "Curve3D"
else:
representation_type = "Curve2D"
representation = self.file.createIfcShapeRepresentation(
ContextOfItems=context,
RepresentationIdentifier=context.ContextIdentifier,
RepresentationType="SweptSolid" if items[0].is_a("IfcExtrudedAreaSolid") else "Curve2D",
RepresentationType=representation_type,
Items=items,
)
return representation
@@ -83,18 +83,34 @@ def get_connected_port(port):
def get_connected_to(element):
# Note: this code is for IFC2X3. IFC4 has a different approach.
results = []
for rel in element.HasPorts:
for rel2 in rel.RelatingPort.ConnectedTo:
results.extend([r.RelatedElement for r in rel2.RelatedPort.ContainedIn if r.RelatedElement != element])
return results
for port in ifcopenshell.util.system.get_ports(element):
for relConnectsPort in port.ConnectedTo:
for disPort in [relConnectsPort.RelatedPort,relConnectsPort.RelatingPort]:
if hasattr(disPort,"Nests"):
for relNest in disPort.Nests:
if relNest.RelatingObject != element:
results.append(relNest.RelatingObject)
# IFC2X3 only, deprecated in IFC4
elif hasattr(disPort,"ContainedIn"):
for relConPortToElement in disPort.ContainedIn:
if relConPortToElement.RelatedElement != element:
results.append(relConPortToElement.RelatedElement)
return(results)
def get_connected_from(element):
# Note: this code is for IFC2X3. IFC4 has a different approach.
results = []
for rel in element.HasPorts:
for rel2 in rel.RelatingPort.ConnectedFrom:
results.extend([r.RelatedElement for r in rel2.RelatingPort.ContainedIn if r.RelatedElement != element])
return results
for port in ifcopenshell.util.system.get_ports(element):
for relConnectsPort in port.ConnectedFrom:
for disPort in [relConnectsPort.RelatedPort,relConnectsPort.RelatingPort]:
if hasattr(disPort,"Nests"):
for relNest in disPort.Nests:
if relNest.RelatingObject != element:
results.append(relNest.RelatingObject)
# IFC2X3 only, deprecated in IFC4
elif hasattr(disPort,"ContainedIn"):
for relConPortToElement in disPort.ContainedIn:
if relConPortToElement.RelatedElement != element:
results.append(relConPortToElement.RelatedElement)
return(results)

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