QuiltiX Plugins 0.0.1
Custom Plugins for QuiltiX
Loading...
Searching...
No Matches
materialxslx/plugin.py
Go to the documentation of this file.
1'''
2@file materialxslx/plugin.py
3@namespace materialxslx/plugin
4@brief MaterialX SLX plugin for QuiltiX
5@details This plugin provides serialization, and compilation/decompilation between MaterialX graphs and SLX format.
6'''
7import logging
8import os
9from pathlib import Path
10from importlib.metadata import version as importlib_version
11
12logger = logging.getLogger(__name__)
13
14# Syntax highlighting
15# Not using pygments since this is done using CodeMirror in the HTML.
16have_highliting = True
17use_pygments = False
18if use_pygments:
19 try:
20 from pygments import highlight
21 from pygments.lexers import CppLexer
22 from pygments.formatters import HtmlFormatter
23 logger.info("Pygments modules loaded successfully")
24 use_pygments = True
25 except ImportError:
26 use_pygments = False
27
28from typing import TYPE_CHECKING
29
30def load_qt_modules():
31 """
32 Function to delay loading of Qt modules until after the QuiltiX UI is initialized.
33 """
34 global QtCore, QAction, QTextEdit, QWebEngineView
35 try:
36 from qtpy import QtCore # type: ignore
37 from qtpy.QtWidgets import QAction, QTextEdit # type: ignore
38 from qtpy.QtWebEngineWidgets import QWebEngineView # type: ignore
39 logger.info("qtpy modules loaded successfully")
40 except ImportError as e:
41 logger.error(f"Failed to import qtpy modules: {e}")
42 raise
43
44from QuiltiX import constants, qx_plugin
45
46have_support_modules = True
47try:
48 import MaterialX as mx
49 from mxslc.Decompiler.decompile import Decompiler
50 from mxslc.compile_file import compile_file
51except ImportError as e:
52 have_support_modules = False
53 logger.error(f"Failed to import decompiler module: {e}")
54
55if TYPE_CHECKING:
56 from QuiltiX import quiltix
57
58class SLXHighighter:
59 '''
60 @brief Class to perform syntax highlighting for SLX code
61 @class SLXHighighter
62 '''
63 def __init__(self, language='c'):
64 '''
65 @brief Initialize the highlighter with specified language
66 @param language The language to use for highlighting ('cpp', 'c')
67 '''
68 if use_pygments:
69 if language.lower() in ['cpp', 'c++', 'cxx']:
70 self.lexer = CppLexer()
71 elif language.lower() == 'c':
72 self.lexer = CppLexer() # CppLexer handles C as well
73
74 # We don't add line numbers since this get's copied with
75 # copy-paste.
76 self.formatter = HtmlFormatter(linenos=False, style='github-dark')
77
78 def highlight(self, text):
79 '''
80 @brief Highlight the given text
81 @param text The text to highlight
82 @return The highlighted HTML
83 '''
84
85
86 if use_pygments:
87 highlighted_html = highlight(text, self.lexer, self.formatter)
88 styles = (
89 f"<style>"
90 f"{self.formatter.get_style_defs('.highlight')} "
91 f"pre {{ line-height: 1.0; margin: 0; }}"
92 f"</style>"
93 )
94 full_html = f"""
95 <html>
96 <head>
97 {styles}
98 <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
99 </head>
100 <body class="bg-dark text-light m-0 p-0 vh-100">
101 <div class="d-flex flex-column h-100 p-3">
102
103 <label for="slxTextarea" class="form-label fw-bold text-light mb-2">
104 ShadingLanguageX Code
105 </label>
106
107 <div id="slxTextarea"
108 class="d-flex flex-column flex-grow-1 bg-dark text-light border border-secondary rounded p-3 overflow-auto">
109 <div class="highlight">{highlighted_html}</div>
110 </div>
111
112 </div>
113 </body>
114 </html>
115 """
116
117 # Use CodeMirror for syntax highlighting
118 else:
119 full_html = f"""
120 <html>
121 <head>
122 <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
123 <link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.7/codemirror.min.css" rel="stylesheet">
124 <link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.7/theme/material-darker.min.css" rel="stylesheet">
125 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.7/codemirror.min.js"></script>
126 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.7/mode/clike/clike.min.js"></script>
127 <link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.7/theme/monokai.min.css" rel="stylesheet">
128 <link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.7/theme/darcula.min.css" rel="stylesheet">
129 </head>
130 <body class="bg-dark text-light m-0 p-0 vh-100">
131 <div class="d-flex flex-column h-100 p-3">
132 <label for="slxTextarea" class="form-label fw-bold text-light mb-2">
133 ShadingLanguageX Code
134 </label>
135 <textarea id="slxTextarea" class="flex-fill">{text}</textarea>
136 </div>
137
138 <script>
139 var editor = CodeMirror.fromTextArea(document.getElementById("slxTextarea"), {{
140 lineNumbers: true,
141 mode: "text/x-csrc",
142 theme: "darcula",
143 readOnly: true,
144 viewportMargin: Infinity
145 }});
146 editor.setSize("100%", "100%");
147 </script>
148 </body>
149 </html>
150 """
151 return full_html
152
154 '''
155 @brief Class to handle SLX serialization in QuiltiX
156 @class ShadingLangaugeXSerializer
157 '''
158 def __init__(self, editor, root):
159 """
160 Initialize the serializer.
161 """
162 self.editor = editor
163 self.root = root
164 self.indent = 4
165
166 # Add SLX menu to the file menu
167 # ----------------------------------------
168 editor.file_menu.addSeparator()
169 slxMenu = editor.file_menu.addMenu("SLX")
170
171 # Export SLX item
172 export_slx = QAction("Save SLX...", editor)
173 export_slx.triggered.connect(self.export_slx_triggered)
174 slxMenu.addAction(export_slx)
175
176 # Import SLX item
177 import_slx = QAction("Load SLX...", editor)
178 import_slx.triggered.connect(self.import_slx_triggered)
179 slxMenu.addAction(import_slx)
180
181 # Show SLX text. Does most of export, except does not write to file
182 show_slx_text = QAction("Show as SLX...", editor)
183 show_slx_text.triggered.connect(self.show_slx_triggered)
184 slxMenu.addAction(show_slx_text)
185
186 def get_header(self):
187 """
188 Get the header for the SLX output.
189 """
190 header = "// MaterialX SLX content generated via QuiltiX plugin\n"
191 header += f"// Materialx version: {mx.getVersionString()}\n"
192 header += f"// SLX version: {importlib_version('mxslc')}\n"
193 return header
194
195 def set_indent(self, indent):
196 """
197 Set the indent for the SLX output.
198 """
199 self.indent = indent
200
201 def get_mtlx_from_graph(self):
202 """
203 Get the MateriaLX for the current graph.
204 """
205 doc = self.editor.qx_node_graph.get_current_mx_graph_doc()
206 if doc:
207 result = mx.writeToXmlString(doc)
208 #logger.debug("MaterialX XML content:", result) # Debug output
209 return result
210 return None
211
212 def decompile_string(self, mtlx_content: str) -> str:
213 """
214 Decompile MaterialX in XML format to SLX string.
215 """
216 result = "// Faied to decompile MaterialX content."
217 try:
218 decompiler = Decompiler(mtlx_content)
219 result = decompiler.decompile()
220 except Exception as e:
221 result = f"// Unable to decompile MaterialX content: {e}"
222
223 return result
224
225 def show_slx_triggered(self):
226 """
227 Show the SLX for the current MaterialX document.
228 """
229 mtlx_content = self.get_mtlx_from_graph()
230 mxsl_output = self.decompile_string(mtlx_content)
231 mxsl_output = self.get_header() + mxsl_output
232
233 # Write SLX UI text box
234 if mxsl_output:
235 self.show_c_code_dialog(mxsl_output, "SLX Representation")
236
237 def export_slx_triggered(self, editor):
238 """
239 Export the current graph to a SLX file.
240 """
241 start_path = self.editor.mx_selection_path
242 if not start_path:
243 start_path = self.editor.geometry_selection_path
244
245 if not start_path:
246 start_path = os.path.join(self.root, "resources", "materials")
247
248 path = self.editor.request_filepath(
249 title="Save SLX file",
250 start_path=start_path,
251 file_filter="SLX files (*.mxsl)",
252 mode="save",
253 )
254 if not path:
255 return
256
257 mtlx_content = self.get_mtlx_from_graph()
258 mxsl_output = self.decompile_string(mtlx_content)
259
260 # Write SLX to file
261 if mxsl_output:
262 # Insert header
263 mxsl_output_file = self.get_header() + mxsl_output # Append the actual SLX content
264 with open(path, "w") as f:
265 f.write(mxsl_output_file)
266 logger.info("Wrote SLX file: " + path)
267
268 self.editor.set_current_filepath(path)
269
270 def import_slx_triggered(self, editor):
271 """
272 Import a SLX file into the current graph.
273 """
274 start_path = self.editor.mx_selection_path
275 if not start_path:
276 start_path = self.editor.geometry_selection_path
277
278 if not start_path:
279 start_path = os.path.join(self.root, "resources", "materials")
280
281 path = self.editor.request_filepath(
282 title="Load SLX file",
283 start_path=start_path,
284 file_filter="SLX files (*.mxsl)",
285 mode="open",
286 )
287 if not path:
288 return
289
290 if not os.path.exists(path):
291 logger.error("Cannot find input file: " + path)
292 return
293
294 # Compile the SLX file to MaterialX
295 # Replace .mxsl with .mtlx
296 doc = None
297 mtlx_path = path.replace('.mxsl', '.mtlx')
298 try:
299 compile_file(Path(path), mtlx_path)
300 # Check if the MaterialX file was created
301 if not os.path.exists(mtlx_path):
302 logger.error("Failed to compile SLX file to MaterialX: " + path)
303 else:
304 logger.info("Compiled SLX file to MaterialX: " + mtlx_path)
305 doc = mx.createDocument()
306 mx.readFromXmlFile(doc, mtlx_path)
307
308 with open(path, "r") as f:
309 mxsl_output = f.read()
310 self.show_c_code_dialog(mxsl_output, "SLX Loaded")
311 except Exception as e:
312 logger.error(f"Failed to compile SLX file: {e}")
313
314 if doc:
315 logger.info("Loaded SLX file: " + path)
316
317 #mx_defs = doc.getNodeDefs()
318 #for mx_def in mx_defs:
319 # print("Loading mx def: ", mx_def.getName())
320 #new_defs = qx_node.qx_node_from_mx_node_group_dict_generator(mx_defs)
321 #self.editor.qx_node_graph.register_nodes(new_defs)
322
323 self.editor.mx_selection_path = mtlx_path
324 # Load from file so that file search path for definition loading kicks in.
325 # Load from doc does not do this.
326 self.editor.qx_node_graph.load_graph_from_mx_file(mtlx_path)
327 #self.editor.qx_node_graph.load_graph_from_mx_doc(doc)
328 self.editor.qx_node_graph.mx_file_loaded.emit(mtlx_path)
329
330 def show_code_dialog(self, text, title="", language='c'):
331 if not use_pygments:
332 slxHighlighter = SLXHighighter(language)
333 highlighted_html = slxHighlighter.highlight(text)
334
335 web_view = QWebEngineView()
336 web_view.setHtml(highlighted_html)
337 web_view.setParent(self.editor, QtCore.Qt.Window)
338 web_view.setWindowTitle(title)
339 web_view.resize(1000, 800)
340 web_view.show()
341 else:
342 te_text = QTextEdit()
343 te_text.setReadOnly(True)
344 te_text.setParent(self.editor, QtCore.Qt.Window)
345 te_text.setWindowTitle(title)
346 te_text.resize(1000, 800)
347 te_text.setPlainText(text)
348
349 te_text.show()
350
351 def show_cpp_code_dialog(self, text, title="C++ Code"):
352 """
353 Show a text box with C++ syntax highlighting.
354 @param text The C++ code to display
355 @param title The window title
356 """
357 self.show_code_dialog(text, title, 'cpp')
358
359 def show_c_code_dialog(self, text, title="C Code"):
360 """
361 Show a text box with C syntax highlighting.
362 @param text The C code to display
363 @param title The window title
364 """
365 self.show_code_dialog(text, title, 'c')
366
367
368@qx_plugin.hookimpl
369def after_ui_init(editor: "quiltix.QuiltiXWindow"):
370 load_qt_modules()
371 editor.json_serializer = ShadingLangaugeXSerializer(editor, constants.ROOT)
372
373
374def plugin_name() -> str:
375 return "MaterialX SLX Serializer"
376
377
378def is_valid() -> bool:
379 return have_support_modules
Class to perform syntax highlighting for SLX code.
Class to handle SLX serialization in QuiltiX.
load_qt_modules()
Function to delay loading of Qt modules until after the QuiltiX UI is initialized.