viewport decorations for IfcAnnotation/MULTI_SYMBOL

This commit is contained in:
Andrej730
2024-03-14 15:48:12 +05:00
parent b72b23741e
commit 8291a0ca6a
2 changed files with 95 additions and 38 deletions
@@ -23,7 +23,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.representation import ifcopenshell.util.representation
import blenderbim.tool as tool import blenderbim.tool as tool
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any, Union
def refresh(): def refresh():
@@ -235,9 +235,9 @@ class DecoratorData:
cut_cache = {} cut_cache = {}
layerset_cache = {} layerset_cache = {}
# used by Ifc Annotations with ObjectType = "BATTING"
@classmethod @classmethod
def get_batting_thickness(cls, obj): def get_batting_thickness(cls, obj):
"""used by IfcAnnotations with ObjectType = "BATTING""""
result = cls.data.get(obj.name, None) result = cls.data.get(obj.name, None)
if result is not None: if result is not None:
return result return result
@@ -249,9 +249,9 @@ class DecoratorData:
cls.data[obj.name] = thickness cls.data[obj.name] = thickness
return thickness return thickness
# used by Ifc Annotations with ObjectType = "SECTION"
@classmethod @classmethod
def get_section_markers_display_data(cls, obj): def get_section_markers_display_data(cls, obj):
"""used by IfcAnnotations with ObjectType = "SECTION""""
result = cls.data.get(obj.name, None) result = cls.data.get(obj.name, None)
if result is not None: if result is not None:
return result return result
@@ -291,18 +291,16 @@ class DecoratorData:
cls.data[obj.name] = display_data cls.data[obj.name] = display_data
return display_data return display_data
# used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER"
@classmethod @classmethod
def get_ifc_text_data(cls, obj: bpy.types.Object) -> dict[str, Any]: 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) result = cls.data.get(obj.name, None)
if result is not None: if result is not None:
return result return result
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
if not element or not tool.Drawing.is_annotation_object_type( if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
element, ["TEXT", "TEXT_LEADER", "SYMBOL"]
):
return None return None
props = obj.BIMTextProperties props = obj.BIMTextProperties
@@ -343,6 +341,11 @@ class DecoratorData:
cls.data[obj.name] = text_data cls.data[obj.name] = text_data
return 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 @classmethod
def get_dimension_data(cls, obj): def get_dimension_data(cls, obj):
"""used by Ifc Annotations with ObjectType: """used by Ifc Annotations with ObjectType:
@@ -70,7 +70,7 @@ class profile_consequential:
cls.lines = [] 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""" """Convert world space verts to window space"""
region = context.region region = context.region
region3d = context.region_data region3d = context.region_data
@@ -81,7 +81,7 @@ def worldspace_to_winspace(verts, context):
return winspace_verts 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""" """Convert winspace verts to world space"""
region = context.region region = context.region
region3d = context.region_data region3d = context.region_data
@@ -266,7 +266,7 @@ class BaseDecorator:
vertices = [obj.matrix_world @ v.co for v in bm.verts] vertices = [obj.matrix_world @ v.co for v in bm.verts]
return vertices, indices return vertices, indices
def decorate(self, context, obj): def decorate(self, context: bpy.types.Context, obj: bpy.types.Object):
"""perform actual drawing stuff""" """perform actual drawing stuff"""
raise NotImplementedError() raise NotImplementedError()
@@ -312,13 +312,28 @@ class BaseDecorator:
self.draw_lines(context, obj, output_verts, output_edges) 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 shader = self.line_shader if shader_type == "LINES" else self.base_shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices) batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color) shader.uniform_float("color", color)
batch.draw(shader) 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""" """`verts` should be in winspace with `(0,0,0)` in the screen left bottom corner, not in the center"""
region = context.region region = context.region
if not color: if not color:
@@ -331,7 +346,7 @@ class BaseDecorator:
gpu.state.blend_set("ALPHA") gpu.state.blend_set("ALPHA")
self.draw_batch("LINES", vertices, color, indices) 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 # Horrific prototype code
factor = self.camera_zoom_to_factor(context.space_data.region_3d.view_camera_zoom) factor = self.camera_zoom_to_factor(context.space_data.region_3d.view_camera_zoom)
camera_width_px = factor * context.region.width camera_width_px = factor * context.region.width
@@ -487,12 +502,11 @@ class BaseDecorator:
decimal_places = drawing_pset_data.get("DecimalPlaces", None) decimal_places = drawing_pset_data.get("DecimalPlaces", None)
return format_distance(value, precision=precision, decimal_places=decimal_places) return format_distance(value, precision=precision, decimal_places=decimal_places)
def draw_asterisk( def draw_asterisk(self, context: bpy.types.Context, pos: Vector, rotation: float = 0.0, scale: float = 1.0) -> None:
self, context: bpy.types.Context, obj: bpy.types.Object, rotation: float = 0.0, scale: float = 1.0 """`pos` is a world space position\n
) -> None: `rotation` value in radians"""
""" "`rotation` value in radians"""
# gather geometry data and convert to winspace # gather geometry data and convert to winspace
verts = [obj.location] verts = [pos]
viewportDrawingScale = self.get_viewport_drawing_scale(context) viewportDrawingScale = self.get_viewport_drawing_scale(context)
winspace_verts = worldspace_to_winspace(verts, context) winspace_verts = worldspace_to_winspace(verts, context)
@@ -511,7 +525,51 @@ class BaseDecorator:
start_i = 0 start_i = 0
for segment in asterisk_head: for segment in asterisk_head:
start_i = add_verts_sequence(add_offsets(v0, segment), start_i, **out_kwargs) 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( def draw_text(
self, self,
@@ -527,17 +585,7 @@ class BaseDecorator:
region = context.region region = context.region
region3d = context.region_data region3d = context.region_data
camera = context.scene.camera text_dir = self.get_annotation_direction(context, obj)
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()
pos = location_3d_to_region_2d(region, region3d, text_world_position) pos = location_3d_to_region_2d(region, region3d, text_world_position)
props = obj.BIMTextProperties 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 # draw asterisk symbol to indicate that there is some symbol that's not shown in viewport
if symbol: if symbol:
rotation = -Vector((1, 0)).angle_signed(text_dir) self.draw_symbol(context, obj, text_dir)
# NOTE: for now we assume that scale is uniform text_scale = obj.scale.x
uniform_scale = obj.scale.x
self.draw_asterisk(context, obj, rotation, uniform_scale)
text_scale = uniform_scale
line_i = 0 line_i = 0
font_size_mm = text_data["FontSize"] * text_scale font_size_mm = text_data["FontSize"] * text_scale
@@ -1017,7 +1062,7 @@ class StairDecorator(BaseDecorator):
class MiscDecorator(BaseDecorator): class MiscDecorator(BaseDecorator):
objecttype = "MISC" objecttype = "MISC"
def decorate(self, context, obj): def decorate(self, context: bpy.types.Context, obj: bpy.types.Object):
if obj.data.is_editmode: if obj.data.is_editmode:
verts, idxs = self.get_editmesh_geom(obj) verts, idxs = self.get_editmesh_geom(obj)
else: else:
@@ -1519,6 +1564,14 @@ class TextDecorator(BaseDecorator):
self.draw_text(context, obj) 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: class CutDecorator:
installed = None installed = None
cache = {} cache = {}
@@ -1932,6 +1985,7 @@ class DecorationsHandler:
SectionDecorator, SectionDecorator,
ElevationDecorator, ElevationDecorator,
TextDecorator, TextDecorator,
SymbolDecorator,
BattingDecorator, BattingDecorator,
FallDecorator, FallDecorator,
] ]
@@ -1959,7 +2013,7 @@ class DecorationsHandler:
self.decorators = {cls.objecttype: cls() for cls in self.decorators_classes} self.decorators = {cls.objecttype: cls() for cls in self.decorators_classes}
for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"): for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
self.decorators[object_type] = self.decorators["FALL"] 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): def get_objects_and_decorators(self, collection):
# TODO: do it in data instead of the handler for performance? # TODO: do it in data instead of the handler for performance?