Example #1
0
    def setObjColor(self, _IDs, _newRGBs, _newAlphas):
        # Changes the color and the alpha values of one or more objects
        # by creating a scene w/o duration
        # Parameters:
        # _IDs            := list of IDs
        # _newRGBs        := list of RGB tuples (bytes)
        # _newAlphas      := list of alpha values (byte)
        # e.g.
        # setObjColor([1,2], [(255,128,0), (0,63,128)], [128,255])
        #
        if (not (isinstance(_IDs, list)) or not (isinstance(_newRGBs, list))
                or not (isinstance(_newRGBs[0], tuple))
                or not (isinstance(_newAlphas, list))
                or (len(_IDs) != len(_newRGBs))
                or (len(_IDs) != len(_newAlphas))):
            self.LastErrC = StimErrC.invalidParamType
            raise StimException

        for ID in _IDs:
            try:
                self.ObjDict[ID]
            except KeyError:
                self.LastErrC = StimErrC.noMatchingID
                raise StimException

        RGBEx = ssp.completeRGBList(_newRGBs)
        newSce = [
            StimSceType.changeObjCol, -1, self.nSce, False, _IDs, RGBEx,
            _newAlphas,
            len(_IDs) * [SC_disableRGBAByVert]
        ]
        self.SceList.append(newSce)
        self.nSce += 1
        self.LastErrC = StimErrC.ok
Example #2
0
    def setObjColorAlphaByVertex(self, _IDs, _newRGBAs):
        # Change the color(s) of the given object(s)
        # (For parameters, see QDS.py)
        #
        if (not (isinstance(_IDs, list)) or not (isinstance(_newRGBAs, list))
                or not (isinstance(_newRGBAs[0], list))
                or not (isinstance(_newRGBAs[0][0], tuple))
                or (len(_IDs) != len(_newRGBAs))):
            self.LastErrC = StimErrC.invalidParamType
            raise StimException

        for ID in _IDs:
            try:
                self.ObjDict[ID]
            except KeyError:
                self.LastErrC = StimErrC.noMatchingID
                raise StimException

        RGBAEx = ssp.completeRGBAList(_newRGBAs)
        newSce = [
            StimSceType.changeObjCol, -1, self.nSce, False, _IDs, RGBAEx,
            len(_IDs) * [0],
            len(_IDs) * [SC_enableRGBAByVert]
        ]
        self.SceList.append(newSce)
        self.nSce += 1
        self.LastErrC = StimErrC.ok
Example #3
0
  def setObjColorAlphaByVertex(self, _IDs, _newRGBAs):
    # Change the color(s) of the given object(s)
    # (For parameters, see QDS.py)
    #
    if (not(isinstance(_IDs, list))
        or not(isinstance(_newRGBAs, list))
        or not(isinstance(_newRGBAs[0], list))
        or not(isinstance(_newRGBAs[0][0], tuple))
        or (len(_IDs) != len(_newRGBAs))):
      self.LastErrC = StimErrC.invalidParamType
      raise StimException

    for ID in _IDs:
      try:
        self.ObjDict[ID]
      except KeyError:
        self.LastErrC = StimErrC.noMatchingID
        raise StimException

    RGBAEx = ssp.completeRGBAList(_newRGBAs)    
    newSce = [StimSceType.changeObjCol, -1, self.nSce, False,
              _IDs,
              RGBAEx, len(_IDs)*[0], len(_IDs)*[SC_enableRGBAByVert]]
    self.SceList.append(newSce)
    self.nSce    += 1
    self.LastErrC = StimErrC.ok
Example #4
0
  def setObjColor(self, _IDs, _newRGBs, _newAlphas):
    # Changes the color and the alpha values of one or more objects
    # by creating a scene w/o duration
    # Parameters:
    # _IDs            := list of IDs
    # _newRGBs        := list of RGB tuples (bytes)
    # _newAlphas      := list of alpha values (byte)
    # e.g.
    # setObjColor([1,2], [(255,128,0), (0,63,128)], [128,255])
    #
    if (not(isinstance(_IDs, list))
        or not(isinstance(_newRGBs, list))
        or not(isinstance(_newRGBs[0], tuple))
        or not(isinstance(_newAlphas, list))
        or (len(_IDs) != len(_newRGBs)) or (len(_IDs) != len(_newAlphas))):
      self.LastErrC = StimErrC.invalidParamType
      raise StimException

    for ID in _IDs:
      try:
        self.ObjDict[ID]
      except KeyError:
        self.LastErrC = StimErrC.noMatchingID
        raise StimException
        
    RGBEx  = ssp.completeRGBList(_newRGBs)     
    newSce = [StimSceType.changeObjCol, -1, self.nSce, False,
              _IDs,
              RGBEx, _newAlphas, len(_IDs)*[SC_disableRGBAByVert]]
    self.SceList.append(newSce)
    self.nSce    += 1
    self.LastErrC = StimErrC.ok
