Browse Source

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.
pull/128/head
Adam Johnson 6 years ago
parent
commit
b6c9551883
Signed by: Hoikas
GPG Key ID: 0B6515D6FF6F271E
  1. 111
      korman/nodes/node_python.py
  2. 16
      korman/operators/op_nodes.py
  3. 16
      korman/plasma_attributes.py
  4. 1
      korman/properties/modifiers/gui.py

111
korman/nodes/node_python.py

@ -15,10 +15,12 @@
import bpy import bpy
from bpy.props import * from bpy.props import *
from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from PyHSPlasma import * from PyHSPlasma import *
from .node_core import * from .node_core import *
from .node_deprecated import PlasmaVersionedNode
from .. import idprops from .. import idprops
_single_user_attribs = { _single_user_attribs = {
@ -166,33 +168,40 @@ class PlasmaAttribute(bpy.types.PropertyGroup):
simple_value = property(_get_simple_value, _set_simple_value) 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_category = "PYTHON"
bl_idname = "PlasmaPythonFileNode" bl_idname = "PlasmaPythonFileNode"
bl_label = "Python File" bl_label = "Python File"
bl_width_default = 210 bl_width_default = 290
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
def _update_pyfile(self, context): 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.attributes.clear()
self.inputs.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", filename = StringProperty(name="File Name",
description="Python Filename") description="Python Filename",
filepath = StringProperty(update=_update_pyfile, update=_update_pyfile)
options={"HIDDEN"}) filepath = StringProperty(options={"HIDDEN"})
text_id = PointerProperty(name="Script File", text_id = PointerProperty(name="Script File",
description="Script file datablock", 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"}) attributes = CollectionProperty(type=PlasmaAttribute, options={"HIDDEN"})
no_update = BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"}) 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 } return { i.attribute_id: i for i in self.attributes }
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
row = layout.row(align=True) main_row = layout.row(align=True)
if self.filename: row = main_row.row(align=True)
row.prop(self, "filename") row.alert = self.text_id is None and bool(self.filename)
try: row.prop(self, "text_id", text="Script")
if Path(self.filepath).exists(): # open operator
operator = row.operator("node.plasma_attributes_to_node", icon="FILE_REFRESH", text="") operator = main_row.operator("file.plasma_file_picker", icon="FILESEL", 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)
operator.filter_glob = "*.py" operator.filter_glob = "*.py"
operator.data_path = self.node_path operator.data_path = self.node_path
operator.filepath_property = "filepath"
operator.filename_property = "filename" 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): def get_key(self, exporter, so):
return exporter.mgr.find_create_key(plPythonFileMod, name=self.key_name, so=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: if not is_init and new_pos != old_pos:
self.inputs.move(old_pos, new_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): def update(self):
if self.no_update: if self.no_update:
return 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 # 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... # happens in PlasmaAttribNodeBase, but we can link much more than those node types...
toasty_sockets = [] toasty_sockets = []
@ -315,6 +344,26 @@ class PlasmaPythonFileNode(PlasmaNodeBase, bpy.types.Node):
while len(unconnected) > 1: while len(unconnected) > 1:
self.inputs.remove(unconnected.pop()) 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): class PlasmaPythonFileNodeSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
attribute_id = IntProperty(options={"HIDDEN"}) attribute_id = IntProperty(options={"HIDDEN"})

16
korman/operators/op_nodes.py

@ -26,6 +26,7 @@ class NodeOperator:
class SelectFileOperator(NodeOperator, bpy.types.Operator): class SelectFileOperator(NodeOperator, bpy.types.Operator):
bl_idname = "file.plasma_file_picker" bl_idname = "file.plasma_file_picker"
bl_label = "Select" bl_label = "Select"
bl_description = "Load a file"
filter_glob = StringProperty(options={"HIDDEN"}) filter_glob = StringProperty(options={"HIDDEN"})
filepath = StringProperty(subtype="FILE_PATH") filepath = StringProperty(subtype="FILE_PATH")
@ -41,6 +42,11 @@ class SelectFileOperator(NodeOperator, bpy.types.Operator):
setattr(dest, self.filepath_property, self.filepath) setattr(dest, self.filepath_property, self.filepath)
if self.filename_property: if self.filename_property:
setattr(dest, self.filename_property, self.filename) 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"} return {"FINISHED"}
def invoke(self, context, event): def invoke(self, context, event):
@ -91,15 +97,17 @@ pyAttribArgMap= {
class PlPyAttributeNodeOperator(NodeOperator, bpy.types.Operator): class PlPyAttributeNodeOperator(NodeOperator, bpy.types.Operator):
bl_idname = "node.plasma_attributes_to_node" 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"} bl_options = {"INTERNAL"}
python_path = StringProperty(subtype="FILE_PATH") text_path = StringProperty()
node_path = StringProperty() node_path = StringProperty()
def execute(self, context): def execute(self, context):
from ..plasma_attributes import get_attributes from ..plasma_attributes import get_attributes_from_str
attribs = get_attributes(self.python_path) text_id = bpy.data.texts[self.text_path]
attribs = get_attributes_from_str(text_id.as_string())
node = eval(self.node_path) node = eval(self.node_path)
node_attrib_map = node.attribute_map node_attrib_map = node.attribute_map

16
korman/plasma_attributes.py

@ -102,20 +102,22 @@ class PlasmaAttributeVisitor(ast.NodeVisitor):
ast.NodeVisitor.generic_visit(self, node) 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 """Scan the file for assignments matching our regex, let our visitor parse them, and return the
file's ptAttribs, if any.""" file's ptAttribs, if any."""
attribs = None with open(str(filepath)) as script:
with open(str(scriptFile)) as script: return get_attributes_from_str(script.read())
results = funcregex.findall(script.read())
def get_attributes_from_str(code):
results = funcregex.findall(code)
if results: if results:
# We'll fake the ptAttribs being all alone in a module... # We'll fake the ptAttribs being all alone in a module...
assigns = ast.parse("\n".join(results)) assigns = ast.parse("\n".join(results))
v = PlasmaAttributeVisitor() v = PlasmaAttributeVisitor()
v.visit(assigns) v.visit(assigns)
if v._attributes: if v._attributes:
attribs = v._attributes return v._attributes
return attribs return {}
if __name__ == "__main__": if __name__ == "__main__":
import json import json
@ -129,7 +131,7 @@ if __name__ == "__main__":
files = Path(readpath).glob("*.py") files = Path(readpath).glob("*.py")
ptAttribs = {} ptAttribs = {}
for scriptFile in files: for scriptFile in files:
attribs = get_attributes(scriptFile) attribs = get_attributes_from_file(scriptFile)
if attribs: if attribs:
ptAttribs[scriptFile.stem] = attribs ptAttribs[scriptFile.stem] = attribs

1
korman/properties/modifiers/gui.py

@ -180,6 +180,7 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
# Assign journal script based on target version # Assign journal script based on target version
journal_pfm = journal_pfms[version] journal_pfm = journal_pfms[version]
journalnode = nodes.new("PlasmaPythonFileNode") journalnode = nodes.new("PlasmaPythonFileNode")
with journalnode.NoUpdate():
journalnode.filename = journal_pfm["filename"] journalnode.filename = journal_pfm["filename"]
# Manually add required attributes to the PFM # Manually add required attributes to the PFM

Loading…
Cancel
Save