#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-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; }
@@ -21,6 +21,18 @@
<path d="M 0 3.5 L 0 10.5 L 10 7" class="annotation" style="fill:black;" />
</g>
</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">
<g>
<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)
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"}
@@ -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":
@@ -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
+1 -2
View File
@@ -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])
+10 -9
View File
@@ -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":