Example #5
0
    def setBkgColor(self, _newRGB):
        # Changes the background color by creating a scene w/o duration
        # Parameters:
        # _newRGB         := RGB tuple (bytes)
        # e.g.
        # setBkgColor((255,128,0))
        #
        if not (isinstance(_newRGB, tuple)):
            self.LastErrC = StimErrC.invalidParamType
            raise StimException

        RGBEx = ssp.completeRGBList([_newRGB])
        newSce = [StimSceType.changeBkgCol, -1, self.nSce, False, RGBEx[0]]
        self.SceList.append(newSce)
        self.nSce += 1
        self.LastErrC = StimErrC.ok
Example #6
0
  def setBkgColor(self, _newRGB):
    # Changes the background color by creating a scene w/o duration
    # Parameters:
    # _newRGB         := RGB tuple (bytes)
    # e.g.
    # setBkgColor((255,128,0))
    #
    if not(isinstance(_newRGB, tuple)):
      self.LastErrC = StimErrC.invalidParamType
      raise StimException

    RGBEx  = ssp.completeRGBList([_newRGB])
    newSce = [StimSceType.changeBkgCol, -1, self.nSce, False,
              RGBEx[0]]
    self.SceList.append(newSce)
    self.nSce    += 1
    self.LastErrC = StimErrC.ok
Example #7
0
    def setShaderParams(self, _shID, _shParams):
        # Set or change the parameters of an existing shader
        # (For parameters, see QDS.py)
        #
        if (not (isinstance(_shID, int)) or not (isinstance(_shParams, list))):
            self.LastErrC = StimErrC.invalidParamType
            raise StimException

        try:
            iSh = self.ShDict[_shID]
        except KeyError:
            self.LastErrC = StimErrC.noMatchingID
            raise StimException

        shP = _shParams
        shType = self.ShList[iSh][SH_field_shaderType]
        try:
            iShD = self.ShManager.getShaderTypes().index(shType)
            ShD = self.ShManager.ShDesc[iShD]
        except ValueError:
            self.LastErrC = StimErrC.invalidShaderType
            raise StimException

        # Convert RBG values for each shader parameter (uniform) that
        # contains the substring "rgb"
        #
        for i, par in enumerate(ShD[1]):
            if (ShD[2][i] > 1) and ("rgb" in par.lower()):
                shP[i] = ssp.scaleRGBShader(self, shP[i])

        newSce = [
            StimSceType.changeShParams, -1, self.nSce, False, [_shID], shP
        ]
        self.SceList.append(newSce)
        self.nSce += 1
        self.LastErrC = StimErrC.ok
Example #8
0
  def setShaderParams(self, _shID, _shParams):
    # Set or change the parameters of an existing shader
    # (For parameters, see QDS.py)
    #
    if (not(isinstance(_shID, int))
        or not(isinstance(_shParams, list))):
      self.LastErrC = StimErrC.invalidParamType
      raise StimException

    try:
      iSh  = self.ShDict[_shID]
    except KeyError:
      self.LastErrC = StimErrC.noMatchingID
      raise StimException

    shP    = _shParams
    shType = self.ShList[iSh][SH_field_shaderType]
    try:
      iShD = self.ShManager.getShaderTypes().index(shType)
      ShD  = self.ShManager.ShDesc[iShD]
    except ValueError:
      self.LastErrC = StimErrC.invalidShaderType
      raise StimException

    # Convert RBG values for each shader parameter (uniform) that
    # contains the substring "rgb"
    #
    for i, par in enumerate(ShD[1]):
      if (ShD[2][i] > 1) and ("rgb" in par.lower()):
        shP[i] = ssp.scaleRGBShader(self, shP[i])

    newSce = [StimSceType.changeShParams, -1, self.nSce, False,
              [_shID], shP]
    self.SceList.append(newSce)
    self.nSce    += 1
    self.LastErrC = StimErrC.ok
