How do screen space transforms work with gui pivots? (SOLVED)

I’m trying to implement something that should be simple, and since it isn’t working, something in my mental model of how defold works is wrong. I’ve read through the API docs for these functions and none of them detail how pivots are expected to interact with gui nodes (if at all):

local touch_node = gui.get_node("touch_stick")
local relative = gui.screen_to_local(touch_node, vmath.vector3(action.x, action.y, 0))

The pivot for touch_stick is its center, so I would expect relative.x to be positive when touching the right arrow and negative when touching the left arrow. I would expect relative.y to be positive when touching up and negative when touching down:

But what I actually see is different values of negative in all cases.

I can hack around this by doing a series of math to transform the action coordinates by hand, but I expect that the proper solution is actually simple and I’m just misunderstanding something. Could anyone shed some light on this for me?

For future me / future folks who are similarly confused, here’s the solution:

local origin = gui.get_screen_position(touch_node)
local relative = vmath.vector3(action.screen_x, action.screen_y, 0) - origin

It looks like “screen” space is something like “real physical pixel space / stretched space,” rather than the logical pixels of display.width and display.height. So I thought I was doing something like a world_to_local transform, but that doesn’t exist on gui objects which explains why I couldn’t find it in the docs.

Notably, this doesn’t work:

local origin = gui.get_position(touch_node)
local relative = vmath.vector3(action.x, action.y, 0) - origin

I think that’s because get_position is relative to a parent transform, and there doesn’t seem to be a way to convert between coordinate spaces of nested GUI nodes (other than manually building up your own transforms). If there is, I would be interested to learn about it!