Cleanup: remove "Tweak" event type
[blender-addons.git] / mesh_bsurfaces.py
blob58ddd7aaed4214692ee87fea6f15658c23a8da4d
1 # SPDX-License-Identifier: GPL-2.0-or-later
4 bl_info = {
5 "name": "Bsurfaces GPL Edition",
6 "author": "Eclectiel, Vladimir Spivak (cwolf3d)",
7 "version": (1, 8, 0),
8 "blender": (2, 80, 0),
9 "location": "View3D EditMode > Sidebar > Edit Tab",
10 "description": "Modeling and retopology tool",
11 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/bsurfaces.html",
12 "category": "Mesh",
16 import bpy
17 import bmesh
18 from bpy_extras import object_utils
20 import operator
21 from mathutils import Matrix, Vector
22 from mathutils.geometry import (
23 intersect_line_line,
24 intersect_point_line,
26 from math import (
27 degrees,
28 pi,
29 sqrt,
31 from bpy.props import (
32 BoolProperty,
33 FloatProperty,
34 IntProperty,
35 StringProperty,
36 PointerProperty,
37 EnumProperty,
38 FloatVectorProperty,
40 from bpy.types import (
41 Operator,
42 Panel,
43 PropertyGroup,
44 AddonPreferences,
47 # ----------------------------
48 # GLOBAL
49 global_shade_smooth = False
50 global_mesh_object = ""
51 global_gpencil_object = ""
52 global_curve_object = ""
54 # ----------------------------
55 # Panels
56 class VIEW3D_PT_tools_SURFSK_mesh(Panel):
57 bl_space_type = 'VIEW_3D'
58 bl_region_type = 'UI'
59 bl_category = 'Edit'
60 bl_label = "Bsurfaces"
62 def draw(self, context):
63 layout = self.layout
64 bs = context.scene.bsurfaces
66 col = layout.column(align=True)
67 row = layout.row()
68 row.separator()
69 col.operator("mesh.surfsk_init", text="Initialize (Add BSurface mesh)")
70 col.operator("mesh.surfsk_add_modifiers", text="Add Mirror and others modifiers")
72 col.label(text="Mesh of BSurface:")
73 col.prop(bs, "SURFSK_mesh", text="")
74 if bs.SURFSK_mesh != None:
75 try: mesh_object = bs.SURFSK_mesh
76 except: pass
77 try: col.prop(mesh_object.data.materials[0], "diffuse_color")
78 except: pass
79 try: col.prop(mesh_object.modifiers['Shrinkwrap'], "offset")
80 except: pass
81 try: col.prop(mesh_object, "show_in_front")
82 except: pass
83 try: col.prop(bs, "SURFSK_shade_smooth")
84 except: pass
85 try: col.prop(mesh_object, "show_wire")
86 except: pass
88 col.label(text="Guide strokes:")
89 col.row().prop(bs, "SURFSK_guide", expand=True)
90 if bs.SURFSK_guide == 'GPencil':
91 col.prop(bs, "SURFSK_gpencil", text="")
92 col.separator()
93 if bs.SURFSK_guide == 'Curve':
94 col.prop(bs, "SURFSK_curve", text="")
95 col.separator()
97 col.separator()
98 col.operator("mesh.surfsk_add_surface", text="Add Surface")
99 col.operator("mesh.surfsk_edit_surface", text="Edit Surface")
101 col.separator()
102 if bs.SURFSK_guide == 'GPencil':
103 col.operator("gpencil.surfsk_add_strokes", text="Add Strokes")
104 col.operator("gpencil.surfsk_edit_strokes", text="Edit Strokes")
105 col.separator()
106 col.operator("gpencil.surfsk_strokes_to_curves", text="Strokes to curves")
108 if bs.SURFSK_guide == 'Annotation':
109 col.operator("gpencil.surfsk_add_annotation", text="Add Annotation")
110 col.separator()
111 col.operator("gpencil.surfsk_annotations_to_curves", text="Annotation to curves")
113 if bs.SURFSK_guide == 'Curve':
114 col.operator("curve.surfsk_edit_curve", text="Edit curve")
116 col.separator()
117 col.label(text="Initial settings:")
118 col.prop(bs, "SURFSK_edges_U")
119 col.prop(bs, "SURFSK_edges_V")
120 col.prop(bs, "SURFSK_cyclic_cross")
121 col.prop(bs, "SURFSK_cyclic_follow")
122 col.prop(bs, "SURFSK_loops_on_strokes")
123 col.prop(bs, "SURFSK_automatic_join")
124 col.prop(bs, "SURFSK_keep_strokes")
126 class VIEW3D_PT_tools_SURFSK_curve(Panel):
127 bl_space_type = 'VIEW_3D'
128 bl_region_type = 'UI'
129 bl_context = "curve_edit"
130 bl_category = 'Edit'
131 bl_label = "Bsurfaces"
133 @classmethod
134 def poll(cls, context):
135 return context.active_object
137 def draw(self, context):
138 layout = self.layout
140 col = layout.column(align=True)
141 row = layout.row()
142 row.separator()
143 col.operator("curve.surfsk_first_points", text="Set First Points")
144 col.operator("curve.switch_direction", text="Switch Direction")
145 col.operator("curve.surfsk_reorder_splines", text="Reorder Splines")
148 # ----------------------------
149 # Returns the type of strokes used
150 def get_strokes_type(context):
151 strokes_type = "NO_STROKES"
152 strokes_num = 0
154 # Check if they are annotation
155 if context.scene.bsurfaces.SURFSK_guide == 'Annotation':
156 try:
157 strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
159 strokes_num = len(strokes)
161 if strokes_num > 0:
162 strokes_type = "GP_ANNOTATION"
163 except:
164 strokes_type = "NO_STROKES"
166 # Check if they are grease pencil
167 if context.scene.bsurfaces.SURFSK_guide == 'GPencil':
168 try:
169 global global_gpencil_object
170 gpencil = bpy.data.objects[global_gpencil_object]
171 strokes = gpencil.data.layers.active.active_frame.strokes
173 strokes_num = len(strokes)
175 if strokes_num > 0:
176 strokes_type = "GP_STROKES"
177 except:
178 strokes_type = "NO_STROKES"
180 # Check if they are curves, if there aren't grease pencil strokes
181 if context.scene.bsurfaces.SURFSK_guide == 'Curve':
182 try:
183 global global_curve_object
184 ob = bpy.data.objects[global_curve_object]
185 if ob.type == "CURVE":
186 strokes_type = "EXTERNAL_CURVE"
187 strokes_num = len(ob.data.splines)
189 # Check if there is any non-bezier spline
190 for i in range(len(ob.data.splines)):
191 if ob.data.splines[i].type != "BEZIER":
192 strokes_type = "CURVE_WITH_NON_BEZIER_SPLINES"
193 break
195 else:
196 strokes_type = "EXTERNAL_NO_CURVE"
197 except:
198 strokes_type = "NO_STROKES"
200 # Check if they are mesh
201 try:
202 global global_mesh_object
203 self.main_object = bpy.data.objects[global_mesh_object]
204 total_vert_sel = len([v for v in self.main_object.data.vertices if v.select])
206 # Check if there is a single stroke without any selection in the object
207 if strokes_num == 1 and total_vert_sel == 0:
208 if strokes_type == "EXTERNAL_CURVE":
209 strokes_type = "SINGLE_CURVE_STROKE_NO_SELECTION"
210 elif strokes_type == "GP_STROKES":
211 strokes_type = "SINGLE_GP_STROKE_NO_SELECTION"
213 if strokes_num == 0 and total_vert_sel > 0:
214 strokes_type = "SELECTION_ALONE"
215 except:
216 pass
218 return strokes_type
220 # ----------------------------
221 # Surface generator operator
222 class MESH_OT_SURFSK_add_surface(Operator):
223 bl_idname = "mesh.surfsk_add_surface"
224 bl_label = "Bsurfaces add surface"
225 bl_description = "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
226 bl_options = {'REGISTER', 'UNDO'}
228 is_crosshatch: BoolProperty(
229 default=False
231 is_fill_faces: BoolProperty(
232 default=False
234 selection_U_exists: BoolProperty(
235 default=False
237 selection_V_exists: BoolProperty(
238 default=False
240 selection_U2_exists: BoolProperty(
241 default=False
243 selection_V2_exists: BoolProperty(
244 default=False
246 selection_V_is_closed: BoolProperty(
247 default=False
249 selection_U_is_closed: BoolProperty(
250 default=False
252 selection_V2_is_closed: BoolProperty(
253 default=False
255 selection_U2_is_closed: BoolProperty(
256 default=False
259 edges_U: IntProperty(
260 name="Cross",
261 description="Number of face-loops crossing the strokes",
262 default=1,
263 min=1,
264 max=200
266 edges_V: IntProperty(
267 name="Follow",
268 description="Number of face-loops following the strokes",
269 default=1,
270 min=1,
271 max=200
273 cyclic_cross: BoolProperty(
274 name="Cyclic Cross",
275 description="Make cyclic the face-loops crossing the strokes",
276 default=False
278 cyclic_follow: BoolProperty(
279 name="Cyclic Follow",
280 description="Make cyclic the face-loops following the strokes",
281 default=False
283 loops_on_strokes: BoolProperty(
284 name="Loops on strokes",
285 description="Make the loops match the paths of the strokes",
286 default=False
288 automatic_join: BoolProperty(
289 name="Automatic join",
290 description="Join automatically vertices of either surfaces generated "
291 "by crosshatching, or from the borders of closed shapes",
292 default=False
294 join_stretch_factor: FloatProperty(
295 name="Stretch",
296 description="Amount of stretching or shrinking allowed for "
297 "edges when joining vertices automatically",
298 default=1,
299 min=0,
300 max=3,
301 subtype='FACTOR'
303 keep_strokes: BoolProperty(
304 name="Keep strokes",
305 description="Keeps the sketched strokes or curves after adding the surface",
306 default=False
308 strokes_type: StringProperty()
309 initial_global_undo_state: BoolProperty()
312 def draw(self, context):
313 layout = self.layout
314 col = layout.column(align=True)
315 row = layout.row()
317 if not self.is_fill_faces:
318 row.separator()
319 if not self.is_crosshatch:
320 if not self.selection_U_exists:
321 col.prop(self, "edges_U")
322 row.separator()
324 if not self.selection_V_exists:
325 col.prop(self, "edges_V")
326 row.separator()
328 row.separator()
330 if not self.selection_U_exists:
331 if not (
332 (self.selection_V_exists and not self.selection_V_is_closed) or
333 (self.selection_V2_exists and not self.selection_V2_is_closed)
335 col.prop(self, "cyclic_cross")
337 if not self.selection_V_exists:
338 if not (
339 (self.selection_U_exists and not self.selection_U_is_closed) or
340 (self.selection_U2_exists and not self.selection_U2_is_closed)
342 col.prop(self, "cyclic_follow")
344 col.prop(self, "loops_on_strokes")
346 col.prop(self, "automatic_join")
348 if self.automatic_join:
349 row.separator()
350 col.separator()
351 row.separator()
352 col.prop(self, "join_stretch_factor")
354 col.prop(self, "keep_strokes")
356 # Get an ordered list of a chain of vertices
357 def get_ordered_verts(self, ob, all_selected_edges_idx, all_selected_verts_idx,
358 first_vert_idx, middle_vertex_idx, closing_vert_idx):
359 # Order selected vertices.
360 verts_ordered = []
361 if closing_vert_idx is not None:
362 verts_ordered.append(ob.data.vertices[closing_vert_idx])
364 verts_ordered.append(ob.data.vertices[first_vert_idx])
365 prev_v = first_vert_idx
366 prev_ed = None
367 finish_while = False
368 while True:
369 edges_non_matched = 0
370 for i in all_selected_edges_idx:
371 if ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[0] == prev_v and \
372 ob.data.edges[i].vertices[1] in all_selected_verts_idx:
374 verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[1]])
375 prev_v = ob.data.edges[i].vertices[1]
376 prev_ed = ob.data.edges[i]
377 elif ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[1] == prev_v and \
378 ob.data.edges[i].vertices[0] in all_selected_verts_idx:
380 verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[0]])
381 prev_v = ob.data.edges[i].vertices[0]
382 prev_ed = ob.data.edges[i]
383 else:
384 edges_non_matched += 1
386 if edges_non_matched == len(all_selected_edges_idx):
387 finish_while = True
389 if finish_while:
390 break
392 if closing_vert_idx is not None:
393 verts_ordered.append(ob.data.vertices[closing_vert_idx])
395 if middle_vertex_idx is not None:
396 verts_ordered.append(ob.data.vertices[middle_vertex_idx])
397 verts_ordered.reverse()
399 return tuple(verts_ordered)
401 # Calculates length of a chain of points.
402 def get_chain_length(self, object, verts_ordered):
403 matrix = object.matrix_world
405 edges_lengths = []
406 edges_lengths_sum = 0
407 for i in range(0, len(verts_ordered)):
408 if i == 0:
409 prev_v_co = matrix @ verts_ordered[i].co
410 else:
411 v_co = matrix @ verts_ordered[i].co
413 v_difs = [prev_v_co[0] - v_co[0], prev_v_co[1] - v_co[1], prev_v_co[2] - v_co[2]]
414 edge_length = abs(sqrt(v_difs[0] * v_difs[0] + v_difs[1] * v_difs[1] + v_difs[2] * v_difs[2]))
416 edges_lengths.append(edge_length)
417 edges_lengths_sum += edge_length
419 prev_v_co = v_co
421 return edges_lengths, edges_lengths_sum
423 # Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
424 def get_edges_proportions(self, edges_lengths, edges_lengths_sum, use_boundaries, fixed_edges_num):
425 edges_proportions = []
426 if use_boundaries:
427 verts_count = 1
428 for l in edges_lengths:
429 edges_proportions.append(l / edges_lengths_sum)
430 verts_count += 1
431 else:
432 verts_count = 1
433 for _n in range(0, fixed_edges_num):
434 edges_proportions.append(1 / fixed_edges_num)
435 verts_count += 1
437 return edges_proportions
439 # Calculates the angle between two pairs of points in space
440 def orientation_difference(self, points_A_co, points_B_co):
441 # each parameter should be a list with two elements,
442 # and each element should be a x,y,z coordinate
443 vec_A = points_A_co[0] - points_A_co[1]
444 vec_B = points_B_co[0] - points_B_co[1]
446 angle = vec_A.angle(vec_B)
448 if angle > 0.5 * pi:
449 angle = abs(angle - pi)
451 return angle
453 # Calculate the which vert of verts_idx list is the nearest one
454 # to the point_co coordinates, and the distance
455 def shortest_distance(self, object, point_co, verts_idx):
456 matrix = object.matrix_world
458 for i in range(0, len(verts_idx)):
459 dist = (point_co - matrix @ object.data.vertices[verts_idx[i]].co).length
460 if i == 0:
461 prev_dist = dist
462 nearest_vert_idx = verts_idx[i]
463 shortest_dist = dist
465 if dist < prev_dist:
466 prev_dist = dist
467 nearest_vert_idx = verts_idx[i]
468 shortest_dist = dist
470 return nearest_vert_idx, shortest_dist
472 # Returns the index of the opposite vert tip in a chain, given a vert tip index
473 # as parameter, and a multidimentional list with all pairs of tips
474 def opposite_tip(self, vert_tip_idx, all_chains_tips_idx):
475 opposite_vert_tip_idx = None
476 for i in range(0, len(all_chains_tips_idx)):
477 if vert_tip_idx == all_chains_tips_idx[i][0]:
478 opposite_vert_tip_idx = all_chains_tips_idx[i][1]
479 if vert_tip_idx == all_chains_tips_idx[i][1]:
480 opposite_vert_tip_idx = all_chains_tips_idx[i][0]
482 return opposite_vert_tip_idx
484 # Simplifies a spline and returns the new points coordinates
485 def simplify_spline(self, spline_coords, segments_num):
486 simplified_spline = []
487 points_between_segments = round(len(spline_coords) / segments_num)
489 simplified_spline.append(spline_coords[0])
490 for i in range(1, segments_num):
491 simplified_spline.append(spline_coords[i * points_between_segments])
493 simplified_spline.append(spline_coords[len(spline_coords) - 1])
495 return simplified_spline
497 # Returns a list with the coords of the points distributed over the splines
498 # passed to this method according to the proportions parameter
499 def distribute_pts(self, surface_splines, proportions):
501 # Calculate the length of each final surface spline
502 surface_splines_lengths = []
503 surface_splines_parsed = []
505 for sp_idx in range(0, len(surface_splines)):
506 # Calculate spline length
507 surface_splines_lengths.append(0)
509 for i in range(0, len(surface_splines[sp_idx].bezier_points)):
510 if i == 0:
511 prev_p = surface_splines[sp_idx].bezier_points[i]
512 else:
513 p = surface_splines[sp_idx].bezier_points[i]
514 edge_length = (prev_p.co - p.co).length
515 surface_splines_lengths[sp_idx] += edge_length
517 prev_p = p
519 # Calculate vertex positions with appropriate edge proportions, and ordered, for each spline
520 for sp_idx in range(0, len(surface_splines)):
521 surface_splines_parsed.append([])
522 surface_splines_parsed[sp_idx].append(surface_splines[sp_idx].bezier_points[0].co)
524 prev_p_co = surface_splines[sp_idx].bezier_points[0].co
525 p_idx = 0
527 for prop_idx in range(len(proportions) - 1):
528 target_length = surface_splines_lengths[sp_idx] * proportions[prop_idx]
529 partial_segment_length = 0
530 finish_while = False
532 while True:
533 # if not it'll pass the p_idx as an index below and crash
534 if p_idx < len(surface_splines[sp_idx].bezier_points):
535 p_co = surface_splines[sp_idx].bezier_points[p_idx].co
536 new_dist = (prev_p_co - p_co).length
538 # The new distance that could have the partial segment if
539 # it is still shorter than the target length
540 potential_segment_length = partial_segment_length + new_dist
542 # If the potential is still shorter, keep adding
543 if potential_segment_length < target_length:
544 partial_segment_length = potential_segment_length
546 p_idx += 1
547 prev_p_co = p_co
549 # If the potential is longer than the target, calculate the target
550 # (a point between the last two points), and assign
551 elif potential_segment_length > target_length:
552 remaining_dist = target_length - partial_segment_length
553 vec = p_co - prev_p_co
554 vec.normalize()
555 intermediate_co = prev_p_co + (vec * remaining_dist)
557 surface_splines_parsed[sp_idx].append(intermediate_co)
559 partial_segment_length += remaining_dist
560 prev_p_co = intermediate_co
562 finish_while = True
564 # If the potential is equal to the target, assign
565 elif potential_segment_length == target_length:
566 surface_splines_parsed[sp_idx].append(p_co)
567 prev_p_co = p_co
569 finish_while = True
571 if finish_while:
572 break
574 # last point of the spline
575 surface_splines_parsed[sp_idx].append(
576 surface_splines[sp_idx].bezier_points[len(surface_splines[sp_idx].bezier_points) - 1].co
579 return surface_splines_parsed
581 # Counts the number of faces that belong to each edge
582 def edge_face_count(self, ob):
583 ed_keys_count_dict = {}
585 for face in ob.data.polygons:
586 for ed_keys in face.edge_keys:
587 if ed_keys not in ed_keys_count_dict:
588 ed_keys_count_dict[ed_keys] = 1
589 else:
590 ed_keys_count_dict[ed_keys] += 1
592 edge_face_count = []
593 for i in range(len(ob.data.edges)):
594 edge_face_count.append(0)
596 for i in range(len(ob.data.edges)):
597 ed = ob.data.edges[i]
599 v1 = ed.vertices[0]
600 v2 = ed.vertices[1]
602 if (v1, v2) in ed_keys_count_dict:
603 edge_face_count[i] = ed_keys_count_dict[(v1, v2)]
604 elif (v2, v1) in ed_keys_count_dict:
605 edge_face_count[i] = ed_keys_count_dict[(v2, v1)]
607 return edge_face_count
609 # Fills with faces all the selected vertices which form empty triangles or quads
610 def fill_with_faces(self, object):
611 all_selected_verts_count = self.main_object_selected_verts_count
613 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
615 # Calculate average length of selected edges
616 all_selected_verts = []
617 original_sel_edges_count = 0
618 for ed in object.data.edges:
619 if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
620 coords = []
621 coords.append(object.data.vertices[ed.vertices[0]].co)
622 coords.append(object.data.vertices[ed.vertices[1]].co)
624 original_sel_edges_count += 1
626 if not ed.vertices[0] in all_selected_verts:
627 all_selected_verts.append(ed.vertices[0])
629 if not ed.vertices[1] in all_selected_verts:
630 all_selected_verts.append(ed.vertices[1])
632 tuple(all_selected_verts)
634 # Check if there is any edge selected. If not, interrupt the script
635 if original_sel_edges_count == 0 and all_selected_verts_count > 0:
636 return 0
638 # Get all edges connected to selected verts
639 all_edges_around_sel_verts = []
640 edges_connected_to_sel_verts = {}
641 verts_connected_to_every_vert = {}
642 for ed_idx in range(len(object.data.edges)):
643 ed = object.data.edges[ed_idx]
644 include_edge = False
646 if ed.vertices[0] in all_selected_verts:
647 if not ed.vertices[0] in edges_connected_to_sel_verts:
648 edges_connected_to_sel_verts[ed.vertices[0]] = []
650 edges_connected_to_sel_verts[ed.vertices[0]].append(ed_idx)
651 include_edge = True
653 if ed.vertices[1] in all_selected_verts:
654 if not ed.vertices[1] in edges_connected_to_sel_verts:
655 edges_connected_to_sel_verts[ed.vertices[1]] = []
657 edges_connected_to_sel_verts[ed.vertices[1]].append(ed_idx)
658 include_edge = True
660 if include_edge is True:
661 all_edges_around_sel_verts.append(ed_idx)
663 # Get all connected verts to each vert
664 if not ed.vertices[0] in verts_connected_to_every_vert:
665 verts_connected_to_every_vert[ed.vertices[0]] = []
667 if not ed.vertices[1] in verts_connected_to_every_vert:
668 verts_connected_to_every_vert[ed.vertices[1]] = []
670 verts_connected_to_every_vert[ed.vertices[0]].append(ed.vertices[1])
671 verts_connected_to_every_vert[ed.vertices[1]].append(ed.vertices[0])
673 # Get all verts connected to faces
674 all_verts_part_of_faces = []
675 all_edges_faces_count = []
676 all_edges_faces_count += self.edge_face_count(object)
678 # Get only the selected edges that have faces attached.
679 count_faces_of_edges_around_sel_verts = {}
680 selected_verts_with_faces = []
681 for ed_idx in all_edges_around_sel_verts:
682 count_faces_of_edges_around_sel_verts[ed_idx] = all_edges_faces_count[ed_idx]
684 if all_edges_faces_count[ed_idx] > 0:
685 ed = object.data.edges[ed_idx]
687 if not ed.vertices[0] in selected_verts_with_faces:
688 selected_verts_with_faces.append(ed.vertices[0])
690 if not ed.vertices[1] in selected_verts_with_faces:
691 selected_verts_with_faces.append(ed.vertices[1])
693 all_verts_part_of_faces.append(ed.vertices[0])
694 all_verts_part_of_faces.append(ed.vertices[1])
696 tuple(selected_verts_with_faces)
698 # Discard unneeded verts from calculations
699 participating_verts = []
700 movable_verts = []
701 for v_idx in all_selected_verts:
702 vert_has_edges_with_one_face = False
704 # Check if the actual vert has at least one edge connected to only one face
705 for ed_idx in edges_connected_to_sel_verts[v_idx]:
706 if count_faces_of_edges_around_sel_verts[ed_idx] == 1:
707 vert_has_edges_with_one_face = True
709 # If the vert has two or less edges connected and the vert is not part of any face.
710 # Or the vert is part of any face and at least one of
711 # the connected edges has only one face attached to it.
712 if (len(edges_connected_to_sel_verts[v_idx]) == 2 and
713 v_idx not in all_verts_part_of_faces) or \
714 len(edges_connected_to_sel_verts[v_idx]) == 1 or \
715 (v_idx in all_verts_part_of_faces and
716 vert_has_edges_with_one_face):
718 participating_verts.append(v_idx)
720 if v_idx not in all_verts_part_of_faces:
721 movable_verts.append(v_idx)
723 # Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
724 for mv_idx in movable_verts:
725 freeze_vert = False
726 mv_connected_verts = verts_connected_to_every_vert[mv_idx]
728 for actual_v_idx in all_selected_verts:
729 count_shared_neighbors = 0
730 checked_verts = []
732 for mv_conn_v_idx in mv_connected_verts:
733 if mv_idx != actual_v_idx:
734 if mv_conn_v_idx in verts_connected_to_every_vert[actual_v_idx] and \
735 mv_conn_v_idx not in checked_verts:
736 count_shared_neighbors += 1
737 checked_verts.append(mv_conn_v_idx)
739 if actual_v_idx in mv_connected_verts:
740 freeze_vert = True
741 break
743 if count_shared_neighbors == 2:
744 freeze_vert = True
745 break
747 if freeze_vert:
748 break
750 if freeze_vert:
751 movable_verts.remove(mv_idx)
753 # Calculate merge distance for participating verts
754 shortest_edge_length = None
755 for ed in object.data.edges:
756 if ed.vertices[0] in movable_verts and ed.vertices[1] in movable_verts:
757 v1 = object.data.vertices[ed.vertices[0]]
758 v2 = object.data.vertices[ed.vertices[1]]
760 length = (v1.co - v2.co).length
762 if shortest_edge_length is None:
763 shortest_edge_length = length
764 else:
765 if length < shortest_edge_length:
766 shortest_edge_length = length
768 if shortest_edge_length is not None:
769 edges_merge_distance = shortest_edge_length * 0.5
770 else:
771 edges_merge_distance = 0
773 # Get together the verts near enough. They will be merged later
774 remaining_verts = []
775 remaining_verts += participating_verts
776 for v1_idx in participating_verts:
777 if v1_idx in remaining_verts and v1_idx in movable_verts:
778 verts_to_merge = []
779 coords_verts_to_merge = {}
781 verts_to_merge.append(v1_idx)
783 v1_co = object.data.vertices[v1_idx].co
784 coords_verts_to_merge[v1_idx] = (v1_co[0], v1_co[1], v1_co[2])
786 for v2_idx in remaining_verts:
787 if v1_idx != v2_idx:
788 v2_co = object.data.vertices[v2_idx].co
790 dist = (v1_co - v2_co).length
792 if dist <= edges_merge_distance: # Add the verts which are near enough
793 verts_to_merge.append(v2_idx)
795 coords_verts_to_merge[v2_idx] = (v2_co[0], v2_co[1], v2_co[2])
797 for vm_idx in verts_to_merge:
798 remaining_verts.remove(vm_idx)
800 if len(verts_to_merge) > 1:
801 # Calculate middle point of the verts to merge.
802 sum_x_co = 0
803 sum_y_co = 0
804 sum_z_co = 0
805 movable_verts_to_merge_count = 0
806 for i in range(len(verts_to_merge)):
807 if verts_to_merge[i] in movable_verts:
808 v_co = object.data.vertices[verts_to_merge[i]].co
810 sum_x_co += v_co[0]
811 sum_y_co += v_co[1]
812 sum_z_co += v_co[2]
814 movable_verts_to_merge_count += 1
816 middle_point_co = [
817 sum_x_co / movable_verts_to_merge_count,
818 sum_y_co / movable_verts_to_merge_count,
819 sum_z_co / movable_verts_to_merge_count
822 # Check if any vert to be merged is not movable
823 shortest_dist = None
824 are_verts_not_movable = False
825 verts_not_movable = []
826 for v_merge_idx in verts_to_merge:
827 if v_merge_idx in participating_verts and v_merge_idx not in movable_verts:
828 are_verts_not_movable = True
829 verts_not_movable.append(v_merge_idx)
831 if are_verts_not_movable:
832 # Get the vert connected to faces, that is nearest to
833 # the middle point of the movable verts
834 shortest_dist = None
835 for vcf_idx in verts_not_movable:
836 dist = abs((object.data.vertices[vcf_idx].co -
837 Vector(middle_point_co)).length)
839 if shortest_dist is None:
840 shortest_dist = dist
841 nearest_vert_idx = vcf_idx
842 else:
843 if dist < shortest_dist:
844 shortest_dist = dist
845 nearest_vert_idx = vcf_idx
847 coords = object.data.vertices[nearest_vert_idx].co
848 target_point_co = [coords[0], coords[1], coords[2]]
849 else:
850 target_point_co = middle_point_co
852 # Move verts to merge to the middle position
853 for v_merge_idx in verts_to_merge:
854 if v_merge_idx in movable_verts: # Only move the verts that are not part of faces
855 object.data.vertices[v_merge_idx].co[0] = target_point_co[0]
856 object.data.vertices[v_merge_idx].co[1] = target_point_co[1]
857 object.data.vertices[v_merge_idx].co[2] = target_point_co[2]
859 # Perform "Remove Doubles" to weld all the disconnected verts
860 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
861 bpy.ops.mesh.remove_doubles(threshold=0.0001)
863 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
865 # Get all the definitive selected edges, after weldding
866 selected_edges = []
867 edges_per_vert = {} # Number of faces of each selected edge
868 for ed in object.data.edges:
869 if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
870 selected_edges.append(ed.index)
872 # Save all the edges that belong to each vertex.
873 if not ed.vertices[0] in edges_per_vert:
874 edges_per_vert[ed.vertices[0]] = []
876 if not ed.vertices[1] in edges_per_vert:
877 edges_per_vert[ed.vertices[1]] = []
879 edges_per_vert[ed.vertices[0]].append(ed.index)
880 edges_per_vert[ed.vertices[1]].append(ed.index)
882 # Check if all the edges connected to each vert have two faces attached to them.
883 # To discard them later and make calculations faster
884 a = []
885 a += self.edge_face_count(object)
886 tuple(a)
887 verts_surrounded_by_faces = {}
888 for v_idx in edges_per_vert:
889 edges_with_two_faces_count = 0
891 for ed_idx in edges_per_vert[v_idx]:
892 if a[ed_idx] == 2:
893 edges_with_two_faces_count += 1
895 if edges_with_two_faces_count == len(edges_per_vert[v_idx]):
896 verts_surrounded_by_faces[v_idx] = True
897 else:
898 verts_surrounded_by_faces[v_idx] = False
900 # Get all the selected vertices
901 selected_verts_idx = []
902 for v in object.data.vertices:
903 if v.select:
904 selected_verts_idx.append(v.index)
906 # Get all the faces of the object
907 all_object_faces_verts_idx = []
908 for face in object.data.polygons:
909 face_verts = []
910 face_verts.append(face.vertices[0])
911 face_verts.append(face.vertices[1])
912 face_verts.append(face.vertices[2])
914 if len(face.vertices) == 4:
915 face_verts.append(face.vertices[3])
917 all_object_faces_verts_idx.append(face_verts)
919 # Deselect all vertices
920 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
921 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
922 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
924 # Make a dictionary with the verts related to each vert
925 related_key_verts = {}
926 for ed_idx in selected_edges:
927 ed = object.data.edges[ed_idx]
929 if not verts_surrounded_by_faces[ed.vertices[0]]:
930 if not ed.vertices[0] in related_key_verts:
931 related_key_verts[ed.vertices[0]] = []
933 if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
934 related_key_verts[ed.vertices[0]].append(ed.vertices[1])
936 if not verts_surrounded_by_faces[ed.vertices[1]]:
937 if not ed.vertices[1] in related_key_verts:
938 related_key_verts[ed.vertices[1]] = []
940 if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
941 related_key_verts[ed.vertices[1]].append(ed.vertices[0])
943 # Get groups of verts forming each face
944 faces_verts_idx = []
945 for v1 in related_key_verts: # verts-1 ....
946 for v2 in related_key_verts: # verts-2
947 if v1 != v2:
948 related_verts_in_common = []
949 v2_in_rel_v1 = False
950 v1_in_rel_v2 = False
951 for rel_v1 in related_key_verts[v1]:
952 # Check if related verts of verts-1 are related verts of verts-2
953 if rel_v1 in related_key_verts[v2]:
954 related_verts_in_common.append(rel_v1)
956 if v2 in related_key_verts[v1]:
957 v2_in_rel_v1 = True
959 if v1 in related_key_verts[v2]:
960 v1_in_rel_v2 = True
962 repeated_face = False
963 # If two verts have two related verts in common, they form a quad
964 if len(related_verts_in_common) == 2:
965 # Check if the face is already saved
966 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
968 for f_verts in all_faces_to_check_idx:
969 repeated_verts = 0
971 if len(f_verts) == 4:
972 if v1 in f_verts:
973 repeated_verts += 1
974 if v2 in f_verts:
975 repeated_verts += 1
976 if related_verts_in_common[0] in f_verts:
977 repeated_verts += 1
978 if related_verts_in_common[1] in f_verts:
979 repeated_verts += 1
981 if repeated_verts == len(f_verts):
982 repeated_face = True
983 break
985 if not repeated_face:
986 faces_verts_idx.append(
987 [v1, related_verts_in_common[0], v2, related_verts_in_common[1]]
990 # If Two verts have one related vert in common and
991 # they are related to each other, they form a triangle
992 elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
993 # Check if the face is already saved.
994 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
996 for f_verts in all_faces_to_check_idx:
997 repeated_verts = 0
999 if len(f_verts) == 3:
1000 if v1 in f_verts:
1001 repeated_verts += 1
1002 if v2 in f_verts:
1003 repeated_verts += 1
1004 if related_verts_in_common[0] in f_verts:
1005 repeated_verts += 1
1007 if repeated_verts == len(f_verts):
1008 repeated_face = True
1009 break
1011 if not repeated_face:
1012 faces_verts_idx.append([v1, related_verts_in_common[0], v2])
1014 # Keep only the faces that don't overlap by ignoring quads
1015 # that overlap with two adjacent triangles
1016 faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate
1017 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
1018 for i in range(len(faces_verts_idx)):
1019 for t in range(len(all_faces_to_check_idx)):
1020 if i != t:
1021 verts_in_common = 0
1023 if len(faces_verts_idx[i]) == 4 and len(all_faces_to_check_idx[t]) == 3:
1024 for v_idx in all_faces_to_check_idx[t]:
1025 if v_idx in faces_verts_idx[i]:
1026 verts_in_common += 1
1027 # If it doesn't have all it's vertices repeated in the other face
1028 if verts_in_common == 3:
1029 if i not in faces_to_not_include_idx:
1030 faces_to_not_include_idx.append(i)
1032 # Build faces discarding the ones in faces_to_not_include
1033 me = object.data
1034 bm = bmesh.new()
1035 bm.from_mesh(me)
1037 num_faces_created = 0
1038 for i in range(len(faces_verts_idx)):
1039 if i not in faces_to_not_include_idx:
1040 bm.faces.new([bm.verts[v] for v in faces_verts_idx[i]])
1042 num_faces_created += 1
1044 bm.to_mesh(me)
1045 bm.free()
1047 for v_idx in selected_verts_idx:
1048 self.main_object.data.vertices[v_idx].select = True
1050 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
1051 bpy.ops.mesh.normals_make_consistent(inside=False)
1052 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
1054 self.update()
1056 return num_faces_created
1058 # Crosshatch skinning
1059 def crosshatch_surface_invoke(self, ob_original_splines):
1060 self.is_crosshatch = False
1061 self.crosshatch_merge_distance = 0
1063 objects_to_delete = [] # duplicated strokes to be deleted.
1065 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1066 # (without this the surface verts merging with the main object doesn't work well)
1067 self.modifiers_prev_viewport_state = []
1068 if len(self.main_object.modifiers) > 0:
1069 for m_idx in range(len(self.main_object.modifiers)):
1070 self.modifiers_prev_viewport_state.append(
1071 self.main_object.modifiers[m_idx].show_viewport
1073 self.main_object.modifiers[m_idx].show_viewport = False
1075 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1076 ob_original_splines.select_set(True)
1077 bpy.context.view_layer.objects.active = ob_original_splines
1079 if len(ob_original_splines.data.splines) >= 2:
1080 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1081 ob_splines = bpy.context.object
1082 ob_splines.name = "SURFSKIO_NE_STR"
1084 # Get estimative merge distance (sum up the distances from the first point to
1085 # all other points, then average them and then divide them)
1086 first_point_dist_sum = 0
1087 first_dist = 0
1088 second_dist = 0
1089 coords_first_pt = ob_splines.data.splines[0].bezier_points[0].co
1090 for i in range(len(ob_splines.data.splines)):
1091 sp = ob_splines.data.splines[i]
1093 if coords_first_pt != sp.bezier_points[0].co:
1094 first_dist = (coords_first_pt - sp.bezier_points[0].co).length
1096 if coords_first_pt != sp.bezier_points[len(sp.bezier_points) - 1].co:
1097 second_dist = (coords_first_pt - sp.bezier_points[len(sp.bezier_points) - 1].co).length
1099 first_point_dist_sum += first_dist + second_dist
1101 if i == 0:
1102 if first_dist != 0:
1103 shortest_dist = first_dist
1104 elif second_dist != 0:
1105 shortest_dist = second_dist
1107 if shortest_dist > first_dist and first_dist != 0:
1108 shortest_dist = first_dist
1110 if shortest_dist > second_dist and second_dist != 0:
1111 shortest_dist = second_dist
1113 self.crosshatch_merge_distance = shortest_dist / 20
1115 # Recalculation of merge distance
1117 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1119 ob_calc_merge_dist = bpy.context.object
1120 ob_calc_merge_dist.name = "SURFSKIO_CALC_TMP"
1122 objects_to_delete.append(ob_calc_merge_dist)
1124 # Smooth out strokes a little to improve crosshatch detection
1125 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1126 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
1128 for i in range(4):
1129 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1131 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1132 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1134 # Convert curves into mesh
1135 ob_calc_merge_dist.data.resolution_u = 12
1136 bpy.ops.object.convert(target='MESH', keep_original=False)
1138 # Find "intersection-nodes"
1139 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1140 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1141 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
1142 threshold=self.crosshatch_merge_distance)
1143 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
1144 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1146 # Remove verts with less than three edges
1147 verts_edges_count = {}
1148 for ed in ob_calc_merge_dist.data.edges:
1149 v = ed.vertices
1151 if v[0] not in verts_edges_count:
1152 verts_edges_count[v[0]] = 0
1154 if v[1] not in verts_edges_count:
1155 verts_edges_count[v[1]] = 0
1157 verts_edges_count[v[0]] += 1
1158 verts_edges_count[v[1]] += 1
1160 nodes_verts_coords = []
1161 for v_idx in verts_edges_count:
1162 v = ob_calc_merge_dist.data.vertices[v_idx]
1164 if verts_edges_count[v_idx] < 3:
1165 v.select = True
1167 # Remove them
1168 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1169 bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
1170 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1172 # Remove doubles to discard very near verts from calculations of distance
1173 bpy.ops.mesh.remove_doubles(
1174 'INVOKE_REGION_WIN',
1175 threshold=self.crosshatch_merge_distance * 4.0
1177 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1179 # Get all coords of the resulting nodes
1180 nodes_verts_coords = [(v.co[0], v.co[1], v.co[2]) for
1181 v in ob_calc_merge_dist.data.vertices]
1183 # Check if the strokes are a crosshatch
1184 if len(nodes_verts_coords) >= 3:
1185 self.is_crosshatch = True
1187 shortest_dist = None
1188 for co_1 in nodes_verts_coords:
1189 for co_2 in nodes_verts_coords:
1190 if co_1 != co_2:
1191 dist = (Vector(co_1) - Vector(co_2)).length
1193 if shortest_dist is not None:
1194 if dist < shortest_dist:
1195 shortest_dist = dist
1196 else:
1197 shortest_dist = dist
1199 self.crosshatch_merge_distance = shortest_dist / 3
1201 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1202 ob_splines.select_set(True)
1203 bpy.context.view_layer.objects.active = ob_splines
1205 # Deselect all points
1206 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1207 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1208 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1210 # Smooth splines in a localized way, to eliminate "saw-teeth"
1211 # like shapes when there are many points
1212 for sp in ob_splines.data.splines:
1213 angle_sum = 0
1215 angle_limit = 2 # Degrees
1216 for t in range(len(sp.bezier_points)):
1217 # Because on each iteration it checks the "next two points"
1218 # of the actual. This way it doesn't go out of range
1219 if t <= len(sp.bezier_points) - 3:
1220 p1 = sp.bezier_points[t]
1221 p2 = sp.bezier_points[t + 1]
1222 p3 = sp.bezier_points[t + 2]
1224 vec_1 = p1.co - p2.co
1225 vec_2 = p2.co - p3.co
1227 if p2.co != p1.co and p2.co != p3.co:
1228 angle = vec_1.angle(vec_2)
1229 angle_sum += degrees(angle)
1231 if angle_sum >= angle_limit: # If sum of angles is grater than the limit
1232 if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
1233 p1.select_control_point = True
1234 p1.select_left_handle = True
1235 p1.select_right_handle = True
1237 p2.select_control_point = True
1238 p2.select_left_handle = True
1239 p2.select_right_handle = True
1241 if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
1242 p3.select_control_point = True
1243 p3.select_left_handle = True
1244 p3.select_right_handle = True
1246 angle_sum = 0
1248 sp.bezier_points[0].select_control_point = False
1249 sp.bezier_points[0].select_left_handle = False
1250 sp.bezier_points[0].select_right_handle = False
1252 sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = False
1253 sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = False
1254 sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = False
1256 # Smooth out strokes a little to improve crosshatch detection
1257 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1259 for i in range(15):
1260 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1262 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1263 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1265 # Simplify the splines
1266 for sp in ob_splines.data.splines:
1267 angle_sum = 0
1269 sp.bezier_points[0].select_control_point = True
1270 sp.bezier_points[0].select_left_handle = True
1271 sp.bezier_points[0].select_right_handle = True
1273 sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = True
1274 sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = True
1275 sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = True
1277 angle_limit = 15 # Degrees
1278 for t in range(len(sp.bezier_points)):
1279 # Because on each iteration it checks the "next two points"
1280 # of the actual. This way it doesn't go out of range
1281 if t <= len(sp.bezier_points) - 3:
1282 p1 = sp.bezier_points[t]
1283 p2 = sp.bezier_points[t + 1]
1284 p3 = sp.bezier_points[t + 2]
1286 vec_1 = p1.co - p2.co
1287 vec_2 = p2.co - p3.co
1289 if p2.co != p1.co and p2.co != p3.co:
1290 angle = vec_1.angle(vec_2)
1291 angle_sum += degrees(angle)
1292 # If sum of angles is grater than the limit
1293 if angle_sum >= angle_limit:
1294 p1.select_control_point = True
1295 p1.select_left_handle = True
1296 p1.select_right_handle = True
1298 p2.select_control_point = True
1299 p2.select_left_handle = True
1300 p2.select_right_handle = True
1302 p3.select_control_point = True
1303 p3.select_left_handle = True
1304 p3.select_right_handle = True
1306 angle_sum = 0
1308 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1309 bpy.ops.curve.select_all(action='INVERT')
1311 bpy.ops.curve.delete(type='VERT')
1312 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1314 objects_to_delete.append(ob_splines)
1316 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1317 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1318 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1320 # Check if the strokes are a crosshatch
1321 if self.is_crosshatch:
1322 all_points_coords = []
1323 for i in range(len(ob_splines.data.splines)):
1324 all_points_coords.append([])
1326 all_points_coords[i] = [Vector((x, y, z)) for
1327 x, y, z in [bp.co for
1328 bp in ob_splines.data.splines[i].bezier_points]]
1330 all_intersections = []
1331 checked_splines = []
1332 for i in range(len(all_points_coords)):
1334 for t in range(len(all_points_coords[i]) - 1):
1335 bp1_co = all_points_coords[i][t]
1336 bp2_co = all_points_coords[i][t + 1]
1338 for i2 in range(len(all_points_coords)):
1339 if i != i2 and i2 not in checked_splines:
1340 for t2 in range(len(all_points_coords[i2]) - 1):
1341 bp3_co = all_points_coords[i2][t2]
1342 bp4_co = all_points_coords[i2][t2 + 1]
1344 intersec_coords = intersect_line_line(
1345 bp1_co, bp2_co, bp3_co, bp4_co
1347 if intersec_coords is not None:
1348 dist = (intersec_coords[0] - intersec_coords[1]).length
1350 if dist <= self.crosshatch_merge_distance * 1.5:
1351 _temp_co, percent1 = intersect_point_line(
1352 intersec_coords[0], bp1_co, bp2_co
1354 if (percent1 >= -0.02 and percent1 <= 1.02):
1355 _temp_co, percent2 = intersect_point_line(
1356 intersec_coords[1], bp3_co, bp4_co
1358 if (percent2 >= -0.02 and percent2 <= 1.02):
1359 # Format: spline index, first point index from
1360 # corresponding segment, percentage from first point of
1361 # actual segment, coords of intersection point
1362 all_intersections.append(
1363 (i, t, percent1,
1364 ob_splines.matrix_world @ intersec_coords[0])
1366 all_intersections.append(
1367 (i2, t2, percent2,
1368 ob_splines.matrix_world @ intersec_coords[1])
1371 checked_splines.append(i)
1372 # Sort list by spline, then by corresponding first point index of segment,
1373 # and then by percentage from first point of segment: elements 0 and 1 respectively
1374 all_intersections.sort(key=operator.itemgetter(0, 1, 2))
1376 self.crosshatch_strokes_coords = {}
1377 for i in range(len(all_intersections)):
1378 if not all_intersections[i][0] in self.crosshatch_strokes_coords:
1379 self.crosshatch_strokes_coords[all_intersections[i][0]] = []
1381 self.crosshatch_strokes_coords[all_intersections[i][0]].append(
1382 all_intersections[i][3]
1383 ) # Save intersection coords
1384 else:
1385 self.is_crosshatch = False
1387 # Delete all duplicates
1388 bpy.ops.object.delete({"selected_objects": objects_to_delete})
1390 # If the main object has modifiers, turn their "viewport view status" to
1391 # what it was before the forced deactivation above
1392 if len(self.main_object.modifiers) > 0:
1393 for m_idx in range(len(self.main_object.modifiers)):
1394 self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
1396 self.update()
1398 return
1400 # Part of the Crosshatch process that is repeated when the operator is tweaked
1401 def crosshatch_surface_execute(self, context):
1402 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1403 # (without this the surface verts merging with the main object doesn't work well)
1404 self.modifiers_prev_viewport_state = []
1405 if len(self.main_object.modifiers) > 0:
1406 for m_idx in range(len(self.main_object.modifiers)):
1407 self.modifiers_prev_viewport_state.append(self.main_object.modifiers[m_idx].show_viewport)
1409 self.main_object.modifiers[m_idx].show_viewport = False
1411 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1413 me_name = "SURFSKIO_STK_TMP"
1414 me = bpy.data.meshes.new(me_name)
1416 all_verts_coords = []
1417 all_edges = []
1418 for st_idx in self.crosshatch_strokes_coords:
1419 for co_idx in range(len(self.crosshatch_strokes_coords[st_idx])):
1420 coords = self.crosshatch_strokes_coords[st_idx][co_idx]
1422 all_verts_coords.append(coords)
1424 if co_idx > 0:
1425 all_edges.append((len(all_verts_coords) - 2, len(all_verts_coords) - 1))
1427 me.from_pydata(all_verts_coords, all_edges, [])
1428 ob = object_utils.object_data_add(context, me)
1429 ob.location = (0.0, 0.0, 0.0)
1430 ob.rotation_euler = (0.0, 0.0, 0.0)
1431 ob.scale = (1.0, 1.0, 1.0)
1433 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1434 ob.select_set(True)
1435 bpy.context.view_layer.objects.active = ob
1437 # Get together each vert and its nearest, to the middle position
1438 verts = ob.data.vertices
1439 checked_verts = []
1440 for i in range(len(verts)):
1441 shortest_dist = None
1443 if i not in checked_verts:
1444 for t in range(len(verts)):
1445 if i != t and t not in checked_verts:
1446 dist = (verts[i].co - verts[t].co).length
1448 if shortest_dist is not None:
1449 if dist < shortest_dist:
1450 shortest_dist = dist
1451 nearest_vert = t
1452 else:
1453 shortest_dist = dist
1454 nearest_vert = t
1456 middle_location = (verts[i].co + verts[nearest_vert].co) / 2
1458 verts[i].co = middle_location
1459 verts[nearest_vert].co = middle_location
1461 checked_verts.append(i)
1462 checked_verts.append(nearest_vert)
1464 # Calculate average length between all the generated edges
1465 ob = bpy.context.object
1466 lengths_sum = 0
1467 for ed in ob.data.edges:
1468 v1 = ob.data.vertices[ed.vertices[0]]
1469 v2 = ob.data.vertices[ed.vertices[1]]
1471 lengths_sum += (v1.co - v2.co).length
1473 edges_count = len(ob.data.edges)
1474 # possible division by zero here
1475 average_edge_length = lengths_sum / edges_count if edges_count != 0 else 0.0001
1477 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1478 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1479 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
1480 threshold=average_edge_length / 15.0)
1481 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1483 final_points_ob = bpy.context.view_layer.objects.active
1485 # Make a dictionary with the verts related to each vert
1486 related_key_verts = {}
1487 for ed in final_points_ob.data.edges:
1488 if not ed.vertices[0] in related_key_verts:
1489 related_key_verts[ed.vertices[0]] = []
1491 if not ed.vertices[1] in related_key_verts:
1492 related_key_verts[ed.vertices[1]] = []
1494 if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
1495 related_key_verts[ed.vertices[0]].append(ed.vertices[1])
1497 if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
1498 related_key_verts[ed.vertices[1]].append(ed.vertices[0])
1500 # Get groups of verts forming each face
1501 faces_verts_idx = []
1502 for v1 in related_key_verts: # verts-1 ....
1503 for v2 in related_key_verts: # verts-2
1504 if v1 != v2:
1505 related_verts_in_common = []
1506 v2_in_rel_v1 = False
1507 v1_in_rel_v2 = False
1508 for rel_v1 in related_key_verts[v1]:
1509 # Check if related verts of verts-1 are related verts of verts-2
1510 if rel_v1 in related_key_verts[v2]:
1511 related_verts_in_common.append(rel_v1)
1513 if v2 in related_key_verts[v1]:
1514 v2_in_rel_v1 = True
1516 if v1 in related_key_verts[v2]:
1517 v1_in_rel_v2 = True
1519 repeated_face = False
1520 # If two verts have two related verts in common, they form a quad
1521 if len(related_verts_in_common) == 2:
1522 # Check if the face is already saved
1523 for f_verts in faces_verts_idx:
1524 repeated_verts = 0
1526 if len(f_verts) == 4:
1527 if v1 in f_verts:
1528 repeated_verts += 1
1529 if v2 in f_verts:
1530 repeated_verts += 1
1531 if related_verts_in_common[0] in f_verts:
1532 repeated_verts += 1
1533 if related_verts_in_common[1] in f_verts:
1534 repeated_verts += 1
1536 if repeated_verts == len(f_verts):
1537 repeated_face = True
1538 break
1540 if not repeated_face:
1541 faces_verts_idx.append([v1, related_verts_in_common[0],
1542 v2, related_verts_in_common[1]])
1544 # If Two verts have one related vert in common and they are
1545 # related to each other, they form a triangle
1546 elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
1547 # Check if the face is already saved.
1548 for f_verts in faces_verts_idx:
1549 repeated_verts = 0
1551 if len(f_verts) == 3:
1552 if v1 in f_verts:
1553 repeated_verts += 1
1554 if v2 in f_verts:
1555 repeated_verts += 1
1556 if related_verts_in_common[0] in f_verts:
1557 repeated_verts += 1
1559 if repeated_verts == len(f_verts):
1560 repeated_face = True
1561 break
1563 if not repeated_face:
1564 faces_verts_idx.append([v1, related_verts_in_common[0], v2])
1566 # Keep only the faces that don't overlap by ignoring
1567 # quads that overlap with two adjacent triangles
1568 faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate
1569 for i in range(len(faces_verts_idx)):
1570 for t in range(len(faces_verts_idx)):
1571 if i != t:
1572 verts_in_common = 0
1574 if len(faces_verts_idx[i]) == 4 and len(faces_verts_idx[t]) == 3:
1575 for v_idx in faces_verts_idx[t]:
1576 if v_idx in faces_verts_idx[i]:
1577 verts_in_common += 1
1578 # If it doesn't have all it's vertices repeated in the other face
1579 if verts_in_common == 3:
1580 if i not in faces_to_not_include_idx:
1581 faces_to_not_include_idx.append(i)
1583 # Build surface
1584 all_surface_verts_co = []
1585 for i in range(len(final_points_ob.data.vertices)):
1586 coords = final_points_ob.data.vertices[i].co
1587 all_surface_verts_co.append([coords[0], coords[1], coords[2]])
1589 # Verts of each face.
1590 all_surface_faces = []
1591 for i in range(len(faces_verts_idx)):
1592 if i not in faces_to_not_include_idx:
1593 face = []
1594 for v_idx in faces_verts_idx[i]:
1595 face.append(v_idx)
1597 all_surface_faces.append(face)
1599 # Build the mesh
1600 surf_me_name = "SURFSKIO_surface"
1601 me_surf = bpy.data.meshes.new(surf_me_name)
1602 me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
1603 ob_surface = object_utils.object_data_add(context, me_surf)
1604 ob_surface.location = (0.0, 0.0, 0.0)
1605 ob_surface.rotation_euler = (0.0, 0.0, 0.0)
1606 ob_surface.scale = (1.0, 1.0, 1.0)
1608 # Delete final points temporal object
1609 bpy.ops.object.delete({"selected_objects": [final_points_ob]})
1611 # Delete isolated verts if there are any
1612 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1613 ob_surface.select_set(True)
1614 bpy.context.view_layer.objects.active = ob_surface
1616 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1617 bpy.ops.mesh.select_all(action='DESELECT')
1618 bpy.ops.mesh.select_face_by_sides(type='NOTEQUAL')
1619 bpy.ops.mesh.delete()
1620 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1622 # Join crosshatch results with original mesh
1624 # Calculate a distance to merge the verts of the crosshatch surface to the main object
1625 edges_length_sum = 0
1626 for ed in ob_surface.data.edges:
1627 edges_length_sum += (
1628 ob_surface.data.vertices[ed.vertices[0]].co -
1629 ob_surface.data.vertices[ed.vertices[1]].co
1630 ).length
1632 # Make dictionary with all the verts connected to each vert, on the new surface object.
1633 surface_connected_verts = {}
1634 for ed in ob_surface.data.edges:
1635 if not ed.vertices[0] in surface_connected_verts:
1636 surface_connected_verts[ed.vertices[0]] = []
1638 surface_connected_verts[ed.vertices[0]].append(ed.vertices[1])
1640 if ed.vertices[1] not in surface_connected_verts:
1641 surface_connected_verts[ed.vertices[1]] = []
1643 surface_connected_verts[ed.vertices[1]].append(ed.vertices[0])
1645 # Duplicate the new surface object, and use shrinkwrap to
1646 # calculate later the nearest verts to the main object
1647 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1648 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
1649 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1651 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1653 final_ob_duplicate = bpy.context.view_layer.objects.active
1655 bpy.ops.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
1656 shrinkwrap_modifier = final_ob_duplicate.modifiers[-1]
1657 shrinkwrap_modifier.wrap_method = "NEAREST_VERTEX"
1658 shrinkwrap_modifier.target = self.main_object
1660 bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', modifier=shrinkwrap_modifier.name)
1662 # Make list with verts of original mesh as index and coords as value
1663 main_object_verts_coords = []
1664 for v in self.main_object.data.vertices:
1665 coords = self.main_object.matrix_world @ v.co
1667 # To avoid problems when taking "-0.00" as a different value as "0.00"
1668 for c in range(len(coords)):
1669 if "%.3f" % coords[c] == "-0.00":
1670 coords[c] = 0
1672 main_object_verts_coords.append(["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]])
1674 tuple(main_object_verts_coords)
1676 # Determine which verts will be merged, snap them to the nearest verts
1677 # on the original verts, and get them selected
1678 crosshatch_verts_to_merge = []
1679 if self.automatic_join:
1680 for i in range(len(ob_surface.data.vertices)-1):
1681 # Calculate the distance from each of the connected verts to the actual vert,
1682 # and compare it with the distance they would have if joined.
1683 # If they don't change much, that vert can be joined
1684 merge_actual_vert = True
1685 try:
1686 if len(surface_connected_verts[i]) < 4:
1687 for c_v_idx in surface_connected_verts[i]:
1688 points_original = []
1689 points_original.append(ob_surface.data.vertices[c_v_idx].co)
1690 points_original.append(ob_surface.data.vertices[i].co)
1692 points_target = []
1693 points_target.append(ob_surface.data.vertices[c_v_idx].co)
1694 points_target.append(final_ob_duplicate.data.vertices[i].co)
1696 vec_A = points_original[0] - points_original[1]
1697 vec_B = points_target[0] - points_target[1]
1699 dist_A = (points_original[0] - points_original[1]).length
1700 dist_B = (points_target[0] - points_target[1]).length
1702 if not (
1703 points_original[0] == points_original[1] or
1704 points_target[0] == points_target[1]
1705 ): # If any vector's length is zero
1707 angle = vec_A.angle(vec_B) / pi
1708 else:
1709 angle = 0
1711 # Set a range of acceptable variation in the connected edges
1712 if dist_B > dist_A * 1.7 * self.join_stretch_factor or \
1713 dist_B < dist_A / 2 / self.join_stretch_factor or \
1714 angle >= 0.15 * self.join_stretch_factor:
1716 merge_actual_vert = False
1717 break
1718 else:
1719 merge_actual_vert = False
1720 except:
1721 self.report({'WARNING'},
1722 "Crosshatch set incorrectly")
1724 if merge_actual_vert:
1725 coords = final_ob_duplicate.data.vertices[i].co
1726 # To avoid problems when taking "-0.000" as a different value as "0.00"
1727 for c in range(len(coords)):
1728 if "%.3f" % coords[c] == "-0.00":
1729 coords[c] = 0
1731 comparison_coords = ["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]]
1733 if comparison_coords in main_object_verts_coords:
1734 # Get the index of the vert with those coords in the main object
1735 main_object_related_vert_idx = main_object_verts_coords.index(comparison_coords)
1737 if self.main_object.data.vertices[main_object_related_vert_idx].select is True or \
1738 self.main_object_selected_verts_count == 0:
1740 ob_surface.data.vertices[i].co = final_ob_duplicate.data.vertices[i].co
1741 ob_surface.data.vertices[i].select = True
1742 crosshatch_verts_to_merge.append(i)
1744 # Make sure the vert in the main object is selected,
1745 # in case it wasn't selected and the "join crosshatch" option is active
1746 self.main_object.data.vertices[main_object_related_vert_idx].select = True
1748 # Delete duplicated object
1749 bpy.ops.object.delete({"selected_objects": [final_ob_duplicate]})
1751 # Join crosshatched surface and main object
1752 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1753 ob_surface.select_set(True)
1754 self.main_object.select_set(True)
1755 bpy.context.view_layer.objects.active = self.main_object
1757 bpy.ops.object.join('INVOKE_REGION_WIN')
1759 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1760 # Perform Remove doubles to merge verts
1761 if not (self.automatic_join is False and self.main_object_selected_verts_count == 0):
1762 bpy.ops.mesh.remove_doubles(threshold=0.0001)
1764 bpy.ops.mesh.select_all(action='DESELECT')
1766 # If the main object has modifiers, turn their "viewport view status"
1767 # to what it was before the forced deactivation above
1768 if len(self.main_object.modifiers) > 0:
1769 for m_idx in range(len(self.main_object.modifiers)):
1770 self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
1772 self.update()
1774 return {'FINISHED'}
1776 def rectangular_surface(self, context):
1777 # Selected edges
1778 all_selected_edges_idx = []
1779 all_selected_verts = []
1780 all_verts_idx = []
1781 for ed in self.main_object.data.edges:
1782 if ed.select:
1783 all_selected_edges_idx.append(ed.index)
1785 # Selected vertices
1786 if not ed.vertices[0] in all_selected_verts:
1787 all_selected_verts.append(self.main_object.data.vertices[ed.vertices[0]])
1788 if not ed.vertices[1] in all_selected_verts:
1789 all_selected_verts.append(self.main_object.data.vertices[ed.vertices[1]])
1791 # All verts (both from each edge) to determine later
1792 # which are at the tips (those not repeated twice)
1793 all_verts_idx.append(ed.vertices[0])
1794 all_verts_idx.append(ed.vertices[1])
1796 # Identify the tips and "middle-vertex" that separates U from V, if there is one
1797 all_chains_tips_idx = []
1798 for v_idx in all_verts_idx:
1799 if all_verts_idx.count(v_idx) < 2:
1800 all_chains_tips_idx.append(v_idx)
1802 edges_connected_to_tips = []
1803 for ed in self.main_object.data.edges:
1804 if (ed.vertices[0] in all_chains_tips_idx or ed.vertices[1] in all_chains_tips_idx) and \
1805 not (ed.vertices[0] in all_verts_idx and ed.vertices[1] in all_verts_idx):
1807 edges_connected_to_tips.append(ed)
1809 # Check closed selections
1810 # List with groups of three verts, where the first element of the pair is
1811 # the unselected vert of a closed selection and the other two elements are the
1812 # selected neighbor verts (it will be useful to determine which selection chain
1813 # the unselected vert belongs to, and determine the "middle-vertex")
1814 single_unselected_verts_and_neighbors = []
1816 # To identify a "closed" selection (a selection that is a closed chain except
1817 # for one vertex) find the vertex in common that have the edges connected to tips.
1818 # If there is a vertex in common, that one is the unselected vert that closes
1819 # the selection or is a "middle-vertex"
1820 single_unselected_verts = []
1821 for ed in edges_connected_to_tips:
1822 for ed_b in edges_connected_to_tips:
1823 if ed != ed_b:
1824 if ed.vertices[0] == ed_b.vertices[0] and \
1825 not self.main_object.data.vertices[ed.vertices[0]].select and \
1826 ed.vertices[0] not in single_unselected_verts:
1828 # The second element is one of the tips of the selected
1829 # vertices of the closed selection
1830 single_unselected_verts_and_neighbors.append(
1831 [ed.vertices[0], ed.vertices[1], ed_b.vertices[1]]
1833 single_unselected_verts.append(ed.vertices[0])
1834 break
1835 elif ed.vertices[0] == ed_b.vertices[1] and \
1836 not self.main_object.data.vertices[ed.vertices[0]].select and \
1837 ed.vertices[0] not in single_unselected_verts:
1839 single_unselected_verts_and_neighbors.append(
1840 [ed.vertices[0], ed.vertices[1], ed_b.vertices[0]]
1842 single_unselected_verts.append(ed.vertices[0])
1843 break
1844 elif ed.vertices[1] == ed_b.vertices[0] and \
1845 not self.main_object.data.vertices[ed.vertices[1]].select and \
1846 ed.vertices[1] not in single_unselected_verts:
1848 single_unselected_verts_and_neighbors.append(
1849 [ed.vertices[1], ed.vertices[0], ed_b.vertices[1]]
1851 single_unselected_verts.append(ed.vertices[1])
1852 break
1853 elif ed.vertices[1] == ed_b.vertices[1] and \
1854 not self.main_object.data.vertices[ed.vertices[1]].select and \
1855 ed.vertices[1] not in single_unselected_verts:
1857 single_unselected_verts_and_neighbors.append(
1858 [ed.vertices[1], ed.vertices[0], ed_b.vertices[0]]
1860 single_unselected_verts.append(ed.vertices[1])
1861 break
1863 middle_vertex_idx = None
1864 tips_to_discard_idx = []
1866 # Check if there is a "middle-vertex", and get its index
1867 for i in range(0, len(single_unselected_verts_and_neighbors)):
1868 actual_chain_verts = self.get_ordered_verts(
1869 self.main_object, all_selected_edges_idx,
1870 all_verts_idx, single_unselected_verts_and_neighbors[i][1],
1871 None, None
1874 if single_unselected_verts_and_neighbors[i][2] != \
1875 actual_chain_verts[len(actual_chain_verts) - 1].index:
1877 middle_vertex_idx = single_unselected_verts_and_neighbors[i][0]
1878 tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][1])
1879 tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][2])
1881 # List with pairs of verts that belong to the tips of each selection chain (row)
1882 verts_tips_same_chain_idx = []
1883 if len(all_chains_tips_idx) >= 2:
1884 checked_v = []
1885 for i in range(0, len(all_chains_tips_idx)):
1886 if all_chains_tips_idx[i] not in checked_v:
1887 v_chain = self.get_ordered_verts(
1888 self.main_object, all_selected_edges_idx,
1889 all_verts_idx, all_chains_tips_idx[i],
1890 middle_vertex_idx, None
1893 verts_tips_same_chain_idx.append([v_chain[0].index, v_chain[len(v_chain) - 1].index])
1895 checked_v.append(v_chain[0].index)
1896 checked_v.append(v_chain[len(v_chain) - 1].index)
1898 # Selection tips (vertices).
1899 verts_tips_parsed_idx = []
1900 if len(all_chains_tips_idx) >= 2:
1901 for spec_v_idx in all_chains_tips_idx:
1902 if (spec_v_idx not in tips_to_discard_idx):
1903 verts_tips_parsed_idx.append(spec_v_idx)
1905 # Identify the type of selection made by the user
1906 if middle_vertex_idx is not None:
1907 # If there are 4 tips (two selection chains), and
1908 # there is only one single unselected vert (the middle vert)
1909 if len(all_chains_tips_idx) == 4 and len(single_unselected_verts_and_neighbors) == 1:
1910 selection_type = "TWO_CONNECTED"
1911 else:
1912 # The type of the selection was not identified, the script stops.
1913 self.report({'WARNING'}, "The selection isn't valid.")
1915 self.stopping_errors = True
1917 return{'CANCELLED'}
1918 else:
1919 if len(all_chains_tips_idx) == 2: # If there are 2 tips
1920 selection_type = "SINGLE"
1921 elif len(all_chains_tips_idx) == 4: # If there are 4 tips
1922 selection_type = "TWO_NOT_CONNECTED"
1923 elif len(all_chains_tips_idx) == 0:
1924 if len(self.main_splines.data.splines) > 1:
1925 selection_type = "NO_SELECTION"
1926 else:
1927 # If the selection was not identified and there is only one stroke,
1928 # there's no possibility to build a surface, so the script is interrupted
1929 self.report({'WARNING'}, "The selection isn't valid.")
1931 self.stopping_errors = True
1933 return{'CANCELLED'}
1934 else:
1935 # The type of the selection was not identified, the script stops
1936 self.report({'WARNING'}, "The selection isn't valid.")
1938 self.stopping_errors = True
1940 return{'CANCELLED'}
1942 # If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script
1943 if selection_type == "TWO_NOT_CONNECTED" and len(self.main_splines.data.splines) == 1:
1944 self.report({'WARNING'},
1945 "At least two strokes are needed when there are two not connected selections")
1947 self.stopping_errors = True
1949 return{'CANCELLED'}
1951 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1953 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1954 self.main_splines.select_set(True)
1955 bpy.context.view_layer.objects.active = self.main_splines
1957 # Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out
1958 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1959 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1960 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1961 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1962 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1963 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1964 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1965 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1967 self.selection_U_exists = False
1968 self.selection_U2_exists = False
1969 self.selection_V_exists = False
1970 self.selection_V2_exists = False
1972 self.selection_U_is_closed = False
1973 self.selection_U2_is_closed = False
1974 self.selection_V_is_closed = False
1975 self.selection_V2_is_closed = False
1977 # Define what vertices are at the tips of each selection and are not the middle-vertex
1978 if selection_type == "TWO_CONNECTED":
1979 self.selection_U_exists = True
1980 self.selection_V_exists = True
1982 closing_vert_U_idx = None
1983 closing_vert_V_idx = None
1984 closing_vert_U2_idx = None
1985 closing_vert_V2_idx = None
1987 # Determine which selection is Selection-U and which is Selection-V
1988 points_A = []
1989 points_B = []
1990 points_first_stroke_tips = []
1992 points_A.append(
1993 self.main_object.matrix_world @ self.main_object.data.vertices[verts_tips_parsed_idx[0]].co
1995 points_A.append(
1996 self.main_object.matrix_world @ self.main_object.data.vertices[middle_vertex_idx].co
1998 points_B.append(
1999 self.main_object.matrix_world @ self.main_object.data.vertices[verts_tips_parsed_idx[1]].co
2001 points_B.append(
2002 self.main_object.matrix_world @ self.main_object.data.vertices[middle_vertex_idx].co
2004 points_first_stroke_tips.append(
2005 self.main_splines.data.splines[0].bezier_points[0].co
2007 points_first_stroke_tips.append(
2008 self.main_splines.data.splines[0].bezier_points[
2009 len(self.main_splines.data.splines[0].bezier_points) - 1
2010 ].co
2013 angle_A = self.orientation_difference(points_A, points_first_stroke_tips)
2014 angle_B = self.orientation_difference(points_B, points_first_stroke_tips)
2016 if angle_A < angle_B:
2017 first_vert_U_idx = verts_tips_parsed_idx[0]
2018 first_vert_V_idx = verts_tips_parsed_idx[1]
2019 else:
2020 first_vert_U_idx = verts_tips_parsed_idx[1]
2021 first_vert_V_idx = verts_tips_parsed_idx[0]
2023 elif selection_type == "SINGLE" or selection_type == "TWO_NOT_CONNECTED":
2024 first_sketched_point_first_stroke_co = self.main_splines.data.splines[0].bezier_points[0].co
2025 last_sketched_point_first_stroke_co = \
2026 self.main_splines.data.splines[0].bezier_points[
2027 len(self.main_splines.data.splines[0].bezier_points) - 1
2028 ].co
2029 first_sketched_point_last_stroke_co = \
2030 self.main_splines.data.splines[
2031 len(self.main_splines.data.splines) - 1
2032 ].bezier_points[0].co
2033 if len(self.main_splines.data.splines) > 1:
2034 first_sketched_point_second_stroke_co = self.main_splines.data.splines[1].bezier_points[0].co
2035 last_sketched_point_second_stroke_co = \
2036 self.main_splines.data.splines[1].bezier_points[
2037 len(self.main_splines.data.splines[1].bezier_points) - 1
2038 ].co
2040 single_unselected_neighbors = [] # Only the neighbors of the single unselected verts
2041 for verts_neig_idx in single_unselected_verts_and_neighbors:
2042 single_unselected_neighbors.append(verts_neig_idx[1])
2043 single_unselected_neighbors.append(verts_neig_idx[2])
2045 all_chains_tips_and_middle_vert = []
2046 for v_idx in all_chains_tips_idx:
2047 if v_idx not in single_unselected_neighbors:
2048 all_chains_tips_and_middle_vert.append(v_idx)
2050 all_chains_tips_and_middle_vert += single_unselected_verts
2052 all_participating_verts = all_chains_tips_and_middle_vert + all_verts_idx
2054 # The tip of the selected vertices nearest to the first point of the first sketched stroke
2055 nearest_tip_to_first_st_first_pt_idx, shortest_distance_to_first_stroke = \
2056 self.shortest_distance(
2057 self.main_object,
2058 first_sketched_point_first_stroke_co,
2059 all_chains_tips_and_middle_vert
2061 # If the nearest tip is not from a closed selection, get the opposite tip vertex index
2062 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2063 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2065 nearest_tip_to_first_st_first_pt_opposite_idx = \
2066 self.opposite_tip(
2067 nearest_tip_to_first_st_first_pt_idx,
2068 verts_tips_same_chain_idx
2070 # The tip of the selected vertices nearest to the last point of the first sketched stroke
2071 nearest_tip_to_first_st_last_pt_idx, _temp_dist = \
2072 self.shortest_distance(
2073 self.main_object,
2074 last_sketched_point_first_stroke_co,
2075 all_chains_tips_and_middle_vert
2077 # The tip of the selected vertices nearest to the first point of the last sketched stroke
2078 nearest_tip_to_last_st_first_pt_idx, shortest_distance_to_last_stroke = \
2079 self.shortest_distance(
2080 self.main_object,
2081 first_sketched_point_last_stroke_co,
2082 all_chains_tips_and_middle_vert
2084 if len(self.main_splines.data.splines) > 1:
2085 # The selected vertex nearest to the first point of the second sketched stroke
2086 # (This will be useful to determine the direction of the closed
2087 # selection V when extruding along strokes)
2088 nearest_vert_to_second_st_first_pt_idx, _temp_dist = \
2089 self.shortest_distance(
2090 self.main_object,
2091 first_sketched_point_second_stroke_co,
2092 all_verts_idx
2094 # The selected vertex nearest to the first point of the second sketched stroke
2095 # (This will be useful to determine the direction of the closed
2096 # selection V2 when extruding along strokes)
2097 nearest_vert_to_second_st_last_pt_idx, _temp_dist = \
2098 self.shortest_distance(
2099 self.main_object,
2100 last_sketched_point_second_stroke_co,
2101 all_verts_idx
2103 # Determine if the single selection will be treated as U or as V
2104 edges_sum = 0
2105 for i in all_selected_edges_idx:
2106 edges_sum += (
2107 (self.main_object.matrix_world @
2108 self.main_object.data.vertices[self.main_object.data.edges[i].vertices[0]].co) -
2109 (self.main_object.matrix_world @
2110 self.main_object.data.vertices[self.main_object.data.edges[i].vertices[1]].co)
2111 ).length
2113 average_edge_length = edges_sum / len(all_selected_edges_idx)
2115 # Get shortest distance from the first point of the last stroke to any participating vertex
2116 _temp_idx, shortest_distance_to_last_stroke = \
2117 self.shortest_distance(
2118 self.main_object,
2119 first_sketched_point_last_stroke_co,
2120 all_participating_verts
2122 # If the beginning of the first stroke is near enough, and its orientation
2123 # difference with the first edge of the nearest selection chain is not too high,
2124 # interpret things as an "extrude along strokes" instead of "extrude through strokes"
2125 if shortest_distance_to_first_stroke < average_edge_length / 4 and \
2126 shortest_distance_to_last_stroke < average_edge_length and \
2127 len(self.main_splines.data.splines) > 1:
2129 self.selection_U_exists = False
2130 self.selection_V_exists = True
2131 # If the first selection is not closed
2132 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2133 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2134 self.selection_V_is_closed = False
2135 closing_vert_U_idx = None
2136 closing_vert_U2_idx = None
2137 closing_vert_V_idx = None
2138 closing_vert_V2_idx = None
2140 first_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
2142 if selection_type == "TWO_NOT_CONNECTED":
2143 self.selection_V2_exists = True
2145 first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2146 else:
2147 self.selection_V_is_closed = True
2148 closing_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
2150 # Get the neighbors of the first (unselected) vert of the closed selection U.
2151 vert_neighbors = []
2152 for verts in single_unselected_verts_and_neighbors:
2153 if verts[0] == nearest_tip_to_first_st_first_pt_idx:
2154 vert_neighbors.append(verts[1])
2155 vert_neighbors.append(verts[2])
2156 break
2158 verts_V = self.get_ordered_verts(
2159 self.main_object, all_selected_edges_idx,
2160 all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
2163 for i in range(0, len(verts_V)):
2164 if verts_V[i].index == nearest_vert_to_second_st_first_pt_idx:
2165 # If the vertex nearest to the first point of the second stroke
2166 # is in the first half of the selected verts
2167 if i >= len(verts_V) / 2:
2168 first_vert_V_idx = vert_neighbors[1]
2169 break
2170 else:
2171 first_vert_V_idx = vert_neighbors[0]
2172 break
2174 if selection_type == "TWO_NOT_CONNECTED":
2175 self.selection_V2_exists = True
2176 # If the second selection is not closed
2177 if nearest_tip_to_first_st_last_pt_idx not in single_unselected_verts or \
2178 nearest_tip_to_first_st_last_pt_idx == middle_vertex_idx:
2180 self.selection_V2_is_closed = False
2181 closing_vert_V2_idx = None
2182 first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2184 else:
2185 self.selection_V2_is_closed = True
2186 closing_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2188 # Get the neighbors of the first (unselected) vert of the closed selection U
2189 vert_neighbors = []
2190 for verts in single_unselected_verts_and_neighbors:
2191 if verts[0] == nearest_tip_to_first_st_last_pt_idx:
2192 vert_neighbors.append(verts[1])
2193 vert_neighbors.append(verts[2])
2194 break
2196 verts_V2 = self.get_ordered_verts(
2197 self.main_object, all_selected_edges_idx,
2198 all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
2201 for i in range(0, len(verts_V2)):
2202 if verts_V2[i].index == nearest_vert_to_second_st_last_pt_idx:
2203 # If the vertex nearest to the first point of the second stroke
2204 # is in the first half of the selected verts
2205 if i >= len(verts_V2) / 2:
2206 first_vert_V2_idx = vert_neighbors[1]
2207 break
2208 else:
2209 first_vert_V2_idx = vert_neighbors[0]
2210 break
2211 else:
2212 self.selection_V2_exists = False
2214 else:
2215 self.selection_U_exists = True
2216 self.selection_V_exists = False
2217 # If the first selection is not closed
2218 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2219 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2220 self.selection_U_is_closed = False
2221 closing_vert_U_idx = None
2223 points_tips = []
2224 points_tips.append(
2225 self.main_object.matrix_world @
2226 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
2228 points_tips.append(
2229 self.main_object.matrix_world @
2230 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_opposite_idx].co
2232 points_first_stroke_tips = []
2233 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
2234 points_first_stroke_tips.append(
2235 self.main_splines.data.splines[0].bezier_points[
2236 len(self.main_splines.data.splines[0].bezier_points) - 1
2237 ].co
2239 vec_A = points_tips[0] - points_tips[1]
2240 vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
2242 # Compare the direction of the selection and the first
2243 # grease pencil stroke to determine which is the "first" vertex of the selection
2244 if vec_A.dot(vec_B) < 0:
2245 first_vert_U_idx = nearest_tip_to_first_st_first_pt_opposite_idx
2246 else:
2247 first_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
2249 else:
2250 self.selection_U_is_closed = True
2251 closing_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
2253 # Get the neighbors of the first (unselected) vert of the closed selection U
2254 vert_neighbors = []
2255 for verts in single_unselected_verts_and_neighbors:
2256 if verts[0] == nearest_tip_to_first_st_first_pt_idx:
2257 vert_neighbors.append(verts[1])
2258 vert_neighbors.append(verts[2])
2259 break
2261 points_first_and_neighbor = []
2262 points_first_and_neighbor.append(
2263 self.main_object.matrix_world @
2264 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
2266 points_first_and_neighbor.append(
2267 self.main_object.matrix_world @
2268 self.main_object.data.vertices[vert_neighbors[0]].co
2270 points_first_stroke_tips = []
2271 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
2272 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[1].co)
2274 vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
2275 vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
2277 # Compare the direction of the selection and the first grease pencil stroke to
2278 # determine which is the vertex neighbor to the first vertex (unselected) of
2279 # the closed selection. This will determine the direction of the closed selection
2280 if vec_A.dot(vec_B) < 0:
2281 first_vert_U_idx = vert_neighbors[1]
2282 else:
2283 first_vert_U_idx = vert_neighbors[0]
2285 if selection_type == "TWO_NOT_CONNECTED":
2286 self.selection_U2_exists = True
2287 # If the second selection is not closed
2288 if nearest_tip_to_last_st_first_pt_idx not in single_unselected_verts or \
2289 nearest_tip_to_last_st_first_pt_idx == middle_vertex_idx:
2291 self.selection_U2_is_closed = False
2292 closing_vert_U2_idx = None
2293 first_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
2294 else:
2295 self.selection_U2_is_closed = True
2296 closing_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
2298 # Get the neighbors of the first (unselected) vert of the closed selection U
2299 vert_neighbors = []
2300 for verts in single_unselected_verts_and_neighbors:
2301 if verts[0] == nearest_tip_to_last_st_first_pt_idx:
2302 vert_neighbors.append(verts[1])
2303 vert_neighbors.append(verts[2])
2304 break
2306 points_first_and_neighbor = []
2307 points_first_and_neighbor.append(
2308 self.main_object.matrix_world @
2309 self.main_object.data.vertices[nearest_tip_to_last_st_first_pt_idx].co
2311 points_first_and_neighbor.append(
2312 self.main_object.matrix_world @
2313 self.main_object.data.vertices[vert_neighbors[0]].co
2315 points_last_stroke_tips = []
2316 points_last_stroke_tips.append(
2317 self.main_splines.data.splines[
2318 len(self.main_splines.data.splines) - 1
2319 ].bezier_points[0].co
2321 points_last_stroke_tips.append(
2322 self.main_splines.data.splines[
2323 len(self.main_splines.data.splines) - 1
2324 ].bezier_points[1].co
2326 vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
2327 vec_B = points_last_stroke_tips[0] - points_last_stroke_tips[1]
2329 # Compare the direction of the selection and the last grease pencil stroke to
2330 # determine which is the vertex neighbor to the first vertex (unselected) of
2331 # the closed selection. This will determine the direction of the closed selection
2332 if vec_A.dot(vec_B) < 0:
2333 first_vert_U2_idx = vert_neighbors[1]
2334 else:
2335 first_vert_U2_idx = vert_neighbors[0]
2336 else:
2337 self.selection_U2_exists = False
2339 elif selection_type == "NO_SELECTION":
2340 self.selection_U_exists = False
2341 self.selection_V_exists = False
2343 # Get an ordered list of the vertices of Selection-U
2344 verts_ordered_U = []
2345 if self.selection_U_exists:
2346 verts_ordered_U = self.get_ordered_verts(
2347 self.main_object, all_selected_edges_idx,
2348 all_verts_idx, first_vert_U_idx,
2349 middle_vertex_idx, closing_vert_U_idx
2352 # Get an ordered list of the vertices of Selection-U2
2353 verts_ordered_U2 = []
2354 if self.selection_U2_exists:
2355 verts_ordered_U2 = self.get_ordered_verts(
2356 self.main_object, all_selected_edges_idx,
2357 all_verts_idx, first_vert_U2_idx,
2358 middle_vertex_idx, closing_vert_U2_idx
2361 # Get an ordered list of the vertices of Selection-V
2362 verts_ordered_V = []
2363 if self.selection_V_exists:
2364 verts_ordered_V = self.get_ordered_verts(
2365 self.main_object, all_selected_edges_idx,
2366 all_verts_idx, first_vert_V_idx,
2367 middle_vertex_idx, closing_vert_V_idx
2369 verts_ordered_V_indices = [x.index for x in verts_ordered_V]
2371 # Get an ordered list of the vertices of Selection-V2
2372 verts_ordered_V2 = []
2373 if self.selection_V2_exists:
2374 verts_ordered_V2 = self.get_ordered_verts(
2375 self.main_object, all_selected_edges_idx,
2376 all_verts_idx, first_vert_V2_idx,
2377 middle_vertex_idx, closing_vert_V2_idx
2380 # Check if when there are two-not-connected selections both have the same
2381 # number of verts. If not terminate the script
2382 if ((self.selection_U2_exists and len(verts_ordered_U) != len(verts_ordered_U2)) or
2383 (self.selection_V2_exists and len(verts_ordered_V) != len(verts_ordered_V2))):
2384 # Display a warning
2385 self.report({'WARNING'}, "Both selections must have the same number of edges")
2387 self.stopping_errors = True
2389 return{'CANCELLED'}
2391 # Calculate edges U proportions
2392 # Sum selected edges U lengths
2393 edges_lengths_U = []
2394 edges_lengths_sum_U = 0
2396 if self.selection_U_exists:
2397 edges_lengths_U, edges_lengths_sum_U = self.get_chain_length(
2398 self.main_object,
2399 verts_ordered_U
2401 if self.selection_U2_exists:
2402 edges_lengths_U2, edges_lengths_sum_U2 = self.get_chain_length(
2403 self.main_object,
2404 verts_ordered_U2
2406 # Sum selected edges V lengths
2407 edges_lengths_V = []
2408 edges_lengths_sum_V = 0
2410 if self.selection_V_exists:
2411 edges_lengths_V, edges_lengths_sum_V = self.get_chain_length(
2412 self.main_object,
2413 verts_ordered_V
2415 if self.selection_V2_exists:
2416 edges_lengths_V2, edges_lengths_sum_V2 = self.get_chain_length(
2417 self.main_object,
2418 verts_ordered_V2
2421 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2422 bpy.ops.curve.subdivide('INVOKE_REGION_WIN',
2423 number_cuts=bpy.context.scene.bsurfaces.SURFSK_precision)
2424 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2426 # Proportions U
2427 edges_proportions_U = []
2428 edges_proportions_U = self.get_edges_proportions(
2429 edges_lengths_U, edges_lengths_sum_U,
2430 self.selection_U_exists, self.edges_U
2432 verts_count_U = len(edges_proportions_U) + 1
2434 if self.selection_U2_exists:
2435 edges_proportions_U2 = []
2436 edges_proportions_U2 = self.get_edges_proportions(
2437 edges_lengths_U2, edges_lengths_sum_U2,
2438 self.selection_U2_exists, self.edges_V
2441 # Proportions V
2442 edges_proportions_V = []
2443 edges_proportions_V = self.get_edges_proportions(
2444 edges_lengths_V, edges_lengths_sum_V,
2445 self.selection_V_exists, self.edges_V
2448 if self.selection_V2_exists:
2449 edges_proportions_V2 = []
2450 edges_proportions_V2 = self.get_edges_proportions(
2451 edges_lengths_V2, edges_lengths_sum_V2,
2452 self.selection_V2_exists, self.edges_V
2455 # Cyclic Follow: simplify sketched curves, make them Cyclic, and complete
2456 # the actual sketched curves with a "closing segment"
2457 if self.cyclic_follow and not self.selection_V_exists and not \
2458 ((self.selection_U_exists and not self.selection_U_is_closed) or
2459 (self.selection_U2_exists and not self.selection_U2_is_closed)):
2461 simplified_spline_coords = []
2462 simplified_curve = []
2463 ob_simplified_curve = []
2464 splines_first_v_co = []
2465 for i in range(len(self.main_splines.data.splines)):
2466 # Create a curve object for the actual spline "cyclic extension"
2467 simplified_curve.append(bpy.data.curves.new('SURFSKIO_simpl_crv', 'CURVE'))
2468 ob_simplified_curve.append(bpy.data.objects.new('SURFSKIO_simpl_crv', simplified_curve[i]))
2469 bpy.context.collection.objects.link(ob_simplified_curve[i])
2471 simplified_curve[i].dimensions = "3D"
2473 spline_coords = []
2474 for bp in self.main_splines.data.splines[i].bezier_points:
2475 spline_coords.append(bp.co)
2477 # Simplification
2478 simplified_spline_coords.append(self.simplify_spline(spline_coords, 5))
2480 # Get the coordinates of the first vert of the actual spline
2481 splines_first_v_co.append(simplified_spline_coords[i][0])
2483 # Generate the spline
2484 spline = simplified_curve[i].splines.new('BEZIER')
2485 # less one because one point is added when the spline is created
2486 spline.bezier_points.add(len(simplified_spline_coords[i]) - 1)
2487 for p in range(0, len(simplified_spline_coords[i])):
2488 spline.bezier_points[p].co = simplified_spline_coords[i][p]
2490 spline.use_cyclic_u = True
2492 spline_bp_count = len(spline.bezier_points)
2494 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2495 ob_simplified_curve[i].select_set(True)
2496 bpy.context.view_layer.objects.active = ob_simplified_curve[i]
2498 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2499 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
2500 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2501 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
2502 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2504 # Select the "closing segment", and subdivide it
2505 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_control_point = True
2506 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_left_handle = True
2507 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_right_handle = True
2509 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_control_point = True
2510 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_left_handle = True
2511 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_right_handle = True
2513 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2514 segments = sqrt(
2515 (ob_simplified_curve[i].data.splines[0].bezier_points[0].co -
2516 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].co).length /
2517 self.average_gp_segment_length
2519 for t in range(2):
2520 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=segments)
2522 # Delete the other vertices and make it non-cyclic to
2523 # keep only the needed verts of the "closing segment"
2524 bpy.ops.curve.select_all(action='INVERT')
2525 bpy.ops.curve.delete(type='VERT')
2526 ob_simplified_curve[i].data.splines[0].use_cyclic_u = False
2527 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2529 # Add the points of the "closing segment" to the original curve from grease pencil stroke
2530 first_new_index = len(self.main_splines.data.splines[i].bezier_points)
2531 self.main_splines.data.splines[i].bezier_points.add(
2532 len(ob_simplified_curve[i].data.splines[0].bezier_points) - 1
2534 for t in range(1, len(ob_simplified_curve[i].data.splines[0].bezier_points)):
2535 self.main_splines.data.splines[i].bezier_points[t - 1 + first_new_index].co = \
2536 ob_simplified_curve[i].data.splines[0].bezier_points[t].co
2538 # Delete the temporal curve
2539 bpy.ops.object.delete({"selected_objects": [ob_simplified_curve[i]]})
2541 # Get the coords of the points distributed along the sketched strokes,
2542 # with proportions-U of the first selection
2543 pts_on_strokes_with_proportions_U = self.distribute_pts(
2544 self.main_splines.data.splines,
2545 edges_proportions_U
2547 sketched_splines_parsed = []
2549 if self.selection_U2_exists:
2550 # Initialize the multidimensional list with the proportions of all the segments
2551 proportions_loops_crossing_strokes = []
2552 for i in range(len(pts_on_strokes_with_proportions_U)):
2553 proportions_loops_crossing_strokes.append([])
2555 for t in range(len(pts_on_strokes_with_proportions_U[0])):
2556 proportions_loops_crossing_strokes[i].append(None)
2558 # Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U
2559 for lp in range(len(pts_on_strokes_with_proportions_U[0])):
2560 loop_segments_lengths = []
2562 for st in range(len(pts_on_strokes_with_proportions_U)):
2563 # When on the first stroke, add the segment from the selection to the dirst stroke
2564 if st == 0:
2565 loop_segments_lengths.append(
2566 ((self.main_object.matrix_world @ verts_ordered_U[lp].co) -
2567 pts_on_strokes_with_proportions_U[0][lp]).length
2569 # For all strokes except for the last, calculate the distance
2570 # from the actual stroke to the next
2571 if st != len(pts_on_strokes_with_proportions_U) - 1:
2572 loop_segments_lengths.append(
2573 (pts_on_strokes_with_proportions_U[st][lp] -
2574 pts_on_strokes_with_proportions_U[st + 1][lp]).length
2576 # When on the last stroke, add the segments
2577 # from the last stroke to the second selection
2578 if st == len(pts_on_strokes_with_proportions_U) - 1:
2579 loop_segments_lengths.append(
2580 (pts_on_strokes_with_proportions_U[st][lp] -
2581 (self.main_object.matrix_world @ verts_ordered_U2[lp].co)).length
2583 # Calculate full loop length
2584 loop_seg_lengths_sum = 0
2585 for i in range(len(loop_segments_lengths)):
2586 loop_seg_lengths_sum += loop_segments_lengths[i]
2588 # Fill the multidimensional list with the proportions of all the segments
2589 for st in range(len(pts_on_strokes_with_proportions_U)):
2590 proportions_loops_crossing_strokes[st][lp] = \
2591 loop_segments_lengths[st] / loop_seg_lengths_sum
2593 # Calculate proportions for each stroke
2594 for st in range(len(pts_on_strokes_with_proportions_U)):
2595 actual_stroke_spline = []
2596 # Needs to be a list for the "distribute_pts" method
2597 actual_stroke_spline.append(self.main_splines.data.splines[st])
2599 # Calculate the proportions for the actual stroke.
2600 actual_edges_proportions_U = []
2601 for i in range(len(edges_proportions_U)):
2602 proportions_sum = 0
2604 # Sum the proportions of this loop up to the actual.
2605 for t in range(0, st + 1):
2606 proportions_sum += proportions_loops_crossing_strokes[t][i]
2607 # i + 1, because proportions_loops_crossing_strokes refers to loops,
2608 # and the proportions refer to edges, so we start at the element 1
2609 # of proportions_loops_crossing_strokes instead of element 0
2610 actual_edges_proportions_U.append(
2611 edges_proportions_U[i] -
2612 ((edges_proportions_U[i] - edges_proportions_U2[i]) * proportions_sum)
2614 points_actual_spline = self.distribute_pts(actual_stroke_spline, actual_edges_proportions_U)
2615 sketched_splines_parsed.append(points_actual_spline[0])
2616 else:
2617 sketched_splines_parsed = pts_on_strokes_with_proportions_U
2619 # If the selection type is "TWO_NOT_CONNECTED" replace the
2620 # points of the last spline with the points in the "target" selection
2621 if selection_type == "TWO_NOT_CONNECTED":
2622 if self.selection_U2_exists:
2623 for i in range(0, len(sketched_splines_parsed[len(sketched_splines_parsed) - 1])):
2624 sketched_splines_parsed[len(sketched_splines_parsed) - 1][i] = \
2625 self.main_object.matrix_world @ verts_ordered_U2[i].co
2627 # Create temporary curves along the "control-points" found
2628 # on the sketched curves and the mesh selection
2629 mesh_ctrl_pts_name = "SURFSKIO_ctrl_pts"
2630 me = bpy.data.meshes.new(mesh_ctrl_pts_name)
2631 ob_ctrl_pts = bpy.data.objects.new(mesh_ctrl_pts_name, me)
2632 ob_ctrl_pts.data = me
2633 bpy.context.collection.objects.link(ob_ctrl_pts)
2635 cyclic_loops_U = []
2636 first_verts = []
2637 second_verts = []
2638 last_verts = []
2640 for i in range(0, verts_count_U):
2641 vert_num_in_spline = 1
2643 if self.selection_U_exists:
2644 ob_ctrl_pts.data.vertices.add(1)
2645 last_v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
2646 last_v.co = self.main_object.matrix_world @ verts_ordered_U[i].co
2648 vert_num_in_spline += 1
2650 for t in range(0, len(sketched_splines_parsed)):
2651 ob_ctrl_pts.data.vertices.add(1)
2652 v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
2653 v.co = sketched_splines_parsed[t][i]
2655 if vert_num_in_spline > 1:
2656 ob_ctrl_pts.data.edges.add(1)
2657 ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[0] = \
2658 len(ob_ctrl_pts.data.vertices) - 2
2659 ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[1] = \
2660 len(ob_ctrl_pts.data.vertices) - 1
2662 if t == 0:
2663 first_verts.append(v.index)
2665 if t == 1:
2666 second_verts.append(v.index)
2668 if t == len(sketched_splines_parsed) - 1:
2669 last_verts.append(v.index)
2671 last_v = v
2672 vert_num_in_spline += 1
2674 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2675 ob_ctrl_pts.select_set(True)
2676 bpy.context.view_layer.objects.active = ob_ctrl_pts
2678 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2679 bpy.ops.mesh.select_all(action='DESELECT')
2680 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2682 # Determine which loops-U will be "Cyclic"
2683 for i in range(0, len(first_verts)):
2684 # When there is Cyclic Cross there is no need of
2685 # Automatic Join, (and there are at least three strokes)
2686 if self.automatic_join and not self.cyclic_cross and \
2687 selection_type != "TWO_CONNECTED" and len(self.main_splines.data.splines) >= 3:
2689 v = ob_ctrl_pts.data.vertices
2690 first_point_co = v[first_verts[i]].co
2691 second_point_co = v[second_verts[i]].co
2692 last_point_co = v[last_verts[i]].co
2694 # Coordinates of the point in the center of both the first and last verts.
2695 verts_center_co = [
2696 (first_point_co[0] + last_point_co[0]) / 2,
2697 (first_point_co[1] + last_point_co[1]) / 2,
2698 (first_point_co[2] + last_point_co[2]) / 2
2700 vec_A = second_point_co - first_point_co
2701 vec_B = second_point_co - Vector(verts_center_co)
2703 # Calculate the length of the first segment of the loop,
2704 # and the length it would have after moving the first vert
2705 # to the middle position between first and last
2706 length_original = (second_point_co - first_point_co).length
2707 length_target = (second_point_co - Vector(verts_center_co)).length
2709 angle = vec_A.angle(vec_B) / pi
2711 # If the target length doesn't stretch too much, and the
2712 # its angle doesn't change to much either
2713 if length_target <= length_original * 1.03 * self.join_stretch_factor and \
2714 angle <= 0.008 * self.join_stretch_factor and not self.selection_U_exists:
2716 cyclic_loops_U.append(True)
2717 # Move the first vert to the center coordinates
2718 ob_ctrl_pts.data.vertices[first_verts[i]].co = verts_center_co
2719 # Select the last verts from Cyclic loops, for later deletion all at once
2720 v[last_verts[i]].select = True
2721 else:
2722 cyclic_loops_U.append(False)
2723 else:
2724 # If "Cyclic Cross" is active then "all" crossing curves become cyclic
2725 if self.cyclic_cross and not self.selection_U_exists and not \
2726 ((self.selection_V_exists and not self.selection_V_is_closed) or
2727 (self.selection_V2_exists and not self.selection_V2_is_closed)):
2729 cyclic_loops_U.append(True)
2730 else:
2731 cyclic_loops_U.append(False)
2733 # The cyclic_loops_U list needs to be reversed.
2734 cyclic_loops_U.reverse()
2736 # Delete the previously selected (last_)verts.
2737 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2738 bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
2739 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2741 # Create curves from control points.
2742 bpy.ops.object.convert('INVOKE_REGION_WIN', target='CURVE', keep_original=False)
2743 ob_curves_surf = bpy.context.view_layer.objects.active
2744 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2745 bpy.ops.curve.spline_type_set('INVOKE_REGION_WIN', type='BEZIER')
2746 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2748 # Make Cyclic the splines designated as Cyclic.
2749 for i in range(0, len(cyclic_loops_U)):
2750 ob_curves_surf.data.splines[i].use_cyclic_u = cyclic_loops_U[i]
2752 # Get the coords of all points on first loop-U, for later comparison with its
2753 # subdivided version, to know which points of the loops-U are crossed by the
2754 # original strokes. The indices will be the same for the other loops-U
2755 if self.loops_on_strokes:
2756 coords_loops_U_control_points = []
2757 for p in ob_ctrl_pts.data.splines[0].bezier_points:
2758 coords_loops_U_control_points.append(["%.4f" % p.co[0], "%.4f" % p.co[1], "%.4f" % p.co[2]])
2760 tuple(coords_loops_U_control_points)
2762 # Calculate number of edges-V in case option "Loops on strokes" is active or inactive
2763 if self.loops_on_strokes and not self.selection_V_exists:
2764 edges_V_count = len(self.main_splines.data.splines) * self.edges_V
2765 else:
2766 edges_V_count = len(edges_proportions_V)
2768 # The Follow precision will vary depending on the number of Follow face-loops
2769 precision_multiplier = round(2 + (edges_V_count / 15))
2770 curve_cuts = bpy.context.scene.bsurfaces.SURFSK_precision * precision_multiplier
2772 # Subdivide the curves
2773 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=curve_cuts)
2775 # The verts position shifting that happens with splines subdivision.
2776 # For later reorder splines points
2777 verts_position_shift = curve_cuts + 1
2778 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2780 # Reorder coordinates of the points of each spline to put the first point of
2781 # the spline starting at the position it was the first point before sudividing
2782 # the curve. And make a new curve object per spline (to handle memory better later)
2783 splines_U_objects = []
2784 for i in range(len(ob_curves_surf.data.splines)):
2785 spline_U_curve = bpy.data.curves.new('SURFSKIO_spline_U_' + str(i), 'CURVE')
2786 ob_spline_U = bpy.data.objects.new('SURFSKIO_spline_U_' + str(i), spline_U_curve)
2787 bpy.context.collection.objects.link(ob_spline_U)
2789 spline_U_curve.dimensions = "3D"
2791 # Add points to the spline in the new curve object
2792 ob_spline_U.data.splines.new('BEZIER')
2793 for t in range(len(ob_curves_surf.data.splines[i].bezier_points)):
2794 if cyclic_loops_U[i] is True and not self.selection_U_exists: # If the loop is cyclic
2795 if t + verts_position_shift <= len(ob_curves_surf.data.splines[i].bezier_points) - 1:
2796 point_index = t + verts_position_shift
2797 else:
2798 point_index = t + verts_position_shift - len(ob_curves_surf.data.splines[i].bezier_points)
2799 else:
2800 point_index = t
2801 # to avoid adding the first point since it's added when the spline is created
2802 if t > 0:
2803 ob_spline_U.data.splines[0].bezier_points.add(1)
2804 ob_spline_U.data.splines[0].bezier_points[t].co = \
2805 ob_curves_surf.data.splines[i].bezier_points[point_index].co
2807 if cyclic_loops_U[i] is True and not self.selection_U_exists: # If the loop is cyclic
2808 # Add a last point at the same location as the first one
2809 ob_spline_U.data.splines[0].bezier_points.add(1)
2810 ob_spline_U.data.splines[0].bezier_points[len(ob_spline_U.data.splines[0].bezier_points) - 1].co = \
2811 ob_spline_U.data.splines[0].bezier_points[0].co
2812 else:
2813 ob_spline_U.data.splines[0].use_cyclic_u = False
2815 splines_U_objects.append(ob_spline_U)
2816 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2817 ob_spline_U.select_set(True)
2818 bpy.context.view_layer.objects.active = ob_spline_U
2820 # When option "Loops on strokes" is active each "Cross" loop will have
2821 # its own proportions according to where the original strokes "touch" them
2822 if self.loops_on_strokes:
2823 # Get the indices of points where the original strokes "touch" loops-U
2824 points_U_crossed_by_strokes = []
2825 for i in range(len(splines_U_objects[0].data.splines[0].bezier_points)):
2826 bp = splines_U_objects[0].data.splines[0].bezier_points[i]
2827 if ["%.4f" % bp.co[0], "%.4f" % bp.co[1], "%.4f" % bp.co[2]] in coords_loops_U_control_points:
2828 points_U_crossed_by_strokes.append(i)
2830 # Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke
2831 edge_order_number_for_splines = {}
2832 if self.selection_V_exists:
2833 # For two-connected selections add a first hypothetic stroke at the beginning.
2834 if selection_type == "TWO_CONNECTED":
2835 edge_order_number_for_splines[0] = 0
2837 for i in range(len(self.main_splines.data.splines)):
2838 sp = self.main_splines.data.splines[i]
2839 v_idx, _dist_temp = self.shortest_distance(
2840 self.main_object,
2841 sp.bezier_points[0].co,
2842 verts_ordered_V_indices
2844 # Get the position (edges count) of the vert v_idx in the selected chain V
2845 edge_idx_in_chain = verts_ordered_V_indices.index(v_idx)
2847 # For two-connected selections the strokes go after the
2848 # hypothetic stroke added before, so the index adds one per spline
2849 if selection_type == "TWO_CONNECTED":
2850 spline_number = i + 1
2851 else:
2852 spline_number = i
2854 edge_order_number_for_splines[spline_number] = edge_idx_in_chain
2856 # Get the first and last verts indices for later comparison
2857 if i == 0:
2858 first_v_idx = v_idx
2859 elif i == len(self.main_splines.data.splines) - 1:
2860 last_v_idx = v_idx
2862 if self.selection_V_is_closed:
2863 # If there is no last stroke on the last vertex (same as first vertex),
2864 # add a hypothetic spline at last vert order
2865 if first_v_idx != last_v_idx:
2866 edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
2867 len(verts_ordered_V_indices) - 1
2868 else:
2869 if self.cyclic_cross:
2870 edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
2871 len(verts_ordered_V_indices) - 2
2872 edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
2873 len(verts_ordered_V_indices) - 1
2874 else:
2875 edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
2876 len(verts_ordered_V_indices) - 1
2878 # Get the coords of the points distributed along the
2879 # "crossing curves", with appropriate proportions-V
2880 surface_splines_parsed = []
2881 for i in range(len(splines_U_objects)):
2882 sp_ob = splines_U_objects[i]
2883 # If "Loops on strokes" option is active, calculate the proportions for each loop-U
2884 if self.loops_on_strokes:
2885 # Segments distances from stroke to stroke
2886 dist = 0
2887 full_dist = 0
2888 segments_distances = []
2889 for t in range(len(sp_ob.data.splines[0].bezier_points)):
2890 bp = sp_ob.data.splines[0].bezier_points[t]
2892 if t == 0:
2893 last_p = bp.co
2894 else:
2895 actual_p = bp.co
2896 dist += (last_p - actual_p).length
2898 if t in points_U_crossed_by_strokes:
2899 segments_distances.append(dist)
2900 full_dist += dist
2902 dist = 0
2904 last_p = actual_p
2906 # Calculate Proportions.
2907 used_edges_proportions_V = []
2908 for t in range(len(segments_distances)):
2909 if self.selection_V_exists:
2910 if t == 0:
2911 order_number_last_stroke = 0
2913 segment_edges_length_V = 0
2914 segment_edges_length_V2 = 0
2915 for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
2916 segment_edges_length_V += edges_lengths_V[order]
2917 if self.selection_V2_exists:
2918 segment_edges_length_V2 += edges_lengths_V2[order]
2920 for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
2921 # Calculate each "sub-segment" (the ones between each stroke) length
2922 if self.selection_V2_exists:
2923 proportion_sub_seg = (edges_lengths_V2[order] -
2924 ((edges_lengths_V2[order] - edges_lengths_V[order]) /
2925 len(splines_U_objects) * i)) / (segment_edges_length_V2 -
2926 (segment_edges_length_V2 - segment_edges_length_V) /
2927 len(splines_U_objects) * i)
2929 sub_seg_dist = segments_distances[t] * proportion_sub_seg
2930 else:
2931 proportion_sub_seg = edges_lengths_V[order] / segment_edges_length_V
2932 sub_seg_dist = segments_distances[t] * proportion_sub_seg
2934 used_edges_proportions_V.append(sub_seg_dist / full_dist)
2936 order_number_last_stroke = edge_order_number_for_splines[t + 1]
2938 else:
2939 for _c in range(self.edges_V):
2940 # Calculate each "sub-segment" (the ones between each stroke) length
2941 sub_seg_dist = segments_distances[t] / self.edges_V
2942 used_edges_proportions_V.append(sub_seg_dist / full_dist)
2944 actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
2945 surface_splines_parsed.append(actual_spline[0])
2947 else:
2948 if self.selection_V2_exists:
2949 used_edges_proportions_V = []
2950 for p in range(len(edges_proportions_V)):
2951 used_edges_proportions_V.append(
2952 edges_proportions_V2[p] -
2953 ((edges_proportions_V2[p] -
2954 edges_proportions_V[p]) / len(splines_U_objects) * i)
2956 else:
2957 used_edges_proportions_V = edges_proportions_V
2959 actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
2960 surface_splines_parsed.append(actual_spline[0])
2962 # Set the verts of the first and last splines to the locations
2963 # of the respective verts in the selections
2964 if self.selection_V_exists:
2965 for i in range(0, len(surface_splines_parsed[0])):
2966 surface_splines_parsed[len(surface_splines_parsed) - 1][i] = \
2967 self.main_object.matrix_world @ verts_ordered_V[i].co
2969 if selection_type == "TWO_NOT_CONNECTED":
2970 if self.selection_V2_exists:
2971 for i in range(0, len(surface_splines_parsed[0])):
2972 surface_splines_parsed[0][i] = self.main_object.matrix_world @ verts_ordered_V2[i].co
2974 # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
2975 # merge the verts of the tips of the loops when they are "near enough"
2976 if self.automatic_join and selection_type != "TWO_CONNECTED":
2977 # Join the tips of "Follow" loops that are near enough and must be "closed"
2978 if not self.selection_V_exists and len(edges_proportions_U) >= 3:
2979 for i in range(len(surface_splines_parsed[0])):
2980 sp = surface_splines_parsed
2981 loop_segment_dist = (sp[0][i] - sp[1][i]).length
2983 verts_middle_position_co = [
2984 (sp[0][i][0] + sp[len(sp) - 1][i][0]) / 2,
2985 (sp[0][i][1] + sp[len(sp) - 1][i][1]) / 2,
2986 (sp[0][i][2] + sp[len(sp) - 1][i][2]) / 2
2988 points_original = []
2989 points_original.append(sp[1][i])
2990 points_original.append(sp[0][i])
2992 points_target = []
2993 points_target.append(sp[1][i])
2994 points_target.append(Vector(verts_middle_position_co))
2996 vec_A = points_original[0] - points_original[1]
2997 vec_B = points_target[0] - points_target[1]
2998 # check for zero angles, not sure if it is a great fix
2999 if vec_A.length != 0 and vec_B.length != 0:
3000 angle = vec_A.angle(vec_B) / pi
3001 edge_new_length = (Vector(verts_middle_position_co) - sp[1][i]).length
3002 else:
3003 angle = 0
3004 edge_new_length = 0
3006 # If after moving the verts to the middle point, the segment doesn't stretch too much
3007 if edge_new_length <= loop_segment_dist * 1.5 * \
3008 self.join_stretch_factor and angle < 0.25 * self.join_stretch_factor:
3010 # Avoid joining when the actual loop must be merged with the original mesh
3011 if not (self.selection_U_exists and i == 0) and \
3012 not (self.selection_U2_exists and i == len(surface_splines_parsed[0]) - 1):
3014 # Change the coords of both verts to the middle position
3015 surface_splines_parsed[0][i] = verts_middle_position_co
3016 surface_splines_parsed[len(surface_splines_parsed) - 1][i] = verts_middle_position_co
3018 # Delete object with control points and object from grease pencil conversion
3019 bpy.ops.object.delete({"selected_objects": [ob_ctrl_pts]})
3021 bpy.ops.object.delete({"selected_objects": splines_U_objects})
3023 # Generate surface
3025 # Get all verts coords
3026 all_surface_verts_co = []
3027 for i in range(0, len(surface_splines_parsed)):
3028 # Get coords of all verts and make a list with them
3029 for pt_co in surface_splines_parsed[i]:
3030 all_surface_verts_co.append(pt_co)
3032 # Define verts for each face
3033 all_surface_faces = []
3034 for i in range(0, len(all_surface_verts_co) - len(surface_splines_parsed[0])):
3035 if ((i + 1) / len(surface_splines_parsed[0]) != int((i + 1) / len(surface_splines_parsed[0]))):
3036 all_surface_faces.append(
3037 [i + 1, i, i + len(surface_splines_parsed[0]),
3038 i + len(surface_splines_parsed[0]) + 1]
3040 # Build the mesh
3041 surf_me_name = "SURFSKIO_surface"
3042 me_surf = bpy.data.meshes.new(surf_me_name)
3043 me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
3044 ob_surface = object_utils.object_data_add(context, me_surf)
3045 ob_surface.location = (0.0, 0.0, 0.0)
3046 ob_surface.rotation_euler = (0.0, 0.0, 0.0)
3047 ob_surface.scale = (1.0, 1.0, 1.0)
3049 # Select all the "unselected but participating" verts, from closed selection
3050 # or double selections with middle-vertex, for later join with remove doubles
3051 for v_idx in single_unselected_verts:
3052 self.main_object.data.vertices[v_idx].select = True
3054 # Join the new mesh to the main object
3055 ob_surface.select_set(True)
3056 self.main_object.select_set(True)
3057 bpy.context.view_layer.objects.active = self.main_object
3059 bpy.ops.object.join('INVOKE_REGION_WIN')
3061 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3063 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN', threshold=0.0001)
3064 bpy.ops.mesh.normals_make_consistent('INVOKE_REGION_WIN', inside=False)
3065 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
3067 self.update()
3069 return{'FINISHED'}
3071 def update(self):
3072 try:
3073 global global_shade_smooth
3074 if global_shade_smooth:
3075 bpy.ops.object.shade_smooth()
3076 else:
3077 bpy.ops.object.shade_flat()
3078 bpy.context.scene.bsurfaces.SURFSK_shade_smooth = global_shade_smooth
3079 except:
3080 pass
3082 return{'FINISHED'}
3084 def execute(self, context):
3086 if bpy.ops.object.mode_set.poll():
3087 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3089 try:
3090 global global_mesh_object
3091 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
3092 bpy.data.objects[global_mesh_object].select_set(True)
3093 self.main_object = bpy.data.objects[global_mesh_object]
3094 bpy.context.view_layer.objects.active = self.main_object
3095 bsurfaces_props = bpy.context.scene.bsurfaces
3096 except:
3097 self.report({'WARNING'}, "Specify the name of the object with retopology")
3098 return{"CANCELLED"}
3099 bpy.context.view_layer.objects.active = self.main_object
3101 self.update()
3103 if not self.is_fill_faces:
3104 bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
3105 value='True, False, False')
3107 # Build splines from the "last saved splines".
3108 last_saved_curve = bpy.data.curves.new('SURFSKIO_last_crv', 'CURVE')
3109 self.main_splines = bpy.data.objects.new('SURFSKIO_last_crv', last_saved_curve)
3110 bpy.context.collection.objects.link(self.main_splines)
3112 last_saved_curve.dimensions = "3D"
3114 for sp in self.last_strokes_splines_coords:
3115 spline = self.main_splines.data.splines.new('BEZIER')
3116 # less one because one point is added when the spline is created
3117 spline.bezier_points.add(len(sp) - 1)
3118 for p in range(0, len(sp)):
3119 spline.bezier_points[p].co = [sp[p][0], sp[p][1], sp[p][2]]
3121 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3123 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3124 self.main_splines.select_set(True)
3125 bpy.context.view_layer.objects.active = self.main_splines
3127 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
3129 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
3130 # Important to make it vector first and then automatic, otherwise the
3131 # tips handles get too big and distort the shrinkwrap results later
3132 bpy.ops.curve.handle_type_set(type='VECTOR')
3133 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
3134 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3135 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3137 self.main_splines.name = "SURFSKIO_temp_strokes"
3139 if self.is_crosshatch:
3140 strokes_for_crosshatch = True
3141 strokes_for_rectangular_surface = False
3142 else:
3143 strokes_for_rectangular_surface = True
3144 strokes_for_crosshatch = False
3146 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3148 if strokes_for_rectangular_surface:
3149 self.rectangular_surface(context)
3150 elif strokes_for_crosshatch:
3151 self.crosshatch_surface_execute(context)
3153 #Set Shade smooth to new polygons
3154 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3155 global global_shade_smooth
3156 if global_shade_smooth:
3157 bpy.ops.object.shade_smooth()
3158 else:
3159 bpy.ops.object.shade_flat()
3161 # Delete main splines
3162 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3163 if self.keep_strokes:
3164 self.main_splines.name = "keep_strokes"
3165 self.main_splines.data.bevel_depth = 0.001
3166 if "keep_strokes_material" in bpy.data.materials :
3167 self.main_splines.data.materials.append(bpy.data.materials["keep_strokes_material"])
3168 else:
3169 mat = bpy.data.materials.new("keep_strokes_material")
3170 mat.diffuse_color = (1, 0, 0, 0)
3171 mat.specular_color = (1, 0, 0)
3172 mat.specular_intensity = 0.0
3173 mat.roughness = 0.0
3174 self.main_splines.data.materials.append(mat)
3175 else:
3176 bpy.ops.object.delete({"selected_objects": [self.main_splines]})
3178 # Delete grease pencil strokes
3179 if self.strokes_type == "GP_STROKES" and not self.stopping_errors:
3180 try:
3181 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
3182 except:
3183 pass
3185 # Delete annotations
3186 if self.strokes_type == "GP_ANNOTATION" and not self.stopping_errors:
3187 try:
3188 bpy.context.annotation_data.layers.active.clear()
3189 except:
3190 pass
3192 bsurfaces_props = bpy.context.scene.bsurfaces
3193 bsurfaces_props.SURFSK_edges_U = self.edges_U
3194 bsurfaces_props.SURFSK_edges_V = self.edges_V
3195 bsurfaces_props.SURFSK_cyclic_cross = self.cyclic_cross
3196 bsurfaces_props.SURFSK_cyclic_follow = self.cyclic_follow
3197 bsurfaces_props.SURFSK_automatic_join = self.automatic_join
3198 bsurfaces_props.SURFSK_loops_on_strokes = self.loops_on_strokes
3199 bsurfaces_props.SURFSK_keep_strokes = self.keep_strokes
3201 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3202 self.main_object.select_set(True)
3203 bpy.context.view_layer.objects.active = self.main_object
3205 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3207 self.update()
3209 return{'FINISHED'}
3211 def invoke(self, context, event):
3213 if bpy.ops.object.mode_set.poll():
3214 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3216 bsurfaces_props = bpy.context.scene.bsurfaces
3217 self.cyclic_cross = bsurfaces_props.SURFSK_cyclic_cross
3218 self.cyclic_follow = bsurfaces_props.SURFSK_cyclic_follow
3219 self.automatic_join = bsurfaces_props.SURFSK_automatic_join
3220 self.loops_on_strokes = bsurfaces_props.SURFSK_loops_on_strokes
3221 self.keep_strokes = bsurfaces_props.SURFSK_keep_strokes
3223 try:
3224 global global_mesh_object
3225 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
3226 bpy.data.objects[global_mesh_object].select_set(True)
3227 self.main_object = bpy.data.objects[global_mesh_object]
3228 bpy.context.view_layer.objects.active = self.main_object
3229 except:
3230 self.report({'WARNING'}, "Specify the name of the object with retopology")
3231 return{"CANCELLED"}
3233 self.update()
3235 self.main_object_selected_verts_count = len([v for v in self.main_object.data.vertices if v.select])
3237 bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
3238 value='True, False, False')
3240 self.edges_U = bsurfaces_props.SURFSK_edges_U
3241 self.edges_V = bsurfaces_props.SURFSK_edges_V
3243 self.is_fill_faces = False
3244 self.stopping_errors = False
3245 self.last_strokes_splines_coords = []
3247 # Determine the type of the strokes
3248 self.strokes_type = get_strokes_type(context)
3250 # Check if it will be used grease pencil strokes or curves
3251 # If there are strokes to be used
3252 if self.strokes_type == "GP_STROKES" or self.strokes_type == "EXTERNAL_CURVE" or self.strokes_type == "GP_ANNOTATION":
3253 if self.strokes_type == "GP_STROKES":
3254 # Convert grease pencil strokes to curve
3255 global global_gpencil_object
3256 gp = bpy.data.objects[global_gpencil_object]
3257 self.original_curve = conver_gpencil_to_curve(self, context, gp, 'GPensil')
3258 self.using_external_curves = False
3260 elif self.strokes_type == "GP_ANNOTATION":
3261 # Convert grease pencil strokes to curve
3262 gp = bpy.context.annotation_data
3263 self.original_curve = conver_gpencil_to_curve(self, context, gp, 'Annotation')
3264 self.using_external_curves = False
3266 elif self.strokes_type == "EXTERNAL_CURVE":
3267 global global_curve_object
3268 self.original_curve = bpy.data.objects[global_curve_object]
3269 self.using_external_curves = True
3271 # Make sure there are no objects left from erroneous
3272 # executions of this operator, with the reserved names used here
3273 for o in bpy.data.objects:
3274 if o.name.find("SURFSKIO_") != -1:
3275 bpy.ops.object.delete({"selected_objects": [o]})
3277 bpy.context.view_layer.objects.active = self.original_curve
3279 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3281 self.temporary_curve = bpy.context.view_layer.objects.active
3283 # Deselect all points of the curve
3284 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3285 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3286 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3288 # Delete splines with only a single isolated point
3289 for i in range(len(self.temporary_curve.data.splines)):
3290 sp = self.temporary_curve.data.splines[i]
3292 if len(sp.bezier_points) == 1:
3293 sp.bezier_points[0].select_control_point = True
3295 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3296 bpy.ops.curve.delete(type='VERT')
3297 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3299 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3300 self.temporary_curve.select_set(True)
3301 bpy.context.view_layer.objects.active = self.temporary_curve
3303 # Set a minimum number of points for crosshatch
3304 minimum_points_num = 15
3306 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3307 # Check if the number of points of each curve has at least the number of points
3308 # of minimum_points_num, which is a bit more than the face-loops limit.
3309 # If not, subdivide to reach at least that number of points
3310 for i in range(len(self.temporary_curve.data.splines)):
3311 sp = self.temporary_curve.data.splines[i]
3313 if len(sp.bezier_points) < minimum_points_num:
3314 for bp in sp.bezier_points:
3315 bp.select_control_point = True
3317 if (len(sp.bezier_points) - 1) != 0:
3318 # Formula to get the number of cuts that will make a curve
3319 # of N number of points have near to "minimum_points_num"
3320 # points, when subdividing with this number of cuts
3321 subdivide_cuts = int(
3322 (minimum_points_num - len(sp.bezier_points)) /
3323 (len(sp.bezier_points) - 1)
3324 ) + 1
3325 else:
3326 subdivide_cuts = 0
3328 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3329 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3331 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3333 # Detect if the strokes are a crosshatch and do it if it is
3334 self.crosshatch_surface_invoke(self.temporary_curve)
3336 if not self.is_crosshatch:
3337 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3338 self.temporary_curve.select_set(True)
3339 bpy.context.view_layer.objects.active = self.temporary_curve
3341 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3343 # Set a minimum number of points for rectangular surfaces
3344 minimum_points_num = 60
3346 # Check if the number of points of each curve has at least the number of points
3347 # of minimum_points_num, which is a bit more than the face-loops limit.
3348 # If not, subdivide to reach at least that number of points
3349 for i in range(len(self.temporary_curve.data.splines)):
3350 sp = self.temporary_curve.data.splines[i]
3352 if len(sp.bezier_points) < minimum_points_num:
3353 for bp in sp.bezier_points:
3354 bp.select_control_point = True
3356 if (len(sp.bezier_points) - 1) != 0:
3357 # Formula to get the number of cuts that will make a curve of
3358 # N number of points have near to "minimum_points_num" points,
3359 # when subdividing with this number of cuts
3360 subdivide_cuts = int(
3361 (minimum_points_num - len(sp.bezier_points)) /
3362 (len(sp.bezier_points) - 1)
3363 ) + 1
3364 else:
3365 subdivide_cuts = 0
3367 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3368 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3370 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3372 # Save coordinates of the actual strokes (as the "last saved splines")
3373 for sp_idx in range(len(self.temporary_curve.data.splines)):
3374 self.last_strokes_splines_coords.append([])
3375 for bp_idx in range(len(self.temporary_curve.data.splines[sp_idx].bezier_points)):
3376 coords = self.temporary_curve.matrix_world @ \
3377 self.temporary_curve.data.splines[sp_idx].bezier_points[bp_idx].co
3378 self.last_strokes_splines_coords[sp_idx].append([coords[0], coords[1], coords[2]])
3380 # Check for cyclic splines, put the first and last points in the middle of their actual positions
3381 for sp_idx in range(len(self.temporary_curve.data.splines)):
3382 if self.temporary_curve.data.splines[sp_idx].use_cyclic_u is True:
3383 first_p_co = self.last_strokes_splines_coords[sp_idx][0]
3384 last_p_co = self.last_strokes_splines_coords[sp_idx][
3385 len(self.last_strokes_splines_coords[sp_idx]) - 1
3387 target_co = [
3388 (first_p_co[0] + last_p_co[0]) / 2,
3389 (first_p_co[1] + last_p_co[1]) / 2,
3390 (first_p_co[2] + last_p_co[2]) / 2
3393 self.last_strokes_splines_coords[sp_idx][0] = target_co
3394 self.last_strokes_splines_coords[sp_idx][
3395 len(self.last_strokes_splines_coords[sp_idx]) - 1
3396 ] = target_co
3397 tuple(self.last_strokes_splines_coords)
3399 # Estimation of the average length of the segments between
3400 # each point of the grease pencil strokes.
3401 # Will be useful to determine whether a curve should be made "Cyclic"
3402 segments_lengths_sum = 0
3403 segments_count = 0
3404 random_spline = self.temporary_curve.data.splines[0].bezier_points
3405 for i in range(0, len(random_spline)):
3406 if i != 0 and len(random_spline) - 1 >= i:
3407 segments_lengths_sum += (random_spline[i - 1].co - random_spline[i].co).length
3408 segments_count += 1
3410 self.average_gp_segment_length = segments_lengths_sum / segments_count
3412 # Delete temporary strokes curve object
3413 bpy.ops.object.delete({"selected_objects": [self.temporary_curve]})
3415 # Set again since "execute()" will turn it again to its initial value
3416 self.execute(context)
3418 if not self.stopping_errors:
3419 # Delete grease pencil strokes
3420 if self.strokes_type == "GP_STROKES":
3421 try:
3422 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
3423 except:
3424 pass
3426 # Delete annotation strokes
3427 elif self.strokes_type == "GP_ANNOTATION":
3428 try:
3429 bpy.context.annotation_data.layers.active.clear()
3430 except:
3431 pass
3433 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3434 bpy.ops.object.delete({"selected_objects": [self.original_curve]})
3435 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3437 return {"FINISHED"}
3438 else:
3439 return{"CANCELLED"}
3441 elif self.strokes_type == "SELECTION_ALONE":
3442 self.is_fill_faces = True
3443 created_faces_count = self.fill_with_faces(self.main_object)
3445 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3447 if created_faces_count == 0:
3448 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3449 return {"CANCELLED"}
3450 else:
3451 return {"FINISHED"}
3453 if self.strokes_type == "EXTERNAL_NO_CURVE":
3454 self.report({'WARNING'}, "The secondary object is not a Curve.")
3455 return{"CANCELLED"}
3457 elif self.strokes_type == "MORE_THAN_ONE_EXTERNAL":
3458 self.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3459 return{"CANCELLED"}
3461 elif self.strokes_type == "SINGLE_GP_STROKE_NO_SELECTION" or \
3462 self.strokes_type == "SINGLE_CURVE_STROKE_NO_SELECTION":
3464 self.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
3465 return{"CANCELLED"}
3467 elif self.strokes_type == "NO_STROKES":
3468 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3469 return{"CANCELLED"}
3471 elif self.strokes_type == "CURVE_WITH_NON_BEZIER_SPLINES":
3472 self.report({'WARNING'}, "All splines must be Bezier.")
3473 return{"CANCELLED"}
3475 else:
3476 return{"CANCELLED"}
3478 # ----------------------------
3479 # Init operator
3480 class MESH_OT_SURFSK_init(Operator):
3481 bl_idname = "mesh.surfsk_init"
3482 bl_label = "Bsurfaces initialize"
3483 bl_description = "Add an empty mesh object with useful settings"
3484 bl_options = {'REGISTER', 'UNDO'}
3486 def execute(self, context):
3488 bs = bpy.context.scene.bsurfaces
3490 if bpy.ops.object.mode_set.poll():
3491 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3493 global global_shade_smooth
3494 global global_mesh_object
3495 global global_gpencil_object
3497 if bs.SURFSK_mesh == None:
3498 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3499 mesh = bpy.data.meshes.new('BSurfaceMesh')
3500 mesh_object = object_utils.object_data_add(context, mesh)
3501 mesh_object.select_set(True)
3502 bpy.context.view_layer.objects.active = mesh_object
3504 mesh_object.show_all_edges = True
3505 mesh_object.display_type = 'SOLID'
3506 mesh_object.show_wire = True
3508 global_shade_smooth = bpy.context.scene.bsurfaces.SURFSK_shade_smooth
3509 if global_shade_smooth:
3510 bpy.ops.object.shade_smooth()
3511 else:
3512 bpy.ops.object.shade_flat()
3514 color_red = [1.0, 0.0, 0.0, 0.3]
3515 material = makeMaterial("BSurfaceMesh", color_red)
3516 mesh_object.data.materials.append(material)
3517 bpy.ops.object.modifier_add(type='SHRINKWRAP')
3518 modifier = mesh_object.modifiers["Shrinkwrap"]
3519 if self.active_object is not None:
3520 modifier.target = self.active_object
3521 modifier.wrap_method = 'TARGET_PROJECT'
3522 modifier.wrap_mode = 'OUTSIDE_SURFACE'
3523 modifier.show_on_cage = True
3525 global_mesh_object = mesh_object.name
3526 bpy.context.scene.bsurfaces.SURFSK_mesh = bpy.data.objects[global_mesh_object]
3528 bpy.context.scene.tool_settings.snap_elements = {'FACE'}
3529 bpy.context.scene.tool_settings.use_snap = True
3530 bpy.context.scene.tool_settings.use_snap_self = False
3531 bpy.context.scene.tool_settings.use_snap_align_rotation = True
3532 bpy.context.scene.tool_settings.use_snap_project = True
3533 bpy.context.scene.tool_settings.use_snap_rotate = True
3534 bpy.context.scene.tool_settings.use_snap_scale = True
3536 bpy.context.scene.tool_settings.use_mesh_automerge = True
3537 bpy.context.scene.tool_settings.double_threshold = 0.01
3539 if context.scene.bsurfaces.SURFSK_guide == 'GPencil' and bs.SURFSK_gpencil == None:
3540 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3541 bpy.ops.object.gpencil_add(radius=1.0, align='WORLD', location=(0.0, 0.0, 0.0), rotation=(0.0, 0.0, 0.0), type='EMPTY')
3542 bpy.context.scene.tool_settings.gpencil_stroke_placement_view3d = 'SURFACE'
3543 gpencil_object = bpy.context.scene.objects[bpy.context.scene.objects[-1].name]
3544 gpencil_object.select_set(True)
3545 bpy.context.view_layer.objects.active = gpencil_object
3546 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
3547 global_gpencil_object = gpencil_object.name
3548 bpy.context.scene.bsurfaces.SURFSK_gpencil = bpy.data.objects[global_gpencil_object]
3549 gpencil_object.data.stroke_depth_order = '3D'
3550 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
3551 bpy.ops.wm.tool_set_by_id(name="builtin_brush.Draw")
3553 if context.scene.bsurfaces.SURFSK_guide == 'Annotation':
3554 bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
3555 bpy.context.scene.tool_settings.annotation_stroke_placement_view3d = 'SURFACE'
3557 def invoke(self, context, event):
3558 if bpy.context.active_object:
3559 self.active_object = bpy.context.active_object
3560 else:
3561 self.active_object = None
3563 self.execute(context)
3565 return {"FINISHED"}
3567 # ----------------------------
3568 # Add modifiers operator
3569 class MESH_OT_SURFSK_add_modifiers(Operator):
3570 bl_idname = "mesh.surfsk_add_modifiers"
3571 bl_label = "Add Mirror and others modifiers"
3572 bl_description = "Add modifiers: Mirror, Shrinkwrap, Subdivision, Solidify"
3573 bl_options = {'REGISTER', 'UNDO'}
3575 def execute(self, context):
3577 bs = bpy.context.scene.bsurfaces
3579 if bpy.ops.object.mode_set.poll():
3580 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3582 if bs.SURFSK_mesh == None:
3583 self.report({'ERROR_INVALID_INPUT'}, "Please select Mesh of BSurface or click Initialize")
3584 else:
3585 mesh_object = bs.SURFSK_mesh
3587 try:
3588 mesh_object.select_set(True)
3589 except:
3590 self.report({'ERROR_INVALID_INPUT'}, "Mesh of BSurface does not exist")
3591 return {"CANCEL"}
3593 bpy.context.view_layer.objects.active = mesh_object
3595 try:
3596 shrinkwrap = mesh_object.modifiers["Shrinkwrap"]
3597 if self.active_object is not None and self.active_object != mesh_object:
3598 shrinkwrap.target = self.active_object
3599 shrinkwrap.wrap_method = 'TARGET_PROJECT'
3600 shrinkwrap.wrap_mode = 'OUTSIDE_SURFACE'
3601 shrinkwrap.show_on_cage = True
3602 shrinkwrap.offset = bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset
3603 except:
3604 bpy.ops.object.modifier_add(type='SHRINKWRAP')
3605 shrinkwrap = mesh_object.modifiers["Shrinkwrap"]
3606 if self.active_object is not None and self.active_object != mesh_object:
3607 shrinkwrap.target = self.active_object
3608 shrinkwrap.wrap_method = 'TARGET_PROJECT'
3609 shrinkwrap.wrap_mode = 'OUTSIDE_SURFACE'
3610 shrinkwrap.show_on_cage = True
3611 shrinkwrap.offset = bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset
3613 try:
3614 mirror = mesh_object.modifiers["Mirror"]
3615 mirror.use_clip = True
3616 except:
3617 bpy.ops.object.modifier_add(type='MIRROR')
3618 mirror = mesh_object.modifiers["Mirror"]
3619 mirror.use_clip = True
3621 try:
3622 _subsurf = mesh_object.modifiers["Subdivision"]
3623 except:
3624 bpy.ops.object.modifier_add(type='SUBSURF')
3625 _subsurf = mesh_object.modifiers["Subdivision"]
3627 try:
3628 solidify = mesh_object.modifiers["Solidify"]
3629 solidify.thickness = 0.01
3630 except:
3631 bpy.ops.object.modifier_add(type='SOLIDIFY')
3632 solidify = mesh_object.modifiers["Solidify"]
3633 solidify.thickness = 0.01
3635 return {"FINISHED"}
3637 def invoke(self, context, event):
3638 if bpy.context.active_object:
3639 self.active_object = bpy.context.active_object
3640 else:
3641 self.active_object = None
3643 self.execute(context)
3645 return {"FINISHED"}
3647 # ----------------------------
3648 # Edit surface operator
3649 class MESH_OT_SURFSK_edit_surface(Operator):
3650 bl_idname = "mesh.surfsk_edit_surface"
3651 bl_label = "Bsurfaces edit surface"
3652 bl_description = "Edit surface mesh"
3653 bl_options = {'REGISTER', 'UNDO'}
3655 def execute(self, context):
3656 if bpy.ops.object.mode_set.poll():
3657 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3658 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3659 bpy.context.scene.bsurfaces.SURFSK_mesh.select_set(True)
3660 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_mesh
3661 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
3662 bpy.ops.wm.tool_set_by_id(name="builtin.select")
3664 def invoke(self, context, event):
3665 try:
3666 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
3667 bpy.data.objects[global_mesh_object].select_set(True)
3668 self.main_object = bpy.data.objects[global_mesh_object]
3669 bpy.context.view_layer.objects.active = self.main_object
3670 except:
3671 self.report({'WARNING'}, "Specify the name of the object with retopology")
3672 return{"CANCELLED"}
3674 self.execute(context)
3676 return {"FINISHED"}
3678 # ----------------------------
3679 # Add strokes operator
3680 class GPENCIL_OT_SURFSK_add_strokes(Operator):
3681 bl_idname = "gpencil.surfsk_add_strokes"
3682 bl_label = "Bsurfaces add strokes"
3683 bl_description = "Add the grease pencil strokes"
3684 bl_options = {'REGISTER', 'UNDO'}
3686 def execute(self, context):
3687 if bpy.ops.object.mode_set.poll():
3688 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3689 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3691 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3692 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_gpencil
3693 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
3694 bpy.ops.wm.tool_set_by_id(name="builtin_brush.Draw")
3696 return{"FINISHED"}
3698 def invoke(self, context, event):
3699 try:
3700 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3701 except:
3702 self.report({'WARNING'}, "Specify the name of the object with strokes")
3703 return{"CANCELLED"}
3705 self.execute(context)
3707 return {"FINISHED"}
3709 # ----------------------------
3710 # Edit strokes operator
3711 class GPENCIL_OT_SURFSK_edit_strokes(Operator):
3712 bl_idname = "gpencil.surfsk_edit_strokes"
3713 bl_label = "Bsurfaces edit strokes"
3714 bl_description = "Edit the grease pencil strokes"
3715 bl_options = {'REGISTER', 'UNDO'}
3717 def execute(self, context):
3718 if bpy.ops.object.mode_set.poll():
3719 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3720 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3722 gpencil_object = bpy.context.scene.bsurfaces.SURFSK_gpencil
3724 gpencil_object.select_set(True)
3725 bpy.context.view_layer.objects.active = gpencil_object
3727 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT_GPENCIL')
3728 try:
3729 bpy.ops.gpencil.select_all(action='SELECT')
3730 except:
3731 pass
3733 def invoke(self, context, event):
3734 try:
3735 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3736 except:
3737 self.report({'WARNING'}, "Specify the name of the object with strokes")
3738 return{"CANCELLED"}
3740 self.execute(context)
3742 return {"FINISHED"}
3744 # ----------------------------
3745 # Convert annotation to curves operator
3746 class GPENCIL_OT_SURFSK_annotation_to_curves(Operator):
3747 bl_idname = "gpencil.surfsk_annotations_to_curves"
3748 bl_label = "Convert annotation to curves"
3749 bl_description = "Convert annotation to curves for editing"
3750 bl_options = {'REGISTER', 'UNDO'}
3752 def execute(self, context):
3754 if bpy.ops.object.mode_set.poll():
3755 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3757 # Convert annotation to curve
3758 curve = conver_gpencil_to_curve(self, context, None, 'Annotation')
3760 if curve != None:
3761 # Delete annotation strokes
3762 try:
3763 bpy.context.annotation_data.layers.active.clear()
3764 except:
3765 pass
3767 # Clean up curves
3768 curve.select_set(True)
3769 bpy.context.view_layer.objects.active = curve
3771 bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
3773 return {"FINISHED"}
3775 def invoke(self, context, event):
3776 try:
3777 strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
3779 _strokes_num = len(strokes)
3780 except:
3781 self.report({'WARNING'}, "Not active annotation")
3782 return{"CANCELLED"}
3784 self.execute(context)
3786 return {"FINISHED"}
3788 # ----------------------------
3789 # Convert strokes to curves operator
3790 class GPENCIL_OT_SURFSK_strokes_to_curves(Operator):
3791 bl_idname = "gpencil.surfsk_strokes_to_curves"
3792 bl_label = "Convert strokes to curves"
3793 bl_description = "Convert grease pencil strokes to curves for editing"
3794 bl_options = {'REGISTER', 'UNDO'}
3796 def execute(self, context):
3798 if bpy.ops.object.mode_set.poll():
3799 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3801 # Convert grease pencil strokes to curve
3802 gp = bpy.context.scene.bsurfaces.SURFSK_gpencil
3803 curve = conver_gpencil_to_curve(self, context, gp, 'GPensil')
3805 if curve != None:
3806 # Delete grease pencil strokes
3807 try:
3808 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
3809 except:
3810 pass
3812 # Clean up curves
3814 curve.select_set(True)
3815 bpy.context.view_layer.objects.active = curve
3817 bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
3819 return {"FINISHED"}
3821 def invoke(self, context, event):
3822 try:
3823 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3824 except:
3825 self.report({'WARNING'}, "Specify the name of the object with strokes")
3826 return{"CANCELLED"}
3828 self.execute(context)
3830 return {"FINISHED"}
3832 # ----------------------------
3833 # Add annotation
3834 class GPENCIL_OT_SURFSK_add_annotation(Operator):
3835 bl_idname = "gpencil.surfsk_add_annotation"
3836 bl_label = "Bsurfaces add annotation"
3837 bl_description = "Add annotation"
3838 bl_options = {'REGISTER', 'UNDO'}
3840 def execute(self, context):
3841 bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
3842 bpy.context.scene.tool_settings.annotation_stroke_placement_view3d = 'SURFACE'
3844 return{"FINISHED"}
3846 def invoke(self, context, event):
3848 self.execute(context)
3850 return {"FINISHED"}
3853 # ----------------------------
3854 # Edit curve operator
3855 class CURVE_OT_SURFSK_edit_curve(Operator):
3856 bl_idname = "curve.surfsk_edit_curve"
3857 bl_label = "Bsurfaces edit curve"
3858 bl_description = "Edit curve"
3859 bl_options = {'REGISTER', 'UNDO'}
3861 def execute(self, context):
3862 if bpy.ops.object.mode_set.poll():
3863 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3864 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3865 bpy.context.scene.bsurfaces.SURFSK_curve.select_set(True)
3866 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_curve
3867 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
3869 def invoke(self, context, event):
3870 try:
3871 bpy.context.scene.bsurfaces.SURFSK_curve.select_set(True)
3872 except:
3873 self.report({'WARNING'}, "Specify the name of the object with curve")
3874 return{"CANCELLED"}
3876 self.execute(context)
3878 return {"FINISHED"}
3880 # ----------------------------
3881 # Reorder splines
3882 class CURVE_OT_SURFSK_reorder_splines(Operator):
3883 bl_idname = "curve.surfsk_reorder_splines"
3884 bl_label = "Bsurfaces reorder splines"
3885 bl_description = "Defines the order of the splines by using grease pencil strokes"
3886 bl_options = {'REGISTER', 'UNDO'}
3888 def execute(self, context):
3889 objects_to_delete = []
3890 # Convert grease pencil strokes to curve.
3891 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3892 bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
3893 for ob in bpy.context.selected_objects:
3894 if ob != bpy.context.view_layer.objects.active and ob.name.startswith("GP_Layer"):
3895 GP_strokes_curve = ob
3897 # GP_strokes_curve = bpy.context.object
3898 objects_to_delete.append(GP_strokes_curve)
3900 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3901 GP_strokes_curve.select_set(True)
3902 bpy.context.view_layer.objects.active = GP_strokes_curve
3904 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3905 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
3906 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=100)
3907 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3909 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3910 GP_strokes_mesh = bpy.context.object
3911 objects_to_delete.append(GP_strokes_mesh)
3913 GP_strokes_mesh.data.resolution_u = 1
3914 bpy.ops.object.convert(target='MESH', keep_original=False)
3916 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3917 self.main_curve.select_set(True)
3918 bpy.context.view_layer.objects.active = self.main_curve
3920 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3921 curves_duplicate_1 = bpy.context.object
3922 objects_to_delete.append(curves_duplicate_1)
3924 minimum_points_num = 500
3926 # Some iterations since the subdivision operator
3927 # has a limit of 100 subdivisions per iteration
3928 for x in range(round(minimum_points_num / 100)):
3929 # Check if the number of points of each curve has at least the number of points
3930 # of minimum_points_num. If not, subdivide to reach at least that number of points
3931 for i in range(len(curves_duplicate_1.data.splines)):
3932 sp = curves_duplicate_1.data.splines[i]
3934 if len(sp.bezier_points) < minimum_points_num:
3935 for bp in sp.bezier_points:
3936 bp.select_control_point = True
3938 if (len(sp.bezier_points) - 1) != 0:
3939 # Formula to get the number of cuts that will make a curve of N
3940 # number of points have near to "minimum_points_num" points,
3941 # when subdividing with this number of cuts
3942 subdivide_cuts = int(
3943 (minimum_points_num - len(sp.bezier_points)) /
3944 (len(sp.bezier_points) - 1)
3945 ) + 1
3946 else:
3947 subdivide_cuts = 0
3949 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3950 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3951 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3952 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3954 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3955 curves_duplicate_2 = bpy.context.object
3956 objects_to_delete.append(curves_duplicate_2)
3958 # Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target
3959 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3960 curves_duplicate_2.select_set(True)
3961 bpy.context.view_layer.objects.active = curves_duplicate_2
3963 bpy.ops.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
3964 curves_duplicate_2.modifiers["Shrinkwrap"].wrap_method = "NEAREST_VERTEX"
3965 curves_duplicate_2.modifiers["Shrinkwrap"].target = GP_strokes_mesh
3966 bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', modifier='Shrinkwrap')
3968 # Get the distance of each vert from its original position to its position with Shrinkwrap
3969 nearest_points_coords = {}
3970 for st_idx in range(len(curves_duplicate_1.data.splines)):
3971 for bp_idx in range(len(curves_duplicate_1.data.splines[st_idx].bezier_points)):
3972 bp_1_co = curves_duplicate_1.matrix_world @ \
3973 curves_duplicate_1.data.splines[st_idx].bezier_points[bp_idx].co
3975 bp_2_co = curves_duplicate_2.matrix_world @ \
3976 curves_duplicate_2.data.splines[st_idx].bezier_points[bp_idx].co
3978 if bp_idx == 0:
3979 shortest_dist = (bp_1_co - bp_2_co).length
3980 nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
3981 "%.4f" % bp_2_co[1],
3982 "%.4f" % bp_2_co[2])
3984 dist = (bp_1_co - bp_2_co).length
3986 if dist < shortest_dist:
3987 nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
3988 "%.4f" % bp_2_co[1],
3989 "%.4f" % bp_2_co[2])
3990 shortest_dist = dist
3992 # Get all coords of GP strokes points, for comparison
3993 GP_strokes_coords = []
3994 for st_idx in range(len(GP_strokes_curve.data.splines)):
3995 GP_strokes_coords.append(
3996 [("%.4f" % x if "%.4f" % x != "-0.00" else "0.00",
3997 "%.4f" % y if "%.4f" % y != "-0.00" else "0.00",
3998 "%.4f" % z if "%.4f" % z != "-0.00" else "0.00") for
3999 x, y, z in [bp.co for bp in GP_strokes_curve.data.splines[st_idx].bezier_points]]
4002 # Check the point of the GP strokes with the same coords as
4003 # the nearest points of the curves (with shrinkwrap)
4005 # Dictionary with GP stroke index as index, and a list as value.
4006 # The list has as index the point index of the GP stroke
4007 # nearest to the spline, and as value the spline index
4008 GP_connection_points = {}
4009 for gp_st_idx in range(len(GP_strokes_coords)):
4010 GPvert_spline_relationship = {}
4012 for splines_st_idx in range(len(nearest_points_coords)):
4013 if nearest_points_coords[splines_st_idx] in GP_strokes_coords[gp_st_idx]:
4014 GPvert_spline_relationship[
4015 GP_strokes_coords[gp_st_idx].index(nearest_points_coords[splines_st_idx])
4016 ] = splines_st_idx
4018 GP_connection_points[gp_st_idx] = GPvert_spline_relationship
4020 # Get the splines new order
4021 splines_new_order = []
4022 for i in GP_connection_points:
4023 dict_keys = sorted(GP_connection_points[i].keys()) # Sort dictionaries by key
4025 for k in dict_keys:
4026 splines_new_order.append(GP_connection_points[i][k])
4028 # Reorder
4029 curve_original_name = self.main_curve.name
4031 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4032 self.main_curve.select_set(True)
4033 bpy.context.view_layer.objects.active = self.main_curve
4035 self.main_curve.name = "SURFSKIO_CRV_ORD"
4037 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4038 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4039 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4041 for _sp_idx in range(len(self.main_curve.data.splines)):
4042 self.main_curve.data.splines[0].bezier_points[0].select_control_point = True
4044 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4045 bpy.ops.curve.separate('EXEC_REGION_WIN')
4046 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4048 # Get the names of the separated splines objects in the original order
4049 splines_unordered = {}
4050 for o in bpy.data.objects:
4051 if o.name.find("SURFSKIO_CRV_ORD") != -1:
4052 spline_order_string = o.name.partition(".")[2]
4054 if spline_order_string != "" and int(spline_order_string) > 0:
4055 spline_order_index = int(spline_order_string) - 1
4056 splines_unordered[spline_order_index] = o.name
4058 # Join all splines objects in final order
4059 for order_idx in splines_new_order:
4060 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4061 bpy.data.objects[splines_unordered[order_idx]].select_set(True)
4062 bpy.data.objects["SURFSKIO_CRV_ORD"].select_set(True)
4063 bpy.context.view_layer.objects.active = bpy.data.objects["SURFSKIO_CRV_ORD"]
4065 bpy.ops.object.join('INVOKE_REGION_WIN')
4067 # Go back to the original name of the curves object.
4068 bpy.context.object.name = curve_original_name
4070 # Delete all unused objects
4071 bpy.ops.object.delete({"selected_objects": objects_to_delete})
4073 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4074 bpy.data.objects[curve_original_name].select_set(True)
4075 bpy.context.view_layer.objects.active = bpy.data.objects[curve_original_name]
4077 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4078 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4080 try:
4081 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
4082 except:
4083 pass
4086 return {"FINISHED"}
4088 def invoke(self, context, event):
4089 self.main_curve = bpy.context.object
4090 there_are_GP_strokes = False
4092 try:
4093 # Get the active grease pencil layer
4094 strokes_num = len(self.main_curve.grease_pencil.layers.active.active_frame.strokes)
4096 if strokes_num > 0:
4097 there_are_GP_strokes = True
4098 except:
4099 pass
4101 if there_are_GP_strokes:
4102 self.execute(context)
4103 self.report({'INFO'}, "Splines have been reordered")
4104 else:
4105 self.report({'WARNING'}, "Draw grease pencil strokes to connect splines")
4107 return {"FINISHED"}
4109 # ----------------------------
4110 # Set first points operator
4111 class CURVE_OT_SURFSK_first_points(Operator):
4112 bl_idname = "curve.surfsk_first_points"
4113 bl_label = "Bsurfaces set first points"
4114 bl_description = "Set the selected points as the first point of each spline"
4115 bl_options = {'REGISTER', 'UNDO'}
4117 def execute(self, context):
4118 splines_to_invert = []
4120 # Check non-cyclic splines to invert
4121 for i in range(len(self.main_curve.data.splines)):
4122 b_points = self.main_curve.data.splines[i].bezier_points
4124 if i not in self.cyclic_splines: # Only for non-cyclic splines
4125 if b_points[len(b_points) - 1].select_control_point:
4126 splines_to_invert.append(i)
4128 # Reorder points of cyclic splines, and set all handles to "Automatic"
4130 # Check first selected point
4131 cyclic_splines_new_first_pt = {}
4132 for i in self.cyclic_splines:
4133 sp = self.main_curve.data.splines[i]
4135 for t in range(len(sp.bezier_points)):
4136 bp = sp.bezier_points[t]
4137 if bp.select_control_point or bp.select_right_handle or bp.select_left_handle:
4138 cyclic_splines_new_first_pt[i] = t
4139 break # To take only one if there are more
4141 # Reorder
4142 for spline_idx in cyclic_splines_new_first_pt:
4143 sp = self.main_curve.data.splines[spline_idx]
4145 spline_old_coords = []
4146 for bp_old in sp.bezier_points:
4147 coords = (bp_old.co[0], bp_old.co[1], bp_old.co[2])
4149 left_handle_type = str(bp_old.handle_left_type)
4150 left_handle_length = float(bp_old.handle_left.length)
4151 left_handle_xyz = (
4152 float(bp_old.handle_left.x),
4153 float(bp_old.handle_left.y),
4154 float(bp_old.handle_left.z)
4156 right_handle_type = str(bp_old.handle_right_type)
4157 right_handle_length = float(bp_old.handle_right.length)
4158 right_handle_xyz = (
4159 float(bp_old.handle_right.x),
4160 float(bp_old.handle_right.y),
4161 float(bp_old.handle_right.z)
4163 spline_old_coords.append(
4164 [coords, left_handle_type,
4165 right_handle_type, left_handle_length,
4166 right_handle_length, left_handle_xyz,
4167 right_handle_xyz]
4170 for t in range(len(sp.bezier_points)):
4171 bp = sp.bezier_points
4173 if t + cyclic_splines_new_first_pt[spline_idx] + 1 <= len(bp) - 1:
4174 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1
4175 else:
4176 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1 - len(bp)
4178 bp[t].co = Vector(spline_old_coords[new_index][0])
4180 bp[t].handle_left.length = spline_old_coords[new_index][3]
4181 bp[t].handle_right.length = spline_old_coords[new_index][4]
4183 bp[t].handle_left_type = "FREE"
4184 bp[t].handle_right_type = "FREE"
4186 bp[t].handle_left.x = spline_old_coords[new_index][5][0]
4187 bp[t].handle_left.y = spline_old_coords[new_index][5][1]
4188 bp[t].handle_left.z = spline_old_coords[new_index][5][2]
4190 bp[t].handle_right.x = spline_old_coords[new_index][6][0]
4191 bp[t].handle_right.y = spline_old_coords[new_index][6][1]
4192 bp[t].handle_right.z = spline_old_coords[new_index][6][2]
4194 bp[t].handle_left_type = spline_old_coords[new_index][1]
4195 bp[t].handle_right_type = spline_old_coords[new_index][2]
4197 # Invert the non-cyclic splines designated above
4198 for i in range(len(splines_to_invert)):
4199 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4201 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4202 self.main_curve.data.splines[splines_to_invert[i]].bezier_points[0].select_control_point = True
4203 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4205 bpy.ops.curve.switch_direction()
4207 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4209 # Keep selected the first vert of each spline
4210 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4211 for i in range(len(self.main_curve.data.splines)):
4212 if not self.main_curve.data.splines[i].use_cyclic_u:
4213 bp = self.main_curve.data.splines[i].bezier_points[0]
4214 else:
4215 bp = self.main_curve.data.splines[i].bezier_points[
4216 len(self.main_curve.data.splines[i].bezier_points) - 1
4219 bp.select_control_point = True
4220 bp.select_right_handle = True
4221 bp.select_left_handle = True
4223 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4225 return {'FINISHED'}
4227 def invoke(self, context, event):
4228 self.main_curve = bpy.context.object
4230 # Check if all curves are Bezier, and detect which ones are cyclic
4231 self.cyclic_splines = []
4232 for i in range(len(self.main_curve.data.splines)):
4233 if self.main_curve.data.splines[i].type != "BEZIER":
4234 self.report({'WARNING'}, "All splines must be Bezier type")
4236 return {'CANCELLED'}
4237 else:
4238 if self.main_curve.data.splines[i].use_cyclic_u:
4239 self.cyclic_splines.append(i)
4241 self.execute(context)
4242 self.report({'INFO'}, "First points have been set")
4244 return {'FINISHED'}
4247 # Add-ons Preferences Update Panel
4249 # Define Panel classes for updating
4250 panels = (
4251 VIEW3D_PT_tools_SURFSK_mesh,
4252 VIEW3D_PT_tools_SURFSK_curve
4256 def conver_gpencil_to_curve(self, context, pencil, type):
4257 newCurve = bpy.data.curves.new(type + '_curve', type='CURVE')
4258 newCurve.dimensions = '3D'
4259 CurveObject = object_utils.object_data_add(context, newCurve)
4260 error = False
4262 if type == 'GPensil':
4263 try:
4264 strokes = pencil.data.layers.active.active_frame.strokes
4265 except:
4266 error = True
4267 CurveObject.location = pencil.location
4268 CurveObject.rotation_euler = pencil.rotation_euler
4269 CurveObject.scale = pencil.scale
4270 elif type == 'Annotation':
4271 try:
4272 strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
4273 except:
4274 error = True
4275 CurveObject.location = (0.0, 0.0, 0.0)
4276 CurveObject.rotation_euler = (0.0, 0.0, 0.0)
4277 CurveObject.scale = (1.0, 1.0, 1.0)
4279 if not error:
4280 for i, _stroke in enumerate(strokes):
4281 stroke_points = strokes[i].points
4282 data_list = [ (point.co.x, point.co.y, point.co.z)
4283 for point in stroke_points ]
4284 points_to_add = len(data_list)-1
4286 flat_list = []
4287 for point in data_list:
4288 flat_list.extend(point)
4290 spline = newCurve.splines.new(type='BEZIER')
4291 spline.bezier_points.add(points_to_add)
4292 spline.bezier_points.foreach_set("co", flat_list)
4294 for point in spline.bezier_points:
4295 point.handle_left_type="AUTO"
4296 point.handle_right_type="AUTO"
4298 return CurveObject
4299 else:
4300 return None
4303 def update_panel(self, context):
4304 message = "Bsurfaces GPL Edition: Updating Panel locations has failed"
4305 try:
4306 for panel in panels:
4307 if "bl_rna" in panel.__dict__:
4308 bpy.utils.unregister_class(panel)
4310 for panel in panels:
4311 category = context.preferences.addons[__name__].preferences.category
4312 if category != 'Tool':
4313 panel.bl_category = context.preferences.addons[__name__].preferences.category
4314 else:
4315 context.preferences.addons[__name__].preferences.category = 'Edit'
4316 panel.bl_category = 'Edit'
4317 raise ValueError("You can not install add-ons in the Tool panel")
4318 bpy.utils.register_class(panel)
4320 except Exception as e:
4321 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
4322 pass
4324 def makeMaterial(name, diffuse):
4326 if name in bpy.data.materials:
4327 material = bpy.data.materials[name]
4328 material.diffuse_color = diffuse
4329 else:
4330 material = bpy.data.materials.new(name)
4331 material.diffuse_color = diffuse
4333 return material
4335 def update_mesh(self, context):
4336 try:
4337 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4338 bpy.ops.object.select_all(action='DESELECT')
4339 bpy.context.view_layer.update()
4340 global global_mesh_object
4341 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
4342 bpy.data.objects[global_mesh_object].select_set(True)
4343 bpy.context.view_layer.objects.active = bpy.data.objects[global_mesh_object]
4344 except:
4345 print("Select mesh object")
4347 def update_gpencil(self, context):
4348 try:
4349 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4350 bpy.ops.object.select_all(action='DESELECT')
4351 bpy.context.view_layer.update()
4352 global global_gpencil_object
4353 global_gpencil_object = bpy.context.scene.bsurfaces.SURFSK_gpencil.name
4354 bpy.data.objects[global_gpencil_object].select_set(True)
4355 bpy.context.view_layer.objects.active = bpy.data.objects[global_gpencil_object]
4356 except:
4357 print("Select gpencil object")
4359 def update_curve(self, context):
4360 try:
4361 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4362 bpy.ops.object.select_all(action='DESELECT')
4363 bpy.context.view_layer.update()
4364 global global_curve_object
4365 global_curve_object = bpy.context.scene.bsurfaces.SURFSK_curve.name
4366 bpy.data.objects[global_curve_object].select_set(True)
4367 bpy.context.view_layer.objects.active = bpy.data.objects[global_curve_object]
4368 except:
4369 print("Select curve object")
4371 def update_shade_smooth(self, context):
4372 try:
4373 global global_shade_smooth
4374 global_shade_smooth = bpy.context.scene.bsurfaces.SURFSK_shade_smooth
4376 contex_mode = bpy.context.mode
4378 if bpy.ops.object.mode_set.poll():
4379 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4381 bpy.ops.object.select_all(action='DESELECT')
4382 global global_mesh_object
4383 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
4384 bpy.data.objects[global_mesh_object].select_set(True)
4386 if global_shade_smooth:
4387 bpy.ops.object.shade_smooth()
4388 else:
4389 bpy.ops.object.shade_flat()
4391 if contex_mode == "EDIT_MESH":
4392 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4394 except:
4395 print("Select mesh object")
4398 class BsurfPreferences(AddonPreferences):
4399 # this must match the addon name, use '__package__'
4400 # when defining this in a submodule of a python package.
4401 bl_idname = __name__
4403 category: StringProperty(
4404 name="Tab Category",
4405 description="Choose a name for the category of the panel",
4406 default="Edit",
4407 update=update_panel
4410 def draw(self, context):
4411 layout = self.layout
4413 row = layout.row()
4414 col = row.column()
4415 col.label(text="Tab Category:")
4416 col.prop(self, "category", text="")
4418 # Properties
4419 class BsurfacesProps(PropertyGroup):
4420 SURFSK_guide: EnumProperty(
4421 name="Guide:",
4422 items=[
4423 ('Annotation', 'Annotation', 'Annotation'),
4424 ('GPencil', 'GPencil', 'GPencil'),
4425 ('Curve', 'Curve', 'Curve')
4427 default="Annotation"
4429 SURFSK_edges_U: IntProperty(
4430 name="Cross",
4431 description="Number of face-loops crossing the strokes",
4432 default=5,
4433 min=1,
4434 max=200
4436 SURFSK_edges_V: IntProperty(
4437 name="Follow",
4438 description="Number of face-loops following the strokes",
4439 default=1,
4440 min=1,
4441 max=200
4443 SURFSK_cyclic_cross: BoolProperty(
4444 name="Cyclic Cross",
4445 description="Make cyclic the face-loops crossing the strokes",
4446 default=False
4448 SURFSK_cyclic_follow: BoolProperty(
4449 name="Cyclic Follow",
4450 description="Make cyclic the face-loops following the strokes",
4451 default=False
4453 SURFSK_keep_strokes: BoolProperty(
4454 name="Keep strokes",
4455 description="Keeps the sketched strokes or curves after adding the surface",
4456 default=False
4458 SURFSK_automatic_join: BoolProperty(
4459 name="Automatic join",
4460 description="Join automatically vertices of either surfaces "
4461 "generated by crosshatching, or from the borders of closed shapes",
4462 default=True
4464 SURFSK_loops_on_strokes: BoolProperty(
4465 name="Loops on strokes",
4466 description="Make the loops match the paths of the strokes",
4467 default=True
4469 SURFSK_precision: IntProperty(
4470 name="Precision",
4471 description="Precision level of the surface calculation",
4472 default=2,
4473 min=1,
4474 max=100
4476 SURFSK_mesh: PointerProperty(
4477 name="Mesh of BSurface",
4478 type=bpy.types.Object,
4479 description="Mesh of BSurface",
4480 update=update_mesh,
4482 SURFSK_gpencil: PointerProperty(
4483 name="GreasePencil object",
4484 type=bpy.types.Object,
4485 description="GreasePencil object",
4486 update=update_gpencil,
4488 SURFSK_curve: PointerProperty(
4489 name="Curve object",
4490 type=bpy.types.Object,
4491 description="Curve object",
4492 update=update_curve,
4494 SURFSK_shade_smooth: BoolProperty(
4495 name="Shade smooth",
4496 description="Render and display faces smooth, using interpolated Vertex Normals",
4497 default=False,
4498 update=update_shade_smooth,
4501 classes = (
4502 MESH_OT_SURFSK_init,
4503 MESH_OT_SURFSK_add_modifiers,
4504 MESH_OT_SURFSK_add_surface,
4505 MESH_OT_SURFSK_edit_surface,
4506 GPENCIL_OT_SURFSK_add_strokes,
4507 GPENCIL_OT_SURFSK_edit_strokes,
4508 GPENCIL_OT_SURFSK_strokes_to_curves,
4509 GPENCIL_OT_SURFSK_annotation_to_curves,
4510 GPENCIL_OT_SURFSK_add_annotation,
4511 CURVE_OT_SURFSK_edit_curve,
4512 CURVE_OT_SURFSK_reorder_splines,
4513 CURVE_OT_SURFSK_first_points,
4514 BsurfPreferences,
4515 BsurfacesProps
4518 def register():
4519 for cls in classes:
4520 bpy.utils.register_class(cls)
4522 for panel in panels:
4523 bpy.utils.register_class(panel)
4525 bpy.types.Scene.bsurfaces = PointerProperty(type=BsurfacesProps)
4526 update_panel(None, bpy.context)
4528 def unregister():
4529 for panel in panels:
4530 bpy.utils.unregister_class(panel)
4532 for cls in classes:
4533 bpy.utils.unregister_class(cls)
4535 del bpy.types.Scene.bsurfaces
4537 if __name__ == "__main__":
4538 register()