Example #9
0
    def load(self, sFileName, _onlyInfo=False):
        # Load the compiled stimulus
        #
        self.clear()
        if not (_onlyInfo):
            ssp.Log.write(" ", "Loading compiled stimulus...", True)

        try:
            with open(sFileName + glo.QDSpy_cPickleFileExt, "rb") as stimFile:

                self.fileName = sFileName.replace("\\\\", "\\")
                stimPick = pickle.Unpickler(stimFile)
                ID = stimPick.load()

                if ID != glo.QDSpy_fileVersionID:
                    self.LastErrC = StimErrC.wrongStimFileFormat
                    ssp.Log.write("ERROR", self.getLastErrStr())
                    raise StimException(self.LastErrC)

                self.nameStr = stimPick.load()
                self.descrStr = stimPick.load()
                self.cFreq_Hz = stimPick.load()
                self.isUseLCr = stimPick.load()
                self.lenStim_s = stimPick.load()
                if not (_onlyInfo):
                    self.maxShObjPerRender = stimPick.load()
                    self.ObjList = stimPick.load()
                    self.ObjDict = stimPick.load()
                    self.ShList = stimPick.load()
                    self.ShDict = stimPick.load()
                    self.MovList = stimPick.load()
                    self.MovDict = stimPick.load()
                    self.VidList = stimPick.load()
                    self.VidDict = stimPick.load()
                    self.SceList = stimPick.load()
                    self.curBgRGB = stimPick.load()
                    self.cScMarkList = stimPick.load()
                    self.cScDurList = stimPick.load()
                    self.cScOList = stimPick.load()
                    self.ncODr = stimPick.load()
                    self.cODr_tr_iVert = stimPick.load()
                    self.cODr_tr_vertCoord = stimPick.load()
                    self.cODr_tr_vertRGBA = stimPick.load()
                    self.cODr_tr_vertRGBA2 = stimPick.load()

        except IOError:
            self.LastErrC = StimErrC.noCompiledStim
            ssp.Log.write("ERROR", self.getLastErrStr())
            raise StimException(self.LastErrC)

        # Get hash for pickle file
        #
        if not (_onlyInfo):
            self.md5Str = ssp.getHashStrForFile(sFileName +
                                                glo.QDSpy_cPickleFileExt)

        # Log some information
        #
        if not (_onlyInfo):
            ssp.Log.write(
                "ok", "Stimulus '{0}' loaded".format(sFileName +
                                                     glo.QDSpy_cPickleFileExt))
            ssp.Log.write(" ", "Name       : {0}".format(self.nameStr))
            ssp.Log.write(" ", "Description: {0}".format(self.descrStr))
            ssp.Log.write(" ", "Frequency  : {0} Hz".format(self.cFreq_Hz))

        self.isComp = True
        self.LastErrC = StimErrC.ok
