Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'io_scene_x3d/import_x3d.py')
-rw-r--r--io_scene_x3d/import_x3d.py2655
1 files changed, 2655 insertions, 0 deletions
diff --git a/io_scene_x3d/import_x3d.py b/io_scene_x3d/import_x3d.py
new file mode 100644
index 00000000..1ee0e9f5
--- /dev/null
+++ b/io_scene_x3d/import_x3d.py
@@ -0,0 +1,2655 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <pep8 compliant>
+
+DEBUG = False
+
+# This should work without a blender at all
+import os
+
+
+def imageConvertCompat(path):
+
+ if os.sep == '\\':
+ return path # assime win32 has quicktime, dont convert
+
+ if path.lower().endswith('.gif'):
+ path_to = path[:-3] + 'png'
+
+ '''
+ if exists(path_to):
+ return path_to
+ '''
+ # print('\n'+path+'\n'+path_to+'\n')
+ os.system('convert "%s" "%s"' % (path, path_to)) # for now just hope we have image magick
+
+ if os.path.exists(path_to):
+ return path_to
+
+ return path
+
+# notes
+# transform are relative
+# order dosnt matter for loc/size/rot
+# right handed rotation
+# angles are in radians
+# rotation first defines axis then ammount in radians
+
+
+# =============================== VRML Spesific
+
+def vrmlFormat(data):
+ '''
+ Keep this as a valid vrml file, but format in a way we can predict.
+ '''
+ # Strip all commends - # not in strings - warning multiline strings are ignored.
+ def strip_comment(l):
+ #l = ' '.join(l.split())
+ l = l.strip()
+
+ if l.startswith('#'):
+ return ''
+
+ i = l.find('#')
+
+ if i == -1:
+ return l
+
+ # Most cases accounted for! if we have a comment at the end of the line do this...
+ #j = l.find('url "')
+ j = l.find('"')
+
+ if j == -1: # simple no strings
+ return l[:i].strip()
+
+ q = False
+ for i, c in enumerate(l):
+ if c == '"':
+ q = not q # invert
+
+ elif c == '#':
+ if q == False:
+ return l[:i - 1]
+
+ return l
+
+ data = '\n'.join([strip_comment(l) for l in data.split('\n')]) # remove all whitespace
+
+ EXTRACT_STRINGS = True # only needed when strings or filesnames containe ,[]{} chars :/
+
+ if EXTRACT_STRINGS:
+
+ # We need this so we can detect URL's
+ data = '\n'.join([' '.join(l.split()) for l in data.split('\n')]) # remove all whitespace
+
+ string_ls = []
+
+ #search = 'url "'
+ search = '"'
+
+ ok = True
+ last_i = 0
+ while ok:
+ ok = False
+ i = data.find(search, last_i)
+ if i != -1:
+
+ start = i + len(search) # first char after end of search
+ end = data.find('"', start)
+ if end != -1:
+ item = data[start:end]
+ string_ls.append(item)
+ data = data[:start] + data[end:]
+ ok = True # keep looking
+
+ last_i = (end - len(item)) + 1
+ # print(last_i, item, '|' + data[last_i] + '|')
+
+ # done with messy extracting strings part
+
+ # Bad, dont take strings into account
+ '''
+ data = data.replace('#', '\n#')
+ data = '\n'.join([ll for l in data.split('\n') for ll in (l.strip(),) if not ll.startswith('#')]) # remove all whitespace
+ '''
+ data = data.replace('{', '\n{\n')
+ data = data.replace('}', '\n}\n')
+ data = data.replace('[', '\n[\n')
+ data = data.replace(']', '\n]\n')
+ data = data.replace(',', ' , ') # make sure comma's separate
+
+ if EXTRACT_STRINGS:
+ # add strings back in
+
+ search = '"' # fill in these empty strings
+
+ ok = True
+ last_i = 0
+ while ok:
+ ok = False
+ i = data.find(search + '"', last_i)
+ # print(i)
+ if i != -1:
+ start = i + len(search) # first char after end of search
+ item = string_ls.pop(0)
+ # print(item)
+ data = data[:start] + item + data[start:]
+
+ last_i = start + len(item) + 1
+
+ ok = True
+
+ # More annoying obscure cases where USE or DEF are placed on a newline
+ # data = data.replace('\nDEF ', ' DEF ')
+ # data = data.replace('\nUSE ', ' USE ')
+
+ data = '\n'.join([' '.join(l.split()) for l in data.split('\n')]) # remove all whitespace
+
+ # Better to parse the file accounting for multiline arrays
+ '''
+ data = data.replace(',\n', ' , ') # remove line endings with commas
+ data = data.replace(']', '\n]\n') # very very annoying - but some comma's are at the end of the list, must run this again.
+ '''
+
+ return [l for l in data.split('\n') if l]
+
+NODE_NORMAL = 1 # {}
+NODE_ARRAY = 2 # []
+NODE_REFERENCE = 3 # USE foobar
+# NODE_PROTO = 4 #
+
+lines = []
+
+
+def getNodePreText(i, words):
+ # print(lines[i])
+ use_node = False
+ while len(words) < 5:
+
+ if i >= len(lines):
+ break
+ '''
+ elif lines[i].startswith('PROTO'):
+ return NODE_PROTO, i+1
+ '''
+ elif lines[i] == '{':
+ # words.append(lines[i]) # no need
+ # print("OK")
+ return NODE_NORMAL, i + 1
+ elif lines[i].count('"') % 2 != 0: # odd number of quotes? - part of a string.
+ # print('ISSTRING')
+ break
+ else:
+ new_words = lines[i].split()
+ if 'USE' in new_words:
+ use_node = True
+
+ words.extend(new_words)
+ i += 1
+
+ # Check for USE node - no {
+ # USE #id - should always be on the same line.
+ if use_node:
+ # print('LINE', i, words[:words.index('USE')+2])
+ words[:] = words[:words.index('USE') + 2]
+ if lines[i] == '{' and lines[i + 1] == '}':
+ # USE sometimes has {} after it anyway
+ i += 2
+ return NODE_REFERENCE, i
+
+ # print("error value!!!", words)
+ return 0, -1
+
+
+def is_nodeline(i, words):
+
+ if not lines[i][0].isalpha():
+ return 0, 0
+
+ #if lines[i].startswith('field'):
+ # return 0, 0
+
+ # Is this a prototype??
+ if lines[i].startswith('PROTO'):
+ words[:] = lines[i].split()
+ return NODE_NORMAL, i + 1 # TODO - assumes the next line is a '[\n', skip that
+ if lines[i].startswith('EXTERNPROTO'):
+ words[:] = lines[i].split()
+ return NODE_ARRAY, i + 1 # TODO - assumes the next line is a '[\n', skip that
+
+ '''
+ proto_type, new_i = is_protoline(i, words, proto_field_defs)
+ if new_i != -1:
+ return proto_type, new_i
+ '''
+
+ # Simple "var [" type
+ if lines[i + 1] == '[':
+ if lines[i].count('"') % 2 == 0:
+ words[:] = lines[i].split()
+ return NODE_ARRAY, i + 2
+
+ node_type, new_i = getNodePreText(i, words)
+
+ if not node_type:
+ if DEBUG:
+ print("not node_type", lines[i])
+ return 0, 0
+
+ # Ok, we have a { after some values
+ # Check the values are not fields
+ for i, val in enumerate(words):
+ if i != 0 and words[i - 1] in {'DEF', 'USE'}:
+ # ignore anything after DEF, it is a ID and can contain any chars.
+ pass
+ elif val[0].isalpha() and val not in {'TRUE', 'FALSE'}:
+ pass
+ else:
+ # There is a number in one of the values, therefor we are not a node.
+ return 0, 0
+
+ #if node_type==NODE_REFERENCE:
+ # print(words, "REF_!!!!!!!")
+ return node_type, new_i
+
+
+def is_numline(i):
+ '''
+ Does this line start with a number?
+ '''
+
+ # Works but too slow.
+ '''
+ l = lines[i]
+ for w in l.split():
+ if w==',':
+ pass
+ else:
+ try:
+ float(w)
+ return True
+
+ except:
+ return False
+
+ return False
+ '''
+
+ l = lines[i]
+
+ line_start = 0
+
+ if l.startswith(', '):
+ line_start += 2
+
+ line_end = len(l) - 1
+ line_end_new = l.find(' ', line_start) # comma's always have a space before them
+
+ if line_end_new != -1:
+ line_end = line_end_new
+
+ try:
+ float(l[line_start:line_end]) # works for a float or int
+ return True
+ except:
+ return False
+
+
+class vrmlNode(object):
+ __slots__ = ('id',
+ 'fields',
+ 'proto_node',
+ 'proto_field_defs',
+ 'proto_fields',
+ 'node_type',
+ 'parent',
+ 'children',
+ 'parent',
+ 'array_data',
+ 'reference',
+ 'lineno',
+ 'filename',
+ 'blendObject',
+ 'DEF_NAMESPACE',
+ 'ROUTE_IPO_NAMESPACE',
+ 'PROTO_NAMESPACE',
+ 'x3dNode')
+
+ def __init__(self, parent, node_type, lineno):
+ self.id = None
+ self.node_type = node_type
+ self.parent = parent
+ self.blendObject = None
+ self.x3dNode = None # for x3d import only
+ if parent:
+ parent.children.append(self)
+
+ self.lineno = lineno
+
+ # This is only set from the root nodes.
+ # Having a filename also denotes a root node
+ self.filename = None
+ self.proto_node = None # proto field definition eg: "field SFColor seatColor .6 .6 .1"
+
+ # Store in the root node because each inline file needs its own root node and its own namespace
+ self.DEF_NAMESPACE = None
+ self.ROUTE_IPO_NAMESPACE = None
+ '''
+ self.FIELD_NAMESPACE = None
+ '''
+
+ self.PROTO_NAMESPACE = None
+
+ self.reference = None
+
+ if node_type == NODE_REFERENCE:
+ # For references, only the parent and ID are needed
+ # the reference its self is assigned on parsing
+ return
+
+ self.fields = [] # fields have no order, in some cases rool level values are not unique so dont use a dict
+
+ self.proto_field_defs = [] # proto field definition eg: "field SFColor seatColor .6 .6 .1"
+ self.proto_fields = [] # proto field usage "diffuseColor IS seatColor"
+ self.children = []
+ self.array_data = [] # use for arrays of data - should only be for NODE_ARRAY types
+
+ # Only available from the root node
+ '''
+ def getFieldDict(self):
+ if self.FIELD_NAMESPACE != None:
+ return self.FIELD_NAMESPACE
+ else:
+ return self.parent.getFieldDict()
+ '''
+ def getProtoDict(self):
+ if self.PROTO_NAMESPACE != None:
+ return self.PROTO_NAMESPACE
+ else:
+ return self.parent.getProtoDict()
+
+ def getDefDict(self):
+ if self.DEF_NAMESPACE != None:
+ return self.DEF_NAMESPACE
+ else:
+ return self.parent.getDefDict()
+
+ def getRouteIpoDict(self):
+ if self.ROUTE_IPO_NAMESPACE != None:
+ return self.ROUTE_IPO_NAMESPACE
+ else:
+ return self.parent.getRouteIpoDict()
+
+ def setRoot(self, filename):
+ self.filename = filename
+ # self.FIELD_NAMESPACE = {}
+ self.DEF_NAMESPACE = {}
+ self.ROUTE_IPO_NAMESPACE = {}
+ self.PROTO_NAMESPACE = {}
+
+ def isRoot(self):
+ if self.filename is None:
+ return False
+ else:
+ return True
+
+ def getFilename(self):
+ if self.filename:
+ return self.filename
+ elif self.parent:
+ return self.parent.getFilename()
+ else:
+ return None
+
+ def getRealNode(self):
+ if self.reference:
+ return self.reference
+ else:
+ return self
+
+ def getSpec(self):
+ self_real = self.getRealNode()
+ try:
+ return self_real.id[-1] # its possible this node has no spec
+ except:
+ return None
+
+ def findSpecRecursive(self, spec):
+ self_real = self.getRealNode()
+ if spec == self_real.getSpec():
+ return self
+
+ for child in self_real.children:
+ if child.findSpecRecursive(spec):
+ return child
+
+ return None
+
+ def getPrefix(self):
+ if self.id:
+ return self.id[0]
+ return None
+
+ def getSpecialTypeName(self, typename):
+ self_real = self.getRealNode()
+ try:
+ return self_real.id[list(self_real.id).index(typename) + 1]
+ except:
+ return None
+
+ def getDefName(self):
+ return self.getSpecialTypeName('DEF')
+
+ def getProtoName(self):
+ return self.getSpecialTypeName('PROTO')
+
+ def getExternprotoName(self):
+ return self.getSpecialTypeName('EXTERNPROTO')
+
+ def getChildrenBySpec(self, node_spec): # spec could be Transform, Shape, Appearance
+ self_real = self.getRealNode()
+ # using getSpec functions allows us to use the spec of USE children that dont have their spec in their ID
+ if type(node_spec) == str:
+ return [child for child in self_real.children if child.getSpec() == node_spec]
+ else:
+ # Check inside a list of optional types
+ return [child for child in self_real.children if child.getSpec() in node_spec]
+
+ def getChildBySpec(self, node_spec): # spec could be Transform, Shape, Appearance
+ # Use in cases where there is only ever 1 child of this type
+ ls = self.getChildrenBySpec(node_spec)
+ if ls:
+ return ls[0]
+ else:
+ return None
+
+ def getChildrenByName(self, node_name): # type could be geometry, children, appearance
+ self_real = self.getRealNode()
+ return [child for child in self_real.children if child.id if child.id[0] == node_name]
+
+ def getChildByName(self, node_name):
+ self_real = self.getRealNode()
+ for child in self_real.children:
+ if child.id and child.id[0] == node_name: # and child.id[-1]==node_spec:
+ return child
+
+ def getSerialized(self, results, ancestry):
+ ''' Return this node and all its children in a flat list '''
+ ancestry = ancestry[:] # always use a copy
+
+ # self_real = self.getRealNode()
+
+ results.append((self, tuple(ancestry)))
+ ancestry.append(self)
+ for child in self.getRealNode().children:
+ if child not in ancestry:
+ # We dont want to load proto's, they are only references
+ # We could enforce this elsewhere
+
+ # Only add this in a very special case
+ # where the parent of this object is not the real parent
+ # - In this case we have added the proto as a child to a node instancing it.
+ # This is a bit arbitary, but its how Proto's are done with this importer.
+ if child.getProtoName() is None and child.getExternprotoName() is None:
+ child.getSerialized(results, ancestry)
+ else:
+
+ if DEBUG:
+ print('getSerialized() is proto:', child.getProtoName(), child.getExternprotoName(), self.getSpec())
+
+ self_spec = self.getSpec()
+
+ if child.getProtoName() == self_spec or child.getExternprotoName() == self_spec:
+ #if DEBUG:
+ # "FoundProto!"
+ child.getSerialized(results, ancestry)
+
+ return results
+
+ def searchNodeTypeID(self, node_spec, results):
+ self_real = self.getRealNode()
+ # print(self.lineno, self.id)
+ if self_real.id and self_real.id[-1] == node_spec: # use last element, could also be only element
+ results.append(self_real)
+ for child in self_real.children:
+ child.searchNodeTypeID(node_spec, results)
+ return results
+
+ def getFieldName(self, field, ancestry, AS_CHILD=False):
+ self_real = self.getRealNode() # incase we're an instance
+
+ for f in self_real.fields:
+ # print(f)
+ if f and f[0] == field:
+ # print('\tfound field', f)
+
+ if len(f) >= 3 and f[1] == 'IS': # eg: 'diffuseColor IS legColor'
+ field_id = f[2]
+
+ # print("\n\n\n\n\n\nFOND IS!!!")
+ f_proto_lookup = None
+ f_proto_child_lookup = None
+ i = len(ancestry)
+ while i:
+ i -= 1
+ node = ancestry[i]
+ node = node.getRealNode()
+
+ # proto settings are stored in "self.proto_node"
+ if node.proto_node:
+ # Get the default value from the proto, this can be overwridden by the proto instace
+ # 'field SFColor legColor .8 .4 .7'
+ if AS_CHILD:
+ for child in node.proto_node.children:
+ #if child.id and len(child.id) >= 3 and child.id[2]==field_id:
+ if child.id and ('point' in child.id or 'points' in child.id):
+ f_proto_child_lookup = child
+
+ else:
+ for f_def in node.proto_node.proto_field_defs:
+ if len(f_def) >= 4:
+ if f_def[0] == 'field' and f_def[2] == field_id:
+ f_proto_lookup = f_def[3:]
+
+ # Node instance, Will be 1 up from the proto-node in the ancestry list. but NOT its parent.
+ # This is the setting as defined by the instance, including this setting is optional,
+ # and will override the default PROTO value
+ # eg: 'legColor 1 0 0'
+ if AS_CHILD:
+ for child in node.children:
+ if child.id and child.id[0] == field_id:
+ f_proto_child_lookup = child
+ else:
+ for f_def in node.fields:
+ if len(f_def) >= 2:
+ if f_def[0] == field_id:
+ if DEBUG:
+ print("getFieldName(), found proto", f_def)
+ f_proto_lookup = f_def[1:]
+
+ if AS_CHILD:
+ if f_proto_child_lookup:
+ if DEBUG:
+ print("getFieldName() - AS_CHILD=True, child found")
+ print(f_proto_child_lookup)
+ return f_proto_child_lookup
+ else:
+ return f_proto_lookup
+ else:
+ if AS_CHILD:
+ return None
+ else:
+ # Not using a proto
+ return f[1:]
+ # print('\tfield not found', field)
+
+ # See if this is a proto name
+ if AS_CHILD:
+ for child in self_real.children:
+ if child.id and len(child.id) == 1 and child.id[0] == field:
+ return child
+
+ return None
+
+ def getFieldAsInt(self, field, default, ancestry):
+ self_real = self.getRealNode() # incase we're an instance
+
+ f = self_real.getFieldName(field, ancestry)
+ if f is None:
+ return default
+ if ',' in f:
+ f = f[:f.index(',')] # strip after the comma
+
+ if len(f) != 1:
+ print('\t"%s" wrong length for int conversion for field "%s"' % (f, field))
+ return default
+
+ try:
+ return int(f[0])
+ except:
+ print('\tvalue "%s" could not be used as an int for field "%s"' % (f[0], field))
+ return default
+
+ def getFieldAsFloat(self, field, default, ancestry):
+ self_real = self.getRealNode() # incase we're an instance
+
+ f = self_real.getFieldName(field, ancestry)
+ if f is None:
+ return default
+ if ',' in f:
+ f = f[:f.index(',')] # strip after the comma
+
+ if len(f) != 1:
+ print('\t"%s" wrong length for float conversion for field "%s"' % (f, field))
+ return default
+
+ try:
+ return float(f[0])
+ except:
+ print('\tvalue "%s" could not be used as a float for field "%s"' % (f[0], field))
+ return default
+
+ def getFieldAsFloatTuple(self, field, default, ancestry):
+ self_real = self.getRealNode() # incase we're an instance
+
+ f = self_real.getFieldName(field, ancestry)
+ if f is None:
+ return default
+ # if ',' in f: f = f[:f.index(',')] # strip after the comma
+
+ if len(f) < 1:
+ print('"%s" wrong length for float tuple conversion for field "%s"' % (f, field))
+ return default
+
+ ret = []
+ for v in f:
+ if v != ',':
+ try:
+ ret.append(float(v))
+ except:
+ break # quit of first non float, perhaps its a new field name on the same line? - if so we are going to ignore it :/ TODO
+ # print(ret)
+
+ if ret:
+ return ret
+ if not ret:
+ print('\tvalue "%s" could not be used as a float tuple for field "%s"' % (f, field))
+ return default
+
+ def getFieldAsBool(self, field, default, ancestry):
+ self_real = self.getRealNode() # incase we're an instance
+
+ f = self_real.getFieldName(field, ancestry)
+ if f is None:
+ return default
+ if ',' in f:
+ f = f[:f.index(',')] # strip after the comma
+
+ if len(f) != 1:
+ print('\t"%s" wrong length for bool conversion for field "%s"' % (f, field))
+ return default
+
+ if f[0].upper() == '"TRUE"' or f[0].upper() == 'TRUE':
+ return True
+ elif f[0].upper() == '"FALSE"' or f[0].upper() == 'FALSE':
+ return False
+ else:
+ print('\t"%s" could not be used as a bool for field "%s"' % (f[1], field))
+ return default
+
+ def getFieldAsString(self, field, default, ancestry):
+ self_real = self.getRealNode() # incase we're an instance
+
+ f = self_real.getFieldName(field, ancestry)
+ if f is None:
+ return default
+ if len(f) < 1:
+ print('\t"%s" wrong length for string conversion for field "%s"' % (f, field))
+ return default
+
+ if len(f) > 1:
+ # String may contain spaces
+ st = ' '.join(f)
+ else:
+ st = f[0]
+
+ # X3D HACK
+ if self.x3dNode:
+ return st
+
+ if st[0] == '"' and st[-1] == '"':
+ return st[1:-1]
+ else:
+ print('\tvalue "%s" could not be used as a string for field "%s"' % (f[0], field))
+ return default
+
+ def getFieldAsArray(self, field, group, ancestry):
+ '''
+ For this parser arrays are children
+ '''
+
+ def array_as_number(array_string):
+ array_data = []
+ try:
+ array_data = [int(val) for val in array_string]
+ except:
+ try:
+ array_data = [float(val) for val in array_string]
+ except:
+ print('\tWarning, could not parse array data from field')
+
+ return array_data
+
+ self_real = self.getRealNode() # incase we're an instance
+
+ child_array = self_real.getFieldName(field, ancestry, True)
+
+ #if type(child_array)==list: # happens occasionaly
+ # array_data = child_array
+
+ if child_array is None:
+ # For x3d, should work ok with vrml too
+ # for x3d arrays are fields, vrml they are nodes, annoying but not tooo bad.
+ data_split = self.getFieldName(field, ancestry)
+ if not data_split:
+ return []
+ array_data = ' '.join(data_split)
+ if array_data is None:
+ return []
+
+ array_data = array_data.replace(',', ' ')
+ data_split = array_data.split()
+
+ array_data = array_as_number(data_split)
+
+ elif type(child_array) == list:
+ # x3d creates these
+ data_split = [w.strip(",") for w in child_array]
+
+ array_data = array_as_number(data_split)
+ else:
+ # print(child_array)
+ # Normal vrml
+ array_data = child_array.array_data
+
+ # print('array_data', array_data)
+ if group == -1 or len(array_data) == 0:
+ return array_data
+
+ # We want a flat list
+ flat = True
+ for item in array_data:
+ if type(item) == list:
+ flat = False
+ break
+
+ # make a flat array
+ if flat:
+ flat_array = array_data # we are alredy flat.
+ else:
+ flat_array = []
+
+ def extend_flat(ls):
+ for item in ls:
+ if type(item) == list:
+ extend_flat(item)
+ else:
+ flat_array.append(item)
+
+ extend_flat(array_data)
+
+ # We requested a flat array
+ if group == 0:
+ return flat_array
+
+ new_array = []
+ sub_array = []
+
+ for item in flat_array:
+ sub_array.append(item)
+ if len(sub_array) == group:
+ new_array.append(sub_array)
+ sub_array = []
+
+ if sub_array:
+ print('\twarning, array was not aligned to requested grouping', group, 'remaining value', sub_array)
+
+ return new_array
+
+ def getFieldAsStringArray(self, field, ancestry):
+ '''
+ Get a list of strings
+ '''
+ self_real = self.getRealNode() # incase we're an instance
+
+ child_array = None
+ for child in self_real.children:
+ if child.id and len(child.id) == 1 and child.id[0] == field:
+ child_array = child
+ break
+ if not child_array:
+ return []
+
+ # each string gets its own list, remove ""'s
+ try:
+ new_array = [f[0][1:-1] for f in child_array.fields]
+ except:
+ print('\twarning, string array could not be made')
+ new_array = []
+
+ return new_array
+
+ def getLevel(self):
+ # Ignore self_real
+ level = 0
+ p = self.parent
+ while p:
+ level += 1
+ p = p.parent
+ if not p:
+ break
+
+ return level
+
+ def __repr__(self):
+ level = self.getLevel()
+ ind = ' ' * level
+ if self.node_type == NODE_REFERENCE:
+ brackets = ''
+ elif self.node_type == NODE_NORMAL:
+ brackets = '{}'
+ else:
+ brackets = '[]'
+
+ if brackets:
+ text = ind + brackets[0] + '\n'
+ else:
+ text = ''
+
+ text += ind + 'ID: ' + str(self.id) + ' ' + str(level) + (' lineno %d\n' % self.lineno)
+
+ if self.node_type == NODE_REFERENCE:
+ text += ind + "(reference node)\n"
+ return text
+
+ if self.proto_node:
+ text += ind + 'PROTO NODE...\n'
+ text += str(self.proto_node)
+ text += ind + 'PROTO NODE_DONE\n'
+
+ text += ind + 'FIELDS:' + str(len(self.fields)) + '\n'
+
+ for i, item in enumerate(self.fields):
+ text += ind + 'FIELD:\n'
+ text += ind + str(item) + '\n'
+
+ text += ind + 'PROTO_FIELD_DEFS:' + str(len(self.proto_field_defs)) + '\n'
+
+ for i, item in enumerate(self.proto_field_defs):
+ text += ind + 'PROTO_FIELD:\n'
+ text += ind + str(item) + '\n'
+
+ text += ind + 'ARRAY: ' + str(len(self.array_data)) + ' ' + str(self.array_data) + '\n'
+ #text += ind + 'ARRAY: ' + str(len(self.array_data)) + '[...] \n'
+
+ text += ind + 'CHILDREN: ' + str(len(self.children)) + '\n'
+ for i, child in enumerate(self.children):
+ text += ind + ('CHILD%d:\n' % i)
+ text += str(child)
+
+ text += '\n' + ind + brackets[1]
+
+ return text
+
+ def parse(self, i, IS_PROTO_DATA=False):
+ new_i = self.__parse(i, IS_PROTO_DATA)
+
+ # print(self.id, self.getFilename())
+
+ # Check if this node was an inline or externproto
+
+ url_ls = []
+
+ if self.node_type == NODE_NORMAL and self.getSpec() == 'Inline':
+ ancestry = [] # Warning! - PROTO's using this wont work at all.
+ url = self.getFieldAsString('url', None, ancestry)
+ if url:
+ url_ls = [(url, None)]
+ del ancestry
+
+ elif self.getExternprotoName():
+ # externproto
+ url_ls = []
+ for f in self.fields:
+
+ if type(f) == str:
+ f = [f]
+
+ for ff in f:
+ for f_split in ff.split('"'):
+ # print(f_split)
+ # "someextern.vrml#SomeID"
+ if '#' in f_split:
+
+ f_split, f_split_id = f_split.split('#') # there should only be 1 # anyway
+
+ url_ls.append((f_split, f_split_id))
+ else:
+ url_ls.append((f_split, None))
+
+ # Was either an Inline or an EXTERNPROTO
+ if url_ls:
+
+ # print(url_ls)
+
+ for url, extern_key in url_ls:
+ print(url)
+ urls = []
+ urls.append(url)
+ urls.append(bpy.path.resolve_ncase(urls[-1]))
+
+ urls.append(os.path.join(os.path.dirname(self.getFilename()), url))
+ urls.append(bpy.path.resolve_ncase(urls[-1]))
+
+ urls.append(os.path.join(os.path.dirname(self.getFilename()), os.path.basename(url)))
+ urls.append(bpy.path.resolve_ncase(urls[-1]))
+
+ try:
+ url = [url for url in urls if os.path.exists(url)][0]
+ url_found = True
+ except:
+ url_found = False
+
+ if not url_found:
+ print('\tWarning: Inline URL could not be found:', url)
+ else:
+ if url == self.getFilename():
+ print('\tWarning: cant Inline yourself recursively:', url)
+ else:
+
+ try:
+ data = gzipOpen(url)
+ except:
+ print('\tWarning: cant open the file:', url)
+ data = None
+
+ if data:
+ # Tricky - inline another VRML
+ print('\tLoading Inline:"%s"...' % url)
+
+ # Watch it! - backup lines
+ lines_old = lines[:]
+
+ lines[:] = vrmlFormat(data)
+
+ lines.insert(0, '{')
+ lines.insert(0, 'root_node____')
+ lines.append('}')
+ '''
+ ff = open('/tmp/test.txt', 'w')
+ ff.writelines([l+'\n' for l in lines])
+ '''
+
+ child = vrmlNode(self, NODE_NORMAL, -1)
+ child.setRoot(url) # initialized dicts
+ child.parse(0)
+
+ # if self.getExternprotoName():
+ if self.getExternprotoName():
+ if not extern_key: # if none is spesified - use the name
+ extern_key = self.getSpec()
+
+ if extern_key:
+
+ self.children.remove(child)
+ child.parent = None
+
+ extern_child = child.findSpecRecursive(extern_key)
+
+ if extern_child:
+ self.children.append(extern_child)
+ extern_child.parent = self
+
+ if DEBUG:
+ print("\tEXTERNPROTO ID found!:", extern_key)
+ else:
+ print("\tEXTERNPROTO ID not found!:", extern_key)
+
+ # Watch it! - restore lines
+ lines[:] = lines_old
+
+ return new_i
+
+ def __parse(self, i, IS_PROTO_DATA=False):
+ '''
+ print('parsing at', i, end="")
+ print(i, self.id, self.lineno)
+ '''
+ l = lines[i]
+
+ if l == '[':
+ # An anonymous list
+ self.id = None
+ i += 1
+ else:
+ words = []
+
+ node_type, new_i = is_nodeline(i, words)
+ if not node_type: # fail for parsing new node.
+ print("Failed to parse new node")
+ raise ValueError
+
+ if self.node_type == NODE_REFERENCE:
+ # Only assign the reference and quit
+ key = words[words.index('USE') + 1]
+ self.id = (words[0],)
+
+ self.reference = self.getDefDict()[key]
+ return new_i
+
+ self.id = tuple(words)
+
+ # fill in DEF/USE
+ key = self.getDefName()
+ if key != None:
+ self.getDefDict()[key] = self
+
+ key = self.getProtoName()
+ if not key:
+ key = self.getExternprotoName()
+
+ proto_dict = self.getProtoDict()
+ if key != None:
+ proto_dict[key] = self
+
+ # Parse the proto nodes fields
+ self.proto_node = vrmlNode(self, NODE_ARRAY, new_i)
+ new_i = self.proto_node.parse(new_i)
+
+ self.children.remove(self.proto_node)
+
+ # print(self.proto_node)
+
+ new_i += 1 # skip past the {
+
+ else: # If we're a proto instance, add the proto node as our child.
+ spec = self.getSpec()
+ try:
+ self.children.append(proto_dict[spec])
+ #pass
+ except:
+ pass
+
+ del spec
+
+ del proto_dict, key
+
+ i = new_i
+
+ # print(self.id)
+ ok = True
+ while ok:
+ if i >= len(lines):
+ return len(lines) - 1
+
+ l = lines[i]
+ # print('\tDEBUG:', i, self.node_type, l)
+ if l == '':
+ i += 1
+ continue
+
+ if l == '}':
+ if self.node_type != NODE_NORMAL: # also ends proto nodes, we may want a type for these too.
+ print('wrong node ending, expected an } ' + str(i) + ' ' + str(self.node_type))
+ if DEBUG:
+ raise ValueError
+ ### print("returning", i)
+ return i + 1
+ if l == ']':
+ if self.node_type != NODE_ARRAY:
+ print('wrong node ending, expected a ] ' + str(i) + ' ' + str(self.node_type))
+ if DEBUG:
+ raise ValueError
+ ### print("returning", i)
+ return i + 1
+
+ node_type, new_i = is_nodeline(i, [])
+ if node_type: # check text\n{
+ child = vrmlNode(self, node_type, i)
+ i = child.parse(i)
+
+ elif l == '[': # some files have these anonymous lists
+ child = vrmlNode(self, NODE_ARRAY, i)
+ i = child.parse(i)
+
+ elif is_numline(i):
+ l_split = l.split(',')
+
+ values = None
+ # See if each item is a float?
+
+ for num_type in (int, float):
+ try:
+ values = [num_type(v) for v in l_split]
+ break
+ except:
+ pass
+
+ try:
+ values = [[num_type(v) for v in segment.split()] for segment in l_split]
+ break
+ except:
+ pass
+
+ if values is None: # dont parse
+ values = l_split
+
+ # This should not extend over multiple lines however it is possible
+ # print(self.array_data)
+ if values:
+ self.array_data.extend(values)
+ i += 1
+ else:
+ words = l.split()
+ if len(words) > 2 and words[1] == 'USE':
+ vrmlNode(self, NODE_REFERENCE, i)
+ else:
+
+ # print("FIELD", i, l)
+ #
+ #words = l.split()
+ ### print('\t\ttag', i)
+ # this is a tag/
+ # print(words, i, l)
+ value = l
+ # print(i)
+ # javastrips can exist as values.
+ quote_count = l.count('"')
+ if quote_count % 2: # odd number?
+ # print('MULTILINE')
+ while 1:
+ i += 1
+ l = lines[i]
+ quote_count = l.count('"')
+ if quote_count % 2: # odd number?
+ value += '\n' + l[:l.rfind('"')]
+ break # assume
+ else:
+ value += '\n' + l
+
+ value_all = value.split()
+
+ def iskey(k):
+ if k[0] != '"' and k[0].isalpha() and k.upper() not in {'TRUE', 'FALSE'}:
+ return True
+ return False
+
+ def split_fields(value):
+ '''
+ key 0.0 otherkey 1,2,3 opt1 opt1 0.0
+ -> [key 0.0], [otherkey 1,2,3], [opt1 opt1 0.0]
+ '''
+ field_list = []
+ field_context = []
+
+ for j in range(len(value)):
+ if iskey(value[j]):
+ if field_context:
+ # this IS a key but the previous value was not a key, ot it was a defined field.
+ if (not iskey(field_context[-1])) or ((len(field_context) == 3 and field_context[1] == 'IS')):
+ field_list.append(field_context)
+
+ field_context = [value[j]]
+ else:
+ # The last item was not a value, multiple keys are needed in some cases.
+ field_context.append(value[j])
+ else:
+ # Is empty, just add this on
+ field_context.append(value[j])
+ else:
+ # Add a value to the list
+ field_context.append(value[j])
+
+ if field_context:
+ field_list.append(field_context)
+
+ return field_list
+
+ for value in split_fields(value_all):
+ # Split
+
+ if value[0] == 'field':
+ # field SFFloat creaseAngle 4
+ self.proto_field_defs.append(value)
+ else:
+ self.fields.append(value)
+ i += 1
+
+
+def gzipOpen(path):
+ try:
+ import gzip
+ except:
+ gzip = None
+
+ data = None
+ if gzip:
+ try:
+ data = gzip.open(path, 'r').read()
+ except:
+ pass
+ else:
+ print('\tNote, gzip module could not be imported, compressed files will fail to load')
+
+ if data is None:
+ try:
+ filehandle = open(path, 'rU')
+ data = filehandle.read()
+ filehandle.close()
+ except:
+ pass
+
+ return data
+
+
+def vrml_parse(path):
+ '''
+ Sets up the root node and returns it so load_web3d() can deal with the blender side of things.
+ Return root (vrmlNode, '') or (None, 'Error String')
+ '''
+ data = gzipOpen(path)
+
+ if data is None:
+ return None, 'Failed to open file: ' + path
+
+ # Stripped above
+ lines[:] = vrmlFormat(data)
+
+ lines.insert(0, '{')
+ lines.insert(0, 'dymmy_node')
+ lines.append('}')
+ # Use for testing our parsed output, so we can check on line numbers.
+
+ '''
+ ff = open('/tmp/test.txt', 'w')
+ ff.writelines([l+'\n' for l in lines])
+ ff.close()
+ '''
+
+ # Now evaluate it
+ node_type, new_i = is_nodeline(0, [])
+ if not node_type:
+ return None, 'Error: VRML file has no starting Node'
+
+ # Trick to make sure we get all root nodes.
+ lines.insert(0, '{')
+ lines.insert(0, 'root_node____') # important the name starts with an ascii char
+ lines.append('}')
+
+ root = vrmlNode(None, NODE_NORMAL, -1)
+ root.setRoot(path) # we need to set the root so we have a namespace and know the path incase of inlineing
+
+ # Parse recursively
+ root.parse(0)
+
+ # This prints a load of text
+ if DEBUG:
+ print(root)
+
+ return root, ''
+
+
+# ====================== END VRML
+
+# ====================== X3d Support
+
+# Sane as vrml but replace the parser
+class x3dNode(vrmlNode):
+ def __init__(self, parent, node_type, x3dNode):
+ vrmlNode.__init__(self, parent, node_type, -1)
+ self.x3dNode = x3dNode
+
+ def parse(self, IS_PROTO_DATA=False):
+ # print(self.x3dNode.tagName)
+
+ define = self.x3dNode.getAttributeNode('DEF')
+ if define:
+ self.getDefDict()[define.value] = self
+ else:
+ use = self.x3dNode.getAttributeNode('USE')
+ if use:
+ try:
+ self.reference = self.getDefDict()[use.value]
+ self.node_type = NODE_REFERENCE
+ except:
+ print('\tWarning: reference', use.value, 'not found')
+ self.parent.children.remove(self)
+
+ return
+
+ for x3dChildNode in self.x3dNode.childNodes:
+ if x3dChildNode.nodeType in {x3dChildNode.TEXT_NODE, x3dChildNode.COMMENT_NODE, x3dChildNode.CDATA_SECTION_NODE}:
+ continue
+
+ node_type = NODE_NORMAL
+ # print(x3dChildNode, dir(x3dChildNode))
+ if x3dChildNode.getAttributeNode('USE'):
+ node_type = NODE_REFERENCE
+
+ child = x3dNode(self, node_type, x3dChildNode)
+ child.parse()
+
+ # TODO - x3d Inline
+
+ def getSpec(self):
+ return self.x3dNode.tagName # should match vrml spec
+
+ def getDefName(self):
+ data = self.x3dNode.getAttributeNode('DEF')
+ if data:
+ data.value # XXX, return??
+ return None
+
+ # Other funcs operate from vrml, but this means we can wrap XML fields, still use nice utility funcs
+ # getFieldAsArray getFieldAsBool etc
+ def getFieldName(self, field, ancestry, AS_CHILD=False):
+ # ancestry and AS_CHILD are ignored, only used for VRML now
+
+ self_real = self.getRealNode() # incase we're an instance
+ field_xml = self.x3dNode.getAttributeNode(field)
+ if field_xml:
+ value = field_xml.value
+
+ # We may want to edit. for x3d spesific stuff
+ # Sucks a bit to return the field name in the list but vrml excepts this :/
+ return value.split()
+ else:
+ return None
+
+
+def x3d_parse(path):
+ '''
+ Sets up the root node and returns it so load_web3d() can deal with the blender side of things.
+ Return root (x3dNode, '') or (None, 'Error String')
+ '''
+
+ try:
+ import xml.dom.minidom
+ except:
+ return None, 'Error, import XML parsing module (xml.dom.minidom) failed, install python'
+
+ '''
+ try: doc = xml.dom.minidom.parse(path)
+ except: return None, 'Could not parse this X3D file, XML error'
+ '''
+
+ # Could add a try/except here, but a console error is more useful.
+ data = gzipOpen(path)
+
+ if data is None:
+ return None, 'Failed to open file: ' + path
+
+ doc = xml.dom.minidom.parseString(data)
+
+ try:
+ x3dnode = doc.getElementsByTagName('X3D')[0]
+ except:
+ return None, 'Not a valid x3d document, cannot import'
+
+ root = x3dNode(None, NODE_NORMAL, x3dnode)
+ root.setRoot(path) # so images and Inline's we load have a relative path
+ root.parse()
+
+ return root, ''
+
+## f = open('/_Cylinder.wrl', 'r')
+# f = open('/fe/wrl/Vrml/EGS/TOUCHSN.WRL', 'r')
+# vrml_parse('/fe/wrl/Vrml/EGS/TOUCHSN.WRL')
+#vrml_parse('/fe/wrl/Vrml/EGS/SCRIPT.WRL')
+'''
+import os
+files = os.popen('find /fe/wrl -iname "*.wrl"').readlines()
+files.sort()
+tot = len(files)
+for i, f in enumerate(files):
+ #if i < 801:
+ # continue
+
+ f = f.strip()
+ print(f, i, tot)
+ vrml_parse(f)
+'''
+
+# NO BLENDER CODE ABOVE THIS LINE.
+# -----------------------------------------------------------------------------------
+import bpy
+from bpy_extras import image_utils
+# import BPyImage
+# import BPySys
+# reload(BPySys)
+# reload(BPyImage)
+# import Blender
+# from Blender import Texture, Material, Mathutils, Mesh, Types, Window
+from mathutils import Vector, Matrix
+
+RAD_TO_DEG = 57.29578
+
+GLOBALS = {'CIRCLE_DETAIL': 16}
+
+
+def translateRotation(rot):
+ ''' axis, angle '''
+ return Matrix.Rotation(rot[3], 4, Vector(rot[:3]))
+
+
+def translateScale(sca):
+ mat = Matrix() # 4x4 default
+ mat[0][0] = sca[0]
+ mat[1][1] = sca[1]
+ mat[2][2] = sca[2]
+ return mat
+
+
+def translateTransform(node, ancestry):
+ cent = node.getFieldAsFloatTuple('center', None, ancestry) # (0.0, 0.0, 0.0)
+ rot = node.getFieldAsFloatTuple('rotation', None, ancestry) # (0.0, 0.0, 1.0, 0.0)
+ sca = node.getFieldAsFloatTuple('scale', None, ancestry) # (1.0, 1.0, 1.0)
+ scaori = node.getFieldAsFloatTuple('scaleOrientation', None, ancestry) # (0.0, 0.0, 1.0, 0.0)
+ tx = node.getFieldAsFloatTuple('translation', None, ancestry) # (0.0, 0.0, 0.0)
+
+ if cent:
+ cent_mat = Matrix.Translation(cent)
+ cent_imat = cent_mat.inverted()
+ else:
+ cent_mat = cent_imat = None
+
+ if rot:
+ rot_mat = translateRotation(rot)
+ else:
+ rot_mat = None
+
+ if sca:
+ sca_mat = translateScale(sca)
+ else:
+ sca_mat = None
+
+ if scaori:
+ scaori_mat = translateRotation(scaori)
+ scaori_imat = scaori_mat.inverted()
+ else:
+ scaori_mat = scaori_imat = None
+
+ if tx:
+ tx_mat = Matrix.Translation(tx)
+ else:
+ tx_mat = None
+
+ new_mat = Matrix()
+
+ mats = [tx_mat, cent_mat, rot_mat, scaori_mat, sca_mat, scaori_imat, cent_imat]
+ for mtx in mats:
+ if mtx:
+ new_mat = new_mat * mtx
+
+ return new_mat
+
+
+def translateTexTransform(node, ancestry):
+ cent = node.getFieldAsFloatTuple('center', None, ancestry) # (0.0, 0.0)
+ rot = node.getFieldAsFloat('rotation', None, ancestry) # 0.0
+ sca = node.getFieldAsFloatTuple('scale', None, ancestry) # (1.0, 1.0)
+ tx = node.getFieldAsFloatTuple('translation', None, ancestry) # (0.0, 0.0)
+
+ if cent:
+ # cent is at a corner by default
+ cent_mat = Matrix.Translation(Vector(cent).to_3d())
+ cent_imat = cent_mat.inverted()
+ else:
+ cent_mat = cent_imat = None
+
+ if rot:
+ rot_mat = Matrix.Rotation(rot, 4, 'Z') # translateRotation(rot)
+ else:
+ rot_mat = None
+
+ if sca:
+ sca_mat = translateScale((sca[0], sca[1], 0.0))
+ else:
+ sca_mat = None
+
+ if tx:
+ tx_mat = Matrix.Translation(Vector(tx).to_3d())
+ else:
+ tx_mat = None
+
+ new_mat = Matrix()
+
+ # as specified in VRML97 docs
+ mats = [cent_imat, sca_mat, rot_mat, cent_mat, tx_mat]
+
+ for mtx in mats:
+ if mtx:
+ new_mat = new_mat * mtx
+
+ return new_mat
+
+
+# 90d X rotation
+import math
+MATRIX_Z_TO_Y = Matrix.Rotation(math.pi / 2.0, 4, 'X')
+
+
+def getFinalMatrix(node, mtx, ancestry, global_matrix):
+
+ transform_nodes = [node_tx for node_tx in ancestry if node_tx.getSpec() == 'Transform']
+ if node.getSpec() == 'Transform':
+ transform_nodes.append(node)
+ transform_nodes.reverse()
+
+ if mtx is None:
+ mtx = Matrix()
+
+ for node_tx in transform_nodes:
+ mat = translateTransform(node_tx, ancestry)
+ mtx = mat * mtx
+
+ # worldspace matrix
+ mtx = global_matrix * mtx
+
+ return mtx
+
+
+def importMesh_IndexedFaceSet(geom, bpyima, ancestry):
+ # print(geom.lineno, geom.id, vrmlNode.DEF_NAMESPACE.keys())
+
+ ccw = geom.getFieldAsBool('ccw', True, ancestry)
+ ifs_colorPerVertex = geom.getFieldAsBool('colorPerVertex', True, ancestry) # per vertex or per face
+ ifs_normalPerVertex = geom.getFieldAsBool('normalPerVertex', True, ancestry)
+
+ # This is odd how point is inside Coordinate
+
+ # VRML not x3d
+ #coord = geom.getChildByName('coord') # 'Coordinate'
+
+ coord = geom.getChildBySpec('Coordinate') # works for x3d and vrml
+
+ if coord:
+ ifs_points = coord.getFieldAsArray('point', 3, ancestry)
+ else:
+ coord = []
+
+ if not coord:
+ print('\tWarnint: IndexedFaceSet has no points')
+ return None, ccw
+
+ ifs_faces = geom.getFieldAsArray('coordIndex', 0, ancestry)
+
+ coords_tex = None
+ if ifs_faces: # In rare cases this causes problems - no faces but UVs???
+
+ # WORKS - VRML ONLY
+ # coords_tex = geom.getChildByName('texCoord')
+ coords_tex = geom.getChildBySpec('TextureCoordinate')
+
+ if coords_tex:
+ ifs_texpoints = coords_tex.getFieldAsArray('point', 2, ancestry)
+ ifs_texfaces = geom.getFieldAsArray('texCoordIndex', 0, ancestry)
+
+ if not ifs_texpoints:
+ # IF we have no coords, then dont bother
+ coords_tex = None
+
+ # WORKS - VRML ONLY
+ # vcolor = geom.getChildByName('color')
+ vcolor = geom.getChildBySpec('Color')
+ vcolor_spot = None # spot color when we dont have an array of colors
+ if vcolor:
+ # float to char
+ ifs_vcol = [(0, 0, 0)] # EEKADOODLE - vertex start at 1
+ ifs_vcol.extend([col for col in vcolor.getFieldAsArray('color', 3, ancestry)])
+ ifs_color_index = geom.getFieldAsArray('colorIndex', 0, ancestry)
+
+ if not ifs_vcol:
+ vcolor_spot = vcolor.getFieldAsFloatTuple('color', [], ancestry)
+
+ # Convert faces into somthing blender can use
+ edges = []
+
+ # All lists are aligned!
+ faces = []
+ faces_uv = [] # if ifs_texfaces is empty then the faces_uv will match faces exactly.
+ faces_orig_index = [] # for ngons, we need to know our original index
+
+ if coords_tex and ifs_texfaces:
+ do_uvmap = True
+ else:
+ do_uvmap = False
+
+ # current_face = [0] # pointer anyone
+
+ def add_face(face, fuvs, orig_index):
+ l = len(face)
+ if l == 3 or l == 4:
+ faces.append(face)
+ # faces_orig_index.append(current_face[0])
+ if do_uvmap:
+ faces_uv.append(fuvs)
+
+ faces_orig_index.append(orig_index)
+ elif l == 2:
+ edges.append(face)
+ elif l > 4:
+ for i in range(2, len(face)):
+ faces.append([face[0], face[i - 1], face[i]])
+ if do_uvmap:
+ faces_uv.append([fuvs[0], fuvs[i - 1], fuvs[i]])
+ faces_orig_index.append(orig_index)
+ else:
+ # faces with 1 verts? pfft!
+ # still will affect index ordering
+ pass
+
+ face = []
+ fuvs = []
+ orig_index = 0
+ for i, fi in enumerate(ifs_faces):
+ # ifs_texfaces and ifs_faces should be aligned
+ if fi != -1:
+ # face.append(int(fi)) # in rare cases this is a float
+ # EEKADOODLE!!!
+ # Annoyance where faces that have a zero index vert get rotated. This will then mess up UVs and VColors
+ face.append(int(fi) + 1) # in rare cases this is a float, +1 because of stupid EEKADOODLE :/
+
+ if do_uvmap:
+ if i >= len(ifs_texfaces):
+ print('\tWarning: UV Texface index out of range')
+ fuvs.append(ifs_texfaces[0])
+ else:
+ fuvs.append(ifs_texfaces[i])
+ else:
+ add_face(face, fuvs, orig_index)
+ face = []
+ if do_uvmap:
+ fuvs = []
+ orig_index += 1
+
+ add_face(face, fuvs, orig_index)
+ del add_face # dont need this func anymore
+
+ bpymesh = bpy.data.meshes.new(name="XXX")
+
+ # EEKADOODLE
+ bpymesh.vertices.add(1 + (len(ifs_points)))
+ bpymesh.vertices.foreach_set("co", [0, 0, 0] + [a for v in ifs_points for a in v]) # XXX25 speed
+
+ # print(len(ifs_points), faces, edges, ngons)
+
+ try:
+ bpymesh.faces.add(len(faces))
+ bpymesh.faces.foreach_set("vertices_raw", [a for f in faces for a in (f + [0] if len(f) == 3 else f)]) # XXX25 speed
+ except KeyError:
+ print("one or more vert indices out of range. corrupt file?")
+ #for f in faces:
+ # bpymesh.faces.extend(faces, smooth=True)
+
+ bpymesh.validate()
+ bpymesh.update()
+
+ if len(bpymesh.faces) != len(faces):
+ print('\tWarning: adding faces did not work! file is invalid, not adding UVs or vcolors')
+ return bpymesh, ccw
+
+ # Apply UVs if we have them
+ if not do_uvmap:
+ faces_uv = faces # fallback, we didnt need a uvmap in the first place, fallback to the face/vert mapping.
+ if coords_tex:
+ #print(ifs_texpoints)
+ # print(geom)
+ uvlay = bpymesh.uv_textures.new()
+
+ for i, f in enumerate(uvlay.data):
+ f.image = bpyima
+ fuv = faces_uv[i] # uv indices
+ for j, uv in enumerate(f.uv):
+ # print(fuv, j, len(ifs_texpoints))
+ try:
+ f.uv[j] = ifs_texpoints[fuv[j]] # XXX25, speedup
+ except:
+ print('\tWarning: UV Index out of range')
+ f.uv[j] = ifs_texpoints[0] # XXX25, speedup
+
+ elif bpyima and len(bpymesh.faces):
+ # Oh Bugger! - we cant really use blenders ORCO for for texture space since texspace dosnt rotate.
+ # we have to create VRML's coords as UVs instead.
+
+ # VRML docs
+ '''
+ If the texCoord field is NULL, a default texture coordinate mapping is calculated using the local
+ coordinate system bounding box of the shape. The longest dimension of the bounding box defines the S coordinates,
+ and the next longest defines the T coordinates. If two or all three dimensions of the bounding box are equal,
+ ties shall be broken by choosing the X, Y, or Z dimension in that order of preference.
+ The value of the S coordinate ranges from 0 to 1, from one end of the bounding box to the other.
+ The T coordinate ranges between 0 and the ratio of the second greatest dimension of the bounding box to the greatest dimension.
+ '''
+
+ # Note, S,T == U,V
+ # U gets longest, V gets second longest
+ xmin, ymin, zmin = ifs_points[0]
+ xmax, ymax, zmax = ifs_points[0]
+ for co in ifs_points:
+ x, y, z = co
+ if x < xmin:
+ xmin = x
+ if y < ymin:
+ ymin = y
+ if z < zmin:
+ zmin = z
+
+ if x > xmax:
+ xmax = x
+ if y > ymax:
+ ymax = y
+ if z > zmax:
+ zmax = z
+
+ xlen = xmax - xmin
+ ylen = ymax - ymin
+ zlen = zmax - zmin
+
+ depth_min = xmin, ymin, zmin
+ depth_list = [xlen, ylen, zlen]
+ depth_sort = depth_list[:]
+ depth_sort.sort()
+
+ depth_idx = [depth_list.index(val) for val in depth_sort]
+
+ axis_u = depth_idx[-1]
+ axis_v = depth_idx[-2] # second longest
+
+ # Hack, swap these !!! TODO - Why swap??? - it seems to work correctly but should not.
+ # axis_u,axis_v = axis_v,axis_u
+
+ min_u = depth_min[axis_u]
+ min_v = depth_min[axis_v]
+ depth_u = depth_list[axis_u]
+ depth_v = depth_list[axis_v]
+
+ depth_list[axis_u]
+
+ if axis_u == axis_v:
+ # This should be safe because when 2 axies have the same length, the lower index will be used.
+ axis_v += 1
+
+ uvlay = bpymesh.uv_textures.new()
+
+ # HACK !!! - seems to be compatible with Cosmo though.
+ depth_v = depth_u = max(depth_v, depth_u)
+
+ bpymesh_vertices = bpymesh.vertices[:]
+ bpymesh_faces = bpymesh.faces[:]
+
+ for j, f in enumerate(uvlay.data):
+ f.image = bpyima
+ fuv = f.uv
+ f_v = bpymesh_faces[j].vertices[:] # XXX25 speed
+
+ for i, v in enumerate(f_v):
+ co = bpymesh_vertices[v].co
+ fuv[i] = (co[axis_u] - min_u) / depth_u, (co[axis_v] - min_v) / depth_v
+
+ # Add vcote
+ if vcolor:
+ # print(ifs_vcol)
+ collay = bpymesh.vertex_colors.new()
+
+ for f_idx, f in enumerate(collay.data):
+ fv = bpymesh.faces[f_idx].vertices[:]
+ if len(fv) == 3: # XXX speed
+ fcol = f.color1, f.color2, f.color3
+ else:
+ fcol = f.color1, f.color2, f.color3, f.color4
+ if ifs_colorPerVertex:
+ for i, c in enumerate(fcol):
+ color_index = fv[i] # color index is vert index
+ if ifs_color_index:
+ try:
+ color_index = ifs_color_index[color_index]
+ except:
+ print('\tWarning: per vertex color index out of range')
+ continue
+
+ if color_index < len(ifs_vcol):
+ c.r, c.g, c.b = ifs_vcol[color_index]
+ else:
+ #print('\tWarning: per face color index out of range')
+ pass
+ else:
+ if vcolor_spot: # use 1 color, when ifs_vcol is []
+ for c in fcol:
+ c.r, c.g, c.b = vcolor_spot
+ else:
+ color_index = faces_orig_index[f_idx] # color index is face index
+ #print(color_index, ifs_color_index)
+ if ifs_color_index:
+ if color_index >= len(ifs_color_index):
+ print('\tWarning: per face color index out of range')
+ color_index = 0
+ else:
+ color_index = ifs_color_index[color_index]
+ try:
+ col = ifs_vcol[color_index]
+ except IndexError:
+ # TODO, look
+ col = (1.0, 1.0, 1.0)
+ for i, c in enumerate(fcol):
+ c.r, c.g, c.b = col
+
+ # XXX25
+ # bpymesh.vertices.delete([0, ]) # EEKADOODLE
+
+ return bpymesh, ccw
+
+
+def importMesh_IndexedLineSet(geom, ancestry):
+ # VRML not x3d
+ #coord = geom.getChildByName('coord') # 'Coordinate'
+ coord = geom.getChildBySpec('Coordinate') # works for x3d and vrml
+ if coord:
+ points = coord.getFieldAsArray('point', 3, ancestry)
+ else:
+ points = []
+
+ if not points:
+ print('\tWarning: IndexedLineSet had no points')
+ return None
+
+ ils_lines = geom.getFieldAsArray('coordIndex', 0, ancestry)
+
+ lines = []
+ line = []
+
+ for il in ils_lines:
+ if il == -1:
+ lines.append(line)
+ line = []
+ else:
+ line.append(int(il))
+ lines.append(line)
+
+ # vcolor = geom.getChildByName('color') # blender dosnt have per vertex color
+
+ bpycurve = bpy.data.curves.new('IndexedCurve', 'CURVE')
+ bpycurve.dimensions = '3D'
+
+ for line in lines:
+ if not line:
+ continue
+ # co = points[line[0]] # UNUSED
+ nu = bpycurve.splines.new('POLY')
+ nu.points.add(len(line) - 1) # the new nu has 1 point to begin with
+ for il, pt in zip(line, nu.points):
+ pt.co[0:3] = points[il]
+
+ return bpycurve
+
+
+def importMesh_PointSet(geom, ancestry):
+ # VRML not x3d
+ #coord = geom.getChildByName('coord') # 'Coordinate'
+ coord = geom.getChildBySpec('Coordinate') # works for x3d and vrml
+ if coord:
+ points = coord.getFieldAsArray('point', 3, ancestry)
+ else:
+ points = []
+
+ # vcolor = geom.getChildByName('color') # blender dosnt have per vertex color
+
+ bpymesh = bpy.data.meshes.new("XXX")
+ bpymesh.vertices.add(len(points))
+ bpymesh.vertices.foreach_set("co", [a for v in points for a in v])
+
+ # No need to validate
+ bpymesh.update()
+ return bpymesh
+
+GLOBALS['CIRCLE_DETAIL'] = 12
+
+
+def bpy_ops_add_object_hack(): # XXX25, evil
+ scene = bpy.context.scene
+ obj = scene.objects[0]
+ scene.objects.unlink(obj)
+ bpymesh = obj.data
+ bpy.data.objects.remove(obj)
+ return bpymesh
+
+
+def importMesh_Sphere(geom, ancestry):
+ diameter = geom.getFieldAsFloat('radius', 0.5, ancestry)
+ # bpymesh = Mesh.Primitives.UVsphere(GLOBALS['CIRCLE_DETAIL'], GLOBALS['CIRCLE_DETAIL'], diameter)
+
+ bpy.ops.mesh.primitive_uv_sphere_add(segments=GLOBALS['CIRCLE_DETAIL'],
+ ring_count=GLOBALS['CIRCLE_DETAIL'],
+ size=diameter,
+ view_align=False,
+ enter_editmode=False,
+ )
+
+ bpymesh = bpy_ops_add_object_hack()
+
+ bpymesh.transform(MATRIX_Z_TO_Y)
+ return bpymesh
+
+
+def importMesh_Cylinder(geom, ancestry):
+ # bpymesh = bpy.data.meshes.new()
+ diameter = geom.getFieldAsFloat('radius', 1.0, ancestry)
+ height = geom.getFieldAsFloat('height', 2, ancestry)
+
+ # bpymesh = Mesh.Primitives.Cylinder(GLOBALS['CIRCLE_DETAIL'], diameter, height)
+
+ bpy.ops.mesh.primitive_cylinder_add(vertices=GLOBALS['CIRCLE_DETAIL'],
+ radius=diameter,
+ depth=height,
+ cap_ends=True,
+ view_align=False,
+ enter_editmode=False,
+ )
+
+ bpymesh = bpy_ops_add_object_hack()
+
+ bpymesh.transform(MATRIX_Z_TO_Y)
+
+ # Warning - Rely in the order Blender adds verts
+ # not nice design but wont change soon.
+
+ bottom = geom.getFieldAsBool('bottom', True, ancestry)
+ side = geom.getFieldAsBool('side', True, ancestry)
+ top = geom.getFieldAsBool('top', True, ancestry)
+
+ if not top: # last vert is top center of tri fan.
+ # bpymesh.vertices.delete([(GLOBALS['CIRCLE_DETAIL'] + GLOBALS['CIRCLE_DETAIL']) + 1]) # XXX25
+ pass
+
+ if not bottom: # second last vert is bottom of triangle fan
+ # XXX25
+ # bpymesh.vertices.delete([GLOBALS['CIRCLE_DETAIL'] + GLOBALS['CIRCLE_DETAIL']])
+ pass
+
+ if not side:
+ # remove all quads
+ # XXX25
+ # bpymesh.faces.delete(1, [f for f in bpymesh.faces if len(f) == 4])
+ pass
+
+ return bpymesh
+
+
+def importMesh_Cone(geom, ancestry):
+ # bpymesh = bpy.data.meshes.new()
+ diameter = geom.getFieldAsFloat('bottomRadius', 1.0, ancestry)
+ height = geom.getFieldAsFloat('height', 2, ancestry)
+
+ # bpymesh = Mesh.Primitives.Cone(GLOBALS['CIRCLE_DETAIL'], diameter, height)
+
+ bpy.ops.mesh.primitive_cone_add(vertices=GLOBALS['CIRCLE_DETAIL'],
+ radius=diameter,
+ depth=height,
+ cap_end=True,
+ view_align=False,
+ enter_editmode=False,
+ )
+
+ bpymesh = bpy_ops_add_object_hack()
+
+ bpymesh.transform(MATRIX_Z_TO_Y)
+
+ # Warning - Rely in the order Blender adds verts
+ # not nice design but wont change soon.
+
+ bottom = geom.getFieldAsBool('bottom', True, ancestry)
+ side = geom.getFieldAsBool('side', True, ancestry)
+
+ if not bottom: # last vert is on the bottom
+ # bpymesh.vertices.delete([GLOBALS['CIRCLE_DETAIL'] + 1]) # XXX25
+ pass
+ if not side: # second last vert is on the pointy bit of the cone
+ # bpymesh.vertices.delete([GLOBALS['CIRCLE_DETAIL']]) # XXX25
+ pass
+
+ return bpymesh
+
+
+def importMesh_Box(geom, ancestry):
+ # bpymesh = bpy.data.meshes.new()
+
+ size = geom.getFieldAsFloatTuple('size', (2.0, 2.0, 2.0), ancestry)
+
+ # bpymesh = Mesh.Primitives.Cube(1.0)
+ bpy.ops.mesh.primitive_cube_add(view_align=False,
+ enter_editmode=False,
+ )
+
+ bpymesh = bpy_ops_add_object_hack()
+
+ # Scale the box to the size set
+ scale_mat = Matrix(((size[0], 0, 0), (0, size[1], 0), (0, 0, size[2]))) * 0.5
+ bpymesh.transform(scale_mat.to_4x4())
+
+ return bpymesh
+
+
+def importShape(node, ancestry, global_matrix):
+ vrmlname = node.getDefName()
+ if not vrmlname:
+ vrmlname = 'Shape'
+
+ # works 100% in vrml, but not x3d
+ #appr = node.getChildByName('appearance') # , 'Appearance'
+ #geom = node.getChildByName('geometry') # , 'IndexedFaceSet'
+
+ # Works in vrml and x3d
+ appr = node.getChildBySpec('Appearance')
+ geom = node.getChildBySpec(['IndexedFaceSet', 'IndexedLineSet', 'PointSet', 'Sphere', 'Box', 'Cylinder', 'Cone'])
+
+ # For now only import IndexedFaceSet's
+ if geom:
+ bpymat = None
+ bpyima = None
+ texmtx = None
+
+ depth = 0 # so we can set alpha face flag later
+
+ if appr:
+
+ #mat = appr.getChildByName('material') # 'Material'
+ #ima = appr.getChildByName('texture') # , 'ImageTexture'
+ #if ima and ima.getSpec() != 'ImageTexture':
+ # print('\tWarning: texture type "%s" is not supported' % ima.getSpec())
+ # ima = None
+ # textx = appr.getChildByName('textureTransform')
+
+ mat = appr.getChildBySpec('Material')
+ ima = appr.getChildBySpec('ImageTexture')
+
+ textx = appr.getChildBySpec('TextureTransform')
+
+ if textx:
+ texmtx = translateTexTransform(textx, ancestry)
+
+ # print(mat, ima)
+ if mat or ima:
+
+ if not mat:
+ mat = ima # This is a bit dumb, but just means we use default values for all
+
+ # all values between 0.0 and 1.0, defaults from VRML docs
+ bpymat = bpy.data.materials.new("XXX")
+ bpymat.ambient = mat.getFieldAsFloat('ambientIntensity', 0.2, ancestry)
+ bpymat.diffuse_color = mat.getFieldAsFloatTuple('diffuseColor', [0.8, 0.8, 0.8], ancestry)
+
+ # NOTE - blender dosnt support emmisive color
+ # Store in mirror color and approximate with emit.
+ emit = mat.getFieldAsFloatTuple('emissiveColor', [0.0, 0.0, 0.0], ancestry)
+ bpymat.mirror_color = emit
+ bpymat.emit = (emit[0] + emit[1] + emit[2]) / 3.0
+
+ bpymat.specular_hardness = int(1 + (510 * mat.getFieldAsFloat('shininess', 0.2, ancestry))) # 0-1 -> 1-511
+ bpymat.specular_color = mat.getFieldAsFloatTuple('specularColor', [0.0, 0.0, 0.0], ancestry)
+ bpymat.alpha = 1.0 - mat.getFieldAsFloat('transparency', 0.0, ancestry)
+ if bpymat.alpha < 0.999:
+ bpymat.use_transparency = True
+
+ if ima:
+ ima_url = ima.getFieldAsString('url', None, ancestry)
+
+ if ima_url is None:
+ try:
+ ima_url = ima.getFieldAsStringArray('url', ancestry)[0] # in some cases we get a list of images.
+ except:
+ ima_url = None
+
+ if ima_url is None:
+ print("\twarning, image with no URL, this is odd")
+ else:
+ bpyima = image_utils.image_load(ima_url, os.path.dirname(node.getFilename()), place_holder=False, recursive=False, convert_callback=imageConvertCompat)
+ if bpyima:
+ texture = bpy.data.textures.new("XXX", 'IMAGE')
+ texture.image = bpyima
+
+ # Adds textures for materials (rendering)
+ try:
+ depth = bpyima.depth
+ except:
+ depth = -1
+
+ if depth == 32:
+ # Image has alpha
+ bpymat.setTexture(0, texture, Texture.TexCo.UV, Texture.MapTo.COL | Texture.MapTo.ALPHA)
+ texture.setImageFlags('MipMap', 'InterPol', 'UseAlpha')
+ bpymat.mode |= Material.Modes.ZTRANSP
+ bpymat.alpha = 0.0
+ else:
+ mtex = bpymat.texture_slots.add()
+ mtex.texture = texture
+ mtex.texture_coords = 'UV'
+ mtex.use_map_diffuse = True
+
+ ima_repS = ima.getFieldAsBool('repeatS', True, ancestry)
+ ima_repT = ima.getFieldAsBool('repeatT', True, ancestry)
+
+ # To make this work properly we'd need to scale the UV's too, better to ignore th
+ # texture.repeat = max(1, ima_repS * 512), max(1, ima_repT * 512)
+
+ if not ima_repS:
+ bpyima.use_clamp_x = True
+ if not ima_repT:
+ bpyima.use_clamp_y = True
+
+ bpydata = None
+ geom_spec = geom.getSpec()
+ ccw = True
+ if geom_spec == 'IndexedFaceSet':
+ bpydata, ccw = importMesh_IndexedFaceSet(geom, bpyima, ancestry)
+ elif geom_spec == 'IndexedLineSet':
+ bpydata = importMesh_IndexedLineSet(geom, ancestry)
+ elif geom_spec == 'PointSet':
+ bpydata = importMesh_PointSet(geom, ancestry)
+ elif geom_spec == 'Sphere':
+ bpydata = importMesh_Sphere(geom, ancestry)
+ elif geom_spec == 'Box':
+ bpydata = importMesh_Box(geom, ancestry)
+ elif geom_spec == 'Cylinder':
+ bpydata = importMesh_Cylinder(geom, ancestry)
+ elif geom_spec == 'Cone':
+ bpydata = importMesh_Cone(geom, ancestry)
+ else:
+ print('\tWarning: unsupported type "%s"' % geom_spec)
+ return
+
+ if bpydata:
+ vrmlname = vrmlname + geom_spec
+
+ bpydata.name = vrmlname
+
+ bpyob = node.blendObject = bpy.data.objects.new(vrmlname, bpydata)
+ bpy.context.scene.objects.link(bpyob)
+
+ if type(bpydata) == bpy.types.Mesh:
+ is_solid = geom.getFieldAsBool('solid', True, ancestry)
+ creaseAngle = geom.getFieldAsFloat('creaseAngle', None, ancestry)
+
+ if creaseAngle is not None:
+ bpydata.auto_smooth_angle = creaseAngle
+ bpydata.use_auto_smooth = True
+
+ # Only ever 1 material per shape
+ if bpymat:
+ bpydata.materials.append(bpymat)
+
+ if bpydata.uv_textures:
+
+ if depth == 32: # set the faces alpha flag?
+ transp = Mesh.FaceTranspModes.ALPHA
+ for f in bpydata.uv_textures.active.data:
+ f.blend_type = 'ALPHA'
+
+ if texmtx:
+ # Apply texture transform?
+ uv_copy = Vector()
+ for f in bpydata.uv_textures.active.data:
+ fuv = f.uv
+ for i, uv in enumerate(fuv):
+ uv_copy.x = uv[0]
+ uv_copy.y = uv[1]
+
+ fuv[i] = (uv_copy * texmtx)[0:2]
+ # Done transforming the texture
+
+ # Must be here and not in IndexedFaceSet because it needs an object for the flip func. Messy :/
+ if not ccw:
+ # bpydata.flipNormals()
+ # XXX25
+ pass
+
+ # else could be a curve for example
+
+ # Can transform data or object, better the object so we can instance the data
+ #bpymesh.transform(getFinalMatrix(node))
+ bpyob.matrix_world = getFinalMatrix(node, None, ancestry, global_matrix)
+
+
+def importLamp_PointLight(node, ancestry):
+ vrmlname = node.getDefName()
+ if not vrmlname:
+ vrmlname = 'PointLight'
+
+ # ambientIntensity = node.getFieldAsFloat('ambientIntensity', 0.0, ancestry) # TODO
+ # attenuation = node.getFieldAsFloatTuple('attenuation', (1.0, 0.0, 0.0), ancestry) # TODO
+ color = node.getFieldAsFloatTuple('color', (1.0, 1.0, 1.0), ancestry)
+ intensity = node.getFieldAsFloat('intensity', 1.0, ancestry) # max is documented to be 1.0 but some files have higher.
+ location = node.getFieldAsFloatTuple('location', (0.0, 0.0, 0.0), ancestry)
+ # is_on = node.getFieldAsBool('on', True, ancestry) # TODO
+ radius = node.getFieldAsFloat('radius', 100.0, ancestry)
+
+ bpylamp = bpy.data.lamps.new("ToDo", 'POINT')
+ bpylamp.energy = intensity
+ bpylamp.distance = radius
+ bpylamp.color = color
+
+ mtx = Matrix.Translation(Vector(location))
+
+ return bpylamp, mtx
+
+
+def importLamp_DirectionalLight(node, ancestry):
+ vrmlname = node.getDefName()
+ if not vrmlname:
+ vrmlname = 'DirectLight'
+
+ # ambientIntensity = node.getFieldAsFloat('ambientIntensity', 0.0) # TODO
+ color = node.getFieldAsFloatTuple('color', (1.0, 1.0, 1.0), ancestry)
+ direction = node.getFieldAsFloatTuple('direction', (0.0, 0.0, -1.0), ancestry)
+ intensity = node.getFieldAsFloat('intensity', 1.0, ancestry) # max is documented to be 1.0 but some files have higher.
+ # is_on = node.getFieldAsBool('on', True, ancestry) # TODO
+
+ bpylamp = bpy.data.lamps.new(vrmlname, 'SUN')
+ bpylamp.energy = intensity
+ bpylamp.color = color
+
+ # lamps have their direction as -z, yup
+ mtx = Vector(direction).to_track_quat('-Z', 'Y').to_matrix().to_4x4()
+
+ return bpylamp, mtx
+
+# looks like default values for beamWidth and cutOffAngle were swapped in VRML docs.
+
+
+def importLamp_SpotLight(node, ancestry):
+ vrmlname = node.getDefName()
+ if not vrmlname:
+ vrmlname = 'SpotLight'
+
+ # ambientIntensity = geom.getFieldAsFloat('ambientIntensity', 0.0, ancestry) # TODO
+ # attenuation = geom.getFieldAsFloatTuple('attenuation', (1.0, 0.0, 0.0), ancestry) # TODO
+ beamWidth = node.getFieldAsFloat('beamWidth', 1.570796, ancestry) # max is documented to be 1.0 but some files have higher.
+ color = node.getFieldAsFloatTuple('color', (1.0, 1.0, 1.0), ancestry)
+ cutOffAngle = node.getFieldAsFloat('cutOffAngle', 0.785398, ancestry) * 2.0 # max is documented to be 1.0 but some files have higher.
+ direction = node.getFieldAsFloatTuple('direction', (0.0, 0.0, -1.0), ancestry)
+ intensity = node.getFieldAsFloat('intensity', 1.0, ancestry) # max is documented to be 1.0 but some files have higher.
+ location = node.getFieldAsFloatTuple('location', (0.0, 0.0, 0.0), ancestry)
+ # is_on = node.getFieldAsBool('on', True, ancestry) # TODO
+ radius = node.getFieldAsFloat('radius', 100.0, ancestry)
+
+ bpylamp = bpy.data.lamps.new(vrmlname, 'SPOT')
+ bpylamp.energy = intensity
+ bpylamp.distance = radius
+ bpylamp.color = color
+ bpylamp.spot_size = cutOffAngle
+ if beamWidth > cutOffAngle:
+ bpylamp.spot_blend = 0.0
+ else:
+ if cutOffAngle == 0.0: # this should never happen!
+ bpylamp.spot_blend = 0.5
+ else:
+ bpylamp.spot_blend = beamWidth / cutOffAngle
+
+ # Convert
+
+ # lamps have their direction as -z, y==up
+ mtx = Matrix.Translation(location) * Vector(direction).to_track_quat('-Z', 'Y').to_matrix().to_4x4()
+
+ return bpylamp, mtx
+
+
+def importLamp(node, spec, ancestry, global_matrix):
+ if spec == 'PointLight':
+ bpylamp, mtx = importLamp_PointLight(node, ancestry)
+ elif spec == 'DirectionalLight':
+ bpylamp, mtx = importLamp_DirectionalLight(node, ancestry)
+ elif spec == 'SpotLight':
+ bpylamp, mtx = importLamp_SpotLight(node, ancestry)
+ else:
+ print("Error, not a lamp")
+ raise ValueError
+
+ bpyob = node.blendObject = bpy.data.objects.new("TODO", bpylamp)
+ bpy.context.scene.objects.link(bpyob)
+
+ bpyob.matrix_world = getFinalMatrix(node, mtx, ancestry, global_matrix)
+
+
+def importViewpoint(node, ancestry, global_matrix):
+ name = node.getDefName()
+ if not name:
+ name = 'Viewpoint'
+
+ fieldOfView = node.getFieldAsFloat('fieldOfView', 0.785398, ancestry) # max is documented to be 1.0 but some files have higher.
+ # jump = node.getFieldAsBool('jump', True, ancestry)
+ orientation = node.getFieldAsFloatTuple('orientation', (0.0, 0.0, 1.0, 0.0), ancestry)
+ position = node.getFieldAsFloatTuple('position', (0.0, 0.0, 0.0), ancestry)
+ description = node.getFieldAsString('description', '', ancestry)
+
+ bpycam = bpy.data.cameras.new(name)
+
+ bpycam.angle = fieldOfView
+
+ mtx = Matrix.Translation(Vector(position)) * translateRotation(orientation)
+
+ bpyob = node.blendObject = bpy.data.objects.new(name, bpycam)
+ bpy.context.scene.objects.link(bpyob)
+ bpyob.matrix_world = getFinalMatrix(node, mtx, ancestry, global_matrix)
+
+
+def importTransform(node, ancestry, global_matrix):
+ name = node.getDefName()
+ if not name:
+ name = 'Transform'
+
+ bpyob = node.blendObject = bpy.data.objects.new(name, None)
+ bpy.context.scene.objects.link(bpyob)
+
+ bpyob.matrix_world = getFinalMatrix(node, None, ancestry, global_matrix)
+
+ # so they are not too annoying
+ bpyob.empty_draw_type = 'PLAIN_AXES'
+ bpyob.empty_draw_size = 0.2
+
+
+#def importTimeSensor(node):
+def action_fcurve_ensure(action, data_path, array_index):
+ for fcu in action.fcurves:
+ if fcu.data_path == data_path and fcu.array_index == array_index:
+ return fcu
+
+ return action.fcurves.new(data_path=data_path, index=array_index)
+
+
+def translatePositionInterpolator(node, action, ancestry):
+ key = node.getFieldAsArray('key', 0, ancestry)
+ keyValue = node.getFieldAsArray('keyValue', 3, ancestry)
+
+ loc_x = action_fcurve_ensure(action, "location", 0)
+ loc_y = action_fcurve_ensure(action, "location", 1)
+ loc_z = action_fcurve_ensure(action, "location", 2)
+
+ for i, time in enumerate(key):
+ try:
+ x, y, z = keyValue[i]
+ except:
+ continue
+
+ loc_x.keyframe_points.insert(time, x)
+ loc_y.keyframe_points.insert(time, y)
+ loc_z.keyframe_points.insert(time, z)
+
+ for fcu in (loc_x, loc_y, loc_z):
+ for kf in fcu.keyframe_points:
+ kf.interpolation = 'LINEAR'
+
+
+def translateOrientationInterpolator(node, action, ancestry):
+ key = node.getFieldAsArray('key', 0, ancestry)
+ keyValue = node.getFieldAsArray('keyValue', 4, ancestry)
+
+ rot_x = action_fcurve_ensure(action, "rotation_euler", 0)
+ rot_y = action_fcurve_ensure(action, "rotation_euler", 1)
+ rot_z = action_fcurve_ensure(action, "rotation_euler", 2)
+
+ for i, time in enumerate(key):
+ try:
+ x, y, z, w = keyValue[i]
+ except:
+ continue
+
+ mtx = translateRotation((x, y, z, w))
+ eul = mtx.to_euler()
+ rot_x.keyframe_points.insert(time, eul.x)
+ rot_y.keyframe_points.insert(time, eul.y)
+ rot_z.keyframe_points.insert(time, eul.z)
+
+ for fcu in (rot_x, rot_y, rot_z):
+ for kf in fcu.keyframe_points:
+ kf.interpolation = 'LINEAR'
+
+
+# Untested!
+def translateScalarInterpolator(node, action, ancestry):
+ key = node.getFieldAsArray('key', 0, ancestry)
+ keyValue = node.getFieldAsArray('keyValue', 4, ancestry)
+
+ sca_x = action_fcurve_ensure(action, "scale", 0)
+ sca_y = action_fcurve_ensure(action, "scale", 1)
+ sca_z = action_fcurve_ensure(action, "scale", 2)
+
+ for i, time in enumerate(key):
+ try:
+ x, y, z = keyValue[i]
+ except:
+ continue
+
+ sca_x.keyframe_points.new(time, x)
+ sca_y.keyframe_points.new(time, y)
+ sca_z.keyframe_points.new(time, z)
+
+
+def translateTimeSensor(node, action, ancestry):
+ '''
+ Apply a time sensor to an action, VRML has many combinations of loop/start/stop/cycle times
+ to give different results, for now just do the basics
+ '''
+
+ # XXX25 TODO
+ if 1:
+ return
+
+ time_cu = action.addCurve('Time')
+ time_cu.interpolation = Blender.IpoCurve.InterpTypes.LINEAR
+
+ cycleInterval = node.getFieldAsFloat('cycleInterval', None, ancestry)
+
+ startTime = node.getFieldAsFloat('startTime', 0.0, ancestry)
+ stopTime = node.getFieldAsFloat('stopTime', 250.0, ancestry)
+
+ if cycleInterval != None:
+ stopTime = startTime + cycleInterval
+
+ loop = node.getFieldAsBool('loop', False, ancestry)
+
+ time_cu.append((1 + startTime, 0.0))
+ time_cu.append((1 + stopTime, 1.0 / 10.0)) # anoying, the UI uses /10
+
+ if loop:
+ time_cu.extend = Blender.IpoCurve.ExtendTypes.CYCLIC # or - EXTRAP, CYCLIC_EXTRAP, CONST,
+
+
+def importRoute(node, ancestry):
+ '''
+ Animation route only at the moment
+ '''
+
+ if not hasattr(node, 'fields'):
+ return
+
+ routeIpoDict = node.getRouteIpoDict()
+
+ def getIpo(id):
+ try:
+ action = routeIpoDict[id]
+ except:
+ action = routeIpoDict[id] = bpy.data.actions.new('web3d_ipo')
+ return action
+
+ # for getting definitions
+ defDict = node.getDefDict()
+ '''
+ Handles routing nodes to eachother
+
+ROUTE vpPI.value_changed TO champFly001.set_position
+ROUTE vpOI.value_changed TO champFly001.set_orientation
+ROUTE vpTs.fraction_changed TO vpPI.set_fraction
+ROUTE vpTs.fraction_changed TO vpOI.set_fraction
+ROUTE champFly001.bindTime TO vpTs.set_startTime
+ '''
+
+ #from_id, from_type = node.id[1].split('.')
+ #to_id, to_type = node.id[3].split('.')
+
+ #value_changed
+ set_position_node = None
+ set_orientation_node = None
+ time_node = None
+
+ for field in node.fields:
+ if field and field[0] == 'ROUTE':
+ try:
+ from_id, from_type = field[1].split('.')
+ to_id, to_type = field[3].split('.')
+ except:
+ print("Warning, invalid ROUTE", field)
+ continue
+
+ if from_type == 'value_changed':
+ if to_type == 'set_position':
+ action = getIpo(to_id)
+ set_data_from_node = defDict[from_id]
+ translatePositionInterpolator(set_data_from_node, action, ancestry)
+
+ if to_type in {'set_orientation', 'rotation'}:
+ action = getIpo(to_id)
+ set_data_from_node = defDict[from_id]
+ translateOrientationInterpolator(set_data_from_node, action, ancestry)
+
+ if to_type == 'set_scale':
+ action = getIpo(to_id)
+ set_data_from_node = defDict[from_id]
+ translateScalarInterpolator(set_data_from_node, action, ancestry)
+
+ elif from_type == 'bindTime':
+ action = getIpo(from_id)
+ time_node = defDict[to_id]
+ translateTimeSensor(time_node, action, ancestry)
+
+
+def load_web3d(path,
+ PREF_FLAT=False,
+ PREF_CIRCLE_DIV=16,
+ global_matrix=None,
+ HELPER_FUNC=None,
+ ):
+
+ # Used when adding blender primitives
+ GLOBALS['CIRCLE_DETAIL'] = PREF_CIRCLE_DIV
+
+ #root_node = vrml_parse('/_Cylinder.wrl')
+ if path.lower().endswith('.x3d'):
+ root_node, msg = x3d_parse(path)
+ else:
+ root_node, msg = vrml_parse(path)
+
+ if not root_node:
+ print(msg)
+ return
+
+ if global_matrix is None:
+ global_matrix = Matrix()
+
+ # fill with tuples - (node, [parents-parent, parent])
+ all_nodes = root_node.getSerialized([], [])
+
+ for node, ancestry in all_nodes:
+ #if 'castle.wrl' not in node.getFilename():
+ # continue
+
+ spec = node.getSpec()
+ '''
+ prefix = node.getPrefix()
+ if prefix=='PROTO':
+ pass
+ else
+ '''
+ if HELPER_FUNC and HELPER_FUNC(node, ancestry):
+ # Note, include this function so the VRML/X3D importer can be extended
+ # by an external script. - gets first pick
+ pass
+ if spec == 'Shape':
+ importShape(node, ancestry, global_matrix)
+ elif spec in {'PointLight', 'DirectionalLight', 'SpotLight'}:
+ importLamp(node, spec, ancestry, global_matrix)
+ elif spec == 'Viewpoint':
+ importViewpoint(node, ancestry, global_matrix)
+ elif spec == 'Transform':
+ # Only use transform nodes when we are not importing a flat object hierarchy
+ if PREF_FLAT == False:
+ importTransform(node, ancestry, global_matrix)
+ '''
+ # These are delt with later within importRoute
+ elif spec=='PositionInterpolator':
+ action = bpy.data.ipos.new('web3d_ipo', 'Object')
+ translatePositionInterpolator(node, action)
+ '''
+
+ # After we import all nodes, route events - anim paths
+ for node, ancestry in all_nodes:
+ importRoute(node, ancestry)
+
+ for node, ancestry in all_nodes:
+ if node.isRoot():
+ # we know that all nodes referenced from will be in
+ # routeIpoDict so no need to run node.getDefDict() for every node.
+ routeIpoDict = node.getRouteIpoDict()
+ defDict = node.getDefDict()
+
+ for key, action in routeIpoDict.items():
+
+ # Assign anim curves
+ node = defDict[key]
+ if node.blendObject is None: # Add an object if we need one for animation
+ node.blendObject = bpy.data.objects.new('AnimOb', None) # , name)
+ bpy.context.scene.objects.link(node.blendObject)
+
+ if node.blendObject.animation_data is None:
+ node.blendObject.animation_data_create()
+
+ node.blendObject.animation_data.action = action
+
+ # Add in hierarchy
+ if PREF_FLAT == False:
+ child_dict = {}
+ for node, ancestry in all_nodes:
+ if node.blendObject:
+ blendObject = None
+
+ # Get the last parent
+ i = len(ancestry)
+ while i:
+ i -= 1
+ blendObject = ancestry[i].blendObject
+ if blendObject:
+ break
+
+ if blendObject:
+ # Parent Slow, - 1 liner but works
+ # blendObject.makeParent([node.blendObject], 0, 1)
+
+ # Parent FAST
+ try:
+ child_dict[blendObject].append(node.blendObject)
+ except:
+ child_dict[blendObject] = [node.blendObject]
+
+ # Parent
+ for parent, children in child_dict.items():
+ for c in children:
+ c.parent = parent
+
+ # update deps
+ bpy.context.scene.update()
+ del child_dict
+
+
+def load(operator, context, filepath="", global_matrix=None):
+
+ load_web3d(filepath,
+ PREF_FLAT=True,
+ PREF_CIRCLE_DIV=16,
+ global_matrix=global_matrix,
+ )
+
+ return {'FINISHED'}