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 "XY": a1 = x, a2 = y, a3 = z
115 "XZ": a1 = x, a2 = z, a3 = y
116 "YZ": a1 = y, a2 = z, a3 = x
119 mode_pl: Plane Selector variable as input
130 return order
[mode_pl
]
133 def set_axis(mode_pl
):
134 """Sets Active Axes for View Orientation.
137 Sets indices for axes from taper vectors
138 Axis order: Rotate Axis, Move Axis, Height Axis
141 mode_pl: Taper Axis Selector variable as input
155 return order
[mode_pl
]
158 def check_selection(num
, bm
, obj
):
159 """Check that the Object's select_history has sufficient entries.
162 If selection history is not Verts, clears selection and history.
165 num: The number of entries required for each operation
166 bm: The Bmesh from the Object
170 list of 3D points as Vectors.
173 if len(bm
.select_history
) < num
:
175 active_vertex
= bm
.select_history
[-1]
176 if isinstance(active_vertex
, bmesh
.types
.BMVert
):
177 vector_a
= active_vertex
.co
181 vector_b
= bm
.select_history
[-2].co
182 return vector_a
, vector_b
184 vector_b
= bm
.select_history
[-2].co
185 vector_c
= bm
.select_history
[-3].co
186 return vector_a
, vector_b
, vector_c
188 vector_b
= bm
.select_history
[-2].co
189 vector_c
= bm
.select_history
[-3].co
190 vector_d
= bm
.select_history
[-4].co
191 return vector_a
, vector_b
, vector_c
, vector_d
199 bmesh
.update_edit_mesh(obj
.data
)
200 bm
.select_history
.clear()
204 def update_sel(bm
, verts
, edges
, faces
):
205 """Updates Vertex, Edge and Face Selections following a function.
209 verts: New Selection for Vertices
210 edges: The Edges on which to operate
211 faces: The Faces on which to operate
230 def view_coords(x_loc
, y_loc
, z_loc
):
231 """Converts input Vector values to new Screen Oriented Vector.
234 x_loc: X coordinate from vector
235 y_loc: Y coordinate from vector
236 z_loc: Z coordinate from vector
239 Vector adjusted to View's Inverted Tranformation Matrix.
242 areas
= [a
for a
in bpy
.context
.screen
.areas
if a
.type == "VIEW_3D"]
244 view_matrix
= areas
[0].spaces
.active
.region_3d
.view_matrix
245 view_matrix
= view_matrix
.to_3x3().normalized().inverted()
246 view_location
= Vector((x_loc
, y_loc
, z_loc
))
247 new_view_location
= view_matrix
@ view_location
248 return new_view_location
250 return Vector((0, 0, 0))
253 def view_coords_i(x_loc
, y_loc
, z_loc
):
254 """Converts Screen Oriented input Vector values to new World Vector.
257 Converts View tranformation Matrix to Rotational Matrix
260 x_loc: X coordinate from vector
261 y_loc: Y coordinate from vector
262 z_loc: Z coordinate from vector
265 Vector adjusted to View's Transformation Matrix.
268 areas
= [a
for a
in bpy
.context
.screen
.areas
if a
.type == "VIEW_3D"]
270 view_matrix
= areas
[0].spaces
.active
.region_3d
.view_matrix
271 view_matrix
= view_matrix
.to_3x3().normalized()
272 view_location
= Vector((x_loc
, y_loc
, z_loc
))
273 new_view_location
= view_matrix
@ view_location
274 return new_view_location
276 return Vector((0, 0, 0))
279 def view_dir(dis_v
, ang_v
):
280 """Converts Distance and Angle to View Oriented Vector.
283 Converts View Transformation Matrix to Rotational Matrix (3x3)
284 Angles are Converts to Radians from degrees.
287 dis_v: Scene PDT distance
288 ang_v: Scene PDT angle
294 areas
= [a
for a
in bpy
.context
.screen
.areas
if a
.type == "VIEW_3D"]
296 view_matrix
= areas
[0].spaces
.active
.region_3d
.view_matrix
297 view_matrix
= view_matrix
.to_3x3().normalized().inverted()
298 view_location
= Vector((0, 0, 0))
299 view_location
.x
= dis_v
* cos(ang_v
* pi
/ 180)
300 view_location
.y
= dis_v
* sin(ang_v
* pi
/ 180)
301 new_view_location
= view_matrix
@ view_location
302 return new_view_location
304 return Vector((0, 0, 0))
307 def euler_to_quaternion(roll
, pitch
, yaw
):
308 """Converts Euler Rotation to Quaternion Rotation.
311 roll: Roll in Euler rotation
312 pitch: Pitch in Euler rotation
313 yaw: Yaw in Euler rotation
320 quat_x
= (np
.sin(roll
/2) * np
.cos(pitch
/2) * np
.cos(yaw
/2)
321 - np
.cos(roll
/2) * np
.sin(pitch
/2) * np
.sin(yaw
/2))
322 quat_y
= (np
.cos(roll
/2) * np
.sin(pitch
/2) * np
.cos(yaw
/2)
323 + np
.sin(roll
/2) * np
.cos(pitch
/2) * np
.sin(yaw
/2))
324 quat_z
= (np
.cos(roll
/2) * np
.cos(pitch
/2) * np
.sin(yaw
/2)
325 - np
.sin(roll
/2) * np
.sin(pitch
/2) * np
.cos(yaw
/2))
326 quat_w
= (np
.cos(roll
/2) * np
.cos(pitch
/2) * np
.cos(yaw
/2)
327 + np
.sin(roll
/2) * np
.sin(pitch
/2) * np
.sin(yaw
/2))
329 return Quaternion((quat_w
, quat_x
, quat_y
, quat_z
))
332 def arc_centre(vector_a
, vector_b
, vector_c
):
333 """Calculates Centre of Arc from 3 Vector Locations using standard Numpy routine
336 vector_a: Active vector location
337 vector_b: Second vector location
338 vector_c: Third vector location
341 Vector representing Arc Centre and Float representing Arc Radius.
344 coord_a
= np
.array([vector_a
.x
, vector_a
.y
, vector_a
.z
])
345 coord_b
= np
.array([vector_b
.x
, vector_b
.y
, vector_b
.z
])
346 coord_c
= np
.array([vector_c
.x
, vector_c
.y
, vector_c
.z
])
347 line_a
= np
.linalg
.norm(coord_c
- coord_b
)
348 line_b
= np
.linalg
.norm(coord_c
- coord_a
)
349 line_c
= np
.linalg
.norm(coord_b
- coord_a
)
351 line_s
= (line_a
+line_b
+line_c
) / 2
353 line_a
*line_b
*line_c
/4
359 base_1
= line_a
*line_a
* (line_b
*line_b
+ line_c
*line_c
- line_a
*line_a
)
360 base_2
= line_b
*line_b
* (line_a
*line_a
+ line_c
*line_c
- line_b
*line_b
)
361 base_3
= line_c
*line_c
* (line_a
*line_a
+ line_b
*line_b
- line_c
*line_c
)
363 intersect_coord
= np
.column_stack((coord_a
, coord_b
, coord_c
))
364 intersect_coord
= intersect_coord
.dot(np
.hstack((base_1
, base_2
, base_3
)))
365 intersect_coord
/= base_1
+ base_2
+ base_3
366 return Vector((intersect_coord
[0], intersect_coord
[1], intersect_coord
[2])), radius
369 def intersection(vertex_a
, vertex_b
, vertex_c
, vertex_d
, plane
):
370 """Calculates Intersection Point of 2 Imagined Lines from 4 Vectors.
373 Calculates Converging Intersect Location and indication of
374 whether the lines are convergent using standard Numpy Routines
377 vertex_a: Active vector location of first line
378 vertex_b: Second vector location of first line
379 vertex_c: Third vector location of 2nd line
380 vertex_d: Fourth vector location of 2nd line
381 plane: Working Plane 4 Vector Locations representing 2 lines and Working Plane
384 Intersection Vector and Boolean for convergent state.
388 vertex_offset
= vertex_b
- vertex_a
389 vertex_b
= view_coords_i(vertex_offset
.x
, vertex_offset
.y
, vertex_offset
.z
)
390 vertex_offset
= vertex_d
- vertex_a
391 vertex_d
= view_coords_i(vertex_offset
.x
, vertex_offset
.y
, vertex_offset
.z
)
392 vertex_offset
= vertex_c
- vertex_a
393 vertex_c
= view_coords_i(vertex_offset
.x
, vertex_offset
.y
, vertex_offset
.z
)
394 vector_ref
= Vector((0, 0, 0))
395 coord_a
= (vertex_c
.x
, vertex_c
.y
)
396 coord_b
= (vertex_d
.x
, vertex_d
.y
)
397 coord_c
= (vertex_b
.x
, vertex_b
.y
)
398 coord_d
= (vector_ref
.x
, vector_ref
.y
)
400 a1
, a2
, a3
= set_mode(plane
)
401 coord_a
= (vertex_c
[a1
], vertex_c
[a2
])
402 coord_b
= (vertex_d
[a1
], vertex_d
[a2
])
403 coord_c
= (vertex_a
[a1
], vertex_a
[a2
])
404 coord_d
= (vertex_b
[a1
], vertex_b
[a2
])
405 v_stack
= np
.vstack([coord_a
, coord_b
, coord_c
, coord_d
])
406 h_stack
= np
.hstack((v_stack
, np
.ones((4, 1))))
407 line_a
= np
.cross(h_stack
[0], h_stack
[1])
408 line_b
= np
.cross(h_stack
[2], h_stack
[3])
409 x_loc
, y_loc
, z_loc
= np
.cross(line_a
, line_b
)
411 return Vector((0, 0, 0)), False
412 new_x_loc
= x_loc
/ z_loc
413 new_z_loc
= y_loc
/ z_loc
417 new_y_loc
= vertex_a
[a3
]
420 vector_delta
= Vector((new_x_loc
, new_y_loc
, new_z_loc
))
422 vector_delta
= Vector((new_x_loc
, new_z_loc
, new_y_loc
))
424 vector_delta
= Vector((new_y_loc
, new_x_loc
, new_z_loc
))
426 # Must be Local View Plane
427 vector_delta
= view_coords(new_x_loc
, new_z_loc
, new_y_loc
) + vertex_a
428 return vector_delta
, True
431 def get_percent(obj
, flip_percent
, per_v
, data
, scene
):
432 """Calculates a Percentage Distance between 2 Vectors.
435 Calculates a point that lies a set percentage between two given points
436 using standard Numpy Routines.
438 Works for either 2 vertices for an object in Edit mode
439 or 2 selected objects in Object mode.
442 obj: The Object under consideration
443 flip_percent: Setting this to True measures the percentage starting from the second vector
444 per_v: Percentage Input Value
445 data: pg.flip, pg.percent scene variables & Operational Mode
454 if obj
.mode
== "EDIT":
455 bm
= bmesh
.from_edit_mesh(obj
.data
)
456 verts
= [v
for v
in bm
.verts
if v
.select
]
458 vector_a
= verts
[0].co
459 vector_b
= verts
[1].co
461 pg
.error
= PDT_ERR_VERT_MODE
462 bpy
.context
.window_manager
.popup_menu(oops
, title
="Error", icon
="ERROR")
465 pg
.error
= PDT_ERR_SEL_2_V_1_E
+ str(len(verts
)) + " Vertices"
466 bpy
.context
.window_manager
.popup_menu(oops
, title
="Error", icon
="ERROR")
468 coord_a
= np
.array([vector_a
.x
, vector_a
.y
, vector_a
.z
])
469 coord_b
= np
.array([vector_b
.x
, vector_b
.y
, vector_b
.z
])
470 if obj
.mode
== "OBJECT":
471 objs
= bpy
.context
.view_layer
.objects
.selected
473 pg
.error
= PDT_ERR_SEL_2_OBJS
+ str(len(objs
)) + ")"
474 bpy
.context
.window_manager
.popup_menu(oops
, title
="Error", icon
="ERROR")
478 objs
[-1].matrix_world
.decompose()[0].x
,
479 objs
[-1].matrix_world
.decompose()[0].y
,
480 objs
[-1].matrix_world
.decompose()[0].z
,
485 objs
[-2].matrix_world
.decompose()[0].x
,
486 objs
[-2].matrix_world
.decompose()[0].y
,
487 objs
[-2].matrix_world
.decompose()[0].z
,
490 coord_c
= coord_b
- coord_a
491 coord_d
= np
.array([0, 0, 0])
493 if (flip_percent
and data
!= "MV") or data
== "MV":
495 coord_out
= (coord_d
+coord_c
) * (_per_v
/ 100) + coord_a
496 return Vector((coord_out
[0], coord_out
[1], coord_out
[2]))
499 def obj_check(obj
, scene
, operation
):
500 """Check Object & Selection Validity.
505 operation: The Operation e.g. Create New Vertex
513 _operation
= operation
.upper()
516 pg
.error
= PDT_ERR_NO_ACT_OBJ
517 bpy
.context
.window_manager
.popup_menu(oops
, title
="Error", icon
="ERROR")
519 if obj
.mode
== "EDIT":
520 bm
= bmesh
.from_edit_mesh(obj
.data
)
521 if _operation
== "S":
522 if len(bm
.edges
) < 1:
523 pg
.error
= f
"{PDT_ERR_SEL_1_EDGEM} {len(bm.edges)})"
524 bpy
.context
.window_manager
.popup_menu(oops
, title
="Error", icon
="ERROR")
527 if len(bm
.select_history
) >= 1:
529 if _operation
not in {"D", "E", "F", "G", "N", "S"}:
530 vector_a
= check_selection(1, bm
, obj
)
532 verts
= [v
for v
in bm
.verts
if v
.select
]
536 pg
.error
= PDT_ERR_VERT_MODE
537 bpy
.context
.window_manager
.popup_menu(oops
, title
="Error", icon
="ERROR")
543 def dis_ang(values
, flip_angle
, plane
, scene
):
544 """Set Working Axes when using Direction command.
547 values: Input Arguments
548 flip_angle: Whether to flip the angle
553 Directional Offset as a Vector.
557 dis_v
= float(values
[0])
558 ang_v
= float(values
[1])
566 vector_delta
= view_dir(dis_v
, ang_v
)
568 a1
, a2
, _
= set_mode(plane
)
569 vector_delta
= Vector((0, 0, 0))
571 vector_delta
[a1
] = vector_delta
[a1
] + (dis_v
* cos(ang_v
* pi
/180))
572 vector_delta
[a2
] = vector_delta
[a2
] + (dis_v
* sin(ang_v
* pi
/180))
577 # Shader for displaying the Pivot Point as Graphics.
579 SHADER
= gpu
.shader
.from_builtin("3D_UNIFORM_COLOR") if not bpy
.app
.background
else None
582 def draw_3d(coords
, gtype
, rgba
, context
):
583 """Draw Pivot Point Graphics.
586 Draws either Lines Points, or Tris using defined shader
589 coords: Input Coordinates List
591 rgba: Colour in RGBA format
592 context: Blender bpy.context instance.
598 batch
= batch_for_shader(SHADER
, gtype
, {"pos": coords
})
601 if coords
is not None:
602 bgl
.glEnable(bgl
.GL_BLEND
)
604 SHADER
.uniform_float("color", rgba
)
607 raise PDT_ShaderError
610 def draw_callback_3d(self
, context
):
611 """Create Coordinate List for Pivot Point Graphic.
614 Creates coordinates for Pivot Point Graphic consisting of 6 Tris
615 and one Point colour coded Red; X axis, Green; Y axis, Blue; Z axis
616 and a yellow point based upon screen scale
619 context: Blender bpy.context instance.
625 scene
= context
.scene
627 region_width
= context
.region
.width
628 x_loc
= pg
.pivot_loc
.x
629 y_loc
= pg
.pivot_loc
.y
630 z_loc
= pg
.pivot_loc
.z
632 areas
= [a
for a
in context
.screen
.areas
if a
.type == "VIEW_3D"]
634 scale_factor
= abs(areas
[0].spaces
.active
.region_3d
.window_matrix
.decompose()[2][1])
635 # Check for orhtographic view and resize
636 #if areas[0].spaces.active.region_3d.is_orthographic_side_view:
637 # dim_a = region_width / sf / 60000 * pg.pivot_size
639 # dim_a = region_width / sf / 5000 * pg.pivot_size
640 dim_a
= region_width
/ scale_factor
/ 50000 * pg
.pivot_size
642 dim_c
= dim_a
* 0.05 + (pg
.pivot_width
* dim_a
* 0.02)
648 (x_loc
, y_loc
, z_loc
),
649 (x_loc
+dim_b
, y_loc
-dim_o
, z_loc
),
650 (x_loc
+dim_b
, y_loc
+dim_o
, z_loc
),
651 (x_loc
+dim_a
, y_loc
, z_loc
),
652 (x_loc
+dim_b
, y_loc
+dim_c
, z_loc
),
653 (x_loc
+dim_b
, y_loc
-dim_c
, z_loc
),
656 colour
= (1.0, 0.0, 0.0, pg
.pivot_alpha
)
657 draw_3d(coords
, "TRIS", colour
, context
)
658 coords
= [(x_loc
, y_loc
, z_loc
), (x_loc
+dim_a
, y_loc
, z_loc
)]
659 draw_3d(coords
, "LINES", colour
, context
)
663 (x_loc
, y_loc
, z_loc
),
664 (x_loc
-dim_o
, y_loc
+dim_b
, z_loc
),
665 (x_loc
+dim_o
, y_loc
+dim_b
, z_loc
),
666 (x_loc
, y_loc
+dim_a
, z_loc
),
667 (x_loc
+dim_c
, y_loc
+dim_b
, z_loc
),
668 (x_loc
-dim_c
, y_loc
+dim_b
, z_loc
),
671 colour
= (0.0, 1.0, 0.0, pg
.pivot_alpha
)
672 draw_3d(coords
, "TRIS", colour
, context
)
673 coords
= [(x_loc
, y_loc
, z_loc
), (x_loc
, y_loc
+ dim_a
, z_loc
)]
674 draw_3d(coords
, "LINES", colour
, context
)
678 (x_loc
, y_loc
, z_loc
),
679 (x_loc
-dim_o
, y_loc
, z_loc
+dim_b
),
680 (x_loc
+dim_o
, y_loc
, z_loc
+dim_b
),
681 (x_loc
, y_loc
, z_loc
+dim_a
),
682 (x_loc
+dim_c
, y_loc
, z_loc
+dim_b
),
683 (x_loc
-dim_c
, y_loc
, z_loc
+dim_b
),
686 colour
= (0.2, 0.5, 1.0, pg
.pivot_alpha
)
687 draw_3d(coords
, "TRIS", colour
, context
)
688 coords
= [(x_loc
, y_loc
, z_loc
), (x_loc
, y_loc
, z_loc
+ dim_a
)]
689 draw_3d(coords
, "LINES", colour
, context
)
691 coords
= [(x_loc
, y_loc
, z_loc
)]
692 colour
= (1.0, 1.0, 0.0, pg
.pivot_alpha
)
693 draw_3d(coords
, "POINTS", colour
, context
)
696 def scale_set(self
, context
):
697 """Sets Scale by dividing Pivot Distance by System Distance.
700 Sets Pivot Point Scale Factors by Measurement
701 Uses pg.pivotdis & pg.distance scene variables
704 context: Blender bpy.context instance.
710 scene
= context
.scene
712 sys_distance
= pg
.distance
713 scale_distance
= pg
.pivot_dis
714 if scale_distance
> 0:
715 scale_factor
= scale_distance
/ sys_distance
716 pg
.pivot_scale
= Vector((scale_factor
, scale_factor
, scale_factor
))