Fix #104941: Node Wrangler cannot use both bump and normal
[blender-addons.git] / object_carver / carver_utils.py
blob268eddc753262a7973c7a019f7fe47610fe5e311
1 # SPDX-FileCopyrightText: 2019-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 import bpy
6 import gpu
7 from gpu_extras.batch import batch_for_shader
8 import math
9 import sys
10 import random
11 import bmesh
12 from mathutils import (
13 Euler,
14 Matrix,
15 Vector,
16 Quaternion,
18 from mathutils.geometry import (
19 intersect_line_plane,
22 from math import (
23 sin,
24 cos,
25 pi,
28 import bpy_extras
30 from bpy_extras import view3d_utils
31 from bpy_extras.view3d_utils import (
32 region_2d_to_vector_3d,
33 region_2d_to_location_3d,
34 location_3d_to_region_2d,
37 # Cut Square
38 def CreateCutSquare(self, context):
39 """ Create a rectangle mesh """
40 far_limit = 10000.0
41 faces=[]
43 # Get the mouse coordinates
44 coord = self.mouse_path[0][0], self.mouse_path[0][1]
46 # New mesh
47 me = bpy.data.meshes.new('CMT_Square')
48 bm = bmesh.new()
49 bm.from_mesh(me)
51 # New object and link it to the scene
52 ob = bpy.data.objects.new('CMT_Square', me)
53 self.CurrentObj = ob
54 context.collection.objects.link(ob)
56 # Scene information
57 region = context.region
58 rv3d = context.region_data
59 depth_location = region_2d_to_vector_3d(region, rv3d, coord)
60 self.ViewVector = depth_location
62 # Get a point on a infinite plane and its direction
63 plane_normal = depth_location
64 plane_direction = plane_normal.normalized()
66 if self.snapCursor:
67 plane_point = context.scene.cursor.location
68 else:
69 plane_point = self.OpsObj.location if self.OpsObj is not None else Vector((0.0, 0.0, 0.0))
71 # Find the intersection of a line going thru each vertex and the infinite plane
72 for v_co in self.rectangle_coord:
73 vec = region_2d_to_vector_3d(region, rv3d, v_co)
74 p0 = region_2d_to_location_3d(region, rv3d,v_co, vec)
75 p1 = region_2d_to_location_3d(region, rv3d,v_co, vec) + plane_direction * far_limit
76 faces.append(bm.verts.new(intersect_line_plane(p0, p1, plane_point, plane_direction)))
78 # Update vertices index
79 bm.verts.index_update()
80 # New faces
81 t_face = bm.faces.new(faces)
82 # Set mesh
83 bm.to_mesh(me)
86 # Cut Line
87 def CreateCutLine(self, context):
88 """ Create a polygon mesh """
89 far_limit = 10000.0
90 vertices = []
91 faces = []
92 loc = []
94 # Get the mouse coordinates
95 coord = self.mouse_path[0][0], self.mouse_path[0][1]
97 # New mesh
98 me = bpy.data.meshes.new('CMT_Line')
99 bm = bmesh.new()
100 bm.from_mesh(me)
102 # New object and link it to the scene
103 ob = bpy.data.objects.new('CMT_Line', me)
104 self.CurrentObj = ob
105 context.collection.objects.link(ob)
107 # Scene information
108 region = context.region
109 rv3d = context.region_data
110 depth_location = region_2d_to_vector_3d(region, rv3d, coord)
111 self.ViewVector = depth_location
113 # Get a point on a infinite plane and its direction
114 plane_normal = depth_location
115 plane_direction = plane_normal.normalized()
117 if self.snapCursor:
118 plane_point = context.scene.cursor.location
119 else:
120 plane_point = self.OpsObj.location if self.OpsObj is not None else Vector((0.0, 0.0, 0.0))
122 # Use dict to remove doubles
123 # Find the intersection of a line going thru each vertex and the infinite plane
124 for idx, v_co in enumerate(list(dict.fromkeys(self.mouse_path))):
125 vec = region_2d_to_vector_3d(region, rv3d, v_co)
126 p0 = region_2d_to_location_3d(region, rv3d,v_co, vec)
127 p1 = region_2d_to_location_3d(region, rv3d,v_co, vec) + plane_direction * far_limit
128 loc.append(intersect_line_plane(p0, p1, plane_point, plane_direction))
129 vertices.append(bm.verts.new(loc[idx]))
131 if idx > 0:
132 bm.edges.new([vertices[idx-1],vertices[idx]])
134 faces.append(vertices[idx])
136 # Update vertices index
137 bm.verts.index_update()
139 # Nothing is selected, create close geometry
140 if self.CreateMode:
141 if self.Closed and len(vertices) > 1:
142 bm.edges.new([vertices[-1], vertices[0]])
143 bm.faces.new(faces)
144 else:
145 # Create faces if more than 2 vertices
146 if len(vertices) > 1 :
147 bm.edges.new([vertices[-1], vertices[0]])
148 bm.faces.new(faces)
150 bm.to_mesh(me)
152 # Cut Circle
153 def CreateCutCircle(self, context):
154 """ Create a circle mesh """
155 far_limit = 10000.0
156 FacesList = []
158 # Get the mouse coordinates
159 mouse_pos_x = self.mouse_path[0][0]
160 mouse_pos_y = self.mouse_path[0][1]
161 coord = self.mouse_path[0][0], self.mouse_path[0][1]
163 # Scene information
164 region = context.region
165 rv3d = context.region_data
166 depth_location = region_2d_to_vector_3d(region, rv3d, coord)
167 self.ViewVector = depth_location
169 # Get a point on a infinite plane and its direction
170 plane_point = context.scene.cursor.location if self.snapCursor else Vector((0.0, 0.0, 0.0))
171 plane_normal = depth_location
172 plane_direction = plane_normal.normalized()
174 # New mesh
175 me = bpy.data.meshes.new('CMT_Circle')
176 bm = bmesh.new()
177 bm.from_mesh(me)
179 # New object and link it to the scene
180 ob = bpy.data.objects.new('CMT_Circle', me)
181 self.CurrentObj = ob
182 context.collection.objects.link(ob)
184 # Create a circle using a tri fan
185 tris_fan, indices = draw_circle(self, mouse_pos_x, mouse_pos_y)
187 # Remove the vertex in the center to get the outer line of the circle
188 verts = tris_fan[1:]
190 # Find the intersection of a line going thru each vertex and the infinite plane
191 for vert in verts:
192 vec = region_2d_to_vector_3d(region, rv3d, vert)
193 p0 = region_2d_to_location_3d(region, rv3d, vert, vec)
194 p1 = p0 + plane_direction * far_limit
195 loc0 = intersect_line_plane(p0, p1, plane_point, plane_direction)
196 t_v0 = bm.verts.new(loc0)
197 FacesList.append(t_v0)
199 bm.verts.index_update()
200 bm.faces.new(FacesList)
201 bm.to_mesh(me)
204 def create_2d_circle(self, step, radius, rotation = 0):
205 """ Create the vertices of a 2d circle at (0,0) """
206 verts = []
207 for angle in range(0, 360, step):
208 verts.append(math.cos(math.radians(angle + rotation)) * radius)
209 verts.append(math.sin(math.radians(angle + rotation)) * radius)
210 verts.append(0.0)
211 verts.append(math.cos(math.radians(0.0 + rotation)) * radius)
212 verts.append(math.sin(math.radians(0.0 + rotation)) * radius)
213 verts.append(0.0)
214 return(verts)
217 def draw_circle(self, mouse_pos_x, mouse_pos_y):
218 """ Return the coordinates + indices of a circle using a triangle fan """
219 tris_verts = []
220 indices = []
221 segments = int(360 / self.stepAngle[self.step])
222 radius = self.mouse_path[1][0] - self.mouse_path[0][0]
223 rotation = (self.mouse_path[1][1] - self.mouse_path[0][1]) / 2
225 # Get the vertices of a 2d circle
226 verts = create_2d_circle(self, self.stepAngle[self.step], radius, rotation)
228 # Create the first vertex at mouse position for the center of the circle
229 tris_verts.append(Vector((mouse_pos_x + self.xpos , mouse_pos_y + self.ypos)))
231 # For each vertex of the circle, add the mouse position and the translation
232 for idx in range(int(len(verts) / 3) - 1):
233 tris_verts.append(Vector((verts[idx * 3] + mouse_pos_x + self.xpos, \
234 verts[idx * 3 + 1] + mouse_pos_y + self.ypos)))
235 i1 = idx+1
236 i2 = idx+2 if idx+2 <= segments else 1
237 indices.append((0,i1,i2))
239 return(tris_verts, indices)
241 # Object dimensions (SCULPT Tools tips)
242 def objDiagonal(obj):
243 return ((obj.dimensions[0]**2) + (obj.dimensions[1]**2) + (obj.dimensions[2]**2))**0.5
246 # Bevel Update
247 def update_bevel(context):
248 selection = context.selected_objects.copy()
249 active = context.active_object
251 if len(selection) > 0:
252 for obj in selection:
253 bpy.ops.object.select_all(action='DESELECT')
254 obj.select_set(True)
255 context.view_layer.objects.active = obj
257 # Test object name
258 # Subdive mode : Only bevel weight
259 if obj.data.name.startswith("S_") or obj.data.name.startswith("S "):
260 bpy.ops.object.mode_set(mode='EDIT')
261 bpy.ops.mesh.region_to_loop()
262 bpy.ops.transform.edge_bevelweight(value=1)
263 bpy.ops.object.mode_set(mode='OBJECT')
265 else:
266 # No subdiv mode : bevel weight + Crease + Sharp
267 CreateBevel(context, obj)
269 bpy.ops.object.select_all(action='DESELECT')
271 for obj in selection:
272 obj.select_set(True)
273 context.view_layer.objects.active = active
275 # Create bevel
276 def CreateBevel(context, CurrentObject):
277 # Save active object
278 SavActive = context.active_object
280 # Test if initial object has bevel
281 bevel_modifier = False
282 for modifier in SavActive.modifiers:
283 if modifier.name == 'Bevel':
284 bevel_modifier = True
286 if bevel_modifier:
287 # Active "CurrentObject"
288 context.view_layer.objects.active = CurrentObject
290 bpy.ops.object.mode_set(mode='EDIT')
292 # Edge mode
293 bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE')
294 # Clear all
295 bpy.ops.mesh.select_all(action='SELECT')
296 bpy.ops.mesh.mark_sharp(clear=True)
297 bpy.ops.transform.edge_crease(value=-1)
298 bpy.ops.transform.edge_bevelweight(value=-1)
300 bpy.ops.mesh.select_all(action='DESELECT')
302 # Select (in radians) all 30° sharp edges
303 bpy.ops.mesh.edges_select_sharp(sharpness=0.523599)
304 # Apply bevel weight + Crease + Sharp to the selected edges
305 bpy.ops.mesh.mark_sharp()
306 bpy.ops.transform.edge_crease(value=1)
307 bpy.ops.transform.edge_bevelweight(value=1)
309 bpy.ops.mesh.select_all(action='DESELECT')
311 bpy.ops.object.mode_set(mode='OBJECT')
313 bevel_weights = CurrentObject.data.attributes["bevel_weight_edge"]
314 if not bevel_weights:
315 bevel_weights = CurrentObject.data.attributes.new("bevel_weight_edge", 'FLOAT', 'EDGE')
316 if bevel_weights.data_type != 'FLOAT' or bevel_weights.domain != 'EDGE':
317 bevel_weights = None
319 for i in range(len(CurrentObject.data.edges)):
320 if CurrentObject.data.edges[i].select is True:
321 if bevel_weights:
322 bevel_weights.data[i] = 1.0
323 CurrentObject.data.edges[i].use_edge_sharp = True
325 bevel_modifier = False
326 for m in CurrentObject.modifiers:
327 if m.name == 'Bevel':
328 bevel_modifier = True
330 if bevel_modifier is False:
331 mod = context.object.modifiers.new("", 'BEVEL')
332 mod.limit_method = 'WEIGHT'
333 mod.width = 0.01
334 mod.profile = 0.699099
335 mod.use_clamp_overlap = False
336 mod.segments = 3
337 mod.loop_slide = False
339 bpy.ops.object.shade_smooth()
341 context.object.data.set_sharp_from_angle(angle=1.0471975)
343 # Restore the active object
344 context.view_layer.objects.active = SavActive
347 def MoveCursor(qRot, location, self):
348 """ In brush mode : Draw a circle around the brush """
349 if qRot is not None:
350 verts = create_2d_circle(self, 10, 1)
351 self.CLR_C.clear()
352 vc = Vector()
353 for idx in range(int(len(verts) / 3)):
354 vc.x = verts[idx * 3]
355 vc.y = verts[idx * 3 + 1]
356 vc.z = verts[idx * 3 + 2]
357 vc = qRot @ vc
358 self.CLR_C.append(vc.x)
359 self.CLR_C.append(vc.y)
360 self.CLR_C.append(vc.z)
363 def rot_axis_quat(vector1, vector2):
364 """ Find the rotation (quaternion) from vector 1 to vector 2"""
365 vector1 = vector1.normalized()
366 vector2 = vector2.normalized()
367 cosTheta = vector1.dot(vector2)
368 rotationAxis = Vector((0.0, 0.0, 0.0))
369 if (cosTheta < -1 + 0.001):
370 v = Vector((0.0, 1.0, 0.0))
371 #Get the vector at the right angles to both
372 rotationAxis = vector1.cross(v)
373 rotationAxis = rotationAxis.normalized()
374 q = Quaternion()
375 q.w = 0.0
376 q.x = rotationAxis.x
377 q.y = rotationAxis.y
378 q.z = rotationAxis.z
379 else:
380 rotationAxis = vector1.cross(vector2)
381 s = math.sqrt((1.0 + cosTheta) * 2.0)
382 invs = 1 / s
383 q = Quaternion()
384 q.w = s * 0.5
385 q.x = rotationAxis.x * invs
386 q.y = rotationAxis.y * invs
387 q.z = rotationAxis.z * invs
388 return q
391 # Picking (template)
392 def Picking(context, event):
393 """ Put the 3d cursor on the closest object"""
395 # get the context arguments
396 scene = context.scene
397 region = context.region
398 rv3d = context.region_data
399 coord = event.mouse_region_x, event.mouse_region_y
401 # get the ray from the viewport and mouse
402 view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord)
403 ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord)
404 ray_target = ray_origin + view_vector
406 def visible_objects_and_duplis():
407 depsgraph = context.evaluated_depsgraph_get()
408 for dup in depsgraph.object_instances:
409 if dup.is_instance: # Real dupli instance
410 obj = dup.instance_object.original
411 yield (obj, dup.matrix.copy())
412 else: # Usual object
413 obj = dup.object.original
414 yield (obj, obj.matrix_world.copy())
416 def obj_ray_cast(obj, matrix):
417 # get the ray relative to the object
418 matrix_inv = matrix.inverted()
419 ray_origin_obj = matrix_inv @ ray_origin
420 ray_target_obj = matrix_inv @ ray_target
421 ray_direction_obj = ray_target_obj - ray_origin_obj
422 # cast the ray
423 success, location, normal, face_index = obj.ray_cast(ray_origin_obj, ray_direction_obj)
424 if success:
425 return location, normal, face_index
426 return None, None, None
428 # cast rays and find the closest object
429 best_length_squared = -1.0
430 best_obj = None
432 # cast rays and find the closest object
433 for obj, matrix in visible_objects_and_duplis():
434 if obj.type == 'MESH':
435 hit, normal, face_index = obj_ray_cast(obj, matrix)
436 if hit is not None:
437 hit_world = matrix @ hit
438 length_squared = (hit_world - ray_origin).length_squared
439 if best_obj is None or length_squared < best_length_squared:
440 scene.cursor.location = hit_world
441 best_length_squared = length_squared
442 best_obj = obj
443 else:
444 if best_obj is None:
445 depth_location = region_2d_to_vector_3d(region, rv3d, coord)
446 loc = region_2d_to_location_3d(region, rv3d, coord, depth_location)
447 scene.cursor.location = loc
450 def Pick(context, event, self, ray_max=10000.0):
451 region = context.region
452 rv3d = context.region_data
453 coord = event.mouse_region_x, event.mouse_region_y
454 view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord)
455 ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord)
456 ray_target = ray_origin + (view_vector * ray_max)
458 def obj_ray_cast(obj, matrix):
459 matrix_inv = matrix.inverted()
460 ray_origin_obj = matrix_inv @ ray_origin
461 ray_target_obj = matrix_inv @ ray_target
462 success, hit, normal, face_index = obj.ray_cast(ray_origin_obj, ray_target_obj)
463 if success:
464 return hit, normal, face_index
465 return None, None, None
467 best_length_squared = ray_max * ray_max
468 best_obj = None
469 for obj in self.CList:
470 matrix = obj.matrix_world
471 hit, normal, face_index = obj_ray_cast(obj, matrix)
472 rotation = obj.rotation_euler.to_quaternion()
473 if hit is not None:
474 hit_world = matrix @ hit
475 length_squared = (hit_world - ray_origin).length_squared
476 if length_squared < best_length_squared:
477 best_length_squared = length_squared
478 best_obj = obj
479 hits = hit_world
480 ns = normal
481 fs = face_index
483 if best_obj is not None:
484 return hits, ns, rotation
486 return None, None, None
488 def SelectObject(self, copyobj):
489 copyobj.select_set(True)
491 for child in copyobj.children:
492 SelectObject(self, child)
494 if copyobj.parent is None:
495 bpy.context.view_layer.objects.active = copyobj
497 # Undo
498 def printUndo(self):
499 for l in self.UList:
500 print(l)
503 def UndoAdd(self, type, obj):
504 """ Create a backup mesh before apply the action to the object """
505 if obj is None:
506 return
508 if type != "DUPLICATE":
509 bm = bmesh.new()
510 bm.from_mesh(obj.data)
511 self.UndoOps.append((obj, type, bm))
512 else:
513 self.UndoOps.append((obj, type, None))
516 def UndoListUpdate(self):
517 self.UList.append((self.UndoOps.copy()))
518 self.UList_Index += 1
519 self.UndoOps.clear()
522 def Undo(self):
523 if self.UList_Index < 0:
524 return
525 # get previous mesh
526 for o in self.UList[self.UList_Index]:
527 if o[1] == "MESH":
528 bm = o[2]
529 bm.to_mesh(o[0].data)
531 SelectObjList = bpy.context.selected_objects.copy()
532 Active_Obj = bpy.context.active_object
533 bpy.ops.object.select_all(action='TOGGLE')
535 for o in self.UList[self.UList_Index]:
536 if o[1] == "REBOOL":
537 o[0].select_set(True)
538 o[0].hide_viewport = False
540 if o[1] == "DUPLICATE":
541 o[0].select_set(True)
542 o[0].hide_viewport = False
544 bpy.ops.object.delete(use_global=False)
546 for so in SelectObjList:
547 bpy.data.objects[so.name].select_set(True)
548 bpy.context.view_layer.objects.active = Active_Obj
550 self.UList_Index -= 1
551 self.UList[self.UList_Index + 1:] = []
554 def duplicateObject(self):
555 if self.Instantiate:
556 bpy.ops.object.duplicate_move_linked(
557 OBJECT_OT_duplicate={
558 "linked": True,
559 "mode": 'TRANSLATION',
561 TRANSFORM_OT_translate={
562 "value": (0, 0, 0),
565 else:
566 bpy.ops.object.duplicate_move(
567 OBJECT_OT_duplicate={
568 "linked": False,
569 "mode": 'TRANSLATION',
571 TRANSFORM_OT_translate={
572 "value": (0, 0, 0),
576 ob_new = bpy.context.active_object
578 ob_new.location = self.CurLoc
579 v = Vector()
580 v.x = v.y = 0.0
581 v.z = self.BrushDepthOffset
582 ob_new.location += self.qRot * v
584 if self.ObjectMode:
585 ob_new.scale = self.ObjectBrush.scale
586 if self.ProfileMode:
587 ob_new.scale = self.ProfileBrush.scale
589 e = Euler()
590 e.x = e.y = 0.0
591 e.z = self.aRotZ / 25.0
593 # If duplicate with a grid, no random rotation (each mesh in the grid is already rotated randomly)
594 if (self.alt is True) and ((self.nbcol + self.nbrow) < 3):
595 if self.RandomRotation:
596 e.z += random.random()
598 qe = e.to_quaternion()
599 qRot = self.qRot * qe
600 ob_new.rotation_mode = 'QUATERNION'
601 ob_new.rotation_quaternion = qRot
602 ob_new.rotation_mode = 'XYZ'
604 if (ob_new.display_type == "WIRE") and (self.BrushSolidify is False):
605 ob_new.hide_viewport = True
607 if self.BrushSolidify:
608 ob_new.display_type = "SOLID"
609 ob_new.show_in_front = False
611 for o in bpy.context.selected_objects:
612 UndoAdd(self, "DUPLICATE", o)
614 if len(bpy.context.selected_objects) > 0:
615 bpy.ops.object.select_all(action='TOGGLE')
616 for o in self.all_sel_obj_list:
617 o.select_set(True)
619 bpy.context.view_layer.objects.active = self.OpsObj
622 def update_grid(self, context):
624 Thanks to batFINGER for his help :
625 source : http://blender.stackexchange.com/questions/55864/multiple-meshes-not-welded-with-pydata
627 verts = []
628 edges = []
629 faces = []
630 numface = 0
632 if self.nbcol < 1:
633 self.nbcol = 1
634 if self.nbrow < 1:
635 self.nbrow = 1
636 if self.gapx < 0:
637 self.gapx = 0
638 if self.gapy < 0:
639 self.gapy = 0
641 # Get the data from the profils or the object
642 if self.ProfileMode:
643 brush = bpy.data.objects.new(
644 self.Profils[self.nProfil][0],
645 bpy.data.meshes[self.Profils[self.nProfil][0]]
647 obj = bpy.data.objects["CT_Profil"]
648 obfaces = brush.data.polygons
649 obverts = brush.data.vertices
650 lenverts = len(obverts)
651 else:
652 brush = bpy.data.objects["CarverBrushCopy"]
653 obj = context.selected_objects[0]
654 obverts = brush.data.vertices
655 obfaces = brush.data.polygons
656 lenverts = len(brush.data.vertices)
658 # Gap between each row / column
659 gapx = self.gapx
660 gapy = self.gapy
662 # Width of each row / column
663 widthx = brush.dimensions.x * self.scale_x
664 widthy = brush.dimensions.y * self.scale_y
666 # Compute the corners so the new object will be always at the center
667 left = -((self.nbcol - 1) * (widthx + gapx)) / 2
668 start = -((self.nbrow - 1) * (widthy + gapy)) / 2
670 for i in range(self.nbrow * self.nbcol):
671 row = i % self.nbrow
672 col = i // self.nbrow
673 startx = left + ((widthx + gapx) * col)
674 starty = start + ((widthy + gapy) * row)
676 # Add random rotation
677 if (self.RandomRotation) and not (self.GridScaleX or self.GridScaleY):
678 rotmat = Matrix.Rotation(math.radians(360 * random.random()), 4, 'Z')
679 for v in obverts:
680 v.co = v.co @ rotmat
682 verts.extend([((v.co.x - startx, v.co.y - starty, v.co.z)) for v in obverts])
683 faces.extend([[v + numface * lenverts for v in p.vertices] for p in obfaces])
684 numface += 1
686 # Update the mesh
687 # Create mesh data
688 mymesh = bpy.data.meshes.new("CT_Profil")
689 # Generate mesh data
690 mymesh.from_pydata(verts, edges, faces)
691 # Calculate the edges
692 mymesh.update(calc_edges=True)
693 # Update data
694 obj.data = mymesh
695 # Make the object active to remove doubles
696 context.view_layer.objects.active = obj
699 def boolean_operation(bool_type="DIFFERENCE"):
700 ActiveObj = bpy.context.active_object
701 sel_index = 0 if bpy.context.selected_objects[0] != bpy.context.active_object else 1
703 # bpy.ops.object.modifier_apply(modifier="CT_SOLIDIFY")
704 bool_name = "CT_" + bpy.context.selected_objects[sel_index].name
705 BoolMod = ActiveObj.modifiers.new(bool_name, "BOOLEAN")
706 BoolMod.object = bpy.context.selected_objects[sel_index]
707 BoolMod.operation = bool_type
708 bpy.context.selected_objects[sel_index].display_type = 'WIRE'
709 while ActiveObj.modifiers.find(bool_name) > 0:
710 bpy.ops.object.modifier_move_up(modifier=bool_name)
713 def Rebool(context, self):
715 target_obj = context.active_object
717 Brush = context.selected_objects[1]
718 Brush.display_type = "WIRE"
720 #Deselect all
721 bpy.ops.object.select_all(action='TOGGLE')
723 target_obj.display_type = "SOLID"
724 target_obj.select_set(True)
725 bpy.ops.object.duplicate()
727 rebool_obj = context.active_object
729 m = rebool_obj.modifiers.new("CT_INTERSECT", "BOOLEAN")
730 m.operation = "INTERSECT"
731 m.object = Brush
733 m = target_obj.modifiers.new("CT_DIFFERENCE", "BOOLEAN")
734 m.operation = "DIFFERENCE"
735 m.object = Brush
737 for mb in target_obj.modifiers:
738 if mb.type == 'BEVEL':
739 mb.show_viewport = False
741 if self.ObjectBrush or self.ProfileBrush:
742 rebool_obj.show_in_front = False
743 try:
744 bpy.ops.object.modifier_apply(modifier="CT_SOLIDIFY")
745 except:
746 exc_type, exc_value, exc_traceback = sys.exc_info()
747 self.report({'ERROR'}, str(exc_value))
749 if self.dont_apply_boolean is False:
750 try:
751 bpy.ops.object.modifier_apply(modifier="CT_INTERSECT")
752 except:
753 exc_type, exc_value, exc_traceback = sys.exc_info()
754 self.report({'ERROR'}, str(exc_value))
756 bpy.ops.object.select_all(action='TOGGLE')
758 for mb in target_obj.modifiers:
759 if mb.type == 'BEVEL':
760 mb.show_viewport = True
762 context.view_layer.objects.active = target_obj
763 target_obj.select_set(True)
764 if self.dont_apply_boolean is False:
765 try:
766 bpy.ops.object.modifier_apply(modifier="CT_DIFFERENCE")
767 except:
768 exc_type, exc_value, exc_traceback = sys.exc_info()
769 self.report({'ERROR'}, str(exc_value))
771 bpy.ops.object.select_all(action='TOGGLE')
773 rebool_obj.select_set(True)
775 def createMeshFromData(self):
776 if self.Profils[self.nProfil][0] not in bpy.data.meshes:
777 # Create mesh and object
778 me = bpy.data.meshes.new(self.Profils[self.nProfil][0])
779 # Create mesh from given verts, faces.
780 me.from_pydata(self.Profils[self.nProfil][2], [], self.Profils[self.nProfil][3])
781 me.validate(verbose=True, clean_customdata=True)
782 # Update mesh with new data
783 me.update()
785 if "CT_Profil" not in bpy.data.objects:
786 ob = bpy.data.objects.new("CT_Profil", bpy.data.meshes[self.Profils[self.nProfil][0]])
787 ob.location = Vector((0.0, 0.0, 0.0))
789 # Link object to scene and make active
790 bpy.context.collection.objects.link(ob)
791 bpy.context.view_layer.update()
792 bpy.context.view_layer.objects.active = ob
793 ob.select_set(True)
794 ob.location = Vector((10000.0, 0.0, 0.0))
795 ob.display_type = "WIRE"
797 self.SolidifyPossible = True
798 else:
799 bpy.data.objects["CT_Profil"].data = bpy.data.meshes[self.Profils[self.nProfil][0]]
801 def Selection_Save_Restore(self):
802 if "CT_Profil" in bpy.data.objects:
803 Selection_Save(self)
804 bpy.ops.object.select_all(action='DESELECT')
805 bpy.data.objects["CT_Profil"].select_set(True)
806 bpy.context.view_layer.objects.active = bpy.data.objects["CT_Profil"]
807 if bpy.data.objects["CT_Profil"] in self.all_sel_obj_list:
808 self.all_sel_obj_list.remove(bpy.data.objects["CT_Profil"])
809 bpy.ops.object.delete(use_global=False)
810 Selection_Restore(self)
812 def Selection_Save(self):
813 obj_name = getattr(bpy.context.active_object, "name", None)
814 self.all_sel_obj_list = bpy.context.selected_objects.copy()
815 self.save_active_obj = obj_name
818 def Selection_Restore(self):
819 for o in self.all_sel_obj_list:
820 o.select_set(True)
821 if self.save_active_obj:
822 bpy.context.view_layer.objects.active = bpy.data.objects.get(self.save_active_obj, None)
824 def Snap_Cursor(self, context, event, mouse_pos):
825 """ Find the closest position on the overlay grid and snap the mouse on it """
826 # Get the context arguments
827 region = context.region
828 rv3d = context.region_data
830 # Get the VIEW3D area
831 for i, a in enumerate(context.screen.areas):
832 if a.type == 'VIEW_3D':
833 space = context.screen.areas[i].spaces.active
835 # Get the grid overlay for the VIEW_3D
836 grid_scale = space.overlay.grid_scale
837 grid_subdivisions = space.overlay.grid_subdivisions
839 # Use the grid scale and subdivision to get the increment
840 increment = (grid_scale / grid_subdivisions)
841 half_increment = increment / 2
843 # Convert the 2d location of the mouse in 3d
844 for index, loc in enumerate(reversed(mouse_pos)):
845 mouse_loc_3d = region_2d_to_location_3d(region, rv3d, loc, (0, 0, 0))
847 # Get the remainder from the mouse location and the ratio
848 # Test if the remainder > to the half of the increment
849 for i in range(3):
850 modulo = mouse_loc_3d[i] % increment
851 if modulo < half_increment:
852 modulo = - modulo
853 else:
854 modulo = increment - modulo
856 # Add the remainder to get the closest location on the grid
857 mouse_loc_3d[i] = mouse_loc_3d[i] + modulo
859 # Get the snapped 2d location
860 snap_loc_2d = location_3d_to_region_2d(region, rv3d, mouse_loc_3d)
862 # Replace the last mouse location by the snapped location
863 if len(self.mouse_path) > 0:
864 self.mouse_path[len(self.mouse_path) - (index + 1) ] = tuple(snap_loc_2d)
866 def mini_grid(self, context, color):
867 """ Draw a snap mini grid around the cursor based on the overlay grid"""
868 # Get the context arguments
869 region = context.region
870 rv3d = context.region_data
872 # Get the VIEW3D area
873 for i, a in enumerate(context.screen.areas):
874 if a.type == 'VIEW_3D':
875 space = context.screen.areas[i].spaces.active
876 screen_height = context.screen.areas[i].height
877 screen_width = context.screen.areas[i].width
879 #Draw the snap grid, only in ortho view
880 if not space.region_3d.is_perspective :
881 grid_scale = space.overlay.grid_scale
882 grid_subdivisions = space.overlay.grid_subdivisions
883 increment = (grid_scale / grid_subdivisions)
885 # Get the 3d location of the mouse forced to a snap value in the operator
886 mouse_coord = self.mouse_path[len(self.mouse_path) - 1]
888 snap_loc = region_2d_to_location_3d(region, rv3d, mouse_coord, (0, 0, 0))
890 # Add the increment to get the closest location on the grid
891 snap_loc[0] += increment
892 snap_loc[1] += increment
894 # Get the 2d location of the snap location
895 snap_loc = location_3d_to_region_2d(region, rv3d, snap_loc)
896 origin = location_3d_to_region_2d(region, rv3d, (0,0,0))
898 # Get the increment value
899 snap_value = snap_loc[0] - mouse_coord[0]
901 grid_coords = []
903 # Draw lines on X and Z axis from the cursor through the screen
904 grid_coords = [
905 (0, mouse_coord[1]), (screen_width, mouse_coord[1]),
906 (mouse_coord[0], 0), (mouse_coord[0], screen_height)
909 # Draw a mlini grid around the cursor to show the snap options
910 grid_coords += [
911 (mouse_coord[0] + snap_value, mouse_coord[1] + 25 + snap_value),
912 (mouse_coord[0] + snap_value, mouse_coord[1] - 25 - snap_value),
913 (mouse_coord[0] + 25 + snap_value, mouse_coord[1] + snap_value),
914 (mouse_coord[0] - 25 - snap_value, mouse_coord[1] + snap_value),
915 (mouse_coord[0] - snap_value, mouse_coord[1] + 25 + snap_value),
916 (mouse_coord[0] - snap_value, mouse_coord[1] - 25 - snap_value),
917 (mouse_coord[0] + 25 + snap_value, mouse_coord[1] - snap_value),
918 (mouse_coord[0] - 25 - snap_value, mouse_coord[1] - snap_value),
920 draw_shader(self, color, 0.3, 'LINES', grid_coords, size=2)
923 def draw_shader(self, color, alpha, type, coords, size=1, indices=None):
924 """ Create a batch for a draw type """
925 gpu.state.blend_set('ALPHA')
926 if type =='POINTS':
927 gpu.state.program_point_size_set(False)
928 gpu.state.point_size_set(size)
929 shader = gpu.shader.from_builtin('UNIFORM_COLOR')
930 else:
931 shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR')
932 shader.uniform_float("viewportSize", gpu.state.viewport_get()[2:])
933 shader.uniform_float("lineWidth", 1.0)
935 try:
936 shader.uniform_float("color", (color[0], color[1], color[2], alpha))
937 batch = batch_for_shader(shader, type, {"pos": coords}, indices=indices)
938 batch.draw(shader)
939 except:
940 exc_type, exc_value, exc_traceback = sys.exc_info()
941 self.report({'ERROR'}, str(exc_value))
943 gpu.state.point_size_set(1.0)
944 gpu.state.blend_set('NONE')