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