1 # SPDX-License-Identifier: GPL-2.0-or-later
5 "name": "Bsurfaces GPL Edition",
6 "author": "Eclectiel, Vladimir Spivak (cwolf3d)",
9 "location": "View3D EditMode > Sidebar > Edit Tab",
10 "description": "Modeling and retopology tool",
11 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/bsurfaces.html",
18 from bpy_extras
import object_utils
21 from mathutils
import Matrix
, Vector
22 from mathutils
.geometry
import (
31 from bpy
.props
import (
40 from bpy
.types
import (
47 # ----------------------------
49 global_shade_smooth
= False
50 global_mesh_object
= ""
51 global_gpencil_object
= ""
52 global_curve_object
= ""
54 # ----------------------------
56 class VIEW3D_PT_tools_SURFSK_mesh(Panel
):
57 bl_space_type
= 'VIEW_3D'
60 bl_label
= "Bsurfaces"
62 def draw(self
, context
):
64 bs
= context
.scene
.bsurfaces
66 col
= layout
.column(align
=True)
69 col
.operator("mesh.surfsk_init", text
="Initialize (Add BSurface mesh)")
70 col
.operator("mesh.surfsk_add_modifiers", text
="Add Mirror and others modifiers")
72 col
.label(text
="Mesh of BSurface:")
73 col
.prop(bs
, "SURFSK_mesh", text
="")
74 if bs
.SURFSK_mesh
!= None:
75 try: mesh_object
= bs
.SURFSK_mesh
77 try: col
.prop(mesh_object
.data
.materials
[0], "diffuse_color")
80 shrinkwrap
= next(mod
for mod
in mesh_object
.modifiers
81 if mod
.type == 'SHRINKWRAP')
82 col
.prop(shrinkwrap
, "offset")
85 try: col
.prop(mesh_object
, "show_in_front")
87 try: col
.prop(bs
, "SURFSK_shade_smooth")
89 try: col
.prop(mesh_object
, "show_wire")
92 col
.label(text
="Guide strokes:")
93 col
.row().prop(bs
, "SURFSK_guide", expand
=True)
94 if bs
.SURFSK_guide
== 'GPencil':
95 col
.prop(bs
, "SURFSK_gpencil", text
="")
97 if bs
.SURFSK_guide
== 'Curve':
98 col
.prop(bs
, "SURFSK_curve", text
="")
102 col
.operator("mesh.surfsk_add_surface", text
="Add Surface")
103 col
.operator("mesh.surfsk_edit_surface", text
="Edit Surface")
106 if bs
.SURFSK_guide
== 'GPencil':
107 col
.operator("gpencil.surfsk_add_strokes", text
="Add Strokes")
108 col
.operator("gpencil.surfsk_edit_strokes", text
="Edit Strokes")
110 col
.operator("gpencil.surfsk_strokes_to_curves", text
="Strokes to curves")
112 if bs
.SURFSK_guide
== 'Annotation':
113 col
.operator("gpencil.surfsk_add_annotation", text
="Add Annotation")
115 col
.operator("gpencil.surfsk_annotations_to_curves", text
="Annotation to curves")
117 if bs
.SURFSK_guide
== 'Curve':
118 col
.operator("curve.surfsk_edit_curve", text
="Edit curve")
121 col
.label(text
="Initial settings:")
122 col
.prop(bs
, "SURFSK_edges_U")
123 col
.prop(bs
, "SURFSK_edges_V")
124 col
.prop(bs
, "SURFSK_cyclic_cross")
125 col
.prop(bs
, "SURFSK_cyclic_follow")
126 col
.prop(bs
, "SURFSK_loops_on_strokes")
127 col
.prop(bs
, "SURFSK_automatic_join")
128 col
.prop(bs
, "SURFSK_keep_strokes")
130 class VIEW3D_PT_tools_SURFSK_curve(Panel
):
131 bl_space_type
= 'VIEW_3D'
132 bl_region_type
= 'UI'
133 bl_context
= "curve_edit"
135 bl_label
= "Bsurfaces"
138 def poll(cls
, context
):
139 return context
.active_object
141 def draw(self
, context
):
144 col
= layout
.column(align
=True)
147 col
.operator("curve.surfsk_first_points", text
="Set First Points")
148 col
.operator("curve.switch_direction", text
="Switch Direction")
149 col
.operator("curve.surfsk_reorder_splines", text
="Reorder Splines")
152 # ----------------------------
153 # Returns the type of strokes used
154 def get_strokes_type(context
):
155 strokes_type
= "NO_STROKES"
158 # Check if they are annotation
159 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Annotation':
161 strokes
= bpy
.context
.annotation_data
.layers
.active
.active_frame
.strokes
163 strokes_num
= len(strokes
)
166 strokes_type
= "GP_ANNOTATION"
168 strokes_type
= "NO_STROKES"
170 # Check if they are grease pencil
171 if context
.scene
.bsurfaces
.SURFSK_guide
== 'GPencil':
173 global global_gpencil_object
174 gpencil
= bpy
.data
.objects
[global_gpencil_object
]
175 strokes
= gpencil
.data
.layers
.active
.active_frame
.strokes
177 strokes_num
= len(strokes
)
180 strokes_type
= "GP_STROKES"
182 strokes_type
= "NO_STROKES"
184 # Check if they are curves, if there aren't grease pencil strokes
185 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Curve':
187 global global_curve_object
188 ob
= bpy
.data
.objects
[global_curve_object
]
189 if ob
.type == "CURVE":
190 strokes_type
= "EXTERNAL_CURVE"
191 strokes_num
= len(ob
.data
.splines
)
193 # Check if there is any non-bezier spline
194 for i
in range(len(ob
.data
.splines
)):
195 if ob
.data
.splines
[i
].type != "BEZIER":
196 strokes_type
= "CURVE_WITH_NON_BEZIER_SPLINES"
200 strokes_type
= "EXTERNAL_NO_CURVE"
202 strokes_type
= "NO_STROKES"
204 # Check if they are mesh
206 global global_mesh_object
207 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
208 total_vert_sel
= len([v
for v
in self
.main_object
.data
.vertices
if v
.select
])
210 # Check if there is a single stroke without any selection in the object
211 if strokes_num
== 1 and total_vert_sel
== 0:
212 if strokes_type
== "EXTERNAL_CURVE":
213 strokes_type
= "SINGLE_CURVE_STROKE_NO_SELECTION"
214 elif strokes_type
== "GP_STROKES":
215 strokes_type
= "SINGLE_GP_STROKE_NO_SELECTION"
217 if strokes_num
== 0 and total_vert_sel
> 0:
218 strokes_type
= "SELECTION_ALONE"
224 # ----------------------------
225 # Surface generator operator
226 class MESH_OT_SURFSK_add_surface(Operator
):
227 bl_idname
= "mesh.surfsk_add_surface"
228 bl_label
= "Bsurfaces add surface"
229 bl_description
= "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
230 bl_options
= {'REGISTER', 'UNDO'}
232 is_crosshatch
: BoolProperty(
235 is_fill_faces
: BoolProperty(
238 selection_U_exists
: BoolProperty(
241 selection_V_exists
: BoolProperty(
244 selection_U2_exists
: BoolProperty(
247 selection_V2_exists
: BoolProperty(
250 selection_V_is_closed
: BoolProperty(
253 selection_U_is_closed
: BoolProperty(
256 selection_V2_is_closed
: BoolProperty(
259 selection_U2_is_closed
: BoolProperty(
263 edges_U
: IntProperty(
265 description
="Number of face-loops crossing the strokes",
270 edges_V
: IntProperty(
272 description
="Number of face-loops following the strokes",
277 cyclic_cross
: BoolProperty(
279 description
="Make cyclic the face-loops crossing the strokes",
282 cyclic_follow
: BoolProperty(
283 name
="Cyclic Follow",
284 description
="Make cyclic the face-loops following the strokes",
287 loops_on_strokes
: BoolProperty(
288 name
="Loops on strokes",
289 description
="Make the loops match the paths of the strokes",
292 automatic_join
: BoolProperty(
293 name
="Automatic join",
294 description
="Join automatically vertices of either surfaces generated "
295 "by crosshatching, or from the borders of closed shapes",
298 join_stretch_factor
: FloatProperty(
300 description
="Amount of stretching or shrinking allowed for "
301 "edges when joining vertices automatically",
307 keep_strokes
: BoolProperty(
309 description
="Keeps the sketched strokes or curves after adding the surface",
312 strokes_type
: StringProperty()
313 initial_global_undo_state
: BoolProperty()
316 def draw(self
, context
):
318 col
= layout
.column(align
=True)
321 if not self
.is_fill_faces
:
323 if not self
.is_crosshatch
:
324 if not self
.selection_U_exists
:
325 col
.prop(self
, "edges_U")
328 if not self
.selection_V_exists
:
329 col
.prop(self
, "edges_V")
334 if not self
.selection_U_exists
:
336 (self
.selection_V_exists
and not self
.selection_V_is_closed
) or
337 (self
.selection_V2_exists
and not self
.selection_V2_is_closed
)
339 col
.prop(self
, "cyclic_cross")
341 if not self
.selection_V_exists
:
343 (self
.selection_U_exists
and not self
.selection_U_is_closed
) or
344 (self
.selection_U2_exists
and not self
.selection_U2_is_closed
)
346 col
.prop(self
, "cyclic_follow")
348 col
.prop(self
, "loops_on_strokes")
350 col
.prop(self
, "automatic_join")
352 if self
.automatic_join
:
356 col
.prop(self
, "join_stretch_factor")
358 col
.prop(self
, "keep_strokes")
360 # Get an ordered list of a chain of vertices
361 def get_ordered_verts(self
, ob
, all_selected_edges_idx
, all_selected_verts_idx
,
362 first_vert_idx
, middle_vertex_idx
, closing_vert_idx
):
363 # Order selected vertices.
365 if closing_vert_idx
is not None:
366 verts_ordered
.append(ob
.data
.vertices
[closing_vert_idx
])
368 verts_ordered
.append(ob
.data
.vertices
[first_vert_idx
])
369 prev_v
= first_vert_idx
373 edges_non_matched
= 0
374 for i
in all_selected_edges_idx
:
375 if ob
.data
.edges
[i
] != prev_ed
and ob
.data
.edges
[i
].vertices
[0] == prev_v
and \
376 ob
.data
.edges
[i
].vertices
[1] in all_selected_verts_idx
:
378 verts_ordered
.append(ob
.data
.vertices
[ob
.data
.edges
[i
].vertices
[1]])
379 prev_v
= ob
.data
.edges
[i
].vertices
[1]
380 prev_ed
= ob
.data
.edges
[i
]
381 elif ob
.data
.edges
[i
] != prev_ed
and ob
.data
.edges
[i
].vertices
[1] == prev_v
and \
382 ob
.data
.edges
[i
].vertices
[0] in all_selected_verts_idx
:
384 verts_ordered
.append(ob
.data
.vertices
[ob
.data
.edges
[i
].vertices
[0]])
385 prev_v
= ob
.data
.edges
[i
].vertices
[0]
386 prev_ed
= ob
.data
.edges
[i
]
388 edges_non_matched
+= 1
390 if edges_non_matched
== len(all_selected_edges_idx
):
396 if closing_vert_idx
is not None:
397 verts_ordered
.append(ob
.data
.vertices
[closing_vert_idx
])
399 if middle_vertex_idx
is not None:
400 verts_ordered
.append(ob
.data
.vertices
[middle_vertex_idx
])
401 verts_ordered
.reverse()
403 return tuple(verts_ordered
)
405 # Calculates length of a chain of points.
406 def get_chain_length(self
, object, verts_ordered
):
407 matrix
= object.matrix_world
410 edges_lengths_sum
= 0
411 for i
in range(0, len(verts_ordered
)):
413 prev_v_co
= matrix
@ verts_ordered
[i
].co
415 v_co
= matrix
@ verts_ordered
[i
].co
417 v_difs
= [prev_v_co
[0] - v_co
[0], prev_v_co
[1] - v_co
[1], prev_v_co
[2] - v_co
[2]]
418 edge_length
= abs(sqrt(v_difs
[0] * v_difs
[0] + v_difs
[1] * v_difs
[1] + v_difs
[2] * v_difs
[2]))
420 edges_lengths
.append(edge_length
)
421 edges_lengths_sum
+= edge_length
425 return edges_lengths
, edges_lengths_sum
427 # Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
428 def get_edges_proportions(self
, edges_lengths
, edges_lengths_sum
, use_boundaries
, fixed_edges_num
):
429 edges_proportions
= []
432 for l
in edges_lengths
:
433 edges_proportions
.append(l
/ edges_lengths_sum
)
437 for _n
in range(0, fixed_edges_num
):
438 edges_proportions
.append(1 / fixed_edges_num
)
441 return edges_proportions
443 # Calculates the angle between two pairs of points in space
444 def orientation_difference(self
, points_A_co
, points_B_co
):
445 # each parameter should be a list with two elements,
446 # and each element should be a x,y,z coordinate
447 vec_A
= points_A_co
[0] - points_A_co
[1]
448 vec_B
= points_B_co
[0] - points_B_co
[1]
450 angle
= vec_A
.angle(vec_B
)
453 angle
= abs(angle
- pi
)
457 # Calculate the which vert of verts_idx list is the nearest one
458 # to the point_co coordinates, and the distance
459 def shortest_distance(self
, object, point_co
, verts_idx
):
460 matrix
= object.matrix_world
462 for i
in range(0, len(verts_idx
)):
463 dist
= (point_co
- matrix
@ object.data
.vertices
[verts_idx
[i
]].co
).length
466 nearest_vert_idx
= verts_idx
[i
]
471 nearest_vert_idx
= verts_idx
[i
]
474 return nearest_vert_idx
, shortest_dist
476 # Returns the index of the opposite vert tip in a chain, given a vert tip index
477 # as parameter, and a multidimentional list with all pairs of tips
478 def opposite_tip(self
, vert_tip_idx
, all_chains_tips_idx
):
479 opposite_vert_tip_idx
= None
480 for i
in range(0, len(all_chains_tips_idx
)):
481 if vert_tip_idx
== all_chains_tips_idx
[i
][0]:
482 opposite_vert_tip_idx
= all_chains_tips_idx
[i
][1]
483 if vert_tip_idx
== all_chains_tips_idx
[i
][1]:
484 opposite_vert_tip_idx
= all_chains_tips_idx
[i
][0]
486 return opposite_vert_tip_idx
488 # Simplifies a spline and returns the new points coordinates
489 def simplify_spline(self
, spline_coords
, segments_num
):
490 simplified_spline
= []
491 points_between_segments
= round(len(spline_coords
) / segments_num
)
493 simplified_spline
.append(spline_coords
[0])
494 for i
in range(1, segments_num
):
495 simplified_spline
.append(spline_coords
[i
* points_between_segments
])
497 simplified_spline
.append(spline_coords
[len(spline_coords
) - 1])
499 return simplified_spline
501 # Returns a list with the coords of the points distributed over the splines
502 # passed to this method according to the proportions parameter
503 def distribute_pts(self
, surface_splines
, proportions
):
505 # Calculate the length of each final surface spline
506 surface_splines_lengths
= []
507 surface_splines_parsed
= []
509 for sp_idx
in range(0, len(surface_splines
)):
510 # Calculate spline length
511 surface_splines_lengths
.append(0)
513 for i
in range(0, len(surface_splines
[sp_idx
].bezier_points
)):
515 prev_p
= surface_splines
[sp_idx
].bezier_points
[i
]
517 p
= surface_splines
[sp_idx
].bezier_points
[i
]
518 edge_length
= (prev_p
.co
- p
.co
).length
519 surface_splines_lengths
[sp_idx
] += edge_length
523 # Calculate vertex positions with appropriate edge proportions, and ordered, for each spline
524 for sp_idx
in range(0, len(surface_splines
)):
525 surface_splines_parsed
.append([])
526 surface_splines_parsed
[sp_idx
].append(surface_splines
[sp_idx
].bezier_points
[0].co
)
528 prev_p_co
= surface_splines
[sp_idx
].bezier_points
[0].co
531 for prop_idx
in range(len(proportions
) - 1):
532 target_length
= surface_splines_lengths
[sp_idx
] * proportions
[prop_idx
]
533 partial_segment_length
= 0
537 # if not it'll pass the p_idx as an index below and crash
538 if p_idx
< len(surface_splines
[sp_idx
].bezier_points
):
539 p_co
= surface_splines
[sp_idx
].bezier_points
[p_idx
].co
540 new_dist
= (prev_p_co
- p_co
).length
542 # The new distance that could have the partial segment if
543 # it is still shorter than the target length
544 potential_segment_length
= partial_segment_length
+ new_dist
546 # If the potential is still shorter, keep adding
547 if potential_segment_length
< target_length
:
548 partial_segment_length
= potential_segment_length
553 # If the potential is longer than the target, calculate the target
554 # (a point between the last two points), and assign
555 elif potential_segment_length
> target_length
:
556 remaining_dist
= target_length
- partial_segment_length
557 vec
= p_co
- prev_p_co
559 intermediate_co
= prev_p_co
+ (vec
* remaining_dist
)
561 surface_splines_parsed
[sp_idx
].append(intermediate_co
)
563 partial_segment_length
+= remaining_dist
564 prev_p_co
= intermediate_co
568 # If the potential is equal to the target, assign
569 elif potential_segment_length
== target_length
:
570 surface_splines_parsed
[sp_idx
].append(p_co
)
578 # last point of the spline
579 surface_splines_parsed
[sp_idx
].append(
580 surface_splines
[sp_idx
].bezier_points
[len(surface_splines
[sp_idx
].bezier_points
) - 1].co
583 return surface_splines_parsed
585 # Counts the number of faces that belong to each edge
586 def edge_face_count(self
, ob
):
587 ed_keys_count_dict
= {}
589 for face
in ob
.data
.polygons
:
590 for ed_keys
in face
.edge_keys
:
591 if ed_keys
not in ed_keys_count_dict
:
592 ed_keys_count_dict
[ed_keys
] = 1
594 ed_keys_count_dict
[ed_keys
] += 1
597 for i
in range(len(ob
.data
.edges
)):
598 edge_face_count
.append(0)
600 for i
in range(len(ob
.data
.edges
)):
601 ed
= ob
.data
.edges
[i
]
606 if (v1
, v2
) in ed_keys_count_dict
:
607 edge_face_count
[i
] = ed_keys_count_dict
[(v1
, v2
)]
608 elif (v2
, v1
) in ed_keys_count_dict
:
609 edge_face_count
[i
] = ed_keys_count_dict
[(v2
, v1
)]
611 return edge_face_count
613 # Fills with faces all the selected vertices which form empty triangles or quads
614 def fill_with_faces(self
, object):
615 all_selected_verts_count
= self
.main_object_selected_verts_count
617 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
619 # Calculate average length of selected edges
620 all_selected_verts
= []
621 original_sel_edges_count
= 0
622 for ed
in object.data
.edges
:
623 if object.data
.vertices
[ed
.vertices
[0]].select
and object.data
.vertices
[ed
.vertices
[1]].select
:
625 coords
.append(object.data
.vertices
[ed
.vertices
[0]].co
)
626 coords
.append(object.data
.vertices
[ed
.vertices
[1]].co
)
628 original_sel_edges_count
+= 1
630 if not ed
.vertices
[0] in all_selected_verts
:
631 all_selected_verts
.append(ed
.vertices
[0])
633 if not ed
.vertices
[1] in all_selected_verts
:
634 all_selected_verts
.append(ed
.vertices
[1])
636 tuple(all_selected_verts
)
638 # Check if there is any edge selected. If not, interrupt the script
639 if original_sel_edges_count
== 0 and all_selected_verts_count
> 0:
642 # Get all edges connected to selected verts
643 all_edges_around_sel_verts
= []
644 edges_connected_to_sel_verts
= {}
645 verts_connected_to_every_vert
= {}
646 for ed_idx
in range(len(object.data
.edges
)):
647 ed
= object.data
.edges
[ed_idx
]
650 if ed
.vertices
[0] in all_selected_verts
:
651 if not ed
.vertices
[0] in edges_connected_to_sel_verts
:
652 edges_connected_to_sel_verts
[ed
.vertices
[0]] = []
654 edges_connected_to_sel_verts
[ed
.vertices
[0]].append(ed_idx
)
657 if ed
.vertices
[1] in all_selected_verts
:
658 if not ed
.vertices
[1] in edges_connected_to_sel_verts
:
659 edges_connected_to_sel_verts
[ed
.vertices
[1]] = []
661 edges_connected_to_sel_verts
[ed
.vertices
[1]].append(ed_idx
)
664 if include_edge
is True:
665 all_edges_around_sel_verts
.append(ed_idx
)
667 # Get all connected verts to each vert
668 if not ed
.vertices
[0] in verts_connected_to_every_vert
:
669 verts_connected_to_every_vert
[ed
.vertices
[0]] = []
671 if not ed
.vertices
[1] in verts_connected_to_every_vert
:
672 verts_connected_to_every_vert
[ed
.vertices
[1]] = []
674 verts_connected_to_every_vert
[ed
.vertices
[0]].append(ed
.vertices
[1])
675 verts_connected_to_every_vert
[ed
.vertices
[1]].append(ed
.vertices
[0])
677 # Get all verts connected to faces
678 all_verts_part_of_faces
= []
679 all_edges_faces_count
= []
680 all_edges_faces_count
+= self
.edge_face_count(object)
682 # Get only the selected edges that have faces attached.
683 count_faces_of_edges_around_sel_verts
= {}
684 selected_verts_with_faces
= []
685 for ed_idx
in all_edges_around_sel_verts
:
686 count_faces_of_edges_around_sel_verts
[ed_idx
] = all_edges_faces_count
[ed_idx
]
688 if all_edges_faces_count
[ed_idx
] > 0:
689 ed
= object.data
.edges
[ed_idx
]
691 if not ed
.vertices
[0] in selected_verts_with_faces
:
692 selected_verts_with_faces
.append(ed
.vertices
[0])
694 if not ed
.vertices
[1] in selected_verts_with_faces
:
695 selected_verts_with_faces
.append(ed
.vertices
[1])
697 all_verts_part_of_faces
.append(ed
.vertices
[0])
698 all_verts_part_of_faces
.append(ed
.vertices
[1])
700 tuple(selected_verts_with_faces
)
702 # Discard unneeded verts from calculations
703 participating_verts
= []
705 for v_idx
in all_selected_verts
:
706 vert_has_edges_with_one_face
= False
708 # Check if the actual vert has at least one edge connected to only one face
709 for ed_idx
in edges_connected_to_sel_verts
[v_idx
]:
710 if count_faces_of_edges_around_sel_verts
[ed_idx
] == 1:
711 vert_has_edges_with_one_face
= True
713 # If the vert has two or less edges connected and the vert is not part of any face.
714 # Or the vert is part of any face and at least one of
715 # the connected edges has only one face attached to it.
716 if (len(edges_connected_to_sel_verts
[v_idx
]) == 2 and
717 v_idx
not in all_verts_part_of_faces
) or \
718 len(edges_connected_to_sel_verts
[v_idx
]) == 1 or \
719 (v_idx
in all_verts_part_of_faces
and
720 vert_has_edges_with_one_face
):
722 participating_verts
.append(v_idx
)
724 if v_idx
not in all_verts_part_of_faces
:
725 movable_verts
.append(v_idx
)
727 # Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
728 for mv_idx
in movable_verts
:
730 mv_connected_verts
= verts_connected_to_every_vert
[mv_idx
]
732 for actual_v_idx
in all_selected_verts
:
733 count_shared_neighbors
= 0
736 for mv_conn_v_idx
in mv_connected_verts
:
737 if mv_idx
!= actual_v_idx
:
738 if mv_conn_v_idx
in verts_connected_to_every_vert
[actual_v_idx
] and \
739 mv_conn_v_idx
not in checked_verts
:
740 count_shared_neighbors
+= 1
741 checked_verts
.append(mv_conn_v_idx
)
743 if actual_v_idx
in mv_connected_verts
:
747 if count_shared_neighbors
== 2:
755 movable_verts
.remove(mv_idx
)
757 # Calculate merge distance for participating verts
758 shortest_edge_length
= None
759 for ed
in object.data
.edges
:
760 if ed
.vertices
[0] in movable_verts
and ed
.vertices
[1] in movable_verts
:
761 v1
= object.data
.vertices
[ed
.vertices
[0]]
762 v2
= object.data
.vertices
[ed
.vertices
[1]]
764 length
= (v1
.co
- v2
.co
).length
766 if shortest_edge_length
is None:
767 shortest_edge_length
= length
769 if length
< shortest_edge_length
:
770 shortest_edge_length
= length
772 if shortest_edge_length
is not None:
773 edges_merge_distance
= shortest_edge_length
* 0.5
775 edges_merge_distance
= 0
777 # Get together the verts near enough. They will be merged later
779 remaining_verts
+= participating_verts
780 for v1_idx
in participating_verts
:
781 if v1_idx
in remaining_verts
and v1_idx
in movable_verts
:
783 coords_verts_to_merge
= {}
785 verts_to_merge
.append(v1_idx
)
787 v1_co
= object.data
.vertices
[v1_idx
].co
788 coords_verts_to_merge
[v1_idx
] = (v1_co
[0], v1_co
[1], v1_co
[2])
790 for v2_idx
in remaining_verts
:
792 v2_co
= object.data
.vertices
[v2_idx
].co
794 dist
= (v1_co
- v2_co
).length
796 if dist
<= edges_merge_distance
: # Add the verts which are near enough
797 verts_to_merge
.append(v2_idx
)
799 coords_verts_to_merge
[v2_idx
] = (v2_co
[0], v2_co
[1], v2_co
[2])
801 for vm_idx
in verts_to_merge
:
802 remaining_verts
.remove(vm_idx
)
804 if len(verts_to_merge
) > 1:
805 # Calculate middle point of the verts to merge.
809 movable_verts_to_merge_count
= 0
810 for i
in range(len(verts_to_merge
)):
811 if verts_to_merge
[i
] in movable_verts
:
812 v_co
= object.data
.vertices
[verts_to_merge
[i
]].co
818 movable_verts_to_merge_count
+= 1
821 sum_x_co
/ movable_verts_to_merge_count
,
822 sum_y_co
/ movable_verts_to_merge_count
,
823 sum_z_co
/ movable_verts_to_merge_count
826 # Check if any vert to be merged is not movable
828 are_verts_not_movable
= False
829 verts_not_movable
= []
830 for v_merge_idx
in verts_to_merge
:
831 if v_merge_idx
in participating_verts
and v_merge_idx
not in movable_verts
:
832 are_verts_not_movable
= True
833 verts_not_movable
.append(v_merge_idx
)
835 if are_verts_not_movable
:
836 # Get the vert connected to faces, that is nearest to
837 # the middle point of the movable verts
839 for vcf_idx
in verts_not_movable
:
840 dist
= abs((object.data
.vertices
[vcf_idx
].co
-
841 Vector(middle_point_co
)).length
)
843 if shortest_dist
is None:
845 nearest_vert_idx
= vcf_idx
847 if dist
< shortest_dist
:
849 nearest_vert_idx
= vcf_idx
851 coords
= object.data
.vertices
[nearest_vert_idx
].co
852 target_point_co
= [coords
[0], coords
[1], coords
[2]]
854 target_point_co
= middle_point_co
856 # Move verts to merge to the middle position
857 for v_merge_idx
in verts_to_merge
:
858 if v_merge_idx
in movable_verts
: # Only move the verts that are not part of faces
859 object.data
.vertices
[v_merge_idx
].co
[0] = target_point_co
[0]
860 object.data
.vertices
[v_merge_idx
].co
[1] = target_point_co
[1]
861 object.data
.vertices
[v_merge_idx
].co
[2] = target_point_co
[2]
863 # Perform "Remove Doubles" to weld all the disconnected verts
864 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
865 bpy
.ops
.mesh
.remove_doubles(threshold
=0.0001)
867 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
869 # Get all the definitive selected edges, after weldding
871 edges_per_vert
= {} # Number of faces of each selected edge
872 for ed
in object.data
.edges
:
873 if object.data
.vertices
[ed
.vertices
[0]].select
and object.data
.vertices
[ed
.vertices
[1]].select
:
874 selected_edges
.append(ed
.index
)
876 # Save all the edges that belong to each vertex.
877 if not ed
.vertices
[0] in edges_per_vert
:
878 edges_per_vert
[ed
.vertices
[0]] = []
880 if not ed
.vertices
[1] in edges_per_vert
:
881 edges_per_vert
[ed
.vertices
[1]] = []
883 edges_per_vert
[ed
.vertices
[0]].append(ed
.index
)
884 edges_per_vert
[ed
.vertices
[1]].append(ed
.index
)
886 # Check if all the edges connected to each vert have two faces attached to them.
887 # To discard them later and make calculations faster
889 a
+= self
.edge_face_count(object)
891 verts_surrounded_by_faces
= {}
892 for v_idx
in edges_per_vert
:
893 edges_with_two_faces_count
= 0
895 for ed_idx
in edges_per_vert
[v_idx
]:
897 edges_with_two_faces_count
+= 1
899 if edges_with_two_faces_count
== len(edges_per_vert
[v_idx
]):
900 verts_surrounded_by_faces
[v_idx
] = True
902 verts_surrounded_by_faces
[v_idx
] = False
904 # Get all the selected vertices
905 selected_verts_idx
= []
906 for v
in object.data
.vertices
:
908 selected_verts_idx
.append(v
.index
)
910 # Get all the faces of the object
911 all_object_faces_verts_idx
= []
912 for face
in object.data
.polygons
:
914 face_verts
.append(face
.vertices
[0])
915 face_verts
.append(face
.vertices
[1])
916 face_verts
.append(face
.vertices
[2])
918 if len(face
.vertices
) == 4:
919 face_verts
.append(face
.vertices
[3])
921 all_object_faces_verts_idx
.append(face_verts
)
923 # Deselect all vertices
924 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
925 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
926 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
928 # Make a dictionary with the verts related to each vert
929 related_key_verts
= {}
930 for ed_idx
in selected_edges
:
931 ed
= object.data
.edges
[ed_idx
]
933 if not verts_surrounded_by_faces
[ed
.vertices
[0]]:
934 if not ed
.vertices
[0] in related_key_verts
:
935 related_key_verts
[ed
.vertices
[0]] = []
937 if not ed
.vertices
[1] in related_key_verts
[ed
.vertices
[0]]:
938 related_key_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
940 if not verts_surrounded_by_faces
[ed
.vertices
[1]]:
941 if not ed
.vertices
[1] in related_key_verts
:
942 related_key_verts
[ed
.vertices
[1]] = []
944 if not ed
.vertices
[0] in related_key_verts
[ed
.vertices
[1]]:
945 related_key_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
947 # Get groups of verts forming each face
949 for v1
in related_key_verts
: # verts-1 ....
950 for v2
in related_key_verts
: # verts-2
952 related_verts_in_common
= []
955 for rel_v1
in related_key_verts
[v1
]:
956 # Check if related verts of verts-1 are related verts of verts-2
957 if rel_v1
in related_key_verts
[v2
]:
958 related_verts_in_common
.append(rel_v1
)
960 if v2
in related_key_verts
[v1
]:
963 if v1
in related_key_verts
[v2
]:
966 repeated_face
= False
967 # If two verts have two related verts in common, they form a quad
968 if len(related_verts_in_common
) == 2:
969 # Check if the face is already saved
970 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
972 for f_verts
in all_faces_to_check_idx
:
975 if len(f_verts
) == 4:
980 if related_verts_in_common
[0] in f_verts
:
982 if related_verts_in_common
[1] in f_verts
:
985 if repeated_verts
== len(f_verts
):
989 if not repeated_face
:
990 faces_verts_idx
.append(
991 [v1
, related_verts_in_common
[0], v2
, related_verts_in_common
[1]]
994 # If Two verts have one related vert in common and
995 # they are related to each other, they form a triangle
996 elif v2_in_rel_v1
and v1_in_rel_v2
and len(related_verts_in_common
) == 1:
997 # Check if the face is already saved.
998 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
1000 for f_verts
in all_faces_to_check_idx
:
1003 if len(f_verts
) == 3:
1008 if related_verts_in_common
[0] in f_verts
:
1011 if repeated_verts
== len(f_verts
):
1012 repeated_face
= True
1015 if not repeated_face
:
1016 faces_verts_idx
.append([v1
, related_verts_in_common
[0], v2
])
1018 # Keep only the faces that don't overlap by ignoring quads
1019 # that overlap with two adjacent triangles
1020 faces_to_not_include_idx
= [] # Indices of faces_verts_idx to eliminate
1021 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
1022 for i
in range(len(faces_verts_idx
)):
1023 for t
in range(len(all_faces_to_check_idx
)):
1027 if len(faces_verts_idx
[i
]) == 4 and len(all_faces_to_check_idx
[t
]) == 3:
1028 for v_idx
in all_faces_to_check_idx
[t
]:
1029 if v_idx
in faces_verts_idx
[i
]:
1030 verts_in_common
+= 1
1031 # If it doesn't have all it's vertices repeated in the other face
1032 if verts_in_common
== 3:
1033 if i
not in faces_to_not_include_idx
:
1034 faces_to_not_include_idx
.append(i
)
1036 # Build faces discarding the ones in faces_to_not_include
1041 num_faces_created
= 0
1042 for i
in range(len(faces_verts_idx
)):
1043 if i
not in faces_to_not_include_idx
:
1044 bm
.faces
.new([bm
.verts
[v
] for v
in faces_verts_idx
[i
]])
1046 num_faces_created
+= 1
1051 for v_idx
in selected_verts_idx
:
1052 self
.main_object
.data
.vertices
[v_idx
].select
= True
1054 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
1055 bpy
.ops
.mesh
.normals_make_consistent(inside
=False)
1056 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
1060 return num_faces_created
1062 # Crosshatch skinning
1063 def crosshatch_surface_invoke(self
, ob_original_splines
):
1064 self
.is_crosshatch
= False
1065 self
.crosshatch_merge_distance
= 0
1067 objects_to_delete
= [] # duplicated strokes to be deleted.
1069 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1070 # (without this the surface verts merging with the main object doesn't work well)
1071 self
.modifiers_prev_viewport_state
= []
1072 if len(self
.main_object
.modifiers
) > 0:
1073 for m_idx
in range(len(self
.main_object
.modifiers
)):
1074 self
.modifiers_prev_viewport_state
.append(
1075 self
.main_object
.modifiers
[m_idx
].show_viewport
1077 self
.main_object
.modifiers
[m_idx
].show_viewport
= False
1079 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1080 ob_original_splines
.select_set(True)
1081 bpy
.context
.view_layer
.objects
.active
= ob_original_splines
1083 if len(ob_original_splines
.data
.splines
) >= 2:
1084 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1085 ob_splines
= bpy
.context
.object
1086 ob_splines
.name
= "SURFSKIO_NE_STR"
1088 # Get estimative merge distance (sum up the distances from the first point to
1089 # all other points, then average them and then divide them)
1090 first_point_dist_sum
= 0
1093 coords_first_pt
= ob_splines
.data
.splines
[0].bezier_points
[0].co
1094 for i
in range(len(ob_splines
.data
.splines
)):
1095 sp
= ob_splines
.data
.splines
[i
]
1097 if coords_first_pt
!= sp
.bezier_points
[0].co
:
1098 first_dist
= (coords_first_pt
- sp
.bezier_points
[0].co
).length
1100 if coords_first_pt
!= sp
.bezier_points
[len(sp
.bezier_points
) - 1].co
:
1101 second_dist
= (coords_first_pt
- sp
.bezier_points
[len(sp
.bezier_points
) - 1].co
).length
1103 first_point_dist_sum
+= first_dist
+ second_dist
1107 shortest_dist
= first_dist
1108 elif second_dist
!= 0:
1109 shortest_dist
= second_dist
1111 if shortest_dist
> first_dist
and first_dist
!= 0:
1112 shortest_dist
= first_dist
1114 if shortest_dist
> second_dist
and second_dist
!= 0:
1115 shortest_dist
= second_dist
1117 self
.crosshatch_merge_distance
= shortest_dist
/ 20
1119 # Recalculation of merge distance
1121 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1123 ob_calc_merge_dist
= bpy
.context
.object
1124 ob_calc_merge_dist
.name
= "SURFSKIO_CALC_TMP"
1126 objects_to_delete
.append(ob_calc_merge_dist
)
1128 # Smooth out strokes a little to improve crosshatch detection
1129 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1130 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1133 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1135 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1136 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1138 # Convert curves into mesh
1139 ob_calc_merge_dist
.data
.resolution_u
= 12
1140 bpy
.ops
.object.convert(target
='MESH', keep_original
=False)
1142 # Find "intersection-nodes"
1143 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1144 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1145 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN',
1146 threshold
=self
.crosshatch_merge_distance
)
1147 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1148 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1150 # Remove verts with less than three edges
1151 verts_edges_count
= {}
1152 for ed
in ob_calc_merge_dist
.data
.edges
:
1155 if v
[0] not in verts_edges_count
:
1156 verts_edges_count
[v
[0]] = 0
1158 if v
[1] not in verts_edges_count
:
1159 verts_edges_count
[v
[1]] = 0
1161 verts_edges_count
[v
[0]] += 1
1162 verts_edges_count
[v
[1]] += 1
1164 nodes_verts_coords
= []
1165 for v_idx
in verts_edges_count
:
1166 v
= ob_calc_merge_dist
.data
.vertices
[v_idx
]
1168 if verts_edges_count
[v_idx
] < 3:
1172 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1173 bpy
.ops
.mesh
.delete('INVOKE_REGION_WIN', type='VERT')
1174 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1176 # Remove doubles to discard very near verts from calculations of distance
1177 bpy
.ops
.mesh
.remove_doubles(
1178 'INVOKE_REGION_WIN',
1179 threshold
=self
.crosshatch_merge_distance
* 4.0
1181 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1183 # Get all coords of the resulting nodes
1184 nodes_verts_coords
= [(v
.co
[0], v
.co
[1], v
.co
[2]) for
1185 v
in ob_calc_merge_dist
.data
.vertices
]
1187 # Check if the strokes are a crosshatch
1188 if len(nodes_verts_coords
) >= 3:
1189 self
.is_crosshatch
= True
1191 shortest_dist
= None
1192 for co_1
in nodes_verts_coords
:
1193 for co_2
in nodes_verts_coords
:
1195 dist
= (Vector(co_1
) - Vector(co_2
)).length
1197 if shortest_dist
is not None:
1198 if dist
< shortest_dist
:
1199 shortest_dist
= dist
1201 shortest_dist
= dist
1203 self
.crosshatch_merge_distance
= shortest_dist
/ 3
1205 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1206 ob_splines
.select_set(True)
1207 bpy
.context
.view_layer
.objects
.active
= ob_splines
1209 # Deselect all points
1210 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1211 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1212 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1214 # Smooth splines in a localized way, to eliminate "saw-teeth"
1215 # like shapes when there are many points
1216 for sp
in ob_splines
.data
.splines
:
1219 angle_limit
= 2 # Degrees
1220 for t
in range(len(sp
.bezier_points
)):
1221 # Because on each iteration it checks the "next two points"
1222 # of the actual. This way it doesn't go out of range
1223 if t
<= len(sp
.bezier_points
) - 3:
1224 p1
= sp
.bezier_points
[t
]
1225 p2
= sp
.bezier_points
[t
+ 1]
1226 p3
= sp
.bezier_points
[t
+ 2]
1228 vec_1
= p1
.co
- p2
.co
1229 vec_2
= p2
.co
- p3
.co
1231 if p2
.co
!= p1
.co
and p2
.co
!= p3
.co
:
1232 angle
= vec_1
.angle(vec_2
)
1233 angle_sum
+= degrees(angle
)
1235 if angle_sum
>= angle_limit
: # If sum of angles is grater than the limit
1236 if (p1
.co
- p2
.co
).length
<= self
.crosshatch_merge_distance
:
1237 p1
.select_control_point
= True
1238 p1
.select_left_handle
= True
1239 p1
.select_right_handle
= True
1241 p2
.select_control_point
= True
1242 p2
.select_left_handle
= True
1243 p2
.select_right_handle
= True
1245 if (p1
.co
- p2
.co
).length
<= self
.crosshatch_merge_distance
:
1246 p3
.select_control_point
= True
1247 p3
.select_left_handle
= True
1248 p3
.select_right_handle
= True
1252 sp
.bezier_points
[0].select_control_point
= False
1253 sp
.bezier_points
[0].select_left_handle
= False
1254 sp
.bezier_points
[0].select_right_handle
= False
1256 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_control_point
= False
1257 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_left_handle
= False
1258 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_right_handle
= False
1260 # Smooth out strokes a little to improve crosshatch detection
1261 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1264 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1266 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1267 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1269 # Simplify the splines
1270 for sp
in ob_splines
.data
.splines
:
1273 sp
.bezier_points
[0].select_control_point
= True
1274 sp
.bezier_points
[0].select_left_handle
= True
1275 sp
.bezier_points
[0].select_right_handle
= True
1277 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_control_point
= True
1278 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_left_handle
= True
1279 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_right_handle
= True
1281 angle_limit
= 15 # Degrees
1282 for t
in range(len(sp
.bezier_points
)):
1283 # Because on each iteration it checks the "next two points"
1284 # of the actual. This way it doesn't go out of range
1285 if t
<= len(sp
.bezier_points
) - 3:
1286 p1
= sp
.bezier_points
[t
]
1287 p2
= sp
.bezier_points
[t
+ 1]
1288 p3
= sp
.bezier_points
[t
+ 2]
1290 vec_1
= p1
.co
- p2
.co
1291 vec_2
= p2
.co
- p3
.co
1293 if p2
.co
!= p1
.co
and p2
.co
!= p3
.co
:
1294 angle
= vec_1
.angle(vec_2
)
1295 angle_sum
+= degrees(angle
)
1296 # If sum of angles is grater than the limit
1297 if angle_sum
>= angle_limit
:
1298 p1
.select_control_point
= True
1299 p1
.select_left_handle
= True
1300 p1
.select_right_handle
= True
1302 p2
.select_control_point
= True
1303 p2
.select_left_handle
= True
1304 p2
.select_right_handle
= True
1306 p3
.select_control_point
= True
1307 p3
.select_left_handle
= True
1308 p3
.select_right_handle
= True
1312 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1313 bpy
.ops
.curve
.select_all(action
='INVERT')
1315 bpy
.ops
.curve
.delete(type='VERT')
1316 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1318 objects_to_delete
.append(ob_splines
)
1320 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1321 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1322 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1324 # Check if the strokes are a crosshatch
1325 if self
.is_crosshatch
:
1326 all_points_coords
= []
1327 for i
in range(len(ob_splines
.data
.splines
)):
1328 all_points_coords
.append([])
1330 all_points_coords
[i
] = [Vector((x
, y
, z
)) for
1331 x
, y
, z
in [bp
.co
for
1332 bp
in ob_splines
.data
.splines
[i
].bezier_points
]]
1334 all_intersections
= []
1335 checked_splines
= []
1336 for i
in range(len(all_points_coords
)):
1338 for t
in range(len(all_points_coords
[i
]) - 1):
1339 bp1_co
= all_points_coords
[i
][t
]
1340 bp2_co
= all_points_coords
[i
][t
+ 1]
1342 for i2
in range(len(all_points_coords
)):
1343 if i
!= i2
and i2
not in checked_splines
:
1344 for t2
in range(len(all_points_coords
[i2
]) - 1):
1345 bp3_co
= all_points_coords
[i2
][t2
]
1346 bp4_co
= all_points_coords
[i2
][t2
+ 1]
1348 intersec_coords
= intersect_line_line(
1349 bp1_co
, bp2_co
, bp3_co
, bp4_co
1351 if intersec_coords
is not None:
1352 dist
= (intersec_coords
[0] - intersec_coords
[1]).length
1354 if dist
<= self
.crosshatch_merge_distance
* 1.5:
1355 _temp_co
, percent1
= intersect_point_line(
1356 intersec_coords
[0], bp1_co
, bp2_co
1358 if (percent1
>= -0.02 and percent1
<= 1.02):
1359 _temp_co
, percent2
= intersect_point_line(
1360 intersec_coords
[1], bp3_co
, bp4_co
1362 if (percent2
>= -0.02 and percent2
<= 1.02):
1363 # Format: spline index, first point index from
1364 # corresponding segment, percentage from first point of
1365 # actual segment, coords of intersection point
1366 all_intersections
.append(
1368 ob_splines
.matrix_world
@ intersec_coords
[0])
1370 all_intersections
.append(
1372 ob_splines
.matrix_world
@ intersec_coords
[1])
1375 checked_splines
.append(i
)
1376 # Sort list by spline, then by corresponding first point index of segment,
1377 # and then by percentage from first point of segment: elements 0 and 1 respectively
1378 all_intersections
.sort(key
=operator
.itemgetter(0, 1, 2))
1380 self
.crosshatch_strokes_coords
= {}
1381 for i
in range(len(all_intersections
)):
1382 if not all_intersections
[i
][0] in self
.crosshatch_strokes_coords
:
1383 self
.crosshatch_strokes_coords
[all_intersections
[i
][0]] = []
1385 self
.crosshatch_strokes_coords
[all_intersections
[i
][0]].append(
1386 all_intersections
[i
][3]
1387 ) # Save intersection coords
1389 self
.is_crosshatch
= False
1391 # Delete all duplicates
1392 with bpy
.context
.temp_override(selected_objects
=objects_to_delete
):
1393 bpy
.ops
.object.delete()
1395 # If the main object has modifiers, turn their "viewport view status" to
1396 # what it was before the forced deactivation above
1397 if len(self
.main_object
.modifiers
) > 0:
1398 for m_idx
in range(len(self
.main_object
.modifiers
)):
1399 self
.main_object
.modifiers
[m_idx
].show_viewport
= self
.modifiers_prev_viewport_state
[m_idx
]
1405 # Part of the Crosshatch process that is repeated when the operator is tweaked
1406 def crosshatch_surface_execute(self
, context
):
1407 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1408 # (without this the surface verts merging with the main object doesn't work well)
1409 self
.modifiers_prev_viewport_state
= []
1410 if len(self
.main_object
.modifiers
) > 0:
1411 for m_idx
in range(len(self
.main_object
.modifiers
)):
1412 self
.modifiers_prev_viewport_state
.append(self
.main_object
.modifiers
[m_idx
].show_viewport
)
1414 self
.main_object
.modifiers
[m_idx
].show_viewport
= False
1416 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1418 me_name
= "SURFSKIO_STK_TMP"
1419 me
= bpy
.data
.meshes
.new(me_name
)
1421 all_verts_coords
= []
1423 for st_idx
in self
.crosshatch_strokes_coords
:
1424 for co_idx
in range(len(self
.crosshatch_strokes_coords
[st_idx
])):
1425 coords
= self
.crosshatch_strokes_coords
[st_idx
][co_idx
]
1427 all_verts_coords
.append(coords
)
1430 all_edges
.append((len(all_verts_coords
) - 2, len(all_verts_coords
) - 1))
1432 me
.from_pydata(all_verts_coords
, all_edges
, [])
1433 ob
= object_utils
.object_data_add(context
, me
)
1434 ob
.location
= (0.0, 0.0, 0.0)
1435 ob
.rotation_euler
= (0.0, 0.0, 0.0)
1436 ob
.scale
= (1.0, 1.0, 1.0)
1438 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1440 bpy
.context
.view_layer
.objects
.active
= ob
1442 # Get together each vert and its nearest, to the middle position
1443 verts
= ob
.data
.vertices
1445 for i
in range(len(verts
)):
1446 shortest_dist
= None
1448 if i
not in checked_verts
:
1449 for t
in range(len(verts
)):
1450 if i
!= t
and t
not in checked_verts
:
1451 dist
= (verts
[i
].co
- verts
[t
].co
).length
1453 if shortest_dist
is not None:
1454 if dist
< shortest_dist
:
1455 shortest_dist
= dist
1458 shortest_dist
= dist
1461 middle_location
= (verts
[i
].co
+ verts
[nearest_vert
].co
) / 2
1463 verts
[i
].co
= middle_location
1464 verts
[nearest_vert
].co
= middle_location
1466 checked_verts
.append(i
)
1467 checked_verts
.append(nearest_vert
)
1469 # Calculate average length between all the generated edges
1470 ob
= bpy
.context
.object
1472 for ed
in ob
.data
.edges
:
1473 v1
= ob
.data
.vertices
[ed
.vertices
[0]]
1474 v2
= ob
.data
.vertices
[ed
.vertices
[1]]
1476 lengths_sum
+= (v1
.co
- v2
.co
).length
1478 edges_count
= len(ob
.data
.edges
)
1479 # possible division by zero here
1480 average_edge_length
= lengths_sum
/ edges_count
if edges_count
!= 0 else 0.0001
1482 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1483 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1484 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN',
1485 threshold
=average_edge_length
/ 15.0)
1486 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1488 final_points_ob
= bpy
.context
.view_layer
.objects
.active
1490 # Make a dictionary with the verts related to each vert
1491 related_key_verts
= {}
1492 for ed
in final_points_ob
.data
.edges
:
1493 if not ed
.vertices
[0] in related_key_verts
:
1494 related_key_verts
[ed
.vertices
[0]] = []
1496 if not ed
.vertices
[1] in related_key_verts
:
1497 related_key_verts
[ed
.vertices
[1]] = []
1499 if not ed
.vertices
[1] in related_key_verts
[ed
.vertices
[0]]:
1500 related_key_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
1502 if not ed
.vertices
[0] in related_key_verts
[ed
.vertices
[1]]:
1503 related_key_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
1505 # Get groups of verts forming each face
1506 faces_verts_idx
= []
1507 for v1
in related_key_verts
: # verts-1 ....
1508 for v2
in related_key_verts
: # verts-2
1510 related_verts_in_common
= []
1511 v2_in_rel_v1
= False
1512 v1_in_rel_v2
= False
1513 for rel_v1
in related_key_verts
[v1
]:
1514 # Check if related verts of verts-1 are related verts of verts-2
1515 if rel_v1
in related_key_verts
[v2
]:
1516 related_verts_in_common
.append(rel_v1
)
1518 if v2
in related_key_verts
[v1
]:
1521 if v1
in related_key_verts
[v2
]:
1524 repeated_face
= False
1525 # If two verts have two related verts in common, they form a quad
1526 if len(related_verts_in_common
) == 2:
1527 # Check if the face is already saved
1528 for f_verts
in faces_verts_idx
:
1531 if len(f_verts
) == 4:
1536 if related_verts_in_common
[0] in f_verts
:
1538 if related_verts_in_common
[1] in f_verts
:
1541 if repeated_verts
== len(f_verts
):
1542 repeated_face
= True
1545 if not repeated_face
:
1546 faces_verts_idx
.append([v1
, related_verts_in_common
[0],
1547 v2
, related_verts_in_common
[1]])
1549 # If Two verts have one related vert in common and they are
1550 # related to each other, they form a triangle
1551 elif v2_in_rel_v1
and v1_in_rel_v2
and len(related_verts_in_common
) == 1:
1552 # Check if the face is already saved.
1553 for f_verts
in faces_verts_idx
:
1556 if len(f_verts
) == 3:
1561 if related_verts_in_common
[0] in f_verts
:
1564 if repeated_verts
== len(f_verts
):
1565 repeated_face
= True
1568 if not repeated_face
:
1569 faces_verts_idx
.append([v1
, related_verts_in_common
[0], v2
])
1571 # Keep only the faces that don't overlap by ignoring
1572 # quads that overlap with two adjacent triangles
1573 faces_to_not_include_idx
= [] # Indices of faces_verts_idx to eliminate
1574 for i
in range(len(faces_verts_idx
)):
1575 for t
in range(len(faces_verts_idx
)):
1579 if len(faces_verts_idx
[i
]) == 4 and len(faces_verts_idx
[t
]) == 3:
1580 for v_idx
in faces_verts_idx
[t
]:
1581 if v_idx
in faces_verts_idx
[i
]:
1582 verts_in_common
+= 1
1583 # If it doesn't have all it's vertices repeated in the other face
1584 if verts_in_common
== 3:
1585 if i
not in faces_to_not_include_idx
:
1586 faces_to_not_include_idx
.append(i
)
1589 all_surface_verts_co
= []
1590 for i
in range(len(final_points_ob
.data
.vertices
)):
1591 coords
= final_points_ob
.data
.vertices
[i
].co
1592 all_surface_verts_co
.append([coords
[0], coords
[1], coords
[2]])
1594 # Verts of each face.
1595 all_surface_faces
= []
1596 for i
in range(len(faces_verts_idx
)):
1597 if i
not in faces_to_not_include_idx
:
1599 for v_idx
in faces_verts_idx
[i
]:
1602 all_surface_faces
.append(face
)
1605 surf_me_name
= "SURFSKIO_surface"
1606 me_surf
= bpy
.data
.meshes
.new(surf_me_name
)
1607 me_surf
.from_pydata(all_surface_verts_co
, [], all_surface_faces
)
1608 ob_surface
= object_utils
.object_data_add(context
, me_surf
)
1609 ob_surface
.location
= (0.0, 0.0, 0.0)
1610 ob_surface
.rotation_euler
= (0.0, 0.0, 0.0)
1611 ob_surface
.scale
= (1.0, 1.0, 1.0)
1613 # Delete final points temporal object
1614 with bpy
.context
.temp_override(selected_objects
=[final_points_ob
]):
1615 bpy
.ops
.object.delete()
1617 # Delete isolated verts if there are any
1618 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1619 ob_surface
.select_set(True)
1620 bpy
.context
.view_layer
.objects
.active
= ob_surface
1622 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1623 bpy
.ops
.mesh
.select_all(action
='DESELECT')
1624 bpy
.ops
.mesh
.select_face_by_sides(type='NOTEQUAL')
1625 bpy
.ops
.mesh
.delete()
1626 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1628 # Join crosshatch results with original mesh
1630 # Calculate a distance to merge the verts of the crosshatch surface to the main object
1631 edges_length_sum
= 0
1632 for ed
in ob_surface
.data
.edges
:
1633 edges_length_sum
+= (
1634 ob_surface
.data
.vertices
[ed
.vertices
[0]].co
-
1635 ob_surface
.data
.vertices
[ed
.vertices
[1]].co
1638 # Make dictionary with all the verts connected to each vert, on the new surface object.
1639 surface_connected_verts
= {}
1640 for ed
in ob_surface
.data
.edges
:
1641 if not ed
.vertices
[0] in surface_connected_verts
:
1642 surface_connected_verts
[ed
.vertices
[0]] = []
1644 surface_connected_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
1646 if ed
.vertices
[1] not in surface_connected_verts
:
1647 surface_connected_verts
[ed
.vertices
[1]] = []
1649 surface_connected_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
1651 # Duplicate the new surface object, and use shrinkwrap to
1652 # calculate later the nearest verts to the main object
1653 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1654 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1655 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1657 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1659 final_ob_duplicate
= bpy
.context
.view_layer
.objects
.active
1661 shrinkwrap_modifier
= context
.object.modifiers
.new("", 'SHRINKWRAP')
1662 shrinkwrap_modifier
.wrap_method
= "NEAREST_VERTEX"
1663 shrinkwrap_modifier
.target
= self
.main_object
1665 bpy
.ops
.object.modifier_apply('INVOKE_REGION_WIN', modifier
=shrinkwrap_modifier
.name
)
1667 # Make list with verts of original mesh as index and coords as value
1668 main_object_verts_coords
= []
1669 for v
in self
.main_object
.data
.vertices
:
1670 coords
= self
.main_object
.matrix_world
@ v
.co
1672 # To avoid problems when taking "-0.00" as a different value as "0.00"
1673 for c
in range(len(coords
)):
1674 if "%.3f" % coords
[c
] == "-0.00":
1677 main_object_verts_coords
.append(["%.3f" % coords
[0], "%.3f" % coords
[1], "%.3f" % coords
[2]])
1679 tuple(main_object_verts_coords
)
1681 # Determine which verts will be merged, snap them to the nearest verts
1682 # on the original verts, and get them selected
1683 crosshatch_verts_to_merge
= []
1684 if self
.automatic_join
:
1685 for i
in range(len(ob_surface
.data
.vertices
)-1):
1686 # Calculate the distance from each of the connected verts to the actual vert,
1687 # and compare it with the distance they would have if joined.
1688 # If they don't change much, that vert can be joined
1689 merge_actual_vert
= True
1691 if len(surface_connected_verts
[i
]) < 4:
1692 for c_v_idx
in surface_connected_verts
[i
]:
1693 points_original
= []
1694 points_original
.append(ob_surface
.data
.vertices
[c_v_idx
].co
)
1695 points_original
.append(ob_surface
.data
.vertices
[i
].co
)
1698 points_target
.append(ob_surface
.data
.vertices
[c_v_idx
].co
)
1699 points_target
.append(final_ob_duplicate
.data
.vertices
[i
].co
)
1701 vec_A
= points_original
[0] - points_original
[1]
1702 vec_B
= points_target
[0] - points_target
[1]
1704 dist_A
= (points_original
[0] - points_original
[1]).length
1705 dist_B
= (points_target
[0] - points_target
[1]).length
1708 points_original
[0] == points_original
[1] or
1709 points_target
[0] == points_target
[1]
1710 ): # If any vector's length is zero
1712 angle
= vec_A
.angle(vec_B
) / pi
1716 # Set a range of acceptable variation in the connected edges
1717 if dist_B
> dist_A
* 1.7 * self
.join_stretch_factor
or \
1718 dist_B
< dist_A
/ 2 / self
.join_stretch_factor
or \
1719 angle
>= 0.15 * self
.join_stretch_factor
:
1721 merge_actual_vert
= False
1724 merge_actual_vert
= False
1726 self
.report({'WARNING'},
1727 "Crosshatch set incorrectly")
1729 if merge_actual_vert
:
1730 coords
= final_ob_duplicate
.data
.vertices
[i
].co
1731 # To avoid problems when taking "-0.000" as a different value as "0.00"
1732 for c
in range(len(coords
)):
1733 if "%.3f" % coords
[c
] == "-0.00":
1736 comparison_coords
= ["%.3f" % coords
[0], "%.3f" % coords
[1], "%.3f" % coords
[2]]
1738 if comparison_coords
in main_object_verts_coords
:
1739 # Get the index of the vert with those coords in the main object
1740 main_object_related_vert_idx
= main_object_verts_coords
.index(comparison_coords
)
1742 if self
.main_object
.data
.vertices
[main_object_related_vert_idx
].select
is True or \
1743 self
.main_object_selected_verts_count
== 0:
1745 ob_surface
.data
.vertices
[i
].co
= final_ob_duplicate
.data
.vertices
[i
].co
1746 ob_surface
.data
.vertices
[i
].select
= True
1747 crosshatch_verts_to_merge
.append(i
)
1749 # Make sure the vert in the main object is selected,
1750 # in case it wasn't selected and the "join crosshatch" option is active
1751 self
.main_object
.data
.vertices
[main_object_related_vert_idx
].select
= True
1753 # Delete duplicated object
1754 with bpy
.context
.temp_override(selected_objects
=[final_ob_duplicate
]):
1755 bpy
.ops
.object.delete()
1757 # Join crosshatched surface and main object
1758 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1759 ob_surface
.select_set(True)
1760 self
.main_object
.select_set(True)
1761 bpy
.context
.view_layer
.objects
.active
= self
.main_object
1763 bpy
.ops
.object.join('INVOKE_REGION_WIN')
1765 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1766 # Perform Remove doubles to merge verts
1767 if not (self
.automatic_join
is False and self
.main_object_selected_verts_count
== 0):
1768 bpy
.ops
.mesh
.remove_doubles(threshold
=0.0001)
1770 bpy
.ops
.mesh
.select_all(action
='DESELECT')
1772 # If the main object has modifiers, turn their "viewport view status"
1773 # to what it was before the forced deactivation above
1774 if len(self
.main_object
.modifiers
) > 0:
1775 for m_idx
in range(len(self
.main_object
.modifiers
)):
1776 self
.main_object
.modifiers
[m_idx
].show_viewport
= self
.modifiers_prev_viewport_state
[m_idx
]
1782 def rectangular_surface(self
, context
):
1784 all_selected_edges_idx
= []
1785 all_selected_verts
= []
1787 for ed
in self
.main_object
.data
.edges
:
1789 all_selected_edges_idx
.append(ed
.index
)
1792 if not ed
.vertices
[0] in all_selected_verts
:
1793 all_selected_verts
.append(self
.main_object
.data
.vertices
[ed
.vertices
[0]])
1794 if not ed
.vertices
[1] in all_selected_verts
:
1795 all_selected_verts
.append(self
.main_object
.data
.vertices
[ed
.vertices
[1]])
1797 # All verts (both from each edge) to determine later
1798 # which are at the tips (those not repeated twice)
1799 all_verts_idx
.append(ed
.vertices
[0])
1800 all_verts_idx
.append(ed
.vertices
[1])
1802 # Identify the tips and "middle-vertex" that separates U from V, if there is one
1803 all_chains_tips_idx
= []
1804 for v_idx
in all_verts_idx
:
1805 if all_verts_idx
.count(v_idx
) < 2:
1806 all_chains_tips_idx
.append(v_idx
)
1808 edges_connected_to_tips
= []
1809 for ed
in self
.main_object
.data
.edges
:
1810 if (ed
.vertices
[0] in all_chains_tips_idx
or ed
.vertices
[1] in all_chains_tips_idx
) and \
1811 not (ed
.vertices
[0] in all_verts_idx
and ed
.vertices
[1] in all_verts_idx
):
1813 edges_connected_to_tips
.append(ed
)
1815 # Check closed selections
1816 # List with groups of three verts, where the first element of the pair is
1817 # the unselected vert of a closed selection and the other two elements are the
1818 # selected neighbor verts (it will be useful to determine which selection chain
1819 # the unselected vert belongs to, and determine the "middle-vertex")
1820 single_unselected_verts_and_neighbors
= []
1822 # To identify a "closed" selection (a selection that is a closed chain except
1823 # for one vertex) find the vertex in common that have the edges connected to tips.
1824 # If there is a vertex in common, that one is the unselected vert that closes
1825 # the selection or is a "middle-vertex"
1826 single_unselected_verts
= []
1827 for ed
in edges_connected_to_tips
:
1828 for ed_b
in edges_connected_to_tips
:
1830 if ed
.vertices
[0] == ed_b
.vertices
[0] and \
1831 not self
.main_object
.data
.vertices
[ed
.vertices
[0]].select
and \
1832 ed
.vertices
[0] not in single_unselected_verts
:
1834 # The second element is one of the tips of the selected
1835 # vertices of the closed selection
1836 single_unselected_verts_and_neighbors
.append(
1837 [ed
.vertices
[0], ed
.vertices
[1], ed_b
.vertices
[1]]
1839 single_unselected_verts
.append(ed
.vertices
[0])
1841 elif ed
.vertices
[0] == ed_b
.vertices
[1] and \
1842 not self
.main_object
.data
.vertices
[ed
.vertices
[0]].select
and \
1843 ed
.vertices
[0] not in single_unselected_verts
:
1845 single_unselected_verts_and_neighbors
.append(
1846 [ed
.vertices
[0], ed
.vertices
[1], ed_b
.vertices
[0]]
1848 single_unselected_verts
.append(ed
.vertices
[0])
1850 elif ed
.vertices
[1] == ed_b
.vertices
[0] and \
1851 not self
.main_object
.data
.vertices
[ed
.vertices
[1]].select
and \
1852 ed
.vertices
[1] not in single_unselected_verts
:
1854 single_unselected_verts_and_neighbors
.append(
1855 [ed
.vertices
[1], ed
.vertices
[0], ed_b
.vertices
[1]]
1857 single_unselected_verts
.append(ed
.vertices
[1])
1859 elif ed
.vertices
[1] == ed_b
.vertices
[1] and \
1860 not self
.main_object
.data
.vertices
[ed
.vertices
[1]].select
and \
1861 ed
.vertices
[1] not in single_unselected_verts
:
1863 single_unselected_verts_and_neighbors
.append(
1864 [ed
.vertices
[1], ed
.vertices
[0], ed_b
.vertices
[0]]
1866 single_unselected_verts
.append(ed
.vertices
[1])
1869 middle_vertex_idx
= None
1870 tips_to_discard_idx
= []
1872 # Check if there is a "middle-vertex", and get its index
1873 for i
in range(0, len(single_unselected_verts_and_neighbors
)):
1874 actual_chain_verts
= self
.get_ordered_verts(
1875 self
.main_object
, all_selected_edges_idx
,
1876 all_verts_idx
, single_unselected_verts_and_neighbors
[i
][1],
1880 if single_unselected_verts_and_neighbors
[i
][2] != \
1881 actual_chain_verts
[len(actual_chain_verts
) - 1].index
:
1883 middle_vertex_idx
= single_unselected_verts_and_neighbors
[i
][0]
1884 tips_to_discard_idx
.append(single_unselected_verts_and_neighbors
[i
][1])
1885 tips_to_discard_idx
.append(single_unselected_verts_and_neighbors
[i
][2])
1887 # List with pairs of verts that belong to the tips of each selection chain (row)
1888 verts_tips_same_chain_idx
= []
1889 if len(all_chains_tips_idx
) >= 2:
1891 for i
in range(0, len(all_chains_tips_idx
)):
1892 if all_chains_tips_idx
[i
] not in checked_v
:
1893 v_chain
= self
.get_ordered_verts(
1894 self
.main_object
, all_selected_edges_idx
,
1895 all_verts_idx
, all_chains_tips_idx
[i
],
1896 middle_vertex_idx
, None
1899 verts_tips_same_chain_idx
.append([v_chain
[0].index
, v_chain
[len(v_chain
) - 1].index
])
1901 checked_v
.append(v_chain
[0].index
)
1902 checked_v
.append(v_chain
[len(v_chain
) - 1].index
)
1904 # Selection tips (vertices).
1905 verts_tips_parsed_idx
= []
1906 if len(all_chains_tips_idx
) >= 2:
1907 for spec_v_idx
in all_chains_tips_idx
:
1908 if (spec_v_idx
not in tips_to_discard_idx
):
1909 verts_tips_parsed_idx
.append(spec_v_idx
)
1911 # Identify the type of selection made by the user
1912 if middle_vertex_idx
is not None:
1913 # If there are 4 tips (two selection chains), and
1914 # there is only one single unselected vert (the middle vert)
1915 if len(all_chains_tips_idx
) == 4 and len(single_unselected_verts_and_neighbors
) == 1:
1916 selection_type
= "TWO_CONNECTED"
1918 # The type of the selection was not identified, the script stops.
1919 self
.report({'WARNING'}, "The selection isn't valid.")
1921 self
.stopping_errors
= True
1925 if len(all_chains_tips_idx
) == 2: # If there are 2 tips
1926 selection_type
= "SINGLE"
1927 elif len(all_chains_tips_idx
) == 4: # If there are 4 tips
1928 selection_type
= "TWO_NOT_CONNECTED"
1929 elif len(all_chains_tips_idx
) == 0:
1930 if len(self
.main_splines
.data
.splines
) > 1:
1931 selection_type
= "NO_SELECTION"
1933 # If the selection was not identified and there is only one stroke,
1934 # there's no possibility to build a surface, so the script is interrupted
1935 self
.report({'WARNING'}, "The selection isn't valid.")
1937 self
.stopping_errors
= True
1941 # The type of the selection was not identified, the script stops
1942 self
.report({'WARNING'}, "The selection isn't valid.")
1944 self
.stopping_errors
= True
1948 # If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script
1949 if selection_type
== "TWO_NOT_CONNECTED" and len(self
.main_splines
.data
.splines
) == 1:
1950 self
.report({'WARNING'},
1951 "At least two strokes are needed when there are two not connected selections")
1953 self
.stopping_errors
= True
1957 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1959 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1960 self
.main_splines
.select_set(True)
1961 bpy
.context
.view_layer
.objects
.active
= self
.main_splines
1963 # Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out
1964 bpy
.ops
.object.editmode_toggle('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
.curve
.smooth('INVOKE_REGION_WIN')
1970 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1971 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1973 self
.selection_U_exists
= False
1974 self
.selection_U2_exists
= False
1975 self
.selection_V_exists
= False
1976 self
.selection_V2_exists
= False
1978 self
.selection_U_is_closed
= False
1979 self
.selection_U2_is_closed
= False
1980 self
.selection_V_is_closed
= False
1981 self
.selection_V2_is_closed
= False
1983 # Define what vertices are at the tips of each selection and are not the middle-vertex
1984 if selection_type
== "TWO_CONNECTED":
1985 self
.selection_U_exists
= True
1986 self
.selection_V_exists
= True
1988 closing_vert_U_idx
= None
1989 closing_vert_V_idx
= None
1990 closing_vert_U2_idx
= None
1991 closing_vert_V2_idx
= None
1993 # Determine which selection is Selection-U and which is Selection-V
1996 points_first_stroke_tips
= []
1999 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[verts_tips_parsed_idx
[0]].co
2002 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[middle_vertex_idx
].co
2005 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[verts_tips_parsed_idx
[1]].co
2008 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[middle_vertex_idx
].co
2010 points_first_stroke_tips
.append(
2011 self
.main_splines
.data
.splines
[0].bezier_points
[0].co
2013 points_first_stroke_tips
.append(
2014 self
.main_splines
.data
.splines
[0].bezier_points
[
2015 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2019 angle_A
= self
.orientation_difference(points_A
, points_first_stroke_tips
)
2020 angle_B
= self
.orientation_difference(points_B
, points_first_stroke_tips
)
2022 if angle_A
< angle_B
:
2023 first_vert_U_idx
= verts_tips_parsed_idx
[0]
2024 first_vert_V_idx
= verts_tips_parsed_idx
[1]
2026 first_vert_U_idx
= verts_tips_parsed_idx
[1]
2027 first_vert_V_idx
= verts_tips_parsed_idx
[0]
2029 elif selection_type
== "SINGLE" or selection_type
== "TWO_NOT_CONNECTED":
2030 first_sketched_point_first_stroke_co
= self
.main_splines
.data
.splines
[0].bezier_points
[0].co
2031 last_sketched_point_first_stroke_co
= \
2032 self
.main_splines
.data
.splines
[0].bezier_points
[
2033 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2035 first_sketched_point_last_stroke_co
= \
2036 self
.main_splines
.data
.splines
[
2037 len(self
.main_splines
.data
.splines
) - 1
2038 ].bezier_points
[0].co
2039 if len(self
.main_splines
.data
.splines
) > 1:
2040 first_sketched_point_second_stroke_co
= self
.main_splines
.data
.splines
[1].bezier_points
[0].co
2041 last_sketched_point_second_stroke_co
= \
2042 self
.main_splines
.data
.splines
[1].bezier_points
[
2043 len(self
.main_splines
.data
.splines
[1].bezier_points
) - 1
2046 single_unselected_neighbors
= [] # Only the neighbors of the single unselected verts
2047 for verts_neig_idx
in single_unselected_verts_and_neighbors
:
2048 single_unselected_neighbors
.append(verts_neig_idx
[1])
2049 single_unselected_neighbors
.append(verts_neig_idx
[2])
2051 all_chains_tips_and_middle_vert
= []
2052 for v_idx
in all_chains_tips_idx
:
2053 if v_idx
not in single_unselected_neighbors
:
2054 all_chains_tips_and_middle_vert
.append(v_idx
)
2056 all_chains_tips_and_middle_vert
+= single_unselected_verts
2058 all_participating_verts
= all_chains_tips_and_middle_vert
+ all_verts_idx
2060 # The tip of the selected vertices nearest to the first point of the first sketched stroke
2061 nearest_tip_to_first_st_first_pt_idx
, shortest_distance_to_first_stroke
= \
2062 self
.shortest_distance(
2064 first_sketched_point_first_stroke_co
,
2065 all_chains_tips_and_middle_vert
2067 # If the nearest tip is not from a closed selection, get the opposite tip vertex index
2068 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2069 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2071 nearest_tip_to_first_st_first_pt_opposite_idx
= \
2073 nearest_tip_to_first_st_first_pt_idx
,
2074 verts_tips_same_chain_idx
2076 # The tip of the selected vertices nearest to the last point of the first sketched stroke
2077 nearest_tip_to_first_st_last_pt_idx
, _temp_dist
= \
2078 self
.shortest_distance(
2080 last_sketched_point_first_stroke_co
,
2081 all_chains_tips_and_middle_vert
2083 # The tip of the selected vertices nearest to the first point of the last sketched stroke
2084 nearest_tip_to_last_st_first_pt_idx
, shortest_distance_to_last_stroke
= \
2085 self
.shortest_distance(
2087 first_sketched_point_last_stroke_co
,
2088 all_chains_tips_and_middle_vert
2090 if len(self
.main_splines
.data
.splines
) > 1:
2091 # The selected vertex nearest to the first point of the second sketched stroke
2092 # (This will be useful to determine the direction of the closed
2093 # selection V when extruding along strokes)
2094 nearest_vert_to_second_st_first_pt_idx
, _temp_dist
= \
2095 self
.shortest_distance(
2097 first_sketched_point_second_stroke_co
,
2100 # The selected vertex nearest to the first point of the second sketched stroke
2101 # (This will be useful to determine the direction of the closed
2102 # selection V2 when extruding along strokes)
2103 nearest_vert_to_second_st_last_pt_idx
, _temp_dist
= \
2104 self
.shortest_distance(
2106 last_sketched_point_second_stroke_co
,
2109 # Determine if the single selection will be treated as U or as V
2111 for i
in all_selected_edges_idx
:
2113 (self
.main_object
.matrix_world
@
2114 self
.main_object
.data
.vertices
[self
.main_object
.data
.edges
[i
].vertices
[0]].co
) -
2115 (self
.main_object
.matrix_world
@
2116 self
.main_object
.data
.vertices
[self
.main_object
.data
.edges
[i
].vertices
[1]].co
)
2119 average_edge_length
= edges_sum
/ len(all_selected_edges_idx
)
2121 # Get shortest distance from the first point of the last stroke to any participating vertex
2122 _temp_idx
, shortest_distance_to_last_stroke
= \
2123 self
.shortest_distance(
2125 first_sketched_point_last_stroke_co
,
2126 all_participating_verts
2128 # If the beginning of the first stroke is near enough, and its orientation
2129 # difference with the first edge of the nearest selection chain is not too high,
2130 # interpret things as an "extrude along strokes" instead of "extrude through strokes"
2131 if shortest_distance_to_first_stroke
< average_edge_length
/ 4 and \
2132 shortest_distance_to_last_stroke
< average_edge_length
and \
2133 len(self
.main_splines
.data
.splines
) > 1:
2135 self
.selection_U_exists
= False
2136 self
.selection_V_exists
= True
2137 # If the first selection is not closed
2138 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2139 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2140 self
.selection_V_is_closed
= False
2141 closing_vert_U_idx
= None
2142 closing_vert_U2_idx
= None
2143 closing_vert_V_idx
= None
2144 closing_vert_V2_idx
= None
2146 first_vert_V_idx
= nearest_tip_to_first_st_first_pt_idx
2148 if selection_type
== "TWO_NOT_CONNECTED":
2149 self
.selection_V2_exists
= True
2151 first_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2153 self
.selection_V_is_closed
= True
2154 closing_vert_V_idx
= nearest_tip_to_first_st_first_pt_idx
2156 # Get the neighbors of the first (unselected) vert of the closed selection U.
2158 for verts
in single_unselected_verts_and_neighbors
:
2159 if verts
[0] == nearest_tip_to_first_st_first_pt_idx
:
2160 vert_neighbors
.append(verts
[1])
2161 vert_neighbors
.append(verts
[2])
2164 verts_V
= self
.get_ordered_verts(
2165 self
.main_object
, all_selected_edges_idx
,
2166 all_verts_idx
, vert_neighbors
[0], middle_vertex_idx
, None
2169 for i
in range(0, len(verts_V
)):
2170 if verts_V
[i
].index
== nearest_vert_to_second_st_first_pt_idx
:
2171 # If the vertex nearest to the first point of the second stroke
2172 # is in the first half of the selected verts
2173 if i
>= len(verts_V
) / 2:
2174 first_vert_V_idx
= vert_neighbors
[1]
2177 first_vert_V_idx
= vert_neighbors
[0]
2180 if selection_type
== "TWO_NOT_CONNECTED":
2181 self
.selection_V2_exists
= True
2182 # If the second selection is not closed
2183 if nearest_tip_to_first_st_last_pt_idx
not in single_unselected_verts
or \
2184 nearest_tip_to_first_st_last_pt_idx
== middle_vertex_idx
:
2186 self
.selection_V2_is_closed
= False
2187 closing_vert_V2_idx
= None
2188 first_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2191 self
.selection_V2_is_closed
= True
2192 closing_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2194 # Get the neighbors of the first (unselected) vert of the closed selection U
2196 for verts
in single_unselected_verts_and_neighbors
:
2197 if verts
[0] == nearest_tip_to_first_st_last_pt_idx
:
2198 vert_neighbors
.append(verts
[1])
2199 vert_neighbors
.append(verts
[2])
2202 verts_V2
= self
.get_ordered_verts(
2203 self
.main_object
, all_selected_edges_idx
,
2204 all_verts_idx
, vert_neighbors
[0], middle_vertex_idx
, None
2207 for i
in range(0, len(verts_V2
)):
2208 if verts_V2
[i
].index
== nearest_vert_to_second_st_last_pt_idx
:
2209 # If the vertex nearest to the first point of the second stroke
2210 # is in the first half of the selected verts
2211 if i
>= len(verts_V2
) / 2:
2212 first_vert_V2_idx
= vert_neighbors
[1]
2215 first_vert_V2_idx
= vert_neighbors
[0]
2218 self
.selection_V2_exists
= False
2221 self
.selection_U_exists
= True
2222 self
.selection_V_exists
= False
2223 # If the first selection is not closed
2224 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2225 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2226 self
.selection_U_is_closed
= False
2227 closing_vert_U_idx
= None
2231 self
.main_object
.matrix_world
@
2232 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_idx
].co
2235 self
.main_object
.matrix_world
@
2236 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_opposite_idx
].co
2238 points_first_stroke_tips
= []
2239 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[0].co
)
2240 points_first_stroke_tips
.append(
2241 self
.main_splines
.data
.splines
[0].bezier_points
[
2242 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2245 vec_A
= points_tips
[0] - points_tips
[1]
2246 vec_B
= points_first_stroke_tips
[0] - points_first_stroke_tips
[1]
2248 # Compare the direction of the selection and the first
2249 # grease pencil stroke to determine which is the "first" vertex of the selection
2250 if vec_A
.dot(vec_B
) < 0:
2251 first_vert_U_idx
= nearest_tip_to_first_st_first_pt_opposite_idx
2253 first_vert_U_idx
= nearest_tip_to_first_st_first_pt_idx
2256 self
.selection_U_is_closed
= True
2257 closing_vert_U_idx
= nearest_tip_to_first_st_first_pt_idx
2259 # Get the neighbors of the first (unselected) vert of the closed selection U
2261 for verts
in single_unselected_verts_and_neighbors
:
2262 if verts
[0] == nearest_tip_to_first_st_first_pt_idx
:
2263 vert_neighbors
.append(verts
[1])
2264 vert_neighbors
.append(verts
[2])
2267 points_first_and_neighbor
= []
2268 points_first_and_neighbor
.append(
2269 self
.main_object
.matrix_world
@
2270 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_idx
].co
2272 points_first_and_neighbor
.append(
2273 self
.main_object
.matrix_world
@
2274 self
.main_object
.data
.vertices
[vert_neighbors
[0]].co
2276 points_first_stroke_tips
= []
2277 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[0].co
)
2278 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[1].co
)
2280 vec_A
= points_first_and_neighbor
[0] - points_first_and_neighbor
[1]
2281 vec_B
= points_first_stroke_tips
[0] - points_first_stroke_tips
[1]
2283 # Compare the direction of the selection and the first grease pencil stroke to
2284 # determine which is the vertex neighbor to the first vertex (unselected) of
2285 # the closed selection. This will determine the direction of the closed selection
2286 if vec_A
.dot(vec_B
) < 0:
2287 first_vert_U_idx
= vert_neighbors
[1]
2289 first_vert_U_idx
= vert_neighbors
[0]
2291 if selection_type
== "TWO_NOT_CONNECTED":
2292 self
.selection_U2_exists
= True
2293 # If the second selection is not closed
2294 if nearest_tip_to_last_st_first_pt_idx
not in single_unselected_verts
or \
2295 nearest_tip_to_last_st_first_pt_idx
== middle_vertex_idx
:
2297 self
.selection_U2_is_closed
= False
2298 closing_vert_U2_idx
= None
2299 first_vert_U2_idx
= nearest_tip_to_last_st_first_pt_idx
2301 self
.selection_U2_is_closed
= True
2302 closing_vert_U2_idx
= nearest_tip_to_last_st_first_pt_idx
2304 # Get the neighbors of the first (unselected) vert of the closed selection U
2306 for verts
in single_unselected_verts_and_neighbors
:
2307 if verts
[0] == nearest_tip_to_last_st_first_pt_idx
:
2308 vert_neighbors
.append(verts
[1])
2309 vert_neighbors
.append(verts
[2])
2312 points_first_and_neighbor
= []
2313 points_first_and_neighbor
.append(
2314 self
.main_object
.matrix_world
@
2315 self
.main_object
.data
.vertices
[nearest_tip_to_last_st_first_pt_idx
].co
2317 points_first_and_neighbor
.append(
2318 self
.main_object
.matrix_world
@
2319 self
.main_object
.data
.vertices
[vert_neighbors
[0]].co
2321 points_last_stroke_tips
= []
2322 points_last_stroke_tips
.append(
2323 self
.main_splines
.data
.splines
[
2324 len(self
.main_splines
.data
.splines
) - 1
2325 ].bezier_points
[0].co
2327 points_last_stroke_tips
.append(
2328 self
.main_splines
.data
.splines
[
2329 len(self
.main_splines
.data
.splines
) - 1
2330 ].bezier_points
[1].co
2332 vec_A
= points_first_and_neighbor
[0] - points_first_and_neighbor
[1]
2333 vec_B
= points_last_stroke_tips
[0] - points_last_stroke_tips
[1]
2335 # Compare the direction of the selection and the last grease pencil stroke to
2336 # determine which is the vertex neighbor to the first vertex (unselected) of
2337 # the closed selection. This will determine the direction of the closed selection
2338 if vec_A
.dot(vec_B
) < 0:
2339 first_vert_U2_idx
= vert_neighbors
[1]
2341 first_vert_U2_idx
= vert_neighbors
[0]
2343 self
.selection_U2_exists
= False
2345 elif selection_type
== "NO_SELECTION":
2346 self
.selection_U_exists
= False
2347 self
.selection_V_exists
= False
2349 # Get an ordered list of the vertices of Selection-U
2350 verts_ordered_U
= []
2351 if self
.selection_U_exists
:
2352 verts_ordered_U
= self
.get_ordered_verts(
2353 self
.main_object
, all_selected_edges_idx
,
2354 all_verts_idx
, first_vert_U_idx
,
2355 middle_vertex_idx
, closing_vert_U_idx
2358 # Get an ordered list of the vertices of Selection-U2
2359 verts_ordered_U2
= []
2360 if self
.selection_U2_exists
:
2361 verts_ordered_U2
= self
.get_ordered_verts(
2362 self
.main_object
, all_selected_edges_idx
,
2363 all_verts_idx
, first_vert_U2_idx
,
2364 middle_vertex_idx
, closing_vert_U2_idx
2367 # Get an ordered list of the vertices of Selection-V
2368 verts_ordered_V
= []
2369 if self
.selection_V_exists
:
2370 verts_ordered_V
= self
.get_ordered_verts(
2371 self
.main_object
, all_selected_edges_idx
,
2372 all_verts_idx
, first_vert_V_idx
,
2373 middle_vertex_idx
, closing_vert_V_idx
2375 verts_ordered_V_indices
= [x
.index
for x
in verts_ordered_V
]
2377 # Get an ordered list of the vertices of Selection-V2
2378 verts_ordered_V2
= []
2379 if self
.selection_V2_exists
:
2380 verts_ordered_V2
= self
.get_ordered_verts(
2381 self
.main_object
, all_selected_edges_idx
,
2382 all_verts_idx
, first_vert_V2_idx
,
2383 middle_vertex_idx
, closing_vert_V2_idx
2386 # Check if when there are two-not-connected selections both have the same
2387 # number of verts. If not terminate the script
2388 if ((self
.selection_U2_exists
and len(verts_ordered_U
) != len(verts_ordered_U2
)) or
2389 (self
.selection_V2_exists
and len(verts_ordered_V
) != len(verts_ordered_V2
))):
2391 self
.report({'WARNING'}, "Both selections must have the same number of edges")
2393 self
.stopping_errors
= True
2397 # Calculate edges U proportions
2398 # Sum selected edges U lengths
2399 edges_lengths_U
= []
2400 edges_lengths_sum_U
= 0
2402 if self
.selection_U_exists
:
2403 edges_lengths_U
, edges_lengths_sum_U
= self
.get_chain_length(
2407 if self
.selection_U2_exists
:
2408 edges_lengths_U2
, edges_lengths_sum_U2
= self
.get_chain_length(
2412 # Sum selected edges V lengths
2413 edges_lengths_V
= []
2414 edges_lengths_sum_V
= 0
2416 if self
.selection_V_exists
:
2417 edges_lengths_V
, edges_lengths_sum_V
= self
.get_chain_length(
2421 if self
.selection_V2_exists
:
2422 edges_lengths_V2
, edges_lengths_sum_V2
= self
.get_chain_length(
2427 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2428 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN',
2429 number_cuts
=bpy
.context
.scene
.bsurfaces
.SURFSK_precision
)
2430 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2433 edges_proportions_U
= []
2434 edges_proportions_U
= self
.get_edges_proportions(
2435 edges_lengths_U
, edges_lengths_sum_U
,
2436 self
.selection_U_exists
, self
.edges_U
2438 verts_count_U
= len(edges_proportions_U
) + 1
2440 if self
.selection_U2_exists
:
2441 edges_proportions_U2
= []
2442 edges_proportions_U2
= self
.get_edges_proportions(
2443 edges_lengths_U2
, edges_lengths_sum_U2
,
2444 self
.selection_U2_exists
, self
.edges_V
2448 edges_proportions_V
= []
2449 edges_proportions_V
= self
.get_edges_proportions(
2450 edges_lengths_V
, edges_lengths_sum_V
,
2451 self
.selection_V_exists
, self
.edges_V
2454 if self
.selection_V2_exists
:
2455 edges_proportions_V2
= []
2456 edges_proportions_V2
= self
.get_edges_proportions(
2457 edges_lengths_V2
, edges_lengths_sum_V2
,
2458 self
.selection_V2_exists
, self
.edges_V
2461 # Cyclic Follow: simplify sketched curves, make them Cyclic, and complete
2462 # the actual sketched curves with a "closing segment"
2463 if self
.cyclic_follow
and not self
.selection_V_exists
and not \
2464 ((self
.selection_U_exists
and not self
.selection_U_is_closed
) or
2465 (self
.selection_U2_exists
and not self
.selection_U2_is_closed
)):
2467 simplified_spline_coords
= []
2468 simplified_curve
= []
2469 ob_simplified_curve
= []
2470 splines_first_v_co
= []
2471 for i
in range(len(self
.main_splines
.data
.splines
)):
2472 # Create a curve object for the actual spline "cyclic extension"
2473 simplified_curve
.append(bpy
.data
.curves
.new('SURFSKIO_simpl_crv', 'CURVE'))
2474 ob_simplified_curve
.append(bpy
.data
.objects
.new('SURFSKIO_simpl_crv', simplified_curve
[i
]))
2475 bpy
.context
.collection
.objects
.link(ob_simplified_curve
[i
])
2477 simplified_curve
[i
].dimensions
= "3D"
2480 for bp
in self
.main_splines
.data
.splines
[i
].bezier_points
:
2481 spline_coords
.append(bp
.co
)
2484 simplified_spline_coords
.append(self
.simplify_spline(spline_coords
, 5))
2486 # Get the coordinates of the first vert of the actual spline
2487 splines_first_v_co
.append(simplified_spline_coords
[i
][0])
2489 # Generate the spline
2490 spline
= simplified_curve
[i
].splines
.new('BEZIER')
2491 # less one because one point is added when the spline is created
2492 spline
.bezier_points
.add(len(simplified_spline_coords
[i
]) - 1)
2493 for p
in range(0, len(simplified_spline_coords
[i
])):
2494 spline
.bezier_points
[p
].co
= simplified_spline_coords
[i
][p
]
2496 spline
.use_cyclic_u
= True
2498 spline_bp_count
= len(spline
.bezier_points
)
2500 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2501 ob_simplified_curve
[i
].select_set(True)
2502 bpy
.context
.view_layer
.objects
.active
= ob_simplified_curve
[i
]
2504 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2505 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
2506 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2507 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2508 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2510 # Select the "closing segment", and subdivide it
2511 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_control_point
= True
2512 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_left_handle
= True
2513 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_right_handle
= True
2515 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_control_point
= True
2516 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_left_handle
= True
2517 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_right_handle
= True
2519 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2521 (ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].co
-
2522 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].co
).length
/
2523 self
.average_gp_segment_length
2526 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=int(segments
))
2528 # Delete the other vertices and make it non-cyclic to
2529 # keep only the needed verts of the "closing segment"
2530 bpy
.ops
.curve
.select_all(action
='INVERT')
2531 bpy
.ops
.curve
.delete(type='VERT')
2532 ob_simplified_curve
[i
].data
.splines
[0].use_cyclic_u
= False
2533 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2535 # Add the points of the "closing segment" to the original curve from grease pencil stroke
2536 first_new_index
= len(self
.main_splines
.data
.splines
[i
].bezier_points
)
2537 self
.main_splines
.data
.splines
[i
].bezier_points
.add(
2538 len(ob_simplified_curve
[i
].data
.splines
[0].bezier_points
) - 1
2540 for t
in range(1, len(ob_simplified_curve
[i
].data
.splines
[0].bezier_points
)):
2541 self
.main_splines
.data
.splines
[i
].bezier_points
[t
- 1 + first_new_index
].co
= \
2542 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[t
].co
2544 # Delete the temporal curve
2545 with bpy
.context
.temp_override(selected_objects
=[ob_simplified_curve
[i
]]):
2546 bpy
.ops
.object.delete()
2548 # Get the coords of the points distributed along the sketched strokes,
2549 # with proportions-U of the first selection
2550 pts_on_strokes_with_proportions_U
= self
.distribute_pts(
2551 self
.main_splines
.data
.splines
,
2554 sketched_splines_parsed
= []
2556 if self
.selection_U2_exists
:
2557 # Initialize the multidimensional list with the proportions of all the segments
2558 proportions_loops_crossing_strokes
= []
2559 for i
in range(len(pts_on_strokes_with_proportions_U
)):
2560 proportions_loops_crossing_strokes
.append([])
2562 for t
in range(len(pts_on_strokes_with_proportions_U
[0])):
2563 proportions_loops_crossing_strokes
[i
].append(None)
2565 # Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U
2566 for lp
in range(len(pts_on_strokes_with_proportions_U
[0])):
2567 loop_segments_lengths
= []
2569 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2570 # When on the first stroke, add the segment from the selection to the first stroke
2572 loop_segments_lengths
.append(
2573 ((self
.main_object
.matrix_world
@ verts_ordered_U
[lp
].co
) -
2574 pts_on_strokes_with_proportions_U
[0][lp
]).length
2576 # For all strokes except for the last, calculate the distance
2577 # from the actual stroke to the next
2578 if st
!= len(pts_on_strokes_with_proportions_U
) - 1:
2579 loop_segments_lengths
.append(
2580 (pts_on_strokes_with_proportions_U
[st
][lp
] -
2581 pts_on_strokes_with_proportions_U
[st
+ 1][lp
]).length
2583 # When on the last stroke, add the segments
2584 # from the last stroke to the second selection
2585 if st
== len(pts_on_strokes_with_proportions_U
) - 1:
2586 loop_segments_lengths
.append(
2587 (pts_on_strokes_with_proportions_U
[st
][lp
] -
2588 (self
.main_object
.matrix_world
@ verts_ordered_U2
[lp
].co
)).length
2590 # Calculate full loop length
2591 loop_seg_lengths_sum
= 0
2592 for i
in range(len(loop_segments_lengths
)):
2593 loop_seg_lengths_sum
+= loop_segments_lengths
[i
]
2595 # Fill the multidimensional list with the proportions of all the segments
2596 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2597 proportions_loops_crossing_strokes
[st
][lp
] = \
2598 loop_segments_lengths
[st
] / loop_seg_lengths_sum
2600 # Calculate proportions for each stroke
2601 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2602 actual_stroke_spline
= []
2603 # Needs to be a list for the "distribute_pts" method
2604 actual_stroke_spline
.append(self
.main_splines
.data
.splines
[st
])
2606 # Calculate the proportions for the actual stroke.
2607 actual_edges_proportions_U
= []
2608 for i
in range(len(edges_proportions_U
)):
2611 # Sum the proportions of this loop up to the actual.
2612 for t
in range(0, st
+ 1):
2613 proportions_sum
+= proportions_loops_crossing_strokes
[t
][i
]
2614 # i + 1, because proportions_loops_crossing_strokes refers to loops,
2615 # and the proportions refer to edges, so we start at the element 1
2616 # of proportions_loops_crossing_strokes instead of element 0
2617 actual_edges_proportions_U
.append(
2618 edges_proportions_U
[i
] -
2619 ((edges_proportions_U
[i
] - edges_proportions_U2
[i
]) * proportions_sum
)
2621 points_actual_spline
= self
.distribute_pts(actual_stroke_spline
, actual_edges_proportions_U
)
2622 sketched_splines_parsed
.append(points_actual_spline
[0])
2624 sketched_splines_parsed
= pts_on_strokes_with_proportions_U
2626 # If the selection type is "TWO_NOT_CONNECTED" replace the
2627 # points of the last spline with the points in the "target" selection
2628 if selection_type
== "TWO_NOT_CONNECTED":
2629 if self
.selection_U2_exists
:
2630 for i
in range(0, len(sketched_splines_parsed
[len(sketched_splines_parsed
) - 1])):
2631 sketched_splines_parsed
[len(sketched_splines_parsed
) - 1][i
] = \
2632 self
.main_object
.matrix_world
@ verts_ordered_U2
[i
].co
2634 # Create temporary curves along the "control-points" found
2635 # on the sketched curves and the mesh selection
2636 mesh_ctrl_pts_name
= "SURFSKIO_ctrl_pts"
2637 me
= bpy
.data
.meshes
.new(mesh_ctrl_pts_name
)
2638 ob_ctrl_pts
= bpy
.data
.objects
.new(mesh_ctrl_pts_name
, me
)
2639 ob_ctrl_pts
.data
= me
2640 bpy
.context
.collection
.objects
.link(ob_ctrl_pts
)
2647 for i
in range(0, verts_count_U
):
2648 vert_num_in_spline
= 1
2650 if self
.selection_U_exists
:
2651 ob_ctrl_pts
.data
.vertices
.add(1)
2652 last_v
= ob_ctrl_pts
.data
.vertices
[len(ob_ctrl_pts
.data
.vertices
) - 1]
2653 last_v
.co
= self
.main_object
.matrix_world
@ verts_ordered_U
[i
].co
2655 vert_num_in_spline
+= 1
2657 for t
in range(0, len(sketched_splines_parsed
)):
2658 ob_ctrl_pts
.data
.vertices
.add(1)
2659 v
= ob_ctrl_pts
.data
.vertices
[len(ob_ctrl_pts
.data
.vertices
) - 1]
2660 v
.co
= sketched_splines_parsed
[t
][i
]
2662 if vert_num_in_spline
> 1:
2663 ob_ctrl_pts
.data
.edges
.add(1)
2664 ob_ctrl_pts
.data
.edges
[len(ob_ctrl_pts
.data
.edges
) - 1].vertices
[0] = \
2665 len(ob_ctrl_pts
.data
.vertices
) - 2
2666 ob_ctrl_pts
.data
.edges
[len(ob_ctrl_pts
.data
.edges
) - 1].vertices
[1] = \
2667 len(ob_ctrl_pts
.data
.vertices
) - 1
2670 first_verts
.append(v
.index
)
2673 second_verts
.append(v
.index
)
2675 if t
== len(sketched_splines_parsed
) - 1:
2676 last_verts
.append(v
.index
)
2679 vert_num_in_spline
+= 1
2681 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2682 ob_ctrl_pts
.select_set(True)
2683 bpy
.context
.view_layer
.objects
.active
= ob_ctrl_pts
2685 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2686 bpy
.ops
.mesh
.select_all(action
='DESELECT')
2687 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2689 # Determine which loops-U will be "Cyclic"
2690 for i
in range(0, len(first_verts
)):
2691 # When there is Cyclic Cross there is no need of
2692 # Automatic Join, (and there are at least three strokes)
2693 if self
.automatic_join
and not self
.cyclic_cross
and \
2694 selection_type
!= "TWO_CONNECTED" and len(self
.main_splines
.data
.splines
) >= 3:
2696 v
= ob_ctrl_pts
.data
.vertices
2697 first_point_co
= v
[first_verts
[i
]].co
2698 second_point_co
= v
[second_verts
[i
]].co
2699 last_point_co
= v
[last_verts
[i
]].co
2701 # Coordinates of the point in the center of both the first and last verts.
2703 (first_point_co
[0] + last_point_co
[0]) / 2,
2704 (first_point_co
[1] + last_point_co
[1]) / 2,
2705 (first_point_co
[2] + last_point_co
[2]) / 2
2707 vec_A
= second_point_co
- first_point_co
2708 vec_B
= second_point_co
- Vector(verts_center_co
)
2710 # Calculate the length of the first segment of the loop,
2711 # and the length it would have after moving the first vert
2712 # to the middle position between first and last
2713 length_original
= (second_point_co
- first_point_co
).length
2714 length_target
= (second_point_co
- Vector(verts_center_co
)).length
2716 angle
= vec_A
.angle(vec_B
) / pi
2718 # If the target length doesn't stretch too much, and the
2719 # its angle doesn't change to much either
2720 if length_target
<= length_original
* 1.03 * self
.join_stretch_factor
and \
2721 angle
<= 0.008 * self
.join_stretch_factor
and not self
.selection_U_exists
:
2723 cyclic_loops_U
.append(True)
2724 # Move the first vert to the center coordinates
2725 ob_ctrl_pts
.data
.vertices
[first_verts
[i
]].co
= verts_center_co
2726 # Select the last verts from Cyclic loops, for later deletion all at once
2727 v
[last_verts
[i
]].select
= True
2729 cyclic_loops_U
.append(False)
2731 # If "Cyclic Cross" is active then "all" crossing curves become cyclic
2732 if self
.cyclic_cross
and not self
.selection_U_exists
and not \
2733 ((self
.selection_V_exists
and not self
.selection_V_is_closed
) or
2734 (self
.selection_V2_exists
and not self
.selection_V2_is_closed
)):
2736 cyclic_loops_U
.append(True)
2738 cyclic_loops_U
.append(False)
2740 # The cyclic_loops_U list needs to be reversed.
2741 cyclic_loops_U
.reverse()
2743 # Delete the previously selected (last_)verts.
2744 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2745 bpy
.ops
.mesh
.delete('INVOKE_REGION_WIN', type='VERT')
2746 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2748 # Create curves from control points.
2749 bpy
.ops
.object.convert('INVOKE_REGION_WIN', target
='CURVE', keep_original
=False)
2750 ob_curves_surf
= bpy
.context
.view_layer
.objects
.active
2751 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2752 bpy
.ops
.curve
.spline_type_set('INVOKE_REGION_WIN', type='BEZIER')
2753 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2755 # Make Cyclic the splines designated as Cyclic.
2756 for i
in range(0, len(cyclic_loops_U
)):
2757 ob_curves_surf
.data
.splines
[i
].use_cyclic_u
= cyclic_loops_U
[i
]
2759 # Get the coords of all points on first loop-U, for later comparison with its
2760 # subdivided version, to know which points of the loops-U are crossed by the
2761 # original strokes. The indices will be the same for the other loops-U
2762 if self
.loops_on_strokes
:
2763 coords_loops_U_control_points
= []
2764 for p
in ob_ctrl_pts
.data
.splines
[0].bezier_points
:
2765 coords_loops_U_control_points
.append(["%.4f" % p
.co
[0], "%.4f" % p
.co
[1], "%.4f" % p
.co
[2]])
2767 tuple(coords_loops_U_control_points
)
2769 # Calculate number of edges-V in case option "Loops on strokes" is active or inactive
2770 if self
.loops_on_strokes
and not self
.selection_V_exists
:
2771 edges_V_count
= len(self
.main_splines
.data
.splines
) * self
.edges_V
2773 edges_V_count
= len(edges_proportions_V
)
2775 # The Follow precision will vary depending on the number of Follow face-loops
2776 precision_multiplier
= round(2 + (edges_V_count
/ 15))
2777 curve_cuts
= bpy
.context
.scene
.bsurfaces
.SURFSK_precision
* precision_multiplier
2779 # Subdivide the curves
2780 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=curve_cuts
)
2782 # The verts position shifting that happens with splines subdivision.
2783 # For later reorder splines points
2784 verts_position_shift
= curve_cuts
+ 1
2785 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2787 # Reorder coordinates of the points of each spline to put the first point of
2788 # the spline starting at the position it was the first point before sudividing
2789 # the curve. And make a new curve object per spline (to handle memory better later)
2790 splines_U_objects
= []
2791 for i
in range(len(ob_curves_surf
.data
.splines
)):
2792 spline_U_curve
= bpy
.data
.curves
.new('SURFSKIO_spline_U_' + str(i
), 'CURVE')
2793 ob_spline_U
= bpy
.data
.objects
.new('SURFSKIO_spline_U_' + str(i
), spline_U_curve
)
2794 bpy
.context
.collection
.objects
.link(ob_spline_U
)
2796 spline_U_curve
.dimensions
= "3D"
2798 # Add points to the spline in the new curve object
2799 ob_spline_U
.data
.splines
.new('BEZIER')
2800 for t
in range(len(ob_curves_surf
.data
.splines
[i
].bezier_points
)):
2801 if cyclic_loops_U
[i
] is True and not self
.selection_U_exists
: # If the loop is cyclic
2802 if t
+ verts_position_shift
<= len(ob_curves_surf
.data
.splines
[i
].bezier_points
) - 1:
2803 point_index
= t
+ verts_position_shift
2805 point_index
= t
+ verts_position_shift
- len(ob_curves_surf
.data
.splines
[i
].bezier_points
)
2808 # to avoid adding the first point since it's added when the spline is created
2810 ob_spline_U
.data
.splines
[0].bezier_points
.add(1)
2811 ob_spline_U
.data
.splines
[0].bezier_points
[t
].co
= \
2812 ob_curves_surf
.data
.splines
[i
].bezier_points
[point_index
].co
2814 if cyclic_loops_U
[i
] is True and not self
.selection_U_exists
: # If the loop is cyclic
2815 # Add a last point at the same location as the first one
2816 ob_spline_U
.data
.splines
[0].bezier_points
.add(1)
2817 ob_spline_U
.data
.splines
[0].bezier_points
[len(ob_spline_U
.data
.splines
[0].bezier_points
) - 1].co
= \
2818 ob_spline_U
.data
.splines
[0].bezier_points
[0].co
2820 ob_spline_U
.data
.splines
[0].use_cyclic_u
= False
2822 splines_U_objects
.append(ob_spline_U
)
2823 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2824 ob_spline_U
.select_set(True)
2825 bpy
.context
.view_layer
.objects
.active
= ob_spline_U
2827 # When option "Loops on strokes" is active each "Cross" loop will have
2828 # its own proportions according to where the original strokes "touch" them
2829 if self
.loops_on_strokes
:
2830 # Get the indices of points where the original strokes "touch" loops-U
2831 points_U_crossed_by_strokes
= []
2832 for i
in range(len(splines_U_objects
[0].data
.splines
[0].bezier_points
)):
2833 bp
= splines_U_objects
[0].data
.splines
[0].bezier_points
[i
]
2834 if ["%.4f" % bp
.co
[0], "%.4f" % bp
.co
[1], "%.4f" % bp
.co
[2]] in coords_loops_U_control_points
:
2835 points_U_crossed_by_strokes
.append(i
)
2837 # Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke
2838 edge_order_number_for_splines
= {}
2839 if self
.selection_V_exists
:
2840 # For two-connected selections add a first hypothetic stroke at the beginning.
2841 if selection_type
== "TWO_CONNECTED":
2842 edge_order_number_for_splines
[0] = 0
2844 for i
in range(len(self
.main_splines
.data
.splines
)):
2845 sp
= self
.main_splines
.data
.splines
[i
]
2846 v_idx
, _dist_temp
= self
.shortest_distance(
2848 sp
.bezier_points
[0].co
,
2849 verts_ordered_V_indices
2851 # Get the position (edges count) of the vert v_idx in the selected chain V
2852 edge_idx_in_chain
= verts_ordered_V_indices
.index(v_idx
)
2854 # For two-connected selections the strokes go after the
2855 # hypothetic stroke added before, so the index adds one per spline
2856 if selection_type
== "TWO_CONNECTED":
2857 spline_number
= i
+ 1
2861 edge_order_number_for_splines
[spline_number
] = edge_idx_in_chain
2863 # Get the first and last verts indices for later comparison
2866 elif i
== len(self
.main_splines
.data
.splines
) - 1:
2869 if self
.selection_V_is_closed
:
2870 # If there is no last stroke on the last vertex (same as first vertex),
2871 # add a hypothetic spline at last vert order
2872 if first_v_idx
!= last_v_idx
:
2873 edge_order_number_for_splines
[(len(self
.main_splines
.data
.splines
) - 1) + 1] = \
2874 len(verts_ordered_V_indices
) - 1
2876 if self
.cyclic_cross
:
2877 edge_order_number_for_splines
[len(self
.main_splines
.data
.splines
) - 1] = \
2878 len(verts_ordered_V_indices
) - 2
2879 edge_order_number_for_splines
[(len(self
.main_splines
.data
.splines
) - 1) + 1] = \
2880 len(verts_ordered_V_indices
) - 1
2882 edge_order_number_for_splines
[len(self
.main_splines
.data
.splines
) - 1] = \
2883 len(verts_ordered_V_indices
) - 1
2885 # Get the coords of the points distributed along the
2886 # "crossing curves", with appropriate proportions-V
2887 surface_splines_parsed
= []
2888 for i
in range(len(splines_U_objects
)):
2889 sp_ob
= splines_U_objects
[i
]
2890 # If "Loops on strokes" option is active, calculate the proportions for each loop-U
2891 if self
.loops_on_strokes
:
2892 # Segments distances from stroke to stroke
2895 segments_distances
= []
2896 for t
in range(len(sp_ob
.data
.splines
[0].bezier_points
)):
2897 bp
= sp_ob
.data
.splines
[0].bezier_points
[t
]
2903 dist
+= (last_p
- actual_p
).length
2905 if t
in points_U_crossed_by_strokes
:
2906 segments_distances
.append(dist
)
2913 # Calculate Proportions.
2914 used_edges_proportions_V
= []
2915 for t
in range(len(segments_distances
)):
2916 if self
.selection_V_exists
:
2918 order_number_last_stroke
= 0
2920 segment_edges_length_V
= 0
2921 segment_edges_length_V2
= 0
2922 for order
in range(order_number_last_stroke
, edge_order_number_for_splines
[t
+ 1]):
2923 segment_edges_length_V
+= edges_lengths_V
[order
]
2924 if self
.selection_V2_exists
:
2925 segment_edges_length_V2
+= edges_lengths_V2
[order
]
2927 for order
in range(order_number_last_stroke
, edge_order_number_for_splines
[t
+ 1]):
2928 # Calculate each "sub-segment" (the ones between each stroke) length
2929 if self
.selection_V2_exists
:
2930 proportion_sub_seg
= (edges_lengths_V2
[order
] -
2931 ((edges_lengths_V2
[order
] - edges_lengths_V
[order
]) /
2932 len(splines_U_objects
) * i
)) / (segment_edges_length_V2
-
2933 (segment_edges_length_V2
- segment_edges_length_V
) /
2934 len(splines_U_objects
) * i
)
2936 sub_seg_dist
= segments_distances
[t
] * proportion_sub_seg
2938 proportion_sub_seg
= edges_lengths_V
[order
] / segment_edges_length_V
2939 sub_seg_dist
= segments_distances
[t
] * proportion_sub_seg
2941 used_edges_proportions_V
.append(sub_seg_dist
/ full_dist
)
2943 order_number_last_stroke
= edge_order_number_for_splines
[t
+ 1]
2946 for _c
in range(self
.edges_V
):
2947 # Calculate each "sub-segment" (the ones between each stroke) length
2948 sub_seg_dist
= segments_distances
[t
] / self
.edges_V
2949 used_edges_proportions_V
.append(sub_seg_dist
/ full_dist
)
2951 actual_spline
= self
.distribute_pts(sp_ob
.data
.splines
, used_edges_proportions_V
)
2952 surface_splines_parsed
.append(actual_spline
[0])
2955 if self
.selection_V2_exists
:
2956 used_edges_proportions_V
= []
2957 for p
in range(len(edges_proportions_V
)):
2958 used_edges_proportions_V
.append(
2959 edges_proportions_V2
[p
] -
2960 ((edges_proportions_V2
[p
] -
2961 edges_proportions_V
[p
]) / len(splines_U_objects
) * i
)
2964 used_edges_proportions_V
= edges_proportions_V
2966 actual_spline
= self
.distribute_pts(sp_ob
.data
.splines
, used_edges_proportions_V
)
2967 surface_splines_parsed
.append(actual_spline
[0])
2969 # Set the verts of the first and last splines to the locations
2970 # of the respective verts in the selections
2971 if self
.selection_V_exists
:
2972 for i
in range(0, len(surface_splines_parsed
[0])):
2973 surface_splines_parsed
[len(surface_splines_parsed
) - 1][i
] = \
2974 self
.main_object
.matrix_world
@ verts_ordered_V
[i
].co
2976 if selection_type
== "TWO_NOT_CONNECTED":
2977 if self
.selection_V2_exists
:
2978 for i
in range(0, len(surface_splines_parsed
[0])):
2979 surface_splines_parsed
[0][i
] = self
.main_object
.matrix_world
@ verts_ordered_V2
[i
].co
2981 # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
2982 # merge the verts of the tips of the loops when they are "near enough"
2983 if self
.automatic_join
and selection_type
!= "TWO_CONNECTED":
2984 # Join the tips of "Follow" loops that are near enough and must be "closed"
2985 if not self
.selection_V_exists
and len(edges_proportions_U
) >= 3:
2986 for i
in range(len(surface_splines_parsed
[0])):
2987 sp
= surface_splines_parsed
2988 loop_segment_dist
= (sp
[0][i
] - sp
[1][i
]).length
2990 verts_middle_position_co
= [
2991 (sp
[0][i
][0] + sp
[len(sp
) - 1][i
][0]) / 2,
2992 (sp
[0][i
][1] + sp
[len(sp
) - 1][i
][1]) / 2,
2993 (sp
[0][i
][2] + sp
[len(sp
) - 1][i
][2]) / 2
2995 points_original
= []
2996 points_original
.append(sp
[1][i
])
2997 points_original
.append(sp
[0][i
])
3000 points_target
.append(sp
[1][i
])
3001 points_target
.append(Vector(verts_middle_position_co
))
3003 vec_A
= points_original
[0] - points_original
[1]
3004 vec_B
= points_target
[0] - points_target
[1]
3005 # check for zero angles, not sure if it is a great fix
3006 if vec_A
.length
!= 0 and vec_B
.length
!= 0:
3007 angle
= vec_A
.angle(vec_B
) / pi
3008 edge_new_length
= (Vector(verts_middle_position_co
) - sp
[1][i
]).length
3013 # If after moving the verts to the middle point, the segment doesn't stretch too much
3014 if edge_new_length
<= loop_segment_dist
* 1.5 * \
3015 self
.join_stretch_factor
and angle
< 0.25 * self
.join_stretch_factor
:
3017 # Avoid joining when the actual loop must be merged with the original mesh
3018 if not (self
.selection_U_exists
and i
== 0) and \
3019 not (self
.selection_U2_exists
and i
== len(surface_splines_parsed
[0]) - 1):
3021 # Change the coords of both verts to the middle position
3022 surface_splines_parsed
[0][i
] = verts_middle_position_co
3023 surface_splines_parsed
[len(surface_splines_parsed
) - 1][i
] = verts_middle_position_co
3025 # Delete object with control points and object from grease pencil conversion
3026 with bpy
.context
.temp_override(selected_objects
=[ob_ctrl_pts
]):
3027 bpy
.ops
.object.delete()
3029 with bpy
.context
.temp_override(selected_objects
=splines_U_objects
):
3030 bpy
.ops
.object.delete()
3034 # Get all verts coords
3035 all_surface_verts_co
= []
3036 for i
in range(0, len(surface_splines_parsed
)):
3037 # Get coords of all verts and make a list with them
3038 for pt_co
in surface_splines_parsed
[i
]:
3039 all_surface_verts_co
.append(pt_co
)
3041 # Define verts for each face
3042 all_surface_faces
= []
3043 for i
in range(0, len(all_surface_verts_co
) - len(surface_splines_parsed
[0])):
3044 if ((i
+ 1) / len(surface_splines_parsed
[0]) != int((i
+ 1) / len(surface_splines_parsed
[0]))):
3045 all_surface_faces
.append(
3046 [i
+ 1, i
, i
+ len(surface_splines_parsed
[0]),
3047 i
+ len(surface_splines_parsed
[0]) + 1]
3050 surf_me_name
= "SURFSKIO_surface"
3051 me_surf
= bpy
.data
.meshes
.new(surf_me_name
)
3052 me_surf
.from_pydata(all_surface_verts_co
, [], all_surface_faces
)
3053 ob_surface
= object_utils
.object_data_add(context
, me_surf
)
3054 ob_surface
.location
= (0.0, 0.0, 0.0)
3055 ob_surface
.rotation_euler
= (0.0, 0.0, 0.0)
3056 ob_surface
.scale
= (1.0, 1.0, 1.0)
3058 # Select all the "unselected but participating" verts, from closed selection
3059 # or double selections with middle-vertex, for later join with remove doubles
3060 for v_idx
in single_unselected_verts
:
3061 self
.main_object
.data
.vertices
[v_idx
].select
= True
3063 # Join the new mesh to the main object
3064 ob_surface
.select_set(True)
3065 self
.main_object
.select_set(True)
3066 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3068 bpy
.ops
.object.join('INVOKE_REGION_WIN')
3070 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3072 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN', threshold
=0.0001)
3073 bpy
.ops
.mesh
.normals_make_consistent('INVOKE_REGION_WIN', inside
=False)
3074 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3082 global global_shade_smooth
3083 if global_shade_smooth
:
3084 bpy
.ops
.object.shade_smooth()
3086 bpy
.ops
.object.shade_flat()
3087 bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
= global_shade_smooth
3093 def execute(self
, context
):
3095 if bpy
.ops
.object.mode_set
.poll():
3096 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3099 global global_mesh_object
3100 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
3101 bpy
.data
.objects
[global_mesh_object
].select_set(True)
3102 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
3103 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3104 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3106 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3108 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3112 if not self
.is_fill_faces
:
3113 bpy
.ops
.wm
.context_set_value(data_path
='tool_settings.mesh_select_mode',
3114 value
='True, False, False')
3116 # Build splines from the "last saved splines".
3117 last_saved_curve
= bpy
.data
.curves
.new('SURFSKIO_last_crv', 'CURVE')
3118 self
.main_splines
= bpy
.data
.objects
.new('SURFSKIO_last_crv', last_saved_curve
)
3119 bpy
.context
.collection
.objects
.link(self
.main_splines
)
3121 last_saved_curve
.dimensions
= "3D"
3123 for sp
in self
.last_strokes_splines_coords
:
3124 spline
= self
.main_splines
.data
.splines
.new('BEZIER')
3125 # less one because one point is added when the spline is created
3126 spline
.bezier_points
.add(len(sp
) - 1)
3127 for p
in range(0, len(sp
)):
3128 spline
.bezier_points
[p
].co
= [sp
[p
][0], sp
[p
][1], sp
[p
][2]]
3130 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3132 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3133 self
.main_splines
.select_set(True)
3134 bpy
.context
.view_layer
.objects
.active
= self
.main_splines
3136 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3138 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
3139 # Important to make it vector first and then automatic, otherwise the
3140 # tips handles get too big and distort the shrinkwrap results later
3141 bpy
.ops
.curve
.handle_type_set(type='VECTOR')
3142 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
3143 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3144 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3146 self
.main_splines
.name
= "SURFSKIO_temp_strokes"
3148 if self
.is_crosshatch
:
3149 strokes_for_crosshatch
= True
3150 strokes_for_rectangular_surface
= False
3152 strokes_for_rectangular_surface
= True
3153 strokes_for_crosshatch
= False
3155 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3157 if strokes_for_rectangular_surface
:
3158 self
.rectangular_surface(context
)
3159 elif strokes_for_crosshatch
:
3160 self
.crosshatch_surface_execute(context
)
3162 #Set Shade smooth to new polygons
3163 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3164 global global_shade_smooth
3165 if global_shade_smooth
:
3166 bpy
.ops
.object.shade_smooth()
3168 bpy
.ops
.object.shade_flat()
3170 # Delete main splines
3171 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3172 if self
.keep_strokes
:
3173 self
.main_splines
.name
= "keep_strokes"
3174 self
.main_splines
.data
.bevel_depth
= 0.001
3175 if "keep_strokes_material" in bpy
.data
.materials
:
3176 self
.main_splines
.data
.materials
.append(bpy
.data
.materials
["keep_strokes_material"])
3178 mat
= bpy
.data
.materials
.new("keep_strokes_material")
3179 mat
.diffuse_color
= (1, 0, 0, 0)
3180 mat
.specular_color
= (1, 0, 0)
3181 mat
.specular_intensity
= 0.0
3183 self
.main_splines
.data
.materials
.append(mat
)
3185 with bpy
.context
.temp_override(selected_objects
=[self
.main_splines
]):
3186 bpy
.ops
.object.delete()
3188 # Delete grease pencil strokes
3189 if self
.strokes_type
== "GP_STROKES" and not self
.stopping_errors
:
3191 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3195 # Delete annotations
3196 if self
.strokes_type
== "GP_ANNOTATION" and not self
.stopping_errors
:
3198 bpy
.context
.annotation_data
.layers
.active
.clear()
3202 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3203 bsurfaces_props
.SURFSK_edges_U
= self
.edges_U
3204 bsurfaces_props
.SURFSK_edges_V
= self
.edges_V
3205 bsurfaces_props
.SURFSK_cyclic_cross
= self
.cyclic_cross
3206 bsurfaces_props
.SURFSK_cyclic_follow
= self
.cyclic_follow
3207 bsurfaces_props
.SURFSK_automatic_join
= self
.automatic_join
3208 bsurfaces_props
.SURFSK_loops_on_strokes
= self
.loops_on_strokes
3209 bsurfaces_props
.SURFSK_keep_strokes
= self
.keep_strokes
3211 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3212 self
.main_object
.select_set(True)
3213 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3215 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3221 def invoke(self
, context
, event
):
3223 if bpy
.ops
.object.mode_set
.poll():
3224 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3226 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3227 self
.cyclic_cross
= bsurfaces_props
.SURFSK_cyclic_cross
3228 self
.cyclic_follow
= bsurfaces_props
.SURFSK_cyclic_follow
3229 self
.automatic_join
= bsurfaces_props
.SURFSK_automatic_join
3230 self
.loops_on_strokes
= bsurfaces_props
.SURFSK_loops_on_strokes
3231 self
.keep_strokes
= bsurfaces_props
.SURFSK_keep_strokes
3234 global global_mesh_object
3235 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
3236 bpy
.data
.objects
[global_mesh_object
].select_set(True)
3237 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
3238 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3240 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3245 self
.main_object_selected_verts_count
= len([v
for v
in self
.main_object
.data
.vertices
if v
.select
])
3247 bpy
.ops
.wm
.context_set_value(data_path
='tool_settings.mesh_select_mode',
3248 value
='True, False, False')
3250 self
.edges_U
= bsurfaces_props
.SURFSK_edges_U
3251 self
.edges_V
= bsurfaces_props
.SURFSK_edges_V
3253 self
.is_fill_faces
= False
3254 self
.stopping_errors
= False
3255 self
.last_strokes_splines_coords
= []
3257 # Determine the type of the strokes
3258 self
.strokes_type
= get_strokes_type(context
)
3260 # Check if it will be used grease pencil strokes or curves
3261 # If there are strokes to be used
3262 if self
.strokes_type
== "GP_STROKES" or self
.strokes_type
== "EXTERNAL_CURVE" or self
.strokes_type
== "GP_ANNOTATION":
3263 if self
.strokes_type
== "GP_STROKES":
3264 # Convert grease pencil strokes to curve
3265 global global_gpencil_object
3266 gp
= bpy
.data
.objects
[global_gpencil_object
]
3267 self
.original_curve
= conver_gpencil_to_curve(self
, context
, gp
, 'GPensil')
3268 self
.using_external_curves
= False
3270 elif self
.strokes_type
== "GP_ANNOTATION":
3271 # Convert grease pencil strokes to curve
3272 gp
= bpy
.context
.annotation_data
3273 self
.original_curve
= conver_gpencil_to_curve(self
, context
, gp
, 'Annotation')
3274 self
.using_external_curves
= False
3276 elif self
.strokes_type
== "EXTERNAL_CURVE":
3277 global global_curve_object
3278 self
.original_curve
= bpy
.data
.objects
[global_curve_object
]
3279 self
.using_external_curves
= True
3281 # Make sure there are no objects left from erroneous
3282 # executions of this operator, with the reserved names used here
3283 for o
in bpy
.data
.objects
:
3284 if o
.name
.find("SURFSKIO_") != -1:
3285 with bpy
.context
.temp_override(selected_objects
=[o
]):
3286 bpy
.ops
.object.delete()
3288 bpy
.context
.view_layer
.objects
.active
= self
.original_curve
3290 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3292 self
.temporary_curve
= bpy
.context
.view_layer
.objects
.active
3294 # Deselect all points of the curve
3295 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3296 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3297 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3299 # Delete splines with only a single isolated point
3300 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3301 sp
= self
.temporary_curve
.data
.splines
[i
]
3303 if len(sp
.bezier_points
) == 1:
3304 sp
.bezier_points
[0].select_control_point
= True
3306 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3307 bpy
.ops
.curve
.delete(type='VERT')
3308 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3310 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3311 self
.temporary_curve
.select_set(True)
3312 bpy
.context
.view_layer
.objects
.active
= self
.temporary_curve
3314 # Set a minimum number of points for crosshatch
3315 minimum_points_num
= 15
3317 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3318 # Check if the number of points of each curve has at least the number of points
3319 # of minimum_points_num, which is a bit more than the face-loops limit.
3320 # If not, subdivide to reach at least that number of points
3321 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3322 sp
= self
.temporary_curve
.data
.splines
[i
]
3324 if len(sp
.bezier_points
) < minimum_points_num
:
3325 for bp
in sp
.bezier_points
:
3326 bp
.select_control_point
= True
3328 if (len(sp
.bezier_points
) - 1) != 0:
3329 # Formula to get the number of cuts that will make a curve
3330 # of N number of points have near to "minimum_points_num"
3331 # points, when subdividing with this number of cuts
3332 subdivide_cuts
= int(
3333 (minimum_points_num
- len(sp
.bezier_points
)) /
3334 (len(sp
.bezier_points
) - 1)
3339 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3340 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3342 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3344 # Detect if the strokes are a crosshatch and do it if it is
3345 self
.crosshatch_surface_invoke(self
.temporary_curve
)
3347 if not self
.is_crosshatch
:
3348 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3349 self
.temporary_curve
.select_set(True)
3350 bpy
.context
.view_layer
.objects
.active
= self
.temporary_curve
3352 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3354 # Set a minimum number of points for rectangular surfaces
3355 minimum_points_num
= 60
3357 # Check if the number of points of each curve has at least the number of points
3358 # of minimum_points_num, which is a bit more than the face-loops limit.
3359 # If not, subdivide to reach at least that number of points
3360 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3361 sp
= self
.temporary_curve
.data
.splines
[i
]
3363 if len(sp
.bezier_points
) < minimum_points_num
:
3364 for bp
in sp
.bezier_points
:
3365 bp
.select_control_point
= True
3367 if (len(sp
.bezier_points
) - 1) != 0:
3368 # Formula to get the number of cuts that will make a curve of
3369 # N number of points have near to "minimum_points_num" points,
3370 # when subdividing with this number of cuts
3371 subdivide_cuts
= int(
3372 (minimum_points_num
- len(sp
.bezier_points
)) /
3373 (len(sp
.bezier_points
) - 1)
3378 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3379 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3381 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3383 # Save coordinates of the actual strokes (as the "last saved splines")
3384 for sp_idx
in range(len(self
.temporary_curve
.data
.splines
)):
3385 self
.last_strokes_splines_coords
.append([])
3386 for bp_idx
in range(len(self
.temporary_curve
.data
.splines
[sp_idx
].bezier_points
)):
3387 coords
= self
.temporary_curve
.matrix_world
@ \
3388 self
.temporary_curve
.data
.splines
[sp_idx
].bezier_points
[bp_idx
].co
3389 self
.last_strokes_splines_coords
[sp_idx
].append([coords
[0], coords
[1], coords
[2]])
3391 # Check for cyclic splines, put the first and last points in the middle of their actual positions
3392 for sp_idx
in range(len(self
.temporary_curve
.data
.splines
)):
3393 if self
.temporary_curve
.data
.splines
[sp_idx
].use_cyclic_u
is True:
3394 first_p_co
= self
.last_strokes_splines_coords
[sp_idx
][0]
3395 last_p_co
= self
.last_strokes_splines_coords
[sp_idx
][
3396 len(self
.last_strokes_splines_coords
[sp_idx
]) - 1
3399 (first_p_co
[0] + last_p_co
[0]) / 2,
3400 (first_p_co
[1] + last_p_co
[1]) / 2,
3401 (first_p_co
[2] + last_p_co
[2]) / 2
3404 self
.last_strokes_splines_coords
[sp_idx
][0] = target_co
3405 self
.last_strokes_splines_coords
[sp_idx
][
3406 len(self
.last_strokes_splines_coords
[sp_idx
]) - 1
3408 tuple(self
.last_strokes_splines_coords
)
3410 # Estimation of the average length of the segments between
3411 # each point of the grease pencil strokes.
3412 # Will be useful to determine whether a curve should be made "Cyclic"
3413 segments_lengths_sum
= 0
3415 random_spline
= self
.temporary_curve
.data
.splines
[0].bezier_points
3416 for i
in range(0, len(random_spline
)):
3417 if i
!= 0 and len(random_spline
) - 1 >= i
:
3418 segments_lengths_sum
+= (random_spline
[i
- 1].co
- random_spline
[i
].co
).length
3421 self
.average_gp_segment_length
= segments_lengths_sum
/ segments_count
3423 # Delete temporary strokes curve object
3424 with bpy
.context
.temp_override(selected_objects
=[self
.temporary_curve
]):
3425 bpy
.ops
.object.delete()
3427 # Set again since "execute()" will turn it again to its initial value
3428 self
.execute(context
)
3430 if not self
.stopping_errors
:
3431 # Delete grease pencil strokes
3432 if self
.strokes_type
== "GP_STROKES":
3434 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3438 # Delete annotation strokes
3439 elif self
.strokes_type
== "GP_ANNOTATION":
3441 bpy
.context
.annotation_data
.layers
.active
.clear()
3445 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3446 with bpy
.context
.temp_override(selected_objects
=[self
.original_curve
]):
3447 bpy
.ops
.object.delete()
3448 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3454 elif self
.strokes_type
== "SELECTION_ALONE":
3455 self
.is_fill_faces
= True
3456 created_faces_count
= self
.fill_with_faces(self
.main_object
)
3458 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3460 if created_faces_count
== 0:
3461 self
.report({'WARNING'}, "There aren't any strokes attached to the object")
3462 return {"CANCELLED"}
3466 if self
.strokes_type
== "EXTERNAL_NO_CURVE":
3467 self
.report({'WARNING'}, "The secondary object is not a Curve.")
3470 elif self
.strokes_type
== "MORE_THAN_ONE_EXTERNAL":
3471 self
.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3474 elif self
.strokes_type
== "SINGLE_GP_STROKE_NO_SELECTION" or \
3475 self
.strokes_type
== "SINGLE_CURVE_STROKE_NO_SELECTION":
3477 self
.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
3480 elif self
.strokes_type
== "NO_STROKES":
3481 self
.report({'WARNING'}, "There aren't any strokes attached to the object")
3484 elif self
.strokes_type
== "CURVE_WITH_NON_BEZIER_SPLINES":
3485 self
.report({'WARNING'}, "All splines must be Bezier.")
3491 # ----------------------------
3493 class MESH_OT_SURFSK_init(Operator
):
3494 bl_idname
= "mesh.surfsk_init"
3495 bl_label
= "Bsurfaces initialize"
3496 bl_description
= "Add an empty mesh object with useful settings"
3497 bl_options
= {'REGISTER', 'UNDO'}
3499 def execute(self
, context
):
3501 bs
= bpy
.context
.scene
.bsurfaces
3503 if bpy
.ops
.object.mode_set
.poll():
3504 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3506 global global_shade_smooth
3507 global global_mesh_object
3508 global global_gpencil_object
3510 if bs
.SURFSK_mesh
== None:
3511 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3512 mesh
= bpy
.data
.meshes
.new('BSurfaceMesh')
3513 mesh_object
= object_utils
.object_data_add(context
, mesh
)
3514 mesh_object
.select_set(True)
3515 bpy
.context
.view_layer
.objects
.active
= mesh_object
3517 mesh_object
.show_all_edges
= True
3518 mesh_object
.display_type
= 'SOLID'
3519 mesh_object
.show_wire
= True
3521 global_shade_smooth
= bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
3522 if global_shade_smooth
:
3523 bpy
.ops
.object.shade_smooth()
3525 bpy
.ops
.object.shade_flat()
3527 color_red
= [1.0, 0.0, 0.0, 0.3]
3528 material
= makeMaterial("BSurfaceMesh", color_red
)
3529 mesh_object
.data
.materials
.append(material
)
3530 modifier
= mesh_object
.modifiers
.new("", 'SHRINKWRAP')
3531 if self
.active_object
is not None:
3532 modifier
.target
= self
.active_object
3533 modifier
.wrap_method
= 'TARGET_PROJECT'
3534 modifier
.wrap_mode
= 'OUTSIDE_SURFACE'
3535 modifier
.show_on_cage
= True
3537 global_mesh_object
= mesh_object
.name
3538 bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
= bpy
.data
.objects
[global_mesh_object
]
3540 bpy
.context
.scene
.tool_settings
.snap_elements
= {'FACE'}
3541 bpy
.context
.scene
.tool_settings
.use_snap
= True
3542 bpy
.context
.scene
.tool_settings
.use_snap_self
= False
3543 bpy
.context
.scene
.tool_settings
.use_snap_align_rotation
= True
3544 bpy
.context
.scene
.tool_settings
.use_snap_project
= True
3545 bpy
.context
.scene
.tool_settings
.use_snap_rotate
= True
3546 bpy
.context
.scene
.tool_settings
.use_snap_scale
= True
3548 bpy
.context
.scene
.tool_settings
.use_mesh_automerge
= True
3549 bpy
.context
.scene
.tool_settings
.double_threshold
= 0.01
3551 if context
.scene
.bsurfaces
.SURFSK_guide
== 'GPencil' and bs
.SURFSK_gpencil
== None:
3552 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3553 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')
3554 bpy
.context
.scene
.tool_settings
.gpencil_stroke_placement_view3d
= 'SURFACE'
3555 gpencil_object
= bpy
.context
.scene
.objects
[bpy
.context
.scene
.objects
[-1].name
]
3556 gpencil_object
.select_set(True)
3557 bpy
.context
.view_layer
.objects
.active
= gpencil_object
3558 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3559 global_gpencil_object
= gpencil_object
.name
3560 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
= bpy
.data
.objects
[global_gpencil_object
]
3561 gpencil_object
.data
.stroke_depth_order
= '3D'
3562 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3563 bpy
.ops
.wm
.tool_set_by_id(name
="builtin_brush.Draw")
3565 if context
.scene
.bsurfaces
.SURFSK_guide
== 'Annotation':
3566 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.annotate")
3567 bpy
.context
.scene
.tool_settings
.annotation_stroke_placement_view3d
= 'SURFACE'
3569 def invoke(self
, context
, event
):
3570 if bpy
.context
.active_object
:
3571 self
.active_object
= bpy
.context
.active_object
3573 self
.active_object
= None
3575 self
.execute(context
)
3579 # ----------------------------
3580 # Add modifiers operator
3581 class MESH_OT_SURFSK_add_modifiers(Operator
):
3582 bl_idname
= "mesh.surfsk_add_modifiers"
3583 bl_label
= "Add Mirror and others modifiers"
3584 bl_description
= "Add modifiers: Mirror, Shrinkwrap, Subdivision, Solidify"
3585 bl_options
= {'REGISTER', 'UNDO'}
3587 def execute(self
, context
):
3589 bs
= bpy
.context
.scene
.bsurfaces
3591 if bpy
.ops
.object.mode_set
.poll():
3592 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3594 if bs
.SURFSK_mesh
== None:
3595 self
.report({'ERROR_INVALID_INPUT'}, "Please select Mesh of BSurface or click Initialize")
3597 mesh_object
= bs
.SURFSK_mesh
3600 mesh_object
.select_set(True)
3602 self
.report({'ERROR_INVALID_INPUT'}, "Mesh of BSurface does not exist")
3605 bpy
.context
.view_layer
.objects
.active
= mesh_object
3608 shrinkwrap
= next(mod
for mod
in mesh_object
.modifiers
3609 if mod
.type == 'SHRINKWRAP')
3611 shrinkwrap
= mesh_object
.modifiers
.new("", 'SHRINKWRAP')
3612 if self
.active_object
is not None and self
.active_object
!= mesh_object
:
3613 shrinkwrap
.target
= self
.active_object
3614 shrinkwrap
.wrap_method
= 'TARGET_PROJECT'
3615 shrinkwrap
.wrap_mode
= 'OUTSIDE_SURFACE'
3616 shrinkwrap
.show_on_cage
= True
3617 shrinkwrap
.offset
= bpy
.context
.scene
.bsurfaces
.SURFSK_Shrinkwrap_offset
3620 mirror
= next(mod
for mod
in mesh_object
.modifiers
3621 if mod
.type == 'MIRROR')
3623 mirror
= mesh_object
.modifiers
.new("", 'MIRROR')
3624 mirror
.use_clip
= True
3627 _subsurf
= next(mod
for mod
in mesh_object
.modifiers
3628 if mod
.type == 'SUBSURF')
3630 _subsurf
= mesh_object
.modifiers
.new("", 'SUBSURF')
3633 solidify
= next(mod
for mod
in mesh_object
.modifiers
3634 if mod
.type == 'SOLIDIFY')
3636 solidify
= mesh_object
.modifiers
.new("", 'SOLIDIFY')
3637 solidify
.thickness
= 0.01
3641 def invoke(self
, context
, event
):
3642 if bpy
.context
.active_object
:
3643 self
.active_object
= bpy
.context
.active_object
3645 self
.active_object
= None
3647 self
.execute(context
)
3651 # ----------------------------
3652 # Edit surface operator
3653 class MESH_OT_SURFSK_edit_surface(Operator
):
3654 bl_idname
= "mesh.surfsk_edit_surface"
3655 bl_label
= "Bsurfaces edit surface"
3656 bl_description
= "Edit surface mesh"
3657 bl_options
= {'REGISTER', 'UNDO'}
3659 def execute(self
, context
):
3660 if bpy
.ops
.object.mode_set
.poll():
3661 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3662 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3663 bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.select_set(True)
3664 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
3665 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3666 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select")
3668 def invoke(self
, context
, event
):
3670 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
3671 bpy
.data
.objects
[global_mesh_object
].select_set(True)
3672 self
.main_object
= bpy
.data
.objects
[global_mesh_object
]
3673 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3675 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3678 self
.execute(context
)
3682 # ----------------------------
3683 # Add strokes operator
3684 class GPENCIL_OT_SURFSK_add_strokes(Operator
):
3685 bl_idname
= "gpencil.surfsk_add_strokes"
3686 bl_label
= "Bsurfaces add strokes"
3687 bl_description
= "Add the grease pencil strokes"
3688 bl_options
= {'REGISTER', 'UNDO'}
3690 def execute(self
, context
):
3691 if bpy
.ops
.object.mode_set
.poll():
3692 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3693 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3695 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3696 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3697 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='PAINT_GPENCIL')
3698 bpy
.ops
.wm
.tool_set_by_id(name
="builtin_brush.Draw")
3702 def invoke(self
, context
, event
):
3704 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3706 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3709 self
.execute(context
)
3713 # ----------------------------
3714 # Edit strokes operator
3715 class GPENCIL_OT_SURFSK_edit_strokes(Operator
):
3716 bl_idname
= "gpencil.surfsk_edit_strokes"
3717 bl_label
= "Bsurfaces edit strokes"
3718 bl_description
= "Edit the grease pencil strokes"
3719 bl_options
= {'REGISTER', 'UNDO'}
3721 def execute(self
, context
):
3722 if bpy
.ops
.object.mode_set
.poll():
3723 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3724 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3726 gpencil_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3728 gpencil_object
.select_set(True)
3729 bpy
.context
.view_layer
.objects
.active
= gpencil_object
3731 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT_GPENCIL')
3733 bpy
.ops
.gpencil
.select_all(action
='SELECT')
3737 def invoke(self
, context
, event
):
3739 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3741 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3744 self
.execute(context
)
3748 # ----------------------------
3749 # Convert annotation to curves operator
3750 class GPENCIL_OT_SURFSK_annotation_to_curves(Operator
):
3751 bl_idname
= "gpencil.surfsk_annotations_to_curves"
3752 bl_label
= "Convert annotation to curves"
3753 bl_description
= "Convert annotation to curves for editing"
3754 bl_options
= {'REGISTER', 'UNDO'}
3756 def execute(self
, context
):
3758 if bpy
.ops
.object.mode_set
.poll():
3759 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3761 # Convert annotation to curve
3762 curve
= conver_gpencil_to_curve(self
, context
, None, 'Annotation')
3765 # Delete annotation strokes
3767 bpy
.context
.annotation_data
.layers
.active
.clear()
3772 curve
.select_set(True)
3773 bpy
.context
.view_layer
.objects
.active
= curve
3775 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select_box")
3779 def invoke(self
, context
, event
):
3781 strokes
= bpy
.context
.annotation_data
.layers
.active
.active_frame
.strokes
3783 _strokes_num
= len(strokes
)
3785 self
.report({'WARNING'}, "Not active annotation")
3788 self
.execute(context
)
3792 # ----------------------------
3793 # Convert strokes to curves operator
3794 class GPENCIL_OT_SURFSK_strokes_to_curves(Operator
):
3795 bl_idname
= "gpencil.surfsk_strokes_to_curves"
3796 bl_label
= "Convert strokes to curves"
3797 bl_description
= "Convert grease pencil strokes to curves for editing"
3798 bl_options
= {'REGISTER', 'UNDO'}
3800 def execute(self
, context
):
3802 if bpy
.ops
.object.mode_set
.poll():
3803 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3805 # Convert grease pencil strokes to curve
3806 gp
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
3807 curve
= conver_gpencil_to_curve(self
, context
, gp
, 'GPensil')
3810 # Delete grease pencil strokes
3812 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
3818 curve
.select_set(True)
3819 bpy
.context
.view_layer
.objects
.active
= curve
3821 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.select_box")
3825 def invoke(self
, context
, event
):
3827 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.select_set(True)
3829 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3832 self
.execute(context
)
3836 # ----------------------------
3838 class GPENCIL_OT_SURFSK_add_annotation(Operator
):
3839 bl_idname
= "gpencil.surfsk_add_annotation"
3840 bl_label
= "Bsurfaces add annotation"
3841 bl_description
= "Add annotation"
3842 bl_options
= {'REGISTER', 'UNDO'}
3844 def execute(self
, context
):
3845 bpy
.ops
.wm
.tool_set_by_id(name
="builtin.annotate")
3846 bpy
.context
.scene
.tool_settings
.annotation_stroke_placement_view3d
= 'SURFACE'
3850 def invoke(self
, context
, event
):
3852 self
.execute(context
)
3857 # ----------------------------
3858 # Edit curve operator
3859 class CURVE_OT_SURFSK_edit_curve(Operator
):
3860 bl_idname
= "curve.surfsk_edit_curve"
3861 bl_label
= "Bsurfaces edit curve"
3862 bl_description
= "Edit curve"
3863 bl_options
= {'REGISTER', 'UNDO'}
3865 def execute(self
, context
):
3866 if bpy
.ops
.object.mode_set
.poll():
3867 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
3868 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3869 bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.select_set(True)
3870 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_curve
3871 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
3873 def invoke(self
, context
, event
):
3875 bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.select_set(True)
3877 self
.report({'WARNING'}, "Specify the name of the object with curve")
3880 self
.execute(context
)
3884 # ----------------------------
3886 class CURVE_OT_SURFSK_reorder_splines(Operator
):
3887 bl_idname
= "curve.surfsk_reorder_splines"
3888 bl_label
= "Bsurfaces reorder splines"
3889 bl_description
= "Defines the order of the splines by using grease pencil strokes"
3890 bl_options
= {'REGISTER', 'UNDO'}
3892 def execute(self
, context
):
3893 objects_to_delete
= []
3894 # Convert grease pencil strokes to curve.
3895 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3896 bpy
.ops
.gpencil
.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes
=False)
3897 for ob
in bpy
.context
.selected_objects
:
3898 if ob
!= bpy
.context
.view_layer
.objects
.active
and ob
.name
.startswith("GP_Layer"):
3899 GP_strokes_curve
= ob
3901 # GP_strokes_curve = bpy.context.object
3902 objects_to_delete
.append(GP_strokes_curve
)
3904 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3905 GP_strokes_curve
.select_set(True)
3906 bpy
.context
.view_layer
.objects
.active
= GP_strokes_curve
3908 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3909 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
3910 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=100)
3911 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3913 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3914 GP_strokes_mesh
= bpy
.context
.object
3915 objects_to_delete
.append(GP_strokes_mesh
)
3917 GP_strokes_mesh
.data
.resolution_u
= 1
3918 bpy
.ops
.object.convert(target
='MESH', keep_original
=False)
3920 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3921 self
.main_curve
.select_set(True)
3922 bpy
.context
.view_layer
.objects
.active
= self
.main_curve
3924 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3925 curves_duplicate_1
= bpy
.context
.object
3926 objects_to_delete
.append(curves_duplicate_1
)
3928 minimum_points_num
= 500
3930 # Some iterations since the subdivision operator
3931 # has a limit of 100 subdivisions per iteration
3932 for x
in range(round(minimum_points_num
/ 100)):
3933 # Check if the number of points of each curve has at least the number of points
3934 # of minimum_points_num. If not, subdivide to reach at least that number of points
3935 for i
in range(len(curves_duplicate_1
.data
.splines
)):
3936 sp
= curves_duplicate_1
.data
.splines
[i
]
3938 if len(sp
.bezier_points
) < minimum_points_num
:
3939 for bp
in sp
.bezier_points
:
3940 bp
.select_control_point
= True
3942 if (len(sp
.bezier_points
) - 1) != 0:
3943 # Formula to get the number of cuts that will make a curve of N
3944 # number of points have near to "minimum_points_num" points,
3945 # when subdividing with this number of cuts
3946 subdivide_cuts
= int(
3947 (minimum_points_num
- len(sp
.bezier_points
)) /
3948 (len(sp
.bezier_points
) - 1)
3953 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3954 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3955 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3956 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3958 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3959 curves_duplicate_2
= bpy
.context
.object
3960 objects_to_delete
.append(curves_duplicate_2
)
3962 # Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target
3963 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3964 curves_duplicate_2
.select_set(True)
3965 bpy
.context
.view_layer
.objects
.active
= curves_duplicate_2
3967 shrinkwrap
= curves_duplicate_2
.modifiers
.new("", 'SHRINKWRAP')
3968 shrinkwrap
.wrap_method
= "NEAREST_VERTEX"
3969 shrinkwrap
.target
= GP_strokes_mesh
3970 bpy
.ops
.object.modifier_apply('INVOKE_REGION_WIN', modifier
=shrinkwrap
.name
)
3972 # Get the distance of each vert from its original position to its position with Shrinkwrap
3973 nearest_points_coords
= {}
3974 for st_idx
in range(len(curves_duplicate_1
.data
.splines
)):
3975 for bp_idx
in range(len(curves_duplicate_1
.data
.splines
[st_idx
].bezier_points
)):
3976 bp_1_co
= curves_duplicate_1
.matrix_world
@ \
3977 curves_duplicate_1
.data
.splines
[st_idx
].bezier_points
[bp_idx
].co
3979 bp_2_co
= curves_duplicate_2
.matrix_world
@ \
3980 curves_duplicate_2
.data
.splines
[st_idx
].bezier_points
[bp_idx
].co
3983 shortest_dist
= (bp_1_co
- bp_2_co
).length
3984 nearest_points_coords
[st_idx
] = ("%.4f" % bp_2_co
[0],
3985 "%.4f" % bp_2_co
[1],
3986 "%.4f" % bp_2_co
[2])
3988 dist
= (bp_1_co
- bp_2_co
).length
3990 if dist
< shortest_dist
:
3991 nearest_points_coords
[st_idx
] = ("%.4f" % bp_2_co
[0],
3992 "%.4f" % bp_2_co
[1],
3993 "%.4f" % bp_2_co
[2])
3994 shortest_dist
= dist
3996 # Get all coords of GP strokes points, for comparison
3997 GP_strokes_coords
= []
3998 for st_idx
in range(len(GP_strokes_curve
.data
.splines
)):
3999 GP_strokes_coords
.append(
4000 [("%.4f" % x
if "%.4f" % x
!= "-0.00" else "0.00",
4001 "%.4f" % y
if "%.4f" % y
!= "-0.00" else "0.00",
4002 "%.4f" % z
if "%.4f" % z
!= "-0.00" else "0.00") for
4003 x
, y
, z
in [bp
.co
for bp
in GP_strokes_curve
.data
.splines
[st_idx
].bezier_points
]]
4006 # Check the point of the GP strokes with the same coords as
4007 # the nearest points of the curves (with shrinkwrap)
4009 # Dictionary with GP stroke index as index, and a list as value.
4010 # The list has as index the point index of the GP stroke
4011 # nearest to the spline, and as value the spline index
4012 GP_connection_points
= {}
4013 for gp_st_idx
in range(len(GP_strokes_coords
)):
4014 GPvert_spline_relationship
= {}
4016 for splines_st_idx
in range(len(nearest_points_coords
)):
4017 if nearest_points_coords
[splines_st_idx
] in GP_strokes_coords
[gp_st_idx
]:
4018 GPvert_spline_relationship
[
4019 GP_strokes_coords
[gp_st_idx
].index(nearest_points_coords
[splines_st_idx
])
4022 GP_connection_points
[gp_st_idx
] = GPvert_spline_relationship
4024 # Get the splines new order
4025 splines_new_order
= []
4026 for i
in GP_connection_points
:
4027 dict_keys
= sorted(GP_connection_points
[i
].keys()) # Sort dictionaries by key
4030 splines_new_order
.append(GP_connection_points
[i
][k
])
4033 curve_original_name
= self
.main_curve
.name
4035 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4036 self
.main_curve
.select_set(True)
4037 bpy
.context
.view_layer
.objects
.active
= self
.main_curve
4039 self
.main_curve
.name
= "SURFSKIO_CRV_ORD"
4041 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4042 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4043 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4045 for _sp_idx
in range(len(self
.main_curve
.data
.splines
)):
4046 self
.main_curve
.data
.splines
[0].bezier_points
[0].select_control_point
= True
4048 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4049 bpy
.ops
.curve
.separate('EXEC_REGION_WIN')
4050 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4052 # Get the names of the separated splines objects in the original order
4053 splines_unordered
= {}
4054 for o
in bpy
.data
.objects
:
4055 if o
.name
.find("SURFSKIO_CRV_ORD") != -1:
4056 spline_order_string
= o
.name
.partition(".")[2]
4058 if spline_order_string
!= "" and int(spline_order_string
) > 0:
4059 spline_order_index
= int(spline_order_string
) - 1
4060 splines_unordered
[spline_order_index
] = o
.name
4062 # Join all splines objects in final order
4063 for order_idx
in splines_new_order
:
4064 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4065 bpy
.data
.objects
[splines_unordered
[order_idx
]].select_set(True)
4066 bpy
.data
.objects
["SURFSKIO_CRV_ORD"].select_set(True)
4067 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
["SURFSKIO_CRV_ORD"]
4069 bpy
.ops
.object.join('INVOKE_REGION_WIN')
4071 # Go back to the original name of the curves object.
4072 bpy
.context
.object.name
= curve_original_name
4074 # Delete all unused objects
4075 with bpy
.context
.temp_override(selected_objects
=objects_to_delete
):
4076 bpy
.ops
.object.delete()
4078 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4079 bpy
.data
.objects
[curve_original_name
].select_set(True)
4080 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[curve_original_name
]
4082 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4083 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4086 bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.data
.layers
.active
.clear()
4093 def invoke(self
, context
, event
):
4094 self
.main_curve
= bpy
.context
.object
4095 there_are_GP_strokes
= False
4098 # Get the active grease pencil layer
4099 strokes_num
= len(self
.main_curve
.grease_pencil
.layers
.active
.active_frame
.strokes
)
4102 there_are_GP_strokes
= True
4106 if there_are_GP_strokes
:
4107 self
.execute(context
)
4108 self
.report({'INFO'}, "Splines have been reordered")
4110 self
.report({'WARNING'}, "Draw grease pencil strokes to connect splines")
4114 # ----------------------------
4115 # Set first points operator
4116 class CURVE_OT_SURFSK_first_points(Operator
):
4117 bl_idname
= "curve.surfsk_first_points"
4118 bl_label
= "Bsurfaces set first points"
4119 bl_description
= "Set the selected points as the first point of each spline"
4120 bl_options
= {'REGISTER', 'UNDO'}
4122 def execute(self
, context
):
4123 splines_to_invert
= []
4125 # Check non-cyclic splines to invert
4126 for i
in range(len(self
.main_curve
.data
.splines
)):
4127 b_points
= self
.main_curve
.data
.splines
[i
].bezier_points
4129 if i
not in self
.cyclic_splines
: # Only for non-cyclic splines
4130 if b_points
[len(b_points
) - 1].select_control_point
:
4131 splines_to_invert
.append(i
)
4133 # Reorder points of cyclic splines, and set all handles to "Automatic"
4135 # Check first selected point
4136 cyclic_splines_new_first_pt
= {}
4137 for i
in self
.cyclic_splines
:
4138 sp
= self
.main_curve
.data
.splines
[i
]
4140 for t
in range(len(sp
.bezier_points
)):
4141 bp
= sp
.bezier_points
[t
]
4142 if bp
.select_control_point
or bp
.select_right_handle
or bp
.select_left_handle
:
4143 cyclic_splines_new_first_pt
[i
] = t
4144 break # To take only one if there are more
4147 for spline_idx
in cyclic_splines_new_first_pt
:
4148 sp
= self
.main_curve
.data
.splines
[spline_idx
]
4150 spline_old_coords
= []
4151 for bp_old
in sp
.bezier_points
:
4152 coords
= (bp_old
.co
[0], bp_old
.co
[1], bp_old
.co
[2])
4154 left_handle_type
= str(bp_old
.handle_left_type
)
4155 left_handle_length
= float(bp_old
.handle_left
.length
)
4157 float(bp_old
.handle_left
.x
),
4158 float(bp_old
.handle_left
.y
),
4159 float(bp_old
.handle_left
.z
)
4161 right_handle_type
= str(bp_old
.handle_right_type
)
4162 right_handle_length
= float(bp_old
.handle_right
.length
)
4163 right_handle_xyz
= (
4164 float(bp_old
.handle_right
.x
),
4165 float(bp_old
.handle_right
.y
),
4166 float(bp_old
.handle_right
.z
)
4168 spline_old_coords
.append(
4169 [coords
, left_handle_type
,
4170 right_handle_type
, left_handle_length
,
4171 right_handle_length
, left_handle_xyz
,
4175 for t
in range(len(sp
.bezier_points
)):
4176 bp
= sp
.bezier_points
4178 if t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1 <= len(bp
) - 1:
4179 new_index
= t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1
4181 new_index
= t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1 - len(bp
)
4183 bp
[t
].co
= Vector(spline_old_coords
[new_index
][0])
4185 bp
[t
].handle_left
.length
= spline_old_coords
[new_index
][3]
4186 bp
[t
].handle_right
.length
= spline_old_coords
[new_index
][4]
4188 bp
[t
].handle_left_type
= "FREE"
4189 bp
[t
].handle_right_type
= "FREE"
4191 bp
[t
].handle_left
.x
= spline_old_coords
[new_index
][5][0]
4192 bp
[t
].handle_left
.y
= spline_old_coords
[new_index
][5][1]
4193 bp
[t
].handle_left
.z
= spline_old_coords
[new_index
][5][2]
4195 bp
[t
].handle_right
.x
= spline_old_coords
[new_index
][6][0]
4196 bp
[t
].handle_right
.y
= spline_old_coords
[new_index
][6][1]
4197 bp
[t
].handle_right
.z
= spline_old_coords
[new_index
][6][2]
4199 bp
[t
].handle_left_type
= spline_old_coords
[new_index
][1]
4200 bp
[t
].handle_right_type
= spline_old_coords
[new_index
][2]
4202 # Invert the non-cyclic splines designated above
4203 for i
in range(len(splines_to_invert
)):
4204 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4206 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4207 self
.main_curve
.data
.splines
[splines_to_invert
[i
]].bezier_points
[0].select_control_point
= True
4208 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4210 bpy
.ops
.curve
.switch_direction()
4212 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
4214 # Keep selected the first vert of each spline
4215 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4216 for i
in range(len(self
.main_curve
.data
.splines
)):
4217 if not self
.main_curve
.data
.splines
[i
].use_cyclic_u
:
4218 bp
= self
.main_curve
.data
.splines
[i
].bezier_points
[0]
4220 bp
= self
.main_curve
.data
.splines
[i
].bezier_points
[
4221 len(self
.main_curve
.data
.splines
[i
].bezier_points
) - 1
4224 bp
.select_control_point
= True
4225 bp
.select_right_handle
= True
4226 bp
.select_left_handle
= True
4228 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4232 def invoke(self
, context
, event
):
4233 self
.main_curve
= bpy
.context
.object
4235 # Check if all curves are Bezier, and detect which ones are cyclic
4236 self
.cyclic_splines
= []
4237 for i
in range(len(self
.main_curve
.data
.splines
)):
4238 if self
.main_curve
.data
.splines
[i
].type != "BEZIER":
4239 self
.report({'WARNING'}, "All splines must be Bezier type")
4241 return {'CANCELLED'}
4243 if self
.main_curve
.data
.splines
[i
].use_cyclic_u
:
4244 self
.cyclic_splines
.append(i
)
4246 self
.execute(context
)
4247 self
.report({'INFO'}, "First points have been set")
4252 # Add-ons Preferences Update Panel
4254 # Define Panel classes for updating
4256 VIEW3D_PT_tools_SURFSK_mesh
,
4257 VIEW3D_PT_tools_SURFSK_curve
4261 def conver_gpencil_to_curve(self
, context
, pencil
, type):
4262 newCurve
= bpy
.data
.curves
.new(type + '_curve', type='CURVE')
4263 newCurve
.dimensions
= '3D'
4264 CurveObject
= object_utils
.object_data_add(context
, newCurve
)
4267 if type == 'GPensil':
4269 strokes
= pencil
.data
.layers
.active
.active_frame
.strokes
4272 CurveObject
.location
= pencil
.location
4273 CurveObject
.rotation_euler
= pencil
.rotation_euler
4274 CurveObject
.scale
= pencil
.scale
4275 elif type == 'Annotation':
4277 strokes
= bpy
.context
.annotation_data
.layers
.active
.active_frame
.strokes
4280 CurveObject
.location
= (0.0, 0.0, 0.0)
4281 CurveObject
.rotation_euler
= (0.0, 0.0, 0.0)
4282 CurveObject
.scale
= (1.0, 1.0, 1.0)
4285 for i
, _stroke
in enumerate(strokes
):
4286 stroke_points
= strokes
[i
].points
4287 data_list
= [ (point
.co
.x
, point
.co
.y
, point
.co
.z
)
4288 for point
in stroke_points
]
4289 points_to_add
= len(data_list
)-1
4292 for point
in data_list
:
4293 flat_list
.extend(point
)
4295 spline
= newCurve
.splines
.new(type='BEZIER')
4296 spline
.bezier_points
.add(points_to_add
)
4297 spline
.bezier_points
.foreach_set("co", flat_list
)
4299 for point
in spline
.bezier_points
:
4300 point
.handle_left_type
="AUTO"
4301 point
.handle_right_type
="AUTO"
4308 def update_panel(self
, context
):
4309 message
= "Bsurfaces GPL Edition: Updating Panel locations has failed"
4311 for panel
in panels
:
4312 if "bl_rna" in panel
.__dict
__:
4313 bpy
.utils
.unregister_class(panel
)
4315 for panel
in panels
:
4316 category
= context
.preferences
.addons
[__name__
].preferences
.category
4317 if category
!= 'Tool':
4318 panel
.bl_category
= context
.preferences
.addons
[__name__
].preferences
.category
4320 context
.preferences
.addons
[__name__
].preferences
.category
= 'Edit'
4321 panel
.bl_category
= 'Edit'
4322 raise ValueError("You can not install add-ons in the Tool panel")
4323 bpy
.utils
.register_class(panel
)
4325 except Exception as e
:
4326 print("\n[{}]\n{}\n\nError:\n{}".format(__name__
, message
, e
))
4329 def makeMaterial(name
, diffuse
):
4331 if name
in bpy
.data
.materials
:
4332 material
= bpy
.data
.materials
[name
]
4333 material
.diffuse_color
= diffuse
4335 material
= bpy
.data
.materials
.new(name
)
4336 material
.diffuse_color
= diffuse
4340 def update_mesh(self
, context
):
4342 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4343 bpy
.ops
.object.select_all(action
='DESELECT')
4344 bpy
.context
.view_layer
.update()
4345 global global_mesh_object
4346 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
4347 bpy
.data
.objects
[global_mesh_object
].select_set(True)
4348 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_mesh_object
]
4350 print("Select mesh object")
4352 def update_gpencil(self
, context
):
4354 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4355 bpy
.ops
.object.select_all(action
='DESELECT')
4356 bpy
.context
.view_layer
.update()
4357 global global_gpencil_object
4358 global_gpencil_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_gpencil
.name
4359 bpy
.data
.objects
[global_gpencil_object
].select_set(True)
4360 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_gpencil_object
]
4362 print("Select gpencil object")
4364 def update_curve(self
, context
):
4366 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4367 bpy
.ops
.object.select_all(action
='DESELECT')
4368 bpy
.context
.view_layer
.update()
4369 global global_curve_object
4370 global_curve_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_curve
.name
4371 bpy
.data
.objects
[global_curve_object
].select_set(True)
4372 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[global_curve_object
]
4374 print("Select curve object")
4376 def update_shade_smooth(self
, context
):
4378 global global_shade_smooth
4379 global_shade_smooth
= bpy
.context
.scene
.bsurfaces
.SURFSK_shade_smooth
4381 contex_mode
= bpy
.context
.mode
4383 if bpy
.ops
.object.mode_set
.poll():
4384 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
4386 bpy
.ops
.object.select_all(action
='DESELECT')
4387 global global_mesh_object
4388 global_mesh_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_mesh
.name
4389 bpy
.data
.objects
[global_mesh_object
].select_set(True)
4391 if global_shade_smooth
:
4392 bpy
.ops
.object.shade_smooth()
4394 bpy
.ops
.object.shade_flat()
4396 if contex_mode
== "EDIT_MESH":
4397 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
4400 print("Select mesh object")
4403 class BsurfPreferences(AddonPreferences
):
4404 # this must match the addon name, use '__package__'
4405 # when defining this in a submodule of a python package.
4406 bl_idname
= __name__
4408 category
: StringProperty(
4409 name
="Tab Category",
4410 description
="Choose a name for the category of the panel",
4415 def draw(self
, context
):
4416 layout
= self
.layout
4420 col
.label(text
="Tab Category:")
4421 col
.prop(self
, "category", text
="")
4424 class BsurfacesProps(PropertyGroup
):
4425 SURFSK_guide
: EnumProperty(
4428 ('Annotation', 'Annotation', 'Annotation'),
4429 ('GPencil', 'GPencil', 'GPencil'),
4430 ('Curve', 'Curve', 'Curve')
4432 default
="Annotation"
4434 SURFSK_edges_U
: IntProperty(
4436 description
="Number of face-loops crossing the strokes",
4441 SURFSK_edges_V
: IntProperty(
4443 description
="Number of face-loops following the strokes",
4448 SURFSK_cyclic_cross
: BoolProperty(
4449 name
="Cyclic Cross",
4450 description
="Make cyclic the face-loops crossing the strokes",
4453 SURFSK_cyclic_follow
: BoolProperty(
4454 name
="Cyclic Follow",
4455 description
="Make cyclic the face-loops following the strokes",
4458 SURFSK_keep_strokes
: BoolProperty(
4459 name
="Keep strokes",
4460 description
="Keeps the sketched strokes or curves after adding the surface",
4463 SURFSK_automatic_join
: BoolProperty(
4464 name
="Automatic join",
4465 description
="Join automatically vertices of either surfaces "
4466 "generated by crosshatching, or from the borders of closed shapes",
4469 SURFSK_loops_on_strokes
: BoolProperty(
4470 name
="Loops on strokes",
4471 description
="Make the loops match the paths of the strokes",
4474 SURFSK_precision
: IntProperty(
4476 description
="Precision level of the surface calculation",
4481 SURFSK_mesh
: PointerProperty(
4482 name
="Mesh of BSurface",
4483 type=bpy
.types
.Object
,
4484 description
="Mesh of BSurface",
4487 SURFSK_gpencil
: PointerProperty(
4488 name
="GreasePencil object",
4489 type=bpy
.types
.Object
,
4490 description
="GreasePencil object",
4491 update
=update_gpencil
,
4493 SURFSK_curve
: PointerProperty(
4494 name
="Curve object",
4495 type=bpy
.types
.Object
,
4496 description
="Curve object",
4497 update
=update_curve
,
4499 SURFSK_shade_smooth
: BoolProperty(
4500 name
="Shade smooth",
4501 description
="Render and display faces smooth, using interpolated Vertex Normals",
4503 update
=update_shade_smooth
,
4507 MESH_OT_SURFSK_init
,
4508 MESH_OT_SURFSK_add_modifiers
,
4509 MESH_OT_SURFSK_add_surface
,
4510 MESH_OT_SURFSK_edit_surface
,
4511 GPENCIL_OT_SURFSK_add_strokes
,
4512 GPENCIL_OT_SURFSK_edit_strokes
,
4513 GPENCIL_OT_SURFSK_strokes_to_curves
,
4514 GPENCIL_OT_SURFSK_annotation_to_curves
,
4515 GPENCIL_OT_SURFSK_add_annotation
,
4516 CURVE_OT_SURFSK_edit_curve
,
4517 CURVE_OT_SURFSK_reorder_splines
,
4518 CURVE_OT_SURFSK_first_points
,
4525 bpy
.utils
.register_class(cls
)
4527 for panel
in panels
:
4528 bpy
.utils
.register_class(panel
)
4530 bpy
.types
.Scene
.bsurfaces
= PointerProperty(type=BsurfacesProps
)
4531 update_panel(None, bpy
.context
)
4534 for panel
in panels
:
4535 bpy
.utils
.unregister_class(panel
)
4538 bpy
.utils
.unregister_class(cls
)
4540 del bpy
.types
.Scene
.bsurfaces
4542 if __name__
== "__main__":