DearPyGui icon indicating copy to clipboard operation
DearPyGui copied to clipboard

dpg.configure_item() Ignores Translation in Transform Matrix on Draw Nodes

Open jacobnrohan opened this issue 1 year ago • 7 comments

Version of Dear PyGui

Version: 1.11.1 Operating System: Debian Bookworm (also tested on Windows 11)

My Issue

For drawings on a draw node with a transform, dpg.configure_item does not work properly. In these cases, it seems to ignore the 4th column (translate values) of the 4x4 transform matrix.

Deleting and re-creating the drawing produces the expected behavior. When dpg.configure_item() is used, drawings are effected by the scale of the transform matrix, but are not property translated.

To Reproduce

Steps to reproduce the behavior: (see code)

  1. Create a draw node and apply a transformation and drawing
  2. Update that drawing using dpg.configure_item() to change its position
  3. notice how the new position is no longer effected by the transformation's translation, but is still effected by scale

Expected behavior

When dpg.configure_item() is used to change a drawing's position, it should remain fully effected by the draw node transform.

Deleting then re-drawing the item produces the expected behavior but is not a practical alternative.

Screenshots/Video

configure_item

Standalone, minimal, complete and verifiable example

import dearpygui.dearpygui as dpg
from dearpygui._dearpygui import mvMat4

