Merge branch 'blender-v2.81-release'
[blender-addons.git] / object_carver / carver_utils.py
blobce66052fe8e365f1cbd4fde34f110554963be1e6
2 import bpy
3 import bgl
4 import gpu
5 from gpu_extras.batch import batch_for_shader
6 import math
7 import sys
8 import random
9 import bmesh
10 from mathutils import (
11 Euler,
12 Matrix,
13 Vector,
14 Quaternion,
16 from mathutils.geometry import (
17 intersect_line_plane,
20 from math import (
21 sin,
22 cos,
23 pi,
26 import bpy_extras
28 from bpy_extras import view3d_utils
29 from bpy_extras.view3d_utils import (
30 region_2d_to_vector_3d,
31 region_2d_to_location_3d,
32 location_3d_to_region_2d,
35 # Cut Square
36 def CreateCutSquare(self, context):
37 """ Create a rectangle mesh """
38 far_limit = 10000.0
39 faces=[]
41 # Get the mouse coordinates
42 coord = self.mouse_path[0][0], self.mouse_path[0][1]
44 # New mesh
45 me = bpy.data.meshes.new('CMT_Square')
46 bm = bmesh.new()
47 bm.from_mesh(me)
49 # New object and link it to the scene
50 ob = bpy.data.objects.new('CMT_Square', me)
51 self.CurrentObj = ob
52 context.collection.objects.link(ob)
54 # Scene information
55 region = context.region
56 rv3d = context.region_data
57 depth_location = region_2d_to_vector_3d(region, rv3d, coord)
58 self.ViewVector = depth_location
60 # Get a point on a infinite plane and its direction
61 plane_normal = depth_location
62 plane_direction = plane_normal.normalized()
64 if self.snapCursor:
65 plane_point = context.scene.cursor.location
66 else:
67 plane_point = self.OpsObj.location if self.OpsObj is not None else Vector((0.0, 0.0, 0.0))
69 # Find the intersection of a line going thru each vertex and the infinite plane
70 for v_co in self.rectangle_coord:
71 vec = region_2d_to_vector_3d(region, rv3d, v_co)
72 p0 = region_2d_to_location_3d(region, rv3d,v_co, vec)
73 p1 = region_2d_to_location_3d(region, rv3d,v_co, vec) + plane_direction * far_limit
74 faces.append(bm.verts.new(intersect_line_plane(p0, p1, plane_point, plane_direction)))
76 # Update vertices index
77 bm.verts.index_update()
78 # New faces
79 t_face = bm.faces.new(faces)
80 # Set mesh
81 bm.to_mesh(me)
84 # Cut Line
85 def CreateCutLine(self, context):
86 """ Create a polygon mesh """
87 far_limit = 10000.0
88 vertices = []
89 faces = []
90 loc = []
92 # Get the mouse coordinates
93 coord = self.mouse_path[0][0], self.mouse_path[0][1]
95 # New mesh
96 me = bpy.data.meshes.new('CMT_Line')
97 bm = bmesh.new()
98 bm.from_mesh(me)
100 # New object and link it to the scene
101 ob = bpy.data.objects.new('CMT_Line', me)
102 self.CurrentObj = ob
103 context.collection.objects.link(ob)
105 # Scene information
106 region = context.region
107 rv3d = context.region_data
108 depth_location = region_2d_to_vector_3d(region, rv3d, coord)
109 self.ViewVector = depth_location
111 # Get a point on a infinite plane and its direction
112 plane_normal = depth_location
113 plane_direction = plane_normal.normalized()
115 if self.snapCursor:
116 plane_point = context.scene.cursor.location
117 else:
118 plane_point = self.OpsObj.location if self.OpsObj is not None else Vector((0.0, 0.0, 0.0))
120 # Use dict to remove doubles
121 # Find the intersection of a line going thru each vertex and the infinite plane
122 for idx, v_co in enumerate(list(dict.fromkeys(self.mouse_path))):
123 vec = region_2d_to_vector_3d(region, rv3d, v_co)
124 p0 = region_2d_to_location_3d(region, rv3d,v_co, vec)
125 p1 = region_2d_to_location_3d(region, rv3d,v_co, vec) + plane_direction * far_limit
126 loc.append(intersect_line_plane(p0, p1, plane_point, plane_direction))
127 vertices.append(bm.verts.new(loc[idx]))
129 if idx > 0:
130 bm.edges.new([vertices[idx-1],vertices[idx]])
132 faces.append(vertices[idx])
134 # Update vertices index
135 bm.verts.index_update()
137 # Nothing is selected, create close geometry
138 if self.CreateMode:
139 if self.Closed and len(vertices) > 1:
140 bm.edges.new([vertices[-1], vertices[0]])
141 bm.faces.new(faces)
142 else:
143 # Create faces if more than 2 vertices
144 if len(vertices) > 1 :
145 bm.edges.new([vertices[-1], vertices[0]])
146 bm.faces.new(faces)
148 bm.to_mesh(me)
150 # Cut Circle
151 def CreateCutCircle(self, context):
152 """ Create a circle mesh """
153 far_limit = 10000.0
154 FacesList = []
156 # Get the mouse coordinates
157 mouse_pos_x = self.mouse_path[0][0]
158 mouse_pos_y = self.mouse_path[0][1]
159 coord = self.mouse_path[0][0], self.mouse_path[0][1]
161 # Scene information
162 region = context.region
163 rv3d = context.region_data
164 depth_location = region_2d_to_vector_3d(region, rv3d, coord)
165 self.ViewVector = depth_location
167 # Get a point on a infinite plane and its direction
168 plane_point = context.scene.cursor.location if self.snapCursor else Vector((0.0, 0.0, 0.0))
169 plane_normal = depth_location
170 plane_direction = plane_normal.normalized()
172 # New mesh
173 me = bpy.data.meshes.new('CMT_Circle')
174 bm = bmesh.new()
175 bm.from_mesh(me)
177 # New object and link it to the scene
178 ob = bpy.data.objects.new('CMT_Circle', me)
179 self.CurrentObj = ob
180 context.collection.objects.link(ob)
182 # Create a circle using a tri fan
183 tris_fan, indices = draw_circle(self, mouse_pos_x, mouse_pos_y)
185 # Remove the vertex in the center to get the outer line of the circle
186 verts = tris_fan[1:]
188 # Find the intersection of a line going thru each vertex and the infinite plane
189 for vert in verts:
190 vec = region_2d_to_vector_3d(region, rv3d, vert)
191 p0 = region_2d_to_location_3d(region, rv3d, vert, vec)
192 p1 = p0 + plane_direction * far_limit
193 loc0 = intersect_line_plane(p0, p1, plane_point, plane_direction)
194 t_v0 = bm.verts.new(loc0)
195 FacesList.append(t_v0)
197 bm.verts.index_update()
198 bm.faces.new(FacesList)
199 bm.to_mesh(me)
202 def create_2d_circle(self, step, radius, rotation = 0):
203 """ Create the vertices of a 2d circle at (0,0) """
204 verts = []
205 for angle in range(0, 360, step):
206 verts.append(math.cos(math.radians(angle + rotation)) * radius)
207 verts.append(math.sin(math.radians(angle + rotation)) * radius)
208 verts.append(0.0)
209 verts.append(math.cos(math.radians(0.0 + rotation)) * radius)
210 verts.append(math.sin(math.radians(0.0 + rotation)) * radius)
211 verts.append(0.0)
212 return(verts)
215 def draw_circle(self, mouse_pos_x, mouse_pos_y):
216 """ Return the coordinates + indices of a circle using a triangle fan """
217 tris_verts = []
218 indices = []
219 segments = int(360 / self.stepAngle[self.step])
220 radius = self.mouse_path[1][0] - self.mouse_path[0][0]
221 rotation = (self.mouse_path[1][1] - self.mouse_path[0][1]) / 2
223 # Get the vertices of a 2d circle
224 verts = create_2d_circle(self, self.stepAngle[self.step], radius, rotation)
226 # Create the first vertex at mouse position for the center of the circle
227 tris_verts.append(Vector((mouse_pos_x + self.xpos , mouse_pos_y + self.ypos)))
229 # For each vertex of the circle, add the mouse position and the translation
230 for idx in range(int(len(verts) / 3) - 1):
231 tris_verts.append(Vector((verts[idx * 3] + mouse_pos_x + self.xpos, \
232 verts[idx * 3 + 1] + mouse_pos_y + self.ypos)))
233 i1 = idx+1
234 i2 = idx+2 if idx+2 <= segments else 1
235 indices.append((0,i1,i2))
237 return(tris_verts, indices)
239 # Object dimensions (SCULPT Tools tips)
240 def objDiagonal(obj):
241 return ((obj.dimensions[0]**2) + (obj.dimensions[1]**2) + (obj.dimensions[2]**2))**0.5
244 # Bevel Update
245 def update_bevel(context):
246 selection = context.selected_objects.copy()
247 active = context.active_object
249 if len(selection) > 0:
250 for obj in selection:
251 bpy.ops.object.select_all(action='DESELECT')
252 obj.select_set(True)
253 context.view_layer.objects.active = obj
255 # Test object name
256 # Subdive mode : Only bevel weight
257 if obj.data.name.startswith("S_") or obj.data.name.startswith("S "):
258 bpy.ops.object.mode_set(mode='EDIT')
259 bpy.ops.mesh.region_to_loop()
260 bpy.ops.transform.edge_bevelweight(value=1)
261 bpy.ops.object.mode_set(mode='OBJECT')
263 else:
264 # No subdiv mode : bevel weight + Crease + Sharp
265 CreateBevel(context, obj)
267 bpy.ops.object.select_all(action='DESELECT')
269 for obj in selection:
270 obj.select_set(True)
271 context.view_layer.objects.active = active
273 # Create bevel
274 def CreateBevel(context, CurrentObject):
275 # Save active object
276 SavActive = context.active_object
278 # Test if initial object has bevel
279 bevel_modifier = False
280 for modifier in SavActive.modifiers:
281 if modifier.name == 'Bevel':
282 bevel_modifier = True
284 if bevel_modifier:
285 # Active "CurrentObject"
286 context.view_layer.objects.active = CurrentObject
288 bpy.ops.object.mode_set(mode='EDIT')
290 # Edge mode
291 bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE')
292 # Clear all
293 bpy.ops.mesh.select_all(action='SELECT')
294 bpy.ops.mesh.mark_sharp(clear=True)
295 bpy.ops.transform.edge_crease(value=-1)
296 bpy.ops.transform.edge_bevelweight(value=-1)
298 bpy.ops.mesh.select_all(action='DESELECT')
300 # Select (in radians) all 30° sharp edges
301 bpy.ops.mesh.edges_select_sharp(sharpness=0.523599)
302 # Apply bevel weight + Crease + Sharp to the selected edges
303 bpy.ops.mesh.mark_sharp()
304 bpy.ops.transform.edge_crease(value=1)
305 bpy.ops.transform.edge_bevelweight(value=1)
307 bpy.ops.mesh.select_all(action='DESELECT')
309 bpy.ops.object.mode_set(mode='OBJECT')
311 CurrentObject.data.use_customdata_edge_bevel = True
313 for i in range(len(CurrentObject.data.edges)):
314 if CurrentObject.data.edges[i].select is True:
315 CurrentObject.data.edges[i].bevel_weight = 1.0
316 CurrentObject.data.edges[i].use_edge_sharp = True
318 bevel_modifier = False
319 for m in CurrentObject.modifiers:
320 if m.name == 'Bevel':
321 bevel_modifier = True
323 if bevel_modifier is False:
324 bpy.ops.object.modifier_add(type='BEVEL')
325 mod = context.object.modifiers[-1]
326 mod.limit_method = 'WEIGHT'
327 mod.width = 0.01
328 mod.profile = 0.699099
329 mod.use_clight_overlap = False
330 mod.segments = 3
331 mod.loop_slide = False
333 bpy.ops.object.shade_smooth()
335 context.object.data.use_auto_smooth = True
336 context.object.data.auto_smooth_angle = 1.0471975
338 # Restore the active object
339 context.view_layer.objects.active = SavActive
342 def MoveCursor(qRot, location, self):
343 """ In brush mode : Draw a circle around the brush """
344 if qRot is not None:
345 verts = create_2d_circle(self, 10, 1)
346 self.CLR_C.clear()
347 vc = Vector()
348 for idx in range(int(len(verts) / 3)):
349 vc.x = verts[idx * 3]
350 vc.y = verts[idx * 3 + 1]
351 vc.z = verts[idx * 3 + 2]
352 vc = qRot @ vc
353 self.CLR_C.append(vc.x)
354 self.CLR_C.append(vc.y)
355 self.CLR_C.append(vc.z)
358 def rot_axis_quat(vector1, vector2):
359 """ Find the rotation (quaternion) from vector 1 to vector 2"""
360 vector1 = vector1.normalized()
361 vector2 = vector2.normalized()
362 cosTheta = vector1.dot(vector2)
363 rotationAxis = Vector((0.0, 0.0, 0.0))
364 if (cosTheta < -1 + 0.001):
365 v = Vector((0.0, 1.0, 0.0))
366 #Get the vector at the right angles to both
367 rotationAxis = vector1.cross(v)
368 rotationAxis = rotationAxis.normalized()
369 q = Quaternion()
370 q.w = 0.0
371 q.x = rotationAxis.x
372 q.y = rotationAxis.y
373 q.z = rotationAxis.z
374 else:
375 rotationAxis = vector1.cross(vector2)
376 s = math.sqrt((1.0 + cosTheta) * 2.0)
377 invs = 1 / s
378 q = Quaternion()
379 q.w = s * 0.5
380 q.x = rotationAxis.x * invs
381 q.y = rotationAxis.y * invs
382 q.z = rotationAxis.z * invs
383 return q
386 # Picking (template)
387 def Picking(context, event):
388 """ Put the 3d cursor on the closest object"""
390 # get the context arguments
391 scene = context.scene
392 region = context.region
393 rv3d = context.region_data
394 coord = event.mouse_region_x, event.mouse_region_y
396 # get the ray from the viewport and mouse
397 view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord)
398 ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord)
399 ray_target = ray_origin + view_vector
401 def visible_objects_and_duplis():
402 depsgraph = context.evaluated_depsgraph_get()
403 for dup in depsgraph.object_instances:
404 if dup.is_instance: # Real dupli instance
405 obj = dup.instance_object.original
406 yield (obj, dup.matrix.copy())
407 else: # Usual object
408 obj = dup.object.original
409 yield (obj, obj.matrix_world.copy())
411 def obj_ray_cast(obj, matrix):
412 # get the ray relative to the object
413 matrix_inv = matrix.inverted()
414 ray_origin_obj = matrix_inv @ ray_origin
415 ray_target_obj = matrix_inv @ ray_target
416 ray_direction_obj = ray_target_obj - ray_origin_obj
417 # cast the ray
418 success, location, normal, face_index = obj.ray_cast(ray_origin_obj, ray_direction_obj)
419 if success:
420 return location, normal, face_index
421 return None, None, None
423 # cast rays and find the closest object
424 best_length_squared = -1.0
425 best_obj = None
427 # cast rays and find the closest object
428 for obj, matrix in visible_objects_and_duplis():
429 if obj.type == 'MESH':
430 hit, normal, face_index = obj_ray_cast(obj, matrix)
431 if hit is not None:
432 hit_world = matrix @ hit
433 length_squared = (hit_world - ray_origin).length_squared
434 if best_obj is None or length_squared < best_length_squared:
435 scene.cursor.location = hit_world
436 best_length_squared = length_squared
437 best_obj = obj
438 else:
439 if best_obj is None:
440 depth_location = region_2d_to_vector_3d(region, rv3d, coord)
441 loc = region_2d_to_location_3d(region, rv3d, coord, depth_location)
442 scene.cursor.location = loc
445 def Pick(context, event, self, ray_max=10000.0):
446 region = context.region
447 rv3d = context.region_data
448 coord = event.mouse_region_x, event.mouse_region_y
449 view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord)
450 ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord)
451 ray_target = ray_origin + (view_vector * ray_max)
453 def obj_ray_cast(obj, matrix):
454 matrix_inv = matrix.inverted()
455 ray_origin_obj = matrix_inv @ ray_origin
456 ray_target_obj = matrix_inv @ ray_target
457 success, hit, normal, face_index = obj.ray_cast(ray_origin_obj, ray_target_obj)
458 if success:
459 return hit, normal, face_index
460 return None, None, None
462 best_length_squared = ray_max * ray_max
463 best_obj = None
464 for obj in self.CList:
465 matrix = obj.matrix_world
466 hit, normal, face_index = obj_ray_cast(obj, matrix)
467 rotation = obj.rotation_euler.to_quaternion()
468 if hit is not None:
469 hit_world = matrix @ hit
470 length_squared = (hit_world - ray_origin).length_squared
471 if length_squared < best_length_squared:
472 best_length_squared = length_squared
473 best_obj = obj
474 hits = hit_world
475 ns = normal
476 fs = face_index
478 if best_obj is not None:
479 return hits, ns, rotation
481 return None, None, None
483 def SelectObject(self, copyobj):
484 copyobj.select_set(True)
486 for child in copyobj.children:
487 SelectObject(self, child)
489 if copyobj.parent is None:
490 bpy.context.view_layer.objects.active = copyobj
492 # Undo
493 def printUndo(self):
494 for l in self.UList:
495 print(l)
498 def UndoAdd(self, type, obj):
499 """ Create a backup mesh before apply the action to the object """
500 if obj is None:
501 return
503 if type != "DUPLICATE":
504 bm = bmesh.new()
505 bm.from_mesh(obj.data)
506 self.UndoOps.append((obj, type, bm))
507 else:
508 self.UndoOps.append((obj, type, None))
511 def UndoListUpdate(self):
512 self.UList.append((self.UndoOps.copy()))
513 self.UList_Index += 1
514 self.UndoOps.clear()
517 def Undo(self):
518 if self.UList_Index < 0:
519 return
520 # get previous mesh
521 for o in self.UList[self.UList_Index]:
522 if o[1] == "MESH":
523 bm = o[2]
524 bm.to_mesh(o[0].data)
526 SelectObjList = bpy.context.selected_objects.copy()
527 Active_Obj = bpy.context.active_object
528 bpy.ops.object.select_all(action='TOGGLE')
530 for o in self.UList[self.UList_Index]:
531 if o[1] == "REBOOL":
532 o[0].select_set(True)
533 o[0].hide_viewport = False
535 if o[1] == "DUPLICATE":
536 o[0].select_set(True)
537 o[0].hide_viewport = False
539 bpy.ops.object.delete(use_global=False)
541 for so in SelectObjList:
542 bpy.data.objects[so.name].select_set(True)
543 bpy.context.view_layer.objects.active = Active_Obj
545 self.UList_Index -= 1
546 self.UList[self.UList_Index + 1:] = []
549 def duplicateObject(self):
550 if self.Instantiate:
551 bpy.ops.object.duplicate_move_linked(
552 OBJECT_OT_duplicate={
553 "linked": True,
554 "mode": 'TRANSLATION',
556 TRANSFORM_OT_translate={
557 "value": (0, 0, 0),
560 else:
561 bpy.ops.object.duplicate_move(
562 OBJECT_OT_duplicate={
563 "linked": False,
564 "mode": 'TRANSLATION',
566 TRANSFORM_OT_translate={
567 "value": (0, 0, 0),
571 ob_new = bpy.context.active_object
573 ob_new.location = self.CurLoc
574 v = Vector()
575 v.x = v.y = 0.0
576 v.z = self.BrushDepthOffset
577 ob_new.location += self.qRot * v
579 if self.ObjectMode:
580 ob_new.scale = self.ObjectBrush.scale
581 if self.ProfileMode:
582 ob_new.scale = self.ProfileBrush.scale
584 e = Euler()
585 e.x = e.y = 0.0
586 e.z = self.aRotZ / 25.0
588 # If duplicate with a grid, no random rotation (each mesh in the grid is already rotated randomly)
589 if (self.alt is True) and ((self.nbcol + self.nbrow) < 3):
590 if self.RandomRotation:
591 e.z += random.random()
593 qe = e.to_quaternion()
594 qRot = self.qRot * qe
595 ob_new.rotation_mode = 'QUATERNION'
596 ob_new.rotation_quaternion = qRot
597 ob_new.rotation_mode = 'XYZ'
599 if (ob_new.display_type == "WIRE") and (self.BrushSolidify is False):
600 ob_new.hide_viewport = True
602 if self.BrushSolidify:
603 ob_new.display_type = "SOLID"
604 ob_new.show_in_front = False
606 for o in bpy.context.selected_objects:
607 UndoAdd(self, "DUPLICATE", o)
609 if len(bpy.context.selected_objects) > 0:
610 bpy.ops.object.select_all(action='TOGGLE')
611 for o in self.all_sel_obj_list:
612 o.select_set(True)
614 bpy.context.view_layer.objects.active = self.OpsObj
617 def update_grid(self, context):
619 Thanks to batFINGER for his help :
620 source : http://blender.stackexchange.com/questions/55864/multiple-meshes-not-welded-with-pydata
622 verts = []
623 edges = []
624 faces = []
625 numface = 0
627 if self.nbcol < 1:
628 self.nbcol = 1
629 if self.nbrow < 1:
630 self.nbrow = 1
631 if self.gapx < 0:
632 self.gapx = 0
633 if self.gapy < 0:
634 self.gapy = 0
636 # Get the data from the profils or the object
637 if self.ProfileMode:
638 brush = bpy.data.objects.new(
639 self.Profils[self.nProfil][0],
640 bpy.data.meshes[self.Profils[self.nProfil][0]]
642 obj = bpy.data.objects["CT_Profil"]
643 obfaces = brush.data.polygons
644 obverts = brush.data.vertices
645 lenverts = len(obverts)
646 else:
647 brush = bpy.data.objects["CarverBrushCopy"]
648 obj = context.selected_objects[0]
649 obverts = brush.data.vertices
650 obfaces = brush.data.polygons
651 lenverts = len(brush.data.vertices)
653 # Gap between each row / column
654 gapx = self.gapx
655 gapy = self.gapy
657 # Width of each row / column
658 widthx = brush.dimensions.x * self.scale_x
659 widthy = brush.dimensions.y * self.scale_y
661 # Compute the corners so the new object will be always at the center
662 left = -((self.nbcol - 1) * (widthx + gapx)) / 2
663 start = -((self.nbrow - 1) * (widthy + gapy)) / 2
665 for i in range(self.nbrow * self.nbcol):
666 row = i % self.nbrow
667 col = i // self.nbrow
668 startx = left + ((widthx + gapx) * col)
669 starty = start + ((widthy + gapy) * row)
671 # Add random rotation
672 if (self.RandomRotation) and not (self.GridScaleX or self.GridScaleY):
673 rotmat = Matrix.Rotation(math.radians(360 * random.random()), 4, 'Z')
674 for v in obverts:
675 v.co = v.co @ rotmat
677 verts.extend([((v.co.x - startx, v.co.y - starty, v.co.z)) for v in obverts])
678 faces.extend([[v + numface * lenverts for v in p.vertices] for p in obfaces])
679 numface += 1
681 # Update the mesh
682 # Create mesh data
683 mymesh = bpy.data.meshes.new("CT_Profil")
684 # Generate mesh data
685 mymesh.from_pydata(verts, edges, faces)
686 # Calculate the edges
687 mymesh.update(calc_edges=True)
688 # Update data
689 obj.data = mymesh
690 # Make the object active to remove doubles
691 context.view_layer.objects.active = obj
694 def boolean_operation(bool_type="DIFFERENCE"):
695 ActiveObj = bpy.context.active_object
696 sel_index = 0 if bpy.context.selected_objects[0] != bpy.context.active_object else 1
698 # bpy.ops.object.modifier_apply(apply_as='DATA', modifier="CT_SOLIDIFY")
699 bool_name = "CT_" + bpy.context.selected_objects[sel_index].name
700 BoolMod = ActiveObj.modifiers.new(bool_name, "BOOLEAN")
701 BoolMod.object = bpy.context.selected_objects[sel_index]
702 BoolMod.operation = bool_type
703 bpy.context.selected_objects[sel_index].display_type = 'WIRE'
704 while ActiveObj.modifiers.find(bool_name) > 0:
705 bpy.ops.object.modifier_move_up(modifier=bool_name)
708 def Rebool(context, self):
710 target_obj = context.active_object
712 Brush = context.selected_objects[1]
713 Brush.display_type = "WIRE"
715 #Deselect all
716 bpy.ops.object.select_all(action='TOGGLE')
718 target_obj.display_type = "SOLID"
719 target_obj.select_set(True)
720 bpy.ops.object.duplicate()
722 rebool_obj = context.active_object
724 m = rebool_obj.modifiers.new("CT_INTERSECT", "BOOLEAN")
725 m.operation = "INTERSECT"
726 m.object = Brush
728 m = target_obj.modifiers.new("CT_DIFFERENCE", "BOOLEAN")
729 m.operation = "DIFFERENCE"
730 m.object = Brush
732 for mb in target_obj.modifiers:
733 if mb.type == 'BEVEL':
734 mb.show_viewport = False
736 if self.ObjectBrush or self.ProfileBrush:
737 rebool_obj.show_in_front = False
738 try:
739 bpy.ops.object.modifier_apply(apply_as='DATA', modifier="CT_SOLIDIFY")
740 except:
741 exc_type, exc_value, exc_traceback = sys.exc_info()
742 self.report({'ERROR'}, str(exc_value))
744 if self.dont_apply_boolean is False:
745 try:
746 bpy.ops.object.modifier_apply(apply_as='DATA', modifier="CT_INTERSECT")
747 except:
748 exc_type, exc_value, exc_traceback = sys.exc_info()
749 self.report({'ERROR'}, str(exc_value))
751 bpy.ops.object.select_all(action='TOGGLE')
753 for mb in target_obj.modifiers:
754 if mb.type == 'BEVEL':
755 mb.show_viewport = True
757 context.view_layer.objects.active = target_obj
758 target_obj.select_set(True)
759 if self.dont_apply_boolean is False:
760 try:
761 bpy.ops.object.modifier_apply(apply_as='DATA', modifier="CT_DIFFERENCE")
762 except:
763 exc_type, exc_value, exc_traceback = sys.exc_info()
764 self.report({'ERROR'}, str(exc_value))
766 bpy.ops.object.select_all(action='TOGGLE')
768 rebool_obj.select_set(True)
770 def createMeshFromData(self):
771 if self.Profils[self.nProfil][0] not in bpy.data.meshes:
772 # Create mesh and object
773 me = bpy.data.meshes.new(self.Profils[self.nProfil][0])
774 # Create mesh from given verts, faces.
775 me.from_pydata(self.Profils[self.nProfil][2], [], self.Profils[self.nProfil][3])
776 me.validate(verbose=True, clean_customdata=True)
777 # Update mesh with new data
778 me.update()
780 if "CT_Profil" not in bpy.data.objects:
781 ob = bpy.data.objects.new("CT_Profil", bpy.data.meshes[self.Profils[self.nProfil][0]])
782 ob.location = Vector((0.0, 0.0, 0.0))
784 # Link object to scene and make active
785 bpy.context.collection.objects.link(ob)
786 bpy.context.view_layer.update()
787 bpy.context.view_layer.objects.active = ob
788 ob.select_set(True)
789 ob.location = Vector((10000.0, 0.0, 0.0))
790 ob.display_type = "WIRE"
792 self.SolidifyPossible = True
793 else:
794 bpy.data.objects["CT_Profil"].data = bpy.data.meshes[self.Profils[self.nProfil][0]]
796 def Selection_Save_Restore(self):
797 if "CT_Profil" in bpy.data.objects:
798 Selection_Save(self)
799 bpy.ops.object.select_all(action='DESELECT')
800 bpy.data.objects["CT_Profil"].select_set(True)
801 bpy.context.view_layer.objects.active = bpy.data.objects["CT_Profil"]
802 if bpy.data.objects["CT_Profil"] in self.all_sel_obj_list:
803 self.all_sel_obj_list.remove(bpy.data.objects["CT_Profil"])
804 bpy.ops.object.delete(use_global=False)
805 Selection_Restore(self)
807 def Selection_Save(self):
808 obj_name = getattr(bpy.context.active_object, "name", None)
809 self.all_sel_obj_list = bpy.context.selected_objects.copy()
810 self.save_active_obj = obj_name
813 def Selection_Restore(self):
814 for o in self.all_sel_obj_list:
815 o.select_set(True)
816 if self.save_active_obj:
817 bpy.context.view_layer.objects.active = bpy.data.objects.get(self.save_active_obj, None)
819 def Snap_Cursor(self, context, event, mouse_pos):
820 """ Find the closest position on the overlay grid and snap the mouse on it """
821 # Get the context arguments
822 region = context.region
823 rv3d = context.region_data
825 # Get the VIEW3D area
826 for i, a in enumerate(context.screen.areas):
827 if a.type == 'VIEW_3D':
828 space = context.screen.areas[i].spaces.active
830 # Get the grid overlay for the VIEW_3D
831 grid_scale = space.overlay.grid_scale
832 grid_subdivisions = space.overlay.grid_subdivisions
834 # Use the grid scale and subdivision to get the increment
835 increment = (grid_scale / grid_subdivisions)
836 half_increment = increment / 2
838 # Convert the 2d location of the mouse in 3d
839 for index, loc in enumerate(reversed(mouse_pos)):
840 mouse_loc_3d = region_2d_to_location_3d(region, rv3d, loc, (0, 0, 0))
842 # Get the remainder from the mouse location and the ratio
843 # Test if the remainder > to the half of the increment
844 for i in range(3):
845 modulo = mouse_loc_3d[i] % increment
846 if modulo < half_increment:
847 modulo = - modulo
848 else:
849 modulo = increment - modulo
851 # Add the remainder to get the closest location on the grid
852 mouse_loc_3d[i] = mouse_loc_3d[i] + modulo
854 # Get the snapped 2d location
855 snap_loc_2d = location_3d_to_region_2d(region, rv3d, mouse_loc_3d)
857 # Replace the last mouse location by the snapped location
858 if len(self.mouse_path) > 0:
859 self.mouse_path[len(self.mouse_path) - (index + 1) ] = tuple(snap_loc_2d)
861 def mini_grid(self, context, color):
862 """ Draw a snap mini grid around the cursor based on the overlay grid"""
863 # Get the context arguments
864 region = context.region
865 rv3d = context.region_data
867 # Get the VIEW3D area
868 for i, a in enumerate(context.screen.areas):
869 if a.type == 'VIEW_3D':
870 space = context.screen.areas[i].spaces.active
871 screen_height = context.screen.areas[i].height
872 screen_width = context.screen.areas[i].width
874 #Draw the snap grid, only in ortho view
875 if not space.region_3d.is_perspective :
876 grid_scale = space.overlay.grid_scale
877 grid_subdivisions = space.overlay.grid_subdivisions
878 increment = (grid_scale / grid_subdivisions)
880 # Get the 3d location of the mouse forced to a snap value in the operator
881 mouse_coord = self.mouse_path[len(self.mouse_path) - 1]
883 snap_loc = region_2d_to_location_3d(region, rv3d, mouse_coord, (0, 0, 0))
885 # Add the increment to get the closest location on the grid
886 snap_loc[0] += increment
887 snap_loc[1] += increment
889 # Get the 2d location of the snap location
890 snap_loc = location_3d_to_region_2d(region, rv3d, snap_loc)
891 origin = location_3d_to_region_2d(region, rv3d, (0,0,0))
893 # Get the increment value
894 snap_value = snap_loc[0] - mouse_coord[0]
896 grid_coords = []
898 # Draw lines on X and Z axis from the cursor through the screen
899 grid_coords = [
900 (0, mouse_coord[1]), (screen_width, mouse_coord[1]),
901 (mouse_coord[0], 0), (mouse_coord[0], screen_height)
904 # Draw a mlini grid around the cursor to show the snap options
905 grid_coords += [
906 (mouse_coord[0] + snap_value, mouse_coord[1] + 25 + snap_value),
907 (mouse_coord[0] + snap_value, mouse_coord[1] - 25 - snap_value),
908 (mouse_coord[0] + 25 + snap_value, mouse_coord[1] + snap_value),
909 (mouse_coord[0] - 25 - snap_value, mouse_coord[1] + snap_value),
910 (mouse_coord[0] - snap_value, mouse_coord[1] + 25 + snap_value),
911 (mouse_coord[0] - snap_value, mouse_coord[1] - 25 - snap_value),
912 (mouse_coord[0] + 25 + snap_value, mouse_coord[1] - snap_value),
913 (mouse_coord[0] - 25 - snap_value, mouse_coord[1] - snap_value),
915 draw_shader(self, color, 0.3, 'LINES', grid_coords, size=2)
918 def draw_shader(self, color, alpha, type, coords, size=1, indices=None):
919 """ Create a batch for a draw type """
920 bgl.glEnable(bgl.GL_BLEND)
921 bgl.glEnable(bgl.GL_LINE_SMOOTH)
922 if type =='POINTS':
923 bgl.glPointSize(size)
924 else:
925 bgl.glLineWidth(size)
926 try:
927 if len(coords[0])>2:
928 shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
929 else:
930 shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
931 batch = batch_for_shader(shader, type, {"pos": coords}, indices=indices)
932 shader.bind()
933 shader.uniform_float("color", (color[0], color[1], color[2], alpha))
934 batch.draw(shader)
935 bgl.glLineWidth(1)
936 bgl.glPointSize(1)
937 bgl.glDisable(bgl.GL_LINE_SMOOTH)
938 bgl.glDisable(bgl.GL_BLEND)
939 except:
940 exc_type, exc_value, exc_traceback = sys.exc_info()
941 self.report({'ERROR'}, str(exc_value))