Example #10
0
  def load(self, sFileName, _onlyInfo=False):
    # Load the compiled stimulus
    #
    self.clear()
    if not(_onlyInfo):
      ssp.Log.write(" ", "Loading compiled stimulus...", True)

    try:
      with open(sFileName + glo.QDSpy_cPickleFileExt, "rb") as stimFile:

        self.fileName = sFileName.replace("\\\\", "\\") 
        stimPick      = pickle.Unpickler(stimFile)
        ID            = stimPick.load()

        if ID !=  glo.QDSpy_fileVersionID:
          self.LastErrC = StimErrC.wrongStimFileFormat
          ssp.Log.write("ERROR", self.getLastErrStr())
          raise StimException(self.LastErrC)
          
        self.nameStr             = stimPick.load()
        self.descrStr            = stimPick.load()
        self.cFreq_Hz            = stimPick.load()
        self.isUseLCr            = stimPick.load()
        self.lenStim_s           = stimPick.load()
        if not(_onlyInfo):
          self.maxShObjPerRender = stimPick.load()
          self.ObjList           = stimPick.load()
          self.ObjDict           = stimPick.load()
          self.ShList            = stimPick.load()
          self.ShDict            = stimPick.load()
          self.MovList           = stimPick.load()
          self.MovDict           = stimPick.load()
          self.VidList           = stimPick.load()
          self.VidDict           = stimPick.load()
          self.SceList           = stimPick.load()
          self.curBgRGB          = stimPick.load()
          self.cScMarkList       = stimPick.load()
          self.cScDurList        = stimPick.load()
          self.cScOList          = stimPick.load()
          self.ncODr             = stimPick.load()
          self.cODr_tr_iVert     = stimPick.load()
          self.cODr_tr_vertCoord = stimPick.load()
          self.cODr_tr_vertRGBA  = stimPick.load()
          self.cODr_tr_vertRGBA2 = stimPick.load()

    except IOError:
      self.LastErrC = StimErrC.noCompiledStim
      ssp.Log.write("ERROR", self.getLastErrStr())
      raise StimException(self.LastErrC)
      
    # Get hash for pickle file  
    #
    if not(_onlyInfo):
      self.md5Str = ssp.getHashStrForFile(sFileName + glo.QDSpy_cPickleFileExt)

    # Log some information
    # 
    if not(_onlyInfo):
      ssp.Log.write("ok", "Stimulus '{0}' loaded"
                    .format(sFileName + glo.QDSpy_cPickleFileExt))
      ssp.Log.write(" ", "Name       : {0}".format(self.nameStr))
      ssp.Log.write(" ", "Description: {0}".format(self.descrStr))
      ssp.Log.write(" ", "Frequency  : {0} Hz".format(self.cFreq_Hz))
    
    self.isComp   = True
    self.LastErrC = StimErrC.ok
Example #11
0
def ell2vert (_ob, _iob, _sc, _Stage, _stim, _nextiV):
  # Generate vertices for an ellipse object from the description in _ob
  #
  (dx, dy)   = _ob[stm.SO_field_size]
  (mx, my)   = _sc[stm.SC_field_magXY][_iob]
  rot_deg    = _sc[stm.SC_field_rot][_iob]
  pxy        = _sc[stm.SC_field_posXY][_iob]

  # Determine center and radii, then calculate number of triangles
  # and vertices
  #
  rx         = dx *mx /2.0
  ry         = dy *my /2.0
  rm         = (rx+ry) /2.0
  nTri       = int(min(max(10, round(rm/1.5)), stm.Ellipse_maxTr))
  nPnts      = nTri +1
  dAng       = 2*np.pi/nTri

  # Center vertex, then points on perimeter
  #
  ang        = 0
  newVert    = [0, 0]
  for i in range(1, nPnts):
    newVert.append(round(rx*np.sin(ang)))
    newVert.append(round(ry*np.cos(ang)))
    ang      += dAng
  newVert    = spp.rotateTranslate(newVert, rot_deg, pxy)
  newVert    = spp.toInt(newVert)

  newiVTr    = []
  for i in range(nTri):
    newiVTr  += [_nextiV, _nextiV+i+1, _nextiV+i+2]
  newiVTr[len(newiVTr)-1] = newiVTr[1]

  if _ob[stm.SO_field_doRGBAByVert]:
    # *************
    # *************    
    # TODO: Currently uses only the first RGBA for the center and the
    #       second RGBA for the circumfence. It would be better, if RGBA[1:]
    #       were interpolated and arranged arround the circumfence ...
    # *************
    # *************
    RGBA0    = _ob[stm.SO_field_fgRGB][0][0:4]
    RGBA1    = _ob[stm.SO_field_fgRGB][1][0:4]
    newRGBA  = spp.scaleRGB(_stim, RGBA0)
    tmpRGBA  = (len(newVert)-1)//2 *spp.scaleRGB(_stim, RGBA1)
    newRGBA  += tmpRGBA
    RGBA0    = _ob[stm.SO_field_fgRGB][0][4:8]
    RGBA1    = _ob[stm.SO_field_fgRGB][1][4:8]
    newRGBA2 = spp.scaleRGB(_stim, RGBA0)
    tmpRGBA  = (len(newVert)-1)//2 *spp.scaleRGB(_stim, RGBA1)
    newRGBA2 += tmpRGBA
    
  else:
    tmpRGBA  = _ob[stm.SO_field_fgRGB][0:3] +(_ob[stm.SO_field_alpha],)
    newRGBA  = len(newVert)//2 *spp.scaleRGB(_stim, tmpRGBA)
    tmpRGBA  = _ob[stm.SO_field_fgRGB][3:6] +(_ob[stm.SO_field_alpha],)
    newRGBA2 = len(newVert)//2 *spp.scaleRGB(_stim, tmpRGBA)

  hList      = [stm.StimObjType.ellipse, dy,dy, mx,my, rot_deg, pxy]
  hStr       = spp.getHashStr(hList.__str__())

  return (newVert, newiVTr, newRGBA, newRGBA2, hStr, pxy, rot_deg)
