Adding and binding a FloatCanvas.Object subclass 27 times causes an extremely long delay before raising exception
Operating system: Ubuntu 20.04 wxPython version & source: 4.1.1 Python version & source: CPython 3.9.12
Description of the problem:
When adding and binding more than 26 FloatCanvas.Object instances the application hangs for a significant amount of time before raising an exception. Digging into the cause, it seems to be due to this method, where the following colour indices are generated:
Add+Bind 1: (0, 0, 1)
Add+Bind 2: (0, 0, 2)
Add+Bind 3: (0, 1, 0)
Add+Bind 4: (0, 1, 1)
Add+Bind 5: (0, 1, 2)
Add+Bind 6: (0, 2, 0)
Add+Bind 7: (0, 2, 1)
Add+Bind 8: (0, 2, 2)
Add+Bind 9: (1, 0, 0)
...
Add+Bind 26: (2, 2, 2)
Add+Bind 27: Fails
Note that while the possible index for each colour is 0-255 it only uses 0-2, but the search space for each colour index remains in 0-255, so the 9th bind takes 256 x 256 calls of colormatch , while the 27th bind takes 256 x 256 x 256 calls of colormatch and causes a very significant delay (more than 20 minutes on my hardware) before raising an AttributeError because it fails to find a match.
Code Example (click to expand)
import wx
from wx.lib.floatcanvas import FloatCanvas as FC
class MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(
self,
parent=None,
id=wx.ID_ANY,
title="Foo",
pos=wx.DefaultPosition,
size=wx.Size(400, 400),
style=wx.CAPTION|wx.CLOSE_BOX|wx.RESIZE_BORDER
)
canvas = FC.FloatCanvas(self)
canvas.SetSize((400, 400))
canvas.Draw(Force=True)
for idx in range(30):
print(f"Adding and binding #{idx}...")
poly = FC.Polygon([[0, 10], [10, 10], [10, 0], [0, 0]])
canvas.AddObject(poly)
poly.Bind(FC.EVT_FC_LEFT_DOWN, self.foo)
def foo(self, event):
return
if __name__ == "__main__":
app = wx.App()
frame = MainFrame()
frame.Show()
app.MainLoop()
I overrode the default FC.Object.Bind method to use a simple generator that iterates consecutively through the colours instead:
def _cycleidxs(indexcount, maxvalue, step):
if indexcount == 0:
yield ()
else:
for r in range(256):
for g in range(256):
for b in range(256):
yield (r, g, b)
While this seems to work for me, without understanding why the original code is so complex I'm not sure whether or not its a suitable replacement.
Thanks for bringing this up -- I wrote that code a LONG time ago, and when generators were brand new (that's how long ago it was) -- I'll take a look.
-CHB
Looking now and refreshing my memory -- all that complexity is because wx wraps the underlying system drawing code, and when I originally wrote that, they did not all use 24 bit color. So it had to actually check if the coor being generated was unique on the platform at runtime. And then there were oher platform dependent issues, like wxMac not allowing you to access a bitmap when selected into a MemoryDC.
I'm guessing all platforms of interest now are 24 bit, so your simple code should work fine.
But I think you've found a plain old bug in the code -- it was supposed to utilize the full 256 per color colorspace -- not sure why it's looping only through 0-2 .... on a 24 or 32 bit system, it should do exactly what your code does.
And I have no idea why it's recursive -- that seems gratuitously complicated.
This was also written before I learned about unit testing :-(
I'm a bit wary of removing colormatch altogether -- it may still be needed. But I think we can fix this bug in any case.
And consider removing colormatch next.
Well, debugging is failing me. On my machine (Mac), its working fine:
def test__cycleidxs_multiple():
"""
make sure it gets the first few anyway ...
"""
CG = _cycleidxs(indexcount=3, maxvalue=256, step=1)
print("first three")
for i in range(3):
print(next(CG))
for i in range(251):
next(CG)
print("after 255")
for i in range(3):
print(next(CG))
assert False
results in:
first three
(0, 0, 0)
(0, 0, 1)
(0, 0, 2)
after 255
(0, 0, 254)
(0, 0, 255)
(0, 1, 0)
so it's behaving as expected.
So this seems to be a platform issue.
The only thing I can think of is that on your platform (and wx version, etc) you're getting 32 bit color, so that:
outcolor == color
Is failing. So can you try replacing that with:
outcolor[:indexcount] == color[:indexcount]
However, this doesn't make sense either -- as comparing a 3-tuple to 4-tuple will always fail.
So what is:
dc.GetPixel(0, 0)
actually returning ???
apparently a three-tuple of 0, 1, or 2s -- which makes no sense to me.
Can you try adding some prints in there (or running in a debugger) and see what's going on.
Seemingly complicated, all colormatch is doing is drawing a pixel with the color, and making sure that you get the same color back when you select the pixel -- which is what the hit-test code does, so it should work!
NOTE: I'm working on this in the fork of the project, if you want to see my test code, and/or submit a PR:
https://github.com/ChrisBarker-NOAA/Phoenix
-CHB
Sorry for the extremely long delay, lost track of this somehow.
I'm running Ubuntu 64-bit 20.04 and print(dc.GetPixel(0, 0), color) gives (0, 109, 190, 255), (0, 145, 176), for example.
Changing to outcolor[:indexcount] == color[:indexcount] doesn't seem to help, either. In my example I'm getting the following when adding print(outcolor[:indexcount], color[:indexcount]) prior to the comparison in L80:
Adding and binding #0...
(0, 0, 0) (0, 0, 0)
(0, 0, 1) (0, 0, 1)
Adding and binding #1...
(0, 0, 2) (0, 0, 2)
Adding and binding #2...
(0, 0, 2) (0, 0, 3)
(0, 0, 3) (0, 0, 4)
(0, 0, 4) (0, 0, 5)
(0, 0, 5) (0, 0, 6)
(0, 0, 5) (0, 0, 7) # note duplicate
(0, 0, 6) (0, 0, 8)
(0, 0, 7) (0, 0, 9)
(0, 0, 8) (0, 0, 10)
(0, 0, 8) (0, 0, 11) # duplicate
(0, 0, 9) (0, 0, 12)
...
(0, 0, 192) (0, 0, 255)
(0, 1, 0) (0, 1, 0)
Adding and binding #3...
(0, 1, 1) (0, 1, 1)
Adding and binding #4...
(0, 1, 2) (0, 1, 2)
Adding and binding #5...
(0, 1, 2) (0, 1, 3)
(0, 1, 3) (0, 1, 4)
...
It's interesting that the actual colour range of the DC is 0-192 rather than 0-255, but I have no idea why it'd be exactly 3/4 of the normal range.
Ah! The bit depth for the display is 24, giving 6-bits per channel.