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_fill_faces
: BoolProperty(
243 selection_U_exists
: BoolProperty(
246 selection_V_exists
: BoolProperty(
249 selection_U2_exists
: BoolProperty(
252 selection_V2_exists
: BoolProperty(
255 selection_V_is_closed
: BoolProperty(
258 selection_U_is_closed
: BoolProperty(
261 selection_V2_is_closed
: BoolProperty(
264 selection_U2_is_closed
: BoolProperty(
268 edges_U
: IntProperty(
270 description
="Number of face-loops crossing the strokes",
275 edges_V
: IntProperty(
277 description
="Number of face-loops following the strokes",
282 cyclic_cross
: BoolProperty(
284 description
="Make cyclic the face-loops crossing the strokes",
287 cyclic_follow
: BoolProperty(
288 name
="Cyclic Follow",
289 description
="Make cyclic the face-loops following the strokes",
292 loops_on_strokes
: BoolProperty(
293 name
="Loops on strokes",
294 description
="Make the loops match the paths of the strokes",
297 automatic_join
: BoolProperty(
298 name
="Automatic join",
299 description
="Join automatically vertices of either surfaces generated "
300 "by crosshatching, or from the borders of closed shapes",
303 join_stretch_factor
: FloatProperty(
305 description
="Amount of stretching or shrinking allowed for "
306 "edges when joining vertices automatically",
312 keep_strokes
: BoolProperty(
314 description
="Keeps the sketched strokes or curves after adding the surface",
317 strokes_type
: StringProperty()
318 initial_global_undo_state
: BoolProperty()
321 def draw(self
, context
):
323 col
= layout
.column(align
=True)
326 if not self
.is_fill_faces
:
328 if not self
.is_crosshatch
:
329 if not self
.selection_U_exists
:
330 col
.prop(self
, "edges_U")
333 if not self
.selection_V_exists
:
334 col
.prop(self
, "edges_V")
339 if not self
.selection_U_exists
:
341 (self
.selection_V_exists
and not self
.selection_V_is_closed
) or
342 (self
.selection_V2_exists
and not self
.selection_V2_is_closed
)
344 col
.prop(self
, "cyclic_cross")
346 if not self
.selection_V_exists
:
348 (self
.selection_U_exists
and not self
.selection_U_is_closed
) or
349 (self
.selection_U2_exists
and not self
.selection_U2_is_closed
)
351 col
.prop(self
, "cyclic_follow")
353 col
.prop(self
, "loops_on_strokes")
355 col
.prop(self
, "automatic_join")
357 if self
.automatic_join
:
361 col
.prop(self
, "join_stretch_factor")
363 col
.prop(self
, "keep_strokes")
365 # Get an ordered list of a chain of vertices
366 def get_ordered_verts(self
, ob
, all_selected_edges_idx
, all_selected_verts_idx
,
367 first_vert_idx
, middle_vertex_idx
, closing_vert_idx
):
368 # Order selected vertices.
370 if closing_vert_idx
is not None:
371 verts_ordered
.append(ob
.data
.vertices
[closing_vert_idx
])
373 verts_ordered
.append(ob
.data
.vertices
[first_vert_idx
])
374 prev_v
= first_vert_idx
378 edges_non_matched
= 0
379 for i
in all_selected_edges_idx
:
380 if ob
.data
.edges
[i
] != prev_ed
and ob
.data
.edges
[i
].vertices
[0] == prev_v
and \
381 ob
.data
.edges
[i
].vertices
[1] in all_selected_verts_idx
:
383 verts_ordered
.append(ob
.data
.vertices
[ob
.data
.edges
[i
].vertices
[1]])
384 prev_v
= ob
.data
.edges
[i
].vertices
[1]
385 prev_ed
= ob
.data
.edges
[i
]
386 elif ob
.data
.edges
[i
] != prev_ed
and ob
.data
.edges
[i
].vertices
[1] == prev_v
and \
387 ob
.data
.edges
[i
].vertices
[0] in all_selected_verts_idx
:
389 verts_ordered
.append(ob
.data
.vertices
[ob
.data
.edges
[i
].vertices
[0]])
390 prev_v
= ob
.data
.edges
[i
].vertices
[0]
391 prev_ed
= ob
.data
.edges
[i
]
393 edges_non_matched
+= 1
395 if edges_non_matched
== len(all_selected_edges_idx
):
401 if closing_vert_idx
is not None:
402 verts_ordered
.append(ob
.data
.vertices
[closing_vert_idx
])
404 if middle_vertex_idx
is not None:
405 verts_ordered
.append(ob
.data
.vertices
[middle_vertex_idx
])
406 verts_ordered
.reverse()
408 return tuple(verts_ordered
)
410 # Calculates length of a chain of points.
411 def get_chain_length(self
, object, verts_ordered
):
412 matrix
= object.matrix_world
415 edges_lengths_sum
= 0
416 for i
in range(0, len(verts_ordered
)):
418 prev_v_co
= matrix
@ verts_ordered
[i
].co
420 v_co
= matrix
@ verts_ordered
[i
].co
422 v_difs
= [prev_v_co
[0] - v_co
[0], prev_v_co
[1] - v_co
[1], prev_v_co
[2] - v_co
[2]]
423 edge_length
= abs(sqrt(v_difs
[0] * v_difs
[0] + v_difs
[1] * v_difs
[1] + v_difs
[2] * v_difs
[2]))
425 edges_lengths
.append(edge_length
)
426 edges_lengths_sum
+= edge_length
430 return edges_lengths
, edges_lengths_sum
432 # Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
433 def get_edges_proportions(self
, edges_lengths
, edges_lengths_sum
, use_boundaries
, fixed_edges_num
):
434 edges_proportions
= []
437 for l
in edges_lengths
:
438 edges_proportions
.append(l
/ edges_lengths_sum
)
442 for _n
in range(0, fixed_edges_num
):
443 edges_proportions
.append(1 / fixed_edges_num
)
446 return edges_proportions
448 # Calculates the angle between two pairs of points in space
449 def orientation_difference(self
, points_A_co
, points_B_co
):
450 # each parameter should be a list with two elements,
451 # and each element should be a x,y,z coordinate
452 vec_A
= points_A_co
[0] - points_A_co
[1]
453 vec_B
= points_B_co
[0] - points_B_co
[1]
455 angle
= vec_A
.angle(vec_B
)
458 angle
= abs(angle
- pi
)
462 # Calculate the which vert of verts_idx list is the nearest one
463 # to the point_co coordinates, and the distance
464 def shortest_distance(self
, object, point_co
, verts_idx
):
465 matrix
= object.matrix_world
467 for i
in range(0, len(verts_idx
)):
468 dist
= (point_co
- matrix
@ object.data
.vertices
[verts_idx
[i
]].co
).length
471 nearest_vert_idx
= verts_idx
[i
]
476 nearest_vert_idx
= verts_idx
[i
]
479 return nearest_vert_idx
, shortest_dist
481 # Returns the index of the opposite vert tip in a chain, given a vert tip index
482 # as parameter, and a multidimentional list with all pairs of tips
483 def opposite_tip(self
, vert_tip_idx
, all_chains_tips_idx
):
484 opposite_vert_tip_idx
= None
485 for i
in range(0, len(all_chains_tips_idx
)):
486 if vert_tip_idx
== all_chains_tips_idx
[i
][0]:
487 opposite_vert_tip_idx
= all_chains_tips_idx
[i
][1]
488 if vert_tip_idx
== all_chains_tips_idx
[i
][1]:
489 opposite_vert_tip_idx
= all_chains_tips_idx
[i
][0]
491 return opposite_vert_tip_idx
493 # Simplifies a spline and returns the new points coordinates
494 def simplify_spline(self
, spline_coords
, segments_num
):
495 simplified_spline
= []
496 points_between_segments
= round(len(spline_coords
) / segments_num
)
498 simplified_spline
.append(spline_coords
[0])
499 for i
in range(1, segments_num
):
500 simplified_spline
.append(spline_coords
[i
* points_between_segments
])
502 simplified_spline
.append(spline_coords
[len(spline_coords
) - 1])
504 return simplified_spline
506 # Returns a list with the coords of the points distributed over the splines
507 # passed to this method according to the proportions parameter
508 def distribute_pts(self
, surface_splines
, proportions
):
510 # Calculate the length of each final surface spline
511 surface_splines_lengths
= []
512 surface_splines_parsed
= []
514 for sp_idx
in range(0, len(surface_splines
)):
515 # Calculate spline length
516 surface_splines_lengths
.append(0)
518 for i
in range(0, len(surface_splines
[sp_idx
].bezier_points
)):
520 prev_p
= surface_splines
[sp_idx
].bezier_points
[i
]
522 p
= surface_splines
[sp_idx
].bezier_points
[i
]
523 edge_length
= (prev_p
.co
- p
.co
).length
524 surface_splines_lengths
[sp_idx
] += edge_length
528 # Calculate vertex positions with appropriate edge proportions, and ordered, for each spline
529 for sp_idx
in range(0, len(surface_splines
)):
530 surface_splines_parsed
.append([])
531 surface_splines_parsed
[sp_idx
].append(surface_splines
[sp_idx
].bezier_points
[0].co
)
533 prev_p_co
= surface_splines
[sp_idx
].bezier_points
[0].co
536 for prop_idx
in range(len(proportions
) - 1):
537 target_length
= surface_splines_lengths
[sp_idx
] * proportions
[prop_idx
]
538 partial_segment_length
= 0
542 # if not it'll pass the p_idx as an index below and crash
543 if p_idx
< len(surface_splines
[sp_idx
].bezier_points
):
544 p_co
= surface_splines
[sp_idx
].bezier_points
[p_idx
].co
545 new_dist
= (prev_p_co
- p_co
).length
547 # The new distance that could have the partial segment if
548 # it is still shorter than the target length
549 potential_segment_length
= partial_segment_length
+ new_dist
551 # If the potential is still shorter, keep adding
552 if potential_segment_length
< target_length
:
553 partial_segment_length
= potential_segment_length
558 # If the potential is longer than the target, calculate the target
559 # (a point between the last two points), and assign
560 elif potential_segment_length
> target_length
:
561 remaining_dist
= target_length
- partial_segment_length
562 vec
= p_co
- prev_p_co
564 intermediate_co
= prev_p_co
+ (vec
* remaining_dist
)
566 surface_splines_parsed
[sp_idx
].append(intermediate_co
)
568 partial_segment_length
+= remaining_dist
569 prev_p_co
= intermediate_co
573 # If the potential is equal to the target, assign
574 elif potential_segment_length
== target_length
:
575 surface_splines_parsed
[sp_idx
].append(p_co
)
583 # last point of the spline
584 surface_splines_parsed
[sp_idx
].append(
585 surface_splines
[sp_idx
].bezier_points
[len(surface_splines
[sp_idx
].bezier_points
) - 1].co
588 return surface_splines_parsed
590 # Counts the number of faces that belong to each edge
591 def edge_face_count(self
, ob
):
592 ed_keys_count_dict
= {}
594 for face
in ob
.data
.polygons
:
595 for ed_keys
in face
.edge_keys
:
596 if ed_keys
not in ed_keys_count_dict
:
597 ed_keys_count_dict
[ed_keys
] = 1
599 ed_keys_count_dict
[ed_keys
] += 1
602 for i
in range(len(ob
.data
.edges
)):
603 edge_face_count
.append(0)
605 for i
in range(len(ob
.data
.edges
)):
606 ed
= ob
.data
.edges
[i
]
611 if (v1
, v2
) in ed_keys_count_dict
:
612 edge_face_count
[i
] = ed_keys_count_dict
[(v1
, v2
)]
613 elif (v2
, v1
) in ed_keys_count_dict
:
614 edge_face_count
[i
] = ed_keys_count_dict
[(v2
, v1
)]
616 return edge_face_count
618 # Fills with faces all the selected vertices which form empty triangles or quads
619 def fill_with_faces(self
, object):
620 all_selected_verts_count
= self
.main_object_selected_verts_count
622 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
624 # Calculate average length of selected edges
625 all_selected_verts
= []
626 original_sel_edges_count
= 0
627 for ed
in object.data
.edges
:
628 if object.data
.vertices
[ed
.vertices
[0]].select
and object.data
.vertices
[ed
.vertices
[1]].select
:
630 coords
.append(object.data
.vertices
[ed
.vertices
[0]].co
)
631 coords
.append(object.data
.vertices
[ed
.vertices
[1]].co
)
633 original_sel_edges_count
+= 1
635 if not ed
.vertices
[0] in all_selected_verts
:
636 all_selected_verts
.append(ed
.vertices
[0])
638 if not ed
.vertices
[1] in all_selected_verts
:
639 all_selected_verts
.append(ed
.vertices
[1])
641 tuple(all_selected_verts
)
643 # Check if there is any edge selected. If not, interrupt the script
644 if original_sel_edges_count
== 0 and all_selected_verts_count
> 0:
647 # Get all edges connected to selected verts
648 all_edges_around_sel_verts
= []
649 edges_connected_to_sel_verts
= {}
650 verts_connected_to_every_vert
= {}
651 for ed_idx
in range(len(object.data
.edges
)):
652 ed
= object.data
.edges
[ed_idx
]
655 if ed
.vertices
[0] in all_selected_verts
:
656 if not ed
.vertices
[0] in edges_connected_to_sel_verts
:
657 edges_connected_to_sel_verts
[ed
.vertices
[0]] = []
659 edges_connected_to_sel_verts
[ed
.vertices
[0]].append(ed_idx
)
662 if ed
.vertices
[1] in all_selected_verts
:
663 if not ed
.vertices
[1] in edges_connected_to_sel_verts
:
664 edges_connected_to_sel_verts
[ed
.vertices
[1]] = []
666 edges_connected_to_sel_verts
[ed
.vertices
[1]].append(ed_idx
)
669 if include_edge
is True:
670 all_edges_around_sel_verts
.append(ed_idx
)
672 # Get all connected verts to each vert
673 if not ed
.vertices
[0] in verts_connected_to_every_vert
:
674 verts_connected_to_every_vert
[ed
.vertices
[0]] = []
676 if not ed
.vertices
[1] in verts_connected_to_every_vert
:
677 verts_connected_to_every_vert
[ed
.vertices
[1]] = []
679 verts_connected_to_every_vert
[ed
.vertices
[0]].append(ed
.vertices
[1])
680 verts_connected_to_every_vert
[ed
.vertices
[1]].append(ed
.vertices
[0])
682 # Get all verts connected to faces
683 all_verts_part_of_faces
= []
684 all_edges_faces_count
= []
685 all_edges_faces_count
+= self
.edge_face_count(object)
687 # Get only the selected edges that have faces attached.
688 count_faces_of_edges_around_sel_verts
= {}
689 selected_verts_with_faces
= []
690 for ed_idx
in all_edges_around_sel_verts
:
691 count_faces_of_edges_around_sel_verts
[ed_idx
] = all_edges_faces_count
[ed_idx
]
693 if all_edges_faces_count
[ed_idx
] > 0:
694 ed
= object.data
.edges
[ed_idx
]
696 if not ed
.vertices
[0] in selected_verts_with_faces
:
697 selected_verts_with_faces
.append(ed
.vertices
[0])
699 if not ed
.vertices
[1] in selected_verts_with_faces
:
700 selected_verts_with_faces
.append(ed
.vertices
[1])
702 all_verts_part_of_faces
.append(ed
.vertices
[0])
703 all_verts_part_of_faces
.append(ed
.vertices
[1])
705 tuple(selected_verts_with_faces
)
707 # Discard unneeded verts from calculations
708 participating_verts
= []
710 for v_idx
in all_selected_verts
:
711 vert_has_edges_with_one_face
= False
713 # Check if the actual vert has at least one edge connected to only one face
714 for ed_idx
in edges_connected_to_sel_verts
[v_idx
]:
715 if count_faces_of_edges_around_sel_verts
[ed_idx
] == 1:
716 vert_has_edges_with_one_face
= True
718 # If the vert has two or less edges connected and the vert is not part of any face.
719 # Or the vert is part of any face and at least one of
720 # the connected edges has only one face attached to it.
721 if (len(edges_connected_to_sel_verts
[v_idx
]) == 2 and
722 v_idx
not in all_verts_part_of_faces
) or \
723 len(edges_connected_to_sel_verts
[v_idx
]) == 1 or \
724 (v_idx
in all_verts_part_of_faces
and
725 vert_has_edges_with_one_face
):
727 participating_verts
.append(v_idx
)
729 if v_idx
not in all_verts_part_of_faces
:
730 movable_verts
.append(v_idx
)
732 # Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
733 for mv_idx
in movable_verts
:
735 mv_connected_verts
= verts_connected_to_every_vert
[mv_idx
]
737 for actual_v_idx
in all_selected_verts
:
738 count_shared_neighbors
= 0
741 for mv_conn_v_idx
in mv_connected_verts
:
742 if mv_idx
!= actual_v_idx
:
743 if mv_conn_v_idx
in verts_connected_to_every_vert
[actual_v_idx
] and \
744 mv_conn_v_idx
not in checked_verts
:
745 count_shared_neighbors
+= 1
746 checked_verts
.append(mv_conn_v_idx
)
748 if actual_v_idx
in mv_connected_verts
:
752 if count_shared_neighbors
== 2:
760 movable_verts
.remove(mv_idx
)
762 # Calculate merge distance for participating verts
763 shortest_edge_length
= None
764 for ed
in object.data
.edges
:
765 if ed
.vertices
[0] in movable_verts
and ed
.vertices
[1] in movable_verts
:
766 v1
= object.data
.vertices
[ed
.vertices
[0]]
767 v2
= object.data
.vertices
[ed
.vertices
[1]]
769 length
= (v1
.co
- v2
.co
).length
771 if shortest_edge_length
is None:
772 shortest_edge_length
= length
774 if length
< shortest_edge_length
:
775 shortest_edge_length
= length
777 if shortest_edge_length
is not None:
778 edges_merge_distance
= shortest_edge_length
* 0.5
780 edges_merge_distance
= 0
782 # Get together the verts near enough. They will be merged later
784 remaining_verts
+= participating_verts
785 for v1_idx
in participating_verts
:
786 if v1_idx
in remaining_verts
and v1_idx
in movable_verts
:
788 coords_verts_to_merge
= {}
790 verts_to_merge
.append(v1_idx
)
792 v1_co
= object.data
.vertices
[v1_idx
].co
793 coords_verts_to_merge
[v1_idx
] = (v1_co
[0], v1_co
[1], v1_co
[2])
795 for v2_idx
in remaining_verts
:
797 v2_co
= object.data
.vertices
[v2_idx
].co
799 dist
= (v1_co
- v2_co
).length
801 if dist
<= edges_merge_distance
: # Add the verts which are near enough
802 verts_to_merge
.append(v2_idx
)
804 coords_verts_to_merge
[v2_idx
] = (v2_co
[0], v2_co
[1], v2_co
[2])
806 for vm_idx
in verts_to_merge
:
807 remaining_verts
.remove(vm_idx
)
809 if len(verts_to_merge
) > 1:
810 # Calculate middle point of the verts to merge.
814 movable_verts_to_merge_count
= 0
815 for i
in range(len(verts_to_merge
)):
816 if verts_to_merge
[i
] in movable_verts
:
817 v_co
= object.data
.vertices
[verts_to_merge
[i
]].co
823 movable_verts_to_merge_count
+= 1
826 sum_x_co
/ movable_verts_to_merge_count
,
827 sum_y_co
/ movable_verts_to_merge_count
,
828 sum_z_co
/ movable_verts_to_merge_count
831 # Check if any vert to be merged is not movable
833 are_verts_not_movable
= False
834 verts_not_movable
= []
835 for v_merge_idx
in verts_to_merge
:
836 if v_merge_idx
in participating_verts
and v_merge_idx
not in movable_verts
:
837 are_verts_not_movable
= True
838 verts_not_movable
.append(v_merge_idx
)
840 if are_verts_not_movable
:
841 # Get the vert connected to faces, that is nearest to
842 # the middle point of the movable verts
844 for vcf_idx
in verts_not_movable
:
845 dist
= abs((object.data
.vertices
[vcf_idx
].co
-
846 Vector(middle_point_co
)).length
)
848 if shortest_dist
is None:
850 nearest_vert_idx
= vcf_idx
852 if dist
< shortest_dist
:
854 nearest_vert_idx
= vcf_idx
856 coords
= object.data
.vertices
[nearest_vert_idx
].co
857 target_point_co
= [coords
[0], coords
[1], coords
[2]]
859 target_point_co
= middle_point_co
861 # Move verts to merge to the middle position
862 for v_merge_idx
in verts_to_merge
:
863 if v_merge_idx
in movable_verts
: # Only move the verts that are not part of faces
864 object.data
.vertices
[v_merge_idx
].co
[0] = target_point_co
[0]
865 object.data
.vertices
[v_merge_idx
].co
[1] = target_point_co
[1]
866 object.data
.vertices
[v_merge_idx
].co
[2] = target_point_co
[2]
868 # Perform "Remove Doubles" to weld all the disconnected verts
869 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
870 bpy
.ops
.mesh
.remove_doubles(threshold
=0.0001)
872 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
874 # Get all the definitive selected edges, after weldding
876 edges_per_vert
= {} # Number of faces of each selected edge
877 for ed
in object.data
.edges
:
878 if object.data
.vertices
[ed
.vertices
[0]].select
and object.data
.vertices
[ed
.vertices
[1]].select
:
879 selected_edges
.append(ed
.index
)
881 # Save all the edges that belong to each vertex.
882 if not ed
.vertices
[0] in edges_per_vert
:
883 edges_per_vert
[ed
.vertices
[0]] = []
885 if not ed
.vertices
[1] in edges_per_vert
:
886 edges_per_vert
[ed
.vertices
[1]] = []
888 edges_per_vert
[ed
.vertices
[0]].append(ed
.index
)
889 edges_per_vert
[ed
.vertices
[1]].append(ed
.index
)
891 # Check if all the edges connected to each vert have two faces attached to them.
892 # To discard them later and make calculations faster
894 a
+= self
.edge_face_count(object)
896 verts_surrounded_by_faces
= {}
897 for v_idx
in edges_per_vert
:
898 edges_with_two_faces_count
= 0
900 for ed_idx
in edges_per_vert
[v_idx
]:
902 edges_with_two_faces_count
+= 1
904 if edges_with_two_faces_count
== len(edges_per_vert
[v_idx
]):
905 verts_surrounded_by_faces
[v_idx
] = True
907 verts_surrounded_by_faces
[v_idx
] = False
909 # Get all the selected vertices
910 selected_verts_idx
= []
911 for v
in object.data
.vertices
:
913 selected_verts_idx
.append(v
.index
)
915 # Get all the faces of the object
916 all_object_faces_verts_idx
= []
917 for face
in object.data
.polygons
:
919 face_verts
.append(face
.vertices
[0])
920 face_verts
.append(face
.vertices
[1])
921 face_verts
.append(face
.vertices
[2])
923 if len(face
.vertices
) == 4:
924 face_verts
.append(face
.vertices
[3])
926 all_object_faces_verts_idx
.append(face_verts
)
928 # Deselect all vertices
929 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
930 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
931 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
933 # Make a dictionary with the verts related to each vert
934 related_key_verts
= {}
935 for ed_idx
in selected_edges
:
936 ed
= object.data
.edges
[ed_idx
]
938 if not verts_surrounded_by_faces
[ed
.vertices
[0]]:
939 if not ed
.vertices
[0] in related_key_verts
:
940 related_key_verts
[ed
.vertices
[0]] = []
942 if not ed
.vertices
[1] in related_key_verts
[ed
.vertices
[0]]:
943 related_key_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
945 if not verts_surrounded_by_faces
[ed
.vertices
[1]]:
946 if not ed
.vertices
[1] in related_key_verts
:
947 related_key_verts
[ed
.vertices
[1]] = []
949 if not ed
.vertices
[0] in related_key_verts
[ed
.vertices
[1]]:
950 related_key_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
952 # Get groups of verts forming each face
954 for v1
in related_key_verts
: # verts-1 ....
955 for v2
in related_key_verts
: # verts-2
957 related_verts_in_common
= []
960 for rel_v1
in related_key_verts
[v1
]:
961 # Check if related verts of verts-1 are related verts of verts-2
962 if rel_v1
in related_key_verts
[v2
]:
963 related_verts_in_common
.append(rel_v1
)
965 if v2
in related_key_verts
[v1
]:
968 if v1
in related_key_verts
[v2
]:
971 repeated_face
= False
972 # If two verts have two related verts in common, they form a quad
973 if len(related_verts_in_common
) == 2:
974 # Check if the face is already saved
975 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
977 for f_verts
in all_faces_to_check_idx
:
980 if len(f_verts
) == 4:
985 if related_verts_in_common
[0] in f_verts
:
987 if related_verts_in_common
[1] in f_verts
:
990 if repeated_verts
== len(f_verts
):
994 if not repeated_face
:
995 faces_verts_idx
.append(
996 [v1
, related_verts_in_common
[0], v2
, related_verts_in_common
[1]]
999 # If Two verts have one related vert in common and
1000 # they are related to each other, they form a triangle
1001 elif v2_in_rel_v1
and v1_in_rel_v2
and len(related_verts_in_common
) == 1:
1002 # Check if the face is already saved.
1003 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
1005 for f_verts
in all_faces_to_check_idx
:
1008 if len(f_verts
) == 3:
1013 if related_verts_in_common
[0] in f_verts
:
1016 if repeated_verts
== len(f_verts
):
1017 repeated_face
= True
1020 if not repeated_face
:
1021 faces_verts_idx
.append([v1
, related_verts_in_common
[0], v2
])
1023 # Keep only the faces that don't overlap by ignoring quads
1024 # that overlap with two adjacent triangles
1025 faces_to_not_include_idx
= [] # Indices of faces_verts_idx to eliminate
1026 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
1027 for i
in range(len(faces_verts_idx
)):
1028 for t
in range(len(all_faces_to_check_idx
)):
1032 if len(faces_verts_idx
[i
]) == 4 and len(all_faces_to_check_idx
[t
]) == 3:
1033 for v_idx
in all_faces_to_check_idx
[t
]:
1034 if v_idx
in faces_verts_idx
[i
]:
1035 verts_in_common
+= 1
1036 # If it doesn't have all it's vertices repeated in the other face
1037 if verts_in_common
== 3:
1038 if i
not in faces_to_not_include_idx
:
1039 faces_to_not_include_idx
.append(i
)
1041 # Build faces discarding the ones in faces_to_not_include
1046 num_faces_created
= 0
1047 for i
in range(len(faces_verts_idx
)):
1048 if i
not in faces_to_not_include_idx
:
1049 bm
.faces
.new([bm
.verts
[v
] for v
in faces_verts_idx
[i
]])
1051 num_faces_created
+= 1
1056 for v_idx
in selected_verts_idx
:
1057 self
.main_object
.data
.vertices
[v_idx
].select
= True
1059 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
1060 bpy
.ops
.mesh
.normals_make_consistent(inside
=False)
1061 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
1065 return num_faces_created
1067 # Crosshatch skinning
1068 def crosshatch_surface_invoke(self
, ob_original_splines
):
1069 self
.is_crosshatch
= False
1070 self
.crosshatch_merge_distance
= 0
1072 objects_to_delete
= [] # duplicated strokes to be deleted.
1074 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1075 # (without this the surface verts merging with the main object doesn't work well)
1076 self
.modifiers_prev_viewport_state
= []
1077 if len(self
.main_object
.modifiers
) > 0:
1078 for m_idx
in range(len(self
.main_object
.modifiers
)):
1079 self
.modifiers_prev_viewport_state
.append(
1080 self
.main_object
.modifiers
[m_idx
].show_viewport
1082 self
.main_object
.modifiers
[m_idx
].show_viewport
= False
1084 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1085 ob_original_splines
.select_set(True)
1086 bpy
.context
.view_layer
.objects
.active
= ob_original_splines
1088 if len(ob_original_splines
.data
.splines
) >= 2:
1089 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1090 ob_splines
= bpy
.context
.object
1091 ob_splines
.name
= "SURFSKIO_NE_STR"
1093 # Get estimative merge distance (sum up the distances from the first point to
1094 # all other points, then average them and then divide them)
1095 first_point_dist_sum
= 0
1098 coords_first_pt
= ob_splines
.data
.splines
[0].bezier_points
[0].co
1099 for i
in range(len(ob_splines
.data
.splines
)):
1100 sp
= ob_splines
.data
.splines
[i
]
1102 if coords_first_pt
!= sp
.bezier_points
[0].co
:
1103 first_dist
= (coords_first_pt
- sp
.bezier_points
[0].co
).length
1105 if coords_first_pt
!= sp
.bezier_points
[len(sp
.bezier_points
) - 1].co
:
1106 second_dist
= (coords_first_pt
- sp
.bezier_points
[len(sp
.bezier_points
) - 1].co
).length
1108 first_point_dist_sum
+= first_dist
+ second_dist
1112 shortest_dist
= first_dist
1113 elif second_dist
!= 0:
1114 shortest_dist
= second_dist
1116 if shortest_dist
> first_dist
and first_dist
!= 0:
1117 shortest_dist
= first_dist
1119 if shortest_dist
> second_dist
and second_dist
!= 0:
1120 shortest_dist
= second_dist
1122 self
.crosshatch_merge_distance
= shortest_dist
/ 20
1124 # Recalculation of merge distance
1126 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1128 ob_calc_merge_dist
= bpy
.context
.object
1129 ob_calc_merge_dist
.name
= "SURFSKIO_CALC_TMP"
1131 objects_to_delete
.append(ob_calc_merge_dist
)
1133 # Smooth out strokes a little to improve crosshatch detection
1134 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1135 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1138 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1140 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1141 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1143 # Convert curves into mesh
1144 ob_calc_merge_dist
.data
.resolution_u
= 12
1145 bpy
.ops
.object.convert(target
='MESH', keep_original
=False)
1147 # Find "intersection-nodes"
1148 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1149 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1150 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN',
1151 threshold
=self
.crosshatch_merge_distance
)
1152 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1153 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1155 # Remove verts with less than three edges
1156 verts_edges_count
= {}
1157 for ed
in ob_calc_merge_dist
.data
.edges
:
1160 if v
[0] not in verts_edges_count
:
1161 verts_edges_count
[v
[0]] = 0
1163 if v
[1] not in verts_edges_count
:
1164 verts_edges_count
[v
[1]] = 0
1166 verts_edges_count
[v
[0]] += 1
1167 verts_edges_count
[v
[1]] += 1
1169 nodes_verts_coords
= []
1170 for v_idx
in verts_edges_count
:
1171 v
= ob_calc_merge_dist
.data
.vertices
[v_idx
]
1173 if verts_edges_count
[v_idx
] < 3:
1177 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1178 bpy
.ops
.mesh
.delete('INVOKE_REGION_WIN', type='VERT')
1179 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1181 # Remove doubles to discard very near verts from calculations of distance
1182 bpy
.ops
.mesh
.remove_doubles(
1183 'INVOKE_REGION_WIN',
1184 threshold
=self
.crosshatch_merge_distance
* 4.0
1186 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1188 # Get all coords of the resulting nodes
1189 nodes_verts_coords
= [(v
.co
[0], v
.co
[1], v
.co
[2]) for
1190 v
in ob_calc_merge_dist
.data
.vertices
]
1192 # Check if the strokes are a crosshatch
1193 if len(nodes_verts_coords
) >= 3:
1194 self
.is_crosshatch
= True
1196 shortest_dist
= None
1197 for co_1
in nodes_verts_coords
:
1198 for co_2
in nodes_verts_coords
:
1200 dist
= (Vector(co_1
) - Vector(co_2
)).length
1202 if shortest_dist
is not None:
1203 if dist
< shortest_dist
:
1204 shortest_dist
= dist
1206 shortest_dist
= dist
1208 self
.crosshatch_merge_distance
= shortest_dist
/ 3
1210 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1211 ob_splines
.select_set(True)
1212 bpy
.context
.view_layer
.objects
.active
= ob_splines
1214 # Deselect all points
1215 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1216 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1217 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1219 # Smooth splines in a localized way, to eliminate "saw-teeth"
1220 # like shapes when there are many points
1221 for sp
in ob_splines
.data
.splines
:
1224 angle_limit
= 2 # Degrees
1225 for t
in range(len(sp
.bezier_points
)):
1226 # Because on each iteration it checks the "next two points"
1227 # of the actual. This way it doesn't go out of range
1228 if t
<= len(sp
.bezier_points
) - 3:
1229 p1
= sp
.bezier_points
[t
]
1230 p2
= sp
.bezier_points
[t
+ 1]
1231 p3
= sp
.bezier_points
[t
+ 2]
1233 vec_1
= p1
.co
- p2
.co
1234 vec_2
= p2
.co
- p3
.co
1236 if p2
.co
!= p1
.co
and p2
.co
!= p3
.co
:
1237 angle
= vec_1
.angle(vec_2
)
1238 angle_sum
+= degrees(angle
)
1240 if angle_sum
>= angle_limit
: # If sum of angles is grater than the limit
1241 if (p1
.co
- p2
.co
).length
<= self
.crosshatch_merge_distance
:
1242 p1
.select_control_point
= True
1243 p1
.select_left_handle
= True
1244 p1
.select_right_handle
= True
1246 p2
.select_control_point
= True
1247 p2
.select_left_handle
= True
1248 p2
.select_right_handle
= True
1250 if (p1
.co
- p2
.co
).length
<= self
.crosshatch_merge_distance
:
1251 p3
.select_control_point
= True
1252 p3
.select_left_handle
= True
1253 p3
.select_right_handle
= True
1257 sp
.bezier_points
[0].select_control_point
= False
1258 sp
.bezier_points
[0].select_left_handle
= False
1259 sp
.bezier_points
[0].select_right_handle
= False
1261 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_control_point
= False
1262 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_left_handle
= False
1263 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_right_handle
= False
1265 # Smooth out strokes a little to improve crosshatch detection
1266 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1269 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1271 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1272 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1274 # Simplify the splines
1275 for sp
in ob_splines
.data
.splines
:
1278 sp
.bezier_points
[0].select_control_point
= True
1279 sp
.bezier_points
[0].select_left_handle
= True
1280 sp
.bezier_points
[0].select_right_handle
= True
1282 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_control_point
= True
1283 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_left_handle
= True
1284 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_right_handle
= True
1286 angle_limit
= 15 # Degrees
1287 for t
in range(len(sp
.bezier_points
)):
1288 # Because on each iteration it checks the "next two points"
1289 # of the actual. This way it doesn't go out of range
1290 if t
<= len(sp
.bezier_points
) - 3:
1291 p1
= sp
.bezier_points
[t
]
1292 p2
= sp
.bezier_points
[t
+ 1]
1293 p3
= sp
.bezier_points
[t
+ 2]
1295 vec_1
= p1
.co
- p2
.co
1296 vec_2
= p2
.co
- p3
.co
1298 if p2
.co
!= p1
.co
and p2
.co
!= p3
.co
:
1299 angle
= vec_1
.angle(vec_2
)
1300 angle_sum
+= degrees(angle
)
1301 # If sum of angles is grater than the limit
1302 if angle_sum
>= angle_limit
:
1303 p1
.select_control_point
= True
1304 p1
.select_left_handle
= True
1305 p1
.select_right_handle
= True
1307 p2
.select_control_point
= True
1308 p2
.select_left_handle
= True
1309 p2
.select_right_handle
= True
1311 p3
.select_control_point
= True
1312 p3
.select_left_handle
= True
1313 p3
.select_right_handle
= True
1317 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1318 bpy
.ops
.curve
.select_all(action
='INVERT')
1320 bpy
.ops
.curve
.delete(type='VERT')
1321 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1323 objects_to_delete
.append(ob_splines
)
1325 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1326 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1327 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1329 # Check if the strokes are a crosshatch
1330 if self
.is_crosshatch
:
1331 all_points_coords
= []
1332 for i
in range(len(ob_splines
.data
.splines
)):
1333 all_points_coords
.append([])
1335 all_points_coords
[i
] = [Vector((x
, y
, z
)) for
1336 x
, y
, z
in [bp
.co
for
1337 bp
in ob_splines
.data
.splines
[i
].bezier_points
]]
1339 all_intersections
= []
1340 checked_splines
= []
1341 for i
in range(len(all_points_coords
)):
1343 for t
in range(len(all_points_coords
[i
]) - 1):
1344 bp1_co
= all_points_coords
[i
][t
]
1345 bp2_co
= all_points_coords
[i
][t
+ 1]
1347 for i2
in range(len(all_points_coords
)):
1348 if i
!= i2
and i2
not in checked_splines
:
1349 for t2
in range(len(all_points_coords
[i2
]) - 1):
1350 bp3_co
= all_points_coords
[i2
][t2
]
1351 bp4_co
= all_points_coords
[i2
][t2
+ 1]
1353 intersec_coords
= intersect_line_line(
1354 bp1_co
, bp2_co
, bp3_co
, bp4_co
1356 if intersec_coords
is not None:
1357 dist
= (intersec_coords
[0] - intersec_coords
[1]).length
1359 if dist
<= self
.crosshatch_merge_distance
* 1.5:
1360 _temp_co
, percent1
= intersect_point_line(
1361 intersec_coords
[0], bp1_co
, bp2_co
1363 if (percent1
>= -0.02 and percent1
<= 1.02):
1364 _temp_co
, percent2
= intersect_point_line(
1365 intersec_coords
[1], bp3_co
, bp4_co
1367 if (percent2
>= -0.02 and percent2
<= 1.02):
1368 # Format: spline index, first point index from
1369 # corresponding segment, percentage from first point of
1370 # actual segment, coords of intersection point
1371 all_intersections
.append(
1373 ob_splines
.matrix_world
@ intersec_coords
[0])
1375 all_intersections
.append(
1377 ob_splines
.matrix_world
@ intersec_coords
[1])
1380 checked_splines
.append(i
)
1381 # Sort list by spline, then by corresponding first point index of segment,
1382 # and then by percentage from first point of segment: elements 0 and 1 respectively
1383 all_intersections
.sort(key
=operator
.itemgetter(0, 1, 2))
1385 self
.crosshatch_strokes_coords
= {}
1386 for i
in range(len(all_intersections
)):
1387 if not all_intersections
[i
][0] in self
.crosshatch_strokes_coords
:
1388 self
.crosshatch_strokes_coords
[all_intersections
[i
][0]] = []
1390 self
.crosshatch_strokes_coords
[all_intersections
[i
][0]].append(
1391 all_intersections
[i
][3]
1392 ) # Save intersection coords
1394 self
.is_crosshatch
= False
1396 # Delete all duplicates
1397 bpy
.ops
.object.delete({"selected_objects": objects_to_delete
})
1399 # If the main object has modifiers, turn their "viewport view status" to
1400 # what it was before the forced deactivation above
1401 if len(self
.main_object
.modifiers
) > 0:
1402 for m_idx
in range(len(self
.main_object
.modifiers
)):
1403 self
.main_object
.modifiers
[m_idx
].show_viewport
= self
.modifiers_prev_viewport_state
[m_idx
]
1409 # Part of the Crosshatch process that is repeated when the operator is tweaked
1410 def crosshatch_surface_execute(self
, context
):
1411 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1412 # (without this the surface verts merging with the main object doesn't work well)
1413 self
.modifiers_prev_viewport_state
= []
1414 if len(self
.main_object
.modifiers
) > 0:
1415 for m_idx
in range(len(self
.main_object
.modifiers
)):
1416 self
.modifiers_prev_viewport_state
.append(self
.main_object
.modifiers
[m_idx
].show_viewport
)
1418 self
.main_object
.modifiers
[m_idx
].show_viewport
= False
1420 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1422 me_name
= "SURFSKIO_STK_TMP"
1423 me
= bpy
.data
.meshes
.new(me_name
)
1425 all_verts_coords
= []
1427 for st_idx
in self
.crosshatch_strokes_coords
:
1428 for co_idx
in range(len(self
.crosshatch_strokes_coords
[st_idx
])):
1429 coords
= self
.crosshatch_strokes_coords
[st_idx
][co_idx
]
1431 all_verts_coords
.append(coords
)
1434 all_edges
.append((len(all_verts_coords
) - 2, len(all_verts_coords
) - 1))
1436 me
.from_pydata(all_verts_coords
, all_edges
, [])
1437 ob
= object_utils
.object_data_add(context
, me
)
1439 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1441 bpy
.context
.view_layer
.objects
.active
= ob
1443 # Get together each vert and its nearest, to the middle position
1444 verts
= ob
.data
.vertices
1446 for i
in range(len(verts
)):
1447 shortest_dist
= None
1449 if i
not in checked_verts
:
1450 for t
in range(len(verts
)):
1451 if i
!= t
and t
not in checked_verts
:
1452 dist
= (verts
[i
].co
- verts
[t
].co
).length
1454 if shortest_dist
is not None:
1455 if dist
< shortest_dist
:
1456 shortest_dist
= dist
1459 shortest_dist
= dist
1462 middle_location
= (verts
[i
].co
+ verts
[nearest_vert
].co
) / 2
1464 verts
[i
].co
= middle_location
1465 verts
[nearest_vert
].co
= middle_location
1467 checked_verts
.append(i
)
1468 checked_verts
.append(nearest_vert
)
1470 # Calculate average length between all the generated edges
1471 ob
= bpy
.context
.object
1473 for ed
in ob
.data
.edges
:
1474 v1
= ob
.data
.vertices
[ed
.vertices
[0]]
1475 v2
= ob
.data
.vertices
[ed
.vertices
[1]]
1477 lengths_sum
+= (v1
.co
- v2
.co
).length
1479 edges_count
= len(ob
.data
.edges
)
1480 # possible division by zero here
1481 average_edge_length
= lengths_sum
/ edges_count
if edges_count
!= 0 else 0.0001
1483 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1484 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1485 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN',
1486 threshold
=average_edge_length
/ 15.0)
1487 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1489 final_points_ob
= bpy
.context
.view_layer
.objects
.active
1491 # Make a dictionary with the verts related to each vert
1492 related_key_verts
= {}
1493 for ed
in final_points_ob
.data
.edges
:
1494 if not ed
.vertices
[0] in related_key_verts
:
1495 related_key_verts
[ed
.vertices
[0]] = []
1497 if not ed
.vertices
[1] in related_key_verts
:
1498 related_key_verts
[ed
.vertices
[1]] = []
1500 if not ed
.vertices
[1] in related_key_verts
[ed
.vertices
[0]]:
1501 related_key_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
1503 if not ed
.vertices
[0] in related_key_verts
[ed
.vertices
[1]]:
1504 related_key_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
1506 # Get groups of verts forming each face
1507 faces_verts_idx
= []
1508 for v1
in related_key_verts
: # verts-1 ....
1509 for v2
in related_key_verts
: # verts-2
1511 related_verts_in_common
= []
1512 v2_in_rel_v1
= False
1513 v1_in_rel_v2
= False
1514 for rel_v1
in related_key_verts
[v1
]:
1515 # Check if related verts of verts-1 are related verts of verts-2
1516 if rel_v1
in related_key_verts
[v2
]:
1517 related_verts_in_common
.append(rel_v1
)
1519 if v2
in related_key_verts
[v1
]:
1522 if v1
in related_key_verts
[v2
]:
1525 repeated_face
= False
1526 # If two verts have two related verts in common, they form a quad
1527 if len(related_verts_in_common
) == 2:
1528 # Check if the face is already saved
1529 for f_verts
in faces_verts_idx
:
1532 if len(f_verts
) == 4:
1537 if related_verts_in_common
[0] in f_verts
:
1539 if related_verts_in_common
[1] in f_verts
:
1542 if repeated_verts
== len(f_verts
):
1543 repeated_face
= True
1546 if not repeated_face
:
1547 faces_verts_idx
.append([v1
, related_verts_in_common
[0],
1548 v2
, related_verts_in_common
[1]])
1550 # If Two verts have one related vert in common and they are
1551 # related to each other, they form a triangle
1552 elif v2_in_rel_v1
and v1_in_rel_v2
and len(related_verts_in_common
) == 1:
1553 # Check if the face is already saved.
1554 for f_verts
in faces_verts_idx
:
1557 if len(f_verts
) == 3:
1562 if related_verts_in_common
[0] in f_verts
:
1565 if repeated_verts
== len(f_verts
):
1566 repeated_face
= True
1569 if not repeated_face
:
1570 faces_verts_idx
.append([v1
, related_verts_in_common
[0], v2
])
1572 # Keep only the faces that don't overlap by ignoring
1573 # quads that overlap with two adjacent triangles
1574 faces_to_not_include_idx
= [] # Indices of faces_verts_idx to eliminate
1575 for i
in range(len(faces_verts_idx
)):
1576 for t
in range(len(faces_verts_idx
)):
1580 if len(faces_verts_idx
[i
]) == 4 and len(faces_verts_idx
[t
]) == 3:
1581 for v_idx
in faces_verts_idx
[t
]:
1582 if v_idx
in faces_verts_idx
[i
]:
1583 verts_in_common
+= 1
1584 # If it doesn't have all it's vertices repeated in the other face
1585 if verts_in_common
== 3:
1586 if i
not in faces_to_not_include_idx
:
1587 faces_to_not_include_idx
.append(i
)
1590 all_surface_verts_co
= []
1591 for i
in range(len(final_points_ob
.data
.vertices
)):
1592 coords
= final_points_ob
.data
.vertices
[i
].co
1593 all_surface_verts_co
.append([coords
[0], coords
[1], coords
[2]])
1595 # Verts of each face.
1596 all_surface_faces
= []
1597 for i
in range(len(faces_verts_idx
)):
1598 if i
not in faces_to_not_include_idx
:
1600 for v_idx
in faces_verts_idx
[i
]:
1603 all_surface_faces
.append(face
)
1606 surf_me_name
= "SURFSKIO_surface"
1607 me_surf
= bpy
.data
.meshes
.new(surf_me_name
)
1608 me_surf
.from_pydata(all_surface_verts_co
, [], all_surface_faces
)
1609 ob_surface
= object_utils
.object_data_add(context
, me_surf
)
1611 # Delete final points temporal object
1612 bpy
.ops
.object.delete({"selected_objects": [final_points_ob
]})
1614 # Delete isolated verts if there are any
1615 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1616 ob_surface
.select_set(True)
1617 bpy
.context
.view_layer
.objects
.active
= ob_surface
1619 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1620 bpy
.ops
.mesh
.select_all(action
='DESELECT')
1621 bpy
.ops
.mesh
.select_face_by_sides(type='NOTEQUAL')
1622 bpy
.ops
.mesh
.delete()
1623 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1625 # Join crosshatch results with original mesh
1627 # Calculate a distance to merge the verts of the crosshatch surface to the main object
1628 edges_length_sum
= 0
1629 for ed
in ob_surface
.data
.edges
:
1630 edges_length_sum
+= (
1631 ob_surface
.data
.vertices
[ed
.vertices
[0]].co
-
1632 ob_surface
.data
.vertices
[ed
.vertices
[1]].co
1635 # Make dictionary with all the verts connected to each vert, on the new surface object.
1636 surface_connected_verts
= {}
1637 for ed
in ob_surface
.data
.edges
:
1638 if not ed
.vertices
[0] in surface_connected_verts
:
1639 surface_connected_verts
[ed
.vertices
[0]] = []
1641 surface_connected_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
1643 if ed
.vertices
[1] not in surface_connected_verts
:
1644 surface_connected_verts
[ed
.vertices
[1]] = []
1646 surface_connected_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
1648 # Duplicate the new surface object, and use shrinkwrap to
1649 # calculate later the nearest verts to the main object
1650 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1651 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1652 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1654 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1656 final_ob_duplicate
= bpy
.context
.view_layer
.objects
.active
1658 bpy
.ops
.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
1659 shrinkwrap_modifier
= final_ob_duplicate
.modifiers
[-1]
1660 shrinkwrap_modifier
.wrap_method
= "NEAREST_VERTEX"
1661 shrinkwrap_modifier
.target
= self
.main_object
1663 bpy
.ops
.object.modifier_apply('INVOKE_REGION_WIN', apply_as
='DATA', modifier
=shrinkwrap_modifier
.name
)
1665 # Make list with verts of original mesh as index and coords as value
1666 main_object_verts_coords
= []
1667 for v
in self
.main_object
.data
.vertices
:
1668 coords
= self
.main_object
.matrix_world
@ v
.co
1670 # To avoid problems when taking "-0.00" as a different value as "0.00"
1671 for c
in range(len(coords
)):
1672 if "%.3f" % coords
[c
] == "-0.00":
1675 main_object_verts_coords
.append(["%.3f" % coords
[0], "%.3f" % coords
[1], "%.3f" % coords
[2]])
1677 tuple(main_object_verts_coords
)
1679 # Determine which verts will be merged, snap them to the nearest verts
1680 # on the original verts, and get them selected
1681 crosshatch_verts_to_merge
= []
1682 if self
.automatic_join
:
1683 for i
in range(len(ob_surface
.data
.vertices
)-1):
1684 # Calculate the distance from each of the connected verts to the actual vert,
1685 # and compare it with the distance they would have if joined.
1686 # If they don't change much, that vert can be joined
1687 merge_actual_vert
= True
1689 if len(surface_connected_verts
[i
]) < 4:
1690 for c_v_idx
in surface_connected_verts
[i
]:
1691 points_original
= []
1692 points_original
.append(ob_surface
.data
.vertices
[c_v_idx
].co
)
1693 points_original
.append(ob_surface
.data
.vertices
[i
].co
)
1696 points_target
.append(ob_surface
.data
.vertices
[c_v_idx
].co
)
1697 points_target
.append(final_ob_duplicate
.data
.vertices
[i
].co
)
1699 vec_A
= points_original
[0] - points_original
[1]
1700 vec_B
= points_target
[0] - points_target
[1]
1702 dist_A
= (points_original
[0] - points_original
[1]).length
1703 dist_B
= (points_target
[0] - points_target
[1]).length
1706 points_original
[0] == points_original
[1] or
1707 points_target
[0] == points_target
[1]
1708 ): # If any vector's length is zero
1710 angle
= vec_A
.angle(vec_B
) / pi
1714 # Set a range of acceptable variation in the connected edges
1715 if dist_B
> dist_A
* 1.7 * self
.join_stretch_factor
or \
1716 dist_B
< dist_A
/ 2 / self
.join_stretch_factor
or \
1717 angle
>= 0.15 * self
.join_stretch_factor
:
1719 merge_actual_vert
= False
1722 merge_actual_vert
= False
1724 self
.report({'WARNING'},
1725 "Crosshatch set incorrectly")
1727 if merge_actual_vert
:
1728 coords
= final_ob_duplicate
.data
.vertices
[i
].co
1729 # To avoid problems when taking "-0.000" as a different value as "0.00"
1730 for c
in range(len(coords
)):
1731 if "%.3f" % coords
[c
] == "-0.00":
1734 comparison_coords
= ["%.3f" % coords
[0], "%.3f" % coords
[1], "%.3f" % coords
[2]]
1736 if comparison_coords
in main_object_verts_coords
:
1737 # Get the index of the vert with those coords in the main object
1738 main_object_related_vert_idx
= main_object_verts_coords
.index(comparison_coords
)
1740 if self
.main_object
.data
.vertices
[main_object_related_vert_idx
].select
is True or \
1741 self
.main_object_selected_verts_count
== 0:
1743 ob_surface
.data
.vertices
[i
].co
= final_ob_duplicate
.data
.vertices
[i
].co
1744 ob_surface
.data
.vertices
[i
].select
= True
1745 crosshatch_verts_to_merge
.append(i
)
1747 # Make sure the vert in the main object is selected,
1748 # in case it wasn't selected and the "join crosshatch" option is active
1749 self
.main_object
.data
.vertices
[main_object_related_vert_idx
].select
= True
1751 # Delete duplicated object
1752 bpy
.ops
.object.delete({"selected_objects": [final_ob_duplicate
]})
1754 # Join crosshatched surface and main object
1755 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1756 ob_surface
.select_set(True)
1757 self
.main_object
.select_set(True)
1758 bpy
.context
.view_layer
.objects
.active
= self
.main_object
1760 bpy
.ops
.object.join('INVOKE_REGION_WIN')
1762 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1763 # Perform Remove doubles to merge verts
1764 if not (self
.automatic_join
is False and self
.main_object_selected_verts_count
== 0):
1765 bpy
.ops
.mesh
.remove_doubles(threshold
=0.0001)
1767 bpy
.ops
.mesh
.select_all(action
='DESELECT')
1769 # If the main object has modifiers, turn their "viewport view status"
1770 # to what it was before the forced deactivation above
1771 if len(self
.main_object
.modifiers
) > 0:
1772 for m_idx
in range(len(self
.main_object
.modifiers
)):
1773 self
.main_object
.modifiers
[m_idx
].show_viewport
= self
.modifiers_prev_viewport_state
[m_idx
]
1779 def rectangular_surface(self
, context
):
1781 all_selected_edges_idx
= []
1782 all_selected_verts
= []
1784 for ed
in self
.main_object
.data
.edges
:
1786 all_selected_edges_idx
.append(ed
.index
)
1789 if not ed
.vertices
[0] in all_selected_verts
:
1790 all_selected_verts
.append(self
.main_object
.data
.vertices
[ed
.vertices
[0]])
1791 if not ed
.vertices
[1] in all_selected_verts
:
1792 all_selected_verts
.append(self
.main_object
.data
.vertices
[ed
.vertices
[1]])
1794 # All verts (both from each edge) to determine later
1795 # which are at the tips (those not repeated twice)
1796 all_verts_idx
.append(ed
.vertices
[0])
1797 all_verts_idx
.append(ed
.vertices
[1])
1799 # Identify the tips and "middle-vertex" that separates U from V, if there is one
1800 all_chains_tips_idx
= []
1801 for v_idx
in all_verts_idx
:
1802 if all_verts_idx
.count(v_idx
) < 2:
1803 all_chains_tips_idx
.append(v_idx
)
1805 edges_connected_to_tips
= []
1806 for ed
in self
.main_object
.data
.edges
:
1807 if (ed
.vertices
[0] in all_chains_tips_idx
or ed
.vertices
[1] in all_chains_tips_idx
) and \
1808 not (ed
.vertices
[0] in all_verts_idx
and ed
.vertices
[1] in all_verts_idx
):
1810 edges_connected_to_tips
.append(ed
)
1812 # Check closed selections
1813 # List with groups of three verts, where the first element of the pair is
1814 # the unselected vert of a closed selection and the other two elements are the
1815 # selected neighbor verts (it will be useful to determine which selection chain
1816 # the unselected vert belongs to, and determine the "middle-vertex")
1817 single_unselected_verts_and_neighbors
= []
1819 # To identify a "closed" selection (a selection that is a closed chain except
1820 # for one vertex) find the vertex in common that have the edges connected to tips.
1821 # If there is a vertex in common, that one is the unselected vert that closes
1822 # the selection or is a "middle-vertex"
1823 single_unselected_verts
= []
1824 for ed
in edges_connected_to_tips
:
1825 for ed_b
in edges_connected_to_tips
:
1827 if ed
.vertices
[0] == ed_b
.vertices
[0] and \
1828 not self
.main_object
.data
.vertices
[ed
.vertices
[0]].select
and \
1829 ed
.vertices
[0] not in single_unselected_verts
:
1831 # The second element is one of the tips of the selected
1832 # vertices of the closed selection
1833 single_unselected_verts_and_neighbors
.append(
1834 [ed
.vertices
[0], ed
.vertices
[1], ed_b
.vertices
[1]]
1836 single_unselected_verts
.append(ed
.vertices
[0])
1838 elif ed
.vertices
[0] == ed_b
.vertices
[1] and \
1839 not self
.main_object
.data
.vertices
[ed
.vertices
[0]].select
and \
1840 ed
.vertices
[0] not in single_unselected_verts
:
1842 single_unselected_verts_and_neighbors
.append(
1843 [ed
.vertices
[0], ed
.vertices
[1], ed_b
.vertices
[0]]
1845 single_unselected_verts
.append(ed
.vertices
[0])
1847 elif ed
.vertices
[1] == ed_b
.vertices
[0] and \
1848 not self
.main_object
.data
.vertices
[ed
.vertices
[1]].select
and \
1849 ed
.vertices
[1] not in single_unselected_verts
:
1851 single_unselected_verts_and_neighbors
.append(
1852 [ed
.vertices
[1], ed
.vertices
[0], ed_b
.vertices
[1]]
1854 single_unselected_verts
.append(ed
.vertices
[1])
1856 elif ed
.vertices
[1] == ed_b
.vertices
[1] 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
[0]]
1863 single_unselected_verts
.append(ed
.vertices
[1])
1866 middle_vertex_idx
= None
1867 tips_to_discard_idx
= []
1869 # Check if there is a "middle-vertex", and get its index
1870 for i
in range(0, len(single_unselected_verts_and_neighbors
)):
1871 actual_chain_verts
= self
.get_ordered_verts(
1872 self
.main_object
, all_selected_edges_idx
,
1873 all_verts_idx
, single_unselected_verts_and_neighbors
[i
][1],
1877 if single_unselected_verts_and_neighbors
[i
][2] != \
1878 actual_chain_verts
[len(actual_chain_verts
) - 1].index
:
1880 middle_vertex_idx
= single_unselected_verts_and_neighbors
[i
][0]
1881 tips_to_discard_idx
.append(single_unselected_verts_and_neighbors
[i
][1])
1882 tips_to_discard_idx
.append(single_unselected_verts_and_neighbors
[i
][2])
1884 # List with pairs of verts that belong to the tips of each selection chain (row)
1885 verts_tips_same_chain_idx
= []
1886 if len(all_chains_tips_idx
) >= 2:
1888 for i
in range(0, len(all_chains_tips_idx
)):
1889 if all_chains_tips_idx
[i
] not in checked_v
:
1890 v_chain
= self
.get_ordered_verts(
1891 self
.main_object
, all_selected_edges_idx
,
1892 all_verts_idx
, all_chains_tips_idx
[i
],
1893 middle_vertex_idx
, None
1896 verts_tips_same_chain_idx
.append([v_chain
[0].index
, v_chain
[len(v_chain
) - 1].index
])
1898 checked_v
.append(v_chain
[0].index
)
1899 checked_v
.append(v_chain
[len(v_chain
) - 1].index
)
1901 # Selection tips (vertices).
1902 verts_tips_parsed_idx
= []
1903 if len(all_chains_tips_idx
) >= 2:
1904 for spec_v_idx
in all_chains_tips_idx
:
1905 if (spec_v_idx
not in tips_to_discard_idx
):
1906 verts_tips_parsed_idx
.append(spec_v_idx
)
1908 # Identify the type of selection made by the user
1909 if middle_vertex_idx
is not None:
1910 # If there are 4 tips (two selection chains), and
1911 # there is only one single unselected vert (the middle vert)
1912 if len(all_chains_tips_idx
) == 4 and len(single_unselected_verts_and_neighbors
) == 1:
1913 selection_type
= "TWO_CONNECTED"
1915 # The type of the selection was not identified, the script stops.
1916 self
.report({'WARNING'}, "The selection isn't valid.")
1918 self
.stopping_errors
= True
1922 if len(all_chains_tips_idx
) == 2: # If there are 2 tips
1923 selection_type
= "SINGLE"
1924 elif len(all_chains_tips_idx
) == 4: # If there are 4 tips
1925 selection_type
= "TWO_NOT_CONNECTED"
1926 elif len(all_chains_tips_idx
) == 0:
1927 if len(self
.main_splines
.data
.splines
) > 1:
1928 selection_type
= "NO_SELECTION"
1930 # If the selection was not identified and there is only one stroke,
1931 # there's no possibility to build a surface, so the script is interrupted
1932 self
.report({'WARNING'}, "The selection isn't valid.")
1934 self
.stopping_errors
= True
1938 # The type of the selection was not identified, the script stops
1939 self
.report({'WARNING'}, "The selection isn't valid.")
1941 self
.stopping_errors
= True
1945 # If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script
1946 if selection_type
== "TWO_NOT_CONNECTED" and len(self
.main_splines
.data
.splines
) == 1:
1947 self
.report({'WARNING'},
1948 "At least two strokes are needed when there are two not connected selections")
1950 self
.stopping_errors
= True
1954 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1956 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1957 self
.main_splines
.select_set(True)
1958 bpy
.context
.view_layer
.objects
.active
= self
.main_splines
1960 # Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out
1961 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1962 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1963 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1964 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1965 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1966 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1967 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1968 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1970 self
.selection_U_exists
= False
1971 self
.selection_U2_exists
= False
1972 self
.selection_V_exists
= False
1973 self
.selection_V2_exists
= False
1975 self
.selection_U_is_closed
= False
1976 self
.selection_U2_is_closed
= False
1977 self
.selection_V_is_closed
= False
1978 self
.selection_V2_is_closed
= False
1980 # Define what vertices are at the tips of each selection and are not the middle-vertex
1981 if selection_type
== "TWO_CONNECTED":
1982 self
.selection_U_exists
= True
1983 self
.selection_V_exists
= True
1985 closing_vert_U_idx
= None
1986 closing_vert_V_idx
= None
1987 closing_vert_U2_idx
= None
1988 closing_vert_V2_idx
= None
1990 # Determine which selection is Selection-U and which is Selection-V
1993 points_first_stroke_tips
= []
1996 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[verts_tips_parsed_idx
[0]].co
1999 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[middle_vertex_idx
].co
2002 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[verts_tips_parsed_idx
[1]].co
2005 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[middle_vertex_idx
].co
2007 points_first_stroke_tips
.append(
2008 self
.main_splines
.data
.splines
[0].bezier_points
[0].co
2010 points_first_stroke_tips
.append(
2011 self
.main_splines
.data
.splines
[0].bezier_points
[
2012 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2016 angle_A
= self
.orientation_difference(points_A
, points_first_stroke_tips
)
2017 angle_B
= self
.orientation_difference(points_B
, points_first_stroke_tips
)
2019 if angle_A
< angle_B
:
2020 first_vert_U_idx
= verts_tips_parsed_idx
[0]
2021 first_vert_V_idx
= verts_tips_parsed_idx
[1]
2023 first_vert_U_idx
= verts_tips_parsed_idx
[1]
2024 first_vert_V_idx
= verts_tips_parsed_idx
[0]
2026 elif selection_type
== "SINGLE" or selection_type
== "TWO_NOT_CONNECTED":
2027 first_sketched_point_first_stroke_co
= self
.main_splines
.data
.splines
[0].bezier_points
[0].co
2028 last_sketched_point_first_stroke_co
= \
2029 self
.main_splines
.data
.splines
[0].bezier_points
[
2030 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2032 first_sketched_point_last_stroke_co
= \
2033 self
.main_splines
.data
.splines
[
2034 len(self
.main_splines
.data
.splines
) - 1
2035 ].bezier_points
[0].co
2036 if len(self
.main_splines
.data
.splines
) > 1:
2037 first_sketched_point_second_stroke_co
= self
.main_splines
.data
.splines
[1].bezier_points
[0].co
2038 last_sketched_point_second_stroke_co
= \
2039 self
.main_splines
.data
.splines
[1].bezier_points
[
2040 len(self
.main_splines
.data
.splines
[1].bezier_points
) - 1
2043 single_unselected_neighbors
= [] # Only the neighbors of the single unselected verts
2044 for verts_neig_idx
in single_unselected_verts_and_neighbors
:
2045 single_unselected_neighbors
.append(verts_neig_idx
[1])
2046 single_unselected_neighbors
.append(verts_neig_idx
[2])
2048 all_chains_tips_and_middle_vert
= []
2049 for v_idx
in all_chains_tips_idx
:
2050 if v_idx
not in single_unselected_neighbors
:
2051 all_chains_tips_and_middle_vert
.append(v_idx
)
2053 all_chains_tips_and_middle_vert
+= single_unselected_verts
2055 all_participating_verts
= all_chains_tips_and_middle_vert
+ all_verts_idx
2057 # The tip of the selected vertices nearest to the first point of the first sketched stroke
2058 nearest_tip_to_first_st_first_pt_idx
, shortest_distance_to_first_stroke
= \
2059 self
.shortest_distance(
2061 first_sketched_point_first_stroke_co
,
2062 all_chains_tips_and_middle_vert
2064 # If the nearest tip is not from a closed selection, get the opposite tip vertex index
2065 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2066 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2068 nearest_tip_to_first_st_first_pt_opposite_idx
= \
2070 nearest_tip_to_first_st_first_pt_idx
,
2071 verts_tips_same_chain_idx
2073 # The tip of the selected vertices nearest to the last point of the first sketched stroke
2074 nearest_tip_to_first_st_last_pt_idx
, _temp_dist
= \
2075 self
.shortest_distance(
2077 last_sketched_point_first_stroke_co
,
2078 all_chains_tips_and_middle_vert
2080 # The tip of the selected vertices nearest to the first point of the last sketched stroke
2081 nearest_tip_to_last_st_first_pt_idx
, shortest_distance_to_last_stroke
= \
2082 self
.shortest_distance(
2084 first_sketched_point_last_stroke_co
,
2085 all_chains_tips_and_middle_vert
2087 if len(self
.main_splines
.data
.splines
) > 1:
2088 # The selected vertex nearest to the first point of the second sketched stroke
2089 # (This will be useful to determine the direction of the closed
2090 # selection V when extruding along strokes)
2091 nearest_vert_to_second_st_first_pt_idx
, _temp_dist
= \
2092 self
.shortest_distance(
2094 first_sketched_point_second_stroke_co
,
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 V2 when extruding along strokes)
2100 nearest_vert_to_second_st_last_pt_idx
, _temp_dist
= \
2101 self
.shortest_distance(
2103 last_sketched_point_second_stroke_co
,
2106 # Determine if the single selection will be treated as U or as V
2108 for i
in all_selected_edges_idx
:
2110 (self
.main_object
.matrix_world
@
2111 self
.main_object
.data
.vertices
[self
.main_object
.data
.edges
[i
].vertices
[0]].co
) -
2112 (self
.main_object
.matrix_world
@
2113 self
.main_object
.data
.vertices
[self
.main_object
.data
.edges
[i
].vertices
[1]].co
)
2116 average_edge_length
= edges_sum
/ len(all_selected_edges_idx
)
2118 # Get shortest distance from the first point of the last stroke to any participating vertex
2119 _temp_idx
, shortest_distance_to_last_stroke
= \
2120 self
.shortest_distance(
2122 first_sketched_point_last_stroke_co
,
2123 all_participating_verts
2125 # If the beginning of the first stroke is near enough, and its orientation
2126 # difference with the first edge of the nearest selection chain is not too high,
2127 # interpret things as an "extrude along strokes" instead of "extrude through strokes"
2128 if shortest_distance_to_first_stroke
< average_edge_length
/ 4 and \
2129 shortest_distance_to_last_stroke
< average_edge_length
and \
2130 len(self
.main_splines
.data
.splines
) > 1:
2132 self
.selection_U_exists
= False
2133 self
.selection_V_exists
= True
2134 # If the first selection is not closed
2135 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2136 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2137 self
.selection_V_is_closed
= False
2138 closing_vert_U_idx
= None
2139 closing_vert_U2_idx
= None
2140 closing_vert_V_idx
= None
2141 closing_vert_V2_idx
= None
2143 first_vert_V_idx
= nearest_tip_to_first_st_first_pt_idx
2145 if selection_type
== "TWO_NOT_CONNECTED":
2146 self
.selection_V2_exists
= True
2148 first_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2150 self
.selection_V_is_closed
= True
2151 closing_vert_V_idx
= nearest_tip_to_first_st_first_pt_idx
2153 # Get the neighbors of the first (unselected) vert of the closed selection U.
2155 for verts
in single_unselected_verts_and_neighbors
:
2156 if verts
[0] == nearest_tip_to_first_st_first_pt_idx
:
2157 vert_neighbors
.append(verts
[1])
2158 vert_neighbors
.append(verts
[2])
2161 verts_V
= self
.get_ordered_verts(
2162 self
.main_object
, all_selected_edges_idx
,
2163 all_verts_idx
, vert_neighbors
[0], middle_vertex_idx
, None
2166 for i
in range(0, len(verts_V
)):
2167 if verts_V
[i
].index
== nearest_vert_to_second_st_first_pt_idx
:
2168 # If the vertex nearest to the first point of the second stroke
2169 # is in the first half of the selected verts
2170 if i
>= len(verts_V
) / 2:
2171 first_vert_V_idx
= vert_neighbors
[1]
2174 first_vert_V_idx
= vert_neighbors
[0]
2177 if selection_type
== "TWO_NOT_CONNECTED":
2178 self
.selection_V2_exists
= True
2179 # If the second selection is not closed
2180 if nearest_tip_to_first_st_last_pt_idx
not in single_unselected_verts
or \
2181 nearest_tip_to_first_st_last_pt_idx
== middle_vertex_idx
:
2183 self
.selection_V2_is_closed
= False
2184 closing_vert_V2_idx
= None
2185 first_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2188 self
.selection_V2_is_closed
= True
2189 closing_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2191 # Get the neighbors of the first (unselected) vert of the closed selection U
2193 for verts
in single_unselected_verts_and_neighbors
:
2194 if verts
[0] == nearest_tip_to_first_st_last_pt_idx
:
2195 vert_neighbors
.append(verts
[1])
2196 vert_neighbors
.append(verts
[2])
2199 verts_V2
= self
.get_ordered_verts(
2200 self
.main_object
, all_selected_edges_idx
,
2201 all_verts_idx
, vert_neighbors
[0], middle_vertex_idx
, None
2204 for i
in range(0, len(verts_V2
)):
2205 if verts_V2
[i
].index
== nearest_vert_to_second_st_last_pt_idx
:
2206 # If the vertex nearest to the first point of the second stroke
2207 # is in the first half of the selected verts
2208 if i
>= len(verts_V2
) / 2:
2209 first_vert_V2_idx
= vert_neighbors
[1]
2212 first_vert_V2_idx
= vert_neighbors
[0]
2215 self
.selection_V2_exists
= False
2218 self
.selection_U_exists
= True
2219 self
.selection_V_exists
= False
2220 # If the first selection is not closed
2221 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2222 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2223 self
.selection_U_is_closed
= False
2224 closing_vert_U_idx
= None
2228 self
.main_object
.matrix_world
@
2229 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_idx
].co
2232 self
.main_object
.matrix_world
@
2233 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_opposite_idx
].co
2235 points_first_stroke_tips
= []
2236 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[0].co
)
2237 points_first_stroke_tips
.append(
2238 self
.main_splines
.data
.splines
[0].bezier_points
[
2239 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2242 vec_A
= points_tips
[0] - points_tips
[1]
2243 vec_B
= points_first_stroke_tips
[0] - points_first_stroke_tips
[1]
2245 # Compare the direction of the selection and the first
2246 # grease pencil stroke to determine which is the "first" vertex of the selection
2247 if vec_A
.dot(vec_B
) < 0:
2248 first_vert_U_idx
= nearest_tip_to_first_st_first_pt_opposite_idx
2250 first_vert_U_idx
= nearest_tip_to_first_st_first_pt_idx
2253 self
.selection_U_is_closed
= True
2254 closing_vert_U_idx
= nearest_tip_to_first_st_first_pt_idx
2256 # Get the neighbors of the first (unselected) vert of the closed selection U
2258 for verts
in single_unselected_verts_and_neighbors
:
2259 if verts
[0] == nearest_tip_to_first_st_first_pt_idx
:
2260 vert_neighbors
.append(verts
[1])
2261 vert_neighbors
.append(verts
[2])
2264 points_first_and_neighbor
= []
2265 points_first_and_neighbor
.append(
2266 self
.main_object
.matrix_world
@
2267 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_idx
].co
2269 points_first_and_neighbor
.append(
2270 self
.main_object
.matrix_world
@
2271 self
.main_object
.data
.vertices
[vert_neighbors
[0]].co
2273 points_first_stroke_tips
= []
2274 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[0].co
)
2275 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[1].co
)
2277 vec_A
= points_first_and_neighbor
[0] - points_first_and_neighbor
[1]
2278 vec_B
= points_first_stroke_tips
[0] - points_first_stroke_tips
[1]
2280 # Compare the direction of the selection and the first grease pencil stroke to
2281 # determine which is the vertex neighbor to the first vertex (unselected) of
2282 # the closed selection. This will determine the direction of the closed selection
2283 if vec_A
.dot(vec_B
) < 0:
2284 first_vert_U_idx
= vert_neighbors
[1]
2286 first_vert_U_idx
= vert_neighbors
[0]
2288 if selection_type
== "TWO_NOT_CONNECTED":
2289 self
.selection_U2_exists
= True
2290 # If the second selection is not closed
2291 if nearest_tip_to_last_st_first_pt_idx
not in single_unselected_verts
or \
2292 nearest_tip_to_last_st_first_pt_idx
== middle_vertex_idx
:
2294 self
.selection_U2_is_closed
= False
2295 closing_vert_U2_idx
= None
2296 first_vert_U2_idx
= nearest_tip_to_last_st_first_pt_idx
2298 self
.selection_U2_is_closed
= True
2299 closing_vert_U2_idx
= nearest_tip_to_last_st_first_pt_idx
2301 # Get the neighbors of the first (unselected) vert of the closed selection U
2303 for verts
in single_unselected_verts_and_neighbors
:
2304 if verts
[0] == nearest_tip_to_last_st_first_pt_idx
:
2305 vert_neighbors
.append(verts
[1])
2306 vert_neighbors
.append(verts
[2])
2309 points_first_and_neighbor
= []
2310 points_first_and_neighbor
.append(
2311 self
.main_object
.matrix_world
@
2312 self
.main_object
.data
.vertices
[nearest_tip_to_last_st_first_pt_idx
].co
2314 points_first_and_neighbor
.append(
2315 self
.main_object
.matrix_world
@
2316 self
.main_object
.data
.vertices
[vert_neighbors
[0]].co
2318 points_last_stroke_tips
= []
2319 points_last_stroke_tips
.append(
2320 self
.main_splines
.data
.splines
[
2321 len(self
.main_splines
.data
.splines
) - 1
2322 ].bezier_points
[0].co
2324 points_last_stroke_tips
.append(
2325 self
.main_splines
.data
.splines
[
2326 len(self
.main_splines
.data
.splines
) - 1
2327 ].bezier_points
[1].co
2329 vec_A
= points_first_and_neighbor
[0] - points_first_and_neighbor
[1]
2330 vec_B
= points_last_stroke_tips
[0] - points_last_stroke_tips
[1]
2332 # Compare the direction of the selection and the last grease pencil stroke to
2333 # determine which is the vertex neighbor to the first vertex (unselected) of
2334 # the closed selection. This will determine the direction of the closed selection
2335 if vec_A
.dot(vec_B
) < 0:
2336 first_vert_U2_idx
= vert_neighbors
[1]
2338 first_vert_U2_idx
= vert_neighbors
[0]
2340 self
.selection_U2_exists
= False
2342 elif selection_type
== "NO_SELECTION":
2343 self
.selection_U_exists
= False
2344 self
.selection_V_exists
= False
2346 # Get an ordered list of the vertices of Selection-U
2347 verts_ordered_U
= []
2348 if self
.selection_U_exists
:
2349 verts_ordered_U
= self
.get_ordered_verts(
2350 self
.main_object
, all_selected_edges_idx
,
2351 all_verts_idx
, first_vert_U_idx
,
2352 middle_vertex_idx
, closing_vert_U_idx
2355 # Get an ordered list of the vertices of Selection-U2
2356 verts_ordered_U2
= []
2357 if self
.selection_U2_exists
:
2358 verts_ordered_U2
= self
.get_ordered_verts(
2359 self
.main_object
, all_selected_edges_idx
,
2360 all_verts_idx
, first_vert_U2_idx
,
2361 middle_vertex_idx
, closing_vert_U2_idx
2364 # Get an ordered list of the vertices of Selection-V
2365 verts_ordered_V
= []
2366 if self
.selection_V_exists
:
2367 verts_ordered_V
= self
.get_ordered_verts(
2368 self
.main_object
, all_selected_edges_idx
,
2369 all_verts_idx
, first_vert_V_idx
,
2370 middle_vertex_idx
, closing_vert_V_idx
2372 verts_ordered_V_indices
= [x
.index
for x
in verts_ordered_V
]
2374 # Get an ordered list of the vertices of Selection-V2
2375 verts_ordered_V2
= []
2376 if self
.selection_V2_exists
:
2377 verts_ordered_V2
= self
.get_ordered_verts(
2378 self
.main_object
, all_selected_edges_idx
,
2379 all_verts_idx
, first_vert_V2_idx
,
2380 middle_vertex_idx
, closing_vert_V2_idx
2383 # Check if when there are two-not-connected selections both have the same
2384 # number of verts. If not terminate the script
2385 if ((self
.selection_U2_exists
and len(verts_ordered_U
) != len(verts_ordered_U2
)) or
2386 (self
.selection_V2_exists
and len(verts_ordered_V
) != len(verts_ordered_V2
))):
2388 self
.report({'WARNING'}, "Both selections must have the same number of edges")
2390 self
.stopping_errors
= True
2394 # Calculate edges U proportions
2395 # Sum selected edges U lengths
2396 edges_lengths_U
= []
2397 edges_lengths_sum_U
= 0
2399 if self
.selection_U_exists
:
2400 edges_lengths_U
, edges_lengths_sum_U
= self
.get_chain_length(
2404 if self
.selection_U2_exists
:
2405 edges_lengths_U2
, edges_lengths_sum_U2
= self
.get_chain_length(
2409 # Sum selected edges V lengths
2410 edges_lengths_V
= []
2411 edges_lengths_sum_V
= 0
2413 if self
.selection_V_exists
:
2414 edges_lengths_V
, edges_lengths_sum_V
= self
.get_chain_length(
2418 if self
.selection_V2_exists
:
2419 edges_lengths_V2
, edges_lengths_sum_V2
= self
.get_chain_length(
2424 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2425 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN',
2426 number_cuts
=bpy
.context
.scene
.bsurfaces
.SURFSK_precision
)
2427 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2430 edges_proportions_U
= []
2431 edges_proportions_U
= self
.get_edges_proportions(
2432 edges_lengths_U
, edges_lengths_sum_U
,
2433 self
.selection_U_exists
, self
.edges_U
2435 verts_count_U
= len(edges_proportions_U
) + 1
2437 if self
.selection_U2_exists
:
2438 edges_proportions_U2
= []
2439 edges_proportions_U2
= self
.get_edges_proportions(
2440 edges_lengths_U2
, edges_lengths_sum_U2
,
2441 self
.selection_U2_exists
, self
.edges_V
2445 edges_proportions_V
= []
2446 edges_proportions_V
= self
.get_edges_proportions(
2447 edges_lengths_V
, edges_lengths_sum_V
,
2448 self
.selection_V_exists
, self
.edges_V
2451 if self
.selection_V2_exists
:
2452 edges_proportions_V2
= []
2453 edges_proportions_V2
= self
.get_edges_proportions(
2454 edges_lengths_V2
, edges_lengths_sum_V2
,
2455 self
.selection_V2_exists
, self
.edges_V
2458 # Cyclic Follow: simplify sketched curves, make them Cyclic, and complete
2459 # the actual sketched curves with a "closing segment"
2460 if self
.cyclic_follow
and not self
.selection_V_exists
and not \
2461 ((self
.selection_U_exists
and not self
.selection_U_is_closed
) or
2462 (self
.selection_U2_exists
and not self
.selection_U2_is_closed
)):
2464 simplified_spline_coords
= []
2465 simplified_curve
= []
2466 ob_simplified_curve
= []
2467 splines_first_v_co
= []
2468 for i
in range(len(self
.main_splines
.data
.splines
)):
2469 # Create a curve object for the actual spline "cyclic extension"
2470 simplified_curve
.append(bpy
.data
.curves
.new('SURFSKIO_simpl_crv', 'CURVE'))
2471 ob_simplified_curve
.append(bpy
.data
.objects
.new('SURFSKIO_simpl_crv', simplified_curve
[i
]))
2472 bpy
.context
.collection
.objects
.link(ob_simplified_curve
[i
])
2474 simplified_curve
[i
].dimensions
= "3D"
2477 for bp
in self
.main_splines
.data
.splines
[i
].bezier_points
:
2478 spline_coords
.append(bp
.co
)
2481 simplified_spline_coords
.append(self
.simplify_spline(spline_coords
, 5))
2483 # Get the coordinates of the first vert of the actual spline
2484 splines_first_v_co
.append(simplified_spline_coords
[i
][0])
2486 # Generate the spline
2487 spline
= simplified_curve
[i
].splines
.new('BEZIER')
2488 # less one because one point is added when the spline is created
2489 spline
.bezier_points
.add(len(simplified_spline_coords
[i
]) - 1)
2490 for p
in range(0, len(simplified_spline_coords
[i
])):
2491 spline
.bezier_points
[p
].co
= simplified_spline_coords
[i
][p
]
2493 spline
.use_cyclic_u
= True
2495 spline_bp_count
= len(spline
.bezier_points
)
2497 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2498 ob_simplified_curve
[i
].select_set(True)
2499 bpy
.context
.view_layer
.objects
.active
= ob_simplified_curve
[i
]
2501 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2502 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
2503 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2504 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2505 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2507 # Select the "closing segment", and subdivide it
2508 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_control_point
= True
2509 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_left_handle
= True
2510 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_right_handle
= True
2512 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_control_point
= True
2513 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_left_handle
= True
2514 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_right_handle
= True
2516 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2518 (ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].co
-
2519 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].co
).length
/
2520 self
.average_gp_segment_length
2523 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=segments
)
2525 # Delete the other vertices and make it non-cyclic to
2526 # keep only the needed verts of the "closing segment"
2527 bpy
.ops
.curve
.select_all(action
='INVERT')
2528 bpy
.ops
.curve
.delete(type='VERT')
2529 ob_simplified_curve
[i
].data
.splines
[0].use_cyclic_u
= False
2530 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2532 # Add the points of the "closing segment" to the original curve from grease pencil stroke
2533 first_new_index
= len(self
.main_splines
.data
.splines
[i
].bezier_points
)
2534 self
.main_splines
.data
.splines
[i
].bezier_points
.add(
2535 len(ob_simplified_curve
[i
].data
.splines
[0].bezier_points
) - 1
2537 for t
in range(1, len(ob_simplified_curve
[i
].data
.splines
[0].bezier_points
)):
2538 self
.main_splines
.data
.splines
[i
].bezier_points
[t
- 1 + first_new_index
].co
= \
2539 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[t
].co
2541 # Delete the temporal curve
2542 bpy
.ops
.object.delete({"selected_objects": [ob_simplified_curve
[i
]]})
2544 # Get the coords of the points distributed along the sketched strokes,
2545 # with proportions-U of the first selection
2546 pts_on_strokes_with_proportions_U
= self
.distribute_pts(
2547 self
.main_splines
.data
.splines
,
2550 sketched_splines_parsed
= []
2552 if self
.selection_U2_exists
:
2553 # Initialize the multidimensional list with the proportions of all the segments
2554 proportions_loops_crossing_strokes
= []
2555 for i
in range(len(pts_on_strokes_with_proportions_U
)):
2556 proportions_loops_crossing_strokes
.append([])
2558 for t
in range(len(pts_on_strokes_with_proportions_U
[0])):
2559 proportions_loops_crossing_strokes
[i
].append(None)
2561 # Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U
2562 for lp
in range(len(pts_on_strokes_with_proportions_U
[0])):
2563 loop_segments_lengths
= []
2565 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2566 # When on the first stroke, add the segment from the selection to the dirst stroke
2568 loop_segments_lengths
.append(
2569 ((self
.main_object
.matrix_world
@ verts_ordered_U
[lp
].co
) -
2570 pts_on_strokes_with_proportions_U
[0][lp
]).length
2572 # For all strokes except for the last, calculate the distance
2573 # from the actual stroke to the next
2574 if st
!= len(pts_on_strokes_with_proportions_U
) - 1:
2575 loop_segments_lengths
.append(
2576 (pts_on_strokes_with_proportions_U
[st
][lp
] -
2577 pts_on_strokes_with_proportions_U
[st
+ 1][lp
]).length
2579 # When on the last stroke, add the segments
2580 # from the last stroke to the second selection
2581 if st
== len(pts_on_strokes_with_proportions_U
) - 1:
2582 loop_segments_lengths
.append(
2583 (pts_on_strokes_with_proportions_U
[st
][lp
] -
2584 (self
.main_object
.matrix_world
@ verts_ordered_U2
[lp
].co
)).length
2586 # Calculate full loop length
2587 loop_seg_lengths_sum
= 0
2588 for i
in range(len(loop_segments_lengths
)):
2589 loop_seg_lengths_sum
+= loop_segments_lengths
[i
]
2591 # Fill the multidimensional list with the proportions of all the segments
2592 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2593 proportions_loops_crossing_strokes
[st
][lp
] = \
2594 loop_segments_lengths
[st
] / loop_seg_lengths_sum
2596 # Calculate proportions for each stroke
2597 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2598 actual_stroke_spline
= []
2599 # Needs to be a list for the "distribute_pts" method
2600 actual_stroke_spline
.append(self
.main_splines
.data
.splines
[st
])
2602 # Calculate the proportions for the actual stroke.
2603 actual_edges_proportions_U
= []
2604 for i
in range(len(edges_proportions_U
)):
2607 # Sum the proportions of this loop up to the actual.
2608 for t
in range(0, st
+ 1):
2609 proportions_sum
+= proportions_loops_crossing_strokes
[t
][i
]
2610 # i + 1, because proportions_loops_crossing_strokes refers to loops,
2611 # and the proportions refer to edges, so we start at the element 1
2612 # of proportions_loops_crossing_strokes instead of element 0
2613 actual_edges_proportions_U
.append(
2614 edges_proportions_U
[i
] -
2615 ((edges_proportions_U
[i
] - edges_proportions_U2
[i
]) * proportions_sum
)
2617 points_actual_spline
= self
.distribute_pts(actual_stroke_spline
, actual_edges_proportions_U
)
2618 sketched_splines_parsed
.append(points_actual_spline
[0])
2620 sketched_splines_parsed
= pts_on_strokes_with_proportions_U
2622 # If the selection type is "TWO_NOT_CONNECTED" replace the
2623 # points of the last spline with the points in the "target" selection
2624 if selection_type
== "TWO_NOT_CONNECTED":
2625 if self
.selection_U2_exists
:
2626 for i
in range(0, len(sketched_splines_parsed
[len(sketched_splines_parsed
) - 1])):
2627 sketched_splines_parsed
[len(sketched_splines_parsed
) - 1][i
] = \
2628 self
.main_object
.matrix_world
@ verts_ordered_U2
[i
].co
2630 # Create temporary curves along the "control-points" found
2631 # on the sketched curves and the mesh selection
2632 mesh_ctrl_pts_name
= "SURFSKIO_ctrl_pts"
2633 me
= bpy
.data
.meshes
.new(mesh_ctrl_pts_name
)
2634 ob_ctrl_pts
= bpy
.data
.objects
.new(mesh_ctrl_pts_name
, me
)
2635 ob_ctrl_pts
.data
= me
2636 bpy
.context
.collection
.objects
.link(ob_ctrl_pts
)
2643 for i
in range(0, verts_count_U
):
2644 vert_num_in_spline
= 1
2646 if self
.selection_U_exists
:
2647 ob_ctrl_pts
.data
.vertices
.add(1)
2648 last_v
= ob_ctrl_pts
.data
.vertices
[len(ob_ctrl_pts
.data
.vertices
) - 1]
2649 last_v
.co
= self
.main_object
.matrix_world
@ verts_ordered_U
[i
].co
2651 vert_num_in_spline
+= 1
2653 for t
in range(0, len(sketched_splines_parsed
)):
2654 ob_ctrl_pts
.data
.vertices
.add(1)
2655 v
= ob_ctrl_pts
.data
.vertices
[len(ob_ctrl_pts
.data
.vertices
) - 1]
2656 v
.co
= sketched_splines_parsed
[t
][i
]
2658 if vert_num_in_spline
> 1:
2659 ob_ctrl_pts
.data
.edges
.add(1)
2660 ob_ctrl_pts
.data
.edges
[len(ob_ctrl_pts
.data
.edges
) - 1].vertices
[0] = \
2661 len(ob_ctrl_pts
.data
.vertices
) - 2
2662 ob_ctrl_pts
.data
.edges
[len(ob_ctrl_pts
.data
.edges
) - 1].vertices
[1] = \
2663 len(ob_ctrl_pts
.data
.vertices
) - 1
2666 first_verts
.append(v
.index
)
2669 second_verts
.append(v
.index
)
2671 if t
== len(sketched_splines_parsed
) - 1:
2672 last_verts
.append(v
.index
)
2675 vert_num_in_spline
+= 1
2677 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2678 ob_ctrl_pts
.select_set(True)
2679 bpy
.context
.view_layer
.objects
.active
= ob_ctrl_pts
2681 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2682 bpy
.ops
.mesh
.select_all(action
='DESELECT')
2683 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2685 # Determine which loops-U will be "Cyclic"
2686 for i
in range(0, len(first_verts
)):
2687 # When there is Cyclic Cross there is no need of
2688 # Automatic Join, (and there are at least three strokes)
2689 if self
.automatic_join
and not self
.cyclic_cross
and \
2690 selection_type
!= "TWO_CONNECTED" and len(self
.main_splines
.data
.splines
) >= 3:
2692 v
= ob_ctrl_pts
.data
.vertices
2693 first_point_co
= v
[first_verts
[i
]].co
2694 second_point_co
= v
[second_verts
[i
]].co
2695 last_point_co
= v
[last_verts
[i
]].co
2697 # Coordinates of the point in the center of both the first and last verts.
2699 (first_point_co
[0] + last_point_co
[0]) / 2,
2700 (first_point_co
[1] + last_point_co
[1]) / 2,
2701 (first_point_co
[2] + last_point_co
[2]) / 2
2703 vec_A
= second_point_co
- first_point_co
2704 vec_B
= second_point_co
- Vector(verts_center_co
)
2706 # Calculate the length of the first segment of the loop,
2707 # and the length it would have after moving the first vert
2708 # to the middle position between first and last
2709 length_original
= (second_point_co
- first_point_co
).length
2710 length_target
= (second_point_co
- Vector(verts_center_co
)).length
2712 angle
= vec_A
.angle(vec_B
) / pi
2714 # If the target length doesn't stretch too much, and the
2715 # its angle doesn't change to much either
2716 if length_target
<= length_original
* 1.03 * self
.join_stretch_factor
and \
2717 angle
<= 0.008 * self
.join_stretch_factor
and not self
.selection_U_exists
:
2719 cyclic_loops_U
.append(True)
2720 # Move the first vert to the center coordinates
2721 ob_ctrl_pts
.data
.vertices
[first_verts
[i
]].co
= verts_center_co
2722 # Select the last verts from Cyclic loops, for later deletion all at once
2723 v
[last_verts
[i
]].select
= True
2725 cyclic_loops_U
.append(False)
2727 # If "Cyclic Cross" is active then "all" crossing curves become cyclic
2728 if self
.cyclic_cross
and not self
.selection_U_exists
and not \
2729 ((self
.selection_V_exists
and not self
.selection_V_is_closed
) or
2730 (self
.selection_V2_exists
and not self
.selection_V2_is_closed
)):
2732 cyclic_loops_U
.append(True)
2734 cyclic_loops_U
.append(False)
2736 # The cyclic_loops_U list needs to be reversed.
2737 cyclic_loops_U
.reverse()
2739 # Delete the previously selected (last_)verts.
2740 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2741 bpy
.ops
.mesh
.delete('INVOKE_REGION_WIN', type='VERT')
2742 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2744 # Create curves from control points.
2745 bpy
.ops
.object.convert('INVOKE_REGION_WIN', target
='CURVE', keep_original
=False)
2746 ob_curves_surf
= bpy
.context
.view_layer
.objects
.active
2747 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2748 bpy
.ops
.curve
.spline_type_set('INVOKE_REGION_WIN', type='BEZIER')
2749 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2751 # Make Cyclic the splines designated as Cyclic.
2752 for i
in range(0, len(cyclic_loops_U
)):
2753 ob_curves_surf
.data
.splines
[i
].use_cyclic_u
= cyclic_loops_U
[i
]
2755 # Get the coords of all points on first loop-U, for later comparison with its
2756 # subdivided version, to know which points of the loops-U are crossed by the
2757 # original strokes. The indices will be the same for the other loops-U
2758 if self
.loops_on_strokes
:
2759 coords_loops_U_control_points
= []
2760 for p
in ob_ctrl_pts
.data
.splines
[0].bezier_points
:
2761 coords_loops_U_control_points
.append(["%.4f" % p
.co
[0], "%.4f" % p
.co
[1], "%.4f" % p
.co
[2]])
2763 tuple(coords_loops_U_control_points
)
2765 # Calculate number of edges-V in case option "Loops on strokes" is active or inactive
2766 if self
.loops_on_strokes
and not self
.selection_V_exists
:
2767 edges_V_count
= len(self
.main_splines
.data
.splines
) * self
.edges_V
2769 edges_V_count
= len(edges_proportions_V
)
2771 # The Follow precision will vary depending on the number of Follow face-loops
2772 precision_multiplier
= round(2 + (edges_V_count
/ 15))
2773 curve_cuts
= bpy
.context
.scene
.bsurfaces
.SURFSK_precision
* precision_multiplier
2775 # Subdivide the curves
2776 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=curve_cuts
)
2778 # The verts position shifting that happens with splines subdivision.
2779 # For later reorder splines points
2780 verts_position_shift
= curve_cuts
+ 1
2781 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2783 # Reorder coordinates of the points of each spline to put the first point of
2784 # the spline starting at the position it was the first point before sudividing
2785 # the curve. And make a new curve object per spline (to handle memory better later)
2786 splines_U_objects
= []
2787 for i
in range(len(ob_curves_surf
.data
.splines
)):
2788 spline_U_curve
= bpy
.data
.curves
.new('SURFSKIO_spline_U_' + str(i
), 'CURVE')
2789 ob_spline_U
= bpy
.data
.objects
.new('SURFSKIO_spline_U_' + str(i
), spline_U_curve
)
2790 bpy
.context
.collection
.objects
.link(ob_spline_U
)
2792 spline_U_curve
.dimensions
= "3D"
2794 # Add points to the spline in the new curve object
2795 ob_spline_U
.data
.splines
.new('BEZIER')
2796 for t
in range(len(ob_curves_surf
.data
.splines
[i
].bezier_points
)):
2797 if cyclic_loops_U
[i
] is True and not self
.selection_U_exists
: # If the loop is cyclic
2798 if t
+ verts_position_shift
<= len(ob_curves_surf
.data
.splines
[i
].bezier_points
) - 1:
2799 point_index
= t
+ verts_position_shift
2801 point_index
= t
+ verts_position_shift
- len(ob_curves_surf
.data
.splines
[i
].bezier_points
)
2804 # to avoid adding the first point since it's added when the spline is created
2806 ob_spline_U
.data
.splines
[0].bezier_points
.add(1)
2807 ob_spline_U
.data
.splines
[0].bezier_points
[t
].co
= \
2808 ob_curves_surf
.data
.splines
[i
].bezier_points
[point_index
].co
2810 if cyclic_loops_U
[i
] is True and not self
.selection_U_exists
: # If the loop is cyclic
2811 # Add a last point at the same location as the first one
2812 ob_spline_U
.data
.splines
[0].bezier_points
.add(1)
2813 ob_spline_U
.data
.splines
[0].bezier_points
[len(ob_spline_U
.data
.splines
[0].bezier_points
) - 1].co
= \
2814 ob_spline_U
.data
.splines
[0].bezier_points
[0].co
2816 ob_spline_U
.data
.splines
[0].use_cyclic_u
= False
2818 splines_U_objects
.append(ob_spline_U
)
2819 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2820 ob_spline_U
.select_set(True)
2821 bpy
.context
.view_layer
.objects
.active
= ob_spline_U
2823 # When option "Loops on strokes" is active each "Cross" loop will have
2824 # its own proportions according to where the original strokes "touch" them
2825 if self
.loops_on_strokes
:
2826 # Get the indices of points where the original strokes "touch" loops-U
2827 points_U_crossed_by_strokes
= []
2828 for i
in range(len(splines_U_objects
[0].data
.splines
[0].bezier_points
)):
2829 bp
= splines_U_objects
[0].data
.splines
[0].bezier_points
[i
]
2830 if ["%.4f" % bp
.co
[0], "%.4f" % bp
.co
[1], "%.4f" % bp
.co
[2]] in coords_loops_U_control_points
:
2831 points_U_crossed_by_strokes
.append(i
)
2833 # Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke
2834 edge_order_number_for_splines
= {}
2835 if self
.selection_V_exists
:
2836 # For two-connected selections add a first hypothetic stroke at the beginning.
2837 if selection_type
== "TWO_CONNECTED":
2838 edge_order_number_for_splines
[0] = 0
2840 for i
in range(len(self
.main_splines
.data
.splines
)):
2841 sp
= self
.main_splines
.data
.splines
[i
]
2842 v_idx
, _dist_temp
= self
.shortest_distance(
2844 sp
.bezier_points
[0].co
,
2845 verts_ordered_V_indices
2847 # Get the position (edges count) of the vert v_idx in the selected chain V
2848 edge_idx_in_chain
= verts_ordered_V_indices
.index(v_idx
)
2850 # For two-connected selections the strokes go after the
2851 # hypothetic stroke added before, so the index adds one per spline
2852 if selection_type
== "TWO_CONNECTED":
2853 spline_number
= i
+ 1
2857 edge_order_number_for_splines
[spline_number
] = edge_idx_in_chain
2859 # Get the first and last verts indices for later comparison
2862 elif i
== len(self
.main_splines
.data
.splines
) - 1:
2865 if self
.selection_V_is_closed
:
2866 # If there is no last stroke on the last vertex (same as first vertex),
2867 # add a hypothetic spline at last vert order
2868 if first_v_idx
!= last_v_idx
:
2869 edge_order_number_for_splines
[(len(self
.main_splines
.data
.splines
) - 1) + 1] = \
2870 len(verts_ordered_V_indices
) - 1
2872 if self
.cyclic_cross
:
2873 edge_order_number_for_splines
[len(self
.main_splines
.data
.splines
) - 1] = \
2874 len(verts_ordered_V_indices
) - 2
2875 edge_order_number_for_splines
[(len(self
.main_splines
.data
.splines
) - 1) + 1] = \
2876 len(verts_ordered_V_indices
) - 1
2878 edge_order_number_for_splines
[len(self
.main_splines
.data
.splines
) - 1] = \
2879 len(verts_ordered_V_indices
) - 1
2881 # Get the coords of the points distributed along the
2882 # "crossing curves", with appropriate proportions-V
2883 surface_splines_parsed
= []
2884 for i
in range(len(splines_U_objects
)):
2885 sp_ob
= splines_U_objects
[i
]
2886 # If "Loops on strokes" option is active, calculate the proportions for each loop-U
2887 if self
.loops_on_strokes
:
2888 # Segments distances from stroke to stroke
2891 segments_distances
= []
2892 for t
in range(len(sp_ob
.data
.splines
[0].bezier_points
)):
2893 bp
= sp_ob
.data
.splines
[0].bezier_points
[t
]
2899 dist
+= (last_p
- actual_p
).length
2901 if t
in points_U_crossed_by_strokes
:
2902 segments_distances
.append(dist
)
2909 # Calculate Proportions.
2910 used_edges_proportions_V
= []
2911 for t
in range(len(segments_distances
)):
2912 if self
.selection_V_exists
:
2914 order_number_last_stroke
= 0
2916 segment_edges_length_V
= 0
2917 segment_edges_length_V2
= 0
2918 for order
in range(order_number_last_stroke
, edge_order_number_for_splines
[t
+ 1]):
2919 segment_edges_length_V
+= edges_lengths_V
[order
]
2920 if self
.selection_V2_exists
:
2921 segment_edges_length_V2
+= edges_lengths_V2
[order
]
2923 for order
in range(order_number_last_stroke
, edge_order_number_for_splines
[t
+ 1]):
2924 # Calculate each "sub-segment" (the ones between each stroke) length
2925 if self
.selection_V2_exists
:
2926 proportion_sub_seg
= (edges_lengths_V2
[order
] -
2927 ((edges_lengths_V2
[order
] - edges_lengths_V
[order
]) /
2928 len(splines_U_objects
) * i
)) / (segment_edges_length_V2
-
2929 (segment_edges_length_V2
- segment_edges_length_V
) /
2930 len(splines_U_objects
) * i
)
2932 sub_seg_dist
= segments_distances
[t
] * proportion_sub_seg
2934 proportion_sub_seg
= edges_lengths_V
[order
] / segment_edges_length_V
2935 sub_seg_dist
= segments_distances
[t
] * proportion_sub_seg
2937 used_edges_proportions_V
.append(sub_seg_dist
/ full_dist
)
2939 order_number_last_stroke
= edge_order_number_for_splines
[t
+ 1]
2942 for _c
in range(self
.edges_V
):
2943 # Calculate each "sub-segment" (the ones between each stroke) length
2944 sub_seg_dist
= segments_distances
[t
] / self
.edges_V
2945 used_edges_proportions_V
.append(sub_seg_dist
/ full_dist
)
2947 actual_spline
= self
.distribute_pts(sp_ob
.data
.splines
, used_edges_proportions_V
)
2948 surface_splines_parsed
.append(actual_spline
[0])
2951 if self
.selection_V2_exists
:
2952 used_edges_proportions_V
= []
2953 for p
in range(len(edges_proportions_V
)):
2954 used_edges_proportions_V
.append(
2955 edges_proportions_V2
[p
] -
2956 ((edges_proportions_V2
[p
] -
2957 edges_proportions_V
[p
]) / len(splines_U_objects
) * i
)
2960 used_edges_proportions_V
= edges_proportions_V
2962 actual_spline
= self
.distribute_pts(sp_ob
.data
.splines
, used_edges_proportions_V
)
2963 surface_splines_parsed
.append(actual_spline
[0])
2965 # Set the verts of the first and last splines to the locations
2966 # of the respective verts in the selections
2967 if self
.selection_V_exists
:
2968 for i
in range(0, len(surface_splines_parsed
[0])):
2969 surface_splines_parsed
[len(surface_splines_parsed
) - 1][i
] = \
2970 self
.main_object
.matrix_world
@ verts_ordered_V
[i
].co
2972 if selection_type
== "TWO_NOT_CONNECTED":
2973 if self
.selection_V2_exists
:
2974 for i
in range(0, len(surface_splines_parsed
[0])):
2975 surface_splines_parsed
[0][i
] = self
.main_object
.matrix_world
@ verts_ordered_V2
[i
].co
2977 # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
2978 # merge the verts of the tips of the loops when they are "near enough"
2979 if self
.automatic_join
and selection_type
!= "TWO_CONNECTED":
2980 # Join the tips of "Follow" loops that are near enough and must be "closed"
2981 if not self
.selection_V_exists
and len(edges_proportions_U
) >= 3:
2982 for i
in range(len(surface_splines_parsed
[0])):
2983 sp
= surface_splines_parsed
2984 loop_segment_dist
= (sp
[0][i
] - sp
[1][i
]).length
2986 verts_middle_position_co
= [
2987 (sp
[0][i
][0] + sp
[len(sp
) - 1][i
][0]) / 2,
2988 (sp
[0][i
][1] + sp
[len(sp
) - 1][i
][1]) / 2,
2989 (sp
[0][i
][2] + sp
[len(sp
) - 1][i
][2]) / 2
2991 points_original
= []
2992 points_original
.append(sp
[1][i
])
2993 points_original
.append(sp
[0][i
])
2996 points_target
.append(sp
[1][i
])
2997 points_target
.append(Vector(verts_middle_position_co
))
2999 vec_A
= points_original
[0] - points_original
[1]
3000 vec_B
= points_target
[0] - points_target
[1]
3001 # check for zero angles, not sure if it is a great fix
3002 if vec_A
.length
!= 0 and vec_B
.length
!= 0:
3003 angle
= vec_A
.angle(vec_B
) / pi
3004 edge_new_length
= (Vector(verts_middle_position_co
) - sp
[1][i
]).length
3009 # If after moving the verts to the middle point, the segment doesn't stretch too much
3010 if edge_new_length
<= loop_segment_dist
* 1.5 * \
3011 self
.join_stretch_factor
and angle
< 0.25 * self
.join_stretch_factor
:
3013 # Avoid joining when the actual loop must be merged with the original mesh
3014 if not (self
.selection_U_exists
and i
== 0) and \
3015 not (self
.selection_U2_exists
and i
== len(surface_splines_parsed
[0]) - 1):
3017 # Change the coords of both verts to the middle position
3018 surface_splines_parsed
[0][i
] = verts_middle_position_co
3019 surface_splines_parsed
[len(surface_splines_parsed
) - 1][i
] = verts_middle_position_co
3021 # Delete object with control points and object from grease pencil conversion
3022 bpy
.ops
.object.delete({"selected_objects": [ob_ctrl_pts
]})
3024 bpy
.ops
.object.delete({"selected_objects": splines_U_objects
})
3028 # Get all verts coords
3029 all_surface_verts_co
= []
3030 for i
in range(0, len(surface_splines_parsed
)):
3031 # Get coords of all verts and make a list with them
3032 for pt_co
in surface_splines_parsed
[i
]:
3033 all_surface_verts_co
.append(pt_co
)
3035 # Define verts for each face
3036 all_surface_faces
= []
3037 for i
in range(0, len(all_surface_verts_co
) - len(surface_splines_parsed
[0])):
3038 if ((i
+ 1) / len(surface_splines_parsed
[0]) != int((i
+ 1) / len(surface_splines_parsed
[0]))):
3039 all_surface_faces
.append(
3040 [i
+ 1, i
, i
+ len(surface_splines_parsed
[0]),
3041 i
+ len(surface_splines_parsed
[0]) + 1]
3044 surf_me_name
= "SURFSKIO_surface"
3045 me_surf
= bpy
.data
.meshes
.new(surf_me_name
)
3046 me_surf
.from_pydata(all_surface_verts_co
, [], all_surface_faces
)
3047 ob_surface
= object_utils
.object_data_add(context
, me_surf
)
3048 ob_surface
.location
= (0.0, 0.0, 0.0)
3049 ob_surface
.rotation_euler
= (0.0, 0.0, 0.0)
3050 ob_surface
.scale
= (1.0, 1.0, 1.0)
3052 # Select all the "unselected but participating" verts, from closed selection
3053 # or double selections with middle-vertex, for later join with remove doubles
3054 for v_idx
in single_unselected_verts
:
3055 self
.main_object
.data
.vertices
[v_idx
].select
= True
3057 # Join the new mesh to the main object
3058 ob_surface
.select_set(True)
3059 self
.main_object
.select_set(True)
3060 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3062 bpy
.ops
.object.join('INVOKE_REGION_WIN')
3064 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3066 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN', threshold
=0.0001)
3067 bpy
.ops
.mesh
.normals_make_consistent('INVOKE_REGION_WIN', inside
=False)
3068 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3076 global global_offset
3077 shrinkwrap
= self
.main_object
.modifiers
["Shrinkwrap"]
3078 shrinkwrap
.offset
= global_offset
3079 bpy
.context
.scene
.bsurfaces
.SURFSK_Shrinkwrap_offset
= global_offset
3085 material
= makeMaterial("BSurfaceMesh", global_color
)
3086 if self
.main_object
.data
.materials
:
3087 self
.main_object
.data
.materials
[0] = material
3089 self
.main_object
.data
.materials
.append(material
)
3090 bpy
.context
.scene
.bsurfaces
.SURFSK_mesh_color
= global_color
3095 global global_in_front
3096 self
.main_object
.show_in_front
= global_in_front
3097 bpy
.context
.scene
.bsurfaces
.SURFSK_in_front
= global_in_front
3102 global global_show_wire
3103 self
.main_object
.show_wire
= global_show_wire
3104 bpy
.context
.scene
.bsurfaces
.SURFSK_show_wire
= global_show_wire
3109 global global_shade_smooth
3110 if global_shade_smooth
:
3111 bpy
.ops
.object.shade_smooth()
3113 bpy
.ops
.object.shade_flat()
3114 bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
= global_shade_smooth
3120 def execute(self
, context
):
3122 if bpy
.ops
.object.mode_set
.poll():
3123 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3126 global global_mesh_object
3127 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
3128 bpy
.data
.objects
[global_mesh_object
].select_set(True)
3129 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
3130 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3131 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3133 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3135 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3139 if not self
.is_fill_faces
:
3140 bpy
.ops
.wm
.context_set_value(data_path
='tool_settings.mesh_select_mode',
3141 value
='True, False, False')
3143 # Build splines from the "last saved splines".
3144 last_saved_curve
= bpy
.data
.curves
.new('SURFSKIO_last_crv', 'CURVE')
3145 self
.main_splines
= bpy
.data
.objects
.new('SURFSKIO_last_crv', last_saved_curve
)
3146 bpy
.context
.collection
.objects
.link(self
.main_splines
)
3148 last_saved_curve
.dimensions
= "3D"
3150 for sp
in self
.last_strokes_splines_coords
:
3151 spline
= self
.main_splines
.data
.splines
.new('BEZIER')
3152 # less one because one point is added when the spline is created
3153 spline
.bezier_points
.add(len(sp
) - 1)
3154 for p
in range(0, len(sp
)):
3155 spline
.bezier_points
[p
].co
= [sp
[p
][0], sp
[p
][1], sp
[p
][2]]
3157 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3159 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3160 self
.main_splines
.select_set(True)
3161 bpy
.context
.view_layer
.objects
.active
= self
.main_splines
3163 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3165 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
3166 # Important to make it vector first and then automatic, otherwise the
3167 # tips handles get too big and distort the shrinkwrap results later
3168 bpy
.ops
.curve
.handle_type_set(type='VECTOR')
3169 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
3170 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3171 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3173 self
.main_splines
.name
= "SURFSKIO_temp_strokes"
3175 if self
.is_crosshatch
:
3176 strokes_for_crosshatch
= True
3177 strokes_for_rectangular_surface
= False
3179 strokes_for_rectangular_surface
= True
3180 strokes_for_crosshatch
= False
3182 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3184 if strokes_for_rectangular_surface
:
3185 self
.rectangular_surface(context
)
3186 elif strokes_for_crosshatch
:
3187 self
.crosshatch_surface_execute(context
)
3189 #Set Shade smooth to new polygons
3190 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3191 global global_shade_smooth
3192 if global_shade_smooth
:
3193 bpy
.ops
.object.shade_smooth()
3195 bpy
.ops
.object.shade_flat()
3197 # Delete main splines
3198 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3199 if self
.keep_strokes
:
3200 self
.main_splines
.name
= "keep_strokes"
3201 self
.main_splines
.data
.bevel_depth
= 0.001
3202 if "keep_strokes_material" in bpy
.data
.materials
:
3203 self
.main_splines
.data
.materials
.append(bpy
.data
.materials
["keep_strokes_material"])
3205 mat
= bpy
.data
.materials
.new("keep_strokes_material")
3206 mat
.diffuse_color
= (1, 0, 0, 0)
3207 mat
.specular_color
= (1, 0, 0)
3208 mat
.specular_intensity
= 0.0
3210 self
.main_splines
.data
.materials
.append(mat
)
3212 bpy
.ops
.object.delete({"selected_objects": [self
.main_splines
]})
3214 # Delete grease pencil strokes
3215 if self
.strokes_type
== "GP_STROKES" and not self
.stopping_errors
:
3217 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3221 # Delete annotations
3222 if self
.strokes_type
== "GP_ANNOTATION" and not self
.stopping_errors
:
3224 bpy
.context
.annotation_data
.layers
.active
.clear()
3228 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3229 bsurfaces_props
.SURFSK_edges_U
= self
.edges_U
3230 bsurfaces_props
.SURFSK_edges_V
= self
.edges_V
3231 bsurfaces_props
.SURFSK_cyclic_cross
= self
.cyclic_cross
3232 bsurfaces_props
.SURFSK_cyclic_follow
= self
.cyclic_follow
3233 bsurfaces_props
.SURFSK_automatic_join
= self
.automatic_join
3234 bsurfaces_props
.SURFSK_loops_on_strokes
= self
.loops_on_strokes
3235 bsurfaces_props
.SURFSK_keep_strokes
= self
.keep_strokes
3237 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3238 self
.main_object
.select_set(True)
3239 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3241 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3247 def invoke(self
, context
, event
):
3249 if bpy
.ops
.object.mode_set
.poll():
3250 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3252 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3253 self
.cyclic_cross
= bsurfaces_props
.SURFSK_cyclic_cross
3254 self
.cyclic_follow
= bsurfaces_props
.SURFSK_cyclic_follow
3255 self
.automatic_join
= bsurfaces_props
.SURFSK_automatic_join
3256 self
.loops_on_strokes
= bsurfaces_props
.SURFSK_loops_on_strokes
3257 self
.keep_strokes
= bsurfaces_props
.SURFSK_keep_strokes
3260 global global_mesh_object
3261 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
3262 bpy
.data
.objects
[global_mesh_object
].select_set(True)
3263 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
3264 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3266 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3271 self
.main_object_selected_verts_count
= len([v
for v
in self
.main_object
.data
.vertices
if v
.select
])
3273 bpy
.ops
.wm
.context_set_value(data_path
='tool_settings.mesh_select_mode',
3274 value
='True, False, False')
3276 self
.edges_U
= bsurfaces_props
.SURFSK_edges_U
3277 self
.edges_V
= bsurfaces_props
.SURFSK_edges_V
3279 self
.is_fill_faces
= False
3280 self
.stopping_errors
= False
3281 self
.last_strokes_splines_coords
= []
3283 # Determine the type of the strokes
3284 self
.strokes_type
= get_strokes_type(context
)
3286 # Check if it will be used grease pencil strokes or curves
3287 # If there are strokes to be used
3288 if self
.strokes_type
== "GP_STROKES" or self
.strokes_type
== "EXTERNAL_CURVE" or self
.strokes_type
== "GP_ANNOTATION":
3289 if self
.strokes_type
== "GP_STROKES":
3290 # Convert grease pencil strokes to curve
3291 global global_gpencil_object
3292 gp
= bpy
.data
.objects
[global_gpencil_object
]
3293 self
.original_curve
= conver_gpencil_to_curve(self
, context
, gp
, 'GPensil')
3294 self
.using_external_curves
= False
3296 elif self
.strokes_type
== "GP_ANNOTATION":
3297 # Convert grease pencil strokes to curve
3298 gp
= bpy
.context
.annotation_data
3299 self
.original_curve
= conver_gpencil_to_curve(self
, context
, gp
, 'Annotation')
3300 self
.using_external_curves
= False
3302 elif self
.strokes_type
== "EXTERNAL_CURVE":
3303 global global_curve_object
3304 self
.original_curve
= bpy
.data
.objects
[global_curve_object
]
3305 self
.using_external_curves
= True
3307 # Make sure there are no objects left from erroneous
3308 # executions of this operator, with the reserved names used here
3309 for o
in bpy
.data
.objects
:
3310 if o
.name
.find("SURFSKIO_") != -1:
3311 bpy
.ops
.object.delete({"selected_objects": [o
]})
3313 bpy
.context
.view_layer
.objects
.active
= self
.original_curve
3315 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3317 self
.temporary_curve
= bpy
.context
.view_layer
.objects
.active
3319 # Deselect all points of the curve
3320 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3321 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3322 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3324 # Delete splines with only a single isolated point
3325 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3326 sp
= self
.temporary_curve
.data
.splines
[i
]
3328 if len(sp
.bezier_points
) == 1:
3329 sp
.bezier_points
[0].select_control_point
= True
3331 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3332 bpy
.ops
.curve
.delete(type='VERT')
3333 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3335 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3336 self
.temporary_curve
.select_set(True)
3337 bpy
.context
.view_layer
.objects
.active
= self
.temporary_curve
3339 # Set a minimum number of points for crosshatch
3340 minimum_points_num
= 15
3342 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3343 # Check if the number of points of each curve has at least the number of points
3344 # of minimum_points_num, which is a bit more than the face-loops limit.
3345 # If not, subdivide to reach at least that number of points
3346 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3347 sp
= self
.temporary_curve
.data
.splines
[i
]
3349 if len(sp
.bezier_points
) < minimum_points_num
:
3350 for bp
in sp
.bezier_points
:
3351 bp
.select_control_point
= True
3353 if (len(sp
.bezier_points
) - 1) != 0:
3354 # Formula to get the number of cuts that will make a curve
3355 # of N number of points have near to "minimum_points_num"
3356 # points, when subdividing with this number of cuts
3357 subdivide_cuts
= int(
3358 (minimum_points_num
- len(sp
.bezier_points
)) /
3359 (len(sp
.bezier_points
) - 1)
3364 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3365 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3367 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3369 # Detect if the strokes are a crosshatch and do it if it is
3370 self
.crosshatch_surface_invoke(self
.temporary_curve
)
3372 if not self
.is_crosshatch
:
3373 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3374 self
.temporary_curve
.select_set(True)
3375 bpy
.context
.view_layer
.objects
.active
= self
.temporary_curve
3377 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3379 # Set a minimum number of points for rectangular surfaces
3380 minimum_points_num
= 60
3382 # Check if the number of points of each curve has at least the number of points
3383 # of minimum_points_num, which is a bit more than the face-loops limit.
3384 # If not, subdivide to reach at least that number of points
3385 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3386 sp
= self
.temporary_curve
.data
.splines
[i
]
3388 if len(sp
.bezier_points
) < minimum_points_num
:
3389 for bp
in sp
.bezier_points
:
3390 bp
.select_control_point
= True
3392 if (len(sp
.bezier_points
) - 1) != 0:
3393 # Formula to get the number of cuts that will make a curve of
3394 # N number of points have near to "minimum_points_num" points,
3395 # when subdividing with this number of cuts
3396 subdivide_cuts
= int(
3397 (minimum_points_num
- len(sp
.bezier_points
)) /
3398 (len(sp
.bezier_points
) - 1)
3403 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3404 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3406 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3408 # Save coordinates of the actual strokes (as the "last saved splines")
3409 for sp_idx
in range(len(self
.temporary_curve
.data
.splines
)):
3410 self
.last_strokes_splines_coords
.append([])
3411 for bp_idx
in range(len(self
.temporary_curve
.data
.splines
[sp_idx
].bezier_points
)):
3412 coords
= self
.temporary_curve
.matrix_world
@ \
3413 self
.temporary_curve
.data
.splines
[sp_idx
].bezier_points
[bp_idx
].co
3414 self
.last_strokes_splines_coords
[sp_idx
].append([coords
[0], coords
[1], coords
[2]])
3416 # Check for cyclic splines, put the first and last points in the middle of their actual positions
3417 for sp_idx
in range(len(self
.temporary_curve
.data
.splines
)):
3418 if self
.temporary_curve
.data
.splines
[sp_idx
].use_cyclic_u
is True:
3419 first_p_co
= self
.last_strokes_splines_coords
[sp_idx
][0]
3420 last_p_co
= self
.last_strokes_splines_coords
[sp_idx
][
3421 len(self
.last_strokes_splines_coords
[sp_idx
]) - 1
3424 (first_p_co
[0] + last_p_co
[0]) / 2,
3425 (first_p_co
[1] + last_p_co
[1]) / 2,
3426 (first_p_co
[2] + last_p_co
[2]) / 2
3429 self
.last_strokes_splines_coords
[sp_idx
][0] = target_co
3430 self
.last_strokes_splines_coords
[sp_idx
][
3431 len(self
.last_strokes_splines_coords
[sp_idx
]) - 1
3433 tuple(self
.last_strokes_splines_coords
)
3435 # Estimation of the average length of the segments between
3436 # each point of the grease pencil strokes.
3437 # Will be useful to determine whether a curve should be made "Cyclic"
3438 segments_lengths_sum
= 0
3440 random_spline
= self
.temporary_curve
.data
.splines
[0].bezier_points
3441 for i
in range(0, len(random_spline
)):
3442 if i
!= 0 and len(random_spline
) - 1 >= i
:
3443 segments_lengths_sum
+= (random_spline
[i
- 1].co
- random_spline
[i
].co
).length
3446 self
.average_gp_segment_length
= segments_lengths_sum
/ segments_count
3448 # Delete temporary strokes curve object
3449 bpy
.ops
.object.delete({"selected_objects": [self
.temporary_curve
]})
3451 # Set again since "execute()" will turn it again to its initial value
3452 self
.execute(context
)
3454 if not self
.stopping_errors
:
3455 # Delete grease pencil strokes
3456 if self
.strokes_type
== "GP_STROKES":
3458 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3462 # Delete annotation strokes
3463 elif self
.strokes_type
== "GP_ANNOTATION":
3465 bpy
.context
.annotation_data
.layers
.active
.clear()
3469 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3470 bpy
.ops
.object.delete({"selected_objects": [self
.original_curve
]})
3471 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3477 elif self
.strokes_type
== "SELECTION_ALONE":
3478 self
.is_fill_faces
= True
3479 created_faces_count
= self
.fill_with_faces(self
.main_object
)
3481 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3483 if created_faces_count
== 0:
3484 self
.report({'WARNING'}, "There aren't any strokes attached to the object")
3485 return {"CANCELLED"}
3489 if self
.strokes_type
== "EXTERNAL_NO_CURVE":
3490 self
.report({'WARNING'}, "The secondary object is not a Curve.")
3493 elif self
.strokes_type
== "MORE_THAN_ONE_EXTERNAL":
3494 self
.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3497 elif self
.strokes_type
== "SINGLE_GP_STROKE_NO_SELECTION" or \
3498 self
.strokes_type
== "SINGLE_CURVE_STROKE_NO_SELECTION":
3500 self
.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
3503 elif self
.strokes_type
== "NO_STROKES":
3504 self
.report({'WARNING'}, "There aren't any strokes attached to the object")
3507 elif self
.strokes_type
== "CURVE_WITH_NON_BEZIER_SPLINES":
3508 self
.report({'WARNING'}, "All splines must be Bezier.")
3514 # ----------------------------
3516 class MESH_OT_SURFSK_init(Operator
):
3517 bl_idname
= "mesh.surfsk_init"
3518 bl_label
= "Bsurfaces initialize"
3519 bl_description
= "Add an empty mesh object with useful settings"
3520 bl_options
= {'REGISTER', 'UNDO'}
3522 def execute(self
, context
):
3524 bs
= bpy
.context
.scene
.bsurfaces
3526 if bpy
.ops
.object.mode_set
.poll():
3527 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3530 global global_offset
3531 global global_in_front
3532 global global_show_wire
3533 global global_shade_smooth
3534 global global_mesh_object
3535 global global_gpencil_object
3537 if bs
.SURFSK_mesh
== None:
3538 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3539 mesh
= bpy
.data
.meshes
.new('BSurfaceMesh')
3540 mesh_object
= object_utils
.object_data_add(context
, mesh
)
3541 mesh_object
.select_set(True)
3542 bpy
.context
.view_layer
.objects
.active
= mesh_object
3544 mesh_object
.show_all_edges
= True
3545 global_in_front
= bpy
.context
.scene
.bsurfaces
.SURFSK_in_front
3546 mesh_object
.show_in_front
= global_in_front
3547 mesh_object
.display_type
= 'SOLID'
3548 mesh_object
.show_wire
= True
3550 global_shade_smooth
= bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
3551 if global_shade_smooth
:
3552 bpy
.ops
.object.shade_smooth()
3554 bpy
.ops
.object.shade_flat()
3556 global_show_wire
= bpy
.context
.scene
.bsurfaces
.SURFSK_show_wire
3557 mesh_object
.show_wire
= global_show_wire
3559 global_color
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh_color
3560 material
= makeMaterial("BSurfaceMesh", global_color
)
3561 mesh_object
.data
.materials
.append(material
)
3562 bpy
.ops
.object.modifier_add(type='SHRINKWRAP')
3563 modifier
= mesh_object
.modifiers
["Shrinkwrap"]
3564 if self
.active_object
is not None:
3565 modifier
.target
= self
.active_object
3566 modifier
.wrap_method
= 'TARGET_PROJECT'
3567 modifier
.wrap_mode
= 'OUTSIDE_SURFACE'
3568 modifier
.show_on_cage
= True
3569 global_offset
= bpy
.context
.scene
.bsurfaces
.SURFSK_Shrinkwrap_offset
3570 modifier
.offset
= global_offset
3572 global_mesh_object
= mesh_object
.name
3573 bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
= bpy
.data
.objects
[global_mesh_object
]
3575 bpy
.context
.scene
.tool_settings
.snap_elements
= {'FACE'}
3576 bpy
.context
.scene
.tool_settings
.use_snap
= True
3577 bpy
.context
.scene
.tool_settings
.use_snap_self
= False
3578 bpy
.context
.scene
.tool_settings
.use_snap_align_rotation
= True
3579 bpy
.context
.scene
.tool_settings
.use_snap_project
= True
3580 bpy
.context
.scene
.tool_settings
.use_snap_rotate
= True
3581 bpy
.context
.scene
.tool_settings
.use_snap_scale
= True
3583 bpy
.context
.scene
.tool_settings
.use_mesh_automerge
= True
3584 bpy
.context
.scene
.tool_settings
.double_threshold
= 0.01
3586 if context
.scene
.bsurfaces
.SURFSK_guide
== 'GPencil' and bs
.SURFSK_gpencil
== None:
3587 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3588 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')
3589 bpy
.context
.scene
.tool_settings
.gpencil_stroke_placement_view3d
= 'SURFACE'
3590 gpencil_object
= bpy
.context
.scene
.objects
[bpy
.context
.scene
.objects
[-1].name
]
3591 gpencil_object
.select_set(True)
3592 bpy
.context
.view_layer
.objects
.active
= gpencil_object
3593 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3594 global_gpencil_object
= gpencil_object
.name
3595 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
= bpy
.data
.objects
[global_gpencil_object
]
3596 gpencil_object
.data
.stroke_depth_order
= '3D'
3597 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3598 bpy
.ops
.wm
.tool_set_by_id(name
="builtin_brush.Draw")
3600 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Annotation':
3601 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.annotate")
3602 bpy
.context
.scene
.tool_settings
.annotation_stroke_placement_view3d
= 'SURFACE'
3604 def invoke(self
, context
, event
):
3605 if bpy
.context
.active_object
:
3606 self
.active_object
= bpy
.context
.active_object
3608 self
.active_object
= None
3610 self
.execute(context
)
3614 # ----------------------------
3615 # Add modifiers operator
3616 class MESH_OT_SURFSK_add_modifiers(Operator
):
3617 bl_idname
= "mesh.surfsk_add_modifiers"
3618 bl_label
= "Add Mirror and others modifiers"
3619 bl_description
= "Add modifiers: Mirror, Shrinkwrap, Subdivision, Solidify"
3620 bl_options
= {'REGISTER', 'UNDO'}
3622 def execute(self
, context
):
3624 bs
= bpy
.context
.scene
.bsurfaces
3626 if bpy
.ops
.object.mode_set
.poll():
3627 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3629 if bs
.SURFSK_mesh
== None:
3630 self
.report({'ERROR_INVALID_INPUT'}, "Please select Mesh of BSurface or click Initialize")
3632 mesh_object
= bs
.SURFSK_mesh
3635 mesh_object
.select_set(True)
3637 self
.report({'ERROR_INVALID_INPUT'}, "Mesh of BSurface does not exist")
3640 bpy
.context
.view_layer
.objects
.active
= mesh_object
3643 shrinkwrap
= mesh_object
.modifiers
["Shrinkwrap"]
3644 if self
.active_object
is not None and self
.active_object
!= mesh_object
:
3645 shrinkwrap
.target
= self
.active_object
3646 shrinkwrap
.wrap_method
= 'TARGET_PROJECT'
3647 shrinkwrap
.wrap_mode
= 'OUTSIDE_SURFACE'
3648 shrinkwrap
.show_on_cage
= True
3649 shrinkwrap
.offset
= bpy
.context
.scene
.bsurfaces
.SURFSK_Shrinkwrap_offset
3651 bpy
.ops
.object.modifier_add(type='SHRINKWRAP')
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
3661 mirror
= mesh_object
.modifiers
["Mirror"]
3662 mirror
.use_clip
= True
3664 bpy
.ops
.object.modifier_add(type='MIRROR')
3665 mirror
= mesh_object
.modifiers
["Mirror"]
3666 mirror
.use_clip
= True
3669 _subsurf
= mesh_object
.modifiers
["Subdivision"]
3671 bpy
.ops
.object.modifier_add(type='SUBSURF')
3672 _subsurf
= mesh_object
.modifiers
["Subdivision"]
3675 solidify
= mesh_object
.modifiers
["Solidify"]
3676 solidify
.thickness
= 0.01
3678 bpy
.ops
.object.modifier_add(type='SOLIDIFY')
3679 solidify
= mesh_object
.modifiers
["Solidify"]
3680 solidify
.thickness
= 0.01
3684 def invoke(self
, context
, event
):
3685 if bpy
.context
.active_object
:
3686 self
.active_object
= bpy
.context
.active_object
3688 self
.active_object
= None
3690 self
.execute(context
)
3694 # ----------------------------
3695 # Edit surface operator
3696 class MESH_OT_SURFSK_edit_surface(Operator
):
3697 bl_idname
= "mesh.surfsk_edit_surface"
3698 bl_label
= "Bsurfaces edit surface"
3699 bl_description
= "Edit surface mesh"
3700 bl_options
= {'REGISTER', 'UNDO'}
3702 def execute(self
, context
):
3703 if bpy
.ops
.object.mode_set
.poll():
3704 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3705 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3706 bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.select_set(True)
3707 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
3708 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3709 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select")
3711 def invoke(self
, context
, event
):
3713 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
3714 bpy
.data
.objects
[global_mesh_object
].select_set(True)
3715 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
3716 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3718 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3721 self
.execute(context
)
3725 # ----------------------------
3726 # Add strokes operator
3727 class GPENCIL_OT_SURFSK_add_strokes(Operator
):
3728 bl_idname
= "gpencil.surfsk_add_strokes"
3729 bl_label
= "Bsurfaces add strokes"
3730 bl_description
= "Add the grease pencil strokes"
3731 bl_options
= {'REGISTER', 'UNDO'}
3733 def execute(self
, context
):
3734 if bpy
.ops
.object.mode_set
.poll():
3735 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3736 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3738 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3739 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3740 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3741 bpy
.ops
.wm
.tool_set_by_id(name
="builtin_brush.Draw")
3745 def invoke(self
, context
, event
):
3747 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3749 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3752 self
.execute(context
)
3756 # ----------------------------
3757 # Edit strokes operator
3758 class GPENCIL_OT_SURFSK_edit_strokes(Operator
):
3759 bl_idname
= "gpencil.surfsk_edit_strokes"
3760 bl_label
= "Bsurfaces edit strokes"
3761 bl_description
= "Edit the grease pencil strokes"
3762 bl_options
= {'REGISTER', 'UNDO'}
3764 def execute(self
, context
):
3765 if bpy
.ops
.object.mode_set
.poll():
3766 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3767 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3769 gpencil_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3771 gpencil_object
.select_set(True)
3772 bpy
.context
.view_layer
.objects
.active
= gpencil_object
3774 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT_GPENCIL')
3776 bpy
.ops
.gpencil
.select_all(action
='SELECT')
3780 def invoke(self
, context
, event
):
3782 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3784 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3787 self
.execute(context
)
3791 # ----------------------------
3792 # Convert annotation to curves operator
3793 class GPENCIL_OT_SURFSK_annotation_to_curves(Operator
):
3794 bl_idname
= "gpencil.surfsk_annotations_to_curves"
3795 bl_label
= "Convert annotation to curves"
3796 bl_description
= "Convert annotation to curves for editing"
3797 bl_options
= {'REGISTER', 'UNDO'}
3799 def execute(self
, context
):
3801 if bpy
.ops
.object.mode_set
.poll():
3802 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3804 # Convert annotation to curve
3805 curve
= conver_gpencil_to_curve(self
, context
, None, 'Annotation')
3808 # Delete annotation strokes
3810 bpy
.context
.annotation_data
.layers
.active
.clear()
3815 curve
.select_set(True)
3816 bpy
.context
.view_layer
.objects
.active
= curve
3818 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select_box")
3822 def invoke(self
, context
, event
):
3824 strokes
= bpy
.context
.annotation_data
.layers
.active
.active_frame
.strokes
3826 _strokes_num
= len(strokes
)
3828 self
.report({'WARNING'}, "Not active annotation")
3831 self
.execute(context
)
3835 # ----------------------------
3836 # Convert strokes to curves operator
3837 class GPENCIL_OT_SURFSK_strokes_to_curves(Operator
):
3838 bl_idname
= "gpencil.surfsk_strokes_to_curves"
3839 bl_label
= "Convert strokes to curves"
3840 bl_description
= "Convert grease pencil strokes to curves for editing"
3841 bl_options
= {'REGISTER', 'UNDO'}
3843 def execute(self
, context
):
3845 if bpy
.ops
.object.mode_set
.poll():
3846 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3848 # Convert grease pencil strokes to curve
3849 gp
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3850 curve
= conver_gpencil_to_curve(self
, context
, gp
, 'GPensil')
3853 # Delete grease pencil strokes
3855 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3861 curve
.select_set(True)
3862 bpy
.context
.view_layer
.objects
.active
= curve
3864 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select_box")
3868 def invoke(self
, context
, event
):
3870 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3872 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3875 self
.execute(context
)
3879 # ----------------------------
3881 class GPENCIL_OT_SURFSK_add_annotation(Operator
):
3882 bl_idname
= "gpencil.surfsk_add_annotation"
3883 bl_label
= "Bsurfaces add annotation"
3884 bl_description
= "Add annotation"
3885 bl_options
= {'REGISTER', 'UNDO'}
3887 def execute(self
, context
):
3888 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.annotate")
3889 bpy
.context
.scene
.tool_settings
.annotation_stroke_placement_view3d
= 'SURFACE'
3893 def invoke(self
, context
, event
):
3895 self
.execute(context
)
3900 # ----------------------------
3901 # Edit curve operator
3902 class CURVE_OT_SURFSK_edit_curve(Operator
):
3903 bl_idname
= "curve.surfsk_edit_curve"
3904 bl_label
= "Bsurfaces edit curve"
3905 bl_description
= "Edit curve"
3906 bl_options
= {'REGISTER', 'UNDO'}
3908 def execute(self
, context
):
3909 if bpy
.ops
.object.mode_set
.poll():
3910 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3911 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3912 bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.select_set(True)
3913 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_curve
3914 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3916 def invoke(self
, context
, event
):
3918 bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.select_set(True)
3920 self
.report({'WARNING'}, "Specify the name of the object with curve")
3923 self
.execute(context
)
3927 # ----------------------------
3929 class CURVE_OT_SURFSK_reorder_splines(Operator
):
3930 bl_idname
= "curve.surfsk_reorder_splines"
3931 bl_label
= "Bsurfaces reorder splines"
3932 bl_description
= "Defines the order of the splines by using grease pencil strokes"
3933 bl_options
= {'REGISTER', 'UNDO'}
3935 def execute(self
, context
):
3936 objects_to_delete
= []
3937 # Convert grease pencil strokes to curve.
3938 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3939 bpy
.ops
.gpencil
.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes
=False)
3940 for ob
in bpy
.context
.selected_objects
:
3941 if ob
!= bpy
.context
.view_layer
.objects
.active
and ob
.name
.startswith("GP_Layer"):
3942 GP_strokes_curve
= ob
3944 # GP_strokes_curve = bpy.context.object
3945 objects_to_delete
.append(GP_strokes_curve
)
3947 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3948 GP_strokes_curve
.select_set(True)
3949 bpy
.context
.view_layer
.objects
.active
= GP_strokes_curve
3951 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3952 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
3953 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=100)
3954 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3956 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3957 GP_strokes_mesh
= bpy
.context
.object
3958 objects_to_delete
.append(GP_strokes_mesh
)
3960 GP_strokes_mesh
.data
.resolution_u
= 1
3961 bpy
.ops
.object.convert(target
='MESH', keep_original
=False)
3963 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3964 self
.main_curve
.select_set(True)
3965 bpy
.context
.view_layer
.objects
.active
= self
.main_curve
3967 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3968 curves_duplicate_1
= bpy
.context
.object
3969 objects_to_delete
.append(curves_duplicate_1
)
3971 minimum_points_num
= 500
3973 # Some iterations since the subdivision operator
3974 # has a limit of 100 subdivisions per iteration
3975 for x
in range(round(minimum_points_num
/ 100)):
3976 # Check if the number of points of each curve has at least the number of points
3977 # of minimum_points_num. If not, subdivide to reach at least that number of points
3978 for i
in range(len(curves_duplicate_1
.data
.splines
)):
3979 sp
= curves_duplicate_1
.data
.splines
[i
]
3981 if len(sp
.bezier_points
) < minimum_points_num
:
3982 for bp
in sp
.bezier_points
:
3983 bp
.select_control_point
= True
3985 if (len(sp
.bezier_points
) - 1) != 0:
3986 # Formula to get the number of cuts that will make a curve of N
3987 # number of points have near to "minimum_points_num" points,
3988 # when subdividing with this number of cuts
3989 subdivide_cuts
= int(
3990 (minimum_points_num
- len(sp
.bezier_points
)) /
3991 (len(sp
.bezier_points
) - 1)
3996 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3997 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3998 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3999 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4001 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
4002 curves_duplicate_2
= bpy
.context
.object
4003 objects_to_delete
.append(curves_duplicate_2
)
4005 # Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target
4006 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4007 curves_duplicate_2
.select_set(True)
4008 bpy
.context
.view_layer
.objects
.active
= curves_duplicate_2
4010 bpy
.ops
.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
4011 curves_duplicate_2
.modifiers
["Shrinkwrap"].wrap_method
= "NEAREST_VERTEX"
4012 curves_duplicate_2
.modifiers
["Shrinkwrap"].target
= GP_strokes_mesh
4013 bpy
.ops
.object.modifier_apply('INVOKE_REGION_WIN', apply_as
='DATA', modifier
='Shrinkwrap')
4015 # Get the distance of each vert from its original position to its position with Shrinkwrap
4016 nearest_points_coords
= {}
4017 for st_idx
in range(len(curves_duplicate_1
.data
.splines
)):
4018 for bp_idx
in range(len(curves_duplicate_1
.data
.splines
[st_idx
].bezier_points
)):
4019 bp_1_co
= curves_duplicate_1
.matrix_world
@ \
4020 curves_duplicate_1
.data
.splines
[st_idx
].bezier_points
[bp_idx
].co
4022 bp_2_co
= curves_duplicate_2
.matrix_world
@ \
4023 curves_duplicate_2
.data
.splines
[st_idx
].bezier_points
[bp_idx
].co
4026 shortest_dist
= (bp_1_co
- bp_2_co
).length
4027 nearest_points_coords
[st_idx
] = ("%.4f" % bp_2_co
[0],
4028 "%.4f" % bp_2_co
[1],
4029 "%.4f" % bp_2_co
[2])
4031 dist
= (bp_1_co
- bp_2_co
).length
4033 if dist
< shortest_dist
:
4034 nearest_points_coords
[st_idx
] = ("%.4f" % bp_2_co
[0],
4035 "%.4f" % bp_2_co
[1],
4036 "%.4f" % bp_2_co
[2])
4037 shortest_dist
= dist
4039 # Get all coords of GP strokes points, for comparison
4040 GP_strokes_coords
= []
4041 for st_idx
in range(len(GP_strokes_curve
.data
.splines
)):
4042 GP_strokes_coords
.append(
4043 [("%.4f" % x
if "%.4f" % x
!= "-0.00" else "0.00",
4044 "%.4f" % y
if "%.4f" % y
!= "-0.00" else "0.00",
4045 "%.4f" % z
if "%.4f" % z
!= "-0.00" else "0.00") for
4046 x
, y
, z
in [bp
.co
for bp
in GP_strokes_curve
.data
.splines
[st_idx
].bezier_points
]]
4049 # Check the point of the GP strokes with the same coords as
4050 # the nearest points of the curves (with shrinkwrap)
4052 # Dictionary with GP stroke index as index, and a list as value.
4053 # The list has as index the point index of the GP stroke
4054 # nearest to the spline, and as value the spline index
4055 GP_connection_points
= {}
4056 for gp_st_idx
in range(len(GP_strokes_coords
)):
4057 GPvert_spline_relationship
= {}
4059 for splines_st_idx
in range(len(nearest_points_coords
)):
4060 if nearest_points_coords
[splines_st_idx
] in GP_strokes_coords
[gp_st_idx
]:
4061 GPvert_spline_relationship
[
4062 GP_strokes_coords
[gp_st_idx
].index(nearest_points_coords
[splines_st_idx
])
4065 GP_connection_points
[gp_st_idx
] = GPvert_spline_relationship
4067 # Get the splines new order
4068 splines_new_order
= []
4069 for i
in GP_connection_points
:
4070 dict_keys
= sorted(GP_connection_points
[i
].keys()) # Sort dictionaries by key
4073 splines_new_order
.append(GP_connection_points
[i
][k
])
4076 curve_original_name
= self
.main_curve
.name
4078 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4079 self
.main_curve
.select_set(True)
4080 bpy
.context
.view_layer
.objects
.active
= self
.main_curve
4082 self
.main_curve
.name
= "SURFSKIO_CRV_ORD"
4084 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4085 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4086 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4088 for _sp_idx
in range(len(self
.main_curve
.data
.splines
)):
4089 self
.main_curve
.data
.splines
[0].bezier_points
[0].select_control_point
= True
4091 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4092 bpy
.ops
.curve
.separate('EXEC_REGION_WIN')
4093 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4095 # Get the names of the separated splines objects in the original order
4096 splines_unordered
= {}
4097 for o
in bpy
.data
.objects
:
4098 if o
.name
.find("SURFSKIO_CRV_ORD") != -1:
4099 spline_order_string
= o
.name
.partition(".")[2]
4101 if spline_order_string
!= "" and int(spline_order_string
) > 0:
4102 spline_order_index
= int(spline_order_string
) - 1
4103 splines_unordered
[spline_order_index
] = o
.name
4105 # Join all splines objects in final order
4106 for order_idx
in splines_new_order
:
4107 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4108 bpy
.data
.objects
[splines_unordered
[order_idx
]].select_set(True)
4109 bpy
.data
.objects
["SURFSKIO_CRV_ORD"].select_set(True)
4110 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
["SURFSKIO_CRV_ORD"]
4112 bpy
.ops
.object.join('INVOKE_REGION_WIN')
4114 # Go back to the original name of the curves object.
4115 bpy
.context
.object.name
= curve_original_name
4117 # Delete all unused objects
4118 bpy
.ops
.object.delete({"selected_objects": objects_to_delete
})
4120 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4121 bpy
.data
.objects
[curve_original_name
].select_set(True)
4122 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[curve_original_name
]
4124 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4125 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4128 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
4135 def invoke(self
, context
, event
):
4136 self
.main_curve
= bpy
.context
.object
4137 there_are_GP_strokes
= False
4140 # Get the active grease pencil layer
4141 strokes_num
= len(self
.main_curve
.grease_pencil
.layers
.active
.active_frame
.strokes
)
4144 there_are_GP_strokes
= True
4148 if there_are_GP_strokes
:
4149 self
.execute(context
)
4150 self
.report({'INFO'}, "Splines have been reordered")
4152 self
.report({'WARNING'}, "Draw grease pencil strokes to connect splines")
4156 # ----------------------------
4157 # Set first points operator
4158 class CURVE_OT_SURFSK_first_points(Operator
):
4159 bl_idname
= "curve.surfsk_first_points"
4160 bl_label
= "Bsurfaces set first points"
4161 bl_description
= "Set the selected points as the first point of each spline"
4162 bl_options
= {'REGISTER', 'UNDO'}
4164 def execute(self
, context
):
4165 splines_to_invert
= []
4167 # Check non-cyclic splines to invert
4168 for i
in range(len(self
.main_curve
.data
.splines
)):
4169 b_points
= self
.main_curve
.data
.splines
[i
].bezier_points
4171 if i
not in self
.cyclic_splines
: # Only for non-cyclic splines
4172 if b_points
[len(b_points
) - 1].select_control_point
:
4173 splines_to_invert
.append(i
)
4175 # Reorder points of cyclic splines, and set all handles to "Automatic"
4177 # Check first selected point
4178 cyclic_splines_new_first_pt
= {}
4179 for i
in self
.cyclic_splines
:
4180 sp
= self
.main_curve
.data
.splines
[i
]
4182 for t
in range(len(sp
.bezier_points
)):
4183 bp
= sp
.bezier_points
[t
]
4184 if bp
.select_control_point
or bp
.select_right_handle
or bp
.select_left_handle
:
4185 cyclic_splines_new_first_pt
[i
] = t
4186 break # To take only one if there are more
4189 for spline_idx
in cyclic_splines_new_first_pt
:
4190 sp
= self
.main_curve
.data
.splines
[spline_idx
]
4192 spline_old_coords
= []
4193 for bp_old
in sp
.bezier_points
:
4194 coords
= (bp_old
.co
[0], bp_old
.co
[1], bp_old
.co
[2])
4196 left_handle_type
= str(bp_old
.handle_left_type
)
4197 left_handle_length
= float(bp_old
.handle_left
.length
)
4199 float(bp_old
.handle_left
.x
),
4200 float(bp_old
.handle_left
.y
),
4201 float(bp_old
.handle_left
.z
)
4203 right_handle_type
= str(bp_old
.handle_right_type
)
4204 right_handle_length
= float(bp_old
.handle_right
.length
)
4205 right_handle_xyz
= (
4206 float(bp_old
.handle_right
.x
),
4207 float(bp_old
.handle_right
.y
),
4208 float(bp_old
.handle_right
.z
)
4210 spline_old_coords
.append(
4211 [coords
, left_handle_type
,
4212 right_handle_type
, left_handle_length
,
4213 right_handle_length
, left_handle_xyz
,
4217 for t
in range(len(sp
.bezier_points
)):
4218 bp
= sp
.bezier_points
4220 if t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1 <= len(bp
) - 1:
4221 new_index
= t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1
4223 new_index
= t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1 - len(bp
)
4225 bp
[t
].co
= Vector(spline_old_coords
[new_index
][0])
4227 bp
[t
].handle_left
.length
= spline_old_coords
[new_index
][3]
4228 bp
[t
].handle_right
.length
= spline_old_coords
[new_index
][4]
4230 bp
[t
].handle_left_type
= "FREE"
4231 bp
[t
].handle_right_type
= "FREE"
4233 bp
[t
].handle_left
.x
= spline_old_coords
[new_index
][5][0]
4234 bp
[t
].handle_left
.y
= spline_old_coords
[new_index
][5][1]
4235 bp
[t
].handle_left
.z
= spline_old_coords
[new_index
][5][2]
4237 bp
[t
].handle_right
.x
= spline_old_coords
[new_index
][6][0]
4238 bp
[t
].handle_right
.y
= spline_old_coords
[new_index
][6][1]
4239 bp
[t
].handle_right
.z
= spline_old_coords
[new_index
][6][2]
4241 bp
[t
].handle_left_type
= spline_old_coords
[new_index
][1]
4242 bp
[t
].handle_right_type
= spline_old_coords
[new_index
][2]
4244 # Invert the non-cyclic splines designated above
4245 for i
in range(len(splines_to_invert
)):
4246 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4248 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4249 self
.main_curve
.data
.splines
[splines_to_invert
[i
]].bezier_points
[0].select_control_point
= True
4250 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4252 bpy
.ops
.curve
.switch_direction()
4254 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4256 # Keep selected the first vert of each spline
4257 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4258 for i
in range(len(self
.main_curve
.data
.splines
)):
4259 if not self
.main_curve
.data
.splines
[i
].use_cyclic_u
:
4260 bp
= self
.main_curve
.data
.splines
[i
].bezier_points
[0]
4262 bp
= self
.main_curve
.data
.splines
[i
].bezier_points
[
4263 len(self
.main_curve
.data
.splines
[i
].bezier_points
) - 1
4266 bp
.select_control_point
= True
4267 bp
.select_right_handle
= True
4268 bp
.select_left_handle
= True
4270 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4274 def invoke(self
, context
, event
):
4275 self
.main_curve
= bpy
.context
.object
4277 # Check if all curves are Bezier, and detect which ones are cyclic
4278 self
.cyclic_splines
= []
4279 for i
in range(len(self
.main_curve
.data
.splines
)):
4280 if self
.main_curve
.data
.splines
[i
].type != "BEZIER":
4281 self
.report({'WARNING'}, "All splines must be Bezier type")
4283 return {'CANCELLED'}
4285 if self
.main_curve
.data
.splines
[i
].use_cyclic_u
:
4286 self
.cyclic_splines
.append(i
)
4288 self
.execute(context
)
4289 self
.report({'INFO'}, "First points have been set")
4294 # Add-ons Preferences Update Panel
4296 # Define Panel classes for updating
4298 VIEW3D_PT_tools_SURFSK_mesh
,
4299 VIEW3D_PT_tools_SURFSK_curve
4303 def conver_gpencil_to_curve(self
, context
, pencil
, type):
4304 newCurve
= bpy
.data
.curves
.new(type + '_curve', type='CURVE')
4305 newCurve
.dimensions
= '3D'
4306 CurveObject
= object_utils
.object_data_add(context
, newCurve
)
4309 if type == 'GPensil':
4311 strokes
= pencil
.data
.layers
.active
.active_frame
.strokes
4314 CurveObject
.location
= pencil
.location
4315 CurveObject
.rotation_euler
= pencil
.rotation_euler
4316 CurveObject
.scale
= pencil
.scale
4317 elif type == 'Annotation':
4319 strokes
= bpy
.context
.annotation_data
.layers
.active
.active_frame
.strokes
4322 CurveObject
.location
= (0.0, 0.0, 0.0)
4323 CurveObject
.rotation_euler
= (0.0, 0.0, 0.0)
4324 CurveObject
.scale
= (1.0, 1.0, 1.0)
4327 for i
, _stroke
in enumerate(strokes
):
4328 stroke_points
= strokes
[i
].points
4329 data_list
= [ (point
.co
.x
, point
.co
.y
, point
.co
.z
)
4330 for point
in stroke_points
]
4331 points_to_add
= len(data_list
)-1
4334 for point
in data_list
:
4335 flat_list
.extend(point
)
4337 spline
= newCurve
.splines
.new(type='BEZIER')
4338 spline
.bezier_points
.add(points_to_add
)
4339 spline
.bezier_points
.foreach_set("co", flat_list
)
4341 for point
in spline
.bezier_points
:
4342 point
.handle_left_type
="AUTO"
4343 point
.handle_right_type
="AUTO"
4350 def update_panel(self
, context
):
4351 message
= "Bsurfaces GPL Edition: Updating Panel locations has failed"
4353 for panel
in panels
:
4354 if "bl_rna" in panel
.__dict
__:
4355 bpy
.utils
.unregister_class(panel
)
4357 for panel
in panels
:
4358 category
= context
.preferences
.addons
[__name__
].preferences
.category
4359 if category
!= 'Tool':
4360 panel
.bl_category
= context
.preferences
.addons
[__name__
].preferences
.category
4362 context
.preferences
.addons
[__name__
].preferences
.category
= 'Edit'
4363 panel
.bl_category
= 'Edit'
4364 raise ValueError("You can not install add-ons in the Tool panel")
4365 bpy
.utils
.register_class(panel
)
4367 except Exception as e
:
4368 print("\n[{}]\n{}\n\nError:\n{}".format(__name__
, message
, e
))
4371 def makeMaterial(name
, diffuse
):
4373 if name
in bpy
.data
.materials
:
4374 material
= bpy
.data
.materials
[name
]
4375 material
.diffuse_color
= diffuse
4377 material
= bpy
.data
.materials
.new(name
)
4378 material
.diffuse_color
= diffuse
4382 def update_mesh(self
, context
):
4384 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4385 bpy
.ops
.object.select_all(action
='DESELECT')
4386 bpy
.context
.view_layer
.update()
4387 global global_mesh_object
4388 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
4389 bpy
.data
.objects
[global_mesh_object
].select_set(True)
4390 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_mesh_object
]
4392 print("Select mesh object")
4394 def update_gpencil(self
, context
):
4396 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4397 bpy
.ops
.object.select_all(action
='DESELECT')
4398 bpy
.context
.view_layer
.update()
4399 global global_gpencil_object
4400 global_gpencil_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.name
4401 bpy
.data
.objects
[global_gpencil_object
].select_set(True)
4402 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_gpencil_object
]
4404 print("Select gpencil object")
4406 def update_curve(self
, context
):
4408 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4409 bpy
.ops
.object.select_all(action
='DESELECT')
4410 bpy
.context
.view_layer
.update()
4411 global global_curve_object
4412 global_curve_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.name
4413 bpy
.data
.objects
[global_curve_object
].select_set(True)
4414 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_curve_object
]
4416 print("Select curve object")
4418 def update_color(self
, context
):
4421 global global_mesh_object
4422 material
= makeMaterial("BSurfaceMesh", bpy
.context
.scene
.bsurfaces
.SURFSK_mesh_color
)
4423 if bpy
.data
.objects
[global_mesh_object
].data
.materials
:
4424 bpy
.data
.objects
[global_mesh_object
].data
.materials
[0] = material
4426 bpy
.data
.objects
[global_mesh_object
].data
.materials
.append(material
)
4427 diffuse_color
= material
.diffuse_color
4428 global_color
= (diffuse_color
[0], diffuse_color
[1], diffuse_color
[2], diffuse_color
[3])
4430 print("Select mesh object")
4432 def update_Shrinkwrap_offset(self
, context
):
4434 global global_offset
4435 global_offset
= bpy
.context
.scene
.bsurfaces
.SURFSK_Shrinkwrap_offset
4436 global global_mesh_object
4437 modifier
= bpy
.data
.objects
[global_mesh_object
].modifiers
["Shrinkwrap"]
4438 modifier
.offset
= global_offset
4440 print("Shrinkwrap modifier not found")
4442 def update_in_front(self
, context
):
4444 global global_in_front
4445 global_in_front
= bpy
.context
.scene
.bsurfaces
.SURFSK_in_front
4446 global global_mesh_object
4447 bpy
.data
.objects
[global_mesh_object
].show_in_front
= global_in_front
4449 print("Select mesh object")
4451 def update_show_wire(self
, context
):
4453 global global_show_wire
4454 global_show_wire
= bpy
.context
.scene
.bsurfaces
.SURFSK_show_wire
4455 global global_mesh_object
4456 bpy
.data
.objects
[global_mesh_object
].show_wire
= global_show_wire
4458 print("Select mesh object")
4460 def update_shade_smooth(self
, context
):
4462 global global_shade_smooth
4463 global_shade_smooth
= bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
4465 contex_mode
= bpy
.context
.mode
4467 if bpy
.ops
.object.mode_set
.poll():
4468 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4470 bpy
.ops
.object.select_all(action
='DESELECT')
4471 global global_mesh_object
4472 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
4473 bpy
.data
.objects
[global_mesh_object
].select_set(True)
4475 if global_shade_smooth
:
4476 bpy
.ops
.object.shade_smooth()
4478 bpy
.ops
.object.shade_flat()
4480 if contex_mode
== "EDIT_MESH":
4481 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4484 print("Select mesh object")
4487 class BsurfPreferences(AddonPreferences
):
4488 # this must match the addon name, use '__package__'
4489 # when defining this in a submodule of a python package.
4490 bl_idname
= __name__
4492 category
: StringProperty(
4493 name
="Tab Category",
4494 description
="Choose a name for the category of the panel",
4499 def draw(self
, context
):
4500 layout
= self
.layout
4504 col
.label(text
="Tab Category:")
4505 col
.prop(self
, "category", text
="")
4508 class BsurfacesProps(PropertyGroup
):
4509 SURFSK_guide
: EnumProperty(
4512 ('Annotation', 'Annotation', 'Annotation'),
4513 ('GPencil', 'GPencil', 'GPencil'),
4514 ('Curve', 'Curve', 'Curve')
4516 default
="Annotation"
4518 SURFSK_edges_U
: IntProperty(
4520 description
="Number of face-loops crossing the strokes",
4525 SURFSK_edges_V
: IntProperty(
4527 description
="Number of face-loops following the strokes",
4532 SURFSK_cyclic_cross
: BoolProperty(
4533 name
="Cyclic Cross",
4534 description
="Make cyclic the face-loops crossing the strokes",
4537 SURFSK_cyclic_follow
: BoolProperty(
4538 name
="Cyclic Follow",
4539 description
="Make cyclic the face-loops following the strokes",
4542 SURFSK_keep_strokes
: BoolProperty(
4543 name
="Keep strokes",
4544 description
="Keeps the sketched strokes or curves after adding the surface",
4547 SURFSK_automatic_join
: BoolProperty(
4548 name
="Automatic join",
4549 description
="Join automatically vertices of either surfaces "
4550 "generated by crosshatching, or from the borders of closed shapes",
4553 SURFSK_loops_on_strokes
: BoolProperty(
4554 name
="Loops on strokes",
4555 description
="Make the loops match the paths of the strokes",
4558 SURFSK_precision
: IntProperty(
4560 description
="Precision level of the surface calculation",
4565 SURFSK_mesh
: PointerProperty(
4566 name
="Mesh of BSurface",
4567 type=bpy
.types
.Object
,
4568 description
="Mesh of BSurface",
4571 SURFSK_gpencil
: PointerProperty(
4572 name
="GreasePencil object",
4573 type=bpy
.types
.Object
,
4574 description
="GreasePencil object",
4575 update
=update_gpencil
,
4577 SURFSK_curve
: PointerProperty(
4578 name
="Curve object",
4579 type=bpy
.types
.Object
,
4580 description
="Curve object",
4581 update
=update_curve
,
4583 SURFSK_mesh_color
: FloatVectorProperty(
4585 default
=(1.0, 0.0, 0.0, 0.3),
4590 update
=update_color
,
4591 description
="Mesh color",
4593 SURFSK_Shrinkwrap_offset
: FloatProperty(
4594 name
="Shrinkwrap offset",
4597 description
="Distance to keep from the target",
4598 update
=update_Shrinkwrap_offset
,
4600 SURFSK_in_front
: BoolProperty(
4602 description
="Make the object draw in front of others",
4604 update
=update_in_front
,
4606 SURFSK_show_wire
: BoolProperty(
4608 description
="Add the object’s wireframe over solid drawing",
4610 update
=update_show_wire
,
4612 SURFSK_shade_smooth
: BoolProperty(
4613 name
="Shade smooth",
4614 description
="Render and display faces smooth, using interpolated Vertex Normals",
4616 update
=update_shade_smooth
,
4620 MESH_OT_SURFSK_init
,
4621 MESH_OT_SURFSK_add_modifiers
,
4622 MESH_OT_SURFSK_add_surface
,
4623 MESH_OT_SURFSK_edit_surface
,
4624 GPENCIL_OT_SURFSK_add_strokes
,
4625 GPENCIL_OT_SURFSK_edit_strokes
,
4626 GPENCIL_OT_SURFSK_strokes_to_curves
,
4627 GPENCIL_OT_SURFSK_annotation_to_curves
,
4628 GPENCIL_OT_SURFSK_add_annotation
,
4629 CURVE_OT_SURFSK_edit_curve
,
4630 CURVE_OT_SURFSK_reorder_splines
,
4631 CURVE_OT_SURFSK_first_points
,
4638 bpy
.utils
.register_class(cls
)
4640 for panel
in panels
:
4641 bpy
.utils
.register_class(panel
)
4643 bpy
.types
.Scene
.bsurfaces
= PointerProperty(type=BsurfacesProps
)
4644 update_panel(None, bpy
.context
)
4647 for panel
in panels
:
4648 bpy
.utils
.unregister_class(panel
)
4651 bpy
.utils
.unregister_class(cls
)
4653 del bpy
.types
.Scene
.bsurfaces
4655 if __name__
== "__main__":