5b1bceb1453f42887271f084e1b7ad9768ed2fa1
[blender-addons-contrib.git] / add_advanced_objects_menu / copy2.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software: you can redistribute it and/or modify
4 #  it under the terms of the GNU General Public License as published by
5 #  the Free Software Foundation, either version 3 of the License, or
6 #  (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program.  If not, see http://www.gnu.org/licenses/
15 #  or write to the Free Software Foundation, Inc., 51 Franklin Street,
16 #  Fifth Floor, Boston, MA 02110-1301, USA.
17 #
18 # ##### END GPL LICENSE BLOCK #####
19
20 bl_info = {
21     "name": "Copy2 Vertices, Edges or Faces",
22     "author": "Eleanor Howick (elfnor.com)",
23     "version": (0, 1, 1),
24     "blender": (2, 71, 0),
25     "location": "3D View > Object > Copy 2",
26     "description": "Copy one object to the selected vertices, edges or faces of another object",
27     "warning": "",
28     "category": "Object"
29 }
30
31 import bpy
32 from bpy.types import Operator
33 from bpy.props import (
34         BoolProperty,
35         EnumProperty,
36         FloatProperty,
37         )
38 from mathutils import (
39         Vector,
40         Matrix,
41         )
42
43
44 class Copy2(Operator):
45     bl_idname = "mesh.copy2"
46     bl_label = "Copy 2"
47     bl_description = ("Copy Vertices, Edges or Faces to the Selected object\n"
48                       "Needs an existing Active Mesh Object")
49     bl_options = {"REGISTER", "UNDO"}
50
51     obj_list = None
52
53     def obj_list_cb(self, context):
54         return Copy2.obj_list
55
56     def sec_axes_list_cb(self, context):
57         if self.priaxes == 'X':
58             sec_list = [('Y', "Y", "Secondary axis Y"),
59                         ('Z', "Z", "Secondary axis Z")]
60
61         if self.priaxes == 'Y':
62             sec_list = [('X', "X", "Secondary axis X"),
63                         ('Z', "Z", "Secondary axis Z")]
64
65         if self.priaxes == 'Z':
66             sec_list = [('X', "X", "Secondary axis X"),
67                         ('Y', "Y", "Secondary axis Y")]
68         return sec_list
69
70     copytype: EnumProperty(
71             items=(('V', "Vertex",
72                     "Paste the Copied Geometry to Vertices of the Active Object", 'VERTEXSEL', 0),
73                    ('E', "Edge",
74                     "Paste the Copied Geometry to Edges of the Active Object", 'EDGESEL', 1),
75                    ('F', "Face",
76                     "Paste the Copied Geometry to Faces of the Active Object", 'FACESEL', 2)),
77             )
78     copyfromobject: EnumProperty(
79             name="Copy from",
80             description="Copy an Object from the list",
81             items=obj_list_cb
82             )
83     priaxes: EnumProperty(
84             description="Primary axes used for Copied Object orientation",
85             items=(('X', "X", "Along X"),
86                    ('Y', "Y", "Along Y"),
87                    ('Z', "Z", "Along Z")),
88             )
89     edgescale: BoolProperty(
90             name="Scale to fill edge",
91             default=False
92             )
93     secaxes: EnumProperty(
94             name="Secondary Axis",
95             description="Secondary axis used for Copied Object orientation",
96             items=sec_axes_list_cb
97             )
98     scale: FloatProperty(
99             name="Scale",
100             default=1.0,
101             min=0.0,
102             )
103
104     @classmethod
105     def poll(cls, context):
106         obj = context.active_object
107         return obj and obj.type == "MESH"
108
109     def draw(self, context):
110         layout = self.layout
111
112         layout.prop(self, "copyfromobject")
113         layout.label(text="to:")
114         layout.prop(self, "copytype", expand=True)
115         layout.label(text="Primary axis:")
116         layout.prop(self, "priaxes", expand=True)
117         layout.label(text="Secondary axis:")
118         layout.prop(self, "secaxes", expand=True)
119         if self.copytype == "E":
120             layout.prop(self, "edgescale")
121             if self.edgescale:
122                 layout.prop(self, "scale")
123         return
124
125     def execute(self, context):
126         copytoobject = context.active_object.name
127         axes = self.priaxes + self.secaxes
128
129         # check if there is a problem with the strings related to some chars
130         copy_to_object = bpy.data.objects[copytoobject] if \
131                          copytoobject in bpy.data.objects else None
132
133         copy_from_object = bpy.data.objects[self.copyfromobject] if \
134                            self.copyfromobject in bpy.data.objects else None
135
136         if copy_to_object is None or copy_from_object is None:
137             self.report({"WARNING"},
138                         "There was a problem with retrieving Object data. Operation Cancelled")
139             return {"CANCELLED"}
140         try:
141             copy_to_from(
142                     context.collection,
143                     copy_to_object,
144                     copy_from_object,
145                     self.copytype,
146                     axes,
147                     self.edgescale,
148                     self.scale
149                     )
150         except Exception as e:
151             self.report({"WARNING"},
152                         "Copy2 could not be completed (Check the Console for more info)")
153             print("\n[Add Advanced Objects]\nOperator: mesh.copy2\n{}\n".format(e))
154
155             return {"CANCELLED"}
156
157         return {"FINISHED"}
158
159     def invoke(self, context, event):
160         Copy2.obj_list = [(obj.name, obj.name, obj.name) for obj in bpy.data.objects]
161
162         return {"FINISHED"}
163
164
165 def copy_to_from(collection, to_obj, from_obj, copymode, axes, edgescale, scale):
166     if copymode == 'V':
167         vertex_copy(collection, to_obj, from_obj, axes)
168
169     if copymode == 'E':
170         # don't pass edgescalling to object types that cannot be scaled
171         if from_obj.type in ["CAMERA", "LIGHT", "EMPTY", "ARMATURE", "SPEAKER", "META"]:
172             edgescale = False
173         edge_copy(collection, to_obj, from_obj, axes, edgescale, scale)
174
175     if copymode == 'F':
176         face_copy(collection, to_obj, from_obj, axes)
177
178
179 axes_dict = {'XY': (1, 2, 0),
180              'XZ': (2, 1, 0),
181              'YX': (0, 2, 1),
182              'YZ': (2, 0, 1),
183              'ZX': (0, 1, 2),
184              'ZY': (1, 0, 2)}
185
186
187 def copyto(collection, source_obj, pos, xdir, zdir, axes, scale=None):
188     """
189     copy the source_obj to pos, so its primary axis points in zdir and its
190     secondary axis points in xdir
191     """
192     copy_obj = source_obj.copy()
193     collection.objects.link(copy_obj)
194
195     xdir = xdir.normalized()
196     zdir = zdir.normalized()
197     # rotation first
198     z_axis = zdir
199     x_axis = xdir
200     y_axis = z_axis.cross(x_axis)
201     # use axes_dict to assign the axis as chosen in panel
202     A, B, C = axes_dict[axes]
203     rot_mat = Matrix()
204     rot_mat[A].xyz = x_axis
205     rot_mat[B].xyz = y_axis
206     rot_mat[C].xyz = z_axis
207     rot_mat.transpose()
208
209     # rotate object
210     copy_obj.matrix_world = rot_mat
211
212     # move object into position
213     copy_obj.location = pos
214
215     # scale object
216     if scale is not None:
217         copy_obj.scale = scale
218
219     return copy_obj
220
221
222 def vertex_copy(collection, obj, source_obj, axes):
223     # vertex select mode
224     sel_verts = []
225     copy_list = []
226
227     for v in obj.data.vertices:
228         if v.select is True:
229             sel_verts.append(v)
230
231     # make a set for each vertex. The set contains all the connected vertices
232     # use sets so the list is unique
233     vert_con = [set() for i in range(len(obj.data.vertices))]
234     for e in obj.data.edges:
235         vert_con[e.vertices[0]].add(e.vertices[1])
236         vert_con[e.vertices[1]].add(e.vertices[0])
237
238     for v in sel_verts:
239         pos = v.co * obj.matrix_world.transposed()
240         xco = obj.data.vertices[list(vert_con[v.index])[0]].co * obj.matrix_world.transposed()
241
242         zdir = (v.co + v.normal) * obj.matrix_world.transposed() - pos
243         zdir = zdir.normalized()
244
245         edir = pos - xco
246
247         # edir is nor perpendicular to z dir
248         # want xdir to be projection of edir onto plane through pos with direction zdir
249         xdir = edir - edir.dot(zdir) * zdir
250         xdir = -xdir.normalized()
251
252         copy = copyto(collection, source_obj, pos, xdir, zdir, axes)
253         copy_list.append(copy)
254
255     # select all copied objects
256     for copy in copy_list:
257         copy.select_set(True)
258     obj.select_set(False)
259
260
261 def edge_copy(collection, obj, source_obj, axes, es, scale):
262     # edge select mode
263     sel_edges = []
264     copy_list = []
265
266     for e in obj.data.edges:
267         if e.select is True:
268             sel_edges.append(e)
269
270     for e in sel_edges:
271         # pos is average of two edge vertexs
272         v0 = obj.data.vertices[e.vertices[0]].co * obj.matrix_world.transposed()
273         v1 = obj.data.vertices[e.vertices[1]].co * obj.matrix_world.transposed()
274         pos = (v0 + v1) / 2
275         # xdir is along edge
276         xdir = v0 - v1
277         xlen = xdir.magnitude
278         xdir = xdir.normalized()
279         # project each edge vertex normal onto plane normal to xdir
280         vn0 = (obj.data.vertices[e.vertices[0]].co * obj.matrix_world.transposed() +
281                obj.data.vertices[e.vertices[0]].normal) - v0
282         vn1 = (obj.data.vertices[e.vertices[1]].co * obj.matrix_world.transposed() +
283                obj.data.vertices[e.vertices[1]].normal) - v1
284         vn0p = vn0 - vn0.dot(xdir) * xdir
285         vn1p = vn1 - vn1.dot(xdir) * xdir
286         # the mean of the two projected normals is the zdir
287         zdir = vn0p + vn1p
288         zdir = zdir.normalized()
289         escale = None
290         if es:
291             escale = Vector([1.0, 1.0, 1.0])
292             i = list('XYZ').index(axes[1])
293             escale[i] = scale * xlen / source_obj.dimensions[i]
294
295         copy = copyto(collection, source_obj, pos, xdir, zdir, axes, scale=escale)
296         copy_list.append(copy)
297
298     # select all copied objects
299     for copy in copy_list:
300         copy.select_set(True)
301     obj.select_set(False)
302
303
304 def face_copy(collection, obj, source_obj, axes):
305     # face select mode
306     sel_faces = []
307     copy_list = []
308
309     for f in obj.data.polygons:
310         if f.select is True:
311             sel_faces.append(f)
312
313     for f in sel_faces:
314         fco = f.center * obj.matrix_world.transposed()
315         # get first vertex corner of transformed object
316         vco = obj.data.vertices[f.vertices[0]].co * obj.matrix_world.transposed()
317         # get face normal of transformed object
318         fn = (f.center + f.normal) * obj.matrix_world.transposed() - fco
319         fn = fn.normalized()
320
321         copy = copyto(collection, source_obj, fco, vco - fco, fn, axes)
322         copy_list.append(copy)
323
324     # select all copied objects
325     for copy in copy_list:
326         copy.select_set(True)
327     obj.select_set(False)
328
329
330 def register():
331     bpy.utils.register_class(Copy2)
332
333
334 def unregister():
335     bpy.utils.unregister_class(Copy2)
336
337
338 if __name__ == "__main__":
339     register()