def generateSphereImageStack(width,height,slices,center=vec3(0.5),scale=vec3(0.45),innerval=0.75,shellval=1.0): '''Create a sphere image on an otherwise blank image stack made with generateImageStack().''' dim=vec3(width,height,slices) center=center*dim images=generateImageStack(width,height,slices) for i,im in enumerate(images): imax=0.0 for n,m in matIterate(im.img): p=(vec3(m,n,i)-center)/(scale*dim) val=0 plen=p.len() # plen is twice the distance from image stack center to p if plen<0.9: # inner part of circle val=innerval elif plen<1.0: # circle edge val=shellval else: continue im.img.setAt(val,n,m) imax=max(imax,val) im.imgmin=0.0 im.imgmax=imax return images
def generateImageStack(width, height, slices, timesteps=1, pos=vec3(), rot=rotator(), spacing=vec3(1), name='img'): '''Create a blank image stack with each timestep ordered bottom-up with integer timesteps.''' assert width > 0 assert height > 0 assert slices > 0 assert spacing.x() > 0 assert spacing.y() > 0 assert timesteps > 0 images = [] positions = [ pos + (rot * vec3(0, 0, spacing.z() * s)) for s in range(slices) ] for t, s in trange(timesteps, slices): siname = '%s_%i_%i' % (name, s, t) si = SharedImage(siname, positions[s], rot, (width, height), (spacing.x(), spacing.y()), t) si.allocateImg(siname + 'Img') si.img.fill(0) images.append(si) return images
def _generatecut(task): for rep, figs in repfigspairs: tslen = len(rep.getTimestepList()) planept = planetrans.getTranslation() planerot = planetrans.getRotation() planenorm = planerot * vec3(0, 0, 1) assert tslen == len(figs) task.setMaxProgress(tslen) for i, tsrep in enumerate(rep.enumSubreprs()): task.setProgress(i + 1) snodes, sinds, scols = MeshAlgorithms.generateMeshPlanecut( tsrep.dataset, 'slicemesh%i' % i, planept, planenorm, self.linewidth, nodecolors=tsrep.nodecolors) vb = None ib = None if snodes: # transform the isolines to orthographic camera space snodes.sub(planept, 0, 0, snodes.n(), 1) snodes.mul(planerot.inverse()) snodes.mul(vec3(1, 1), 0, 0, snodes.n(), 1) vb = renderer.MatrixVertexBuffer(snodes, scols) ib = renderer.MatrixIndexBuffer(sinds) self.mgr.callThreadSafe(figs[i].fillData, vb, ib) self._repaintDelay()
def fillLineFig(self): '''Fills the figure representing the drawn line so that it is visible on screen while drawing.''' if self.drawingLine: orthot = self._planeToWorldTransform().inverse() vbuf = PyVertexBuffer([(orthot * self.lineStart) * vec3(1, 1), (orthot * self.lineEnd) * vec3(1, 1)], [vec3(0, 0, 1)] * 2, [self.drawcolor] * 2) ibuf = PyIndexBuffer([(0, 1)]) self.lineFig.fillData(vbuf, ibuf)
def mouseDrag(self, e, dx, dy): h = self.getSelectedHandle() if h: h.mouseDrag(e, vec3(dx, dy)) elif e.buttons() == Qt.LeftButton: self.scroll += vec3(dx, -dy, 0) elif e.buttons() == Qt.RightButton: self.zoom = max(0.01, self.zoom - dy * 0.01) self._repaintDelay()
def calculateReprIsoplaneMesh(rep, planetrans, stackpos, slicewidth): ''' Calculates the mesh for image representation object `rep' at plane `planetrans' and at the stack position `stackpos' (which is meaningful only for ImageSeriesRepr types). This will determine where `rep' is intersected by `planetrans' and returns the triple (nodes,indices,xis) representing a mesh for that intersecting geometry. 2D images viewed at oblique angles produces slices which are `slicewidth' wide. Returns a (nodes,indices,xis) triple where the nodes are in world space coordinates. ''' assert isinstance(rep, (ImageSeriesRepr, ImageVolumeRepr)) nodes, indices, xis = defaultImageQuad # default plane values # get a plane point and norm from the transform planept = planetrans.getTranslation() planenorm = planetrans.getRotation() * vec3(0, 0, 1) # calculate the plane mesh by transforming the default quad by the repr's defined transform, if isinstance(rep, ImageSeriesRepr): trans = rep.getDefinedTransform(stackpos) nodes = [trans * v for v in nodes] reppt = trans.getTranslation() repnorm = trans.getRotation() * vec3(0, 0, 1) # if this is being viewed at an oblique angle, determine where the image plane intersects the view plane and represent that as a 2D textured line if not SceneUtils.equalPlanes(reppt, repnorm, planept, planenorm): et = MathDef.ElemType.Quad1NL heights = [n.planeDist(planept, planenorm) for n in nodes ] # determine distance from each node to the view plane xipair = first( SceneUtils.calculateQuadIsoline(heights, et, 0) ) # calculate the isoline of intersection between the image plane and view plane if xipair: # if an isoline is found, calculate an isoline quad with a width of slicewidth xi1, xi2 = xipair pt1 = et.applyBasis(nodes, *xi1) pt2 = et.applyBasis(nodes, *xi2) nodes = SceneUtils.generateQuadFromLine( pt1, pt2, planenorm, slicewidth) xis = [xi1, xi1, xi2, xi2] else: nodes = [ ] # otherwise given no nodes, meaning nothing to see here # calculate the plane mesh by slicing through the volume at the plane defined by `planetrans' elif isinstance(rep, ImageVolumeRepr): inters = rep.getPlaneIntersects( planept, planenorm, True) # get the (node,xi) pairs for the intersecting polygon if len(inters): nodes, xis = list( zip(*inters)) # divide nodes and xis into separate lists indices = [(0, i + 1, i + 2) for i in range(len(inters) - 2)] # triangle fan indices return nodes, indices, xis
def fillContourFig(self): '''Fills the figure representing the drawn contour with lines so that it is visible on screen while drawing.''' if self.drawingContour: orthot = self._planeToWorldTransform().inverse() contour = self.contour[::10] if len( self.contour ) > 200 else self.contour # simplify contour if a lot of points are present nodes = [(orthot * n) * vec3(1, 1) for n in contour] # convert contour to 2D coordinates num = len(nodes) vbuf = PyVertexBuffer(nodes, [vec3(0, 0, 1)] * num, [self.drawcolor] * num) ibuf = PyIndexBuffer([(i, i + 1) for i in range(num - 1)]) self.contourFig.fillData(vbuf, ibuf)
def extendImage(obj,name,mx,my,mz,fillVal=0,numProcs=0,task=None): ''' Extends the image `obj' by the given margins (mx,my,mz) in each dimension. The extended regions are filled with the value `fillVall'. The `numProcs' value is used in determining how many processes to use (0 for all). The returned object has name `name' and represents the same spatial data as the original plus the added margins. ''' if mx==0 and my==0: images=[i.clone() for i in obj.images] else: obj.setShared(True) result=extendImageRange(len(obj.images),numProcs,task,obj.images,mx,my,fillVal,partitionArgs=(obj.images,)) checkResultMap(result) images=sumResultMap(result) dz=obj.getVolumeTransform().getRotation()*(obj.getVoxelSize()*vec3(0,0,1)) if mz>0: for stack in obj.getVolumeStacks(): for i in range(1,mz+1): img=images[stack[0]].clone() img.img.fill(fillVal) img.position-=dz*i img.calculateDimensions() images.append(img) img=images[stack[-1]].clone() img.img.fill(fillVal) img.position+=dz*i img.calculateDimensions() images.append(img) return obj.plugin.createSceneObject(name,images,obj.source,obj.isTimeDependent)
def generateTestImageStack(width,height,slices,timesteps=1,pos=vec3(),rot=rotator(),spacing=vec3(1)): '''Create a test pattern on an otherwise blank image stack made with generateImageStack().''' images=generateImageStack(width,height,slices,timesteps,pos,rot,spacing) for ind,im in enumerate(images): t=int(float(ind)/slices) i=ind%slices imax=0.0 for n,m in trange(im.img.n(),im.img.m()): val=0.0 if n==t and m==t: val=4.0 elif n==(i+1) or m==(i+1): val=0.5 elif n<20 and m<20: val=3.0 elif m<15 and i<15: val=2.0 elif n<10 and i<10: val=1.0 im.img.setAt(val,n,m) imax=max(imax,val) im.imgmin=0.0 im.imgmax=imax return images
def addPoint(self, name, text, col): ''' Add a positionable point called `name' with description `text' and color `col'. This will produce a quadruple (handle,label,button,edit) containing the PointHandle2D, QLable, QPushButton, and QLineEdit used to represent this point, storing it in self.pointMap keyed to name as well as returning it. ''' handle = SceneComponents.PointHandle2D(self, vec3(), col) label = QtWidgets.QLabel(text) button = QtWidgets.QPushButton('Set Plane') edit = QtWidgets.QLineEdit() self.gridLayout.addWidget(label, len(self.pointMap), 0) self.gridLayout.addWidget(button, len(self.pointMap), 1) self.gridLayout.addWidget(edit, len(self.pointMap), 2) label.setStyleSheet('background-color: %s;' % str(toQtColor(col).name())) edit.setReadOnly(True) def _setfunc(): handle.setNode(0, self.getWorldPosition(0.5, 0.5, False)) self._repaintDelay() button.clicked.connect(_setfunc) handle.setVisible(True) handle.setVisible3D(False) self.addHandle(handle) self.pointMap[name] = (handle, label, button, edit) return (handle, label, button, edit)
def getBBTransform(self): ''' Returns the boundbox transform which adjusts the figures to fit inside the selected viewing area based on the scene bound box, scroll, and zoom parameters. ''' bbw, bbh, bbd = self.sceneBB.getDimensions() bscale = self.getBoxFitScale(bbw, bbh) return transform(self.scroll - self.sceneBB.center * bscale, vec3(self.zoom * bscale, self.zoom * bscale))
def setParam(self,name,value): assert self.isInScene(), 'Cannot set parameters until representation is added to the scene' for fig in self.figs: if name=='width': fig.setDimension(value,fig.getHeight()) elif name=='height': fig.setDimension(fig.getWidth(),value) elif name=='glyphscale': fig.setGlyphScale(vec3(*toIterable(value))) elif name=='glyphname': fig.setGlyphName(value)
def mousePress(self, e): # check for handle selection if e.buttons() == Qt.LeftButton: self.selectHandleScreenCoord(e.x(), e.y()) else: self.deselectHandles() # reset view if e.buttons() == Qt.MiddleButton: self.scroll = vec3() self.zoom = 1.0 self._repaintDelay()
def loadImageFile(filename,imgobj,pos=vec3(),rot=rotator(),spacing=(1.0,1.0)): '''Returns a SharedImage object containing the image data from `imgobj' which must have a fillRealMatrix(mat) method.''' #imgobj=imgobj or Image.loadImageFile(filename) w=imgobj.getWidth() h=imgobj.getHeight() si=SharedImage(filename,pos,rot,(w,h),spacing) si.allocateImg(os.path.split(filename)[1]) imgobj.fillRealMatrix(si.img) si.readMinMaxValues() return si
def __init__(self,parent,reprtype,reprcount,refine,reprdata,parentdataset=None,drawInternal=False,externalOnly=False,matname='Default',aabb=None,**kwargs): SceneObjectRepr.__init__(self,parent,reprtype,reprcount,matname) self.refine=refine self.drawInternal=drawInternal self.externalOnly=externalOnly self.figs=[] #list of figures, one for each index set self.kwargs=dict(kwargs) self.position=vec3() self.scale=vec3(1,1,1) self.rotation=(0.0,0.0,0.0) self.datafuncs={} # map from names to functional expressions for tranforming data to material information self.datafuncs['valfunc']=MeshAlgorithms.ValueFunc._Average # data-to-unitvalue function self.datafuncs['alphafunc']=MeshAlgorithms.UnitFunc._One # unitvalue-to-unitvalue alpha function self.dataset,self.origindices=reprdata self.parentdataset=parentdataset self.nodes=self.dataset.getNodes() self.lines=self.dataset.getIndexSet(self.dataset.getName()+MatrixType.lines[1]) self.tris=self.dataset.getIndexSet(self.dataset.getName()+MatrixType.tris[1]) self.nodeprops=self.dataset.getIndexSet(self.dataset.getName()+MatrixType.props[1]) self.extinds=self.dataset.getIndexSet(self.dataset.getName()+MatrixType.extinds[1]) self.datafield=None #self.minfield=None #self.maxfield=None self.selminfield=None self.selmaxfield=None self.aabb=aabb self.bufferGen=ModifierBufferGenerator() self.nodecolors=ColorMatrix('Colors',self.nodes.n()) self.nodecolors.fill(color(1.0,1.0,1.0,1.0))
def setPlaneIndicator(self, obj, planetrans): '''Set the plane indicator in the 3D view for object `obj' to be at transform `planetrans'.''' if isinstance(obj.parent, ImageSceneObject) and obj.parent.is2D: self.indicatorPlane.setVisible(False) self.indicatorTrans = transform() else: bb = obj.getAABB() trans = transform(planetrans.getTranslation(), planetrans.getScale() * vec3(bb.radius * 1.5), planetrans.getRotation()) # needed to prevent update loops with _repaintDelay and _repaint3DDelay if trans != self.indicatorTrans or self.indicatorPlane.isVisible( ) != self.indicatorVisible: self.indicatorTrans = trans self.indicatorPlane.setTransform(trans) self.indicatorPlane.setVisible(self.indicatorVisible) self._repaint3DDelay()
def selectHandleScreenCoord(self, x, y): '''Select a handle based on the screen coordinate (`x',`y'). Returns the handle is selected, None otherwise.''' selected = None pos = vec3(x, y) for h in self.handles: if h.checkSelected( pos): # call this to set each handle's selected property selected = selected or h # if a handle was selected, call the notice method and set all other handles to be inactive if selected: self.handleSelected(selected) for h in self.handles: h.setActive(h == selected) self._repaintDelay() return selected
def drawMouseDrag(self, e, dx, dy): ''' Add points to the contour when the mouse is dragged with position delta (dx,dy). This will interpolate over large distances to (mostly) ensure a uniform distribution of points along the drawn contour. Returns True if the contour was drawn, False otherwise. ''' if not self.drawingContour: return False p = self.getWorldPosition(e.x(), e.y()) dlen = int(vec3(dx, dy).len()) if dlen: # if the mouse was moved relatively fast, add uniformly-spaced points along the drag path self.contour += [ Utils.lerp(i / dlen, self.contour[-1], p) for i in Utils.frange(dlen) ] else: self.contour.append(p) return True
def _imagePlaneMesh(self, rep, planetrans, stackpos, centerInView=False): ''' Returns the (nodes,indices,xis) mesh for the isoplane mesh defining the slice of of image representation `rep' at stack position `stackpos' or plane position `planetrans'. The nodes are in screen ortho coordinate space, adjusted to be within the viewing bound box if `centerInView' is True. ''' # get the mesh for the plane figure in world space coordinates nodes, indices, xis = ImageAlgorithms.calculateReprIsoplaneMesh( rep, planetrans, stackpos, self.slicewidth) # transform nodes into camera space coordinates invtrans = planetrans.inverse() invtrans.setScale(vec3(1, 1)) nodes = [invtrans * v for v in nodes] # transform the nodes to fit within the current viewing bound box if centerInView: bbt = self.getBBTransform() nodes = [bbt * n for n in nodes] return nodes, indices, xis
def getImagePosition(self, x, y): pos = self.getScreenOrthoPosition(x, y) rep = self.mgr.findObject(self.sourceName) imgstackpos = self.getImageStackPosition() trans = rep.getDefinedTransform(imgstackpos) invtrans = self.viewplane.inverse() invtrans.setScale(vec3(1, 1)) screennodes, indices, xis = self._imagePlaneMesh( rep, self.viewplane, imgstackpos, True) for tri in indices: nodes = [screennodes[i] for i in tri] trixis = [xis[i] for i in tri] nodes += [nodes[0] + nodes[0].planeNorm(nodes[1], nodes[2])] xi = pointSearchLinTet(pos, *nodes) if xi.isInUnitCube() and sum(xi) <= 1.0: imgxi = ElemType.Tri1NL.applyBasis(trixis, xi.x(), xi.y(), 0) return imgxi * trans.getScale().abs(), imgstackpos return None, imgstackpos
def getOrthoPosition(self, pos): ''' Returns the orthographic camera coordinate of the world vector `pos'. In orthographic coordinates, the screen center is (0,0) and bottom-right is (1,1). ''' return (self._planeToWorldTransform() / pos) * vec3(1, 1)
# Hounsfield value range Hounsfield=Utils.enum( ('air',-1000), ('water',0), ('muscle',10,40), ('blood',40), ('bone',1000), ('min',-1000), ('max',4000), # 4095? doc='Known Hounsfield unit values for various tissues, 1 or 2 values for a minimum and optional maximum range' ) # coordinates, indices, and xi values for a 2-triangle square on the XY plane centered on the origin defaultImageQuad=( (vec3(-0.5,0.5), vec3(0.5,0.5), vec3(-0.5,-0.5), vec3(0.5,-0.5)), # vertices ((0, 2, 1), (1, 2, 3)), # triangle indices (vec3(0,0), vec3(1,0), vec3(0,1), vec3(1,1)) # xi values ) def hounsfieldToUnit(h): '''Converts a Hounsfield unit value `h' to a unit value, 0 if `h' is the minimal Hounsfield value to 1 if maximal.''' return clamp(lerpXi(h,Hounsfield.min,Hounsfield.max),0.0,1.0) @timing def checkNan(obj): '''Assert that all values in the image object `obj' are not NaN.''' for i,img in enumerate(obj.images): for n,m in matIterate(img.img):
def initDefaultAssets(mgr): '''Initializes scene colors and lights, loads GPU scripts, and creates materials.''' # set the scene's default camera to a single camera, ambient light, and background color to match the UI color scheme mgr.setSingleFreeCamera() mgr.setAmbientLight(color(0.5, 0.5, 0.5)) mgr.setBackgroundColor(color(0.3515625, 0.3515625, 0.3515625)) if mgr.conf.get(platformID, ConfVars.camerazlock).lower() == 'false': mgr.setCameraZLocked(False) # create a default directional light that follows the camera cl = mgr.createLight(LightType._cdir, 'Camera Light') cl.setColor(color(0.25, 0.25, 0.25)) res = mgr.conf.get(platformID, ConfVars.resdir) mgr.scene.addResourceDir(res) mgr.scene.initializeResources() # load shaders/fragment programs for s in mgr.conf.get('Shaders', ConfVars.shaders).split(','): spec = mgr.conf.get('Shaders', s).split(',') ptype = PT_FRAGMENT if spec[0] == 'fragment' else PT_VERTEX profiles = spec[1] if len(spec) > 1 else None mgr.loadGPUScriptFile(res + s, ptype, profiles=profiles, ignoreError=True) # create spectrums s2 = mgr.createSpectrum('BW') s2.setSpectrumData([color(0, 0, 0), color()]) s3 = mgr.createSpectrum('BWAlpha') s3.setSpectrumData([color(0, 0, 0), color()], [0, 1], [vec3(0, 0), vec3(1, 1)]) s1 = mgr.createSpectrum('Rainbow') s1.setSpectrumData([ color(0.0, 0.0, 1.0), color(0.0, 1.0, 1.0), color(0.0, 1.0, 0.0), color(1.0, 1.0, 0.0), color(1.0, 0.0, 0.0) ]) s4 = mgr.createSpectrum('RedBlue') s4.setSpectrumData([color(1.0, 0.0, 0.0), color(0.0, 0.0, 1.0)]) s5 = mgr.createSpectrum('BlackGreenWhite') s5.setSpectrumData( [color(0.0, 0.0, 0.0), color(0, 1.0, 0), color(1.0, 1.0, 1.0)]) s6 = mgr.createSpectrum('BlueWhiteRed') s6.setSpectrumData( [color(0.0, 0.0, 1.0), color(1.0, 1.0, 1.0), color(1.0, 0.0, 0.0)]) s7 = mgr.createSpectrum('EMSpectrum') s7.setSpectrumData([ color(0.6, 0.0, 0.6), color(0.0, 1.0, 1.0), color(0.0, 1.0, 0.0), color(1.0, 1.0, 0.0), color(1.0, 0.0, 0.0), color(0.3, 0.0, 0.0) ]) ctspec = [ color(0, 0, 0), color(1, 0, 0), color(0.85, 0.85, 0.5), color(1, 1, 1) ] ctspecpos = [ hounsfieldToUnit(-150), hounsfieldToUnit(200), hounsfieldToUnit(400), hounsfieldToUnit(550) ] ctalpha = [ vec3(hounsfieldToUnit(-425), 0), vec3(hounsfieldToUnit(300), 0.25), vec3(hounsfieldToUnit(550), 1.0), vec3(hounsfieldToUnit(1050), 1.0), vec3(hounsfieldToUnit(2000), 0.0) ] s8 = mgr.createSpectrum('CardiacCT') s8.setSpectrumData(ctspec, ctspecpos, ctalpha) s9 = mgr.createSpectrum('CardiacCT2D') s9.setSpectrumData( ctspec, ctspecpos, [vec3(hounsfieldToUnit(-150), 0), vec3(hounsfieldToUnit(-100), 1.0)]) s10 = mgr.createSpectrum('Thermal') s10.setSpectrumData([ color(0.105, 0.047, 0.253), color(0.291, 0.046, 0.419), color(0.472, 0.111, 0.428), color(0.646, 0.174, 0.378), color(0.807, 0.263, 0.279), color(0.930, 0.411, 0.145), color(0.985, 0.601, 0.024), color(0.971, 0.813, 0.228), color(0.988, 0.998, 0.645) ]) # create materials m = mgr.createMaterial('Default') m = mgr.createMaterial('BlueWhiteRed') m.copySpectrumFrom(s6) m = mgr.createMaterial('Rainbow') m.copySpectrumFrom(s1) # prime color materials m = mgr.createMaterial('Red') m.setDiffuse(color(1, 0, 0)) m = mgr.createMaterial('Green') m.setDiffuse(color(0, 1, 0)) m = mgr.createMaterial('Blue') m.setDiffuse(color(0, 0, 1)) m = mgr.createMaterial('Yellow') m.setDiffuse(color(1, 1, 0)) m = mgr.createMaterial('Magenta') m.setDiffuse(color(1, 0, 1)) m = mgr.createMaterial('Cyan') m.setDiffuse(color(0, 1, 1)) m = mgr.createMaterial('BaseImage') m.useVertexColor(False) m.setGPUProgram('BaseImage', PT_FRAGMENT) m.copySpectrumFrom(s3) m = mgr.createMaterial('BaseImage2D') m.useVertexColor(False) m.setGPUProgram('BaseImage', PT_FRAGMENT) m.copySpectrumFrom(s2) m = mgr.createMaterial('BoundBoxes') m.useLighting(False) m = mgr.createMaterial('CtrlNode') m.useVertexColor(False) m.setPointSizeAbs(5.0) m.useLighting(False) m.setDiffuse(color(0.75, 0, 0)) m = mgr.createMaterial('Handle') m.useLighting(False) m.useDepthCheck(False) m = mgr.createMaterial('Contour') m.useLighting(False)
def calculateViewPlane(self, reptrans, planename=None, imgxi=0): ''' Return the transform object representing the plane in space named by `planename' or by `reptrans' alone if this isn't provided. If `planename' names a representation object then its transform is used to define the plane, specifically if its a ImageSceneObjectRepr then getDefinedTransform() is called to get this transform. If `planename' is one of the standard plane names (XY, YZ, or XZ) then the transform is defined to represent this plane at image xi value `imgxi' in the direction normal to the plane (ie. this is Z axis xi value for plane XY). The `reptrans' transform represents the transformation from xi space to world of the object the plane bisects, thus if the object is a volume this is the transformation from texture coordinates to world coordinates. This used to transform the standard plane definitions to world coordinates, and to define the resulting transform if `planename' names neither a representation object nor a standard plane. The return value is a transform in world space with a (1,1,1) scale component. ''' planerep = self.mgr.findObject(planename) if planerep != None: if isinstance(planerep, ImageSceneObjectRepr): planetrans = planerep.getDefinedTransform() planetrans.setScale(vec3(1)) else: planept = planerep.getPosition(True) planerot = rotator(*planerep.getRotation(True)) planetrans = transform( planept - (planerot * vec3(0, 0, self.planeShift)), vec3(1), planerot) elif self.isStdPlaneName(planename): xi = clamp(imgxi, epsilon * 100, 1.0 - epsilon * 100) # choose xi values in the volume representing the plane's center, normal, and right-hand direction if planename == 'XY': xipos = vec3(0.5, 0.5, xi) xinorm = xipos + vec3(0, 0, 1) xiright = xipos + vec3(1, 0, 0) elif planename == 'YZ': xipos = vec3(xi, 0.5, 0.5) xinorm = xipos + vec3(1, 0, 0) xiright = xipos + vec3(0, 1, 0) else: # XZ xipos = vec3(0.5, xi, 0.5) xinorm = xipos + vec3(0, 1, 0) xiright = xipos + vec3(1, 0, 0) # calculate a world position and rotation by applying the transform to the xi values planept = reptrans * xipos planerot = rotator((reptrans * xiright) - planept, (reptrans * xinorm) - planept, vec3(1, 0, 0), vec3(0, 0, 1)) planetrans = transform(planept, vec3(1), planerot) else: planetrans = transform(reptrans.getTranslation(), vec3(1), reptrans.getRotation()) return planetrans
def __init__(self, mgr, camera, indicatorCol=color(1, 1, 1, 0.25), parent=None): Base2DWidget.__init__(self, parent) self.mgr = mgr self.camera = camera self.camera.setOrtho(True) self.camera.setSecondaryCamera(True) self.camera.setPosition(vec3(0, 0, 0)) self.camera.setLookAt(vec3(0, 0, -1)) self.camera.setNearClip(epsilon * 100) self.camera.setFarClip(100.0) # ensure the skybox is clipped out self.sourceName = None self.planeName = None self.scroll = vec3() self.zoom = 1.0 self.viewplane = transform( ) # the plane in world space corresponding to the current 2D view self.objFigMap = { } # maps representation names to (planetrans,figs) pairs self.handles = [] # list of handles in this view self.slicewidth = 0.2 # width in world units of 2D slices self.linewidth = 1.0 # width in world units of mesh lines self.planeShift = 0.00005 # amount to move slice position down by in world units so that slice planes don't cut out the image where we want to see it self.sceneBB = BoundBox(BaseCamera2DWidget.defaultQuad[0]) self.indicatorCol = indicatorCol self.indicatorMaterial = self.mgr.scene.createMaterial( 'IndicatorPlane') self.indicatorMaterial.setDiffuse(color(1, 1, 1, 1)) self.indicatorMaterial.useLighting(False) self.indicatorTrans = transform() self.indicatorPlane = self.createFigure('PlaneIndicator%i' % id(self), FT_TRILIST, False) self.indicatorPlane.setMaterial(self.indicatorMaterial) self.indicatorPlane.setTransparent(True) self.indicatorPlane.setVisible(False) self.indicatorVisible = True # construct a quad with a cylinder rim for the indicator plane q = BaseCamera2DWidget.defaultQuad[0] mq = (q[0] + q[1]) * 0.5 cnodes, cinds = SceneUtils.generateCylinder( [mq, q[1], q[3], q[2], q[0], mq], [0.0025] * 6, 1, 4, False) nodes = list(BaseCamera2DWidget.defaultQuad[0]) + cnodes inds = list(BaseCamera2DWidget.defaultQuad[1]) + cinds self.indicatorPlane.fillData( PyVertexBuffer(nodes, [vec3(0, 0, 1)] * len(nodes), [indicatorCol] * len(nodes)), PyIndexBuffer(inds), False, True) delayedMethodWeak( self, '_repaintDelay' ) #delay method for repainting allows safe calling multiple times and from task threads mgr.addEventHandler(EventType._widgetPreDraw, self._repaintDelay)
class BaseCamera2DWidget(Base2DWidget): ''' This is the base class for all 2D drawing widgets using a camera from the renderer. It handles the update cycle of the camera rendering to a stream which is then fed into the image object for the widget. It provides createFigure() which will create Figure objects only visible to the internal camera. This class has no UI components and relies on inheriting subtypes calling modifyDrawWidget() to perform the correct association between the widget to draw into and the fillImage() method which updates the camera and copies its data over. The UI notion of this widget is to view a single primary image representation object in 2D, which can be dragged and zoomed. An image stack value allows scrolling in the through-plane direction if supported, this represents moving in the through-plane direction of an image volume or between planes of an image stack. Secondary images and meshes can be rendered over top of this image, these are sliced at the plane in 3D space which 2D view is currently "viewing". The following methods return default values and must be overridden in a subtype for this class to function: getImageStackPosition(), getImageStackMax(), getSecondaryNames(). ''' defaultQuad = ( (vec3(-0.5, 0.5), vec3(0.5, 0.5), vec3(-0.5, -0.5), vec3(0.5, -0.5)), # vertices ((0, 2, 1), (1, 2, 3)), # triangle indices (vec3(0, 0), vec3(1, 0), vec3(0, 1), vec3(1, 1)) # xi values ) standardPlanes = ('XY', 'XZ', 'YZ') def __init__(self, mgr, camera, indicatorCol=color(1, 1, 1, 0.25), parent=None): Base2DWidget.__init__(self, parent) self.mgr = mgr self.camera = camera self.camera.setOrtho(True) self.camera.setSecondaryCamera(True) self.camera.setPosition(vec3(0, 0, 0)) self.camera.setLookAt(vec3(0, 0, -1)) self.camera.setNearClip(epsilon * 100) self.camera.setFarClip(100.0) # ensure the skybox is clipped out self.sourceName = None self.planeName = None self.scroll = vec3() self.zoom = 1.0 self.viewplane = transform( ) # the plane in world space corresponding to the current 2D view self.objFigMap = { } # maps representation names to (planetrans,figs) pairs self.handles = [] # list of handles in this view self.slicewidth = 0.2 # width in world units of 2D slices self.linewidth = 1.0 # width in world units of mesh lines self.planeShift = 0.00005 # amount to move slice position down by in world units so that slice planes don't cut out the image where we want to see it self.sceneBB = BoundBox(BaseCamera2DWidget.defaultQuad[0]) self.indicatorCol = indicatorCol self.indicatorMaterial = self.mgr.scene.createMaterial( 'IndicatorPlane') self.indicatorMaterial.setDiffuse(color(1, 1, 1, 1)) self.indicatorMaterial.useLighting(False) self.indicatorTrans = transform() self.indicatorPlane = self.createFigure('PlaneIndicator%i' % id(self), FT_TRILIST, False) self.indicatorPlane.setMaterial(self.indicatorMaterial) self.indicatorPlane.setTransparent(True) self.indicatorPlane.setVisible(False) self.indicatorVisible = True # construct a quad with a cylinder rim for the indicator plane q = BaseCamera2DWidget.defaultQuad[0] mq = (q[0] + q[1]) * 0.5 cnodes, cinds = SceneUtils.generateCylinder( [mq, q[1], q[3], q[2], q[0], mq], [0.0025] * 6, 1, 4, False) nodes = list(BaseCamera2DWidget.defaultQuad[0]) + cnodes inds = list(BaseCamera2DWidget.defaultQuad[1]) + cinds self.indicatorPlane.fillData( PyVertexBuffer(nodes, [vec3(0, 0, 1)] * len(nodes), [indicatorCol] * len(nodes)), PyIndexBuffer(inds), False, True) delayedMethodWeak( self, '_repaintDelay' ) #delay method for repainting allows safe calling multiple times and from task threads mgr.addEventHandler(EventType._widgetPreDraw, self._repaintDelay) def _repaintDelay(self): self.mgr.callThreadSafe(self.repaint) @delayedcall( 0.5 ) # only need/want 1 delay thread for repainting the 3D scene, don't want to do this often def _repaint3DDelay(self): self.mgr.callThreadSafe(self.mgr.repaint) def getImageStackPosition(self): '''Get the index in the image stack of the source object. This must be overridden to set the stack position.''' return 0 def getImageStackMax(self): '''Get the maximum stack index. This must be overridden to define the max value as something other than 0.''' return 0 def getSecondaryNames(self): '''Get the names of secondary viewable objects. This must be overridden to return something other than [].''' return [] def getObjectNames(self): return [self.sourceName] + list(self.getSecondaryNames()) def getImageXiPosition(self): '''Get the xi value on the unit interval representing Z position within the stack the current view represents.''' maxv = self.getImageStackMax() return 0 if maxv == 0 else self.getImageStackPosition() / float(maxv) def mousePress(self, e): # check for handle selection if e.buttons() == Qt.LeftButton: self.selectHandleScreenCoord(e.x(), e.y()) else: self.deselectHandles() # reset view if e.buttons() == Qt.MiddleButton: self.scroll = vec3() self.zoom = 1.0 self._repaintDelay() def mouseDrag(self, e, dx, dy): h = self.getSelectedHandle() if h: h.mouseDrag(e, vec3(dx, dy)) elif e.buttons() == Qt.LeftButton: self.scroll += vec3(dx, -dy, 0) elif e.buttons() == Qt.RightButton: self.zoom = max(0.01, self.zoom - dy * 0.01) self._repaintDelay() def mouseRelease(self, e): self.deselectHandles() def parentClosed(self, e): self.removeHandles() self.mgr.removeEventHandler(self._repaintDelay) self.mgr.removeCamera(self.camera) self.camera = None self.indicatorPlane.setVisible(False) def fillImage(self, img): assert isMainThread() w, h = self.getDrawDims() if w > 0 and h > 0: self.camera.setAspectRatio(float(w) / h) self.camera.renderToStream(img.bits(), w, h, renderer.TF_ARGB32) def isStdPlaneName(self, name): '''Returns True if `name' is the name of a standard plane (ie. XY, YZ, XZ).''' return name in BaseCamera2DWidget.standardPlanes def calculateViewPlane(self, reptrans, planename=None, imgxi=0): ''' Return the transform object representing the plane in space named by `planename' or by `reptrans' alone if this isn't provided. If `planename' names a representation object then its transform is used to define the plane, specifically if its a ImageSceneObjectRepr then getDefinedTransform() is called to get this transform. If `planename' is one of the standard plane names (XY, YZ, or XZ) then the transform is defined to represent this plane at image xi value `imgxi' in the direction normal to the plane (ie. this is Z axis xi value for plane XY). The `reptrans' transform represents the transformation from xi space to world of the object the plane bisects, thus if the object is a volume this is the transformation from texture coordinates to world coordinates. This used to transform the standard plane definitions to world coordinates, and to define the resulting transform if `planename' names neither a representation object nor a standard plane. The return value is a transform in world space with a (1,1,1) scale component. ''' planerep = self.mgr.findObject(planename) if planerep != None: if isinstance(planerep, ImageSceneObjectRepr): planetrans = planerep.getDefinedTransform() planetrans.setScale(vec3(1)) else: planept = planerep.getPosition(True) planerot = rotator(*planerep.getRotation(True)) planetrans = transform( planept - (planerot * vec3(0, 0, self.planeShift)), vec3(1), planerot) elif self.isStdPlaneName(planename): xi = clamp(imgxi, epsilon * 100, 1.0 - epsilon * 100) # choose xi values in the volume representing the plane's center, normal, and right-hand direction if planename == 'XY': xipos = vec3(0.5, 0.5, xi) xinorm = xipos + vec3(0, 0, 1) xiright = xipos + vec3(1, 0, 0) elif planename == 'YZ': xipos = vec3(xi, 0.5, 0.5) xinorm = xipos + vec3(1, 0, 0) xiright = xipos + vec3(0, 1, 0) else: # XZ xipos = vec3(0.5, xi, 0.5) xinorm = xipos + vec3(0, 1, 0) xiright = xipos + vec3(1, 0, 0) # calculate a world position and rotation by applying the transform to the xi values planept = reptrans * xipos planerot = rotator((reptrans * xiright) - planept, (reptrans * xinorm) - planept, vec3(1, 0, 0), vec3(0, 0, 1)) planetrans = transform(planept, vec3(1), planerot) else: planetrans = transform(reptrans.getTranslation(), vec3(1), reptrans.getRotation()) return planetrans def createFigure(self, name, ftype=FT_TRILIST, is2DOnly=True): ''' Helper method for creating a figure with name `name' and type `ftype'. If `is2DOnly' is True then the returned figure is visible to this widget's camera only, otherwise it's default behaviour is unchanged. ''' assert isMainThread() fig = self.mgr.scene.createFigure(name, '', ftype) fig.fillData(PyVertexBuffer([]), PyIndexBuffer([])) fig.setVisible(True) if is2DOnly: # visible to this widget's 2D camera only fig.setCameraVisibility(None, False) fig.setCameraVisibility(self.camera, True) return fig def createMaterial(self, name, useLighting=False, useVertexColor=True): '''Helper method for creating a blank material which bypasses the UI.''' mat = self.mgr.scene.createMaterial(name) mat.useLighting(useLighting) mat.useVertexColor(useVertexColor) return mat def createTexture(self, name, width, height, format): '''Helper method for creating a texture which bypasses the UI.''' return self.mgr.scene.createTexture(name, width, height, 0, format) def addHandle(self, handle): '''Add the handle to the view and to self.handles.''' handle.addToScene(self.mgr, self.mgr.scene) self.handles.append(handle) self._repaintDelay() def removeHandle(self, index): '''Remove the handle at position `index' in self.handles to the view.''' h = self.handles.pop(index) h.removeFromScene(self.mgr, self.mgr.scene) self._repaintDelay() def removeHandles(self): '''Remove all handles from the view.''' while len(self.handles) > 0: self.removeHandle(0) self.handles = [] self._repaintDelay() def getHandle(self, index): '''Return handle at position `index' in the list of handles.''' return self.handles[index] def getSelectedHandle(self): '''Returns the selected handle, None otherwise.''' return first(h for h in self.handles if h.isSelected()) def selectHandleScreenCoord(self, x, y): '''Select a handle based on the screen coordinate (`x',`y'). Returns the handle is selected, None otherwise.''' selected = None pos = vec3(x, y) for h in self.handles: if h.checkSelected( pos): # call this to set each handle's selected property selected = selected or h # if a handle was selected, call the notice method and set all other handles to be inactive if selected: self.handleSelected(selected) for h in self.handles: h.setActive(h == selected) self._repaintDelay() return selected def deselectHandles(self): '''Deselect all handles.''' for h in self.handles: h.setSelected(False) def handleSelected(self, handle): ''' Called when the given handle object is selected by mouse click. If this is not overridden, handles are never activated but the view will otherwise function correctly. This method should be overridden to make a selected handle active. ''' pass def getObjFigures(self, name, numfigs=1, ftype=FT_TRILIST): '''Get the Figure objects for the object `name', or create `numfigs' objects of type `ftype' if none found.''' if name not in self.objFigMap: self.objFigMap[name] = (transform(), []) trans, figs = self.objFigMap[name] lfigs = len(figs) if lfigs < numfigs: for i in range(numfigs - lfigs): figs.append( self.createFigure('%s_2DFig%i' % (name, i + lfigs), ftype)) for i, f in enumerate(figs): f.setVisible(i < numfigs) return trans, figs def retainObjFigures(self, names): '''Keep only the figures for those objects named in the iterable `names.''' notfound = set(self.objFigMap.keys()).difference(set(names)) for nf in notfound: figs = self.objFigMap.pop(nf)[1] for f in figs: f.setVisible(False) def setObjPlane(self, name, planetrans): '''Set the view plane for the object named by `name' in self.objFigMap, retaining the figure list.''' self.objFigMap[name] = (planetrans, self.objFigMap[name][1]) def setFigsVisible(self, name, vis): '''Set the visibility of the figures to `vis' for object named by `name'.''' if name in self.objFigMap: for fig in self.objFigMap[name][1]: fig.setVisible(vis) def setPlaneIndicator(self, obj, planetrans): '''Set the plane indicator in the 3D view for object `obj' to be at transform `planetrans'.''' if isinstance(obj.parent, ImageSceneObject) and obj.parent.is2D: self.indicatorPlane.setVisible(False) self.indicatorTrans = transform() else: bb = obj.getAABB() trans = transform(planetrans.getTranslation(), planetrans.getScale() * vec3(bb.radius * 1.5), planetrans.getRotation()) # needed to prevent update loops with _repaintDelay and _repaint3DDelay if trans != self.indicatorTrans or self.indicatorPlane.isVisible( ) != self.indicatorVisible: self.indicatorTrans = trans self.indicatorPlane.setTransform(trans) self.indicatorPlane.setVisible(self.indicatorVisible) self._repaint3DDelay() def setIndicatorVisible(self, visible): '''Set whether the 3D plane indicator is visible or not.''' self.indicatorVisible = visible self._repaintDelay() def getBBTransform(self): ''' Returns the boundbox transform which adjusts the figures to fit inside the selected viewing area based on the scene bound box, scroll, and zoom parameters. ''' bbw, bbh, bbd = self.sceneBB.getDimensions() bscale = self.getBoxFitScale(bbw, bbh) return transform(self.scroll - self.sceneBB.center * bscale, vec3(self.zoom * bscale, self.zoom * bscale)) def _planeToWorldTransform(self): '''Returns the transform from plane-relative coordinates to world coordinates.''' return self.viewplane * self.getBBTransform().inverse() def getWorldPosition(self, x, y, isAbsolute=True): ''' Returns the world position of the screen coordinate (x,y). If `isAbsolute', (x,y) is an absolute pixel coordinate, otherwise it is a screen proportionate coordinate (ie. (0,0) is top-left corner of screen and (1,1) is bottom-right). ''' return self._planeToWorldTransform() * self.getScreenOrthoPosition( x, y, isAbsolute) def getScreenPosition(self, pos): '''Returns the screen coordinate of the world vector `pos'.''' return self.camera.getScreenPosition(self.getOrthoPosition(pos)) def getOrthoPosition(self, pos): ''' Returns the orthographic camera coordinate of the world vector `pos'. In orthographic coordinates, the screen center is (0,0) and bottom-right is (1,1). ''' return (self._planeToWorldTransform() / pos) * vec3(1, 1) def getScreenOrthoPosition(self, x, y, isAbsolute=True): ''' Returns the screen orthographic coordinate from the screen coordinate (x,y).If `isAbsolute', (x,y) is an absolute coordinate, otherwise it is a screen proportionate coordinate (ie. (0,0) is top-left corner, (1,1) is bottom-right). ''' return self.camera.getWorldPosition( x, y, isAbsolute ) # because the camera doesn't move from the origin this converts to ortho coordinates def getImagePosition(self, x, y): pos = self.getScreenOrthoPosition(x, y) rep = self.mgr.findObject(self.sourceName) imgstackpos = self.getImageStackPosition() trans = rep.getDefinedTransform(imgstackpos) invtrans = self.viewplane.inverse() invtrans.setScale(vec3(1, 1)) screennodes, indices, xis = self._imagePlaneMesh( rep, self.viewplane, imgstackpos, True) for tri in indices: nodes = [screennodes[i] for i in tri] trixis = [xis[i] for i in tri] nodes += [nodes[0] + nodes[0].planeNorm(nodes[1], nodes[2])] xi = pointSearchLinTet(pos, *nodes) if xi.isInUnitCube() and sum(xi) <= 1.0: imgxi = ElemType.Tri1NL.applyBasis(trixis, xi.x(), xi.y(), 0) return imgxi * trans.getScale().abs(), imgstackpos return None, imgstackpos def setFigTransforms(self): '''Set the transforms for all figures to fit them in the viewing area and translate/scale as inputed by user.''' bbt = self.getBBTransform() for _, figs in self.objFigMap.values(): for fig in figs: fig.setTransform(bbt) def _imagePlaneMesh(self, rep, planetrans, stackpos, centerInView=False): ''' Returns the (nodes,indices,xis) mesh for the isoplane mesh defining the slice of of image representation `rep' at stack position `stackpos' or plane position `planetrans'. The nodes are in screen ortho coordinate space, adjusted to be within the viewing bound box if `centerInView' is True. ''' # get the mesh for the plane figure in world space coordinates nodes, indices, xis = ImageAlgorithms.calculateReprIsoplaneMesh( rep, planetrans, stackpos, self.slicewidth) # transform nodes into camera space coordinates invtrans = planetrans.inverse() invtrans.setScale(vec3(1, 1)) nodes = [invtrans * v for v in nodes] # transform the nodes to fit within the current viewing bound box if centerInView: bbt = self.getBBTransform() nodes = [bbt * n for n in nodes] return nodes, indices, xis def _updatePlaneFig(self, fig, rep, planetrans, stackpos=0): ''' Updates `fig' to contain mesh data for isoplane cut through `rep' at plane `planetrans' if `rep' is a volume, otherwise at image stack position `stackpos'. Returns the bound box of the mesh. ''' assert rep != None, 'Cannot find representation for 2D view' nodes, indices, xis = self._imagePlaneMesh( rep, planetrans, stackpos) # mesh in screen ortho coordinates vb = PyVertexBuffer(nodes, [vec3.Z()] * len(nodes), None, xis) ib = PyIndexBuffer(indices) fig.fillData(vb, ib) return BoundBox(nodes) @delayedcall(0.15) def _updateMeshPlanecutFigs(self, repfigspairs, planetrans): '''Updates the figures containing mesh slice data for each secondary mesh object.''' @Utils.taskroutine('Generating Mesh Planecut') @Utils.timing def _generatecut(task): for rep, figs in repfigspairs: tslen = len(rep.getTimestepList()) planept = planetrans.getTranslation() planerot = planetrans.getRotation() planenorm = planerot * vec3(0, 0, 1) assert tslen == len(figs) task.setMaxProgress(tslen) for i, tsrep in enumerate(rep.enumSubreprs()): task.setProgress(i + 1) snodes, sinds, scols = MeshAlgorithms.generateMeshPlanecut( tsrep.dataset, 'slicemesh%i' % i, planept, planenorm, self.linewidth, nodecolors=tsrep.nodecolors) vb = None ib = None if snodes: # transform the isolines to orthographic camera space snodes.sub(planept, 0, 0, snodes.n(), 1) snodes.mul(planerot.inverse()) snodes.mul(vec3(1, 1), 0, 0, snodes.n(), 1) vb = renderer.MatrixVertexBuffer(snodes, scols) ib = renderer.MatrixIndexBuffer(sinds) self.mgr.callThreadSafe(figs[i].fillData, vb, ib) self._repaintDelay() return self.mgr.runTasks(_generatecut()) def updateView(self): ''' Update the visible data for the current view's position. This will update the quad for the main image, secondary images, and refill the isoline meshes for the secondary meshes. The plane in world space the 2D view currently shows will be set to self.viewplane. Handles will also be updated as necessary, and all figures will be transformed to fit into the current viewing position. If this method is overridden, the override should call this one to perform these operations after updating subtype-specific state, ie. as the last statement in the method. ''' assert isMainThread() rep = self.mgr.findObject( self.sourceName ) # get the main object, this is None if it's been deleted since the last update if rep == None: return imgstackpos = self.getImageStackPosition() self.retainObjFigures([ r.getName() for r in self.mgr.enumSceneObjectReprs() ]) # remove reprs that don't exist anymore # calculate the plane in space this view is located at self.viewplane = self.calculateViewPlane( rep.getDefinedTransform(imgstackpos), self.planeName, self.getImageXiPosition()) self.setPlaneIndicator(rep, self.viewplane) # update handle visibility/activity for h in self.handles: h.setPlaneVisible(self.viewplane) h.updateHandle() _, mainfig = self.getObjFigures( self.sourceName) # get the main object's figure # update the main figure's plane figure and set it's material to the representation's currently used one self.sceneBB = self._updatePlaneFig(mainfig[0], rep, self.viewplane, imgstackpos) mainfig[0].setOverlay(False) mat = rep.getCurrentTimestepMaterial(imgstackpos) if mat: mainfig[0].setMaterial(mat) repfigspairs = [] # update each secondary representation, either image or mesh types for s in self.getSecondaryNames(): s = s.split('<', 1)[0].strip() srep = self.mgr.findObject( s ) # split the label by <, assuming there's no < in the repr or object names assert srep, 'Cannot find %r' % s # image repr, 1 plane for volumes since they slice through at the view plane, 1 plane per slice of image series since they're 2D if isinstance(srep, ImageSceneObjectRepr): numslices = srep.getNumStackSlices() if isinstance( srep, ImageSeriesRepr) else 1 _, sfig = self.getObjFigures(s, numslices) for sslice in range(numslices): self.sceneBB += self._updatePlaneFig( sfig[sslice], srep, self.viewplane, sslice) sfig[sslice].setMaterial( srep.getCurrentTimestepMaterial(sslice)) sfig[sslice].setOverlay(True) else: # mesh repr, find the closest timestep to "now" timesteps = srep.getTimestepList() tsrepr = srep.getTimestepRepr() matname = tsrepr.getMaterialName() nearestTS, _ = minmaxIndices( abs(self.mgr.timestep - v) for v in timesteps) #strans,sfigs=self.getObjFigures(s,len(timesteps),FT_LINELIST) # get the view plane and figures for this repr strans, sfigs = self.getObjFigures(s, len( timesteps)) # get the view plane and figures for this repr self.setObjPlane(s, self.viewplane) # set the repr's view plane # calculate the repr's isolines where it intersects the viewing plane if the current view plane # differs from the one the previous isolines were calculated for, this ensure the calculation is # only done if the view plane has moved. if isinstance(tsrepr, MeshSceneObjectRepr) and ( tsrepr.tris != None or tsrepr.lines != None) and self.viewplane != strans: repfigspairs.append((srep, sfigs)) # set figure properties, making only the current timestep's figure visible for i, f in enumerate(sfigs): f.setOverlay(True) f.setMaterial(matname) f.setVisible( i == nearestTS ) # True only for the figure at the current timestep if repfigspairs: self._updateMeshPlanecutFigs(repfigspairs, self.viewplane) self.setFigTransforms()