diff --git a/src/blenderbim/blenderbim/bim/module/drawing/data.py b/src/blenderbim/blenderbim/bim/module/drawing/data.py index b733ceefbc..e6b2b856e2 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/data.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/data.py @@ -23,7 +23,7 @@ import ifcopenshell.util.element import ifcopenshell.util.representation import blenderbim.tool as tool from pathlib import Path -from typing import Any +from typing import Any, Union def refresh(): @@ -235,9 +235,9 @@ class DecoratorData: cut_cache = {} layerset_cache = {} - # used by Ifc Annotations with ObjectType = "BATTING" @classmethod def get_batting_thickness(cls, obj): + """used by IfcAnnotations with ObjectType = "BATTING"""" result = cls.data.get(obj.name, None) if result is not None: return result @@ -249,9 +249,9 @@ class DecoratorData: cls.data[obj.name] = thickness return thickness - # used by Ifc Annotations with ObjectType = "SECTION" @classmethod def get_section_markers_display_data(cls, obj): + """used by IfcAnnotations with ObjectType = "SECTION"""" result = cls.data.get(obj.name, None) if result is not None: return result @@ -291,18 +291,16 @@ class DecoratorData: cls.data[obj.name] = display_data return display_data - # used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER" @classmethod def get_ifc_text_data(cls, obj: bpy.types.Object) -> dict[str, Any]: - """returns font size in mm for current ifc text object""" + """used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER"\n + returns font size in mm for current ifc text object""" result = cls.data.get(obj.name, None) if result is not None: return result element = tool.Ifc.get_entity(obj) - if not element or not tool.Drawing.is_annotation_object_type( - element, ["TEXT", "TEXT_LEADER", "SYMBOL"] - ): + if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]): return None props = obj.BIMTextProperties @@ -343,6 +341,11 @@ class DecoratorData: cls.data[obj.name] = text_data return text_data + @classmethod + def get_symbol(cls, obj: bpy.types.Object) -> Union[str, None]: + """used by IfcAnnotations with ObjectType MULTI_SYMBOL""" + return tool.Drawing.get_annotation_symbol(tool.Ifc.get_entity(obj)) + @classmethod def get_dimension_data(cls, obj): """used by Ifc Annotations with ObjectType: diff --git a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py index 94dae638a4..4b271e6d3d 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py @@ -70,7 +70,7 @@ class profile_consequential: cls.lines = [] -def worldspace_to_winspace(verts, context): +def worldspace_to_winspace(verts: list[Vector], context: bpy.types.Context) -> list[Vector]: """Convert world space verts to window space""" region = context.region region3d = context.region_data @@ -81,7 +81,7 @@ def worldspace_to_winspace(verts, context): return winspace_verts -def winspace_to_worldspace(verts, context): +def winspace_to_worldspace(verts: list[Vector], context: bpy.types.Context) -> list[Vector]: """Convert winspace verts to world space""" region = context.region region3d = context.region_data @@ -266,7 +266,7 @@ class BaseDecorator: vertices = [obj.matrix_world @ v.co for v in bm.verts] return vertices, indices - def decorate(self, context, obj): + def decorate(self, context: bpy.types.Context, obj: bpy.types.Object): """perform actual drawing stuff""" raise NotImplementedError() @@ -312,13 +312,28 @@ class BaseDecorator: self.draw_lines(context, obj, output_verts, output_edges) - def draw_batch(self, shader_type, content_pos, color, indices=None): + def draw_batch( + self, + shader_type: str, + content_pos: list[Vector], + color: tuple[float, float, float, float], + indices: Optional[list[tuple[int, int]]] = None, + ) -> None: shader = self.line_shader if shader_type == "LINES" else self.base_shader batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices) shader.uniform_float("color", color) batch.draw(shader) - def draw_lines(self, context, obj, vertices, indices, color=None, line_width=1.0): + def draw_lines( + self, + context: bpy.types.Context, + obj: Optional[bpy.types.Object], + vertices: list[Vector], + indices: list[tuple[int, int]], + color: Optional[tuple[float, float, float, float]] = None, + line_width: float = 1.0, + ) -> None: + # TODO: `obj` is actually never used, need to remove it """`verts` should be in winspace with `(0,0,0)` in the screen left bottom corner, not in the center""" region = context.region if not color: @@ -331,7 +346,7 @@ class BaseDecorator: gpu.state.blend_set("ALPHA") self.draw_batch("LINES", vertices, color, indices) - def get_viewport_drawing_scale(self, context): + def get_viewport_drawing_scale(self, context: bpy.types.Context) -> float: # Horrific prototype code factor = self.camera_zoom_to_factor(context.space_data.region_3d.view_camera_zoom) camera_width_px = factor * context.region.width @@ -487,12 +502,11 @@ class BaseDecorator: decimal_places = drawing_pset_data.get("DecimalPlaces", None) return format_distance(value, precision=precision, decimal_places=decimal_places) - def draw_asterisk( - self, context: bpy.types.Context, obj: bpy.types.Object, rotation: float = 0.0, scale: float = 1.0 - ) -> None: - """ "`rotation` value in radians""" + def draw_asterisk(self, context: bpy.types.Context, pos: Vector, rotation: float = 0.0, scale: float = 1.0) -> None: + """`pos` is a world space position\n + `rotation` value in radians""" # gather geometry data and convert to winspace - verts = [obj.location] + verts = [pos] viewportDrawingScale = self.get_viewport_drawing_scale(context) winspace_verts = worldspace_to_winspace(verts, context) @@ -511,7 +525,51 @@ class BaseDecorator: start_i = 0 for segment in asterisk_head: start_i = add_verts_sequence(add_offsets(v0, segment), start_i, **out_kwargs) - self.draw_lines(context, obj, output_verts, output_edges) + self.draw_lines(context, None, output_verts, output_edges) + + def get_annotation_direction(self, context: bpy.types.Context, obj: bpy.types.Object) -> Vector: + camera = context.scene.camera + + def get_basis_vector(matrix, i=0): + """returns basis vector for i in world space, unaffected by object scale""" + return matrix.inverted()[i].to_3d().normalized() + + text_dir_world_x_axis = get_basis_vector(obj.matrix_world) + camera_matrix = tool.Drawing.get_camera_matrix(camera) + text_dir = (camera_matrix.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized() + return text_dir + + def draw_symbol(self, context: bpy.types.Context, obj: bpy.types.Object, annotation_dir: Vector) -> None: + if obj.type not in ("MESH", "EMPTY"): + return + + if obj.type == "MESH": + mesh: bpy.types.Mesh = obj.data + + if len(mesh.vertices) == 0: + return + + if mesh.edges: + MiscDecorator.decorate(self, context, obj) + return + + symbol = DecoratorData.get_symbol(obj) + if not symbol: + return + + for vert in mesh.vertices: + self.draw_asterisk(context, obj.matrix_world @ vert.co) + + return + + # EMPTY objects + symbol = DecoratorData.get_symbol(obj) + if not symbol: + return + + rotation = -Vector((1, 0)).angle_signed(annotation_dir) + # NOTE: for now we assume that scale is uniform + self.draw_asterisk(context, obj.location, rotation, obj.scale.x) def draw_text( self, @@ -527,17 +585,7 @@ class BaseDecorator: region = context.region region3d = context.region_data - camera = context.scene.camera - - def get_basis_vector(matrix, i=0): - """returns basis vector for i in world space, unaffected by object scale""" - return matrix.inverted()[i].to_3d().normalized() - - text_dir_world_x_axis = get_basis_vector(obj.matrix_world) - - camera_matrix = tool.Drawing.get_camera_matrix(camera) - - text_dir = (camera_matrix.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized() + text_dir = self.get_annotation_direction(context, obj) pos = location_3d_to_region_2d(region, region3d, text_world_position) props = obj.BIMTextProperties @@ -550,11 +598,8 @@ class BaseDecorator: # draw asterisk symbol to indicate that there is some symbol that's not shown in viewport if symbol: - rotation = -Vector((1, 0)).angle_signed(text_dir) - # NOTE: for now we assume that scale is uniform - uniform_scale = obj.scale.x - self.draw_asterisk(context, obj, rotation, uniform_scale) - text_scale = uniform_scale + self.draw_symbol(context, obj, text_dir) + text_scale = obj.scale.x line_i = 0 font_size_mm = text_data["FontSize"] * text_scale @@ -1017,7 +1062,7 @@ class StairDecorator(BaseDecorator): class MiscDecorator(BaseDecorator): objecttype = "MISC" - def decorate(self, context, obj): + def decorate(self, context: bpy.types.Context, obj: bpy.types.Object): if obj.data.is_editmode: verts, idxs = self.get_editmesh_geom(obj) else: @@ -1519,6 +1564,14 @@ class TextDecorator(BaseDecorator): self.draw_text(context, obj) +class SymbolDecorator(BaseDecorator): + objecttype = "SYMBOL" + + def decorate(self, context: bpy.types.Context, obj: bpy.types.Object) -> None: + annotation_dir = self.get_annotation_direction(context, obj) + self.draw_symbol(context, obj, annotation_dir) + + class CutDecorator: installed = None cache = {} @@ -1932,6 +1985,7 @@ class DecorationsHandler: SectionDecorator, ElevationDecorator, TextDecorator, + SymbolDecorator, BattingDecorator, FallDecorator, ] @@ -1959,7 +2013,7 @@ class DecorationsHandler: self.decorators = {cls.objecttype: cls() for cls in self.decorators_classes} for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"): self.decorators[object_type] = self.decorators["FALL"] - self.decorators["SYMBOL"] = self.decorators["TEXT"] + self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"] def get_objects_and_decorators(self, collection): # TODO: do it in data instead of the handler for performance?