From 32c101cf74c8e05da801cc62cb723da03e9ae315 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 2 May 2022 09:20:33 +1000 Subject: [PATCH] #1153 Implement angular dimension annotations --- .../blenderbim/bim/data/styles/default.css | 1 + .../blenderbim/bim/data/templates/markers.svg | 12 ++ .../blenderbim/bim/module/cad/operator.py | 24 ++-- .../bim/module/drawing/annotation.py | 9 +- .../bim/module/drawing/svgwriter.py | 111 ++++++++++++++++++ src/blenderbim/blenderbim/tool/cad.py | 3 +- src/blenderbim/blenderbim/tool/drawing.py | 19 +-- 7 files changed, 157 insertions(+), 22 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/data/styles/default.css b/src/blenderbim/blenderbim/bim/data/styles/default.css index d1054ad4e2..2f7601f990 100644 --- a/src/blenderbim/blenderbim/bim/data/styles/default.css +++ b/src/blenderbim/blenderbim/bim/data/styles/default.css @@ -31,6 +31,7 @@ text { fill: black; stroke: none; } .PredefinedType-SECTIONLEVEL { marker-start: url(#section-level-marker); stroke-dasharray: 12.5, 3, 3, 3; } .PredefinedType-PLANLEVEL { marker-end: url(#plan-level-marker); } .PredefinedType-DIMENSION { marker-start: url(#dimension-marker-start); marker-end: url(#dimension-marker-end); } +.PredefinedType-ANGLE { marker-start: url(#angle-marker-start); marker-end: url(#angle-marker-end); } .PredefinedType-RADIUS { marker-end: url(#radius-marker-end); } .PredefinedType-DIAMETER { marker-start: url(#diameter-marker-start); marker-end: url(#diameter-marker-end); } .PredefinedType-HIDDENLINE { stroke-dasharray: 3, 2; } diff --git a/src/blenderbim/blenderbim/bim/data/templates/markers.svg b/src/blenderbim/blenderbim/bim/data/templates/markers.svg index 06c72f146e..87ba553db2 100644 --- a/src/blenderbim/blenderbim/bim/data/templates/markers.svg +++ b/src/blenderbim/blenderbim/bim/data/templates/markers.svg @@ -21,6 +21,18 @@ + + + + + + + + + + + + diff --git a/src/blenderbim/blenderbim/bim/module/cad/operator.py b/src/blenderbim/blenderbim/bim/module/cad/operator.py index 5f3a974a3e..c380cd3175 100644 --- a/src/blenderbim/blenderbim/bim/module/cad/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cad/operator.py @@ -223,6 +223,9 @@ class CadArcFrom2Points(bpy.types.Operator): layout.prop(self, prop) def execute(self, context): + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.mode_set(mode="EDIT") + if bpy.context.mode != "EDIT_MESH": return {"CANCELLED"} obj = bpy.context.active_object @@ -233,12 +236,13 @@ class CadArcFrom2Points(bpy.types.Operator): if not center: return {"CANCELLED"} mesh = obj.data + mw = obj.matrix_world bm = bmesh.from_edit_mesh(mesh) selected_verts = [v for v in bm.verts if v.select] if len(selected_verts) != 2: return {"CANCELLED"} - v1 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, selected_verts[0].co) - v2 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, selected_verts[1].co) + v1 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[0].co) + v2 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[1].co) l1 = v1 - center l2 = v2 - center angle = l1.angle_signed(l2) @@ -252,10 +256,16 @@ class CadArcFrom2Points(bpy.types.Operator): v = selected_verts[0] bm.verts.remove(selected_verts[1]) axis = region_3d.view_rotation @ mathutils.Vector((0, 0, 1)) - bmesh.ops.spin(bm, geom=[v], axis=axis, cent=cursor, steps=self.resolution * 4, angle=-angle) + bmesh.ops.spin( + bm, + geom=[v], + axis=mw.inverted().to_quaternion() @ axis, + cent=mw.inverted() @ cursor, + steps=self.resolution * 4, + angle=-angle, + ) bmesh.update_edit_mesh(mesh) mesh.update() - bm.free() return {"FINISHED"} @@ -291,15 +301,16 @@ class CadArcFrom3Points(bpy.types.Operator): if len(selected_verts) != 3: return {"CANCELLED"} pts = [v.co for v in selected_verts] - center = tool.Cad.generate_3PT(pts, obj, self.resolution * 4) + center = tool.Cad.get_center_of_arc(pts, obj) if not center: return {"CANCELLED"} bpy.context.scene.cursor.location = center if self.only_recalculate_center: - bm.free() return {"FINISHED"} + center = obj.matrix_world.inverted() @ center + def get_distance_to_other_points(vert): other_verts = [v for v in selected_verts if v != vert] total_vector = mathutils.Vector((0, 0, 0)) @@ -331,5 +342,4 @@ class CadArcFrom3Points(bpy.types.Operator): bmesh.ops.spin(bm, geom=[v], axis=normal, cent=center, steps=self.resolution * 4, angle=-angle) bmesh.update_edit_mesh(mesh) mesh.update() - bm.free() return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/drawing/annotation.py b/src/blenderbim/blenderbim/bim/module/drawing/annotation.py index e89e4db636..7401b4eccb 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/annotation.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/annotation.py @@ -164,10 +164,11 @@ class Annotator: obj = bpy.data.objects.new(object_type, data) collection.objects.link(obj) return obj - for obj in collection.objects: - element = tool.Ifc.get_entity(obj) - if element and element.ObjectType == object_type: - return obj + if object_type != "ANGLE": + for obj in collection.objects: + element = tool.Ifc.get_entity(obj) + if element and element.ObjectType == object_type: + return obj if data_type == "mesh": data = bpy.data.meshes.new(object_type) elif data_type == "curve": diff --git a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py index fa0f8fbcef..30233de575 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py @@ -20,7 +20,9 @@ import os import re import bpy import math +import bmesh import pystache +import mathutils import xml.etree.ElementTree as ET import svgwrite import ifcopenshell @@ -153,6 +155,8 @@ class SvgWriter: self.draw_stair_annotation(obj) elif element.ObjectType == "DIMENSION": self.draw_dimension_annotations(obj) + elif element.ObjectType == "ANGLE": + self.draw_angle_annotations(obj) elif element.ObjectType == "RADIUS": self.draw_radius_annotations(obj) elif element.ObjectType == "DIAMETER": @@ -623,6 +627,113 @@ class SvgWriter: ) ) + def draw_angle_annotations(self, obj): + # This implementation uses an SVG arc, which means that it can only draw + # arcs that are orthogonal to the view (e.g. not arcs in 3D). + # Gosh this is bad code :( + x_offset = self.raw_width / 2 + y_offset = self.raw_height / 2 + classes = self.get_attribute_classes(obj) + matrix_world = obj.matrix_world + + points = [v.co for v in obj.data.vertices][:3] + center = tool.Cad.get_center_of_arc(points, obj) + + bm = bmesh.new() + bm.from_mesh(obj.data) + bm.verts.ensure_lookup_table() + arc_end_verts = [v for v in bm.verts if len(v.link_edges) == 1] + arc_end_pts = [matrix_world @ v.co for v in arc_end_verts] + + # Probably need this when rewriting to use an SVG polyline instead of an arc + # arc_path = [arc_end_verts[0]] + # while True: + # last_point = arc_end_verts[0] + # found_another_point = False + # for edge in last_point.link_edges: + # v = edge.other_vert(last_point) + # if v not in arc_path: + # found_another_point = True + # arc_path.append(v) + # if not found_another_point: + # break + + distance_between_end_verts = (arc_end_verts[0].co - arc_end_verts[1].co).length + arc_mid_vert = arc_end_verts[0].link_edges[0].other_vert(arc_end_verts[0]) + is_reflex = 0 if (arc_mid_vert.co - arc_end_verts[1].co).length < distance_between_end_verts else 1 + + bm.free() + + # Calculate the angle + # This is the true normal in 3D, whereas the camera projection we use is the drawing direction. + # This assumes (because we use SVG arcs) that the radius is always orthogonal to our view. + # When rewriting to use polylines, we should use this normal instead. + # normal = mathutils.geometry.normal([arc_end_pts[0], arc_end_pts[1], center]) + normal = Vector(self.camera_projection) + dir1 = (arc_end_pts[0] - center).normalized() + dir2 = (arc_end_pts[1] - center).normalized() + + # Let's get the matrix that represents the coordinate system of the arc. + # This matrix allows us to get 2D vectors for calculating the signed arc angle. + z = normal + x = (arc_end_pts[0] - center).normalized() + y = z.cross(x) + arc_matrix = mathutils.Matrix([x, y, z]).transposed().to_4x4() + + dir1 = ((arc_matrix.inverted() @ arc_end_pts[0]) - (arc_matrix.inverted() @ center)).normalized() + dir2 = ((arc_matrix.inverted() @ arc_end_pts[1]) - (arc_matrix.inverted() @ center)).normalized() + angle = -dir1.xy.angle_signed(dir2.xy) + + #if is_reflex: + # angle = angle % (math.pi * 2) + + # Center of gravity of all vertices, used to help position the text + cog = Vector((0, 0, 0)) + for vert in obj.data.vertices: + cog += vert.co + cog = matrix_world @ (cog / len(obj.data.vertices)) + + radius = ((matrix_world @ obj.data.vertices[0].co) - center).length + + arc_midpoint = center + ((cog - center).normalized() * radius) + + text_position = self.project_point_onto_camera(arc_midpoint) + text_position = Vector(((x_offset + text_position.x) * self.scale, (y_offset - text_position.y) * self.scale)) + + center_projected = self.project_point_onto_camera(center) + center_position = Vector( + ((x_offset + center_projected.x) * self.scale, (y_offset - center_projected.y) * self.scale) + ) + text_offset = (text_position - center_position).xy.normalized() * 5 + text_position += text_offset + + text_style = { + "font-size": annotation.Annotator.get_svg_text_size(2.5), + "font-family": "OpenGost Type B TT", + "text-anchor": "middle", + "alignment-baseline": "middle", + "dominant-baseline": "middle", + } + + angle_text = abs(round(math.degrees(angle), 3)) + if is_reflex: + angle_text = 360 - angle_text + self.svg.add(self.svg.text(f"{angle_text}deg", insert=tuple(text_position), **text_style)) + + # Draw SVG arc, see for details: http://xahlee.info/js/svg_circle_arc.html + arc_proj_end_pts = [self.project_point_onto_camera(v) for v in arc_end_pts] + p1 = Vector(((x_offset + arc_proj_end_pts[0].x) * self.scale, (y_offset - arc_proj_end_pts[0].y) * self.scale)) + p2 = Vector(((x_offset + arc_proj_end_pts[1].x) * self.scale, (y_offset - arc_proj_end_pts[1].y) * self.scale)) + r = radius * self.scale + # reflex = 1 if angle > math.pi else 0 + reflex = is_reflex + if reflex: + sense = 0 if angle > 0 else 1 + else: + sense = 1 if angle > 0 else 0 + d = f"M {p1.x} {p1.y} A {r} {r} 0 {reflex} {sense} {p2.x} {p2.y}" + path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes))) + def draw_radius_annotations(self, obj): x_offset = self.raw_width / 2 y_offset = self.raw_height / 2 diff --git a/src/blenderbim/blenderbim/tool/cad.py b/src/blenderbim/blenderbim/tool/cad.py index d668e0bff9..e367e93810 100644 --- a/src/blenderbim/blenderbim/tool/cad.py +++ b/src/blenderbim/blenderbim/tool/cad.py @@ -303,10 +303,9 @@ class Cad: return bm @classmethod - def generate_3PT(cls, pts, obj, nv, mode=1): + def get_center_of_arc(cls, pts, obj): mw = obj.matrix_world V = Vector - nv = max(3, nv) # construction v1, v2, v3, v4 = V(pts[0]), V(pts[1]), V(pts[1]), V(pts[2]) diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 22e91b7ebb..168f0e9fa8 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -38,18 +38,19 @@ class Drawing(blenderbim.core.tool.Drawing): @classmethod def create_annotation_object(cls, object_type): data_type = { - "DIMENSION": "curve", - "RADIUS": "curve", + "ANGLE": "mesh", + "BREAKLINE": "mesh", "DIAMETER": "curve", + "DIMENSION": "curve", + "FILL_AREA": "mesh", + "HIDDEN_LINE": "mesh", + "LINEWORK": "mesh", + "PLAN_LEVEL": "curve", + "RADIUS": "curve", + "SECTION_LEVEL": "curve", + "STAIR_ARROW": "curve", "TEXT": "empty", "TEXT_LEADER": "curve", - "STAIR_ARROW": "curve", - "HIDDEN_LINE": "mesh", - "PLAN_LEVEL": "curve", - "SECTION_LEVEL": "curve", - "BREAKLINE": "mesh", - "FILL_AREA": "mesh", - "LINEWORK": "mesh", }[object_type] obj = annotation.Annotator.get_annotation_obj(object_type, data_type) if object_type == "FILL_AREA":