Source code for plotoptix.tkoptix

"""
Tkinter UI for PlotOptiX raytracer.

https://github.com/rnd-team-dev/plotoptix/blob/master/LICENSE.txt

Have a look at examples on GitHub: https://github.com/rnd-team-dev/plotoptix.
"""

import logging
import os
import numpy as np
import tkinter as tk
from tkinter import filedialog

from PIL import Image, ImageTk
from ctypes import byref, c_float, c_uint
from typing import List, Tuple, Optional, Union

from plotoptix.enums import *
from plotoptix._load_lib import PLATFORM
from plotoptix.npoptix import NpOptiX

class TkOptiX(NpOptiX):
    """Tkinter based UI for PlotOptiX. Derived from :class:`plotoptix.NpOptiX`.

    Summary of mouse and keys actions:


    - camera is selected by default, double-click an object/light to select it, double click again to select a primitive within the object, double-click in empty area or double-right-click to select camera
    - select parent mesh to apply transformations to all children as well

    With camera selected:

    - rotate eye around the target: hold and drag left mouse button
    - rotate target around the eye (pan/tilt): hold and drag right mouse button
    - zoom out/in (change field of view): hold shift + left mouse button and drag up/down
    - move eye backward/forward (dolly): hold shift + right mouse button and drag up/down
    - change focus distance in "depth of field" cameras: hold ctrl + left mouse button and drag up/down
    - change aperture radius in "depth of field" cameras: hold ctrl + right mouse button and drag up/down
    - focus at an object: hold ctrl + double-click left mouse button
    - select an object: double-click left mouse button

    With a light or an object / primitive selected:

    - rotate around camera XY (right, up) coordinates: hold and drag left mouse button
    - rotate around camera XZ (right, forward) coordinates: hold ctrl and drag left mouse button
    - move in camera XY (right, up) coordinates: hold shift and drag left mouse button
    - move in camera XZ (right, forward) coordinates: hold and drag right mouse button
    - move in the normal direction (parallelogram light only): shift + right mouse button and drag up/down
    - scale up/down: hold ctrl + shift + left mouse button and drag up/down
    - select camera: double-click left mouse button in empty area or double-right-click anywhere

    Keyboard:

    - save image: F12

    Note: functions with the names ``_gui_*`` can be used from the
    GUI thread (Tk event loop) only.

    Parameters
    ----------
    src : string or dict, optional
        Scene description, file name or dictionary. Empty scene is prepared
        if the default ``None`` value is used.
    on_initialization : callable or list, optional
        Callable or list of callables to execute upon starting the raytracing
        thread. These callbacks are executed on the main thread.
    on_scene_compute : callable or list, optional
        Callable or list of callables to execute upon starting the new frame.
        Callbacks are executed in a thread parallel to the raytracing.
    on_rt_completed : callable or list, optional
        Callable or list of callables to execute when the frame raytracing
        is completed (execution may be paused with pause_compute() method).
        Callbacks are executed in a thread parallel to the raytracing.
    on_launch_finished : callable or list, optional
        Callable or list of callables to execute when the frame raytracing
        is completed. These callbacks are executed on the raytracing thread.
    on_rt_accum_done : callable or list, optional
        Callable or list of callables to execute when the last accumulation
        frame is finished. These callbacks are executed on the raytracing thread.
    width : int, optional
        Pixel width of the raytracing output. Default value is half of the
        screen width.
    height : int, optional
        Pixel height of the raytracing output. Default value is half of the
        screen height.
    start_now : bool, optional
        Open the GUI window and start raytracing thread immediately. If set
        to ``False``, then user should call ``start()`` or ``show()`` method.
        Default is ``False``.
    devices : list, optional
        List of selected devices, with the primary device at index 0. Empty list
        is default, resulting with all compatible devices selected for processing.
    log_level : int or string, optional
        Log output level. Default is ``WARN``.
    """

    def __init__(self,
                 src: Optional[Union[str, dict]] = None,
                 on_initialization = None,
                 on_scene_compute = None,
                 on_rt_completed = None,
                 on_launch_finished = None,
                 on_rt_accum_done = None,
                 width: int = -1,
                 height: int = -1,
                 start_now: bool = False,
                 devices: List = [],
                 log_level: Union[int, str] = logging.WARN) -> None:
        """TkOptiX constructor
        """

        # pass all arguments, except start_now - we'll do that later
        super().__init__(
            src=src,
            on_initialization=on_initialization,
            on_scene_compute=on_scene_compute,
            on_rt_completed=on_rt_completed,
            on_launch_finished=on_launch_finished,
            on_rt_accum_done=on_rt_accum_done,
            width=width, height=height,
            start_now=False, # do not start yet
            devices=devices,
            log_level=log_level)

        # save initial values to set size of Tk window on startup
        self._ini_width = width
        self._ini_height = height

        self._dummy_rgba = np.ascontiguousarray(np.zeros((8, 8, 4), dtype=np.uint8))

        if PLATFORM == "Windows":
            dpi_scale = self._optix.get_display_scaling()
            self._logger.info("DPI scaling: %d", dpi_scale)
            if dpi_scale != 1:
                self._logger.warn("DPI setting may cause blurred raytracing output, see this answer")
                self._logger.warn("for the solution https://stackoverflow.com/a/52599951/10037996:")
                self._logger.warn("set python.exe and pythonw.exe files Properties -> Compatibility")
                self._logger.warn("-> Change high DPI settings -> check Override high DPI scaling")
                self._logger.warn("behaviour, select Application in the drop-down menu.")

        if self._is_scene_created and start_now:
                self._logger.info("Starting TkOptiX window and raytracing thread.")
                self.start()
        ###############################################################

    # For matplotlib users convenience.
