Cleanup: remove "Tweak" event type
[blender-addons.git] / precision_drawing_tools / pdt_functions.py
blob66bf1de2f1e33684967f551320321e989b9ec739
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # -----------------------------------------------------------------------
4 # Author: Alan Odom (Clockmender), Rune Morling (ermo) Copyright (c) 2019
5 # -----------------------------------------------------------------------
7 # Common Functions used in more than one place in PDT Operations
9 import bpy
10 import bmesh
11 import bgl
12 import gpu
13 import numpy as np
14 from mathutils import Vector, Quaternion
15 from gpu_extras.batch import batch_for_shader
16 from math import cos, sin, pi
17 from .pdt_msg_strings import (
18 PDT_ERR_VERT_MODE,
19 PDT_ERR_SEL_2_V_1_E,
20 PDT_ERR_SEL_2_OBJS,
21 PDT_ERR_NO_ACT_OBJ,
22 PDT_ERR_SEL_1_EDGEM,
24 from . import pdt_exception
25 PDT_ShaderError = pdt_exception.ShaderError
28 def debug(msg, prefix=""):
29 """Print a debug message to the console if PDT's or Blender's debug flags are set.
31 Note:
32 The printed message will be of the form:
34 {prefix}{caller file name:line number}| {msg}
36 Args:
37 msg: Incoming message to display
38 prefix: Always Blank
40 Returns:
41 Nothing.
42 """
44 pdt_debug = bpy.context.preferences.addons[__package__].preferences.debug
45 if bpy.app.debug or bpy.app.debug_python or pdt_debug:
46 import traceback
48 def extract_filename(fullpath):
49 """Return only the filename part of fullpath (excluding its path).
51 Args:
52 fullpath: Filename's full path
54 Returns:
55 filename.
56 """
57 # Expected to end up being a string containing only the filename
58 # (i.e. excluding its preceding '/' separated path)
59 filename = fullpath.split('/')[-1]
60 #print(filename)
61 # something went wrong
62 if len(filename) < 1:
63 return fullpath
64 # since this is a string, just return it
65 return filename
67 # stack frame corresponding to the line where debug(msg) was called
68 #print(traceback.extract_stack()[-2])
69 laststack = traceback.extract_stack()[-2]
70 #print(laststack[0])
71 # laststack[0] is the caller's full file name, laststack[1] is the line number
72 print(f"{prefix}{extract_filename(laststack[0])}:{laststack[1]}| {msg}")
74 def oops(self, context):
75 """Error Routine.
77 Note:
78 Displays error message in a popup.
80 Args:
81 context: Blender bpy.context instance.
83 Returns:
84 Nothing.
85 """
87 scene = context.scene
88 pg = scene.pdt_pg
89 self.layout.label(text=pg.error)
92 def set_mode(mode_pl):
93 """Sets Active Axes for View Orientation.
95 Note:
96 Sets indices of axes for locational vectors:
97 a3 is normal to screen, or depth
98 "XY": a1 = x, a2 = y, a3 = z
99 "XZ": a1 = x, a2 = z, a3 = y
100 "YZ": a1 = y, a2 = z, a3 = x
102 Args:
103 mode_pl: Plane Selector variable as input
105 Returns:
106 3 Integer indices.
109 order = {
110 "XY": (0, 1, 2),
111 "XZ": (0, 2, 1),
112 "YZ": (1, 2, 0),
113 "LO": (0, 1, 2),
115 return order[mode_pl]
118 def set_axis(mode_pl):
119 """Sets Active Axes for View Orientation.
121 Note:
122 Sets indices for axes from taper vectors
123 Axis order: Rotate Axis, Move Axis, Height Axis
125 Args:
126 mode_pl: Taper Axis Selector variable as input
128 Returns:
129 3 Integer Indicies.
132 order = {
133 "RX-MY": (0, 1, 2),
134 "RX-MZ": (0, 2, 1),
135 "RY-MX": (1, 0, 2),
136 "RY-MZ": (1, 2, 0),
137 "RZ-MX": (2, 0, 1),
138 "RZ-MY": (2, 1, 0),
140 return order[mode_pl]
143 def check_selection(num, bm, obj):
144 """Check that the Object's select_history has sufficient entries.
146 Note:
147 If selection history is not Verts, clears selection and history.
149 Args:
150 num: The number of entries required for each operation
151 bm: The Bmesh from the Object
152 obj: The Object
154 Returns:
155 list of 3D points as Vectors.
158 if len(bm.select_history) < num:
159 return None
160 active_vertex = bm.select_history[-1]
161 if isinstance(active_vertex, bmesh.types.BMVert):
162 vector_a = active_vertex.co
163 if num == 1:
164 return vector_a
165 if num == 2:
166 vector_b = bm.select_history[-2].co
167 return vector_a, vector_b
168 if num == 3:
169 vector_b = bm.select_history[-2].co
170 vector_c = bm.select_history[-3].co
171 return vector_a, vector_b, vector_c
172 if num == 4:
173 vector_b = bm.select_history[-2].co
174 vector_c = bm.select_history[-3].co
175 vector_d = bm.select_history[-4].co
176 return vector_a, vector_b, vector_c, vector_d
177 else:
178 for f in bm.faces:
179 f.select_set(False)
180 for e in bm.edges:
181 e.select_set(False)
182 for v in bm.verts:
183 v.select_set(False)
184 bmesh.update_edit_mesh(obj.data)
185 bm.select_history.clear()
186 return None
189 def update_sel(bm, verts, edges, faces):
190 """Updates Vertex, Edge and Face Selections following a function.
192 Args:
193 bm: Object Bmesh
194 verts: New Selection for Vertices
195 edges: The Edges on which to operate
196 faces: The Faces on which to operate
198 Returns:
199 Nothing.
201 for f in bm.faces:
202 f.select_set(False)
203 for e in bm.edges:
204 e.select_set(False)
205 for v in bm.verts:
206 v.select_set(False)
207 for v in verts:
208 v.select_set(True)
209 for e in edges:
210 e.select_set(True)
211 for f in faces:
212 f.select_set(True)
215 def view_coords(x_loc, y_loc, z_loc):
216 """Converts input Vector values to new Screen Oriented Vector.
218 Args:
219 x_loc: X coordinate from vector
220 y_loc: Y coordinate from vector
221 z_loc: Z coordinate from vector
223 Returns:
224 Vector adjusted to View's Inverted Transformation Matrix.
227 areas = [a for a in bpy.context.screen.areas if a.type == "VIEW_3D"]
228 if len(areas) > 0:
229 view_matrix = areas[0].spaces.active.region_3d.view_matrix
230 view_matrix = view_matrix.to_3x3().normalized().inverted()
231 view_location = Vector((x_loc, y_loc, z_loc))
232 new_view_location = view_matrix @ view_location
233 return new_view_location
235 return Vector((0, 0, 0))
238 def view_coords_i(x_loc, y_loc, z_loc):
239 """Converts Screen Oriented input Vector values to new World Vector.
241 Note:
242 Converts View transformation Matrix to Rotational Matrix
244 Args:
245 x_loc: X coordinate from vector
246 y_loc: Y coordinate from vector
247 z_loc: Z coordinate from vector
249 Returns:
250 Vector adjusted to View's Transformation Matrix.
253 areas = [a for a in bpy.context.screen.areas if a.type == "VIEW_3D"]
254 if len(areas) > 0:
255 view_matrix = areas[0].spaces.active.region_3d.view_matrix
256 view_matrix = view_matrix.to_3x3().normalized()
257 view_location = Vector((x_loc, y_loc, z_loc))
258 new_view_location = view_matrix @ view_location
259 return new_view_location
261 return Vector((0, 0, 0))
264 def view_dir(dis_v, ang_v):
265 """Converts Distance and Angle to View Oriented Vector.
267 Note:
268 Converts View Transformation Matrix to Rotational Matrix (3x3)
269 Angles are Converts to Radians from degrees.
271 Args:
272 dis_v: Scene PDT distance
273 ang_v: Scene PDT angle
275 Returns:
276 World Vector.
279 areas = [a for a in bpy.context.screen.areas if a.type == "VIEW_3D"]
280 if len(areas) > 0:
281 view_matrix = areas[0].spaces.active.region_3d.view_matrix
282 view_matrix = view_matrix.to_3x3().normalized().inverted()
283 view_location = Vector((0, 0, 0))
284 view_location.x = dis_v * cos(ang_v * pi / 180)
285 view_location.y = dis_v * sin(ang_v * pi / 180)
286 new_view_location = view_matrix @ view_location
287 return new_view_location
289 return Vector((0, 0, 0))
292 def euler_to_quaternion(roll, pitch, yaw):
293 """Converts Euler Rotation to Quaternion Rotation.
295 Args:
296 roll: Roll in Euler rotation
297 pitch: Pitch in Euler rotation
298 yaw: Yaw in Euler rotation
300 Returns:
301 Quaternion Rotation.
304 # fmt: off
305 quat_x = (np.sin(roll/2) * np.cos(pitch/2) * np.cos(yaw/2)
306 - np.cos(roll/2) * np.sin(pitch/2) * np.sin(yaw/2))
307 quat_y = (np.cos(roll/2) * np.sin(pitch/2) * np.cos(yaw/2)
308 + np.sin(roll/2) * np.cos(pitch/2) * np.sin(yaw/2))
309 quat_z = (np.cos(roll/2) * np.cos(pitch/2) * np.sin(yaw/2)
310 - np.sin(roll/2) * np.sin(pitch/2) * np.cos(yaw/2))
311 quat_w = (np.cos(roll/2) * np.cos(pitch/2) * np.cos(yaw/2)
312 + np.sin(roll/2) * np.sin(pitch/2) * np.sin(yaw/2))
313 # fmt: on
314 return Quaternion((quat_w, quat_x, quat_y, quat_z))
317 def arc_centre(vector_a, vector_b, vector_c):
318 """Calculates Centre of Arc from 3 Vector Locations using standard Numpy routine
320 Args:
321 vector_a: Active vector location
322 vector_b: Second vector location
323 vector_c: Third vector location
325 Returns:
326 Vector representing Arc Centre and Float representing Arc Radius.
329 coord_a = np.array([vector_a.x, vector_a.y, vector_a.z])
330 coord_b = np.array([vector_b.x, vector_b.y, vector_b.z])
331 coord_c = np.array([vector_c.x, vector_c.y, vector_c.z])
332 line_a = np.linalg.norm(coord_c - coord_b)
333 line_b = np.linalg.norm(coord_c - coord_a)
334 line_c = np.linalg.norm(coord_b - coord_a)
335 # fmt: off
336 line_s = (line_a+line_b+line_c) / 2
337 radius = (
338 line_a*line_b*line_c/4
339 / np.sqrt(line_s
340 * (line_s-line_a)
341 * (line_s-line_b)
342 * (line_s-line_c))
344 base_1 = line_a*line_a * (line_b*line_b + line_c*line_c - line_a*line_a)
345 base_2 = line_b*line_b * (line_a*line_a + line_c*line_c - line_b*line_b)
346 base_3 = line_c*line_c * (line_a*line_a + line_b*line_b - line_c*line_c)
347 # fmt: on
348 intersect_coord = np.column_stack((coord_a, coord_b, coord_c))
349 intersect_coord = intersect_coord.dot(np.hstack((base_1, base_2, base_3)))
350 intersect_coord /= base_1 + base_2 + base_3
351 return Vector((intersect_coord[0], intersect_coord[1], intersect_coord[2])), radius
354 def intersection(vertex_a, vertex_b, vertex_c, vertex_d, plane):
355 """Calculates Intersection Point of 2 Imagined Lines from 4 Vectors.
357 Note:
358 Calculates Converging Intersect Location and indication of
359 whether the lines are convergent using standard Numpy Routines
361 Args:
362 vertex_a: Active vector location of first line
363 vertex_b: Second vector location of first line
364 vertex_c: Third vector location of 2nd line
365 vertex_d: Fourth vector location of 2nd line
366 plane: Working Plane 4 Vector Locations representing 2 lines and Working Plane
368 Returns:
369 Intersection Vector and Boolean for convergent state.
372 if plane == "LO":
373 vertex_offset = vertex_b - vertex_a
374 vertex_b = view_coords_i(vertex_offset.x, vertex_offset.y, vertex_offset.z)
375 vertex_offset = vertex_d - vertex_a
376 vertex_d = view_coords_i(vertex_offset.x, vertex_offset.y, vertex_offset.z)
377 vertex_offset = vertex_c - vertex_a
378 vertex_c = view_coords_i(vertex_offset.x, vertex_offset.y, vertex_offset.z)
379 vector_ref = Vector((0, 0, 0))
380 coord_a = (vertex_c.x, vertex_c.y)
381 coord_b = (vertex_d.x, vertex_d.y)
382 coord_c = (vertex_b.x, vertex_b.y)
383 coord_d = (vector_ref.x, vector_ref.y)
384 else:
385 a1, a2, a3 = set_mode(plane)
386 coord_a = (vertex_c[a1], vertex_c[a2])
387 coord_b = (vertex_d[a1], vertex_d[a2])
388 coord_c = (vertex_a[a1], vertex_a[a2])
389 coord_d = (vertex_b[a1], vertex_b[a2])
390 v_stack = np.vstack([coord_a, coord_b, coord_c, coord_d])
391 h_stack = np.hstack((v_stack, np.ones((4, 1))))
392 line_a = np.cross(h_stack[0], h_stack[1])
393 line_b = np.cross(h_stack[2], h_stack[3])
394 x_loc, y_loc, z_loc = np.cross(line_a, line_b)
395 if z_loc == 0:
396 return Vector((0, 0, 0)), False
397 new_x_loc = x_loc / z_loc
398 new_z_loc = y_loc / z_loc
399 if plane == "LO":
400 new_y_loc = 0
401 else:
402 new_y_loc = vertex_a[a3]
403 # Order Vector Delta
404 if plane == "XZ":
405 vector_delta = Vector((new_x_loc, new_y_loc, new_z_loc))
406 elif plane == "XY":
407 vector_delta = Vector((new_x_loc, new_z_loc, new_y_loc))
408 elif plane == "YZ":
409 vector_delta = Vector((new_y_loc, new_x_loc, new_z_loc))
410 else:
411 # Must be Local View Plane
412 vector_delta = view_coords(new_x_loc, new_z_loc, new_y_loc) + vertex_a
413 return vector_delta, True
416 def get_percent(obj, flip_percent, per_v, data, scene):
417 """Calculates a Percentage Distance between 2 Vectors.
419 Note:
420 Calculates a point that lies a set percentage between two given points
421 using standard Numpy Routines.
423 Works for either 2 vertices for an object in Edit mode
424 or 2 selected objects in Object mode.
426 Args:
427 obj: The Object under consideration
428 flip_percent: Setting this to True measures the percentage starting from the second vector
429 per_v: Percentage Input Value
430 data: pg.flip, pg.percent scene variables & Operational Mode
431 scene: Context Scene
433 Returns:
434 World Vector.
437 pg = scene.pdt_pg
439 if obj.mode == "EDIT":
440 bm = bmesh.from_edit_mesh(obj.data)
441 verts = [v for v in bm.verts if v.select]
442 if len(verts) == 2:
443 vector_a = verts[0].co
444 vector_b = verts[1].co
445 if vector_a is None:
446 pg.error = PDT_ERR_VERT_MODE
447 bpy.context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
448 return None
449 else:
450 pg.error = PDT_ERR_SEL_2_V_1_E + str(len(verts)) + " Vertices"
451 bpy.context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
452 return None
453 coord_a = np.array([vector_a.x, vector_a.y, vector_a.z])
454 coord_b = np.array([vector_b.x, vector_b.y, vector_b.z])
455 if obj.mode == "OBJECT":
456 objs = bpy.context.view_layer.objects.selected
457 if len(objs) != 2:
458 pg.error = PDT_ERR_SEL_2_OBJS + str(len(objs)) + ")"
459 bpy.context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
460 return None
461 coord_a = np.array(
463 objs[-1].matrix_world.decompose()[0].x,
464 objs[-1].matrix_world.decompose()[0].y,
465 objs[-1].matrix_world.decompose()[0].z,
468 coord_b = np.array(
470 objs[-2].matrix_world.decompose()[0].x,
471 objs[-2].matrix_world.decompose()[0].y,
472 objs[-2].matrix_world.decompose()[0].z,
475 coord_c = coord_b - coord_a
476 coord_d = np.array([0, 0, 0])
477 _per_v = per_v
478 if (flip_percent and data != "MV") or data == "MV":
479 _per_v = 100 - per_v
480 coord_out = (coord_d+coord_c) * (_per_v / 100) + coord_a
481 return Vector((coord_out[0], coord_out[1], coord_out[2]))
484 def obj_check(obj, scene, operation):
485 """Check Object & Selection Validity.
487 Args:
488 obj: Active Object
489 scene: Active Scene
490 operation: The Operation e.g. Create New Vertex
492 Returns:
493 Object Bmesh
494 Validity Boolean.
497 pg = scene.pdt_pg
498 _operation = operation.upper()
500 if obj is None:
501 pg.error = PDT_ERR_NO_ACT_OBJ
502 bpy.context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
503 return None, False
504 if obj.mode == "EDIT":
505 bm = bmesh.from_edit_mesh(obj.data)
506 if _operation == "S":
507 if len(bm.edges) < 1:
508 pg.error = f"{PDT_ERR_SEL_1_EDGEM} {len(bm.edges)})"
509 bpy.context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
510 return None, False
511 return bm, True
512 if len(bm.select_history) >= 1:
513 vector_a = None
514 if _operation not in {"D", "E", "F", "G", "N", "S"}:
515 vector_a = check_selection(1, bm, obj)
516 else:
517 verts = [v for v in bm.verts if v.select]
518 if len(verts) > 0:
519 vector_a = verts[0]
520 if vector_a is None:
521 pg.error = PDT_ERR_VERT_MODE
522 bpy.context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
523 return None, False
524 return bm, True
525 return None, True
528 def dis_ang(values, flip_angle, plane, scene):
529 """Set Working Axes when using Direction command.
531 Args:
532 values: Input Arguments
533 flip_angle: Whether to flip the angle
534 plane: Working Plane
535 scene: Current Scene
537 Returns:
538 Directional Offset as a Vector.
541 pg = scene.pdt_pg
542 dis_v = float(values[0])
543 ang_v = float(values[1])
544 if flip_angle:
545 if ang_v > 0:
546 ang_v = ang_v - 180
547 else:
548 ang_v = ang_v + 180
549 pg.angle = ang_v
550 if plane == "LO":
551 vector_delta = view_dir(dis_v, ang_v)
552 else:
553 a1, a2, _ = set_mode(plane)
554 vector_delta = Vector((0, 0, 0))
555 # fmt: off
556 vector_delta[a1] = vector_delta[a1] + (dis_v * cos(ang_v * pi/180))
557 vector_delta[a2] = vector_delta[a2] + (dis_v * sin(ang_v * pi/180))
558 # fmt: on
559 return vector_delta
562 # Shader for displaying the Pivot Point as Graphics.
564 SHADER = gpu.shader.from_builtin("3D_UNIFORM_COLOR") if not bpy.app.background else None
567 def draw_3d(coords, gtype, rgba, context):
568 """Draw Pivot Point Graphics.
570 Note:
571 Draws either Lines Points, or Tris using defined shader
573 Args:
574 coords: Input Coordinates List
575 gtype: Graphic Type
576 rgba: Colour in RGBA format
577 context: Blender bpy.context instance.
579 Returns:
580 Nothing.
583 batch = batch_for_shader(SHADER, gtype, {"pos": coords})
585 try:
586 if coords is not None:
587 bgl.glEnable(bgl.GL_BLEND)
588 SHADER.bind()
589 SHADER.uniform_float("color", rgba)
590 batch.draw(SHADER)
591 except:
592 raise PDT_ShaderError
595 def draw_callback_3d(self, context):
596 """Create Coordinate List for Pivot Point Graphic.
598 Note:
599 Creates coordinates for Pivot Point Graphic consisting of 6 Tris
600 and one Point colour coded Red; X axis, Green; Y axis, Blue; Z axis
601 and a yellow point based upon screen scale
603 Args:
604 context: Blender bpy.context instance.
606 Returns:
607 Nothing.
610 scene = context.scene
611 pg = scene.pdt_pg
612 region_width = context.region.width
613 x_loc = pg.pivot_loc.x
614 y_loc = pg.pivot_loc.y
615 z_loc = pg.pivot_loc.z
616 # Scale it from view
617 areas = [a for a in context.screen.areas if a.type == "VIEW_3D"]
618 if len(areas) > 0:
619 scale_factor = abs(areas[0].spaces.active.region_3d.window_matrix.decompose()[2][1])
620 # Check for orhtographic view and resize
621 #if areas[0].spaces.active.region_3d.is_orthographic_side_view:
622 # dim_a = region_width / sf / 60000 * pg.pivot_size
623 #else:
624 # dim_a = region_width / sf / 5000 * pg.pivot_size
625 dim_a = region_width / scale_factor / 50000 * pg.pivot_size
626 dim_b = dim_a * 0.65
627 dim_c = dim_a * 0.05 + (pg.pivot_width * dim_a * 0.02)
628 dim_o = dim_c / 3
630 # fmt: off
631 # X Axis
632 coords = [
633 (x_loc, y_loc, z_loc),
634 (x_loc+dim_b, y_loc-dim_o, z_loc),
635 (x_loc+dim_b, y_loc+dim_o, z_loc),
636 (x_loc+dim_a, y_loc, z_loc),
637 (x_loc+dim_b, y_loc+dim_c, z_loc),
638 (x_loc+dim_b, y_loc-dim_c, z_loc),
640 # fmt: on
641 colour = (1.0, 0.0, 0.0, pg.pivot_alpha)
642 draw_3d(coords, "TRIS", colour, context)
643 coords = [(x_loc, y_loc, z_loc), (x_loc+dim_a, y_loc, z_loc)]
644 draw_3d(coords, "LINES", colour, context)
645 # fmt: off
646 # Y Axis
647 coords = [
648 (x_loc, y_loc, z_loc),
649 (x_loc-dim_o, y_loc+dim_b, z_loc),
650 (x_loc+dim_o, y_loc+dim_b, z_loc),
651 (x_loc, y_loc+dim_a, z_loc),
652 (x_loc+dim_c, y_loc+dim_b, z_loc),
653 (x_loc-dim_c, y_loc+dim_b, z_loc),
655 # fmt: on
656 colour = (0.0, 1.0, 0.0, pg.pivot_alpha)
657 draw_3d(coords, "TRIS", colour, context)
658 coords = [(x_loc, y_loc, z_loc), (x_loc, y_loc + dim_a, z_loc)]
659 draw_3d(coords, "LINES", colour, context)
660 # fmt: off
661 # Z Axis
662 coords = [
663 (x_loc, y_loc, z_loc),
664 (x_loc-dim_o, y_loc, z_loc+dim_b),
665 (x_loc+dim_o, y_loc, z_loc+dim_b),
666 (x_loc, y_loc, z_loc+dim_a),
667 (x_loc+dim_c, y_loc, z_loc+dim_b),
668 (x_loc-dim_c, y_loc, z_loc+dim_b),
670 # fmt: on
671 colour = (0.2, 0.5, 1.0, pg.pivot_alpha)
672 draw_3d(coords, "TRIS", colour, context)
673 coords = [(x_loc, y_loc, z_loc), (x_loc, y_loc, z_loc + dim_a)]
674 draw_3d(coords, "LINES", colour, context)
675 # Centre
676 coords = [(x_loc, y_loc, z_loc)]
677 colour = (1.0, 1.0, 0.0, pg.pivot_alpha)
678 draw_3d(coords, "POINTS", colour, context)
681 def scale_set(self, context):
682 """Sets Scale by dividing Pivot Distance by System Distance.
684 Note:
685 Sets Pivot Point Scale Factors by Measurement
686 Uses pg.pivotdis & pg.distance scene variables
688 Args:
689 context: Blender bpy.context instance.
691 Returns:
692 Status Set.
695 scene = context.scene
696 pg = scene.pdt_pg
697 sys_distance = pg.distance
698 scale_distance = pg.pivot_dis
699 if scale_distance > 0:
700 scale_factor = scale_distance / sys_distance
701 pg.pivot_scale = Vector((scale_factor, scale_factor, scale_factor))