Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
GKPath question
-
I have a float2 struct that works perfectly:
class float2(Structure): _fields_ = [('x',c_float),('y',c_float)]```
How can I implement this method of GKPath:
- (instancetype)initWithPoints:(vector_float2 *)points count:(size_t)count radius:(float)radius cyclical:(BOOL)cyclical
Specifically, the problem is when I pass array of float2 to points parameters, app crashes.
Reference:
GKPath initWithPoints:count:radius:cyclical: -
Can you post your code that is creating the array and calling the method?
-
This worked for me
from objc_util import * from ctypes import * from scene import * load_framework('Foundation') load_framework('GameplayKit') GKPath = ObjCClass('GKPath') class float2(Structure): _fields_ = [('x',c_float),('y',c_float)] def __repr__(self): return 'float2({},{})'.format(self.x,self.y) pts=(float2*5)() #array of 5 points pts[0]=float2(10,11) pts[1]=float2(20,21) pts[2]=float2(30,31) pts[3]=float2(40,41) pts[4]=float2(50,51) #for some reason, count needs to be count+1? path=GKPath.pathWithPoints_count_radius_cyclical_(pts,len(pts)+1 , 0.5,0,argtypes=[c_void_p, c_size_t, c_float, c_bool], restype=c_void_p) #on my 32bit ipad i have had trouble with the right way to read back a float2, the following gets x #ptx=path.pointAtIndex_(1,argtypes=[c_ulong],restype=c_float) #this just crashes: #ptx=path.pointAtIndex_(1,argtypes=[c_ulong],restype=float2) #maybe because objc_util is not using objc_msgSend_stret for custom restype? so this might work on 64bit #this works in 32 bit, but probably does not work on 64bit machines for i in range(5): #on my 32bit ipad i have had trouble with the right way to read back a float2, the following gets x #ptx=path.pointAtIndex_(i,argtypes=[c_ulong],restype=c_float) #this just crashes: #ptx=path.pointAtIndex_(i,argtypes=[c_ulong],restype=float2) #maybe because objc_util is not using objc_msgSend_stret for custom restype? so might work on 64bit #this works in 32 bit, but probably does not work on 64bit machines p=c_uint64(path.pointAtIndex_(i,argtypes=[c_ulong],restype=c_uint64)) pt=float2.from_address(addressof(p)) print(pt)
Objc_util seems to have trouble with both of these methods, figuring out argtypes.
-
@JonB Try adding an extra underscore at the end of the methods that take/return
vector_float2
s, and then use thefloat2
struct. Last time that worked I think. -
nope, selector not found.
I have not looked at the code, but i think this is just a problem with objc_util. There is special code for handling structure returns on 32 bit, my guess is that it uses the inferred restype, not the user passed one to make that decision.
The encodings seem to be screwed up on some of these
>>> path.pointAtIndex_.encoding b'12@0:4I8'
seems to be missing the return type character!
A separete issue:
GKPath.pathWithPoints_count_radius_cyclical_.encoding b'@24@0:4^8L12f16c20'
objc_util barfs on the ^8 when parsing argtypes(ignores it completely)
-
Yeah, the part of Clang that generates encodings simply does not do vector types. When it sees one, it encodes it as an empty string, which makes it basically impossible to parse the encoding. I don't think this is something
objc_util
can support (and since method calls involving vectors require some manual work anyway, I don't think that's really an issue).Now that I think about it, it makes sense that the
pathWithPoints...
method doesn't have an "underscore" version. The vectors are only passed indirectly through a pointer, and not directly as a normal parameter. For direct vector parameters, the compiler seems to generate two methods - the regular one, which probably uses fancy vector registers, and the "underscored" one, which uses regular structs. For indirect parameters this isn't necessary - the vectors are passed by pointer, so they have to be in memory, and it's not possible to use vector registers for passing anyway.Can you check if there is a
pointWithIndex__
method with two underscores, and try to use that with afloat2
struct restype? -
>>> split_encoding('@24@0:4^8L12f16c20') ['@', '@', ':', '^L', 'f', 'c']
Knowing that clang doesn't properly represent vectors in the encoding, it seems like we could split on numbers appearing where they are not supposed to ... so ^8L should not be interpreted as ^L (pointer to unisgned long), but a pointer to something we are not sure of (c_void_p) followed by a long. vector restype is obviously a little trickier, especially since the size info never seems correct.
I am on ios9, but there are no double underscore pointWithIndex or pointAtIndex. I think on 10 maybe it was replaced with float2AtIndex.
-
@JonB and @dgelessus, do I read the above correctly to mean that: there is no way to call ObjC methods that expect a vector of float2s from Pythonista?
-
No, you just have to specify the argtypes and restype manually.
We probably ought to go back through old forum posts to find the various cases when the encoding parser fails, and monkey patch that function.
-
@JonB, after asking the question above, I have been trying to go the manual encoding route, with no real success.
What I have is an identity SKWarpGeometryGrid created with
gridWithColumns_rows_
. It reports the correct number of rows, cols and vertices, and self-identifies as an identity grid where the source and destination grids point to the same grid.Now I try to change the destination grid with
gridByReplacingDestPositions_
. This requiresvector_float2
and returns a new grid with the destination positions replaced.These are the destination points I would like to try:
dest = [ (0.0, -0.5), (0.5, 0.0), (1.0, 0.0), (0.0, 0.5), (0.5, 0.5), (1.0, 0.5), (0.0, 1.0), (0.5, 1.0), (1.0, 1.0), ]
(Only the second float of the first tuple is changed from the identity grid.)
I have this manual definition:
f = self.geometry.gridByReplacingDestPositions_ f.argtypes = [c_void_p] f.restype = c_void_p f.encoding = b'@@:@'
And these for creating the destination argument from the array above:
class float2(Structure): _fields_ = [('x',c_float),('y',c_float)] @classmethod def _get_float2(cls, positions): floats = [] for pos in positions: f = float2() f.x, f.y = pos floats.append(f) floats_array = (float2 * len(floats))(*floats) return floats_array
The method gets called and returns an updated SKWarpGeometryGrid, which unfortunately is still the same identity warp. This leads me to believe that my float2 is probably wrong and gets implicitly rejected without error.
I have also tried
gridWithColumns_rows_sourcePositions_destPositions_
, with same identity grid result.Any next steps to try would be greatly appreciated.
-
How many points does the source grid have? If you are just replacing the dest, I think you have to make sure the number is the same.
I'll play around with it -- do you have code that initializes the grid in the first place?
-
Okay, playing with this some more, I can’t event get valid points out of the destPositionsAt, etc. doing the same in playgrounds does provide valid values. The issue seems to be that libffi doesn’t seem to support SIMD, or at least it didn’t for the version used by Pythonista.
There might be some workarounds using MTLKit, but not sure
-
@JonB, I get expected values out with the following:
addr = ctypes.addressof(warp_geometry.destPositions().contents) for _ in range(9): p = float2.from_address(addr) print(p.x, p.y) addr += ctypes.sizeof(float2)
... but still cannot get the destGeometry set, even if I modify this same memory area and set it back.
-
-
@cvp, thanks, no I did not. But now that I did, seems to match the docs, except for the disheartening ”Warning: Unrecognized filer type: '1' using 'void*'” everywhere that the grids are being referred to.
-
Ahh, the extra pointer was throwing me off.
This is a pointer to a float2 array.Ok, so this does seem to work, both modifying and creating from scratch.
Note the somewhat simpler cast of the c_void_p directly to the POINTER type we want (you could also have set restype to POINTER(float2*n)), allowing list-like access to the contents rather than mucking about with addresses. Likewise, passing in can just use byref.I think there is a caveat that you probably have to be careful if you create from scratch, because float2 might have some memory alignment requirements that this doesn’t enforce.
from objc_util import * from ctypes import * NSBundle.bundleWithPath_('/System/Library/Frameworks/SpriteKit.framework').load() NSBundle.bundleWithPath_('/System/Library/Frameworks/Metal.framework').load() SKWarpGeometryGrid=ObjCClass('SKWarpGeometryGrid') class float2(Structure): _fields_ = [('x',c_float),('y',c_float)] def __repr__(self): 'convienence repr' return ' float2({},{})'.format(self.x,self.y) @classmethod def _get_float2(cls, positions): floats = [] for pos in positions: f = float2() f.x, f.y = pos floats.append(f) floats_array = (float2 * len(floats))(*floats) return floats_array def dump_float2array(ptr,n): data = cast(ptr, POINTER(float2*n)).contents for d in data: print(d) return data g=SKWarpGeometryGrid.gridWithColumns_rows_(3,3) '''note, destPositins returns a pointer to an arrayof float2. we can cast then access contents, which gves us easy array like access to data''' print('initial dest') dst0=dump_float2array(g.destPositions(), 9) '''it seems modifying the returned value directly modifies the original object. so, make a copy first. could have also done this in the dump fcn so we don't forget''' new_dst=(float2*9).from_buffer_copy(dst0) ''' just change one element to check that this works''' new_dst[2].x=.999 new_dst[2].y=.998 '''it appears that changing the returned memory changes the actual grid, maybe not what is intended. might be better to do some sort of memcopy before modifying''' print('initial dest after modifying memory') dump_float2array(g.destPositions(), 9) g2=g.gridByReplacingDestPositions_(byref(new_dst), restype=c_void_p, argtypes=[c_void_p]) print('new grid dest') dump_float2array(g2.destPositions(), 9) '''create from scratch''' src=float2._get_float2([[0,0],[0,.5],[0.5,0],[.5,.5]]) dst=float2._get_float2([[0,0],[0,.75],[0.75,0],[.75,.75]]) g3=SKWarpGeometryGrid.gridWithColumns_rows_sourcePositions_destPositions_(2,2,byref(src),byref(dst), restype=c_void_p, argtypes=[c_int,c_int, c_void_p,c_void_p]) print('from scratch') dump_float2array(g3.destPositions(), 4)
-
By the way, ctypes is smart enough to figure out
dest = [ (0.0, -0.5), (0.5, 0.0), (1.0, 0.0), (0.0, 0.5), (0.5, 0.5), (1.0, 0.5), (0.0, 1.0), (0.5, 1.0), (1.0, 1.0), ] new_dst=(float2*9)(*dest)
So, the _get_float2 method is not needed, or at least doesn’t need the loop.
This also works for nested struts,like
(CGRect*2)(((0,0),(50,75)),((50,0),(50,75)))
Where essentially the tuple is interpreted outside-> in, each nested tuple becomes the constructor arguments for the next lower level field, etc.
-
@JonB, color me impressed, once again.
Here is all that is needed, then:
warp = SKWarpGeometryGrid.gridWithColumns_rows_(2, 2) dest = [ (0.0, -0.5), (0.5, 0.0), (1.0, 0.0), (0.0, 0.5), (0.5, 0.5), (1.0, 0.5), (0.0, 1.0), (0.5, 1.0), (1.0, 1.0), ] new_dest = (float2*9)(*dest) g2 = warp.gridByReplacingDestPositions_( byref(new_dest), restype=c_void_p, argtypes=[c_void_p])
(Note that for some Apple-ish reason, the col & row numbers are one less than what you would think given the number of vertices.)
-