1 # ***** BEGIN GPL LICENSE BLOCK *****
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # ***** END GPL LICENCE BLOCK *****
20 # -----------------------------------------------------------------------
21 # Author: Alan Odom (Clockmender), Rune Morling (ermo) Copyright (c) 2019
22 # -----------------------------------------------------------------------
24 # Common Functions used in more than one place in PDT Operations
31 from mathutils
import Vector
, Quaternion
32 from gpu_extras
.batch
import batch_for_shader
33 from math
import cos
, sin
, pi
34 from .pdt_msg_strings
import (
41 from . import pdt_exception
42 PDT_ShaderError
= pdt_exception
.ShaderError
45 def debug(msg
, prefix
=""):
46 """Print a debug message to the console if PDT's or Blender's debug flags are set.
49 The printed message will be of the form:
51 {prefix}{caller file name:line number}| {msg}
54 msg: Incomming message to display
61 pdt_debug
= bpy
.context
.preferences
.addons
[__package__
].preferences
.debug
62 if bpy
.app
.debug
or bpy
.app
.debug_python
or pdt_debug
:
65 def extract_filename(fullpath
):
66 """Return only the filename part of fullpath (excluding its path).
69 fullpath: Filename's full path
74 # Expected to end up being a string containing only the filename
75 # (i.e. excluding its preceding '/' separated path)
76 filename
= fullpath
.split('/')[-1]
78 # something went wrong
81 # since this is a string, just return it
84 # stack frame corresponding to the line where debug(msg) was called
85 #print(traceback.extract_stack()[-2])
86 laststack
= traceback
.extract_stack()[-2]
88 # laststack[0] is the caller's full file name, laststack[1] is the line number
89 print(f
"{prefix}{extract_filename(laststack[0])}:{laststack[1]}| {msg}")
91 def oops(self
, context
):
95 Displays error message in a popup.
98 context: Blender bpy.context instance.
104 scene
= context
.scene
106 self
.layout
.label(text
=pg
.error
)
109 def set_mode(mode_pl
):
110 """Sets Active Axes for View Orientation.
113 Sets indices of axes for locational vectors:
114 a3 is normal to screen, or depth
115 "XY": a1 = x, a2 = y, a3 = z
116 "XZ": a1 = x, a2 = z, a3 = y
117 "YZ": a1 = y, a2 = z, a3 = x
120 mode_pl: Plane Selector variable as input
132 return order
[mode_pl
]
135 def set_axis(mode_pl
):
136 """Sets Active Axes for View Orientation.
139 Sets indices for axes from taper vectors
140 Axis order: Rotate Axis, Move Axis, Height Axis
143 mode_pl: Taper Axis Selector variable as input
157 return order
[mode_pl
]
160 def check_selection(num
, bm
, obj
):
161 """Check that the Object's select_history has sufficient entries.
164 If selection history is not Verts, clears selection and history.
167 num: The number of entries required for each operation
168 bm: The Bmesh from the Object
172 list of 3D points as Vectors.
175 if len(bm
.select_history
) < num
:
177 active_vertex
= bm
.select_history
[-1]
178 if isinstance(active_vertex
, bmesh
.types
.BMVert
):
179 vector_a
= active_vertex
.co
183 vector_b
= bm
.select_history
[-2].co
184 return vector_a
, vector_b
186 vector_b
= bm
.select_history
[-2].co
187 vector_c
= bm
.select_history
[-3].co
188 return vector_a
, vector_b
, vector_c
190 vector_b
= bm
.select_history
[-2].co
191 vector_c
= bm
.select_history
[-3].co
192 vector_d
= bm
.select_history
[-4].co
193 return vector_a
, vector_b
, vector_c
, vector_d
201 bmesh
.update_edit_mesh(obj
.data
)
202 bm
.select_history
.clear()
206 def update_sel(bm
, verts
, edges
, faces
):
207 """Updates Vertex, Edge and Face Selections following a function.
211 verts: New Selection for Vertices
212 edges: The Edges on which to operate
213 faces: The Faces on which to operate
232 def view_coords(x_loc
, y_loc
, z_loc
):
233 """Converts input Vector values to new Screen Oriented Vector.
236 x_loc: X coordinate from vector
237 y_loc: Y coordinate from vector
238 z_loc: Z coordinate from vector
241 Vector adjusted to View's Inverted Tranformation Matrix.
244 areas
= [a
for a
in bpy
.context
.screen
.areas
if a
.type == "VIEW_3D"]
246 view_matrix
= areas
[0].spaces
.active
.region_3d
.view_matrix
247 view_matrix
= view_matrix
.to_3x3().normalized().inverted()
248 view_location
= Vector((x_loc
, y_loc
, z_loc
))
249 new_view_location
= view_matrix
@ view_location
250 return new_view_location
252 return Vector((0, 0, 0))
255 def view_coords_i(x_loc
, y_loc
, z_loc
):
256 """Converts Screen Oriented input Vector values to new World Vector.
259 Converts View tranformation Matrix to Rotational Matrix
262 x_loc: X coordinate from vector
263 y_loc: Y coordinate from vector
264 z_loc: Z coordinate from vector
267 Vector adjusted to View's Transformation Matrix.
270 areas
= [a
for a
in bpy
.context
.screen
.areas
if a
.type == "VIEW_3D"]
272 view_matrix
= areas
[0].spaces
.active
.region_3d
.view_matrix
273 view_matrix
= view_matrix
.to_3x3().normalized()
274 view_location
= Vector((x_loc
, y_loc
, z_loc
))
275 new_view_location
= view_matrix
@ view_location
276 return new_view_location
278 return Vector((0, 0, 0))
281 def view_dir(dis_v
, ang_v
):
282 """Converts Distance and Angle to View Oriented Vector.
285 Converts View Transformation Matrix to Rotational Matrix (3x3)
286 Angles are Converts to Radians from degrees.
289 dis_v: Scene PDT distance
290 ang_v: Scene PDT angle
296 areas
= [a
for a
in bpy
.context
.screen
.areas
if a
.type == "VIEW_3D"]
298 view_matrix
= areas
[0].spaces
.active
.region_3d
.view_matrix
299 view_matrix
= view_matrix
.to_3x3().normalized().inverted()
300 view_location
= Vector((0, 0, 0))
301 view_location
.x
= dis_v
* cos(ang_v
* pi
/ 180)
302 view_location
.y
= dis_v
* sin(ang_v
* pi
/ 180)
303 new_view_location
= view_matrix
@ view_location
304 return new_view_location
306 return Vector((0, 0, 0))
309 def euler_to_quaternion(roll
, pitch
, yaw
):
310 """Converts Euler Rotation to Quaternion Rotation.
313 roll: Roll in Euler rotation
314 pitch: Pitch in Euler rotation
315 yaw: Yaw in Euler rotation
322 quat_x
= (np
.sin(roll
/2) * np
.cos(pitch
/2) * np
.cos(yaw
/2)
323 - np
.cos(roll
/2) * np
.sin(pitch
/2) * np
.sin(yaw
/2))
324 quat_y
= (np
.cos(roll
/2) * np
.sin(pitch
/2) * np
.cos(yaw
/2)
325 + np
.sin(roll
/2) * np
.cos(pitch
/2) * np
.sin(yaw
/2))
326 quat_z
= (np
.cos(roll
/2) * np
.cos(pitch
/2) * np
.sin(yaw
/2)
327 - np
.sin(roll
/2) * np
.sin(pitch
/2) * np
.cos(yaw
/2))
328 quat_w
= (np
.cos(roll
/2) * np
.cos(pitch
/2) * np
.cos(yaw
/2)
329 + np
.sin(roll
/2) * np
.sin(pitch
/2) * np
.sin(yaw
/2))
331 return Quaternion((quat_w
, quat_x
, quat_y
, quat_z
))
334 def arc_centre(vector_a
, vector_b
, vector_c
):
335 """Calculates Centre of Arc from 3 Vector Locations using standard Numpy routine
338 vector_a: Active vector location
339 vector_b: Second vector location
340 vector_c: Third vector location
343 Vector representing Arc Centre and Float representing Arc Radius.
346 coord_a
= np
.array([vector_a
.x
, vector_a
.y
, vector_a
.z
])
347 coord_b
= np
.array([vector_b
.x
, vector_b
.y
, vector_b
.z
])
348 coord_c
= np
.array([vector_c
.x
, vector_c
.y
, vector_c
.z
])
349 line_a
= np
.linalg
.norm(coord_c
- coord_b
)
350 line_b
= np
.linalg
.norm(coord_c
- coord_a
)
351 line_c
= np
.linalg
.norm(coord_b
- coord_a
)
353 line_s
= (line_a
+line_b
+line_c
) / 2
355 line_a
*line_b
*line_c
/4
361 base_1
= line_a
*line_a
* (line_b
*line_b
+ line_c
*line_c
- line_a
*line_a
)
362 base_2
= line_b
*line_b
* (line_a
*line_a
+ line_c
*line_c
- line_b
*line_b
)
363 base_3
= line_c
*line_c
* (line_a
*line_a
+ line_b
*line_b
- line_c
*line_c
)
365 intersect_coord
= np
.column_stack((coord_a
, coord_b
, coord_c
))
366 intersect_coord
= intersect_coord
.dot(np
.hstack((base_1
, base_2
, base_3
)))
367 intersect_coord
/= base_1
+ base_2
+ base_3
368 return Vector((intersect_coord
[0], intersect_coord
[1], intersect_coord
[2])), radius
371 def intersection(vertex_a
, vertex_b
, vertex_c
, vertex_d
, plane
):
372 """Calculates Intersection Point of 2 Imagined Lines from 4 Vectors.
375 Calculates Converging Intersect Location and indication of
376 whether the lines are convergent using standard Numpy Routines
379 vertex_a: Active vector location of first line
380 vertex_b: Second vector location of first line
381 vertex_c: Third vector location of 2nd line
382 vertex_d: Fourth vector location of 2nd line
383 plane: Working Plane 4 Vector Locations representing 2 lines and Working Plane
386 Intersection Vector and Boolean for convergent state.
390 vertex_offset
= vertex_b
- vertex_a
391 vertex_b
= view_coords_i(vertex_offset
.x
, vertex_offset
.y
, vertex_offset
.z
)
392 vertex_offset
= vertex_d
- vertex_a
393 vertex_d
= view_coords_i(vertex_offset
.x
, vertex_offset
.y
, vertex_offset
.z
)
394 vertex_offset
= vertex_c
- vertex_a
395 vertex_c
= view_coords_i(vertex_offset
.x
, vertex_offset
.y
, vertex_offset
.z
)
396 vector_ref
= Vector((0, 0, 0))
397 coord_a
= (vertex_c
.x
, vertex_c
.y
)
398 coord_b
= (vertex_d
.x
, vertex_d
.y
)
399 coord_c
= (vertex_b
.x
, vertex_b
.y
)
400 coord_d
= (vector_ref
.x
, vector_ref
.y
)
402 a1
, a2
, a3
= set_mode(plane
)
403 coord_a
= (vertex_c
[a1
], vertex_c
[a2
])
404 coord_b
= (vertex_d
[a1
], vertex_d
[a2
])
405 coord_c
= (vertex_a
[a1
], vertex_a
[a2
])
406 coord_d
= (vertex_b
[a1
], vertex_b
[a2
])
407 v_stack
= np
.vstack([coord_a
, coord_b
, coord_c
, coord_d
])
408 h_stack
= np
.hstack((v_stack
, np
.ones((4, 1))))
409 line_a
= np
.cross(h_stack
[0], h_stack
[1])
410 line_b
= np
.cross(h_stack
[2], h_stack
[3])
411 x_loc
, y_loc
, z_loc
= np
.cross(line_a
, line_b
)
413 return Vector((0, 0, 0)), False
414 new_x_loc
= x_loc
/ z_loc
415 new_z_loc
= y_loc
/ z_loc
419 new_y_loc
= vertex_a
[a3
]
422 vector_delta
= Vector((new_x_loc
, new_y_loc
, new_z_loc
))
424 vector_delta
= Vector((new_x_loc
, new_z_loc
, new_y_loc
))
426 vector_delta
= Vector((new_y_loc
, new_x_loc
, new_z_loc
))
428 # Must be Local View Plane
429 vector_delta
= view_coords(new_x_loc
, new_z_loc
, new_y_loc
) + vertex_a
430 return vector_delta
, True
433 def get_percent(obj
, flip_percent
, per_v
, data
, scene
):
434 """Calculates a Percentage Distance between 2 Vectors.
437 Calculates a point that lies a set percentage between two given points
438 using standard Numpy Routines.
440 Works for either 2 vertices for an object in Edit mode
441 or 2 selected objects in Object mode.
444 obj: The Object under consideration
445 flip_percent: Setting this to True measures the percentage starting from the second vector
446 per_v: Percentage Input Value
447 data: pg.flip, pg.percent scene variables & Operational Mode
456 if obj
.mode
== "EDIT":
457 bm
= bmesh
.from_edit_mesh(obj
.data
)
458 verts
= [v
for v
in bm
.verts
if v
.select
]
460 vector_a
= verts
[0].co
461 vector_b
= verts
[1].co
463 pg
.error
= PDT_ERR_VERT_MODE
464 bpy
.context
.window_manager
.popup_menu(oops
, title
="Error", icon
="ERROR")
467 pg
.error
= PDT_ERR_SEL_2_V_1_E
+ str(len(verts
)) + " Vertices"
468 bpy
.context
.window_manager
.popup_menu(oops
, title
="Error", icon
="ERROR")
470 coord_a
= np
.array([vector_a
.x
, vector_a
.y
, vector_a
.z
])
471 coord_b
= np
.array([vector_b
.x
, vector_b
.y
, vector_b
.z
])
472 if obj
.mode
== "OBJECT":
473 objs
= bpy
.context
.view_layer
.objects
.selected
475 pg
.error
= PDT_ERR_SEL_2_OBJS
+ str(len(objs
)) + ")"
476 bpy
.context
.window_manager
.popup_menu(oops
, title
="Error", icon
="ERROR")
480 objs
[-1].matrix_world
.decompose()[0].x
,
481 objs
[-1].matrix_world
.decompose()[0].y
,
482 objs
[-1].matrix_world
.decompose()[0].z
,
487 objs
[-2].matrix_world
.decompose()[0].x
,
488 objs
[-2].matrix_world
.decompose()[0].y
,
489 objs
[-2].matrix_world
.decompose()[0].z
,
492 coord_c
= coord_b
- coord_a
493 coord_d
= np
.array([0, 0, 0])
495 if (flip_percent
and data
!= "MV") or data
== "MV":
497 coord_out
= (coord_d
+coord_c
) * (_per_v
/ 100) + coord_a
498 return Vector((coord_out
[0], coord_out
[1], coord_out
[2]))
501 def obj_check(obj
, scene
, operation
):
502 """Check Object & Selection Validity.
507 operation: The Operation e.g. Create New Vertex
515 _operation
= operation
.upper()
518 pg
.error
= PDT_ERR_NO_ACT_OBJ
519 bpy
.context
.window_manager
.popup_menu(oops
, title
="Error", icon
="ERROR")
521 if obj
.mode
== "EDIT":
522 bm
= bmesh
.from_edit_mesh(obj
.data
)
523 if _operation
== "S":
524 if len(bm
.edges
) < 1:
525 pg
.error
= f
"{PDT_ERR_SEL_1_EDGEM} {len(bm.edges)})"
526 bpy
.context
.window_manager
.popup_menu(oops
, title
="Error", icon
="ERROR")
529 if len(bm
.select_history
) >= 1:
531 if _operation
not in {"D", "E", "F", "G", "N", "S"}:
532 vector_a
= check_selection(1, bm
, obj
)
534 verts
= [v
for v
in bm
.verts
if v
.select
]
538 pg
.error
= PDT_ERR_VERT_MODE
539 bpy
.context
.window_manager
.popup_menu(oops
, title
="Error", icon
="ERROR")
545 def dis_ang(values
, flip_angle
, plane
, scene
):
546 """Set Working Axes when using Direction command.
549 values: Input Arguments
550 flip_angle: Whether to flip the angle
555 Directional Offset as a Vector.
559 dis_v
= float(values
[0])
560 ang_v
= float(values
[1])
568 vector_delta
= view_dir(dis_v
, ang_v
)
570 a1
, a2
, _
= set_mode(plane
)
571 vector_delta
= Vector((0, 0, 0))
573 vector_delta
[a1
] = vector_delta
[a1
] + (dis_v
* cos(ang_v
* pi
/180))
574 vector_delta
[a2
] = vector_delta
[a2
] + (dis_v
* sin(ang_v
* pi
/180))
579 # Shader for displaying the Pivot Point as Graphics.
581 SHADER
= gpu
.shader
.from_builtin("3D_UNIFORM_COLOR") if not bpy
.app
.background
else None
584 def draw_3d(coords
, gtype
, rgba
, context
):
585 """Draw Pivot Point Graphics.
588 Draws either Lines Points, or Tris using defined shader
591 coords: Input Coordinates List
593 rgba: Colour in RGBA format
594 context: Blender bpy.context instance.
600 batch
= batch_for_shader(SHADER
, gtype
, {"pos": coords
})
603 if coords
is not None:
604 bgl
.glEnable(bgl
.GL_BLEND
)
606 SHADER
.uniform_float("color", rgba
)
609 raise PDT_ShaderError
612 def draw_callback_3d(self
, context
):
613 """Create Coordinate List for Pivot Point Graphic.
616 Creates coordinates for Pivot Point Graphic consisting of 6 Tris
617 and one Point colour coded Red; X axis, Green; Y axis, Blue; Z axis
618 and a yellow point based upon screen scale
621 context: Blender bpy.context instance.
627 scene
= context
.scene
629 region_width
= context
.region
.width
630 x_loc
= pg
.pivot_loc
.x
631 y_loc
= pg
.pivot_loc
.y
632 z_loc
= pg
.pivot_loc
.z
634 areas
= [a
for a
in context
.screen
.areas
if a
.type == "VIEW_3D"]
636 scale_factor
= abs(areas
[0].spaces
.active
.region_3d
.window_matrix
.decompose()[2][1])
637 # Check for orhtographic view and resize
638 #if areas[0].spaces.active.region_3d.is_orthographic_side_view:
639 # dim_a = region_width / sf / 60000 * pg.pivot_size
641 # dim_a = region_width / sf / 5000 * pg.pivot_size
642 dim_a
= region_width
/ scale_factor
/ 50000 * pg
.pivot_size
644 dim_c
= dim_a
* 0.05 + (pg
.pivot_width
* dim_a
* 0.02)
650 (x_loc
, y_loc
, z_loc
),
651 (x_loc
+dim_b
, y_loc
-dim_o
, z_loc
),
652 (x_loc
+dim_b
, y_loc
+dim_o
, z_loc
),
653 (x_loc
+dim_a
, y_loc
, z_loc
),
654 (x_loc
+dim_b
, y_loc
+dim_c
, z_loc
),
655 (x_loc
+dim_b
, y_loc
-dim_c
, z_loc
),
658 colour
= (1.0, 0.0, 0.0, pg
.pivot_alpha
)
659 draw_3d(coords
, "TRIS", colour
, context
)
660 coords
= [(x_loc
, y_loc
, z_loc
), (x_loc
+dim_a
, y_loc
, z_loc
)]
661 draw_3d(coords
, "LINES", colour
, context
)
665 (x_loc
, y_loc
, z_loc
),
666 (x_loc
-dim_o
, y_loc
+dim_b
, z_loc
),
667 (x_loc
+dim_o
, y_loc
+dim_b
, z_loc
),
668 (x_loc
, y_loc
+dim_a
, z_loc
),
669 (x_loc
+dim_c
, y_loc
+dim_b
, z_loc
),
670 (x_loc
-dim_c
, y_loc
+dim_b
, z_loc
),
673 colour
= (0.0, 1.0, 0.0, pg
.pivot_alpha
)
674 draw_3d(coords
, "TRIS", colour
, context
)
675 coords
= [(x_loc
, y_loc
, z_loc
), (x_loc
, y_loc
+ dim_a
, z_loc
)]
676 draw_3d(coords
, "LINES", colour
, context
)
680 (x_loc
, y_loc
, z_loc
),
681 (x_loc
-dim_o
, y_loc
, z_loc
+dim_b
),
682 (x_loc
+dim_o
, y_loc
, z_loc
+dim_b
),
683 (x_loc
, y_loc
, z_loc
+dim_a
),
684 (x_loc
+dim_c
, y_loc
, z_loc
+dim_b
),
685 (x_loc
-dim_c
, y_loc
, z_loc
+dim_b
),
688 colour
= (0.2, 0.5, 1.0, pg
.pivot_alpha
)
689 draw_3d(coords
, "TRIS", colour
, context
)
690 coords
= [(x_loc
, y_loc
, z_loc
), (x_loc
, y_loc
, z_loc
+ dim_a
)]
691 draw_3d(coords
, "LINES", colour
, context
)
693 coords
= [(x_loc
, y_loc
, z_loc
)]
694 colour
= (1.0, 1.0, 0.0, pg
.pivot_alpha
)
695 draw_3d(coords
, "POINTS", colour
, context
)
698 def scale_set(self
, context
):
699 """Sets Scale by dividing Pivot Distance by System Distance.
702 Sets Pivot Point Scale Factors by Measurement
703 Uses pg.pivotdis & pg.distance scene variables
706 context: Blender bpy.context instance.
712 scene
= context
.scene
714 sys_distance
= pg
.distance
715 scale_distance
= pg
.pivot_dis
716 if scale_distance
> 0:
717 scale_factor
= scale_distance
/ sys_distance
718 pg
.pivot_scale
= Vector((scale_factor
, scale_factor
, scale_factor
))