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 "wiki_url": "https://docs.blender.org/manual/nb/dev/addons/"
28 "mesh/bsurfaces.html",
35 from bpy_extras
import object_utils
38 from mathutils
import Matrix
, Vector
39 from mathutils
.geometry
import (
48 from bpy
.props
import (
57 from bpy
.types
import (
64 # ----------------------------
66 global_color
= [1.0, 0.0, 0.0, 0.3]
68 global_in_front
= False
69 global_shade_smooth
= False
70 global_show_wire
= True
71 global_mesh_object
= ""
72 global_gpencil_object
= ""
73 global_curve_object
= ""
75 # ----------------------------
77 class VIEW3D_PT_tools_SURFSK_mesh(Panel
):
78 bl_space_type
= 'VIEW_3D'
81 bl_label
= "Bsurfaces"
83 def draw(self
, context
):
85 scn
= context
.scene
.bsurfaces
87 col
= layout
.column(align
=True)
90 col
.operator("mesh.surfsk_init", text
="Initialize (Add BSurface mesh)")
91 col
.operator("mesh.surfsk_add_modifiers", text
="Add Mirror and others modifiers")
93 col
.label(text
="Mesh of BSurface:")
94 col
.prop(scn
, "SURFSK_mesh", text
="")
95 col
.prop(scn
, "SURFSK_mesh_color")
96 col
.prop(scn
, "SURFSK_Shrinkwrap_offset")
97 col
.prop(scn
, "SURFSK_in_front")
98 col
.prop(scn
, "SURFSK_shade_smooth")
99 col
.prop(scn
, "SURFSK_show_wire")
101 col
.label(text
="Guide strokes:")
102 col
.row().prop(scn
, "SURFSK_guide", expand
=True)
103 if scn
.SURFSK_guide
== 'GPencil':
104 col
.prop(scn
, "SURFSK_gpencil", text
="")
106 if scn
.SURFSK_guide
== 'Curve':
107 col
.prop(scn
, "SURFSK_curve", text
="")
111 col
.operator("mesh.surfsk_add_surface", text
="Add Surface")
112 col
.operator("mesh.surfsk_edit_surface", text
="Edit Surface")
115 if scn
.SURFSK_guide
== 'GPencil':
116 col
.operator("gpencil.surfsk_add_strokes", text
="Add Strokes")
117 col
.operator("gpencil.surfsk_edit_strokes", text
="Edit Strokes")
119 col
.operator("gpencil.surfsk_strokes_to_curves", text
="Strokes to curves")
121 if scn
.SURFSK_guide
== 'Annotation':
122 col
.operator("gpencil.surfsk_add_annotation", text
="Add Annotation")
124 col
.operator("gpencil.surfsk_annotations_to_curves", text
="Annotation to curves")
126 if scn
.SURFSK_guide
== 'Curve':
127 col
.operator("curve.surfsk_edit_curve", text
="Edit curve")
130 col
.label(text
="Initial settings:")
131 col
.prop(scn
, "SURFSK_edges_U")
132 col
.prop(scn
, "SURFSK_edges_V")
133 col
.prop(scn
, "SURFSK_cyclic_cross")
134 col
.prop(scn
, "SURFSK_cyclic_follow")
135 col
.prop(scn
, "SURFSK_loops_on_strokes")
136 col
.prop(scn
, "SURFSK_automatic_join")
137 col
.prop(scn
, "SURFSK_keep_strokes")
139 class VIEW3D_PT_tools_SURFSK_curve(Panel
):
140 bl_space_type
= 'VIEW_3D'
141 bl_region_type
= 'UI'
142 bl_context
= "curve_edit"
144 bl_label
= "Bsurfaces"
147 def poll(cls
, context
):
148 return context
.active_object
150 def draw(self
, context
):
153 col
= layout
.column(align
=True)
156 col
.operator("curve.surfsk_first_points", text
="Set First Points")
157 col
.operator("curve.switch_direction", text
="Switch Direction")
158 col
.operator("curve.surfsk_reorder_splines", text
="Reorder Splines")
161 # ----------------------------
162 # Returns the type of strokes used
163 def get_strokes_type(context
):
164 strokes_type
= "NO_STROKES"
167 # Check if they are annotation
168 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Annotation':
170 strokes
= bpy
.data
.grease_pencils
[0].layers
.active
.active_frame
.strokes
172 strokes_num
= len(strokes
)
175 strokes_type
= "GP_ANNOTATION"
177 strokes_type
= "NO_STROKES"
179 # Check if they are grease pencil
180 if context
.scene
.bsurfaces
.SURFSK_guide
== 'GPencil':
182 global global_gpencil_object
183 gpencil
= bpy
.data
.objects
[global_gpencil_object
]
184 strokes
= gpencil
.data
.layers
.active
.active_frame
.strokes
186 strokes_num
= len(strokes
)
189 strokes_type
= "GP_STROKES"
191 strokes_type
= "NO_STROKES"
193 # Check if they are curves, if there aren't grease pencil strokes
194 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Curve':
196 global global_curve_object
197 ob
= bpy
.data
.objects
[global_curve_object
]
198 if ob
.type == "CURVE":
199 strokes_type
= "EXTERNAL_CURVE"
200 strokes_num
= len(ob
.data
.splines
)
202 # Check if there is any non-bezier spline
203 for i
in range(len(ob
.data
.splines
)):
204 if ob
.data
.splines
[i
].type != "BEZIER":
205 strokes_type
= "CURVE_WITH_NON_BEZIER_SPLINES"
209 strokes_type
= "EXTERNAL_NO_CURVE"
211 strokes_type
= "NO_STROKES"
213 # Check if they are mesh
215 global global_mesh_object
216 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
217 total_vert_sel
= len([v
for v
in self
.main_object
.data
.vertices
if v
.select
])
219 # Check if there is a single stroke without any selection in the object
220 if strokes_num
== 1 and total_vert_sel
== 0:
221 if strokes_type
== "EXTERNAL_CURVE":
222 strokes_type
= "SINGLE_CURVE_STROKE_NO_SELECTION"
223 elif strokes_type
== "GP_STROKES":
224 strokes_type
= "SINGLE_GP_STROKE_NO_SELECTION"
226 if strokes_num
== 0 and total_vert_sel
> 0:
227 strokes_type
= "SELECTION_ALONE"
233 # ----------------------------
234 # Surface generator operator
235 class MESH_OT_SURFSK_add_surface(Operator
):
236 bl_idname
= "mesh.surfsk_add_surface"
237 bl_label
= "Bsurfaces add surface"
238 bl_description
= "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
239 bl_options
= {'REGISTER', 'UNDO'}
241 is_fill_faces
: BoolProperty(
244 selection_U_exists
: BoolProperty(
247 selection_V_exists
: BoolProperty(
250 selection_U2_exists
: BoolProperty(
253 selection_V2_exists
: BoolProperty(
256 selection_V_is_closed
: BoolProperty(
259 selection_U_is_closed
: BoolProperty(
262 selection_V2_is_closed
: BoolProperty(
265 selection_U2_is_closed
: BoolProperty(
269 edges_U
: IntProperty(
271 description
="Number of face-loops crossing the strokes",
276 edges_V
: IntProperty(
278 description
="Number of face-loops following the strokes",
283 cyclic_cross
: BoolProperty(
285 description
="Make cyclic the face-loops crossing the strokes",
288 cyclic_follow
: BoolProperty(
289 name
="Cyclic Follow",
290 description
="Make cyclic the face-loops following the strokes",
293 loops_on_strokes
: BoolProperty(
294 name
="Loops on strokes",
295 description
="Make the loops match the paths of the strokes",
298 automatic_join
: BoolProperty(
299 name
="Automatic join",
300 description
="Join automatically vertices of either surfaces generated "
301 "by crosshatching, or from the borders of closed shapes",
304 join_stretch_factor
: FloatProperty(
306 description
="Amount of stretching or shrinking allowed for "
307 "edges when joining vertices automatically",
313 keep_strokes
: BoolProperty(
315 description
="Keeps the sketched strokes or curves after adding the surface",
318 strokes_type
: StringProperty()
319 initial_global_undo_state
: BoolProperty()
322 def draw(self
, context
):
324 col
= layout
.column(align
=True)
327 if not self
.is_fill_faces
:
329 if not self
.is_crosshatch
:
330 if not self
.selection_U_exists
:
331 col
.prop(self
, "edges_U")
334 if not self
.selection_V_exists
:
335 col
.prop(self
, "edges_V")
340 if not self
.selection_U_exists
:
342 (self
.selection_V_exists
and not self
.selection_V_is_closed
) or
343 (self
.selection_V2_exists
and not self
.selection_V2_is_closed
)
345 col
.prop(self
, "cyclic_cross")
347 if not self
.selection_V_exists
:
349 (self
.selection_U_exists
and not self
.selection_U_is_closed
) or
350 (self
.selection_U2_exists
and not self
.selection_U2_is_closed
)
352 col
.prop(self
, "cyclic_follow")
354 col
.prop(self
, "loops_on_strokes")
356 col
.prop(self
, "automatic_join")
358 if self
.automatic_join
:
362 col
.prop(self
, "join_stretch_factor")
364 col
.prop(self
, "keep_strokes")
366 # Get an ordered list of a chain of vertices
367 def get_ordered_verts(self
, ob
, all_selected_edges_idx
, all_selected_verts_idx
,
368 first_vert_idx
, middle_vertex_idx
, closing_vert_idx
):
369 # Order selected vertices.
371 if closing_vert_idx
is not None:
372 verts_ordered
.append(ob
.data
.vertices
[closing_vert_idx
])
374 verts_ordered
.append(ob
.data
.vertices
[first_vert_idx
])
375 prev_v
= first_vert_idx
379 edges_non_matched
= 0
380 for i
in all_selected_edges_idx
:
381 if ob
.data
.edges
[i
] != prev_ed
and ob
.data
.edges
[i
].vertices
[0] == prev_v
and \
382 ob
.data
.edges
[i
].vertices
[1] in all_selected_verts_idx
:
384 verts_ordered
.append(ob
.data
.vertices
[ob
.data
.edges
[i
].vertices
[1]])
385 prev_v
= ob
.data
.edges
[i
].vertices
[1]
386 prev_ed
= ob
.data
.edges
[i
]
387 elif ob
.data
.edges
[i
] != prev_ed
and ob
.data
.edges
[i
].vertices
[1] == prev_v
and \
388 ob
.data
.edges
[i
].vertices
[0] in all_selected_verts_idx
:
390 verts_ordered
.append(ob
.data
.vertices
[ob
.data
.edges
[i
].vertices
[0]])
391 prev_v
= ob
.data
.edges
[i
].vertices
[0]
392 prev_ed
= ob
.data
.edges
[i
]
394 edges_non_matched
+= 1
396 if edges_non_matched
== len(all_selected_edges_idx
):
402 if closing_vert_idx
is not None:
403 verts_ordered
.append(ob
.data
.vertices
[closing_vert_idx
])
405 if middle_vertex_idx
is not None:
406 verts_ordered
.append(ob
.data
.vertices
[middle_vertex_idx
])
407 verts_ordered
.reverse()
409 return tuple(verts_ordered
)
411 # Calculates length of a chain of points.
412 def get_chain_length(self
, object, verts_ordered
):
413 matrix
= object.matrix_world
416 edges_lengths_sum
= 0
417 for i
in range(0, len(verts_ordered
)):
419 prev_v_co
= matrix
@ verts_ordered
[i
].co
421 v_co
= matrix
@ verts_ordered
[i
].co
423 v_difs
= [prev_v_co
[0] - v_co
[0], prev_v_co
[1] - v_co
[1], prev_v_co
[2] - v_co
[2]]
424 edge_length
= abs(sqrt(v_difs
[0] * v_difs
[0] + v_difs
[1] * v_difs
[1] + v_difs
[2] * v_difs
[2]))
426 edges_lengths
.append(edge_length
)
427 edges_lengths_sum
+= edge_length
431 return edges_lengths
, edges_lengths_sum
433 # Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
434 def get_edges_proportions(self
, edges_lengths
, edges_lengths_sum
, use_boundaries
, fixed_edges_num
):
435 edges_proportions
= []
438 for l
in edges_lengths
:
439 edges_proportions
.append(l
/ edges_lengths_sum
)
443 for _n
in range(0, fixed_edges_num
):
444 edges_proportions
.append(1 / fixed_edges_num
)
447 return edges_proportions
449 # Calculates the angle between two pairs of points in space
450 def orientation_difference(self
, points_A_co
, points_B_co
):
451 # each parameter should be a list with two elements,
452 # and each element should be a x,y,z coordinate
453 vec_A
= points_A_co
[0] - points_A_co
[1]
454 vec_B
= points_B_co
[0] - points_B_co
[1]
456 angle
= vec_A
.angle(vec_B
)
459 angle
= abs(angle
- pi
)
463 # Calculate the which vert of verts_idx list is the nearest one
464 # to the point_co coordinates, and the distance
465 def shortest_distance(self
, object, point_co
, verts_idx
):
466 matrix
= object.matrix_world
468 for i
in range(0, len(verts_idx
)):
469 dist
= (point_co
- matrix
@ object.data
.vertices
[verts_idx
[i
]].co
).length
472 nearest_vert_idx
= verts_idx
[i
]
477 nearest_vert_idx
= verts_idx
[i
]
480 return nearest_vert_idx
, shortest_dist
482 # Returns the index of the opposite vert tip in a chain, given a vert tip index
483 # as parameter, and a multidimentional list with all pairs of tips
484 def opposite_tip(self
, vert_tip_idx
, all_chains_tips_idx
):
485 opposite_vert_tip_idx
= None
486 for i
in range(0, len(all_chains_tips_idx
)):
487 if vert_tip_idx
== all_chains_tips_idx
[i
][0]:
488 opposite_vert_tip_idx
= all_chains_tips_idx
[i
][1]
489 if vert_tip_idx
== all_chains_tips_idx
[i
][1]:
490 opposite_vert_tip_idx
= all_chains_tips_idx
[i
][0]
492 return opposite_vert_tip_idx
494 # Simplifies a spline and returns the new points coordinates
495 def simplify_spline(self
, spline_coords
, segments_num
):
496 simplified_spline
= []
497 points_between_segments
= round(len(spline_coords
) / segments_num
)
499 simplified_spline
.append(spline_coords
[0])
500 for i
in range(1, segments_num
):
501 simplified_spline
.append(spline_coords
[i
* points_between_segments
])
503 simplified_spline
.append(spline_coords
[len(spline_coords
) - 1])
505 return simplified_spline
507 # Returns a list with the coords of the points distributed over the splines
508 # passed to this method according to the proportions parameter
509 def distribute_pts(self
, surface_splines
, proportions
):
511 # Calculate the length of each final surface spline
512 surface_splines_lengths
= []
513 surface_splines_parsed
= []
515 for sp_idx
in range(0, len(surface_splines
)):
516 # Calculate spline length
517 surface_splines_lengths
.append(0)
519 for i
in range(0, len(surface_splines
[sp_idx
].bezier_points
)):
521 prev_p
= surface_splines
[sp_idx
].bezier_points
[i
]
523 p
= surface_splines
[sp_idx
].bezier_points
[i
]
524 edge_length
= (prev_p
.co
- p
.co
).length
525 surface_splines_lengths
[sp_idx
] += edge_length
529 # Calculate vertex positions with appropriate edge proportions, and ordered, for each spline
530 for sp_idx
in range(0, len(surface_splines
)):
531 surface_splines_parsed
.append([])
532 surface_splines_parsed
[sp_idx
].append(surface_splines
[sp_idx
].bezier_points
[0].co
)
534 prev_p_co
= surface_splines
[sp_idx
].bezier_points
[0].co
537 for prop_idx
in range(len(proportions
) - 1):
538 target_length
= surface_splines_lengths
[sp_idx
] * proportions
[prop_idx
]
539 partial_segment_length
= 0
543 # if not it'll pass the p_idx as an index below and crash
544 if p_idx
< len(surface_splines
[sp_idx
].bezier_points
):
545 p_co
= surface_splines
[sp_idx
].bezier_points
[p_idx
].co
546 new_dist
= (prev_p_co
- p_co
).length
548 # The new distance that could have the partial segment if
549 # it is still shorter than the target length
550 potential_segment_length
= partial_segment_length
+ new_dist
552 # If the potential is still shorter, keep adding
553 if potential_segment_length
< target_length
:
554 partial_segment_length
= potential_segment_length
559 # If the potential is longer than the target, calculate the target
560 # (a point between the last two points), and assign
561 elif potential_segment_length
> target_length
:
562 remaining_dist
= target_length
- partial_segment_length
563 vec
= p_co
- prev_p_co
565 intermediate_co
= prev_p_co
+ (vec
* remaining_dist
)
567 surface_splines_parsed
[sp_idx
].append(intermediate_co
)
569 partial_segment_length
+= remaining_dist
570 prev_p_co
= intermediate_co
574 # If the potential is equal to the target, assign
575 elif potential_segment_length
== target_length
:
576 surface_splines_parsed
[sp_idx
].append(p_co
)
584 # last point of the spline
585 surface_splines_parsed
[sp_idx
].append(
586 surface_splines
[sp_idx
].bezier_points
[len(surface_splines
[sp_idx
].bezier_points
) - 1].co
589 return surface_splines_parsed
591 # Counts the number of faces that belong to each edge
592 def edge_face_count(self
, ob
):
593 ed_keys_count_dict
= {}
595 for face
in ob
.data
.polygons
:
596 for ed_keys
in face
.edge_keys
:
597 if ed_keys
not in ed_keys_count_dict
:
598 ed_keys_count_dict
[ed_keys
] = 1
600 ed_keys_count_dict
[ed_keys
] += 1
603 for i
in range(len(ob
.data
.edges
)):
604 edge_face_count
.append(0)
606 for i
in range(len(ob
.data
.edges
)):
607 ed
= ob
.data
.edges
[i
]
612 if (v1
, v2
) in ed_keys_count_dict
:
613 edge_face_count
[i
] = ed_keys_count_dict
[(v1
, v2
)]
614 elif (v2
, v1
) in ed_keys_count_dict
:
615 edge_face_count
[i
] = ed_keys_count_dict
[(v2
, v1
)]
617 return edge_face_count
619 # Fills with faces all the selected vertices which form empty triangles or quads
620 def fill_with_faces(self
, object):
621 all_selected_verts_count
= self
.main_object_selected_verts_count
623 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
625 # Calculate average length of selected edges
626 all_selected_verts
= []
627 original_sel_edges_count
= 0
628 for ed
in object.data
.edges
:
629 if object.data
.vertices
[ed
.vertices
[0]].select
and object.data
.vertices
[ed
.vertices
[1]].select
:
631 coords
.append(object.data
.vertices
[ed
.vertices
[0]].co
)
632 coords
.append(object.data
.vertices
[ed
.vertices
[1]].co
)
634 original_sel_edges_count
+= 1
636 if not ed
.vertices
[0] in all_selected_verts
:
637 all_selected_verts
.append(ed
.vertices
[0])
639 if not ed
.vertices
[1] in all_selected_verts
:
640 all_selected_verts
.append(ed
.vertices
[1])
642 tuple(all_selected_verts
)
644 # Check if there is any edge selected. If not, interrupt the script
645 if original_sel_edges_count
== 0 and all_selected_verts_count
> 0:
648 # Get all edges connected to selected verts
649 all_edges_around_sel_verts
= []
650 edges_connected_to_sel_verts
= {}
651 verts_connected_to_every_vert
= {}
652 for ed_idx
in range(len(object.data
.edges
)):
653 ed
= object.data
.edges
[ed_idx
]
656 if ed
.vertices
[0] in all_selected_verts
:
657 if not ed
.vertices
[0] in edges_connected_to_sel_verts
:
658 edges_connected_to_sel_verts
[ed
.vertices
[0]] = []
660 edges_connected_to_sel_verts
[ed
.vertices
[0]].append(ed_idx
)
663 if ed
.vertices
[1] in all_selected_verts
:
664 if not ed
.vertices
[1] in edges_connected_to_sel_verts
:
665 edges_connected_to_sel_verts
[ed
.vertices
[1]] = []
667 edges_connected_to_sel_verts
[ed
.vertices
[1]].append(ed_idx
)
670 if include_edge
is True:
671 all_edges_around_sel_verts
.append(ed_idx
)
673 # Get all connected verts to each vert
674 if not ed
.vertices
[0] in verts_connected_to_every_vert
:
675 verts_connected_to_every_vert
[ed
.vertices
[0]] = []
677 if not ed
.vertices
[1] in verts_connected_to_every_vert
:
678 verts_connected_to_every_vert
[ed
.vertices
[1]] = []
680 verts_connected_to_every_vert
[ed
.vertices
[0]].append(ed
.vertices
[1])
681 verts_connected_to_every_vert
[ed
.vertices
[1]].append(ed
.vertices
[0])
683 # Get all verts connected to faces
684 all_verts_part_of_faces
= []
685 all_edges_faces_count
= []
686 all_edges_faces_count
+= self
.edge_face_count(object)
688 # Get only the selected edges that have faces attached.
689 count_faces_of_edges_around_sel_verts
= {}
690 selected_verts_with_faces
= []
691 for ed_idx
in all_edges_around_sel_verts
:
692 count_faces_of_edges_around_sel_verts
[ed_idx
] = all_edges_faces_count
[ed_idx
]
694 if all_edges_faces_count
[ed_idx
] > 0:
695 ed
= object.data
.edges
[ed_idx
]
697 if not ed
.vertices
[0] in selected_verts_with_faces
:
698 selected_verts_with_faces
.append(ed
.vertices
[0])
700 if not ed
.vertices
[1] in selected_verts_with_faces
:
701 selected_verts_with_faces
.append(ed
.vertices
[1])
703 all_verts_part_of_faces
.append(ed
.vertices
[0])
704 all_verts_part_of_faces
.append(ed
.vertices
[1])
706 tuple(selected_verts_with_faces
)
708 # Discard unneeded verts from calculations
709 participating_verts
= []
711 for v_idx
in all_selected_verts
:
712 vert_has_edges_with_one_face
= False
714 # Check if the actual vert has at least one edge connected to only one face
715 for ed_idx
in edges_connected_to_sel_verts
[v_idx
]:
716 if count_faces_of_edges_around_sel_verts
[ed_idx
] == 1:
717 vert_has_edges_with_one_face
= True
719 # If the vert has two or less edges connected and the vert is not part of any face.
720 # Or the vert is part of any face and at least one of
721 # the connected edges has only one face attached to it.
722 if (len(edges_connected_to_sel_verts
[v_idx
]) == 2 and
723 v_idx
not in all_verts_part_of_faces
) or \
724 len(edges_connected_to_sel_verts
[v_idx
]) == 1 or \
725 (v_idx
in all_verts_part_of_faces
and
726 vert_has_edges_with_one_face
):
728 participating_verts
.append(v_idx
)
730 if v_idx
not in all_verts_part_of_faces
:
731 movable_verts
.append(v_idx
)
733 # Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
734 for mv_idx
in movable_verts
:
736 mv_connected_verts
= verts_connected_to_every_vert
[mv_idx
]
738 for actual_v_idx
in all_selected_verts
:
739 count_shared_neighbors
= 0
742 for mv_conn_v_idx
in mv_connected_verts
:
743 if mv_idx
!= actual_v_idx
:
744 if mv_conn_v_idx
in verts_connected_to_every_vert
[actual_v_idx
] and \
745 mv_conn_v_idx
not in checked_verts
:
746 count_shared_neighbors
+= 1
747 checked_verts
.append(mv_conn_v_idx
)
749 if actual_v_idx
in mv_connected_verts
:
753 if count_shared_neighbors
== 2:
761 movable_verts
.remove(mv_idx
)
763 # Calculate merge distance for participating verts
764 shortest_edge_length
= None
765 for ed
in object.data
.edges
:
766 if ed
.vertices
[0] in movable_verts
and ed
.vertices
[1] in movable_verts
:
767 v1
= object.data
.vertices
[ed
.vertices
[0]]
768 v2
= object.data
.vertices
[ed
.vertices
[1]]
770 length
= (v1
.co
- v2
.co
).length
772 if shortest_edge_length
is None:
773 shortest_edge_length
= length
775 if length
< shortest_edge_length
:
776 shortest_edge_length
= length
778 if shortest_edge_length
is not None:
779 edges_merge_distance
= shortest_edge_length
* 0.5
781 edges_merge_distance
= 0
783 # Get together the verts near enough. They will be merged later
785 remaining_verts
+= participating_verts
786 for v1_idx
in participating_verts
:
787 if v1_idx
in remaining_verts
and v1_idx
in movable_verts
:
789 coords_verts_to_merge
= {}
791 verts_to_merge
.append(v1_idx
)
793 v1_co
= object.data
.vertices
[v1_idx
].co
794 coords_verts_to_merge
[v1_idx
] = (v1_co
[0], v1_co
[1], v1_co
[2])
796 for v2_idx
in remaining_verts
:
798 v2_co
= object.data
.vertices
[v2_idx
].co
800 dist
= (v1_co
- v2_co
).length
802 if dist
<= edges_merge_distance
: # Add the verts which are near enough
803 verts_to_merge
.append(v2_idx
)
805 coords_verts_to_merge
[v2_idx
] = (v2_co
[0], v2_co
[1], v2_co
[2])
807 for vm_idx
in verts_to_merge
:
808 remaining_verts
.remove(vm_idx
)
810 if len(verts_to_merge
) > 1:
811 # Calculate middle point of the verts to merge.
815 movable_verts_to_merge_count
= 0
816 for i
in range(len(verts_to_merge
)):
817 if verts_to_merge
[i
] in movable_verts
:
818 v_co
= object.data
.vertices
[verts_to_merge
[i
]].co
824 movable_verts_to_merge_count
+= 1
827 sum_x_co
/ movable_verts_to_merge_count
,
828 sum_y_co
/ movable_verts_to_merge_count
,
829 sum_z_co
/ movable_verts_to_merge_count
832 # Check if any vert to be merged is not movable
834 are_verts_not_movable
= False
835 verts_not_movable
= []
836 for v_merge_idx
in verts_to_merge
:
837 if v_merge_idx
in participating_verts
and v_merge_idx
not in movable_verts
:
838 are_verts_not_movable
= True
839 verts_not_movable
.append(v_merge_idx
)
841 if are_verts_not_movable
:
842 # Get the vert connected to faces, that is nearest to
843 # the middle point of the movable verts
845 for vcf_idx
in verts_not_movable
:
846 dist
= abs((object.data
.vertices
[vcf_idx
].co
-
847 Vector(middle_point_co
)).length
)
849 if shortest_dist
is None:
851 nearest_vert_idx
= vcf_idx
853 if dist
< shortest_dist
:
855 nearest_vert_idx
= vcf_idx
857 coords
= object.data
.vertices
[nearest_vert_idx
].co
858 target_point_co
= [coords
[0], coords
[1], coords
[2]]
860 target_point_co
= middle_point_co
862 # Move verts to merge to the middle position
863 for v_merge_idx
in verts_to_merge
:
864 if v_merge_idx
in movable_verts
: # Only move the verts that are not part of faces
865 object.data
.vertices
[v_merge_idx
].co
[0] = target_point_co
[0]
866 object.data
.vertices
[v_merge_idx
].co
[1] = target_point_co
[1]
867 object.data
.vertices
[v_merge_idx
].co
[2] = target_point_co
[2]
869 # Perform "Remove Doubles" to weld all the disconnected verts
870 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
871 bpy
.ops
.mesh
.remove_doubles(threshold
=0.0001)
873 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
875 # Get all the definitive selected edges, after weldding
877 edges_per_vert
= {} # Number of faces of each selected edge
878 for ed
in object.data
.edges
:
879 if object.data
.vertices
[ed
.vertices
[0]].select
and object.data
.vertices
[ed
.vertices
[1]].select
:
880 selected_edges
.append(ed
.index
)
882 # Save all the edges that belong to each vertex.
883 if not ed
.vertices
[0] in edges_per_vert
:
884 edges_per_vert
[ed
.vertices
[0]] = []
886 if not ed
.vertices
[1] in edges_per_vert
:
887 edges_per_vert
[ed
.vertices
[1]] = []
889 edges_per_vert
[ed
.vertices
[0]].append(ed
.index
)
890 edges_per_vert
[ed
.vertices
[1]].append(ed
.index
)
892 # Check if all the edges connected to each vert have two faces attached to them.
893 # To discard them later and make calculations faster
895 a
+= self
.edge_face_count(object)
897 verts_surrounded_by_faces
= {}
898 for v_idx
in edges_per_vert
:
899 edges_with_two_faces_count
= 0
901 for ed_idx
in edges_per_vert
[v_idx
]:
903 edges_with_two_faces_count
+= 1
905 if edges_with_two_faces_count
== len(edges_per_vert
[v_idx
]):
906 verts_surrounded_by_faces
[v_idx
] = True
908 verts_surrounded_by_faces
[v_idx
] = False
910 # Get all the selected vertices
911 selected_verts_idx
= []
912 for v
in object.data
.vertices
:
914 selected_verts_idx
.append(v
.index
)
916 # Get all the faces of the object
917 all_object_faces_verts_idx
= []
918 for face
in object.data
.polygons
:
920 face_verts
.append(face
.vertices
[0])
921 face_verts
.append(face
.vertices
[1])
922 face_verts
.append(face
.vertices
[2])
924 if len(face
.vertices
) == 4:
925 face_verts
.append(face
.vertices
[3])
927 all_object_faces_verts_idx
.append(face_verts
)
929 # Deselect all vertices
930 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
931 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
932 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
934 # Make a dictionary with the verts related to each vert
935 related_key_verts
= {}
936 for ed_idx
in selected_edges
:
937 ed
= object.data
.edges
[ed_idx
]
939 if not verts_surrounded_by_faces
[ed
.vertices
[0]]:
940 if not ed
.vertices
[0] in related_key_verts
:
941 related_key_verts
[ed
.vertices
[0]] = []
943 if not ed
.vertices
[1] in related_key_verts
[ed
.vertices
[0]]:
944 related_key_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
946 if not verts_surrounded_by_faces
[ed
.vertices
[1]]:
947 if not ed
.vertices
[1] in related_key_verts
:
948 related_key_verts
[ed
.vertices
[1]] = []
950 if not ed
.vertices
[0] in related_key_verts
[ed
.vertices
[1]]:
951 related_key_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
953 # Get groups of verts forming each face
955 for v1
in related_key_verts
: # verts-1 ....
956 for v2
in related_key_verts
: # verts-2
958 related_verts_in_common
= []
961 for rel_v1
in related_key_verts
[v1
]:
962 # Check if related verts of verts-1 are related verts of verts-2
963 if rel_v1
in related_key_verts
[v2
]:
964 related_verts_in_common
.append(rel_v1
)
966 if v2
in related_key_verts
[v1
]:
969 if v1
in related_key_verts
[v2
]:
972 repeated_face
= False
973 # If two verts have two related verts in common, they form a quad
974 if len(related_verts_in_common
) == 2:
975 # Check if the face is already saved
976 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
978 for f_verts
in all_faces_to_check_idx
:
981 if len(f_verts
) == 4:
986 if related_verts_in_common
[0] in f_verts
:
988 if related_verts_in_common
[1] in f_verts
:
991 if repeated_verts
== len(f_verts
):
995 if not repeated_face
:
996 faces_verts_idx
.append(
997 [v1
, related_verts_in_common
[0], v2
, related_verts_in_common
[1]]
1000 # If Two verts have one related vert in common and
1001 # they are related to each other, they form a triangle
1002 elif v2_in_rel_v1
and v1_in_rel_v2
and len(related_verts_in_common
) == 1:
1003 # Check if the face is already saved.
1004 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
1006 for f_verts
in all_faces_to_check_idx
:
1009 if len(f_verts
) == 3:
1014 if related_verts_in_common
[0] in f_verts
:
1017 if repeated_verts
== len(f_verts
):
1018 repeated_face
= True
1021 if not repeated_face
:
1022 faces_verts_idx
.append([v1
, related_verts_in_common
[0], v2
])
1024 # Keep only the faces that don't overlap by ignoring quads
1025 # that overlap with two adjacent triangles
1026 faces_to_not_include_idx
= [] # Indices of faces_verts_idx to eliminate
1027 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
1028 for i
in range(len(faces_verts_idx
)):
1029 for t
in range(len(all_faces_to_check_idx
)):
1033 if len(faces_verts_idx
[i
]) == 4 and len(all_faces_to_check_idx
[t
]) == 3:
1034 for v_idx
in all_faces_to_check_idx
[t
]:
1035 if v_idx
in faces_verts_idx
[i
]:
1036 verts_in_common
+= 1
1037 # If it doesn't have all it's vertices repeated in the other face
1038 if verts_in_common
== 3:
1039 if i
not in faces_to_not_include_idx
:
1040 faces_to_not_include_idx
.append(i
)
1042 # Build faces discarding the ones in faces_to_not_include
1047 num_faces_created
= 0
1048 for i
in range(len(faces_verts_idx
)):
1049 if i
not in faces_to_not_include_idx
:
1050 bm
.faces
.new([bm
.verts
[v
] for v
in faces_verts_idx
[i
]])
1052 num_faces_created
+= 1
1057 for v_idx
in selected_verts_idx
:
1058 self
.main_object
.data
.vertices
[v_idx
].select
= True
1060 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
1061 bpy
.ops
.mesh
.normals_make_consistent(inside
=False)
1062 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
1066 return num_faces_created
1068 # Crosshatch skinning
1069 def crosshatch_surface_invoke(self
, ob_original_splines
):
1070 self
.is_crosshatch
= False
1071 self
.crosshatch_merge_distance
= 0
1073 objects_to_delete
= [] # duplicated strokes to be deleted.
1075 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1076 # (without this the surface verts merging with the main object doesn't work well)
1077 self
.modifiers_prev_viewport_state
= []
1078 if len(self
.main_object
.modifiers
) > 0:
1079 for m_idx
in range(len(self
.main_object
.modifiers
)):
1080 self
.modifiers_prev_viewport_state
.append(
1081 self
.main_object
.modifiers
[m_idx
].show_viewport
1083 self
.main_object
.modifiers
[m_idx
].show_viewport
= False
1085 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1086 ob_original_splines
.select_set(True)
1087 bpy
.context
.view_layer
.objects
.active
= ob_original_splines
1089 if len(ob_original_splines
.data
.splines
) >= 2:
1090 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1091 ob_splines
= bpy
.context
.object
1092 ob_splines
.name
= "SURFSKIO_NE_STR"
1094 # Get estimative merge distance (sum up the distances from the first point to
1095 # all other points, then average them and then divide them)
1096 first_point_dist_sum
= 0
1099 coords_first_pt
= ob_splines
.data
.splines
[0].bezier_points
[0].co
1100 for i
in range(len(ob_splines
.data
.splines
)):
1101 sp
= ob_splines
.data
.splines
[i
]
1103 if coords_first_pt
!= sp
.bezier_points
[0].co
:
1104 first_dist
= (coords_first_pt
- sp
.bezier_points
[0].co
).length
1106 if coords_first_pt
!= sp
.bezier_points
[len(sp
.bezier_points
) - 1].co
:
1107 second_dist
= (coords_first_pt
- sp
.bezier_points
[len(sp
.bezier_points
) - 1].co
).length
1109 first_point_dist_sum
+= first_dist
+ second_dist
1113 shortest_dist
= first_dist
1114 elif second_dist
!= 0:
1115 shortest_dist
= second_dist
1117 if shortest_dist
> first_dist
and first_dist
!= 0:
1118 shortest_dist
= first_dist
1120 if shortest_dist
> second_dist
and second_dist
!= 0:
1121 shortest_dist
= second_dist
1123 self
.crosshatch_merge_distance
= shortest_dist
/ 20
1125 # Recalculation of merge distance
1127 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1129 ob_calc_merge_dist
= bpy
.context
.object
1130 ob_calc_merge_dist
.name
= "SURFSKIO_CALC_TMP"
1132 objects_to_delete
.append(ob_calc_merge_dist
)
1134 # Smooth out strokes a little to improve crosshatch detection
1135 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1136 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1139 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1141 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1142 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1144 # Convert curves into mesh
1145 ob_calc_merge_dist
.data
.resolution_u
= 12
1146 bpy
.ops
.object.convert(target
='MESH', keep_original
=False)
1148 # Find "intersection-nodes"
1149 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1150 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1151 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN',
1152 threshold
=self
.crosshatch_merge_distance
)
1153 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1154 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1156 # Remove verts with less than three edges
1157 verts_edges_count
= {}
1158 for ed
in ob_calc_merge_dist
.data
.edges
:
1161 if v
[0] not in verts_edges_count
:
1162 verts_edges_count
[v
[0]] = 0
1164 if v
[1] not in verts_edges_count
:
1165 verts_edges_count
[v
[1]] = 0
1167 verts_edges_count
[v
[0]] += 1
1168 verts_edges_count
[v
[1]] += 1
1170 nodes_verts_coords
= []
1171 for v_idx
in verts_edges_count
:
1172 v
= ob_calc_merge_dist
.data
.vertices
[v_idx
]
1174 if verts_edges_count
[v_idx
] < 3:
1178 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1179 bpy
.ops
.mesh
.delete('INVOKE_REGION_WIN', type='VERT')
1180 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1182 # Remove doubles to discard very near verts from calculations of distance
1183 bpy
.ops
.mesh
.remove_doubles(
1184 'INVOKE_REGION_WIN',
1185 threshold
=self
.crosshatch_merge_distance
* 4.0
1187 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1189 # Get all coords of the resulting nodes
1190 nodes_verts_coords
= [(v
.co
[0], v
.co
[1], v
.co
[2]) for
1191 v
in ob_calc_merge_dist
.data
.vertices
]
1193 # Check if the strokes are a crosshatch
1194 if len(nodes_verts_coords
) >= 3:
1195 self
.is_crosshatch
= True
1197 shortest_dist
= None
1198 for co_1
in nodes_verts_coords
:
1199 for co_2
in nodes_verts_coords
:
1201 dist
= (Vector(co_1
) - Vector(co_2
)).length
1203 if shortest_dist
is not None:
1204 if dist
< shortest_dist
:
1205 shortest_dist
= dist
1207 shortest_dist
= dist
1209 self
.crosshatch_merge_distance
= shortest_dist
/ 3
1211 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1212 ob_splines
.select_set(True)
1213 bpy
.context
.view_layer
.objects
.active
= ob_splines
1215 # Deselect all points
1216 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1217 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1218 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1220 # Smooth splines in a localized way, to eliminate "saw-teeth"
1221 # like shapes when there are many points
1222 for sp
in ob_splines
.data
.splines
:
1225 angle_limit
= 2 # Degrees
1226 for t
in range(len(sp
.bezier_points
)):
1227 # Because on each iteration it checks the "next two points"
1228 # of the actual. This way it doesn't go out of range
1229 if t
<= len(sp
.bezier_points
) - 3:
1230 p1
= sp
.bezier_points
[t
]
1231 p2
= sp
.bezier_points
[t
+ 1]
1232 p3
= sp
.bezier_points
[t
+ 2]
1234 vec_1
= p1
.co
- p2
.co
1235 vec_2
= p2
.co
- p3
.co
1237 if p2
.co
!= p1
.co
and p2
.co
!= p3
.co
:
1238 angle
= vec_1
.angle(vec_2
)
1239 angle_sum
+= degrees(angle
)
1241 if angle_sum
>= angle_limit
: # If sum of angles is grater than the limit
1242 if (p1
.co
- p2
.co
).length
<= self
.crosshatch_merge_distance
:
1243 p1
.select_control_point
= True
1244 p1
.select_left_handle
= True
1245 p1
.select_right_handle
= True
1247 p2
.select_control_point
= True
1248 p2
.select_left_handle
= True
1249 p2
.select_right_handle
= True
1251 if (p1
.co
- p2
.co
).length
<= self
.crosshatch_merge_distance
:
1252 p3
.select_control_point
= True
1253 p3
.select_left_handle
= True
1254 p3
.select_right_handle
= True
1258 sp
.bezier_points
[0].select_control_point
= False
1259 sp
.bezier_points
[0].select_left_handle
= False
1260 sp
.bezier_points
[0].select_right_handle
= False
1262 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_control_point
= False
1263 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_left_handle
= False
1264 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_right_handle
= False
1266 # Smooth out strokes a little to improve crosshatch detection
1267 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1270 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1272 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1273 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1275 # Simplify the splines
1276 for sp
in ob_splines
.data
.splines
:
1279 sp
.bezier_points
[0].select_control_point
= True
1280 sp
.bezier_points
[0].select_left_handle
= True
1281 sp
.bezier_points
[0].select_right_handle
= True
1283 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_control_point
= True
1284 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_left_handle
= True
1285 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_right_handle
= True
1287 angle_limit
= 15 # Degrees
1288 for t
in range(len(sp
.bezier_points
)):
1289 # Because on each iteration it checks the "next two points"
1290 # of the actual. This way it doesn't go out of range
1291 if t
<= len(sp
.bezier_points
) - 3:
1292 p1
= sp
.bezier_points
[t
]
1293 p2
= sp
.bezier_points
[t
+ 1]
1294 p3
= sp
.bezier_points
[t
+ 2]
1296 vec_1
= p1
.co
- p2
.co
1297 vec_2
= p2
.co
- p3
.co
1299 if p2
.co
!= p1
.co
and p2
.co
!= p3
.co
:
1300 angle
= vec_1
.angle(vec_2
)
1301 angle_sum
+= degrees(angle
)
1302 # If sum of angles is grater than the limit
1303 if angle_sum
>= angle_limit
:
1304 p1
.select_control_point
= True
1305 p1
.select_left_handle
= True
1306 p1
.select_right_handle
= True
1308 p2
.select_control_point
= True
1309 p2
.select_left_handle
= True
1310 p2
.select_right_handle
= True
1312 p3
.select_control_point
= True
1313 p3
.select_left_handle
= True
1314 p3
.select_right_handle
= True
1318 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1319 bpy
.ops
.curve
.select_all(action
='INVERT')
1321 bpy
.ops
.curve
.delete(type='VERT')
1322 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1324 objects_to_delete
.append(ob_splines
)
1326 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1327 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1328 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1330 # Check if the strokes are a crosshatch
1331 if self
.is_crosshatch
:
1332 all_points_coords
= []
1333 for i
in range(len(ob_splines
.data
.splines
)):
1334 all_points_coords
.append([])
1336 all_points_coords
[i
] = [Vector((x
, y
, z
)) for
1337 x
, y
, z
in [bp
.co
for
1338 bp
in ob_splines
.data
.splines
[i
].bezier_points
]]
1340 all_intersections
= []
1341 checked_splines
= []
1342 for i
in range(len(all_points_coords
)):
1344 for t
in range(len(all_points_coords
[i
]) - 1):
1345 bp1_co
= all_points_coords
[i
][t
]
1346 bp2_co
= all_points_coords
[i
][t
+ 1]
1348 for i2
in range(len(all_points_coords
)):
1349 if i
!= i2
and i2
not in checked_splines
:
1350 for t2
in range(len(all_points_coords
[i2
]) - 1):
1351 bp3_co
= all_points_coords
[i2
][t2
]
1352 bp4_co
= all_points_coords
[i2
][t2
+ 1]
1354 intersec_coords
= intersect_line_line(
1355 bp1_co
, bp2_co
, bp3_co
, bp4_co
1357 if intersec_coords
is not None:
1358 dist
= (intersec_coords
[0] - intersec_coords
[1]).length
1360 if dist
<= self
.crosshatch_merge_distance
* 1.5:
1361 _temp_co
, percent1
= intersect_point_line(
1362 intersec_coords
[0], bp1_co
, bp2_co
1364 if (percent1
>= -0.02 and percent1
<= 1.02):
1365 _temp_co
, percent2
= intersect_point_line(
1366 intersec_coords
[1], bp3_co
, bp4_co
1368 if (percent2
>= -0.02 and percent2
<= 1.02):
1369 # Format: spline index, first point index from
1370 # corresponding segment, percentage from first point of
1371 # actual segment, coords of intersection point
1372 all_intersections
.append(
1374 ob_splines
.matrix_world
@ intersec_coords
[0])
1376 all_intersections
.append(
1378 ob_splines
.matrix_world
@ intersec_coords
[1])
1381 checked_splines
.append(i
)
1382 # Sort list by spline, then by corresponding first point index of segment,
1383 # and then by percentage from first point of segment: elements 0 and 1 respectively
1384 all_intersections
.sort(key
=operator
.itemgetter(0, 1, 2))
1386 self
.crosshatch_strokes_coords
= {}
1387 for i
in range(len(all_intersections
)):
1388 if not all_intersections
[i
][0] in self
.crosshatch_strokes_coords
:
1389 self
.crosshatch_strokes_coords
[all_intersections
[i
][0]] = []
1391 self
.crosshatch_strokes_coords
[all_intersections
[i
][0]].append(
1392 all_intersections
[i
][3]
1393 ) # Save intersection coords
1395 self
.is_crosshatch
= False
1397 # Delete all duplicates
1398 bpy
.ops
.object.delete({"selected_objects": objects_to_delete
})
1400 # If the main object has modifiers, turn their "viewport view status" to
1401 # what it was before the forced deactivation above
1402 if len(self
.main_object
.modifiers
) > 0:
1403 for m_idx
in range(len(self
.main_object
.modifiers
)):
1404 self
.main_object
.modifiers
[m_idx
].show_viewport
= self
.modifiers_prev_viewport_state
[m_idx
]
1410 # Part of the Crosshatch process that is repeated when the operator is tweaked
1411 def crosshatch_surface_execute(self
, context
):
1412 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1413 # (without this the surface verts merging with the main object doesn't work well)
1414 self
.modifiers_prev_viewport_state
= []
1415 if len(self
.main_object
.modifiers
) > 0:
1416 for m_idx
in range(len(self
.main_object
.modifiers
)):
1417 self
.modifiers_prev_viewport_state
.append(self
.main_object
.modifiers
[m_idx
].show_viewport
)
1419 self
.main_object
.modifiers
[m_idx
].show_viewport
= False
1421 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1423 me_name
= "SURFSKIO_STK_TMP"
1424 me
= bpy
.data
.meshes
.new(me_name
)
1426 all_verts_coords
= []
1428 for st_idx
in self
.crosshatch_strokes_coords
:
1429 for co_idx
in range(len(self
.crosshatch_strokes_coords
[st_idx
])):
1430 coords
= self
.crosshatch_strokes_coords
[st_idx
][co_idx
]
1432 all_verts_coords
.append(coords
)
1435 all_edges
.append((len(all_verts_coords
) - 2, len(all_verts_coords
) - 1))
1437 me
.from_pydata(all_verts_coords
, all_edges
, [])
1438 ob
= object_utils
.object_data_add(context
, me
)
1440 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1442 bpy
.context
.view_layer
.objects
.active
= ob
1444 # Get together each vert and its nearest, to the middle position
1445 verts
= ob
.data
.vertices
1447 for i
in range(len(verts
)):
1448 shortest_dist
= None
1450 if i
not in checked_verts
:
1451 for t
in range(len(verts
)):
1452 if i
!= t
and t
not in checked_verts
:
1453 dist
= (verts
[i
].co
- verts
[t
].co
).length
1455 if shortest_dist
is not None:
1456 if dist
< shortest_dist
:
1457 shortest_dist
= dist
1460 shortest_dist
= dist
1463 middle_location
= (verts
[i
].co
+ verts
[nearest_vert
].co
) / 2
1465 verts
[i
].co
= middle_location
1466 verts
[nearest_vert
].co
= middle_location
1468 checked_verts
.append(i
)
1469 checked_verts
.append(nearest_vert
)
1471 # Calculate average length between all the generated edges
1472 ob
= bpy
.context
.object
1474 for ed
in ob
.data
.edges
:
1475 v1
= ob
.data
.vertices
[ed
.vertices
[0]]
1476 v2
= ob
.data
.vertices
[ed
.vertices
[1]]
1478 lengths_sum
+= (v1
.co
- v2
.co
).length
1480 edges_count
= len(ob
.data
.edges
)
1481 # possible division by zero here
1482 average_edge_length
= lengths_sum
/ edges_count
if edges_count
!= 0 else 0.0001
1484 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1485 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1486 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN',
1487 threshold
=average_edge_length
/ 15.0)
1488 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1490 final_points_ob
= bpy
.context
.view_layer
.objects
.active
1492 # Make a dictionary with the verts related to each vert
1493 related_key_verts
= {}
1494 for ed
in final_points_ob
.data
.edges
:
1495 if not ed
.vertices
[0] in related_key_verts
:
1496 related_key_verts
[ed
.vertices
[0]] = []
1498 if not ed
.vertices
[1] in related_key_verts
:
1499 related_key_verts
[ed
.vertices
[1]] = []
1501 if not ed
.vertices
[1] in related_key_verts
[ed
.vertices
[0]]:
1502 related_key_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
1504 if not ed
.vertices
[0] in related_key_verts
[ed
.vertices
[1]]:
1505 related_key_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
1507 # Get groups of verts forming each face
1508 faces_verts_idx
= []
1509 for v1
in related_key_verts
: # verts-1 ....
1510 for v2
in related_key_verts
: # verts-2
1512 related_verts_in_common
= []
1513 v2_in_rel_v1
= False
1514 v1_in_rel_v2
= False
1515 for rel_v1
in related_key_verts
[v1
]:
1516 # Check if related verts of verts-1 are related verts of verts-2
1517 if rel_v1
in related_key_verts
[v2
]:
1518 related_verts_in_common
.append(rel_v1
)
1520 if v2
in related_key_verts
[v1
]:
1523 if v1
in related_key_verts
[v2
]:
1526 repeated_face
= False
1527 # If two verts have two related verts in common, they form a quad
1528 if len(related_verts_in_common
) == 2:
1529 # Check if the face is already saved
1530 for f_verts
in faces_verts_idx
:
1533 if len(f_verts
) == 4:
1538 if related_verts_in_common
[0] in f_verts
:
1540 if related_verts_in_common
[1] in f_verts
:
1543 if repeated_verts
== len(f_verts
):
1544 repeated_face
= True
1547 if not repeated_face
:
1548 faces_verts_idx
.append([v1
, related_verts_in_common
[0],
1549 v2
, related_verts_in_common
[1]])
1551 # If Two verts have one related vert in common and they are
1552 # related to each other, they form a triangle
1553 elif v2_in_rel_v1
and v1_in_rel_v2
and len(related_verts_in_common
) == 1:
1554 # Check if the face is already saved.
1555 for f_verts
in faces_verts_idx
:
1558 if len(f_verts
) == 3:
1563 if related_verts_in_common
[0] in f_verts
:
1566 if repeated_verts
== len(f_verts
):
1567 repeated_face
= True
1570 if not repeated_face
:
1571 faces_verts_idx
.append([v1
, related_verts_in_common
[0], v2
])
1573 # Keep only the faces that don't overlap by ignoring
1574 # quads that overlap with two adjacent triangles
1575 faces_to_not_include_idx
= [] # Indices of faces_verts_idx to eliminate
1576 for i
in range(len(faces_verts_idx
)):
1577 for t
in range(len(faces_verts_idx
)):
1581 if len(faces_verts_idx
[i
]) == 4 and len(faces_verts_idx
[t
]) == 3:
1582 for v_idx
in faces_verts_idx
[t
]:
1583 if v_idx
in faces_verts_idx
[i
]:
1584 verts_in_common
+= 1
1585 # If it doesn't have all it's vertices repeated in the other face
1586 if verts_in_common
== 3:
1587 if i
not in faces_to_not_include_idx
:
1588 faces_to_not_include_idx
.append(i
)
1591 all_surface_verts_co
= []
1592 for i
in range(len(final_points_ob
.data
.vertices
)):
1593 coords
= final_points_ob
.data
.vertices
[i
].co
1594 all_surface_verts_co
.append([coords
[0], coords
[1], coords
[2]])
1596 # Verts of each face.
1597 all_surface_faces
= []
1598 for i
in range(len(faces_verts_idx
)):
1599 if i
not in faces_to_not_include_idx
:
1601 for v_idx
in faces_verts_idx
[i
]:
1604 all_surface_faces
.append(face
)
1607 surf_me_name
= "SURFSKIO_surface"
1608 me_surf
= bpy
.data
.meshes
.new(surf_me_name
)
1609 me_surf
.from_pydata(all_surface_verts_co
, [], all_surface_faces
)
1610 ob_surface
= object_utils
.object_data_add(context
, me_surf
)
1612 # Delete final points temporal object
1613 bpy
.ops
.object.delete({"selected_objects": [final_points_ob
]})
1615 # Delete isolated verts if there are any
1616 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1617 ob_surface
.select_set(True)
1618 bpy
.context
.view_layer
.objects
.active
= ob_surface
1620 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1621 bpy
.ops
.mesh
.select_all(action
='DESELECT')
1622 bpy
.ops
.mesh
.select_face_by_sides(type='NOTEQUAL')
1623 bpy
.ops
.mesh
.delete()
1624 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1626 # Join crosshatch results with original mesh
1628 # Calculate a distance to merge the verts of the crosshatch surface to the main object
1629 edges_length_sum
= 0
1630 for ed
in ob_surface
.data
.edges
:
1631 edges_length_sum
+= (
1632 ob_surface
.data
.vertices
[ed
.vertices
[0]].co
-
1633 ob_surface
.data
.vertices
[ed
.vertices
[1]].co
1636 # Make dictionary with all the verts connected to each vert, on the new surface object.
1637 surface_connected_verts
= {}
1638 for ed
in ob_surface
.data
.edges
:
1639 if not ed
.vertices
[0] in surface_connected_verts
:
1640 surface_connected_verts
[ed
.vertices
[0]] = []
1642 surface_connected_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
1644 if ed
.vertices
[1] not in surface_connected_verts
:
1645 surface_connected_verts
[ed
.vertices
[1]] = []
1647 surface_connected_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
1649 # Duplicate the new surface object, and use shrinkwrap to
1650 # calculate later the nearest verts to the main object
1651 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1652 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1653 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1655 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1657 final_ob_duplicate
= bpy
.context
.view_layer
.objects
.active
1659 bpy
.ops
.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
1660 shrinkwrap_modifier
= final_ob_duplicate
.modifiers
[-1]
1661 shrinkwrap_modifier
.wrap_method
= "NEAREST_VERTEX"
1662 shrinkwrap_modifier
.target
= self
.main_object
1664 bpy
.ops
.object.modifier_apply('INVOKE_REGION_WIN', apply_as
='DATA', modifier
=shrinkwrap_modifier
.name
)
1666 # Make list with verts of original mesh as index and coords as value
1667 main_object_verts_coords
= []
1668 for v
in self
.main_object
.data
.vertices
:
1669 coords
= self
.main_object
.matrix_world
@ v
.co
1671 # To avoid problems when taking "-0.00" as a different value as "0.00"
1672 for c
in range(len(coords
)):
1673 if "%.3f" % coords
[c
] == "-0.00":
1676 main_object_verts_coords
.append(["%.3f" % coords
[0], "%.3f" % coords
[1], "%.3f" % coords
[2]])
1678 tuple(main_object_verts_coords
)
1680 # Determine which verts will be merged, snap them to the nearest verts
1681 # on the original verts, and get them selected
1682 crosshatch_verts_to_merge
= []
1683 if self
.automatic_join
:
1684 for i
in range(len(ob_surface
.data
.vertices
)-1):
1685 # Calculate the distance from each of the connected verts to the actual vert,
1686 # and compare it with the distance they would have if joined.
1687 # If they don't change much, that vert can be joined
1688 merge_actual_vert
= True
1690 if len(surface_connected_verts
[i
]) < 4:
1691 for c_v_idx
in surface_connected_verts
[i
]:
1692 points_original
= []
1693 points_original
.append(ob_surface
.data
.vertices
[c_v_idx
].co
)
1694 points_original
.append(ob_surface
.data
.vertices
[i
].co
)
1697 points_target
.append(ob_surface
.data
.vertices
[c_v_idx
].co
)
1698 points_target
.append(final_ob_duplicate
.data
.vertices
[i
].co
)
1700 vec_A
= points_original
[0] - points_original
[1]
1701 vec_B
= points_target
[0] - points_target
[1]
1703 dist_A
= (points_original
[0] - points_original
[1]).length
1704 dist_B
= (points_target
[0] - points_target
[1]).length
1707 points_original
[0] == points_original
[1] or
1708 points_target
[0] == points_target
[1]
1709 ): # If any vector's length is zero
1711 angle
= vec_A
.angle(vec_B
) / pi
1715 # Set a range of acceptable variation in the connected edges
1716 if dist_B
> dist_A
* 1.7 * self
.join_stretch_factor
or \
1717 dist_B
< dist_A
/ 2 / self
.join_stretch_factor
or \
1718 angle
>= 0.15 * self
.join_stretch_factor
:
1720 merge_actual_vert
= False
1723 merge_actual_vert
= False
1725 self
.report({'WARNING'},
1726 "Crosshatch set incorrectly")
1728 if merge_actual_vert
:
1729 coords
= final_ob_duplicate
.data
.vertices
[i
].co
1730 # To avoid problems when taking "-0.000" as a different value as "0.00"
1731 for c
in range(len(coords
)):
1732 if "%.3f" % coords
[c
] == "-0.00":
1735 comparison_coords
= ["%.3f" % coords
[0], "%.3f" % coords
[1], "%.3f" % coords
[2]]
1737 if comparison_coords
in main_object_verts_coords
:
1738 # Get the index of the vert with those coords in the main object
1739 main_object_related_vert_idx
= main_object_verts_coords
.index(comparison_coords
)
1741 if self
.main_object
.data
.vertices
[main_object_related_vert_idx
].select
is True or \
1742 self
.main_object_selected_verts_count
== 0:
1744 ob_surface
.data
.vertices
[i
].co
= final_ob_duplicate
.data
.vertices
[i
].co
1745 ob_surface
.data
.vertices
[i
].select
= True
1746 crosshatch_verts_to_merge
.append(i
)
1748 # Make sure the vert in the main object is selected,
1749 # in case it wasn't selected and the "join crosshatch" option is active
1750 self
.main_object
.data
.vertices
[main_object_related_vert_idx
].select
= True
1752 # Delete duplicated object
1753 bpy
.ops
.object.delete({"selected_objects": [final_ob_duplicate
]})
1755 # Join crosshatched surface and main object
1756 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1757 ob_surface
.select_set(True)
1758 self
.main_object
.select_set(True)
1759 bpy
.context
.view_layer
.objects
.active
= self
.main_object
1761 bpy
.ops
.object.join('INVOKE_REGION_WIN')
1763 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1764 # Perform Remove doubles to merge verts
1765 if not (self
.automatic_join
is False and self
.main_object_selected_verts_count
== 0):
1766 bpy
.ops
.mesh
.remove_doubles(threshold
=0.0001)
1768 bpy
.ops
.mesh
.select_all(action
='DESELECT')
1770 # If the main object has modifiers, turn their "viewport view status"
1771 # to what it was before the forced deactivation above
1772 if len(self
.main_object
.modifiers
) > 0:
1773 for m_idx
in range(len(self
.main_object
.modifiers
)):
1774 self
.main_object
.modifiers
[m_idx
].show_viewport
= self
.modifiers_prev_viewport_state
[m_idx
]
1780 def rectangular_surface(self
, context
):
1782 all_selected_edges_idx
= []
1783 all_selected_verts
= []
1785 for ed
in self
.main_object
.data
.edges
:
1787 all_selected_edges_idx
.append(ed
.index
)
1790 if not ed
.vertices
[0] in all_selected_verts
:
1791 all_selected_verts
.append(self
.main_object
.data
.vertices
[ed
.vertices
[0]])
1792 if not ed
.vertices
[1] in all_selected_verts
:
1793 all_selected_verts
.append(self
.main_object
.data
.vertices
[ed
.vertices
[1]])
1795 # All verts (both from each edge) to determine later
1796 # which are at the tips (those not repeated twice)
1797 all_verts_idx
.append(ed
.vertices
[0])
1798 all_verts_idx
.append(ed
.vertices
[1])
1800 # Identify the tips and "middle-vertex" that separates U from V, if there is one
1801 all_chains_tips_idx
= []
1802 for v_idx
in all_verts_idx
:
1803 if all_verts_idx
.count(v_idx
) < 2:
1804 all_chains_tips_idx
.append(v_idx
)
1806 edges_connected_to_tips
= []
1807 for ed
in self
.main_object
.data
.edges
:
1808 if (ed
.vertices
[0] in all_chains_tips_idx
or ed
.vertices
[1] in all_chains_tips_idx
) and \
1809 not (ed
.vertices
[0] in all_verts_idx
and ed
.vertices
[1] in all_verts_idx
):
1811 edges_connected_to_tips
.append(ed
)
1813 # Check closed selections
1814 # List with groups of three verts, where the first element of the pair is
1815 # the unselected vert of a closed selection and the other two elements are the
1816 # selected neighbor verts (it will be useful to determine which selection chain
1817 # the unselected vert belongs to, and determine the "middle-vertex")
1818 single_unselected_verts_and_neighbors
= []
1820 # To identify a "closed" selection (a selection that is a closed chain except
1821 # for one vertex) find the vertex in common that have the edges connected to tips.
1822 # If there is a vertex in common, that one is the unselected vert that closes
1823 # the selection or is a "middle-vertex"
1824 single_unselected_verts
= []
1825 for ed
in edges_connected_to_tips
:
1826 for ed_b
in edges_connected_to_tips
:
1828 if ed
.vertices
[0] == ed_b
.vertices
[0] and \
1829 not self
.main_object
.data
.vertices
[ed
.vertices
[0]].select
and \
1830 ed
.vertices
[0] not in single_unselected_verts
:
1832 # The second element is one of the tips of the selected
1833 # vertices of the closed selection
1834 single_unselected_verts_and_neighbors
.append(
1835 [ed
.vertices
[0], ed
.vertices
[1], ed_b
.vertices
[1]]
1837 single_unselected_verts
.append(ed
.vertices
[0])
1839 elif ed
.vertices
[0] == ed_b
.vertices
[1] and \
1840 not self
.main_object
.data
.vertices
[ed
.vertices
[0]].select
and \
1841 ed
.vertices
[0] not in single_unselected_verts
:
1843 single_unselected_verts_and_neighbors
.append(
1844 [ed
.vertices
[0], ed
.vertices
[1], ed_b
.vertices
[0]]
1846 single_unselected_verts
.append(ed
.vertices
[0])
1848 elif ed
.vertices
[1] == ed_b
.vertices
[0] and \
1849 not self
.main_object
.data
.vertices
[ed
.vertices
[1]].select
and \
1850 ed
.vertices
[1] not in single_unselected_verts
:
1852 single_unselected_verts_and_neighbors
.append(
1853 [ed
.vertices
[1], ed
.vertices
[0], ed_b
.vertices
[1]]
1855 single_unselected_verts
.append(ed
.vertices
[1])
1857 elif ed
.vertices
[1] == ed_b
.vertices
[1] and \
1858 not self
.main_object
.data
.vertices
[ed
.vertices
[1]].select
and \
1859 ed
.vertices
[1] not in single_unselected_verts
:
1861 single_unselected_verts_and_neighbors
.append(
1862 [ed
.vertices
[1], ed
.vertices
[0], ed_b
.vertices
[0]]
1864 single_unselected_verts
.append(ed
.vertices
[1])
1867 middle_vertex_idx
= None
1868 tips_to_discard_idx
= []
1870 # Check if there is a "middle-vertex", and get its index
1871 for i
in range(0, len(single_unselected_verts_and_neighbors
)):
1872 actual_chain_verts
= self
.get_ordered_verts(
1873 self
.main_object
, all_selected_edges_idx
,
1874 all_verts_idx
, single_unselected_verts_and_neighbors
[i
][1],
1878 if single_unselected_verts_and_neighbors
[i
][2] != \
1879 actual_chain_verts
[len(actual_chain_verts
) - 1].index
:
1881 middle_vertex_idx
= single_unselected_verts_and_neighbors
[i
][0]
1882 tips_to_discard_idx
.append(single_unselected_verts_and_neighbors
[i
][1])
1883 tips_to_discard_idx
.append(single_unselected_verts_and_neighbors
[i
][2])
1885 # List with pairs of verts that belong to the tips of each selection chain (row)
1886 verts_tips_same_chain_idx
= []
1887 if len(all_chains_tips_idx
) >= 2:
1889 for i
in range(0, len(all_chains_tips_idx
)):
1890 if all_chains_tips_idx
[i
] not in checked_v
:
1891 v_chain
= self
.get_ordered_verts(
1892 self
.main_object
, all_selected_edges_idx
,
1893 all_verts_idx
, all_chains_tips_idx
[i
],
1894 middle_vertex_idx
, None
1897 verts_tips_same_chain_idx
.append([v_chain
[0].index
, v_chain
[len(v_chain
) - 1].index
])
1899 checked_v
.append(v_chain
[0].index
)
1900 checked_v
.append(v_chain
[len(v_chain
) - 1].index
)
1902 # Selection tips (vertices).
1903 verts_tips_parsed_idx
= []
1904 if len(all_chains_tips_idx
) >= 2:
1905 for spec_v_idx
in all_chains_tips_idx
:
1906 if (spec_v_idx
not in tips_to_discard_idx
):
1907 verts_tips_parsed_idx
.append(spec_v_idx
)
1909 # Identify the type of selection made by the user
1910 if middle_vertex_idx
is not None:
1911 # If there are 4 tips (two selection chains), and
1912 # there is only one single unselected vert (the middle vert)
1913 if len(all_chains_tips_idx
) == 4 and len(single_unselected_verts_and_neighbors
) == 1:
1914 selection_type
= "TWO_CONNECTED"
1916 # The type of the selection was not identified, the script stops.
1917 self
.report({'WARNING'}, "The selection isn't valid.")
1919 self
.stopping_errors
= True
1923 if len(all_chains_tips_idx
) == 2: # If there are 2 tips
1924 selection_type
= "SINGLE"
1925 elif len(all_chains_tips_idx
) == 4: # If there are 4 tips
1926 selection_type
= "TWO_NOT_CONNECTED"
1927 elif len(all_chains_tips_idx
) == 0:
1928 if len(self
.main_splines
.data
.splines
) > 1:
1929 selection_type
= "NO_SELECTION"
1931 # If the selection was not identified and there is only one stroke,
1932 # there's no possibility to build a surface, so the script is interrupted
1933 self
.report({'WARNING'}, "The selection isn't valid.")
1935 self
.stopping_errors
= True
1939 # The type of the selection was not identified, the script stops
1940 self
.report({'WARNING'}, "The selection isn't valid.")
1942 self
.stopping_errors
= True
1946 # If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script
1947 if selection_type
== "TWO_NOT_CONNECTED" and len(self
.main_splines
.data
.splines
) == 1:
1948 self
.report({'WARNING'},
1949 "At least two strokes are needed when there are two not connected selections")
1951 self
.stopping_errors
= True
1955 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1957 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1958 self
.main_splines
.select_set(True)
1959 bpy
.context
.view_layer
.objects
.active
= self
.main_splines
1961 # Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out
1962 bpy
.ops
.object.editmode_toggle('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
.curve
.smooth('INVOKE_REGION_WIN')
1969 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1971 self
.selection_U_exists
= False
1972 self
.selection_U2_exists
= False
1973 self
.selection_V_exists
= False
1974 self
.selection_V2_exists
= False
1976 self
.selection_U_is_closed
= False
1977 self
.selection_U2_is_closed
= False
1978 self
.selection_V_is_closed
= False
1979 self
.selection_V2_is_closed
= False
1981 # Define what vertices are at the tips of each selection and are not the middle-vertex
1982 if selection_type
== "TWO_CONNECTED":
1983 self
.selection_U_exists
= True
1984 self
.selection_V_exists
= True
1986 closing_vert_U_idx
= None
1987 closing_vert_V_idx
= None
1988 closing_vert_U2_idx
= None
1989 closing_vert_V2_idx
= None
1991 # Determine which selection is Selection-U and which is Selection-V
1994 points_first_stroke_tips
= []
1997 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[verts_tips_parsed_idx
[0]].co
2000 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[middle_vertex_idx
].co
2003 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[verts_tips_parsed_idx
[1]].co
2006 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[middle_vertex_idx
].co
2008 points_first_stroke_tips
.append(
2009 self
.main_splines
.data
.splines
[0].bezier_points
[0].co
2011 points_first_stroke_tips
.append(
2012 self
.main_splines
.data
.splines
[0].bezier_points
[
2013 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2017 angle_A
= self
.orientation_difference(points_A
, points_first_stroke_tips
)
2018 angle_B
= self
.orientation_difference(points_B
, points_first_stroke_tips
)
2020 if angle_A
< angle_B
:
2021 first_vert_U_idx
= verts_tips_parsed_idx
[0]
2022 first_vert_V_idx
= verts_tips_parsed_idx
[1]
2024 first_vert_U_idx
= verts_tips_parsed_idx
[1]
2025 first_vert_V_idx
= verts_tips_parsed_idx
[0]
2027 elif selection_type
== "SINGLE" or selection_type
== "TWO_NOT_CONNECTED":
2028 first_sketched_point_first_stroke_co
= self
.main_splines
.data
.splines
[0].bezier_points
[0].co
2029 last_sketched_point_first_stroke_co
= \
2030 self
.main_splines
.data
.splines
[0].bezier_points
[
2031 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2033 first_sketched_point_last_stroke_co
= \
2034 self
.main_splines
.data
.splines
[
2035 len(self
.main_splines
.data
.splines
) - 1
2036 ].bezier_points
[0].co
2037 if len(self
.main_splines
.data
.splines
) > 1:
2038 first_sketched_point_second_stroke_co
= self
.main_splines
.data
.splines
[1].bezier_points
[0].co
2039 last_sketched_point_second_stroke_co
= \
2040 self
.main_splines
.data
.splines
[1].bezier_points
[
2041 len(self
.main_splines
.data
.splines
[1].bezier_points
) - 1
2044 single_unselected_neighbors
= [] # Only the neighbors of the single unselected verts
2045 for verts_neig_idx
in single_unselected_verts_and_neighbors
:
2046 single_unselected_neighbors
.append(verts_neig_idx
[1])
2047 single_unselected_neighbors
.append(verts_neig_idx
[2])
2049 all_chains_tips_and_middle_vert
= []
2050 for v_idx
in all_chains_tips_idx
:
2051 if v_idx
not in single_unselected_neighbors
:
2052 all_chains_tips_and_middle_vert
.append(v_idx
)
2054 all_chains_tips_and_middle_vert
+= single_unselected_verts
2056 all_participating_verts
= all_chains_tips_and_middle_vert
+ all_verts_idx
2058 # The tip of the selected vertices nearest to the first point of the first sketched stroke
2059 nearest_tip_to_first_st_first_pt_idx
, shortest_distance_to_first_stroke
= \
2060 self
.shortest_distance(
2062 first_sketched_point_first_stroke_co
,
2063 all_chains_tips_and_middle_vert
2065 # If the nearest tip is not from a closed selection, get the opposite tip vertex index
2066 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2067 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2069 nearest_tip_to_first_st_first_pt_opposite_idx
= \
2071 nearest_tip_to_first_st_first_pt_idx
,
2072 verts_tips_same_chain_idx
2074 # The tip of the selected vertices nearest to the last point of the first sketched stroke
2075 nearest_tip_to_first_st_last_pt_idx
, _temp_dist
= \
2076 self
.shortest_distance(
2078 last_sketched_point_first_stroke_co
,
2079 all_chains_tips_and_middle_vert
2081 # The tip of the selected vertices nearest to the first point of the last sketched stroke
2082 nearest_tip_to_last_st_first_pt_idx
, shortest_distance_to_last_stroke
= \
2083 self
.shortest_distance(
2085 first_sketched_point_last_stroke_co
,
2086 all_chains_tips_and_middle_vert
2088 if len(self
.main_splines
.data
.splines
) > 1:
2089 # The selected vertex nearest to the first point of the second sketched stroke
2090 # (This will be useful to determine the direction of the closed
2091 # selection V when extruding along strokes)
2092 nearest_vert_to_second_st_first_pt_idx
, _temp_dist
= \
2093 self
.shortest_distance(
2095 first_sketched_point_second_stroke_co
,
2098 # The selected vertex nearest to the first point of the second sketched stroke
2099 # (This will be useful to determine the direction of the closed
2100 # selection V2 when extruding along strokes)
2101 nearest_vert_to_second_st_last_pt_idx
, _temp_dist
= \
2102 self
.shortest_distance(
2104 last_sketched_point_second_stroke_co
,
2107 # Determine if the single selection will be treated as U or as V
2109 for i
in all_selected_edges_idx
:
2111 (self
.main_object
.matrix_world
@
2112 self
.main_object
.data
.vertices
[self
.main_object
.data
.edges
[i
].vertices
[0]].co
) -
2113 (self
.main_object
.matrix_world
@
2114 self
.main_object
.data
.vertices
[self
.main_object
.data
.edges
[i
].vertices
[1]].co
)
2117 average_edge_length
= edges_sum
/ len(all_selected_edges_idx
)
2119 # Get shortest distance from the first point of the last stroke to any participating vertex
2120 _temp_idx
, shortest_distance_to_last_stroke
= \
2121 self
.shortest_distance(
2123 first_sketched_point_last_stroke_co
,
2124 all_participating_verts
2126 # If the beginning of the first stroke is near enough, and its orientation
2127 # difference with the first edge of the nearest selection chain is not too high,
2128 # interpret things as an "extrude along strokes" instead of "extrude through strokes"
2129 if shortest_distance_to_first_stroke
< average_edge_length
/ 4 and \
2130 shortest_distance_to_last_stroke
< average_edge_length
and \
2131 len(self
.main_splines
.data
.splines
) > 1:
2133 self
.selection_U_exists
= False
2134 self
.selection_V_exists
= True
2135 # If the first selection is not closed
2136 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2137 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2138 self
.selection_V_is_closed
= False
2139 closing_vert_U_idx
= None
2140 closing_vert_U2_idx
= None
2141 closing_vert_V_idx
= None
2142 closing_vert_V2_idx
= None
2144 first_vert_V_idx
= nearest_tip_to_first_st_first_pt_idx
2146 if selection_type
== "TWO_NOT_CONNECTED":
2147 self
.selection_V2_exists
= True
2149 first_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2151 self
.selection_V_is_closed
= True
2152 closing_vert_V_idx
= nearest_tip_to_first_st_first_pt_idx
2154 # Get the neighbors of the first (unselected) vert of the closed selection U.
2156 for verts
in single_unselected_verts_and_neighbors
:
2157 if verts
[0] == nearest_tip_to_first_st_first_pt_idx
:
2158 vert_neighbors
.append(verts
[1])
2159 vert_neighbors
.append(verts
[2])
2162 verts_V
= self
.get_ordered_verts(
2163 self
.main_object
, all_selected_edges_idx
,
2164 all_verts_idx
, vert_neighbors
[0], middle_vertex_idx
, None
2167 for i
in range(0, len(verts_V
)):
2168 if verts_V
[i
].index
== nearest_vert_to_second_st_first_pt_idx
:
2169 # If the vertex nearest to the first point of the second stroke
2170 # is in the first half of the selected verts
2171 if i
>= len(verts_V
) / 2:
2172 first_vert_V_idx
= vert_neighbors
[1]
2175 first_vert_V_idx
= vert_neighbors
[0]
2178 if selection_type
== "TWO_NOT_CONNECTED":
2179 self
.selection_V2_exists
= True
2180 # If the second selection is not closed
2181 if nearest_tip_to_first_st_last_pt_idx
not in single_unselected_verts
or \
2182 nearest_tip_to_first_st_last_pt_idx
== middle_vertex_idx
:
2184 self
.selection_V2_is_closed
= False
2185 closing_vert_V2_idx
= None
2186 first_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2189 self
.selection_V2_is_closed
= True
2190 closing_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2192 # Get the neighbors of the first (unselected) vert of the closed selection U
2194 for verts
in single_unselected_verts_and_neighbors
:
2195 if verts
[0] == nearest_tip_to_first_st_last_pt_idx
:
2196 vert_neighbors
.append(verts
[1])
2197 vert_neighbors
.append(verts
[2])
2200 verts_V2
= self
.get_ordered_verts(
2201 self
.main_object
, all_selected_edges_idx
,
2202 all_verts_idx
, vert_neighbors
[0], middle_vertex_idx
, None
2205 for i
in range(0, len(verts_V2
)):
2206 if verts_V2
[i
].index
== nearest_vert_to_second_st_last_pt_idx
:
2207 # If the vertex nearest to the first point of the second stroke
2208 # is in the first half of the selected verts
2209 if i
>= len(verts_V2
) / 2:
2210 first_vert_V2_idx
= vert_neighbors
[1]
2213 first_vert_V2_idx
= vert_neighbors
[0]
2216 self
.selection_V2_exists
= False
2219 self
.selection_U_exists
= True
2220 self
.selection_V_exists
= False
2221 # If the first selection is not closed
2222 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2223 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2224 self
.selection_U_is_closed
= False
2225 closing_vert_U_idx
= None
2229 self
.main_object
.matrix_world
@
2230 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_idx
].co
2233 self
.main_object
.matrix_world
@
2234 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_opposite_idx
].co
2236 points_first_stroke_tips
= []
2237 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[0].co
)
2238 points_first_stroke_tips
.append(
2239 self
.main_splines
.data
.splines
[0].bezier_points
[
2240 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2243 vec_A
= points_tips
[0] - points_tips
[1]
2244 vec_B
= points_first_stroke_tips
[0] - points_first_stroke_tips
[1]
2246 # Compare the direction of the selection and the first
2247 # grease pencil stroke to determine which is the "first" vertex of the selection
2248 if vec_A
.dot(vec_B
) < 0:
2249 first_vert_U_idx
= nearest_tip_to_first_st_first_pt_opposite_idx
2251 first_vert_U_idx
= nearest_tip_to_first_st_first_pt_idx
2254 self
.selection_U_is_closed
= True
2255 closing_vert_U_idx
= nearest_tip_to_first_st_first_pt_idx
2257 # Get the neighbors of the first (unselected) vert of the closed selection U
2259 for verts
in single_unselected_verts_and_neighbors
:
2260 if verts
[0] == nearest_tip_to_first_st_first_pt_idx
:
2261 vert_neighbors
.append(verts
[1])
2262 vert_neighbors
.append(verts
[2])
2265 points_first_and_neighbor
= []
2266 points_first_and_neighbor
.append(
2267 self
.main_object
.matrix_world
@
2268 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_idx
].co
2270 points_first_and_neighbor
.append(
2271 self
.main_object
.matrix_world
@
2272 self
.main_object
.data
.vertices
[vert_neighbors
[0]].co
2274 points_first_stroke_tips
= []
2275 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[0].co
)
2276 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[1].co
)
2278 vec_A
= points_first_and_neighbor
[0] - points_first_and_neighbor
[1]
2279 vec_B
= points_first_stroke_tips
[0] - points_first_stroke_tips
[1]
2281 # Compare the direction of the selection and the first grease pencil stroke to
2282 # determine which is the vertex neighbor to the first vertex (unselected) of
2283 # the closed selection. This will determine the direction of the closed selection
2284 if vec_A
.dot(vec_B
) < 0:
2285 first_vert_U_idx
= vert_neighbors
[1]
2287 first_vert_U_idx
= vert_neighbors
[0]
2289 if selection_type
== "TWO_NOT_CONNECTED":
2290 self
.selection_U2_exists
= True
2291 # If the second selection is not closed
2292 if nearest_tip_to_last_st_first_pt_idx
not in single_unselected_verts
or \
2293 nearest_tip_to_last_st_first_pt_idx
== middle_vertex_idx
:
2295 self
.selection_U2_is_closed
= False
2296 closing_vert_U2_idx
= None
2297 first_vert_U2_idx
= nearest_tip_to_last_st_first_pt_idx
2299 self
.selection_U2_is_closed
= True
2300 closing_vert_U2_idx
= nearest_tip_to_last_st_first_pt_idx
2302 # Get the neighbors of the first (unselected) vert of the closed selection U
2304 for verts
in single_unselected_verts_and_neighbors
:
2305 if verts
[0] == nearest_tip_to_last_st_first_pt_idx
:
2306 vert_neighbors
.append(verts
[1])
2307 vert_neighbors
.append(verts
[2])
2310 points_first_and_neighbor
= []
2311 points_first_and_neighbor
.append(
2312 self
.main_object
.matrix_world
@
2313 self
.main_object
.data
.vertices
[nearest_tip_to_last_st_first_pt_idx
].co
2315 points_first_and_neighbor
.append(
2316 self
.main_object
.matrix_world
@
2317 self
.main_object
.data
.vertices
[vert_neighbors
[0]].co
2319 points_last_stroke_tips
= []
2320 points_last_stroke_tips
.append(
2321 self
.main_splines
.data
.splines
[
2322 len(self
.main_splines
.data
.splines
) - 1
2323 ].bezier_points
[0].co
2325 points_last_stroke_tips
.append(
2326 self
.main_splines
.data
.splines
[
2327 len(self
.main_splines
.data
.splines
) - 1
2328 ].bezier_points
[1].co
2330 vec_A
= points_first_and_neighbor
[0] - points_first_and_neighbor
[1]
2331 vec_B
= points_last_stroke_tips
[0] - points_last_stroke_tips
[1]
2333 # Compare the direction of the selection and the last grease pencil stroke to
2334 # determine which is the vertex neighbor to the first vertex (unselected) of
2335 # the closed selection. This will determine the direction of the closed selection
2336 if vec_A
.dot(vec_B
) < 0:
2337 first_vert_U2_idx
= vert_neighbors
[1]
2339 first_vert_U2_idx
= vert_neighbors
[0]
2341 self
.selection_U2_exists
= False
2343 elif selection_type
== "NO_SELECTION":
2344 self
.selection_U_exists
= False
2345 self
.selection_V_exists
= False
2347 # Get an ordered list of the vertices of Selection-U
2348 verts_ordered_U
= []
2349 if self
.selection_U_exists
:
2350 verts_ordered_U
= self
.get_ordered_verts(
2351 self
.main_object
, all_selected_edges_idx
,
2352 all_verts_idx
, first_vert_U_idx
,
2353 middle_vertex_idx
, closing_vert_U_idx
2356 # Get an ordered list of the vertices of Selection-U2
2357 verts_ordered_U2
= []
2358 if self
.selection_U2_exists
:
2359 verts_ordered_U2
= self
.get_ordered_verts(
2360 self
.main_object
, all_selected_edges_idx
,
2361 all_verts_idx
, first_vert_U2_idx
,
2362 middle_vertex_idx
, closing_vert_U2_idx
2365 # Get an ordered list of the vertices of Selection-V
2366 verts_ordered_V
= []
2367 if self
.selection_V_exists
:
2368 verts_ordered_V
= self
.get_ordered_verts(
2369 self
.main_object
, all_selected_edges_idx
,
2370 all_verts_idx
, first_vert_V_idx
,
2371 middle_vertex_idx
, closing_vert_V_idx
2373 verts_ordered_V_indices
= [x
.index
for x
in verts_ordered_V
]
2375 # Get an ordered list of the vertices of Selection-V2
2376 verts_ordered_V2
= []
2377 if self
.selection_V2_exists
:
2378 verts_ordered_V2
= self
.get_ordered_verts(
2379 self
.main_object
, all_selected_edges_idx
,
2380 all_verts_idx
, first_vert_V2_idx
,
2381 middle_vertex_idx
, closing_vert_V2_idx
2384 # Check if when there are two-not-connected selections both have the same
2385 # number of verts. If not terminate the script
2386 if ((self
.selection_U2_exists
and len(verts_ordered_U
) != len(verts_ordered_U2
)) or
2387 (self
.selection_V2_exists
and len(verts_ordered_V
) != len(verts_ordered_V2
))):
2389 self
.report({'WARNING'}, "Both selections must have the same number of edges")
2391 self
.stopping_errors
= True
2395 # Calculate edges U proportions
2396 # Sum selected edges U lengths
2397 edges_lengths_U
= []
2398 edges_lengths_sum_U
= 0
2400 if self
.selection_U_exists
:
2401 edges_lengths_U
, edges_lengths_sum_U
= self
.get_chain_length(
2405 if self
.selection_U2_exists
:
2406 edges_lengths_U2
, edges_lengths_sum_U2
= self
.get_chain_length(
2410 # Sum selected edges V lengths
2411 edges_lengths_V
= []
2412 edges_lengths_sum_V
= 0
2414 if self
.selection_V_exists
:
2415 edges_lengths_V
, edges_lengths_sum_V
= self
.get_chain_length(
2419 if self
.selection_V2_exists
:
2420 edges_lengths_V2
, edges_lengths_sum_V2
= self
.get_chain_length(
2425 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2426 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN',
2427 number_cuts
=bpy
.context
.scene
.bsurfaces
.SURFSK_precision
)
2428 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2431 edges_proportions_U
= []
2432 edges_proportions_U
= self
.get_edges_proportions(
2433 edges_lengths_U
, edges_lengths_sum_U
,
2434 self
.selection_U_exists
, self
.edges_U
2436 verts_count_U
= len(edges_proportions_U
) + 1
2438 if self
.selection_U2_exists
:
2439 edges_proportions_U2
= []
2440 edges_proportions_U2
= self
.get_edges_proportions(
2441 edges_lengths_U2
, edges_lengths_sum_U2
,
2442 self
.selection_U2_exists
, self
.edges_V
2446 edges_proportions_V
= []
2447 edges_proportions_V
= self
.get_edges_proportions(
2448 edges_lengths_V
, edges_lengths_sum_V
,
2449 self
.selection_V_exists
, self
.edges_V
2452 if self
.selection_V2_exists
:
2453 edges_proportions_V2
= []
2454 edges_proportions_V2
= self
.get_edges_proportions(
2455 edges_lengths_V2
, edges_lengths_sum_V2
,
2456 self
.selection_V2_exists
, self
.edges_V
2459 # Cyclic Follow: simplify sketched curves, make them Cyclic, and complete
2460 # the actual sketched curves with a "closing segment"
2461 if self
.cyclic_follow
and not self
.selection_V_exists
and not \
2462 ((self
.selection_U_exists
and not self
.selection_U_is_closed
) or
2463 (self
.selection_U2_exists
and not self
.selection_U2_is_closed
)):
2465 simplified_spline_coords
= []
2466 simplified_curve
= []
2467 ob_simplified_curve
= []
2468 splines_first_v_co
= []
2469 for i
in range(len(self
.main_splines
.data
.splines
)):
2470 # Create a curve object for the actual spline "cyclic extension"
2471 simplified_curve
.append(bpy
.data
.curves
.new('SURFSKIO_simpl_crv', 'CURVE'))
2472 ob_simplified_curve
.append(bpy
.data
.objects
.new('SURFSKIO_simpl_crv', simplified_curve
[i
]))
2473 bpy
.context
.collection
.objects
.link(ob_simplified_curve
[i
])
2475 simplified_curve
[i
].dimensions
= "3D"
2478 for bp
in self
.main_splines
.data
.splines
[i
].bezier_points
:
2479 spline_coords
.append(bp
.co
)
2482 simplified_spline_coords
.append(self
.simplify_spline(spline_coords
, 5))
2484 # Get the coordinates of the first vert of the actual spline
2485 splines_first_v_co
.append(simplified_spline_coords
[i
][0])
2487 # Generate the spline
2488 spline
= simplified_curve
[i
].splines
.new('BEZIER')
2489 # less one because one point is added when the spline is created
2490 spline
.bezier_points
.add(len(simplified_spline_coords
[i
]) - 1)
2491 for p
in range(0, len(simplified_spline_coords
[i
])):
2492 spline
.bezier_points
[p
].co
= simplified_spline_coords
[i
][p
]
2494 spline
.use_cyclic_u
= True
2496 spline_bp_count
= len(spline
.bezier_points
)
2498 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2499 ob_simplified_curve
[i
].select_set(True)
2500 bpy
.context
.view_layer
.objects
.active
= ob_simplified_curve
[i
]
2502 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2503 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
2504 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2505 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2506 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2508 # Select the "closing segment", and subdivide it
2509 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_control_point
= True
2510 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_left_handle
= True
2511 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_right_handle
= True
2513 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_control_point
= True
2514 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_left_handle
= True
2515 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_right_handle
= True
2517 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2519 (ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].co
-
2520 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].co
).length
/
2521 self
.average_gp_segment_length
2524 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=segments
)
2526 # Delete the other vertices and make it non-cyclic to
2527 # keep only the needed verts of the "closing segment"
2528 bpy
.ops
.curve
.select_all(action
='INVERT')
2529 bpy
.ops
.curve
.delete(type='VERT')
2530 ob_simplified_curve
[i
].data
.splines
[0].use_cyclic_u
= False
2531 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2533 # Add the points of the "closing segment" to the original curve from grease pencil stroke
2534 first_new_index
= len(self
.main_splines
.data
.splines
[i
].bezier_points
)
2535 self
.main_splines
.data
.splines
[i
].bezier_points
.add(
2536 len(ob_simplified_curve
[i
].data
.splines
[0].bezier_points
) - 1
2538 for t
in range(1, len(ob_simplified_curve
[i
].data
.splines
[0].bezier_points
)):
2539 self
.main_splines
.data
.splines
[i
].bezier_points
[t
- 1 + first_new_index
].co
= \
2540 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[t
].co
2542 # Delete the temporal curve
2543 bpy
.ops
.object.delete({"selected_objects": [ob_simplified_curve
[i
]]})
2545 # Get the coords of the points distributed along the sketched strokes,
2546 # with proportions-U of the first selection
2547 pts_on_strokes_with_proportions_U
= self
.distribute_pts(
2548 self
.main_splines
.data
.splines
,
2551 sketched_splines_parsed
= []
2553 if self
.selection_U2_exists
:
2554 # Initialize the multidimensional list with the proportions of all the segments
2555 proportions_loops_crossing_strokes
= []
2556 for i
in range(len(pts_on_strokes_with_proportions_U
)):
2557 proportions_loops_crossing_strokes
.append([])
2559 for t
in range(len(pts_on_strokes_with_proportions_U
[0])):
2560 proportions_loops_crossing_strokes
[i
].append(None)
2562 # Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U
2563 for lp
in range(len(pts_on_strokes_with_proportions_U
[0])):
2564 loop_segments_lengths
= []
2566 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2567 # When on the first stroke, add the segment from the selection to the dirst stroke
2569 loop_segments_lengths
.append(
2570 ((self
.main_object
.matrix_world
@ verts_ordered_U
[lp
].co
) -
2571 pts_on_strokes_with_proportions_U
[0][lp
]).length
2573 # For all strokes except for the last, calculate the distance
2574 # from the actual stroke to the next
2575 if st
!= len(pts_on_strokes_with_proportions_U
) - 1:
2576 loop_segments_lengths
.append(
2577 (pts_on_strokes_with_proportions_U
[st
][lp
] -
2578 pts_on_strokes_with_proportions_U
[st
+ 1][lp
]).length
2580 # When on the last stroke, add the segments
2581 # from the last stroke to the second selection
2582 if st
== len(pts_on_strokes_with_proportions_U
) - 1:
2583 loop_segments_lengths
.append(
2584 (pts_on_strokes_with_proportions_U
[st
][lp
] -
2585 (self
.main_object
.matrix_world
@ verts_ordered_U2
[lp
].co
)).length
2587 # Calculate full loop length
2588 loop_seg_lengths_sum
= 0
2589 for i
in range(len(loop_segments_lengths
)):
2590 loop_seg_lengths_sum
+= loop_segments_lengths
[i
]
2592 # Fill the multidimensional list with the proportions of all the segments
2593 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2594 proportions_loops_crossing_strokes
[st
][lp
] = \
2595 loop_segments_lengths
[st
] / loop_seg_lengths_sum
2597 # Calculate proportions for each stroke
2598 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2599 actual_stroke_spline
= []
2600 # Needs to be a list for the "distribute_pts" method
2601 actual_stroke_spline
.append(self
.main_splines
.data
.splines
[st
])
2603 # Calculate the proportions for the actual stroke.
2604 actual_edges_proportions_U
= []
2605 for i
in range(len(edges_proportions_U
)):
2608 # Sum the proportions of this loop up to the actual.
2609 for t
in range(0, st
+ 1):
2610 proportions_sum
+= proportions_loops_crossing_strokes
[t
][i
]
2611 # i + 1, because proportions_loops_crossing_strokes refers to loops,
2612 # and the proportions refer to edges, so we start at the element 1
2613 # of proportions_loops_crossing_strokes instead of element 0
2614 actual_edges_proportions_U
.append(
2615 edges_proportions_U
[i
] -
2616 ((edges_proportions_U
[i
] - edges_proportions_U2
[i
]) * proportions_sum
)
2618 points_actual_spline
= self
.distribute_pts(actual_stroke_spline
, actual_edges_proportions_U
)
2619 sketched_splines_parsed
.append(points_actual_spline
[0])
2621 sketched_splines_parsed
= pts_on_strokes_with_proportions_U
2623 # If the selection type is "TWO_NOT_CONNECTED" replace the
2624 # points of the last spline with the points in the "target" selection
2625 if selection_type
== "TWO_NOT_CONNECTED":
2626 if self
.selection_U2_exists
:
2627 for i
in range(0, len(sketched_splines_parsed
[len(sketched_splines_parsed
) - 1])):
2628 sketched_splines_parsed
[len(sketched_splines_parsed
) - 1][i
] = \
2629 self
.main_object
.matrix_world
@ verts_ordered_U2
[i
].co
2631 # Create temporary curves along the "control-points" found
2632 # on the sketched curves and the mesh selection
2633 mesh_ctrl_pts_name
= "SURFSKIO_ctrl_pts"
2634 me
= bpy
.data
.meshes
.new(mesh_ctrl_pts_name
)
2635 ob_ctrl_pts
= bpy
.data
.objects
.new(mesh_ctrl_pts_name
, me
)
2636 ob_ctrl_pts
.data
= me
2637 bpy
.context
.collection
.objects
.link(ob_ctrl_pts
)
2644 for i
in range(0, verts_count_U
):
2645 vert_num_in_spline
= 1
2647 if self
.selection_U_exists
:
2648 ob_ctrl_pts
.data
.vertices
.add(1)
2649 last_v
= ob_ctrl_pts
.data
.vertices
[len(ob_ctrl_pts
.data
.vertices
) - 1]
2650 last_v
.co
= self
.main_object
.matrix_world
@ verts_ordered_U
[i
].co
2652 vert_num_in_spline
+= 1
2654 for t
in range(0, len(sketched_splines_parsed
)):
2655 ob_ctrl_pts
.data
.vertices
.add(1)
2656 v
= ob_ctrl_pts
.data
.vertices
[len(ob_ctrl_pts
.data
.vertices
) - 1]
2657 v
.co
= sketched_splines_parsed
[t
][i
]
2659 if vert_num_in_spline
> 1:
2660 ob_ctrl_pts
.data
.edges
.add(1)
2661 ob_ctrl_pts
.data
.edges
[len(ob_ctrl_pts
.data
.edges
) - 1].vertices
[0] = \
2662 len(ob_ctrl_pts
.data
.vertices
) - 2
2663 ob_ctrl_pts
.data
.edges
[len(ob_ctrl_pts
.data
.edges
) - 1].vertices
[1] = \
2664 len(ob_ctrl_pts
.data
.vertices
) - 1
2667 first_verts
.append(v
.index
)
2670 second_verts
.append(v
.index
)
2672 if t
== len(sketched_splines_parsed
) - 1:
2673 last_verts
.append(v
.index
)
2676 vert_num_in_spline
+= 1
2678 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2679 ob_ctrl_pts
.select_set(True)
2680 bpy
.context
.view_layer
.objects
.active
= ob_ctrl_pts
2682 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2683 bpy
.ops
.mesh
.select_all(action
='DESELECT')
2684 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2686 # Determine which loops-U will be "Cyclic"
2687 for i
in range(0, len(first_verts
)):
2688 # When there is Cyclic Cross there is no need of
2689 # Automatic Join, (and there are at least three strokes)
2690 if self
.automatic_join
and not self
.cyclic_cross
and \
2691 selection_type
!= "TWO_CONNECTED" and len(self
.main_splines
.data
.splines
) >= 3:
2693 v
= ob_ctrl_pts
.data
.vertices
2694 first_point_co
= v
[first_verts
[i
]].co
2695 second_point_co
= v
[second_verts
[i
]].co
2696 last_point_co
= v
[last_verts
[i
]].co
2698 # Coordinates of the point in the center of both the first and last verts.
2700 (first_point_co
[0] + last_point_co
[0]) / 2,
2701 (first_point_co
[1] + last_point_co
[1]) / 2,
2702 (first_point_co
[2] + last_point_co
[2]) / 2
2704 vec_A
= second_point_co
- first_point_co
2705 vec_B
= second_point_co
- Vector(verts_center_co
)
2707 # Calculate the length of the first segment of the loop,
2708 # and the length it would have after moving the first vert
2709 # to the middle position between first and last
2710 length_original
= (second_point_co
- first_point_co
).length
2711 length_target
= (second_point_co
- Vector(verts_center_co
)).length
2713 angle
= vec_A
.angle(vec_B
) / pi
2715 # If the target length doesn't stretch too much, and the
2716 # its angle doesn't change to much either
2717 if length_target
<= length_original
* 1.03 * self
.join_stretch_factor
and \
2718 angle
<= 0.008 * self
.join_stretch_factor
and not self
.selection_U_exists
:
2720 cyclic_loops_U
.append(True)
2721 # Move the first vert to the center coordinates
2722 ob_ctrl_pts
.data
.vertices
[first_verts
[i
]].co
= verts_center_co
2723 # Select the last verts from Cyclic loops, for later deletion all at once
2724 v
[last_verts
[i
]].select
= True
2726 cyclic_loops_U
.append(False)
2728 # If "Cyclic Cross" is active then "all" crossing curves become cyclic
2729 if self
.cyclic_cross
and not self
.selection_U_exists
and not \
2730 ((self
.selection_V_exists
and not self
.selection_V_is_closed
) or
2731 (self
.selection_V2_exists
and not self
.selection_V2_is_closed
)):
2733 cyclic_loops_U
.append(True)
2735 cyclic_loops_U
.append(False)
2737 # The cyclic_loops_U list needs to be reversed.
2738 cyclic_loops_U
.reverse()
2740 # Delete the previously selected (last_)verts.
2741 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2742 bpy
.ops
.mesh
.delete('INVOKE_REGION_WIN', type='VERT')
2743 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2745 # Create curves from control points.
2746 bpy
.ops
.object.convert('INVOKE_REGION_WIN', target
='CURVE', keep_original
=False)
2747 ob_curves_surf
= bpy
.context
.view_layer
.objects
.active
2748 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2749 bpy
.ops
.curve
.spline_type_set('INVOKE_REGION_WIN', type='BEZIER')
2750 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2752 # Make Cyclic the splines designated as Cyclic.
2753 for i
in range(0, len(cyclic_loops_U
)):
2754 ob_curves_surf
.data
.splines
[i
].use_cyclic_u
= cyclic_loops_U
[i
]
2756 # Get the coords of all points on first loop-U, for later comparison with its
2757 # subdivided version, to know which points of the loops-U are crossed by the
2758 # original strokes. The indices will be the same for the other loops-U
2759 if self
.loops_on_strokes
:
2760 coords_loops_U_control_points
= []
2761 for p
in ob_ctrl_pts
.data
.splines
[0].bezier_points
:
2762 coords_loops_U_control_points
.append(["%.4f" % p
.co
[0], "%.4f" % p
.co
[1], "%.4f" % p
.co
[2]])
2764 tuple(coords_loops_U_control_points
)
2766 # Calculate number of edges-V in case option "Loops on strokes" is active or inactive
2767 if self
.loops_on_strokes
and not self
.selection_V_exists
:
2768 edges_V_count
= len(self
.main_splines
.data
.splines
) * self
.edges_V
2770 edges_V_count
= len(edges_proportions_V
)
2772 # The Follow precision will vary depending on the number of Follow face-loops
2773 precision_multiplier
= round(2 + (edges_V_count
/ 15))
2774 curve_cuts
= bpy
.context
.scene
.bsurfaces
.SURFSK_precision
* precision_multiplier
2776 # Subdivide the curves
2777 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=curve_cuts
)
2779 # The verts position shifting that happens with splines subdivision.
2780 # For later reorder splines points
2781 verts_position_shift
= curve_cuts
+ 1
2782 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2784 # Reorder coordinates of the points of each spline to put the first point of
2785 # the spline starting at the position it was the first point before sudividing
2786 # the curve. And make a new curve object per spline (to handle memory better later)
2787 splines_U_objects
= []
2788 for i
in range(len(ob_curves_surf
.data
.splines
)):
2789 spline_U_curve
= bpy
.data
.curves
.new('SURFSKIO_spline_U_' + str(i
), 'CURVE')
2790 ob_spline_U
= bpy
.data
.objects
.new('SURFSKIO_spline_U_' + str(i
), spline_U_curve
)
2791 bpy
.context
.collection
.objects
.link(ob_spline_U
)
2793 spline_U_curve
.dimensions
= "3D"
2795 # Add points to the spline in the new curve object
2796 ob_spline_U
.data
.splines
.new('BEZIER')
2797 for t
in range(len(ob_curves_surf
.data
.splines
[i
].bezier_points
)):
2798 if cyclic_loops_U
[i
] is True and not self
.selection_U_exists
: # If the loop is cyclic
2799 if t
+ verts_position_shift
<= len(ob_curves_surf
.data
.splines
[i
].bezier_points
) - 1:
2800 point_index
= t
+ verts_position_shift
2802 point_index
= t
+ verts_position_shift
- len(ob_curves_surf
.data
.splines
[i
].bezier_points
)
2805 # to avoid adding the first point since it's added when the spline is created
2807 ob_spline_U
.data
.splines
[0].bezier_points
.add(1)
2808 ob_spline_U
.data
.splines
[0].bezier_points
[t
].co
= \
2809 ob_curves_surf
.data
.splines
[i
].bezier_points
[point_index
].co
2811 if cyclic_loops_U
[i
] is True and not self
.selection_U_exists
: # If the loop is cyclic
2812 # Add a last point at the same location as the first one
2813 ob_spline_U
.data
.splines
[0].bezier_points
.add(1)
2814 ob_spline_U
.data
.splines
[0].bezier_points
[len(ob_spline_U
.data
.splines
[0].bezier_points
) - 1].co
= \
2815 ob_spline_U
.data
.splines
[0].bezier_points
[0].co
2817 ob_spline_U
.data
.splines
[0].use_cyclic_u
= False
2819 splines_U_objects
.append(ob_spline_U
)
2820 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2821 ob_spline_U
.select_set(True)
2822 bpy
.context
.view_layer
.objects
.active
= ob_spline_U
2824 # When option "Loops on strokes" is active each "Cross" loop will have
2825 # its own proportions according to where the original strokes "touch" them
2826 if self
.loops_on_strokes
:
2827 # Get the indices of points where the original strokes "touch" loops-U
2828 points_U_crossed_by_strokes
= []
2829 for i
in range(len(splines_U_objects
[0].data
.splines
[0].bezier_points
)):
2830 bp
= splines_U_objects
[0].data
.splines
[0].bezier_points
[i
]
2831 if ["%.4f" % bp
.co
[0], "%.4f" % bp
.co
[1], "%.4f" % bp
.co
[2]] in coords_loops_U_control_points
:
2832 points_U_crossed_by_strokes
.append(i
)
2834 # Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke
2835 edge_order_number_for_splines
= {}
2836 if self
.selection_V_exists
:
2837 # For two-connected selections add a first hypothetic stroke at the beginning.
2838 if selection_type
== "TWO_CONNECTED":
2839 edge_order_number_for_splines
[0] = 0
2841 for i
in range(len(self
.main_splines
.data
.splines
)):
2842 sp
= self
.main_splines
.data
.splines
[i
]
2843 v_idx
, _dist_temp
= self
.shortest_distance(
2845 sp
.bezier_points
[0].co
,
2846 verts_ordered_V_indices
2848 # Get the position (edges count) of the vert v_idx in the selected chain V
2849 edge_idx_in_chain
= verts_ordered_V_indices
.index(v_idx
)
2851 # For two-connected selections the strokes go after the
2852 # hypothetic stroke added before, so the index adds one per spline
2853 if selection_type
== "TWO_CONNECTED":
2854 spline_number
= i
+ 1
2858 edge_order_number_for_splines
[spline_number
] = edge_idx_in_chain
2860 # Get the first and last verts indices for later comparison
2863 elif i
== len(self
.main_splines
.data
.splines
) - 1:
2866 if self
.selection_V_is_closed
:
2867 # If there is no last stroke on the last vertex (same as first vertex),
2868 # add a hypothetic spline at last vert order
2869 if first_v_idx
!= last_v_idx
:
2870 edge_order_number_for_splines
[(len(self
.main_splines
.data
.splines
) - 1) + 1] = \
2871 len(verts_ordered_V_indices
) - 1
2873 if self
.cyclic_cross
:
2874 edge_order_number_for_splines
[len(self
.main_splines
.data
.splines
) - 1] = \
2875 len(verts_ordered_V_indices
) - 2
2876 edge_order_number_for_splines
[(len(self
.main_splines
.data
.splines
) - 1) + 1] = \
2877 len(verts_ordered_V_indices
) - 1
2879 edge_order_number_for_splines
[len(self
.main_splines
.data
.splines
) - 1] = \
2880 len(verts_ordered_V_indices
) - 1
2882 # Get the coords of the points distributed along the
2883 # "crossing curves", with appropriate proportions-V
2884 surface_splines_parsed
= []
2885 for i
in range(len(splines_U_objects
)):
2886 sp_ob
= splines_U_objects
[i
]
2887 # If "Loops on strokes" option is active, calculate the proportions for each loop-U
2888 if self
.loops_on_strokes
:
2889 # Segments distances from stroke to stroke
2892 segments_distances
= []
2893 for t
in range(len(sp_ob
.data
.splines
[0].bezier_points
)):
2894 bp
= sp_ob
.data
.splines
[0].bezier_points
[t
]
2900 dist
+= (last_p
- actual_p
).length
2902 if t
in points_U_crossed_by_strokes
:
2903 segments_distances
.append(dist
)
2910 # Calculate Proportions.
2911 used_edges_proportions_V
= []
2912 for t
in range(len(segments_distances
)):
2913 if self
.selection_V_exists
:
2915 order_number_last_stroke
= 0
2917 segment_edges_length_V
= 0
2918 segment_edges_length_V2
= 0
2919 for order
in range(order_number_last_stroke
, edge_order_number_for_splines
[t
+ 1]):
2920 segment_edges_length_V
+= edges_lengths_V
[order
]
2921 if self
.selection_V2_exists
:
2922 segment_edges_length_V2
+= edges_lengths_V2
[order
]
2924 for order
in range(order_number_last_stroke
, edge_order_number_for_splines
[t
+ 1]):
2925 # Calculate each "sub-segment" (the ones between each stroke) length
2926 if self
.selection_V2_exists
:
2927 proportion_sub_seg
= (edges_lengths_V2
[order
] -
2928 ((edges_lengths_V2
[order
] - edges_lengths_V
[order
]) /
2929 len(splines_U_objects
) * i
)) / (segment_edges_length_V2
-
2930 (segment_edges_length_V2
- segment_edges_length_V
) /
2931 len(splines_U_objects
) * i
)
2933 sub_seg_dist
= segments_distances
[t
] * proportion_sub_seg
2935 proportion_sub_seg
= edges_lengths_V
[order
] / segment_edges_length_V
2936 sub_seg_dist
= segments_distances
[t
] * proportion_sub_seg
2938 used_edges_proportions_V
.append(sub_seg_dist
/ full_dist
)
2940 order_number_last_stroke
= edge_order_number_for_splines
[t
+ 1]
2943 for _c
in range(self
.edges_V
):
2944 # Calculate each "sub-segment" (the ones between each stroke) length
2945 sub_seg_dist
= segments_distances
[t
] / self
.edges_V
2946 used_edges_proportions_V
.append(sub_seg_dist
/ full_dist
)
2948 actual_spline
= self
.distribute_pts(sp_ob
.data
.splines
, used_edges_proportions_V
)
2949 surface_splines_parsed
.append(actual_spline
[0])
2952 if self
.selection_V2_exists
:
2953 used_edges_proportions_V
= []
2954 for p
in range(len(edges_proportions_V
)):
2955 used_edges_proportions_V
.append(
2956 edges_proportions_V2
[p
] -
2957 ((edges_proportions_V2
[p
] -
2958 edges_proportions_V
[p
]) / len(splines_U_objects
) * i
)
2961 used_edges_proportions_V
= edges_proportions_V
2963 actual_spline
= self
.distribute_pts(sp_ob
.data
.splines
, used_edges_proportions_V
)
2964 surface_splines_parsed
.append(actual_spline
[0])
2966 # Set the verts of the first and last splines to the locations
2967 # of the respective verts in the selections
2968 if self
.selection_V_exists
:
2969 for i
in range(0, len(surface_splines_parsed
[0])):
2970 surface_splines_parsed
[len(surface_splines_parsed
) - 1][i
] = \
2971 self
.main_object
.matrix_world
@ verts_ordered_V
[i
].co
2973 if selection_type
== "TWO_NOT_CONNECTED":
2974 if self
.selection_V2_exists
:
2975 for i
in range(0, len(surface_splines_parsed
[0])):
2976 surface_splines_parsed
[0][i
] = self
.main_object
.matrix_world
@ verts_ordered_V2
[i
].co
2978 # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
2979 # merge the verts of the tips of the loops when they are "near enough"
2980 if self
.automatic_join
and selection_type
!= "TWO_CONNECTED":
2981 # Join the tips of "Follow" loops that are near enough and must be "closed"
2982 if not self
.selection_V_exists
and len(edges_proportions_U
) >= 3:
2983 for i
in range(len(surface_splines_parsed
[0])):
2984 sp
= surface_splines_parsed
2985 loop_segment_dist
= (sp
[0][i
] - sp
[1][i
]).length
2987 verts_middle_position_co
= [
2988 (sp
[0][i
][0] + sp
[len(sp
) - 1][i
][0]) / 2,
2989 (sp
[0][i
][1] + sp
[len(sp
) - 1][i
][1]) / 2,
2990 (sp
[0][i
][2] + sp
[len(sp
) - 1][i
][2]) / 2
2992 points_original
= []
2993 points_original
.append(sp
[1][i
])
2994 points_original
.append(sp
[0][i
])
2997 points_target
.append(sp
[1][i
])
2998 points_target
.append(Vector(verts_middle_position_co
))
3000 vec_A
= points_original
[0] - points_original
[1]
3001 vec_B
= points_target
[0] - points_target
[1]
3002 # check for zero angles, not sure if it is a great fix
3003 if vec_A
.length
!= 0 and vec_B
.length
!= 0:
3004 angle
= vec_A
.angle(vec_B
) / pi
3005 edge_new_length
= (Vector(verts_middle_position_co
) - sp
[1][i
]).length
3010 # If after moving the verts to the middle point, the segment doesn't stretch too much
3011 if edge_new_length
<= loop_segment_dist
* 1.5 * \
3012 self
.join_stretch_factor
and angle
< 0.25 * self
.join_stretch_factor
:
3014 # Avoid joining when the actual loop must be merged with the original mesh
3015 if not (self
.selection_U_exists
and i
== 0) and \
3016 not (self
.selection_U2_exists
and i
== len(surface_splines_parsed
[0]) - 1):
3018 # Change the coords of both verts to the middle position
3019 surface_splines_parsed
[0][i
] = verts_middle_position_co
3020 surface_splines_parsed
[len(surface_splines_parsed
) - 1][i
] = verts_middle_position_co
3022 # Delete object with control points and object from grease pencil conversion
3023 bpy
.ops
.object.delete({"selected_objects": [ob_ctrl_pts
]})
3025 bpy
.ops
.object.delete({"selected_objects": splines_U_objects
})
3029 # Get all verts coords
3030 all_surface_verts_co
= []
3031 for i
in range(0, len(surface_splines_parsed
)):
3032 # Get coords of all verts and make a list with them
3033 for pt_co
in surface_splines_parsed
[i
]:
3034 all_surface_verts_co
.append(pt_co
)
3036 # Define verts for each face
3037 all_surface_faces
= []
3038 for i
in range(0, len(all_surface_verts_co
) - len(surface_splines_parsed
[0])):
3039 if ((i
+ 1) / len(surface_splines_parsed
[0]) != int((i
+ 1) / len(surface_splines_parsed
[0]))):
3040 all_surface_faces
.append(
3041 [i
+ 1, i
, i
+ len(surface_splines_parsed
[0]),
3042 i
+ len(surface_splines_parsed
[0]) + 1]
3045 surf_me_name
= "SURFSKIO_surface"
3046 me_surf
= bpy
.data
.meshes
.new(surf_me_name
)
3047 me_surf
.from_pydata(all_surface_verts_co
, [], all_surface_faces
)
3048 ob_surface
= object_utils
.object_data_add(context
, me_surf
)
3050 # Select all the "unselected but participating" verts, from closed selection
3051 # or double selections with middle-vertex, for later join with remove doubles
3052 for v_idx
in single_unselected_verts
:
3053 self
.main_object
.data
.vertices
[v_idx
].select
= True
3055 # Join the new mesh to the main object
3056 ob_surface
.select_set(True)
3057 self
.main_object
.select_set(True)
3058 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3060 bpy
.ops
.object.join('INVOKE_REGION_WIN')
3062 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3064 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN', threshold
=0.0001)
3065 bpy
.ops
.mesh
.normals_make_consistent('INVOKE_REGION_WIN', inside
=False)
3066 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3074 global global_offset
3075 shrinkwrap
= self
.main_object
.modifiers
["Shrinkwrap"]
3076 shrinkwrap
.offset
= global_offset
3077 bpy
.context
.scene
.bsurfaces
.SURFSK_Shrinkwrap_offset
= global_offset
3083 material
= makeMaterial("BSurfaceMesh", global_color
)
3084 if self
.main_object
.data
.materials
:
3085 self
.main_object
.data
.materials
[0] = material
3087 self
.main_object
.data
.materials
.append(material
)
3088 bpy
.context
.scene
.bsurfaces
.SURFSK_mesh_color
= global_color
3093 global global_in_front
3094 self
.main_object
.show_in_front
= global_in_front
3095 bpy
.context
.scene
.bsurfaces
.SURFSK_in_front
= global_in_front
3100 global global_show_wire
3101 self
.main_object
.show_wire
= global_show_wire
3102 bpy
.context
.scene
.bsurfaces
.SURFSK_show_wire
= global_show_wire
3107 global global_shade_smooth
3108 if global_shade_smooth
:
3109 bpy
.ops
.object.shade_smooth()
3111 bpy
.ops
.object.shade_flat()
3112 bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
= global_shade_smooth
3118 def execute(self
, context
):
3120 if bpy
.ops
.object.mode_set
.poll():
3121 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3124 global global_mesh_object
3125 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3126 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
3127 self
.main_object
.select_set(True)
3129 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3131 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3135 if not self
.is_fill_faces
:
3136 bpy
.ops
.wm
.context_set_value(data_path
='tool_settings.mesh_select_mode',
3137 value
='True, False, False')
3139 # Build splines from the "last saved splines".
3140 last_saved_curve
= bpy
.data
.curves
.new('SURFSKIO_last_crv', 'CURVE')
3141 self
.main_splines
= bpy
.data
.objects
.new('SURFSKIO_last_crv', last_saved_curve
)
3142 bpy
.context
.collection
.objects
.link(self
.main_splines
)
3144 last_saved_curve
.dimensions
= "3D"
3146 for sp
in self
.last_strokes_splines_coords
:
3147 spline
= self
.main_splines
.data
.splines
.new('BEZIER')
3148 # less one because one point is added when the spline is created
3149 spline
.bezier_points
.add(len(sp
) - 1)
3150 for p
in range(0, len(sp
)):
3151 spline
.bezier_points
[p
].co
= [sp
[p
][0], sp
[p
][1], sp
[p
][2]]
3153 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3155 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3156 self
.main_splines
.select_set(True)
3157 bpy
.context
.view_layer
.objects
.active
= self
.main_splines
3159 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3161 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
3162 # Important to make it vector first and then automatic, otherwise the
3163 # tips handles get too big and distort the shrinkwrap results later
3164 bpy
.ops
.curve
.handle_type_set(type='VECTOR')
3165 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
3166 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3167 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3169 self
.main_splines
.name
= "SURFSKIO_temp_strokes"
3171 if self
.is_crosshatch
:
3172 strokes_for_crosshatch
= True
3173 strokes_for_rectangular_surface
= False
3175 strokes_for_rectangular_surface
= True
3176 strokes_for_crosshatch
= False
3178 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3180 if strokes_for_rectangular_surface
:
3181 self
.rectangular_surface(context
)
3182 elif strokes_for_crosshatch
:
3183 self
.crosshatch_surface_execute(context
)
3185 #Set Shade smooth to new polygons
3186 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3187 global global_shade_smooth
3188 if global_shade_smooth
:
3189 bpy
.ops
.object.shade_smooth()
3191 bpy
.ops
.object.shade_flat()
3193 # Delete main splines
3194 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3195 if self
.keep_strokes
:
3196 self
.main_splines
.name
= "keep_strokes"
3197 self
.main_splines
.data
.bevel_depth
= 0.001
3198 if "keep_strokes_material" in bpy
.data
.materials
:
3199 self
.main_splines
.data
.materials
.append(bpy
.data
.materials
["keep_strokes_material"])
3201 mat
= bpy
.data
.materials
.new("keep_strokes_material")
3202 mat
.diffuse_color
= (1, 0, 0, 0)
3203 mat
.specular_color
= (1, 0, 0)
3204 mat
.specular_intensity
= 0.0
3206 self
.main_splines
.data
.materials
.append(mat
)
3208 bpy
.ops
.object.delete({"selected_objects": [self
.main_splines
]})
3210 # Delete grease pencil strokes
3211 if self
.strokes_type
== "GP_STROKES" and not self
.stopping_errors
:
3213 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3217 # Delete annotations
3218 if self
.strokes_type
== "GP_ANNOTATION" and not self
.stopping_errors
:
3220 bpy
.data
.grease_pencils
[0].layers
.active
.clear()
3224 bsurfaces_props
.SURFSK_edges_U
= self
.edges_U
3225 bsurfaces_props
.SURFSK_edges_V
= self
.edges_V
3226 bsurfaces_props
.SURFSK_cyclic_cross
= self
.cyclic_cross
3227 bsurfaces_props
.SURFSK_cyclic_follow
= self
.cyclic_follow
3228 bsurfaces_props
.SURFSK_automatic_join
= self
.automatic_join
3229 bsurfaces_props
.SURFSK_loops_on_strokes
= self
.loops_on_strokes
3230 bsurfaces_props
.SURFSK_keep_strokes
= self
.keep_strokes
3232 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3233 self
.main_object
.select_set(True)
3234 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3236 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3242 def invoke(self
, context
, event
):
3244 if bpy
.ops
.object.mode_set
.poll():
3245 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3247 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3248 self
.cyclic_cross
= bsurfaces_props
.SURFSK_cyclic_cross
3249 self
.cyclic_follow
= bsurfaces_props
.SURFSK_cyclic_follow
3250 self
.automatic_join
= bsurfaces_props
.SURFSK_automatic_join
3251 self
.loops_on_strokes
= bsurfaces_props
.SURFSK_loops_on_strokes
3252 self
.keep_strokes
= bsurfaces_props
.SURFSK_keep_strokes
3255 global global_mesh_object
3256 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
3257 self
.main_object
.select_set(True)
3258 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3260 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3265 self
.main_object_selected_verts_count
= len([v
for v
in self
.main_object
.data
.vertices
if v
.select
])
3267 bpy
.ops
.wm
.context_set_value(data_path
='tool_settings.mesh_select_mode',
3268 value
='True, False, False')
3270 self
.edges_U
= bsurfaces_props
.SURFSK_edges_U
3271 self
.edges_V
= bsurfaces_props
.SURFSK_edges_V
3273 self
.is_fill_faces
= False
3274 self
.stopping_errors
= False
3275 self
.last_strokes_splines_coords
= []
3277 # Determine the type of the strokes
3278 self
.strokes_type
= get_strokes_type(context
)
3280 # Check if it will be used grease pencil strokes or curves
3281 # If there are strokes to be used
3282 if self
.strokes_type
== "GP_STROKES" or self
.strokes_type
== "EXTERNAL_CURVE" or self
.strokes_type
== "GP_ANNOTATION":
3283 if self
.strokes_type
== "GP_STROKES":
3284 # Convert grease pencil strokes to curve
3285 global global_gpencil_object
3286 gp
= bpy
.data
.objects
[global_gpencil_object
]
3287 self
.original_curve
= conver_gpencil_to_curve(self
, context
, gp
, 'GPensil')
3288 self
.using_external_curves
= False
3290 elif self
.strokes_type
== "GP_ANNOTATION":
3291 # Convert grease pencil strokes to curve
3292 gp
= bpy
.data
.grease_pencils
["Annotations"]
3293 self
.original_curve
= conver_gpencil_to_curve(self
, context
, gp
, 'Annotation')
3294 self
.using_external_curves
= False
3296 elif self
.strokes_type
== "EXTERNAL_CURVE":
3297 global global_curve_object
3298 self
.original_curve
= bpy
.data
.objects
[global_curve_object
]
3299 self
.using_external_curves
= True
3301 # Make sure there are no objects left from erroneous
3302 # executions of this operator, with the reserved names used here
3303 for o
in bpy
.data
.objects
:
3304 if o
.name
.find("SURFSKIO_") != -1:
3305 bpy
.ops
.object.delete({"selected_objects": [o
]})
3307 bpy
.context
.view_layer
.objects
.active
= self
.original_curve
3309 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3311 self
.temporary_curve
= bpy
.context
.view_layer
.objects
.active
3313 # Deselect all points of the curve
3314 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3315 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3316 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3318 # Delete splines with only a single isolated point
3319 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3320 sp
= self
.temporary_curve
.data
.splines
[i
]
3322 if len(sp
.bezier_points
) == 1:
3323 sp
.bezier_points
[0].select_control_point
= True
3325 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3326 bpy
.ops
.curve
.delete(type='VERT')
3327 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3329 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3330 self
.temporary_curve
.select_set(True)
3331 bpy
.context
.view_layer
.objects
.active
= self
.temporary_curve
3333 # Set a minimum number of points for crosshatch
3334 minimum_points_num
= 15
3336 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3337 # Check if the number of points of each curve has at least the number of points
3338 # of minimum_points_num, which is a bit more than the face-loops limit.
3339 # If not, subdivide to reach at least that number of points
3340 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3341 sp
= self
.temporary_curve
.data
.splines
[i
]
3343 if len(sp
.bezier_points
) < minimum_points_num
:
3344 for bp
in sp
.bezier_points
:
3345 bp
.select_control_point
= True
3347 if (len(sp
.bezier_points
) - 1) != 0:
3348 # Formula to get the number of cuts that will make a curve
3349 # of N number of points have near to "minimum_points_num"
3350 # points, when subdividing with this number of cuts
3351 subdivide_cuts
= int(
3352 (minimum_points_num
- len(sp
.bezier_points
)) /
3353 (len(sp
.bezier_points
) - 1)
3358 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3359 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3361 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3363 # Detect if the strokes are a crosshatch and do it if it is
3364 self
.crosshatch_surface_invoke(self
.temporary_curve
)
3366 if not self
.is_crosshatch
:
3367 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3368 self
.temporary_curve
.select_set(True)
3369 bpy
.context
.view_layer
.objects
.active
= self
.temporary_curve
3371 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3373 # Set a minimum number of points for rectangular surfaces
3374 minimum_points_num
= 60
3376 # Check if the number of points of each curve has at least the number of points
3377 # of minimum_points_num, which is a bit more than the face-loops limit.
3378 # If not, subdivide to reach at least that number of points
3379 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3380 sp
= self
.temporary_curve
.data
.splines
[i
]
3382 if len(sp
.bezier_points
) < minimum_points_num
:
3383 for bp
in sp
.bezier_points
:
3384 bp
.select_control_point
= True
3386 if (len(sp
.bezier_points
) - 1) != 0:
3387 # Formula to get the number of cuts that will make a curve of
3388 # N number of points have near to "minimum_points_num" points,
3389 # when subdividing with this number of cuts
3390 subdivide_cuts
= int(
3391 (minimum_points_num
- len(sp
.bezier_points
)) /
3392 (len(sp
.bezier_points
) - 1)
3397 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3398 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3400 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3402 # Save coordinates of the actual strokes (as the "last saved splines")
3403 for sp_idx
in range(len(self
.temporary_curve
.data
.splines
)):
3404 self
.last_strokes_splines_coords
.append([])
3405 for bp_idx
in range(len(self
.temporary_curve
.data
.splines
[sp_idx
].bezier_points
)):
3406 coords
= self
.temporary_curve
.matrix_world
@ \
3407 self
.temporary_curve
.data
.splines
[sp_idx
].bezier_points
[bp_idx
].co
3408 self
.last_strokes_splines_coords
[sp_idx
].append([coords
[0], coords
[1], coords
[2]])
3410 # Check for cyclic splines, put the first and last points in the middle of their actual positions
3411 for sp_idx
in range(len(self
.temporary_curve
.data
.splines
)):
3412 if self
.temporary_curve
.data
.splines
[sp_idx
].use_cyclic_u
is True:
3413 first_p_co
= self
.last_strokes_splines_coords
[sp_idx
][0]
3414 last_p_co
= self
.last_strokes_splines_coords
[sp_idx
][
3415 len(self
.last_strokes_splines_coords
[sp_idx
]) - 1
3418 (first_p_co
[0] + last_p_co
[0]) / 2,
3419 (first_p_co
[1] + last_p_co
[1]) / 2,
3420 (first_p_co
[2] + last_p_co
[2]) / 2
3423 self
.last_strokes_splines_coords
[sp_idx
][0] = target_co
3424 self
.last_strokes_splines_coords
[sp_idx
][
3425 len(self
.last_strokes_splines_coords
[sp_idx
]) - 1
3427 tuple(self
.last_strokes_splines_coords
)
3429 # Estimation of the average length of the segments between
3430 # each point of the grease pencil strokes.
3431 # Will be useful to determine whether a curve should be made "Cyclic"
3432 segments_lengths_sum
= 0
3434 random_spline
= self
.temporary_curve
.data
.splines
[0].bezier_points
3435 for i
in range(0, len(random_spline
)):
3436 if i
!= 0 and len(random_spline
) - 1 >= i
:
3437 segments_lengths_sum
+= (random_spline
[i
- 1].co
- random_spline
[i
].co
).length
3440 self
.average_gp_segment_length
= segments_lengths_sum
/ segments_count
3442 # Delete temporary strokes curve object
3443 bpy
.ops
.object.delete({"selected_objects": [self
.temporary_curve
]})
3445 # Set again since "execute()" will turn it again to its initial value
3446 self
.execute(context
)
3448 if not self
.stopping_errors
:
3449 # Delete grease pencil strokes
3450 if self
.strokes_type
== "GP_STROKES":
3452 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3456 # Delete annotation strokes
3457 elif self
.strokes_type
== "GP_ANNOTATION":
3459 bpy
.data
.grease_pencils
[0].layers
.active
.clear()
3463 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3464 bpy
.ops
.object.delete({"selected_objects": [self
.original_curve
]})
3465 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3471 elif self
.strokes_type
== "SELECTION_ALONE":
3472 self
.is_fill_faces
= True
3473 created_faces_count
= self
.fill_with_faces(self
.main_object
)
3475 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3477 if created_faces_count
== 0:
3478 self
.report({'WARNING'}, "There aren't any strokes attached to the object")
3479 return {"CANCELLED"}
3483 if self
.strokes_type
== "EXTERNAL_NO_CURVE":
3484 self
.report({'WARNING'}, "The secondary object is not a Curve.")
3487 elif self
.strokes_type
== "MORE_THAN_ONE_EXTERNAL":
3488 self
.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3491 elif self
.strokes_type
== "SINGLE_GP_STROKE_NO_SELECTION" or \
3492 self
.strokes_type
== "SINGLE_CURVE_STROKE_NO_SELECTION":
3494 self
.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
3497 elif self
.strokes_type
== "NO_STROKES":
3498 self
.report({'WARNING'}, "There aren't any strokes attached to the object")
3501 elif self
.strokes_type
== "CURVE_WITH_NON_BEZIER_SPLINES":
3502 self
.report({'WARNING'}, "All splines must be Bezier.")
3508 # ----------------------------
3510 class MESH_OT_SURFSK_init(Operator
):
3511 bl_idname
= "mesh.surfsk_init"
3512 bl_label
= "Bsurfaces initialize"
3513 bl_description
= "Add an empty mesh object with useful settings"
3514 bl_options
= {'REGISTER', 'UNDO'}
3516 def execute(self
, context
):
3518 bs
= bpy
.context
.scene
.bsurfaces
3520 if bpy
.ops
.object.mode_set
.poll():
3521 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3524 global global_offset
3525 global global_in_front
3526 global global_show_wire
3527 global global_shade_smooth
3528 global global_mesh_object
3529 global global_gpencil_object
3531 if bs
.SURFSK_mesh
== None:
3532 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3533 mesh
= bpy
.data
.meshes
.new('BSurfaceMesh')
3534 mesh_object
= object_utils
.object_data_add(context
, mesh
)
3535 mesh_object
.select_set(True)
3536 bpy
.context
.view_layer
.objects
.active
= mesh_object
3538 mesh_object
.show_all_edges
= True
3539 global_in_front
= bpy
.context
.scene
.bsurfaces
.SURFSK_in_front
3540 mesh_object
.show_in_front
= global_in_front
3541 mesh_object
.display_type
= 'SOLID'
3542 mesh_object
.show_wire
= True
3544 global_shade_smooth
= bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
3545 if global_shade_smooth
:
3546 bpy
.ops
.object.shade_smooth()
3548 bpy
.ops
.object.shade_flat()
3550 global_show_wire
= bpy
.context
.scene
.bsurfaces
.SURFSK_show_wire
3551 mesh_object
.show_wire
= global_show_wire
3553 global_color
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh_color
3554 material
= makeMaterial("BSurfaceMesh", global_color
)
3555 mesh_object
.data
.materials
.append(material
)
3556 bpy
.ops
.object.modifier_add(type='SHRINKWRAP')
3557 modifier
= mesh_object
.modifiers
["Shrinkwrap"]
3558 if self
.active_object
is not None:
3559 modifier
.target
= self
.active_object
3560 modifier
.wrap_method
= 'TARGET_PROJECT'
3561 modifier
.wrap_mode
= 'OUTSIDE_SURFACE'
3562 modifier
.show_on_cage
= True
3563 global_offset
= bpy
.context
.scene
.bsurfaces
.SURFSK_Shrinkwrap_offset
3564 modifier
.offset
= global_offset
3566 global_mesh_object
= mesh_object
.name
3567 bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
= bpy
.data
.objects
[global_mesh_object
]
3569 bpy
.context
.scene
.tool_settings
.snap_elements
= {'FACE'}
3570 bpy
.context
.scene
.tool_settings
.use_snap
= True
3571 bpy
.context
.scene
.tool_settings
.use_snap_self
= False
3572 bpy
.context
.scene
.tool_settings
.use_snap_align_rotation
= True
3573 bpy
.context
.scene
.tool_settings
.use_snap_project
= True
3574 bpy
.context
.scene
.tool_settings
.use_snap_rotate
= True
3575 bpy
.context
.scene
.tool_settings
.use_snap_scale
= True
3577 bpy
.context
.scene
.tool_settings
.use_mesh_automerge
= True
3578 bpy
.context
.scene
.tool_settings
.double_threshold
= 0.01
3580 if context
.scene
.bsurfaces
.SURFSK_guide
== 'GPencil' and bs
.SURFSK_gpencil
== None:
3581 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3582 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')
3583 bpy
.context
.scene
.tool_settings
.gpencil_stroke_placement_view3d
= 'SURFACE'
3584 gpencil_object
= bpy
.context
.scene
.objects
[bpy
.context
.scene
.objects
[-1].name
]
3585 gpencil_object
.select_set(True)
3586 bpy
.context
.view_layer
.objects
.active
= gpencil_object
3587 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3588 global_gpencil_object
= gpencil_object
.name
3589 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
= bpy
.data
.objects
[global_gpencil_object
]
3590 gpencil_object
.data
.stroke_depth_order
= '3D'
3591 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3592 bpy
.ops
.wm
.tool_set_by_id(name
="builtin_brush.Draw")
3594 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Annotation':
3595 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.annotate")
3596 bpy
.context
.scene
.tool_settings
.annotation_stroke_placement_view3d
= 'SURFACE'
3598 def invoke(self
, context
, event
):
3599 if bpy
.context
.active_object
:
3600 self
.active_object
= bpy
.context
.active_object
3602 self
.active_object
= None
3604 self
.execute(context
)
3608 # ----------------------------
3609 # Add modifiers operator
3610 class MESH_OT_SURFSK_add_modifiers(Operator
):
3611 bl_idname
= "mesh.surfsk_add_modifiers"
3612 bl_label
= "Add Mirror and others modifiers"
3613 bl_description
= "Add modifiers: Mirror, Shrinkwrap, Subdivision, Solidify"
3614 bl_options
= {'REGISTER', 'UNDO'}
3616 def execute(self
, context
):
3618 bs
= bpy
.context
.scene
.bsurfaces
3620 if bpy
.ops
.object.mode_set
.poll():
3621 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3623 if bs
.SURFSK_mesh
== None:
3624 self
.report({'ERROR_INVALID_INPUT'}, "Please select Mesh of BSurface or click Initialize")
3626 mesh_object
= bs
.SURFSK_mesh
3629 mesh_object
.select_set(True)
3631 self
.report({'ERROR_INVALID_INPUT'}, "Mesh of BSurface does not exist")
3634 bpy
.context
.view_layer
.objects
.active
= mesh_object
3637 shrinkwrap
= mesh_object
.modifiers
["Shrinkwrap"]
3638 if self
.active_object
is not None and self
.active_object
!= mesh_object
:
3639 shrinkwrap
.target
= self
.active_object
3640 shrinkwrap
.wrap_method
= 'TARGET_PROJECT'
3641 shrinkwrap
.wrap_mode
= 'OUTSIDE_SURFACE'
3642 shrinkwrap
.show_on_cage
= True
3643 shrinkwrap
.offset
= bpy
.context
.scene
.bsurfaces
.SURFSK_Shrinkwrap_offset
3645 bpy
.ops
.object.modifier_add(type='SHRINKWRAP')
3646 shrinkwrap
= mesh_object
.modifiers
["Shrinkwrap"]
3647 if self
.active_object
is not None and self
.active_object
!= mesh_object
:
3648 shrinkwrap
.target
= self
.active_object
3649 shrinkwrap
.wrap_method
= 'TARGET_PROJECT'
3650 shrinkwrap
.wrap_mode
= 'OUTSIDE_SURFACE'
3651 shrinkwrap
.show_on_cage
= True
3652 shrinkwrap
.offset
= bpy
.context
.scene
.bsurfaces
.SURFSK_Shrinkwrap_offset
3655 mirror
= mesh_object
.modifiers
["Mirror"]
3656 mirror
.use_clip
= True
3658 bpy
.ops
.object.modifier_add(type='MIRROR')
3659 mirror
= mesh_object
.modifiers
["Mirror"]
3660 mirror
.use_clip
= True
3663 _subsurf
= mesh_object
.modifiers
["Subdivision"]
3665 bpy
.ops
.object.modifier_add(type='SUBSURF')
3666 _subsurf
= mesh_object
.modifiers
["Subdivision"]
3669 solidify
= mesh_object
.modifiers
["Solidify"]
3670 solidify
.thickness
= 0.01
3672 bpy
.ops
.object.modifier_add(type='SOLIDIFY')
3673 solidify
= mesh_object
.modifiers
["Solidify"]
3674 solidify
.thickness
= 0.01
3678 def invoke(self
, context
, event
):
3679 if bpy
.context
.active_object
:
3680 self
.active_object
= bpy
.context
.active_object
3682 self
.active_object
= None
3684 self
.execute(context
)
3688 # ----------------------------
3689 # Edit surface operator
3690 class MESH_OT_SURFSK_edit_surface(Operator
):
3691 bl_idname
= "mesh.surfsk_edit_surface"
3692 bl_label
= "Bsurfaces edit surface"
3693 bl_description
= "Edit surface mesh"
3694 bl_options
= {'REGISTER', 'UNDO'}
3696 def execute(self
, context
):
3697 if bpy
.ops
.object.mode_set
.poll():
3698 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3699 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3700 bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.select_set(True)
3701 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
3702 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3703 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select")
3705 def invoke(self
, context
, event
):
3707 bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.select_set(True)
3709 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3712 self
.execute(context
)
3716 # ----------------------------
3717 # Add strokes operator
3718 class GPENCIL_OT_SURFSK_add_strokes(Operator
):
3719 bl_idname
= "gpencil.surfsk_add_strokes"
3720 bl_label
= "Bsurfaces add strokes"
3721 bl_description
= "Add the grease pencil strokes"
3722 bl_options
= {'REGISTER', 'UNDO'}
3724 def execute(self
, context
):
3725 if bpy
.ops
.object.mode_set
.poll():
3726 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3727 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3729 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3730 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3731 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3732 bpy
.ops
.wm
.tool_set_by_id(name
="builtin_brush.Draw")
3736 def invoke(self
, context
, event
):
3738 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3740 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3743 self
.execute(context
)
3747 # ----------------------------
3748 # Edit strokes operator
3749 class GPENCIL_OT_SURFSK_edit_strokes(Operator
):
3750 bl_idname
= "gpencil.surfsk_edit_strokes"
3751 bl_label
= "Bsurfaces edit strokes"
3752 bl_description
= "Edit the grease pencil strokes"
3753 bl_options
= {'REGISTER', 'UNDO'}
3755 def execute(self
, context
):
3756 if bpy
.ops
.object.mode_set
.poll():
3757 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3758 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3760 gpencil_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3762 gpencil_object
.select_set(True)
3763 bpy
.context
.view_layer
.objects
.active
= gpencil_object
3765 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT_GPENCIL')
3767 bpy
.ops
.gpencil
.select_all(action
='SELECT')
3771 def invoke(self
, context
, event
):
3773 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3775 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3778 self
.execute(context
)
3782 # ----------------------------
3783 # Convert annotation to curves operator
3784 class GPENCIL_OT_SURFSK_annotation_to_curves(Operator
):
3785 bl_idname
= "gpencil.surfsk_annotations_to_curves"
3786 bl_label
= "Convert annotation to curves"
3787 bl_description
= "Convert annotation to curves for editing"
3788 bl_options
= {'REGISTER', 'UNDO'}
3790 def execute(self
, context
):
3792 if bpy
.ops
.object.mode_set
.poll():
3793 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3795 # Convert annotation to curve
3796 curve
= conver_gpencil_to_curve(self
, context
, None, 'Annotation')
3799 # Delete annotation strokes
3801 bpy
.data
.grease_pencils
[0].layers
.active
.clear()
3806 curve
.select_set(True)
3807 bpy
.context
.view_layer
.objects
.active
= curve
3809 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select_box")
3813 def invoke(self
, context
, event
):
3815 strokes
= bpy
.data
.grease_pencils
[0].layers
.active
.active_frame
.strokes
3817 _strokes_num
= len(strokes
)
3819 self
.report({'WARNING'}, "Not active annotation")
3822 self
.execute(context
)
3826 # ----------------------------
3827 # Convert strokes to curves operator
3828 class GPENCIL_OT_SURFSK_strokes_to_curves(Operator
):
3829 bl_idname
= "gpencil.surfsk_strokes_to_curves"
3830 bl_label
= "Convert strokes to curves"
3831 bl_description
= "Convert grease pencil strokes to curves for editing"
3832 bl_options
= {'REGISTER', 'UNDO'}
3834 def execute(self
, context
):
3836 if bpy
.ops
.object.mode_set
.poll():
3837 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3839 # Convert grease pencil strokes to curve
3840 gp
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3841 curve
= conver_gpencil_to_curve(self
, context
, gp
, 'GPensil')
3844 # Delete grease pencil strokes
3846 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3852 curve
.select_set(True)
3853 bpy
.context
.view_layer
.objects
.active
= curve
3855 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select_box")
3859 def invoke(self
, context
, event
):
3861 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3863 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3866 self
.execute(context
)
3870 # ----------------------------
3872 class GPENCIL_OT_SURFSK_add_annotation(Operator
):
3873 bl_idname
= "gpencil.surfsk_add_annotation"
3874 bl_label
= "Bsurfaces add annotation"
3875 bl_description
= "Add annotation"
3876 bl_options
= {'REGISTER', 'UNDO'}
3878 def execute(self
, context
):
3879 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.annotate")
3880 bpy
.context
.scene
.tool_settings
.annotation_stroke_placement_view3d
= 'SURFACE'
3884 def invoke(self
, context
, event
):
3886 self
.execute(context
)
3891 # ----------------------------
3892 # Edit curve operator
3893 class CURVE_OT_SURFSK_edit_curve(Operator
):
3894 bl_idname
= "curve.surfsk_edit_curve"
3895 bl_label
= "Bsurfaces edit curve"
3896 bl_description
= "Edit curve"
3897 bl_options
= {'REGISTER', 'UNDO'}
3899 def execute(self
, context
):
3900 if bpy
.ops
.object.mode_set
.poll():
3901 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3902 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3903 bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.select_set(True)
3904 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_curve
3905 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3907 def invoke(self
, context
, event
):
3909 bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.select_set(True)
3911 self
.report({'WARNING'}, "Specify the name of the object with curve")
3914 self
.execute(context
)
3918 # ----------------------------
3920 class CURVE_OT_SURFSK_reorder_splines(Operator
):
3921 bl_idname
= "curve.surfsk_reorder_splines"
3922 bl_label
= "Bsurfaces reorder splines"
3923 bl_description
= "Defines the order of the splines by using grease pencil strokes"
3924 bl_options
= {'REGISTER', 'UNDO'}
3926 def execute(self
, context
):
3927 objects_to_delete
= []
3928 # Convert grease pencil strokes to curve.
3929 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3930 bpy
.ops
.gpencil
.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes
=False)
3931 for ob
in bpy
.context
.selected_objects
:
3932 if ob
!= bpy
.context
.view_layer
.objects
.active
and ob
.name
.startswith("GP_Layer"):
3933 GP_strokes_curve
= ob
3935 # GP_strokes_curve = bpy.context.object
3936 objects_to_delete
.append(GP_strokes_curve
)
3938 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3939 GP_strokes_curve
.select_set(True)
3940 bpy
.context
.view_layer
.objects
.active
= GP_strokes_curve
3942 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3943 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
3944 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=100)
3945 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3947 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3948 GP_strokes_mesh
= bpy
.context
.object
3949 objects_to_delete
.append(GP_strokes_mesh
)
3951 GP_strokes_mesh
.data
.resolution_u
= 1
3952 bpy
.ops
.object.convert(target
='MESH', keep_original
=False)
3954 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3955 self
.main_curve
.select_set(True)
3956 bpy
.context
.view_layer
.objects
.active
= self
.main_curve
3958 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3959 curves_duplicate_1
= bpy
.context
.object
3960 objects_to_delete
.append(curves_duplicate_1
)
3962 minimum_points_num
= 500
3964 # Some iterations since the subdivision operator
3965 # has a limit of 100 subdivisions per iteration
3966 for x
in range(round(minimum_points_num
/ 100)):
3967 # Check if the number of points of each curve has at least the number of points
3968 # of minimum_points_num. If not, subdivide to reach at least that number of points
3969 for i
in range(len(curves_duplicate_1
.data
.splines
)):
3970 sp
= curves_duplicate_1
.data
.splines
[i
]
3972 if len(sp
.bezier_points
) < minimum_points_num
:
3973 for bp
in sp
.bezier_points
:
3974 bp
.select_control_point
= True
3976 if (len(sp
.bezier_points
) - 1) != 0:
3977 # Formula to get the number of cuts that will make a curve of N
3978 # number of points have near to "minimum_points_num" points,
3979 # when subdividing with this number of cuts
3980 subdivide_cuts
= int(
3981 (minimum_points_num
- len(sp
.bezier_points
)) /
3982 (len(sp
.bezier_points
) - 1)
3987 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3988 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3989 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3990 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3992 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3993 curves_duplicate_2
= bpy
.context
.object
3994 objects_to_delete
.append(curves_duplicate_2
)
3996 # Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target
3997 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3998 curves_duplicate_2
.select_set(True)
3999 bpy
.context
.view_layer
.objects
.active
= curves_duplicate_2
4001 bpy
.ops
.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
4002 curves_duplicate_2
.modifiers
["Shrinkwrap"].wrap_method
= "NEAREST_VERTEX"
4003 curves_duplicate_2
.modifiers
["Shrinkwrap"].target
= GP_strokes_mesh
4004 bpy
.ops
.object.modifier_apply('INVOKE_REGION_WIN', apply_as
='DATA', modifier
='Shrinkwrap')
4006 # Get the distance of each vert from its original position to its position with Shrinkwrap
4007 nearest_points_coords
= {}
4008 for st_idx
in range(len(curves_duplicate_1
.data
.splines
)):
4009 for bp_idx
in range(len(curves_duplicate_1
.data
.splines
[st_idx
].bezier_points
)):
4010 bp_1_co
= curves_duplicate_1
.matrix_world
@ \
4011 curves_duplicate_1
.data
.splines
[st_idx
].bezier_points
[bp_idx
].co
4013 bp_2_co
= curves_duplicate_2
.matrix_world
@ \
4014 curves_duplicate_2
.data
.splines
[st_idx
].bezier_points
[bp_idx
].co
4017 shortest_dist
= (bp_1_co
- bp_2_co
).length
4018 nearest_points_coords
[st_idx
] = ("%.4f" % bp_2_co
[0],
4019 "%.4f" % bp_2_co
[1],
4020 "%.4f" % bp_2_co
[2])
4022 dist
= (bp_1_co
- bp_2_co
).length
4024 if dist
< shortest_dist
:
4025 nearest_points_coords
[st_idx
] = ("%.4f" % bp_2_co
[0],
4026 "%.4f" % bp_2_co
[1],
4027 "%.4f" % bp_2_co
[2])
4028 shortest_dist
= dist
4030 # Get all coords of GP strokes points, for comparison
4031 GP_strokes_coords
= []
4032 for st_idx
in range(len(GP_strokes_curve
.data
.splines
)):
4033 GP_strokes_coords
.append(
4034 [("%.4f" % x
if "%.4f" % x
!= "-0.00" else "0.00",
4035 "%.4f" % y
if "%.4f" % y
!= "-0.00" else "0.00",
4036 "%.4f" % z
if "%.4f" % z
!= "-0.00" else "0.00") for
4037 x
, y
, z
in [bp
.co
for bp
in GP_strokes_curve
.data
.splines
[st_idx
].bezier_points
]]
4040 # Check the point of the GP strokes with the same coords as
4041 # the nearest points of the curves (with shrinkwrap)
4043 # Dictionary with GP stroke index as index, and a list as value.
4044 # The list has as index the point index of the GP stroke
4045 # nearest to the spline, and as value the spline index
4046 GP_connection_points
= {}
4047 for gp_st_idx
in range(len(GP_strokes_coords
)):
4048 GPvert_spline_relationship
= {}
4050 for splines_st_idx
in range(len(nearest_points_coords
)):
4051 if nearest_points_coords
[splines_st_idx
] in GP_strokes_coords
[gp_st_idx
]:
4052 GPvert_spline_relationship
[
4053 GP_strokes_coords
[gp_st_idx
].index(nearest_points_coords
[splines_st_idx
])
4056 GP_connection_points
[gp_st_idx
] = GPvert_spline_relationship
4058 # Get the splines new order
4059 splines_new_order
= []
4060 for i
in GP_connection_points
:
4061 dict_keys
= sorted(GP_connection_points
[i
].keys()) # Sort dictionaries by key
4064 splines_new_order
.append(GP_connection_points
[i
][k
])
4067 curve_original_name
= self
.main_curve
.name
4069 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4070 self
.main_curve
.select_set(True)
4071 bpy
.context
.view_layer
.objects
.active
= self
.main_curve
4073 self
.main_curve
.name
= "SURFSKIO_CRV_ORD"
4075 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4076 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4077 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4079 for _sp_idx
in range(len(self
.main_curve
.data
.splines
)):
4080 self
.main_curve
.data
.splines
[0].bezier_points
[0].select_control_point
= True
4082 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4083 bpy
.ops
.curve
.separate('EXEC_REGION_WIN')
4084 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4086 # Get the names of the separated splines objects in the original order
4087 splines_unordered
= {}
4088 for o
in bpy
.data
.objects
:
4089 if o
.name
.find("SURFSKIO_CRV_ORD") != -1:
4090 spline_order_string
= o
.name
.partition(".")[2]
4092 if spline_order_string
!= "" and int(spline_order_string
) > 0:
4093 spline_order_index
= int(spline_order_string
) - 1
4094 splines_unordered
[spline_order_index
] = o
.name
4096 # Join all splines objects in final order
4097 for order_idx
in splines_new_order
:
4098 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4099 bpy
.data
.objects
[splines_unordered
[order_idx
]].select_set(True)
4100 bpy
.data
.objects
["SURFSKIO_CRV_ORD"].select_set(True)
4101 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
["SURFSKIO_CRV_ORD"]
4103 bpy
.ops
.object.join('INVOKE_REGION_WIN')
4105 # Go back to the original name of the curves object.
4106 bpy
.context
.object.name
= curve_original_name
4108 # Delete all unused objects
4109 bpy
.ops
.object.delete({"selected_objects": objects_to_delete
})
4111 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4112 bpy
.data
.objects
[curve_original_name
].select_set(True)
4113 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[curve_original_name
]
4115 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4116 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4119 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
4126 def invoke(self
, context
, event
):
4127 self
.main_curve
= bpy
.context
.object
4128 there_are_GP_strokes
= False
4131 # Get the active grease pencil layer
4132 strokes_num
= len(self
.main_curve
.grease_pencil
.layers
.active
.active_frame
.strokes
)
4135 there_are_GP_strokes
= True
4139 if there_are_GP_strokes
:
4140 self
.execute(context
)
4141 self
.report({'INFO'}, "Splines have been reordered")
4143 self
.report({'WARNING'}, "Draw grease pencil strokes to connect splines")
4147 # ----------------------------
4148 # Set first points operator
4149 class CURVE_OT_SURFSK_first_points(Operator
):
4150 bl_idname
= "curve.surfsk_first_points"
4151 bl_label
= "Bsurfaces set first points"
4152 bl_description
= "Set the selected points as the first point of each spline"
4153 bl_options
= {'REGISTER', 'UNDO'}
4155 def execute(self
, context
):
4156 splines_to_invert
= []
4158 # Check non-cyclic splines to invert
4159 for i
in range(len(self
.main_curve
.data
.splines
)):
4160 b_points
= self
.main_curve
.data
.splines
[i
].bezier_points
4162 if i
not in self
.cyclic_splines
: # Only for non-cyclic splines
4163 if b_points
[len(b_points
) - 1].select_control_point
:
4164 splines_to_invert
.append(i
)
4166 # Reorder points of cyclic splines, and set all handles to "Automatic"
4168 # Check first selected point
4169 cyclic_splines_new_first_pt
= {}
4170 for i
in self
.cyclic_splines
:
4171 sp
= self
.main_curve
.data
.splines
[i
]
4173 for t
in range(len(sp
.bezier_points
)):
4174 bp
= sp
.bezier_points
[t
]
4175 if bp
.select_control_point
or bp
.select_right_handle
or bp
.select_left_handle
:
4176 cyclic_splines_new_first_pt
[i
] = t
4177 break # To take only one if there are more
4180 for spline_idx
in cyclic_splines_new_first_pt
:
4181 sp
= self
.main_curve
.data
.splines
[spline_idx
]
4183 spline_old_coords
= []
4184 for bp_old
in sp
.bezier_points
:
4185 coords
= (bp_old
.co
[0], bp_old
.co
[1], bp_old
.co
[2])
4187 left_handle_type
= str(bp_old
.handle_left_type
)
4188 left_handle_length
= float(bp_old
.handle_left
.length
)
4190 float(bp_old
.handle_left
.x
),
4191 float(bp_old
.handle_left
.y
),
4192 float(bp_old
.handle_left
.z
)
4194 right_handle_type
= str(bp_old
.handle_right_type
)
4195 right_handle_length
= float(bp_old
.handle_right
.length
)
4196 right_handle_xyz
= (
4197 float(bp_old
.handle_right
.x
),
4198 float(bp_old
.handle_right
.y
),
4199 float(bp_old
.handle_right
.z
)
4201 spline_old_coords
.append(
4202 [coords
, left_handle_type
,
4203 right_handle_type
, left_handle_length
,
4204 right_handle_length
, left_handle_xyz
,
4208 for t
in range(len(sp
.bezier_points
)):
4209 bp
= sp
.bezier_points
4211 if t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1 <= len(bp
) - 1:
4212 new_index
= t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1
4214 new_index
= t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1 - len(bp
)
4216 bp
[t
].co
= Vector(spline_old_coords
[new_index
][0])
4218 bp
[t
].handle_left
.length
= spline_old_coords
[new_index
][3]
4219 bp
[t
].handle_right
.length
= spline_old_coords
[new_index
][4]
4221 bp
[t
].handle_left_type
= "FREE"
4222 bp
[t
].handle_right_type
= "FREE"
4224 bp
[t
].handle_left
.x
= spline_old_coords
[new_index
][5][0]
4225 bp
[t
].handle_left
.y
= spline_old_coords
[new_index
][5][1]
4226 bp
[t
].handle_left
.z
= spline_old_coords
[new_index
][5][2]
4228 bp
[t
].handle_right
.x
= spline_old_coords
[new_index
][6][0]
4229 bp
[t
].handle_right
.y
= spline_old_coords
[new_index
][6][1]
4230 bp
[t
].handle_right
.z
= spline_old_coords
[new_index
][6][2]
4232 bp
[t
].handle_left_type
= spline_old_coords
[new_index
][1]
4233 bp
[t
].handle_right_type
= spline_old_coords
[new_index
][2]
4235 # Invert the non-cyclic splines designated above
4236 for i
in range(len(splines_to_invert
)):
4237 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4239 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4240 self
.main_curve
.data
.splines
[splines_to_invert
[i
]].bezier_points
[0].select_control_point
= True
4241 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4243 bpy
.ops
.curve
.switch_direction()
4245 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4247 # Keep selected the first vert of each spline
4248 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4249 for i
in range(len(self
.main_curve
.data
.splines
)):
4250 if not self
.main_curve
.data
.splines
[i
].use_cyclic_u
:
4251 bp
= self
.main_curve
.data
.splines
[i
].bezier_points
[0]
4253 bp
= self
.main_curve
.data
.splines
[i
].bezier_points
[
4254 len(self
.main_curve
.data
.splines
[i
].bezier_points
) - 1
4257 bp
.select_control_point
= True
4258 bp
.select_right_handle
= True
4259 bp
.select_left_handle
= True
4261 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4265 def invoke(self
, context
, event
):
4266 self
.main_curve
= bpy
.context
.object
4268 # Check if all curves are Bezier, and detect which ones are cyclic
4269 self
.cyclic_splines
= []
4270 for i
in range(len(self
.main_curve
.data
.splines
)):
4271 if self
.main_curve
.data
.splines
[i
].type != "BEZIER":
4272 self
.report({'WARNING'}, "All splines must be Bezier type")
4274 return {'CANCELLED'}
4276 if self
.main_curve
.data
.splines
[i
].use_cyclic_u
:
4277 self
.cyclic_splines
.append(i
)
4279 self
.execute(context
)
4280 self
.report({'INFO'}, "First points have been set")
4285 # Add-ons Preferences Update Panel
4287 # Define Panel classes for updating
4289 VIEW3D_PT_tools_SURFSK_mesh
,
4290 VIEW3D_PT_tools_SURFSK_curve
4294 def conver_gpencil_to_curve(self
, context
, pencil
, type):
4295 newCurve
= bpy
.data
.curves
.new(type + '_curve', type='CURVE')
4296 newCurve
.dimensions
= '3D'
4297 CurveObject
= object_utils
.object_data_add(context
, newCurve
)
4300 if type == 'GPensil':
4302 strokes
= pencil
.data
.layers
.active
.active_frame
.strokes
4305 CurveObject
.location
= pencil
.location
4306 CurveObject
.rotation_euler
= pencil
.rotation_euler
4307 CurveObject
.scale
= pencil
.scale
4308 elif type == 'Annotation':
4309 grease_pencil
= bpy
.data
.grease_pencils
[0]
4311 strokes
= grease_pencil
.layers
.active
.active_frame
.strokes
4314 CurveObject
.location
= (0.0, 0.0, 0.0)
4315 CurveObject
.rotation_euler
= (0.0, 0.0, 0.0)
4316 CurveObject
.scale
= (1.0, 1.0, 1.0)
4319 for i
, _stroke
in enumerate(strokes
):
4320 stroke_points
= strokes
[i
].points
4321 data_list
= [ (point
.co
.x
, point
.co
.y
, point
.co
.z
)
4322 for point
in stroke_points
]
4323 points_to_add
= len(data_list
)-1
4326 for point
in data_list
:
4327 flat_list
.extend(point
)
4329 spline
= newCurve
.splines
.new(type='BEZIER')
4330 spline
.bezier_points
.add(points_to_add
)
4331 spline
.bezier_points
.foreach_set("co", flat_list
)
4333 for point
in spline
.bezier_points
:
4334 point
.handle_left_type
="AUTO"
4335 point
.handle_right_type
="AUTO"
4342 def update_panel(self
, context
):
4343 message
= "Bsurfaces GPL Edition: Updating Panel locations has failed"
4345 for panel
in panels
:
4346 if "bl_rna" in panel
.__dict
__:
4347 bpy
.utils
.unregister_class(panel
)
4349 for panel
in panels
:
4350 category
= context
.preferences
.addons
[__name__
].preferences
.category
4351 if category
!= 'Tool':
4352 panel
.bl_category
= context
.preferences
.addons
[__name__
].preferences
.category
4354 context
.preferences
.addons
[__name__
].preferences
.category
= 'Edit'
4355 panel
.bl_category
= 'Edit'
4356 raise ValueError("You can not install add-ons in the Tool panel")
4357 bpy
.utils
.register_class(panel
)
4359 except Exception as e
:
4360 print("\n[{}]\n{}\n\nError:\n{}".format(__name__
, message
, e
))
4363 def makeMaterial(name
, diffuse
):
4365 if name
in bpy
.data
.materials
:
4366 material
= bpy
.data
.materials
[name
]
4367 material
.diffuse_color
= diffuse
4369 material
= bpy
.data
.materials
.new(name
)
4370 material
.diffuse_color
= diffuse
4374 def update_mesh(self
, context
):
4376 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4377 bpy
.ops
.object.select_all(action
='DESELECT')
4378 bpy
.context
.view_layer
.update()
4379 global global_mesh_object
4380 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
4381 bpy
.data
.objects
[global_mesh_object
].select_set(True)
4382 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_mesh_object
]
4384 print("Select mesh object")
4386 def update_gpencil(self
, context
):
4388 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4389 bpy
.ops
.object.select_all(action
='DESELECT')
4390 bpy
.context
.view_layer
.update()
4391 global global_gpencil_object
4392 global_gpencil_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.name
4393 bpy
.data
.objects
[global_gpencil_object
].select_set(True)
4394 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_gpencil_object
]
4396 print("Select gpencil object")
4398 def update_curve(self
, context
):
4400 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4401 bpy
.ops
.object.select_all(action
='DESELECT')
4402 bpy
.context
.view_layer
.update()
4403 global global_curve_object
4404 global_curve_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.name
4405 bpy
.data
.objects
[global_curve_object
].select_set(True)
4406 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_curve_object
]
4408 print("Select curve object")
4410 def update_color(self
, context
):
4413 global global_mesh_object
4414 material
= makeMaterial("BSurfaceMesh", bpy
.context
.scene
.bsurfaces
.SURFSK_mesh_color
)
4415 if bpy
.data
.objects
[global_mesh_object
].data
.materials
:
4416 bpy
.data
.objects
[global_mesh_object
].data
.materials
[0] = material
4418 bpy
.data
.objects
[global_mesh_object
].data
.materials
.append(material
)
4419 diffuse_color
= material
.diffuse_color
4420 global_color
= (diffuse_color
[0], diffuse_color
[1], diffuse_color
[2], diffuse_color
[3])
4422 print("Select mesh object")
4424 def update_Shrinkwrap_offset(self
, context
):
4426 global global_offset
4427 global_offset
= bpy
.context
.scene
.bsurfaces
.SURFSK_Shrinkwrap_offset
4428 global global_mesh_object
4429 modifier
= bpy
.data
.objects
[global_mesh_object
].modifiers
["Shrinkwrap"]
4430 modifier
.offset
= global_offset
4432 print("Shrinkwrap modifier not found")
4434 def update_in_front(self
, context
):
4436 global global_in_front
4437 global_in_front
= bpy
.context
.scene
.bsurfaces
.SURFSK_in_front
4438 global global_mesh_object
4439 bpy
.data
.objects
[global_mesh_object
].show_in_front
= global_in_front
4441 print("Select mesh object")
4443 def update_show_wire(self
, context
):
4445 global global_show_wire
4446 global_show_wire
= bpy
.context
.scene
.bsurfaces
.SURFSK_show_wire
4447 global global_mesh_object
4448 bpy
.data
.objects
[global_mesh_object
].show_wire
= global_show_wire
4450 print("Select mesh object")
4452 def update_shade_smooth(self
, context
):
4454 global global_shade_smooth
4455 global_shade_smooth
= bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
4457 contex_mode
= bpy
.context
.mode
4459 if bpy
.ops
.object.mode_set
.poll():
4460 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4462 bpy
.ops
.object.select_all(action
='DESELECT')
4463 global global_mesh_object
4464 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
4465 bpy
.data
.objects
[global_mesh_object
].select_set(True)
4467 if global_shade_smooth
:
4468 bpy
.ops
.object.shade_smooth()
4470 bpy
.ops
.object.shade_flat()
4472 if contex_mode
== "EDIT_MESH":
4473 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4476 print("Select mesh object")
4479 class BsurfPreferences(AddonPreferences
):
4480 # this must match the addon name, use '__package__'
4481 # when defining this in a submodule of a python package.
4482 bl_idname
= __name__
4484 category
: StringProperty(
4485 name
="Tab Category",
4486 description
="Choose a name for the category of the panel",
4491 def draw(self
, context
):
4492 layout
= self
.layout
4496 col
.label(text
="Tab Category:")
4497 col
.prop(self
, "category", text
="")
4500 class BsurfacesProps(PropertyGroup
):
4501 SURFSK_guide
: EnumProperty(
4504 ('Annotation', 'Annotation', 'Annotation'),
4505 ('GPencil', 'GPencil', 'GPencil'),
4506 ('Curve', 'Curve', 'Curve')
4508 default
="Annotation"
4510 SURFSK_edges_U
: IntProperty(
4512 description
="Number of face-loops crossing the strokes",
4517 SURFSK_edges_V
: IntProperty(
4519 description
="Number of face-loops following the strokes",
4524 SURFSK_cyclic_cross
: BoolProperty(
4525 name
="Cyclic Cross",
4526 description
="Make cyclic the face-loops crossing the strokes",
4529 SURFSK_cyclic_follow
: BoolProperty(
4530 name
="Cyclic Follow",
4531 description
="Make cyclic the face-loops following the strokes",
4534 SURFSK_keep_strokes
: BoolProperty(
4535 name
="Keep strokes",
4536 description
="Keeps the sketched strokes or curves after adding the surface",
4539 SURFSK_automatic_join
: BoolProperty(
4540 name
="Automatic join",
4541 description
="Join automatically vertices of either surfaces "
4542 "generated by crosshatching, or from the borders of closed shapes",
4545 SURFSK_loops_on_strokes
: BoolProperty(
4546 name
="Loops on strokes",
4547 description
="Make the loops match the paths of the strokes",
4550 SURFSK_precision
: IntProperty(
4552 description
="Precision level of the surface calculation",
4557 SURFSK_mesh
: PointerProperty(
4558 name
="Mesh of BSurface",
4559 type=bpy
.types
.Object
,
4560 description
="Mesh of BSurface",
4563 SURFSK_gpencil
: PointerProperty(
4564 name
="GreasePencil object",
4565 type=bpy
.types
.Object
,
4566 description
="GreasePencil object",
4567 update
=update_gpencil
,
4569 SURFSK_curve
: PointerProperty(
4570 name
="Curve object",
4571 type=bpy
.types
.Object
,
4572 description
="Curve object",
4573 update
=update_curve
,
4575 SURFSK_mesh_color
: FloatVectorProperty(
4577 default
=(1.0, 0.0, 0.0, 0.3),
4582 update
=update_color
,
4583 description
="Mesh color",
4585 SURFSK_Shrinkwrap_offset
: FloatProperty(
4586 name
="Shrinkwrap offset",
4589 description
="Distance to keep from the target",
4590 update
=update_Shrinkwrap_offset
,
4592 SURFSK_in_front
: BoolProperty(
4594 description
="Make the object draw in front of others",
4596 update
=update_in_front
,
4598 SURFSK_show_wire
: BoolProperty(
4600 description
="Add the object’s wireframe over solid drawing",
4602 update
=update_show_wire
,
4604 SURFSK_shade_smooth
: BoolProperty(
4605 name
="Shade smooth",
4606 description
="Render and display faces smooth, using interpolated Vertex Normals",
4608 update
=update_shade_smooth
,
4612 MESH_OT_SURFSK_init
,
4613 MESH_OT_SURFSK_add_modifiers
,
4614 MESH_OT_SURFSK_add_surface
,
4615 MESH_OT_SURFSK_edit_surface
,
4616 GPENCIL_OT_SURFSK_add_strokes
,
4617 GPENCIL_OT_SURFSK_edit_strokes
,
4618 GPENCIL_OT_SURFSK_strokes_to_curves
,
4619 GPENCIL_OT_SURFSK_annotation_to_curves
,
4620 GPENCIL_OT_SURFSK_add_annotation
,
4621 CURVE_OT_SURFSK_edit_curve
,
4622 CURVE_OT_SURFSK_reorder_splines
,
4623 CURVE_OT_SURFSK_first_points
,
4630 bpy
.utils
.register_class(cls
)
4632 for panel
in panels
:
4633 bpy
.utils
.register_class(panel
)
4635 bpy
.types
.Scene
.bsurfaces
= PointerProperty(type=BsurfacesProps
)
4636 update_panel(None, bpy
.context
)
4639 for panel
in panels
:
4640 bpy
.utils
.unregister_class(panel
)
4643 bpy
.utils
.unregister_class(cls
)
4645 del bpy
.types
.Scene
.bsurfaces
4647 if __name__
== "__main__":