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
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 (
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.
30 The printed message will be of the form:
32 {prefix}{caller file name:line number}| {msg}
35 msg: Incoming message to display
42 pdt_debug
= bpy
.context
.preferences
.addons
[__package__
].preferences
.debug
43 if bpy
.app
.debug
or bpy
.app
.debug_python
or pdt_debug
:
46 def extract_filename(fullpath
):
47 """Return only the filename part of fullpath (excluding its path).
50 fullpath: Filename's full path
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]
59 # something went wrong
62 # since this is a string, just return it
65 # stack frame corresponding to the line where debug(msg) was called
66 #print(traceback.extract_stack()[-2])
67 laststack
= traceback
.extract_stack()[-2]
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
):
76 Displays error message in a popup.
79 context: Blender bpy.context instance.
87 self
.layout
.label(text
=pg
.error
)
90 def set_mode(mode_pl
):
91 """Sets Active Axes for View Orientation.
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
101 mode_pl: Plane Selector variable as input
113 return order
[mode_pl
]
116 def set_axis(mode_pl
):
117 """Sets Active Axes for View Orientation.
120 Sets indices for axes from taper vectors
121 Axis order: Rotate Axis, Move Axis, Height Axis
124 mode_pl: Taper Axis Selector variable as input
138 return order
[mode_pl
]
141 def check_selection(num
, bm
, obj
):
142 """Check that the Object's select_history has sufficient entries.
145 If selection history is not Verts, clears selection and history.
148 num: The number of entries required for each operation
149 bm: The Bmesh from the Object
153 list of 3D points as Vectors.
156 if len(bm
.select_history
) < num
:
158 active_vertex
= bm
.select_history
[-1]
159 if isinstance(active_vertex
, bmesh
.types
.BMVert
):
160 vector_a
= active_vertex
.co
164 vector_b
= bm
.select_history
[-2].co
165 return vector_a
, vector_b
167 vector_b
= bm
.select_history
[-2].co
168 vector_c
= bm
.select_history
[-3].co
169 return vector_a
, vector_b
, vector_c
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
182 bmesh
.update_edit_mesh(obj
.data
)
183 bm
.select_history
.clear()
187 def update_sel(bm
, verts
, edges
, faces
):
188 """Updates Vertex, Edge and Face Selections following a function.
192 verts: New Selection for Vertices
193 edges: The Edges on which to operate
194 faces: The Faces on which to operate
213 def view_coords(x_loc
, y_loc
, z_loc
):
214 """Converts input Vector values to new Screen Oriented Vector.
217 x_loc: X coordinate from vector
218 y_loc: Y coordinate from vector
219 z_loc: Z coordinate from vector
222 Vector adjusted to View's Inverted Transformation Matrix.
225 areas
= [a
for a
in bpy
.context
.screen
.areas
if a
.type == "VIEW_3D"]
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.
240 Converts View transformation Matrix to Rotational Matrix
243 x_loc: X coordinate from vector
244 y_loc: Y coordinate from vector
245 z_loc: Z coordinate from vector
248 Vector adjusted to View's Transformation Matrix.
251 areas
= [a
for a
in bpy
.context
.screen
.areas
if a
.type == "VIEW_3D"]
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.
266 Converts View Transformation Matrix to Rotational Matrix (3x3)
267 Angles are Converts to Radians from degrees.
270 dis_v: Scene PDT distance
271 ang_v: Scene PDT angle
277 areas
= [a
for a
in bpy
.context
.screen
.areas
if a
.type == "VIEW_3D"]
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.
294 roll: Roll in Euler rotation
295 pitch: Pitch in Euler rotation
296 yaw: Yaw in Euler rotation
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))
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
319 vector_a: Active vector location
320 vector_b: Second vector location
321 vector_c: Third vector location
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
)
334 line_s
= (line_a
+line_b
+line_c
) / 2
336 line_a
*line_b
*line_c
/4
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
)
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.
356 Calculates Converging Intersect Location and indication of
357 whether the lines are convergent using standard Numpy Routines
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
367 Intersection Vector and Boolean for convergent state.
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
)
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
)
394 return Vector((0, 0, 0)), False
395 new_x_loc
= x_loc
/ z_loc
396 new_z_loc
= y_loc
/ z_loc
400 new_y_loc
= vertex_a
[a3
]
403 vector_delta
= Vector((new_x_loc
, new_y_loc
, new_z_loc
))
405 vector_delta
= Vector((new_x_loc
, new_z_loc
, new_y_loc
))
407 vector_delta
= Vector((new_y_loc
, new_x_loc
, new_z_loc
))
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.
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.
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
437 if obj
.mode
== "EDIT":
438 bm
= bmesh
.from_edit_mesh(obj
.data
)
439 verts
= [v
for v
in bm
.verts
if v
.select
]
441 vector_a
= verts
[0].co
442 vector_b
= verts
[1].co
444 pg
.error
= PDT_ERR_VERT_MODE
445 bpy
.context
.window_manager
.popup_menu(oops
, title
="Error", icon
="ERROR")
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")
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
456 pg
.error
= PDT_ERR_SEL_2_OBJS
+ str(len(objs
)) + ")"
457 bpy
.context
.window_manager
.popup_menu(oops
, title
="Error", icon
="ERROR")
461 objs
[-1].matrix_world
.decompose()[0].x
,
462 objs
[-1].matrix_world
.decompose()[0].y
,
463 objs
[-1].matrix_world
.decompose()[0].z
,
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])
476 if (flip_percent
and data
!= "MV") or data
== "MV":
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.
488 operation: The Operation e.g. Create New Vertex
496 _operation
= operation
.upper()
499 pg
.error
= PDT_ERR_NO_ACT_OBJ
500 bpy
.context
.window_manager
.popup_menu(oops
, title
="Error", icon
="ERROR")
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")
510 if len(bm
.select_history
) >= 1:
512 if _operation
not in {"D", "E", "F", "G", "N", "S"}:
513 vector_a
= check_selection(1, bm
, obj
)
515 verts
= [v
for v
in bm
.verts
if v
.select
]
519 pg
.error
= PDT_ERR_VERT_MODE
520 bpy
.context
.window_manager
.popup_menu(oops
, title
="Error", icon
="ERROR")
526 def dis_ang(values
, flip_angle
, plane
, scene
):
527 """Set Working Axes when using Direction command.
530 values: Input Arguments
531 flip_angle: Whether to flip the angle
536 Directional Offset as a Vector.
540 dis_v
= float(values
[0])
541 ang_v
= float(values
[1])
549 vector_delta
= view_dir(dis_v
, ang_v
)
551 a1
, a2
, _
= set_mode(plane
)
552 vector_delta
= Vector((0, 0, 0))
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))
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.
569 Draws either Lines Points, or Tris using defined shader
572 coords: Input Coordinates List
574 rgba: Colour in RGBA format
575 context: Blender bpy.context instance.
581 batch
= batch_for_shader(SHADER
, gtype
, {"pos": coords
})
584 if coords
is not None:
585 gpu
.state
.blend_set('ALPHA')
587 SHADER
.uniform_float("color", rgba
)
590 raise PDT_ShaderError
593 def draw_callback_3d(self
, context
):
594 """Create Coordinate List for Pivot Point Graphic.
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
602 context: Blender bpy.context instance.
608 scene
= context
.scene
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
615 areas
= [a
for a
in context
.screen
.areas
if a
.type == "VIEW_3D"]
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
622 # dim_a = region_width / sf / 5000 * pg.pivot_size
623 dim_a
= region_width
/ scale_factor
/ 50000 * pg
.pivot_size
625 dim_c
= dim_a
* 0.05 + (pg
.pivot_width
* dim_a
* 0.02)
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
),
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
)
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
),
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
)
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
),
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
)
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.
683 Sets Pivot Point Scale Factors by Measurement
684 Uses pg.pivotdis & pg.distance scene variables
687 context: Blender bpy.context instance.
693 scene
= context
.scene
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
))