diff --git a/flatland/utils/editor.py b/flatland/utils/editor.py index a09b131eb6e72ac3a4445b65da4d3cbca4b4d6c1..f44deda7dc0d63512ee9926825a987b76409f2b5 100644 --- a/flatland/utils/editor.py +++ b/flatland/utils/editor.py @@ -46,7 +46,7 @@ class JupEditor(object): self.yxBase = array([6, 21]) # pixel offset self.nPixCell = 700 / self.env.rail.width # 35 - self.rcHistory = [] + self.lrcStroke = [] self.iTransLast = -1 self.gRCTrans = array([[-1, 0], [0, 1], [1, 0], [0, -1]]) # NESW in RC @@ -112,19 +112,25 @@ class JupEditor(object): y = event['canvasY'] env = self.env qEvents = self.qEvents - rcHistory = self.rcHistory + lrcStroke = self.lrcStroke bRedrawn = False writableData = None if self.debug and (event["buttons"] > 0 or self.debug_move): - self.log("debug:", len(qEvents), len(rcHistory), event) + self.log("debug:", len(qEvents), len(lrcStroke), event) assert wid == self.wid_img, "wid not same as wid_img" # If the mouse is held down, enqueue an event in our own queue + # The intention was to avoid too many redraws. if event["buttons"] > 0: qEvents.append((time.time(), x, y)) + # Process the events in our queue: + # Draw a black square to indicate a trail + # TODO: infer a vector of moves between these squares to avoid gaps + # Convert the xy position to a cell rc + # Enqueue transitions across cells in another queue if len(qEvents) > 0: tNow = time.time() if tNow - qEvents[0][0] > 0.1: # wait before trying to draw @@ -142,93 +148,39 @@ class JupEditor(object): # Translate and scale from x,y to integer row,col (note order change) rcCell = ((array([y, x]) - self.yxBase) / self.nPixCell).astype(int) - if len(rcHistory) > 1: - rcLast = rcHistory[-1] + # Store the row,col location of the click, if we have entered a new cell + if len(lrcStroke) > 0: + rcLast = lrcStroke[-1] if not np.array_equal(rcLast, rcCell): # only save at transition # print(y, x, rcCell) - rcHistory.append(rcCell) + lrcStroke.append(rcCell) else: - rcHistory.append(rcCell) - - elif len(rcHistory) >= 3: + # This is the first cell in a mouse stroke + lrcStroke.append(rcCell) + + # This elif means we wait until all the mouse events have been processed (black square drawn) + # before trying to draw rails. (We could change this behaviour) + # Equivalent to waiting for mouse button to be lifted (and a mouse event is necessary: + # the mouse may need to be moved) + elif len(lrcStroke) >= 3: # If we have already touched 3 cells # We have a transition into a cell, and out of it. if self.drawMode == "Draw": - bTransition = True + bAddRemove = True elif self.drawMode == "Erase": - bTransition = False - - while len(rcHistory) >= 3: - rc3Cells = array(rcHistory[:3]) # the 3 cells - rcMiddle = rc3Cells[1] # the middle cell which we will update + bAddRemove = False - # Save the original state of the cell - oTransrcMiddle = self.env.rail.get_transitions(rcMiddle) - sTransrcMiddle = self.env.rail.cell_repr(rcMiddle) + # If the first cell in a stroke is empty, add a deadend to cell 0 + if self.env.rail.get_transitions(lrcStroke[0]) == 0: + self.add_rail_2cells(lrcStroke, bAddRemove, iCellToMod=0) - # get the 2 row, col deltas between the 3 cells, eg [-1,0] = North - rc2Trans = np.diff(rc3Cells, axis=0) - - # get the direction index for the 2 transitions - liTrans = [] - for rcTrans in rc2Trans: - # gRCTrans - rcTrans gives an array of vector differences between our rcTrans - # and the 4 directions stored in gRCTrans. - # Where the vector difference is zero, we have a match... - # np.all detects where the whole row,col vector is zero. - # argwhere gives the index of the zero vector, ie the direction index - iTrans = np.argwhere(np.all(self.gRCTrans - rcTrans == 0, axis=1)) - if len(iTrans) > 0: - iTrans = iTrans[0][0] - liTrans.append(iTrans) - - # check that we have two transitions - if len(liTrans) == 2: - # Set the transition - env.rail.set_transition((*rcMiddle, liTrans[0]), liTrans[1], bTransition) - - # Also set the reverse transition - # use the reversed outbound transition for inbound - # and the reversed inbound transition for outbound - env.rail.set_transition((*rcMiddle, mirror(liTrans[1])), mirror(liTrans[0]), bTransition) - - bValid = env.rail.is_cell_valid(rcMiddle) - if not bValid: - # Reset cell transition values - env.rail.grid[tuple(rcMiddle)] = oTransrcMiddle - - self.log(rcMiddle, "Orig:", sTransrcMiddle, "Mod:", self.env.rail.cell_repr(rcMiddle)) - rcHistory.pop(0) # remove the last-but-one + while len(lrcStroke) >= 3: + self.add_rail_3cells(lrcStroke, env, bAddRemove) # If final cell empty, insert deadend: - if len(rcHistory) == 2 and (self.env.rail.get_transitions(rcHistory[1]) == 0): - rc2Cells = array(rcHistory[:2]) # the 2 cells - rcFinal = rc2Cells[1] # the final cell which we will update - - # get the row, col delta between the 2 cells, eg [-1,0] = North - rc2Trans = np.diff(rc2Cells, axis=0) - - # get the direction index for the 2 transitions - liTrans = [] - for rcTrans in rc2Trans: - iTrans = np.argwhere(np.all(self.gRCTrans - rcTrans == 0, axis=1)) - if len(iTrans) > 0: - iTrans = iTrans[0][0] - liTrans.append(iTrans) - - # check that we have one transition - if len(liTrans) == 1: - # Set the transition as a deadend - env.rail.set_transition((*rcFinal, liTrans[0]), mirror(liTrans[0]), bTransition) - - bValid = env.rail.is_cell_valid(rcMiddle) - if not bValid: - # Reset cell transition values - env.rail.grid[tuple(rcMiddle)] = oTransrcMiddle - - self.log(rcMiddle, "Orig:", sTransrcMiddle, "Mod:", self.env.rail.cell_repr(rcMiddle)) - rcHistory.pop(0) # remove the last-but-one + if len(lrcStroke) == 2 and (self.env.rail.get_transitions(lrcStroke[1]) == 0): + self.add_rail_2cells(lrcStroke, bAddRemove, iCellToMod=1) self.redraw() bRedrawn = True @@ -237,7 +189,100 @@ class JupEditor(object): if not bRedrawn and writableData is not None: # This updates the image in the browser to be the new edited version self.wid_img.data = writableData - + + def add_rail_3cells(self, rcCells, bAddRemove=True, bPop=True): + """ + Add transitions for rail spanning three cells. + lrcCells -- list of 3 rc cells + bAddRemove -- whether to add (True) or remove (False) the transition + The transition is added to or removed from the 2nd cell, consistent with + entering from the 1st cell, and exiting into the 3rd. + Both the forward and backward transitions are added, + eg rcCells [(3,4), (2,4), (2,5)] would result in the transitions + N->E and W->S in cell (2,4). + """ + rc3Cells = array(rcCells[:3]) # the 3 cells + rcMiddle = rc3Cells[1] # the middle cell which we will update + bDeadend = np.all(rcCells[0] == rcCells[2]) # deadend means cell 0 == cell 2 + + # Save the original state of the cell + # oTransrcMiddle = self.env.rail.get_transitions(rcMiddle) + # sTransrcMiddle = self.env.rail.cell_repr(rcMiddle) + + # get the 2 row, col deltas between the 3 cells, eg [-1,0] = North + rc2Trans = np.diff(rc3Cells, axis=0) + + # get the direction index for the 2 transitions + liTrans = [] + for rcTrans in rc2Trans: + # gRCTrans - rcTrans gives an array of vector differences between our rcTrans + # and the 4 directions stored in gRCTrans. + # Where the vector difference is zero, we have a match... + # np.all detects where the whole row,col vector is zero. + # argwhere gives the index of the zero vector, ie the direction index + iTrans = np.argwhere(np.all(self.gRCTrans - rcTrans == 0, axis=1)) + if len(iTrans) > 0: + iTrans = iTrans[0][0] + liTrans.append(iTrans) + + # check that we have two transitions + if len(liTrans) == 2: + # Set the transition + # If this transition spans 3 cells, it is not a deadend, so remove any deadends. + # The user will need to resolve any conflicts. + self.env.rail.set_transition((*rcMiddle, liTrans[0]), liTrans[1], bAddRemove, + remove_deadends=not bDeadend) + + # Also set the reverse transition + # use the reversed outbound transition for inbound + # and the reversed inbound transition for outbound + self.env.rail.set_transition((*rcMiddle, mirror(liTrans[1])), + mirror(liTrans[0]), bAddRemove, remove_deadends=not bDeadend) + + # bValid = self.env.rail.is_cell_valid(rcMiddle) + # if not bValid: + # # Reset cell transition values + # self.env.rail.grid[tuple(rcMiddle)] = oTransrcMiddle + + # self.log(rcMiddle, "Orig:", sTransrcMiddle, "Mod:", self.env.rail.cell_repr(rcMiddle)) + if bPop: + rcCells.pop(0) # remove the first cell in the stroke + + def add_rail_2cells(self, lrcCells, bAddRemove=True, iCellToMod=0, bPop=False): + """ + Add transitions for rail between two cells + lrcCells -- list of two rc cells + bAddRemove -- whether to add (True) or remove (False) the transition + iCellToMod -- the index of the cell to modify: either 0 or 1 + """ + rc2Cells = array(lrcCells[:2]) # the 2 cells + rcMod = rc2Cells[iCellToMod] # the cell which we will update + + # get the row, col delta between the 2 cells, eg [-1,0] = North + rc1Trans = np.diff(rc2Cells, axis=0) + + # get the direction index for the transition + liTrans = [] + for rcTrans in rc1Trans: + iTrans = np.argwhere(np.all(self.gRCTrans - rcTrans == 0, axis=1)) + if len(iTrans) > 0: + iTrans = iTrans[0][0] + liTrans.append(iTrans) + + # check that we have one transition + if len(liTrans) == 1: + # Set the transition as a deadend + # The transition is going from cell 0 to cell 1. + if iCellToMod == 0: + # if 0, reverse the transition, we need to be entering cell 0 + self.env.rail.set_transition((*rcMod, mirror(liTrans[0])), liTrans[0], bAddRemove) + else: + # if 1, the transition is entering cell 1 + self.env.rail.set_transition((*rcMod, liTrans[0]), mirror(liTrans[0]), bAddRemove) + + if bPop: + lrcCells.pop(0) + def redraw(self, hide_stdout=True, update=True): # if hide_stdout: