diff --git a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py index a9609906ff..ab12fd9c8b 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py @@ -27,14 +27,13 @@ import ifcopenshell import ifcopenshell.util.element import blenderbim.tool as tool import blenderbim.bim.module.drawing.helper as helper -from math import pi, sin, cos, tan, acos, atan, degrees +from math import pi, sin, cos, tan, acos, atan, degrees, radians, ceil from bpy.types import SpaceView3D from mathutils import Vector, Matrix from bpy_extras.view3d_utils import location_3d_to_region_2d -from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat from gpu_extras.batch import batch_for_shader from blenderbim.bim.module.drawing.data import DecoratorData -from blenderbim.bim.module.drawing.shaders import BASE_LIB_GLSL, BASE_DEF_GLSL +from blenderbim.bim.module.drawing.shaders import BASE_LIB_GLSL, BASE_DEF_GLSL, add_verts_sequence, add_offsets def ccw(A, B, C): @@ -42,75 +41,104 @@ def ccw(A, B, C): return (C.y - A.y) * (B.x - A.x) > (B.y - A.y) * (C.x - A.x) +def worldspace_to_winspace(verts, context): + """Convert world space verts to window space""" + region = context.region + region3d = context.region_data + clipspace_verts = [region3d.perspective_matrix @ Vector(v) for v in verts] + winspace_vector = Vector([region.width / 2, region.height / 2, 1]) + winspace_vector_offset = Vector([region.width / 2, region.height / 2, 0]) + winspace_verts = [v * winspace_vector + winspace_vector_offset for v in clipspace_verts] + return winspace_verts + + +def winspace_to_worldspace(verts, context): + """Convert winspace verts to world space""" + region = context.region + region3d = context.region_data + # clipspace_vector = 1 / winspace_vector + clipspace_vector = Vector([1 / (region.width / 2), 1 / (region.height / 2), 1]) + winspace_vector_offset = Vector([region.width / 2, region.height / 2, 0]) + clipspace_verts = [(v - winspace_vector_offset) * clipspace_vector for v in verts] + worldspace_verts = [region3d.perspective_matrix.inverted() @ v for v in clipspace_verts] + return worldspace_verts + + +def get_arrow_head(edge_dir, size, rot_matrix_cw, rot_matrix_ccw): + head = [] + head.append(edge_dir * size) + head.append((rot_matrix_cw @ head[0].xy).to_3d()) + head.append((rot_matrix_ccw @ head[0].xy).to_3d()) + return head + + +def get_triangle_head(edge_dir, side, length, width): + head = [ + edge_dir * length * (-0.5), + side * width, + edge_dir * length * (0.5), + ] + return head + + +def get_callout_head(edge_dir, edge_side, callout_size, callout_gap): + head = [ + # callout handle + edge_dir * -callout_size + edge_side * callout_gap, + # callout triangle + edge_dir * callout_gap * 2 + edge_side * callout_gap, + edge_dir * callout_gap, + edge_side * callout_gap, + ] + return head + + +def get_circle_head(size, segments=12): + angle_d = 2 * pi / segments + head = [] + for i in range(segments): + angle = angle_d * i + head.append(Vector([cos(angle), sin(angle), 0]) * size) + return head + + +def get_circle_head_asterisk(size, segments=6): + circle_head = get_circle_head(size, segments) + middle = segments // 2 + return zip(circle_head[:middle], circle_head[middle:]) + + +def get_angle_circle(circle_start, circle_angle, counterclockwise, segments=12): + angle_d = 2 * pi / segments + angle_segs = max(1, ceil(circle_angle / angle_d)) + angle_d = circle_angle / angle_segs + head = [] + circle_start = circle_start.xy + + for i in range(angle_segs + 1): + angle = angle_d * i + if counterclockwise: + rot_matrix_ccw = Matrix.Rotation(angle, 2) + head.append((rot_matrix_ccw @ circle_start).to_3d()) + else: + rot_matrix_cw = Matrix.Rotation(-angle, 2) + head.append((rot_matrix_cw @ circle_start).to_3d()) + return head + + class BaseDecorator: # base name of objects to decorate objecttype = "NOTDEFINED" - DEF_GLSL = BASE_DEF_GLSL - LIB_GLSL = BASE_LIB_GLSL - - VERT_GLSL = """ - uniform mat4 viewMatrix; - in vec3 pos; - in uint topo; - in vec3 next_vert; - out uint type; - out vec4 v_next_vert; - - void main() { - gl_Position = viewMatrix * vec4(pos, 1.0); - type = topo; - v_next_vert = viewMatrix * vec4(next_vert, 1.0); - } - """ - - 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); //test - } - } - """ - def __init__(self): - # NB: libcode param doesn't work - self.shader = GPUShader( - vertexcode=self.VERT_GLSL, - fragcode=self.FRAG_GLSL, - geocode=self.LIB_GLSL + self.GEOM_GLSL, - defines=self.DEF_GLSL, - ) - self.font_id = blf.load( os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", "OpenGost Type B TT.ttf") ) + # 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.base_shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR") + def get_camera_width_mm(self): # Horrific prototype code to ensure bgl draws at drawing scales # https://blender.stackexchange.com/questions/16493/is-there-a-way-to-fit-the-viewport-to-the-current-field-of-view @@ -188,6 +216,20 @@ class BaseDecorator: results.append(obj) return results + def get_splines(self, obj): + """Iterates through splines + Args: + obj: Blender object with Curve data + + Yields: + verts: points of each spline, world coords + """ + for spline in obj.data.splines: + spline_points = spline.bezier_points if spline.bezier_points else spline.points + if len(spline_points) < 2: + continue + yield [obj.matrix_world @ p.co for p in spline_points] + def get_path_geom(self, obj, topo=True): """Parses path geometry into line segments @@ -224,7 +266,7 @@ class BaseDecorator: return vertices, indices, topology - def get_mesh_geom(self, obj): + def get_mesh_geom(self, obj, check_mode=True): """Parses mesh geometry into line segments Args: @@ -234,6 +276,9 @@ class BaseDecorator: vertices: 3-tuples of coords indices: 2-tuples of each segment verices' indices """ + if check_mode and obj.data.is_editmode: + return self.get_editmesh_geom(obj) + vertices = [obj.matrix_world @ v.co for v in obj.data.vertices] indices = [e.vertices for e in obj.data.edges] return vertices, indices @@ -249,69 +294,76 @@ class BaseDecorator: """perform actual drawing stuff""" raise NotImplementedError() - def draw_lines( - self, - context, - obj, - vertices, - indices, - topology=None, - is_scale_dependant=True, - fill_next_vertices=False, - extra_float_kwargs={}, - smoothing=True, - ): - """use `is_scale_dependant` = `False` if shader is not using uniform viewportDrawingScale - otherwise uniform will be discarded during the optimization process - and you will get `ValueError: GPUShader.uniform_float: uniform viewportDrawingScale not found` - """ + def draw_arrow(self, context, obj): + # gather geometry data and convert to winspace + verts, edges_original, _ = self.get_path_geom(obj, topo=False) + if not edges_original: + return + viewportDrawingScale = self.get_viewport_drawing_scale(context) + winspace_verts = worldspace_to_winspace(verts, context) + # setup geometry parameters + arrow_size = viewportDrawingScale * 16 + angle = radians(15) + rot_matrix_cw = Matrix.Rotation(-angle, 2) + rot_matrix_ccw = Matrix.Rotation(angle, 2) + + output_verts = [] + output_edges = [] + out_kwargs = { + "output_verts": output_verts, + "output_edges": output_edges, + } + last_vert = len(winspace_verts) - 1 + + # process edges + for edge in edges_original: + v0, v1 = winspace_verts[edge[0]], winspace_verts[edge[1]] + start_i = len(output_verts) + + # arrow head on last vert + if edge[1] == last_vert: + edge_dir = (v1 - v0).normalized() + arrow_head = get_arrow_head(edge_dir, arrow_size, rot_matrix_cw, rot_matrix_ccw) + add_verts_sequence([v1, v1 - arrow_head[1], v1 - arrow_head[2]], start_i, **out_kwargs, closed=True) + start_i += 3 + gap = edge_dir * arrow_size + # stem with gaps for arrows + add_verts_sequence([v0, v1 - gap], start_i, **out_kwargs) + else: + # stem with gaps for arrows + add_verts_sequence([v0, v1], start_i, **out_kwargs) + + self.draw_lines(context, obj, output_verts, output_edges) + + def draw_batch(self, shader_type, content_pos, color, indices=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): + """`verts` should be in winspace with `(0,0,0)` in the screen left bottom corner, not in the center""" region = context.region - region3d = context.region_data - color = context.preferences.addons["blenderbim"].preferences.decorations_colour - - fmt = GPUVertFormat() - fmt.attr_add(id="pos", comp_type="F32", len=3, fetch_mode="FLOAT") - if topology: - fmt.attr_add(id="topo", comp_type="U8", len=1, fetch_mode="INT") - if fill_next_vertices: - fmt.attr_add(id="next_vert", comp_type="F32", len=3, fetch_mode="FLOAT") - - vbo = GPUVertBuf(len=len(vertices), format=fmt) - vbo.attr_fill(id="pos", data=vertices) - if topology: - vbo.attr_fill(id="topo", data=topology) - - if fill_next_vertices: - shifted_vertices = vertices[1:] + [vertices[0]] - vbo.attr_fill(id="next_vert", data=shifted_vertices) - - ibo = GPUIndexBuf(type="LINES", seq=indices) - - batch = GPUBatch(type="LINES", buf=vbo, elem=ibo) - - self.shader.bind() - self.shader.uniform_float("viewMatrix", region3d.perspective_matrix) - self.shader.uniform_float("winsize", (region.width, region.height)) - self.shader.uniform_float("color", color) - if smoothing: - self.shader.uniform_float("lineWidth", 1.0) - - if is_scale_dependant: - # Horrific prototype code - factor = self.camera_zoom_to_factor(context.space_data.region_3d.view_camera_zoom) - camera_width_px = factor * context.region.width - mm_to_px = camera_width_px / self.get_camera_width_mm() - # 0.00025 is a magic constant number I visually discovered to get the right number. - # It probably should be dynamically calculated using system.dpi or something. - viewport_drawing_scale = 0.00025 * mm_to_px - self.shader.uniform_float("viewportDrawingScale", viewport_drawing_scale) - - for kwarg, value in extra_float_kwargs.items(): - self.shader.uniform_float(kwarg, value) + if not color: + color = context.preferences.addons["blenderbim"].preferences.decorations_colour + self.line_shader.bind() + # POLYLINE_UNIFORM_COLOR specific uniforms + self.line_shader.uniform_float("viewportSize", (region.width, region.height)) + self.line_shader.uniform_float("lineWidth", 1.0) gpu.state.blend_set("ALPHA") - batch.draw(self.shader) + self.draw_batch("LINES", vertices, color, indices) + + def get_viewport_drawing_scale(self, context): + # Horrific prototype code + factor = self.camera_zoom_to_factor(context.space_data.region_3d.view_camera_zoom) + camera_width_px = factor * context.region.width + mm_to_px = camera_width_px / self.get_camera_width_mm() + # 0.00025 is a magic constant number I visually discovered to get the right number. + # It probably should be dynamically calculated using system.dpi or something. + viewport_drawing_scale = 0.00025 * mm_to_px + return viewport_drawing_scale def draw_label( self, @@ -385,16 +437,16 @@ class BaseDecorator: pos -= rotation_matrix.transposed() @ box_alignment_offset else: + # horizontal centering if center: - # horizontal centering pos -= Vector((cos, sin)) * w * 0.5 + # vertical centering if vcenter: - # vertical centering pos -= Vector((-sin, cos)) * h * 0.5 + # side-shifting if gap: - # side-shifting pos += Vector((-sin, cos)) * gap blf.enable(font_id, blf.ROTATION) @@ -417,6 +469,29 @@ class BaseDecorator: split_unit=context.scene.unit_settings.system == "IMPERIAL", ) + def draw_asterisk(self, context, obj): + # gather geometry data and convert to winspace + verts = [obj.location] + viewportDrawingScale = self.get_viewport_drawing_scale(context) + winspace_verts = worldspace_to_winspace(verts, context) + + # setup geometry parameters + circle_size = viewportDrawingScale * 4 + + output_verts = [] + output_edges = [] + out_kwargs = { + "output_verts": output_verts, + "output_edges": output_edges, + } + + v0 = winspace_verts[0] + asterisk_head = get_circle_head_asterisk(circle_size) + 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) + def draw_text(self, context, obj, text_world_position=None): """if `text_world_position` is not provided, the object's location will be used""" if not text_world_position: @@ -442,9 +517,7 @@ class BaseDecorator: # draw asterisk symbol to indicate that there is some symbol that's not shown in viewport if symbol: - verts = [text_world_position] - idxs = [(0, 0)] - self.draw_lines(context, obj, verts, idxs) + self.draw_asterisk(context, obj) # NOTE: for now we assume that scale is uniform text_scale = obj.scale.x @@ -476,102 +549,70 @@ class DimensionDecorator(BaseDecorator): objecttype = "DIMENSION" - DEF_GLSL = ( - BaseDecorator.DEF_GLSL - + """ - #define OBLIQUE_SYMBOL_ANGLE PI / 4.0 - #define OLBIQUE_SYMBOL_SIZE 10.0 - #define OBLIQUE_HEAD_VERTS 7 - - #define ARROW_ANGLE PI / 12.0 - #define ARROW_SIZE 16.0 - """ - ) - - GEOM_GLSL = """ - uniform float viewportDrawingScale; - uniform float use_oblique_style; - - layout(lines) in; - layout(triangle_strip, max_vertices=MAX_POINTS) out; - - void oblique_dimension_head(in vec4 dir, in float size, - in float angle, out vec4 head[OBLIQUE_HEAD_VERTS]) { - float c = cos(angle), s = sin(angle); - vec4 ortho = vec4(-dir.y, dir.x, 0, 0); - head[0] = -dir * size*0.66; - head[1] = vec4(0); - head[2] = ortho * size; - head[3] = -ortho * size; - head[4] = vec4(0); - head[5] = vec4((mat2(c, +s, -s, c) * dir.xy) * size * 0.47, 0, 0); - head[6] = -head[5]; - } - - void main() { - vec4 clip2win = matCLIP2WIN(); - vec4 win2clip = matWIN2CLIP(); - - vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position; - - vec4 p0w = CLIP2WIN(p0), p1w = CLIP2WIN(p1); - vec4 edge = p1w - p0w, dir = normalize(edge); - - vec4 p, p_next; - vec2 EDGE_DIR; - - if (use_oblique_style > 0) { - vec4 head[OBLIQUE_HEAD_VERTS]; - oblique_dimension_head(dir, viewportDrawingScale * OLBIQUE_SYMBOL_SIZE, OBLIQUE_SYMBOL_ANGLE, head); - - // start edge arrow - p_next = WIN2CLIP( (p0w + head[0]) ); - for (int i = 0; i < OBLIQUE_HEAD_VERTS-1; i++) { - do_edge_verts_win( (p0w + head[i]), (p0w + head[i+1]) ); - } - EndPrimitive(); - - // end edge arrow - p_next = WIN2CLIP( (p1w + head[0]) ); - for (int i = 0; i < OBLIQUE_HEAD_VERTS-1; i++) { - do_edge_verts_win( (p1w + head[i]), (p1w + head[i+1]) ); - } - EndPrimitive(); - - // stem - do_edge_verts(p0, p1); - EndPrimitive(); - - } else { - vec4 head[3]; - arrow_head(dir, viewportDrawingScale * ARROW_SIZE, ARROW_ANGLE, head); - - // start edge arrow - do_edge_verts( p0, WIN2CLIP( (p0w + head[1]) ) ); - do_edge_verts_win( p0w + head[1], p0w + head[2] ); - do_edge_verts( WIN2CLIP( p0w + head[2] ), p0 ); - EndPrimitive(); - - // end edge arrow - do_edge_verts( p1, WIN2CLIP( p1w - head[1] ) ); - do_edge_verts_win( p1w - head[1], p1w - head[2] ); - do_edge_verts( WIN2CLIP( p1w - head[2] ), p1 ); - EndPrimitive(); - - // stem, with gaps for arrows - do_edge_verts_win( p0w + head[0], p1w - head[0] ); - EndPrimitive(); - } - } - """ - def decorate(self, context, obj): - verts, idxs, _ = self.get_path_geom(obj, topo=False) + # gather geometry data and convert to winspace + verts_original, edges_original, _ = self.get_path_geom(obj, topo=False) + if not edges_original: + return + winspace_verts = worldspace_to_winspace(verts_original, context) + viewportDrawingScale = self.get_viewport_drawing_scale(context) + + # setup geometry parameters dimension_style = DecoratorData.get_dimension_data(obj)["dimension_style"] - self.draw_lines( - context, obj, verts, idxs, extra_float_kwargs={"use_oblique_style": float(dimension_style == "oblique")} - ) - self.draw_labels(context, obj, verts, idxs) + if dimension_style == "oblique": + size = viewportDrawingScale * 10 # OLBIQUE_SYMBOL_SIZE + angle = radians(45) + else: + size = viewportDrawingScale * 16 # ARROW_SIZE + angle = radians(15) + rot_matrix_cw = Matrix.Rotation(-angle, 2) + rot_matrix_ccw = Matrix.Rotation(angle, 2) + + output_verts = [] + output_edges = [] + out_kwargs = { + "output_verts": output_verts, + "output_edges": output_edges, + } + + # process edges + for edge in edges_original: + v0, v1 = winspace_verts[edge[0]], winspace_verts[edge[1]] + edge_dir = (v1 - v0).normalized() + start_i = len(output_verts) + + if dimension_style == "oblique": + ortho = Vector((-edge_dir.y, edge_dir.x)).to_3d() + # oblique dimension head + head = [] + head.append(-edge_dir * size * 0.66) + head.append(Vector([0] * 3)) + head.append(ortho * size) + head.append(-head[-1]) + head.append(Vector([0] * 3)) + head.append(((rot_matrix_ccw @ edge_dir.xy) * size * 0.47).to_3d()) + head.append(-head[-1]) + + n_segments = len(head) - 1 + # start edge arrow + add_verts_sequence([v0 + v for v in head], start_i, **out_kwargs) + # end edge arrow + add_verts_sequence([v1 + v for v in head], start_i + n_segments + 1, **out_kwargs) + # stem + add_verts_sequence([v0, v1], start_i + n_segments * 2 + 2, **out_kwargs) + + else: + # arrow dimension head + head = get_arrow_head(edge_dir, size, rot_matrix_cw, rot_matrix_ccw) + # start edge arrow + add_verts_sequence([v0, v0 + head[1], v0 + head[2]], start_i, **out_kwargs, closed=True) + # end edge arrow + add_verts_sequence([v1, v1 - head[1], v1 - head[2]], start_i + 3, **out_kwargs, closed=True) + # stem with gaps for arrows + add_verts_sequence([v0 + head[0], v1 - head[0]], start_i + 6, **out_kwargs) + + self.draw_lines(context, obj, output_verts, output_edges) + self.draw_labels(context, obj, verts_original, edges_original) def draw_labels(self, context, obj, vertices, indices): region = context.region @@ -614,82 +655,58 @@ class AngleDecorator(BaseDecorator): objecttype = "ANGLE" - DEF_GLSL = ( - BaseDecorator.DEF_GLSL - + """ - #define ARROW_ANGLE PI / 12.0 - #define ARROW_SIZE 8.0 - #define CIRCLE_SIZE 6.0 - """ - ) - - GEOM_GLSL = """ - uniform float viewportDrawingScale; - - layout(lines) in; - layout(triangle_strip, max_vertices=MAX_POINTS) out; - in uint type[]; - in vec4 v_next_vert[]; - - // per edge shader - 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; - vec4 p2 = v_next_vert[1]; - uint t0 = type[0], t1 = type[1]; - - vec4 p0w = CLIP2WIN(p0), p1w = CLIP2WIN(p1); - vec4 p2w = CLIP2WIN(p2); - vec4 edge0 = p1w - p0w, dir = normalize(edge0); - vec4 p; - - // draw a segment line - do_edge_verts(p0, p1); - EndPrimitive(); - - // end edge with angle circle for the non-last segment - if (t1 == 0u) { // draws only on internal vertex - edge0 = p0w - p1w; - vec4 dir0 = normalize(edge0); - vec4 edge1 = p2w - p1w; - vec4 dir1 = normalize(edge1); - - float angle_circle_size = min( length(edge0), length(edge1) ); - vec4 circle_start = dir0 * angle_circle_size; - vec4 circle_end = dir1 * angle_circle_size; - - float cos_a = dot( edge0, edge1 ) / ( length(edge0) * length(edge1) ); - float circle_angle = acos(cos_a); - - vec4 circle_head_data[CIRCLE_SEGS+1]; - float angle_segs; - bool counterclockwise = check_counterclockwise(p2w, p1w, p0w); - angle_circle_head( - circle_start, - circle_angle, - counterclockwise, - circle_head_data, - angle_segs); - - for(int i=0; i 0) { - do_circle_head(p0w, head); - EndPrimitive(); - } - - if (display_start_symbol > 0) { - // start edge triangle - do_triangle_head(p0w, head5); - EndPrimitive(); - } - - // end edge circle - if (display_end_circle > 0) { - do_circle_head(p1w, head); - EndPrimitive(); - } - - // end edge triangle - if (display_end_symbol > 0) { - do_triangle_head(p1w, head5); - EndPrimitive(); - } - - vec4 divider_line_size[2]; - divider_line_size[0] = side * viewportDrawingScale * CIRCLE_SIZE; - divider_line_size[1] = side * viewportDrawingScale * CIRCLE_SIZE; - if (connect_markers == 0) { - divider_line_size[0] = divider_line_size[0] * 3; - } - - // NOTE: basically we consider first vertex to be - // start and the second to be the end marker - // which is a bit inconsistent with svg - // but not that anyone will use multiple edge section annotation - - vec4 gap[2]; - - // start reference divider - if (display_start_symbol > 0) { - do_edge_verts_win( p0w + divider_line_size[0], p0w - divider_line_size[1] ); - EndPrimitive(); - } - - // end reference divider - if (display_end_symbol > 0) { - do_edge_verts_win( p1w + divider_line_size[1], p1w - divider_line_size[0] ); - EndPrimitive(); - } - - // stem - if (connect_markers > 0) { - gap[0] = (display_start_symbol > 0 ? (side * viewportDrawingScale * CIRCLE_SIZE) : vec4(0)); - gap[1] = (display_end_symbol > 0 ? (side * viewportDrawingScale * CIRCLE_SIZE) : vec4(0)); - do_edge_verts_win( p0w + gap[0], p1w - gap[1] ); - EndPrimitive(); - } - } - """ - def decorate(self, context, obj): + # gather geometry data and convert to winspace if obj.data.is_editmode: - verts, idxs = self.get_editmesh_geom(obj) + verts, edges_original = self.get_editmesh_geom(obj) else: - verts, idxs = self.get_mesh_geom(obj) + verts, edges_original = self.get_mesh_geom(obj) + if not edges_original: + return + viewportDrawingScale = self.get_viewport_drawing_scale(context) + winspace_verts = worldspace_to_winspace(verts, context) + + # setup geometry parameters + triangle_length = viewportDrawingScale * 22.63 + triangle_width = viewportDrawingScale * 11.31 + circle_size = viewportDrawingScale * 8 + + output_verts = [] + output_edges = [] + out_kwargs = { + "output_verts": output_verts, + "output_edges": output_edges, + } + + display_data = DecoratorData.get_section_markers_display_data(obj) + connect_markers = display_data["connect_markers"] + display_start_symbol = display_data["start"]["add_symbol"] + display_end_symbol = display_data["end"]["add_symbol"] + display_start_circle = display_data["start"]["add_circle"] + display_end_circle = display_data["end"]["add_circle"] + + # process edges + for edge in edges_original: + v0, v1 = winspace_verts[edge[0]], winspace_verts[edge[1]] + start_i = len(output_verts) + + if display_start_circle or display_end_circle: + circle_head = get_circle_head(circle_size) + + if display_start_symbol or display_end_symbol or connect_markers: + edge_dir = (v1 - v0).normalized() + side = (edge_dir.yx * Vector((1, -1))).to_3d() + edge_dir_circle = edge_dir * circle_size + + if display_start_symbol or display_end_symbol: + triangle_head = get_triangle_head(edge_dir, -side, triangle_length, triangle_width) + divider_offset = [] + divider_offset.append(edge_dir_circle if connect_markers else edge_dir_circle * 3) + divider_offset.append(edge_dir_circle) + + if display_start_circle: + start_i = add_verts_sequence([v + v0 for v in circle_head], start_i, **out_kwargs, closed=True) + # circle middle divider + if not display_start_symbol: + start_i = add_verts_sequence( + [v0 + divider_offset[0], v0 - divider_offset[1]], start_i, **out_kwargs + ) + + if display_start_symbol: + start_i = add_verts_sequence([v + v0 for v in triangle_head], start_i, **out_kwargs, closed=True) + + if display_end_circle: + start_i = add_verts_sequence([v + v1 for v in circle_head], start_i, **out_kwargs, closed=True) + # circle middle divider + if not display_end_symbol: + start_i = add_verts_sequence( + [v1 + divider_offset[1], v1 - divider_offset[0]], start_i, **out_kwargs + ) + + if display_end_symbol: + start_i = add_verts_sequence([v + v1 for v in triangle_head], start_i, **out_kwargs, closed=True) + + if connect_markers: + gap = [] + gap.append(edge_dir_circle if display_start_symbol else Vector((0, 0, 0))) + gap.append(edge_dir_circle if display_end_symbol else Vector((0, 0, 0))) + add_verts_sequence([v0 + gap[0], v1 - gap[1]], start_i, **out_kwargs) # TODO: add dashed line to shader with frag shader - display_data = DecoratorData.get_section_markers_display_data(obj) - self.draw_lines( - context, - obj, - verts, - idxs, - extra_float_kwargs={ - "connect_markers": float(display_data["connect_markers"]), - "display_start_symbol": float(display_data["start"]["add_symbol"]), - "display_end_symbol": float(display_data["end"]["add_symbol"]), - "display_start_circle": float(display_data["start"]["add_circle"]), - "display_end_circle": float(display_data["end"]["add_circle"]), - }, - ) + self.draw_lines(context, obj, output_verts, output_edges) class TextDecorator(BaseDecorator): @@ -1848,48 +1391,6 @@ class TextDecorator(BaseDecorator): objecttype = "TEXT" - DEF_GLSL = ( - BaseDecorator.DEF_GLSL - + """ - #define CIRCLE_SIZE 4.0 - #define CIRCLE_SEGS_ASTERISK 6 - """ - ) - - GEOM_GLSL = """ - uniform float viewportDrawingScale; - - layout(lines) in; - layout(triangle_strip, max_vertices=MAX_POINTS) out; - - void circle_head_asterisk(in float size, out vec4 head[CIRCLE_SEGS_ASTERISK]) { - float angle_d = PI * 2 / CIRCLE_SEGS_ASTERISK; - for(int i = 0; i. -from gpu.types import GPUShader from gpu_extras.batch import batch_for_shader import gpu +from mathutils import Vector + + +# NOTES: +# Since Metal doesn't support geometry shaders we stick to builtin shaders +# and generate all geometry data in python before passing it to the shader. +# This way was considered to be the most reliable atm. +# More: https://blender.stackexchange.com/questions/291674/migrating-geometry-shaders-to-metal +# +# BGL deprecation: +# 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_DEF_GLSL = """ @@ -29,12 +43,6 @@ BASE_DEF_GLSL = """ #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; @@ -160,12 +168,11 @@ void do_vertex_util(vec4 pos, vec2 ofs) // 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; + // TODO: radius is unnecessary? head[0] = side * -radius; - head[1] = nose * -.5; - head[2] = vec4(0) + ear; - head[3] = nose * .5; + head[1] = side * length * -.5; + head[2] = dir * width; + head[3] = side * length * .5; head[4] = side * radius; } @@ -189,6 +196,22 @@ void do_circle_head(vec4 pos_w, vec4 head[CIRCLE_SEGS]) { """ +def add_verts_sequence(verts, start_i, output_verts, output_edges, closed=False): + """Add sequence of verts to output lists, returns next vertex index""" + for i, v in enumerate(verts[:-1], start_i): + output_verts.append(v) + output_edges.append((i, i + 1)) + output_verts.append(verts[-1]) + if closed: + output_edges.append((i + 1, start_i)) + return i + 2 + + +def add_offsets(v, offsets): + """returns list of verts with offsets added""" + return [v + offset for offset in offsets] + + class BaseShader: """Wrapper for GPUShader To use for viewport decorations with geometry generated on GPU side. @@ -265,48 +288,49 @@ class BaseShader: """ def __init__(self): - # NB: libcode arg doesn't work - # TODO: rename to .shader - self.prog = GPUShader( - vertexcode=self.VERT_GLSL, - fragcode=self.FRAG_GLSL, - geocode=self.LIB_GLSL + self.GEOM_GLSL, - defines=self.DEF_GLSL, - ) + # 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.base_shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR") + + def get_shader(self): + """Returns shader for this type""" + return self.line_shader if self.TYPE == "LINES" else self.base_shader def batch(self, indices=None, **data): """Returns automatic GPUBatch filled with provided parameters""" - batch = batch_for_shader(self.prog, self.TYPE, data, indices=indices) - batch.program_set(self.prog) + shader = self.get_shader() + batch = batch_for_shader(shader, self.TYPE, data, indices=indices) return batch def bind(self): - self.prog.bind() + """need to bind shader before changing it's uniforms""" + shader = self.get_shader() + shader.bind() + return shader def glenable(self): gpu.state.blend_set("ALPHA") gpu.state.depth_test_set("LESS_EQUAL") def uniform_region(self, ctx): + shader = self.bind() + region = ctx.region region3d = ctx.region_data uniform_floats = { - "viewMatrix": region3d.perspective_matrix, - "winsize": (region.width, region.height), + "ModelViewProjectionMatrix": region3d.perspective_matrix, + # POLYLINE_UNIFORM_COLOR specific uniforms + "viewportSize": (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 + shader.uniform_float(name, value) -# TODO: add smoothing if this shades is going to be used +# TODO: add smoothing if this shader is going to be used +# TODO: dead code? class BaseLinesShader(BaseShader): """Draws line segments with gaps around vertices at endpoints""" @@ -429,37 +453,24 @@ class ExtrusionGuidesShader(GizmoShader): TYPE = "LINES" - DEF_GLSL = ( - BaseShader.DEF_GLSL - + """ - #define CROSS_SIZE .5 - """ - ) + def process_geometry(self, verts): + CROSS_SIZE = 0.5 - GEOM_GLSL = """ - uniform mat4 ModelViewProjectionMatrix; + p0, p1 = verts + bx = Vector((1, 0, 0)) * CROSS_SIZE + by = Vector((0, 1, 0)) * CROSS_SIZE - layout(lines) in; - layout(triangle_strip, max_vertices=MAX_POINTS) out; + output_verts = [] + output_edges = [] + out_kwargs = { + "output_verts": output_verts, + "output_edges": output_edges, + } - void main() { - // default setup for macro to work - vec2 EDGE_DIR; + start_i = 0 + start_i = add_verts_sequence(add_offsets(p0, [-bx, bx]), start_i, **out_kwargs) + start_i = add_verts_sequence(add_offsets(p0, [-by, by]), start_i, **out_kwargs) + start_i = add_verts_sequence(add_offsets(p1, [-bx, bx]), start_i, **out_kwargs) + start_i = add_verts_sequence(add_offsets(p1, [-by, by]), start_i, **out_kwargs) - 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; - - 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(); - } - """ + return output_verts, output_edges