-
Notifications
You must be signed in to change notification settings - Fork 29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Plugin Logic Proposal #75
Changes from 1 commit
40d5de2
ddfa194
0874cd5
31d2e1d
ae961e0
bd94fd6
b237609
33c8904
ff279d3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
# | ||
# Sample plug-in for QuiltiX which adds import, export, and preview functionality for MaterialX in JSON format | ||
# | ||
import logging, os | ||
from qtpy import QtCore # type: ignore | ||
from qtpy.QtWidgets import ( # type: ignore | ||
QAction, | ||
QTextEdit ) | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
try: | ||
import materialxjson.core as jsoncore | ||
except ImportError: | ||
logger.error("materialxjson.core not found") | ||
|
||
class QuiltiX_JSON_serializer(): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no encapsulation of the core code as is the case with the current way of just overriding the entire application. |
||
|
||
def __init__(self, editor, root): | ||
''' | ||
Initialize the JSON serializer. | ||
''' | ||
self.editor = editor | ||
self.root = root | ||
self.indent = 4 | ||
|
||
# Add JSON menu to the file menu | ||
# ---------------------------------------- | ||
editor.file_menu.addSeparator() | ||
gltfMenu = editor.file_menu.addMenu("JSON") | ||
|
||
# Export JSON item | ||
export_json = QAction("Save JSON...", editor) | ||
export_json.triggered.connect(self.export_json_triggered) | ||
gltfMenu.addAction(export_json) | ||
|
||
# Import JSON item | ||
import_json = QAction("Load JSON...", editor) | ||
import_json.triggered.connect(self.import_json_triggered) | ||
gltfMenu.addAction(import_json) | ||
|
||
# Show JSON text. Does most of export, except does not write to file | ||
show_json_text = QAction("Show as JSON...", editor) | ||
show_json_text.triggered.connect(self.show_json_triggered) | ||
gltfMenu.addAction(show_json_text) | ||
|
||
def set_indent(self, indent): | ||
''' | ||
Set the indent for the JSON output. | ||
''' | ||
self.indent = indent | ||
|
||
def get_json_from_graph(self): | ||
''' | ||
Get the JSON for the given MaterialX document. | ||
''' | ||
doc = self.editor.qx_node_graph.get_current_mx_graph_doc() | ||
if doc: | ||
exporter = jsoncore.MaterialXJson() | ||
json_result = exporter.documentToJSON(doc) | ||
return json_result | ||
return None | ||
|
||
def show_json_triggered(self): | ||
''' | ||
Show the current USD Stage. | ||
kwokcb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
''' | ||
json_result = self.get_json_from_graph() | ||
|
||
# Write JSON UI text box | ||
if json_result: | ||
text = jsoncore.Util.jsonToJSONString(json_result, self.indent) | ||
self.show_text_box(text, 'JSON Representation') | ||
|
||
def export_json_triggered(self): | ||
''' | ||
Export the current graph to a JSON file. | ||
''' | ||
start_path = self.editor.mx_selection_path | ||
if not start_path: | ||
start_path = self.editor.geometry_selection_path | ||
|
||
if not start_path: | ||
start_path = os.path.join(self.root, "resources", "materials") | ||
|
||
path = self.editor.request_filepath( | ||
title="Save JSON file", start_path=start_path, file_filter="JSON files (*.json)", mode="save", | ||
) | ||
|
||
if not path: | ||
return | ||
|
||
json_result = self.get_json_from_graph() | ||
|
||
# Write JSON to file | ||
if json_result: | ||
with open(path, 'w') as outfile: | ||
jsoncore.Util.writeJson(json_result, path, 2) | ||
logger.info('Wrote JSON file: ' + path) | ||
|
||
self.editor.set_current_filepath(path) | ||
|
||
def import_json_triggered(self): | ||
''' | ||
Import a JSON file into the current graph. | ||
''' | ||
start_path = self.editor.mx_selection_path | ||
if not start_path: | ||
start_path = self.editor.geometry_selection_path | ||
|
||
if not start_path: | ||
start_path = os.path.join(self.root, "resources", "materials") | ||
|
||
path = self.editor.request_filepath( | ||
title="Load JSON file", start_path=start_path, file_filter="JSON files (*.json)", mode="open", | ||
) | ||
|
||
if not os.path.exists(path): | ||
logger.error('Cannot find input file: ' + path) | ||
return | ||
|
||
doc = jsoncore.Util.jsonFileToXml(path) | ||
if doc: | ||
logger.info('Loaded JSON file: ' + path) | ||
self.editor.mx_selection_path = path | ||
self.editor.qx_node_graph.load_graph_from_mx_doc(doc) | ||
self.editor.qx_node_graph.mx_file_loaded.emit(path) | ||
|
||
# Helper functions | ||
def show_text_box(self, text, title=""): | ||
''' | ||
Show a text box with the given text. | ||
''' | ||
te_text = QTextEdit() | ||
te_text.setText(text) | ||
te_text.setReadOnly(True) | ||
te_text.setParent(self.editor, QtCore.Qt.Window) | ||
te_text.setWindowTitle(title) | ||
te_text.resize(1000, 800) | ||
te_text.show() | ||
|
||
def install_plugin(editor, root, result): | ||
''' | ||
Plugin entry point. | ||
''' | ||
if not jsoncore: | ||
logger.error("MaterialX JSON plugin not installed") | ||
return [None, None] | ||
|
||
plugin = QuiltiX_JSON_serializer(editor, root) | ||
|
||
result.plugin = plugin | ||
result.id = "MaterialX JSON Serializer" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import importlib.util, os, logging | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
class QuiltiXPlugin(): | ||
def __init__(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. More fields can be added. It's simple for now with a key and implementation. |
||
self.id = "" | ||
self.plugin = None | ||
|
||
class QuiltiXPluginManager(): | ||
def __init__(self, editor, root): | ||
self.editor = editor | ||
self.root = root | ||
self.plugins = [] | ||
|
||
def install_plugins(self): | ||
self.plugins = [] | ||
plugin_roots = [os.path.join(self.root, "plugins"), os.getenv("QUILTIX_PLUGIN_FOLDER", "")] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This gives an initial package and env var location. |
||
for plugin_folder in plugin_roots: | ||
if os.path.exists(plugin_folder): | ||
absolute_plugin_folder = os.path.abspath(plugin_folder) | ||
logger.debug(f"Loading plugin from {absolute_plugin_folder}...") | ||
self.install_plugins_from_folder(plugin_folder) | ||
|
||
def install_plugins_from_folder(self, plugin_folder): | ||
if not os.path.isdir(plugin_folder): | ||
logger.warning(f"Plugin folder {plugin_folder} not found.") | ||
return | ||
|
||
# Get the list of all subfolders in the plugin_folder | ||
for entry in os.listdir(plugin_folder): | ||
entry_path = os.path.join(plugin_folder, entry) | ||
|
||
if os.path.isdir(entry_path): | ||
# Check for the presence of plugin.py in the subfolder | ||
plugin_file = os.path.join(entry_path, 'plugin.py') | ||
if os.path.isfile(plugin_file): | ||
module_name = f"{entry}.plugin" | ||
|
||
# Dynamically import the module | ||
spec = importlib.util.spec_from_file_location(module_name, plugin_file) | ||
module = importlib.util.module_from_spec(spec) | ||
spec.loader.exec_module(module) | ||
|
||
# Call module install_plugin function if it exists | ||
if hasattr(module, "install_plugin"): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A specific entry point is looked for. |
||
pluginInfo = QuiltiXPlugin() | ||
module.install_plugin(self.editor, self.root, pluginInfo) | ||
if pluginInfo.id and pluginInfo.plugin: | ||
pluginExists = None | ||
for installed_plugin in self.plugins: | ||
if installed_plugin.id == pluginInfo.id: | ||
pluginExists = installed_plugin | ||
break | ||
if pluginExists: | ||
logger.warning(f"Plugin with id {pluginInfo.id} already installed.") | ||
else: | ||
self.plugins.append(pluginInfo) | ||
logger.debug(f"Installed plugin {pluginInfo.id} from {plugin_file}.") | ||
else: | ||
logger.warning(f"No installPlugin function found in {plugin_file}.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This plugin isn't usable without jsoncore. Would it make sense to stop loading the plugin at this point and somehow disable it after logging the error?
Some logic for plugins to define requirements, which need to be met before the plugin can be loaded, could be handy. This could be used to define plugins, which only work on a specific OS or with a specific MaterialX versions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @RichardFrangenberg
There used to be a validity check before the refactor. I've added this back in with an explicit `is_valid()1 check on the plugin. It leaves it up to the plugin to decide where it can work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've changed it so a plugin doesn't need to implement
is_valid
, but it can choose to do so.