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