Fix object_fracture_cell, collection option
[blender-addons.git] / mesh_bsurfaces.py
blob1fc761ae60083e3176869a58cfbd76c6814f7345
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; version 2
6 # of the License.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
20 bl_info = {
21 "name": "Bsurfaces GPL Edition",
22 "author": "Eclectiel, Spivak Vladimir(cwolf3d)",
23 "version": (1, 6, 0),
24 "blender": (2, 80, 0),
25 "location": "View3D EditMode > Sidebar > Edit Tab",
26 "description": "Modeling and retopology tool",
27 "wiki_url": "https://wiki.blender.org/index.php/Dev:Ref/Release_Notes/2.64/Bsurfaces_1.5",
28 "category": "Mesh",
32 import bpy
33 import bmesh
34 from bpy_extras import object_utils
36 import operator
37 from mathutils import Matrix, Vector
38 from mathutils.geometry import (
39 intersect_line_line,
40 intersect_point_line,
42 from math import (
43 degrees,
44 pi,
45 sqrt,
47 from bpy.props import (
48 BoolProperty,
49 FloatProperty,
50 IntProperty,
51 StringProperty,
52 PointerProperty,
54 from bpy.types import (
55 Operator,
56 Panel,
57 PropertyGroup,
58 AddonPreferences,
62 class VIEW3D_PT_tools_SURFSK_mesh(Panel):
63 bl_space_type = 'VIEW_3D'
64 bl_region_type = 'UI'
65 bl_category = 'Edit'
66 #bl_context = "mesh_edit"
67 bl_label = "Bsurfaces"
69 def draw(self, context):
70 layout = self.layout
71 scn = context.scene.bsurfaces
73 col = layout.column(align=True)
74 row = layout.row()
75 row.separator()
76 col.operator("gpencil.surfsk_init", text="Initialize")
77 col.prop(scn, "SURFSK_object_with_retopology")
78 col.prop(scn, "SURFSK_use_annotation")
79 if not scn.SURFSK_use_annotation:
80 col.prop(scn, "SURFSK_object_with_strokes")
81 col.separator()
82 col.operator("gpencil.surfsk_add_surface", text="Add Surface")
83 col.operator("gpencil.surfsk_edit_surface", text="Edit Surface")
84 if not scn.SURFSK_use_annotation:
85 col.operator("gpencil.surfsk_add_strokes", text="Add Strokes")
86 col.operator("gpencil.surfsk_edit_strokes", text="Edit Strokes")
87 else:
88 col.operator("gpencil.surfsk_add_annotation", text="Add Annotation")
89 col.separator()
90 col.prop(scn, "SURFSK_edges_U")
91 col.prop(scn, "SURFSK_edges_V")
92 col.prop(scn, "SURFSK_cyclic_cross")
93 col.prop(scn, "SURFSK_cyclic_follow")
94 col.prop(scn, "SURFSK_loops_on_strokes")
95 col.prop(scn, "SURFSK_automatic_join")
96 col.prop(scn, "SURFSK_keep_strokes")
98 class VIEW3D_PT_tools_SURFSK_curve(Panel):
99 bl_space_type = 'VIEW_3D'
100 bl_region_type = 'UI'
101 bl_context = "curve_edit"
102 bl_category = 'Edit'
103 bl_label = "Bsurfaces"
105 @classmethod
106 def poll(cls, context):
107 return context.active_object
109 def draw(self, context):
110 layout = self.layout
112 col = layout.column(align=True)
113 row = layout.row()
114 row.separator()
115 col.operator("curve.surfsk_first_points", text="Set First Points")
116 col.operator("curve.switch_direction", text="Switch Direction")
117 col.operator("curve.surfsk_reorder_splines", text="Reorder Splines")
120 # Returns the type of strokes used
121 def get_strokes_type(context):
122 strokes_type = ""
123 strokes_num = 0
125 # Check if they are grease pencil
126 if context.scene.bsurfaces.SURFSK_use_annotation:
127 try:
128 frame = bpy.data.grease_pencils["Annotations"].layers["Note"].active_frame
130 strokes_num = len(frame.strokes)
132 if strokes_num > 0:
133 strokes_type = "GP_ANNOTATION"
134 except:
135 pass
137 try:
138 gpencil = bpy.context.scene.bsurfaces.SURFSK_object_with_strokes
139 layer = gpencil.data.layers[0]
140 frame = layer.frames[0]
142 strokes_num = len(frame.strokes)
144 if strokes_num > 0:
145 strokes_type = "GP_STROKES"
146 except:
147 pass
149 # Check if they are mesh
150 try:
151 main_object = bpy.context.scene.bsurfaces.SURFSK_object_with_retopology
152 except:
153 pass
155 # Check if they are curves, if there aren't grease pencil strokes
156 if strokes_type == "":
157 if len(bpy.context.selected_objects) == 2:
158 for ob in bpy.context.selected_objects:
159 if ob != bpy.context.view_layer.objects.active and ob.type == "CURVE":
160 strokes_type = "EXTERNAL_CURVE"
161 strokes_num = len(ob.data.splines)
163 # Check if there is any non-bezier spline
164 for i in range(len(ob.data.splines)):
165 if ob.data.splines[i].type != "BEZIER":
166 strokes_type = "CURVE_WITH_NON_BEZIER_SPLINES"
167 break
169 elif ob != bpy.context.view_layer.objects.active and ob.type != "CURVE":
170 strokes_type = "EXTERNAL_NO_CURVE"
171 elif len(bpy.context.selected_objects) > 2:
172 strokes_type = "MORE_THAN_ONE_EXTERNAL"
174 # Check if there is a single stroke without any selection in the object
175 if strokes_num == 1 and main_object.data.total_vert_sel == 0:
176 if strokes_type == "EXTERNAL_CURVE":
177 strokes_type = "SINGLE_CURVE_STROKE_NO_SELECTION"
178 elif strokes_type == "GP_STROKES":
179 strokes_type = "SINGLE_GP_STROKE_NO_SELECTION"
181 if strokes_num == 0 and main_object.data.total_vert_sel > 0:
182 strokes_type = "SELECTION_ALONE"
184 if strokes_type == "":
185 strokes_type = "NO_STROKES"
187 return strokes_type
190 # Surface generator operator
191 class GPENCIL_OT_SURFSK_add_surface(Operator):
192 bl_idname = "gpencil.surfsk_add_surface"
193 bl_label = "Bsurfaces add surface"
194 bl_description = "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
195 bl_options = {'REGISTER', 'UNDO'}
197 is_fill_faces: BoolProperty(
198 default=False
200 selection_U_exists: BoolProperty(
201 default=False
203 selection_V_exists: BoolProperty(
204 default=False
206 selection_U2_exists: BoolProperty(
207 default=False
209 selection_V2_exists: BoolProperty(
210 default=False
212 selection_V_is_closed: BoolProperty(
213 default=False
215 selection_U_is_closed: BoolProperty(
216 default=False
218 selection_V2_is_closed: BoolProperty(
219 default=False
221 selection_U2_is_closed: BoolProperty(
222 default=False
225 edges_U: IntProperty(
226 name="Cross",
227 description="Number of face-loops crossing the strokes",
228 default=1,
229 min=1,
230 max=200
232 edges_V: IntProperty(
233 name="Follow",
234 description="Number of face-loops following the strokes",
235 default=1,
236 min=1,
237 max=200
239 cyclic_cross: BoolProperty(
240 name="Cyclic Cross",
241 description="Make cyclic the face-loops crossing the strokes",
242 default=False
244 cyclic_follow: BoolProperty(
245 name="Cyclic Follow",
246 description="Make cyclic the face-loops following the strokes",
247 default=False
249 loops_on_strokes: BoolProperty(
250 name="Loops on strokes",
251 description="Make the loops match the paths of the strokes",
252 default=False
254 automatic_join: BoolProperty(
255 name="Automatic join",
256 description="Join automatically vertices of either surfaces generated "
257 "by crosshatching, or from the borders of closed shapes",
258 default=False
260 join_stretch_factor: FloatProperty(
261 name="Stretch",
262 description="Amount of stretching or shrinking allowed for "
263 "edges when joining vertices automatically",
264 default=1,
265 min=0,
266 max=3,
267 subtype='FACTOR'
269 keep_strokes: BoolProperty(
270 name="Keep strokes",
271 description="Keeps the sketched strokes or curves after adding the surface",
272 default=False
274 strokes_type: StringProperty()
275 initial_global_undo_state: BoolProperty()
278 def draw(self, context):
279 layout = self.layout
280 col = layout.column(align=True)
281 row = layout.row()
283 if not self.is_fill_faces:
284 row.separator()
285 if not self.is_crosshatch:
286 if not self.selection_U_exists:
287 col.prop(self, "edges_U")
288 row.separator()
290 if not self.selection_V_exists:
291 col.prop(self, "edges_V")
292 row.separator()
294 row.separator()
296 if not self.selection_U_exists:
297 if not (
298 (self.selection_V_exists and not self.selection_V_is_closed) or
299 (self.selection_V2_exists and not self.selection_V2_is_closed)
301 col.prop(self, "cyclic_cross")
303 if not self.selection_V_exists:
304 if not (
305 (self.selection_U_exists and not self.selection_U_is_closed) or
306 (self.selection_U2_exists and not self.selection_U2_is_closed)
308 col.prop(self, "cyclic_follow")
310 col.prop(self, "loops_on_strokes")
312 col.prop(self, "automatic_join")
314 if self.automatic_join:
315 row.separator()
316 col.separator()
317 row.separator()
318 col.prop(self, "join_stretch_factor")
320 col.prop(self, "keep_strokes")
322 # Get an ordered list of a chain of vertices
323 def get_ordered_verts(self, ob, all_selected_edges_idx, all_selected_verts_idx,
324 first_vert_idx, middle_vertex_idx, closing_vert_idx):
325 # Order selected vertices.
326 verts_ordered = []
327 if closing_vert_idx is not None:
328 verts_ordered.append(ob.data.vertices[closing_vert_idx])
330 verts_ordered.append(ob.data.vertices[first_vert_idx])
331 prev_v = first_vert_idx
332 prev_ed = None
333 finish_while = False
334 while True:
335 edges_non_matched = 0
336 for i in all_selected_edges_idx:
337 if ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[0] == prev_v and \
338 ob.data.edges[i].vertices[1] in all_selected_verts_idx:
340 verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[1]])
341 prev_v = ob.data.edges[i].vertices[1]
342 prev_ed = ob.data.edges[i]
343 elif ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[1] == prev_v and \
344 ob.data.edges[i].vertices[0] in all_selected_verts_idx:
346 verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[0]])
347 prev_v = ob.data.edges[i].vertices[0]
348 prev_ed = ob.data.edges[i]
349 else:
350 edges_non_matched += 1
352 if edges_non_matched == len(all_selected_edges_idx):
353 finish_while = True
355 if finish_while:
356 break
358 if closing_vert_idx is not None:
359 verts_ordered.append(ob.data.vertices[closing_vert_idx])
361 if middle_vertex_idx is not None:
362 verts_ordered.append(ob.data.vertices[middle_vertex_idx])
363 verts_ordered.reverse()
365 return tuple(verts_ordered)
367 # Calculates length of a chain of points.
368 def get_chain_length(self, object, verts_ordered):
369 matrix = object.matrix_world
371 edges_lengths = []
372 edges_lengths_sum = 0
373 for i in range(0, len(verts_ordered)):
374 if i == 0:
375 prev_v_co = matrix @ verts_ordered[i].co
376 else:
377 v_co = matrix @ verts_ordered[i].co
379 v_difs = [prev_v_co[0] - v_co[0], prev_v_co[1] - v_co[1], prev_v_co[2] - v_co[2]]
380 edge_length = abs(sqrt(v_difs[0] * v_difs[0] + v_difs[1] * v_difs[1] + v_difs[2] * v_difs[2]))
382 edges_lengths.append(edge_length)
383 edges_lengths_sum += edge_length
385 prev_v_co = v_co
387 return edges_lengths, edges_lengths_sum
389 # Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
390 def get_edges_proportions(self, edges_lengths, edges_lengths_sum, use_boundaries, fixed_edges_num):
391 edges_proportions = []
392 if use_boundaries:
393 verts_count = 1
394 for l in edges_lengths:
395 edges_proportions.append(l / edges_lengths_sum)
396 verts_count += 1
397 else:
398 verts_count = 1
399 for n in range(0, fixed_edges_num):
400 edges_proportions.append(1 / fixed_edges_num)
401 verts_count += 1
403 return edges_proportions
405 # Calculates the angle between two pairs of points in space
406 def orientation_difference(self, points_A_co, points_B_co):
407 # each parameter should be a list with two elements,
408 # and each element should be a x,y,z coordinate
409 vec_A = points_A_co[0] - points_A_co[1]
410 vec_B = points_B_co[0] - points_B_co[1]
412 angle = vec_A.angle(vec_B)
414 if angle > 0.5 * pi:
415 angle = abs(angle - pi)
417 return angle
419 # Calculate the which vert of verts_idx list is the nearest one
420 # to the point_co coordinates, and the distance
421 def shortest_distance(self, object, point_co, verts_idx):
422 matrix = object.matrix_world
424 for i in range(0, len(verts_idx)):
425 dist = (point_co - matrix @ object.data.vertices[verts_idx[i]].co).length
426 if i == 0:
427 prev_dist = dist
428 nearest_vert_idx = verts_idx[i]
429 shortest_dist = dist
431 if dist < prev_dist:
432 prev_dist = dist
433 nearest_vert_idx = verts_idx[i]
434 shortest_dist = dist
436 return nearest_vert_idx, shortest_dist
438 # Returns the index of the opposite vert tip in a chain, given a vert tip index
439 # as parameter, and a multidimentional list with all pairs of tips
440 def opposite_tip(self, vert_tip_idx, all_chains_tips_idx):
441 opposite_vert_tip_idx = None
442 for i in range(0, len(all_chains_tips_idx)):
443 if vert_tip_idx == all_chains_tips_idx[i][0]:
444 opposite_vert_tip_idx = all_chains_tips_idx[i][1]
445 if vert_tip_idx == all_chains_tips_idx[i][1]:
446 opposite_vert_tip_idx = all_chains_tips_idx[i][0]
448 return opposite_vert_tip_idx
450 # Simplifies a spline and returns the new points coordinates
451 def simplify_spline(self, spline_coords, segments_num):
452 simplified_spline = []
453 points_between_segments = round(len(spline_coords) / segments_num)
455 simplified_spline.append(spline_coords[0])
456 for i in range(1, segments_num):
457 simplified_spline.append(spline_coords[i * points_between_segments])
459 simplified_spline.append(spline_coords[len(spline_coords) - 1])
461 return simplified_spline
463 # Cleans up the scene and gets it the same it was at the beginning,
464 # in case the script is interrupted in the middle of the execution
465 def cleanup_on_interruption(self):
466 # If the original strokes curve comes from conversion
467 # from grease pencil and wasn't made by hand, delete it
468 if not self.using_external_curves:
469 try:
470 bpy.ops.object.delete({"selected_objects": [self.original_curve]})
471 except:
472 pass
474 #bpy.ops.object.delete({"selected_objects": [self.main_object]})
475 else:
476 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
477 self.original_curve.select_set(True)
478 self.main_object.select_set(True)
479 bpy.context.view_layer.objects.active = self.main_object
481 #bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
482 bpy.ops.object.mode_set(mode='OBJECT')
484 # Returns a list with the coords of the points distributed over the splines
485 # passed to this method according to the proportions parameter
486 def distribute_pts(self, surface_splines, proportions):
488 # Calculate the length of each final surface spline
489 surface_splines_lengths = []
490 surface_splines_parsed = []
492 for sp_idx in range(0, len(surface_splines)):
493 # Calculate spline length
494 surface_splines_lengths.append(0)
496 for i in range(0, len(surface_splines[sp_idx].bezier_points)):
497 if i == 0:
498 prev_p = surface_splines[sp_idx].bezier_points[i]
499 else:
500 p = surface_splines[sp_idx].bezier_points[i]
501 edge_length = (prev_p.co - p.co).length
502 surface_splines_lengths[sp_idx] += edge_length
504 prev_p = p
506 # Calculate vertex positions with appropriate edge proportions, and ordered, for each spline
507 for sp_idx in range(0, len(surface_splines)):
508 surface_splines_parsed.append([])
509 surface_splines_parsed[sp_idx].append(surface_splines[sp_idx].bezier_points[0].co)
511 prev_p_co = surface_splines[sp_idx].bezier_points[0].co
512 p_idx = 0
514 for prop_idx in range(len(proportions) - 1):
515 target_length = surface_splines_lengths[sp_idx] * proportions[prop_idx]
516 partial_segment_length = 0
517 finish_while = False
519 while True:
520 # if not it'll pass the p_idx as an index below and crash
521 if p_idx < len(surface_splines[sp_idx].bezier_points):
522 p_co = surface_splines[sp_idx].bezier_points[p_idx].co
523 new_dist = (prev_p_co - p_co).length
525 # The new distance that could have the partial segment if
526 # it is still shorter than the target length
527 potential_segment_length = partial_segment_length + new_dist
529 # If the potential is still shorter, keep adding
530 if potential_segment_length < target_length:
531 partial_segment_length = potential_segment_length
533 p_idx += 1
534 prev_p_co = p_co
536 # If the potential is longer than the target, calculate the target
537 # (a point between the last two points), and assign
538 elif potential_segment_length > target_length:
539 remaining_dist = target_length - partial_segment_length
540 vec = p_co - prev_p_co
541 vec.normalize()
542 intermediate_co = prev_p_co + (vec * remaining_dist)
544 surface_splines_parsed[sp_idx].append(intermediate_co)
546 partial_segment_length += remaining_dist
547 prev_p_co = intermediate_co
549 finish_while = True
551 # If the potential is equal to the target, assign
552 elif potential_segment_length == target_length:
553 surface_splines_parsed[sp_idx].append(p_co)
554 prev_p_co = p_co
556 finish_while = True
558 if finish_while:
559 break
561 # last point of the spline
562 surface_splines_parsed[sp_idx].append(
563 surface_splines[sp_idx].bezier_points[len(surface_splines[sp_idx].bezier_points) - 1].co
566 return surface_splines_parsed
568 # Counts the number of faces that belong to each edge
569 def edge_face_count(self, ob):
570 ed_keys_count_dict = {}
572 for face in ob.data.polygons:
573 for ed_keys in face.edge_keys:
574 if ed_keys not in ed_keys_count_dict:
575 ed_keys_count_dict[ed_keys] = 1
576 else:
577 ed_keys_count_dict[ed_keys] += 1
579 edge_face_count = []
580 for i in range(len(ob.data.edges)):
581 edge_face_count.append(0)
583 for i in range(len(ob.data.edges)):
584 ed = ob.data.edges[i]
586 v1 = ed.vertices[0]
587 v2 = ed.vertices[1]
589 if (v1, v2) in ed_keys_count_dict:
590 edge_face_count[i] = ed_keys_count_dict[(v1, v2)]
591 elif (v2, v1) in ed_keys_count_dict:
592 edge_face_count[i] = ed_keys_count_dict[(v2, v1)]
594 return edge_face_count
596 # Fills with faces all the selected vertices which form empty triangles or quads
597 def fill_with_faces(self, object):
598 all_selected_verts_count = self.main_object_selected_verts_count
600 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
602 # Calculate average length of selected edges
603 all_selected_verts = []
604 original_sel_edges_count = 0
605 for ed in object.data.edges:
606 if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
607 coords = []
608 coords.append(object.data.vertices[ed.vertices[0]].co)
609 coords.append(object.data.vertices[ed.vertices[1]].co)
611 original_sel_edges_count += 1
613 if not ed.vertices[0] in all_selected_verts:
614 all_selected_verts.append(ed.vertices[0])
616 if not ed.vertices[1] in all_selected_verts:
617 all_selected_verts.append(ed.vertices[1])
619 tuple(all_selected_verts)
621 # Check if there is any edge selected. If not, interrupt the script
622 if original_sel_edges_count == 0 and all_selected_verts_count > 0:
623 return 0
625 # Get all edges connected to selected verts
626 all_edges_around_sel_verts = []
627 edges_connected_to_sel_verts = {}
628 verts_connected_to_every_vert = {}
629 for ed_idx in range(len(object.data.edges)):
630 ed = object.data.edges[ed_idx]
631 include_edge = False
633 if ed.vertices[0] in all_selected_verts:
634 if not ed.vertices[0] in edges_connected_to_sel_verts:
635 edges_connected_to_sel_verts[ed.vertices[0]] = []
637 edges_connected_to_sel_verts[ed.vertices[0]].append(ed_idx)
638 include_edge = True
640 if ed.vertices[1] in all_selected_verts:
641 if not ed.vertices[1] in edges_connected_to_sel_verts:
642 edges_connected_to_sel_verts[ed.vertices[1]] = []
644 edges_connected_to_sel_verts[ed.vertices[1]].append(ed_idx)
645 include_edge = True
647 if include_edge is True:
648 all_edges_around_sel_verts.append(ed_idx)
650 # Get all connected verts to each vert
651 if not ed.vertices[0] in verts_connected_to_every_vert:
652 verts_connected_to_every_vert[ed.vertices[0]] = []
654 if not ed.vertices[1] in verts_connected_to_every_vert:
655 verts_connected_to_every_vert[ed.vertices[1]] = []
657 verts_connected_to_every_vert[ed.vertices[0]].append(ed.vertices[1])
658 verts_connected_to_every_vert[ed.vertices[1]].append(ed.vertices[0])
660 # Get all verts connected to faces
661 all_verts_part_of_faces = []
662 all_edges_faces_count = []
663 all_edges_faces_count += self.edge_face_count(object)
665 # Get only the selected edges that have faces attached.
666 count_faces_of_edges_around_sel_verts = {}
667 selected_verts_with_faces = []
668 for ed_idx in all_edges_around_sel_verts:
669 count_faces_of_edges_around_sel_verts[ed_idx] = all_edges_faces_count[ed_idx]
671 if all_edges_faces_count[ed_idx] > 0:
672 ed = object.data.edges[ed_idx]
674 if not ed.vertices[0] in selected_verts_with_faces:
675 selected_verts_with_faces.append(ed.vertices[0])
677 if not ed.vertices[1] in selected_verts_with_faces:
678 selected_verts_with_faces.append(ed.vertices[1])
680 all_verts_part_of_faces.append(ed.vertices[0])
681 all_verts_part_of_faces.append(ed.vertices[1])
683 tuple(selected_verts_with_faces)
685 # Discard unneeded verts from calculations
686 participating_verts = []
687 movable_verts = []
688 for v_idx in all_selected_verts:
689 vert_has_edges_with_one_face = False
691 # Check if the actual vert has at least one edge connected to only one face
692 for ed_idx in edges_connected_to_sel_verts[v_idx]:
693 if count_faces_of_edges_around_sel_verts[ed_idx] == 1:
694 vert_has_edges_with_one_face = True
696 # If the vert has two or less edges connected and the vert is not part of any face.
697 # Or the vert is part of any face and at least one of
698 # the connected edges has only one face attached to it.
699 if (len(edges_connected_to_sel_verts[v_idx]) == 2 and
700 v_idx not in all_verts_part_of_faces) or \
701 len(edges_connected_to_sel_verts[v_idx]) == 1 or \
702 (v_idx in all_verts_part_of_faces and
703 vert_has_edges_with_one_face):
705 participating_verts.append(v_idx)
707 if v_idx not in all_verts_part_of_faces:
708 movable_verts.append(v_idx)
710 # Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
711 for mv_idx in movable_verts:
712 freeze_vert = False
713 mv_connected_verts = verts_connected_to_every_vert[mv_idx]
715 for actual_v_idx in all_selected_verts:
716 count_shared_neighbors = 0
717 checked_verts = []
719 for mv_conn_v_idx in mv_connected_verts:
720 if mv_idx != actual_v_idx:
721 if mv_conn_v_idx in verts_connected_to_every_vert[actual_v_idx] and \
722 mv_conn_v_idx not in checked_verts:
723 count_shared_neighbors += 1
724 checked_verts.append(mv_conn_v_idx)
726 if actual_v_idx in mv_connected_verts:
727 freeze_vert = True
728 break
730 if count_shared_neighbors == 2:
731 freeze_vert = True
732 break
734 if freeze_vert:
735 break
737 if freeze_vert:
738 movable_verts.remove(mv_idx)
740 # Calculate merge distance for participating verts
741 shortest_edge_length = None
742 for ed in object.data.edges:
743 if ed.vertices[0] in movable_verts and ed.vertices[1] in movable_verts:
744 v1 = object.data.vertices[ed.vertices[0]]
745 v2 = object.data.vertices[ed.vertices[1]]
747 length = (v1.co - v2.co).length
749 if shortest_edge_length is None:
750 shortest_edge_length = length
751 else:
752 if length < shortest_edge_length:
753 shortest_edge_length = length
755 if shortest_edge_length is not None:
756 edges_merge_distance = shortest_edge_length * 0.5
757 else:
758 edges_merge_distance = 0
760 # Get together the verts near enough. They will be merged later
761 remaining_verts = []
762 remaining_verts += participating_verts
763 for v1_idx in participating_verts:
764 if v1_idx in remaining_verts and v1_idx in movable_verts:
765 verts_to_merge = []
766 coords_verts_to_merge = {}
768 verts_to_merge.append(v1_idx)
770 v1_co = object.data.vertices[v1_idx].co
771 coords_verts_to_merge[v1_idx] = (v1_co[0], v1_co[1], v1_co[2])
773 for v2_idx in remaining_verts:
774 if v1_idx != v2_idx:
775 v2_co = object.data.vertices[v2_idx].co
777 dist = (v1_co - v2_co).length
779 if dist <= edges_merge_distance: # Add the verts which are near enough
780 verts_to_merge.append(v2_idx)
782 coords_verts_to_merge[v2_idx] = (v2_co[0], v2_co[1], v2_co[2])
784 for vm_idx in verts_to_merge:
785 remaining_verts.remove(vm_idx)
787 if len(verts_to_merge) > 1:
788 # Calculate middle point of the verts to merge.
789 sum_x_co = 0
790 sum_y_co = 0
791 sum_z_co = 0
792 movable_verts_to_merge_count = 0
793 for i in range(len(verts_to_merge)):
794 if verts_to_merge[i] in movable_verts:
795 v_co = object.data.vertices[verts_to_merge[i]].co
797 sum_x_co += v_co[0]
798 sum_y_co += v_co[1]
799 sum_z_co += v_co[2]
801 movable_verts_to_merge_count += 1
803 middle_point_co = [
804 sum_x_co / movable_verts_to_merge_count,
805 sum_y_co / movable_verts_to_merge_count,
806 sum_z_co / movable_verts_to_merge_count
809 # Check if any vert to be merged is not movable
810 shortest_dist = None
811 are_verts_not_movable = False
812 verts_not_movable = []
813 for v_merge_idx in verts_to_merge:
814 if v_merge_idx in participating_verts and v_merge_idx not in movable_verts:
815 are_verts_not_movable = True
816 verts_not_movable.append(v_merge_idx)
818 if are_verts_not_movable:
819 # Get the vert connected to faces, that is nearest to
820 # the middle point of the movable verts
821 shortest_dist = None
822 for vcf_idx in verts_not_movable:
823 dist = abs((object.data.vertices[vcf_idx].co -
824 Vector(middle_point_co)).length)
826 if shortest_dist is None:
827 shortest_dist = dist
828 nearest_vert_idx = vcf_idx
829 else:
830 if dist < shortest_dist:
831 shortest_dist = dist
832 nearest_vert_idx = vcf_idx
834 coords = object.data.vertices[nearest_vert_idx].co
835 target_point_co = [coords[0], coords[1], coords[2]]
836 else:
837 target_point_co = middle_point_co
839 # Move verts to merge to the middle position
840 for v_merge_idx in verts_to_merge:
841 if v_merge_idx in movable_verts: # Only move the verts that are not part of faces
842 object.data.vertices[v_merge_idx].co[0] = target_point_co[0]
843 object.data.vertices[v_merge_idx].co[1] = target_point_co[1]
844 object.data.vertices[v_merge_idx].co[2] = target_point_co[2]
846 # Perform "Remove Doubles" to weld all the disconnected verts
847 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
848 bpy.ops.mesh.remove_doubles(threshold=0.0001)
850 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
852 # Get all the definitive selected edges, after weldding
853 selected_edges = []
854 edges_per_vert = {} # Number of faces of each selected edge
855 for ed in object.data.edges:
856 if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
857 selected_edges.append(ed.index)
859 # Save all the edges that belong to each vertex.
860 if not ed.vertices[0] in edges_per_vert:
861 edges_per_vert[ed.vertices[0]] = []
863 if not ed.vertices[1] in edges_per_vert:
864 edges_per_vert[ed.vertices[1]] = []
866 edges_per_vert[ed.vertices[0]].append(ed.index)
867 edges_per_vert[ed.vertices[1]].append(ed.index)
869 # Check if all the edges connected to each vert have two faces attached to them.
870 # To discard them later and make calculations faster
871 a = []
872 a += self.edge_face_count(object)
873 tuple(a)
874 verts_surrounded_by_faces = {}
875 for v_idx in edges_per_vert:
876 edges = edges_per_vert[v_idx]
877 edges_with_two_faces_count = 0
879 for ed_idx in edges_per_vert[v_idx]:
880 if a[ed_idx] == 2:
881 edges_with_two_faces_count += 1
883 if edges_with_two_faces_count == len(edges_per_vert[v_idx]):
884 verts_surrounded_by_faces[v_idx] = True
885 else:
886 verts_surrounded_by_faces[v_idx] = False
888 # Get all the selected vertices
889 selected_verts_idx = []
890 for v in object.data.vertices:
891 if v.select:
892 selected_verts_idx.append(v.index)
894 # Get all the faces of the object
895 all_object_faces_verts_idx = []
896 for face in object.data.polygons:
897 face_verts = []
898 face_verts.append(face.vertices[0])
899 face_verts.append(face.vertices[1])
900 face_verts.append(face.vertices[2])
902 if len(face.vertices) == 4:
903 face_verts.append(face.vertices[3])
905 all_object_faces_verts_idx.append(face_verts)
907 # Deselect all vertices
908 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
909 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
910 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
912 # Make a dictionary with the verts related to each vert
913 related_key_verts = {}
914 for ed_idx in selected_edges:
915 ed = object.data.edges[ed_idx]
917 if not verts_surrounded_by_faces[ed.vertices[0]]:
918 if not ed.vertices[0] in related_key_verts:
919 related_key_verts[ed.vertices[0]] = []
921 if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
922 related_key_verts[ed.vertices[0]].append(ed.vertices[1])
924 if not verts_surrounded_by_faces[ed.vertices[1]]:
925 if not ed.vertices[1] in related_key_verts:
926 related_key_verts[ed.vertices[1]] = []
928 if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
929 related_key_verts[ed.vertices[1]].append(ed.vertices[0])
931 # Get groups of verts forming each face
932 faces_verts_idx = []
933 for v1 in related_key_verts: # verts-1 ....
934 for v2 in related_key_verts: # verts-2
935 if v1 != v2:
936 related_verts_in_common = []
937 v2_in_rel_v1 = False
938 v1_in_rel_v2 = False
939 for rel_v1 in related_key_verts[v1]:
940 # Check if related verts of verts-1 are related verts of verts-2
941 if rel_v1 in related_key_verts[v2]:
942 related_verts_in_common.append(rel_v1)
944 if v2 in related_key_verts[v1]:
945 v2_in_rel_v1 = True
947 if v1 in related_key_verts[v2]:
948 v1_in_rel_v2 = True
950 repeated_face = False
951 # If two verts have two related verts in common, they form a quad
952 if len(related_verts_in_common) == 2:
953 # Check if the face is already saved
954 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
956 for f_verts in all_faces_to_check_idx:
957 repeated_verts = 0
959 if len(f_verts) == 4:
960 if v1 in f_verts:
961 repeated_verts += 1
962 if v2 in f_verts:
963 repeated_verts += 1
964 if related_verts_in_common[0] in f_verts:
965 repeated_verts += 1
966 if related_verts_in_common[1] in f_verts:
967 repeated_verts += 1
969 if repeated_verts == len(f_verts):
970 repeated_face = True
971 break
973 if not repeated_face:
974 faces_verts_idx.append(
975 [v1, related_verts_in_common[0], v2, related_verts_in_common[1]]
978 # If Two verts have one related vert in common and
979 # they are related to each other, they form a triangle
980 elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
981 # Check if the face is already saved.
982 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
984 for f_verts in all_faces_to_check_idx:
985 repeated_verts = 0
987 if len(f_verts) == 3:
988 if v1 in f_verts:
989 repeated_verts += 1
990 if v2 in f_verts:
991 repeated_verts += 1
992 if related_verts_in_common[0] in f_verts:
993 repeated_verts += 1
995 if repeated_verts == len(f_verts):
996 repeated_face = True
997 break
999 if not repeated_face:
1000 faces_verts_idx.append([v1, related_verts_in_common[0], v2])
1002 # Keep only the faces that don't overlap by ignoring quads
1003 # that overlap with two adjacent triangles
1004 faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate
1005 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
1006 for i in range(len(faces_verts_idx)):
1007 for t in range(len(all_faces_to_check_idx)):
1008 if i != t:
1009 verts_in_common = 0
1011 if len(faces_verts_idx[i]) == 4 and len(all_faces_to_check_idx[t]) == 3:
1012 for v_idx in all_faces_to_check_idx[t]:
1013 if v_idx in faces_verts_idx[i]:
1014 verts_in_common += 1
1015 # If it doesn't have all it's vertices repeated in the other face
1016 if verts_in_common == 3:
1017 if i not in faces_to_not_include_idx:
1018 faces_to_not_include_idx.append(i)
1020 # Build faces discarding the ones in faces_to_not_include
1021 me = object.data
1022 bm = bmesh.new()
1023 bm.from_mesh(me)
1025 num_faces_created = 0
1026 for i in range(len(faces_verts_idx)):
1027 if i not in faces_to_not_include_idx:
1028 bm.faces.new([bm.verts[v] for v in faces_verts_idx[i]])
1030 num_faces_created += 1
1032 bm.to_mesh(me)
1033 bm.free()
1035 for v_idx in selected_verts_idx:
1036 self.main_object.data.vertices[v_idx].select = True
1038 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
1039 bpy.ops.mesh.normals_make_consistent(inside=False)
1040 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
1042 return num_faces_created
1044 # Crosshatch skinning
1045 def crosshatch_surface_invoke(self, ob_original_splines):
1046 self.is_crosshatch = False
1047 self.crosshatch_merge_distance = 0
1049 objects_to_delete = [] # duplicated strokes to be deleted.
1051 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1052 # (without this the surface verts merging with the main object doesn't work well)
1053 self.modifiers_prev_viewport_state = []
1054 if len(self.main_object.modifiers) > 0:
1055 for m_idx in range(len(self.main_object.modifiers)):
1056 self.modifiers_prev_viewport_state.append(
1057 self.main_object.modifiers[m_idx].show_viewport
1059 self.main_object.modifiers[m_idx].show_viewport = False
1061 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1062 ob_original_splines.select_set(True)
1063 bpy.context.view_layer.objects.active = ob_original_splines
1065 if len(ob_original_splines.data.splines) >= 2:
1066 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1067 ob_splines = bpy.context.object
1068 ob_splines.name = "SURFSKIO_NE_STR"
1070 # Get estimative merge distance (sum up the distances from the first point to
1071 # all other points, then average them and then divide them)
1072 first_point_dist_sum = 0
1073 first_dist = 0
1074 second_dist = 0
1075 coords_first_pt = ob_splines.data.splines[0].bezier_points[0].co
1076 for i in range(len(ob_splines.data.splines)):
1077 sp = ob_splines.data.splines[i]
1079 if coords_first_pt != sp.bezier_points[0].co:
1080 first_dist = (coords_first_pt - sp.bezier_points[0].co).length
1082 if coords_first_pt != sp.bezier_points[len(sp.bezier_points) - 1].co:
1083 second_dist = (coords_first_pt - sp.bezier_points[len(sp.bezier_points) - 1].co).length
1085 first_point_dist_sum += first_dist + second_dist
1087 if i == 0:
1088 if first_dist != 0:
1089 shortest_dist = first_dist
1090 elif second_dist != 0:
1091 shortest_dist = second_dist
1093 if shortest_dist > first_dist and first_dist != 0:
1094 shortest_dist = first_dist
1096 if shortest_dist > second_dist and second_dist != 0:
1097 shortest_dist = second_dist
1099 self.crosshatch_merge_distance = shortest_dist / 20
1101 # Recalculation of merge distance
1103 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1105 ob_calc_merge_dist = bpy.context.object
1106 ob_calc_merge_dist.name = "SURFSKIO_CALC_TMP"
1108 objects_to_delete.append(ob_calc_merge_dist)
1110 # Smooth out strokes a little to improve crosshatch detection
1111 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1112 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
1114 for i in range(4):
1115 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1117 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1118 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1120 # Convert curves into mesh
1121 ob_calc_merge_dist.data.resolution_u = 12
1122 bpy.ops.object.convert(target='MESH', keep_original=False)
1124 # Find "intersection-nodes"
1125 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1126 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1127 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
1128 threshold=self.crosshatch_merge_distance)
1129 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
1130 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1132 # Remove verts with less than three edges
1133 verts_edges_count = {}
1134 for ed in ob_calc_merge_dist.data.edges:
1135 v = ed.vertices
1137 if v[0] not in verts_edges_count:
1138 verts_edges_count[v[0]] = 0
1140 if v[1] not in verts_edges_count:
1141 verts_edges_count[v[1]] = 0
1143 verts_edges_count[v[0]] += 1
1144 verts_edges_count[v[1]] += 1
1146 nodes_verts_coords = []
1147 for v_idx in verts_edges_count:
1148 v = ob_calc_merge_dist.data.vertices[v_idx]
1150 if verts_edges_count[v_idx] < 3:
1151 v.select = True
1153 # Remove them
1154 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1155 bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
1156 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1158 # Remove doubles to discard very near verts from calculations of distance
1159 bpy.ops.mesh.remove_doubles(
1160 'INVOKE_REGION_WIN',
1161 threshold=self.crosshatch_merge_distance * 4.0
1163 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1165 # Get all coords of the resulting nodes
1166 nodes_verts_coords = [(v.co[0], v.co[1], v.co[2]) for
1167 v in ob_calc_merge_dist.data.vertices]
1169 # Check if the strokes are a crosshatch
1170 if len(nodes_verts_coords) >= 3:
1171 self.is_crosshatch = True
1173 shortest_dist = None
1174 for co_1 in nodes_verts_coords:
1175 for co_2 in nodes_verts_coords:
1176 if co_1 != co_2:
1177 dist = (Vector(co_1) - Vector(co_2)).length
1179 if shortest_dist is not None:
1180 if dist < shortest_dist:
1181 shortest_dist = dist
1182 else:
1183 shortest_dist = dist
1185 self.crosshatch_merge_distance = shortest_dist / 3
1187 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1188 ob_splines.select_set(True)
1189 bpy.context.view_layer.objects.active = ob_splines
1191 # Deselect all points
1192 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1193 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1194 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1196 # Smooth splines in a localized way, to eliminate "saw-teeth"
1197 # like shapes when there are many points
1198 for sp in ob_splines.data.splines:
1199 angle_sum = 0
1201 angle_limit = 2 # Degrees
1202 for t in range(len(sp.bezier_points)):
1203 # Because on each iteration it checks the "next two points"
1204 # of the actual. This way it doesn't go out of range
1205 if t <= len(sp.bezier_points) - 3:
1206 p1 = sp.bezier_points[t]
1207 p2 = sp.bezier_points[t + 1]
1208 p3 = sp.bezier_points[t + 2]
1210 vec_1 = p1.co - p2.co
1211 vec_2 = p2.co - p3.co
1213 if p2.co != p1.co and p2.co != p3.co:
1214 angle = vec_1.angle(vec_2)
1215 angle_sum += degrees(angle)
1217 if angle_sum >= angle_limit: # If sum of angles is grater than the limit
1218 if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
1219 p1.select_control_point = True
1220 p1.select_left_handle = True
1221 p1.select_right_handle = True
1223 p2.select_control_point = True
1224 p2.select_left_handle = True
1225 p2.select_right_handle = True
1227 if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
1228 p3.select_control_point = True
1229 p3.select_left_handle = True
1230 p3.select_right_handle = True
1232 angle_sum = 0
1234 sp.bezier_points[0].select_control_point = False
1235 sp.bezier_points[0].select_left_handle = False
1236 sp.bezier_points[0].select_right_handle = False
1238 sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = False
1239 sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = False
1240 sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = False
1242 # Smooth out strokes a little to improve crosshatch detection
1243 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1245 for i in range(15):
1246 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1248 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1249 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1251 # Simplify the splines
1252 for sp in ob_splines.data.splines:
1253 angle_sum = 0
1255 sp.bezier_points[0].select_control_point = True
1256 sp.bezier_points[0].select_left_handle = True
1257 sp.bezier_points[0].select_right_handle = True
1259 sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = True
1260 sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = True
1261 sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = True
1263 angle_limit = 15 # Degrees
1264 for t in range(len(sp.bezier_points)):
1265 # Because on each iteration it checks the "next two points"
1266 # of the actual. This way it doesn't go out of range
1267 if t <= len(sp.bezier_points) - 3:
1268 p1 = sp.bezier_points[t]
1269 p2 = sp.bezier_points[t + 1]
1270 p3 = sp.bezier_points[t + 2]
1272 vec_1 = p1.co - p2.co
1273 vec_2 = p2.co - p3.co
1275 if p2.co != p1.co and p2.co != p3.co:
1276 angle = vec_1.angle(vec_2)
1277 angle_sum += degrees(angle)
1278 # If sum of angles is grater than the limit
1279 if angle_sum >= angle_limit:
1280 p1.select_control_point = True
1281 p1.select_left_handle = True
1282 p1.select_right_handle = True
1284 p2.select_control_point = True
1285 p2.select_left_handle = True
1286 p2.select_right_handle = True
1288 p3.select_control_point = True
1289 p3.select_left_handle = True
1290 p3.select_right_handle = True
1292 angle_sum = 0
1294 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1295 bpy.ops.curve.select_all(action='INVERT')
1297 bpy.ops.curve.delete(type='VERT')
1298 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1300 objects_to_delete.append(ob_splines)
1302 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1303 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1304 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1306 # Check if the strokes are a crosshatch
1307 if self.is_crosshatch:
1308 all_points_coords = []
1309 for i in range(len(ob_splines.data.splines)):
1310 all_points_coords.append([])
1312 all_points_coords[i] = [Vector((x, y, z)) for
1313 x, y, z in [bp.co for
1314 bp in ob_splines.data.splines[i].bezier_points]]
1316 all_intersections = []
1317 checked_splines = []
1318 for i in range(len(all_points_coords)):
1320 for t in range(len(all_points_coords[i]) - 1):
1321 bp1_co = all_points_coords[i][t]
1322 bp2_co = all_points_coords[i][t + 1]
1324 for i2 in range(len(all_points_coords)):
1325 if i != i2 and i2 not in checked_splines:
1326 for t2 in range(len(all_points_coords[i2]) - 1):
1327 bp3_co = all_points_coords[i2][t2]
1328 bp4_co = all_points_coords[i2][t2 + 1]
1330 intersec_coords = intersect_line_line(
1331 bp1_co, bp2_co, bp3_co, bp4_co
1333 if intersec_coords is not None:
1334 dist = (intersec_coords[0] - intersec_coords[1]).length
1336 if dist <= self.crosshatch_merge_distance * 1.5:
1337 temp_co, percent1 = intersect_point_line(
1338 intersec_coords[0], bp1_co, bp2_co
1340 if (percent1 >= -0.02 and percent1 <= 1.02):
1341 temp_co, percent2 = intersect_point_line(
1342 intersec_coords[1], bp3_co, bp4_co
1344 if (percent2 >= -0.02 and percent2 <= 1.02):
1345 # Format: spline index, first point index from
1346 # corresponding segment, percentage from first point of
1347 # actual segment, coords of intersection point
1348 all_intersections.append(
1349 (i, t, percent1,
1350 ob_splines.matrix_world @ intersec_coords[0])
1352 all_intersections.append(
1353 (i2, t2, percent2,
1354 ob_splines.matrix_world @ intersec_coords[1])
1357 checked_splines.append(i)
1358 # Sort list by spline, then by corresponding first point index of segment,
1359 # and then by percentage from first point of segment: elements 0 and 1 respectively
1360 all_intersections.sort(key=operator.itemgetter(0, 1, 2))
1362 self.crosshatch_strokes_coords = {}
1363 for i in range(len(all_intersections)):
1364 if not all_intersections[i][0] in self.crosshatch_strokes_coords:
1365 self.crosshatch_strokes_coords[all_intersections[i][0]] = []
1367 self.crosshatch_strokes_coords[all_intersections[i][0]].append(
1368 all_intersections[i][3]
1369 ) # Save intersection coords
1370 else:
1371 self.is_crosshatch = False
1373 # Delete all duplicates
1374 bpy.ops.object.delete({"selected_objects": objects_to_delete})
1376 # If the main object has modifiers, turn their "viewport view status" to
1377 # what it was before the forced deactivation above
1378 if len(self.main_object.modifiers) > 0:
1379 for m_idx in range(len(self.main_object.modifiers)):
1380 self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
1382 return
1384 # Part of the Crosshatch process that is repeated when the operator is tweaked
1385 def crosshatch_surface_execute(self):
1386 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1387 # (without this the surface verts merging with the main object doesn't work well)
1388 self.modifiers_prev_viewport_state = []
1389 if len(self.main_object.modifiers) > 0:
1390 for m_idx in range(len(self.main_object.modifiers)):
1391 self.modifiers_prev_viewport_state.append(self.main_object.modifiers[m_idx].show_viewport)
1393 self.main_object.modifiers[m_idx].show_viewport = False
1395 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1397 me_name = "SURFSKIO_STK_TMP"
1398 me = bpy.data.meshes.new(me_name)
1400 all_verts_coords = []
1401 all_edges = []
1402 for st_idx in self.crosshatch_strokes_coords:
1403 for co_idx in range(len(self.crosshatch_strokes_coords[st_idx])):
1404 coords = self.crosshatch_strokes_coords[st_idx][co_idx]
1406 all_verts_coords.append(coords)
1408 if co_idx > 0:
1409 all_edges.append((len(all_verts_coords) - 2, len(all_verts_coords) - 1))
1411 me.from_pydata(all_verts_coords, all_edges, [])
1413 me.update()
1415 ob = bpy.data.objects.new(me_name, me)
1416 ob.data = me
1417 bpy.context.collection.objects.link(ob)
1419 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1420 ob.select_set(True)
1421 bpy.context.view_layer.objects.active = ob
1423 # Get together each vert and its nearest, to the middle position
1424 verts = ob.data.vertices
1425 checked_verts = []
1426 for i in range(len(verts)):
1427 shortest_dist = None
1429 if i not in checked_verts:
1430 for t in range(len(verts)):
1431 if i != t and t not in checked_verts:
1432 dist = (verts[i].co - verts[t].co).length
1434 if shortest_dist is not None:
1435 if dist < shortest_dist:
1436 shortest_dist = dist
1437 nearest_vert = t
1438 else:
1439 shortest_dist = dist
1440 nearest_vert = t
1442 middle_location = (verts[i].co + verts[nearest_vert].co) / 2
1444 verts[i].co = middle_location
1445 verts[nearest_vert].co = middle_location
1447 checked_verts.append(i)
1448 checked_verts.append(nearest_vert)
1450 # Calculate average length between all the generated edges
1451 ob = bpy.context.object
1452 lengths_sum = 0
1453 for ed in ob.data.edges:
1454 v1 = ob.data.vertices[ed.vertices[0]]
1455 v2 = ob.data.vertices[ed.vertices[1]]
1457 lengths_sum += (v1.co - v2.co).length
1459 edges_count = len(ob.data.edges)
1460 # possible division by zero here
1461 average_edge_length = lengths_sum / edges_count if edges_count != 0 else 0.0001
1463 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1464 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1465 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
1466 threshold=average_edge_length / 15.0)
1467 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1469 final_points_ob = bpy.context.view_layer.objects.active
1471 # Make a dictionary with the verts related to each vert
1472 related_key_verts = {}
1473 for ed in final_points_ob.data.edges:
1474 if not ed.vertices[0] in related_key_verts:
1475 related_key_verts[ed.vertices[0]] = []
1477 if not ed.vertices[1] in related_key_verts:
1478 related_key_verts[ed.vertices[1]] = []
1480 if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
1481 related_key_verts[ed.vertices[0]].append(ed.vertices[1])
1483 if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
1484 related_key_verts[ed.vertices[1]].append(ed.vertices[0])
1486 # Get groups of verts forming each face
1487 faces_verts_idx = []
1488 for v1 in related_key_verts: # verts-1 ....
1489 for v2 in related_key_verts: # verts-2
1490 if v1 != v2:
1491 related_verts_in_common = []
1492 v2_in_rel_v1 = False
1493 v1_in_rel_v2 = False
1494 for rel_v1 in related_key_verts[v1]:
1495 # Check if related verts of verts-1 are related verts of verts-2
1496 if rel_v1 in related_key_verts[v2]:
1497 related_verts_in_common.append(rel_v1)
1499 if v2 in related_key_verts[v1]:
1500 v2_in_rel_v1 = True
1502 if v1 in related_key_verts[v2]:
1503 v1_in_rel_v2 = True
1505 repeated_face = False
1506 # If two verts have two related verts in common, they form a quad
1507 if len(related_verts_in_common) == 2:
1508 # Check if the face is already saved
1509 for f_verts in faces_verts_idx:
1510 repeated_verts = 0
1512 if len(f_verts) == 4:
1513 if v1 in f_verts:
1514 repeated_verts += 1
1515 if v2 in f_verts:
1516 repeated_verts += 1
1517 if related_verts_in_common[0] in f_verts:
1518 repeated_verts += 1
1519 if related_verts_in_common[1] in f_verts:
1520 repeated_verts += 1
1522 if repeated_verts == len(f_verts):
1523 repeated_face = True
1524 break
1526 if not repeated_face:
1527 faces_verts_idx.append([v1, related_verts_in_common[0],
1528 v2, related_verts_in_common[1]])
1530 # If Two verts have one related vert in common and they are
1531 # related to each other, they form a triangle
1532 elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
1533 # Check if the face is already saved.
1534 for f_verts in faces_verts_idx:
1535 repeated_verts = 0
1537 if len(f_verts) == 3:
1538 if v1 in f_verts:
1539 repeated_verts += 1
1540 if v2 in f_verts:
1541 repeated_verts += 1
1542 if related_verts_in_common[0] in f_verts:
1543 repeated_verts += 1
1545 if repeated_verts == len(f_verts):
1546 repeated_face = True
1547 break
1549 if not repeated_face:
1550 faces_verts_idx.append([v1, related_verts_in_common[0], v2])
1552 # Keep only the faces that don't overlap by ignoring
1553 # quads that overlap with two adjacent triangles
1554 faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate
1555 for i in range(len(faces_verts_idx)):
1556 for t in range(len(faces_verts_idx)):
1557 if i != t:
1558 verts_in_common = 0
1560 if len(faces_verts_idx[i]) == 4 and len(faces_verts_idx[t]) == 3:
1561 for v_idx in faces_verts_idx[t]:
1562 if v_idx in faces_verts_idx[i]:
1563 verts_in_common += 1
1564 # If it doesn't have all it's vertices repeated in the other face
1565 if verts_in_common == 3:
1566 if i not in faces_to_not_include_idx:
1567 faces_to_not_include_idx.append(i)
1569 # Build surface
1570 all_surface_verts_co = []
1571 verts_idx_translation = {}
1572 for i in range(len(final_points_ob.data.vertices)):
1573 coords = final_points_ob.data.vertices[i].co
1574 all_surface_verts_co.append([coords[0], coords[1], coords[2]])
1576 # Verts of each face.
1577 all_surface_faces = []
1578 for i in range(len(faces_verts_idx)):
1579 if i not in faces_to_not_include_idx:
1580 face = []
1581 for v_idx in faces_verts_idx[i]:
1582 face.append(v_idx)
1584 all_surface_faces.append(face)
1586 # Build the mesh
1587 surf_me_name = "SURFSKIO_surface"
1588 me_surf = bpy.data.meshes.new(surf_me_name)
1590 me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
1592 me_surf.update()
1594 ob_surface = bpy.data.objects.new(surf_me_name, me_surf)
1595 bpy.context.collection.objects.link(ob_surface)
1597 # Delete final points temporal object
1598 bpy.ops.object.delete({"selected_objects": [final_points_ob]})
1600 # Delete isolated verts if there are any
1601 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1602 ob_surface.select_set(True)
1603 bpy.context.view_layer.objects.active = ob_surface
1605 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1606 bpy.ops.mesh.select_all(action='DESELECT')
1607 bpy.ops.mesh.select_face_by_sides(type='NOTEQUAL')
1608 bpy.ops.mesh.delete()
1609 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1611 # Join crosshatch results with original mesh
1613 # Calculate a distance to merge the verts of the crosshatch surface to the main object
1614 edges_length_sum = 0
1615 for ed in ob_surface.data.edges:
1616 edges_length_sum += (
1617 ob_surface.data.vertices[ed.vertices[0]].co -
1618 ob_surface.data.vertices[ed.vertices[1]].co
1619 ).length
1621 if len(ob_surface.data.edges) > 0:
1622 average_surface_edges_length = edges_length_sum / len(ob_surface.data.edges)
1623 else:
1624 average_surface_edges_length = 0.0001
1626 # Make dictionary with all the verts connected to each vert, on the new surface object.
1627 surface_connected_verts = {}
1628 for ed in ob_surface.data.edges:
1629 if not ed.vertices[0] in surface_connected_verts:
1630 surface_connected_verts[ed.vertices[0]] = []
1632 surface_connected_verts[ed.vertices[0]].append(ed.vertices[1])
1634 if ed.vertices[1] not in surface_connected_verts:
1635 surface_connected_verts[ed.vertices[1]] = []
1637 surface_connected_verts[ed.vertices[1]].append(ed.vertices[0])
1639 # Duplicate the new surface object, and use shrinkwrap to
1640 # calculate later the nearest verts to the main object
1641 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1642 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
1643 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1645 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1647 final_ob_duplicate = bpy.context.view_layer.objects.active
1649 bpy.ops.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
1650 shrinkwrap_modifier = final_ob_duplicate.modifiers[-1]
1651 shrinkwrap_modifier.wrap_method = "NEAREST_VERTEX"
1652 shrinkwrap_modifier.target = self.main_object
1654 bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', apply_as='DATA', modifier=shrinkwrap_modifier.name)
1656 # Make list with verts of original mesh as index and coords as value
1657 main_object_verts_coords = []
1658 for v in self.main_object.data.vertices:
1659 coords = self.main_object.matrix_world @ v.co
1661 # To avoid problems when taking "-0.00" as a different value as "0.00"
1662 for c in range(len(coords)):
1663 if "%.3f" % coords[c] == "-0.00":
1664 coords[c] = 0
1666 main_object_verts_coords.append(["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]])
1668 tuple(main_object_verts_coords)
1670 # Determine which verts will be merged, snap them to the nearest verts
1671 # on the original verts, and get them selected
1672 crosshatch_verts_to_merge = []
1673 if self.automatic_join:
1674 for i in range(len(ob_surface.data.vertices)):
1675 # Calculate the distance from each of the connected verts to the actual vert,
1676 # and compare it with the distance they would have if joined.
1677 # If they don't change much, that vert can be joined
1678 merge_actual_vert = True
1679 if len(surface_connected_verts[i]) < 4:
1680 for c_v_idx in surface_connected_verts[i]:
1681 points_original = []
1682 points_original.append(ob_surface.data.vertices[c_v_idx].co)
1683 points_original.append(ob_surface.data.vertices[i].co)
1685 points_target = []
1686 points_target.append(ob_surface.data.vertices[c_v_idx].co)
1687 points_target.append(final_ob_duplicate.data.vertices[i].co)
1689 vec_A = points_original[0] - points_original[1]
1690 vec_B = points_target[0] - points_target[1]
1692 dist_A = (points_original[0] - points_original[1]).length
1693 dist_B = (points_target[0] - points_target[1]).length
1695 if not (
1696 points_original[0] == points_original[1] or
1697 points_target[0] == points_target[1]
1698 ): # If any vector's length is zero
1700 angle = vec_A.angle(vec_B) / pi
1701 else:
1702 angle = 0
1704 # Set a range of acceptable variation in the connected edges
1705 if dist_B > dist_A * 1.7 * self.join_stretch_factor or \
1706 dist_B < dist_A / 2 / self.join_stretch_factor or \
1707 angle >= 0.15 * self.join_stretch_factor:
1709 merge_actual_vert = False
1710 break
1711 else:
1712 merge_actual_vert = False
1714 if merge_actual_vert:
1715 coords = final_ob_duplicate.data.vertices[i].co
1716 # To avoid problems when taking "-0.000" as a different value as "0.00"
1717 for c in range(len(coords)):
1718 if "%.3f" % coords[c] == "-0.00":
1719 coords[c] = 0
1721 comparison_coords = ["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]]
1723 if comparison_coords in main_object_verts_coords:
1724 # Get the index of the vert with those coords in the main object
1725 main_object_related_vert_idx = main_object_verts_coords.index(comparison_coords)
1727 if self.main_object.data.vertices[main_object_related_vert_idx].select is True or \
1728 self.main_object_selected_verts_count == 0:
1730 ob_surface.data.vertices[i].co = final_ob_duplicate.data.vertices[i].co
1731 ob_surface.data.vertices[i].select_set(True)
1732 crosshatch_verts_to_merge.append(i)
1734 # Make sure the vert in the main object is selected,
1735 # in case it wasn't selected and the "join crosshatch" option is active
1736 self.main_object.data.vertices[main_object_related_vert_idx].select_set(True)
1738 # Delete duplicated object
1739 bpy.ops.object.delete({"selected_objects": [final_ob_duplicate]})
1741 # Join crosshatched surface and main object
1742 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1743 ob_surface.select_set(True)
1744 self.main_object.select_set(True)
1745 bpy.context.view_layer.objects.active = self.main_object
1747 bpy.ops.object.join('INVOKE_REGION_WIN')
1749 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1750 # Perform Remove doubles to merge verts
1751 if not (self.automatic_join is False and self.main_object_selected_verts_count == 0):
1752 bpy.ops.mesh.remove_doubles(threshold=0.0001)
1754 bpy.ops.mesh.select_all(action='DESELECT')
1756 # If the main object has modifiers, turn their "viewport view status"
1757 # to what it was before the forced deactivation above
1758 if len(self.main_object.modifiers) > 0:
1759 for m_idx in range(len(self.main_object.modifiers)):
1760 self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
1762 return {'FINISHED'}
1764 def rectangular_surface(self):
1765 # Selected edges
1766 all_selected_edges_idx = []
1767 all_selected_verts = []
1768 all_verts_idx = []
1769 for ed in self.main_object.data.edges:
1770 if ed.select:
1771 all_selected_edges_idx.append(ed.index)
1773 # Selected vertices
1774 if not ed.vertices[0] in all_selected_verts:
1775 all_selected_verts.append(self.main_object.data.vertices[ed.vertices[0]])
1776 if not ed.vertices[1] in all_selected_verts:
1777 all_selected_verts.append(self.main_object.data.vertices[ed.vertices[1]])
1779 # All verts (both from each edge) to determine later
1780 # which are at the tips (those not repeated twice)
1781 all_verts_idx.append(ed.vertices[0])
1782 all_verts_idx.append(ed.vertices[1])
1784 # Identify the tips and "middle-vertex" that separates U from V, if there is one
1785 all_chains_tips_idx = []
1786 for v_idx in all_verts_idx:
1787 if all_verts_idx.count(v_idx) < 2:
1788 all_chains_tips_idx.append(v_idx)
1790 edges_connected_to_tips = []
1791 for ed in self.main_object.data.edges:
1792 if (ed.vertices[0] in all_chains_tips_idx or ed.vertices[1] in all_chains_tips_idx) and \
1793 not (ed.vertices[0] in all_verts_idx and ed.vertices[1] in all_verts_idx):
1795 edges_connected_to_tips.append(ed)
1797 # Check closed selections
1798 # List with groups of three verts, where the first element of the pair is
1799 # the unselected vert of a closed selection and the other two elements are the
1800 # selected neighbor verts (it will be useful to determine which selection chain
1801 # the unselected vert belongs to, and determine the "middle-vertex")
1802 single_unselected_verts_and_neighbors = []
1804 # To identify a "closed" selection (a selection that is a closed chain except
1805 # for one vertex) find the vertex in common that have the edges connected to tips.
1806 # If there is a vertex in common, that one is the unselected vert that closes
1807 # the selection or is a "middle-vertex"
1808 single_unselected_verts = []
1809 for ed in edges_connected_to_tips:
1810 for ed_b in edges_connected_to_tips:
1811 if ed != ed_b:
1812 if ed.vertices[0] == ed_b.vertices[0] and \
1813 not self.main_object.data.vertices[ed.vertices[0]].select and \
1814 ed.vertices[0] not in single_unselected_verts:
1816 # The second element is one of the tips of the selected
1817 # vertices of the closed selection
1818 single_unselected_verts_and_neighbors.append(
1819 [ed.vertices[0], ed.vertices[1], ed_b.vertices[1]]
1821 single_unselected_verts.append(ed.vertices[0])
1822 break
1823 elif ed.vertices[0] == ed_b.vertices[1] and \
1824 not self.main_object.data.vertices[ed.vertices[0]].select and \
1825 ed.vertices[0] not in single_unselected_verts:
1827 single_unselected_verts_and_neighbors.append(
1828 [ed.vertices[0], ed.vertices[1], ed_b.vertices[0]]
1830 single_unselected_verts.append(ed.vertices[0])
1831 break
1832 elif ed.vertices[1] == ed_b.vertices[0] and \
1833 not self.main_object.data.vertices[ed.vertices[1]].select and \
1834 ed.vertices[1] not in single_unselected_verts:
1836 single_unselected_verts_and_neighbors.append(
1837 [ed.vertices[1], ed.vertices[0], ed_b.vertices[1]]
1839 single_unselected_verts.append(ed.vertices[1])
1840 break
1841 elif ed.vertices[1] == ed_b.vertices[1] and \
1842 not self.main_object.data.vertices[ed.vertices[1]].select and \
1843 ed.vertices[1] not in single_unselected_verts:
1845 single_unselected_verts_and_neighbors.append(
1846 [ed.vertices[1], ed.vertices[0], ed_b.vertices[0]]
1848 single_unselected_verts.append(ed.vertices[1])
1849 break
1851 middle_vertex_idx = None
1852 tips_to_discard_idx = []
1854 # Check if there is a "middle-vertex", and get its index
1855 for i in range(0, len(single_unselected_verts_and_neighbors)):
1856 actual_chain_verts = self.get_ordered_verts(
1857 self.main_object, all_selected_edges_idx,
1858 all_verts_idx, single_unselected_verts_and_neighbors[i][1],
1859 None, None
1862 if single_unselected_verts_and_neighbors[i][2] != \
1863 actual_chain_verts[len(actual_chain_verts) - 1].index:
1865 middle_vertex_idx = single_unselected_verts_and_neighbors[i][0]
1866 tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][1])
1867 tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][2])
1869 # List with pairs of verts that belong to the tips of each selection chain (row)
1870 verts_tips_same_chain_idx = []
1871 if len(all_chains_tips_idx) >= 2:
1872 checked_v = []
1873 for i in range(0, len(all_chains_tips_idx)):
1874 if all_chains_tips_idx[i] not in checked_v:
1875 v_chain = self.get_ordered_verts(
1876 self.main_object, all_selected_edges_idx,
1877 all_verts_idx, all_chains_tips_idx[i],
1878 middle_vertex_idx, None
1881 verts_tips_same_chain_idx.append([v_chain[0].index, v_chain[len(v_chain) - 1].index])
1883 checked_v.append(v_chain[0].index)
1884 checked_v.append(v_chain[len(v_chain) - 1].index)
1886 # Selection tips (vertices).
1887 verts_tips_parsed_idx = []
1888 if len(all_chains_tips_idx) >= 2:
1889 for spec_v_idx in all_chains_tips_idx:
1890 if (spec_v_idx not in tips_to_discard_idx):
1891 verts_tips_parsed_idx.append(spec_v_idx)
1893 # Identify the type of selection made by the user
1894 if middle_vertex_idx is not None:
1895 # If there are 4 tips (two selection chains), and
1896 # there is only one single unselected vert (the middle vert)
1897 if len(all_chains_tips_idx) == 4 and len(single_unselected_verts_and_neighbors) == 1:
1898 selection_type = "TWO_CONNECTED"
1899 else:
1900 # The type of the selection was not identified, the script stops.
1901 self.report({'WARNING'}, "The selection isn't valid.")
1902 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1903 self.cleanup_on_interruption()
1904 self.stopping_errors = True
1906 return{'CANCELLED'}
1907 else:
1908 if len(all_chains_tips_idx) == 2: # If there are 2 tips
1909 selection_type = "SINGLE"
1910 elif len(all_chains_tips_idx) == 4: # If there are 4 tips
1911 selection_type = "TWO_NOT_CONNECTED"
1912 elif len(all_chains_tips_idx) == 0:
1913 if len(self.main_splines.data.splines) > 1:
1914 selection_type = "NO_SELECTION"
1915 else:
1916 # If the selection was not identified and there is only one stroke,
1917 # there's no possibility to build a surface, so the script is interrupted
1918 self.report({'WARNING'}, "The selection isn't valid.")
1919 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1920 self.cleanup_on_interruption()
1921 self.stopping_errors = True
1923 return{'CANCELLED'}
1924 else:
1925 # The type of the selection was not identified, the script stops
1926 self.report({'WARNING'}, "The selection isn't valid.")
1928 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1929 self.cleanup_on_interruption()
1931 self.stopping_errors = True
1933 return{'CANCELLED'}
1935 # If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script
1936 if selection_type == "TWO_NOT_CONNECTED" and len(self.main_splines.data.splines) == 1:
1937 self.report({'WARNING'},
1938 "At least two strokes are needed when there are two not connected selections")
1939 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1940 self.cleanup_on_interruption()
1941 self.stopping_errors = True
1943 return{'CANCELLED'}
1945 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1947 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1948 self.main_splines.select_set(True)
1949 bpy.context.view_layer.objects.active = self.main_splines
1951 # Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out
1952 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1953 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1954 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1955 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1956 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1957 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1958 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1959 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1961 self.selection_U_exists = False
1962 self.selection_U2_exists = False
1963 self.selection_V_exists = False
1964 self.selection_V2_exists = False
1966 self.selection_U_is_closed = False
1967 self.selection_U2_is_closed = False
1968 self.selection_V_is_closed = False
1969 self.selection_V2_is_closed = False
1971 # Define what vertices are at the tips of each selection and are not the middle-vertex
1972 if selection_type == "TWO_CONNECTED":
1973 self.selection_U_exists = True
1974 self.selection_V_exists = True
1976 closing_vert_U_idx = None
1977 closing_vert_V_idx = None
1978 closing_vert_U2_idx = None
1979 closing_vert_V2_idx = None
1981 # Determine which selection is Selection-U and which is Selection-V
1982 points_A = []
1983 points_B = []
1984 points_first_stroke_tips = []
1986 points_A.append(
1987 self.main_object.matrix_world @ self.main_object.data.vertices[verts_tips_parsed_idx[0]].co
1989 points_A.append(
1990 self.main_object.matrix_world @ self.main_object.data.vertices[middle_vertex_idx].co
1992 points_B.append(
1993 self.main_object.matrix_world @ self.main_object.data.vertices[verts_tips_parsed_idx[1]].co
1995 points_B.append(
1996 self.main_object.matrix_world @ self.main_object.data.vertices[middle_vertex_idx].co
1998 points_first_stroke_tips.append(
1999 self.main_splines.data.splines[0].bezier_points[0].co
2001 points_first_stroke_tips.append(
2002 self.main_splines.data.splines[0].bezier_points[
2003 len(self.main_splines.data.splines[0].bezier_points) - 1
2004 ].co
2007 angle_A = self.orientation_difference(points_A, points_first_stroke_tips)
2008 angle_B = self.orientation_difference(points_B, points_first_stroke_tips)
2010 if angle_A < angle_B:
2011 first_vert_U_idx = verts_tips_parsed_idx[0]
2012 first_vert_V_idx = verts_tips_parsed_idx[1]
2013 else:
2014 first_vert_U_idx = verts_tips_parsed_idx[1]
2015 first_vert_V_idx = verts_tips_parsed_idx[0]
2017 elif selection_type == "SINGLE" or selection_type == "TWO_NOT_CONNECTED":
2018 first_sketched_point_first_stroke_co = self.main_splines.data.splines[0].bezier_points[0].co
2019 last_sketched_point_first_stroke_co = \
2020 self.main_splines.data.splines[0].bezier_points[
2021 len(self.main_splines.data.splines[0].bezier_points) - 1
2022 ].co
2023 first_sketched_point_last_stroke_co = \
2024 self.main_splines.data.splines[
2025 len(self.main_splines.data.splines) - 1
2026 ].bezier_points[0].co
2027 if len(self.main_splines.data.splines) > 1:
2028 first_sketched_point_second_stroke_co = self.main_splines.data.splines[1].bezier_points[0].co
2029 last_sketched_point_second_stroke_co = \
2030 self.main_splines.data.splines[1].bezier_points[
2031 len(self.main_splines.data.splines[1].bezier_points) - 1
2032 ].co
2034 single_unselected_neighbors = [] # Only the neighbors of the single unselected verts
2035 for verts_neig_idx in single_unselected_verts_and_neighbors:
2036 single_unselected_neighbors.append(verts_neig_idx[1])
2037 single_unselected_neighbors.append(verts_neig_idx[2])
2039 all_chains_tips_and_middle_vert = []
2040 for v_idx in all_chains_tips_idx:
2041 if v_idx not in single_unselected_neighbors:
2042 all_chains_tips_and_middle_vert.append(v_idx)
2044 all_chains_tips_and_middle_vert += single_unselected_verts
2046 all_participating_verts = all_chains_tips_and_middle_vert + all_verts_idx
2048 # The tip of the selected vertices nearest to the first point of the first sketched stroke
2049 nearest_tip_to_first_st_first_pt_idx, shortest_distance_to_first_stroke = \
2050 self.shortest_distance(
2051 self.main_object,
2052 first_sketched_point_first_stroke_co,
2053 all_chains_tips_and_middle_vert
2055 # If the nearest tip is not from a closed selection, get the opposite tip vertex index
2056 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2057 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2059 nearest_tip_to_first_st_first_pt_opposite_idx = \
2060 self.opposite_tip(
2061 nearest_tip_to_first_st_first_pt_idx,
2062 verts_tips_same_chain_idx
2064 # The tip of the selected vertices nearest to the last point of the first sketched stroke
2065 nearest_tip_to_first_st_last_pt_idx, temp_dist = \
2066 self.shortest_distance(
2067 self.main_object,
2068 last_sketched_point_first_stroke_co,
2069 all_chains_tips_and_middle_vert
2071 # The tip of the selected vertices nearest to the first point of the last sketched stroke
2072 nearest_tip_to_last_st_first_pt_idx, shortest_distance_to_last_stroke = \
2073 self.shortest_distance(
2074 self.main_object,
2075 first_sketched_point_last_stroke_co,
2076 all_chains_tips_and_middle_vert
2078 if len(self.main_splines.data.splines) > 1:
2079 # The selected vertex nearest to the first point of the second sketched stroke
2080 # (This will be useful to determine the direction of the closed
2081 # selection V when extruding along strokes)
2082 nearest_vert_to_second_st_first_pt_idx, temp_dist = \
2083 self.shortest_distance(
2084 self.main_object,
2085 first_sketched_point_second_stroke_co,
2086 all_verts_idx
2088 # The selected vertex nearest to the first point of the second sketched stroke
2089 # (This will be useful to determine the direction of the closed
2090 # selection V2 when extruding along strokes)
2091 nearest_vert_to_second_st_last_pt_idx, temp_dist = \
2092 self.shortest_distance(
2093 self.main_object,
2094 last_sketched_point_second_stroke_co,
2095 all_verts_idx
2097 # Determine if the single selection will be treated as U or as V
2098 edges_sum = 0
2099 for i in all_selected_edges_idx:
2100 edges_sum += (
2101 (self.main_object.matrix_world @
2102 self.main_object.data.vertices[self.main_object.data.edges[i].vertices[0]].co) -
2103 (self.main_object.matrix_world @
2104 self.main_object.data.vertices[self.main_object.data.edges[i].vertices[1]].co)
2105 ).length
2107 average_edge_length = edges_sum / len(all_selected_edges_idx)
2109 # Get shortest distance from the first point of the last stroke to any participating vertex
2110 temp_idx, shortest_distance_to_last_stroke = \
2111 self.shortest_distance(
2112 self.main_object,
2113 first_sketched_point_last_stroke_co,
2114 all_participating_verts
2116 # If the beginning of the first stroke is near enough, and its orientation
2117 # difference with the first edge of the nearest selection chain is not too high,
2118 # interpret things as an "extrude along strokes" instead of "extrude through strokes"
2119 if shortest_distance_to_first_stroke < average_edge_length / 4 and \
2120 shortest_distance_to_last_stroke < average_edge_length and \
2121 len(self.main_splines.data.splines) > 1:
2123 self.selection_U_exists = False
2124 self.selection_V_exists = True
2125 # If the first selection is not closed
2126 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2127 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2128 self.selection_V_is_closed = False
2129 first_neighbor_V_idx = None
2130 closing_vert_U_idx = None
2131 closing_vert_U2_idx = None
2132 closing_vert_V_idx = None
2133 closing_vert_V2_idx = None
2135 first_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
2137 if selection_type == "TWO_NOT_CONNECTED":
2138 self.selection_V2_exists = True
2140 first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2141 else:
2142 self.selection_V_is_closed = True
2143 closing_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
2145 # Get the neighbors of the first (unselected) vert of the closed selection U.
2146 vert_neighbors = []
2147 for verts in single_unselected_verts_and_neighbors:
2148 if verts[0] == nearest_tip_to_first_st_first_pt_idx:
2149 vert_neighbors.append(verts[1])
2150 vert_neighbors.append(verts[2])
2151 break
2153 verts_V = self.get_ordered_verts(
2154 self.main_object, all_selected_edges_idx,
2155 all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
2158 for i in range(0, len(verts_V)):
2159 if verts_V[i].index == nearest_vert_to_second_st_first_pt_idx:
2160 # If the vertex nearest to the first point of the second stroke
2161 # is in the first half of the selected verts
2162 if i >= len(verts_V) / 2:
2163 first_vert_V_idx = vert_neighbors[1]
2164 break
2165 else:
2166 first_vert_V_idx = vert_neighbors[0]
2167 break
2169 if selection_type == "TWO_NOT_CONNECTED":
2170 self.selection_V2_exists = True
2171 # If the second selection is not closed
2172 if nearest_tip_to_first_st_last_pt_idx not in single_unselected_verts or \
2173 nearest_tip_to_first_st_last_pt_idx == middle_vertex_idx:
2175 self.selection_V2_is_closed = False
2176 first_neighbor_V2_idx = None
2177 closing_vert_V2_idx = None
2178 first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2180 else:
2181 self.selection_V2_is_closed = True
2182 closing_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2184 # Get the neighbors of the first (unselected) vert of the closed selection U
2185 vert_neighbors = []
2186 for verts in single_unselected_verts_and_neighbors:
2187 if verts[0] == nearest_tip_to_first_st_last_pt_idx:
2188 vert_neighbors.append(verts[1])
2189 vert_neighbors.append(verts[2])
2190 break
2192 verts_V2 = self.get_ordered_verts(
2193 self.main_object, all_selected_edges_idx,
2194 all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
2197 for i in range(0, len(verts_V2)):
2198 if verts_V2[i].index == nearest_vert_to_second_st_last_pt_idx:
2199 # If the vertex nearest to the first point of the second stroke
2200 # is in the first half of the selected verts
2201 if i >= len(verts_V2) / 2:
2202 first_vert_V2_idx = vert_neighbors[1]
2203 break
2204 else:
2205 first_vert_V2_idx = vert_neighbors[0]
2206 break
2207 else:
2208 self.selection_V2_exists = False
2210 else:
2211 self.selection_U_exists = True
2212 self.selection_V_exists = False
2213 # If the first selection is not closed
2214 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2215 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2216 self.selection_U_is_closed = False
2217 first_neighbor_U_idx = None
2218 closing_vert_U_idx = None
2220 points_tips = []
2221 points_tips.append(
2222 self.main_object.matrix_world @
2223 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
2225 points_tips.append(
2226 self.main_object.matrix_world @
2227 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_opposite_idx].co
2229 points_first_stroke_tips = []
2230 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
2231 points_first_stroke_tips.append(
2232 self.main_splines.data.splines[0].bezier_points[
2233 len(self.main_splines.data.splines[0].bezier_points) - 1
2234 ].co
2236 vec_A = points_tips[0] - points_tips[1]
2237 vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
2239 # Compare the direction of the selection and the first
2240 # grease pencil stroke to determine which is the "first" vertex of the selection
2241 if vec_A.dot(vec_B) < 0:
2242 first_vert_U_idx = nearest_tip_to_first_st_first_pt_opposite_idx
2243 else:
2244 first_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
2246 else:
2247 self.selection_U_is_closed = True
2248 closing_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
2250 # Get the neighbors of the first (unselected) vert of the closed selection U
2251 vert_neighbors = []
2252 for verts in single_unselected_verts_and_neighbors:
2253 if verts[0] == nearest_tip_to_first_st_first_pt_idx:
2254 vert_neighbors.append(verts[1])
2255 vert_neighbors.append(verts[2])
2256 break
2258 points_first_and_neighbor = []
2259 points_first_and_neighbor.append(
2260 self.main_object.matrix_world @
2261 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
2263 points_first_and_neighbor.append(
2264 self.main_object.matrix_world @
2265 self.main_object.data.vertices[vert_neighbors[0]].co
2267 points_first_stroke_tips = []
2268 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
2269 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[1].co)
2271 vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
2272 vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
2274 # Compare the direction of the selection and the first grease pencil stroke to
2275 # determine which is the vertex neighbor to the first vertex (unselected) of
2276 # the closed selection. This will determine the direction of the closed selection
2277 if vec_A.dot(vec_B) < 0:
2278 first_vert_U_idx = vert_neighbors[1]
2279 else:
2280 first_vert_U_idx = vert_neighbors[0]
2282 if selection_type == "TWO_NOT_CONNECTED":
2283 self.selection_U2_exists = True
2284 # If the second selection is not closed
2285 if nearest_tip_to_last_st_first_pt_idx not in single_unselected_verts or \
2286 nearest_tip_to_last_st_first_pt_idx == middle_vertex_idx:
2288 self.selection_U2_is_closed = False
2289 first_neighbor_U2_idx = None
2290 closing_vert_U2_idx = None
2291 first_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
2292 else:
2293 self.selection_U2_is_closed = True
2294 closing_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
2296 # Get the neighbors of the first (unselected) vert of the closed selection U
2297 vert_neighbors = []
2298 for verts in single_unselected_verts_and_neighbors:
2299 if verts[0] == nearest_tip_to_last_st_first_pt_idx:
2300 vert_neighbors.append(verts[1])
2301 vert_neighbors.append(verts[2])
2302 break
2304 points_first_and_neighbor = []
2305 points_first_and_neighbor.append(
2306 self.main_object.matrix_world @
2307 self.main_object.data.vertices[nearest_tip_to_last_st_first_pt_idx].co
2309 points_first_and_neighbor.append(
2310 self.main_object.matrix_world @
2311 self.main_object.data.vertices[vert_neighbors[0]].co
2313 points_last_stroke_tips = []
2314 points_last_stroke_tips.append(
2315 self.main_splines.data.splines[
2316 len(self.main_splines.data.splines) - 1
2317 ].bezier_points[0].co
2319 points_last_stroke_tips.append(
2320 self.main_splines.data.splines[
2321 len(self.main_splines.data.splines) - 1
2322 ].bezier_points[1].co
2324 vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
2325 vec_B = points_last_stroke_tips[0] - points_last_stroke_tips[1]
2327 # Compare the direction of the selection and the last grease pencil stroke to
2328 # determine which is the vertex neighbor to the first vertex (unselected) of
2329 # the closed selection. This will determine the direction of the closed selection
2330 if vec_A.dot(vec_B) < 0:
2331 first_vert_U2_idx = vert_neighbors[1]
2332 else:
2333 first_vert_U2_idx = vert_neighbors[0]
2334 else:
2335 self.selection_U2_exists = False
2337 elif selection_type == "NO_SELECTION":
2338 self.selection_U_exists = False
2339 self.selection_V_exists = False
2341 # Get an ordered list of the vertices of Selection-U
2342 verts_ordered_U = []
2343 if self.selection_U_exists:
2344 verts_ordered_U = self.get_ordered_verts(
2345 self.main_object, all_selected_edges_idx,
2346 all_verts_idx, first_vert_U_idx,
2347 middle_vertex_idx, closing_vert_U_idx
2349 verts_ordered_U_indices = [x.index for x in verts_ordered_U]
2351 # Get an ordered list of the vertices of Selection-U2
2352 verts_ordered_U2 = []
2353 if self.selection_U2_exists:
2354 verts_ordered_U2 = self.get_ordered_verts(
2355 self.main_object, all_selected_edges_idx,
2356 all_verts_idx, first_vert_U2_idx,
2357 middle_vertex_idx, closing_vert_U2_idx
2359 verts_ordered_U2_indices = [x.index for x in verts_ordered_U2]
2361 # Get an ordered list of the vertices of Selection-V
2362 verts_ordered_V = []
2363 if self.selection_V_exists:
2364 verts_ordered_V = self.get_ordered_verts(
2365 self.main_object, all_selected_edges_idx,
2366 all_verts_idx, first_vert_V_idx,
2367 middle_vertex_idx, closing_vert_V_idx
2369 verts_ordered_V_indices = [x.index for x in verts_ordered_V]
2371 # Get an ordered list of the vertices of Selection-V2
2372 verts_ordered_V2 = []
2373 if self.selection_V2_exists:
2374 verts_ordered_V2 = self.get_ordered_verts(
2375 self.main_object, all_selected_edges_idx,
2376 all_verts_idx, first_vert_V2_idx,
2377 middle_vertex_idx, closing_vert_V2_idx
2379 verts_ordered_V2_indices = [x.index for x in verts_ordered_V2]
2381 # Check if when there are two-not-connected selections both have the same
2382 # number of verts. If not terminate the script
2383 if ((self.selection_U2_exists and len(verts_ordered_U) != len(verts_ordered_U2)) or
2384 (self.selection_V2_exists and len(verts_ordered_V) != len(verts_ordered_V2))):
2385 # Display a warning
2386 self.report({'WARNING'}, "Both selections must have the same number of edges")
2388 self.cleanup_on_interruption()
2389 self.stopping_errors = True
2391 return{'CANCELLED'}
2393 # Calculate edges U proportions
2394 # Sum selected edges U lengths
2395 edges_lengths_U = []
2396 edges_lengths_sum_U = 0
2398 if self.selection_U_exists:
2399 edges_lengths_U, edges_lengths_sum_U = self.get_chain_length(
2400 self.main_object,
2401 verts_ordered_U
2403 if self.selection_U2_exists:
2404 edges_lengths_U2, edges_lengths_sum_U2 = self.get_chain_length(
2405 self.main_object,
2406 verts_ordered_U2
2408 # Sum selected edges V lengths
2409 edges_lengths_V = []
2410 edges_lengths_sum_V = 0
2412 if self.selection_V_exists:
2413 edges_lengths_V, edges_lengths_sum_V = self.get_chain_length(
2414 self.main_object,
2415 verts_ordered_V
2417 if self.selection_V2_exists:
2418 edges_lengths_V2, edges_lengths_sum_V2 = self.get_chain_length(
2419 self.main_object,
2420 verts_ordered_V2
2423 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2424 bpy.ops.curve.subdivide('INVOKE_REGION_WIN',
2425 number_cuts=bpy.context.scene.bsurfaces.SURFSK_precision)
2426 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2428 # Proportions U
2429 edges_proportions_U = []
2430 edges_proportions_U = self.get_edges_proportions(
2431 edges_lengths_U, edges_lengths_sum_U,
2432 self.selection_U_exists, self.edges_U
2434 verts_count_U = len(edges_proportions_U) + 1
2436 if self.selection_U2_exists:
2437 edges_proportions_U2 = []
2438 edges_proportions_U2 = self.get_edges_proportions(
2439 edges_lengths_U2, edges_lengths_sum_U2,
2440 self.selection_U2_exists, self.edges_V
2442 verts_count_U2 = len(edges_proportions_U2) + 1
2444 # Proportions V
2445 edges_proportions_V = []
2446 edges_proportions_V = self.get_edges_proportions(
2447 edges_lengths_V, edges_lengths_sum_V,
2448 self.selection_V_exists, self.edges_V
2450 verts_count_V = len(edges_proportions_V) + 1
2452 if self.selection_V2_exists:
2453 edges_proportions_V2 = []
2454 edges_proportions_V2 = self.get_edges_proportions(
2455 edges_lengths_V2, edges_lengths_sum_V2,
2456 self.selection_V2_exists, self.edges_V
2458 verts_count_V2 = len(edges_proportions_V2) + 1
2460 # Cyclic Follow: simplify sketched curves, make them Cyclic, and complete
2461 # the actual sketched curves with a "closing segment"
2462 if self.cyclic_follow and not self.selection_V_exists and not \
2463 ((self.selection_U_exists and not self.selection_U_is_closed) or
2464 (self.selection_U2_exists and not self.selection_U2_is_closed)):
2466 simplified_spline_coords = []
2467 simplified_curve = []
2468 ob_simplified_curve = []
2469 splines_first_v_co = []
2470 for i in range(len(self.main_splines.data.splines)):
2471 # Create a curve object for the actual spline "cyclic extension"
2472 simplified_curve.append(bpy.data.curves.new('SURFSKIO_simpl_crv', 'CURVE'))
2473 ob_simplified_curve.append(bpy.data.objects.new('SURFSKIO_simpl_crv', simplified_curve[i]))
2474 bpy.context.collection.objects.link(ob_simplified_curve[i])
2476 simplified_curve[i].dimensions = "3D"
2478 spline_coords = []
2479 for bp in self.main_splines.data.splines[i].bezier_points:
2480 spline_coords.append(bp.co)
2482 # Simplification
2483 simplified_spline_coords.append(self.simplify_spline(spline_coords, 5))
2485 # Get the coordinates of the first vert of the actual spline
2486 splines_first_v_co.append(simplified_spline_coords[i][0])
2488 # Generate the spline
2489 spline = simplified_curve[i].splines.new('BEZIER')
2490 # less one because one point is added when the spline is created
2491 spline.bezier_points.add(len(simplified_spline_coords[i]) - 1)
2492 for p in range(0, len(simplified_spline_coords[i])):
2493 spline.bezier_points[p].co = simplified_spline_coords[i][p]
2495 spline.use_cyclic_u = True
2497 spline_bp_count = len(spline.bezier_points)
2499 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2500 ob_simplified_curve[i].select_set(True)
2501 bpy.context.view_layer.objects.active = ob_simplified_curve[i]
2503 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2504 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
2505 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2506 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
2507 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2509 # Select the "closing segment", and subdivide it
2510 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_control_point = True
2511 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_left_handle = True
2512 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_right_handle = True
2514 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_control_point = True
2515 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_left_handle = True
2516 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_right_handle = True
2518 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2519 segments = sqrt(
2520 (ob_simplified_curve[i].data.splines[0].bezier_points[0].co -
2521 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].co).length /
2522 self.average_gp_segment_length
2524 for t in range(2):
2525 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=segments)
2527 # Delete the other vertices and make it non-cyclic to
2528 # keep only the needed verts of the "closing segment"
2529 bpy.ops.curve.select_all(action='INVERT')
2530 bpy.ops.curve.delete(type='VERT')
2531 ob_simplified_curve[i].data.splines[0].use_cyclic_u = False
2532 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2534 # Add the points of the "closing segment" to the original curve from grease pencil stroke
2535 first_new_index = len(self.main_splines.data.splines[i].bezier_points)
2536 self.main_splines.data.splines[i].bezier_points.add(
2537 len(ob_simplified_curve[i].data.splines[0].bezier_points) - 1
2539 for t in range(1, len(ob_simplified_curve[i].data.splines[0].bezier_points)):
2540 self.main_splines.data.splines[i].bezier_points[t - 1 + first_new_index].co = \
2541 ob_simplified_curve[i].data.splines[0].bezier_points[t].co
2543 # Delete the temporal curve
2544 bpy.ops.object.delete({"selected_objects": [ob_simplified_curve[i]]})
2546 # Get the coords of the points distributed along the sketched strokes,
2547 # with proportions-U of the first selection
2548 pts_on_strokes_with_proportions_U = self.distribute_pts(
2549 self.main_splines.data.splines,
2550 edges_proportions_U
2552 sketched_splines_parsed = []
2554 if self.selection_U2_exists:
2555 # Initialize the multidimensional list with the proportions of all the segments
2556 proportions_loops_crossing_strokes = []
2557 for i in range(len(pts_on_strokes_with_proportions_U)):
2558 proportions_loops_crossing_strokes.append([])
2560 for t in range(len(pts_on_strokes_with_proportions_U[0])):
2561 proportions_loops_crossing_strokes[i].append(None)
2563 # Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U
2564 for lp in range(len(pts_on_strokes_with_proportions_U[0])):
2565 loop_segments_lengths = []
2567 for st in range(len(pts_on_strokes_with_proportions_U)):
2568 # When on the first stroke, add the segment from the selection to the dirst stroke
2569 if st == 0:
2570 loop_segments_lengths.append(
2571 ((self.main_object.matrix_world @ verts_ordered_U[lp].co) -
2572 pts_on_strokes_with_proportions_U[0][lp]).length
2574 # For all strokes except for the last, calculate the distance
2575 # from the actual stroke to the next
2576 if st != len(pts_on_strokes_with_proportions_U) - 1:
2577 loop_segments_lengths.append(
2578 (pts_on_strokes_with_proportions_U[st][lp] -
2579 pts_on_strokes_with_proportions_U[st + 1][lp]).length
2581 # When on the last stroke, add the segments
2582 # from the last stroke to the second selection
2583 if st == len(pts_on_strokes_with_proportions_U) - 1:
2584 loop_segments_lengths.append(
2585 (pts_on_strokes_with_proportions_U[st][lp] -
2586 (self.main_object.matrix_world @ verts_ordered_U2[lp].co)).length
2588 # Calculate full loop length
2589 loop_seg_lengths_sum = 0
2590 for i in range(len(loop_segments_lengths)):
2591 loop_seg_lengths_sum += loop_segments_lengths[i]
2593 # Fill the multidimensional list with the proportions of all the segments
2594 for st in range(len(pts_on_strokes_with_proportions_U)):
2595 proportions_loops_crossing_strokes[st][lp] = \
2596 loop_segments_lengths[st] / loop_seg_lengths_sum
2598 # Calculate proportions for each stroke
2599 for st in range(len(pts_on_strokes_with_proportions_U)):
2600 actual_stroke_spline = []
2601 # Needs to be a list for the "distribute_pts" method
2602 actual_stroke_spline.append(self.main_splines.data.splines[st])
2604 # Calculate the proportions for the actual stroke.
2605 actual_edges_proportions_U = []
2606 for i in range(len(edges_proportions_U)):
2607 proportions_sum = 0
2609 # Sum the proportions of this loop up to the actual.
2610 for t in range(0, st + 1):
2611 proportions_sum += proportions_loops_crossing_strokes[t][i]
2612 # i + 1, because proportions_loops_crossing_strokes refers to loops,
2613 # and the proportions refer to edges, so we start at the element 1
2614 # of proportions_loops_crossing_strokes instead of element 0
2615 actual_edges_proportions_U.append(
2616 edges_proportions_U[i] -
2617 ((edges_proportions_U[i] - edges_proportions_U2[i]) * proportions_sum)
2619 points_actual_spline = self.distribute_pts(actual_stroke_spline, actual_edges_proportions_U)
2620 sketched_splines_parsed.append(points_actual_spline[0])
2621 else:
2622 sketched_splines_parsed = pts_on_strokes_with_proportions_U
2624 # If the selection type is "TWO_NOT_CONNECTED" replace the
2625 # points of the last spline with the points in the "target" selection
2626 if selection_type == "TWO_NOT_CONNECTED":
2627 if self.selection_U2_exists:
2628 for i in range(0, len(sketched_splines_parsed[len(sketched_splines_parsed) - 1])):
2629 sketched_splines_parsed[len(sketched_splines_parsed) - 1][i] = \
2630 self.main_object.matrix_world @ verts_ordered_U2[i].co
2632 # Create temporary curves along the "control-points" found
2633 # on the sketched curves and the mesh selection
2634 mesh_ctrl_pts_name = "SURFSKIO_ctrl_pts"
2635 me = bpy.data.meshes.new(mesh_ctrl_pts_name)
2636 ob_ctrl_pts = bpy.data.objects.new(mesh_ctrl_pts_name, me)
2637 ob_ctrl_pts.data = me
2638 bpy.context.collection.objects.link(ob_ctrl_pts)
2640 cyclic_loops_U = []
2641 first_verts = []
2642 second_verts = []
2643 last_verts = []
2645 for i in range(0, verts_count_U):
2646 vert_num_in_spline = 1
2648 if self.selection_U_exists:
2649 ob_ctrl_pts.data.vertices.add(1)
2650 last_v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
2651 last_v.co = self.main_object.matrix_world @ verts_ordered_U[i].co
2653 vert_num_in_spline += 1
2655 for t in range(0, len(sketched_splines_parsed)):
2656 ob_ctrl_pts.data.vertices.add(1)
2657 v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
2658 v.co = sketched_splines_parsed[t][i]
2660 if vert_num_in_spline > 1:
2661 ob_ctrl_pts.data.edges.add(1)
2662 ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[0] = \
2663 len(ob_ctrl_pts.data.vertices) - 2
2664 ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[1] = \
2665 len(ob_ctrl_pts.data.vertices) - 1
2667 if t == 0:
2668 first_verts.append(v.index)
2670 if t == 1:
2671 second_verts.append(v.index)
2673 if t == len(sketched_splines_parsed) - 1:
2674 last_verts.append(v.index)
2676 last_v = v
2677 vert_num_in_spline += 1
2679 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2680 ob_ctrl_pts.select_set(True)
2681 bpy.context.view_layer.objects.active = ob_ctrl_pts
2683 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2684 bpy.ops.mesh.select_all(action='DESELECT')
2685 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2687 # Determine which loops-U will be "Cyclic"
2688 for i in range(0, len(first_verts)):
2689 # When there is Cyclic Cross there is no need of
2690 # Automatic Join, (and there are at least three strokes)
2691 if self.automatic_join and not self.cyclic_cross and \
2692 selection_type != "TWO_CONNECTED" and len(self.main_splines.data.splines) >= 3:
2694 v = ob_ctrl_pts.data.vertices
2695 first_point_co = v[first_verts[i]].co
2696 second_point_co = v[second_verts[i]].co
2697 last_point_co = v[last_verts[i]].co
2699 # Coordinates of the point in the center of both the first and last verts.
2700 verts_center_co = [
2701 (first_point_co[0] + last_point_co[0]) / 2,
2702 (first_point_co[1] + last_point_co[1]) / 2,
2703 (first_point_co[2] + last_point_co[2]) / 2
2705 vec_A = second_point_co - first_point_co
2706 vec_B = second_point_co - Vector(verts_center_co)
2708 # Calculate the length of the first segment of the loop,
2709 # and the length it would have after moving the first vert
2710 # to the middle position between first and last
2711 length_original = (second_point_co - first_point_co).length
2712 length_target = (second_point_co - Vector(verts_center_co)).length
2714 angle = vec_A.angle(vec_B) / pi
2716 # If the target length doesn't stretch too much, and the
2717 # its angle doesn't change to much either
2718 if length_target <= length_original * 1.03 * self.join_stretch_factor and \
2719 angle <= 0.008 * self.join_stretch_factor and not self.selection_U_exists:
2721 cyclic_loops_U.append(True)
2722 # Move the first vert to the center coordinates
2723 ob_ctrl_pts.data.vertices[first_verts[i]].co = verts_center_co
2724 # Select the last verts from Cyclic loops, for later deletion all at once
2725 v[last_verts[i]].select_set(True)
2726 else:
2727 cyclic_loops_U.append(False)
2728 else:
2729 # If "Cyclic Cross" is active then "all" crossing curves become cyclic
2730 if self.cyclic_cross and not self.selection_U_exists and not \
2731 ((self.selection_V_exists and not self.selection_V_is_closed) or
2732 (self.selection_V2_exists and not self.selection_V2_is_closed)):
2734 cyclic_loops_U.append(True)
2735 else:
2736 cyclic_loops_U.append(False)
2738 # The cyclic_loops_U list needs to be reversed.
2739 cyclic_loops_U.reverse()
2741 # Delete the previously selected (last_)verts.
2742 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2743 bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
2744 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2746 # Create curves from control points.
2747 bpy.ops.object.convert('INVOKE_REGION_WIN', target='CURVE', keep_original=False)
2748 ob_curves_surf = bpy.context.view_layer.objects.active
2749 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2750 bpy.ops.curve.spline_type_set('INVOKE_REGION_WIN', type='BEZIER')
2751 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2753 # Make Cyclic the splines designated as Cyclic.
2754 for i in range(0, len(cyclic_loops_U)):
2755 ob_curves_surf.data.splines[i].use_cyclic_u = cyclic_loops_U[i]
2757 # Get the coords of all points on first loop-U, for later comparison with its
2758 # subdivided version, to know which points of the loops-U are crossed by the
2759 # original strokes. The indices will be the same for the other loops-U
2760 if self.loops_on_strokes:
2761 coords_loops_U_control_points = []
2762 for p in ob_ctrl_pts.data.splines[0].bezier_points:
2763 coords_loops_U_control_points.append(["%.4f" % p.co[0], "%.4f" % p.co[1], "%.4f" % p.co[2]])
2765 tuple(coords_loops_U_control_points)
2767 # Calculate number of edges-V in case option "Loops on strokes" is active or inactive
2768 if self.loops_on_strokes and not self.selection_V_exists:
2769 edges_V_count = len(self.main_splines.data.splines) * self.edges_V
2770 else:
2771 edges_V_count = len(edges_proportions_V)
2773 # The Follow precision will vary depending on the number of Follow face-loops
2774 precision_multiplier = round(2 + (edges_V_count / 15))
2775 curve_cuts = bpy.context.scene.bsurfaces.SURFSK_precision * precision_multiplier
2777 # Subdivide the curves
2778 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=curve_cuts)
2780 # The verts position shifting that happens with splines subdivision.
2781 # For later reorder splines points
2782 verts_position_shift = curve_cuts + 1
2783 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2785 # Reorder coordinates of the points of each spline to put the first point of
2786 # the spline starting at the position it was the first point before sudividing
2787 # the curve. And make a new curve object per spline (to handle memory better later)
2788 splines_U_objects = []
2789 for i in range(len(ob_curves_surf.data.splines)):
2790 spline_U_curve = bpy.data.curves.new('SURFSKIO_spline_U_' + str(i), 'CURVE')
2791 ob_spline_U = bpy.data.objects.new('SURFSKIO_spline_U_' + str(i), spline_U_curve)
2792 bpy.context.collection.objects.link(ob_spline_U)
2794 spline_U_curve.dimensions = "3D"
2796 # Add points to the spline in the new curve object
2797 ob_spline_U.data.splines.new('BEZIER')
2798 for t in range(len(ob_curves_surf.data.splines[i].bezier_points)):
2799 if cyclic_loops_U[i] is True and not self.selection_U_exists: # If the loop is cyclic
2800 if t + verts_position_shift <= len(ob_curves_surf.data.splines[i].bezier_points) - 1:
2801 point_index = t + verts_position_shift
2802 else:
2803 point_index = t + verts_position_shift - len(ob_curves_surf.data.splines[i].bezier_points)
2804 else:
2805 point_index = t
2806 # to avoid adding the first point since it's added when the spline is created
2807 if t > 0:
2808 ob_spline_U.data.splines[0].bezier_points.add(1)
2809 ob_spline_U.data.splines[0].bezier_points[t].co = \
2810 ob_curves_surf.data.splines[i].bezier_points[point_index].co
2812 if cyclic_loops_U[i] is True and not self.selection_U_exists: # If the loop is cyclic
2813 # Add a last point at the same location as the first one
2814 ob_spline_U.data.splines[0].bezier_points.add(1)
2815 ob_spline_U.data.splines[0].bezier_points[len(ob_spline_U.data.splines[0].bezier_points) - 1].co = \
2816 ob_spline_U.data.splines[0].bezier_points[0].co
2817 else:
2818 ob_spline_U.data.splines[0].use_cyclic_u = False
2820 splines_U_objects.append(ob_spline_U)
2821 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2822 ob_spline_U.select_set(True)
2823 bpy.context.view_layer.objects.active = ob_spline_U
2825 # When option "Loops on strokes" is active each "Cross" loop will have
2826 # its own proportions according to where the original strokes "touch" them
2827 if self.loops_on_strokes:
2828 # Get the indices of points where the original strokes "touch" loops-U
2829 points_U_crossed_by_strokes = []
2830 for i in range(len(splines_U_objects[0].data.splines[0].bezier_points)):
2831 bp = splines_U_objects[0].data.splines[0].bezier_points[i]
2832 if ["%.4f" % bp.co[0], "%.4f" % bp.co[1], "%.4f" % bp.co[2]] in coords_loops_U_control_points:
2833 points_U_crossed_by_strokes.append(i)
2835 # Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke
2836 edge_order_number_for_splines = {}
2837 if self.selection_V_exists:
2838 # For two-connected selections add a first hypothetic stroke at the beginning.
2839 if selection_type == "TWO_CONNECTED":
2840 edge_order_number_for_splines[0] = 0
2842 for i in range(len(self.main_splines.data.splines)):
2843 sp = self.main_splines.data.splines[i]
2844 v_idx, dist_temp = self.shortest_distance(
2845 self.main_object,
2846 sp.bezier_points[0].co,
2847 verts_ordered_V_indices
2849 # Get the position (edges count) of the vert v_idx in the selected chain V
2850 edge_idx_in_chain = verts_ordered_V_indices.index(v_idx)
2852 # For two-connected selections the strokes go after the
2853 # hypothetic stroke added before, so the index adds one per spline
2854 if selection_type == "TWO_CONNECTED":
2855 spline_number = i + 1
2856 else:
2857 spline_number = i
2859 edge_order_number_for_splines[spline_number] = edge_idx_in_chain
2861 # Get the first and last verts indices for later comparison
2862 if i == 0:
2863 first_v_idx = v_idx
2864 elif i == len(self.main_splines.data.splines) - 1:
2865 last_v_idx = v_idx
2867 if self.selection_V_is_closed:
2868 # If there is no last stroke on the last vertex (same as first vertex),
2869 # add a hypothetic spline at last vert order
2870 if first_v_idx != last_v_idx:
2871 edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
2872 len(verts_ordered_V_indices) - 1
2873 else:
2874 if self.cyclic_cross:
2875 edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
2876 len(verts_ordered_V_indices) - 2
2877 edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
2878 len(verts_ordered_V_indices) - 1
2879 else:
2880 edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
2881 len(verts_ordered_V_indices) - 1
2883 # Get the coords of the points distributed along the
2884 # "crossing curves", with appropriate proportions-V
2885 surface_splines_parsed = []
2886 for i in range(len(splines_U_objects)):
2887 sp_ob = splines_U_objects[i]
2888 # If "Loops on strokes" option is active, calculate the proportions for each loop-U
2889 if self.loops_on_strokes:
2890 # Segments distances from stroke to stroke
2891 dist = 0
2892 full_dist = 0
2893 segments_distances = []
2894 for t in range(len(sp_ob.data.splines[0].bezier_points)):
2895 bp = sp_ob.data.splines[0].bezier_points[t]
2897 if t == 0:
2898 last_p = bp.co
2899 else:
2900 actual_p = bp.co
2901 dist += (last_p - actual_p).length
2903 if t in points_U_crossed_by_strokes:
2904 segments_distances.append(dist)
2905 full_dist += dist
2907 dist = 0
2909 last_p = actual_p
2911 # Calculate Proportions.
2912 used_edges_proportions_V = []
2913 for t in range(len(segments_distances)):
2914 if self.selection_V_exists:
2915 if t == 0:
2916 order_number_last_stroke = 0
2918 segment_edges_length_V = 0
2919 segment_edges_length_V2 = 0
2920 for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
2921 segment_edges_length_V += edges_lengths_V[order]
2922 if self.selection_V2_exists:
2923 segment_edges_length_V2 += edges_lengths_V2[order]
2925 for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
2926 # Calculate each "sub-segment" (the ones between each stroke) length
2927 if self.selection_V2_exists:
2928 proportion_sub_seg = (edges_lengths_V2[order] -
2929 ((edges_lengths_V2[order] - edges_lengths_V[order]) /
2930 len(splines_U_objects) * i)) / (segment_edges_length_V2 -
2931 (segment_edges_length_V2 - segment_edges_length_V) /
2932 len(splines_U_objects) * i)
2934 sub_seg_dist = segments_distances[t] * proportion_sub_seg
2935 else:
2936 proportion_sub_seg = edges_lengths_V[order] / segment_edges_length_V
2937 sub_seg_dist = segments_distances[t] * proportion_sub_seg
2939 used_edges_proportions_V.append(sub_seg_dist / full_dist)
2941 order_number_last_stroke = edge_order_number_for_splines[t + 1]
2943 else:
2944 for c in range(self.edges_V):
2945 # Calculate each "sub-segment" (the ones between each stroke) length
2946 sub_seg_dist = segments_distances[t] / self.edges_V
2947 used_edges_proportions_V.append(sub_seg_dist / full_dist)
2949 actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
2950 surface_splines_parsed.append(actual_spline[0])
2952 else:
2953 if self.selection_V2_exists:
2954 used_edges_proportions_V = []
2955 for p in range(len(edges_proportions_V)):
2956 used_edges_proportions_V.append(
2957 edges_proportions_V2[p] -
2958 ((edges_proportions_V2[p] -
2959 edges_proportions_V[p]) / len(splines_U_objects) * i)
2961 else:
2962 used_edges_proportions_V = edges_proportions_V
2964 actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
2965 surface_splines_parsed.append(actual_spline[0])
2967 # Set the verts of the first and last splines to the locations
2968 # of the respective verts in the selections
2969 if self.selection_V_exists:
2970 for i in range(0, len(surface_splines_parsed[0])):
2971 surface_splines_parsed[len(surface_splines_parsed) - 1][i] = \
2972 self.main_object.matrix_world @ verts_ordered_V[i].co
2974 if selection_type == "TWO_NOT_CONNECTED":
2975 if self.selection_V2_exists:
2976 for i in range(0, len(surface_splines_parsed[0])):
2977 surface_splines_parsed[0][i] = self.main_object.matrix_world @ verts_ordered_V2[i].co
2979 # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
2980 # merge the verts of the tips of the loops when they are "near enough"
2981 if self.automatic_join and selection_type != "TWO_CONNECTED":
2982 # Join the tips of "Follow" loops that are near enough and must be "closed"
2983 if not self.selection_V_exists and len(edges_proportions_U) >= 3:
2984 for i in range(len(surface_splines_parsed[0])):
2985 sp = surface_splines_parsed
2986 loop_segment_dist = (sp[0][i] - sp[1][i]).length
2987 full_loop_dist = loop_segment_dist * self.edges_U
2989 verts_middle_position_co = [
2990 (sp[0][i][0] + sp[len(sp) - 1][i][0]) / 2,
2991 (sp[0][i][1] + sp[len(sp) - 1][i][1]) / 2,
2992 (sp[0][i][2] + sp[len(sp) - 1][i][2]) / 2
2994 points_original = []
2995 points_original.append(sp[1][i])
2996 points_original.append(sp[0][i])
2998 points_target = []
2999 points_target.append(sp[1][i])
3000 points_target.append(Vector(verts_middle_position_co))
3002 vec_A = points_original[0] - points_original[1]
3003 vec_B = points_target[0] - points_target[1]
3004 # check for zero angles, not sure if it is a great fix
3005 if vec_A.length != 0 and vec_B.length != 0:
3006 angle = vec_A.angle(vec_B) / pi
3007 edge_new_length = (Vector(verts_middle_position_co) - sp[1][i]).length
3008 else:
3009 angle = 0
3010 edge_new_length = 0
3012 # If after moving the verts to the middle point, the segment doesn't stretch too much
3013 if edge_new_length <= loop_segment_dist * 1.5 * \
3014 self.join_stretch_factor and angle < 0.25 * self.join_stretch_factor:
3016 # Avoid joining when the actual loop must be merged with the original mesh
3017 if not (self.selection_U_exists and i == 0) and \
3018 not (self.selection_U2_exists and i == len(surface_splines_parsed[0]) - 1):
3020 # Change the coords of both verts to the middle position
3021 surface_splines_parsed[0][i] = verts_middle_position_co
3022 surface_splines_parsed[len(surface_splines_parsed) - 1][i] = verts_middle_position_co
3024 # Delete object with control points and object from grease pencil conversion
3025 bpy.ops.object.delete({"selected_objects": [ob_ctrl_pts]})
3027 bpy.ops.object.delete({"selected_objects": splines_U_objects})
3029 # Generate surface
3031 # Get all verts coords
3032 all_surface_verts_co = []
3033 for i in range(0, len(surface_splines_parsed)):
3034 # Get coords of all verts and make a list with them
3035 for pt_co in surface_splines_parsed[i]:
3036 all_surface_verts_co.append(pt_co)
3038 # Define verts for each face
3039 all_surface_faces = []
3040 for i in range(0, len(all_surface_verts_co) - len(surface_splines_parsed[0])):
3041 if ((i + 1) / len(surface_splines_parsed[0]) != int((i + 1) / len(surface_splines_parsed[0]))):
3042 all_surface_faces.append(
3043 [i + 1, i, i + len(surface_splines_parsed[0]),
3044 i + len(surface_splines_parsed[0]) + 1]
3046 # Build the mesh
3047 surf_me_name = "SURFSKIO_surface"
3048 me_surf = bpy.data.meshes.new(surf_me_name)
3050 me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
3052 me_surf.update()
3054 ob_surface = bpy.data.objects.new(surf_me_name, me_surf)
3055 bpy.context.collection.objects.link(ob_surface)
3057 # Select all the "unselected but participating" verts, from closed selection
3058 # or double selections with middle-vertex, for later join with remove doubles
3059 for v_idx in single_unselected_verts:
3060 self.main_object.data.vertices[v_idx].select_set(True)
3062 # Join the new mesh to the main object
3063 ob_surface.select_set(True)
3064 self.main_object.select_set(True)
3065 bpy.context.view_layer.objects.active = self.main_object
3067 bpy.ops.object.join('INVOKE_REGION_WIN')
3069 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3071 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN', threshold=0.0001)
3072 bpy.ops.mesh.normals_make_consistent('INVOKE_REGION_WIN', inside=False)
3073 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
3075 return{'FINISHED'}
3077 def execute(self, context):
3079 if bpy.ops.object.mode_set.poll():
3080 bpy.ops.object.mode_set(mode='OBJECT')
3082 bsurfaces_props = bpy.context.scene.bsurfaces
3084 self.main_object = bsurfaces_props.SURFSK_object_with_retopology
3085 self.main_object.select_set(True)
3086 bpy.context.view_layer.objects.active = self.main_object
3088 if not self.is_fill_faces:
3089 bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
3090 value='True, False, False')
3092 # Build splines from the "last saved splines".
3093 last_saved_curve = bpy.data.curves.new('SURFSKIO_last_crv', 'CURVE')
3094 self.main_splines = bpy.data.objects.new('SURFSKIO_last_crv', last_saved_curve)
3095 bpy.context.collection.objects.link(self.main_splines)
3097 last_saved_curve.dimensions = "3D"
3099 for sp in self.last_strokes_splines_coords:
3100 spline = self.main_splines.data.splines.new('BEZIER')
3101 # less one because one point is added when the spline is created
3102 spline.bezier_points.add(len(sp) - 1)
3103 for p in range(0, len(sp)):
3104 spline.bezier_points[p].co = [sp[p][0], sp[p][1], sp[p][2]]
3106 #bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3107 bpy.ops.object.mode_set(mode='OBJECT')
3109 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3110 self.main_splines.select_set(True)
3111 bpy.context.view_layer.objects.active = self.main_splines
3113 #bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3114 bpy.ops.object.mode_set(mode='EDIT')
3116 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
3117 # Important to make it vector first and then automatic, otherwise the
3118 # tips handles get too big and distort the shrinkwrap results later
3119 bpy.ops.curve.handle_type_set(type='VECTOR')
3120 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
3121 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3122 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3124 self.main_splines.name = "SURFSKIO_temp_strokes"
3126 if self.is_crosshatch:
3127 strokes_for_crosshatch = True
3128 strokes_for_rectangular_surface = False
3129 else:
3130 strokes_for_rectangular_surface = True
3131 strokes_for_crosshatch = False
3133 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3134 self.main_object.select_set(True)
3135 bpy.context.view_layer.objects.active = self.main_object
3137 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3139 if strokes_for_rectangular_surface:
3140 self.rectangular_surface()
3141 elif strokes_for_crosshatch:
3142 self.crosshatch_surface_execute()
3144 # Delete main splines
3145 #bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3146 bpy.ops.object.mode_set(mode='OBJECT')
3147 bpy.ops.object.delete({"selected_objects": [self.main_splines]})
3149 # Delete grease pencil strokes
3150 if self.strokes_type == "GP_STROKES" and not self.stopping_errors and not self.keep_strokes:
3151 bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.data.layers[0].clear()
3153 # Delete annotations
3154 if self.strokes_type == "GP_ANNOTATION" and not self.stopping_errors and not self.keep_strokes:
3155 bpy.data.grease_pencils["Annotations"].layers["Note"].clear()
3157 bsurfaces_props.SURFSK_edges_U = self.edges_U
3158 bsurfaces_props.SURFSK_edges_V = self.edges_V
3159 bsurfaces_props.SURFSK_cyclic_cross = self.cyclic_cross
3160 bsurfaces_props.SURFSK_cyclic_follow = self.cyclic_follow
3161 bsurfaces_props.SURFSK_automatic_join = self.automatic_join
3162 bsurfaces_props.SURFSK_loops_on_strokes = self.loops_on_strokes
3163 bsurfaces_props.SURFSK_keep_strokes = self.keep_strokes
3165 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3166 self.main_object.select_set(True)
3167 bpy.context.view_layer.objects.active = self.main_object
3169 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3171 return{'FINISHED'}
3173 def invoke(self, context, event):
3174 if bpy.ops.object.mode_set.poll():
3175 bpy.ops.object.mode_set(mode='OBJECT')
3177 bsurfaces_props = bpy.context.scene.bsurfaces
3178 self.cyclic_cross = bsurfaces_props.SURFSK_cyclic_cross
3179 self.cyclic_follow = bsurfaces_props.SURFSK_cyclic_follow
3180 self.automatic_join = bsurfaces_props.SURFSK_automatic_join
3181 self.loops_on_strokes = bsurfaces_props.SURFSK_loops_on_strokes
3182 self.keep_strokes = bsurfaces_props.SURFSK_keep_strokes
3184 self.main_object = bsurfaces_props.SURFSK_object_with_retopology
3185 try:
3186 self.main_object.select_set(True)
3187 except:
3188 self.report({'WARNING'}, "Specify the name of the object with retopology")
3189 return{"CANCELLED"}
3190 bpy.context.view_layer.objects.active = self.main_object
3192 self.main_object_selected_verts_count = int(self.main_object.data.total_vert_sel)
3194 bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
3195 value='True, False, False')
3197 if self.loops_on_strokes:
3198 self.edges_V = 1
3199 else:
3200 self.edges_V = bsurfaces_props.SURFSK_edges_V
3202 self.is_fill_faces = False
3203 self.stopping_errors = False
3204 self.last_strokes_splines_coords = []
3206 # Determine the type of the strokes
3207 self.strokes_type = get_strokes_type(context)
3209 # Check if it will be used grease pencil strokes or curves
3210 # If there are strokes to be used
3211 if self.strokes_type == "GP_STROKES" or self.strokes_type == "EXTERNAL_CURVE" or self.strokes_type == "GP_ANNOTATION":
3212 if self.strokes_type == "GP_STROKES":
3213 # Convert grease pencil strokes to curve
3214 gp = bsurfaces_props.SURFSK_object_with_strokes
3215 #bpy.ops.gpencil.convert(type='CURVE', use_link_strokes=False)
3216 self.original_curve = conver_gpencil_to_curve(self, context, gp, 'GPensil')
3217 # XXX gpencil.convert now keep org object as active/selected, *not* newly created curve!
3218 # XXX This is far from perfect, but should work in most cases...
3219 # self.original_curve = bpy.context.object
3220 gplayer_prefix_translated = bpy.app.translations.pgettext_data('GP_Layer')
3221 for ob in bpy.context.selected_objects:
3222 if ob != bpy.context.view_layer.objects.active and \
3223 ob.name.startswith((gplayer_prefix_translated, 'GP_Layer')):
3224 self.original_curve = ob
3225 self.using_external_curves = False
3227 elif self.strokes_type == "GP_ANNOTATION":
3228 # Convert grease pencil strokes to curve
3229 gp = bpy.data.grease_pencils["Annotations"]
3230 #bpy.ops.gpencil.convert(type='CURVE', use_link_strokes=False)
3231 self.original_curve = conver_gpencil_to_curve(self, context, gp, 'Annotation')
3232 # XXX gpencil.convert now keep org object as active/selected, *not* newly created curve!
3233 # XXX This is far from perfect, but should work in most cases...
3234 # self.original_curve = bpy.context.object
3235 gplayer_prefix_translated = bpy.app.translations.pgettext_data('GP_Layer')
3236 for ob in bpy.context.selected_objects:
3237 if ob != bpy.context.view_layer.objects.active and \
3238 ob.name.startswith((gplayer_prefix_translated, 'GP_Layer')):
3239 self.original_curve = ob
3240 self.using_external_curves = False
3242 elif self.strokes_type == "EXTERNAL_CURVE":
3243 for ob in bpy.context.selected_objects:
3244 if ob != bpy.context.view_layer.objects.active:
3245 self.original_curve = ob
3246 self.using_external_curves = True
3248 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3250 # Make sure there are no objects left from erroneous
3251 # executions of this operator, with the reserved names used here
3252 for o in bpy.data.objects:
3253 if o.name.find("SURFSKIO_") != -1:
3254 bpy.ops.object.delete({"selected_objects": [o]})
3256 #bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3257 self.original_curve.select_set(True)
3258 bpy.context.view_layer.objects.active = self.original_curve
3260 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3262 self.temporary_curve = bpy.context.view_layer.objects.active
3264 # Deselect all points of the curve
3265 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3266 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3267 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3269 # Delete splines with only a single isolated point
3270 for i in range(len(self.temporary_curve.data.splines)):
3271 sp = self.temporary_curve.data.splines[i]
3273 if len(sp.bezier_points) == 1:
3274 sp.bezier_points[0].select_control_point = True
3276 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3277 bpy.ops.curve.delete(type='VERT')
3278 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3280 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3281 self.temporary_curve.select_set(True)
3282 bpy.context.view_layer.objects.active = self.temporary_curve
3284 # Set a minimum number of points for crosshatch
3285 minimum_points_num = 15
3287 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3288 # Check if the number of points of each curve has at least the number of points
3289 # of minimum_points_num, which is a bit more than the face-loops limit.
3290 # If not, subdivide to reach at least that number of points
3291 for i in range(len(self.temporary_curve.data.splines)):
3292 sp = self.temporary_curve.data.splines[i]
3294 if len(sp.bezier_points) < minimum_points_num:
3295 for bp in sp.bezier_points:
3296 bp.select_control_point = True
3298 if (len(sp.bezier_points) - 1) != 0:
3299 # Formula to get the number of cuts that will make a curve
3300 # of N number of points have near to "minimum_points_num"
3301 # points, when subdividing with this number of cuts
3302 subdivide_cuts = int(
3303 (minimum_points_num - len(sp.bezier_points)) /
3304 (len(sp.bezier_points) - 1)
3305 ) + 1
3306 else:
3307 subdivide_cuts = 0
3309 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3310 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3312 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3314 # Detect if the strokes are a crosshatch and do it if it is
3315 self.crosshatch_surface_invoke(self.temporary_curve)
3317 if not self.is_crosshatch:
3318 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3319 self.temporary_curve.select_set(True)
3320 bpy.context.view_layer.objects.active = self.temporary_curve
3322 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3324 # Set a minimum number of points for rectangular surfaces
3325 minimum_points_num = 60
3327 # Check if the number of points of each curve has at least the number of points
3328 # of minimum_points_num, which is a bit more than the face-loops limit.
3329 # If not, subdivide to reach at least that number of points
3330 for i in range(len(self.temporary_curve.data.splines)):
3331 sp = self.temporary_curve.data.splines[i]
3333 if len(sp.bezier_points) < minimum_points_num:
3334 for bp in sp.bezier_points:
3335 bp.select_control_point = True
3337 if (len(sp.bezier_points) - 1) != 0:
3338 # Formula to get the number of cuts that will make a curve of
3339 # N number of points have near to "minimum_points_num" points,
3340 # when subdividing with this number of cuts
3341 subdivide_cuts = int(
3342 (minimum_points_num - len(sp.bezier_points)) /
3343 (len(sp.bezier_points) - 1)
3344 ) + 1
3345 else:
3346 subdivide_cuts = 0
3348 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3349 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3351 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3353 # Save coordinates of the actual strokes (as the "last saved splines")
3354 for sp_idx in range(len(self.temporary_curve.data.splines)):
3355 self.last_strokes_splines_coords.append([])
3356 for bp_idx in range(len(self.temporary_curve.data.splines[sp_idx].bezier_points)):
3357 coords = self.temporary_curve.matrix_world @ \
3358 self.temporary_curve.data.splines[sp_idx].bezier_points[bp_idx].co
3359 self.last_strokes_splines_coords[sp_idx].append([coords[0], coords[1], coords[2]])
3361 # Check for cyclic splines, put the first and last points in the middle of their actual positions
3362 for sp_idx in range(len(self.temporary_curve.data.splines)):
3363 if self.temporary_curve.data.splines[sp_idx].use_cyclic_u is True:
3364 first_p_co = self.last_strokes_splines_coords[sp_idx][0]
3365 last_p_co = self.last_strokes_splines_coords[sp_idx][
3366 len(self.last_strokes_splines_coords[sp_idx]) - 1
3368 target_co = [
3369 (first_p_co[0] + last_p_co[0]) / 2,
3370 (first_p_co[1] + last_p_co[1]) / 2,
3371 (first_p_co[2] + last_p_co[2]) / 2
3374 self.last_strokes_splines_coords[sp_idx][0] = target_co
3375 self.last_strokes_splines_coords[sp_idx][
3376 len(self.last_strokes_splines_coords[sp_idx]) - 1
3377 ] = target_co
3378 tuple(self.last_strokes_splines_coords)
3380 # Estimation of the average length of the segments between
3381 # each point of the grease pencil strokes.
3382 # Will be useful to determine whether a curve should be made "Cyclic"
3383 segments_lengths_sum = 0
3384 segments_count = 0
3385 random_spline = self.temporary_curve.data.splines[0].bezier_points
3386 for i in range(0, len(random_spline)):
3387 if i != 0 and len(random_spline) - 1 >= i:
3388 segments_lengths_sum += (random_spline[i - 1].co - random_spline[i].co).length
3389 segments_count += 1
3391 self.average_gp_segment_length = segments_lengths_sum / segments_count
3393 # Delete temporary strokes curve object
3394 bpy.ops.object.delete({"selected_objects": [self.temporary_curve]})
3396 # If "Keep strokes" option is not active, delete original strokes curve object
3397 if (not self.stopping_errors and not self.keep_strokes) or self.is_crosshatch:
3398 bpy.ops.object.delete({"selected_objects": [self.original_curve]})
3400 # Delete grease pencil strokes
3401 if self.strokes_type == "GP_STROKES" and not self.stopping_errors and not self.keep_strokes:
3402 bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.data.layers[0].clear()
3404 # Delete grease pencil strokes
3405 if self.strokes_type == "GP_ANNOTATION" and not self.stopping_errors and not self.keep_strokes:
3406 bpy.data.grease_pencils["Annotations"].layers["Note"].clear()
3408 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3409 self.main_object.select_set(True)
3410 bpy.context.view_layer.objects.active = self.main_object
3412 # Set again since "execute()" will turn it again to its initial value
3413 self.execute(context)
3415 if not self.stopping_errors:
3416 return {"FINISHED"}
3417 else:
3418 return{"CANCELLED"}
3420 elif self.strokes_type == "SELECTION_ALONE":
3421 self.is_fill_faces = True
3422 created_faces_count = self.fill_with_faces(self.main_object)
3424 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3426 if created_faces_count == 0:
3427 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3428 return {"CANCELLED"}
3429 else:
3430 return {"FINISHED"}
3432 if self.strokes_type == "EXTERNAL_NO_CURVE":
3433 self.report({'WARNING'}, "The secondary object is not a Curve.")
3434 return{"CANCELLED"}
3436 elif self.strokes_type == "MORE_THAN_ONE_EXTERNAL":
3437 self.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3438 return{"CANCELLED"}
3440 elif self.strokes_type == "SINGLE_GP_STROKE_NO_SELECTION" or \
3441 self.strokes_type == "SINGLE_CURVE_STROKE_NO_SELECTION":
3443 self.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
3444 return{"CANCELLED"}
3446 elif self.strokes_type == "NO_STROKES":
3447 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3448 return{"CANCELLED"}
3450 elif self.strokes_type == "CURVE_WITH_NON_BEZIER_SPLINES":
3451 self.report({'WARNING'}, "All splines must be Bezier.")
3452 return{"CANCELLED"}
3454 else:
3455 return{"CANCELLED"}
3457 # Edit strokes operator
3458 class GPENCIL_OT_SURFSK_init(Operator):
3459 bl_idname = "gpencil.surfsk_init"
3460 bl_label = "Bsurfaces initialize"
3461 bl_description = "Bsurfaces initialiaze"
3463 active_object: PointerProperty(type=bpy.types.Object)
3465 def execute(self, context):
3467 bs = bpy.context.scene.bsurfaces
3469 if bpy.ops.object.mode_set.poll():
3470 bpy.ops.object.mode_set(mode='OBJECT')
3472 if bs.SURFSK_object_with_retopology == None:
3473 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3474 mesh = bpy.data.meshes.new('BSurfaceMesh')
3475 mesh_object = object_utils.object_data_add(context, mesh, operator=None)
3476 mesh_object.select_set(True)
3477 mesh_object.show_all_edges = True
3478 mesh_object.show_in_front = True
3479 mesh_object.display_type = 'SOLID'
3480 mesh_object.show_wire = True
3481 bpy.context.view_layer.objects.active = mesh_object
3482 bpy.ops.object.modifier_add(type='SHRINKWRAP')
3483 modifier = mesh_object.modifiers["Shrinkwrap"]
3484 if self.active_object is not None:
3485 modifier.target = self.active_object
3486 modifier.wrap_method = 'TARGET_PROJECT'
3487 modifier.wrap_mode = 'OUTSIDE_SURFACE'
3488 #modifier.offset = 0.05
3490 bpy.context.scene.bsurfaces.SURFSK_object_with_retopology = mesh_object
3492 if not context.scene.bsurfaces.SURFSK_use_annotation and bs.SURFSK_object_with_strokes == None:
3493 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3494 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')
3495 bpy.context.scene.tool_settings.gpencil_stroke_placement_view3d = 'SURFACE'
3496 gpencil_object = bpy.context.scene.objects[bpy.context.scene.objects[-1].name]
3497 gpencil_object.select_set(True)
3498 bpy.context.view_layer.objects.active = gpencil_object
3499 bpy.ops.object.mode_set(mode='PAINT_GPENCIL')
3500 bpy.context.scene.bsurfaces.SURFSK_object_with_strokes = gpencil_object
3501 gpencil_object.data.stroke_depth_order = '3D'
3503 if context.scene.bsurfaces.SURFSK_use_annotation:
3504 bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
3505 bpy.context.scene.tool_settings.annotation_stroke_placement_view3d = 'SURFACE'
3507 return {"FINISHED"}
3509 def invoke(self, context, event):
3510 if bpy.context.active_object:
3511 self.active_object = bpy.context.active_object
3512 else:
3513 self.active_object = None
3515 self.execute(context)
3517 return {"FINISHED"}
3519 # Edit surface operator
3520 class GPENCIL_OT_SURFSK_edit_surface(Operator):
3521 bl_idname = "gpencil.surfsk_edit_surface"
3522 bl_label = "Bsurfaces edit surface"
3523 bl_description = "Edit surface mesh"
3525 def execute(self, context):
3526 bpy.context.scene.bsurfaces.SURFSK_object_with_retopology.select_set(True)
3527 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_object_with_retopology
3528 bpy.ops.object.mode_set(mode='EDIT')
3530 def invoke(self, context, event):
3531 try:
3532 bpy.context.scene.bsurfaces.SURFSK_object_with_retopology.select_set(True)
3533 except:
3534 self.report({'WARNING'}, "Specify the name of the object with retopology")
3535 return{"CANCELLED"}
3537 self.execute(context)
3539 return {"FINISHED"}
3541 # Add strokes operator
3542 class GPENCIL_OT_SURFSK_add_strokes(Operator):
3543 bl_idname = "gpencil.surfsk_add_strokes"
3544 bl_label = "Bsurfaces add strokes"
3545 bl_description = "Add the grease pencil strokes"
3547 def execute(self, context):
3548 # Determine the type of the strokes
3549 self.strokes_type = get_strokes_type(context)
3550 # Check if strokes are grease pencil strokes or a curves object
3551 selected_objs = bpy.context.selected_objects
3552 if self.strokes_type == "EXTERNAL_CURVE" or self.strokes_type == "SINGLE_CURVE_STROKE_NO_SELECTION":
3553 for ob in selected_objs:
3554 if ob != bpy.context.view_layer.objects.active:
3555 curve_ob = ob
3557 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3559 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3560 curve_ob.select_set(True)
3561 bpy.context.view_layer.objects.active = curve_ob
3563 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3564 else:
3565 bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.select_set(True)
3566 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_object_with_strokes
3567 bpy.ops.object.mode_set(mode='PAINT_GPENCIL')
3569 return{"FINISHED"}
3571 def invoke(self, context, event):
3572 try:
3573 bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.select_set(True)
3574 except:
3575 self.report({'WARNING'}, "Specify the name of the object with strokes")
3576 return{"CANCELLED"}
3578 self.execute(context)
3580 return {"FINISHED"}
3582 # Edit strokes operator
3583 class GPENCIL_OT_SURFSK_edit_strokes(Operator):
3584 bl_idname = "gpencil.surfsk_edit_strokes"
3585 bl_label = "Bsurfaces edit strokes"
3586 bl_description = "Edit the grease pencil strokes or curves used"
3588 def execute(self, context):
3589 # Determine the type of the strokes
3590 self.strokes_type = get_strokes_type(context)
3591 # Check if strokes are grease pencil strokes or a curves object
3592 selected_objs = bpy.context.selected_objects
3593 if self.strokes_type == "EXTERNAL_CURVE" or self.strokes_type == "SINGLE_CURVE_STROKE_NO_SELECTION":
3594 for ob in selected_objs:
3595 if ob != bpy.context.view_layer.objects.active:
3596 curve_ob = ob
3598 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3600 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3601 curve_ob.select_set(True)
3602 bpy.context.view_layer.objects.active = curve_ob
3604 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3605 elif self.strokes_type == "GP_STROKES" or self.strokes_type == "SINGLE_GP_STROKE_NO_SELECTION":
3606 # Convert grease pencil strokes to curve
3607 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3608 #bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
3609 gp = bpy.context.scene.bsurfaces.SURFSK_object_with_strokes
3610 conver_gpencil_to_curve(self, context, gp, 'GPensil')
3611 for ob in bpy.context.selected_objects:
3612 if ob != bpy.context.view_layer.objects.active and ob.name.startswith("GP_Layer"):
3613 ob_gp_strokes = ob
3615 ob_gp_strokes = bpy.context.object
3617 # Delete grease pencil strokes
3618 bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.data.layers[0].clear()
3620 # Clean up curves
3621 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3622 ob_gp_strokes.select_set(True)
3623 bpy.context.view_layer.objects.active = ob_gp_strokes
3625 curve_crv = ob_gp_strokes.data
3626 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3627 bpy.ops.curve.spline_type_set('INVOKE_REGION_WIN', type="BEZIER")
3628 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type="AUTOMATIC")
3629 #curve_crv.show_handles = False
3630 #curve_crv.show_normal_face = False
3632 elif self.strokes_type == "EXTERNAL_NO_CURVE":
3633 self.report({'WARNING'}, "The secondary object is not a Curve.")
3634 return{"CANCELLED"}
3636 elif self.strokes_type == "MORE_THAN_ONE_EXTERNAL":
3637 self.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3638 return{"CANCELLED"}
3640 elif self.strokes_type == "NO_STROKES" or self.strokes_type == "SELECTION_ALONE":
3641 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3642 return{"CANCELLED"}
3644 else:
3645 return{"CANCELLED"}
3647 def invoke(self, context, event):
3648 try:
3649 bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.select_set(True)
3650 except:
3651 self.report({'WARNING'}, "Specify the name of the object with strokes")
3652 return{"CANCELLED"}
3654 self.execute(context)
3656 return {"FINISHED"}
3658 # Add annotation
3659 class GPENCIL_OT_SURFSK_add_annotation(Operator):
3660 bl_idname = "gpencil.surfsk_add_annotation"
3661 bl_label = "Bsurfaces add annotation"
3662 bl_description = "Add annotation"
3664 def execute(self, context):
3665 bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
3666 bpy.context.scene.tool_settings.annotation_stroke_placement_view3d = 'SURFACE'
3668 return{"FINISHED"}
3670 def invoke(self, context, event):
3672 self.execute(context)
3674 return {"FINISHED"}
3676 class CURVE_OT_SURFSK_reorder_splines(Operator):
3677 bl_idname = "curve.surfsk_reorder_splines"
3678 bl_label = "Bsurfaces reorder splines"
3679 bl_description = "Defines the order of the splines by using grease pencil strokes"
3680 bl_options = {'REGISTER', 'UNDO'}
3682 def execute(self, context):
3683 objects_to_delete = []
3684 # Convert grease pencil strokes to curve.
3685 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3686 bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
3687 for ob in bpy.context.selected_objects:
3688 if ob != bpy.context.view_layer.objects.active and ob.name.startswith("GP_Layer"):
3689 GP_strokes_curve = ob
3691 # GP_strokes_curve = bpy.context.object
3692 objects_to_delete.append(GP_strokes_curve)
3694 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3695 GP_strokes_curve.select_set(True)
3696 bpy.context.view_layer.objects.active = GP_strokes_curve
3698 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3699 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
3700 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=100)
3701 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3703 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3704 GP_strokes_mesh = bpy.context.object
3705 objects_to_delete.append(GP_strokes_mesh)
3707 GP_strokes_mesh.data.resolution_u = 1
3708 bpy.ops.object.convert(target='MESH', keep_original=False)
3710 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3711 self.main_curve.select_set(True)
3712 bpy.context.view_layer.objects.active = self.main_curve
3714 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3715 curves_duplicate_1 = bpy.context.object
3716 objects_to_delete.append(curves_duplicate_1)
3718 minimum_points_num = 500
3720 # Some iterations since the subdivision operator
3721 # has a limit of 100 subdivisions per iteration
3722 for x in range(round(minimum_points_num / 100)):
3723 # Check if the number of points of each curve has at least the number of points
3724 # of minimum_points_num. If not, subdivide to reach at least that number of points
3725 for i in range(len(curves_duplicate_1.data.splines)):
3726 sp = curves_duplicate_1.data.splines[i]
3728 if len(sp.bezier_points) < minimum_points_num:
3729 for bp in sp.bezier_points:
3730 bp.select_control_point = True
3732 if (len(sp.bezier_points) - 1) != 0:
3733 # Formula to get the number of cuts that will make a curve of N
3734 # number of points have near to "minimum_points_num" points,
3735 # when subdividing with this number of cuts
3736 subdivide_cuts = int(
3737 (minimum_points_num - len(sp.bezier_points)) /
3738 (len(sp.bezier_points) - 1)
3739 ) + 1
3740 else:
3741 subdivide_cuts = 0
3743 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3744 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3745 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3746 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3748 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3749 curves_duplicate_2 = bpy.context.object
3750 objects_to_delete.append(curves_duplicate_2)
3752 # Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target
3753 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3754 curves_duplicate_2.select_set(True)
3755 bpy.context.view_layer.objects.active = curves_duplicate_2
3757 bpy.ops.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
3758 curves_duplicate_2.modifiers["Shrinkwrap"].wrap_method = "NEAREST_VERTEX"
3759 curves_duplicate_2.modifiers["Shrinkwrap"].target = GP_strokes_mesh
3760 bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', apply_as='DATA', modifier='Shrinkwrap')
3762 # Get the distance of each vert from its original position to its position with Shrinkwrap
3763 nearest_points_coords = {}
3764 for st_idx in range(len(curves_duplicate_1.data.splines)):
3765 for bp_idx in range(len(curves_duplicate_1.data.splines[st_idx].bezier_points)):
3766 bp_1_co = curves_duplicate_1.matrix_world @ \
3767 curves_duplicate_1.data.splines[st_idx].bezier_points[bp_idx].co
3769 bp_2_co = curves_duplicate_2.matrix_world @ \
3770 curves_duplicate_2.data.splines[st_idx].bezier_points[bp_idx].co
3772 if bp_idx == 0:
3773 shortest_dist = (bp_1_co - bp_2_co).length
3774 nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
3775 "%.4f" % bp_2_co[1],
3776 "%.4f" % bp_2_co[2])
3778 dist = (bp_1_co - bp_2_co).length
3780 if dist < shortest_dist:
3781 nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
3782 "%.4f" % bp_2_co[1],
3783 "%.4f" % bp_2_co[2])
3784 shortest_dist = dist
3786 # Get all coords of GP strokes points, for comparison
3787 GP_strokes_coords = []
3788 for st_idx in range(len(GP_strokes_curve.data.splines)):
3789 GP_strokes_coords.append(
3790 [("%.4f" % x if "%.4f" % x != "-0.00" else "0.00",
3791 "%.4f" % y if "%.4f" % y != "-0.00" else "0.00",
3792 "%.4f" % z if "%.4f" % z != "-0.00" else "0.00") for
3793 x, y, z in [bp.co for bp in GP_strokes_curve.data.splines[st_idx].bezier_points]]
3796 # Check the point of the GP strokes with the same coords as
3797 # the nearest points of the curves (with shrinkwrap)
3799 # Dictionary with GP stroke index as index, and a list as value.
3800 # The list has as index the point index of the GP stroke
3801 # nearest to the spline, and as value the spline index
3802 GP_connection_points = {}
3803 for gp_st_idx in range(len(GP_strokes_coords)):
3804 GPvert_spline_relationship = {}
3806 for splines_st_idx in range(len(nearest_points_coords)):
3807 if nearest_points_coords[splines_st_idx] in GP_strokes_coords[gp_st_idx]:
3808 GPvert_spline_relationship[
3809 GP_strokes_coords[gp_st_idx].index(nearest_points_coords[splines_st_idx])
3810 ] = splines_st_idx
3812 GP_connection_points[gp_st_idx] = GPvert_spline_relationship
3814 # Get the splines new order
3815 splines_new_order = []
3816 for i in GP_connection_points:
3817 dict_keys = sorted(GP_connection_points[i].keys()) # Sort dictionaries by key
3819 for k in dict_keys:
3820 splines_new_order.append(GP_connection_points[i][k])
3822 # Reorder
3823 curve_original_name = self.main_curve.name
3825 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3826 self.main_curve.select_set(True)
3827 bpy.context.view_layer.objects.active = self.main_curve
3829 self.main_curve.name = "SURFSKIO_CRV_ORD"
3831 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3832 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3833 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3835 for sp_idx in range(len(self.main_curve.data.splines)):
3836 self.main_curve.data.splines[0].bezier_points[0].select_control_point = True
3838 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3839 bpy.ops.curve.separate('EXEC_REGION_WIN')
3840 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3842 # Get the names of the separated splines objects in the original order
3843 splines_unordered = {}
3844 for o in bpy.data.objects:
3845 if o.name.find("SURFSKIO_CRV_ORD") != -1:
3846 spline_order_string = o.name.partition(".")[2]
3848 if spline_order_string != "" and int(spline_order_string) > 0:
3849 spline_order_index = int(spline_order_string) - 1
3850 splines_unordered[spline_order_index] = o.name
3852 # Join all splines objects in final order
3853 for order_idx in splines_new_order:
3854 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3855 bpy.data.objects[splines_unordered[order_idx]].select_set(True)
3856 bpy.data.objects["SURFSKIO_CRV_ORD"].select_set(True)
3857 bpy.context.view_layer.objects.active = bpy.data.objects["SURFSKIO_CRV_ORD"]
3859 bpy.ops.object.join('INVOKE_REGION_WIN')
3861 # Go back to the original name of the curves object.
3862 bpy.context.object.name = curve_original_name
3864 # Delete all unused objects
3865 bpy.ops.object.delete({"selected_objects": objects_to_delete})
3867 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3868 bpy.data.objects[curve_original_name].select_set(True)
3869 bpy.context.view_layer.objects.active = bpy.data.objects[curve_original_name]
3871 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3872 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3874 bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.data.layers[0].clear()
3876 return {"FINISHED"}
3878 def invoke(self, context, event):
3879 self.main_curve = bpy.context.object
3880 there_are_GP_strokes = False
3882 try:
3883 # Get the active grease pencil layer
3884 strokes_num = len(self.main_curve.grease_pencil.layers.active.active_frame.strokes)
3886 if strokes_num > 0:
3887 there_are_GP_strokes = True
3888 except:
3889 pass
3891 if there_are_GP_strokes:
3892 self.execute(context)
3893 self.report({'INFO'}, "Splines have been reordered")
3894 else:
3895 self.report({'WARNING'}, "Draw grease pencil strokes to connect splines")
3897 return {"FINISHED"}
3900 class CURVE_OT_SURFSK_first_points(Operator):
3901 bl_idname = "curve.surfsk_first_points"
3902 bl_label = "Bsurfaces set first points"
3903 bl_description = "Set the selected points as the first point of each spline"
3904 bl_options = {'REGISTER', 'UNDO'}
3906 def execute(self, context):
3907 splines_to_invert = []
3909 # Check non-cyclic splines to invert
3910 for i in range(len(self.main_curve.data.splines)):
3911 b_points = self.main_curve.data.splines[i].bezier_points
3913 if i not in self.cyclic_splines: # Only for non-cyclic splines
3914 if b_points[len(b_points) - 1].select_control_point:
3915 splines_to_invert.append(i)
3917 # Reorder points of cyclic splines, and set all handles to "Automatic"
3919 # Check first selected point
3920 cyclic_splines_new_first_pt = {}
3921 for i in self.cyclic_splines:
3922 sp = self.main_curve.data.splines[i]
3924 for t in range(len(sp.bezier_points)):
3925 bp = sp.bezier_points[t]
3926 if bp.select_control_point or bp.select_right_handle or bp.select_left_handle:
3927 cyclic_splines_new_first_pt[i] = t
3928 break # To take only one if there are more
3930 # Reorder
3931 for spline_idx in cyclic_splines_new_first_pt:
3932 sp = self.main_curve.data.splines[spline_idx]
3934 spline_old_coords = []
3935 for bp_old in sp.bezier_points:
3936 coords = (bp_old.co[0], bp_old.co[1], bp_old.co[2])
3938 left_handle_type = str(bp_old.handle_left_type)
3939 left_handle_length = float(bp_old.handle_left.length)
3940 left_handle_xyz = (
3941 float(bp_old.handle_left.x),
3942 float(bp_old.handle_left.y),
3943 float(bp_old.handle_left.z)
3945 right_handle_type = str(bp_old.handle_right_type)
3946 right_handle_length = float(bp_old.handle_right.length)
3947 right_handle_xyz = (
3948 float(bp_old.handle_right.x),
3949 float(bp_old.handle_right.y),
3950 float(bp_old.handle_right.z)
3952 spline_old_coords.append(
3953 [coords, left_handle_type,
3954 right_handle_type, left_handle_length,
3955 right_handle_length, left_handle_xyz,
3956 right_handle_xyz]
3959 for t in range(len(sp.bezier_points)):
3960 bp = sp.bezier_points
3962 if t + cyclic_splines_new_first_pt[spline_idx] + 1 <= len(bp) - 1:
3963 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1
3964 else:
3965 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1 - len(bp)
3967 bp[t].co = Vector(spline_old_coords[new_index][0])
3969 bp[t].handle_left.length = spline_old_coords[new_index][3]
3970 bp[t].handle_right.length = spline_old_coords[new_index][4]
3972 bp[t].handle_left_type = "FREE"
3973 bp[t].handle_right_type = "FREE"
3975 bp[t].handle_left.x = spline_old_coords[new_index][5][0]
3976 bp[t].handle_left.y = spline_old_coords[new_index][5][1]
3977 bp[t].handle_left.z = spline_old_coords[new_index][5][2]
3979 bp[t].handle_right.x = spline_old_coords[new_index][6][0]
3980 bp[t].handle_right.y = spline_old_coords[new_index][6][1]
3981 bp[t].handle_right.z = spline_old_coords[new_index][6][2]
3983 bp[t].handle_left_type = spline_old_coords[new_index][1]
3984 bp[t].handle_right_type = spline_old_coords[new_index][2]
3986 # Invert the non-cyclic splines designated above
3987 for i in range(len(splines_to_invert)):
3988 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3990 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3991 self.main_curve.data.splines[splines_to_invert[i]].bezier_points[0].select_control_point = True
3992 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3994 bpy.ops.curve.switch_direction()
3996 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3998 # Keep selected the first vert of each spline
3999 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4000 for i in range(len(self.main_curve.data.splines)):
4001 if not self.main_curve.data.splines[i].use_cyclic_u:
4002 bp = self.main_curve.data.splines[i].bezier_points[0]
4003 else:
4004 bp = self.main_curve.data.splines[i].bezier_points[
4005 len(self.main_curve.data.splines[i].bezier_points) - 1
4008 bp.select_control_point = True
4009 bp.select_right_handle = True
4010 bp.select_left_handle = True
4012 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4014 return {'FINISHED'}
4016 def invoke(self, context, event):
4017 self.main_curve = bpy.context.object
4019 # Check if all curves are Bezier, and detect which ones are cyclic
4020 self.cyclic_splines = []
4021 for i in range(len(self.main_curve.data.splines)):
4022 if self.main_curve.data.splines[i].type != "BEZIER":
4023 self.report({'WARNING'}, "All splines must be Bezier type")
4025 return {'CANCELLED'}
4026 else:
4027 if self.main_curve.data.splines[i].use_cyclic_u:
4028 self.cyclic_splines.append(i)
4030 self.execute(context)
4031 self.report({'INFO'}, "First points have been set")
4033 return {'FINISHED'}
4036 # Add-ons Preferences Update Panel
4038 # Define Panel classes for updating
4039 panels = (
4040 VIEW3D_PT_tools_SURFSK_mesh,
4041 VIEW3D_PT_tools_SURFSK_curve
4045 def update_panel(self, context):
4046 message = "Bsurfaces GPL Edition: Updating Panel locations has failed"
4047 try:
4048 for panel in panels:
4049 if "bl_rna" in panel.__dict__:
4050 bpy.utils.unregister_class(panel)
4052 for panel in panels:
4053 panel.bl_category = context.preferences.addons[__name__].preferences.category
4054 bpy.utils.register_class(panel)
4056 except Exception as e:
4057 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
4058 pass
4060 def conver_gpencil_to_curve(self, context, pencil, type):
4061 newCurve = bpy.data.curves.new('gpencil_curve', type='CURVE') # curvedatablock
4062 newCurve.dimensions = '3D'
4063 CurveObject = object_utils.object_data_add(context, newCurve) # place in active scene
4064 CurveObject.location = self.main_object.location
4066 if type == 'GPensil':
4067 strokes = pencil.data.layers[0].active_frame.strokes
4068 elif type == 'Annotation':
4069 strokes = bpy.data.grease_pencils["Annotations"].layers["Note"].active_frame.strokes
4071 for i, stroke in enumerate(strokes):
4072 stroke_points = strokes[i].points
4073 data_list = [ (point.co.x, point.co.y, point.co.z)
4074 for point in stroke_points ]
4075 points_to_add = len(data_list)-1
4077 flat_list = []
4078 for point in data_list:
4079 flat_list.extend(point)
4081 spline = newCurve.splines.new(type='BEZIER') # spline
4082 spline.bezier_points.add(points_to_add)
4083 spline.bezier_points.foreach_set("co", flat_list)
4085 for point in spline.bezier_points:
4086 point.handle_left_type="AUTO"
4087 point.handle_right_type="AUTO"
4089 return CurveObject
4091 class BsurfPreferences(AddonPreferences):
4092 # this must match the addon name, use '__package__'
4093 # when defining this in a submodule of a python package.
4094 bl_idname = __name__
4096 category: StringProperty(
4097 name="Tab Category",
4098 description="Choose a name for the category of the panel",
4099 default="Edit",
4100 update=update_panel
4103 def draw(self, context):
4104 layout = self.layout
4106 row = layout.row()
4107 col = row.column()
4108 col.label(text="Tab Category:")
4109 col.prop(self, "category", text="")
4112 # Properties
4113 class BsurfacesProps(PropertyGroup):
4114 SURFSK_use_annotation: BoolProperty(
4115 name="Use Annotation",
4116 default=True
4118 SURFSK_edges_U: IntProperty(
4119 name="Cross",
4120 description="Number of face-loops crossing the strokes",
4121 default=5,
4122 min=1,
4123 max=200
4125 SURFSK_edges_V: IntProperty(
4126 name="Follow",
4127 description="Number of face-loops following the strokes",
4128 default=1,
4129 min=1,
4130 max=200
4132 SURFSK_cyclic_cross: BoolProperty(
4133 name="Cyclic Cross",
4134 description="Make cyclic the face-loops crossing the strokes",
4135 default=False
4137 SURFSK_cyclic_follow: BoolProperty(
4138 name="Cyclic Follow",
4139 description="Make cyclic the face-loops following the strokes",
4140 default=False
4142 SURFSK_keep_strokes: BoolProperty(
4143 name="Keep strokes",
4144 description="Keeps the sketched strokes or curves after adding the surface",
4145 default=False
4147 SURFSK_automatic_join: BoolProperty(
4148 name="Automatic join",
4149 description="Join automatically vertices of either surfaces "
4150 "generated by crosshatching, or from the borders of closed shapes",
4151 default=True
4153 SURFSK_loops_on_strokes: BoolProperty(
4154 name="Loops on strokes",
4155 description="Make the loops match the paths of the strokes",
4156 default=True
4158 SURFSK_precision: IntProperty(
4159 name="Precision",
4160 description="Precision level of the surface calculation",
4161 default=2,
4162 min=1,
4163 max=100
4165 SURFSK_object_with_retopology: PointerProperty(
4166 name="Retopology",
4167 type=bpy.types.Object
4169 SURFSK_object_with_strokes: PointerProperty(
4170 name="Strokes",
4171 type=bpy.types.Object
4174 classes = (
4175 GPENCIL_OT_SURFSK_init,
4176 GPENCIL_OT_SURFSK_add_surface,
4177 GPENCIL_OT_SURFSK_edit_surface,
4178 GPENCIL_OT_SURFSK_add_strokes,
4179 GPENCIL_OT_SURFSK_edit_strokes,
4180 GPENCIL_OT_SURFSK_add_annotation,
4181 CURVE_OT_SURFSK_reorder_splines,
4182 CURVE_OT_SURFSK_first_points,
4183 BsurfPreferences,
4184 BsurfacesProps
4187 def register():
4188 for cls in classes:
4189 bpy.utils.register_class(cls)
4191 bpy.types.Scene.bsurfaces = PointerProperty(type=BsurfacesProps)
4192 update_panel(None, bpy.context)
4194 def unregister():
4195 for cls in classes:
4196 bpy.utils.unregister_class(cls)
4198 del bpy.types.Scene.bsurfaces
4200 if __name__ == "__main__":
4201 register()