Merge branch 'blender-v4.0-release'
[blender-addons.git] / precision_drawing_tools / pdt_functions.py
blob1da112ff7f78c55056044c4f8cc24917a69f9aaf
1 # SPDX-FileCopyrightText: 2019-2022 Alan Odom (Clockmender)
2 # SPDX-FileCopyrightText: 2019-2022 Rune Morling (ermo)
4 # SPDX-License-Identifier: GPL-2.0-or-later
6 # Common Functions used in more than one place in PDT Operations
8 import bpy
9 import bmesh
10 import gpu
11 import numpy as np
12 from mathutils import Vector, Quaternion
13 from gpu_extras.batch import batch_for_shader
14 from math import cos, sin, pi
15 from .pdt_msg_strings import (
16 PDT_ERR_VERT_MODE,
17 PDT_ERR_SEL_2_V_1_E,
18 PDT_ERR_SEL_2_OBJS,
19 PDT_ERR_NO_ACT_OBJ,
20 PDT_ERR_SEL_1_EDGEM,
22 from . import pdt_exception
23 PDT_ShaderError = pdt_exception.ShaderError
26 def debug(msg, prefix=""):
27 """Print a debug message to the console if PDT's or Blender's debug flags are set.
29 Note:
30 The printed message will be of the form:
32 {prefix}{caller file name:line number}| {msg}
34 Args:
35 msg: Incoming message to display
36 prefix: Always Blank
38 Returns:
39 Nothing.
40 """
42 pdt_debug = bpy.context.preferences.addons[__package__].preferences.debug
43 if bpy.app.debug or bpy.app.debug_python or pdt_debug:
44 import traceback
46 def extract_filename(fullpath):
47 """Return only the filename part of fullpath (excluding its path).
49 Args:
50 fullpath: Filename's full path
52 Returns:
53 filename.
54 """
55 # Expected to end up being a string containing only the filename
56 # (i.e. excluding its preceding '/' separated path)
57 filename = fullpath.split('/')[-1]
58 #print(filename)
59 # something went wrong
60 if len(filename) < 1:
61 return fullpath
62 # since this is a string, just return it
63 return filename
65 # stack frame corresponding to the line where debug(msg) was called
66 #print(traceback.extract_stack()[-2])
67 laststack = traceback.extract_stack()[-2]
68 #print(laststack[0])
69 # laststack[0] is the caller's full file name, laststack[1] is the line number
70 print(f"{prefix}{extract_filename(laststack[0])}:{laststack[1]}| {msg}")
72 def oops(self, context):
73 """Error Routine.
75 Note:
76 Displays error message in a popup.
78 Args:
79 context: Blender bpy.context instance.
81 Returns:
82 Nothing.
83 """
85 scene = context.scene
86 pg = scene.pdt_pg
87 self.layout.label(text=pg.error)
90 def set_mode(mode_pl):
91 """Sets Active Axes for View Orientation.
93 Note:
94 Sets indices of axes for locational vectors:
95 a3 is normal to screen, or depth
96 "XY": a1 = x, a2 = y, a3 = z
97 "XZ": a1 = x, a2 = z, a3 = y
98 "YZ": a1 = y, a2 = z, a3 = x
100 Args:
101 mode_pl: Plane Selector variable as input
103 Returns:
104 3 Integer indices.
107 order = {
108 "XY": (0, 1, 2),
109 "XZ": (0, 2, 1),
110 "YZ": (1, 2, 0),
111 "LO": (0, 1, 2),
113 return order[mode_pl]
116 def set_axis(mode_pl):
117 """Sets Active Axes for View Orientation.
119 Note:
120 Sets indices for axes from taper vectors
121 Axis order: Rotate Axis, Move Axis, Height Axis
123 Args:
124 mode_pl: Taper Axis Selector variable as input
126 Returns:
127 3 Integer Indices.
130 order = {
131 "RX-MY": (0, 1, 2),
132 "RX-MZ": (0, 2, 1),
133 "RY-MX": (1, 0, 2),
134 "RY-MZ": (1, 2, 0),
135 "RZ-MX": (2, 0, 1),
136 "RZ-MY": (2, 1, 0),
138 return order[mode_pl]
141 def check_selection(num, bm, obj):
142 """Check that the Object's select_history has sufficient entries.
144 Note:
145 If selection history is not Verts, clears selection and history.
147 Args:
148 num: The number of entries required for each operation
149 bm: The Bmesh from the Object
150 obj: The Object
152 Returns:
153 list of 3D points as Vectors.
156 if len(bm.select_history) < num:
157 return None
158 active_vertex = bm.select_history[-1]
159 if isinstance(active_vertex, bmesh.types.BMVert):
160 vector_a = active_vertex.co
161 if num == 1:
162 return vector_a
163 if num == 2:
164 vector_b = bm.select_history[-2].co
165 return vector_a, vector_b
166 if num == 3:
167 vector_b = bm.select_history[-2].co
168 vector_c = bm.select_history[-3].co
169 return vector_a, vector_b, vector_c
170 if num == 4:
171 vector_b = bm.select_history[-2].co
172 vector_c = bm.select_history[-3].co
173 vector_d = bm.select_history[-4].co
174 return vector_a, vector_b, vector_c, vector_d
175 else:
176 for f in bm.faces:
177 f.select_set(False)
178 for e in bm.edges:
179 e.select_set(False)
180 for v in bm.verts:
181 v.select_set(False)
182 bmesh.update_edit_mesh(obj.data)
183 bm.select_history.clear()
184 return None
187 def update_sel(bm, verts, edges, faces):
188 """Updates Vertex, Edge and Face Selections following a function.
190 Args:
191 bm: Object Bmesh
192 verts: New Selection for Vertices
193 edges: The Edges on which to operate
194 faces: The Faces on which to operate
196 Returns:
197 Nothing.
199 for f in bm.faces:
200 f.select_set(False)
201 for e in bm.edges:
202 e.select_set(False)
203 for v in bm.verts:
204 v.select_set(False)
205 for v in verts:
206 v.select_set(True)
207 for e in edges:
208 e.select_set(True)
209 for f in faces:
210 f.select_set(True)
213 def view_coords(x_loc, y_loc, z_loc):
214 """Converts input Vector values to new Screen Oriented Vector.
216 Args:
217 x_loc: X coordinate from vector
218 y_loc: Y coordinate from vector
219 z_loc: Z coordinate from vector
221 Returns:
222 Vector adjusted to View's Inverted Transformation Matrix.
225 areas = [a for a in bpy.context.screen.areas if a.type == "VIEW_3D"]
226 if len(areas) > 0:
227 view_matrix = areas[0].spaces.active.region_3d.view_matrix
228 view_matrix = view_matrix.to_3x3().normalized().inverted()
229 view_location = Vector((x_loc, y_loc, z_loc))
230 new_view_location = view_matrix @ view_location
231 return new_view_location
233 return Vector((0, 0, 0))
236 def view_coords_i(x_loc, y_loc, z_loc):
237 """Converts Screen Oriented input Vector values to new World Vector.
239 Note:
240 Converts View transformation Matrix to Rotational Matrix
242 Args:
243 x_loc: X coordinate from vector
244 y_loc: Y coordinate from vector
245 z_loc: Z coordinate from vector
247 Returns:
248 Vector adjusted to View's Transformation Matrix.
251 areas = [a for a in bpy.context.screen.areas if a.type == "VIEW_3D"]
252 if len(areas) > 0:
253 view_matrix = areas[0].spaces.active.region_3d.view_matrix
254 view_matrix = view_matrix.to_3x3().normalized()
255 view_location = Vector((x_loc, y_loc, z_loc))
256 new_view_location = view_matrix @ view_location
257 return new_view_location
259 return Vector((0, 0, 0))
262 def view_dir(dis_v, ang_v):
263 """Converts Distance and Angle to View Oriented Vector.
265 Note:
266 Converts View Transformation Matrix to Rotational Matrix (3x3)
267 Angles are Converts to Radians from degrees.
269 Args:
270 dis_v: Scene PDT distance
271 ang_v: Scene PDT angle
273 Returns:
274 World Vector.
277 areas = [a for a in bpy.context.screen.areas if a.type == "VIEW_3D"]
278 if len(areas) > 0:
279 view_matrix = areas[0].spaces.active.region_3d.view_matrix
280 view_matrix = view_matrix.to_3x3().normalized().inverted()
281 view_location = Vector((0, 0, 0))
282 view_location.x = dis_v * cos(ang_v * pi / 180)
283 view_location.y = dis_v * sin(ang_v * pi / 180)
284 new_view_location = view_matrix @ view_location
285 return new_view_location
287 return Vector((0, 0, 0))
290 def euler_to_quaternion(roll, pitch, yaw):
291 """Converts Euler Rotation to Quaternion Rotation.
293 Args:
294 roll: Roll in Euler rotation
295 pitch: Pitch in Euler rotation
296 yaw: Yaw in Euler rotation
298 Returns:
299 Quaternion Rotation.
302 # fmt: off
303 quat_x = (np.sin(roll/2) * np.cos(pitch/2) * np.cos(yaw/2)
304 - np.cos(roll/2) * np.sin(pitch/2) * np.sin(yaw/2))
305 quat_y = (np.cos(roll/2) * np.sin(pitch/2) * np.cos(yaw/2)
306 + np.sin(roll/2) * np.cos(pitch/2) * np.sin(yaw/2))
307 quat_z = (np.cos(roll/2) * np.cos(pitch/2) * np.sin(yaw/2)
308 - np.sin(roll/2) * np.sin(pitch/2) * np.cos(yaw/2))
309 quat_w = (np.cos(roll/2) * np.cos(pitch/2) * np.cos(yaw/2)
310 + np.sin(roll/2) * np.sin(pitch/2) * np.sin(yaw/2))
311 # fmt: on
312 return Quaternion((quat_w, quat_x, quat_y, quat_z))
315 def arc_centre(vector_a, vector_b, vector_c):
316 """Calculates Centre of Arc from 3 Vector Locations using standard Numpy routine
318 Args:
319 vector_a: Active vector location
320 vector_b: Second vector location
321 vector_c: Third vector location
323 Returns:
324 Vector representing Arc Centre and Float representing Arc Radius.
327 coord_a = np.array([vector_a.x, vector_a.y, vector_a.z])
328 coord_b = np.array([vector_b.x, vector_b.y, vector_b.z])
329 coord_c = np.array([vector_c.x, vector_c.y, vector_c.z])
330 line_a = np.linalg.norm(coord_c - coord_b)
331 line_b = np.linalg.norm(coord_c - coord_a)
332 line_c = np.linalg.norm(coord_b - coord_a)
333 # fmt: off
334 line_s = (line_a+line_b+line_c) / 2
335 radius = (
336 line_a*line_b*line_c/4
337 / np.sqrt(line_s
338 * (line_s-line_a)
339 * (line_s-line_b)
340 * (line_s-line_c))
342 base_1 = line_a*line_a * (line_b*line_b + line_c*line_c - line_a*line_a)
343 base_2 = line_b*line_b * (line_a*line_a + line_c*line_c - line_b*line_b)
344 base_3 = line_c*line_c * (line_a*line_a + line_b*line_b - line_c*line_c)
345 # fmt: on
346 intersect_coord = np.column_stack((coord_a, coord_b, coord_c))
347 intersect_coord = intersect_coord.dot(np.hstack((base_1, base_2, base_3)))
348 intersect_coord /= base_1 + base_2 + base_3
349 return Vector((intersect_coord[0], intersect_coord[1], intersect_coord[2])), radius
352 def intersection(vertex_a, vertex_b, vertex_c, vertex_d, plane):
353 """Calculates Intersection Point of 2 Imagined Lines from 4 Vectors.
355 Note:
356 Calculates Converging Intersect Location and indication of
357 whether the lines are convergent using standard Numpy Routines
359 Args:
360 vertex_a: Active vector location of first line
361 vertex_b: Second vector location of first line
362 vertex_c: Third vector location of 2nd line
363 vertex_d: Fourth vector location of 2nd line
364 plane: Working Plane 4 Vector Locations representing 2 lines and Working Plane
366 Returns:
367 Intersection Vector and Boolean for convergent state.
370 if plane == "LO":
371 vertex_offset = vertex_b - vertex_a
372 vertex_b = view_coords_i(vertex_offset.x, vertex_offset.y, vertex_offset.z)
373 vertex_offset = vertex_d - vertex_a
374 vertex_d = view_coords_i(vertex_offset.x, vertex_offset.y, vertex_offset.z)
375 vertex_offset = vertex_c - vertex_a
376 vertex_c = view_coords_i(vertex_offset.x, vertex_offset.y, vertex_offset.z)
377 vector_ref = Vector((0, 0, 0))
378 coord_a = (vertex_c.x, vertex_c.y)
379 coord_b = (vertex_d.x, vertex_d.y)
380 coord_c = (vertex_b.x, vertex_b.y)
381 coord_d = (vector_ref.x, vector_ref.y)
382 else:
383 a1, a2, a3 = set_mode(plane)
384 coord_a = (vertex_c[a1], vertex_c[a2])
385 coord_b = (vertex_d[a1], vertex_d[a2])
386 coord_c = (vertex_a[a1], vertex_a[a2])
387 coord_d = (vertex_b[a1], vertex_b[a2])
388 v_stack = np.vstack([coord_a, coord_b, coord_c, coord_d])
389 h_stack = np.hstack((v_stack, np.ones((4, 1))))
390 line_a = np.cross(h_stack[0], h_stack[1])
391 line_b = np.cross(h_stack[2], h_stack[3])
392 x_loc, y_loc, z_loc = np.cross(line_a, line_b)
393 if z_loc == 0:
394 return Vector((0, 0, 0)), False
395 new_x_loc = x_loc / z_loc
396 new_z_loc = y_loc / z_loc
397 if plane == "LO":
398 new_y_loc = 0
399 else:
400 new_y_loc = vertex_a[a3]
401 # Order Vector Delta
402 if plane == "XZ":
403 vector_delta = Vector((new_x_loc, new_y_loc, new_z_loc))
404 elif plane == "XY":
405 vector_delta = Vector((new_x_loc, new_z_loc, new_y_loc))
406 elif plane == "YZ":
407 vector_delta = Vector((new_y_loc, new_x_loc, new_z_loc))
408 else:
409 # Must be Local View Plane
410 vector_delta = view_coords(new_x_loc, new_z_loc, new_y_loc) + vertex_a
411 return vector_delta, True
414 def get_percent(obj, flip_percent, per_v, data, scene):
415 """Calculates a Percentage Distance between 2 Vectors.
417 Note:
418 Calculates a point that lies a set percentage between two given points
419 using standard Numpy Routines.
421 Works for either 2 vertices for an object in Edit mode
422 or 2 selected objects in Object mode.
424 Args:
425 obj: The Object under consideration
426 flip_percent: Setting this to True measures the percentage starting from the second vector
427 per_v: Percentage Input Value
428 data: pg.flip, pg.percent scene variables & Operational Mode
429 scene: Context Scene
431 Returns:
432 World Vector.
435 pg = scene.pdt_pg
437 if obj.mode == "EDIT":
438 bm = bmesh.from_edit_mesh(obj.data)
439 verts = [v for v in bm.verts if v.select]
440 if len(verts) == 2:
441 vector_a = verts[0].co
442 vector_b = verts[1].co
443 if vector_a is None:
444 pg.error = PDT_ERR_VERT_MODE
445 bpy.context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
446 return None
447 else:
448 pg.error = PDT_ERR_SEL_2_V_1_E + str(len(verts)) + " Vertices"
449 bpy.context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
450 return None
451 coord_a = np.array([vector_a.x, vector_a.y, vector_a.z])
452 coord_b = np.array([vector_b.x, vector_b.y, vector_b.z])
453 if obj.mode == "OBJECT":
454 objs = bpy.context.view_layer.objects.selected
455 if len(objs) != 2:
456 pg.error = PDT_ERR_SEL_2_OBJS + str(len(objs)) + ")"
457 bpy.context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
458 return None
459 coord_a = np.array(
461 objs[-1].matrix_world.decompose()[0].x,
462 objs[-1].matrix_world.decompose()[0].y,
463 objs[-1].matrix_world.decompose()[0].z,
466 coord_b = np.array(
468 objs[-2].matrix_world.decompose()[0].x,
469 objs[-2].matrix_world.decompose()[0].y,
470 objs[-2].matrix_world.decompose()[0].z,
473 coord_c = coord_b - coord_a
474 coord_d = np.array([0, 0, 0])
475 _per_v = per_v
476 if (flip_percent and data != "MV") or data == "MV":
477 _per_v = 100 - per_v
478 coord_out = (coord_d+coord_c) * (_per_v / 100) + coord_a
479 return Vector((coord_out[0], coord_out[1], coord_out[2]))
482 def obj_check(obj, scene, operation):
483 """Check Object & Selection Validity.
485 Args:
486 obj: Active Object
487 scene: Active Scene
488 operation: The Operation e.g. Create New Vertex
490 Returns:
491 Object Bmesh
492 Validity Boolean.
495 pg = scene.pdt_pg
496 _operation = operation.upper()
498 if obj is None:
499 pg.error = PDT_ERR_NO_ACT_OBJ
500 bpy.context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
501 return None, False
502 if obj.mode == "EDIT":
503 bm = bmesh.from_edit_mesh(obj.data)
504 if _operation == "S":
505 if len(bm.edges) < 1:
506 pg.error = f"{PDT_ERR_SEL_1_EDGEM} {len(bm.edges)})"
507 bpy.context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
508 return None, False
509 return bm, True
510 if len(bm.select_history) >= 1:
511 vector_a = None
512 if _operation not in {"D", "E", "F", "G", "N", "S"}:
513 vector_a = check_selection(1, bm, obj)
514 else:
515 verts = [v for v in bm.verts if v.select]
516 if len(verts) > 0:
517 vector_a = verts[0]
518 if vector_a is None:
519 pg.error = PDT_ERR_VERT_MODE
520 bpy.context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
521 return None, False
522 return bm, True
523 return None, True
526 def dis_ang(values, flip_angle, plane, scene):
527 """Set Working Axes when using Direction command.
529 Args:
530 values: Input Arguments
531 flip_angle: Whether to flip the angle
532 plane: Working Plane
533 scene: Current Scene
535 Returns:
536 Directional Offset as a Vector.
539 pg = scene.pdt_pg
540 dis_v = float(values[0])
541 ang_v = float(values[1])
542 if flip_angle:
543 if ang_v > 0:
544 ang_v = ang_v - 180
545 else:
546 ang_v = ang_v + 180
547 pg.angle = ang_v
548 if plane == "LO":
549 vector_delta = view_dir(dis_v, ang_v)
550 else:
551 a1, a2, _ = set_mode(plane)
552 vector_delta = Vector((0, 0, 0))
553 # fmt: off
554 vector_delta[a1] = vector_delta[a1] + (dis_v * cos(ang_v * pi/180))
555 vector_delta[a2] = vector_delta[a2] + (dis_v * sin(ang_v * pi/180))
556 # fmt: on
557 return vector_delta
560 # Shader for displaying the Pivot Point as Graphics.
562 SHADER = gpu.shader.from_builtin("UNIFORM_COLOR") if not bpy.app.background else None
565 def draw_3d(coords, gtype, rgba, context):
566 """Draw Pivot Point Graphics.
568 Note:
569 Draws either Lines Points, or Tris using defined shader
571 Args:
572 coords: Input Coordinates List
573 gtype: Graphic Type
574 rgba: Colour in RGBA format
575 context: Blender bpy.context instance.
577 Returns:
578 Nothing.
581 batch = batch_for_shader(SHADER, gtype, {"pos": coords})
583 try:
584 if coords is not None:
585 gpu.state.blend_set('ALPHA')
586 SHADER.bind()
587 SHADER.uniform_float("color", rgba)
588 batch.draw(SHADER)
589 except:
590 raise PDT_ShaderError
593 def draw_callback_3d(self, context):
594 """Create Coordinate List for Pivot Point Graphic.
596 Note:
597 Creates coordinates for Pivot Point Graphic consisting of 6 Tris
598 and one Point colour coded Red; X axis, Green; Y axis, Blue; Z axis
599 and a yellow point based upon screen scale
601 Args:
602 context: Blender bpy.context instance.
604 Returns:
605 Nothing.
608 scene = context.scene
609 pg = scene.pdt_pg
610 region_width = context.region.width
611 x_loc = pg.pivot_loc.x
612 y_loc = pg.pivot_loc.y
613 z_loc = pg.pivot_loc.z
614 # Scale it from view
615 areas = [a for a in context.screen.areas if a.type == "VIEW_3D"]
616 if len(areas) > 0:
617 scale_factor = abs(areas[0].spaces.active.region_3d.window_matrix.decompose()[2][1])
618 # Check for orhtographic view and resize
619 #if areas[0].spaces.active.region_3d.is_orthographic_side_view:
620 # dim_a = region_width / sf / 60000 * pg.pivot_size
621 #else:
622 # dim_a = region_width / sf / 5000 * pg.pivot_size
623 dim_a = region_width / scale_factor / 50000 * pg.pivot_size
624 dim_b = dim_a * 0.65
625 dim_c = dim_a * 0.05 + (pg.pivot_width * dim_a * 0.02)
626 dim_o = dim_c / 3
628 # fmt: off
629 # X Axis
630 coords = [
631 (x_loc, y_loc, z_loc),
632 (x_loc+dim_b, y_loc-dim_o, z_loc),
633 (x_loc+dim_b, y_loc+dim_o, z_loc),
634 (x_loc+dim_a, y_loc, z_loc),
635 (x_loc+dim_b, y_loc+dim_c, z_loc),
636 (x_loc+dim_b, y_loc-dim_c, z_loc),
638 # fmt: on
639 colour = (1.0, 0.0, 0.0, pg.pivot_alpha)
640 draw_3d(coords, "TRIS", colour, context)
641 coords = [(x_loc, y_loc, z_loc), (x_loc+dim_a, y_loc, z_loc)]
642 draw_3d(coords, "LINES", colour, context)
643 # fmt: off
644 # Y Axis
645 coords = [
646 (x_loc, y_loc, z_loc),
647 (x_loc-dim_o, y_loc+dim_b, z_loc),
648 (x_loc+dim_o, y_loc+dim_b, z_loc),
649 (x_loc, y_loc+dim_a, z_loc),
650 (x_loc+dim_c, y_loc+dim_b, z_loc),
651 (x_loc-dim_c, y_loc+dim_b, z_loc),
653 # fmt: on
654 colour = (0.0, 1.0, 0.0, pg.pivot_alpha)
655 draw_3d(coords, "TRIS", colour, context)
656 coords = [(x_loc, y_loc, z_loc), (x_loc, y_loc + dim_a, z_loc)]
657 draw_3d(coords, "LINES", colour, context)
658 # fmt: off
659 # Z Axis
660 coords = [
661 (x_loc, y_loc, z_loc),
662 (x_loc-dim_o, y_loc, z_loc+dim_b),
663 (x_loc+dim_o, y_loc, z_loc+dim_b),
664 (x_loc, y_loc, z_loc+dim_a),
665 (x_loc+dim_c, y_loc, z_loc+dim_b),
666 (x_loc-dim_c, y_loc, z_loc+dim_b),
668 # fmt: on
669 colour = (0.2, 0.5, 1.0, pg.pivot_alpha)
670 draw_3d(coords, "TRIS", colour, context)
671 coords = [(x_loc, y_loc, z_loc), (x_loc, y_loc, z_loc + dim_a)]
672 draw_3d(coords, "LINES", colour, context)
673 # Centre
674 coords = [(x_loc, y_loc, z_loc)]
675 colour = (1.0, 1.0, 0.0, pg.pivot_alpha)
676 draw_3d(coords, "POINTS", colour, context)
679 def scale_set(self, context):
680 """Sets Scale by dividing Pivot Distance by System Distance.
682 Note:
683 Sets Pivot Point Scale Factors by Measurement
684 Uses pg.pivotdis & pg.distance scene variables
686 Args:
687 context: Blender bpy.context instance.
689 Returns:
690 Status Set.
693 scene = context.scene
694 pg = scene.pdt_pg
695 sys_distance = pg.distance
696 scale_distance = pg.pivot_dis
697 if scale_distance > 0:
698 scale_factor = scale_distance / sys_distance
699 pg.pivot_scale = Vector((scale_factor, scale_factor, scale_factor))