diff --git a/src/blenderbim/blenderbim/bim/data/pset/Psets_BBIM_Annotation.ifc b/src/blenderbim/blenderbim/bim/data/pset/Psets_BBIM_Annotation.ifc index a466449dcc..1478bcfba2 100644 --- a/src/blenderbim/blenderbim/bim/data/pset/Psets_BBIM_Annotation.ifc +++ b/src/blenderbim/blenderbim/bim/data/pset/Psets_BBIM_Annotation.ifc @@ -5,14 +5,14 @@ FILE_NAME('Psets_BBIM_Annotation.ifc','2020-01-01T00:00:00',(),(),'Psets_BBIM_An FILE_SCHEMA(('IFC4')); ENDSEC; DATA; -#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4)); +#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#2,#3,#4)); #2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.); #3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); #4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','Classes separarated by spaces that end up in classes for this element in svg. Can be used to specify the text font size: small - 1.8mm; regular - 2.5mm; large - 3.5mm; header - 5mm; title - 7mm. By default regular size is used.',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); -#5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#6,#7)); +#5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#6,#7)); #6=IFCSIMPLEPROPERTYTEMPLATE('0t2LEesGT1QRQtrIZUAR8L',$,'Thickness','Batting thickness',.P_SINGLEVALUE.,'IfcPositiveLengthMeasure',$,$,$,$,$,.READWRITE.); #7=IFCSIMPLEPROPERTYTEMPLATE('082PndS6v2kBOiJoSboMnh',$,'Reverse pattern direction','Reverse batting pattern (swap starting and ending points)',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); -#8=IFCPROPERTYSETTEMPLATE('1Dx2EiZnP67xotInXpwz90',$,'BBIM_Section','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#9,#10,#11,#12,#13)); +#8=IFCPROPERTYSETTEMPLATE('1Dx2EiZnP67xotInXpwz90',$,'BBIM_Section','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#9,#10,#11,#12,#13)); #9=IFCSIMPLEPROPERTYTEMPLATE('2a_9s8spHDc9dZHtgg71XL',$,'ShowStartArrow','Display start arrow.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); #10=IFCSIMPLEPROPERTYTEMPLATE('3i6SH_GbT7zhKea$wVE56A',$,'StartArrowSymbol','Custom symbol for the start of the section marker arrow. Need to make sure it''s present in "symbols.svg".',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); #11=IFCSIMPLEPROPERTYTEMPLATE('1DtsPn5a9FG8$zXHDDMavY',$,'ShowEndArrow','Display end arrow.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); diff --git a/src/blenderbim/blenderbim/bim/module/drawing/__init__.py b/src/blenderbim/blenderbim/bim/module/drawing/__init__.py index 49e7f38939..d828f6dea5 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/__init__.py @@ -17,7 +17,7 @@ # along with BlenderBIM Add-on. If not, see . import bpy -from . import ui, prop, operator, handler, gizmos +from . import ui, prop, operator, handler, gizmos, workspace classes = ( operator.ActivateDrawing, @@ -80,6 +80,7 @@ classes = ( prop.Literal, prop.BIMTextProperties, prop.BIMAssignedProductProperties, + prop.BIMAnnotationProperties, ui.BIM_PT_camera, ui.BIM_PT_drawing_underlay, ui.BIM_PT_annotation_utilities, @@ -95,11 +96,15 @@ classes = ( gizmos.DimensionLabelGizmo, gizmos.ExtrusionGuidesGizmo, gizmos.ExtrusionWidget, + workspace.Hotkey, ) def register(): + if not bpy.app.background: + bpy.utils.register_tool(workspace.AnnotationTool, after={"bim.bim_tool"}, separator=True, group=True) bpy.types.Scene.DocProperties = bpy.props.PointerProperty(type=prop.DocProperties) + bpy.types.Scene.BIMAnnotationProperties = bpy.props.PointerProperty(type=prop.BIMAnnotationProperties) bpy.types.Camera.BIMCameraProperties = bpy.props.PointerProperty(type=prop.BIMCameraProperties) bpy.types.Object.BIMAssignedProductProperties = bpy.props.PointerProperty(type=prop.BIMAssignedProductProperties) bpy.types.Object.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties) @@ -109,7 +114,10 @@ def register(): def unregister(): + if not bpy.app.background: + bpy.utils.unregister_tool(workspace.AnnotationTool) del bpy.types.Scene.DocProperties + del bpy.types.Scene.BIMAnnotationProperties del bpy.types.Camera.BIMCameraProperties del bpy.types.Object.BIMAssignedProductProperties del bpy.types.Object.BIMTextProperties diff --git a/src/blenderbim/blenderbim/bim/module/drawing/annotation.py b/src/blenderbim/blenderbim/bim/module/drawing/annotation.py index 2eaf8d353b..062cb4ec04 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/annotation.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/annotation.py @@ -77,17 +77,20 @@ class Annotator: co1, co2, _, _ = Annotator.get_placeholder_coords() co1 = obj.matrix_world.inverted() @ co1 co2 = obj.matrix_world.inverted() @ co2 + if isinstance(obj.data, bpy.types.Mesh): obj.data.vertices.add(2) obj.data.vertices[-2].co = co1 obj.data.vertices[-1].co = co2 obj.data.edges.add(1) obj.data.edges[-1].vertices = (obj.data.vertices[-2].index, obj.data.vertices[-1].index) + if isinstance(obj.data, bpy.types.Curve): polyline = obj.data.splines.new("POLY") polyline.points.add(1) polyline.points[-2].co = list(co1) + [1] polyline.points[-1].co = list(co2) + [1] + return obj @staticmethod @@ -114,11 +117,13 @@ class Annotator: matrix_world[1][3] = co1.y matrix_world[2][3] = co1.z collection = camera.users_collection[0] + if object_type == "TEXT": obj = bpy.data.objects.new(object_type, None) obj.matrix_world = matrix_world collection.objects.link(obj) return obj + elif object_type in ("TEXT_LEADER", "SECTION_LEVEL"): data = bpy.data.curves.new(object_type, type="CURVE") data.dimensions = "3D" @@ -127,17 +132,20 @@ class Annotator: obj.matrix_world = matrix_world collection.objects.link(obj) return obj + if object_type != "ANGLE": for obj in collection.objects: element = tool.Ifc.get_entity(obj) if element and element.ObjectType == object_type and obj.type == object_type.upper(): return obj + if data_type == "mesh": data = bpy.data.meshes.new(object_type) elif data_type == "curve": data = bpy.data.curves.new(object_type, type="CURVE") data.dimensions = "3D" data.resolution_u = 2 + obj = bpy.data.objects.new(object_type, data) obj.matrix_world = matrix_world collection.objects.link(obj) @@ -148,14 +156,13 @@ class Annotator: if not camera: camera = bpy.context.scene.camera z_offset = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1)) - if bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y: - y = ( - camera.data.ortho_scale - * (bpy.context.scene.render.resolution_y / bpy.context.scene.render.resolution_x) - / 4 - ) - else: - y = camera.data.ortho_scale / 4 + y = camera.data.ortho_scale / 4 + + res_x = bpy.context.scene.render.resolution_x + res_y = bpy.context.scene.render.resolution_y + if res_x > res_y: + y *= res_y / res_x + y_offset = camera.matrix_world.to_quaternion() @ Vector((0, y, 0)) x_offset = camera.matrix_world.to_quaternion() @ Vector((y / 2, 0, 0)) return ( diff --git a/src/blenderbim/blenderbim/bim/module/drawing/data.py b/src/blenderbim/blenderbim/bim/module/drawing/data.py index d9b70cf9a4..2911449977 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/data.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/data.py @@ -31,6 +31,7 @@ def refresh(): DrawingsData.is_loaded = False DecoratorData.data = {} DecoratorData.cut_cache = {} + AnnotationData.is_loaded = False class ProductAssignmentsData: @@ -220,7 +221,7 @@ class DecoratorData: return result element = tool.Ifc.get_entity(obj) - if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in ["TEXT", "TEXT_LEADER"]: + if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]): return None props = obj.BIMTextProperties @@ -276,3 +277,28 @@ class DecoratorData: cls.data[obj.name] = dimension_style return dimension_style + + +class AnnotationData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.is_loaded = True + cls.props = bpy.context.scene.BIMAnnotationProperties + cls.data["relating_types"] = cls.get_relating_types() + + @classmethod + def get_relating_types(cls): + object_type = cls.props.object_type + relating_types = [] + for relating_type in tool.Ifc.get().by_type("IfcTypeProduct"): + if tool.Drawing.is_annotation_object_type(relating_type, object_type): + relating_types.append(relating_type) + + enum_items = [(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in relating_types] + + # item to create anootations without relating types + enum_items.insert(0, ("0", "-", "")) + return enum_items diff --git a/src/blenderbim/blenderbim/bim/module/drawing/ops.authoring.annotation.dat b/src/blenderbim/blenderbim/bim/module/drawing/ops.authoring.annotation.dat new file mode 100644 index 0000000000..32a87c24e3 Binary files /dev/null and b/src/blenderbim/blenderbim/bim/module/drawing/ops.authoring.annotation.dat differ diff --git a/src/blenderbim/blenderbim/bim/module/drawing/prop.py b/src/blenderbim/blenderbim/bim/module/drawing/prop.py index 2a04df4342..f34c7982c8 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/prop.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/prop.py @@ -25,7 +25,7 @@ import blenderbim.tool as tool import blenderbim.core.drawing as core import blenderbim.bim.module.drawing.annotation as annotation import blenderbim.bim.module.drawing.decoration as decoration -from blenderbim.bim.module.drawing.data import DrawingsData, DecoratorData, SheetsData +from blenderbim.bim.module.drawing.data import DrawingsData, DecoratorData, SheetsData, AnnotationData from blenderbim.bim.module.drawing.data import refresh as refresh_drawing_data from pathlib import Path from blenderbim.bim.prop import Attribute, StrProperty @@ -465,3 +465,55 @@ class BIMTextProperties(PropertyGroup): class BIMAssignedProductProperties(PropertyGroup): is_editing_product: BoolProperty(name="Is Editing Product", default=False) relating_product: PointerProperty(name="Relating Product", type=bpy.types.Object) + + +# ObjectType: annotation_name, description, icon, data_type +ANNOTATION_TYPES_DATA = { + "DIMENSION": ("Dimension", "", "FIXED_SIZE", "curve"), + "ANGLE": ("Angle", "", "DRIVER_ROTATIONAL_DIFFERENCE", "curve"), + "RADIUS": ("Radius", "", "FORWARD", "curve"), + "DIAMETER": ("Diameter", "", "ARROW_LEFTRIGHT", "curve"), + "TEXT": ("Text", "", "SMALL_CAPS", "empty"), + "TEXT_LEADER": ("Leader", "", "TRACKING_BACKWARDS", "curve"), + "STAIR_ARROW": ("Stair Arrow", "Add stair arrow annotation.\nIf you have IfcStairFlight object selected, it will be used as a reference for the annotation", "SCREEN_BACK", "curve"), + "HIDDEN_LINE": ("Hidden", "", "CON_TRACKTO", "mesh"), + "PLAN_LEVEL": ("Level (Plan)", "", "SORTBYEXT", "curve"), + "SECTION_LEVEL": ("Level (Section)", "", "TRIA_DOWN", "curve"), + "BREAKLINE": ("Breakline", "", "FCURVE", "mesh"), + "LINEWORK": ("Line", "", "MESH_MONKEY", "mesh"), + "BATTING": ("Batting", "Add batting annotation.\nThickness could be changed through Thickness property of BBIM_Batting property set", "FORCE_FORCE", "mesh"), + "FILL_AREA": ("Fill Area", "", "NODE_TEXTURE", "mesh"), + "FALL": ("Fall", "", "SORT_ASC", "curve"), +} + +annotation_classes = [(x, *ANNOTATION_TYPES_DATA[x][:3], i) for i, x in enumerate(ANNOTATION_TYPES_DATA)] + + +def get_annotation_data_prop(prop_name): + def function(self, context): + if not AnnotationData.is_loaded: + AnnotationData.load() + return AnnotationData.data[prop_name] + + return function + + +def update_annotation_object_type(self, context): + self.relating_type_id = "0" + # changing enum doesn't trigger refresh by itself + AnnotationData.is_loaded = False + + +class BIMAnnotationProperties(PropertyGroup): + object_type: bpy.props.EnumProperty( + name="Annotation Object Type", items=annotation_classes, default="TEXT", update=update_annotation_object_type + ) + relating_type_id: bpy.props.EnumProperty( + name="Relating Annotation Type", items=get_annotation_data_prop("relating_types") + ) + create_representation_for_type: bpy.props.BoolProperty( + name="Create Representation For Type", + default=False, + description='Whether "Add type" should define a representation for the type \n' + "or allow occurences to have their own", + ) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/ui.py b/src/blenderbim/blenderbim/bim/module/drawing/ui.py index 64210467b0..aead40f808 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/ui.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/ui.py @@ -371,7 +371,7 @@ class BIM_PT_text(Panel): element = tool.Ifc.get_entity(context.active_object) if not element: return - return element.is_a("IfcAnnotation") and element.ObjectType in ["TEXT", "TEXT_LEADER"] + return tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]) def draw(self, context): obj = context.active_object @@ -474,7 +474,6 @@ 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" diff --git a/src/blenderbim/blenderbim/bim/module/drawing/workspace.py b/src/blenderbim/blenderbim/bim/module/drawing/workspace.py new file mode 100644 index 0000000000..b0a13b6187 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/drawing/workspace.py @@ -0,0 +1,296 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2023 @Andrej730 +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + + +import os +import bpy +import blenderbim.tool as tool +from blenderbim.bim.helper import prop_with_search +from bpy.types import WorkSpaceTool + +# from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData +from blenderbim.bim.module.drawing.prop import ANNOTATION_TYPES_DATA +from blenderbim.bim.module.drawing.data import DecoratorData, AnnotationData +from blenderbim.bim.ifc import IfcStore +import blenderbim.bim.handler + + +# declaring it here to avoid circular import problems +class Operator: + def execute(self, context): + IfcStore.execute_ifc_operator(self, context) + blenderbim.bim.handler.refresh_ui_data() + return {"FINISHED"} + + +class AnnotationTool(WorkSpaceTool): + bl_space_type = "VIEW_3D" + bl_context_mode = "OBJECT" + + bl_idname = "bim.annotation_tool" + bl_label = "Annotation Tool" + bl_description = "Gives you Annotation related superpowers" + bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.annotation") + bl_widget = None + # https://docs.blender.org/api/current/bpy.types.KeyMapItems.html + bl_keymap = tool.Blender.get_default_selection_keypmap() + ( + ("bim.annotation_hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}), + ("bim.annotation_hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}), + ("bim.annotation_hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}), + ("bim.annotation_hotkey", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}), + ("bim.annotation_hotkey", {"type": "G", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_G")]}), + ("bim.annotation_hotkey", {"type": "K", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_K")]}), + ("bim.annotation_hotkey", {"type": "M", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_M")]}), + ("bim.annotation_hotkey", {"type": "O", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_O")]}), + ("bim.annotation_hotkey", {"type": "Q", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Q")]}), + ("bim.annotation_hotkey", {"type": "R", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_R")]}), + ("bim.annotation_hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}), + ("bim.annotation_hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_V")]}), + ("bim.annotation_hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}), + ("bim.annotation_hotkey", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Y")]}), + ("bim.annotation_hotkey", {"type": "D", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_D")]}), + ("bim.annotation_hotkey", {"type": "E", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_E")]}), + ("bim.annotation_hotkey", {"type": "O", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_O")]}), + ) + + def draw_settings(context, layout, ws_tool): + # Unlike operators, Blender doesn't treat workspace tools as a class, so we'll create our own. + AnnotationToolUI.draw(context, layout) + + +def add_layout_hotkey_operator(layout, text, hotkey, description): + modifiers = { + "A": "EVENT_ALT", + "S": "EVENT_SHIFT", + } + modifier, key = hotkey.split("_") + + row = layout.row(align=True) + row.label(text="", icon=modifiers[modifier]) + row.label(text="", icon=f"EVENT_{key}") + + op = row.operator("bim.annotation_hotkey", text=text) + op.hotkey = hotkey + op.description = description + return op, row + + +# TODO: move to operator +def create_annotation_type(context): + # just empty to store parameters + props = context.scene.BIMAnnotationProperties + object_type = props.object_type + create_representation = props.create_representation_for_type + drawing = tool.Ifc.get_entity(bpy.context.scene.camera) + + if props.create_representation_for_type: + obj = tool.Drawing.create_annotation_object(drawing, object_type) + else: + obj = bpy.data.objects.new(object_type, None) + + obj.name = f"{object_type}_TYPE" + obj.location = context.scene.cursor.location + tool.Drawing.ensure_annotation_in_drawing_plane(obj) + + drawing = tool.Ifc.get_entity(context.scene.camera) + ifc_context = tool.Drawing.get_annotation_context(tool.Drawing.get_drawing_target_view(drawing)) + + element = tool.Drawing.run_root_assign_class( + obj=obj, + ifc_class="IfcTypeProduct", + predefined_type=object_type, + should_add_representation=create_representation, + context=ifc_context, + ifc_representation_class=tool.Drawing.get_ifc_representation_class(object_type), + ) + element.ApplicableOccurrence = f"IfcAnnotation/{object_type}" + + tool.Blender.select_and_activate_single_object(context, obj) + + +# TODO: move to operator +def create_annotation_occurence(context): + # object_type = context.scene.BIMAnnotationProperties.object_type + props = context.scene.BIMAnnotationProperties + relating_type = tool.Ifc.get().by_id(int(props.relating_type_id)) + object_type = props.object_type + + drawing = tool.Ifc.get_entity(context.scene.camera) + obj = tool.Drawing.create_annotation_object(drawing, object_type) + obj.name = relating_type.Name + ifc_context = tool.Drawing.get_annotation_context(tool.Drawing.get_drawing_target_view(drawing)) + relating_type_repr = tool.Drawing.get_annotation_representation(relating_type) + element = tool.Drawing.run_root_assign_class( + obj=obj, + ifc_class="IfcAnnotation", + predefined_type=object_type, + should_add_representation=not bool(relating_type_repr), + context=ifc_context, + ifc_representation_class=tool.Drawing.get_ifc_representation_class(object_type), + ) + + blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type) + + tool.Ifc.run("group.assign_group", group=tool.Drawing.get_drawing_group(drawing), products=[element]) + tool.Collector.assign(obj) + tool.Blender.select_and_activate_single_object(context, obj) + + +def create_annotation(): + props = bpy.context.scene.BIMAnnotationProperties + create_type_occurence = props.relating_type_id != "0" + if create_type_occurence: + create_annotation_occurence(bpy.context) + else: + object_type = props.object_type + bpy.ops.bim.add_annotation(object_type=object_type, data_type=ANNOTATION_TYPES_DATA[object_type][-1]) + + +class AnnotationToolUI: + @classmethod + def draw(cls, context, layout): + cls.layout = layout + cls.props = context.scene.BIMAnnotationProperties + + row = cls.layout.row(align=True) + if not tool.Ifc.get(): + row.label(text="No IFC Project", icon="ERROR") + return + + if not AnnotationData.is_loaded: + AnnotationData.load() + + cls.draw_type_selection_interface() + + if context.active_object and context.selected_objects: + cls.draw_edit_object_interface(context) + elif not context.selected_objects: + cls.draw_create_object_interface() + + @classmethod + def draw_create_object_interface(cls): + row = cls.layout.row(align=True) + op, row = add_layout_hotkey_operator(cls.layout, "Add Type", "S_C", "Create a new annotation type") + selected_icon = "CHECKBOX_HLT" if cls.props.create_representation_for_type else "CHECKBOX_DEHLT" + row.prop(cls.props, "create_representation_for_type", text="", icon=selected_icon) + + @classmethod + def draw_edit_object_interface(cls, context): + if DecoratorData.get_ifc_text_data(bpy.context.object): + add_layout_hotkey_operator(cls.layout, "Edit Text", "S_E", "") + + @classmethod + def draw_header_interface(cls): + cls.draw_type_selection_interface() + + @classmethod + def draw_basic_annotation_tool_interface(cls): + cls.draw_type_selection_interface() + + @classmethod + def draw_type_selection_interface(cls): + # shared by both sidebar and header + object_type = cls.props.object_type + + row = cls.layout.row(align=True) + row.label(text="", icon="FILE_VOLUME") + prop_with_search(row, cls.props, "object_type", text="") + + row = cls.layout.row(align=True) + row.label(text="", icon="FILE_3D") + prop_with_search(row, cls.props, "relating_type_id", text="") + + create_type_occurence = cls.props.relating_type_id != "0" + label = "Add Type Occurence" if create_type_occurence else "Add Annotation" + add_layout_hotkey_operator(cls.layout, label, "S_A", "Create a new annotation") + + if object_type in ("TEXT", "STAIR_ARROW"): + add_layout_hotkey_operator( + cls.layout, + "Bulk Tag", + "S_T", + "Create new annotations and automatically adjust them to the selected objects", + ) + add_layout_hotkey_operator( + cls.layout, "Readjust", "S_G", "Readjust tags based on the products they are assigned to" + ) + + +class Hotkey(bpy.types.Operator, Operator): + bl_idname = "bim.annotation_hotkey" + bl_label = "Hotkey" + bl_options = {"REGISTER", "UNDO"} + hotkey: bpy.props.StringProperty() + description: bpy.props.StringProperty() + + @classmethod + def poll(cls, context): + return tool.Ifc.get() + + @classmethod + def description(cls, context, operator): + return operator.description or "" + + def _execute(self, context): + self.props = context.scene.BIMAnnotationProperties + getattr(self, f"hotkey_{self.hotkey}")() + + def invoke(self, context, event): + # https://blender.stackexchange.com/questions/276035/how-do-i-make-operators-remember-their-property-values-when-called-from-a-hotkey + self.props = context.scene.BIMAnnotationProperties + return self.execute(context) + + def draw(self, context): + pass + + def hotkey_S_T(self): + props = bpy.context.scene.BIMAnnotationProperties + object_type = props.object_type + + related_objects = bpy.context.selected_objects + for related_object in related_objects: + create_annotation() + obj = bpy.context.active_object + bpy.ops.object.mode_set(mode="OBJECT") + tool.Drawing.setup_annotation_object(obj, object_type, related_object) + + def hotkey_S_A(self): + create_annotation() + + def hotkey_S_E(self): + if not bpy.context.object: + return + + if DecoratorData.get_ifc_text_data(bpy.context.object): + bpy.ops.bim.edit_text_popup() + + def hotkey_S_G(self): + for obj in bpy.context.selected_objects: + element = tool.Ifc.get_entity(obj) + if not element or not element.is_a("IfcAnnotation"): + continue + + related_product = tool.Drawing.get_assigned_product(element) + if not related_product: + continue + related_object = tool.Ifc.get_object(related_product) + + tool.Drawing.setup_annotation_object(obj, element.ObjectType, related_object) + + def hotkey_S_C(self): + create_annotation_type(bpy.context) diff --git a/src/blenderbim/blenderbim/bim/module/type/operator.py b/src/blenderbim/blenderbim/bim/module/type/operator.py index 094fd5f033..dc41a89912 100644 --- a/src/blenderbim/blenderbim/bim/module/type/operator.py +++ b/src/blenderbim/blenderbim/bim/module/type/operator.py @@ -68,9 +68,7 @@ class UnassignType(bpy.types.Operator): return attribute.is_a("IfcProfileDef") and attribute.ProfileName self.file = IfcStore.get_file() - objs = ( - [bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects - ) + objs = [bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects for obj in objs: element = tool.Ifc.get_entity(obj) if not element or element.is_a("IfcElementType"): @@ -90,7 +88,10 @@ class UnassignType(bpy.types.Operator): else: # We must unmap representations. copied_representation = ifcopenshell.util.element.copy_deep( - tool.Ifc.get(), resolved_representation, exclude=["IfcGeometricRepresentationContext"], exclude_callback=exclude_callback + tool.Ifc.get(), + resolved_representation, + exclude=["IfcGeometricRepresentationContext"], + exclude_callback=exclude_callback, ) representations.append(copied_representation) if representation.ContextOfItems == active_context: @@ -227,6 +228,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator): context=body, ifc_representation_class=None, ) + elif template in ("LAYERSET_AXIS2", "LAYERSET_AXIS3"): unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) obj = bpy.data.objects.new(name, None) @@ -260,6 +262,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator): elif template == "LAYERSET_AXIS3": axis = "AXIS3" ifcopenshell.api.run("pset.edit_pset", ifc_file, pset=pset, properties={"LayerSetDirection": axis}) + elif template == "PROFILESET": unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) obj = bpy.data.objects.new(name, None) diff --git a/src/blenderbim/blenderbim/core/drawing.py b/src/blenderbim/blenderbim/core/drawing.py index c6854767fa..4471987541 100644 --- a/src/blenderbim/blenderbim/core/drawing.py +++ b/src/blenderbim/blenderbim/core/drawing.py @@ -321,7 +321,6 @@ 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) diff --git a/src/blenderbim/blenderbim/tool/blender.py b/src/blenderbim/blenderbim/tool/blender.py index 2cb2ccde57..57726dca98 100644 --- a/src/blenderbim/blenderbim/tool/blender.py +++ b/src/blenderbim/blenderbim/tool/blender.py @@ -19,6 +19,7 @@ import bpy import ifcopenshell.api import blenderbim.tool as tool +from mathutils import Vector class Blender: @@ -175,9 +176,17 @@ class Blender: "max_y": bound_box[6][1], "min_z": bound_box[0][2], "max_z": bound_box[6][2], + "center": (Vector(bound_box[6]) - Vector(bound_box[0])) / 2, } return bbox_dict + @classmethod + def select_and_activate_single_object(cls, context, active_object): + for obj in context.selected_objects: + obj.select_set(False) + context.view_layer.objects.active = active_object + active_object.select_set(True) + ## BMESH UTILS ## @classmethod def apply_bmesh(cls, mesh, bm, obj=None): diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 9f004b2edb..f42c445aa6 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -40,6 +40,7 @@ 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 +import collections class Drawing(blenderbim.core.tool.Drawing): @@ -80,8 +81,28 @@ class Drawing(blenderbim.core.tool.Drawing): return obj - # TODO: add a way for users to run this method to readjust the annotations - # based on non-modifier stairs + @classmethod + def ensure_annotation_in_drawing_plane(cls, obj, camera=None): + """Make sure annotation object is going to be visible in the camera view""" + + def get_camera_from_annotation_object(obj): + entity = tool.Ifc.get_entity(obj) + if not entity: + return + for rel in entity.HasAssignments: + if rel.is_a("IfcRelAssignsToGroup"): + for e in rel.RelatedObjects: + if e.ObjectType == "DRAWING": + return tool.Ifc.get_object(e) + + if not camera: + camera = get_camera_from_annotation_object(obj) or bpy.context.scene.camera + + current_pos = camera.matrix_world.inverted() @ obj.location + current_pos.z = -camera.data.clip_start + current_pos = camera.matrix_world @ current_pos + obj.location = current_pos + @classmethod def setup_annotation_object(cls, obj, object_type, related_object=None): """Finish object's adjustments after both object and entity are created""" @@ -89,27 +110,70 @@ class Drawing(blenderbim.core.tool.Drawing): if not related_object: related_object = bpy.context.active_object related_entity = tool.Ifc.get_entity(related_object) + if not related_entity: + return + + obj_entity = tool.Ifc.get_entity(obj) + assign_product = False 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 + if related_entity.is_a("IfcStairFlight"): + stair, arrow = related_object, obj # 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"])) + arrow.location = stair.matrix_world @ 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 + cls.ensure_annotation_in_drawing_plane(obj) + assign_product = True + + elif object_type == "TEXT": + bbox = tool.Blender.get_object_bounding_box(related_object) + + obj.location = related_object.matrix_world @ bbox["center"] + cls.ensure_annotation_in_drawing_plane(obj) + assign_product = True + + if assign_product and not cls.get_assigned_product(obj_entity): + tool.Ifc.run("drawing.assign_product", relating_product=related_entity, related_object=obj_entity) + + if object_type == "TEXT": + tool.Drawing.update_text_value(obj) + + @classmethod + def is_annotation_object_type(cls, element, object_types): + if not isinstance(object_types, collections.abc.Iterable): + object_types = [object_types] + + element_type = element.is_a() + + if element_type == "IfcAnnotation" and element.ObjectType in object_types: + return True + + if element_type == "IfcTypeProduct" and element.ApplicableOccurrence.startswith("IfcAnnotation/"): + applicable_object_type = element.ApplicableOccurrence.split("/")[1] + if applicable_object_type in object_types: + return True + + return False + + @classmethod + def get_annotation_representation(cls, element): + rep = ifcopenshell.util.representation.get_representation( + element, "Plan", "Annotation" + ) or ifcopenshell.util.representation.get_representation(element, "Model", "Annotation") + if not rep: + return None + + rep = tool.Geometry.resolve_mapped_representation(rep) + return rep @classmethod def create_camera(cls, name, matrix): @@ -376,13 +440,11 @@ class Drawing(blenderbim.core.tool.Drawing): element = tool.Ifc.get_entity(obj) if not element: return - rep = ifcopenshell.util.representation.get_representation( - element, "Plan", "Annotation" - ) or ifcopenshell.util.representation.get_representation(element, "Model", "Annotation") + rep = cls.get_annotation_representation(element) if not rep: - return - items = [i for i in rep.Items if i.is_a("IfcTextLiteral")] + return [] if return_list else None + items = [i for i in rep.Items if i.is_a("IfcTextLiteral")] if not items: return [] if return_list else None if return_list: @@ -395,9 +457,7 @@ class Drawing(blenderbim.core.tool.Drawing): if not element: return - rep = ifcopenshell.util.representation.get_representation( - element, "Plan", "Annotation" - ) or ifcopenshell.util.representation.get_representation(element, "Model", "Annotation") + rep = cls.get_annotation_representation(element) if not rep: return @@ -435,9 +495,7 @@ class Drawing(blenderbim.core.tool.Drawing): if not element: return - rep = ifcopenshell.util.representation.get_representation( - element, "Plan", "Annotation" - ) or ifcopenshell.util.representation.get_representation(element, "Model", "Annotation") + rep = cls.get_annotation_representation(element) if not rep: return diff --git a/src/blenderbim/blenderbim_icons.blend b/src/blenderbim/blenderbim_icons.blend new file mode 100644 index 0000000000..75dd8bf260 Binary files /dev/null and b/src/blenderbim/blenderbim_icons.blend differ diff --git a/src/blenderbim/icon-cad.blend b/src/blenderbim/icon-cad.blend deleted file mode 100644 index 92dc30b4a1..0000000000 Binary files a/src/blenderbim/icon-cad.blend and /dev/null differ diff --git a/src/blenderbim/icon.blend b/src/blenderbim/icon.blend deleted file mode 100644 index b804c2ab9b..0000000000 Binary files a/src/blenderbim/icon.blend and /dev/null differ diff --git a/src/blenderbim/test/core/test_drawing.py b/src/blenderbim/test/core/test_drawing.py index c1ac757556..1d18d840a8 100644 --- a/src/blenderbim/test/core/test_drawing.py +++ b/src/blenderbim/test/core/test_drawing.py @@ -420,7 +420,6 @@ 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")