1 # SPDX-License-Identifier: GPL-2.0-or-later
3 """Define the POV render engine from generic Blender RenderEngine class."""
8 import builtins
as __builtin__
11 from sys
import platform
15 from bpy
.utils
import register_class
, unregister_class
18 def console_get(context
):
19 #context = bpy.context
20 for win
in context
.window_manager
.windows
:
21 if win
.screen
is not None:
23 for area
in scr
.areas
:
24 if area
.type == 'CONSOLE':
25 for space
in area
.spaces
:
26 if space
.type == 'CONSOLE':
27 return area
, space
, win
, scr
28 return None, None, None, None
30 def console_write(context
, txt
):
31 area
, space
, window
, screen
= console_get()
34 #context = bpy.context.copy()
38 region
=area
.regions
[-1],
42 for line
in txt
.split("\n"):
43 bpy
.ops
.console
.scrollback_append(context
, text
=line
, type='INFO')
45 class RENDER_OT_test(bpy.types.Operator):
46 bl_idname = 'pov.oha_test'
48 bl_options = {'REGISTER', 'UNDO'}
50 txt: bpy.props.StringProperty(
54 def execute(self, context):
56 console_write(context, self.txt)
59 self.report({'INFO'}, 'Printing report to Info window.')
62 def console_print(*args, **kwargs):
64 #screens = (win.screen for win in context.window_manager.windows if win.screen is not None)
65 for win in context.window_manager.windows:
66 if win.screen is not None:
69 if a.type == 'CONSOLE':
73 c['space_data'] = a.spaces.active
74 c['region'] = a.regions[-1]
77 s = " ".join([str(arg) for arg in args])
78 for line in s.split("\n"):
79 bpy.ops.console.scrollback_append(c, text=line, type='INFO')
81 except BaseException as e:
83 print('An exception occurred: {}'.format(e))
87 def print(*args, **kwargs):
88 console_print(*args, **kwargs) # to Python Console
89 __builtin__.print(*args, **kwargs) # to System Console
92 user_dir
= bpy
.utils
.resource_path('USER')
93 preview_dir
= os
.path
.join(user_dir
, "preview")
94 # Make sure Preview directory exists and is empty
95 smoke_path
= os
.path
.join(preview_dir
, "smoke.df3")
97 class PovRender(bpy
.types
.RenderEngine
):
98 """Define the external renderer"""
100 bl_idname
= 'POVRAY_RENDER'
101 bl_label
= "Persitence Of Vision"
102 bl_use_eevee_viewport
= True
103 bl_use_shading_nodes_custom
= False
107 def _locate_binary():
108 """Identify POV engine"""
109 addon_prefs
= bpy
.context
.preferences
.addons
[__package__
].preferences
111 # Use the system preference if its set.
112 if pov_binary
:= addon_prefs
.filepath_povray
:
113 if os
.path
.exists(pov_binary
):
115 # Implicit else, as here return was still not triggered:
116 print("User Preferences path to povray %r NOT FOUND, checking $PATH" % pov_binary
)
119 # assume if there is a 64bit binary that the user has a 64bit capable OS
120 if platform
.startswith('win'):
123 win_reg_key
= winreg
.OpenKey(
124 winreg
.HKEY_CURRENT_USER
, "Software\\POV-Ray\\v3.7\\Windows"
126 win_home
= winreg
.QueryValueEx(win_reg_key
, "Home")[0]
128 # First try 64bits UberPOV
129 pov_binary
= os
.path
.join(win_home
, "bin", "uberpov64.exe")
130 if os
.path
.exists(pov_binary
):
133 # Then try 64bits POV
134 pov_binary
= os
.path
.join(win_home
, "bin", "pvengine64.exe")
135 if os
.path
.exists(pov_binary
):
138 # search the path all os's
139 pov_binary_default
= "povray"
141 os_path_ls
= os
.getenv("PATH").split(':') + [""]
143 for dir_name
in os_path_ls
:
144 pov_binary
= os
.path
.join(dir_name
, pov_binary_default
)
145 if os
.path
.exists(pov_binary
):
149 def _export(self
, depsgraph
, pov_path
, image_render_path
):
150 """gather all necessary output files paths user defined and auto generated and export there"""
152 scene
= bpy
.context
.scene
153 if scene
.pov
.tempfiles_enable
:
154 self
._temp
_file
_in
= tempfile
.NamedTemporaryFile(suffix
=".pov", delete
=False).name
155 # PNG with POV 3.7, can show the background color with alpha. In the long run using the
156 # POV-Ray interactive preview like bishop 3D could solve the preview for all formats.
157 self
._temp
_file
_out
= tempfile
.NamedTemporaryFile(suffix
=".png", delete
=False).name
158 # self._temp_file_out = tempfile.NamedTemporaryFile(suffix=".tga", delete=False).name
159 self
._temp
_file
_ini
= tempfile
.NamedTemporaryFile(suffix
=".ini", delete
=False).name
160 log_path
= os
.path
.join(tempfile
.gettempdir(), "alltext.out")
162 self
._temp
_file
_in
= pov_path
+ ".pov"
163 # PNG with POV 3.7, can show the background color with alpha. In the long run using the
164 # POV-Ray interactive preview like bishop 3D could solve the preview for all formats.
165 self
._temp
_file
_out
= image_render_path
+ ".png"
166 # self._temp_file_out = image_render_path + ".tga"
167 self
._temp
_file
_ini
= pov_path
+ ".ini"
168 scene_path
= scene
.pov
.scene_path
169 abs_log_path
= bpy
.path
.abspath(scene_path
)
170 log_path
= os
.path
.join(abs_log_path
, "alltext.out")
172 self._temp_file_in = "/test.pov"
173 # PNG with POV 3.7, can show the background color with alpha. In the long run using the
174 # POV-Ray interactive preview like bishop 3D could solve the preview for all formats.
175 self._temp_file_out = "/test.png"
176 #self._temp_file_out = "/test.tga"
177 self._temp_file_ini = "/test.ini"
180 self
._temp
_file
_log
= log_path
181 # self._temp_file_log = log_path.replace('\\', '/') # unnecessary relying on os.path
183 if scene
.pov
.text_block
== "":
185 def info_callback(txt
):
186 self
.update_stats("", "POV-Ray 3.7: " + txt
)
188 # os.makedirs(user_dir, exist_ok=True) # handled with previews
189 os
.makedirs(preview_dir
, exist_ok
=True)
191 render
.write_pov(self
._temp
_file
_in
, scene
, info_callback
)
195 def _render(self
, depsgraph
):
196 """Export necessary files and render image."""
197 scene
= bpy
.context
.scene
199 os
.remove(self
._temp
_file
_out
) # so as not to load the old file
203 pov_binary
= PovRender
._locate
_binary
()
205 print("POV-Ray 3.7: could not execute povray, possibly POV-Ray isn't installed")
208 render
.write_pov_ini(
209 self
._temp
_file
_ini
, self
._temp
_file
_log
, self
._temp
_file
_in
, self
._temp
_file
_out
212 print("***-STARTING-***")
215 # Always add user preferences include path field when specified
216 if (pov_documents
:= bpy
.context
.preferences
.addons
[__package__
].preferences
.docpath_povray
)!="":
217 extra_args
.append("+L"+ pov_documents
)
218 if scene
.pov
.command_line_switches
!= "":
219 extra_args
.extend(iter(scene
.pov
.command_line_switches
.split(" ")))
220 self
._is
_windows
= False
221 if platform
.startswith('win'):
222 self
._is
_windows
= True
223 if "/EXIT" not in extra_args
and not scene
.pov
.pov_editor
:
224 extra_args
.append("/EXIT")
226 # added -d option to prevent render window popup which leads to segfault on linux
227 extra_args
.append("-d")
231 self
._process
= subprocess
.Popen(
232 [pov_binary
, self
._temp
_file
_ini
] + extra_args
,
233 stdout
=subprocess
.PIPE
,
234 stderr
=subprocess
.STDOUT
,
238 print("POV-Ray 3.7: could not execute '%s'" % pov_binary
)
241 traceback
.print_exc()
242 print("***-DONE-***")
246 print("Engine ready!...")
247 print("Command line arguments passed: " + str(extra_args
))
251 """Delete temp files and unpacked ones"""
252 for f
in (self
._temp
_file
_in
, self
._temp
_file
_ini
, self
._temp
_file
_out
):
258 # Wait a bit before retrying file might be still in use by Blender,
259 # and Windows does not know how to delete a file in use!
260 time
.sleep(self
.DELAY
)
261 for i
in render
.unpacked_images
:
267 # Wait a bit before retrying file might be still in use by Blender,
268 # and Windows does not know how to delete a file in use!
269 time
.sleep(self
.DELAY
)
270 # avoid some crashes if memory leaks from one render to the next?
271 #self.free_blender_memory()
273 def render(self
, depsgraph
):
274 """Export necessary files from text editor and render image."""
276 scene
= bpy
.context
.scene
278 x
= int(r
.resolution_x
* r
.resolution_percentage
* 0.01)
279 y
= int(r
.resolution_y
* r
.resolution_percentage
* 0.01)
280 print("\n***INITIALIZING***")
282 # This makes some tests on the render, returning True if all goes good, and False if
283 # it was finished one way or the other.
284 # It also pauses the script (time.sleep())
286 time
.sleep(self
.DELAY
)
288 # User interrupts the rendering
289 if self
.test_break():
291 self
._process
.terminate()
292 print("***POV INTERRUPTED***")
297 poll_result
= self
._process
.poll()
298 except AttributeError:
299 print("***CHECK POV PATH IN PREFERENCES***")
301 # POV process is finisehd, one way or the other
302 if poll_result
is not None:
304 print("***POV PROCESS FAILED : %s ***" % poll_result
)
305 self
.update_stats("", "POV-Ray 3.7: Failed")
310 if bpy
.context
.scene
.pov
.text_block
!= "":
311 if scene
.pov
.tempfiles_enable
:
312 self
._temp
_file
_in
= tempfile
.NamedTemporaryFile(suffix
=".pov", delete
=False).name
313 self
._temp
_file
_out
= tempfile
.NamedTemporaryFile(suffix
=".png", delete
=False).name
314 # self._temp_file_out = tempfile.NamedTemporaryFile(suffix=".tga", delete=False).name
315 self
._temp
_file
_ini
= tempfile
.NamedTemporaryFile(suffix
=".ini", delete
=False).name
316 self
._temp
_file
_log
= os
.path
.join(tempfile
.gettempdir(), "alltext.out")
318 pov_path
= scene
.pov
.text_block
319 image_render_path
= os
.path
.splitext(pov_path
)[0]
320 self
._temp
_file
_out
= os
.path
.join(preview_dir
, image_render_path
)
321 self
._temp
_file
_in
= os
.path
.join(preview_dir
, pov_path
)
322 self
._temp
_file
_ini
= os
.path
.join(
323 preview_dir
, (os
.path
.splitext(self
._temp
_file
_in
)[0] + ".INI")
325 self
._temp
_file
_log
= os
.path
.join(preview_dir
, "alltext.out")
329 os.remove(self._temp_file_in) # so as not to load the old file
333 print(scene
.pov
.text_block
)
334 text
= bpy
.data
.texts
[scene
.pov
.text_block
]
335 with
open(self
._temp
_file
_in
, "w") as file:
336 # Why are the newlines needed?
338 file.write(text
.as_string())
342 # has to be called to update the frame on exporting animations
343 scene
.frame_set(scene
.frame_current
)
345 pov_binary
= PovRender
._locate
_binary
()
348 print("Could not execute POV-Ray, which installation possibly isn't standard ?")
351 # start ini UI options export
352 self
.update_stats("", "POV-Ray 3.7: Exporting ini options from Blender")
354 render
.write_pov_ini(
361 print("***-STARTING-***")
365 if scene
.pov
.command_line_switches
!= "":
366 for new_arg
in scene
.pov
.command_line_switches
.split(" "):
367 extra_args
.append(new_arg
)
369 if platform
.startswith('win'):
370 if "/EXIT" not in extra_args
and not scene
.pov
.pov_editor
:
371 extra_args
.append("/EXIT")
373 # added -d option to prevent render window popup which leads to segfault on linux
374 extra_args
.append("-d")
378 if scene
.pov
.sdl_window_enable
and not platform
.startswith(
380 ): # segfault on linux == False !!!
381 env
= {'POV_DISPLAY_SCALED': 'off'}
382 env
.update(os
.environ
)
383 self
._process
= subprocess
.Popen(
384 [pov_binary
, self
._temp
_file
_ini
],
385 stdout
=subprocess
.PIPE
,
386 stderr
=subprocess
.STDOUT
,
390 self
._process
= subprocess
.Popen(
391 [pov_binary
, self
._temp
_file
_ini
] + extra_args
,
392 stdout
=subprocess
.PIPE
,
393 stderr
=subprocess
.STDOUT
,
397 print("POV-Ray 3.7: could not execute '%s'" % pov_binary
)
400 traceback
.print_exc()
401 print("***-DONE-***")
405 print("Engine ready!...")
406 print("Command line arguments passed: " + str(extra_args
))
408 self
.update_stats("", "POV-Ray 3.7: Parsing File")
410 # Indented in main function now so repeated here but still not working
411 # to bring back render result to its buffer
413 if os
.path
.exists(self
._temp
_file
_out
):
414 xmin
= int(r
.border_min_x
* x
)
415 ymin
= int(r
.border_min_y
* y
)
416 xmax
= int(r
.border_max_x
* x
)
417 ymax
= int(r
.border_max_y
* y
)
418 result
= self
.begin_result(0, 0, x
, y
)
419 lay
= result
.layers
[0]
421 time
.sleep(self
.DELAY
)
423 lay
.load_from_file(self
._temp
_file
_out
)
425 print("***POV ERROR WHILE READING OUTPUT FILE***")
426 self
.end_result(result
)
427 # print(self._temp_file_log) #bring the pov log to blender console with proper path?
430 ) as f
: # The with keyword automatically closes the file when you are done
431 print(f
.read()) # console_write(f.read())
433 self
.update_stats("", "")
435 if scene
.pov
.tempfiles_enable
or scene
.pov
.deletefiles_enable
:
440 # if r.image_settings.file_format == 'OPENEXR':
442 # render.image_settings.color_mode = 'RGBA'
445 # r.image_settings.file_format = 'TARGA'
446 # r.image_settings.color_mode = 'RGBA'
448 blend_scene_name
= bpy
.data
.filepath
.split(os
.path
.sep
)[-1].split(".")[0]
451 image_render_path
= ""
453 # has to be called to update the frame on exporting animations
454 scene
.frame_set(scene
.frame_current
)
456 if not scene
.pov
.tempfiles_enable
:
459 pov_path
= bpy
.path
.abspath(scene
.pov
.scene_path
).replace('\\', '/')
461 if bpy
.data
.is_saved
:
462 pov_path
= bpy
.path
.abspath("//")
464 pov_path
= tempfile
.gettempdir()
465 elif pov_path
.endswith("/"):
467 pov_path
= bpy
.path
.abspath("//")
469 pov_path
= bpy
.path
.abspath(scene
.pov
.scene_path
)
471 if not os
.path
.exists(pov_path
):
473 os
.makedirs(pov_path
)
474 except BaseException
as e
:
476 print('An exception occurred: {}'.format(e
))
479 traceback
.print_exc()
481 print("POV-Ray 3.7: Cannot create scenes directory: %r" % pov_path
)
483 "", "POV-Ray 3.7: Cannot create scenes directory %r" % pov_path
490 image_render_path = bpy.path.abspath(scene.pov.renderimage_path).replace('\\','/')
491 if image_render_path == "":
492 if bpy.data.is_saved:
493 image_render_path = bpy.path.abspath("//")
495 image_render_path = tempfile.gettempdir()
496 #print("Path: " + image_render_path)
497 elif path.endswith("/"):
498 if image_render_path == "/":
499 image_render_path = bpy.path.abspath("//")
501 image_render_path = bpy.path.abspath(scene.pov.)
502 if not os.path.exists(path):
503 print("POV-Ray 3.7: Cannot find render image directory")
504 self.update_stats("", "POV-Ray 3.7: Cannot find render image directory")
510 if scene
.pov
.scene_name
== "":
511 if blend_scene_name
!= "":
512 pov_scene_name
= blend_scene_name
514 pov_scene_name
= "untitled"
516 pov_scene_name
= scene
.pov
.scene_name
517 if os
.path
.isfile(pov_scene_name
):
518 pov_scene_name
= os
.path
.basename(pov_scene_name
)
519 pov_scene_name
= pov_scene_name
.split('/')[-1].split('\\')[-1]
520 if not pov_scene_name
:
521 print("POV-Ray 3.7: Invalid scene name")
522 self
.update_stats("", "POV-Ray 3.7: Invalid scene name")
525 pov_scene_name
= os
.path
.splitext(pov_scene_name
)[0]
527 print("Scene name: " + pov_scene_name
)
528 print("Export path: " + pov_path
)
529 pov_path
= os
.path
.join(pov_path
, pov_scene_name
)
530 pov_path
= os
.path
.realpath(pov_path
)
532 image_render_path
= pov_path
533 # print("Render Image path: " + image_render_path)
536 self
.update_stats("", "POV-Ray 3.7: Exporting data from Blender")
537 self
._export
(depsgraph
, pov_path
, image_render_path
)
538 self
.update_stats("", "POV-Ray 3.7: Parsing File")
540 if not self
._render
(depsgraph
):
541 self
.update_stats("", "POV-Ray 3.7: Not found")
546 # x = int(r.resolution_x * r.resolution_percentage * 0.01)
547 # y = int(r.resolution_y * r.resolution_percentage * 0.01)
549 # Wait for the file to be created
550 # XXX This is no more valid, as 3.7 always creates output file once render is finished!
551 parsing
= re
.compile(br
"= \[Parsing\.\.\.\] =")
552 rendering
= re
.compile(br
"= \[Rendering\.\.\.\] =")
553 percent
= re
.compile(r
"\(([0-9]{1,3})%\)")
554 # print("***POV WAITING FOR FILE***")
559 # POV in Windows did not output its stdout/stderr, it displayed them in its GUI
560 # But now writes file
562 self
.update_stats("", "POV-Ray 3.7: Rendering File")
564 t_data
= self
._process
.stdout
.read(10000)
569 # XXX This is working for UNIX, not sure whether it might need adjustments for
571 # First replace is for windows
572 t_data
= str(t_data
).replace('\\r\\n', '\\n').replace('\\r', '\r')
573 lines
= t_data
.split('\\n')
574 last_line
+= lines
[0]
576 print('\n'.join(lines
), end
="")
577 last_line
= lines
[-1]
579 if rendering
.search(data
):
580 _pov_rendering
= True
581 match
= percent
.findall(str(data
))
583 self
.update_stats("", "POV-Ray 3.7: Rendering File (%s%%)" % match
[-1])
585 self
.update_stats("", "POV-Ray 3.7: Rendering File")
587 elif parsing
.search(data
):
588 self
.update_stats("", "POV-Ray 3.7: Parsing File")
590 if os
.path
.exists(self
._temp
_file
_out
):
591 # print("***POV FILE OK***")
592 # self.update_stats("", "POV-Ray 3.7: Rendering")
596 xmin
= int(r
.border_min_x
* x
)
597 ymin
= int(r
.border_min_y
* y
)
598 xmax
= int(r
.border_max_x
* x
)
599 ymax
= int(r
.border_max_y
* y
)
601 # print("***POV UPDATING IMAGE***")
602 result
= self
.begin_result(0, 0, x
, y
)
603 # XXX, tests for border render.
604 # result = self.begin_result(xmin, ymin, xmax - xmin, ymax - ymin)
605 # result = self.begin_result(0, 0, xmax - xmin, ymax - ymin)
606 lay
= result
.layers
[0]
608 # This assumes the file has been fully written We wait a bit, just in case!
609 time
.sleep(self
.DELAY
)
611 lay
.load_from_file(self
._temp
_file
_out
)
612 # XXX, tests for border render.
613 # lay.load_from_file(self._temp_file_out, xmin, ymin)
615 print("***POV ERROR WHILE READING OUTPUT FILE***")
617 # Not needed right now, might only be useful if we find a way to use temp raw output of
618 # pov 3.7 (in which case it might go under _test_wait()).
621 # possible the image wont load early on.
623 lay.load_from_file(self._temp_file_out)
624 # XXX, tests for border render.
625 #lay.load_from_file(self._temp_file_out, xmin, ymin)
626 #lay.load_from_file(self._temp_file_out, xmin, ymin)
630 # Update while POV-Ray renders
632 # print("***POV RENDER LOOP***")
634 # test if POV-Ray exists
635 if self._process.poll() is not None:
636 print("***POV PROCESS FINISHED***")
641 if self.test_break():
643 self._process.terminate()
644 print("***POV PROCESS INTERRUPTED***")
650 # Would be nice to redirect the output
651 # stdout_value, stderr_value = self._process.communicate() # locks
653 # check if the file updated
654 new_size = os.path.getsize(self._temp_file_out)
656 if new_size != prev_size:
660 time.sleep(self.DELAY)
663 self
.end_result(result
)
665 print("***NO POV OUTPUT IMAGE***")
667 print("***POV INPUT FILE WRITTEN***")
669 # print(filename_log) #bring the pov log to blender console with proper path?
672 self
._temp
_file
_log
, encoding
='utf-8'
673 ) as f
: # The with keyword automatically closes the file when you are done
675 if isinstance(msg
, str):
678 elif type(msg
) == bytes
:
679 #stdmsg = msg.split('\n')
680 stdmsg
= msg
.encode('utf-8', "replace")
681 # stdmsg = msg.encode("utf-8", "replace")
683 # stdmsg = msg.decode(encoding)
685 # msg.encode('utf-8').decode('utf-8')
686 stdmsg
.replace("\t", " ")
687 print(stdmsg
) # console_write(stdmsg) # todo fix segfault and use
688 except FileNotFoundError
:
689 print("No render log to read")
690 self
.update_stats("", "")
692 if scene
.pov
.tempfiles_enable
or scene
.pov
.deletefiles_enable
:
695 sound_on
= bpy
.context
.preferences
.addons
[__package__
].preferences
.use_sounds
696 finished_render_message
= "\'Et Voilà!\'"
698 if platform
.startswith('win') and sound_on
:
699 # Could not find tts Windows command so playing beeps instead :-)
700 # "Korobeiniki"(Коробе́йники)
701 # aka "A-Type" Tetris theme
704 winsound
.Beep(494, 250) # B
705 winsound
.Beep(370, 125) # F
706 winsound
.Beep(392, 125) # G
707 winsound
.Beep(440, 250) # A
708 winsound
.Beep(392, 125) # G
709 winsound
.Beep(370, 125) # F#
710 winsound
.Beep(330, 275) # E
711 winsound
.Beep(330, 125) # E
712 winsound
.Beep(392, 125) # G
713 winsound
.Beep(494, 275) # B
714 winsound
.Beep(440, 125) # A
715 winsound
.Beep(392, 125) # G
716 winsound
.Beep(370, 275) # F
717 winsound
.Beep(370, 125) # F
718 winsound
.Beep(392, 125) # G
719 winsound
.Beep(440, 250) # A
720 winsound
.Beep(494, 250) # B
721 winsound
.Beep(392, 250) # G
722 winsound
.Beep(330, 350) # E
724 winsound
.Beep(440, 250) # A
725 winsound
.Beep(440, 150) # A
726 winsound
.Beep(523, 125) # D8
727 winsound
.Beep(659, 250) # E8
728 winsound
.Beep(587, 125) # D8
729 winsound
.Beep(523, 125) # C8
730 winsound
.Beep(494, 250) # B
731 winsound
.Beep(494, 125) # B
732 winsound
.Beep(392, 125) # G
733 winsound
.Beep(494, 250) # B
734 winsound
.Beep(440, 150) # A
735 winsound
.Beep(392, 125) # G
736 winsound
.Beep(370, 250) # F#
737 winsound
.Beep(370, 125) # F#
738 winsound
.Beep(392, 125) # G
739 winsound
.Beep(440, 250) # A
740 winsound
.Beep(494, 250) # B
741 winsound
.Beep(392, 250) # G
742 winsound
.Beep(330, 300) # E
744 # Mac supports natively say command
745 elif platform
== "darwin":
746 # We don't want the say command to block Python,
747 # so we add an ampersand after the message
748 # but if the os TTS package isn't up to date it
749 # still does thus, the try except clause
751 os
.system("say -v Amelie %s &" % finished_render_message
)
752 except BaseException
as e
:
754 print("your Mac may need an update, try to restart computer")
756 # While Linux frequently has espeak installed or at least can suggest
757 # Maybe windows could as well ?
758 elif platform
== "linux":
759 # We don't want the espeak command to block Python,
760 # so we add an ampersand after the message
761 # but if the espeak TTS package isn't installed it
762 # still does thus, the try except clause
764 os
.system("echo %s | espeak &" % finished_render_message
)
765 except BaseException
as e
:
781 for cls
in reversed(classes
):
782 unregister_class(cls
)