def realtimeArrowMotion(self, event, snap=True): """ Animated arrow that has no semantic meaning, but aids interactive drawing """ cb = self.cb dc = cb.getCanvas() arrow = self.pilotArrow.getArrow(create=False) if (not arrow): print 'arrow motion' return coords = dc.coords(arrow) x, y = cb.getCanvasCoords(event) # Snap Grid if (self.snapGridInfoTuple and self.snapGridInfoTuple[2]): gridSize = self.snapGridInfoTuple[0] x, y = [snapIt(x, x, gridSize), snapIt(y, y, gridSize)] # If still have 2 points, do object Snapping on first point # Don't snap if we have a lock in effect (useful for Named Ports) if (snap and len(coords) == 4 and not self.pilotArrow.getSnapLock()): fromObj = self.pilotArrow.getFromObject() if (fromObj): # Regular entity if (fromObj.hasConnectors()): x0, y0 = fromObj.getMinDistance_Connectors2Fixed(fromObj, x, y) coords = [x0, y0, x, y] dc.coords(*[arrow] + coords) # Look under the mouse pointer for treasures :D itemTuple = cb.getItemUnderCursor(self, event, ignore=arrow) if (not itemTuple): return dc.coords(*[arrow] + coords[:-2] + [x, y]) obj = itemTuple[2] # Snap the final point to the nearest object connector (Avoid snapping to self) if (snap and obj != self.pilotArrow.getFromObject()): # Regular Entity if (obj.hasConnectors()): # Calc distance from before last point to the nearest connector x, y = obj.getMinDistance_Connectors2Fixed(obj, coords[-4], coords[-3]) # Hyperedge! Snap to the center point of the edge elif (isHyperEdge(obj)): x, y = obj.getCenterCoord() dc.coords(*[arrow] + coords[:-2] + [x, y])
def realtimeArrowMotion(self, event, snap = True): """ Animated arrow that has no semantic meaning, but aids interactive drawing """ cb = self.cb dc = cb.getCanvas() arrow = self.pilotArrow.getArrow(create=False) if(not arrow): print 'arrow motion' return coords = dc.coords(arrow) x, y = cb.getCanvasCoords(event) # Snap Grid if(self.snapGridInfoTuple and self.snapGridInfoTuple[2]): gridSize = self.snapGridInfoTuple[0] x, y = [ snapIt(x, x, gridSize), snapIt(y, y, gridSize) ] # If still have 2 points, do object Snapping on first point # Don't snap if we have a lock in effect (useful for Named Ports) if(snap and len(coords) == 4 and not self.pilotArrow.getSnapLock()): fromObj = self.pilotArrow.getFromObject() if(fromObj): # Regular entity if(fromObj.hasConnectors()): x0, y0 = fromObj.getMinDistance_Connectors2Fixed(fromObj, x, y) coords = [x0, y0, x, y] dc.coords(* [arrow] + coords) # Look under the mouse pointer for treasures :D itemTuple = cb.getItemUnderCursor(self, event, ignore=arrow) if(not itemTuple): return dc.coords(* [ arrow ] + coords[:-2] + [x, y]) obj = itemTuple[2] # Snap the final point to the nearest object connector (Avoid snapping to self) if(snap and obj != self.pilotArrow.getFromObject()): # Regular Entity if(obj.hasConnectors()): # Calc distance from before last point to the nearest connector x, y = obj.getMinDistance_Connectors2Fixed(obj, coords[-4], coords[-3]) # Hyperedge! Snap to the center point of the edge elif(isHyperEdge(obj)): x, y = obj.getCenterCoord() dc.coords(* [ arrow ] + coords[:-2] + [x, y])
def dragInMotion(self, event): """ Dragged objects follow mouse motion If snap mode, snaps the top-left of the object. Replaced object.x, object.y with the x,y = object.getCenterCoord() , for a center snap. Do the same to the snapNewEntity method in Utilities. NOTE: Center snap causes problems when saving/loading models because it is applied to new nodes created by loading a model. See ASG.py, addNode() """ cb = self.cb x0, y0 = cb.getLastClickCoord() x1, y1 = cb.getCanvasCoords(event) dx, dy = [x1-x0, y1-y0] selectionSet = cb.getSelectionObjectSet() # Many entities, try to make dragging more responsive (faster) if(len(selectionSet) > 1): dragMotion(self, [x0, y0], [x1, y1], selectionSet) elif(self.snapGridInfoTuple): gridSize, snapArrowNode, snapControlPoints = self.snapGridInfoTuple # Apply Snap only to entities, exclude arrows if(not snapArrowNode): for object in selectionSet: if(isEntityNode(object)): #x,y = object.getCenterCoord() x1 = snapIt(object.x + dx, x1, gridSize) y1 = snapIt(object.y + dy, y1, gridSize) cb.setLastClickCoords([x1, y1]) break # Snap Entities or Arrows else: for object in selectionSet: #x,y = object.getCenterCoord() x1 = snapIt(object.x + dx, x1, gridSize) y1 = snapIt(object.y + dy, y1, gridSize) cb.setLastClickCoords([x1, y1]) break dragMotion(self, [x0, y0], [x1, y1], selectionSet) optimizeConnectionPorts(self)
def dragInMotion(self, event): """ Dragged objects follow mouse motion If snap mode, snaps the top-left of the object. Replaced object.x, object.y with the x,y = object.getCenterCoord() , for a center snap. Do the same to the snapNewEntity method in Utilities. NOTE: Center snap causes problems when saving/loading models because it is applied to new nodes created by loading a model. See ASG.py, addNode() """ cb = self.cb x0, y0 = cb.getLastClickCoord() x1, y1 = cb.getCanvasCoords(event) dx, dy = [x1 - x0, y1 - y0] selectionSet = cb.getSelectionObjectSet() # Many entities, try to make dragging more responsive (faster) if (len(selectionSet) > 1): dragMotion(self, [x0, y0], [x1, y1], selectionSet) elif (self.snapGridInfoTuple): gridSize, snapArrowNode, snapControlPoints = self.snapGridInfoTuple # Apply Snap only to entities, exclude arrows if (not snapArrowNode): for object in selectionSet: if (isEntityNode(object)): #x,y = object.getCenterCoord() x1 = snapIt(object.x + dx, x1, gridSize) y1 = snapIt(object.y + dy, y1, gridSize) cb.setLastClickCoords([x1, y1]) break # Snap Entities or Arrows else: for object in selectionSet: #x,y = object.getCenterCoord() x1 = snapIt(object.x + dx, x1, gridSize) y1 = snapIt(object.y + dy, y1, gridSize) cb.setLastClickCoords([x1, y1]) break dragMotion(self, [x0, y0], [x1, y1], selectionSet) optimizeConnectionPorts(self)
def onGFButtonMotion(self, gf, event): """translate the selected GFs and keep track of the total translation since the handler started""" # Using the snap grid if( self.editor.snapGridInfoTuple and len( self.gfList ) > 0 ): gridSize = self.editor.snapGridInfoTuple[0] / self.zoom x0 = self.previousX y0 = self.previousY ex = event.x / self.zoom ey = event.y / self.zoom # Snapping to the top left corner of the first object encountered x,y = self.gfList[0].getCoords()[:2] x1 = snapIt( x + ex - x0, ex, gridSize) y1 = snapIt( y + ey - y0, ey, gridSize) self.previousX = x1 self.previousY = y1 dx = x1 - x0 dy = y1 - y0 self.totalTranslationX += dx self.totalTranslationY += dy for g in self.gfList: g.translate(dx, dy) # No snap grid else: ex = event.x / self.zoom ey = event.y / self.zoom dx = ex - self.previousX dy = ey - self.previousY for g in self.gfList: g.translate(dx, dy) self.totalTranslationX += dx self.totalTranslationY += dy self.previousX = ex self.previousY = ey
def onHandleButtonMotion(self, handleNumber, event): newXY = self.poly.getCoords() # Using the snap grid if( self.editor.snapGridInfoTuple ): gridSize = self.editor.snapGridInfoTuple[0] x0 = self.previousHX y0 = self.previousHY # Snapping to the top left corner of the first object encountered x = newXY[handleNumber * 2] y = newXY[handleNumber * 2 + 1] x1 = snapIt( x + event.x - x0,event.x,gridSize) y1 = snapIt( y + event.y - y0,event.y,gridSize) dx = x1 - x0 dy = y1 - y0 newXY[handleNumber * 2] += dx/self.zoom newXY[handleNumber * 2 + 1] += dy/self.zoom self.poly.setCoords(newXY) self.handles[handleNumber].translate(dx, dy) self.previousHX = x1 self.previousHY = y1 # No snaps else: dx = (event.x - self.previousHX) dy = (event.y - self.previousHY) newXY[handleNumber * 2] += dx/self.zoom newXY[handleNumber * 2 + 1] += dy/self.zoom self.poly.setCoords(newXY) self.handles[handleNumber].translate(dx, dy) self.previousHX = event.x self.previousHY = event.y
def dragOps(self, x0, y0, x1, y1, mouseMove=True): """ Drag operation on a control point knob """ deltaX, deltaY = (x1 - x0, y1 - y0) knob, color, type, index = self.activeControlPointTuple knobTag = self.dc.gettags(knob) if (not knobTag): raise Exception, "No tag, no game!" # Move the "real" arrow. # Step 1: If the knob is the center object of the link if (knobTag[0] == self.INTERMEIDATE_OBJ_TAG): obj = self.intermediateObject # Move the label/drawing attached to the node if (self.moveLabelDrawingMode): if (obj.centerObject): obj.centerObject.Move(deltaX, deltaY, 0) # Save the offest as a graphical layout constraint for model save/load if (not obj.layConstraints.has_key('Label Offset')): obj.layConstraints['Label Offset'] = [deltaX, deltaY] else: dx, dy = obj.layConstraints['Label Offset'] obj.layConstraints['Label Offset'] = [ dx + deltaX, dy + deltaY ] return # Move the actual node & dependencies using the obj.Move method else: # Snap grid: snap the intermediate object if (self.snapGridInfoTuple and mouseMove): gridSize, snapArrowNode, snapControlPoints = self.snapGridInfoTuple if (snapControlPoints): cpX, cpY = obj.getCenterCoord() x1 = snapIt(cpX + deltaX, x1, gridSize) y1 = snapIt(cpY + deltaY, y1, gridSize) self.lastMousePosition = [x1, y1] deltaX, deltaY = (x1 - x0, y1 - y0) obj.setCenterSelect() obj.Move(deltaX, deltaY) # Step 2: If the knob is a regular control point, use the hacky method elif (knobTag[0][:3] == "Seg"): controlPointCoords = self.dc.coords(knob) cpX = controlPointCoords[0] + self.CONTROL_POINT_SIZE cpY = controlPointCoords[1] + self.CONTROL_POINT_SIZE # Snap grid: snap the dragged control points if (self.snapGridInfoTuple and mouseMove): gridSize, snapArrowNode, snapControlPoints = self.snapGridInfoTuple if (snapControlPoints): x1 = snapIt(cpX + deltaX, x1, gridSize) y1 = snapIt(cpY + deltaY, y1, gridSize) self.lastMousePosition = [x1, y1] deltaX, deltaY = (x1 - x0, y1 - y0) # Segment 1 coordinates seg1Coords = self.dc.coords(self.itemsTuple[0]) # Segment 1 length minus the starting point and the intermediate object seg1Len = len(seg1Coords[2:-2]) # Segment 1 if (knobTag[0][-1:] == "1"): # Failure case that will never happen... really... it won't... if (index * 2 > seg1Len): raise Exception, "Noooooo! Don't do it Bun-Bun!" seg1Coords[index * 2 + 2] += deltaX seg1Coords[index * 2 + 3] += deltaY self.dc.coords(*[self.itemsTuple[0]] + seg1Coords) # Segment 2 elif (knobTag[0][-1:] == "2"): seg2Coords = self.dc.coords(self.itemsTuple[1]) # Segment 2 coordinates minus the starting point and the intermediate object seg2Len = len(seg2Coords[2:-2]) normalizedIndex = index * 2 - seg1Len - 2 # Subtract seg1 & inter. object # Failure cases that should *never* happen if (normalizedIndex > seg2Len): raise Exception, "Noooooo! Don't do it Bun-Bun." elif (normalizedIndex < 0): raise Exception, "Beware the kitten..." # Flip the index because of some really freaky reversal normalizedIndex = seg2Len - normalizedIndex seg2Coords[normalizedIndex] += deltaX seg2Coords[normalizedIndex + 1] += deltaY self.dc.coords(*[self.itemsTuple[1]] + seg2Coords) # It will always be segment 1 or 2... or I'm gonna get embarassed here... else: raise Exception, "Game called on account of naked chick (see www.sluggy.com)." # Since all knobs are given a tag, this won't happen... else: raise Exception, "No tag, no game!" # Move the Control Point Knob controlPointCoords = self.dc.coords(knob) self.dc.move(knob, deltaX, deltaY) # Move the colorful arrows... coords = self.dc.coords(self.innerArrowItem) coords[(index + 1) * 2] += deltaX coords[(index + 1) * 2 + 1] += deltaY self.dc.coords(*[self.innerArrowItem] + coords) self.dc.coords(*[self.outerArrowItem] + coords) # Make sure connections are nice (snap the end points to closest connector) self.optimalConnectionCheck()
def dragOps(self, x0,y0, x1,y1, mouseMove = True ): """ Drag operation on a control point knob """ deltaX, deltaY = (x1-x0,y1-y0) knob, color, type, index = self.activeControlPointTuple knobTag = self.dc.gettags( knob ) if( not knobTag ): raise Exception, "No tag, no game!" # Move the "real" arrow. # Step 1: If the knob is the center object of the link if( knobTag[0] == self.INTERMEIDATE_OBJ_TAG ): obj = self.intermediateObject # Move the label/drawing attached to the node if( self.moveLabelDrawingMode ): if( obj.centerObject ): obj.centerObject.Move(deltaX, deltaY, 0) # Save the offest as a graphical layout constraint for model save/load if( not obj.layConstraints.has_key( 'Label Offset' ) ): obj.layConstraints['Label Offset'] = [deltaX,deltaY] else: dx,dy = obj.layConstraints['Label Offset'] obj.layConstraints['Label Offset'] = [dx+deltaX,dy+deltaY] return # Move the actual node & dependencies using the obj.Move method else: # Snap grid: snap the intermediate object if( self.snapGridInfoTuple and mouseMove ): gridSize, snapArrowNode, snapControlPoints = self.snapGridInfoTuple if( snapControlPoints ): cpX,cpY = obj.getCenterCoord() x1 = snapIt( cpX+deltaX,x1,gridSize) y1 = snapIt( cpY+deltaY,y1,gridSize) self.lastMousePosition = [x1,y1] deltaX, deltaY = (x1-x0,y1-y0) obj.setCenterSelect() obj.Move( deltaX, deltaY ) # Step 2: If the knob is a regular control point, use the hacky method elif( knobTag[0][:3] == "Seg" ): controlPointCoords = self.dc.coords(knob) cpX = controlPointCoords[0] + self.CONTROL_POINT_SIZE cpY = controlPointCoords[1] + self.CONTROL_POINT_SIZE # Snap grid: snap the dragged control points if( self.snapGridInfoTuple and mouseMove ): gridSize, snapArrowNode, snapControlPoints = self.snapGridInfoTuple if( snapControlPoints ): x1 = snapIt( cpX+deltaX,x1,gridSize) y1 = snapIt( cpY+deltaY,y1,gridSize) self.lastMousePosition = [x1,y1] deltaX, deltaY = (x1-x0,y1-y0) # Segment 1 coordinates seg1Coords = self.dc.coords( self.itemsTuple[0] ) # Segment 1 length minus the starting point and the intermediate object seg1Len = len( seg1Coords[2:-2] ) # Segment 1 if( knobTag[0][-1:] == "1" ): # Failure case that will never happen... really... it won't... if( index*2 > seg1Len ): raise Exception, "Noooooo! Don't do it Bun-Bun!" seg1Coords[index*2+2] += deltaX seg1Coords[index*2+3] += deltaY self.dc.coords( * [self.itemsTuple[0]] + seg1Coords ) # Segment 2 elif( knobTag[0][-1:] == "2" ): seg2Coords = self.dc.coords( self.itemsTuple[1] ) # Segment 2 coordinates minus the starting point and the intermediate object seg2Len = len( seg2Coords[2:-2] ) normalizedIndex = index * 2 - seg1Len - 2 # Subtract seg1 & inter. object # Failure cases that should *never* happen if( normalizedIndex > seg2Len ): raise Exception, "Noooooo! Don't do it Bun-Bun." elif( normalizedIndex < 0 ): raise Exception, "Beware the kitten..." # Flip the index because of some really freaky reversal normalizedIndex = seg2Len - normalizedIndex seg2Coords[normalizedIndex] += deltaX seg2Coords[normalizedIndex+1] += deltaY self.dc.coords( * [self.itemsTuple[1]] + seg2Coords ) # It will always be segment 1 or 2... or I'm gonna get embarassed here... else: raise Exception, "Game called on account of naked chick (see www.sluggy.com)."
def dropArrowPoints(self, event, snap = True, filterLinkTypeList=None): """ Drops intermediate points in the arrow or connects the arrow to the final object. If connection is not possible due to the final object not having connectors, the arrow will be rolled back 1 connection point, or destroyed """ cb = self.cb # User-interface callback object dc = cb.getCanvas() x, y = cb.getCanvasCoords(event) # Event coordinates arrow = self.pilotArrow.getArrow() coords = dc.coords(arrow) # Find the closest item to the arrow itemTuple = cb.getItemUnderCursor(self, event, ignore=arrow, allTags=True) if(itemTuple and snap): itemHandler, tags, obj = itemTuple # Embedded model ---> WARNING: Not tested, original AToM3 code if(len(tags) >= 2 and tags[1][:4] == 'ASG_'): self.toClass = tags[0] # Open to select the entity inside the model ATOM3TypeDialog(self, obj.semanticObject, ATOM3TypeDialog.OPEN, (None, self.setConnectMode)) # Check if we have both sides... if self.sem_objFrom and self.sem_objTo: # we have both sides... drawConnection(self) # Object elif(tags[0][:3] == 'Obj'): # Get the actual connector point that we are going to connect to # coords[-4],coords[-3] are the before last point, so the min distance # that point to the connector yields the optimal final point. # Some objects may not have connectors, in this case gracefully rollback # the arrow if(obj.hasConnectors()): coords[-2], coords[-1] = obj.getMinDistance_Connectors2Fixed(obj, coords[-4], coords[-3]) if(obj.hasNamedPorts() and self.showNamedPortMessage): name = obj.guesstimateNamedConnAtPoint(coords[-2], coords[-1]) if(name): text = 'Arrow ended on named port: ' \ + name \ + '\n\nThis message is shown only when you end an arrow' \ + ' directly on a named port' \ + '\n\nIf you choose the 2nd option and want to see these '\ + 'messages again, restart AToM3' dialog = Dialog.Dialog(None, {'title': 'Lock Current Named Port', 'text': text, 'bitmap': '', 'default': 0, 'strings': (name, 'Rollback arrow', 'Never show this message')}) if(dialog.num == 1): return if(dialog.num == 2): self.showNamedPortMessage = False # Edge just happens to have a connector elif(obj.hasCenterObjectConnector()): coords[-2], coords[-1] = obj.getCenterCoord() # Hyperedge! This means that links can connect to links elif(isHyperEdge(obj)): coords[-2], coords[-1] = obj.getCenterCoord() else: # Failure! The object had no connectors. Oooops. if(self.pilotArrow.rollbackArrow([x, y])): # self.UI_scope.event("Reset") self.UI_scope("Reset", None) return # Get the semantic object self.sem_objTo = obj.semanticObject self.sem_objFrom = self.pilotArrow.getFromObject().semanticObject self.toClass = tags[0] self.fromClass = self.pilotArrow.getFromTag() # Arrow is connecting the object to itself with just 2 points! BAD if(self.sem_objTo == self.sem_objFrom and len(coords) <= 4): pass # Draw the connection, turn off simpleConnect, since using intermediate # points else: smooth = self.pilotArrow.getSmoothness() self.inter_connect_points = coords drawConnection(self, smooth, simpleConnect = False, filterLinkTypeList=filterLinkTypeList) # Unknown something... else: raise Exception, tags # Empty variables self.fromClass = None self.toClass = None self.sem_objFrom = None self.sem_objTo = None # Model changed! modelChange(self) # Pilot arrow is no longer needed, GOODBYE! self.pilotArrow.removeArrow(False) self.UI_scope("<Arrow Created>", None) # Tell UI we succeeded! # Nope, keep adding intermediate points else: # Snap Grid if(self.snapGridInfoTuple and self.snapGridInfoTuple[2]): gridSize = self.snapGridInfoTuple[0] coords[-2:] = [ snapIt(x, x, gridSize), snapIt(y, y, gridSize) ] # No grid snapping else: coords[-2:] = [ x, y ] # Apply coords & add a duplicate point dc.coords(* [arrow] + coords + coords[-2:])
def dropArrowPoints(self, event, snap=True, filterLinkTypeList=None): """ Drops intermediate points in the arrow or connects the arrow to the final object. If connection is not possible due to the final object not having connectors, the arrow will be rolled back 1 connection point, or destroyed """ cb = self.cb # User-interface callback object dc = cb.getCanvas() x, y = cb.getCanvasCoords(event) # Event coordinates arrow = self.pilotArrow.getArrow() coords = dc.coords(arrow) # Find the closest item to the arrow itemTuple = cb.getItemUnderCursor(self, event, ignore=arrow, allTags=True) if (itemTuple and snap): itemHandler, tags, obj = itemTuple # Embedded model ---> WARNING: Not tested, original AToM3 code if (len(tags) >= 2 and tags[1][:4] == 'ASG_'): self.toClass = tags[0] # Open to select the entity inside the model ATOM3TypeDialog(self, obj.semanticObject, ATOM3TypeDialog.OPEN, (None, self.setConnectMode)) # Check if we have both sides... if self.sem_objFrom and self.sem_objTo: # we have both sides... drawConnection(self) # Object elif (tags[0][:3] == 'Obj'): # Get the actual connector point that we are going to connect to # coords[-4],coords[-3] are the before last point, so the min distance # that point to the connector yields the optimal final point. # Some objects may not have connectors, in this case gracefully rollback # the arrow if (obj.hasConnectors()): coords[-2], coords[-1] = obj.getMinDistance_Connectors2Fixed( obj, coords[-4], coords[-3]) if (obj.hasNamedPorts() and self.showNamedPortMessage): name = obj.guesstimateNamedConnAtPoint( coords[-2], coords[-1]) if (name): text = 'Arrow ended on named port: ' \ + name \ + '\n\nThis message is shown only when you end an arrow' \ + ' directly on a named port' \ + '\n\nIf you choose the 2nd option and want to see these '\ + 'messages again, restart AToM3' dialog = Dialog.Dialog( None, { 'title': 'Lock Current Named Port', 'text': text, 'bitmap': '', 'default': 0, 'strings': (name, 'Rollback arrow', 'Never show this message') }) if (dialog.num == 1): return if (dialog.num == 2): self.showNamedPortMessage = False # Edge just happens to have a connector elif (obj.hasCenterObjectConnector()): coords[-2], coords[-1] = obj.getCenterCoord() # Hyperedge! This means that links can connect to links elif (isHyperEdge(obj)): coords[-2], coords[-1] = obj.getCenterCoord() else: # Failure! The object had no connectors. Oooops. if (self.pilotArrow.rollbackArrow([x, y])): # self.UI_scope.event("Reset") self.UI_scope("Reset", None) return # Get the semantic object self.sem_objTo = obj.semanticObject self.sem_objFrom = self.pilotArrow.getFromObject().semanticObject self.toClass = tags[0] self.fromClass = self.pilotArrow.getFromTag() # Arrow is connecting the object to itself with just 2 points! BAD if (self.sem_objTo == self.sem_objFrom and len(coords) <= 4): pass # Draw the connection, turn off simpleConnect, since using intermediate # points else: smooth = self.pilotArrow.getSmoothness() self.inter_connect_points = coords drawConnection(self, smooth, simpleConnect=False, filterLinkTypeList=filterLinkTypeList) # Unknown something... else: raise Exception, tags # Empty variables self.fromClass = None self.toClass = None self.sem_objFrom = None self.sem_objTo = None # Model changed! modelChange(self) # Pilot arrow is no longer needed, GOODBYE! self.pilotArrow.removeArrow(False) self.UI_scope("<Arrow Created>", None) # Tell UI we succeeded! # Nope, keep adding intermediate points else: # Snap Grid if (self.snapGridInfoTuple and self.snapGridInfoTuple[2]): gridSize = self.snapGridInfoTuple[0] coords[-2:] = [snapIt(x, x, gridSize), snapIt(y, y, gridSize)] # No grid snapping else: coords[-2:] = [x, y] # Apply coords & add a duplicate point dc.coords(*[arrow] + coords + coords[-2:])