From b6c9551883f828ec0843a517525da85d92ed1a8a Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 6 Jan 2019 20:27:14 -0500 Subject: [PATCH] PythonFile Node v2 Version 2 of the python file node is now backed by a `bpy.types.Text` datablock in the case of a file whose attributes are updated from a backing file. --- korman/nodes/node_python.py | 111 +++++++++++++++++++++-------- korman/operators/op_nodes.py | 16 +++-- korman/plasma_attributes.py | 28 ++++---- korman/properties/modifiers/gui.py | 19 ++--- 4 files changed, 117 insertions(+), 57 deletions(-) diff --git a/korman/nodes/node_python.py b/korman/nodes/node_python.py index 96c9dcf..8d9b646 100644 --- a/korman/nodes/node_python.py +++ b/korman/nodes/node_python.py @@ -15,10 +15,12 @@ import bpy from bpy.props import * +from contextlib import contextmanager from pathlib import Path from PyHSPlasma import * from .node_core import * +from .node_deprecated import PlasmaVersionedNode from .. import idprops _single_user_attribs = { @@ -166,33 +168,40 @@ class PlasmaAttribute(bpy.types.PropertyGroup): simple_value = property(_get_simple_value, _set_simple_value) -class PlasmaPythonFileNode(PlasmaNodeBase, bpy.types.Node): +class PlasmaPythonFileNode(PlasmaVersionedNode, bpy.types.Node): bl_category = "PYTHON" bl_idname = "PlasmaPythonFileNode" bl_label = "Python File" - bl_width_default = 210 - - class _NoUpdate: - def __init__(self, node): - self._node = node - def __enter__(self): - self._node.no_update = True - def __exit__(self, type, value, traceback): - self._node.no_update = False + bl_width_default = 290 def _update_pyfile(self, context): - with self._NoUpdate(self) as _hack: + if self.no_update: + return + text_id = bpy.data.texts.get(self.filename, None) + if text_id: + self.text_id = text_id + + def _update_pytext(self, context): + if self.no_update: + return + with self.NoUpdate(): + self.filename = self.text_id.name self.attributes.clear() self.inputs.clear() - bpy.ops.node.plasma_attributes_to_node(node_path=self.node_path, python_path=self.filepath) + if self.text_id is not None: + bpy.ops.node.plasma_attributes_to_node(node_path=self.node_path, text_path=self.text_id.name) filename = StringProperty(name="File Name", - description="Python Filename") - filepath = StringProperty(update=_update_pyfile, - options={"HIDDEN"}) + description="Python Filename", + update=_update_pyfile) + filepath = StringProperty(options={"HIDDEN"}) text_id = PointerProperty(name="Script File", description="Script file datablock", - type=bpy.types.Text) + type=bpy.types.Text, + update=_update_pytext) + + # This property exists for UI purposes ONLY + package = BoolProperty(options={"HIDDEN", "SKIP_SAVE"}) attributes = CollectionProperty(type=PlasmaAttribute, options={"HIDDEN"}) no_update = BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"}) @@ -202,23 +211,35 @@ class PlasmaPythonFileNode(PlasmaNodeBase, bpy.types.Node): return { i.attribute_id: i for i in self.attributes } def draw_buttons(self, context, layout): - row = layout.row(align=True) - if self.filename: - row.prop(self, "filename") - try: - if Path(self.filepath).exists(): - operator = row.operator("node.plasma_attributes_to_node", icon="FILE_REFRESH", text="") - operator.python_path = self.filepath - operator.node_path = self.node_path - except OSError: - pass - - op_text = "" if self.filename else "Select" - operator = row.operator("file.plasma_file_picker", icon="SCRIPT", text=op_text) + main_row = layout.row(align=True) + row = main_row.row(align=True) + row.alert = self.text_id is None and bool(self.filename) + row.prop(self, "text_id", text="Script") + # open operator + operator = main_row.operator("file.plasma_file_picker", icon="FILESEL", text="") operator.filter_glob = "*.py" operator.data_path = self.node_path - operator.filepath_property = "filepath" operator.filename_property = "filename" + # package button + row = main_row.row(align=True) + if self.text_id is not None: + row.enabled = True + icon = "PACKAGE" if self.text_id.plasma_text.package else "UGLYPACKAGE" + row.prop(self.text_id.plasma_text, "package", icon=icon, text="") + else: + row.enabled = False + row.prop(self, "package", text="", icon="UGLYPACKAGE") + # rescan operator + row = main_row.row(align=True) + row.enabled = self.text_id is not None + operator = row.operator("node.plasma_attributes_to_node", icon="FILE_REFRESH", text="") + if self.text_id is not None: + operator.text_path = self.text_id.name + operator.node_path = self.node_path + + # This could happen on an upgrade + if self.text_id is None and self.filename: + layout.label(text="Script '{}' is not loaded in Blender".format(self.filename), icon="ERROR") def get_key(self, exporter, so): return exporter.mgr.find_create_key(plPythonFileMod, name=self.key_name, so=so) @@ -276,10 +297,18 @@ class PlasmaPythonFileNode(PlasmaNodeBase, bpy.types.Node): if not is_init and new_pos != old_pos: self.inputs.move(old_pos, new_pos) + @contextmanager + def NoUpdate(self): + self.no_update = True + try: + yield self + finally: + self.no_update = False + def update(self): if self.no_update: return - with self._NoUpdate(self) as _no_recurse: + with self.NoUpdate(): # First, we really want to make sure our junk matches up. Yes, this does dupe what # happens in PlasmaAttribNodeBase, but we can link much more than those node types... toasty_sockets = [] @@ -315,6 +344,26 @@ class PlasmaPythonFileNode(PlasmaNodeBase, bpy.types.Node): while len(unconnected) > 1: self.inputs.remove(unconnected.pop()) + @property + def latest_version(self): + return 2 + + def upgrade(self): + # In version 1 of this node, Python scripts were referenced by their filename in the + # python package and by their path on the local machine. This created an undue dependency + # on the artist's environment. In version 2, we will use Blender's text data blocks to back + # Python scripts. It is still legal to export Python File nodes that are not backed by a script. + if self.version == 1: + text_id = bpy.data.texts.get(self.filename, None) + if text_id is None: + path = Path(self.filepath) + if path.exists(): + text_id = bpy.data.texts.load(self.filepath) + with self.NoUpdate(): + self.text_id = text_id + self.property_unset("filepath") + self.version = 2 + class PlasmaPythonFileNodeSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket): attribute_id = IntProperty(options={"HIDDEN"}) diff --git a/korman/operators/op_nodes.py b/korman/operators/op_nodes.py index 64e0391..6fa1e96 100644 --- a/korman/operators/op_nodes.py +++ b/korman/operators/op_nodes.py @@ -26,6 +26,7 @@ class NodeOperator: class SelectFileOperator(NodeOperator, bpy.types.Operator): bl_idname = "file.plasma_file_picker" bl_label = "Select" + bl_description = "Load a file" filter_glob = StringProperty(options={"HIDDEN"}) filepath = StringProperty(subtype="FILE_PATH") @@ -41,6 +42,11 @@ class SelectFileOperator(NodeOperator, bpy.types.Operator): setattr(dest, self.filepath_property, self.filepath) if self.filename_property: setattr(dest, self.filename_property, self.filename) + + if bpy.data.texts.get(self.filename, None) is None: + bpy.data.texts.load(self.filepath) + else: + self.report({"WARNING"}, "A file named '{}' is already loaded. It will be used.".format(self.filename)) return {"FINISHED"} def invoke(self, context, event): @@ -91,15 +97,17 @@ pyAttribArgMap= { class PlPyAttributeNodeOperator(NodeOperator, bpy.types.Operator): bl_idname = "node.plasma_attributes_to_node" - bl_label = "R" + bl_label = "Refresh Sockets" + bl_description = "Refresh the Python File node's attribute sockets" bl_options = {"INTERNAL"} - python_path = StringProperty(subtype="FILE_PATH") + text_path = StringProperty() node_path = StringProperty() def execute(self, context): - from ..plasma_attributes import get_attributes - attribs = get_attributes(self.python_path) + from ..plasma_attributes import get_attributes_from_str + text_id = bpy.data.texts[self.text_path] + attribs = get_attributes_from_str(text_id.as_string()) node = eval(self.node_path) node_attrib_map = node.attribute_map diff --git a/korman/plasma_attributes.py b/korman/plasma_attributes.py index b2dfd34..70408fb 100644 --- a/korman/plasma_attributes.py +++ b/korman/plasma_attributes.py @@ -102,20 +102,22 @@ class PlasmaAttributeVisitor(ast.NodeVisitor): ast.NodeVisitor.generic_visit(self, node) -def get_attributes(scriptFile): +def get_attributes_from_file(filepath): """Scan the file for assignments matching our regex, let our visitor parse them, and return the file's ptAttribs, if any.""" - attribs = None - with open(str(scriptFile)) as script: - results = funcregex.findall(script.read()) - if results: - # We'll fake the ptAttribs being all alone in a module... - assigns = ast.parse("\n".join(results)) - v = PlasmaAttributeVisitor() - v.visit(assigns) - if v._attributes: - attribs = v._attributes - return attribs + with open(str(filepath)) as script: + return get_attributes_from_str(script.read()) + +def get_attributes_from_str(code): + results = funcregex.findall(code) + if results: + # We'll fake the ptAttribs being all alone in a module... + assigns = ast.parse("\n".join(results)) + v = PlasmaAttributeVisitor() + v.visit(assigns) + if v._attributes: + return v._attributes + return {} if __name__ == "__main__": import json @@ -129,7 +131,7 @@ if __name__ == "__main__": files = Path(readpath).glob("*.py") ptAttribs = {} for scriptFile in files: - attribs = get_attributes(scriptFile) + attribs = get_attributes_from_file(scriptFile) if attribs: ptAttribs[scriptFile.stem] = attribs diff --git a/korman/properties/modifiers/gui.py b/korman/properties/modifiers/gui.py index 62cecdf..2c42f9d 100644 --- a/korman/properties/modifiers/gui.py +++ b/korman/properties/modifiers/gui.py @@ -180,15 +180,16 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz # Assign journal script based on target version journal_pfm = journal_pfms[version] journalnode = nodes.new("PlasmaPythonFileNode") - journalnode.filename = journal_pfm["filename"] - - # Manually add required attributes to the PFM - journal_attribs = journal_pfm["attribs"] - for attr in journal_attribs: - new_attr = journalnode.attributes.add() - new_attr.attribute_id = attr["id"] - new_attr.attribute_type = attr["type"] - new_attr.attribute_name = attr["name"] + with journalnode.NoUpdate(): + journalnode.filename = journal_pfm["filename"] + + # Manually add required attributes to the PFM + journal_attribs = journal_pfm["attribs"] + for attr in journal_attribs: + new_attr = journalnode.attributes.add() + new_attr.attribute_id = attr["id"] + new_attr.attribute_type = attr["type"] + new_attr.attribute_name = attr["name"] journalnode.update() if version == pvPrime: