diff --git a/canvas/Canvas.py b/canvas/Canvas.py index bcc17a27..17a39c2b 100644 --- a/canvas/Canvas.py +++ b/canvas/Canvas.py @@ -92,6 +92,15 @@ class Canvas(QtGui.QWidget): if name is not None: self.registeredName = CanvasManager.instance().registerCanvas(self, name) self.ui.redirectCombo.setHostName(self.registeredName) + + self.menu = QtGui.QMenu() + #self.menu.setTitle("Image") + remAct = QtGui.QAction("Remove item", self.menu) + remAct.triggered.connect(self.removeClicked) + self.menu.addAction(remAct) + self.menu.remAct = remAct + self.ui.itemList.contextMenuEvent = self.itemListContextMenuEvent + def storeSvg(self): self.ui.view.writeSvg() @@ -513,10 +522,20 @@ class Canvas(QtGui.QWidget): listItem.setCheckState(0, QtCore.Qt.Unchecked) def removeItem(self, item): + if isinstance(item, QtGui.QTreeWidgetItem): + item = item.canvasItem() + + if isinstance(item, CanvasItem): item.setCanvas(None) - self.itemList.removeTopLevelItem(item.listItem) + listItem = item.listItem + listItem.canvasItem = None + item.listItem = None + self.itemList.removeTopLevelItem(listItem) self.items.remove(item) + ctrl = item.ctrlWidget() + ctrl.hide() + self.ui.ctrlLayout.removeWidget(ctrl) else: if hasattr(item, '_canvasItem'): self.removeItem(item._canvasItem) @@ -555,7 +574,15 @@ class Canvas(QtGui.QWidget): #self.emit(QtCore.SIGNAL('itemTransformChangeFinished'), self, item) self.sigItemTransformChangeFinished.emit(self, item) - + def itemListContextMenuEvent(self, ev): + self.menuItem = self.itemList.itemAt(ev.pos()) + self.menu.popup(ev.globalPos()) + + def removeClicked(self): + self.removeItem(self.menuItem) + self.menuItem = None + import gc + gc.collect() class SelectBox(ROI): def __init__(self, scalable=False): diff --git a/console/Console.py b/console/Console.py index 4d5b691a..6fbe44a7 100644 --- a/console/Console.py +++ b/console/Console.py @@ -208,8 +208,11 @@ class ConsoleWidget(QtGui.QWidget): #self.stdout.write("
") self.output.insertPlainText(strn) #self.stdout.write(strn) - + def displayException(self): + """ + Display the current exception and stack. + """ tb = traceback.format_exc() lines = [] indent = 4 diff --git a/documentation/source/functions.rst b/documentation/source/functions.rst index ad43ca06..65f2c202 100644 --- a/documentation/source/functions.rst +++ b/documentation/source/functions.rst @@ -8,7 +8,7 @@ Simple Data Display Functions .. autofunction:: pyqtgraph.image - +.. autofunction:: pyqtgraph.dbg Color, Pen, and Brush Functions ------------------------------- @@ -34,6 +34,8 @@ Qt uses the classes QColor, QPen, and QBrush to determine how to draw lines and .. autofunction:: pyqtgraph.colorStr +.. autofunction:: pyqtgraph.glColor + Data Slicing ------------ @@ -41,6 +43,18 @@ Data Slicing .. autofunction:: pyqtgraph.affineSlice +Coordinate Transformation +------------------------- + +.. autofunction:: pyqtgraph.transformToArray + +.. autofunction:: pyqtgraph.transformCoordinates + +.. autofunction:: pyqtgraph.solve3DTransform + +.. autofunction:: pyqtgraph.solveBilinearTransform + + SI Unit Conversion Functions ---------------------------- @@ -59,6 +73,12 @@ Image Preparation Functions .. autofunction:: pyqtgraph.makeQImage +.. autofunction:: pyqtgraph.applyLookupTable + +.. autofunction:: pyqtgraph.rescaleData + +.. autofunction:: pyqtgraph.imageToArray + Mesh Generation Functions ------------------------- @@ -68,4 +88,13 @@ Mesh Generation Functions .. autofunction:: pyqtgraph.isosurface +Miscellaneous Functions +----------------------- + +.. autofunction:: pyqtgraph.pseudoScatter + +.. autofunction:: pyqtgraph.systemInfo + + + diff --git a/examples/GLIsosurface.py b/examples/GLIsosurface.py index 32478605..4324d9cf 100644 --- a/examples/GLIsosurface.py +++ b/examples/GLIsosurface.py @@ -45,9 +45,9 @@ data = np.abs(np.fromfunction(psi, (50,50,100))) print("Generating isosurface..") -verts = pg.isosurface(data, data.max()/4.) +verts, faces = pg.isosurface(data, data.max()/4.) -md = gl.MeshData(vertexes=verts) +md = gl.MeshData(vertexes=verts, faces=faces) colors = np.ones((md.faceCount(), 4), dtype=float) colors[:,3] = 0.2 diff --git a/examples/__main__.py b/examples/__main__.py index 1d69731c..6f4bf138 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -175,8 +175,11 @@ def testFile(name, f, exe, lib): try: %s import %s + import sys print("test complete") + sys.stdout.flush() import pyqtgraph as pg + import time while True: ## run a little event loop pg.QtGui.QApplication.processEvents() time.sleep(0.01) @@ -186,7 +189,7 @@ except: """ % ("import %s" % lib if lib != '' else "", os.path.splitext(os.path.split(fn)[1])[0]) #print code - process = subprocess.Popen(['%s -i' % (exe)], shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + process = subprocess.Popen(['exec %s -i' % (exe)], shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) process.stdin.write(code.encode('UTF-8')) #process.stdin.close() output = '' @@ -202,10 +205,11 @@ except: fail = True break time.sleep(1) - process.terminate() + process.kill() + #process.wait() res = process.communicate() - #if 'exception' in res[1].lower() or 'error' in res[1].lower(): - if fail: + + if fail or 'exception' in res[1].decode().lower() or 'error' in res[1].decode().lower(): print('.' * (50-len(name)) + 'FAILED') print(res[0].decode()) print(res[1].decode()) diff --git a/examples/isocurve.py b/examples/isocurve.py index eea525b9..14a3e56a 100644 --- a/examples/isocurve.py +++ b/examples/isocurve.py @@ -19,6 +19,7 @@ frames = 200 data = np.random.normal(size=(frames,30,30), loc=0, scale=100) data = np.concatenate([data, data], axis=0) data = ndi.gaussian_filter(data, (10, 10, 10))[frames/2:frames + frames/2] +data[:, 15:16, 15:17] += 1 win = pg.GraphicsWindow() vb = win.addViewBox() diff --git a/exporters/Exporter.py b/exporters/Exporter.py index c3917fca..2540b327 100644 --- a/exporters/Exporter.py +++ b/exporters/Exporter.py @@ -73,7 +73,8 @@ class Exporter(object): def getSourceRect(self): if isinstance(self.item, pg.GraphicsScene): - return self.item.getViewWidget().viewRect() + w = self.item.getViewWidget() + return w.viewportTransform().inverted()[0].mapRect(w.rect()) else: return self.item.sceneBoundingRect() diff --git a/exporters/SVGExporter.py b/exporters/SVGExporter.py index 0538aae7..2d040282 100644 --- a/exporters/SVGExporter.py +++ b/exporters/SVGExporter.py @@ -36,11 +36,14 @@ class SVGExporter(Exporter): return self.svg = QtSvg.QSvgGenerator() self.svg.setFileName(fileName) - self.svg.setSize(QtCore.QSize(100,100)) - #self.svg.setResolution(600) + dpi = QtGui.QDesktopWidget().physicalDpiX() + ## not really sure why this works, but it seems to be important: + self.svg.setSize(QtCore.QSize(self.params['width']*dpi/90., self.params['height']*dpi/90.)) + self.svg.setResolution(dpi) #self.svg.setViewBox() targetRect = QtCore.QRect(0, 0, self.params['width'], self.params['height']) sourceRect = self.getSourceRect() + painter = QtGui.QPainter(self.svg) try: self.setExportMode(True) diff --git a/functions.py b/functions.py index b83a2186..3f5a01d9 100644 --- a/functions.py +++ b/functions.py @@ -491,9 +491,6 @@ def transformToArray(tr): ## map coordinates through transform mapped = np.dot(m, coords) """ - if isinstance(tr, np.ndarray): - return tr - #return np.array([[tr.m11(), tr.m12(), tr.m13()],[tr.m21(), tr.m22(), tr.m23()],[tr.m31(), tr.m32(), tr.m33()]]) ## The order of elements given by the method names m11..m33 is misleading-- ## It is most common for x,y translation to occupy the positions 1,3 and 2,3 in @@ -506,18 +503,28 @@ def transformToArray(tr): else: raise Exception("Transform argument must be either QTransform or QMatrix4x4.") -def transformCoordinates(tr, coords): +def transformCoordinates(tr, coords, transpose=False): """ Map a set of 2D or 3D coordinates through a QTransform or QMatrix4x4. The shape of coords must be (2,...) or (3,...) The mapping will _ignore_ any perspective transformations. + + For coordinate arrays with ndim=2, this is basically equivalent to matrix multiplication. + Most arrays, however, prefer to put the coordinate axis at the end (eg. shape=(...,3)). To + allow this, use transpose=True. + """ + + if transpose: + ## move last axis to beginning. This transposition will be reversed before returning the mapped coordinates. + coords = coords.transpose((coords.ndim-1,) + tuple(range(0,coords.ndim-1))) + nd = coords.shape[0] - if not isinstance(tr, np.ndarray): + if isinstance(tr, np.ndarray): + m = tr + else: m = transformToArray(tr) m = m[:m.shape[0]-1] # remove perspective - else: - m = tr ## If coords are 3D and tr is 2D, assume no change for Z axis if m.shape == (2,3) and nd == 3: @@ -545,9 +552,15 @@ def transformCoordinates(tr, coords): ## map coordinates and return mapped = (m*coords).sum(axis=1) ## apply scale/rotate mapped += translate + + if transpose: + ## move first axis to end. + mapped = mapped.transpose(tuple(range(1,mapped.ndim)) + (0,)) return mapped + + def solve3DTransform(points1, points2): """ Find a 3D transformation matrix that maps points1 onto points2 @@ -782,7 +795,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): if levels.shape != (data.shape[-1], 2): raise Exception('levels must have shape (data.shape[-1], 2)') else: - print(levels) + print levels raise Exception("levels argument must be 1D or 2D.") #levels = np.array(levels) #if levels.shape == (2,): @@ -1066,16 +1079,43 @@ def imageToArray(img, copy=False, transpose=True): #return facets -def isocurve(data, level): +def isocurve(data, level, connected=False, extendToEdge=False, path=False): """ Generate isocurve from 2D data using marching squares algorithm. - *data* 2D numpy array of scalar values - *level* The level at which to generate an isosurface + ============= ========================================================= + Arguments + data 2D numpy array of scalar values + level The level at which to generate an isosurface + connected If False, return a single long list of point pairs + If True, return multiple long lists of connected point + locations. (This is slower but better for drawing + continuous lines) + extendToEdge If True, extend the curves to reach the exact edges of + the data. + path if True, return a QPainterPath rather than a list of + vertex coordinates. This forces connected=True. + ============= ========================================================= This function is SLOW; plenty of room for optimization here. """ + if path is True: + connected = True + + if extendToEdge: + d2 = np.empty((data.shape[0]+2, data.shape[1]+2), dtype=data.dtype) + d2[1:-1, 1:-1] = data + d2[0, 1:-1] = data[0] + d2[-1, 1:-1] = data[-1] + d2[1:-1, 0] = data[:, 0] + d2[1:-1, -1] = data[:, -1] + d2[0,0] = d2[0,1] + d2[0,-1] = d2[1,-1] + d2[-1,0] = d2[-1,1] + d2[-1,-1] = d2[-1,-2] + data = d2 + sideTable = [ [], [0,1], @@ -1096,7 +1136,7 @@ def isocurve(data, level): ] edgeKey=[ - [(0,1),(0,0)], + [(0,1), (0,0)], [(0,0), (1,0)], [(1,0), (1,1)], [(1,1), (0,1)] @@ -1140,347 +1180,482 @@ def isocurve(data, level): p1[0]*fi + p2[0]*f + i + 0.5, p1[1]*fi + p2[1]*f + j + 0.5 ) - pts.append(p) + if extendToEdge: + ## check bounds + p = ( + min(data.shape[0]-2, max(0, p[0]-1)), + min(data.shape[1]-2, max(0, p[1]-1)), + ) + if connected: + gridKey = i + (1 if edges[m]==2 else 0), j + (1 if edges[m]==3 else 0), edges[m]%2 + pts.append((p, gridKey)) ## give the actual position and a key identifying the grid location (for connecting segments) + else: + pts.append(p) + lines.append(pts) - return lines ## a list of pairs of points + if not connected: + return lines + + ## turn disjoint list of segments into continuous lines + + #lines = [[2,5], [5,4], [3,4], [1,3], [6,7], [7,8], [8,6], [11,12], [12,15], [11,13], [13,14]] + #lines = [[(float(a), a), (float(b), b)] for a,b in lines] + points = {} ## maps each point to its connections + for a,b in lines: + if a[1] not in points: + points[a[1]] = [] + points[a[1]].append([a,b]) + if b[1] not in points: + points[b[1]] = [] + points[b[1]].append([b,a]) + + ## rearrange into chains + for k in points.keys(): + try: + chains = points[k] + except KeyError: ## already used this point elsewhere + continue + #print "===========", k + for chain in chains: + #print " chain:", chain + x = None + while True: + if x == chain[-1][1]: + break ## nothing left to do on this chain + + x = chain[-1][1] + if x == k: + break ## chain has looped; we're done and can ignore the opposite chain + y = chain[-2][1] + connects = points[x] + for conn in connects[:]: + if conn[1][1] != y: + #print " ext:", conn + chain.extend(conn[1:]) + #print " del:", x + del points[x] + if chain[0][1] == chain[-1][1]: # looped chain; no need to continue the other direction + chains.pop() + break + + + ## extract point locations + lines = [] + for chain in points.values(): + if len(chain) == 2: + chain = chain[1][1:][::-1] + chain[0] # join together ends of chain + else: + chain = chain[0] + lines.append([p[0] for p in chain]) + + if not path: + return lines ## a list of pairs of points + + path = QtGui.QPainterPath() + for line in lines: + path.moveTo(*line[0]) + for p in line[1:]: + path.lineTo(*p) + + return path +def traceImage(image, values, smooth=0.5): + """ + Convert an image to a set of QPainterPath curves. + One curve will be generated for each item in *values*; each curve outlines the area + of the image that is closer to its value than to any others. + + If image is RGB or RGBA, then the shape of values should be (nvals, 3/4) + The parameter *smooth* is expressed in pixels. + """ + import scipy.ndimage as ndi + if values.ndim == 2: + values = values.T + values = values[np.newaxis, np.newaxis, ...].astype(float) + image = image[..., np.newaxis].astype(float) + diff = np.abs(image-values) + if values.ndim == 4: + diff = diff.sum(axis=2) + + labels = np.argmin(diff, axis=2) + + paths = [] + for i in range(diff.shape[-1]): + d = (labels==i).astype(float) + d = ndi.gaussian_filter(d, (smooth, smooth)) + lines = isocurve(d, 0.5, connected=True, extendToEdge=True) + path = QtGui.QPainterPath() + for line in lines: + path.moveTo(*line[0]) + for p in line[1:]: + path.lineTo(*p) + + paths.append(path) + return paths + + + +IsosurfaceDataCache = None def isosurface(data, level): """ Generate isosurface from volumetric data using marching cubes algorithm. See Paul Bourke, "Polygonising a Scalar Field" - (http://local.wasp.uwa.edu.au/~pbourke/geometry/polygonise/) + (http://paulbourke.net/geometry/polygonise/) *data* 3D numpy array of scalar values *level* The level at which to generate an isosurface - Returns an array of vertex coordinates (N, 3, 3); - - This function is SLOW; plenty of room for optimization here. + Returns an array of vertex coordinates (Nv, 3) and an array of + per-face vertex indexes (Nf, 3) """ + ## For improvement, see: + ## + ## Efficient implementation of Marching Cubes' cases with topological guarantees. + ## Thomas Lewiner, Helio Lopes, Antonio Wilson Vieira and Geovan Tavares. + ## Journal of Graphics Tools 8(2): pp. 1-15 (december 2003) + + ## Precompute lookup tables on the first run + global IsosurfaceDataCache + if IsosurfaceDataCache is None: + ## map from grid cell index to edge index. + ## grid cell index tells us which corners are below the isosurface, + ## edge index tells us which edges are cut by the isosurface. + ## (Data stolen from Bourk; see above.) + edgeTable = np.array([ + 0x0 , 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c, + 0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00, + 0x190, 0x99 , 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c, + 0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90, + 0x230, 0x339, 0x33 , 0x13a, 0x636, 0x73f, 0x435, 0x53c, + 0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30, + 0x3a0, 0x2a9, 0x1a3, 0xaa , 0x7a6, 0x6af, 0x5a5, 0x4ac, + 0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0, + 0x460, 0x569, 0x663, 0x76a, 0x66 , 0x16f, 0x265, 0x36c, + 0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60, + 0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0xff , 0x3f5, 0x2fc, + 0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0, + 0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x55 , 0x15c, + 0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950, + 0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0xcc , + 0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0, + 0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc, + 0xcc , 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0, + 0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c, + 0x15c, 0x55 , 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650, + 0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc, + 0x2fc, 0x3f5, 0xff , 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0, + 0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c, + 0x36c, 0x265, 0x16f, 0x66 , 0x76a, 0x663, 0x569, 0x460, + 0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac, + 0x4ac, 0x5a5, 0x6af, 0x7a6, 0xaa , 0x1a3, 0x2a9, 0x3a0, + 0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c, + 0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x33 , 0x339, 0x230, + 0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c, + 0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190, + 0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c, + 0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0 ], dtype=np.uint16) + + ## Table of triangles to use for filling each grid cell. + ## Each set of three integers tells us which three edges to + ## draw a triangle between. + ## (Data stolen from Bourk; see above.) + triTable = [ + [], + [0, 8, 3], + [0, 1, 9], + [1, 8, 3, 9, 8, 1], + [1, 2, 10], + [0, 8, 3, 1, 2, 10], + [9, 2, 10, 0, 2, 9], + [2, 8, 3, 2, 10, 8, 10, 9, 8], + [3, 11, 2], + [0, 11, 2, 8, 11, 0], + [1, 9, 0, 2, 3, 11], + [1, 11, 2, 1, 9, 11, 9, 8, 11], + [3, 10, 1, 11, 10, 3], + [0, 10, 1, 0, 8, 10, 8, 11, 10], + [3, 9, 0, 3, 11, 9, 11, 10, 9], + [9, 8, 10, 10, 8, 11], + [4, 7, 8], + [4, 3, 0, 7, 3, 4], + [0, 1, 9, 8, 4, 7], + [4, 1, 9, 4, 7, 1, 7, 3, 1], + [1, 2, 10, 8, 4, 7], + [3, 4, 7, 3, 0, 4, 1, 2, 10], + [9, 2, 10, 9, 0, 2, 8, 4, 7], + [2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4], + [8, 4, 7, 3, 11, 2], + [11, 4, 7, 11, 2, 4, 2, 0, 4], + [9, 0, 1, 8, 4, 7, 2, 3, 11], + [4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1], + [3, 10, 1, 3, 11, 10, 7, 8, 4], + [1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4], + [4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3], + [4, 7, 11, 4, 11, 9, 9, 11, 10], + [9, 5, 4], + [9, 5, 4, 0, 8, 3], + [0, 5, 4, 1, 5, 0], + [8, 5, 4, 8, 3, 5, 3, 1, 5], + [1, 2, 10, 9, 5, 4], + [3, 0, 8, 1, 2, 10, 4, 9, 5], + [5, 2, 10, 5, 4, 2, 4, 0, 2], + [2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8], + [9, 5, 4, 2, 3, 11], + [0, 11, 2, 0, 8, 11, 4, 9, 5], + [0, 5, 4, 0, 1, 5, 2, 3, 11], + [2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5], + [10, 3, 11, 10, 1, 3, 9, 5, 4], + [4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10], + [5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3], + [5, 4, 8, 5, 8, 10, 10, 8, 11], + [9, 7, 8, 5, 7, 9], + [9, 3, 0, 9, 5, 3, 5, 7, 3], + [0, 7, 8, 0, 1, 7, 1, 5, 7], + [1, 5, 3, 3, 5, 7], + [9, 7, 8, 9, 5, 7, 10, 1, 2], + [10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3], + [8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2], + [2, 10, 5, 2, 5, 3, 3, 5, 7], + [7, 9, 5, 7, 8, 9, 3, 11, 2], + [9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11], + [2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7], + [11, 2, 1, 11, 1, 7, 7, 1, 5], + [9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11], + [5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0], + [11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0], + [11, 10, 5, 7, 11, 5], + [10, 6, 5], + [0, 8, 3, 5, 10, 6], + [9, 0, 1, 5, 10, 6], + [1, 8, 3, 1, 9, 8, 5, 10, 6], + [1, 6, 5, 2, 6, 1], + [1, 6, 5, 1, 2, 6, 3, 0, 8], + [9, 6, 5, 9, 0, 6, 0, 2, 6], + [5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8], + [2, 3, 11, 10, 6, 5], + [11, 0, 8, 11, 2, 0, 10, 6, 5], + [0, 1, 9, 2, 3, 11, 5, 10, 6], + [5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11], + [6, 3, 11, 6, 5, 3, 5, 1, 3], + [0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6], + [3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9], + [6, 5, 9, 6, 9, 11, 11, 9, 8], + [5, 10, 6, 4, 7, 8], + [4, 3, 0, 4, 7, 3, 6, 5, 10], + [1, 9, 0, 5, 10, 6, 8, 4, 7], + [10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4], + [6, 1, 2, 6, 5, 1, 4, 7, 8], + [1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7], + [8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6], + [7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9], + [3, 11, 2, 7, 8, 4, 10, 6, 5], + [5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11], + [0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6], + [9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6], + [8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6], + [5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11], + [0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7], + [6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9], + [10, 4, 9, 6, 4, 10], + [4, 10, 6, 4, 9, 10, 0, 8, 3], + [10, 0, 1, 10, 6, 0, 6, 4, 0], + [8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10], + [1, 4, 9, 1, 2, 4, 2, 6, 4], + [3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4], + [0, 2, 4, 4, 2, 6], + [8, 3, 2, 8, 2, 4, 4, 2, 6], + [10, 4, 9, 10, 6, 4, 11, 2, 3], + [0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6], + [3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10], + [6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1], + [9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3], + [8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1], + [3, 11, 6, 3, 6, 0, 0, 6, 4], + [6, 4, 8, 11, 6, 8], + [7, 10, 6, 7, 8, 10, 8, 9, 10], + [0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10], + [10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0], + [10, 6, 7, 10, 7, 1, 1, 7, 3], + [1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7], + [2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9], + [7, 8, 0, 7, 0, 6, 6, 0, 2], + [7, 3, 2, 6, 7, 2], + [2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7], + [2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7], + [1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11], + [11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1], + [8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6], + [0, 9, 1, 11, 6, 7], + [7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0], + [7, 11, 6], + [7, 6, 11], + [3, 0, 8, 11, 7, 6], + [0, 1, 9, 11, 7, 6], + [8, 1, 9, 8, 3, 1, 11, 7, 6], + [10, 1, 2, 6, 11, 7], + [1, 2, 10, 3, 0, 8, 6, 11, 7], + [2, 9, 0, 2, 10, 9, 6, 11, 7], + [6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8], + [7, 2, 3, 6, 2, 7], + [7, 0, 8, 7, 6, 0, 6, 2, 0], + [2, 7, 6, 2, 3, 7, 0, 1, 9], + [1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6], + [10, 7, 6, 10, 1, 7, 1, 3, 7], + [10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8], + [0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7], + [7, 6, 10, 7, 10, 8, 8, 10, 9], + [6, 8, 4, 11, 8, 6], + [3, 6, 11, 3, 0, 6, 0, 4, 6], + [8, 6, 11, 8, 4, 6, 9, 0, 1], + [9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6], + [6, 8, 4, 6, 11, 8, 2, 10, 1], + [1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6], + [4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9], + [10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3], + [8, 2, 3, 8, 4, 2, 4, 6, 2], + [0, 4, 2, 4, 6, 2], + [1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8], + [1, 9, 4, 1, 4, 2, 2, 4, 6], + [8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1], + [10, 1, 0, 10, 0, 6, 6, 0, 4], + [4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3], + [10, 9, 4, 6, 10, 4], + [4, 9, 5, 7, 6, 11], + [0, 8, 3, 4, 9, 5, 11, 7, 6], + [5, 0, 1, 5, 4, 0, 7, 6, 11], + [11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5], + [9, 5, 4, 10, 1, 2, 7, 6, 11], + [6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5], + [7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2], + [3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6], + [7, 2, 3, 7, 6, 2, 5, 4, 9], + [9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7], + [3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0], + [6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8], + [9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7], + [1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4], + [4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10], + [7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10], + [6, 9, 5, 6, 11, 9, 11, 8, 9], + [3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5], + [0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11], + [6, 11, 3, 6, 3, 5, 5, 3, 1], + [1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6], + [0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10], + [11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5], + [6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3], + [5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2], + [9, 5, 6, 9, 6, 0, 0, 6, 2], + [1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8], + [1, 5, 6, 2, 1, 6], + [1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6], + [10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0], + [0, 3, 8, 5, 6, 10], + [10, 5, 6], + [11, 5, 10, 7, 5, 11], + [11, 5, 10, 11, 7, 5, 8, 3, 0], + [5, 11, 7, 5, 10, 11, 1, 9, 0], + [10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1], + [11, 1, 2, 11, 7, 1, 7, 5, 1], + [0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11], + [9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7], + [7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2], + [2, 5, 10, 2, 3, 5, 3, 7, 5], + [8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5], + [9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2], + [9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2], + [1, 3, 5, 3, 7, 5], + [0, 8, 7, 0, 7, 1, 1, 7, 5], + [9, 0, 3, 9, 3, 5, 5, 3, 7], + [9, 8, 7, 5, 9, 7], + [5, 8, 4, 5, 10, 8, 10, 11, 8], + [5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0], + [0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5], + [10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4], + [2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8], + [0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11], + [0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5], + [9, 4, 5, 2, 11, 3], + [2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4], + [5, 10, 2, 5, 2, 4, 4, 2, 0], + [3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9], + [5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2], + [8, 4, 5, 8, 5, 3, 3, 5, 1], + [0, 4, 5, 1, 0, 5], + [8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5], + [9, 4, 5], + [4, 11, 7, 4, 9, 11, 9, 10, 11], + [0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11], + [1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11], + [3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4], + [4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2], + [9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3], + [11, 7, 4, 11, 4, 2, 2, 4, 0], + [11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4], + [2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9], + [9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7], + [3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10], + [1, 10, 2, 8, 7, 4], + [4, 9, 1, 4, 1, 7, 7, 1, 3], + [4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1], + [4, 0, 3, 7, 4, 3], + [4, 8, 7], + [9, 10, 8, 10, 11, 8], + [3, 0, 9, 3, 9, 11, 11, 9, 10], + [0, 1, 10, 0, 10, 8, 8, 10, 11], + [3, 1, 10, 11, 3, 10], + [1, 2, 11, 1, 11, 9, 9, 11, 8], + [3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9], + [0, 2, 11, 8, 0, 11], + [3, 2, 11], + [2, 3, 8, 2, 8, 10, 10, 8, 9], + [9, 10, 2, 0, 9, 2], + [2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8], + [1, 10, 2], + [1, 3, 8, 9, 1, 8], + [0, 9, 1], + [0, 3, 8], + [] + ] + edgeShifts = np.array([ ## maps edge ID (0-11) to (x,y,z) cell offset and edge ID (0-2) + [0, 0, 0, 0], + [1, 0, 0, 1], + [0, 1, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0], + [1, 0, 1, 1], + [0, 1, 1, 0], + [0, 0, 1, 1], + [0, 0, 0, 2], + [1, 0, 0, 2], + [1, 1, 0, 2], + [0, 1, 0, 2], + #[9, 9, 9, 9] ## fake + ], dtype=np.ubyte) + nTableFaces = np.array([len(f)/3 for f in triTable], dtype=np.ubyte) + faceShiftTables = [None] + for i in range(1,6): + ## compute lookup table of index: vertexes mapping + faceTableI = np.zeros((len(triTable), i*3), dtype=np.ubyte) + faceTableInds = np.argwhere(nTableFaces == i) + faceTableI[faceTableInds[:,0]] = np.array([triTable[j] for j in faceTableInds]) + faceTableI = faceTableI.reshape((len(triTable), i, 3)) + faceShiftTables.append(edgeShifts[faceTableI]) + + ## Let's try something different: + #faceTable = np.empty((256, 5, 3, 4), dtype=np.ubyte) # (grid cell index, faces, vertexes, edge lookup) + #for i,f in enumerate(triTable): + #f = np.array(f + [12] * (15-len(f))).reshape(5,3) + #faceTable[i] = edgeShifts[f] + + + IsosurfaceDataCache = (faceShiftTables, edgeShifts, edgeTable, nTableFaces) + else: + faceShiftTables, edgeShifts, edgeTable, nTableFaces = IsosurfaceDataCache - ## map from grid cell index to edge index. - ## grid cell index tells us which corners are below the isosurface, - ## edge index tells us which edges are cut by the isosurface. - ## (Data stolen from Bourk; see above.) - edgeTable = [ - 0x0 , 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c, - 0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00, - 0x190, 0x99 , 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c, - 0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90, - 0x230, 0x339, 0x33 , 0x13a, 0x636, 0x73f, 0x435, 0x53c, - 0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30, - 0x3a0, 0x2a9, 0x1a3, 0xaa , 0x7a6, 0x6af, 0x5a5, 0x4ac, - 0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0, - 0x460, 0x569, 0x663, 0x76a, 0x66 , 0x16f, 0x265, 0x36c, - 0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60, - 0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0xff , 0x3f5, 0x2fc, - 0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0, - 0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x55 , 0x15c, - 0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950, - 0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0xcc , - 0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0, - 0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc, - 0xcc , 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0, - 0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c, - 0x15c, 0x55 , 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650, - 0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc, - 0x2fc, 0x3f5, 0xff , 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0, - 0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c, - 0x36c, 0x265, 0x16f, 0x66 , 0x76a, 0x663, 0x569, 0x460, - 0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac, - 0x4ac, 0x5a5, 0x6af, 0x7a6, 0xaa , 0x1a3, 0x2a9, 0x3a0, - 0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c, - 0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x33 , 0x339, 0x230, - 0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c, - 0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190, - 0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c, - 0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0 ] - ## Table of triangles to use for filling each grid cell. - ## Each set of three integers tells us which three edges to - ## draw a triangle between. - ## (Data stolen from Bourk; see above.) - triTable = [ - [], - [0, 8, 3], - [0, 1, 9], - [1, 8, 3, 9, 8, 1], - [1, 2, 10], - [0, 8, 3, 1, 2, 10], - [9, 2, 10, 0, 2, 9], - [2, 8, 3, 2, 10, 8, 10, 9, 8], - [3, 11, 2], - [0, 11, 2, 8, 11, 0], - [1, 9, 0, 2, 3, 11], - [1, 11, 2, 1, 9, 11, 9, 8, 11], - [3, 10, 1, 11, 10, 3], - [0, 10, 1, 0, 8, 10, 8, 11, 10], - [3, 9, 0, 3, 11, 9, 11, 10, 9], - [9, 8, 10, 10, 8, 11], - [4, 7, 8], - [4, 3, 0, 7, 3, 4], - [0, 1, 9, 8, 4, 7], - [4, 1, 9, 4, 7, 1, 7, 3, 1], - [1, 2, 10, 8, 4, 7], - [3, 4, 7, 3, 0, 4, 1, 2, 10], - [9, 2, 10, 9, 0, 2, 8, 4, 7], - [2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4], - [8, 4, 7, 3, 11, 2], - [11, 4, 7, 11, 2, 4, 2, 0, 4], - [9, 0, 1, 8, 4, 7, 2, 3, 11], - [4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1], - [3, 10, 1, 3, 11, 10, 7, 8, 4], - [1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4], - [4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3], - [4, 7, 11, 4, 11, 9, 9, 11, 10], - [9, 5, 4], - [9, 5, 4, 0, 8, 3], - [0, 5, 4, 1, 5, 0], - [8, 5, 4, 8, 3, 5, 3, 1, 5], - [1, 2, 10, 9, 5, 4], - [3, 0, 8, 1, 2, 10, 4, 9, 5], - [5, 2, 10, 5, 4, 2, 4, 0, 2], - [2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8], - [9, 5, 4, 2, 3, 11], - [0, 11, 2, 0, 8, 11, 4, 9, 5], - [0, 5, 4, 0, 1, 5, 2, 3, 11], - [2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5], - [10, 3, 11, 10, 1, 3, 9, 5, 4], - [4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10], - [5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3], - [5, 4, 8, 5, 8, 10, 10, 8, 11], - [9, 7, 8, 5, 7, 9], - [9, 3, 0, 9, 5, 3, 5, 7, 3], - [0, 7, 8, 0, 1, 7, 1, 5, 7], - [1, 5, 3, 3, 5, 7], - [9, 7, 8, 9, 5, 7, 10, 1, 2], - [10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3], - [8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2], - [2, 10, 5, 2, 5, 3, 3, 5, 7], - [7, 9, 5, 7, 8, 9, 3, 11, 2], - [9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11], - [2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7], - [11, 2, 1, 11, 1, 7, 7, 1, 5], - [9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11], - [5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0], - [11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0], - [11, 10, 5, 7, 11, 5], - [10, 6, 5], - [0, 8, 3, 5, 10, 6], - [9, 0, 1, 5, 10, 6], - [1, 8, 3, 1, 9, 8, 5, 10, 6], - [1, 6, 5, 2, 6, 1], - [1, 6, 5, 1, 2, 6, 3, 0, 8], - [9, 6, 5, 9, 0, 6, 0, 2, 6], - [5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8], - [2, 3, 11, 10, 6, 5], - [11, 0, 8, 11, 2, 0, 10, 6, 5], - [0, 1, 9, 2, 3, 11, 5, 10, 6], - [5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11], - [6, 3, 11, 6, 5, 3, 5, 1, 3], - [0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6], - [3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9], - [6, 5, 9, 6, 9, 11, 11, 9, 8], - [5, 10, 6, 4, 7, 8], - [4, 3, 0, 4, 7, 3, 6, 5, 10], - [1, 9, 0, 5, 10, 6, 8, 4, 7], - [10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4], - [6, 1, 2, 6, 5, 1, 4, 7, 8], - [1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7], - [8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6], - [7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9], - [3, 11, 2, 7, 8, 4, 10, 6, 5], - [5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11], - [0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6], - [9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6], - [8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6], - [5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11], - [0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7], - [6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9], - [10, 4, 9, 6, 4, 10], - [4, 10, 6, 4, 9, 10, 0, 8, 3], - [10, 0, 1, 10, 6, 0, 6, 4, 0], - [8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10], - [1, 4, 9, 1, 2, 4, 2, 6, 4], - [3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4], - [0, 2, 4, 4, 2, 6], - [8, 3, 2, 8, 2, 4, 4, 2, 6], - [10, 4, 9, 10, 6, 4, 11, 2, 3], - [0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6], - [3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10], - [6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1], - [9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3], - [8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1], - [3, 11, 6, 3, 6, 0, 0, 6, 4], - [6, 4, 8, 11, 6, 8], - [7, 10, 6, 7, 8, 10, 8, 9, 10], - [0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10], - [10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0], - [10, 6, 7, 10, 7, 1, 1, 7, 3], - [1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7], - [2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9], - [7, 8, 0, 7, 0, 6, 6, 0, 2], - [7, 3, 2, 6, 7, 2], - [2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7], - [2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7], - [1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11], - [11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1], - [8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6], - [0, 9, 1, 11, 6, 7], - [7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0], - [7, 11, 6], - [7, 6, 11], - [3, 0, 8, 11, 7, 6], - [0, 1, 9, 11, 7, 6], - [8, 1, 9, 8, 3, 1, 11, 7, 6], - [10, 1, 2, 6, 11, 7], - [1, 2, 10, 3, 0, 8, 6, 11, 7], - [2, 9, 0, 2, 10, 9, 6, 11, 7], - [6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8], - [7, 2, 3, 6, 2, 7], - [7, 0, 8, 7, 6, 0, 6, 2, 0], - [2, 7, 6, 2, 3, 7, 0, 1, 9], - [1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6], - [10, 7, 6, 10, 1, 7, 1, 3, 7], - [10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8], - [0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7], - [7, 6, 10, 7, 10, 8, 8, 10, 9], - [6, 8, 4, 11, 8, 6], - [3, 6, 11, 3, 0, 6, 0, 4, 6], - [8, 6, 11, 8, 4, 6, 9, 0, 1], - [9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6], - [6, 8, 4, 6, 11, 8, 2, 10, 1], - [1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6], - [4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9], - [10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3], - [8, 2, 3, 8, 4, 2, 4, 6, 2], - [0, 4, 2, 4, 6, 2], - [1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8], - [1, 9, 4, 1, 4, 2, 2, 4, 6], - [8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1], - [10, 1, 0, 10, 0, 6, 6, 0, 4], - [4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3], - [10, 9, 4, 6, 10, 4], - [4, 9, 5, 7, 6, 11], - [0, 8, 3, 4, 9, 5, 11, 7, 6], - [5, 0, 1, 5, 4, 0, 7, 6, 11], - [11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5], - [9, 5, 4, 10, 1, 2, 7, 6, 11], - [6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5], - [7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2], - [3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6], - [7, 2, 3, 7, 6, 2, 5, 4, 9], - [9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7], - [3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0], - [6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8], - [9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7], - [1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4], - [4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10], - [7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10], - [6, 9, 5, 6, 11, 9, 11, 8, 9], - [3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5], - [0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11], - [6, 11, 3, 6, 3, 5, 5, 3, 1], - [1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6], - [0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10], - [11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5], - [6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3], - [5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2], - [9, 5, 6, 9, 6, 0, 0, 6, 2], - [1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8], - [1, 5, 6, 2, 1, 6], - [1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6], - [10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0], - [0, 3, 8, 5, 6, 10], - [10, 5, 6], - [11, 5, 10, 7, 5, 11], - [11, 5, 10, 11, 7, 5, 8, 3, 0], - [5, 11, 7, 5, 10, 11, 1, 9, 0], - [10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1], - [11, 1, 2, 11, 7, 1, 7, 5, 1], - [0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11], - [9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7], - [7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2], - [2, 5, 10, 2, 3, 5, 3, 7, 5], - [8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5], - [9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2], - [9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2], - [1, 3, 5, 3, 7, 5], - [0, 8, 7, 0, 7, 1, 1, 7, 5], - [9, 0, 3, 9, 3, 5, 5, 3, 7], - [9, 8, 7, 5, 9, 7], - [5, 8, 4, 5, 10, 8, 10, 11, 8], - [5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0], - [0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5], - [10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4], - [2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8], - [0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11], - [0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5], - [9, 4, 5, 2, 11, 3], - [2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4], - [5, 10, 2, 5, 2, 4, 4, 2, 0], - [3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9], - [5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2], - [8, 4, 5, 8, 5, 3, 3, 5, 1], - [0, 4, 5, 1, 0, 5], - [8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5], - [9, 4, 5], - [4, 11, 7, 4, 9, 11, 9, 10, 11], - [0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11], - [1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11], - [3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4], - [4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2], - [9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3], - [11, 7, 4, 11, 4, 2, 2, 4, 0], - [11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4], - [2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9], - [9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7], - [3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10], - [1, 10, 2, 8, 7, 4], - [4, 9, 1, 4, 1, 7, 7, 1, 3], - [4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1], - [4, 0, 3, 7, 4, 3], - [4, 8, 7], - [9, 10, 8, 10, 11, 8], - [3, 0, 9, 3, 9, 11, 11, 9, 10], - [0, 1, 10, 0, 10, 8, 8, 10, 11], - [3, 1, 10, 11, 3, 10], - [1, 2, 11, 1, 11, 9, 9, 11, 8], - [3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9], - [0, 2, 11, 8, 0, 11], - [3, 2, 11], - [2, 3, 8, 2, 8, 10, 10, 8, 9], - [9, 10, 2, 0, 9, 2], - [2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8], - [1, 10, 2], - [1, 3, 8, 9, 1, 8], - [0, 9, 1], - [0, 3, 8], - [] - ] - - ## translation between edge index and - ## the vertex indexes that bound the edge - edgeKey = [ - [(0,0,0), (1,0,0)], - [(1,0,0), (1,1,0)], - [(1,1,0), (0,1,0)], - [(0,1,0), (0,0,0)], - [(0,0,1), (1,0,1)], - [(1,0,1), (1,1,1)], - [(1,1,1), (0,1,1)], - [(0,1,1), (0,0,1)], - [(0,0,0), (0,0,1)], - [(1,0,0), (1,0,1)], - [(1,1,0), (1,1,1)], - [(0,1,0), (0,1,1)], - ] - - - - facets = [] ## mark everything below the isosurface level mask = data < level @@ -1494,35 +1669,93 @@ def isosurface(data, level): for k in [0,1]: fields[i,j,k] = mask[slices[i], slices[j], slices[k]] vertIndex = i - 2*j*i + 3*j + 4*k ## this is just to match Bourk's vertex numbering scheme - #print i,j,k," : ", fields[i,j,k], 2**vertIndex index += fields[i,j,k] * 2**vertIndex - #print index - #print index - ## add facets - for i in range(index.shape[0]): # data x-axis - for j in range(index.shape[1]): # data y-axis - for k in range(index.shape[2]): # data z-axis - tris = triTable[index[i,j,k]] - for l in range(0, len(tris), 3): ## faces for this grid cell - edges = tris[l:l+3] - pts = [] - for m in [0,1,2]: # points in this face - p1 = edgeKey[edges[m]][0] - p2 = edgeKey[edges[m]][1] - v1 = data[i+p1[0], j+p1[1], k+p1[2]] - v2 = data[i+p2[0], j+p2[1], k+p2[2]] - f = (level-v1) / (v2-v1) - fi = 1.0 - f - p = ( ## interpolate between corners - p1[0]*fi + p2[0]*f + i + 0.5, - p1[1]*fi + p2[1]*f + j + 0.5, - p1[2]*fi + p2[2]*f + k + 0.5 - ) - pts.append(p) - facets.append(pts) + ### Generate table of edges that have been cut + cutEdges = np.zeros([x+1 for x in index.shape]+[3], dtype=np.uint32) + edges = edgeTable[index] + for i, shift in enumerate(edgeShifts[:12]): + slices = [slice(shift[j],cutEdges.shape[j]+(shift[j]-1)) for j in range(3)] + cutEdges[slices[0], slices[1], slices[2], shift[3]] += edges & 2**i + + ## for each cut edge, interpolate to see where exactly the edge is cut and generate vertex positions + m = cutEdges > 0 + vertexInds = np.argwhere(m) ## argwhere is slow! + vertexes = vertexInds[:,:3].astype(np.float32) + dataFlat = data.reshape(data.shape[0]*data.shape[1]*data.shape[2]) + + ## re-use the cutEdges array as a lookup table for vertex IDs + cutEdges[vertexInds[:,0], vertexInds[:,1], vertexInds[:,2], vertexInds[:,3]] = np.arange(vertexInds.shape[0]) + + for i in [0,1,2]: + vim = vertexInds[:,3] == i + vi = vertexInds[vim, :3] + viFlat = (vi * (np.array(data.strides[:3]) / data.itemsize)[np.newaxis,:]).sum(axis=1) + v1 = dataFlat[viFlat] + v2 = dataFlat[viFlat + data.strides[i]/data.itemsize] + vertexes[vim,i] += (level-v1) / (v2-v1) + + ### compute the set of vertex indexes for each face. + + ## This works, but runs a bit slower. + #cells = np.argwhere((index != 0) & (index != 255)) ## all cells with at least one face + #cellInds = index[cells[:,0], cells[:,1], cells[:,2]] + #verts = faceTable[cellInds] + #mask = verts[...,0,0] != 9 + #verts[...,:3] += cells[:,np.newaxis,np.newaxis,:] ## we now have indexes into cutEdges + #verts = verts[mask] + #faces = cutEdges[verts[...,0], verts[...,1], verts[...,2], verts[...,3]] ## and these are the vertex indexes we want. + + + ## To allow this to be vectorized efficiently, we count the number of faces in each + ## grid cell and handle each group of cells with the same number together. + ## determine how many faces to assign to each grid cell + nFaces = nTableFaces[index] + totFaces = nFaces.sum() + faces = np.empty((totFaces, 3), dtype=np.uint32) + ptr = 0 + #import debug + #p = debug.Profiler('isosurface', disabled=False) + + ## this helps speed up an indexing operation later on + cs = np.array(cutEdges.strides)/cutEdges.itemsize + cutEdges = cutEdges.flatten() - return np.array(facets) + ## this, strangely, does not seem to help. + #ins = np.array(index.strides)/index.itemsize + #index = index.flatten() + + for i in range(1,6): + ### expensive: + #p.mark('1') + cells = np.argwhere(nFaces == i) ## all cells which require i faces (argwhere is expensive) + #p.mark('2') + if cells.shape[0] == 0: + continue + #cellInds = index[(cells*ins[np.newaxis,:]).sum(axis=1)] + cellInds = index[cells[:,0], cells[:,1], cells[:,2]] ## index values of cells to process for this round + #p.mark('3') + + ### expensive: + verts = faceShiftTables[i][cellInds] + #p.mark('4') + verts[...,:3] += cells[:,np.newaxis,np.newaxis,:] ## we now have indexes into cutEdges + verts = verts.reshape((verts.shape[0]*i,)+verts.shape[2:]) + #p.mark('5') + + ### expensive: + #print verts.shape + verts = (verts * cs[np.newaxis, np.newaxis, :]).sum(axis=2) + #vertInds = cutEdges[verts[...,0], verts[...,1], verts[...,2], verts[...,3]] ## and these are the vertex indexes we want. + vertInds = cutEdges[verts] + #p.mark('6') + nv = vertInds.shape[0] + #p.mark('7') + faces[ptr:ptr+nv] = vertInds #.reshape((nv, 3)) + #p.mark('8') + ptr += nv + + return vertexes, faces @@ -1592,4 +1825,4 @@ def pseudoScatter(data, spacing=None, shuffle=True): yvals[i] = y - return yvals[np.argsort(inds)] ## un-shuffle values before returning + return yvals[np.argsort(inds)] ## un-shuffle values before returning \ No newline at end of file diff --git a/graphicsItems/GraphicsItem.py b/graphicsItems/GraphicsItem.py index 4f4753b1..43b8148c 100644 --- a/graphicsItems/GraphicsItem.py +++ b/graphicsItems/GraphicsItem.py @@ -3,6 +3,7 @@ from pyqtgraph.GraphicsScene import GraphicsScene from pyqtgraph.Point import Point import pyqtgraph.functions as fn import weakref +import operator class GraphicsItem(object): """ @@ -395,8 +396,16 @@ class GraphicsItem(object): ## disconnect from previous view if oldView is not None: #print "disconnect:", self, oldView - oldView.sigRangeChanged.disconnect(self.viewRangeChanged) - oldView.sigTransformChanged.disconnect(self.viewTransformChanged) + try: + oldView.sigRangeChanged.disconnect(self.viewRangeChanged) + except TypeError: + pass + + try: + oldView.sigTransformChanged.disconnect(self.viewTransformChanged) + except TypeError: + pass + self._connectedView = None ## connect to new view @@ -449,4 +458,22 @@ class GraphicsItem(object): view = self.getViewBox() if view is not None and hasattr(view, 'implements') and view.implements('ViewBox'): view.itemBoundsChanged(self) ## inform view so it can update its range if it wants - \ No newline at end of file + + def childrenShape(self): + """Return the union of the shapes of all descendants of this item in local coordinates.""" + childs = self.allChildItems() + shapes = [self.mapFromItem(c, c.shape()) for c in self.allChildItems()] + return reduce(operator.add, shapes) + + def allChildItems(self, root=None): + """Return list of the entire item tree descending from this item.""" + if root is None: + root = self + tree = [] + for ch in root.childItems(): + tree.append(ch) + tree.extend(self.allChildItems(ch)) + return tree + + + \ No newline at end of file diff --git a/graphicsItems/GraphicsObject.py b/graphicsItems/GraphicsObject.py index 4361d1e6..121a67ea 100644 --- a/graphicsItems/GraphicsObject.py +++ b/graphicsItems/GraphicsObject.py @@ -1,4 +1,6 @@ -from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE +if not USE_PYSIDE: + import sip from .GraphicsItem import GraphicsItem __all__ = ['GraphicsObject'] @@ -20,4 +22,10 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject): self._updateView() if change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]: self.informViewBoundsChanged() + + ## workaround for pyqt bug: + ## http://www.riverbankcomputing.com/pipermail/pyqt/2012-August/031818.html + if not USE_PYSIDE and change == self.ItemParentChange and isinstance(ret, QtGui.QGraphicsItem): + ret = sip.cast(ret, QtGui.QGraphicsItem) + return ret diff --git a/graphicsItems/IsocurveItem.py b/graphicsItems/IsocurveItem.py index ef09928b..01ef57b6 100644 --- a/graphicsItems/IsocurveItem.py +++ b/graphicsItems/IsocurveItem.py @@ -45,12 +45,12 @@ class IsocurveItem(GraphicsObject): """ Set the data/image to draw isocurves for. - ============= ================================================================ + ============= ======================================================================== **Arguments** data A 2-dimensional ndarray. level The cutoff value at which to draw the curve. If level is not specified, - the previous level is used. - ============= ================================================================ + the previously set level is used. + ============= ======================================================================== """ if level is None: level = self.level @@ -74,6 +74,12 @@ class IsocurveItem(GraphicsObject): self.pen = fn.mkPen(*args, **kwargs) self.update() + def setBrush(self, *args, **kwargs): + """Set the brush used to draw the isocurve. Arguments can be any that are valid + for :func:`mkBrush `""" + self.brush = fn.mkBrush(*args, **kwargs) + self.update() + def updateLines(self, data, level): ##print "data:", data @@ -88,22 +94,28 @@ class IsocurveItem(GraphicsObject): self.setData(data, level) def boundingRect(self): - if self.path is None: + if self.data is None: return QtCore.QRectF() + if self.path is None: + self.generatePath() return self.path.boundingRect() def generatePath(self): - self.path = QtGui.QPainterPath() if self.data is None: + self.path = None return - lines = fn.isocurve(self.data, self.level) + lines = fn.isocurve(self.data, self.level, connected=True, extendToEdge=True) + self.path = QtGui.QPainterPath() for line in lines: self.path.moveTo(*line[0]) - self.path.lineTo(*line[1]) + for p in line[1:]: + self.path.lineTo(*p) def paint(self, p, *args): + if self.data is None: + return if self.path is None: self.generatePath() p.setPen(self.pen) p.drawPath(self.path) - \ No newline at end of file + \ No newline at end of file diff --git a/graphicsItems/ScatterPlotItem.py b/graphicsItems/ScatterPlotItem.py index f0dcfb60..155330f1 100644 --- a/graphicsItems/ScatterPlotItem.py +++ b/graphicsItems/ScatterPlotItem.py @@ -233,7 +233,7 @@ class ScatterPlotItem(GraphicsObject): self.bounds = [None, None] ## caches data bounds self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots - self.opts = {'pxMode': True, 'useCache': True} ## If useCache is False, symbols are re-drawn on every paint. + self.opts = {'pxMode': True, 'useCache': True, 'exportMode': False} ## If useCache is False, symbols are re-drawn on every paint. self.setPen(200,200,200, update=False) self.setBrush(100,100,150, update=False) @@ -664,10 +664,14 @@ class ScatterPlotItem(GraphicsObject): rect = QtCore.QRectF(y, x, h, w) self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect)) + def setExportMode(self, enabled, opts): + self.opts['exportMode'] = enabled + + def paint(self, p, *args): #p.setPen(fn.mkPen('r')) #p.drawRect(self.boundingRect()) - if self.opts['pxMode']: + if self.opts['pxMode'] is True: atlas = self.fragmentAtlas.getAtlas() #arr = fn.imageToArray(atlas.toImage(), copy=True) #if hasattr(self, 'lastAtlas'): @@ -681,7 +685,7 @@ class ScatterPlotItem(GraphicsObject): p.resetTransform() - if not USE_PYSIDE and self.opts['useCache']: + if not USE_PYSIDE and self.opts['useCache'] and self.opts['exportMode'] is False: p.drawPixmapFragments(self.fragments, atlas) else: for i in range(len(self.data)): diff --git a/graphicsItems/TextItem.py b/graphicsItems/TextItem.py index 5d84e23f..a85e919d 100644 --- a/graphicsItems/TextItem.py +++ b/graphicsItems/TextItem.py @@ -7,7 +7,7 @@ class TextItem(UIGraphicsItem): """ GraphicsItem displaying unscaled text (the text will always appear normal even inside a scaled ViewBox). """ - def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0), border=None, fill=None): + def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0), border=None, fill=None, angle=0): """ =========== ================================================================================= Arguments: @@ -22,6 +22,12 @@ class TextItem(UIGraphicsItem): *fill* A brush to use when filling within the border =========== ================================================================================= """ + + ## not working yet + #*angle* Angle in degrees to rotate text (note that the rotation assigned in this item's + #transformation will be ignored) + + UIGraphicsItem.__init__(self) self.textItem = QtGui.QGraphicsTextItem() self.lastTransform = None @@ -33,6 +39,7 @@ class TextItem(UIGraphicsItem): self.anchor = pg.Point(anchor) self.fill = pg.mkBrush(fill) self.border = pg.mkPen(border) + self.angle = angle #self.setFlag(self.ItemIgnoresTransformations) ## This is required to keep the text unscaled inside the viewport def setText(self, text, color=(200,200,200)): @@ -115,9 +122,11 @@ class TextItem(UIGraphicsItem): #p.fillRect(tbr) p.resetTransform() - p.drawRect(tbr) + #p.drawRect(tbr) p.translate(tbr.left(), tbr.top()) + p.rotate(self.angle) + p.drawRect(QtCore.QRectF(0, 0, tbr.width(), tbr.height())) self.textItem.paint(p, QtGui.QStyleOptionGraphicsItem(), None) \ No newline at end of file diff --git a/graphicsItems/UIGraphicsItem.py b/graphicsItems/UIGraphicsItem.py index 2bcdd66e..19fda424 100644 --- a/graphicsItems/UIGraphicsItem.py +++ b/graphicsItems/UIGraphicsItem.py @@ -1,6 +1,8 @@ -from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE import weakref from .GraphicsObject import GraphicsObject +if not USE_PYSIDE: + import sip __all__ = ['UIGraphicsItem'] class UIGraphicsItem(GraphicsObject): @@ -44,9 +46,12 @@ class UIGraphicsItem(GraphicsObject): def itemChange(self, change, value): ret = GraphicsObject.itemChange(self, change, value) - #if change == self.ItemParentHasChanged or change == self.ItemSceneHasChanged: ## handled by GraphicsItem now. - ##print "caught parent/scene change:", self.parentItem(), self.scene() - #self.updateView() + + ## workaround for pyqt bug: + ## http://www.riverbankcomputing.com/pipermail/pyqt/2012-August/031818.html + if not USE_PYSIDE and change == self.ItemParentChange and isinstance(ret, QtGui.QGraphicsItem): + ret = sip.cast(ret, QtGui.QGraphicsItem) + if change == self.ItemScenePositionHasChanged: self.setNewBounds() return ret diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py index 0602939f..a1cfde89 100644 --- a/graphicsItems/ViewBox/ViewBox.py +++ b/graphicsItems/ViewBox/ViewBox.py @@ -111,6 +111,7 @@ class ViewBox(GraphicsWidget): } self._updatingRange = False ## Used to break recursive loops. See updateAutoRange. + self.locateGroup = None ## items displayed when using ViewBox.locate(item) self.setFlag(self.ItemClipsChildrenToShape) self.setFlag(self.ItemIsFocusable, True) ## so we can receive key presses @@ -286,6 +287,12 @@ class ViewBox(GraphicsWidget): self.scene().removeItem(item) self.updateAutoRange() + def clear(self): + for i in self.addedItems[:]: + self.removeItem(i) + for ch in self.childGroup.childItems(): + ch.setParent(None) + def resizeEvent(self, ev): #self.setRange(self.range, padding=0) #self.updateAutoRange() @@ -1231,6 +1238,46 @@ class ViewBox(GraphicsWidget): k.destroyed.disconnect() except RuntimeError: ## signal is already disconnected. pass + + def locate(self, item, timeout=3.0, children=False): + """ + Temporarily display the bounding rect of an item and lines connecting to the center of the view. + This is useful for determining the location of items that may be out of the range of the ViewBox. + if allChildren is True, then the bounding rect of all item's children will be shown instead. + """ + self.clearLocate() + if item.scene() is not self.scene(): + raise Exception("Item does not share a scene with this ViewBox.") + c = self.viewRect().center() + if children: + br = self.mapFromItemToView(item, item.childrenBoundingRect()).boundingRect() + else: + br = self.mapFromItemToView(item, item.boundingRect()).boundingRect() + + g = ItemGroup() + g.setParentItem(self.childGroup) + self.locateGroup = g + g.box = QtGui.QGraphicsRectItem(br) + g.box.setParentItem(g) + g.lines = [] + for p in (br.topLeft(), br.bottomLeft(), br.bottomRight(), br.topRight()): + line = QtGui.QGraphicsLineItem(c.x(), c.y(), p.x(), p.y()) + line.setParentItem(g) + g.lines.append(line) + + for item in g.childItems(): + item.setPen(fn.mkPen(color='y', width=3)) + item.setZValue(1000000) + + QtCore.QTimer.singleShot(timeout*1000, self.clearLocate) + + def clearLocate(self): + if self.locateGroup is None: + return + self.scene().removeItem(self.locateGroup) + self.locateGroup = None + + from .ViewBoxMenu import ViewBoxMenu diff --git a/opengl/items/GLMeshItem.py b/opengl/items/GLMeshItem.py index 5d98285f..4222c96b 100644 --- a/opengl/items/GLMeshItem.py +++ b/opengl/items/GLMeshItem.py @@ -158,7 +158,7 @@ class GLMeshItem(GLGraphicsItem): if self.colors is None: color = self.opts['color'] if isinstance(color, QtGui.QColor): - glColor4f(*fn.glColor(color)) + glColor4f(*pg.glColor(color)) else: glColor4f(*color) else: diff --git a/python2_3.py b/python2_3.py index c295dbfc..2182d3a1 100644 --- a/python2_3.py +++ b/python2_3.py @@ -53,6 +53,7 @@ if sys.version_info[0] == 3: else: return 0 builtins.cmp = cmp + builtins.xrange = range #else: ## don't use __builtin__ -- this confuses things like pyshell and ActiveState's lazy import recipe #import __builtin__ #__builtin__.asUnicode = asUnicode diff --git a/reload.py b/reload.py index 91801fe1..b9459073 100644 --- a/reload.py +++ b/reload.py @@ -35,6 +35,7 @@ def reloadAll(prefix=None, debug=False): - if prefix is None, checks all loaded modules """ failed = [] + changed = [] for modName, mod in list(sys.modules.items()): ## don't use iteritems; size may change during reload if not inspect.ismodule(mod): continue @@ -50,10 +51,11 @@ def reloadAll(prefix=None, debug=False): ## ignore if the .pyc is newer than the .py (or if there is no pyc or py) py = os.path.splitext(mod.__file__)[0] + '.py' pyc = py + 'c' - if os.path.isfile(pyc) and os.path.isfile(py) and os.stat(pyc).st_mtime >= os.stat(py).st_mtime: + if py not in changed and os.path.isfile(pyc) and os.path.isfile(py) and os.stat(pyc).st_mtime >= os.stat(py).st_mtime: #if debug: #print "Ignoring module %s; unchanged" % str(mod) continue + changed.append(py) ## keep track of which modules have changed to insure that duplicate-import modules get reloaded. try: reload(mod, debug=debug) @@ -73,7 +75,7 @@ def reload(module, debug=False, lists=False, dicts=False): - Requires that class and function names have not changed """ if debug: - print("Reloading", module) + print("Reloading %s" % str(module)) ## make a copy of the old module dictionary, reload, then grab the new module dictionary for comparison oldDict = module.__dict__.copy() @@ -158,7 +160,7 @@ def updateClass(old, new, debug): if isinstance(ref, old) and ref.__class__ is old: ref.__class__ = new if debug: - print(" Changed class for", safeStr(ref)) + print(" Changed class for %s" % safeStr(ref)) elif inspect.isclass(ref) and issubclass(ref, old) and old in ref.__bases__: ind = ref.__bases__.index(old) @@ -174,7 +176,7 @@ def updateClass(old, new, debug): ## (and I presume this may slow things down?) ref.__bases__ = ref.__bases__[:ind] + (new,old) + ref.__bases__[ind+1:] if debug: - print(" Changed superclass for", safeStr(ref)) + print(" Changed superclass for %s" % safeStr(ref)) #else: #if debug: #print " Ignoring reference", type(ref) @@ -208,7 +210,7 @@ def updateClass(old, new, debug): for attr in dir(new): if not hasattr(old, attr): if debug: - print(" Adding missing attribute", attr) + print(" Adding missing attribute %s" % attr) setattr(old, attr, getattr(new, attr)) ## finally, update any previous versions still hanging around.. diff --git a/setup.py b/setup.py index 9d56a0cf..31376ce5 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,11 @@ all_packages = ['.'.join(p) for p in subdirs] setup(name='pyqtgraph', version='', description='Scientific Graphics and GUI Library for Python', - long_description="PyQtGraph is a pure-python graphics and GUI library built on PyQt4 and numpy. It is intended for use in mathematics / scientific / engineering applications. Despite being written entirely in python, the library is very fast due to its heavy leverage of numpy for number crunching and Qt's GraphicsView framework for fast display.", + long_description="""\ +PyQtGraph is a pure-python graphics and GUI library built on PyQt4/PySide and numpy. + +It is intended for use in mathematics / scientific / engineering applications. Despite being written entirely in python, the library is very fast due to its heavy leverage of numpy for number crunching and Qt's GraphicsView framework for fast display. +""", license='MIT', url='http://www.pyqtgraph.org', author='Luke Campagnola', @@ -17,5 +21,17 @@ setup(name='pyqtgraph', packages=all_packages, package_dir = {'pyqtgraph': '.'}, package_data={'pyqtgraph': ['graphicsItems/PlotItem/*.png']}, + classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Development Status :: 4 - Beta", + "Environment :: Other Environment", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Scientific/Engineering :: Visualization", + "Topic :: Software Development :: User Interfaces", + ], )