Merge remote-tracking branch 'refs/remotes/elrond/sql/media_data'
[mediagoblin.git] / mediagoblin / media_types / stl / model_loader.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (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 Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17
18 import struct
19
20
21 class ThreeDeeParseError(Exception):
22 pass
23
24
25 class ThreeDee():
26 """
27 3D model parser base class. Derrived classes are used for basic
28 analysis of 3D models, and are not intended to be used for 3D
29 rendering.
30 """
31
32 def __init__(self, fileob):
33 self.verts = []
34 self.average = [0, 0, 0]
35 self.min = [None, None, None]
36 self.max = [None, None, None]
37 self.width = 0 # x axis
38 self.depth = 0 # y axis
39 self.height = 0 # z axis
40
41 self.load(fileob)
42 if not len(self.verts):
43 raise ThreeDeeParseError("Empty model.")
44
45 for vector in self.verts:
46 for i in range(3):
47 num = vector[i]
48 self.average[i] += num
49 if not self.min[i]:
50 self.min[i] = num
51 self.max[i] = num
52 else:
53 if self.min[i] > num:
54 self.min[i] = num
55 if self.max[i] < num:
56 self.max[i] = num
57
58 for i in range(3):
59 self.average[i]/=len(self.verts)
60
61 self.width = abs(self.min[0] - self.max[0])
62 self.depth = abs(self.min[1] - self.max[1])
63 self.height = abs(self.min[2] - self.max[2])
64
65
66 def load(self, fileob):
67 """Override this method in your subclass."""
68 pass
69
70
71 class ObjModel(ThreeDee):
72 """
73 Parser for textureless wavefront obj files. File format
74 reference: http://en.wikipedia.org/wiki/Wavefront_.obj_file
75 """
76
77 def __vector(self, line, expected=3):
78 nums = map(float, line.strip().split(" ")[1:])
79 return tuple(nums[:expected])
80
81 def load(self, fileob):
82 for line in fileob:
83 if line[0] == "v":
84 self.verts.append(self.__vector(line))
85
86
87 class BinaryStlModel(ThreeDee):
88 """
89 Parser for ascii-encoded stl files. File format reference:
90 http://en.wikipedia.org/wiki/STL_%28file_format%29#Binary_STL
91 """
92
93 def load(self, fileob):
94 fileob.seek(80) # skip the header
95 count = struct.unpack("<I", fileob.read(4))[0]
96 for i in range(count):
97 fileob.read(12) # skip the normal vector
98 for v in range(3):
99 self.verts.append(struct.unpack("<3f", fileob.read(12)))
100 fileob.read(2) # skip the attribute bytes
101
102
103 def auto_detect(fileob, hint):
104 """
105 Attempt to divine which parser to use to divine information about
106 the model / verify the file."""
107
108 if hint == "obj" or not hint:
109 try:
110 return ObjModel(fileob)
111 except ThreeDeeParseError:
112 pass
113
114 if hint == "stl" or not hint:
115 try:
116 # HACK Ascii formatted stls are similar enough to obj
117 # files that we can just use the same parser for both.
118 # Isn't that something?
119 return ObjModel(fileob)
120 except ThreeDeeParseError:
121 pass
122 except ValueError:
123 pass
124 try:
125 # It is pretty important that the binary stl model loader
126 # is tried second, because its possible for it to parse
127 # total garbage from plaintext =)
128 return BinaryStlModel(fileob)
129 except ThreeDeeParseError:
130 pass
131 except MemoryError:
132 pass
133
134 raise ThreeDeeParseError("Could not successfully parse the model :(")