class application:

    def __init__(self, ):

        # init variables for origin/dragging
        self.scale = 0
        self.origin_pre = [200.0, 200.0]
        self.dragfrom = [0.0, 0.0]
        self.dragto = [0.0, 0.0]
        self.__any_hovered__ = False

        # init dpg
        dpg.create_context()
        dpg.create_viewport(title='issue with dpg.configure_item()',vsync=False)
        dpg.setup_dearpygui()

        # init a viewport and draw node
        self.id_background = dpg.add_viewport_drawlist(front=False, tag="background")
        dpg.add_draw_node(tag="draw_node", parent="background")
        dpg.apply_transform("draw_node", self.transform)

        # init left-click-drag to move drawings
        self.dragging_mouse = False
        with dpg.handler_registry():
            dpg.add_mouse_click_handler(callback=self.mouse_click_handler)
            dpg.add_mouse_move_handler(callback=self.mouse_move_handler)
            dpg.add_mouse_release_handler(callback=self.mouse_release_handler)
            dpg.add_mouse_wheel_handler(callback=self.mouse_wheel_handler)

        # add basic drawings
        dpg.draw_circle(center=[0, 0], radius=5, fill=[255, 0, 0], color=[0, 0, 0, 0], parent="draw_node") # origin
        dpg.draw_text(pos=[0, 0], text='(0, 0)', color=[255, 0, 0], size=32, parent="draw_node")
        dpg.draw_circle(center=[200, 0], radius=5, fill=[255, 0, 0], color=[0, 0, 0, 0], parent="draw_node") # origin
        dpg.draw_text(pos=[200, 0], text='(200, 0)', color=[255, 0, 0], size=32, parent="draw_node")
        self.id = dpg.draw_rectangle(pmin=[0, 0], pmax=[100, 100], fill=[0, 0, 255, 128], parent="draw_node")
        dpg.draw_rectangle(pmin=[200, 0], pmax=[300, 100], fill=[0, 0, 0], parent="draw_node")

        # add controls to update drawing
        with dpg.window(label="move object",no_background=False):
            dpg.add_text("left-click and drag the background to update the draw_node transform.")
            dpg.add_text("scroll to zoom in and out.")
            self.button0 = dpg.add_button(label="reset", callback=self.reset)
            self.button1 = dpg.add_button(label="dpg.configure_item", callback=self.move_rectangle)
            self.button2 = dpg.add_button(label="dpg.delete_item and dpg.draw_rectangle", callback=self.replace_rectangle)
        with dpg.tooltip(self.button1, delay=0.10, hide_on_activity=True):
            dpg.add_text('actual behavior: demo shows that dpg.configure_item does not\n'\
                         'properly update drawings on draw nodes w/ transformations.')
        with dpg.tooltip(self.button2, delay=0.10, hide_on_activity=True):
            dpg.add_text('expected behavior')

        # main loop
        dpg.show_viewport()
        dpg.start_dearpygui()
        dpg.destroy_context()

    @property
    def any_hovered(self):
        # https://github.com/hoffstadt/DearPyGui/issues/2281
        return any([s["hovered"] for s in [dpg.get_item_state(w) for w in dpg.get_windows()] if ("hovered" in s.keys())])

    def mouse_click_handler(self, sender, app_data, user_data):
        if app_data == 0: # left click
            if not self.__any_hovered__ and not self.dragging_mouse:
                self.dragging_mouse = True
                xy = dpg.get_mouse_pos(local=False)
                # print('mouse_click_handler xy:', xy)
                self.dragfrom = xy
                self.dragto = xy

    def mouse_move_handler(self, sender, app_data, user_data):
        self.__any_hovered__ = self.any_hovered
        # print("any_hovered?:", self.any_hovered)
        if self.dragging_mouse:
            xy = dpg.get_mouse_pos(local=False)
            # print('mouse_move_handler xy:', xy)
            self.dragto = xy
            dpg.apply_transform("draw_node", self.transform)

    def mouse_release_handler(self, sender, app_data, user_data):
        if app_data == 0: # left click
            if not self.__any_hovered__ and self.dragging_mouse:
                self.dragging_mouse = False
                self.origin_pre = self.origin
                self.dragfrom = [0.0, 0.0]
                self.dragto = [0.0, 0.0]
                # print('mouse_release_handler')

    def mouse_wheel_handler(self, sender, app_data, user_data):
        if not self.__any_hovered__:
            if app_data > 0:
                self.scale += 1
            elif app_data < 0:
                self.scale -= 1
            dpg.apply_transform("draw_node", self.transform)        

    @property
    def origin(self):
        return [o + t - f for o, f, t in zip(self.origin_pre, self.dragfrom, self.dragto)]
    
    @property
    def transform(self):
        s = 2 ** (self.scale / 8)
        x, y = self.origin
        return mvMat4(
              s, 0.0, 0.0, x,
            0.0,  -s, 0.0, y,
            0.0, 0.0, 1.0, 0.0,
            0.0, 0.0, 0.0, 1.0)

    def move_rectangle(self, sender, app_data, user_data):
        dpg.configure_item(self.id, pmin=[200, 0], pmax=[300, 100], parent="draw_node") # TODO: NEEDS FIX

    def replace_rectangle(self, sender, app_data, user_data):
        dpg.delete_item(self.id)
        self.id = dpg.draw_rectangle(pmin=[200, 0], pmax=[300, 100], fill=[0, 0, 255, 128], parent="draw_node")

    def reset(self, sender, app_data, user_data):
        dpg.delete_item(self.id)
        self.id = dpg.draw_rectangle(pmin=[0, 0], pmax=[100, 100], fill=[0, 0, 255, 128], parent="draw_node")

if __name__ == "__main__":
    app = application()

jacobnrohan avatar Aug 22 '24 16:08 jacobnrohan

A quick tip: instead of specifying parent="draw_node" on every primitive, you can do it this way:

# init a viewport and draw node
with dpg.viewport_drawlist(front=False, tag="background") as self.id_background:
    with dpg.draw_node(tag="draw_node", parent="background"):
        dpg.apply_transform("draw_node", self.transform)

        dpg.draw_circle(center=[0, 0], radius=5, fill=[255, 0, 0], color=[0, 0, 0, 0]) # origin
        dpg.draw_text(pos=[0, 0], text='(0, 0)', color=[255, 0, 0], size=32)
        dpg.draw_circle(center=[200, 0], radius=5, fill=[255, 0, 0], color=[0, 0, 0, 0]) # origin
        dpg.draw_text(pos=[200, 0], text='(200, 0)', color=[255, 0, 0], size=32)
        self.id = dpg.draw_rectangle(pmin=[0, 0], pmax=[100, 100], fill=[0, 0, 255, 128])
        dpg.draw_rectangle(pmin=[200, 0], pmax=[300, 100], fill=[0, 0, 0])

