123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410 |
- # ***** 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>
- import bpy
- def build_property_typemap(skip_classes, skip_typemap):
- property_typemap = {}
- for attr in dir(bpy.types):
- cls = getattr(bpy.types, attr)
- if issubclass(cls, skip_classes):
- continue
- # # to support skip-save we can't get all props
- # properties = cls.bl_rna.properties.keys()
- properties = []
- for prop_id, prop in cls.bl_rna.properties.items():
- if not prop.is_skip_save:
- properties.append(prop_id)
- properties.remove("rna_type")
- property_typemap[attr] = properties
- if skip_typemap:
- for cls_name, properties_blacklist in skip_typemap.items():
- properties = property_typemap.get(cls_name)
- if properties is not None:
- for prop_id in properties_blacklist:
- try:
- properties.remove(prop_id)
- except:
- print("skip_typemap unknown prop_id '%s.%s'" % (cls_name, prop_id))
- else:
- print("skip_typemap unknown class '%s'" % cls_name)
- return property_typemap
- def print_ln(data):
- print(data, end="")
- def rna2xml(
- fw=print_ln,
- root_node="",
- root_rna=None, # must be set
- root_rna_skip=set(),
- root_ident="",
- ident_val=" ",
- skip_classes=(
- bpy.types.Operator,
- bpy.types.Panel,
- bpy.types.KeyingSet,
- bpy.types.Header,
- bpy.types.PropertyGroup,
- ),
- skip_typemap=None,
- pretty_format=True,
- method='DATA',
- ):
- from xml.sax.saxutils import quoteattr
- property_typemap = build_property_typemap(skip_classes, skip_typemap)
- # don't follow properties of this type, just reference them by name
- # they MUST have a unique 'name' property.
- # 'ID' covers most types
- referenced_classes = (
- bpy.types.ID,
- bpy.types.Bone,
- bpy.types.ActionGroup,
- bpy.types.PoseBone,
- bpy.types.Node,
- bpy.types.Sequence,
- )
- def number_to_str(val, val_type):
- if val_type == int:
- return "%d" % val
- elif val_type == float:
- return "%.6g" % val
- elif val_type == bool:
- return "TRUE" if val else "FALSE"
- else:
- raise NotImplemented("this type is not a number %s" % val_type)
- def rna2xml_node(ident, value, parent):
- ident_next = ident + ident_val
- # divide into attrs and nodes.
- node_attrs = []
- nodes_items = []
- nodes_lists = []
- value_type = type(value)
- if issubclass(value_type, skip_classes):
- return
- # XXX, fixme, pointcache has eternal nested pointer to its self.
- if value == parent:
- return
- value_type_name = value_type.__name__
- for prop in property_typemap[value_type_name]:
- subvalue = getattr(value, prop)
- subvalue_type = type(subvalue)
- if subvalue_type in {int, bool, float}:
- node_attrs.append("%s=\"%s\"" % (prop, number_to_str(subvalue, subvalue_type)))
- elif subvalue_type is str:
- node_attrs.append("%s=%s" % (prop, quoteattr(subvalue)))
- elif subvalue_type is set:
- node_attrs.append("%s=%s" % (prop, quoteattr("{" + ",".join(list(subvalue)) + "}")))
- elif subvalue is None:
- node_attrs.append("%s=\"NONE\"" % prop)
- elif issubclass(subvalue_type, referenced_classes):
- # special case, ID's are always referenced.
- node_attrs.append("%s=%s" % (prop, quoteattr(subvalue_type.__name__ + "::" + subvalue.name)))
- else:
- try:
- subvalue_ls = list(subvalue)
- except:
- subvalue_ls = None
- if subvalue_ls is None:
- nodes_items.append((prop, subvalue, subvalue_type))
- else:
- # check if the list contains native types
- subvalue_rna = value.path_resolve(prop, False)
- if type(subvalue_rna).__name__ == "bpy_prop_array":
- # check if this is a 0-1 color (rgb, rgba)
- # in that case write as a hexadecimal
- prop_rna = value.bl_rna.properties[prop]
- if (prop_rna.subtype == 'COLOR_GAMMA' and
- prop_rna.hard_min == 0.0 and
- prop_rna.hard_max == 1.0 and
- prop_rna.array_length in {3, 4}):
- # -----
- # color
- array_value = "#" + "".join(("%.2x" % int(v * 255) for v in subvalue_rna))
- else:
- # default
- def str_recursive(s):
- subsubvalue_type = type(s)
- if subsubvalue_type in {int, float, bool}:
- return number_to_str(s, subsubvalue_type)
- else:
- return " ".join([str_recursive(si) for si in s])
- array_value = " ".join(str_recursive(v) for v in subvalue_rna)
- node_attrs.append("%s=\"%s\"" % (prop, array_value))
- else:
- nodes_lists.append((prop, subvalue_ls, subvalue_type))
- # declare + attributes
- if pretty_format:
- if node_attrs:
- fw("%s<%s\n" % (ident, value_type_name))
- for node_attr in node_attrs:
- fw("%s%s\n" % (ident_next, node_attr))
- fw("%s>\n" % (ident_next,))
- else:
- fw("%s<%s>\n" % (ident, value_type_name))
- else:
- fw("%s<%s %s>\n" % (ident, value_type_name, " ".join(node_attrs)))
- # unique members
- for prop, subvalue, subvalue_type in nodes_items:
- fw("%s<%s>\n" % (ident_next, prop)) # XXX, this is awkward, how best to solve?
- rna2xml_node(ident_next + ident_val, subvalue, value)
- fw("%s</%s>\n" % (ident_next, prop)) # XXX, need to check on this.
- # list members
- for prop, subvalue, subvalue_type in nodes_lists:
- fw("%s<%s>\n" % (ident_next, prop))
- for subvalue_item in subvalue:
- if subvalue_item is not None:
- rna2xml_node(ident_next + ident_val, subvalue_item, value)
- fw("%s</%s>\n" % (ident_next, prop))
- fw("%s</%s>\n" % (ident, value_type_name))
- # -------------------------------------------------------------------------
- # needs re-working to be generic
- if root_node:
- fw("%s<%s>\n" % (root_ident, root_node))
- # bpy.data
- if method == 'DATA':
- ident = root_ident + ident_val
- for attr in dir(root_rna):
- # exceptions
- if attr.startswith("_"):
- continue
- elif attr in root_rna_skip:
- continue
- value = getattr(root_rna, attr)
- try:
- ls = value[:]
- except:
- ls = None
- if type(ls) == list:
- fw("%s<%s>\n" % (ident, attr))
- for blend_id in ls:
- rna2xml_node(ident + ident_val, blend_id, None)
- fw("%s</%s>\n" % (ident_val, attr))
- # any attribute
- elif method == 'ATTR':
- rna2xml_node(root_ident, root_rna, None)
- if root_node:
- fw("%s</%s>\n" % (root_ident, root_node))
- def xml2rna(root_xml,
- root_rna=None, # must be set
- ):
- def rna2xml_node(xml_node, value):
- # print("evaluating:", xml_node.nodeName)
- # ---------------------------------------------------------------------
- # Simple attributes
- for attr in xml_node.attributes.keys():
- # print(" ", attr)
- subvalue = getattr(value, attr, Ellipsis)
- if subvalue is Ellipsis:
- print("%s.%s not found" % (type(value).__name__, attr))
- else:
- value_xml = xml_node.attributes[attr].value
- subvalue_type = type(subvalue)
- tp_name = 'UNKNOWN'
- if subvalue_type == float:
- value_xml_coerce = float(value_xml)
- tp_name = 'FLOAT'
- elif subvalue_type == int:
- value_xml_coerce = int(value_xml)
- tp_name = 'INT'
- elif subvalue_type == bool:
- value_xml_coerce = {'TRUE': True, 'FALSE': False}[value_xml]
- tp_name = 'BOOL'
- elif subvalue_type == str:
- value_xml_coerce = value_xml
- tp_name = 'STR'
- elif hasattr(subvalue, "__len__"):
- if value_xml.startswith("#"):
- # read hexadecimal value as float array
- value_xml_split = value_xml[1:]
- value_xml_coerce = [int(value_xml_split[i:i + 2], 16) /
- 255 for i in range(0, len(value_xml_split), 2)]
- del value_xml_split
- else:
- value_xml_split = value_xml.split()
- try:
- value_xml_coerce = [int(v) for v in value_xml_split]
- except ValueError:
- try:
- value_xml_coerce = [float(v) for v in value_xml_split]
- except ValueError: # bool vector property
- value_xml_coerce = [{'TRUE': True, 'FALSE': False}[v] for v in value_xml_split]
- del value_xml_split
- tp_name = 'ARRAY'
- # print(" %s.%s (%s) --- %s" % (type(value).__name__, attr, tp_name, subvalue_type))
- try:
- setattr(value, attr, value_xml_coerce)
- except ValueError:
- # size mismatch
- val = getattr(value, attr)
- if len(val) < len(value_xml_coerce):
- setattr(value, attr, value_xml_coerce[:len(val)])
- else:
- setattr(value, attr, list(value_xml_coerce) + list(val)[len(value_xml_coerce):])
- # ---------------------------------------------------------------------
- # Complex attributes
- for child_xml in xml_node.childNodes:
- if child_xml.nodeType == child_xml.ELEMENT_NODE:
- # print()
- # print(child_xml.nodeName)
- subvalue = getattr(value, child_xml.nodeName, None)
- if subvalue is not None:
- elems = []
- for child_xml_real in child_xml.childNodes:
- if child_xml_real.nodeType == child_xml_real.ELEMENT_NODE:
- elems.append(child_xml_real)
- del child_xml_real
- if hasattr(subvalue, "__len__"):
- # Collection
- if len(elems) != len(subvalue):
- print("Size Mismatch! collection:", child_xml.nodeName)
- else:
- for i in range(len(elems)):
- child_xml_real = elems[i]
- subsubvalue = subvalue[i]
- if child_xml_real is None or subsubvalue is None:
- print("None found %s - %d collection:", (child_xml.nodeName, i))
- else:
- rna2xml_node(child_xml_real, subsubvalue)
- else:
- # print(elems)
- if len(elems) == 1:
- # sub node named by its type
- child_xml_real, = elems
- # print(child_xml_real, subvalue)
- rna2xml_node(child_xml_real, subvalue)
- else:
- # empty is valid too
- pass
- rna2xml_node(root_xml, root_rna)
- # -----------------------------------------------------------------------------
- # Utility function used by presets.
- # The idea is you can run a preset like a script with a few args.
- #
- # This roughly matches the operator 'bpy.ops.script.python_file_run'
- def _get_context_val(context, path):
- path_full = "context." + path
- try:
- value = eval(path_full)
- except:
- import traceback
- traceback.print_exc()
- print("Error: %r could not be found" % path_full)
- value = Ellipsis
- return value
- def xml_file_run(context, filepath, rna_map):
- import xml.dom.minidom
- xml_nodes = xml.dom.minidom.parse(filepath)
- bpy_xml = xml_nodes.getElementsByTagName("bpy")[0]
- for rna_path, xml_tag in rna_map:
- # first get xml
- # TODO, error check
- xml_node = bpy_xml.getElementsByTagName(xml_tag)[0]
- value = _get_context_val(context, rna_path)
- if value is not Ellipsis and value is not None:
- print(" loading XML: %r -> %r" % (filepath, rna_path))
- xml2rna(xml_node, root_rna=value)
- def xml_file_write(context, filepath, rna_map, skip_typemap=None):
- file = open(filepath, "w", encoding="utf-8")
- fw = file.write
- fw("<bpy>\n")
- for rna_path, _xml_tag in rna_map:
- # xml_tag is ignored, we get this from the rna
- value = _get_context_val(context, rna_path)
- rna2xml(fw,
- root_rna=value,
- method='ATTR',
- root_ident=" ",
- ident_val=" ",
- skip_typemap=skip_typemap,
- )
- fw("</bpy>\n")
- file.close()
|