Example #12
0
def box2vert (_ob, _iob, _sc, _Stage, _stim, _nextiV):
  # Generate vertices for a box object from the description in _ob
  #
  (dx, dy)  = _ob[stm.SO_field_size]
  (mx, my)  = _sc[stm.SC_field_magXY][_iob]
  dx2       = dx *mx /2.0
  dy2       = dy *my /2.0
  rot_deg   = _sc[stm.SC_field_rot][_iob]
  pxy       = _sc[stm.SC_field_posXY][_iob]
  rect      = [-dx2, -dy2, dx2, dy2]
  newVert   = [rect[0], rect[1], rect[2], rect[1],
               rect[2], rect[3], rect[0], rect[3]]
  newVert   = spp.rotateTranslate(newVert, rot_deg, pxy)
  newVert   = spp.toInt(newVert)
  newiVTr   = [_nextiV, _nextiV+1, _nextiV+2, _nextiV, _nextiV+2, _nextiV+3]
  if _ob[stm.SO_field_doRGBAByVert]:
    newRGBA = spp.scaleRGB(_stim, _ob[stm.SO_field_fgRGB][0][0:4]) +\
              spp.scaleRGB(_stim, _ob[stm.SO_field_fgRGB][1][0:4]) +\
              spp.scaleRGB(_stim, _ob[stm.SO_field_fgRGB][2][0:4]) +\
              spp.scaleRGB(_stim, _ob[stm.SO_field_fgRGB][3][0:4])
    newRGBA2= spp.scaleRGB(_stim, _ob[stm.SO_field_fgRGB][0][4:8]) +\
              spp.scaleRGB(_stim, _ob[stm.SO_field_fgRGB][1][4:8]) +\
              spp.scaleRGB(_stim, _ob[stm.SO_field_fgRGB][2][4:8]) +\
              spp.scaleRGB(_stim, _ob[stm.SO_field_fgRGB][3][4:8])
  else:
    tmpRGBA = _ob[stm.SO_field_fgRGB][0:3] +(_ob[stm.SO_field_alpha],) 
    newRGBA = len(newVert)//2 *spp.scaleRGB(_stim, tmpRGBA)
    tmpRGBA = _ob[stm.SO_field_fgRGB][3:6] +(_ob[stm.SO_field_alpha],)
    newRGBA2= len(newVert)//2 *spp.scaleRGB(_stim, tmpRGBA)

  hList     = [stm.StimObjType.box, dy,dy, mx,my, rot_deg, pxy]
  hStr      = spp.getHashStr(hList.__str__())

  return (newVert, newiVTr, newRGBA, newRGBA2, hStr, pxy, rot_deg)
