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 5 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. 28
      korman/plasma_attributes.py
  4. 19
      korman/properties/modifiers/gui.py

111
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"})

16
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

28
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

19
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:

Loading…
Cancel
Save