It's less typing and easier to read and edit.

v-ein avatar Aug 22 '24 18:08 v-ein

Thanks for the tip @v-ein! I've updated the code accordingly.

The issue persists. I've notice that this issue persist when using dpg.create_scale_matrix() and dpg.create_translation_matrix() instead of mvMat4.

updated code
import dearpygui.dearpygui as dpg
from dearpygui._dearpygui import mvMat4

# posted to https://github.com/hoffstadt/DearPyGui/issues/2382

class application:

    def __init__(self, ):

        # init variables for origin/dragging
        self.scale = 0
        self.origin_pre = [200.0, 200.0]
        self.dragfrom = [0.0, 0.0]
        self.dragto = [0.0, 0.0]
        self.__any_hovered__ = False

        # init dpg
        dpg.create_context()
        dpg.create_viewport(title='issue with dpg.configure_item()',vsync=False)
        dpg.setup_dearpygui()

        # init a viewport and draw node
        with dpg.viewport_drawlist(front=False, tag="background") as id_background:
            self.id_background = id_background
            with dpg.draw_node(tag="draw_node", parent="background"):
                dpg.apply_transform("draw_node", self.transform)

                dpg.draw_circle(center=[0, 0], radius=5, fill=[255, 0, 0], color=[0, 0, 0, 0]) # origin
                dpg.draw_text(pos=[0, 0], text='(0, 0)', color=[255, 0, 0], size=32)
                dpg.draw_circle(center=[200, 0], radius=5, fill=[255, 0, 0], color=[0, 0, 0, 0]) # origin
                dpg.draw_text(pos=[200, 0], text='(200, 0)', color=[255, 0, 0], size=32)
                self.id = dpg.draw_rectangle(pmin=[0, 0], pmax=[100, 100], fill=[0, 0, 255, 128])
                dpg.draw_rectangle(pmin=[200, 0], pmax=[300, 100], fill=[0, 0, 0])

        # init left-click-drag to move drawings
        self.dragging_mouse = False
        with dpg.handler_registry():
            dpg.add_mouse_click_handler(callback=self.mouse_click_handler)
            dpg.add_mouse_move_handler(callback=self.mouse_move_handler)
            dpg.add_mouse_release_handler(callback=self.mouse_release_handler)
            dpg.add_mouse_wheel_handler(callback=self.mouse_wheel_handler)

        # add controls to update drawing
        with dpg.window(label="move object",no_background=False):
            dpg.add_text("left-click and drag the background to update the draw_node transform.")
            dpg.add_text("scroll to zoom in and out.")
            self.button0 = dpg.add_button(label="reset", callback=self.reset)
            self.button1 = dpg.add_button(label="dpg.configure_item", callback=self.move_rectangle)
            self.button2 = dpg.add_button(label="dpg.delete_item and dpg.draw_rectangle", callback=self.replace_rectangle)

        # with dpg.tooltip(self.button0, delay=0.10, hide_on_activity=True):
        #     dpg.add_text('reset the demo')
        with dpg.tooltip(self.button1, delay=0.10, hide_on_activity=True):
            dpg.add_text('actual behavior: demo shows that dpg.configure_item does not\n'\
                         'properly update drawings on draw nodes w/ transformations.')
        with dpg.tooltip(self.button2, delay=0.10, hide_on_activity=True):
            dpg.add_text('expected behavior')

        # main loop
        dpg.show_viewport()
        dpg.start_dearpygui()
        dpg.destroy_context()

    @property
    def any_hovered(self):
        # https://github.com/hoffstadt/DearPyGui/issues/2281
        return any([s["hovered"] for s in [dpg.get_item_state(w) for w in dpg.get_windows()] if ("hovered" in s.keys())])

    def mouse_click_handler(self, sender, app_data, user_data):
        if app_data == 0: # left click
            if not self.__any_hovered__ and not self.dragging_mouse:
                self.dragging_mouse = True
                xy = dpg.get_mouse_pos(local=False)
                # print('mouse_click_handler xy:', xy)
                self.dragfrom = xy
                self.dragto = xy

    def mouse_move_handler(self, sender, app_data, user_data):
        self.__any_hovered__ = self.any_hovered
        # print("any_hovered?:", self.any_hovered)
        if self.dragging_mouse:
            xy = dpg.get_mouse_pos(local=False)
            # print('mouse_move_handler xy:', xy)
            self.dragto = xy
            dpg.apply_transform("draw_node", self.transform)

    def mouse_release_handler(self, sender, app_data, user_data):
        if app_data == 0: # left click
            if not self.__any_hovered__ and self.dragging_mouse:
                self.dragging_mouse = False
                self.origin_pre = self.origin
                self.dragfrom = [0.0, 0.0]
                self.dragto = [0.0, 0.0]
                # print('mouse_release_handler')

    def mouse_wheel_handler(self, sender, app_data, user_data):
        if not self.__any_hovered__:
            if app_data > 0:
                self.scale += 1
            elif app_data < 0:
                self.scale -= 1
            dpg.apply_transform("draw_node", self.transform)        

    @property
    def origin(self):
        return [o + t - f for o, f, t in zip(self.origin_pre, self.dragfrom, self.dragto)]
    
    @property
    def transform(self):
        s = 2 ** (self.scale / 8)
        x, y = self.origin
        # return mvMat4(
        #       s, 0.0, 0.0, x,
        #     0.0,   s, 0.0, y,
        #     0.0, 0.0, 1.0, 0.0,
        #     0.0, 0.0, 0.0, 1.0)
        return dpg.create_scale_matrix(scales=[s, s, 1, 1]) * dpg.create_translation_matrix([x, y])

    def move_rectangle(self, sender, app_data, user_data):
        dpg.configure_item(self.id, pmin=[200, 0], pmax=[300, 100], parent="draw_node") # TODO: NEEDS FIX

    def replace_rectangle(self, sender, app_data, user_data):
        dpg.delete_item(self.id)
        self.id = dpg.draw_rectangle(pmin=[200, 0], pmax=[300, 100], fill=[0, 0, 255, 128], parent="draw_node")

    def reset(self, sender, app_data, user_data):
        dpg.delete_item(self.id)
        self.id = dpg.draw_rectangle(pmin=[0, 0], pmax=[100, 100], fill=[0, 0, 255, 128], parent="draw_node")

if __name__ == "__main__":
    app = application()

jacobnrohan avatar Aug 28 '24 14:08 jacobnrohan

Yep, I was able to recreate it too. Not sure why it happens; needs some research.

v-ein avatar Aug 28 '24 16:08 v-ein

Any progress so far? I guess the workaround is deleting and redrawing primitives every frame?

zznewclear13 avatar Nov 08 '25 15:11 zznewclear13

Any progress so far?

Not yet, unfortunately. No ETA either.

I guess the workaround is deleting and redrawing primitives every frame?

Not sure about "every frame", looks more like "each time you need to change its pmin/pmax"?

v-ein avatar Nov 09 '25 19:11 v-ein

Ok, thanks. I made an animation with primitives so I have to update every frame.

zznewclear13 avatar Nov 10 '25 03:11 zznewclear13

@v-ein Hi, just want to let you know, adding

	_pmax.w = 1.0f;
	_pmin.w = 1.0f;

to src/mvDrawings.cpp line 1512-1513, function void mvDrawRect::handleSpecificKeywordArgs(PyObject* dict) solves this issue.

zznewclear13 avatar Nov 10 '25 08:11 zznewclear13