Example #13
0
def sct2vert (_ob, _iob, _sc, _Stage, _stim, _nextiV):
  # Generate vertices for a sector object from the description in _ob
  #
  (r,offs,acenter,awidth,astep) = _ob[stm.SO_field_size]
  (mx, my)  = _sc[stm.SC_field_magXY][_iob]
  rot_deg   = _sc[stm.SC_field_rot][_iob]
  pxy       = _sc[stm.SC_field_posXY][_iob]

  if astep == None:
    # If astep is not given, choose the best one
    #
    if awidth == 360:
      # It's a full circle
      #
      astep = stm.Sector_maxStep
      
    else:
      # Try to minimize the number of triangles needed while
      # preserving the precission
      #
      for astep in range(stm.Sector_maxStep, 0, -1):
        if (int(awidth) % astep) == 0:
          break

  nSteps    = int(min(max(1, awidth/astep), stm.Sector_maxTr))
  '''
  spp.Log.write("DEBUG", "sct2vert: # steps={0}, angle={1}°, step angle={2}°"
                .format(nSteps, awidth, astep))
  '''
  acenter   = -2*np.pi *acenter/360.0 +np.pi
  awidth    = 2*np.pi *awidth/360.0
  astep     = 2*np.pi *astep/360.0

  if offs > 0:
    # Is arc ...
    #
    nPnts   = 2*nSteps +2
    ang     = acenter -awidth/2.0
    i       = 0
    newVert = []
    while i < (nPnts-1):
      newVert.append(r *np.sin(ang))
      newVert.append(-r *np.cos(ang))
      i     += 1
      newVert.append(offs *np.sin(ang))
      newVert.append(-offs *np.cos(ang))
      i     += 1
      ang   += astep

    newiVTr = []
    for i in range(nSteps*2):
      newiVTr  += [_nextiV+i, _nextiV+i+1, _nextiV+i+2]

    if _ob[stm.SO_field_doRGBAByVert]:
      RGBAout  = spp.scaleRGB(_stim, _ob[stm.SO_field_fgRGB][0][0:4])
      RGBAin   = spp.scaleRGB(_stim, _ob[stm.SO_field_fgRGB][1][0:4])
      RGBAout2 = spp.scaleRGB(_stim, _ob[stm.SO_field_fgRGB][0][4:8])
      RGBAin2  = spp.scaleRGB(_stim, _ob[stm.SO_field_fgRGB][1][4:8])
      newRGBA  = []
      newRGBA2 = []
      for i in range(len(newVert)//4):
        newRGBA  += RGBAout
        newRGBA  += RGBAin
        newRGBA2 += RGBAout2
        newRGBA2 += RGBAin2
        
    else:
      tmpRGBA  = _ob[stm.SO_field_fgRGB][0:3] +(_ob[stm.SO_field_alpha],)
      newRGBA  = len(newVert)//2 *spp.scaleRGB(_stim, tmpRGBA)
      tmpRGBA  = _ob[stm.SO_field_fgRGB][3:6] +(_ob[stm.SO_field_alpha],)
      newRGBA2 = len(newVert)//2 *spp.scaleRGB(_stim, tmpRGBA)

  else:
    # Is sector ...
    #
    nPnts   = nSteps +2
    ang     = acenter -awidth/2.0
    newVert = [0, 0]
    for i in range(1, nPnts):
      newVert.append(r *np.sin(ang))
      newVert.append(-r *np.cos(ang))
      ang   += astep

    newiVTr   = []
    for i in range(nSteps +1):
      newiVTr += [_nextiV, _nextiV+i+1, _nextiV+i+2]
      #newiVTr[len(newiVTr)-1] = newiVTr[1]

    if _ob[stm.SO_field_doRGBAByVert]:
      RGBA0    = _ob[stm.SO_field_fgRGB][0][0:4]
      RGBA1    = _ob[stm.SO_field_fgRGB][1][0:4]
      newRGBA  = spp.scaleRGB(_stim, RGBA0)
      tmpRGBA  = (len(newVert)-1)//2 *spp.scaleRGB(_stim, RGBA1)
      newRGBA  += tmpRGBA
      RGBA0    = _ob[stm.SO_field_fgRGB][0][4:8]
      RGBA1    = _ob[stm.SO_field_fgRGB][1][4:8]
      newRGBA  = spp.scaleRGB(_stim, RGBA0)
      tmpRGBA  = (len(newVert)-1)//2 *spp.scaleRGB(_stim, RGBA1)
      newRGBA2 += tmpRGBA
      
    else:
      tmpRGBA  = _ob[stm.SO_field_fgRGB][0:3] +(_ob[stm.SO_field_alpha],)
      newRGBA  = len(newVert)//2 *spp.scaleRGB(_stim, tmpRGBA)
      tmpRGBA  = _ob[stm.SO_field_fgRGB][3:6] +(_ob[stm.SO_field_alpha],)
      newRGBA2 = len(newVert)//2 *spp.scaleRGB(_stim, tmpRGBA)

  newVert   = spp.rotateTranslate(newVert, rot_deg, pxy)
  newVert   = spp.toInt(newVert)

  hList     = [stm.StimObjType.sector, r,offs,acenter,awidth,astep, mx,my,
               rot_deg, pxy]
  hStr      = spp.getHashStr(hList.__str__())

  return (newVert, newiVTr, newRGBA, newRGBA2, hStr, pxy, rot_deg)