binding movement and rotation of kivy scatter widgets

Question:

Below I have a code that displays 2 scatter widgets in a kivy screen. If one widget is dragged, the other also drags and vice versa.

What I want is in addition to movement, if one is rotated, the other rotates in the same way. In other words, they are exactly mimicking each other with both rotation and position.

The problem I am facing is that kivy’s default position is the bottom left of the widget. So, if the user rotates one widget from some random axis point, the other widget rotates at the bottom left. When introducing rotation while the positions are locked, it becomes all messed up, and I cannot find a way to overcome this bug in order to match both rotation and movement.

Here is code with positions locked.

from kivy.uix.scatter import Scatter
from kivy.uix.relativelayout import RelativeLayout
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.lang import Builder

class ScatterWidget(Scatter):
    pass

class SquareWidget(Widget):
    pass

class WindowManager(ScreenManager):
    pass

class GeneralWindow(Screen,RelativeLayout):

    start_position_1=(200,200)
    start_position_2=(300,400)
    def on_touch_move(self, touch):
        app=self.manager.get_screen('General')
        pos1=app.ids['widget10'].parent.to_parent(*self.pos)
        pos2=app.ids['widget20'].parent.to_parent(*self.pos)
        mov1=(pos1[0]-self.start_position_1[0],pos1[1]-self.start_position_1[1])
        mov2=(pos2[0]-self.start_position_2[0],pos2[1]-self.start_position_2[1])

        if self.ids.one.collide_point(*touch.pos):
            app.ids['two'].pos=(mov1[0]+self.start_position_2[0],mov1[1]+self.start_position_2[1])
        if self.ids.two.collide_point(*touch.pos):
            app.ids['one'].pos =(mov2[0]+self.start_position_1[0],mov2[1]+self.start_position_1[1])

KV = Builder.load_string("""

WindowManager:
    GeneralWindow:

<GeneralWindow>:
    name: 'General'
    RelativeLayout:
        canvas: 
            Color: 
                rgba: 0,0,0,0.3
            Rectangle: 
                size: self.size
                pos: self.pos
        
        ScatterWidget:
            do_scale: False
            id: one
            size_hint: None,None
            size: widget10.size
            rotation: 0
            pos: root.start_position_1

            SquareWidget:
                id: widget10
                size: (200,20)
                canvas: 
                    Color:
                        rgba: 1,1,0,0.7
                    Rectangle:
                        size: self.size
                        pos:  self.pos  

        ScatterWidget:
            do_scale: False
            id: two
            size_hint: None,None
            size: widget20.size
            rotation: 0
            pos: root.start_position_2

            SquareWidget:
                id: widget20
                size: (200,20)
                canvas: 
                    Color:
                        rgba: 0,1,0,0.7
                    Rectangle:
                        size: self.size
                        pos:  self.pos 

""")

class TApp(App):

    def build(self):
        return KV

if __name__ == '__main__':
    TApp().run()
Asked By: BH10001

||

Answers:

The Scatter widget has a rotation property, that performs a rotation about the center of the widget. By using this property, and specifying do_rotation: False for both of your ScatterWidgets, I think your problem is simplified. Also, modifying your movement code to work on the center property of the widgets adds another simplification.

Here is a modified version of your code that does this. In this code, the left button is used to move the widgets and the right button is used to rotate the widgets:

from kivy.config import Config
Config.set('input', 'mouse', 'mouse,disable_multitouch')

from kivy.app import App
from kivy.uix.scatter import Scatter
from kivy.uix.relativelayout import RelativeLayout
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.widget import Widget
from kivy.lang import Builder
from kivy.vector import Vector


class ScatterWidget(Scatter):

    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos) and touch.button == 'right':
            touch.grab(self)
            return True
        return super(ScatterWidget, self).on_touch_down(touch)

    def on_touch_move(self, touch):
        if touch.grab_current is self and touch.button == 'right':
            self.rotation = -Vector(1, 0).angle(Vector(touch.pos) - Vector(self.center))
            return True
        return super(ScatterWidget, self).on_touch_move(touch)

    def on_touch_up(self, touch):
        if touch.grab_current is self:
            touch.ungrab(self)
        return super(ScatterWidget, self).on_touch_up(touch)


class SquareWidget(Widget):
    pass


class WindowManager(ScreenManager):
    pass


class GeneralWindow(Screen, RelativeLayout):
    start_center_1 = (200, 200)
    start_center_2 = (300, 400)

    def on_touch_move(self, touch):
        app = self.manager.get_screen('General')
        scatter_one = app.ids['one']
        scatter_two = app.ids['two']
        center1 = scatter_one.center
        center2 = scatter_two.center
        mov1 = (center1[0] - self.start_center_1[0], center1[1] - self.start_center_1[1])
        mov2 = (center2[0] - self.start_center_2[0], center2[1] - self.start_center_2[1])

        # convert touch.grab_list of WeakReferences to a grab_list of actual objects
        grab_list = []
        for wr in touch.grab_list:
            grab_list.append(wr())

        if scatter_one in grab_list:
            if touch.button == 'right':
                scatter_two.rotation = scatter_one.rotation
            else:
                scatter_two.center = (mov1[0] + self.start_center_2[0], mov1[1] + self.start_center_2[1])
        elif scatter_two in grab_list:
            if touch.button == 'right':
                scatter_one.rotation = scatter_two.rotation
            else:
                scatter_one.center = (mov2[0] + self.start_center_1[0], mov2[1] + self.start_center_1[1])

        return super(GeneralWindow, self).on_touch_move(touch)


KV = Builder.load_string("""

WindowManager:
    GeneralWindow:

<GeneralWindow>:
    name: 'General'
    RelativeLayout:
        canvas: 
            Color: 
                rgba: 0,0,0,0.3
            Rectangle: 
                size: self.size
                pos: self.pos

        ScatterWidget:
            do_scale: False
            do_rotation: False
            id: one
            size_hint: None,None
            size: widget10.size
            rotation: 0
            center: root.start_center_1

            SquareWidget:
                id: widget10
                size: (200,20)
                canvas: 
                    Color:
                        rgba: 1,1,0,0.7
                    Rectangle:
                        size: self.size
                        pos:  self.pos  

        ScatterWidget:
            do_scale: False
            do_rotation: False
            id: two
            size_hint: None,None
            size: widget20.size
            rotation: 0
            center: root.start_center_2

            SquareWidget:
                id: widget20
                size: (200,20)
                canvas: 
                    Color:
                        rgba: 0,1,0,0.7
                    Rectangle:
                        size: self.size
                        pos:  self.pos 

""")


class TApp(App):

    def build(self):
        return KV


if __name__ == '__main__':
    TApp().run()
Answered By: John Anderson