Update scripts to account for removal of the context override to bpy.ops
[blender-addons.git] / mesh_bsurfaces.py
blobaee45a060918579c08b0f1b6d8fe65f16a0b059f
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, 1),
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:
80 shrinkwrap = next(mod for mod in mesh_object.modifiers
81 if mod.type == 'SHRINKWRAP')
82 col.prop(shrinkwrap, "offset")
83 except:
84 pass
85 try: col.prop(mesh_object, "show_in_front")
86 except: pass
87 try: col.prop(bs, "SURFSK_shade_smooth")
88 except: pass
89 try: col.prop(mesh_object, "show_wire")
90 except: pass
92 col.label(text="Guide strokes:")
93 col.row().prop(bs, "SURFSK_guide", expand=True)
94 if bs.SURFSK_guide == 'GPencil':
95 col.prop(bs, "SURFSK_gpencil", text="")
96 col.separator()
97 if bs.SURFSK_guide == 'Curve':
98 col.prop(bs, "SURFSK_curve", text="")
99 col.separator()
101 col.separator()
102 col.operator("mesh.surfsk_add_surface", text="Add Surface")
103 col.operator("mesh.surfsk_edit_surface", text="Edit Surface")
105 col.separator()
106 if bs.SURFSK_guide == 'GPencil':
107 col.operator("gpencil.surfsk_add_strokes", text="Add Strokes")
108 col.operator("gpencil.surfsk_edit_strokes", text="Edit Strokes")
109 col.separator()
110 col.operator("gpencil.surfsk_strokes_to_curves", text="Strokes to curves")
112 if bs.SURFSK_guide == 'Annotation':
113 col.operator("gpencil.surfsk_add_annotation", text="Add Annotation")
114 col.separator()
115 col.operator("gpencil.surfsk_annotations_to_curves", text="Annotation to curves")
117 if bs.SURFSK_guide == 'Curve':
118 col.operator("curve.surfsk_edit_curve", text="Edit curve")
120 col.separator()
121 col.label(text="Initial settings:")
122 col.prop(bs, "SURFSK_edges_U")
123 col.prop(bs, "SURFSK_edges_V")
124 col.prop(bs, "SURFSK_cyclic_cross")
125 col.prop(bs, "SURFSK_cyclic_follow")
126 col.prop(bs, "SURFSK_loops_on_strokes")
127 col.prop(bs, "SURFSK_automatic_join")
128 col.prop(bs, "SURFSK_keep_strokes")
130 class VIEW3D_PT_tools_SURFSK_curve(Panel):
131 bl_space_type = 'VIEW_3D'
132 bl_region_type = 'UI'
133 bl_context = "curve_edit"
134 bl_category = 'Edit'
135 bl_label = "Bsurfaces"
137 @classmethod
138 def poll(cls, context):
139 return context.active_object
141 def draw(self, context):
142 layout = self.layout
144 col = layout.column(align=True)
145 row = layout.row()
146 row.separator()
147 col.operator("curve.surfsk_first_points", text="Set First Points")
148 col.operator("curve.switch_direction", text="Switch Direction")
149 col.operator("curve.surfsk_reorder_splines", text="Reorder Splines")
152 # ----------------------------
153 # Returns the type of strokes used
154 def get_strokes_type(context):
155 strokes_type = "NO_STROKES"
156 strokes_num = 0
158 # Check if they are annotation
159 if context.scene.bsurfaces.SURFSK_guide == 'Annotation':
160 try:
161 strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
163 strokes_num = len(strokes)
165 if strokes_num > 0:
166 strokes_type = "GP_ANNOTATION"
167 except:
168 strokes_type = "NO_STROKES"
170 # Check if they are grease pencil
171 if context.scene.bsurfaces.SURFSK_guide == 'GPencil':
172 try:
173 global global_gpencil_object
174 gpencil = bpy.data.objects[global_gpencil_object]
175 strokes = gpencil.data.layers.active.active_frame.strokes
177 strokes_num = len(strokes)
179 if strokes_num > 0:
180 strokes_type = "GP_STROKES"
181 except:
182 strokes_type = "NO_STROKES"
184 # Check if they are curves, if there aren't grease pencil strokes
185 if context.scene.bsurfaces.SURFSK_guide == 'Curve':
186 try:
187 global global_curve_object
188 ob = bpy.data.objects[global_curve_object]
189 if ob.type == "CURVE":
190 strokes_type = "EXTERNAL_CURVE"
191 strokes_num = len(ob.data.splines)
193 # Check if there is any non-bezier spline
194 for i in range(len(ob.data.splines)):
195 if ob.data.splines[i].type != "BEZIER":
196 strokes_type = "CURVE_WITH_NON_BEZIER_SPLINES"
197 break
199 else:
200 strokes_type = "EXTERNAL_NO_CURVE"
201 except:
202 strokes_type = "NO_STROKES"
204 # Check if they are mesh
205 try:
206 global global_mesh_object
207 self.main_object = bpy.data.objects[global_mesh_object]
208 total_vert_sel = len([v for v in self.main_object.data.vertices if v.select])
210 # Check if there is a single stroke without any selection in the object
211 if strokes_num == 1 and total_vert_sel == 0:
212 if strokes_type == "EXTERNAL_CURVE":
213 strokes_type = "SINGLE_CURVE_STROKE_NO_SELECTION"
214 elif strokes_type == "GP_STROKES":
215 strokes_type = "SINGLE_GP_STROKE_NO_SELECTION"
217 if strokes_num == 0 and total_vert_sel > 0:
218 strokes_type = "SELECTION_ALONE"
219 except:
220 pass
222 return strokes_type
224 # ----------------------------
225 # Surface generator operator
226 class MESH_OT_SURFSK_add_surface(Operator):
227 bl_idname = "mesh.surfsk_add_surface"
228 bl_label = "Bsurfaces add surface"
229 bl_description = "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
230 bl_options = {'REGISTER', 'UNDO'}
232 is_crosshatch: BoolProperty(
233 default=False
235 is_fill_faces: BoolProperty(
236 default=False
238 selection_U_exists: BoolProperty(
239 default=False
241 selection_V_exists: BoolProperty(
242 default=False
244 selection_U2_exists: BoolProperty(
245 default=False
247 selection_V2_exists: BoolProperty(
248 default=False
250 selection_V_is_closed: BoolProperty(
251 default=False
253 selection_U_is_closed: BoolProperty(
254 default=False
256 selection_V2_is_closed: BoolProperty(
257 default=False
259 selection_U2_is_closed: BoolProperty(
260 default=False
263 edges_U: IntProperty(
264 name="Cross",
265 description="Number of face-loops crossing the strokes",
266 default=1,
267 min=1,
268 max=200
270 edges_V: IntProperty(
271 name="Follow",
272 description="Number of face-loops following the strokes",
273 default=1,
274 min=1,
275 max=200
277 cyclic_cross: BoolProperty(
278 name="Cyclic Cross",
279 description="Make cyclic the face-loops crossing the strokes",
280 default=False
282 cyclic_follow: BoolProperty(
283 name="Cyclic Follow",
284 description="Make cyclic the face-loops following the strokes",
285 default=False
287 loops_on_strokes: BoolProperty(
288 name="Loops on strokes",
289 description="Make the loops match the paths of the strokes",
290 default=False
292 automatic_join: BoolProperty(
293 name="Automatic join",
294 description="Join automatically vertices of either surfaces generated "
295 "by crosshatching, or from the borders of closed shapes",
296 default=False
298 join_stretch_factor: FloatProperty(
299 name="Stretch",
300 description="Amount of stretching or shrinking allowed for "
301 "edges when joining vertices automatically",
302 default=1,
303 min=0,
304 max=3,
305 subtype='FACTOR'
307 keep_strokes: BoolProperty(
308 name="Keep strokes",
309 description="Keeps the sketched strokes or curves after adding the surface",
310 default=False
312 strokes_type: StringProperty()
313 initial_global_undo_state: BoolProperty()
316 def draw(self, context):
317 layout = self.layout
318 col = layout.column(align=True)
319 row = layout.row()
321 if not self.is_fill_faces:
322 row.separator()
323 if not self.is_crosshatch:
324 if not self.selection_U_exists:
325 col.prop(self, "edges_U")
326 row.separator()
328 if not self.selection_V_exists:
329 col.prop(self, "edges_V")
330 row.separator()
332 row.separator()
334 if not self.selection_U_exists:
335 if not (
336 (self.selection_V_exists and not self.selection_V_is_closed) or
337 (self.selection_V2_exists and not self.selection_V2_is_closed)
339 col.prop(self, "cyclic_cross")
341 if not self.selection_V_exists:
342 if not (
343 (self.selection_U_exists and not self.selection_U_is_closed) or
344 (self.selection_U2_exists and not self.selection_U2_is_closed)
346 col.prop(self, "cyclic_follow")
348 col.prop(self, "loops_on_strokes")
350 col.prop(self, "automatic_join")
352 if self.automatic_join:
353 row.separator()
354 col.separator()
355 row.separator()
356 col.prop(self, "join_stretch_factor")
358 col.prop(self, "keep_strokes")
360 # Get an ordered list of a chain of vertices
361 def get_ordered_verts(self, ob, all_selected_edges_idx, all_selected_verts_idx,
362 first_vert_idx, middle_vertex_idx, closing_vert_idx):
363 # Order selected vertices.
364 verts_ordered = []
365 if closing_vert_idx is not None:
366 verts_ordered.append(ob.data.vertices[closing_vert_idx])
368 verts_ordered.append(ob.data.vertices[first_vert_idx])
369 prev_v = first_vert_idx
370 prev_ed = None
371 finish_while = False
372 while True:
373 edges_non_matched = 0
374 for i in all_selected_edges_idx:
375 if ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[0] == prev_v and \
376 ob.data.edges[i].vertices[1] in all_selected_verts_idx:
378 verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[1]])
379 prev_v = ob.data.edges[i].vertices[1]
380 prev_ed = ob.data.edges[i]
381 elif ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[1] == prev_v and \
382 ob.data.edges[i].vertices[0] in all_selected_verts_idx:
384 verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[0]])
385 prev_v = ob.data.edges[i].vertices[0]
386 prev_ed = ob.data.edges[i]
387 else:
388 edges_non_matched += 1
390 if edges_non_matched == len(all_selected_edges_idx):
391 finish_while = True
393 if finish_while:
394 break
396 if closing_vert_idx is not None:
397 verts_ordered.append(ob.data.vertices[closing_vert_idx])
399 if middle_vertex_idx is not None:
400 verts_ordered.append(ob.data.vertices[middle_vertex_idx])
401 verts_ordered.reverse()
403 return tuple(verts_ordered)
405 # Calculates length of a chain of points.
406 def get_chain_length(self, object, verts_ordered):
407 matrix = object.matrix_world
409 edges_lengths = []
410 edges_lengths_sum = 0
411 for i in range(0, len(verts_ordered)):
412 if i == 0:
413 prev_v_co = matrix @ verts_ordered[i].co
414 else:
415 v_co = matrix @ verts_ordered[i].co
417 v_difs = [prev_v_co[0] - v_co[0], prev_v_co[1] - v_co[1], prev_v_co[2] - v_co[2]]
418 edge_length = abs(sqrt(v_difs[0] * v_difs[0] + v_difs[1] * v_difs[1] + v_difs[2] * v_difs[2]))
420 edges_lengths.append(edge_length)
421 edges_lengths_sum += edge_length
423 prev_v_co = v_co
425 return edges_lengths, edges_lengths_sum
427 # Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
428 def get_edges_proportions(self, edges_lengths, edges_lengths_sum, use_boundaries, fixed_edges_num):
429 edges_proportions = []
430 if use_boundaries:
431 verts_count = 1
432 for l in edges_lengths:
433 edges_proportions.append(l / edges_lengths_sum)
434 verts_count += 1
435 else:
436 verts_count = 1
437 for _n in range(0, fixed_edges_num):
438 edges_proportions.append(1 / fixed_edges_num)
439 verts_count += 1
441 return edges_proportions
443 # Calculates the angle between two pairs of points in space
444 def orientation_difference(self, points_A_co, points_B_co):
445 # each parameter should be a list with two elements,
446 # and each element should be a x,y,z coordinate
447 vec_A = points_A_co[0] - points_A_co[1]
448 vec_B = points_B_co[0] - points_B_co[1]
450 angle = vec_A.angle(vec_B)
452 if angle > 0.5 * pi:
453 angle = abs(angle - pi)
455 return angle
457 # Calculate the which vert of verts_idx list is the nearest one
458 # to the point_co coordinates, and the distance
459 def shortest_distance(self, object, point_co, verts_idx):
460 matrix = object.matrix_world
462 for i in range(0, len(verts_idx)):
463 dist = (point_co - matrix @ object.data.vertices[verts_idx[i]].co).length
464 if i == 0:
465 prev_dist = dist
466 nearest_vert_idx = verts_idx[i]
467 shortest_dist = dist
469 if dist < prev_dist:
470 prev_dist = dist
471 nearest_vert_idx = verts_idx[i]
472 shortest_dist = dist
474 return nearest_vert_idx, shortest_dist
476 # Returns the index of the opposite vert tip in a chain, given a vert tip index
477 # as parameter, and a multidimentional list with all pairs of tips
478 def opposite_tip(self, vert_tip_idx, all_chains_tips_idx):
479 opposite_vert_tip_idx = None
480 for i in range(0, len(all_chains_tips_idx)):
481 if vert_tip_idx == all_chains_tips_idx[i][0]:
482 opposite_vert_tip_idx = all_chains_tips_idx[i][1]
483 if vert_tip_idx == all_chains_tips_idx[i][1]:
484 opposite_vert_tip_idx = all_chains_tips_idx[i][0]
486 return opposite_vert_tip_idx
488 # Simplifies a spline and returns the new points coordinates
489 def simplify_spline(self, spline_coords, segments_num):
490 simplified_spline = []
491 points_between_segments = round(len(spline_coords) / segments_num)
493 simplified_spline.append(spline_coords[0])
494 for i in range(1, segments_num):
495 simplified_spline.append(spline_coords[i * points_between_segments])
497 simplified_spline.append(spline_coords[len(spline_coords) - 1])
499 return simplified_spline
501 # Returns a list with the coords of the points distributed over the splines
502 # passed to this method according to the proportions parameter
503 def distribute_pts(self, surface_splines, proportions):
505 # Calculate the length of each final surface spline
506 surface_splines_lengths = []
507 surface_splines_parsed = []
509 for sp_idx in range(0, len(surface_splines)):
510 # Calculate spline length
511 surface_splines_lengths.append(0)
513 for i in range(0, len(surface_splines[sp_idx].bezier_points)):
514 if i == 0:
515 prev_p = surface_splines[sp_idx].bezier_points[i]
516 else:
517 p = surface_splines[sp_idx].bezier_points[i]
518 edge_length = (prev_p.co - p.co).length
519 surface_splines_lengths[sp_idx] += edge_length
521 prev_p = p
523 # Calculate vertex positions with appropriate edge proportions, and ordered, for each spline
524 for sp_idx in range(0, len(surface_splines)):
525 surface_splines_parsed.append([])
526 surface_splines_parsed[sp_idx].append(surface_splines[sp_idx].bezier_points[0].co)
528 prev_p_co = surface_splines[sp_idx].bezier_points[0].co
529 p_idx = 0
531 for prop_idx in range(len(proportions) - 1):
532 target_length = surface_splines_lengths[sp_idx] * proportions[prop_idx]
533 partial_segment_length = 0
534 finish_while = False
536 while True:
537 # if not it'll pass the p_idx as an index below and crash
538 if p_idx < len(surface_splines[sp_idx].bezier_points):
539 p_co = surface_splines[sp_idx].bezier_points[p_idx].co
540 new_dist = (prev_p_co - p_co).length
542 # The new distance that could have the partial segment if
543 # it is still shorter than the target length
544 potential_segment_length = partial_segment_length + new_dist
546 # If the potential is still shorter, keep adding
547 if potential_segment_length < target_length:
548 partial_segment_length = potential_segment_length
550 p_idx += 1
551 prev_p_co = p_co
553 # If the potential is longer than the target, calculate the target
554 # (a point between the last two points), and assign
555 elif potential_segment_length > target_length:
556 remaining_dist = target_length - partial_segment_length
557 vec = p_co - prev_p_co
558 vec.normalize()
559 intermediate_co = prev_p_co + (vec * remaining_dist)
561 surface_splines_parsed[sp_idx].append(intermediate_co)
563 partial_segment_length += remaining_dist
564 prev_p_co = intermediate_co
566 finish_while = True
568 # If the potential is equal to the target, assign
569 elif potential_segment_length == target_length:
570 surface_splines_parsed[sp_idx].append(p_co)
571 prev_p_co = p_co
573 finish_while = True
575 if finish_while:
576 break
578 # last point of the spline
579 surface_splines_parsed[sp_idx].append(
580 surface_splines[sp_idx].bezier_points[len(surface_splines[sp_idx].bezier_points) - 1].co
583 return surface_splines_parsed
585 # Counts the number of faces that belong to each edge
586 def edge_face_count(self, ob):
587 ed_keys_count_dict = {}
589 for face in ob.data.polygons:
590 for ed_keys in face.edge_keys:
591 if ed_keys not in ed_keys_count_dict:
592 ed_keys_count_dict[ed_keys] = 1
593 else:
594 ed_keys_count_dict[ed_keys] += 1
596 edge_face_count = []
597 for i in range(len(ob.data.edges)):
598 edge_face_count.append(0)
600 for i in range(len(ob.data.edges)):
601 ed = ob.data.edges[i]
603 v1 = ed.vertices[0]
604 v2 = ed.vertices[1]
606 if (v1, v2) in ed_keys_count_dict:
607 edge_face_count[i] = ed_keys_count_dict[(v1, v2)]
608 elif (v2, v1) in ed_keys_count_dict:
609 edge_face_count[i] = ed_keys_count_dict[(v2, v1)]
611 return edge_face_count
613 # Fills with faces all the selected vertices which form empty triangles or quads
614 def fill_with_faces(self, object):
615 all_selected_verts_count = self.main_object_selected_verts_count
617 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
619 # Calculate average length of selected edges
620 all_selected_verts = []
621 original_sel_edges_count = 0
622 for ed in object.data.edges:
623 if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
624 coords = []
625 coords.append(object.data.vertices[ed.vertices[0]].co)
626 coords.append(object.data.vertices[ed.vertices[1]].co)
628 original_sel_edges_count += 1
630 if not ed.vertices[0] in all_selected_verts:
631 all_selected_verts.append(ed.vertices[0])
633 if not ed.vertices[1] in all_selected_verts:
634 all_selected_verts.append(ed.vertices[1])
636 tuple(all_selected_verts)
638 # Check if there is any edge selected. If not, interrupt the script
639 if original_sel_edges_count == 0 and all_selected_verts_count > 0:
640 return 0
642 # Get all edges connected to selected verts
643 all_edges_around_sel_verts = []
644 edges_connected_to_sel_verts = {}
645 verts_connected_to_every_vert = {}
646 for ed_idx in range(len(object.data.edges)):
647 ed = object.data.edges[ed_idx]
648 include_edge = False
650 if ed.vertices[0] in all_selected_verts:
651 if not ed.vertices[0] in edges_connected_to_sel_verts:
652 edges_connected_to_sel_verts[ed.vertices[0]] = []
654 edges_connected_to_sel_verts[ed.vertices[0]].append(ed_idx)
655 include_edge = True
657 if ed.vertices[1] in all_selected_verts:
658 if not ed.vertices[1] in edges_connected_to_sel_verts:
659 edges_connected_to_sel_verts[ed.vertices[1]] = []
661 edges_connected_to_sel_verts[ed.vertices[1]].append(ed_idx)
662 include_edge = True
664 if include_edge is True:
665 all_edges_around_sel_verts.append(ed_idx)
667 # Get all connected verts to each vert
668 if not ed.vertices[0] in verts_connected_to_every_vert:
669 verts_connected_to_every_vert[ed.vertices[0]] = []
671 if not ed.vertices[1] in verts_connected_to_every_vert:
672 verts_connected_to_every_vert[ed.vertices[1]] = []
674 verts_connected_to_every_vert[ed.vertices[0]].append(ed.vertices[1])
675 verts_connected_to_every_vert[ed.vertices[1]].append(ed.vertices[0])
677 # Get all verts connected to faces
678 all_verts_part_of_faces = []
679 all_edges_faces_count = []
680 all_edges_faces_count += self.edge_face_count(object)
682 # Get only the selected edges that have faces attached.
683 count_faces_of_edges_around_sel_verts = {}
684 selected_verts_with_faces = []
685 for ed_idx in all_edges_around_sel_verts:
686 count_faces_of_edges_around_sel_verts[ed_idx] = all_edges_faces_count[ed_idx]
688 if all_edges_faces_count[ed_idx] > 0:
689 ed = object.data.edges[ed_idx]
691 if not ed.vertices[0] in selected_verts_with_faces:
692 selected_verts_with_faces.append(ed.vertices[0])
694 if not ed.vertices[1] in selected_verts_with_faces:
695 selected_verts_with_faces.append(ed.vertices[1])
697 all_verts_part_of_faces.append(ed.vertices[0])
698 all_verts_part_of_faces.append(ed.vertices[1])
700 tuple(selected_verts_with_faces)
702 # Discard unneeded verts from calculations
703 participating_verts = []
704 movable_verts = []
705 for v_idx in all_selected_verts:
706 vert_has_edges_with_one_face = False
708 # Check if the actual vert has at least one edge connected to only one face
709 for ed_idx in edges_connected_to_sel_verts[v_idx]:
710 if count_faces_of_edges_around_sel_verts[ed_idx] == 1:
711 vert_has_edges_with_one_face = True
713 # If the vert has two or less edges connected and the vert is not part of any face.
714 # Or the vert is part of any face and at least one of
715 # the connected edges has only one face attached to it.
716 if (len(edges_connected_to_sel_verts[v_idx]) == 2 and
717 v_idx not in all_verts_part_of_faces) or \
718 len(edges_connected_to_sel_verts[v_idx]) == 1 or \
719 (v_idx in all_verts_part_of_faces and
720 vert_has_edges_with_one_face):
722 participating_verts.append(v_idx)
724 if v_idx not in all_verts_part_of_faces:
725 movable_verts.append(v_idx)
727 # Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
728 for mv_idx in movable_verts:
729 freeze_vert = False
730 mv_connected_verts = verts_connected_to_every_vert[mv_idx]
732 for actual_v_idx in all_selected_verts:
733 count_shared_neighbors = 0
734 checked_verts = []
736 for mv_conn_v_idx in mv_connected_verts:
737 if mv_idx != actual_v_idx:
738 if mv_conn_v_idx in verts_connected_to_every_vert[actual_v_idx] and \
739 mv_conn_v_idx not in checked_verts:
740 count_shared_neighbors += 1
741 checked_verts.append(mv_conn_v_idx)
743 if actual_v_idx in mv_connected_verts:
744 freeze_vert = True
745 break
747 if count_shared_neighbors == 2:
748 freeze_vert = True
749 break
751 if freeze_vert:
752 break
754 if freeze_vert:
755 movable_verts.remove(mv_idx)
757 # Calculate merge distance for participating verts
758 shortest_edge_length = None
759 for ed in object.data.edges:
760 if ed.vertices[0] in movable_verts and ed.vertices[1] in movable_verts:
761 v1 = object.data.vertices[ed.vertices[0]]
762 v2 = object.data.vertices[ed.vertices[1]]
764 length = (v1.co - v2.co).length
766 if shortest_edge_length is None:
767 shortest_edge_length = length
768 else:
769 if length < shortest_edge_length:
770 shortest_edge_length = length
772 if shortest_edge_length is not None:
773 edges_merge_distance = shortest_edge_length * 0.5
774 else:
775 edges_merge_distance = 0
777 # Get together the verts near enough. They will be merged later
778 remaining_verts = []
779 remaining_verts += participating_verts
780 for v1_idx in participating_verts:
781 if v1_idx in remaining_verts and v1_idx in movable_verts:
782 verts_to_merge = []
783 coords_verts_to_merge = {}
785 verts_to_merge.append(v1_idx)
787 v1_co = object.data.vertices[v1_idx].co
788 coords_verts_to_merge[v1_idx] = (v1_co[0], v1_co[1], v1_co[2])
790 for v2_idx in remaining_verts:
791 if v1_idx != v2_idx:
792 v2_co = object.data.vertices[v2_idx].co
794 dist = (v1_co - v2_co).length
796 if dist <= edges_merge_distance: # Add the verts which are near enough
797 verts_to_merge.append(v2_idx)
799 coords_verts_to_merge[v2_idx] = (v2_co[0], v2_co[1], v2_co[2])
801 for vm_idx in verts_to_merge:
802 remaining_verts.remove(vm_idx)
804 if len(verts_to_merge) > 1:
805 # Calculate middle point of the verts to merge.
806 sum_x_co = 0
807 sum_y_co = 0
808 sum_z_co = 0
809 movable_verts_to_merge_count = 0
810 for i in range(len(verts_to_merge)):
811 if verts_to_merge[i] in movable_verts:
812 v_co = object.data.vertices[verts_to_merge[i]].co
814 sum_x_co += v_co[0]
815 sum_y_co += v_co[1]
816 sum_z_co += v_co[2]
818 movable_verts_to_merge_count += 1
820 middle_point_co = [
821 sum_x_co / movable_verts_to_merge_count,
822 sum_y_co / movable_verts_to_merge_count,
823 sum_z_co / movable_verts_to_merge_count
826 # Check if any vert to be merged is not movable
827 shortest_dist = None
828 are_verts_not_movable = False
829 verts_not_movable = []
830 for v_merge_idx in verts_to_merge:
831 if v_merge_idx in participating_verts and v_merge_idx not in movable_verts:
832 are_verts_not_movable = True
833 verts_not_movable.append(v_merge_idx)
835 if are_verts_not_movable:
836 # Get the vert connected to faces, that is nearest to
837 # the middle point of the movable verts
838 shortest_dist = None
839 for vcf_idx in verts_not_movable:
840 dist = abs((object.data.vertices[vcf_idx].co -
841 Vector(middle_point_co)).length)
843 if shortest_dist is None:
844 shortest_dist = dist
845 nearest_vert_idx = vcf_idx
846 else:
847 if dist < shortest_dist:
848 shortest_dist = dist
849 nearest_vert_idx = vcf_idx
851 coords = object.data.vertices[nearest_vert_idx].co
852 target_point_co = [coords[0], coords[1], coords[2]]
853 else:
854 target_point_co = middle_point_co
856 # Move verts to merge to the middle position
857 for v_merge_idx in verts_to_merge:
858 if v_merge_idx in movable_verts: # Only move the verts that are not part of faces
859 object.data.vertices[v_merge_idx].co[0] = target_point_co[0]
860 object.data.vertices[v_merge_idx].co[1] = target_point_co[1]
861 object.data.vertices[v_merge_idx].co[2] = target_point_co[2]
863 # Perform "Remove Doubles" to weld all the disconnected verts
864 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
865 bpy.ops.mesh.remove_doubles(threshold=0.0001)
867 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
869 # Get all the definitive selected edges, after weldding
870 selected_edges = []
871 edges_per_vert = {} # Number of faces of each selected edge
872 for ed in object.data.edges:
873 if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
874 selected_edges.append(ed.index)
876 # Save all the edges that belong to each vertex.
877 if not ed.vertices[0] in edges_per_vert:
878 edges_per_vert[ed.vertices[0]] = []
880 if not ed.vertices[1] in edges_per_vert:
881 edges_per_vert[ed.vertices[1]] = []
883 edges_per_vert[ed.vertices[0]].append(ed.index)
884 edges_per_vert[ed.vertices[1]].append(ed.index)
886 # Check if all the edges connected to each vert have two faces attached to them.
887 # To discard them later and make calculations faster
888 a = []
889 a += self.edge_face_count(object)
890 tuple(a)
891 verts_surrounded_by_faces = {}
892 for v_idx in edges_per_vert:
893 edges_with_two_faces_count = 0
895 for ed_idx in edges_per_vert[v_idx]:
896 if a[ed_idx] == 2:
897 edges_with_two_faces_count += 1
899 if edges_with_two_faces_count == len(edges_per_vert[v_idx]):
900 verts_surrounded_by_faces[v_idx] = True
901 else:
902 verts_surrounded_by_faces[v_idx] = False
904 # Get all the selected vertices
905 selected_verts_idx = []
906 for v in object.data.vertices:
907 if v.select:
908 selected_verts_idx.append(v.index)
910 # Get all the faces of the object
911 all_object_faces_verts_idx = []
912 for face in object.data.polygons:
913 face_verts = []
914 face_verts.append(face.vertices[0])
915 face_verts.append(face.vertices[1])
916 face_verts.append(face.vertices[2])
918 if len(face.vertices) == 4:
919 face_verts.append(face.vertices[3])
921 all_object_faces_verts_idx.append(face_verts)
923 # Deselect all vertices
924 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
925 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
926 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
928 # Make a dictionary with the verts related to each vert
929 related_key_verts = {}
930 for ed_idx in selected_edges:
931 ed = object.data.edges[ed_idx]
933 if not verts_surrounded_by_faces[ed.vertices[0]]:
934 if not ed.vertices[0] in related_key_verts:
935 related_key_verts[ed.vertices[0]] = []
937 if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
938 related_key_verts[ed.vertices[0]].append(ed.vertices[1])
940 if not verts_surrounded_by_faces[ed.vertices[1]]:
941 if not ed.vertices[1] in related_key_verts:
942 related_key_verts[ed.vertices[1]] = []
944 if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
945 related_key_verts[ed.vertices[1]].append(ed.vertices[0])
947 # Get groups of verts forming each face
948 faces_verts_idx = []
949 for v1 in related_key_verts: # verts-1 ....
950 for v2 in related_key_verts: # verts-2
951 if v1 != v2:
952 related_verts_in_common = []
953 v2_in_rel_v1 = False
954 v1_in_rel_v2 = False
955 for rel_v1 in related_key_verts[v1]:
956 # Check if related verts of verts-1 are related verts of verts-2
957 if rel_v1 in related_key_verts[v2]:
958 related_verts_in_common.append(rel_v1)
960 if v2 in related_key_verts[v1]:
961 v2_in_rel_v1 = True
963 if v1 in related_key_verts[v2]:
964 v1_in_rel_v2 = True
966 repeated_face = False
967 # If two verts have two related verts in common, they form a quad
968 if len(related_verts_in_common) == 2:
969 # Check if the face is already saved
970 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
972 for f_verts in all_faces_to_check_idx:
973 repeated_verts = 0
975 if len(f_verts) == 4:
976 if v1 in f_verts:
977 repeated_verts += 1
978 if v2 in f_verts:
979 repeated_verts += 1
980 if related_verts_in_common[0] in f_verts:
981 repeated_verts += 1
982 if related_verts_in_common[1] in f_verts:
983 repeated_verts += 1
985 if repeated_verts == len(f_verts):
986 repeated_face = True
987 break
989 if not repeated_face:
990 faces_verts_idx.append(
991 [v1, related_verts_in_common[0], v2, related_verts_in_common[1]]
994 # If Two verts have one related vert in common and
995 # they are related to each other, they form a triangle
996 elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
997 # Check if the face is already saved.
998 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
1000 for f_verts in all_faces_to_check_idx:
1001 repeated_verts = 0
1003 if len(f_verts) == 3:
1004 if v1 in f_verts:
1005 repeated_verts += 1
1006 if v2 in f_verts:
1007 repeated_verts += 1
1008 if related_verts_in_common[0] in f_verts:
1009 repeated_verts += 1
1011 if repeated_verts == len(f_verts):
1012 repeated_face = True
1013 break
1015 if not repeated_face:
1016 faces_verts_idx.append([v1, related_verts_in_common[0], v2])
1018 # Keep only the faces that don't overlap by ignoring quads
1019 # that overlap with two adjacent triangles
1020 faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate
1021 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
1022 for i in range(len(faces_verts_idx)):
1023 for t in range(len(all_faces_to_check_idx)):
1024 if i != t:
1025 verts_in_common = 0
1027 if len(faces_verts_idx[i]) == 4 and len(all_faces_to_check_idx[t]) == 3:
1028 for v_idx in all_faces_to_check_idx[t]:
1029 if v_idx in faces_verts_idx[i]:
1030 verts_in_common += 1
1031 # If it doesn't have all it's vertices repeated in the other face
1032 if verts_in_common == 3:
1033 if i not in faces_to_not_include_idx:
1034 faces_to_not_include_idx.append(i)
1036 # Build faces discarding the ones in faces_to_not_include
1037 me = object.data
1038 bm = bmesh.new()
1039 bm.from_mesh(me)
1041 num_faces_created = 0
1042 for i in range(len(faces_verts_idx)):
1043 if i not in faces_to_not_include_idx:
1044 bm.faces.new([bm.verts[v] for v in faces_verts_idx[i]])
1046 num_faces_created += 1
1048 bm.to_mesh(me)
1049 bm.free()
1051 for v_idx in selected_verts_idx:
1052 self.main_object.data.vertices[v_idx].select = True
1054 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
1055 bpy.ops.mesh.normals_make_consistent(inside=False)
1056 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
1058 self.update()
1060 return num_faces_created
1062 # Crosshatch skinning
1063 def crosshatch_surface_invoke(self, ob_original_splines):
1064 self.is_crosshatch = False
1065 self.crosshatch_merge_distance = 0
1067 objects_to_delete = [] # duplicated strokes to be deleted.
1069 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1070 # (without this the surface verts merging with the main object doesn't work well)
1071 self.modifiers_prev_viewport_state = []
1072 if len(self.main_object.modifiers) > 0:
1073 for m_idx in range(len(self.main_object.modifiers)):
1074 self.modifiers_prev_viewport_state.append(
1075 self.main_object.modifiers[m_idx].show_viewport
1077 self.main_object.modifiers[m_idx].show_viewport = False
1079 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1080 ob_original_splines.select_set(True)
1081 bpy.context.view_layer.objects.active = ob_original_splines
1083 if len(ob_original_splines.data.splines) >= 2:
1084 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1085 ob_splines = bpy.context.object
1086 ob_splines.name = "SURFSKIO_NE_STR"
1088 # Get estimative merge distance (sum up the distances from the first point to
1089 # all other points, then average them and then divide them)
1090 first_point_dist_sum = 0
1091 first_dist = 0
1092 second_dist = 0
1093 coords_first_pt = ob_splines.data.splines[0].bezier_points[0].co
1094 for i in range(len(ob_splines.data.splines)):
1095 sp = ob_splines.data.splines[i]
1097 if coords_first_pt != sp.bezier_points[0].co:
1098 first_dist = (coords_first_pt - sp.bezier_points[0].co).length
1100 if coords_first_pt != sp.bezier_points[len(sp.bezier_points) - 1].co:
1101 second_dist = (coords_first_pt - sp.bezier_points[len(sp.bezier_points) - 1].co).length
1103 first_point_dist_sum += first_dist + second_dist
1105 if i == 0:
1106 if first_dist != 0:
1107 shortest_dist = first_dist
1108 elif second_dist != 0:
1109 shortest_dist = second_dist
1111 if shortest_dist > first_dist and first_dist != 0:
1112 shortest_dist = first_dist
1114 if shortest_dist > second_dist and second_dist != 0:
1115 shortest_dist = second_dist
1117 self.crosshatch_merge_distance = shortest_dist / 20
1119 # Recalculation of merge distance
1121 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1123 ob_calc_merge_dist = bpy.context.object
1124 ob_calc_merge_dist.name = "SURFSKIO_CALC_TMP"
1126 objects_to_delete.append(ob_calc_merge_dist)
1128 # Smooth out strokes a little to improve crosshatch detection
1129 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1130 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
1132 for i in range(4):
1133 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1135 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1136 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1138 # Convert curves into mesh
1139 ob_calc_merge_dist.data.resolution_u = 12
1140 bpy.ops.object.convert(target='MESH', keep_original=False)
1142 # Find "intersection-nodes"
1143 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1144 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1145 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
1146 threshold=self.crosshatch_merge_distance)
1147 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
1148 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1150 # Remove verts with less than three edges
1151 verts_edges_count = {}
1152 for ed in ob_calc_merge_dist.data.edges:
1153 v = ed.vertices
1155 if v[0] not in verts_edges_count:
1156 verts_edges_count[v[0]] = 0
1158 if v[1] not in verts_edges_count:
1159 verts_edges_count[v[1]] = 0
1161 verts_edges_count[v[0]] += 1
1162 verts_edges_count[v[1]] += 1
1164 nodes_verts_coords = []
1165 for v_idx in verts_edges_count:
1166 v = ob_calc_merge_dist.data.vertices[v_idx]
1168 if verts_edges_count[v_idx] < 3:
1169 v.select = True
1171 # Remove them
1172 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1173 bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
1174 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1176 # Remove doubles to discard very near verts from calculations of distance
1177 bpy.ops.mesh.remove_doubles(
1178 'INVOKE_REGION_WIN',
1179 threshold=self.crosshatch_merge_distance * 4.0
1181 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1183 # Get all coords of the resulting nodes
1184 nodes_verts_coords = [(v.co[0], v.co[1], v.co[2]) for
1185 v in ob_calc_merge_dist.data.vertices]
1187 # Check if the strokes are a crosshatch
1188 if len(nodes_verts_coords) >= 3:
1189 self.is_crosshatch = True
1191 shortest_dist = None
1192 for co_1 in nodes_verts_coords:
1193 for co_2 in nodes_verts_coords:
1194 if co_1 != co_2:
1195 dist = (Vector(co_1) - Vector(co_2)).length
1197 if shortest_dist is not None:
1198 if dist < shortest_dist:
1199 shortest_dist = dist
1200 else:
1201 shortest_dist = dist
1203 self.crosshatch_merge_distance = shortest_dist / 3
1205 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1206 ob_splines.select_set(True)
1207 bpy.context.view_layer.objects.active = ob_splines
1209 # Deselect all points
1210 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1211 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1212 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1214 # Smooth splines in a localized way, to eliminate "saw-teeth"
1215 # like shapes when there are many points
1216 for sp in ob_splines.data.splines:
1217 angle_sum = 0
1219 angle_limit = 2 # Degrees
1220 for t in range(len(sp.bezier_points)):
1221 # Because on each iteration it checks the "next two points"
1222 # of the actual. This way it doesn't go out of range
1223 if t <= len(sp.bezier_points) - 3:
1224 p1 = sp.bezier_points[t]
1225 p2 = sp.bezier_points[t + 1]
1226 p3 = sp.bezier_points[t + 2]
1228 vec_1 = p1.co - p2.co
1229 vec_2 = p2.co - p3.co
1231 if p2.co != p1.co and p2.co != p3.co:
1232 angle = vec_1.angle(vec_2)
1233 angle_sum += degrees(angle)
1235 if angle_sum >= angle_limit: # If sum of angles is grater than the limit
1236 if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
1237 p1.select_control_point = True
1238 p1.select_left_handle = True
1239 p1.select_right_handle = True
1241 p2.select_control_point = True
1242 p2.select_left_handle = True
1243 p2.select_right_handle = True
1245 if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
1246 p3.select_control_point = True
1247 p3.select_left_handle = True
1248 p3.select_right_handle = True
1250 angle_sum = 0
1252 sp.bezier_points[0].select_control_point = False
1253 sp.bezier_points[0].select_left_handle = False
1254 sp.bezier_points[0].select_right_handle = False
1256 sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = False
1257 sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = False
1258 sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = False
1260 # Smooth out strokes a little to improve crosshatch detection
1261 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1263 for i in range(15):
1264 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1266 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1267 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1269 # Simplify the splines
1270 for sp in ob_splines.data.splines:
1271 angle_sum = 0
1273 sp.bezier_points[0].select_control_point = True
1274 sp.bezier_points[0].select_left_handle = True
1275 sp.bezier_points[0].select_right_handle = True
1277 sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = True
1278 sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = True
1279 sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = True
1281 angle_limit = 15 # Degrees
1282 for t in range(len(sp.bezier_points)):
1283 # Because on each iteration it checks the "next two points"
1284 # of the actual. This way it doesn't go out of range
1285 if t <= len(sp.bezier_points) - 3:
1286 p1 = sp.bezier_points[t]
1287 p2 = sp.bezier_points[t + 1]
1288 p3 = sp.bezier_points[t + 2]
1290 vec_1 = p1.co - p2.co
1291 vec_2 = p2.co - p3.co
1293 if p2.co != p1.co and p2.co != p3.co:
1294 angle = vec_1.angle(vec_2)
1295 angle_sum += degrees(angle)
1296 # If sum of angles is grater than the limit
1297 if angle_sum >= angle_limit:
1298 p1.select_control_point = True
1299 p1.select_left_handle = True
1300 p1.select_right_handle = True
1302 p2.select_control_point = True
1303 p2.select_left_handle = True
1304 p2.select_right_handle = True
1306 p3.select_control_point = True
1307 p3.select_left_handle = True
1308 p3.select_right_handle = True
1310 angle_sum = 0
1312 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1313 bpy.ops.curve.select_all(action='INVERT')
1315 bpy.ops.curve.delete(type='VERT')
1316 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1318 objects_to_delete.append(ob_splines)
1320 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1321 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1322 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1324 # Check if the strokes are a crosshatch
1325 if self.is_crosshatch:
1326 all_points_coords = []
1327 for i in range(len(ob_splines.data.splines)):
1328 all_points_coords.append([])
1330 all_points_coords[i] = [Vector((x, y, z)) for
1331 x, y, z in [bp.co for
1332 bp in ob_splines.data.splines[i].bezier_points]]
1334 all_intersections = []
1335 checked_splines = []
1336 for i in range(len(all_points_coords)):
1338 for t in range(len(all_points_coords[i]) - 1):
1339 bp1_co = all_points_coords[i][t]
1340 bp2_co = all_points_coords[i][t + 1]
1342 for i2 in range(len(all_points_coords)):
1343 if i != i2 and i2 not in checked_splines:
1344 for t2 in range(len(all_points_coords[i2]) - 1):
1345 bp3_co = all_points_coords[i2][t2]
1346 bp4_co = all_points_coords[i2][t2 + 1]
1348 intersec_coords = intersect_line_line(
1349 bp1_co, bp2_co, bp3_co, bp4_co
1351 if intersec_coords is not None:
1352 dist = (intersec_coords[0] - intersec_coords[1]).length
1354 if dist <= self.crosshatch_merge_distance * 1.5:
1355 _temp_co, percent1 = intersect_point_line(
1356 intersec_coords[0], bp1_co, bp2_co
1358 if (percent1 >= -0.02 and percent1 <= 1.02):
1359 _temp_co, percent2 = intersect_point_line(
1360 intersec_coords[1], bp3_co, bp4_co
1362 if (percent2 >= -0.02 and percent2 <= 1.02):
1363 # Format: spline index, first point index from
1364 # corresponding segment, percentage from first point of
1365 # actual segment, coords of intersection point
1366 all_intersections.append(
1367 (i, t, percent1,
1368 ob_splines.matrix_world @ intersec_coords[0])
1370 all_intersections.append(
1371 (i2, t2, percent2,
1372 ob_splines.matrix_world @ intersec_coords[1])
1375 checked_splines.append(i)
1376 # Sort list by spline, then by corresponding first point index of segment,
1377 # and then by percentage from first point of segment: elements 0 and 1 respectively
1378 all_intersections.sort(key=operator.itemgetter(0, 1, 2))
1380 self.crosshatch_strokes_coords = {}
1381 for i in range(len(all_intersections)):
1382 if not all_intersections[i][0] in self.crosshatch_strokes_coords:
1383 self.crosshatch_strokes_coords[all_intersections[i][0]] = []
1385 self.crosshatch_strokes_coords[all_intersections[i][0]].append(
1386 all_intersections[i][3]
1387 ) # Save intersection coords
1388 else:
1389 self.is_crosshatch = False
1391 # Delete all duplicates
1392 with bpy.context.temp_override(selected_objects=objects_to_delete):
1393 bpy.ops.object.delete()
1395 # If the main object has modifiers, turn their "viewport view status" to
1396 # what it was before the forced deactivation above
1397 if len(self.main_object.modifiers) > 0:
1398 for m_idx in range(len(self.main_object.modifiers)):
1399 self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
1401 self.update()
1403 return
1405 # Part of the Crosshatch process that is repeated when the operator is tweaked
1406 def crosshatch_surface_execute(self, context):
1407 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1408 # (without this the surface verts merging with the main object doesn't work well)
1409 self.modifiers_prev_viewport_state = []
1410 if len(self.main_object.modifiers) > 0:
1411 for m_idx in range(len(self.main_object.modifiers)):
1412 self.modifiers_prev_viewport_state.append(self.main_object.modifiers[m_idx].show_viewport)
1414 self.main_object.modifiers[m_idx].show_viewport = False
1416 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1418 me_name = "SURFSKIO_STK_TMP"
1419 me = bpy.data.meshes.new(me_name)
1421 all_verts_coords = []
1422 all_edges = []
1423 for st_idx in self.crosshatch_strokes_coords:
1424 for co_idx in range(len(self.crosshatch_strokes_coords[st_idx])):
1425 coords = self.crosshatch_strokes_coords[st_idx][co_idx]
1427 all_verts_coords.append(coords)
1429 if co_idx > 0:
1430 all_edges.append((len(all_verts_coords) - 2, len(all_verts_coords) - 1))
1432 me.from_pydata(all_verts_coords, all_edges, [])
1433 ob = object_utils.object_data_add(context, me)
1434 ob.location = (0.0, 0.0, 0.0)
1435 ob.rotation_euler = (0.0, 0.0, 0.0)
1436 ob.scale = (1.0, 1.0, 1.0)
1438 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1439 ob.select_set(True)
1440 bpy.context.view_layer.objects.active = ob
1442 # Get together each vert and its nearest, to the middle position
1443 verts = ob.data.vertices
1444 checked_verts = []
1445 for i in range(len(verts)):
1446 shortest_dist = None
1448 if i not in checked_verts:
1449 for t in range(len(verts)):
1450 if i != t and t not in checked_verts:
1451 dist = (verts[i].co - verts[t].co).length
1453 if shortest_dist is not None:
1454 if dist < shortest_dist:
1455 shortest_dist = dist
1456 nearest_vert = t
1457 else:
1458 shortest_dist = dist
1459 nearest_vert = t
1461 middle_location = (verts[i].co + verts[nearest_vert].co) / 2
1463 verts[i].co = middle_location
1464 verts[nearest_vert].co = middle_location
1466 checked_verts.append(i)
1467 checked_verts.append(nearest_vert)
1469 # Calculate average length between all the generated edges
1470 ob = bpy.context.object
1471 lengths_sum = 0
1472 for ed in ob.data.edges:
1473 v1 = ob.data.vertices[ed.vertices[0]]
1474 v2 = ob.data.vertices[ed.vertices[1]]
1476 lengths_sum += (v1.co - v2.co).length
1478 edges_count = len(ob.data.edges)
1479 # possible division by zero here
1480 average_edge_length = lengths_sum / edges_count if edges_count != 0 else 0.0001
1482 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1483 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1484 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
1485 threshold=average_edge_length / 15.0)
1486 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1488 final_points_ob = bpy.context.view_layer.objects.active
1490 # Make a dictionary with the verts related to each vert
1491 related_key_verts = {}
1492 for ed in final_points_ob.data.edges:
1493 if not ed.vertices[0] in related_key_verts:
1494 related_key_verts[ed.vertices[0]] = []
1496 if not ed.vertices[1] in related_key_verts:
1497 related_key_verts[ed.vertices[1]] = []
1499 if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
1500 related_key_verts[ed.vertices[0]].append(ed.vertices[1])
1502 if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
1503 related_key_verts[ed.vertices[1]].append(ed.vertices[0])
1505 # Get groups of verts forming each face
1506 faces_verts_idx = []
1507 for v1 in related_key_verts: # verts-1 ....
1508 for v2 in related_key_verts: # verts-2
1509 if v1 != v2:
1510 related_verts_in_common = []
1511 v2_in_rel_v1 = False
1512 v1_in_rel_v2 = False
1513 for rel_v1 in related_key_verts[v1]:
1514 # Check if related verts of verts-1 are related verts of verts-2
1515 if rel_v1 in related_key_verts[v2]:
1516 related_verts_in_common.append(rel_v1)
1518 if v2 in related_key_verts[v1]:
1519 v2_in_rel_v1 = True
1521 if v1 in related_key_verts[v2]:
1522 v1_in_rel_v2 = True
1524 repeated_face = False
1525 # If two verts have two related verts in common, they form a quad
1526 if len(related_verts_in_common) == 2:
1527 # Check if the face is already saved
1528 for f_verts in faces_verts_idx:
1529 repeated_verts = 0
1531 if len(f_verts) == 4:
1532 if v1 in f_verts:
1533 repeated_verts += 1
1534 if v2 in f_verts:
1535 repeated_verts += 1
1536 if related_verts_in_common[0] in f_verts:
1537 repeated_verts += 1
1538 if related_verts_in_common[1] in f_verts:
1539 repeated_verts += 1
1541 if repeated_verts == len(f_verts):
1542 repeated_face = True
1543 break
1545 if not repeated_face:
1546 faces_verts_idx.append([v1, related_verts_in_common[0],
1547 v2, related_verts_in_common[1]])
1549 # If Two verts have one related vert in common and they are
1550 # related to each other, they form a triangle
1551 elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
1552 # Check if the face is already saved.
1553 for f_verts in faces_verts_idx:
1554 repeated_verts = 0
1556 if len(f_verts) == 3:
1557 if v1 in f_verts:
1558 repeated_verts += 1
1559 if v2 in f_verts:
1560 repeated_verts += 1
1561 if related_verts_in_common[0] in f_verts:
1562 repeated_verts += 1
1564 if repeated_verts == len(f_verts):
1565 repeated_face = True
1566 break
1568 if not repeated_face:
1569 faces_verts_idx.append([v1, related_verts_in_common[0], v2])
1571 # Keep only the faces that don't overlap by ignoring
1572 # quads that overlap with two adjacent triangles
1573 faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate
1574 for i in range(len(faces_verts_idx)):
1575 for t in range(len(faces_verts_idx)):
1576 if i != t:
1577 verts_in_common = 0
1579 if len(faces_verts_idx[i]) == 4 and len(faces_verts_idx[t]) == 3:
1580 for v_idx in faces_verts_idx[t]:
1581 if v_idx in faces_verts_idx[i]:
1582 verts_in_common += 1
1583 # If it doesn't have all it's vertices repeated in the other face
1584 if verts_in_common == 3:
1585 if i not in faces_to_not_include_idx:
1586 faces_to_not_include_idx.append(i)
1588 # Build surface
1589 all_surface_verts_co = []
1590 for i in range(len(final_points_ob.data.vertices)):
1591 coords = final_points_ob.data.vertices[i].co
1592 all_surface_verts_co.append([coords[0], coords[1], coords[2]])
1594 # Verts of each face.
1595 all_surface_faces = []
1596 for i in range(len(faces_verts_idx)):
1597 if i not in faces_to_not_include_idx:
1598 face = []
1599 for v_idx in faces_verts_idx[i]:
1600 face.append(v_idx)
1602 all_surface_faces.append(face)
1604 # Build the mesh
1605 surf_me_name = "SURFSKIO_surface"
1606 me_surf = bpy.data.meshes.new(surf_me_name)
1607 me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
1608 ob_surface = object_utils.object_data_add(context, me_surf)
1609 ob_surface.location = (0.0, 0.0, 0.0)
1610 ob_surface.rotation_euler = (0.0, 0.0, 0.0)
1611 ob_surface.scale = (1.0, 1.0, 1.0)
1613 # Delete final points temporal object
1614 with bpy.context.temp_override(selected_objects=[final_points_ob]):
1615 bpy.ops.object.delete()
1617 # Delete isolated verts if there are any
1618 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1619 ob_surface.select_set(True)
1620 bpy.context.view_layer.objects.active = ob_surface
1622 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1623 bpy.ops.mesh.select_all(action='DESELECT')
1624 bpy.ops.mesh.select_face_by_sides(type='NOTEQUAL')
1625 bpy.ops.mesh.delete()
1626 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1628 # Join crosshatch results with original mesh
1630 # Calculate a distance to merge the verts of the crosshatch surface to the main object
1631 edges_length_sum = 0
1632 for ed in ob_surface.data.edges:
1633 edges_length_sum += (
1634 ob_surface.data.vertices[ed.vertices[0]].co -
1635 ob_surface.data.vertices[ed.vertices[1]].co
1636 ).length
1638 # Make dictionary with all the verts connected to each vert, on the new surface object.
1639 surface_connected_verts = {}
1640 for ed in ob_surface.data.edges:
1641 if not ed.vertices[0] in surface_connected_verts:
1642 surface_connected_verts[ed.vertices[0]] = []
1644 surface_connected_verts[ed.vertices[0]].append(ed.vertices[1])
1646 if ed.vertices[1] not in surface_connected_verts:
1647 surface_connected_verts[ed.vertices[1]] = []
1649 surface_connected_verts[ed.vertices[1]].append(ed.vertices[0])
1651 # Duplicate the new surface object, and use shrinkwrap to
1652 # calculate later the nearest verts to the main object
1653 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1654 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
1655 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1657 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1659 final_ob_duplicate = bpy.context.view_layer.objects.active
1661 shrinkwrap_modifier = context.object.modifiers.new("", 'SHRINKWRAP')
1662 shrinkwrap_modifier.wrap_method = "NEAREST_VERTEX"
1663 shrinkwrap_modifier.target = self.main_object
1665 bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', modifier=shrinkwrap_modifier.name)
1667 # Make list with verts of original mesh as index and coords as value
1668 main_object_verts_coords = []
1669 for v in self.main_object.data.vertices:
1670 coords = self.main_object.matrix_world @ v.co
1672 # To avoid problems when taking "-0.00" as a different value as "0.00"
1673 for c in range(len(coords)):
1674 if "%.3f" % coords[c] == "-0.00":
1675 coords[c] = 0
1677 main_object_verts_coords.append(["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]])
1679 tuple(main_object_verts_coords)
1681 # Determine which verts will be merged, snap them to the nearest verts
1682 # on the original verts, and get them selected
1683 crosshatch_verts_to_merge = []
1684 if self.automatic_join:
1685 for i in range(len(ob_surface.data.vertices)-1):
1686 # Calculate the distance from each of the connected verts to the actual vert,
1687 # and compare it with the distance they would have if joined.
1688 # If they don't change much, that vert can be joined
1689 merge_actual_vert = True
1690 try:
1691 if len(surface_connected_verts[i]) < 4:
1692 for c_v_idx in surface_connected_verts[i]:
1693 points_original = []
1694 points_original.append(ob_surface.data.vertices[c_v_idx].co)
1695 points_original.append(ob_surface.data.vertices[i].co)
1697 points_target = []
1698 points_target.append(ob_surface.data.vertices[c_v_idx].co)
1699 points_target.append(final_ob_duplicate.data.vertices[i].co)
1701 vec_A = points_original[0] - points_original[1]
1702 vec_B = points_target[0] - points_target[1]
1704 dist_A = (points_original[0] - points_original[1]).length
1705 dist_B = (points_target[0] - points_target[1]).length
1707 if not (
1708 points_original[0] == points_original[1] or
1709 points_target[0] == points_target[1]
1710 ): # If any vector's length is zero
1712 angle = vec_A.angle(vec_B) / pi
1713 else:
1714 angle = 0
1716 # Set a range of acceptable variation in the connected edges
1717 if dist_B > dist_A * 1.7 * self.join_stretch_factor or \
1718 dist_B < dist_A / 2 / self.join_stretch_factor or \
1719 angle >= 0.15 * self.join_stretch_factor:
1721 merge_actual_vert = False
1722 break
1723 else:
1724 merge_actual_vert = False
1725 except:
1726 self.report({'WARNING'},
1727 "Crosshatch set incorrectly")
1729 if merge_actual_vert:
1730 coords = final_ob_duplicate.data.vertices[i].co
1731 # To avoid problems when taking "-0.000" as a different value as "0.00"
1732 for c in range(len(coords)):
1733 if "%.3f" % coords[c] == "-0.00":
1734 coords[c] = 0
1736 comparison_coords = ["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]]
1738 if comparison_coords in main_object_verts_coords:
1739 # Get the index of the vert with those coords in the main object
1740 main_object_related_vert_idx = main_object_verts_coords.index(comparison_coords)
1742 if self.main_object.data.vertices[main_object_related_vert_idx].select is True or \
1743 self.main_object_selected_verts_count == 0:
1745 ob_surface.data.vertices[i].co = final_ob_duplicate.data.vertices[i].co
1746 ob_surface.data.vertices[i].select = True
1747 crosshatch_verts_to_merge.append(i)
1749 # Make sure the vert in the main object is selected,
1750 # in case it wasn't selected and the "join crosshatch" option is active
1751 self.main_object.data.vertices[main_object_related_vert_idx].select = True
1753 # Delete duplicated object
1754 with bpy.context.temp_override(selected_objects=[final_ob_duplicate]):
1755 bpy.ops.object.delete()
1757 # Join crosshatched surface and main object
1758 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1759 ob_surface.select_set(True)
1760 self.main_object.select_set(True)
1761 bpy.context.view_layer.objects.active = self.main_object
1763 bpy.ops.object.join('INVOKE_REGION_WIN')
1765 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1766 # Perform Remove doubles to merge verts
1767 if not (self.automatic_join is False and self.main_object_selected_verts_count == 0):
1768 bpy.ops.mesh.remove_doubles(threshold=0.0001)
1770 bpy.ops.mesh.select_all(action='DESELECT')
1772 # If the main object has modifiers, turn their "viewport view status"
1773 # to what it was before the forced deactivation above
1774 if len(self.main_object.modifiers) > 0:
1775 for m_idx in range(len(self.main_object.modifiers)):
1776 self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
1778 self.update()
1780 return {'FINISHED'}
1782 def rectangular_surface(self, context):
1783 # Selected edges
1784 all_selected_edges_idx = []
1785 all_selected_verts = []
1786 all_verts_idx = []
1787 for ed in self.main_object.data.edges:
1788 if ed.select:
1789 all_selected_edges_idx.append(ed.index)
1791 # Selected vertices
1792 if not ed.vertices[0] in all_selected_verts:
1793 all_selected_verts.append(self.main_object.data.vertices[ed.vertices[0]])
1794 if not ed.vertices[1] in all_selected_verts:
1795 all_selected_verts.append(self.main_object.data.vertices[ed.vertices[1]])
1797 # All verts (both from each edge) to determine later
1798 # which are at the tips (those not repeated twice)
1799 all_verts_idx.append(ed.vertices[0])
1800 all_verts_idx.append(ed.vertices[1])
1802 # Identify the tips and "middle-vertex" that separates U from V, if there is one
1803 all_chains_tips_idx = []
1804 for v_idx in all_verts_idx:
1805 if all_verts_idx.count(v_idx) < 2:
1806 all_chains_tips_idx.append(v_idx)
1808 edges_connected_to_tips = []
1809 for ed in self.main_object.data.edges:
1810 if (ed.vertices[0] in all_chains_tips_idx or ed.vertices[1] in all_chains_tips_idx) and \
1811 not (ed.vertices[0] in all_verts_idx and ed.vertices[1] in all_verts_idx):
1813 edges_connected_to_tips.append(ed)
1815 # Check closed selections
1816 # List with groups of three verts, where the first element of the pair is
1817 # the unselected vert of a closed selection and the other two elements are the
1818 # selected neighbor verts (it will be useful to determine which selection chain
1819 # the unselected vert belongs to, and determine the "middle-vertex")
1820 single_unselected_verts_and_neighbors = []
1822 # To identify a "closed" selection (a selection that is a closed chain except
1823 # for one vertex) find the vertex in common that have the edges connected to tips.
1824 # If there is a vertex in common, that one is the unselected vert that closes
1825 # the selection or is a "middle-vertex"
1826 single_unselected_verts = []
1827 for ed in edges_connected_to_tips:
1828 for ed_b in edges_connected_to_tips:
1829 if ed != ed_b:
1830 if ed.vertices[0] == ed_b.vertices[0] and \
1831 not self.main_object.data.vertices[ed.vertices[0]].select and \
1832 ed.vertices[0] not in single_unselected_verts:
1834 # The second element is one of the tips of the selected
1835 # vertices of the closed selection
1836 single_unselected_verts_and_neighbors.append(
1837 [ed.vertices[0], ed.vertices[1], ed_b.vertices[1]]
1839 single_unselected_verts.append(ed.vertices[0])
1840 break
1841 elif ed.vertices[0] == ed_b.vertices[1] and \
1842 not self.main_object.data.vertices[ed.vertices[0]].select and \
1843 ed.vertices[0] not in single_unselected_verts:
1845 single_unselected_verts_and_neighbors.append(
1846 [ed.vertices[0], ed.vertices[1], ed_b.vertices[0]]
1848 single_unselected_verts.append(ed.vertices[0])
1849 break
1850 elif ed.vertices[1] == ed_b.vertices[0] and \
1851 not self.main_object.data.vertices[ed.vertices[1]].select and \
1852 ed.vertices[1] not in single_unselected_verts:
1854 single_unselected_verts_and_neighbors.append(
1855 [ed.vertices[1], ed.vertices[0], ed_b.vertices[1]]
1857 single_unselected_verts.append(ed.vertices[1])
1858 break
1859 elif ed.vertices[1] == ed_b.vertices[1] and \
1860 not self.main_object.data.vertices[ed.vertices[1]].select and \
1861 ed.vertices[1] not in single_unselected_verts:
1863 single_unselected_verts_and_neighbors.append(
1864 [ed.vertices[1], ed.vertices[0], ed_b.vertices[0]]
1866 single_unselected_verts.append(ed.vertices[1])
1867 break
1869 middle_vertex_idx = None
1870 tips_to_discard_idx = []
1872 # Check if there is a "middle-vertex", and get its index
1873 for i in range(0, len(single_unselected_verts_and_neighbors)):
1874 actual_chain_verts = self.get_ordered_verts(
1875 self.main_object, all_selected_edges_idx,
1876 all_verts_idx, single_unselected_verts_and_neighbors[i][1],
1877 None, None
1880 if single_unselected_verts_and_neighbors[i][2] != \
1881 actual_chain_verts[len(actual_chain_verts) - 1].index:
1883 middle_vertex_idx = single_unselected_verts_and_neighbors[i][0]
1884 tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][1])
1885 tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][2])
1887 # List with pairs of verts that belong to the tips of each selection chain (row)
1888 verts_tips_same_chain_idx = []
1889 if len(all_chains_tips_idx) >= 2:
1890 checked_v = []
1891 for i in range(0, len(all_chains_tips_idx)):
1892 if all_chains_tips_idx[i] not in checked_v:
1893 v_chain = self.get_ordered_verts(
1894 self.main_object, all_selected_edges_idx,
1895 all_verts_idx, all_chains_tips_idx[i],
1896 middle_vertex_idx, None
1899 verts_tips_same_chain_idx.append([v_chain[0].index, v_chain[len(v_chain) - 1].index])
1901 checked_v.append(v_chain[0].index)
1902 checked_v.append(v_chain[len(v_chain) - 1].index)
1904 # Selection tips (vertices).
1905 verts_tips_parsed_idx = []
1906 if len(all_chains_tips_idx) >= 2:
1907 for spec_v_idx in all_chains_tips_idx:
1908 if (spec_v_idx not in tips_to_discard_idx):
1909 verts_tips_parsed_idx.append(spec_v_idx)
1911 # Identify the type of selection made by the user
1912 if middle_vertex_idx is not None:
1913 # If there are 4 tips (two selection chains), and
1914 # there is only one single unselected vert (the middle vert)
1915 if len(all_chains_tips_idx) == 4 and len(single_unselected_verts_and_neighbors) == 1:
1916 selection_type = "TWO_CONNECTED"
1917 else:
1918 # The type of the selection was not identified, the script stops.
1919 self.report({'WARNING'}, "The selection isn't valid.")
1921 self.stopping_errors = True
1923 return{'CANCELLED'}
1924 else:
1925 if len(all_chains_tips_idx) == 2: # If there are 2 tips
1926 selection_type = "SINGLE"
1927 elif len(all_chains_tips_idx) == 4: # If there are 4 tips
1928 selection_type = "TWO_NOT_CONNECTED"
1929 elif len(all_chains_tips_idx) == 0:
1930 if len(self.main_splines.data.splines) > 1:
1931 selection_type = "NO_SELECTION"
1932 else:
1933 # If the selection was not identified and there is only one stroke,
1934 # there's no possibility to build a surface, so the script is interrupted
1935 self.report({'WARNING'}, "The selection isn't valid.")
1937 self.stopping_errors = True
1939 return{'CANCELLED'}
1940 else:
1941 # The type of the selection was not identified, the script stops
1942 self.report({'WARNING'}, "The selection isn't valid.")
1944 self.stopping_errors = True
1946 return{'CANCELLED'}
1948 # If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script
1949 if selection_type == "TWO_NOT_CONNECTED" and len(self.main_splines.data.splines) == 1:
1950 self.report({'WARNING'},
1951 "At least two strokes are needed when there are two not connected selections")
1953 self.stopping_errors = True
1955 return{'CANCELLED'}
1957 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1959 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1960 self.main_splines.select_set(True)
1961 bpy.context.view_layer.objects.active = self.main_splines
1963 # Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out
1964 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1965 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1966 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1967 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1968 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1969 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1970 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1971 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1973 self.selection_U_exists = False
1974 self.selection_U2_exists = False
1975 self.selection_V_exists = False
1976 self.selection_V2_exists = False
1978 self.selection_U_is_closed = False
1979 self.selection_U2_is_closed = False
1980 self.selection_V_is_closed = False
1981 self.selection_V2_is_closed = False
1983 # Define what vertices are at the tips of each selection and are not the middle-vertex
1984 if selection_type == "TWO_CONNECTED":
1985 self.selection_U_exists = True
1986 self.selection_V_exists = True
1988 closing_vert_U_idx = None
1989 closing_vert_V_idx = None
1990 closing_vert_U2_idx = None
1991 closing_vert_V2_idx = None
1993 # Determine which selection is Selection-U and which is Selection-V
1994 points_A = []
1995 points_B = []
1996 points_first_stroke_tips = []
1998 points_A.append(
1999 self.main_object.matrix_world @ self.main_object.data.vertices[verts_tips_parsed_idx[0]].co
2001 points_A.append(
2002 self.main_object.matrix_world @ self.main_object.data.vertices[middle_vertex_idx].co
2004 points_B.append(
2005 self.main_object.matrix_world @ self.main_object.data.vertices[verts_tips_parsed_idx[1]].co
2007 points_B.append(
2008 self.main_object.matrix_world @ self.main_object.data.vertices[middle_vertex_idx].co
2010 points_first_stroke_tips.append(
2011 self.main_splines.data.splines[0].bezier_points[0].co
2013 points_first_stroke_tips.append(
2014 self.main_splines.data.splines[0].bezier_points[
2015 len(self.main_splines.data.splines[0].bezier_points) - 1
2016 ].co
2019 angle_A = self.orientation_difference(points_A, points_first_stroke_tips)
2020 angle_B = self.orientation_difference(points_B, points_first_stroke_tips)
2022 if angle_A < angle_B:
2023 first_vert_U_idx = verts_tips_parsed_idx[0]
2024 first_vert_V_idx = verts_tips_parsed_idx[1]
2025 else:
2026 first_vert_U_idx = verts_tips_parsed_idx[1]
2027 first_vert_V_idx = verts_tips_parsed_idx[0]
2029 elif selection_type == "SINGLE" or selection_type == "TWO_NOT_CONNECTED":
2030 first_sketched_point_first_stroke_co = self.main_splines.data.splines[0].bezier_points[0].co
2031 last_sketched_point_first_stroke_co = \
2032 self.main_splines.data.splines[0].bezier_points[
2033 len(self.main_splines.data.splines[0].bezier_points) - 1
2034 ].co
2035 first_sketched_point_last_stroke_co = \
2036 self.main_splines.data.splines[
2037 len(self.main_splines.data.splines) - 1
2038 ].bezier_points[0].co
2039 if len(self.main_splines.data.splines) > 1:
2040 first_sketched_point_second_stroke_co = self.main_splines.data.splines[1].bezier_points[0].co
2041 last_sketched_point_second_stroke_co = \
2042 self.main_splines.data.splines[1].bezier_points[
2043 len(self.main_splines.data.splines[1].bezier_points) - 1
2044 ].co
2046 single_unselected_neighbors = [] # Only the neighbors of the single unselected verts
2047 for verts_neig_idx in single_unselected_verts_and_neighbors:
2048 single_unselected_neighbors.append(verts_neig_idx[1])
2049 single_unselected_neighbors.append(verts_neig_idx[2])
2051 all_chains_tips_and_middle_vert = []
2052 for v_idx in all_chains_tips_idx:
2053 if v_idx not in single_unselected_neighbors:
2054 all_chains_tips_and_middle_vert.append(v_idx)
2056 all_chains_tips_and_middle_vert += single_unselected_verts
2058 all_participating_verts = all_chains_tips_and_middle_vert + all_verts_idx
2060 # The tip of the selected vertices nearest to the first point of the first sketched stroke
2061 nearest_tip_to_first_st_first_pt_idx, shortest_distance_to_first_stroke = \
2062 self.shortest_distance(
2063 self.main_object,
2064 first_sketched_point_first_stroke_co,
2065 all_chains_tips_and_middle_vert
2067 # If the nearest tip is not from a closed selection, get the opposite tip vertex index
2068 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2069 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2071 nearest_tip_to_first_st_first_pt_opposite_idx = \
2072 self.opposite_tip(
2073 nearest_tip_to_first_st_first_pt_idx,
2074 verts_tips_same_chain_idx
2076 # The tip of the selected vertices nearest to the last point of the first sketched stroke
2077 nearest_tip_to_first_st_last_pt_idx, _temp_dist = \
2078 self.shortest_distance(
2079 self.main_object,
2080 last_sketched_point_first_stroke_co,
2081 all_chains_tips_and_middle_vert
2083 # The tip of the selected vertices nearest to the first point of the last sketched stroke
2084 nearest_tip_to_last_st_first_pt_idx, shortest_distance_to_last_stroke = \
2085 self.shortest_distance(
2086 self.main_object,
2087 first_sketched_point_last_stroke_co,
2088 all_chains_tips_and_middle_vert
2090 if len(self.main_splines.data.splines) > 1:
2091 # The selected vertex nearest to the first point of the second sketched stroke
2092 # (This will be useful to determine the direction of the closed
2093 # selection V when extruding along strokes)
2094 nearest_vert_to_second_st_first_pt_idx, _temp_dist = \
2095 self.shortest_distance(
2096 self.main_object,
2097 first_sketched_point_second_stroke_co,
2098 all_verts_idx
2100 # The selected vertex nearest to the first point of the second sketched stroke
2101 # (This will be useful to determine the direction of the closed
2102 # selection V2 when extruding along strokes)
2103 nearest_vert_to_second_st_last_pt_idx, _temp_dist = \
2104 self.shortest_distance(
2105 self.main_object,
2106 last_sketched_point_second_stroke_co,
2107 all_verts_idx
2109 # Determine if the single selection will be treated as U or as V
2110 edges_sum = 0
2111 for i in all_selected_edges_idx:
2112 edges_sum += (
2113 (self.main_object.matrix_world @
2114 self.main_object.data.vertices[self.main_object.data.edges[i].vertices[0]].co) -
2115 (self.main_object.matrix_world @
2116 self.main_object.data.vertices[self.main_object.data.edges[i].vertices[1]].co)
2117 ).length
2119 average_edge_length = edges_sum / len(all_selected_edges_idx)
2121 # Get shortest distance from the first point of the last stroke to any participating vertex
2122 _temp_idx, shortest_distance_to_last_stroke = \
2123 self.shortest_distance(
2124 self.main_object,
2125 first_sketched_point_last_stroke_co,
2126 all_participating_verts
2128 # If the beginning of the first stroke is near enough, and its orientation
2129 # difference with the first edge of the nearest selection chain is not too high,
2130 # interpret things as an "extrude along strokes" instead of "extrude through strokes"
2131 if shortest_distance_to_first_stroke < average_edge_length / 4 and \
2132 shortest_distance_to_last_stroke < average_edge_length and \
2133 len(self.main_splines.data.splines) > 1:
2135 self.selection_U_exists = False
2136 self.selection_V_exists = True
2137 # If the first selection is not closed
2138 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2139 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2140 self.selection_V_is_closed = False
2141 closing_vert_U_idx = None
2142 closing_vert_U2_idx = None
2143 closing_vert_V_idx = None
2144 closing_vert_V2_idx = None
2146 first_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
2148 if selection_type == "TWO_NOT_CONNECTED":
2149 self.selection_V2_exists = True
2151 first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2152 else:
2153 self.selection_V_is_closed = True
2154 closing_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
2156 # Get the neighbors of the first (unselected) vert of the closed selection U.
2157 vert_neighbors = []
2158 for verts in single_unselected_verts_and_neighbors:
2159 if verts[0] == nearest_tip_to_first_st_first_pt_idx:
2160 vert_neighbors.append(verts[1])
2161 vert_neighbors.append(verts[2])
2162 break
2164 verts_V = self.get_ordered_verts(
2165 self.main_object, all_selected_edges_idx,
2166 all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
2169 for i in range(0, len(verts_V)):
2170 if verts_V[i].index == nearest_vert_to_second_st_first_pt_idx:
2171 # If the vertex nearest to the first point of the second stroke
2172 # is in the first half of the selected verts
2173 if i >= len(verts_V) / 2:
2174 first_vert_V_idx = vert_neighbors[1]
2175 break
2176 else:
2177 first_vert_V_idx = vert_neighbors[0]
2178 break
2180 if selection_type == "TWO_NOT_CONNECTED":
2181 self.selection_V2_exists = True
2182 # If the second selection is not closed
2183 if nearest_tip_to_first_st_last_pt_idx not in single_unselected_verts or \
2184 nearest_tip_to_first_st_last_pt_idx == middle_vertex_idx:
2186 self.selection_V2_is_closed = False
2187 closing_vert_V2_idx = None
2188 first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2190 else:
2191 self.selection_V2_is_closed = True
2192 closing_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2194 # Get the neighbors of the first (unselected) vert of the closed selection U
2195 vert_neighbors = []
2196 for verts in single_unselected_verts_and_neighbors:
2197 if verts[0] == nearest_tip_to_first_st_last_pt_idx:
2198 vert_neighbors.append(verts[1])
2199 vert_neighbors.append(verts[2])
2200 break
2202 verts_V2 = self.get_ordered_verts(
2203 self.main_object, all_selected_edges_idx,
2204 all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
2207 for i in range(0, len(verts_V2)):
2208 if verts_V2[i].index == nearest_vert_to_second_st_last_pt_idx:
2209 # If the vertex nearest to the first point of the second stroke
2210 # is in the first half of the selected verts
2211 if i >= len(verts_V2) / 2:
2212 first_vert_V2_idx = vert_neighbors[1]
2213 break
2214 else:
2215 first_vert_V2_idx = vert_neighbors[0]
2216 break
2217 else:
2218 self.selection_V2_exists = False
2220 else:
2221 self.selection_U_exists = True
2222 self.selection_V_exists = False
2223 # If the first selection is not closed
2224 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2225 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2226 self.selection_U_is_closed = False
2227 closing_vert_U_idx = None
2229 points_tips = []
2230 points_tips.append(
2231 self.main_object.matrix_world @
2232 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
2234 points_tips.append(
2235 self.main_object.matrix_world @
2236 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_opposite_idx].co
2238 points_first_stroke_tips = []
2239 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
2240 points_first_stroke_tips.append(
2241 self.main_splines.data.splines[0].bezier_points[
2242 len(self.main_splines.data.splines[0].bezier_points) - 1
2243 ].co
2245 vec_A = points_tips[0] - points_tips[1]
2246 vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
2248 # Compare the direction of the selection and the first
2249 # grease pencil stroke to determine which is the "first" vertex of the selection
2250 if vec_A.dot(vec_B) < 0:
2251 first_vert_U_idx = nearest_tip_to_first_st_first_pt_opposite_idx
2252 else:
2253 first_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
2255 else:
2256 self.selection_U_is_closed = True
2257 closing_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
2259 # Get the neighbors of the first (unselected) vert of the closed selection U
2260 vert_neighbors = []
2261 for verts in single_unselected_verts_and_neighbors:
2262 if verts[0] == nearest_tip_to_first_st_first_pt_idx:
2263 vert_neighbors.append(verts[1])
2264 vert_neighbors.append(verts[2])
2265 break
2267 points_first_and_neighbor = []
2268 points_first_and_neighbor.append(
2269 self.main_object.matrix_world @
2270 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
2272 points_first_and_neighbor.append(
2273 self.main_object.matrix_world @
2274 self.main_object.data.vertices[vert_neighbors[0]].co
2276 points_first_stroke_tips = []
2277 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
2278 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[1].co)
2280 vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
2281 vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
2283 # Compare the direction of the selection and the first grease pencil stroke to
2284 # determine which is the vertex neighbor to the first vertex (unselected) of
2285 # the closed selection. This will determine the direction of the closed selection
2286 if vec_A.dot(vec_B) < 0:
2287 first_vert_U_idx = vert_neighbors[1]
2288 else:
2289 first_vert_U_idx = vert_neighbors[0]
2291 if selection_type == "TWO_NOT_CONNECTED":
2292 self.selection_U2_exists = True
2293 # If the second selection is not closed
2294 if nearest_tip_to_last_st_first_pt_idx not in single_unselected_verts or \
2295 nearest_tip_to_last_st_first_pt_idx == middle_vertex_idx:
2297 self.selection_U2_is_closed = False
2298 closing_vert_U2_idx = None
2299 first_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
2300 else:
2301 self.selection_U2_is_closed = True
2302 closing_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
2304 # Get the neighbors of the first (unselected) vert of the closed selection U
2305 vert_neighbors = []
2306 for verts in single_unselected_verts_and_neighbors:
2307 if verts[0] == nearest_tip_to_last_st_first_pt_idx:
2308 vert_neighbors.append(verts[1])
2309 vert_neighbors.append(verts[2])
2310 break
2312 points_first_and_neighbor = []
2313 points_first_and_neighbor.append(
2314 self.main_object.matrix_world @
2315 self.main_object.data.vertices[nearest_tip_to_last_st_first_pt_idx].co
2317 points_first_and_neighbor.append(
2318 self.main_object.matrix_world @
2319 self.main_object.data.vertices[vert_neighbors[0]].co
2321 points_last_stroke_tips = []
2322 points_last_stroke_tips.append(
2323 self.main_splines.data.splines[
2324 len(self.main_splines.data.splines) - 1
2325 ].bezier_points[0].co
2327 points_last_stroke_tips.append(
2328 self.main_splines.data.splines[
2329 len(self.main_splines.data.splines) - 1
2330 ].bezier_points[1].co
2332 vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
2333 vec_B = points_last_stroke_tips[0] - points_last_stroke_tips[1]
2335 # Compare the direction of the selection and the last grease pencil stroke to
2336 # determine which is the vertex neighbor to the first vertex (unselected) of
2337 # the closed selection. This will determine the direction of the closed selection
2338 if vec_A.dot(vec_B) < 0:
2339 first_vert_U2_idx = vert_neighbors[1]
2340 else:
2341 first_vert_U2_idx = vert_neighbors[0]
2342 else:
2343 self.selection_U2_exists = False
2345 elif selection_type == "NO_SELECTION":
2346 self.selection_U_exists = False
2347 self.selection_V_exists = False
2349 # Get an ordered list of the vertices of Selection-U
2350 verts_ordered_U = []
2351 if self.selection_U_exists:
2352 verts_ordered_U = self.get_ordered_verts(
2353 self.main_object, all_selected_edges_idx,
2354 all_verts_idx, first_vert_U_idx,
2355 middle_vertex_idx, closing_vert_U_idx
2358 # Get an ordered list of the vertices of Selection-U2
2359 verts_ordered_U2 = []
2360 if self.selection_U2_exists:
2361 verts_ordered_U2 = self.get_ordered_verts(
2362 self.main_object, all_selected_edges_idx,
2363 all_verts_idx, first_vert_U2_idx,
2364 middle_vertex_idx, closing_vert_U2_idx
2367 # Get an ordered list of the vertices of Selection-V
2368 verts_ordered_V = []
2369 if self.selection_V_exists:
2370 verts_ordered_V = self.get_ordered_verts(
2371 self.main_object, all_selected_edges_idx,
2372 all_verts_idx, first_vert_V_idx,
2373 middle_vertex_idx, closing_vert_V_idx
2375 verts_ordered_V_indices = [x.index for x in verts_ordered_V]
2377 # Get an ordered list of the vertices of Selection-V2
2378 verts_ordered_V2 = []
2379 if self.selection_V2_exists:
2380 verts_ordered_V2 = self.get_ordered_verts(
2381 self.main_object, all_selected_edges_idx,
2382 all_verts_idx, first_vert_V2_idx,
2383 middle_vertex_idx, closing_vert_V2_idx
2386 # Check if when there are two-not-connected selections both have the same
2387 # number of verts. If not terminate the script
2388 if ((self.selection_U2_exists and len(verts_ordered_U) != len(verts_ordered_U2)) or
2389 (self.selection_V2_exists and len(verts_ordered_V) != len(verts_ordered_V2))):
2390 # Display a warning
2391 self.report({'WARNING'}, "Both selections must have the same number of edges")
2393 self.stopping_errors = True
2395 return{'CANCELLED'}
2397 # Calculate edges U proportions
2398 # Sum selected edges U lengths
2399 edges_lengths_U = []
2400 edges_lengths_sum_U = 0
2402 if self.selection_U_exists:
2403 edges_lengths_U, edges_lengths_sum_U = self.get_chain_length(
2404 self.main_object,
2405 verts_ordered_U
2407 if self.selection_U2_exists:
2408 edges_lengths_U2, edges_lengths_sum_U2 = self.get_chain_length(
2409 self.main_object,
2410 verts_ordered_U2
2412 # Sum selected edges V lengths
2413 edges_lengths_V = []
2414 edges_lengths_sum_V = 0
2416 if self.selection_V_exists:
2417 edges_lengths_V, edges_lengths_sum_V = self.get_chain_length(
2418 self.main_object,
2419 verts_ordered_V
2421 if self.selection_V2_exists:
2422 edges_lengths_V2, edges_lengths_sum_V2 = self.get_chain_length(
2423 self.main_object,
2424 verts_ordered_V2
2427 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2428 bpy.ops.curve.subdivide('INVOKE_REGION_WIN',
2429 number_cuts=bpy.context.scene.bsurfaces.SURFSK_precision)
2430 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2432 # Proportions U
2433 edges_proportions_U = []
2434 edges_proportions_U = self.get_edges_proportions(
2435 edges_lengths_U, edges_lengths_sum_U,
2436 self.selection_U_exists, self.edges_U
2438 verts_count_U = len(edges_proportions_U) + 1
2440 if self.selection_U2_exists:
2441 edges_proportions_U2 = []
2442 edges_proportions_U2 = self.get_edges_proportions(
2443 edges_lengths_U2, edges_lengths_sum_U2,
2444 self.selection_U2_exists, self.edges_V
2447 # Proportions V
2448 edges_proportions_V = []
2449 edges_proportions_V = self.get_edges_proportions(
2450 edges_lengths_V, edges_lengths_sum_V,
2451 self.selection_V_exists, self.edges_V
2454 if self.selection_V2_exists:
2455 edges_proportions_V2 = []
2456 edges_proportions_V2 = self.get_edges_proportions(
2457 edges_lengths_V2, edges_lengths_sum_V2,
2458 self.selection_V2_exists, self.edges_V
2461 # Cyclic Follow: simplify sketched curves, make them Cyclic, and complete
2462 # the actual sketched curves with a "closing segment"
2463 if self.cyclic_follow and not self.selection_V_exists and not \
2464 ((self.selection_U_exists and not self.selection_U_is_closed) or
2465 (self.selection_U2_exists and not self.selection_U2_is_closed)):
2467 simplified_spline_coords = []
2468 simplified_curve = []
2469 ob_simplified_curve = []
2470 splines_first_v_co = []
2471 for i in range(len(self.main_splines.data.splines)):
2472 # Create a curve object for the actual spline "cyclic extension"
2473 simplified_curve.append(bpy.data.curves.new('SURFSKIO_simpl_crv', 'CURVE'))
2474 ob_simplified_curve.append(bpy.data.objects.new('SURFSKIO_simpl_crv', simplified_curve[i]))
2475 bpy.context.collection.objects.link(ob_simplified_curve[i])
2477 simplified_curve[i].dimensions = "3D"
2479 spline_coords = []
2480 for bp in self.main_splines.data.splines[i].bezier_points:
2481 spline_coords.append(bp.co)
2483 # Simplification
2484 simplified_spline_coords.append(self.simplify_spline(spline_coords, 5))
2486 # Get the coordinates of the first vert of the actual spline
2487 splines_first_v_co.append(simplified_spline_coords[i][0])
2489 # Generate the spline
2490 spline = simplified_curve[i].splines.new('BEZIER')
2491 # less one because one point is added when the spline is created
2492 spline.bezier_points.add(len(simplified_spline_coords[i]) - 1)
2493 for p in range(0, len(simplified_spline_coords[i])):
2494 spline.bezier_points[p].co = simplified_spline_coords[i][p]
2496 spline.use_cyclic_u = True
2498 spline_bp_count = len(spline.bezier_points)
2500 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2501 ob_simplified_curve[i].select_set(True)
2502 bpy.context.view_layer.objects.active = ob_simplified_curve[i]
2504 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2505 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
2506 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2507 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
2508 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2510 # Select the "closing segment", and subdivide it
2511 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_control_point = True
2512 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_left_handle = True
2513 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_right_handle = True
2515 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_control_point = True
2516 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_left_handle = True
2517 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_right_handle = True
2519 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2520 segments = sqrt(
2521 (ob_simplified_curve[i].data.splines[0].bezier_points[0].co -
2522 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].co).length /
2523 self.average_gp_segment_length
2525 for t in range(2):
2526 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=int(segments))
2528 # Delete the other vertices and make it non-cyclic to
2529 # keep only the needed verts of the "closing segment"
2530 bpy.ops.curve.select_all(action='INVERT')
2531 bpy.ops.curve.delete(type='VERT')
2532 ob_simplified_curve[i].data.splines[0].use_cyclic_u = False
2533 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2535 # Add the points of the "closing segment" to the original curve from grease pencil stroke
2536 first_new_index = len(self.main_splines.data.splines[i].bezier_points)
2537 self.main_splines.data.splines[i].bezier_points.add(
2538 len(ob_simplified_curve[i].data.splines[0].bezier_points) - 1
2540 for t in range(1, len(ob_simplified_curve[i].data.splines[0].bezier_points)):
2541 self.main_splines.data.splines[i].bezier_points[t - 1 + first_new_index].co = \
2542 ob_simplified_curve[i].data.splines[0].bezier_points[t].co
2544 # Delete the temporal curve
2545 with bpy.context.temp_override(selected_objects=[ob_simplified_curve[i]]):
2546 bpy.ops.object.delete()
2548 # Get the coords of the points distributed along the sketched strokes,
2549 # with proportions-U of the first selection
2550 pts_on_strokes_with_proportions_U = self.distribute_pts(
2551 self.main_splines.data.splines,
2552 edges_proportions_U
2554 sketched_splines_parsed = []
2556 if self.selection_U2_exists:
2557 # Initialize the multidimensional list with the proportions of all the segments
2558 proportions_loops_crossing_strokes = []
2559 for i in range(len(pts_on_strokes_with_proportions_U)):
2560 proportions_loops_crossing_strokes.append([])
2562 for t in range(len(pts_on_strokes_with_proportions_U[0])):
2563 proportions_loops_crossing_strokes[i].append(None)
2565 # Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U
2566 for lp in range(len(pts_on_strokes_with_proportions_U[0])):
2567 loop_segments_lengths = []
2569 for st in range(len(pts_on_strokes_with_proportions_U)):
2570 # When on the first stroke, add the segment from the selection to the first stroke
2571 if st == 0:
2572 loop_segments_lengths.append(
2573 ((self.main_object.matrix_world @ verts_ordered_U[lp].co) -
2574 pts_on_strokes_with_proportions_U[0][lp]).length
2576 # For all strokes except for the last, calculate the distance
2577 # from the actual stroke to the next
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 pts_on_strokes_with_proportions_U[st + 1][lp]).length
2583 # When on the last stroke, add the segments
2584 # from the last stroke to the second selection
2585 if st == len(pts_on_strokes_with_proportions_U) - 1:
2586 loop_segments_lengths.append(
2587 (pts_on_strokes_with_proportions_U[st][lp] -
2588 (self.main_object.matrix_world @ verts_ordered_U2[lp].co)).length
2590 # Calculate full loop length
2591 loop_seg_lengths_sum = 0
2592 for i in range(len(loop_segments_lengths)):
2593 loop_seg_lengths_sum += loop_segments_lengths[i]
2595 # Fill the multidimensional list with the proportions of all the segments
2596 for st in range(len(pts_on_strokes_with_proportions_U)):
2597 proportions_loops_crossing_strokes[st][lp] = \
2598 loop_segments_lengths[st] / loop_seg_lengths_sum
2600 # Calculate proportions for each stroke
2601 for st in range(len(pts_on_strokes_with_proportions_U)):
2602 actual_stroke_spline = []
2603 # Needs to be a list for the "distribute_pts" method
2604 actual_stroke_spline.append(self.main_splines.data.splines[st])
2606 # Calculate the proportions for the actual stroke.
2607 actual_edges_proportions_U = []
2608 for i in range(len(edges_proportions_U)):
2609 proportions_sum = 0
2611 # Sum the proportions of this loop up to the actual.
2612 for t in range(0, st + 1):
2613 proportions_sum += proportions_loops_crossing_strokes[t][i]
2614 # i + 1, because proportions_loops_crossing_strokes refers to loops,
2615 # and the proportions refer to edges, so we start at the element 1
2616 # of proportions_loops_crossing_strokes instead of element 0
2617 actual_edges_proportions_U.append(
2618 edges_proportions_U[i] -
2619 ((edges_proportions_U[i] - edges_proportions_U2[i]) * proportions_sum)
2621 points_actual_spline = self.distribute_pts(actual_stroke_spline, actual_edges_proportions_U)
2622 sketched_splines_parsed.append(points_actual_spline[0])
2623 else:
2624 sketched_splines_parsed = pts_on_strokes_with_proportions_U
2626 # If the selection type is "TWO_NOT_CONNECTED" replace the
2627 # points of the last spline with the points in the "target" selection
2628 if selection_type == "TWO_NOT_CONNECTED":
2629 if self.selection_U2_exists:
2630 for i in range(0, len(sketched_splines_parsed[len(sketched_splines_parsed) - 1])):
2631 sketched_splines_parsed[len(sketched_splines_parsed) - 1][i] = \
2632 self.main_object.matrix_world @ verts_ordered_U2[i].co
2634 # Create temporary curves along the "control-points" found
2635 # on the sketched curves and the mesh selection
2636 mesh_ctrl_pts_name = "SURFSKIO_ctrl_pts"
2637 me = bpy.data.meshes.new(mesh_ctrl_pts_name)
2638 ob_ctrl_pts = bpy.data.objects.new(mesh_ctrl_pts_name, me)
2639 ob_ctrl_pts.data = me
2640 bpy.context.collection.objects.link(ob_ctrl_pts)
2642 cyclic_loops_U = []
2643 first_verts = []
2644 second_verts = []
2645 last_verts = []
2647 for i in range(0, verts_count_U):
2648 vert_num_in_spline = 1
2650 if self.selection_U_exists:
2651 ob_ctrl_pts.data.vertices.add(1)
2652 last_v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
2653 last_v.co = self.main_object.matrix_world @ verts_ordered_U[i].co
2655 vert_num_in_spline += 1
2657 for t in range(0, len(sketched_splines_parsed)):
2658 ob_ctrl_pts.data.vertices.add(1)
2659 v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
2660 v.co = sketched_splines_parsed[t][i]
2662 if vert_num_in_spline > 1:
2663 ob_ctrl_pts.data.edges.add(1)
2664 ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[0] = \
2665 len(ob_ctrl_pts.data.vertices) - 2
2666 ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[1] = \
2667 len(ob_ctrl_pts.data.vertices) - 1
2669 if t == 0:
2670 first_verts.append(v.index)
2672 if t == 1:
2673 second_verts.append(v.index)
2675 if t == len(sketched_splines_parsed) - 1:
2676 last_verts.append(v.index)
2678 last_v = v
2679 vert_num_in_spline += 1
2681 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2682 ob_ctrl_pts.select_set(True)
2683 bpy.context.view_layer.objects.active = ob_ctrl_pts
2685 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2686 bpy.ops.mesh.select_all(action='DESELECT')
2687 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2689 # Determine which loops-U will be "Cyclic"
2690 for i in range(0, len(first_verts)):
2691 # When there is Cyclic Cross there is no need of
2692 # Automatic Join, (and there are at least three strokes)
2693 if self.automatic_join and not self.cyclic_cross and \
2694 selection_type != "TWO_CONNECTED" and len(self.main_splines.data.splines) >= 3:
2696 v = ob_ctrl_pts.data.vertices
2697 first_point_co = v[first_verts[i]].co
2698 second_point_co = v[second_verts[i]].co
2699 last_point_co = v[last_verts[i]].co
2701 # Coordinates of the point in the center of both the first and last verts.
2702 verts_center_co = [
2703 (first_point_co[0] + last_point_co[0]) / 2,
2704 (first_point_co[1] + last_point_co[1]) / 2,
2705 (first_point_co[2] + last_point_co[2]) / 2
2707 vec_A = second_point_co - first_point_co
2708 vec_B = second_point_co - Vector(verts_center_co)
2710 # Calculate the length of the first segment of the loop,
2711 # and the length it would have after moving the first vert
2712 # to the middle position between first and last
2713 length_original = (second_point_co - first_point_co).length
2714 length_target = (second_point_co - Vector(verts_center_co)).length
2716 angle = vec_A.angle(vec_B) / pi
2718 # If the target length doesn't stretch too much, and the
2719 # its angle doesn't change to much either
2720 if length_target <= length_original * 1.03 * self.join_stretch_factor and \
2721 angle <= 0.008 * self.join_stretch_factor and not self.selection_U_exists:
2723 cyclic_loops_U.append(True)
2724 # Move the first vert to the center coordinates
2725 ob_ctrl_pts.data.vertices[first_verts[i]].co = verts_center_co
2726 # Select the last verts from Cyclic loops, for later deletion all at once
2727 v[last_verts[i]].select = True
2728 else:
2729 cyclic_loops_U.append(False)
2730 else:
2731 # If "Cyclic Cross" is active then "all" crossing curves become cyclic
2732 if self.cyclic_cross and not self.selection_U_exists and not \
2733 ((self.selection_V_exists and not self.selection_V_is_closed) or
2734 (self.selection_V2_exists and not self.selection_V2_is_closed)):
2736 cyclic_loops_U.append(True)
2737 else:
2738 cyclic_loops_U.append(False)
2740 # The cyclic_loops_U list needs to be reversed.
2741 cyclic_loops_U.reverse()
2743 # Delete the previously selected (last_)verts.
2744 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2745 bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
2746 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2748 # Create curves from control points.
2749 bpy.ops.object.convert('INVOKE_REGION_WIN', target='CURVE', keep_original=False)
2750 ob_curves_surf = bpy.context.view_layer.objects.active
2751 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2752 bpy.ops.curve.spline_type_set('INVOKE_REGION_WIN', type='BEZIER')
2753 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2755 # Make Cyclic the splines designated as Cyclic.
2756 for i in range(0, len(cyclic_loops_U)):
2757 ob_curves_surf.data.splines[i].use_cyclic_u = cyclic_loops_U[i]
2759 # Get the coords of all points on first loop-U, for later comparison with its
2760 # subdivided version, to know which points of the loops-U are crossed by the
2761 # original strokes. The indices will be the same for the other loops-U
2762 if self.loops_on_strokes:
2763 coords_loops_U_control_points = []
2764 for p in ob_ctrl_pts.data.splines[0].bezier_points:
2765 coords_loops_U_control_points.append(["%.4f" % p.co[0], "%.4f" % p.co[1], "%.4f" % p.co[2]])
2767 tuple(coords_loops_U_control_points)
2769 # Calculate number of edges-V in case option "Loops on strokes" is active or inactive
2770 if self.loops_on_strokes and not self.selection_V_exists:
2771 edges_V_count = len(self.main_splines.data.splines) * self.edges_V
2772 else:
2773 edges_V_count = len(edges_proportions_V)
2775 # The Follow precision will vary depending on the number of Follow face-loops
2776 precision_multiplier = round(2 + (edges_V_count / 15))
2777 curve_cuts = bpy.context.scene.bsurfaces.SURFSK_precision * precision_multiplier
2779 # Subdivide the curves
2780 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=curve_cuts)
2782 # The verts position shifting that happens with splines subdivision.
2783 # For later reorder splines points
2784 verts_position_shift = curve_cuts + 1
2785 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2787 # Reorder coordinates of the points of each spline to put the first point of
2788 # the spline starting at the position it was the first point before sudividing
2789 # the curve. And make a new curve object per spline (to handle memory better later)
2790 splines_U_objects = []
2791 for i in range(len(ob_curves_surf.data.splines)):
2792 spline_U_curve = bpy.data.curves.new('SURFSKIO_spline_U_' + str(i), 'CURVE')
2793 ob_spline_U = bpy.data.objects.new('SURFSKIO_spline_U_' + str(i), spline_U_curve)
2794 bpy.context.collection.objects.link(ob_spline_U)
2796 spline_U_curve.dimensions = "3D"
2798 # Add points to the spline in the new curve object
2799 ob_spline_U.data.splines.new('BEZIER')
2800 for t in range(len(ob_curves_surf.data.splines[i].bezier_points)):
2801 if cyclic_loops_U[i] is True and not self.selection_U_exists: # If the loop is cyclic
2802 if t + verts_position_shift <= len(ob_curves_surf.data.splines[i].bezier_points) - 1:
2803 point_index = t + verts_position_shift
2804 else:
2805 point_index = t + verts_position_shift - len(ob_curves_surf.data.splines[i].bezier_points)
2806 else:
2807 point_index = t
2808 # to avoid adding the first point since it's added when the spline is created
2809 if t > 0:
2810 ob_spline_U.data.splines[0].bezier_points.add(1)
2811 ob_spline_U.data.splines[0].bezier_points[t].co = \
2812 ob_curves_surf.data.splines[i].bezier_points[point_index].co
2814 if cyclic_loops_U[i] is True and not self.selection_U_exists: # If the loop is cyclic
2815 # Add a last point at the same location as the first one
2816 ob_spline_U.data.splines[0].bezier_points.add(1)
2817 ob_spline_U.data.splines[0].bezier_points[len(ob_spline_U.data.splines[0].bezier_points) - 1].co = \
2818 ob_spline_U.data.splines[0].bezier_points[0].co
2819 else:
2820 ob_spline_U.data.splines[0].use_cyclic_u = False
2822 splines_U_objects.append(ob_spline_U)
2823 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2824 ob_spline_U.select_set(True)
2825 bpy.context.view_layer.objects.active = ob_spline_U
2827 # When option "Loops on strokes" is active each "Cross" loop will have
2828 # its own proportions according to where the original strokes "touch" them
2829 if self.loops_on_strokes:
2830 # Get the indices of points where the original strokes "touch" loops-U
2831 points_U_crossed_by_strokes = []
2832 for i in range(len(splines_U_objects[0].data.splines[0].bezier_points)):
2833 bp = splines_U_objects[0].data.splines[0].bezier_points[i]
2834 if ["%.4f" % bp.co[0], "%.4f" % bp.co[1], "%.4f" % bp.co[2]] in coords_loops_U_control_points:
2835 points_U_crossed_by_strokes.append(i)
2837 # Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke
2838 edge_order_number_for_splines = {}
2839 if self.selection_V_exists:
2840 # For two-connected selections add a first hypothetic stroke at the beginning.
2841 if selection_type == "TWO_CONNECTED":
2842 edge_order_number_for_splines[0] = 0
2844 for i in range(len(self.main_splines.data.splines)):
2845 sp = self.main_splines.data.splines[i]
2846 v_idx, _dist_temp = self.shortest_distance(
2847 self.main_object,
2848 sp.bezier_points[0].co,
2849 verts_ordered_V_indices
2851 # Get the position (edges count) of the vert v_idx in the selected chain V
2852 edge_idx_in_chain = verts_ordered_V_indices.index(v_idx)
2854 # For two-connected selections the strokes go after the
2855 # hypothetic stroke added before, so the index adds one per spline
2856 if selection_type == "TWO_CONNECTED":
2857 spline_number = i + 1
2858 else:
2859 spline_number = i
2861 edge_order_number_for_splines[spline_number] = edge_idx_in_chain
2863 # Get the first and last verts indices for later comparison
2864 if i == 0:
2865 first_v_idx = v_idx
2866 elif i == len(self.main_splines.data.splines) - 1:
2867 last_v_idx = v_idx
2869 if self.selection_V_is_closed:
2870 # If there is no last stroke on the last vertex (same as first vertex),
2871 # add a hypothetic spline at last vert order
2872 if first_v_idx != last_v_idx:
2873 edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
2874 len(verts_ordered_V_indices) - 1
2875 else:
2876 if self.cyclic_cross:
2877 edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
2878 len(verts_ordered_V_indices) - 2
2879 edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
2880 len(verts_ordered_V_indices) - 1
2881 else:
2882 edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
2883 len(verts_ordered_V_indices) - 1
2885 # Get the coords of the points distributed along the
2886 # "crossing curves", with appropriate proportions-V
2887 surface_splines_parsed = []
2888 for i in range(len(splines_U_objects)):
2889 sp_ob = splines_U_objects[i]
2890 # If "Loops on strokes" option is active, calculate the proportions for each loop-U
2891 if self.loops_on_strokes:
2892 # Segments distances from stroke to stroke
2893 dist = 0
2894 full_dist = 0
2895 segments_distances = []
2896 for t in range(len(sp_ob.data.splines[0].bezier_points)):
2897 bp = sp_ob.data.splines[0].bezier_points[t]
2899 if t == 0:
2900 last_p = bp.co
2901 else:
2902 actual_p = bp.co
2903 dist += (last_p - actual_p).length
2905 if t in points_U_crossed_by_strokes:
2906 segments_distances.append(dist)
2907 full_dist += dist
2909 dist = 0
2911 last_p = actual_p
2913 # Calculate Proportions.
2914 used_edges_proportions_V = []
2915 for t in range(len(segments_distances)):
2916 if self.selection_V_exists:
2917 if t == 0:
2918 order_number_last_stroke = 0
2920 segment_edges_length_V = 0
2921 segment_edges_length_V2 = 0
2922 for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
2923 segment_edges_length_V += edges_lengths_V[order]
2924 if self.selection_V2_exists:
2925 segment_edges_length_V2 += edges_lengths_V2[order]
2927 for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
2928 # Calculate each "sub-segment" (the ones between each stroke) length
2929 if self.selection_V2_exists:
2930 proportion_sub_seg = (edges_lengths_V2[order] -
2931 ((edges_lengths_V2[order] - edges_lengths_V[order]) /
2932 len(splines_U_objects) * i)) / (segment_edges_length_V2 -
2933 (segment_edges_length_V2 - segment_edges_length_V) /
2934 len(splines_U_objects) * i)
2936 sub_seg_dist = segments_distances[t] * proportion_sub_seg
2937 else:
2938 proportion_sub_seg = edges_lengths_V[order] / segment_edges_length_V
2939 sub_seg_dist = segments_distances[t] * proportion_sub_seg
2941 used_edges_proportions_V.append(sub_seg_dist / full_dist)
2943 order_number_last_stroke = edge_order_number_for_splines[t + 1]
2945 else:
2946 for _c in range(self.edges_V):
2947 # Calculate each "sub-segment" (the ones between each stroke) length
2948 sub_seg_dist = segments_distances[t] / self.edges_V
2949 used_edges_proportions_V.append(sub_seg_dist / full_dist)
2951 actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
2952 surface_splines_parsed.append(actual_spline[0])
2954 else:
2955 if self.selection_V2_exists:
2956 used_edges_proportions_V = []
2957 for p in range(len(edges_proportions_V)):
2958 used_edges_proportions_V.append(
2959 edges_proportions_V2[p] -
2960 ((edges_proportions_V2[p] -
2961 edges_proportions_V[p]) / len(splines_U_objects) * i)
2963 else:
2964 used_edges_proportions_V = edges_proportions_V
2966 actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
2967 surface_splines_parsed.append(actual_spline[0])
2969 # Set the verts of the first and last splines to the locations
2970 # of the respective verts in the selections
2971 if self.selection_V_exists:
2972 for i in range(0, len(surface_splines_parsed[0])):
2973 surface_splines_parsed[len(surface_splines_parsed) - 1][i] = \
2974 self.main_object.matrix_world @ verts_ordered_V[i].co
2976 if selection_type == "TWO_NOT_CONNECTED":
2977 if self.selection_V2_exists:
2978 for i in range(0, len(surface_splines_parsed[0])):
2979 surface_splines_parsed[0][i] = self.main_object.matrix_world @ verts_ordered_V2[i].co
2981 # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
2982 # merge the verts of the tips of the loops when they are "near enough"
2983 if self.automatic_join and selection_type != "TWO_CONNECTED":
2984 # Join the tips of "Follow" loops that are near enough and must be "closed"
2985 if not self.selection_V_exists and len(edges_proportions_U) >= 3:
2986 for i in range(len(surface_splines_parsed[0])):
2987 sp = surface_splines_parsed
2988 loop_segment_dist = (sp[0][i] - sp[1][i]).length
2990 verts_middle_position_co = [
2991 (sp[0][i][0] + sp[len(sp) - 1][i][0]) / 2,
2992 (sp[0][i][1] + sp[len(sp) - 1][i][1]) / 2,
2993 (sp[0][i][2] + sp[len(sp) - 1][i][2]) / 2
2995 points_original = []
2996 points_original.append(sp[1][i])
2997 points_original.append(sp[0][i])
2999 points_target = []
3000 points_target.append(sp[1][i])
3001 points_target.append(Vector(verts_middle_position_co))
3003 vec_A = points_original[0] - points_original[1]
3004 vec_B = points_target[0] - points_target[1]
3005 # check for zero angles, not sure if it is a great fix
3006 if vec_A.length != 0 and vec_B.length != 0:
3007 angle = vec_A.angle(vec_B) / pi
3008 edge_new_length = (Vector(verts_middle_position_co) - sp[1][i]).length
3009 else:
3010 angle = 0
3011 edge_new_length = 0
3013 # If after moving the verts to the middle point, the segment doesn't stretch too much
3014 if edge_new_length <= loop_segment_dist * 1.5 * \
3015 self.join_stretch_factor and angle < 0.25 * self.join_stretch_factor:
3017 # Avoid joining when the actual loop must be merged with the original mesh
3018 if not (self.selection_U_exists and i == 0) and \
3019 not (self.selection_U2_exists and i == len(surface_splines_parsed[0]) - 1):
3021 # Change the coords of both verts to the middle position
3022 surface_splines_parsed[0][i] = verts_middle_position_co
3023 surface_splines_parsed[len(surface_splines_parsed) - 1][i] = verts_middle_position_co
3025 # Delete object with control points and object from grease pencil conversion
3026 with bpy.context.temp_override(selected_objects=[ob_ctrl_pts]):
3027 bpy.ops.object.delete()
3029 with bpy.context.temp_override(selected_objects=splines_U_objects):
3030 bpy.ops.object.delete()
3032 # Generate surface
3034 # Get all verts coords
3035 all_surface_verts_co = []
3036 for i in range(0, len(surface_splines_parsed)):
3037 # Get coords of all verts and make a list with them
3038 for pt_co in surface_splines_parsed[i]:
3039 all_surface_verts_co.append(pt_co)
3041 # Define verts for each face
3042 all_surface_faces = []
3043 for i in range(0, len(all_surface_verts_co) - len(surface_splines_parsed[0])):
3044 if ((i + 1) / len(surface_splines_parsed[0]) != int((i + 1) / len(surface_splines_parsed[0]))):
3045 all_surface_faces.append(
3046 [i + 1, i, i + len(surface_splines_parsed[0]),
3047 i + len(surface_splines_parsed[0]) + 1]
3049 # Build the mesh
3050 surf_me_name = "SURFSKIO_surface"
3051 me_surf = bpy.data.meshes.new(surf_me_name)
3052 me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
3053 ob_surface = object_utils.object_data_add(context, me_surf)
3054 ob_surface.location = (0.0, 0.0, 0.0)
3055 ob_surface.rotation_euler = (0.0, 0.0, 0.0)
3056 ob_surface.scale = (1.0, 1.0, 1.0)
3058 # Select all the "unselected but participating" verts, from closed selection
3059 # or double selections with middle-vertex, for later join with remove doubles
3060 for v_idx in single_unselected_verts:
3061 self.main_object.data.vertices[v_idx].select = True
3063 # Join the new mesh to the main object
3064 ob_surface.select_set(True)
3065 self.main_object.select_set(True)
3066 bpy.context.view_layer.objects.active = self.main_object
3068 bpy.ops.object.join('INVOKE_REGION_WIN')
3070 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3072 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN', threshold=0.0001)
3073 bpy.ops.mesh.normals_make_consistent('INVOKE_REGION_WIN', inside=False)
3074 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
3076 self.update()
3078 return{'FINISHED'}
3080 def update(self):
3081 try:
3082 global global_shade_smooth
3083 if global_shade_smooth:
3084 bpy.ops.object.shade_smooth()
3085 else:
3086 bpy.ops.object.shade_flat()
3087 bpy.context.scene.bsurfaces.SURFSK_shade_smooth = global_shade_smooth
3088 except:
3089 pass
3091 return{'FINISHED'}
3093 def execute(self, context):
3095 if bpy.ops.object.mode_set.poll():
3096 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3098 try:
3099 global global_mesh_object
3100 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
3101 bpy.data.objects[global_mesh_object].select_set(True)
3102 self.main_object = bpy.data.objects[global_mesh_object]
3103 bpy.context.view_layer.objects.active = self.main_object
3104 bsurfaces_props = bpy.context.scene.bsurfaces
3105 except:
3106 self.report({'WARNING'}, "Specify the name of the object with retopology")
3107 return{"CANCELLED"}
3108 bpy.context.view_layer.objects.active = self.main_object
3110 self.update()
3112 if not self.is_fill_faces:
3113 bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
3114 value='True, False, False')
3116 # Build splines from the "last saved splines".
3117 last_saved_curve = bpy.data.curves.new('SURFSKIO_last_crv', 'CURVE')
3118 self.main_splines = bpy.data.objects.new('SURFSKIO_last_crv', last_saved_curve)
3119 bpy.context.collection.objects.link(self.main_splines)
3121 last_saved_curve.dimensions = "3D"
3123 for sp in self.last_strokes_splines_coords:
3124 spline = self.main_splines.data.splines.new('BEZIER')
3125 # less one because one point is added when the spline is created
3126 spline.bezier_points.add(len(sp) - 1)
3127 for p in range(0, len(sp)):
3128 spline.bezier_points[p].co = [sp[p][0], sp[p][1], sp[p][2]]
3130 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3132 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3133 self.main_splines.select_set(True)
3134 bpy.context.view_layer.objects.active = self.main_splines
3136 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
3138 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
3139 # Important to make it vector first and then automatic, otherwise the
3140 # tips handles get too big and distort the shrinkwrap results later
3141 bpy.ops.curve.handle_type_set(type='VECTOR')
3142 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
3143 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3144 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3146 self.main_splines.name = "SURFSKIO_temp_strokes"
3148 if self.is_crosshatch:
3149 strokes_for_crosshatch = True
3150 strokes_for_rectangular_surface = False
3151 else:
3152 strokes_for_rectangular_surface = True
3153 strokes_for_crosshatch = False
3155 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3157 if strokes_for_rectangular_surface:
3158 self.rectangular_surface(context)
3159 elif strokes_for_crosshatch:
3160 self.crosshatch_surface_execute(context)
3162 #Set Shade smooth to new polygons
3163 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3164 global global_shade_smooth
3165 if global_shade_smooth:
3166 bpy.ops.object.shade_smooth()
3167 else:
3168 bpy.ops.object.shade_flat()
3170 # Delete main splines
3171 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3172 if self.keep_strokes:
3173 self.main_splines.name = "keep_strokes"
3174 self.main_splines.data.bevel_depth = 0.001
3175 if "keep_strokes_material" in bpy.data.materials :
3176 self.main_splines.data.materials.append(bpy.data.materials["keep_strokes_material"])
3177 else:
3178 mat = bpy.data.materials.new("keep_strokes_material")
3179 mat.diffuse_color = (1, 0, 0, 0)
3180 mat.specular_color = (1, 0, 0)
3181 mat.specular_intensity = 0.0
3182 mat.roughness = 0.0
3183 self.main_splines.data.materials.append(mat)
3184 else:
3185 with bpy.context.temp_override(selected_objects=[self.main_splines]):
3186 bpy.ops.object.delete()
3188 # Delete grease pencil strokes
3189 if self.strokes_type == "GP_STROKES" and not self.stopping_errors:
3190 try:
3191 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
3192 except:
3193 pass
3195 # Delete annotations
3196 if self.strokes_type == "GP_ANNOTATION" and not self.stopping_errors:
3197 try:
3198 bpy.context.annotation_data.layers.active.clear()
3199 except:
3200 pass
3202 bsurfaces_props = bpy.context.scene.bsurfaces
3203 bsurfaces_props.SURFSK_edges_U = self.edges_U
3204 bsurfaces_props.SURFSK_edges_V = self.edges_V
3205 bsurfaces_props.SURFSK_cyclic_cross = self.cyclic_cross
3206 bsurfaces_props.SURFSK_cyclic_follow = self.cyclic_follow
3207 bsurfaces_props.SURFSK_automatic_join = self.automatic_join
3208 bsurfaces_props.SURFSK_loops_on_strokes = self.loops_on_strokes
3209 bsurfaces_props.SURFSK_keep_strokes = self.keep_strokes
3211 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3212 self.main_object.select_set(True)
3213 bpy.context.view_layer.objects.active = self.main_object
3215 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3217 self.update()
3219 return{'FINISHED'}
3221 def invoke(self, context, event):
3223 if bpy.ops.object.mode_set.poll():
3224 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3226 bsurfaces_props = bpy.context.scene.bsurfaces
3227 self.cyclic_cross = bsurfaces_props.SURFSK_cyclic_cross
3228 self.cyclic_follow = bsurfaces_props.SURFSK_cyclic_follow
3229 self.automatic_join = bsurfaces_props.SURFSK_automatic_join
3230 self.loops_on_strokes = bsurfaces_props.SURFSK_loops_on_strokes
3231 self.keep_strokes = bsurfaces_props.SURFSK_keep_strokes
3233 try:
3234 global global_mesh_object
3235 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
3236 bpy.data.objects[global_mesh_object].select_set(True)
3237 self.main_object = bpy.data.objects[global_mesh_object]
3238 bpy.context.view_layer.objects.active = self.main_object
3239 except:
3240 self.report({'WARNING'}, "Specify the name of the object with retopology")
3241 return{"CANCELLED"}
3243 self.update()
3245 self.main_object_selected_verts_count = len([v for v in self.main_object.data.vertices if v.select])
3247 bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
3248 value='True, False, False')
3250 self.edges_U = bsurfaces_props.SURFSK_edges_U
3251 self.edges_V = bsurfaces_props.SURFSK_edges_V
3253 self.is_fill_faces = False
3254 self.stopping_errors = False
3255 self.last_strokes_splines_coords = []
3257 # Determine the type of the strokes
3258 self.strokes_type = get_strokes_type(context)
3260 # Check if it will be used grease pencil strokes or curves
3261 # If there are strokes to be used
3262 if self.strokes_type == "GP_STROKES" or self.strokes_type == "EXTERNAL_CURVE" or self.strokes_type == "GP_ANNOTATION":
3263 if self.strokes_type == "GP_STROKES":
3264 # Convert grease pencil strokes to curve
3265 global global_gpencil_object
3266 gp = bpy.data.objects[global_gpencil_object]
3267 self.original_curve = conver_gpencil_to_curve(self, context, gp, 'GPensil')
3268 self.using_external_curves = False
3270 elif self.strokes_type == "GP_ANNOTATION":
3271 # Convert grease pencil strokes to curve
3272 gp = bpy.context.annotation_data
3273 self.original_curve = conver_gpencil_to_curve(self, context, gp, 'Annotation')
3274 self.using_external_curves = False
3276 elif self.strokes_type == "EXTERNAL_CURVE":
3277 global global_curve_object
3278 self.original_curve = bpy.data.objects[global_curve_object]
3279 self.using_external_curves = True
3281 # Make sure there are no objects left from erroneous
3282 # executions of this operator, with the reserved names used here
3283 for o in bpy.data.objects:
3284 if o.name.find("SURFSKIO_") != -1:
3285 with bpy.context.temp_override(selected_objects=[o]):
3286 bpy.ops.object.delete()
3288 bpy.context.view_layer.objects.active = self.original_curve
3290 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3292 self.temporary_curve = bpy.context.view_layer.objects.active
3294 # Deselect all points of the curve
3295 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3296 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3297 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3299 # Delete splines with only a single isolated point
3300 for i in range(len(self.temporary_curve.data.splines)):
3301 sp = self.temporary_curve.data.splines[i]
3303 if len(sp.bezier_points) == 1:
3304 sp.bezier_points[0].select_control_point = True
3306 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3307 bpy.ops.curve.delete(type='VERT')
3308 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3310 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3311 self.temporary_curve.select_set(True)
3312 bpy.context.view_layer.objects.active = self.temporary_curve
3314 # Set a minimum number of points for crosshatch
3315 minimum_points_num = 15
3317 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3318 # Check if the number of points of each curve has at least the number of points
3319 # of minimum_points_num, which is a bit more than the face-loops limit.
3320 # If not, subdivide to reach at least that number of points
3321 for i in range(len(self.temporary_curve.data.splines)):
3322 sp = self.temporary_curve.data.splines[i]
3324 if len(sp.bezier_points) < minimum_points_num:
3325 for bp in sp.bezier_points:
3326 bp.select_control_point = True
3328 if (len(sp.bezier_points) - 1) != 0:
3329 # Formula to get the number of cuts that will make a curve
3330 # of N number of points have near to "minimum_points_num"
3331 # points, when subdividing with this number of cuts
3332 subdivide_cuts = int(
3333 (minimum_points_num - len(sp.bezier_points)) /
3334 (len(sp.bezier_points) - 1)
3335 ) + 1
3336 else:
3337 subdivide_cuts = 0
3339 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3340 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3342 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3344 # Detect if the strokes are a crosshatch and do it if it is
3345 self.crosshatch_surface_invoke(self.temporary_curve)
3347 if not self.is_crosshatch:
3348 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3349 self.temporary_curve.select_set(True)
3350 bpy.context.view_layer.objects.active = self.temporary_curve
3352 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3354 # Set a minimum number of points for rectangular surfaces
3355 minimum_points_num = 60
3357 # Check if the number of points of each curve has at least the number of points
3358 # of minimum_points_num, which is a bit more than the face-loops limit.
3359 # If not, subdivide to reach at least that number of points
3360 for i in range(len(self.temporary_curve.data.splines)):
3361 sp = self.temporary_curve.data.splines[i]
3363 if len(sp.bezier_points) < minimum_points_num:
3364 for bp in sp.bezier_points:
3365 bp.select_control_point = True
3367 if (len(sp.bezier_points) - 1) != 0:
3368 # Formula to get the number of cuts that will make a curve of
3369 # N number of points have near to "minimum_points_num" points,
3370 # when subdividing with this number of cuts
3371 subdivide_cuts = int(
3372 (minimum_points_num - len(sp.bezier_points)) /
3373 (len(sp.bezier_points) - 1)
3374 ) + 1
3375 else:
3376 subdivide_cuts = 0
3378 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3379 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3381 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3383 # Save coordinates of the actual strokes (as the "last saved splines")
3384 for sp_idx in range(len(self.temporary_curve.data.splines)):
3385 self.last_strokes_splines_coords.append([])
3386 for bp_idx in range(len(self.temporary_curve.data.splines[sp_idx].bezier_points)):
3387 coords = self.temporary_curve.matrix_world @ \
3388 self.temporary_curve.data.splines[sp_idx].bezier_points[bp_idx].co
3389 self.last_strokes_splines_coords[sp_idx].append([coords[0], coords[1], coords[2]])
3391 # Check for cyclic splines, put the first and last points in the middle of their actual positions
3392 for sp_idx in range(len(self.temporary_curve.data.splines)):
3393 if self.temporary_curve.data.splines[sp_idx].use_cyclic_u is True:
3394 first_p_co = self.last_strokes_splines_coords[sp_idx][0]
3395 last_p_co = self.last_strokes_splines_coords[sp_idx][
3396 len(self.last_strokes_splines_coords[sp_idx]) - 1
3398 target_co = [
3399 (first_p_co[0] + last_p_co[0]) / 2,
3400 (first_p_co[1] + last_p_co[1]) / 2,
3401 (first_p_co[2] + last_p_co[2]) / 2
3404 self.last_strokes_splines_coords[sp_idx][0] = target_co
3405 self.last_strokes_splines_coords[sp_idx][
3406 len(self.last_strokes_splines_coords[sp_idx]) - 1
3407 ] = target_co
3408 tuple(self.last_strokes_splines_coords)
3410 # Estimation of the average length of the segments between
3411 # each point of the grease pencil strokes.
3412 # Will be useful to determine whether a curve should be made "Cyclic"
3413 segments_lengths_sum = 0
3414 segments_count = 0
3415 random_spline = self.temporary_curve.data.splines[0].bezier_points
3416 for i in range(0, len(random_spline)):
3417 if i != 0 and len(random_spline) - 1 >= i:
3418 segments_lengths_sum += (random_spline[i - 1].co - random_spline[i].co).length
3419 segments_count += 1
3421 self.average_gp_segment_length = segments_lengths_sum / segments_count
3423 # Delete temporary strokes curve object
3424 with bpy.context.temp_override(selected_objects=[self.temporary_curve]):
3425 bpy.ops.object.delete()
3427 # Set again since "execute()" will turn it again to its initial value
3428 self.execute(context)
3430 if not self.stopping_errors:
3431 # Delete grease pencil strokes
3432 if self.strokes_type == "GP_STROKES":
3433 try:
3434 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
3435 except:
3436 pass
3438 # Delete annotation strokes
3439 elif self.strokes_type == "GP_ANNOTATION":
3440 try:
3441 bpy.context.annotation_data.layers.active.clear()
3442 except:
3443 pass
3445 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3446 with bpy.context.temp_override(selected_objects=[self.original_curve]):
3447 bpy.ops.object.delete()
3448 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3450 return {"FINISHED"}
3451 else:
3452 return{"CANCELLED"}
3454 elif self.strokes_type == "SELECTION_ALONE":
3455 self.is_fill_faces = True
3456 created_faces_count = self.fill_with_faces(self.main_object)
3458 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3460 if created_faces_count == 0:
3461 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3462 return {"CANCELLED"}
3463 else:
3464 return {"FINISHED"}
3466 if self.strokes_type == "EXTERNAL_NO_CURVE":
3467 self.report({'WARNING'}, "The secondary object is not a Curve.")
3468 return{"CANCELLED"}
3470 elif self.strokes_type == "MORE_THAN_ONE_EXTERNAL":
3471 self.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3472 return{"CANCELLED"}
3474 elif self.strokes_type == "SINGLE_GP_STROKE_NO_SELECTION" or \
3475 self.strokes_type == "SINGLE_CURVE_STROKE_NO_SELECTION":
3477 self.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
3478 return{"CANCELLED"}
3480 elif self.strokes_type == "NO_STROKES":
3481 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3482 return{"CANCELLED"}
3484 elif self.strokes_type == "CURVE_WITH_NON_BEZIER_SPLINES":
3485 self.report({'WARNING'}, "All splines must be Bezier.")
3486 return{"CANCELLED"}
3488 else:
3489 return{"CANCELLED"}
3491 # ----------------------------
3492 # Init operator
3493 class MESH_OT_SURFSK_init(Operator):
3494 bl_idname = "mesh.surfsk_init"
3495 bl_label = "Bsurfaces initialize"
3496 bl_description = "Add an empty mesh object with useful settings"
3497 bl_options = {'REGISTER', 'UNDO'}
3499 def execute(self, context):
3501 bs = bpy.context.scene.bsurfaces
3503 if bpy.ops.object.mode_set.poll():
3504 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3506 global global_shade_smooth
3507 global global_mesh_object
3508 global global_gpencil_object
3510 if bs.SURFSK_mesh == None:
3511 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3512 mesh = bpy.data.meshes.new('BSurfaceMesh')
3513 mesh_object = object_utils.object_data_add(context, mesh)
3514 mesh_object.select_set(True)
3515 bpy.context.view_layer.objects.active = mesh_object
3517 mesh_object.show_all_edges = True
3518 mesh_object.display_type = 'SOLID'
3519 mesh_object.show_wire = True
3521 global_shade_smooth = bpy.context.scene.bsurfaces.SURFSK_shade_smooth
3522 if global_shade_smooth:
3523 bpy.ops.object.shade_smooth()
3524 else:
3525 bpy.ops.object.shade_flat()
3527 color_red = [1.0, 0.0, 0.0, 0.3]
3528 material = makeMaterial("BSurfaceMesh", color_red)
3529 mesh_object.data.materials.append(material)
3530 modifier = mesh_object.modifiers.new("", 'SHRINKWRAP')
3531 if self.active_object is not None:
3532 modifier.target = self.active_object
3533 modifier.wrap_method = 'TARGET_PROJECT'
3534 modifier.wrap_mode = 'OUTSIDE_SURFACE'
3535 modifier.show_on_cage = True
3537 global_mesh_object = mesh_object.name
3538 bpy.context.scene.bsurfaces.SURFSK_mesh = bpy.data.objects[global_mesh_object]
3540 bpy.context.scene.tool_settings.snap_elements = {'FACE'}
3541 bpy.context.scene.tool_settings.use_snap = True
3542 bpy.context.scene.tool_settings.use_snap_self = False
3543 bpy.context.scene.tool_settings.use_snap_align_rotation = True
3544 bpy.context.scene.tool_settings.use_snap_project = True
3545 bpy.context.scene.tool_settings.use_snap_rotate = True
3546 bpy.context.scene.tool_settings.use_snap_scale = True
3548 bpy.context.scene.tool_settings.use_mesh_automerge = True
3549 bpy.context.scene.tool_settings.double_threshold = 0.01
3551 if context.scene.bsurfaces.SURFSK_guide == 'GPencil' and bs.SURFSK_gpencil == None:
3552 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3553 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')
3554 bpy.context.scene.tool_settings.gpencil_stroke_placement_view3d = 'SURFACE'
3555 gpencil_object = bpy.context.scene.objects[bpy.context.scene.objects[-1].name]
3556 gpencil_object.select_set(True)
3557 bpy.context.view_layer.objects.active = gpencil_object
3558 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
3559 global_gpencil_object = gpencil_object.name
3560 bpy.context.scene.bsurfaces.SURFSK_gpencil = bpy.data.objects[global_gpencil_object]
3561 gpencil_object.data.stroke_depth_order = '3D'
3562 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
3563 bpy.ops.wm.tool_set_by_id(name="builtin_brush.Draw")
3565 if context.scene.bsurfaces.SURFSK_guide == 'Annotation':
3566 bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
3567 bpy.context.scene.tool_settings.annotation_stroke_placement_view3d = 'SURFACE'
3569 def invoke(self, context, event):
3570 if bpy.context.active_object:
3571 self.active_object = bpy.context.active_object
3572 else:
3573 self.active_object = None
3575 self.execute(context)
3577 return {"FINISHED"}
3579 # ----------------------------
3580 # Add modifiers operator
3581 class MESH_OT_SURFSK_add_modifiers(Operator):
3582 bl_idname = "mesh.surfsk_add_modifiers"
3583 bl_label = "Add Mirror and others modifiers"
3584 bl_description = "Add modifiers: Mirror, Shrinkwrap, Subdivision, Solidify"
3585 bl_options = {'REGISTER', 'UNDO'}
3587 def execute(self, context):
3589 bs = bpy.context.scene.bsurfaces
3591 if bpy.ops.object.mode_set.poll():
3592 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3594 if bs.SURFSK_mesh == None:
3595 self.report({'ERROR_INVALID_INPUT'}, "Please select Mesh of BSurface or click Initialize")
3596 else:
3597 mesh_object = bs.SURFSK_mesh
3599 try:
3600 mesh_object.select_set(True)
3601 except:
3602 self.report({'ERROR_INVALID_INPUT'}, "Mesh of BSurface does not exist")
3603 return {"CANCEL"}
3605 bpy.context.view_layer.objects.active = mesh_object
3607 try:
3608 shrinkwrap = next(mod for mod in mesh_object.modifiers
3609 if mod.type == 'SHRINKWRAP')
3610 except:
3611 shrinkwrap = mesh_object.modifiers.new("", 'SHRINKWRAP')
3612 if self.active_object is not None and self.active_object != mesh_object:
3613 shrinkwrap.target = self.active_object
3614 shrinkwrap.wrap_method = 'TARGET_PROJECT'
3615 shrinkwrap.wrap_mode = 'OUTSIDE_SURFACE'
3616 shrinkwrap.show_on_cage = True
3617 shrinkwrap.offset = bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset
3619 try:
3620 mirror = next(mod for mod in mesh_object.modifiers
3621 if mod.type == 'MIRROR')
3622 except:
3623 mirror = mesh_object.modifiers.new("", 'MIRROR')
3624 mirror.use_clip = True
3626 try:
3627 _subsurf = next(mod for mod in mesh_object.modifiers
3628 if mod.type == 'SUBSURF')
3629 except:
3630 _subsurf = mesh_object.modifiers.new("", 'SUBSURF')
3632 try:
3633 solidify = next(mod for mod in mesh_object.modifiers
3634 if mod.type == 'SOLIDIFY')
3635 except:
3636 solidify = mesh_object.modifiers.new("", 'SOLIDIFY')
3637 solidify.thickness = 0.01
3639 return {"FINISHED"}
3641 def invoke(self, context, event):
3642 if bpy.context.active_object:
3643 self.active_object = bpy.context.active_object
3644 else:
3645 self.active_object = None
3647 self.execute(context)
3649 return {"FINISHED"}
3651 # ----------------------------
3652 # Edit surface operator
3653 class MESH_OT_SURFSK_edit_surface(Operator):
3654 bl_idname = "mesh.surfsk_edit_surface"
3655 bl_label = "Bsurfaces edit surface"
3656 bl_description = "Edit surface mesh"
3657 bl_options = {'REGISTER', 'UNDO'}
3659 def execute(self, context):
3660 if bpy.ops.object.mode_set.poll():
3661 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3662 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3663 bpy.context.scene.bsurfaces.SURFSK_mesh.select_set(True)
3664 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_mesh
3665 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
3666 bpy.ops.wm.tool_set_by_id(name="builtin.select")
3668 def invoke(self, context, event):
3669 try:
3670 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
3671 bpy.data.objects[global_mesh_object].select_set(True)
3672 self.main_object = bpy.data.objects[global_mesh_object]
3673 bpy.context.view_layer.objects.active = self.main_object
3674 except:
3675 self.report({'WARNING'}, "Specify the name of the object with retopology")
3676 return{"CANCELLED"}
3678 self.execute(context)
3680 return {"FINISHED"}
3682 # ----------------------------
3683 # Add strokes operator
3684 class GPENCIL_OT_SURFSK_add_strokes(Operator):
3685 bl_idname = "gpencil.surfsk_add_strokes"
3686 bl_label = "Bsurfaces add strokes"
3687 bl_description = "Add the grease pencil strokes"
3688 bl_options = {'REGISTER', 'UNDO'}
3690 def execute(self, context):
3691 if bpy.ops.object.mode_set.poll():
3692 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3693 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3695 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3696 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_gpencil
3697 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
3698 bpy.ops.wm.tool_set_by_id(name="builtin_brush.Draw")
3700 return{"FINISHED"}
3702 def invoke(self, context, event):
3703 try:
3704 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3705 except:
3706 self.report({'WARNING'}, "Specify the name of the object with strokes")
3707 return{"CANCELLED"}
3709 self.execute(context)
3711 return {"FINISHED"}
3713 # ----------------------------
3714 # Edit strokes operator
3715 class GPENCIL_OT_SURFSK_edit_strokes(Operator):
3716 bl_idname = "gpencil.surfsk_edit_strokes"
3717 bl_label = "Bsurfaces edit strokes"
3718 bl_description = "Edit the grease pencil strokes"
3719 bl_options = {'REGISTER', 'UNDO'}
3721 def execute(self, context):
3722 if bpy.ops.object.mode_set.poll():
3723 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3724 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3726 gpencil_object = bpy.context.scene.bsurfaces.SURFSK_gpencil
3728 gpencil_object.select_set(True)
3729 bpy.context.view_layer.objects.active = gpencil_object
3731 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT_GPENCIL')
3732 try:
3733 bpy.ops.gpencil.select_all(action='SELECT')
3734 except:
3735 pass
3737 def invoke(self, context, event):
3738 try:
3739 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3740 except:
3741 self.report({'WARNING'}, "Specify the name of the object with strokes")
3742 return{"CANCELLED"}
3744 self.execute(context)
3746 return {"FINISHED"}
3748 # ----------------------------
3749 # Convert annotation to curves operator
3750 class GPENCIL_OT_SURFSK_annotation_to_curves(Operator):
3751 bl_idname = "gpencil.surfsk_annotations_to_curves"
3752 bl_label = "Convert annotation to curves"
3753 bl_description = "Convert annotation to curves for editing"
3754 bl_options = {'REGISTER', 'UNDO'}
3756 def execute(self, context):
3758 if bpy.ops.object.mode_set.poll():
3759 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3761 # Convert annotation to curve
3762 curve = conver_gpencil_to_curve(self, context, None, 'Annotation')
3764 if curve != None:
3765 # Delete annotation strokes
3766 try:
3767 bpy.context.annotation_data.layers.active.clear()
3768 except:
3769 pass
3771 # Clean up curves
3772 curve.select_set(True)
3773 bpy.context.view_layer.objects.active = curve
3775 bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
3777 return {"FINISHED"}
3779 def invoke(self, context, event):
3780 try:
3781 strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
3783 _strokes_num = len(strokes)
3784 except:
3785 self.report({'WARNING'}, "Not active annotation")
3786 return{"CANCELLED"}
3788 self.execute(context)
3790 return {"FINISHED"}
3792 # ----------------------------
3793 # Convert strokes to curves operator
3794 class GPENCIL_OT_SURFSK_strokes_to_curves(Operator):
3795 bl_idname = "gpencil.surfsk_strokes_to_curves"
3796 bl_label = "Convert strokes to curves"
3797 bl_description = "Convert grease pencil strokes to curves for editing"
3798 bl_options = {'REGISTER', 'UNDO'}
3800 def execute(self, context):
3802 if bpy.ops.object.mode_set.poll():
3803 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3805 # Convert grease pencil strokes to curve
3806 gp = bpy.context.scene.bsurfaces.SURFSK_gpencil
3807 curve = conver_gpencil_to_curve(self, context, gp, 'GPensil')
3809 if curve != None:
3810 # Delete grease pencil strokes
3811 try:
3812 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
3813 except:
3814 pass
3816 # Clean up curves
3818 curve.select_set(True)
3819 bpy.context.view_layer.objects.active = curve
3821 bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
3823 return {"FINISHED"}
3825 def invoke(self, context, event):
3826 try:
3827 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3828 except:
3829 self.report({'WARNING'}, "Specify the name of the object with strokes")
3830 return{"CANCELLED"}
3832 self.execute(context)
3834 return {"FINISHED"}
3836 # ----------------------------
3837 # Add annotation
3838 class GPENCIL_OT_SURFSK_add_annotation(Operator):
3839 bl_idname = "gpencil.surfsk_add_annotation"
3840 bl_label = "Bsurfaces add annotation"
3841 bl_description = "Add annotation"
3842 bl_options = {'REGISTER', 'UNDO'}
3844 def execute(self, context):
3845 bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
3846 bpy.context.scene.tool_settings.annotation_stroke_placement_view3d = 'SURFACE'
3848 return{"FINISHED"}
3850 def invoke(self, context, event):
3852 self.execute(context)
3854 return {"FINISHED"}
3857 # ----------------------------
3858 # Edit curve operator
3859 class CURVE_OT_SURFSK_edit_curve(Operator):
3860 bl_idname = "curve.surfsk_edit_curve"
3861 bl_label = "Bsurfaces edit curve"
3862 bl_description = "Edit curve"
3863 bl_options = {'REGISTER', 'UNDO'}
3865 def execute(self, context):
3866 if bpy.ops.object.mode_set.poll():
3867 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3868 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3869 bpy.context.scene.bsurfaces.SURFSK_curve.select_set(True)
3870 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_curve
3871 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
3873 def invoke(self, context, event):
3874 try:
3875 bpy.context.scene.bsurfaces.SURFSK_curve.select_set(True)
3876 except:
3877 self.report({'WARNING'}, "Specify the name of the object with curve")
3878 return{"CANCELLED"}
3880 self.execute(context)
3882 return {"FINISHED"}
3884 # ----------------------------
3885 # Reorder splines
3886 class CURVE_OT_SURFSK_reorder_splines(Operator):
3887 bl_idname = "curve.surfsk_reorder_splines"
3888 bl_label = "Bsurfaces reorder splines"
3889 bl_description = "Defines the order of the splines by using grease pencil strokes"
3890 bl_options = {'REGISTER', 'UNDO'}
3892 def execute(self, context):
3893 objects_to_delete = []
3894 # Convert grease pencil strokes to curve.
3895 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3896 bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
3897 for ob in bpy.context.selected_objects:
3898 if ob != bpy.context.view_layer.objects.active and ob.name.startswith("GP_Layer"):
3899 GP_strokes_curve = ob
3901 # GP_strokes_curve = bpy.context.object
3902 objects_to_delete.append(GP_strokes_curve)
3904 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3905 GP_strokes_curve.select_set(True)
3906 bpy.context.view_layer.objects.active = GP_strokes_curve
3908 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3909 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
3910 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=100)
3911 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3913 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3914 GP_strokes_mesh = bpy.context.object
3915 objects_to_delete.append(GP_strokes_mesh)
3917 GP_strokes_mesh.data.resolution_u = 1
3918 bpy.ops.object.convert(target='MESH', keep_original=False)
3920 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3921 self.main_curve.select_set(True)
3922 bpy.context.view_layer.objects.active = self.main_curve
3924 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3925 curves_duplicate_1 = bpy.context.object
3926 objects_to_delete.append(curves_duplicate_1)
3928 minimum_points_num = 500
3930 # Some iterations since the subdivision operator
3931 # has a limit of 100 subdivisions per iteration
3932 for x in range(round(minimum_points_num / 100)):
3933 # Check if the number of points of each curve has at least the number of points
3934 # of minimum_points_num. If not, subdivide to reach at least that number of points
3935 for i in range(len(curves_duplicate_1.data.splines)):
3936 sp = curves_duplicate_1.data.splines[i]
3938 if len(sp.bezier_points) < minimum_points_num:
3939 for bp in sp.bezier_points:
3940 bp.select_control_point = True
3942 if (len(sp.bezier_points) - 1) != 0:
3943 # Formula to get the number of cuts that will make a curve of N
3944 # number of points have near to "minimum_points_num" points,
3945 # when subdividing with this number of cuts
3946 subdivide_cuts = int(
3947 (minimum_points_num - len(sp.bezier_points)) /
3948 (len(sp.bezier_points) - 1)
3949 ) + 1
3950 else:
3951 subdivide_cuts = 0
3953 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3954 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3955 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3956 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3958 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3959 curves_duplicate_2 = bpy.context.object
3960 objects_to_delete.append(curves_duplicate_2)
3962 # Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target
3963 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3964 curves_duplicate_2.select_set(True)
3965 bpy.context.view_layer.objects.active = curves_duplicate_2
3967 shrinkwrap = curves_duplicate_2.modifiers.new("", 'SHRINKWRAP')
3968 shrinkwrap.wrap_method = "NEAREST_VERTEX"
3969 shrinkwrap.target = GP_strokes_mesh
3970 bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', modifier=shrinkwrap.name)
3972 # Get the distance of each vert from its original position to its position with Shrinkwrap
3973 nearest_points_coords = {}
3974 for st_idx in range(len(curves_duplicate_1.data.splines)):
3975 for bp_idx in range(len(curves_duplicate_1.data.splines[st_idx].bezier_points)):
3976 bp_1_co = curves_duplicate_1.matrix_world @ \
3977 curves_duplicate_1.data.splines[st_idx].bezier_points[bp_idx].co
3979 bp_2_co = curves_duplicate_2.matrix_world @ \
3980 curves_duplicate_2.data.splines[st_idx].bezier_points[bp_idx].co
3982 if bp_idx == 0:
3983 shortest_dist = (bp_1_co - bp_2_co).length
3984 nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
3985 "%.4f" % bp_2_co[1],
3986 "%.4f" % bp_2_co[2])
3988 dist = (bp_1_co - bp_2_co).length
3990 if dist < shortest_dist:
3991 nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
3992 "%.4f" % bp_2_co[1],
3993 "%.4f" % bp_2_co[2])
3994 shortest_dist = dist
3996 # Get all coords of GP strokes points, for comparison
3997 GP_strokes_coords = []
3998 for st_idx in range(len(GP_strokes_curve.data.splines)):
3999 GP_strokes_coords.append(
4000 [("%.4f" % x if "%.4f" % x != "-0.00" else "0.00",
4001 "%.4f" % y if "%.4f" % y != "-0.00" else "0.00",
4002 "%.4f" % z if "%.4f" % z != "-0.00" else "0.00") for
4003 x, y, z in [bp.co for bp in GP_strokes_curve.data.splines[st_idx].bezier_points]]
4006 # Check the point of the GP strokes with the same coords as
4007 # the nearest points of the curves (with shrinkwrap)
4009 # Dictionary with GP stroke index as index, and a list as value.
4010 # The list has as index the point index of the GP stroke
4011 # nearest to the spline, and as value the spline index
4012 GP_connection_points = {}
4013 for gp_st_idx in range(len(GP_strokes_coords)):
4014 GPvert_spline_relationship = {}
4016 for splines_st_idx in range(len(nearest_points_coords)):
4017 if nearest_points_coords[splines_st_idx] in GP_strokes_coords[gp_st_idx]:
4018 GPvert_spline_relationship[
4019 GP_strokes_coords[gp_st_idx].index(nearest_points_coords[splines_st_idx])
4020 ] = splines_st_idx
4022 GP_connection_points[gp_st_idx] = GPvert_spline_relationship
4024 # Get the splines new order
4025 splines_new_order = []
4026 for i in GP_connection_points:
4027 dict_keys = sorted(GP_connection_points[i].keys()) # Sort dictionaries by key
4029 for k in dict_keys:
4030 splines_new_order.append(GP_connection_points[i][k])
4032 # Reorder
4033 curve_original_name = self.main_curve.name
4035 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4036 self.main_curve.select_set(True)
4037 bpy.context.view_layer.objects.active = self.main_curve
4039 self.main_curve.name = "SURFSKIO_CRV_ORD"
4041 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4042 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4043 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4045 for _sp_idx in range(len(self.main_curve.data.splines)):
4046 self.main_curve.data.splines[0].bezier_points[0].select_control_point = True
4048 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4049 bpy.ops.curve.separate('EXEC_REGION_WIN')
4050 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4052 # Get the names of the separated splines objects in the original order
4053 splines_unordered = {}
4054 for o in bpy.data.objects:
4055 if o.name.find("SURFSKIO_CRV_ORD") != -1:
4056 spline_order_string = o.name.partition(".")[2]
4058 if spline_order_string != "" and int(spline_order_string) > 0:
4059 spline_order_index = int(spline_order_string) - 1
4060 splines_unordered[spline_order_index] = o.name
4062 # Join all splines objects in final order
4063 for order_idx in splines_new_order:
4064 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4065 bpy.data.objects[splines_unordered[order_idx]].select_set(True)
4066 bpy.data.objects["SURFSKIO_CRV_ORD"].select_set(True)
4067 bpy.context.view_layer.objects.active = bpy.data.objects["SURFSKIO_CRV_ORD"]
4069 bpy.ops.object.join('INVOKE_REGION_WIN')
4071 # Go back to the original name of the curves object.
4072 bpy.context.object.name = curve_original_name
4074 # Delete all unused objects
4075 with bpy.context.temp_override(selected_objects=objects_to_delete):
4076 bpy.ops.object.delete()
4078 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4079 bpy.data.objects[curve_original_name].select_set(True)
4080 bpy.context.view_layer.objects.active = bpy.data.objects[curve_original_name]
4082 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4083 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4085 try:
4086 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
4087 except:
4088 pass
4091 return {"FINISHED"}
4093 def invoke(self, context, event):
4094 self.main_curve = bpy.context.object
4095 there_are_GP_strokes = False
4097 try:
4098 # Get the active grease pencil layer
4099 strokes_num = len(self.main_curve.grease_pencil.layers.active.active_frame.strokes)
4101 if strokes_num > 0:
4102 there_are_GP_strokes = True
4103 except:
4104 pass
4106 if there_are_GP_strokes:
4107 self.execute(context)
4108 self.report({'INFO'}, "Splines have been reordered")
4109 else:
4110 self.report({'WARNING'}, "Draw grease pencil strokes to connect splines")
4112 return {"FINISHED"}
4114 # ----------------------------
4115 # Set first points operator
4116 class CURVE_OT_SURFSK_first_points(Operator):
4117 bl_idname = "curve.surfsk_first_points"
4118 bl_label = "Bsurfaces set first points"
4119 bl_description = "Set the selected points as the first point of each spline"
4120 bl_options = {'REGISTER', 'UNDO'}
4122 def execute(self, context):
4123 splines_to_invert = []
4125 # Check non-cyclic splines to invert
4126 for i in range(len(self.main_curve.data.splines)):
4127 b_points = self.main_curve.data.splines[i].bezier_points
4129 if i not in self.cyclic_splines: # Only for non-cyclic splines
4130 if b_points[len(b_points) - 1].select_control_point:
4131 splines_to_invert.append(i)
4133 # Reorder points of cyclic splines, and set all handles to "Automatic"
4135 # Check first selected point
4136 cyclic_splines_new_first_pt = {}
4137 for i in self.cyclic_splines:
4138 sp = self.main_curve.data.splines[i]
4140 for t in range(len(sp.bezier_points)):
4141 bp = sp.bezier_points[t]
4142 if bp.select_control_point or bp.select_right_handle or bp.select_left_handle:
4143 cyclic_splines_new_first_pt[i] = t
4144 break # To take only one if there are more
4146 # Reorder
4147 for spline_idx in cyclic_splines_new_first_pt:
4148 sp = self.main_curve.data.splines[spline_idx]
4150 spline_old_coords = []
4151 for bp_old in sp.bezier_points:
4152 coords = (bp_old.co[0], bp_old.co[1], bp_old.co[2])
4154 left_handle_type = str(bp_old.handle_left_type)
4155 left_handle_length = float(bp_old.handle_left.length)
4156 left_handle_xyz = (
4157 float(bp_old.handle_left.x),
4158 float(bp_old.handle_left.y),
4159 float(bp_old.handle_left.z)
4161 right_handle_type = str(bp_old.handle_right_type)
4162 right_handle_length = float(bp_old.handle_right.length)
4163 right_handle_xyz = (
4164 float(bp_old.handle_right.x),
4165 float(bp_old.handle_right.y),
4166 float(bp_old.handle_right.z)
4168 spline_old_coords.append(
4169 [coords, left_handle_type,
4170 right_handle_type, left_handle_length,
4171 right_handle_length, left_handle_xyz,
4172 right_handle_xyz]
4175 for t in range(len(sp.bezier_points)):
4176 bp = sp.bezier_points
4178 if t + cyclic_splines_new_first_pt[spline_idx] + 1 <= len(bp) - 1:
4179 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1
4180 else:
4181 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1 - len(bp)
4183 bp[t].co = Vector(spline_old_coords[new_index][0])
4185 bp[t].handle_left.length = spline_old_coords[new_index][3]
4186 bp[t].handle_right.length = spline_old_coords[new_index][4]
4188 bp[t].handle_left_type = "FREE"
4189 bp[t].handle_right_type = "FREE"
4191 bp[t].handle_left.x = spline_old_coords[new_index][5][0]
4192 bp[t].handle_left.y = spline_old_coords[new_index][5][1]
4193 bp[t].handle_left.z = spline_old_coords[new_index][5][2]
4195 bp[t].handle_right.x = spline_old_coords[new_index][6][0]
4196 bp[t].handle_right.y = spline_old_coords[new_index][6][1]
4197 bp[t].handle_right.z = spline_old_coords[new_index][6][2]
4199 bp[t].handle_left_type = spline_old_coords[new_index][1]
4200 bp[t].handle_right_type = spline_old_coords[new_index][2]
4202 # Invert the non-cyclic splines designated above
4203 for i in range(len(splines_to_invert)):
4204 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4206 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4207 self.main_curve.data.splines[splines_to_invert[i]].bezier_points[0].select_control_point = True
4208 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4210 bpy.ops.curve.switch_direction()
4212 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4214 # Keep selected the first vert of each spline
4215 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4216 for i in range(len(self.main_curve.data.splines)):
4217 if not self.main_curve.data.splines[i].use_cyclic_u:
4218 bp = self.main_curve.data.splines[i].bezier_points[0]
4219 else:
4220 bp = self.main_curve.data.splines[i].bezier_points[
4221 len(self.main_curve.data.splines[i].bezier_points) - 1
4224 bp.select_control_point = True
4225 bp.select_right_handle = True
4226 bp.select_left_handle = True
4228 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4230 return {'FINISHED'}
4232 def invoke(self, context, event):
4233 self.main_curve = bpy.context.object
4235 # Check if all curves are Bezier, and detect which ones are cyclic
4236 self.cyclic_splines = []
4237 for i in range(len(self.main_curve.data.splines)):
4238 if self.main_curve.data.splines[i].type != "BEZIER":
4239 self.report({'WARNING'}, "All splines must be Bezier type")
4241 return {'CANCELLED'}
4242 else:
4243 if self.main_curve.data.splines[i].use_cyclic_u:
4244 self.cyclic_splines.append(i)
4246 self.execute(context)
4247 self.report({'INFO'}, "First points have been set")
4249 return {'FINISHED'}
4252 # Add-ons Preferences Update Panel
4254 # Define Panel classes for updating
4255 panels = (
4256 VIEW3D_PT_tools_SURFSK_mesh,
4257 VIEW3D_PT_tools_SURFSK_curve
4261 def conver_gpencil_to_curve(self, context, pencil, type):
4262 newCurve = bpy.data.curves.new(type + '_curve', type='CURVE')
4263 newCurve.dimensions = '3D'
4264 CurveObject = object_utils.object_data_add(context, newCurve)
4265 error = False
4267 if type == 'GPensil':
4268 try:
4269 strokes = pencil.data.layers.active.active_frame.strokes
4270 except:
4271 error = True
4272 CurveObject.location = pencil.location
4273 CurveObject.rotation_euler = pencil.rotation_euler
4274 CurveObject.scale = pencil.scale
4275 elif type == 'Annotation':
4276 try:
4277 strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
4278 except:
4279 error = True
4280 CurveObject.location = (0.0, 0.0, 0.0)
4281 CurveObject.rotation_euler = (0.0, 0.0, 0.0)
4282 CurveObject.scale = (1.0, 1.0, 1.0)
4284 if not error:
4285 for i, _stroke in enumerate(strokes):
4286 stroke_points = strokes[i].points
4287 data_list = [ (point.co.x, point.co.y, point.co.z)
4288 for point in stroke_points ]
4289 points_to_add = len(data_list)-1
4291 flat_list = []
4292 for point in data_list:
4293 flat_list.extend(point)
4295 spline = newCurve.splines.new(type='BEZIER')
4296 spline.bezier_points.add(points_to_add)
4297 spline.bezier_points.foreach_set("co", flat_list)
4299 for point in spline.bezier_points:
4300 point.handle_left_type="AUTO"
4301 point.handle_right_type="AUTO"
4303 return CurveObject
4304 else:
4305 return None
4308 def update_panel(self, context):
4309 message = "Bsurfaces GPL Edition: Updating Panel locations has failed"
4310 try:
4311 for panel in panels:
4312 if "bl_rna" in panel.__dict__:
4313 bpy.utils.unregister_class(panel)
4315 for panel in panels:
4316 category = context.preferences.addons[__name__].preferences.category
4317 if category != 'Tool':
4318 panel.bl_category = context.preferences.addons[__name__].preferences.category
4319 else:
4320 context.preferences.addons[__name__].preferences.category = 'Edit'
4321 panel.bl_category = 'Edit'
4322 raise ValueError("You can not install add-ons in the Tool panel")
4323 bpy.utils.register_class(panel)
4325 except Exception as e:
4326 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
4327 pass
4329 def makeMaterial(name, diffuse):
4331 if name in bpy.data.materials:
4332 material = bpy.data.materials[name]
4333 material.diffuse_color = diffuse
4334 else:
4335 material = bpy.data.materials.new(name)
4336 material.diffuse_color = diffuse
4338 return material
4340 def update_mesh(self, context):
4341 try:
4342 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4343 bpy.ops.object.select_all(action='DESELECT')
4344 bpy.context.view_layer.update()
4345 global global_mesh_object
4346 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
4347 bpy.data.objects[global_mesh_object].select_set(True)
4348 bpy.context.view_layer.objects.active = bpy.data.objects[global_mesh_object]
4349 except:
4350 print("Select mesh object")
4352 def update_gpencil(self, context):
4353 try:
4354 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4355 bpy.ops.object.select_all(action='DESELECT')
4356 bpy.context.view_layer.update()
4357 global global_gpencil_object
4358 global_gpencil_object = bpy.context.scene.bsurfaces.SURFSK_gpencil.name
4359 bpy.data.objects[global_gpencil_object].select_set(True)
4360 bpy.context.view_layer.objects.active = bpy.data.objects[global_gpencil_object]
4361 except:
4362 print("Select gpencil object")
4364 def update_curve(self, context):
4365 try:
4366 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4367 bpy.ops.object.select_all(action='DESELECT')
4368 bpy.context.view_layer.update()
4369 global global_curve_object
4370 global_curve_object = bpy.context.scene.bsurfaces.SURFSK_curve.name
4371 bpy.data.objects[global_curve_object].select_set(True)
4372 bpy.context.view_layer.objects.active = bpy.data.objects[global_curve_object]
4373 except:
4374 print("Select curve object")
4376 def update_shade_smooth(self, context):
4377 try:
4378 global global_shade_smooth
4379 global_shade_smooth = bpy.context.scene.bsurfaces.SURFSK_shade_smooth
4381 contex_mode = bpy.context.mode
4383 if bpy.ops.object.mode_set.poll():
4384 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4386 bpy.ops.object.select_all(action='DESELECT')
4387 global global_mesh_object
4388 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
4389 bpy.data.objects[global_mesh_object].select_set(True)
4391 if global_shade_smooth:
4392 bpy.ops.object.shade_smooth()
4393 else:
4394 bpy.ops.object.shade_flat()
4396 if contex_mode == "EDIT_MESH":
4397 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4399 except:
4400 print("Select mesh object")
4403 class BsurfPreferences(AddonPreferences):
4404 # this must match the addon name, use '__package__'
4405 # when defining this in a submodule of a python package.
4406 bl_idname = __name__
4408 category: StringProperty(
4409 name="Tab Category",
4410 description="Choose a name for the category of the panel",
4411 default="Edit",
4412 update=update_panel
4415 def draw(self, context):
4416 layout = self.layout
4418 row = layout.row()
4419 col = row.column()
4420 col.label(text="Tab Category:")
4421 col.prop(self, "category", text="")
4423 # Properties
4424 class BsurfacesProps(PropertyGroup):
4425 SURFSK_guide: EnumProperty(
4426 name="Guide:",
4427 items=[
4428 ('Annotation', 'Annotation', 'Annotation'),
4429 ('GPencil', 'GPencil', 'GPencil'),
4430 ('Curve', 'Curve', 'Curve')
4432 default="Annotation"
4434 SURFSK_edges_U: IntProperty(
4435 name="Cross",
4436 description="Number of face-loops crossing the strokes",
4437 default=5,
4438 min=1,
4439 max=200
4441 SURFSK_edges_V: IntProperty(
4442 name="Follow",
4443 description="Number of face-loops following the strokes",
4444 default=1,
4445 min=1,
4446 max=200
4448 SURFSK_cyclic_cross: BoolProperty(
4449 name="Cyclic Cross",
4450 description="Make cyclic the face-loops crossing the strokes",
4451 default=False
4453 SURFSK_cyclic_follow: BoolProperty(
4454 name="Cyclic Follow",
4455 description="Make cyclic the face-loops following the strokes",
4456 default=False
4458 SURFSK_keep_strokes: BoolProperty(
4459 name="Keep strokes",
4460 description="Keeps the sketched strokes or curves after adding the surface",
4461 default=False
4463 SURFSK_automatic_join: BoolProperty(
4464 name="Automatic join",
4465 description="Join automatically vertices of either surfaces "
4466 "generated by crosshatching, or from the borders of closed shapes",
4467 default=True
4469 SURFSK_loops_on_strokes: BoolProperty(
4470 name="Loops on strokes",
4471 description="Make the loops match the paths of the strokes",
4472 default=True
4474 SURFSK_precision: IntProperty(
4475 name="Precision",
4476 description="Precision level of the surface calculation",
4477 default=2,
4478 min=1,
4479 max=100
4481 SURFSK_mesh: PointerProperty(
4482 name="Mesh of BSurface",
4483 type=bpy.types.Object,
4484 description="Mesh of BSurface",
4485 update=update_mesh,
4487 SURFSK_gpencil: PointerProperty(
4488 name="GreasePencil object",
4489 type=bpy.types.Object,
4490 description="GreasePencil object",
4491 update=update_gpencil,
4493 SURFSK_curve: PointerProperty(
4494 name="Curve object",
4495 type=bpy.types.Object,
4496 description="Curve object",
4497 update=update_curve,
4499 SURFSK_shade_smooth: BoolProperty(
4500 name="Shade smooth",
4501 description="Render and display faces smooth, using interpolated Vertex Normals",
4502 default=False,
4503 update=update_shade_smooth,
4506 classes = (
4507 MESH_OT_SURFSK_init,
4508 MESH_OT_SURFSK_add_modifiers,
4509 MESH_OT_SURFSK_add_surface,
4510 MESH_OT_SURFSK_edit_surface,
4511 GPENCIL_OT_SURFSK_add_strokes,
4512 GPENCIL_OT_SURFSK_edit_strokes,
4513 GPENCIL_OT_SURFSK_strokes_to_curves,
4514 GPENCIL_OT_SURFSK_annotation_to_curves,
4515 GPENCIL_OT_SURFSK_add_annotation,
4516 CURVE_OT_SURFSK_edit_curve,
4517 CURVE_OT_SURFSK_reorder_splines,
4518 CURVE_OT_SURFSK_first_points,
4519 BsurfPreferences,
4520 BsurfacesProps
4523 def register():
4524 for cls in classes:
4525 bpy.utils.register_class(cls)
4527 for panel in panels:
4528 bpy.utils.register_class(panel)
4530 bpy.types.Scene.bsurfaces = PointerProperty(type=BsurfacesProps)
4531 update_panel(None, bpy.context)
4533 def unregister():
4534 for panel in panels:
4535 bpy.utils.unregister_class(panel)
4537 for cls in classes:
4538 bpy.utils.unregister_class(cls)
4540 del bpy.types.Scene.bsurfaces
4542 if __name__ == "__main__":
4543 register()