[docs] def show(self) -> None: """Start raytracing thread and open the GUI window. Convenience method to call :meth:`plotoptix.NpOptiX.start`. Actions provided with ``on_initialization`` parameter of TkOptiX constructor are executed by this method on the main thread, before the ratracing thread is started and GUI window open. """ self.start()
def _run_event_loop(self): """Override NpOptiX's method for running the UI event loop. Configure the GUI window properties and events, prepare image to display raytracing output. """ # setup Tk window ############################################# self._root = tk.Tk() screen_width = self._root.winfo_screenwidth() screen_height = self._root.winfo_screenheight() if self._ini_width <= 0: self._ini_width = int(screen_width / 2) else: self._ini_width = None if self._ini_height <= 0: self._ini_height = int(screen_height / 2) else: self._ini_height = None self.resize(self._ini_width, self._ini_height) self._mouse_from_x = 0 self._mouse_from_y = 0 self._mouse_to_x = 0 self._mouse_to_y = 0 self._left_mouse = False self._right_mouse = False self._any_mouse = False self._ctrl_key = False self._shift_key = False self._any_key = False self._selection_handle = -1 self._selection_index = -1 self._fixed_size = None self._image_scale = 1.0 self._image_at = (0, 0) self._root.title("R&D PlotOptiX") self._root.protocol("WM_DELETE_WINDOW", self._gui_quit_callback) self._canvas = tk.Canvas(self._root, width=self._width, height=self._height) self._canvas.grid(column=0, row=0, columnspan=3, sticky="nsew") self._canvas.pack_propagate(0) self._canvas.bind("<Configure>", self._gui_configure) self._canvas.bind('<Motion>', self._gui_motion) self._canvas.bind('<B1-Motion>', self._gui_motion_pressed) self._canvas.bind('<B3-Motion>', self._gui_motion_pressed) self._canvas.bind("<Button-1>", self._gui_pressed_left) self._canvas.bind("<Button-3>", self._gui_pressed_right) self._canvas.bind("<ButtonRelease-1>", self._gui_released_left) self._canvas.bind("<ButtonRelease-3>", self._gui_released_right) self._canvas.bind("<Double-Button-1>", self._gui_doubleclick_left) self._canvas.bind("<Double-Button-3>", self._gui_doubleclick_right) self._root.bind_all("<KeyPress>", self._gui_key_pressed) self._root.bind_all("<KeyRelease>", self._gui_key_released) self._canvas.bind("<<LaunchFinished>>", self._gui_update_content) self._canvas.bind("<<ApplyUiEdits>>", self._gui_apply_scene_edits) self._canvas.bind("<<CloseScene>>", self._gui_quit_callback) self._status_main_text = tk.StringVar(value="Selection: camera") self._status_main = tk.Label(self._root, textvariable=self._status_main_text, bd=1, relief=tk.SUNKEN, anchor=tk.W) self._status_main.grid(column=0, row=1, sticky="ew") self._status_action_text = tk.StringVar(value="") self._status_action = tk.Label(self._root, textvariable=self._status_action_text, width=70, bd=1, relief=tk.SUNKEN, anchor=tk.W) self._status_action.grid(column=1, row=1, sticky="ew") self._status_fps_text = tk.StringVar(value="FPS") self._status_fps = tk.Label(self._root, textvariable=self._status_fps_text, width=16, bd=1, relief=tk.SUNKEN, anchor=tk.W) self._status_fps.grid(column=2, row=1, sticky="ew") self._root.rowconfigure(0, weight=1) self._root.columnconfigure(0, weight=1) self._logger.info("Tkinter widgets ready.") self._logger.info("Couple scene to the output window...") with self._padlock: if self._img_rgba is not None: pil_img = Image.fromarray(self._img_rgba, mode="RGBX") else: pil_img = Image.fromarray(self._dummy_rgba, mode="RGBX") self._tk_img = ImageTk.PhotoImage(pil_img) self._img_id = self._canvas.create_image(0, 0, image=self._tk_img, anchor=tk.NW) ############################################################### # start event loop ############################################ self._logger.info("Start UI event loop...") self._is_started = True self._update_req = False self._root.mainloop() ###############################################################
[docs] def close(self) -> None: """Stop the raytracing thread, release resources. Raytracing cannot be restarted after this method is called. See Also -------- :meth:`plotoptix.NpOptiX.close` """ if not self._is_closed: self._optix.break_launch() self._canvas.event_generate("<<CloseScene>>", when="head") else: self._logger.warn("UI already closed.")
def _gui_quit_callback(self, *args): super().close() self._root.quit() def _get_image_xy(self, wnd_x, wnd_y): if self._fixed_size is None: return wnd_x, wnd_y else: x = int((wnd_x - self._image_at[0]) / self._image_scale) y = int((wnd_y - self._image_at[1]) / self._image_scale) return x, y def _get_hit_at(self, x, y): c_x = c_float() c_y = c_float() c_z = c_float() c_d = c_float() if self._optix.get_hit_at(x, y, byref(c_x), byref(c_y), byref(c_z), byref(c_d)): return c_x.value, c_y.value, c_z.value, c_d.value else: return 0, 0, 0, 0 def _gui_get_object_at(self, x, y): c_handle = c_uint() c_index = c_uint() c_prim = c_uint() if self._optix.get_object_at(x, y, byref(c_handle), byref(c_index), byref(c_prim)): return c_handle.value, c_index.value, c_prim.value else: return None, None, None def _gui_motion(self, event): if not (self._any_mouse or self._any_key): x, y = self._get_image_xy(event.x, event.y) handle, index, prim = self._gui_get_object_at(x, y) if (handle != 0x3FFFFFFF): hx, hy, hz, hd = self._get_hit_at(x, y) if handle in self.geometry_names: if (prim != 0xFFFFFFFF): self._status_action_text.set("%s[prim:%d; vtx:%d]: 2D (%d %d), 3D (%f %f %f), at dist.: %f" % (self.geometry_names[handle], prim, index, x, y, hx, hy, hz, hd)) else: self._status_action_text.set("%s[%d]: 2D (%d %d), 3D (%f %f %f), at dist.: %f" % (self.geometry_names[handle], index, x, y, hx, hy, hz, hd)) else: lh = self._optix.get_light_handle(handle, index) if lh in self.light_names: self._status_action_text.set("%s: 2D (%d %d), 3D (%f %f %f), at dist.: %f" % (self.light_names[lh], x, y, hx, hy, hz, hd)) else: self._status_action_text.set("unknown: 2D (%d %d), 3D (%f %f %f), at dist.: %f" % (x, y, hx, hy, hz, hd)) else: self._status_action_text.set("empty area") def _gui_motion_pressed(self, event): self._mouse_to_x, self._mouse_to_y = self._get_image_xy(event.x, event.y) self._optix.break_launch() def _gui_pressed_left(self, event): self._mouse_from_x, self._mouse_from_y = self._get_image_xy(event.x, event.y) self._mouse_to_x = self._mouse_from_x self._mouse_to_y = self._mouse_from_y self._left_mouse = True self._any_mouse = True def _gui_pressed_right(self, event): self._mouse_from_x, self._mouse_from_y = self._get_image_xy(event.x, event.y) self._mouse_to_x = self._mouse_from_x self._mouse_to_y = self._mouse_from_y self._right_mouse = True self._any_mouse = True def _gui_released_left(self, event): self._mouse_to_x, self._mouse_to_y = self._get_image_xy(event.x, event.y) self._mouse_from_x = self._mouse_to_x self._mouse_from_y = self._mouse_to_y self._left_mouse = False self._any_mouse = False def _gui_released_right(self, event): self._mouse_to_x, self._mouse_to_y = self._get_image_xy(event.x, event.y) self._mouse_from_x = self._mouse_to_x self._mouse_from_y = self._mouse_to_y self._right_mouse = False self._any_mouse = False def _gui_doubleclick_left(self, event): assert self._is_started, "Raytracing thread not running." x, y = self._get_image_xy(event.x, event.y) handle, index, _ = self._gui_get_object_at(x, y) if (handle != 0xFFFFFFFF): if handle in self.geometry_names: # switch selection: primitive / whole geom if self._ctrl_key or (self._selection_handle == handle and self._selection_index == -1): self._status_main_text.set("Selection: %s[%d]" % (self.geometry_names[handle], index)) self._selection_index = index else: self._status_main_text.set("Selection: %s" % self.geometry_names[handle]) self._selection_handle = handle self._selection_index = -1 if self._ctrl_key: hx, hy, hz, hd = self._get_hit_at(x, y) if hd > 0: self._status_action_text.set("Focused at (%f %f %f), distance %f" % (hx, hy, hz, hd)) cam_info = self.get_camera() if "fisheye" in cam_info["RayGeneration"]: w = np.array([hx, hy, hz], dtype=np.float32) - np.array(cam_info["Eye"], dtype=np.float32) _ = self._optix.set_camera_focal_length(np.linalg.norm(w)) else: _ = self._optix.set_camera_focal_length(hd) return else: lh = self._optix.get_light_handle(handle, index) if lh in self.light_names: self._status_main_text.set("Selection: %s" % self.light_names[lh]) self._selection_handle = -2 self._selection_index = lh return self._status_main_text.set("Selection: camera") self._selection_handle = -1 self._selection_index = -1
[docs] def select(self, name: Optional[str] = None, index: int = -1): """Select geometry, light or camera. Select object for manual manipulations (rotations, shifts, etc). Geometry or light is selected by its name. If ``name`` is not provided, then active camera is selected. Optional ``index`` allows selection of a primitive within the geometry. Parameters ---------- name : string, optional Name of the geometry or light to select. If ``None`` then active camera is selected. index : int, optional Primitive index to select. Entire geometry is selected if ``index`` is out of primitives range. """ if name is None: self._status_main_text.set("Selection: camera") self._selection_handle = -1 self._selection_index = -1 return if name in self.geometry_data: self._selection_handle = self.geometry_data[name]._handle if index >= 0 and index < self.geometry_data[name]._size: self._status_main_text.set("Selection: %s[%d]" % (name, index)) self._selection_index = index else: self._status_main_text.set("Selection: %s" % name) self._selection_index = -1 return if name in self.light_handles: self._status_main_text.set("Selection: %s" % name) self._selection_handle = -2 self._selection_index = self.light_handles[name] return
def _gui_doubleclick_right(self, event): self._status_main_text.set("Selection: camera") self._selection_handle = -1 self._selection_index = -1 def _gui_save_image(self): filename = filedialog.asksaveasfilename( initialdir=".", title="Save output as image", initialfile="render_output.jpg", defaultextension=".jpg", filetypes=( ("JPEG files", "*.jpg"), ("PNG files", "*.png"), ("TIFF 8-bit files", "*.tif"), # sorry, have to use a different extension to be able to differentiate from 8-bit tif... ("TIFF 16-bit files", "*.tiff") ) ) if filename: fname, fext = os.path.splitext(filename) if fext.lower() == ".tiff": self.save_image(filename, bps="Bps16") else: self.save_image(filename, bps="Bps8") def _gui_key_pressed(self, event): if event.keysym == "Control_L": self._ctrl_key = True self._any_key = True elif event.keysym == "Shift_L": self._shift_key = True self._any_key = True self._any_key = True else: self._any_key = False if event.keysym == "F12": self._gui_save_image() def _gui_key_released(self, event): if event.keysym == "Control_L": self._ctrl_key = False elif event.keysym == "Shift_L": self._shift_key = False self._any_key = False
[docs] def get_rt_size(self) -> Tuple[int, int]: """Get size of ray-tracing output image. Get fixed dimensions of the output image or ``None`` if the image is fit to the GUI window size. Returns ------- out : tuple (int, int) Output image size or ``None`` if set auto-fit mode. See Also -------- :meth:`plotoptix.NpOptiX.get_size` """ return self._fixed_size
[docs] def set_rt_size(self, size: Tuple[int, int]) -> None: """Set fixed / free size of ray-tracing output image. Set fixed dimensions of the output image or allow automatic fit to the GUI window size. Fixed size image updates are slower, but allow ray tracing of any size. Default mode is fit to the GUI window size. Parameters ---------- size : tuple (int, int) Output image size or ``None`` to set auto-fit mode. """ assert self._is_started, "Raytracing thread not running." if self._fixed_size == size: return self._fixed_size = size with self._padlock: if self._fixed_size is None: w, h = self._canvas.winfo_width(), self._canvas.winfo_height() else: w, h = self._fixed_size self.resize(width=w, height=h)
def _gui_internal_image_update(self): if self._img_rgba is not None: pil_img = Image.fromarray(self._img_rgba, mode="RGBX") else: pil_img = Image.fromarray(self._dummy_rgba, mode="RGBX") move_to = (0, 0) self._image_scale = 1.0 if self._fixed_size is not None: wc, hc = self._canvas.winfo_width(), self._canvas.winfo_height() if self._width / wc > self._height / hc: self._image_scale = wc / self._width hnew = int(self._height * self._image_scale) pil_img = pil_img.resize((wc, hnew), Image.ANTIALIAS) move_to = (0, (hc - hnew) // 2) else: self._image_scale = hc / self._height wnew = int(self._width * self._image_scale) pil_img = pil_img.resize((wnew, hc), Image.ANTIALIAS) move_to = ((wc - wnew) // 2, 0) tk_img = ImageTk.PhotoImage(pil_img) # update image on canvas self._canvas.itemconfig(self._img_id, image=tk_img) if self._image_at != move_to: self._canvas.move(self._img_id, -self._image_at[0], -self._image_at[1]) self._canvas.move(self._img_id, move_to[0], move_to[1]) self._image_at = move_to # swap reference stored in the window instance self._tk_img = tk_img # no redraws until the next launch self._update_req = False def _gui_configure(self, event): assert self._is_started, "Raytracing thread not running." if not self._started_event.is_set(): self._started_event.set() with self._padlock: if self._fixed_size is None: w, h = self._canvas.winfo_width(), self._canvas.winfo_height() if (w == self._width) and (h == self._height): return self._logger.info("Resize to: %d x %d", w, h) self.resize(width=w, height=h) self._gui_internal_image_update() ########################################################################### # update raytraced image in Tk window ######## def _gui_update_content(self, *args): assert self._is_started, "Raytracing thread not running." if self._update_req: self._status_fps_text.set("FPS: %.3f" % self._optix.get_fps()) with self._padlock: self._gui_internal_image_update() def _launch_finished_callback(self, rt_result: int): super()._launch_finished_callback(rt_result) if self._is_started and rt_result < RtResult.NoUpdates.value: self._update_req = True self._canvas.event_generate("<<LaunchFinished>>", when="now") ########################################################################### ########################################################################### # apply manual scene edits made in ui ######## def _gui_apply_scene_edits(self, *args): if (self._mouse_from_x == self._mouse_to_x) and (self._mouse_from_y == self._mouse_to_y): return if self._selection_handle == -1: # manipulate camera: if self._left_mouse: if not self._any_key: self._status_action_text.set("rotate camera eye XZ") self._optix.rotate_camera_eye( self._mouse_from_x, self._mouse_from_y, self._mouse_to_x, self._mouse_to_y) elif self._ctrl_key: self._status_action_text.set("change camera focus") df = 1 + 0.01 * (self._mouse_from_y - self._mouse_to_y) f = self._optix.get_camera_focal_scale(0) # 0 is current cam self._optix.set_camera_focal_scale(df * f) elif self._shift_key: self._status_action_text.set("change camera FoV") df = 1 + 0.005 * (self._mouse_from_y - self._mouse_to_y) f = self._optix.get_camera_fov(0) # 0 is current cam self._optix.set_camera_fov(df * f) elif self._right_mouse: if not self._any_key: self._status_action_text.set("camera pan/tilt") self._optix.rotate_camera_tgt( self._mouse_from_x, self._mouse_from_y, self._mouse_to_x, self._mouse_to_y) elif self._ctrl_key: self._status_action_text.set("change camera aperture") da = 1 + 0.01 * (self._mouse_from_y - self._mouse_to_y) a = self._optix.get_camera_aperture(0) # 0 is current cam self._optix.set_camera_aperture(da * a) elif self._shift_key: self._status_action_text.set("camera dolly") target = np.ascontiguousarray([0, 0, 0], dtype=np.float32) self._optix.get_camera_target(0, target.ctypes.data) # 0 is current cam eye = np.ascontiguousarray([0, 0, 0], dtype=np.float32) self._optix.get_camera_eye(0, eye.ctypes.data) # 0 is current cam dl = 0.01 * (self._mouse_from_y - self._mouse_to_y) eye = eye - dl * (target - eye) self._optix.set_camera_eye(eye.ctypes.data) elif self._selection_handle == -2: # manipulate light: if self._selection_index in self.light_names: name = self.light_names[self._selection_index] if self._left_mouse: if not self._any_key: rx = np.pi * (self._mouse_to_y - self._mouse_from_y) / self._height ry = np.pi * (self._mouse_to_x - self._mouse_from_x) / self._width self._status_action_text.set("rotate light in camera XY") self._optix.rotate_light_in_view(name, rx, ry, 0) elif self._ctrl_key and self._shift_key: s = 1 - (self._mouse_to_y - self._mouse_from_y) / self._height self._status_action_text.set("scale light") self._optix.scale_light(name, s) elif self._ctrl_key: rx = np.pi * (self._mouse_to_y - self._mouse_from_y) / self._height rz = np.pi * (self._mouse_from_x - self._mouse_to_x) / self._width self._status_action_text.set("rotate light in camera XZ") self._optix.rotate_light_in_view(name, rx, 0, rz) elif self._shift_key: dx = (self._mouse_to_x - self._mouse_from_x) / self._width dy = (self._mouse_from_y - self._mouse_to_y) / self._height self._status_action_text.set("move light in camera XY") self._optix.move_light_in_view(name, dx, dy, 0) elif self._right_mouse: if not self._any_key: dx = (self._mouse_to_x - self._mouse_from_x) / self._width dz = (self._mouse_to_y - self._mouse_from_y) / self._height self._status_action_text.set("move light in camera XZ") self._optix.move_light_in_view(name, dx, 0, dz) elif self._shift_key: dx = (self._mouse_from_y - self._mouse_to_y) / self._height self._status_action_text.set("move light in normal direction") self._optix.dolly_light(name, dx) else: # manipulate selected ogject name = self.geometry_names[self._selection_handle] if not self._optix.sync_geometry_data(name): self._logger.error("CPU data not synced to GPU copies.") if self._left_mouse: if not self._any_key: rx = np.pi * (self._mouse_to_y - self._mouse_from_y) / self._height ry = np.pi * (self._mouse_to_x - self._mouse_from_x) / self._width if self._selection_index == -1: self._status_action_text.set("rotate geometry in camera XY") self._optix.rotate_geometry_in_view(name, rx, ry, 0, True) else: self._status_action_text.set("rotate primitive in camera XY") self._optix.rotate_primitive_in_view(name, self._selection_index, rx, ry, 0, True) elif self._ctrl_key and self._shift_key: s = 1 - (self._mouse_to_y - self._mouse_from_y) / self._height if self._selection_index == -1: self._status_action_text.set("scale geometry") self._optix.scale_geometry(name, s, True) else: self._status_action_text.set("scale primitive") self._optix.scale_primitive(name, self._selection_index, s, True) elif self._ctrl_key: rx = np.pi * (self._mouse_to_y - self._mouse_from_y) / self._height rz = np.pi * (self._mouse_from_x - self._mouse_to_x) / self._width if self._selection_index == -1: self._status_action_text.set("rotate geometry in camera XZ") self._optix.rotate_geometry_in_view(name, rx, 0, rz, True) else: self._status_action_text.set("rotate primitive in camera XY") self._optix.rotate_primitive_in_view(name, self._selection_index, rx, 0, rz, True) elif self._shift_key: dx = (self._mouse_to_x - self._mouse_from_x) / self._width dy = (self._mouse_from_y - self._mouse_to_y) / self._height if self._selection_index == -1: self._status_action_text.set("move geometry in camera XY") self._optix.move_geometry_in_view(name, dx, dy, 0, True) else: self._status_action_text.set("move primitive in camera XY") self._optix.move_primitive_in_view(name, self._selection_index, dx, dy, 0, True) elif self._right_mouse: if not self._any_key: dx = (self._mouse_to_x - self._mouse_from_x) / self._width dz = (self._mouse_to_y - self._mouse_from_y) / self._height if self._selection_index == -1: self._status_action_text.set("move geometry in camera XZ") self._optix.move_geometry_in_view(name, dx, 0, dz, True) else: self._status_action_text.set("move primitive in camera XZ") self._optix.move_primitive_in_view(name, self._selection_index, dx, 0, dz, True) self._mouse_from_x = self._mouse_to_x self._mouse_from_y = self._mouse_to_y def _scene_rt_starting_callback(self): if self._is_started: super()._scene_rt_starting_callback() self._canvas.event_generate("<<ApplyUiEdits>>", when="now") ###########################################################################