Refactored svg angle decorator code, solves #3602, more constistency between viewport and svg

Now it should be more consistent and more stable since svg and viewport are now using much more similar approach.
This commit is contained in:
Andrej730
2023-08-18 17:59:40 +05:00
parent 130355f915
commit b021103a99
3 changed files with 82 additions and 116 deletions
@@ -64,11 +64,6 @@ class profile_consequential:
cls.lines = []
def ccw(A, B, C):
"""whether a-b-c located in counter-clockwise order in 2d space"""
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
@@ -748,7 +743,7 @@ class AngleDecorator(BaseDecorator):
except ZeroDivisionError:
continue
circle_angle = acos(cos_a)
counter_clockwise = ccw(v2, v1, v0)
counter_clockwise = tool.Cad.is_counter_clockwise_order(v2, v1, v0)
angle_circle = get_angle_circle(circle_start, circle_angle, counter_clockwise)
add_verts_sequence([v1 + v for v in angle_circle], start_i_arcs, **out_kwargs_arcs)
@@ -780,7 +775,7 @@ class AngleDecorator(BaseDecorator):
region3d = context.region_data
viewportDrawingScale = self.get_viewport_drawing_scale(context)
ANGLE_LABEL_OFFSET = 25 * viewportDrawingScale
ANGLE_LABEL_OFFSET = 20 * viewportDrawingScale
last_segment_i = len(indices) - 1
for edge_i, edge_vertices in enumerate(indices):
@@ -798,24 +793,24 @@ class AngleDecorator(BaseDecorator):
edge1_ws = v2 - v1
try:
cos_a = edge0_ws.dot(edge1_ws) / (edge0_ws.length * edge1_ws.length)
angle_rad = acos(cos_a)
angle = angle_rad / pi * 180
except ZeroDivisionError:
continue
circle_angle_rad = acos(cos_a)
circle_angle = circle_angle_rad / pi * 180
angle = 0
# calculate angle position
p0 = location_3d_to_region_2d(region, region3d, v0)
p1 = location_3d_to_region_2d(region, region3d, v1)
p2 = location_3d_to_region_2d(region, region3d, v2)
p0, p1, p2 = [location_3d_to_region_2d(region, region3d, p) for p in vertices[i0 : i1 + 2]]
edge0 = p0 - p1
edge1 = p2 - p1
base_edge = edge0 if ccw(p0, p1, p2) else edge1
text_offset = (Matrix.Rotation(-circle_angle_rad / 2, 2) @ base_edge).normalized() * ANGLE_LABEL_OFFSET
radius = min(edge0.length_squared, edge1.length_squared) ** 0.5
# TODO: can be helpful for svg positioning
base_edge = edge0 if tool.Cad.is_counter_clockwise_order(p0, p1, p2) else edge1
text_offset = (Matrix.Rotation(-angle_rad / 2, 2) @ base_edge).normalized() * (radius + ANGLE_LABEL_OFFSET)
label_position = p1 + text_offset
text = f"{int(circle_angle)}d"
text = f"{int(angle)}deg"
label_dir = Vector((1, 0))
self.draw_label(context, text, label_position, label_dir)
self.draw_label(context, text, label_position, label_dir, box_alignment="center")
class DiameterDecorator(DimensionDecorator):
@@ -36,7 +36,7 @@ import blenderbim.bim.module.drawing.annotation as annotation
from blenderbim.bim.module.drawing.data import DecoratorData
from blenderbim.bim.ifc import IfcStore
from math import pi, ceil, atan, degrees
from math import pi, ceil, atan, degrees, acos
from mathutils import geometry, Vector
from bpy_extras import view3d_utils
@@ -949,54 +949,54 @@ class SvgWriter:
def draw_angle_annotations(self, obj):
points = obj.data.splines[0].points
region = bpy.context.region
area = tool.Blender.get_viewport_context()["area"]
region_3d = area.spaces.active.region_3d
# [1, 2, 3, 4, 5] -> [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
points_chunked = [points[i : i + 3] for i in range(len(points) - 2)]
for points_chunk in points_chunked:
points_2d = [view3d_utils.location_3d_to_region_2d(region, region_3d, p.co.xyz) for p in points_chunk]
points_chunk = [obj.matrix_world @ p.co.xyz for p in points_chunk]
self.draw_svg_3point_arc(obj, points_chunk)
edge0 = points_2d[0] - points_2d[1]
edge1 = points_2d[2] - points_2d[1]
angle_radius = min(edge0.length, edge1.length)
dir0 = edge0.normalized()
dir1 = edge1.normalized()
dir2 = ((dir0 + dir1) / 2).normalized()
# calculate p3 which is the center of the arc
# to use draw_svg_3point_arc()
p3 = points_2d[1] + dir2 * angle_radius
# make all edges the same radius
p0 = points_2d[1] + dir0 * angle_radius
p2 = points_2d[1] + dir1 * angle_radius
points_chunk = [view3d_utils.region_2d_to_origin_3d(region, region_3d, p) for p in [p0, p3, p2]]
# points = [p.co.xyz for p in bpy.context.active_object.data.splines[0].points[:3]]
bm = bmesh.new()
bm.verts.index_update()
bm.edges.index_update()
new_verts = [bm.verts.new(p) for p in points_chunk]
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in ((0, 1), (1, 2))]
self.draw_svg_3point_arc(obj, bm)
def draw_svg_3point_arc(self, obj, bm):
def draw_svg_3point_arc(self, obj, angle_points):
"""`angle_points` are expected to be already in world space"""
# 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 :(
points = [v.co for v in bm.verts][:3]
center = tool.Cad.get_center_of_arc(points, obj)
classes = self.get_attribute_classes(obj)
matrix_world = obj.matrix_world
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
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]
def position_on_svg(p):
p = self.project_point_onto_camera(p)
offset = Vector([self.raw_width / 2, self.raw_height / 2])
return (offset + p.xy * Vector((1, -1))) * self.svg_scale
# Probably need this when rewriting to use an SVG polyline instead of an arc
def get_angle_value():
"""points should be in world space"""
# calculate arc angle, need to make sure we do it in world space
v0, v1, v2 = angle_points
edge0_ws = v0 - v1
edge1_ws = v2 - v1
try:
cos_a = edge0_ws.dot(edge1_ws) / (edge0_ws.length * edge1_ws.length)
angle_rad = acos(cos_a)
angle = angle_rad / pi * 180
except ZeroDivisionError:
angle = 0
return angle
angle = get_angle_value()
angle_points = [position_on_svg(p) for p in angle_points]
# creating arc and making sure radius is consistent across the arc
edge0 = angle_points[0] - angle_points[1]
edge1 = angle_points[2] - angle_points[1]
angle_radius = min(edge0.length, edge1.length)
dir0 = edge0.normalized()
dir1 = edge1.normalized()
arc_mid_dir = ((dir0 + dir1) / 2).normalized()
arc_points = [angle_points[1] + direction * angle_radius for direction in [dir0, arc_mid_dir, dir1]]
arc_end_pts = [arc_points[0], arc_points[2]]
arc_mid_point = arc_points[1]
# The commented code below can be useful when we start using SVG polyline instead of an arc
# arc_path = [arc_end_verts[0]]
# while True:
# last_point = arc_end_verts[0]
@@ -1009,79 +1009,44 @@ class SvgWriter:
# 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.
# center = tool.Cad.get_center_of_arc([p.to_3d() for p in arc_points], None).to_2d()
# 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()
# normal = Vector(self.camera_projection)
# dir1 = (arc_end_pts[0] - center).normalized()
# arc_mid_dir = (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()
# 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 point in points:
cog += point
cog = matrix_world @ (cog / len(points))
radius = ((matrix_world @ points[0]) - 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.svg_scale, (y_offset - text_position.y) * self.svg_scale)
)
center_projected = self.project_point_onto_camera(center)
center_position = Vector(
((x_offset + center_projected.x) * self.svg_scale, (y_offset - center_projected.y) * self.svg_scale)
)
text_offset = (text_position - center_position).xy.normalized() * 5
text_position += text_offset
# 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)
# calculating text parameters and adding text
text_position = arc_mid_point + arc_mid_dir * 5
text_style = SvgWriter.get_box_alignment_parameters("center")
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), class_="ANGLE", **text_style))
angle_text = f"{int(angle)}deg"
self.svg.add(self.svg.text(angle_text, insert=text_position, class_="ANGLE", **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.svg_scale, (y_offset - arc_proj_end_pts[0].y) * self.svg_scale)
)
p2 = Vector(
((x_offset + arc_proj_end_pts[1].x) * self.svg_scale, (y_offset - arc_proj_end_pts[1].y) * self.svg_scale)
)
r = radius * self.svg_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
p1, p2 = arc_end_pts
r = angle_radius
# reflex: 0 => arc < 180 degrees
# 1 => arc > 180 degrees
reflex = int(angle > 180)
# sense: 0 => moving at negative angles
# 1 => moving at positive angles
sense = int(tool.Cad.is_counter_clockwise_order(*arc_points))
d = f"M {p1.x} {p1.y} A {r} {r} 0 {reflex} {sense} {p2.x} {p2.y}"
classes = self.get_attribute_classes(obj)
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
def draw_radius_annotations(self, obj):
+6
View File
@@ -413,6 +413,7 @@ class Cad:
@classmethod
def get_center_of_arc(cls, pts, obj=None):
"""also will convert center of arc from local space of `obj` (if it's provided)"""
mw = obj.matrix_world if obj else None
V = Vector
@@ -490,3 +491,8 @@ class Cad:
edges = [(n, n + 1) for n in range(len(verts) - 1)]
return verts, edges
@classmethod
def is_counter_clockwise_order(cls, A, B, C):
"""whether A-B-C located in counter-clockwise order in 2d space"""
return (C.y - A.y) * (B.x - A.x) > (B.y - A.y) * (C.x - A.x)