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
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 #####
21 "name": "Bsurfaces GPL Edition",
22 "author": "Eclectiel, Spivak Vladimir(cwolf3d)",
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",
34 from bpy_extras
import object_utils
37 from mathutils
import Matrix
, Vector
38 from mathutils
.geometry
import (
47 from bpy
.props
import (
55 from bpy
.types
import (
63 class VIEW3D_PT_tools_SURFSK_mesh(Panel
):
64 bl_space_type
= 'VIEW_3D'
67 #bl_context = "mesh_edit"
68 bl_label
= "Bsurfaces"
70 def draw(self
, context
):
72 scn
= context
.scene
.bsurfaces
74 col
= layout
.column(align
=True)
77 col
.operator("gpencil.surfsk_init", text
="Initialize")
78 col
.prop(scn
, "SURFSK_object_with_retopology")
79 col
.row().prop(scn
, "SURFSK_guide", expand
=True)
80 if not scn
.SURFSK_guide
== 'Annotation':
81 col
.prop(scn
, "SURFSK_object_with_strokes")
83 props
= col
.operator("gpencil.surfsk_add_surface", text
="Add Surface")
84 col
.operator("gpencil.surfsk_edit_surface", text
="Edit Surface")
85 if scn
.SURFSK_guide
== 'GPencil':
86 col
.operator("gpencil.surfsk_add_strokes", text
="Add Strokes")
87 col
.operator("gpencil.surfsk_edit_strokes", text
="Edit Strokes")
88 if scn
.SURFSK_guide
== 'Annotation':
89 col
.operator("gpencil.surfsk_add_annotation", text
="Add Annotation")
91 col
.label(text
="Initial settings:")
92 col
.prop(scn
, "SURFSK_edges_U")
93 col
.prop(scn
, "SURFSK_edges_V")
94 col
.prop(scn
, "SURFSK_cyclic_cross")
95 col
.prop(scn
, "SURFSK_cyclic_follow")
96 col
.prop(scn
, "SURFSK_loops_on_strokes")
97 col
.prop(scn
, "SURFSK_automatic_join")
98 col
.prop(scn
, "SURFSK_keep_strokes")
100 class VIEW3D_PT_tools_SURFSK_curve(Panel
):
101 bl_space_type
= 'VIEW_3D'
102 bl_region_type
= 'UI'
103 bl_context
= "curve_edit"
105 bl_label
= "Bsurfaces"
108 def poll(cls
, context
):
109 return context
.active_object
111 def draw(self
, context
):
114 col
= layout
.column(align
=True)
117 col
.operator("curve.surfsk_first_points", text
="Set First Points")
118 col
.operator("curve.switch_direction", text
="Switch Direction")
119 col
.operator("curve.surfsk_reorder_splines", text
="Reorder Splines")
122 # Returns the type of strokes used
123 def get_strokes_type(context
):
127 # Check if they are grease pencil
128 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Annotation':
130 strokes
= bpy
.data
.grease_pencils
[0].layers
.active
.active_frame
.strokes
132 strokes_num
= len(strokes
)
135 strokes_type
= "GP_ANNOTATION"
140 gpencil
= bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
141 strokes
= gpencil
.data
.layers
.active
.active_frame
.strokes
143 strokes_num
= len(strokes
)
146 strokes_type
= "GP_STROKES"
150 # Check if they are mesh
151 main_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_retopology
152 total_vert_sel
= len([v
for v
in main_object
.data
.vertices
if v
.select
])
154 # Check if they are curves, if there aren't grease pencil strokes
155 if strokes_type
== "":
157 ob
= bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
158 if ob
.type == "CURVE":
159 strokes_type
= "EXTERNAL_CURVE"
160 strokes_num
= len(ob
.data
.splines
)
162 # Check if there is any non-bezier spline
163 for i
in range(len(ob
.data
.splines
)):
164 if ob
.data
.splines
[i
].type != "BEZIER":
165 strokes_type
= "CURVE_WITH_NON_BEZIER_SPLINES"
169 strokes_type
= "EXTERNAL_NO_CURVE"
173 # Check if there is a single stroke without any selection in the object
174 if strokes_num
== 1 and total_vert_sel
== 0:
175 if strokes_type
== "EXTERNAL_CURVE":
176 strokes_type
= "SINGLE_CURVE_STROKE_NO_SELECTION"
177 elif strokes_type
== "GP_STROKES":
178 strokes_type
= "SINGLE_GP_STROKE_NO_SELECTION"
180 if strokes_num
== 0 and total_vert_sel
> 0:
181 strokes_type
= "SELECTION_ALONE"
183 if strokes_type
== "":
184 strokes_type
= "NO_STROKES"
189 # Surface generator operator
190 class GPENCIL_OT_SURFSK_add_surface(Operator
):
191 bl_idname
= "gpencil.surfsk_add_surface"
192 bl_label
= "Bsurfaces add surface"
193 bl_description
= "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
194 bl_options
= {'REGISTER', 'UNDO'}
196 is_fill_faces
: BoolProperty(
199 selection_U_exists
: BoolProperty(
202 selection_V_exists
: BoolProperty(
205 selection_U2_exists
: BoolProperty(
208 selection_V2_exists
: BoolProperty(
211 selection_V_is_closed
: BoolProperty(
214 selection_U_is_closed
: BoolProperty(
217 selection_V2_is_closed
: BoolProperty(
220 selection_U2_is_closed
: BoolProperty(
224 edges_U
: IntProperty(
226 description
="Number of face-loops crossing the strokes",
231 edges_V
: IntProperty(
233 description
="Number of face-loops following the strokes",
238 cyclic_cross
: BoolProperty(
240 description
="Make cyclic the face-loops crossing the strokes",
243 cyclic_follow
: BoolProperty(
244 name
="Cyclic Follow",
245 description
="Make cyclic the face-loops following the strokes",
248 loops_on_strokes
: BoolProperty(
249 name
="Loops on strokes",
250 description
="Make the loops match the paths of the strokes",
253 automatic_join
: BoolProperty(
254 name
="Automatic join",
255 description
="Join automatically vertices of either surfaces generated "
256 "by crosshatching, or from the borders of closed shapes",
259 join_stretch_factor
: FloatProperty(
261 description
="Amount of stretching or shrinking allowed for "
262 "edges when joining vertices automatically",
268 keep_strokes
: BoolProperty(
270 description
="Keeps the sketched strokes or curves after adding the surface",
273 strokes_type
: StringProperty()
274 initial_global_undo_state
: BoolProperty()
277 def draw(self
, context
):
279 col
= layout
.column(align
=True)
282 if not self
.is_fill_faces
:
284 if not self
.is_crosshatch
:
285 if not self
.selection_U_exists
:
286 col
.prop(self
, "edges_U")
289 if not self
.selection_V_exists
:
290 col
.prop(self
, "edges_V")
295 if not self
.selection_U_exists
:
297 (self
.selection_V_exists
and not self
.selection_V_is_closed
) or
298 (self
.selection_V2_exists
and not self
.selection_V2_is_closed
)
300 col
.prop(self
, "cyclic_cross")
302 if not self
.selection_V_exists
:
304 (self
.selection_U_exists
and not self
.selection_U_is_closed
) or
305 (self
.selection_U2_exists
and not self
.selection_U2_is_closed
)
307 col
.prop(self
, "cyclic_follow")
309 col
.prop(self
, "loops_on_strokes")
311 col
.prop(self
, "automatic_join")
313 if self
.automatic_join
:
317 col
.prop(self
, "join_stretch_factor")
319 col
.prop(self
, "keep_strokes")
321 # Get an ordered list of a chain of vertices
322 def get_ordered_verts(self
, ob
, all_selected_edges_idx
, all_selected_verts_idx
,
323 first_vert_idx
, middle_vertex_idx
, closing_vert_idx
):
324 # Order selected vertices.
326 if closing_vert_idx
is not None:
327 verts_ordered
.append(ob
.data
.vertices
[closing_vert_idx
])
329 verts_ordered
.append(ob
.data
.vertices
[first_vert_idx
])
330 prev_v
= first_vert_idx
334 edges_non_matched
= 0
335 for i
in all_selected_edges_idx
:
336 if ob
.data
.edges
[i
] != prev_ed
and ob
.data
.edges
[i
].vertices
[0] == prev_v
and \
337 ob
.data
.edges
[i
].vertices
[1] in all_selected_verts_idx
:
339 verts_ordered
.append(ob
.data
.vertices
[ob
.data
.edges
[i
].vertices
[1]])
340 prev_v
= ob
.data
.edges
[i
].vertices
[1]
341 prev_ed
= ob
.data
.edges
[i
]
342 elif ob
.data
.edges
[i
] != prev_ed
and ob
.data
.edges
[i
].vertices
[1] == prev_v
and \
343 ob
.data
.edges
[i
].vertices
[0] in all_selected_verts_idx
:
345 verts_ordered
.append(ob
.data
.vertices
[ob
.data
.edges
[i
].vertices
[0]])
346 prev_v
= ob
.data
.edges
[i
].vertices
[0]
347 prev_ed
= ob
.data
.edges
[i
]
349 edges_non_matched
+= 1
351 if edges_non_matched
== len(all_selected_edges_idx
):
357 if closing_vert_idx
is not None:
358 verts_ordered
.append(ob
.data
.vertices
[closing_vert_idx
])
360 if middle_vertex_idx
is not None:
361 verts_ordered
.append(ob
.data
.vertices
[middle_vertex_idx
])
362 verts_ordered
.reverse()
364 return tuple(verts_ordered
)
366 # Calculates length of a chain of points.
367 def get_chain_length(self
, object, verts_ordered
):
368 matrix
= object.matrix_world
371 edges_lengths_sum
= 0
372 for i
in range(0, len(verts_ordered
)):
374 prev_v_co
= matrix
@ verts_ordered
[i
].co
376 v_co
= matrix
@ verts_ordered
[i
].co
378 v_difs
= [prev_v_co
[0] - v_co
[0], prev_v_co
[1] - v_co
[1], prev_v_co
[2] - v_co
[2]]
379 edge_length
= abs(sqrt(v_difs
[0] * v_difs
[0] + v_difs
[1] * v_difs
[1] + v_difs
[2] * v_difs
[2]))
381 edges_lengths
.append(edge_length
)
382 edges_lengths_sum
+= edge_length
386 return edges_lengths
, edges_lengths_sum
388 # Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
389 def get_edges_proportions(self
, edges_lengths
, edges_lengths_sum
, use_boundaries
, fixed_edges_num
):
390 edges_proportions
= []
393 for l
in edges_lengths
:
394 edges_proportions
.append(l
/ edges_lengths_sum
)
398 for n
in range(0, fixed_edges_num
):
399 edges_proportions
.append(1 / fixed_edges_num
)
402 return edges_proportions
404 # Calculates the angle between two pairs of points in space
405 def orientation_difference(self
, points_A_co
, points_B_co
):
406 # each parameter should be a list with two elements,
407 # and each element should be a x,y,z coordinate
408 vec_A
= points_A_co
[0] - points_A_co
[1]
409 vec_B
= points_B_co
[0] - points_B_co
[1]
411 angle
= vec_A
.angle(vec_B
)
414 angle
= abs(angle
- pi
)
418 # Calculate the which vert of verts_idx list is the nearest one
419 # to the point_co coordinates, and the distance
420 def shortest_distance(self
, object, point_co
, verts_idx
):
421 matrix
= object.matrix_world
423 for i
in range(0, len(verts_idx
)):
424 dist
= (point_co
- matrix
@ object.data
.vertices
[verts_idx
[i
]].co
).length
427 nearest_vert_idx
= verts_idx
[i
]
432 nearest_vert_idx
= verts_idx
[i
]
435 return nearest_vert_idx
, shortest_dist
437 # Returns the index of the opposite vert tip in a chain, given a vert tip index
438 # as parameter, and a multidimentional list with all pairs of tips
439 def opposite_tip(self
, vert_tip_idx
, all_chains_tips_idx
):
440 opposite_vert_tip_idx
= None
441 for i
in range(0, len(all_chains_tips_idx
)):
442 if vert_tip_idx
== all_chains_tips_idx
[i
][0]:
443 opposite_vert_tip_idx
= all_chains_tips_idx
[i
][1]
444 if vert_tip_idx
== all_chains_tips_idx
[i
][1]:
445 opposite_vert_tip_idx
= all_chains_tips_idx
[i
][0]
447 return opposite_vert_tip_idx
449 # Simplifies a spline and returns the new points coordinates
450 def simplify_spline(self
, spline_coords
, segments_num
):
451 simplified_spline
= []
452 points_between_segments
= round(len(spline_coords
) / segments_num
)
454 simplified_spline
.append(spline_coords
[0])
455 for i
in range(1, segments_num
):
456 simplified_spline
.append(spline_coords
[i
* points_between_segments
])
458 simplified_spline
.append(spline_coords
[len(spline_coords
) - 1])
460 return simplified_spline
462 # Cleans up the scene and gets it the same it was at the beginning,
463 # in case the script is interrupted in the middle of the execution
464 def cleanup_on_interruption(self
):
465 # If the original strokes curve comes from conversion
466 # from grease pencil and wasn't made by hand, delete it
467 if not self
.using_external_curves
:
469 bpy
.ops
.object.delete({"selected_objects": [self
.original_curve
]})
473 #bpy.ops.object.delete({"selected_objects": [self.main_object]})
475 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
476 self
.original_curve
.select_set(True)
477 self
.main_object
.select_set(True)
478 bpy
.context
.view_layer
.objects
.active
= self
.main_object
480 #bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
481 bpy
.ops
.object.mode_set(mode
='OBJECT')
483 # Returns a list with the coords of the points distributed over the splines
484 # passed to this method according to the proportions parameter
485 def distribute_pts(self
, surface_splines
, proportions
):
487 # Calculate the length of each final surface spline
488 surface_splines_lengths
= []
489 surface_splines_parsed
= []
491 for sp_idx
in range(0, len(surface_splines
)):
492 # Calculate spline length
493 surface_splines_lengths
.append(0)
495 for i
in range(0, len(surface_splines
[sp_idx
].bezier_points
)):
497 prev_p
= surface_splines
[sp_idx
].bezier_points
[i
]
499 p
= surface_splines
[sp_idx
].bezier_points
[i
]
500 edge_length
= (prev_p
.co
- p
.co
).length
501 surface_splines_lengths
[sp_idx
] += edge_length
505 # Calculate vertex positions with appropriate edge proportions, and ordered, for each spline
506 for sp_idx
in range(0, len(surface_splines
)):
507 surface_splines_parsed
.append([])
508 surface_splines_parsed
[sp_idx
].append(surface_splines
[sp_idx
].bezier_points
[0].co
)
510 prev_p_co
= surface_splines
[sp_idx
].bezier_points
[0].co
513 for prop_idx
in range(len(proportions
) - 1):
514 target_length
= surface_splines_lengths
[sp_idx
] * proportions
[prop_idx
]
515 partial_segment_length
= 0
519 # if not it'll pass the p_idx as an index below and crash
520 if p_idx
< len(surface_splines
[sp_idx
].bezier_points
):
521 p_co
= surface_splines
[sp_idx
].bezier_points
[p_idx
].co
522 new_dist
= (prev_p_co
- p_co
).length
524 # The new distance that could have the partial segment if
525 # it is still shorter than the target length
526 potential_segment_length
= partial_segment_length
+ new_dist
528 # If the potential is still shorter, keep adding
529 if potential_segment_length
< target_length
:
530 partial_segment_length
= potential_segment_length
535 # If the potential is longer than the target, calculate the target
536 # (a point between the last two points), and assign
537 elif potential_segment_length
> target_length
:
538 remaining_dist
= target_length
- partial_segment_length
539 vec
= p_co
- prev_p_co
541 intermediate_co
= prev_p_co
+ (vec
* remaining_dist
)
543 surface_splines_parsed
[sp_idx
].append(intermediate_co
)
545 partial_segment_length
+= remaining_dist
546 prev_p_co
= intermediate_co
550 # If the potential is equal to the target, assign
551 elif potential_segment_length
== target_length
:
552 surface_splines_parsed
[sp_idx
].append(p_co
)
560 # last point of the spline
561 surface_splines_parsed
[sp_idx
].append(
562 surface_splines
[sp_idx
].bezier_points
[len(surface_splines
[sp_idx
].bezier_points
) - 1].co
565 return surface_splines_parsed
567 # Counts the number of faces that belong to each edge
568 def edge_face_count(self
, ob
):
569 ed_keys_count_dict
= {}
571 for face
in ob
.data
.polygons
:
572 for ed_keys
in face
.edge_keys
:
573 if ed_keys
not in ed_keys_count_dict
:
574 ed_keys_count_dict
[ed_keys
] = 1
576 ed_keys_count_dict
[ed_keys
] += 1
579 for i
in range(len(ob
.data
.edges
)):
580 edge_face_count
.append(0)
582 for i
in range(len(ob
.data
.edges
)):
583 ed
= ob
.data
.edges
[i
]
588 if (v1
, v2
) in ed_keys_count_dict
:
589 edge_face_count
[i
] = ed_keys_count_dict
[(v1
, v2
)]
590 elif (v2
, v1
) in ed_keys_count_dict
:
591 edge_face_count
[i
] = ed_keys_count_dict
[(v2
, v1
)]
593 return edge_face_count
595 # Fills with faces all the selected vertices which form empty triangles or quads
596 def fill_with_faces(self
, object):
597 all_selected_verts_count
= self
.main_object_selected_verts_count
599 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
601 # Calculate average length of selected edges
602 all_selected_verts
= []
603 original_sel_edges_count
= 0
604 for ed
in object.data
.edges
:
605 if object.data
.vertices
[ed
.vertices
[0]].select
and object.data
.vertices
[ed
.vertices
[1]].select
:
607 coords
.append(object.data
.vertices
[ed
.vertices
[0]].co
)
608 coords
.append(object.data
.vertices
[ed
.vertices
[1]].co
)
610 original_sel_edges_count
+= 1
612 if not ed
.vertices
[0] in all_selected_verts
:
613 all_selected_verts
.append(ed
.vertices
[0])
615 if not ed
.vertices
[1] in all_selected_verts
:
616 all_selected_verts
.append(ed
.vertices
[1])
618 tuple(all_selected_verts
)
620 # Check if there is any edge selected. If not, interrupt the script
621 if original_sel_edges_count
== 0 and all_selected_verts_count
> 0:
624 # Get all edges connected to selected verts
625 all_edges_around_sel_verts
= []
626 edges_connected_to_sel_verts
= {}
627 verts_connected_to_every_vert
= {}
628 for ed_idx
in range(len(object.data
.edges
)):
629 ed
= object.data
.edges
[ed_idx
]
632 if ed
.vertices
[0] in all_selected_verts
:
633 if not ed
.vertices
[0] in edges_connected_to_sel_verts
:
634 edges_connected_to_sel_verts
[ed
.vertices
[0]] = []
636 edges_connected_to_sel_verts
[ed
.vertices
[0]].append(ed_idx
)
639 if ed
.vertices
[1] in all_selected_verts
:
640 if not ed
.vertices
[1] in edges_connected_to_sel_verts
:
641 edges_connected_to_sel_verts
[ed
.vertices
[1]] = []
643 edges_connected_to_sel_verts
[ed
.vertices
[1]].append(ed_idx
)
646 if include_edge
is True:
647 all_edges_around_sel_verts
.append(ed_idx
)
649 # Get all connected verts to each vert
650 if not ed
.vertices
[0] in verts_connected_to_every_vert
:
651 verts_connected_to_every_vert
[ed
.vertices
[0]] = []
653 if not ed
.vertices
[1] in verts_connected_to_every_vert
:
654 verts_connected_to_every_vert
[ed
.vertices
[1]] = []
656 verts_connected_to_every_vert
[ed
.vertices
[0]].append(ed
.vertices
[1])
657 verts_connected_to_every_vert
[ed
.vertices
[1]].append(ed
.vertices
[0])
659 # Get all verts connected to faces
660 all_verts_part_of_faces
= []
661 all_edges_faces_count
= []
662 all_edges_faces_count
+= self
.edge_face_count(object)
664 # Get only the selected edges that have faces attached.
665 count_faces_of_edges_around_sel_verts
= {}
666 selected_verts_with_faces
= []
667 for ed_idx
in all_edges_around_sel_verts
:
668 count_faces_of_edges_around_sel_verts
[ed_idx
] = all_edges_faces_count
[ed_idx
]
670 if all_edges_faces_count
[ed_idx
] > 0:
671 ed
= object.data
.edges
[ed_idx
]
673 if not ed
.vertices
[0] in selected_verts_with_faces
:
674 selected_verts_with_faces
.append(ed
.vertices
[0])
676 if not ed
.vertices
[1] in selected_verts_with_faces
:
677 selected_verts_with_faces
.append(ed
.vertices
[1])
679 all_verts_part_of_faces
.append(ed
.vertices
[0])
680 all_verts_part_of_faces
.append(ed
.vertices
[1])
682 tuple(selected_verts_with_faces
)
684 # Discard unneeded verts from calculations
685 participating_verts
= []
687 for v_idx
in all_selected_verts
:
688 vert_has_edges_with_one_face
= False
690 # Check if the actual vert has at least one edge connected to only one face
691 for ed_idx
in edges_connected_to_sel_verts
[v_idx
]:
692 if count_faces_of_edges_around_sel_verts
[ed_idx
] == 1:
693 vert_has_edges_with_one_face
= True
695 # If the vert has two or less edges connected and the vert is not part of any face.
696 # Or the vert is part of any face and at least one of
697 # the connected edges has only one face attached to it.
698 if (len(edges_connected_to_sel_verts
[v_idx
]) == 2 and
699 v_idx
not in all_verts_part_of_faces
) or \
700 len(edges_connected_to_sel_verts
[v_idx
]) == 1 or \
701 (v_idx
in all_verts_part_of_faces
and
702 vert_has_edges_with_one_face
):
704 participating_verts
.append(v_idx
)
706 if v_idx
not in all_verts_part_of_faces
:
707 movable_verts
.append(v_idx
)
709 # Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
710 for mv_idx
in movable_verts
:
712 mv_connected_verts
= verts_connected_to_every_vert
[mv_idx
]
714 for actual_v_idx
in all_selected_verts
:
715 count_shared_neighbors
= 0
718 for mv_conn_v_idx
in mv_connected_verts
:
719 if mv_idx
!= actual_v_idx
:
720 if mv_conn_v_idx
in verts_connected_to_every_vert
[actual_v_idx
] and \
721 mv_conn_v_idx
not in checked_verts
:
722 count_shared_neighbors
+= 1
723 checked_verts
.append(mv_conn_v_idx
)
725 if actual_v_idx
in mv_connected_verts
:
729 if count_shared_neighbors
== 2:
737 movable_verts
.remove(mv_idx
)
739 # Calculate merge distance for participating verts
740 shortest_edge_length
= None
741 for ed
in object.data
.edges
:
742 if ed
.vertices
[0] in movable_verts
and ed
.vertices
[1] in movable_verts
:
743 v1
= object.data
.vertices
[ed
.vertices
[0]]
744 v2
= object.data
.vertices
[ed
.vertices
[1]]
746 length
= (v1
.co
- v2
.co
).length
748 if shortest_edge_length
is None:
749 shortest_edge_length
= length
751 if length
< shortest_edge_length
:
752 shortest_edge_length
= length
754 if shortest_edge_length
is not None:
755 edges_merge_distance
= shortest_edge_length
* 0.5
757 edges_merge_distance
= 0
759 # Get together the verts near enough. They will be merged later
761 remaining_verts
+= participating_verts
762 for v1_idx
in participating_verts
:
763 if v1_idx
in remaining_verts
and v1_idx
in movable_verts
:
765 coords_verts_to_merge
= {}
767 verts_to_merge
.append(v1_idx
)
769 v1_co
= object.data
.vertices
[v1_idx
].co
770 coords_verts_to_merge
[v1_idx
] = (v1_co
[0], v1_co
[1], v1_co
[2])
772 for v2_idx
in remaining_verts
:
774 v2_co
= object.data
.vertices
[v2_idx
].co
776 dist
= (v1_co
- v2_co
).length
778 if dist
<= edges_merge_distance
: # Add the verts which are near enough
779 verts_to_merge
.append(v2_idx
)
781 coords_verts_to_merge
[v2_idx
] = (v2_co
[0], v2_co
[1], v2_co
[2])
783 for vm_idx
in verts_to_merge
:
784 remaining_verts
.remove(vm_idx
)
786 if len(verts_to_merge
) > 1:
787 # Calculate middle point of the verts to merge.
791 movable_verts_to_merge_count
= 0
792 for i
in range(len(verts_to_merge
)):
793 if verts_to_merge
[i
] in movable_verts
:
794 v_co
= object.data
.vertices
[verts_to_merge
[i
]].co
800 movable_verts_to_merge_count
+= 1
803 sum_x_co
/ movable_verts_to_merge_count
,
804 sum_y_co
/ movable_verts_to_merge_count
,
805 sum_z_co
/ movable_verts_to_merge_count
808 # Check if any vert to be merged is not movable
810 are_verts_not_movable
= False
811 verts_not_movable
= []
812 for v_merge_idx
in verts_to_merge
:
813 if v_merge_idx
in participating_verts
and v_merge_idx
not in movable_verts
:
814 are_verts_not_movable
= True
815 verts_not_movable
.append(v_merge_idx
)
817 if are_verts_not_movable
:
818 # Get the vert connected to faces, that is nearest to
819 # the middle point of the movable verts
821 for vcf_idx
in verts_not_movable
:
822 dist
= abs((object.data
.vertices
[vcf_idx
].co
-
823 Vector(middle_point_co
)).length
)
825 if shortest_dist
is None:
827 nearest_vert_idx
= vcf_idx
829 if dist
< shortest_dist
:
831 nearest_vert_idx
= vcf_idx
833 coords
= object.data
.vertices
[nearest_vert_idx
].co
834 target_point_co
= [coords
[0], coords
[1], coords
[2]]
836 target_point_co
= middle_point_co
838 # Move verts to merge to the middle position
839 for v_merge_idx
in verts_to_merge
:
840 if v_merge_idx
in movable_verts
: # Only move the verts that are not part of faces
841 object.data
.vertices
[v_merge_idx
].co
[0] = target_point_co
[0]
842 object.data
.vertices
[v_merge_idx
].co
[1] = target_point_co
[1]
843 object.data
.vertices
[v_merge_idx
].co
[2] = target_point_co
[2]
845 # Perform "Remove Doubles" to weld all the disconnected verts
846 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
847 bpy
.ops
.mesh
.remove_doubles(threshold
=0.0001)
849 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
851 # Get all the definitive selected edges, after weldding
853 edges_per_vert
= {} # Number of faces of each selected edge
854 for ed
in object.data
.edges
:
855 if object.data
.vertices
[ed
.vertices
[0]].select
and object.data
.vertices
[ed
.vertices
[1]].select
:
856 selected_edges
.append(ed
.index
)
858 # Save all the edges that belong to each vertex.
859 if not ed
.vertices
[0] in edges_per_vert
:
860 edges_per_vert
[ed
.vertices
[0]] = []
862 if not ed
.vertices
[1] in edges_per_vert
:
863 edges_per_vert
[ed
.vertices
[1]] = []
865 edges_per_vert
[ed
.vertices
[0]].append(ed
.index
)
866 edges_per_vert
[ed
.vertices
[1]].append(ed
.index
)
868 # Check if all the edges connected to each vert have two faces attached to them.
869 # To discard them later and make calculations faster
871 a
+= self
.edge_face_count(object)
873 verts_surrounded_by_faces
= {}
874 for v_idx
in edges_per_vert
:
875 edges
= edges_per_vert
[v_idx
]
876 edges_with_two_faces_count
= 0
878 for ed_idx
in edges_per_vert
[v_idx
]:
880 edges_with_two_faces_count
+= 1
882 if edges_with_two_faces_count
== len(edges_per_vert
[v_idx
]):
883 verts_surrounded_by_faces
[v_idx
] = True
885 verts_surrounded_by_faces
[v_idx
] = False
887 # Get all the selected vertices
888 selected_verts_idx
= []
889 for v
in object.data
.vertices
:
891 selected_verts_idx
.append(v
.index
)
893 # Get all the faces of the object
894 all_object_faces_verts_idx
= []
895 for face
in object.data
.polygons
:
897 face_verts
.append(face
.vertices
[0])
898 face_verts
.append(face
.vertices
[1])
899 face_verts
.append(face
.vertices
[2])
901 if len(face
.vertices
) == 4:
902 face_verts
.append(face
.vertices
[3])
904 all_object_faces_verts_idx
.append(face_verts
)
906 # Deselect all vertices
907 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
908 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
909 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
911 # Make a dictionary with the verts related to each vert
912 related_key_verts
= {}
913 for ed_idx
in selected_edges
:
914 ed
= object.data
.edges
[ed_idx
]
916 if not verts_surrounded_by_faces
[ed
.vertices
[0]]:
917 if not ed
.vertices
[0] in related_key_verts
:
918 related_key_verts
[ed
.vertices
[0]] = []
920 if not ed
.vertices
[1] in related_key_verts
[ed
.vertices
[0]]:
921 related_key_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
923 if not verts_surrounded_by_faces
[ed
.vertices
[1]]:
924 if not ed
.vertices
[1] in related_key_verts
:
925 related_key_verts
[ed
.vertices
[1]] = []
927 if not ed
.vertices
[0] in related_key_verts
[ed
.vertices
[1]]:
928 related_key_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
930 # Get groups of verts forming each face
932 for v1
in related_key_verts
: # verts-1 ....
933 for v2
in related_key_verts
: # verts-2
935 related_verts_in_common
= []
938 for rel_v1
in related_key_verts
[v1
]:
939 # Check if related verts of verts-1 are related verts of verts-2
940 if rel_v1
in related_key_verts
[v2
]:
941 related_verts_in_common
.append(rel_v1
)
943 if v2
in related_key_verts
[v1
]:
946 if v1
in related_key_verts
[v2
]:
949 repeated_face
= False
950 # If two verts have two related verts in common, they form a quad
951 if len(related_verts_in_common
) == 2:
952 # Check if the face is already saved
953 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
955 for f_verts
in all_faces_to_check_idx
:
958 if len(f_verts
) == 4:
963 if related_verts_in_common
[0] in f_verts
:
965 if related_verts_in_common
[1] in f_verts
:
968 if repeated_verts
== len(f_verts
):
972 if not repeated_face
:
973 faces_verts_idx
.append(
974 [v1
, related_verts_in_common
[0], v2
, related_verts_in_common
[1]]
977 # If Two verts have one related vert in common and
978 # they are related to each other, they form a triangle
979 elif v2_in_rel_v1
and v1_in_rel_v2
and len(related_verts_in_common
) == 1:
980 # Check if the face is already saved.
981 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
983 for f_verts
in all_faces_to_check_idx
:
986 if len(f_verts
) == 3:
991 if related_verts_in_common
[0] in f_verts
:
994 if repeated_verts
== len(f_verts
):
998 if not repeated_face
:
999 faces_verts_idx
.append([v1
, related_verts_in_common
[0], v2
])
1001 # Keep only the faces that don't overlap by ignoring quads
1002 # that overlap with two adjacent triangles
1003 faces_to_not_include_idx
= [] # Indices of faces_verts_idx to eliminate
1004 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
1005 for i
in range(len(faces_verts_idx
)):
1006 for t
in range(len(all_faces_to_check_idx
)):
1010 if len(faces_verts_idx
[i
]) == 4 and len(all_faces_to_check_idx
[t
]) == 3:
1011 for v_idx
in all_faces_to_check_idx
[t
]:
1012 if v_idx
in faces_verts_idx
[i
]:
1013 verts_in_common
+= 1
1014 # If it doesn't have all it's vertices repeated in the other face
1015 if verts_in_common
== 3:
1016 if i
not in faces_to_not_include_idx
:
1017 faces_to_not_include_idx
.append(i
)
1019 # Build faces discarding the ones in faces_to_not_include
1024 num_faces_created
= 0
1025 for i
in range(len(faces_verts_idx
)):
1026 if i
not in faces_to_not_include_idx
:
1027 bm
.faces
.new([bm
.verts
[v
] for v
in faces_verts_idx
[i
]])
1029 num_faces_created
+= 1
1034 for v_idx
in selected_verts_idx
:
1035 self
.main_object
.data
.vertices
[v_idx
].select
= True
1037 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
1038 bpy
.ops
.mesh
.normals_make_consistent(inside
=False)
1039 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
1041 return num_faces_created
1043 # Crosshatch skinning
1044 def crosshatch_surface_invoke(self
, ob_original_splines
):
1045 self
.is_crosshatch
= False
1046 self
.crosshatch_merge_distance
= 0
1048 objects_to_delete
= [] # duplicated strokes to be deleted.
1050 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1051 # (without this the surface verts merging with the main object doesn't work well)
1052 self
.modifiers_prev_viewport_state
= []
1053 if len(self
.main_object
.modifiers
) > 0:
1054 for m_idx
in range(len(self
.main_object
.modifiers
)):
1055 self
.modifiers_prev_viewport_state
.append(
1056 self
.main_object
.modifiers
[m_idx
].show_viewport
1058 self
.main_object
.modifiers
[m_idx
].show_viewport
= False
1060 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1061 ob_original_splines
.select_set(True)
1062 bpy
.context
.view_layer
.objects
.active
= ob_original_splines
1064 if len(ob_original_splines
.data
.splines
) >= 2:
1065 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1066 ob_splines
= bpy
.context
.object
1067 ob_splines
.name
= "SURFSKIO_NE_STR"
1069 # Get estimative merge distance (sum up the distances from the first point to
1070 # all other points, then average them and then divide them)
1071 first_point_dist_sum
= 0
1074 coords_first_pt
= ob_splines
.data
.splines
[0].bezier_points
[0].co
1075 for i
in range(len(ob_splines
.data
.splines
)):
1076 sp
= ob_splines
.data
.splines
[i
]
1078 if coords_first_pt
!= sp
.bezier_points
[0].co
:
1079 first_dist
= (coords_first_pt
- sp
.bezier_points
[0].co
).length
1081 if coords_first_pt
!= sp
.bezier_points
[len(sp
.bezier_points
) - 1].co
:
1082 second_dist
= (coords_first_pt
- sp
.bezier_points
[len(sp
.bezier_points
) - 1].co
).length
1084 first_point_dist_sum
+= first_dist
+ second_dist
1088 shortest_dist
= first_dist
1089 elif second_dist
!= 0:
1090 shortest_dist
= second_dist
1092 if shortest_dist
> first_dist
and first_dist
!= 0:
1093 shortest_dist
= first_dist
1095 if shortest_dist
> second_dist
and second_dist
!= 0:
1096 shortest_dist
= second_dist
1098 self
.crosshatch_merge_distance
= shortest_dist
/ 20
1100 # Recalculation of merge distance
1102 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1104 ob_calc_merge_dist
= bpy
.context
.object
1105 ob_calc_merge_dist
.name
= "SURFSKIO_CALC_TMP"
1107 objects_to_delete
.append(ob_calc_merge_dist
)
1109 # Smooth out strokes a little to improve crosshatch detection
1110 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1111 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1114 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1116 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1117 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1119 # Convert curves into mesh
1120 ob_calc_merge_dist
.data
.resolution_u
= 12
1121 bpy
.ops
.object.convert(target
='MESH', keep_original
=False)
1123 # Find "intersection-nodes"
1124 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1125 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1126 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN',
1127 threshold
=self
.crosshatch_merge_distance
)
1128 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1129 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1131 # Remove verts with less than three edges
1132 verts_edges_count
= {}
1133 for ed
in ob_calc_merge_dist
.data
.edges
:
1136 if v
[0] not in verts_edges_count
:
1137 verts_edges_count
[v
[0]] = 0
1139 if v
[1] not in verts_edges_count
:
1140 verts_edges_count
[v
[1]] = 0
1142 verts_edges_count
[v
[0]] += 1
1143 verts_edges_count
[v
[1]] += 1
1145 nodes_verts_coords
= []
1146 for v_idx
in verts_edges_count
:
1147 v
= ob_calc_merge_dist
.data
.vertices
[v_idx
]
1149 if verts_edges_count
[v_idx
] < 3:
1153 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1154 bpy
.ops
.mesh
.delete('INVOKE_REGION_WIN', type='VERT')
1155 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1157 # Remove doubles to discard very near verts from calculations of distance
1158 bpy
.ops
.mesh
.remove_doubles(
1159 'INVOKE_REGION_WIN',
1160 threshold
=self
.crosshatch_merge_distance
* 4.0
1162 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1164 # Get all coords of the resulting nodes
1165 nodes_verts_coords
= [(v
.co
[0], v
.co
[1], v
.co
[2]) for
1166 v
in ob_calc_merge_dist
.data
.vertices
]
1168 # Check if the strokes are a crosshatch
1169 if len(nodes_verts_coords
) >= 3:
1170 self
.is_crosshatch
= True
1172 shortest_dist
= None
1173 for co_1
in nodes_verts_coords
:
1174 for co_2
in nodes_verts_coords
:
1176 dist
= (Vector(co_1
) - Vector(co_2
)).length
1178 if shortest_dist
is not None:
1179 if dist
< shortest_dist
:
1180 shortest_dist
= dist
1182 shortest_dist
= dist
1184 self
.crosshatch_merge_distance
= shortest_dist
/ 3
1186 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1187 ob_splines
.select_set(True)
1188 bpy
.context
.view_layer
.objects
.active
= ob_splines
1190 # Deselect all points
1191 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1192 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1193 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1195 # Smooth splines in a localized way, to eliminate "saw-teeth"
1196 # like shapes when there are many points
1197 for sp
in ob_splines
.data
.splines
:
1200 angle_limit
= 2 # Degrees
1201 for t
in range(len(sp
.bezier_points
)):
1202 # Because on each iteration it checks the "next two points"
1203 # of the actual. This way it doesn't go out of range
1204 if t
<= len(sp
.bezier_points
) - 3:
1205 p1
= sp
.bezier_points
[t
]
1206 p2
= sp
.bezier_points
[t
+ 1]
1207 p3
= sp
.bezier_points
[t
+ 2]
1209 vec_1
= p1
.co
- p2
.co
1210 vec_2
= p2
.co
- p3
.co
1212 if p2
.co
!= p1
.co
and p2
.co
!= p3
.co
:
1213 angle
= vec_1
.angle(vec_2
)
1214 angle_sum
+= degrees(angle
)
1216 if angle_sum
>= angle_limit
: # If sum of angles is grater than the limit
1217 if (p1
.co
- p2
.co
).length
<= self
.crosshatch_merge_distance
:
1218 p1
.select_control_point
= True
1219 p1
.select_left_handle
= True
1220 p1
.select_right_handle
= True
1222 p2
.select_control_point
= True
1223 p2
.select_left_handle
= True
1224 p2
.select_right_handle
= True
1226 if (p1
.co
- p2
.co
).length
<= self
.crosshatch_merge_distance
:
1227 p3
.select_control_point
= True
1228 p3
.select_left_handle
= True
1229 p3
.select_right_handle
= True
1233 sp
.bezier_points
[0].select_control_point
= False
1234 sp
.bezier_points
[0].select_left_handle
= False
1235 sp
.bezier_points
[0].select_right_handle
= False
1237 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_control_point
= False
1238 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_left_handle
= False
1239 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_right_handle
= False
1241 # Smooth out strokes a little to improve crosshatch detection
1242 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1245 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1247 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1248 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1250 # Simplify the splines
1251 for sp
in ob_splines
.data
.splines
:
1254 sp
.bezier_points
[0].select_control_point
= True
1255 sp
.bezier_points
[0].select_left_handle
= True
1256 sp
.bezier_points
[0].select_right_handle
= True
1258 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_control_point
= True
1259 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_left_handle
= True
1260 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_right_handle
= True
1262 angle_limit
= 15 # Degrees
1263 for t
in range(len(sp
.bezier_points
)):
1264 # Because on each iteration it checks the "next two points"
1265 # of the actual. This way it doesn't go out of range
1266 if t
<= len(sp
.bezier_points
) - 3:
1267 p1
= sp
.bezier_points
[t
]
1268 p2
= sp
.bezier_points
[t
+ 1]
1269 p3
= sp
.bezier_points
[t
+ 2]
1271 vec_1
= p1
.co
- p2
.co
1272 vec_2
= p2
.co
- p3
.co
1274 if p2
.co
!= p1
.co
and p2
.co
!= p3
.co
:
1275 angle
= vec_1
.angle(vec_2
)
1276 angle_sum
+= degrees(angle
)
1277 # If sum of angles is grater than the limit
1278 if angle_sum
>= angle_limit
:
1279 p1
.select_control_point
= True
1280 p1
.select_left_handle
= True
1281 p1
.select_right_handle
= True
1283 p2
.select_control_point
= True
1284 p2
.select_left_handle
= True
1285 p2
.select_right_handle
= True
1287 p3
.select_control_point
= True
1288 p3
.select_left_handle
= True
1289 p3
.select_right_handle
= True
1293 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1294 bpy
.ops
.curve
.select_all(action
='INVERT')
1296 bpy
.ops
.curve
.delete(type='VERT')
1297 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1299 objects_to_delete
.append(ob_splines
)
1301 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1302 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1303 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1305 # Check if the strokes are a crosshatch
1306 if self
.is_crosshatch
:
1307 all_points_coords
= []
1308 for i
in range(len(ob_splines
.data
.splines
)):
1309 all_points_coords
.append([])
1311 all_points_coords
[i
] = [Vector((x
, y
, z
)) for
1312 x
, y
, z
in [bp
.co
for
1313 bp
in ob_splines
.data
.splines
[i
].bezier_points
]]
1315 all_intersections
= []
1316 checked_splines
= []
1317 for i
in range(len(all_points_coords
)):
1319 for t
in range(len(all_points_coords
[i
]) - 1):
1320 bp1_co
= all_points_coords
[i
][t
]
1321 bp2_co
= all_points_coords
[i
][t
+ 1]
1323 for i2
in range(len(all_points_coords
)):
1324 if i
!= i2
and i2
not in checked_splines
:
1325 for t2
in range(len(all_points_coords
[i2
]) - 1):
1326 bp3_co
= all_points_coords
[i2
][t2
]
1327 bp4_co
= all_points_coords
[i2
][t2
+ 1]
1329 intersec_coords
= intersect_line_line(
1330 bp1_co
, bp2_co
, bp3_co
, bp4_co
1332 if intersec_coords
is not None:
1333 dist
= (intersec_coords
[0] - intersec_coords
[1]).length
1335 if dist
<= self
.crosshatch_merge_distance
* 1.5:
1336 temp_co
, percent1
= intersect_point_line(
1337 intersec_coords
[0], bp1_co
, bp2_co
1339 if (percent1
>= -0.02 and percent1
<= 1.02):
1340 temp_co
, percent2
= intersect_point_line(
1341 intersec_coords
[1], bp3_co
, bp4_co
1343 if (percent2
>= -0.02 and percent2
<= 1.02):
1344 # Format: spline index, first point index from
1345 # corresponding segment, percentage from first point of
1346 # actual segment, coords of intersection point
1347 all_intersections
.append(
1349 ob_splines
.matrix_world
@ intersec_coords
[0])
1351 all_intersections
.append(
1353 ob_splines
.matrix_world
@ intersec_coords
[1])
1356 checked_splines
.append(i
)
1357 # Sort list by spline, then by corresponding first point index of segment,
1358 # and then by percentage from first point of segment: elements 0 and 1 respectively
1359 all_intersections
.sort(key
=operator
.itemgetter(0, 1, 2))
1361 self
.crosshatch_strokes_coords
= {}
1362 for i
in range(len(all_intersections
)):
1363 if not all_intersections
[i
][0] in self
.crosshatch_strokes_coords
:
1364 self
.crosshatch_strokes_coords
[all_intersections
[i
][0]] = []
1366 self
.crosshatch_strokes_coords
[all_intersections
[i
][0]].append(
1367 all_intersections
[i
][3]
1368 ) # Save intersection coords
1370 self
.is_crosshatch
= False
1372 # Delete all duplicates
1373 bpy
.ops
.object.delete({"selected_objects": objects_to_delete
})
1375 # If the main object has modifiers, turn their "viewport view status" to
1376 # what it was before the forced deactivation above
1377 if len(self
.main_object
.modifiers
) > 0:
1378 for m_idx
in range(len(self
.main_object
.modifiers
)):
1379 self
.main_object
.modifiers
[m_idx
].show_viewport
= self
.modifiers_prev_viewport_state
[m_idx
]
1383 # Part of the Crosshatch process that is repeated when the operator is tweaked
1384 def crosshatch_surface_execute(self
):
1385 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1386 # (without this the surface verts merging with the main object doesn't work well)
1387 self
.modifiers_prev_viewport_state
= []
1388 if len(self
.main_object
.modifiers
) > 0:
1389 for m_idx
in range(len(self
.main_object
.modifiers
)):
1390 self
.modifiers_prev_viewport_state
.append(self
.main_object
.modifiers
[m_idx
].show_viewport
)
1392 self
.main_object
.modifiers
[m_idx
].show_viewport
= False
1394 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1396 me_name
= "SURFSKIO_STK_TMP"
1397 me
= bpy
.data
.meshes
.new(me_name
)
1399 all_verts_coords
= []
1401 for st_idx
in self
.crosshatch_strokes_coords
:
1402 for co_idx
in range(len(self
.crosshatch_strokes_coords
[st_idx
])):
1403 coords
= self
.crosshatch_strokes_coords
[st_idx
][co_idx
]
1405 all_verts_coords
.append(coords
)
1408 all_edges
.append((len(all_verts_coords
) - 2, len(all_verts_coords
) - 1))
1410 me
.from_pydata(all_verts_coords
, all_edges
, [])
1414 ob
= bpy
.data
.objects
.new(me_name
, me
)
1416 bpy
.context
.collection
.objects
.link(ob
)
1418 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1420 bpy
.context
.view_layer
.objects
.active
= ob
1422 # Get together each vert and its nearest, to the middle position
1423 verts
= ob
.data
.vertices
1425 for i
in range(len(verts
)):
1426 shortest_dist
= None
1428 if i
not in checked_verts
:
1429 for t
in range(len(verts
)):
1430 if i
!= t
and t
not in checked_verts
:
1431 dist
= (verts
[i
].co
- verts
[t
].co
).length
1433 if shortest_dist
is not None:
1434 if dist
< shortest_dist
:
1435 shortest_dist
= dist
1438 shortest_dist
= dist
1441 middle_location
= (verts
[i
].co
+ verts
[nearest_vert
].co
) / 2
1443 verts
[i
].co
= middle_location
1444 verts
[nearest_vert
].co
= middle_location
1446 checked_verts
.append(i
)
1447 checked_verts
.append(nearest_vert
)
1449 # Calculate average length between all the generated edges
1450 ob
= bpy
.context
.object
1452 for ed
in ob
.data
.edges
:
1453 v1
= ob
.data
.vertices
[ed
.vertices
[0]]
1454 v2
= ob
.data
.vertices
[ed
.vertices
[1]]
1456 lengths_sum
+= (v1
.co
- v2
.co
).length
1458 edges_count
= len(ob
.data
.edges
)
1459 # possible division by zero here
1460 average_edge_length
= lengths_sum
/ edges_count
if edges_count
!= 0 else 0.0001
1462 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1463 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1464 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN',
1465 threshold
=average_edge_length
/ 15.0)
1466 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1468 final_points_ob
= bpy
.context
.view_layer
.objects
.active
1470 # Make a dictionary with the verts related to each vert
1471 related_key_verts
= {}
1472 for ed
in final_points_ob
.data
.edges
:
1473 if not ed
.vertices
[0] in related_key_verts
:
1474 related_key_verts
[ed
.vertices
[0]] = []
1476 if not ed
.vertices
[1] in related_key_verts
:
1477 related_key_verts
[ed
.vertices
[1]] = []
1479 if not ed
.vertices
[1] in related_key_verts
[ed
.vertices
[0]]:
1480 related_key_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
1482 if not ed
.vertices
[0] in related_key_verts
[ed
.vertices
[1]]:
1483 related_key_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
1485 # Get groups of verts forming each face
1486 faces_verts_idx
= []
1487 for v1
in related_key_verts
: # verts-1 ....
1488 for v2
in related_key_verts
: # verts-2
1490 related_verts_in_common
= []
1491 v2_in_rel_v1
= False
1492 v1_in_rel_v2
= False
1493 for rel_v1
in related_key_verts
[v1
]:
1494 # Check if related verts of verts-1 are related verts of verts-2
1495 if rel_v1
in related_key_verts
[v2
]:
1496 related_verts_in_common
.append(rel_v1
)
1498 if v2
in related_key_verts
[v1
]:
1501 if v1
in related_key_verts
[v2
]:
1504 repeated_face
= False
1505 # If two verts have two related verts in common, they form a quad
1506 if len(related_verts_in_common
) == 2:
1507 # Check if the face is already saved
1508 for f_verts
in faces_verts_idx
:
1511 if len(f_verts
) == 4:
1516 if related_verts_in_common
[0] in f_verts
:
1518 if related_verts_in_common
[1] in f_verts
:
1521 if repeated_verts
== len(f_verts
):
1522 repeated_face
= True
1525 if not repeated_face
:
1526 faces_verts_idx
.append([v1
, related_verts_in_common
[0],
1527 v2
, related_verts_in_common
[1]])
1529 # If Two verts have one related vert in common and they are
1530 # related to each other, they form a triangle
1531 elif v2_in_rel_v1
and v1_in_rel_v2
and len(related_verts_in_common
) == 1:
1532 # Check if the face is already saved.
1533 for f_verts
in faces_verts_idx
:
1536 if len(f_verts
) == 3:
1541 if related_verts_in_common
[0] in f_verts
:
1544 if repeated_verts
== len(f_verts
):
1545 repeated_face
= True
1548 if not repeated_face
:
1549 faces_verts_idx
.append([v1
, related_verts_in_common
[0], v2
])
1551 # Keep only the faces that don't overlap by ignoring
1552 # quads that overlap with two adjacent triangles
1553 faces_to_not_include_idx
= [] # Indices of faces_verts_idx to eliminate
1554 for i
in range(len(faces_verts_idx
)):
1555 for t
in range(len(faces_verts_idx
)):
1559 if len(faces_verts_idx
[i
]) == 4 and len(faces_verts_idx
[t
]) == 3:
1560 for v_idx
in faces_verts_idx
[t
]:
1561 if v_idx
in faces_verts_idx
[i
]:
1562 verts_in_common
+= 1
1563 # If it doesn't have all it's vertices repeated in the other face
1564 if verts_in_common
== 3:
1565 if i
not in faces_to_not_include_idx
:
1566 faces_to_not_include_idx
.append(i
)
1569 all_surface_verts_co
= []
1570 verts_idx_translation
= {}
1571 for i
in range(len(final_points_ob
.data
.vertices
)):
1572 coords
= final_points_ob
.data
.vertices
[i
].co
1573 all_surface_verts_co
.append([coords
[0], coords
[1], coords
[2]])
1575 # Verts of each face.
1576 all_surface_faces
= []
1577 for i
in range(len(faces_verts_idx
)):
1578 if i
not in faces_to_not_include_idx
:
1580 for v_idx
in faces_verts_idx
[i
]:
1583 all_surface_faces
.append(face
)
1586 surf_me_name
= "SURFSKIO_surface"
1587 me_surf
= bpy
.data
.meshes
.new(surf_me_name
)
1589 me_surf
.from_pydata(all_surface_verts_co
, [], all_surface_faces
)
1593 ob_surface
= bpy
.data
.objects
.new(surf_me_name
, me_surf
)
1594 bpy
.context
.collection
.objects
.link(ob_surface
)
1596 # Delete final points temporal object
1597 bpy
.ops
.object.delete({"selected_objects": [final_points_ob
]})
1599 # Delete isolated verts if there are any
1600 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1601 ob_surface
.select_set(True)
1602 bpy
.context
.view_layer
.objects
.active
= ob_surface
1604 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1605 bpy
.ops
.mesh
.select_all(action
='DESELECT')
1606 bpy
.ops
.mesh
.select_face_by_sides(type='NOTEQUAL')
1607 bpy
.ops
.mesh
.delete()
1608 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1610 # Join crosshatch results with original mesh
1612 # Calculate a distance to merge the verts of the crosshatch surface to the main object
1613 edges_length_sum
= 0
1614 for ed
in ob_surface
.data
.edges
:
1615 edges_length_sum
+= (
1616 ob_surface
.data
.vertices
[ed
.vertices
[0]].co
-
1617 ob_surface
.data
.vertices
[ed
.vertices
[1]].co
1620 if len(ob_surface
.data
.edges
) > 0:
1621 average_surface_edges_length
= edges_length_sum
/ len(ob_surface
.data
.edges
)
1623 average_surface_edges_length
= 0.0001
1625 # Make dictionary with all the verts connected to each vert, on the new surface object.
1626 surface_connected_verts
= {}
1627 for ed
in ob_surface
.data
.edges
:
1628 if not ed
.vertices
[0] in surface_connected_verts
:
1629 surface_connected_verts
[ed
.vertices
[0]] = []
1631 surface_connected_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
1633 if ed
.vertices
[1] not in surface_connected_verts
:
1634 surface_connected_verts
[ed
.vertices
[1]] = []
1636 surface_connected_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
1638 # Duplicate the new surface object, and use shrinkwrap to
1639 # calculate later the nearest verts to the main object
1640 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1641 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1642 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1644 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1646 final_ob_duplicate
= bpy
.context
.view_layer
.objects
.active
1648 bpy
.ops
.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
1649 shrinkwrap_modifier
= final_ob_duplicate
.modifiers
[-1]
1650 shrinkwrap_modifier
.wrap_method
= "NEAREST_VERTEX"
1651 shrinkwrap_modifier
.target
= self
.main_object
1653 bpy
.ops
.object.modifier_apply('INVOKE_REGION_WIN', apply_as
='DATA', modifier
=shrinkwrap_modifier
.name
)
1655 # Make list with verts of original mesh as index and coords as value
1656 main_object_verts_coords
= []
1657 for v
in self
.main_object
.data
.vertices
:
1658 coords
= self
.main_object
.matrix_world
@ v
.co
1660 # To avoid problems when taking "-0.00" as a different value as "0.00"
1661 for c
in range(len(coords
)):
1662 if "%.3f" % coords
[c
] == "-0.00":
1665 main_object_verts_coords
.append(["%.3f" % coords
[0], "%.3f" % coords
[1], "%.3f" % coords
[2]])
1667 tuple(main_object_verts_coords
)
1669 # Determine which verts will be merged, snap them to the nearest verts
1670 # on the original verts, and get them selected
1671 crosshatch_verts_to_merge
= []
1672 if self
.automatic_join
:
1673 for i
in range(len(ob_surface
.data
.vertices
)):
1674 # Calculate the distance from each of the connected verts to the actual vert,
1675 # and compare it with the distance they would have if joined.
1676 # If they don't change much, that vert can be joined
1677 merge_actual_vert
= True
1678 if len(surface_connected_verts
[i
]) < 4:
1679 for c_v_idx
in surface_connected_verts
[i
]:
1680 points_original
= []
1681 points_original
.append(ob_surface
.data
.vertices
[c_v_idx
].co
)
1682 points_original
.append(ob_surface
.data
.vertices
[i
].co
)
1685 points_target
.append(ob_surface
.data
.vertices
[c_v_idx
].co
)
1686 points_target
.append(final_ob_duplicate
.data
.vertices
[i
].co
)
1688 vec_A
= points_original
[0] - points_original
[1]
1689 vec_B
= points_target
[0] - points_target
[1]
1691 dist_A
= (points_original
[0] - points_original
[1]).length
1692 dist_B
= (points_target
[0] - points_target
[1]).length
1695 points_original
[0] == points_original
[1] or
1696 points_target
[0] == points_target
[1]
1697 ): # If any vector's length is zero
1699 angle
= vec_A
.angle(vec_B
) / pi
1703 # Set a range of acceptable variation in the connected edges
1704 if dist_B
> dist_A
* 1.7 * self
.join_stretch_factor
or \
1705 dist_B
< dist_A
/ 2 / self
.join_stretch_factor
or \
1706 angle
>= 0.15 * self
.join_stretch_factor
:
1708 merge_actual_vert
= False
1711 merge_actual_vert
= False
1713 if merge_actual_vert
:
1714 coords
= final_ob_duplicate
.data
.vertices
[i
].co
1715 # To avoid problems when taking "-0.000" as a different value as "0.00"
1716 for c
in range(len(coords
)):
1717 if "%.3f" % coords
[c
] == "-0.00":
1720 comparison_coords
= ["%.3f" % coords
[0], "%.3f" % coords
[1], "%.3f" % coords
[2]]
1722 if comparison_coords
in main_object_verts_coords
:
1723 # Get the index of the vert with those coords in the main object
1724 main_object_related_vert_idx
= main_object_verts_coords
.index(comparison_coords
)
1726 if self
.main_object
.data
.vertices
[main_object_related_vert_idx
].select
is True or \
1727 self
.main_object_selected_verts_count
== 0:
1729 ob_surface
.data
.vertices
[i
].co
= final_ob_duplicate
.data
.vertices
[i
].co
1730 ob_surface
.data
.vertices
[i
].select
= True
1731 crosshatch_verts_to_merge
.append(i
)
1733 # Make sure the vert in the main object is selected,
1734 # in case it wasn't selected and the "join crosshatch" option is active
1735 self
.main_object
.data
.vertices
[main_object_related_vert_idx
].select
= True
1737 # Delete duplicated object
1738 bpy
.ops
.object.delete({"selected_objects": [final_ob_duplicate
]})
1740 # Join crosshatched surface and main object
1741 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1742 ob_surface
.select_set(True)
1743 self
.main_object
.select_set(True)
1744 bpy
.context
.view_layer
.objects
.active
= self
.main_object
1746 bpy
.ops
.object.join('INVOKE_REGION_WIN')
1748 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1749 # Perform Remove doubles to merge verts
1750 if not (self
.automatic_join
is False and self
.main_object_selected_verts_count
== 0):
1751 bpy
.ops
.mesh
.remove_doubles(threshold
=0.0001)
1753 bpy
.ops
.mesh
.select_all(action
='DESELECT')
1755 # If the main object has modifiers, turn their "viewport view status"
1756 # to what it was before the forced deactivation above
1757 if len(self
.main_object
.modifiers
) > 0:
1758 for m_idx
in range(len(self
.main_object
.modifiers
)):
1759 self
.main_object
.modifiers
[m_idx
].show_viewport
= self
.modifiers_prev_viewport_state
[m_idx
]
1763 def rectangular_surface(self
):
1765 all_selected_edges_idx
= []
1766 all_selected_verts
= []
1768 for ed
in self
.main_object
.data
.edges
:
1770 all_selected_edges_idx
.append(ed
.index
)
1773 if not ed
.vertices
[0] in all_selected_verts
:
1774 all_selected_verts
.append(self
.main_object
.data
.vertices
[ed
.vertices
[0]])
1775 if not ed
.vertices
[1] in all_selected_verts
:
1776 all_selected_verts
.append(self
.main_object
.data
.vertices
[ed
.vertices
[1]])
1778 # All verts (both from each edge) to determine later
1779 # which are at the tips (those not repeated twice)
1780 all_verts_idx
.append(ed
.vertices
[0])
1781 all_verts_idx
.append(ed
.vertices
[1])
1783 # Identify the tips and "middle-vertex" that separates U from V, if there is one
1784 all_chains_tips_idx
= []
1785 for v_idx
in all_verts_idx
:
1786 if all_verts_idx
.count(v_idx
) < 2:
1787 all_chains_tips_idx
.append(v_idx
)
1789 edges_connected_to_tips
= []
1790 for ed
in self
.main_object
.data
.edges
:
1791 if (ed
.vertices
[0] in all_chains_tips_idx
or ed
.vertices
[1] in all_chains_tips_idx
) and \
1792 not (ed
.vertices
[0] in all_verts_idx
and ed
.vertices
[1] in all_verts_idx
):
1794 edges_connected_to_tips
.append(ed
)
1796 # Check closed selections
1797 # List with groups of three verts, where the first element of the pair is
1798 # the unselected vert of a closed selection and the other two elements are the
1799 # selected neighbor verts (it will be useful to determine which selection chain
1800 # the unselected vert belongs to, and determine the "middle-vertex")
1801 single_unselected_verts_and_neighbors
= []
1803 # To identify a "closed" selection (a selection that is a closed chain except
1804 # for one vertex) find the vertex in common that have the edges connected to tips.
1805 # If there is a vertex in common, that one is the unselected vert that closes
1806 # the selection or is a "middle-vertex"
1807 single_unselected_verts
= []
1808 for ed
in edges_connected_to_tips
:
1809 for ed_b
in edges_connected_to_tips
:
1811 if ed
.vertices
[0] == ed_b
.vertices
[0] and \
1812 not self
.main_object
.data
.vertices
[ed
.vertices
[0]].select
and \
1813 ed
.vertices
[0] not in single_unselected_verts
:
1815 # The second element is one of the tips of the selected
1816 # vertices of the closed selection
1817 single_unselected_verts_and_neighbors
.append(
1818 [ed
.vertices
[0], ed
.vertices
[1], ed_b
.vertices
[1]]
1820 single_unselected_verts
.append(ed
.vertices
[0])
1822 elif ed
.vertices
[0] == ed_b
.vertices
[1] and \
1823 not self
.main_object
.data
.vertices
[ed
.vertices
[0]].select
and \
1824 ed
.vertices
[0] not in single_unselected_verts
:
1826 single_unselected_verts_and_neighbors
.append(
1827 [ed
.vertices
[0], ed
.vertices
[1], ed_b
.vertices
[0]]
1829 single_unselected_verts
.append(ed
.vertices
[0])
1831 elif ed
.vertices
[1] == ed_b
.vertices
[0] and \
1832 not self
.main_object
.data
.vertices
[ed
.vertices
[1]].select
and \
1833 ed
.vertices
[1] not in single_unselected_verts
:
1835 single_unselected_verts_and_neighbors
.append(
1836 [ed
.vertices
[1], ed
.vertices
[0], ed_b
.vertices
[1]]
1838 single_unselected_verts
.append(ed
.vertices
[1])
1840 elif ed
.vertices
[1] == ed_b
.vertices
[1] and \
1841 not self
.main_object
.data
.vertices
[ed
.vertices
[1]].select
and \
1842 ed
.vertices
[1] not in single_unselected_verts
:
1844 single_unselected_verts_and_neighbors
.append(
1845 [ed
.vertices
[1], ed
.vertices
[0], ed_b
.vertices
[0]]
1847 single_unselected_verts
.append(ed
.vertices
[1])
1850 middle_vertex_idx
= None
1851 tips_to_discard_idx
= []
1853 # Check if there is a "middle-vertex", and get its index
1854 for i
in range(0, len(single_unselected_verts_and_neighbors
)):
1855 actual_chain_verts
= self
.get_ordered_verts(
1856 self
.main_object
, all_selected_edges_idx
,
1857 all_verts_idx
, single_unselected_verts_and_neighbors
[i
][1],
1861 if single_unselected_verts_and_neighbors
[i
][2] != \
1862 actual_chain_verts
[len(actual_chain_verts
) - 1].index
:
1864 middle_vertex_idx
= single_unselected_verts_and_neighbors
[i
][0]
1865 tips_to_discard_idx
.append(single_unselected_verts_and_neighbors
[i
][1])
1866 tips_to_discard_idx
.append(single_unselected_verts_and_neighbors
[i
][2])
1868 # List with pairs of verts that belong to the tips of each selection chain (row)
1869 verts_tips_same_chain_idx
= []
1870 if len(all_chains_tips_idx
) >= 2:
1872 for i
in range(0, len(all_chains_tips_idx
)):
1873 if all_chains_tips_idx
[i
] not in checked_v
:
1874 v_chain
= self
.get_ordered_verts(
1875 self
.main_object
, all_selected_edges_idx
,
1876 all_verts_idx
, all_chains_tips_idx
[i
],
1877 middle_vertex_idx
, None
1880 verts_tips_same_chain_idx
.append([v_chain
[0].index
, v_chain
[len(v_chain
) - 1].index
])
1882 checked_v
.append(v_chain
[0].index
)
1883 checked_v
.append(v_chain
[len(v_chain
) - 1].index
)
1885 # Selection tips (vertices).
1886 verts_tips_parsed_idx
= []
1887 if len(all_chains_tips_idx
) >= 2:
1888 for spec_v_idx
in all_chains_tips_idx
:
1889 if (spec_v_idx
not in tips_to_discard_idx
):
1890 verts_tips_parsed_idx
.append(spec_v_idx
)
1892 # Identify the type of selection made by the user
1893 if middle_vertex_idx
is not None:
1894 # If there are 4 tips (two selection chains), and
1895 # there is only one single unselected vert (the middle vert)
1896 if len(all_chains_tips_idx
) == 4 and len(single_unselected_verts_and_neighbors
) == 1:
1897 selection_type
= "TWO_CONNECTED"
1899 # The type of the selection was not identified, the script stops.
1900 self
.report({'WARNING'}, "The selection isn't valid.")
1901 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1902 self
.cleanup_on_interruption()
1903 self
.stopping_errors
= True
1907 if len(all_chains_tips_idx
) == 2: # If there are 2 tips
1908 selection_type
= "SINGLE"
1909 elif len(all_chains_tips_idx
) == 4: # If there are 4 tips
1910 selection_type
= "TWO_NOT_CONNECTED"
1911 elif len(all_chains_tips_idx
) == 0:
1912 if len(self
.main_splines
.data
.splines
) > 1:
1913 selection_type
= "NO_SELECTION"
1915 # If the selection was not identified and there is only one stroke,
1916 # there's no possibility to build a surface, so the script is interrupted
1917 self
.report({'WARNING'}, "The selection isn't valid.")
1918 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1919 self
.cleanup_on_interruption()
1920 self
.stopping_errors
= True
1924 # The type of the selection was not identified, the script stops
1925 self
.report({'WARNING'}, "The selection isn't valid.")
1927 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1928 self
.cleanup_on_interruption()
1930 self
.stopping_errors
= True
1934 # If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script
1935 if selection_type
== "TWO_NOT_CONNECTED" and len(self
.main_splines
.data
.splines
) == 1:
1936 self
.report({'WARNING'},
1937 "At least two strokes are needed when there are two not connected selections")
1938 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1939 self
.cleanup_on_interruption()
1940 self
.stopping_errors
= True
1944 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1946 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1947 self
.main_splines
.select_set(True)
1948 bpy
.context
.view_layer
.objects
.active
= self
.main_splines
1950 # Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out
1951 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1952 bpy
.ops
.curve
.smooth('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
.object.editmode_toggle('INVOKE_REGION_WIN')
1960 self
.selection_U_exists
= False
1961 self
.selection_U2_exists
= False
1962 self
.selection_V_exists
= False
1963 self
.selection_V2_exists
= False
1965 self
.selection_U_is_closed
= False
1966 self
.selection_U2_is_closed
= False
1967 self
.selection_V_is_closed
= False
1968 self
.selection_V2_is_closed
= False
1970 # Define what vertices are at the tips of each selection and are not the middle-vertex
1971 if selection_type
== "TWO_CONNECTED":
1972 self
.selection_U_exists
= True
1973 self
.selection_V_exists
= True
1975 closing_vert_U_idx
= None
1976 closing_vert_V_idx
= None
1977 closing_vert_U2_idx
= None
1978 closing_vert_V2_idx
= None
1980 # Determine which selection is Selection-U and which is Selection-V
1983 points_first_stroke_tips
= []
1986 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[verts_tips_parsed_idx
[0]].co
1989 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[middle_vertex_idx
].co
1992 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[verts_tips_parsed_idx
[1]].co
1995 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[middle_vertex_idx
].co
1997 points_first_stroke_tips
.append(
1998 self
.main_splines
.data
.splines
[0].bezier_points
[0].co
2000 points_first_stroke_tips
.append(
2001 self
.main_splines
.data
.splines
[0].bezier_points
[
2002 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2006 angle_A
= self
.orientation_difference(points_A
, points_first_stroke_tips
)
2007 angle_B
= self
.orientation_difference(points_B
, points_first_stroke_tips
)
2009 if angle_A
< angle_B
:
2010 first_vert_U_idx
= verts_tips_parsed_idx
[0]
2011 first_vert_V_idx
= verts_tips_parsed_idx
[1]
2013 first_vert_U_idx
= verts_tips_parsed_idx
[1]
2014 first_vert_V_idx
= verts_tips_parsed_idx
[0]
2016 elif selection_type
== "SINGLE" or selection_type
== "TWO_NOT_CONNECTED":
2017 first_sketched_point_first_stroke_co
= self
.main_splines
.data
.splines
[0].bezier_points
[0].co
2018 last_sketched_point_first_stroke_co
= \
2019 self
.main_splines
.data
.splines
[0].bezier_points
[
2020 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2022 first_sketched_point_last_stroke_co
= \
2023 self
.main_splines
.data
.splines
[
2024 len(self
.main_splines
.data
.splines
) - 1
2025 ].bezier_points
[0].co
2026 if len(self
.main_splines
.data
.splines
) > 1:
2027 first_sketched_point_second_stroke_co
= self
.main_splines
.data
.splines
[1].bezier_points
[0].co
2028 last_sketched_point_second_stroke_co
= \
2029 self
.main_splines
.data
.splines
[1].bezier_points
[
2030 len(self
.main_splines
.data
.splines
[1].bezier_points
) - 1
2033 single_unselected_neighbors
= [] # Only the neighbors of the single unselected verts
2034 for verts_neig_idx
in single_unselected_verts_and_neighbors
:
2035 single_unselected_neighbors
.append(verts_neig_idx
[1])
2036 single_unselected_neighbors
.append(verts_neig_idx
[2])
2038 all_chains_tips_and_middle_vert
= []
2039 for v_idx
in all_chains_tips_idx
:
2040 if v_idx
not in single_unselected_neighbors
:
2041 all_chains_tips_and_middle_vert
.append(v_idx
)
2043 all_chains_tips_and_middle_vert
+= single_unselected_verts
2045 all_participating_verts
= all_chains_tips_and_middle_vert
+ all_verts_idx
2047 # The tip of the selected vertices nearest to the first point of the first sketched stroke
2048 nearest_tip_to_first_st_first_pt_idx
, shortest_distance_to_first_stroke
= \
2049 self
.shortest_distance(
2051 first_sketched_point_first_stroke_co
,
2052 all_chains_tips_and_middle_vert
2054 # If the nearest tip is not from a closed selection, get the opposite tip vertex index
2055 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2056 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2058 nearest_tip_to_first_st_first_pt_opposite_idx
= \
2060 nearest_tip_to_first_st_first_pt_idx
,
2061 verts_tips_same_chain_idx
2063 # The tip of the selected vertices nearest to the last point of the first sketched stroke
2064 nearest_tip_to_first_st_last_pt_idx
, temp_dist
= \
2065 self
.shortest_distance(
2067 last_sketched_point_first_stroke_co
,
2068 all_chains_tips_and_middle_vert
2070 # The tip of the selected vertices nearest to the first point of the last sketched stroke
2071 nearest_tip_to_last_st_first_pt_idx
, shortest_distance_to_last_stroke
= \
2072 self
.shortest_distance(
2074 first_sketched_point_last_stroke_co
,
2075 all_chains_tips_and_middle_vert
2077 if len(self
.main_splines
.data
.splines
) > 1:
2078 # The selected vertex nearest to the first point of the second sketched stroke
2079 # (This will be useful to determine the direction of the closed
2080 # selection V when extruding along strokes)
2081 nearest_vert_to_second_st_first_pt_idx
, temp_dist
= \
2082 self
.shortest_distance(
2084 first_sketched_point_second_stroke_co
,
2087 # The selected vertex nearest to the first point of the second sketched stroke
2088 # (This will be useful to determine the direction of the closed
2089 # selection V2 when extruding along strokes)
2090 nearest_vert_to_second_st_last_pt_idx
, temp_dist
= \
2091 self
.shortest_distance(
2093 last_sketched_point_second_stroke_co
,
2096 # Determine if the single selection will be treated as U or as V
2098 for i
in all_selected_edges_idx
:
2100 (self
.main_object
.matrix_world
@
2101 self
.main_object
.data
.vertices
[self
.main_object
.data
.edges
[i
].vertices
[0]].co
) -
2102 (self
.main_object
.matrix_world
@
2103 self
.main_object
.data
.vertices
[self
.main_object
.data
.edges
[i
].vertices
[1]].co
)
2106 average_edge_length
= edges_sum
/ len(all_selected_edges_idx
)
2108 # Get shortest distance from the first point of the last stroke to any participating vertex
2109 temp_idx
, shortest_distance_to_last_stroke
= \
2110 self
.shortest_distance(
2112 first_sketched_point_last_stroke_co
,
2113 all_participating_verts
2115 # If the beginning of the first stroke is near enough, and its orientation
2116 # difference with the first edge of the nearest selection chain is not too high,
2117 # interpret things as an "extrude along strokes" instead of "extrude through strokes"
2118 if shortest_distance_to_first_stroke
< average_edge_length
/ 4 and \
2119 shortest_distance_to_last_stroke
< average_edge_length
and \
2120 len(self
.main_splines
.data
.splines
) > 1:
2122 self
.selection_U_exists
= False
2123 self
.selection_V_exists
= True
2124 # If the first selection is not closed
2125 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2126 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2127 self
.selection_V_is_closed
= False
2128 first_neighbor_V_idx
= None
2129 closing_vert_U_idx
= None
2130 closing_vert_U2_idx
= None
2131 closing_vert_V_idx
= None
2132 closing_vert_V2_idx
= None
2134 first_vert_V_idx
= nearest_tip_to_first_st_first_pt_idx
2136 if selection_type
== "TWO_NOT_CONNECTED":
2137 self
.selection_V2_exists
= True
2139 first_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2141 self
.selection_V_is_closed
= True
2142 closing_vert_V_idx
= nearest_tip_to_first_st_first_pt_idx
2144 # Get the neighbors of the first (unselected) vert of the closed selection U.
2146 for verts
in single_unselected_verts_and_neighbors
:
2147 if verts
[0] == nearest_tip_to_first_st_first_pt_idx
:
2148 vert_neighbors
.append(verts
[1])
2149 vert_neighbors
.append(verts
[2])
2152 verts_V
= self
.get_ordered_verts(
2153 self
.main_object
, all_selected_edges_idx
,
2154 all_verts_idx
, vert_neighbors
[0], middle_vertex_idx
, None
2157 for i
in range(0, len(verts_V
)):
2158 if verts_V
[i
].index
== nearest_vert_to_second_st_first_pt_idx
:
2159 # If the vertex nearest to the first point of the second stroke
2160 # is in the first half of the selected verts
2161 if i
>= len(verts_V
) / 2:
2162 first_vert_V_idx
= vert_neighbors
[1]
2165 first_vert_V_idx
= vert_neighbors
[0]
2168 if selection_type
== "TWO_NOT_CONNECTED":
2169 self
.selection_V2_exists
= True
2170 # If the second selection is not closed
2171 if nearest_tip_to_first_st_last_pt_idx
not in single_unselected_verts
or \
2172 nearest_tip_to_first_st_last_pt_idx
== middle_vertex_idx
:
2174 self
.selection_V2_is_closed
= False
2175 first_neighbor_V2_idx
= None
2176 closing_vert_V2_idx
= None
2177 first_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2180 self
.selection_V2_is_closed
= True
2181 closing_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2183 # Get the neighbors of the first (unselected) vert of the closed selection U
2185 for verts
in single_unselected_verts_and_neighbors
:
2186 if verts
[0] == nearest_tip_to_first_st_last_pt_idx
:
2187 vert_neighbors
.append(verts
[1])
2188 vert_neighbors
.append(verts
[2])
2191 verts_V2
= self
.get_ordered_verts(
2192 self
.main_object
, all_selected_edges_idx
,
2193 all_verts_idx
, vert_neighbors
[0], middle_vertex_idx
, None
2196 for i
in range(0, len(verts_V2
)):
2197 if verts_V2
[i
].index
== nearest_vert_to_second_st_last_pt_idx
:
2198 # If the vertex nearest to the first point of the second stroke
2199 # is in the first half of the selected verts
2200 if i
>= len(verts_V2
) / 2:
2201 first_vert_V2_idx
= vert_neighbors
[1]
2204 first_vert_V2_idx
= vert_neighbors
[0]
2207 self
.selection_V2_exists
= False
2210 self
.selection_U_exists
= True
2211 self
.selection_V_exists
= False
2212 # If the first selection is not closed
2213 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2214 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2215 self
.selection_U_is_closed
= False
2216 first_neighbor_U_idx
= None
2217 closing_vert_U_idx
= None
2221 self
.main_object
.matrix_world
@
2222 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_idx
].co
2225 self
.main_object
.matrix_world
@
2226 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_opposite_idx
].co
2228 points_first_stroke_tips
= []
2229 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[0].co
)
2230 points_first_stroke_tips
.append(
2231 self
.main_splines
.data
.splines
[0].bezier_points
[
2232 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2235 vec_A
= points_tips
[0] - points_tips
[1]
2236 vec_B
= points_first_stroke_tips
[0] - points_first_stroke_tips
[1]
2238 # Compare the direction of the selection and the first
2239 # grease pencil stroke to determine which is the "first" vertex of the selection
2240 if vec_A
.dot(vec_B
) < 0:
2241 first_vert_U_idx
= nearest_tip_to_first_st_first_pt_opposite_idx
2243 first_vert_U_idx
= nearest_tip_to_first_st_first_pt_idx
2246 self
.selection_U_is_closed
= True
2247 closing_vert_U_idx
= nearest_tip_to_first_st_first_pt_idx
2249 # Get the neighbors of the first (unselected) vert of the closed selection U
2251 for verts
in single_unselected_verts_and_neighbors
:
2252 if verts
[0] == nearest_tip_to_first_st_first_pt_idx
:
2253 vert_neighbors
.append(verts
[1])
2254 vert_neighbors
.append(verts
[2])
2257 points_first_and_neighbor
= []
2258 points_first_and_neighbor
.append(
2259 self
.main_object
.matrix_world
@
2260 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_idx
].co
2262 points_first_and_neighbor
.append(
2263 self
.main_object
.matrix_world
@
2264 self
.main_object
.data
.vertices
[vert_neighbors
[0]].co
2266 points_first_stroke_tips
= []
2267 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[0].co
)
2268 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[1].co
)
2270 vec_A
= points_first_and_neighbor
[0] - points_first_and_neighbor
[1]
2271 vec_B
= points_first_stroke_tips
[0] - points_first_stroke_tips
[1]
2273 # Compare the direction of the selection and the first grease pencil stroke to
2274 # determine which is the vertex neighbor to the first vertex (unselected) of
2275 # the closed selection. This will determine the direction of the closed selection
2276 if vec_A
.dot(vec_B
) < 0:
2277 first_vert_U_idx
= vert_neighbors
[1]
2279 first_vert_U_idx
= vert_neighbors
[0]
2281 if selection_type
== "TWO_NOT_CONNECTED":
2282 self
.selection_U2_exists
= True
2283 # If the second selection is not closed
2284 if nearest_tip_to_last_st_first_pt_idx
not in single_unselected_verts
or \
2285 nearest_tip_to_last_st_first_pt_idx
== middle_vertex_idx
:
2287 self
.selection_U2_is_closed
= False
2288 first_neighbor_U2_idx
= None
2289 closing_vert_U2_idx
= None
2290 first_vert_U2_idx
= nearest_tip_to_last_st_first_pt_idx
2292 self
.selection_U2_is_closed
= True
2293 closing_vert_U2_idx
= nearest_tip_to_last_st_first_pt_idx
2295 # Get the neighbors of the first (unselected) vert of the closed selection U
2297 for verts
in single_unselected_verts_and_neighbors
:
2298 if verts
[0] == nearest_tip_to_last_st_first_pt_idx
:
2299 vert_neighbors
.append(verts
[1])
2300 vert_neighbors
.append(verts
[2])
2303 points_first_and_neighbor
= []
2304 points_first_and_neighbor
.append(
2305 self
.main_object
.matrix_world
@
2306 self
.main_object
.data
.vertices
[nearest_tip_to_last_st_first_pt_idx
].co
2308 points_first_and_neighbor
.append(
2309 self
.main_object
.matrix_world
@
2310 self
.main_object
.data
.vertices
[vert_neighbors
[0]].co
2312 points_last_stroke_tips
= []
2313 points_last_stroke_tips
.append(
2314 self
.main_splines
.data
.splines
[
2315 len(self
.main_splines
.data
.splines
) - 1
2316 ].bezier_points
[0].co
2318 points_last_stroke_tips
.append(
2319 self
.main_splines
.data
.splines
[
2320 len(self
.main_splines
.data
.splines
) - 1
2321 ].bezier_points
[1].co
2323 vec_A
= points_first_and_neighbor
[0] - points_first_and_neighbor
[1]
2324 vec_B
= points_last_stroke_tips
[0] - points_last_stroke_tips
[1]
2326 # Compare the direction of the selection and the last grease pencil stroke to
2327 # determine which is the vertex neighbor to the first vertex (unselected) of
2328 # the closed selection. This will determine the direction of the closed selection
2329 if vec_A
.dot(vec_B
) < 0:
2330 first_vert_U2_idx
= vert_neighbors
[1]
2332 first_vert_U2_idx
= vert_neighbors
[0]
2334 self
.selection_U2_exists
= False
2336 elif selection_type
== "NO_SELECTION":
2337 self
.selection_U_exists
= False
2338 self
.selection_V_exists
= False
2340 # Get an ordered list of the vertices of Selection-U
2341 verts_ordered_U
= []
2342 if self
.selection_U_exists
:
2343 verts_ordered_U
= self
.get_ordered_verts(
2344 self
.main_object
, all_selected_edges_idx
,
2345 all_verts_idx
, first_vert_U_idx
,
2346 middle_vertex_idx
, closing_vert_U_idx
2348 verts_ordered_U_indices
= [x
.index
for x
in verts_ordered_U
]
2350 # Get an ordered list of the vertices of Selection-U2
2351 verts_ordered_U2
= []
2352 if self
.selection_U2_exists
:
2353 verts_ordered_U2
= self
.get_ordered_verts(
2354 self
.main_object
, all_selected_edges_idx
,
2355 all_verts_idx
, first_vert_U2_idx
,
2356 middle_vertex_idx
, closing_vert_U2_idx
2358 verts_ordered_U2_indices
= [x
.index
for x
in verts_ordered_U2
]
2360 # Get an ordered list of the vertices of Selection-V
2361 verts_ordered_V
= []
2362 if self
.selection_V_exists
:
2363 verts_ordered_V
= self
.get_ordered_verts(
2364 self
.main_object
, all_selected_edges_idx
,
2365 all_verts_idx
, first_vert_V_idx
,
2366 middle_vertex_idx
, closing_vert_V_idx
2368 verts_ordered_V_indices
= [x
.index
for x
in verts_ordered_V
]
2370 # Get an ordered list of the vertices of Selection-V2
2371 verts_ordered_V2
= []
2372 if self
.selection_V2_exists
:
2373 verts_ordered_V2
= self
.get_ordered_verts(
2374 self
.main_object
, all_selected_edges_idx
,
2375 all_verts_idx
, first_vert_V2_idx
,
2376 middle_vertex_idx
, closing_vert_V2_idx
2378 verts_ordered_V2_indices
= [x
.index
for x
in verts_ordered_V2
]
2380 # Check if when there are two-not-connected selections both have the same
2381 # number of verts. If not terminate the script
2382 if ((self
.selection_U2_exists
and len(verts_ordered_U
) != len(verts_ordered_U2
)) or
2383 (self
.selection_V2_exists
and len(verts_ordered_V
) != len(verts_ordered_V2
))):
2385 self
.report({'WARNING'}, "Both selections must have the same number of edges")
2387 self
.cleanup_on_interruption()
2388 self
.stopping_errors
= True
2392 # Calculate edges U proportions
2393 # Sum selected edges U lengths
2394 edges_lengths_U
= []
2395 edges_lengths_sum_U
= 0
2397 if self
.selection_U_exists
:
2398 edges_lengths_U
, edges_lengths_sum_U
= self
.get_chain_length(
2402 if self
.selection_U2_exists
:
2403 edges_lengths_U2
, edges_lengths_sum_U2
= self
.get_chain_length(
2407 # Sum selected edges V lengths
2408 edges_lengths_V
= []
2409 edges_lengths_sum_V
= 0
2411 if self
.selection_V_exists
:
2412 edges_lengths_V
, edges_lengths_sum_V
= self
.get_chain_length(
2416 if self
.selection_V2_exists
:
2417 edges_lengths_V2
, edges_lengths_sum_V2
= self
.get_chain_length(
2422 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2423 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN',
2424 number_cuts
=bpy
.context
.scene
.bsurfaces
.SURFSK_precision
)
2425 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2428 edges_proportions_U
= []
2429 edges_proportions_U
= self
.get_edges_proportions(
2430 edges_lengths_U
, edges_lengths_sum_U
,
2431 self
.selection_U_exists
, self
.edges_U
2433 verts_count_U
= len(edges_proportions_U
) + 1
2435 if self
.selection_U2_exists
:
2436 edges_proportions_U2
= []
2437 edges_proportions_U2
= self
.get_edges_proportions(
2438 edges_lengths_U2
, edges_lengths_sum_U2
,
2439 self
.selection_U2_exists
, self
.edges_V
2441 verts_count_U2
= len(edges_proportions_U2
) + 1
2444 edges_proportions_V
= []
2445 edges_proportions_V
= self
.get_edges_proportions(
2446 edges_lengths_V
, edges_lengths_sum_V
,
2447 self
.selection_V_exists
, self
.edges_V
2449 verts_count_V
= len(edges_proportions_V
) + 1
2451 if self
.selection_V2_exists
:
2452 edges_proportions_V2
= []
2453 edges_proportions_V2
= self
.get_edges_proportions(
2454 edges_lengths_V2
, edges_lengths_sum_V2
,
2455 self
.selection_V2_exists
, self
.edges_V
2457 verts_count_V2
= len(edges_proportions_V2
) + 1
2459 # Cyclic Follow: simplify sketched curves, make them Cyclic, and complete
2460 # the actual sketched curves with a "closing segment"
2461 if self
.cyclic_follow
and not self
.selection_V_exists
and not \
2462 ((self
.selection_U_exists
and not self
.selection_U_is_closed
) or
2463 (self
.selection_U2_exists
and not self
.selection_U2_is_closed
)):
2465 simplified_spline_coords
= []
2466 simplified_curve
= []
2467 ob_simplified_curve
= []
2468 splines_first_v_co
= []
2469 for i
in range(len(self
.main_splines
.data
.splines
)):
2470 # Create a curve object for the actual spline "cyclic extension"
2471 simplified_curve
.append(bpy
.data
.curves
.new('SURFSKIO_simpl_crv', 'CURVE'))
2472 ob_simplified_curve
.append(bpy
.data
.objects
.new('SURFSKIO_simpl_crv', simplified_curve
[i
]))
2473 bpy
.context
.collection
.objects
.link(ob_simplified_curve
[i
])
2475 simplified_curve
[i
].dimensions
= "3D"
2478 for bp
in self
.main_splines
.data
.splines
[i
].bezier_points
:
2479 spline_coords
.append(bp
.co
)
2482 simplified_spline_coords
.append(self
.simplify_spline(spline_coords
, 5))
2484 # Get the coordinates of the first vert of the actual spline
2485 splines_first_v_co
.append(simplified_spline_coords
[i
][0])
2487 # Generate the spline
2488 spline
= simplified_curve
[i
].splines
.new('BEZIER')
2489 # less one because one point is added when the spline is created
2490 spline
.bezier_points
.add(len(simplified_spline_coords
[i
]) - 1)
2491 for p
in range(0, len(simplified_spline_coords
[i
])):
2492 spline
.bezier_points
[p
].co
= simplified_spline_coords
[i
][p
]
2494 spline
.use_cyclic_u
= True
2496 spline_bp_count
= len(spline
.bezier_points
)
2498 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2499 ob_simplified_curve
[i
].select_set(True)
2500 bpy
.context
.view_layer
.objects
.active
= ob_simplified_curve
[i
]
2502 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2503 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
2504 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2505 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2506 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2508 # Select the "closing segment", and subdivide it
2509 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_control_point
= True
2510 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_left_handle
= True
2511 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_right_handle
= True
2513 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_control_point
= True
2514 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_left_handle
= True
2515 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_right_handle
= True
2517 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2519 (ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].co
-
2520 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].co
).length
/
2521 self
.average_gp_segment_length
2524 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=segments
)
2526 # Delete the other vertices and make it non-cyclic to
2527 # keep only the needed verts of the "closing segment"
2528 bpy
.ops
.curve
.select_all(action
='INVERT')
2529 bpy
.ops
.curve
.delete(type='VERT')
2530 ob_simplified_curve
[i
].data
.splines
[0].use_cyclic_u
= False
2531 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2533 # Add the points of the "closing segment" to the original curve from grease pencil stroke
2534 first_new_index
= len(self
.main_splines
.data
.splines
[i
].bezier_points
)
2535 self
.main_splines
.data
.splines
[i
].bezier_points
.add(
2536 len(ob_simplified_curve
[i
].data
.splines
[0].bezier_points
) - 1
2538 for t
in range(1, len(ob_simplified_curve
[i
].data
.splines
[0].bezier_points
)):
2539 self
.main_splines
.data
.splines
[i
].bezier_points
[t
- 1 + first_new_index
].co
= \
2540 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[t
].co
2542 # Delete the temporal curve
2543 bpy
.ops
.object.delete({"selected_objects": [ob_simplified_curve
[i
]]})
2545 # Get the coords of the points distributed along the sketched strokes,
2546 # with proportions-U of the first selection
2547 pts_on_strokes_with_proportions_U
= self
.distribute_pts(
2548 self
.main_splines
.data
.splines
,
2551 sketched_splines_parsed
= []
2553 if self
.selection_U2_exists
:
2554 # Initialize the multidimensional list with the proportions of all the segments
2555 proportions_loops_crossing_strokes
= []
2556 for i
in range(len(pts_on_strokes_with_proportions_U
)):
2557 proportions_loops_crossing_strokes
.append([])
2559 for t
in range(len(pts_on_strokes_with_proportions_U
[0])):
2560 proportions_loops_crossing_strokes
[i
].append(None)
2562 # Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U
2563 for lp
in range(len(pts_on_strokes_with_proportions_U
[0])):
2564 loop_segments_lengths
= []
2566 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2567 # When on the first stroke, add the segment from the selection to the dirst stroke
2569 loop_segments_lengths
.append(
2570 ((self
.main_object
.matrix_world
@ verts_ordered_U
[lp
].co
) -
2571 pts_on_strokes_with_proportions_U
[0][lp
]).length
2573 # For all strokes except for the last, calculate the distance
2574 # from the actual stroke to the next
2575 if st
!= len(pts_on_strokes_with_proportions_U
) - 1:
2576 loop_segments_lengths
.append(
2577 (pts_on_strokes_with_proportions_U
[st
][lp
] -
2578 pts_on_strokes_with_proportions_U
[st
+ 1][lp
]).length
2580 # When on the last stroke, add the segments
2581 # from the last stroke to the second selection
2582 if st
== len(pts_on_strokes_with_proportions_U
) - 1:
2583 loop_segments_lengths
.append(
2584 (pts_on_strokes_with_proportions_U
[st
][lp
] -
2585 (self
.main_object
.matrix_world
@ verts_ordered_U2
[lp
].co
)).length
2587 # Calculate full loop length
2588 loop_seg_lengths_sum
= 0
2589 for i
in range(len(loop_segments_lengths
)):
2590 loop_seg_lengths_sum
+= loop_segments_lengths
[i
]
2592 # Fill the multidimensional list with the proportions of all the segments
2593 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2594 proportions_loops_crossing_strokes
[st
][lp
] = \
2595 loop_segments_lengths
[st
] / loop_seg_lengths_sum
2597 # Calculate proportions for each stroke
2598 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2599 actual_stroke_spline
= []
2600 # Needs to be a list for the "distribute_pts" method
2601 actual_stroke_spline
.append(self
.main_splines
.data
.splines
[st
])
2603 # Calculate the proportions for the actual stroke.
2604 actual_edges_proportions_U
= []
2605 for i
in range(len(edges_proportions_U
)):
2608 # Sum the proportions of this loop up to the actual.
2609 for t
in range(0, st
+ 1):
2610 proportions_sum
+= proportions_loops_crossing_strokes
[t
][i
]
2611 # i + 1, because proportions_loops_crossing_strokes refers to loops,
2612 # and the proportions refer to edges, so we start at the element 1
2613 # of proportions_loops_crossing_strokes instead of element 0
2614 actual_edges_proportions_U
.append(
2615 edges_proportions_U
[i
] -
2616 ((edges_proportions_U
[i
] - edges_proportions_U2
[i
]) * proportions_sum
)
2618 points_actual_spline
= self
.distribute_pts(actual_stroke_spline
, actual_edges_proportions_U
)
2619 sketched_splines_parsed
.append(points_actual_spline
[0])
2621 sketched_splines_parsed
= pts_on_strokes_with_proportions_U
2623 # If the selection type is "TWO_NOT_CONNECTED" replace the
2624 # points of the last spline with the points in the "target" selection
2625 if selection_type
== "TWO_NOT_CONNECTED":
2626 if self
.selection_U2_exists
:
2627 for i
in range(0, len(sketched_splines_parsed
[len(sketched_splines_parsed
) - 1])):
2628 sketched_splines_parsed
[len(sketched_splines_parsed
) - 1][i
] = \
2629 self
.main_object
.matrix_world
@ verts_ordered_U2
[i
].co
2631 # Create temporary curves along the "control-points" found
2632 # on the sketched curves and the mesh selection
2633 mesh_ctrl_pts_name
= "SURFSKIO_ctrl_pts"
2634 me
= bpy
.data
.meshes
.new(mesh_ctrl_pts_name
)
2635 ob_ctrl_pts
= bpy
.data
.objects
.new(mesh_ctrl_pts_name
, me
)
2636 ob_ctrl_pts
.data
= me
2637 bpy
.context
.collection
.objects
.link(ob_ctrl_pts
)
2644 for i
in range(0, verts_count_U
):
2645 vert_num_in_spline
= 1
2647 if self
.selection_U_exists
:
2648 ob_ctrl_pts
.data
.vertices
.add(1)
2649 last_v
= ob_ctrl_pts
.data
.vertices
[len(ob_ctrl_pts
.data
.vertices
) - 1]
2650 last_v
.co
= self
.main_object
.matrix_world
@ verts_ordered_U
[i
].co
2652 vert_num_in_spline
+= 1
2654 for t
in range(0, len(sketched_splines_parsed
)):
2655 ob_ctrl_pts
.data
.vertices
.add(1)
2656 v
= ob_ctrl_pts
.data
.vertices
[len(ob_ctrl_pts
.data
.vertices
) - 1]
2657 v
.co
= sketched_splines_parsed
[t
][i
]
2659 if vert_num_in_spline
> 1:
2660 ob_ctrl_pts
.data
.edges
.add(1)
2661 ob_ctrl_pts
.data
.edges
[len(ob_ctrl_pts
.data
.edges
) - 1].vertices
[0] = \
2662 len(ob_ctrl_pts
.data
.vertices
) - 2
2663 ob_ctrl_pts
.data
.edges
[len(ob_ctrl_pts
.data
.edges
) - 1].vertices
[1] = \
2664 len(ob_ctrl_pts
.data
.vertices
) - 1
2667 first_verts
.append(v
.index
)
2670 second_verts
.append(v
.index
)
2672 if t
== len(sketched_splines_parsed
) - 1:
2673 last_verts
.append(v
.index
)
2676 vert_num_in_spline
+= 1
2678 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2679 ob_ctrl_pts
.select_set(True)
2680 bpy
.context
.view_layer
.objects
.active
= ob_ctrl_pts
2682 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2683 bpy
.ops
.mesh
.select_all(action
='DESELECT')
2684 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2686 # Determine which loops-U will be "Cyclic"
2687 for i
in range(0, len(first_verts
)):
2688 # When there is Cyclic Cross there is no need of
2689 # Automatic Join, (and there are at least three strokes)
2690 if self
.automatic_join
and not self
.cyclic_cross
and \
2691 selection_type
!= "TWO_CONNECTED" and len(self
.main_splines
.data
.splines
) >= 3:
2693 v
= ob_ctrl_pts
.data
.vertices
2694 first_point_co
= v
[first_verts
[i
]].co
2695 second_point_co
= v
[second_verts
[i
]].co
2696 last_point_co
= v
[last_verts
[i
]].co
2698 # Coordinates of the point in the center of both the first and last verts.
2700 (first_point_co
[0] + last_point_co
[0]) / 2,
2701 (first_point_co
[1] + last_point_co
[1]) / 2,
2702 (first_point_co
[2] + last_point_co
[2]) / 2
2704 vec_A
= second_point_co
- first_point_co
2705 vec_B
= second_point_co
- Vector(verts_center_co
)
2707 # Calculate the length of the first segment of the loop,
2708 # and the length it would have after moving the first vert
2709 # to the middle position between first and last
2710 length_original
= (second_point_co
- first_point_co
).length
2711 length_target
= (second_point_co
- Vector(verts_center_co
)).length
2713 angle
= vec_A
.angle(vec_B
) / pi
2715 # If the target length doesn't stretch too much, and the
2716 # its angle doesn't change to much either
2717 if length_target
<= length_original
* 1.03 * self
.join_stretch_factor
and \
2718 angle
<= 0.008 * self
.join_stretch_factor
and not self
.selection_U_exists
:
2720 cyclic_loops_U
.append(True)
2721 # Move the first vert to the center coordinates
2722 ob_ctrl_pts
.data
.vertices
[first_verts
[i
]].co
= verts_center_co
2723 # Select the last verts from Cyclic loops, for later deletion all at once
2724 v
[last_verts
[i
]].select
= True
2726 cyclic_loops_U
.append(False)
2728 # If "Cyclic Cross" is active then "all" crossing curves become cyclic
2729 if self
.cyclic_cross
and not self
.selection_U_exists
and not \
2730 ((self
.selection_V_exists
and not self
.selection_V_is_closed
) or
2731 (self
.selection_V2_exists
and not self
.selection_V2_is_closed
)):
2733 cyclic_loops_U
.append(True)
2735 cyclic_loops_U
.append(False)
2737 # The cyclic_loops_U list needs to be reversed.
2738 cyclic_loops_U
.reverse()
2740 # Delete the previously selected (last_)verts.
2741 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2742 bpy
.ops
.mesh
.delete('INVOKE_REGION_WIN', type='VERT')
2743 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2745 # Create curves from control points.
2746 bpy
.ops
.object.convert('INVOKE_REGION_WIN', target
='CURVE', keep_original
=False)
2747 ob_curves_surf
= bpy
.context
.view_layer
.objects
.active
2748 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2749 bpy
.ops
.curve
.spline_type_set('INVOKE_REGION_WIN', type='BEZIER')
2750 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2752 # Make Cyclic the splines designated as Cyclic.
2753 for i
in range(0, len(cyclic_loops_U
)):
2754 ob_curves_surf
.data
.splines
[i
].use_cyclic_u
= cyclic_loops_U
[i
]
2756 # Get the coords of all points on first loop-U, for later comparison with its
2757 # subdivided version, to know which points of the loops-U are crossed by the
2758 # original strokes. The indices will be the same for the other loops-U
2759 if self
.loops_on_strokes
:
2760 coords_loops_U_control_points
= []
2761 for p
in ob_ctrl_pts
.data
.splines
[0].bezier_points
:
2762 coords_loops_U_control_points
.append(["%.4f" % p
.co
[0], "%.4f" % p
.co
[1], "%.4f" % p
.co
[2]])
2764 tuple(coords_loops_U_control_points
)
2766 # Calculate number of edges-V in case option "Loops on strokes" is active or inactive
2767 if self
.loops_on_strokes
and not self
.selection_V_exists
:
2768 edges_V_count
= len(self
.main_splines
.data
.splines
) * self
.edges_V
2770 edges_V_count
= len(edges_proportions_V
)
2772 # The Follow precision will vary depending on the number of Follow face-loops
2773 precision_multiplier
= round(2 + (edges_V_count
/ 15))
2774 curve_cuts
= bpy
.context
.scene
.bsurfaces
.SURFSK_precision
* precision_multiplier
2776 # Subdivide the curves
2777 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=curve_cuts
)
2779 # The verts position shifting that happens with splines subdivision.
2780 # For later reorder splines points
2781 verts_position_shift
= curve_cuts
+ 1
2782 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2784 # Reorder coordinates of the points of each spline to put the first point of
2785 # the spline starting at the position it was the first point before sudividing
2786 # the curve. And make a new curve object per spline (to handle memory better later)
2787 splines_U_objects
= []
2788 for i
in range(len(ob_curves_surf
.data
.splines
)):
2789 spline_U_curve
= bpy
.data
.curves
.new('SURFSKIO_spline_U_' + str(i
), 'CURVE')
2790 ob_spline_U
= bpy
.data
.objects
.new('SURFSKIO_spline_U_' + str(i
), spline_U_curve
)
2791 bpy
.context
.collection
.objects
.link(ob_spline_U
)
2793 spline_U_curve
.dimensions
= "3D"
2795 # Add points to the spline in the new curve object
2796 ob_spline_U
.data
.splines
.new('BEZIER')
2797 for t
in range(len(ob_curves_surf
.data
.splines
[i
].bezier_points
)):
2798 if cyclic_loops_U
[i
] is True and not self
.selection_U_exists
: # If the loop is cyclic
2799 if t
+ verts_position_shift
<= len(ob_curves_surf
.data
.splines
[i
].bezier_points
) - 1:
2800 point_index
= t
+ verts_position_shift
2802 point_index
= t
+ verts_position_shift
- len(ob_curves_surf
.data
.splines
[i
].bezier_points
)
2805 # to avoid adding the first point since it's added when the spline is created
2807 ob_spline_U
.data
.splines
[0].bezier_points
.add(1)
2808 ob_spline_U
.data
.splines
[0].bezier_points
[t
].co
= \
2809 ob_curves_surf
.data
.splines
[i
].bezier_points
[point_index
].co
2811 if cyclic_loops_U
[i
] is True and not self
.selection_U_exists
: # If the loop is cyclic
2812 # Add a last point at the same location as the first one
2813 ob_spline_U
.data
.splines
[0].bezier_points
.add(1)
2814 ob_spline_U
.data
.splines
[0].bezier_points
[len(ob_spline_U
.data
.splines
[0].bezier_points
) - 1].co
= \
2815 ob_spline_U
.data
.splines
[0].bezier_points
[0].co
2817 ob_spline_U
.data
.splines
[0].use_cyclic_u
= False
2819 splines_U_objects
.append(ob_spline_U
)
2820 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2821 ob_spline_U
.select_set(True)
2822 bpy
.context
.view_layer
.objects
.active
= ob_spline_U
2824 # When option "Loops on strokes" is active each "Cross" loop will have
2825 # its own proportions according to where the original strokes "touch" them
2826 if self
.loops_on_strokes
:
2827 # Get the indices of points where the original strokes "touch" loops-U
2828 points_U_crossed_by_strokes
= []
2829 for i
in range(len(splines_U_objects
[0].data
.splines
[0].bezier_points
)):
2830 bp
= splines_U_objects
[0].data
.splines
[0].bezier_points
[i
]
2831 if ["%.4f" % bp
.co
[0], "%.4f" % bp
.co
[1], "%.4f" % bp
.co
[2]] in coords_loops_U_control_points
:
2832 points_U_crossed_by_strokes
.append(i
)
2834 # Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke
2835 edge_order_number_for_splines
= {}
2836 if self
.selection_V_exists
:
2837 # For two-connected selections add a first hypothetic stroke at the beginning.
2838 if selection_type
== "TWO_CONNECTED":
2839 edge_order_number_for_splines
[0] = 0
2841 for i
in range(len(self
.main_splines
.data
.splines
)):
2842 sp
= self
.main_splines
.data
.splines
[i
]
2843 v_idx
, dist_temp
= self
.shortest_distance(
2845 sp
.bezier_points
[0].co
,
2846 verts_ordered_V_indices
2848 # Get the position (edges count) of the vert v_idx in the selected chain V
2849 edge_idx_in_chain
= verts_ordered_V_indices
.index(v_idx
)
2851 # For two-connected selections the strokes go after the
2852 # hypothetic stroke added before, so the index adds one per spline
2853 if selection_type
== "TWO_CONNECTED":
2854 spline_number
= i
+ 1
2858 edge_order_number_for_splines
[spline_number
] = edge_idx_in_chain
2860 # Get the first and last verts indices for later comparison
2863 elif i
== len(self
.main_splines
.data
.splines
) - 1:
2866 if self
.selection_V_is_closed
:
2867 # If there is no last stroke on the last vertex (same as first vertex),
2868 # add a hypothetic spline at last vert order
2869 if first_v_idx
!= last_v_idx
:
2870 edge_order_number_for_splines
[(len(self
.main_splines
.data
.splines
) - 1) + 1] = \
2871 len(verts_ordered_V_indices
) - 1
2873 if self
.cyclic_cross
:
2874 edge_order_number_for_splines
[len(self
.main_splines
.data
.splines
) - 1] = \
2875 len(verts_ordered_V_indices
) - 2
2876 edge_order_number_for_splines
[(len(self
.main_splines
.data
.splines
) - 1) + 1] = \
2877 len(verts_ordered_V_indices
) - 1
2879 edge_order_number_for_splines
[len(self
.main_splines
.data
.splines
) - 1] = \
2880 len(verts_ordered_V_indices
) - 1
2882 # Get the coords of the points distributed along the
2883 # "crossing curves", with appropriate proportions-V
2884 surface_splines_parsed
= []
2885 for i
in range(len(splines_U_objects
)):
2886 sp_ob
= splines_U_objects
[i
]
2887 # If "Loops on strokes" option is active, calculate the proportions for each loop-U
2888 if self
.loops_on_strokes
:
2889 # Segments distances from stroke to stroke
2892 segments_distances
= []
2893 for t
in range(len(sp_ob
.data
.splines
[0].bezier_points
)):
2894 bp
= sp_ob
.data
.splines
[0].bezier_points
[t
]
2900 dist
+= (last_p
- actual_p
).length
2902 if t
in points_U_crossed_by_strokes
:
2903 segments_distances
.append(dist
)
2910 # Calculate Proportions.
2911 used_edges_proportions_V
= []
2912 for t
in range(len(segments_distances
)):
2913 if self
.selection_V_exists
:
2915 order_number_last_stroke
= 0
2917 segment_edges_length_V
= 0
2918 segment_edges_length_V2
= 0
2919 for order
in range(order_number_last_stroke
, edge_order_number_for_splines
[t
+ 1]):
2920 segment_edges_length_V
+= edges_lengths_V
[order
]
2921 if self
.selection_V2_exists
:
2922 segment_edges_length_V2
+= edges_lengths_V2
[order
]
2924 for order
in range(order_number_last_stroke
, edge_order_number_for_splines
[t
+ 1]):
2925 # Calculate each "sub-segment" (the ones between each stroke) length
2926 if self
.selection_V2_exists
:
2927 proportion_sub_seg
= (edges_lengths_V2
[order
] -
2928 ((edges_lengths_V2
[order
] - edges_lengths_V
[order
]) /
2929 len(splines_U_objects
) * i
)) / (segment_edges_length_V2
-
2930 (segment_edges_length_V2
- segment_edges_length_V
) /
2931 len(splines_U_objects
) * i
)
2933 sub_seg_dist
= segments_distances
[t
] * proportion_sub_seg
2935 proportion_sub_seg
= edges_lengths_V
[order
] / segment_edges_length_V
2936 sub_seg_dist
= segments_distances
[t
] * proportion_sub_seg
2938 used_edges_proportions_V
.append(sub_seg_dist
/ full_dist
)
2940 order_number_last_stroke
= edge_order_number_for_splines
[t
+ 1]
2943 for c
in range(self
.edges_V
):
2944 # Calculate each "sub-segment" (the ones between each stroke) length
2945 sub_seg_dist
= segments_distances
[t
] / self
.edges_V
2946 used_edges_proportions_V
.append(sub_seg_dist
/ full_dist
)
2948 actual_spline
= self
.distribute_pts(sp_ob
.data
.splines
, used_edges_proportions_V
)
2949 surface_splines_parsed
.append(actual_spline
[0])
2952 if self
.selection_V2_exists
:
2953 used_edges_proportions_V
= []
2954 for p
in range(len(edges_proportions_V
)):
2955 used_edges_proportions_V
.append(
2956 edges_proportions_V2
[p
] -
2957 ((edges_proportions_V2
[p
] -
2958 edges_proportions_V
[p
]) / len(splines_U_objects
) * i
)
2961 used_edges_proportions_V
= edges_proportions_V
2963 actual_spline
= self
.distribute_pts(sp_ob
.data
.splines
, used_edges_proportions_V
)
2964 surface_splines_parsed
.append(actual_spline
[0])
2966 # Set the verts of the first and last splines to the locations
2967 # of the respective verts in the selections
2968 if self
.selection_V_exists
:
2969 for i
in range(0, len(surface_splines_parsed
[0])):
2970 surface_splines_parsed
[len(surface_splines_parsed
) - 1][i
] = \
2971 self
.main_object
.matrix_world
@ verts_ordered_V
[i
].co
2973 if selection_type
== "TWO_NOT_CONNECTED":
2974 if self
.selection_V2_exists
:
2975 for i
in range(0, len(surface_splines_parsed
[0])):
2976 surface_splines_parsed
[0][i
] = self
.main_object
.matrix_world
@ verts_ordered_V2
[i
].co
2978 # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
2979 # merge the verts of the tips of the loops when they are "near enough"
2980 if self
.automatic_join
and selection_type
!= "TWO_CONNECTED":
2981 # Join the tips of "Follow" loops that are near enough and must be "closed"
2982 if not self
.selection_V_exists
and len(edges_proportions_U
) >= 3:
2983 for i
in range(len(surface_splines_parsed
[0])):
2984 sp
= surface_splines_parsed
2985 loop_segment_dist
= (sp
[0][i
] - sp
[1][i
]).length
2986 full_loop_dist
= loop_segment_dist
* self
.edges_U
2988 verts_middle_position_co
= [
2989 (sp
[0][i
][0] + sp
[len(sp
) - 1][i
][0]) / 2,
2990 (sp
[0][i
][1] + sp
[len(sp
) - 1][i
][1]) / 2,
2991 (sp
[0][i
][2] + sp
[len(sp
) - 1][i
][2]) / 2
2993 points_original
= []
2994 points_original
.append(sp
[1][i
])
2995 points_original
.append(sp
[0][i
])
2998 points_target
.append(sp
[1][i
])
2999 points_target
.append(Vector(verts_middle_position_co
))
3001 vec_A
= points_original
[0] - points_original
[1]
3002 vec_B
= points_target
[0] - points_target
[1]
3003 # check for zero angles, not sure if it is a great fix
3004 if vec_A
.length
!= 0 and vec_B
.length
!= 0:
3005 angle
= vec_A
.angle(vec_B
) / pi
3006 edge_new_length
= (Vector(verts_middle_position_co
) - sp
[1][i
]).length
3011 # If after moving the verts to the middle point, the segment doesn't stretch too much
3012 if edge_new_length
<= loop_segment_dist
* 1.5 * \
3013 self
.join_stretch_factor
and angle
< 0.25 * self
.join_stretch_factor
:
3015 # Avoid joining when the actual loop must be merged with the original mesh
3016 if not (self
.selection_U_exists
and i
== 0) and \
3017 not (self
.selection_U2_exists
and i
== len(surface_splines_parsed
[0]) - 1):
3019 # Change the coords of both verts to the middle position
3020 surface_splines_parsed
[0][i
] = verts_middle_position_co
3021 surface_splines_parsed
[len(surface_splines_parsed
) - 1][i
] = verts_middle_position_co
3023 # Delete object with control points and object from grease pencil conversion
3024 bpy
.ops
.object.delete({"selected_objects": [ob_ctrl_pts
]})
3026 bpy
.ops
.object.delete({"selected_objects": splines_U_objects
})
3030 # Get all verts coords
3031 all_surface_verts_co
= []
3032 for i
in range(0, len(surface_splines_parsed
)):
3033 # Get coords of all verts and make a list with them
3034 for pt_co
in surface_splines_parsed
[i
]:
3035 all_surface_verts_co
.append(pt_co
)
3037 # Define verts for each face
3038 all_surface_faces
= []
3039 for i
in range(0, len(all_surface_verts_co
) - len(surface_splines_parsed
[0])):
3040 if ((i
+ 1) / len(surface_splines_parsed
[0]) != int((i
+ 1) / len(surface_splines_parsed
[0]))):
3041 all_surface_faces
.append(
3042 [i
+ 1, i
, i
+ len(surface_splines_parsed
[0]),
3043 i
+ len(surface_splines_parsed
[0]) + 1]
3046 surf_me_name
= "SURFSKIO_surface"
3047 me_surf
= bpy
.data
.meshes
.new(surf_me_name
)
3049 me_surf
.from_pydata(all_surface_verts_co
, [], all_surface_faces
)
3053 ob_surface
= bpy
.data
.objects
.new(surf_me_name
, me_surf
)
3054 bpy
.context
.collection
.objects
.link(ob_surface
)
3056 # Select all the "unselected but participating" verts, from closed selection
3057 # or double selections with middle-vertex, for later join with remove doubles
3058 for v_idx
in single_unselected_verts
:
3059 self
.main_object
.data
.vertices
[v_idx
].select
= True
3061 # Join the new mesh to the main object
3062 ob_surface
.select_set(True)
3063 self
.main_object
.select_set(True)
3064 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3066 bpy
.ops
.object.join('INVOKE_REGION_WIN')
3068 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3070 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN', threshold
=0.0001)
3071 bpy
.ops
.mesh
.normals_make_consistent('INVOKE_REGION_WIN', inside
=False)
3072 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3076 def execute(self
, context
):
3078 if bpy
.ops
.object.mode_set
.poll():
3079 bpy
.ops
.object.mode_set(mode
='OBJECT')
3081 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3082 self
.main_object
= bsurfaces_props
.SURFSK_object_with_retopology
3083 self
.main_object
.select_set(True)
3084 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3086 if not self
.is_fill_faces
:
3087 bpy
.ops
.wm
.context_set_value(data_path
='tool_settings.mesh_select_mode',
3088 value
='True, False, False')
3090 # Build splines from the "last saved splines".
3091 last_saved_curve
= bpy
.data
.curves
.new('SURFSKIO_last_crv', 'CURVE')
3092 self
.main_splines
= bpy
.data
.objects
.new('SURFSKIO_last_crv', last_saved_curve
)
3093 bpy
.context
.collection
.objects
.link(self
.main_splines
)
3095 last_saved_curve
.dimensions
= "3D"
3097 for sp
in self
.last_strokes_splines_coords
:
3098 spline
= self
.main_splines
.data
.splines
.new('BEZIER')
3099 # less one because one point is added when the spline is created
3100 spline
.bezier_points
.add(len(sp
) - 1)
3101 for p
in range(0, len(sp
)):
3102 spline
.bezier_points
[p
].co
= [sp
[p
][0], sp
[p
][1], sp
[p
][2]]
3104 #bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3105 bpy
.ops
.object.mode_set(mode
='OBJECT')
3107 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3108 self
.main_splines
.select_set(True)
3109 bpy
.context
.view_layer
.objects
.active
= self
.main_splines
3111 #bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3112 bpy
.ops
.object.mode_set(mode
='EDIT')
3114 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
3115 # Important to make it vector first and then automatic, otherwise the
3116 # tips handles get too big and distort the shrinkwrap results later
3117 bpy
.ops
.curve
.handle_type_set(type='VECTOR')
3118 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
3119 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3120 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3122 self
.main_splines
.name
= "SURFSKIO_temp_strokes"
3124 if self
.is_crosshatch
:
3125 strokes_for_crosshatch
= True
3126 strokes_for_rectangular_surface
= False
3128 strokes_for_rectangular_surface
= True
3129 strokes_for_crosshatch
= False
3131 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3132 self
.main_object
.select_set(True)
3133 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3135 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3137 if strokes_for_rectangular_surface
:
3138 self
.rectangular_surface()
3139 elif strokes_for_crosshatch
:
3140 self
.crosshatch_surface_execute()
3142 # Delete main splines
3143 #bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3144 bpy
.ops
.object.mode_set(mode
='OBJECT')
3145 if self
.keep_strokes
:
3146 self
.main_splines
.name
= "keep_strokes"
3147 self
.main_splines
.data
.bevel_depth
= 0.001
3148 if "keep_strokes_material" in bpy
.data
.materials
:
3149 self
.main_splines
.data
.materials
.append(bpy
.data
.materials
["keep_strokes_material"])
3151 mat
= bpy
.data
.materials
.new("keep_strokes_material")
3152 mat
.diffuse_color
= (1, 0, 0, 0)
3153 mat
.specular_color
= (1, 0, 0)
3154 mat
.specular_intensity
= 0.0
3156 self
.main_splines
.data
.materials
.append(mat
)
3158 bpy
.ops
.object.delete({"selected_objects": [self
.main_splines
]})
3160 # Delete grease pencil strokes
3161 if self
.strokes_type
== "GP_STROKES" and not self
.stopping_errors
:
3163 bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
.data
.layers
.active
.clear()
3167 # Delete annotations
3168 if self
.strokes_type
== "GP_ANNOTATION" and not self
.stopping_errors
:
3170 bpy
.data
.grease_pencils
[0].layers
.active
.clear()
3174 bsurfaces_props
.SURFSK_edges_U
= self
.edges_U
3175 bsurfaces_props
.SURFSK_edges_V
= self
.edges_V
3176 bsurfaces_props
.SURFSK_cyclic_cross
= self
.cyclic_cross
3177 bsurfaces_props
.SURFSK_cyclic_follow
= self
.cyclic_follow
3178 bsurfaces_props
.SURFSK_automatic_join
= self
.automatic_join
3179 bsurfaces_props
.SURFSK_loops_on_strokes
= self
.loops_on_strokes
3180 bsurfaces_props
.SURFSK_keep_strokes
= self
.keep_strokes
3182 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3183 self
.main_object
.select_set(True)
3184 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3186 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3190 def invoke(self
, context
, event
):
3191 if bpy
.ops
.object.mode_set
.poll():
3192 bpy
.ops
.object.mode_set(mode
='OBJECT')
3194 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3195 self
.cyclic_cross
= bsurfaces_props
.SURFSK_cyclic_cross
3196 self
.cyclic_follow
= bsurfaces_props
.SURFSK_cyclic_follow
3197 self
.automatic_join
= bsurfaces_props
.SURFSK_automatic_join
3198 self
.loops_on_strokes
= bsurfaces_props
.SURFSK_loops_on_strokes
3199 self
.keep_strokes
= bsurfaces_props
.SURFSK_keep_strokes
3200 self
.main_object
= bsurfaces_props
.SURFSK_object_with_retopology
3202 self
.main_object
.select_set(True)
3204 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3206 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3208 self
.main_object_selected_verts_count
= len([v
for v
in self
.main_object
.data
.vertices
if v
.select
])
3210 bpy
.ops
.wm
.context_set_value(data_path
='tool_settings.mesh_select_mode',
3211 value
='True, False, False')
3213 #if self.loops_on_strokes:
3216 # self.edges_V = bsurfaces_props.SURFSK_edges_V
3217 self
.edges_U
= bsurfaces_props
.SURFSK_edges_U
3218 self
.edges_V
= bsurfaces_props
.SURFSK_edges_V
3220 self
.is_fill_faces
= False
3221 self
.stopping_errors
= False
3222 self
.last_strokes_splines_coords
= []
3224 # Determine the type of the strokes
3225 self
.strokes_type
= get_strokes_type(context
)
3227 # Check if it will be used grease pencil strokes or curves
3228 # If there are strokes to be used
3229 if self
.strokes_type
== "GP_STROKES" or self
.strokes_type
== "EXTERNAL_CURVE" or self
.strokes_type
== "GP_ANNOTATION":
3230 if self
.strokes_type
== "GP_STROKES":
3231 # Convert grease pencil strokes to curve
3232 gp
= bsurfaces_props
.SURFSK_object_with_strokes
3233 #bpy.ops.gpencil.convert(type='CURVE', use_link_strokes=False)
3234 self
.original_curve
= conver_gpencil_to_curve(self
, context
, gp
, 'GPensil')
3235 # XXX gpencil.convert now keep org object as active/selected, *not* newly created curve!
3236 # XXX This is far from perfect, but should work in most cases...
3237 # self.original_curve = bpy.context.object
3238 gplayer_prefix_translated
= bpy
.app
.translations
.pgettext_data('GP_Layer')
3239 for ob
in bpy
.context
.selected_objects
:
3240 if ob
!= bpy
.context
.view_layer
.objects
.active
and \
3241 ob
.name
.startswith((gplayer_prefix_translated
, 'GP_Layer')):
3242 self
.original_curve
= ob
3243 self
.using_external_curves
= False
3245 elif self
.strokes_type
== "GP_ANNOTATION":
3246 # Convert grease pencil strokes to curve
3247 gp
= bpy
.data
.grease_pencils
["Annotations"]
3248 #bpy.ops.gpencil.convert(type='CURVE', use_link_strokes=False)
3249 self
.original_curve
= conver_gpencil_to_curve(self
, context
, gp
, 'Annotation')
3250 # XXX gpencil.convert now keep org object as active/selected, *not* newly created curve!
3251 # XXX This is far from perfect, but should work in most cases...
3252 # self.original_curve = bpy.context.object
3253 gplayer_prefix_translated
= bpy
.app
.translations
.pgettext_data('GP_Layer')
3254 for ob
in bpy
.context
.selected_objects
:
3255 if ob
!= bpy
.context
.view_layer
.objects
.active
and \
3256 ob
.name
.startswith((gplayer_prefix_translated
, 'GP_Layer')):
3257 self
.original_curve
= ob
3258 self
.using_external_curves
= False
3260 elif self
.strokes_type
== "EXTERNAL_CURVE":
3261 self
.original_curve
= bsurfaces_props
.SURFSK_object_with_strokes
3262 self
.using_external_curves
= True
3264 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3266 # Make sure there are no objects left from erroneous
3267 # executions of this operator, with the reserved names used here
3268 for o
in bpy
.data
.objects
:
3269 if o
.name
.find("SURFSKIO_") != -1:
3270 bpy
.ops
.object.delete({"selected_objects": [o
]})
3272 #bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3273 self
.original_curve
.select_set(True)
3274 bpy
.context
.view_layer
.objects
.active
= self
.original_curve
3276 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3278 self
.temporary_curve
= bpy
.context
.view_layer
.objects
.active
3280 # Deselect all points of the curve
3281 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3282 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3283 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3285 # Delete splines with only a single isolated point
3286 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3287 sp
= self
.temporary_curve
.data
.splines
[i
]
3289 if len(sp
.bezier_points
) == 1:
3290 sp
.bezier_points
[0].select_control_point
= True
3292 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3293 bpy
.ops
.curve
.delete(type='VERT')
3294 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3296 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3297 self
.temporary_curve
.select_set(True)
3298 bpy
.context
.view_layer
.objects
.active
= self
.temporary_curve
3300 # Set a minimum number of points for crosshatch
3301 minimum_points_num
= 15
3303 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3304 # Check if the number of points of each curve has at least the number of points
3305 # of minimum_points_num, which is a bit more than the face-loops limit.
3306 # If not, subdivide to reach at least that number of points
3307 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3308 sp
= self
.temporary_curve
.data
.splines
[i
]
3310 if len(sp
.bezier_points
) < minimum_points_num
:
3311 for bp
in sp
.bezier_points
:
3312 bp
.select_control_point
= True
3314 if (len(sp
.bezier_points
) - 1) != 0:
3315 # Formula to get the number of cuts that will make a curve
3316 # of N number of points have near to "minimum_points_num"
3317 # points, when subdividing with this number of cuts
3318 subdivide_cuts
= int(
3319 (minimum_points_num
- len(sp
.bezier_points
)) /
3320 (len(sp
.bezier_points
) - 1)
3325 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3326 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3328 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3330 # Detect if the strokes are a crosshatch and do it if it is
3331 self
.crosshatch_surface_invoke(self
.temporary_curve
)
3333 if not self
.is_crosshatch
:
3334 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3335 self
.temporary_curve
.select_set(True)
3336 bpy
.context
.view_layer
.objects
.active
= self
.temporary_curve
3338 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3340 # Set a minimum number of points for rectangular surfaces
3341 minimum_points_num
= 60
3343 # Check if the number of points of each curve has at least the number of points
3344 # of minimum_points_num, which is a bit more than the face-loops limit.
3345 # If not, subdivide to reach at least that number of points
3346 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3347 sp
= self
.temporary_curve
.data
.splines
[i
]
3349 if len(sp
.bezier_points
) < minimum_points_num
:
3350 for bp
in sp
.bezier_points
:
3351 bp
.select_control_point
= True
3353 if (len(sp
.bezier_points
) - 1) != 0:
3354 # Formula to get the number of cuts that will make a curve of
3355 # N number of points have near to "minimum_points_num" points,
3356 # when subdividing with this number of cuts
3357 subdivide_cuts
= int(
3358 (minimum_points_num
- len(sp
.bezier_points
)) /
3359 (len(sp
.bezier_points
) - 1)
3364 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3365 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3367 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3369 # Save coordinates of the actual strokes (as the "last saved splines")
3370 for sp_idx
in range(len(self
.temporary_curve
.data
.splines
)):
3371 self
.last_strokes_splines_coords
.append([])
3372 for bp_idx
in range(len(self
.temporary_curve
.data
.splines
[sp_idx
].bezier_points
)):
3373 coords
= self
.temporary_curve
.matrix_world
@ \
3374 self
.temporary_curve
.data
.splines
[sp_idx
].bezier_points
[bp_idx
].co
3375 self
.last_strokes_splines_coords
[sp_idx
].append([coords
[0], coords
[1], coords
[2]])
3377 # Check for cyclic splines, put the first and last points in the middle of their actual positions
3378 for sp_idx
in range(len(self
.temporary_curve
.data
.splines
)):
3379 if self
.temporary_curve
.data
.splines
[sp_idx
].use_cyclic_u
is True:
3380 first_p_co
= self
.last_strokes_splines_coords
[sp_idx
][0]
3381 last_p_co
= self
.last_strokes_splines_coords
[sp_idx
][
3382 len(self
.last_strokes_splines_coords
[sp_idx
]) - 1
3385 (first_p_co
[0] + last_p_co
[0]) / 2,
3386 (first_p_co
[1] + last_p_co
[1]) / 2,
3387 (first_p_co
[2] + last_p_co
[2]) / 2
3390 self
.last_strokes_splines_coords
[sp_idx
][0] = target_co
3391 self
.last_strokes_splines_coords
[sp_idx
][
3392 len(self
.last_strokes_splines_coords
[sp_idx
]) - 1
3394 tuple(self
.last_strokes_splines_coords
)
3396 # Estimation of the average length of the segments between
3397 # each point of the grease pencil strokes.
3398 # Will be useful to determine whether a curve should be made "Cyclic"
3399 segments_lengths_sum
= 0
3401 random_spline
= self
.temporary_curve
.data
.splines
[0].bezier_points
3402 for i
in range(0, len(random_spline
)):
3403 if i
!= 0 and len(random_spline
) - 1 >= i
:
3404 segments_lengths_sum
+= (random_spline
[i
- 1].co
- random_spline
[i
].co
).length
3407 self
.average_gp_segment_length
= segments_lengths_sum
/ segments_count
3409 # Delete temporary strokes curve object
3410 bpy
.ops
.object.delete({"selected_objects": [self
.temporary_curve
]})
3412 # If "Keep strokes" option is not active, delete original strokes curve object
3413 if not self
.stopping_errors
or self
.is_crosshatch
:
3414 bpy
.ops
.object.delete({"selected_objects": [self
.original_curve
]})
3416 # Delete grease pencil strokes
3417 if self
.strokes_type
== "GP_STROKES" and not self
.stopping_errors
:
3419 bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
.data
.layers
.active
.clear()
3423 # Delete grease pencil strokes
3424 if self
.strokes_type
== "GP_ANNOTATION" and not self
.stopping_errors
:
3426 bpy
.data
.grease_pencils
[0].layers
.active
.clear()
3430 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3431 self
.main_object
.select_set(True)
3432 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3434 # Set again since "execute()" will turn it again to its initial value
3435 self
.execute(context
)
3437 if not self
.stopping_errors
:
3442 elif self
.strokes_type
== "SELECTION_ALONE":
3443 self
.is_fill_faces
= True
3444 created_faces_count
= self
.fill_with_faces(self
.main_object
)
3446 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3448 if created_faces_count
== 0:
3449 self
.report({'WARNING'}, "There aren't any strokes attached to the object")
3450 return {"CANCELLED"}
3454 if self
.strokes_type
== "EXTERNAL_NO_CURVE":
3455 self
.report({'WARNING'}, "The secondary object is not a Curve.")
3458 elif self
.strokes_type
== "MORE_THAN_ONE_EXTERNAL":
3459 self
.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3462 elif self
.strokes_type
== "SINGLE_GP_STROKE_NO_SELECTION" or \
3463 self
.strokes_type
== "SINGLE_CURVE_STROKE_NO_SELECTION":
3465 self
.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
3468 elif self
.strokes_type
== "NO_STROKES":
3469 self
.report({'WARNING'}, "There aren't any strokes attached to the object")
3472 elif self
.strokes_type
== "CURVE_WITH_NON_BEZIER_SPLINES":
3473 self
.report({'WARNING'}, "All splines must be Bezier.")
3479 # Edit strokes operator
3480 class GPENCIL_OT_SURFSK_init(Operator
):
3481 bl_idname
= "gpencil.surfsk_init"
3482 bl_label
= "Bsurfaces initialize"
3483 bl_description
= "Bsurfaces initialize"
3485 active_object
: PointerProperty(type=bpy
.types
.Object
)
3487 def execute(self
, context
):
3489 bs
= bpy
.context
.scene
.bsurfaces
3491 if bpy
.ops
.object.mode_set
.poll():
3492 bpy
.ops
.object.mode_set(mode
='OBJECT')
3494 if bs
.SURFSK_object_with_retopology
== None:
3495 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3496 mesh
= bpy
.data
.meshes
.new('BSurfaceMesh')
3497 mesh_object
= object_utils
.object_data_add(context
, mesh
, operator
=None)
3498 mesh_object
.select_set(True)
3499 mesh_object
.show_all_edges
= True
3500 mesh_object
.show_in_front
= True
3501 mesh_object
.display_type
= 'SOLID'
3502 mesh_object
.show_wire
= True
3503 bpy
.context
.view_layer
.objects
.active
= mesh_object
3504 bpy
.ops
.object.modifier_add(type='SHRINKWRAP')
3505 modifier
= mesh_object
.modifiers
["Shrinkwrap"]
3506 if self
.active_object
is not None:
3507 modifier
.target
= self
.active_object
3508 modifier
.wrap_method
= 'TARGET_PROJECT'
3509 modifier
.wrap_mode
= 'OUTSIDE_SURFACE'
3510 #modifier.offset = 0.05
3512 bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_retopology
= mesh_object
3514 if context
.scene
.bsurfaces
.SURFSK_guide
== 'GPencil' and bs
.SURFSK_object_with_strokes
== None:
3515 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3516 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')
3517 bpy
.context
.scene
.tool_settings
.gpencil_stroke_placement_view3d
= 'SURFACE'
3518 gpencil_object
= bpy
.context
.scene
.objects
[bpy
.context
.scene
.objects
[-1].name
]
3519 gpencil_object
.select_set(True)
3520 bpy
.context
.view_layer
.objects
.active
= gpencil_object
3521 bpy
.ops
.object.mode_set(mode
='PAINT_GPENCIL')
3522 bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
= gpencil_object
3523 gpencil_object
.data
.stroke_depth_order
= '3D'
3525 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Annotation':
3526 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.annotate")
3527 bpy
.context
.scene
.tool_settings
.annotation_stroke_placement_view3d
= 'SURFACE'
3531 def invoke(self
, context
, event
):
3532 if bpy
.context
.active_object
:
3533 self
.active_object
= bpy
.context
.active_object
3535 self
.active_object
= None
3537 self
.execute(context
)
3541 # Edit surface operator
3542 class GPENCIL_OT_SURFSK_edit_surface(Operator
):
3543 bl_idname
= "gpencil.surfsk_edit_surface"
3544 bl_label
= "Bsurfaces edit surface"
3545 bl_description
= "Edit surface mesh"
3547 def execute(self
, context
):
3548 bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_retopology
.select_set(True)
3549 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_retopology
3550 bpy
.ops
.object.mode_set(mode
='EDIT')
3552 def invoke(self
, context
, event
):
3554 bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_retopology
.select_set(True)
3556 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3559 self
.execute(context
)
3563 # Add strokes operator
3564 class GPENCIL_OT_SURFSK_add_strokes(Operator
):
3565 bl_idname
= "gpencil.surfsk_add_strokes"
3566 bl_label
= "Bsurfaces add strokes"
3567 bl_description
= "Add the grease pencil strokes"
3569 def execute(self
, context
):
3570 # Determine the type of the strokes
3571 self
.strokes_type
= get_strokes_type(context
)
3572 # Check if strokes are grease pencil strokes or a curves object
3573 selected_objs
= bpy
.context
.selected_objects
3574 if self
.strokes_type
== "EXTERNAL_CURVE" or self
.strokes_type
== "SINGLE_CURVE_STROKE_NO_SELECTION":
3575 for ob
in selected_objs
:
3576 if ob
!= bpy
.context
.view_layer
.objects
.active
:
3579 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3581 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3582 curve_ob
.select_set(True)
3583 bpy
.context
.view_layer
.objects
.active
= curve_ob
3585 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3587 bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
.select_set(True)
3588 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
3589 bpy
.ops
.object.mode_set(mode
='PAINT_GPENCIL')
3593 def invoke(self
, context
, event
):
3595 bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
.select_set(True)
3597 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3600 self
.execute(context
)
3604 # Edit strokes operator
3605 class GPENCIL_OT_SURFSK_edit_strokes(Operator
):
3606 bl_idname
= "gpencil.surfsk_edit_strokes"
3607 bl_label
= "Bsurfaces edit strokes"
3608 bl_description
= "Edit the grease pencil strokes or curves used"
3610 def execute(self
, context
):
3611 # Determine the type of the strokes
3612 self
.strokes_type
= get_strokes_type(context
)
3613 # Check if strokes are grease pencil strokes or a curves object
3614 selected_objs
= bpy
.context
.selected_objects
3615 if self
.strokes_type
== "EXTERNAL_CURVE" or self
.strokes_type
== "SINGLE_CURVE_STROKE_NO_SELECTION":
3616 for ob
in selected_objs
:
3617 if ob
!= bpy
.context
.view_layer
.objects
.active
:
3620 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3622 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3623 curve_ob
.select_set(True)
3624 bpy
.context
.view_layer
.objects
.active
= curve_ob
3626 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3627 elif self
.strokes_type
== "GP_STROKES" or self
.strokes_type
== "SINGLE_GP_STROKE_NO_SELECTION":
3628 # Convert grease pencil strokes to curve
3629 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3630 #bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
3631 gp
= bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
3632 conver_gpencil_to_curve(self
, context
, gp
, 'GPensil')
3633 for ob
in bpy
.context
.selected_objects
:
3634 if ob
!= bpy
.context
.view_layer
.objects
.active
and ob
.name
.startswith("GP_Layer"):
3637 ob_gp_strokes
= bpy
.context
.object
3639 # Delete grease pencil strokes
3641 bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
.data
.layers
.active
.clear()
3646 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3647 ob_gp_strokes
.select_set(True)
3648 bpy
.context
.view_layer
.objects
.active
= ob_gp_strokes
3650 curve_crv
= ob_gp_strokes
.data
3651 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3652 bpy
.ops
.curve
.spline_type_set('INVOKE_REGION_WIN', type="BEZIER")
3653 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type="AUTOMATIC")
3654 #curve_crv.show_handles = False
3655 #curve_crv.show_normal_face = False
3657 elif self
.strokes_type
== "EXTERNAL_NO_CURVE":
3658 self
.report({'WARNING'}, "The secondary object is not a Curve.")
3661 elif self
.strokes_type
== "MORE_THAN_ONE_EXTERNAL":
3662 self
.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3665 elif self
.strokes_type
== "NO_STROKES" or self
.strokes_type
== "SELECTION_ALONE":
3666 self
.report({'WARNING'}, "There aren't any strokes attached to the object")
3672 def invoke(self
, context
, event
):
3674 bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
.select_set(True)
3676 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3679 self
.execute(context
)
3684 class GPENCIL_OT_SURFSK_add_annotation(Operator
):
3685 bl_idname
= "gpencil.surfsk_add_annotation"
3686 bl_label
= "Bsurfaces add annotation"
3687 bl_description
= "Add annotation"
3689 def execute(self
, context
):
3690 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.annotate")
3691 bpy
.context
.scene
.tool_settings
.annotation_stroke_placement_view3d
= 'SURFACE'
3695 def invoke(self
, context
, event
):
3697 self
.execute(context
)
3701 class CURVE_OT_SURFSK_reorder_splines(Operator
):
3702 bl_idname
= "curve.surfsk_reorder_splines"
3703 bl_label
= "Bsurfaces reorder splines"
3704 bl_description
= "Defines the order of the splines by using grease pencil strokes"
3705 bl_options
= {'REGISTER', 'UNDO'}
3707 def execute(self
, context
):
3708 objects_to_delete
= []
3709 # Convert grease pencil strokes to curve.
3710 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3711 bpy
.ops
.gpencil
.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes
=False)
3712 for ob
in bpy
.context
.selected_objects
:
3713 if ob
!= bpy
.context
.view_layer
.objects
.active
and ob
.name
.startswith("GP_Layer"):
3714 GP_strokes_curve
= ob
3716 # GP_strokes_curve = bpy.context.object
3717 objects_to_delete
.append(GP_strokes_curve
)
3719 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3720 GP_strokes_curve
.select_set(True)
3721 bpy
.context
.view_layer
.objects
.active
= GP_strokes_curve
3723 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3724 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
3725 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=100)
3726 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3728 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3729 GP_strokes_mesh
= bpy
.context
.object
3730 objects_to_delete
.append(GP_strokes_mesh
)
3732 GP_strokes_mesh
.data
.resolution_u
= 1
3733 bpy
.ops
.object.convert(target
='MESH', keep_original
=False)
3735 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3736 self
.main_curve
.select_set(True)
3737 bpy
.context
.view_layer
.objects
.active
= self
.main_curve
3739 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3740 curves_duplicate_1
= bpy
.context
.object
3741 objects_to_delete
.append(curves_duplicate_1
)
3743 minimum_points_num
= 500
3745 # Some iterations since the subdivision operator
3746 # has a limit of 100 subdivisions per iteration
3747 for x
in range(round(minimum_points_num
/ 100)):
3748 # Check if the number of points of each curve has at least the number of points
3749 # of minimum_points_num. If not, subdivide to reach at least that number of points
3750 for i
in range(len(curves_duplicate_1
.data
.splines
)):
3751 sp
= curves_duplicate_1
.data
.splines
[i
]
3753 if len(sp
.bezier_points
) < minimum_points_num
:
3754 for bp
in sp
.bezier_points
:
3755 bp
.select_control_point
= True
3757 if (len(sp
.bezier_points
) - 1) != 0:
3758 # Formula to get the number of cuts that will make a curve of N
3759 # number of points have near to "minimum_points_num" points,
3760 # when subdividing with this number of cuts
3761 subdivide_cuts
= int(
3762 (minimum_points_num
- len(sp
.bezier_points
)) /
3763 (len(sp
.bezier_points
) - 1)
3768 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3769 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3770 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3771 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3773 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3774 curves_duplicate_2
= bpy
.context
.object
3775 objects_to_delete
.append(curves_duplicate_2
)
3777 # Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target
3778 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3779 curves_duplicate_2
.select_set(True)
3780 bpy
.context
.view_layer
.objects
.active
= curves_duplicate_2
3782 bpy
.ops
.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
3783 curves_duplicate_2
.modifiers
["Shrinkwrap"].wrap_method
= "NEAREST_VERTEX"
3784 curves_duplicate_2
.modifiers
["Shrinkwrap"].target
= GP_strokes_mesh
3785 bpy
.ops
.object.modifier_apply('INVOKE_REGION_WIN', apply_as
='DATA', modifier
='Shrinkwrap')
3787 # Get the distance of each vert from its original position to its position with Shrinkwrap
3788 nearest_points_coords
= {}
3789 for st_idx
in range(len(curves_duplicate_1
.data
.splines
)):
3790 for bp_idx
in range(len(curves_duplicate_1
.data
.splines
[st_idx
].bezier_points
)):
3791 bp_1_co
= curves_duplicate_1
.matrix_world
@ \
3792 curves_duplicate_1
.data
.splines
[st_idx
].bezier_points
[bp_idx
].co
3794 bp_2_co
= curves_duplicate_2
.matrix_world
@ \
3795 curves_duplicate_2
.data
.splines
[st_idx
].bezier_points
[bp_idx
].co
3798 shortest_dist
= (bp_1_co
- bp_2_co
).length
3799 nearest_points_coords
[st_idx
] = ("%.4f" % bp_2_co
[0],
3800 "%.4f" % bp_2_co
[1],
3801 "%.4f" % bp_2_co
[2])
3803 dist
= (bp_1_co
- bp_2_co
).length
3805 if dist
< shortest_dist
:
3806 nearest_points_coords
[st_idx
] = ("%.4f" % bp_2_co
[0],
3807 "%.4f" % bp_2_co
[1],
3808 "%.4f" % bp_2_co
[2])
3809 shortest_dist
= dist
3811 # Get all coords of GP strokes points, for comparison
3812 GP_strokes_coords
= []
3813 for st_idx
in range(len(GP_strokes_curve
.data
.splines
)):
3814 GP_strokes_coords
.append(
3815 [("%.4f" % x
if "%.4f" % x
!= "-0.00" else "0.00",
3816 "%.4f" % y
if "%.4f" % y
!= "-0.00" else "0.00",
3817 "%.4f" % z
if "%.4f" % z
!= "-0.00" else "0.00") for
3818 x
, y
, z
in [bp
.co
for bp
in GP_strokes_curve
.data
.splines
[st_idx
].bezier_points
]]
3821 # Check the point of the GP strokes with the same coords as
3822 # the nearest points of the curves (with shrinkwrap)
3824 # Dictionary with GP stroke index as index, and a list as value.
3825 # The list has as index the point index of the GP stroke
3826 # nearest to the spline, and as value the spline index
3827 GP_connection_points
= {}
3828 for gp_st_idx
in range(len(GP_strokes_coords
)):
3829 GPvert_spline_relationship
= {}
3831 for splines_st_idx
in range(len(nearest_points_coords
)):
3832 if nearest_points_coords
[splines_st_idx
] in GP_strokes_coords
[gp_st_idx
]:
3833 GPvert_spline_relationship
[
3834 GP_strokes_coords
[gp_st_idx
].index(nearest_points_coords
[splines_st_idx
])
3837 GP_connection_points
[gp_st_idx
] = GPvert_spline_relationship
3839 # Get the splines new order
3840 splines_new_order
= []
3841 for i
in GP_connection_points
:
3842 dict_keys
= sorted(GP_connection_points
[i
].keys()) # Sort dictionaries by key
3845 splines_new_order
.append(GP_connection_points
[i
][k
])
3848 curve_original_name
= self
.main_curve
.name
3850 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3851 self
.main_curve
.select_set(True)
3852 bpy
.context
.view_layer
.objects
.active
= self
.main_curve
3854 self
.main_curve
.name
= "SURFSKIO_CRV_ORD"
3856 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3857 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3858 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3860 for sp_idx
in range(len(self
.main_curve
.data
.splines
)):
3861 self
.main_curve
.data
.splines
[0].bezier_points
[0].select_control_point
= True
3863 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3864 bpy
.ops
.curve
.separate('EXEC_REGION_WIN')
3865 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3867 # Get the names of the separated splines objects in the original order
3868 splines_unordered
= {}
3869 for o
in bpy
.data
.objects
:
3870 if o
.name
.find("SURFSKIO_CRV_ORD") != -1:
3871 spline_order_string
= o
.name
.partition(".")[2]
3873 if spline_order_string
!= "" and int(spline_order_string
) > 0:
3874 spline_order_index
= int(spline_order_string
) - 1
3875 splines_unordered
[spline_order_index
] = o
.name
3877 # Join all splines objects in final order
3878 for order_idx
in splines_new_order
:
3879 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3880 bpy
.data
.objects
[splines_unordered
[order_idx
]].select_set(True)
3881 bpy
.data
.objects
["SURFSKIO_CRV_ORD"].select_set(True)
3882 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
["SURFSKIO_CRV_ORD"]
3884 bpy
.ops
.object.join('INVOKE_REGION_WIN')
3886 # Go back to the original name of the curves object.
3887 bpy
.context
.object.name
= curve_original_name
3889 # Delete all unused objects
3890 bpy
.ops
.object.delete({"selected_objects": objects_to_delete
})
3892 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3893 bpy
.data
.objects
[curve_original_name
].select_set(True)
3894 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[curve_original_name
]
3896 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3897 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3900 bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
.data
.layers
.active
.clear()
3907 def invoke(self
, context
, event
):
3908 self
.main_curve
= bpy
.context
.object
3909 there_are_GP_strokes
= False
3912 # Get the active grease pencil layer
3913 strokes_num
= len(self
.main_curve
.grease_pencil
.layers
.active
.active_frame
.strokes
)
3916 there_are_GP_strokes
= True
3920 if there_are_GP_strokes
:
3921 self
.execute(context
)
3922 self
.report({'INFO'}, "Splines have been reordered")
3924 self
.report({'WARNING'}, "Draw grease pencil strokes to connect splines")
3929 class CURVE_OT_SURFSK_first_points(Operator
):
3930 bl_idname
= "curve.surfsk_first_points"
3931 bl_label
= "Bsurfaces set first points"
3932 bl_description
= "Set the selected points as the first point of each spline"
3933 bl_options
= {'REGISTER', 'UNDO'}
3935 def execute(self
, context
):
3936 splines_to_invert
= []
3938 # Check non-cyclic splines to invert
3939 for i
in range(len(self
.main_curve
.data
.splines
)):
3940 b_points
= self
.main_curve
.data
.splines
[i
].bezier_points
3942 if i
not in self
.cyclic_splines
: # Only for non-cyclic splines
3943 if b_points
[len(b_points
) - 1].select_control_point
:
3944 splines_to_invert
.append(i
)
3946 # Reorder points of cyclic splines, and set all handles to "Automatic"
3948 # Check first selected point
3949 cyclic_splines_new_first_pt
= {}
3950 for i
in self
.cyclic_splines
:
3951 sp
= self
.main_curve
.data
.splines
[i
]
3953 for t
in range(len(sp
.bezier_points
)):
3954 bp
= sp
.bezier_points
[t
]
3955 if bp
.select_control_point
or bp
.select_right_handle
or bp
.select_left_handle
:
3956 cyclic_splines_new_first_pt
[i
] = t
3957 break # To take only one if there are more
3960 for spline_idx
in cyclic_splines_new_first_pt
:
3961 sp
= self
.main_curve
.data
.splines
[spline_idx
]
3963 spline_old_coords
= []
3964 for bp_old
in sp
.bezier_points
:
3965 coords
= (bp_old
.co
[0], bp_old
.co
[1], bp_old
.co
[2])
3967 left_handle_type
= str(bp_old
.handle_left_type
)
3968 left_handle_length
= float(bp_old
.handle_left
.length
)
3970 float(bp_old
.handle_left
.x
),
3971 float(bp_old
.handle_left
.y
),
3972 float(bp_old
.handle_left
.z
)
3974 right_handle_type
= str(bp_old
.handle_right_type
)
3975 right_handle_length
= float(bp_old
.handle_right
.length
)
3976 right_handle_xyz
= (
3977 float(bp_old
.handle_right
.x
),
3978 float(bp_old
.handle_right
.y
),
3979 float(bp_old
.handle_right
.z
)
3981 spline_old_coords
.append(
3982 [coords
, left_handle_type
,
3983 right_handle_type
, left_handle_length
,
3984 right_handle_length
, left_handle_xyz
,
3988 for t
in range(len(sp
.bezier_points
)):
3989 bp
= sp
.bezier_points
3991 if t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1 <= len(bp
) - 1:
3992 new_index
= t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1
3994 new_index
= t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1 - len(bp
)
3996 bp
[t
].co
= Vector(spline_old_coords
[new_index
][0])
3998 bp
[t
].handle_left
.length
= spline_old_coords
[new_index
][3]
3999 bp
[t
].handle_right
.length
= spline_old_coords
[new_index
][4]
4001 bp
[t
].handle_left_type
= "FREE"
4002 bp
[t
].handle_right_type
= "FREE"
4004 bp
[t
].handle_left
.x
= spline_old_coords
[new_index
][5][0]
4005 bp
[t
].handle_left
.y
= spline_old_coords
[new_index
][5][1]
4006 bp
[t
].handle_left
.z
= spline_old_coords
[new_index
][5][2]
4008 bp
[t
].handle_right
.x
= spline_old_coords
[new_index
][6][0]
4009 bp
[t
].handle_right
.y
= spline_old_coords
[new_index
][6][1]
4010 bp
[t
].handle_right
.z
= spline_old_coords
[new_index
][6][2]
4012 bp
[t
].handle_left_type
= spline_old_coords
[new_index
][1]
4013 bp
[t
].handle_right_type
= spline_old_coords
[new_index
][2]
4015 # Invert the non-cyclic splines designated above
4016 for i
in range(len(splines_to_invert
)):
4017 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4019 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4020 self
.main_curve
.data
.splines
[splines_to_invert
[i
]].bezier_points
[0].select_control_point
= True
4021 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4023 bpy
.ops
.curve
.switch_direction()
4025 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4027 # Keep selected the first vert of each spline
4028 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4029 for i
in range(len(self
.main_curve
.data
.splines
)):
4030 if not self
.main_curve
.data
.splines
[i
].use_cyclic_u
:
4031 bp
= self
.main_curve
.data
.splines
[i
].bezier_points
[0]
4033 bp
= self
.main_curve
.data
.splines
[i
].bezier_points
[
4034 len(self
.main_curve
.data
.splines
[i
].bezier_points
) - 1
4037 bp
.select_control_point
= True
4038 bp
.select_right_handle
= True
4039 bp
.select_left_handle
= True
4041 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4045 def invoke(self
, context
, event
):
4046 self
.main_curve
= bpy
.context
.object
4048 # Check if all curves are Bezier, and detect which ones are cyclic
4049 self
.cyclic_splines
= []
4050 for i
in range(len(self
.main_curve
.data
.splines
)):
4051 if self
.main_curve
.data
.splines
[i
].type != "BEZIER":
4052 self
.report({'WARNING'}, "All splines must be Bezier type")
4054 return {'CANCELLED'}
4056 if self
.main_curve
.data
.splines
[i
].use_cyclic_u
:
4057 self
.cyclic_splines
.append(i
)
4059 self
.execute(context
)
4060 self
.report({'INFO'}, "First points have been set")
4065 # Add-ons Preferences Update Panel
4067 # Define Panel classes for updating
4069 VIEW3D_PT_tools_SURFSK_mesh
,
4070 VIEW3D_PT_tools_SURFSK_curve
4074 def update_panel(self
, context
):
4075 message
= "Bsurfaces GPL Edition: Updating Panel locations has failed"
4077 for panel
in panels
:
4078 if "bl_rna" in panel
.__dict
__:
4079 bpy
.utils
.unregister_class(panel
)
4081 for panel
in panels
:
4082 panel
.bl_category
= context
.preferences
.addons
[__name__
].preferences
.category
4083 bpy
.utils
.register_class(panel
)
4085 except Exception as e
:
4086 print("\n[{}]\n{}\n\nError:\n{}".format(__name__
, message
, e
))
4089 def conver_gpencil_to_curve(self
, context
, pencil
, type):
4090 newCurve
= bpy
.data
.curves
.new('gpencil_curve', type='CURVE')
4091 newCurve
.dimensions
= '3D'
4092 CurveObject
= object_utils
.object_data_add(context
, newCurve
)
4094 if type == 'GPensil':
4095 strokes
= pencil
.data
.layers
.active
.active_frame
.strokes
4096 CurveObject
.location
= pencil
.location
4097 CurveObject
.rotation_euler
= pencil
.rotation_euler
4098 CurveObject
.scale
= pencil
.scale
4099 elif type == 'Annotation':
4100 grease_pencil
= bpy
.data
.grease_pencils
[0]
4101 strokes
= grease_pencil
.layers
.active
.active_frame
.strokes
4102 CurveObject
.location
= (0.0, 0.0, 0.0)
4103 CurveObject
.rotation_euler
= (0.0, 0.0, 0.0)
4104 CurveObject
.scale
= (1.0, 1.0, 1.0)
4106 for i
, stroke
in enumerate(strokes
):
4107 stroke_points
= strokes
[i
].points
4108 data_list
= [ (point
.co
.x
, point
.co
.y
, point
.co
.z
)
4109 for point
in stroke_points
]
4110 points_to_add
= len(data_list
)-1
4113 for point
in data_list
:
4114 flat_list
.extend(point
)
4116 spline
= newCurve
.splines
.new(type='BEZIER')
4117 spline
.bezier_points
.add(points_to_add
)
4118 spline
.bezier_points
.foreach_set("co", flat_list
)
4120 for point
in spline
.bezier_points
:
4121 point
.handle_left_type
="AUTO"
4122 point
.handle_right_type
="AUTO"
4126 class BsurfPreferences(AddonPreferences
):
4127 # this must match the addon name, use '__package__'
4128 # when defining this in a submodule of a python package.
4129 bl_idname
= __name__
4131 category
: StringProperty(
4132 name
="Tab Category",
4133 description
="Choose a name for the category of the panel",
4138 def draw(self
, context
):
4139 layout
= self
.layout
4143 col
.label(text
="Tab Category:")
4144 col
.prop(self
, "category", text
="")
4147 class BsurfacesProps(PropertyGroup
):
4148 SURFSK_guide
: EnumProperty(
4151 ('Annotation', 'Annotation', 'Annotation'),
4152 ('GPencil', 'GPencil', 'GPencil'),
4153 ('Curve', 'Curve', 'Curve')
4155 default
="Annotation"
4157 SURFSK_edges_U
: IntProperty(
4159 description
="Number of face-loops crossing the strokes",
4164 SURFSK_edges_V
: IntProperty(
4166 description
="Number of face-loops following the strokes",
4171 SURFSK_cyclic_cross
: BoolProperty(
4172 name
="Cyclic Cross",
4173 description
="Make cyclic the face-loops crossing the strokes",
4176 SURFSK_cyclic_follow
: BoolProperty(
4177 name
="Cyclic Follow",
4178 description
="Make cyclic the face-loops following the strokes",
4181 SURFSK_keep_strokes
: BoolProperty(
4182 name
="Keep strokes",
4183 description
="Keeps the sketched strokes or curves after adding the surface",
4186 SURFSK_automatic_join
: BoolProperty(
4187 name
="Automatic join",
4188 description
="Join automatically vertices of either surfaces "
4189 "generated by crosshatching, or from the borders of closed shapes",
4192 SURFSK_loops_on_strokes
: BoolProperty(
4193 name
="Loops on strokes",
4194 description
="Make the loops match the paths of the strokes",
4197 SURFSK_precision
: IntProperty(
4199 description
="Precision level of the surface calculation",
4204 SURFSK_object_with_retopology
: PointerProperty(
4205 name
="Mesh of BSurface",
4206 type=bpy
.types
.Object
4208 SURFSK_object_with_strokes
: PointerProperty(
4209 name
="GPensil or Curve object",
4210 type=bpy
.types
.Object
4214 GPENCIL_OT_SURFSK_init
,
4215 GPENCIL_OT_SURFSK_add_surface
,
4216 GPENCIL_OT_SURFSK_edit_surface
,
4217 GPENCIL_OT_SURFSK_add_strokes
,
4218 GPENCIL_OT_SURFSK_edit_strokes
,
4219 GPENCIL_OT_SURFSK_add_annotation
,
4220 CURVE_OT_SURFSK_reorder_splines
,
4221 CURVE_OT_SURFSK_first_points
,
4228 bpy
.utils
.register_class(cls
)
4230 bpy
.types
.Scene
.bsurfaces
= PointerProperty(type=BsurfacesProps
)
4231 update_panel(None, bpy
.context
)
4235 bpy
.utils
.unregister_class(cls
)
4237 del bpy
.types
.Scene
.bsurfaces
4239 if __name__
== "__main__":