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
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 (
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.
32 The printed message will be of the form:
34 {prefix}{caller file name:line number}| {msg}
37 msg: Incoming message to display
44 pdt_debug
= bpy
.context
.preferences
.addons
[__package__
].preferences
.debug
45 if bpy
.app
.debug
or bpy
.app
.debug_python
or pdt_debug
:
48 def extract_filename(fullpath
):
49 """Return only the filename part of fullpath (excluding its path).
52 fullpath: Filename's full path
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]
61 # something went wrong
64 # since this is a string, just return it
67 # stack frame corresponding to the line where debug(msg) was called
68 #print(traceback.extract_stack()[-2])
69 laststack
= traceback
.extract_stack()[-2]
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
):
78 Displays error message in a popup.
81 context: Blender bpy.context instance.
89 self
.layout
.label(text
=pg
.error
)
92 def set_mode(mode_pl
):
93 """Sets Active Axes for View Orientation.
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
103 mode_pl: Plane Selector variable as input
115 return order
[mode_pl
]
118 def set_axis(mode_pl
):
119 """Sets Active Axes for View Orientation.
122 Sets indices for axes from taper vectors
123 Axis order: Rotate Axis, Move Axis, Height Axis
126 mode_pl: Taper Axis Selector variable as input
140 return order
[mode_pl
]
143 def check_selection(num
, bm
, obj
):
144 """Check that the Object's select_history has sufficient entries.
147 If selection history is not Verts, clears selection and history.
150 num: The number of entries required for each operation
151 bm: The Bmesh from the Object
155 list of 3D points as Vectors.
158 if len(bm
.select_history
) < num
:
160 active_vertex
= bm
.select_history
[-1]
161 if isinstance(active_vertex
, bmesh
.types
.BMVert
):
162 vector_a
= active_vertex
.co
166 vector_b
= bm
.select_history
[-2].co
167 return vector_a
, vector_b
169 vector_b
= bm
.select_history
[-2].co
170 vector_c
= bm
.select_history
[-3].co
171 return vector_a
, vector_b
, vector_c
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
184 bmesh
.update_edit_mesh(obj
.data
)
185 bm
.select_history
.clear()
189 def update_sel(bm
, verts
, edges
, faces
):
190 """Updates Vertex, Edge and Face Selections following a function.
194 verts: New Selection for Vertices
195 edges: The Edges on which to operate
196 faces: The Faces on which to operate
215 def view_coords(x_loc
, y_loc
, z_loc
):
216 """Converts input Vector values to new Screen Oriented Vector.
219 x_loc: X coordinate from vector
220 y_loc: Y coordinate from vector
221 z_loc: Z coordinate from vector
224 Vector adjusted to View's Inverted Transformation Matrix.
227 areas
= [a
for a
in bpy
.context
.screen
.areas
if a
.type == "VIEW_3D"]
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.
242 Converts View transformation Matrix to Rotational Matrix
245 x_loc: X coordinate from vector
246 y_loc: Y coordinate from vector
247 z_loc: Z coordinate from vector
250 Vector adjusted to View's Transformation Matrix.
253 areas
= [a
for a
in bpy
.context
.screen
.areas
if a
.type == "VIEW_3D"]
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.
268 Converts View Transformation Matrix to Rotational Matrix (3x3)
269 Angles are Converts to Radians from degrees.
272 dis_v: Scene PDT distance
273 ang_v: Scene PDT angle
279 areas
= [a
for a
in bpy
.context
.screen
.areas
if a
.type == "VIEW_3D"]
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.
296 roll: Roll in Euler rotation
297 pitch: Pitch in Euler rotation
298 yaw: Yaw in Euler rotation
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))
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
321 vector_a: Active vector location
322 vector_b: Second vector location
323 vector_c: Third vector location
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
)
336 line_s
= (line_a
+line_b
+line_c
) / 2
338 line_a
*line_b
*line_c
/4
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
)
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.
358 Calculates Converging Intersect Location and indication of
359 whether the lines are convergent using standard Numpy Routines
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
369 Intersection Vector and Boolean for convergent state.
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
)
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
)
396 return Vector((0, 0, 0)), False
397 new_x_loc
= x_loc
/ z_loc
398 new_z_loc
= y_loc
/ z_loc
402 new_y_loc
= vertex_a
[a3
]
405 vector_delta
= Vector((new_x_loc
, new_y_loc
, new_z_loc
))
407 vector_delta
= Vector((new_x_loc
, new_z_loc
, new_y_loc
))
409 vector_delta
= Vector((new_y_loc
, new_x_loc
, new_z_loc
))
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.
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.
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
439 if obj
.mode
== "EDIT":
440 bm
= bmesh
.from_edit_mesh(obj
.data
)
441 verts
= [v
for v
in bm
.verts
if v
.select
]
443 vector_a
= verts
[0].co
444 vector_b
= verts
[1].co
446 pg
.error
= PDT_ERR_VERT_MODE
447 bpy
.context
.window_manager
.popup_menu(oops
, title
="Error", icon
="ERROR")
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")
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
458 pg
.error
= PDT_ERR_SEL_2_OBJS
+ str(len(objs
)) + ")"
459 bpy
.context
.window_manager
.popup_menu(oops
, title
="Error", icon
="ERROR")
463 objs
[-1].matrix_world
.decompose()[0].x
,
464 objs
[-1].matrix_world
.decompose()[0].y
,
465 objs
[-1].matrix_world
.decompose()[0].z
,
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])
478 if (flip_percent
and data
!= "MV") or data
== "MV":
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.
490 operation: The Operation e.g. Create New Vertex
498 _operation
= operation
.upper()
501 pg
.error
= PDT_ERR_NO_ACT_OBJ
502 bpy
.context
.window_manager
.popup_menu(oops
, title
="Error", icon
="ERROR")
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")
512 if len(bm
.select_history
) >= 1:
514 if _operation
not in {"D", "E", "F", "G", "N", "S"}:
515 vector_a
= check_selection(1, bm
, obj
)
517 verts
= [v
for v
in bm
.verts
if v
.select
]
521 pg
.error
= PDT_ERR_VERT_MODE
522 bpy
.context
.window_manager
.popup_menu(oops
, title
="Error", icon
="ERROR")
528 def dis_ang(values
, flip_angle
, plane
, scene
):
529 """Set Working Axes when using Direction command.
532 values: Input Arguments
533 flip_angle: Whether to flip the angle
538 Directional Offset as a Vector.
542 dis_v
= float(values
[0])
543 ang_v
= float(values
[1])
551 vector_delta
= view_dir(dis_v
, ang_v
)
553 a1
, a2
, _
= set_mode(plane
)
554 vector_delta
= Vector((0, 0, 0))
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))
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.
571 Draws either Lines Points, or Tris using defined shader
574 coords: Input Coordinates List
576 rgba: Colour in RGBA format
577 context: Blender bpy.context instance.
583 batch
= batch_for_shader(SHADER
, gtype
, {"pos": coords
})
586 if coords
is not None:
587 bgl
.glEnable(bgl
.GL_BLEND
)
589 SHADER
.uniform_float("color", rgba
)
592 raise PDT_ShaderError
595 def draw_callback_3d(self
, context
):
596 """Create Coordinate List for Pivot Point Graphic.
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
604 context: Blender bpy.context instance.
610 scene
= context
.scene
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
617 areas
= [a
for a
in context
.screen
.areas
if a
.type == "VIEW_3D"]
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
624 # dim_a = region_width / sf / 5000 * pg.pivot_size
625 dim_a
= region_width
/ scale_factor
/ 50000 * pg
.pivot_size
627 dim_c
= dim_a
* 0.05 + (pg
.pivot_width
* dim_a
* 0.02)
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
),
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
)
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
),
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
)
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
),
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
)
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.
685 Sets Pivot Point Scale Factors by Measurement
686 Uses pg.pivotdis & pg.distance scene variables
689 context: Blender bpy.context instance.
695 scene
= context
.scene
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
))