Spacebar menu: remove references to removed operators
[blender-addons.git] / mesh_bsurfaces.py
blob753d202347ef9ccd4cf07efac0d7437c3f1ecaa0
1 # SPDX-FileCopyrightText: 2010-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
6 bl_info = {
7 "name": "Bsurfaces GPL Edition",
8 "author": "Eclectiel, Vladimir Spivak (cwolf3d)",
9 "version": (1, 8, 1),
10 "blender": (2, 80, 0),
11 "location": "View3D EditMode > Sidebar > Edit Tab",
12 "description": "Modeling and retopology tool",
13 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/bsurfaces.html",
14 "category": "Mesh",
18 import bpy
19 import bmesh
20 from bpy_extras import object_utils
22 import operator
23 from mathutils import Matrix, Vector
24 from mathutils.geometry import (
25 intersect_line_line,
26 intersect_point_line,
28 from math import (
29 degrees,
30 pi,
31 sqrt,
33 from bpy.props import (
34 BoolProperty,
35 FloatProperty,
36 IntProperty,
37 StringProperty,
38 PointerProperty,
39 EnumProperty,
40 FloatVectorProperty,
42 from bpy.types import (
43 Operator,
44 Panel,
45 PropertyGroup,
46 AddonPreferences,
49 # ----------------------------
50 # GLOBAL
51 global_shade_smooth = False
52 global_mesh_object = ""
53 global_gpencil_object = ""
54 global_curve_object = ""
56 # ----------------------------
57 # Panels
58 class VIEW3D_PT_tools_SURFSK_mesh(Panel):
59 bl_space_type = 'VIEW_3D'
60 bl_region_type = 'UI'
61 bl_category = 'Edit'
62 bl_label = "Bsurfaces"
64 def draw(self, context):
65 layout = self.layout
66 bs = context.scene.bsurfaces
68 col = layout.column(align=True)
69 row = layout.row()
70 row.separator()
71 col.operator("mesh.surfsk_init", text="Initialize (Add BSurface mesh)")
72 col.operator("mesh.surfsk_add_modifiers", text="Add Mirror and others modifiers")
74 col.label(text="Mesh of BSurface:")
75 col.prop(bs, "SURFSK_mesh", text="")
76 if bs.SURFSK_mesh != None:
77 try: mesh_object = bs.SURFSK_mesh
78 except: pass
79 try: col.prop(mesh_object.data.materials[0], "diffuse_color")
80 except: pass
81 try:
82 shrinkwrap = next(mod for mod in mesh_object.modifiers
83 if mod.type == 'SHRINKWRAP')
84 col.prop(shrinkwrap, "offset")
85 except:
86 pass
87 try: col.prop(mesh_object, "show_in_front")
88 except: pass
89 try: col.prop(bs, "SURFSK_shade_smooth")
90 except: pass
91 try: col.prop(mesh_object, "show_wire")
92 except: pass
94 col.label(text="Guide strokes:")
95 col.row().prop(bs, "SURFSK_guide", expand=True)
96 if bs.SURFSK_guide == 'GPencil':
97 col.prop(bs, "SURFSK_gpencil", text="")
98 col.separator()
99 if bs.SURFSK_guide == 'Curve':
100 col.prop(bs, "SURFSK_curve", text="")
101 col.separator()
103 col.separator()
104 col.operator("mesh.surfsk_add_surface", text="Add Surface")
105 col.operator("mesh.surfsk_edit_surface", text="Edit Surface")
107 col.separator()
108 if bs.SURFSK_guide == 'GPencil':
109 col.operator("gpencil.surfsk_add_strokes", text="Add Strokes")
110 col.operator("gpencil.surfsk_edit_strokes", text="Edit Strokes")
111 col.separator()
112 col.operator("gpencil.surfsk_strokes_to_curves", text="Strokes to curves")
114 if bs.SURFSK_guide == 'Annotation':
115 col.operator("gpencil.surfsk_add_annotation", text="Add Annotation")
116 col.separator()
117 col.operator("gpencil.surfsk_annotations_to_curves", text="Annotation to curves")
119 if bs.SURFSK_guide == 'Curve':
120 col.operator("curve.surfsk_edit_curve", text="Edit curve")
122 col.separator()
123 col.label(text="Initial settings:")
124 col.prop(bs, "SURFSK_edges_U")
125 col.prop(bs, "SURFSK_edges_V")
126 col.prop(bs, "SURFSK_cyclic_cross")
127 col.prop(bs, "SURFSK_cyclic_follow")
128 col.prop(bs, "SURFSK_loops_on_strokes")
129 col.prop(bs, "SURFSK_automatic_join")
130 col.prop(bs, "SURFSK_keep_strokes")
132 class VIEW3D_PT_tools_SURFSK_curve(Panel):
133 bl_space_type = 'VIEW_3D'
134 bl_region_type = 'UI'
135 bl_context = "curve_edit"
136 bl_category = 'Edit'
137 bl_label = "Bsurfaces"
139 @classmethod
140 def poll(cls, context):
141 return context.active_object
143 def draw(self, context):
144 layout = self.layout
146 col = layout.column(align=True)
147 row = layout.row()
148 row.separator()
149 col.operator("curve.surfsk_first_points", text="Set First Points")
150 col.operator("curve.switch_direction", text="Switch Direction")
151 col.operator("curve.surfsk_reorder_splines", text="Reorder Splines")
154 # ----------------------------
155 # Returns the type of strokes used
156 def get_strokes_type(context):
157 strokes_type = "NO_STROKES"
158 strokes_num = 0
160 # Check if they are annotation
161 if context.scene.bsurfaces.SURFSK_guide == 'Annotation':
162 try:
163 strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
165 strokes_num = len(strokes)
167 if strokes_num > 0:
168 strokes_type = "GP_ANNOTATION"
169 except:
170 strokes_type = "NO_STROKES"
172 # Check if they are grease pencil
173 if context.scene.bsurfaces.SURFSK_guide == 'GPencil':
174 try:
175 global global_gpencil_object
176 gpencil = bpy.data.objects[global_gpencil_object]
177 strokes = gpencil.data.layers.active.active_frame.strokes
179 strokes_num = len(strokes)
181 if strokes_num > 0:
182 strokes_type = "GP_STROKES"
183 except:
184 strokes_type = "NO_STROKES"
186 # Check if they are curves, if there aren't grease pencil strokes
187 if context.scene.bsurfaces.SURFSK_guide == 'Curve':
188 try:
189 global global_curve_object
190 ob = bpy.data.objects[global_curve_object]
191 if ob.type == "CURVE":
192 strokes_type = "EXTERNAL_CURVE"
193 strokes_num = len(ob.data.splines)
195 # Check if there is any non-bezier spline
196 for i in range(len(ob.data.splines)):
197 if ob.data.splines[i].type != "BEZIER":
198 strokes_type = "CURVE_WITH_NON_BEZIER_SPLINES"
199 break
201 else:
202 strokes_type = "EXTERNAL_NO_CURVE"
203 except:
204 strokes_type = "NO_STROKES"
206 # Check if they are mesh
207 try:
208 global global_mesh_object
209 self.main_object = bpy.data.objects[global_mesh_object]
210 total_vert_sel = len([v for v in self.main_object.data.vertices if v.select])
212 # Check if there is a single stroke without any selection in the object
213 if strokes_num == 1 and total_vert_sel == 0:
214 if strokes_type == "EXTERNAL_CURVE":
215 strokes_type = "SINGLE_CURVE_STROKE_NO_SELECTION"
216 elif strokes_type == "GP_STROKES":
217 strokes_type = "SINGLE_GP_STROKE_NO_SELECTION"
219 if strokes_num == 0 and total_vert_sel > 0:
220 strokes_type = "SELECTION_ALONE"
221 except:
222 pass
224 return strokes_type
226 # ----------------------------
227 # Surface generator operator
228 class MESH_OT_SURFSK_add_surface(Operator):
229 bl_idname = "mesh.surfsk_add_surface"
230 bl_label = "Bsurfaces add surface"
231 bl_description = "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
232 bl_options = {'REGISTER', 'UNDO'}
234 is_crosshatch: BoolProperty(
235 default=False
237 is_fill_faces: BoolProperty(
238 default=False
240 selection_U_exists: BoolProperty(
241 default=False
243 selection_V_exists: BoolProperty(
244 default=False
246 selection_U2_exists: BoolProperty(
247 default=False
249 selection_V2_exists: BoolProperty(
250 default=False
252 selection_V_is_closed: BoolProperty(
253 default=False
255 selection_U_is_closed: BoolProperty(
256 default=False
258 selection_V2_is_closed: BoolProperty(
259 default=False
261 selection_U2_is_closed: BoolProperty(
262 default=False
265 edges_U: IntProperty(
266 name="Cross",
267 description="Number of face-loops crossing the strokes",
268 default=1,
269 min=1,
270 max=200
272 edges_V: IntProperty(
273 name="Follow",
274 description="Number of face-loops following the strokes",
275 default=1,
276 min=1,
277 max=200
279 cyclic_cross: BoolProperty(
280 name="Cyclic Cross",
281 description="Make cyclic the face-loops crossing the strokes",
282 default=False
284 cyclic_follow: BoolProperty(
285 name="Cyclic Follow",
286 description="Make cyclic the face-loops following the strokes",
287 default=False
289 loops_on_strokes: BoolProperty(
290 name="Loops on strokes",
291 description="Make the loops match the paths of the strokes",
292 default=False
294 automatic_join: BoolProperty(
295 name="Automatic join",
296 description="Join automatically vertices of either surfaces generated "
297 "by crosshatching, or from the borders of closed shapes",
298 default=False
300 join_stretch_factor: FloatProperty(
301 name="Stretch",
302 description="Amount of stretching or shrinking allowed for "
303 "edges when joining vertices automatically",
304 default=1,
305 min=0,
306 max=3,
307 subtype='FACTOR'
309 keep_strokes: BoolProperty(
310 name="Keep strokes",
311 description="Keeps the sketched strokes or curves after adding the surface",
312 default=False
314 strokes_type: StringProperty()
315 initial_global_undo_state: BoolProperty()
318 def draw(self, context):
319 layout = self.layout
320 col = layout.column(align=True)
321 row = layout.row()
323 if not self.is_fill_faces:
324 row.separator()
325 if not self.is_crosshatch:
326 if not self.selection_U_exists:
327 col.prop(self, "edges_U")
328 row.separator()
330 if not self.selection_V_exists:
331 col.prop(self, "edges_V")
332 row.separator()
334 row.separator()
336 if not self.selection_U_exists:
337 if not (
338 (self.selection_V_exists and not self.selection_V_is_closed) or
339 (self.selection_V2_exists and not self.selection_V2_is_closed)
341 col.prop(self, "cyclic_cross")
343 if not self.selection_V_exists:
344 if not (
345 (self.selection_U_exists and not self.selection_U_is_closed) or
346 (self.selection_U2_exists and not self.selection_U2_is_closed)
348 col.prop(self, "cyclic_follow")
350 col.prop(self, "loops_on_strokes")
352 col.prop(self, "automatic_join")
354 if self.automatic_join:
355 row.separator()
356 col.separator()
357 row.separator()
358 col.prop(self, "join_stretch_factor")
360 col.prop(self, "keep_strokes")
362 # Get an ordered list of a chain of vertices
363 def get_ordered_verts(self, ob, all_selected_edges_idx, all_selected_verts_idx,
364 first_vert_idx, middle_vertex_idx, closing_vert_idx):
365 # Order selected vertices.
366 verts_ordered = []
367 if closing_vert_idx is not None:
368 verts_ordered.append(ob.data.vertices[closing_vert_idx])
370 verts_ordered.append(ob.data.vertices[first_vert_idx])
371 prev_v = first_vert_idx
372 prev_ed = None
373 finish_while = False
374 while True:
375 edges_non_matched = 0
376 for i in all_selected_edges_idx:
377 if ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[0] == prev_v and \
378 ob.data.edges[i].vertices[1] in all_selected_verts_idx:
380 verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[1]])
381 prev_v = ob.data.edges[i].vertices[1]
382 prev_ed = ob.data.edges[i]
383 elif ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[1] == prev_v and \
384 ob.data.edges[i].vertices[0] in all_selected_verts_idx:
386 verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[0]])
387 prev_v = ob.data.edges[i].vertices[0]
388 prev_ed = ob.data.edges[i]
389 else:
390 edges_non_matched += 1
392 if edges_non_matched == len(all_selected_edges_idx):
393 finish_while = True
395 if finish_while:
396 break
398 if closing_vert_idx is not None:
399 verts_ordered.append(ob.data.vertices[closing_vert_idx])
401 if middle_vertex_idx is not None:
402 verts_ordered.append(ob.data.vertices[middle_vertex_idx])
403 verts_ordered.reverse()
405 return tuple(verts_ordered)
407 # Calculates length of a chain of points.
408 def get_chain_length(self, object, verts_ordered):
409 matrix = object.matrix_world
411 edges_lengths = []
412 edges_lengths_sum = 0
413 for i in range(0, len(verts_ordered)):
414 if i == 0:
415 prev_v_co = matrix @ verts_ordered[i].co
416 else:
417 v_co = matrix @ verts_ordered[i].co
419 v_difs = [prev_v_co[0] - v_co[0], prev_v_co[1] - v_co[1], prev_v_co[2] - v_co[2]]
420 edge_length = abs(sqrt(v_difs[0] * v_difs[0] + v_difs[1] * v_difs[1] + v_difs[2] * v_difs[2]))
422 edges_lengths.append(edge_length)
423 edges_lengths_sum += edge_length
425 prev_v_co = v_co
427 return edges_lengths, edges_lengths_sum
429 # Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
430 def get_edges_proportions(self, edges_lengths, edges_lengths_sum, use_boundaries, fixed_edges_num):
431 edges_proportions = []
432 if use_boundaries:
433 verts_count = 1
434 for l in edges_lengths:
435 edges_proportions.append(l / edges_lengths_sum)
436 verts_count += 1
437 else:
438 verts_count = 1
439 for _n in range(0, fixed_edges_num):
440 edges_proportions.append(1 / fixed_edges_num)
441 verts_count += 1
443 return edges_proportions
445 # Calculates the angle between two pairs of points in space
446 def orientation_difference(self, points_A_co, points_B_co):
447 # each parameter should be a list with two elements,
448 # and each element should be a x,y,z coordinate
449 vec_A = points_A_co[0] - points_A_co[1]
450 vec_B = points_B_co[0] - points_B_co[1]
452 angle = vec_A.angle(vec_B)
454 if angle > 0.5 * pi:
455 angle = abs(angle - pi)
457 return angle
459 # Calculate the which vert of verts_idx list is the nearest one
460 # to the point_co coordinates, and the distance
461 def shortest_distance(self, object, point_co, verts_idx):
462 matrix = object.matrix_world
464 for i in range(0, len(verts_idx)):
465 dist = (point_co - matrix @ object.data.vertices[verts_idx[i]].co).length
466 if i == 0:
467 prev_dist = dist
468 nearest_vert_idx = verts_idx[i]
469 shortest_dist = dist
471 if dist < prev_dist:
472 prev_dist = dist
473 nearest_vert_idx = verts_idx[i]
474 shortest_dist = dist
476 return nearest_vert_idx, shortest_dist
478 # Returns the index of the opposite vert tip in a chain, given a vert tip index
479 # as parameter, and a multidimentional list with all pairs of tips
480 def opposite_tip(self, vert_tip_idx, all_chains_tips_idx):
481 opposite_vert_tip_idx = None
482 for i in range(0, len(all_chains_tips_idx)):
483 if vert_tip_idx == all_chains_tips_idx[i][0]:
484 opposite_vert_tip_idx = all_chains_tips_idx[i][1]
485 if vert_tip_idx == all_chains_tips_idx[i][1]:
486 opposite_vert_tip_idx = all_chains_tips_idx[i][0]
488 return opposite_vert_tip_idx
490 # Simplifies a spline and returns the new points coordinates
491 def simplify_spline(self, spline_coords, segments_num):
492 simplified_spline = []
493 points_between_segments = round(len(spline_coords) / segments_num)
495 simplified_spline.append(spline_coords[0])
496 for i in range(1, segments_num):
497 simplified_spline.append(spline_coords[i * points_between_segments])
499 simplified_spline.append(spline_coords[len(spline_coords) - 1])
501 return simplified_spline
503 # Returns a list with the coords of the points distributed over the splines
504 # passed to this method according to the proportions parameter
505 def distribute_pts(self, surface_splines, proportions):
507 # Calculate the length of each final surface spline
508 surface_splines_lengths = []
509 surface_splines_parsed = []
511 for sp_idx in range(0, len(surface_splines)):
512 # Calculate spline length
513 surface_splines_lengths.append(0)
515 for i in range(0, len(surface_splines[sp_idx].bezier_points)):
516 if i == 0:
517 prev_p = surface_splines[sp_idx].bezier_points[i]
518 else:
519 p = surface_splines[sp_idx].bezier_points[i]
520 edge_length = (prev_p.co - p.co).length
521 surface_splines_lengths[sp_idx] += edge_length
523 prev_p = p
525 # Calculate vertex positions with appropriate edge proportions, and ordered, for each spline
526 for sp_idx in range(0, len(surface_splines)):
527 surface_splines_parsed.append([])
528 surface_splines_parsed[sp_idx].append(surface_splines[sp_idx].bezier_points[0].co)
530 prev_p_co = surface_splines[sp_idx].bezier_points[0].co
531 p_idx = 0
533 for prop_idx in range(len(proportions) - 1):
534 target_length = surface_splines_lengths[sp_idx] * proportions[prop_idx]
535 partial_segment_length = 0
536 finish_while = False
538 while True:
539 # if not it'll pass the p_idx as an index below and crash
540 if p_idx < len(surface_splines[sp_idx].bezier_points):
541 p_co = surface_splines[sp_idx].bezier_points[p_idx].co
542 new_dist = (prev_p_co - p_co).length
544 # The new distance that could have the partial segment if
545 # it is still shorter than the target length
546 potential_segment_length = partial_segment_length + new_dist
548 # If the potential is still shorter, keep adding
549 if potential_segment_length < target_length:
550 partial_segment_length = potential_segment_length
552 p_idx += 1
553 prev_p_co = p_co
555 # If the potential is longer than the target, calculate the target
556 # (a point between the last two points), and assign
557 elif potential_segment_length > target_length:
558 remaining_dist = target_length - partial_segment_length
559 vec = p_co - prev_p_co
560 vec.normalize()
561 intermediate_co = prev_p_co + (vec * remaining_dist)
563 surface_splines_parsed[sp_idx].append(intermediate_co)
565 partial_segment_length += remaining_dist
566 prev_p_co = intermediate_co
568 finish_while = True
570 # If the potential is equal to the target, assign
571 elif potential_segment_length == target_length:
572 surface_splines_parsed[sp_idx].append(p_co)
573 prev_p_co = p_co
575 finish_while = True
577 if finish_while:
578 break
580 # last point of the spline
581 surface_splines_parsed[sp_idx].append(
582 surface_splines[sp_idx].bezier_points[len(surface_splines[sp_idx].bezier_points) - 1].co
585 return surface_splines_parsed
587 # Counts the number of faces that belong to each edge
588 def edge_face_count(self, ob):
589 ed_keys_count_dict = {}
591 for face in ob.data.polygons:
592 for ed_keys in face.edge_keys:
593 if ed_keys not in ed_keys_count_dict:
594 ed_keys_count_dict[ed_keys] = 1
595 else:
596 ed_keys_count_dict[ed_keys] += 1
598 edge_face_count = []
599 for i in range(len(ob.data.edges)):
600 edge_face_count.append(0)
602 for i in range(len(ob.data.edges)):
603 ed = ob.data.edges[i]
605 v1 = ed.vertices[0]
606 v2 = ed.vertices[1]
608 if (v1, v2) in ed_keys_count_dict:
609 edge_face_count[i] = ed_keys_count_dict[(v1, v2)]
610 elif (v2, v1) in ed_keys_count_dict:
611 edge_face_count[i] = ed_keys_count_dict[(v2, v1)]
613 return edge_face_count
615 # Fills with faces all the selected vertices which form empty triangles or quads
616 def fill_with_faces(self, object):
617 all_selected_verts_count = self.main_object_selected_verts_count
619 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
621 # Calculate average length of selected edges
622 all_selected_verts = []
623 original_sel_edges_count = 0
624 for ed in object.data.edges:
625 if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
626 coords = []
627 coords.append(object.data.vertices[ed.vertices[0]].co)
628 coords.append(object.data.vertices[ed.vertices[1]].co)
630 original_sel_edges_count += 1
632 if not ed.vertices[0] in all_selected_verts:
633 all_selected_verts.append(ed.vertices[0])
635 if not ed.vertices[1] in all_selected_verts:
636 all_selected_verts.append(ed.vertices[1])
638 tuple(all_selected_verts)
640 # Check if there is any edge selected. If not, interrupt the script
641 if original_sel_edges_count == 0 and all_selected_verts_count > 0:
642 return 0
644 # Get all edges connected to selected verts
645 all_edges_around_sel_verts = []
646 edges_connected_to_sel_verts = {}
647 verts_connected_to_every_vert = {}
648 for ed_idx in range(len(object.data.edges)):
649 ed = object.data.edges[ed_idx]
650 include_edge = False
652 if ed.vertices[0] in all_selected_verts:
653 if not ed.vertices[0] in edges_connected_to_sel_verts:
654 edges_connected_to_sel_verts[ed.vertices[0]] = []
656 edges_connected_to_sel_verts[ed.vertices[0]].append(ed_idx)
657 include_edge = True
659 if ed.vertices[1] in all_selected_verts:
660 if not ed.vertices[1] in edges_connected_to_sel_verts:
661 edges_connected_to_sel_verts[ed.vertices[1]] = []
663 edges_connected_to_sel_verts[ed.vertices[1]].append(ed_idx)
664 include_edge = True
666 if include_edge is True:
667 all_edges_around_sel_verts.append(ed_idx)
669 # Get all connected verts to each vert
670 if not ed.vertices[0] in verts_connected_to_every_vert:
671 verts_connected_to_every_vert[ed.vertices[0]] = []
673 if not ed.vertices[1] in verts_connected_to_every_vert:
674 verts_connected_to_every_vert[ed.vertices[1]] = []
676 verts_connected_to_every_vert[ed.vertices[0]].append(ed.vertices[1])
677 verts_connected_to_every_vert[ed.vertices[1]].append(ed.vertices[0])
679 # Get all verts connected to faces
680 all_verts_part_of_faces = []
681 all_edges_faces_count = []
682 all_edges_faces_count += self.edge_face_count(object)
684 # Get only the selected edges that have faces attached.
685 count_faces_of_edges_around_sel_verts = {}
686 selected_verts_with_faces = []
687 for ed_idx in all_edges_around_sel_verts:
688 count_faces_of_edges_around_sel_verts[ed_idx] = all_edges_faces_count[ed_idx]
690 if all_edges_faces_count[ed_idx] > 0:
691 ed = object.data.edges[ed_idx]
693 if not ed.vertices[0] in selected_verts_with_faces:
694 selected_verts_with_faces.append(ed.vertices[0])
696 if not ed.vertices[1] in selected_verts_with_faces:
697 selected_verts_with_faces.append(ed.vertices[1])
699 all_verts_part_of_faces.append(ed.vertices[0])
700 all_verts_part_of_faces.append(ed.vertices[1])
702 tuple(selected_verts_with_faces)
704 # Discard unneeded verts from calculations
705 participating_verts = []
706 movable_verts = []
707 for v_idx in all_selected_verts:
708 vert_has_edges_with_one_face = False
710 # Check if the actual vert has at least one edge connected to only one face
711 for ed_idx in edges_connected_to_sel_verts[v_idx]:
712 if count_faces_of_edges_around_sel_verts[ed_idx] == 1:
713 vert_has_edges_with_one_face = True
715 # If the vert has two or less edges connected and the vert is not part of any face.
716 # Or the vert is part of any face and at least one of
717 # the connected edges has only one face attached to it.
718 if (len(edges_connected_to_sel_verts[v_idx]) == 2 and
719 v_idx not in all_verts_part_of_faces) or \
720 len(edges_connected_to_sel_verts[v_idx]) == 1 or \
721 (v_idx in all_verts_part_of_faces and
722 vert_has_edges_with_one_face):
724 participating_verts.append(v_idx)
726 if v_idx not in all_verts_part_of_faces:
727 movable_verts.append(v_idx)
729 # Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
730 for mv_idx in movable_verts:
731 freeze_vert = False
732 mv_connected_verts = verts_connected_to_every_vert[mv_idx]
734 for actual_v_idx in all_selected_verts:
735 count_shared_neighbors = 0
736 checked_verts = []
738 for mv_conn_v_idx in mv_connected_verts:
739 if mv_idx != actual_v_idx:
740 if mv_conn_v_idx in verts_connected_to_every_vert[actual_v_idx] and \
741 mv_conn_v_idx not in checked_verts:
742 count_shared_neighbors += 1
743 checked_verts.append(mv_conn_v_idx)
745 if actual_v_idx in mv_connected_verts:
746 freeze_vert = True
747 break
749 if count_shared_neighbors == 2:
750 freeze_vert = True
751 break
753 if freeze_vert:
754 break
756 if freeze_vert:
757 movable_verts.remove(mv_idx)
759 # Calculate merge distance for participating verts
760 shortest_edge_length = None
761 for ed in object.data.edges:
762 if ed.vertices[0] in movable_verts and ed.vertices[1] in movable_verts:
763 v1 = object.data.vertices[ed.vertices[0]]
764 v2 = object.data.vertices[ed.vertices[1]]
766 length = (v1.co - v2.co).length
768 if shortest_edge_length is None:
769 shortest_edge_length = length
770 else:
771 if length < shortest_edge_length:
772 shortest_edge_length = length
774 if shortest_edge_length is not None:
775 edges_merge_distance = shortest_edge_length * 0.5
776 else:
777 edges_merge_distance = 0
779 # Get together the verts near enough. They will be merged later
780 remaining_verts = []
781 remaining_verts += participating_verts
782 for v1_idx in participating_verts:
783 if v1_idx in remaining_verts and v1_idx in movable_verts:
784 verts_to_merge = []
785 coords_verts_to_merge = {}
787 verts_to_merge.append(v1_idx)
789 v1_co = object.data.vertices[v1_idx].co
790 coords_verts_to_merge[v1_idx] = (v1_co[0], v1_co[1], v1_co[2])
792 for v2_idx in remaining_verts:
793 if v1_idx != v2_idx:
794 v2_co = object.data.vertices[v2_idx].co
796 dist = (v1_co - v2_co).length
798 if dist <= edges_merge_distance: # Add the verts which are near enough
799 verts_to_merge.append(v2_idx)
801 coords_verts_to_merge[v2_idx] = (v2_co[0], v2_co[1], v2_co[2])
803 for vm_idx in verts_to_merge:
804 remaining_verts.remove(vm_idx)
806 if len(verts_to_merge) > 1:
807 # Calculate middle point of the verts to merge.
808 sum_x_co = 0
809 sum_y_co = 0
810 sum_z_co = 0
811 movable_verts_to_merge_count = 0
812 for i in range(len(verts_to_merge)):
813 if verts_to_merge[i] in movable_verts:
814 v_co = object.data.vertices[verts_to_merge[i]].co
816 sum_x_co += v_co[0]
817 sum_y_co += v_co[1]
818 sum_z_co += v_co[2]
820 movable_verts_to_merge_count += 1
822 middle_point_co = [
823 sum_x_co / movable_verts_to_merge_count,
824 sum_y_co / movable_verts_to_merge_count,
825 sum_z_co / movable_verts_to_merge_count
828 # Check if any vert to be merged is not movable
829 shortest_dist = None
830 are_verts_not_movable = False
831 verts_not_movable = []
832 for v_merge_idx in verts_to_merge:
833 if v_merge_idx in participating_verts and v_merge_idx not in movable_verts:
834 are_verts_not_movable = True
835 verts_not_movable.append(v_merge_idx)
837 if are_verts_not_movable:
838 # Get the vert connected to faces, that is nearest to
839 # the middle point of the movable verts
840 shortest_dist = None
841 for vcf_idx in verts_not_movable:
842 dist = abs((object.data.vertices[vcf_idx].co -
843 Vector(middle_point_co)).length)
845 if shortest_dist is None:
846 shortest_dist = dist
847 nearest_vert_idx = vcf_idx
848 else:
849 if dist < shortest_dist:
850 shortest_dist = dist
851 nearest_vert_idx = vcf_idx
853 coords = object.data.vertices[nearest_vert_idx].co
854 target_point_co = [coords[0], coords[1], coords[2]]
855 else:
856 target_point_co = middle_point_co
858 # Move verts to merge to the middle position
859 for v_merge_idx in verts_to_merge:
860 if v_merge_idx in movable_verts: # Only move the verts that are not part of faces
861 object.data.vertices[v_merge_idx].co[0] = target_point_co[0]
862 object.data.vertices[v_merge_idx].co[1] = target_point_co[1]
863 object.data.vertices[v_merge_idx].co[2] = target_point_co[2]
865 # Perform "Remove Doubles" to weld all the disconnected verts
866 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
867 bpy.ops.mesh.remove_doubles(threshold=0.0001)
869 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
871 # Get all the definitive selected edges, after weldding
872 selected_edges = []
873 edges_per_vert = {} # Number of faces of each selected edge
874 for ed in object.data.edges:
875 if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
876 selected_edges.append(ed.index)
878 # Save all the edges that belong to each vertex.
879 if not ed.vertices[0] in edges_per_vert:
880 edges_per_vert[ed.vertices[0]] = []
882 if not ed.vertices[1] in edges_per_vert:
883 edges_per_vert[ed.vertices[1]] = []
885 edges_per_vert[ed.vertices[0]].append(ed.index)
886 edges_per_vert[ed.vertices[1]].append(ed.index)
888 # Check if all the edges connected to each vert have two faces attached to them.
889 # To discard them later and make calculations faster
890 a = []
891 a += self.edge_face_count(object)
892 tuple(a)
893 verts_surrounded_by_faces = {}
894 for v_idx in edges_per_vert:
895 edges_with_two_faces_count = 0
897 for ed_idx in edges_per_vert[v_idx]:
898 if a[ed_idx] == 2:
899 edges_with_two_faces_count += 1
901 if edges_with_two_faces_count == len(edges_per_vert[v_idx]):
902 verts_surrounded_by_faces[v_idx] = True
903 else:
904 verts_surrounded_by_faces[v_idx] = False
906 # Get all the selected vertices
907 selected_verts_idx = []
908 for v in object.data.vertices:
909 if v.select:
910 selected_verts_idx.append(v.index)
912 # Get all the faces of the object
913 all_object_faces_verts_idx = []
914 for face in object.data.polygons:
915 face_verts = []
916 face_verts.append(face.vertices[0])
917 face_verts.append(face.vertices[1])
918 face_verts.append(face.vertices[2])
920 if len(face.vertices) == 4:
921 face_verts.append(face.vertices[3])
923 all_object_faces_verts_idx.append(face_verts)
925 # Deselect all vertices
926 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
927 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
928 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
930 # Make a dictionary with the verts related to each vert
931 related_key_verts = {}
932 for ed_idx in selected_edges:
933 ed = object.data.edges[ed_idx]
935 if not verts_surrounded_by_faces[ed.vertices[0]]:
936 if not ed.vertices[0] in related_key_verts:
937 related_key_verts[ed.vertices[0]] = []
939 if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
940 related_key_verts[ed.vertices[0]].append(ed.vertices[1])
942 if not verts_surrounded_by_faces[ed.vertices[1]]:
943 if not ed.vertices[1] in related_key_verts:
944 related_key_verts[ed.vertices[1]] = []
946 if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
947 related_key_verts[ed.vertices[1]].append(ed.vertices[0])
949 # Get groups of verts forming each face
950 faces_verts_idx = []
951 for v1 in related_key_verts: # verts-1 ....
952 for v2 in related_key_verts: # verts-2
953 if v1 != v2:
954 related_verts_in_common = []
955 v2_in_rel_v1 = False
956 v1_in_rel_v2 = False
957 for rel_v1 in related_key_verts[v1]:
958 # Check if related verts of verts-1 are related verts of verts-2
959 if rel_v1 in related_key_verts[v2]:
960 related_verts_in_common.append(rel_v1)
962 if v2 in related_key_verts[v1]:
963 v2_in_rel_v1 = True
965 if v1 in related_key_verts[v2]:
966 v1_in_rel_v2 = True
968 repeated_face = False
969 # If two verts have two related verts in common, they form a quad
970 if len(related_verts_in_common) == 2:
971 # Check if the face is already saved
972 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
974 for f_verts in all_faces_to_check_idx:
975 repeated_verts = 0
977 if len(f_verts) == 4:
978 if v1 in f_verts:
979 repeated_verts += 1
980 if v2 in f_verts:
981 repeated_verts += 1
982 if related_verts_in_common[0] in f_verts:
983 repeated_verts += 1
984 if related_verts_in_common[1] in f_verts:
985 repeated_verts += 1
987 if repeated_verts == len(f_verts):
988 repeated_face = True
989 break
991 if not repeated_face:
992 faces_verts_idx.append(
993 [v1, related_verts_in_common[0], v2, related_verts_in_common[1]]
996 # If Two verts have one related vert in common and
997 # they are related to each other, they form a triangle
998 elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
999 # Check if the face is already saved.
1000 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
1002 for f_verts in all_faces_to_check_idx:
1003 repeated_verts = 0
1005 if len(f_verts) == 3:
1006 if v1 in f_verts:
1007 repeated_verts += 1
1008 if v2 in f_verts:
1009 repeated_verts += 1
1010 if related_verts_in_common[0] in f_verts:
1011 repeated_verts += 1
1013 if repeated_verts == len(f_verts):
1014 repeated_face = True
1015 break
1017 if not repeated_face:
1018 faces_verts_idx.append([v1, related_verts_in_common[0], v2])
1020 # Keep only the faces that don't overlap by ignoring quads
1021 # that overlap with two adjacent triangles
1022 faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate
1023 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
1024 for i in range(len(faces_verts_idx)):
1025 for t in range(len(all_faces_to_check_idx)):
1026 if i != t:
1027 verts_in_common = 0
1029 if len(faces_verts_idx[i]) == 4 and len(all_faces_to_check_idx[t]) == 3:
1030 for v_idx in all_faces_to_check_idx[t]:
1031 if v_idx in faces_verts_idx[i]:
1032 verts_in_common += 1
1033 # If it doesn't have all it's vertices repeated in the other face
1034 if verts_in_common == 3:
1035 if i not in faces_to_not_include_idx:
1036 faces_to_not_include_idx.append(i)
1038 # Build faces discarding the ones in faces_to_not_include
1039 me = object.data
1040 bm = bmesh.new()
1041 bm.from_mesh(me)
1043 num_faces_created = 0
1044 for i in range(len(faces_verts_idx)):
1045 if i not in faces_to_not_include_idx:
1046 bm.faces.new([bm.verts[v] for v in faces_verts_idx[i]])
1048 num_faces_created += 1
1050 bm.to_mesh(me)
1051 bm.free()
1053 for v_idx in selected_verts_idx:
1054 self.main_object.data.vertices[v_idx].select = True
1056 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
1057 bpy.ops.mesh.normals_make_consistent(inside=False)
1058 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
1060 self.update()
1062 return num_faces_created
1064 # Crosshatch skinning
1065 def crosshatch_surface_invoke(self, ob_original_splines):
1066 self.is_crosshatch = False
1067 self.crosshatch_merge_distance = 0
1069 objects_to_delete = [] # duplicated strokes to be deleted.
1071 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1072 # (without this the surface verts merging with the main object doesn't work well)
1073 self.modifiers_prev_viewport_state = []
1074 if len(self.main_object.modifiers) > 0:
1075 for m_idx in range(len(self.main_object.modifiers)):
1076 self.modifiers_prev_viewport_state.append(
1077 self.main_object.modifiers[m_idx].show_viewport
1079 self.main_object.modifiers[m_idx].show_viewport = False
1081 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1082 ob_original_splines.select_set(True)
1083 bpy.context.view_layer.objects.active = ob_original_splines
1085 if len(ob_original_splines.data.splines) >= 2:
1086 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1087 ob_splines = bpy.context.object
1088 ob_splines.name = "SURFSKIO_NE_STR"
1090 # Get estimative merge distance (sum up the distances from the first point to
1091 # all other points, then average them and then divide them)
1092 first_point_dist_sum = 0
1093 first_dist = 0
1094 second_dist = 0
1095 coords_first_pt = ob_splines.data.splines[0].bezier_points[0].co
1096 for i in range(len(ob_splines.data.splines)):
1097 sp = ob_splines.data.splines[i]
1099 if coords_first_pt != sp.bezier_points[0].co:
1100 first_dist = (coords_first_pt - sp.bezier_points[0].co).length
1102 if coords_first_pt != sp.bezier_points[len(sp.bezier_points) - 1].co:
1103 second_dist = (coords_first_pt - sp.bezier_points[len(sp.bezier_points) - 1].co).length
1105 first_point_dist_sum += first_dist + second_dist
1107 if i == 0:
1108 if first_dist != 0:
1109 shortest_dist = first_dist
1110 elif second_dist != 0:
1111 shortest_dist = second_dist
1113 if shortest_dist > first_dist and first_dist != 0:
1114 shortest_dist = first_dist
1116 if shortest_dist > second_dist and second_dist != 0:
1117 shortest_dist = second_dist
1119 self.crosshatch_merge_distance = shortest_dist / 20
1121 # Recalculation of merge distance
1123 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1125 ob_calc_merge_dist = bpy.context.object
1126 ob_calc_merge_dist.name = "SURFSKIO_CALC_TMP"
1128 objects_to_delete.append(ob_calc_merge_dist)
1130 # Smooth out strokes a little to improve crosshatch detection
1131 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1132 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
1134 for i in range(4):
1135 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1137 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1138 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1140 # Convert curves into mesh
1141 ob_calc_merge_dist.data.resolution_u = 12
1142 bpy.ops.object.convert(target='MESH', keep_original=False)
1144 # Find "intersection-nodes"
1145 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1146 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1147 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
1148 threshold=self.crosshatch_merge_distance)
1149 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
1150 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1152 # Remove verts with less than three edges
1153 verts_edges_count = {}
1154 for ed in ob_calc_merge_dist.data.edges:
1155 v = ed.vertices
1157 if v[0] not in verts_edges_count:
1158 verts_edges_count[v[0]] = 0
1160 if v[1] not in verts_edges_count:
1161 verts_edges_count[v[1]] = 0
1163 verts_edges_count[v[0]] += 1
1164 verts_edges_count[v[1]] += 1
1166 nodes_verts_coords = []
1167 for v_idx in verts_edges_count:
1168 v = ob_calc_merge_dist.data.vertices[v_idx]
1170 if verts_edges_count[v_idx] < 3:
1171 v.select = True
1173 # Remove them
1174 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1175 bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
1176 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1178 # Remove doubles to discard very near verts from calculations of distance
1179 bpy.ops.mesh.remove_doubles(
1180 'INVOKE_REGION_WIN',
1181 threshold=self.crosshatch_merge_distance * 4.0
1183 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1185 # Get all coords of the resulting nodes
1186 nodes_verts_coords = [(v.co[0], v.co[1], v.co[2]) for
1187 v in ob_calc_merge_dist.data.vertices]
1189 # Check if the strokes are a crosshatch
1190 if len(nodes_verts_coords) >= 3:
1191 self.is_crosshatch = True
1193 shortest_dist = None
1194 for co_1 in nodes_verts_coords:
1195 for co_2 in nodes_verts_coords:
1196 if co_1 != co_2:
1197 dist = (Vector(co_1) - Vector(co_2)).length
1199 if shortest_dist is not None:
1200 if dist < shortest_dist:
1201 shortest_dist = dist
1202 else:
1203 shortest_dist = dist
1205 self.crosshatch_merge_distance = shortest_dist / 3
1207 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1208 ob_splines.select_set(True)
1209 bpy.context.view_layer.objects.active = ob_splines
1211 # Deselect all points
1212 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1213 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1214 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1216 # Smooth splines in a localized way, to eliminate "saw-teeth"
1217 # like shapes when there are many points
1218 for sp in ob_splines.data.splines:
1219 angle_sum = 0
1221 angle_limit = 2 # Degrees
1222 for t in range(len(sp.bezier_points)):
1223 # Because on each iteration it checks the "next two points"
1224 # of the actual. This way it doesn't go out of range
1225 if t <= len(sp.bezier_points) - 3:
1226 p1 = sp.bezier_points[t]
1227 p2 = sp.bezier_points[t + 1]
1228 p3 = sp.bezier_points[t + 2]
1230 vec_1 = p1.co - p2.co
1231 vec_2 = p2.co - p3.co
1233 if p2.co != p1.co and p2.co != p3.co:
1234 angle = vec_1.angle(vec_2)
1235 angle_sum += degrees(angle)
1237 if angle_sum >= angle_limit: # If sum of angles is grater than the limit
1238 if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
1239 p1.select_control_point = True
1240 p1.select_left_handle = True
1241 p1.select_right_handle = True
1243 p2.select_control_point = True
1244 p2.select_left_handle = True
1245 p2.select_right_handle = True
1247 if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
1248 p3.select_control_point = True
1249 p3.select_left_handle = True
1250 p3.select_right_handle = True
1252 angle_sum = 0
1254 sp.bezier_points[0].select_control_point = False
1255 sp.bezier_points[0].select_left_handle = False
1256 sp.bezier_points[0].select_right_handle = False
1258 sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = False
1259 sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = False
1260 sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = False
1262 # Smooth out strokes a little to improve crosshatch detection
1263 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1265 for i in range(15):
1266 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1268 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1269 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1271 # Simplify the splines
1272 for sp in ob_splines.data.splines:
1273 angle_sum = 0
1275 sp.bezier_points[0].select_control_point = True
1276 sp.bezier_points[0].select_left_handle = True
1277 sp.bezier_points[0].select_right_handle = True
1279 sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = True
1280 sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = True
1281 sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = True
1283 angle_limit = 15 # Degrees
1284 for t in range(len(sp.bezier_points)):
1285 # Because on each iteration it checks the "next two points"
1286 # of the actual. This way it doesn't go out of range
1287 if t <= len(sp.bezier_points) - 3:
1288 p1 = sp.bezier_points[t]
1289 p2 = sp.bezier_points[t + 1]
1290 p3 = sp.bezier_points[t + 2]
1292 vec_1 = p1.co - p2.co
1293 vec_2 = p2.co - p3.co
1295 if p2.co != p1.co and p2.co != p3.co:
1296 angle = vec_1.angle(vec_2)
1297 angle_sum += degrees(angle)
1298 # If sum of angles is grater than the limit
1299 if angle_sum >= angle_limit:
1300 p1.select_control_point = True
1301 p1.select_left_handle = True
1302 p1.select_right_handle = True
1304 p2.select_control_point = True
1305 p2.select_left_handle = True
1306 p2.select_right_handle = True
1308 p3.select_control_point = True
1309 p3.select_left_handle = True
1310 p3.select_right_handle = True
1312 angle_sum = 0
1314 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1315 bpy.ops.curve.select_all(action='INVERT')
1317 bpy.ops.curve.delete(type='VERT')
1318 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1320 objects_to_delete.append(ob_splines)
1322 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1323 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1324 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1326 # Check if the strokes are a crosshatch
1327 if self.is_crosshatch:
1328 all_points_coords = []
1329 for i in range(len(ob_splines.data.splines)):
1330 all_points_coords.append([])
1332 all_points_coords[i] = [Vector((x, y, z)) for
1333 x, y, z in [bp.co for
1334 bp in ob_splines.data.splines[i].bezier_points]]
1336 all_intersections = []
1337 checked_splines = []
1338 for i in range(len(all_points_coords)):
1340 for t in range(len(all_points_coords[i]) - 1):
1341 bp1_co = all_points_coords[i][t]
1342 bp2_co = all_points_coords[i][t + 1]
1344 for i2 in range(len(all_points_coords)):
1345 if i != i2 and i2 not in checked_splines:
1346 for t2 in range(len(all_points_coords[i2]) - 1):
1347 bp3_co = all_points_coords[i2][t2]
1348 bp4_co = all_points_coords[i2][t2 + 1]
1350 intersec_coords = intersect_line_line(
1351 bp1_co, bp2_co, bp3_co, bp4_co
1353 if intersec_coords is not None:
1354 dist = (intersec_coords[0] - intersec_coords[1]).length
1356 if dist <= self.crosshatch_merge_distance * 1.5:
1357 _temp_co, percent1 = intersect_point_line(
1358 intersec_coords[0], bp1_co, bp2_co
1360 if (percent1 >= -0.02 and percent1 <= 1.02):
1361 _temp_co, percent2 = intersect_point_line(
1362 intersec_coords[1], bp3_co, bp4_co
1364 if (percent2 >= -0.02 and percent2 <= 1.02):
1365 # Format: spline index, first point index from
1366 # corresponding segment, percentage from first point of
1367 # actual segment, coords of intersection point
1368 all_intersections.append(
1369 (i, t, percent1,
1370 ob_splines.matrix_world @ intersec_coords[0])
1372 all_intersections.append(
1373 (i2, t2, percent2,
1374 ob_splines.matrix_world @ intersec_coords[1])
1377 checked_splines.append(i)
1378 # Sort list by spline, then by corresponding first point index of segment,
1379 # and then by percentage from first point of segment: elements 0 and 1 respectively
1380 all_intersections.sort(key=operator.itemgetter(0, 1, 2))
1382 self.crosshatch_strokes_coords = {}
1383 for i in range(len(all_intersections)):
1384 if not all_intersections[i][0] in self.crosshatch_strokes_coords:
1385 self.crosshatch_strokes_coords[all_intersections[i][0]] = []
1387 self.crosshatch_strokes_coords[all_intersections[i][0]].append(
1388 all_intersections[i][3]
1389 ) # Save intersection coords
1390 else:
1391 self.is_crosshatch = False
1393 # Delete all duplicates
1394 with bpy.context.temp_override(selected_objects=objects_to_delete):
1395 bpy.ops.object.delete()
1397 # If the main object has modifiers, turn their "viewport view status" to
1398 # what it was before the forced deactivation above
1399 if len(self.main_object.modifiers) > 0:
1400 for m_idx in range(len(self.main_object.modifiers)):
1401 self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
1403 self.update()
1405 return
1407 # Part of the Crosshatch process that is repeated when the operator is tweaked
1408 def crosshatch_surface_execute(self, context):
1409 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1410 # (without this the surface verts merging with the main object doesn't work well)
1411 self.modifiers_prev_viewport_state = []
1412 if len(self.main_object.modifiers) > 0:
1413 for m_idx in range(len(self.main_object.modifiers)):
1414 self.modifiers_prev_viewport_state.append(self.main_object.modifiers[m_idx].show_viewport)
1416 self.main_object.modifiers[m_idx].show_viewport = False
1418 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1420 me_name = "SURFSKIO_STK_TMP"
1421 me = bpy.data.meshes.new(me_name)
1423 all_verts_coords = []
1424 all_edges = []
1425 for st_idx in self.crosshatch_strokes_coords:
1426 for co_idx in range(len(self.crosshatch_strokes_coords[st_idx])):
1427 coords = self.crosshatch_strokes_coords[st_idx][co_idx]
1429 all_verts_coords.append(coords)
1431 if co_idx > 0:
1432 all_edges.append((len(all_verts_coords) - 2, len(all_verts_coords) - 1))
1434 me.from_pydata(all_verts_coords, all_edges, [])
1435 ob = object_utils.object_data_add(context, me)
1436 ob.location = (0.0, 0.0, 0.0)
1437 ob.rotation_euler = (0.0, 0.0, 0.0)
1438 ob.scale = (1.0, 1.0, 1.0)
1440 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1441 ob.select_set(True)
1442 bpy.context.view_layer.objects.active = ob
1444 # Get together each vert and its nearest, to the middle position
1445 verts = ob.data.vertices
1446 checked_verts = []
1447 for i in range(len(verts)):
1448 shortest_dist = None
1450 if i not in checked_verts:
1451 for t in range(len(verts)):
1452 if i != t and t not in checked_verts:
1453 dist = (verts[i].co - verts[t].co).length
1455 if shortest_dist is not None:
1456 if dist < shortest_dist:
1457 shortest_dist = dist
1458 nearest_vert = t
1459 else:
1460 shortest_dist = dist
1461 nearest_vert = t
1463 middle_location = (verts[i].co + verts[nearest_vert].co) / 2
1465 verts[i].co = middle_location
1466 verts[nearest_vert].co = middle_location
1468 checked_verts.append(i)
1469 checked_verts.append(nearest_vert)
1471 # Calculate average length between all the generated edges
1472 ob = bpy.context.object
1473 lengths_sum = 0
1474 for ed in ob.data.edges:
1475 v1 = ob.data.vertices[ed.vertices[0]]
1476 v2 = ob.data.vertices[ed.vertices[1]]
1478 lengths_sum += (v1.co - v2.co).length
1480 edges_count = len(ob.data.edges)
1481 # possible division by zero here
1482 average_edge_length = lengths_sum / edges_count if edges_count != 0 else 0.0001
1484 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1485 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1486 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
1487 threshold=average_edge_length / 15.0)
1488 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1490 final_points_ob = bpy.context.view_layer.objects.active
1492 # Make a dictionary with the verts related to each vert
1493 related_key_verts = {}
1494 for ed in final_points_ob.data.edges:
1495 if not ed.vertices[0] in related_key_verts:
1496 related_key_verts[ed.vertices[0]] = []
1498 if not ed.vertices[1] in related_key_verts:
1499 related_key_verts[ed.vertices[1]] = []
1501 if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
1502 related_key_verts[ed.vertices[0]].append(ed.vertices[1])
1504 if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
1505 related_key_verts[ed.vertices[1]].append(ed.vertices[0])
1507 # Get groups of verts forming each face
1508 faces_verts_idx = []
1509 for v1 in related_key_verts: # verts-1 ....
1510 for v2 in related_key_verts: # verts-2
1511 if v1 != v2:
1512 related_verts_in_common = []
1513 v2_in_rel_v1 = False
1514 v1_in_rel_v2 = False
1515 for rel_v1 in related_key_verts[v1]:
1516 # Check if related verts of verts-1 are related verts of verts-2
1517 if rel_v1 in related_key_verts[v2]:
1518 related_verts_in_common.append(rel_v1)
1520 if v2 in related_key_verts[v1]:
1521 v2_in_rel_v1 = True
1523 if v1 in related_key_verts[v2]:
1524 v1_in_rel_v2 = True
1526 repeated_face = False
1527 # If two verts have two related verts in common, they form a quad
1528 if len(related_verts_in_common) == 2:
1529 # Check if the face is already saved
1530 for f_verts in faces_verts_idx:
1531 repeated_verts = 0
1533 if len(f_verts) == 4:
1534 if v1 in f_verts:
1535 repeated_verts += 1
1536 if v2 in f_verts:
1537 repeated_verts += 1
1538 if related_verts_in_common[0] in f_verts:
1539 repeated_verts += 1
1540 if related_verts_in_common[1] in f_verts:
1541 repeated_verts += 1
1543 if repeated_verts == len(f_verts):
1544 repeated_face = True
1545 break
1547 if not repeated_face:
1548 faces_verts_idx.append([v1, related_verts_in_common[0],
1549 v2, related_verts_in_common[1]])
1551 # If Two verts have one related vert in common and they are
1552 # related to each other, they form a triangle
1553 elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
1554 # Check if the face is already saved.
1555 for f_verts in faces_verts_idx:
1556 repeated_verts = 0
1558 if len(f_verts) == 3:
1559 if v1 in f_verts:
1560 repeated_verts += 1
1561 if v2 in f_verts:
1562 repeated_verts += 1
1563 if related_verts_in_common[0] in f_verts:
1564 repeated_verts += 1
1566 if repeated_verts == len(f_verts):
1567 repeated_face = True
1568 break
1570 if not repeated_face:
1571 faces_verts_idx.append([v1, related_verts_in_common[0], v2])
1573 # Keep only the faces that don't overlap by ignoring
1574 # quads that overlap with two adjacent triangles
1575 faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate
1576 for i in range(len(faces_verts_idx)):
1577 for t in range(len(faces_verts_idx)):
1578 if i != t:
1579 verts_in_common = 0
1581 if len(faces_verts_idx[i]) == 4 and len(faces_verts_idx[t]) == 3:
1582 for v_idx in faces_verts_idx[t]:
1583 if v_idx in faces_verts_idx[i]:
1584 verts_in_common += 1
1585 # If it doesn't have all it's vertices repeated in the other face
1586 if verts_in_common == 3:
1587 if i not in faces_to_not_include_idx:
1588 faces_to_not_include_idx.append(i)
1590 # Build surface
1591 all_surface_verts_co = []
1592 for i in range(len(final_points_ob.data.vertices)):
1593 coords = final_points_ob.data.vertices[i].co
1594 all_surface_verts_co.append([coords[0], coords[1], coords[2]])
1596 # Verts of each face.
1597 all_surface_faces = []
1598 for i in range(len(faces_verts_idx)):
1599 if i not in faces_to_not_include_idx:
1600 face = []
1601 for v_idx in faces_verts_idx[i]:
1602 face.append(v_idx)
1604 all_surface_faces.append(face)
1606 # Build the mesh
1607 surf_me_name = "SURFSKIO_surface"
1608 me_surf = bpy.data.meshes.new(surf_me_name)
1609 me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
1610 ob_surface = object_utils.object_data_add(context, me_surf)
1611 ob_surface.location = (0.0, 0.0, 0.0)
1612 ob_surface.rotation_euler = (0.0, 0.0, 0.0)
1613 ob_surface.scale = (1.0, 1.0, 1.0)
1615 # Delete final points temporal object
1616 with bpy.context.temp_override(selected_objects=[final_points_ob]):
1617 bpy.ops.object.delete()
1619 # Delete isolated verts if there are any
1620 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1621 ob_surface.select_set(True)
1622 bpy.context.view_layer.objects.active = ob_surface
1624 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1625 bpy.ops.mesh.select_all(action='DESELECT')
1626 bpy.ops.mesh.select_face_by_sides(type='NOTEQUAL')
1627 bpy.ops.mesh.delete()
1628 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1630 # Join crosshatch results with original mesh
1632 # Calculate a distance to merge the verts of the crosshatch surface to the main object
1633 edges_length_sum = 0
1634 for ed in ob_surface.data.edges:
1635 edges_length_sum += (
1636 ob_surface.data.vertices[ed.vertices[0]].co -
1637 ob_surface.data.vertices[ed.vertices[1]].co
1638 ).length
1640 # Make dictionary with all the verts connected to each vert, on the new surface object.
1641 surface_connected_verts = {}
1642 for ed in ob_surface.data.edges:
1643 if not ed.vertices[0] in surface_connected_verts:
1644 surface_connected_verts[ed.vertices[0]] = []
1646 surface_connected_verts[ed.vertices[0]].append(ed.vertices[1])
1648 if ed.vertices[1] not in surface_connected_verts:
1649 surface_connected_verts[ed.vertices[1]] = []
1651 surface_connected_verts[ed.vertices[1]].append(ed.vertices[0])
1653 # Duplicate the new surface object, and use shrinkwrap to
1654 # calculate later the nearest verts to the main object
1655 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1656 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
1657 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1659 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1661 final_ob_duplicate = bpy.context.view_layer.objects.active
1663 shrinkwrap_modifier = context.object.modifiers.new("", 'SHRINKWRAP')
1664 shrinkwrap_modifier.wrap_method = "NEAREST_VERTEX"
1665 shrinkwrap_modifier.target = self.main_object
1667 bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', modifier=shrinkwrap_modifier.name)
1669 # Make list with verts of original mesh as index and coords as value
1670 main_object_verts_coords = []
1671 for v in self.main_object.data.vertices:
1672 coords = self.main_object.matrix_world @ v.co
1674 # To avoid problems when taking "-0.00" as a different value as "0.00"
1675 for c in range(len(coords)):
1676 if "%.3f" % coords[c] == "-0.00":
1677 coords[c] = 0
1679 main_object_verts_coords.append(["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]])
1681 tuple(main_object_verts_coords)
1683 # Determine which verts will be merged, snap them to the nearest verts
1684 # on the original verts, and get them selected
1685 crosshatch_verts_to_merge = []
1686 if self.automatic_join:
1687 for i in range(len(ob_surface.data.vertices)-1):
1688 # Calculate the distance from each of the connected verts to the actual vert,
1689 # and compare it with the distance they would have if joined.
1690 # If they don't change much, that vert can be joined
1691 merge_actual_vert = True
1692 try:
1693 if len(surface_connected_verts[i]) < 4:
1694 for c_v_idx in surface_connected_verts[i]:
1695 points_original = []
1696 points_original.append(ob_surface.data.vertices[c_v_idx].co)
1697 points_original.append(ob_surface.data.vertices[i].co)
1699 points_target = []
1700 points_target.append(ob_surface.data.vertices[c_v_idx].co)
1701 points_target.append(final_ob_duplicate.data.vertices[i].co)
1703 vec_A = points_original[0] - points_original[1]
1704 vec_B = points_target[0] - points_target[1]
1706 dist_A = (points_original[0] - points_original[1]).length
1707 dist_B = (points_target[0] - points_target[1]).length
1709 if not (
1710 points_original[0] == points_original[1] or
1711 points_target[0] == points_target[1]
1712 ): # If any vector's length is zero
1714 angle = vec_A.angle(vec_B) / pi
1715 else:
1716 angle = 0
1718 # Set a range of acceptable variation in the connected edges
1719 if dist_B > dist_A * 1.7 * self.join_stretch_factor or \
1720 dist_B < dist_A / 2 / self.join_stretch_factor or \
1721 angle >= 0.15 * self.join_stretch_factor:
1723 merge_actual_vert = False
1724 break
1725 else:
1726 merge_actual_vert = False
1727 except:
1728 self.report({'WARNING'},
1729 "Crosshatch set incorrectly")
1731 if merge_actual_vert:
1732 coords = final_ob_duplicate.data.vertices[i].co
1733 # To avoid problems when taking "-0.000" as a different value as "0.00"
1734 for c in range(len(coords)):
1735 if "%.3f" % coords[c] == "-0.00":
1736 coords[c] = 0
1738 comparison_coords = ["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]]
1740 if comparison_coords in main_object_verts_coords:
1741 # Get the index of the vert with those coords in the main object
1742 main_object_related_vert_idx = main_object_verts_coords.index(comparison_coords)
1744 if self.main_object.data.vertices[main_object_related_vert_idx].select is True or \
1745 self.main_object_selected_verts_count == 0:
1747 ob_surface.data.vertices[i].co = final_ob_duplicate.data.vertices[i].co
1748 ob_surface.data.vertices[i].select = True
1749 crosshatch_verts_to_merge.append(i)
1751 # Make sure the vert in the main object is selected,
1752 # in case it wasn't selected and the "join crosshatch" option is active
1753 self.main_object.data.vertices[main_object_related_vert_idx].select = True
1755 # Delete duplicated object
1756 with bpy.context.temp_override(selected_objects=[final_ob_duplicate]):
1757 bpy.ops.object.delete()
1759 # Join crosshatched surface and main object
1760 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1761 ob_surface.select_set(True)
1762 self.main_object.select_set(True)
1763 bpy.context.view_layer.objects.active = self.main_object
1765 bpy.ops.object.join('INVOKE_REGION_WIN')
1767 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1768 # Perform Remove doubles to merge verts
1769 if not (self.automatic_join is False and self.main_object_selected_verts_count == 0):
1770 bpy.ops.mesh.remove_doubles(threshold=0.0001)
1772 bpy.ops.mesh.select_all(action='DESELECT')
1774 # If the main object has modifiers, turn their "viewport view status"
1775 # to what it was before the forced deactivation above
1776 if len(self.main_object.modifiers) > 0:
1777 for m_idx in range(len(self.main_object.modifiers)):
1778 self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
1780 self.update()
1782 return {'FINISHED'}
1784 def rectangular_surface(self, context):
1785 # Selected edges
1786 all_selected_edges_idx = []
1787 all_selected_verts = []
1788 all_verts_idx = []
1789 for ed in self.main_object.data.edges:
1790 if ed.select:
1791 all_selected_edges_idx.append(ed.index)
1793 # Selected vertices
1794 if not ed.vertices[0] in all_selected_verts:
1795 all_selected_verts.append(self.main_object.data.vertices[ed.vertices[0]])
1796 if not ed.vertices[1] in all_selected_verts:
1797 all_selected_verts.append(self.main_object.data.vertices[ed.vertices[1]])
1799 # All verts (both from each edge) to determine later
1800 # which are at the tips (those not repeated twice)
1801 all_verts_idx.append(ed.vertices[0])
1802 all_verts_idx.append(ed.vertices[1])
1804 # Identify the tips and "middle-vertex" that separates U from V, if there is one
1805 all_chains_tips_idx = []
1806 for v_idx in all_verts_idx:
1807 if all_verts_idx.count(v_idx) < 2:
1808 all_chains_tips_idx.append(v_idx)
1810 edges_connected_to_tips = []
1811 for ed in self.main_object.data.edges:
1812 if (ed.vertices[0] in all_chains_tips_idx or ed.vertices[1] in all_chains_tips_idx) and \
1813 not (ed.vertices[0] in all_verts_idx and ed.vertices[1] in all_verts_idx):
1815 edges_connected_to_tips.append(ed)
1817 # Check closed selections
1818 # List with groups of three verts, where the first element of the pair is
1819 # the unselected vert of a closed selection and the other two elements are the
1820 # selected neighbor verts (it will be useful to determine which selection chain
1821 # the unselected vert belongs to, and determine the "middle-vertex")
1822 single_unselected_verts_and_neighbors = []
1824 # To identify a "closed" selection (a selection that is a closed chain except
1825 # for one vertex) find the vertex in common that have the edges connected to tips.
1826 # If there is a vertex in common, that one is the unselected vert that closes
1827 # the selection or is a "middle-vertex"
1828 single_unselected_verts = []
1829 for ed in edges_connected_to_tips:
1830 for ed_b in edges_connected_to_tips:
1831 if ed != ed_b:
1832 if ed.vertices[0] == ed_b.vertices[0] and \
1833 not self.main_object.data.vertices[ed.vertices[0]].select and \
1834 ed.vertices[0] not in single_unselected_verts:
1836 # The second element is one of the tips of the selected
1837 # vertices of the closed selection
1838 single_unselected_verts_and_neighbors.append(
1839 [ed.vertices[0], ed.vertices[1], ed_b.vertices[1]]
1841 single_unselected_verts.append(ed.vertices[0])
1842 break
1843 elif ed.vertices[0] == ed_b.vertices[1] and \
1844 not self.main_object.data.vertices[ed.vertices[0]].select and \
1845 ed.vertices[0] not in single_unselected_verts:
1847 single_unselected_verts_and_neighbors.append(
1848 [ed.vertices[0], ed.vertices[1], ed_b.vertices[0]]
1850 single_unselected_verts.append(ed.vertices[0])
1851 break
1852 elif ed.vertices[1] == ed_b.vertices[0] and \
1853 not self.main_object.data.vertices[ed.vertices[1]].select and \
1854 ed.vertices[1] not in single_unselected_verts:
1856 single_unselected_verts_and_neighbors.append(
1857 [ed.vertices[1], ed.vertices[0], ed_b.vertices[1]]
1859 single_unselected_verts.append(ed.vertices[1])
1860 break
1861 elif ed.vertices[1] == ed_b.vertices[1] and \
1862 not self.main_object.data.vertices[ed.vertices[1]].select and \
1863 ed.vertices[1] not in single_unselected_verts:
1865 single_unselected_verts_and_neighbors.append(
1866 [ed.vertices[1], ed.vertices[0], ed_b.vertices[0]]
1868 single_unselected_verts.append(ed.vertices[1])
1869 break
1871 middle_vertex_idx = None
1872 tips_to_discard_idx = []
1874 # Check if there is a "middle-vertex", and get its index
1875 for i in range(0, len(single_unselected_verts_and_neighbors)):
1876 actual_chain_verts = self.get_ordered_verts(
1877 self.main_object, all_selected_edges_idx,
1878 all_verts_idx, single_unselected_verts_and_neighbors[i][1],
1879 None, None
1882 if single_unselected_verts_and_neighbors[i][2] != \
1883 actual_chain_verts[len(actual_chain_verts) - 1].index:
1885 middle_vertex_idx = single_unselected_verts_and_neighbors[i][0]
1886 tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][1])
1887 tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][2])
1889 # List with pairs of verts that belong to the tips of each selection chain (row)
1890 verts_tips_same_chain_idx = []
1891 if len(all_chains_tips_idx) >= 2:
1892 checked_v = []
1893 for i in range(0, len(all_chains_tips_idx)):
1894 if all_chains_tips_idx[i] not in checked_v:
1895 v_chain = self.get_ordered_verts(
1896 self.main_object, all_selected_edges_idx,
1897 all_verts_idx, all_chains_tips_idx[i],
1898 middle_vertex_idx, None
1901 verts_tips_same_chain_idx.append([v_chain[0].index, v_chain[len(v_chain) - 1].index])
1903 checked_v.append(v_chain[0].index)
1904 checked_v.append(v_chain[len(v_chain) - 1].index)
1906 # Selection tips (vertices).
1907 verts_tips_parsed_idx = []
1908 if len(all_chains_tips_idx) >= 2:
1909 for spec_v_idx in all_chains_tips_idx:
1910 if (spec_v_idx not in tips_to_discard_idx):
1911 verts_tips_parsed_idx.append(spec_v_idx)
1913 # Identify the type of selection made by the user
1914 if middle_vertex_idx is not None:
1915 # If there are 4 tips (two selection chains), and
1916 # there is only one single unselected vert (the middle vert)
1917 if len(all_chains_tips_idx) == 4 and len(single_unselected_verts_and_neighbors) == 1:
1918 selection_type = "TWO_CONNECTED"
1919 else:
1920 # The type of the selection was not identified, the script stops.
1921 self.report({'WARNING'}, "The selection isn't valid.")
1923 self.stopping_errors = True
1925 return{'CANCELLED'}
1926 else:
1927 if len(all_chains_tips_idx) == 2: # If there are 2 tips
1928 selection_type = "SINGLE"
1929 elif len(all_chains_tips_idx) == 4: # If there are 4 tips
1930 selection_type = "TWO_NOT_CONNECTED"
1931 elif len(all_chains_tips_idx) == 0:
1932 if len(self.main_splines.data.splines) > 1:
1933 selection_type = "NO_SELECTION"
1934 else:
1935 # If the selection was not identified and there is only one stroke,
1936 # there's no possibility to build a surface, so the script is interrupted
1937 self.report({'WARNING'}, "The selection isn't valid.")
1939 self.stopping_errors = True
1941 return{'CANCELLED'}
1942 else:
1943 # The type of the selection was not identified, the script stops
1944 self.report({'WARNING'}, "The selection isn't valid.")
1946 self.stopping_errors = True
1948 return{'CANCELLED'}
1950 # If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script
1951 if selection_type == "TWO_NOT_CONNECTED" and len(self.main_splines.data.splines) == 1:
1952 self.report({'WARNING'},
1953 "At least two strokes are needed when there are two not connected selections")
1955 self.stopping_errors = True
1957 return{'CANCELLED'}
1959 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1961 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1962 self.main_splines.select_set(True)
1963 bpy.context.view_layer.objects.active = self.main_splines
1965 # Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out
1966 bpy.ops.object.editmode_toggle('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.curve.smooth('INVOKE_REGION_WIN')
1972 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1973 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1975 self.selection_U_exists = False
1976 self.selection_U2_exists = False
1977 self.selection_V_exists = False
1978 self.selection_V2_exists = False
1980 self.selection_U_is_closed = False
1981 self.selection_U2_is_closed = False
1982 self.selection_V_is_closed = False
1983 self.selection_V2_is_closed = False
1985 # Define what vertices are at the tips of each selection and are not the middle-vertex
1986 if selection_type == "TWO_CONNECTED":
1987 self.selection_U_exists = True
1988 self.selection_V_exists = True
1990 closing_vert_U_idx = None
1991 closing_vert_V_idx = None
1992 closing_vert_U2_idx = None
1993 closing_vert_V2_idx = None
1995 # Determine which selection is Selection-U and which is Selection-V
1996 points_A = []
1997 points_B = []
1998 points_first_stroke_tips = []
2000 points_A.append(
2001 self.main_object.matrix_world @ self.main_object.data.vertices[verts_tips_parsed_idx[0]].co
2003 points_A.append(
2004 self.main_object.matrix_world @ self.main_object.data.vertices[middle_vertex_idx].co
2006 points_B.append(
2007 self.main_object.matrix_world @ self.main_object.data.vertices[verts_tips_parsed_idx[1]].co
2009 points_B.append(
2010 self.main_object.matrix_world @ self.main_object.data.vertices[middle_vertex_idx].co
2012 points_first_stroke_tips.append(
2013 self.main_splines.data.splines[0].bezier_points[0].co
2015 points_first_stroke_tips.append(
2016 self.main_splines.data.splines[0].bezier_points[
2017 len(self.main_splines.data.splines[0].bezier_points) - 1
2018 ].co
2021 angle_A = self.orientation_difference(points_A, points_first_stroke_tips)
2022 angle_B = self.orientation_difference(points_B, points_first_stroke_tips)
2024 if angle_A < angle_B:
2025 first_vert_U_idx = verts_tips_parsed_idx[0]
2026 first_vert_V_idx = verts_tips_parsed_idx[1]
2027 else:
2028 first_vert_U_idx = verts_tips_parsed_idx[1]
2029 first_vert_V_idx = verts_tips_parsed_idx[0]
2031 elif selection_type == "SINGLE" or selection_type == "TWO_NOT_CONNECTED":
2032 first_sketched_point_first_stroke_co = self.main_splines.data.splines[0].bezier_points[0].co
2033 last_sketched_point_first_stroke_co = \
2034 self.main_splines.data.splines[0].bezier_points[
2035 len(self.main_splines.data.splines[0].bezier_points) - 1
2036 ].co
2037 first_sketched_point_last_stroke_co = \
2038 self.main_splines.data.splines[
2039 len(self.main_splines.data.splines) - 1
2040 ].bezier_points[0].co
2041 if len(self.main_splines.data.splines) > 1:
2042 first_sketched_point_second_stroke_co = self.main_splines.data.splines[1].bezier_points[0].co
2043 last_sketched_point_second_stroke_co = \
2044 self.main_splines.data.splines[1].bezier_points[
2045 len(self.main_splines.data.splines[1].bezier_points) - 1
2046 ].co
2048 single_unselected_neighbors = [] # Only the neighbors of the single unselected verts
2049 for verts_neig_idx in single_unselected_verts_and_neighbors:
2050 single_unselected_neighbors.append(verts_neig_idx[1])
2051 single_unselected_neighbors.append(verts_neig_idx[2])
2053 all_chains_tips_and_middle_vert = []
2054 for v_idx in all_chains_tips_idx:
2055 if v_idx not in single_unselected_neighbors:
2056 all_chains_tips_and_middle_vert.append(v_idx)
2058 all_chains_tips_and_middle_vert += single_unselected_verts
2060 all_participating_verts = all_chains_tips_and_middle_vert + all_verts_idx
2062 # The tip of the selected vertices nearest to the first point of the first sketched stroke
2063 nearest_tip_to_first_st_first_pt_idx, shortest_distance_to_first_stroke = \
2064 self.shortest_distance(
2065 self.main_object,
2066 first_sketched_point_first_stroke_co,
2067 all_chains_tips_and_middle_vert
2069 # If the nearest tip is not from a closed selection, get the opposite tip vertex index
2070 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2071 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2073 nearest_tip_to_first_st_first_pt_opposite_idx = \
2074 self.opposite_tip(
2075 nearest_tip_to_first_st_first_pt_idx,
2076 verts_tips_same_chain_idx
2078 # The tip of the selected vertices nearest to the last point of the first sketched stroke
2079 nearest_tip_to_first_st_last_pt_idx, _temp_dist = \
2080 self.shortest_distance(
2081 self.main_object,
2082 last_sketched_point_first_stroke_co,
2083 all_chains_tips_and_middle_vert
2085 # The tip of the selected vertices nearest to the first point of the last sketched stroke
2086 nearest_tip_to_last_st_first_pt_idx, shortest_distance_to_last_stroke = \
2087 self.shortest_distance(
2088 self.main_object,
2089 first_sketched_point_last_stroke_co,
2090 all_chains_tips_and_middle_vert
2092 if len(self.main_splines.data.splines) > 1:
2093 # The selected vertex nearest to the first point of the second sketched stroke
2094 # (This will be useful to determine the direction of the closed
2095 # selection V when extruding along strokes)
2096 nearest_vert_to_second_st_first_pt_idx, _temp_dist = \
2097 self.shortest_distance(
2098 self.main_object,
2099 first_sketched_point_second_stroke_co,
2100 all_verts_idx
2102 # The selected vertex nearest to the first point of the second sketched stroke
2103 # (This will be useful to determine the direction of the closed
2104 # selection V2 when extruding along strokes)
2105 nearest_vert_to_second_st_last_pt_idx, _temp_dist = \
2106 self.shortest_distance(
2107 self.main_object,
2108 last_sketched_point_second_stroke_co,
2109 all_verts_idx
2111 # Determine if the single selection will be treated as U or as V
2112 edges_sum = 0
2113 for i in all_selected_edges_idx:
2114 edges_sum += (
2115 (self.main_object.matrix_world @
2116 self.main_object.data.vertices[self.main_object.data.edges[i].vertices[0]].co) -
2117 (self.main_object.matrix_world @
2118 self.main_object.data.vertices[self.main_object.data.edges[i].vertices[1]].co)
2119 ).length
2121 average_edge_length = edges_sum / len(all_selected_edges_idx)
2123 # Get shortest distance from the first point of the last stroke to any participating vertex
2124 _temp_idx, shortest_distance_to_last_stroke = \
2125 self.shortest_distance(
2126 self.main_object,
2127 first_sketched_point_last_stroke_co,
2128 all_participating_verts
2130 # If the beginning of the first stroke is near enough, and its orientation
2131 # difference with the first edge of the nearest selection chain is not too high,
2132 # interpret things as an "extrude along strokes" instead of "extrude through strokes"
2133 if shortest_distance_to_first_stroke < average_edge_length / 4 and \
2134 shortest_distance_to_last_stroke < average_edge_length and \
2135 len(self.main_splines.data.splines) > 1:
2137 self.selection_U_exists = False
2138 self.selection_V_exists = True
2139 # If the first selection is not closed
2140 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2141 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2142 self.selection_V_is_closed = False
2143 closing_vert_U_idx = None
2144 closing_vert_U2_idx = None
2145 closing_vert_V_idx = None
2146 closing_vert_V2_idx = None
2148 first_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
2150 if selection_type == "TWO_NOT_CONNECTED":
2151 self.selection_V2_exists = True
2153 first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2154 else:
2155 self.selection_V_is_closed = True
2156 closing_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
2158 # Get the neighbors of the first (unselected) vert of the closed selection U.
2159 vert_neighbors = []
2160 for verts in single_unselected_verts_and_neighbors:
2161 if verts[0] == nearest_tip_to_first_st_first_pt_idx:
2162 vert_neighbors.append(verts[1])
2163 vert_neighbors.append(verts[2])
2164 break
2166 verts_V = self.get_ordered_verts(
2167 self.main_object, all_selected_edges_idx,
2168 all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
2171 for i in range(0, len(verts_V)):
2172 if verts_V[i].index == nearest_vert_to_second_st_first_pt_idx:
2173 # If the vertex nearest to the first point of the second stroke
2174 # is in the first half of the selected verts
2175 if i >= len(verts_V) / 2:
2176 first_vert_V_idx = vert_neighbors[1]
2177 break
2178 else:
2179 first_vert_V_idx = vert_neighbors[0]
2180 break
2182 if selection_type == "TWO_NOT_CONNECTED":
2183 self.selection_V2_exists = True
2184 # If the second selection is not closed
2185 if nearest_tip_to_first_st_last_pt_idx not in single_unselected_verts or \
2186 nearest_tip_to_first_st_last_pt_idx == middle_vertex_idx:
2188 self.selection_V2_is_closed = False
2189 closing_vert_V2_idx = None
2190 first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2192 else:
2193 self.selection_V2_is_closed = True
2194 closing_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2196 # Get the neighbors of the first (unselected) vert of the closed selection U
2197 vert_neighbors = []
2198 for verts in single_unselected_verts_and_neighbors:
2199 if verts[0] == nearest_tip_to_first_st_last_pt_idx:
2200 vert_neighbors.append(verts[1])
2201 vert_neighbors.append(verts[2])
2202 break
2204 verts_V2 = self.get_ordered_verts(
2205 self.main_object, all_selected_edges_idx,
2206 all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
2209 for i in range(0, len(verts_V2)):
2210 if verts_V2[i].index == nearest_vert_to_second_st_last_pt_idx:
2211 # If the vertex nearest to the first point of the second stroke
2212 # is in the first half of the selected verts
2213 if i >= len(verts_V2) / 2:
2214 first_vert_V2_idx = vert_neighbors[1]
2215 break
2216 else:
2217 first_vert_V2_idx = vert_neighbors[0]
2218 break
2219 else:
2220 self.selection_V2_exists = False
2222 else:
2223 self.selection_U_exists = True
2224 self.selection_V_exists = False
2225 # If the first selection is not closed
2226 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2227 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2228 self.selection_U_is_closed = False
2229 closing_vert_U_idx = None
2231 points_tips = []
2232 points_tips.append(
2233 self.main_object.matrix_world @
2234 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
2236 points_tips.append(
2237 self.main_object.matrix_world @
2238 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_opposite_idx].co
2240 points_first_stroke_tips = []
2241 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
2242 points_first_stroke_tips.append(
2243 self.main_splines.data.splines[0].bezier_points[
2244 len(self.main_splines.data.splines[0].bezier_points) - 1
2245 ].co
2247 vec_A = points_tips[0] - points_tips[1]
2248 vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
2250 # Compare the direction of the selection and the first
2251 # grease pencil stroke to determine which is the "first" vertex of the selection
2252 if vec_A.dot(vec_B) < 0:
2253 first_vert_U_idx = nearest_tip_to_first_st_first_pt_opposite_idx
2254 else:
2255 first_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
2257 else:
2258 self.selection_U_is_closed = True
2259 closing_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
2261 # Get the neighbors of the first (unselected) vert of the closed selection U
2262 vert_neighbors = []
2263 for verts in single_unselected_verts_and_neighbors:
2264 if verts[0] == nearest_tip_to_first_st_first_pt_idx:
2265 vert_neighbors.append(verts[1])
2266 vert_neighbors.append(verts[2])
2267 break
2269 points_first_and_neighbor = []
2270 points_first_and_neighbor.append(
2271 self.main_object.matrix_world @
2272 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
2274 points_first_and_neighbor.append(
2275 self.main_object.matrix_world @
2276 self.main_object.data.vertices[vert_neighbors[0]].co
2278 points_first_stroke_tips = []
2279 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
2280 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[1].co)
2282 vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
2283 vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
2285 # Compare the direction of the selection and the first grease pencil stroke to
2286 # determine which is the vertex neighbor to the first vertex (unselected) of
2287 # the closed selection. This will determine the direction of the closed selection
2288 if vec_A.dot(vec_B) < 0:
2289 first_vert_U_idx = vert_neighbors[1]
2290 else:
2291 first_vert_U_idx = vert_neighbors[0]
2293 if selection_type == "TWO_NOT_CONNECTED":
2294 self.selection_U2_exists = True
2295 # If the second selection is not closed
2296 if nearest_tip_to_last_st_first_pt_idx not in single_unselected_verts or \
2297 nearest_tip_to_last_st_first_pt_idx == middle_vertex_idx:
2299 self.selection_U2_is_closed = False
2300 closing_vert_U2_idx = None
2301 first_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
2302 else:
2303 self.selection_U2_is_closed = True
2304 closing_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
2306 # Get the neighbors of the first (unselected) vert of the closed selection U
2307 vert_neighbors = []
2308 for verts in single_unselected_verts_and_neighbors:
2309 if verts[0] == nearest_tip_to_last_st_first_pt_idx:
2310 vert_neighbors.append(verts[1])
2311 vert_neighbors.append(verts[2])
2312 break
2314 points_first_and_neighbor = []
2315 points_first_and_neighbor.append(
2316 self.main_object.matrix_world @
2317 self.main_object.data.vertices[nearest_tip_to_last_st_first_pt_idx].co
2319 points_first_and_neighbor.append(
2320 self.main_object.matrix_world @
2321 self.main_object.data.vertices[vert_neighbors[0]].co
2323 points_last_stroke_tips = []
2324 points_last_stroke_tips.append(
2325 self.main_splines.data.splines[
2326 len(self.main_splines.data.splines) - 1
2327 ].bezier_points[0].co
2329 points_last_stroke_tips.append(
2330 self.main_splines.data.splines[
2331 len(self.main_splines.data.splines) - 1
2332 ].bezier_points[1].co
2334 vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
2335 vec_B = points_last_stroke_tips[0] - points_last_stroke_tips[1]
2337 # Compare the direction of the selection and the last grease pencil stroke to
2338 # determine which is the vertex neighbor to the first vertex (unselected) of
2339 # the closed selection. This will determine the direction of the closed selection
2340 if vec_A.dot(vec_B) < 0:
2341 first_vert_U2_idx = vert_neighbors[1]
2342 else:
2343 first_vert_U2_idx = vert_neighbors[0]
2344 else:
2345 self.selection_U2_exists = False
2347 elif selection_type == "NO_SELECTION":
2348 self.selection_U_exists = False
2349 self.selection_V_exists = False
2351 # Get an ordered list of the vertices of Selection-U
2352 verts_ordered_U = []
2353 if self.selection_U_exists:
2354 verts_ordered_U = self.get_ordered_verts(
2355 self.main_object, all_selected_edges_idx,
2356 all_verts_idx, first_vert_U_idx,
2357 middle_vertex_idx, closing_vert_U_idx
2360 # Get an ordered list of the vertices of Selection-U2
2361 verts_ordered_U2 = []
2362 if self.selection_U2_exists:
2363 verts_ordered_U2 = self.get_ordered_verts(
2364 self.main_object, all_selected_edges_idx,
2365 all_verts_idx, first_vert_U2_idx,
2366 middle_vertex_idx, closing_vert_U2_idx
2369 # Get an ordered list of the vertices of Selection-V
2370 verts_ordered_V = []
2371 if self.selection_V_exists:
2372 verts_ordered_V = self.get_ordered_verts(
2373 self.main_object, all_selected_edges_idx,
2374 all_verts_idx, first_vert_V_idx,
2375 middle_vertex_idx, closing_vert_V_idx
2377 verts_ordered_V_indices = [x.index for x in verts_ordered_V]
2379 # Get an ordered list of the vertices of Selection-V2
2380 verts_ordered_V2 = []
2381 if self.selection_V2_exists:
2382 verts_ordered_V2 = self.get_ordered_verts(
2383 self.main_object, all_selected_edges_idx,
2384 all_verts_idx, first_vert_V2_idx,
2385 middle_vertex_idx, closing_vert_V2_idx
2388 # Check if when there are two-not-connected selections both have the same
2389 # number of verts. If not terminate the script
2390 if ((self.selection_U2_exists and len(verts_ordered_U) != len(verts_ordered_U2)) or
2391 (self.selection_V2_exists and len(verts_ordered_V) != len(verts_ordered_V2))):
2392 # Display a warning
2393 self.report({'WARNING'}, "Both selections must have the same number of edges")
2395 self.stopping_errors = True
2397 return{'CANCELLED'}
2399 # Calculate edges U proportions
2400 # Sum selected edges U lengths
2401 edges_lengths_U = []
2402 edges_lengths_sum_U = 0
2404 if self.selection_U_exists:
2405 edges_lengths_U, edges_lengths_sum_U = self.get_chain_length(
2406 self.main_object,
2407 verts_ordered_U
2409 if self.selection_U2_exists:
2410 edges_lengths_U2, edges_lengths_sum_U2 = self.get_chain_length(
2411 self.main_object,
2412 verts_ordered_U2
2414 # Sum selected edges V lengths
2415 edges_lengths_V = []
2416 edges_lengths_sum_V = 0
2418 if self.selection_V_exists:
2419 edges_lengths_V, edges_lengths_sum_V = self.get_chain_length(
2420 self.main_object,
2421 verts_ordered_V
2423 if self.selection_V2_exists:
2424 edges_lengths_V2, edges_lengths_sum_V2 = self.get_chain_length(
2425 self.main_object,
2426 verts_ordered_V2
2429 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2430 bpy.ops.curve.subdivide('INVOKE_REGION_WIN',
2431 number_cuts=bpy.context.scene.bsurfaces.SURFSK_precision)
2432 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2434 # Proportions U
2435 edges_proportions_U = []
2436 edges_proportions_U = self.get_edges_proportions(
2437 edges_lengths_U, edges_lengths_sum_U,
2438 self.selection_U_exists, self.edges_U
2440 verts_count_U = len(edges_proportions_U) + 1
2442 if self.selection_U2_exists:
2443 edges_proportions_U2 = []
2444 edges_proportions_U2 = self.get_edges_proportions(
2445 edges_lengths_U2, edges_lengths_sum_U2,
2446 self.selection_U2_exists, self.edges_V
2449 # Proportions V
2450 edges_proportions_V = []
2451 edges_proportions_V = self.get_edges_proportions(
2452 edges_lengths_V, edges_lengths_sum_V,
2453 self.selection_V_exists, self.edges_V
2456 if self.selection_V2_exists:
2457 edges_proportions_V2 = []
2458 edges_proportions_V2 = self.get_edges_proportions(
2459 edges_lengths_V2, edges_lengths_sum_V2,
2460 self.selection_V2_exists, self.edges_V
2463 # Cyclic Follow: simplify sketched curves, make them Cyclic, and complete
2464 # the actual sketched curves with a "closing segment"
2465 if self.cyclic_follow and not self.selection_V_exists and not \
2466 ((self.selection_U_exists and not self.selection_U_is_closed) or
2467 (self.selection_U2_exists and not self.selection_U2_is_closed)):
2469 simplified_spline_coords = []
2470 simplified_curve = []
2471 ob_simplified_curve = []
2472 splines_first_v_co = []
2473 for i in range(len(self.main_splines.data.splines)):
2474 # Create a curve object for the actual spline "cyclic extension"
2475 simplified_curve.append(bpy.data.curves.new('SURFSKIO_simpl_crv', 'CURVE'))
2476 ob_simplified_curve.append(bpy.data.objects.new('SURFSKIO_simpl_crv', simplified_curve[i]))
2477 bpy.context.collection.objects.link(ob_simplified_curve[i])
2479 simplified_curve[i].dimensions = "3D"
2481 spline_coords = []
2482 for bp in self.main_splines.data.splines[i].bezier_points:
2483 spline_coords.append(bp.co)
2485 # Simplification
2486 simplified_spline_coords.append(self.simplify_spline(spline_coords, 5))
2488 # Get the coordinates of the first vert of the actual spline
2489 splines_first_v_co.append(simplified_spline_coords[i][0])
2491 # Generate the spline
2492 spline = simplified_curve[i].splines.new('BEZIER')
2493 # less one because one point is added when the spline is created
2494 spline.bezier_points.add(len(simplified_spline_coords[i]) - 1)
2495 for p in range(0, len(simplified_spline_coords[i])):
2496 spline.bezier_points[p].co = simplified_spline_coords[i][p]
2498 spline.use_cyclic_u = True
2500 spline_bp_count = len(spline.bezier_points)
2502 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2503 ob_simplified_curve[i].select_set(True)
2504 bpy.context.view_layer.objects.active = ob_simplified_curve[i]
2506 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2507 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
2508 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2509 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
2510 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2512 # Select the "closing segment", and subdivide it
2513 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_control_point = True
2514 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_left_handle = True
2515 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_right_handle = True
2517 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_control_point = True
2518 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_left_handle = True
2519 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_right_handle = True
2521 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2522 segments = sqrt(
2523 (ob_simplified_curve[i].data.splines[0].bezier_points[0].co -
2524 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].co).length /
2525 self.average_gp_segment_length
2527 for t in range(2):
2528 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=int(segments))
2530 # Delete the other vertices and make it non-cyclic to
2531 # keep only the needed verts of the "closing segment"
2532 bpy.ops.curve.select_all(action='INVERT')
2533 bpy.ops.curve.delete(type='VERT')
2534 ob_simplified_curve[i].data.splines[0].use_cyclic_u = False
2535 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2537 # Add the points of the "closing segment" to the original curve from grease pencil stroke
2538 first_new_index = len(self.main_splines.data.splines[i].bezier_points)
2539 self.main_splines.data.splines[i].bezier_points.add(
2540 len(ob_simplified_curve[i].data.splines[0].bezier_points) - 1
2542 for t in range(1, len(ob_simplified_curve[i].data.splines[0].bezier_points)):
2543 self.main_splines.data.splines[i].bezier_points[t - 1 + first_new_index].co = \
2544 ob_simplified_curve[i].data.splines[0].bezier_points[t].co
2546 # Delete the temporal curve
2547 with bpy.context.temp_override(selected_objects=[ob_simplified_curve[i]]):
2548 bpy.ops.object.delete()
2550 # Get the coords of the points distributed along the sketched strokes,
2551 # with proportions-U of the first selection
2552 pts_on_strokes_with_proportions_U = self.distribute_pts(
2553 self.main_splines.data.splines,
2554 edges_proportions_U
2556 sketched_splines_parsed = []
2558 if self.selection_U2_exists:
2559 # Initialize the multidimensional list with the proportions of all the segments
2560 proportions_loops_crossing_strokes = []
2561 for i in range(len(pts_on_strokes_with_proportions_U)):
2562 proportions_loops_crossing_strokes.append([])
2564 for t in range(len(pts_on_strokes_with_proportions_U[0])):
2565 proportions_loops_crossing_strokes[i].append(None)
2567 # Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U
2568 for lp in range(len(pts_on_strokes_with_proportions_U[0])):
2569 loop_segments_lengths = []
2571 for st in range(len(pts_on_strokes_with_proportions_U)):
2572 # When on the first stroke, add the segment from the selection to the first stroke
2573 if st == 0:
2574 loop_segments_lengths.append(
2575 ((self.main_object.matrix_world @ verts_ordered_U[lp].co) -
2576 pts_on_strokes_with_proportions_U[0][lp]).length
2578 # For all strokes except for the last, calculate the distance
2579 # from the actual stroke to the next
2580 if st != len(pts_on_strokes_with_proportions_U) - 1:
2581 loop_segments_lengths.append(
2582 (pts_on_strokes_with_proportions_U[st][lp] -
2583 pts_on_strokes_with_proportions_U[st + 1][lp]).length
2585 # When on the last stroke, add the segments
2586 # from the last stroke to the second selection
2587 if st == len(pts_on_strokes_with_proportions_U) - 1:
2588 loop_segments_lengths.append(
2589 (pts_on_strokes_with_proportions_U[st][lp] -
2590 (self.main_object.matrix_world @ verts_ordered_U2[lp].co)).length
2592 # Calculate full loop length
2593 loop_seg_lengths_sum = 0
2594 for i in range(len(loop_segments_lengths)):
2595 loop_seg_lengths_sum += loop_segments_lengths[i]
2597 # Fill the multidimensional list with the proportions of all the segments
2598 for st in range(len(pts_on_strokes_with_proportions_U)):
2599 proportions_loops_crossing_strokes[st][lp] = \
2600 loop_segments_lengths[st] / loop_seg_lengths_sum
2602 # Calculate proportions for each stroke
2603 for st in range(len(pts_on_strokes_with_proportions_U)):
2604 actual_stroke_spline = []
2605 # Needs to be a list for the "distribute_pts" method
2606 actual_stroke_spline.append(self.main_splines.data.splines[st])
2608 # Calculate the proportions for the actual stroke.
2609 actual_edges_proportions_U = []
2610 for i in range(len(edges_proportions_U)):
2611 proportions_sum = 0
2613 # Sum the proportions of this loop up to the actual.
2614 for t in range(0, st + 1):
2615 proportions_sum += proportions_loops_crossing_strokes[t][i]
2616 # i + 1, because proportions_loops_crossing_strokes refers to loops,
2617 # and the proportions refer to edges, so we start at the element 1
2618 # of proportions_loops_crossing_strokes instead of element 0
2619 actual_edges_proportions_U.append(
2620 edges_proportions_U[i] -
2621 ((edges_proportions_U[i] - edges_proportions_U2[i]) * proportions_sum)
2623 points_actual_spline = self.distribute_pts(actual_stroke_spline, actual_edges_proportions_U)
2624 sketched_splines_parsed.append(points_actual_spline[0])
2625 else:
2626 sketched_splines_parsed = pts_on_strokes_with_proportions_U
2628 # If the selection type is "TWO_NOT_CONNECTED" replace the
2629 # points of the last spline with the points in the "target" selection
2630 if selection_type == "TWO_NOT_CONNECTED":
2631 if self.selection_U2_exists:
2632 for i in range(0, len(sketched_splines_parsed[len(sketched_splines_parsed) - 1])):
2633 sketched_splines_parsed[len(sketched_splines_parsed) - 1][i] = \
2634 self.main_object.matrix_world @ verts_ordered_U2[i].co
2636 # Create temporary curves along the "control-points" found
2637 # on the sketched curves and the mesh selection
2638 mesh_ctrl_pts_name = "SURFSKIO_ctrl_pts"
2639 me = bpy.data.meshes.new(mesh_ctrl_pts_name)
2640 ob_ctrl_pts = bpy.data.objects.new(mesh_ctrl_pts_name, me)
2641 ob_ctrl_pts.data = me
2642 bpy.context.collection.objects.link(ob_ctrl_pts)
2644 cyclic_loops_U = []
2645 first_verts = []
2646 second_verts = []
2647 last_verts = []
2649 for i in range(0, verts_count_U):
2650 vert_num_in_spline = 1
2652 if self.selection_U_exists:
2653 ob_ctrl_pts.data.vertices.add(1)
2654 last_v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
2655 last_v.co = self.main_object.matrix_world @ verts_ordered_U[i].co
2657 vert_num_in_spline += 1
2659 for t in range(0, len(sketched_splines_parsed)):
2660 ob_ctrl_pts.data.vertices.add(1)
2661 v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
2662 v.co = sketched_splines_parsed[t][i]
2664 if vert_num_in_spline > 1:
2665 ob_ctrl_pts.data.edges.add(1)
2666 ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[0] = \
2667 len(ob_ctrl_pts.data.vertices) - 2
2668 ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[1] = \
2669 len(ob_ctrl_pts.data.vertices) - 1
2671 if t == 0:
2672 first_verts.append(v.index)
2674 if t == 1:
2675 second_verts.append(v.index)
2677 if t == len(sketched_splines_parsed) - 1:
2678 last_verts.append(v.index)
2680 last_v = v
2681 vert_num_in_spline += 1
2683 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2684 ob_ctrl_pts.select_set(True)
2685 bpy.context.view_layer.objects.active = ob_ctrl_pts
2687 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2688 bpy.ops.mesh.select_all(action='DESELECT')
2689 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2691 # Determine which loops-U will be "Cyclic"
2692 for i in range(0, len(first_verts)):
2693 # When there is Cyclic Cross there is no need of
2694 # Automatic Join, (and there are at least three strokes)
2695 if self.automatic_join and not self.cyclic_cross and \
2696 selection_type != "TWO_CONNECTED" and len(self.main_splines.data.splines) >= 3:
2698 v = ob_ctrl_pts.data.vertices
2699 first_point_co = v[first_verts[i]].co
2700 second_point_co = v[second_verts[i]].co
2701 last_point_co = v[last_verts[i]].co
2703 # Coordinates of the point in the center of both the first and last verts.
2704 verts_center_co = [
2705 (first_point_co[0] + last_point_co[0]) / 2,
2706 (first_point_co[1] + last_point_co[1]) / 2,
2707 (first_point_co[2] + last_point_co[2]) / 2
2709 vec_A = second_point_co - first_point_co
2710 vec_B = second_point_co - Vector(verts_center_co)
2712 # Calculate the length of the first segment of the loop,
2713 # and the length it would have after moving the first vert
2714 # to the middle position between first and last
2715 length_original = (second_point_co - first_point_co).length
2716 length_target = (second_point_co - Vector(verts_center_co)).length
2718 angle = vec_A.angle(vec_B) / pi
2720 # If the target length doesn't stretch too much, and the
2721 # its angle doesn't change to much either
2722 if length_target <= length_original * 1.03 * self.join_stretch_factor and \
2723 angle <= 0.008 * self.join_stretch_factor and not self.selection_U_exists:
2725 cyclic_loops_U.append(True)
2726 # Move the first vert to the center coordinates
2727 ob_ctrl_pts.data.vertices[first_verts[i]].co = verts_center_co
2728 # Select the last verts from Cyclic loops, for later deletion all at once
2729 v[last_verts[i]].select = True
2730 else:
2731 cyclic_loops_U.append(False)
2732 else:
2733 # If "Cyclic Cross" is active then "all" crossing curves become cyclic
2734 if self.cyclic_cross and not self.selection_U_exists and not \
2735 ((self.selection_V_exists and not self.selection_V_is_closed) or
2736 (self.selection_V2_exists and not self.selection_V2_is_closed)):
2738 cyclic_loops_U.append(True)
2739 else:
2740 cyclic_loops_U.append(False)
2742 # The cyclic_loops_U list needs to be reversed.
2743 cyclic_loops_U.reverse()
2745 # Delete the previously selected (last_)verts.
2746 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2747 bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
2748 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2750 # Create curves from control points.
2751 bpy.ops.object.convert('INVOKE_REGION_WIN', target='CURVE', keep_original=False)
2752 ob_curves_surf = bpy.context.view_layer.objects.active
2753 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2754 bpy.ops.curve.spline_type_set('INVOKE_REGION_WIN', type='BEZIER')
2755 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2757 # Make Cyclic the splines designated as Cyclic.
2758 for i in range(0, len(cyclic_loops_U)):
2759 ob_curves_surf.data.splines[i].use_cyclic_u = cyclic_loops_U[i]
2761 # Get the coords of all points on first loop-U, for later comparison with its
2762 # subdivided version, to know which points of the loops-U are crossed by the
2763 # original strokes. The indices will be the same for the other loops-U
2764 if self.loops_on_strokes:
2765 coords_loops_U_control_points = []
2766 for p in ob_ctrl_pts.data.splines[0].bezier_points:
2767 coords_loops_U_control_points.append(["%.4f" % p.co[0], "%.4f" % p.co[1], "%.4f" % p.co[2]])
2769 tuple(coords_loops_U_control_points)
2771 # Calculate number of edges-V in case option "Loops on strokes" is active or inactive
2772 if self.loops_on_strokes and not self.selection_V_exists:
2773 edges_V_count = len(self.main_splines.data.splines) * self.edges_V
2774 else:
2775 edges_V_count = len(edges_proportions_V)
2777 # The Follow precision will vary depending on the number of Follow face-loops
2778 precision_multiplier = round(2 + (edges_V_count / 15))
2779 curve_cuts = bpy.context.scene.bsurfaces.SURFSK_precision * precision_multiplier
2781 # Subdivide the curves
2782 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=curve_cuts)
2784 # The verts position shifting that happens with splines subdivision.
2785 # For later reorder splines points
2786 verts_position_shift = curve_cuts + 1
2787 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2789 # Reorder coordinates of the points of each spline to put the first point of
2790 # the spline starting at the position it was the first point before sudividing
2791 # the curve. And make a new curve object per spline (to handle memory better later)
2792 splines_U_objects = []
2793 for i in range(len(ob_curves_surf.data.splines)):
2794 spline_U_curve = bpy.data.curves.new('SURFSKIO_spline_U_' + str(i), 'CURVE')
2795 ob_spline_U = bpy.data.objects.new('SURFSKIO_spline_U_' + str(i), spline_U_curve)
2796 bpy.context.collection.objects.link(ob_spline_U)
2798 spline_U_curve.dimensions = "3D"
2800 # Add points to the spline in the new curve object
2801 ob_spline_U.data.splines.new('BEZIER')
2802 for t in range(len(ob_curves_surf.data.splines[i].bezier_points)):
2803 if cyclic_loops_U[i] is True and not self.selection_U_exists: # If the loop is cyclic
2804 if t + verts_position_shift <= len(ob_curves_surf.data.splines[i].bezier_points) - 1:
2805 point_index = t + verts_position_shift
2806 else:
2807 point_index = t + verts_position_shift - len(ob_curves_surf.data.splines[i].bezier_points)
2808 else:
2809 point_index = t
2810 # to avoid adding the first point since it's added when the spline is created
2811 if t > 0:
2812 ob_spline_U.data.splines[0].bezier_points.add(1)
2813 ob_spline_U.data.splines[0].bezier_points[t].co = \
2814 ob_curves_surf.data.splines[i].bezier_points[point_index].co
2816 if cyclic_loops_U[i] is True and not self.selection_U_exists: # If the loop is cyclic
2817 # Add a last point at the same location as the first one
2818 ob_spline_U.data.splines[0].bezier_points.add(1)
2819 ob_spline_U.data.splines[0].bezier_points[len(ob_spline_U.data.splines[0].bezier_points) - 1].co = \
2820 ob_spline_U.data.splines[0].bezier_points[0].co
2821 else:
2822 ob_spline_U.data.splines[0].use_cyclic_u = False
2824 splines_U_objects.append(ob_spline_U)
2825 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2826 ob_spline_U.select_set(True)
2827 bpy.context.view_layer.objects.active = ob_spline_U
2829 # When option "Loops on strokes" is active each "Cross" loop will have
2830 # its own proportions according to where the original strokes "touch" them
2831 if self.loops_on_strokes:
2832 # Get the indices of points where the original strokes "touch" loops-U
2833 points_U_crossed_by_strokes = []
2834 for i in range(len(splines_U_objects[0].data.splines[0].bezier_points)):
2835 bp = splines_U_objects[0].data.splines[0].bezier_points[i]
2836 if ["%.4f" % bp.co[0], "%.4f" % bp.co[1], "%.4f" % bp.co[2]] in coords_loops_U_control_points:
2837 points_U_crossed_by_strokes.append(i)
2839 # Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke
2840 edge_order_number_for_splines = {}
2841 if self.selection_V_exists:
2842 # For two-connected selections add a first hypothetic stroke at the beginning.
2843 if selection_type == "TWO_CONNECTED":
2844 edge_order_number_for_splines[0] = 0
2846 for i in range(len(self.main_splines.data.splines)):
2847 sp = self.main_splines.data.splines[i]
2848 v_idx, _dist_temp = self.shortest_distance(
2849 self.main_object,
2850 sp.bezier_points[0].co,
2851 verts_ordered_V_indices
2853 # Get the position (edges count) of the vert v_idx in the selected chain V
2854 edge_idx_in_chain = verts_ordered_V_indices.index(v_idx)
2856 # For two-connected selections the strokes go after the
2857 # hypothetic stroke added before, so the index adds one per spline
2858 if selection_type == "TWO_CONNECTED":
2859 spline_number = i + 1
2860 else:
2861 spline_number = i
2863 edge_order_number_for_splines[spline_number] = edge_idx_in_chain
2865 # Get the first and last verts indices for later comparison
2866 if i == 0:
2867 first_v_idx = v_idx
2868 elif i == len(self.main_splines.data.splines) - 1:
2869 last_v_idx = v_idx
2871 if self.selection_V_is_closed:
2872 # If there is no last stroke on the last vertex (same as first vertex),
2873 # add a hypothetic spline at last vert order
2874 if first_v_idx != last_v_idx:
2875 edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
2876 len(verts_ordered_V_indices) - 1
2877 else:
2878 if self.cyclic_cross:
2879 edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
2880 len(verts_ordered_V_indices) - 2
2881 edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
2882 len(verts_ordered_V_indices) - 1
2883 else:
2884 edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
2885 len(verts_ordered_V_indices) - 1
2887 # Get the coords of the points distributed along the
2888 # "crossing curves", with appropriate proportions-V
2889 surface_splines_parsed = []
2890 for i in range(len(splines_U_objects)):
2891 sp_ob = splines_U_objects[i]
2892 # If "Loops on strokes" option is active, calculate the proportions for each loop-U
2893 if self.loops_on_strokes:
2894 # Segments distances from stroke to stroke
2895 dist = 0
2896 full_dist = 0
2897 segments_distances = []
2898 for t in range(len(sp_ob.data.splines[0].bezier_points)):
2899 bp = sp_ob.data.splines[0].bezier_points[t]
2901 if t == 0:
2902 last_p = bp.co
2903 else:
2904 actual_p = bp.co
2905 dist += (last_p - actual_p).length
2907 if t in points_U_crossed_by_strokes:
2908 segments_distances.append(dist)
2909 full_dist += dist
2911 dist = 0
2913 last_p = actual_p
2915 # Calculate Proportions.
2916 used_edges_proportions_V = []
2917 for t in range(len(segments_distances)):
2918 if self.selection_V_exists:
2919 if t == 0:
2920 order_number_last_stroke = 0
2922 segment_edges_length_V = 0
2923 segment_edges_length_V2 = 0
2924 for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
2925 segment_edges_length_V += edges_lengths_V[order]
2926 if self.selection_V2_exists:
2927 segment_edges_length_V2 += edges_lengths_V2[order]
2929 for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
2930 # Calculate each "sub-segment" (the ones between each stroke) length
2931 if self.selection_V2_exists:
2932 proportion_sub_seg = (edges_lengths_V2[order] -
2933 ((edges_lengths_V2[order] - edges_lengths_V[order]) /
2934 len(splines_U_objects) * i)) / (segment_edges_length_V2 -
2935 (segment_edges_length_V2 - segment_edges_length_V) /
2936 len(splines_U_objects) * i)
2938 sub_seg_dist = segments_distances[t] * proportion_sub_seg
2939 else:
2940 proportion_sub_seg = edges_lengths_V[order] / segment_edges_length_V
2941 sub_seg_dist = segments_distances[t] * proportion_sub_seg
2943 used_edges_proportions_V.append(sub_seg_dist / full_dist)
2945 order_number_last_stroke = edge_order_number_for_splines[t + 1]
2947 else:
2948 for _c in range(self.edges_V):
2949 # Calculate each "sub-segment" (the ones between each stroke) length
2950 sub_seg_dist = segments_distances[t] / self.edges_V
2951 used_edges_proportions_V.append(sub_seg_dist / full_dist)
2953 actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
2954 surface_splines_parsed.append(actual_spline[0])
2956 else:
2957 if self.selection_V2_exists:
2958 used_edges_proportions_V = []
2959 for p in range(len(edges_proportions_V)):
2960 used_edges_proportions_V.append(
2961 edges_proportions_V2[p] -
2962 ((edges_proportions_V2[p] -
2963 edges_proportions_V[p]) / len(splines_U_objects) * i)
2965 else:
2966 used_edges_proportions_V = edges_proportions_V
2968 actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
2969 surface_splines_parsed.append(actual_spline[0])
2971 # Set the verts of the first and last splines to the locations
2972 # of the respective verts in the selections
2973 if self.selection_V_exists:
2974 for i in range(0, len(surface_splines_parsed[0])):
2975 surface_splines_parsed[len(surface_splines_parsed) - 1][i] = \
2976 self.main_object.matrix_world @ verts_ordered_V[i].co
2978 if selection_type == "TWO_NOT_CONNECTED":
2979 if self.selection_V2_exists:
2980 for i in range(0, len(surface_splines_parsed[0])):
2981 surface_splines_parsed[0][i] = self.main_object.matrix_world @ verts_ordered_V2[i].co
2983 # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
2984 # merge the verts of the tips of the loops when they are "near enough"
2985 if self.automatic_join and selection_type != "TWO_CONNECTED":
2986 # Join the tips of "Follow" loops that are near enough and must be "closed"
2987 if not self.selection_V_exists and len(edges_proportions_U) >= 3:
2988 for i in range(len(surface_splines_parsed[0])):
2989 sp = surface_splines_parsed
2990 loop_segment_dist = (sp[0][i] - sp[1][i]).length
2992 verts_middle_position_co = [
2993 (sp[0][i][0] + sp[len(sp) - 1][i][0]) / 2,
2994 (sp[0][i][1] + sp[len(sp) - 1][i][1]) / 2,
2995 (sp[0][i][2] + sp[len(sp) - 1][i][2]) / 2
2997 points_original = []
2998 points_original.append(sp[1][i])
2999 points_original.append(sp[0][i])
3001 points_target = []
3002 points_target.append(sp[1][i])
3003 points_target.append(Vector(verts_middle_position_co))
3005 vec_A = points_original[0] - points_original[1]
3006 vec_B = points_target[0] - points_target[1]
3007 # check for zero angles, not sure if it is a great fix
3008 if vec_A.length != 0 and vec_B.length != 0:
3009 angle = vec_A.angle(vec_B) / pi
3010 edge_new_length = (Vector(verts_middle_position_co) - sp[1][i]).length
3011 else:
3012 angle = 0
3013 edge_new_length = 0
3015 # If after moving the verts to the middle point, the segment doesn't stretch too much
3016 if edge_new_length <= loop_segment_dist * 1.5 * \
3017 self.join_stretch_factor and angle < 0.25 * self.join_stretch_factor:
3019 # Avoid joining when the actual loop must be merged with the original mesh
3020 if not (self.selection_U_exists and i == 0) and \
3021 not (self.selection_U2_exists and i == len(surface_splines_parsed[0]) - 1):
3023 # Change the coords of both verts to the middle position
3024 surface_splines_parsed[0][i] = verts_middle_position_co
3025 surface_splines_parsed[len(surface_splines_parsed) - 1][i] = verts_middle_position_co
3027 # Delete object with control points and object from grease pencil conversion
3028 with bpy.context.temp_override(selected_objects=[ob_ctrl_pts]):
3029 bpy.ops.object.delete()
3031 with bpy.context.temp_override(selected_objects=splines_U_objects):
3032 bpy.ops.object.delete()
3034 # Generate surface
3036 # Get all verts coords
3037 all_surface_verts_co = []
3038 for i in range(0, len(surface_splines_parsed)):
3039 # Get coords of all verts and make a list with them
3040 for pt_co in surface_splines_parsed[i]:
3041 all_surface_verts_co.append(pt_co)
3043 # Define verts for each face
3044 all_surface_faces = []
3045 for i in range(0, len(all_surface_verts_co) - len(surface_splines_parsed[0])):
3046 if ((i + 1) / len(surface_splines_parsed[0]) != int((i + 1) / len(surface_splines_parsed[0]))):
3047 all_surface_faces.append(
3048 [i + 1, i, i + len(surface_splines_parsed[0]),
3049 i + len(surface_splines_parsed[0]) + 1]
3051 # Build the mesh
3052 surf_me_name = "SURFSKIO_surface"
3053 me_surf = bpy.data.meshes.new(surf_me_name)
3054 me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
3055 ob_surface = object_utils.object_data_add(context, me_surf)
3056 ob_surface.location = (0.0, 0.0, 0.0)
3057 ob_surface.rotation_euler = (0.0, 0.0, 0.0)
3058 ob_surface.scale = (1.0, 1.0, 1.0)
3060 # Select all the "unselected but participating" verts, from closed selection
3061 # or double selections with middle-vertex, for later join with remove doubles
3062 for v_idx in single_unselected_verts:
3063 self.main_object.data.vertices[v_idx].select = True
3065 # Join the new mesh to the main object
3066 ob_surface.select_set(True)
3067 self.main_object.select_set(True)
3068 bpy.context.view_layer.objects.active = self.main_object
3070 bpy.ops.object.join('INVOKE_REGION_WIN')
3072 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3074 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN', threshold=0.0001)
3075 bpy.ops.mesh.normals_make_consistent('INVOKE_REGION_WIN', inside=False)
3076 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
3078 self.update()
3080 return{'FINISHED'}
3082 def update(self):
3083 try:
3084 global global_shade_smooth
3085 if global_shade_smooth:
3086 bpy.ops.object.shade_smooth()
3087 else:
3088 bpy.ops.object.shade_flat()
3089 bpy.context.scene.bsurfaces.SURFSK_shade_smooth = global_shade_smooth
3090 except:
3091 pass
3093 return{'FINISHED'}
3095 def execute(self, context):
3097 if bpy.ops.object.mode_set.poll():
3098 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3100 try:
3101 global global_mesh_object
3102 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
3103 bpy.data.objects[global_mesh_object].select_set(True)
3104 self.main_object = bpy.data.objects[global_mesh_object]
3105 bpy.context.view_layer.objects.active = self.main_object
3106 bsurfaces_props = bpy.context.scene.bsurfaces
3107 except:
3108 self.report({'WARNING'}, "Specify the name of the object with retopology")
3109 return{"CANCELLED"}
3110 bpy.context.view_layer.objects.active = self.main_object
3112 self.update()
3114 if not self.is_fill_faces:
3115 bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
3116 value='True, False, False')
3118 # Build splines from the "last saved splines".
3119 last_saved_curve = bpy.data.curves.new('SURFSKIO_last_crv', 'CURVE')
3120 self.main_splines = bpy.data.objects.new('SURFSKIO_last_crv', last_saved_curve)
3121 bpy.context.collection.objects.link(self.main_splines)
3123 last_saved_curve.dimensions = "3D"
3125 for sp in self.last_strokes_splines_coords:
3126 spline = self.main_splines.data.splines.new('BEZIER')
3127 # less one because one point is added when the spline is created
3128 spline.bezier_points.add(len(sp) - 1)
3129 for p in range(0, len(sp)):
3130 spline.bezier_points[p].co = [sp[p][0], sp[p][1], sp[p][2]]
3132 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3134 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3135 self.main_splines.select_set(True)
3136 bpy.context.view_layer.objects.active = self.main_splines
3138 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
3140 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
3141 # Important to make it vector first and then automatic, otherwise the
3142 # tips handles get too big and distort the shrinkwrap results later
3143 bpy.ops.curve.handle_type_set(type='VECTOR')
3144 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
3145 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3146 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3148 self.main_splines.name = "SURFSKIO_temp_strokes"
3150 if self.is_crosshatch:
3151 strokes_for_crosshatch = True
3152 strokes_for_rectangular_surface = False
3153 else:
3154 strokes_for_rectangular_surface = True
3155 strokes_for_crosshatch = False
3157 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3159 if strokes_for_rectangular_surface:
3160 self.rectangular_surface(context)
3161 elif strokes_for_crosshatch:
3162 self.crosshatch_surface_execute(context)
3164 #Set Shade smooth to new polygons
3165 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3166 global global_shade_smooth
3167 if global_shade_smooth:
3168 bpy.ops.object.shade_smooth()
3169 else:
3170 bpy.ops.object.shade_flat()
3172 # Delete main splines
3173 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3174 if self.keep_strokes:
3175 self.main_splines.name = "keep_strokes"
3176 self.main_splines.data.bevel_depth = 0.001
3177 if "keep_strokes_material" in bpy.data.materials :
3178 self.main_splines.data.materials.append(bpy.data.materials["keep_strokes_material"])
3179 else:
3180 mat = bpy.data.materials.new("keep_strokes_material")
3181 mat.diffuse_color = (1, 0, 0, 0)
3182 mat.specular_color = (1, 0, 0)
3183 mat.specular_intensity = 0.0
3184 mat.roughness = 0.0
3185 self.main_splines.data.materials.append(mat)
3186 else:
3187 with bpy.context.temp_override(selected_objects=[self.main_splines]):
3188 bpy.ops.object.delete()
3190 # Delete grease pencil strokes
3191 if self.strokes_type == "GP_STROKES" and not self.stopping_errors:
3192 try:
3193 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
3194 except:
3195 pass
3197 # Delete annotations
3198 if self.strokes_type == "GP_ANNOTATION" and not self.stopping_errors:
3199 try:
3200 bpy.context.annotation_data.layers.active.clear()
3201 except:
3202 pass
3204 bsurfaces_props = bpy.context.scene.bsurfaces
3205 bsurfaces_props.SURFSK_edges_U = self.edges_U
3206 bsurfaces_props.SURFSK_edges_V = self.edges_V
3207 bsurfaces_props.SURFSK_cyclic_cross = self.cyclic_cross
3208 bsurfaces_props.SURFSK_cyclic_follow = self.cyclic_follow
3209 bsurfaces_props.SURFSK_automatic_join = self.automatic_join
3210 bsurfaces_props.SURFSK_loops_on_strokes = self.loops_on_strokes
3211 bsurfaces_props.SURFSK_keep_strokes = self.keep_strokes
3213 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3214 self.main_object.select_set(True)
3215 bpy.context.view_layer.objects.active = self.main_object
3217 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3219 self.update()
3221 return{'FINISHED'}
3223 def invoke(self, context, event):
3225 if bpy.ops.object.mode_set.poll():
3226 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3228 bsurfaces_props = bpy.context.scene.bsurfaces
3229 self.cyclic_cross = bsurfaces_props.SURFSK_cyclic_cross
3230 self.cyclic_follow = bsurfaces_props.SURFSK_cyclic_follow
3231 self.automatic_join = bsurfaces_props.SURFSK_automatic_join
3232 self.loops_on_strokes = bsurfaces_props.SURFSK_loops_on_strokes
3233 self.keep_strokes = bsurfaces_props.SURFSK_keep_strokes
3235 try:
3236 global global_mesh_object
3237 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
3238 bpy.data.objects[global_mesh_object].select_set(True)
3239 self.main_object = bpy.data.objects[global_mesh_object]
3240 bpy.context.view_layer.objects.active = self.main_object
3241 except:
3242 self.report({'WARNING'}, "Specify the name of the object with retopology")
3243 return{"CANCELLED"}
3245 self.update()
3247 self.main_object_selected_verts_count = len([v for v in self.main_object.data.vertices if v.select])
3249 bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
3250 value='True, False, False')
3252 self.edges_U = bsurfaces_props.SURFSK_edges_U
3253 self.edges_V = bsurfaces_props.SURFSK_edges_V
3255 self.is_fill_faces = False
3256 self.stopping_errors = False
3257 self.last_strokes_splines_coords = []
3259 # Determine the type of the strokes
3260 self.strokes_type = get_strokes_type(context)
3262 # Check if it will be used grease pencil strokes or curves
3263 # If there are strokes to be used
3264 if self.strokes_type == "GP_STROKES" or self.strokes_type == "EXTERNAL_CURVE" or self.strokes_type == "GP_ANNOTATION":
3265 if self.strokes_type == "GP_STROKES":
3266 # Convert grease pencil strokes to curve
3267 global global_gpencil_object
3268 gp = bpy.data.objects[global_gpencil_object]
3269 self.original_curve = conver_gpencil_to_curve(self, context, gp, 'GPensil')
3270 self.using_external_curves = False
3272 elif self.strokes_type == "GP_ANNOTATION":
3273 # Convert grease pencil strokes to curve
3274 gp = bpy.context.annotation_data
3275 self.original_curve = conver_gpencil_to_curve(self, context, gp, 'Annotation')
3276 self.using_external_curves = False
3278 elif self.strokes_type == "EXTERNAL_CURVE":
3279 global global_curve_object
3280 self.original_curve = bpy.data.objects[global_curve_object]
3281 self.using_external_curves = True
3283 # Make sure there are no objects left from erroneous
3284 # executions of this operator, with the reserved names used here
3285 for o in bpy.data.objects:
3286 if o.name.find("SURFSKIO_") != -1:
3287 with bpy.context.temp_override(selected_objects=[o]):
3288 bpy.ops.object.delete()
3290 bpy.context.view_layer.objects.active = self.original_curve
3292 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3294 self.temporary_curve = bpy.context.view_layer.objects.active
3296 # Deselect all points of the curve
3297 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3298 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3299 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3301 # Delete splines with only a single isolated point
3302 for i in range(len(self.temporary_curve.data.splines)):
3303 sp = self.temporary_curve.data.splines[i]
3305 if len(sp.bezier_points) == 1:
3306 sp.bezier_points[0].select_control_point = True
3308 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3309 bpy.ops.curve.delete(type='VERT')
3310 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3312 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3313 self.temporary_curve.select_set(True)
3314 bpy.context.view_layer.objects.active = self.temporary_curve
3316 # Set a minimum number of points for crosshatch
3317 minimum_points_num = 15
3319 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3320 # Check if the number of points of each curve has at least the number of points
3321 # of minimum_points_num, which is a bit more than the face-loops limit.
3322 # If not, subdivide to reach at least that number of points
3323 for i in range(len(self.temporary_curve.data.splines)):
3324 sp = self.temporary_curve.data.splines[i]
3326 if len(sp.bezier_points) < minimum_points_num:
3327 for bp in sp.bezier_points:
3328 bp.select_control_point = True
3330 if (len(sp.bezier_points) - 1) != 0:
3331 # Formula to get the number of cuts that will make a curve
3332 # of N number of points have near to "minimum_points_num"
3333 # points, when subdividing with this number of cuts
3334 subdivide_cuts = int(
3335 (minimum_points_num - len(sp.bezier_points)) /
3336 (len(sp.bezier_points) - 1)
3337 ) + 1
3338 else:
3339 subdivide_cuts = 0
3341 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3342 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3344 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3346 # Detect if the strokes are a crosshatch and do it if it is
3347 self.crosshatch_surface_invoke(self.temporary_curve)
3349 if not self.is_crosshatch:
3350 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3351 self.temporary_curve.select_set(True)
3352 bpy.context.view_layer.objects.active = self.temporary_curve
3354 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3356 # Set a minimum number of points for rectangular surfaces
3357 minimum_points_num = 60
3359 # Check if the number of points of each curve has at least the number of points
3360 # of minimum_points_num, which is a bit more than the face-loops limit.
3361 # If not, subdivide to reach at least that number of points
3362 for i in range(len(self.temporary_curve.data.splines)):
3363 sp = self.temporary_curve.data.splines[i]
3365 if len(sp.bezier_points) < minimum_points_num:
3366 for bp in sp.bezier_points:
3367 bp.select_control_point = True
3369 if (len(sp.bezier_points) - 1) != 0:
3370 # Formula to get the number of cuts that will make a curve of
3371 # N number of points have near to "minimum_points_num" points,
3372 # when subdividing with this number of cuts
3373 subdivide_cuts = int(
3374 (minimum_points_num - len(sp.bezier_points)) /
3375 (len(sp.bezier_points) - 1)
3376 ) + 1
3377 else:
3378 subdivide_cuts = 0
3380 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3381 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3383 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3385 # Save coordinates of the actual strokes (as the "last saved splines")
3386 for sp_idx in range(len(self.temporary_curve.data.splines)):
3387 self.last_strokes_splines_coords.append([])
3388 for bp_idx in range(len(self.temporary_curve.data.splines[sp_idx].bezier_points)):
3389 coords = self.temporary_curve.matrix_world @ \
3390 self.temporary_curve.data.splines[sp_idx].bezier_points[bp_idx].co
3391 self.last_strokes_splines_coords[sp_idx].append([coords[0], coords[1], coords[2]])
3393 # Check for cyclic splines, put the first and last points in the middle of their actual positions
3394 for sp_idx in range(len(self.temporary_curve.data.splines)):
3395 if self.temporary_curve.data.splines[sp_idx].use_cyclic_u is True:
3396 first_p_co = self.last_strokes_splines_coords[sp_idx][0]
3397 last_p_co = self.last_strokes_splines_coords[sp_idx][
3398 len(self.last_strokes_splines_coords[sp_idx]) - 1
3400 target_co = [
3401 (first_p_co[0] + last_p_co[0]) / 2,
3402 (first_p_co[1] + last_p_co[1]) / 2,
3403 (first_p_co[2] + last_p_co[2]) / 2
3406 self.last_strokes_splines_coords[sp_idx][0] = target_co
3407 self.last_strokes_splines_coords[sp_idx][
3408 len(self.last_strokes_splines_coords[sp_idx]) - 1
3409 ] = target_co
3410 tuple(self.last_strokes_splines_coords)
3412 # Estimation of the average length of the segments between
3413 # each point of the grease pencil strokes.
3414 # Will be useful to determine whether a curve should be made "Cyclic"
3415 segments_lengths_sum = 0
3416 segments_count = 0
3417 random_spline = self.temporary_curve.data.splines[0].bezier_points
3418 for i in range(0, len(random_spline)):
3419 if i != 0 and len(random_spline) - 1 >= i:
3420 segments_lengths_sum += (random_spline[i - 1].co - random_spline[i].co).length
3421 segments_count += 1
3423 self.average_gp_segment_length = segments_lengths_sum / segments_count
3425 # Delete temporary strokes curve object
3426 with bpy.context.temp_override(selected_objects=[self.temporary_curve]):
3427 bpy.ops.object.delete()
3429 # Set again since "execute()" will turn it again to its initial value
3430 self.execute(context)
3432 if not self.stopping_errors:
3433 # Delete grease pencil strokes
3434 if self.strokes_type == "GP_STROKES":
3435 try:
3436 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
3437 except:
3438 pass
3440 # Delete annotation strokes
3441 elif self.strokes_type == "GP_ANNOTATION":
3442 try:
3443 bpy.context.annotation_data.layers.active.clear()
3444 except:
3445 pass
3447 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3448 with bpy.context.temp_override(selected_objects=[self.original_curve]):
3449 bpy.ops.object.delete()
3450 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3452 return {"FINISHED"}
3453 else:
3454 return{"CANCELLED"}
3456 elif self.strokes_type == "SELECTION_ALONE":
3457 self.is_fill_faces = True
3458 created_faces_count = self.fill_with_faces(self.main_object)
3460 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3462 if created_faces_count == 0:
3463 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3464 return {"CANCELLED"}
3465 else:
3466 return {"FINISHED"}
3468 if self.strokes_type == "EXTERNAL_NO_CURVE":
3469 self.report({'WARNING'}, "The secondary object is not a Curve.")
3470 return{"CANCELLED"}
3472 elif self.strokes_type == "MORE_THAN_ONE_EXTERNAL":
3473 self.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3474 return{"CANCELLED"}
3476 elif self.strokes_type == "SINGLE_GP_STROKE_NO_SELECTION" or \
3477 self.strokes_type == "SINGLE_CURVE_STROKE_NO_SELECTION":
3479 self.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
3480 return{"CANCELLED"}
3482 elif self.strokes_type == "NO_STROKES":
3483 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3484 return{"CANCELLED"}
3486 elif self.strokes_type == "CURVE_WITH_NON_BEZIER_SPLINES":
3487 self.report({'WARNING'}, "All splines must be Bezier.")
3488 return{"CANCELLED"}
3490 else:
3491 return{"CANCELLED"}
3493 # ----------------------------
3494 # Init operator
3495 class MESH_OT_SURFSK_init(Operator):
3496 bl_idname = "mesh.surfsk_init"
3497 bl_label = "Bsurfaces initialize"
3498 bl_description = "Add an empty mesh object with useful settings"
3499 bl_options = {'REGISTER', 'UNDO'}
3501 def execute(self, context):
3503 bs = bpy.context.scene.bsurfaces
3505 if bpy.ops.object.mode_set.poll():
3506 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3508 global global_shade_smooth
3509 global global_mesh_object
3510 global global_gpencil_object
3512 if bs.SURFSK_mesh == None:
3513 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3514 mesh = bpy.data.meshes.new('BSurfaceMesh')
3515 mesh_object = object_utils.object_data_add(context, mesh)
3516 mesh_object.select_set(True)
3517 bpy.context.view_layer.objects.active = mesh_object
3519 mesh_object.show_all_edges = True
3520 mesh_object.display_type = 'SOLID'
3521 mesh_object.show_wire = True
3523 global_shade_smooth = bpy.context.scene.bsurfaces.SURFSK_shade_smooth
3524 if global_shade_smooth:
3525 bpy.ops.object.shade_smooth()
3526 else:
3527 bpy.ops.object.shade_flat()
3529 color_red = [1.0, 0.0, 0.0, 0.3]
3530 material = makeMaterial("BSurfaceMesh", color_red)
3531 mesh_object.data.materials.append(material)
3532 modifier = mesh_object.modifiers.new("", 'SHRINKWRAP')
3533 if self.active_object is not None:
3534 modifier.target = self.active_object
3535 modifier.wrap_method = 'TARGET_PROJECT'
3536 modifier.wrap_mode = 'OUTSIDE_SURFACE'
3537 modifier.show_on_cage = True
3539 global_mesh_object = mesh_object.name
3540 bpy.context.scene.bsurfaces.SURFSK_mesh = bpy.data.objects[global_mesh_object]
3542 bpy.context.scene.tool_settings.snap_elements = {'FACE'}
3543 bpy.context.scene.tool_settings.use_snap = True
3544 bpy.context.scene.tool_settings.use_snap_self = False
3545 bpy.context.scene.tool_settings.use_snap_align_rotation = True
3546 bpy.context.scene.tool_settings.use_snap_project = True
3547 bpy.context.scene.tool_settings.use_snap_rotate = True
3548 bpy.context.scene.tool_settings.use_snap_scale = True
3550 bpy.context.scene.tool_settings.use_mesh_automerge = True
3551 bpy.context.scene.tool_settings.double_threshold = 0.01
3553 if context.scene.bsurfaces.SURFSK_guide == 'GPencil' and bs.SURFSK_gpencil == None:
3554 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3555 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')
3556 bpy.context.scene.tool_settings.gpencil_stroke_placement_view3d = 'SURFACE'
3557 gpencil_object = bpy.context.scene.objects[bpy.context.scene.objects[-1].name]
3558 gpencil_object.select_set(True)
3559 bpy.context.view_layer.objects.active = gpencil_object
3560 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
3561 global_gpencil_object = gpencil_object.name
3562 bpy.context.scene.bsurfaces.SURFSK_gpencil = bpy.data.objects[global_gpencil_object]
3563 gpencil_object.data.stroke_depth_order = '3D'
3564 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
3565 bpy.ops.wm.tool_set_by_id(name="builtin_brush.Draw")
3567 if context.scene.bsurfaces.SURFSK_guide == 'Annotation':
3568 bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
3569 bpy.context.scene.tool_settings.annotation_stroke_placement_view3d = 'SURFACE'
3571 def invoke(self, context, event):
3572 if bpy.context.active_object:
3573 self.active_object = bpy.context.active_object
3574 else:
3575 self.active_object = None
3577 self.execute(context)
3579 return {"FINISHED"}
3581 # ----------------------------
3582 # Add modifiers operator
3583 class MESH_OT_SURFSK_add_modifiers(Operator):
3584 bl_idname = "mesh.surfsk_add_modifiers"
3585 bl_label = "Add Mirror and others modifiers"
3586 bl_description = "Add modifiers: Mirror, Shrinkwrap, Subdivision, Solidify"
3587 bl_options = {'REGISTER', 'UNDO'}
3589 def execute(self, context):
3591 bs = bpy.context.scene.bsurfaces
3593 if bpy.ops.object.mode_set.poll():
3594 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3596 if bs.SURFSK_mesh == None:
3597 self.report({'ERROR_INVALID_INPUT'}, "Please select Mesh of BSurface or click Initialize")
3598 else:
3599 mesh_object = bs.SURFSK_mesh
3601 try:
3602 mesh_object.select_set(True)
3603 except:
3604 self.report({'ERROR_INVALID_INPUT'}, "Mesh of BSurface does not exist")
3605 return {"CANCEL"}
3607 bpy.context.view_layer.objects.active = mesh_object
3609 try:
3610 shrinkwrap = next(mod for mod in mesh_object.modifiers
3611 if mod.type == 'SHRINKWRAP')
3612 except:
3613 shrinkwrap = mesh_object.modifiers.new("", 'SHRINKWRAP')
3614 if self.active_object is not None and self.active_object != mesh_object:
3615 shrinkwrap.target = self.active_object
3616 shrinkwrap.wrap_method = 'TARGET_PROJECT'
3617 shrinkwrap.wrap_mode = 'OUTSIDE_SURFACE'
3618 shrinkwrap.show_on_cage = True
3619 shrinkwrap.offset = bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset
3621 try:
3622 mirror = next(mod for mod in mesh_object.modifiers
3623 if mod.type == 'MIRROR')
3624 except:
3625 mirror = mesh_object.modifiers.new("", 'MIRROR')
3626 mirror.use_clip = True
3628 try:
3629 _subsurf = next(mod for mod in mesh_object.modifiers
3630 if mod.type == 'SUBSURF')
3631 except:
3632 _subsurf = mesh_object.modifiers.new("", 'SUBSURF')
3634 try:
3635 solidify = next(mod for mod in mesh_object.modifiers
3636 if mod.type == 'SOLIDIFY')
3637 except:
3638 solidify = mesh_object.modifiers.new("", 'SOLIDIFY')
3639 solidify.thickness = 0.01
3641 return {"FINISHED"}
3643 def invoke(self, context, event):
3644 if bpy.context.active_object:
3645 self.active_object = bpy.context.active_object
3646 else:
3647 self.active_object = None
3649 self.execute(context)
3651 return {"FINISHED"}
3653 # ----------------------------
3654 # Edit surface operator
3655 class MESH_OT_SURFSK_edit_surface(Operator):
3656 bl_idname = "mesh.surfsk_edit_surface"
3657 bl_label = "Bsurfaces edit surface"
3658 bl_description = "Edit surface mesh"
3659 bl_options = {'REGISTER', 'UNDO'}
3661 def execute(self, context):
3662 if bpy.ops.object.mode_set.poll():
3663 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3664 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3665 bpy.context.scene.bsurfaces.SURFSK_mesh.select_set(True)
3666 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_mesh
3667 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
3668 bpy.ops.wm.tool_set_by_id(name="builtin.select")
3670 def invoke(self, context, event):
3671 try:
3672 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
3673 bpy.data.objects[global_mesh_object].select_set(True)
3674 self.main_object = bpy.data.objects[global_mesh_object]
3675 bpy.context.view_layer.objects.active = self.main_object
3676 except:
3677 self.report({'WARNING'}, "Specify the name of the object with retopology")
3678 return{"CANCELLED"}
3680 self.execute(context)
3682 return {"FINISHED"}
3684 # ----------------------------
3685 # Add strokes operator
3686 class GPENCIL_OT_SURFSK_add_strokes(Operator):
3687 bl_idname = "gpencil.surfsk_add_strokes"
3688 bl_label = "Bsurfaces add strokes"
3689 bl_description = "Add the grease pencil strokes"
3690 bl_options = {'REGISTER', 'UNDO'}
3692 def execute(self, context):
3693 if bpy.ops.object.mode_set.poll():
3694 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3695 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3697 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3698 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_gpencil
3699 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
3700 bpy.ops.wm.tool_set_by_id(name="builtin_brush.Draw")
3702 return{"FINISHED"}
3704 def invoke(self, context, event):
3705 try:
3706 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3707 except:
3708 self.report({'WARNING'}, "Specify the name of the object with strokes")
3709 return{"CANCELLED"}
3711 self.execute(context)
3713 return {"FINISHED"}
3715 # ----------------------------
3716 # Edit strokes operator
3717 class GPENCIL_OT_SURFSK_edit_strokes(Operator):
3718 bl_idname = "gpencil.surfsk_edit_strokes"
3719 bl_label = "Bsurfaces edit strokes"
3720 bl_description = "Edit the grease pencil strokes"
3721 bl_options = {'REGISTER', 'UNDO'}
3723 def execute(self, context):
3724 if bpy.ops.object.mode_set.poll():
3725 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3726 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3728 gpencil_object = bpy.context.scene.bsurfaces.SURFSK_gpencil
3730 gpencil_object.select_set(True)
3731 bpy.context.view_layer.objects.active = gpencil_object
3733 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT_GPENCIL')
3734 try:
3735 bpy.ops.gpencil.select_all(action='SELECT')
3736 except:
3737 pass
3739 def invoke(self, context, event):
3740 try:
3741 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3742 except:
3743 self.report({'WARNING'}, "Specify the name of the object with strokes")
3744 return{"CANCELLED"}
3746 self.execute(context)
3748 return {"FINISHED"}
3750 # ----------------------------
3751 # Convert annotation to curves operator
3752 class GPENCIL_OT_SURFSK_annotation_to_curves(Operator):
3753 bl_idname = "gpencil.surfsk_annotations_to_curves"
3754 bl_label = "Convert annotation to curves"
3755 bl_description = "Convert annotation to curves for editing"
3756 bl_options = {'REGISTER', 'UNDO'}
3758 def execute(self, context):
3760 if bpy.ops.object.mode_set.poll():
3761 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3763 # Convert annotation to curve
3764 curve = conver_gpencil_to_curve(self, context, None, 'Annotation')
3766 if curve != None:
3767 # Delete annotation strokes
3768 try:
3769 bpy.context.annotation_data.layers.active.clear()
3770 except:
3771 pass
3773 # Clean up curves
3774 curve.select_set(True)
3775 bpy.context.view_layer.objects.active = curve
3777 bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
3779 return {"FINISHED"}
3781 def invoke(self, context, event):
3782 try:
3783 strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
3785 _strokes_num = len(strokes)
3786 except:
3787 self.report({'WARNING'}, "Not active annotation")
3788 return{"CANCELLED"}
3790 self.execute(context)
3792 return {"FINISHED"}
3794 # ----------------------------
3795 # Convert strokes to curves operator
3796 class GPENCIL_OT_SURFSK_strokes_to_curves(Operator):
3797 bl_idname = "gpencil.surfsk_strokes_to_curves"
3798 bl_label = "Convert strokes to curves"
3799 bl_description = "Convert grease pencil strokes to curves for editing"
3800 bl_options = {'REGISTER', 'UNDO'}
3802 def execute(self, context):
3804 if bpy.ops.object.mode_set.poll():
3805 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3807 # Convert grease pencil strokes to curve
3808 gp = bpy.context.scene.bsurfaces.SURFSK_gpencil
3809 curve = conver_gpencil_to_curve(self, context, gp, 'GPensil')
3811 if curve != None:
3812 # Delete grease pencil strokes
3813 try:
3814 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
3815 except:
3816 pass
3818 # Clean up curves
3820 curve.select_set(True)
3821 bpy.context.view_layer.objects.active = curve
3823 bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
3825 return {"FINISHED"}
3827 def invoke(self, context, event):
3828 try:
3829 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3830 except:
3831 self.report({'WARNING'}, "Specify the name of the object with strokes")
3832 return{"CANCELLED"}
3834 self.execute(context)
3836 return {"FINISHED"}
3838 # ----------------------------
3839 # Add annotation
3840 class GPENCIL_OT_SURFSK_add_annotation(Operator):
3841 bl_idname = "gpencil.surfsk_add_annotation"
3842 bl_label = "Bsurfaces add annotation"
3843 bl_description = "Add annotation"
3844 bl_options = {'REGISTER', 'UNDO'}
3846 def execute(self, context):
3847 bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
3848 bpy.context.scene.tool_settings.annotation_stroke_placement_view3d = 'SURFACE'
3850 return{"FINISHED"}
3852 def invoke(self, context, event):
3854 self.execute(context)
3856 return {"FINISHED"}
3859 # ----------------------------
3860 # Edit curve operator
3861 class CURVE_OT_SURFSK_edit_curve(Operator):
3862 bl_idname = "curve.surfsk_edit_curve"
3863 bl_label = "Bsurfaces edit curve"
3864 bl_description = "Edit curve"
3865 bl_options = {'REGISTER', 'UNDO'}
3867 def execute(self, context):
3868 if bpy.ops.object.mode_set.poll():
3869 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3870 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3871 bpy.context.scene.bsurfaces.SURFSK_curve.select_set(True)
3872 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_curve
3873 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
3875 def invoke(self, context, event):
3876 try:
3877 bpy.context.scene.bsurfaces.SURFSK_curve.select_set(True)
3878 except:
3879 self.report({'WARNING'}, "Specify the name of the object with curve")
3880 return{"CANCELLED"}
3882 self.execute(context)
3884 return {"FINISHED"}
3886 # ----------------------------
3887 # Reorder splines
3888 class CURVE_OT_SURFSK_reorder_splines(Operator):
3889 bl_idname = "curve.surfsk_reorder_splines"
3890 bl_label = "Bsurfaces reorder splines"
3891 bl_description = "Defines the order of the splines by using grease pencil strokes"
3892 bl_options = {'REGISTER', 'UNDO'}
3894 def execute(self, context):
3895 objects_to_delete = []
3896 # Convert grease pencil strokes to curve.
3897 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3898 bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
3899 for ob in bpy.context.selected_objects:
3900 if ob != bpy.context.view_layer.objects.active and ob.name.startswith("GP_Layer"):
3901 GP_strokes_curve = ob
3903 # GP_strokes_curve = bpy.context.object
3904 objects_to_delete.append(GP_strokes_curve)
3906 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3907 GP_strokes_curve.select_set(True)
3908 bpy.context.view_layer.objects.active = GP_strokes_curve
3910 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3911 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
3912 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=100)
3913 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3915 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3916 GP_strokes_mesh = bpy.context.object
3917 objects_to_delete.append(GP_strokes_mesh)
3919 GP_strokes_mesh.data.resolution_u = 1
3920 bpy.ops.object.convert(target='MESH', keep_original=False)
3922 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3923 self.main_curve.select_set(True)
3924 bpy.context.view_layer.objects.active = self.main_curve
3926 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3927 curves_duplicate_1 = bpy.context.object
3928 objects_to_delete.append(curves_duplicate_1)
3930 minimum_points_num = 500
3932 # Some iterations since the subdivision operator
3933 # has a limit of 100 subdivisions per iteration
3934 for x in range(round(minimum_points_num / 100)):
3935 # Check if the number of points of each curve has at least the number of points
3936 # of minimum_points_num. If not, subdivide to reach at least that number of points
3937 for i in range(len(curves_duplicate_1.data.splines)):
3938 sp = curves_duplicate_1.data.splines[i]
3940 if len(sp.bezier_points) < minimum_points_num:
3941 for bp in sp.bezier_points:
3942 bp.select_control_point = True
3944 if (len(sp.bezier_points) - 1) != 0:
3945 # Formula to get the number of cuts that will make a curve of N
3946 # number of points have near to "minimum_points_num" points,
3947 # when subdividing with this number of cuts
3948 subdivide_cuts = int(
3949 (minimum_points_num - len(sp.bezier_points)) /
3950 (len(sp.bezier_points) - 1)
3951 ) + 1
3952 else:
3953 subdivide_cuts = 0
3955 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3956 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3957 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3958 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3960 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3961 curves_duplicate_2 = bpy.context.object
3962 objects_to_delete.append(curves_duplicate_2)
3964 # Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target
3965 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3966 curves_duplicate_2.select_set(True)
3967 bpy.context.view_layer.objects.active = curves_duplicate_2
3969 shrinkwrap = curves_duplicate_2.modifiers.new("", 'SHRINKWRAP')
3970 shrinkwrap.wrap_method = "NEAREST_VERTEX"
3971 shrinkwrap.target = GP_strokes_mesh
3972 bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', modifier=shrinkwrap.name)
3974 # Get the distance of each vert from its original position to its position with Shrinkwrap
3975 nearest_points_coords = {}
3976 for st_idx in range(len(curves_duplicate_1.data.splines)):
3977 for bp_idx in range(len(curves_duplicate_1.data.splines[st_idx].bezier_points)):
3978 bp_1_co = curves_duplicate_1.matrix_world @ \
3979 curves_duplicate_1.data.splines[st_idx].bezier_points[bp_idx].co
3981 bp_2_co = curves_duplicate_2.matrix_world @ \
3982 curves_duplicate_2.data.splines[st_idx].bezier_points[bp_idx].co
3984 if bp_idx == 0:
3985 shortest_dist = (bp_1_co - bp_2_co).length
3986 nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
3987 "%.4f" % bp_2_co[1],
3988 "%.4f" % bp_2_co[2])
3990 dist = (bp_1_co - bp_2_co).length
3992 if dist < shortest_dist:
3993 nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
3994 "%.4f" % bp_2_co[1],
3995 "%.4f" % bp_2_co[2])
3996 shortest_dist = dist
3998 # Get all coords of GP strokes points, for comparison
3999 GP_strokes_coords = []
4000 for st_idx in range(len(GP_strokes_curve.data.splines)):
4001 GP_strokes_coords.append(
4002 [("%.4f" % x if "%.4f" % x != "-0.00" else "0.00",
4003 "%.4f" % y if "%.4f" % y != "-0.00" else "0.00",
4004 "%.4f" % z if "%.4f" % z != "-0.00" else "0.00") for
4005 x, y, z in [bp.co for bp in GP_strokes_curve.data.splines[st_idx].bezier_points]]
4008 # Check the point of the GP strokes with the same coords as
4009 # the nearest points of the curves (with shrinkwrap)
4011 # Dictionary with GP stroke index as index, and a list as value.
4012 # The list has as index the point index of the GP stroke
4013 # nearest to the spline, and as value the spline index
4014 GP_connection_points = {}
4015 for gp_st_idx in range(len(GP_strokes_coords)):
4016 GPvert_spline_relationship = {}
4018 for splines_st_idx in range(len(nearest_points_coords)):
4019 if nearest_points_coords[splines_st_idx] in GP_strokes_coords[gp_st_idx]:
4020 GPvert_spline_relationship[
4021 GP_strokes_coords[gp_st_idx].index(nearest_points_coords[splines_st_idx])
4022 ] = splines_st_idx
4024 GP_connection_points[gp_st_idx] = GPvert_spline_relationship
4026 # Get the splines new order
4027 splines_new_order = []
4028 for i in GP_connection_points:
4029 dict_keys = sorted(GP_connection_points[i].keys()) # Sort dictionaries by key
4031 for k in dict_keys:
4032 splines_new_order.append(GP_connection_points[i][k])
4034 # Reorder
4035 curve_original_name = self.main_curve.name
4037 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4038 self.main_curve.select_set(True)
4039 bpy.context.view_layer.objects.active = self.main_curve
4041 self.main_curve.name = "SURFSKIO_CRV_ORD"
4043 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4044 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4045 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4047 for _sp_idx in range(len(self.main_curve.data.splines)):
4048 self.main_curve.data.splines[0].bezier_points[0].select_control_point = True
4050 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4051 bpy.ops.curve.separate('EXEC_REGION_WIN')
4052 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4054 # Get the names of the separated splines objects in the original order
4055 splines_unordered = {}
4056 for o in bpy.data.objects:
4057 if o.name.find("SURFSKIO_CRV_ORD") != -1:
4058 spline_order_string = o.name.partition(".")[2]
4060 if spline_order_string != "" and int(spline_order_string) > 0:
4061 spline_order_index = int(spline_order_string) - 1
4062 splines_unordered[spline_order_index] = o.name
4064 # Join all splines objects in final order
4065 for order_idx in splines_new_order:
4066 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4067 bpy.data.objects[splines_unordered[order_idx]].select_set(True)
4068 bpy.data.objects["SURFSKIO_CRV_ORD"].select_set(True)
4069 bpy.context.view_layer.objects.active = bpy.data.objects["SURFSKIO_CRV_ORD"]
4071 bpy.ops.object.join('INVOKE_REGION_WIN')
4073 # Go back to the original name of the curves object.
4074 bpy.context.object.name = curve_original_name
4076 # Delete all unused objects
4077 with bpy.context.temp_override(selected_objects=objects_to_delete):
4078 bpy.ops.object.delete()
4080 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4081 bpy.data.objects[curve_original_name].select_set(True)
4082 bpy.context.view_layer.objects.active = bpy.data.objects[curve_original_name]
4084 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4085 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4087 try:
4088 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
4089 except:
4090 pass
4093 return {"FINISHED"}
4095 def invoke(self, context, event):
4096 self.main_curve = bpy.context.object
4097 there_are_GP_strokes = False
4099 try:
4100 # Get the active grease pencil layer
4101 strokes_num = len(self.main_curve.grease_pencil.layers.active.active_frame.strokes)
4103 if strokes_num > 0:
4104 there_are_GP_strokes = True
4105 except:
4106 pass
4108 if there_are_GP_strokes:
4109 self.execute(context)
4110 self.report({'INFO'}, "Splines have been reordered")
4111 else:
4112 self.report({'WARNING'}, "Draw grease pencil strokes to connect splines")
4114 return {"FINISHED"}
4116 # ----------------------------
4117 # Set first points operator
4118 class CURVE_OT_SURFSK_first_points(Operator):
4119 bl_idname = "curve.surfsk_first_points"
4120 bl_label = "Bsurfaces set first points"
4121 bl_description = "Set the selected points as the first point of each spline"
4122 bl_options = {'REGISTER', 'UNDO'}
4124 def execute(self, context):
4125 splines_to_invert = []
4127 # Check non-cyclic splines to invert
4128 for i in range(len(self.main_curve.data.splines)):
4129 b_points = self.main_curve.data.splines[i].bezier_points
4131 if i not in self.cyclic_splines: # Only for non-cyclic splines
4132 if b_points[len(b_points) - 1].select_control_point:
4133 splines_to_invert.append(i)
4135 # Reorder points of cyclic splines, and set all handles to "Automatic"
4137 # Check first selected point
4138 cyclic_splines_new_first_pt = {}
4139 for i in self.cyclic_splines:
4140 sp = self.main_curve.data.splines[i]
4142 for t in range(len(sp.bezier_points)):
4143 bp = sp.bezier_points[t]
4144 if bp.select_control_point or bp.select_right_handle or bp.select_left_handle:
4145 cyclic_splines_new_first_pt[i] = t
4146 break # To take only one if there are more
4148 # Reorder
4149 for spline_idx in cyclic_splines_new_first_pt:
4150 sp = self.main_curve.data.splines[spline_idx]
4152 spline_old_coords = []
4153 for bp_old in sp.bezier_points:
4154 coords = (bp_old.co[0], bp_old.co[1], bp_old.co[2])
4156 left_handle_type = str(bp_old.handle_left_type)
4157 left_handle_length = float(bp_old.handle_left.length)
4158 left_handle_xyz = (
4159 float(bp_old.handle_left.x),
4160 float(bp_old.handle_left.y),
4161 float(bp_old.handle_left.z)
4163 right_handle_type = str(bp_old.handle_right_type)
4164 right_handle_length = float(bp_old.handle_right.length)
4165 right_handle_xyz = (
4166 float(bp_old.handle_right.x),
4167 float(bp_old.handle_right.y),
4168 float(bp_old.handle_right.z)
4170 spline_old_coords.append(
4171 [coords, left_handle_type,
4172 right_handle_type, left_handle_length,
4173 right_handle_length, left_handle_xyz,
4174 right_handle_xyz]
4177 for t in range(len(sp.bezier_points)):
4178 bp = sp.bezier_points
4180 if t + cyclic_splines_new_first_pt[spline_idx] + 1 <= len(bp) - 1:
4181 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1
4182 else:
4183 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1 - len(bp)
4185 bp[t].co = Vector(spline_old_coords[new_index][0])
4187 bp[t].handle_left.length = spline_old_coords[new_index][3]
4188 bp[t].handle_right.length = spline_old_coords[new_index][4]
4190 bp[t].handle_left_type = "FREE"
4191 bp[t].handle_right_type = "FREE"
4193 bp[t].handle_left.x = spline_old_coords[new_index][5][0]
4194 bp[t].handle_left.y = spline_old_coords[new_index][5][1]
4195 bp[t].handle_left.z = spline_old_coords[new_index][5][2]
4197 bp[t].handle_right.x = spline_old_coords[new_index][6][0]
4198 bp[t].handle_right.y = spline_old_coords[new_index][6][1]
4199 bp[t].handle_right.z = spline_old_coords[new_index][6][2]
4201 bp[t].handle_left_type = spline_old_coords[new_index][1]
4202 bp[t].handle_right_type = spline_old_coords[new_index][2]
4204 # Invert the non-cyclic splines designated above
4205 for i in range(len(splines_to_invert)):
4206 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4208 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4209 self.main_curve.data.splines[splines_to_invert[i]].bezier_points[0].select_control_point = True
4210 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4212 bpy.ops.curve.switch_direction()
4214 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4216 # Keep selected the first vert of each spline
4217 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4218 for i in range(len(self.main_curve.data.splines)):
4219 if not self.main_curve.data.splines[i].use_cyclic_u:
4220 bp = self.main_curve.data.splines[i].bezier_points[0]
4221 else:
4222 bp = self.main_curve.data.splines[i].bezier_points[
4223 len(self.main_curve.data.splines[i].bezier_points) - 1
4226 bp.select_control_point = True
4227 bp.select_right_handle = True
4228 bp.select_left_handle = True
4230 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4232 return {'FINISHED'}
4234 def invoke(self, context, event):
4235 self.main_curve = bpy.context.object
4237 # Check if all curves are Bezier, and detect which ones are cyclic
4238 self.cyclic_splines = []
4239 for i in range(len(self.main_curve.data.splines)):
4240 if self.main_curve.data.splines[i].type != "BEZIER":
4241 self.report({'WARNING'}, "All splines must be Bezier type")
4243 return {'CANCELLED'}
4244 else:
4245 if self.main_curve.data.splines[i].use_cyclic_u:
4246 self.cyclic_splines.append(i)
4248 self.execute(context)
4249 self.report({'INFO'}, "First points have been set")
4251 return {'FINISHED'}
4254 # Add-ons Preferences Update Panel
4256 # Define Panel classes for updating
4257 panels = (
4258 VIEW3D_PT_tools_SURFSK_mesh,
4259 VIEW3D_PT_tools_SURFSK_curve
4263 def conver_gpencil_to_curve(self, context, pencil, type):
4264 newCurve = bpy.data.curves.new(type + '_curve', type='CURVE')
4265 newCurve.dimensions = '3D'
4266 CurveObject = object_utils.object_data_add(context, newCurve)
4267 error = False
4269 if type == 'GPensil':
4270 try:
4271 strokes = pencil.data.layers.active.active_frame.strokes
4272 except:
4273 error = True
4274 CurveObject.location = pencil.location
4275 CurveObject.rotation_euler = pencil.rotation_euler
4276 CurveObject.scale = pencil.scale
4277 elif type == 'Annotation':
4278 try:
4279 strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
4280 except:
4281 error = True
4282 CurveObject.location = (0.0, 0.0, 0.0)
4283 CurveObject.rotation_euler = (0.0, 0.0, 0.0)
4284 CurveObject.scale = (1.0, 1.0, 1.0)
4286 if not error:
4287 for i, _stroke in enumerate(strokes):
4288 stroke_points = strokes[i].points
4289 data_list = [ (point.co.x, point.co.y, point.co.z)
4290 for point in stroke_points ]
4291 points_to_add = len(data_list)-1
4293 flat_list = []
4294 for point in data_list:
4295 flat_list.extend(point)
4297 spline = newCurve.splines.new(type='BEZIER')
4298 spline.bezier_points.add(points_to_add)
4299 spline.bezier_points.foreach_set("co", flat_list)
4301 for point in spline.bezier_points:
4302 point.handle_left_type="AUTO"
4303 point.handle_right_type="AUTO"
4305 return CurveObject
4306 else:
4307 return None
4310 def update_panel(self, context):
4311 message = "Bsurfaces GPL Edition: Updating Panel locations has failed"
4312 try:
4313 for panel in panels:
4314 if "bl_rna" in panel.__dict__:
4315 bpy.utils.unregister_class(panel)
4317 for panel in panels:
4318 category = context.preferences.addons[__name__].preferences.category
4319 if category != 'Tool':
4320 panel.bl_category = context.preferences.addons[__name__].preferences.category
4321 else:
4322 context.preferences.addons[__name__].preferences.category = 'Edit'
4323 panel.bl_category = 'Edit'
4324 raise ValueError("You can not install add-ons in the Tool panel")
4325 bpy.utils.register_class(panel)
4327 except Exception as e:
4328 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
4329 pass
4331 def makeMaterial(name, diffuse):
4333 if name in bpy.data.materials:
4334 material = bpy.data.materials[name]
4335 material.diffuse_color = diffuse
4336 else:
4337 material = bpy.data.materials.new(name)
4338 material.diffuse_color = diffuse
4340 return material
4342 def update_mesh(self, context):
4343 try:
4344 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4345 bpy.ops.object.select_all(action='DESELECT')
4346 bpy.context.view_layer.update()
4347 global global_mesh_object
4348 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
4349 bpy.data.objects[global_mesh_object].select_set(True)
4350 bpy.context.view_layer.objects.active = bpy.data.objects[global_mesh_object]
4351 except:
4352 print("Select mesh object")
4354 def update_gpencil(self, context):
4355 try:
4356 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4357 bpy.ops.object.select_all(action='DESELECT')
4358 bpy.context.view_layer.update()
4359 global global_gpencil_object
4360 global_gpencil_object = bpy.context.scene.bsurfaces.SURFSK_gpencil.name
4361 bpy.data.objects[global_gpencil_object].select_set(True)
4362 bpy.context.view_layer.objects.active = bpy.data.objects[global_gpencil_object]
4363 except:
4364 print("Select gpencil object")
4366 def update_curve(self, context):
4367 try:
4368 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4369 bpy.ops.object.select_all(action='DESELECT')
4370 bpy.context.view_layer.update()
4371 global global_curve_object
4372 global_curve_object = bpy.context.scene.bsurfaces.SURFSK_curve.name
4373 bpy.data.objects[global_curve_object].select_set(True)
4374 bpy.context.view_layer.objects.active = bpy.data.objects[global_curve_object]
4375 except:
4376 print("Select curve object")
4378 def update_shade_smooth(self, context):
4379 try:
4380 global global_shade_smooth
4381 global_shade_smooth = bpy.context.scene.bsurfaces.SURFSK_shade_smooth
4383 contex_mode = bpy.context.mode
4385 if bpy.ops.object.mode_set.poll():
4386 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4388 bpy.ops.object.select_all(action='DESELECT')
4389 global global_mesh_object
4390 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
4391 bpy.data.objects[global_mesh_object].select_set(True)
4393 if global_shade_smooth:
4394 bpy.ops.object.shade_smooth()
4395 else:
4396 bpy.ops.object.shade_flat()
4398 if contex_mode == "EDIT_MESH":
4399 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4401 except:
4402 print("Select mesh object")
4405 class BsurfPreferences(AddonPreferences):
4406 # this must match the addon name, use '__package__'
4407 # when defining this in a submodule of a python package.
4408 bl_idname = __name__
4410 category: StringProperty(
4411 name="Tab Category",
4412 description="Choose a name for the category of the panel",
4413 default="Edit",
4414 update=update_panel
4417 def draw(self, context):
4418 layout = self.layout
4420 row = layout.row()
4421 col = row.column()
4422 col.label(text="Tab Category:")
4423 col.prop(self, "category", text="")
4425 # Properties
4426 class BsurfacesProps(PropertyGroup):
4427 SURFSK_guide: EnumProperty(
4428 name="Guide:",
4429 items=[
4430 ('Annotation', 'Annotation', 'Annotation'),
4431 ('GPencil', 'GPencil', 'GPencil'),
4432 ('Curve', 'Curve', 'Curve')
4434 default="Annotation"
4436 SURFSK_edges_U: IntProperty(
4437 name="Cross",
4438 description="Number of face-loops crossing the strokes",
4439 default=5,
4440 min=1,
4441 max=200
4443 SURFSK_edges_V: IntProperty(
4444 name="Follow",
4445 description="Number of face-loops following the strokes",
4446 default=1,
4447 min=1,
4448 max=200
4450 SURFSK_cyclic_cross: BoolProperty(
4451 name="Cyclic Cross",
4452 description="Make cyclic the face-loops crossing the strokes",
4453 default=False
4455 SURFSK_cyclic_follow: BoolProperty(
4456 name="Cyclic Follow",
4457 description="Make cyclic the face-loops following the strokes",
4458 default=False
4460 SURFSK_keep_strokes: BoolProperty(
4461 name="Keep strokes",
4462 description="Keeps the sketched strokes or curves after adding the surface",
4463 default=False
4465 SURFSK_automatic_join: BoolProperty(
4466 name="Automatic join",
4467 description="Join automatically vertices of either surfaces "
4468 "generated by crosshatching, or from the borders of closed shapes",
4469 default=True
4471 SURFSK_loops_on_strokes: BoolProperty(
4472 name="Loops on strokes",
4473 description="Make the loops match the paths of the strokes",
4474 default=True
4476 SURFSK_precision: IntProperty(
4477 name="Precision",
4478 description="Precision level of the surface calculation",
4479 default=2,
4480 min=1,
4481 max=100
4483 SURFSK_mesh: PointerProperty(
4484 name="Mesh of BSurface",
4485 type=bpy.types.Object,
4486 description="Mesh of BSurface",
4487 update=update_mesh,
4489 SURFSK_gpencil: PointerProperty(
4490 name="GreasePencil object",
4491 type=bpy.types.Object,
4492 description="GreasePencil object",
4493 update=update_gpencil,
4495 SURFSK_curve: PointerProperty(
4496 name="Curve object",
4497 type=bpy.types.Object,
4498 description="Curve object",
4499 update=update_curve,
4501 SURFSK_shade_smooth: BoolProperty(
4502 name="Shade smooth",
4503 description="Render and display faces smooth, using interpolated Vertex Normals",
4504 default=False,
4505 update=update_shade_smooth,
4508 classes = (
4509 MESH_OT_SURFSK_init,
4510 MESH_OT_SURFSK_add_modifiers,
4511 MESH_OT_SURFSK_add_surface,
4512 MESH_OT_SURFSK_edit_surface,
4513 GPENCIL_OT_SURFSK_add_strokes,
4514 GPENCIL_OT_SURFSK_edit_strokes,
4515 GPENCIL_OT_SURFSK_strokes_to_curves,
4516 GPENCIL_OT_SURFSK_annotation_to_curves,
4517 GPENCIL_OT_SURFSK_add_annotation,
4518 CURVE_OT_SURFSK_edit_curve,
4519 CURVE_OT_SURFSK_reorder_splines,
4520 CURVE_OT_SURFSK_first_points,
4521 BsurfPreferences,
4522 BsurfacesProps
4525 def register():
4526 for cls in classes:
4527 bpy.utils.register_class(cls)
4529 for panel in panels:
4530 bpy.utils.register_class(panel)
4532 bpy.types.Scene.bsurfaces = PointerProperty(type=BsurfacesProps)
4533 update_panel(None, bpy.context)
4535 def unregister():
4536 for panel in panels:
4537 bpy.utils.unregister_class(panel)
4539 for cls in classes:
4540 bpy.utils.unregister_class(cls)
4542 del bpy.types.Scene.bsurfaces
4544 if __name__ == "__main__":
4545 register()