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":