QuiltiX Plugins 0.0.1
Custom Plugins for QuiltiX
Loading...
Searching...
No Matches
materialxgltf/plugin.py
Go to the documentation of this file.
1'''
2@file materialxgltf/plugin.py
3@namespace materialxgtf/plugin
4'''
5
6import logging
7import os
8import base64
9
10# Optional syntax highlighting if pygments is installed
11have_highliting = True
12try:
13 from pygments import highlight
14 from pygments.lexers import JsonLexer
15 from pygments.formatters import HtmlFormatter
16except ImportError:
17 have_highliting = False
18
19from typing import TYPE_CHECKING
20
22 global QtCore, QAction, QTextEdit, QDockWidget, QVBoxLayout, QWidget
23 try:
24 from qtpy import QtCore # type: ignore
25 from qtpy.QtWidgets import QAction, QDockWidget, QVBoxLayout, QTextEdit, QWidget
26 logger.info("qtpy modules loaded successfully")
27 return True
28 except ImportError as e:
29 logger.error(f"Failed to import qtpy modules: {e}")
30 return False
31
32from QuiltiX import constants, qx_plugin
33from QuiltiX.constants import ROOT
34
35#from qtpy.QtWebEngineCore import QWebEnginePage, QWebEngineUrlRequestInterceptor, QWebEngineProfile, QWebEngineSettings
36from qtpy.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
37from qtpy.QtQuick import QQuickWindow
38from qtpy.QtQuick import QSGRendererInterface
39# Ensure the application uses OpenGL for rendering
40QQuickWindow.setGraphicsApi(QSGRendererInterface.GraphicsApi.OpenGL)
41
42logger = logging.getLogger(__name__)
43
44# MaterialX and related modules
45import MaterialX as mx
46
47haveGLTF = False
48try:
49 import materialxgltf.core as core
50 import materialxgltf
51 haveGLTF = True
52except ImportError:
53 logger.error("materialxgltf module is not installed.")
54
55# Optional syntax highlighting if pygments is installed
56have_highliting = True
57try:
58 from pygments import highlight
59 from pygments.lexers import JsonLexer
60 from pygments.formatters import HtmlFormatter
61except ImportError:
62 have_highliting = False
63
64# Use to allow access to resources in the package
65import pkg_resources
66#from importlib.resources import files as pkg_files
67
69 '''
70 @brief Highlighter for glTF text
71 '''
72 def __init__(self):
73 '''
74 @brief Initialize the highlighter
75 '''
76 self.lexer = JsonLexer()
77 # We don't add line numbers since this get's copied with
78 # copy-paste.
79 self.formatter = HtmlFormatter(linenos=False, style='github-dark')
80
81 def highlight(self, text):
82 '''
83 @brief Highlight the text
84 @param text: The text to highlight
85 @return: The highlighted text
86 '''
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"<html><head>{styles}</head><body>{highlighted_html}</body></html>"
95 return full_html
96
97#class MyRequestInterceptor(QWebEngineUrlRequestInterceptor):
98# def interceptRequest(self, info):
99# info.setHttpHeader(b'Permissions-Policy', b'interest-cohort=()')
100
101class glTFEnginePage(QWebEnginePage):
102 '''
103 @brief Custom web engine page for the glTF viewer
104 '''
105 def __init__(self, parent=None):
106 '''
107 @brief Initialize the custom web engine page
108 @param parent: The parent widget
109 '''
110 super().__init__(parent)
111 self.debug = False
112
113 def setDebug(self, debug):
114 '''
115 @brief Set the debug flag
116 @param debug: The debug flag
117 '''
118 self.debug = debug
119
120 def javaScriptConsoleMessage(self, level, message, lineNumber, sourceID):
121 '''
122 @brief Handle JavaScript console messages
123 '''
124 if self.debug:
125 print(f"JS: {message} (line: {lineNumber}, source: {sourceID})")
126
128 '''
129 Add interestCohort function to the document to disable FLoC
130 '''
131 script = """
132 document.addEventListener('DOMContentLoaded', (event) => {
133 document.interestCohort = function() { return false; };
134 });
135 """
136 self.runJavaScript(script)
137
138 def load_glb(self, glb_file_path):
139 # Read the .glb file in binary mode
140 #glb_file_path = "/path/to/your/model.glb" # Change this to your .glb file path
141 if (not os.path.exists(glb_file_path)):
142 print(f"Error: glTF file not found: {glb_file_path}")
143 return
144
145 with open(glb_file_path, "rb") as f:
146 binary_data = f.read()
147
148 # Encode the binary data as Base64
149 base64_data = base64.b64encode(binary_data).decode('utf-8')
150
151 # Run the JavaScript code to store the data in the browser's local storage
152 # Inject the Base64 encoded data into local storage using JavaScript
153 script = f"localStorage.setItem('glbData', '{base64_data}');"
154 self.runJavaScript(script)
155
156 print("GLB data injected into localStorage.")
157
158 self.runJavaScript('loadFromLocalStorage()')
159 print("Update viewer from localStorage.")
160
161
163 '''
164 @brief glTF serializer for MaterialX
165 '''
166
167 def __init__(self, editor, root) -> None:
168 '''
169 Initialize the plugin. Adds in:
170 - Menu items for loading and saving glTF files
171 - Menu items for setting options for glTF export
172 - Menu item for showing the current graph as glTF text
173
174 @param editor: The QuiltiX editor
175 @param root: The root path of QuitiX
176 '''
177 self.editor = editor
178 self.root = root
179
180 # glTF menu items
181 # ----------------------------------------
182 # Update 'File' menu
183
184 editor.file_menu.addSeparator()
185 gltfMenu1 = self.editor.file_menu.addMenu("glTF")
186
187 # Load menu item
188 import_gltf_item = QAction("Load glTF...", editor)
189 import_gltf_item.triggered.connect(self.import_gltf_triggeredimport_gltf_triggered)
190 gltfMenu1.addAction(import_gltf_item)
191
192 # Save menu item
193 export_gltf_item = QAction("Save glTF...", editor)
194 export_gltf_item.triggered.connect(lambda: self.export_gltf_triggered(False))
195 gltfMenu1.addAction(export_gltf_item)
196
197 # Export to Viewer menu item
198 export_view_gltf_item = QAction("Export to Viewer...", editor)
199 export_view_gltf_item.triggered.connect(lambda: self.export_gltf_triggered(True))
200 gltfMenu1.addAction(export_view_gltf_item)
201
202 # Show glTF text. Does most of export, except does not write to file
203 show_gltf_text = QAction("Show glTF as text...", editor)
204 show_gltf_text.triggered.connect(self.show_gltf_text_triggeredshow_gltf_text_triggered)
205 gltfMenu1.addAction(show_gltf_text)
206
207 # Update 'Options' menu
208
209 editor.options_menu.addSeparator()
210 gltfMenu2 = editor.options_menu.addMenu("glTF Options")
211
212 self.bake_textures_option = QAction("Always Bake Textures", editor)
213 self.bake_textures_option.setCheckable(True)
214 self.bake_textures_option.setChecked(False)
215 gltfMenu2.addAction(self.bake_textures_option)
216
217 #version = 'materialxgltf version: ' + materialxgltf.__version__
218 #version_action = QAction(version, self.editor)
219 #version_action.setEnabled(False)
220 #gltfMenu.addAction(version_action)
221
222 # Add glTF Viewer
224
225 # Update 'View' menu
226
228 self.act_gltf_viewer = QAction("glTF Viewer", editor)
229 self.act_gltf_viewer.setCheckable(True)
231 editor.view_menu.addSeparator()
232 editor.view_menu.addAction(self.act_gltf_viewer)
233
234 # Override about to show event to update the gltf viewer toggle
236
237 # Catch when file is loaded.
238 self.editor.qx_node_graph.mx_file_loaded.connect(self.handle_file_loadedhandle_file_loaded)
241
242 def handle_file_loaded(self, data):
243 # Slot to handle the emitted data
244 logger.info(f"Watch file loaded: {data}")
245 self.current_mx_file = data
246
248 '''
249 Custom about to show event for the view menu. Updates the glTF viewer toggle.
250 '''
251 self.editor.on_view_menu_showing()
252 self.act_gltf_viewer.setChecked(self.web_dock_widget.isVisible())
253
254 def on_gltf_viewer_toggled(self, checked) -> None:
255 '''
256 Toggle the glTF viewer dock widget.
257 '''
258 self.web_dock_widget.setVisible(checked)
259
260 def setup_gltf_viewer_doc(self) -> None:
261 '''
262 Set up the glTF viewer dock widget.
263 '''
264 class glTFWidget(QDockWidget):
265 '''
266 @brief glTF Viewer widget
267 '''
268 def __init__(self, parent=None):
269 '''
270 @brief Sets up a web view and loads in a sample glTF viewer page.
271 '''
272 super(glTFWidget, self).__init__(parent)
273
274 self.setWindowTitle("glTF Viewer")
275
276 self.setFloating(False)
277
278 # Create a web view.
279 self.web_view = QWebEngineView()
280 self.viewer_options = '?hideSave=true'
281 self.viewer_options += '&env=https://kwokcb.github.io/MaterialXLab/documents/resources/Lights/rural_crossroads_1k.hdr'
282 self.viewer_address = 'https://kwokcb.github.io/MaterialXLab/documents/gltfViewer_simple.html'
283 # e.g. For debugging can set to local host if you want to run a local page
284 #self.viewer_address = 'http://localhost:8000/documents/gltfViewer_simple.html'
285 self.viewer_address += self.viewer_options
286 #self.logger.debug('glTF Viewer address: ' + self.viewer_address)
287
288 # Create a custom page and set it to the web view
290 self.web_view.setPage(self.page)
291
292 self.web_view.setUrl(QtCore.QUrl(self.viewer_address))
293
294 # Apply the request interceptor
295 #interceptor = MyRequestInterceptor()
296 #QWebEngineProfile.defaultProfile().setUrlRequestInterceptor(interceptor)
297
298 #settings = self.web_view.settings()
299 # Disable features that might trigger the error (general approach)
300 #settings.setAttribute(QWebEngineSettings.WebAttribute.LocalStorageEnabled, False)
301 #settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, False)
302 #settings.setAttribute(QWebEngineSettings.WebAttribute.XSSAuditingEnabled, False)
303
304 # Set up the layout
305 layout = QVBoxLayout()
306 layout.addWidget(self.web_view)
307
308 # Create a central widget to hold the layout
309 central_widget = QWidget()
310 central_widget.setLayout(layout)
311
312 # Inject JavaScript after the page is loaded
313 #self.web_view.loadFinished.connect(self.page.injectJavaScript)
314
315 # Set the central widget of the dock widget
316 self.setWidget(central_widget)
317
318 self.web_dock_widget = glTFWidget(self.editor)
319 self.editor.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.web_dock_widget)
320
321
322 def show_text_box(self, text, title="") -> None:
323 '''
324 Show a text box with the given text.
325 @param text: The text to show
326 @param title: The title of the text box. Default is empty string.
327 '''
328 te_text = QTextEdit()
329
330 if have_highliting:
331 jsonHighlighter = GLTFHighlighter()
332 highlighted_html = jsonHighlighter.highlight(text)
333 te_text.setHtml(highlighted_html)
334 else:
335 te_text.setText(text)
336 te_text.setReadOnly(True)
337 te_text.setParent(self.editor, QtCore.Qt.Window)
338 te_text.setWindowTitle(title)
339 te_text.resize(1000, 800)
340 te_text.show()
341
342 def import_gltf_triggered(self) -> None:
343 '''
344 Import a glTF file into the current graph.
345 '''
346 start_path = self.editor.mx_selection_path
347 if not start_path:
348 start_path = self.editor.geometry_selection_path
349
350 if not start_path:
351 start_path = os.path.join(ROOT, "resources", "materials")
352
353 path = self.editor.request_filepath(
354 title="Load glTF file", start_path=start_path, file_filter="glTF files (*.gltf)", mode="open",
355 )
356
357 if not os.path.exists(path):
358 logger.error('Cannot find input file: ' + path)
359 return
360
361 # Setup defaults for glTF import and create the MaterialX document
362 # - Do not create assignments
363 # - Do not add all inputs
364 # - Add extract nodes. Only required for pre 1.39 MaterialX
365 options = core.GLTF2MtlxOptions()
366 options['createAssignments'] = False
367 options['addAllInputs'] = False
368 options['addExtractNodes'] = True
369 gltf2MtlxReader = core.GLTF2MtlxReader()
370 gltf2MtlxReader.setOptions(options)
371 doc = gltf2MtlxReader.convert(path)
372
373 success = True
374 if not doc:
375 success = False
376 logger.error('Error converting glTF file to MaterialX file')
377 else:
378 success , err = doc.validate()
379 if not success:
380 logger.error(err)
381 else:
382 # Extract out the appropriate data from the original MaterialX document
383 docString = core.Util.writeMaterialXDocString(doc)
384 doc = mx.createDocument()
385 mx.readFromXmlString(doc, docString)
386
387 # Auto ng creation can cause issues for nodes with multiple outputs. Turn off on import
388 #ng_abstraction = self.editor.act_ng_abstraction.isChecked()
389 #self.editor.act_ng_abstraction.setChecked(False)
390
391 self.editor.mx_selection_path = path
392 self.editor.qx_node_graph.load_graph_from_mx_doc(doc)
393 # Blank out as there is no MaterialX file this was read from
394 self.editor.qx_node_graph.mx_file_loaded.emit("")
395
396 #self.editor.act_ng_abstraction.setChecked(ng_abstraction)
397
398 def setup_default_export_options(self, path, bakeFileName, bakeResolution=1024, embed_geometry=False) -> dict:
399 '''
400 Set up the default export options for gltf output.
401 @param path (str): path to the gltf file
402 @param bakeFileName (str): path to the baked file
403 @param bakeResolution (int): resolution of the baked textures. Default is 1024.
404 @param embed_geometry (bool): whether to embed the geometry in the gltf file. Default is False.
405 @return options (dict): Dictionary of options for the conversion
406 '''
407 options = core.MTLX2GLTFOptions()
408
409 options['debugOutput'] = False
410 options['bakeFileName'] = bakeFileName
411 # Clamp to a minimum power-of-2 resolution
412 bakeResolution = max(bakeResolution, 16)
413 options['bakeResolution'] = bakeResolution
414
415 # TODO: Expose these options to the user
416 if embed_geometry:
417 # By default uses the "MaterialX" shader ball provided as part of
418 # the materialxgltf package for binary export.
419 #data_path = pkg_files(__package__) / 'data' / 'shaderBall.gltf'
420 #with open(data_path, 'rb') as f:
421 # gltfGeometryFile = f.read()
422 gltfGeometryFile = pkg_resources.resource_filename('materialxgltf', 'data/shaderBall.gltf')
423 msg = '> Load default geometry: %s' % mx.FilePath(gltfGeometryFile).getBaseName()
424 logger.info(msg)
425 options['geometryFile'] = gltfGeometryFile
426 options['primsPerMaterial'] = True
427 options['writeDefaultInputs'] = False
428 options['translateShaders'] = True
429 options['bakeTextures'] = True
430 # Always should be set
431 options['addExtractNodes'] = True
432
433 searchPath = mx.getDefaultDataSearchPath()
434 if not mx.FilePath(path).isAbsolute():
435 path = os.path.abspath(path)
436 searchPath.append(mx.FilePath(path).getParentPath())
437 searchPath.append(mx.FilePath.getCurrentPath())
438 searchPath.append(mx.FilePath(self.current_mx_file).getParentPath())
439 if embed_geometry:
440 searchPath.append(mx.FilePath(gltfGeometryFile).getParentPath())
441 options['searchPath'] = searchPath
442 logger.info(f'Export Search path: {searchPath.asString()}')
443
444 return options
445
446 def create_baked_path(self, path):
447 '''
448 Create a baked path name from an original path
449 @param path (str): The original path
450 @return: The baked path
451 '''
452 # If is a directory, add a file name
453 if os.path.isdir(path):
454 path = os.path.join(path, 'temp_baked.mtlx')
455 else:
456 path = path.replace('.mtlx', '_baked.mtlx')
457 return path
458
459 def export_gltf_triggered(self, writeToTemp=False) -> None:
460 '''
461 Export the current graph to a glTF file in binary format (glb)
462 - Will perform shader translation if needed to glTF
463 - Will perform baking if needed
464 - Will package to a binary file
465 @param writeToTemp (bool): Whether to write to a temporary file
466 @return: None
467 '''
468 if writeToTemp:
469 start_path = os.environ["TEMP"]
470 if not start_path:
471 start_path = os.path.join(ROOT, "resources", "materials")
472 path = os.path.join(start_path, f"_tmp_quiltix{self.temp_file_counter}.gltf")
473 self.temp_file_counter += 1
474 else:
475 start_path = self.editor.mx_selection_path
476 if not start_path:
477 start_path = self.editor.geometry_selection_path
478
479 if not start_path:
480 start_path = os.path.join(ROOT, "resources", "materials")
481
482 path = self.editor.request_filepath(
483 title="Save glTF file", start_path=start_path, file_filter="glTF files (*.gltf)", mode="save",
484 )
485
486 # Set up export options
487 bakeFileName = self.create_baked_path(start_path)
488 options = self.setup_default_export_options(path, bakeFileName, bakeResolution=1024, embed_geometry=True)
489 gltf_string = self.convert_graph_to_gltf(options)
490
491 if gltf_string == '{}':
492 return
493
494 #self.editor.set_current_filepath("")
495
496 # To be able to view the exported file in the glTF viewer, we need to package to a binary file
497 options['packageBinary'] = True
498 try:
499 with open(path, "w") as f:
500 f.write(gltf_string)
501 logger.info(f"Wrote .gltf file to {path}")
502
503 # Package binary file
504 binaryFileName = str(path)
505 binaryFileName = binaryFileName.replace('.gltf', '.glb')
506 logger.debug('- Packaging GLB file...')
507 mtlx2glTFWriter = core.MTLX2GLTFWriter()
508 mtlx2glTFWriter.setOptions(options)
509 saved, images, buffers = mtlx2glTFWriter.packageGLTF(path, binaryFileName)
510 logger.info('- Save GLB file:' + binaryFileName + '. Status:' + str(saved))
511 for image in images:
512 logger.debug(' - Embedded image: ' + image)
513 for buffer in buffers:
514 logger.debug(' - Embedded buffer: ' + buffer)
515 logger.debug('- Packaging GLB file... finished.')
516
517 # Load the glb file into the glTF viewer
518 logger.debug(f'Loading GLB file into glTF viewer: {binaryFileName}')
519 if self.web_dock_widget.page:
520 self.web_dock_widget.page.load_glb(binaryFileName)
521
522 except Exception as e:
523 logger.error(e)
524
525 def convert_graph_to_gltf(self, options) -> str:
526 '''
527 Convert the current graph to a glTF document string.
528 Will perform:
529 - Shader translation if needed (not that only standard surface is supported)
530 - Baking if needed. Note that this writes local files.
531 - Uses the materialgltf package to perform conversion
532
533 @param options (dict): Dictionary of options for the conversion
534 @return The glTF string.
535 '''
536 # Disable auto nodegraph creation during
537 #ng_abstraction = self.editor.act_ng_abstraction.isChecked()
538 #self.editor.act_ng_abstraction.setChecked(False)
539
540 doc = self.editor.qx_node_graph.get_current_mx_graph_doc()
541
542 #self.editor.act_ng_abstraction.setChecked(ng_abstraction)
543
544 mtlx2glTFWriter = core.MTLX2GLTFWriter()
545 mtlx2glTFWriter.setOptions(options)
546
547 # Need to load in definition libraries to get translation graphs
548 stdlib = mx.createDocument()
549 searchPath = mx.getDefaultDataSearchPath()
550 libraryFolders = []
551 libraryFolders.extend(mx.getDefaultDataLibraryFolders())
552 mx.loadLibraries(libraryFolders, searchPath, stdlib)
553 doc.importLibrary(stdlib)
554
555 # Perform shader translation if needed
556 translatedCount = 0
557 if options['translateShaders']:
558 translatedCount = mtlx2glTFWriter.translateShaders(doc)
559 logger.debug('- Translated shaders: ' + str(translatedCount))
560
561 forceBake = False
562 if self.bake_textures_option.isChecked():
563 logger.debug('--- Forcing baking of textures')
564 forceBake = True
565
566 # Perform baking if needed
567 if forceBake or (translatedCount > 0 and options['bakeTextures']):
568 bakedFileName = options['bakeFileName']
569 bakeResolution = 1024
570 if options['bakeResolution']:
571 bakeResolution = options['bakeResolution']
572 logger.debug(f'- START baking to {bakedFileName}. Resolution: {bakeResolution} ...')
573 mtlx2glTFWriter.bakeTextures(doc, False, bakeResolution, bakeResolution, True,
574 False, False, bakedFileName)
575 if os.path.exists(bakedFileName):
576 logger.debug(' - Baked textures to: ' + bakedFileName)
577 doc, libFiles = core.Util.createMaterialXDoc()
578 mx.readFromXmlFile(doc, bakedFileName, options['searchPath'])
579 remappedUris = core.Util.makeFilePathsRelative(doc, bakedFileName)
580 for uri in remappedUris:
581 logger.debug(' - Remapped URI: ' + uri[0] + ' to ' + uri[1])
582 core.Util.writeMaterialXDoc(doc, bakedFileName)
583 logger.debug('- ... END baking.')
584
585 gltfString = mtlx2glTFWriter.convert(doc)
586 return gltfString
587
589 '''
590 Show the current graph as glTF text popup.
591 '''
592 path = self.editor.mx_selection_path
593 if not path:
594 path = self.editor.geometry_selection_path
595 if not path:
596 path = os.path.join(ROOT, "resources", "materials")
597
598 # Setup export options
599 bakeFileName = self.create_baked_path(path)
600 options = self.setup_default_export_options(path, bakeFileName, bakeResolution=64, embed_geometry=False)
601
602 logger.debug('Show glTF text triggered. Path:' + path + '. bakeFileName: ' + bakeFileName)
603
604 # Convert and display the text
605 text = self.convert_graph_to_gltf(options)
606 self.show_text_box(text, 'glTF Representation')
607
608@qx_plugin.hookimpl
609def after_ui_init(editor: "quiltix.QuiltiXWindow"):
610 '''
611 @brief After UI initialization, add the MaterialX glTF serializer to the editor.
612 '''
613 logger.info("Adding MaterialX glTF serializer")
614 loaded_qt = load_qt_modules()
615 if loaded_qt:
616 editor.gltf_serializer = QuiltiX_glTF_serializer(editor, constants.ROOT)
617
618def plugin_name() -> str:
619 '''
620 @brief Get the name of the plugin.
621 @return The name of the plugin.'''
622 if haveGLTF:
623 return "MaterialX glTF Serializer"
624 return ""
625
626def is_valid() -> bool:
627 '''
628 @brief Check if the plugin is valid. That is the glTF serializer module is installed.
629 @return True if the plugin is valid), False otherwise.
630 '''
631 return haveGLTF
Highlighter for glTF text.
__init__(self)
Initialize the highlighter.
highlight(self, text)
Highlight the text.
glTF serializer for MaterialX
None export_gltf_triggered(self, writeToTemp=False)
Export the current graph to a glTF file in binary format (glb)
dict setup_default_export_options(self, path, bakeFileName, bakeResolution=1024, embed_geometry=False)
Set up the default export options for gltf output.
create_baked_path(self, path)
Create a baked path name from an original path.
str convert_graph_to_gltf(self, options)
Convert the current graph to a glTF document string.
None on_gltf_viewer_toggled(self, checked)
Toggle the glTF viewer dock widget.
None import_gltf_triggered(self)
Import a glTF file into the current graph.
None __init__(self, editor, root)
Initialize the plugin.
None setup_gltf_viewer_doc(self)
Set up the glTF viewer dock widget.
show_gltf_text_triggered(self)
Show the current graph as glTF text popup.
None show_text_box(self, text, title="")
Core utilities.
custom_on_view_menu_about_to_show(self)
Custom about to show event for the view menu.
Custom web engine page for the glTF viewer.
setDebug(self, debug)
Set the debug flag.
__init__(self, parent=None)
Initialize the custom web engine page.
injectJavaScript(self)
Add interestCohort function to the document to disable FLoC.
javaScriptConsoleMessage(self, level, message, lineNumber, sourceID)
Handle JavaScript console messages.
load_glb(self, glb_file_path)
after_ui_init("quiltix.QuiltiXWindow" editor)
After UI initialization, add the MaterialX glTF serializer to the editor.
bool is_valid()
Check if the plugin is valid.
str plugin_name()
Get the name of the plugin.
load_qt_modules()
Function to delay loading of Qt modules until after the QuiltiX UI is initialized.