Forum Archive

draw ui.Path within the coordinate system of a node

zipit

Hi,

I do not really understand how the frame of an ui.Path object works. If have cooked up a small scene that illustrates the problems I have. I am trying to draw an ui.Path that is matching points (coordinates) in the frame of a node. It seems that an ui.Path is always being centered on the center of the presenting ShapeNode.

So my question is: How do I draw a path which is not centered on the center of its presenting ShapeNode - for example one that only lives in the x+y+ frame ? I would like to be able to actually draw within the coordinates system/frame of the presenting ShapeNode .

Cheers,
zipit

```
from scene import *

class MyScene (Scene):
def setup(self):
sx, sy = self.size.w * .5, self.size.h * .5
# I would expect the white rect to have its lower left corner
# at the center of the screen. But it does not, it is sitting
# on the origin of the node, x and y seem to have no effect.
self.white = ShapeNode(ui.Path.rect(sx, sy, 200, 200),
parent=self,
position=(0, 0))
# a reference rect as our white rect is kinda off screen
self.red = ShapeNode(ui.Path.rect(0, 0, 150, 150),
parent=self,
fill_color = 'red',
position=(sx, sy))
# Here I would expect a line from the right top corner
# of the red rect going to a point (25, 50) in the
# top right direction. But again the path is centered
# on the node and also the y coordinate is being inverted.
path = ui.Path()
path.move_to(75, 75)
path.line_to(sx + 100, sy + 125)
path.line_width = 3
self.cyan = ShapeNode(path,
parent=self.red,
stroke_color='cyan',
position=(0, 0))

if name == 'main':
run(MyScene(), show_fps=False)```

JonB

@zipit said:

from scene import *

class MyScene (Scene):
def setup(self):
sx, sy = self.size.w * .5, self.size.h * .5
# I would expect the white rect to have its lower left corner
# at the center of the screen. But it does not, it is sitting
# on the origin of the node, x and y seem to have no effect.
self.white = ShapeNode(ui.Path.rect(sx, sy, 200, 200),
parent=self,
position=(0, 0))
# a reference rect as our white rect is kinda off screen
self.red = ShapeNode(ui.Path.rect(0, 0, 150, 150),
parent=self,
fill_color = 'red',
position=(sx, sy))
# Here I would expect a line from the right top corner
# of the red rect going to a point (25, 50) in the
# top right direction. But again the path is centered
# on the node and also the y coordinate is being inverted.
path = ui.Path()
path.move_to(75, 75)
path.line_to(sx + 100, sy + 125)
path.line_width = 3
self.cyan = ShapeNode(path,
parent=self.red,
stroke_color='cyan',
position=(0, 0))

