#1153 Implement angular dimension annotations

This commit is contained in:
Dion Moult
2022-05-02 09:20:33 +10:00
parent 2a1aa535ba
commit 32c101cf74
7 changed files with 157 additions and 22 deletions
@@ -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-SECTIONLEVEL { marker-start: url(#section-level-marker); stroke-dasharray: 12.5, 3, 3, 3; }
.PredefinedType-PLANLEVEL { marker-end: url(#plan-level-marker); } .PredefinedType-PLANLEVEL { marker-end: url(#plan-level-marker); }
.PredefinedType-DIMENSION { marker-start: url(#dimension-marker-start); marker-end: url(#dimension-marker-end); } .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-RADIUS { marker-end: url(#radius-marker-end); }
.PredefinedType-DIAMETER { marker-start: url(#diameter-marker-start); marker-end: url(#diameter-marker-end); } .PredefinedType-DIAMETER { marker-start: url(#diameter-marker-start); marker-end: url(#diameter-marker-end); }
.PredefinedType-HIDDENLINE { stroke-dasharray: 3, 2; } .PredefinedType-HIDDENLINE { stroke-dasharray: 3, 2; }
@@ -21,6 +21,18 @@
<path d="M 0 3.5 L 0 10.5 L 10 7" class="annotation" style="fill:black;" /> <path d="M 0 3.5 L 0 10.5 L 10 7" class="annotation" style="fill:black;" />
</g> </g>
</marker> </marker>
<marker id="angle-marker-start" markerHeight="14" markerWidth="11" orient="auto" refX="1" refY="7">
<g>
<path d="M 11 3.5 L 11 10.5 L 1 7" class="annotation" style="fill:black;" />
<path d="M 1 0 L 1 14" class="annotation" style="stroke-width:1;" />
</g>
</marker>
<marker id="angle-marker-end" markerHeight="14" markerWidth="11" orient="auto" refX="10" refY="7">
<g>
<path d="M 0 3.5 L 0 10.5 L 10 7" class="annotation" style="fill:black;" />
<path d="M 10 0 L 10 14" class="annotation" style="stroke-width:1;" />
</g>
</marker>
<marker id="diameter-marker-start" markerHeight="14" markerWidth="11" orient="auto" refX="1" refY="7"> <marker id="diameter-marker-start" markerHeight="14" markerWidth="11" orient="auto" refX="1" refY="7">
<g> <g>
<path d="M 11 3.5 L 11 10.5 L 1 7" class="annotation" style="fill:black;" /> <path d="M 11 3.5 L 11 10.5 L 1 7" class="annotation" style="fill:black;" />

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

@@ -223,6 +223,9 @@ class CadArcFrom2Points(bpy.types.Operator):
layout.prop(self, prop) layout.prop(self, prop)
def execute(self, context): def execute(self, context):
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.mode_set(mode="EDIT")
if bpy.context.mode != "EDIT_MESH": if bpy.context.mode != "EDIT_MESH":
return {"CANCELLED"} return {"CANCELLED"}
obj = bpy.context.active_object obj = bpy.context.active_object
@@ -233,12 +236,13 @@ class CadArcFrom2Points(bpy.types.Operator):
if not center: if not center:
return {"CANCELLED"} return {"CANCELLED"}
mesh = obj.data mesh = obj.data
mw = obj.matrix_world
bm = bmesh.from_edit_mesh(mesh) bm = bmesh.from_edit_mesh(mesh)
selected_verts = [v for v in bm.verts if v.select] selected_verts = [v for v in bm.verts if v.select]
if len(selected_verts) != 2: if len(selected_verts) != 2:
return {"CANCELLED"} return {"CANCELLED"}
v1 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, selected_verts[0].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, selected_verts[1].co) v2 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[1].co)
l1 = v1 - center l1 = v1 - center
l2 = v2 - center l2 = v2 - center
angle = l1.angle_signed(l2) angle = l1.angle_signed(l2)
@@ -252,10 +256,16 @@ class CadArcFrom2Points(bpy.types.Operator):
v = selected_verts[0] v = selected_verts[0]
bm.verts.remove(selected_verts[1]) bm.verts.remove(selected_verts[1])
axis = region_3d.view_rotation @ mathutils.Vector((0, 0, 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) bmesh.update_edit_mesh(mesh)
mesh.update() mesh.update()
bm.free()
return {"FINISHED"} return {"FINISHED"}
@@ -291,15 +301,16 @@ class CadArcFrom3Points(bpy.types.Operator):
if len(selected_verts) != 3: if len(selected_verts) != 3:
return {"CANCELLED"} return {"CANCELLED"}
pts = [v.co for v in selected_verts] 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: if not center:
return {"CANCELLED"} return {"CANCELLED"}
bpy.context.scene.cursor.location = center bpy.context.scene.cursor.location = center
if self.only_recalculate_center: if self.only_recalculate_center:
bm.free()
return {"FINISHED"} return {"FINISHED"}
center = obj.matrix_world.inverted() @ center
def get_distance_to_other_points(vert): def get_distance_to_other_points(vert):
other_verts = [v for v in selected_verts if v != vert] other_verts = [v for v in selected_verts if v != vert]
total_vector = mathutils.Vector((0, 0, 0)) 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.ops.spin(bm, geom=[v], axis=normal, cent=center, steps=self.resolution * 4, angle=-angle)
bmesh.update_edit_mesh(mesh) bmesh.update_edit_mesh(mesh)
mesh.update() mesh.update()
bm.free()
return {"FINISHED"} return {"FINISHED"}
@@ -164,10 +164,11 @@ class Annotator:
obj = bpy.data.objects.new(object_type, data) obj = bpy.data.objects.new(object_type, data)
collection.objects.link(obj) collection.objects.link(obj)
return obj return obj
for obj in collection.objects: if object_type != "ANGLE":
element = tool.Ifc.get_entity(obj) for obj in collection.objects:
if element and element.ObjectType == object_type: element = tool.Ifc.get_entity(obj)
return obj if element and element.ObjectType == object_type:
return obj
if data_type == "mesh": if data_type == "mesh":
data = bpy.data.meshes.new(object_type) data = bpy.data.meshes.new(object_type)
elif data_type == "curve": elif data_type == "curve":
@@ -20,7 +20,9 @@ import os
import re import re
import bpy import bpy
import math import math
import bmesh
import pystache import pystache
import mathutils
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import svgwrite import svgwrite
import ifcopenshell import ifcopenshell
@@ -153,6 +155,8 @@ class SvgWriter:
self.draw_stair_annotation(obj) self.draw_stair_annotation(obj)
elif element.ObjectType == "DIMENSION": elif element.ObjectType == "DIMENSION":
self.draw_dimension_annotations(obj) self.draw_dimension_annotations(obj)
elif element.ObjectType == "ANGLE":
self.draw_angle_annotations(obj)
elif element.ObjectType == "RADIUS": elif element.ObjectType == "RADIUS":
self.draw_radius_annotations(obj) self.draw_radius_annotations(obj)
elif element.ObjectType == "DIAMETER": 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): def draw_radius_annotations(self, obj):
x_offset = self.raw_width / 2 x_offset = self.raw_width / 2
y_offset = self.raw_height / 2 y_offset = self.raw_height / 2
+1 -2
View File
@@ -303,10 +303,9 @@ class Cad:
return bm return bm
@classmethod @classmethod
def generate_3PT(cls, pts, obj, nv, mode=1): def get_center_of_arc(cls, pts, obj):
mw = obj.matrix_world mw = obj.matrix_world
V = Vector V = Vector
nv = max(3, nv)
# construction # construction
v1, v2, v3, v4 = V(pts[0]), V(pts[1]), V(pts[1]), V(pts[2]) v1, v2, v3, v4 = V(pts[0]), V(pts[1]), V(pts[1]), V(pts[2])
+10 -9
View File
@@ -38,18 +38,19 @@ class Drawing(blenderbim.core.tool.Drawing):
@classmethod @classmethod
def create_annotation_object(cls, object_type): def create_annotation_object(cls, object_type):
data_type = { data_type = {
"DIMENSION": "curve", "ANGLE": "mesh",
"RADIUS": "curve", "BREAKLINE": "mesh",
"DIAMETER": "curve", "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": "empty",
"TEXT_LEADER": "curve", "TEXT_LEADER": "curve",
"STAIR_ARROW": "curve",
"HIDDEN_LINE": "mesh",
"PLAN_LEVEL": "curve",
"SECTION_LEVEL": "curve",
"BREAKLINE": "mesh",
"FILL_AREA": "mesh",
"LINEWORK": "mesh",
}[object_type] }[object_type]
obj = annotation.Annotator.get_annotation_obj(object_type, data_type) obj = annotation.Annotator.get_annotation_obj(object_type, data_type)
if object_type == "FILL_AREA": if object_type == "FILL_AREA":