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, Vladimir Spivak (cwolf3d)",
24 "blender": (2, 80, 0),
25 "location": "View3D EditMode > Sidebar > Edit Tab",
26 "description": "Modeling and retopology tool",
27 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/bsurfaces.html",
34 from bpy_extras
import object_utils
37 from mathutils
import Matrix
, Vector
38 from mathutils
.geometry
import (
47 from bpy
.props
import (
56 from bpy
.types
import (
63 # ----------------------------
65 global_color
= [1.0, 0.0, 0.0, 0.3]
67 global_in_front
= False
68 global_shade_smooth
= False
69 global_show_wire
= True
70 global_mesh_object
= ""
71 global_gpencil_object
= ""
72 global_curve_object
= ""
74 # ----------------------------
76 class VIEW3D_PT_tools_SURFSK_mesh(Panel
):
77 bl_space_type
= 'VIEW_3D'
80 bl_label
= "Bsurfaces"
82 def draw(self
, context
):
84 scn
= context
.scene
.bsurfaces
86 col
= layout
.column(align
=True)
89 col
.operator("mesh.surfsk_init", text
="Initialize (Add BSurface mesh)")
90 col
.operator("mesh.surfsk_add_modifiers", text
="Add Mirror and others modifiers")
92 col
.label(text
="Mesh of BSurface:")
93 col
.prop(scn
, "SURFSK_mesh", text
="")
94 col
.prop(scn
, "SURFSK_mesh_color")
95 col
.prop(scn
, "SURFSK_Shrinkwrap_offset")
96 col
.prop(scn
, "SURFSK_in_front")
97 col
.prop(scn
, "SURFSK_shade_smooth")
98 col
.prop(scn
, "SURFSK_show_wire")
100 col
.label(text
="Guide strokes:")
101 col
.row().prop(scn
, "SURFSK_guide", expand
=True)
102 if scn
.SURFSK_guide
== 'GPencil':
103 col
.prop(scn
, "SURFSK_gpencil", text
="")
105 if scn
.SURFSK_guide
== 'Curve':
106 col
.prop(scn
, "SURFSK_curve", text
="")
110 col
.operator("mesh.surfsk_add_surface", text
="Add Surface")
111 col
.operator("mesh.surfsk_edit_surface", text
="Edit Surface")
114 if scn
.SURFSK_guide
== 'GPencil':
115 col
.operator("gpencil.surfsk_add_strokes", text
="Add Strokes")
116 col
.operator("gpencil.surfsk_edit_strokes", text
="Edit Strokes")
118 col
.operator("gpencil.surfsk_strokes_to_curves", text
="Strokes to curves")
120 if scn
.SURFSK_guide
== 'Annotation':
121 col
.operator("gpencil.surfsk_add_annotation", text
="Add Annotation")
123 col
.operator("gpencil.surfsk_annotations_to_curves", text
="Annotation to curves")
125 if scn
.SURFSK_guide
== 'Curve':
126 col
.operator("curve.surfsk_edit_curve", text
="Edit curve")
129 col
.label(text
="Initial settings:")
130 col
.prop(scn
, "SURFSK_edges_U")
131 col
.prop(scn
, "SURFSK_edges_V")
132 col
.prop(scn
, "SURFSK_cyclic_cross")
133 col
.prop(scn
, "SURFSK_cyclic_follow")
134 col
.prop(scn
, "SURFSK_loops_on_strokes")
135 col
.prop(scn
, "SURFSK_automatic_join")
136 col
.prop(scn
, "SURFSK_keep_strokes")
138 class VIEW3D_PT_tools_SURFSK_curve(Panel
):
139 bl_space_type
= 'VIEW_3D'
140 bl_region_type
= 'UI'
141 bl_context
= "curve_edit"
143 bl_label
= "Bsurfaces"
146 def poll(cls
, context
):
147 return context
.active_object
149 def draw(self
, context
):
152 col
= layout
.column(align
=True)
155 col
.operator("curve.surfsk_first_points", text
="Set First Points")
156 col
.operator("curve.switch_direction", text
="Switch Direction")
157 col
.operator("curve.surfsk_reorder_splines", text
="Reorder Splines")
160 # ----------------------------
161 # Returns the type of strokes used
162 def get_strokes_type(context
):
163 strokes_type
= "NO_STROKES"
166 # Check if they are annotation
167 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Annotation':
169 strokes
= bpy
.context
.annotation_data
.layers
.active
.active_frame
.strokes
171 strokes_num
= len(strokes
)
174 strokes_type
= "GP_ANNOTATION"
176 strokes_type
= "NO_STROKES"
178 # Check if they are grease pencil
179 if context
.scene
.bsurfaces
.SURFSK_guide
== 'GPencil':
181 global global_gpencil_object
182 gpencil
= bpy
.data
.objects
[global_gpencil_object
]
183 strokes
= gpencil
.data
.layers
.active
.active_frame
.strokes
185 strokes_num
= len(strokes
)
188 strokes_type
= "GP_STROKES"
190 strokes_type
= "NO_STROKES"
192 # Check if they are curves, if there aren't grease pencil strokes
193 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Curve':
195 global global_curve_object
196 ob
= bpy
.data
.objects
[global_curve_object
]
197 if ob
.type == "CURVE":
198 strokes_type
= "EXTERNAL_CURVE"
199 strokes_num
= len(ob
.data
.splines
)
201 # Check if there is any non-bezier spline
202 for i
in range(len(ob
.data
.splines
)):
203 if ob
.data
.splines
[i
].type != "BEZIER":
204 strokes_type
= "CURVE_WITH_NON_BEZIER_SPLINES"
208 strokes_type
= "EXTERNAL_NO_CURVE"
210 strokes_type
= "NO_STROKES"
212 # Check if they are mesh
214 global global_mesh_object
215 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
216 total_vert_sel
= len([v
for v
in self
.main_object
.data
.vertices
if v
.select
])
218 # Check if there is a single stroke without any selection in the object
219 if strokes_num
== 1 and total_vert_sel
== 0:
220 if strokes_type
== "EXTERNAL_CURVE":
221 strokes_type
= "SINGLE_CURVE_STROKE_NO_SELECTION"
222 elif strokes_type
== "GP_STROKES":
223 strokes_type
= "SINGLE_GP_STROKE_NO_SELECTION"
225 if strokes_num
== 0 and total_vert_sel
> 0:
226 strokes_type
= "SELECTION_ALONE"
232 # ----------------------------
233 # Surface generator operator
234 class MESH_OT_SURFSK_add_surface(Operator
):
235 bl_idname
= "mesh.surfsk_add_surface"
236 bl_label
= "Bsurfaces add surface"
237 bl_description
= "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
238 bl_options
= {'REGISTER', 'UNDO'}
240 is_crosshatch
: BoolProperty(
243 is_fill_faces
: BoolProperty(
246 selection_U_exists
: BoolProperty(
249 selection_V_exists
: BoolProperty(
252 selection_U2_exists
: BoolProperty(
255 selection_V2_exists
: BoolProperty(
258 selection_V_is_closed
: BoolProperty(
261 selection_U_is_closed
: BoolProperty(
264 selection_V2_is_closed
: BoolProperty(
267 selection_U2_is_closed
: BoolProperty(
271 edges_U
: IntProperty(
273 description
="Number of face-loops crossing the strokes",
278 edges_V
: IntProperty(
280 description
="Number of face-loops following the strokes",
285 cyclic_cross
: BoolProperty(
287 description
="Make cyclic the face-loops crossing the strokes",
290 cyclic_follow
: BoolProperty(
291 name
="Cyclic Follow",
292 description
="Make cyclic the face-loops following the strokes",
295 loops_on_strokes
: BoolProperty(
296 name
="Loops on strokes",
297 description
="Make the loops match the paths of the strokes",
300 automatic_join
: BoolProperty(
301 name
="Automatic join",
302 description
="Join automatically vertices of either surfaces generated "
303 "by crosshatching, or from the borders of closed shapes",
306 join_stretch_factor
: FloatProperty(
308 description
="Amount of stretching or shrinking allowed for "
309 "edges when joining vertices automatically",
315 keep_strokes
: BoolProperty(
317 description
="Keeps the sketched strokes or curves after adding the surface",
320 strokes_type
: StringProperty()
321 initial_global_undo_state
: BoolProperty()
324 def draw(self
, context
):
326 col
= layout
.column(align
=True)
329 if not self
.is_fill_faces
:
331 if not self
.is_crosshatch
:
332 if not self
.selection_U_exists
:
333 col
.prop(self
, "edges_U")
336 if not self
.selection_V_exists
:
337 col
.prop(self
, "edges_V")
342 if not self
.selection_U_exists
:
344 (self
.selection_V_exists
and not self
.selection_V_is_closed
) or
345 (self
.selection_V2_exists
and not self
.selection_V2_is_closed
)
347 col
.prop(self
, "cyclic_cross")
349 if not self
.selection_V_exists
:
351 (self
.selection_U_exists
and not self
.selection_U_is_closed
) or
352 (self
.selection_U2_exists
and not self
.selection_U2_is_closed
)
354 col
.prop(self
, "cyclic_follow")
356 col
.prop(self
, "loops_on_strokes")
358 col
.prop(self
, "automatic_join")
360 if self
.automatic_join
:
364 col
.prop(self
, "join_stretch_factor")
366 col
.prop(self
, "keep_strokes")
368 # Get an ordered list of a chain of vertices
369 def get_ordered_verts(self
, ob
, all_selected_edges_idx
, all_selected_verts_idx
,
370 first_vert_idx
, middle_vertex_idx
, closing_vert_idx
):
371 # Order selected vertices.
373 if closing_vert_idx
is not None:
374 verts_ordered
.append(ob
.data
.vertices
[closing_vert_idx
])
376 verts_ordered
.append(ob
.data
.vertices
[first_vert_idx
])
377 prev_v
= first_vert_idx
381 edges_non_matched
= 0
382 for i
in all_selected_edges_idx
:
383 if ob
.data
.edges
[i
] != prev_ed
and ob
.data
.edges
[i
].vertices
[0] == prev_v
and \
384 ob
.data
.edges
[i
].vertices
[1] in all_selected_verts_idx
:
386 verts_ordered
.append(ob
.data
.vertices
[ob
.data
.edges
[i
].vertices
[1]])
387 prev_v
= ob
.data
.edges
[i
].vertices
[1]
388 prev_ed
= ob
.data
.edges
[i
]
389 elif ob
.data
.edges
[i
] != prev_ed
and ob
.data
.edges
[i
].vertices
[1] == prev_v
and \
390 ob
.data
.edges
[i
].vertices
[0] in all_selected_verts_idx
:
392 verts_ordered
.append(ob
.data
.vertices
[ob
.data
.edges
[i
].vertices
[0]])
393 prev_v
= ob
.data
.edges
[i
].vertices
[0]
394 prev_ed
= ob
.data
.edges
[i
]
396 edges_non_matched
+= 1
398 if edges_non_matched
== len(all_selected_edges_idx
):
404 if closing_vert_idx
is not None:
405 verts_ordered
.append(ob
.data
.vertices
[closing_vert_idx
])
407 if middle_vertex_idx
is not None:
408 verts_ordered
.append(ob
.data
.vertices
[middle_vertex_idx
])
409 verts_ordered
.reverse()
411 return tuple(verts_ordered
)
413 # Calculates length of a chain of points.
414 def get_chain_length(self
, object, verts_ordered
):
415 matrix
= object.matrix_world
418 edges_lengths_sum
= 0
419 for i
in range(0, len(verts_ordered
)):
421 prev_v_co
= matrix
@ verts_ordered
[i
].co
423 v_co
= matrix
@ verts_ordered
[i
].co
425 v_difs
= [prev_v_co
[0] - v_co
[0], prev_v_co
[1] - v_co
[1], prev_v_co
[2] - v_co
[2]]
426 edge_length
= abs(sqrt(v_difs
[0] * v_difs
[0] + v_difs
[1] * v_difs
[1] + v_difs
[2] * v_difs
[2]))
428 edges_lengths
.append(edge_length
)
429 edges_lengths_sum
+= edge_length
433 return edges_lengths
, edges_lengths_sum
435 # Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
436 def get_edges_proportions(self
, edges_lengths
, edges_lengths_sum
, use_boundaries
, fixed_edges_num
):
437 edges_proportions
= []
440 for l
in edges_lengths
:
441 edges_proportions
.append(l
/ edges_lengths_sum
)
445 for _n
in range(0, fixed_edges_num
):
446 edges_proportions
.append(1 / fixed_edges_num
)
449 return edges_proportions
451 # Calculates the angle between two pairs of points in space
452 def orientation_difference(self
, points_A_co
, points_B_co
):
453 # each parameter should be a list with two elements,
454 # and each element should be a x,y,z coordinate
455 vec_A
= points_A_co
[0] - points_A_co
[1]
456 vec_B
= points_B_co
[0] - points_B_co
[1]
458 angle
= vec_A
.angle(vec_B
)
461 angle
= abs(angle
- pi
)
465 # Calculate the which vert of verts_idx list is the nearest one
466 # to the point_co coordinates, and the distance
467 def shortest_distance(self
, object, point_co
, verts_idx
):
468 matrix
= object.matrix_world
470 for i
in range(0, len(verts_idx
)):
471 dist
= (point_co
- matrix
@ object.data
.vertices
[verts_idx
[i
]].co
).length
474 nearest_vert_idx
= verts_idx
[i
]
479 nearest_vert_idx
= verts_idx
[i
]
482 return nearest_vert_idx
, shortest_dist
484 # Returns the index of the opposite vert tip in a chain, given a vert tip index
485 # as parameter, and a multidimentional list with all pairs of tips
486 def opposite_tip(self
, vert_tip_idx
, all_chains_tips_idx
):
487 opposite_vert_tip_idx
= None
488 for i
in range(0, len(all_chains_tips_idx
)):
489 if vert_tip_idx
== all_chains_tips_idx
[i
][0]:
490 opposite_vert_tip_idx
= all_chains_tips_idx
[i
][1]
491 if vert_tip_idx
== all_chains_tips_idx
[i
][1]:
492 opposite_vert_tip_idx
= all_chains_tips_idx
[i
][0]
494 return opposite_vert_tip_idx
496 # Simplifies a spline and returns the new points coordinates
497 def simplify_spline(self
, spline_coords
, segments_num
):
498 simplified_spline
= []
499 points_between_segments
= round(len(spline_coords
) / segments_num
)
501 simplified_spline
.append(spline_coords
[0])
502 for i
in range(1, segments_num
):
503 simplified_spline
.append(spline_coords
[i
* points_between_segments
])
505 simplified_spline
.append(spline_coords
[len(spline_coords
) - 1])
507 return simplified_spline
509 # Returns a list with the coords of the points distributed over the splines
510 # passed to this method according to the proportions parameter
511 def distribute_pts(self
, surface_splines
, proportions
):
513 # Calculate the length of each final surface spline
514 surface_splines_lengths
= []
515 surface_splines_parsed
= []
517 for sp_idx
in range(0, len(surface_splines
)):
518 # Calculate spline length
519 surface_splines_lengths
.append(0)
521 for i
in range(0, len(surface_splines
[sp_idx
].bezier_points
)):
523 prev_p
= surface_splines
[sp_idx
].bezier_points
[i
]
525 p
= surface_splines
[sp_idx
].bezier_points
[i
]
526 edge_length
= (prev_p
.co
- p
.co
).length
527 surface_splines_lengths
[sp_idx
] += edge_length
531 # Calculate vertex positions with appropriate edge proportions, and ordered, for each spline
532 for sp_idx
in range(0, len(surface_splines
)):
533 surface_splines_parsed
.append([])
534 surface_splines_parsed
[sp_idx
].append(surface_splines
[sp_idx
].bezier_points
[0].co
)
536 prev_p_co
= surface_splines
[sp_idx
].bezier_points
[0].co
539 for prop_idx
in range(len(proportions
) - 1):
540 target_length
= surface_splines_lengths
[sp_idx
] * proportions
[prop_idx
]
541 partial_segment_length
= 0
545 # if not it'll pass the p_idx as an index below and crash
546 if p_idx
< len(surface_splines
[sp_idx
].bezier_points
):
547 p_co
= surface_splines
[sp_idx
].bezier_points
[p_idx
].co
548 new_dist
= (prev_p_co
- p_co
).length
550 # The new distance that could have the partial segment if
551 # it is still shorter than the target length
552 potential_segment_length
= partial_segment_length
+ new_dist
554 # If the potential is still shorter, keep adding
555 if potential_segment_length
< target_length
:
556 partial_segment_length
= potential_segment_length
561 # If the potential is longer than the target, calculate the target
562 # (a point between the last two points), and assign
563 elif potential_segment_length
> target_length
:
564 remaining_dist
= target_length
- partial_segment_length
565 vec
= p_co
- prev_p_co
567 intermediate_co
= prev_p_co
+ (vec
* remaining_dist
)
569 surface_splines_parsed
[sp_idx
].append(intermediate_co
)
571 partial_segment_length
+= remaining_dist
572 prev_p_co
= intermediate_co
576 # If the potential is equal to the target, assign
577 elif potential_segment_length
== target_length
:
578 surface_splines_parsed
[sp_idx
].append(p_co
)
586 # last point of the spline
587 surface_splines_parsed
[sp_idx
].append(
588 surface_splines
[sp_idx
].bezier_points
[len(surface_splines
[sp_idx
].bezier_points
) - 1].co
591 return surface_splines_parsed
593 # Counts the number of faces that belong to each edge
594 def edge_face_count(self
, ob
):
595 ed_keys_count_dict
= {}
597 for face
in ob
.data
.polygons
:
598 for ed_keys
in face
.edge_keys
:
599 if ed_keys
not in ed_keys_count_dict
:
600 ed_keys_count_dict
[ed_keys
] = 1
602 ed_keys_count_dict
[ed_keys
] += 1
605 for i
in range(len(ob
.data
.edges
)):
606 edge_face_count
.append(0)
608 for i
in range(len(ob
.data
.edges
)):
609 ed
= ob
.data
.edges
[i
]
614 if (v1
, v2
) in ed_keys_count_dict
:
615 edge_face_count
[i
] = ed_keys_count_dict
[(v1
, v2
)]
616 elif (v2
, v1
) in ed_keys_count_dict
:
617 edge_face_count
[i
] = ed_keys_count_dict
[(v2
, v1
)]
619 return edge_face_count
621 # Fills with faces all the selected vertices which form empty triangles or quads
622 def fill_with_faces(self
, object):
623 all_selected_verts_count
= self
.main_object_selected_verts_count
625 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
627 # Calculate average length of selected edges
628 all_selected_verts
= []
629 original_sel_edges_count
= 0
630 for ed
in object.data
.edges
:
631 if object.data
.vertices
[ed
.vertices
[0]].select
and object.data
.vertices
[ed
.vertices
[1]].select
:
633 coords
.append(object.data
.vertices
[ed
.vertices
[0]].co
)
634 coords
.append(object.data
.vertices
[ed
.vertices
[1]].co
)
636 original_sel_edges_count
+= 1
638 if not ed
.vertices
[0] in all_selected_verts
:
639 all_selected_verts
.append(ed
.vertices
[0])
641 if not ed
.vertices
[1] in all_selected_verts
:
642 all_selected_verts
.append(ed
.vertices
[1])
644 tuple(all_selected_verts
)
646 # Check if there is any edge selected. If not, interrupt the script
647 if original_sel_edges_count
== 0 and all_selected_verts_count
> 0:
650 # Get all edges connected to selected verts
651 all_edges_around_sel_verts
= []
652 edges_connected_to_sel_verts
= {}
653 verts_connected_to_every_vert
= {}
654 for ed_idx
in range(len(object.data
.edges
)):
655 ed
= object.data
.edges
[ed_idx
]
658 if ed
.vertices
[0] in all_selected_verts
:
659 if not ed
.vertices
[0] in edges_connected_to_sel_verts
:
660 edges_connected_to_sel_verts
[ed
.vertices
[0]] = []
662 edges_connected_to_sel_verts
[ed
.vertices
[0]].append(ed_idx
)
665 if ed
.vertices
[1] in all_selected_verts
:
666 if not ed
.vertices
[1] in edges_connected_to_sel_verts
:
667 edges_connected_to_sel_verts
[ed
.vertices
[1]] = []
669 edges_connected_to_sel_verts
[ed
.vertices
[1]].append(ed_idx
)
672 if include_edge
is True:
673 all_edges_around_sel_verts
.append(ed_idx
)
675 # Get all connected verts to each vert
676 if not ed
.vertices
[0] in verts_connected_to_every_vert
:
677 verts_connected_to_every_vert
[ed
.vertices
[0]] = []
679 if not ed
.vertices
[1] in verts_connected_to_every_vert
:
680 verts_connected_to_every_vert
[ed
.vertices
[1]] = []
682 verts_connected_to_every_vert
[ed
.vertices
[0]].append(ed
.vertices
[1])
683 verts_connected_to_every_vert
[ed
.vertices
[1]].append(ed
.vertices
[0])
685 # Get all verts connected to faces
686 all_verts_part_of_faces
= []
687 all_edges_faces_count
= []
688 all_edges_faces_count
+= self
.edge_face_count(object)
690 # Get only the selected edges that have faces attached.
691 count_faces_of_edges_around_sel_verts
= {}
692 selected_verts_with_faces
= []
693 for ed_idx
in all_edges_around_sel_verts
:
694 count_faces_of_edges_around_sel_verts
[ed_idx
] = all_edges_faces_count
[ed_idx
]
696 if all_edges_faces_count
[ed_idx
] > 0:
697 ed
= object.data
.edges
[ed_idx
]
699 if not ed
.vertices
[0] in selected_verts_with_faces
:
700 selected_verts_with_faces
.append(ed
.vertices
[0])
702 if not ed
.vertices
[1] in selected_verts_with_faces
:
703 selected_verts_with_faces
.append(ed
.vertices
[1])
705 all_verts_part_of_faces
.append(ed
.vertices
[0])
706 all_verts_part_of_faces
.append(ed
.vertices
[1])
708 tuple(selected_verts_with_faces
)
710 # Discard unneeded verts from calculations
711 participating_verts
= []
713 for v_idx
in all_selected_verts
:
714 vert_has_edges_with_one_face
= False
716 # Check if the actual vert has at least one edge connected to only one face
717 for ed_idx
in edges_connected_to_sel_verts
[v_idx
]:
718 if count_faces_of_edges_around_sel_verts
[ed_idx
] == 1:
719 vert_has_edges_with_one_face
= True
721 # If the vert has two or less edges connected and the vert is not part of any face.
722 # Or the vert is part of any face and at least one of
723 # the connected edges has only one face attached to it.
724 if (len(edges_connected_to_sel_verts
[v_idx
]) == 2 and
725 v_idx
not in all_verts_part_of_faces
) or \
726 len(edges_connected_to_sel_verts
[v_idx
]) == 1 or \
727 (v_idx
in all_verts_part_of_faces
and
728 vert_has_edges_with_one_face
):
730 participating_verts
.append(v_idx
)
732 if v_idx
not in all_verts_part_of_faces
:
733 movable_verts
.append(v_idx
)
735 # Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
736 for mv_idx
in movable_verts
:
738 mv_connected_verts
= verts_connected_to_every_vert
[mv_idx
]
740 for actual_v_idx
in all_selected_verts
:
741 count_shared_neighbors
= 0
744 for mv_conn_v_idx
in mv_connected_verts
:
745 if mv_idx
!= actual_v_idx
:
746 if mv_conn_v_idx
in verts_connected_to_every_vert
[actual_v_idx
] and \
747 mv_conn_v_idx
not in checked_verts
:
748 count_shared_neighbors
+= 1
749 checked_verts
.append(mv_conn_v_idx
)
751 if actual_v_idx
in mv_connected_verts
:
755 if count_shared_neighbors
== 2:
763 movable_verts
.remove(mv_idx
)
765 # Calculate merge distance for participating verts
766 shortest_edge_length
= None
767 for ed
in object.data
.edges
:
768 if ed
.vertices
[0] in movable_verts
and ed
.vertices
[1] in movable_verts
:
769 v1
= object.data
.vertices
[ed
.vertices
[0]]
770 v2
= object.data
.vertices
[ed
.vertices
[1]]
772 length
= (v1
.co
- v2
.co
).length
774 if shortest_edge_length
is None:
775 shortest_edge_length
= length
777 if length
< shortest_edge_length
:
778 shortest_edge_length
= length
780 if shortest_edge_length
is not None:
781 edges_merge_distance
= shortest_edge_length
* 0.5
783 edges_merge_distance
= 0
785 # Get together the verts near enough. They will be merged later
787 remaining_verts
+= participating_verts
788 for v1_idx
in participating_verts
:
789 if v1_idx
in remaining_verts
and v1_idx
in movable_verts
:
791 coords_verts_to_merge
= {}
793 verts_to_merge
.append(v1_idx
)
795 v1_co
= object.data
.vertices
[v1_idx
].co
796 coords_verts_to_merge
[v1_idx
] = (v1_co
[0], v1_co
[1], v1_co
[2])
798 for v2_idx
in remaining_verts
:
800 v2_co
= object.data
.vertices
[v2_idx
].co
802 dist
= (v1_co
- v2_co
).length
804 if dist
<= edges_merge_distance
: # Add the verts which are near enough
805 verts_to_merge
.append(v2_idx
)
807 coords_verts_to_merge
[v2_idx
] = (v2_co
[0], v2_co
[1], v2_co
[2])
809 for vm_idx
in verts_to_merge
:
810 remaining_verts
.remove(vm_idx
)
812 if len(verts_to_merge
) > 1:
813 # Calculate middle point of the verts to merge.
817 movable_verts_to_merge_count
= 0
818 for i
in range(len(verts_to_merge
)):
819 if verts_to_merge
[i
] in movable_verts
:
820 v_co
= object.data
.vertices
[verts_to_merge
[i
]].co
826 movable_verts_to_merge_count
+= 1
829 sum_x_co
/ movable_verts_to_merge_count
,
830 sum_y_co
/ movable_verts_to_merge_count
,
831 sum_z_co
/ movable_verts_to_merge_count
834 # Check if any vert to be merged is not movable
836 are_verts_not_movable
= False
837 verts_not_movable
= []
838 for v_merge_idx
in verts_to_merge
:
839 if v_merge_idx
in participating_verts
and v_merge_idx
not in movable_verts
:
840 are_verts_not_movable
= True
841 verts_not_movable
.append(v_merge_idx
)
843 if are_verts_not_movable
:
844 # Get the vert connected to faces, that is nearest to
845 # the middle point of the movable verts
847 for vcf_idx
in verts_not_movable
:
848 dist
= abs((object.data
.vertices
[vcf_idx
].co
-
849 Vector(middle_point_co
)).length
)
851 if shortest_dist
is None:
853 nearest_vert_idx
= vcf_idx
855 if dist
< shortest_dist
:
857 nearest_vert_idx
= vcf_idx
859 coords
= object.data
.vertices
[nearest_vert_idx
].co
860 target_point_co
= [coords
[0], coords
[1], coords
[2]]
862 target_point_co
= middle_point_co
864 # Move verts to merge to the middle position
865 for v_merge_idx
in verts_to_merge
:
866 if v_merge_idx
in movable_verts
: # Only move the verts that are not part of faces
867 object.data
.vertices
[v_merge_idx
].co
[0] = target_point_co
[0]
868 object.data
.vertices
[v_merge_idx
].co
[1] = target_point_co
[1]
869 object.data
.vertices
[v_merge_idx
].co
[2] = target_point_co
[2]
871 # Perform "Remove Doubles" to weld all the disconnected verts
872 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
873 bpy
.ops
.mesh
.remove_doubles(threshold
=0.0001)
875 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
877 # Get all the definitive selected edges, after weldding
879 edges_per_vert
= {} # Number of faces of each selected edge
880 for ed
in object.data
.edges
:
881 if object.data
.vertices
[ed
.vertices
[0]].select
and object.data
.vertices
[ed
.vertices
[1]].select
:
882 selected_edges
.append(ed
.index
)
884 # Save all the edges that belong to each vertex.
885 if not ed
.vertices
[0] in edges_per_vert
:
886 edges_per_vert
[ed
.vertices
[0]] = []
888 if not ed
.vertices
[1] in edges_per_vert
:
889 edges_per_vert
[ed
.vertices
[1]] = []
891 edges_per_vert
[ed
.vertices
[0]].append(ed
.index
)
892 edges_per_vert
[ed
.vertices
[1]].append(ed
.index
)
894 # Check if all the edges connected to each vert have two faces attached to them.
895 # To discard them later and make calculations faster
897 a
+= self
.edge_face_count(object)
899 verts_surrounded_by_faces
= {}
900 for v_idx
in edges_per_vert
:
901 edges_with_two_faces_count
= 0
903 for ed_idx
in edges_per_vert
[v_idx
]:
905 edges_with_two_faces_count
+= 1
907 if edges_with_two_faces_count
== len(edges_per_vert
[v_idx
]):
908 verts_surrounded_by_faces
[v_idx
] = True
910 verts_surrounded_by_faces
[v_idx
] = False
912 # Get all the selected vertices
913 selected_verts_idx
= []
914 for v
in object.data
.vertices
:
916 selected_verts_idx
.append(v
.index
)
918 # Get all the faces of the object
919 all_object_faces_verts_idx
= []
920 for face
in object.data
.polygons
:
922 face_verts
.append(face
.vertices
[0])
923 face_verts
.append(face
.vertices
[1])
924 face_verts
.append(face
.vertices
[2])
926 if len(face
.vertices
) == 4:
927 face_verts
.append(face
.vertices
[3])
929 all_object_faces_verts_idx
.append(face_verts
)
931 # Deselect all vertices
932 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
933 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
934 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
936 # Make a dictionary with the verts related to each vert
937 related_key_verts
= {}
938 for ed_idx
in selected_edges
:
939 ed
= object.data
.edges
[ed_idx
]
941 if not verts_surrounded_by_faces
[ed
.vertices
[0]]:
942 if not ed
.vertices
[0] in related_key_verts
:
943 related_key_verts
[ed
.vertices
[0]] = []
945 if not ed
.vertices
[1] in related_key_verts
[ed
.vertices
[0]]:
946 related_key_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
948 if not verts_surrounded_by_faces
[ed
.vertices
[1]]:
949 if not ed
.vertices
[1] in related_key_verts
:
950 related_key_verts
[ed
.vertices
[1]] = []
952 if not ed
.vertices
[0] in related_key_verts
[ed
.vertices
[1]]:
953 related_key_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
955 # Get groups of verts forming each face
957 for v1
in related_key_verts
: # verts-1 ....
958 for v2
in related_key_verts
: # verts-2
960 related_verts_in_common
= []
963 for rel_v1
in related_key_verts
[v1
]:
964 # Check if related verts of verts-1 are related verts of verts-2
965 if rel_v1
in related_key_verts
[v2
]:
966 related_verts_in_common
.append(rel_v1
)
968 if v2
in related_key_verts
[v1
]:
971 if v1
in related_key_verts
[v2
]:
974 repeated_face
= False
975 # If two verts have two related verts in common, they form a quad
976 if len(related_verts_in_common
) == 2:
977 # Check if the face is already saved
978 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
980 for f_verts
in all_faces_to_check_idx
:
983 if len(f_verts
) == 4:
988 if related_verts_in_common
[0] in f_verts
:
990 if related_verts_in_common
[1] in f_verts
:
993 if repeated_verts
== len(f_verts
):
997 if not repeated_face
:
998 faces_verts_idx
.append(
999 [v1
, related_verts_in_common
[0], v2
, related_verts_in_common
[1]]
1002 # If Two verts have one related vert in common and
1003 # they are related to each other, they form a triangle
1004 elif v2_in_rel_v1
and v1_in_rel_v2
and len(related_verts_in_common
) == 1:
1005 # Check if the face is already saved.
1006 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
1008 for f_verts
in all_faces_to_check_idx
:
1011 if len(f_verts
) == 3:
1016 if related_verts_in_common
[0] in f_verts
:
1019 if repeated_verts
== len(f_verts
):
1020 repeated_face
= True
1023 if not repeated_face
:
1024 faces_verts_idx
.append([v1
, related_verts_in_common
[0], v2
])
1026 # Keep only the faces that don't overlap by ignoring quads
1027 # that overlap with two adjacent triangles
1028 faces_to_not_include_idx
= [] # Indices of faces_verts_idx to eliminate
1029 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
1030 for i
in range(len(faces_verts_idx
)):
1031 for t
in range(len(all_faces_to_check_idx
)):
1035 if len(faces_verts_idx
[i
]) == 4 and len(all_faces_to_check_idx
[t
]) == 3:
1036 for v_idx
in all_faces_to_check_idx
[t
]:
1037 if v_idx
in faces_verts_idx
[i
]:
1038 verts_in_common
+= 1
1039 # If it doesn't have all it's vertices repeated in the other face
1040 if verts_in_common
== 3:
1041 if i
not in faces_to_not_include_idx
:
1042 faces_to_not_include_idx
.append(i
)
1044 # Build faces discarding the ones in faces_to_not_include
1049 num_faces_created
= 0
1050 for i
in range(len(faces_verts_idx
)):
1051 if i
not in faces_to_not_include_idx
:
1052 bm
.faces
.new([bm
.verts
[v
] for v
in faces_verts_idx
[i
]])
1054 num_faces_created
+= 1
1059 for v_idx
in selected_verts_idx
:
1060 self
.main_object
.data
.vertices
[v_idx
].select
= True
1062 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
1063 bpy
.ops
.mesh
.normals_make_consistent(inside
=False)
1064 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
1068 return num_faces_created
1070 # Crosshatch skinning
1071 def crosshatch_surface_invoke(self
, ob_original_splines
):
1072 self
.is_crosshatch
= False
1073 self
.crosshatch_merge_distance
= 0
1075 objects_to_delete
= [] # duplicated strokes to be deleted.
1077 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1078 # (without this the surface verts merging with the main object doesn't work well)
1079 self
.modifiers_prev_viewport_state
= []
1080 if len(self
.main_object
.modifiers
) > 0:
1081 for m_idx
in range(len(self
.main_object
.modifiers
)):
1082 self
.modifiers_prev_viewport_state
.append(
1083 self
.main_object
.modifiers
[m_idx
].show_viewport
1085 self
.main_object
.modifiers
[m_idx
].show_viewport
= False
1087 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1088 ob_original_splines
.select_set(True)
1089 bpy
.context
.view_layer
.objects
.active
= ob_original_splines
1091 if len(ob_original_splines
.data
.splines
) >= 2:
1092 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1093 ob_splines
= bpy
.context
.object
1094 ob_splines
.name
= "SURFSKIO_NE_STR"
1096 # Get estimative merge distance (sum up the distances from the first point to
1097 # all other points, then average them and then divide them)
1098 first_point_dist_sum
= 0
1101 coords_first_pt
= ob_splines
.data
.splines
[0].bezier_points
[0].co
1102 for i
in range(len(ob_splines
.data
.splines
)):
1103 sp
= ob_splines
.data
.splines
[i
]
1105 if coords_first_pt
!= sp
.bezier_points
[0].co
:
1106 first_dist
= (coords_first_pt
- sp
.bezier_points
[0].co
).length
1108 if coords_first_pt
!= sp
.bezier_points
[len(sp
.bezier_points
) - 1].co
:
1109 second_dist
= (coords_first_pt
- sp
.bezier_points
[len(sp
.bezier_points
) - 1].co
).length
1111 first_point_dist_sum
+= first_dist
+ second_dist
1115 shortest_dist
= first_dist
1116 elif second_dist
!= 0:
1117 shortest_dist
= second_dist
1119 if shortest_dist
> first_dist
and first_dist
!= 0:
1120 shortest_dist
= first_dist
1122 if shortest_dist
> second_dist
and second_dist
!= 0:
1123 shortest_dist
= second_dist
1125 self
.crosshatch_merge_distance
= shortest_dist
/ 20
1127 # Recalculation of merge distance
1129 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1131 ob_calc_merge_dist
= bpy
.context
.object
1132 ob_calc_merge_dist
.name
= "SURFSKIO_CALC_TMP"
1134 objects_to_delete
.append(ob_calc_merge_dist
)
1136 # Smooth out strokes a little to improve crosshatch detection
1137 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1138 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1141 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1143 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1144 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1146 # Convert curves into mesh
1147 ob_calc_merge_dist
.data
.resolution_u
= 12
1148 bpy
.ops
.object.convert(target
='MESH', keep_original
=False)
1150 # Find "intersection-nodes"
1151 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1152 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1153 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN',
1154 threshold
=self
.crosshatch_merge_distance
)
1155 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1156 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1158 # Remove verts with less than three edges
1159 verts_edges_count
= {}
1160 for ed
in ob_calc_merge_dist
.data
.edges
:
1163 if v
[0] not in verts_edges_count
:
1164 verts_edges_count
[v
[0]] = 0
1166 if v
[1] not in verts_edges_count
:
1167 verts_edges_count
[v
[1]] = 0
1169 verts_edges_count
[v
[0]] += 1
1170 verts_edges_count
[v
[1]] += 1
1172 nodes_verts_coords
= []
1173 for v_idx
in verts_edges_count
:
1174 v
= ob_calc_merge_dist
.data
.vertices
[v_idx
]
1176 if verts_edges_count
[v_idx
] < 3:
1180 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1181 bpy
.ops
.mesh
.delete('INVOKE_REGION_WIN', type='VERT')
1182 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1184 # Remove doubles to discard very near verts from calculations of distance
1185 bpy
.ops
.mesh
.remove_doubles(
1186 'INVOKE_REGION_WIN',
1187 threshold
=self
.crosshatch_merge_distance
* 4.0
1189 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1191 # Get all coords of the resulting nodes
1192 nodes_verts_coords
= [(v
.co
[0], v
.co
[1], v
.co
[2]) for
1193 v
in ob_calc_merge_dist
.data
.vertices
]
1195 # Check if the strokes are a crosshatch
1196 if len(nodes_verts_coords
) >= 3:
1197 self
.is_crosshatch
= True
1199 shortest_dist
= None
1200 for co_1
in nodes_verts_coords
:
1201 for co_2
in nodes_verts_coords
:
1203 dist
= (Vector(co_1
) - Vector(co_2
)).length
1205 if shortest_dist
is not None:
1206 if dist
< shortest_dist
:
1207 shortest_dist
= dist
1209 shortest_dist
= dist
1211 self
.crosshatch_merge_distance
= shortest_dist
/ 3
1213 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1214 ob_splines
.select_set(True)
1215 bpy
.context
.view_layer
.objects
.active
= ob_splines
1217 # Deselect all points
1218 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1219 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1220 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1222 # Smooth splines in a localized way, to eliminate "saw-teeth"
1223 # like shapes when there are many points
1224 for sp
in ob_splines
.data
.splines
:
1227 angle_limit
= 2 # Degrees
1228 for t
in range(len(sp
.bezier_points
)):
1229 # Because on each iteration it checks the "next two points"
1230 # of the actual. This way it doesn't go out of range
1231 if t
<= len(sp
.bezier_points
) - 3:
1232 p1
= sp
.bezier_points
[t
]
1233 p2
= sp
.bezier_points
[t
+ 1]
1234 p3
= sp
.bezier_points
[t
+ 2]
1236 vec_1
= p1
.co
- p2
.co
1237 vec_2
= p2
.co
- p3
.co
1239 if p2
.co
!= p1
.co
and p2
.co
!= p3
.co
:
1240 angle
= vec_1
.angle(vec_2
)
1241 angle_sum
+= degrees(angle
)
1243 if angle_sum
>= angle_limit
: # If sum of angles is grater than the limit
1244 if (p1
.co
- p2
.co
).length
<= self
.crosshatch_merge_distance
:
1245 p1
.select_control_point
= True
1246 p1
.select_left_handle
= True
1247 p1
.select_right_handle
= True
1249 p2
.select_control_point
= True
1250 p2
.select_left_handle
= True
1251 p2
.select_right_handle
= True
1253 if (p1
.co
- p2
.co
).length
<= self
.crosshatch_merge_distance
:
1254 p3
.select_control_point
= True
1255 p3
.select_left_handle
= True
1256 p3
.select_right_handle
= True
1260 sp
.bezier_points
[0].select_control_point
= False
1261 sp
.bezier_points
[0].select_left_handle
= False
1262 sp
.bezier_points
[0].select_right_handle
= False
1264 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_control_point
= False
1265 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_left_handle
= False
1266 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_right_handle
= False
1268 # Smooth out strokes a little to improve crosshatch detection
1269 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1272 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1274 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1275 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1277 # Simplify the splines
1278 for sp
in ob_splines
.data
.splines
:
1281 sp
.bezier_points
[0].select_control_point
= True
1282 sp
.bezier_points
[0].select_left_handle
= True
1283 sp
.bezier_points
[0].select_right_handle
= True
1285 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_control_point
= True
1286 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_left_handle
= True
1287 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_right_handle
= True
1289 angle_limit
= 15 # Degrees
1290 for t
in range(len(sp
.bezier_points
)):
1291 # Because on each iteration it checks the "next two points"
1292 # of the actual. This way it doesn't go out of range
1293 if t
<= len(sp
.bezier_points
) - 3:
1294 p1
= sp
.bezier_points
[t
]
1295 p2
= sp
.bezier_points
[t
+ 1]
1296 p3
= sp
.bezier_points
[t
+ 2]
1298 vec_1
= p1
.co
- p2
.co
1299 vec_2
= p2
.co
- p3
.co
1301 if p2
.co
!= p1
.co
and p2
.co
!= p3
.co
:
1302 angle
= vec_1
.angle(vec_2
)
1303 angle_sum
+= degrees(angle
)
1304 # If sum of angles is grater than the limit
1305 if angle_sum
>= angle_limit
:
1306 p1
.select_control_point
= True
1307 p1
.select_left_handle
= True
1308 p1
.select_right_handle
= True
1310 p2
.select_control_point
= True
1311 p2
.select_left_handle
= True
1312 p2
.select_right_handle
= True
1314 p3
.select_control_point
= True
1315 p3
.select_left_handle
= True
1316 p3
.select_right_handle
= True
1320 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1321 bpy
.ops
.curve
.select_all(action
='INVERT')
1323 bpy
.ops
.curve
.delete(type='VERT')
1324 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1326 objects_to_delete
.append(ob_splines
)
1328 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1329 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1330 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1332 # Check if the strokes are a crosshatch
1333 if self
.is_crosshatch
:
1334 all_points_coords
= []
1335 for i
in range(len(ob_splines
.data
.splines
)):
1336 all_points_coords
.append([])
1338 all_points_coords
[i
] = [Vector((x
, y
, z
)) for
1339 x
, y
, z
in [bp
.co
for
1340 bp
in ob_splines
.data
.splines
[i
].bezier_points
]]
1342 all_intersections
= []
1343 checked_splines
= []
1344 for i
in range(len(all_points_coords
)):
1346 for t
in range(len(all_points_coords
[i
]) - 1):
1347 bp1_co
= all_points_coords
[i
][t
]
1348 bp2_co
= all_points_coords
[i
][t
+ 1]
1350 for i2
in range(len(all_points_coords
)):
1351 if i
!= i2
and i2
not in checked_splines
:
1352 for t2
in range(len(all_points_coords
[i2
]) - 1):
1353 bp3_co
= all_points_coords
[i2
][t2
]
1354 bp4_co
= all_points_coords
[i2
][t2
+ 1]
1356 intersec_coords
= intersect_line_line(
1357 bp1_co
, bp2_co
, bp3_co
, bp4_co
1359 if intersec_coords
is not None:
1360 dist
= (intersec_coords
[0] - intersec_coords
[1]).length
1362 if dist
<= self
.crosshatch_merge_distance
* 1.5:
1363 _temp_co
, percent1
= intersect_point_line(
1364 intersec_coords
[0], bp1_co
, bp2_co
1366 if (percent1
>= -0.02 and percent1
<= 1.02):
1367 _temp_co
, percent2
= intersect_point_line(
1368 intersec_coords
[1], bp3_co
, bp4_co
1370 if (percent2
>= -0.02 and percent2
<= 1.02):
1371 # Format: spline index, first point index from
1372 # corresponding segment, percentage from first point of
1373 # actual segment, coords of intersection point
1374 all_intersections
.append(
1376 ob_splines
.matrix_world
@ intersec_coords
[0])
1378 all_intersections
.append(
1380 ob_splines
.matrix_world
@ intersec_coords
[1])
1383 checked_splines
.append(i
)
1384 # Sort list by spline, then by corresponding first point index of segment,
1385 # and then by percentage from first point of segment: elements 0 and 1 respectively
1386 all_intersections
.sort(key
=operator
.itemgetter(0, 1, 2))
1388 self
.crosshatch_strokes_coords
= {}
1389 for i
in range(len(all_intersections
)):
1390 if not all_intersections
[i
][0] in self
.crosshatch_strokes_coords
:
1391 self
.crosshatch_strokes_coords
[all_intersections
[i
][0]] = []
1393 self
.crosshatch_strokes_coords
[all_intersections
[i
][0]].append(
1394 all_intersections
[i
][3]
1395 ) # Save intersection coords
1397 self
.is_crosshatch
= False
1399 # Delete all duplicates
1400 bpy
.ops
.object.delete({"selected_objects": objects_to_delete
})
1402 # If the main object has modifiers, turn their "viewport view status" to
1403 # what it was before the forced deactivation above
1404 if len(self
.main_object
.modifiers
) > 0:
1405 for m_idx
in range(len(self
.main_object
.modifiers
)):
1406 self
.main_object
.modifiers
[m_idx
].show_viewport
= self
.modifiers_prev_viewport_state
[m_idx
]
1412 # Part of the Crosshatch process that is repeated when the operator is tweaked
1413 def crosshatch_surface_execute(self
, context
):
1414 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1415 # (without this the surface verts merging with the main object doesn't work well)
1416 self
.modifiers_prev_viewport_state
= []
1417 if len(self
.main_object
.modifiers
) > 0:
1418 for m_idx
in range(len(self
.main_object
.modifiers
)):
1419 self
.modifiers_prev_viewport_state
.append(self
.main_object
.modifiers
[m_idx
].show_viewport
)
1421 self
.main_object
.modifiers
[m_idx
].show_viewport
= False
1423 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1425 me_name
= "SURFSKIO_STK_TMP"
1426 me
= bpy
.data
.meshes
.new(me_name
)
1428 all_verts_coords
= []
1430 for st_idx
in self
.crosshatch_strokes_coords
:
1431 for co_idx
in range(len(self
.crosshatch_strokes_coords
[st_idx
])):
1432 coords
= self
.crosshatch_strokes_coords
[st_idx
][co_idx
]
1434 all_verts_coords
.append(coords
)
1437 all_edges
.append((len(all_verts_coords
) - 2, len(all_verts_coords
) - 1))
1439 me
.from_pydata(all_verts_coords
, all_edges
, [])
1440 ob
= object_utils
.object_data_add(context
, me
)
1441 ob
.location
= (0.0, 0.0, 0.0)
1442 ob
.rotation_euler
= (0.0, 0.0, 0.0)
1443 ob
.scale
= (1.0, 1.0, 1.0)
1445 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1447 bpy
.context
.view_layer
.objects
.active
= ob
1449 # Get together each vert and its nearest, to the middle position
1450 verts
= ob
.data
.vertices
1452 for i
in range(len(verts
)):
1453 shortest_dist
= None
1455 if i
not in checked_verts
:
1456 for t
in range(len(verts
)):
1457 if i
!= t
and t
not in checked_verts
:
1458 dist
= (verts
[i
].co
- verts
[t
].co
).length
1460 if shortest_dist
is not None:
1461 if dist
< shortest_dist
:
1462 shortest_dist
= dist
1465 shortest_dist
= dist
1468 middle_location
= (verts
[i
].co
+ verts
[nearest_vert
].co
) / 2
1470 verts
[i
].co
= middle_location
1471 verts
[nearest_vert
].co
= middle_location
1473 checked_verts
.append(i
)
1474 checked_verts
.append(nearest_vert
)
1476 # Calculate average length between all the generated edges
1477 ob
= bpy
.context
.object
1479 for ed
in ob
.data
.edges
:
1480 v1
= ob
.data
.vertices
[ed
.vertices
[0]]
1481 v2
= ob
.data
.vertices
[ed
.vertices
[1]]
1483 lengths_sum
+= (v1
.co
- v2
.co
).length
1485 edges_count
= len(ob
.data
.edges
)
1486 # possible division by zero here
1487 average_edge_length
= lengths_sum
/ edges_count
if edges_count
!= 0 else 0.0001
1489 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1490 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1491 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN',
1492 threshold
=average_edge_length
/ 15.0)
1493 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1495 final_points_ob
= bpy
.context
.view_layer
.objects
.active
1497 # Make a dictionary with the verts related to each vert
1498 related_key_verts
= {}
1499 for ed
in final_points_ob
.data
.edges
:
1500 if not ed
.vertices
[0] in related_key_verts
:
1501 related_key_verts
[ed
.vertices
[0]] = []
1503 if not ed
.vertices
[1] in related_key_verts
:
1504 related_key_verts
[ed
.vertices
[1]] = []
1506 if not ed
.vertices
[1] in related_key_verts
[ed
.vertices
[0]]:
1507 related_key_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
1509 if not ed
.vertices
[0] in related_key_verts
[ed
.vertices
[1]]:
1510 related_key_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
1512 # Get groups of verts forming each face
1513 faces_verts_idx
= []
1514 for v1
in related_key_verts
: # verts-1 ....
1515 for v2
in related_key_verts
: # verts-2
1517 related_verts_in_common
= []
1518 v2_in_rel_v1
= False
1519 v1_in_rel_v2
= False
1520 for rel_v1
in related_key_verts
[v1
]:
1521 # Check if related verts of verts-1 are related verts of verts-2
1522 if rel_v1
in related_key_verts
[v2
]:
1523 related_verts_in_common
.append(rel_v1
)
1525 if v2
in related_key_verts
[v1
]:
1528 if v1
in related_key_verts
[v2
]:
1531 repeated_face
= False
1532 # If two verts have two related verts in common, they form a quad
1533 if len(related_verts_in_common
) == 2:
1534 # Check if the face is already saved
1535 for f_verts
in faces_verts_idx
:
1538 if len(f_verts
) == 4:
1543 if related_verts_in_common
[0] in f_verts
:
1545 if related_verts_in_common
[1] in f_verts
:
1548 if repeated_verts
== len(f_verts
):
1549 repeated_face
= True
1552 if not repeated_face
:
1553 faces_verts_idx
.append([v1
, related_verts_in_common
[0],
1554 v2
, related_verts_in_common
[1]])
1556 # If Two verts have one related vert in common and they are
1557 # related to each other, they form a triangle
1558 elif v2_in_rel_v1
and v1_in_rel_v2
and len(related_verts_in_common
) == 1:
1559 # Check if the face is already saved.
1560 for f_verts
in faces_verts_idx
:
1563 if len(f_verts
) == 3:
1568 if related_verts_in_common
[0] in f_verts
:
1571 if repeated_verts
== len(f_verts
):
1572 repeated_face
= True
1575 if not repeated_face
:
1576 faces_verts_idx
.append([v1
, related_verts_in_common
[0], v2
])
1578 # Keep only the faces that don't overlap by ignoring
1579 # quads that overlap with two adjacent triangles
1580 faces_to_not_include_idx
= [] # Indices of faces_verts_idx to eliminate
1581 for i
in range(len(faces_verts_idx
)):
1582 for t
in range(len(faces_verts_idx
)):
1586 if len(faces_verts_idx
[i
]) == 4 and len(faces_verts_idx
[t
]) == 3:
1587 for v_idx
in faces_verts_idx
[t
]:
1588 if v_idx
in faces_verts_idx
[i
]:
1589 verts_in_common
+= 1
1590 # If it doesn't have all it's vertices repeated in the other face
1591 if verts_in_common
== 3:
1592 if i
not in faces_to_not_include_idx
:
1593 faces_to_not_include_idx
.append(i
)
1596 all_surface_verts_co
= []
1597 for i
in range(len(final_points_ob
.data
.vertices
)):
1598 coords
= final_points_ob
.data
.vertices
[i
].co
1599 all_surface_verts_co
.append([coords
[0], coords
[1], coords
[2]])
1601 # Verts of each face.
1602 all_surface_faces
= []
1603 for i
in range(len(faces_verts_idx
)):
1604 if i
not in faces_to_not_include_idx
:
1606 for v_idx
in faces_verts_idx
[i
]:
1609 all_surface_faces
.append(face
)
1612 surf_me_name
= "SURFSKIO_surface"
1613 me_surf
= bpy
.data
.meshes
.new(surf_me_name
)
1614 me_surf
.from_pydata(all_surface_verts_co
, [], all_surface_faces
)
1615 ob_surface
= object_utils
.object_data_add(context
, me_surf
)
1616 ob_surface
.location
= (0.0, 0.0, 0.0)
1617 ob_surface
.rotation_euler
= (0.0, 0.0, 0.0)
1618 ob_surface
.scale
= (1.0, 1.0, 1.0)
1620 # Delete final points temporal object
1621 bpy
.ops
.object.delete({"selected_objects": [final_points_ob
]})
1623 # Delete isolated verts if there are any
1624 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1625 ob_surface
.select_set(True)
1626 bpy
.context
.view_layer
.objects
.active
= ob_surface
1628 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1629 bpy
.ops
.mesh
.select_all(action
='DESELECT')
1630 bpy
.ops
.mesh
.select_face_by_sides(type='NOTEQUAL')
1631 bpy
.ops
.mesh
.delete()
1632 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1634 # Join crosshatch results with original mesh
1636 # Calculate a distance to merge the verts of the crosshatch surface to the main object
1637 edges_length_sum
= 0
1638 for ed
in ob_surface
.data
.edges
:
1639 edges_length_sum
+= (
1640 ob_surface
.data
.vertices
[ed
.vertices
[0]].co
-
1641 ob_surface
.data
.vertices
[ed
.vertices
[1]].co
1644 # Make dictionary with all the verts connected to each vert, on the new surface object.
1645 surface_connected_verts
= {}
1646 for ed
in ob_surface
.data
.edges
:
1647 if not ed
.vertices
[0] in surface_connected_verts
:
1648 surface_connected_verts
[ed
.vertices
[0]] = []
1650 surface_connected_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
1652 if ed
.vertices
[1] not in surface_connected_verts
:
1653 surface_connected_verts
[ed
.vertices
[1]] = []
1655 surface_connected_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
1657 # Duplicate the new surface object, and use shrinkwrap to
1658 # calculate later the nearest verts to the main object
1659 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1660 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1661 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1663 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1665 final_ob_duplicate
= bpy
.context
.view_layer
.objects
.active
1667 bpy
.ops
.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
1668 shrinkwrap_modifier
= final_ob_duplicate
.modifiers
[-1]
1669 shrinkwrap_modifier
.wrap_method
= "NEAREST_VERTEX"
1670 shrinkwrap_modifier
.target
= self
.main_object
1672 bpy
.ops
.object.modifier_apply('INVOKE_REGION_WIN', modifier
=shrinkwrap_modifier
.name
)
1674 # Make list with verts of original mesh as index and coords as value
1675 main_object_verts_coords
= []
1676 for v
in self
.main_object
.data
.vertices
:
1677 coords
= self
.main_object
.matrix_world
@ v
.co
1679 # To avoid problems when taking "-0.00" as a different value as "0.00"
1680 for c
in range(len(coords
)):
1681 if "%.3f" % coords
[c
] == "-0.00":
1684 main_object_verts_coords
.append(["%.3f" % coords
[0], "%.3f" % coords
[1], "%.3f" % coords
[2]])
1686 tuple(main_object_verts_coords
)
1688 # Determine which verts will be merged, snap them to the nearest verts
1689 # on the original verts, and get them selected
1690 crosshatch_verts_to_merge
= []
1691 if self
.automatic_join
:
1692 for i
in range(len(ob_surface
.data
.vertices
)-1):
1693 # Calculate the distance from each of the connected verts to the actual vert,
1694 # and compare it with the distance they would have if joined.
1695 # If they don't change much, that vert can be joined
1696 merge_actual_vert
= True
1698 if len(surface_connected_verts
[i
]) < 4:
1699 for c_v_idx
in surface_connected_verts
[i
]:
1700 points_original
= []
1701 points_original
.append(ob_surface
.data
.vertices
[c_v_idx
].co
)
1702 points_original
.append(ob_surface
.data
.vertices
[i
].co
)
1705 points_target
.append(ob_surface
.data
.vertices
[c_v_idx
].co
)
1706 points_target
.append(final_ob_duplicate
.data
.vertices
[i
].co
)
1708 vec_A
= points_original
[0] - points_original
[1]
1709 vec_B
= points_target
[0] - points_target
[1]
1711 dist_A
= (points_original
[0] - points_original
[1]).length
1712 dist_B
= (points_target
[0] - points_target
[1]).length
1715 points_original
[0] == points_original
[1] or
1716 points_target
[0] == points_target
[1]
1717 ): # If any vector's length is zero
1719 angle
= vec_A
.angle(vec_B
) / pi
1723 # Set a range of acceptable variation in the connected edges
1724 if dist_B
> dist_A
* 1.7 * self
.join_stretch_factor
or \
1725 dist_B
< dist_A
/ 2 / self
.join_stretch_factor
or \
1726 angle
>= 0.15 * self
.join_stretch_factor
:
1728 merge_actual_vert
= False
1731 merge_actual_vert
= False
1733 self
.report({'WARNING'},
1734 "Crosshatch set incorrectly")
1736 if merge_actual_vert
:
1737 coords
= final_ob_duplicate
.data
.vertices
[i
].co
1738 # To avoid problems when taking "-0.000" as a different value as "0.00"
1739 for c
in range(len(coords
)):
1740 if "%.3f" % coords
[c
] == "-0.00":
1743 comparison_coords
= ["%.3f" % coords
[0], "%.3f" % coords
[1], "%.3f" % coords
[2]]
1745 if comparison_coords
in main_object_verts_coords
:
1746 # Get the index of the vert with those coords in the main object
1747 main_object_related_vert_idx
= main_object_verts_coords
.index(comparison_coords
)
1749 if self
.main_object
.data
.vertices
[main_object_related_vert_idx
].select
is True or \
1750 self
.main_object_selected_verts_count
== 0:
1752 ob_surface
.data
.vertices
[i
].co
= final_ob_duplicate
.data
.vertices
[i
].co
1753 ob_surface
.data
.vertices
[i
].select
= True
1754 crosshatch_verts_to_merge
.append(i
)
1756 # Make sure the vert in the main object is selected,
1757 # in case it wasn't selected and the "join crosshatch" option is active
1758 self
.main_object
.data
.vertices
[main_object_related_vert_idx
].select
= True
1760 # Delete duplicated object
1761 bpy
.ops
.object.delete({"selected_objects": [final_ob_duplicate
]})
1763 # Join crosshatched surface and main object
1764 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1765 ob_surface
.select_set(True)
1766 self
.main_object
.select_set(True)
1767 bpy
.context
.view_layer
.objects
.active
= self
.main_object
1769 bpy
.ops
.object.join('INVOKE_REGION_WIN')
1771 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1772 # Perform Remove doubles to merge verts
1773 if not (self
.automatic_join
is False and self
.main_object_selected_verts_count
== 0):
1774 bpy
.ops
.mesh
.remove_doubles(threshold
=0.0001)
1776 bpy
.ops
.mesh
.select_all(action
='DESELECT')
1778 # If the main object has modifiers, turn their "viewport view status"
1779 # to what it was before the forced deactivation above
1780 if len(self
.main_object
.modifiers
) > 0:
1781 for m_idx
in range(len(self
.main_object
.modifiers
)):
1782 self
.main_object
.modifiers
[m_idx
].show_viewport
= self
.modifiers_prev_viewport_state
[m_idx
]
1788 def rectangular_surface(self
, context
):
1790 all_selected_edges_idx
= []
1791 all_selected_verts
= []
1793 for ed
in self
.main_object
.data
.edges
:
1795 all_selected_edges_idx
.append(ed
.index
)
1798 if not ed
.vertices
[0] in all_selected_verts
:
1799 all_selected_verts
.append(self
.main_object
.data
.vertices
[ed
.vertices
[0]])
1800 if not ed
.vertices
[1] in all_selected_verts
:
1801 all_selected_verts
.append(self
.main_object
.data
.vertices
[ed
.vertices
[1]])
1803 # All verts (both from each edge) to determine later
1804 # which are at the tips (those not repeated twice)
1805 all_verts_idx
.append(ed
.vertices
[0])
1806 all_verts_idx
.append(ed
.vertices
[1])
1808 # Identify the tips and "middle-vertex" that separates U from V, if there is one
1809 all_chains_tips_idx
= []
1810 for v_idx
in all_verts_idx
:
1811 if all_verts_idx
.count(v_idx
) < 2:
1812 all_chains_tips_idx
.append(v_idx
)
1814 edges_connected_to_tips
= []
1815 for ed
in self
.main_object
.data
.edges
:
1816 if (ed
.vertices
[0] in all_chains_tips_idx
or ed
.vertices
[1] in all_chains_tips_idx
) and \
1817 not (ed
.vertices
[0] in all_verts_idx
and ed
.vertices
[1] in all_verts_idx
):
1819 edges_connected_to_tips
.append(ed
)
1821 # Check closed selections
1822 # List with groups of three verts, where the first element of the pair is
1823 # the unselected vert of a closed selection and the other two elements are the
1824 # selected neighbor verts (it will be useful to determine which selection chain
1825 # the unselected vert belongs to, and determine the "middle-vertex")
1826 single_unselected_verts_and_neighbors
= []
1828 # To identify a "closed" selection (a selection that is a closed chain except
1829 # for one vertex) find the vertex in common that have the edges connected to tips.
1830 # If there is a vertex in common, that one is the unselected vert that closes
1831 # the selection or is a "middle-vertex"
1832 single_unselected_verts
= []
1833 for ed
in edges_connected_to_tips
:
1834 for ed_b
in edges_connected_to_tips
:
1836 if ed
.vertices
[0] == ed_b
.vertices
[0] and \
1837 not self
.main_object
.data
.vertices
[ed
.vertices
[0]].select
and \
1838 ed
.vertices
[0] not in single_unselected_verts
:
1840 # The second element is one of the tips of the selected
1841 # vertices of the closed selection
1842 single_unselected_verts_and_neighbors
.append(
1843 [ed
.vertices
[0], ed
.vertices
[1], ed_b
.vertices
[1]]
1845 single_unselected_verts
.append(ed
.vertices
[0])
1847 elif ed
.vertices
[0] == ed_b
.vertices
[1] and \
1848 not self
.main_object
.data
.vertices
[ed
.vertices
[0]].select
and \
1849 ed
.vertices
[0] not in single_unselected_verts
:
1851 single_unselected_verts_and_neighbors
.append(
1852 [ed
.vertices
[0], ed
.vertices
[1], ed_b
.vertices
[0]]
1854 single_unselected_verts
.append(ed
.vertices
[0])
1856 elif ed
.vertices
[1] == ed_b
.vertices
[0] and \
1857 not self
.main_object
.data
.vertices
[ed
.vertices
[1]].select
and \
1858 ed
.vertices
[1] not in single_unselected_verts
:
1860 single_unselected_verts_and_neighbors
.append(
1861 [ed
.vertices
[1], ed
.vertices
[0], ed_b
.vertices
[1]]
1863 single_unselected_verts
.append(ed
.vertices
[1])
1865 elif ed
.vertices
[1] == ed_b
.vertices
[1] and \
1866 not self
.main_object
.data
.vertices
[ed
.vertices
[1]].select
and \
1867 ed
.vertices
[1] not in single_unselected_verts
:
1869 single_unselected_verts_and_neighbors
.append(
1870 [ed
.vertices
[1], ed
.vertices
[0], ed_b
.vertices
[0]]
1872 single_unselected_verts
.append(ed
.vertices
[1])
1875 middle_vertex_idx
= None
1876 tips_to_discard_idx
= []
1878 # Check if there is a "middle-vertex", and get its index
1879 for i
in range(0, len(single_unselected_verts_and_neighbors
)):
1880 actual_chain_verts
= self
.get_ordered_verts(
1881 self
.main_object
, all_selected_edges_idx
,
1882 all_verts_idx
, single_unselected_verts_and_neighbors
[i
][1],
1886 if single_unselected_verts_and_neighbors
[i
][2] != \
1887 actual_chain_verts
[len(actual_chain_verts
) - 1].index
:
1889 middle_vertex_idx
= single_unselected_verts_and_neighbors
[i
][0]
1890 tips_to_discard_idx
.append(single_unselected_verts_and_neighbors
[i
][1])
1891 tips_to_discard_idx
.append(single_unselected_verts_and_neighbors
[i
][2])
1893 # List with pairs of verts that belong to the tips of each selection chain (row)
1894 verts_tips_same_chain_idx
= []
1895 if len(all_chains_tips_idx
) >= 2:
1897 for i
in range(0, len(all_chains_tips_idx
)):
1898 if all_chains_tips_idx
[i
] not in checked_v
:
1899 v_chain
= self
.get_ordered_verts(
1900 self
.main_object
, all_selected_edges_idx
,
1901 all_verts_idx
, all_chains_tips_idx
[i
],
1902 middle_vertex_idx
, None
1905 verts_tips_same_chain_idx
.append([v_chain
[0].index
, v_chain
[len(v_chain
) - 1].index
])
1907 checked_v
.append(v_chain
[0].index
)
1908 checked_v
.append(v_chain
[len(v_chain
) - 1].index
)
1910 # Selection tips (vertices).
1911 verts_tips_parsed_idx
= []
1912 if len(all_chains_tips_idx
) >= 2:
1913 for spec_v_idx
in all_chains_tips_idx
:
1914 if (spec_v_idx
not in tips_to_discard_idx
):
1915 verts_tips_parsed_idx
.append(spec_v_idx
)
1917 # Identify the type of selection made by the user
1918 if middle_vertex_idx
is not None:
1919 # If there are 4 tips (two selection chains), and
1920 # there is only one single unselected vert (the middle vert)
1921 if len(all_chains_tips_idx
) == 4 and len(single_unselected_verts_and_neighbors
) == 1:
1922 selection_type
= "TWO_CONNECTED"
1924 # The type of the selection was not identified, the script stops.
1925 self
.report({'WARNING'}, "The selection isn't valid.")
1927 self
.stopping_errors
= True
1931 if len(all_chains_tips_idx
) == 2: # If there are 2 tips
1932 selection_type
= "SINGLE"
1933 elif len(all_chains_tips_idx
) == 4: # If there are 4 tips
1934 selection_type
= "TWO_NOT_CONNECTED"
1935 elif len(all_chains_tips_idx
) == 0:
1936 if len(self
.main_splines
.data
.splines
) > 1:
1937 selection_type
= "NO_SELECTION"
1939 # If the selection was not identified and there is only one stroke,
1940 # there's no possibility to build a surface, so the script is interrupted
1941 self
.report({'WARNING'}, "The selection isn't valid.")
1943 self
.stopping_errors
= True
1947 # The type of the selection was not identified, the script stops
1948 self
.report({'WARNING'}, "The selection isn't valid.")
1950 self
.stopping_errors
= True
1954 # If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script
1955 if selection_type
== "TWO_NOT_CONNECTED" and len(self
.main_splines
.data
.splines
) == 1:
1956 self
.report({'WARNING'},
1957 "At least two strokes are needed when there are two not connected selections")
1959 self
.stopping_errors
= True
1963 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1965 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1966 self
.main_splines
.select_set(True)
1967 bpy
.context
.view_layer
.objects
.active
= self
.main_splines
1969 # Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out
1970 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1971 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1972 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1973 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1974 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1975 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1976 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1977 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1979 self
.selection_U_exists
= False
1980 self
.selection_U2_exists
= False
1981 self
.selection_V_exists
= False
1982 self
.selection_V2_exists
= False
1984 self
.selection_U_is_closed
= False
1985 self
.selection_U2_is_closed
= False
1986 self
.selection_V_is_closed
= False
1987 self
.selection_V2_is_closed
= False
1989 # Define what vertices are at the tips of each selection and are not the middle-vertex
1990 if selection_type
== "TWO_CONNECTED":
1991 self
.selection_U_exists
= True
1992 self
.selection_V_exists
= True
1994 closing_vert_U_idx
= None
1995 closing_vert_V_idx
= None
1996 closing_vert_U2_idx
= None
1997 closing_vert_V2_idx
= None
1999 # Determine which selection is Selection-U and which is Selection-V
2002 points_first_stroke_tips
= []
2005 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[verts_tips_parsed_idx
[0]].co
2008 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[middle_vertex_idx
].co
2011 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[verts_tips_parsed_idx
[1]].co
2014 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[middle_vertex_idx
].co
2016 points_first_stroke_tips
.append(
2017 self
.main_splines
.data
.splines
[0].bezier_points
[0].co
2019 points_first_stroke_tips
.append(
2020 self
.main_splines
.data
.splines
[0].bezier_points
[
2021 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2025 angle_A
= self
.orientation_difference(points_A
, points_first_stroke_tips
)
2026 angle_B
= self
.orientation_difference(points_B
, points_first_stroke_tips
)
2028 if angle_A
< angle_B
:
2029 first_vert_U_idx
= verts_tips_parsed_idx
[0]
2030 first_vert_V_idx
= verts_tips_parsed_idx
[1]
2032 first_vert_U_idx
= verts_tips_parsed_idx
[1]
2033 first_vert_V_idx
= verts_tips_parsed_idx
[0]
2035 elif selection_type
== "SINGLE" or selection_type
== "TWO_NOT_CONNECTED":
2036 first_sketched_point_first_stroke_co
= self
.main_splines
.data
.splines
[0].bezier_points
[0].co
2037 last_sketched_point_first_stroke_co
= \
2038 self
.main_splines
.data
.splines
[0].bezier_points
[
2039 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2041 first_sketched_point_last_stroke_co
= \
2042 self
.main_splines
.data
.splines
[
2043 len(self
.main_splines
.data
.splines
) - 1
2044 ].bezier_points
[0].co
2045 if len(self
.main_splines
.data
.splines
) > 1:
2046 first_sketched_point_second_stroke_co
= self
.main_splines
.data
.splines
[1].bezier_points
[0].co
2047 last_sketched_point_second_stroke_co
= \
2048 self
.main_splines
.data
.splines
[1].bezier_points
[
2049 len(self
.main_splines
.data
.splines
[1].bezier_points
) - 1
2052 single_unselected_neighbors
= [] # Only the neighbors of the single unselected verts
2053 for verts_neig_idx
in single_unselected_verts_and_neighbors
:
2054 single_unselected_neighbors
.append(verts_neig_idx
[1])
2055 single_unselected_neighbors
.append(verts_neig_idx
[2])
2057 all_chains_tips_and_middle_vert
= []
2058 for v_idx
in all_chains_tips_idx
:
2059 if v_idx
not in single_unselected_neighbors
:
2060 all_chains_tips_and_middle_vert
.append(v_idx
)
2062 all_chains_tips_and_middle_vert
+= single_unselected_verts
2064 all_participating_verts
= all_chains_tips_and_middle_vert
+ all_verts_idx
2066 # The tip of the selected vertices nearest to the first point of the first sketched stroke
2067 nearest_tip_to_first_st_first_pt_idx
, shortest_distance_to_first_stroke
= \
2068 self
.shortest_distance(
2070 first_sketched_point_first_stroke_co
,
2071 all_chains_tips_and_middle_vert
2073 # If the nearest tip is not from a closed selection, get the opposite tip vertex index
2074 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2075 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2077 nearest_tip_to_first_st_first_pt_opposite_idx
= \
2079 nearest_tip_to_first_st_first_pt_idx
,
2080 verts_tips_same_chain_idx
2082 # The tip of the selected vertices nearest to the last point of the first sketched stroke
2083 nearest_tip_to_first_st_last_pt_idx
, _temp_dist
= \
2084 self
.shortest_distance(
2086 last_sketched_point_first_stroke_co
,
2087 all_chains_tips_and_middle_vert
2089 # The tip of the selected vertices nearest to the first point of the last sketched stroke
2090 nearest_tip_to_last_st_first_pt_idx
, shortest_distance_to_last_stroke
= \
2091 self
.shortest_distance(
2093 first_sketched_point_last_stroke_co
,
2094 all_chains_tips_and_middle_vert
2096 if len(self
.main_splines
.data
.splines
) > 1:
2097 # The selected vertex nearest to the first point of the second sketched stroke
2098 # (This will be useful to determine the direction of the closed
2099 # selection V when extruding along strokes)
2100 nearest_vert_to_second_st_first_pt_idx
, _temp_dist
= \
2101 self
.shortest_distance(
2103 first_sketched_point_second_stroke_co
,
2106 # The selected vertex nearest to the first point of the second sketched stroke
2107 # (This will be useful to determine the direction of the closed
2108 # selection V2 when extruding along strokes)
2109 nearest_vert_to_second_st_last_pt_idx
, _temp_dist
= \
2110 self
.shortest_distance(
2112 last_sketched_point_second_stroke_co
,
2115 # Determine if the single selection will be treated as U or as V
2117 for i
in all_selected_edges_idx
:
2119 (self
.main_object
.matrix_world
@
2120 self
.main_object
.data
.vertices
[self
.main_object
.data
.edges
[i
].vertices
[0]].co
) -
2121 (self
.main_object
.matrix_world
@
2122 self
.main_object
.data
.vertices
[self
.main_object
.data
.edges
[i
].vertices
[1]].co
)
2125 average_edge_length
= edges_sum
/ len(all_selected_edges_idx
)
2127 # Get shortest distance from the first point of the last stroke to any participating vertex
2128 _temp_idx
, shortest_distance_to_last_stroke
= \
2129 self
.shortest_distance(
2131 first_sketched_point_last_stroke_co
,
2132 all_participating_verts
2134 # If the beginning of the first stroke is near enough, and its orientation
2135 # difference with the first edge of the nearest selection chain is not too high,
2136 # interpret things as an "extrude along strokes" instead of "extrude through strokes"
2137 if shortest_distance_to_first_stroke
< average_edge_length
/ 4 and \
2138 shortest_distance_to_last_stroke
< average_edge_length
and \
2139 len(self
.main_splines
.data
.splines
) > 1:
2141 self
.selection_U_exists
= False
2142 self
.selection_V_exists
= True
2143 # If the first selection is not closed
2144 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2145 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2146 self
.selection_V_is_closed
= False
2147 closing_vert_U_idx
= None
2148 closing_vert_U2_idx
= None
2149 closing_vert_V_idx
= None
2150 closing_vert_V2_idx
= None
2152 first_vert_V_idx
= nearest_tip_to_first_st_first_pt_idx
2154 if selection_type
== "TWO_NOT_CONNECTED":
2155 self
.selection_V2_exists
= True
2157 first_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2159 self
.selection_V_is_closed
= True
2160 closing_vert_V_idx
= nearest_tip_to_first_st_first_pt_idx
2162 # Get the neighbors of the first (unselected) vert of the closed selection U.
2164 for verts
in single_unselected_verts_and_neighbors
:
2165 if verts
[0] == nearest_tip_to_first_st_first_pt_idx
:
2166 vert_neighbors
.append(verts
[1])
2167 vert_neighbors
.append(verts
[2])
2170 verts_V
= self
.get_ordered_verts(
2171 self
.main_object
, all_selected_edges_idx
,
2172 all_verts_idx
, vert_neighbors
[0], middle_vertex_idx
, None
2175 for i
in range(0, len(verts_V
)):
2176 if verts_V
[i
].index
== nearest_vert_to_second_st_first_pt_idx
:
2177 # If the vertex nearest to the first point of the second stroke
2178 # is in the first half of the selected verts
2179 if i
>= len(verts_V
) / 2:
2180 first_vert_V_idx
= vert_neighbors
[1]
2183 first_vert_V_idx
= vert_neighbors
[0]
2186 if selection_type
== "TWO_NOT_CONNECTED":
2187 self
.selection_V2_exists
= True
2188 # If the second selection is not closed
2189 if nearest_tip_to_first_st_last_pt_idx
not in single_unselected_verts
or \
2190 nearest_tip_to_first_st_last_pt_idx
== middle_vertex_idx
:
2192 self
.selection_V2_is_closed
= False
2193 closing_vert_V2_idx
= None
2194 first_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2197 self
.selection_V2_is_closed
= True
2198 closing_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2200 # Get the neighbors of the first (unselected) vert of the closed selection U
2202 for verts
in single_unselected_verts_and_neighbors
:
2203 if verts
[0] == nearest_tip_to_first_st_last_pt_idx
:
2204 vert_neighbors
.append(verts
[1])
2205 vert_neighbors
.append(verts
[2])
2208 verts_V2
= self
.get_ordered_verts(
2209 self
.main_object
, all_selected_edges_idx
,
2210 all_verts_idx
, vert_neighbors
[0], middle_vertex_idx
, None
2213 for i
in range(0, len(verts_V2
)):
2214 if verts_V2
[i
].index
== nearest_vert_to_second_st_last_pt_idx
:
2215 # If the vertex nearest to the first point of the second stroke
2216 # is in the first half of the selected verts
2217 if i
>= len(verts_V2
) / 2:
2218 first_vert_V2_idx
= vert_neighbors
[1]
2221 first_vert_V2_idx
= vert_neighbors
[0]
2224 self
.selection_V2_exists
= False
2227 self
.selection_U_exists
= True
2228 self
.selection_V_exists
= False
2229 # If the first selection is not closed
2230 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2231 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2232 self
.selection_U_is_closed
= False
2233 closing_vert_U_idx
= None
2237 self
.main_object
.matrix_world
@
2238 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_idx
].co
2241 self
.main_object
.matrix_world
@
2242 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_opposite_idx
].co
2244 points_first_stroke_tips
= []
2245 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[0].co
)
2246 points_first_stroke_tips
.append(
2247 self
.main_splines
.data
.splines
[0].bezier_points
[
2248 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2251 vec_A
= points_tips
[0] - points_tips
[1]
2252 vec_B
= points_first_stroke_tips
[0] - points_first_stroke_tips
[1]
2254 # Compare the direction of the selection and the first
2255 # grease pencil stroke to determine which is the "first" vertex of the selection
2256 if vec_A
.dot(vec_B
) < 0:
2257 first_vert_U_idx
= nearest_tip_to_first_st_first_pt_opposite_idx
2259 first_vert_U_idx
= nearest_tip_to_first_st_first_pt_idx
2262 self
.selection_U_is_closed
= True
2263 closing_vert_U_idx
= nearest_tip_to_first_st_first_pt_idx
2265 # Get the neighbors of the first (unselected) vert of the closed selection U
2267 for verts
in single_unselected_verts_and_neighbors
:
2268 if verts
[0] == nearest_tip_to_first_st_first_pt_idx
:
2269 vert_neighbors
.append(verts
[1])
2270 vert_neighbors
.append(verts
[2])
2273 points_first_and_neighbor
= []
2274 points_first_and_neighbor
.append(
2275 self
.main_object
.matrix_world
@
2276 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_idx
].co
2278 points_first_and_neighbor
.append(
2279 self
.main_object
.matrix_world
@
2280 self
.main_object
.data
.vertices
[vert_neighbors
[0]].co
2282 points_first_stroke_tips
= []
2283 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[0].co
)
2284 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[1].co
)
2286 vec_A
= points_first_and_neighbor
[0] - points_first_and_neighbor
[1]
2287 vec_B
= points_first_stroke_tips
[0] - points_first_stroke_tips
[1]
2289 # Compare the direction of the selection and the first grease pencil stroke to
2290 # determine which is the vertex neighbor to the first vertex (unselected) of
2291 # the closed selection. This will determine the direction of the closed selection
2292 if vec_A
.dot(vec_B
) < 0:
2293 first_vert_U_idx
= vert_neighbors
[1]
2295 first_vert_U_idx
= vert_neighbors
[0]
2297 if selection_type
== "TWO_NOT_CONNECTED":
2298 self
.selection_U2_exists
= True
2299 # If the second selection is not closed
2300 if nearest_tip_to_last_st_first_pt_idx
not in single_unselected_verts
or \
2301 nearest_tip_to_last_st_first_pt_idx
== middle_vertex_idx
:
2303 self
.selection_U2_is_closed
= False
2304 closing_vert_U2_idx
= None
2305 first_vert_U2_idx
= nearest_tip_to_last_st_first_pt_idx
2307 self
.selection_U2_is_closed
= True
2308 closing_vert_U2_idx
= nearest_tip_to_last_st_first_pt_idx
2310 # Get the neighbors of the first (unselected) vert of the closed selection U
2312 for verts
in single_unselected_verts_and_neighbors
:
2313 if verts
[0] == nearest_tip_to_last_st_first_pt_idx
:
2314 vert_neighbors
.append(verts
[1])
2315 vert_neighbors
.append(verts
[2])
2318 points_first_and_neighbor
= []
2319 points_first_and_neighbor
.append(
2320 self
.main_object
.matrix_world
@
2321 self
.main_object
.data
.vertices
[nearest_tip_to_last_st_first_pt_idx
].co
2323 points_first_and_neighbor
.append(
2324 self
.main_object
.matrix_world
@
2325 self
.main_object
.data
.vertices
[vert_neighbors
[0]].co
2327 points_last_stroke_tips
= []
2328 points_last_stroke_tips
.append(
2329 self
.main_splines
.data
.splines
[
2330 len(self
.main_splines
.data
.splines
) - 1
2331 ].bezier_points
[0].co
2333 points_last_stroke_tips
.append(
2334 self
.main_splines
.data
.splines
[
2335 len(self
.main_splines
.data
.splines
) - 1
2336 ].bezier_points
[1].co
2338 vec_A
= points_first_and_neighbor
[0] - points_first_and_neighbor
[1]
2339 vec_B
= points_last_stroke_tips
[0] - points_last_stroke_tips
[1]
2341 # Compare the direction of the selection and the last grease pencil stroke to
2342 # determine which is the vertex neighbor to the first vertex (unselected) of
2343 # the closed selection. This will determine the direction of the closed selection
2344 if vec_A
.dot(vec_B
) < 0:
2345 first_vert_U2_idx
= vert_neighbors
[1]
2347 first_vert_U2_idx
= vert_neighbors
[0]
2349 self
.selection_U2_exists
= False
2351 elif selection_type
== "NO_SELECTION":
2352 self
.selection_U_exists
= False
2353 self
.selection_V_exists
= False
2355 # Get an ordered list of the vertices of Selection-U
2356 verts_ordered_U
= []
2357 if self
.selection_U_exists
:
2358 verts_ordered_U
= self
.get_ordered_verts(
2359 self
.main_object
, all_selected_edges_idx
,
2360 all_verts_idx
, first_vert_U_idx
,
2361 middle_vertex_idx
, closing_vert_U_idx
2364 # Get an ordered list of the vertices of Selection-U2
2365 verts_ordered_U2
= []
2366 if self
.selection_U2_exists
:
2367 verts_ordered_U2
= self
.get_ordered_verts(
2368 self
.main_object
, all_selected_edges_idx
,
2369 all_verts_idx
, first_vert_U2_idx
,
2370 middle_vertex_idx
, closing_vert_U2_idx
2373 # Get an ordered list of the vertices of Selection-V
2374 verts_ordered_V
= []
2375 if self
.selection_V_exists
:
2376 verts_ordered_V
= self
.get_ordered_verts(
2377 self
.main_object
, all_selected_edges_idx
,
2378 all_verts_idx
, first_vert_V_idx
,
2379 middle_vertex_idx
, closing_vert_V_idx
2381 verts_ordered_V_indices
= [x
.index
for x
in verts_ordered_V
]
2383 # Get an ordered list of the vertices of Selection-V2
2384 verts_ordered_V2
= []
2385 if self
.selection_V2_exists
:
2386 verts_ordered_V2
= self
.get_ordered_verts(
2387 self
.main_object
, all_selected_edges_idx
,
2388 all_verts_idx
, first_vert_V2_idx
,
2389 middle_vertex_idx
, closing_vert_V2_idx
2392 # Check if when there are two-not-connected selections both have the same
2393 # number of verts. If not terminate the script
2394 if ((self
.selection_U2_exists
and len(verts_ordered_U
) != len(verts_ordered_U2
)) or
2395 (self
.selection_V2_exists
and len(verts_ordered_V
) != len(verts_ordered_V2
))):
2397 self
.report({'WARNING'}, "Both selections must have the same number of edges")
2399 self
.stopping_errors
= True
2403 # Calculate edges U proportions
2404 # Sum selected edges U lengths
2405 edges_lengths_U
= []
2406 edges_lengths_sum_U
= 0
2408 if self
.selection_U_exists
:
2409 edges_lengths_U
, edges_lengths_sum_U
= self
.get_chain_length(
2413 if self
.selection_U2_exists
:
2414 edges_lengths_U2
, edges_lengths_sum_U2
= self
.get_chain_length(
2418 # Sum selected edges V lengths
2419 edges_lengths_V
= []
2420 edges_lengths_sum_V
= 0
2422 if self
.selection_V_exists
:
2423 edges_lengths_V
, edges_lengths_sum_V
= self
.get_chain_length(
2427 if self
.selection_V2_exists
:
2428 edges_lengths_V2
, edges_lengths_sum_V2
= self
.get_chain_length(
2433 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2434 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN',
2435 number_cuts
=bpy
.context
.scene
.bsurfaces
.SURFSK_precision
)
2436 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2439 edges_proportions_U
= []
2440 edges_proportions_U
= self
.get_edges_proportions(
2441 edges_lengths_U
, edges_lengths_sum_U
,
2442 self
.selection_U_exists
, self
.edges_U
2444 verts_count_U
= len(edges_proportions_U
) + 1
2446 if self
.selection_U2_exists
:
2447 edges_proportions_U2
= []
2448 edges_proportions_U2
= self
.get_edges_proportions(
2449 edges_lengths_U2
, edges_lengths_sum_U2
,
2450 self
.selection_U2_exists
, self
.edges_V
2454 edges_proportions_V
= []
2455 edges_proportions_V
= self
.get_edges_proportions(
2456 edges_lengths_V
, edges_lengths_sum_V
,
2457 self
.selection_V_exists
, self
.edges_V
2460 if self
.selection_V2_exists
:
2461 edges_proportions_V2
= []
2462 edges_proportions_V2
= self
.get_edges_proportions(
2463 edges_lengths_V2
, edges_lengths_sum_V2
,
2464 self
.selection_V2_exists
, self
.edges_V
2467 # Cyclic Follow: simplify sketched curves, make them Cyclic, and complete
2468 # the actual sketched curves with a "closing segment"
2469 if self
.cyclic_follow
and not self
.selection_V_exists
and not \
2470 ((self
.selection_U_exists
and not self
.selection_U_is_closed
) or
2471 (self
.selection_U2_exists
and not self
.selection_U2_is_closed
)):
2473 simplified_spline_coords
= []
2474 simplified_curve
= []
2475 ob_simplified_curve
= []
2476 splines_first_v_co
= []
2477 for i
in range(len(self
.main_splines
.data
.splines
)):
2478 # Create a curve object for the actual spline "cyclic extension"
2479 simplified_curve
.append(bpy
.data
.curves
.new('SURFSKIO_simpl_crv', 'CURVE'))
2480 ob_simplified_curve
.append(bpy
.data
.objects
.new('SURFSKIO_simpl_crv', simplified_curve
[i
]))
2481 bpy
.context
.collection
.objects
.link(ob_simplified_curve
[i
])
2483 simplified_curve
[i
].dimensions
= "3D"
2486 for bp
in self
.main_splines
.data
.splines
[i
].bezier_points
:
2487 spline_coords
.append(bp
.co
)
2490 simplified_spline_coords
.append(self
.simplify_spline(spline_coords
, 5))
2492 # Get the coordinates of the first vert of the actual spline
2493 splines_first_v_co
.append(simplified_spline_coords
[i
][0])
2495 # Generate the spline
2496 spline
= simplified_curve
[i
].splines
.new('BEZIER')
2497 # less one because one point is added when the spline is created
2498 spline
.bezier_points
.add(len(simplified_spline_coords
[i
]) - 1)
2499 for p
in range(0, len(simplified_spline_coords
[i
])):
2500 spline
.bezier_points
[p
].co
= simplified_spline_coords
[i
][p
]
2502 spline
.use_cyclic_u
= True
2504 spline_bp_count
= len(spline
.bezier_points
)
2506 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2507 ob_simplified_curve
[i
].select_set(True)
2508 bpy
.context
.view_layer
.objects
.active
= ob_simplified_curve
[i
]
2510 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2511 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
2512 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2513 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2514 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2516 # Select the "closing segment", and subdivide it
2517 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_control_point
= True
2518 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_left_handle
= True
2519 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_right_handle
= True
2521 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_control_point
= True
2522 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_left_handle
= True
2523 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_right_handle
= True
2525 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2527 (ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].co
-
2528 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].co
).length
/
2529 self
.average_gp_segment_length
2532 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=segments
)
2534 # Delete the other vertices and make it non-cyclic to
2535 # keep only the needed verts of the "closing segment"
2536 bpy
.ops
.curve
.select_all(action
='INVERT')
2537 bpy
.ops
.curve
.delete(type='VERT')
2538 ob_simplified_curve
[i
].data
.splines
[0].use_cyclic_u
= False
2539 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2541 # Add the points of the "closing segment" to the original curve from grease pencil stroke
2542 first_new_index
= len(self
.main_splines
.data
.splines
[i
].bezier_points
)
2543 self
.main_splines
.data
.splines
[i
].bezier_points
.add(
2544 len(ob_simplified_curve
[i
].data
.splines
[0].bezier_points
) - 1
2546 for t
in range(1, len(ob_simplified_curve
[i
].data
.splines
[0].bezier_points
)):
2547 self
.main_splines
.data
.splines
[i
].bezier_points
[t
- 1 + first_new_index
].co
= \
2548 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[t
].co
2550 # Delete the temporal curve
2551 bpy
.ops
.object.delete({"selected_objects": [ob_simplified_curve
[i
]]})
2553 # Get the coords of the points distributed along the sketched strokes,
2554 # with proportions-U of the first selection
2555 pts_on_strokes_with_proportions_U
= self
.distribute_pts(
2556 self
.main_splines
.data
.splines
,
2559 sketched_splines_parsed
= []
2561 if self
.selection_U2_exists
:
2562 # Initialize the multidimensional list with the proportions of all the segments
2563 proportions_loops_crossing_strokes
= []
2564 for i
in range(len(pts_on_strokes_with_proportions_U
)):
2565 proportions_loops_crossing_strokes
.append([])
2567 for t
in range(len(pts_on_strokes_with_proportions_U
[0])):
2568 proportions_loops_crossing_strokes
[i
].append(None)
2570 # Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U
2571 for lp
in range(len(pts_on_strokes_with_proportions_U
[0])):
2572 loop_segments_lengths
= []
2574 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2575 # When on the first stroke, add the segment from the selection to the dirst stroke
2577 loop_segments_lengths
.append(
2578 ((self
.main_object
.matrix_world
@ verts_ordered_U
[lp
].co
) -
2579 pts_on_strokes_with_proportions_U
[0][lp
]).length
2581 # For all strokes except for the last, calculate the distance
2582 # from the actual stroke to the next
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 pts_on_strokes_with_proportions_U
[st
+ 1][lp
]).length
2588 # When on the last stroke, add the segments
2589 # from the last stroke to the second selection
2590 if st
== len(pts_on_strokes_with_proportions_U
) - 1:
2591 loop_segments_lengths
.append(
2592 (pts_on_strokes_with_proportions_U
[st
][lp
] -
2593 (self
.main_object
.matrix_world
@ verts_ordered_U2
[lp
].co
)).length
2595 # Calculate full loop length
2596 loop_seg_lengths_sum
= 0
2597 for i
in range(len(loop_segments_lengths
)):
2598 loop_seg_lengths_sum
+= loop_segments_lengths
[i
]
2600 # Fill the multidimensional list with the proportions of all the segments
2601 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2602 proportions_loops_crossing_strokes
[st
][lp
] = \
2603 loop_segments_lengths
[st
] / loop_seg_lengths_sum
2605 # Calculate proportions for each stroke
2606 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2607 actual_stroke_spline
= []
2608 # Needs to be a list for the "distribute_pts" method
2609 actual_stroke_spline
.append(self
.main_splines
.data
.splines
[st
])
2611 # Calculate the proportions for the actual stroke.
2612 actual_edges_proportions_U
= []
2613 for i
in range(len(edges_proportions_U
)):
2616 # Sum the proportions of this loop up to the actual.
2617 for t
in range(0, st
+ 1):
2618 proportions_sum
+= proportions_loops_crossing_strokes
[t
][i
]
2619 # i + 1, because proportions_loops_crossing_strokes refers to loops,
2620 # and the proportions refer to edges, so we start at the element 1
2621 # of proportions_loops_crossing_strokes instead of element 0
2622 actual_edges_proportions_U
.append(
2623 edges_proportions_U
[i
] -
2624 ((edges_proportions_U
[i
] - edges_proportions_U2
[i
]) * proportions_sum
)
2626 points_actual_spline
= self
.distribute_pts(actual_stroke_spline
, actual_edges_proportions_U
)
2627 sketched_splines_parsed
.append(points_actual_spline
[0])
2629 sketched_splines_parsed
= pts_on_strokes_with_proportions_U
2631 # If the selection type is "TWO_NOT_CONNECTED" replace the
2632 # points of the last spline with the points in the "target" selection
2633 if selection_type
== "TWO_NOT_CONNECTED":
2634 if self
.selection_U2_exists
:
2635 for i
in range(0, len(sketched_splines_parsed
[len(sketched_splines_parsed
) - 1])):
2636 sketched_splines_parsed
[len(sketched_splines_parsed
) - 1][i
] = \
2637 self
.main_object
.matrix_world
@ verts_ordered_U2
[i
].co
2639 # Create temporary curves along the "control-points" found
2640 # on the sketched curves and the mesh selection
2641 mesh_ctrl_pts_name
= "SURFSKIO_ctrl_pts"
2642 me
= bpy
.data
.meshes
.new(mesh_ctrl_pts_name
)
2643 ob_ctrl_pts
= bpy
.data
.objects
.new(mesh_ctrl_pts_name
, me
)
2644 ob_ctrl_pts
.data
= me
2645 bpy
.context
.collection
.objects
.link(ob_ctrl_pts
)
2652 for i
in range(0, verts_count_U
):
2653 vert_num_in_spline
= 1
2655 if self
.selection_U_exists
:
2656 ob_ctrl_pts
.data
.vertices
.add(1)
2657 last_v
= ob_ctrl_pts
.data
.vertices
[len(ob_ctrl_pts
.data
.vertices
) - 1]
2658 last_v
.co
= self
.main_object
.matrix_world
@ verts_ordered_U
[i
].co
2660 vert_num_in_spline
+= 1
2662 for t
in range(0, len(sketched_splines_parsed
)):
2663 ob_ctrl_pts
.data
.vertices
.add(1)
2664 v
= ob_ctrl_pts
.data
.vertices
[len(ob_ctrl_pts
.data
.vertices
) - 1]
2665 v
.co
= sketched_splines_parsed
[t
][i
]
2667 if vert_num_in_spline
> 1:
2668 ob_ctrl_pts
.data
.edges
.add(1)
2669 ob_ctrl_pts
.data
.edges
[len(ob_ctrl_pts
.data
.edges
) - 1].vertices
[0] = \
2670 len(ob_ctrl_pts
.data
.vertices
) - 2
2671 ob_ctrl_pts
.data
.edges
[len(ob_ctrl_pts
.data
.edges
) - 1].vertices
[1] = \
2672 len(ob_ctrl_pts
.data
.vertices
) - 1
2675 first_verts
.append(v
.index
)
2678 second_verts
.append(v
.index
)
2680 if t
== len(sketched_splines_parsed
) - 1:
2681 last_verts
.append(v
.index
)
2684 vert_num_in_spline
+= 1
2686 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2687 ob_ctrl_pts
.select_set(True)
2688 bpy
.context
.view_layer
.objects
.active
= ob_ctrl_pts
2690 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2691 bpy
.ops
.mesh
.select_all(action
='DESELECT')
2692 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2694 # Determine which loops-U will be "Cyclic"
2695 for i
in range(0, len(first_verts
)):
2696 # When there is Cyclic Cross there is no need of
2697 # Automatic Join, (and there are at least three strokes)
2698 if self
.automatic_join
and not self
.cyclic_cross
and \
2699 selection_type
!= "TWO_CONNECTED" and len(self
.main_splines
.data
.splines
) >= 3:
2701 v
= ob_ctrl_pts
.data
.vertices
2702 first_point_co
= v
[first_verts
[i
]].co
2703 second_point_co
= v
[second_verts
[i
]].co
2704 last_point_co
= v
[last_verts
[i
]].co
2706 # Coordinates of the point in the center of both the first and last verts.
2708 (first_point_co
[0] + last_point_co
[0]) / 2,
2709 (first_point_co
[1] + last_point_co
[1]) / 2,
2710 (first_point_co
[2] + last_point_co
[2]) / 2
2712 vec_A
= second_point_co
- first_point_co
2713 vec_B
= second_point_co
- Vector(verts_center_co
)
2715 # Calculate the length of the first segment of the loop,
2716 # and the length it would have after moving the first vert
2717 # to the middle position between first and last
2718 length_original
= (second_point_co
- first_point_co
).length
2719 length_target
= (second_point_co
- Vector(verts_center_co
)).length
2721 angle
= vec_A
.angle(vec_B
) / pi
2723 # If the target length doesn't stretch too much, and the
2724 # its angle doesn't change to much either
2725 if length_target
<= length_original
* 1.03 * self
.join_stretch_factor
and \
2726 angle
<= 0.008 * self
.join_stretch_factor
and not self
.selection_U_exists
:
2728 cyclic_loops_U
.append(True)
2729 # Move the first vert to the center coordinates
2730 ob_ctrl_pts
.data
.vertices
[first_verts
[i
]].co
= verts_center_co
2731 # Select the last verts from Cyclic loops, for later deletion all at once
2732 v
[last_verts
[i
]].select
= True
2734 cyclic_loops_U
.append(False)
2736 # If "Cyclic Cross" is active then "all" crossing curves become cyclic
2737 if self
.cyclic_cross
and not self
.selection_U_exists
and not \
2738 ((self
.selection_V_exists
and not self
.selection_V_is_closed
) or
2739 (self
.selection_V2_exists
and not self
.selection_V2_is_closed
)):
2741 cyclic_loops_U
.append(True)
2743 cyclic_loops_U
.append(False)
2745 # The cyclic_loops_U list needs to be reversed.
2746 cyclic_loops_U
.reverse()
2748 # Delete the previously selected (last_)verts.
2749 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2750 bpy
.ops
.mesh
.delete('INVOKE_REGION_WIN', type='VERT')
2751 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2753 # Create curves from control points.
2754 bpy
.ops
.object.convert('INVOKE_REGION_WIN', target
='CURVE', keep_original
=False)
2755 ob_curves_surf
= bpy
.context
.view_layer
.objects
.active
2756 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2757 bpy
.ops
.curve
.spline_type_set('INVOKE_REGION_WIN', type='BEZIER')
2758 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2760 # Make Cyclic the splines designated as Cyclic.
2761 for i
in range(0, len(cyclic_loops_U
)):
2762 ob_curves_surf
.data
.splines
[i
].use_cyclic_u
= cyclic_loops_U
[i
]
2764 # Get the coords of all points on first loop-U, for later comparison with its
2765 # subdivided version, to know which points of the loops-U are crossed by the
2766 # original strokes. The indices will be the same for the other loops-U
2767 if self
.loops_on_strokes
:
2768 coords_loops_U_control_points
= []
2769 for p
in ob_ctrl_pts
.data
.splines
[0].bezier_points
:
2770 coords_loops_U_control_points
.append(["%.4f" % p
.co
[0], "%.4f" % p
.co
[1], "%.4f" % p
.co
[2]])
2772 tuple(coords_loops_U_control_points
)
2774 # Calculate number of edges-V in case option "Loops on strokes" is active or inactive
2775 if self
.loops_on_strokes
and not self
.selection_V_exists
:
2776 edges_V_count
= len(self
.main_splines
.data
.splines
) * self
.edges_V
2778 edges_V_count
= len(edges_proportions_V
)
2780 # The Follow precision will vary depending on the number of Follow face-loops
2781 precision_multiplier
= round(2 + (edges_V_count
/ 15))
2782 curve_cuts
= bpy
.context
.scene
.bsurfaces
.SURFSK_precision
* precision_multiplier
2784 # Subdivide the curves
2785 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=curve_cuts
)
2787 # The verts position shifting that happens with splines subdivision.
2788 # For later reorder splines points
2789 verts_position_shift
= curve_cuts
+ 1
2790 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2792 # Reorder coordinates of the points of each spline to put the first point of
2793 # the spline starting at the position it was the first point before sudividing
2794 # the curve. And make a new curve object per spline (to handle memory better later)
2795 splines_U_objects
= []
2796 for i
in range(len(ob_curves_surf
.data
.splines
)):
2797 spline_U_curve
= bpy
.data
.curves
.new('SURFSKIO_spline_U_' + str(i
), 'CURVE')
2798 ob_spline_U
= bpy
.data
.objects
.new('SURFSKIO_spline_U_' + str(i
), spline_U_curve
)
2799 bpy
.context
.collection
.objects
.link(ob_spline_U
)
2801 spline_U_curve
.dimensions
= "3D"
2803 # Add points to the spline in the new curve object
2804 ob_spline_U
.data
.splines
.new('BEZIER')
2805 for t
in range(len(ob_curves_surf
.data
.splines
[i
].bezier_points
)):
2806 if cyclic_loops_U
[i
] is True and not self
.selection_U_exists
: # If the loop is cyclic
2807 if t
+ verts_position_shift
<= len(ob_curves_surf
.data
.splines
[i
].bezier_points
) - 1:
2808 point_index
= t
+ verts_position_shift
2810 point_index
= t
+ verts_position_shift
- len(ob_curves_surf
.data
.splines
[i
].bezier_points
)
2813 # to avoid adding the first point since it's added when the spline is created
2815 ob_spline_U
.data
.splines
[0].bezier_points
.add(1)
2816 ob_spline_U
.data
.splines
[0].bezier_points
[t
].co
= \
2817 ob_curves_surf
.data
.splines
[i
].bezier_points
[point_index
].co
2819 if cyclic_loops_U
[i
] is True and not self
.selection_U_exists
: # If the loop is cyclic
2820 # Add a last point at the same location as the first one
2821 ob_spline_U
.data
.splines
[0].bezier_points
.add(1)
2822 ob_spline_U
.data
.splines
[0].bezier_points
[len(ob_spline_U
.data
.splines
[0].bezier_points
) - 1].co
= \
2823 ob_spline_U
.data
.splines
[0].bezier_points
[0].co
2825 ob_spline_U
.data
.splines
[0].use_cyclic_u
= False
2827 splines_U_objects
.append(ob_spline_U
)
2828 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2829 ob_spline_U
.select_set(True)
2830 bpy
.context
.view_layer
.objects
.active
= ob_spline_U
2832 # When option "Loops on strokes" is active each "Cross" loop will have
2833 # its own proportions according to where the original strokes "touch" them
2834 if self
.loops_on_strokes
:
2835 # Get the indices of points where the original strokes "touch" loops-U
2836 points_U_crossed_by_strokes
= []
2837 for i
in range(len(splines_U_objects
[0].data
.splines
[0].bezier_points
)):
2838 bp
= splines_U_objects
[0].data
.splines
[0].bezier_points
[i
]
2839 if ["%.4f" % bp
.co
[0], "%.4f" % bp
.co
[1], "%.4f" % bp
.co
[2]] in coords_loops_U_control_points
:
2840 points_U_crossed_by_strokes
.append(i
)
2842 # Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke
2843 edge_order_number_for_splines
= {}
2844 if self
.selection_V_exists
:
2845 # For two-connected selections add a first hypothetic stroke at the beginning.
2846 if selection_type
== "TWO_CONNECTED":
2847 edge_order_number_for_splines
[0] = 0
2849 for i
in range(len(self
.main_splines
.data
.splines
)):
2850 sp
= self
.main_splines
.data
.splines
[i
]
2851 v_idx
, _dist_temp
= self
.shortest_distance(
2853 sp
.bezier_points
[0].co
,
2854 verts_ordered_V_indices
2856 # Get the position (edges count) of the vert v_idx in the selected chain V
2857 edge_idx_in_chain
= verts_ordered_V_indices
.index(v_idx
)
2859 # For two-connected selections the strokes go after the
2860 # hypothetic stroke added before, so the index adds one per spline
2861 if selection_type
== "TWO_CONNECTED":
2862 spline_number
= i
+ 1
2866 edge_order_number_for_splines
[spline_number
] = edge_idx_in_chain
2868 # Get the first and last verts indices for later comparison
2871 elif i
== len(self
.main_splines
.data
.splines
) - 1:
2874 if self
.selection_V_is_closed
:
2875 # If there is no last stroke on the last vertex (same as first vertex),
2876 # add a hypothetic spline at last vert order
2877 if first_v_idx
!= last_v_idx
:
2878 edge_order_number_for_splines
[(len(self
.main_splines
.data
.splines
) - 1) + 1] = \
2879 len(verts_ordered_V_indices
) - 1
2881 if self
.cyclic_cross
:
2882 edge_order_number_for_splines
[len(self
.main_splines
.data
.splines
) - 1] = \
2883 len(verts_ordered_V_indices
) - 2
2884 edge_order_number_for_splines
[(len(self
.main_splines
.data
.splines
) - 1) + 1] = \
2885 len(verts_ordered_V_indices
) - 1
2887 edge_order_number_for_splines
[len(self
.main_splines
.data
.splines
) - 1] = \
2888 len(verts_ordered_V_indices
) - 1
2890 # Get the coords of the points distributed along the
2891 # "crossing curves", with appropriate proportions-V
2892 surface_splines_parsed
= []
2893 for i
in range(len(splines_U_objects
)):
2894 sp_ob
= splines_U_objects
[i
]
2895 # If "Loops on strokes" option is active, calculate the proportions for each loop-U
2896 if self
.loops_on_strokes
:
2897 # Segments distances from stroke to stroke
2900 segments_distances
= []
2901 for t
in range(len(sp_ob
.data
.splines
[0].bezier_points
)):
2902 bp
= sp_ob
.data
.splines
[0].bezier_points
[t
]
2908 dist
+= (last_p
- actual_p
).length
2910 if t
in points_U_crossed_by_strokes
:
2911 segments_distances
.append(dist
)
2918 # Calculate Proportions.
2919 used_edges_proportions_V
= []
2920 for t
in range(len(segments_distances
)):
2921 if self
.selection_V_exists
:
2923 order_number_last_stroke
= 0
2925 segment_edges_length_V
= 0
2926 segment_edges_length_V2
= 0
2927 for order
in range(order_number_last_stroke
, edge_order_number_for_splines
[t
+ 1]):
2928 segment_edges_length_V
+= edges_lengths_V
[order
]
2929 if self
.selection_V2_exists
:
2930 segment_edges_length_V2
+= edges_lengths_V2
[order
]
2932 for order
in range(order_number_last_stroke
, edge_order_number_for_splines
[t
+ 1]):
2933 # Calculate each "sub-segment" (the ones between each stroke) length
2934 if self
.selection_V2_exists
:
2935 proportion_sub_seg
= (edges_lengths_V2
[order
] -
2936 ((edges_lengths_V2
[order
] - edges_lengths_V
[order
]) /
2937 len(splines_U_objects
) * i
)) / (segment_edges_length_V2
-
2938 (segment_edges_length_V2
- segment_edges_length_V
) /
2939 len(splines_U_objects
) * i
)
2941 sub_seg_dist
= segments_distances
[t
] * proportion_sub_seg
2943 proportion_sub_seg
= edges_lengths_V
[order
] / segment_edges_length_V
2944 sub_seg_dist
= segments_distances
[t
] * proportion_sub_seg
2946 used_edges_proportions_V
.append(sub_seg_dist
/ full_dist
)
2948 order_number_last_stroke
= edge_order_number_for_splines
[t
+ 1]
2951 for _c
in range(self
.edges_V
):
2952 # Calculate each "sub-segment" (the ones between each stroke) length
2953 sub_seg_dist
= segments_distances
[t
] / self
.edges_V
2954 used_edges_proportions_V
.append(sub_seg_dist
/ full_dist
)
2956 actual_spline
= self
.distribute_pts(sp_ob
.data
.splines
, used_edges_proportions_V
)
2957 surface_splines_parsed
.append(actual_spline
[0])
2960 if self
.selection_V2_exists
:
2961 used_edges_proportions_V
= []
2962 for p
in range(len(edges_proportions_V
)):
2963 used_edges_proportions_V
.append(
2964 edges_proportions_V2
[p
] -
2965 ((edges_proportions_V2
[p
] -
2966 edges_proportions_V
[p
]) / len(splines_U_objects
) * i
)
2969 used_edges_proportions_V
= edges_proportions_V
2971 actual_spline
= self
.distribute_pts(sp_ob
.data
.splines
, used_edges_proportions_V
)
2972 surface_splines_parsed
.append(actual_spline
[0])
2974 # Set the verts of the first and last splines to the locations
2975 # of the respective verts in the selections
2976 if self
.selection_V_exists
:
2977 for i
in range(0, len(surface_splines_parsed
[0])):
2978 surface_splines_parsed
[len(surface_splines_parsed
) - 1][i
] = \
2979 self
.main_object
.matrix_world
@ verts_ordered_V
[i
].co
2981 if selection_type
== "TWO_NOT_CONNECTED":
2982 if self
.selection_V2_exists
:
2983 for i
in range(0, len(surface_splines_parsed
[0])):
2984 surface_splines_parsed
[0][i
] = self
.main_object
.matrix_world
@ verts_ordered_V2
[i
].co
2986 # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
2987 # merge the verts of the tips of the loops when they are "near enough"
2988 if self
.automatic_join
and selection_type
!= "TWO_CONNECTED":
2989 # Join the tips of "Follow" loops that are near enough and must be "closed"
2990 if not self
.selection_V_exists
and len(edges_proportions_U
) >= 3:
2991 for i
in range(len(surface_splines_parsed
[0])):
2992 sp
= surface_splines_parsed
2993 loop_segment_dist
= (sp
[0][i
] - sp
[1][i
]).length
2995 verts_middle_position_co
= [
2996 (sp
[0][i
][0] + sp
[len(sp
) - 1][i
][0]) / 2,
2997 (sp
[0][i
][1] + sp
[len(sp
) - 1][i
][1]) / 2,
2998 (sp
[0][i
][2] + sp
[len(sp
) - 1][i
][2]) / 2
3000 points_original
= []
3001 points_original
.append(sp
[1][i
])
3002 points_original
.append(sp
[0][i
])
3005 points_target
.append(sp
[1][i
])
3006 points_target
.append(Vector(verts_middle_position_co
))
3008 vec_A
= points_original
[0] - points_original
[1]
3009 vec_B
= points_target
[0] - points_target
[1]
3010 # check for zero angles, not sure if it is a great fix
3011 if vec_A
.length
!= 0 and vec_B
.length
!= 0:
3012 angle
= vec_A
.angle(vec_B
) / pi
3013 edge_new_length
= (Vector(verts_middle_position_co
) - sp
[1][i
]).length
3018 # If after moving the verts to the middle point, the segment doesn't stretch too much
3019 if edge_new_length
<= loop_segment_dist
* 1.5 * \
3020 self
.join_stretch_factor
and angle
< 0.25 * self
.join_stretch_factor
:
3022 # Avoid joining when the actual loop must be merged with the original mesh
3023 if not (self
.selection_U_exists
and i
== 0) and \
3024 not (self
.selection_U2_exists
and i
== len(surface_splines_parsed
[0]) - 1):
3026 # Change the coords of both verts to the middle position
3027 surface_splines_parsed
[0][i
] = verts_middle_position_co
3028 surface_splines_parsed
[len(surface_splines_parsed
) - 1][i
] = verts_middle_position_co
3030 # Delete object with control points and object from grease pencil conversion
3031 bpy
.ops
.object.delete({"selected_objects": [ob_ctrl_pts
]})
3033 bpy
.ops
.object.delete({"selected_objects": splines_U_objects
})
3037 # Get all verts coords
3038 all_surface_verts_co
= []
3039 for i
in range(0, len(surface_splines_parsed
)):
3040 # Get coords of all verts and make a list with them
3041 for pt_co
in surface_splines_parsed
[i
]:
3042 all_surface_verts_co
.append(pt_co
)
3044 # Define verts for each face
3045 all_surface_faces
= []
3046 for i
in range(0, len(all_surface_verts_co
) - len(surface_splines_parsed
[0])):
3047 if ((i
+ 1) / len(surface_splines_parsed
[0]) != int((i
+ 1) / len(surface_splines_parsed
[0]))):
3048 all_surface_faces
.append(
3049 [i
+ 1, i
, i
+ len(surface_splines_parsed
[0]),
3050 i
+ len(surface_splines_parsed
[0]) + 1]
3053 surf_me_name
= "SURFSKIO_surface"
3054 me_surf
= bpy
.data
.meshes
.new(surf_me_name
)
3055 me_surf
.from_pydata(all_surface_verts_co
, [], all_surface_faces
)
3056 ob_surface
= object_utils
.object_data_add(context
, me_surf
)
3057 ob_surface
.location
= (0.0, 0.0, 0.0)
3058 ob_surface
.rotation_euler
= (0.0, 0.0, 0.0)
3059 ob_surface
.scale
= (1.0, 1.0, 1.0)
3061 # Select all the "unselected but participating" verts, from closed selection
3062 # or double selections with middle-vertex, for later join with remove doubles
3063 for v_idx
in single_unselected_verts
:
3064 self
.main_object
.data
.vertices
[v_idx
].select
= True
3066 # Join the new mesh to the main object
3067 ob_surface
.select_set(True)
3068 self
.main_object
.select_set(True)
3069 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3071 bpy
.ops
.object.join('INVOKE_REGION_WIN')
3073 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3075 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN', threshold
=0.0001)
3076 bpy
.ops
.mesh
.normals_make_consistent('INVOKE_REGION_WIN', inside
=False)
3077 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3085 global global_offset
3086 shrinkwrap
= self
.main_object
.modifiers
["Shrinkwrap"]
3087 shrinkwrap
.offset
= global_offset
3088 bpy
.context
.scene
.bsurfaces
.SURFSK_Shrinkwrap_offset
= global_offset
3094 material
= makeMaterial("BSurfaceMesh", global_color
)
3095 if self
.main_object
.data
.materials
:
3096 self
.main_object
.data
.materials
[0] = material
3098 self
.main_object
.data
.materials
.append(material
)
3099 bpy
.context
.scene
.bsurfaces
.SURFSK_mesh_color
= global_color
3104 global global_in_front
3105 self
.main_object
.show_in_front
= global_in_front
3106 bpy
.context
.scene
.bsurfaces
.SURFSK_in_front
= global_in_front
3111 global global_show_wire
3112 self
.main_object
.show_wire
= global_show_wire
3113 bpy
.context
.scene
.bsurfaces
.SURFSK_show_wire
= global_show_wire
3118 global global_shade_smooth
3119 if global_shade_smooth
:
3120 bpy
.ops
.object.shade_smooth()
3122 bpy
.ops
.object.shade_flat()
3123 bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
= global_shade_smooth
3129 def execute(self
, context
):
3131 if bpy
.ops
.object.mode_set
.poll():
3132 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3135 global global_mesh_object
3136 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
3137 bpy
.data
.objects
[global_mesh_object
].select_set(True)
3138 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
3139 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3140 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3142 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3144 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3148 if not self
.is_fill_faces
:
3149 bpy
.ops
.wm
.context_set_value(data_path
='tool_settings.mesh_select_mode',
3150 value
='True, False, False')
3152 # Build splines from the "last saved splines".
3153 last_saved_curve
= bpy
.data
.curves
.new('SURFSKIO_last_crv', 'CURVE')
3154 self
.main_splines
= bpy
.data
.objects
.new('SURFSKIO_last_crv', last_saved_curve
)
3155 bpy
.context
.collection
.objects
.link(self
.main_splines
)
3157 last_saved_curve
.dimensions
= "3D"
3159 for sp
in self
.last_strokes_splines_coords
:
3160 spline
= self
.main_splines
.data
.splines
.new('BEZIER')
3161 # less one because one point is added when the spline is created
3162 spline
.bezier_points
.add(len(sp
) - 1)
3163 for p
in range(0, len(sp
)):
3164 spline
.bezier_points
[p
].co
= [sp
[p
][0], sp
[p
][1], sp
[p
][2]]
3166 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3168 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3169 self
.main_splines
.select_set(True)
3170 bpy
.context
.view_layer
.objects
.active
= self
.main_splines
3172 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3174 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
3175 # Important to make it vector first and then automatic, otherwise the
3176 # tips handles get too big and distort the shrinkwrap results later
3177 bpy
.ops
.curve
.handle_type_set(type='VECTOR')
3178 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
3179 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3180 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3182 self
.main_splines
.name
= "SURFSKIO_temp_strokes"
3184 if self
.is_crosshatch
:
3185 strokes_for_crosshatch
= True
3186 strokes_for_rectangular_surface
= False
3188 strokes_for_rectangular_surface
= True
3189 strokes_for_crosshatch
= False
3191 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3193 if strokes_for_rectangular_surface
:
3194 self
.rectangular_surface(context
)
3195 elif strokes_for_crosshatch
:
3196 self
.crosshatch_surface_execute(context
)
3198 #Set Shade smooth to new polygons
3199 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3200 global global_shade_smooth
3201 if global_shade_smooth
:
3202 bpy
.ops
.object.shade_smooth()
3204 bpy
.ops
.object.shade_flat()
3206 # Delete main splines
3207 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3208 if self
.keep_strokes
:
3209 self
.main_splines
.name
= "keep_strokes"
3210 self
.main_splines
.data
.bevel_depth
= 0.001
3211 if "keep_strokes_material" in bpy
.data
.materials
:
3212 self
.main_splines
.data
.materials
.append(bpy
.data
.materials
["keep_strokes_material"])
3214 mat
= bpy
.data
.materials
.new("keep_strokes_material")
3215 mat
.diffuse_color
= (1, 0, 0, 0)
3216 mat
.specular_color
= (1, 0, 0)
3217 mat
.specular_intensity
= 0.0
3219 self
.main_splines
.data
.materials
.append(mat
)
3221 bpy
.ops
.object.delete({"selected_objects": [self
.main_splines
]})
3223 # Delete grease pencil strokes
3224 if self
.strokes_type
== "GP_STROKES" and not self
.stopping_errors
:
3226 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3230 # Delete annotations
3231 if self
.strokes_type
== "GP_ANNOTATION" and not self
.stopping_errors
:
3233 bpy
.context
.annotation_data
.layers
.active
.clear()
3237 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3238 bsurfaces_props
.SURFSK_edges_U
= self
.edges_U
3239 bsurfaces_props
.SURFSK_edges_V
= self
.edges_V
3240 bsurfaces_props
.SURFSK_cyclic_cross
= self
.cyclic_cross
3241 bsurfaces_props
.SURFSK_cyclic_follow
= self
.cyclic_follow
3242 bsurfaces_props
.SURFSK_automatic_join
= self
.automatic_join
3243 bsurfaces_props
.SURFSK_loops_on_strokes
= self
.loops_on_strokes
3244 bsurfaces_props
.SURFSK_keep_strokes
= self
.keep_strokes
3246 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3247 self
.main_object
.select_set(True)
3248 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3250 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3256 def invoke(self
, context
, event
):
3258 if bpy
.ops
.object.mode_set
.poll():
3259 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3261 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3262 self
.cyclic_cross
= bsurfaces_props
.SURFSK_cyclic_cross
3263 self
.cyclic_follow
= bsurfaces_props
.SURFSK_cyclic_follow
3264 self
.automatic_join
= bsurfaces_props
.SURFSK_automatic_join
3265 self
.loops_on_strokes
= bsurfaces_props
.SURFSK_loops_on_strokes
3266 self
.keep_strokes
= bsurfaces_props
.SURFSK_keep_strokes
3269 global global_mesh_object
3270 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
3271 bpy
.data
.objects
[global_mesh_object
].select_set(True)
3272 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
3273 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3275 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3280 self
.main_object_selected_verts_count
= len([v
for v
in self
.main_object
.data
.vertices
if v
.select
])
3282 bpy
.ops
.wm
.context_set_value(data_path
='tool_settings.mesh_select_mode',
3283 value
='True, False, False')
3285 self
.edges_U
= bsurfaces_props
.SURFSK_edges_U
3286 self
.edges_V
= bsurfaces_props
.SURFSK_edges_V
3288 self
.is_fill_faces
= False
3289 self
.stopping_errors
= False
3290 self
.last_strokes_splines_coords
= []
3292 # Determine the type of the strokes
3293 self
.strokes_type
= get_strokes_type(context
)
3295 # Check if it will be used grease pencil strokes or curves
3296 # If there are strokes to be used
3297 if self
.strokes_type
== "GP_STROKES" or self
.strokes_type
== "EXTERNAL_CURVE" or self
.strokes_type
== "GP_ANNOTATION":
3298 if self
.strokes_type
== "GP_STROKES":
3299 # Convert grease pencil strokes to curve
3300 global global_gpencil_object
3301 gp
= bpy
.data
.objects
[global_gpencil_object
]
3302 self
.original_curve
= conver_gpencil_to_curve(self
, context
, gp
, 'GPensil')
3303 self
.using_external_curves
= False
3305 elif self
.strokes_type
== "GP_ANNOTATION":
3306 # Convert grease pencil strokes to curve
3307 gp
= bpy
.context
.annotation_data
3308 self
.original_curve
= conver_gpencil_to_curve(self
, context
, gp
, 'Annotation')
3309 self
.using_external_curves
= False
3311 elif self
.strokes_type
== "EXTERNAL_CURVE":
3312 global global_curve_object
3313 self
.original_curve
= bpy
.data
.objects
[global_curve_object
]
3314 self
.using_external_curves
= True
3316 # Make sure there are no objects left from erroneous
3317 # executions of this operator, with the reserved names used here
3318 for o
in bpy
.data
.objects
:
3319 if o
.name
.find("SURFSKIO_") != -1:
3320 bpy
.ops
.object.delete({"selected_objects": [o
]})
3322 bpy
.context
.view_layer
.objects
.active
= self
.original_curve
3324 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3326 self
.temporary_curve
= bpy
.context
.view_layer
.objects
.active
3328 # Deselect all points of the curve
3329 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3330 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3331 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3333 # Delete splines with only a single isolated point
3334 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3335 sp
= self
.temporary_curve
.data
.splines
[i
]
3337 if len(sp
.bezier_points
) == 1:
3338 sp
.bezier_points
[0].select_control_point
= True
3340 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3341 bpy
.ops
.curve
.delete(type='VERT')
3342 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3344 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3345 self
.temporary_curve
.select_set(True)
3346 bpy
.context
.view_layer
.objects
.active
= self
.temporary_curve
3348 # Set a minimum number of points for crosshatch
3349 minimum_points_num
= 15
3351 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3352 # Check if the number of points of each curve has at least the number of points
3353 # of minimum_points_num, which is a bit more than the face-loops limit.
3354 # If not, subdivide to reach at least that number of points
3355 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3356 sp
= self
.temporary_curve
.data
.splines
[i
]
3358 if len(sp
.bezier_points
) < minimum_points_num
:
3359 for bp
in sp
.bezier_points
:
3360 bp
.select_control_point
= True
3362 if (len(sp
.bezier_points
) - 1) != 0:
3363 # Formula to get the number of cuts that will make a curve
3364 # of N number of points have near to "minimum_points_num"
3365 # points, when subdividing with this number of cuts
3366 subdivide_cuts
= int(
3367 (minimum_points_num
- len(sp
.bezier_points
)) /
3368 (len(sp
.bezier_points
) - 1)
3373 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3374 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3376 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3378 # Detect if the strokes are a crosshatch and do it if it is
3379 self
.crosshatch_surface_invoke(self
.temporary_curve
)
3381 if not self
.is_crosshatch
:
3382 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3383 self
.temporary_curve
.select_set(True)
3384 bpy
.context
.view_layer
.objects
.active
= self
.temporary_curve
3386 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3388 # Set a minimum number of points for rectangular surfaces
3389 minimum_points_num
= 60
3391 # Check if the number of points of each curve has at least the number of points
3392 # of minimum_points_num, which is a bit more than the face-loops limit.
3393 # If not, subdivide to reach at least that number of points
3394 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3395 sp
= self
.temporary_curve
.data
.splines
[i
]
3397 if len(sp
.bezier_points
) < minimum_points_num
:
3398 for bp
in sp
.bezier_points
:
3399 bp
.select_control_point
= True
3401 if (len(sp
.bezier_points
) - 1) != 0:
3402 # Formula to get the number of cuts that will make a curve of
3403 # N number of points have near to "minimum_points_num" points,
3404 # when subdividing with this number of cuts
3405 subdivide_cuts
= int(
3406 (minimum_points_num
- len(sp
.bezier_points
)) /
3407 (len(sp
.bezier_points
) - 1)
3412 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3413 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3415 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3417 # Save coordinates of the actual strokes (as the "last saved splines")
3418 for sp_idx
in range(len(self
.temporary_curve
.data
.splines
)):
3419 self
.last_strokes_splines_coords
.append([])
3420 for bp_idx
in range(len(self
.temporary_curve
.data
.splines
[sp_idx
].bezier_points
)):
3421 coords
= self
.temporary_curve
.matrix_world
@ \
3422 self
.temporary_curve
.data
.splines
[sp_idx
].bezier_points
[bp_idx
].co
3423 self
.last_strokes_splines_coords
[sp_idx
].append([coords
[0], coords
[1], coords
[2]])
3425 # Check for cyclic splines, put the first and last points in the middle of their actual positions
3426 for sp_idx
in range(len(self
.temporary_curve
.data
.splines
)):
3427 if self
.temporary_curve
.data
.splines
[sp_idx
].use_cyclic_u
is True:
3428 first_p_co
= self
.last_strokes_splines_coords
[sp_idx
][0]
3429 last_p_co
= self
.last_strokes_splines_coords
[sp_idx
][
3430 len(self
.last_strokes_splines_coords
[sp_idx
]) - 1
3433 (first_p_co
[0] + last_p_co
[0]) / 2,
3434 (first_p_co
[1] + last_p_co
[1]) / 2,
3435 (first_p_co
[2] + last_p_co
[2]) / 2
3438 self
.last_strokes_splines_coords
[sp_idx
][0] = target_co
3439 self
.last_strokes_splines_coords
[sp_idx
][
3440 len(self
.last_strokes_splines_coords
[sp_idx
]) - 1
3442 tuple(self
.last_strokes_splines_coords
)
3444 # Estimation of the average length of the segments between
3445 # each point of the grease pencil strokes.
3446 # Will be useful to determine whether a curve should be made "Cyclic"
3447 segments_lengths_sum
= 0
3449 random_spline
= self
.temporary_curve
.data
.splines
[0].bezier_points
3450 for i
in range(0, len(random_spline
)):
3451 if i
!= 0 and len(random_spline
) - 1 >= i
:
3452 segments_lengths_sum
+= (random_spline
[i
- 1].co
- random_spline
[i
].co
).length
3455 self
.average_gp_segment_length
= segments_lengths_sum
/ segments_count
3457 # Delete temporary strokes curve object
3458 bpy
.ops
.object.delete({"selected_objects": [self
.temporary_curve
]})
3460 # Set again since "execute()" will turn it again to its initial value
3461 self
.execute(context
)
3463 if not self
.stopping_errors
:
3464 # Delete grease pencil strokes
3465 if self
.strokes_type
== "GP_STROKES":
3467 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3471 # Delete annotation strokes
3472 elif self
.strokes_type
== "GP_ANNOTATION":
3474 bpy
.context
.annotation_data
.layers
.active
.clear()
3478 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3479 bpy
.ops
.object.delete({"selected_objects": [self
.original_curve
]})
3480 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3486 elif self
.strokes_type
== "SELECTION_ALONE":
3487 self
.is_fill_faces
= True
3488 created_faces_count
= self
.fill_with_faces(self
.main_object
)
3490 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3492 if created_faces_count
== 0:
3493 self
.report({'WARNING'}, "There aren't any strokes attached to the object")
3494 return {"CANCELLED"}
3498 if self
.strokes_type
== "EXTERNAL_NO_CURVE":
3499 self
.report({'WARNING'}, "The secondary object is not a Curve.")
3502 elif self
.strokes_type
== "MORE_THAN_ONE_EXTERNAL":
3503 self
.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3506 elif self
.strokes_type
== "SINGLE_GP_STROKE_NO_SELECTION" or \
3507 self
.strokes_type
== "SINGLE_CURVE_STROKE_NO_SELECTION":
3509 self
.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
3512 elif self
.strokes_type
== "NO_STROKES":
3513 self
.report({'WARNING'}, "There aren't any strokes attached to the object")
3516 elif self
.strokes_type
== "CURVE_WITH_NON_BEZIER_SPLINES":
3517 self
.report({'WARNING'}, "All splines must be Bezier.")
3523 # ----------------------------
3525 class MESH_OT_SURFSK_init(Operator
):
3526 bl_idname
= "mesh.surfsk_init"
3527 bl_label
= "Bsurfaces initialize"
3528 bl_description
= "Add an empty mesh object with useful settings"
3529 bl_options
= {'REGISTER', 'UNDO'}
3531 def execute(self
, context
):
3533 bs
= bpy
.context
.scene
.bsurfaces
3535 if bpy
.ops
.object.mode_set
.poll():
3536 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3539 global global_offset
3540 global global_in_front
3541 global global_show_wire
3542 global global_shade_smooth
3543 global global_mesh_object
3544 global global_gpencil_object
3546 if bs
.SURFSK_mesh
== None:
3547 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3548 mesh
= bpy
.data
.meshes
.new('BSurfaceMesh')
3549 mesh_object
= object_utils
.object_data_add(context
, mesh
)
3550 mesh_object
.select_set(True)
3551 bpy
.context
.view_layer
.objects
.active
= mesh_object
3553 mesh_object
.show_all_edges
= True
3554 global_in_front
= bpy
.context
.scene
.bsurfaces
.SURFSK_in_front
3555 mesh_object
.show_in_front
= global_in_front
3556 mesh_object
.display_type
= 'SOLID'
3557 mesh_object
.show_wire
= True
3559 global_shade_smooth
= bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
3560 if global_shade_smooth
:
3561 bpy
.ops
.object.shade_smooth()
3563 bpy
.ops
.object.shade_flat()
3565 global_show_wire
= bpy
.context
.scene
.bsurfaces
.SURFSK_show_wire
3566 mesh_object
.show_wire
= global_show_wire
3568 global_color
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh_color
3569 material
= makeMaterial("BSurfaceMesh", global_color
)
3570 mesh_object
.data
.materials
.append(material
)
3571 bpy
.ops
.object.modifier_add(type='SHRINKWRAP')
3572 modifier
= mesh_object
.modifiers
["Shrinkwrap"]
3573 if self
.active_object
is not None:
3574 modifier
.target
= self
.active_object
3575 modifier
.wrap_method
= 'TARGET_PROJECT'
3576 modifier
.wrap_mode
= 'OUTSIDE_SURFACE'
3577 modifier
.show_on_cage
= True
3578 global_offset
= bpy
.context
.scene
.bsurfaces
.SURFSK_Shrinkwrap_offset
3579 modifier
.offset
= global_offset
3581 global_mesh_object
= mesh_object
.name
3582 bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
= bpy
.data
.objects
[global_mesh_object
]
3584 bpy
.context
.scene
.tool_settings
.snap_elements
= {'FACE'}
3585 bpy
.context
.scene
.tool_settings
.use_snap
= True
3586 bpy
.context
.scene
.tool_settings
.use_snap_self
= False
3587 bpy
.context
.scene
.tool_settings
.use_snap_align_rotation
= True
3588 bpy
.context
.scene
.tool_settings
.use_snap_project
= True
3589 bpy
.context
.scene
.tool_settings
.use_snap_rotate
= True
3590 bpy
.context
.scene
.tool_settings
.use_snap_scale
= True
3592 bpy
.context
.scene
.tool_settings
.use_mesh_automerge
= True
3593 bpy
.context
.scene
.tool_settings
.double_threshold
= 0.01
3595 if context
.scene
.bsurfaces
.SURFSK_guide
== 'GPencil' and bs
.SURFSK_gpencil
== None:
3596 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3597 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')
3598 bpy
.context
.scene
.tool_settings
.gpencil_stroke_placement_view3d
= 'SURFACE'
3599 gpencil_object
= bpy
.context
.scene
.objects
[bpy
.context
.scene
.objects
[-1].name
]
3600 gpencil_object
.select_set(True)
3601 bpy
.context
.view_layer
.objects
.active
= gpencil_object
3602 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3603 global_gpencil_object
= gpencil_object
.name
3604 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
= bpy
.data
.objects
[global_gpencil_object
]
3605 gpencil_object
.data
.stroke_depth_order
= '3D'
3606 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3607 bpy
.ops
.wm
.tool_set_by_id(name
="builtin_brush.Draw")
3609 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Annotation':
3610 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.annotate")
3611 bpy
.context
.scene
.tool_settings
.annotation_stroke_placement_view3d
= 'SURFACE'
3613 def invoke(self
, context
, event
):
3614 if bpy
.context
.active_object
:
3615 self
.active_object
= bpy
.context
.active_object
3617 self
.active_object
= None
3619 self
.execute(context
)
3623 # ----------------------------
3624 # Add modifiers operator
3625 class MESH_OT_SURFSK_add_modifiers(Operator
):
3626 bl_idname
= "mesh.surfsk_add_modifiers"
3627 bl_label
= "Add Mirror and others modifiers"
3628 bl_description
= "Add modifiers: Mirror, Shrinkwrap, Subdivision, Solidify"
3629 bl_options
= {'REGISTER', 'UNDO'}
3631 def execute(self
, context
):
3633 bs
= bpy
.context
.scene
.bsurfaces
3635 if bpy
.ops
.object.mode_set
.poll():
3636 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3638 if bs
.SURFSK_mesh
== None:
3639 self
.report({'ERROR_INVALID_INPUT'}, "Please select Mesh of BSurface or click Initialize")
3641 mesh_object
= bs
.SURFSK_mesh
3644 mesh_object
.select_set(True)
3646 self
.report({'ERROR_INVALID_INPUT'}, "Mesh of BSurface does not exist")
3649 bpy
.context
.view_layer
.objects
.active
= mesh_object
3652 shrinkwrap
= mesh_object
.modifiers
["Shrinkwrap"]
3653 if self
.active_object
is not None and self
.active_object
!= mesh_object
:
3654 shrinkwrap
.target
= self
.active_object
3655 shrinkwrap
.wrap_method
= 'TARGET_PROJECT'
3656 shrinkwrap
.wrap_mode
= 'OUTSIDE_SURFACE'
3657 shrinkwrap
.show_on_cage
= True
3658 shrinkwrap
.offset
= bpy
.context
.scene
.bsurfaces
.SURFSK_Shrinkwrap_offset
3660 bpy
.ops
.object.modifier_add(type='SHRINKWRAP')
3661 shrinkwrap
= mesh_object
.modifiers
["Shrinkwrap"]
3662 if self
.active_object
is not None and self
.active_object
!= mesh_object
:
3663 shrinkwrap
.target
= self
.active_object
3664 shrinkwrap
.wrap_method
= 'TARGET_PROJECT'
3665 shrinkwrap
.wrap_mode
= 'OUTSIDE_SURFACE'
3666 shrinkwrap
.show_on_cage
= True
3667 shrinkwrap
.offset
= bpy
.context
.scene
.bsurfaces
.SURFSK_Shrinkwrap_offset
3670 mirror
= mesh_object
.modifiers
["Mirror"]
3671 mirror
.use_clip
= True
3673 bpy
.ops
.object.modifier_add(type='MIRROR')
3674 mirror
= mesh_object
.modifiers
["Mirror"]
3675 mirror
.use_clip
= True
3678 _subsurf
= mesh_object
.modifiers
["Subdivision"]
3680 bpy
.ops
.object.modifier_add(type='SUBSURF')
3681 _subsurf
= mesh_object
.modifiers
["Subdivision"]
3684 solidify
= mesh_object
.modifiers
["Solidify"]
3685 solidify
.thickness
= 0.01
3687 bpy
.ops
.object.modifier_add(type='SOLIDIFY')
3688 solidify
= mesh_object
.modifiers
["Solidify"]
3689 solidify
.thickness
= 0.01
3693 def invoke(self
, context
, event
):
3694 if bpy
.context
.active_object
:
3695 self
.active_object
= bpy
.context
.active_object
3697 self
.active_object
= None
3699 self
.execute(context
)
3703 # ----------------------------
3704 # Edit surface operator
3705 class MESH_OT_SURFSK_edit_surface(Operator
):
3706 bl_idname
= "mesh.surfsk_edit_surface"
3707 bl_label
= "Bsurfaces edit surface"
3708 bl_description
= "Edit surface mesh"
3709 bl_options
= {'REGISTER', 'UNDO'}
3711 def execute(self
, context
):
3712 if bpy
.ops
.object.mode_set
.poll():
3713 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3714 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3715 bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.select_set(True)
3716 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
3717 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3718 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select")
3720 def invoke(self
, context
, event
):
3722 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
3723 bpy
.data
.objects
[global_mesh_object
].select_set(True)
3724 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
3725 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3727 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3730 self
.execute(context
)
3734 # ----------------------------
3735 # Add strokes operator
3736 class GPENCIL_OT_SURFSK_add_strokes(Operator
):
3737 bl_idname
= "gpencil.surfsk_add_strokes"
3738 bl_label
= "Bsurfaces add strokes"
3739 bl_description
= "Add the grease pencil strokes"
3740 bl_options
= {'REGISTER', 'UNDO'}
3742 def execute(self
, context
):
3743 if bpy
.ops
.object.mode_set
.poll():
3744 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3745 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3747 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3748 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3749 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3750 bpy
.ops
.wm
.tool_set_by_id(name
="builtin_brush.Draw")
3754 def invoke(self
, context
, event
):
3756 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3758 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3761 self
.execute(context
)
3765 # ----------------------------
3766 # Edit strokes operator
3767 class GPENCIL_OT_SURFSK_edit_strokes(Operator
):
3768 bl_idname
= "gpencil.surfsk_edit_strokes"
3769 bl_label
= "Bsurfaces edit strokes"
3770 bl_description
= "Edit the grease pencil strokes"
3771 bl_options
= {'REGISTER', 'UNDO'}
3773 def execute(self
, context
):
3774 if bpy
.ops
.object.mode_set
.poll():
3775 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3776 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3778 gpencil_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3780 gpencil_object
.select_set(True)
3781 bpy
.context
.view_layer
.objects
.active
= gpencil_object
3783 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT_GPENCIL')
3785 bpy
.ops
.gpencil
.select_all(action
='SELECT')
3789 def invoke(self
, context
, event
):
3791 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3793 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3796 self
.execute(context
)
3800 # ----------------------------
3801 # Convert annotation to curves operator
3802 class GPENCIL_OT_SURFSK_annotation_to_curves(Operator
):
3803 bl_idname
= "gpencil.surfsk_annotations_to_curves"
3804 bl_label
= "Convert annotation to curves"
3805 bl_description
= "Convert annotation to curves for editing"
3806 bl_options
= {'REGISTER', 'UNDO'}
3808 def execute(self
, context
):
3810 if bpy
.ops
.object.mode_set
.poll():
3811 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3813 # Convert annotation to curve
3814 curve
= conver_gpencil_to_curve(self
, context
, None, 'Annotation')
3817 # Delete annotation strokes
3819 bpy
.context
.annotation_data
.layers
.active
.clear()
3824 curve
.select_set(True)
3825 bpy
.context
.view_layer
.objects
.active
= curve
3827 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select_box")
3831 def invoke(self
, context
, event
):
3833 strokes
= bpy
.context
.annotation_data
.layers
.active
.active_frame
.strokes
3835 _strokes_num
= len(strokes
)
3837 self
.report({'WARNING'}, "Not active annotation")
3840 self
.execute(context
)
3844 # ----------------------------
3845 # Convert strokes to curves operator
3846 class GPENCIL_OT_SURFSK_strokes_to_curves(Operator
):
3847 bl_idname
= "gpencil.surfsk_strokes_to_curves"
3848 bl_label
= "Convert strokes to curves"
3849 bl_description
= "Convert grease pencil strokes to curves for editing"
3850 bl_options
= {'REGISTER', 'UNDO'}
3852 def execute(self
, context
):
3854 if bpy
.ops
.object.mode_set
.poll():
3855 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3857 # Convert grease pencil strokes to curve
3858 gp
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3859 curve
= conver_gpencil_to_curve(self
, context
, gp
, 'GPensil')
3862 # Delete grease pencil strokes
3864 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3870 curve
.select_set(True)
3871 bpy
.context
.view_layer
.objects
.active
= curve
3873 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select_box")
3877 def invoke(self
, context
, event
):
3879 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3881 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3884 self
.execute(context
)
3888 # ----------------------------
3890 class GPENCIL_OT_SURFSK_add_annotation(Operator
):
3891 bl_idname
= "gpencil.surfsk_add_annotation"
3892 bl_label
= "Bsurfaces add annotation"
3893 bl_description
= "Add annotation"
3894 bl_options
= {'REGISTER', 'UNDO'}
3896 def execute(self
, context
):
3897 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.annotate")
3898 bpy
.context
.scene
.tool_settings
.annotation_stroke_placement_view3d
= 'SURFACE'
3902 def invoke(self
, context
, event
):
3904 self
.execute(context
)
3909 # ----------------------------
3910 # Edit curve operator
3911 class CURVE_OT_SURFSK_edit_curve(Operator
):
3912 bl_idname
= "curve.surfsk_edit_curve"
3913 bl_label
= "Bsurfaces edit curve"
3914 bl_description
= "Edit curve"
3915 bl_options
= {'REGISTER', 'UNDO'}
3917 def execute(self
, context
):
3918 if bpy
.ops
.object.mode_set
.poll():
3919 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3920 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3921 bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.select_set(True)
3922 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_curve
3923 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3925 def invoke(self
, context
, event
):
3927 bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.select_set(True)
3929 self
.report({'WARNING'}, "Specify the name of the object with curve")
3932 self
.execute(context
)
3936 # ----------------------------
3938 class CURVE_OT_SURFSK_reorder_splines(Operator
):
3939 bl_idname
= "curve.surfsk_reorder_splines"
3940 bl_label
= "Bsurfaces reorder splines"
3941 bl_description
= "Defines the order of the splines by using grease pencil strokes"
3942 bl_options
= {'REGISTER', 'UNDO'}
3944 def execute(self
, context
):
3945 objects_to_delete
= []
3946 # Convert grease pencil strokes to curve.
3947 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3948 bpy
.ops
.gpencil
.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes
=False)
3949 for ob
in bpy
.context
.selected_objects
:
3950 if ob
!= bpy
.context
.view_layer
.objects
.active
and ob
.name
.startswith("GP_Layer"):
3951 GP_strokes_curve
= ob
3953 # GP_strokes_curve = bpy.context.object
3954 objects_to_delete
.append(GP_strokes_curve
)
3956 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3957 GP_strokes_curve
.select_set(True)
3958 bpy
.context
.view_layer
.objects
.active
= GP_strokes_curve
3960 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3961 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
3962 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=100)
3963 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3965 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3966 GP_strokes_mesh
= bpy
.context
.object
3967 objects_to_delete
.append(GP_strokes_mesh
)
3969 GP_strokes_mesh
.data
.resolution_u
= 1
3970 bpy
.ops
.object.convert(target
='MESH', keep_original
=False)
3972 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3973 self
.main_curve
.select_set(True)
3974 bpy
.context
.view_layer
.objects
.active
= self
.main_curve
3976 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3977 curves_duplicate_1
= bpy
.context
.object
3978 objects_to_delete
.append(curves_duplicate_1
)
3980 minimum_points_num
= 500
3982 # Some iterations since the subdivision operator
3983 # has a limit of 100 subdivisions per iteration
3984 for x
in range(round(minimum_points_num
/ 100)):
3985 # Check if the number of points of each curve has at least the number of points
3986 # of minimum_points_num. If not, subdivide to reach at least that number of points
3987 for i
in range(len(curves_duplicate_1
.data
.splines
)):
3988 sp
= curves_duplicate_1
.data
.splines
[i
]
3990 if len(sp
.bezier_points
) < minimum_points_num
:
3991 for bp
in sp
.bezier_points
:
3992 bp
.select_control_point
= True
3994 if (len(sp
.bezier_points
) - 1) != 0:
3995 # Formula to get the number of cuts that will make a curve of N
3996 # number of points have near to "minimum_points_num" points,
3997 # when subdividing with this number of cuts
3998 subdivide_cuts
= int(
3999 (minimum_points_num
- len(sp
.bezier_points
)) /
4000 (len(sp
.bezier_points
) - 1)
4005 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4006 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
4007 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4008 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4010 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
4011 curves_duplicate_2
= bpy
.context
.object
4012 objects_to_delete
.append(curves_duplicate_2
)
4014 # Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target
4015 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4016 curves_duplicate_2
.select_set(True)
4017 bpy
.context
.view_layer
.objects
.active
= curves_duplicate_2
4019 bpy
.ops
.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
4020 curves_duplicate_2
.modifiers
["Shrinkwrap"].wrap_method
= "NEAREST_VERTEX"
4021 curves_duplicate_2
.modifiers
["Shrinkwrap"].target
= GP_strokes_mesh
4022 bpy
.ops
.object.modifier_apply('INVOKE_REGION_WIN', modifier
='Shrinkwrap')
4024 # Get the distance of each vert from its original position to its position with Shrinkwrap
4025 nearest_points_coords
= {}
4026 for st_idx
in range(len(curves_duplicate_1
.data
.splines
)):
4027 for bp_idx
in range(len(curves_duplicate_1
.data
.splines
[st_idx
].bezier_points
)):
4028 bp_1_co
= curves_duplicate_1
.matrix_world
@ \
4029 curves_duplicate_1
.data
.splines
[st_idx
].bezier_points
[bp_idx
].co
4031 bp_2_co
= curves_duplicate_2
.matrix_world
@ \
4032 curves_duplicate_2
.data
.splines
[st_idx
].bezier_points
[bp_idx
].co
4035 shortest_dist
= (bp_1_co
- bp_2_co
).length
4036 nearest_points_coords
[st_idx
] = ("%.4f" % bp_2_co
[0],
4037 "%.4f" % bp_2_co
[1],
4038 "%.4f" % bp_2_co
[2])
4040 dist
= (bp_1_co
- bp_2_co
).length
4042 if dist
< shortest_dist
:
4043 nearest_points_coords
[st_idx
] = ("%.4f" % bp_2_co
[0],
4044 "%.4f" % bp_2_co
[1],
4045 "%.4f" % bp_2_co
[2])
4046 shortest_dist
= dist
4048 # Get all coords of GP strokes points, for comparison
4049 GP_strokes_coords
= []
4050 for st_idx
in range(len(GP_strokes_curve
.data
.splines
)):
4051 GP_strokes_coords
.append(
4052 [("%.4f" % x
if "%.4f" % x
!= "-0.00" else "0.00",
4053 "%.4f" % y
if "%.4f" % y
!= "-0.00" else "0.00",
4054 "%.4f" % z
if "%.4f" % z
!= "-0.00" else "0.00") for
4055 x
, y
, z
in [bp
.co
for bp
in GP_strokes_curve
.data
.splines
[st_idx
].bezier_points
]]
4058 # Check the point of the GP strokes with the same coords as
4059 # the nearest points of the curves (with shrinkwrap)
4061 # Dictionary with GP stroke index as index, and a list as value.
4062 # The list has as index the point index of the GP stroke
4063 # nearest to the spline, and as value the spline index
4064 GP_connection_points
= {}
4065 for gp_st_idx
in range(len(GP_strokes_coords
)):
4066 GPvert_spline_relationship
= {}
4068 for splines_st_idx
in range(len(nearest_points_coords
)):
4069 if nearest_points_coords
[splines_st_idx
] in GP_strokes_coords
[gp_st_idx
]:
4070 GPvert_spline_relationship
[
4071 GP_strokes_coords
[gp_st_idx
].index(nearest_points_coords
[splines_st_idx
])
4074 GP_connection_points
[gp_st_idx
] = GPvert_spline_relationship
4076 # Get the splines new order
4077 splines_new_order
= []
4078 for i
in GP_connection_points
:
4079 dict_keys
= sorted(GP_connection_points
[i
].keys()) # Sort dictionaries by key
4082 splines_new_order
.append(GP_connection_points
[i
][k
])
4085 curve_original_name
= self
.main_curve
.name
4087 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4088 self
.main_curve
.select_set(True)
4089 bpy
.context
.view_layer
.objects
.active
= self
.main_curve
4091 self
.main_curve
.name
= "SURFSKIO_CRV_ORD"
4093 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4094 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4095 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4097 for _sp_idx
in range(len(self
.main_curve
.data
.splines
)):
4098 self
.main_curve
.data
.splines
[0].bezier_points
[0].select_control_point
= True
4100 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4101 bpy
.ops
.curve
.separate('EXEC_REGION_WIN')
4102 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4104 # Get the names of the separated splines objects in the original order
4105 splines_unordered
= {}
4106 for o
in bpy
.data
.objects
:
4107 if o
.name
.find("SURFSKIO_CRV_ORD") != -1:
4108 spline_order_string
= o
.name
.partition(".")[2]
4110 if spline_order_string
!= "" and int(spline_order_string
) > 0:
4111 spline_order_index
= int(spline_order_string
) - 1
4112 splines_unordered
[spline_order_index
] = o
.name
4114 # Join all splines objects in final order
4115 for order_idx
in splines_new_order
:
4116 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4117 bpy
.data
.objects
[splines_unordered
[order_idx
]].select_set(True)
4118 bpy
.data
.objects
["SURFSKIO_CRV_ORD"].select_set(True)
4119 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
["SURFSKIO_CRV_ORD"]
4121 bpy
.ops
.object.join('INVOKE_REGION_WIN')
4123 # Go back to the original name of the curves object.
4124 bpy
.context
.object.name
= curve_original_name
4126 # Delete all unused objects
4127 bpy
.ops
.object.delete({"selected_objects": objects_to_delete
})
4129 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4130 bpy
.data
.objects
[curve_original_name
].select_set(True)
4131 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[curve_original_name
]
4133 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4134 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4137 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
4144 def invoke(self
, context
, event
):
4145 self
.main_curve
= bpy
.context
.object
4146 there_are_GP_strokes
= False
4149 # Get the active grease pencil layer
4150 strokes_num
= len(self
.main_curve
.grease_pencil
.layers
.active
.active_frame
.strokes
)
4153 there_are_GP_strokes
= True
4157 if there_are_GP_strokes
:
4158 self
.execute(context
)
4159 self
.report({'INFO'}, "Splines have been reordered")
4161 self
.report({'WARNING'}, "Draw grease pencil strokes to connect splines")
4165 # ----------------------------
4166 # Set first points operator
4167 class CURVE_OT_SURFSK_first_points(Operator
):
4168 bl_idname
= "curve.surfsk_first_points"
4169 bl_label
= "Bsurfaces set first points"
4170 bl_description
= "Set the selected points as the first point of each spline"
4171 bl_options
= {'REGISTER', 'UNDO'}
4173 def execute(self
, context
):
4174 splines_to_invert
= []
4176 # Check non-cyclic splines to invert
4177 for i
in range(len(self
.main_curve
.data
.splines
)):
4178 b_points
= self
.main_curve
.data
.splines
[i
].bezier_points
4180 if i
not in self
.cyclic_splines
: # Only for non-cyclic splines
4181 if b_points
[len(b_points
) - 1].select_control_point
:
4182 splines_to_invert
.append(i
)
4184 # Reorder points of cyclic splines, and set all handles to "Automatic"
4186 # Check first selected point
4187 cyclic_splines_new_first_pt
= {}
4188 for i
in self
.cyclic_splines
:
4189 sp
= self
.main_curve
.data
.splines
[i
]
4191 for t
in range(len(sp
.bezier_points
)):
4192 bp
= sp
.bezier_points
[t
]
4193 if bp
.select_control_point
or bp
.select_right_handle
or bp
.select_left_handle
:
4194 cyclic_splines_new_first_pt
[i
] = t
4195 break # To take only one if there are more
4198 for spline_idx
in cyclic_splines_new_first_pt
:
4199 sp
= self
.main_curve
.data
.splines
[spline_idx
]
4201 spline_old_coords
= []
4202 for bp_old
in sp
.bezier_points
:
4203 coords
= (bp_old
.co
[0], bp_old
.co
[1], bp_old
.co
[2])
4205 left_handle_type
= str(bp_old
.handle_left_type
)
4206 left_handle_length
= float(bp_old
.handle_left
.length
)
4208 float(bp_old
.handle_left
.x
),
4209 float(bp_old
.handle_left
.y
),
4210 float(bp_old
.handle_left
.z
)
4212 right_handle_type
= str(bp_old
.handle_right_type
)
4213 right_handle_length
= float(bp_old
.handle_right
.length
)
4214 right_handle_xyz
= (
4215 float(bp_old
.handle_right
.x
),
4216 float(bp_old
.handle_right
.y
),
4217 float(bp_old
.handle_right
.z
)
4219 spline_old_coords
.append(
4220 [coords
, left_handle_type
,
4221 right_handle_type
, left_handle_length
,
4222 right_handle_length
, left_handle_xyz
,
4226 for t
in range(len(sp
.bezier_points
)):
4227 bp
= sp
.bezier_points
4229 if t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1 <= len(bp
) - 1:
4230 new_index
= t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1
4232 new_index
= t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1 - len(bp
)
4234 bp
[t
].co
= Vector(spline_old_coords
[new_index
][0])
4236 bp
[t
].handle_left
.length
= spline_old_coords
[new_index
][3]
4237 bp
[t
].handle_right
.length
= spline_old_coords
[new_index
][4]
4239 bp
[t
].handle_left_type
= "FREE"
4240 bp
[t
].handle_right_type
= "FREE"
4242 bp
[t
].handle_left
.x
= spline_old_coords
[new_index
][5][0]
4243 bp
[t
].handle_left
.y
= spline_old_coords
[new_index
][5][1]
4244 bp
[t
].handle_left
.z
= spline_old_coords
[new_index
][5][2]
4246 bp
[t
].handle_right
.x
= spline_old_coords
[new_index
][6][0]
4247 bp
[t
].handle_right
.y
= spline_old_coords
[new_index
][6][1]
4248 bp
[t
].handle_right
.z
= spline_old_coords
[new_index
][6][2]
4250 bp
[t
].handle_left_type
= spline_old_coords
[new_index
][1]
4251 bp
[t
].handle_right_type
= spline_old_coords
[new_index
][2]
4253 # Invert the non-cyclic splines designated above
4254 for i
in range(len(splines_to_invert
)):
4255 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4257 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4258 self
.main_curve
.data
.splines
[splines_to_invert
[i
]].bezier_points
[0].select_control_point
= True
4259 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4261 bpy
.ops
.curve
.switch_direction()
4263 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4265 # Keep selected the first vert of each spline
4266 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4267 for i
in range(len(self
.main_curve
.data
.splines
)):
4268 if not self
.main_curve
.data
.splines
[i
].use_cyclic_u
:
4269 bp
= self
.main_curve
.data
.splines
[i
].bezier_points
[0]
4271 bp
= self
.main_curve
.data
.splines
[i
].bezier_points
[
4272 len(self
.main_curve
.data
.splines
[i
].bezier_points
) - 1
4275 bp
.select_control_point
= True
4276 bp
.select_right_handle
= True
4277 bp
.select_left_handle
= True
4279 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4283 def invoke(self
, context
, event
):
4284 self
.main_curve
= bpy
.context
.object
4286 # Check if all curves are Bezier, and detect which ones are cyclic
4287 self
.cyclic_splines
= []
4288 for i
in range(len(self
.main_curve
.data
.splines
)):
4289 if self
.main_curve
.data
.splines
[i
].type != "BEZIER":
4290 self
.report({'WARNING'}, "All splines must be Bezier type")
4292 return {'CANCELLED'}
4294 if self
.main_curve
.data
.splines
[i
].use_cyclic_u
:
4295 self
.cyclic_splines
.append(i
)
4297 self
.execute(context
)
4298 self
.report({'INFO'}, "First points have been set")
4303 # Add-ons Preferences Update Panel
4305 # Define Panel classes for updating
4307 VIEW3D_PT_tools_SURFSK_mesh
,
4308 VIEW3D_PT_tools_SURFSK_curve
4312 def conver_gpencil_to_curve(self
, context
, pencil
, type):
4313 newCurve
= bpy
.data
.curves
.new(type + '_curve', type='CURVE')
4314 newCurve
.dimensions
= '3D'
4315 CurveObject
= object_utils
.object_data_add(context
, newCurve
)
4318 if type == 'GPensil':
4320 strokes
= pencil
.data
.layers
.active
.active_frame
.strokes
4323 CurveObject
.location
= pencil
.location
4324 CurveObject
.rotation_euler
= pencil
.rotation_euler
4325 CurveObject
.scale
= pencil
.scale
4326 elif type == 'Annotation':
4328 strokes
= bpy
.context
.annotation_data
.layers
.active
.active_frame
.strokes
4331 CurveObject
.location
= (0.0, 0.0, 0.0)
4332 CurveObject
.rotation_euler
= (0.0, 0.0, 0.0)
4333 CurveObject
.scale
= (1.0, 1.0, 1.0)
4336 for i
, _stroke
in enumerate(strokes
):
4337 stroke_points
= strokes
[i
].points
4338 data_list
= [ (point
.co
.x
, point
.co
.y
, point
.co
.z
)
4339 for point
in stroke_points
]
4340 points_to_add
= len(data_list
)-1
4343 for point
in data_list
:
4344 flat_list
.extend(point
)
4346 spline
= newCurve
.splines
.new(type='BEZIER')
4347 spline
.bezier_points
.add(points_to_add
)
4348 spline
.bezier_points
.foreach_set("co", flat_list
)
4350 for point
in spline
.bezier_points
:
4351 point
.handle_left_type
="AUTO"
4352 point
.handle_right_type
="AUTO"
4359 def update_panel(self
, context
):
4360 message
= "Bsurfaces GPL Edition: Updating Panel locations has failed"
4362 for panel
in panels
:
4363 if "bl_rna" in panel
.__dict
__:
4364 bpy
.utils
.unregister_class(panel
)
4366 for panel
in panels
:
4367 category
= context
.preferences
.addons
[__name__
].preferences
.category
4368 if category
!= 'Tool':
4369 panel
.bl_category
= context
.preferences
.addons
[__name__
].preferences
.category
4371 context
.preferences
.addons
[__name__
].preferences
.category
= 'Edit'
4372 panel
.bl_category
= 'Edit'
4373 raise ValueError("You can not install add-ons in the Tool panel")
4374 bpy
.utils
.register_class(panel
)
4376 except Exception as e
:
4377 print("\n[{}]\n{}\n\nError:\n{}".format(__name__
, message
, e
))
4380 def makeMaterial(name
, diffuse
):
4382 if name
in bpy
.data
.materials
:
4383 material
= bpy
.data
.materials
[name
]
4384 material
.diffuse_color
= diffuse
4386 material
= bpy
.data
.materials
.new(name
)
4387 material
.diffuse_color
= diffuse
4391 def update_mesh(self
, context
):
4393 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4394 bpy
.ops
.object.select_all(action
='DESELECT')
4395 bpy
.context
.view_layer
.update()
4396 global global_mesh_object
4397 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
4398 bpy
.data
.objects
[global_mesh_object
].select_set(True)
4399 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_mesh_object
]
4401 print("Select mesh object")
4403 def update_gpencil(self
, context
):
4405 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4406 bpy
.ops
.object.select_all(action
='DESELECT')
4407 bpy
.context
.view_layer
.update()
4408 global global_gpencil_object
4409 global_gpencil_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.name
4410 bpy
.data
.objects
[global_gpencil_object
].select_set(True)
4411 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_gpencil_object
]
4413 print("Select gpencil object")
4415 def update_curve(self
, context
):
4417 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4418 bpy
.ops
.object.select_all(action
='DESELECT')
4419 bpy
.context
.view_layer
.update()
4420 global global_curve_object
4421 global_curve_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.name
4422 bpy
.data
.objects
[global_curve_object
].select_set(True)
4423 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_curve_object
]
4425 print("Select curve object")
4427 def update_color(self
, context
):
4430 global global_mesh_object
4431 material
= makeMaterial("BSurfaceMesh", bpy
.context
.scene
.bsurfaces
.SURFSK_mesh_color
)
4432 if bpy
.data
.objects
[global_mesh_object
].data
.materials
:
4433 bpy
.data
.objects
[global_mesh_object
].data
.materials
[0] = material
4435 bpy
.data
.objects
[global_mesh_object
].data
.materials
.append(material
)
4436 diffuse_color
= material
.diffuse_color
4437 global_color
= (diffuse_color
[0], diffuse_color
[1], diffuse_color
[2], diffuse_color
[3])
4439 print("Select mesh object")
4441 def update_Shrinkwrap_offset(self
, context
):
4443 global global_offset
4444 global_offset
= bpy
.context
.scene
.bsurfaces
.SURFSK_Shrinkwrap_offset
4445 global global_mesh_object
4446 modifier
= bpy
.data
.objects
[global_mesh_object
].modifiers
["Shrinkwrap"]
4447 modifier
.offset
= global_offset
4449 print("Shrinkwrap modifier not found")
4451 def update_in_front(self
, context
):
4453 global global_in_front
4454 global_in_front
= bpy
.context
.scene
.bsurfaces
.SURFSK_in_front
4455 global global_mesh_object
4456 bpy
.data
.objects
[global_mesh_object
].show_in_front
= global_in_front
4458 print("Select mesh object")
4460 def update_show_wire(self
, context
):
4462 global global_show_wire
4463 global_show_wire
= bpy
.context
.scene
.bsurfaces
.SURFSK_show_wire
4464 global global_mesh_object
4465 bpy
.data
.objects
[global_mesh_object
].show_wire
= global_show_wire
4467 print("Select mesh object")
4469 def update_shade_smooth(self
, context
):
4471 global global_shade_smooth
4472 global_shade_smooth
= bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
4474 contex_mode
= bpy
.context
.mode
4476 if bpy
.ops
.object.mode_set
.poll():
4477 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4479 bpy
.ops
.object.select_all(action
='DESELECT')
4480 global global_mesh_object
4481 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
4482 bpy
.data
.objects
[global_mesh_object
].select_set(True)
4484 if global_shade_smooth
:
4485 bpy
.ops
.object.shade_smooth()
4487 bpy
.ops
.object.shade_flat()
4489 if contex_mode
== "EDIT_MESH":
4490 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4493 print("Select mesh object")
4496 class BsurfPreferences(AddonPreferences
):
4497 # this must match the addon name, use '__package__'
4498 # when defining this in a submodule of a python package.
4499 bl_idname
= __name__
4501 category
: StringProperty(
4502 name
="Tab Category",
4503 description
="Choose a name for the category of the panel",
4508 def draw(self
, context
):
4509 layout
= self
.layout
4513 col
.label(text
="Tab Category:")
4514 col
.prop(self
, "category", text
="")
4517 class BsurfacesProps(PropertyGroup
):
4518 SURFSK_guide
: EnumProperty(
4521 ('Annotation', 'Annotation', 'Annotation'),
4522 ('GPencil', 'GPencil', 'GPencil'),
4523 ('Curve', 'Curve', 'Curve')
4525 default
="Annotation"
4527 SURFSK_edges_U
: IntProperty(
4529 description
="Number of face-loops crossing the strokes",
4534 SURFSK_edges_V
: IntProperty(
4536 description
="Number of face-loops following the strokes",
4541 SURFSK_cyclic_cross
: BoolProperty(
4542 name
="Cyclic Cross",
4543 description
="Make cyclic the face-loops crossing the strokes",
4546 SURFSK_cyclic_follow
: BoolProperty(
4547 name
="Cyclic Follow",
4548 description
="Make cyclic the face-loops following the strokes",
4551 SURFSK_keep_strokes
: BoolProperty(
4552 name
="Keep strokes",
4553 description
="Keeps the sketched strokes or curves after adding the surface",
4556 SURFSK_automatic_join
: BoolProperty(
4557 name
="Automatic join",
4558 description
="Join automatically vertices of either surfaces "
4559 "generated by crosshatching, or from the borders of closed shapes",
4562 SURFSK_loops_on_strokes
: BoolProperty(
4563 name
="Loops on strokes",
4564 description
="Make the loops match the paths of the strokes",
4567 SURFSK_precision
: IntProperty(
4569 description
="Precision level of the surface calculation",
4574 SURFSK_mesh
: PointerProperty(
4575 name
="Mesh of BSurface",
4576 type=bpy
.types
.Object
,
4577 description
="Mesh of BSurface",
4580 SURFSK_gpencil
: PointerProperty(
4581 name
="GreasePencil object",
4582 type=bpy
.types
.Object
,
4583 description
="GreasePencil object",
4584 update
=update_gpencil
,
4586 SURFSK_curve
: PointerProperty(
4587 name
="Curve object",
4588 type=bpy
.types
.Object
,
4589 description
="Curve object",
4590 update
=update_curve
,
4592 SURFSK_mesh_color
: FloatVectorProperty(
4594 default
=(1.0, 0.0, 0.0, 0.3),
4599 update
=update_color
,
4600 description
="Mesh color",
4602 SURFSK_Shrinkwrap_offset
: FloatProperty(
4603 name
="Shrinkwrap offset",
4606 description
="Distance to keep from the target",
4607 update
=update_Shrinkwrap_offset
,
4609 SURFSK_in_front
: BoolProperty(
4611 description
="Make the object draw in front of others",
4613 update
=update_in_front
,
4615 SURFSK_show_wire
: BoolProperty(
4617 description
="Add the object’s wireframe over solid drawing",
4619 update
=update_show_wire
,
4621 SURFSK_shade_smooth
: BoolProperty(
4622 name
="Shade smooth",
4623 description
="Render and display faces smooth, using interpolated Vertex Normals",
4625 update
=update_shade_smooth
,
4629 MESH_OT_SURFSK_init
,
4630 MESH_OT_SURFSK_add_modifiers
,
4631 MESH_OT_SURFSK_add_surface
,
4632 MESH_OT_SURFSK_edit_surface
,
4633 GPENCIL_OT_SURFSK_add_strokes
,
4634 GPENCIL_OT_SURFSK_edit_strokes
,
4635 GPENCIL_OT_SURFSK_strokes_to_curves
,
4636 GPENCIL_OT_SURFSK_annotation_to_curves
,
4637 GPENCIL_OT_SURFSK_add_annotation
,
4638 CURVE_OT_SURFSK_edit_curve
,
4639 CURVE_OT_SURFSK_reorder_splines
,
4640 CURVE_OT_SURFSK_first_points
,
4647 bpy
.utils
.register_class(cls
)
4649 for panel
in panels
:
4650 bpy
.utils
.register_class(panel
)
4652 bpy
.types
.Scene
.bsurfaces
= PointerProperty(type=BsurfacesProps
)
4653 update_panel(None, bpy
.context
)
4656 for panel
in panels
:
4657 bpy
.utils
.unregister_class(panel
)
4660 bpy
.utils
.unregister_class(cls
)
4662 del bpy
.types
.Scene
.bsurfaces
4664 if __name__
== "__main__":