if name == 'main':
run(MyScene(), show_fps=False)```

anchor_point can help in some cases.

but, the problem seems to be that, no matter what you draw in a shape node, the coordinate system of the path is not honored, instead the bbox of the resulting path is computed, and then you can anchor it to a point relative to the bounding box. That makes it easier to do some things, but harder for others... for instance a diagonal line from (0,0) to (x,y), depending on the sign of x and y, the same anchor point (0,0) refers to either the start, end, or diagonal corner of the line.

I seem to recall a forum post with a workaround that I posted, but my Google-fu is failing me this morning. I think you basically have to keep track of the math yourself on what the "center" and bounds of the path will be, and the use the anchor point to keep things aligned.

omz

A ShapeNode is basically just a SpriteNode that automatically creates its Texture using a path. The code is pretty simple, and you can actually look at it yourself by opening "Modules & Templates/Standard Library/site-packages/scene.py".

If you want full control over the resulting texture's size, you could simply use a vanilla SpriteNode and draw the shape yourself.

import ui, scene

def texture_from_path(path, fill_color, width, height):
    with ui.ImageContext(width, height) as ctx:
        ui.set_color(fill_color)
        path.fill()
        img = ctx.get_image()
    return scene.Texture(img)

This is simplified a bit, but it shows the basic approach. You can look at ShapeNode's source code if you need its support of shadows, outlines and such.

zipit

Ah, okay, that does make sense now. I do have a follow up question though. Is it possible to access the points of a path after it has been created? edit: and with access I mean read them.

Cheers,
zipit

omz

@zipit No, that's not possible, you'd have to keep a reference to the points yourself.

zipit

Okay, thanks. I gave the thread a more meaningful title. I cannot provide a full example as my code relies on other stuff, but here is a snippet how I did solve the problem now (a bit clunky). Maybe it will help someone in the future. I wrote two comments so that the code does make some sense.

def draw_line(self):
        '''
        '''
        if self.line is not None:
            self.line.remove_from_parent()
        minx, miny = None, None
        path = ui.Path()
        path.line_width = 2
        # self is a pythonista node. self.anchors are some phythonista node 
        # objects that are children of self. We want to draw a line through all
        # these anchors within the coord system of self.
        for i, anchor in enumerate(self.anchors):
            p = anchor.position
            # get/update the lower left corner minimum
            minx, miny = (p.x if minx is None else min(minx, p.x),
                          p.y if miny is None else min(miny, p.y))
            if i == 0: path.move_to(p.x, -p.y)
            else: path.line_to(p.x, -p.y)
        # the offset(position) of our node has to be the lower left corner
        # point plus the center vector of our path
        self.line = ShapeNode(path,
                              stroke_color='green',
                              fill_color='transparent',
                              position = (minx + path.bounds.w * .5,
                                          miny + path.bounds.h * .5),
                              parent=self)
mikeno

Hi, looking back to this example, I tried to play a bit and discovered what seems to be an issue:


from scene import *
import time
class MyScene (Scene):
    def setup(self):
        sx, sy = self.size.w, self.size.h
        self.red = ShapeNode(ui.Path.rect(0,0,sx,sy),parent=self,fill_color='red',position=(0,0),anchor_point=(0,0))
        path = ui.Path()
        path.line_width = 3
        path.move_to(0,0)
        path.line_to(100,0)
        #path.line_to(0,100) # Uncommenting this line modifies the drawing of both previous lines
        self.cyan = ShapeNode(path,parent=self.red,stroke_color='white',position=(0,0),anchor_point=(0,0))
if __name__ == '__main__':
    run(MyScene())

Removing the comment modifies how both previous lines are drawn!
I tried to understand but it is still unclear for me

mikael

@mikeno, without running this, I think what you are seeing is that the commented line changes the bounding box of the whole path and thus the node, and as the node position defines the center position, the path seems to move to left.

mikeno

Hi Mikael, thx for replying but is there a way to avoid this?

mikael

@mikeno, do you need to use scene, or in other words, what are you trying to do?

mikeno

I just want to draw a filled polygone following numerous coordinates, I already managed to do it with ui and canvas but only scene gives me the possibility to use full screen

cvp

@mikeno try this, and to close, swipe down with two fingers

import ui
class my(ui.View):
    def draw(self):
        w,h = ui.get_screen_size()
        path = ui.Path()#.rect(0,0,w,h)
        path.line_width = 3
        ui.set_color('red')
        path.move_to(100,100)
        path.line_to(200,100)
        path.line_to(100,200)
        path.close()
        path.fill()
        #path.stroke()
v = my()
v.present('fullscreen',hide_title_bar=True)
mikeno

Yes thx it works but it doesn’t with several polygons

mikeno

... and then, I need to redraw in order to see the polygons in a different zoom factor. As I wrote, I already tried with ui but I stoped because I could not redraw, that’s why I tried with scene

cvp

@mikeno said:

it doesn’t with several polygons

import ui
class my(ui.View):
    def draw(self):
        w,h = ui.get_screen_size()
        path1 = ui.Path()
        path1.line_width = 3
        ui.set_color('red')
        path1.move_to(100,100)
        path1.line_to(200,100)
        path1.line_to(100,200)
        path1.close()
        path1.fill()
        path2 = ui.Path()#.rect(0,0,w,h)
        path2.line_width = 3
        ui.set_color('blue')
        path2.move_to(300,100)
        path2.line_to(400,100)
        path2.line_to(300,200)
        path2.close()
        path2.fill()
v = my()
v.present('fullscreen',hide_title_bar=True)

cvp

@mikeno said:

zoom see @mikael Gestures

mikeno

Thx to both of you, concerning the gestures, it seems to be a little bit complicated but I will try

mikeno

My last issue is that the drawing doesn’t refresh even when I call explicitly drawTest() as you can see in the example below:


import ui

class my(ui.View):

    def __init__(self, *args, **kwargs):
        self.factor = .5

    def draw(self):
        print('draw()')
        self.drawTest()

    def drawTest(self):
        print('drawTest()')
        print('%.1f' % (self.factor))
        w,h = ui.get_screen_size()
        path1 = ui.Path()
        path1.line_width = 3
        ui.set_color('red')
        path1.move_to(100*self.factor,100*self.factor)
        path1.line_to(200*self.factor,100*self.factor)
        path1.line_to(100*self.factor,200*self.factor)
        path1.close()
        path1.fill()
        path2 = ui.Path()#.rect(0,0,w,h)
        path2.line_width = 3
        ui.set_color('blue')
        path2.move_to(300*self.factor,100*self.factor)
        path2.line_to(400*self.factor,100*self.factor)
        path2.line_to(300*self.factor,200*self.factor)
        path2.close()
        path2.fill()

    def touch_began(self, touch):
        print('touch_began()')
        self.factor += .5
        self.drawTest()

    def will_close(self):
        print('will_close()')

v = my()
v.present('fullscreen',hide_title_bar=True)

It redraws only when I turn the iPad, could you please tell me how I can force the refresh with the right zoom factor?

cvp

@mikeno start from

import ui

class my(ui.View):

    def __init__(self, *args, **kwargs):
        self.w,self.h = ui.get_screen_size()
        iv = ui.ImageView(name='iv')
        iv.frame = (0,0,self.w,self.h)
        self.add_subview(iv)
        self.factor = .5
        self.update_interval = 1

    def update(self):
        with ui.ImageContext(self.w,self.h) as ctx:
          path1 = ui.Path()
          path1.line_width = 3
          ui.set_color('red')
          path1.move_to(100*self.factor,100*self.factor)
          path1.line_to(200*self.factor,100*self.factor)
          path1.line_to(100*self.factor,200*self.factor)
          path1.close()
          path1.fill()
          path2 = ui.Path()#.rect(0,0,w,h)
          path2.line_width = 3
          ui.set_color('blue')
          path2.move_to(300*self.factor,100*self.factor)
          path2.line_to(400*self.factor,100*self.factor)
          path2.line_to(300*self.factor,200*self.factor)
          path2.close()
          path2.fill()
          ui_image = ctx.get_image()
        self['iv'].image = ui_image

    def touch_began(self, touch):
        self.factor += .5

    def will_close(self):
        print('will_close()')

v = my()
v.present('fullscreen',hide_title_bar=True)

mikeno

Whao, I’m impressed, thank you!
But I don’t understand why it doesn’t work in my last example

cvp

@mikeno said:

But I don’t understand why it doesn’t work in my last example

Sincerely, me neither 😂

cvp

@mikeno just to show how it is easy to use @mikael 's gestures module, use two fingers to pinch

import ui
from gestures import *

class my(ui.View):

    def __init__(self, *args, **kwargs):
        self.w,self.h = ui.get_screen_size()
        iv = ui.ImageView(name='iv')
        iv.frame = (0,0,self.w,self.h)
        self.add_subview(iv)
        self.factor = .5
        #self.update_interval = 1
        self.pinch = pinch(iv,self.did_pinch)
        self.update()

    def update(self):
        with ui.ImageContext(self.w,self.h) as ctx:
          path1 = ui.Path()
          path1.line_width = 3
          ui.set_color('red')
          path1.move_to(100*self.factor,100*self.factor)
          path1.line_to(200*self.factor,100*self.factor)
          path1.line_to(100*self.factor,200*self.factor)
          path1.close()
          path1.fill()
          path2 = ui.Path()#.rect(0,0,w,h)
          path2.line_width = 3
          ui.set_color('blue')
          path2.move_to(300*self.factor,100*self.factor)
          path2.line_to(400*self.factor,100*self.factor)
          path2.line_to(300*self.factor,200*self.factor)
          path2.close()
          path2.fill()
          ui_image = ctx.get_image()
        self['iv'].image = ui_image

    def did_pinch(self, data):
        self.factor = data.scale
        self.update()

    def will_close(self):
        print('will_close()')

def pinch_handler(data):
        print(data)

v = my()
v.present('fullscreen',hide_title_bar=True)
mikeno

Thx cvp but I don’t know how to download a module into Pythonista

cvp

@mikeno use, and keep for the future, this script

import requests
import os
url = 'https://raw.githubusercontent.com/mikaelho/pythonista-gestures/master/gestures.py'
response = requests.get(url)
content = str(response.content.decode('utf-8'))
file_path = os.path.expanduser('~/Documents/site-packages/gestures.py')
with open(file_path,mode='wt',encoding='utf-8') as fil:
    fil.write(content)
mikeno

Super, thank you

ccc

@cvp said:

content = str(response.content.decode('utf-8'))

text = response.text

cvp

@ccc you're right here with a script but I'm sure that I had to use decode in the past for some web page containing special characters...But never mind, your code is better.

mikael

@mikeno, your script did not redraw on touch because your method did not get called with the drawing context that draw does. To make it work, trigger the draw with self.set_needs_display().

For installing stuff, recommend taking the time to install stash, for pip install support.

But keep @cvp’s install script around, most of the little things we build do not end up in pip/PyPI.

cvp

@mikael said:

To make it work, trigger the draw with self.set_needs_display().

Forgotten, halala this too old memory 😢

rajputaman04

I do have a follow up question though. Is it possible to access the points of a path after it has been created? edit: and with access I mean read them. mx player

mikael

@rajputaman04, unfortunately no.