operators were copying the properties from the rna operator into the class instance.
[blender.git] / release / scripts / io / export_3ds.py
1 # coding: utf-8
2 # ##### BEGIN GPL LICENSE BLOCK #####
3 #
4 #  This program is free software; you can redistribute it and/or
5 #  modify it under the terms of the GNU General Public License
6 #  as published by the Free Software Foundation; either version 2
7 #  of the License, or (at your option) any later version.
8
9 #  This program is distributed in the hope that it will be useful,
10 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
11 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 #  GNU General Public License for more details.
13
14 #  You should have received a copy of the GNU General Public License
15 #  along with this program; if not, write to the Free Software Foundation,
16 #  Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
17 #
18 # ##### END GPL LICENSE BLOCK #####
19
20 __author__ = ["Campbell Barton", "Bob Holcomb", "Richard Lärkäng", "Damien McGinnes", "Mark Stijnman"]
21 __url__ = ("blenderartists.org", "www.blender.org", "www.gametutorials.com", "lib3ds.sourceforge.net/")
22 __version__ = "0.90a"
23 __bpydoc__ = """\
24
25 3ds Exporter
26
27 This script Exports a 3ds file.
28
29 Exporting is based on 3ds loader from www.gametutorials.com(Thanks DigiBen) and using information
30 from the lib3ds project (http://lib3ds.sourceforge.net/) sourcecode.
31 """
32
33 # ***** BEGIN GPL LICENSE BLOCK *****
34 #
35 # Script copyright (C) Bob Holcomb 
36 #
37 # This program is free software; you can redistribute it and/or
38 # modify it under the terms of the GNU General Public License
39 # as published by the Free Software Foundation; either version 2
40 # of the License, or (at your option) any later version.
41 #
42 # This program is distributed in the hope that it will be useful,
43 # but WITHOUT ANY WARRANTY; without even the implied warranty of
44 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
45 # GNU General Public License for more details.
46 #
47 # You should have received a copy of the GNU General Public License
48 # along with this program; if not, write to the Free Software Foundation,
49 # Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
50 #
51 # ***** END GPL LICENCE BLOCK *****
52 # --------------------------------------------------------------------------
53
54
55 ######################################################
56 # Importing modules
57 ######################################################
58
59 import struct
60 import os
61 import time
62
63 import bpy
64
65 # import Blender
66 # from BPyMesh import getMeshFromObject
67 # from BPyObject import getDerivedObjects
68 # try: 
69 #     import struct
70 # except: 
71 #     struct = None
72
73 # also used by X3D exporter
74 # return a tuple (free, object list), free is True if memory should be freed later with free_derived_objects()
75 def create_derived_objects(ob):
76         if ob.parent and ob.parent.dupli_type != 'NONE':
77                 return False, None
78
79         if ob.dupli_type != 'NONE':
80                 ob.create_dupli_list()
81                 return True, [(dob.object, dob.matrix) for dob in ob.dupli_list]
82         else:
83                 return False, [(ob, ob.matrix)]
84
85 # also used by X3D exporter
86 def free_derived_objects(ob):
87         ob.free_dupli_list()
88
89 # So 3ds max can open files, limit names to 12 in length
90 # this is verry annoying for filenames!
91 name_unique = []
92 name_mapping = {}
93 def sane_name(name):
94         name_fixed = name_mapping.get(name)
95         if name_fixed != None:
96                 return name_fixed
97         
98         if len(name) > 12:
99                 new_name = name[:12]
100         else:
101                 new_name = name
102         
103         i = 0
104         
105         while new_name in name_unique:
106                 new_name = new_name[:-4] + '.%.3d' % i
107                 i+=1
108         
109         name_unique.append(new_name)
110         name_mapping[name] = new_name
111         return new_name
112
113 ######################################################
114 # Data Structures
115 ######################################################
116
117 #Some of the chunks that we will export
118 #----- Primary Chunk, at the beginning of each file
119 PRIMARY= int("0x4D4D",16)
120
121 #------ Main Chunks
122 OBJECTINFO   =      int("0x3D3D",16);      #This gives the version of the mesh and is found right before the material and object information
123 VERSION      =      int("0x0002",16);      #This gives the version of the .3ds file
124 KFDATA       =      int("0xB000",16);      #This is the header for all of the key frame info
125
126 #------ sub defines of OBJECTINFO
127 MATERIAL=45055          #0xAFFF                         // This stored the texture info
128 OBJECT=16384            #0x4000                         // This stores the faces, vertices, etc...
129
130 #>------ sub defines of MATERIAL
131 MATNAME    =      int("0xA000",16);      # This holds the material name
132 MATAMBIENT   =      int("0xA010",16);      # Ambient color of the object/material
133 MATDIFFUSE   =      int("0xA020",16);      # This holds the color of the object/material
134 MATSPECULAR   =      int("0xA030",16);      # SPecular color of the object/material
135 MATSHINESS   =      int("0xA040",16);      # ??
136 MATMAP       =      int("0xA200",16);      # This is a header for a new material
137 MATMAPFILE    =      int("0xA300",16);      # This holds the file name of the texture
138
139 RGB1=   int("0x0011",16)
140 RGB2=   int("0x0012",16)
141
142 #>------ sub defines of OBJECT
143 OBJECT_MESH  =      int("0x4100",16);      # This lets us know that we are reading a new object
144 OBJECT_LIGHT =      int("0x4600",16);      # This lets un know we are reading a light object
145 OBJECT_CAMERA=      int("0x4700",16);      # This lets un know we are reading a camera object
146
147 #>------ sub defines of CAMERA
148 OBJECT_CAM_RANGES=   int("0x4720",16);      # The camera range values
149
150 #>------ sub defines of OBJECT_MESH
151 OBJECT_VERTICES =   int("0x4110",16);      # The objects vertices
152 OBJECT_FACES    =   int("0x4120",16);      # The objects faces
153 OBJECT_MATERIAL =   int("0x4130",16);      # This is found if the object has a material, either texture map or color
154 OBJECT_UV       =   int("0x4140",16);      # The UV texture coordinates
155 OBJECT_TRANS_MATRIX  =   int("0x4160",16); # The Object Matrix
156
157 #>------ sub defines of KFDATA
158 KFDATA_KFHDR            = int("0xB00A",16);
159 KFDATA_KFSEG            = int("0xB008",16);
160 KFDATA_KFCURTIME        = int("0xB009",16);
161 KFDATA_OBJECT_NODE_TAG  = int("0xB002",16);
162
163 #>------ sub defines of OBJECT_NODE_TAG
164 OBJECT_NODE_ID          = int("0xB030",16);
165 OBJECT_NODE_HDR         = int("0xB010",16);
166 OBJECT_PIVOT            = int("0xB013",16);
167 OBJECT_INSTANCE_NAME    = int("0xB011",16);
168 POS_TRACK_TAG                   = int("0xB020",16);
169 ROT_TRACK_TAG                   = int("0xB021",16);
170 SCL_TRACK_TAG                   = int("0xB022",16);
171
172 def uv_key(uv):
173         return round(uv[0], 6), round(uv[1], 6)
174 #       return round(uv.x, 6), round(uv.y, 6)
175
176 # size defines: 
177 SZ_SHORT = 2
178 SZ_INT   = 4
179 SZ_FLOAT = 4
180
181 class _3ds_short(object):
182         '''Class representing a short (2-byte integer) for a 3ds file.
183         *** This looks like an unsigned short H is unsigned from the struct docs - Cam***'''
184         __slots__ = 'value'
185         def __init__(self, val=0):
186                 self.value=val
187         
188         def get_size(self):
189                 return SZ_SHORT
190
191         def write(self,file):
192                 file.write(struct.pack("<H", self.value))
193                 
194         def __str__(self):
195                 return str(self.value)
196
197 class _3ds_int(object):
198         '''Class representing an int (4-byte integer) for a 3ds file.'''
199         __slots__ = 'value'
200         def __init__(self, val=0):
201                 self.value=val
202         
203         def get_size(self):
204                 return SZ_INT
205
206         def write(self,file):
207                 file.write(struct.pack("<I", self.value))
208         
209         def __str__(self):
210                 return str(self.value)
211
212 class _3ds_float(object):
213         '''Class representing a 4-byte IEEE floating point number for a 3ds file.'''
214         __slots__ = 'value'
215         def __init__(self, val=0.0):
216                 self.value=val
217         
218         def get_size(self):
219                 return SZ_FLOAT
220
221         def write(self,file):
222                 file.write(struct.pack("<f", self.value))
223         
224         def __str__(self):
225                 return str(self.value)
226
227
228 class _3ds_string(object):
229         '''Class representing a zero-terminated string for a 3ds file.'''
230         __slots__ = 'value'
231         def __init__(self, val=""):
232                 self.value=val
233         
234         def get_size(self):
235                 return (len(self.value)+1)
236
237         def write(self,file):
238                 binary_format = "<%ds" % (len(self.value)+1)
239                 file.write(struct.pack(binary_format, self.value))
240         
241         def __str__(self):
242                 return self.value
243
244 class _3ds_point_3d(object):
245         '''Class representing a three-dimensional point for a 3ds file.'''
246         __slots__ = 'x','y','z'
247         def __init__(self, point=(0.0,0.0,0.0)):
248                 self.x, self.y, self.z = point
249                 
250         def get_size(self):
251                 return 3*SZ_FLOAT
252
253         def write(self,file):
254                 file.write(struct.pack('<3f', self.x, self.y, self.z))
255         
256         def __str__(self):
257                 return '(%f, %f, %f)' % (self.x, self.y, self.z)
258                 
259 # Used for writing a track
260 """
261 class _3ds_point_4d(object):
262         '''Class representing a four-dimensional point for a 3ds file, for instance a quaternion.'''
263         __slots__ = 'x','y','z','w'
264         def __init__(self, point=(0.0,0.0,0.0,0.0)):
265                 self.x, self.y, self.z, self.w = point  
266         
267         def get_size(self):
268                 return 4*SZ_FLOAT
269
270         def write(self,file):
271                 data=struct.pack('<4f', self.x, self.y, self.z, self.w)
272                 file.write(data)
273
274         def __str__(self):
275                 return '(%f, %f, %f, %f)' % (self.x, self.y, self.z, self.w)
276 """
277
278 class _3ds_point_uv(object):
279         '''Class representing a UV-coordinate for a 3ds file.'''
280         __slots__ = 'uv'
281         def __init__(self, point=(0.0,0.0)):
282                 self.uv = point
283         
284         def __cmp__(self, other):
285                 return cmp(self.uv,other.uv)    
286         
287         def get_size(self):
288                 return 2*SZ_FLOAT
289         
290         def write(self,file):
291                 data=struct.pack('<2f', self.uv[0], self.uv[1])
292                 file.write(data)
293         
294         def __str__(self):
295                 return '(%g, %g)' % self.uv
296
297 class _3ds_rgb_color(object):
298         '''Class representing a (24-bit) rgb color for a 3ds file.'''
299         __slots__ = 'r','g','b'
300         def __init__(self, col=(0,0,0)):
301                 self.r, self.g, self.b = col
302         
303         def get_size(self):
304                 return 3
305         
306         def write(self,file):
307                 file.write( struct.pack('<3B', int(255*self.r), int(255*self.g), int(255*self.b) ) )
308 #               file.write( struct.pack('<3c', chr(int(255*self.r)), chr(int(255*self.g)), chr(int(255*self.b)) ) )
309         
310         def __str__(self):
311                 return '{%f, %f, %f}' % (self.r, self.g, self.b)
312
313 class _3ds_face(object):
314         '''Class representing a face for a 3ds file.'''
315         __slots__ = 'vindex'
316         def __init__(self, vindex):
317                 self.vindex = vindex
318         
319         def get_size(self):
320                 return 4*SZ_SHORT
321         
322         def write(self,file):
323                 # The last zero is only used by 3d studio
324                 file.write(struct.pack("<4H", self.vindex[0],self.vindex[1], self.vindex[2], 0))
325         
326         def __str__(self):
327                 return '[%d %d %d]' % (self.vindex[0],self.vindex[1], self.vindex[2])
328
329 class _3ds_array(object):
330         '''Class representing an array of variables for a 3ds file.
331
332         Consists of a _3ds_short to indicate the number of items, followed by the items themselves.
333         '''
334         __slots__ = 'values', 'size'
335         def __init__(self):
336                 self.values=[]
337                 self.size=SZ_SHORT
338         
339         # add an item:
340         def add(self,item):
341                 self.values.append(item)
342                 self.size+=item.get_size()
343         
344         def get_size(self):
345                 return self.size
346         
347         def write(self,file):
348                 _3ds_short(len(self.values)).write(file)
349                 #_3ds_int(len(self.values)).write(file)
350                 for value in self.values:
351                         value.write(file)
352         
353         # To not overwhelm the output in a dump, a _3ds_array only
354         # outputs the number of items, not all of the actual items. 
355         def __str__(self):
356                 return '(%d items)' % len(self.values)
357
358 class _3ds_named_variable(object):
359         '''Convenience class for named variables.'''
360         
361         __slots__ = 'value', 'name'
362         def __init__(self, name, val=None):
363                 self.name=name
364                 self.value=val
365         
366         def get_size(self):
367                 if (self.value==None): 
368                         return 0
369                 else:
370                         return self.value.get_size()
371         
372         def write(self, file):
373                 if (self.value!=None): 
374                         self.value.write(file)
375         
376         def dump(self,indent):
377                 if (self.value!=None):
378                         spaces=""
379                         for i in range(indent):
380                                 spaces+="  ";
381                         if (self.name!=""):
382                                 print(spaces, self.name, " = ", self.value)
383                         else:
384                                 print(spaces, "[unnamed]", " = ", self.value)
385
386
387 #the chunk class
388 class _3ds_chunk(object):
389         '''Class representing a chunk in a 3ds file.
390
391         Chunks contain zero or more variables, followed by zero or more subchunks.
392         '''
393         __slots__ = 'ID', 'size', 'variables', 'subchunks'
394         def __init__(self, id=0):
395                 self.ID=_3ds_short(id)
396                 self.size=_3ds_int(0)
397                 self.variables=[]
398                 self.subchunks=[]
399         
400         def set_ID(id):
401                 self.ID=_3ds_short(id)
402         
403         def add_variable(self, name, var):
404                 '''Add a named variable. 
405                 
406                 The name is mostly for debugging purposes.'''
407                 self.variables.append(_3ds_named_variable(name,var))
408         
409         def add_subchunk(self, chunk):
410                 '''Add a subchunk.'''
411                 self.subchunks.append(chunk)
412
413         def get_size(self):
414                 '''Calculate the size of the chunk and return it.
415                 
416                 The sizes of the variables and subchunks are used to determine this chunk\'s size.'''
417                 tmpsize=self.ID.get_size()+self.size.get_size()
418                 for variable in self.variables:
419                         tmpsize+=variable.get_size()
420                 for subchunk in self.subchunks:
421                         tmpsize+=subchunk.get_size()
422                 self.size.value=tmpsize
423                 return self.size.value
424
425         def write(self, file):
426                 '''Write the chunk to a file.
427                 
428                 Uses the write function of the variables and the subchunks to do the actual work.'''
429                 #write header
430                 self.ID.write(file)
431                 self.size.write(file)
432                 for variable in self.variables:
433                         variable.write(file)
434                 for subchunk in self.subchunks:
435                         subchunk.write(file)
436                 
437                 
438         def dump(self, indent=0):
439                 '''Write the chunk to a file.
440                 
441                 Dump is used for debugging purposes, to dump the contents of a chunk to the standard output. 
442                 Uses the dump function of the named variables and the subchunks to do the actual work.'''
443                 spaces=""
444                 for i in range(indent):
445                         spaces+="  ";
446                 print(spaces, "ID=", hex(self.ID.value), "size=", self.get_size())
447                 for variable in self.variables:
448                         variable.dump(indent+1)
449                 for subchunk in self.subchunks:
450                         subchunk.dump(indent+1)
451
452
453
454 ######################################################
455 # EXPORT
456 ######################################################
457
458 def get_material_images(material):
459         # blender utility func.
460         if material:
461                 return [s.texture.image for s in material.textures if s and s.texture.type == 'IMAGE' and s.texture.image]
462
463         return []
464 #       images = []
465 #       if material:
466 #               for mtex in material.getTextures():
467 #                       if mtex and mtex.tex.type == Blender.Texture.Types.IMAGE:
468 #                               image = mtex.tex.image
469 #                               if image:
470 #                                       images.append(image) # maye want to include info like diffuse, spec here.
471 #       return images
472
473
474 def make_material_subchunk(id, color):
475         '''Make a material subchunk.
476         
477         Used for color subchunks, such as diffuse color or ambient color subchunks.'''
478         mat_sub = _3ds_chunk(id)
479         col1 = _3ds_chunk(RGB1)
480         col1.add_variable("color1", _3ds_rgb_color(color));
481         mat_sub.add_subchunk(col1)
482 # optional:
483 #       col2 = _3ds_chunk(RGB1)
484 #       col2.add_variable("color2", _3ds_rgb_color(color));
485 #       mat_sub.add_subchunk(col2)
486         return mat_sub
487
488
489 def make_material_texture_chunk(id, images):
490         """Make Material Map texture chunk
491         """
492         mat_sub = _3ds_chunk(id)
493         
494         def add_image(img):
495                 filename = os.path.basename(image.filename)
496 #               filename = image.filename.split('\\')[-1].split('/')[-1]
497                 mat_sub_file = _3ds_chunk(MATMAPFILE)
498                 mat_sub_file.add_variable("mapfile", _3ds_string(sane_name(filename)))
499                 mat_sub.add_subchunk(mat_sub_file)
500         
501         for image in images:
502                 add_image(image)
503         
504         return mat_sub
505
506 def make_material_chunk(material, image):
507         '''Make a material chunk out of a blender material.'''
508         material_chunk = _3ds_chunk(MATERIAL)
509         name = _3ds_chunk(MATNAME)
510         
511         if material:    name_str = material.name
512         else:                   name_str = 'None'
513         if image:       name_str += image.name
514                 
515         name.add_variable("name", _3ds_string(sane_name(name_str)))
516         material_chunk.add_subchunk(name)
517         
518         if not material:
519                 material_chunk.add_subchunk(make_material_subchunk(MATAMBIENT, (0,0,0) ))
520                 material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, (.8, .8, .8) ))
521                 material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, (1,1,1) ))
522         
523         else:
524                 material_chunk.add_subchunk(make_material_subchunk(MATAMBIENT, [a*material.ambient for a in material.diffuse_color] ))
525 #               material_chunk.add_subchunk(make_material_subchunk(MATAMBIENT, [a*material.amb for a in material.rgbCol] ))
526                 material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, material.diffuse_color))
527 #               material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, material.rgbCol))
528                 material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, material.specular_color))
529 #               material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, material.specCol))
530                 
531                 images = get_material_images(material) # can be None
532                 if image: images.append(image)
533                 
534                 if images:
535                         material_chunk.add_subchunk(make_material_texture_chunk(MATMAP, images))
536         
537         return material_chunk
538
539 class tri_wrapper(object):
540         '''Class representing a triangle.
541         
542         Used when converting faces to triangles'''
543         
544         __slots__ = 'vertex_index', 'mat', 'image', 'faceuvs', 'offset'
545         def __init__(self, vindex=(0,0,0), mat=None, image=None, faceuvs=None):
546                 self.vertex_index= vindex
547                 self.mat= mat
548                 self.image= image
549                 self.faceuvs= faceuvs
550                 self.offset= [0, 0, 0] # offset indicies
551
552
553 def extract_triangles(mesh):
554         '''Extract triangles from a mesh.
555         
556         If the mesh contains quads, they will be split into triangles.'''
557         tri_list = []
558         do_uv = len(mesh.uv_textures)
559 #       do_uv = mesh.faceUV
560         
561 #       if not do_uv:
562 #               face_uv = None
563         
564         img = None
565         for i, face in enumerate(mesh.faces):
566                 f_v = face.verts
567 #               f_v = face.v
568
569                 uf = mesh.active_uv_texture.data[i] if do_uv else None
570                 
571                 if do_uv:
572                         f_uv = uf.uv
573                         # f_uv =  (uf.uv1, uf.uv2, uf.uv3, uf.uv4) if face.verts[3] else (uf.uv1, uf.uv2, uf.uv3)
574 #                       f_uv = face.uv
575                         img = uf.image if uf else None
576 #                       img = face.image
577                         if img: img = img.name
578                 
579                 # if f_v[3] == 0:
580                 if len(f_v)==3:
581                         new_tri = tri_wrapper((f_v[0], f_v[1], f_v[2]), face.material_index, img)
582 #                       new_tri = tri_wrapper((f_v[0].index, f_v[1].index, f_v[2].index), face.mat, img)
583                         if (do_uv): new_tri.faceuvs= uv_key(f_uv[0]), uv_key(f_uv[1]), uv_key(f_uv[2])
584                         tri_list.append(new_tri)
585                 
586                 else: #it's a quad
587                         new_tri = tri_wrapper((f_v[0], f_v[1], f_v[2]), face.material_index, img)
588 #                       new_tri = tri_wrapper((f_v[0].index, f_v[1].index, f_v[2].index), face.mat, img)
589                         new_tri_2 = tri_wrapper((f_v[0], f_v[2], f_v[3]), face.material_index, img)
590 #                       new_tri_2 = tri_wrapper((f_v[0].index, f_v[2].index, f_v[3].index), face.mat, img)
591                         
592                         if (do_uv):
593                                 new_tri.faceuvs= uv_key(f_uv[0]), uv_key(f_uv[1]), uv_key(f_uv[2])
594                                 new_tri_2.faceuvs= uv_key(f_uv[0]), uv_key(f_uv[2]), uv_key(f_uv[3])
595                         
596                         tri_list.append( new_tri )
597                         tri_list.append( new_tri_2 )
598                 
599         return tri_list
600         
601         
602 def remove_face_uv(verts, tri_list):
603         '''Remove face UV coordinates from a list of triangles.
604                 
605         Since 3ds files only support one pair of uv coordinates for each vertex, face uv coordinates
606         need to be converted to vertex uv coordinates. That means that vertices need to be duplicated when
607         there are multiple uv coordinates per vertex.'''
608         
609         # initialize a list of UniqueLists, one per vertex:
610         #uv_list = [UniqueList() for i in xrange(len(verts))]
611         unique_uvs= [{} for i in range(len(verts))]
612         
613         # for each face uv coordinate, add it to the UniqueList of the vertex
614         for tri in tri_list:
615                 for i in range(3):
616                         # store the index into the UniqueList for future reference:
617                         # offset.append(uv_list[tri.vertex_index[i]].add(_3ds_point_uv(tri.faceuvs[i])))
618                         
619                         context_uv_vert= unique_uvs[tri.vertex_index[i]]
620                         uvkey= tri.faceuvs[i]
621                         
622                         offset_index__uv_3ds = context_uv_vert.get(uvkey)
623                         
624                         if not offset_index__uv_3ds:                            
625                                 offset_index__uv_3ds = context_uv_vert[uvkey] = len(context_uv_vert), _3ds_point_uv(uvkey)
626                         
627                         tri.offset[i] = offset_index__uv_3ds[0]
628                         
629                         
630                 
631         # At this point, each vertex has a UniqueList containing every uv coordinate that is associated with it
632         # only once.
633         
634         # Now we need to duplicate every vertex as many times as it has uv coordinates and make sure the
635         # faces refer to the new face indices:
636         vert_index = 0
637         vert_array = _3ds_array()
638         uv_array = _3ds_array()
639         index_list = []
640         for i,vert in enumerate(verts):
641                 index_list.append(vert_index)
642                 
643                 pt = _3ds_point_3d(vert.co) # reuse, should be ok
644                 uvmap = [None] * len(unique_uvs[i])
645                 for ii, uv_3ds in unique_uvs[i].values():
646                         # add a vertex duplicate to the vertex_array for every uv associated with this vertex:
647                         vert_array.add(pt)
648                         # add the uv coordinate to the uv array:
649                         # This for loop does not give uv's ordered by ii, so we create a new map
650                         # and add the uv's later
651                         # uv_array.add(uv_3ds)
652                         uvmap[ii] = uv_3ds
653                 
654                 # Add the uv's in the correct order
655                 for uv_3ds in uvmap:
656                         # add the uv coordinate to the uv array:
657                         uv_array.add(uv_3ds)
658                 
659                 vert_index += len(unique_uvs[i])
660         
661         # Make sure the triangle vertex indices now refer to the new vertex list:
662         for tri in tri_list:
663                 for i in range(3):
664                         tri.offset[i]+=index_list[tri.vertex_index[i]]
665                 tri.vertex_index= tri.offset
666         
667         return vert_array, uv_array, tri_list
668
669 def make_faces_chunk(tri_list, mesh, materialDict):
670         '''Make a chunk for the faces.
671         
672         Also adds subchunks assigning materials to all faces.'''
673         
674         materials = mesh.materials
675         if not materials:
676                 mat = None
677         
678         face_chunk = _3ds_chunk(OBJECT_FACES)
679         face_list = _3ds_array()
680         
681         
682         if len(mesh.uv_textures):
683 #       if mesh.faceUV:
684                 # Gather materials used in this mesh - mat/image pairs
685                 unique_mats = {}
686                 for i,tri in enumerate(tri_list):
687                         
688                         face_list.add(_3ds_face(tri.vertex_index))
689                         
690                         if materials:
691                                 mat = materials[tri.mat]
692                                 if mat: mat = mat.name
693                         
694                         img = tri.image
695                         
696                         try:
697                                 context_mat_face_array = unique_mats[mat, img][1]
698                         except:
699                                 
700                                 if mat: name_str = mat
701                                 else:   name_str = 'None'
702                                 if img: name_str += img
703                                 
704                                 context_mat_face_array = _3ds_array()
705                                 unique_mats[mat, img] = _3ds_string(sane_name(name_str)), context_mat_face_array
706                                 
707                         
708                         context_mat_face_array.add(_3ds_short(i))
709                         # obj_material_faces[tri.mat].add(_3ds_short(i))
710                 
711                 face_chunk.add_variable("faces", face_list)
712                 for mat_name, mat_faces in unique_mats.values():
713                         obj_material_chunk=_3ds_chunk(OBJECT_MATERIAL)
714                         obj_material_chunk.add_variable("name", mat_name)
715                         obj_material_chunk.add_variable("face_list", mat_faces)
716                         face_chunk.add_subchunk(obj_material_chunk)
717                         
718         else:
719                 
720                 obj_material_faces=[]
721                 obj_material_names=[]
722                 for m in materials:
723                         if m:
724                                 obj_material_names.append(_3ds_string(sane_name(m.name)))
725                                 obj_material_faces.append(_3ds_array())
726                 n_materials = len(obj_material_names)
727                 
728                 for i,tri in enumerate(tri_list):
729                         face_list.add(_3ds_face(tri.vertex_index))
730                         if (tri.mat < n_materials):
731                                 obj_material_faces[tri.mat].add(_3ds_short(i))
732                 
733                 face_chunk.add_variable("faces", face_list)
734                 for i in range(n_materials):
735                         obj_material_chunk=_3ds_chunk(OBJECT_MATERIAL)
736                         obj_material_chunk.add_variable("name", obj_material_names[i])
737                         obj_material_chunk.add_variable("face_list", obj_material_faces[i])
738                         face_chunk.add_subchunk(obj_material_chunk)
739         
740         return face_chunk
741
742 def make_vert_chunk(vert_array):
743         '''Make a vertex chunk out of an array of vertices.'''
744         vert_chunk = _3ds_chunk(OBJECT_VERTICES)
745         vert_chunk.add_variable("vertices",vert_array)
746         return vert_chunk
747
748 def make_uv_chunk(uv_array):
749         '''Make a UV chunk out of an array of UVs.'''
750         uv_chunk = _3ds_chunk(OBJECT_UV)
751         uv_chunk.add_variable("uv coords", uv_array)
752         return uv_chunk
753
754 def make_mesh_chunk(mesh, materialDict):
755         '''Make a chunk out of a Blender mesh.'''
756         
757         # Extract the triangles from the mesh:
758         tri_list = extract_triangles(mesh)
759         
760         if len(mesh.uv_textures):
761 #       if mesh.faceUV:
762                 # Remove the face UVs and convert it to vertex UV:
763                 vert_array, uv_array, tri_list = remove_face_uv(mesh.verts, tri_list)
764         else:
765                 # Add the vertices to the vertex array:
766                 vert_array = _3ds_array()
767                 for vert in mesh.verts:
768                         vert_array.add(_3ds_point_3d(vert.co))
769                 # If the mesh has vertex UVs, create an array of UVs:
770                 if len(mesh.sticky):
771 #               if mesh.vertexUV:
772                         uv_array = _3ds_array()
773                         for uv in mesh.sticky:
774 #                       for vert in mesh.verts:
775                                 uv_array.add(_3ds_point_uv(uv.co))
776 #                               uv_array.add(_3ds_point_uv(vert.uvco))
777                 else:
778                         # no UV at all:
779                         uv_array = None
780
781         # create the chunk:
782         mesh_chunk = _3ds_chunk(OBJECT_MESH)
783         
784         # add vertex chunk:
785         mesh_chunk.add_subchunk(make_vert_chunk(vert_array))
786         # add faces chunk:
787         
788         mesh_chunk.add_subchunk(make_faces_chunk(tri_list, mesh, materialDict))
789         
790         # if available, add uv chunk:
791         if uv_array:
792                 mesh_chunk.add_subchunk(make_uv_chunk(uv_array))
793         
794         return mesh_chunk
795
796 """ # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
797 def make_kfdata(start=0, stop=0, curtime=0):
798         '''Make the basic keyframe data chunk'''
799         kfdata = _3ds_chunk(KFDATA)
800         
801         kfhdr = _3ds_chunk(KFDATA_KFHDR)
802         kfhdr.add_variable("revision", _3ds_short(0))
803         # Not really sure what filename is used for, but it seems it is usually used
804         # to identify the program that generated the .3ds:
805         kfhdr.add_variable("filename", _3ds_string("Blender"))
806         kfhdr.add_variable("animlen", _3ds_int(stop-start))
807         
808         kfseg = _3ds_chunk(KFDATA_KFSEG)
809         kfseg.add_variable("start", _3ds_int(start))
810         kfseg.add_variable("stop", _3ds_int(stop))
811         
812         kfcurtime = _3ds_chunk(KFDATA_KFCURTIME)
813         kfcurtime.add_variable("curtime", _3ds_int(curtime))
814         
815         kfdata.add_subchunk(kfhdr)
816         kfdata.add_subchunk(kfseg)
817         kfdata.add_subchunk(kfcurtime)
818         return kfdata
819 """
820
821 """
822 def make_track_chunk(ID, obj):
823         '''Make a chunk for track data.
824         
825         Depending on the ID, this will construct a position, rotation or scale track.'''
826         track_chunk = _3ds_chunk(ID)
827         track_chunk.add_variable("track_flags", _3ds_short())
828         track_chunk.add_variable("unknown", _3ds_int())
829         track_chunk.add_variable("unknown", _3ds_int())
830         track_chunk.add_variable("nkeys", _3ds_int(1))
831         # Next section should be repeated for every keyframe, but for now, animation is not actually supported.
832         track_chunk.add_variable("tcb_frame", _3ds_int(0))
833         track_chunk.add_variable("tcb_flags", _3ds_short())
834         if obj.type=='Empty':
835                 if ID==POS_TRACK_TAG:
836                         # position vector:
837                         track_chunk.add_variable("position", _3ds_point_3d(obj.getLocation()))
838                 elif ID==ROT_TRACK_TAG:
839                         # rotation (quaternion, angle first, followed by axis):
840                         q = obj.getEuler().toQuat()
841                         track_chunk.add_variable("rotation", _3ds_point_4d((q.angle, q.axis[0], q.axis[1], q.axis[2])))
842                 elif ID==SCL_TRACK_TAG:
843                         # scale vector:
844                         track_chunk.add_variable("scale", _3ds_point_3d(obj.getSize()))
845         else:
846                 # meshes have their transformations applied before 
847                 # exporting, so write identity transforms here:
848                 if ID==POS_TRACK_TAG:
849                         # position vector:
850                         track_chunk.add_variable("position", _3ds_point_3d((0.0,0.0,0.0)))
851                 elif ID==ROT_TRACK_TAG:
852                         # rotation (quaternion, angle first, followed by axis):
853                         track_chunk.add_variable("rotation", _3ds_point_4d((0.0, 1.0, 0.0, 0.0)))
854                 elif ID==SCL_TRACK_TAG:
855                         # scale vector:
856                         track_chunk.add_variable("scale", _3ds_point_3d((1.0, 1.0, 1.0)))
857         
858         return track_chunk
859 """
860
861 """
862 def make_kf_obj_node(obj, name_to_id):
863         '''Make a node chunk for a Blender object.
864         
865         Takes the Blender object as a parameter. Object id's are taken from the dictionary name_to_id.
866         Blender Empty objects are converted to dummy nodes.'''
867         
868         name = obj.name
869         # main object node chunk:
870         kf_obj_node = _3ds_chunk(KFDATA_OBJECT_NODE_TAG)
871         # chunk for the object id: 
872         obj_id_chunk = _3ds_chunk(OBJECT_NODE_ID)
873         # object id is from the name_to_id dictionary:
874         obj_id_chunk.add_variable("node_id", _3ds_short(name_to_id[name]))
875         
876         # object node header:
877         obj_node_header_chunk = _3ds_chunk(OBJECT_NODE_HDR)
878         # object name:
879         if obj.type == 'Empty':
880                 # Empties are called "$$$DUMMY" and use the OBJECT_INSTANCE_NAME chunk 
881                 # for their name (see below):
882                 obj_node_header_chunk.add_variable("name", _3ds_string("$$$DUMMY"))
883         else:
884                 # Add the name:
885                 obj_node_header_chunk.add_variable("name", _3ds_string(sane_name(name)))
886         # Add Flag variables (not sure what they do):
887         obj_node_header_chunk.add_variable("flags1", _3ds_short(0))
888         obj_node_header_chunk.add_variable("flags2", _3ds_short(0))
889         
890         # Check parent-child relationships:
891         parent = obj.parent
892         if (parent == None) or (parent.name not in name_to_id):
893                 # If no parent, or the parents name is not in the name_to_id dictionary,
894                 # parent id becomes -1:
895                 obj_node_header_chunk.add_variable("parent", _3ds_short(-1))
896         else:
897                 # Get the parent's id from the name_to_id dictionary:
898                 obj_node_header_chunk.add_variable("parent", _3ds_short(name_to_id[parent.name]))
899         
900         # Add pivot chunk:
901         obj_pivot_chunk = _3ds_chunk(OBJECT_PIVOT)
902         obj_pivot_chunk.add_variable("pivot", _3ds_point_3d(obj.getLocation()))
903         kf_obj_node.add_subchunk(obj_pivot_chunk)
904         
905         # add subchunks for object id and node header:
906         kf_obj_node.add_subchunk(obj_id_chunk)
907         kf_obj_node.add_subchunk(obj_node_header_chunk)
908
909         # Empty objects need to have an extra chunk for the instance name:
910         if obj.type == 'Empty':
911                 obj_instance_name_chunk = _3ds_chunk(OBJECT_INSTANCE_NAME)
912                 obj_instance_name_chunk.add_variable("name", _3ds_string(sane_name(name)))
913                 kf_obj_node.add_subchunk(obj_instance_name_chunk)
914         
915         # Add track chunks for position, rotation and scale:
916         kf_obj_node.add_subchunk(make_track_chunk(POS_TRACK_TAG, obj))
917         kf_obj_node.add_subchunk(make_track_chunk(ROT_TRACK_TAG, obj))
918         kf_obj_node.add_subchunk(make_track_chunk(SCL_TRACK_TAG, obj))
919
920         return kf_obj_node
921 """
922
923 # import BPyMessages
924 def save_3ds(filename, context):
925         '''Save the Blender scene to a 3ds file.'''
926         # Time the export
927         
928         if not filename.lower().endswith('.3ds'):
929                 filename += '.3ds'
930         
931         # XXX
932 #       if not BPyMessages.Warning_SaveOver(filename):
933 #               return
934         
935         # XXX
936         time1 = time.clock()
937 #       time1= Blender.sys.time()
938 #       Blender.Window.WaitCursor(1)
939
940         sce = context.scene
941 #       sce= bpy.data.scenes.active
942         
943         # Initialize the main chunk (primary):
944         primary = _3ds_chunk(PRIMARY)
945         # Add version chunk:
946         version_chunk = _3ds_chunk(VERSION)
947         version_chunk.add_variable("version", _3ds_int(3))
948         primary.add_subchunk(version_chunk)
949         
950         # init main object info chunk:
951         object_info = _3ds_chunk(OBJECTINFO)
952         
953         ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
954         # init main key frame data chunk:
955         kfdata = make_kfdata()
956         '''
957         
958         # Get all the supported objects selected in this scene:
959         # ob_sel= list(sce.objects.context)
960         # mesh_objects = [ (ob, me) for ob in ob_sel   for me in (BPyMesh.getMeshFromObject(ob, None, True, False, sce),) if me ]
961         # empty_objects = [ ob for ob in ob_sel if ob.type == 'Empty' ]
962         
963         # Make a list of all materials used in the selected meshes (use a dictionary,
964         # each material is added once):
965         materialDict = {}
966         mesh_objects = []
967         for ob in [ob for ob in context.scene.objects if ob.is_visible()]:
968 #       for ob in sce.objects.context:
969
970                 # get derived objects
971                 free, derived = create_derived_objects(ob)
972
973                 if derived == None: continue
974
975                 for ob_derived, mat in derived:
976 #               for ob_derived, mat in getDerivedObjects(ob, False):
977
978                         if ob.type not in ('MESH', 'CURVE', 'SURFACE', 'TEXT', 'META'):
979                                 continue
980
981                         data = ob_derived.create_mesh(True, 'PREVIEW')
982 #                       data = getMeshFromObject(ob_derived, None, True, False, sce)
983                         if data:
984                                 data.transform(mat)
985 #                               data.transform(mat, recalc_normals=False)
986                                 mesh_objects.append((ob_derived, data))
987                                 mat_ls = data.materials
988                                 mat_ls_len = len(mat_ls)
989
990                                 # get material/image tuples.
991                                 if len(data.uv_textures):
992 #                               if data.faceUV:
993                                         if not mat_ls:
994                                                 mat = mat_name = None
995                                         
996                                         for f, uf in zip(data.faces, data.active_uv_texture.data):
997                                                 if mat_ls:
998                                                         mat_index = f.material_index
999 #                                                       mat_index = f.mat
1000                                                         if mat_index >= mat_ls_len:
1001                                                                 mat_index = f.mat = 0
1002                                                         mat = mat_ls[mat_index]
1003                                                         if mat: mat_name = mat.name
1004                                                         else:   mat_name = None
1005                                                 # else there alredy set to none
1006                                                         
1007                                                 img = uf.image
1008 #                                               img = f.image
1009                                                 if img: img_name = img.name
1010                                                 else:   img_name = None
1011                                                 
1012                                                 materialDict.setdefault((mat_name, img_name), (mat, img) )
1013                                                 
1014                                         
1015                                 else:
1016                                         for mat in mat_ls:
1017                                                 if mat: # material may be None so check its not.
1018                                                         materialDict.setdefault((mat.name, None), (mat, None) )
1019                                         
1020                                         # Why 0 Why!
1021                                         for f in data.faces:
1022                                                 if f.material_index >= mat_ls_len:
1023 #                                               if f.mat >= mat_ls_len:
1024                                                         f.material_index = 0
1025                                                         # f.mat = 0
1026
1027                 if free:
1028                         free_derived_objects(ob)
1029
1030         
1031         # Make material chunks for all materials used in the meshes:
1032         for mat_and_image in materialDict.values():
1033                 object_info.add_subchunk(make_material_chunk(mat_and_image[0], mat_and_image[1]))
1034         
1035         # Give all objects a unique ID and build a dictionary from object name to object id:
1036         """
1037         name_to_id = {}
1038         for ob, data in mesh_objects:
1039                 name_to_id[ob.name]= len(name_to_id)
1040         #for ob in empty_objects:
1041         #       name_to_id[ob.name]= len(name_to_id)
1042         """
1043         
1044         # Create object chunks for all meshes:
1045         i = 0
1046         for ob, blender_mesh in mesh_objects:
1047                 # create a new object chunk
1048                 object_chunk = _3ds_chunk(OBJECT)
1049                 
1050                 # set the object name
1051                 object_chunk.add_variable("name", _3ds_string(sane_name(ob.name)))
1052                 
1053                 # make a mesh chunk out of the mesh:
1054                 object_chunk.add_subchunk(make_mesh_chunk(blender_mesh, materialDict))
1055                 object_info.add_subchunk(object_chunk)
1056                 
1057                 ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
1058                 # make a kf object node for the object:
1059                 kfdata.add_subchunk(make_kf_obj_node(ob, name_to_id))
1060                 '''
1061 #               if not blender_mesh.users:
1062                 bpy.data.remove_mesh(blender_mesh)
1063 #               blender_mesh.verts = None
1064
1065                 i+=i
1066
1067         # Create chunks for all empties:
1068         ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
1069         for ob in empty_objects:
1070                 # Empties only require a kf object node:
1071                 kfdata.add_subchunk(make_kf_obj_node(ob, name_to_id))
1072                 pass
1073         '''
1074         
1075         # Add main object info chunk to primary chunk:
1076         primary.add_subchunk(object_info)
1077         
1078         ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
1079         # Add main keyframe data chunk to primary chunk:
1080         primary.add_subchunk(kfdata)
1081         '''
1082         
1083         # At this point, the chunk hierarchy is completely built.
1084         
1085         # Check the size:
1086         primary.get_size()
1087         # Open the file for writing:
1088         file = open( filename, 'wb' )
1089         
1090         # Recursively write the chunks to file:
1091         primary.write(file)
1092         
1093         # Close the file:
1094         file.close()
1095         
1096         # Debugging only: report the exporting time:
1097 #       Blender.Window.WaitCursor(0)
1098         print("3ds export time: %.2f" % (time.clock() - time1))
1099 #       print("3ds export time: %.2f" % (Blender.sys.time() - time1))
1100         
1101         # Debugging only: dump the chunk hierarchy:
1102         #primary.dump()
1103
1104
1105 # if __name__=='__main__':
1106 #     if struct:
1107 #         Blender.Window.FileSelector(save_3ds, "Export 3DS", Blender.sys.makename(ext='.3ds'))
1108 #     else:
1109 #         Blender.Draw.PupMenu("Error%t|This script requires a full python installation")
1110 # # save_3ds('/test_b.3ds')
1111 from bpy.props import *
1112 class Export3DS(bpy.types.Operator):
1113         '''Export to 3DS file format (.3ds).'''
1114         bl_idname = "export.autodesk_3ds"
1115         bl_label = 'Export 3DS'
1116         
1117         # List of operator properties, the attributes will be assigned
1118         # to the class instance from the operator settings before calling.
1119
1120         
1121         # filename = StringProperty(name="File Name", description="File name used for exporting the 3DS file", maxlen= 1024, default= ""),
1122         path = StringProperty(name="File Path", description="File path used for exporting the 3DS file", maxlen= 1024, default= "")
1123
1124         
1125         def execute(self, context):
1126                 save_3ds(self.properties.path, context)
1127                 return ('FINISHED',)
1128         
1129         def invoke(self, context, event):
1130                 wm = context.manager
1131                 wm.add_fileselect(self)
1132                 return ('RUNNING_MODAL',)
1133         
1134         def poll(self, context): # Poll isnt working yet
1135                 print("Poll")
1136                 return context.active_object != None
1137
1138 bpy.ops.add(Export3DS)
1139
1140 # Add to a menu
1141 import dynamic_menu
1142
1143 def menu_func(self, context):
1144     default_path = bpy.data.filename.replace(".blend", ".3ds")
1145     self.layout.item_stringO(Export3DS.bl_idname, "path", default_path, text="Autodesk 3DS...")
1146
1147 menu_item = dynamic_menu.add(bpy.types.INFO_MT_file_export, menu_func)